说明
估计平时大部分人都是通过netstat
来查看网络状态,但是事实是netstat
已经逐渐被其他的命令替代,很多新的Linux发行版本中很多都不支持了netstat
。以ubuntu 18.04
为例来进行说明:1
2~ netstat
zsh: command not found: netstat
按照difference between netstat and ss in linux?这篇文章的说法,
NOTE
This program is obsolete. Replacement for netstat is ss. Replacement
for netstat -r is ip route. Replacement for netstat -i is ip -s link.
Replacement for netstat -g is ip maddr.
中文含义就是:netstat
已经过时了,netstat
的部分命令已经被ip
这个命令取代了,当然还有更为强大的ss
。ss
命令用来显示处于活动状态的套接字信息。ss命令可以用来获取socket统计信息,它可以显示和netstat类似的内容。但ss的优势在于它能够显示更多更详细的有关TCP和连接状态的信息,而且比netstat
更快速更高效。netstat
的原理显示网络的原理仅仅只是解析/proc/net/tcp
,所以如果服务器的socket连接数量变得非常大,那么通过netstat
执行速度是非常慢。而ss
采用的是通过tcp_diag
的方式来获取网络信息,tcp_diag
通过netlink的方式从内核拿到网络信息,这也是ss
更高效更全面的原因。
下图就展示了ss
和nestat
在监控上面的区别。
ss
是获取的socket
的信息,而netstat
是通过解析/proc/net/
下面的文件来获取信息包括Sockets
,TCP/UDP
,IP
,Ethernet
信息。
netstat
和ss
的效率的对比,找同一台机器执行:1
2
3
4
5
6
7
8
9
10time ss
........
real 0m0.016s
user 0m0.001s
sys 0m0.001s
--------------------------------
time netstat
real 0m0.198s
user 0m0.009s
sys 0m0.011s
ss
明显比netstat
更加高效.
netstat简介
netstat
是在net-tools
工具包下面的一个工具集,net-tools提供了一份net-tools
的源码,我们通过net-tools
来看看netstat
的实现原理。
netstat源代码调试
下载net-tools
之后,导入到Clion
中,创建CMakeLists.txt
文件,内容如下:1
2
3
4
5
6
7cmake_minimum_required(VERSION 3.13)
project(test C)
set(BUILD_DIR .)
#add_executable()
add_custom_target(netstat command -c ${BUILD_DIR})
修改根目录下的Makefile
中的59行的编译配置为:1
CFLAGS ?= -O0 -g3
按照如上图设置自己的编译选项
以上就是搭建netstat
的源代码调试过程。
tcp show
在netstat不需要任何参数的情况,程序首先会运行到2317行的tcp_info()
1
2
3
4
5
6
7
8
9
10
11
12
13
if (!flag_arg || flag_tcp) {
i = tcp_info();
if (i)
return (i);
}
if (!flag_arg || flag_sctp) {
i = sctp_info();
if (i)
return (i);
}
.........
跟踪进入到tcp_info()
:1
2
3
4
5static int tcp_info(void)
{
INFO_GUTS6(_PATH_PROCNET_TCP, _PATH_PROCNET_TCP6, "AF INET (tcp)",
tcp_do_one, "tcp", "tcp6");
}
参数的情况如下:
- _PATH_PROCNET_TCP,在
lib/pathnames.h
中定义,是#define _PATH_PROCNET_TCP "/proc/net/tcp"
- _PATH_PROCNET_TCP6, 在
lib/pathnames.h
中定义, 是#define _PATH_PROCNET_TCP6 "/proc/net/tcp6"
tcp_do_one
,函数指针,位于1100行,部分代码如下:1
2
3
4
5
6
7
8
9
10static void tcp_do_one(int lnr, const char *line, const char *prot)
{
unsigned long rxq, txq, time_len, retr, inode;
int num, local_port, rem_port, d, state, uid, timer_run, timeout;
char rem_addr[128], local_addr[128], timers[64];
const struct aftype *ap;
struct sockaddr_storage localsas, remsas;
struct sockaddr_in *localaddr = (struct sockaddr_in *)&localsas;
struct sockaddr_in *remaddr = (struct sockaddr_in *)&remsas;
......tcp_do_one()
就是用来解析/proc/net/tcp
和/proc/net/tcp6
每一行的含义的,关于/proc/net/tcp
的每一行的含义可以参考之前写过的osquery源码解读之分析process_open_socket中的扩展章节。
INFO_GUTS6
1 |
|
INFO_GUTS6
采用了#define
的方式进行定义,最终根据是flag_inet
(IPv4)或者flag_inet6
(IPv6)的选项分别调用不同的函数,我们以INFO_GUTS1(file,name,proc,prot4)
进一步分析。
INFO_GUTS1
1 |
|
rocinfo = proc_fopen((file))
获取/proc/net/tcp
的文件句柄fgets(buffer, sizeof(buffer), procinfo)
解析文件内容并将每一行的内容存储在buffer中(proc)(lnr++, buffer,prot)
,利用(proc)
函数解析buffer。(proc)
就是前面说明的tcp_do_one()
函数
tcp_do_one
以" 14: 020110AC:B498 CF0DE1B9:4362 06 00000000:00000000 03:000001B2 00000000 0 0 0 3 0000000000000000
这一行为例来说明tcp_do_one()
函数的执行过程。
由于分析是Ipv4
,所以会跳过#if HAVE_AFINET6
这段代码。之后执行:1
2
3
4
5
6
7
8num = sscanf(line,
"%d: %64[0-9A-Fa-f]:%X %64[0-9A-Fa-f]:%X %X %lX:%lX %X:%lX %lX %d %d %lu %*s\n",
&d, local_addr, &local_port, rem_addr, &rem_port, &state,
&txq, &rxq, &timer_run, &time_len, &retr, &uid, &timeout, &inode);
if (num < 11) {
fprintf(stderr, _("warning, got bogus tcp line.\n"));
return;
}
解析数据,并将每一列的数据分别填充到对应的字段上面。分析一下其中的每个字段的定义:1
2
3
4char rem_addr[128], local_addr[128], timers[64];
struct sockaddr_storage localsas, remsas;
struct sockaddr_in *localaddr = (struct sockaddr_in *)&localsas;
struct sockaddr_in *remaddr = (struct sockaddr_in *)&remsas;
在Linux
中sockaddr_in
和sockaddr_storage
的定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25struct sockaddr {
unsigned short sa_family; // address family, AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
struct sockaddr_storage {
sa_family_t ss_family; // address family
// all this is padding, implementation specific, ignore it:
char __ss_pad1[_SS_PAD1SIZE];
int64_t __ss_align;
char __ss_pad2[_SS_PAD2SIZE];
};
之后代码继续执行:1
2
3
4sscanf(local_addr, "%X", &localaddr->sin_addr.s_addr);
sscanf(rem_addr, "%X", &remaddr->sin_addr.s_addr);
localsas.ss_family = AF_INET;
remsas.ss_family = AF_INET;
将local_addr
使用sscanf(,"%X")
得到对应的十六进制,保存到&localaddr->sin_addr.s_addr
(即in_addr
结构体中的s_addr
)中,同理&remaddr->sin_addr.s_addr
。运行结果如下所示:
addr_do_one
1 | addr_do_one(local_addr, sizeof(local_addr), 22, ap, &localsas, local_port, "tcp"); |
程序继续执行,最终会执行到addr_do_one()
函数,用于解析本地IP地址和端口,以及远程IP地址和端口。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23static void addr_do_one(char *buf, size_t buf_len, size_t short_len, const struct aftype *ap,
const struct sockaddr_storage *addr,
int port, const char *proto
)
{
const char *sport, *saddr;
size_t port_len, addr_len;
saddr = ap->sprint(addr, flag_not & FLAG_NUM_HOST);
sport = get_sname(htons(port), proto, flag_not & FLAG_NUM_PORT);
addr_len = strlen(saddr);
port_len = strlen(sport);
if (!flag_wide && (addr_len + port_len > short_len)) {
/* Assume port name is short */
port_len = netmin(port_len, short_len - 4);
addr_len = short_len - port_len;
strncpy(buf, saddr, addr_len);
buf[addr_len] = '\0';
strcat(buf, ":");
strncat(buf, sport, port_len);
} else
snprintf(buf, buf_len, "%s:%s", saddr, sport);
}
saddr = ap->sprint(addr, flag_not & FLAG_NUM_HOST);
这个表示是否需要将addr
转换为域名的形式。由于addr
值是127.0.0.1
,转换之后得到的就是localhost
,其中FLAG_NUM_HOST
的就等价于--numeric-hosts
的选项。sport = get_sname(htons(port), proto, flag_not & FLAG_NUM_PORT);
,port
无法无法转换,其中的FLAG_NUM_PORT
就等价于--numeric-ports
这个选项。!flag_wide && (addr_len + port_len > short_len
这个代码的含义是判断是否需要对IP
和PORT
进行截断。其中flag_wide
的等同于-W, --wide don't truncate IP addresses
。而short_len
长度是22.snprintf(buf, buf_len, "%s:%s", saddr, sport);
,将IP:PORT
赋值给buf
.
output
最终程序执行1
2printf("%-4s %6ld %6ld %-*s %-*s %-11s",
prot, rxq, txq, (int)netmax(23,strlen(local_addr)), local_addr, (int)netmax(23,strlen(rem_addr)), rem_addr, _(tcp_state[state]));
按照制定的格式解析,输出结果
finish_this_one
最终程序会执行finish_this_one(uid,inode,timers);
.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20static void finish_this_one(int uid, unsigned long inode, const char *timers)
{
struct passwd *pw;
if (flag_exp > 1) {
if (!(flag_not & FLAG_NUM_USER) && ((pw = getpwuid(uid)) != NULL))
printf(" %-10s ", pw->pw_name);
else
printf(" %-10d ", uid);
printf("%-10lu",inode);
}
if (flag_prg)
printf(" %-" PROGNAME_WIDTHs "s",prg_cache_get(inode));
if (flag_selinux)
printf(" %-" SELINUX_WIDTHs "s",prg_cache_get_con(inode));
if (flag_opt)
printf(" %s", timers);
putchar('\n');
}
flag_exp
等同于-e
的参数。-e, --extend display other/more information
.举例如下:1
2
3
4
5
6
7netstat -e
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode
tcp 0 0 localhost:6379 172.16.1.200:46702 ESTABLISHED redis 437788048
netstat
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 localhost:6379 172.16.1.200:46702 ESTABLISHED发现使用
-e
参数会多显示User
和Inode
号码。而在本例中还可以如果用户名不存在,则显示uid
getpwuidflag_prg
等同于-p, --programs display PID/Program name for sockets
.举例如下:1
2
3
4
5
6
7netstat -pe
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode PID/Program name
tcp 0 0 localhost:6379 172.16.1.200:34062 ESTABLISHED redis 437672000 6017/redis-server *
netstat -e
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode
tcp 0 0 localhost:6379 172.16.1.200:46702 ESTABLISHED redis 437788048可以看到是通过
prg_cache_get(inode)
,inode来找到对应的PID和进程信息;flag_selinux
等同于-Z, --context display SELinux security context for sockets
prg_cache_get
对于上面的通过inode
找到对应进程的方法非常的好奇,于是去追踪prg_cache_get()
函数的实现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static struct prg_node {
struct prg_node *next;
unsigned long inode;
char name[PROGNAME_WIDTH];
char scon[SELINUX_WIDTH];
} *prg_hash[PRG_HASH_SIZE];
static const char *prg_cache_get(unsigned long inode)
{
unsigned hi = PRG_HASHIT(inode);
struct prg_node *pn;
for (pn = prg_hash[hi]; pn; pn = pn->next)
if (pn->inode == inode)
return (pn->name);
return ("-");
}
在prg_hash
中存储了所有的inode编号与program
的对应关系,所以当给定一个inode编号时就能够找到对应的程序名称。那么prg_hash
又是如何初始化的呢?
prg_cache_load
我们使用debug模式,加入-p
的运行参数:
程序会运行到2289行的prg_cache_load(); 进入到prg_cache_load()函数中.
由于整个函数的代码较长,拆分来分析.
获取fd
1 |
|
dirproc=opendir(PATH_PROC);errno = 0, direproc = readdir(dirproc)
遍历/proc拿到所有的pidprocfdlen = snprintf(line,sizeof(line),PATH_PROC_X_FD,direproc→d_name);
遍历所有的/proc/pid拿到所有进程的fddirfd = opendir(line);
得到/proc/pid/fd的文件句柄
获取inode
1 | while ((direfd = readdir(dirfd))) { |
memcpy(line + procfdlen - PATH_FD_SUFFl, PATH_FD_SUFF "/",PATH_FD_SUFFl + 1);safe_strncpy(line + procfdlen + 1, direfd->d_name, sizeof(line) - procfdlen - 1);
得到遍历之后的fd信息,比如/proc/pid/fdlnamelen = readlink(line, lname, sizeof(lname) - 1);
得到fd所指向的link,因为通常情况下fd一般都是链接,要么是socket链接要么是pipe链接.如下所示:1
2
3
4
5
6
7
8$ ls -al /proc/1289/fd
total 0
dr-x------ 2 username username 0 May 25 15:45 .
dr-xr-xr-x 9 username username 0 May 25 09:11 ..
lr-x------ 1 username username 64 May 25 16:23 0 -> 'pipe:[365366]'
l-wx------ 1 username username 64 May 25 16:23 1 -> 'pipe:[365367]'
l-wx------ 1 username username 64 May 25 16:23 2 -> 'pipe:[365368]'
lr-x------ 1 username username 64 May 25 16:23 3 -> /proc/uptime通过extract_type_1_socket_inode获取到link中对应的inode编号.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static int extract_type_1_socket_inode(const char lname[], unsigned long * inode_p) {
/* If lname is of the form "socket:[12345]", extract the "12345"
as *inode_p. Otherwise, return -1 as *inode_p.
*/
// 判断长度是否小于 strlen(socket:[)+3
if (strlen(lname) < PRG_SOCKET_PFXl+3) return(-1);
//函数说明:memcmp()用来比较s1 和s2 所指的内存区间前n 个字符。
// 判断lname是否以 socket:[ 开头
if (memcmp(lname, PRG_SOCKET_PFX, PRG_SOCKET_PFXl)) return(-1);
if (lname[strlen(lname)-1] != ']') return(-1); {
char inode_str[strlen(lname + 1)]; /* e.g. "12345" */
const int inode_str_len = strlen(lname) - PRG_SOCKET_PFXl - 1;
char *serr;
// 获取到inode的编号
strncpy(inode_str, lname+PRG_SOCKET_PFXl, inode_str_len);
inode_str[inode_str_len] = '\0';
*inode_p = strtoul(inode_str, &serr, 0);
if (!serr || *serr || *inode_p == ~0)
return(-1);
}获取程序对应的cmdline
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19if (!cmdlp) {
if (procfdlen - PATH_FD_SUFFl + PATH_CMDLINEl >=sizeof(line) - 5)
continue;
safe_strncpy(line + procfdlen - PATH_FD_SUFFl, PATH_CMDLINE,sizeof(line) - procfdlen + PATH_FD_SUFFl);
fd = open(line, O_RDONLY);
if (fd < 0)
continue;
cmdllen = read(fd, cmdlbuf, sizeof(cmdlbuf) - 1);
if (close(fd))
continue;
if (cmdllen == -1)
continue;
if (cmdllen < sizeof(cmdlbuf) - 1)
cmdlbuf[cmdllen]='\0';
if (cmdlbuf[0] == '/' && (cmdlp = strrchr(cmdlbuf, '/')))
cmdlp++;
else
cmdlp = cmdlbuf;
}由于cmdline是可以直接读取的,所以并不需要像读取fd那样借助与readlink()函数,直接通过 read(fd, cmdlbuf, sizeof(cmdlbuf) - 1) 即可读取文件内容.
snprintf(finbuf, sizeof(finbuf), "%s/%s", direproc->d_name, cmdlp);
拼接pid和cmdlp,最终得到的就是类似与 6017/redis-server * 这样的效果- 最终程序调用
prg_cache_add(inode, finbuf, "-");
将解析得到的inode和finbuf 加入到缓存中.
prg_cache_add
1 |
|
unsigned hi = PRG_HASHIT(inode);
使用inode整除211得到作为hash值for (pnp = prg_hash + hi; (pn = *pnp); pnp = &pn->next)
由于prg_hash是一个链表结构,所以通过for循环找到链表的结尾;pn = *pnp;pn->next = NULL;pn->inode = inode;safe_strncpy(pn->name, name, sizeof(pn→name));
为新的inode赋值并将其加入到链表的末尾;
所以prg_node是一个全局变量,是一个链表结果,保存了inode编号与pid/cmdline之间的对应关系;
prg_cache_get
1 | static const char *prg_cache_get(unsigned long inode) |
分析完毕prg_cache_add()之后,看prg_cache_get()就很简单了.
unsigned hi = PRG_HASHIT(inode);
通过inode号拿到hash值for (pn = prg_hash[hi]; pn; pn = pn->next)
遍历prg_hash链表中的每一个节点,如果遍历的inode与目标的inode相符就返回对应的信息.
总结
通过对netstat的一个简单的分析,可以发现其实netstat就是通过遍历/proc目录下的目录或者是文件来获取对应的信息.如果在一个网络进程频繁关闭打开关闭,那么使用netstat显然是相当耗时的.