内核中的TCP的追踪分析-8-TCP(IPV4)的socket连接

2020-05-25 00:00:00 函数 地址 路由 键值 路由表

我们继续探讨socket的连接,同样象在unix的socket章节一样,我们还是先从练习中的例子看起
connect(sockfd, (struct sockaddr *)&address, len)
在这个练习很明显是客户端的socket发起的,中间的过程我们不详细叙述了,需要了解的朋友请看我在博客中的关于unix的socket连接那篇文章
这里我们还是从err = sock->ops->connect(sock, (struct sockaddr *)address, addrlen,sock->file->f_flags); 看起
struct proto tcp_prot = {
。。。。。。
.connect = tcp_v4_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;
struct rtable *rt;
__be32 daddr, nexthop;
int tmp;
int err;
if (addr_len sizeof(struct sockaddr_in))
return -EINVAL;
if (usin->sin_family != AF_INET)
return -EAFNOSUPPORT;
nexthop = daddr = usin->sin_addr.s_addr;
if (inet->opt && inet->opt->srr) {
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;
}
if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
ip_rt_put(rt);
return -ENETUNREACH;
}
if (!inet->opt || !inet->opt->srr)
daddr = rt->rt_dst;
if (!inet->saddr)
inet->saddr = rt->rt_src;
inet->rcv_saddr = inet->saddr;
if (tp->rx_opt.ts_recent_stamp && inet->daddr != daddr) {
/* Reset inherited state */
tp->rx_opt.ts_recent = 0;
tp->rx_opt.ts_recent_stamp = 0;
tp->write_seq = 0;
}
我们知道参数uaddr是从用户空间传递过来的地址结构变量,这里转变成IP地址结构struct sockaddr_in,这种结构我们已经在以前看过了,首先是对这个地址结构的检测是否是IP协议,此后是检测地址类型,接着进入ip_route_connect()函数这是与路由相关的函数
static inline int ip_route_connect(struct rtable **rp, __be32 dst,
__be32 src, u32 tos, int oif, u8 protocol,
__be16 sport, __be16 dport, struct sock *sk,
int flags)
{
struct flowi fl = { .oif = oif,
.mark = sk->sk_mark,
.nl_u = { .ip4_u = { .daddr = dst,
.saddr = src,
.tos = tos } },
.proto = protocol,
.uli_u = { .ports =
{ .sport = sport,
.dport = dport } } };
int err;
struct net *net = sock_net(sk);
if (!dst || !src) {
err = __ip_route_output_key(net, rp, &fl);
if (err)
return err;
fl.fl4_dst = (*rp)->rt_dst;
fl.fl4_src = (*rp)->rt_src;
ip_rt_put(*rp);
*rp = NULL;
}
security_sk_classify_flow(sk, &fl);
return ip_route_output_flow(net, rp, &fl, sk, flags);
}
我们在函数头部看到一个数据结构struct flowi这是个专门用于路由的键值,我们先大概的看一下,在使用的过程中可以对照
struct flowi {
int oif;
int iif;
__u32 mark;
union {
struct {
__be32 daddr;
__be32 saddr;
__u8 tos;
__u8 scope;
} ip4_u;

struct {
struct in6_addr daddr;
struct in6_addr saddr;
__be32 flowlabel;
} ip6_u;
struct {
__le16 daddr;
__le16 saddr;
__u8 scope;
} dn_u;
} nl_u;
#define fld_dst nl_u.dn_u.daddr
#define fld_src nl_u.dn_u.saddr
#define fld_scope nl_u.dn_u.scope
#define fl6_dst nl_u.ip6_u.daddr
#define fl6_src nl_u.ip6_u.saddr
#define fl6_flowlabel nl_u.ip6_u.flowlabel
#define fl4_dst nl_u.ip4_u.daddr
#define fl4_src nl_u.ip4_u.saddr
#define fl4_tos nl_u.ip4_u.tos
#define fl4_scope nl_u.ip4_u.scope
__u8 proto;
__u8 flags;
union {
struct {
__be16 sport;
__be16 dport;
} ports;
struct {
__u8 type;
__u8 code;
} icmpt;
struct {
__le16 sport;
__le16 dport;
} dnports;
__be32 spi;
struct {
__u8 type;
} mht;
} uli_u;
#define fl_ip_sport uli_u.ports.sport
#define fl_ip_dport uli_u.ports.dport
#define fl_icmp_type uli_u.icmpt.type
#define fl_icmp_code uli_u.icmpt.code
#define fl_ipsec_spi uli_u.spi
#define fl_mh_type uli_u.mht.type
__u32 secid; /* used by xfrm; see secid.txt */
} __attribute__((__aligned__(BITS_PER_LONG/8)));
这里很显然我们需要先看参数才能搞明白路由函数的过程,从tcp_v4_connect函数传递的参数来看分别是rt,它是一个查询路由表使用的结构
struct rtable
{
union
{
struct dst_entry dst;
} u;
/* Cache lookup keys */
struct flowi fl;
struct in_device *idev;

int rt_genid;
unsigned rt_flags;
__u16 rt_type;
__be32 rt_dst; /* Path destination */
__be32 rt_src; /* Path source */
int rt_iif;
/* Info on neighbour */
__be32 rt_gateway;
/* Miscellaneous cached information */
__be32 rt_spec_dst; /* RFC1122 specific destination */
struct inet_peer *peer; /* long-living peer info */
};
然后nexthop是要服务器的地址,即目标地址,inet->saddr是我们绑定地址那节中看到的绑定的127.0.0.1这个地址,然后根据sock中的tos和SOCK_LOCALROUTE状态位来确定终的tos,tos就是type of service的意思,即服务类型。sk->sk_bound_dev_if是socket绑定到的设备,IPPROTO_TCP宏是确定使用的传输控制协议,inet->sport是客户端的socket的端口,而usin->sin_port则是服务器端口即目标端口。上面这些参数一一对应ip_route_connect()函数的参数。我们看到将这些参数设置到了路由的键值结构变量fl中了。然后取得网络命名空间结构,在我们这里取得的是全局变量init_net,如果地址类型是多播或者是广播的话那么我们这里源地址就是0,这个是在绑定地址章节中看到过的,
http://blog.chinaunix.net/u2/64681/showart_1387214.html
所以会进入__ip_route_output_key()函数中,这个函数是根据我们上面设置的路由键值查找适用的路由表
int __ip_route_output_key(struct net *net, struct rtable **rp,
const struct flowi *flp)
{
unsigned hash;
struct rtable *rth;
hash = rt_hash(flp->fl4_dst, flp->fl4_src, flp->oif);
rcu_read_lock_bh();
for (rth = rcu_dereference(rt_hash_table[hash].chain); rth;
rth = rcu_dereference(rth->u.dst.rt_next)) {
if (rth->fl.fl4_dst == flp->fl4_dst &&
rth->fl.fl4_src == flp->fl4_src &&
rth->fl.iif == 0 &&
rth->fl.oif == flp->oif &&
rth->fl.mark == flp->mark &&
!((rth->fl.fl4_tos ^ flp->fl4_tos) &
(IPTOS_RT_MASK | RTO_ONLINK)) &&
net_eq(dev_net(rth->u.dst.dev), net) &&
rth->rt_genid == atomic_read(&rt_genid)) {
dst_use(&rth->u.dst, jiffies);
RT_CACHE_STAT_INC(out_hit);
rcu_read_unlock_bh();
*rp = rth;
return 0;
}
RT_CACHE_STAT_INC(out_hlist_search);
}
rcu_read_unlock_bh();
return ip_route_output_slow(net, rp, flp);
}
函数首先根据路由键值确定一个hash值,然后在rt_hash_table的杂凑表中找到适用的路由表,朋友们可以看已阅读这段代码,如果找到了就会返回0,如果没有找到就会进入ip_route_output_slow()函数,这个函数总的来说就是再建立一个新的路由键值,然后根据我们这里的键值进行一系列的初始化操作,后根据这个键值进入fib_lookup()函数在我们全局的网络空间依次查找适用的路由表,后找到路由表并建立缓存后通过rp参数得到了路由表。这个过程我们暂时放一放了。我们继续看ip_route_connect()函数中得到了这个rp路由表后则根据路由表中提供的目标地址和源地址调整这里的路由键值fl,后函数进入ip_route_output_flow()返回
int ip_route_output_flow(struct net *net, struct rtable **rp, struct flowi *flp,
struct sock *sk, int flags)
{
int err;
if ((err = __ip_route_output_key(net, rp, flp)) != 0)
return err;
if (flp->proto) {
if (!flp->fl4_src)
flp->fl4_src = (*rp)->rt_src;
if (!flp->fl4_dst)
flp->fl4_dst = (*rp)->rt_dst;
err = __xfrm_lookup((struct dst_entry **)rp, flp, sk,
flags ? XFRM_LOOKUP_WAIT : 0);
if (err == -EREMOTE)
err = ipv4_dst_blackhole(rp, flp);
return err;
}
return 0;
}
我们看到在这个函数中再次通过__ip_route_output_key()函数确定一下是否能在路由的杂凑缓存表中找到我们的路由表,然后通过__xfrm_lookup()函数查找策略,这个函数__xfrm_lookup()非常大主要是ipsec的过程,我们也暂时放一放。这里暂时用网上的一句简短的描述概括一下函数的主要过程:下面摘取linuxforum论坛中的一段文字
事实上无论是本机的包还是转发的包,都必须经过xfrm_lookup这个函数的处理。
而每个skbuff在xfrm_lookup处理前dst_output 已经有了一个默认的设置,比如说是ip_output
而在经过xfrm_lookup处理过程中首先要检查的是策略,也就是policy,策略无非就是绕过、处理和丢弃。如果是绕过,则不作任何的处理,直接返回,然后根据原有的dst_output将数据包发出。这个过程和Linux-2.4内核的处理过程基本相同。关键区别是在加入ipsec处理上
如果策略决定数据包需要经过ipsec处理,那在xfrm_lookup中就有一个复杂的处理过程了,大概顺序是
1、查策略
2、找sa(xfrm_state),xfrm_find_bundle
3、没有找到就创建一个xfrm_bundle_create
4、创建完以后还要添加sa(xfrm_state)
5、后关键的一步是在dst_output的修改上,经过处理后的skbuff的dst_output已经不再是原有的ip_output,而是根据查找的xfrm_state设置成具体的ah_output或者是esp_output。
而这些所有的dst_output都连成了一个链,往往需要进行ipsec处理的都放在链头先获得处理,处理完后又被修改为ip_output这样的正常处理,又重新挂到链上处理。
这样需要ipsec处理的包可以得到不同的处理。
到此我们返回到tcp_v4_connect()函数中继续往下看,我们看到除了一些对函数返回值的检查外还对inet的地址进行了调整。由于时间关系我们明天继续。


文章来源CU社区:内核中的TCP的追踪分析-8-TCP(IPV4)的socket连接

相关文章