SNI proxy with TCP multiplexer
Project description
SniTun
End-to-End encryption with SNI proxy on top of a TCP multiplexer
Connection flow
sequenceDiagram
participant E as Endpoint
participant C as Client
participant M as Session Master
participant S as SniTun
participant D as Device
Note over C,M: Trusted connection
C->>M: Auth / config
M-->>C: Fernet token
Note over C,S: Insecure connection
C->>S: Fernet token
S->>C: Challenge (AES/CBC)
C->>S: Response (AES/CBC)
Note over C,S: Client enters multiplexer mode
D->>S: External connection (TLS / SNI)
S->>C: Forward over multiplexer (AES/CBC)
C->>E: Open local connection
Note over E,D: End-to-end SSL (trusted connection)
Fernet token
The session master encrypts the client's configuration into a Fernet token. The session master and the SniTun server share the Fernet key(s), so only the SniTun server can decrypt the token; the client just relays it when it connects.
The token payload is a JSON object:
{
"valid": 1924948800.0,
"hostname": "myname.ui.nabu.casa",
"aes_key": "401933e35f9f43d18db1d1de2e5d2e9a9f4c3b2a1d0e9f8c7b6a5d4c3b2a1f0e",
"aes_iv": "9b2c4d6e8f0a1b3c5d7e9f0a1b2c3d4e",
"protocol_version": 2,
"cipher": "aes-gcm",
"alias": ["www.myname.ui.nabu.casa"]
}
| Field | Type | Description |
|---|---|---|
valid |
float | Expiry as a UTC Unix timestamp in seconds. The SniTun server rejects the token once this time has passed. |
hostname |
string | Primary hostname (matched against the TLS SNI) that this peer serves. |
aes_key |
string | Hex-encoded 32-byte key (AES-256) used to encrypt the multiplexer header. |
aes_iv |
string | Hex-encoded 16-byte initialization vector. Used by aes-cbc; ignored by aes-gcm/aes-gcm-siv (which use a random per-frame nonce). |
protocol_version |
int | Multiplexer protocol version the client speaks (see Protocol versioning considerations). Optional; the server assumes 0 when omitted. |
cipher |
string | Multiplexer cipher: aes-cbc (default), aes-gcm, or aes-gcm-siv (both authenticated). Optional; the server assumes aes-cbc when omitted. Both ends must be configured the same. |
alias |
string[] | Additional hostnames the peer also serves. Optional. |
The SniTun server must be able to decrypt this token to validate the client's authenticity. SniTun then initiates a challenge-response handling to validate the AES key and ensure that it is the same client that requested the Fernet token from the session master.
Note: SniTun server does not perform any user authentication!
Challenge/Response
The SniTun server creates a SHA256 hash from a random 40-bit value. This value is encrypted and sent to the client, who then decrypts the value and performs another SHA256 hash with the value and sends it encrypted back to SniTun. If it is valid, the client enters the Multiplexer mode.
Multiplexer Protocol
The header is encrypted using the cipher selected by the token (see Fernet token): aes-cbc (default) or the authenticated aes-gcm/aes-gcm-siv. The payload should be SSL. The ID changes for every TCP connection and is unique for each connection. The size is for the data payload.
With aes-gcm or aes-gcm-siv each encrypted unit (the header, and the encrypted New data) is framed as nonce(12) || ciphertext || tag(16), so the header occupies 60 bytes on the wire instead of 32 and the source IP in a New message carries its own nonce and tag. The tag makes the header — including SIZE — tamper-evident, which AES-CBC cannot detect.
aes-gcm builds its nonce deterministically as direction(4) || counter(8): a per-direction prefix (the client takes 0, the server takes 1) followed by a 64-bit per-frame counter. This guarantees the nonce is unique within a session without the birthday risk of a random 96-bit nonce, and the opposite prefixes keep the two directions — which share one AES key — in disjoint nonce ranges. Because the counter restarts at 0 for every connection, aes-gcm requires a fresh AES key per session: reusing a key across reconnects would repeat nonces, which is catastrophic for GCM. aes-gcm-siv (RFC 8452) is nonce-misuse resistant — a repeated nonce only reveals whether two units were identical — so it keeps a stateless random nonce and stays safe even when an AES key can outlive a single connection. It requires OpenSSL 3.0+; servers without it should keep using aes-gcm with a per-session key.
The extra information could include the caller IP address for a New message on protocol version < 2. From protocol version 2 the caller IP is sent in the (encrypted) data instead — see the New message type below. Otherwise, it is random bits.
The 32-byte header is followed by a variable-length data payload (byte offsets shown):
packet-beta
0-15: "ID (16 bytes)"
16-16: "FLAG (1 byte)"
17-20: "SIZE (4 bytes)"
21-31: "EXTRA (11 bytes)"
32-63: "DATA (variable)"
Message Flags/Types:
0x01: New | Carries the caller IP address.- Protocol version < 2: the
EXTRAfield holds the first byte as an ASCII4(IPv6 is not supported), followed by the 4-byte IPv4 address; thedatapayload is empty. - Protocol version >= 2: the address is sent in the encrypted
datapayload (its own encrypted unit, following the header) as a one-byte family marker (4or6) followed by the packed address (4 bytes for IPv4, 16 for IPv6), padded with random bytes to an AES block boundary. This keeps the caller IP off the wire in clear text and allows IPv6 to be carried.
- Protocol version < 2: the
0x02: DATA0x04: Close0x08: Ping | The extra data is apingorpongresponse to a ping.0x16: Pause the remote reader (added in protocol version 1)0x32: Resume the remote reader (added in protocol version 1)
Configuration via environment variables
The following environment variables, which, to be effective, must be set before importing this package, are available to override internal defaults:
MULTIPLEXER_INCOMING_QUEUE_MAX_BYTES_CHANNEL- The maximum number of bytes allowed in the incoming queue for each multiplexer channel.MULTIPLEXER_INCOMING_QUEUE_MAX_BYTES_CHANNEL_V0- The maximum number of bytes allowed in the incoming queue for protocol version 0 channels (default: 256MB, larger than standard channels since v0 lacks flow control).MULTIPLEXER_INCOMING_QUEUE_LOW_WATERMARK- The low watermark threshold, in bytes, for the incoming queue for each multiplexer channel.MULTIPLEXER_INCOMING_QUEUE_HIGH_WATERMARK- The high watermark threshold, in bytes, for the incoming queue for each multiplexer channel.MULTIPLEXER_OUTGOING_QUEUE_MAX_BYTES_CHANNEL- The maximum number of bytes allowed in the outgoing queue for the multiplexer channel.MULTIPLEXER_OUTGOING_QUEUE_LOW_WATERMARK- The low watermark threshold, in bytes, for the outgoing queue for each multiplexer channel.MULTIPLEXER_OUTGOING_QUEUE_HIGH_WATERMARK- The high watermark threshold, in bytes, for the outgoing queue for each multiplexer channel.
Protocol versioning considerations
- The client is responsible for setting the
protocol_versionkey in the token. If noprotocol_versionis provided, the server must assume protocol version 0. - The server side must always be updated first when incrementing the protocol version as the client assumes that the server is always running a protocol version that it supports.
- When new message types are added to the Multiplexer, the protocol version must be incremented.
- Both ends of a connection must agree on the protocol version, since some versions change the wire format (see below). The negotiated version is symmetric.
Versions:
0: Base protocol. No flow control. Caller IPv4 in the headerEXTRAfield.1: Adds reader pause/resume (0x16/0x32) for flow control.2: Sends the caller IP in the encryptedNewmessage data instead ofEXTRA, which keeps it off the wire in clear and adds IPv6 support.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file snitun-0.46.1.tar.gz.
File metadata
- Download URL: snitun-0.46.1.tar.gz
- Upload date:
- Size: 64.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bdeeda3684b7d4facfdaa2a87ed7afb50af8ab48363fbb7e158100addf7c121a
|
|
| MD5 |
04a950e971a806d818f56084ca1d7bad
|
|
| BLAKE2b-256 |
6c38da9d6156a4598b517f1723c9913313b47496a79d8d00f39066ba575cfa11
|
Provenance
The following attestation bundles were made for snitun-0.46.1.tar.gz:
Publisher:
release.yml on NabuCasa/snitun
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
snitun-0.46.1.tar.gz -
Subject digest:
bdeeda3684b7d4facfdaa2a87ed7afb50af8ab48363fbb7e158100addf7c121a - Sigstore transparency entry: 1789295324
- Sigstore integration time:
-
Permalink:
NabuCasa/snitun@0274a93b7d83fb126f3ff36218b60b7acc9ef7b0 -
Branch / Tag:
refs/tags/0.46.1 - Owner: https://github.com/NabuCasa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0274a93b7d83fb126f3ff36218b60b7acc9ef7b0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file snitun-0.46.1-py3-none-any.whl.
File metadata
- Download URL: snitun-0.46.1-py3-none-any.whl
- Upload date:
- Size: 72.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed80eb0607bead7c6e507155ed6b61eb04731e078b12d3ac958ee0030d551d8e
|
|
| MD5 |
99515f4acdc682b4214e733860c1cf77
|
|
| BLAKE2b-256 |
b2d7601ee547826ca960fdb6824e3a4e6d3a68c0a60c3c7c0ac773636cf9726c
|
Provenance
The following attestation bundles were made for snitun-0.46.1-py3-none-any.whl:
Publisher:
release.yml on NabuCasa/snitun
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
snitun-0.46.1-py3-none-any.whl -
Subject digest:
ed80eb0607bead7c6e507155ed6b61eb04731e078b12d3ac958ee0030d551d8e - Sigstore transparency entry: 1789295338
- Sigstore integration time:
-
Permalink:
NabuCasa/snitun@0274a93b7d83fb126f3ff36218b60b7acc9ef7b0 -
Branch / Tag:
refs/tags/0.46.1 - Owner: https://github.com/NabuCasa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0274a93b7d83fb126f3ff36218b60b7acc9ef7b0 -
Trigger Event:
release
-
Statement type: