1.简介
Linux NAT(Network Address Translation)转换是一种网络技术,用于将一个或多个私有网络内的IP地址转换为一个公共的IP地址,以便与互联网通信。
在k8s业务场景中,业务组件之间的关系十分复杂.
由于 Kubernetes 的网络模型假设Pod之间访问时使用的是对方Pod 的实际地址,所以一个Pod内部的应用程序看到的自己的IP地址和端口与集群内其他Pod看到的一样 。它们都是Pod实际分配的IP地址 。 将IP地址和端口在Pod内部和外部都保待一致,也就不需要使用 NAT 进行地址转换了。
一些场景如cilium 并没有使用Netfilter的NAT转换.
2.linux中的NAT转换
linux的NAT转换是基于 Netfilter 网络框架实现的。
Netfilter 是 Linux 内核中一个对数据 包进行控制、修改和过滤(manipulation and filtering)的框架。它在内核协议 栈中设置了若干hook 点,以此对数据包进行拦截、过滤或其他处理。 Netfilter 是最古老的内核框架之一,1998 年开始开发,2000 年合并到 2.4.x 内 核主线版本 [5]。
Netfilter 官方文档:
https://www.netfilter.org/documentation/index.html
netfilter是Linux内核的包过滤框架,它提供了一系列的钩子(Hook)供其他模块控制包的流动。这些钩子包括
NF_IP_PRE_ROUTING:刚刚通过数据链路层解包进入网络层的数据包通过此钩子,它在路由之前处理
NF_IP_LOCAL_IN:经过路由查找后,送往本机(目的地址在本地)的包会通过此钩子
NF_IP_FORWARD:不是本地产生的并且目的地不是本地的包(即转发的包)会通过此钩子
NF_IP_LOCAL_OUT:所有本地生成的发往其他机器的包会通过该钩子
NF_IP_POST_ROUTING:在包就要离开本机之前会通过该钩子,它在路由之后处理。
3.使用conntrack读取nat转换
conntrack 是 netfilter一个模块。
NAT是在连接跟踪的基础上实现的,所以conntrack肯定是在NAT之前建立的。
conntrack注册的优先级:
enum nf_ip_hook_priorities {
NF_IP_PRI_FIRST = INT_MIN,
NF_IP_PRI_RAW_BEFORE_DEFRAG = -450,
NF_IP_PRI_CONNTRACK_DEFRAG = -400,
NF_IP_PRI_RAW = -300,
NF_IP_PRI_SELINUX_FIRST = -225,
NF_IP_PRI_CONNTRACK = -200,
NF_IP_PRI_MANGLE = -150,
NF_IP_PRI_NAT_DST = -100,
NF_IP_PRI_FILTER = 0,
NF_IP_PRI_SECURITY = 50,
NF_IP_PRI_NAT_SRC = 100,
NF_IP_PRI_SELINUX_LAST = 225,
NF_IP_PRI_CONNTRACK_HELPER = 300,
NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
NF_IP_PRI_LAST = INT_MAX,
};
我们可以看到,NAT是在连接跟踪的基础上实现的,所以连接跟踪肯定是在NAT之前建立的。在上面的优先级中,优先级越小,越容易被先调用。
使用conntrack 查看当前节点NAT 转换情况
sudo conntrack -L -j
tcp 6 82 SYN_SENT src=172.19.0.2 dst=192.168.101.98 sport=55628 dport=443 [UNREPLIED] src=192.168.101.98 dst=192.168.1.3 sport=443 dport=55628 mark=0 use=1 conntrack v1.4.6 (conntrack-tools): 1 flow entries have been shown.
所以我们使用ebpf hook conntrack 就可以查看当前节点NAT 转换情况。
4. 使用ebpf hook conntrack
使用ebpf 拦截conntrack,我们主要拦截:
SEC("kprobe/__nf_conntrack_hash_insert")
SEC("kprobe/ctnetlink_fill_info")
实现主流程
hook住 __nf_conntrack_hash_insert:
SEC("kprobe/__nf_conntrack_hash_insert")
int BPF_KPROBE(kprobe___nf_conntrack_hash_insert, struct nf_conn *ct,unsigned int hash, unsigned int reply_hash) {
u32 status = ct_status(ct);
__maybe_unused possible_net_t p_net = BPF_CORE_READ(ct, ct_net);
if (!(status&IPS_CONFIRMED)) {
log_debug("kprobe/__nf_conntrack_hash_insert include IPS_CONFIRMED: netns: %u, status: %x\n", get_netns(&p_net), status);
return 0;
}
if (!(status&IPS_NAT_MASK)) {
return 0;
}
if (!(status&IPS_CONFIRMED) || !(status&IPS_NAT_MASK)) {
log_debug("kprobe/filter: netns: %u, status: %x\n", get_netns(&p_net), status);
return 0;
}
conntrack_tuple_t orig = {}, reply = {};
if (nf_conn_to_conntrack_tuples(ct, &orig, &reply) != 0) {
return 0;
}
bpf_map_update_with_telemetry(conntrack, &orig, &reply, BPF_ANY);
bpf_map_update_with_telemetry(conntrack, &reply, &orig, BPF_ANY);
increment_telemetry_registers_count();
return 0;
}
hook住ctnetlink_fill_info:
SEC("kprobe/ctnetlink_fill_info")
int BPF_KPROBE(kprobe_ctnetlink_fill_info, struct nf_conn *ct) {
proc_t proc = {};
bpf_get_current_comm(&proc.comm, sizeof(proc.comm));
if (!proc_t_comm_prefix_equals("system-probe", 12, proc)) {
log_debug("skipping kprobe/ctnetlink_fill_info invocation from non-system-probe process\n");
return 0;
}
u32 status = ct_status(ct);
if (!(status&IPS_CONFIRMED) || !(status&IPS_NAT_MASK)) {
return 0;
}
__maybe_unused possible_net_t c_net = BPF_CORE_READ(ct, ct_net);
log_debug("kprobe/ctnetlink_fill_info: netns: %u, status: %x\n", get_netns(&c_net), status);
conntrack_tuple_t orig = {}, reply = {};
if (nf_conn_to_conntrack_tuples(ct, &orig, &reply) != 0) {
return 0;
}
bpf_map_update_with_telemetry(conntrack, &orig, &reply, BPF_ANY);
bpf_map_update_with_telemetry(conntrack, &reply, &orig, BPF_ANY);
increment_telemetry_registers_count();
return 0;
}
自此将kernel中的nat 的sock 五元组采集到了ebpf的map中,上报到用户空间。