From 525fe688a8a6ea03f76bd2d4a93d506130eeb98d Mon Sep 17 00:00:00 2001 From: Dream95 Date: Fri, 10 Jul 2026 08:58:40 +0000 Subject: [PATCH 1/2] feat: more metadata from ebpf Signed-off-by: Dream95 --- cmd/conn_meta.go | 147 +++++++++++++++++++++++++++++++++++++++ cmd/load_bpf.go | 6 +- cmd/proxy.c | 105 +++++++++++++++++++++++----- cmd/proxy_arm64_bpfel.go | 18 ++++- cmd/proxy_x86_bpfel.go | 18 ++++- cmd/tcp_proxy.go | 75 +++++++++++--------- cmd/udp_proxy.go | 33 +++------ 7 files changed, 320 insertions(+), 82 deletions(-) create mode 100644 cmd/conn_meta.go diff --git a/cmd/conn_meta.go b/cmd/conn_meta.go new file mode 100644 index 0000000..19de528 --- /dev/null +++ b/cmd/conn_meta.go @@ -0,0 +1,147 @@ +package main + +import ( + "encoding/binary" + "fmt" + "net" + "strconv" + "strings" + + "github.com/cilium/ebpf" +) + +// Match flag bits — keep in sync with enum match_flags in proxy.c. +const ( + MatchCmd uint32 = 1 << 0 + MatchPid uint32 = 1 << 1 + MatchPgid uint32 = 1 << 2 + MatchContainer uint32 = 1 << 3 + MatchTracked uint32 = 1 << 4 +) + +// ConnMeta is userspace connection metadata read from eBPF maps for routing. +type ConnMeta struct { + Pid uint32 + Tgid uint32 + Pgid uint32 + Pidns uint32 + Mntns uint32 + Netns uint32 + MatchFlags uint32 + Comm string + DstIP net.IP + DstPort uint16 +} + +func (m ConnMeta) TargetAddr() string { + if m.DstIP == nil { + return "" + } + return fmt.Sprintf("%s:%d", m.DstIP.String(), m.DstPort) +} + +func (m ConnMeta) String() string { + return fmt.Sprintf("pid=%d tgid=%d pgid=%d comm=%q flags=0x%x ns(pid=%d mnt=%d net=%d) dst=%s", + m.Pid, m.Tgid, m.Pgid, m.Comm, m.MatchFlags, m.Pidns, m.Mntns, m.Netns, m.TargetAddr()) +} + +func connMetaFromBPF(meta proxyConnMeta, dstIP uint32, dstPort uint16) ConnMeta { + return ConnMeta{ + Pid: meta.Pid, + Tgid: meta.Tgid, + Pgid: meta.Pgid, + Pidns: meta.PidnsInum, + Mntns: meta.MntnsInum, + Netns: meta.NetnsInum, + MatchFlags: meta.MatchFlags, + Comm: int8ArrayToString(meta.Comm[:]), + DstIP: hostOrderIPv4(dstIP), + DstPort: dstPort, + } +} + +func int8ArrayToString(b []int8) string { + raw := make([]byte, len(b)) + for i, v := range b { + if v == 0 { + return string(raw[:i]) + } + raw[i] = byte(v) + } + return strings.TrimRight(string(raw), "\x00") +} + +// hostOrderIPv4 converts a host-order IPv4 uint32 (as stored by bpf_ntohl) to net.IP. +func hostOrderIPv4(ip uint32) net.IP { + return net.IPv4(byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip)) +} + +func parseRemoteIPv4Port(addr net.Addr) (ip uint32, port uint16, err error) { + host, portStr, err := net.SplitHostPort(addr.String()) + if err != nil { + return 0, 0, err + } + ip4 := net.ParseIP(host).To4() + if ip4 == nil { + return 0, 0, fmt.Errorf("not IPv4: %s", host) + } + p, err := strconv.Atoi(portStr) + if err != nil { + return 0, 0, err + } + if p < 0 || p > 65535 { + return 0, 0, fmt.Errorf("invalid port: %s", portStr) + } + // map_ports keys use sockops local_ip4 / sk->dst_ip4, which are IPv4 + // addresses in network byte order stored as __u32. On little-endian that + // numeric value matches NativeEndian over the wire-order byte slice + // (e.g. 127.0.0.1 -> 0x0100007f), not BigEndian (0x7f000001). + return binary.NativeEndian.Uint32(ip4), uint16(p), nil +} + +// lookupTCPConnMeta resolves ConnMeta via map_ports → map_socks using the client RemoteAddr. +func lookupTCPConnMeta(remote net.Addr, portsMap, socksMap *ebpf.Map) (ConnMeta, error) { + if portsMap == nil || socksMap == nil { + return ConnMeta{}, fmt.Errorf("tcp meta maps not available") + } + srcIP, srcPort, err := parseRemoteIPv4Port(remote) + if err != nil { + return ConnMeta{}, err + } + key := proxyPortKey{ + SrcIp: srcIP, + SrcPort: srcPort, + } + var cookie uint64 + if err := portsMap.Lookup(&key, &cookie); err != nil { + return ConnMeta{}, err + } + var sock proxySocket + if err := socksMap.Lookup(&cookie, &sock); err != nil { + return ConnMeta{}, err + } + return connMetaFromBPF(sock.Meta, sock.DstAddr, sock.DstPort), nil +} + +// lookupUDPConnMeta looks up map_udp_dest for the client address (with src_ip=0 fallback). +func lookupUDPConnMeta(clientAddr *net.UDPAddr, udpMap *ebpf.Map) (ConnMeta, error) { + if udpMap == nil { + return ConnMeta{}, fmt.Errorf("udp map not available") + } + ip4 := clientAddr.IP.To4() + if ip4 == nil { + return ConnMeta{}, fmt.Errorf("not IPv4") + } + key := proxyUdpDestKey{ + SrcIp: binary.BigEndian.Uint32(ip4), + SrcPort: uint16(clientAddr.Port), + } + var val proxyUdpDestVal + if err := udpMap.Lookup(&key, &val); err != nil { + key.SrcIp = 0 + if err := udpMap.Lookup(&key, &val); err != nil { + return ConnMeta{}, err + } + } + return connMetaFromBPF(val.Meta, val.DstIp, val.DstPort), nil +} diff --git a/cmd/load_bpf.go b/cmd/load_bpf.go index cd949b3..c760271 100644 --- a/cmd/load_bpf.go +++ b/cmd/load_bpf.go @@ -12,7 +12,7 @@ import ( "github.com/cilium/ebpf/rlimit" ) -//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cflags "-D ARCH_$TARGET" -target $TARGET -type Config proxy proxy.c -- -I../.output/ -I../libbpf/include/uapi -I../vmlinux/$TARGET +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cflags "-D ARCH_$TARGET" -target $TARGET -type Config -type ConnMeta -type Socket -type UdpDestVal -type PortKey -type UdpDestKey proxy proxy.c -- -I../.output/ -I../libbpf/include/uapi -I../vmlinux/$TARGET const ( CGROUP_PATH = "/sys/fs/cgroup" // Root cgroup path @@ -85,9 +85,9 @@ func LoadBpf(options *Options) { byte(proxyRedirectIP)) } - // Start TCP (and UDP) proxy so it can use objs.MapUdpDest for UDP original-dest lookup + // Start TCP (and UDP) proxy so it can use BPF maps for original-dest / ConnMeta lookup if options.ProxyPid == 0 { - StartProxy(objs.MapUdpDest, options.EnableTCP, options.EnableUDP, proxyListenHost, options.Mirror) + StartProxy(objs.MapUdpDest, objs.MapPorts, objs.MapSocks, options.EnableTCP, options.EnableUDP, proxyListenHost, options.Mirror) } // Attach eBPF programs to the root cgroup diff --git a/cmd/proxy.c b/cmd/proxy.c index 642f0f3..d1ca859 100644 --- a/cmd/proxy.c +++ b/cmd/proxy.c @@ -38,11 +38,34 @@ struct Config { char command[TASK_COMM_LEN]; }; +enum match_flags { + MATCH_CMD = 1 << 0, + MATCH_PID = 1 << 1, + MATCH_PGID = 1 << 2, + MATCH_CONTAINER = 1 << 3, + MATCH_TRACKED = 1 << 4, +}; + +/* Connection metadata for userspace routing (fixed-size, map-friendly). */ +struct ConnMeta { + __u32 pid; + __u32 tgid; + __u32 pgid; + __u32 pidns_inum; + __u32 mntns_inum; + __u32 netns_inum; + __u32 match_flags; + char comm[TASK_COMM_LEN]; +}; + struct Socket { __u32 src_addr; __u16 src_port; + __u16 pad0; __u32 dst_addr; __u16 dst_port; + __u16 pad1; + struct ConnMeta meta; }; struct PortKey { @@ -114,11 +137,12 @@ struct UdpDestKey { __u16 pad; }; -// Value: original destination that was redirected to proxy +// Value: original destination + ConnMeta for userspace routing struct UdpDestVal { __u32 dst_ip; __u16 dst_port; __u16 pad; + struct ConnMeta meta; }; struct { @@ -157,18 +181,25 @@ get_current_pgid(void) return BPF_CORE_READ(pgid_pid, numbers[0].nr); } -static __always_inline bool -match_container_ns(void) +static __always_inline void +read_current_ns(__u32 *pidns_inum, __u32 *mntns_inum, __u32 *netns_inum) { + *pidns_inum = 0; + *mntns_inum = 0; + *netns_inum = 0; + struct task_struct *task = (struct task_struct *)bpf_get_current_task_btf(); - if (!task) { - return false; - } + if (!task) + return; - __u32 pidns_id = BPF_CORE_READ(task, nsproxy, pid_ns_for_children, ns.inum); - __u32 mntns_id = BPF_CORE_READ(task, nsproxy, mnt_ns, ns.inum); - __u32 netns_id = BPF_CORE_READ(task, nsproxy, net_ns, ns.inum); + *pidns_inum = BPF_CORE_READ(task, nsproxy, pid_ns_for_children, ns.inum); + *mntns_inum = BPF_CORE_READ(task, nsproxy, mnt_ns, ns.inum); + *netns_inum = BPF_CORE_READ(task, nsproxy, net_ns, ns.inum); +} +static __always_inline bool +match_container_ns(__u32 pidns_id, __u32 mntns_id, __u32 netns_id) +{ if (pidns_id && bpf_map_lookup_elem(&filter_pidns_map, &pidns_id)) { return true; } @@ -182,6 +213,19 @@ match_container_ns(void) return false; } +static __always_inline void +fill_conn_meta(struct ConnMeta *meta, __u32 match_flags) +{ + __u64 pid_tgid = bpf_get_current_pid_tgid(); + __builtin_memset(meta, 0, sizeof(*meta)); + meta->tgid = pid_tgid >> 32; + meta->pid = (__u32)pid_tgid; + meta->pgid = get_current_pgid(); + meta->match_flags = match_flags; + bpf_get_current_comm(meta->comm, sizeof(meta->comm)); + read_current_ns(&meta->pidns_inum, &meta->mntns_inum, &meta->netns_inum); +} + static __always_inline bool comm_matches_command(const char comm[TASK_COMM_LEN], const char command[TASK_COMM_LEN]) { @@ -213,20 +257,24 @@ track_pid(__u32 pid) bpf_map_update_elem(&filter_tracked_map, &pid, &val, BPF_ANY); } +/* Returns whether the current process should be redirected; out_flags records why. */ static __always_inline bool -match_process(struct Config *conf) +match_process(struct Config *conf, __u32 *out_flags) { + __u32 flags = 0; bool has_cmd = conf->command[0] != '\0'; bool has_pid_filter = conf->filter_by_pid || conf->filter_by_pgid; bool has_container_filter = conf->filter_by_container; if (!has_cmd && !has_pid_filter && !has_container_filter) { + *out_flags = 0; return true; } bool cmd_matched = false; bool tracked_matched = false; bool pid_matched = false; + bool pgid_matched = false; bool container_matched = false; if (has_cmd) { @@ -261,17 +309,32 @@ match_process(struct Config *conf) if (has_pid_filter && !pid_matched && conf->filter_by_pgid) { __u32 current_pgid = get_current_pgid(); if (current_pgid && bpf_map_lookup_elem(&filter_pid_map, ¤t_pgid)) { - pid_matched = true; + pgid_matched = true; } } if (has_container_filter) { - container_matched = match_container_ns(); + __u32 pidns_id = 0, mntns_id = 0, netns_id = 0; + read_current_ns(&pidns_id, &mntns_id, &netns_id); + container_matched = match_container_ns(pidns_id, mntns_id, netns_id); } + if (cmd_matched) + flags |= MATCH_CMD; + if (tracked_matched) + flags |= MATCH_TRACKED; + if (pid_matched) + flags |= MATCH_PID; + if (pgid_matched) + flags |= MATCH_PGID; + if (container_matched) + flags |= MATCH_CONTAINER; + + *out_flags = flags; + if ((has_container_filter && container_matched) || (has_cmd && (cmd_matched || tracked_matched)) || - (has_pid_filter && pid_matched)) { + (has_pid_filter && (pid_matched || pgid_matched))) { return true; } return false; @@ -371,7 +434,8 @@ int cg_connect4(struct bpf_sock_addr *ctx) { } if (current_pid == conf->proxy_pid) return 1; - if (!match_process(conf)) + __u32 match_flags = 0; + if (!match_process(conf, &match_flags)) return 1; if (conf->filter_ip) @@ -388,6 +452,9 @@ int cg_connect4(struct bpf_sock_addr *ctx) { if (dst_addr == conf->proxy_ip && dst_port == conf->proxy_port) return 1; + struct ConnMeta meta; + fill_conn_meta(&meta, match_flags); + if (ctx->protocol == IPPROTO_TCP) { if (!conf->enable_tcp) return 1; __u64 cookie = bpf_get_socket_cookie(ctx); @@ -395,12 +462,13 @@ int cg_connect4(struct bpf_sock_addr *ctx) { __builtin_memset(&sock, 0, sizeof(sock)); sock.dst_addr = dst_addr; sock.dst_port = dst_port; + sock.meta = meta; bpf_map_update_elem(&map_socks, &cookie, &sock, 0); ctx->user_ip4 = bpf_htonl(conf->proxy_ip); ctx->user_port = bpf_htonl(conf->proxy_port << 16); - BPF_LOG_DEBUG("connect4: tcp redirect dst=%x:%u -> proxy=%x:%u pid=%u\n", - dst_addr, dst_port, conf->proxy_ip, conf->proxy_port, current_pid); + BPF_LOG_DEBUG("connect4: tcp redirect dst=%x:%u -> proxy=%x:%u pid=%u flags=%u\n", + dst_addr, dst_port, conf->proxy_ip, conf->proxy_port, current_pid, match_flags); return 1; } @@ -453,12 +521,13 @@ int cg_connect4(struct bpf_sock_addr *ctx) { __builtin_memset(&dval, 0, sizeof(dval)); dval.dst_ip = dst_addr; dval.dst_port = dst_port; + dval.meta = meta; bpf_map_update_elem(&map_udp_dest, &dkey, &dval, 0); ctx->user_ip4 = bpf_htonl(conf->proxy_ip); ctx->user_port = bpf_htonl(conf->proxy_port << 16); - BPF_LOG_DEBUG("connect4: udp redirect dst=%x:%u src_port=%u proxy=%x:%u pid=%u\n", - dst_addr, dst_port, src_port, conf->proxy_ip, conf->proxy_port, current_pid); + BPF_LOG_DEBUG("connect4: udp redirect dst=%x:%u src_port=%u proxy=%x:%u pid=%u flags=%u\n", + dst_addr, dst_port, src_port, conf->proxy_ip, conf->proxy_port, current_pid, match_flags); return 1; } diff --git a/cmd/proxy_arm64_bpfel.go b/cmd/proxy_arm64_bpfel.go index b61689e..89cb1b4 100644 --- a/cmd/proxy_arm64_bpfel.go +++ b/cmd/proxy_arm64_bpfel.go @@ -31,6 +31,18 @@ type proxyConfig struct { _ [1]byte } +type proxyConnMeta struct { + _ structs.HostLayout + Pid uint32 + Tgid uint32 + Pgid uint32 + PidnsInum uint32 + MntnsInum uint32 + NetnsInum uint32 + MatchFlags uint32 + Comm [16]int8 +} + type proxyPortKey struct { _ structs.HostLayout SrcIp uint32 @@ -42,10 +54,11 @@ type proxySocket struct { _ structs.HostLayout SrcAddr uint32 SrcPort uint16 - _ [2]byte + Pad0 uint16 DstAddr uint32 DstPort uint16 - _ [2]byte + Pad1 uint16 + Meta proxyConnMeta } type proxyUdpDestKey struct { @@ -60,6 +73,7 @@ type proxyUdpDestVal struct { DstIp uint32 DstPort uint16 Pad uint16 + Meta proxyConnMeta } // loadProxy returns the embedded CollectionSpec for proxy. diff --git a/cmd/proxy_x86_bpfel.go b/cmd/proxy_x86_bpfel.go index 88578c7..a4cacb6 100644 --- a/cmd/proxy_x86_bpfel.go +++ b/cmd/proxy_x86_bpfel.go @@ -31,6 +31,18 @@ type proxyConfig struct { _ [1]byte } +type proxyConnMeta struct { + _ structs.HostLayout + Pid uint32 + Tgid uint32 + Pgid uint32 + PidnsInum uint32 + MntnsInum uint32 + NetnsInum uint32 + MatchFlags uint32 + Comm [16]int8 +} + type proxyPortKey struct { _ structs.HostLayout SrcIp uint32 @@ -42,10 +54,11 @@ type proxySocket struct { _ structs.HostLayout SrcAddr uint32 SrcPort uint16 - _ [2]byte + Pad0 uint16 DstAddr uint32 DstPort uint16 - _ [2]byte + Pad1 uint16 + Meta proxyConnMeta } type proxyUdpDestKey struct { @@ -60,6 +73,7 @@ type proxyUdpDestVal struct { DstIp uint32 DstPort uint16 Pad uint16 + Meta proxyConnMeta } // loadProxy returns the embedded CollectionSpec for proxy. diff --git a/cmd/tcp_proxy.go b/cmd/tcp_proxy.go index 3b5c045..ba6347d 100644 --- a/cmd/tcp_proxy.go +++ b/cmd/tcp_proxy.go @@ -18,7 +18,7 @@ import ( ) // StartProxy starts TCP/UDP proxy on proxyPort based on enableTCP/enableUDP. -func StartProxy(udpMap *ebpf.Map, enableTCP bool, enableUDP bool, listenHost string, mirrorOpts MirrorOptions) { +func StartProxy(udpMap, portsMap, socksMap *ebpf.Map, enableTCP bool, enableUDP bool, listenHost string, mirrorOpts MirrorOptions) { proxyAddr := fmt.Sprintf("%s:%d", listenHost, proxyPort) mirror := NewMirrorDispatcher(mirrorOpts) if mirror != nil && strings.TrimSpace(mirrorOpts.Target) == proxyAddr { @@ -36,7 +36,7 @@ func StartProxy(udpMap *ebpf.Map, enableTCP bool, enableUDP bool, listenHost str log.Fatalf("Failed to start TCP proxy server: %v", err) } log.Printf("TCP proxy server with PID %d listening on %s", os.Getpid(), proxyAddr) - go acceptLoop(listener, mirror) + go acceptLoop(listener, mirror, portsMap, socksMap) } if enableUDP && udpMap != nil { @@ -45,7 +45,7 @@ func StartProxy(udpMap *ebpf.Map, enableTCP bool, enableUDP bool, listenHost str } } -func acceptLoop(listener net.Listener, mirror *MirrorDispatcher) { +func acceptLoop(listener net.Listener, mirror *MirrorDispatcher, portsMap, socksMap *ebpf.Map) { defer listener.Close() for { conn, err := listener.Accept() @@ -54,7 +54,7 @@ func acceptLoop(listener net.Listener, mirror *MirrorDispatcher) { continue } - go handleConnection(conn, mirror) + go handleConnection(conn, mirror, portsMap, socksMap) } } @@ -66,10 +66,10 @@ func getsockopt(s int, level int, optname int, optval unsafe.Pointer, optlen *ui return } -func handleConnection(conn net.Conn, mirror *MirrorDispatcher) { +func handleConnection(conn net.Conn, mirror *MirrorDispatcher, portsMap, socksMap *ebpf.Map) { defer conn.Close() - targetConn, err := getTargetConnection(conn) + targetConn, err := getTargetConnection(conn, portsMap, socksMap) if err != nil { log.Printf("Connection error: %v", err) return @@ -189,31 +189,7 @@ func isExpectedCopyError(err error) bool { return false } -func getTargetConnection(conn net.Conn) (net.Conn, error) { - - // Using RawConn is necessary to perform low-level operations on the underlying socket file descriptor in Go. - // This allows us to use getsockopt to retrieve the original destination address set by the SO_ORIGINAL_DST option, - // which isn't directly accessible through Go's higher-level networking API. - rawConn, err := conn.(*net.TCPConn).SyscallConn() - if err != nil { - log.Printf("Failed to get raw connection: %v", err) - return nil, err - } - - var originalDst SockAddrIn - // If Control is not nil, it is called after creating the network connection but before binding it to the operating system. - rawConn.Control(func(fd uintptr) { - optlen := uint32(unsafe.Sizeof(originalDst)) - // Retrieve the original destination address by making a syscall with the SO_ORIGINAL_DST option. - err = getsockopt(int(fd), syscall.SOL_IP, SO_ORIGINAL_DST, unsafe.Pointer(&originalDst), &optlen) - if err != nil { - log.Printf("getsockopt SO_ORIGINAL_DST failed: %v", err) - } - }) - - targetAddr := net.IPv4(originalDst.SinAddr[0], originalDst.SinAddr[1], originalDst.SinAddr[2], originalDst.SinAddr[3]).String() - targetPort := (uint16(originalDst.SinPort[0]) << 8) | uint16(originalDst.SinPort[1]) - +func getTargetConnection(conn net.Conn, portsMap, socksMap *ebpf.Map) (net.Conn, error) { sourceAddr := conn.RemoteAddr().String() sourceIP, sourcePort, splitErr := net.SplitHostPort(sourceAddr) if splitErr != nil { @@ -221,9 +197,42 @@ func getTargetConnection(conn net.Conn) (net.Conn, error) { sourcePort = "unknown" } - log.Printf("TCP Source: %s:%s -> Original destination: %s:%d", sourceIP, sourcePort, targetAddr, targetPort) + var target string + if meta, err := lookupTCPConnMeta(conn.RemoteAddr(), portsMap, socksMap); err == nil { + target = meta.TargetAddr() + log.Printf("TCP Source: %s:%s -> Original destination: %s meta={%s}", sourceIP, sourcePort, target, meta) + } else { + // Fallback: SO_ORIGINAL_DST restores destination; also retries map + // lookup afterward in case sockops had not yet populated map_ports. + rawConn, rawErr := conn.(*net.TCPConn).SyscallConn() + if rawErr != nil { + log.Printf("Failed to get raw connection: %v", rawErr) + return nil, rawErr + } + + var originalDst SockAddrIn + var sockErr error + rawConn.Control(func(fd uintptr) { + optlen := uint32(unsafe.Sizeof(originalDst)) + sockErr = getsockopt(int(fd), syscall.SOL_IP, SO_ORIGINAL_DST, unsafe.Pointer(&originalDst), &optlen) + }) + if sockErr != nil { + log.Printf("getsockopt SO_ORIGINAL_DST failed: %v (map lookup: %v)", sockErr, err) + return nil, sockErr + } + + targetAddr := net.IPv4(originalDst.SinAddr[0], originalDst.SinAddr[1], originalDst.SinAddr[2], originalDst.SinAddr[3]).String() + targetPort := (uint16(originalDst.SinPort[0]) << 8) | uint16(originalDst.SinPort[1]) + target = fmt.Sprintf("%s:%d", targetAddr, targetPort) - target := fmt.Sprintf("%s:%d", targetAddr, targetPort) + if meta, retryErr := lookupTCPConnMeta(conn.RemoteAddr(), portsMap, socksMap); retryErr == nil { + target = meta.TargetAddr() + log.Printf("TCP Source: %s:%s -> Original destination: %s meta={%s}", sourceIP, sourcePort, target, meta) + } else { + log.Printf("TCP Source: %s:%s -> Original destination: %s (SO_ORIGINAL_DST fallback, map err: %v)", + sourceIP, sourcePort, target, err) + } + } if httpProxyAddr != "" { targetConn, err := dialViaHTTPConnect(httpProxyAddr, target) diff --git a/cmd/udp_proxy.go b/cmd/udp_proxy.go index 7d16cb4..a105a3e 100644 --- a/cmd/udp_proxy.go +++ b/cmd/udp_proxy.go @@ -1,7 +1,6 @@ package main import ( - "encoding/binary" "errors" "fmt" "log" @@ -108,13 +107,13 @@ func (m *udpSessionManager) dispatch(clientAddr *net.UDPAddr, payload []byte) { } func (m *udpSessionManager) createSession(clientAddr *net.UDPAddr) (*udpSession, error) { - targetAddr, err := getUDPOriginalDest(clientAddr, m.udpMap) + meta, err := lookupUDPConnMeta(clientAddr, m.udpMap) if err != nil { log.Printf("UDP proxy: lookup original dest for %s: %v", clientAddr, err) return nil, err } - targetAddr = maybeRewriteLocalDNSStub(targetAddr) - log.Printf("UDP Original destination: %s", targetAddr) + targetAddr := maybeRewriteLocalDNSStub(meta.TargetAddr()) + log.Printf("UDP Original destination: %s meta={%s}", targetAddr, meta) var remoteConn net.Conn if socks5ProxyAddr == "" { @@ -252,28 +251,14 @@ func maybeRewriteLocalDNSStub(targetAddr string) string { return publicDNSAddr } -// getUDPOriginalDest looks up the BPF map with key (clientIP, clientPort) and returns "ip:port". -// BPF stores: key src_ip (network order), src_port (host); value dst_ip (network order), dst_port (host). +// getUDPOriginalDest looks up the BPF map and returns "ip:port". +// Prefer lookupUDPConnMeta when ConnMeta is needed. func getUDPOriginalDest(clientAddr *net.UDPAddr, udpMap *ebpf.Map) (string, error) { - ip4 := clientAddr.IP.To4() - if ip4 == nil { - return "", fmt.Errorf("not IPv4") - } - key := proxyUdpDestKey{ - SrcIp: binary.BigEndian.Uint32(ip4), - SrcPort: uint16(clientAddr.Port), - } - var val proxyUdpDestVal - if err := udpMap.Lookup(&key, &val); err != nil { - // Fallback entry keyed only by source port for container/bridge paths. - key.SrcIp = 0 - if err := udpMap.Lookup(&key, &val); err != nil { - return "", err - } + meta, err := lookupUDPConnMeta(clientAddr, udpMap) + if err != nil { + return "", err } - // DstIp is network order (big-endian), DstPort is host order - targetIP := net.IPv4(byte(val.DstIp>>24), byte(val.DstIp>>16), byte(val.DstIp>>8), byte(val.DstIp)) - return fmt.Sprintf("%s:%d", targetIP.String(), val.DstPort), nil + return meta.TargetAddr(), nil } func dialUDPViaSOCKS5(targetAddr string) (net.Conn, error) { From ec3fb6c6d887a7f0426a1b17cd07280db8dda0de Mon Sep 17 00:00:00 2001 From: Dream95 Date: Sun, 12 Jul 2026 07:35:11 +0000 Subject: [PATCH 2/2] test: add conn meta test Signed-off-by: Dream95 --- scripts/test_conn_meta.sh | 390 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100755 scripts/test_conn_meta.sh diff --git a/scripts/test_conn_meta.sh b/scripts/test_conn_meta.sh new file mode 100755 index 0000000..5dae860 --- /dev/null +++ b/scripts/test_conn_meta.sh @@ -0,0 +1,390 @@ +#!/usr/bin/env bash +# gotproxy ConnMeta integration test (pid / cmd / flags / dst) +# Usage: sudo ./scripts/test_conn_meta.sh +# Requires: built gotproxy, curl; root (CAP_BPF). UDP case needs dig (optional). + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +GOTPROXY_BIN="${GOTPROXY_BIN:-$REPO_ROOT/gotproxy}" +PROXY_PORT="${PROXY_PORT:-18003}" +LOG_FILE="" +GOTPROXY_PID="" + +TEST_HOST="${TEST_HOST:-one.one.one.one}" +EXAMPLE_IP="${EXAMPLE_IP:-1.1.1.1}" +TEST_URL="${TEST_URL:-https://${TEST_HOST}/}" + +# Keep in sync with enum match_flags / Match* in proxy.c / conn_meta.go +MATCH_CMD=$((1 << 0)) +MATCH_PID=$((1 << 1)) +MATCH_PGID=$((1 << 2)) +MATCH_CONTAINER=$((1 << 3)) +MATCH_TRACKED=$((1 << 4)) + +PASSED=0 +FAILED=0 + +info() { echo "[INFO] $*"; } +ok() { echo "[OK] $*"; ((PASSED++)) || true; } +fail() { echo "[FAIL] $*"; ((FAILED++)) || true; } +abort() { + echo "[ABORT] $*" + if [[ -n "${LOG_FILE:-}" && -f "$LOG_FILE" ]]; then + echo "[ABORT] gotproxy log:" + cat "$LOG_FILE" + fi + stop_gotproxy 2>/dev/null + exit 1 +} + +start_gotproxy() { + local extra_args=("$@") + LOG_FILE=$(mktemp) + "$GOTPROXY_BIN" --p-port "$PROXY_PORT" "${extra_args[@]}" >"$LOG_FILE" 2>&1 & + GOTPROXY_PID=$! + for _ in {1..30}; do + if grep -q "listening on" "$LOG_FILE" 2>/dev/null; then + # StartProxy logs before cgroup attach + MapConfig update; give BPF a moment. + sleep 1 + return 0 + fi + sleep 0.2 + done + abort "gotproxy did not start in time, see $LOG_FILE" +} + +stop_gotproxy() { + if [[ -n "$GOTPROXY_PID" ]] && kill -0 "$GOTPROXY_PID" 2>/dev/null; then + kill "$GOTPROXY_PID" 2>/dev/null || true + wait "$GOTPROXY_PID" 2>/dev/null || true + fi + GOTPROXY_PID="" + [[ -n "$LOG_FILE" && -f "$LOG_FILE" ]] && rm -f "$LOG_FILE" + LOG_FILE="" +} + +check_env() { + if [[ "$(id -u)" -ne 0 ]]; then + abort "Please run as root: sudo $0" + fi + if [[ ! -x "$GOTPROXY_BIN" ]]; then + abort "gotproxy not found or not executable: $GOTPROXY_BIN. Run make build-bpf && make first." + fi + if ! command -v curl &>/dev/null; then + abort "curl not found. Please install curl." + fi + info "Using gotproxy=$GOTPROXY_BIN, port=$PROXY_PORT" +} + +# Extract the first TCP/UDP meta={...} line that mentions $dst_hint (optional). +# Prints the full meta={...} blob. +find_meta_blob() { + local proto="$1" # tcp|udp + local dst_hint="${2:-}" + [[ -z "$LOG_FILE" || ! -f "$LOG_FILE" ]] && return 1 + + local pattern + if [[ "$proto" == "udp" ]]; then + pattern='UDP Original destination:.*meta=\{[^}]+\}' + else + pattern='Original destination:.*meta=\{[^}]+\}' + fi + + local line + line=$(grep -E "$pattern" "$LOG_FILE" 2>/dev/null | head -n1) || true + if [[ -n "$dst_hint" ]]; then + line=$(grep -E "$pattern" "$LOG_FILE" 2>/dev/null | grep -F "$dst_hint" | head -n1) || true + fi + [[ -z "$line" ]] && return 1 + echo "$line" | grep -oE 'meta=\{[^}]+\}' +} + +parse_meta_field() { + local blob="$1" + local key="$2" + case "$key" in + comm) + echo "$blob" | sed -n 's/.*comm="\([^"]*\)".*/\1/p' + ;; + flags) + echo "$blob" | sed -n 's/.*flags=\(0x[0-9a-fA-F]*\).*/\1/p' + ;; + dst) + echo "$blob" | sed -n 's/.*dst=\([^ }]*\).*/\1/p' + ;; + pid) + # Prefer leading meta={pid=} — avoid matching ns(pid=...). + echo "$blob" | sed -n 's/^meta={pid=\([0-9][0-9]*\).*/\1/p' + ;; + *) + echo "$blob" | sed -n "s/.*${key}=\([0-9][0-9]*\).*/\1/p" + ;; + esac +} + +flags_has() { + local flags_hex="$1" + local bit="$2" + local flags=$((flags_hex)) + [[ $((flags & bit)) -ne 0 ]] +} + +# Run curl under a known PID (same PID after exec). +# Writes PID to $1, sleeps so caller can start gotproxy + settle BPF, then exec curl. +# Sets global _HELPER_PID (do not capture via $() — that ties curl stdout to a pipe and SIGPIPEs it). +run_curl_with_known_pid() { + local pid_file="$1" + local delay_secs="${2:-5}" + ( + echo "$BASHPID" >"$pid_file" + sleep "$delay_secs" + exec curl -sS -4 -o /dev/null -w "" --connect-timeout 15 \ + --resolve "${TEST_HOST}:443:${EXAMPLE_IP}" \ + "$TEST_URL" + ) >/dev/null 2>&1 & + _HELPER_PID=$! +} + +# True if ConnMeta pid (TID) or tgid (TGID) equals the expected process id. +meta_pid_matches() { + local blob="$1" + local expect_pid="$2" + local pid tgid + pid=$(parse_meta_field "$blob" pid) + tgid=$(parse_meta_field "$blob" tgid) + [[ "$pid" == "$expect_pid" || "$tgid" == "$expect_pid" ]] +} + +assert_meta() { + local label="$1" + local blob="$2" + local expect_pid="$3" + local expect_comm="$4" + local expect_flag_bit="$5" + local expect_dst_substr="$6" + + local pid tgid comm flags dst + pid=$(parse_meta_field "$blob" pid) + tgid=$(parse_meta_field "$blob" tgid) + comm=$(parse_meta_field "$blob" comm) + flags=$(parse_meta_field "$blob" flags) + dst=$(parse_meta_field "$blob" dst) + + info "$label meta: pid=$pid tgid=$tgid comm=$comm flags=$flags dst=$dst" + + local bad=0 + if [[ -n "$expect_pid" ]] && ! meta_pid_matches "$blob" "$expect_pid"; then + fail "$label: expected pid or tgid=$expect_pid, got pid=$pid tgid=$tgid" + bad=1 + fi + if [[ -n "$expect_comm" && "$comm" != "$expect_comm" ]]; then + fail "$label: expected comm=$expect_comm, got comm=$comm" + bad=1 + fi + if [[ -n "$expect_flag_bit" ]]; then + if [[ -z "$flags" ]] || ! flags_has "$flags" "$expect_flag_bit"; then + fail "$label: expected flags to include bit $expect_flag_bit, got flags=$flags" + bad=1 + fi + fi + if [[ -n "$expect_dst_substr" && "$dst" != *"$expect_dst_substr"* ]]; then + fail "$label: expected dst to contain $expect_dst_substr, got dst=$dst" + bad=1 + fi + if [[ -z "$tgid" || "$tgid" == "0" ]]; then + fail "$label: tgid missing or zero" + bad=1 + fi + + if [[ "$bad" -eq 0 ]]; then + ok "$label: ConnMeta pid/comm/flags/dst look correct" + fi +} + +# --- TCP: --cmd curl should report curl's pid/comm and MATCH_CMD --- +test_tcp_cmd_meta() { + info "Test: TCP ConnMeta with --cmd curl" + # Start proxy first so BPF is ready before traffic (avoids race with helper sleep). + start_gotproxy --cmd "curl" + + local pid_file helper_pid expect_pid + pid_file=$(mktemp) + ( + echo "$BASHPID" >"$pid_file" + exec curl -sS -4 -o /dev/null -w "" --connect-timeout 15 \ + --resolve "${TEST_HOST}:443:${EXAMPLE_IP}" \ + "$TEST_URL" + ) & + helper_pid=$! + wait "$helper_pid" 2>/dev/null || true + expect_pid=$(cat "$pid_file" 2>/dev/null || true) + + local blob log_snapshot="" + blob=$(find_meta_blob tcp "$EXAMPLE_IP") || true + [[ -n "$LOG_FILE" && -f "$LOG_FILE" ]] && log_snapshot=$(cat "$LOG_FILE") + stop_gotproxy + rm -f "$pid_file" + + if [[ -z "$expect_pid" ]]; then + fail "TCP --cmd meta: could not get curl PID" + return + fi + if [[ -z "$blob" ]]; then + fail "TCP --cmd meta: no meta={...} line with $EXAMPLE_IP in log" + [[ -n "$log_snapshot" ]] && { info "log:"; echo "$log_snapshot"; } + return + fi + assert_meta "TCP --cmd" "$blob" "$expect_pid" "curl" "$MATCH_CMD" "$EXAMPLE_IP" +} + +# --- TCP: --pids should report that pid and MATCH_PID --- +test_tcp_pid_meta() { + info "Test: TCP ConnMeta with --pids" + local pid_file helper_pid expect_pid + pid_file=$(mktemp) + # Need PID before starting gotproxy; delay must cover start + BPF settle. + run_curl_with_known_pid "$pid_file" 5 + helper_pid="$_HELPER_PID" + local i=0 + while [[ ! -s "$pid_file" ]] && [[ $i -lt 50 ]]; do sleep 0.2; i=$((i + 1)); done + expect_pid=$(cat "$pid_file" 2>/dev/null || true) + if [[ -z "$expect_pid" ]]; then + kill "$helper_pid" 2>/dev/null || true + wait "$helper_pid" 2>/dev/null || true + rm -f "$pid_file" + fail "TCP --pids meta: could not get curl helper PID" + return + fi + + start_gotproxy --pids "$expect_pid" + wait "$helper_pid" 2>/dev/null || true + + local blob log_snapshot="" + blob=$(find_meta_blob tcp "$EXAMPLE_IP") || true + [[ -n "$LOG_FILE" && -f "$LOG_FILE" ]] && log_snapshot=$(cat "$LOG_FILE") + stop_gotproxy + rm -f "$pid_file" + + if [[ -z "$blob" ]]; then + fail "TCP --pids meta: no meta={...} line with $EXAMPLE_IP in log" + [[ -n "$log_snapshot" ]] && { info "log:"; echo "$log_snapshot"; } + return + fi + + local flags pid tgid comm dst + flags=$(parse_meta_field "$blob" flags) + pid=$(parse_meta_field "$blob" pid) + tgid=$(parse_meta_field "$blob" tgid) + comm=$(parse_meta_field "$blob" comm) + dst=$(parse_meta_field "$blob" dst) + info "TCP --pids meta: pid=$pid tgid=$tgid comm=$comm flags=$flags dst=$dst" + + local bad=0 + if ! meta_pid_matches "$blob" "$expect_pid"; then + fail "TCP --pids: expected pid or tgid=$expect_pid, got pid=$pid tgid=$tgid" + bad=1 + fi + if [[ "$comm" != "curl" ]]; then + fail "TCP --pids: expected comm=curl, got comm=$comm" + bad=1 + fi + if ! flags_has "$flags" "$MATCH_PID" && ! flags_has "$flags" "$MATCH_PGID"; then + fail "TCP --pids: expected MATCH_PID or MATCH_PGID in flags=$flags" + bad=1 + fi + if [[ "$dst" != *"$EXAMPLE_IP"* ]]; then + fail "TCP --pids: expected dst to contain $EXAMPLE_IP, got dst=$dst" + bad=1 + fi + if [[ "$bad" -eq 0 ]]; then + ok "TCP --pids: ConnMeta pid/comm/flags/dst look correct" + fi +} + +# Send one UDP datagram to EXAMPLE_IP:53 from the current process (no worker threads). +# Must call connect() — gotproxy only hooks cgroup/connect4; bare sendto() is invisible. +udp_send_probe() { + if command -v python3 &>/dev/null; then + exec python3 -c "import socket; s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(2); s.connect(('${EXAMPLE_IP}', 53)); s.send(b'\\x00'); s.close()" + fi + if command -v nc &>/dev/null; then + # Prefer connected UDP if supported; fall back to -u. + exec nc -u -w1 "$EXAMPLE_IP" 53 /dev/udp/${EXAMPLE_IP}/53" +} + +# --- UDP: single-process client should report matching pid/comm and MATCH_CMD --- +test_udp_cmd_meta() { + local udp_comm="" + if command -v python3 &>/dev/null; then + udp_comm="python3" + elif command -v nc &>/dev/null; then + udp_comm="nc" + else + udp_comm="bash" + fi + + info "Test: UDP ConnMeta with --cmd ${udp_comm}" + # Start proxy first; dig-style races are avoided by sending only after BPF is ready. + start_gotproxy --cmd "$udp_comm" --follow-forks=false --proto udp + + local pid_file helper_pid expect_pid + pid_file=$(mktemp) + ( + echo "$BASHPID" >"$pid_file" + udp_send_probe + ) &>/dev/null & + helper_pid=$! + wait "$helper_pid" 2>/dev/null || true + expect_pid=$(cat "$pid_file" 2>/dev/null || true) + + local blob log_snapshot="" + blob=$(find_meta_blob udp "${EXAMPLE_IP}:53") || true + # Also accept meta.dst without requiring it on the "Original destination" prefix. + if [[ -z "$blob" ]]; then + blob=$(find_meta_blob udp "$EXAMPLE_IP") || true + fi + [[ -n "$LOG_FILE" && -f "$LOG_FILE" ]] && log_snapshot=$(cat "$LOG_FILE") + stop_gotproxy + rm -f "$pid_file" + + if [[ -z "$expect_pid" ]]; then + fail "UDP --cmd meta: could not get helper PID" + return + fi + if [[ -z "$blob" ]]; then + fail "UDP --cmd meta: no UDP meta={...} line with ${EXAMPLE_IP}:53 in log" + [[ -n "$log_snapshot" ]] && { info "log:"; echo "$log_snapshot"; } + return + fi + + assert_meta "UDP --cmd" "$blob" "$expect_pid" "$udp_comm" "$MATCH_CMD" "$EXAMPLE_IP" + + if grep -qE "UDP Original destination:.*${EXAMPLE_IP}:53" <<<"$log_snapshot" 2>/dev/null || \ + grep -qE "meta=\{.*dst=${EXAMPLE_IP}:53" <<<"$log_snapshot" 2>/dev/null; then + ok "UDP --cmd: log shows destination ${EXAMPLE_IP}:53" + else + fail "UDP --cmd: missing destination ${EXAMPLE_IP}:53 in log line" + fi +} + +main() { + check_env + echo "==========================================" + echo " gotproxy ConnMeta tests" + echo "==========================================" + test_tcp_cmd_meta + test_tcp_pid_meta + test_udp_cmd_meta + echo "==========================================" + echo " Passed: $PASSED Failed: $FAILED" + echo "==========================================" + [[ "$FAILED" -eq 0 ]] && exit 0 || exit 1 +} + +trap 'stop_gotproxy; exit 130' INT TERM +main "$@"