netstat and ss alternative to get the list of listening TCP ports

4 min read | by Jordi Prats

On docker containers we might not have neither netstat nor ss installed, yet we can still get the list of listening TCP ports by looking at the /proc filesystem

The files /proc/net/tcp and /proc/net/tcp6 contain information about currently active TCP connections and listening ports. But this information is not in a human friendly format:

$ cat /proc/net/tcp
  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode    
   0: 0100007F:DB73 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 2469436 1 0000000000000000 100 0 0 10 0                   
   1: 017AA8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 50509 1 0000000000000000 100 0 0 10 0                     
   2: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000   101        0 40528 1 0000000000000000 100 0 0 10 0                     
   3: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 44230 1 0000000000000000 100 0 0 10 0                     
   4: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 5452235 1 0000000000000000 100 0 0 10 0                   
   5: 0100007F:DBB7 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 2235992 1 0000000000000000 100 0 0 10 0                   
   6: 00000000:88B7 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 2418076 1 0000000000000000 100 0 0 10 0                   
   7: 00000000:01BB 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 44802 1 0000000000000000 100 0 0 10 0                     
   8: 0100007F:D3A3 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 2234403 1 0000000000000000 100 0 0 10 0                   
   9: 00000000:962B 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 2418083 1 0000000000000000 100 0 0 10 0                   
  10: 0100007F:A0ED 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 2235993 1 0000000000000000 100 0 0 10 0                   
  11: 00000000:9470 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 2418082 1 0000000000000000 100 0 0 10 0                   
  (...)

To be able to extract just the TCP listening ports, the forth column is the state of the connection. If it is "0A" means that it is listening, so we can get the list of listening addresses from this list using awk:

$ awk '$4 == "0A" { print $2 }' /proc/net/tcp /proc/net/tcp6
0100007F:DB73
017AA8C0:0035
3500007F:0035
00000000:0016
0100007F:0277
0100007F:DBB7
00000000:88B7
00000000:01BB
0100007F:D3A3
00000000:962B
0100007F:A0ED
00000000:9470
00000000000000000000000000000000:06B4
00000000000000000000000000000000:0016
00000000000000000000000001000000:0277
00000000000000000000000000000000:9858
00000000000000000000000000000000:01BB

We can then transform the hexadecimal notation to human readable IPs an ports using:

awk '
function hextodec(str,ret,n,i,k,c){
    ret = 0
    n = length(str)
    for (i = 1; i <= n; i++) {
        c = tolower(substr(str, i, 1))
        k = index("123456789abcdef", c)
        ret = ret * 16 + k
    }
    return ret
}
function getIP(str,ret){
    ret=hextodec(substr(str,index(str,":")-2,2));
    for (i=5; i>0; i-=2) {
        ret = ret"."hextodec(substr(str,i,2))
    }
    ret = ret":"hextodec(substr(str,index(str,":")+1,4))
    return ret
}
$4 == "0A" { print getIP($2) }' /proc/net/tcp /proc/net/tcp6

It's output will look like:

$ awk '
function hextodec(str,ret,n,i,k,c){
    ret = 0
    n = length(str)
    for (i = 1; i <= n; i++) {
        c = tolower(substr(str, i, 1))
        k = index("123456789abcdef", c)
        ret = ret * 16 + k
    }
    return ret
}
function getIP(str,ret){
    ret=hextodec(substr(str,index(str,":")-2,2));
    for (i=5; i>0; i-=2) {
        ret = ret"."hextodec(substr(str,i,2))
    }
    ret = ret":"hextodec(substr(str,index(str,":")+1,4))
    return ret
}
$4 == "0A" { print getIP($2) }' /proc/net/tcp /proc/net/tcp6
127.0.0.1:56179
192.168.122.1:53
127.0.0.53:53
0.0.0.0:22
127.0.0.1:631
127.0.0.1:56247
0.0.0.0:34999
0.0.0.0:443
127.0.0.1:54179
0.0.0.0:38443
127.0.0.1:41197
0.0.0.0:38000
0.0.0.0:1716
0.0.0.0:22
0.0.0.0:631
0.0.0.0:39000
0.0.0.0:443

We'll need to take into account that for IPv6 it's output it's not quite right, but yet we are getting a similar output we would get using netstat or ss:

$ netstat -tln |tail +3 | awk '{print $4}'
127.0.0.1:56179
192.168.122.1:53
127.0.0.53:53
0.0.0.0:22
127.0.0.1:631
127.0.0.1:56247
0.0.0.0:34999
0.0.0.0:443
127.0.0.1:54179
0.0.0.0:38443
127.0.0.1:41197
0.0.0.0:38000
:::1716
:::22
::1:631
:::39000
:::443

Posted on 07/07/2021