transport: add SOCKS5 proxy support with UDP-under-proxy guard

Splits pkg/transport into a router (tcp.go) plus two platform-specific
direct dialers: direct_libc.go preserves the existing cgo libc connect()
path on Unix (so proxychains can still hook it), and direct_portable.go
provides a pure-Go fallback for CGO_ENABLED=0 and Windows builds.

Adds proxy.go with:
- Configure(Options) / ConfigureProxy to wire a SOCKS5 URL (socks5 or
  socks5h), with ALL_PROXY / all_proxy env fallback read directly via
  os.Getenv (not proxy.FromEnvironment, whose sync.Once cache is not
  test-friendly).
- DialContext routing through the configured proxy, or directDial.
- DialUDP returning ErrUDPUnderProxy when proxied, rather than silently
  leaking UDP packets past the SOCKS5.
- IsProxyConfigured and ProxyURL for callers that need to short-circuit
  features that can't be tunneled.
- libcForwarder so the TCP leg to the SOCKS5 proxy itself still goes
  through libc, enabling proxychains -> gopacket -> -proxy chaining.

Tests:
- export_test.go exposes ResetForTest for test isolation (Configure
  panics on double-call, so each test resets package state).
- socks5_server_test.go hand-rolls a minimal in-process SOCKS5 server
  (CONNECT + no-auth, ~130 LOC, no new deps).
- proxy_test.go covers invalid scheme rejection, socks5/socks5h accept,
  ALL_PROXY env fallback, double-call panic, default state,
  DialUDP/DialContext UDP rejection, credential redaction, direct-path
  behavior when unconfigured, and an end-to-end round-trip through the
  in-process SOCKS5.
This commit is contained in:
psycep
2026-04-22 09:54:09 -05:00
parent 2c01188d2d
commit 90038dcfc2
7 changed files with 774 additions and 99 deletions
+137
View File
@@ -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 <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
// 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)
}
+38
View File
@@ -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)
}
+22
View File
@@ -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)
}
+165
View File
@@ -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)
}
+242
View File
@@ -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)
}
}
+147
View File
@@ -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
}
+23 -99
View File
@@ -14,116 +14,44 @@
package transport
/*
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
// 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
if timeoutSec <= 0 {
timeoutSec = DefaultTimeout
}
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 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)
}
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)
}
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)
}