前面创建完bgp peer之后,peer是active的话就会开启bgp start timer,然后开始BGP状态机的协商,不然费那么大劲创建出来不做事情就就很尴尬。
状态机简介
下面是BGP的状态和事件驱动的定义:
bgp是基于tcp协议的,即包含了tcp协议的优点,因此上面的状态机也就跟tcp连接有一定的关系:
- tcp连接建立阶段的状态:Idle, Connect, Active
- tcp连接建立完成之后: OpenSent, OpenConfirm, Established
Idle
BGP协议初始时是处于Idle状态。在这个状态时,系统不分配任何资源,也拒绝所有进入的BGP连接。只有收到Start Event时,才分配BGP资源,启动ConnectRetry计时器, 启动对其它BGP对等体的传输层连接,同时也侦听是否有来自其它对等体的连接请求。
Connect
这个状态下,BGP等待TCP完成连接。若连接成功,本地清空ConnectRetry计时器,并向对等体发送OPEN报文,然后状态改变为OpenSent状态;否则,本地重置ConnectRetry计时器,侦听是否有对等体启动连接, 并移至Active状态。
Active
这个状态下,BGP初始化TCP连接来获得一个对等体。如果连接成功,本地清空ConnectRetry计时器,并向对等体发送OPEN报文,并转至OpenSent状态。
OpenSent
这个状态下,BGP等待对等体的OPEN报文。收到报文后对报文进行检查,如果发现错误, 本地发送NOTIFICATION报文给对等体,并改变状态为IDLE。如果报文正确,BGP发送KEEPALIVE报文,并转至OpenConfirm状态。
OpenConfirm
这个状态下,BGP等待KEEPALIVE或NOTIFICATION报文。如果收到KEEPALIVE报文,则进入Established状态,如果收到NOTIFICATION报文,则变为Idle状态。
Established
这 个 状 态 下,B G P 可 以 和 其 他 对 等 体 交 换 U P D A T E ,NOTIFICATION, KEEPALIVE报文。如果收到了正确的UPDATE或KEEPALIVE报文,就认为对端处于正常运行状态, 本地重置Hold Timer。如果收到NOTIFICATION报文,本地转到Idle状态。如果收到错误的UPDATE报文,本地发送NOTIFICATION报文通知对端,并改变本地状态为Idle。 如果收到了TCP拆链通知,本地关闭BGP连接,并回到Idle状态。
综上, 我们可以画出BGP的有限状态机:
BGP的FSM代码写的相当漂亮,使用当前状态 + EVENT作为FSM index,查找到对应的执行函数和下一个状态:
通过FSM数组,bgp_event_update让状态机转化的代码起到了化繁为简的神奇效果:
状态机转化
Peer初始状态是IDLE,那么对等体一旦start起来,就会进入各自的状态,在不同的状态下处理各自的事件消息。
-
IDLE
start定时器到期后,EVNET 为BGP_Start,在bgp_event_update函数里面,通过FSM的数组获取
next = Connect
func = bgp_start
bgp_start 做一些检查后,调用 bgp_connect 开始建立TCP的连接,bgp使用tcp连接,每个bgp实例自身是peer的一个tcp server端,同时也是peer的tcp client端,server端在bgp_create之后都建立自己的socket服务端开始监听179端口。
- peer->fd = vrf_sockunion_socket 创建连接peer的客服端的TCP fd,同时设置FD为非阻塞,因为FRR是事件驱动,不能由于fd阻塞导致线程给挂起。
- 如果配置了密码会添加TCP MD5签名验证选项。
if ((ret = setsockopt(sock, IPPROTO_TCP, TCP_MD5SIG, &md5sig,
sizeof md5sig));
上面的代码即完成MD5SIG选项。
如果md5值不正确或者密码错误,内核会丢弃当前的报文。
struct tcp_md5sig {
struct __kernel_sockaddr_storage tcpm_addr; /* address associated */
__u16 __tcpm_pad1; /* zero */
__u16 tcpm_keylen; /* key length */
__u32 __tcpm_pad2; /* zero */
__u8 tcpm_key[TCP_MD5SIG_MAXKEYLEN]; /* key (binary) */
};
在int tcp_v4_rcv(struct sk_buff *skb)函数里:
#ifdef CONFIG_TCP_MD5SIG
/*
* We really want to reject the packet as early as possible
* if:
* o We're expecting an MD5'd packet and this is no MD5 tcp option
* o There is an MD5 option and we're not expecting one
*/
if (tcp_v4_inbound_md5_hash(sk, skb))
goto discard_and_relse;
#endif
因此在服务端,直接由内核在tcp接收处理时就完成了签名验证。
- 更新TCP连接的源IP地址
最后会调用bind 绑定配置的源IP地址
- Connect 处理
最后调用sockunion_connect处理connect到服务端
由于fd 是no block的,所以调用connect触发TCP的三次握手,大概率不会立马成功(如果是阻塞的话,线程会被挂住),那小概率会返回成功是什么情况下会发生了??
按照connect的man帮助,返回值如下,查看解释,可以理解代码的返回值处理:
RETURN VALUE
If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropriately.
EINPROGRESS
The socket is nonblocking and the connection cannot be completed immediately. It is possible to select(2) or poll(2) for completion by selecting the socket for writing. After select(2) indicates writability, use getsock‐opt(2) to read the SO_ERROR option at level SOL_SOCKET to determine whether connect() completed successfully (SO_ERROR is zero) or unsuccessfully (SO_ERROR is one of the usual error codes listed here, explaining the reason for the failure).
所以当bgp_connect返回的时候,根据返回值的不同会做不同的处理:
- connect_success :触发TCP_connection_open 事件 BGP_EVENT_ADD(peer, TCP_connection_open);
- connect_in_progress:说明connect并没有成功,会触发fd的可读和可写的事件(根据前面man的说明可以理解这点),bgp_connect_check 回调函数根据getsockopt(peer->fd, SOL_SOCKET, SO_ERROR, (void *)&status, &slen);来判断connect链接是否OK。
然后函数直接返回,最后达到bgp_event_update,会把peer的状态改成Connect (connect触发TCP链接不是立马成功的情况下)
Connect
当调用connect触发TCP的三次握手的时候,connect没有阻塞而是直接返回了,然后添加了FD可读和可写的事件,回调函数是bgp_connect_check函数,然后调用getsockopt 获取TCP connection的情况,如果status返回的是0,那么说明TCP connection is established,然后触发 TCP_connection_open事件:
而此时peer的状态是connect,根据全局的FSM可以知道:
下一个状态:OpenSent
回调函数:bgp_connect_success
bgp_connect_success 主要完成:
- 开启IO线程对peer fd的可读事件,当对端发送消息到peer的时候,IO线程就会被唤醒,调用回调函数bgp_process_reads处理报文
- 构造OPEN 报文,并开启IO线程对peer fd的可写事件,当本端发送消息到peer的时候,IO线程就会被唤醒,调用回调函数bgp_process_writes处理报文
当函数返回到bgp_event_update,会把peer的状态改成OpenSend
IO线程被唤醒,执行bgp_process_writes,然后调用bgp_write 把OPEN消息发送到对端。
OpenSend
IO线程此时睡眠,等待对端OPEN报文的到来,当收到报文后,IO线程就会被唤醒,调用回调函数bgp_process_reads处理报文
如果读取到了报文,那么放入peer的struct stream_fifo *ibuf; // packets waiting to be processed 里面,唤醒主线程取报文处理,主线程回调处理函数为bgp_process_packet
主线程唤醒执行bgp_process_packet 处理ibuf的报文,然后根据解析出来的BGP 报文类型,走不同的处理逻辑,本次我们关心的接受到OPEN的消息的处理,处理函数是bgp_open_receive
OPEN 报文包含各种能力值的验证,如果存在差错,则返回BGP_stop 事件,后续会发送Notification消息,并把状态迁移到IDLE状态,如果没有差错,则返回Receive_OPEN_message 事件,然后根据返回的事件处理状态机mprc的值本次就是Receive_OPEN_message
根据FSM数组可以定位到,收到OPEN消息后的处理,下一个状态是OpenConfirm,处理函数是:bgp_fsm_open
发送keeplive消息后,状态机转化为OpenConfirm
OpenConfirm
前面IO线程处理和bgp_process_packet处理都一样,只是此时OpenConfirm等待Keeplive或者Notification消息
事件是Receive_KEEPALIVE_message,根据FSM数组可以定位到,收到Keeplive消息后的处理,下一个状态是Established,处理函数是:bgp_establish
bgp_establish 会做很多事情,包含peer很多字段的赋值和一些处理,目前还不是很理解,等后续理解清楚了,在来补充