#TCP你學得會# 之 TCP端口選擇那些事兒

    本文所討論的內容基於Linux Kernel 3.13.0。app

    Linux內核中TCP鏈接的源端口選擇是由inet_hash_connect()函數完成的:less

/*
 * Bind a port for a connect operation and hash it.
 */
int inet_hash_connect(struct inet_timewait_death_row *death_row,
              struct sock *sk)
{
    return __inet_hash_connect(death_row, sk, inet_sk_port_offset(sk),
            __inet_check_established, __inet_hash_nolisten);
}
EXPORT_SYMBOL_GPL(inet_hash_connect);

    具體工做由__inet_hash_connect()函數完成:dom

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 **),
        int (*hash)(struct sock *sk, struct inet_timewait_sock *twp))
{
    struct inet_hashinfo *hinfo = death_row->hashinfo;
    const unsigned short snum = inet_sk(sk)->inet_num;
    struct inet_bind_hashbucket *head;
    struct inet_bind_bucket *tb;
    int ret;
    struct net *net = sock_net(sk);
    int twrefcnt = 1;

    if (!snum) {
        int i, remaining, low, high, port;
        static u32 hint;
        u32 offset = hint + port_offset;
        struct inet_timewait_sock *tw = NULL;

        inet_get_local_port_range(net, &low, &high);
        remaining = (high - low) + 1;

        local_bh_disable();
        for (i = 1; i <= remaining; i++) {
            port = low + (i + offset) % remaining;
            if (inet_is_reserved_local_port(port))
                continue;
            ...
        }
        local_bh_enable();
        return -EADDRNOTAVAIL;
ok:
        hint += i;
        ...
}

    在__inet_hash_connect()函數中與端口選擇相關的參數和變量有下面這幾個:tcp

port_offset   傳入的參數,由inet_sk_port_offset()函數計算獲得,實際至關於一個隨機因子;
snum          源端口,若是沒有進行過bind操做的話這個值爲0;
low           本地可選端口範圍的最小值;
high          本地可選端口範圍的最大值;
remaining     本地可選端口數;(low 和 high 的具體取值能夠經過/proc/sys/net/ipv4/ip_local_port_range查看;)
hint          靜態變量,用於全局控制;
offset        函數上次執行完畢後的hint值加上本次傳入的隨機因子port_offset, 這個值基本肯定了本次端口號的取值;
port          待肯定的端口號;

    當咱們肯定了offset值以後,剩下的內容就比較好理解了,主要工做集中在一個for循環中,從offset以後的值開始逐個嘗試,通常一次就能成功。若是該端口被預留,或者已經被佔用且不可reuse,那麼就嘗試下一個。函數

    下面就看看offset值是如何得到的:工具

u32 offset = hint + port_offset;

     能夠看到,本次端口選擇與兩個因素有關:測試

     一個是靜態變量hint,__inet_hash_connect()函數每成功調用一次該hint值就加一,用於全局控制;this

     另外一個是port_offset,這是一個輸入參數,實際是inet_sk_port_offset()函數的返回值;rest

static inline 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);
}

u32 secure_ipv4_port_ephemeral(__be32 saddr, __be32 daddr, __be16 dport)
{
    u32 hash[MD5_DIGEST_WORDS];

    net_secret_init();
    hash[0] = (__force u32)saddr;
    hash[1] = (__force u32)daddr;
    hash[2] = (__force u32)dport ^ net_secret[14];
    hash[3] = net_secret[15];

    md5_transform(hash, net_secret);

    return hash[0];
}
EXPORT_SYMBOL_GPL(secure_ipv4_port_ephemeral);

    能夠看到,inet_sk_port_offset()的返回值是經過源地址、目的地址、目的端口和隨機因子經過md5計算出來的。
code

    下面咱們就實際測試一下,這裏須要使用SystemTap工具協助將__inet_hash_connect()函數中相關變量的值打印出來:

begin to probe
/*telnet 192.168.28.1 3次*/
snum: 0,  port_offset: 3837244845
i: 1,  hint: 13, port: 45898

snum: 0,  port_offset: 3837244845
i: 1,  hint: 14, port: 45899

snum: 0,  port_offset: 3837244845
i: 1,  hint: 15, port: 45900

/*telnet 192.168.28.11 2次*/
snum: 0,  port_offset: 918745431
i: 1,  hint: 16, port: 48163

snum: 0,  port_offset: 918745431
i: 1,  hint: 17, port: 48164

/*telnet 192.168.28.1 2次*/
snum: 0,  port_offset: 3837244845
i: 1,  hint: 18, port: 45903

snum: 0,  port_offset: 3837244845
i: 1,  hint: 19, port: 45904

/*telnet 192.168.28.111 2次*/
snum: 0,  port_offset: 1738081703
i: 1,  hint: 20, port: 34546

snum: 0,  port_offset: 1738081703
i: 1,  hint: 21, port: 34547
^Cend to probe

    測試結果與前面的分析一致,hint值在每次測試中連續遞增。對於不一樣的目的地址,計算獲得的port_offset值不一樣,所以不一樣鏈接選擇的源端口有必定的隨機性,對於相同鏈接,因爲有hint值的參與,先後兩次選擇的源端口也未必連續,須要看中間是否還有其餘鏈接調用過__inet_hash_connect()函數。

    下面咱們就對net_secret_init()函數比較好奇了,隨機因子究竟是如何生成的呢:

#if IS_ENABLED(CONFIG_IPV6) || IS_ENABLED(CONFIG_INET)
#define NET_SECRET_SIZE (MD5_MESSAGE_BYTES / 4)

static u32 net_secret[NET_SECRET_SIZE] ____cacheline_aligned;


static __always_inline void net_secret_init(void)
{
    net_get_random_once(net_secret, sizeof(net_secret));
}
#endif

        
#define net_get_random_once(buf, nbytes)                \
    ({                                \
        bool ___ret = false;                    \
        static bool ___done = false;                \
        static struct static_key ___once_key =            \
            STATIC_KEY_INIT_TRUE;                \
        if (static_key_true(&___once_key))            \
            ___ret = __net_get_random_once(buf,        \
                               nbytes,        \
                               &___done,    \
                               &___once_key);    \
        ___ret;                            \
    })
   
    
bool __net_get_random_once(void *buf, int nbytes, bool *done,
               struct static_key *once_key)
{
    static DEFINE_SPINLOCK(lock);
    unsigned long flags;

    spin_lock_irqsave(&lock, flags);
    if (*done) {
        spin_unlock_irqrestore(&lock, flags);
        return false;
    }

    get_random_bytes(buf, nbytes);
    *done = true;
    spin_unlock_irqrestore(&lock, flags);

    __net_random_once_disable_jump(once_key);

    return true;
}
EXPORT_SYMBOL(__net_get_random_once);

    net_get_random_once是一個宏定義,其中___done 和 ___once_key都是靜態變量。從函數實現能夠看出,只有在第一次執行的時候(__done爲false),纔會調用get_random_bytes()獲取隨機數,隨後就將__done置爲true。因此在上述測試中,對於相同的源地址、目的地址和目的端口,獲取的port_offset老是相同的,固然若是系統重啓了那麼確定會有變化。

    延伸:

    從源碼中能夠看到,TCP的序列號也是經過相似的方法選擇的:

__u32 secure_tcp_sequence_number(__be32 saddr, __be32 daddr,
                 __be16 sport, __be16 dport)
{
    u32 hash[MD5_DIGEST_WORDS];

    net_secret_init();
    hash[0] = (__force u32)saddr;
    hash[1] = (__force u32)daddr;
    hash[2] = ((__force u16)sport << 16) + (__force u16)dport;
    hash[3] = net_secret[15];

    md5_transform(hash, net_secret);

    return seq_scale(hash[0]);
}

#ifdef CONFIG_INET
static u32 seq_scale(u32 seq)
{
    /*
     *    As close as possible to RFC 793, which
     *    suggests using a 250 kHz clock.
     *    Further reading shows this assumes 2 Mb/s networks.
     *    For 10 Mb/s Ethernet, a 1 MHz clock is appropriate.
     *    For 10 Gb/s Ethernet, a 1 GHz clock should be ok, but
     *    we also need to limit the resolution so that the u32 seq
     *    overlaps less than one time per MSL (2 minutes).
     *    Choosing a clock of 64 ns period is OK. (period of 274 s)
     */
    return seq + (ktime_to_ns(ktime_get_real()) >> 6);
}
#endif

    因爲在secure_tcp_sequence_number()函數返回時引入了seq_scale(),將時間因子也添加進來了,因此對於四元組相同的鏈接來講,序列號的選擇則不會重複。


    到這裏,TCP鏈接源端口選擇的內容就分析完了,下面附上測試中使用的SystemTap腳本:

#!/usr/bin/stap

probe begin
{
    log("begin to probe")
}

probe kernel.statement("__inet_hash_connect@inet_hashtables.c:491")
{
    printf("snum: %u,  port_offset: %u\n",$snum, $port_offset);
}

probe kernel.statement("__inet_hash_connect@inet_hashtables.c:503")
{
    printf("i: %u,  hint: %u, port: %u\n",$i, $hint, $port);
}

probe end
{
    log("end to probe")
}


    清明小長假就要結束了,你的假期計劃都完成了嗎? :)

相關文章
相關標籤/搜索