USTPS means UDP Speedy Transmission Protocol Secure.
USTP-Secure keeps USTP on UDP and adds authenticated packet protection.
By default, USTPS uses AEAD for DATA
.
It also supports an optional negotiated cleartext + HMAC
mode for DATA
, where payload bytes are visible on the wire but tampering is detected and rejected.
USTPS now supports an optional congestion controller called USTPS Congestion
. It is still UDP-first and still keeps unordered delivery, but it can optionally slow down and ramp back up when the path starts showing congestion signals.
Status: Beta
USTPS is no longer just a proof of concept. It is currently in the Beta phase.
USTPS can be used for many kinds of applications and transports.
This repository, however, is focused specifically on streaming over USTPS.
- Built with
Codex
usingGPT-5.4 (Low)
. - Verified without freezing at
--loss 33
. - Test path:
Brazil -> Canada
with about140ms
RTT.
- Transport remains UDP (no TCP tunnel)
- AEAD ciphers:
chacha20
=CHACHA20_POLY1305
aes-256-gcm
=AES_256_GCM
aes-128-gcm
=AES_128_GCM
-
Default AEAD cipher:
chacha20 -
Default
DATA
protection mode is AEAD. - Optional
DATA
protection mode is cleartext + per-packet HMAC. - In cleartext mode, payload bytes are not encrypted, but modifications are detected and invalid packets are discarded.
- Transport control packets (
HELLO
,ACK
,RETRANSMIT_REQUEST
,CLOSE
) stay plaintext on purpose. - Control packets are serialized as ASCII transport records.
ACK
andNACK
/RETRANSMIT_REQUEST
remain plaintext, but are authenticated with a per-session HMAC tag.- This prevents off-path forged ACK/NACK control packets from forcing ACK attacks or retransmission DoS after the secure session is established.
DATA
packets use a binary frame format namedUPACK
(UPAK
on the wire).- No static PSK is used.
- Each client performs an X25519 key exchange when it joins.
- Each client gets a separate AEAD session key.
- Servers support multiple clients.
- The server validates the client with a challenge round-trip on the source
IP:port
. - If
--cipher
is set on the server, the server uses that exact cipher. - If
--cipher
is omitted or set toauto
, the server uses the cipher requested by the client. - Clients reject unexpected cipher negotiation.
DATA
protection mode is negotiated separately from cipher choice:- server:
--cleartext auto|on|off
-
client:
--cleartext on|off -
server default:
auto -
client default:
off -
server:
-
With server
auto
, the server follows the client request. - With server
on
, the server forces cleartext + HMAC. - With server
off
, the server forces AEAD. - TOFU (Trust On First Use) is enabled on the client to detect unexpected server key changes after the first connection.
- The server keeps a persistent X25519 host key in
~/.ustps_host_key
by default so TOFU remains stable across reconnects and restarts. - A normal server restart does not change the host key.
- Use
--regen-key
on the server only when you intentionally want to rotate that host key.
ACK:
,NACK:
,HELLO:
, andCLOSE:
are the plaintext control record prefixes.USS1
meansUDP Speedy Secure
, version 1.USC1
meansUDP Speedy Clear
, version 1.UPAK
is the binaryUPACK
DATA frame marker.- In USTPS, plaintext control is human-readable ASCII such as
ACK: 10
,NACK: 42
,HELLO: ...
, andCLOSE:
. - In USTPS,
UPAK
identifies binary DATA packets after decryption. - In USTPS,
USS1
is the outer secure AEAD envelope format. - In USTPS,
USC1
is the outer cleartext+HMACDATA
envelope format. - So, on the wire you normally see:
USS1...
for AEAD-protectedDATA
USC1...
for cleartext+HMACDATA
-
readable control lines for transport control
-
USTPS is reliable over UDP, but it is unordered by design. - USTPS can run with optional
USTPS Congestion
, negotiated during the handshake. - Packets carry both a transport
seq
and an application-facingstream_pos
. seq
is used for ACK, loss detection, retransmission, andRTT
sampling.stream_pos
tells the application where the payload belongs in the logical byte stream.- In the current implementation,
seq
is a 32-bit counter that starts at1
for each fresh session. - In the current implementation,
stream_pos
is a 64-bit byte counter that starts at0
for each fresh logical stream. - The receiver accepts out-of-order packets immediately instead of blocking delivery behind one missing packet.
Example:
-
Physical arrival:
1 2 3 5 6 -
Packet
4
is missing, so the receiver buffers the gap information and sendsRETRANSMIT_REQUEST
for4
. - Packets
5
and6
are still accepted immediately. - When packet
4
arrives later, the application can reconstruct the logical order by usingstream_pos
, not by trusting arrival order.
- The client starts with a plaintext transport
HELLO
carrying its X25519 public key, requested cipher, requested congestion-control mode (on
oroff
), and requestedDATA
protection mode (cleartext on|off
). - The server does not send media immediately. It first sends a plaintext challenge containing:
-
a random retry token
-
a generated Base64
session_id -
the selected cipher
-
the negotiated congestion-control mode
-
the negotiated
DATA
protection mode - the server public key
- The client must answer with that exact same token and session metadata.
- Only after that token round-trip succeeds does the server create the session and begin sending
DATA
. - After validation, the session is bound to the source
IP:port
that completed the challenge. session_id
is still used as a session label, but it is not accepted from a differentIP:port
.
-
USTPS uses a plaintext retry-token step before any encrypted media session is accepted.
-
Flow:
-
client sends
HELLO -
server replies with
USTPS-CHALLENGE1
carryingtoken
,session_id
, selected cipher, negotiated congestion-control mode, negotiatedDATA
protection mode, and server public key - client echoes that token back in
USTPS-CHALLENGE-REPLY1
-
only then does the server create the session and send
USTPS-SESSION1 -
client sends
-
Purpose:
-
prove that the sender at that source
IP:port
can actually receive packets there - avoid sending encrypted media immediately to an unvalidated source address
-
bind the final session creation to the endpoint that completed the round-trip
-
prove that the sender at that source
-
The retry token is not the session key.
-
It is only a reachability proof and handshake gate before the real USTPS session is created.
-
It is also not used as a nonce, not used as an ACK/NACK MAC key by itself, and not reused as packet payload state.
-
Current
UPACK
DATA payload limit:1200
bytes. UPACK
fixed header:20
bytes.- Outer
USS1
secure envelope overhead in AEAD mode:4
bytes magic1
byte cipher id12
bytes AEAD nonce16
bytes AEAD tag
- So the encrypted USTPS DATA datagram is about
1253
bytes before IP/UDP headers. - Outer
USC1
cleartext envelope overhead in cleartext mode:4
bytes magic16
bytes HMAC tag
- So the cleartext USTPS DATA datagram is about
1240
bytes before IP/UDP headers. - With IPv4 + UDP headers, that is about
1281
bytes on the wire. - With IPv6 + UDP headers, that is about
1301
bytes on the wire. - USTPS currently does not implement transport-level fragmentation. - USTPS currently does not implement PMTU discovery. - The implementation instead uses a fixed conservative payload ceiling.
-
If IP fragmentation still happens underneath and one fragment is lost, the whole UDP datagram is lost and USTPS recovers it with normal selective retransmission.
-
Duplicate packets inside the current session are ignored after their
seq
was already accepted. - Very old packets can age out of the receiver history window and then be ignored as stale.
- Old ACK/NACK packets for data that has already been retired from the retransmission buffer are ignored.
- A stale control packet does not recreate an already-finished packet in the sender.
DATA
encryption uses a fresh random12
-byte AEAD nonce per encrypted packet.- The nonce is generated randomly, not derived from
seq
. - Nonce reuse with the same session key is forbidden.
seq
is for transport reliability.stream_pos
is for logical application ordering.nonce
is only for AEAD packet protection.- Cleartext+HMAC mode does not use an AEAD nonce because it does not use AEAD encryption for
DATA
.
USTPS Congestion
is optional.- It does not change USTPS into an ordered transport and it does not add TCP-style HoL blocking.
-
It only changes how aggressively the sender injects packets into the network.
-
Negotiation model:
-
server:
--congestion-control auto|on|off -
client:
--congestion-control on|off -
server:
-
Default behavior:
-
server default is
auto -
client default is
off -
with server
auto
, the server follows what the client asked for - with server
on
, congestion control is forced on even if the client asked foroff
- with server
off
, congestion control is forced off even if the client asked foron
- server default is
- Runtime behavior:
- starts at a normal send rate
- gradually increases the effective send window and burst size while the path stays healthy
- watches measured
RTT
, retransmission timeout events (RTO
), and explicit retransmit requests (NACK
) - if
RTT
inflates,RTO
starts happening, or loss/retransmit pressure rises, it backs off - once the path stabilizes again, it slowly ramps back up
- The sender still uses selective retransmission for missing packets only.
USTPS Congestion
controls rate pressure, not reliability semantics.
- Automatic network/path migration has been removed from this implementation.
- If the client changes network and its source
IP:port
changes, the current session is expected to end and the client should reconnect cleanly. - The migration implementation was removed because it caused practical reliability and security problems:
-
repeated migration floods when NAT or mobile networks changed paths quickly
-
stale sessions that looked recovered but no longer delivered media
-
long silent periods followed by GAP-only behavior
-
ambiguity between a real roaming client and spoofed packets claiming an existing
session_id -
complex recovery state that could reset stream ordering or retransmission state at the wrong time
-
The current model is intentionally simpler: prove reachability with a challenge on the current
IP:port
, bind the session to that endpoint, and reconnect if the endpoint changes.
ACK
is serialized likeACK: 10 MAC:<tag>
or batched likeACK: 10 11 12 ... MAC:<tag>
.RETRANSMIT_REQUEST
is serialized likeNACK: 42 MAC:<tag>
or batched likeNACK: 42 43 44 ... MAC:<tag>
.HELLO
is serialized likeHELLO: <base64-payload>
.CLOSE
is serialized likeCLOSE:
.DATA
uses the binaryUPACK
frame format instead of ASCII to avoid bloating media payload packets.- In AEAD mode, captures typically look like readable control lines plus encrypted
USS1...
datagrams that decrypt toUPAK...
DATA frames. - In cleartext mode, captures typically look like readable control lines plus authenticated
USC1...
datagrams carrying visibleUPACK
bytes and an HMAC tag. - The
MAC:<tag>
value is computed from the session key and is stripped after verification before the packet reaches the transport state machine.
- USTPS uses selective retransmission, not Go-Back-N.
- Every unique
DATA
packet is ACKed individually. - Missing packets trigger
RETRANSMIT_REQUEST
only for the missingseq
. - The sender keeps sent packets in a retransmission buffer until ACKed.
RTO
is not fixed-only: it is adapted from measuredRTT
samples of non-retransmitted packets.- Only the missing packets are retransmitted.
ACK
: the receiver acknowledged one or moreseq
values, so the sender can retire them from the retransmission buffer.NACK
: the receiver detected a missingseq
and explicitly requested retransmission of that missing packet only.GAP
: the client received a packet whosestream_pos
is ahead of the next ordered output position, so there is currently a hole in the logical byte stream.RECOVERY
: a late packet arrived withstream_pos
below the current frontier, meaning an earlier gap is being repaired or was repaired after newer data had already been seen.RESYNC
: the client anchored ordered output to a newstream_pos
after a clean stream-state reset.RTO
: retransmission timeout. The sender did not see ACK progress in time, so it queued a packet for retry even without an explicit NACK.no data for 10s
: the client did not receive stream data for long enough and exits so a new clean session can be started.stream state reset
: the client cleared local reorder/gap state after a clean new stream/session boundary.
- TCP has transport-level Head-of-Line blocking: if one segment is missing, later data in the same byte stream cannot be delivered to the application yet.
- USTPS does not do that at the transport layer.
- A missing packet does not stop later packets from being received, ACKed, buffered, or passed upward.
- That is why USTPS can physically observe flows like
5, 6, 4, 7, 8
while still preserving enough metadata for the application to rebuild the logical order if it wants ordered output.
Important:
-
If your final application output is a strict ordered byte stream, then reordering still has to happen somewhere above USTPS.
-
In that case, the application layer may still choose to wait before emitting bytes, but that waiting is an application behavior, not transport-level HoL blocking inside USTPS itself.
-
Treat USTPS as a reliable unordered datagram transport with stream position metadata.
-
Do not assume packet arrival order is the real stream order.
-
Use
stream_pos
to rebuild ordered output when your application needs a byte stream. - If your application can consume unordered chunks directly, you can process payloads immediately and avoid ordered buffering entirely.
- If your application needs ordered output, keep a reorder buffer keyed by
stream_pos
and release data only when the required positions are available. - Do not rebuild ordering by
seq
; useseq
only for transport reliability logic.
- TCP: TCP is reliable and ordered, but that ordering is enforced by the transport itself. A single missing segment blocks later data in the same stream.
- TCP with multiplexing above it: Even if an application multiplexes many logical channels over one TCP connection, loss in the underlying TCP byte stream still blocks progress behind the gap.
- QUIC: QUIC removes cross-stream HoL blocking between different streams, which is a big improvement over TCP for multiplexed applications.
- QUIC stream behavior: Inside one individual QUIC stream, ordering is still enforced. Missing data in that stream blocks later bytes for that same stream.
- USTPS:
USTPS does not enforce ordered delivery at the transport layer. It accepts later packets without waiting for earlier missing ones, and relies on
stream_pos
metadata if the application wants to reconstruct ordered output. IfUSTPS Congestion
is enabled, the sender may slow or speed up, but that does not change the unordered transport model.
python3 server.py \
--peer-port 0 \
--bind-ip 0.0.0.0 \
--bind-port 40001 \
--video "<HLS_URL_OR_LOCAL_FILE>" \
--stream-container mpegts \
--cipher chacha20 \
--congestion-control auto \
--cleartext auto
The default stream container is mpegts
because it is the most reliable option with VLC in the current TCP-local playback path.
You can change the FFmpeg muxer/container with --stream-container
.
Examples:
--stream-container mpegts
(default, classic MPEG-TS compatibility)--stream-container flv
(experimental here; VLC may misdetect it as audio-only in this pipeline)--stream-container nut
(low overhead FFmpeg-native streaming container, but VLC may not open it)--stream-container matroska
(MKV/Matroska)
If you want custom ffmpeg encoding/transcoding parameters instead of the default copy mode, use --video-parameters
.
Example:
python3 server.py \
--peer-port 0 \
--bind-ip 0.0.0.0 \
--bind-port 40001 \
--video "<HLS_URL_OR_LOCAL_FILE>" \
--stream-container mpegts \
--video-parameters "-c:v libx264 -preset veryfast -b:v 2500k -c:a aac -b:a 128k" \
--cipher chacha20 \
--cleartext off
Behavior:
- without
--video-parameters
: uses-c copy
- with
--stream-container mpegts
and no--video-parameters
: also adds-mpegts_flags +resend_headers
- with
--video-parameters
: uses exactly what you passed instead of the default copy settings - the selected container is always passed to FFmpeg as
-f <stream-container>
--loss
simulates outbound packet loss on the server side for testing recovery behavior.- Value range:
0
to100
- Example:
python3 server.py \
--peer-port 0 \
--bind-ip 0.0.0.0 \
--bind-port 40001 \
--video "<HLS_URL_OR_LOCAL_FILE>" \
--cipher chacha20 \
--loss 40
--loss 0
means no simulated loss.--loss 40
means the server randomly drops about 40% of its outbound packets before they leave the process.- This is useful for validating:
-
retransmission behavior
-
gap detection
-
ACK/NACK handling
-
playback resilience under controlled packet loss
-
In normal real-world usage, leave
--loss
at0
.
python3 client.py \
--peer-ip <SERVER_IP_OR_DOMAIN> \
--peer-port 40001 \
--bind-ip 0.0.0.0 \
--bind-port 0 \
--output-mode tcp \
--tcp-host 127.0.0.1 \
--tcp-port 1238 \
--cipher chacha20 \
--congestion-control off \
--cleartext off
Examples:
-
Request normal AEAD mode:
--cleartext off -
Request cleartext + HMAC mode:
--cleartext on
Notes:
- The default playout/reorder delay is now
1500ms
. - The client stores the first seen server X25519 public key in
~/.ustps_known_hosts.json
. - If that key changes later, the client aborts with a TOFU mismatch error instead of silently trusting the new key.
- If you intentionally rotated the server host key, run the client with
--regen-key
to allow replacing the stored TOFU key after interactive confirmation. - TOFU entries are stored per
<peer-ip-or-domain>:<peer-port>
, so a different server at a different address/port is treated as a different host identity.
--udp-unordered-live
is dangerous and generally not recommended for normal media players.- In that mode, payloads are forwarded immediately in raw arrival order.
- If a packet is retransmitted later, a generic player may treat that recovered payload as if it were a brand-new frame or packet instead of late data that belongs earlier in the logical stream.
- That can cause visible corruption, duplicated playback artifacts, decoder confusion, or unstable playback.
- For normal player compatibility, prefer local
TCP
output or ordered UDP output with a reorder buffer.
VLC:
tcp://127.0.0.1:1238
- USTP: reliable UDP transport, no encryption by default.
- USTPS: same UDP transport plus authenticated
DATA
protection, human-readable plaintext transport control, challenge validation before data flow, and endpoint-bound sessions. - Client exits with explicit error if no valid encrypted packets are received after the handshake finishes.
USTPS
Internet-Draft:https://datatracker.ietf.org/doc/draft-x1co-ustps/
USSH
: a shell/remote terminal protocol implemented fully from scratch on top of USTPS:https://github.com/x1colegal/USSH