TC BPFを使用したポートリダイレクト

TC BPFを使用したポートリダイレクト

あるポートから別のポートに着信TC BPFトラフィックをリダイレクトするために使用したいと思います。以下は私のコードですが、次の例も試しました。808080人8tc-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

解決策

私が受け入れた答えで説明したように、以下は受信NAT 90-> 8080と送信de-NAT 8080 -> 90を実行するTCP用のサンプルコードです。

#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エンジン(使用つながるルックアップエントリ)明示的なルールなしで応答トラフィックを自動的にde-NATし、他の場所にNATを実装します。はい無国籍そして、処理すべき2つの方向があります。着信接続の場合、これは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からのみ送信されます(すべてのポート80情報はeBPFコードの後に​​失われます))、クライアントはポート80からの回答も楽しみです。クライアントのカーネルはTCP RSTで応答し、クライアントは再試行しますが、同じ結果(接続なし)が表示されます。

同等の逆変換を実行する必要があります。出口。これらすべてがそうであるように無国籍つまり、これが完了すると、同じ理由でポート8080に直接接続できなくなります。これで同じ効果が得られます。これで、ポート 8080 への接続はポート 80 を使用して戻されます。

これとは対照的に、UDP に同じ設定を適用すると、UDP はトラフィックを受信したときに何も送信しなくて済むため、着信トラフィックにのみ適用されます。ただし、ICMPエラーを再送信すること(つまり、サーバーが受信しなくなったことをクライアントに通知する信号)は失敗します。 eBPF コードが UDP の異なる方向に対して実行されていても、ICMP エラーには、まだ UDP ペイロードの一部に無効な UDP ポートが含まれています。 NetfilterのNATもこの問題を解決できます。

関連情報