使用 TC BPF 重定向端口

使用 TC BPF 重定向端口

我想用来TC BPF将传入流量从一个端口重定向80到另一个端口8080。下面是我自己的代码,但我也尝试过来自的示例人 8 tc-bpf(搜索8080),我得到相同的结果。

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/tcp.h>
#include <linux/in.h>
#include <linux/ip.h>

#include <linux/filter.h>

static inline void set_tcp_dport(struct __sk_buff *skb, int nh_off,
                                            __u16 old_port, __u16 new_port)
{
    bpf_l4_csum_replace(skb, nh_off + offsetof(struct tcphdr, check),
                        old_port, new_port, sizeof(new_port));
    bpf_skb_store_bytes(skb, nh_off + offsetof(struct tcphdr, dest),
                        &new_port, sizeof(new_port), 0);
}

SEC("tc_my")
int tc_bpf_my(struct __sk_buff *skb)
{
    struct iphdr ip;
    struct tcphdr tcp;
    if (0 != bpf_skb_load_bytes(skb, sizeof(struct ethhdr), &ip, sizeof(struct iphdr))) {
        bpf_printk("bpf_skb_load_bytes iph failed");
        return TC_ACT_OK;
    }

    if (0 != bpf_skb_load_bytes(skb, sizeof(struct ethhdr) + (ip.ihl << 2), &tcp, sizeof(struct tcphdr))) {
        bpf_printk("bpf_skb_load_bytes ethh failed");
        return TC_ACT_OK;
    }

    unsigned int src_port = bpf_ntohs(tcp.source);
    unsigned int dst_port = bpf_ntohs(tcp.dest);

    if (src_port == 80 || dst_port == 80 || src_port == 8080 || dst_port == 8080)
        bpf_printk("%pI4:%u -> %pI4:%u", &ip.saddr, src_port, &ip.daddr, dst_port);

    if (dst_port != 80)
        return TC_ACT_OK;

    set_tcp_dport(skb, ETH_HLEN + sizeof(struct iphdr), __constant_htons(80), __constant_htons(8080));

    return TC_ACT_OK;
}

char LICENSE[] SEC("license") = "GPL";

在机器A上,我正在运行:

clang -g -O2 -Wall -target bpf -c tc_my.c -o tc_my.o
tc qdisc add dev ens160 clsact
tc filter add dev ens160 ingress bpf da obj tc_my.o sec tc_my
nc -l 8080

在机器B上:

nc $IP_A 80

在机器 B 上,nc似乎已连接,但ss显示:

SYN-SENT   0      1       $IP_B:53442   $IP_A:80    users:(("nc",pid=30180,fd=3))

SYN-RECV在机器 A 上,连接在断开之前仍保持连接。

我期望我的程序表现得就像我添加了这条iptables规则:

iptables -t nat -A PREROUTING -p tcp -m tcp --dport 80 -j REDIRECT --to-port 8080

也许我的期望是错误的,但我想了解为什么。我怎样才能让我的TC BPF重定向生效?

解决方案

按照我接受的答案中的解释,这里是一个适用于 TCP 的示例代码,执行入口 NAT 90->8080 和出口 de-NAT 8080->90。

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/tcp.h>
#include <linux/in.h>
#include <linux/ip.h>

#include <linux/filter.h>

static inline void set_tcp_dport(struct __sk_buff *skb, int nh_off,
                                 __u16 old_port, __u16 new_port)
{
    bpf_l4_csum_replace(skb, nh_off + offsetof(struct tcphdr, check),
                        old_port, new_port, sizeof(new_port));
    bpf_skb_store_bytes(skb, nh_off + offsetof(struct tcphdr, dest),
                        &new_port, sizeof(new_port), 0);
}

static inline void set_tcp_sport(struct __sk_buff *skb, int nh_off,
                                 __u16 old_port, __u16 new_port)
{
    bpf_l4_csum_replace(skb, nh_off + offsetof(struct tcphdr, check),
                        old_port, new_port, sizeof(new_port));
    bpf_skb_store_bytes(skb, nh_off + offsetof(struct tcphdr, source),
                        &new_port, sizeof(new_port), 0);
}

SEC("tc_ingress")
int tc_ingress_(struct __sk_buff *skb)
{
    struct iphdr ip;
    struct tcphdr tcp;
    if (0 != bpf_skb_load_bytes(skb, sizeof(struct ethhdr), &ip, sizeof(struct iphdr)))
    {
        bpf_printk("bpf_skb_load_bytes iph failed");
        return TC_ACT_OK;
    }

    if (0 != bpf_skb_load_bytes(skb, sizeof(struct ethhdr) + (ip.ihl << 2), &tcp, sizeof(struct tcphdr)))
    {
        bpf_printk("bpf_skb_load_bytes ethh failed");
        return TC_ACT_OK;
    }

    unsigned int src_port = bpf_ntohs(tcp.source);
    unsigned int dst_port = bpf_ntohs(tcp.dest);

    if (src_port == 90 || dst_port == 90 || src_port == 8080 || dst_port == 8080)
        bpf_printk("INGRESS %pI4:%u -> %pI4:%u", &ip.saddr, src_port, &ip.daddr, dst_port);

    if (dst_port != 90)
        return TC_ACT_OK;

    set_tcp_dport(skb, ETH_HLEN + sizeof(struct iphdr), __constant_htons(90), __constant_htons(8080));

    return TC_ACT_OK;
}

SEC("tc_egress")
int tc_egress_(struct __sk_buff *skb)
{
    struct iphdr ip;
    struct tcphdr tcp;
    if (0 != bpf_skb_load_bytes(skb, sizeof(struct ethhdr), &ip, sizeof(struct iphdr)))
    {
        bpf_printk("bpf_skb_load_bytes iph failed");
        return TC_ACT_OK;
    }

    if (0 != bpf_skb_load_bytes(skb, sizeof(struct ethhdr) + (ip.ihl << 2), &tcp, sizeof(struct tcphdr)))
    {
        bpf_printk("bpf_skb_load_bytes ethh failed");
        return TC_ACT_OK;
    }

    unsigned int src_port = bpf_ntohs(tcp.source);
    unsigned int dst_port = bpf_ntohs(tcp.dest);

    if (src_port == 90 || dst_port == 90 || src_port == 8080 || dst_port == 8080)
        bpf_printk("EGRESS %pI4:%u -> %pI4:%u", &ip.saddr, src_port, &ip.daddr, dst_port);

    if (src_port != 8080)
        return TC_ACT_OK;

    set_tcp_sport(skb, ETH_HLEN + sizeof(struct iphdr), __constant_htons(8080), __constant_htons(90));

    return TC_ACT_OK;
}

char LICENSE[] SEC("license") = "GPL";

以下是我在程序中构建和加载不同部分的方法:

clang -g -O2 -Wall -target bpf -c tc_my.c -o tc_my.o
tc filter add dev ens32 ingress bpf da obj /tc_my.o sec tc_ingress
tc filter add dev ens32 egress bpf da obj /tc_my.o sec tc_egress

答案1

与 Netfilter 相反,Netfilter 包括有状态的NAT 引擎(使用连线查找条目),并且会自动对回复流量进行去 NAT,而无需明确的规则告诉它这样做,在其他地方实施 NAT 是无国籍的并且需要处理两个方向。对于传入连接,这意味着在以下位置处理 NAT入口 但是也处理 de-NAT出口明确地。

正如跑步所见证的tcp转储在客户端:

# tcpdump -ttt -l -n -s0 -p -i lxcbr0 tcp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on lxcbr0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
 00:00:00.000000 IP 10.0.3.1.52542 > 10.0.3.214.80: Flags [S], seq 3033230443, win 64240, options [mss 1460,sackOK,TS val 2154801903 ecr 0,nop,wscale 7], length 0
 00:00:00.000058 IP 10.0.3.214.8080 > 10.0.3.1.52542: Flags [S.], seq 1400064141, ack 3033230444, win 65160, options [mss 1460,sackOK,TS val 3949758745 ecr 2154801903,nop,wscale 7], length 0
 00:00:00.000013 IP 10.0.3.1.52542 > 10.0.3.214.8080: Flags [R], seq 3033230444, win 0, length 0

当前的 eBPF 代码只完成了第一部分。因此,传入端口 80 的 TCP 数据包确实会在网络堆栈的任何其他部分知道之前切换到端口 8080,但随后回复流量将仅从端口 8080 发出(在 eBPF 代码之后,任何端口 80 的信息都会丢失) ),而客户端也期望来自端口 80 的回复:客户端的内核使用 TCP RST 回复,客户端再次尝试,结果相同:无连接。

必须进行等效逆变换出口。正如这一切都是无国籍的这意味着一旦完成,将不再可能出于相同的原因直接连接到端口 8080:然后会发生相同的效果:现在将使用端口 80 回复到端口 8080 的连接。

相比之下,对 UDP 应用等效设置仅适用于传入流量,因为 UDP 在接收流量时不需要发回任何内容。但是发回 ICMP 错误(例如向客户端发出信号,表明服务器不再侦听)将会失败。即使 eBPF 代码是针对 UDP 的另一个方向完成的,ICMP 错误仍会在其部分 UDP 负载中包含错误的 UDP 端口。 Netfilter 的 NAT 也可以解决这个问题。

相关内容