Skip to content

Latest commit

 

History

History
685 lines (558 loc) · 32.6 KB

linux网络(一).md

File metadata and controls

685 lines (558 loc) · 32.6 KB

搞不懂网络的话很多东西都不懂,本篇尤其是关于路由那一块,简直要了命,更是直接没搞明白,还得沉淀一段时间以后再补。

  1. tun/tap
  2. veth-pair
  3. linux bridge
  4. macvlan&ipvlan

上面都是非基础的东西,但是是需要解决并落地的东西,所以在学习完基础后就会开始搞,也是关于实际网络设备到云计算网络的转变。

协议栈

先说下OSI七层模型,这个模型是由ISO组织制定的用于划分网络层次的规则,定义了每一层的功能与数据结构。

ce88d095-f382-4822-8969-2cae7b775d09.png

我们知道的什么tcpudp各种协议都被包含在这七层之中,但是协议都是协议,都是约定好的纸面的东西,大家都按照这个数据标准来处理。 比如数据的封装与解包:

78ec13c0-316b-45ef-b4d4-18409c54024e.png

17aab707-6031-4a54-99b7-095012991e59.png

标准与规范都定义好了,那总得有代码有函数去真正实现,让数据的上下传递符合协议标准,协议不同,那实际实现的代码就不同,因此很多很多不同协议的代码堆叠在一起,这些代码为上层提供调用接口,封装成数据,由此构成了一套成熟的代码集合,也就是协议栈。

a8e8deaa-6b08-4b23-9cf2-82938851347d.png

协议栈就是协议套件的软件实现,是各层协议的总和

一个协议栈被实现好后,那用户态的app发出的数据自然都会传输到协议栈中,然后由内部的规则最后通过物理层的设备传输出去。但是呢,很多时候网络数据或者说流量的传输并非都是这么直来直去,或者说一开始的网络是这样的,在一个系统内,网络数据就是这么单纯的从上层到下层,出了这个系统后,就全靠网络设备去转发,比如路由器,比如交换机,其实现在也是这样的,然而随着云计算虚拟化这些技术的兴起,越来越多的网络问题需要在一个系统内就解决,那虚拟网络设备自然也就应运而生,然而在说虚拟网络设备前还有个绕不开的点就是路由规则,因为协议栈需要根据路由规则才能决定将数据交给哪个网络设备

路由决策原则

顾名思义,这是路由器根据路由表中的信息,选择最佳的路径将数据转发出去,在不考虑转发方案的基础上,最重要的一点就是路由表信息,其中存储着三种路由信息:

  1. 直连路由
  2. 静态路由
  3. 动态路由

通过route命令可以查看本机的路由表信息

$route
Kernel IP routing table
Destination         Gateway          Genmask         Flags          Metric         Ref           Use         Iface
default            _gateway          0.0.0.0          UG             600            0             0          wlp3s0
10.1.44.0           0.0.0.0        255.255.255.0      U               0             0             0          docker0
172.16.141.0        0.0.0.0        255.255.255.0      U               0             0             0          vmnet8
192.168.40.0        0.0.0.0        255.255.248.0      U              600            0             0          wlp3s0
192.168.82.0        0.0.0.0        255.255.255.0      U               0             0             0          vmnet1

注意一下那个docker0的路由,其实在没有启动docker服务的时候,这条路由信息是不存在的,而启动后则插入了这条路由。

报文传输

一切都要从报文传输说起。

应用层(TCP/IP模型)的网络传输基本上都是通过socket系统调用将数据推入协议栈中,这也是linux网络编程的入口,先前学习过socket网络传输的流程,但是数据是怎么从client到达server的呢?就用一个简单的程序来测试一下:

/*==============================================================================
# Author: lang [email protected]
# Filetype: C source code
# Environment: Linux & Archlinux
# Tool: Vim & Gcc
# Date: 2019.10.14
# Descprition: Randomly written code
================================================================================*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLEN 1024


int main(void)
{
 /*socket file description*/
 int sockfd,connc;
 char *message = "This is test\n",buf[MAXLEN];
 struct sockaddr_in servaddr;


 sockfd = socket(AF_INET, SOCK_STREAM,0);
 if(sockfd == -1){
  perror("sock created");
  exit(-1);
 }


 /*set serverAddr default=0*/
 bzero(&servaddr, sizeof(servaddr));


 /*set serverAddr info*/
 servaddr.sin_family = AF_INET;
 servaddr.sin_port = htons(9999);
 servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");


 connc = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));


 if(connc == -1){
  perror("connect error");
  exit(-1);
 }
 write(sockfd, message, strlen(message));
 read(sockfd, buf, MAXLEN);
 close(sockfd);
 return 0;
}

研究过socket的连接模型,自然就应当知道,TCP client发起connect的时候就会与服务端开始产生通信,那切入点就从connect开始探究。

linux网络流程可以将上面的程序自下而上分为四层:驱动层协议无关层IP层socket层

connect

直接内核源码看过去,connect的实现是在net/socket.c中定义的:

SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
  int, addrlen)

从上往下来说:

  1. sockfd_lookup_light是寻找socket实例
  2. move_addr_to_kernel从用户空间拷贝套接字地址到内核空间
  3. security_socket_connect安全检测,对协议本身的实现没有什么干扰
  4. sock->ops->connect调用不同协议本身的connect

关于第四点需要看一个数组:

此处前置socket_create 中的全局协议链表,会去设置相应的sock->ops,参考前置知识

static struct inet_protosw inetsw_array[] =
{
 {
  .type = SOCK_STREAM,
  .protocol = IPPROTO_TCP,
  .prot = &tcp_prot,
  .ops = &inet_stream_ops,
  .flags = INET_PROTOSW_PERMANENT |
         INET_PROTOSW_ICSK,
 },


 {
  .type = SOCK_DGRAM,
  .protocol = IPPROTO_UDP,
  .prot = &udp_prot,
  .ops = &inet_dgram_ops,
  .flags = INET_PROTOSW_PERMANENT,
       },


       {
  .type = SOCK_DGRAM,
  .protocol = IPPROTO_ICMP,
  .prot = &ping_prot,
  .ops = &inet_sockraw_ops,
  .flags = INET_PROTOSW_REUSE,
       },


       {
        .type = SOCK_RAW,
        .protocol = IPPROTO_IP, /* wild card */
        .prot = &raw_prot,
        .ops = &inet_sockraw_ops,
        .flags = INET_PROTOSW_REUSE,
       }
};

对于SOCK_STREAM类型的调用的最终会是inet_stream_connect,这个才是真正去处理connect的函数。

这部分东西需要前置sockfs

inet_stream_connect

这里需要注意的也只有一行而已:

err = __inet_stream_connect(sock, uaddr, addr_len, flags, 0);

__inet_stream_connect

检查了地址长度和协议族,针对AF_UNSPEC协议族做了特殊的处理(去断开连接,然后根据返回来设置socket状态),接着检查一下当前socket状态然后分别作不同的处理,总结一下就是只有状态为SS_CONNECTING或者是SS_UNCONNECTED才有操作,其余的就直接goto out了。 对于一个socket连接的话,理想上都应该是进入到SS_UNCONECTED的流程,因此就直接进入此流程去看。

err = sk->sk_prot->connect(sk, uaddr, addr_len);

这儿和先前的一样,都是已经确定的东西,因为使用的传输层协议是TCP,所以实际调用的connect方式是tcp_v4_connect

后面就是收尾的处理了,有兴趣的话去看参考资料,这儿不去研究。

tcp_v4_connect

先不急着去看实现流程,单纯的想一想,这个函数也就是去实现一个TCP协议的三次握手,实际上来说就是发送一个SYN报文,然后处理一个ACK报文。那么代码中就应该存在的逻辑有:

  1. 报文格式的构造或者说处理
  2. 网络

那对于我来说,我需要关注报文本身吗?很明显不需要,我所关注的只是单纯的网络,即代码是怎么知道把这个报文发向哪儿的?再扩展一点,看一下tcp_v4_connect的传入参数可以看出来,人为去控制的关于地址的信息就只有uaddr,追其来源就是如下信息:

 servaddr.sin_family = AF_INET;
 servaddr.sin_port = htons(9999);
 servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

换成uaddr的结构体信息也就是:

struct sockaddr_in {
  __kernel_sa_family_t sin_family; /* Address family */
  __be16 sin_port; /* Port number */
  struct in_addr sin_addr; /* Internet address */


  /* Pad to size of `struct sockaddr'. */
  unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
   sizeof(unsigned short int) - sizeof(struct in_addr)];
};

其实去分析的话也会发现关于报文的处理其实并不是很多,大部分还都是关于路由的处理。 1.如果用户已经设置源IP,则直接使用设置的源IP 2.如果没有设置源IP则根据目的ip和路由查找源IP 3.如果用户已经设置源port,则直接使用源Port 4.如果没有设置源PORT 根据目的IP port 和当前系统的establish tcp hash表查找sport。 5.查找目的路由 6.将TCP sock 状态设置为TCP_SYN_SENT。 7.根据已经查找到的路由及路由出口设备的能力设置一些GSO TSO标志等。sk_setup_caps。

整个函数上来就是强制类型转换:

 struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
 struct inet_sock *inet = inet_sk(sk);
 struct tcp_sock *tp = tcp_sk(sk);

我们知道unix中万物皆文件,没错,bsd在实现上把socket设计成一种文件,然后通过虚拟文件系统的操作接口就可以访问socket,而访问socket时会调用相应的驱动程序,从而也就是使用底层协议进行通信,需要去了解一下不同的socket结构体意义:

  1. struct socket:这是基本的BSD socket,应用程序通过系统调用创建的socket都是这个结构体,是基于VFS创建出来的,类型主要有三种:流式SOCK_STREAM数据报SOCK_DGRAM原始套接字SOCK_RAW
  2. struct sock:这是网络层socket,对应有TCPUDPRAW三种
  3. struct inet_sockINET域socket,是对struct sock的扩展,提供了INET域的属性,比如TTL组播列表IP地址端口
  4. struct raw_sockRAW协议的socket,针对struct inet_sock的扩展,处理ICMP相关的内容
  5. struct udp_sockUDP协议的socket,针对struct inet_sock的扩展
  6. struct tcp_sockTCP协议针对inet_connextion_sock的扩展,增加了滑动窗口拥塞控制等专用属性
  7. struct inet_connection_sock:是所有面向连接的socket表示,基于struct inet_sock的扩展
  8. struct inet_timewait_sock:网络层用于超时控制的socket
  9. struct tcp_timewait_sockTCP协议用于超时控制的socket

然后简单地判断下地址长度(if (addr_len < sizeof(struct sockaddr_in)))和协议族后(if (usin->sin_family != AF_INET))就进入到了网络地址设置。

首先是设置下一跳地址和目的地址,都设置成用户定义的地址:

nexthop = daddr = usin->sin_addr.s_addr;

获取ip路由选项

inet_opt = rcu_dereference_protected(inet->inet_opt,
                         lockdep_sock_is_held(sk));

如果有路由选项的话则把下一跳设置成ip路由选项中的第一跳地址,同时设置到源端口目的端口

nexthop = inet_opt->opt.faddr;
orig_sport = inet->inet_sport;
orig_dport = usin->sin_port;

设置flowi4流控,也就是路由表查找的键值fl4 = &inet->cork.fl.u.ip4;,接着就是调用ip_route_connect进行路由查找。 路由查找先往后放,因为越过了socket create的流程,所以会发现现在很多东西都很别扭,比如flowi4inet_opt这些都是怎么来的?

4899ae06-a0e3-48cd-96d4-d14a83054695.png

整了个函数流的图出来,可以看出来最终使用到的信息inet_sock来源于最初的一个struct socket *sock,中途只有一个inet_sk的强制类型转换的操作。那实际上来说,关于后续用到的所有数据,就已经封装到struct socket *sock中了。

__sock_create

这儿不去深究整个逻辑,就看struct socket *sock是怎么被创建出来的

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) -> sock_create -> __sock_create这个函数流完成了一个socket的创建,而基本核心逻辑都在__sock_create之中。

首先一个前后顺序问题就是先创建了socket对象,然后才影射出socket文件描述符

 /*
  * Allocate the socket and allow the family to set things up. if
  * the protocol is 0, the family is instructed to select an appropriate
  * default.
  */
 sock = sock_alloc();

首先就是在VFS上分配一个struct socket对象,之后就是针对此对象的处理

pf->create(net, sock, protocol, kern);

然而pf->create得往前看是pf = rcu_dereference(net_families[family]);,而family根据代码的话是传入的AF_INET,那关于这个数组继续往前看

static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;


struct net_proto_family {
 int family;
 int (*create)(struct net *net, struct socket *sock,
      int protocol, int kern);
 struct module *owner;
};

越过一些知识

这个数组是在inet_init中被初始化的:(void)sock_register(&inet_family_ops);

static const struct net_proto_family inet_family_ops = {
 .family = PF_INET,
 .create = inet_create,
 .owner = THIS_MODULE,
};

那么pf->create就是inet_create :)

好,先前提到过关于sock->ops的前置知识,现在就直接看一下,同样是在inet_init的过程中:

static struct list_head inetsw[SOCK_MAX];
 /* Register the socket-side information for inet_create. */
 for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
  INIT_LIST_HEAD(r);


 for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
  inet_register_protosw(q);

inetsw数组的每一个元素都是一个双向链表,在初始化每个元素的头后,将inetsw_array数组的元素使用inet_register_protosw函数注册到inetsw数组中,而关于inetsw_array数组上面有说到。

回来继续看代码,在inet_create中第一个重要的操作就是关于struct sock对象的创建,先前说过struct socketstruct sock的区别,这个对象中有关于网络层的信息,也是需要着重关注的信息。

sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);

接着就是针对sk的数据初始化

sock_init_data(sock, sk);

主要设置一些队列信息还有连接状态等,也就是与IP协议相关联的部分。再往下走就到了需要去着重关注的一步:

 if (sk->sk_prot->init) {
  err = sk->sk_prot->init(sk);

这儿的设置还是回顾到sk_alloc()函数中sk->sk_prot = sk->sk_prot_creator = prot;,即inetsw_array中的元素tcp_prot,那这儿真正使用的函数是:

.init = tcp_v4_init_sock

到这就闭环一下__sock_createinet_create,因为这两个里面的其余操作已经无关紧要了,因为他们最终的功效都是去调整生成最终要被利用的struct sock对象。

tcp_v4_init_sock

操作比较少,实际就两个,先说下第二个就是设置了传输层的各种处理函数接口,暂时并没有用起来。而第一个也只是初始化各种tcp参数信息。

tcp_init_sock(sk);
icsk->icsk_af_ops = &ipv4_specific;

什么前置信息都没有,这时候才想到,这些信息不会是等到要用时才开始初始化的吧?

重回connect

创建的socket对象并不是很复杂,也没有什么有用的信息,因此还是回来重新分析先前的流程

上面说道ip_route_connect还有fl4的问题,一路分析过来其实是没有看到fl4是怎么被填充的,因此就直接跟入ip_route_connect去看逻辑。 首先有个初始化的操作,如果跟踪一下函数链就是如下:

ip_route_connect_init -> flowi4_init_output

这个函数会去初始化fl4的数据,例如fl4->daddrfl4->saddrfl4->fl4_dportfl4->fl4_sport,但是并非都是已经设置好值的,因为想要发送第一个SYN报文,就需要完整的来源/目的地址来源/目的端口,然而在客户端的socket connect中,只有目的地址/端口可以被用户提供出来,来源端口可以从未用端口中动态分配一个,然而一个主机上可能有多个IP,那么来源地址如果错误的话,就很有可能造成数据不可达,因此这时候需要有路由参与在内。

而关于这个来源地址在接口层次上还分为bindno-bind类型,如果是bind类型的话来源地址就是bind地址,但是no-bind类型的话则需要根据路由的结果确定来源地址

这儿再说关于路由的问题,一个数据包传输到本地后,需要先在IP层查找路由,然后在传输层查找socket,因此为了高效利用,linux内核提供了一个特性ip_early_demux,其能力就是在socket中增加一个dst字段作为缓存路由,skb在查找路由前优先查找socket,找到的话就把缓存的dst设置到skb,那么再去查找路由的时候发现已经有dst了,就省去查找路由的过程。 复制粘贴点题外话:

所有的路由器设计都要遵循以下规则:
IF 目的地址配置在本机
    THEN 本机接收
ELSE
    查找路由表并在找到路由的情况下转发
END

回来继续看ip_route_connect,初始化完fl4后会有一个判断,因为src是没有设置的,所以会自动进入到__ip_route_output_key的流程,接着在ip_route_output_key_hash中填充一些fl4的数据然后进入到ip_route_output_key_hash_rcu中。

内核从3.6开始,就没有了ip_route_output_slow这个函数了,因为kernel 3.6以后去除了针对路由cache的支持,数据想要发送出去就必须查找路由表,但是如果完全依靠即时的alloc明显会造成巨大的内存开销,因此在此之后开始缓存查找结果的nexthop

函数开始便是三个路由条件判断:

if (fl4->saddr) {  //来源地址,为空
if (fl4->flowi4_oif) {   //出口设备,为0
if (!fl4->daddr) {  //目标地址,为用户定义地址

三个条件都没有符合,直接进入到err = fib_lookup(net, fl4, res, 0);函数,这个是路由转发表检索函数,fl4是查找条件,而res是路由查找结果。

因为此时的来源地址为空,那么策略路由的from关键字将无法匹配到所有没有bind地址的程序发出的数据包,实际上来说这就导致不会进入到任何一张自定义策略路由表中。

简单点说,策略路由就是根据来源地址目的地址入接口出接口等元素决定数据包在路由前是否进入到该张策略路由表

实际分析代码,在fib_lookup中会优先判断是否有(自定义?)路由策略(fib_has_custom_rules),从而判断如何进行路由查询。 这儿涉及到一部分策略路由初始化(fib_rules_init)相关的东西,分为非协议相关初始化和协议相关初始化,非协议相关主要为操作,协议相关主要为网络。 例如AF_INET协议族初始化过程中关于三张默认策略路由规则的初始化:

static int fib_default_rules_init(struct fib_rules_ops *ops)
{
 int err;


 err = fib_default_rule_add(ops, 0, RT_TABLE_LOCAL, 0);
 if (err < 0)
  return err;
 err = fib_default_rule_add(ops, 0x7FFE, RT_TABLE_MAIN, 0);
 if (err < 0)
  return err;
 err = fib_default_rule_add(ops, 0x7FFF, RT_TABLE_DEFAULT, 0);
 if (err < 0)
  return err;
 return 0;
}

套用别人的总结

策略路由的初始化做的事情其实很清晰:
对于非协议相关的初始化:
    1. 向RT_NETLINK注册三个子命令,分别用于策略路由的增加、删除和查询;
    2. 初始化管理各个协议族fib_rules_ops对象的链表和锁;
    3. 向网络设备接口层注册回调,当网卡状态发生变化时,能够更新策略路由数据库;
对于协议族相关的初始化,以AF_INET为例:
    1. 向框架注册自己的fib_rules_ops对象;
    2. 创建三条默认的策略路由, 分别用于查询local、main和default表;

这儿要注意三条默认的ruleaction都是FR_ACT_TO_TBL, AF_INET协议族初始化后会设置net->ipv4.fib_has_custom_rules = false;,但是我们手动fib_nl_newrule一下(sudo ip rule add fwmark 3 table local),让代码逻辑进入到__fib_lookup,其中函数内部涉及到一个l3mdev设备的逻辑,是4.8内核开始引入的新机制,如果没有设置VRF的话可以暂时不用看。那么实际的路由逻辑在fib_rules_lookup之中。

fib_nl_newrule会调用fib4_rule_configure,然后设置net->ipv4.fib_has_custom_rules = true;,其实我这儿还没有搞清楚默认的到底是false还是true,但是手动置为true

err = fib_rules_lookup(net->ipv4.rules_ops, flowi4_to_flowi(flp), 0, &arg);

net->ipv4.rules_ops中的rules_list链表保存了当前net namespace所有的路由策略。通过list_for_each_entry_rcu遍历链表,然后通过fib_rule_match匹配策略。 这个可以先看一下当前情况:

$ip rule
0: from all lookup local
32765: from all fwmark 0x3 lookup local
32766: from all lookup main
32767: from all lookup default

而关于函数match的方法

通用规则匹配
1)如指定入接口,数据包的入接口必须和策略rule中指定的入接口相同; //iif eth1
2)如指定出接口,二者的出接口必须相同;  //oif eth0
3)如指定流标记,二者的流标记必须相同; //fwmark 0x3
4)如指定隧道ID,二者的隧道ID必须相同; //tunid


另外,针对IPv4协议:
5)策略rule中指定的源IP地址与数据包的源IP地址,与掩码进行位与操作,结果必须相同;//from
5)策略rule中指定的目的IP地址与数据包的目的IP地址,与掩码进行位与操作,结果必须相同; //to
7)如指定TOS值,二者的TOS值必须相同; //服务类型,即指定服务的流量 //tos

首先遍历到的0: from all lookup local在进入到fib4_rule_match后会因为什么都没有设置而直接返回1,也就是直接匹配到。此刻本条规则的大致数据如下:

r->action = FR_ACT_TO_TBL;
r->pref = 0;
r->table = RT_TABLE_LOCAL;
r->flags = 0;

匹配到规则后返回至fib_rules_lookup,判断下action接后执行ops->action的操作,此刻应该是fib4_rule_action,在没有使用l3mdev的情况下,使用rule->table作为table id,然后调用fib_get_table获取该表,最后通过fib_table_lookup进行路由项查找。

kernel支持两种FIB的存储方式:一种hash,一种单词查找树trie,通过内核编译选项CONFIG_IP_FIB_HASHCONFIG_IP_FIB_TRIE决定,不过新版kernel取消了针对hash的支持

看一下我自己的LOCAL表信息:

broadcast 127.0.0.0 dev lo proto kernel scope link src 127.0.0.1 
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1 
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1 
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1 
broadcast 192.168.40.0 dev wlp3s0 proto kernel scope link src 192.168.43.42 
local 192.168.43.42 dev wlp3s0 proto kernel scope host src 192.168.43.42 
broadcast 192.168.47.255 dev wlp3s0 proto kernel scope link src 192.168.43.42 

而关于查找算法,需要知道关于Internet路由之路由表查找算法概述-哈希/LC-Trie树/256-way-mtrie树的前置知识,这个我不会,甚至fib_table_lookup这个函数我都看不太明白,因此只能换个方法去反推,最后的结果是返回的err,搜了一下整个函数中只有一个关于err的赋值:

err = fib_props[fa->fa_type].error;

关于这个数组内容如下:

const struct fib_prop fib_props[RTN_MAX + 1] = {
 [RTN_UNSPEC] = {
  .error = 0,
  .scope = RT_SCOPE_NOWHERE,
 },
 [RTN_UNICAST] = {
  .error = 0,
  .scope = RT_SCOPE_UNIVERSE,
 },
 [RTN_LOCAL] = {
  .error = 0,
  .scope = RT_SCOPE_HOST,
 },
 [RTN_BROADCAST] = {
  .error = 0,
  .scope = RT_SCOPE_LINK,
 },
 [RTN_ANYCAST] = {
  .error = 0,
  .scope = RT_SCOPE_LINK,
 },
 [RTN_MULTICAST] = {
  .error = 0,
  .scope = RT_SCOPE_UNIVERSE,
 },
 [RTN_BLACKHOLE] = {
  .error = -EINVAL,
  .scope = RT_SCOPE_UNIVERSE,
 },
 [RTN_UNREACHABLE] = {
  .error = -EHOSTUNREACH,
  .scope = RT_SCOPE_UNIVERSE,
 },
 [RTN_PROHIBIT] = {
  .error = -EACCES,
  .scope = RT_SCOPE_UNIVERSE,
 },
 [RTN_THROW] = {
  .error = -EAGAIN,
  .scope = RT_SCOPE_UNIVERSE,
 },
 [RTN_NAT] = {
  .error = -EINVAL,
  .scope = RT_SCOPE_NOWHERE,
 },
 [RTN_XRESOLVE] = {
  .error = -EINVAL,
  .scope = RT_SCOPE_NOWHERE,
 },
};

这个赋值在一个链表的循环里面,而前置条件是:

  if ((BITS_PER_LONG > KEYLENGTH) || (fa->fa_slen < KEYLENGTH)) {
   if (index >= (1ul << fa->fa_slen))
    continue;
  }
  if (fa->fa_tos && fa->fa_tos != flp->flowi4_tos)
   continue;
  if (fi->fib_dead)
   continue;
  if (fa->fa_info->fib_scope < flp->flowi4_scope)
   continue;

上述循环找到一条具体的路由,至于判断条件基本都是看最后一个,也就是关于scope的长度。 flp->flowi4_scope初始化是RT_SCOPE_UNIVERSE=0,然后在ip_route_output_key_hash中设置fl4->flowi4_scope = ((tos & RTO_ONLINK) ?RT_SCOPE_LINK : RT_SCOPE_UNIVERSE);也还是RT_SCOPE_UNIVERSE(因为IP_TOS default=0),这个条件就是任何路由都会满足。

如果设置了MSG_DONTROUTE,则TOS = RTO_ONLINK,从而导致scope = RT_SCOPE_LINK

往下走是叶子节点的链表遍历。 fafib_alias对应的是一条路由,多个fib_alias可以共享一个相同的fib_info,这是真实路由信息,比如设备,下一跳什么的,而其中的fib_info->fib_nh[nhsel]代表了下一跳地址。这儿的nhsel一般是1,除非是多路径支持,不然一条路由一般只有一个下一跳。 那么从上往下到return err需要过的条件有:

   if (fi->fib_flags & RTNH_F_DEAD) // fi->fib_flags != RTNH_F_DEAD(失效的地址),所以先刷新路由。
    continue;
   if (nh->nh_flags & RTNH_F_DEAD)  // nh->nh_flags != RTNH_F_DEAD
    continue;
   if (in_dev &&   // fib_flags == arg->flags == 0
       IN_DEV_IGNORE_ROUTES_WITH_LINKDOWN(in_dev) &&
       nh->nh_flags & RTNH_F_LINKDOWN &&
       !(fib_flags & FIB_LOOKUP_IGNORE_LINKSTATE))
    continue;
   if (!(flp->flowi4_flags & FLOWI_FLAG_SKIP_NH_OIF)) {   //flp->flowi4_oif == 0
    if (flp->flowi4_oif &&
        flp->flowi4_oif != nh->nh_oif)
     continue;
   }

有好多路由本身的信息又要涉及到路由添加中的逻辑,那个后面再说吧。

而如上的逻辑其实都是写在found中的,那么得重新往前看,用到的比较重要的一个数据就是const t_key key = ntohl(flp->daddr);,实际上说来说去,还是地址比较在里面。 去除关于trie路由查找算法的相关知识,就会在此函数中根据目的地址找到一个路由项,然后填充结果:

local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1 

最后一直返回的err = 0,又因为res->type = RTN_LOCAL,且fl4->saddr = 0,所以fl4->saddr = 127.0.0.1fl4->flowi4_oif = loopback_dev,接着创建路由缓存条目__mkroute_output。 直接看到rt_set_nexthop(rth, fl4->daddr, res, fnhe, fi, type, 0, do_cache);,然后重新填充fl4的数据

/* Reset some input parameters after previous lookup */
static inline void flowi4_update_output(struct flowi4 *fl4, int oif, __u8 tos,
     __be32 daddr, __be32 saddr)
{
 fl4->flowi4_oif = oif;
 fl4->flowi4_tos = tos;
 fl4->daddr = daddr;
 fl4->saddr = saddr;
}

后面的流程又是个重复,就不跟了,直接退出来。

 if (!inet_opt || !inet_opt->opt.srr)
  daddr = fl4->daddr;


 if (!inet->inet_saddr)
  inet->inet_saddr = fl4->saddr;
 sk_rcv_saddr_set(sk, inet->inet_saddr);
 inet->inet_dport = usin->sin_port;
 sk_daddr_set(sk, daddr);

经过设置后,saddr = 127.0.0.1; daddr = 127.0.0.1; dport = 9999

rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
          inet->inet_sport, inet->inet_dport, sk);

动态设置端口。

sk_setup_caps(sk, &rt->dst);

设置TCP出口路由缓存,也就是先前一直说的dst,都整完了后就是发送第一个SYN包了。

参考资料