最近业务有个需求,需要在宿主机上获取容器ip。获取ip这就不是个事,平凡而普通。
常用手段获取ip
常用的命令ifconfig, ip等等首先被pass,因为宿主机环境未知,这些命令可能没有。
那么只能从系统api着手。getifaddrs可以获取网卡名字和ip, 一轮测试下来,发现只能拿到容器虚拟网卡名字。显然不符合业务需求,首战就这样折戟沉沙。
利用容器命令获取ip
可以用的容器命令是什么呢?docekr? crictl? … 基于开源魔改的容器管理服务*ocekr? 甚至于完全自研的XXX?一切皆有可能,这种方案细想一下也直接被pass了。
原本以为挺简单的需求此时陷入僵局,常见的方法都不好使了。
查询路由表获取ip
在k8s节点上寻寻觅觅,发现路由表有针对容器虚拟网的详细路由。
此时的虚拟网卡为主流开源方案calico的虚拟网卡,本着Destination和Iface的对应关系,解决了容器ip的问题。
虽然可以使用,但是心里却是没底的。因为容器方案层出不穷。果不其然,可用了没多长时间就遇到了担心的事情,另一个场景是基于Iaas自研的容器管理方案。此时只见容器,不见calico,路由表上空空如也。又一次陷入僵局。
进入 net ns 获取ip
容器实现的核心之一就是linux的namespace,如果我能进入容器的net-ns,就可以获取到容器的ip。
因为namespace的资料基本都是针对进程的,所以不确定一个进程的多个线程能不能分属于不同的net-ns。因为是多线程程序,且对外有网络连接,所以不可能整个进程直接进入容器的net-ns,至少发起网络连接的线程需要在默认net-ns下。写了测试程序发现一个进程的多个线程可以分属于不同的net-ns。
于是遍历/proc/目录,先获取进程1的net-ns。
/proc/1/ns/net --> symlink
再逐个获取宿主机上所有进程的net-ns,只要和进程1的symlink不相同的,就表示是一个新的net-ns,我就需要进入其中获取这个net-ns下的ip。
进入容器的net-ns后,利用getifaddrs获取网卡的ip和ifindex(用于标识系统中的每个网络接口唯一ID)。
宿主机上同样利用getifaddrs获取网卡名称,同时获取网卡XXX的iflink(主要被隧道设备使用,用于标识隧道另一头的设备ID)
/sys/class/net/XXX/iflink
如果容器内网卡A的ifindex和宿主机容器网卡A1的iflink能匹配上,意味着A和A1属于一对veth pair,那么容器网卡A1的ip就是容器内网卡A的ip。
到此完美解决,理论上适用于任意形态容器的ip获取。同时以read-only进入容器的net-ns,也不会破坏容器的net-ns。
遗失的iflink
经过测试,可以获取docker系容器网卡ip。
但是测试常用组合containerd + calico时,却发现行不通了。最终发现是因为所有容器内网卡的ifindex都是同一个值,因为属于不同的net-ns,ifindex可以是同一个值。自然宿主机容器网卡的iflink的值也都是同一个值。
解决起来,也比较简单。 宿主机容器网卡都属于默认net-ns,所以他们的ifindex必定不一样,用容器内网卡的iflink去匹配宿主机容器网卡的ifindex即可。
尝试从/sys/class/net/XXX/iflink
读取容器网卡的iflink。嗯?读取失败?文件不存在?原来只进入了net-ns,没有进入mnt-ns,所以无法读取该文件。
于是尝试利用setns让 ‘获取容器网卡ip的线程’ 再进入一个mnt-ns,嗯?调用失败?invalid arguments?近在眼前的胜利怎么能让它阻挡,各种查资料,最后得出结论,mnt-ns只能在主线程中setns,并且调用setns成功后,才能创建其他线程。 最后不信邪地写代码验证,发现确实如此。Go语言因为天然多线程,最后只能利用 cgo constructor trick
在go的runtime还没启动时提前setns。
我的程序不可能在主线程设置mnt-ns,这与程序记录日志格格不如。又一次陷入僵局,这次似乎无解了。。。
柳暗花明的iproute2
nsenter进入容器的net-ns, 利用ip命令居然可以读出来iflink,类似这样的网卡eth0@if6
,if6表示iflink的值是6。在只进入net-ns前提下,它为什么可以读出来iflink?
于是下载ip命令的源码iproute2,通过添加printf分析源码,发现它是利用netlink从内核得到的iflink,事情似乎又有转机了。
netlink寻找iflink
利用AF_NETLINK的socket,向内核发起RTM_GETLINK请求,从响应ifinfomsg中解析出iflink。因为在容器net-ns下创建的socket,所以socket的属性与这个容器网络相关联,可以查出容器网卡的信息。
示例源码如下:
std::unordered_map<std::string, uint32_t> iflinks;
struct nl_req {struct nlmsghdr hdr;struct rtgenmsg gen;
};struct sockaddr_nl kernel {};
kernel.nl_family = AF_NETLINK;struct sockaddr_nl local {};
local.nl_family = AF_NETLINK;
local.nl_pid = getpid();
local.nl_groups = 0;nl_req req{};
req.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtgenmsg));
req.hdr.nlmsg_type = RTM_GETLINK;
req.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
req.hdr.nlmsg_pid = getpid();
req.hdr.nlmsg_seq = 1;
req.gen.rtgen_family = AF_PACKET;struct iovec iov {};
iov.iov_base = &req;
iov.iov_len = req.hdr.nlmsg_len;struct msghdr msg {};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_name = &kernel;
msg.msg_namelen = sizeof(kernel);auto fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd < 0) {return iflinks;
}
auto closer = std::shared_ptr<char>(new char,[fd](char* p) {delete p; close(fd); });auto ok = bind(fd, (struct sockaddr*)&local, sizeof(local));
if (ok < 0) {return iflinks;
}
sendmsg(fd, (struct msghdr*)&msg, 0);memset(&iov, 0, sizeof(iov));
constexpr int buf_size = 1024 * 32;
char buf[buf_size]{};
iov.iov_base = buf;
iov.iov_len = buf_size;
int64_t msg_len = 0;while (true) {msg_len = recvmsg(fd, &msg, 0);if (msg_len < 0) {break;}auto nlmsg_ptr = (struct nlmsghdr*)buf;if (nlmsg_ptr->nlmsg_type == NLMSG_DONE ||nlmsg_ptr->nlmsg_type == NLMSG_ERROR) {break;}while (NLMSG_OK(nlmsg_ptr, msg_len)) {if (nlmsg_ptr->nlmsg_type != RTM_NEWLINK) {nlmsg_ptr = NLMSG_NEXT(nlmsg_ptr, msg_len);continue;}auto ifi_ptr = (struct ifinfomsg*)NLMSG_DATA(nlmsg_ptr);auto attr_ptr = IFLA_RTA(ifi_ptr);auto attr_len = nlmsg_ptr->nlmsg_len - NLMSG_LENGTH(sizeof(*ifi_ptr));std::string ifname;uint32_t iflink = 0;while (RTA_OK(attr_ptr, attr_len)) {if (attr_ptr->rta_type == IFLA_IFNAME) {ifname = (char*)RTA_DATA(attr_ptr);}if (attr_ptr->rta_type == IFLA_LINK) {iflink = *((uint32_t*)RTA_DATA(attr_ptr));}attr_ptr = RTA_NEXT(attr_ptr, attr_len);}if (iflink != 0) {iflinks.emplace(std::move(ifname), iflink);}nlmsg_ptr = NLMSG_NEXT(nlmsg_ptr, msg_len);}
}
完美终章
遍历宿主机所有进程的net-ns,选择出和进程1不一样的新net-ns,逐个进入新的net-ns,利用getifaddrs获取容器网卡ip,利用netlink获取容器网卡iflink。
与宿主机上的网卡ifindex做关联比对,确定宿主机容器网卡ip。到此在宿主机上获取容器网卡ip完成。
如果是K8s daemonset模式,spec增加hostNetwork: true和hostPID: true 即可