eBPF对TCP listen socket lookup的逻辑进行重定义

eBPF让Linux内核(其它OS内核对eBPF的支持,我不清楚,仅谈Linux)本身变得可编程,前面我已经展示了eBPF很多的trick用法,本文我来展示如何让eBPF干涉socket的查找。

我们知道,数据包到达四层后,会进行socket查找,以TCP为例,这个过程在tcp_v4_rcv函数中执行,所谓的查找过程就是一个简单的hash查找,然而hash查找算法的执行过程会随着输入数据的不同而产生畸变,由于代码是写死的,我们对这种畸变束手无策,只能寄希望于实现更好的hash算法。

eBPF改变了这一切,它可以让socket的查找过程变得可编程,你可以自定义自己的查找算法,绕开内核提供的标准hash查找过程。

先从一个概念,Anycast,开始。

Anycast即多个节点对外通告相同的地址,通过就近路由来寻址最合适的节点。采用Anycast有两个明显的益处:

  1. 实现广域范围的负载均衡。
  2. 所有节点配置相同,可以透明无缝迁移。

由于任何运营商都不会通告/32的前缀,甚至/24的很少通告,因此可以将IP地址的最后几位用于负载均衡,因此,一个服务器节点一般通告一个Anycast网段而不是一个/32地址,比如 192.168.40.0/24

这有几个益处:

  • 广域范围,DNS调度系统可以为用户吐192.168.40.0/24段的任意地址,实现地址隐藏。
  • 局域网范围,前端LB(比如LVS)可以target 192.168.40.0/24内的任意主机,实现负载均衡。
  • 实现更好的用户分组和策略路由。

类似的用法,参见iptables的 CLUSTERIP ,我之前写过一篇分析文章:
https://blog.csdn.net/dog250/article/details/77993563

现在言归正传,虽然可以通告一个网段,但是一个TCP服务却无法侦听一个网段,一个TCP服务要么侦听特定的/32地址,要么侦听0.0.0.0,别无它选。

假设一个服务器上驻留了两个服务,服务A通告192.168.40.0/24网段的80端口,服务B通告172.16.40.0/24网段的80端口,如何?

显然,侦听0.0.0.0:80是不可以的,这样会混杂两个服务,所以,你必须显示侦听所有这2段/24的地址,一共512个,一个socket只能侦听1个IP/Port元组,这意味着你必须显示创建512个socket,然而你的本意可能只需要2个就够了,一个侦听192.168.40.0/24:80,另一个侦听172.16.40.0/24:80,问题又回到了原点,怎么办?

一个见招拆招的方案就是 允许一个socket侦听一个非/32段。 这意味着要修改内核。我们可以找到这个patch:
https://www.spinics.net/lists/netdev/msg370789.html
我很赞同这种风格,事实上我出过很多如此捣鼓的方案。

然而,eBPF是更好的选择:
Programming socket lookup with BPF: https://lwn.net/Articles/797596/

这个topic事实上新增了一种eBPF类型(这种新增类型的事情一直在持续发生),我们只需要看一下代码就知道发生了什么。

遗憾的是,在Linux 5.5的合并窗口,这个patch目前依然没有进入主线,我们只能从支线来拉取代码。不管怎样,我觉得它是有意义的,甚至可以和eBPF的REUSEPORT作用点进行合并。

首先,我们把patch该功能的代码拉下来:

git clone https://github.com/jsitnicki/linux.git

然后翻阅 net/ipv4/inet_hashtables.c 文件的 __inet_lookup_listener函数:

struct sock *__inet_lookup_listener(struct net *net,
                                    struct inet_hashinfo *hashinfo,
                                    struct sk_buff *skb, int doff,
                                    const __be32 saddr, __be16 sport,
                                    const __be32 daddr, const unsigned short hnum,
                                    const int dif, const int sdif)
{
        struct inet_listen_hashbucket *ilb2;
        struct sock *result = NULL;
        unsigned int hash2;

		// 新增逻辑!增加了对eBPF程序的调用支持
        result = inet_lookup_run_bpf(net, hashinfo->protocol,
                                     saddr, sport, daddr, hnum);
        if (result)
                goto done;

		// 即便这里采用了2元组的hash2替代了仅仅端口hash的listen_hash,
		// 在海量listener情况下依然存在冲突链表过长的问题,毕竟hash桶是宏定义写死的。
        hash2 = ipv4_portaddr_hash(net, daddr, hnum);
        ilb2 = inet_lhash2_bucket(hashinfo, hash2);

        result = inet_lhash2_lookup(net, ilb2, skb, doff,
                                    saddr, sport, daddr, hnum,
                                    dif, sdif);
        if (result)
                goto done;

        /* Lookup lhash2 with INADDR_ANY */
        hash2 = ipv4_portaddr_hash(net, htonl(INADDR_ANY), hnum);
        ilb2 = inet_lhash2_bucket(hashinfo, hash2);

        result = inet_lhash2_lookup(net, ilb2, skb, doff,
                                    saddr, sport, htonl(INADDR_ANY), hnum,
                                    dif, sdif);
done:
        if (IS_ERR(result))
                return NULL;
        return result;
}

我们可以在eBPF程序中大有所为:

static inline struct sock *__inet_lookup_run_bpf(const struct net *net,
                                                 struct bpf_inet_lookup_kern *ctx)
{
        struct bpf_prog *prog;
        int ret = BPF_OK;

        rcu_read_lock();
        prog = rcu_dereference(net->inet_lookup_prog);
        if (prog) // 这里定义我们自己的lookup逻辑就是了。
                ret = BPF_PROG_RUN(prog, ctx);
        rcu_read_unlock();

        return ret == BPF_REDIRECT ? ctx->redir_sk : NULL;
}

现在有解了,还是上面的需求,我们可以让两个服务均侦听0.0.0.0:80,然后用eBPF程序进行分流:

#define NET1 (IP4(192,  168,   40, 0) >> 8)
#define NET2 (IP4(172, 16, 40, 0) >> 8)
struct {
	__uint(type, BPF_MAP_TYPE_REUSEPORT_SOCKARRAY);
	__uint(max_entries, MAX_SERVERS);
	__type(key, __u32);
	__type(value, __u64);
} redir_map SEC(".maps");

SEC("inet_lookup/demo_two_servers")
int demo_two_http_servers(struct bpf_inet_lookup *ctx)
{
	__u32 index = 0;
	__u64 flags = 0;

	if (ctx->family != AF_INET)
		return BPF_OK;
	if (ctx->protocol != IPPROTO_TCP)
		return BPF_OK;
	if (ctx->local_port != 80)
		return BPF_OK;

	switch (bpf_ntohl(ctx->local_ip4) >> 8) {
		case NET1:
		index = 0;
		break;
		case NET2:
		index = 1;
		break;
		default:
		return BPF_OK;
	}

	return bpf_redirect_lookup(ctx, &redir_map, &index, flags);
}

我们在两个用户态的服务里所要做的,仅仅是:

  1. 将上述eBPF程序载入内核。
  2. 找到redir_map这个map。
  3. 将通告192.168.40.0/24的服务socket以index=0插入map。
  4. 将通告172.16.40.0/24的服务socket以index=1插入map。

是不是非常有意义!


事实上,socket查找这件事是非常灵活的,TPROXY就是其简单的case之一。我们考虑以下场景:

  • 服务器侦听192.168.56.110:80

此时如果从另一台机器发起一个到 192.168.56.110:1234 的TCP连接,很明显是不通的,理由就是用192.168.56.110:1234作为key查找socket表的时候,没有发现有任何socket侦听1234端口。

然而这只是常规的hash元组匹配算法的结果,如果你手工指定 就是让这个访问到达192.168.56.110:80这个socket ,那么事实上也是可以建连成功的,TPROXY就是干这个的,这只需要在192.168.56.110上配置下面的iptables规则:

iptables -t mangle -A PREROUTING -p tcp --dport 1234 -j TPROXY --on-port 80

这个时候,你再试试?

实际上,TPROXY还是有所限制的,比如其侦听socket必须携带TRANSPARENT选项,毕竟从TPROXY顾名思义,它截获的包一般目的地并不是它自己,它只是一个代理。

用eBPF实现上面的从1234端口到80端口的重定向就简单多了:

SEC("inet_lookup/demo_two_servers")
int redirect_to_port_80(struct bpf_inet_lookup *ctx)
{
	__u32 index = 0;

	if (ctx->family != AF_INET)
		return BPF_OK;
	if (ctx->protocol != IPPROTO_TCP)
		return BPF_OK;
	if (ctx->local_port != 1234)
		return BPF_OK;
	// 80服务socket在服务进程中以index=0插入到map中即可!
	return bpf_redirect_lookup(ctx, &sk_map, &index, 0);
}

有点意思。虽然经理可能并不认同。


浙江温州皮鞋湿,下雨进水不会胖。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
dial tcp:lookup registry-1.docker.io 这个错误通常表示的是访问注册表(Registry) 时出现了连接问题或者 DNS 解析问题。Docker 是一种开源的容器化引擎,它可以在 Linux、Windows 和 macOS 等不同的操作系统上运行,能够将应用程序及其依赖打包成可移植的容器来简化应用部署。Docker 中的镜像(Image) 是容器的基础模板,其存储在 Docker 的注册表中,当运行一个容器时,Docker 引擎会自动从注册表下载相应的镜像。所以如果出现了 dial tcp:lookup registry-1.docker.io 错误,那么我们无法下载镜像,也就无法运行容器。 造成 dial tcp:lookup registry-1.docker.io 错误的原因可能有很多,可能是网络不稳定、DNS 解析问题、代理问题以及操作系统配置等。解决这种错误的方法也有很多,可以尝试更改 DNS 解析设置、切换到稳定的网络,或者禁用代理等措施。一些解决方法如下: 1. 修改 DNS 设置。一些情况下 DNS 缓存造成了问题,需清除 DNS 缓存,新解析。另外,有时候也可以修改本地的 DNS 设置,在/etc/resolv.conf 文件中添加 Google 的 DNS 8.8.8.8 即可。命令为: echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf > /dev/null 2. 检查网络连接。首先确定本地的网络连接是否正常。可以使用 ping 命令检测 DNS 是否正常解析,然后再尝试连接 Docker 服务器。如果是代理的问题,尝试关闭代理,再试一下。 3. 更新操作系统。检查操作系统是否需要更新,特别是针对网络连接方面。 4. 更换 Docker 镜像源。由于网络原因,国内镜像源可能无法正常访问,所以可以尝试更改 Docker 镜像源为国外的源,比如 Docker 官方的镜像源等。 总之,如何解决 dial tcp:lookup registry-1.docker.io 错误是需要具体情况具体分析,解决方法也可能因具体情况而异,需要根据错误提示和具体问题来进行判断和解决。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值