diff --git a/tuic/Cargo.toml b/tuic/Cargo.toml index d7f8606..0fb1a00 100644 --- a/tuic/Cargo.toml +++ b/tuic/Cargo.toml @@ -1,7 +1,15 @@ [package] name = "tuic" -version = "0.1.0" +version = "5.0.0-pre-alpha.0" +authors = ["EAimTY "] +description = "Delicately-TUICed 0-RTT proxy protocol" +categories = ["network-programming"] +keywords = ["network", "proxy", "tuic"] edition = "2021" +rust-version = "1.65.0" +readme = "README.md" +license = "GPL-3.0-or-later" +repository = "https://github.com/EAimTY/tuic" [features] 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 } register-count = { version = "0.1.0", default-features = false, features = ["std"], optional = true } thiserror = { version = "1.0.38", default-features = false, optional = true } +uuid = { version = "1.3.0", default-features = false, features = ["std"] } [dev-dependencies] tuic = { path = ".", features = ["async_marshal", "marshal", "model"] } + +[package.metadata.docs.rs] +all-features = true diff --git a/tuic/README.md b/tuic/README.md new file mode 100644 index 0000000..1d266d1 --- /dev/null +++ b/tuic/README.md @@ -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 diff --git a/tuic/SPEC.md b/tuic/SPEC.md new file mode 100644 index 0000000..09ec73b --- /dev/null +++ b/tuic/SPEC.md @@ -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. diff --git a/tuic/src/lib.rs b/tuic/src/lib.rs index bb3aaf5..d857d9c 100644 --- a/tuic/src/lib.rs +++ b/tuic/src/lib.rs @@ -1,4 +1,4 @@ -//! The TUIC protocol +#![doc = include_str!("../README.md")] mod protocol; diff --git a/tuic/src/marshal.rs b/tuic/src/marshal.rs index 94a9068..c87a590 100644 --- a/tuic/src/marshal.rs +++ b/tuic/src/marshal.rs @@ -9,6 +9,7 @@ use std::{ }; impl Header { + /// Marshals the header into an `AsyncWrite` stream #[cfg(feature = "async_marshal")] pub async fn async_marshal(&self, s: &mut (impl AsyncWrite + Unpin)) -> Result<(), IoError> { let mut buf = vec![0; self.len()]; @@ -16,6 +17,7 @@ impl Header { s.write_all(&buf).await } + /// Marshals the header into a `Write` stream #[cfg(feature = "marshal")] pub fn marshal(&self, s: &mut impl Write) -> Result<(), IoError> { let mut buf = vec![0; self.len()]; @@ -23,6 +25,7 @@ impl Header { s.write_all(&buf) } + /// Writes the header into a `BufMut` pub fn write(&self, buf: &mut impl BufMut) { buf.put_u8(VERSION); buf.put_u8(self.type_code()); @@ -64,6 +67,7 @@ impl Address { impl Authenticate { fn write(&self, buf: &mut impl BufMut) { + buf.put_slice(self.uuid().as_ref()); buf.put_slice(&self.token()); } } diff --git a/tuic/src/model/authenticate.rs b/tuic/src/model/authenticate.rs index 9dcb965..b49c3d2 100644 --- a/tuic/src/model/authenticate.rs +++ b/tuic/src/model/authenticate.rs @@ -1,45 +1,71 @@ use super::side::{self, Side}; use crate::protocol::{Authenticate as AuthenticateHeader, Header}; +use uuid::Uuid; +/// The model of the `Authenticate` command pub struct Authenticate { inner: Side, _marker: M, } -pub struct Tx { +struct Tx { header: Header, } impl Authenticate { - pub(super) fn new(token: [u8; 32]) -> Self { + pub(super) fn new(uuid: Uuid, exporter: impl KeyingMaterialExporter) -> Self { Self { 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, } } + /// Returns the header of the `Authenticate` command pub fn header(&self) -> &Header { let Side::Tx(tx) = &self.inner else { unreachable!() }; &tx.header } } -pub struct Rx { +struct Rx { + uuid: Uuid, token: [u8; 32], } impl Authenticate { - pub(super) fn new(token: [u8; 32]) -> Self { + pub(super) fn new(uuid: Uuid, token: [u8; 32]) -> Self { Self { - inner: Side::Rx(Rx { token }), + inner: Side::Rx(Rx { uuid, token }), _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] { let Side::Rx(rx) = &self.inner else { unreachable!() }; 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]; } diff --git a/tuic/src/model/connect.rs b/tuic/src/model/connect.rs index 672cb59..a40c89b 100644 --- a/tuic/src/model/connect.rs +++ b/tuic/src/model/connect.rs @@ -2,6 +2,7 @@ use super::side::{self, Side}; use crate::protocol::{Address, Connect as ConnectHeader, Header}; use register_count::Register; +/// The model of the `Connect` command pub struct Connect { inner: Side, _marker: M, @@ -23,6 +24,7 @@ impl Connect { } } + /// Returns the header of the `Connect` command pub fn header(&self) -> &Header { let Side::Tx(tx) = &self.inner else { unreachable!() }; &tx.header @@ -45,6 +47,7 @@ impl Connect { } } + /// Returns the address pub fn addr(&self) -> &Address { let Side::Rx(rx) = &self.inner else { unreachable!() }; &rx.addr diff --git a/tuic/src/model/dissociate.rs b/tuic/src/model/dissociate.rs index 79321b0..cb51c50 100644 --- a/tuic/src/model/dissociate.rs +++ b/tuic/src/model/dissociate.rs @@ -1,12 +1,13 @@ use super::side::{self, Side}; use crate::protocol::{Dissociate as DissociateHeader, Header}; +/// The model of the `Dissociate` command pub struct Dissociate { inner: Side, _marker: M, } -pub struct Tx { +struct Tx { header: Header, } @@ -20,13 +21,14 @@ impl Dissociate { } } + /// Returns the header of the `Dissociate` command pub fn header(&self) -> &Header { let Side::Tx(tx) = &self.inner else { unreachable!() }; &tx.header } } -pub struct Rx { +struct Rx { assoc_id: u16, } @@ -38,6 +40,7 @@ impl Dissociate { } } + /// Returns the UDP session ID pub fn assoc_id(&self) -> u16 { let Side::Rx(rx) = &self.inner else { unreachable!() }; rx.assoc_id diff --git a/tuic/src/model/heartbeat.rs b/tuic/src/model/heartbeat.rs index 8fce8b0..2d80081 100644 --- a/tuic/src/model/heartbeat.rs +++ b/tuic/src/model/heartbeat.rs @@ -6,7 +6,7 @@ pub struct Heartbeat { _marker: M, } -pub struct Tx { +struct Tx { header: Header, } @@ -20,13 +20,14 @@ impl Heartbeat { } } + /// Returns the header of the `Heartbeat` command pub fn header(&self) -> &Header { let Side::Tx(tx) = &self.inner else { unreachable!() }; &tx.header } } -pub struct Rx; +struct Rx; impl Heartbeat { pub(super) fn new() -> Self { diff --git a/tuic/src/model/mod.rs b/tuic/src/model/mod.rs index c65e6cb..9354ee1 100644 --- a/tuic/src/model/mod.rs +++ b/tuic/src/model/mod.rs @@ -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::{ Address, Authenticate as AuthenticateHeader, Connect as ConnectHeader, Dissociate as DissociateHeader, Heartbeat as HeartbeatHeader, Packet as PacketHeader, @@ -14,6 +16,7 @@ use std::{ time::{Duration, Instant}, }; use thiserror::Error; +use uuid::Uuid; mod authenticate; mod connect; @@ -22,14 +25,14 @@ mod heartbeat; mod packet; pub use self::{ - authenticate::Authenticate, + authenticate::{Authenticate, KeyingMaterialExporter}, connect::Connect, dissociate::Dissociate, heartbeat::Heartbeat, 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 { udp_sessions: Arc>>, task_connect_count: Counter, @@ -40,6 +43,7 @@ impl Connection where B: AsRef<[u8]>, { + /// Creates a new `Connection` #[allow(clippy::new_without_default)] pub fn new() -> Self { let task_associate_count = Counter::new(); @@ -51,24 +55,33 @@ where } } - pub fn send_authenticate(&self, token: [u8; 32]) -> Authenticate { - Authenticate::::new(token) + /// Sends an `Authenticate` + pub fn send_authenticate( + &self, + uuid: Uuid, + exporter: impl KeyingMaterialExporter, + ) -> Authenticate { + Authenticate::::new(uuid, exporter) } + /// Receives an `Authenticate` pub fn recv_authenticate(&self, header: AuthenticateHeader) -> Authenticate { - let (token,) = header.into(); - Authenticate::::new(token) + let (uuid, token) = header.into(); + Authenticate::::new(uuid, token) } + /// Sends a `Connect` pub fn send_connect(&self, addr: Address) -> Connect { Connect::::new(self.task_connect_count.reg(), addr) } + /// Receives a `Connect` pub fn recv_connect(&self, header: ConnectHeader) -> Connect { let (addr,) = header.into(); Connect::::new(self.task_connect_count.reg(), addr) } + /// Sends a `Packet` pub fn send_packet( &self, assoc_id: u16, @@ -80,6 +93,7 @@ where .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> { let (assoc_id, pkt_id, frag_total, frag_id, size, addr) = header.into(); 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 { let (assoc_id, pkt_id, frag_total, frag_id, size, addr) = header.into(); self.udp_sessions.lock().recv_packet_unrestricted( @@ -106,39 +121,49 @@ where ) } + /// Sends a `Dissociate` pub fn send_dissociate(&self, assoc_id: u16) -> Dissociate { self.udp_sessions.lock().send_dissociate(assoc_id) } + /// Receives a `Dissociate` pub fn recv_dissociate(&self, header: DissociateHeader) -> Dissociate { let (assoc_id,) = header.into(); self.udp_sessions.lock().recv_dissociate(assoc_id) } + /// Sends a `Heartbeat` pub fn send_heartbeat(&self) -> Heartbeat { Heartbeat::::new() } + /// Receives a `Heartbeat` pub fn recv_heartbeat(&self, header: HeartbeatHeader) -> Heartbeat { let () = header.into(); Heartbeat::::new() } + /// Returns the number of `Connect` tasks pub fn task_connect_count(&self) -> usize { self.task_connect_count.count() } + /// Returns the number of active UDP sessions pub fn task_associate_count(&self) -> usize { self.task_associate_count.count() } + /// Removes fragments that can not be reassembled within the specified timeout pub fn collect_garbage(&self, timeout: Duration) { self.udp_sessions.lock().collect_garbage(timeout); } } +/// Abstracts the side of a task pub mod side { + /// The side of a task that sends data pub struct Tx; + /// The side of a task that receives data pub struct Rx; pub(super) enum Side { @@ -392,6 +417,7 @@ where } } +/// A complete packet that can be assembled pub struct Assemblable { buf: Vec>, addr: Address, @@ -420,6 +446,7 @@ where } } +/// A trait for assembling a packet pub trait Assembler where Self: Sized, @@ -439,6 +466,7 @@ where } } +/// An error that can occur when assembling a packet #[derive(Debug, Error)] pub enum AssembleError { #[error("invalid fragment id {1} in total {0} fragments")] diff --git a/tuic/src/model/packet.rs b/tuic/src/model/packet.rs index 6bc5fe1..b763ea1 100644 --- a/tuic/src/model/packet.rs +++ b/tuic/src/model/packet.rs @@ -11,7 +11,7 @@ pub struct Packet { _marker: M, } -pub struct Tx { +struct Tx { assoc_id: u16, pkt_id: u16, addr: Address, @@ -31,6 +31,7 @@ impl Packet { } } + /// Fragment the payload into multiple packets pub fn into_fragments<'a, P>(self, payload: P) -> Fragments<'a, P> where P: AsRef<[u8]>, @@ -39,18 +40,20 @@ impl Packet { 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 { let Side::Tx(tx) = &self.inner else { unreachable!() }; tx.assoc_id } + /// Returns the address pub fn addr(&self) -> &Address { let Side::Tx(tx) = &self.inner else { unreachable!() }; &tx.addr } } -pub struct Rx { +struct Rx { sessions: Arc>>, assoc_id: u16, pkt_id: u16, @@ -118,6 +121,7 @@ where } } +/// Iterator over fragments of a packet pub struct Fragments<'a, P> where P: 'a, diff --git a/tuic/src/protocol/authenticate.rs b/tuic/src/protocol/authenticate.rs index 7c86ae7..67400d1 100644 --- a/tuic/src/protocol/authenticate.rs +++ b/tuic/src/protocol/authenticate.rs @@ -1,36 +1,56 @@ -// +-------+ -// | TOKEN | -// +-------+ -// | 32 | -// +-------+ +use uuid::Uuid; + +/// Command `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 #[derive(Clone, Debug)] pub struct Authenticate { + uuid: Uuid, token: [u8; 32], } impl Authenticate { const TYPE_CODE: u8 = 0x00; - pub const fn new(token: [u8; 32]) -> Self { - Self { token } + /// Creates a new `Authenticate` command + 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] { self.token } + /// Returns the command type code pub const fn type_code() -> u8 { Self::TYPE_CODE } + /// Returns the serialized length of the command #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { - 32 + 16 + 32 } } -impl From for ([u8; 32],) { +impl From for (Uuid, [u8; 32]) { fn from(auth: Authenticate) -> Self { - (auth.token,) + (auth.uuid, auth.token) } } diff --git a/tuic/src/protocol/connect.rs b/tuic/src/protocol/connect.rs index 6f00984..5e5f50b 100644 --- a/tuic/src/protocol/connect.rs +++ b/tuic/src/protocol/connect.rs @@ -1,10 +1,17 @@ use super::Address; -// +----------+ -// | ADDR | -// +----------+ -// | Variable | -// +----------+ +/// Command `Connect` +/// ```plain +/// +----------+ +/// | ADDR | +/// +----------+ +/// | Variable | +/// +----------+ +/// ``` +/// +/// where: +/// +/// - `ADDR` - target address #[derive(Clone, Debug)] pub struct Connect { addr: Address, @@ -13,18 +20,22 @@ pub struct Connect { impl Connect { const TYPE_CODE: u8 = 0x01; + /// Creates a new `Connect` command pub const fn new(addr: Address) -> Self { Self { addr } } + /// Returns the address pub fn addr(&self) -> &Address { &self.addr } + /// Returns the command type code pub const fn type_code() -> u8 { Self::TYPE_CODE } + /// Returns the serialized length of the command #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { self.addr.len() diff --git a/tuic/src/protocol/dissociate.rs b/tuic/src/protocol/dissociate.rs index eb29754..45fa603 100644 --- a/tuic/src/protocol/dissociate.rs +++ b/tuic/src/protocol/dissociate.rs @@ -1,8 +1,16 @@ -// +----------+ -// | ASSOC_ID | -// +----------+ -// | 2 | -// +----------+ +/// Command `Dissociate` +/// +/// ```plain +/// +----------+ +/// | ASSOC_ID | +/// +----------+ +/// | 2 | +/// +----------+ +/// ``` +/// +/// where: +/// +/// - `ASSOC_ID` - UDP relay session ID #[derive(Clone, Debug)] pub struct Dissociate { assoc_id: u16, @@ -11,18 +19,22 @@ pub struct Dissociate { impl Dissociate { const TYPE_CODE: u8 = 0x03; + /// Creates a new `Dissociate` command pub const fn new(assoc_id: u16) -> Self { Self { assoc_id } } + /// Returns the UDP relay session ID pub fn assoc_id(&self) -> u16 { self.assoc_id } + /// Returns the command type code pub const fn type_code() -> u8 { Self::TYPE_CODE } + /// Returns the serialized length of the command #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { 2 diff --git a/tuic/src/protocol/heartbeat.rs b/tuic/src/protocol/heartbeat.rs index ec665b3..d73a883 100644 --- a/tuic/src/protocol/heartbeat.rs +++ b/tuic/src/protocol/heartbeat.rs @@ -1,22 +1,28 @@ -// +-+ -// | | -// +-+ -// | | -// +-+ +/// Command `Heartbeat` +/// ```plain +/// +-+ +/// | | +/// +-+ +/// | | +/// +-+ +/// ``` #[derive(Clone, Debug)] pub struct Heartbeat; impl Heartbeat { const TYPE_CODE: u8 = 0x04; + /// Creates a new `Heartbeat` command pub const fn new() -> Self { Self } + /// Returns the command type code pub const fn type_code() -> u8 { Self::TYPE_CODE } + /// Returns the serialized length of the command #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { 0 diff --git a/tuic/src/protocol/mod.rs b/tuic/src/protocol/mod.rs index a675def..6dcc2fd 100644 --- a/tuic/src/protocol/mod.rs +++ b/tuic/src/protocol/mod.rs @@ -15,17 +15,35 @@ pub use self::{ packet::Packet, }; +/// The TUIC protocol version pub const VERSION: u8 = 0x05; -/// Header -/// +/// The command header for negotiating tasks /// ```plain -/// +-----+----------+----------+ -/// | VER | TYPE | OPT | -/// +-----+----------+----------+ -/// | 1 | 1 | Variable | -/// +-----+----------+----------+ +/// +-----+------+----------+ +/// | 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) #[non_exhaustive] #[derive(Clone, Debug)] pub enum Header { @@ -43,6 +61,7 @@ impl Header { pub const TYPE_CODE_DISSOCIATE: u8 = Dissociate::type_code(); pub const TYPE_CODE_HEARTBEAT: u8 = Heartbeat::type_code(); + /// Returns the command type code pub const fn type_code(&self) -> u8 { match self { Self::Authenticate(_) => Authenticate::type_code(), @@ -53,6 +72,7 @@ impl Header { } } + /// Returns the serialized length of the command #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { 2 + match self { @@ -65,22 +85,30 @@ impl Header { } } -/// Address +/// Variable-length field that encodes the network address /// /// ```plain -/// +------+----------+ -/// | TYPE | ADDR | -/// +------+----------+ -/// | 1 | Variable | -/// +------+----------+ +/// +------+----------+----------+ +/// | 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 +/// - `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. #[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_IPV6: u8 = 0x02; + /// Returns the address type code pub const fn type_code(&self) -> u8 { match self { 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)] pub fn len(&self) -> usize { 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 { mem::take(self) } + /// Returns `true` if the address is `None` pub fn is_none(&self) -> bool { matches!(self, Self::None) } + /// Returns `true` if the address is a fully-qualified domain name pub fn is_domain(&self) -> bool { matches!(self, Self::DomainAddress(_, _)) } + /// Returns `true` if the address is an IPv4 address pub fn is_ipv4(&self) -> bool { matches!(self, Self::SocketAddress(SocketAddr::V4(_))) } + /// Returns `true` if the address is an IPv6 address pub fn is_ipv6(&self) -> bool { matches!(self, Self::SocketAddress(SocketAddr::V6(_))) } diff --git a/tuic/src/protocol/packet.rs b/tuic/src/protocol/packet.rs index f6703c7..95f5767 100644 --- a/tuic/src/protocol/packet.rs +++ b/tuic/src/protocol/packet.rs @@ -1,10 +1,22 @@ use super::Address; -// +----------+--------+------------+---------+------+----------+ -// | ASSOC_ID | PKT_ID | FRAG_TOTAL | FRAG_ID | SIZE | ADDR | -// +----------+--------+------------+---------+------+----------+ -// | 2 | 2 | 1 | 1 | 2 | Variable | -// +----------+--------+------------+---------+------+----------+ +/// Command `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 +/// - `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)] pub struct Packet { assoc_id: u16, @@ -18,6 +30,7 @@ pub struct Packet { impl Packet { const TYPE_CODE: u8 = 0x02; + /// Creates a new `Packet` command pub const fn new( assoc_id: u16, pkt_id: u16, @@ -36,39 +49,48 @@ impl Packet { } } + /// Returns the UDP relay session ID pub fn assoc_id(&self) -> u16 { self.assoc_id } + /// Returns the packet ID pub fn pkt_id(&self) -> u16 { self.pkt_id } + /// Returns the total number of fragments of the UDP packet pub fn frag_total(&self) -> u8 { self.frag_total } + /// Returns the fragment ID of the UDP packet pub fn frag_id(&self) -> u8 { self.frag_id } + /// Returns the length of the (fragmented) UDP packet pub fn size(&self) -> u16 { self.size } + /// Returns the target (from client) or source (from server) address pub fn addr(&self) -> &Address { &self.addr } + /// Returns the command type code pub const fn type_code() -> u8 { Self::TYPE_CODE } + /// Returns the serialized length of the command #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { Self::len_without_addr() + self.addr.len() } + /// Returns the serialized length of the command without the address pub const fn len_without_addr() -> usize { 2 + 2 + 1 + 1 + 2 } diff --git a/tuic/src/unmarshal.rs b/tuic/src/unmarshal.rs index ca037c2..a6b7422 100644 --- a/tuic/src/unmarshal.rs +++ b/tuic/src/unmarshal.rs @@ -8,8 +8,10 @@ use std::{ string::FromUtf8Error, }; use thiserror::Error; +use uuid::{Error as UuidError, Uuid}; impl Header { + /// Unmarshals a header from an `AsyncRead` stream #[cfg(feature = "async_marshal")] pub async fn async_unmarshal(s: &mut (impl AsyncRead + Unpin)) -> Result { let mut buf = [0; 1]; @@ -36,6 +38,7 @@ impl Header { } } + /// Unmarshals a header from a `Read` stream #[cfg(feature = "marshal")] pub fn unmarshal(s: &mut impl Read) -> Result { let mut buf = [0; 1]; @@ -164,16 +167,20 @@ impl Address { impl Authenticate { #[cfg(feature = "async_marshal")] async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result { - let mut buf = [0; 32]; + let mut buf = [0; 48]; 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")] fn read(s: &mut impl Read) -> Result { let mut buf = [0; 32]; 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)] pub enum UnmarshalError { #[error(transparent)] @@ -259,6 +267,8 @@ pub enum UnmarshalError { InvalidVersion(u8), #[error("invalid command: {0}")] InvalidCommand(u8), + #[error("invalid UUID: {0}")] + InvalidUuid(#[from] UuidError), #[error("invalid address type: {0}")] InvalidAddressType(u8), #[error("address parsing error: {0}")]