当前位置:网站首页>flannel 原理 之 TUN模式

flannel 原理 之 TUN模式

2022-04-23 14:11:00 Mrpre

flannel 原理 之 TUN模式

首先,TUN模式 原理详见 https://wonderful.blog.csdn.net/article/details/113105456 ,通常用来两个私网通过公网穿越。总的来说要熟悉掌握TUN设备的特性,被路由到TUN设备发出去的数据,都会被Open TUN的socket读到,做到拦截;write到TUN的数据,都会被TUN重放到协议栈,模拟了TUN设备所在的机器收包操作。

流程

假设当前 2台node node1为11.238.116.75 node2的物理IP 为11.238.116.73 并且启动的flannel。flannel进程和simpletun运行模式非常类似
main函数找那个执行Run()

  log.Info("Running backend.")
  wg.Add(1)
  go func() {
    
    bn.Run(ctx)
    wg.Done()
  }()

Run()函数在backend/udp/cproxy_amd64.go中实现。


func (n *network) Run(ctx context.Context) {
    
  defer func() {
    
    n.tun.Close()
    n.conn.Close()
    n.ctl.Close()
    n.ctl2.Close()
  }()

  // one for each goroutine below
  wg := sync.WaitGroup{
    }
  defer wg.Wait()

  wg.Add(1)
  go func() {
    
    //n.tun是tun设备
    //n.conn是listen的本机地址,用来接收其他物理机封装的overlay报文
    //n.ctl2是控制通道,用来执行控制面消息
    runCProxy(n.tun, n.conn, n.ctl2, n.tunNet.IP, n.MTU())
    wg.Done()
  }()

   ......
}

func runCProxy(tun *os.File, conn *net.UDPConn, ctl *os.File, tunIP ip.IP4, tunMTU int) {
    
  var log_errors int
  if log.V(1) {
    
    log_errors = 1
  }

  c, err := conn.File()
  if err != nil {
    
    log.Error("Converting UDPConn to File failed: ", err)
    return
  }
  defer c.Close()

  //C.run_proxy 函数是 backend/udp/proxy_amd64.c
  C.run_proxy(
    C.int(tun.Fd()),
    C.int(c.Fd()),
    C.int(ctl.Fd()),
    C.in_addr_t(tunIP.NetworkOrder()),
    C.size_t(tunMTU),
    C.int(log_errors),
  )
}

backend/udp/proxy_amd64.c

void run_proxy(int tun, int sock, int ctl, in_addr_t tun_ip, size_t tun_mtu, int log_errors) {
    
  char *buf;
  struct pollfd fds[PFD_CNT] = {
    
    {
    
      .fd = tun,
      .events = POLLIN
    },
    {
    
      .fd = sock,
      .events = POLLIN
    },
    {
    
      .fd = ctl,
      .events = POLLIN
    },
  };

  exit_flag = 0;
  tun_addr = tun_ip;
  log_enabled = log_errors;

  buf = (char *) malloc(tun_mtu);
  if( !buf ) {
    
    log_error("Failed to allocate %d byte buffer\n", tun_mtu);
    exit(1);
  }

  fcntl(tun, F_SETFL, O_NONBLOCK);

  while( !exit_flag ) {
    
    int nfds = poll(fds, PFD_CNT, -1), activity;
    if( nfds < 0 ) {
    
      if( errno == EINTR )
        continue;

      log_error("Poll failed: %s\n", strerror(errno));
      exit(1);
    }

    if( fds[PFD_CTL].revents & POLLIN )
      process_cmd(ctl);

    //如果是listen的端口有可读信息,就无脑从listen端口读取出去,写入TUN,这样TUN设备就能将这个报文模拟成收包动作,当前Node进行收包操作。由udp_to_tun函数处理。
    //如果是tun设备有可读信号,表示本Node上面的容器有数据发出去,那么我们就需要判断这个目的ip地址在哪个node上,然后发到该node上面的flannel进程。由tun_to_udp函数处理。
    if( fds[PFD_TUN].revents & POLLIN || fds[PFD_SOCK].revents & POLLIN )
      do {
    
        activity = 0;
        activity += tun_to_udp(tun, sock, buf, tun_mtu);
        activity += udp_to_tun(sock, tun, buf, tun_mtu);

        /* As long as tun or udp is readable bypass poll(). * We'll just occasionally get EAGAIN on an unreadable fd which * is cheaper than the poll() call, the rest of the time the * read/recvfrom call moves data which poll() never does for us. * * This is at the expense of the ctl socket, a counter could be * used to place an upper bound on how long we may neglect ctl. */
      } while( activity );
  }

  free(buf);
}

tun_to_udp函数,读取tun中的请求,判断其目的ip,目的ip必然是其他node上容器的网段(否则也不会被路由到tun设备)。那么本函数的主要功能是根据目的ip的网段,找到对应的node

static int tun_to_udp(int tun, int sock, char *buf, size_t buflen) {
    
  struct iphdr *iph;
  struct sockaddr_in *next_hop;

  ssize_t pktlen = tun_recv_packet(tun, buf, buflen);
  if( pktlen < 0 )
    return 0;
  
  iph = (struct iphdr *)buf;

  //找路由,next_hop 就是目的ip容器所在的node的物理地址
  next_hop = find_route((in_addr_t) iph->daddr);
  if( !next_hop ) {
    
    send_net_unreachable(tun, buf);
    goto _active;
  }

  if( !decrement_ttl(iph) ) {
    
    /* TTL went to 0, discard. * TODO: send back ICMP Time Exceeded */
    goto _active;
  }

  //将请求原封不动,通过UDP封装发给对应node的flannel进程
  sock_send_packet(sock, buf, pktlen, next_hop);
_active:
  return 1;
}

路由表哪里来?上一篇 flannel 原理 之 子网划分中,我们提到了,flannel将本机分配的的子网网段,注册到了$PREFIX/subnets/目录下,那么,其他flannel进程只需要watch这个目录即可。

func (esr *etcdSubnetRegistry) watchSubnets(ctx context.Context, since uint64) (Event, uint64, error) {
    
  key := path.Join(esr.etcdCfg.Prefix, "subnets")
  opts := &etcd.WatcherOptions{
    
    AfterIndex: since,
    Recursive:  true,
  }
  e, err := esr.client().Watcher(key, opts).Next(ctx)
  if err != nil {
    
    return Event{
    }, 0, err
  }

  evt, err := parseSubnetWatchResponse(e)
  return evt, e.Node.ModifiedIndex, err
}

抓包结果

当前存在2台node,分别是A 11.238.116.75 以及B 11.238.116.73,A中启动了容器182.48.56.2,B中启动了容器182.48.17.2。在A中执行Ping 182.48.17.2。

首先在A的docker0上抓包,可见报文为原始的3层头
在这里插入图片描述

在A的flannel0上抓包,源地址变为了flannel0的地址,这是因为Docker会在node上面添加了snat规则

Chain POSTROUTING (policy ACCEPT 16250 packets, 1188K bytes)
 pkts bytes target     prot opt in     out     source               destination
  172 11132 MASQUERADE  all  --  *      !docker0  182.48.56.0/24       0.0.0.0/0

由于 是从 flannel0 出去,自然,源地址被替换为flannel0的IP

在这里插入图片描述

在A的eth0上抓包,可见ping报文的原始报文,被封装了物理网络(Node)的ip,被发送到了182.48.17.2。这个容器所在的Node的flannel进程。在B的eth0抓包,必然也是这个报文,这里不再多截图。
需要注意的是,这个报文实际上不是原始ping报文,原始ping报文的sip被改成了A的flannel0的IP地址。(TODO 具体原因待考究)
在这里插入图片描述

此时,overlay报文已经到了B的eth0,并且被B的flannel进程监听,且读取。B要做的事情,就是将该报文的负载(即包含3层头的Ping报文)写入他的tun设备。tun设备即flannel0。

在B的flannel0上抓包,flannel0
在这里插入图片描述

版权声明
本文为[Mrpre]所创,转载请带上原文链接,感谢
https://wonderful.blog.csdn.net/article/details/115180402