Files

40 KiB
Raw Permalink Blame History

Multiprotocol UDP Transport Layer

Functional & Technical Specification v1.0

1. Purpose

This project is a Linux userspace client/server system for transporting arbitrary UDP datagrams through networks where direct UDP connectivity may be restricted.

Its primary initial use case is carrying WireGuard traffic, but the software MUST remain completely protocol-agnostic.

The system MUST NOT:

  • configure WireGuard;
  • inspect WireGuard packets;
  • depend on WireGuard libraries;
  • manipulate WireGuard interfaces;
  • assume anything about the encapsulated UDP protocol.

The fundamental abstraction is:

Transport opaque UDP datagrams between two endpoints using the least complex viable carrier.

Typical WireGuard deployment:

WireGuard client
      │
      │ UDP
      ▼
127.0.0.1:51821
      │
      ▼
Transport client
      │
      │ selected carrier
      ▼
Internet
      │
      ▼
Transport server
      │
      │ UDP
      ▼
127.0.0.1:51820
      │
      ▼
WireGuard server

The exact same setup MUST be usable with any other UDP application.


2. Design Principles

The project SHALL follow these principles:

  1. Protocol independence.
  2. Minimal packet transformation.
  3. Datagram semantics preserved wherever possible.
  4. No custom cryptography.
  5. Pluggable transports.
  6. Automatic fallback from simpler transports to more compatible ones.
  7. Low-overhead direct mode.
  8. Multi-client server from the first release.
  9. Unprivileged execution wherever possible.
  10. Human-readable TOML configuration.
  11. Sensible defaults with optional advanced tuning.
  12. Explicit observability and diagnostics.
  13. Architecture prepared for multiple client tunnels in a future release.
  14. Security-sensitive parsing kept small and deterministic.
  15. Transport-specific behavior MUST NOT leak unnecessarily into the core.

3. Initial Platform Scope

Initial supported operating system:

Linux

The project SHALL run entirely in userspace.

Root privileges SHALL NOT be required during normal operation.

Binding privileged ports such as 443 MAY be accomplished using:

CAP_NET_BIND_SERVICE

or an external supervisor/socket mechanism.

The daemon SHALL NOT require:

CAP_NET_ADMIN
CAP_SYS_ADMIN

for normal operation.


4. Operating Roles

The project has two runtime roles:

client
server

They SHOULD initially be exposed through one executable:

project client --config client.toml
project server --config server.toml

The final project name may replace project.


5. Client Data Path

The client listens on a configured local UDP address.

Default example:

127.0.0.1:51821

An arbitrary local application sends UDP datagrams to this socket.

The client:

  1. receives the datagram;
  2. associates it with the configured tunnel;
  3. passes it unchanged to the active transport;
  4. transports it to the server;
  5. receives return datagrams;
  6. sends them to the originating local UDP peer.

The application payload MUST remain opaque.


6. Local Client Peer Handling

Although v1 represents one configured tunnel, the local UDP listener MUST track the local source peer.

For example:

127.0.0.1:49218 -> 127.0.0.1:51821

The most recently active local peer becomes the return destination.

For v1, one active local peer per tunnel is sufficient.

If packets arrive concurrently from a different local peer, the implementation SHOULD:

  1. update the active peer;
  2. emit a warning/debug event;
  3. continue operation.

This avoids coupling the software to a fixed local source port.

Future multi-flow support MAY replace this behavior.


7. Server Data Path

The server MUST support multiple clients concurrently.

Every authenticated client transport session maps to its own backend UDP socket.

Example:

Client A
   │
   ▼
Session A
   │
UDP socket :41001
   │
   ├─────────────► backend :51820

Client B
   │
   ▼
Session B
   │
UDP socket :41002
   │
   ├─────────────► backend :51820

Client C
   │
   ▼
Session C
   │
UDP socket :41003
   │
   └─────────────► backend :51820

Each backend socket provides natural response demultiplexing.

The server MUST NOT inspect the backend protocol in order to determine response ownership.


8. Backend Target

The server configuration defines one UDP backend destination for v1.

Example:

[backend]
target = "127.0.0.1:51820"

Every authenticated client receives an independent UDP socket connected to this backend.

Future versions MAY support client-specific backend targets.

That feature is explicitly out of scope for v1.


9. Transport Architecture

The core operates on logical datagrams.

A transport session SHALL conceptually expose:

pub trait TransportSession {
    async fn send_datagram(&self, data: Bytes) -> Result<()>;
    async fn recv_datagram(&self) -> Result<Bytes>;
    async fn close(&self) -> Result<()>;
}

Connection establishment SHALL be separated from established-session behavior.

Conceptually:

pub trait TransportConnector {
    async fn connect(
        &self,
        endpoint: &ServerEndpoint,
    ) -> Result<Box<dyn TransportSession>>;
}

The final implementation MAY use associated types, enums, channels, or trait objects depending on ergonomics and performance.

The architectural boundary is more important than the exact Rust syntax.


10. Supported Transports

The planned v1 transport order is:

1. direct UDP
2. QUIC DATAGRAM
3. HTTP/3 CONNECT-UDP
4. TLS/TCP framed fallback

All four SHALL share the same logical session API.


11. Direct UDP Transport

Direct UDP is the lowest-overhead transport.

It provides:

  • native datagram behavior;
  • minimal encapsulation;
  • easiest debugging;
  • lowest latency;
  • baseline performance measurements.

It also provides the least firewall traversal capability.

Direct UDP MUST therefore have its own lightweight session and authentication protocol.


12. QUIC Transport

QUIC SHALL be the primary encapsulated datagram transport.

Application packets SHALL use QUIC DATAGRAM support rather than QUIC streams.

Desired semantics:

application datagram
        │
        ▼
QUIC DATAGRAM
        │
        ▼
QUIC
        │
        ▼
UDP

QUIC provides:

  • TLS 1.3;
  • server authentication;
  • encrypted transport metadata;
  • connection identity;
  • connection state;
  • congestion control;
  • transport-level integrity;
  • migration primitives;
  • keepalive support.

The QUIC connection itself constitutes the transport session.

No redundant project session identifier SHALL be placed in every QUIC data packet.


13. HTTP/3 Transport

HTTP/3 SHALL use legitimate HTTP/3 semantics.

Preferred architecture:

UDP payload
    │
    ▼
CONNECT-UDP
    │
    ▼
HTTP Datagram
    │
    ▼
HTTP/3
    │
    ▼
QUIC

The implementation SHOULD follow the standardized MASQUE CONNECT-UDP architecture rather than inventing an HTTP-shaped custom transport.

The purpose of this transport is firewall compatibility, not sophisticated fingerprint impersonation.


14. TLS/TCP Fallback

Some restrictive networks may block UDP completely.

The final fallback SHALL therefore operate over TLS/TCP.

Datagrams MUST be explicitly framed because TCP has no message boundaries.

Frame format:

+----------------+----------------------+
| payload length | UDP payload          |
| u16 BE         | variable             |
+----------------+----------------------+

Maximum payload:

65535 bytes

In practice the project SHALL enforce a much lower configurable transport maximum.

The fallback MUST preserve original datagram boundaries after reassembly.

TCP mode SHALL be considered a compatibility mechanism rather than a performance-optimal transport because head-of-line blocking is unavoidable.


15. Automatic Transport Selection

Default client configuration:

[transport]
mode = "auto"

Automatic mode SHALL attempt transports in the following order:

direct_udp
    ↓ failure
quic
    ↓ failure
http3
    ↓ failure
tls_tcp

The first working transport is selected.

The selected transport remains active until:

  • the connection fails;
  • the daemon is restarted;
  • the user explicitly requests reconnection;
  • configuration is reloaded requiring reconnection.

After failure, the client SHALL begin transport probing again.


16. Probe Behavior

Default values:

[transport.auto]
probe_timeout_ms = 1500
retries = 2

A transport is considered unavailable if connection establishment or authentication fails within the configured limit.

Authentication rejection MUST be distinguished from connectivity failure.

A rejected credential MUST NOT cause automatic fallback through every transport indefinitely.

Instead:

authentication rejected
        ↓
stop probing
        ↓
report configuration/authentication error

This prevents invalid credentials from producing unnecessary connection traffic.


17. Transport Preference Override

Users MAY explicitly select a transport:

[transport]
mode = "quic"

Valid initial values:

auto
direct_udp
quic
http3
tls_tcp

When a specific transport is configured, automatic fallback SHALL be disabled unless separately enabled in a future version.


18. Direct UDP Protocol

Direct UDP requires a small project-specific wire protocol.

Protocol version:

1

All integer fields SHALL use network byte order.

All packet parsers MUST reject malformed or truncated messages before allocating large buffers.


19. Direct UDP Packet Types

Initial packet types:

0x01 HELLO
0x02 ACCEPT
0x03 REJECT

0x10 DATA
0x11 KEEPALIVE
0x12 CLOSE

Unknown packet types MUST be ignored or rejected according to protocol context.

They MUST NOT cause daemon termination.


20. Direct UDP Common Header

All direct transport messages use:

+---------+---------+---------+---------+
| version | type    | flags   | hdr_len |
| u8      | u8      | u8      | u8      |
+---------+---------+---------+---------+
|                                   |
| session_id                        |
| u64                              |
|                                   |
+-----------------------------------+

Total common header:

12 bytes

Fields:

version
    protocol version

type
    packet type

flags
    type-specific flags

hdr_len
    complete header length in bytes

session_id
    zero during initial HELLO
    assigned by server after authentication

A 64-bit session identifier is selected for v1.

It is sufficiently large when generated randomly and checked for collision.


21. Session Identifier Generation

Session IDs MUST:

  • be generated by a cryptographically secure random number generator;
  • never use sequential counters;
  • never be predictable;
  • never use zero;
  • be collision-checked against active sessions.

If a collision occurs, the server generates another identifier.

Session IDs are identifiers, not secrets.

Authentication MUST NOT rely solely on knowledge of the session ID.


22. Client Identity

Each configured client has:

client_id
shared_secret

Example server configuration:

[[auth.clients]]
id = "laptop"
secret = "base64-or-hex-secret"

[[auth.clients]]
id = "server-a"
secret = "base64-or-hex-secret"

Client IDs are not confidential.

Secrets MUST have at least 256 bits of entropy.

A project CLI SHOULD eventually provide:

project generate-secret

23. Direct UDP HELLO

The HELLO packet contains:

common header
client_id_length      u8
client_id             bytes
timestamp             u64
client_nonce          16 bytes
authenticator         32 bytes

session_id SHALL be zero.

The authenticator is:

HMAC-SHA256(
    client_secret,
    protocol_version
    || client_id
    || timestamp
    || client_nonce
)

The exact serialization used for the HMAC input MUST be canonical and documented in the protocol implementation.


24. Replay Protection

The server MUST protect HELLO authentication against replay.

A HELLO is accepted only if:

abs(server_time - client_timestamp) <= allowed_clock_skew

Default:

60 seconds

The server SHALL also retain recently accepted client nonces for the replay window.

A repeated:

client_id + client_nonce

combination MUST be rejected.

Nonce replay state MAY expire after:

2 × allowed_clock_skew

25. Direct UDP ACCEPT

After successful authentication, the server creates a session and responds:

type          ACCEPT
session_id    generated u64

server_nonce  16 bytes
idle_timeout  u32 seconds
max_datagram  u16
authenticator 32 bytes

The server authenticator SHALL be:

HMAC-SHA256(
    client_secret,
    session_id
    || client_nonce
    || server_nonce
    || idle_timeout
    || max_datagram
)

This proves that the ACCEPT originated from a party possessing the configured secret.


26. Direct UDP REJECT

REJECT SHALL contain a small reason code.

Example codes:

0x01 unsupported_version
0x02 unknown_client
0x03 authentication_failed
0x04 replay_detected
0x05 server_busy
0x06 rate_limited

Detailed authentication information MUST NOT be returned.

For example, an unknown client and an invalid HMAC MAY both be externally represented as:

authentication_failed

if reducing information leakage is preferred.


27. Direct DATA Packets

DATA packets SHALL use:

common header
sequence      u32
auth_tag      16 bytes
payload       remaining bytes

Sequence numbers are included.

They provide:

  • replay protection;
  • diagnostics;
  • loss measurement;
  • duplicate detection;
  • future protocol extensibility.

They MUST NOT be used to reorder packets.

The transport preserves the natural UDP arrival order.


28. Direct Session Authentication

Direct UDP DATA packets require integrity/authentication.

A per-session key SHALL be derived after the handshake.

Conceptually:

session_key = HKDF-SHA256(
    client_secret,
    client_nonce || server_nonce || session_id
)

The implementation SHALL use a well-established Rust cryptographic library.

The project SHALL NOT implement HMAC, HKDF, or MAC primitives manually.


29. DATA Authentication Tag

Each DATA packet SHALL carry a truncated authentication tag.

Recommended:

HMAC-SHA256 truncated to 128 bits

The MAC covers:

version
type
flags
session_id
sequence
payload

A 128-bit tag provides ample integrity protection while keeping packet overhead manageable.

Unauthenticated DATA packets MUST be discarded before forwarding.


30. Direct DATA Overhead

Approximate direct data overhead:

common header     12 bytes
sequence           4 bytes
auth tag          16 bytes
--------------------------
total             32 bytes

The design favors simple, robust parsing over saving a handful of bytes.

Future protocol versions MAY reduce overhead if profiling shows a meaningful benefit.


31. Sequence Numbers

Each direction SHALL maintain an independent 32-bit sequence counter.

Example:

client -> server counter
server -> client counter

Initial value SHALL be random.

Sequence numbers increment modulo 2^32.

Replay protection SHALL use a sliding receive window.

Recommended window:

1024 packets

Packets may arrive out of order within this window.

Already-seen sequence numbers MUST be dropped.

Packets older than the replay window MUST be dropped.


32. NAT Rebinding

Direct UDP sessions MUST tolerate client NAT rebinding.

The server SHALL NOT permanently bind session identity to:

source IP + source port

Instead:

  1. receive packet;
  2. parse session ID;
  3. verify packet authentication;
  4. verify sequence/replay protection;
  5. if valid, update the session's current peer address.

Therefore:

192.0.2.20:43871

may become:

192.0.2.20:51203

without renegotiating the session.

An attacker cannot redirect the session merely by spoofing its session ID because a valid packet MAC is also required.


33. KEEPALIVE

Direct transport KEEPALIVE messages SHALL use the same session authentication mechanism as DATA messages.

They carry:

common header
sequence
auth_tag

with no application payload.

Default interval:

15 seconds

Keepalive MAY be disabled.

Keepalives count as session activity.


34. CLOSE

CLOSE SHALL be authenticated.

The receiver SHOULD immediately release the associated session.

Loss of a CLOSE packet is harmless because idle timeout remains authoritative.


35. Session Timeout

Default idle timeout:

120 seconds

A session is active when an authenticated:

DATA
KEEPALIVE

packet is received.

Expired sessions SHALL:

  1. close their backend socket;
  2. release memory;
  3. remove replay state;
  4. increment expiration metrics.

36. QUIC and HTTP/3 Authentication

TLS authenticates the server but does not necessarily authenticate the client.

QUIC and HTTP/3 transports SHALL therefore use the same logical:

client_id
shared_secret

credentials as direct mode.

Authentication occurs once after transport establishment.

It SHALL use a challenge-response protocol based on HMAC rather than directly sending the shared secret.

Conceptually:

server -> random challenge

client -> client_id
          client_nonce
          HMAC(secret, challenge || client_nonce || client_id)

server -> authenticated

The exact representation MAY use:

  • a QUIC control stream;
  • an HTTP request;
  • a dedicated initial protocol message.

Application DATA MUST NOT be accepted before authentication completes.


37. TLS/TCP Authentication

TLS/TCP SHALL use the same challenge-response client authentication mechanism.

This gives all transports one conceptual authentication model:

server authenticated by TLS where applicable
client authenticated by project shared secret

38. Future Authentication

The architecture SHOULD permit later introduction of:

mTLS
public-key client identity
short-lived credentials
external authentication

without altering the core datagram API.

These are not required for v1.


39. DoS and Resource Protection

The server MUST assume that its public listeners receive hostile traffic.

It SHALL implement:

maximum active sessions
maximum sessions per source IP
handshake timeout
authentication rate limiting
maximum authentication attempts per source
maximum datagram size
idle session timeout
maximum pending handshakes

Defaults SHOULD be conservative.

Example:

[limits]
max_sessions = 1024
max_sessions_per_ip = 32
max_pending_handshakes = 256
handshake_timeout_ms = 3000

Limits MUST be configurable.


40. Allocation Policy

Expensive resources MUST NOT be allocated before authentication where avoidable.

In particular, the server SHOULD NOT open a backend UDP socket until client authentication succeeds.

Desired order:

receive handshake
      ↓
cheap validation
      ↓
authentication
      ↓
rate-limit check
      ↓
session allocation
      ↓
backend socket creation

41. MTU and Datagram Size

The project SHALL treat transport datagram size as a first-class capability.

Every active transport session exposes:

max_datagram_size

The core SHALL reject application datagrams larger than this value.

Initial behavior:

oversized datagram
      ↓
drop
      ↓
increment counter
      ↓
debug/warn log

The project SHALL NOT implement application-layer fragmentation in v1.


42. Recommended Initial Datagram Limit

A conservative default application payload limit SHOULD begin around:

1200 bytes

and later be refined using transport/path information.

Users MAY override this value.

The default MUST favor reliability over maximum packet size.


43. Configuration Format

Configuration SHALL use TOML.

Parsing stack:

serde
toml

Configuration MUST be validated before network listeners start.

Invalid configuration SHALL result in a clear fatal error.


44. Minimal Client Configuration

[client]
server = "vpn.example.com"
id = "laptop"
secret_file = "/etc/project/client.secret"

[local]
listen = "127.0.0.1:51821"

[transport]
mode = "auto"

The server hostname is static in v1.

DNS resolution SHALL use the system resolver.


45. Minimal Server Configuration

[server]
listen = "0.0.0.0"

[backend]
target = "127.0.0.1:51820"

[tls]
certificate = "/etc/letsencrypt/live/vpn.example.com/fullchain.pem"
private_key = "/etc/letsencrypt/live/vpn.example.com/privkey.pem"

[[auth.clients]]
id = "laptop"
secret_file = "/etc/project/clients/laptop.secret"

46. Advanced Client Configuration

[client]
server = "vpn.example.com"
id = "laptop"
secret_file = "/etc/project/client.secret"

[local]
listen = "127.0.0.1:51821"

[transport]
mode = "auto"

[transport.auto]
order = [
    "direct_udp",
    "quic",
    "http3",
    "tls_tcp",
]

probe_timeout_ms = 1500
retries = 2

[transport.direct_udp]
port = 51830
keepalive_secs = 15

[transport.quic]
port = 443
keepalive_secs = 15

[transport.http3]
port = 443
path = "/tunnel"

[transport.tls_tcp]
port = 443

[network]
max_datagram_size = 1200

[logging]
level = "info"

47. Advanced Server Configuration

[server]
listen = "0.0.0.0"

[backend]
target = "127.0.0.1:51820"

[transports.direct_udp]
enabled = true
port = 51830

[transports.quic]
enabled = true
port = 443

[transports.http3]
enabled = true
port = 443
path = "/tunnel"

[transports.tls_tcp]
enabled = true
port = 443

[tls]
certificate = "/etc/letsencrypt/live/vpn.example.com/fullchain.pem"
private_key = "/etc/letsencrypt/live/vpn.example.com/privkey.pem"

[session]
idle_timeout_secs = 120

[limits]
max_sessions = 1024
max_sessions_per_ip = 32
max_pending_handshakes = 256
handshake_timeout_ms = 3000

[[auth.clients]]
id = "laptop"
secret_file = "/etc/project/clients/laptop.secret"

[[auth.clients]]
id = "desktop"
secret_file = "/etc/project/clients/desktop.secret"

[logging]
level = "info"

48. Secret Storage

Inline secrets MAY be accepted for testing but SHOULD generate a warning.

Preferred configuration:

secret_file = "/path/to/secret"

Secret files SHOULD require restrictive permissions.

Recommended:

0600

Secrets MUST NOT appear in logs.


49. TLS Certificates

TLS-capable transports SHALL use normal X.509 server certificates.

Expected provisioning includes:

Certbot
acme.sh
Caddy
other ACME tooling

The project does not implement ACME itself in v1.

Clients SHALL perform normal hostname verification against the configured static hostname.

Certificate verification MUST NOT be disabled by default.


50. Certificate Reload

Automatic certificate reload is desirable but optional for the first prototype.

The architecture SHOULD allow certificates to be reloaded without rebuilding transport logic.


51. Session Model

Core session representation:

struct Session {
    id: SessionId,
    client_id: ClientId,
    transport: TransportKind,
    backend: UdpSocket,
    created_at: Instant,
    last_activity: Instant,
    stats: SessionStats,
}

Transport-specific information SHOULD remain outside the generic session where possible.


52. Datagram Representation

Initial representation:

struct Datagram {
    payload: Bytes,
}

Do not add unnecessary metadata until required.

Potential future form:

struct Datagram {
    payload: Bytes,
    received_at: Instant,
    flow_id: Option<FlowId>,
}

53. Client State Machine

STARTING
   │
   ▼
RESOLVING
   │
   ▼
PROBING
   │
   ├─ direct_udp
   ├─ quic
   ├─ http3
   └─ tls_tcp
   │
   ▼
AUTHENTICATING
   │
   ▼
CONNECTED
   │
   ├──────── traffic
   │
   └──────── failure
               │
               ▼
         RECONNECTING
               │
               ▼
            PROBING

A transport failure MUST NOT terminate the daemon.


54. Backoff

Repeated connection failures SHALL use exponential backoff with jitter.

Example bounds:

minimum  250 ms
maximum   30 s

Successful connection resets the backoff.

This avoids aggressive reconnect loops during outages.


55. Server Session Lifecycle

HANDSHAKE
   │
   ▼
AUTHENTICATING
   │
   ├── reject
   │
   ▼
ACTIVE
   │
   ├── client close
   ├── transport loss
   ├── timeout
   └── server shutdown
          │
          ▼
        CLOSED

Resources SHALL be released deterministically.


56. Keepalive Strategy

Keepalive behavior is transport-specific.

direct_udp
    authenticated protocol KEEPALIVE

quic
    QUIC keepalive

http3
    underlying QUIC keepalive

tls_tcp
    transport heartbeat or TCP state

Keepalive defaults SHOULD preserve common NAT mappings without producing unnecessary traffic.


57. Observability

At minimum the client SHALL expose:

state
server hostname
resolved remote address
selected transport
connection duration
RTT where available
bytes sent
bytes received
datagrams sent
datagrams received
datagrams dropped
oversized datagrams
authentication failures
reconnections
max datagram size

The server SHALL additionally expose:

active sessions
sessions by transport
sessions by client ID
handshake attempts
authentication rejections
rate-limited attempts
expired sessions
backend errors

58. Logging

Use:

tracing
tracing-subscriber

Logs SHOULD contain structured fields such as:

client_id
session_id
transport
remote_addr
event

Secrets and application payloads MUST NEVER be logged by default.

Packet payload logging SHOULD not exist in normal release builds.


59. Logging Levels

Suggested semantics:

ERROR
    unrecoverable subsystem failure

WARN
    transport failure
    malformed traffic
    rejected config behavior

INFO
    startup
    successful connection
    selected transport
    authenticated session
    reconnect
    shutdown

DEBUG
    probing details
    MTU decisions
    session lifecycle

TRACE
    low-level protocol state

60. Local Control Interface

The daemon SHOULD expose a Unix domain control socket.

Example:

$XDG_RUNTIME_DIR/project.sock

The control protocol MAY initially use newline-delimited JSON.

Expected commands:

status
stats
reconnect
reload
shutdown

The control socket MUST use filesystem permissions to prevent unauthorized local control.


61. CLI

Initial commands:

project client --config FILE
project server --config FILE

project check-config --config FILE
project generate-secret

Later:

project status
project stats
project reconnect
project reload

62. Graceful Shutdown

On SIGTERM or SIGINT:

  1. stop accepting new sessions;
  2. notify transports where appropriate;
  3. stop local ingestion;
  4. close backend sockets;
  5. flush useful final metrics/logs;
  6. terminate cleanly.

Shutdown MUST have a bounded timeout.


63. Configuration Reload

Full hot reload is not mandatory for the first milestone.

Architecture SHOULD nonetheless separate:

parsed configuration
runtime state

A future reload MUST be capable of updating:

  • clients;
  • limits;
  • log level;
  • certificate paths;

without requiring architectural redesign.


64. Future Multiple Tunnel Support

V1 exposes one client tunnel.

Internally, configuration SHOULD already use:

struct TunnelConfig

rather than embedding tunnel fields directly everywhere.

Initial:

struct ClientConfig {
    tunnel: TunnelConfig,
}

Future:

struct ClientConfig {
    tunnels: Vec<TunnelConfig>,
}

Future TOML:

[[tunnels]]
name = "home"

[[tunnels]]
name = "work"

No v1 UI or runtime behavior is required for multiple tunnels.


65. Rust Technical Stack

Recommended language:

Rust

Recommended runtime:

Tokio

Recommended major libraries:

tokio
quinn
rustls
h3
h3-quinn
bytes
serde
toml
clap
tracing
tracing-subscriber
thiserror
anyhow
socket2
hmac
sha2
hkdf
rand
subtle

Potential optional libraries:

secrecy
zeroize

Exact versions SHALL be selected at implementation time.


66. Error Handling

Library/core modules SHOULD use typed errors with:

thiserror

Binary/CLI boundaries MAY use:

anyhow

Recoverable network failure MUST NOT be represented as an unrecoverable process error.


67. Constant-Time Authentication

Authentication tag comparison MUST use constant-time equality.

Do not compare authentication data with ordinary:

a == b

unless the cryptographic library explicitly guarantees appropriate behavior.

Use primitives supplied by the selected crypto implementation or a constant-time helper library.


68. Crypto Policy

The project SHALL NOT:

  • invent encryption algorithms;
  • implement SHA manually;
  • implement HMAC manually;
  • implement HKDF manually;
  • implement TLS manually.

All cryptographic operations SHALL use maintained, reviewed Rust libraries.

The direct transport payload itself does not require encryption because the encapsulated application may already be encrypted.

Direct transport DOES require integrity and authentication.

Users requiring outer confidentiality SHALL use QUIC, HTTP/3, or TLS/TCP.


69. Core Module Structure

Initial source tree SHOULD remain simple:

src/
├── main.rs
├── cli.rs
├── config.rs
├── datagram.rs
├── session.rs
├── stats.rs
├── client/
│   ├── mod.rs
│   ├── listener.rs
│   └── supervisor.rs
├── server/
│   ├── mod.rs
│   ├── session.rs
│   ├── backend.rs
│   └── limits.rs
├── protocol/
│   ├── mod.rs
│   ├── direct.rs
│   ├── auth.rs
│   └── replay.rs
└── transport/
    ├── mod.rs
    ├── direct_udp.rs
    ├── quic.rs
    ├── http3.rs
    └── tls_tcp.rs

A Cargo workspace SHOULD NOT be introduced until module boundaries have stabilized.


70. Core Dependency Direction

Desired dependency structure:

            configuration
                 │
                 ▼
               core
        ┌────────┴────────┐
        │                 │
      client            server
        │                 │
        └──── transport ──┘
                 │
       ┌─────────┼──────────┐
       ▼         ▼          ▼
      UDP       QUIC      HTTP/3
                            │
                           etc.

The core MUST NOT contain scattered transport type checks.

Avoid:

if transport == Transport::Quic {
    ...
}

unless behavior genuinely belongs at the orchestration layer.


71. Performance Model

The fast path SHOULD minimize allocations.

Preferred packet representation:

Bytes
BytesMut

Buffers SHOULD be reused where practical.

The implementation SHOULD avoid:

  • copying payloads unnecessarily;
  • global mutexes on every packet;
  • per-packet heap allocation where easily avoidable;
  • synchronous filesystem operations in packet paths.

Correctness takes priority over premature micro-optimization.


72. Concurrency Model

Tokio tasks SHALL be used around logical session operations.

A server session will approximately contain:

transport receive ─────► backend UDP send
transport send ◄─────── backend UDP receive

These may be implemented using:

  • paired tasks;
  • tokio::select!;
  • channels;
  • a session supervisor.

The simplest implementation that preserves cancellation and ownership clarity SHOULD be preferred.


73. Cancellation

All long-lived tasks MUST support deterministic cancellation.

Recommended primitives include:

CancellationToken
broadcast/watch channels
task ownership through supervisors

Detached tasks that cannot be shut down SHOULD be avoided.


74. Server Multi-Client Requirements

The server MUST:

  • accept concurrent clients;
  • isolate session errors;
  • prevent one client from crashing another session;
  • enforce per-client limits;
  • maintain independent backend sockets;
  • clean up dead sessions;
  • maintain aggregate and per-session stats.

One malformed client packet MUST NOT terminate a listener.


75. Direct UDP Listener Model

The direct listener may use one public UDP socket.

Incoming packets are dispatched by:

packet type
session ID

Initial HELLO packets use:

session_id = 0

Authenticated DATA packets identify their session through the assigned random ID.


76. QUIC Listener Model

One QUIC endpoint accepts many connections.

Each authenticated QUIC connection becomes one server session.

QUIC connection state acts as transport identity.

Application packets arrive through QUIC DATAGRAM frames.


77. HTTP/3 Listener Model

HTTP/3 operates over the QUIC listener architecture.

Each successful authenticated CONNECT-UDP exchange maps to one backend session.

Where feasible, QUIC and HTTP/3 MAY share certificate/configuration infrastructure.


78. TCP Listener Model

Each accepted TLS/TCP connection becomes one potential session.

No backend socket is created until project authentication succeeds.

After authentication, framed datagrams are transported bidirectionally.


79. Protocol Versioning

Direct wire protocol packets contain:

version = 1

Breaking protocol changes SHALL increment the version.

Unknown versions MUST result in clean rejection.

Configuration schema and project binary version are independent from transport wire protocol version.


80. Metrics and Benchmarking

The project SHOULD eventually include benchmarks for:

datagrams/sec
throughput
latency overhead
memory/session
CPU usage
direct UDP overhead
QUIC overhead
HTTP/3 overhead
TCP fallback overhead

Performance tests SHOULD compare the tunnel against direct UDP baseline.


81. Testing Strategy

Unit tests SHALL cover:

packet serialization
packet parsing
malformed packets
HMAC verification
session key derivation
sequence windows
replay detection
configuration validation
timeout logic

Integration tests SHALL cover:

client ↔ server direct UDP
multiple simultaneous clients
NAT-like source port change
authentication rejection
server restart
client reconnect
backend unavailable
oversized datagram
session expiration

Later tests SHALL cover each transport.


82. Fuzzing

The direct UDP protocol parser is a good fuzzing target.

The parser SHOULD be written so arbitrary byte strings can be fed into it independently of network state.

Fuzz targets SHOULD include:

direct packet parser
TCP frame parser
authentication message parser
configuration parser where appropriate

83. Development Milestone 1

Goal:

generic authenticated UDP relay

Implement only:

client local UDP listener
direct UDP transport
server listener
authentication handshake
session creation
backend UDP sockets
DATA packet authentication
replay protection
keepalive
session expiry
multiple simultaneous clients
TOML configuration
structured logs
basic statistics

No QUIC yet.

This validates the entire core architecture.


84. Development Milestone 2

Add QUIC.

Requirements:

TLS certificate loading
hostname validation
QUIC connection establishment
project client authentication
QUIC DATAGRAM send/receive
keepalive
MTU reporting
reconnect
multi-client server handling

No core datagram API redesign should be necessary.

If substantial core redesign is required, the transport abstraction should be reconsidered before continuing.


85. Development Milestone 3

Implement:

transport = "auto"

Automatic order:

direct_udp
quic

Include:

  • probing;
  • timeouts;
  • retries;
  • exponential reconnect backoff;
  • status reporting.

86. Development Milestone 4

Implement:

HTTP/3 CONNECT-UDP

using the existing QUIC infrastructure where practical.

Add:

HTTP/3 listener
CONNECT request handling
authentication
HTTP datagrams
backend session mapping

87. Development Milestone 5

Implement TLS/TCP fallback.

Add:

TLS listener
framed packet parser
project authentication
bidirectional datagram reconstruction
automatic fallback integration

88. Development Milestone 6

Operational polish:

Unix control socket
status command
stats command
reconnect command
config validation command
systemd units
CAP_NET_BIND_SERVICE documentation
certificate reload
configuration reload
benchmarks
fuzzing
installation documentation

89. Definition of v1 Completion

Version 1 is considered complete when:

  1. multiple clients can concurrently reach one UDP backend;
  2. no WireGuard-specific logic exists;
  3. direct UDP works reliably;
  4. QUIC DATAGRAM works reliably;
  5. HTTP/3 CONNECT-UDP works;
  6. TLS/TCP fallback works;
  7. automatic transport selection works;
  8. authentication is enforced on every public transport;
  9. direct UDP includes replay and integrity protection;
  10. NAT rebinding works for direct UDP;
  11. sessions expire and clean up correctly;
  12. the server survives malformed and unauthenticated traffic;
  13. the daemon runs unprivileged except for optional privileged-port capability;
  14. TOML configuration is validated clearly;
  15. meaningful status and statistics are available;
  16. reconnects occur automatically;
  17. automated integration tests cover multi-client operation.

90. Explicit Non-Goals for v1

The following SHALL remain out of scope:

WireGuard configuration
TUN/TAP interfaces
routing management
firewall management
multi-hop routing
browser fingerprint imitation
traffic shaping for censorship evasion
packet-size mimicry
TLS fingerprint spoofing
dynamic server discovery
server clustering
automatic roaming between servers
GUI
Windows
macOS
multiple simultaneous client tunnels
application-layer fragmentation
custom encryption

These MAY be explored after the core transport system is mature.


91. Architectural Summary

The system can be summarized as:

                   CLIENT

Application
    │
    │ UDP
    ▼
Local listener
    │
    ▼
Tunnel/session core
    │
    ▼
Transport abstraction
    │
    ├── Direct UDP
    ├── QUIC DATAGRAM
    ├── HTTP/3 CONNECT-UDP
    └── TLS/TCP
    │
════════════════ Internet ════════════════
    │
    ▼
Transport listener
    │
    ▼
Authenticated server session
    │
    ▼
Dedicated backend UDP socket
    │
    ▼
Configured UDP backend

The core sees only:

send datagram
receive datagram
session opened
session closed
transport capabilities

It does not care whether the external representation is:

custom direct UDP packet
QUIC connection
HTTP/3 CONNECT-UDP flow
TLS/TCP stream

92. Final Design Rule

The project SHALL always preserve this separation:

application protocol
        ≠
tunnel session
        ≠
transport
        ≠
authentication

The encapsulated application remains opaque.

Sessions provide logical client isolation.

Transports determine how datagrams cross the network.

Authentication determines who may create sessions.

Keeping those four responsibilities independent is the central architectural rule of the project.