Unix 时间戳前一文《aodv之一》是记录了aodv内核模块kaodv的实现机制和工作过程,因为内核模块使用netlink与用户空间进行通信,所以下面这个文章记录自己阅读aodv用户层代码的一些理解
首先定义一些变量,必须的变量有网络设备名ifname,文件描述符集rfds,readers,一个守护进程变量,两个时间结构体,struct timeval *timeout; struct timespec timeout_spec;前者是秒与微妙级别的,后者是秒与纳秒级别的,struct sigacton sigact 用于处理关联信号与回调函数。sigset_t mask, origmask是信号集。
- 运行程序的一些配置
第一步是获取本程序名称。
第二步就是关联一些会影响到程序的信号,如SIGTERM,SIGHUP,SIGINT还有SIGSEGV,不过关联的处理程序signal_handler并没有做任何的处理,只是输出一些信息。虽然这样设置了,但是后面程序又使用sigprocmask(SIG_BLOCK, &mask, &origmask)阻塞对应的信号,感觉和前面设计关联处理函数实现一样的功能,好像重复了(有待研究)
第三步就是解析命令行参数,主要包括:
-d 为关闭调试模式同时使进程处于守护进程模式
-f 设置链路层返回,应该是用于确认链路有效的,链路有效路由默认设为10000(10s)
-g rreq_gratuitous使能RREQ的功能,就是要求RREQ达到目的节点的时候才返回RREP,而不是遇到可到目的节点的节点就返回RREP(还是返过来?)
-i 设置网络设备接口的名字,默认是第一个无线接口
-j 触发hello_jittering(默认启动)
-l log调试输出到文件
-n 设置接收到多少个hellos才把节点当作邻居节点,默认不少于2
-o 是否优化hello,如果优化的话,仅在转发数据的情况下发送hello(处于试验性的)
-q 对于aodv的控制数据packet(RREQ,RREP,RERR)设置最小的信号质量阈值
-r 每N秒log输出路由表(默认0,关闭路由表输出)
-u 检测和避免不对称链路(试验性的)
-w 试验性的,使能网关模式支持
-x 失能对于RREQ的扩展令牌搜索
-L 使能local repair
-D 失能重启后15秒的延迟,也就是说默认重启后有15秒的延迟处理
-R 触发速率限制(RREQs,RERRs),默认使能速率限制
-V 打印AODV-UU版本
- 程序是否为守护进程
geteuid()判断该进程是否以root权限运行,因为要处理内核通信,同时对相关设备的操作,必须运行于root权限
如果要以守护进程的方式运行,必须fork()子进程,父进程退出,关闭stdout,stderr,同时setsid()把该子进程设置为新的会话的领头进程
,使其与父进程的会话组和进程组脱离,同时与终端脱离。 - 进一步初始化
struct routing_table{
unsigned int num_entries;
unsigned int num_active;
list_t tbl[RT_TABLESIZE];
}
对路由表的处理是路由条目和有效路由都为0,路由表为哈希循环双向路由链表
log 初始化,这里不讨论ns的仿真,只看实现
默认log终端输出,否则log_to_file为1,则在文件/var/log/aodvd.log输出调试信息,如果使能路由表信息输出,则在/var/log/aodvd.rtlog文件输出信息
主机初始化host_init
struct host_info{
u_int32_t seqno;//序列号
struct timeval bcast_time;//最近一次广播时间
struct timeval fwd_time;//最近一次转发数据packet时间
u_int32_t rreq_id;//RREQ id
int nif;//接口号
struct dev_info devs[MAX_NR_INTERFACES+1];//设备信息
}
struct dev_info{
int enabled;/* 1 if struct is used, else 0 */
int sock;
#ifdef CONFIG_GATEWAY
int psock;/* Socket to send buffered data packets. */
#endif
unsigned int ifindex;
char ifname[IFNAMSIZ];
struct in_addr ipaddr;/* The local IP address */
struct in_addr netmask;/* The netmask we use */
struct in_addr broadcast;
}
如果设备名为空,我们搜索第一个无线设备
struct ifconf{
int ifc_len;/*size of buffer*/
union{
char *ifcu_buf;
struct ifreq *ifcu_req;
}ifc_ifcu;
};
struct ifreq{
#define IFHWADDRLEN 6
union{
char ifrn_name[IFNAMSIZ];/*if name, e.g. "en0"*/
}ifr_ifrn;
union{
struct sockaddr ifru_addr;
struct sockaddr ifru_dstaddr;
struct sockaddr ifru_broadaddr;
struct sockaddr ifru_netmask;
struct sockaddr ifru_hwaddr;
short ifru_flags;
int ifru_ivalue;
int ifru_mtu;
struct ifmap ifru_map;
char ifru_slave[IFNAMSIZ]; /* Just fits the size */
char ifru_newname[IFNAMSIZ];
void * ifru_data;
struct if_settings ifru_settings;
}ifr_ifru;
}
通过ioctl(iw_sock, SIOCGIFCONF, &ifc)获取接口信息
struct iwreq
{
union
{
char ifrn_name[IFNAMSIZ]; /* if name, e.g. "eth0" */
}ifr_ifrn; /* Data part (defined just above)*/
};
iwreq和ifreq是一样的
union iwreq_data
{
/*Config - generic*/
char name[IFNAMSIZ];
/*Name: used to verify the presence of wireless extensions .
*Name of the protocol/provider...*/
struct iw_point essid;/* Extended network name */
struct iw_param nwid; /* network id (or domain - the cell) */
struct iw_freq freq;/* frequency or channel :* 0-1000 = channel
* > 1000 = frequency in Hz */
struct iw_param sens;/* signal level threshold */
struct iw_param bitrate;/* default bit rate */
struct iw_param txpower;/* default transmit power */
struct iw_param rts;/* RTS threshold threshold */
struct iw_param frag;/* Fragmentation threshold */
__u32 mode;/* Operation mode */
struct iw_param retry;/* Retry limits & lifetime */
struct iw_point encoding;/* Encoding stuff : tokens */
struct iw_param power;/* PM duration/timeout */
struct iw_quality qual;/* Quality part of statistics */
struct sockaddr ap_addr;/* Access point address */
struct sockaddr addr;/* Destination address (hw/mac) */
struct iw_param param;/* Other small parameters */
struct iw_point data; /* Other large parameters */
}
序列号初始化为1,rreq_id号为0,上一次的广播时间(第一次)为当前时间。接着就是根据接口名字获取接口ip地址,子网掩码,广播地址,标记相关索引结构体dev_info已被使用。然后装载kaodv.ko模块。装载内核模块主要采用了system命令,同时休眠1s
- 内核方面的设置
启用数据包转发功能/proc/sys/net/ipv4/ip_forward
关闭icmp重定向功能/proc/sys/net/ipv4/conf/eth0/send_redirects
/proc/sys/net/ipv4/conf/eth0/accept_redirects
还有
/proc/sys/net/ipv4/conf/all/send_redirects
/proc/sys/net/ipv4/conf/all/accept_redirects
关闭以上重定向功能,则不用经过路由器转发(如当你在同一个实体网络内假设一部路由器,但这个实体网络有两个IP网络,,例如192.168.0.0/24 192.168.1.0/24 这时192.168.0.100想要向192.168.1.100传送信息时,路由器可能会传送一个ICMP redirect封包告知192.168.0.100直接传送数据给192.168.1.100即可,而不需要透过路由器。) - netlink初始化
struct sockaddr_nl{
__kernel_sa_family_t nl_family; /*AF_NETLINK*/
unsigned short nl_pad;/*zero*/
__u32 nl_pid; /*port ID*/
__u32 nl_groups; /*multicast groups mask*/
}
自定义的结构体
struct nlsock{
int sock;
int seq;
struct sockaddr_nl local;
}
通过socket(PF_NETLINK, SOCK_RAW, NETLINK_AODV);创建netlink套接字,用于与内核通信
一个重要的函数就是attach_callback_func(aodvnl.sock, nl_kaodv_callback),
首先aodv应用程序限制了5个回调函数的上限数量,而函数attach_callback_func就是判断回调函数数量是否已达上限,同时未达上限时对全局变量callbacks[]赋值,就是设置对应的文件描述符和回调函数。所以这里就是把aodvnl.sock文件描述符与其回调函数nl_kaodv_callbac结合起来。这里有一个结构体kaodv_rt_msg(kaodv_rt_msg_t)
typedef struct kadov_rt_msg{
u_int8_t type;
u_int32_t src;
u_int32_t dst;
u_int32_t nhop;
u_int32_t flags;
int ifindex;
long time;
}kaodv_rt_msg_t;
struct nlmsghdr{
__u32 nlmsg_len;/* Length of message including header */
__u16 nlmsg_type;/* Message content */
__u16 nlmsg_flags;/* Additional flags */
__u32 nlmsg_seq;/* Sequence number */
__u32 nlmsg_pid;/* Sending process port ID */
}
struct nlmsgerr{
int error;
struct nlmsghdr msg;
}
在aodv用户空间中处理的nl信息类型有NLMSG_ERROR,KAODVM_DEBUG,KAODVM_TIMEOUT,KAODVM_ROUTE_REQ,KAODVM_REPAIR,KAODVM_ROUTE_UPDATE,KAODVM_SEND_RERR。
对于NLMSG_ERROR,处理的方式就是输出调试信息“NLMSG——ERROR, error=%d type=%s“,其中type有Add route,Delete route,Timeout,Route Request, Route repair, No route found,Route update, Send route error,Configuration,Debug
对于KAODVM_DEBUG,输出类似与”kaodv:%s”的调试信息,%s为具体的数据信息.
对于KAODVM_TIMEOUT,输出调试信息”Got TIMEOUT msg from kernel for %s”,%s为对应目的节点的ip地址,同时对路由表进行timeout操作,在aodv中路由表条目如下
这个是自定义的结构体
struct rt_table{
list_t l;
struct in_addr dest_addr;/* IP address of the destination */
u_int32_t dest_seqno;
unsigned int ifindex;/* Network interface index... */
struct in_addr next_hop;/* IP address of the next hop to the dest */
u_int8_t hcnt;/* Distance (in hops) to the destination */
u_int16_t flags; /* Routing flags */
u_int8_t state;/* The state of this entry */
struct timer rt_timer;/* The timer associated with this entry */
struct timer ack_timer;/* RREP_ack timer for this destination */
struct timer hello_timer;
struct timer last_hello_time;
u_int8_t hello_cnt;
hash_value hash;
int nprec;/* Number of precursors */
list_t precursors; /* List of neighbors using the route */
};
struct timer{
list_t l;
int used;
struct timeval timeout;
timeout_func_t handler;
void *data;
};
其中如果找到匹配的路由条目的state为有效的(VALID),则意味者之前并没有出现timeout情况,所以要对该路由进行处理,如该路由为到目的节点的距离为1,即rt->hcnt == 1,则表示目的节点是本节点的邻居,利用函数neighbor_link_break处理(移除路由条目上定时器结构体(rt_timer,hello_timer,ack_timer),路由条目设为无效,路由表的有效路由数目减1,该路由条目的hello计数清0,对应的路由条目的序列号+1,路由条目的上一次hello time清零,向内核发送删除对应路由的信息KAODVM_DELROUTE,处理向内核发送KAODVM_DELROUTE,还有更进一步的操作内核路由表(这个是系统内核路由表,就是说除了kaodv内核模块的路由表,还要处理系统的静态路由表))
struct rtmsg {
unsigned char rtm_family;
unsigned char rtm_dst_len;
unsigned char rtm_src_len;
unsigned char rtm_tos;
unsigned char rtm_table;
unsigned char rtm_protocol;
unsigned char rtm_scope;
unsigned char rtm_type;
unsigned rtm_flags;
}
具体上面的数据结构表示什么,看参考文件/usr/include/linux/rtnetlink.h
如果定义了网关,则也要把路由表中下一跳是该网关地址的路由删除掉,由于网关路由应该有很多邻居,所以也要删除掉该路由条目的前驱(邻居),无论如何,对应路由条目的前驱(邻居)要删除掉。
同时要查找整个路由表,路由条目有效的前提下,如果路由条目的下一跳是对应要删除的目的节点地址,或者该路由条目的目的节点地址对应要删除的目的节点地址
对于KAODVM_ROUTE_REQ路由请求,rreq_route_discovery(dest_addr, 0, NULL)发起路由请求,这里涉及了一个数据结构
/* This is a list of nodes that route discovery are performed for */
typedef struct seek_list{
list_t l;
struct in_addr dest_dest;
u_int32_t dest_seqno;
struct ip_data *ipd;
u_int8_t flags;/* The flags we are using for resending the RREQ */
int reqs;
int ttl;
struct timer seek_timer;
}seek_list_t;
在这里,要注意的一种情况就是gratuitous RREP,当指示解决的问题是,如果RREQ发送到达一个中继节点node,该node有到目的节点的路由的情况下,如果没有设置Destination Only标记,则该节点会返回RREP给源节点,但是这种情况下目的节点并不知道达到源节点的路由路径,如果是TCP建立握手连接的话,目的节点就要发起(originating node)RREQ,因此,设置gratuitous RREP,则该中继节点会向目的节点单播RREP,告诉目的节点有到源节点的路由.
每发送一个rreq,在seek_list_t中插入记录,防止多操作。在插入seek_list_t中,还对其路由发现的超时做了相对应处理timer_init(&entry->seek_timer, &NS_CLASS route_discovery_timeout, entry);
在函数route_discovery_timeout中,如果rreq发送没有超出RREQ_RETRIES(2),则继续重发,如果超出了这个次数,则利用netlink向内核路由发送KAODVM_NOROUTE_FOUND信息.同时用户空间查找路由表条目,如果有该路由而且该路由处于RT_REPAIR则报告路由修复失败同时发送相关的RRER广播信息。
KAODVM_REPAIR
执行rreq_local_repair(fwd_rt, src_addr, NULL)进行路由修复,主要也是发送相应的rreq
KAODVM_ROUTE_UPDATE
根据输入的数据packet和输出的数据packet,更新到源节点和到目的节点路由条目的时间有效性
KAODVM_SEND_RERR
发送rerr的广播数据包
rtnl是用于修改内核路由表的socket,不是kaodv.ko的,而是系统的
RTMGRP_NOTIFY|RTMGRP_IPV4_IFADDR|RTMGRP_IPV4_ROUTE,为了实时监控本机路由表的,需要创建以上参数的netlink套接字
aodv中,该netlink sock是和内核通信的,主要处理的路由事件有
NLMSG_ERROR/RTM_NEWLINK/RTM_NEWADDR/RTM_NEWROUTE,
nl_rt_callback该函数只是处理内核发送的一些提示信息,没有做什么实质性的东西,但是该sock可能在后续修改路由表中会有用到
函数nl_send_conf_msg主要是向kaodv内核模块发送KAODVM_CONFIG配置信息,主要配置的信息有active_route_timeout/qual_th/is_gateway
默认的阈值qual_threshold是0,这个值会影响到AODV控制信息的接收,active_route_timeout默认为3000,即3s,is_gateway默认不开启。
aodv_socket_init:创建sock UDP套接字(用于aodv socket),如果define CONFIG_GATEWAY,则创建sock IPPROTO_RAW(Data packet send socket)
对aodv socket还要设置允许广播,因为这些socket专门用于传送RREQ,RREP(单播),RRER的广播包的。还有设置很多其他参数如SO_BINDTODEVICE/SO_PRIORITY/IP_RECVTTL/IP_PKTINFO/SO_BINDTODEVICE/SO_SNDBUF/SO_RCVBUF等
最后也设置了回调函数aodv_socket_read
llf_init,如果设置了llfeedback,就是通过link来感知邻居节点的话,创建netlink socket用于通信
以上初始完之后,进入死循环,不断地检查定时器过期与否与检测那些文件描述符是可读的。
在这里使用select来监控所有创建的socket文件描述符,readers记录了所有的文件描述符,是一个文件描述符集合
函数timer_age_queue()就是询问定时器队列过期的情况,检查到过期的定时器就去执行对应的函数,没有过期的就返回离第一个就要过期的定时器的剩余时间值。
pselect函数使用的超时值就是上面第一个定时器要过期的剩余时间值,因此,对定时器或可读描述符都能及时响应。