refactor(proxy): simplify source address binding to use net.IP instead of strings

This commit is contained in:
Alexandr Stelnykovych
2026-04-24 17:58:21 +03:00
parent 52a3b9256a
commit 29cc58fecb
5 changed files with 30 additions and 33 deletions
+16 -7
View File
@@ -28,13 +28,13 @@ shutdown.
```go
// DeciderFunc is called once per new session to determine the upstream
// destination and an optional local address to bind the outgoing connection to.
// destination and an optional local IP to bind the outgoing connection to.
// local is the proxy's listen address; peer is the connecting client's address.
// Return a non-nil error to reject the session.
type DeciderFunc func(local net.Addr, peer net.Addr) (
remoteIP net.IP,
remotePort uint16,
localAddr string, // "host:port" to pin source address, or "" for OS default
localIP net.IP, // source IP to pin, or nil for OS default
extraInfo any, // optional value attached to the session's ConnContext
err error,
)
@@ -87,6 +87,15 @@ func NewUDPProxyWithConfig(listenAddr string, network string, decider DeciderFun
Both constructors bind the socket and start background goroutines immediately.
They return an error if binding fails or if `decider` is nil.
### Address
```go
func (p *TCPProxy) Addr() net.Addr
func (p *UDPProxy) Addr() net.Addr
```
Returns the address the proxy is currently listening on.
### Configuration
```go
@@ -159,8 +168,8 @@ func (p *UDPProxy) Metrics() Metrics
### Transparent TCP proxy (always route to a fixed backend)
```go
decider := func(local, peer net.Addr) (net.IP, uint16, string, any, error) {
return net.ParseIP("192.168.1.10"), 8080, "", nil, nil
decider := func(local, peer net.Addr) (net.IP, uint16, net.IP, any, error) {
return net.ParseIP("192.168.1.10"), 8080, nil, nil, nil
}
p, err := proxy.NewTCPProxy(":8080", "tcp4", decider, nil)
@@ -181,13 +190,13 @@ p.Shutdown(ctx)
### Per-client routing with source-address binding (split tunnelling)
```go
decider := func(local, peer net.Addr) (net.IP, uint16, string, any, error) {
decider := func(local, peer net.Addr) (net.IP, uint16, net.IP, any, error) {
host, _, _ := net.SplitHostPort(peer.String())
if isTunnelledIP(host) {
// Route through VPN interface, binding source to its local address.
return vpnGatewayIP, 443, "10.0.0.1:0", nil, nil
return vpnGatewayIP, 443, net.ParseIP("10.0.0.1"), nil, nil
}
return directGatewayIP, 443, "", nil, nil
return directGatewayIP, 443, nil, nil, nil
}
p, err := proxy.NewTCPProxy(":443", "tcp4", decider, myLogger)
+2 -2
View File
@@ -20,10 +20,10 @@ import (
// It returns:
// - remoteIP: required upstream IP address
// - remotePort: required upstream port
// - localAddr: optional local "host:port" (empty string = OS chooses source)
// - localIP: optional local IP to use as the source address (nil = OS chooses)
// - extraInfo: optional user-defined object attached to the session context
// - err: non-nil rejects the session
type DeciderFunc func(local net.Addr, peer net.Addr) (remoteIP net.IP, remotePort uint16, localAddr string, extraInfo any, err error)
type DeciderFunc func(local net.Addr, peer net.Addr) (remoteIP net.IP, remotePort uint16, localIP net.IP, extraInfo any, err error)
// Logger is the minimal structured logging interface expected by the proxies.
// Pass nil to disable all logging.
+8 -8
View File
@@ -16,17 +16,17 @@ import (
// passThroughDecider always routes to dest.
func passThroughDecider(dest string) DeciderFunc {
addr, _ := net.ResolveTCPAddr("tcp", dest)
return func(_, _ net.Addr) (net.IP, uint16, string, any, error) {
return func(_, _ net.Addr) (net.IP, uint16, net.IP, any, error) {
if addr == nil {
return nil, 0, "", nil, fmt.Errorf("invalid dest %q", dest)
return nil, 0, nil, nil, fmt.Errorf("invalid dest %q", dest)
}
return addr.IP, uint16(addr.Port), "", nil, nil
return addr.IP, uint16(addr.Port), nil, nil, nil
}
}
// refuseDecider always rejects sessions.
func refuseDecider(_ net.Addr, _ net.Addr) (net.IP, uint16, string, any, error) {
return nil, 0, "", nil, fmt.Errorf("rejected")
func refuseDecider(_ net.Addr, _ net.Addr) (net.IP, uint16, net.IP, any, error) {
return nil, 0, nil, nil, fmt.Errorf("rejected")
}
// startTCPEchoServer starts a TCP echo server on a random port.
@@ -434,12 +434,12 @@ func TestUDPProxy_MaxSessions(t *testing.T) {
// Count how many sessions the decider accepts; reject beyond limit.
var accepted atomic.Int32
const limit = 2
decider := func(local, peer net.Addr) (net.IP, uint16, string, any, error) {
decider := func(local, peer net.Addr) (net.IP, uint16, net.IP, any, error) {
if accepted.Load() >= limit {
return nil, 0, "", nil, fmt.Errorf("max sessions")
return nil, 0, nil, nil, fmt.Errorf("max sessions")
}
accepted.Add(1)
return nil, 0, "", nil, fmt.Errorf("no upstream needed for this test")
return nil, 0, nil, nil, fmt.Errorf("no upstream needed for this test")
}
cfg := DefaultConfig()
+2 -7
View File
@@ -216,13 +216,8 @@ func (p *TCPProxy) handleConn(clientConn net.Conn) {
// DialContext is cancelled immediately if the proxy is shut down.
dialer := net.Dialer{Timeout: p.cfg.DialTimeout}
if localAddr != "" {
tcpAddr, resolveErr := net.ResolveTCPAddr(p.network, localAddr)
if resolveErr != nil {
p.log.Errorf("tcp proxy: resolve local addr %q: %v", localAddr, resolveErr)
return
}
dialer.LocalAddr = tcpAddr
if localAddr != nil {
dialer.LocalAddr = &net.TCPAddr{IP: localAddr}
}
upstreamConn, err := dialer.DialContext(p.shutdownCtx, p.network, destAddr)
if err != nil {
+2 -9
View File
@@ -235,15 +235,8 @@ func (p *UDPProxy) handlePacket(clientAddr *net.UDPAddr, data []byte) {
remoteAddr := &net.UDPAddr{IP: destIP, Port: int(destPort)}
var localUDPAddr *net.UDPAddr
if localAddr != "" {
var resolveErr error
localUDPAddr, resolveErr = net.ResolveUDPAddr("udp", localAddr)
if resolveErr != nil {
p.cache.remove(connCtx)
cancel()
p.log.Errorf("udp proxy: resolve local addr %q: %v", localAddr, resolveErr)
return
}
if localAddr != nil {
localUDPAddr = &net.UDPAddr{IP: localAddr}
}
remoteConn, err := net.DialUDP("udp", localUDPAddr, remoteAddr)
if err != nil {