netstat 使用 go 语言实现是什么操作?本文从 netstat 原理出发详细解读了这一实践。
netstat 工作原理
netstat 命令是 linux 系统中查看网络情况的一个命令。比如我们可以通过netstat \-ntlp | grep 8080查看监听 8080 端口的进程。
netstat 工作原理如下:
- 通过读取/proc/net/tcp 、/proc/net/tcp6 文件,获取 socket 本地地址,本地端口,远程地址,远程端口,状态,inode 等信息
- 接着扫描所有/proc/[pid]/fd 目录下的的 socket 文件描述符,建立 inode 到进程 pid 映射
- 根据 pid 读取/proc/[pid]/cmdline 文件,获取进程命令和启动参数
- 根据 2,3 步骤,即可以获得 1 中对应 socket 的相关进程信息
我们可以做个测试验证整个流程。先使用 nc 命令监听 8090 端口:
- nc -l 8090
找到上面 nc 进程的 pid,查看该进程所有打开的文件描述符:
- vagrant@vagrant:/proc/25556/fd$ ls -alh
- total 0
- dr-x------ 2 vagrant vagrant 0 Nov 18 12:21 .
- dr-xr-xr-x 9 vagrant vagrant 0 Nov 18 12:20 ..
- lrwx------ 1 vagrant vagrant 64 Nov 18 12:21 0 -> /dev/pts/1
- lrwx------ 1 vagrant vagrant 64 Nov 18 12:21 1 -> /dev/pts/1
- lrwx------ 1 vagrant vagrant 64 Nov 18 12:21 2 -> /dev/pts/1
- lrwx------ 1 vagrant vagrant 64 Nov 18 12:21 3 -> socket:[2226056]
上面列出的所有文件描述中,socket:[2226056]为 nc 命令监听 8090 端口所创建的 socket。其中2226056为该 socket 的 inode。
根据该 inode 号,我们查看/proc/net/tcp对应的记录信息,其中1F9A为本地端口号,转换成十进制恰好为 8090:
- vagrant@vagrant:/proc/25556/fd$ cat /proc/net/tcp | grep 2226056
- 1: 00000000:1F9A 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 2226056 1 0000000000000000 100 0 0 10 0
根据进程 id,我们查看进程名称和启动参数:
- vagrant@vagrant:/proc/25556/fd$ cat /proc/25556/cmdline
- nc-l8090
下面我们看下/proc/net/tcp文件格式。
/proc/net/tcp 文件格式
/proc/net/tcp文件首先会列出所有监听状态的 TCP 套接字,然后列出所有已建立的 TCP 套接字。我们通过head \-n 5 /proc/net/tcp命令查看该文件头五行:
- sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
- 0: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 22279 1 0000000000000000 100 0 0 10 0
- 1: 00000000:1FBB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 21205 1 0000000000000000 100 0 0 10 0
- 2: 00000000:26FB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 21203 1 0000000000000000 100 0 0 10 0
- 3: 00000000:26FD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 21201 1 0000000000000000 100 0 0 10 0
每一行各个字段解释说明如下,由于太长分为三部分说明:
第一部分:
- 46: 010310AC:9C4C 030310AC:1770 01
- | | | | | |--> 连接状态,16 进制表示,具体值见下面说明
- | | | | |------> 远程 TCP 端口号,主机字节序,16 进制表示
- | | | |-------------> 远程 IPv4 地址,网络字节序,16 进制表示
- | | |--------------------> 本地 TCP 端口号,主机字节序,16 进制表示
- | |---------------------------> 本地 IPv4 地址,网络字节序,16 进制表示
- |----------------------------------> 条目编号,从 0 开始
上面连接状态所有值如下,具体参见 linux 源码 tcp\_states.h[1]:
- enum {
- TCP_ESTABLISHED = 1,
- TCP_SYN_SENT,
- TCP_SYN_RECV,
- TCP_FIN_WAIT1,
- TCP_FIN_WAIT2,
- TCP_TIME_WAIT,
- TCP_CLOSE,
- TCP_CLOSE_WAIT,
- TCP_LAST_ACK,
- TCP_LISTEN,
- TCP_CLOSING, /* Now a valid state */
- TCP_NEW_SYN_RECV,
- TCP_MAX_STATES /* Leave at the end! */
- };
第二部分:
- 00000150:00000000 01:00000019 00000000
- | | | | |--> number of unrecovered RTO timeouts
- | | | |----------> number of jiffies until timer expires
- | | |----------------> timer_active,具体值见下面说明
- | |----------------------> receive-queue,当状态是 ESTABLISHED,表示接收队列中数据长度;状态是 LISTEN,表示已经完成连接队列的长度
- |-------------------------------> transmit-queue,发送队列中数据长度
timer_active 所有值与说明如下:
- 0 no timer is pending
- 1 retransmit-timer is pending
- 2 another timer (e.g. delayed ack or keepalive) is pending
- 3 this is a socket in TIME_WAIT state. Not all fields will contain data (or even exist)
- 4 zero window probe timer is pending
第三部分:
- 1000 0 54165785 4 cd1e6040 25 4 27 3 -1
- | | | | | | | | | |--> slow start size threshold,
- | | | | | | | | | or -1 if the threshold
- | | | | | | | | | is >= 0xFFFF
- | | | | | | | | |----> sending congestion window
- | | | | | | | |-------> (ack.quick<<1)|ack.pingpong
- | | | | | | |---------> Predicted tick of soft clock
- | | | | | | (delayed ACK control data)
- | | | | | |------------> retransmit timeout
- | | | | |------------------> location of socket in memory
- | | | |-----------------------> socket reference count
- | | |-----------------------------> socket 的 inode 号
- | |----------------------------------> unanswered 0-window probes
- |---------------------------------------------> socket 所属用户的 uid
Go 实现简易版本 netstat 命令
netstat 工作原理和/proc/net/tcp文件结构,我们都已经了解了,现在可以使用据此使用 Go 实现一个简单版本的 netstat 命令。
核心代码如下,完整代码参加 go-netstat[2]:
- // 状态码值
- const (
- TCP_ESTABLISHED = iota + 1
- TCP_SYN_SENT
- TCP_SYN_RECV
- TCP_FIN_WAIT1
- TCP_FIN_WAIT2
- TCP_TIME_WAIT
- TCP_CLOSE
- TCP_CLOSE_WAIT
- TCP_LAST_ACK
- TCP_LISTEN
- TCP_CLOSING
- //TCP_NEW_SYN_RECV
- //TCP_MAX_STATES
- )
- // 状态码
- var states = map[int]string{
- TCP_ESTABLISHED: "ESTABLISHED",
- TCP_SYN_SENT: "SYN_SENT",
- TCP_SYN_RECV: "SYN_RECV",
- TCP_FIN_WAIT1: "FIN_WAIT1",
- TCP_FIN_WAIT2: "FIN_WAIT2",
- TCP_TIME_WAIT: "TIME_WAIT",
- TCP_CLOSE: "CLOSE",
- TCP_CLOSE_WAIT: "CLOSE_WAIT",
- TCP_LAST_ACK: "LAST_ACK",
- TCP_LISTEN: "LISTEN",
- TCP_CLOSING: "CLOSING",
- //TCP_NEW_SYN_RECV: "NEW_SYN_RECV",
- //TCP_MAX_STATES: "MAX_STATES",
- }
- // socketEntry 结构体,用来存储/proc/net/tcp 每一行解析后数据信息
- type socketEntry struct {
- id int
- srcIP net.IP
- srcPort int
- dstIP net.IP
- dstPort int
- state string
- txQueue int
- rxQueue int
- timer int8
- timerDuration time.Duration
- rto time.Duration // retransmission timeout
- uid int
- uname string
- timeout time.Duration
- inode string
- }
- // 解析/proc/net/tcp 行记录
- func parseRawSocketEntry(entry string) (*socketEntry, error) {
- se := &socketEntry{}
- entrys := strings.Split(strings.TrimSpace(entry), " ")
- entryItems := make([]string, 0, 17)
- for _, ent := range entrys {
- if ent == "" {
- continue
- }
- entryItems = append(entryItems, ent)
- }
- id, err := strconv.Atoi(string(entryItems[0][:len(entryItems[0])-1]))
- if err != nil {
- return nil, err
- }
- se.id = id // sockect entry id
- localAddr := strings.Split(entryItems[1], ":") // 本地 ip
- se.srcIP = parseHexBigEndianIPStr(localAddr[0])
- port, err := strconv.ParseInt(localAddr[1], 16, 32) // 本地 port
- if err != nil {
- return nil, err
- }
- se.srcPort = int(port)
- remoteAddr := strings.Split(entryItems[2], ":") // 远程 ip
- se.dstIP = parseHexBigEndianIPStr(remoteAddr[0])
- port, err = strconv.ParseInt(remoteAddr[1], 16, 32) // 远程 port
- if err != nil {
- return nil, err
- }
- se.dstPort = int(port)
- state, _ := strconv.ParseInt(entryItems[3], 16, 32) // socket 状态
- se.state = states[int(state)]
- tcpQueue := strings.Split(entryItems[4], ":")
- tQueue, err := strconv.ParseInt(tcpQueue[0], 16, 32) // 发送队列数据长度
- if err != nil {
- return nil, err
- }
- se.txQueue = int(tQueue)
- sQueue, err := strconv.ParseInt(tcpQueue[1], 16, 32) // 接收队列数据长度
- if err != nil {
- return nil, err
- }
- se.rxQueue = int(sQueue)
- se.uid, err = strconv.Atoi(entryItems[7]) // socket uid
- if err != nil {
- return nil, err
- }
- se.uname = systemUsers[entryItems[7]] // socket user name
- se.inode = entryItems[9] // socket inode
- return se, nil
- }
- // hexIP 是网络字节序/大端法转换成的 16 进制的字符串
- func parseHexBigEndianIPStr(hexIP string) net.IP {
- b := []byte(hexIP)
- for i, j := 1, len(b)-2; i < j; i, j = i+2, j-2 { // 反转字节,转换成小端法
- b[i], b[i-1], b[j], b[j+1] = b[j+1], b[j], b[i-1], b[i]
- }
- l, _ := strconv.ParseInt(string(b), 16, 64)
- return net.IPv4(byte(l>>24), byte(l>>16), byte(l>>8), byte(l))
- }