diff --git a/README.md b/README.md index 0b46b95..dae911f 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ sudo ./gotproxy [flags] | :--- | :--- | | **--cmd** | The command name to be proxied. If not provided, all traffic will be proxied globally. | | **--pids** | The pid to be proxied, seperate by ','. | +| **--container-name** | The container name to be proxied (Docker running container name). | | **--ip** | The Target IP address to be proxied. Supports IPv4 and IPv4 CIDR notation.| | **--p-pid** | The process ID of the proxy. If not provided, the program will automatically start a forwarding proxy. | | **--p-port** | The proxy port. | @@ -75,6 +76,17 @@ sudo ./gotproxy --proto tcp sudo ./gotproxy --proto udp ``` +5. Proxy by container name: +```bash +sudo ./gotproxy --container-name test-kyanos +``` + +6. Use container and pid together: +```bash +sudo ./gotproxy --container-name test-kyanos --pids 1234 +``` +When multiple process/container filters are specified (such as `--container-name`, `--cmd`, `--pids`), they use OR semantics: matching any one filter will be proxied. + ## Known Limitations ## * Theoretically, a connection should be determined by a 5-tuple, but for most cases, connection mapping is currently based only on protocol type and source port. diff --git a/README_CN.md b/README_CN.md index 3458119..9c834c2 100644 --- a/README_CN.md +++ b/README_CN.md @@ -36,6 +36,7 @@ sudo ./gotproxy [flags] | :--- | :--- | | **--cmd** | 需要代理的进程名称. 如果没有配置,则会进行全局流量代理. | | **--pids** | 需要代理的进程id, 按照逗号进行分割. | +| **--container-name** | 需要代理的容器名称(Docker 运行中的容器名)。 | | **--ip** | 需要代理的目标ip. 支持ipv4和ipv4 CIDR.| | **--p-pid** | 代理程序的进程id. 会自动过滤不代理该进程的网络通信,以免网络循环。如果没有配置, 本程序会自动启动一个转发代理服务. | | **--p-port** | 代理服务监听的端口。 | @@ -80,6 +81,17 @@ sudo ./gotproxy --proto tcp sudo ./gotproxy --proto udp ``` +5. 按容器名称代理: +```bash +sudo ./gotproxy --container-name test-kyanos +``` + +6. 容器名 + pid 同时过滤: +```bash +sudo ./gotproxy --container-name test-kyanos --pids 1234 +``` +当同时配置多个进程/容器过滤条件(如 `--container-name`、`--cmd`、`--pids`)时,使用 OR 关系:命中任意一个条件就会被代理。 + ## 已知限制: * 理论上应该根据5元组确定一个连接,但是考虑大多数情况目前只根据协议类型和源端口进行连接映射。 * 在根据进程名称进行代理的场景中,如果进程启动了子进程并使用了execve执行一个新命令,会无法进行代理。 diff --git a/cmd/cmd.go b/cmd/cmd.go index 808ac88..7a76932 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -15,6 +15,7 @@ var ( proxyPort uint16 proxyPid uint64 pids []string + containerName string ipStr string socks5ProxyAddr string socks5User string @@ -43,11 +44,12 @@ var rootCmd = &cobra.Command{ } Options := &Options{ - Command: command, - ProxyPid: proxyPid, - ProxyPort: proxyPort, - EnableTCP: enableTCP, - EnableUDP: enableUDP, + Command: command, + ProxyPid: proxyPid, + ProxyPort: proxyPort, + ContainerName: containerName, + EnableTCP: enableTCP, + EnableUDP: enableUDP, } if ok, err := common.HasPermission(); err != nil { @@ -93,6 +95,7 @@ func init() { rootCmd.PersistentFlags().Uint16Var(&proxyPort, "p-port", 18000, "The proxy port") rootCmd.PersistentFlags().Uint64Var(&proxyPid, "p-pid", 0, "The process ID of the proxy. If not provided, the program will automatically start a forwarding proxy.") rootCmd.PersistentFlags().StringSliceVar(&pids, "pids", []string{}, "The pid to be proxied, seperate by ','") + rootCmd.PersistentFlags().StringVar(&containerName, "container-name", "", "The container name to be proxied") rootCmd.PersistentFlags().StringVar(&ipStr, "ip", "", "The ip to be proxied,only support ipv4") rootCmd.PersistentFlags().StringVar(&socks5ProxyAddr, "socks5", "", "The socks5 proxyAddr.") rootCmd.PersistentFlags().StringVar(&socks5User, "socks5-user", "", "The SOCKS5 username. Requires --socks5-pass.") diff --git a/cmd/loadBpf.go b/cmd/loadBpf.go index 29a505a..16477c9 100644 --- a/cmd/loadBpf.go +++ b/cmd/loadBpf.go @@ -1,6 +1,9 @@ package main import ( + "context" + "encoding/binary" + "gotproxy/common" "log" "os" @@ -25,14 +28,15 @@ type SockAddrIn struct { Pad [8]byte } type Options struct { - ProxyPort uint16 // Port where the proxy server listens - ProxyPid uint64 // PID of the proxy server - Command string - Pids []uint64 - Ip4 uint32 - Ip4Mask uint8 - EnableTCP bool - EnableUDP bool + ProxyPort uint16 // Port where the proxy server listens + ProxyPid uint64 // PID of the proxy server + Command string + Pids []uint64 + ContainerName string + Ip4 uint32 + Ip4Mask uint8 + EnableTCP bool + EnableUDP bool } func LoadBpf(options *Options) { @@ -59,9 +63,29 @@ func LoadBpf(options *Options) { } log.Printf("Proxy protocol enabled: %s (tcp=%v udp=%v)", mode, options.EnableTCP, options.EnableUDP) + proxyListenHost := "127.0.0.1" + proxyRedirectIP := uint32(0x7f000001) + if options.ContainerName != "" { + proxyListenHost = "0.0.0.0" + gatewayIP, err := common.ResolveDockerBridgeGatewayIPv4(context.Background()) + if err != nil { + // Fallback to Docker default bridge gateway for compatibility with typical setups. + log.Printf("Resolve Docker bridge gateway failed: %v, fallback to 172.17.0.1", err) + proxyRedirectIP = 0xac110001 + } else { + proxyRedirectIP = binary.BigEndian.Uint32(gatewayIP.To4()) + } + log.Printf("Container mode enabled: proxy listen=%s, redirect gateway=%d.%d.%d.%d", + proxyListenHost, + byte(proxyRedirectIP>>24), + byte(proxyRedirectIP>>16), + byte(proxyRedirectIP>>8), + byte(proxyRedirectIP)) + } + // Start TCP (and UDP) proxy so it can use objs.MapUdpDest for UDP original-dest lookup if options.ProxyPid == 0 { - StartProxy(objs.MapUdpDest, options.EnableTCP, options.EnableUDP) + StartProxy(objs.MapUdpDest, options.EnableTCP, options.EnableUDP, proxyListenHost) } // Attach eBPF programs to the root cgroup @@ -109,14 +133,16 @@ func LoadBpf(options *Options) { pid = options.ProxyPid } config := proxyConfig{ - ProxyPort: options.ProxyPort, - ProxyPid: pid, - FilterByPid: len(options.Pids) > 0, - FilterByPgid: len(options.Pids) > 0, - FilterIp: options.Ip4, - FilterIpMask: options.Ip4Mask, - EnableTcp: options.EnableTCP, - EnableUdp: options.EnableUDP, + ProxyPort: options.ProxyPort, + ProxyPid: pid, + ProxyIp: proxyRedirectIP, + FilterByPid: len(options.Pids) > 0, + FilterByPgid: len(options.Pids) > 0, + FilterByContainer: options.ContainerName != "", + FilterIp: options.Ip4, + FilterIpMask: options.Ip4Mask, + EnableTcp: options.EnableTCP, + EnableUdp: options.EnableUDP, } stringToInt8Array(config.Command[:], options.Command) err = objs.proxyMaps.MapConfig.Update(&key, &config, ebpf.UpdateAny) @@ -129,6 +155,36 @@ func LoadBpf(options *Options) { log.Fatalf("Failed to update FilterPidMap: %v", err) } } + if options.ContainerName != "" { + if err := common.EnsureKernelAtLeast(common.KernelVersion{Major: 4, Minor: 14, Patch: 0}, "container-name filter"); err != nil { + log.Fatal(err) + } + + containerNS, err := common.ResolveContainerNamespacesByName(context.Background(), options.ContainerName) + if err != nil { + log.Fatalf("Failed to resolve container namespaces: %v", err) + } + if containerNS.PidNS == 0 && containerNS.MntNS == 0 && containerNS.NetNS == 0 { + log.Fatalf("Container %q does not expose usable namespaces for filtering", options.ContainerName) + } + log.Printf("Container %q namespaces: pid=%d mnt=%d net=%d", options.ContainerName, containerNS.PidNS, containerNS.MntNS, containerNS.NetNS) + + if containerNS.PidNS != 0 { + if err := objs.FilterPidnsMap.Update(containerNS.PidNS, int8(1), ebpf.UpdateAny); err != nil { + log.Fatalf("Failed to update FilterPidnsMap: %v", err) + } + } + if containerNS.MntNS != 0 { + if err := objs.FilterMntnsMap.Update(containerNS.MntNS, int8(1), ebpf.UpdateAny); err != nil { + log.Fatalf("Failed to update FilterMntnsMap: %v", err) + } + } + if containerNS.NetNS != 0 { + if err := objs.FilterNetnsMap.Update(containerNS.NetNS, int8(1), ebpf.UpdateAny); err != nil { + log.Fatalf("Failed to update FilterNetnsMap: %v", err) + } + } + } select {} } diff --git a/cmd/proxy.c b/cmd/proxy.c index b104d7c..0380685 100644 --- a/cmd/proxy.c +++ b/cmd/proxy.c @@ -17,14 +17,19 @@ #define BPF_LOG_DEBUG(fmt, ...) #endif +/* Always-on logs for troubleshooting key hook paths. */ +#define BPF_LOG_INFO(fmt, ...) bpf_printk(fmt, ##__VA_ARGS__) + struct Config { __u16 proxy_port; __u64 proxy_pid; + __u32 proxy_ip; __u32 filter_ip; __u8 filter_ip_mask; bool filter_by_pid; bool filter_by_pgid; + bool filter_by_container; bool enable_tcp; bool enable_udp; char command[TASK_COMM_LEN]; @@ -44,6 +49,27 @@ struct { __type(value, u8); } filter_pid_map SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_PIDS); + __type(key, u32); + __type(value, u8); +} filter_pidns_map SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_PIDS); + __type(key, u32); + __type(value, u8); +} filter_mntns_map SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_PIDS); + __type(key, u32); + __type(value, u8); +} filter_netns_map SEC(".maps"); + struct { int (*type)[BPF_MAP_TYPE_ARRAY]; int (*max_entries)[1]; @@ -95,8 +121,10 @@ struct { } map_udp_cookie_to_port SEC(".maps"); #define SO_ORIGINAL_DST 80 +#define SOL_IP 0 #define AF_INET 2 +#define AF_INET6 10 static __always_inline __u32 get_current_pgid(void) @@ -113,29 +141,75 @@ get_current_pgid(void) return BPF_CORE_READ(pgid_pid, numbers[0].nr); } +static __always_inline bool +match_container_ns(void) +{ + struct task_struct *task = (struct task_struct *)bpf_get_current_task_btf(); + if (!task) { + return false; + } + + __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); + + if (pidns_id && bpf_map_lookup_elem(&filter_pidns_map, &pidns_id)) { + return true; + } + if (mntns_id && bpf_map_lookup_elem(&filter_mntns_map, &mntns_id)) { + return true; + } + if (netns_id && bpf_map_lookup_elem(&filter_netns_map, &netns_id)) { + return true; + } + + return false; +} + static __always_inline bool match_process(struct Config *conf) { - if (conf->command[0] == '\0' && !conf->filter_by_pid && !conf->filter_by_pgid){ + 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) { return true; } - if (conf->command[0] != '\0') { + bool cmd_matched = false; + bool pid_matched = false; + bool container_matched = false; + + if (has_cmd) { char comm[TASK_COMM_LEN]; bpf_get_current_comm(comm, sizeof(comm)); - if (__builtin_memcmp(comm, conf->command, TASK_COMM_LEN) == 0) return true; + cmd_matched = (__builtin_memcmp(comm, conf->command, TASK_COMM_LEN) == 0); } - if(conf->filter_by_pid){ + if (has_pid_filter && conf->filter_by_pid) { __u32 current_pid = bpf_get_current_pid_tgid() >> 32; - if (bpf_map_lookup_elem(&filter_pid_map, ¤t_pid)) return true; + if (bpf_map_lookup_elem(&filter_pid_map, ¤t_pid)) { + pid_matched = true; + } } - if(conf->filter_by_pgid){ + 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)) return true; + if (current_pgid && bpf_map_lookup_elem(&filter_pid_map, ¤t_pgid)) { + pid_matched = true; + } + } + + if (has_container_filter) { + container_matched = match_container_ns(); } + if ((has_container_filter && container_matched) || + (has_cmd && cmd_matched) || + (has_pid_filter && pid_matched)) { + return true; + } return false; } @@ -145,19 +219,26 @@ int cg_connect4(struct bpf_sock_addr *ctx) { if (ctx->user_family != AF_INET) return 1; if (ctx->protocol != IPPROTO_TCP && ctx->protocol != IPPROTO_UDP) return 1; + __u32 current_pid = bpf_get_current_pid_tgid() >> 32; __u32 key = 0; struct Config *conf = bpf_map_lookup_elem(&map_config, &key); - if (!conf) return 1; - if ((bpf_get_current_pid_tgid() >> 32) == conf->proxy_pid) return 1; + if (!conf) { + BPF_LOG_INFO("connect4: config miss pid=%u\n", current_pid); + return 1; + } + if (current_pid == conf->proxy_pid) return 1; - if (!match_process(conf)) return 1; + if (!match_process(conf)) { + BPF_LOG_INFO("connect4: process no match pid=%u\n", current_pid); + return 1; + } if (conf->filter_ip) { __u32 mask = 0xFFFFFFFF >> (32 - conf->filter_ip_mask); if ((ctx->user_ip4 & mask) != (conf->filter_ip & mask)) { - BPF_LOG_DEBUG("not match ip\n"); + BPF_LOG_INFO("connect4: ip no match pid=%u\n", current_pid); return 1; } } @@ -165,9 +246,11 @@ int cg_connect4(struct bpf_sock_addr *ctx) { __u32 dst_addr = bpf_ntohl(ctx->user_ip4); __u16 dst_port = bpf_ntohl(ctx->user_port) >> 16; - /* Do not proxy localhost (IPv4 loopback 127.0.0.0/8). */ - if ((dst_addr & 0xff000000) == 0x7f000000) + /* Do not re-proxy traffic that already targets the proxy endpoint. */ + if (dst_addr == conf->proxy_ip && dst_port == conf->proxy_port) { + BPF_LOG_INFO("connect4: skip self target=%x:%u\n", dst_addr, dst_port); return 1; + } if (ctx->protocol == IPPROTO_TCP) { if (!conf->enable_tcp) return 1; @@ -178,10 +261,11 @@ int cg_connect4(struct bpf_sock_addr *ctx) { sock.dst_port = dst_port; bpf_map_update_elem(&map_socks, &cookie, &sock, 0); - ctx->user_ip4 = bpf_htonl(0x7f000001); + ctx->user_ip4 = bpf_htonl(conf->proxy_ip); ctx->user_port = bpf_htonl(conf->proxy_port << 16); - BPF_LOG_DEBUG("TCP: Redirecting to proxy\n"); + BPF_LOG_INFO("connect4: tcp redirect dst=%x:%u -> proxy=%x:%u pid=%u\n", + dst_addr, dst_port, conf->proxy_ip, conf->proxy_port, current_pid); return 1; } @@ -194,7 +278,10 @@ int cg_connect4(struct bpf_sock_addr *ctx) { */ if (!conf->enable_udp) return 1; struct bpf_sock *sk = ctx->sk; - if (!sk) return 1; + if (!sk) { + BPF_LOG_INFO("connect4: udp no sk pid=%u\n", current_pid); + return 1; + } __u16 src_port = sk->src_port; if (src_port == 0) { @@ -220,7 +307,11 @@ int cg_connect4(struct bpf_sock_addr *ctx) { struct UdpDestKey dkey; __builtin_memset(&dkey, 0, sizeof(dkey)); - dkey.src_ip = 0x7f000001; + /* + * Source IP seen by user-space proxy differs between host/container + * network paths, so keep key portable by matching on source port. + */ + dkey.src_ip = 0; dkey.src_port = src_port; struct UdpDestVal dval; @@ -229,11 +320,11 @@ int cg_connect4(struct bpf_sock_addr *ctx) { dval.dst_port = dst_port; bpf_map_update_elem(&map_udp_dest, &dkey, &dval, 0); - ctx->user_ip4 = bpf_htonl(0x7f000001); + ctx->user_ip4 = bpf_htonl(conf->proxy_ip); ctx->user_port = bpf_htonl(conf->proxy_port << 16); - BPF_LOG_DEBUG("UDP: redirect %x:%d -> proxy, src_port=%d\n", - dst_addr, dst_port, src_port); + BPF_LOG_INFO("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); return 1; } @@ -252,9 +343,12 @@ int cg_sock_ops(struct bpf_sock_ops *ctx) { if (sock) { __u16 src_port = ctx->local_port; bpf_map_update_elem(&map_ports, &src_port, &cookie, 0); + BPF_LOG_INFO("sockops: map_ports set src_port=%u dst=%x:%u\n", + src_port, sock->dst_addr, sock->dst_port); + } else { + BPF_LOG_INFO("sockops: map_socks miss local_port=%u\n", ctx->local_port); } } - BPF_LOG_DEBUG("sockops hook successful\n"); return 0; } @@ -265,29 +359,67 @@ int cg_sock_ops(struct bpf_sock_ops *ctx) { SEC("cgroup/getsockopt") int cg_sock_opt(struct bpf_sockopt *ctx) { if (ctx->optname != SO_ORIGINAL_DST) return 1; - // Only forward IPv4 TCP connections - if (ctx->sk->family != AF_INET) return 1; - if (ctx->sk->protocol != IPPROTO_TCP) return 1; + BPF_LOG_INFO("getsockopt: start level=%d optname=%d optlen=%d\n", + ctx->level, ctx->optname, ctx->optlen); + + /* + * SO_ORIGINAL_DST is scoped by level (SOL_IP). Without this check we may + * hit unrelated options that reuse numeric value 80 under other levels. + */ + if (ctx->level != SOL_IP) { + BPF_LOG_INFO("getsockopt: skip non-sol_ip level=%d\n", ctx->level); + return 1; + } + + if (!ctx->sk) { + BPF_LOG_INFO("getsockopt: sk is null\n"); + return 1; + } + + /* + * Go may accept redirected IPv4 traffic on a dual-stack listener as + * AF_INET6 sockets (v4-mapped). Keep processing both AF_INET/AF_INET6 + * and restore IPv4 original destination to userspace. + */ + if (ctx->sk->family != AF_INET && ctx->sk->family != AF_INET6) { + BPF_LOG_INFO("getsockopt: skip unsupported family=%u\n", ctx->sk->family); + return 1; + } + if (ctx->sk->protocol != IPPROTO_TCP) { + BPF_LOG_INFO("getsockopt: skip protocol=%u\n", ctx->sk->protocol); + return 1; + } __u16 src_port = bpf_ntohs(ctx->sk->dst_port); // Retrieve the socket cookie using the clients' src_port __u64 *cookie = bpf_map_lookup_elem(&map_ports, &src_port); - if (!cookie) return 1; + if (!cookie) { + BPF_LOG_INFO("getsockopt: map_ports miss src_port=%u\n", src_port); + return 1; + } // Using the cookie (socket identifier), retrieve the original socket (client connect to destination) from map_socks struct Socket *sock = bpf_map_lookup_elem(&map_socks, cookie); - if (!sock) return 1; + if (!sock) { + BPF_LOG_INFO("getsockopt: map_socks miss src_port=%u\n", src_port); + return 1; + } struct sockaddr_in *sa = ctx->optval; - if ((void*)(sa + 1) > ctx->optval_end) return 1; + if ((void*)(sa + 1) > ctx->optval_end) { + BPF_LOG_INFO("getsockopt: optval too short optlen=%d\n", ctx->optlen); + return 1; + } // Establish a connection with the original destination target ctx->optlen = sizeof(*sa); - sa->sin_family = ctx->sk->family; + sa->sin_family = AF_INET; sa->sin_addr.s_addr = bpf_htonl(sock->dst_addr); sa->sin_port = bpf_htons(sock->dst_port); ctx->retval = 0; + BPF_LOG_INFO("getsockopt: restore src_port=%u dst=%x:%u\n", + src_port, sock->dst_addr, sock->dst_port); return 1; } @@ -303,7 +435,7 @@ int tcp_set_state(struct pt_regs *ctx) __u64 *cookie = bpf_map_lookup_elem(&map_ports, &src_port); if (cookie) { - BPF_LOG_DEBUG("tcp close\n"); + BPF_LOG_INFO("tcp_close: cleanup src_port=%u\n", src_port); bpf_map_delete_elem(&map_ports, &src_port); bpf_map_delete_elem(&map_socks, &cookie); } diff --git a/cmd/proxy_arm64_bpfel.go b/cmd/proxy_arm64_bpfel.go index 2590c0c..26edfbe 100644 --- a/cmd/proxy_arm64_bpfel.go +++ b/cmd/proxy_arm64_bpfel.go @@ -14,18 +14,20 @@ import ( ) type proxyConfig struct { - _ structs.HostLayout - ProxyPort uint16 - _ [6]byte - ProxyPid uint64 - FilterIp uint32 - FilterIpMask uint8 - FilterByPid bool - FilterByPgid bool - EnableTcp bool - EnableUdp bool - Command [16]int8 - _ [7]byte + _ structs.HostLayout + ProxyPort uint16 + _ [6]byte + ProxyPid uint64 + ProxyIp uint32 + FilterIp uint32 + FilterIpMask uint8 + FilterByPid bool + FilterByPgid bool + FilterByContainer bool + EnableTcp bool + EnableUdp bool + Command [16]int8 + _ [2]byte } type proxySocket struct { @@ -104,7 +106,10 @@ type proxyProgramSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type proxyMapSpecs struct { + FilterMntnsMap *ebpf.MapSpec `ebpf:"filter_mntns_map"` + FilterNetnsMap *ebpf.MapSpec `ebpf:"filter_netns_map"` FilterPidMap *ebpf.MapSpec `ebpf:"filter_pid_map"` + FilterPidnsMap *ebpf.MapSpec `ebpf:"filter_pidns_map"` MapConfig *ebpf.MapSpec `ebpf:"map_config"` MapPorts *ebpf.MapSpec `ebpf:"map_ports"` MapSocks *ebpf.MapSpec `ebpf:"map_socks"` @@ -138,7 +143,10 @@ func (o *proxyObjects) Close() error { // // It can be passed to loadProxyObjects or ebpf.CollectionSpec.LoadAndAssign. type proxyMaps struct { + FilterMntnsMap *ebpf.Map `ebpf:"filter_mntns_map"` + FilterNetnsMap *ebpf.Map `ebpf:"filter_netns_map"` FilterPidMap *ebpf.Map `ebpf:"filter_pid_map"` + FilterPidnsMap *ebpf.Map `ebpf:"filter_pidns_map"` MapConfig *ebpf.Map `ebpf:"map_config"` MapPorts *ebpf.Map `ebpf:"map_ports"` MapSocks *ebpf.Map `ebpf:"map_socks"` @@ -148,7 +156,10 @@ type proxyMaps struct { func (m *proxyMaps) Close() error { return _ProxyClose( + m.FilterMntnsMap, + m.FilterNetnsMap, m.FilterPidMap, + m.FilterPidnsMap, m.MapConfig, m.MapPorts, m.MapSocks, diff --git a/cmd/proxy_x86_bpfel.go b/cmd/proxy_x86_bpfel.go index 9fdfe9d..b8db2b5 100644 --- a/cmd/proxy_x86_bpfel.go +++ b/cmd/proxy_x86_bpfel.go @@ -14,18 +14,20 @@ import ( ) type proxyConfig struct { - _ structs.HostLayout - ProxyPort uint16 - _ [6]byte - ProxyPid uint64 - FilterIp uint32 - FilterIpMask uint8 - FilterByPid bool - FilterByPgid bool - EnableTcp bool - EnableUdp bool - Command [16]int8 - _ [7]byte + _ structs.HostLayout + ProxyPort uint16 + _ [6]byte + ProxyPid uint64 + ProxyIp uint32 + FilterIp uint32 + FilterIpMask uint8 + FilterByPid bool + FilterByPgid bool + FilterByContainer bool + EnableTcp bool + EnableUdp bool + Command [16]int8 + _ [2]byte } type proxySocket struct { @@ -104,7 +106,10 @@ type proxyProgramSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type proxyMapSpecs struct { + FilterMntnsMap *ebpf.MapSpec `ebpf:"filter_mntns_map"` + FilterNetnsMap *ebpf.MapSpec `ebpf:"filter_netns_map"` FilterPidMap *ebpf.MapSpec `ebpf:"filter_pid_map"` + FilterPidnsMap *ebpf.MapSpec `ebpf:"filter_pidns_map"` MapConfig *ebpf.MapSpec `ebpf:"map_config"` MapPorts *ebpf.MapSpec `ebpf:"map_ports"` MapSocks *ebpf.MapSpec `ebpf:"map_socks"` @@ -138,7 +143,10 @@ func (o *proxyObjects) Close() error { // // It can be passed to loadProxyObjects or ebpf.CollectionSpec.LoadAndAssign. type proxyMaps struct { + FilterMntnsMap *ebpf.Map `ebpf:"filter_mntns_map"` + FilterNetnsMap *ebpf.Map `ebpf:"filter_netns_map"` FilterPidMap *ebpf.Map `ebpf:"filter_pid_map"` + FilterPidnsMap *ebpf.Map `ebpf:"filter_pidns_map"` MapConfig *ebpf.Map `ebpf:"map_config"` MapPorts *ebpf.Map `ebpf:"map_ports"` MapSocks *ebpf.Map `ebpf:"map_socks"` @@ -148,7 +156,10 @@ type proxyMaps struct { func (m *proxyMaps) Close() error { return _ProxyClose( + m.FilterMntnsMap, + m.FilterNetnsMap, m.FilterPidMap, + m.FilterPidnsMap, m.MapConfig, m.MapPorts, m.MapSocks, diff --git a/cmd/tcpProxy.go b/cmd/tcpProxy.go index 4c72db0..eef5a88 100644 --- a/cmd/tcpProxy.go +++ b/cmd/tcpProxy.go @@ -16,8 +16,8 @@ import ( ) // StartProxy starts TCP/UDP proxy on proxyPort based on enableTCP/enableUDP. -func StartProxy(udpMap *ebpf.Map, enableTCP bool, enableUDP bool) { - proxyAddr := fmt.Sprintf("127.0.0.1:%d", proxyPort) +func StartProxy(udpMap *ebpf.Map, enableTCP bool, enableUDP bool, listenHost string) { + proxyAddr := fmt.Sprintf("%s:%d", listenHost, proxyPort) if !enableTCP && !enableUDP { log.Printf("Proxy: enableTCP and enableUDP are both false, nothing to start") @@ -106,7 +106,14 @@ func getTargetConnection(conn net.Conn) (net.Conn, error) { 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]) - fmt.Printf("TCP Original destination: %s:%d\n", targetAddr, targetPort) + sourceAddr := conn.RemoteAddr().String() + sourceIP, sourcePort, splitErr := net.SplitHostPort(sourceAddr) + if splitErr != nil { + sourceIP = sourceAddr + sourcePort = "unknown" + } + + fmt.Printf("TCP Source: %s:%s -> Original destination: %s:%d\n", sourceIP, sourcePort, targetAddr, targetPort) if socks5ProxyAddr == "" { targetConn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", targetAddr, targetPort), 5*time.Second) diff --git a/cmd/udpProxy.go b/cmd/udpProxy.go index e7a778f..d5905d3 100644 --- a/cmd/udpProxy.go +++ b/cmd/udpProxy.go @@ -107,7 +107,11 @@ func getUDPOriginalDest(clientAddr *net.UDPAddr, udpMap *ebpf.Map) (string, erro } var val proxyUdpDestVal if err := udpMap.Lookup(&key, &val); err != nil { - return "", err + // Fallback entry keyed only by source port for container/bridge paths. + key.SrcIp = 0 + if err := udpMap.Lookup(&key, &val); 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)) diff --git a/common/container_linux.go b/common/container_linux.go new file mode 100644 index 0000000..bb0c470 --- /dev/null +++ b/common/container_linux.go @@ -0,0 +1,224 @@ +//go:build linux + +package common + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "strconv" + "strings" + "time" +) + +const defaultDockerSocket = "/var/run/docker.sock" + +type ContainerNamespaces struct { + PidNS uint32 + MntNS uint32 + NetNS uint32 +} + +type dockerContainerSummary struct { + ID string `json:"Id"` + Names []string `json:"Names"` +} + +type dockerContainerInspect struct { + State struct { + Pid int `json:"Pid"` + } `json:"State"` +} + +type dockerNetworkInspect struct { + IPAM struct { + Config []struct { + Gateway string `json:"Gateway"` + } `json:"Config"` + } `json:"IPAM"` +} + +func ResolveContainerNamespacesByName(ctx context.Context, name string) (ContainerNamespaces, error) { + name = strings.TrimSpace(name) + if name == "" { + return ContainerNamespaces{}, fmt.Errorf("container name cannot be empty") + } + + client, err := newDockerHTTPClient(defaultDockerSocket) + if err != nil { + return ContainerNamespaces{}, err + } + + containers, err := dockerListContainers(ctx, client) + if err != nil { + return ContainerNamespaces{}, err + } + + var matched []dockerContainerSummary + for _, c := range containers { + for _, n := range c.Names { + if strings.TrimPrefix(n, "/") == name { + matched = append(matched, c) + break + } + } + } + + if len(matched) == 0 { + return ContainerNamespaces{}, fmt.Errorf("no running container found by name %q", name) + } + if len(matched) > 1 { + return ContainerNamespaces{}, fmt.Errorf("found more than one running container by name %q", name) + } + + pid, err := dockerInspectContainerPID(ctx, client, matched[0].ID) + if err != nil { + return ContainerNamespaces{}, err + } + if pid <= 0 { + return ContainerNamespaces{}, fmt.Errorf("container %q has invalid pid %d", name, pid) + } + + ns, err := readNamespacesFromPID(pid) + if err != nil { + return ContainerNamespaces{}, err + } + return ns, nil +} + +func newDockerHTTPClient(socketPath string) (*http.Client, error) { + if _, err := os.Stat(socketPath); err != nil { + return nil, fmt.Errorf("docker socket %s not available: %w", socketPath, err) + } + + transport := &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", socketPath) + }, + } + return &http.Client{ + Transport: transport, + Timeout: 5 * time.Second, + }, nil +} + +func dockerListContainers(ctx context.Context, client *http.Client) ([]dockerContainerSummary, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://unix/containers/json", nil) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("docker list containers failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode/100 != 2 { + return nil, fmt.Errorf("docker list containers returned status %s", resp.Status) + } + + var containers []dockerContainerSummary + if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil { + return nil, fmt.Errorf("decode docker containers response: %w", err) + } + return containers, nil +} + +func dockerInspectContainerPID(ctx context.Context, client *http.Client, containerID string) (int, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://unix/containers/"+containerID+"/json", nil) + if err != nil { + return 0, err + } + resp, err := client.Do(req) + if err != nil { + return 0, fmt.Errorf("docker inspect failed: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode/100 != 2 { + return 0, fmt.Errorf("docker inspect returned status %s", resp.Status) + } + + var inspect dockerContainerInspect + if err := json.NewDecoder(resp.Body).Decode(&inspect); err != nil { + return 0, fmt.Errorf("decode docker inspect response: %w", err) + } + return inspect.State.Pid, nil +} + +func ResolveDockerBridgeGatewayIPv4(ctx context.Context) (net.IP, error) { + client, err := newDockerHTTPClient(defaultDockerSocket) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://unix/networks/bridge", nil) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("docker inspect bridge network failed: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode/100 != 2 { + return nil, fmt.Errorf("docker inspect bridge network returned status %s", resp.Status) + } + + var inspect dockerNetworkInspect + if err := json.NewDecoder(resp.Body).Decode(&inspect); err != nil { + return nil, fmt.Errorf("decode docker bridge network response: %w", err) + } + if len(inspect.IPAM.Config) == 0 { + return nil, fmt.Errorf("docker bridge network has no IPAM config") + } + gateway := strings.TrimSpace(inspect.IPAM.Config[0].Gateway) + if gateway == "" { + return nil, fmt.Errorf("docker bridge network gateway is empty") + } + ip := net.ParseIP(gateway).To4() + if ip == nil { + return nil, fmt.Errorf("docker bridge gateway %q is not an IPv4 address", gateway) + } + return ip, nil +} + +func readNamespacesFromPID(pid int) (ContainerNamespaces, error) { + base := fmt.Sprintf("/proc/%d/ns", pid) + pidNS, err := readNamespaceInode(base + "/pid") + if err != nil { + return ContainerNamespaces{}, err + } + mntNS, err := readNamespaceInode(base + "/mnt") + if err != nil { + return ContainerNamespaces{}, err + } + netNS, err := readNamespaceInode(base + "/net") + if err != nil { + return ContainerNamespaces{}, err + } + return ContainerNamespaces{ + PidNS: pidNS, + MntNS: mntNS, + NetNS: netNS, + }, nil +} + +func readNamespaceInode(path string) (uint32, error) { + link, err := os.Readlink(path) + if err != nil { + return 0, fmt.Errorf("readlink %s: %w", path, err) + } + left := strings.IndexByte(link, '[') + right := strings.IndexByte(link, ']') + if left < 0 || right <= left+1 { + return 0, fmt.Errorf("unexpected namespace link format %q", link) + } + id, err := strconv.ParseUint(link[left+1:right], 10, 32) + if err != nil { + return 0, fmt.Errorf("parse namespace inode %q: %w", link, err) + } + return uint32(id), nil +} diff --git a/common/container_other.go b/common/container_other.go new file mode 100644 index 0000000..b1f1959 --- /dev/null +++ b/common/container_other.go @@ -0,0 +1,23 @@ +//go:build !linux + +package common + +import ( + "context" + "fmt" + "net" +) + +type ContainerNamespaces struct { + PidNS uint32 + MntNS uint32 + NetNS uint32 +} + +func ResolveContainerNamespacesByName(_ context.Context, _ string) (ContainerNamespaces, error) { + return ContainerNamespaces{}, fmt.Errorf("container filtering requires Linux") +} + +func ResolveDockerBridgeGatewayIPv4(_ context.Context) (net.IP, error) { + return nil, fmt.Errorf("docker bridge gateway discovery requires Linux") +}