Fooling around with the proc directory and network connections
Lately I have been curious to try to know if certain linux users are accessing certain resources, in an effort to discover and expose dependencies between services.
This users are actually the ones with which containers are being run with from k8s itself (in toolforge, each k8s user has a linux user, and it’s containers run as that user).
To do so, I wrote a small golang script. Here’s some notes on trying to get that information.
Where to get the info from
The ideal would be to operationalize the code, so the services themselves send statistics about their dependencies, but given the nature of the services (self-hoster, volunteer driven, any language, etc.) trying to touch all the services code is a no-go.
For that reason the information has to be gotten in a passible form.
Some ideas would be:
- Using a transparent proxy
- Hooking into the kernel (using systemtap, ebpf or overryding the system libraries)
- Sniffing the host traffic
I don’t have much experience (hello world!) with systemtap and ebpf, and I had less than a week to complete the project, so I went with the network sniffing :)
Sniffing in golang
This was way easier than I though it would be, there’s a module named gopacket that provides a quite easy API to do so, that will give you a channel where you can extract packets from:
|
|
Then all you have to do is receive from that channel the packages:
for {
select {
case packet := <-packetChannel:
// do something
case default:
// do something if there's no packages yet
}
}
Anyhow, from there, from the IP layer we can get the IPs involved, and from the TCP or UDP layers we can get the ports associated with each.
From that information, how do we know which process is the one sending that package?
How does ss
do it?
I was curious to check what the ss
command (the successor of netstat
) does to get that information, so I went and
straced it xd
$ strace ss -tnp 2>&1 1>/dev/null | vim -
From there I was seeing that it’s navigating the whole /proc/<pid>
tree, and for each is checking the fd
, and then
doing a readlink
on each of the entries, checking then where are they pointing too and using that info to get the
sockets and ports:
... lots of permission denied, I was not root
openat(AT_FDCWD, "/proc/3105/fd/", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 4
...
readlink("/proc/3105/fd/0", "/dev/null", 63) = 9
readlink("/proc/3105/fd/1", "socket:[31324]", 63) = 14 # this is a socket on port 31324
...
So that was interesting, netstat
ends up doing the same too.
/proc/net
Another option is /proc/net
, here you’ll find all the network connections. So that’s the first thing I tried.
More info on it in the kernel docs here.
Easy enough, that was working really well when trying to sniff on simple processes (ex. on one terminal you start the
sniffer, and on another you run a curl
command), but you will not see things running on containers, here’s why:
/proc/net
is a link to /proc/self/net
, and /proc/self
is in itself a link to /proc/<pid>
where pid
is the
process id of the current process.
Now, anything under /proc/<pid>/net
show all the connections in that process network namespace, and well, in
linux containers are implemented with those namespaces so what you are seeing is actually the network connections for
the namespace of the current process, and that most probably does not include containers.
Another thing to have into account, is that /proc/<pid>/net
has all the network connections in the process network
namespace, including the ones not started by that process, so you still have to filter them out.
So on a quick note, here’s how many processes are there on each network namespace on a k8s node (looking directly into
/proc/<pid>/ns
as docker does not add an entry in the global /var/run/netns
):
dcaro@tools-k8s-worker-32:~/netstnoop$ sudo ls -la /proc/*/ns/net | awk '{print $11}' | sort | uniq -c | head
ls: cannot read symbolic link '/proc/17747/ns/net': No such file or directory
1
209 net:[4026531992]
1 net:[4026532222]
20 net:[4026532352]
6 net:[4026532449]
9 net:[4026532534]
6 net:[4026532607]
7 net:[4026532807]
9 net:[4026533066]
...
Note also that even the time between bash expanding /proc/*/ns/net
and ls reading the files some processes don’t
exist anymore, giving the No such file
error.
How to figure all that out then?
Given that:
- we don’t care about that specific packet, just any “user” that has a namespace in which a process is contacting those services
- the process file owner is the same as the user we care about
for every pid in `/proc`, do
check the `net/*` directories for the services we are interested in
if any matches:
add user owner of the process and the namespace service counts to the stats
How could you find out the specific process that sent the package?
That is a bit more complicated :/
For that we have to:
- Follow the process above to get which namespace is the one that is contacting that IP/port
- Once you have the namespace, you can also extract the inode (from the same net file
/proc/<pid>/net/*
) - With the inode, you can then look at all the open files for each process on that same namespace (
/proc/*/fd/*
) - That will give you the specific processes that have that socket open (note that could be more than one!)
Enjoy!