Operations 12 min read

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.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
How to Build a Simple Netstat Tool in Go: From Linux Internals to Code

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 0

Converting 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-l8090

Format 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 0

Each 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 length

Timer_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 owner

Simple 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))
}
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GonetworkLinuxSocketnetstatprocfs
MaGe Linux Operations
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.