加载conntrack时模块会自动追踪已经存在的连接吗
intro
k8s网络重度依赖了linux的iptables。在iptables提供的功能中,网络地址转换(NAT—Net Address Translation)又是一个重要的功能:当POD中的container需要访问集群外的网络时,就可能需要进行源地址转换(SNAT/MASQUERADE)。
既然要进行源地址选择,就需要从node中选择一个没有被使用的五元组(protoco/src ip/src port/dst ip/dst port)。直观上理解,选择是基于node自身可能得部分来选择的:本机上五元组中任何没有被使用的五元组都可以被选中。
对于TCP来说,这个流程可能是搜索当前node上排除掉所有已经bind的端口、已经established的连接等,从剩下的端口中选择。
但是查看SNAT的相关的代码可以发现:SNAT的时候并没有遍历TCP模块维护的连接信息,而是只遍历了nf_conntrack自己位于的链路跟踪信息。
那么问题就是:这个nf_conntrack会跟踪所有的已经建立的连接吗?假设系统开始没有启用这个模块,在连接建立之后加载nf_conntrack,这个加载的nf_conntrack会跟踪到之前建立的连接吗?
SNAT
在下面的流程中,只是判断了所有conntrack自己维护的连接信息(对于TCP来说,并不会查找系统维护的已经建立的TCP连接信息)。
xt_snat_target_v2>>nf_nat_setup_info>>get_unique_tuple>>nf_nat_l4proto_unique_tuple>>nf_nat_used_tuple_harder>>__nf_conntrack_find_get>>____nf_conntrack_find
///@file: linux\net\netfilter\nf_conntrack_core.c
/*
* Warning :
* - Caller must take a reference on returned object
* and recheck nf_ct_tuple_equal(tuple, &h->tuple)
*/
static struct nf_conntrack_tuple_hash *
____nf_conntrack_find(struct net *net, const struct nf_conntrack_zone *zone,
const struct nf_conntrack_tuple *tuple, u32 hash)
{
struct nf_conntrack_tuple_hash *h;
struct hlist_nulls_head *ct_hash;
struct hlist_nulls_node *n;
unsigned int bucket, hsize;
begin:
nf_conntrack_get_ht(&ct_hash, &hsize);
bucket = reciprocal_scale(hash, hsize);
hlist_nulls_for_each_entry_rcu(h, n, &ct_hash[bucket], hnnode) {
struct nf_conn *ct;
ct = nf_ct_tuplehash_to_ctrack(h);
if (nf_ct_is_expired(ct)) {
nf_ct_gc_expired(ct);
continue;
}
if (nf_ct_key_equal(h, tuple, zone, net))
return h;
}
/*
* if the nulls value we got at the end of this lookup is
* not the expected one, we must restart lookup.
* We probably met an item that was moved to another chain.
*/
if (get_nulls_value(n) != bucket) {
NF_CT_STAT_INC_ATOMIC(net, search_restart);
goto begin;
}
return NULL;
}
nf_conntrack的完备性
以收包为例,这个流程中暂时没看到特别的、不创建conntrack的流程,所以暂时认为只要加载/使能了conntrack模块,就会跟踪所有连接。
ipv4_conntrack_in>>nf_conntrack_in>>resolve_normal_ct
/* On success, returns 0, sets skb->_nfct | ctinfo */
static int
resolve_normal_ct(struct nf_conn *tmpl,
struct sk_buff *skb,
unsigned int dataoff,
u_int8_t protonum,
const struct nf_hook_state *state)
{
const struct nf_conntrack_zone *zone;
struct nf_conntrack_tuple tuple;
struct nf_conntrack_tuple_hash *h;
enum ip_conntrack_info ctinfo;
struct nf_conntrack_zone tmp;
u32 hash, zone_id, rid;
struct nf_conn *ct;
if (!nf_ct_get_tuple(skb, skb_network_offset(skb),
dataoff, state->pf, protonum, state->net,
&tuple))
return 0;
/* look for tuple match */
zone = nf_ct_zone_tmpl(tmpl, skb, &tmp);
zone_id = nf_ct_zone_id(zone, IP_CT_DIR_ORIGINAL);
hash = hash_conntrack_raw(&tuple, zone_id, state->net);
h = __nf_conntrack_find_get(state->net, zone, &tuple, hash);
if (!h) {
rid = nf_ct_zone_id(zone, IP_CT_DIR_REPLY);
if (zone_id != rid) {
u32 tmp = hash_conntrack_raw(&tuple, rid, state->net);
h = __nf_conntrack_find_get(state->net, zone, &tuple, tmp);
}
}
if (!h) {
h = init_conntrack(state->net, tmpl, &tuple,
skb, dataoff, hash);
if (!h)
return 0;
if (IS_ERR(h))
return PTR_ERR(h);
}
ct = nf_ct_tuplehash_to_ctrack(h);
/* It exists; we have (non-exclusive) reference. */
if (NF_CT_DIRECTION(h) == IP_CT_DIR_REPLY) {
ctinfo = IP_CT_ESTABLISHED_REPLY;
} else {
unsigned long status = READ_ONCE(ct->status);
/* Once we've had two way comms, always ESTABLISHED. */
if (likely(status & IPS_SEEN_REPLY))
ctinfo = IP_CT_ESTABLISHED;
else if (status & IPS_EXPECTED)
ctinfo = IP_CT_RELATED;
else
ctinfo = IP_CT_NEW;
}
nf_ct_set(skb, ct, ctinfo);
return 0;
}
测试动态开启
如果假设nf_conntrack在系统启动时是关闭的,然后建立socket连接(并且socket建立之后连接之间不再收发数据),然后再加载conntrack模块。新加载的conntrack会跟踪到加载前的连接吗?
卸载nf_conntrack
通过查看iptables输出,可以确定iptables中对nf_conntrack的引用主要是由于docker引入的,所以卸载nf_conntrack之前需要下关掉docker服务。
由于是在个人环境测试,所以安全起见,使用reboot重启了测试环境。重启之后通过lsmod确认没有加载nf_nat、nf_conntrack等模块。
tsecer@harry: systemctl disable docker.service docker.socket
客户端
基于这里例子需改。可以通过命令行指定自己绑定的端口地址,连接本地服务器127.0.0.1:8080端口。
#include <arpa/inet.h> // inet_addr()
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> // bzero()
#include <sys/socket.h>
#include <unistd.h> // read(), write(), close()
#include <error.h>
#define MAX 80
#define PORT 8080
#define CPORT 40000
#define SA struct sockaddr
void func(int sockfd)
{
char buff[MAX];
int n;
for (;;) {
bzero(buff, sizeof(buff));
printf("Enter the string : ");
n = 0;
while ((buff[n++] = getchar()) != '\n')
;
if (write(sockfd, buff, sizeof(buff)) < 0)
{
perror("write failed");
}
bzero(buff, sizeof(buff));
if (read(sockfd, buff, sizeof(buff)) < 0)
{
perror("read failed");
}
else
{
printf("From Server : %s", buff);
if ((strncmp(buff, "exit", 4)) == 0) {
printf("Client Exit...\n");
break;
}
}
}
}
int main(int argc, const char *argv[])
{
int sockfd, connfd;
struct sockaddr_in servaddr, cli;
if (argc < 2)
{
printf("port needed");
return -1;
}
int port = atoi(argv[1]);
if (port <= 0)
{
printf("invalid port %d", port);
return -1;
}
printf("port %d", port);
// socket create and verification
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
printf("socket creation failed...\n");
exit(0);
}
else
printf("Socket successfully created..\n");
bzero(&servaddr, sizeof(servaddr));
// assign IP, PORT
cli.sin_family = AF_INET;
cli.sin_addr.s_addr = inet_addr("127.0.0.1");
cli.sin_port = htons(port);
// Binding newly created socket to given IP and verification
if ((bind(sockfd, (SA*)&cli, sizeof(cli))) != 0) {
printf("socket bind failed...\n");
exit(0);
}
else
printf("Socket successfully binded..\n");
// assign IP, PORT
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(PORT);
// connect the client socket to server socket
if (connect(sockfd, (SA*)&servaddr, sizeof(servaddr)) != 0) {
printf("connection with the server failed...\n");
exit(0);
}
else
printf("connected to the server..\n");
// function for chat
func(sockfd);
// close the socket
close(sockfd);
}
服务端
基于此处代码修改,可以同时接收多个客户端连接。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h> // inet_addr()
void error(const char *msg) {
perror(msg);
exit(1);
}
int main(int argc, char *argv[]) {
int sockfd, newsockfd, portno;
socklen_t clilen;
char buffer[256];
struct sockaddr_in serv_addr, cli_addr;
int n;
if (argc < 2) {
fprintf(stderr, "ERROR, no port provided\n");
exit(1);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
error("ERROR opening socket");
bzero((char *) &serv_addr, sizeof(serv_addr));
portno = atoi(argv[1]);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(portno);
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
error("ERROR on binding");
int listenfd = listen(sockfd, 5);
printf("after listen %d\n", listenfd);
clilen = sizeof(cli_addr);
//Below code is modified to handle multiple clients using fork
//------------------------------------------------------------------
int pid;
while (1) {
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0)
error("ERROR on accept");
else
printf("accept new sockfd %d\n", newsockfd);
//fork new process
pid = fork();
if (pid < 0) {
error("ERROR in new process creation");
}
if (pid == 0) {
//child process
//close(sockfd);
//do whatever you want
bzero(buffer, 256);
n = read(newsockfd, buffer, 255);
if (n < 0)
error("ERROR reading from socket");
printf("Here is the message: %s\n", buffer);
n = write(newsockfd, "I got your message", 18);
if (n < 0)
error("ERROR writing to socket");
close(newsockfd);
} else {
//parent process
close(newsockfd);
}
}
//-------------------------------------------------------------------
return 0;
}
测试流程
- 启动服务器
服务器在127.0.0.1:8080侦听并接受连接。
- 启动第一个客户端连接
第一个客户端指定40000端口连接服务器。
- 开启SNAT
将所有目的是127.0.0.1:8080的TCP连接的源地址修改为127.0.0.1:40000。
tsecer@harry: sudo iptables -t nat -A POSTROUTING -p tcp -d 127.0.0.1 --dport 8080 -j SNAT --to-source 127.0.0.1:40000
- 启动第二个客户端
先开启抓包
tsecer@harry: tcpdump -nn -i any tcp and port 8080
启动第二个客户端,此时客户端使用新的50000端口连接服务器。
- tcpdump输出
可以看到,SNAT因为没有感知到NAT模块加载前存在的[40000, 8080]连接,所以在新的连接到来时,SNAT将50000端口映射为已经在使用的40000端口。
当新连接发送同步包(协议中[S]字段包)时,对方发送的是一个常规的ACK包;新客户端收到ACK包之后发送了RESET包([R]标志报文)重置了之前已经存在的连接(第一个客户端在服务器上建立的连接);之后启动新连接(新的一轮三次握手)。
tsecer@harry: tcpdump -nn -i any tcp and port 8080
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked v1), capture size 262144 bytes
00:20:50.475914 IP 127.0.0.1.40000 > 127.0.0.1.8080: Flags [S], seq 2551266112, win 65495, options [mss 65495,sackOK,TS val 1185681891 ecr 0,nop,wscale 7], length 0
00:20:50.475920 IP 127.0.0.1.8080 > 127.0.0.1.50000: Flags [.], ack 1427243005, win 512, options [nop,nop,TS val 1185681891 ecr 1185579931], length 0
00:20:50.475927 IP 127.0.0.1.40000 > 127.0.0.1.8080: Flags [R], seq 1427243005, win 0, length 0
00:20:51.510873 IP 127.0.0.1.40000 > 127.0.0.1.8080: Flags [S], seq 2551266112, win 65495, options [mss 65495,sackOK,TS val 1185682926 ecr 0,nop,wscale 7], length 0
00:20:51.510891 IP 127.0.0.1.8080 > 127.0.0.1.50000: Flags [S.], seq 1739282581, ack 2551266113, win 65483, options [mss 65495,sackOK,TS val 1185682926 ecr 1185682926,nop,wscale 7], length 0
00:20:51.510904 IP 127.0.0.1.40000 > 127.0.0.1.8080: Flags [.], ack 1739282582, win 512, options [nop,nop,TS val 1185682926 ecr 1185682926], length 0
结合conntrack的输出
tsecer@harry: sudo iptables -t nat -A POSTROUTING -o lo -p tcp -j SNAT --to-source 127.0.0.1:40000
tsecer@harry: conntrack -Lg | fgrep 8080
conntrack v1.4.4 (conntrack-tools): 50 flow entries have been shown.
tsecer@harry: conntrack -Lg | fgrep 8080
tcp 6 431988 ESTABLISHED src=127.0.0.1 dst=127.0.0.1 sport=50000 dport=8080 src=127.0.0.1 dst=127.0.0.1 sport=8080 dport=40000 [ASSURED] mark=0 use=1
conntrack v1.4.4 (conntrack-tools): 161 flow entries have been shown.
tsecer@harry:
结论
nf_conntrack动态开启的时候并不会主动跟踪已经建立的连接,但是当这些连接上如果有数据通讯的时候,conntrack就会完成对这个连接的跟踪。
outro
如果要使用conntrack的话,最好在系统启动的时候就开启该功能,否则可能会有一些意料外的问题。
但是docker的安装并不要求重启服务器,这个可能是假设SNAT选择一般是有随机性的,并且在一定时间内,之前存在的连接中会有数据通讯?
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架