TCP客戶端端口號選擇

如下函數inet_hash_connect,如果沒有指定綁定的接口,在發起連接的時候,由函數inet_sk_port_offset先選擇一個端口偏移量(port_offset),函數__inet_hash_connect負責綁定端口。

int inet_hash_connect(struct inet_timewait_death_row *death_row, struct sock *sk)
{
    u32 port_offset = 0;

    if (!inet_sk(sk)->inet_num)
        port_offset = inet_sk_port_offset(sk);
    return __inet_hash_connect(death_row, sk, port_offset,
                   __inet_check_established);
}

端口偏移量根據套接口的監聽地址,目的地址和目的端口生成。

static u32 inet_sk_port_offset(const struct sock *sk)
{
    const struct inet_sock *inet = inet_sk(sk);

    return secure_ipv4_port_ephemeral(inet->inet_rcv_saddr,
                      inet->inet_daddr, inet->inet_dport);
}

如下函數__inet_hash_connect,如果指定了端口號(port),直接定位到端口號對應的綁定結構體inet_bind_bucket,如果此結構體(tb)的擁有者owners鏈表中的首個套接口等於當前套接口,並且擁有者owners僅有一個(只有當前套接口監聽此端口號),將套接口移入ehash鏈表(TCP客戶端沒有listen鏈表)。

否則,如果tb中有多個套接口,或者tb的擁有者鏈表的首位不是當前套接口,由指針函數check_established檢查此端口號是否被使用,指針函數實際上爲函數__inet_check_established,稍後介紹。

int __inet_hash_connect(struct inet_timewait_death_row *death_row,
        struct sock *sk, u32 port_offset,
        int (*check_established)(struct inet_timewait_death_row *,
            struct sock *, __u16, struct inet_timewait_sock **))
{
    struct inet_hashinfo *hinfo = death_row->hashinfo;
    struct inet_timewait_sock *tw = NULL;
    struct inet_bind_hashbucket *head;
    int port = inet_sk(sk)->inet_num;
    struct net *net = sock_net(sk);
    struct inet_bind_bucket *tb;
	static u32 hint;

    if (port) {
        head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)];
        tb = inet_csk(sk)->icsk_bind_hash;
        spin_lock_bh(&head->lock);
        if (sk_head(&tb->owners) == sk && !sk->sk_bind_node.next) {
            inet_ehash_nolisten(sk, NULL);
            spin_unlock_bh(&head->lock);
            return 0;
        }
        spin_unlock(&head->lock);
        /* No definite answer... Walk to established hash table */
        ret = check_established(death_row, sk, port, NULL);
        local_bh_enable();
        return ret;
    }

以下是沒有指定端口號,進行自動分配的情況。首先確保端口的數量值(remaining = high-low)爲偶數;其次,hint值爲一個靜態變量,表示可能可用的端口號,其記錄了上一次選擇的端口號加上2的結果值。而offset爲以上函數inet_sk_port_offset生成的哈希值。根據以上兩個值(hint和port_offset)生成一個偏移量offset,確保其爲偶數值。在此函數中,首先嚐試端口範圍內與low的奇偶性相同的端口號,offset爲偶數,保證不會改變之後port的奇偶性(加/減偶數,奇偶性不改變)。同理,對於偶數變量remaining,其也不會改變port的奇偶性。

與此不同,在函數inet_csk_find_open_port中,優先選擇與low奇偶性不同的端口號。

    l3mdev = inet_sk_bound_l3mdev(sk);

    inet_get_local_port_range(net, &low, &high);
    high++;                /* [32768, 60999] -> [32768, 61000[ */
    remaining = high - low;
    if (likely(remaining > 1))
        remaining &= ~1U;

    offset = (hint + port_offset) % remaining;
    /* In first pass we try ports of @low parity. inet_csk_get_port() does the opposite choice.
     */
    offset &= ~1U;

以下開始端口號遍歷過程,根據端口號和網絡命名空間定位到inet_bind_bucket結構鏈表,遍歷其中的每個tb結構,如果已經設置了地址(fastreuse)或者端口重用(fastreuseport),表明已經有套接口監聽在此端口號上,結束處理,開始遍歷下一個端口號。

否則,使用指針函數check_established檢查端口號是否可用,參見下節對函數__inet_check_established的介紹。

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);

        /* Does not bother with rcv_saddr checks, because the established check is already unique enough.
         */
        inet_bind_bucket_for_each(tb, &head->chain) {
            if (net_eq(ib_net(tb), net) && tb->l3mdev == l3mdev && tb->port == port) {
                if (tb->fastreuse >= 0 || tb->fastreuseport >= 0)
                    goto next_port;
                WARN_ON(hlist_empty(&tb->owners));
                if (!check_established(death_row, sk, port, &tw))
                    goto ok;
                goto next_port;
            }
        }

流程走到以下部分,表明端口號對應的inet_bind_bucket結構還沒有創建,端口可用。以下創建tb結構,將其成員fastreuse和fastreuseport設置爲-1,表示客戶端地址/端口號不可重用。

        tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep, net, head, port, l3mdev);
        if (!tb) {
            spin_unlock_bh(&head->lock);
            return -ENOMEM;
        }
        tb->fastreuse = -1;
        tb->fastreuseport = -1;
        goto ok;
next_port:
        spin_unlock_bh(&head->lock);
        cond_resched();
    }

如果在遍歷所有的與low奇偶性相同的端口號時,沒有找到可用的端口號,以下跳轉回去,在端口範圍內遍歷所有與low的奇偶性不同的端口號。如果所有端口號已經遍歷完成(offset & 1),返回地址不可用的錯誤碼EADDRNOTAVAIL。

    offset++;
    if ((offset & 1) && remaining > 1)
        goto other_parity_scan;

    return -EADDRNOTAVAIL;

變量hint記錄下一個可用的與low的奇偶性不同的端口號。最後,將當前套接口添加到連接建立哈希鏈表中。

ok:
    hint += i + 2;

    /* Head lock still held and bh's disabled */
    inet_bind_hash(sk, tb, port);
    if (sk_unhashed(sk)) {
        inet_sk(sk)->inet_sport = htons(port);
        inet_ehash_nolisten(sk, (struct sock *)tw);
    }
    if (tw)
        inet_twsk_bind_unhash(tw, hinfo);
    spin_unlock(&head->lock);
    if (tw)
        inet_twsk_deschedule_put(tw);
    local_bh_enable();
    return 0;

連接建立鏈表端口檢查

連接狀態套接口鏈表的檢查函數__inet_check_established如下,使用選取的本地端口號lport,與監聽地址,以及目的地址/目的端口號,計算連接建立鏈表的相應哈希值,定位到對應的哈希鏈表。

static int __inet_check_established(struct inet_timewait_death_row *death_row,
                    struct sock *sk, __u16 lport, struct inet_timewait_sock **twp)
{
    struct inet_hashinfo *hinfo = death_row->hashinfo;
    struct inet_sock *inet = inet_sk(sk);
    __be32 daddr = inet->inet_rcv_saddr;
    __be32 saddr = inet->inet_daddr;
    int dif = sk->sk_bound_dev_if;
    struct net *net = sock_net(sk);
    int sdif = l3mdev_master_ifindex_by_index(net, dif);
    INET_ADDR_COOKIE(acookie, saddr, daddr);
    const __portpair ports = INET_COMBINED_PORTS(inet->inet_dport, lport);
    unsigned int hash = inet_ehashfn(net, daddr, lport, saddr, inet->inet_dport);
    struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash);

遍歷鏈表,如果找到一個源地址/目的地址,源端口/目的端口,綁定接口和網絡命名空間與當前套接口(sk)都相同的套接口(sk2),表明當前選擇的端口號已經被使用,返回錯誤值EADDRNOTAVAIL。但是,有一個例外情況,如果套接口sk2處於TCP_TIME_WAIT狀態,使用函數twsk_unique檢查是否可安全的重用其端口號,PROC文件(/proc/sys/net/ipv4/tcp_tw_reuse)控制是否可重用,默認情況下其值爲0,禁止重用。如果其值爲1,則可進行重用。

具體判斷由函數twsk_unique完成,如果tcp_tw_reuse值爲1,爲安全起見,在重用之前,將修改套接口(sk)的發送序號以及timestamps接收時間戳,以抵禦sk2套接口殘留的報文。

    spin_lock(lock);

    sk_nulls_for_each(sk2, node, &head->chain) {
        if (sk2->sk_hash != hash)
            continue;

        if (likely(INET_MATCH(sk2, net, acookie,
                     saddr, daddr, ports, dif, sdif))) {
            if (sk2->sk_state == TCP_TIME_WAIT) {
                tw = inet_twsk(sk2);
                if (twsk_unique(sk, sk2, twp))
                    break;
            }
            goto not_unique;
        }
    }

到這一步已經確認選擇的端口號可以使用,將當前套接口sk添加到連接建立鏈表中。如果tw有值,將tw套接口由鏈表中刪除。

    /* Must record num and sport now. Otherwise we will see
     * in hash table socket with a funny identity.
     */
    inet->inet_num = lport;
    inet->inet_sport = htons(lport);
    sk->sk_hash = hash;
    WARN_ON(!sk_unhashed(sk));
    __sk_nulls_add_node_rcu(sk, &head->chain);
    if (tw) {
        sk_nulls_del_node_init_rcu((struct sock *)tw);
        __NET_INC_STATS(net, LINUX_MIB_TIMEWAITRECYCLED);
    }
    spin_unlock(lock);
    sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);

    if (twp) {
        *twp = tw;
    } else if (tw) {
        /* Silly. Should hash-dance instead... */
        inet_twsk_deschedule_put(tw);
    }
    return 0;

not_unique:
    spin_unlock(lock);
    return -EADDRNOTAVAIL;

內核版本 5.0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章