加载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;
}

测试流程

  1. 启动服务器

服务器在127.0.0.1:8080侦听并接受连接。

  1. 启动第一个客户端连接

第一个客户端指定40000端口连接服务器。

  1. 开启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
  1. 启动第二个客户端

先开启抓包

tsecer@harry: tcpdump -nn -i any tcp and port 8080

启动第二个客户端,此时客户端使用新的50000端口连接服务器。

  1. 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选择一般是有随机性的,并且在一定时间内,之前存在的连接中会有数据通讯?

posted on   tsecer  阅读(9)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

点击右上角即可分享
微信分享提示