文档章节

#TCP你学得会# 之 TCP端口选择那些事儿

n
 nodouble
发布于 2016/04/04 16:59
字数 1507
阅读 190
收藏 0

    本文所讨论的内容基于Linux Kernel 3.13.0。

    Linux内核中TCP连接的源端口选择是由inet_hash_connect()函数完成的:

/*
 * 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()函数完成:

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()函数中与端口选择相关的参数和变量有下面这几个:

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值就加一,用于全局控制;

     另一个是port_offset,这是一个输入参数,实际是inet_sk_port_offset()函数的返回值;

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计算出来的。

    下面我们就实际测试一下,这里需要使用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")
}


    清明小长假就要结束了,你的假期计划都完成了吗? :)




© 著作权归作者所有

n
粉丝 3
博文 34
码字总数 40111
作品 0
东城
私信 提问
TCP 的那些事儿(上)

TCP是一个巨复杂的协议,因为他要解决很多问题,而这些问题又带出了很多子问题和阴暗面。所以学习TCP本身是个比较痛苦的过程,但对于学习的过程却能让人有很多收获。关于   TCP这个协议的细...

Oscarfff
2016/07/24
23
0
TCP 的那些事儿

TCP 的那些事儿(上) TCP是一个巨复杂的协议,因为他要解决很多问题,而这些问题又带出了很多子问题和阴暗面。所以学习TCP本身是个比较痛苦的过程,但对于学习的过程却能让人有很多收获。关...

精通吹水
2016/04/13
159
0
使用Godaddy和Linode建站的菜鸟初体验

终于凑够了点钱可以买自己的空间和域名玩玩建站了,虽然在公司已经干了三年网站的活但是那时候服务器维护和域名都轮不到咱去碰,最多也就碰碰Linux还不怎么熟。所以建站对自己来说还是小有难...

gangzz
2013/12/08
5.3K
13
TCP 的那些事儿(下)

https://coolshell.cn/articles/11609.html 这篇文章是下篇,所以如果你对TCP不熟悉的话,还请你先看看上篇《TCP的那些事儿(上)》 上篇中,我们介绍了TCP的协议头、状态机、数据重传中的东...

libaineu2004
2017/12/14
0
0
游戏通讯协议的选择TCP?UDP?HTTP?WebSocket?

(一)游戏通讯协议的选择TCP?UDP?HTTP?WebSocket? 网络游戏 游戏技术那些事儿(猴哥) · 2016-07-26 10:01 一、协议特性 游戏设计之初需要决定选择哪种协议来通讯,那么我整理了一张图,关于...

pingglala
2016/11/21
0
0

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周日乱弹 —— 我,小小编辑,食人族酋长

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @宇辰OSC :分享娃娃的单曲《飘洋过海来看你》: #今日歌曲推荐# 《飘洋过海来看你》- 娃娃 手机党少年们想听歌,请使劲儿戳(这里) @宇辰OSC...

小小编辑
今天
533
10
MongoDB系列-- SpringBoot 中对 MongoDB 的 基本操作

SpringBoot 中对 MongoDB 的 基本操作 Database 库的创建 首先 在MongoDB 操作客户端 Robo 3T 中 创建数据库: 增加用户User: 创建 Collections 集合(类似mysql 中的 表): 后面我们大部分都...

TcWong
今天
31
0
spring cloud

一、从面试题入手 1.1、什么事微服务 1.2、微服务之间如何独立通讯的 1.3、springCloud和Dubbo有哪些区别 1.通信机制:DUbbo基于RPC远程过程调用;微服务cloud基于http restFUL API 1.4、spr...

榴莲黑芝麻糊
今天
16
0
Executor线程池原理与源码解读

线程池为线程生命周期的开销和资源不足问题提供了解决方 案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。 线程实现方式 Thread、Runnable、Callable //实现Runnable接口的...

小强的进阶之路
昨天
47
0
maven 环境隔离

解决问题 即 在 resource 文件夹下面 ,新增对应的资源配置文件夹,对应 开发,测试,生产的不同的配置内容 <resources> <resource> <directory>src/main/resources.${deplo......

之渊
昨天
56
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部