How Netstat works

The netstat command is used to check the network status in Linux. For example, we can through the netstat – NTLP | grep 8080 to check the process of listen on port 8080.

Netstat works as follows:

  1. By reading /proc/net/tcp and /proc/net/tcp6 files, the socket local address, local port, remote address, remote port, status, inode and other information can be obtained
  2. Then scan all socket file descriptors in /proc/[pid]/fd to establish inode to pid mapping
  3. Read the /proc/[pid]/cmdline file based on pid to obtain the process command and start parameters
  4. According to step 2 and 3, you can obtain process information about the socket corresponding to step 1

We can run a test to validate the process. Run the nc command to listen on port 8090:

nc -l 8090
Copy the code

Find the PID of the above nc process and check all open file descriptors of the process:

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]
Copy the code

In all file descriptions listed above, socket:[2226056] is the socket created for the NC command to listen on port 8090. 2226056 is the inode of the socket.

1F9A is the local port number, which in decimal form is exactly 8090: /proc/net/tcp

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
Copy the code

Based on the process ID, we look at the process name and startup parameters:

vagrant@vagrant:/proc/25556/fd$ cat /proc/25556/cmdline
nc-l8090
Copy the code

Let’s take a look at the /proc/net/tcp file format.

The file format is /proc/net/tcp

The /proc/net/tcp file first lists all TCP sockets listening on state, and then all established TCP sockets. We look at the first five lines of the file with the head -n 5 /proc/net/tcp command:

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
Copy the code

The description of each field in each line is as follows. It is divided into three parts because it is too long:

Part I:

46: 010310 ac: 9 c4c 030310 ac: 1770 01 | | | | | | -- - > connection status, hexadecimal, said see below shows the specific value | | | | | -- -- -- -- -- - > remote TCP port number, host byte order, Hexadecimal said | | | | -- -- -- -- -- -- -- -- -- -- -- -- -- > remote IPv4 addresses, network byte order, hexadecimal said | | | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- > local TCP port number, host byte order, Hexadecimal said | | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - > local IPv4 addresses, network byte order, hexadecimal said | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - > item number, starting from 0Copy the code

All values of the connection status above are as follows, for details, refer to the Linux source code tcp_states.h:

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! * /
};
Copy the code

Part II:

00000150:00000000 01:00000019 00000000 | | | | |--> number of unrecovered RTO timeouts | | | |----------> number of Jiffies until the timer expires | | | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - > timer_active, see below shows the specific value | | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - > the receive queue, ESTABLISHED indicates the length of the data in the receiving queue. State is LISTEN, says it has complete connection queue length | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - > the transmit queue, send the queue length of dataCopy the code

All values and descriptions of timer_active are as follows:

  • 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

Part III:

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 the memory | | | | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- > socket reference count | | | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - > the socket inode number | |----------------------------------> unanswered 0-window probes |---------------------------------------------> Uid of the user to which the socket belongsCopy the code

Go implements a simple version of the netstat command

Now that you know how netstat works and the /proc/net/tcp file structure, you can use it to implement a simple version of the netstat command using Go.

The core code is as follows, and the complete code is included in Go-netstat:

// Status code value
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
)

/ / status code
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",
}

The // socketEntry structure is used to store data information after each parsed row of /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
}

// Parse /proc/net/tcp line records
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]))
	iferr ! =nil {
		return nil, err
	}
	se.id = id                                     // sockect entry id
	localAddr := strings.Split(entryItems[1].":") / / local IP
	se.srcIP = parseHexBigEndianIPStr(localAddr[0])
	port, err := strconv.ParseInt(localAddr[1].16.32) / / local port
	iferr ! =nil {
		return nil, err
	}
	se.srcPort = int(port)

	remoteAddr := strings.Split(entryItems[2].":") / / remote IP
	se.dstIP = parseHexBigEndianIPStr(remoteAddr[0])
	port, err = strconv.ParseInt(remoteAddr[1].16.32) / / the remote port
	iferr ! =nil {
		return nil, err
	}
	se.dstPort = int(port)

	state, _ := strconv.ParseInt(entryItems[3].16.32) / / state of the socket
	se.state = states[int(state)]

	tcpQueue := strings.Split(entryItems[4].":")
	tQueue, err := strconv.ParseInt(tcpQueue[0].16.32) // Send queue data length
	iferr ! =nil {
		return nil, err
	}
	se.txQueue = int(tQueue)
	sQueue, err := strconv.ParseInt(tcpQueue[1].16.32) // Receive queue data length
	iferr ! =nil {
		return nil, err
	}
	se.rxQueue = int(sQueue)

	se.uid, err = strconv.Atoi(entryItems[7]) // socket uid
	iferr ! =nil {
		return nil, err
	}
	se.uname = systemUsers[entryItems[7]] // socket user name
	se.inode = entryItems[9]              // socket inode
	return se, nil
}

// hexIP is a hexadecimal string converted from network byte order/big-endian
func parseHexBigEndianIPStr(hexIP string) net.IP {
	b := []byte(hexIP)
	for i, j := 1.len(b)2 -; i < j; i, j = i+2, j2 - { // Invert the byte and convert it to the small-endian method
		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))
}
Copy the code

reference

  • The original address
  • /proc/net/tcp Official document