udpbara

package module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 21, 2026 License: MIT Imports: 11 Imported by: 1

README

udpbara

udpbara

Userspace UDP relay for QUIC/HTTP3 through SOCKS5 proxies
Cuz UDP deserves to chill in tunnel too.

WhyHow It WorksInstallUsageAPI


Why

So here's the deal, QUIC and HTTP/3 run over UDP. SOCKS5 proxies technically support UDP through this thing called UDP ASSOCIATE. In theory, tunneling QUIC through SOCKS5 should be straightforward. In practice? Literally nobody does this. And honestly, fair enough:

  • quic-go is picky fr: it doesn't just want any net.PacketConn. It needs a real *net.UDPConn because it's doing OOB messages, ECN bits, platform-specific socket ops, basically the whole deal. Hand it a fake conn and watch things break in the most confusing ways possible.
  • SOCKS5 UDP ASSOCIATE is unhinged: it binds some random relay address, wraps every single packet with its own headers, and straight up kills your session if the TCP control connection drops. Most SOCKS5 libraries looked at this part of the spec and said "nah".
  • The hostname-vs-IP nightmare: residential/ISP proxies auth based on the hostname in your SOCKS5 request. Resolve DNS locally and send raw IPs? Auth fails silently. Send hostnames? Cool, but now the relay responds with IPs and your dispatcher has zero idea which connection those packets belong to.

udpbara deals with all of this. It spins up a real local UDP socket pair per connection and quic-go gets its beloved *net.UDPConn with full OOB/ECN support, while udpbara handles the SOCKS5 wrapping and dispatch behind the scenes.

No TUN interfaces. No root. No iptables. No kernel modules. Just pure userspace Go doing its thing.

How It Works

┌─────────────┐         ┌──────────────────────┐         ┌──────────────┐
│   quic-go   │  UDP    │       udpbara        │  UDP    │  SOCKS5      │
│             │◄───────►│                      │◄───────►│  Proxy       │
│  (real UDP  │  local  │  appConn ↔ relayConn │  inet   │  (UDP relay) │
│   socket)   │  pair   │  + SOCKS5 wrapping   │         │              │
└─────────────┘         └──────────────────────┘         └──────────────┘
                              │                                │
                              │ TCP (control)                  │
                              └────────────────────────────────┘
  1. TCP Handshake — udpbara connects to the SOCKS5 proxy over TCP, authenticates (username/password), and sends UDP ASSOCIATE to get a relay address.

  2. Local Socket Pair — For each target, it creates two *net.UDPConn sockets on localhost:

    • appConn — handed to quic-go (or whatever UDP client you're using)
    • relayConn — internal side that wraps/unwraps SOCKS5 UDP headers
  3. Outbound (app → proxy) — Packets from appConn hit relayConn, get wrapped with the SOCKS5 UDP header (preserving the hostname for proxy auth), and are sent to the proxy's relay address.

  4. Inbound (proxy → app) — Packets from the proxy are read on the tunnel's shared UDP socket, stripped of the SOCKS5 header, and dispatched to the correct relayConnappConn based on source address. Dual-key lookup (hostname + resolved IPs) with fallback broadcast handles the hostname-vs-IP mismatch.

  5. Lifecycle — The TCP control connection is monitored. If it drops, udpbara auto-reconnects with exponential backoff (up to 5 attempts).

Install

go get github.com/sardanioss/udpbara

Usage

Quick — One-Shot Dial

import "github.com/sardanioss/udpbara"

// Creates tunnel + connection in one call
conn, err := udpbara.Dial("socks5h://user:pass@proxy:10000", "target.com:443")
if err != nil {
    log.Fatal(err)
}
defer conn.Close() // cleans up tunnel too

// Use with quic-go
transport := &quic.Transport{
    Conn: conn.PacketConn(), // real *net.UDPConn
}
quicConn, err := transport.Dial(ctx, conn.RelayAddr(), tlsConfig, quicConfig)

Tunnel — Multiple Targets, One Proxy

// Create and connect tunnel
tunnel, err := udpbara.NewTunnel("socks5h://user:pass@proxy:10000")
if err != nil {
    log.Fatal(err)
}
if err := tunnel.Connect(); err != nil {
    log.Fatal(err)
}
defer tunnel.Close()

// Dial multiple targets through the same proxy session
conn1, _ := tunnel.Dial("api.example.com:443")
conn2, _ := tunnel.Dial("cdn.example.com:443")
defer conn1.Close()
defer conn2.Close()

// Each gets its own *net.UDPConn
fmt.Println(conn1.PacketConn().LocalAddr()) // 127.0.0.1:xxxxx
fmt.Println(conn2.PacketConn().LocalAddr()) // 127.0.0.1:yyyyy

Manager — Multiple Proxies

mgr := udpbara.NewManager()

// Add tunnels for different proxies
mgr.AddTunnel("us-east", "socks5h://user:pass@us-east-proxy:10000")
mgr.AddTunnel("eu-west", "socks5h://user:pass@eu-west-proxy:10000")

// Dial through specific tunnels
conn, _ := mgr.Dial("us-east", "", "target.com:443")
defer conn.Close()

// Cleanup
mgr.CloseAll()

API

Top-Level

Function Description
Dial(proxyURL, target, ...Config) One-shot: creates tunnel + connection, caller owns both
NewTunnel(proxyURL, ...Config) Creates a reusable tunnel (call Connect() next)
NewManager(...Config) Creates a multi-tunnel manager

Tunnel

Method Description
Connect() Establishes SOCKS5 UDP ASSOCIATE session
Dial(target) Creates a new connection to host:port through this tunnel
Stats() Returns packet/byte counters
Close() Shuts down tunnel and all connections

Connection

Method Description
PacketConn() Returns *net.UDPConn for quic-go
RelayAddr() Returns *net.UDPAddr to pass to quic.Transport.Dial()
Tunnel() Returns the parent *Tunnel
Close() Closes this connection (and tunnel if created via top-level Dial)

Config

Field Default Description
ReadBufferSize 7 MB UDP socket read buffer
WriteBufferSize 7 MB UDP socket write buffer
TCPKeepAlive true Keepalive on SOCKS5 control connection
TCPKeepAlivePeriod 30s Keepalive probe interval
ConnectTimeout 10s Proxy connection timeout
AutoReconnect true Auto-reconnect on control drop

License

MIT

Documentation

Overview

Package udpbara provides userspace UDP relay through SOCKS5 proxies.

It enables QUIC/HTTP3 and other UDP protocols to work through SOCKS5 proxies that require hostname-based authentication, without requiring root access, TUN interfaces, or system-wide routing changes.

Each tunnel maintains a single SOCKS5 UDP ASSOCIATE session and can multiplex multiple target destinations through the same proxy connection. Connections expose a real *net.UDPConn for full compatibility with quic-go (OOB/ECN support).

Basic usage:

conn, err := udpbara.Dial("socks5h://user:pass@proxy:10000", "target.com:443")
if err != nil { log.Fatal(err) }
defer conn.Close()

transport := &quic.Transport{Conn: conn.PacketConn()}
quicConn, err := transport.Dial(ctx, conn.RelayAddr(), tlsConfig, quicConfig)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ParseProxyURL

func ParseProxyURL(proxyURL string) (addr, user, pass string, err error)

ParseProxyURL parses a SOCKS5 proxy URL and returns its components. Accepted formats:

socks5://user:pass@host:port
socks5h://user:pass@host:port
socks5://host:port  (no auth)

Both socks5 and socks5h schemes are accepted (udpbara always preserves hostnames in SOCKS5 UDP headers regardless of scheme).

Types

type Config

type Config struct {
	// ReadBufferSize is the UDP socket read buffer size in bytes.
	// Default: 7MB (recommended for QUIC).
	ReadBufferSize int

	// WriteBufferSize is the UDP socket write buffer size in bytes.
	// Default: 7MB (recommended for QUIC).
	WriteBufferSize int

	// TCPKeepAlive enables TCP keepalive on the SOCKS5 control connection.
	// Default: true.
	TCPKeepAlive bool

	// TCPKeepAlivePeriod is the interval between TCP keepalive probes in seconds.
	// Default: 30.
	TCPKeepAlivePeriod int

	// ConnectTimeout is the timeout for connecting to the SOCKS5 proxy in seconds.
	// Default: 10.
	ConnectTimeout int

	// AutoReconnect enables automatic reconnection when the TCP control drops.
	// Default: true.
	AutoReconnect bool

	// Logger is an optional logger for tunnel events.
	// If nil, no logging is performed. Default: nil.
	Logger Logger
}

Config holds configuration for a tunnel. All fields have sensible defaults via DefaultConfig(). Zero-value fields will not override defaults when passed to NewTunnel or Dial — use DefaultConfig() and modify specific fields instead.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a Config with sensible defaults.

type Connection

type Connection struct {
	// contains filtered or unexported fields
}

Connection holds the resources for a single target connection through a tunnel. Use PacketConn() to get the *net.UDPConn for quic-go, and RelayAddr() for the address to pass to quic.Transport.Dial().

func Dial

func Dial(proxyURL, target string, config ...Config) (*Connection, error)

Dial is a convenience function that creates a tunnel and dials a target in one call. Returns a *Connection with a real *net.UDPConn for quic-go compatibility. The caller must close the returned Connection when done.

Example:

conn, err := udpbara.Dial("socks5h://user:pass@proxy.com:10000", "target.com:443")
if err != nil { ... }
defer conn.Close()

func DialContext

func DialContext(ctx context.Context, proxyURL, target string, config ...Config) (*Connection, error)

DialContext is a convenience function like Dial but respects context cancellation. The context is used for both the proxy connection and target DNS resolution.

Example:

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
conn, err := udpbara.DialContext(ctx, "socks5h://user:pass@proxy:10000", "target.com:443")

func (*Connection) Close

func (c *Connection) Close() error

Close shuts down this connection. If this Connection was created by the top-level Dial() function, it also closes the underlying tunnel.

func (*Connection) PacketConn

func (c *Connection) PacketConn() *net.UDPConn

PacketConn returns the real *net.UDPConn for use with quic-go.

func (*Connection) RelayAddr

func (c *Connection) RelayAddr() *net.UDPAddr

RelayAddr returns the address that QUIC should dial to.

func (*Connection) Stats

func (c *Connection) Stats() Stats

Stats returns per-connection packet and byte counters.

func (*Connection) Target

func (c *Connection) Target() string

Target returns the target address this connection was dialed to (e.g., "example.com:443").

func (*Connection) Tunnel

func (c *Connection) Tunnel() *Tunnel

Tunnel returns the underlying Tunnel this connection belongs to. Useful for accessing tunnel-level Stats() on connections created via the top-level Dial().

type Logger

type Logger interface {
	// Debug logs a debug-level message (packet dispatch, connection registration).
	Debug(msg string, args ...any)
	// Info logs an info-level message (connect, disconnect, reconnect).
	Info(msg string, args ...any)
	// Error logs an error-level message (connection failures, protocol errors).
	Error(msg string, args ...any)
}

Logger is an optional logging interface for tunnel events. Implement this to integrate with your application's logging framework.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager manages multiple named tunnels through different SOCKS5 proxies. It provides a higher-level API for applications that need to maintain connections through multiple proxy endpoints simultaneously. All methods are safe for concurrent use.

func NewManager

func NewManager(config ...Config) *Manager

NewManager creates a new tunnel manager with optional configuration. If no config is provided, DefaultConfig() is used for all tunnels.

func (*Manager) AddTunnel

func (m *Manager) AddTunnel(name, proxyURL string) (*Tunnel, error)

AddTunnel creates and connects a named tunnel through a SOCKS5 proxy. Returns an error if a tunnel with the same name already exists.

func (*Manager) AddTunnelContext

func (m *Manager) AddTunnelContext(ctx context.Context, name, proxyURL string) (*Tunnel, error)

AddTunnelContext is like AddTunnel but respects context cancellation.

func (*Manager) CloseAll

func (m *Manager) CloseAll()

CloseAll stops and removes all tunnels managed by this Manager. All connections through all tunnels are closed.

func (*Manager) Dial

func (m *Manager) Dial(name, proxyURL, target string) (*Connection, error)

Dial creates a connection through a named tunnel to a target. If the tunnel doesn't exist, it creates and connects one using the given proxyURL. If the tunnel already exists, proxyURL is ignored.

func (*Manager) DialContext

func (m *Manager) DialContext(ctx context.Context, name, proxyURL, target string) (*Connection, error)

DialContext is like Dial but respects context cancellation.

func (*Manager) GetTunnel

func (m *Manager) GetTunnel(name string) (*Tunnel, error)

GetTunnel returns a named tunnel, or an error if not found.

func (*Manager) List

func (m *Manager) List() []string

List returns all active tunnel names.

func (*Manager) RemoveTunnel

func (m *Manager) RemoveTunnel(name string) error

RemoveTunnel stops and removes a named tunnel. All connections through the tunnel are closed. Returns an error if the tunnel is not found.

type Stats

type Stats struct {
	PacketsSent uint64
	PacketsRecv uint64
	BytesSent   uint64
	BytesRecv   uint64
}

Stats contains packet and byte counters. Used for both tunnel-level and connection-level statistics.

type Tunnel

type Tunnel struct {
	// contains filtered or unexported fields
}

Tunnel maintains a SOCKS5 UDP ASSOCIATE session through a single proxy. It can create multiple connections for different targets, all sharing the same proxy connection and UDP relay.

A Tunnel handles the full SOCKS5 lifecycle: TCP control connection, authentication, UDP relay setup, packet dispatch, and automatic reconnection. Use NewTunnel() to create, Connect() to establish, and Dial() to create connections.

func NewTunnel

func NewTunnel(proxyURL string, config ...Config) (*Tunnel, error)

NewTunnel creates a new tunnel through a SOCKS5 proxy. Call Connect() to establish the SOCKS5 session, then Dial() to create connections.

func (*Tunnel) Close

func (t *Tunnel) Close() error

Close shuts down the tunnel and all connections.

func (*Tunnel) Connect

func (t *Tunnel) Connect() error

Connect establishes the SOCKS5 UDP ASSOCIATE session with the proxy. This performs the TCP connection, SOCKS5 handshake, and UDP relay setup. It is safe to call multiple times — subsequent calls are no-ops if already connected. After Connect returns, call Dial() to create connections to targets.

func (*Tunnel) ConnectContext

func (t *Tunnel) ConnectContext(ctx context.Context) error

ConnectContext is like Connect but respects context cancellation and deadlines. Returns context.DeadlineExceeded or context.Canceled if the context expires before the connection is established.

func (*Tunnel) Dial

func (t *Tunnel) Dial(target string) (*Connection, error)

Dial creates a new connection through this tunnel to the specified target. Returns a Connection with a real *net.UDPConn (fully compatible with quic-go).

The target format is "host:port" (e.g., "www.example.com:443"). Hostnames are preserved in the SOCKS5 UDP header for proxy-side DNS resolution and auth. The connection is also registered under resolved IP keys for response dispatch.

Multiple connections to different targets can share the same tunnel.

func (*Tunnel) DialContext

func (t *Tunnel) DialContext(ctx context.Context, target string) (*Connection, error)

DialContext is like Dial but respects context cancellation. The context is used for DNS resolution of the target hostname.

func (*Tunnel) Stats

func (t *Tunnel) Stats() TunnelStats

Stats returns tunnel statistics.

type TunnelStats

type TunnelStats = Stats

TunnelStats is an alias for Stats for backward compatibility.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL