内核提供了两个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