From d8c856e2fb6f167cbbf4d298fa00ef25e38186be Mon Sep 17 00:00:00 2001 From: Dream95 Date: Tue, 10 Mar 2026 14:07:46 +0000 Subject: [PATCH 1/2] feat: add udp proxy --- cmd/cmd.go | 3 - cmd/loadBpf.go | 8 ++- cmd/proxy.c | 104 ++++++++++++++++++++++++++++++----- cmd/proxy_arm64_bpfel.go | 36 +++++++++--- cmd/proxy_x86_bpfel.go | 36 +++++++++--- cmd/tcpProxy.go | 11 ++-- cmd/udpProxy.go | 116 +++++++++++++++++++++++++++++++++++++++ scripts/test_proxy.sh | 33 ++++++++++- 8 files changed, 306 insertions(+), 41 deletions(-) create mode 100644 cmd/udpProxy.go diff --git a/cmd/cmd.go b/cmd/cmd.go index 9b1b462..fb9f4af 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -44,9 +44,6 @@ var rootCmd = &cobra.Command{ Options.Ip4 = ip Options.Ip4Mask = mask - if proxyPid == 0 { - StartProxy() - } for _, pid := range pids { pidInt, err := strconv.ParseUint(pid, 10, 64) if err != nil { diff --git a/cmd/loadBpf.go b/cmd/loadBpf.go index cd3f530..0449216 100644 --- a/cmd/loadBpf.go +++ b/cmd/loadBpf.go @@ -40,13 +40,17 @@ func LoadBpf(options *Options) { } // Load the compiled eBPF ELF and load it into the kernel - // NOTE: we could also pin the eBPF program var objs proxyObjects if err := loadProxyObjects(&objs, nil); err != nil { - log.Print("Error loading eBPF objects:", err) + log.Fatalf("Error loading eBPF objects: %v", err) } defer objs.Close() + // Start TCP (and UDP) proxy so it can use objs.MapUdpDest for UDP original-dest lookup + if options.ProxyPid == 0 { + StartProxy(objs.MapUdpDest) + } + // Attach eBPF programs to the root cgroup connect4Link, err := link.AttachCgroup(link.CgroupOptions{ Path: CGROUP_PATH, diff --git a/cmd/proxy.c b/cmd/proxy.c index bb43614..a550098 100644 --- a/cmd/proxy.c +++ b/cmd/proxy.c @@ -62,6 +62,35 @@ struct { __u64 *value; } map_ports SEC(".maps"); +// Key for UDP original destination lookup: (client_ip, client_port) as seen by proxy +struct UdpDestKey { + __u32 src_ip; + __u16 src_port; + __u16 pad; +}; + +// Value: original destination that was redirected to proxy +struct UdpDestVal { + __u32 dst_ip; + __u16 dst_port; + __u16 pad; +}; + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_CONNECTIONS); + __type(key, struct UdpDestKey); + __type(value, struct UdpDestVal); +} map_udp_dest SEC(".maps"); + + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_CONNECTIONS); + __type(key, __u64); + __type(value, __u16); +} map_udp_cookie_to_port SEC(".maps"); + #define SO_ORIGINAL_DST 80 #define AF_INET 2 @@ -89,11 +118,9 @@ match_process(struct Config *conf) SEC("cgroup/connect4") int cg_connect4(struct bpf_sock_addr *ctx) { - // Only forward IPv4 TCP connections if (ctx->user_family != AF_INET) return 1; - if (ctx->protocol != IPPROTO_TCP) return 1; + if (ctx->protocol != IPPROTO_TCP && ctx->protocol != IPPROTO_UDP) return 1; - // This prevents the proxy from proxying itself __u32 key = 0; struct Config *conf = bpf_map_lookup_elem(&map_config, &key); if (!conf) return 1; @@ -113,21 +140,70 @@ 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; - __u64 cookie = bpf_get_socket_cookie(ctx); - // Store destination socket under cookie key - struct Socket sock; - __builtin_memset(&sock, 0, sizeof(sock)); - sock.dst_addr = dst_addr; - sock.dst_port = dst_port; - bpf_map_update_elem(&map_socks, &cookie, &sock, 0); + if (ctx->protocol == IPPROTO_TCP) { + __u64 cookie = bpf_get_socket_cookie(ctx); + struct Socket sock; + __builtin_memset(&sock, 0, sizeof(sock)); + sock.dst_addr = dst_addr; + sock.dst_port = dst_port; + bpf_map_update_elem(&map_socks, &cookie, &sock, 0); + + ctx->user_ip4 = bpf_htonl(0x7f000001); + ctx->user_port = bpf_htonl(conf->proxy_port << 16); + + BPF_LOG_DEBUG("TCP: Redirecting to proxy\n"); + return 1; + } + + /* + * UDP: read ctx->sk BEFORE any helper that passes ctx as argument. + * Helpers like bpf_get_socket_cookie(ctx) or bpf_bind(ctx,...) mark + * ctx as "modified", after which the verifier forbids pointer + * dereferences through it (e.g. ctx->sk). Scalar reads/writes + * (ctx->user_ip4, ctx->user_port) remain fine. + */ + struct bpf_sock *sk = ctx->sk; + if (!sk) return 1; + __u16 src_port = sk->src_port; + + if (src_port == 0) { + /* + * Socket not yet bound — force-bind to a random port. + * We pick the value ourselves so we know it without having to + * read back from ctx->sk (which is forbidden after bpf_bind). + */ + struct sockaddr_in bind_sa; + __builtin_memset(&bind_sa, 0, sizeof(bind_sa)); + bind_sa.sin_family = AF_INET; + bind_sa.sin_addr.s_addr = 0; + + __u32 rand = bpf_get_prandom_u32(); + __u16 port = 10000 + (__u16)(rand % 55536); + bind_sa.sin_port = bpf_htons(port); + if (bpf_bind(ctx, (struct sockaddr *)&bind_sa, sizeof(bind_sa)) == 0) + src_port = port; + } + + if (src_port == 0) + return 1; + + struct UdpDestKey dkey; + __builtin_memset(&dkey, 0, sizeof(dkey)); + dkey.src_ip = 0x7f000001; + dkey.src_port = src_port; - // Redirect the connection to the proxy - ctx->user_ip4 = bpf_htonl(0x7f000001); // 127.0.0.1 == proxy IP - ctx->user_port = bpf_htonl(conf->proxy_port << 16); // Proxy port + struct UdpDestVal dval; + __builtin_memset(&dval, 0, sizeof(dval)); + dval.dst_ip = dst_addr; + dval.dst_port = dst_port; + bpf_map_update_elem(&map_udp_dest, &dkey, &dval, 0); - BPF_LOG_DEBUG("Redirecting client connection to proxy\n"); + ctx->user_ip4 = bpf_htonl(0x7f000001); + 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); return 1; } diff --git a/cmd/proxy_arm64_bpfel.go b/cmd/proxy_arm64_bpfel.go index c395e54..c18dd7b 100644 --- a/cmd/proxy_arm64_bpfel.go +++ b/cmd/proxy_arm64_bpfel.go @@ -35,6 +35,20 @@ type proxySocket struct { _ [2]byte } +type proxyUdpDestKey struct { + _ structs.HostLayout + SrcIp uint32 + SrcPort uint16 + Pad uint16 +} + +type proxyUdpDestVal struct { + _ structs.HostLayout + DstIp uint32 + DstPort uint16 + Pad uint16 +} + // loadProxy returns the embedded CollectionSpec for proxy. func loadProxy() (*ebpf.CollectionSpec, error) { reader := bytes.NewReader(_ProxyBytes) @@ -87,10 +101,12 @@ type proxyProgramSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type proxyMapSpecs struct { - FilterPidMap *ebpf.MapSpec `ebpf:"filter_pid_map"` - MapConfig *ebpf.MapSpec `ebpf:"map_config"` - MapPorts *ebpf.MapSpec `ebpf:"map_ports"` - MapSocks *ebpf.MapSpec `ebpf:"map_socks"` + FilterPidMap *ebpf.MapSpec `ebpf:"filter_pid_map"` + MapConfig *ebpf.MapSpec `ebpf:"map_config"` + MapPorts *ebpf.MapSpec `ebpf:"map_ports"` + MapSocks *ebpf.MapSpec `ebpf:"map_socks"` + MapUdpCookieToPort *ebpf.MapSpec `ebpf:"map_udp_cookie_to_port"` + MapUdpDest *ebpf.MapSpec `ebpf:"map_udp_dest"` } // proxyVariableSpecs contains global variables before they are loaded into the kernel. @@ -119,10 +135,12 @@ func (o *proxyObjects) Close() error { // // It can be passed to loadProxyObjects or ebpf.CollectionSpec.LoadAndAssign. type proxyMaps struct { - FilterPidMap *ebpf.Map `ebpf:"filter_pid_map"` - MapConfig *ebpf.Map `ebpf:"map_config"` - MapPorts *ebpf.Map `ebpf:"map_ports"` - MapSocks *ebpf.Map `ebpf:"map_socks"` + FilterPidMap *ebpf.Map `ebpf:"filter_pid_map"` + MapConfig *ebpf.Map `ebpf:"map_config"` + MapPorts *ebpf.Map `ebpf:"map_ports"` + MapSocks *ebpf.Map `ebpf:"map_socks"` + MapUdpCookieToPort *ebpf.Map `ebpf:"map_udp_cookie_to_port"` + MapUdpDest *ebpf.Map `ebpf:"map_udp_dest"` } func (m *proxyMaps) Close() error { @@ -131,6 +149,8 @@ func (m *proxyMaps) Close() error { m.MapConfig, m.MapPorts, m.MapSocks, + m.MapUdpCookieToPort, + m.MapUdpDest, ) } diff --git a/cmd/proxy_x86_bpfel.go b/cmd/proxy_x86_bpfel.go index a714d14..2bd0f1e 100644 --- a/cmd/proxy_x86_bpfel.go +++ b/cmd/proxy_x86_bpfel.go @@ -35,6 +35,20 @@ type proxySocket struct { _ [2]byte } +type proxyUdpDestKey struct { + _ structs.HostLayout + SrcIp uint32 + SrcPort uint16 + Pad uint16 +} + +type proxyUdpDestVal struct { + _ structs.HostLayout + DstIp uint32 + DstPort uint16 + Pad uint16 +} + // loadProxy returns the embedded CollectionSpec for proxy. func loadProxy() (*ebpf.CollectionSpec, error) { reader := bytes.NewReader(_ProxyBytes) @@ -87,10 +101,12 @@ type proxyProgramSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type proxyMapSpecs struct { - FilterPidMap *ebpf.MapSpec `ebpf:"filter_pid_map"` - MapConfig *ebpf.MapSpec `ebpf:"map_config"` - MapPorts *ebpf.MapSpec `ebpf:"map_ports"` - MapSocks *ebpf.MapSpec `ebpf:"map_socks"` + FilterPidMap *ebpf.MapSpec `ebpf:"filter_pid_map"` + MapConfig *ebpf.MapSpec `ebpf:"map_config"` + MapPorts *ebpf.MapSpec `ebpf:"map_ports"` + MapSocks *ebpf.MapSpec `ebpf:"map_socks"` + MapUdpCookieToPort *ebpf.MapSpec `ebpf:"map_udp_cookie_to_port"` + MapUdpDest *ebpf.MapSpec `ebpf:"map_udp_dest"` } // proxyVariableSpecs contains global variables before they are loaded into the kernel. @@ -119,10 +135,12 @@ func (o *proxyObjects) Close() error { // // It can be passed to loadProxyObjects or ebpf.CollectionSpec.LoadAndAssign. type proxyMaps struct { - FilterPidMap *ebpf.Map `ebpf:"filter_pid_map"` - MapConfig *ebpf.Map `ebpf:"map_config"` - MapPorts *ebpf.Map `ebpf:"map_ports"` - MapSocks *ebpf.Map `ebpf:"map_socks"` + FilterPidMap *ebpf.Map `ebpf:"filter_pid_map"` + MapConfig *ebpf.Map `ebpf:"map_config"` + MapPorts *ebpf.Map `ebpf:"map_ports"` + MapSocks *ebpf.Map `ebpf:"map_socks"` + MapUdpCookieToPort *ebpf.Map `ebpf:"map_udp_cookie_to_port"` + MapUdpDest *ebpf.Map `ebpf:"map_udp_dest"` } func (m *proxyMaps) Close() error { @@ -131,6 +149,8 @@ func (m *proxyMaps) Close() error { m.MapConfig, m.MapPorts, m.MapSocks, + m.MapUdpCookieToPort, + m.MapUdpDest, ) } diff --git a/cmd/tcpProxy.go b/cmd/tcpProxy.go index 1d60f9e..d2c7230 100644 --- a/cmd/tcpProxy.go +++ b/cmd/tcpProxy.go @@ -5,19 +5,19 @@ import ( "io" "log" "net" + "os" "syscall" "time" "unsafe" + "github.com/cilium/ebpf" "golang.org/x/net/proxy" ) -func StartProxy() { - // log.Printf("Proxy server with PID %d listening on %s", options.ProxyPid, proxyAddr) +// StartProxy starts TCP (and optionally UDP when udpMap is not nil) proxy on proxyPort. +func StartProxy(udpMap *ebpf.Map) { proxyAddr := fmt.Sprintf("127.0.0.1:%d", proxyPort) - // Start the proxy server on the localhost - // We only demonstrate IPv4 in this example, but the same approach can be used for IPv6 listener, err := net.Listen("tcp", proxyAddr) if err != nil { log.Fatalf("Failed to start proxy server: %v", err) @@ -25,6 +25,9 @@ func StartProxy() { log.Printf("Proxy server with PID %d listening on %s", os.Getpid(), proxyAddr) go acceptLoop(listener) + if udpMap != nil { + go StartUDPProxy(proxyAddr, udpMap) + } } func acceptLoop(listener net.Listener) { diff --git a/cmd/udpProxy.go b/cmd/udpProxy.go new file mode 100644 index 0000000..9e003a6 --- /dev/null +++ b/cmd/udpProxy.go @@ -0,0 +1,116 @@ +package main + +import ( + "encoding/binary" + "fmt" + "log" + "net" + "time" + + "github.com/cilium/ebpf" + "golang.org/x/net/proxy" +) + +const udpReadTimeout = 5 * time.Second + +// StartUDPProxy listens on addr (UDP) and forwards packets to original destinations +// looked up from the BPF map (key = client addr, value = original dst ip:port). +func StartUDPProxy(addr string, udpMap *ebpf.Map) { + if udpMap == nil { + return + } + udpAddr, err := net.ResolveUDPAddr("udp4", addr) + if err != nil { + log.Printf("UDP proxy: resolve %s: %v", addr, err) + return + } + conn, err := net.ListenUDP("udp4", udpAddr) + if err != nil { + log.Printf("UDP proxy: listen %s: %v", addr, err) + return + } + defer conn.Close() + log.Printf("UDP proxy listening on %s", addr) + + buf := make([]byte, 64*1024) + for { + n, clientAddr, err := conn.ReadFromUDP(buf) + if err != nil { + log.Printf("UDP proxy: read: %v", err) + continue + } + if n == 0 { + continue + } + payload := make([]byte, n) + copy(payload, buf[:n]) + go handleUDPPacket(conn, clientAddr, payload, udpMap) + } +} + +func handleUDPPacket(proxyConn *net.UDPConn, clientAddr *net.UDPAddr, payload []byte, udpMap *ebpf.Map) { + targetAddr, err := getUDPOriginalDest(clientAddr, udpMap) + if err != nil { + log.Printf("UDP proxy: lookup original dest for %s: %v", clientAddr, err) + return + } + fmt.Printf("UDP Original destination: %s\n", targetAddr) + + var remoteConn net.Conn + if socks5ProxyAddr == "" { + remoteConn, err = net.DialTimeout("udp", targetAddr, 5*time.Second) + } else { + remoteConn, err = dialUDPViaSOCKS5(targetAddr) + } + if err != nil { + log.Printf("UDP proxy: dial %s: %v", targetAddr, err) + return + } + defer remoteConn.Close() + + _, err = remoteConn.Write(payload) + if err != nil { + log.Printf("UDP proxy: write to %s: %v", targetAddr, err) + return + } + + remoteConn.SetReadDeadline(time.Now().Add(udpReadTimeout)) + respBuf := make([]byte, 64*1024) + m, err := remoteConn.Read(respBuf) + if err != nil { + log.Printf("UDP proxy: read from %s: %v", targetAddr, err) + return + } + _, err = proxyConn.WriteToUDP(respBuf[:m], clientAddr) + if err != nil { + log.Printf("UDP proxy: write back to client %s: %v", clientAddr, err) + } +} + +// 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). +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 { + 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 +} + +func dialUDPViaSOCKS5(targetAddr string) (net.Conn, error) { + dialer, err := proxy.SOCKS5("udp", socks5ProxyAddr, nil, proxy.Direct) + if err != nil { + return nil, fmt.Errorf("SOCKS5 dialer: %w", err) + } + return dialer.Dial("udp", targetAddr) +} diff --git a/scripts/test_proxy.sh b/scripts/test_proxy.sh index a4b1ea0..2fb1d7c 100755 --- a/scripts/test_proxy.sh +++ b/scripts/test_proxy.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# gotproxy basic proxy integration test +# gotproxy basic proxy integration test (TCP + UDP) # Usage: sudo ./scripts/test_proxy.sh -# Requires: built gotproxy, curl; root (CAP_BPF) +# Requires: built gotproxy, curl; root (CAP_BPF). UDP test needs dig or nc. set -e @@ -69,6 +69,14 @@ log_contains_dest() { grep -q "Original destination:.*$pattern" "$LOG_FILE" 2>/dev/null } +# Check if log contains UDP original destination (e.g. 1.1.1.1:53) +# UDP proxy may log "UDP Original destination: ..." or same format as TCP +log_contains_udp_dest() { + local pattern="$1" + [[ -z "$LOG_FILE" || ! -f "$LOG_FILE" ]] && return 1 + grep -qE "(UDP.*Original destination:|Original destination:).*$pattern" "$LOG_FILE" 2>/dev/null +} + # Environment checks check_env() { if [[ "$(id -u)" -ne 0 ]]; then @@ -181,6 +189,26 @@ test_pids_filter() { fi } +# --- UDP proxy (basic): DNS over UDP is redirected and forwarded --- +# Uses DNS (UDP 1.1.1.1:53) as test traffic. Passes when proxy logs UDP original destination. +test_udp_basic_proxy() { + if ! command -v dig &>/dev/null; then + info "Test: UDP basic proxy — skipped (dig not installed)" + return + fi + info "Test: UDP basic proxy (DNS to ${EXAMPLE_IP}:53)" + start_gotproxy + # DNS query over UDP; dig sends to EXAMPLE_IP:53 + dig +short +time=5 +tries=1 @"$EXAMPLE_IP" example.com &>/dev/null || true + if log_contains_udp_dest "${EXAMPLE_IP}:53"; then + ok "UDP proxy: DNS request was forwarded (log shows ${EXAMPLE_IP}:53)" + else + # UDP proxy not implemented yet: no UDP log line + fail "UDP proxy: no UDP original destination for ${EXAMPLE_IP}:53 in log (UDP support may not be implemented yet)" + fi + stop_gotproxy +} + # --- Combined IP + process-name filter (--ip + --cmd): only when both match --- test_ip_cmd_filter() { info "Test: combined IP + process-name filter (--ip $EXAMPLE_IP/32 --cmd curl)" @@ -226,6 +254,7 @@ main() { test_cmd_filter test_pids_filter test_ip_cmd_filter + test_udp_basic_proxy echo "==========================================" echo " Passed: $PASSED Failed: $FAILED" echo "==========================================" From 5bc09137c485f5635dcdf9c790c88090595a9048 Mon Sep 17 00:00:00 2001 From: Dream95 Date: Sun, 22 Mar 2026 14:13:21 +0000 Subject: [PATCH 2/2] fix(udp): route SOCKS5 UDP via txthinking/socks5 Signed-off-by: Dream95 --- cmd/udpProxy.go | 10 ++++++---- go.mod | 3 +++ go.sum | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/cmd/udpProxy.go b/cmd/udpProxy.go index 9e003a6..5c9e374 100644 --- a/cmd/udpProxy.go +++ b/cmd/udpProxy.go @@ -8,7 +8,7 @@ import ( "time" "github.com/cilium/ebpf" - "golang.org/x/net/proxy" + "github.com/txthinking/socks5" ) const udpReadTimeout = 5 * time.Second @@ -108,9 +108,11 @@ func getUDPOriginalDest(clientAddr *net.UDPAddr, udpMap *ebpf.Map) (string, erro } func dialUDPViaSOCKS5(targetAddr string) (net.Conn, error) { - dialer, err := proxy.SOCKS5("udp", socks5ProxyAddr, nil, proxy.Direct) + + client, err := socks5.NewClient(socks5ProxyAddr, "", "", 0, 0) if err != nil { - return nil, fmt.Errorf("SOCKS5 dialer: %w", err) + return nil, fmt.Errorf("SOCKS5 client: %w", err) } - return dialer.Dial("udp", targetAddr) + + return client.Dial("udp", targetAddr) } diff --git a/go.mod b/go.mod index f786d5c..fd17246 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,14 @@ go 1.24.4 require ( github.com/cilium/ebpf v0.19.0 github.com/spf13/cobra v1.9.1 + github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e golang.org/x/net v0.38.0 golang.org/x/sys v0.31.0 ) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/spf13/pflag v1.0.6 // indirect + github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect ) diff --git a/go.sum b/go.sum index d805e61..d139844 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,9 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -26,11 +29,44 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0= +github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM= +github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e h1:xA7GVlbz6teIF4FdvuqwbX6C4tiqNk2PH7FRPIDerao= +github.com/txthinking/socks5 v0.0.0-20251011041537-5c31f201a10e/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=