我想用来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 也可以解决这个问题。