mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-12 05:11:35 +01:00
Add `relay_proxy_protocol_header` configuration option for TCP routes that enables forwarding the original client IP address to upstream services via PROXY protocol v2 headers. This feature is only available for TCP routes and includes validation to prevent misuse on UDP routes. - Add RelayProxyProtocolHeader field to Route struct with JSON tag - Implement writeProxyProtocolHeader in stream package to craft v2 headers - Update TCPTCPStream to conditionally send PROXY header to upstream - Add validation ensuring feature is TCP-only - Include tests for both enabled/disabled states and incoming proxy header relay
internal/route/stream
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
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
// 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
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
classDiagram
class Stream {
<<interface>>
+ListenAndServe(ctx, preDial, onRead)
+Close() error
+LocalAddr() net.Addr
}
class TCPTCPStream {
+listener net.Listener
+laddr *net.TCPAddr
+dst *net.TCPAddr
+ListenAndServe(ctx, preDial, onRead)
+pipe(conn1, conn2)
}
class UDPUDPStream {
+listener net.PacketConn
+laddr *net.TCPAddr
+dst *net.TCPAddr
+conns map
+ListenAndServe(ctx, preDial, onRead)
+handleUDP()
}
class udpUDPConn {
+srcAddr *net.UDPAddr
+dstConn *net.UDPConn
+lastUsed atomic.Time
+close()
}
Stream <|-- TCPTCPStream
Stream <|-- UDPUDPStream
UDPUDPStream --> udpUDPConn : manages
TCP Stream Flow
sequenceDiagram
participant C as Client
participant L as Listener
participant P as Pipe
participant S as Server
C->>L: TCP Connection
L->>P: Accept connection
P->>S: Dial TCP
S-->>P: Connection established
loop Data Transfer
C->>P: Data
P->>S: Forward data
S->>P: Response
P->>C: Forward response
end
UDP Stream Flow
sequenceDiagram
participant C as Client
participant L as Listener
participant M as Connection Manager
participant S as Server
C->>L: UDP Datagram
L->>M: Get/Create connection
alt New Connection
M->>S: Dial UDP
S-->>M: Connection ready
M->>C: Forward initial packet
else Existing Connection
M->>C: Forward packet
end
loop Response Handler
S->>M: Response
M->>C: Forward response
end
Constants
const (
udpBufferSize = 16 * 1024 // 16KB buffer
udpIdleTimeout = 5 * time.Minute
udpCleanupInterval = 1 * time.Minute
udpReadTimeout = 30 * time.Second
)
Configuration Surface
Route Configuration
routes:
ssh-proxy:
scheme: tcp4
bind: 0.0.0.0 # optional
port: 2222:22 # listening port: target port
relay_proxy_protocol_header: true # optional, tcp only
dns-proxy:
scheme: udp4
bind: 0.0.0.0 # optional
port: 53:53 # listening port: target port
Docker Labels
services:
ssh:
image: alpine/ssh
labels:
proxy.aliases: ssh
proxy.ssh.port: 2222:22 # listening port: target port
Dependency and Integration Map
| Dependency | Purpose |
|---|---|
internal/acl |
Access control for listeners |
internal/entrypoint |
Proxy protocol support |
internal/net/types |
Stream interface definitions |
github.com/pires/go-proxyproto |
PROXY protocol header |
github.com/yusing/goutils/errs |
Error 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
- TCP routes can optionally emit a fresh upstream PROXY v2 header with
relay_proxy_protocol_header: true - No protocol validation (relies on upstream)
- Connection limits managed by OS
Failure Modes and Recovery
| Failure | Behavior | Recovery |
|---|---|---|
| Bind fails | Stream creation error | Check port availability |
| Dial fails | Connection error | Fix target address |
| Pipe broken | Connection closed | Client reconnects |
| UDP idle timeout | Connection removed | Client reconnects |
Usage Examples
Creating a TCP Stream Route
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
tcpStream, err := stream.NewTCPTCPStream("tcp", ":8080", "localhost:22")
if err != nil {
return err
}
tcpStream.ListenAndServe(ctx, preDialHook, onReadHook)
Using Hook Functions
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
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->tcpudp4/udp6->udp
- No UDP broadcast/multicast support