Skip to content

Stream Handling

Implements TCP and UDP stream proxying for non-HTTP protocols.

Overview

The internal/route/stream package provides protocol-agnostic proxying of TCP and UDP connections. It enables GoDoxy to handle protocols like SSH, DNS, game servers, and other binary protocols that don't use HTTP.

Primary Consumers

  • Route layer: Creates stream routes for TCP/UDP schemes
  • Entry point: Mounts stream listeners
  • ACL system: Applies access control to listeners

Non-goals

  • Does not implement HTTP/1.1 or HTTP/2 (handled by reverse proxy)
  • Does not handle WebSocket (handled by rules engine)
  • Does not provide protocol-specific parsing

Stability

Internal package with stable nettypes.Stream interface.

Public API

Exported Types

go
type TCPTCPStream struct {
    network  string
    listener net.Listener
    laddr    *net.TCPAddr
    dst      *net.TCPAddr
    preDial  nettypes.HookFunc
    onRead   nettypes.HookFunc
    closed   atomic.Bool
}

type UDPUDPStream struct {
    network   string
    listener  net.PacketConn
    laddr     *net.UDPAddr
    dst       *net.TCPAddr
    cleanUpTicker *time.Ticker
    conns     map[string]*udpUDPConn
    closed    atomic.Bool
    mu        sync.Mutex
}

Exported Functions

go
// Create a TCP stream
func NewTCPTCPStream(network, listenAddr, dstAddr string) (nettypes.Stream, error)

// Create a UDP stream
func NewUDPUDPStream(network, listenAddr, dstAddr string) (nettypes.Stream, error)

Stream Interface

go
type Stream interface {
    ListenAndServe(ctx context.Context, preDial, onRead HookFunc) error
    Close() error
    LocalAddr() net.Addr
}

type HookFunc func(ctx context.Context) error

Architecture

Core Components

TCP Stream Flow

UDP Stream Flow

Constants

go
const (
    udpBufferSize      = 16 * 1024  // 16KB buffer
    udpIdleTimeout     = 5 * time.Minute
    udpCleanupInterval = 1 * time.Minute
    udpReadTimeout     = 30 * time.Second
)

Configuration Surface

Route Configuration

yaml
routes:
  ssh-proxy:
    scheme: tcp4
      bind: 0.0.0.0 # optional
      port: 2222:22 # listening port: target port

  dns-proxy:
    scheme: udp4
    bind: 0.0.0.0 # optional
    port: 53:53 # listening port: target port

Docker Labels

yaml
services:
  ssh:
    image: alpine/ssh
    labels:
      proxy.aliases: ssh
      proxy.ssh.port: 2222:22 # listening port: target port

Dependency and Integration Map

DependencyPurpose
internal/aclAccess control for listeners
internal/entrypointProxy protocol support
internal/net/typesStream interface definitions
github.com/pires/go-proxyprotoPROXY protocol header
github.com/yusing/goutils/errsError handling

Observability

Logs

  • INFO: Stream start/stop, connection accepted
  • DEBUG: Data transfer, connection details
  • ERROR: Accept failures, pipe errors

Log context includes: protocol, listen, dst, action

Security Considerations

  • ACL wrapping available for TCP and UDP listeners
  • PROXY protocol support for original client IP
  • No protocol validation (relies on upstream)
  • Connection limits managed by OS

Failure Modes and Recovery

FailureBehaviorRecovery
Bind failsStream creation errorCheck port availability
Dial failsConnection errorFix target address
Pipe brokenConnection closedClient reconnects
UDP idle timeoutConnection removedClient reconnects

Usage Examples

Creating a TCP Stream Route

go
baseRoute := &route.Route{
    LisURL:   &url.URL{Scheme: "tcp4", Host: ":2222"},
    ProxyURL: &url.URL{Scheme: "tcp", Host: "localhost:22"},
}

streamRoute, err := route.NewStreamRoute(baseRoute)
if err != nil {
    return err
}

Programmatic Stream Creation

go
tcpStream, err := stream.NewTCPTCPStream("tcp", ":8080", "localhost:22")
if err != nil {
    return err
}

tcpStream.ListenAndServe(ctx, preDialHook, onReadHook)

Using Hook Functions

go
stream.ListenAndServe(ctx,
    func(ctx context.Context) error {
        // Pre-dial: authentication, rate limiting
        log.Println("Pre-dial check")
        return nil
    },
    func(ctx context.Context) error {
        // On-read: metrics, throttling
        return nil
    },
)

ACL Integration

go
stream := tcpStream
if acl := acl.ActiveConfig.Load(); acl != nil {
    stream.listener = acl.WrapTCP(stream.listener)
    // or for UDP
    stream.listener = acl.WrapUDP(stream.listener)
}

Performance Considerations

  • TCP: Bidirectional pipe with goroutines per connection
  • UDP: 16KB buffer with sized pool
  • Cleanup: Periodic cleanup of idle UDP connections
  • Concurrency: Each connection handled independently

Limitations

  • Load balancing not yet supported
  • Coherent scheme required:
    • tcp4/tcp6 -> tcp
    • udp4/udp6 -> udp
  • No UDP broadcast/multicast support

Released under the MIT License.