本文只分析ip选项中的宽松路由选项和严格路由选项
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源路径选项