1
0

fully documenting crate tuic

This commit is contained in:
EAimTY 2023-02-05 03:44:16 +09:00
parent aea3eefa02
commit a5d92381bb
18 changed files with 487 additions and 70 deletions

View File

@ -1,7 +1,15 @@
[package] [package]
name = "tuic" name = "tuic"
version = "0.1.0" version = "5.0.0-pre-alpha.0"
authors = ["EAimTY <ea.imty@gmail.com>"]
description = "Delicately-TUICed 0-RTT proxy protocol"
categories = ["network-programming"]
keywords = ["network", "proxy", "tuic"]
edition = "2021" edition = "2021"
rust-version = "1.65.0"
readme = "README.md"
license = "GPL-3.0-or-later"
repository = "https://github.com/EAimTY/tuic"
[features] [features]
async_marshal = ["bytes", "futures-util"] async_marshal = ["bytes", "futures-util"]
@ -14,6 +22,10 @@ futures-util = { version = "0.3.26", default-features = false, features = ["io",
parking_lot = { version = "0.12.1", default-features = false, optional = true } parking_lot = { version = "0.12.1", default-features = false, optional = true }
register-count = { version = "0.1.0", default-features = false, features = ["std"], optional = true } register-count = { version = "0.1.0", default-features = false, features = ["std"], optional = true }
thiserror = { version = "1.0.38", default-features = false, optional = true } thiserror = { version = "1.0.38", default-features = false, optional = true }
uuid = { version = "1.3.0", default-features = false, features = ["std"] }
[dev-dependencies] [dev-dependencies]
tuic = { path = ".", features = ["async_marshal", "marshal", "model"] } tuic = { path = ".", features = ["async_marshal", "marshal", "model"] }
[package.metadata.docs.rs]
all-features = true

23
tuic/README.md Normal file
View File

@ -0,0 +1,23 @@
# tuic
Delicately-TUICed 0-RTT proxy protocol
[![Version](https://img.shields.io/crates/v/tuic.svg?style=flat)](https://crates.io/crates/tuic)
[![Documentation](https://img.shields.io/badge/docs-release-brightgreen.svg?style=flat)](https://docs.rs/tuic)
[![License](https://img.shields.io/crates/l/tuic.svg?style=flat)](https://github.com/EAimTY/tuic/blob/dev/LICENSE)
## Overview
The TUIC protocol specification can be found in [SPEC.md](https://github.com/EAimTY/tuic/blob/dev/tuic/SPEC.md). This crate provides the low-level abstract of the TUIC protocol in Rust.
Some optional features that can be enabled:
- `model` - Provides a model of the TUIC protocol, with packet fragmentation and task counter built-in. No I/O operation is involved.
- `marshal` - Provides methods for (un)marsalling the protocol in sync flavor.
- `async_marshal` - Provides methods for (un)marsalling the protocol in async flavor.
The root of the protocol abstraction is the [`Header`](https://docs.rs/tuic/latest/tuic/enum.Header.html).
## License
GNU General Public License v3.0

197
tuic/SPEC.md Normal file
View File

@ -0,0 +1,197 @@
# TUIC Protocol
## Version
`0x05`
## Overview
The TUIC protocol relies on a multiplex-able TLS-encrypted stream. All relaying tasks are negotiated by the `Header` in `Command`s.
The protocol doesn't care about the underlying transport. However, it is mainly designed to be used with [QUIC](https://en.wikipedia.org/wiki/QUIC). See [Protocol Flow](#protocol-flow) for detailed mechanism.
All fields are in Big Endian unless otherwise noted.
## Command
```plain
+-----+------+----------+
| VER | TYPE | OPT |
+-----+------+----------+
| 1 | 1 | Variable |
+-----+------+----------+
```
where:
- `VER` - the TUIC protocol version
- `TYPE` - command type
- `OPT` - command type specific data
### Command Types
There are five types of command:
- `0x00` - `Authenticate` - for authenticating the multiplexed stream
- `0x01` - `Connect` - for establishing a TCP relay
- `0x02` - `Packet` - for relaying (fragmented part of) a UDP packet
- `0x03` - `Dissociate` - for terminating a UDP relaying session
- `0x04` - `Heartbeat` - for keeping the QUIC connection alive
Command `Connect` and `Packet` carry payload (stream / packet fragment)
### Command Type Specific Data
#### `Authenticate`
```plain
+------+-------+
| UUID | TOKEN |
+------+-------+
| 16 | 32 |
+------+-------+
```
where:
- `UUID` - client UUID
- `TOKEN` - client token. The client UUID is hashed into a 256-bit long token using [TLS Keying Material Exporter](https://www.rfc-editor.org/rfc/rfc5705) on current TLS session. While exporting, both the `label` and `context` should be the client UUID
#### `Connect`
```plain
+----------+
| ADDR |
+----------+
| Variable |
+----------+
```
where:
- `ADDR` - target address. See [Address](#address)
#### `Packet`
```plain
+----------+--------+------------+---------+------+----------+
| ASSOC_ID | PKT_ID | FRAG_TOTAL | FRAG_ID | SIZE | ADDR |
+----------+--------+------------+---------+------+----------+
| 2 | 2 | 1 | 1 | 2 | Variable |
+----------+--------+------------+---------+------+----------+
```
where:
- `ASSOC_ID` - UDP relay session ID. See [UDP relaying](#udp-relaying)
- `PKT_ID` - UDP packet ID. See [UDP relaying](#udp-relaying)
- `FRAG_TOTAL` - total number of fragments of the UDP packet
- `FRAG_ID` - fragment ID of the UDP packet
- `SIZE` - length of the (fragmented) UDP packet
- `ADDR` - target (from client) or source (from server) address. See [Address](#address)
#### `Dissociate`
```plain
+----------+
| ASSOC_ID |
+----------+
| 2 |
+----------+
```
where:
- `ASSOC_ID` - UDP relay session ID. See [UDP relaying](#udp-relaying)
#### `Heartbeat`
```plain
+-+
| |
+-+
| |
+-+
```
### `Address`
`Address` is a variable-length field that encodes the network address
```plain
+------+----------+----------+
| TYPE | ADDR | PORT |
+------+----------+----------+
| 1 | Variable | 2 |
+------+----------+----------+
```
where:
- `TYPE` - the address type
- `ADDR` - the address
- `PORT` - the port
The address type can be one of the following:
- `0xff`: None
- `0x00`: Fully-qualified domain name (the first byte indicates the length of the domain name)
- `0x01`: IPv4 address
- `0x02`: IPv6 address
Address type `None` is used in `Packet` commands that is not the first fragment of a UDP packet.
The port number is encoded in 2 bytes after the Domain name / IP address.
## Protocol Flow
This section describes the protocol flow in detail with QUIC as the underlying transport.
The TUIC protocol doesn't care about how the underlying transport is managed. It can even be integrated into other existing services, such as HTTP/3.
Here is a typical flow of the TUIC protocol on a QUIC connection:
### Authentication
The client opens a `unidirectional_stream` and sends a `Authenticate` command. This procedure can be parallelized with other commands (relaying tasks).
The server receives the `Authenticate` command and verifies the token. If the token is valid, the connection is authenticated and ready for other relaying tasks.
If the server receives other commands before the `Authenticate` command, it should only accept the command header part and pause. After the connection is authenticated, the server should resume all the paused tasks.
### TCP relaying
Command `Connect` is used for initializing a TCP relay.
The client opens a `bidirectional_stream` and sends a `Connect` command. After the command header transmission is completed, the client can start using the stream for TCP relaying, no need to wait for the server's response (server will never respond, actually).
The server receives the `Connect` command and opens a TCP stream to the target address. After the stream is established, the server can start relaying data between the TCP stream and the `bidirectional_stream`.
### UDP relaying
TUIC achieves 0-RTT Full Cone UDP forwarding by syncing UDP session ID (associate ID) between the client and the server.
Both the client and the server should create a UDP session table for each QUIC connection, mapping every associate ID to an associated UDP socket.
The associate ID is a 16-bit unsigned integer generated by the client. If the client wants to send UDP packets using the same socket of the server, the attached associate ID in the `Packet` command should be the same.
When receiving a `Packet` command, the server should check whether the attached associate ID is already associated with a UDP socket. If not, the server should allocate a UDP socket for the associate ID. The server will use this UDP socket to send UDP packets requested by the client, and accepting UDP packets from any destination at the same time, prefixing them with the `Packet` command header then sends back to the client.
A UDP packet can be fragmented into multiple `Packet` commands. Field `PKT_ID`, `FRAG_TOTAL` and `FRAG_ID` are used to identify and reassemble the fragmented UDP packets.
As a client, a `Packet` can be sent through:
- QUIC `unidirectional_stream` (UDP relay mode quic)
- QUIC `datagram` (UDP relay mode native)
When the server receives the first `Packet` from an UDP relay session (associate ID), it should use the same mode to send back the `Packet` commands.
A UDP session can be dissociated by sending a `Dissociate` command through a QUIC `unidirectional_stream` by client. The server will remove the UDP session and release the associated UDP socket.
### Heartbeat
When there is any ongoing relaying task, the client should send a `Heartbeat` command through a QUIC `datagram` periodically to keep the QUIC connection alive.
## Error Handling
Note that there is no response for any command. If the server receives a command that is not valid, or encounters any error during the processing (e.g. the target address is unreachable, authentication failure), there is no *standard* way to deal with it. The behavior is implementation-defined.

View File

@ -1,4 +1,4 @@
//! The TUIC protocol #![doc = include_str!("../README.md")]
mod protocol; mod protocol;

View File

@ -9,6 +9,7 @@ use std::{
}; };
impl Header { impl Header {
/// Marshals the header into an `AsyncWrite` stream
#[cfg(feature = "async_marshal")] #[cfg(feature = "async_marshal")]
pub async fn async_marshal(&self, s: &mut (impl AsyncWrite + Unpin)) -> Result<(), IoError> { pub async fn async_marshal(&self, s: &mut (impl AsyncWrite + Unpin)) -> Result<(), IoError> {
let mut buf = vec![0; self.len()]; let mut buf = vec![0; self.len()];
@ -16,6 +17,7 @@ impl Header {
s.write_all(&buf).await s.write_all(&buf).await
} }
/// Marshals the header into a `Write` stream
#[cfg(feature = "marshal")] #[cfg(feature = "marshal")]
pub fn marshal(&self, s: &mut impl Write) -> Result<(), IoError> { pub fn marshal(&self, s: &mut impl Write) -> Result<(), IoError> {
let mut buf = vec![0; self.len()]; let mut buf = vec![0; self.len()];
@ -23,6 +25,7 @@ impl Header {
s.write_all(&buf) s.write_all(&buf)
} }
/// Writes the header into a `BufMut`
pub fn write(&self, buf: &mut impl BufMut) { pub fn write(&self, buf: &mut impl BufMut) {
buf.put_u8(VERSION); buf.put_u8(VERSION);
buf.put_u8(self.type_code()); buf.put_u8(self.type_code());
@ -64,6 +67,7 @@ impl Address {
impl Authenticate { impl Authenticate {
fn write(&self, buf: &mut impl BufMut) { fn write(&self, buf: &mut impl BufMut) {
buf.put_slice(self.uuid().as_ref());
buf.put_slice(&self.token()); buf.put_slice(&self.token());
} }
} }

View File

@ -1,45 +1,71 @@
use super::side::{self, Side}; use super::side::{self, Side};
use crate::protocol::{Authenticate as AuthenticateHeader, Header}; use crate::protocol::{Authenticate as AuthenticateHeader, Header};
use uuid::Uuid;
/// The model of the `Authenticate` command
pub struct Authenticate<M> { pub struct Authenticate<M> {
inner: Side<Tx, Rx>, inner: Side<Tx, Rx>,
_marker: M, _marker: M,
} }
pub struct Tx { struct Tx {
header: Header, header: Header,
} }
impl Authenticate<side::Tx> { impl Authenticate<side::Tx> {
pub(super) fn new(token: [u8; 32]) -> Self { pub(super) fn new(uuid: Uuid, exporter: impl KeyingMaterialExporter) -> Self {
Self { Self {
inner: Side::Tx(Tx { inner: Side::Tx(Tx {
header: Header::Authenticate(AuthenticateHeader::new(token)), header: Header::Authenticate(AuthenticateHeader::new(
uuid,
exporter.export_keying_material(uuid.as_ref(), uuid.as_ref()),
)),
}), }),
_marker: side::Tx, _marker: side::Tx,
} }
} }
/// Returns the header of the `Authenticate` command
pub fn header(&self) -> &Header { pub fn header(&self) -> &Header {
let Side::Tx(tx) = &self.inner else { unreachable!() }; let Side::Tx(tx) = &self.inner else { unreachable!() };
&tx.header &tx.header
} }
} }
pub struct Rx { struct Rx {
uuid: Uuid,
token: [u8; 32], token: [u8; 32],
} }
impl Authenticate<side::Rx> { impl Authenticate<side::Rx> {
pub(super) fn new(token: [u8; 32]) -> Self { pub(super) fn new(uuid: Uuid, token: [u8; 32]) -> Self {
Self { Self {
inner: Side::Rx(Rx { token }), inner: Side::Rx(Rx { uuid, token }),
_marker: side::Rx, _marker: side::Rx,
} }
} }
/// Returns the UUID of the peer
pub fn uuid(&self) -> Uuid {
let Side::Rx(rx) = &self.inner else { unreachable!() };
rx.uuid
}
/// Returns the token of the peer
pub fn token(&self) -> [u8; 32] { pub fn token(&self) -> [u8; 32] {
let Side::Rx(rx) = &self.inner else { unreachable!() }; let Side::Rx(rx) = &self.inner else { unreachable!() };
rx.token rx.token
} }
/// Returns whether the token is valid
pub fn is_valid(&self, exporter: impl KeyingMaterialExporter) -> bool {
let Side::Rx(rx) = &self.inner else { unreachable!() };
rx.token == exporter.export_keying_material(rx.uuid.as_ref(), rx.uuid.as_ref())
}
}
/// The trait for exporting keying material
pub trait KeyingMaterialExporter {
/// Exports keying material
fn export_keying_material(&self, label: &[u8], context: &[u8]) -> [u8; 32];
} }

View File

@ -2,6 +2,7 @@ use super::side::{self, Side};
use crate::protocol::{Address, Connect as ConnectHeader, Header}; use crate::protocol::{Address, Connect as ConnectHeader, Header};
use register_count::Register; use register_count::Register;
/// The model of the `Connect` command
pub struct Connect<M> { pub struct Connect<M> {
inner: Side<Tx, Rx>, inner: Side<Tx, Rx>,
_marker: M, _marker: M,
@ -23,6 +24,7 @@ impl Connect<side::Tx> {
} }
} }
/// Returns the header of the `Connect` command
pub fn header(&self) -> &Header { pub fn header(&self) -> &Header {
let Side::Tx(tx) = &self.inner else { unreachable!() }; let Side::Tx(tx) = &self.inner else { unreachable!() };
&tx.header &tx.header
@ -45,6 +47,7 @@ impl Connect<side::Rx> {
} }
} }
/// Returns the address
pub fn addr(&self) -> &Address { pub fn addr(&self) -> &Address {
let Side::Rx(rx) = &self.inner else { unreachable!() }; let Side::Rx(rx) = &self.inner else { unreachable!() };
&rx.addr &rx.addr

View File

@ -1,12 +1,13 @@
use super::side::{self, Side}; use super::side::{self, Side};
use crate::protocol::{Dissociate as DissociateHeader, Header}; use crate::protocol::{Dissociate as DissociateHeader, Header};
/// The model of the `Dissociate` command
pub struct Dissociate<M> { pub struct Dissociate<M> {
inner: Side<Tx, Rx>, inner: Side<Tx, Rx>,
_marker: M, _marker: M,
} }
pub struct Tx { struct Tx {
header: Header, header: Header,
} }
@ -20,13 +21,14 @@ impl Dissociate<side::Tx> {
} }
} }
/// Returns the header of the `Dissociate` command
pub fn header(&self) -> &Header { pub fn header(&self) -> &Header {
let Side::Tx(tx) = &self.inner else { unreachable!() }; let Side::Tx(tx) = &self.inner else { unreachable!() };
&tx.header &tx.header
} }
} }
pub struct Rx { struct Rx {
assoc_id: u16, assoc_id: u16,
} }
@ -38,6 +40,7 @@ impl Dissociate<side::Rx> {
} }
} }
/// Returns the UDP session ID
pub fn assoc_id(&self) -> u16 { pub fn assoc_id(&self) -> u16 {
let Side::Rx(rx) = &self.inner else { unreachable!() }; let Side::Rx(rx) = &self.inner else { unreachable!() };
rx.assoc_id rx.assoc_id

View File

@ -6,7 +6,7 @@ pub struct Heartbeat<M> {
_marker: M, _marker: M,
} }
pub struct Tx { struct Tx {
header: Header, header: Header,
} }
@ -20,13 +20,14 @@ impl Heartbeat<side::Tx> {
} }
} }
/// Returns the header of the `Heartbeat` command
pub fn header(&self) -> &Header { pub fn header(&self) -> &Header {
let Side::Tx(tx) = &self.inner else { unreachable!() }; let Side::Tx(tx) = &self.inner else { unreachable!() };
&tx.header &tx.header
} }
} }
pub struct Rx; struct Rx;
impl Heartbeat<side::Rx> { impl Heartbeat<side::Rx> {
pub(super) fn new() -> Self { pub(super) fn new() -> Self {

View File

@ -1,3 +1,5 @@
//! An abstraction of a TUIC connection, with packet fragmentation management and task counters. No I/O operation is involved internally
use crate::protocol::{ use crate::protocol::{
Address, Authenticate as AuthenticateHeader, Connect as ConnectHeader, Address, Authenticate as AuthenticateHeader, Connect as ConnectHeader,
Dissociate as DissociateHeader, Heartbeat as HeartbeatHeader, Packet as PacketHeader, Dissociate as DissociateHeader, Heartbeat as HeartbeatHeader, Packet as PacketHeader,
@ -14,6 +16,7 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use thiserror::Error; use thiserror::Error;
use uuid::Uuid;
mod authenticate; mod authenticate;
mod connect; mod connect;
@ -22,14 +25,14 @@ mod heartbeat;
mod packet; mod packet;
pub use self::{ pub use self::{
authenticate::Authenticate, authenticate::{Authenticate, KeyingMaterialExporter},
connect::Connect, connect::Connect,
dissociate::Dissociate, dissociate::Dissociate,
heartbeat::Heartbeat, heartbeat::Heartbeat,
packet::{Fragments, Packet}, packet::{Fragments, Packet},
}; };
#[derive(Clone)] /// An abstraction of a TUIC connection, with packet fragmentation management and task counters. No I/O operation is involved internally
pub struct Connection<B> { pub struct Connection<B> {
udp_sessions: Arc<Mutex<UdpSessions<B>>>, udp_sessions: Arc<Mutex<UdpSessions<B>>>,
task_connect_count: Counter, task_connect_count: Counter,
@ -40,6 +43,7 @@ impl<B> Connection<B>
where where
B: AsRef<[u8]>, B: AsRef<[u8]>,
{ {
/// Creates a new `Connection`
#[allow(clippy::new_without_default)] #[allow(clippy::new_without_default)]
pub fn new() -> Self { pub fn new() -> Self {
let task_associate_count = Counter::new(); let task_associate_count = Counter::new();
@ -51,24 +55,33 @@ where
} }
} }
pub fn send_authenticate(&self, token: [u8; 32]) -> Authenticate<side::Tx> { /// Sends an `Authenticate`
Authenticate::<side::Tx>::new(token) pub fn send_authenticate(
&self,
uuid: Uuid,
exporter: impl KeyingMaterialExporter,
) -> Authenticate<side::Tx> {
Authenticate::<side::Tx>::new(uuid, exporter)
} }
/// Receives an `Authenticate`
pub fn recv_authenticate(&self, header: AuthenticateHeader) -> Authenticate<side::Rx> { pub fn recv_authenticate(&self, header: AuthenticateHeader) -> Authenticate<side::Rx> {
let (token,) = header.into(); let (uuid, token) = header.into();
Authenticate::<side::Rx>::new(token) Authenticate::<side::Rx>::new(uuid, token)
} }
/// Sends a `Connect`
pub fn send_connect(&self, addr: Address) -> Connect<side::Tx> { pub fn send_connect(&self, addr: Address) -> Connect<side::Tx> {
Connect::<side::Tx>::new(self.task_connect_count.reg(), addr) Connect::<side::Tx>::new(self.task_connect_count.reg(), addr)
} }
/// Receives a `Connect`
pub fn recv_connect(&self, header: ConnectHeader) -> Connect<side::Rx> { pub fn recv_connect(&self, header: ConnectHeader) -> Connect<side::Rx> {
let (addr,) = header.into(); let (addr,) = header.into();
Connect::<side::Rx>::new(self.task_connect_count.reg(), addr) Connect::<side::Rx>::new(self.task_connect_count.reg(), addr)
} }
/// Sends a `Packet`
pub fn send_packet( pub fn send_packet(
&self, &self,
assoc_id: u16, assoc_id: u16,
@ -80,6 +93,7 @@ where
.send_packet(assoc_id, addr, max_pkt_size) .send_packet(assoc_id, addr, max_pkt_size)
} }
/// Receives a `Packet`. If the association ID is not found, returns `None`
pub fn recv_packet(&self, header: PacketHeader) -> Option<Packet<side::Rx, B>> { pub fn recv_packet(&self, header: PacketHeader) -> Option<Packet<side::Rx, B>> {
let (assoc_id, pkt_id, frag_total, frag_id, size, addr) = header.into(); let (assoc_id, pkt_id, frag_total, frag_id, size, addr) = header.into();
self.udp_sessions.lock().recv_packet( self.udp_sessions.lock().recv_packet(
@ -93,6 +107,7 @@ where
) )
} }
/// Receives a `Packet` without checking the association ID
pub fn recv_packet_unrestricted(&self, header: PacketHeader) -> Packet<side::Rx, B> { pub fn recv_packet_unrestricted(&self, header: PacketHeader) -> Packet<side::Rx, B> {
let (assoc_id, pkt_id, frag_total, frag_id, size, addr) = header.into(); let (assoc_id, pkt_id, frag_total, frag_id, size, addr) = header.into();
self.udp_sessions.lock().recv_packet_unrestricted( self.udp_sessions.lock().recv_packet_unrestricted(
@ -106,39 +121,49 @@ where
) )
} }
/// Sends a `Dissociate`
pub fn send_dissociate(&self, assoc_id: u16) -> Dissociate<side::Tx> { pub fn send_dissociate(&self, assoc_id: u16) -> Dissociate<side::Tx> {
self.udp_sessions.lock().send_dissociate(assoc_id) self.udp_sessions.lock().send_dissociate(assoc_id)
} }
/// Receives a `Dissociate`
pub fn recv_dissociate(&self, header: DissociateHeader) -> Dissociate<side::Rx> { pub fn recv_dissociate(&self, header: DissociateHeader) -> Dissociate<side::Rx> {
let (assoc_id,) = header.into(); let (assoc_id,) = header.into();
self.udp_sessions.lock().recv_dissociate(assoc_id) self.udp_sessions.lock().recv_dissociate(assoc_id)
} }
/// Sends a `Heartbeat`
pub fn send_heartbeat(&self) -> Heartbeat<side::Tx> { pub fn send_heartbeat(&self) -> Heartbeat<side::Tx> {
Heartbeat::<side::Tx>::new() Heartbeat::<side::Tx>::new()
} }
/// Receives a `Heartbeat`
pub fn recv_heartbeat(&self, header: HeartbeatHeader) -> Heartbeat<side::Rx> { pub fn recv_heartbeat(&self, header: HeartbeatHeader) -> Heartbeat<side::Rx> {
let () = header.into(); let () = header.into();
Heartbeat::<side::Rx>::new() Heartbeat::<side::Rx>::new()
} }
/// Returns the number of `Connect` tasks
pub fn task_connect_count(&self) -> usize { pub fn task_connect_count(&self) -> usize {
self.task_connect_count.count() self.task_connect_count.count()
} }
/// Returns the number of active UDP sessions
pub fn task_associate_count(&self) -> usize { pub fn task_associate_count(&self) -> usize {
self.task_associate_count.count() self.task_associate_count.count()
} }
/// Removes fragments that can not be reassembled within the specified timeout
pub fn collect_garbage(&self, timeout: Duration) { pub fn collect_garbage(&self, timeout: Duration) {
self.udp_sessions.lock().collect_garbage(timeout); self.udp_sessions.lock().collect_garbage(timeout);
} }
} }
/// Abstracts the side of a task
pub mod side { pub mod side {
/// The side of a task that sends data
pub struct Tx; pub struct Tx;
/// The side of a task that receives data
pub struct Rx; pub struct Rx;
pub(super) enum Side<T, R> { pub(super) enum Side<T, R> {
@ -392,6 +417,7 @@ where
} }
} }
/// A complete packet that can be assembled
pub struct Assemblable<B> { pub struct Assemblable<B> {
buf: Vec<Option<B>>, buf: Vec<Option<B>>,
addr: Address, addr: Address,
@ -420,6 +446,7 @@ where
} }
} }
/// A trait for assembling a packet
pub trait Assembler<B> pub trait Assembler<B>
where where
Self: Sized, Self: Sized,
@ -439,6 +466,7 @@ where
} }
} }
/// An error that can occur when assembling a packet
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum AssembleError { pub enum AssembleError {
#[error("invalid fragment id {1} in total {0} fragments")] #[error("invalid fragment id {1} in total {0} fragments")]

View File

@ -11,7 +11,7 @@ pub struct Packet<M, B> {
_marker: M, _marker: M,
} }
pub struct Tx { struct Tx {
assoc_id: u16, assoc_id: u16,
pkt_id: u16, pkt_id: u16,
addr: Address, addr: Address,
@ -31,6 +31,7 @@ impl<B> Packet<side::Tx, B> {
} }
} }
/// Fragment the payload into multiple packets
pub fn into_fragments<'a, P>(self, payload: P) -> Fragments<'a, P> pub fn into_fragments<'a, P>(self, payload: P) -> Fragments<'a, P>
where where
P: AsRef<[u8]>, P: AsRef<[u8]>,
@ -39,18 +40,20 @@ impl<B> Packet<side::Tx, B> {
Fragments::new(tx.assoc_id, tx.pkt_id, tx.addr, tx.max_pkt_size, payload) Fragments::new(tx.assoc_id, tx.pkt_id, tx.addr, tx.max_pkt_size, payload)
} }
/// Returns the UDP session ID
pub fn assoc_id(&self) -> u16 { pub fn assoc_id(&self) -> u16 {
let Side::Tx(tx) = &self.inner else { unreachable!() }; let Side::Tx(tx) = &self.inner else { unreachable!() };
tx.assoc_id tx.assoc_id
} }
/// Returns the address
pub fn addr(&self) -> &Address { pub fn addr(&self) -> &Address {
let Side::Tx(tx) = &self.inner else { unreachable!() }; let Side::Tx(tx) = &self.inner else { unreachable!() };
&tx.addr &tx.addr
} }
} }
pub struct Rx<B> { struct Rx<B> {
sessions: Arc<Mutex<UdpSessions<B>>>, sessions: Arc<Mutex<UdpSessions<B>>>,
assoc_id: u16, assoc_id: u16,
pkt_id: u16, pkt_id: u16,
@ -118,6 +121,7 @@ where
} }
} }
/// Iterator over fragments of a packet
pub struct Fragments<'a, P> pub struct Fragments<'a, P>
where where
P: 'a, P: 'a,

View File

@ -1,36 +1,56 @@
// +-------+ use uuid::Uuid;
// | TOKEN |
// +-------+ /// Command `Authenticate`
// | 32 | /// ```plain
// +-------+ /// +------+-------+
/// | UUID | TOKEN |
/// +------+-------+
/// | 16 | 32 |
/// +------+-------+
/// ```
///
/// where:
///
/// - `UUID` - client UUID
/// - `TOKEN` - client token. The client UUID is hashed into a 256-bit long token using [TLS Keying Material Exporter](https://www.rfc-editor.org/rfc/rfc5705) on current TLS session. While exporting, both the `label` and `context` should be the client UUID
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Authenticate { pub struct Authenticate {
uuid: Uuid,
token: [u8; 32], token: [u8; 32],
} }
impl Authenticate { impl Authenticate {
const TYPE_CODE: u8 = 0x00; const TYPE_CODE: u8 = 0x00;
pub const fn new(token: [u8; 32]) -> Self { /// Creates a new `Authenticate` command
Self { token } pub const fn new(uuid: Uuid, token: [u8; 32]) -> Self {
Self { uuid, token }
} }
/// Returns the UUID
pub fn uuid(&self) -> Uuid {
self.uuid
}
/// Returns the token
pub fn token(&self) -> [u8; 32] { pub fn token(&self) -> [u8; 32] {
self.token self.token
} }
/// Returns the command type code
pub const fn type_code() -> u8 { pub const fn type_code() -> u8 {
Self::TYPE_CODE Self::TYPE_CODE
} }
/// Returns the serialized length of the command
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
32 16 + 32
} }
} }
impl From<Authenticate> for ([u8; 32],) { impl From<Authenticate> for (Uuid, [u8; 32]) {
fn from(auth: Authenticate) -> Self { fn from(auth: Authenticate) -> Self {
(auth.token,) (auth.uuid, auth.token)
} }
} }

View File

@ -1,10 +1,17 @@
use super::Address; use super::Address;
// +----------+ /// Command `Connect`
// | ADDR | /// ```plain
// +----------+ /// +----------+
// | Variable | /// | ADDR |
// +----------+ /// +----------+
/// | Variable |
/// +----------+
/// ```
///
/// where:
///
/// - `ADDR` - target address
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Connect { pub struct Connect {
addr: Address, addr: Address,
@ -13,18 +20,22 @@ pub struct Connect {
impl Connect { impl Connect {
const TYPE_CODE: u8 = 0x01; const TYPE_CODE: u8 = 0x01;
/// Creates a new `Connect` command
pub const fn new(addr: Address) -> Self { pub const fn new(addr: Address) -> Self {
Self { addr } Self { addr }
} }
/// Returns the address
pub fn addr(&self) -> &Address { pub fn addr(&self) -> &Address {
&self.addr &self.addr
} }
/// Returns the command type code
pub const fn type_code() -> u8 { pub const fn type_code() -> u8 {
Self::TYPE_CODE Self::TYPE_CODE
} }
/// Returns the serialized length of the command
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.addr.len() self.addr.len()

View File

@ -1,8 +1,16 @@
// +----------+ /// Command `Dissociate`
// | ASSOC_ID | ///
// +----------+ /// ```plain
// | 2 | /// +----------+
// +----------+ /// | ASSOC_ID |
/// +----------+
/// | 2 |
/// +----------+
/// ```
///
/// where:
///
/// - `ASSOC_ID` - UDP relay session ID
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Dissociate { pub struct Dissociate {
assoc_id: u16, assoc_id: u16,
@ -11,18 +19,22 @@ pub struct Dissociate {
impl Dissociate { impl Dissociate {
const TYPE_CODE: u8 = 0x03; const TYPE_CODE: u8 = 0x03;
/// Creates a new `Dissociate` command
pub const fn new(assoc_id: u16) -> Self { pub const fn new(assoc_id: u16) -> Self {
Self { assoc_id } Self { assoc_id }
} }
/// Returns the UDP relay session ID
pub fn assoc_id(&self) -> u16 { pub fn assoc_id(&self) -> u16 {
self.assoc_id self.assoc_id
} }
/// Returns the command type code
pub const fn type_code() -> u8 { pub const fn type_code() -> u8 {
Self::TYPE_CODE Self::TYPE_CODE
} }
/// Returns the serialized length of the command
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
2 2

View File

@ -1,22 +1,28 @@
// +-+ /// Command `Heartbeat`
// | | /// ```plain
// +-+ /// +-+
// | | /// | |
// +-+ /// +-+
/// | |
/// +-+
/// ```
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Heartbeat; pub struct Heartbeat;
impl Heartbeat { impl Heartbeat {
const TYPE_CODE: u8 = 0x04; const TYPE_CODE: u8 = 0x04;
/// Creates a new `Heartbeat` command
pub const fn new() -> Self { pub const fn new() -> Self {
Self Self
} }
/// Returns the command type code
pub const fn type_code() -> u8 { pub const fn type_code() -> u8 {
Self::TYPE_CODE Self::TYPE_CODE
} }
/// Returns the serialized length of the command
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
0 0

View File

@ -15,17 +15,35 @@ pub use self::{
packet::Packet, packet::Packet,
}; };
/// The TUIC protocol version
pub const VERSION: u8 = 0x05; pub const VERSION: u8 = 0x05;
/// Header /// The command header for negotiating tasks
///
/// ```plain /// ```plain
/// +-----+----------+----------+ /// +-----+------+----------+
/// | VER | TYPE | OPT | /// | VER | TYPE | OPT |
/// +-----+----------+----------+ /// +-----+------+----------+
/// | 1 | 1 | Variable | /// | 1 | 1 | Variable |
/// +-----+----------+----------+ /// +-----+------+----------+
/// ``` /// ```
///
/// where:
///
/// - `VER` - the TUIC protocol version
/// - `TYPE` - command type
/// - `OPT` - command type specific data
///
/// ## Command Types
///
/// There are five types of command:
///
/// - `0x00` - `Authenticate` - for authenticating the multiplexed stream
/// - `0x01` - `Connect` - for establishing a TCP relay
/// - `0x02` - `Packet` - for relaying (fragmented part of) a UDP packet
/// - `0x03` - `Dissociate` - for terminating a UDP relaying session
/// - `0x04` - `Heartbeat` - for keeping the QUIC connection alive
///
/// Command `Connect` and `Packet` carry payload (stream / packet fragment)
#[non_exhaustive] #[non_exhaustive]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Header { pub enum Header {
@ -43,6 +61,7 @@ impl Header {
pub const TYPE_CODE_DISSOCIATE: u8 = Dissociate::type_code(); pub const TYPE_CODE_DISSOCIATE: u8 = Dissociate::type_code();
pub const TYPE_CODE_HEARTBEAT: u8 = Heartbeat::type_code(); pub const TYPE_CODE_HEARTBEAT: u8 = Heartbeat::type_code();
/// Returns the command type code
pub const fn type_code(&self) -> u8 { pub const fn type_code(&self) -> u8 {
match self { match self {
Self::Authenticate(_) => Authenticate::type_code(), Self::Authenticate(_) => Authenticate::type_code(),
@ -53,6 +72,7 @@ impl Header {
} }
} }
/// Returns the serialized length of the command
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
2 + match self { 2 + match self {
@ -65,22 +85,30 @@ impl Header {
} }
} }
/// Address /// Variable-length field that encodes the network address
/// ///
/// ```plain /// ```plain
/// +------+----------+ /// +------+----------+----------+
/// | TYPE | ADDR | /// | TYPE | ADDR | PORT |
/// +------+----------+ /// +------+----------+----------+
/// | 1 | Variable | /// | 1 | Variable | 2 |
/// +------+----------+ /// +------+----------+----------+
/// ``` /// ```
/// ///
/// where:
///
/// - `TYPE` - the address type
/// - `ADDR` - the address
/// - `PORT` - the port
///
/// The address type can be one of the following: /// The address type can be one of the following:
/// ///
/// - 0xff: None /// - `0xff`: None
/// - 0x00: Fully-qualified domain name (the first byte indicates the length of the domain name) /// - `0x00`: Fully-qualified domain name (the first byte indicates the length of the domain name)
/// - 0x01: IPv4 address /// - `0x01`: IPv4 address
/// - 0x02: IPv6 address /// - `0x02`: IPv6 address
///
/// Address type `None` is used in `Packet` commands that is not the first fragment of a UDP packet.
/// ///
/// The port number is encoded in 2 bytes after the Domain name / IP address. /// The port number is encoded in 2 bytes after the Domain name / IP address.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
@ -96,6 +124,7 @@ impl Address {
pub const TYPE_CODE_IPV4: u8 = 0x01; pub const TYPE_CODE_IPV4: u8 = 0x01;
pub const TYPE_CODE_IPV6: u8 = 0x02; pub const TYPE_CODE_IPV6: u8 = 0x02;
/// Returns the address type code
pub const fn type_code(&self) -> u8 { pub const fn type_code(&self) -> u8 {
match self { match self {
Self::None => Self::TYPE_CODE_NONE, Self::None => Self::TYPE_CODE_NONE,
@ -107,6 +136,7 @@ impl Address {
} }
} }
/// Returns the serialized length of the address
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
1 + match self { 1 + match self {
@ -119,22 +149,27 @@ impl Address {
} }
} }
/// Takes the address out, leaving a `None` in its place
pub fn take(&mut self) -> Self { pub fn take(&mut self) -> Self {
mem::take(self) mem::take(self)
} }
/// Returns `true` if the address is `None`
pub fn is_none(&self) -> bool { pub fn is_none(&self) -> bool {
matches!(self, Self::None) matches!(self, Self::None)
} }
/// Returns `true` if the address is a fully-qualified domain name
pub fn is_domain(&self) -> bool { pub fn is_domain(&self) -> bool {
matches!(self, Self::DomainAddress(_, _)) matches!(self, Self::DomainAddress(_, _))
} }
/// Returns `true` if the address is an IPv4 address
pub fn is_ipv4(&self) -> bool { pub fn is_ipv4(&self) -> bool {
matches!(self, Self::SocketAddress(SocketAddr::V4(_))) matches!(self, Self::SocketAddress(SocketAddr::V4(_)))
} }
/// Returns `true` if the address is an IPv6 address
pub fn is_ipv6(&self) -> bool { pub fn is_ipv6(&self) -> bool {
matches!(self, Self::SocketAddress(SocketAddr::V6(_))) matches!(self, Self::SocketAddress(SocketAddr::V6(_)))
} }

View File

@ -1,10 +1,22 @@
use super::Address; use super::Address;
// +----------+--------+------------+---------+------+----------+ /// Command `Packet`
// | ASSOC_ID | PKT_ID | FRAG_TOTAL | FRAG_ID | SIZE | ADDR | /// ```plain
// +----------+--------+------------+---------+------+----------+ /// +----------+--------+------------+---------+------+----------+
// | 2 | 2 | 1 | 1 | 2 | Variable | /// | ASSOC_ID | PKT_ID | FRAG_TOTAL | FRAG_ID | SIZE | ADDR |
// +----------+--------+------------+---------+------+----------+ /// +----------+--------+------------+---------+------+----------+
/// | 2 | 2 | 1 | 1 | 2 | Variable |
/// +----------+--------+------------+---------+------+----------+
/// ```
///
/// where:
///
/// - `ASSOC_ID` - UDP relay session ID
/// - `PKT_ID` - UDP packet ID
/// - `FRAG_TOTAL` - total number of fragments of the UDP packet
/// - `FRAG_ID` - fragment ID of the UDP packet
/// - `SIZE` - length of the (fragmented) UDP packet
/// - `ADDR` - target (from client) or source (from server) address
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Packet { pub struct Packet {
assoc_id: u16, assoc_id: u16,
@ -18,6 +30,7 @@ pub struct Packet {
impl Packet { impl Packet {
const TYPE_CODE: u8 = 0x02; const TYPE_CODE: u8 = 0x02;
/// Creates a new `Packet` command
pub const fn new( pub const fn new(
assoc_id: u16, assoc_id: u16,
pkt_id: u16, pkt_id: u16,
@ -36,39 +49,48 @@ impl Packet {
} }
} }
/// Returns the UDP relay session ID
pub fn assoc_id(&self) -> u16 { pub fn assoc_id(&self) -> u16 {
self.assoc_id self.assoc_id
} }
/// Returns the packet ID
pub fn pkt_id(&self) -> u16 { pub fn pkt_id(&self) -> u16 {
self.pkt_id self.pkt_id
} }
/// Returns the total number of fragments of the UDP packet
pub fn frag_total(&self) -> u8 { pub fn frag_total(&self) -> u8 {
self.frag_total self.frag_total
} }
/// Returns the fragment ID of the UDP packet
pub fn frag_id(&self) -> u8 { pub fn frag_id(&self) -> u8 {
self.frag_id self.frag_id
} }
/// Returns the length of the (fragmented) UDP packet
pub fn size(&self) -> u16 { pub fn size(&self) -> u16 {
self.size self.size
} }
/// Returns the target (from client) or source (from server) address
pub fn addr(&self) -> &Address { pub fn addr(&self) -> &Address {
&self.addr &self.addr
} }
/// Returns the command type code
pub const fn type_code() -> u8 { pub const fn type_code() -> u8 {
Self::TYPE_CODE Self::TYPE_CODE
} }
/// Returns the serialized length of the command
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
Self::len_without_addr() + self.addr.len() Self::len_without_addr() + self.addr.len()
} }
/// Returns the serialized length of the command without the address
pub const fn len_without_addr() -> usize { pub const fn len_without_addr() -> usize {
2 + 2 + 1 + 1 + 2 2 + 2 + 1 + 1 + 2
} }

View File

@ -8,8 +8,10 @@ use std::{
string::FromUtf8Error, string::FromUtf8Error,
}; };
use thiserror::Error; use thiserror::Error;
use uuid::{Error as UuidError, Uuid};
impl Header { impl Header {
/// Unmarshals a header from an `AsyncRead` stream
#[cfg(feature = "async_marshal")] #[cfg(feature = "async_marshal")]
pub async fn async_unmarshal(s: &mut (impl AsyncRead + Unpin)) -> Result<Self, UnmarshalError> { pub async fn async_unmarshal(s: &mut (impl AsyncRead + Unpin)) -> Result<Self, UnmarshalError> {
let mut buf = [0; 1]; let mut buf = [0; 1];
@ -36,6 +38,7 @@ impl Header {
} }
} }
/// Unmarshals a header from a `Read` stream
#[cfg(feature = "marshal")] #[cfg(feature = "marshal")]
pub fn unmarshal(s: &mut impl Read) -> Result<Self, UnmarshalError> { pub fn unmarshal(s: &mut impl Read) -> Result<Self, UnmarshalError> {
let mut buf = [0; 1]; let mut buf = [0; 1];
@ -164,16 +167,20 @@ impl Address {
impl Authenticate { impl Authenticate {
#[cfg(feature = "async_marshal")] #[cfg(feature = "async_marshal")]
async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result<Self, UnmarshalError> { async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result<Self, UnmarshalError> {
let mut buf = [0; 32]; let mut buf = [0; 48];
s.read_exact(&mut buf).await?; s.read_exact(&mut buf).await?;
Ok(Self::new(buf)) let uuid = Uuid::from_slice(&buf[..16])?;
let token = TryFrom::try_from(&buf[16..]).unwrap();
Ok(Self::new(uuid, token))
} }
#[cfg(feature = "marshal")] #[cfg(feature = "marshal")]
fn read(s: &mut impl Read) -> Result<Self, UnmarshalError> { fn read(s: &mut impl Read) -> Result<Self, UnmarshalError> {
let mut buf = [0; 32]; let mut buf = [0; 32];
s.read_exact(&mut buf)?; s.read_exact(&mut buf)?;
Ok(Self::new(buf)) let uuid = Uuid::from_slice(&buf[..16])?;
let token = TryFrom::try_from(&buf[16..]).unwrap();
Ok(Self::new(uuid, token))
} }
} }
@ -251,6 +258,7 @@ impl Heartbeat {
} }
} }
/// Errors that can occur when unmarshalling a packet
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum UnmarshalError { pub enum UnmarshalError {
#[error(transparent)] #[error(transparent)]
@ -259,6 +267,8 @@ pub enum UnmarshalError {
InvalidVersion(u8), InvalidVersion(u8),
#[error("invalid command: {0}")] #[error("invalid command: {0}")]
InvalidCommand(u8), InvalidCommand(u8),
#[error("invalid UUID: {0}")]
InvalidUuid(#[from] UuidError),
#[error("invalid address type: {0}")] #[error("invalid address type: {0}")]
InvalidAddressType(u8), InvalidAddressType(u8),
#[error("address parsing error: {0}")] #[error("address parsing error: {0}")]