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를 구현합니다. 예무국적그리고 처리해야 할 두 가지 방향이 있습니다. 들어오는 연결의 경우 이는 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도 이 문제를 해결할 수 있습니다.

관련 정보