diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 762cf81..19b946c 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -102,7 +102,27 @@ When reporting, please include: --- -## 8. Remaining Gaps (Low Priority) +## 8. UDP Features Disabled Under `-proxy` + +**Symptom:** Tools that depend on UDP fail with `UDP disabled under -proxy; the underlying feature cannot be tunneled` when `-proxy` (or `ALL_PROXY`) is set. + +**Details:** SOCKS5 UDP ASSOCIATE is rarely implemented correctly by proxy servers and client libraries. Silently bypassing the proxy for UDP when `-proxy` is configured would leak the operator's real source IP. gopacket therefore refuses UDP when proxied and surfaces a clear error through `transport.ErrUDPUnderProxy`. + +**Affected features:** + +| Feature | Tool(s) | Workaround | +|---------|---------|------------| +| SQL Server Browser discovery (UDP 1434) | `mssqlinstance` | Specify the port directly with `-port 1433` on `mssqlclient`; skip auto-discovery | +| DNS SRV lookup routed through the DC | `CheckLDAPStatus` | Pass `-dc-host ` to skip discovery | +| DNS hostname resolution via the DC | `GetADComputers` | Pass the target as an IP, or `-dc-ip ` | +| Forest-FQDN DNS fallback | `raiseChild` | Pass `-parent-dc ` explicitly | +| Local source-IP discovery | `smbexec` | Set `-target-ip ` manually | + +**Status:** By design. UDP tunneling over SOCKS5 is not a gopacket goal. If your workflow genuinely needs UDP over a proxy, use `proxychains` (which hooks libc at a lower level and can intercept UDP sockets) or a full VPN instead of `-proxy`. + +--- + +## 9. Remaining Gaps (Low Priority) These Impacket features are not yet implemented due to infrastructure requirements: diff --git a/README.md b/README.md index 6743193..5ef624f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,11 @@ To uninstall: ./install.sh --uninstall ``` -## Proxychains Support +## Proxy Support + +gopacket supports two independent proxying paths. They can also be chained. + +### proxychains (LD_PRELOAD) All gopacket tools work through proxychains. Go binaries normally bypass proxychains because Go's runtime handles DNS and networking internally, skipping the `LD_PRELOAD` hooks that proxychains relies on. gopacket works around this by linking against the system C library for network operations, allowing proxychains to intercept connections normally. @@ -50,6 +54,19 @@ proxychains gopacket-secretsdump 'domain/user:password@target' proxychains gopacket-smbclient -k -no-pass 'domain/user@dc.domain.local' ``` +### Internal SOCKS5 proxy (`-proxy`) + +Every tool accepts `-proxy` to route outbound TCP through a SOCKS5 server without relying on `LD_PRELOAD`. Accepted schemes: `socks5` and `socks5h`. When `-proxy` is unset, the `ALL_PROXY` / `all_proxy` environment variables are consulted as a fallback. + +```bash +gopacket-secretsdump -proxy socks5h://127.0.0.1:1080 'domain/user:password@target' +ALL_PROXY=socks5h://127.0.0.1:1080 gopacket-smbclient 'domain/user:password@target' +``` + +UDP-dependent features are **disabled** under `-proxy` rather than silently leaking packets (SOCKS5 UDP ASSOCIATE is rarely supported by proxies, and bypassing the proxy for UDP would reveal the attacker's real source IP). Affected features and their workarounds are documented in [KNOWN_ISSUES.md #9](KNOWN_ISSUES.md). + +**Chaining:** `-proxy` is compatible with proxychains. The TCP connection to the SOCKS5 proxy itself still goes through libc `connect()`, so `proxychains → gopacket → -proxy → target` works for nested routing scenarios. + ## Documentation See the [Library Developer Guide](https://github.com/mandiant/gopacket/wiki) for full API documentation, code examples, and architecture overview for building custom tools on top of gopacket's 24 protocol packages. @@ -196,6 +213,7 @@ KRB5CCNAME=ticket.ccache gopacket-secretsdump -k -no-pass 'domain/user@target' | `-dc-ip IP` | IP address of the domain controller | | `-target-ip IP` | IP address of the target (when using hostname for Kerberos) | | `-port PORT` | Target port (defaults vary by tool) | +| `-proxy URL` | Route outbound TCP through a SOCKS5 proxy (e.g. `socks5h://127.0.0.1:1080`). UDP features are disabled. | | `-debug` | Enable debug output | ### Quick Examples @@ -218,6 +236,9 @@ sudo gopacket-ntlmrelayx -t smb://target -socks # LDAP relay for RBCD sudo gopacket-ntlmrelayx -t ldaps://dc01.corp.local --delegate-access + +# Route all outbound traffic through a SOCKS5 proxy +gopacket-secretsdump -proxy socks5h://127.0.0.1:1080 'corp.local/admin:pass@dc01.corp.local' ``` ## Library @@ -357,7 +378,7 @@ new tooling stays private. Open-sourcing gopacket narrows that gap. - Kerberos authentication requires a valid ccache file (TGT or service ticket) - For Kerberos, use the FQDN hostname - not an IP address - If `KRB5CCNAME` is not set, tools will look for `.ccache` in the current directory -- All tools work through proxychains +- All tools support both proxychains and an internal `-proxy` SOCKS5 flag (see Proxy Support) - This project is for authorized security testing and research purposes only ## License diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go index bb4590a..4380bbe 100644 --- a/pkg/flags/flags.go +++ b/pkg/flags/flags.go @@ -23,6 +23,7 @@ import ( "gopacket/internal/build" "gopacket/pkg/session" + "gopacket/pkg/transport" ) // ExtraUsageLine is appended to the "Usage: tool [options] target" line (e.g. "[maxRid]") @@ -46,6 +47,7 @@ type Options struct { TargetIP string Port int IPv6 bool + Proxy string // Utility InputFile string @@ -62,6 +64,29 @@ type Options struct { Arguments []string } +// ProxyFlagUsage is the shared usage string for -proxy. Keep tools consistent +// so the flag behaves identically whether registered via Parse() or +// RegisterProxyFlag(). +const ProxyFlagUsage = "SOCKS5 proxy URL (e.g. socks5h://127.0.0.1:1080). Routes TCP through the proxy. UDP features are disabled. If unset, ALL_PROXY env is consulted." + +// ConfigureProxy wires the transport layer with the given proxy URL. Exits on +// error so a misconfigured -proxy fails the tool instead of silently bypassing +// the proxy. Call after flag.Parse(). +func ConfigureProxy(proxyURL string) { + if err := transport.Configure(transport.Options{Proxy: proxyURL}); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } +} + +// RegisterProxyFlag registers -proxy on the default flag.CommandLine and +// returns a finalizer to call after flag.Parse(). Intended for tools that +// hand-roll their flag setup instead of using Parse(). +func RegisterProxyFlag() func() { + proxyURL := flag.String("proxy", "", ProxyFlagUsage) + return func() { ConfigureProxy(*proxyURL) } +} + // CheckHelp scans os.Args for -h/--help anywhere and shows usage if found. // Call this after setting flag.Usage but before flag.Parse(). // This handles the case where -h appears after positional arguments, @@ -90,6 +115,7 @@ func Parse() *Options { flag.StringVar(&opts.TargetIP, "target-ip", "", "IP Address of the target machine") flag.IntVar(&opts.Port, "port", 445, "Destination port to connect to SMB Server") flag.BoolVar(&opts.IPv6, "6", false, "Connect via IPv6") + flag.StringVar(&opts.Proxy, "proxy", "", ProxyFlagUsage) flag.StringVar(&opts.InputFile, "inputfile", "", "input file with list of entries") flag.StringVar(&opts.OutputFile, "outputfile", "", "base output filename") @@ -120,6 +146,17 @@ func Parse() *Options { flag.Parse() + // Set global settings and configure transport unconditionally, even when + // no positional args were given. Tools that take config entirely via + // flags (e.g. listeners) still need Debug/Timestamp/-proxy to take effect. + if opts.Debug { + build.Debug = true + } + if opts.Timestamp { + build.Timestamp = true + } + ConfigureProxy(opts.Proxy) + // Handle Positional Arguments (target + optional command/args) if flag.NArg() == 0 { return opts @@ -129,19 +166,11 @@ func Parse() *Options { opts.Arguments = flag.Args()[1:] } - // Set Global Settings - if opts.Debug { - build.Debug = true - } - if opts.Timestamp { - build.Timestamp = true - } - return opts } // Version is the current gopacket release version. -const Version = "v0.1.0-beta" +const Version = "v0.1.1-beta" // Banner returns the standard gopacket banner string. func Banner() string { @@ -154,7 +183,7 @@ func printBanner() { func printGroupedHelp() { authFlags := []string{"hashes", "no-pass", "k", "aesKey", "keytab"} - connFlags := []string{"6", "dc-host", "dc-ip", "target-ip", "port"} + connFlags := []string{"6", "dc-host", "dc-ip", "target-ip", "port", "proxy"} miscFlags := []string{"inputfile", "outputfile", "ts", "debug"} // Maps to store flags diff --git a/pkg/relay/http_client.go b/pkg/relay/http_client.go index 057a5df..f505a5e 100644 --- a/pkg/relay/http_client.go +++ b/pkg/relay/http_client.go @@ -15,6 +15,7 @@ package relay import ( + "context" "crypto/tls" "encoding/base64" "fmt" @@ -24,8 +25,10 @@ import ( "net/http" "regexp" "strings" + "time" "gopacket/internal/build" + "gopacket/pkg/transport" ) // HTTPRelayClient implements ProtocolClient for relaying NTLM auth to HTTP/HTTPS targets. @@ -73,20 +76,22 @@ func (c *HTTPRelayClient) SetPath(path string) { func (c *HTTPRelayClient) InitConnection() error { // Use a custom transport that keeps connections alive for the NTLM handshake. // The entire 3-leg NTLM exchange must happen on the same TCP connection. - transport := &http.Transport{ + tr := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, DisableKeepAlives: false, // Force single connection reuse for NTLM auth MaxIdleConnsPerHost: 1, - DialContext: (&net.Dialer{ - Timeout: 10 * 1e9, // 10 seconds - }).DialContext, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + return transport.DialContext(ctx, network, addr) + }, } c.httpClient = &http.Client{ - Transport: transport, + Transport: tr, // Don't follow redirects — we need to see the raw 401/302 responses CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse diff --git a/pkg/relay/socks.go b/pkg/relay/socks.go index 38d7ee4..4c425f9 100644 --- a/pkg/relay/socks.go +++ b/pkg/relay/socks.go @@ -447,6 +447,9 @@ func (s *SOCKSServer) handleConnection(conn net.Conn) { func (s *SOCKSServer) handleDNSPassthrough(clientConn net.Conn, host string, port int) { target := fmt.Sprintf("%s:%d", host, port) + // Intentionally uses net.DialTimeout, not transport.DialTimeout: this is + // the outbound leg of our own SOCKS5 server, so routing it through the + // operator's -proxy would double-tunnel. remotConn, err := net.DialTimeout("tcp", target, 10*time.Second) if err != nil { if build.Debug { diff --git a/pkg/relay/winrm_client.go b/pkg/relay/winrm_client.go index 88a0afe..59ad560 100644 --- a/pkg/relay/winrm_client.go +++ b/pkg/relay/winrm_client.go @@ -15,6 +15,7 @@ package relay import ( + "context" "crypto/tls" "encoding/base64" "fmt" @@ -24,8 +25,10 @@ import ( "net/http" "regexp" "strings" + "time" "gopacket/internal/build" + "gopacket/pkg/transport" ) // WinRMRelayClient implements ProtocolClient for relaying NTLM auth to WinRM targets. @@ -50,20 +53,22 @@ func NewWinRMRelayClient(addr string, useTLS bool) *WinRMRelayClient { // InitConnection creates the HTTP client with connection reuse for NTLM handshake. // Implements ProtocolClient. func (c *WinRMRelayClient) InitConnection() error { - transport := &http.Transport{ + tr := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, DisableKeepAlives: false, // Force single connection reuse for NTLM auth MaxIdleConnsPerHost: 1, - DialContext: (&net.Dialer{ - Timeout: 10 * 1e9, // 10 seconds - }).DialContext, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + return transport.DialContext(ctx, network, addr) + }, } c.httpClient = &http.Client{ - Transport: transport, + Transport: tr, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, diff --git a/pkg/tds/sqlr.go b/pkg/tds/sqlr.go index fb46edd..c1f1baa 100644 --- a/pkg/tds/sqlr.go +++ b/pkg/tds/sqlr.go @@ -16,9 +16,10 @@ package tds import ( "fmt" - "net" "strings" "time" + + "gopacket/pkg/transport" ) // SQLRInstance represents a discovered SQL Server instance @@ -31,15 +32,10 @@ type SQLRInstance struct { NamedPipe string } -// GetInstances queries the SQL Server Browser service for instances +// GetInstances queries the SQL Server Browser service for instances. +// Fails under -proxy because SQL Browser is UDP and cannot be tunneled. func GetInstances(server string, timeout time.Duration) ([]SQLRInstance, error) { - // Create UDP connection - addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", server, SQLRPort)) - if err != nil { - return nil, err - } - - conn, err := net.DialUDP("udp", nil, addr) + conn, err := transport.DialUDP(fmt.Sprintf("%s:%d", server, SQLRPort)) if err != nil { return nil, err } @@ -66,15 +62,9 @@ func GetInstances(server string, timeout time.Duration) ([]SQLRInstance, error) return parseInstanceResponse(buf[:n]) } -// GetInstancePort queries for a specific instance's port +// GetInstancePort queries for a specific instance's port. Fails under -proxy (UDP). func GetInstancePort(server, instance string, timeout time.Duration) (int, error) { - // Create UDP connection - addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", server, SQLRPort)) - if err != nil { - return 0, err - } - - conn, err := net.DialUDP("udp", nil, addr) + conn, err := transport.DialUDP(fmt.Sprintf("%s:%d", server, SQLRPort)) if err != nil { return 0, err } diff --git a/pkg/transport/direct_libc.go b/pkg/transport/direct_libc.go new file mode 100644 index 0000000..d9f9b95 --- /dev/null +++ b/pkg/transport/direct_libc.go @@ -0,0 +1,137 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build cgo && !windows + +package transport + +/* +#include +#include +#include +#include +#include +#include +#include + +// libc_dial connects via libc's getaddrinfo + connect, which ARE hookable by LD_PRELOAD. +// Returns the file descriptor on success, or -1 (getaddrinfo fail) / -2 (connect fail) on error. +int libc_dial(const char *host, const char *port, int timeout_sec) { + struct addrinfo hints, *res, *p; + int sockfd; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + int rv = getaddrinfo(host, port, &hints, &res); + if (rv != 0) { + return -1; + } + + for (p = res; p != NULL; p = p->ai_next) { + sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (sockfd == -1) continue; + + // Set connect timeout via SO_SNDTIMEO (Linux honors this for connect()) + if (timeout_sec > 0) { + struct timeval tv; + tv.tv_sec = timeout_sec; + tv.tv_usec = 0; + setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + } + + if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) { + close(sockfd); + continue; + } + break; + } + + freeaddrinfo(res); + + if (p == NULL) return -2; + + // Clear the send timeout so it doesn't affect subsequent writes + if (timeout_sec > 0) { + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + } + + return sockfd; +} +*/ +import "C" + +import ( + "context" + "fmt" + "net" + "os" + "strings" + "time" + "unsafe" +) + +// directDial opens a connection via libc connect(), bypassing any configured +// proxy. The libc path is what LD_PRELOAD-based proxies like proxychains hook. +// UDP falls through to net.Dial because libc_dial is wired for SOCK_STREAM only +// and LD_PRELOAD proxies rarely handle UDP anyway. +func directDial(network, address string, timeoutSec int) (net.Conn, error) { + if strings.HasPrefix(network, "udp") { + return net.Dial(network, address) + } + + host, port, err := splitHostPort(address) + if err != nil { + return nil, err + } + + cHost := C.CString(host) + cPort := C.CString(port) + defer C.free(unsafe.Pointer(cHost)) + defer C.free(unsafe.Pointer(cPort)) + + fd := C.libc_dial(cHost, cPort, C.int(timeoutSec)) + if fd == -1 { + return nil, fmt.Errorf("getaddrinfo failed for %s", address) + } + if fd == -2 { + return nil, fmt.Errorf("connect failed for %s", address) + } + + f := os.NewFile(uintptr(fd), fmt.Sprintf("tcp:%s", address)) + conn, err := net.FileConn(f) + f.Close() // FileConn dups the fd + if err != nil { + return nil, fmt.Errorf("FileConn failed: %w", err) + } + return conn, nil +} + +// directDialContext dials directly, honoring ctx's deadline for the connect timeout. +func directDialContext(ctx context.Context, network, address string) (net.Conn, error) { + timeout := DefaultTimeout + if dl, ok := ctx.Deadline(); ok { + if d := time.Until(dl); d > 0 { + timeout = int(d.Seconds()) + if timeout < 1 { + timeout = 1 + } + } + } + return directDial(network, address, timeout) +} diff --git a/pkg/transport/direct_portable.go b/pkg/transport/direct_portable.go new file mode 100644 index 0000000..63e81a1 --- /dev/null +++ b/pkg/transport/direct_portable.go @@ -0,0 +1,38 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !cgo || windows + +package transport + +import ( + "context" + "net" + "time" +) + +// directDial opens a connection using the Go standard net.Dialer, bypassing +// any configured proxy. This path is used on Windows and on CGO_ENABLED=0 +// builds. It does not interoperate with LD_PRELOAD proxies like proxychains. +// Users on those platforms should use the -proxy flag instead. +func directDial(network, address string, timeoutSec int) (net.Conn, error) { + d := net.Dialer{Timeout: time.Duration(timeoutSec) * time.Second} + return d.Dial(network, address) +} + +// directDialContext dials directly using ctx for timeout/cancellation. +func directDialContext(ctx context.Context, network, address string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, network, address) +} diff --git a/pkg/transport/export_test.go b/pkg/transport/export_test.go new file mode 100644 index 0000000..6f07658 --- /dev/null +++ b/pkg/transport/export_test.go @@ -0,0 +1,22 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +// ResetForTest clears package-level proxy state so a subsequent Configure +// call won't panic on the one-shot guard. Only linked into test binaries. +func ResetForTest() { + proxyHolder.Store(nil) + configured.Store(false) +} diff --git a/pkg/transport/proxy.go b/pkg/transport/proxy.go new file mode 100644 index 0000000..769e90c --- /dev/null +++ b/pkg/transport/proxy.go @@ -0,0 +1,165 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "context" + "errors" + "fmt" + "net" + "net/url" + "os" + "strings" + "sync/atomic" + + "golang.org/x/net/proxy" +) + +// ErrUDPUnderProxy is returned by DialUDP when a proxy is configured. SOCKS5 +// UDP ASSOCIATE is rarely supported by proxies and servers, and silently +// leaking UDP packets from the attacker host when a proxy is configured would +// reveal the operator's real source IP. Callers should arrange for direct +// operation, or skip the UDP-dependent feature under -proxy. +var ErrUDPUnderProxy = errors.New("UDP disabled under -proxy; the underlying feature cannot be tunneled") + +// Options holds runtime configuration for the transport layer. +type Options struct { + // Proxy, if non-empty, is a SOCKS5 URL that outbound TCP is routed through. + // Accepted schemes: socks5, socks5h. When empty, the ALL_PROXY / all_proxy + // environment variables are consulted. + Proxy string +} + +// proxyHolder points to the configured proxy dialer, or nil for direct. +// Stored atomically so Dial is lock-free on the hot path. +var proxyHolder atomic.Pointer[dialerHolder] + +type dialerHolder struct { + cd proxy.ContextDialer + url string +} + +var configured atomic.Bool + +// Configure initializes the transport layer. Must be called exactly once, +// typically from flags.Parse at tool startup. Panics on subsequent calls so a +// misconfigured tool fails loudly rather than silently racing on package state. +func Configure(opts Options) error { + if !configured.CompareAndSwap(false, true) { + panic("transport.Configure called more than once") + } + + // We intentionally don't use proxy.FromEnvironment here: it caches the env + // value via sync.Once for the process lifetime, which breaks tests and + // prevents consistent URL validation for env-supplied values. + fromEnv := false + if opts.Proxy == "" { + envURL := os.Getenv("ALL_PROXY") + if envURL == "" { + envURL = os.Getenv("all_proxy") + } + if envURL == "" { + return nil + } + opts.Proxy = envURL + fromEnv = true + } + + u, err := url.Parse(opts.Proxy) + if err != nil { + return fmt.Errorf("transport: invalid proxy URL %q: %v", opts.Proxy, err) + } + switch strings.ToLower(u.Scheme) { + case "socks5", "socks5h": + // x/net/proxy treats both identically (always sends hostname to the + // proxy; server decides resolution). socks5h is the documented + // recommendation because it mirrors proxychains' proxy_dns=on default. + default: + return fmt.Errorf("transport: unsupported proxy scheme %q (supported: socks5, socks5h)", u.Scheme) + } + + // Route the TCP connection to the SOCKS5 server through the libc dialer so + // LD_PRELOAD-based proxies (proxychains) can still hook it, useful for + // chaining: proxychains -> gopacket -> -proxy SOCKS5 -> target. + d, err := proxy.FromURL(u, libcForwarder{}) + if err != nil { + return fmt.Errorf("transport: initialize proxy dialer for %q: %v", opts.Proxy, err) + } + cd, ok := d.(proxy.ContextDialer) + if !ok { + return fmt.Errorf("transport: proxy dialer %T does not implement ContextDialer", d) + } + urlLabel := u.Redacted() + if fromEnv { + urlLabel += " (from ALL_PROXY)" + } + proxyHolder.Store(&dialerHolder{cd: cd, url: urlLabel}) + return nil +} + +// IsProxyConfigured reports whether a proxy is in effect. Callers that cannot +// meaningfully operate through a proxy (UDP probes, local-IP discovery) should +// consult this and short-circuit. +func IsProxyConfigured() bool { return proxyHolder.Load() != nil } + +// ProxyURL returns the configured proxy URL (redacted), or "" if none. +func ProxyURL() string { + if h := proxyHolder.Load(); h != nil { + return h.url + } + return "" +} + +// libcForwarder is the base proxy.Dialer used to reach the SOCKS5 server +// itself. Goes through directDial so the TCP connect to the proxy is still +// hookable by LD_PRELOAD. +type libcForwarder struct{} + +func (libcForwarder) Dial(network, address string) (net.Conn, error) { + return directDial(network, address, DefaultTimeout) +} + +func (libcForwarder) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + return directDialContext(ctx, network, address) +} + +// DialContext is a context-aware variant of Dial, suitable as an +// http.Transport.DialContext. Routes through the configured proxy if set. +// UDP under -proxy returns ErrUDPUnderProxy rather than letting a cryptic +// "network not implemented" bubble up from the SOCKS5 layer. +func DialContext(ctx context.Context, network, address string) (net.Conn, error) { + if h := proxyHolder.Load(); h != nil { + if strings.HasPrefix(network, "udp") { + return nil, ErrUDPUnderProxy + } + return h.cd.DialContext(ctx, network, address) + } + return directDialContext(ctx, network, address) +} + +// DialUDP opens a connected UDP socket to address. Returns ErrUDPUnderProxy +// if a proxy is configured. SOCKS5 UDP ASSOCIATE is rarely supported and +// silently bypassing the proxy under -proxy would reveal the operator's real +// source IP. +func DialUDP(address string) (*net.UDPConn, error) { + if IsProxyConfigured() { + return nil, ErrUDPUnderProxy + } + addr, err := net.ResolveUDPAddr("udp", address) + if err != nil { + return nil, err + } + return net.DialUDP("udp", nil, addr) +} diff --git a/pkg/transport/proxy_test.go b/pkg/transport/proxy_test.go new file mode 100644 index 0000000..8be905b --- /dev/null +++ b/pkg/transport/proxy_test.go @@ -0,0 +1,242 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "errors" + "io" + "net" + "strings" + "testing" +) + +func TestConfigureRejectsInvalidScheme(t *testing.T) { + t.Cleanup(ResetForTest) + err := Configure(Options{Proxy: "http://127.0.0.1:8080"}) + if err == nil || !strings.Contains(err.Error(), "unsupported proxy scheme") { + t.Fatalf("want unsupported scheme error, got %v", err) + } +} + +func TestConfigureRejectsMalformedURL(t *testing.T) { + t.Cleanup(ResetForTest) + err := Configure(Options{Proxy: "::not a url::"}) + if err == nil { + t.Fatalf("want error for malformed URL, got nil") + } +} + +func TestConfigureAcceptsSocks5(t *testing.T) { + t.Cleanup(ResetForTest) + if err := Configure(Options{Proxy: "socks5://127.0.0.1:1080"}); err != nil { + t.Fatalf("socks5:// should be accepted: %v", err) + } + if !IsProxyConfigured() { + t.Fatal("IsProxyConfigured should be true after successful Configure") + } +} + +func TestConfigureAcceptsSocks5h(t *testing.T) { + t.Cleanup(ResetForTest) + if err := Configure(Options{Proxy: "socks5h://127.0.0.1:1080"}); err != nil { + t.Fatalf("socks5h:// should be accepted: %v", err) + } +} + +func TestConfigureFromEnv(t *testing.T) { + t.Cleanup(ResetForTest) + t.Setenv("ALL_PROXY", "socks5h://127.0.0.1:1080") + if err := Configure(Options{Proxy: ""}); err != nil { + t.Fatalf("env-based configure should succeed: %v", err) + } + if !IsProxyConfigured() { + t.Fatal("ALL_PROXY should enable proxy") + } +} + +func TestConfigureDoubleCallPanics(t *testing.T) { + t.Cleanup(ResetForTest) + if err := Configure(Options{}); err != nil { + t.Fatalf("first Configure failed: %v", err) + } + defer func() { + if r := recover(); r == nil { + t.Fatal("second Configure should panic") + } + }() + _ = Configure(Options{}) +} + +func TestIsProxyConfiguredFalseByDefault(t *testing.T) { + t.Cleanup(ResetForTest) + t.Setenv("ALL_PROXY", "") + if err := Configure(Options{}); err != nil { + t.Fatalf("Configure: %v", err) + } + if IsProxyConfigured() { + t.Fatal("IsProxyConfigured should be false with no -proxy and no ALL_PROXY") + } +} + +func TestDialUDPReturnsErrWhenProxied(t *testing.T) { + t.Cleanup(ResetForTest) + if err := Configure(Options{Proxy: "socks5h://127.0.0.1:1080"}); err != nil { + t.Fatalf("Configure: %v", err) + } + _, err := DialUDP("127.0.0.1:53") + if !errors.Is(err, ErrUDPUnderProxy) { + t.Fatalf("want ErrUDPUnderProxy, got %v", err) + } +} + +func TestDialContextReturnsErrOnUDPUnderProxy(t *testing.T) { + t.Cleanup(ResetForTest) + if err := Configure(Options{Proxy: "socks5h://127.0.0.1:1080"}); err != nil { + t.Fatalf("Configure: %v", err) + } + _, err := DialContext(t.Context(), "udp", "127.0.0.1:53") + if !errors.Is(err, ErrUDPUnderProxy) { + t.Fatalf("want ErrUDPUnderProxy, got %v", err) + } +} + +func TestProxyURLRedactsCredentials(t *testing.T) { + t.Cleanup(ResetForTest) + if err := Configure(Options{Proxy: "socks5h://user:s3cret@127.0.0.1:1080"}); err != nil { + t.Fatalf("Configure: %v", err) + } + got := ProxyURL() + if strings.Contains(got, "s3cret") { + t.Fatalf("ProxyURL leaked password: %q", got) + } + if !strings.Contains(got, "user") || !strings.Contains(got, "127.0.0.1:1080") { + t.Fatalf("ProxyURL unexpected shape: %q", got) + } +} + +func TestProxyURLEmptyWhenUnconfigured(t *testing.T) { + t.Cleanup(ResetForTest) + t.Setenv("ALL_PROXY", "") + if err := Configure(Options{}); err != nil { + t.Fatalf("Configure: %v", err) + } + if got := ProxyURL(); got != "" { + t.Fatalf("ProxyURL should be empty when unconfigured, got %q", got) + } +} + +// TestDialDirectWithNoProxy verifies that with no -proxy and no ALL_PROXY, +// transport.Dial reaches a target directly (via directDial). Guards against +// regressions where proxy plumbing accidentally intercepts the no-proxy path. +func TestDialDirectWithNoProxy(t *testing.T) { + t.Cleanup(ResetForTest) + t.Setenv("ALL_PROXY", "") + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + defer ln.Close() + go func() { + for { + c, err := ln.Accept() + if err != nil { + return + } + go func() { + defer c.Close() + _, _ = io.Copy(c, c) + }() + } + }() + + if err := Configure(Options{}); err != nil { + t.Fatalf("Configure: %v", err) + } + if IsProxyConfigured() { + t.Fatal("no proxy should be configured") + } + + conn, err := Dial("tcp", ln.Addr().String()) + if err != nil { + t.Fatalf("Dial direct: %v", err) + } + defer conn.Close() + + want := []byte("direct-path-ok") + if _, err := conn.Write(want); err != nil { + t.Fatalf("write: %v", err) + } + got := make([]byte, len(want)) + if _, err := io.ReadFull(conn, got); err != nil { + t.Fatalf("read: %v", err) + } + if string(got) != string(want) { + t.Fatalf("echo mismatch: got %q want %q", got, want) + } +} + +// TestEndToEndDialThroughSocks5 wires Configure to an in-process SOCKS5 server +// and verifies a TCP round-trip flows through it. +func TestEndToEndDialThroughSocks5(t *testing.T) { + t.Cleanup(ResetForTest) + + // Echo server, the "real" target. + echoLn, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen echo: %v", err) + } + defer echoLn.Close() + go func() { + for { + c, err := echoLn.Accept() + if err != nil { + return + } + go func() { + defer c.Close() + _, _ = io.Copy(c, c) + }() + } + }() + + socks := newTestSOCKS5(t) + + if err := Configure(Options{Proxy: "socks5h://" + socks.addr}); err != nil { + t.Fatalf("Configure: %v", err) + } + + conn, err := Dial("tcp", echoLn.Addr().String()) + if err != nil { + t.Fatalf("Dial through proxy: %v", err) + } + defer conn.Close() + + want := []byte("hello-over-socks5") + if _, err := conn.Write(want); err != nil { + t.Fatalf("write: %v", err) + } + got := make([]byte, len(want)) + if _, err := io.ReadFull(conn, got); err != nil { + t.Fatalf("read: %v", err) + } + if string(got) != string(want) { + t.Fatalf("echo mismatch: got %q want %q", got, want) + } + + if n := socks.connects.Load(); n != 1 { + t.Fatalf("SOCKS5 server saw %d CONNECTs, want 1", n) + } +} diff --git a/pkg/transport/socks5_server_test.go b/pkg/transport/socks5_server_test.go new file mode 100644 index 0000000..b2a1497 --- /dev/null +++ b/pkg/transport/socks5_server_test.go @@ -0,0 +1,147 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "encoding/binary" + "fmt" + "io" + "net" + "sync" + "sync/atomic" + "testing" +) + +// testSOCKS5 is a minimal SOCKS5 server for use in transport tests. It +// supports only CONNECT with no authentication (method 0x00) and only IPv4 +// and DOMAIN address types. It is NOT a production SOCKS5 implementation. +type testSOCKS5 struct { + ln net.Listener + addr string + wg sync.WaitGroup + connects atomic.Int32 // number of successful CONNECTs handled +} + +// newTestSOCKS5 starts a SOCKS5 server on 127.0.0.1 with an ephemeral port. +// It terminates when t.Cleanup runs. +func newTestSOCKS5(t *testing.T) *testSOCKS5 { + t.Helper() + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + s := &testSOCKS5{ln: ln, addr: ln.Addr().String()} + s.wg.Add(1) + go s.serve() + t.Cleanup(func() { + ln.Close() + s.wg.Wait() + }) + return s +} + +func (s *testSOCKS5) serve() { + defer s.wg.Done() + for { + c, err := s.ln.Accept() + if err != nil { + return + } + s.wg.Add(1) + go func() { + defer s.wg.Done() + s.handle(c) + }() + } +} + +func (s *testSOCKS5) handle(client net.Conn) { + defer client.Close() + + // Greeting: VER | NMETHODS | METHODS... + hdr := make([]byte, 2) + if _, err := io.ReadFull(client, hdr); err != nil { + return + } + if hdr[0] != 0x05 { + return + } + methods := make([]byte, hdr[1]) + if _, err := io.ReadFull(client, methods); err != nil { + return + } + // Accept only NO AUTH (0x00) + if _, err := client.Write([]byte{0x05, 0x00}); err != nil { + return + } + + // Request: VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT + req := make([]byte, 4) + if _, err := io.ReadFull(client, req); err != nil { + return + } + if req[0] != 0x05 || req[1] != 0x01 /* CONNECT */ { + _, _ = client.Write([]byte{0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0}) // cmd not supported + return + } + + var host string + switch req[3] { + case 0x01: // IPv4 + b := make([]byte, 4) + if _, err := io.ReadFull(client, b); err != nil { + return + } + host = net.IP(b).String() + case 0x03: // DOMAIN + lb := make([]byte, 1) + if _, err := io.ReadFull(client, lb); err != nil { + return + } + b := make([]byte, lb[0]) + if _, err := io.ReadFull(client, b); err != nil { + return + } + host = string(b) + default: + _, _ = client.Write([]byte{0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0}) // atyp not supported + return + } + + pb := make([]byte, 2) + if _, err := io.ReadFull(client, pb); err != nil { + return + } + port := binary.BigEndian.Uint16(pb) + + target, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port)) + if err != nil { + _, _ = client.Write([]byte{0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0}) // conn refused + return + } + defer target.Close() + + // Reply: VER | REP=0 | RSV | ATYP=1 | BND.ADDR=0 | BND.PORT=0 + if _, err := client.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}); err != nil { + return + } + s.connects.Add(1) + + // Splice both directions + done := make(chan struct{}, 2) + go func() { _, _ = io.Copy(target, client); done <- struct{}{} }() + go func() { _, _ = io.Copy(client, target); done <- struct{}{} }() + <-done +} diff --git a/pkg/transport/tcp.go b/pkg/transport/tcp.go index 173bcdb..fed17d3 100644 --- a/pkg/transport/tcp.go +++ b/pkg/transport/tcp.go @@ -14,116 +14,44 @@ package transport -/* -#include -#include -#include -#include -#include -#include -#include - -// libc_dial connects via libc's getaddrinfo + connect, which ARE hookable by LD_PRELOAD. -// Returns the file descriptor on success, or -1 (getaddrinfo fail) / -2 (connect fail) on error. -int libc_dial(const char *host, const char *port, int timeout_sec) { - struct addrinfo hints, *res, *p; - int sockfd; - - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - - int rv = getaddrinfo(host, port, &hints, &res); - if (rv != 0) { - return -1; - } - - for (p = res; p != NULL; p = p->ai_next) { - sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); - if (sockfd == -1) continue; - - // Set connect timeout via SO_SNDTIMEO (Linux honors this for connect()) - if (timeout_sec > 0) { - struct timeval tv; - tv.tv_sec = timeout_sec; - tv.tv_usec = 0; - setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); - } - - if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) { - close(sockfd); - continue; - } - break; - } - - freeaddrinfo(res); - - if (p == NULL) return -2; - - // Clear the send timeout so it doesn't affect subsequent writes - if (timeout_sec > 0) { - struct timeval tv; - tv.tv_sec = 0; - tv.tv_usec = 0; - setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); - } - - return sockfd; -} -*/ -import "C" - import ( + "context" "crypto/tls" "fmt" "net" - "os" "strings" - "unsafe" + "time" ) // DefaultTimeout is the default connect timeout in seconds. const DefaultTimeout = 30 -// Dial connects to the address on the named network using libc's connect(), -// which is hookable by LD_PRELOAD-based proxies like proxychains. -// The address must be in "host:port" format. +// Dial opens a TCP connection. Routes through the configured proxy if one was +// set via Configure; otherwise uses the platform's direct dialer (libc +// connect() on Unix/cgo, net.Dialer elsewhere). func Dial(network, address string) (net.Conn, error) { return DialTimeout(network, address, DefaultTimeout) } -// DialTimeout connects using libc's connect() with the given timeout in seconds. +// DialTimeout is Dial with an explicit connect timeout in seconds. +// A non-positive timeoutSec is normalized to DefaultTimeout so the direct and +// proxy branches behave consistently. func DialTimeout(network, address string, timeoutSec int) (net.Conn, error) { - host, port, err := splitHostPort(address) - if err != nil { - return nil, err - } - - cHost := C.CString(host) - cPort := C.CString(port) - defer C.free(unsafe.Pointer(cHost)) - defer C.free(unsafe.Pointer(cPort)) - - fd := C.libc_dial(cHost, cPort, C.int(timeoutSec)) - if fd == -1 { - return nil, fmt.Errorf("getaddrinfo failed for %s", address) + if timeoutSec <= 0 { + timeoutSec = DefaultTimeout } - if fd == -2 { - return nil, fmt.Errorf("connect failed for %s", address) - } - - // Convert C file descriptor to Go net.Conn - f := os.NewFile(uintptr(fd), fmt.Sprintf("tcp:%s", address)) - conn, err := net.FileConn(f) - f.Close() // FileConn dups the fd, so close the original - if err != nil { - return nil, fmt.Errorf("FileConn failed: %w", err) + if h := proxyHolder.Load(); h != nil { + if strings.HasPrefix(network, "udp") { + return nil, ErrUDPUnderProxy + } + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second) + defer cancel() + return h.cd.DialContext(ctx, network, address) } - return conn, nil + return directDial(network, address, timeoutSec) } -// DialTLS connects via libc then wraps the connection in TLS. +// DialTLS opens a TCP connection (via proxy if configured) and wraps it in TLS. func DialTLS(network, address string, config *tls.Config) (*tls.Conn, error) { rawConn, err := Dial(network, address) if err != nil { @@ -142,24 +70,20 @@ func DialTLS(network, address string, config *tls.Config) (*tls.Conn, error) { return tlsConn, nil } -// Dialer provides a way to establish connections via libc. +// Dialer is a value-typed dialer suitable for APIs that expect a struct with a +// Dial method (e.g. pkg/smb, pkg/ldap). Respects the configured proxy. type Dialer struct { TimeoutSec int } -// Dial establishes a TCP connection to the specified address using libc's connect(). +// Dial establishes a TCP connection to address. Routes through the proxy if configured. func (d *Dialer) Dial(network, address string) (net.Conn, error) { - timeout := d.TimeoutSec - if timeout == 0 { - timeout = DefaultTimeout - } - return DialTimeout(network, address, timeout) + return DialTimeout(network, address, d.TimeoutSec) } func splitHostPort(address string) (host, port string, err error) { host, port, err = net.SplitHostPort(address) if err != nil { - // Try treating the whole thing as a host (no port) if !strings.Contains(address, ":") { return address, "", fmt.Errorf("missing port in address: %s", address) } diff --git a/tools/CheckLDAPStatus/main.go b/tools/CheckLDAPStatus/main.go index 6350df6..a6343db 100644 --- a/tools/CheckLDAPStatus/main.go +++ b/tools/CheckLDAPStatus/main.go @@ -39,7 +39,7 @@ var ( func main() { flag.Usage = func() { - fmt.Fprintln(os.Stderr, "gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Fprintln(os.Stderr, "gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr, "LDAP signing and channel binding enumeration utility.") fmt.Fprintln(os.Stderr) @@ -52,9 +52,11 @@ func main() { // Standard flags debug := flag.Bool("debug", false, "Turn DEBUG output ON") ts := flag.Bool("ts", false, "Adds timestamp to every logging output") + configureProxy := flags.RegisterProxyFlag() flags.CheckHelp() flag.Parse() + configureProxy() // Validate required flags if *dcHost == "" && (*dcIP == "" || *domain == "") { @@ -120,12 +122,13 @@ func (c *LDAPChecker) Run() error { } func (c *LDAPChecker) listDCs() ([]string, error) { - // Create custom resolver using the DC as DNS server + // Custom resolver using the DC as DNS server. Under -proxy the resolver's + // UDP attempt will fail at the SOCKS5 layer; supply -dc-host directly to + // skip this lookup when proxied. resolver := &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{Timeout: c.timeout} - return d.DialContext(ctx, "udp", net.JoinHostPort(c.dcIP, "53")) + return transport.DialContext(ctx, network, net.JoinHostPort(c.dcIP, "53")) }, } diff --git a/tools/DumpNTLMInfo/main.go b/tools/DumpNTLMInfo/main.go index 056ef00..cc86b82 100644 --- a/tools/DumpNTLMInfo/main.go +++ b/tools/DumpNTLMInfo/main.go @@ -76,7 +76,7 @@ const EPOCH_DIFF = 116444736000000000 func main() { flag.Usage = func() { - fmt.Fprintln(os.Stderr, "gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Fprintln(os.Stderr, "gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr, "Do NTLM authentication and parse information.") fmt.Fprintln(os.Stderr) @@ -88,9 +88,11 @@ func main() { debug := flag.Bool("debug", false, "Turn DEBUG output ON") ts := flag.Bool("ts", false, "Adds timestamp to every logging output") + configureProxy := flags.RegisterProxyFlag() flags.CheckHelp() flag.Parse() + configureProxy() if flag.NArg() < 1 { flag.Usage() diff --git a/tools/GetADComputers/main.go b/tools/GetADComputers/main.go index 3a00ec2..15ed989 100644 --- a/tools/GetADComputers/main.go +++ b/tools/GetADComputers/main.go @@ -28,6 +28,7 @@ import ( "gopacket/pkg/flags" "gopacket/pkg/ldap" "gopacket/pkg/session" + "gopacket/pkg/transport" ) var ( @@ -161,12 +162,12 @@ func resolveHostname(hostname, dnsServer string) string { return "" } - // Use custom resolver to query the DC's DNS + // Custom resolver using the DC's DNS. Under -proxy, UDP DNS cannot be + // tunneled via SOCKS5; callers should pass an IP directly in that case. resolver := &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{Timeout: 2 * time.Second} - return d.DialContext(ctx, "udp", dnsServer+":53") + return transport.DialContext(ctx, network, dnsServer+":53") }, } diff --git a/tools/changepasswd/main.go b/tools/changepasswd/main.go index d54120e..2875170 100644 --- a/tools/changepasswd/main.go +++ b/tools/changepasswd/main.go @@ -20,7 +20,6 @@ import ( "fmt" "io" "log" - "net" "os" "strings" "time" @@ -33,6 +32,7 @@ import ( "gopacket/pkg/ldap" "gopacket/pkg/session" "gopacket/pkg/smb" + "gopacket/pkg/transport" goldap "github.com/go-ldap/ldap/v3" krbclient "github.com/jcmturner/gokrb5/v8/client" @@ -616,7 +616,7 @@ func doKpasswd(opts *flags.Options, target session.Target, targetUsername, targe // sendKpasswd sends a kpasswd request to the KDC on TCP port 464 and returns the response. func sendKpasswd(kdc string, data []byte) ([]byte, error) { addr := fmt.Sprintf("%s:464", kdc) - conn, err := net.DialTimeout("tcp", addr, 10*time.Second) + conn, err := transport.DialTimeout("tcp", addr, 10) if err != nil { return nil, fmt.Errorf("failed to connect to kpasswd %s: %v", addr, err) } diff --git a/tools/describeTicket/main.go b/tools/describeTicket/main.go index 742136e..a74a0d4 100644 --- a/tools/describeTicket/main.go +++ b/tools/describeTicket/main.go @@ -966,7 +966,7 @@ func loadCCacheSafe(path string) (ccache *credentials.CCache, err error) { } func printUsage() { - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() fmt.Println("Parses a ccache ticket file and displays credential information.") fmt.Println("With a decryption key, decrypts the ticket and shows the full PAC.") diff --git a/tools/getArch/main.go b/tools/getArch/main.go index cdf1ce1..22921f0 100644 --- a/tools/getArch/main.go +++ b/tools/getArch/main.go @@ -123,7 +123,7 @@ func checkArch(machine string, timeoutSec int) { } func printUsage() { - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() fmt.Println("Gets the target system's OS architecture version") fmt.Println() diff --git a/tools/karmaSMB/main.go b/tools/karmaSMB/main.go index 5184ec2..d531c0b 100644 --- a/tools/karmaSMB/main.go +++ b/tools/karmaSMB/main.go @@ -275,7 +275,7 @@ var NDR_UUID = []byte{ func main() { flag.Usage = func() { - fmt.Fprintf(os.Stderr, "gopacket v0.1.0-beta - Copyright 2026 Google LLC\n\n") + fmt.Fprintf(os.Stderr, "gopacket v0.1.1-beta - Copyright 2026 Google LLC\n\n") fmt.Fprintf(os.Stderr, "For every file request received, this module will return the pathname\n") fmt.Fprintf(os.Stderr, "contents based on extension matching.\n\n") fmt.Fprintf(os.Stderr, "Usage: karmaSMB [options] pathname\n\n") diff --git a/tools/keylistattack/main.go b/tools/keylistattack/main.go index bddba47..626230f 100644 --- a/tools/keylistattack/main.go +++ b/tools/keylistattack/main.go @@ -73,7 +73,7 @@ func main() { } func usage() { - fmt.Fprintf(os.Stderr, `gopacket v0.1.0-beta - Copyright 2026 Google LLC + fmt.Fprintf(os.Stderr, `gopacket v0.1.1-beta - Copyright 2026 Google LLC Performs the KERB-KEY-LIST-REQ attack to dump secrets from the remote machine without executing any agent there. diff --git a/tools/lookupsid/main.go b/tools/lookupsid/main.go index a2c1b90..f28095b 100644 --- a/tools/lookupsid/main.go +++ b/tools/lookupsid/main.go @@ -63,7 +63,7 @@ func main() { } } - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() fmt.Printf("[*] Brute forcing SIDs at %s\n", target.Host) diff --git a/tools/mssqlclient/main.go b/tools/mssqlclient/main.go index bdf33bf..c4dc71f 100644 --- a/tools/mssqlclient/main.go +++ b/tools/mssqlclient/main.go @@ -42,7 +42,7 @@ func main() { os.Exit(1) } - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() // Parse target diff --git a/tools/mssqlinstance/main.go b/tools/mssqlinstance/main.go index 2070792..62043b5 100644 --- a/tools/mssqlinstance/main.go +++ b/tools/mssqlinstance/main.go @@ -20,6 +20,7 @@ import ( "os" "time" + "gopacket/pkg/flags" "gopacket/pkg/tds" ) @@ -29,7 +30,7 @@ var ( func main() { flag.Usage = func() { - fmt.Fprintf(os.Stderr, `gopacket v0.1.0-beta - Copyright 2026 Google LLC + fmt.Fprintf(os.Stderr, `gopacket v0.1.1-beta - Copyright 2026 Google LLC SQL Server Browser Protocol discovery tool. @@ -58,14 +59,16 @@ Note: Requires the SQL Server Browser service to be running on the target. `, os.Args[0], os.Args[0]) } + configureProxy := flags.RegisterProxyFlag() flag.Parse() + configureProxy() if flag.NArg() < 1 { flag.Usage() os.Exit(1) } - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() target := flag.Arg(0) diff --git a/tools/net/main.go b/tools/net/main.go index bcfd625..e8524a6 100644 --- a/tools/net/main.go +++ b/tools/net/main.go @@ -111,7 +111,7 @@ func main() { } } - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() // Connect via SMB @@ -196,7 +196,7 @@ func main() { } func printUsage() { - fmt.Fprintln(os.Stderr, "gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Fprintln(os.Stderr, "gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr, "Usage: net [auth-flags] target [command-flags]") fmt.Fprintln(os.Stderr) diff --git a/tools/ntfs-read/main.go b/tools/ntfs-read/main.go index 7d5caa8..122582a 100644 --- a/tools/ntfs-read/main.go +++ b/tools/ntfs-read/main.go @@ -400,7 +400,7 @@ func cleanPath(path string) string { } func printUsage() { - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() fmt.Println("NTFS explorer (read-only)") fmt.Println() diff --git a/tools/ping/main.go b/tools/ping/main.go index 1355e36..0e333a6 100644 --- a/tools/ping/main.go +++ b/tools/ping/main.go @@ -32,7 +32,7 @@ const ( func main() { if len(os.Args) < 3 { - fmt.Fprintf(os.Stderr, "gopacket v0.1.0-beta - Copyright 2026 Google LLC\n\n") + fmt.Fprintf(os.Stderr, "gopacket v0.1.1-beta - Copyright 2026 Google LLC\n\n") fmt.Fprintf(os.Stderr, "Simple ICMP ping using raw sockets.\n\n") fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) fmt.Fprintf(os.Stderr, "\nNote: Requires root/CAP_NET_RAW privileges.\n") diff --git a/tools/ping6/main.go b/tools/ping6/main.go index 14b3961..ae43248 100644 --- a/tools/ping6/main.go +++ b/tools/ping6/main.go @@ -30,7 +30,7 @@ const ( ) func main() { - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() if len(os.Args) < 3 { diff --git a/tools/raiseChild/main.go b/tools/raiseChild/main.go index 641ee82..cf48b33 100644 --- a/tools/raiseChild/main.go +++ b/tools/raiseChild/main.go @@ -35,6 +35,7 @@ import ( "gopacket/pkg/kerberos" "gopacket/pkg/session" "gopacket/pkg/smb" + "gopacket/pkg/transport" ) var ( @@ -356,7 +357,14 @@ func discoverParentDC(forestFQDN string, target session.Target, creds *session.C } } - // Fallback: DNS resolution of forest FQDN + // Fallback: DNS resolution of forest FQDN. Disabled under -proxy because + // net.LookupHost uses the OS resolver, which would silently bypass the + // configured SOCKS5 proxy and leak DNS. Callers should pass the parent DC + // explicitly via -parent-dc in that case. + if transport.IsProxyConfigured() { + fmt.Println("[!] DNS fallback disabled under -proxy, pass parent DC explicitly via -parent-dc") + return "" + } addrs, err := net.LookupHost(forestFQDN) if err == nil && len(addrs) > 0 { fmt.Printf("[*] DNS resolved %s to %s\n", forestFQDN, addrs[0]) diff --git a/tools/rbcd/main.go b/tools/rbcd/main.go index e762e93..9c45e28 100644 --- a/tools/rbcd/main.go +++ b/tools/rbcd/main.go @@ -30,7 +30,7 @@ import ( ) func printUsage() { - fmt.Fprintf(os.Stderr, `gopacket v0.1.0-beta - Copyright 2026 Google LLC + fmt.Fprintf(os.Stderr, `gopacket v0.1.1-beta - Copyright 2026 Google LLC usage: rbcd [-h] [-delegate-to DELEGATE_TO] [-delegate-from DELEGATE_FROM] [-action {read,write,remove,flush}] [-use-ldaps] [-debug] [-ts] diff --git a/tools/reg/main.go b/tools/reg/main.go index fe24c3d..326854b 100644 --- a/tools/reg/main.go +++ b/tools/reg/main.go @@ -107,7 +107,7 @@ func main() { } } - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() // Connect via SMB @@ -192,7 +192,7 @@ func main() { } func printUsage() { - fmt.Fprintln(os.Stderr, "gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Fprintln(os.Stderr, "gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr, "Usage: reg [auth-flags] target [command-flags]") fmt.Fprintln(os.Stderr) diff --git a/tools/rpcdump/main.go b/tools/rpcdump/main.go index 7d9081b..64b0402 100644 --- a/tools/rpcdump/main.go +++ b/tools/rpcdump/main.go @@ -37,7 +37,7 @@ func main() { os.Exit(1) } - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() target, creds, err := session.ParseTargetString(opts.TargetStr) diff --git a/tools/rpcmap/main.go b/tools/rpcmap/main.go index 2025260..bf32b42 100644 --- a/tools/rpcmap/main.go +++ b/tools/rpcmap/main.go @@ -27,6 +27,7 @@ import ( "gopacket/internal/build" "gopacket/pkg/dcerpc" "gopacket/pkg/dcerpc/epmapper" + "gopacket/pkg/flags" "gopacket/pkg/transport" ) @@ -89,7 +90,7 @@ type ifaceResult struct { func main() { flag.Usage = func() { - fmt.Fprintf(os.Stderr, `gopacket v0.1.0-beta - Copyright 2026 Google LLC + fmt.Fprintf(os.Stderr, `gopacket v0.1.1-beta - Copyright 2026 Google LLC Scans for listening MSRPC interfaces. Tries the MGMT interface first, falls back to UUID bruteforce if MGMT is not available. @@ -111,14 +112,16 @@ Examples: `, os.Args[0], os.Args[0], os.Args[0]) } + configureProxy := flags.RegisterProxyFlag() flag.Parse() + configureProxy() if flag.NArg() < 1 { flag.Usage() os.Exit(1) } - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() if *debug { diff --git a/tools/samrdump/main.go b/tools/samrdump/main.go index 9d66e80..8a32ac0 100644 --- a/tools/samrdump/main.go +++ b/tools/samrdump/main.go @@ -55,7 +55,7 @@ func main() { } } - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() fmt.Printf("[*] Retrieving endpoint list from %s\n", target.Host) diff --git a/tools/services/main.go b/tools/services/main.go index 3a969e4..371af9f 100644 --- a/tools/services/main.go +++ b/tools/services/main.go @@ -100,7 +100,7 @@ func main() { } } - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() // Connect via SMB @@ -390,7 +390,7 @@ func errorControlName(t uint32) string { } func printUsage() { - fmt.Fprintln(os.Stderr, "gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Fprintln(os.Stderr, "gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr, "Usage: services [auth-flags] target [action-flags]") fmt.Fprintln(os.Stderr) diff --git a/tools/smbexec/main.go b/tools/smbexec/main.go index 39686fa..f7ea088 100644 --- a/tools/smbexec/main.go +++ b/tools/smbexec/main.go @@ -36,6 +36,7 @@ import ( "gopacket/pkg/flags" "gopacket/pkg/session" "gopacket/pkg/smb" + "gopacket/pkg/transport" ) var ( @@ -446,8 +447,15 @@ func (e *SMBExec) finish() { } } -// getLocalIP determines the local IP address used to reach the target host +// getLocalIP determines the local IP address used to reach the target host. +// Under -proxy the concept doesn't apply (traffic flows via the proxy, not +// directly from this host), so we return "" and let the caller fall back. +// The UDP socket here doesn't actually send packets — it just asks the kernel +// which source address would be picked for a hypothetical connection. func getLocalIP(targetHost string) string { + if transport.IsProxyConfigured() { + return "" + } conn, err := net.Dial("udp", targetHost+":445") if err != nil { return "" diff --git a/tools/smbserver/main.go b/tools/smbserver/main.go index 7cd2844..7eada86 100644 --- a/tools/smbserver/main.go +++ b/tools/smbserver/main.go @@ -366,7 +366,7 @@ func (s *Server) GetShares() []*Share { func main() { flag.Usage = func() { - fmt.Fprintf(os.Stderr, "gopacket v0.1.0-beta - Copyright 2026 Google LLC\n\n") + fmt.Fprintf(os.Stderr, "gopacket v0.1.1-beta - Copyright 2026 Google LLC\n\n") fmt.Fprintf(os.Stderr, "usage: smbserver [-h] [-comment COMMENT] [-username USERNAME]\n") fmt.Fprintf(os.Stderr, " [-password PASSWORD] [-hashes LMHASH:NTHASH] [-ts]\n") fmt.Fprintf(os.Stderr, " [-debug] [-ip INTERFACE_ADDRESS] [-port PORT]\n") diff --git a/tools/sniff/main.go b/tools/sniff/main.go index 81b1909..b6bfb77 100644 --- a/tools/sniff/main.go +++ b/tools/sniff/main.go @@ -41,7 +41,7 @@ var ( func main() { flag.Usage = func() { - fmt.Fprintf(os.Stderr, `gopacket v0.1.0-beta - Copyright 2026 Google LLC + fmt.Fprintf(os.Stderr, `gopacket v0.1.1-beta - Copyright 2026 Google LLC Simple packet sniffer using pcap. @@ -63,7 +63,7 @@ Note: Requires root/CAP_NET_RAW privileges. flag.Parse() - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() // List interfaces mode diff --git a/tools/sniffer/main.go b/tools/sniffer/main.go index f0c0737..77bfe50 100644 --- a/tools/sniffer/main.go +++ b/tools/sniffer/main.go @@ -38,7 +38,7 @@ var protoMap = map[string]int{ } func main() { - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() // Default protocols if none specified diff --git a/tools/split/main.go b/tools/split/main.go index ed09ab2..1980828 100644 --- a/tools/split/main.go +++ b/tools/split/main.go @@ -59,7 +59,7 @@ type ConnectionWriter struct { } func main() { - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() fmt.Println("[!] This tool is deprecated and may be removed in future versions.") fmt.Println() diff --git a/tools/ticketer/main.go b/tools/ticketer/main.go index ba945aa..70d6c53 100644 --- a/tools/ticketer/main.go +++ b/tools/ticketer/main.go @@ -446,7 +446,7 @@ func parseExtraSIDs() []string { } func usage() { - fmt.Fprintf(os.Stderr, "gopacket v0.1.0-beta - Copyright 2026 Google LLC\n\n") + fmt.Fprintf(os.Stderr, "gopacket v0.1.1-beta - Copyright 2026 Google LLC\n\n") fmt.Fprintf(os.Stderr, "Creates Kerberos golden/silver/sapphire tickets based on user options\n\n") fmt.Fprintf(os.Stderr, "Usage: ticketer [options] \n\n") fmt.Fprintf(os.Stderr, "Positional:\n") diff --git a/tools/tstool/main.go b/tools/tstool/main.go index c899575..58a3a0e 100644 --- a/tools/tstool/main.go +++ b/tools/tstool/main.go @@ -108,7 +108,7 @@ func main() { } } - fmt.Println("gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Println("gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Println() // Connect via SMB @@ -809,7 +809,7 @@ func cmdMsg(smbClient *smb.Client, auth *authContext, sessionID int, title, mess } func printUsage() { - fmt.Fprintln(os.Stderr, "gopacket v0.1.0-beta - Copyright 2026 Google LLC") + fmt.Fprintln(os.Stderr, "gopacket v0.1.1-beta - Copyright 2026 Google LLC") fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr, "Usage: tstool [auth-flags] target [action-flags]") fmt.Fprintln(os.Stderr) diff --git a/tools/wmipersist/main.go b/tools/wmipersist/main.go index 5fcd41b..e37b350 100644 --- a/tools/wmipersist/main.go +++ b/tools/wmipersist/main.go @@ -59,7 +59,7 @@ var ( ) func printUsage() { - fmt.Fprintf(os.Stderr, `gopacket v0.1.0-beta - Copyright 2026 Google LLC + fmt.Fprintf(os.Stderr, `gopacket v0.1.1-beta - Copyright 2026 Google LLC usage: wmipersist [-h] [-debug] [-ts] [-com-version MAJOR_VERSION:MINOR_VERSION] [-hashes LMHASH:NTHASH] [-no-pass] [-k] [-aesKey hex key]