How to Build a Simple Netstat Tool in Go: From Linux Internals to Code
This article explains the inner workings of the Linux netstat command, walks through reading /proc/net/tcp and related files, demonstrates how to map sockets to processes, and provides a complete Go implementation of a lightweight netstat utility with code examples and detailed parsing logic.
Netstat Working Principle
The netstat command is used on Linux to view network connections, routing tables, and socket statistics, e.g., netstat -ntlp | grep 8080 shows processes listening on port 8080.
Netstat works by:
Reading /proc/net/tcp and /proc/net/tcp6 to obtain socket local/remote addresses, ports, state, inode, etc.
Scanning all /proc/[pid]/fd directories to map socket inodes to process IDs.
Reading /proc/[pid]/cmdline to get the command line and arguments of each process.
Combining the above information to associate sockets with their owning processes.
Verification example using nc to listen on port 8090: nc -l 8090 Find the PID of the nc process and list its open file descriptors:
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]The entry socket:[2226056] corresponds to the socket created by nc on port 8090; 2226056 is its inode.
Using the inode, we locate the corresponding line in /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 0Converting the hex port 1F9A to decimal yields 8090.
We then read /proc/25556/cmdline to get the process command:
vagrant@vagrant:/proc/25556/fd$ cat /proc/25556/cmdline
nc-l8090Format of /proc/net/tcp
The file lists listening TCP sockets followed by established ones. Example of the first five lines:
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 0Each column is explained in three parts. First part (address and state):
46: 010310AC:9C4C 030310AC:1770 01
| | | | |--> connection state (hex)
| | | |------> remote TCP port (hex)
| | |------> remote IPv4 address (hex)
| |------> local TCP port (hex)
|------> local IPv4 address (hex)
|--> entry number (starting at 0)Connection state values are defined in 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! */
};Second part (queues and timers):
00000150:00000000 01:00000019 00000000
| | | |--> number of unrecovered RTO timeouts
| | |------> jiffies until timer expires
| |----------> timer_active (see below)
|--------> receive-queue length (or completed connection queue length if LISTEN)
|--> transmit-queue lengthTimer_active values:
0 no timer is pending
1 retransmit-timer is pending
2 another timer (e.g., delayed ack or keepalive) is pending
3 socket in TIME_WAIT state
4 zero window probe timer is pending
Third part (additional fields):
1000 0 54165785 4 cd1e6040 25 4 27 3 -1
| | | | | | | |--> slow start size threshold (or -1 if not set)
| | | | | | |------> sending congestion window
| | | | | |----------> (ack.quick<<1) | ack.pingpong
| | | | |--------------> predicted tick of soft clock (delayed ACK data)
| | | |--------------------> retransmit timeout
| | |----------------------------> socket memory location
| |---------------------------------> socket reference count
|------------------------------------------> socket inode number
|----------------------------------------------> unanswered 0-window probes
-----------------------------------------------> UID of socket ownerSimple Go Implementation of Netstat
With the understanding of netstat’s operation and the /proc/net/tcp file format, we can implement a lightweight netstat utility in Go.
Core code (full source available in the go‑netstat project):
// 状态码值
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",
}
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
}
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
localAddr := strings.Split(entryItems[1], ":")
se.srcIP = parseHexBigEndianIPStr(localAddr[0])
port, err := strconv.ParseInt(localAddr[1], 16, 32)
if err != nil {
return nil, err
}
se.srcPort = int(port)
remoteAddr := strings.Split(entryItems[2], ":")
se.dstIP = parseHexBigEndianIPStr(remoteAddr[0])
port, err = strconv.ParseInt(remoteAddr[1], 16, 32)
if err != nil {
return nil, err
}
se.dstPort = int(port)
state, _ := strconv.ParseInt(entryItems[3], 16, 32)
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])
if err != nil {
return nil, err
}
se.uname = systemUsers[entryItems[7]]
se.inode = entryItems[9]
return se, nil
}
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))
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
