1.ip头中选项格式
由于IP首部中可以存在选项,且可以同时存在多个选项,因此IP首部的长度是可变的,IPv4允许选项最长可达40字节。选项的格式有单字节和多字节两种,单字节的即只包括一个字节的选项类型,而多字节的则除一个字节的类型之外,还包括选项长度以及选项数据等。
多字节的选项格式如下所示:
所有选项都以1字节类型(type)字段开始。在多字节选项中,类型字段后面紧接着一个长度(len)字段,其他的字节是数据(data)。许多选项数据字段的第一个字节是1字节的位移(offset)字段,指向数据字段内的某个字节。长度字节的计算覆盖了类型、长度和数据字段。类型被继续分成三个子字段: 1 bit 备份(copied )标志、2 bit 类(class)字段和5 bit 数字(number)字段。
2.linux内核存储ip选项的结构
- struct ip_options {
- __u32 faddr;//存在宽松路由或严格路由选项时,用来记录吓一跳的IP地址
- unsigned char optlen;//标识IP首部中选项所占的字节数
- unsigned char srr;//记录宽松路由或严格路由选项在IP首部中的偏移量,即选项的第一个字节的地址
- 减去IP首部的第一个字节的地址
- unsigned char rr;//用于记录记录路径选项在IP首部中的偏移量
- unsigned char ts;
- unsigned char is_setbyuser:1,
- is_data:1,
- is_strictroute:1,
- srr_is_hit:1,
- is_changed:1,
- rr_needaddr:1,
- ts_needtime:1,
- ts_needaddr:1;
- unsigned char router_alert;
- unsigned char cipso;
- unsigned char __pad2;
- unsigned char __data[0];//若选项有数据则从该字段开始,使之紧跟在ip_options结构后面,最多不
- 超过40字节
- };
3.宽松路由选项(LSRR)和严格路由选项(SSRR)
LSRR在选项的ip地址列表中并不列出一条完备而严格的路径,而是只给出路径中的某些关键点。在关键的之间可以通过路由器的自动路由选择功能进行路由,此选项在数据包分片的时候也必须被复制。
SSRR选项要求数据包必须严格按照发送方规定的路径经过每一个路由器,这些路由器应该是一一相连的,每两个指定的路由器之间不能有其他未指定的路由器,且路由器的顺序是不能改变的。如果数据包在传输时无法直接到达下一跳指定的路由器,路由器就会丢弃该数据包,然后产生一个源路由失败的目的不可达的ICMP差错报文报告给发送方。
内核定义的ip选项类型值:
- /* IP options */
- #define IPOPT_COPY 0x80
- #define IPOPT_CLASS_MASK 0x60
- #define IPOPT_NUMBER_MASK 0x1f
- #define IPOPT_COPIED(o) ((o)&IPOPT_COPY)
- #define IPOPT_CLASS(o) ((o)&IPOPT_CLASS_MASK)
- #define IPOPT_NUMBER(o) ((o)&IPOPT_NUMBER_MASK)
- #define IPOPT_CONTROL 0x00
- #define IPOPT_RESERVED1 0x20
- #define IPOPT_MEASUREMENT 0x40
- #define IPOPT_RESERVED2 0x60
- #define IPOPT_END (0 |IPOPT_CONTROL)
- #define IPOPT_NOOP (1 |IPOPT_CONTROL)
- #define IPOPT_SEC (2 |IPOPT_CONTROL|IPOPT_COPY)
- #define IPOPT_LSRR (3 |IPOPT_CONTROL|IPOPT_COPY)//宽松路由的type值,即0x83
- #define IPOPT_TIMESTAMP (4 |IPOPT_MEASUREMENT)
- #define IPOPT_CIPSO (6 |IPOPT_CONTROL|IPOPT_COPY)
- #define IPOPT_RR (7 |IPOPT_CONTROL)
- #define IPOPT_SID (8 |IPOPT_CONTROL|IPOPT_COPY)
- #define IPOPT_SSRR (9 |IPOPT_CONTROL|IPOPT_COPY)//严格路由的type值,即0x89
- #define IPOPT_RA (20|IPOPT_CONTROL|IPOPT_COPY)
4.分析一个完整的ip选项处理流程(从setsockopt设置选项到发送syn、路由节点接收syn到转发syn及目的端接收syn到发送synack的ip选项的处理过程)
(1)从setsockopt设置选项到发送syn
用户层socket编程可以通过setsockopt来设置ip选项,在connect前设置选项值
- setsockopt(sockfd,IPPROTO_IP,IP_OPTIONS,(void*)opt,optlen);
setsockopt系统调用在内核中的实现,之前已经介绍过了。现在我们直接跳到函数
- int ip_setsockopt(struct sock *sk, int level,
- int optname, char __user *optval, int optlen)
- {
- ......
- err = do_ip_setsockopt(sk, level, optname, optval, optlen);//IPPPROTO_IP的处理函数
- ......
- }
- static int do_ip_setsockopt(struct sock *sk, int level,
- int optname, char __user *optval, int optlen)
- {
- struct inet_sock *inet = inet_sk(sk);
- ......
- lock_sock(sk);
- switch (optname) {
- case IP_OPTIONS://ip_options选项处理
- {
- struct ip_options * opt = NULL;
- if (optlen > 40 || optlen < 0)
- goto e_inval;
- err = ip_options_get_from_user(&opt, optval, optlen);
- if (err)
- break;
- if (inet->is_icsk) {
- struct inet_connection_sock *icsk = inet_csk(sk);
- ......
- if (inet->opt)
- icsk->icsk_ext_hdr_len -= inet->opt->optlen;
- if (opt)
- icsk->icsk_ext_hdr_len += opt->optlen;
- icsk->icsk_sync_mss(sk, icsk->icsk_pmtu_cookie);
- ......
- }
- opt = xchg(&inet->opt, opt);//将opt与inet->opt交换;现在inet->opt中存储了我们解
- 析过的选项的值,inet->opt->faddr为下一跳的地址
- kfree(opt);
- break;
- }
- ......
- }
- int ip_options_get_from_user(struct ip_options **optp, unsigned char __user *data, int optlen)
- {
- struct ip_options *opt = ip_options_get_alloc(optlen);
- if (!opt)
- return -ENOMEM;
- if (optlen && copy_from_user(opt->__data, data, optlen)) {// 应用层数据拷贝到opt->__da
- ta指向的内存处
- kfree(opt);
- return -EFAULT;
- }
- return ip_options_get_finish(optp, opt, optlen);//解析opt信息并用optp返回
- }
- static int ip_options_get_finish(struct ip_options **optp,
- struct ip_options *opt, int optlen)
- {
- while (optlen & 3)
- opt->__data[optlen++] = IPOPT_END;//如IP选项内容不是以四字节对齐的,则将未对齐部分用选
- 项列表结束符填充,使之对齐
- opt->optlen = optlen;
- opt->is_data = 1;
- opt->is_setbyuser = 1;
- if (optlen && ip_options_compile(opt, NULL)) {//解析IP选项信息块各字段值
- kfree(opt);
- return -EINVAL;
- }
- kfree(*optp);
- *optp = opt;
- return 0;
- }
- int ip_options_compile(struct ip_options * opt, struct sk_buff * skb)
- {
- ......
- switch (*optptr) {
- case IPOPT_SSRR:
- case IPOPT_LSRR:
- if (optlen < 3) {//1字节type,1字节len,1字节offset
- pp_ptr = optptr + 1;
- goto error;
- }
- if (optptr[2] < 4) {//offset至少为4,指向第一个ip
- pp_ptr = optptr + 2;
- goto error;
- }
- /* NB: cf RFC-1812 5.2.4.1 */
- if (opt->srr) {
- pp_ptr = optptr;
- goto error;
- }
- if (!skb) {
- if (optptr[2] != 4 || optlen < 7 || ((optlen-3) & 3)) {//至少能容下一个ip
- pp_ptr = optptr + 1;
- goto error;
- }
- memcpy(&opt->faddr, &optptr[3], 4);//取出第一个地址作为下一跳地址,
- opt->faddr为下一跳的地址
- if (optlen > 7)
- memmove(&optptr[3], &optptr[7], optlen-7);//在路径列表中多于一个地址
- 时,将剩余的所有地址往前移动4个字节
- }
- opt->is_strictroute = (optptr[0] == IPOPT_SSRR);//记录是否为严格路由选项
- opt->srr = optptr - iph;//记录源路由选项在IP首部的偏移量
- break;
- ......
- }
options的offset项没有修改,它指向了源地址路由的下一个节点,因为此时第一个路由节点已经从选项中删除,其他路由节点向前移动了4个字节
应用层socket编程connect函数将执行系统调用sys_connect
- asmlinkage long sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
- {
- struct socket *sock;
- ......
- err = sock->ops->connect(sock, (struct sockaddr *) address, addrlen,
- sock->file->f_flags);
- ......
- }
sock->ops指向注册的proto_ops钩子,此时为tcp_prot
- struct proto tcp_prot = {
- .name = "TCP",
- ......
- .connect = tcp_v4_connect,
- ......
- };
- int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
- {
- struct inet_sock *inet = inet_sk(sk);
- struct tcp_sock *tp = tcp_sk(sk);
- struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
- ......
- nexthop = daddr = usin->sin_addr.s_addr;
- if (inet->opt && inet->opt->srr) {//将临时变量下一跳地址和目的地址值都暂时设置为connect参
- 数中的地址,如果使用源地址路由,则将下一跳地址设置为IP选项中的faddr
- if (!daddr)
- return -EINVAL;
- nexthop = inet->opt->faddr;
- }
- tmp = ip_route_connect(&rt, nexthop, inet->saddr,//查找路由缓存项
- RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
- IPPROTO_TCP,
- inet->sport, usin->sin_port, sk, 1);//
- if (tmp < 0) {
- if (tmp == -ENETUNREACH)
- IP_INC_STATS_BH(IPSTATS_MIB_OUTNOROUTES);
- return tmp;
- }
- ......
- err = tcp_connect(sk);//发送syn包
- rt = NULL;
- if (err)
- goto failure;
- ......
- }
- int tcp_connect(struct sock *sk)
- {
- ......
- buff = alloc_skb_fclone(MAX_TCP_HEADER + 15, sk->sk_allocation);//分配skbuff
- ......
- tcp_transmit_skb(sk, buff, 1, GFP_KERNEL);//构造tcp头和ip头并发送
- ......
- }
- static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask)
- {
- ......
- err = icsk->icsk_af_ops->queue_xmit(skb, 0);//构造ip头并发送,queue_xmit注册为
- ip_queue_xmit函数
- ......
- }
- struct inet_connection_sock_af_ops ipv4_specific = {
- .queue_xmit = ip_queue_xmit,
- ......
- };
- int ip_queue_xmit(struct sk_buff *skb, int ipfragok)
- {
- struct sock *sk = skb->sk;
- struct inet_sock *inet = inet_sk(sk);
- struct ip_options *opt = inet->opt;
- struct rtable *rt;
- struct iphdr *iph;
- ......
- /* Use correct destination address if we have options. */
- daddr = inet->daddr;
- if(opt && opt->srr)//如果使用源地址路由,下一跳为inet->opt->faddr,即选项中的第一个地址
- daddr = opt->faddr;
- {
- struct flowi fl = { .oif = sk->sk_bound_dev_if,
- .nl_u = { .ip4_u =
- { .daddr = daddr,
- .saddr = inet->saddr,
- .tos = RT_CONN_FLAGS(sk) } },
- .proto = sk->sk_protocol,
- .uli_u = { .ports =
- { .sport = inet->sport,
- .dport = inet->dport } } };
- /* If this fails, retransmit mechanism of transport layer will
- * keep trying until route appears or the connection times
- * itself out.
- */
- security_sk_classify_flow(sk, &fl);
- if (ip_route_output_flow(&rt, &fl, sk, 0))
- goto no_route;
- }
- sk_setup_caps(sk, &rt->u.dst);
- }
- skb->dst = dst_clone(&rt->u.dst);
- packet_routed:
- if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)//如果是严格路由选项,并且网关地址和路由的下一跳不一致则中断处理流程,这也是严格路由所要求的:下一跳必须是选项中的顺序的地址
- goto no_route;
- ......
- iph->saddr = rt->rt_src;
- iph->daddr = rt->rt_dst;//目的地址已经是路由的下一跳地址,即为选项中的第一个地址,即为
- inet->opt->faddr
- skb->nh.iph = iph;
- /* Transport layer set skb->h.foo itself. */
- if (opt && opt->optlen) {
- iph->ihl += opt->optlen >> 2;
- ip_options_build(skb, opt, inet->daddr, rt, 0);//将目的地址到options中最后一个ip地
- 址后面
- }
- ......
- }
发送syn的处理告一段落,将通过一个实例来体现这一流程:
实验一(LSRR):
客户端:192.168.18.73 路由节点1:192.168.18.71 路由节点2:192.168.18.72 服务器端:192.168.18.76 18.71上18.0网段有个网关192.168.18.79
18.73抓包(发送syn包)结果如下:
一个NOP是为了对齐而设置的。从图中可以看出目的地址改为了options中首个ip地址,偏移量指向了下一个路由节点地址,绝对目的地址被加入了options末尾。len为1个字节的type+1个字节len+1个字节的offset+2个四个字节的ip地址
如果在73上设定网关18.79后,syn包发送不受影响
实验二(SSRR):
如果设定18.79网关,syn包就发不出去了,原因在ip_queue_xmit函数中分析过了
参考资料:
tcp/ip详解:第9章 ip选项处理
linux内核源码剖析-tcp/ip实现:第12章 IP选项处理 作者:樊东东 莫澜
UNIX网络编程第一卷:27.3节 IP源路径选项
阅读(1102) | 评论(0) | 转发(0) |