内核提供了两个PROC文件可以控制套接口使用的端口号范围,其中文件ipv4_local_port_range定义了可使用的端口范围;文件ip_local_reserved_ports定义了保留的端口范围。
static struct ctl_table ipv4_net_table[] = {
{
.procname = "ip_local_port_range",
.maxlen = sizeof(init_net.ipv4.ip_local_ports.range),
.data = &init_net.ipv4.ip_local_ports.range,
.mode = 0644,
.proc_handler = ipv4_local_port_range,
},
{
.procname = "ip_local_reserved_ports",
.data = &init_net.ipv4.sysctl_local_reserved_ports,
.maxlen = 65536,
.mode = 0644,
.proc_handler = proc_do_large_bitmap,
},
默认情况下,保留端口范围文件ip_local_reserved_ports为空。
# cat /proc/sys/net/ipv4/ip_local_reserved_ports
#
可用端口范围文件ip_local_port_range的内容为32768到60999。
# cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999
#
端口号选择
如下函数inet_csk_find_open_port实现端口的选择。首先,如果套接口设置了可重用选项SK_CAN_REUSE,并且端口范围大于4,将可用的端口范围平均分成两个部分。这里采用了先缩小4倍,在增加1倍的操作,两个部分的端口号数量最大差值可能到3,并且保证了low加上了一个偶数值,low和half保持一致的奇偶性(remaining为偶数)。注意在查找之前,将最高端口号high增加一,导致在之后的查找中,high不是可用的合法端口号。之后确保high和low的差值为偶数。
不太清楚为何要将可用端口范围分成两部分查找,一种可能是,在设置重用选项之后,端口大概率位于前半部分,算是一种加速查找,不确认。
static struct inet_bind_hashbucket *inet_csk_find_open_port(struct sock *sk, struct inet_bind_bucket **tb_ret, int *port_ret)
{
struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo;
int port = 0;
struct inet_bind_hashbucket *head;
struct inet_bind_bucket *tb;
l3mdev = inet_sk_bound_l3mdev(sk);
attempt_half = (sk->sk_reuse == SK_CAN_REUSE) ? 1 : 0;
other_half_scan:
inet_get_local_port_range(net, &low, &high);
high++; /* [32768, 60999] -> [32768, 61000[ */
if (high - low < 4)
attempt_half = 0;
if (attempt_half) {
int half = low + (((high - low) >> 2) << 1);
if (attempt_half == 1) high = half;
else low = half;
}
remaining = high - low;
if (likely(remaining > 1))
remaining &= ~1U;
其次,在前半部分进行第一轮的端口查找(如果未拆分,就是在全部端口范围内的查找),使用函数prandom_u32在其中随机选择一个偏移值,确保此偏移值为奇数,这样low与一个奇数相加,将改变奇偶性,所以此处得到的端口号与low的奇偶性不同(另外,函数__inet_hash_connect获取和low相同奇偶性的端口)。之后,由此偏移值开始,遍历前半部端口范围中的所有与low奇偶性不同的端口号,如果当前遍历的端口号不是保留端口,并且,遍历绑定hash链表时,也没有发现与当前套接口所用端口冲突的套接口,表明可以使用此端口号进行监听。
偶数端口号尽量留给发起连接的connect函数使用(TCP客户端)。
offset = prandom_u32() % remaining;
/* __inet_hash_connect() favors ports having @low parity
* We do the opposite to not pollute connect() users.
*/
offset |= 1U;
other_parity_scan:
port = low + offset;
for (i = 0; i < remaining; i += 2, port += 2) {
if (unlikely(port >= high))
port -= remaining;
if (inet_is_local_reserved_port(net, port))
continue;
head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)];
spin_lock_bh(&head->lock);
inet_bind_bucket_for_each(tb, &head->chain)
if (net_eq(ib_net(tb), net) && tb->l3mdev == l3mdev && tb->port == port) {
if (!inet_csk_bind_conflict(sk, tb, false, false))
goto success;
goto next_port;
}
tb = NULL;
goto success;
next_port:
spin_unlock_bh(&head->lock);
cond_resched();
}
如果以上没有找到可用的端口号,以下将偏移值offset减一,使其变为偶数值,跳转回标签other_parity_scan,遍历端口范围内的所有与low奇偶性相同的端口号。如果减一之后offset不为偶数值,表明第二次遍历已经完成,不再遍历,防止进入死循环。
offset--;
if (!(offset & 1))
goto other_parity_scan;
最后,如果端口范围进行了拆分,跳转回标签other_half_scan,遍历后半部分的端口范围(先是遍历与low奇偶性不同的端口号,之后遍历与low奇偶性相同的端口号)。
if (attempt_half == 1) {
/* OK we now try the upper half of the range */
attempt_half = 2;
goto other_half_scan;
}
return NULL;
success:
*port_ret = port;
*tb_ret = tb;
return head;
如下函数inet_csk_bind_conflict检查套接口绑定端口是否与已经存在的套接口冲突,这里不进行网络命名空间的比较,调用函数需要先确保tb与套接口sk位于相同的命名空间。在以上函数inet_csk_find_open_port调用此函数时,最后两个参数都是false(relax为false表示不进行严格的比对,reuseport_ok为false表示比对时不考虑端口重用的情况)。
xxx
如果匹配到一个套接口(sk2)其监听地址与当前套接口(sk)相等(inet_rcv_saddr_equal),表明两者存在冲突可能,
xxx
遍历所有监听在此端口的套接口,符合条件的冲突套接口sk2需满足以下条件:
1) 套接口(sk2)与当前检查的套接口(sk)不同;
2.a) 两个套接口(sk2和sk)都没有绑定特定接口;
2.b) 或者两个套接口绑定在同一个接口上。
由以上条件可见,内核允许绑定不同接口的套接口具有相同的监听地址和端口,不计为冲突。
static int inet_csk_bind_conflict(const struct sock *sk,
const struct inet_bind_bucket *tb, bool relax, bool reuseport_ok)
{
bool reuse = sk->sk_reuse;
bool reuseport = !!sk->sk_reuseport && reuseport_ok;
kuid_t uid = sock_i_uid((struct sock *)sk);
sk_for_each_bound(sk2, &tb->owners) {
if (sk != sk2 &&
(!sk->sk_bound_dev_if ||
!sk2->sk_bound_dev_if ||
sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {
以下检查,套接口sk2和sk是否能够重用使用相同的地址和端口,不能重用,则需进行冲突检查:
1) 两个套接口(sk2和sk)中有一个没有开启地址重用;
2) 或者都开启了地址重用,但是较早的套接口sk2已经处于监听状态TCP_LISTEN;
if ((!reuse || !sk2->sk_reuse ||
sk2->sk_state == TCP_LISTEN) &&
如果以上条件不满足,即两个套接口的地址可以重用,需要继续对两者的端口号进行冲突检查:
1) 两个套接口(sk2和sk)中只要有一个没有开启端口重用;
2) 或者都开启了端口重用,但是后一个套接口sk已经处于监听状态,加进监听套接口链表(sk_reuseport_cb为空);
3) 如果以上条件1)和2)都不成立,即两个套接口都开启了端口重用,套接口sk未加入端口重用组,那么,如果套接口sk2的状态不等于TCP_TIME_WAIT(否则,TIME_WAIT状态表明端口马上要被释放),并且两个套接口的uid不相同,也表明两者端口不能重用,执行以下的地址检查。
以上3个条件表明端口重用需要两个套接口都开启,并且两个套接口的uid要相同(基于安全考虑)。之后,调用函数inet_rcv_saddr_equal比较监听地址,如果相等,即找到了冲突的套接口sk2。
(!reuseport || !sk2->sk_reuseport ||
rcu_access_pointer(sk->sk_reuseport_cb) ||
(sk2->sk_state != TCP_TIME_WAIT &&
!uid_eq(uid, sock_i_uid(sk2))))) {
if (inet_rcv_saddr_equal(sk, sk2, true))
break;
}
如果以上条件不成立,未能匹配到冲突的套接口,以下,在relax值为false的时候,放宽检查条件(不检查端口重用),只要两个套接口开启了地址重用,并且sk2不处于TCP_LISTEN状态,则进行监听地址的比较。
if (!relax && reuse && sk2->sk_reuse &&
sk2->sk_state != TCP_LISTEN) {
if (inet_rcv_saddr_equal(sk, sk2, true))
break;
}
}
}
return sk2 != NULL;
端口绑定
如下函数inet_csk_get_port,申请端口号,绑定端口。如果参数snum为空,将使用上一节的函数inet_csk_find_open_port随机选择一个可用的端口号,此函数在失败后返回空,否则返回新端口对应的绑定哈希链表头head。结构体类型为inet_bind_bucket的变量tb为空,意味着对应于此端口的相应结构还没有创建过,需要在此函数(inet_csk_get_port)中创建。否则,如果tb已经创建,跳过tb创建的部分。
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
bool reuse = sk->sk_reuse && sk->sk_state != TCP_LISTEN;
struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo;
int ret = 1, port = snum;
struct inet_bind_hashbucket *head;
struct net *net = sock_net(sk);
struct inet_bind_bucket *tb = NULL;
kuid_t uid = sock_i_uid(sk);
l3mdev = inet_sk_bound_l3mdev(sk);
if (!port) {
head = inet_csk_find_open_port(sk, &tb, &port);
if (!head)
return ret;
if (!tb)
goto tb_not_found;
goto success;
}
不同于port值为零的情况,如果用户指定了要绑定的端口号,head还没有进行赋值,其为空,以下获取绑定链表头部指针head,遍历此链表,查找新端口port对应的绑定结构inet_bind_bucket是否已经创建,为真跳过创建部分,否则,调用函数inet_bind_bucket_create进行创建,并将其添加到绑定链表中。
head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)];
spin_lock_bh(&head->lock);
inet_bind_bucket_for_each(tb, &head->chain)
if (net_eq(ib_net(tb), net) && tb->l3mdev == l3mdev && tb->port == port)
goto tb_found;
tb_not_found:
tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep, net, head, port, l3mdev);
if (!tb)
goto fail_unlock;
如果tb的所有者owners链表不为空,表明已经存在套接口监听与新分配端口相同的端口号,进行地址重用检查。首先如果套接口成员变量sk_reuse的值为SK_FORCE_REUSE,表明其可重复使用任何套接口已绑定的地址,无需再进行冲突检查。
其次,如果tb中成员fastreuse大于零,允许重用,并且当前套接口的sk_reuse设置为SK_CAN_REUSE,而且其没有处于监听状态(参见变量reuse的赋值),允许使用此端口。或者端口重用检查函数sk_reuseport_match返回真,也表示可使用此端口。
最后,以上条件不成立,由函数inet_csk_bind_conflict检查新套接口是否与tb中套接口冲突,参见上节对此函数的介绍。
tb_found:
if (!hlist_empty(&tb->owners)) {
if (sk->sk_reuse == SK_FORCE_REUSE)
goto success;
if ((tb->fastreuse > 0 && reuse) || sk_reuseport_match(tb, sk))
goto success;
if (inet_csk_bind_conflict(sk, tb, true, true))
goto fail_unlock;
}
对于首次创建的inet_bind_bucket结构变量tb,其拥有者owners链表为空,如果此唯一的套接口开启了端口重用,初始化其地址重用成员变量fastreuse为FASTREUSEPORT_ANY,表示允许之后的套接口重用与此套接口相同的端口号,加速在函数sk_reuseport_match和__inet_hash_connect函数中的对比处理。之后,缓存端口重用相关的对比所用变量。
success:
if (hlist_empty(&tb->owners)) {
tb->fastreuse = reuse;
if (sk->sk_reuseport) {
tb->fastreuseport = FASTREUSEPORT_ANY;
tb->fastuid = uid;
tb->fast_rcv_saddr = sk->sk_rcv_saddr;
tb->fast_ipv6_only = ipv6_only_sock(sk);
tb->fast_sk_family = sk->sk_family;
#if IS_ENABLED(CONFIG_IPV6)
tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr;
#endif
} else {
tb->fastreuseport = 0;
}
如果inet_bind_bucket结构变量tb的拥有者链表不为空,根据新加入的套接口信息,更新tb的地址重用成员变量fastreuse,如下所示,如果当前套接口没有开启地址重用,或者套接口sk已经处于TCP_LISTEN监听状态(reuse),将全局的关闭tb的地址重用功能(fastreuse=0)。即监听开始之后,不再允许其它监听此地址的新套接口加入。
} else {
if (!reuse)
tb->fastreuse = 0;
如果套接口开启了端口号重用功能,检查套接口是否与tb相匹配(参见以下对函数sk_reuseport_match的介绍)。如果不匹配,表明当前tb中的套接口并不支持端口重用,由于套接口sk并不与当前tb中的套接口冲突,在将套接口sk加入之后,将使用当前的套接口sk信息,更新tb中的端口重用缓存信息,这样可允许之后加入的套接口与sk进行快速匹配,并且将fastreuseport设置为FASTREUSEPORT_STRICT,对以后要加入重用端口号的套接口执行相对严格的地址检查。
if (sk->sk_reuseport) {
/* We didn't match or we don't have fastreuseport set on
* the tb, but we have sk_reuseport set on this socket
* and we know that there are no bind conflicts with
* this socket in this tb, so reset our tb's reuseport
* settings so that any subsequent sockets that match
* our current socket will be put on the fast path.
*
* If we reset we need to set FASTREUSEPORT_STRICT so we
* do extra checking for all subsequent sk_reuseport socks.
*/
if (!sk_reuseport_match(tb, sk)) {
tb->fastreuseport = FASTREUSEPORT_STRICT;
tb->fastuid = uid;
tb->fast_rcv_saddr = sk->sk_rcv_saddr;
tb->fast_ipv6_only = ipv6_only_sock(sk);
tb->fast_sk_family = sk->sk_family;
#if IS_ENABLED(CONFIG_IPV6)
tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr;
#endif
}
} else {
tb->fastreuseport = 0;
}
}
最后,如果套接口sk还没有绑定到tb,使用函数inet_bind_hash进行绑定,套接口sk链接到tb的拥有者owners链表中。
if (!inet_csk(sk)->icsk_bind_hash)
inet_bind_hash(sk, tb, port);
WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
ret = 0;
fail_unlock:
spin_unlock_bh(&head->lock);
return ret;
以下函数sk_reuseport_match,检查套件口sk是否与tb结构相匹配。不匹配的情况有以下几种:
1) fastreuseport小于等于零,表明tb的端口重用已经关闭,直接返回零,不匹配;
2) 套接口sk没有开启端口重用;
3) 套接口sk的成员sk_reuseport_cb不为空,已经处于监听状态;
4) 套接口sk的uid与tb的uid不相同;
之后,检查tb的fastreuseport的值,如果等于FASTREUSEPORT_ANY,返回1,两者匹配。否则,进行监听地址的比对,相等(或者其中一个监听地址为零)返回1,
static inline int sk_reuseport_match(struct inet_bind_bucket *tb, struct sock *sk)
{
kuid_t uid = sock_i_uid(sk);
if (tb->fastreuseport <= 0)
return 0;
if (!sk->sk_reuseport)
return 0;
if (rcu_access_pointer(sk->sk_reuseport_cb))
return 0;
if (!uid_eq(tb->fastuid, uid))
return 0;
/* We only need to check the rcv_saddr if this tb was once marked
* without fastreuseport and then was reset, as we can only know that
* the fast_*rcv_saddr doesn't have any conflicts with the socks on the
* owners list.
*/
if (tb->fastreuseport == FASTREUSEPORT_ANY)
return 1;
#if IS_ENABLED(CONFIG_IPV6)
...
#endif
return ipv4_rcv_saddr_equal(tb->fast_rcv_saddr, sk->sk_rcv_saddr,
ipv6_only_sock(sk), true);
内核版本 5.0