commit
aea3eefa02
175
.github/workflows/build-and-release.yml
vendored
175
.github/workflows/build-and-release.yml
vendored
@ -1,175 +0,0 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
name: build-and-release
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# x86_64-linux-gnu
|
||||
- arch-name: x86_64-linux-gnu
|
||||
os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
cross: false
|
||||
file-ext:
|
||||
# x86_64-linux-musl
|
||||
- arch-name: x86_64-linux-musl
|
||||
os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
cross: true
|
||||
file-ext:
|
||||
# x86_64-windows-msvc
|
||||
- arch-name: x86_64-windows-msvc
|
||||
os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
cross: false
|
||||
file-ext: .exe
|
||||
# x86_64-windows-gnu
|
||||
- arch-name: x86_64-windows-gnu
|
||||
os: ubuntu-latest
|
||||
target: x86_64-pc-windows-gnu
|
||||
cross: true
|
||||
file-ext: .exe
|
||||
# x86_64-macos
|
||||
- arch-name: x86_64-macos
|
||||
os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
cross: false
|
||||
file-ext:
|
||||
# x86_64-android
|
||||
- arch-name: x86_64-android
|
||||
os: ubuntu-latest
|
||||
target: x86_64-linux-android
|
||||
cross: true
|
||||
file-ext:
|
||||
# aarch64-linux-gnu
|
||||
- arch-name: aarch64-linux-gnu
|
||||
os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
cross: true
|
||||
file-ext:
|
||||
# aarch64-linux-musl
|
||||
- arch-name: aarch64-linux-musl
|
||||
os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
cross: true
|
||||
file-ext:
|
||||
# aarch64-macos
|
||||
- arch-name: aarch64-macos
|
||||
os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
cross: true
|
||||
file-ext:
|
||||
# aarch64-android
|
||||
- arch-name: aarch64-android
|
||||
os: ubuntu-latest
|
||||
target: aarch64-linux-android
|
||||
cross: true
|
||||
file-ext:
|
||||
# aarch64-ios
|
||||
- arch-name: aarch64-ios
|
||||
os: macos-latest
|
||||
target: aarch64-apple-ios
|
||||
cross: true
|
||||
file-ext:
|
||||
# i686-linux-gnu
|
||||
- arch-name: i686-linux-gnu
|
||||
os: ubuntu-latest
|
||||
target: i686-unknown-linux-gnu
|
||||
cross: true
|
||||
file-ext:
|
||||
# i686-linux-musl
|
||||
- arch-name: i686-linux-musl
|
||||
os: ubuntu-latest
|
||||
target: i686-unknown-linux-musl
|
||||
cross: true
|
||||
file-ext:
|
||||
# i686-windows-msvc
|
||||
- arch-name: i686-windows-msvc
|
||||
os: windows-latest
|
||||
target: i686-pc-windows-msvc
|
||||
cross: true
|
||||
file-ext: .exe
|
||||
# i686-android
|
||||
- arch-name: i686-android
|
||||
os: ubuntu-latest
|
||||
target: i686-linux-android
|
||||
cross: true
|
||||
file-ext:
|
||||
# arm-linux-gnueabihf
|
||||
- arch-name: arm-linux-gnueabihf
|
||||
os: ubuntu-latest
|
||||
target: arm-unknown-linux-gnueabihf
|
||||
cross: true
|
||||
file-ext:
|
||||
# armv7-linux-musleabihf
|
||||
- arch-name: armv7-linux-musleabihf
|
||||
os: ubuntu-latest
|
||||
target: armv7-unknown-linux-musleabihf
|
||||
cross: true
|
||||
file-ext:
|
||||
# armv7-android
|
||||
- arch-name: armv7-android
|
||||
os: ubuntu-latest
|
||||
target: armv7-linux-androideabi
|
||||
cross: true
|
||||
file-ext:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get the latest tag
|
||||
id: tag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
|
||||
- name: Build server
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.cross }}
|
||||
command: build
|
||||
args: --release -p tuic-server --target ${{ matrix.target }}
|
||||
|
||||
- name: Build client
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.cross }}
|
||||
command: build
|
||||
args: --release -p tuic-client --target ${{ matrix.target }}
|
||||
|
||||
- name: Move binaries
|
||||
run: |
|
||||
mkdir artifacts/
|
||||
mv target/${{ matrix.target }}/release/tuic-server${{ matrix.file-ext }} artifacts/tuic-server-${{ steps.tag.outputs.tag }}-${{ matrix.arch-name }}${{ matrix.file-ext }}
|
||||
mv target/${{ matrix.target }}/release/tuic-client${{ matrix.file-ext }} artifacts/tuic-client-${{ steps.tag.outputs.tag }}-${{ matrix.arch-name }}${{ matrix.file-ext }}
|
||||
|
||||
- name: Calculate SHA256
|
||||
run: |
|
||||
cd artifacts/
|
||||
openssl dgst -sha256 -r tuic-server-${{ steps.tag.outputs.tag }}-${{ matrix.arch-name }}${{ matrix.file-ext }} > tuic-server-${{ steps.tag.outputs.tag }}-${{ matrix.arch-name }}${{ matrix.file-ext }}.sha256sum
|
||||
openssl dgst -sha256 -r tuic-client-${{ steps.tag.outputs.tag }}-${{ matrix.arch-name }}${{ matrix.file-ext }} > tuic-client-${{ steps.tag.outputs.tag }}-${{ matrix.arch-name }}${{ matrix.file-ext }}.sha256sum
|
||||
|
||||
- name: Release binaries
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "artifacts/*"
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
name: ${{ steps.tag.outputs.tag }}
|
||||
allowUpdates: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,2 +1,4 @@
|
||||
/target
|
||||
.vscode/
|
||||
target/
|
||||
.DS_Store
|
||||
Cargo.lock
|
||||
|
12
Cargo.toml
12
Cargo.toml
@ -1,12 +1,2 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"client",
|
||||
"server",
|
||||
"protocol",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
members = ["tuic", "tuic-quinn", "tuic-server", "tuic-client"]
|
||||
|
283
README.md
283
README.md
@ -1,274 +1,37 @@
|
||||
# TUIC
|
||||
|
||||
Delicately-TUICed high-performance proxy built on top of the [QUIC](https://en.wikipedia.org/wiki/QUIC) protocol
|
||||
Delicately-TUICed 0-RTT proxy protocol
|
||||
|
||||
**TUIC's goal is to minimize the handshake latency as much as possible**
|
||||
**Warning: TUIC's [dev](https://github.com/EAimTY/tuic/tree/dev) branch is under heavy development. For end-user, please check out the latest released branch**
|
||||
|
||||
## Features
|
||||
## Introduction
|
||||
|
||||
- 1-RTT TCP relaying
|
||||
- 0-RTT UDP relaying with [Full Cone NAT](https://www.rfc-editor.org/rfc/rfc3489#section-5)
|
||||
- Two UDP relay modes: `native` (native UDP mechanisms) and `quic` (100% delivery rate)
|
||||
- Bidirectional user-space congestion control (BBR, New Reno and CUBIC)
|
||||
- Multiplexing all tasks into a single QUIC connection (tasks are separately flow controlled)
|
||||
- Smooth session transfer on network switching
|
||||
- Paralleled 0-RTT authentication
|
||||
- Optional QUIC 0-RTT handshake
|
||||
TUIC is a proxy protocol focusing on the simplicity. It aims to minimize the additional handshake latency caused by relaying as much as possible
|
||||
|
||||
## Design
|
||||
TUIC is originally designed to be used on top of the [QUIC](https://en.wikipedia.org/wiki/QUIC) protocol, but you can use it with any other protocol, e.g. TCP, in theory
|
||||
|
||||
TUIC was designed on the basis of the QUIC protocol from the very beginning. It can make full use of the advantages brought by QUIC. You can find more information about the TUIC protocol [here](https://github.com/EAimTY/tuic/tree/master/protocol).
|
||||
When paired with QUIC, TUIC can achieve:
|
||||
|
||||
### Multiplexing
|
||||
- 0-RTT TCP proxying
|
||||
- 0-RTT UDP proxying with NAT type [Full Cone](https://www.rfc-editor.org/rfc/rfc3489#section-5)
|
||||
- 0-RTT authentication
|
||||
- Two UDP proxying modes:
|
||||
- `native`: Having characteristics of native UDP mechanism
|
||||
- `quic`: Transferring UDP packets losslessly using QUIC streams
|
||||
- Fully multiplexed
|
||||
- All the advantages of QUIC:
|
||||
- Bidirectional user-space congestion control
|
||||
- Connection migration
|
||||
- Optional 0-RTT connection handshake
|
||||
|
||||
TUIC multiplexes all tasks into a single QUIC connection using QUIC's multi-streams mechanism. This means that unless the QUIC connection is forcibly interrupted or no task within the maximum idle time, negotiating new relay task does not need to go through the process of QUIC handshake and TUIC authentication.
|
||||
## Overview
|
||||
|
||||
### UDP Relaying
|
||||
There are 4 crates provided in this repository:
|
||||
|
||||
TUIC has 2 UDP relay modes:
|
||||
|
||||
- `native` - using QUIC's datagram to transmit UDP packets. As with native UDP, packets may be lost, but the overhead of the acknowledgment mechanism is omitted. Relayed packets are still encrypted by QUIC.
|
||||
|
||||
- `quic` - transporting UDP packets as QUIC streams. Because of the acknowledgment and retransmission mechanism, UDP packets can guarantee a 100% delivery rate, but have additional transmission overhead as a result. Note that each UDP data packet is transmitted as a separate stream, and the flow controlled separately, so the loss and retransmission of one packet will not cause other packets to be blocked. This mode can be used to transmit UDP packets larger than the MTU of the underlying network.
|
||||
|
||||
### Bidirectional User-space Congestion Control
|
||||
|
||||
Since QUIC is implemented over UDP, its congestion control implementation is not limited by platform and operating system. For poor quality network, [BBR algorithm](https://en.wikipedia.org/wiki/TCP_congestion_control#TCP_BBR) can be used on both the server and the client to achieve better transmission performance.
|
||||
|
||||
### Security
|
||||
|
||||
As mentioned above, TUIC is based on the QUIC protocol, which uses TLS to encrypt data. TUIC protocol itself does not provide any security, but the QUIC protocol provides a strong security guarantee. TUIC also supports QUIC's 0-RTT handshake, but it came with a cost of weakened security, [read more about QUIC 0-RTT handshake](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones).
|
||||
|
||||
## Usage
|
||||
|
||||
TUIC depends on [rustls](https://github.com/rustls/rustls), which uses [ring](https://github.com/briansmith/ring) for implementing the cryptography in TLS. As a result, TUIC only runs on platforms supported by ring. At the time of writing this means x86, x86-64, armv7, and aarch64.
|
||||
|
||||
You can find pre-compiled binaries in the latest [releases](https://github.com/EAimTY/tuic/releases).
|
||||
|
||||
### Server
|
||||
|
||||
```
|
||||
tuic-server
|
||||
|
||||
Options:
|
||||
-c, --config CONFIG_FILE
|
||||
Read configuration from a file. Note that command line
|
||||
arguments will override the configuration file
|
||||
--port SERVER_PORT
|
||||
Set the server listening port
|
||||
--token TOKEN Set the token for TUIC authentication. This option can
|
||||
be used multiple times to set multiple tokens.
|
||||
--certificate CERTIFICATE
|
||||
Set the X.509 certificate. This must be an end-entity
|
||||
certificate
|
||||
--private-key PRIVATE_KEY
|
||||
Set the certificate private key
|
||||
--ip IP Set the server listening IP. Default: 0.0.0.0
|
||||
--congestion-controller CONGESTION_CONTROLLER
|
||||
Set the congestion control algorithm. Available:
|
||||
"cubic", "new_reno", "bbr". Default: "cubic"
|
||||
--max-idle-time MAX_IDLE_TIME
|
||||
Set the maximum idle time for QUIC connections, in
|
||||
milliseconds. Default: 15000
|
||||
--authentication-timeout AUTHENTICATION_TIMEOUT
|
||||
Set the maximum time allowed between a QUIC connection
|
||||
established and the TUIC authentication packet
|
||||
received, in milliseconds. Default: 1000
|
||||
--alpn ALPN_PROTOCOL
|
||||
Set ALPN protocols that the server accepts. This
|
||||
option can be used multiple times to set multiple ALPN
|
||||
protocols. If not set, the server will not check ALPN
|
||||
at all
|
||||
--max-udp-relay-packet-size MAX_UDP_RELAY_PACKET_SIZE
|
||||
UDP relay mode QUIC can transmit UDP packets larger
|
||||
than the MTU. Set this to a higher value allows
|
||||
outbound to receive larger UDP packet. Default: 1500
|
||||
--log-level LOG_LEVEL
|
||||
Set the log level. Available: "off", "error", "warn",
|
||||
"info", "debug", "trace". Default: "info"
|
||||
-v, --version Print the version
|
||||
-h, --help Print this help menu
|
||||
```
|
||||
|
||||
The configuration file is in JSON format:
|
||||
|
||||
```json
|
||||
{
|
||||
"port": 443,
|
||||
"token": ["TOKEN0", "TOKEN1"],
|
||||
"certificate": "/PATH/TO/CERT",
|
||||
"private_key": "/PATH/TO/PRIV_KEY",
|
||||
|
||||
"ip": "0.0.0.0",
|
||||
"congestion_controller": "cubic",
|
||||
"max_idle_time": 15000,
|
||||
"authentication_timeout": 1000,
|
||||
"alpn": ["h3"],
|
||||
"max_udp_relay_packet_size": 1500,
|
||||
"log_level": "info"
|
||||
}
|
||||
```
|
||||
|
||||
Fields `port`, `token`, `certificate`, `private_key` are required. Other fields are optional and can be deleted to fall-back the default value.
|
||||
|
||||
Note that command line arguments can override the configuration file.
|
||||
|
||||
### Client
|
||||
|
||||
```
|
||||
tuic-client
|
||||
|
||||
Options:
|
||||
-c, --config CONFIG_FILE
|
||||
Read configuration from a file. Note that command line
|
||||
arguments will override the configuration file
|
||||
--server SERVER Set the server address. This address must be included
|
||||
in the certificate
|
||||
--server-port SERVER_PORT
|
||||
Set the server port
|
||||
--token TOKEN Set the token for TUIC authentication
|
||||
--server-ip SERVER_IP
|
||||
Set the server IP, for overwriting the DNS lookup
|
||||
result of the server address set in option 'server'
|
||||
--certificate CERTIFICATE
|
||||
Set custom X.509 certificate alongside native CA roots
|
||||
for the QUIC handshake. This option can be used
|
||||
multiple times to set multiple certificates
|
||||
--udp-relay-mode UDP_MODE
|
||||
Set the UDP relay mode. Available: "native", "quic".
|
||||
Default: "native"
|
||||
--congestion-controller CONGESTION_CONTROLLER
|
||||
Set the congestion control algorithm. Available:
|
||||
"cubic", "new_reno", "bbr". Default: "cubic"
|
||||
--heartbeat-interval HEARTBEAT_INTERVAL
|
||||
Set the heartbeat interval to ensures that the QUIC
|
||||
connection is not closed when there are relay tasks
|
||||
but no data transfer, in milliseconds. This value
|
||||
needs to be smaller than the maximum idle time set at
|
||||
the server side. Default: 10000
|
||||
--alpn ALPN_PROTOCOL
|
||||
Set ALPN protocols included in the TLS client hello.
|
||||
This option can be used multiple times to set multiple
|
||||
ALPN protocols. If not set, no ALPN extension will be
|
||||
sent
|
||||
--disable-sni Not sending the Server Name Indication (SNI) extension
|
||||
during the client TLS handshake
|
||||
--reduce-rtt Enable 0-RTT QUIC handshake
|
||||
--request-timeout REQUEST_TIMEOUT
|
||||
Set the timeout for negotiating tasks between client
|
||||
and the server, in milliseconds. Default: 8000
|
||||
--max-udp-relay-packet-size MAX_UDP_RELAY_PACKET_SIZE
|
||||
UDP relay mode QUIC can transmit UDP packets larger
|
||||
than the MTU. Set this to a higher value allows
|
||||
inbound to receive larger UDP packet. Default: 1500
|
||||
--local-port LOCAL_PORT
|
||||
Set the listening port for the local socks5 server
|
||||
--local-ip LOCAL_IP
|
||||
Set the listening IP for the local socks5 server. Note
|
||||
that the sock5 server socket will be a dual-stack
|
||||
socket if it is IPv6. Default: "127.0.0.1"
|
||||
--local-username LOCAL_USERNAME
|
||||
Set the username for the local socks5 server
|
||||
authentication
|
||||
--local-password LOCAL_PASSWORD
|
||||
Set the password for the local socks5 server
|
||||
authentication
|
||||
--log-level LOG_LEVEL
|
||||
Set the log level. Available: "off", "error", "warn",
|
||||
"info", "debug", "trace". Default: "info"
|
||||
-v, --version Print the version
|
||||
-h, --help Print this help menu
|
||||
```
|
||||
|
||||
The configuration file is in JSON format:
|
||||
|
||||
```json
|
||||
{
|
||||
"relay": {
|
||||
"server": "SERVER",
|
||||
"port": 443,
|
||||
"token": "TOKEN",
|
||||
|
||||
"ip": "SERVER_IP",
|
||||
"certificates": ["/PATH/TO/CERT"],
|
||||
"udp_relay_mode": "native",
|
||||
"congestion_controller": "cubic",
|
||||
"heartbeat_interval": 10000,
|
||||
"alpn": ["h3"],
|
||||
"disable_sni": false,
|
||||
"reduce_rtt": false,
|
||||
"request_timeout": 8000,
|
||||
"max_udp_relay_packet_size": 1500
|
||||
},
|
||||
"local": {
|
||||
"port": 1080,
|
||||
|
||||
"ip": "127.0.0.1",
|
||||
"username": "SOCKS5_USERNAME",
|
||||
"password": "SOCKS5_PASSWORD"
|
||||
},
|
||||
"log_level": "info"
|
||||
}
|
||||
```
|
||||
|
||||
Fields `server`, `token` and `port` in both sections are required. Other fields are optional and can be deleted to fall-back the default value.
|
||||
|
||||
Note that command line arguments can override the configuration file.
|
||||
|
||||
## GUI Clients
|
||||
|
||||
### Android
|
||||
|
||||
- [SagerNet](https://sagernet.org/)
|
||||
|
||||
### iOS
|
||||
|
||||
- [Stash](https://stash.ws/) *
|
||||
|
||||
*[Stash](https://stash.ws/) re-implemented the TUIC protocol from scratch, so it didn't preserve the GPL License.
|
||||
|
||||
### Windows
|
||||
|
||||
- [v2rayN](https://github.com/2dust/v2rayN)
|
||||
|
||||
## FAQ
|
||||
|
||||
### What are the advantages of TUIC over other proxy protocols / implementions?
|
||||
|
||||
As mentioned before, TUIC's goal is to minimize the handshake latency as much as possible. Thus, the core of the TUIC protocol is to reduce the additional round trip time added by the relay. For TCP relaying, TUIC only adds a single round trip between the TUIC server and the TUIC client - half of a typical TCP-based proxy would require. TUIC also has a unique UDP relaying mechanism. It achieves 0-RTT UDP relaying by syncing UDP relay sessions implicitly between the server and the client.
|
||||
|
||||
Low handshake latency means faster connection establishment and UDP packet delay time. TUIC also supports both UDP over streams and UDP over datagrams for UDP relaying. All of these makes TUIC one of the most efficient proxy protocol for UDP relaying.
|
||||
|
||||
### Why my TUIC is slower than other proxy protocols / implementions?
|
||||
|
||||
For an Internet connection, fast / slow is defined by both:
|
||||
|
||||
- Handshake latency
|
||||
- Bandwidth
|
||||
|
||||
They are equally important. For the first case, TUIC can be one of the best solution right now. You can directly feel it from things like the speed of opening a web page in your browser. For the second case, TUIC may be a bit slower than other TCP-based proxy protocols due to ISPs' QoS, but TUIC's bandwidth can still be competitive in most scenario.
|
||||
|
||||
### How can I listen both IPv4 and IPv6 on TUIC server / TUIC client's socks5 server?
|
||||
|
||||
TUIC always constructs an IPv6 listener as a dual-stack socket. If you need to listen on both IPv4 and IPv6, you can set the bind IP to the unspecified IPv6 address `::`.
|
||||
|
||||
### Why TUIC client doesn't support other inbound / advanced route settings?
|
||||
|
||||
Since there are already many great proxy convert / distribute solutions, there really is no need for me to reimplement those again. If you need those functions, the best choice to chain a V2Ray layer in front of the TUIC client. For a typical network program, there is basically no performance cost for local relaying.
|
||||
|
||||
### Why TUIC client is not able to convert the first connection into 0-RTT
|
||||
|
||||
It is totally fine and designed to be like that.
|
||||
|
||||
> The basic idea behind 0-RTT connection resumption is that if the client and server had previously established a TLS connection between each other, they can use information cached from that session to establish a new one without having to negotiate the connection’s parameters from scratch. Notably this allows the client to compute the private encryption keys required to protect application data before even talking to the server.
|
||||
>
|
||||
> *--[Even faster connection establishment with QUIC 0-RTT resumption](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption) - Cloudflare*
|
||||
|
||||
When the client program starts, trying to convert the very first connection to 0-RTT will always fail because the client has no server-related information yet. This connection handshake will fall-back to the regular 1-RTT one.
|
||||
|
||||
Once the client caches server information from the first connection, any subsequent connection will be convert into a 0-RTT one. That is why you only see this warning message once just after starting the client.
|
||||
|
||||
Therefore, you can safely ignore this warn.
|
||||
- **[tuic](https://github.com/EAimTY/tuic/tree/dev/tuic)** - Library. The protocol itself, protcol & model abstraction, synchronous / asynchronous marshalling
|
||||
- **[tuic-quinn](https://github.com/EAimTY/tuic/tree/dev/tuic-quinn)** - Library. A thin layer on top of [quinn](https://github.com/quinn-rs/quinn) to provide functions for TUIC
|
||||
- **[tuic-server](https://github.com/EAimTY/tuic/tree/dev/tuic-server)** - Binary. Minimalistic TUIC server implementation as a reference, focusing on the simplicity
|
||||
- **[tuic-client](https://github.com/EAimTY/tuic/tree/dev/tuic-client)** - Binary. Minimalistic TUIC client implementation as a reference, focusing on the simplicity
|
||||
|
||||
## License
|
||||
|
||||
|
@ -1,37 +0,0 @@
|
||||
[package]
|
||||
name = "tuic-client"
|
||||
version = "0.8.5"
|
||||
authors = ["EAimTY <ea.imty@gmail.com>"]
|
||||
description = "Delicately-TUICed high-performance proxy built on top of the QUIC protocol"
|
||||
categories = ["network-programming", "command-line-utilities"]
|
||||
keywords = ["tuic", "proxy", "quic"]
|
||||
edition = "2021"
|
||||
rust-version = "1.59"
|
||||
readme = "../README.md"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/EAimTY/tuic"
|
||||
|
||||
[dependencies]
|
||||
tuic-protocol = { path="../protocol" }
|
||||
|
||||
blake3 = "1.3.*"
|
||||
bytes = "1.2.*"
|
||||
env_logger = { version = "0.9.*", features = ["humantime"], default-features = false }
|
||||
futures-util = { version = "0.3.*", default-features = false }
|
||||
getopts = "0.2.*"
|
||||
log = { version = "0.4.*", features = ["serde", "std"] }
|
||||
once_cell = "1.13.*"
|
||||
parking_lot = "0.12.*"
|
||||
quinn = "0.8.*"
|
||||
rand = "0.8.*"
|
||||
rustls = { version = "0.20.*", features = ["quic"], default-features = false }
|
||||
rustls-native-certs = "0.6.*"
|
||||
rustls-pemfile = "1.0.*"
|
||||
serde = { version = "1.0.*", features = ["derive", "std"], default-features = false }
|
||||
serde_json = { version = "1.0.*", features = ["std"], default-features = false }
|
||||
socket2 = "0.4.*"
|
||||
socks5-proto = "0.3.*"
|
||||
socks5-server = "0.8.*"
|
||||
thiserror = "1.0.*"
|
||||
tokio = { version = "1.20.*", features = ["io-util", "macros", "net", "parking_lot", "rt-multi-thread", "sync", "time"] }
|
||||
webpki = { version = "0.22.*", default-features = false }
|
@ -1,36 +0,0 @@
|
||||
use crate::config::ConfigError;
|
||||
use rustls::{Certificate, RootCertStore};
|
||||
use rustls_pemfile::Item;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::BufReader,
|
||||
};
|
||||
|
||||
pub fn load_certificates(files: Vec<String>) -> Result<RootCertStore, ConfigError> {
|
||||
let mut certs = RootCertStore::empty();
|
||||
|
||||
for file in &files {
|
||||
let mut file =
|
||||
BufReader::new(File::open(file).map_err(|err| ConfigError::Io(file.to_owned(), err))?);
|
||||
|
||||
while let Ok(Some(item)) = rustls_pemfile::read_one(&mut file) {
|
||||
if let Item::X509Certificate(cert) = item {
|
||||
certs.add(&Certificate(cert))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if certs.is_empty() {
|
||||
for file in &files {
|
||||
certs.add(&Certificate(
|
||||
fs::read(file).map_err(|err| ConfigError::Io(file.to_owned(), err))?,
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
||||
for cert in rustls_native_certs::load_native_certs().map_err(ConfigError::NativeCertificate)? {
|
||||
certs.add(&Certificate(cert.0))?;
|
||||
}
|
||||
|
||||
Ok(certs)
|
||||
}
|
@ -1,629 +0,0 @@
|
||||
use crate::{
|
||||
certificate,
|
||||
relay::{ServerAddr, UdpRelayMode},
|
||||
};
|
||||
use getopts::{Fail, Options};
|
||||
use log::{LevelFilter, ParseLevelError};
|
||||
use quinn::{
|
||||
congestion::{BbrConfig, CubicConfig, NewRenoConfig},
|
||||
ClientConfig,
|
||||
};
|
||||
use rustls::{version::TLS13, ClientConfig as RustlsClientConfig};
|
||||
use serde::{de::Error as DeError, Deserialize, Deserializer};
|
||||
use serde_json::Error as JsonError;
|
||||
use socks5_server::{
|
||||
auth::{NoAuth, Password},
|
||||
Auth,
|
||||
};
|
||||
use std::{
|
||||
env::ArgsOs,
|
||||
fmt::Display,
|
||||
fs::File,
|
||||
io::Error as IoError,
|
||||
net::{AddrParseError, IpAddr, Ipv4Addr, SocketAddr},
|
||||
num::ParseIntError,
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use webpki::Error as WebpkiError;
|
||||
|
||||
pub struct Config {
|
||||
pub client_config: ClientConfig,
|
||||
pub server_addr: ServerAddr,
|
||||
pub token_digest: [u8; 32],
|
||||
pub udp_relay_mode: UdpRelayMode<(), ()>,
|
||||
pub heartbeat_interval: u64,
|
||||
pub reduce_rtt: bool,
|
||||
pub request_timeout: u64,
|
||||
pub max_udp_relay_packet_size: usize,
|
||||
pub local_addr: SocketAddr,
|
||||
pub socks5_auth: Arc<dyn Auth + Send + Sync>,
|
||||
pub log_level: LevelFilter,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn parse(args: ArgsOs) -> Result<Self, ConfigError> {
|
||||
let raw = RawConfig::parse(args)?;
|
||||
|
||||
let client_config = {
|
||||
let certs = certificate::load_certificates(raw.relay.certificates)?;
|
||||
|
||||
let mut crypto = RustlsClientConfig::builder()
|
||||
.with_safe_default_cipher_suites()
|
||||
.with_safe_default_kx_groups()
|
||||
.with_protocol_versions(&[&TLS13])
|
||||
.unwrap()
|
||||
.with_root_certificates(certs)
|
||||
.with_no_client_auth();
|
||||
|
||||
crypto.alpn_protocols = raw
|
||||
.relay
|
||||
.alpn
|
||||
.into_iter()
|
||||
.map(|alpn| alpn.into_bytes())
|
||||
.collect();
|
||||
|
||||
crypto.enable_early_data = true;
|
||||
crypto.enable_sni = !raw.relay.disable_sni;
|
||||
|
||||
let mut config = ClientConfig::new(Arc::new(crypto));
|
||||
let transport = Arc::get_mut(&mut config.transport).unwrap();
|
||||
|
||||
match raw.relay.congestion_controller {
|
||||
CongestionController::Bbr => {
|
||||
transport.congestion_controller_factory(Arc::new(BbrConfig::default()));
|
||||
}
|
||||
CongestionController::Cubic => {
|
||||
transport.congestion_controller_factory(Arc::new(CubicConfig::default()));
|
||||
}
|
||||
CongestionController::NewReno => {
|
||||
transport.congestion_controller_factory(Arc::new(NewRenoConfig::default()));
|
||||
}
|
||||
}
|
||||
|
||||
transport.max_idle_timeout(None);
|
||||
|
||||
config
|
||||
};
|
||||
|
||||
let server_addr = {
|
||||
let name = raw.relay.server.unwrap();
|
||||
let port = raw.relay.port.unwrap();
|
||||
|
||||
if let Some(ip) = raw.relay.ip {
|
||||
ServerAddr::SocketAddr {
|
||||
addr: SocketAddr::new(ip, port),
|
||||
name,
|
||||
}
|
||||
} else {
|
||||
ServerAddr::DomainAddr { domain: name, port }
|
||||
}
|
||||
};
|
||||
|
||||
let token_digest = *blake3::hash(&raw.relay.token.unwrap().into_bytes()).as_bytes();
|
||||
let udp_relay_mode = raw.relay.udp_relay_mode;
|
||||
let heartbeat_interval = raw.relay.heartbeat_interval;
|
||||
let reduce_rtt = raw.relay.reduce_rtt;
|
||||
let request_timeout = raw.relay.request_timeout;
|
||||
let max_udp_relay_packet_size = raw.relay.max_udp_relay_packet_size;
|
||||
|
||||
let local_addr = SocketAddr::from((raw.local.ip, raw.local.port.unwrap()));
|
||||
|
||||
let socks5_auth = match (raw.local.username, raw.local.password) {
|
||||
(None, None) => Arc::new(NoAuth) as Arc<dyn Auth + Send + Sync>,
|
||||
(Some(username), Some(password)) => {
|
||||
Arc::new(Password::new(username.into_bytes(), password.into_bytes()))
|
||||
as Arc<dyn Auth + Send + Sync>
|
||||
}
|
||||
_ => return Err(ConfigError::LocalAuthentication),
|
||||
};
|
||||
|
||||
let log_level = raw.log_level;
|
||||
|
||||
Ok(Self {
|
||||
client_config,
|
||||
server_addr,
|
||||
token_digest,
|
||||
udp_relay_mode,
|
||||
heartbeat_interval,
|
||||
reduce_rtt,
|
||||
request_timeout,
|
||||
max_udp_relay_packet_size,
|
||||
local_addr,
|
||||
socks5_auth,
|
||||
log_level,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawConfig {
|
||||
relay: RawRelayConfig,
|
||||
local: RawLocalConfig,
|
||||
|
||||
#[serde(default = "default::log_level")]
|
||||
log_level: LevelFilter,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawRelayConfig {
|
||||
server: Option<String>,
|
||||
port: Option<u16>,
|
||||
token: Option<String>,
|
||||
ip: Option<IpAddr>,
|
||||
|
||||
#[serde(default = "default::certificates")]
|
||||
certificates: Vec<String>,
|
||||
|
||||
#[serde(
|
||||
default = "default::udp_relay_mode",
|
||||
deserialize_with = "deserialize_from_str"
|
||||
)]
|
||||
udp_relay_mode: UdpRelayMode<(), ()>,
|
||||
|
||||
#[serde(
|
||||
default = "default::congestion_controller",
|
||||
deserialize_with = "deserialize_from_str"
|
||||
)]
|
||||
congestion_controller: CongestionController,
|
||||
|
||||
#[serde(default = "default::heartbeat_interval")]
|
||||
heartbeat_interval: u64,
|
||||
|
||||
#[serde(default = "default::alpn")]
|
||||
alpn: Vec<String>,
|
||||
|
||||
#[serde(default = "default::disable_sni")]
|
||||
disable_sni: bool,
|
||||
|
||||
#[serde(default = "default::reduce_rtt")]
|
||||
reduce_rtt: bool,
|
||||
|
||||
#[serde(default = "default::request_timeout")]
|
||||
request_timeout: u64,
|
||||
|
||||
#[serde(default = "default::max_udp_relay_packet_size")]
|
||||
max_udp_relay_packet_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawLocalConfig {
|
||||
port: Option<u16>,
|
||||
|
||||
#[serde(default = "default::local_ip")]
|
||||
ip: IpAddr,
|
||||
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for RawConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
relay: RawRelayConfig::default(),
|
||||
local: RawLocalConfig::default(),
|
||||
log_level: default::log_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RawRelayConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
server: None,
|
||||
port: None,
|
||||
ip: None,
|
||||
token: None,
|
||||
certificates: default::certificates(),
|
||||
udp_relay_mode: default::udp_relay_mode(),
|
||||
congestion_controller: default::congestion_controller(),
|
||||
heartbeat_interval: default::heartbeat_interval(),
|
||||
alpn: default::alpn(),
|
||||
disable_sni: default::disable_sni(),
|
||||
reduce_rtt: default::reduce_rtt(),
|
||||
request_timeout: default::request_timeout(),
|
||||
max_udp_relay_packet_size: default::max_udp_relay_packet_size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RawLocalConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
port: None,
|
||||
ip: default::local_ip(),
|
||||
username: None,
|
||||
password: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RawConfig {
|
||||
fn parse(args: ArgsOs) -> Result<Self, ConfigError> {
|
||||
let mut opts = Options::new();
|
||||
|
||||
opts.optopt(
|
||||
"c",
|
||||
"config",
|
||||
"Read configuration from a file. Note that command line arguments will override the configuration file",
|
||||
"CONFIG_FILE",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"server",
|
||||
"Set the server address. This address must be included in the certificate",
|
||||
"SERVER",
|
||||
);
|
||||
|
||||
opts.optopt("", "server-port", "Set the server port", "SERVER_PORT");
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"token",
|
||||
"Set the token for TUIC authentication",
|
||||
"TOKEN",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"server-ip",
|
||||
"Set the server IP, for overwriting the DNS lookup result of the server address set in option 'server'",
|
||||
"SERVER_IP",
|
||||
);
|
||||
|
||||
opts.optmulti(
|
||||
"",
|
||||
"certificate",
|
||||
"Set custom X.509 certificate alongside native CA roots for the QUIC handshake. This option can be used multiple times to set multiple certificates",
|
||||
"CERTIFICATE",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"udp-relay-mode",
|
||||
r#"Set the UDP relay mode. Available: "native", "quic". Default: "native""#,
|
||||
"UDP_MODE",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"congestion-controller",
|
||||
r#"Set the congestion control algorithm. Available: "cubic", "new_reno", "bbr". Default: "cubic""#,
|
||||
"CONGESTION_CONTROLLER",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"heartbeat-interval",
|
||||
"Set the heartbeat interval to ensures that the QUIC connection is not closed when there are relay tasks but no data transfer, in milliseconds. This value needs to be smaller than the maximum idle time set at the server side. Default: 10000",
|
||||
"HEARTBEAT_INTERVAL",
|
||||
);
|
||||
|
||||
opts.optmulti(
|
||||
"",
|
||||
"alpn",
|
||||
"Set ALPN protocols included in the TLS client hello. This option can be used multiple times to set multiple ALPN protocols. If not set, no ALPN extension will be sent",
|
||||
"ALPN_PROTOCOL",
|
||||
);
|
||||
|
||||
opts.optflag(
|
||||
"",
|
||||
"disable-sni",
|
||||
"Not sending the Server Name Indication (SNI) extension during the client TLS handshake",
|
||||
);
|
||||
|
||||
opts.optflag("", "reduce-rtt", "Enable 0-RTT QUIC handshake");
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"request-timeout",
|
||||
"Set the timeout for negotiating tasks between client and the server, in milliseconds. Default: 8000",
|
||||
"REQUEST_TIMEOUT",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"max-udp-relay-packet-size",
|
||||
"UDP relay mode QUIC can transmit UDP packets larger than the MTU. Set this to a higher value allows inbound to receive larger UDP packet. Default: 1500",
|
||||
"MAX_UDP_RELAY_PACKET_SIZE",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"local-port",
|
||||
"Set the listening port for the local socks5 server",
|
||||
"LOCAL_PORT",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"local-ip",
|
||||
r#"Set the listening IP for the local socks5 server. Note that the sock5 server socket will be a dual-stack socket if it is IPv6. Default: "127.0.0.1""#,
|
||||
"LOCAL_IP",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"local-username",
|
||||
"Set the username for the local socks5 server authentication",
|
||||
"LOCAL_USERNAME",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"local-password",
|
||||
"Set the password for the local socks5 server authentication",
|
||||
"LOCAL_PASSWORD",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"log-level",
|
||||
r#"Set the log level. Available: "off", "error", "warn", "info", "debug", "trace". Default: "info""#,
|
||||
"LOG_LEVEL",
|
||||
);
|
||||
|
||||
opts.optflag("v", "version", "Print the version");
|
||||
opts.optflag("h", "help", "Print this help menu");
|
||||
|
||||
let matches = opts.parse(args.skip(1))?;
|
||||
|
||||
if matches.opt_present("help") {
|
||||
return Err(ConfigError::Help(opts.usage(env!("CARGO_PKG_NAME"))));
|
||||
}
|
||||
|
||||
if matches.opt_present("version") {
|
||||
return Err(ConfigError::Version(env!("CARGO_PKG_VERSION")));
|
||||
}
|
||||
|
||||
if !matches.free.is_empty() {
|
||||
return Err(ConfigError::UnexpectedArguments(matches.free.join(", ")));
|
||||
}
|
||||
|
||||
let server = matches.opt_str("server");
|
||||
let server_port = matches.opt_str("server-port").map(|port| port.parse());
|
||||
let token = matches.opt_str("token");
|
||||
let local_port = matches.opt_str("local-port").map(|port| port.parse());
|
||||
|
||||
let mut raw = if let Some(path) = matches.opt_str("config") {
|
||||
let mut raw = RawConfig::from_file(path)?;
|
||||
|
||||
raw.relay.server = Some(
|
||||
server
|
||||
.or(raw.relay.server)
|
||||
.ok_or(ConfigError::MissingOption("server address"))?,
|
||||
);
|
||||
|
||||
raw.relay.port = Some(
|
||||
server_port
|
||||
.transpose()?
|
||||
.or(raw.relay.port)
|
||||
.ok_or(ConfigError::MissingOption("server port"))?,
|
||||
);
|
||||
|
||||
raw.relay.token = Some(
|
||||
token
|
||||
.or(raw.relay.token)
|
||||
.ok_or(ConfigError::MissingOption("token"))?,
|
||||
);
|
||||
|
||||
raw.local.port = Some(
|
||||
local_port
|
||||
.transpose()?
|
||||
.or(raw.local.port)
|
||||
.ok_or(ConfigError::MissingOption("local port"))?,
|
||||
);
|
||||
|
||||
raw
|
||||
} else {
|
||||
let relay = RawRelayConfig {
|
||||
server: Some(server.ok_or(ConfigError::MissingOption("server address"))?),
|
||||
port: Some(server_port.ok_or(ConfigError::MissingOption("server port"))??),
|
||||
token: Some(token.ok_or(ConfigError::MissingOption("token"))?),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let local = RawLocalConfig {
|
||||
port: Some(local_port.ok_or(ConfigError::MissingOption("local port"))??),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
RawConfig {
|
||||
relay,
|
||||
local,
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ip) = matches.opt_str("server-ip") {
|
||||
raw.relay.ip = Some(ip.parse()?);
|
||||
};
|
||||
|
||||
let certificates = matches.opt_strs("certificate");
|
||||
|
||||
if !certificates.is_empty() {
|
||||
raw.relay.certificates = certificates;
|
||||
}
|
||||
|
||||
if let Some(mode) = matches.opt_str("udp-relay-mode") {
|
||||
raw.relay.udp_relay_mode = mode.parse()?;
|
||||
};
|
||||
|
||||
if let Some(cgstn_ctrl) = matches.opt_str("congestion-controller") {
|
||||
raw.relay.congestion_controller = cgstn_ctrl.parse()?;
|
||||
};
|
||||
|
||||
if let Some(interval) = matches.opt_str("heartbeat-interval") {
|
||||
raw.relay.heartbeat_interval = interval.parse()?;
|
||||
};
|
||||
|
||||
let alpn = matches.opt_strs("alpn");
|
||||
|
||||
if !alpn.is_empty() {
|
||||
raw.relay.alpn = alpn;
|
||||
}
|
||||
|
||||
raw.relay.disable_sni |= matches.opt_present("disable-sni");
|
||||
raw.relay.reduce_rtt |= matches.opt_present("reduce-rtt");
|
||||
|
||||
if let Some(timeout) = matches.opt_str("request-timeout") {
|
||||
raw.relay.request_timeout = timeout.parse()?;
|
||||
};
|
||||
|
||||
if let Some(size) = matches.opt_str("max-udp-relay-packet-size") {
|
||||
raw.relay.max_udp_relay_packet_size = size.parse()?;
|
||||
};
|
||||
|
||||
if let Some(local_ip) = matches.opt_str("local-ip") {
|
||||
raw.local.ip = local_ip.parse()?;
|
||||
};
|
||||
|
||||
raw.local.username = matches.opt_str("local-username").or(raw.local.username);
|
||||
raw.local.password = matches.opt_str("local-password").or(raw.local.password);
|
||||
|
||||
if let Some(log_level) = matches.opt_str("log-level") {
|
||||
raw.log_level = log_level.parse()?;
|
||||
};
|
||||
|
||||
Ok(raw)
|
||||
}
|
||||
|
||||
fn from_file(path: String) -> Result<Self, ConfigError> {
|
||||
let file = File::open(&path).map_err(|err| ConfigError::Io(path, err))?;
|
||||
let raw = serde_json::from_reader(file)?;
|
||||
Ok(raw)
|
||||
}
|
||||
}
|
||||
|
||||
enum CongestionController {
|
||||
Cubic,
|
||||
NewReno,
|
||||
Bbr,
|
||||
}
|
||||
|
||||
impl FromStr for CongestionController {
|
||||
type Err = ConfigError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.eq_ignore_ascii_case("cubic") {
|
||||
Ok(Self::Cubic)
|
||||
} else if s.eq_ignore_ascii_case("new_reno") || s.eq_ignore_ascii_case("newreno") {
|
||||
Ok(Self::NewReno)
|
||||
} else if s.eq_ignore_ascii_case("bbr") {
|
||||
Ok(Self::Bbr)
|
||||
} else {
|
||||
Err(ConfigError::InvalidCongestionController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for UdpRelayMode<(), ()> {
|
||||
type Err = ConfigError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.eq_ignore_ascii_case("native") {
|
||||
Ok(Self::Native(()))
|
||||
} else if s.eq_ignore_ascii_case("quic") {
|
||||
Ok(Self::Quic(()))
|
||||
} else {
|
||||
Err(ConfigError::InvalidUdpRelayMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_from_str<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Display,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
T::from_str(&s).map_err(DeError::custom)
|
||||
}
|
||||
|
||||
mod default {
|
||||
use super::*;
|
||||
|
||||
pub(super) const fn certificates() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub(super) const fn udp_relay_mode() -> UdpRelayMode<(), ()> {
|
||||
UdpRelayMode::Native(())
|
||||
}
|
||||
|
||||
pub(super) const fn congestion_controller() -> CongestionController {
|
||||
CongestionController::Cubic
|
||||
}
|
||||
|
||||
pub(super) const fn heartbeat_interval() -> u64 {
|
||||
10000
|
||||
}
|
||||
|
||||
pub(super) const fn alpn() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub(super) const fn disable_sni() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub(super) const fn reduce_rtt() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub(super) const fn request_timeout() -> u64 {
|
||||
8000
|
||||
}
|
||||
|
||||
pub(super) const fn max_udp_relay_packet_size() -> usize {
|
||||
1500
|
||||
}
|
||||
|
||||
pub(super) const fn local_ip() -> IpAddr {
|
||||
IpAddr::V4(Ipv4Addr::LOCALHOST)
|
||||
}
|
||||
|
||||
pub(super) const fn log_level() -> LevelFilter {
|
||||
LevelFilter::Info
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
#[error("{0}")]
|
||||
Help(String),
|
||||
#[error("{0}")]
|
||||
Version(&'static str),
|
||||
#[error("Failed to read '{0}': {1}")]
|
||||
Io(String, #[source] IoError),
|
||||
#[error("Failed to parse the config file: {0}")]
|
||||
ParseConfigJson(#[from] JsonError),
|
||||
#[error(transparent)]
|
||||
ParseArgument(#[from] Fail),
|
||||
#[error("Unexpected arguments: {0}")]
|
||||
UnexpectedArguments(String),
|
||||
#[error("Missing option: {0}")]
|
||||
MissingOption(&'static str),
|
||||
#[error(transparent)]
|
||||
ParseInt(#[from] ParseIntError),
|
||||
#[error(transparent)]
|
||||
ParseAddr(#[from] AddrParseError),
|
||||
#[error("Invalid congestion controller")]
|
||||
InvalidCongestionController,
|
||||
#[error("Invalid udp relay mode")]
|
||||
InvalidUdpRelayMode,
|
||||
#[error("Failed to load the certificate: {0}")]
|
||||
Certificate(#[from] WebpkiError),
|
||||
#[error("Could not load platform certs: {0}")]
|
||||
NativeCertificate(#[source] IoError),
|
||||
#[error("Username and password must be set together for the local socks5 server")]
|
||||
LocalAuthentication,
|
||||
#[error(transparent)]
|
||||
ParseLogLevel(#[from] ParseLevelError),
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
use crate::config::{Config, ConfigError};
|
||||
use std::{env, process};
|
||||
|
||||
mod certificate;
|
||||
mod config;
|
||||
mod relay;
|
||||
mod socks5;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = env::args_os();
|
||||
|
||||
let config = match Config::parse(args) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
match err {
|
||||
ConfigError::Help(help) => println!("{help}"),
|
||||
ConfigError::Version(version) => println!("{version}"),
|
||||
err => eprintln!("{err}"),
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
env_logger::builder()
|
||||
.filter_level(config.log_level)
|
||||
.format_level(true)
|
||||
.format_target(false)
|
||||
.format_module_path(false)
|
||||
.init();
|
||||
|
||||
let (relay, req_tx) = relay::init(
|
||||
config.client_config,
|
||||
config.server_addr,
|
||||
config.token_digest,
|
||||
config.heartbeat_interval,
|
||||
config.reduce_rtt,
|
||||
config.udp_relay_mode,
|
||||
config.request_timeout,
|
||||
config.max_udp_relay_packet_size,
|
||||
)
|
||||
.await;
|
||||
|
||||
let socks5 = match socks5::init(config.local_addr, config.socks5_auth, req_tx).await {
|
||||
Ok(socks5) => socks5,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
res = relay => res,
|
||||
res = socks5 => res,
|
||||
};
|
||||
|
||||
process::exit(1);
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
net::SocketAddr,
|
||||
};
|
||||
use tuic_protocol::Address as TuicAddress;
|
||||
|
||||
pub enum Address {
|
||||
DomainAddress(String, u16),
|
||||
SocketAddress(SocketAddr),
|
||||
}
|
||||
|
||||
impl From<TuicAddress> for Address {
|
||||
fn from(address: TuicAddress) -> Self {
|
||||
match address {
|
||||
TuicAddress::DomainAddress(hostname, port) => Self::DomainAddress(hostname, port),
|
||||
TuicAddress::SocketAddress(socket_addr) => Self::SocketAddress(socket_addr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Address> for TuicAddress {
|
||||
fn from(address: Address) -> Self {
|
||||
match address {
|
||||
Address::DomainAddress(hostname, port) => Self::DomainAddress(hostname, port),
|
||||
Address::SocketAddress(socket_addr) => Self::SocketAddress(socket_addr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Address {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
Address::DomainAddress(hostname, port) => write!(f, "{hostname}:{port}"),
|
||||
Address::SocketAddress(socket_addr) => write!(f, "{socket_addr}"),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,389 +0,0 @@
|
||||
use super::{
|
||||
incoming::{self, Sender as IncomingSender},
|
||||
request::Wait as WaitRequest,
|
||||
stream::{BiStream, IncomingUniStreams, RecvStream, Register as StreamRegister, SendStream},
|
||||
Address, ServerAddr, UdpRelayMode,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use parking_lot::Mutex;
|
||||
use quinn::{ClientConfig, Connection as QuinnConnection, Datagrams, Endpoint, NewConnection};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
future::Future,
|
||||
io::{Error, ErrorKind, Result},
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
ops::{Deref, DerefMut},
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
task::{Context, Poll, Waker},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{
|
||||
net,
|
||||
sync::{mpsc::Sender as MpscSender, Mutex as AsyncMutex, OwnedMutexGuard},
|
||||
time,
|
||||
};
|
||||
use tuic_protocol::Command;
|
||||
|
||||
pub async fn manage_connection(
|
||||
config: ConnectionConfig,
|
||||
conn: Arc<AsyncMutex<Option<Connection>>>,
|
||||
lock: OwnedMutexGuard<Option<Connection>>,
|
||||
mut next_incoming_tx: UdpRelayMode<
|
||||
IncomingSender<Datagrams>,
|
||||
IncomingSender<IncomingUniStreams>,
|
||||
>,
|
||||
wait_req: WaitRequest,
|
||||
) {
|
||||
let mut lock = Some(lock);
|
||||
|
||||
loop {
|
||||
// establish a new connection
|
||||
let new_conn = loop {
|
||||
// start the procedure only if there is a request waiting
|
||||
wait_req.clone().await;
|
||||
|
||||
// try to establish a new connection
|
||||
let (new_conn, dg, uni) = match Connection::connect(&config).await {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
log::error!("[relay] [connection] {err}");
|
||||
|
||||
// sleep 1 second to avoid drawing too much CPU
|
||||
time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// renew the connection mutex
|
||||
// safety: the mutex must be locked before, so this container must have a lock guard inside
|
||||
let mut lock = lock.take().unwrap();
|
||||
*lock.deref_mut() = Some(new_conn.clone());
|
||||
|
||||
// send the incoming streams to `incoming::listen_incoming`
|
||||
match next_incoming_tx {
|
||||
UdpRelayMode::Native(incoming_tx) => {
|
||||
let (tx, rx) = incoming::channel::<Datagrams>();
|
||||
let _ = incoming_tx.send(new_conn.clone(), dg, rx);
|
||||
next_incoming_tx = UdpRelayMode::Native(tx);
|
||||
}
|
||||
UdpRelayMode::Quic(incoming_tx) => {
|
||||
let (tx, rx) = incoming::channel::<IncomingUniStreams>();
|
||||
let _ = incoming_tx.send(new_conn.clone(), uni, rx);
|
||||
next_incoming_tx = UdpRelayMode::Quic(tx);
|
||||
}
|
||||
}
|
||||
|
||||
new_conn.update_max_udp_relay_packet_size();
|
||||
|
||||
// connection established, drop the lock implicitly
|
||||
break new_conn;
|
||||
};
|
||||
|
||||
log::debug!("[relay] [connection] [establish]");
|
||||
|
||||
// wait for the connection to be closed, lock the mutex
|
||||
new_conn.wait_close().await;
|
||||
|
||||
log::debug!("[relay] [connection] [disconnect]");
|
||||
lock = Some(conn.clone().lock_owned().await);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Connection {
|
||||
controller: QuinnConnection,
|
||||
udp_sessions: Arc<UdpSessionMap>,
|
||||
stream_reg: Arc<StreamRegister>,
|
||||
udp_relay_mode: UdpRelayMode<(), ()>,
|
||||
is_closed: IsClosed,
|
||||
default_max_udp_relay_packet_size: usize,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
async fn connect(config: &ConnectionConfig) -> Result<(Self, Datagrams, IncomingUniStreams)> {
|
||||
let (addrs, name) = match &config.server_addr {
|
||||
ServerAddr::SocketAddr { addr, name } => Ok((vec![*addr], name)),
|
||||
ServerAddr::DomainAddr { domain, port } => net::lookup_host((domain.as_str(), *port))
|
||||
.await
|
||||
.map(|res| (res.collect(), domain)),
|
||||
}?;
|
||||
|
||||
let mut conn = None;
|
||||
let mut last_err = None;
|
||||
|
||||
for addr in addrs {
|
||||
match Self::connect_addr(config, addr, name).await {
|
||||
Ok(new_conn) => {
|
||||
conn = Some(new_conn);
|
||||
break;
|
||||
}
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
}
|
||||
|
||||
conn.ok_or_else(|| {
|
||||
last_err
|
||||
.unwrap_or_else(|| Error::new(ErrorKind::Other, "Unable to connect to the server"))
|
||||
})
|
||||
}
|
||||
|
||||
async fn connect_addr(
|
||||
config: &ConnectionConfig,
|
||||
addr: SocketAddr,
|
||||
name: &str,
|
||||
) -> Result<(Self, Datagrams, IncomingUniStreams)> {
|
||||
let bind_addr = match addr {
|
||||
SocketAddr::V4(_) => SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)),
|
||||
SocketAddr::V6(_) => SocketAddr::from((Ipv6Addr::UNSPECIFIED, 0)),
|
||||
};
|
||||
|
||||
let conn = Endpoint::client(bind_addr)?
|
||||
.connect_with(config.quinn_config.clone(), addr, name)
|
||||
.map_err(|err| Error::new(ErrorKind::Other, err))?;
|
||||
|
||||
let NewConnection {
|
||||
connection,
|
||||
datagrams,
|
||||
uni_streams,
|
||||
..
|
||||
} = if config.reduce_rtt {
|
||||
match conn.into_0rtt() {
|
||||
Ok((conn, _)) => conn,
|
||||
Err(conn) => {
|
||||
log::warn!("[relay] [connection] Unable to convert the connection into 0-RTT");
|
||||
conn.await?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conn.await?
|
||||
};
|
||||
|
||||
let conn = Self::new(connection, config).await;
|
||||
let uni_streams = IncomingUniStreams::new(uni_streams, conn.stream_reg.get_registry());
|
||||
|
||||
Ok((conn, datagrams, uni_streams))
|
||||
}
|
||||
|
||||
async fn new(conn: QuinnConnection, config: &ConnectionConfig) -> Self {
|
||||
let conn = Self {
|
||||
controller: conn,
|
||||
udp_sessions: Arc::new(UdpSessionMap::new()),
|
||||
stream_reg: Arc::new(StreamRegister::new()),
|
||||
udp_relay_mode: config.udp_relay_mode,
|
||||
is_closed: IsClosed::new(),
|
||||
default_max_udp_relay_packet_size: config.max_udp_relay_packet_size,
|
||||
};
|
||||
|
||||
// send auth
|
||||
tokio::spawn(Self::send_authentication(conn.clone(), config.token_digest));
|
||||
|
||||
// heartbeat
|
||||
tokio::spawn(Self::heartbeat(conn.clone(), config.heartbeat_interval));
|
||||
|
||||
conn
|
||||
}
|
||||
|
||||
async fn send_authentication(self, token_digest: [u8; 32]) {
|
||||
async fn send_token(conn: &Connection, token_digest: [u8; 32]) -> Result<()> {
|
||||
let mut send = conn.get_send_stream().await?;
|
||||
let cmd = Command::new_authenticate(token_digest);
|
||||
cmd.write_to(&mut send).await?;
|
||||
send.finish().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
match send_token(&self, token_digest).await {
|
||||
Ok(()) => log::debug!("[relay] [connection] [authentication]"),
|
||||
Err(err) => log::warn!("[relay] [connection] [authentication] {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn heartbeat(self, heartbeat_interval: u64) {
|
||||
async fn send_heartbeat(conn: &Connection) -> Result<()> {
|
||||
let mut send = conn.get_send_stream().await?;
|
||||
let cmd = Command::new_heartbeat();
|
||||
cmd.write_to(&mut send).await?;
|
||||
send.finish().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut interval = time::interval(Duration::from_millis(heartbeat_interval));
|
||||
|
||||
while tokio::select! {
|
||||
() = self.wait_close() => false,
|
||||
_ = interval.tick() => true,
|
||||
} {
|
||||
if !self.no_active_stream() || !self.no_active_udp_session() {
|
||||
match send_heartbeat(&self).await {
|
||||
Ok(()) => log::debug!("[relay] [connection] [heartbeat]"),
|
||||
Err(err) => log::warn!("[relay] [connection] [heartbeat] {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_send_stream(&self) -> Result<SendStream> {
|
||||
let send = self.controller.open_uni().await?;
|
||||
let reg = (*self.stream_reg).clone(); // clone inner, not itself
|
||||
Ok(SendStream::new(send, reg))
|
||||
}
|
||||
|
||||
pub async fn get_bi_stream(&self) -> Result<BiStream> {
|
||||
let (send, recv) = self.controller.open_bi().await?;
|
||||
let reg = (*self.stream_reg).clone(); // clone inner, not itself
|
||||
|
||||
Ok(BiStream::new(
|
||||
SendStream::new(send, reg.clone()),
|
||||
RecvStream::new(recv, reg),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn send_datagram(&self, data: Bytes) -> Result<()> {
|
||||
self.controller
|
||||
.send_datagram(data)
|
||||
.map_err(|err| Error::new(ErrorKind::Other, err))
|
||||
}
|
||||
|
||||
pub fn udp_sessions(&self) -> &UdpSessionMap {
|
||||
self.udp_sessions.deref()
|
||||
}
|
||||
|
||||
pub fn udp_relay_mode(&self) -> UdpRelayMode<(), ()> {
|
||||
self.udp_relay_mode
|
||||
}
|
||||
|
||||
pub fn update_max_udp_relay_packet_size(&self) {
|
||||
let size = match self.udp_relay_mode {
|
||||
UdpRelayMode::Native(()) => match self.controller.max_datagram_size() {
|
||||
Some(size) => size,
|
||||
None => {
|
||||
log::warn!("[relay] [connection] Failed to detect the max datagram size");
|
||||
self.default_max_udp_relay_packet_size
|
||||
}
|
||||
},
|
||||
UdpRelayMode::Quic(()) => self.default_max_udp_relay_packet_size,
|
||||
};
|
||||
|
||||
super::MAX_UDP_RELAY_PACKET_SIZE.store(size, Ordering::Release);
|
||||
}
|
||||
|
||||
fn no_active_stream(&self) -> bool {
|
||||
self.stream_reg.count() == 1
|
||||
}
|
||||
|
||||
fn no_active_udp_session(&self) -> bool {
|
||||
self.udp_sessions.is_empty()
|
||||
}
|
||||
|
||||
pub fn set_closed(&self) {
|
||||
self.is_closed.set()
|
||||
}
|
||||
|
||||
fn wait_close(&self) -> IsClosed {
|
||||
self.is_closed.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConnectionConfig {
|
||||
quinn_config: ClientConfig,
|
||||
server_addr: ServerAddr,
|
||||
token_digest: [u8; 32],
|
||||
udp_relay_mode: UdpRelayMode<(), ()>,
|
||||
heartbeat_interval: u64,
|
||||
reduce_rtt: bool,
|
||||
max_udp_relay_packet_size: usize,
|
||||
}
|
||||
|
||||
impl ConnectionConfig {
|
||||
pub fn new(
|
||||
quinn_config: ClientConfig,
|
||||
server_addr: ServerAddr,
|
||||
token_digest: [u8; 32],
|
||||
udp_relay_mode: UdpRelayMode<(), ()>,
|
||||
heartbeat_interval: u64,
|
||||
reduce_rtt: bool,
|
||||
max_udp_relay_packet_size: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
quinn_config,
|
||||
server_addr,
|
||||
token_digest,
|
||||
udp_relay_mode,
|
||||
heartbeat_interval,
|
||||
reduce_rtt,
|
||||
max_udp_relay_packet_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UdpSessionMap(Mutex<HashMap<u32, MpscSender<(Bytes, Address)>>>);
|
||||
|
||||
impl UdpSessionMap {
|
||||
fn new() -> Self {
|
||||
Self(Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&self,
|
||||
id: u32,
|
||||
tx: MpscSender<(Bytes, Address)>,
|
||||
) -> Option<MpscSender<(Bytes, Address)>> {
|
||||
self.0.lock().insert(id, tx)
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &u32) -> Option<MpscSender<(Bytes, Address)>> {
|
||||
self.0.lock().get(id).cloned()
|
||||
}
|
||||
|
||||
pub fn remove(&self, id: &u32) -> Option<MpscSender<(Bytes, Address)>> {
|
||||
self.0.lock().remove(id)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.0.lock().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct IsClosed(Arc<IsClosedInner>);
|
||||
|
||||
struct IsClosedInner {
|
||||
is_closed: AtomicBool,
|
||||
waker: Mutex<Vec<Waker>>,
|
||||
}
|
||||
|
||||
impl IsClosed {
|
||||
fn new() -> Self {
|
||||
Self(Arc::new(IsClosedInner {
|
||||
is_closed: AtomicBool::new(false),
|
||||
// Needs at least 2 slots for `manage_connection()` and `heartbeat()`
|
||||
waker: Mutex::new(Vec::with_capacity(2)),
|
||||
}))
|
||||
}
|
||||
|
||||
fn set(&self) {
|
||||
self.0.is_closed.store(true, Ordering::Release);
|
||||
|
||||
for waker in self.0.waker.lock().drain(..) {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for IsClosed {
|
||||
type Output = ();
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if self.0.is_closed.load(Ordering::Acquire) {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
self.0.waker.lock().push(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
use super::{
|
||||
stream::{IncomingUniStreams, RecvStream},
|
||||
Address, Connection, UdpRelayMode,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures_util::StreamExt;
|
||||
use quinn::{ConnectionError, Datagrams};
|
||||
use std::{
|
||||
io::{Error, ErrorKind, Result},
|
||||
result::Result as StdResult,
|
||||
};
|
||||
use tokio::{
|
||||
io::AsyncReadExt,
|
||||
sync::oneshot::{self, error::RecvError, Receiver as OneshotReceiver, Sender as OneshotSender},
|
||||
};
|
||||
use tuic_protocol::Command as TuicCommand;
|
||||
|
||||
pub async fn listen_incoming(
|
||||
mut next_incoming_rx: UdpRelayMode<Receiver<Datagrams>, Receiver<IncomingUniStreams>>,
|
||||
) {
|
||||
loop {
|
||||
let (conn, incoming);
|
||||
(conn, incoming, next_incoming_rx) = match next_incoming_rx {
|
||||
UdpRelayMode::Native(incoming_rx) => {
|
||||
let (conn, incoming, next_incoming_rx) = incoming_rx.next().await.unwrap(); // safety: the channel must not be closed unless the whole program is already terminated
|
||||
(
|
||||
conn,
|
||||
UdpRelayMode::Native(incoming),
|
||||
UdpRelayMode::Native(next_incoming_rx),
|
||||
)
|
||||
}
|
||||
UdpRelayMode::Quic(incoming_rx) => {
|
||||
let (conn, incoming, next_incoming_rx) = incoming_rx.next().await.unwrap(); // safety: the channel must not be closed unless the whole program is already terminated
|
||||
(
|
||||
conn,
|
||||
UdpRelayMode::Quic(incoming),
|
||||
UdpRelayMode::Quic(next_incoming_rx),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let err = match incoming {
|
||||
UdpRelayMode::Native(mut incoming) => loop {
|
||||
let pkt = match incoming.next().await {
|
||||
Some(Ok(pkt)) => pkt,
|
||||
Some(Err(err)) => break err,
|
||||
None => break ConnectionError::LocallyClosed,
|
||||
};
|
||||
|
||||
// process datagram
|
||||
tokio::spawn(conn.clone().process_incoming_datagram(pkt));
|
||||
},
|
||||
UdpRelayMode::Quic(mut uni) => loop {
|
||||
let recv = match uni.next().await {
|
||||
Some(Ok(recv)) => recv,
|
||||
Some(Err(err)) => break err,
|
||||
None => break ConnectionError::LocallyClosed,
|
||||
};
|
||||
|
||||
// process uni stream
|
||||
tokio::spawn(conn.clone().process_incoming_uni_stream(recv));
|
||||
},
|
||||
};
|
||||
|
||||
match err {
|
||||
ConnectionError::LocallyClosed => log::debug!("[relay] [connection] Locally closed"),
|
||||
ConnectionError::TimedOut => log::debug!("[relay] [connection] Timeout"),
|
||||
err => log::error!("[relay] [connection] {err}"),
|
||||
}
|
||||
|
||||
conn.set_closed();
|
||||
}
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
async fn process_incoming_datagram(self, pkt: Bytes) {
|
||||
async fn parse_header(pkt: Bytes) -> Result<(u32, Bytes, Address)> {
|
||||
let cmd = TuicCommand::read_from(&mut pkt.as_ref()).await?;
|
||||
let cmd_len = cmd.serialized_len();
|
||||
|
||||
match cmd {
|
||||
TuicCommand::Packet {
|
||||
assoc_id,
|
||||
len,
|
||||
addr,
|
||||
} => Ok((
|
||||
assoc_id,
|
||||
pkt.slice(cmd_len..cmd_len + len as usize),
|
||||
Address::from(addr),
|
||||
)),
|
||||
_ => Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"[relay] [connection] Unexpected incoming datagram",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
match parse_header(pkt).await {
|
||||
Ok((assoc_id, pkt, addr)) => self.handle_packet_from(assoc_id, pkt, addr).await,
|
||||
Err(err) => log::warn!("[relay] [connection] {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_incoming_uni_stream(self, recv: RecvStream) {
|
||||
async fn parse_header(mut recv: RecvStream) -> Result<(u32, Bytes, Address)> {
|
||||
let cmd = TuicCommand::read_from(&mut recv).await?;
|
||||
|
||||
match cmd {
|
||||
TuicCommand::Packet {
|
||||
assoc_id,
|
||||
len,
|
||||
addr,
|
||||
} => {
|
||||
let mut buf = vec![0; len as usize];
|
||||
recv.read_exact(&mut buf).await?;
|
||||
let pkt = Bytes::from(buf);
|
||||
Ok((assoc_id, pkt, Address::from(addr)))
|
||||
}
|
||||
_ => Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"[relay] [connection] Unexpected incoming uni stream",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
match parse_header(recv).await {
|
||||
Ok((assoc_id, pkt, addr)) => self.handle_packet_from(assoc_id, pkt, addr).await,
|
||||
Err(err) => log::warn!("[relay] [connection] {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
(Sender(tx), Receiver(rx))
|
||||
}
|
||||
|
||||
pub struct Sender<T>(OneshotSender<(Connection, T, Receiver<T>)>);
|
||||
|
||||
impl<T> Sender<T> {
|
||||
pub fn send(
|
||||
self,
|
||||
conn: Connection,
|
||||
incoming: T,
|
||||
next_incoming_rx: Receiver<T>,
|
||||
) -> StdResult<(), (Connection, T, Receiver<T>)> {
|
||||
self.0.send((conn, incoming, next_incoming_rx))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Receiver<T>(OneshotReceiver<(Connection, T, Self)>);
|
||||
|
||||
impl<T> Receiver<T> {
|
||||
async fn next(self) -> StdResult<(Connection, T, Self), RecvError> {
|
||||
self.0.await
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
use self::{connection::ConnectionConfig, stream::IncomingUniStreams};
|
||||
use quinn::{ClientConfig, Datagrams};
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
future::Future,
|
||||
net::SocketAddr,
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
};
|
||||
use tokio::sync::{
|
||||
mpsc::{self, Sender},
|
||||
Mutex as AsyncMutex,
|
||||
};
|
||||
|
||||
pub use self::{address::Address, connection::Connection, request::Request};
|
||||
|
||||
mod address;
|
||||
mod connection;
|
||||
mod incoming;
|
||||
mod request;
|
||||
mod stream;
|
||||
mod task;
|
||||
|
||||
pub static MAX_UDP_RELAY_PACKET_SIZE: AtomicUsize = AtomicUsize::new(1500);
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn init(
|
||||
quinn_config: ClientConfig,
|
||||
server_addr: ServerAddr,
|
||||
token_digest: [u8; 32],
|
||||
heartbeat_interval: u64,
|
||||
reduce_rtt: bool,
|
||||
udp_relay_mode: UdpRelayMode<(), ()>,
|
||||
req_timeout: u64,
|
||||
max_udp_relay_packet_size: usize,
|
||||
) -> (impl Future<Output = ()>, Sender<Request>) {
|
||||
let (req_tx, req_rx) = mpsc::channel(1);
|
||||
|
||||
let config = ConnectionConfig::new(
|
||||
quinn_config,
|
||||
server_addr.clone(),
|
||||
token_digest,
|
||||
udp_relay_mode,
|
||||
heartbeat_interval,
|
||||
reduce_rtt,
|
||||
max_udp_relay_packet_size,
|
||||
);
|
||||
|
||||
let conn = Arc::new(AsyncMutex::new(None));
|
||||
let conn_lock = conn.clone().lock_owned().await;
|
||||
|
||||
let (incoming_tx, incoming_rx) = match udp_relay_mode {
|
||||
UdpRelayMode::Native(()) => {
|
||||
let (tx, rx) = incoming::channel::<Datagrams>();
|
||||
(UdpRelayMode::Native(tx), UdpRelayMode::Native(rx))
|
||||
}
|
||||
UdpRelayMode::Quic(()) => {
|
||||
let (tx, rx) = incoming::channel::<IncomingUniStreams>();
|
||||
(UdpRelayMode::Quic(tx), UdpRelayMode::Quic(rx))
|
||||
}
|
||||
};
|
||||
|
||||
let (listen_requests, wait_req) = request::listen_requests(conn.clone(), req_rx, req_timeout);
|
||||
let listen_incoming = incoming::listen_incoming(incoming_rx);
|
||||
|
||||
let manage_connection =
|
||||
connection::manage_connection(config, conn, conn_lock, incoming_tx, wait_req);
|
||||
|
||||
let task = async move {
|
||||
log::info!("[relay] Started. Target server: {server_addr}");
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::spawn(manage_connection) => {}
|
||||
_ = tokio::spawn(listen_requests) => {}
|
||||
_ = tokio::spawn(listen_incoming) => {}
|
||||
}
|
||||
};
|
||||
|
||||
(task, req_tx)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ServerAddr {
|
||||
SocketAddr { addr: SocketAddr, name: String },
|
||||
DomainAddr { domain: String, port: u16 },
|
||||
}
|
||||
|
||||
impl Display for ServerAddr {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
ServerAddr::SocketAddr { addr, name } => write!(f, "{addr} ({name})"),
|
||||
ServerAddr::DomainAddr { domain, port } => write!(f, "{domain}:{port}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum UdpRelayMode<N, Q> {
|
||||
Native(N),
|
||||
Quic(Q),
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
use super::{stream::BiStream, Address, Connection};
|
||||
use bytes::Bytes;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use rand::{rngs::StdRng, RngCore, SeedableRng};
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::{Arc, Weak},
|
||||
task::{Context, Poll, Waker},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc::{self, Receiver as MpscReceiver, Sender as MpscSender},
|
||||
oneshot::{self, Receiver as OneshotReceiver, Sender as OneshotSender},
|
||||
Mutex as AsyncMutex,
|
||||
},
|
||||
time,
|
||||
};
|
||||
|
||||
pub fn listen_requests(
|
||||
conn: Arc<AsyncMutex<Option<Connection>>>,
|
||||
mut req_rx: MpscReceiver<Request>,
|
||||
timeout: u64,
|
||||
) -> (impl Future<Output = ()>, Wait) {
|
||||
let (reg, count) = Register::new();
|
||||
|
||||
let listen = async move {
|
||||
while let Some(req) = req_rx.recv().await {
|
||||
tokio::spawn(process_request(conn.clone(), req, timeout, reg.clone()));
|
||||
}
|
||||
};
|
||||
|
||||
(listen, count)
|
||||
}
|
||||
|
||||
async fn process_request(
|
||||
conn: Arc<AsyncMutex<Option<Connection>>>,
|
||||
req: Request,
|
||||
timeout: u64,
|
||||
_reg: Register,
|
||||
) {
|
||||
log::info!("[relay] [task] {req}");
|
||||
|
||||
// try to get the current connection
|
||||
if let Ok(lock) = time::timeout(Duration::from_millis(timeout), conn.lock()).await {
|
||||
let conn = lock.as_ref().unwrap().clone(); // safety: there must be a connection if the lock is aquirable
|
||||
drop(lock);
|
||||
|
||||
match req {
|
||||
Request::Connect { addr, tx } => conn.clone().handle_connect(addr, tx).await,
|
||||
Request::Associate {
|
||||
assoc_id,
|
||||
mut pkt_send_rx,
|
||||
pkt_recv_tx,
|
||||
} => {
|
||||
conn.udp_sessions().insert(assoc_id, pkt_recv_tx);
|
||||
while let Some((pkt, addr)) = pkt_send_rx.recv().await {
|
||||
tokio::spawn(conn.clone().handle_packet_to(
|
||||
assoc_id,
|
||||
pkt,
|
||||
addr,
|
||||
conn.udp_relay_mode(),
|
||||
));
|
||||
}
|
||||
|
||||
log::info!("[relay] [task] [dissociate] [{assoc_id}]");
|
||||
conn.clone().udp_sessions().remove(&assoc_id);
|
||||
conn.handle_dissociate(assoc_id).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("[relay] [task] {req} [timeout]");
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Request {
|
||||
Connect {
|
||||
addr: Address,
|
||||
tx: ConnectResponseSender,
|
||||
},
|
||||
Associate {
|
||||
assoc_id: u32,
|
||||
pkt_send_rx: AssociateSendPacketReceiver,
|
||||
pkt_recv_tx: AssociateRecvPacketSender,
|
||||
},
|
||||
}
|
||||
|
||||
type ConnectResponseSender = OneshotSender<BiStream>;
|
||||
type ConnectResponseReceiver = OneshotReceiver<BiStream>;
|
||||
type AssociateSendPacketSender = MpscSender<(Bytes, Address)>;
|
||||
type AssociateSendPacketReceiver = MpscReceiver<(Bytes, Address)>;
|
||||
type AssociateRecvPacketSender = MpscSender<(Bytes, Address)>;
|
||||
type AssociateRecvPacketReceiver = MpscReceiver<(Bytes, Address)>;
|
||||
|
||||
impl Request {
|
||||
pub fn new_connect(addr: Address) -> (Self, ConnectResponseReceiver) {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
(Request::Connect { addr, tx }, rx)
|
||||
}
|
||||
|
||||
pub fn new_associate() -> (Self, AssociateSendPacketSender, AssociateRecvPacketReceiver) {
|
||||
let assoc_id = get_random_u32();
|
||||
let (pkt_send_tx, pkt_send_rx) = mpsc::channel(1);
|
||||
let (pkt_recv_tx, pkt_recv_rx) = mpsc::channel(1);
|
||||
|
||||
(
|
||||
Self::Associate {
|
||||
assoc_id,
|
||||
pkt_send_rx,
|
||||
pkt_recv_tx,
|
||||
},
|
||||
pkt_send_tx,
|
||||
pkt_recv_rx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Request {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
Request::Connect { addr, .. } => write!(f, "[connect] [{addr}]"),
|
||||
Request::Associate { assoc_id, .. } => write!(f, "[associate] [{assoc_id}]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static RNG: Lazy<Mutex<StdRng>> = Lazy::new(|| Mutex::new(StdRng::from_entropy()));
|
||||
|
||||
fn get_random_u32() -> u32 {
|
||||
RNG.lock().next_u32()
|
||||
}
|
||||
|
||||
pub struct Register(Arc<Mutex<Option<Waker>>>);
|
||||
|
||||
impl Register {
|
||||
pub fn new() -> (Self, Wait) {
|
||||
let reg = Self(Arc::new(Mutex::new(None)));
|
||||
let count = Wait(Arc::downgrade(®.0));
|
||||
(reg, count)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Register {
|
||||
fn clone(&self) -> Self {
|
||||
let reg = Self(self.0.clone());
|
||||
|
||||
// wake the `Wait` hold by `guard_connection`
|
||||
if let Some(waker) = self.0.lock().take() {
|
||||
waker.wake();
|
||||
}
|
||||
|
||||
reg
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Wait(Weak<Mutex<Option<Waker>>>);
|
||||
|
||||
impl Future for Wait {
|
||||
type Output = ();
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if self.0.strong_count() > 1 {
|
||||
// there is a request waiting
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
// there is no request waiting, pend the task
|
||||
if let Some(reg) = self.0.upgrade() {
|
||||
*reg.lock() = Some(cx.waker().clone());
|
||||
}
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
@ -1,208 +0,0 @@
|
||||
use futures_util::Stream;
|
||||
use quinn::{
|
||||
ConnectionError, IncomingUniStreams as QuinnIncomingUniStreams, RecvStream as QuinnRecvStream,
|
||||
SendStream as QuinnSendStream,
|
||||
};
|
||||
use std::{
|
||||
io::{Error, IoSlice, Result},
|
||||
pin::Pin,
|
||||
result::Result as StdResult,
|
||||
sync::{Arc, Weak},
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
|
||||
pub struct SendStream {
|
||||
send: QuinnSendStream,
|
||||
_reg: Register,
|
||||
}
|
||||
|
||||
impl SendStream {
|
||||
#[inline]
|
||||
pub fn new(send: QuinnSendStream, reg: Register) -> Self {
|
||||
Self { send, _reg: reg }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn finish(&mut self) -> Result<()> {
|
||||
self.send.finish().await.map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for SendStream {
|
||||
#[inline]
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize>> {
|
||||
Pin::new(&mut self.send).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_write_vectored(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
bufs: &[IoSlice<'_>],
|
||||
) -> Poll<Result<usize>> {
|
||||
Pin::new(&mut self.send).poll_write_vectored(cx, bufs)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_write_vectored(&self) -> bool {
|
||||
self.send.is_write_vectored()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
|
||||
Pin::new(&mut self.send).poll_flush(cx)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
|
||||
Pin::new(&mut self.send).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RecvStream {
|
||||
recv: QuinnRecvStream,
|
||||
_reg: Register,
|
||||
}
|
||||
|
||||
impl RecvStream {
|
||||
#[inline]
|
||||
pub fn new(recv: QuinnRecvStream, reg: Register) -> Self {
|
||||
Self { recv, _reg: reg }
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for RecvStream {
|
||||
#[inline]
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<Result<()>> {
|
||||
Pin::new(&mut self.recv).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BiStream {
|
||||
send: SendStream,
|
||||
recv: RecvStream,
|
||||
}
|
||||
|
||||
impl BiStream {
|
||||
#[inline]
|
||||
pub fn new(send: SendStream, recv: RecvStream) -> Self {
|
||||
Self { send, recv }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn finish(&mut self) -> Result<()> {
|
||||
self.send.finish().await
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for BiStream {
|
||||
#[inline]
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<Result<()>> {
|
||||
Pin::new(&mut self.recv).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for BiStream {
|
||||
#[inline]
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize>> {
|
||||
Pin::new(&mut self.send).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_write_vectored(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
bufs: &[IoSlice<'_>],
|
||||
) -> Poll<Result<usize>> {
|
||||
Pin::new(&mut self.send).poll_write_vectored(cx, bufs)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_write_vectored(&self) -> bool {
|
||||
self.send.is_write_vectored()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
|
||||
Pin::new(&mut self.send).poll_flush(cx)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
|
||||
Pin::new(&mut self.send).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IncomingUniStreams {
|
||||
incoming: QuinnIncomingUniStreams,
|
||||
reg: Registry,
|
||||
}
|
||||
|
||||
impl IncomingUniStreams {
|
||||
#[inline]
|
||||
pub fn new(incoming: QuinnIncomingUniStreams, reg: Registry) -> Self {
|
||||
Self { incoming, reg }
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for IncomingUniStreams {
|
||||
type Item = StdResult<RecvStream, ConnectionError>;
|
||||
|
||||
#[inline]
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
if let Some(reg) = self.reg.get_register() {
|
||||
Pin::new(&mut self.incoming)
|
||||
.poll_next(cx)
|
||||
.map_ok(|recv| RecvStream::new(recv, reg))
|
||||
} else {
|
||||
// the connection is already dropped
|
||||
Poll::Ready(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Register(Arc<()>);
|
||||
|
||||
impl Register {
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_registry(&self) -> Registry {
|
||||
Registry(Arc::downgrade(&self.0))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn count(&self) -> usize {
|
||||
Arc::strong_count(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Registry(Weak<()>);
|
||||
|
||||
impl Registry {
|
||||
#[inline]
|
||||
pub fn get_register(&self) -> Option<Register> {
|
||||
self.0.upgrade().map(Register)
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
use super::{stream::BiStream, Address, Connection, UdpRelayMode};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use std::io::Result;
|
||||
use tokio::{io::AsyncWriteExt, sync::oneshot::Sender as OneshotSender};
|
||||
use tuic_protocol::{Address as TuicAddress, Command as TuicCommand};
|
||||
|
||||
impl Connection {
|
||||
pub async fn handle_connect(self, addr: Address, tx: OneshotSender<BiStream>) {
|
||||
async fn negotiate_connect(conn: Connection, addr: Address) -> Result<Option<BiStream>> {
|
||||
let cmd = TuicCommand::new_connect(TuicAddress::from(addr));
|
||||
|
||||
let mut stream = conn.get_bi_stream().await?;
|
||||
cmd.write_to(&mut stream).await?;
|
||||
|
||||
let resp = match TuicCommand::read_from(&mut stream).await {
|
||||
Ok(resp) => resp,
|
||||
Err(err) => {
|
||||
stream.finish().await?;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
if let TuicCommand::Response(true) = resp {
|
||||
Ok(Some(stream))
|
||||
} else {
|
||||
stream.finish().await?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
let display_addr = format!("{addr}");
|
||||
|
||||
match negotiate_connect(self, addr).await {
|
||||
Ok(Some(stream)) => {
|
||||
log::debug!("[relay] [task] [connect] [{display_addr}] [success]");
|
||||
let _ = tx.send(stream);
|
||||
}
|
||||
Ok(None) => log::debug!("[relay] [task] [connect] [{display_addr}] [fail]"),
|
||||
Err(err) => log::warn!("[relay] [task] [connect] [{display_addr}] {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_packet_to(
|
||||
self,
|
||||
assoc_id: u32,
|
||||
pkt: Bytes,
|
||||
addr: Address,
|
||||
mode: UdpRelayMode<(), ()>,
|
||||
) {
|
||||
async fn send_packet(
|
||||
conn: Connection,
|
||||
assoc_id: u32,
|
||||
pkt: Bytes,
|
||||
addr: Address,
|
||||
mode: UdpRelayMode<(), ()>,
|
||||
) -> Result<()> {
|
||||
let cmd = TuicCommand::new_packet(assoc_id, pkt.len() as u16, TuicAddress::from(addr));
|
||||
|
||||
match mode {
|
||||
UdpRelayMode::Native(()) => {
|
||||
let mut buf = BytesMut::with_capacity(cmd.serialized_len());
|
||||
cmd.write_to_buf(&mut buf);
|
||||
buf.extend_from_slice(&pkt);
|
||||
let pkt = buf.freeze();
|
||||
conn.send_datagram(pkt)?;
|
||||
}
|
||||
UdpRelayMode::Quic(()) => {
|
||||
let mut send = conn.get_send_stream().await?;
|
||||
cmd.write_to(&mut send).await?;
|
||||
send.write_all(&pkt).await?;
|
||||
send.finish().await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
self.update_max_udp_relay_packet_size();
|
||||
let display_addr = format!("{addr}");
|
||||
|
||||
match send_packet(self, assoc_id, pkt, addr, mode).await {
|
||||
Ok(()) => log::debug!(
|
||||
"[relay] [task] [associate] [{assoc_id}] [send] [{display_addr}] [success]"
|
||||
),
|
||||
Err(err) => {
|
||||
log::warn!("[relay] [task] [associate] [{assoc_id}] [send] [{display_addr}] {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_packet_from(self, assoc_id: u32, pkt: Bytes, addr: Address) {
|
||||
self.update_max_udp_relay_packet_size();
|
||||
let display_addr = format!("{addr}");
|
||||
|
||||
if let Some(recv_pkt_tx) = self.udp_sessions().get(&assoc_id) {
|
||||
log::debug!(
|
||||
"[relay] [task] [associate] [{assoc_id}] [recv] [{display_addr}] [success]"
|
||||
);
|
||||
let _ = recv_pkt_tx.send((pkt, addr)).await;
|
||||
} else {
|
||||
log::warn!("[relay] [task] [associate] [{assoc_id}] [recv] [{display_addr}] No corresponding UDP relay session found");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_dissociate(self, assoc_id: u32) {
|
||||
async fn send_dissociate(conn: Connection, assoc_id: u32) -> Result<()> {
|
||||
let cmd = TuicCommand::new_dissociate(assoc_id);
|
||||
|
||||
let mut send = conn.get_send_stream().await?;
|
||||
cmd.write_to(&mut send).await?;
|
||||
send.finish().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
match send_dissociate(self, assoc_id).await {
|
||||
Ok(()) => log::debug!("[relay] [task] [dissociate] [{assoc_id}] [success]"),
|
||||
Err(err) => log::warn!("relay] [task] [dissociate] [{assoc_id}] {err}"),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
use crate::relay::{self, Address as RelayAddress, Request as RelayRequest};
|
||||
use bytes::Bytes;
|
||||
use socks5_proto::{Address, Reply, UdpHeader};
|
||||
use socks5_server::{
|
||||
connection::associate::{AssociatedUdpSocket, NeedReply},
|
||||
Associate,
|
||||
};
|
||||
use std::{
|
||||
io::Result,
|
||||
net::SocketAddr,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
};
|
||||
use tokio::{
|
||||
net::UdpSocket,
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
};
|
||||
use tuic_protocol::Command as TuicCommand;
|
||||
|
||||
pub async fn handle(
|
||||
conn: Associate<NeedReply>,
|
||||
req_tx: Sender<RelayRequest>,
|
||||
target_addr: Address,
|
||||
) -> Result<()> {
|
||||
async fn bind_udp_socket(conn: &Associate<NeedReply>) -> Result<UdpSocket> {
|
||||
UdpSocket::bind(SocketAddr::from((conn.local_addr()?.ip(), 0))).await
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"[socks5] [{}] [associate] [{target_addr}]",
|
||||
conn.peer_addr()?
|
||||
);
|
||||
|
||||
match bind_udp_socket(&conn)
|
||||
.await
|
||||
.and_then(|socket| socket.local_addr().map(|addr| (socket, addr)))
|
||||
{
|
||||
Ok((socket, socket_addr)) => {
|
||||
let (relay_req, pkt_send_tx, pkt_recv_rx) = RelayRequest::new_associate();
|
||||
let _ = req_tx.send(relay_req).await;
|
||||
|
||||
let mut conn = conn
|
||||
.reply(Reply::Succeeded, Address::SocketAddress(socket_addr))
|
||||
.await?;
|
||||
|
||||
let buf_size = relay::MAX_UDP_RELAY_PACKET_SIZE.load(Ordering::Acquire)
|
||||
- (TuicCommand::max_serialized_len() - UdpHeader::max_serialized_len());
|
||||
let socket = Arc::new(AssociatedUdpSocket::from((socket, buf_size)));
|
||||
let ctrl_addr = conn.peer_addr()?;
|
||||
|
||||
let res = tokio::select! {
|
||||
_ = conn.wait_until_closed() => Ok(()),
|
||||
res = socks5_to_relay(socket.clone(),ctrl_addr, pkt_send_tx) => res,
|
||||
res = relay_to_socks5(socket,ctrl_addr, pkt_recv_rx) => res,
|
||||
};
|
||||
|
||||
let _ = conn.shutdown().await;
|
||||
|
||||
log::info!("[socks5] [{ctrl_addr}] [dissociate] [{target_addr}]");
|
||||
|
||||
res
|
||||
}
|
||||
Err(err) => {
|
||||
let mut conn = conn
|
||||
.reply(Reply::GeneralFailure, Address::unspecified())
|
||||
.await?;
|
||||
|
||||
let _ = conn.shutdown().await;
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn socks5_to_relay(
|
||||
socket: Arc<AssociatedUdpSocket>,
|
||||
ctrl_addr: SocketAddr,
|
||||
pkt_send_tx: Sender<(Bytes, RelayAddress)>,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
let buf_size = relay::MAX_UDP_RELAY_PACKET_SIZE.load(Ordering::Acquire)
|
||||
- (TuicCommand::max_serialized_len() - UdpHeader::max_serialized_len());
|
||||
socket.set_max_packet_size(buf_size);
|
||||
|
||||
let (pkt, frag, dst_addr, src_addr) = socket.recv_from().await?;
|
||||
|
||||
if frag == 0 {
|
||||
log::debug!("[socks5] [{ctrl_addr}] [associate] [packet-to] {dst_addr}");
|
||||
|
||||
let dst_addr = match dst_addr {
|
||||
Address::DomainAddress(domain, port) => RelayAddress::DomainAddress(domain, port),
|
||||
Address::SocketAddress(addr) => RelayAddress::SocketAddress(addr),
|
||||
};
|
||||
|
||||
let _ = pkt_send_tx.send((pkt, dst_addr)).await;
|
||||
socket.connect(src_addr).await?;
|
||||
break;
|
||||
} else {
|
||||
log::warn!("[socks5] [{ctrl_addr}] [associate] [packet-to] socks5 UDP packet fragment is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let buf_size = relay::MAX_UDP_RELAY_PACKET_SIZE.load(Ordering::Acquire)
|
||||
- (TuicCommand::max_serialized_len() - UdpHeader::max_serialized_len());
|
||||
socket.set_max_packet_size(buf_size);
|
||||
|
||||
let (pkt, frag, dst_addr) = socket.recv().await?;
|
||||
|
||||
if frag == 0 {
|
||||
log::debug!("[socks5] [{ctrl_addr}] [associate] [packet-to] {dst_addr}");
|
||||
|
||||
let dst_addr = match dst_addr {
|
||||
Address::DomainAddress(domain, port) => RelayAddress::DomainAddress(domain, port),
|
||||
Address::SocketAddress(addr) => RelayAddress::SocketAddress(addr),
|
||||
};
|
||||
|
||||
let _ = pkt_send_tx.send((pkt, dst_addr)).await;
|
||||
} else {
|
||||
log::warn!("[socks5] [{ctrl_addr}] [associate] [packet-to] socks5 UDP packet fragment is not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn relay_to_socks5(
|
||||
socket: Arc<AssociatedUdpSocket>,
|
||||
ctrl_addr: SocketAddr,
|
||||
mut pkt_recv_rx: Receiver<(Bytes, RelayAddress)>,
|
||||
) -> Result<()> {
|
||||
while let Some((pkt, src_addr)) = pkt_recv_rx.recv().await {
|
||||
log::debug!("[socks5] [{ctrl_addr}] [associate] [packet-from] {src_addr}");
|
||||
|
||||
let src_addr = match src_addr {
|
||||
RelayAddress::DomainAddress(domain, port) => Address::DomainAddress(domain, port),
|
||||
RelayAddress::SocketAddress(addr) => Address::SocketAddress(addr),
|
||||
};
|
||||
|
||||
socket.send(pkt, 0, src_addr).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
use crate::relay::Request as RelayRequest;
|
||||
use socks5_proto::{Address, Reply};
|
||||
use socks5_server::{connection::bind::NeedFirstReply, Bind};
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
pub async fn handle(
|
||||
conn: Bind<NeedFirstReply>,
|
||||
_req_tx: Sender<RelayRequest>,
|
||||
target_addr: Address,
|
||||
) -> Result<()> {
|
||||
log::info!("[socks5] [{}] [bind] [{target_addr}]", conn.peer_addr()?);
|
||||
|
||||
let mut conn = conn
|
||||
.reply(Reply::CommandNotSupported, Address::unspecified())
|
||||
.await?;
|
||||
|
||||
let _ = conn.shutdown().await;
|
||||
|
||||
Err(Error::new(
|
||||
ErrorKind::Unsupported,
|
||||
"BIND command is not supported",
|
||||
))
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
use crate::relay::{Address as RelayAddress, Request as RelayRequest};
|
||||
use socks5_proto::{Address, Reply};
|
||||
use socks5_server::{connection::connect::NeedReply, Connect};
|
||||
use std::io::Result;
|
||||
use tokio::{io, sync::mpsc::Sender};
|
||||
|
||||
pub async fn handle(
|
||||
conn: Connect<NeedReply>,
|
||||
req_tx: Sender<RelayRequest>,
|
||||
target_addr: Address,
|
||||
) -> Result<()> {
|
||||
log::info!("[socks5] [{}] [connect] [{target_addr}]", conn.peer_addr()?);
|
||||
|
||||
let target_addr = match target_addr {
|
||||
Address::DomainAddress(domain, port) => RelayAddress::DomainAddress(domain, port),
|
||||
Address::SocketAddress(addr) => RelayAddress::SocketAddress(addr),
|
||||
};
|
||||
|
||||
let (relay_req, relay_resp_rx) = RelayRequest::new_connect(target_addr);
|
||||
let _ = req_tx.send(relay_req).await;
|
||||
|
||||
if let Ok(mut relay) = relay_resp_rx.await {
|
||||
let mut conn = conn.reply(Reply::Succeeded, Address::unspecified()).await?;
|
||||
io::copy_bidirectional(&mut conn, &mut relay).await?;
|
||||
} else {
|
||||
let mut conn = conn
|
||||
.reply(Reply::NetworkUnreachable, Address::unspecified())
|
||||
.await?;
|
||||
|
||||
let _ = conn.shutdown().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
use crate::relay::Request as RelayRequest;
|
||||
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
|
||||
use socks5_server::{Auth, Connection, IncomingConnection, Server};
|
||||
use std::{
|
||||
future::Future,
|
||||
io::Result,
|
||||
net::{SocketAddr, TcpListener as StdTcpListener},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{net::TcpListener, sync::mpsc::Sender};
|
||||
|
||||
mod associate;
|
||||
mod bind;
|
||||
mod connect;
|
||||
|
||||
pub async fn init(
|
||||
local_addr: SocketAddr,
|
||||
auth: Arc<dyn Auth + Send + Sync>,
|
||||
req_tx: Sender<RelayRequest>,
|
||||
) -> Result<impl Future<Output = ()>> {
|
||||
let socks5 = Socks5::init(local_addr, auth, req_tx).await?;
|
||||
Ok(socks5.run())
|
||||
}
|
||||
|
||||
struct Socks5 {
|
||||
server: Server,
|
||||
req_tx: Sender<RelayRequest>,
|
||||
}
|
||||
|
||||
impl Socks5 {
|
||||
async fn init(
|
||||
local_addr: SocketAddr,
|
||||
auth: Arc<dyn Auth + Send + Sync>,
|
||||
req_tx: Sender<RelayRequest>,
|
||||
) -> Result<Self> {
|
||||
let listener = if local_addr.is_ipv4() {
|
||||
TcpListener::bind(local_addr).await?
|
||||
} else {
|
||||
let socket = Socket::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP))?;
|
||||
socket.set_only_v6(false)?;
|
||||
socket.set_reuse_address(true)?;
|
||||
socket.bind(&SockAddr::from(local_addr))?;
|
||||
socket.listen(128)?;
|
||||
TcpListener::from_std(StdTcpListener::from(socket))?
|
||||
};
|
||||
|
||||
let server = Server::new(listener, auth);
|
||||
|
||||
Ok(Self { server, req_tx })
|
||||
}
|
||||
|
||||
async fn run(self) {
|
||||
async fn handle_connection(
|
||||
conn: IncomingConnection,
|
||||
req_tx: Sender<RelayRequest>,
|
||||
) -> Result<()> {
|
||||
match conn.handshake().await? {
|
||||
Connection::Connect(conn, addr) => connect::handle(conn, req_tx, addr).await,
|
||||
Connection::Bind(conn, addr) => bind::handle(conn, req_tx, addr).await,
|
||||
Connection::Associate(conn, addr) => associate::handle(conn, req_tx, addr).await,
|
||||
}
|
||||
}
|
||||
|
||||
match self.server.local_addr() {
|
||||
Ok(addr) => log::info!("[socks5] Started. Listening: {addr}"),
|
||||
Err(err) => {
|
||||
log::error!("[socks5] Failed to get local socks5 server address: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let (conn, addr) = match self.server.accept().await {
|
||||
Ok((conn, addr)) => {
|
||||
log::debug!("[socks5] [{addr}] [establish]");
|
||||
(conn, addr)
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("[socks5] Failed to accept connection: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let req_tx = self.req_tx.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match handle_connection(conn, req_tx).await {
|
||||
Ok(()) => log::debug!("[socks5] [{addr}] [disconnect]"),
|
||||
Err(err) => log::warn!("[socks5] [{addr}] {err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "tuic-protocol"
|
||||
version = "4.1.2"
|
||||
authors = ["EAimTY <ea.imty@gmail.com>"]
|
||||
description = ""
|
||||
categories = ["network-programming"]
|
||||
keywords = ["tuic", "proxy", "quic"]
|
||||
edition = "2021"
|
||||
rust-version = "1.59"
|
||||
readme = "../README.md"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/EAimTY/tuic"
|
||||
|
||||
[dependencies]
|
||||
byteorder = "1.4.*"
|
||||
bytes = "1.2.*"
|
||||
tokio = { version = "1.20.*", features = ["io-util"] }
|
@ -1,202 +0,0 @@
|
||||
# tuic-protocol
|
||||
|
||||
TUIC protocol is used to communicate between the TUIC client and the TUIC server.
|
||||
|
||||
## Overview
|
||||
|
||||
TUIC protocol is a stateful protocol. It is designed to be simple yet efficient. The current version is `0x04`.
|
||||
|
||||
## Command
|
||||
|
||||
Relay tasks are negotiated with `Command`s.
|
||||
All fields are in Big Endian unless otherwise noted.
|
||||
|
||||
```plain
|
||||
+-----+------+----------+
|
||||
| VER | TYPE | OPT |
|
||||
+-----+------+----------+
|
||||
| 1 | 1 | Variable |
|
||||
+-----+------+----------+
|
||||
```
|
||||
|
||||
where:
|
||||
|
||||
- `VER` - protocol version
|
||||
- `TYPE` - command type
|
||||
- `OPT` - command type specific data
|
||||
|
||||
### Command Types
|
||||
|
||||
There are six types of commands:
|
||||
|
||||
- `0x00` - `Authenticate` - used to authenticate the client
|
||||
- `0x01` - `Connect` - used to request a client-to-server TCP relay
|
||||
- `0x02` - `Packet` - used to forward a UDP packet
|
||||
- `0x03` - `Dissociate` - used to stop a UDP relay session
|
||||
- `0x04` - `Heartbeat` - used to keep a QUIC connection alive
|
||||
- `0xff` - `Response` - used to respond to a `Command` (currently only used for replying `Connect`)
|
||||
|
||||
### Command Type Specific Data
|
||||
|
||||
#### `Authenticate`
|
||||
|
||||
```plain
|
||||
+-----+
|
||||
| TKN |
|
||||
+-----+
|
||||
| 32 |
|
||||
+-----+
|
||||
```
|
||||
|
||||
where:
|
||||
|
||||
- `TKN` - authentication token, hashed with [BLAKE3](https://github.com/BLAKE3-team/BLAKE3)
|
||||
|
||||
#### `Connect`
|
||||
|
||||
```plain
|
||||
+----------+
|
||||
| ADDR |
|
||||
+----------+
|
||||
| Variable |
|
||||
+----------+
|
||||
```
|
||||
|
||||
where:
|
||||
|
||||
- `ADDR` - target address. See [Address](#address)
|
||||
|
||||
#### `Packet`
|
||||
|
||||
```plain
|
||||
+----------+-----+----------+
|
||||
| ASSOC_ID | LEN | ADDR |
|
||||
+----------+-----+----------+
|
||||
| 4 | 2 | Variable |
|
||||
+----------+-----+----------+
|
||||
```
|
||||
|
||||
where:
|
||||
|
||||
- `ASSOC_ID` - UDP relay session ID. See [UDP relaying](#udp-relaying)
|
||||
- `LEN` - length of the UDP packet
|
||||
- `ADDR` - target (command from TUIC client) or source (command from TUIC server) address. See [Address](#address)
|
||||
|
||||
#### `Dissociate`
|
||||
|
||||
```plain
|
||||
+----------+
|
||||
| ASSOC_ID |
|
||||
+----------+
|
||||
| 4 |
|
||||
+----------+
|
||||
```
|
||||
|
||||
#### `Heartbeat`
|
||||
|
||||
```plain
|
||||
+-+
|
||||
| |
|
||||
+-+
|
||||
| |
|
||||
+-+
|
||||
```
|
||||
|
||||
#### `Response`
|
||||
|
||||
```plain
|
||||
+-----+
|
||||
| REP |
|
||||
+-----+
|
||||
| 1 |
|
||||
+-----+
|
||||
```
|
||||
|
||||
where:
|
||||
|
||||
- `REP` - reply code, which can be:
|
||||
|
||||
- `0x00` - SUCCEEDED
|
||||
- `0xff` - FAILED
|
||||
|
||||
### 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:
|
||||
|
||||
- `0x00` - fully-qualified domain name(the first byte indicates the length of the domain name)
|
||||
- `0x01` - IPv4 address
|
||||
- `0x02` - IPv6 address
|
||||
|
||||
## Procedures
|
||||
|
||||
TUIC protocol relies heavily on the multiplex-able trusted channel provided by QUIC. The protocol itself does not provide any security.
|
||||
|
||||
### Authentication
|
||||
|
||||
Once the QUIC connection is established between the server and the client, the client must immediately open a unidirectional stream and send an `Authenticate` command.
|
||||
|
||||
If the authentication token is unmatched, or the server does not receive an authentication request from the client within the set time, the server will close the QUIC connection with specific error code and reason. See [Error Handling](#error-handling) for more details.
|
||||
|
||||
Note that the server will not reply to the `Authenticate` command. The client should close the stream immediately after successfully sending the command. The client can start sending other data without waiting for the `Authenticate` command to be sent.
|
||||
|
||||
The server will accept other streams carrying relay task requests before the authentication is completed, but it will stop after the Command Header is read, and will not do actual processing until the authentication is completed.
|
||||
|
||||
### TCP Relaying
|
||||
|
||||
`Connect` is used to request a client-to-server TCP relay.
|
||||
|
||||
To establish a TCP connection with the target address via the relay server, the client needs to open a bidirectional stream and send a `Connect` command. After the server receives the request, it will try to establish a TCP connection to the target address. Depending on success, the server replies with a `Response` command via the same bidirectional stream.
|
||||
|
||||
If the attempt to connect to the target address fails, the server must close the bidirectional stream as soon as the `Response` transmission is complete.
|
||||
|
||||
If the connection to the target is successful, the server will synchronize the data in the bidirectional stream with the TCP stream between the server and the target address until one of the streams is disconnected.
|
||||
|
||||
### UDP Relaying
|
||||
|
||||
TUIC achieves 0-RTT FullCone UDP forwarding by synchronizing UDP session ID between the client and the server.
|
||||
|
||||
The server should create a UDP session table for each QUIC connection, mapping every associate ID to a UDP socket.
|
||||
|
||||
The associate ID is a 32-bit unsigned integer randomly generated by the client, which is placed in the `Packet` command and appended to the UDP packet data to be sent. When the client wants to send UDP packets using the same UDP socket of the server, the attached associate ID should be the same.
|
||||
|
||||
When the server receives the `Packet` command, it 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, appends the `Packet` command then sends back to the client.
|
||||
|
||||
When the client wants to relay a UDP packet, it should send the UDP packet with the `Packet` command attached from:
|
||||
|
||||
- Unidirectional stream (UDP relay mode `quic`)
|
||||
- Datagram (UDP relay mode `native`)
|
||||
|
||||
When the server receives the first `Packet` command, it will consider that the client is using corresponded UDP relay mode. When the UDP socket associated receives a UDP packet, the server should send the packet back to the client in the same way.
|
||||
|
||||
When a client wants to stop associating a UDP socket, it should notify the server by sending a `Dissociate` command using a unidirectional stream. The server will remove the associate ID and release the UDP socket from the UDP session table.
|
||||
|
||||
When the QUIC connection is disconnected, the server will release all UDP sockets in the connection's UDP session table and delete all sessions.
|
||||
|
||||
### Heartbeat
|
||||
|
||||
Even if there is an unclosed stream between the server and the client, the QUIC connection will still timeout after a period of idle time. This affects the timeout behavior for tasks without persistent data transfer (such as SSH connections).
|
||||
|
||||
To solve this problem, when there is an active relay task (TCP relaying or UDP session), the client should send a `Heartbeat` command to the server every few seconds to keep the connection alive.
|
||||
|
||||
### Error Handling
|
||||
|
||||
When the server detects the following errors, it should close the QUIC connection immediately with the corresponding error code:
|
||||
|
||||
- Protocol Error - `0xfffffff0` - TUIC protocol version mismatch, or the server cannot parse the header
|
||||
- Authentication Failed - `0xfffffff1` - Authentication token mismatch
|
||||
- Authentication Timeout - `0xfffffff2` - Authentication timeout
|
||||
- Bad Command - `0xfffffff3` - Command received from wrong stream / datagram
|
@ -1,355 +0,0 @@
|
||||
//! The TUIC protocol
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use bytes::BufMut;
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
io::{Cursor, Error, ErrorKind, Result},
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
|
||||
pub const TUIC_PROTOCOL_VERSION: u8 = 0x04;
|
||||
|
||||
/// Command
|
||||
///
|
||||
/// ```plain
|
||||
/// +-----+------+----------+
|
||||
/// | VER | TYPE | OPT |
|
||||
/// +-----+------+----------+
|
||||
/// | 1 | 1 | Variable |
|
||||
/// +-----+------+----------+
|
||||
/// ```
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone)]
|
||||
pub enum Command {
|
||||
Response(bool),
|
||||
Authenticate {
|
||||
digest: [u8; 32],
|
||||
},
|
||||
Connect {
|
||||
addr: Address,
|
||||
},
|
||||
Packet {
|
||||
assoc_id: u32,
|
||||
len: u16,
|
||||
addr: Address,
|
||||
},
|
||||
Dissociate {
|
||||
assoc_id: u32,
|
||||
},
|
||||
Heartbeat,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
const TYPE_RESPONSE: u8 = 0xff;
|
||||
const TYPE_AUTHENTICATE: u8 = 0x00;
|
||||
const TYPE_CONNECT: u8 = 0x01;
|
||||
const TYPE_PACKET: u8 = 0x02;
|
||||
const TYPE_DISSOCIATE: u8 = 0x03;
|
||||
const TYPE_HEARTBEAT: u8 = 0x04;
|
||||
|
||||
const RESPONSE_SUCCEEDED: u8 = 0x00;
|
||||
const RESPONSE_FAILED: u8 = 0xff;
|
||||
|
||||
pub fn new_response(is_succeeded: bool) -> Self {
|
||||
Self::Response(is_succeeded)
|
||||
}
|
||||
|
||||
pub fn new_authenticate(digest: [u8; 32]) -> Self {
|
||||
Self::Authenticate { digest }
|
||||
}
|
||||
|
||||
pub fn new_connect(addr: Address) -> Self {
|
||||
Self::Connect { addr }
|
||||
}
|
||||
|
||||
pub fn new_packet(assoc_id: u32, len: u16, addr: Address) -> Self {
|
||||
Self::Packet {
|
||||
assoc_id,
|
||||
len,
|
||||
addr,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_dissociate(assoc_id: u32) -> Self {
|
||||
Self::Dissociate { assoc_id }
|
||||
}
|
||||
|
||||
pub fn new_heartbeat() -> Self {
|
||||
Self::Heartbeat
|
||||
}
|
||||
|
||||
pub async fn read_from<R>(r: &mut R) -> Result<Self>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
let ver = r.read_u8().await?;
|
||||
|
||||
if ver != TUIC_PROTOCOL_VERSION {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Unsupported,
|
||||
format!("Unsupported TUIC version: {ver}"),
|
||||
));
|
||||
}
|
||||
|
||||
let cmd = r.read_u8().await?;
|
||||
match cmd {
|
||||
Self::TYPE_RESPONSE => {
|
||||
let resp = r.read_u8().await?;
|
||||
match resp {
|
||||
Self::RESPONSE_SUCCEEDED => Ok(Self::new_response(true)),
|
||||
Self::RESPONSE_FAILED => Ok(Self::new_response(false)),
|
||||
_ => Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Invalid response code: {resp}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
Self::TYPE_AUTHENTICATE => {
|
||||
let mut digest = [0; 32];
|
||||
r.read_exact(&mut digest).await?;
|
||||
Ok(Self::new_authenticate(digest))
|
||||
}
|
||||
Self::TYPE_CONNECT => {
|
||||
let addr = Address::read_from(r).await?;
|
||||
Ok(Self::new_connect(addr))
|
||||
}
|
||||
Self::TYPE_PACKET => {
|
||||
let mut buf = [0; 6];
|
||||
r.read_exact(&mut buf).await?;
|
||||
let mut rdr = Cursor::new(buf);
|
||||
|
||||
let assoc_id = ReadBytesExt::read_u32::<BigEndian>(&mut rdr).unwrap();
|
||||
let len = ReadBytesExt::read_u16::<BigEndian>(&mut rdr).unwrap();
|
||||
let addr = Address::read_from(r).await?;
|
||||
|
||||
Ok(Self::new_packet(assoc_id, len, addr))
|
||||
}
|
||||
Self::TYPE_DISSOCIATE => {
|
||||
let assoc_id = r.read_u32().await?;
|
||||
Ok(Self::new_dissociate(assoc_id))
|
||||
}
|
||||
Self::TYPE_HEARTBEAT => Ok(Self::new_heartbeat()),
|
||||
_ => Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Invalid command: {cmd}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write_to<W>(&self, w: &mut W) -> Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
let mut buf = Vec::with_capacity(self.serialized_len());
|
||||
self.write_to_buf(&mut buf);
|
||||
w.write_all(&buf).await
|
||||
}
|
||||
|
||||
pub fn write_to_buf<B: BufMut>(&self, buf: &mut B) {
|
||||
buf.put_u8(TUIC_PROTOCOL_VERSION);
|
||||
|
||||
match self {
|
||||
Self::Response(is_succeeded) => {
|
||||
buf.put_u8(Self::TYPE_RESPONSE);
|
||||
if *is_succeeded {
|
||||
buf.put_u8(Self::RESPONSE_SUCCEEDED);
|
||||
} else {
|
||||
buf.put_u8(Self::RESPONSE_FAILED);
|
||||
}
|
||||
}
|
||||
Self::Authenticate { digest } => {
|
||||
buf.put_u8(Self::TYPE_AUTHENTICATE);
|
||||
buf.put_slice(digest);
|
||||
}
|
||||
Self::Connect { addr } => {
|
||||
buf.put_u8(Self::TYPE_CONNECT);
|
||||
addr.write_to_buf(buf);
|
||||
}
|
||||
Self::Packet {
|
||||
assoc_id,
|
||||
len,
|
||||
addr,
|
||||
} => {
|
||||
buf.put_u8(Self::TYPE_PACKET);
|
||||
buf.put_u32(*assoc_id);
|
||||
buf.put_u16(*len);
|
||||
addr.write_to_buf(buf);
|
||||
}
|
||||
Self::Dissociate { assoc_id } => {
|
||||
buf.put_u8(Self::TYPE_DISSOCIATE);
|
||||
buf.put_u32(*assoc_id);
|
||||
}
|
||||
Self::Heartbeat => {
|
||||
buf.put_u8(Self::TYPE_HEARTBEAT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialized_len(&self) -> usize {
|
||||
2 + match self {
|
||||
Self::Response(_) => 1,
|
||||
Self::Authenticate { .. } => 32,
|
||||
Self::Connect { addr } => addr.serialized_len(),
|
||||
Self::Packet { addr, .. } => 6 + addr.serialized_len(),
|
||||
Self::Dissociate { .. } => 4,
|
||||
Self::Heartbeat => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn max_serialized_len() -> usize {
|
||||
2 + 6 + Address::max_serialized_len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Address
|
||||
///
|
||||
/// ```plain
|
||||
/// +------+----------+----------+
|
||||
/// | TYPE | ADDR | PORT |
|
||||
/// +------+----------+----------+
|
||||
/// | 1 | Variable | 2 |
|
||||
/// +------+----------+----------+
|
||||
/// ```
|
||||
///
|
||||
/// The address type can be one of the following:
|
||||
/// 0x00: fully-qualified domain name (the first byte indicates the length of the domain name)
|
||||
/// 0x01: IPv4 address
|
||||
/// 0x02: IPv6 address
|
||||
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum Address {
|
||||
DomainAddress(String, u16),
|
||||
SocketAddress(SocketAddr),
|
||||
}
|
||||
|
||||
impl Address {
|
||||
const TYPE_DOMAIN: u8 = 0x00;
|
||||
const TYPE_IPV4: u8 = 0x01;
|
||||
const TYPE_IPV6: u8 = 0x02;
|
||||
|
||||
pub async fn read_from<R>(stream: &mut R) -> Result<Self>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
let addr_type = stream.read_u8().await?;
|
||||
|
||||
match addr_type {
|
||||
Self::TYPE_DOMAIN => {
|
||||
let len = stream.read_u8().await? as usize;
|
||||
|
||||
let mut buf = vec![0; len + 2];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
|
||||
let port = ReadBytesExt::read_u16::<BigEndian>(&mut &buf[len..]).unwrap();
|
||||
buf.truncate(len);
|
||||
|
||||
let addr = String::from_utf8(buf).map_err(|err| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("Invalid address encoding: {err}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Self::DomainAddress(addr, port))
|
||||
}
|
||||
Self::TYPE_IPV4 => {
|
||||
let mut buf = [0; 6];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
let mut rdr = Cursor::new(buf);
|
||||
|
||||
let addr = Ipv4Addr::new(
|
||||
ReadBytesExt::read_u8(&mut rdr).unwrap(),
|
||||
ReadBytesExt::read_u8(&mut rdr).unwrap(),
|
||||
ReadBytesExt::read_u8(&mut rdr).unwrap(),
|
||||
ReadBytesExt::read_u8(&mut rdr).unwrap(),
|
||||
);
|
||||
|
||||
let port = ReadBytesExt::read_u16::<BigEndian>(&mut rdr).unwrap();
|
||||
|
||||
Ok(Self::SocketAddress(SocketAddr::from((addr, port))))
|
||||
}
|
||||
Self::TYPE_IPV6 => {
|
||||
let mut buf = [0; 18];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
let mut rdr = Cursor::new(buf);
|
||||
|
||||
let addr = Ipv6Addr::new(
|
||||
ReadBytesExt::read_u16::<BigEndian>(&mut rdr).unwrap(),
|
||||
ReadBytesExt::read_u16::<BigEndian>(&mut rdr).unwrap(),
|
||||
ReadBytesExt::read_u16::<BigEndian>(&mut rdr).unwrap(),
|
||||
ReadBytesExt::read_u16::<BigEndian>(&mut rdr).unwrap(),
|
||||
ReadBytesExt::read_u16::<BigEndian>(&mut rdr).unwrap(),
|
||||
ReadBytesExt::read_u16::<BigEndian>(&mut rdr).unwrap(),
|
||||
ReadBytesExt::read_u16::<BigEndian>(&mut rdr).unwrap(),
|
||||
ReadBytesExt::read_u16::<BigEndian>(&mut rdr).unwrap(),
|
||||
);
|
||||
|
||||
let port = ReadBytesExt::read_u16::<BigEndian>(&mut rdr).unwrap();
|
||||
|
||||
Ok(Self::SocketAddress(SocketAddr::from((addr, port))))
|
||||
}
|
||||
_ => Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Unsupported address type: {addr_type}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write_to<W>(&self, writer: &mut W) -> Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
let mut buf = Vec::with_capacity(self.serialized_len());
|
||||
self.write_to_buf(&mut buf);
|
||||
writer.write_all(&buf).await
|
||||
}
|
||||
|
||||
pub fn write_to_buf<B: BufMut>(&self, buf: &mut B) {
|
||||
match self {
|
||||
Self::DomainAddress(addr, port) => {
|
||||
buf.put_u8(Self::TYPE_DOMAIN);
|
||||
buf.put_u8(addr.len() as u8);
|
||||
buf.put_slice(addr.as_bytes());
|
||||
buf.put_u16(*port);
|
||||
}
|
||||
Self::SocketAddress(addr) => match addr {
|
||||
SocketAddr::V4(addr) => {
|
||||
buf.put_u8(Self::TYPE_IPV4);
|
||||
buf.put_slice(&addr.ip().octets());
|
||||
buf.put_u16(addr.port());
|
||||
}
|
||||
SocketAddr::V6(addr) => {
|
||||
buf.put_u8(Self::TYPE_IPV6);
|
||||
for seg in addr.ip().segments() {
|
||||
buf.put_u16(seg);
|
||||
}
|
||||
buf.put_u16(addr.port());
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialized_len(&self) -> usize {
|
||||
1 + match self {
|
||||
Address::DomainAddress(addr, _) => 1 + addr.len() + 2,
|
||||
Address::SocketAddress(addr) => match addr {
|
||||
SocketAddr::V4(_) => 6,
|
||||
SocketAddr::V6(_) => 18,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn max_serialized_len() -> usize {
|
||||
1 + 1 + u8::MAX as usize + 2
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Address {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
Self::DomainAddress(addr, port) => write!(f, "{addr}:{port}"),
|
||||
Self::SocketAddress(addr) => write!(f, "{addr}"),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
[package]
|
||||
name = "tuic-server"
|
||||
version = "0.8.5"
|
||||
authors = ["EAimTY <ea.imty@gmail.com>"]
|
||||
description = "Delicately-TUICed high-performance proxy built on top of the QUIC protocol"
|
||||
categories = ["network-programming", "command-line-utilities"]
|
||||
keywords = ["tuic", "proxy", "quic"]
|
||||
edition = "2021"
|
||||
rust-version = "1.59"
|
||||
readme = "../README.md"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/EAimTY/tuic"
|
||||
|
||||
[dependencies]
|
||||
tuic-protocol = { path="../protocol" }
|
||||
|
||||
blake3 = "1.3.*"
|
||||
bytes = "1.2.*"
|
||||
crossbeam-utils = { version = "0.8.*", default-features = false }
|
||||
env_logger = { version = "0.9.*", features = ["humantime"], default-features = false }
|
||||
futures-util = { version = "0.3.*", default-features = false }
|
||||
getopts = "0.2.*"
|
||||
log = { version = "0.4.*", features = ["serde", "std"] }
|
||||
parking_lot = { version = "0.12.*", features = ["send_guard"] }
|
||||
quinn = "0.8.*"
|
||||
rustls = { version = "0.20.*", features = ["quic"], default-features = false }
|
||||
rustls-pemfile = "1.0.*"
|
||||
serde = { version = "1.0.*", features = ["derive", "std"], default-features = false }
|
||||
serde_json = { version = "1.0.*", features = ["std"], default-features = false }
|
||||
socket2 = "0.4.*"
|
||||
thiserror = "1.0.*"
|
||||
tokio = { version = "1.20.*", features = ["io-util", "macros", "net", "parking_lot", "rt-multi-thread", "sync", "time"] }
|
@ -1,39 +0,0 @@
|
||||
use rustls::{Certificate, PrivateKey};
|
||||
use rustls_pemfile::Item;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{BufReader, Error as IoError},
|
||||
};
|
||||
|
||||
pub fn load_certificates(path: &str) -> Result<Vec<Certificate>, IoError> {
|
||||
let mut file = BufReader::new(File::open(path)?);
|
||||
let mut certs = Vec::new();
|
||||
|
||||
while let Ok(Some(item)) = rustls_pemfile::read_one(&mut file) {
|
||||
if let Item::X509Certificate(cert) = item {
|
||||
certs.push(Certificate(cert));
|
||||
}
|
||||
}
|
||||
|
||||
if certs.is_empty() {
|
||||
certs = vec![Certificate(fs::read(path)?)];
|
||||
}
|
||||
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
pub fn load_private_key(path: &str) -> Result<PrivateKey, IoError> {
|
||||
let mut file = BufReader::new(File::open(path)?);
|
||||
let mut priv_key = None;
|
||||
|
||||
while let Ok(Some(item)) = rustls_pemfile::read_one(&mut file) {
|
||||
if let Item::RSAKey(key) | Item::PKCS8Key(key) | Item::ECKey(key) = item {
|
||||
priv_key = Some(key);
|
||||
}
|
||||
}
|
||||
|
||||
priv_key
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| fs::read(path))
|
||||
.map(PrivateKey)
|
||||
}
|
@ -1,428 +0,0 @@
|
||||
use crate::certificate;
|
||||
use getopts::{Fail, Options};
|
||||
use log::{LevelFilter, ParseLevelError};
|
||||
use quinn::{
|
||||
congestion::{BbrConfig, CubicConfig, NewRenoConfig},
|
||||
IdleTimeout, ServerConfig, VarInt,
|
||||
};
|
||||
use rustls::{version::TLS13, Error as RustlsError, ServerConfig as RustlsServerConfig};
|
||||
use serde::{de::Error as DeError, Deserialize, Deserializer};
|
||||
use serde_json::Error as JsonError;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
env::ArgsOs,
|
||||
fmt::Display,
|
||||
fs::File,
|
||||
io::Error as IoError,
|
||||
net::{AddrParseError, IpAddr, Ipv4Addr, SocketAddr},
|
||||
num::ParseIntError,
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
pub struct Config {
|
||||
pub server_config: ServerConfig,
|
||||
pub listen_addr: SocketAddr,
|
||||
pub token: HashSet<[u8; 32]>,
|
||||
pub authentication_timeout: Duration,
|
||||
pub max_udp_relay_packet_size: usize,
|
||||
pub log_level: LevelFilter,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn parse(args: ArgsOs) -> Result<Self, ConfigError> {
|
||||
let raw = RawConfig::parse(args)?;
|
||||
|
||||
let server_config = {
|
||||
let cert_path = raw.certificate.unwrap();
|
||||
let certs = certificate::load_certificates(&cert_path)
|
||||
.map_err(|err| ConfigError::Io(cert_path, err))?;
|
||||
|
||||
let priv_key_path = raw.private_key.unwrap();
|
||||
let priv_key = certificate::load_private_key(&priv_key_path)
|
||||
.map_err(|err| ConfigError::Io(priv_key_path, err))?;
|
||||
|
||||
let mut crypto = RustlsServerConfig::builder()
|
||||
.with_safe_default_cipher_suites()
|
||||
.with_safe_default_kx_groups()
|
||||
.with_protocol_versions(&[&TLS13])
|
||||
.unwrap()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, priv_key)?;
|
||||
|
||||
crypto.max_early_data_size = u32::MAX;
|
||||
crypto.alpn_protocols = raw.alpn.into_iter().map(|alpn| alpn.into_bytes()).collect();
|
||||
|
||||
let mut config = ServerConfig::with_crypto(Arc::new(crypto));
|
||||
let transport = Arc::get_mut(&mut config.transport).unwrap();
|
||||
|
||||
match raw.congestion_controller {
|
||||
CongestionController::Bbr => {
|
||||
transport.congestion_controller_factory(Arc::new(BbrConfig::default()));
|
||||
}
|
||||
CongestionController::Cubic => {
|
||||
transport.congestion_controller_factory(Arc::new(CubicConfig::default()));
|
||||
}
|
||||
CongestionController::NewReno => {
|
||||
transport.congestion_controller_factory(Arc::new(NewRenoConfig::default()));
|
||||
}
|
||||
}
|
||||
|
||||
transport
|
||||
.max_idle_timeout(Some(IdleTimeout::from(VarInt::from_u32(raw.max_idle_time))));
|
||||
|
||||
config
|
||||
};
|
||||
|
||||
let listen_addr = SocketAddr::from((raw.ip, raw.port.unwrap()));
|
||||
|
||||
let token = raw
|
||||
.token
|
||||
.into_iter()
|
||||
.map(|token| *blake3::hash(&token.into_bytes()).as_bytes())
|
||||
.collect();
|
||||
|
||||
let authentication_timeout = Duration::from_secs(raw.authentication_timeout);
|
||||
let max_udp_relay_packet_size = raw.max_udp_relay_packet_size;
|
||||
let log_level = raw.log_level;
|
||||
|
||||
Ok(Self {
|
||||
server_config,
|
||||
listen_addr,
|
||||
token,
|
||||
authentication_timeout,
|
||||
max_udp_relay_packet_size,
|
||||
log_level,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawConfig {
|
||||
port: Option<u16>,
|
||||
token: Vec<String>,
|
||||
certificate: Option<String>,
|
||||
private_key: Option<String>,
|
||||
|
||||
#[serde(default = "default::ip")]
|
||||
ip: IpAddr,
|
||||
|
||||
#[serde(
|
||||
default = "default::congestion_controller",
|
||||
deserialize_with = "deserialize_from_str"
|
||||
)]
|
||||
congestion_controller: CongestionController,
|
||||
|
||||
#[serde(default = "default::max_idle_time")]
|
||||
max_idle_time: u32,
|
||||
|
||||
#[serde(default = "default::authentication_timeout")]
|
||||
authentication_timeout: u64,
|
||||
|
||||
#[serde(default = "default::alpn")]
|
||||
alpn: Vec<String>,
|
||||
|
||||
#[serde(default = "default::max_udp_relay_packet_size")]
|
||||
max_udp_relay_packet_size: usize,
|
||||
|
||||
#[serde(default = "default::log_level")]
|
||||
log_level: LevelFilter,
|
||||
}
|
||||
|
||||
impl Default for RawConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
port: None,
|
||||
token: Vec::new(),
|
||||
certificate: None,
|
||||
private_key: None,
|
||||
ip: default::ip(),
|
||||
congestion_controller: default::congestion_controller(),
|
||||
max_idle_time: default::max_idle_time(),
|
||||
authentication_timeout: default::authentication_timeout(),
|
||||
alpn: default::alpn(),
|
||||
max_udp_relay_packet_size: default::max_udp_relay_packet_size(),
|
||||
log_level: default::log_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RawConfig {
|
||||
fn parse(args: ArgsOs) -> Result<Self, ConfigError> {
|
||||
let mut opts = Options::new();
|
||||
|
||||
opts.optopt(
|
||||
"c",
|
||||
"config",
|
||||
"Read configuration from a file. Note that command line arguments will override the configuration file",
|
||||
"CONFIG_FILE",
|
||||
);
|
||||
|
||||
opts.optopt("", "port", "Set the server listening port", "SERVER_PORT");
|
||||
|
||||
opts.optmulti(
|
||||
"",
|
||||
"token",
|
||||
"Set the token for TUIC authentication. This option can be used multiple times to set multiple tokens.",
|
||||
"TOKEN",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"certificate",
|
||||
"Set the X.509 certificate. This must be an end-entity certificate",
|
||||
"CERTIFICATE",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"private-key",
|
||||
"Set the certificate private key",
|
||||
"PRIVATE_KEY",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"ip",
|
||||
"Set the server listening IP. Default: 0.0.0.0",
|
||||
"IP",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"congestion-controller",
|
||||
r#"Set the congestion control algorithm. Available: "cubic", "new_reno", "bbr". Default: "cubic""#,
|
||||
"CONGESTION_CONTROLLER",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"max-idle-time",
|
||||
"Set the maximum idle time for QUIC connections, in milliseconds. Default: 15000",
|
||||
"MAX_IDLE_TIME",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"authentication-timeout",
|
||||
"Set the maximum time allowed between a QUIC connection established and the TUIC authentication packet received, in milliseconds. Default: 1000",
|
||||
"AUTHENTICATION_TIMEOUT",
|
||||
);
|
||||
|
||||
opts.optmulti(
|
||||
"",
|
||||
"alpn",
|
||||
"Set ALPN protocols that the server accepts. This option can be used multiple times to set multiple ALPN protocols. If not set, the server will not check ALPN at all",
|
||||
"ALPN_PROTOCOL",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"max-udp-relay-packet-size",
|
||||
"UDP relay mode QUIC can transmit UDP packets larger than the MTU. Set this to a higher value allows outbound to receive larger UDP packet. Default: 1500",
|
||||
"MAX_UDP_RELAY_PACKET_SIZE",
|
||||
);
|
||||
|
||||
opts.optopt(
|
||||
"",
|
||||
"log-level",
|
||||
r#"Set the log level. Available: "off", "error", "warn", "info", "debug", "trace". Default: "info""#,
|
||||
"LOG_LEVEL",
|
||||
);
|
||||
|
||||
opts.optflag("v", "version", "Print the version");
|
||||
opts.optflag("h", "help", "Print this help menu");
|
||||
|
||||
let matches = opts.parse(args.skip(1))?;
|
||||
|
||||
if matches.opt_present("help") {
|
||||
return Err(ConfigError::Help(opts.usage(env!("CARGO_PKG_NAME"))));
|
||||
}
|
||||
|
||||
if matches.opt_present("version") {
|
||||
return Err(ConfigError::Version(env!("CARGO_PKG_VERSION")));
|
||||
}
|
||||
|
||||
if !matches.free.is_empty() {
|
||||
return Err(ConfigError::UnexpectedArguments(matches.free.join(", ")));
|
||||
}
|
||||
|
||||
let port = matches.opt_str("port").map(|port| port.parse());
|
||||
let token = matches.opt_strs("token");
|
||||
let certificate = matches.opt_str("certificate");
|
||||
let private_key = matches.opt_str("private-key");
|
||||
|
||||
let mut raw = if let Some(path) = matches.opt_str("config") {
|
||||
let mut raw = RawConfig::from_file(path)?;
|
||||
|
||||
raw.port = Some(
|
||||
port.transpose()?
|
||||
.or(raw.port)
|
||||
.ok_or(ConfigError::MissingOption("port"))?,
|
||||
);
|
||||
|
||||
if !token.is_empty() {
|
||||
raw.token = token;
|
||||
} else if raw.token.is_empty() {
|
||||
return Err(ConfigError::MissingOption("token"));
|
||||
}
|
||||
|
||||
raw.certificate = Some(
|
||||
certificate
|
||||
.or(raw.certificate)
|
||||
.ok_or(ConfigError::MissingOption("certificate"))?,
|
||||
);
|
||||
|
||||
raw.private_key = Some(
|
||||
private_key
|
||||
.or(raw.private_key)
|
||||
.ok_or(ConfigError::MissingOption("private key"))?,
|
||||
);
|
||||
|
||||
raw
|
||||
} else {
|
||||
RawConfig {
|
||||
port: Some(port.ok_or(ConfigError::MissingOption("port"))??),
|
||||
token: (!token.is_empty())
|
||||
.then(|| token)
|
||||
.ok_or(ConfigError::MissingOption("token"))?,
|
||||
certificate: Some(certificate.ok_or(ConfigError::MissingOption("certificate"))?),
|
||||
private_key: Some(private_key.ok_or(ConfigError::MissingOption("private key"))?),
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ip) = matches.opt_str("ip") {
|
||||
raw.ip = ip.parse()?;
|
||||
};
|
||||
|
||||
if let Some(cgstn_ctrl) = matches.opt_str("congestion-controller") {
|
||||
raw.congestion_controller = cgstn_ctrl.parse()?;
|
||||
};
|
||||
|
||||
if let Some(timeout) = matches.opt_str("max-idle-time") {
|
||||
raw.max_idle_time = timeout.parse()?;
|
||||
};
|
||||
|
||||
if let Some(timeout) = matches.opt_str("authentication-timeout") {
|
||||
raw.authentication_timeout = timeout.parse()?;
|
||||
};
|
||||
|
||||
if let Some(size) = matches.opt_str("max-udp-relay-packet-size") {
|
||||
raw.max_udp_relay_packet_size = size.parse()?;
|
||||
};
|
||||
|
||||
let alpn = matches.opt_strs("alpn");
|
||||
|
||||
if !alpn.is_empty() {
|
||||
raw.alpn = alpn;
|
||||
}
|
||||
|
||||
if let Some(log_level) = matches.opt_str("log-level") {
|
||||
raw.log_level = log_level.parse()?;
|
||||
};
|
||||
|
||||
Ok(raw)
|
||||
}
|
||||
|
||||
fn from_file(path: String) -> Result<Self, ConfigError> {
|
||||
let file = File::open(&path).map_err(|err| ConfigError::Io(path, err))?;
|
||||
let raw = serde_json::from_reader(file)?;
|
||||
Ok(raw)
|
||||
}
|
||||
}
|
||||
|
||||
enum CongestionController {
|
||||
Cubic,
|
||||
NewReno,
|
||||
Bbr,
|
||||
}
|
||||
|
||||
impl FromStr for CongestionController {
|
||||
type Err = ConfigError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.eq_ignore_ascii_case("cubic") {
|
||||
Ok(CongestionController::Cubic)
|
||||
} else if s.eq_ignore_ascii_case("new_reno") || s.eq_ignore_ascii_case("newreno") {
|
||||
Ok(CongestionController::NewReno)
|
||||
} else if s.eq_ignore_ascii_case("bbr") {
|
||||
Ok(CongestionController::Bbr)
|
||||
} else {
|
||||
Err(ConfigError::InvalidCongestionController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_from_str<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Display,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
T::from_str(&s).map_err(DeError::custom)
|
||||
}
|
||||
|
||||
mod default {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn ip() -> IpAddr {
|
||||
IpAddr::V4(Ipv4Addr::UNSPECIFIED)
|
||||
}
|
||||
|
||||
pub(super) const fn congestion_controller() -> CongestionController {
|
||||
CongestionController::Cubic
|
||||
}
|
||||
|
||||
pub(super) const fn max_idle_time() -> u32 {
|
||||
15000
|
||||
}
|
||||
|
||||
pub(super) const fn authentication_timeout() -> u64 {
|
||||
1000
|
||||
}
|
||||
|
||||
pub(super) const fn alpn() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub(super) const fn max_udp_relay_packet_size() -> usize {
|
||||
1500
|
||||
}
|
||||
|
||||
pub(super) const fn log_level() -> LevelFilter {
|
||||
LevelFilter::Info
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
#[error("{0}")]
|
||||
Help(String),
|
||||
#[error("{0}")]
|
||||
Version(&'static str),
|
||||
#[error("Failed to read '{0}': {1}")]
|
||||
Io(String, #[source] IoError),
|
||||
#[error("Failed to parse the config file: {0}")]
|
||||
ParseConfigJson(#[from] JsonError),
|
||||
#[error(transparent)]
|
||||
ParseArgument(#[from] Fail),
|
||||
#[error("Unexpected arguments: {0}")]
|
||||
UnexpectedArguments(String),
|
||||
#[error("Missing option: {0}")]
|
||||
MissingOption(&'static str),
|
||||
#[error(transparent)]
|
||||
ParseInt(#[from] ParseIntError),
|
||||
#[error(transparent)]
|
||||
ParseAddr(#[from] AddrParseError),
|
||||
#[error("Invalid congestion controller")]
|
||||
InvalidCongestionController,
|
||||
#[error(transparent)]
|
||||
ParseLogLevel(#[from] ParseLevelError),
|
||||
#[error("Failed to load certificate / private key: {0}")]
|
||||
Rustls(#[from] RustlsError),
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
use super::IsClosed;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
task::{Context, Poll, Waker},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IsAuthenticated {
|
||||
is_connection_closed: IsClosed,
|
||||
is_authenticated: Arc<AtomicBool>,
|
||||
broadcast: Arc<Mutex<Vec<Waker>>>,
|
||||
}
|
||||
|
||||
impl IsAuthenticated {
|
||||
pub fn new(is_closed: IsClosed) -> Self {
|
||||
Self {
|
||||
is_connection_closed: is_closed,
|
||||
is_authenticated: Arc::new(AtomicBool::new(false)),
|
||||
broadcast: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_authenticated(&self) {
|
||||
self.is_authenticated.store(true, Ordering::Release);
|
||||
}
|
||||
|
||||
pub fn wake(&self) {
|
||||
for waker in self.broadcast.lock().drain(..) {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for IsAuthenticated {
|
||||
type Output = bool;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if self.is_connection_closed.check() {
|
||||
Poll::Ready(false)
|
||||
} else if self.is_authenticated.load(Ordering::Relaxed) {
|
||||
Poll::Ready(true)
|
||||
} else {
|
||||
self.broadcast.lock().push(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
@ -1,226 +0,0 @@
|
||||
use super::{task, Connection, UdpPacketSource};
|
||||
use bytes::Bytes;
|
||||
use quinn::{RecvStream, SendStream, VarInt};
|
||||
use std::io::Error as IoError;
|
||||
use thiserror::Error;
|
||||
use tuic_protocol::{Address, Command};
|
||||
|
||||
impl Connection {
|
||||
pub async fn process_uni_stream(&self, mut stream: RecvStream) -> Result<(), DispatchError> {
|
||||
let rmt_addr = self.controller.remote_address();
|
||||
let cmd = Command::read_from(&mut stream).await?;
|
||||
|
||||
if let Command::Authenticate { digest } = cmd {
|
||||
if self.token.contains(&digest) {
|
||||
log::debug!("[{rmt_addr}] [authentication]");
|
||||
|
||||
self.is_authenticated.set_authenticated();
|
||||
self.is_authenticated.wake();
|
||||
return Ok(());
|
||||
} else {
|
||||
let err = DispatchError::AuthenticationFailed;
|
||||
self.controller
|
||||
.close(err.as_error_code(), err.to_string().as_bytes());
|
||||
self.is_authenticated.wake();
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_authenticated.clone().await {
|
||||
match cmd {
|
||||
Command::Authenticate { .. } => unreachable!(),
|
||||
Command::Packet {
|
||||
assoc_id,
|
||||
len,
|
||||
addr,
|
||||
} => {
|
||||
if self.udp_packet_from.uni_stream() {
|
||||
let dst_addr = addr.to_string();
|
||||
log::debug!("[{rmt_addr}] [packet-from-quic] [{assoc_id}] [{dst_addr}]");
|
||||
|
||||
let res = task::packet_from_uni_stream(
|
||||
stream,
|
||||
self.udp_sessions.clone(),
|
||||
assoc_id,
|
||||
len,
|
||||
addr,
|
||||
rmt_addr,
|
||||
)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => log::warn!(
|
||||
"[{rmt_addr}] [packet-from-quic] [{assoc_id}] [{dst_addr}] {err}"
|
||||
),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(DispatchError::BadCommand)
|
||||
}
|
||||
}
|
||||
Command::Dissociate { assoc_id } => {
|
||||
let res = task::dissociate(self.udp_sessions.clone(), assoc_id, rmt_addr).await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => log::warn!("[{rmt_addr}] [dissociate] {err}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Command::Heartbeat => {
|
||||
log::debug!("[{rmt_addr}] [heartbeat]");
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(DispatchError::BadCommand),
|
||||
}
|
||||
} else {
|
||||
Err(DispatchError::AuthenticationTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process_bi_stream(
|
||||
&self,
|
||||
send: SendStream,
|
||||
mut recv: RecvStream,
|
||||
) -> Result<(), DispatchError> {
|
||||
let cmd = Command::read_from(&mut recv).await?;
|
||||
let rmt_addr = self.controller.remote_address();
|
||||
|
||||
if self.is_authenticated.clone().await {
|
||||
match cmd {
|
||||
Command::Connect { addr } => {
|
||||
let dst_addr = addr.to_string();
|
||||
log::info!("[{rmt_addr}] [connect] [{dst_addr}]");
|
||||
|
||||
let res = task::connect(send, recv, addr).await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => log::warn!("[{rmt_addr}] [connect] [{dst_addr}] {err}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(DispatchError::BadCommand),
|
||||
}
|
||||
} else {
|
||||
Err(DispatchError::AuthenticationTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process_datagram(&self, datagram: Bytes) -> Result<(), DispatchError> {
|
||||
let cmd = Command::read_from(&mut datagram.as_ref()).await?;
|
||||
let rmt_addr = self.controller.remote_address();
|
||||
let cmd_len = cmd.serialized_len();
|
||||
|
||||
if self.is_authenticated.clone().await {
|
||||
match cmd {
|
||||
Command::Packet { assoc_id, addr, .. } => {
|
||||
if self.udp_packet_from.datagram() {
|
||||
let dst_addr = addr.to_string();
|
||||
log::debug!("[{rmt_addr}] [packet-from-native] [{assoc_id}] [{dst_addr}]");
|
||||
|
||||
let res = task::packet_from_datagram(
|
||||
datagram.slice(cmd_len..),
|
||||
self.udp_sessions.clone(),
|
||||
assoc_id,
|
||||
addr,
|
||||
rmt_addr,
|
||||
)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"[{rmt_addr}] [packet-from-native] [{assoc_id}] [{dst_addr}] {err}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(DispatchError::BadCommand)
|
||||
}
|
||||
}
|
||||
_ => Err(DispatchError::BadCommand),
|
||||
}
|
||||
} else {
|
||||
Err(DispatchError::AuthenticationTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process_received_udp_packet(
|
||||
&self,
|
||||
assoc_id: u32,
|
||||
pkt: Bytes,
|
||||
addr: Address,
|
||||
) -> Result<(), DispatchError> {
|
||||
let rmt_addr = self.controller.remote_address();
|
||||
let dst_addr = addr.to_string();
|
||||
|
||||
match self.udp_packet_from.check().unwrap() {
|
||||
UdpPacketSource::UniStream => {
|
||||
log::debug!("[{rmt_addr}] [packet-to-quic] [{assoc_id}] [{dst_addr}]");
|
||||
|
||||
let res =
|
||||
task::packet_to_uni_stream(self.controller.clone(), assoc_id, pkt, addr).await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
log::warn!("[{rmt_addr}] [packet-to-quic] [{assoc_id}] [{dst_addr}] {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
UdpPacketSource::Datagram => {
|
||||
log::debug!("[{rmt_addr}] [packet-to-native] [{assoc_id}] [{dst_addr}]");
|
||||
|
||||
let res =
|
||||
task::packet_to_datagram(self.controller.clone(), assoc_id, pkt, addr).await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"[{rmt_addr}] [packet-to-native] [{assoc_id}] [{dst_addr}] {err}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DispatchError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
#[error("authentication failed")]
|
||||
AuthenticationFailed,
|
||||
#[error("authentication timeout")]
|
||||
AuthenticationTimeout,
|
||||
#[error("bad command")]
|
||||
BadCommand,
|
||||
}
|
||||
|
||||
impl DispatchError {
|
||||
const CODE_PROTOCOL: VarInt = VarInt::from_u32(0xfffffff0);
|
||||
const CODE_AUTHENTICATION_FAILED: VarInt = VarInt::from_u32(0xfffffff1);
|
||||
const CODE_AUTHENTICATION_TIMEOUT: VarInt = VarInt::from_u32(0xfffffff2);
|
||||
const CODE_BAD_COMMAND: VarInt = VarInt::from_u32(0xfffffff3);
|
||||
|
||||
pub fn as_error_code(&self) -> VarInt {
|
||||
match self {
|
||||
Self::Io(_) => Self::CODE_PROTOCOL,
|
||||
Self::AuthenticationFailed => Self::CODE_AUTHENTICATION_FAILED,
|
||||
Self::AuthenticationTimeout => Self::CODE_AUTHENTICATION_TIMEOUT,
|
||||
Self::BadCommand => Self::CODE_BAD_COMMAND,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,258 +0,0 @@
|
||||
use self::{
|
||||
authenticate::IsAuthenticated,
|
||||
dispatch::DispatchError,
|
||||
udp::{RecvPacketReceiver, UdpPacketFrom, UdpPacketSource, UdpSessionMap},
|
||||
};
|
||||
use futures_util::StreamExt;
|
||||
use parking_lot::Mutex;
|
||||
use quinn::{
|
||||
Connecting, Connection as QuinnConnection, ConnectionError, Datagrams, IncomingBiStreams,
|
||||
IncomingUniStreams, NewConnection,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
task::{Context, Poll, Waker},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::time;
|
||||
|
||||
mod authenticate;
|
||||
mod dispatch;
|
||||
mod task;
|
||||
mod udp;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Connection {
|
||||
controller: QuinnConnection,
|
||||
udp_packet_from: UdpPacketFrom,
|
||||
udp_sessions: Arc<UdpSessionMap>,
|
||||
token: Arc<HashSet<[u8; 32]>>,
|
||||
is_authenticated: IsAuthenticated,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub async fn handle(
|
||||
conn: Connecting,
|
||||
token: Arc<HashSet<[u8; 32]>>,
|
||||
auth_timeout: Duration,
|
||||
max_pkt_size: usize,
|
||||
) {
|
||||
let rmt_addr = conn.remote_address();
|
||||
|
||||
match conn.await {
|
||||
Ok(NewConnection {
|
||||
connection,
|
||||
uni_streams,
|
||||
bi_streams,
|
||||
datagrams,
|
||||
..
|
||||
}) => {
|
||||
log::debug!("[{rmt_addr}] [establish]");
|
||||
|
||||
let (udp_sessions, recv_pkt_rx) = UdpSessionMap::new(max_pkt_size);
|
||||
let is_closed = IsClosed::new();
|
||||
let is_authed = IsAuthenticated::new(is_closed.clone());
|
||||
|
||||
let conn = Self {
|
||||
controller: connection,
|
||||
udp_packet_from: UdpPacketFrom::new(),
|
||||
udp_sessions: Arc::new(udp_sessions),
|
||||
token,
|
||||
is_authenticated: is_authed,
|
||||
};
|
||||
|
||||
let res = tokio::select! {
|
||||
res = Self::listen_uni_streams(conn.clone(), uni_streams) => res,
|
||||
res = Self::listen_bi_streams(conn.clone(), bi_streams) => res,
|
||||
res = Self::listen_datagrams(conn.clone(), datagrams) => res,
|
||||
res = Self::listen_received_udp_packet(conn.clone(), recv_pkt_rx) => res,
|
||||
Err(err) = Self::handle_authentication_timeout(conn, auth_timeout) => Err(err),
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(()) => unreachable!(),
|
||||
Err(err) => {
|
||||
is_closed.set_closed();
|
||||
|
||||
match err {
|
||||
ConnectionError::TimedOut => {
|
||||
log::debug!("[{rmt_addr}] [disconnect] [connection timeout]")
|
||||
}
|
||||
ConnectionError::LocallyClosed => {
|
||||
log::debug!("[{rmt_addr}] [disconnect] [locally closed]")
|
||||
}
|
||||
err => log::error!("[{rmt_addr}] [disconnect] {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => log::error!("[{rmt_addr}] {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_uni_streams(
|
||||
self,
|
||||
mut uni_streams: IncomingUniStreams,
|
||||
) -> Result<(), ConnectionError> {
|
||||
while let Some(stream) = uni_streams.next().await {
|
||||
let stream = stream?;
|
||||
let conn = self.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match conn.process_uni_stream(stream).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
conn.controller
|
||||
.close(err.as_error_code(), err.to_string().as_bytes());
|
||||
|
||||
let rmt_addr = conn.controller.remote_address();
|
||||
log::error!("[{rmt_addr}] {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Err(ConnectionError::LocallyClosed)
|
||||
}
|
||||
|
||||
async fn listen_bi_streams(
|
||||
self,
|
||||
mut bi_streams: IncomingBiStreams,
|
||||
) -> Result<(), ConnectionError> {
|
||||
while let Some(stream) = bi_streams.next().await {
|
||||
let (send, recv) = stream?;
|
||||
let conn = self.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match conn.process_bi_stream(send, recv).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
conn.controller
|
||||
.close(err.as_error_code(), err.to_string().as_bytes());
|
||||
|
||||
let rmt_addr = conn.controller.remote_address();
|
||||
log::error!("[{rmt_addr}] {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Err(ConnectionError::LocallyClosed)
|
||||
}
|
||||
|
||||
async fn listen_datagrams(self, mut datagrams: Datagrams) -> Result<(), ConnectionError> {
|
||||
while let Some(datagram) = datagrams.next().await {
|
||||
let datagram = datagram?;
|
||||
let conn = self.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match conn.process_datagram(datagram).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
conn.controller
|
||||
.close(err.as_error_code(), err.to_string().as_bytes());
|
||||
|
||||
let rmt_addr = conn.controller.remote_address();
|
||||
log::error!("[{rmt_addr}] {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Err(ConnectionError::LocallyClosed)
|
||||
}
|
||||
|
||||
async fn listen_received_udp_packet(
|
||||
self,
|
||||
mut recv_pkt_rx: RecvPacketReceiver,
|
||||
) -> Result<(), ConnectionError> {
|
||||
while let Some((assoc_id, pkt, addr)) = recv_pkt_rx.recv().await {
|
||||
let conn = self.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match conn.process_received_udp_packet(assoc_id, pkt, addr).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
conn.controller
|
||||
.close(err.as_error_code(), err.to_string().as_bytes());
|
||||
|
||||
let rmt_addr = conn.controller.remote_address();
|
||||
log::error!("[{rmt_addr}] {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Err(ConnectionError::LocallyClosed)
|
||||
}
|
||||
|
||||
async fn handle_authentication_timeout(self, timeout: Duration) -> Result<(), ConnectionError> {
|
||||
let is_timeout = tokio::select! {
|
||||
_ = self.is_authenticated.clone() => false,
|
||||
() = time::sleep(timeout) => true,
|
||||
};
|
||||
|
||||
if !is_timeout {
|
||||
Ok(())
|
||||
} else {
|
||||
let err = DispatchError::AuthenticationTimeout;
|
||||
|
||||
self.controller
|
||||
.close(err.as_error_code(), err.to_string().as_bytes());
|
||||
self.is_authenticated.wake();
|
||||
|
||||
let rmt_addr = self.controller.remote_address();
|
||||
log::error!("[{rmt_addr}] {err}");
|
||||
|
||||
Err(ConnectionError::LocallyClosed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IsClosed(Arc<IsClosedInner>);
|
||||
|
||||
struct IsClosedInner {
|
||||
is_closed: AtomicBool,
|
||||
waker: Mutex<Option<Waker>>,
|
||||
}
|
||||
|
||||
impl IsClosed {
|
||||
fn new() -> Self {
|
||||
Self(Arc::new(IsClosedInner {
|
||||
is_closed: AtomicBool::new(false),
|
||||
waker: Mutex::new(None),
|
||||
}))
|
||||
}
|
||||
|
||||
fn set_closed(&self) {
|
||||
self.0.is_closed.store(true, Ordering::Release);
|
||||
|
||||
if let Some(waker) = self.0.waker.lock().take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
fn check(&self) -> bool {
|
||||
self.0.is_closed.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for IsClosed {
|
||||
type Output = ();
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if self.0.is_closed.load(Ordering::Acquire) {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
*self.0.waker.lock() = Some(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
@ -1,186 +0,0 @@
|
||||
use super::udp::UdpSessionMap;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use quinn::{
|
||||
Connection as QuinnConnection, ConnectionError, ReadExactError, RecvStream, SendDatagramError,
|
||||
SendStream, WriteError,
|
||||
};
|
||||
use std::{
|
||||
io::{Error as IoError, IoSlice},
|
||||
net::SocketAddr,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio::{
|
||||
io::{self, AsyncRead, AsyncWrite, ReadBuf},
|
||||
net::{self, TcpStream},
|
||||
};
|
||||
use tuic_protocol::{Address, Command};
|
||||
|
||||
pub async fn connect(
|
||||
mut send: SendStream,
|
||||
recv: RecvStream,
|
||||
addr: Address,
|
||||
) -> Result<(), TaskError> {
|
||||
let mut target = None;
|
||||
|
||||
let addrs = match addr {
|
||||
Address::SocketAddress(addr) => Ok(vec![addr]),
|
||||
Address::DomainAddress(domain, port) => net::lookup_host((domain.as_str(), port))
|
||||
.await
|
||||
.map(|res| res.collect()),
|
||||
}?;
|
||||
|
||||
for addr in addrs {
|
||||
if let Ok(target_stream) = TcpStream::connect(addr).await {
|
||||
target = Some(target_stream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut target) = target {
|
||||
let resp = Command::new_response(true);
|
||||
resp.write_to(&mut send).await?;
|
||||
let mut tunnel = BiStream(send, recv);
|
||||
io::copy_bidirectional(&mut target, &mut tunnel).await?;
|
||||
} else {
|
||||
let resp = Command::new_response(false);
|
||||
resp.write_to(&mut send).await?;
|
||||
send.finish().await?;
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn packet_from_uni_stream(
|
||||
mut stream: RecvStream,
|
||||
udp_sessions: Arc<UdpSessionMap>,
|
||||
assoc_id: u32,
|
||||
len: u16,
|
||||
addr: Address,
|
||||
src_addr: SocketAddr,
|
||||
) -> Result<(), TaskError> {
|
||||
let mut buf = vec![0; len as usize];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
|
||||
let pkt = Bytes::from(buf);
|
||||
udp_sessions.send(assoc_id, pkt, addr, src_addr).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn packet_from_datagram(
|
||||
pkt: Bytes,
|
||||
udp_sessions: Arc<UdpSessionMap>,
|
||||
assoc_id: u32,
|
||||
addr: Address,
|
||||
src_addr: SocketAddr,
|
||||
) -> Result<(), TaskError> {
|
||||
udp_sessions.send(assoc_id, pkt, addr, src_addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn packet_to_uni_stream(
|
||||
conn: QuinnConnection,
|
||||
assoc_id: u32,
|
||||
pkt: Bytes,
|
||||
addr: Address,
|
||||
) -> Result<(), TaskError> {
|
||||
let mut stream = conn.open_uni().await?;
|
||||
|
||||
let cmd = Command::new_packet(assoc_id, pkt.len() as u16, addr);
|
||||
cmd.write_to(&mut stream).await?;
|
||||
stream.write_all(&pkt).await?;
|
||||
stream.finish().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn packet_to_datagram(
|
||||
conn: QuinnConnection,
|
||||
assoc_id: u32,
|
||||
pkt: Bytes,
|
||||
addr: Address,
|
||||
) -> Result<(), TaskError> {
|
||||
let cmd = Command::new_packet(assoc_id, pkt.len() as u16, addr);
|
||||
|
||||
let mut buf = BytesMut::with_capacity(cmd.serialized_len());
|
||||
cmd.write_to_buf(&mut buf);
|
||||
buf.extend_from_slice(&pkt);
|
||||
|
||||
let pkt = buf.freeze();
|
||||
conn.send_datagram(pkt)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn dissociate(
|
||||
udp_sessions: Arc<UdpSessionMap>,
|
||||
assoc_id: u32,
|
||||
src_addr: SocketAddr,
|
||||
) -> Result<(), TaskError> {
|
||||
udp_sessions.dissociate(assoc_id, src_addr);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct BiStream(SendStream, RecvStream);
|
||||
|
||||
impl AsyncRead for BiStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<Result<(), IoError>> {
|
||||
Pin::new(&mut self.1).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for BiStream {
|
||||
#[inline]
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, IoError>> {
|
||||
Pin::new(&mut self.0).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_write_vectored(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
bufs: &[IoSlice<'_>],
|
||||
) -> Poll<Result<usize, IoError>> {
|
||||
Pin::new(&mut self.0).poll_write_vectored(cx, bufs)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_write_vectored(&self) -> bool {
|
||||
self.0.is_write_vectored()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), IoError>> {
|
||||
Pin::new(&mut self.0).poll_flush(cx)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), IoError>> {
|
||||
Pin::new(&mut self.0).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TaskError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
#[error(transparent)]
|
||||
Connection(#[from] ConnectionError),
|
||||
#[error(transparent)]
|
||||
ReadStream(#[from] ReadExactError),
|
||||
#[error(transparent)]
|
||||
WriteStream(#[from] WriteError),
|
||||
#[error(transparent)]
|
||||
SendDatagram(#[from] SendDatagramError),
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
use bytes::Bytes;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::Result,
|
||||
net::{Ipv6Addr, SocketAddr},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
net::UdpSocket,
|
||||
sync::mpsc::{self, Receiver, Sender},
|
||||
};
|
||||
use tuic_protocol::Address;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UdpPacketFrom(Arc<AtomicCell<Option<UdpPacketSource>>>);
|
||||
|
||||
impl UdpPacketFrom {
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(AtomicCell::new(None)))
|
||||
}
|
||||
|
||||
pub fn check(&self) -> Option<UdpPacketSource> {
|
||||
self.0.load()
|
||||
}
|
||||
|
||||
pub fn uni_stream(&self) -> bool {
|
||||
self.0
|
||||
.compare_exchange(None, Some(UdpPacketSource::UniStream))
|
||||
.map_or_else(|from| from == Some(UdpPacketSource::UniStream), |_| true)
|
||||
}
|
||||
|
||||
pub fn datagram(&self) -> bool {
|
||||
self.0
|
||||
.compare_exchange(None, Some(UdpPacketSource::Datagram))
|
||||
.map_or_else(|from| from == Some(UdpPacketSource::Datagram), |_| true)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub enum UdpPacketSource {
|
||||
UniStream,
|
||||
Datagram,
|
||||
}
|
||||
|
||||
pub type SendPacketSender = Sender<(Bytes, Address)>;
|
||||
pub type SendPacketReceiver = Receiver<(Bytes, Address)>;
|
||||
pub type RecvPacketSender = Sender<(u32, Bytes, Address)>;
|
||||
pub type RecvPacketReceiver = Receiver<(u32, Bytes, Address)>;
|
||||
|
||||
pub struct UdpSessionMap {
|
||||
map: Mutex<HashMap<u32, UdpSession>>,
|
||||
recv_pkt_tx_for_clone: RecvPacketSender,
|
||||
max_pkt_size: usize,
|
||||
}
|
||||
|
||||
impl UdpSessionMap {
|
||||
pub fn new(max_pkt_size: usize) -> (Self, RecvPacketReceiver) {
|
||||
let (recv_pkt_tx, recv_pkt_rx) = mpsc::channel(1);
|
||||
|
||||
(
|
||||
Self {
|
||||
map: Mutex::new(HashMap::new()),
|
||||
recv_pkt_tx_for_clone: recv_pkt_tx,
|
||||
max_pkt_size,
|
||||
},
|
||||
recv_pkt_rx,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::await_holding_lock)]
|
||||
pub async fn send(
|
||||
&self,
|
||||
assoc_id: u32,
|
||||
pkt: Bytes,
|
||||
addr: Address,
|
||||
src_addr: SocketAddr,
|
||||
) -> Result<()> {
|
||||
let map = self.map.lock();
|
||||
|
||||
let send_pkt_tx = if let Some(session) = map.get(&assoc_id) {
|
||||
let send_pkt_tx = session.0.clone();
|
||||
drop(map);
|
||||
send_pkt_tx
|
||||
} else {
|
||||
log::info!("[{src_addr}] [associate] [{assoc_id}]");
|
||||
drop(map);
|
||||
|
||||
let assoc = UdpSession::new(
|
||||
assoc_id,
|
||||
self.recv_pkt_tx_for_clone.clone(),
|
||||
src_addr,
|
||||
self.max_pkt_size,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let send_pkt_tx = assoc.0.clone();
|
||||
|
||||
let mut map = self.map.lock();
|
||||
map.insert(assoc_id, assoc);
|
||||
|
||||
send_pkt_tx
|
||||
};
|
||||
|
||||
let _ = send_pkt_tx.send((pkt, addr)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn dissociate(&self, assoc_id: u32, src_addr: SocketAddr) {
|
||||
log::info!("[{src_addr}] [dissociate] [{assoc_id}]");
|
||||
self.map.lock().remove(&assoc_id);
|
||||
}
|
||||
}
|
||||
|
||||
struct UdpSession(SendPacketSender);
|
||||
|
||||
impl UdpSession {
|
||||
async fn new(
|
||||
assoc_id: u32,
|
||||
recv_pkt_tx: RecvPacketSender,
|
||||
src_addr: SocketAddr,
|
||||
max_pkt_size: usize,
|
||||
) -> Result<Self> {
|
||||
let socket = Arc::new(UdpSocket::bind(SocketAddr::from((Ipv6Addr::UNSPECIFIED, 0))).await?);
|
||||
let (send_pkt_tx, send_pkt_rx) = mpsc::channel(1);
|
||||
|
||||
tokio::spawn(async move {
|
||||
match tokio::select! {
|
||||
res = Self::listen_send_packet(socket.clone(), send_pkt_rx) => res,
|
||||
res = Self::listen_receive_packet(socket, assoc_id, recv_pkt_tx, max_pkt_size) => res,
|
||||
} {
|
||||
Ok(()) => (),
|
||||
Err(err) => log::warn!("[{src_addr}] [udp-session] [{assoc_id}] {err}"),
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self(send_pkt_tx))
|
||||
}
|
||||
|
||||
async fn listen_send_packet(
|
||||
socket: Arc<UdpSocket>,
|
||||
mut send_pkt_rx: SendPacketReceiver,
|
||||
) -> Result<()> {
|
||||
while let Some((pkt, addr)) = send_pkt_rx.recv().await {
|
||||
match addr {
|
||||
Address::DomainAddress(hostname, port) => {
|
||||
socket.send_to(&pkt, (hostname, port)).await?;
|
||||
}
|
||||
Address::SocketAddress(addr) => {
|
||||
socket.send_to(&pkt, addr).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn listen_receive_packet(
|
||||
socket: Arc<UdpSocket>,
|
||||
assoc_id: u32,
|
||||
recv_pkt_tx: RecvPacketSender,
|
||||
max_pkt_size: usize,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
let mut buf = vec![0; max_pkt_size];
|
||||
let (len, addr) = socket.recv_from(&mut buf).await?;
|
||||
buf.truncate(len);
|
||||
|
||||
let pkt = Bytes::from(buf);
|
||||
let _ = recv_pkt_tx
|
||||
.send((assoc_id, pkt, Address::SocketAddress(addr)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
use crate::{
|
||||
config::{Config, ConfigError},
|
||||
server::Server,
|
||||
};
|
||||
use std::{env, process};
|
||||
|
||||
mod certificate;
|
||||
mod config;
|
||||
mod connection;
|
||||
mod server;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = env::args_os();
|
||||
|
||||
let config = match Config::parse(args) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
match err {
|
||||
ConfigError::Help(help) => println!("{help}"),
|
||||
ConfigError::Version(version) => println!("{version}"),
|
||||
err => eprintln!("{err}"),
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
env_logger::builder()
|
||||
.filter_level(config.log_level)
|
||||
.format_level(true)
|
||||
.format_target(false)
|
||||
.format_module_path(false)
|
||||
.init();
|
||||
|
||||
let server = match Server::init(
|
||||
config.server_config,
|
||||
config.listen_addr,
|
||||
config.token,
|
||||
config.authentication_timeout,
|
||||
config.max_udp_relay_packet_size,
|
||||
) {
|
||||
Ok(server) => server,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
server.run().await;
|
||||
process::exit(1);
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
use crate::connection::Connection;
|
||||
use futures_util::StreamExt;
|
||||
use quinn::{Endpoint, EndpointConfig, Incoming, ServerConfig};
|
||||
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
io::Result,
|
||||
net::{SocketAddr, UdpSocket},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub struct Server {
|
||||
incoming: Incoming,
|
||||
listen_addr: SocketAddr,
|
||||
token: Arc<HashSet<[u8; 32]>>,
|
||||
authentication_timeout: Duration,
|
||||
max_pkt_size: usize,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn init(
|
||||
config: ServerConfig,
|
||||
listen_addr: SocketAddr,
|
||||
token: HashSet<[u8; 32]>,
|
||||
auth_timeout: Duration,
|
||||
max_pkt_size: usize,
|
||||
) -> Result<Self> {
|
||||
let socket = match listen_addr {
|
||||
SocketAddr::V4(_) => UdpSocket::bind(listen_addr)?,
|
||||
SocketAddr::V6(_) => {
|
||||
let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
|
||||
socket.set_only_v6(false)?;
|
||||
socket.bind(&SockAddr::from(listen_addr))?;
|
||||
UdpSocket::from(socket)
|
||||
}
|
||||
};
|
||||
|
||||
let (_, incoming) = Endpoint::new(EndpointConfig::default(), Some(config), socket)?;
|
||||
|
||||
Ok(Self {
|
||||
incoming,
|
||||
listen_addr,
|
||||
token: Arc::new(token),
|
||||
authentication_timeout: auth_timeout,
|
||||
max_pkt_size,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(mut self) {
|
||||
log::info!("Server started. Listening: {}", self.listen_addr);
|
||||
|
||||
while let Some(conn) = self.incoming.next().await {
|
||||
tokio::spawn(Connection::handle(
|
||||
conn,
|
||||
self.token.clone(),
|
||||
self.authentication_timeout,
|
||||
self.max_pkt_size,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
27
tuic-client/Cargo.toml
Normal file
27
tuic-client/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "tuic-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytes = { version = "1.4.0", default-features = false, features = ["std"] }
|
||||
crossbeam-utils = { version = "0.8.14", default-features = false, features = ["std"] }
|
||||
lexopt = { version = "0.3.0", default-features = false }
|
||||
once_cell = { version = "1.17.0", default-features = false, features = ["parking_lot", "std"] }
|
||||
parking_lot = { version = "0.12.1", default-features = false, features = ["send_guard"] }
|
||||
quinn = { version = "0.9.3", default-features = false, features = ["futures-io", "runtime-tokio", "tls-rustls"] }
|
||||
register-count = { version = "0.1.0", default-features = false, features = ["std"] }
|
||||
rustls = { version = "0.20.8", default-features = false, features = ["quic"] }
|
||||
rustls-native-certs = { version = "0.6.2", default-features = false }
|
||||
rustls-pemfile = { version = "1.0.2", default-features = false }
|
||||
serde = { version = "1.0.152", default-features = false, features = ["derive", "std"] }
|
||||
serde_json = { version = "1.0.91", default-features = false, features = ["std"] }
|
||||
socket2 = { version = "0.4.7", default-features = false }
|
||||
socks5-proto = { version = "0.3.3", default-features = false }
|
||||
socks5-server = { version = "0.8.3", default-features = false }
|
||||
thiserror = { version = "1.0.38", default-features = false }
|
||||
tokio = { version = "1.25.0", default-features = false, features = ["macros", "net", "parking_lot", "rt-multi-thread", "time"] }
|
||||
tokio-util = { version = "0.7.4", default-features = false, features = ["compat"] }
|
||||
tuic = { path = "../tuic", default-features = false }
|
||||
tuic-quinn = { path = "../tuic-quinn", default-features = false }
|
||||
webpki = { version = "0.22.0", default-features = false }
|
209
tuic-client/src/config.rs
Normal file
209
tuic-client/src/config.rs
Normal file
@ -0,0 +1,209 @@
|
||||
use crate::utils::{CongestionControl, UdpRelayMode};
|
||||
use lexopt::{Arg, Error as ArgumentError, Parser};
|
||||
use serde::{de::Error as DeError, Deserialize, Deserializer};
|
||||
use serde_json::Error as SerdeError;
|
||||
use std::{
|
||||
env::ArgsOs,
|
||||
fmt::Display,
|
||||
fs::File,
|
||||
io::Error as IoError,
|
||||
net::{IpAddr, SocketAddr},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
const HELP_MSG: &str = r#"
|
||||
Usage tuic-client [arguments]
|
||||
|
||||
Arguments:
|
||||
-c, --config <path> Path to the config file (required)
|
||||
-v, --version Print the version
|
||||
-h, --help Print this help message
|
||||
"#;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
pub relay: Relay,
|
||||
pub local: Local,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Relay {
|
||||
#[serde(deserialize_with = "deserialize_server")]
|
||||
pub server: (String, u16),
|
||||
pub token: String,
|
||||
pub ip: Option<IpAddr>,
|
||||
#[serde(default = "default::relay::certificates")]
|
||||
pub certificates: Vec<PathBuf>,
|
||||
#[serde(
|
||||
default = "default::relay::udp_relay_mode",
|
||||
deserialize_with = "deserialize_from_str"
|
||||
)]
|
||||
pub udp_relay_mode: UdpRelayMode,
|
||||
#[serde(
|
||||
default = "default::relay::congestion_control",
|
||||
deserialize_with = "deserialize_from_str"
|
||||
)]
|
||||
pub congestion_control: CongestionControl,
|
||||
#[serde(default = "default::relay::alpn")]
|
||||
pub alpn: Vec<String>,
|
||||
#[serde(default = "default::relay::zero_rtt_handshake")]
|
||||
pub zero_rtt_handshake: bool,
|
||||
#[serde(default = "default::relay::disable_sni")]
|
||||
pub disable_sni: bool,
|
||||
#[serde(default = "default::relay::timeout")]
|
||||
pub timeout: Duration,
|
||||
#[serde(default = "default::relay::heartbeat")]
|
||||
pub heartbeat: Duration,
|
||||
#[serde(default = "default::relay::disable_native_certs")]
|
||||
pub disable_native_certs: bool,
|
||||
#[serde(default = "default::relay::gc_interval")]
|
||||
pub gc_interval: Duration,
|
||||
#[serde(default = "default::relay::gc_lifetime")]
|
||||
pub gc_lifetime: Duration,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Local {
|
||||
pub server: SocketAddr,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub dual_stack: Option<bool>,
|
||||
#[serde(default = "default::local::max_packet_size")]
|
||||
pub max_packet_size: usize,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn parse(args: ArgsOs) -> Result<Self, ConfigError> {
|
||||
let mut parser = Parser::from_iter(args);
|
||||
let mut path = None;
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Arg::Short('c') | Arg::Long("config") => {
|
||||
if path.is_none() {
|
||||
path = Some(parser.value()?);
|
||||
} else {
|
||||
return Err(ConfigError::Argument(arg.unexpected()));
|
||||
}
|
||||
}
|
||||
Arg::Short('v') | Arg::Long("version") => {
|
||||
return Err(ConfigError::Version(env!("CARGO_PKG_VERSION")))
|
||||
}
|
||||
Arg::Short('h') | Arg::Long("help") => return Err(ConfigError::Help(HELP_MSG)),
|
||||
_ => return Err(ConfigError::Argument(arg.unexpected())),
|
||||
}
|
||||
}
|
||||
|
||||
if path.is_none() {
|
||||
return Err(ConfigError::NoConfig);
|
||||
}
|
||||
|
||||
let file = File::open(path.unwrap())?;
|
||||
Ok(serde_json::from_reader(file)?)
|
||||
}
|
||||
}
|
||||
|
||||
mod default {
|
||||
pub mod relay {
|
||||
use crate::utils::{CongestionControl, UdpRelayMode};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
pub fn certificates() -> Vec<PathBuf> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn udp_relay_mode() -> UdpRelayMode {
|
||||
UdpRelayMode::Native
|
||||
}
|
||||
|
||||
pub fn congestion_control() -> CongestionControl {
|
||||
CongestionControl::Cubic
|
||||
}
|
||||
|
||||
pub fn alpn() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn zero_rtt_handshake() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn disable_sni() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn timeout() -> Duration {
|
||||
Duration::from_secs(8)
|
||||
}
|
||||
|
||||
pub fn heartbeat() -> Duration {
|
||||
Duration::from_secs(3)
|
||||
}
|
||||
|
||||
pub fn disable_native_certs() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn gc_interval() -> Duration {
|
||||
Duration::from_secs(3)
|
||||
}
|
||||
|
||||
pub fn gc_lifetime() -> Duration {
|
||||
Duration::from_secs(15)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod local {
|
||||
pub fn max_packet_size() -> usize {
|
||||
1500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_from_str<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Display,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
T::from_str(&s).map_err(DeError::custom)
|
||||
}
|
||||
|
||||
pub fn deserialize_server<'de, D>(deserializer: D) -> Result<(String, u16), D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
let mut parts = s.split(':');
|
||||
|
||||
match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some(domain), Some(port), None) => port.parse().map_or_else(
|
||||
|e| Err(DeError::custom(e)),
|
||||
|port| Ok((domain.to_owned(), port)),
|
||||
),
|
||||
_ => Err(DeError::custom("invalid server address")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
#[error(transparent)]
|
||||
Argument(#[from] ArgumentError),
|
||||
#[error("no config file specified")]
|
||||
NoConfig,
|
||||
#[error("{0}")]
|
||||
Version(&'static str),
|
||||
#[error("{0}")]
|
||||
Help(&'static str),
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
#[error(transparent)]
|
||||
Serde(#[from] SerdeError),
|
||||
}
|
440
tuic-client/src/connection.rs
Normal file
440
tuic-client/src/connection.rs
Normal file
@ -0,0 +1,440 @@
|
||||
use crate::{
|
||||
config::Relay,
|
||||
socks5::Server as Socks5Server,
|
||||
utils::{self, CongestionControl, ServerAddr, UdpRelayMode},
|
||||
Error,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use quinn::{
|
||||
congestion::{BbrConfig, CubicConfig, NewRenoConfig},
|
||||
ClientConfig, Connection as QuinnConnection, Endpoint as QuinnEndpoint, EndpointConfig,
|
||||
RecvStream, SendStream, TokioRuntime, TransportConfig, VarInt,
|
||||
};
|
||||
use register_count::{Counter, Register};
|
||||
use rustls::{version, ClientConfig as RustlsClientConfig};
|
||||
use socks5_proto::Address as Socks5Address;
|
||||
use std::{
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{
|
||||
sync::{Mutex as AsyncMutex, OnceCell as AsyncOnceCell},
|
||||
time,
|
||||
};
|
||||
use tuic::Address;
|
||||
use tuic_quinn::{side, Connect, Connection as Model, Task};
|
||||
|
||||
static ENDPOINT: OnceCell<Mutex<Endpoint>> = OnceCell::new();
|
||||
static CONNECTION: AsyncOnceCell<AsyncMutex<Connection>> = AsyncOnceCell::const_new();
|
||||
static TIMEOUT: AtomicCell<Duration> = AtomicCell::new(Duration::from_secs(0));
|
||||
|
||||
const DEFAULT_CONCURRENT_STREAMS: usize = 32;
|
||||
|
||||
pub struct Endpoint {
|
||||
ep: QuinnEndpoint,
|
||||
server: ServerAddr,
|
||||
token: Arc<[u8]>,
|
||||
udp_relay_mode: UdpRelayMode,
|
||||
zero_rtt_handshake: bool,
|
||||
heartbeat: Duration,
|
||||
gc_interval: Duration,
|
||||
gc_lifetime: Duration,
|
||||
}
|
||||
|
||||
impl Endpoint {
|
||||
pub fn set_config(cfg: Relay) -> Result<(), Error> {
|
||||
let certs = utils::load_certs(cfg.certificates, cfg.disable_native_certs)?;
|
||||
|
||||
let mut crypto = RustlsClientConfig::builder()
|
||||
.with_safe_default_cipher_suites()
|
||||
.with_safe_default_kx_groups()
|
||||
.with_protocol_versions(&[&version::TLS13])
|
||||
.unwrap()
|
||||
.with_root_certificates(certs)
|
||||
.with_no_client_auth();
|
||||
|
||||
crypto.alpn_protocols = cfg.alpn.into_iter().map(|alpn| alpn.into_bytes()).collect();
|
||||
crypto.enable_early_data = true;
|
||||
crypto.enable_sni = !cfg.disable_sni;
|
||||
|
||||
let mut config = ClientConfig::new(Arc::new(crypto));
|
||||
let mut tp_cfg = TransportConfig::default();
|
||||
|
||||
tp_cfg
|
||||
.max_concurrent_bidi_streams(VarInt::from(DEFAULT_CONCURRENT_STREAMS as u32))
|
||||
.max_concurrent_uni_streams(VarInt::from(DEFAULT_CONCURRENT_STREAMS as u32))
|
||||
.max_idle_timeout(None);
|
||||
|
||||
match cfg.congestion_control {
|
||||
CongestionControl::Cubic => {
|
||||
tp_cfg.congestion_controller_factory(Arc::new(CubicConfig::default()))
|
||||
}
|
||||
CongestionControl::NewReno => {
|
||||
tp_cfg.congestion_controller_factory(Arc::new(NewRenoConfig::default()))
|
||||
}
|
||||
CongestionControl::Bbr => {
|
||||
tp_cfg.congestion_controller_factory(Arc::new(BbrConfig::default()))
|
||||
}
|
||||
};
|
||||
|
||||
config.transport_config(Arc::new(tp_cfg));
|
||||
|
||||
let socket = UdpSocket::bind(SocketAddr::from(([0, 0, 0, 0], 0)))?;
|
||||
let mut ep = QuinnEndpoint::new(EndpointConfig::default(), None, socket, TokioRuntime)?;
|
||||
ep.set_default_client_config(config);
|
||||
|
||||
let ep = Self {
|
||||
ep,
|
||||
server: ServerAddr::new(cfg.server.0, cfg.server.1, cfg.ip),
|
||||
token: Arc::from(cfg.token.into_bytes().into_boxed_slice()),
|
||||
udp_relay_mode: cfg.udp_relay_mode,
|
||||
zero_rtt_handshake: cfg.zero_rtt_handshake,
|
||||
heartbeat: cfg.heartbeat,
|
||||
gc_interval: cfg.gc_interval,
|
||||
gc_lifetime: cfg.gc_lifetime,
|
||||
};
|
||||
|
||||
ENDPOINT
|
||||
.set(Mutex::new(ep))
|
||||
.map_err(|_| "endpoint already initialized")
|
||||
.unwrap();
|
||||
|
||||
TIMEOUT.store(cfg.timeout);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect(&mut self) -> Result<Connection, Error> {
|
||||
async fn connect_to(
|
||||
ep: &mut QuinnEndpoint,
|
||||
addr: SocketAddr,
|
||||
server_name: &str,
|
||||
udp_relay_mode: UdpRelayMode,
|
||||
zero_rtt_handshake: bool,
|
||||
) -> Result<Connection, Error> {
|
||||
let match_ipv4 = addr.is_ipv4() && ep.local_addr().map_or(false, |addr| addr.is_ipv4());
|
||||
let match_ipv6 = addr.is_ipv6() && ep.local_addr().map_or(false, |addr| addr.is_ipv6());
|
||||
|
||||
if !match_ipv4 && !match_ipv6 {
|
||||
let bind_addr = if addr.is_ipv4() {
|
||||
SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))
|
||||
} else {
|
||||
SocketAddr::from((Ipv6Addr::UNSPECIFIED, 0))
|
||||
};
|
||||
|
||||
ep.rebind(UdpSocket::bind(bind_addr)?)?;
|
||||
}
|
||||
|
||||
let conn = ep.connect(addr, server_name)?;
|
||||
|
||||
let conn = if zero_rtt_handshake {
|
||||
match conn.into_0rtt() {
|
||||
Ok((conn, _)) => conn,
|
||||
Err(conn) => {
|
||||
eprintln!("0-RTT handshake failed, fallback to 1-RTT handshake");
|
||||
conn.await?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conn.await?
|
||||
};
|
||||
|
||||
Ok(Connection::new(conn, udp_relay_mode))
|
||||
}
|
||||
|
||||
let mut last_err = None;
|
||||
|
||||
for addr in self.server.resolve().await? {
|
||||
match connect_to(
|
||||
&mut self.ep,
|
||||
addr,
|
||||
self.server.server_name(),
|
||||
self.udp_relay_mode,
|
||||
self.zero_rtt_handshake,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(conn) => {
|
||||
tokio::spawn(conn.clone().init(
|
||||
self.token.clone(),
|
||||
self.heartbeat,
|
||||
self.gc_interval,
|
||||
self.gc_lifetime,
|
||||
));
|
||||
return Ok(conn);
|
||||
}
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_err.unwrap_or(Error::DnsResolve))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Connection {
|
||||
conn: QuinnConnection,
|
||||
model: Model<side::Client>,
|
||||
udp_relay_mode: UdpRelayMode,
|
||||
remote_uni_stream_cnt: Counter,
|
||||
remote_bi_stream_cnt: Counter,
|
||||
max_concurrent_uni_streams: Arc<AtomicUsize>,
|
||||
max_concurrent_bi_streams: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
fn new(conn: QuinnConnection, udp_relay_mode: UdpRelayMode) -> Self {
|
||||
Self {
|
||||
conn: conn.clone(),
|
||||
model: Model::<side::Client>::new(conn),
|
||||
udp_relay_mode,
|
||||
remote_uni_stream_cnt: Counter::new(),
|
||||
remote_bi_stream_cnt: Counter::new(),
|
||||
max_concurrent_uni_streams: Arc::new(AtomicUsize::new(DEFAULT_CONCURRENT_STREAMS)),
|
||||
max_concurrent_bi_streams: Arc::new(AtomicUsize::new(DEFAULT_CONCURRENT_STREAMS)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get() -> Result<Connection, Error> {
|
||||
let try_init_conn = async {
|
||||
ENDPOINT
|
||||
.get()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.connect()
|
||||
.await
|
||||
.map(AsyncMutex::new)
|
||||
};
|
||||
|
||||
let try_get_conn = async {
|
||||
let mut conn = CONNECTION
|
||||
.get_or_try_init(|| try_init_conn)
|
||||
.await?
|
||||
.lock()
|
||||
.await;
|
||||
|
||||
if conn.is_closed() {
|
||||
let new_conn = ENDPOINT.get().unwrap().lock().connect().await?;
|
||||
*conn = new_conn;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(conn.clone())
|
||||
};
|
||||
|
||||
let conn = time::timeout(TIMEOUT.load(), try_get_conn)
|
||||
.await
|
||||
.map_err(|_| Error::Timeout)??;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub async fn connect(&self, addr: Address) -> Result<Connect, Error> {
|
||||
Ok(self.model.connect(addr).await?)
|
||||
}
|
||||
|
||||
pub async fn packet(&self, pkt: Bytes, addr: Address, assoc_id: u16) -> Result<(), Error> {
|
||||
match self.udp_relay_mode {
|
||||
UdpRelayMode::Native => self.model.packet_native(pkt, addr, assoc_id)?,
|
||||
UdpRelayMode::Quic => self.model.packet_quic(pkt, addr, assoc_id).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn dissociate(&self, assoc_id: u16) -> Result<(), Error> {
|
||||
self.model.dissociate(assoc_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_closed(&self) -> bool {
|
||||
self.conn.close_reason().is_some()
|
||||
}
|
||||
|
||||
async fn accept_uni_stream(&self) -> Result<(RecvStream, Register), Error> {
|
||||
let max = self.max_concurrent_uni_streams.load(Ordering::Relaxed);
|
||||
|
||||
if self.remote_uni_stream_cnt.count() == max {
|
||||
self.max_concurrent_uni_streams
|
||||
.store(max * 2, Ordering::Relaxed);
|
||||
|
||||
self.conn
|
||||
.set_max_concurrent_uni_streams(VarInt::from((max * 2) as u32));
|
||||
}
|
||||
|
||||
let recv = self.conn.accept_uni().await?;
|
||||
let reg = self.remote_uni_stream_cnt.reg();
|
||||
Ok((recv, reg))
|
||||
}
|
||||
|
||||
async fn accept_bi_stream(&self) -> Result<(SendStream, RecvStream, Register), Error> {
|
||||
let max = self.max_concurrent_bi_streams.load(Ordering::Relaxed);
|
||||
|
||||
if self.remote_bi_stream_cnt.count() == max {
|
||||
self.max_concurrent_bi_streams
|
||||
.store(max * 2, Ordering::Relaxed);
|
||||
|
||||
self.conn
|
||||
.set_max_concurrent_bi_streams(VarInt::from((max * 2) as u32));
|
||||
}
|
||||
|
||||
let (send, recv) = self.conn.accept_bi().await?;
|
||||
let reg = self.remote_bi_stream_cnt.reg();
|
||||
Ok((send, recv, reg))
|
||||
}
|
||||
|
||||
async fn accept_datagram(&self) -> Result<Bytes, Error> {
|
||||
Ok(self.conn.read_datagram().await?)
|
||||
}
|
||||
|
||||
async fn handle_uni_stream(self, recv: RecvStream, _reg: Register) {
|
||||
let res = match self.model.accept_uni_stream(recv).await {
|
||||
Err(err) => Err(Error::from(err)),
|
||||
Ok(Task::Packet(pkt)) => match self.udp_relay_mode {
|
||||
UdpRelayMode::Quic => match pkt.accept().await {
|
||||
Ok(Some((pkt, addr, assoc_id))) => {
|
||||
let addr = match addr {
|
||||
Address::None => unreachable!(),
|
||||
Address::DomainAddress(domain, port) => {
|
||||
Socks5Address::DomainAddress(domain, port)
|
||||
}
|
||||
Address::SocketAddress(addr) => Socks5Address::SocketAddress(addr),
|
||||
};
|
||||
Socks5Server::recv_pkt(pkt, addr, assoc_id).await
|
||||
}
|
||||
Ok(None) => Ok(()),
|
||||
Err(err) => Err(Error::from(err)),
|
||||
},
|
||||
UdpRelayMode::Native => Err(Error::WrongPacketSource),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_bi_stream(self, send: SendStream, recv: RecvStream, _reg: Register) {
|
||||
let res = match self.model.accept_bi_stream(send, recv).await {
|
||||
Err(err) => Err(Error::from(err)),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_datagram(self, dg: Bytes) {
|
||||
let res = match self.model.accept_datagram(dg) {
|
||||
Err(err) => Err(Error::from(err)),
|
||||
Ok(Task::Packet(pkt)) => match self.udp_relay_mode {
|
||||
UdpRelayMode::Native => match pkt.accept().await {
|
||||
Ok(Some((pkt, addr, assoc_id))) => {
|
||||
let addr = match addr {
|
||||
Address::None => unreachable!(),
|
||||
Address::DomainAddress(domain, port) => {
|
||||
Socks5Address::DomainAddress(domain, port)
|
||||
}
|
||||
Address::SocketAddress(addr) => Socks5Address::SocketAddress(addr),
|
||||
};
|
||||
Socks5Server::recv_pkt(pkt, addr, assoc_id).await
|
||||
}
|
||||
Ok(None) => Ok(()),
|
||||
Err(err) => Err(Error::from(err)),
|
||||
},
|
||||
UdpRelayMode::Quic => Err(Error::WrongPacketSource),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn authenticate(self, token: Arc<[u8]>) {
|
||||
let mut buf = [0; 32];
|
||||
|
||||
match self.conn.export_keying_material(&mut buf, &token, &token) {
|
||||
Ok(()) => {}
|
||||
Err(_) => {
|
||||
eprintln!("token length too short");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
match self.model.authenticate(buf).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn heartbeat(self, heartbeat: Duration) {
|
||||
loop {
|
||||
time::sleep(heartbeat).await;
|
||||
|
||||
if self.is_closed() {
|
||||
break;
|
||||
}
|
||||
|
||||
match self.model.heartbeat().await {
|
||||
Ok(()) => {}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_garbage(self, gc_interval: Duration, gc_lifetime: Duration) {
|
||||
loop {
|
||||
time::sleep(gc_interval).await;
|
||||
|
||||
if self.is_closed() {
|
||||
break;
|
||||
}
|
||||
|
||||
self.model.collect_garbage(gc_lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
async fn init(
|
||||
self,
|
||||
token: Arc<[u8]>,
|
||||
heartbeat: Duration,
|
||||
gc_interval: Duration,
|
||||
gc_lifetime: Duration,
|
||||
) {
|
||||
tokio::spawn(self.clone().authenticate(token));
|
||||
tokio::spawn(self.clone().heartbeat(heartbeat));
|
||||
tokio::spawn(self.clone().collect_garbage(gc_interval, gc_lifetime));
|
||||
|
||||
let err = loop {
|
||||
tokio::select! {
|
||||
res = self.accept_uni_stream() => match res {
|
||||
Ok((recv, reg)) => tokio::spawn(self.clone().handle_uni_stream(recv, reg)),
|
||||
Err(err) => break err,
|
||||
},
|
||||
res = self.accept_bi_stream() => match res {
|
||||
Ok((send, recv, reg)) => tokio::spawn(self.clone().handle_bi_stream(send, recv, reg)),
|
||||
Err(err) => break err,
|
||||
},
|
||||
res = self.accept_datagram() => match res {
|
||||
Ok(dg) => tokio::spawn(self.clone().handle_datagram(dg)),
|
||||
Err(err) => break err,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
eprintln!("{err}");
|
||||
}
|
||||
}
|
70
tuic-client/src/main.rs
Normal file
70
tuic-client/src/main.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use self::{
|
||||
config::{Config, ConfigError},
|
||||
connection::Endpoint,
|
||||
socks5::Server as Socks5Server,
|
||||
};
|
||||
use quinn::{ConnectError, ConnectionError};
|
||||
use std::{env, io::Error as IoError, process};
|
||||
use thiserror::Error;
|
||||
use tuic_quinn::Error as ModelError;
|
||||
use webpki::Error as WebpkiError;
|
||||
|
||||
mod config;
|
||||
mod connection;
|
||||
mod socks5;
|
||||
mod utils;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cfg = match Config::parse(env::args_os()) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(ConfigError::Version(msg) | ConfigError::Help(msg)) => {
|
||||
println!("{msg}");
|
||||
process::exit(0);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
match Endpoint::set_config(cfg.relay) {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
match Socks5Server::set_config(cfg.local) {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Socks5Server::start().await;
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
#[error(transparent)]
|
||||
Connect(#[from] ConnectError),
|
||||
#[error(transparent)]
|
||||
Connection(#[from] ConnectionError),
|
||||
#[error(transparent)]
|
||||
Model(#[from] ModelError),
|
||||
#[error(transparent)]
|
||||
Webpki(#[from] WebpkiError),
|
||||
#[error("timeout establishing connection")]
|
||||
Timeout,
|
||||
#[error("cannot resolve the server name")]
|
||||
DnsResolve,
|
||||
#[error("received packet from an unexpected source")]
|
||||
WrongPacketSource,
|
||||
#[error("invalid socks5 authentication")]
|
||||
InvalidSocks5Auth,
|
||||
}
|
294
tuic-client/src/socks5.rs
Normal file
294
tuic-client/src/socks5.rs
Normal file
@ -0,0 +1,294 @@
|
||||
use crate::{config::Local, connection::Connection as TuicConnection, Error};
|
||||
use bytes::Bytes;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
|
||||
use socks5_proto::{Address, Reply};
|
||||
use socks5_server::{
|
||||
auth::{NoAuth, Password},
|
||||
connection::{associate, bind, connect},
|
||||
Associate, AssociatedUdpSocket, Auth, Bind, Connect, Connection, Server as Socks5Server,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{Error as IoError, ErrorKind},
|
||||
net::{IpAddr, SocketAddr, TcpListener as StdTcpListener, UdpSocket as StdUdpSocket},
|
||||
sync::{
|
||||
atomic::{AtomicU16, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use tokio::{
|
||||
io::{self, AsyncWriteExt},
|
||||
net::{TcpListener, UdpSocket},
|
||||
};
|
||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||
use tuic::Address as TuicAddress;
|
||||
|
||||
static SERVER: OnceCell<Server> = OnceCell::new();
|
||||
|
||||
pub struct Server {
|
||||
inner: Socks5Server,
|
||||
addr: SocketAddr,
|
||||
dual_stack: Option<bool>,
|
||||
max_pkt_size: usize,
|
||||
next_assoc_id: AtomicU16,
|
||||
udp_sessions: Mutex<HashMap<u16, Arc<AssociatedUdpSocket>>>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn set_config(cfg: Local) -> Result<(), Error> {
|
||||
let socket = {
|
||||
let domain = match cfg.server.ip() {
|
||||
IpAddr::V4(_) => Domain::IPV4,
|
||||
IpAddr::V6(_) => Domain::IPV6,
|
||||
};
|
||||
|
||||
let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?;
|
||||
|
||||
if let Some(dual_stack) = cfg.dual_stack {
|
||||
socket.set_only_v6(!dual_stack)?;
|
||||
}
|
||||
|
||||
socket.set_reuse_address(true)?;
|
||||
socket.bind(&SockAddr::from(cfg.server))?;
|
||||
TcpListener::from_std(StdTcpListener::from(socket))?
|
||||
};
|
||||
|
||||
let auth: Arc<dyn Auth + Send + Sync> = match (cfg.username, cfg.password) {
|
||||
(Some(username), Some(password)) => {
|
||||
Arc::new(Password::new(username.into_bytes(), password.into_bytes()))
|
||||
}
|
||||
(None, None) => Arc::new(NoAuth),
|
||||
_ => return Err(Error::InvalidSocks5Auth),
|
||||
};
|
||||
|
||||
let server = Self {
|
||||
inner: Socks5Server::new(socket, auth),
|
||||
addr: cfg.server,
|
||||
dual_stack: cfg.dual_stack,
|
||||
max_pkt_size: cfg.max_packet_size,
|
||||
next_assoc_id: AtomicU16::new(0),
|
||||
udp_sessions: Mutex::new(HashMap::new()),
|
||||
};
|
||||
|
||||
SERVER
|
||||
.set(server)
|
||||
.map_err(|_| "socks5 server already initialized")
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start() {
|
||||
let server = SERVER.get().unwrap();
|
||||
|
||||
loop {
|
||||
match server.inner.accept().await {
|
||||
Ok((conn, _)) => {
|
||||
tokio::spawn(async move {
|
||||
let res = match conn.handshake().await {
|
||||
Ok(Connection::Associate(associate, addr)) => {
|
||||
Self::handle_associate(associate, addr).await
|
||||
}
|
||||
Ok(Connection::Bind(bind, addr)) => Self::handle_bind(bind, addr).await,
|
||||
Ok(Connection::Connect(connect, addr)) => {
|
||||
Self::handle_connect(connect, addr).await
|
||||
}
|
||||
Err(err) => Err(Error::from(err)),
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(_) => {}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_associate(
|
||||
assoc: Associate<associate::NeedReply>,
|
||||
_addr: Address,
|
||||
) -> Result<(), Error> {
|
||||
async fn get_assoc_socket() -> Result<(Arc<AssociatedUdpSocket>, SocketAddr), IoError> {
|
||||
let domain = match SERVER.get().unwrap().addr.ip() {
|
||||
IpAddr::V4(_) => Domain::IPV4,
|
||||
IpAddr::V6(_) => Domain::IPV6,
|
||||
};
|
||||
|
||||
let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
|
||||
|
||||
if let Some(dual_stack) = SERVER.get().unwrap().dual_stack {
|
||||
socket.set_only_v6(!dual_stack)?;
|
||||
}
|
||||
|
||||
socket.bind(&SockAddr::from(SERVER.get().unwrap().addr))?;
|
||||
|
||||
let socket = AssociatedUdpSocket::from((
|
||||
UdpSocket::from_std(StdUdpSocket::from(socket))?,
|
||||
SERVER.get().unwrap().max_pkt_size,
|
||||
));
|
||||
|
||||
let addr = socket.local_addr()?;
|
||||
Ok((Arc::new(socket), addr))
|
||||
}
|
||||
|
||||
match get_assoc_socket().await {
|
||||
Ok((assoc_socket, assoc_addr)) => {
|
||||
let assoc = assoc
|
||||
.reply(Reply::Succeeded, Address::SocketAddress(assoc_addr))
|
||||
.await?;
|
||||
Self::send_pkt(assoc, assoc_socket).await
|
||||
}
|
||||
Err(err) => {
|
||||
let mut assoc = assoc
|
||||
.reply(Reply::GeneralFailure, Address::unspecified())
|
||||
.await?;
|
||||
let _ = assoc.shutdown().await;
|
||||
Err(Error::from(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_bind(bind: Bind<bind::NeedFirstReply>, _addr: Address) -> Result<(), Error> {
|
||||
let mut conn = bind
|
||||
.reply(Reply::CommandNotSupported, Address::unspecified())
|
||||
.await?;
|
||||
let _ = conn.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_connect(conn: Connect<connect::NeedReply>, addr: Address) -> Result<(), Error> {
|
||||
let target_addr = match addr {
|
||||
Address::DomainAddress(domain, port) => TuicAddress::DomainAddress(domain, port),
|
||||
Address::SocketAddress(addr) => TuicAddress::SocketAddress(addr),
|
||||
};
|
||||
|
||||
let relay = match TuicConnection::get().await {
|
||||
Ok(conn) => conn.connect(target_addr).await,
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
match relay {
|
||||
Ok(relay) => {
|
||||
let mut relay = relay.compat();
|
||||
let conn = conn.reply(Reply::Succeeded, Address::unspecified()).await;
|
||||
|
||||
match conn {
|
||||
Ok(mut conn) => match io::copy_bidirectional(&mut conn, &mut relay).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
let _ = conn.shutdown().await;
|
||||
let _ = relay.shutdown().await;
|
||||
Err(Error::from(err))
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
let _ = relay.shutdown().await;
|
||||
Err(Error::from(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let mut conn = conn
|
||||
.reply(Reply::GeneralFailure, Address::unspecified())
|
||||
.await?;
|
||||
let _ = conn.shutdown().await;
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_pkt(
|
||||
mut assoc: Associate<associate::Ready>,
|
||||
assoc_socket: Arc<AssociatedUdpSocket>,
|
||||
) -> Result<(), Error> {
|
||||
let assoc_id = SERVER
|
||||
.get()
|
||||
.unwrap()
|
||||
.next_assoc_id
|
||||
.fetch_add(1, Ordering::AcqRel);
|
||||
|
||||
SERVER
|
||||
.get()
|
||||
.unwrap()
|
||||
.udp_sessions
|
||||
.lock()
|
||||
.insert(assoc_id, assoc_socket.clone());
|
||||
|
||||
let mut connected = None;
|
||||
|
||||
async fn accept_pkt(
|
||||
assoc_socket: &AssociatedUdpSocket,
|
||||
connected: &mut Option<SocketAddr>,
|
||||
assoc_id: u16,
|
||||
) -> Result<(), Error> {
|
||||
let (pkt, frag, dst_addr, src_addr) = assoc_socket.recv_from().await?;
|
||||
|
||||
if let Some(connected) = connected {
|
||||
if connected != &src_addr {
|
||||
Err(IoError::new(
|
||||
ErrorKind::Other,
|
||||
format!("invalid source address: {src_addr}"),
|
||||
))?;
|
||||
}
|
||||
} else {
|
||||
assoc_socket.connect(src_addr).await?;
|
||||
*connected = Some(src_addr);
|
||||
}
|
||||
|
||||
if frag != 0 {
|
||||
Err(IoError::new(
|
||||
ErrorKind::Other,
|
||||
"fragmented packet is not supported",
|
||||
))?;
|
||||
}
|
||||
|
||||
let target_addr = match dst_addr {
|
||||
Address::DomainAddress(domain, port) => TuicAddress::DomainAddress(domain, port),
|
||||
Address::SocketAddress(addr) => TuicAddress::SocketAddress(addr),
|
||||
};
|
||||
|
||||
TuicConnection::get()
|
||||
.await?
|
||||
.packet(pkt, target_addr, assoc_id)
|
||||
.await
|
||||
}
|
||||
|
||||
let res = tokio::select! {
|
||||
res = assoc.wait_until_closed() => res,
|
||||
_ = async { loop {
|
||||
if let Err(err) = accept_pkt(&assoc_socket, &mut connected, assoc_id).await {
|
||||
eprintln!("{err}");
|
||||
}
|
||||
}} => unreachable!(),
|
||||
};
|
||||
|
||||
let _ = assoc.shutdown().await;
|
||||
SERVER.get().unwrap().udp_sessions.lock().remove(&assoc_id);
|
||||
|
||||
match TuicConnection::get().await {
|
||||
Ok(conn) => match conn.dissociate(assoc_id).await {
|
||||
Ok(_) => {}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
},
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
|
||||
Ok(res?)
|
||||
}
|
||||
|
||||
pub async fn recv_pkt(pkt: Bytes, addr: Address, assoc_id: u16) -> Result<(), Error> {
|
||||
let assoc_socket = {
|
||||
let sessions = SERVER.get().unwrap().udp_sessions.lock();
|
||||
let Some(assoc_socket) = sessions.get(&assoc_id) else { unreachable!() };
|
||||
assoc_socket.clone()
|
||||
};
|
||||
|
||||
assoc_socket.send(pkt, 0, addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
108
tuic-client/src/utils.rs
Normal file
108
tuic-client/src/utils.rs
Normal file
@ -0,0 +1,108 @@
|
||||
use crate::Error;
|
||||
use rustls::{Certificate, RootCertStore};
|
||||
use rustls_pemfile::Item;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::BufReader,
|
||||
net::{IpAddr, SocketAddr},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
};
|
||||
use tokio::net;
|
||||
|
||||
pub fn load_certs(paths: Vec<PathBuf>, disable_native: bool) -> Result<RootCertStore, Error> {
|
||||
let mut certs = RootCertStore::empty();
|
||||
|
||||
for path in &paths {
|
||||
let mut file = BufReader::new(File::open(path)?);
|
||||
|
||||
while let Ok(Some(item)) = rustls_pemfile::read_one(&mut file) {
|
||||
if let Item::X509Certificate(cert) = item {
|
||||
certs.add(&Certificate(cert))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if certs.is_empty() {
|
||||
for path in &paths {
|
||||
certs.add(&Certificate(fs::read(path)?))?;
|
||||
}
|
||||
}
|
||||
|
||||
if !disable_native {
|
||||
for cert in rustls_native_certs::load_native_certs()? {
|
||||
certs.add(&Certificate(cert.0))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
pub struct ServerAddr {
|
||||
domain: String,
|
||||
port: u16,
|
||||
ip: Option<IpAddr>,
|
||||
}
|
||||
|
||||
impl ServerAddr {
|
||||
pub fn new(domain: String, port: u16, ip: Option<IpAddr>) -> Self {
|
||||
Self { domain, port, ip }
|
||||
}
|
||||
|
||||
pub fn server_name(&self) -> &str {
|
||||
&self.domain
|
||||
}
|
||||
|
||||
pub async fn resolve(&self) -> Result<impl Iterator<Item = SocketAddr>, Error> {
|
||||
if let Some(ip) = self.ip {
|
||||
Ok(vec![SocketAddr::from((ip, self.port))].into_iter())
|
||||
} else {
|
||||
Ok(net::lookup_host((self.domain.as_str(), self.port))
|
||||
.await?
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum UdpRelayMode {
|
||||
Native,
|
||||
Quic,
|
||||
}
|
||||
|
||||
impl FromStr for UdpRelayMode {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.eq_ignore_ascii_case("native") {
|
||||
Ok(Self::Native)
|
||||
} else if s.eq_ignore_ascii_case("quic") {
|
||||
Ok(Self::Quic)
|
||||
} else {
|
||||
Err("invalid UDP relay mode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum CongestionControl {
|
||||
Cubic,
|
||||
NewReno,
|
||||
Bbr,
|
||||
}
|
||||
|
||||
impl FromStr for CongestionControl {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.eq_ignore_ascii_case("cubic") {
|
||||
Ok(Self::Cubic)
|
||||
} else if s.eq_ignore_ascii_case("new_reno") || s.eq_ignore_ascii_case("newreno") {
|
||||
Ok(Self::NewReno)
|
||||
} else if s.eq_ignore_ascii_case("bbr") {
|
||||
Ok(Self::Bbr)
|
||||
} else {
|
||||
Err("invalid congestion control")
|
||||
}
|
||||
}
|
||||
}
|
11
tuic-quinn/Cargo.toml
Normal file
11
tuic-quinn/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "tuic-quinn"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytes = { version = "1.4.0", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.26", default-features = false, features = ["io", "std"] }
|
||||
quinn = { version = "0.9.3", default-features = false, features = ["futures-io"] }
|
||||
thiserror = { version = "1.0.38", default-features = false }
|
||||
tuic = { path = "../tuic", default-features = false, features = ["async_marshal", "marshal", "model"] }
|
424
tuic-quinn/src/lib.rs
Normal file
424
tuic-quinn/src/lib.rs
Normal file
@ -0,0 +1,424 @@
|
||||
use self::side::Side;
|
||||
use bytes::{BufMut, Bytes};
|
||||
use futures_util::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use quinn::{
|
||||
Connection as QuinnConnection, ConnectionError, RecvStream, SendDatagramError, SendStream,
|
||||
};
|
||||
use std::{
|
||||
io::{Cursor, Error as IoError},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tuic::{
|
||||
model::{
|
||||
side::{Rx, Tx},
|
||||
AssembleError, Connect as ConnectModel, Connection as ConnectionModel,
|
||||
Packet as PacketModel,
|
||||
},
|
||||
Address, Header, UnmarshalError,
|
||||
};
|
||||
|
||||
pub mod side {
|
||||
#[derive(Clone)]
|
||||
pub struct Client;
|
||||
#[derive(Clone)]
|
||||
pub struct Server;
|
||||
|
||||
pub(super) enum Side<C, S> {
|
||||
Client(C),
|
||||
Server(S),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Connection<Side> {
|
||||
conn: QuinnConnection,
|
||||
model: ConnectionModel<Bytes>,
|
||||
_marker: Side,
|
||||
}
|
||||
|
||||
impl<Side> Connection<Side> {
|
||||
pub fn packet_native(
|
||||
&self,
|
||||
pkt: impl AsRef<[u8]>,
|
||||
addr: Address,
|
||||
assoc_id: u16,
|
||||
) -> Result<(), Error> {
|
||||
let Some(max_pkt_size) = self.conn.max_datagram_size() else {
|
||||
return Err(Error::SendDatagram(SendDatagramError::Disabled));
|
||||
};
|
||||
|
||||
let model = self.model.send_packet(assoc_id, addr, max_pkt_size);
|
||||
|
||||
for (header, frag) in model.into_fragments(pkt) {
|
||||
let mut buf = vec![0; header.len() + frag.len()];
|
||||
header.write(&mut buf);
|
||||
buf.put_slice(frag);
|
||||
self.conn.send_datagram(Bytes::from(buf))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn packet_quic(
|
||||
&self,
|
||||
pkt: impl AsRef<[u8]>,
|
||||
addr: Address,
|
||||
assoc_id: u16,
|
||||
) -> Result<(), Error> {
|
||||
let model = self.model.send_packet(assoc_id, addr, u16::MAX as usize);
|
||||
|
||||
for (header, frag) in model.into_fragments(pkt) {
|
||||
let mut send = self.conn.open_uni().await?;
|
||||
header.async_marshal(&mut send).await?;
|
||||
AsyncWriteExt::write_all(&mut send, frag).await?;
|
||||
send.close().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn task_connect_count(&self) -> usize {
|
||||
self.model.task_connect_count()
|
||||
}
|
||||
|
||||
pub fn task_associate_count(&self) -> usize {
|
||||
self.model.task_associate_count()
|
||||
}
|
||||
|
||||
pub fn collect_garbage(&self, timeout: Duration) {
|
||||
self.model.collect_garbage(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
impl Connection<side::Client> {
|
||||
pub fn new(conn: QuinnConnection) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
model: ConnectionModel::new(),
|
||||
_marker: side::Client,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn authenticate(&self, token: [u8; 32]) -> Result<(), Error> {
|
||||
let model = self.model.send_authenticate(token);
|
||||
let mut send = self.conn.open_uni().await?;
|
||||
model.header().async_marshal(&mut send).await?;
|
||||
send.close().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn connect(&self, addr: Address) -> Result<Connect, Error> {
|
||||
let model = self.model.send_connect(addr);
|
||||
let (mut send, recv) = self.conn.open_bi().await?;
|
||||
model.header().async_marshal(&mut send).await?;
|
||||
Ok(Connect::new(Side::Client(model), send, recv))
|
||||
}
|
||||
|
||||
pub async fn dissociate(&self, assoc_id: u16) -> Result<(), Error> {
|
||||
let model = self.model.send_dissociate(assoc_id);
|
||||
let mut send = self.conn.open_uni().await?;
|
||||
model.header().async_marshal(&mut send).await?;
|
||||
send.close().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn heartbeat(&self) -> Result<(), Error> {
|
||||
let model = self.model.send_heartbeat();
|
||||
let mut buf = Vec::with_capacity(model.header().len());
|
||||
model.header().async_marshal(&mut buf).await.unwrap();
|
||||
self.conn.send_datagram(Bytes::from(buf))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn accept_uni_stream(&self, mut recv: RecvStream) -> Result<Task, Error> {
|
||||
let header = match Header::async_unmarshal(&mut recv).await {
|
||||
Ok(header) => header,
|
||||
Err(err) => return Err(Error::UnmarshalUniStream(err, recv)),
|
||||
};
|
||||
|
||||
match header {
|
||||
Header::Authenticate(_) => Err(Error::BadCommandUniStream("authenticate", recv)),
|
||||
Header::Connect(_) => Err(Error::BadCommandUniStream("connect", recv)),
|
||||
Header::Packet(pkt) => {
|
||||
let assoc_id = pkt.assoc_id();
|
||||
self.model
|
||||
.recv_packet(pkt)
|
||||
.map_or(Err(Error::InvalidUdpSession(assoc_id)), |pkt| {
|
||||
Ok(Task::Packet(Packet::new(pkt, PacketSource::Quic(recv))))
|
||||
})
|
||||
}
|
||||
Header::Dissociate(_) => Err(Error::BadCommandUniStream("dissociate", recv)),
|
||||
Header::Heartbeat(_) => Err(Error::BadCommandUniStream("heartbeat", recv)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn accept_bi_stream(
|
||||
&self,
|
||||
send: SendStream,
|
||||
mut recv: RecvStream,
|
||||
) -> Result<Task, Error> {
|
||||
let header = match Header::async_unmarshal(&mut recv).await {
|
||||
Ok(header) => header,
|
||||
Err(err) => return Err(Error::UnmarshalBiStream(err, send, recv)),
|
||||
};
|
||||
|
||||
match header {
|
||||
Header::Authenticate(_) => Err(Error::BadCommandBiStream("authenticate", send, recv)),
|
||||
Header::Connect(_) => Err(Error::BadCommandBiStream("connect", send, recv)),
|
||||
Header::Packet(_) => Err(Error::BadCommandBiStream("packet", send, recv)),
|
||||
Header::Dissociate(_) => Err(Error::BadCommandBiStream("dissociate", send, recv)),
|
||||
Header::Heartbeat(_) => Err(Error::BadCommandBiStream("heartbeat", send, recv)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept_datagram(&self, dg: Bytes) -> Result<Task, Error> {
|
||||
let mut dg = Cursor::new(dg);
|
||||
|
||||
let header = match Header::unmarshal(&mut dg) {
|
||||
Ok(header) => header,
|
||||
Err(err) => return Err(Error::UnmarshalDatagram(err, dg.into_inner())),
|
||||
};
|
||||
|
||||
match header {
|
||||
Header::Authenticate(_) => {
|
||||
Err(Error::BadCommandDatagram("authenticate", dg.into_inner()))
|
||||
}
|
||||
Header::Connect(_) => Err(Error::BadCommandDatagram("connect", dg.into_inner())),
|
||||
Header::Packet(pkt) => {
|
||||
let assoc_id = pkt.assoc_id();
|
||||
if let Some(pkt) = self.model.recv_packet(pkt) {
|
||||
let pos = dg.position() as usize;
|
||||
let mut buf = dg.into_inner();
|
||||
if (pos + pkt.size() as usize) < buf.len() {
|
||||
buf = buf.slice(pos..pos + pkt.size() as usize);
|
||||
Ok(Task::Packet(Packet::new(pkt, PacketSource::Native(buf))))
|
||||
} else {
|
||||
Err(Error::PayloadLength(pkt.size() as usize, buf.len() - pos))
|
||||
}
|
||||
} else {
|
||||
Err(Error::InvalidUdpSession(assoc_id))
|
||||
}
|
||||
}
|
||||
Header::Dissociate(_) => Err(Error::BadCommandDatagram("dissociate", dg.into_inner())),
|
||||
Header::Heartbeat(_) => Err(Error::BadCommandDatagram("heartbeat", dg.into_inner())),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Connection<side::Server> {
|
||||
pub fn new(conn: QuinnConnection) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
model: ConnectionModel::new(),
|
||||
_marker: side::Server,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn accept_uni_stream(&self, mut recv: RecvStream) -> Result<Task, Error> {
|
||||
let header = match Header::async_unmarshal(&mut recv).await {
|
||||
Ok(header) => header,
|
||||
Err(err) => return Err(Error::UnmarshalUniStream(err, recv)),
|
||||
};
|
||||
|
||||
match header {
|
||||
Header::Authenticate(auth) => {
|
||||
let model = self.model.recv_authenticate(auth);
|
||||
Ok(Task::Authenticate(model.token()))
|
||||
}
|
||||
Header::Connect(_) => Err(Error::BadCommandUniStream("connect", recv)),
|
||||
Header::Packet(pkt) => {
|
||||
let model = self.model.recv_packet_unrestricted(pkt);
|
||||
Ok(Task::Packet(Packet::new(model, PacketSource::Quic(recv))))
|
||||
}
|
||||
Header::Dissociate(dissoc) => {
|
||||
let model = self.model.recv_dissociate(dissoc);
|
||||
Ok(Task::Dissociate(model.assoc_id()))
|
||||
}
|
||||
Header::Heartbeat(_) => Err(Error::BadCommandUniStream("heartbeat", recv)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn accept_bi_stream(
|
||||
&self,
|
||||
send: SendStream,
|
||||
mut recv: RecvStream,
|
||||
) -> Result<Task, Error> {
|
||||
let header = match Header::async_unmarshal(&mut recv).await {
|
||||
Ok(header) => header,
|
||||
Err(err) => return Err(Error::UnmarshalBiStream(err, send, recv)),
|
||||
};
|
||||
|
||||
match header {
|
||||
Header::Authenticate(_) => Err(Error::BadCommandBiStream("authenticate", send, recv)),
|
||||
Header::Connect(conn) => {
|
||||
let model = self.model.recv_connect(conn);
|
||||
Ok(Task::Connect(Connect::new(Side::Server(model), send, recv)))
|
||||
}
|
||||
Header::Packet(_) => Err(Error::BadCommandBiStream("packet", send, recv)),
|
||||
Header::Dissociate(_) => Err(Error::BadCommandBiStream("dissociate", send, recv)),
|
||||
Header::Heartbeat(_) => Err(Error::BadCommandBiStream("heartbeat", send, recv)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept_datagram(&self, dg: Bytes) -> Result<Task, Error> {
|
||||
let mut dg = Cursor::new(dg);
|
||||
|
||||
let header = match Header::unmarshal(&mut dg) {
|
||||
Ok(header) => header,
|
||||
Err(err) => return Err(Error::UnmarshalDatagram(err, dg.into_inner())),
|
||||
};
|
||||
|
||||
match header {
|
||||
Header::Authenticate(_) => {
|
||||
Err(Error::BadCommandDatagram("authenticate", dg.into_inner()))
|
||||
}
|
||||
Header::Connect(_) => Err(Error::BadCommandDatagram("connect", dg.into_inner())),
|
||||
Header::Packet(pkt) => {
|
||||
let model = self.model.recv_packet_unrestricted(pkt);
|
||||
let pos = dg.position() as usize;
|
||||
let buf = dg.into_inner().slice(pos..pos + model.size() as usize);
|
||||
Ok(Task::Packet(Packet::new(model, PacketSource::Native(buf))))
|
||||
}
|
||||
Header::Dissociate(_) => Err(Error::BadCommandDatagram("dissociate", dg.into_inner())),
|
||||
Header::Heartbeat(hb) => {
|
||||
let _ = self.model.recv_heartbeat(hb);
|
||||
Ok(Task::Heartbeat)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Connect {
|
||||
model: Side<ConnectModel<Tx>, ConnectModel<Rx>>,
|
||||
send: SendStream,
|
||||
recv: RecvStream,
|
||||
}
|
||||
|
||||
impl Connect {
|
||||
fn new(
|
||||
model: Side<ConnectModel<Tx>, ConnectModel<Rx>>,
|
||||
send: SendStream,
|
||||
recv: RecvStream,
|
||||
) -> Self {
|
||||
Self { model, send, recv }
|
||||
}
|
||||
|
||||
pub fn addr(&self) -> &Address {
|
||||
match &self.model {
|
||||
Side::Client(model) => {
|
||||
let Header::Connect(conn) = model.header() else { unreachable!() };
|
||||
conn.addr()
|
||||
}
|
||||
Side::Server(model) => model.addr(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for Connect {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<Result<usize, IoError>> {
|
||||
AsyncRead::poll_read(Pin::new(&mut self.get_mut().recv), cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for Connect {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, IoError>> {
|
||||
AsyncWrite::poll_write(Pin::new(&mut self.get_mut().send), cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), IoError>> {
|
||||
AsyncWrite::poll_flush(Pin::new(&mut self.get_mut().send), cx)
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), IoError>> {
|
||||
AsyncWrite::poll_close(Pin::new(&mut self.get_mut().send), cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Packet {
|
||||
model: PacketModel<Rx, Bytes>,
|
||||
src: PacketSource,
|
||||
}
|
||||
|
||||
enum PacketSource {
|
||||
Quic(RecvStream),
|
||||
Native(Bytes),
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
fn new(model: PacketModel<Rx, Bytes>, src: PacketSource) -> Self {
|
||||
Self { src, model }
|
||||
}
|
||||
|
||||
pub async fn accept(self) -> Result<Option<(Bytes, Address, u16)>, Error> {
|
||||
let pkt = match self.src {
|
||||
PacketSource::Quic(mut recv) => {
|
||||
let mut buf = vec![0; self.model.size() as usize];
|
||||
AsyncReadExt::read_exact(&mut recv, &mut buf).await?;
|
||||
Bytes::from(buf)
|
||||
}
|
||||
PacketSource::Native(pkt) => pkt,
|
||||
};
|
||||
|
||||
let mut asm = Vec::new();
|
||||
|
||||
Ok(self
|
||||
.model
|
||||
.assemble(pkt)?
|
||||
.map(|pkt| pkt.assemble(&mut asm))
|
||||
.map(|(addr, assoc_id)| (Bytes::from(asm), addr, assoc_id)))
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
pub enum Task {
|
||||
Authenticate([u8; 32]),
|
||||
Connect(Connect),
|
||||
Packet(Packet),
|
||||
Dissociate(u16),
|
||||
Heartbeat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
#[error(transparent)]
|
||||
Connection(#[from] ConnectionError),
|
||||
#[error(transparent)]
|
||||
SendDatagram(#[from] SendDatagramError),
|
||||
#[error("expecting payload length {0} but got {1}")]
|
||||
PayloadLength(usize, usize),
|
||||
#[error("invalid udp session {0}")]
|
||||
InvalidUdpSession(u16),
|
||||
#[error(transparent)]
|
||||
Assemble(#[from] AssembleError),
|
||||
#[error("error unmarshaling uni_stream: {0}")]
|
||||
UnmarshalUniStream(UnmarshalError, RecvStream),
|
||||
#[error("error unmarshaling bi_stream: {0}")]
|
||||
UnmarshalBiStream(UnmarshalError, SendStream, RecvStream),
|
||||
#[error("error unmarshaling datagram: {0}")]
|
||||
UnmarshalDatagram(UnmarshalError, Bytes),
|
||||
#[error("bad command `{0}` from uni_stream")]
|
||||
BadCommandUniStream(&'static str, RecvStream),
|
||||
#[error("bad command `{0}` from bi_stream")]
|
||||
BadCommandBiStream(&'static str, SendStream, RecvStream),
|
||||
#[error("bad command `{0}` from datagram")]
|
||||
BadCommandDatagram(&'static str, Bytes),
|
||||
}
|
22
tuic-server/Cargo.toml
Normal file
22
tuic-server/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "tuic-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytes = { version = "1.4.0", default-features = false, features = ["std"] }
|
||||
crossbeam-utils = { version = "0.8.14", default-features = false, features = ["std"] }
|
||||
lexopt = { version = "0.3.0", default-features = false }
|
||||
parking_lot = { version = "0.12.1", default-features = false }
|
||||
quinn = { version = "0.9.3", default-features = false, features = ["futures-io", "runtime-tokio", "tls-rustls"] }
|
||||
register-count = { version = "0.1.0", default-features = false, features = ["std"] }
|
||||
rustls = { version = "0.20.8", default-features = false, features = ["quic"] }
|
||||
rustls-pemfile = { version = "1.0.2", default-features = false }
|
||||
serde = { version = "1.0.152", default-features = false, features = ["derive", "std"] }
|
||||
serde_json = { version = "1.0.91", default-features = false, features = ["std"] }
|
||||
socket2 = { version = "0.4.7", default-features = false }
|
||||
thiserror = { version = "1.0.38", default-features = false }
|
||||
tokio = { version = "1.25.0", default-features = false, features = ["macros", "net", "parking_lot", "rt-multi-thread", "time"] }
|
||||
tokio-util = { version = "0.7.4", default-features = false, features = ["compat"] }
|
||||
tuic = { path = "../tuic", default-features = false }
|
||||
tuic-quinn = { path = "../tuic-quinn", default-features = false }
|
147
tuic-server/src/config.rs
Normal file
147
tuic-server/src/config.rs
Normal file
@ -0,0 +1,147 @@
|
||||
use crate::utils::CongestionControl;
|
||||
use lexopt::{Arg, Error as ArgumentError, Parser};
|
||||
use serde::{de::Error as DeError, Deserialize, Deserializer};
|
||||
use serde_json::Error as SerdeError;
|
||||
use std::{
|
||||
env::ArgsOs, fmt::Display, fs::File, io::Error as IoError, net::SocketAddr, path::PathBuf,
|
||||
str::FromStr, time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
const HELP_MSG: &str = r#"
|
||||
Usage tuic-server [arguments]
|
||||
|
||||
Arguments:
|
||||
-c, --config <path> Path to the config file (required)
|
||||
-v, --version Print the version
|
||||
-h, --help Print this help message
|
||||
"#;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
pub server: SocketAddr,
|
||||
pub token: String,
|
||||
pub certificate: PathBuf,
|
||||
pub private_key: PathBuf,
|
||||
#[serde(
|
||||
default = "default::congestion_control",
|
||||
deserialize_with = "deserialize_from_str"
|
||||
)]
|
||||
pub congestion_control: CongestionControl,
|
||||
#[serde(default = "default::alpn")]
|
||||
pub alpn: Vec<String>,
|
||||
#[serde(default = "default::udp_relay_ipv6")]
|
||||
pub udp_relay_ipv6: bool,
|
||||
#[serde(default = "default::zero_rtt_handshake")]
|
||||
pub zero_rtt_handshake: bool,
|
||||
pub dual_stack: Option<bool>,
|
||||
#[serde(default = "default::auth_timeout")]
|
||||
pub auth_timeout: Duration,
|
||||
#[serde(default = "default::max_idle_time")]
|
||||
pub max_idle_time: Duration,
|
||||
#[serde(default = "default::max_external_packet_size")]
|
||||
pub max_external_packet_size: usize,
|
||||
#[serde(default = "default::gc_interval")]
|
||||
pub gc_interval: Duration,
|
||||
#[serde(default = "default::gc_lifetime")]
|
||||
pub gc_lifetime: Duration,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn parse(args: ArgsOs) -> Result<Self, ConfigError> {
|
||||
let mut parser = Parser::from_iter(args);
|
||||
let mut path = None;
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Arg::Short('c') | Arg::Long("config") => {
|
||||
if path.is_none() {
|
||||
path = Some(parser.value()?);
|
||||
} else {
|
||||
return Err(ConfigError::Argument(arg.unexpected()));
|
||||
}
|
||||
}
|
||||
Arg::Short('v') | Arg::Long("version") => {
|
||||
return Err(ConfigError::Version(env!("CARGO_PKG_VERSION")))
|
||||
}
|
||||
Arg::Short('h') | Arg::Long("help") => return Err(ConfigError::Help(HELP_MSG)),
|
||||
_ => return Err(ConfigError::Argument(arg.unexpected())),
|
||||
}
|
||||
}
|
||||
|
||||
if path.is_none() {
|
||||
return Err(ConfigError::NoConfig);
|
||||
}
|
||||
|
||||
let file = File::open(path.unwrap())?;
|
||||
Ok(serde_json::from_reader(file)?)
|
||||
}
|
||||
}
|
||||
|
||||
mod default {
|
||||
use crate::utils::CongestionControl;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn congestion_control() -> CongestionControl {
|
||||
CongestionControl::Cubic
|
||||
}
|
||||
|
||||
pub fn alpn() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn udp_relay_ipv6() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn zero_rtt_handshake() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn auth_timeout() -> Duration {
|
||||
Duration::from_secs(3)
|
||||
}
|
||||
|
||||
pub fn max_idle_time() -> Duration {
|
||||
Duration::from_secs(15)
|
||||
}
|
||||
|
||||
pub fn max_external_packet_size() -> usize {
|
||||
1500
|
||||
}
|
||||
|
||||
pub fn gc_interval() -> Duration {
|
||||
Duration::from_secs(3)
|
||||
}
|
||||
|
||||
pub fn gc_lifetime() -> Duration {
|
||||
Duration::from_secs(15)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_from_str<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Display,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
T::from_str(&s).map_err(DeError::custom)
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
#[error(transparent)]
|
||||
Argument(#[from] ArgumentError),
|
||||
#[error("no config file specified")]
|
||||
NoConfig,
|
||||
#[error("{0}")]
|
||||
Version(&'static str),
|
||||
#[error("{0}")]
|
||||
Help(&'static str),
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
#[error(transparent)]
|
||||
Serde(#[from] SerdeError),
|
||||
}
|
61
tuic-server/src/main.rs
Normal file
61
tuic-server/src/main.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use self::{
|
||||
config::{Config, ConfigError},
|
||||
server::Server,
|
||||
};
|
||||
use quinn::ConnectionError;
|
||||
use rustls::Error as RustlsError;
|
||||
use std::{env, io::Error as IoError, net::SocketAddr, process};
|
||||
use thiserror::Error;
|
||||
use tuic::Address;
|
||||
use tuic_quinn::Error as ModelError;
|
||||
|
||||
mod config;
|
||||
mod server;
|
||||
mod utils;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cfg = match Config::parse(env::args_os()) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(ConfigError::Version(msg) | ConfigError::Help(msg)) => {
|
||||
println!("{msg}");
|
||||
process::exit(0);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
match Server::init(cfg) {
|
||||
Ok(server) => server.start().await,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
#[error(transparent)]
|
||||
Rustls(#[from] RustlsError),
|
||||
#[error("invalid max idle time")]
|
||||
InvalidMaxIdleTime,
|
||||
#[error(transparent)]
|
||||
Connection(#[from] ConnectionError),
|
||||
#[error(transparent)]
|
||||
Model(#[from] ModelError),
|
||||
#[error("duplicated authentication")]
|
||||
DuplicatedAuth,
|
||||
#[error("token length too short")]
|
||||
ExportKeyingMaterial,
|
||||
#[error("authentication failed")]
|
||||
AuthFailed,
|
||||
#[error("received packet from unexpected source")]
|
||||
UnexpectedPacketSource,
|
||||
#[error("{0} resolved to {1} but IPv6 UDP relay disabled")]
|
||||
UdpRelayIpv6Disabled(Address, SocketAddr),
|
||||
}
|
672
tuic-server/src/server.rs
Normal file
672
tuic-server/src/server.rs
Normal file
@ -0,0 +1,672 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
utils::{self, CongestionControl, UdpRelayMode},
|
||||
Error,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use parking_lot::Mutex;
|
||||
use quinn::{
|
||||
congestion::{BbrConfig, CubicConfig, NewRenoConfig},
|
||||
Connecting, Connection as QuinnConnection, Endpoint, EndpointConfig, IdleTimeout, RecvStream,
|
||||
SendStream, ServerConfig, TokioRuntime, TransportConfig, VarInt,
|
||||
};
|
||||
use register_count::{Counter, Register};
|
||||
use rustls::{version, ServerConfig as RustlsServerConfig};
|
||||
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
future::Future,
|
||||
io::{Error as IoError, ErrorKind},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket as StdUdpSocket},
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
task::{Context, Poll, Waker},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{
|
||||
io::{self, AsyncWriteExt},
|
||||
net::{self, TcpStream, UdpSocket},
|
||||
sync::{
|
||||
oneshot::{self, Receiver, Sender},
|
||||
Mutex as AsyncMutex,
|
||||
},
|
||||
time,
|
||||
};
|
||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||
use tuic::Address;
|
||||
use tuic_quinn::{side, Connect, Connection as Model, Packet, Task};
|
||||
|
||||
const DEFAULT_CONCURRENT_STREAMS: usize = 32;
|
||||
|
||||
pub struct Server {
|
||||
ep: Endpoint,
|
||||
token: Arc<[u8]>,
|
||||
udp_relay_ipv6: bool,
|
||||
zero_rtt_handshake: bool,
|
||||
auth_timeout: Duration,
|
||||
max_external_pkt_size: usize,
|
||||
gc_interval: Duration,
|
||||
gc_lifetime: Duration,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn init(cfg: Config) -> Result<Self, Error> {
|
||||
let certs = utils::load_certs(cfg.certificate)?;
|
||||
let priv_key = utils::load_priv_key(cfg.private_key)?;
|
||||
|
||||
let mut crypto = RustlsServerConfig::builder()
|
||||
.with_safe_default_cipher_suites()
|
||||
.with_safe_default_kx_groups()
|
||||
.with_protocol_versions(&[&version::TLS13])
|
||||
.unwrap()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, priv_key)?;
|
||||
|
||||
crypto.alpn_protocols = cfg.alpn.into_iter().map(|alpn| alpn.into_bytes()).collect();
|
||||
crypto.max_early_data_size = u32::MAX;
|
||||
crypto.send_half_rtt_data = cfg.zero_rtt_handshake;
|
||||
|
||||
let mut config = ServerConfig::with_crypto(Arc::new(crypto));
|
||||
let mut tp_cfg = TransportConfig::default();
|
||||
|
||||
tp_cfg
|
||||
.max_concurrent_bidi_streams(VarInt::from(DEFAULT_CONCURRENT_STREAMS as u32))
|
||||
.max_concurrent_uni_streams(VarInt::from(DEFAULT_CONCURRENT_STREAMS as u32))
|
||||
.max_idle_timeout(Some(
|
||||
IdleTimeout::try_from(cfg.max_idle_time).map_err(|_| Error::InvalidMaxIdleTime)?,
|
||||
));
|
||||
|
||||
match cfg.congestion_control {
|
||||
CongestionControl::Cubic => {
|
||||
tp_cfg.congestion_controller_factory(Arc::new(CubicConfig::default()))
|
||||
}
|
||||
CongestionControl::NewReno => {
|
||||
tp_cfg.congestion_controller_factory(Arc::new(NewRenoConfig::default()))
|
||||
}
|
||||
CongestionControl::Bbr => {
|
||||
tp_cfg.congestion_controller_factory(Arc::new(BbrConfig::default()))
|
||||
}
|
||||
};
|
||||
|
||||
config.transport_config(Arc::new(tp_cfg));
|
||||
|
||||
let domain = match cfg.server.ip() {
|
||||
IpAddr::V4(_) => Domain::IPV4,
|
||||
IpAddr::V6(_) => Domain::IPV6,
|
||||
};
|
||||
|
||||
let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
|
||||
|
||||
if let Some(dual_stack) = cfg.dual_stack {
|
||||
socket.set_only_v6(!dual_stack)?;
|
||||
}
|
||||
|
||||
socket.bind(&SockAddr::from(cfg.server))?;
|
||||
let socket = StdUdpSocket::from(socket);
|
||||
|
||||
let ep = Endpoint::new(
|
||||
EndpointConfig::default(),
|
||||
Some(config),
|
||||
socket,
|
||||
TokioRuntime,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
ep,
|
||||
token: Arc::from(cfg.token.into_bytes().into_boxed_slice()),
|
||||
udp_relay_ipv6: cfg.udp_relay_ipv6,
|
||||
zero_rtt_handshake: cfg.zero_rtt_handshake,
|
||||
auth_timeout: cfg.auth_timeout,
|
||||
max_external_pkt_size: cfg.max_external_packet_size,
|
||||
gc_interval: cfg.gc_interval,
|
||||
gc_lifetime: cfg.gc_lifetime,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start(&self) {
|
||||
loop {
|
||||
let conn = self.ep.accept().await.unwrap();
|
||||
|
||||
tokio::spawn(Connection::handle(
|
||||
conn,
|
||||
self.token.clone(),
|
||||
self.udp_relay_ipv6,
|
||||
self.zero_rtt_handshake,
|
||||
self.auth_timeout,
|
||||
self.max_external_pkt_size,
|
||||
self.gc_interval,
|
||||
self.gc_lifetime,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Connection {
|
||||
inner: QuinnConnection,
|
||||
model: Model<side::Server>,
|
||||
token: Arc<[u8]>,
|
||||
udp_relay_ipv6: bool,
|
||||
is_authed: IsAuthed,
|
||||
udp_sessions: Arc<AsyncMutex<HashMap<u16, UdpSession>>>,
|
||||
udp_relay_mode: Arc<AtomicCell<Option<UdpRelayMode>>>,
|
||||
max_external_pkt_size: usize,
|
||||
remote_uni_stream_cnt: Counter,
|
||||
remote_bi_stream_cnt: Counter,
|
||||
max_concurrent_uni_streams: Arc<AtomicUsize>,
|
||||
max_concurrent_bi_streams: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
impl Connection {
|
||||
async fn handle(
|
||||
conn: Connecting,
|
||||
token: Arc<[u8]>,
|
||||
udp_relay_ipv6: bool,
|
||||
zero_rtt_handshake: bool,
|
||||
auth_timeout: Duration,
|
||||
max_external_pkt_size: usize,
|
||||
gc_interval: Duration,
|
||||
gc_lifetime: Duration,
|
||||
) {
|
||||
match Self::init(
|
||||
conn,
|
||||
token,
|
||||
udp_relay_ipv6,
|
||||
zero_rtt_handshake,
|
||||
max_external_pkt_size,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(conn) => {
|
||||
tokio::spawn(conn.clone().handle_auth_timeout(auth_timeout));
|
||||
tokio::spawn(conn.clone().collect_garbage(gc_interval, gc_lifetime));
|
||||
|
||||
loop {
|
||||
if conn.is_closed() {
|
||||
break;
|
||||
}
|
||||
|
||||
match conn.accept().await {
|
||||
Ok(()) => {}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn init(
|
||||
conn: Connecting,
|
||||
token: Arc<[u8]>,
|
||||
udp_relay_ipv6: bool,
|
||||
zero_rtt_handshake: bool,
|
||||
max_external_pkt_size: usize,
|
||||
) -> Result<Self, Error> {
|
||||
let conn = if zero_rtt_handshake {
|
||||
match conn.into_0rtt() {
|
||||
Ok((conn, _)) => conn,
|
||||
Err(conn) => {
|
||||
eprintln!("0-RTT handshake failed, fallback to 1-RTT handshake");
|
||||
conn.await?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conn.await?
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
inner: conn.clone(),
|
||||
model: Model::<side::Server>::new(conn),
|
||||
token,
|
||||
udp_relay_ipv6,
|
||||
is_authed: IsAuthed::new(),
|
||||
udp_sessions: Arc::new(AsyncMutex::new(HashMap::new())),
|
||||
udp_relay_mode: Arc::new(AtomicCell::new(None)),
|
||||
max_external_pkt_size,
|
||||
remote_uni_stream_cnt: Counter::new(),
|
||||
remote_bi_stream_cnt: Counter::new(),
|
||||
max_concurrent_uni_streams: Arc::new(AtomicUsize::new(DEFAULT_CONCURRENT_STREAMS)),
|
||||
max_concurrent_bi_streams: Arc::new(AtomicUsize::new(DEFAULT_CONCURRENT_STREAMS)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn accept(&self) -> Result<(), Error> {
|
||||
tokio::select! {
|
||||
res = self.inner.accept_uni() =>
|
||||
tokio::spawn(self.clone().handle_uni_stream(res?, self.remote_uni_stream_cnt.reg())),
|
||||
res = self.inner.accept_bi() =>
|
||||
tokio::spawn(self.clone().handle_bi_stream(res?, self.remote_bi_stream_cnt.reg())),
|
||||
res = self.inner.read_datagram() =>
|
||||
tokio::spawn(self.clone().handle_datagram(res?)),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_uni_stream(self, recv: RecvStream, _reg: Register) {
|
||||
let max = self.max_concurrent_uni_streams.load(Ordering::Relaxed);
|
||||
|
||||
if self.remote_uni_stream_cnt.count() == max {
|
||||
self.max_concurrent_uni_streams
|
||||
.store(max * 2, Ordering::Relaxed);
|
||||
|
||||
self.inner
|
||||
.set_max_concurrent_uni_streams(VarInt::from((max * 2) as u32));
|
||||
}
|
||||
|
||||
async fn pre_process(conn: &Connection, recv: RecvStream) -> Result<Task, Error> {
|
||||
let task = conn.model.accept_uni_stream(recv).await?;
|
||||
|
||||
if let Task::Authenticate(token) = &task {
|
||||
if conn.is_authed() {
|
||||
return Err(Error::DuplicatedAuth);
|
||||
} else {
|
||||
let mut buf = [0; 32];
|
||||
|
||||
conn.inner
|
||||
.export_keying_material(&mut buf, &conn.token, &conn.token)
|
||||
.map_err(|_| Error::ExportKeyingMaterial)?;
|
||||
|
||||
if token == &buf {
|
||||
conn.set_authed();
|
||||
} else {
|
||||
return Err(Error::AuthFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
() = conn.authed() => {}
|
||||
err = conn.inner.closed() => Err(err)?,
|
||||
};
|
||||
|
||||
let same_pkt_src = matches!(task, Task::Packet(_))
|
||||
&& matches!(conn.get_udp_relay_mode(), Some(UdpRelayMode::Native));
|
||||
if same_pkt_src {
|
||||
return Err(Error::UnexpectedPacketSource);
|
||||
}
|
||||
|
||||
Ok(task)
|
||||
}
|
||||
|
||||
match pre_process(&self, recv).await {
|
||||
Ok(Task::Packet(pkt)) => {
|
||||
self.set_udp_relay_mode(UdpRelayMode::Quic);
|
||||
match self.handle_packet(pkt).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
}
|
||||
Ok(Task::Dissociate(assoc_id)) => match self.handle_dissociate(assoc_id).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
},
|
||||
Ok(_) => unreachable!(),
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
self.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_bi_stream(self, (send, recv): (SendStream, RecvStream), _reg: Register) {
|
||||
let max = self.max_concurrent_bi_streams.load(Ordering::Relaxed);
|
||||
|
||||
if self.remote_bi_stream_cnt.count() == max {
|
||||
self.max_concurrent_bi_streams
|
||||
.store(max * 2, Ordering::Relaxed);
|
||||
|
||||
self.inner
|
||||
.set_max_concurrent_bi_streams(VarInt::from((max * 2) as u32));
|
||||
}
|
||||
|
||||
async fn pre_process(
|
||||
conn: &Connection,
|
||||
send: SendStream,
|
||||
recv: RecvStream,
|
||||
) -> Result<Task, Error> {
|
||||
let task = conn.model.accept_bi_stream(send, recv).await?;
|
||||
|
||||
tokio::select! {
|
||||
() = conn.authed() => {}
|
||||
err = conn.inner.closed() => Err(err)?,
|
||||
};
|
||||
|
||||
Ok(task)
|
||||
}
|
||||
|
||||
match pre_process(&self, send, recv).await {
|
||||
Ok(Task::Connect(conn)) => match self.handle_connect(conn).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
},
|
||||
Ok(_) => unreachable!(),
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
self.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_datagram(self, dg: Bytes) {
|
||||
async fn pre_process(conn: &Connection, dg: Bytes) -> Result<Task, Error> {
|
||||
let task = conn.model.accept_datagram(dg)?;
|
||||
|
||||
tokio::select! {
|
||||
() = conn.authed() => {}
|
||||
err = conn.inner.closed() => Err(err)?,
|
||||
};
|
||||
|
||||
let same_pkt_src = matches!(task, Task::Packet(_))
|
||||
&& matches!(conn.get_udp_relay_mode(), Some(UdpRelayMode::Quic));
|
||||
if same_pkt_src {
|
||||
return Err(Error::UnexpectedPacketSource);
|
||||
}
|
||||
|
||||
Ok(task)
|
||||
}
|
||||
|
||||
match pre_process(&self, dg).await {
|
||||
Ok(Task::Packet(pkt)) => {
|
||||
self.set_udp_relay_mode(UdpRelayMode::Native);
|
||||
match self.handle_packet(pkt).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
}
|
||||
Ok(Task::Heartbeat) => {}
|
||||
Ok(_) => unreachable!(),
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
self.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connect(&self, conn: Connect) -> Result<(), Error> {
|
||||
let mut stream = None;
|
||||
let mut last_err = None;
|
||||
|
||||
match resolve_dns(conn.addr()).await {
|
||||
Ok(addrs) => {
|
||||
for addr in addrs {
|
||||
match TcpStream::connect(addr).await {
|
||||
Ok(s) => {
|
||||
stream = Some(s);
|
||||
break;
|
||||
}
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
|
||||
if let Some(mut stream) = stream {
|
||||
let mut conn = conn.compat();
|
||||
let res = io::copy_bidirectional(&mut conn, &mut stream).await;
|
||||
let _ = conn.shutdown().await;
|
||||
let _ = stream.shutdown().await;
|
||||
res?;
|
||||
Ok(())
|
||||
} else {
|
||||
let _ = conn.compat().shutdown().await;
|
||||
Err(last_err
|
||||
.unwrap_or_else(|| IoError::new(ErrorKind::NotFound, "no address resolved")))?
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_packet(&self, pkt: Packet) -> Result<(), Error> {
|
||||
let Some((pkt, addr, assoc_id)) = pkt.accept().await? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let (socket_v4, socket_v6) = match self.udp_sessions.lock().await.entry(assoc_id) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
let session = entry.get_mut();
|
||||
(session.socket_v4.clone(), session.socket_v6.clone())
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let session = entry
|
||||
.insert(UdpSession::new(assoc_id, self.clone(), self.udp_relay_ipv6).await?);
|
||||
(session.socket_v4.clone(), session.socket_v6.clone())
|
||||
}
|
||||
};
|
||||
|
||||
let Some(socket_addr) = resolve_dns(&addr).await?.next() else {
|
||||
Err(IoError::new(ErrorKind::NotFound, "no address resolved"))?
|
||||
};
|
||||
|
||||
let socket = match socket_addr {
|
||||
SocketAddr::V4(_) => socket_v4,
|
||||
SocketAddr::V6(_) => {
|
||||
socket_v6.ok_or_else(|| Error::UdpRelayIpv6Disabled(addr, socket_addr))?
|
||||
}
|
||||
};
|
||||
|
||||
socket.send_to(&pkt, socket_addr).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_dissociate(&self, assoc_id: u16) -> Result<(), Error> {
|
||||
self.udp_sessions.lock().await.remove(&assoc_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_auth_timeout(self, timeout: Duration) {
|
||||
time::sleep(timeout).await;
|
||||
|
||||
if !self.is_authed() {
|
||||
self.close();
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_garbage(self, gc_interval: Duration, gc_lifetime: Duration) {
|
||||
loop {
|
||||
time::sleep(gc_interval).await;
|
||||
|
||||
if self.is_closed() {
|
||||
break;
|
||||
}
|
||||
|
||||
self.model.collect_garbage(gc_lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_authed(&self) {
|
||||
self.is_authed.set_authed();
|
||||
}
|
||||
|
||||
fn is_authed(&self) -> bool {
|
||||
self.is_authed.is_authed()
|
||||
}
|
||||
|
||||
fn authed(&self) -> IsAuthed {
|
||||
self.is_authed.clone()
|
||||
}
|
||||
|
||||
fn set_udp_relay_mode(&self, mode: UdpRelayMode) {
|
||||
self.udp_relay_mode.store(Some(mode));
|
||||
}
|
||||
|
||||
fn get_udp_relay_mode(&self) -> Option<UdpRelayMode> {
|
||||
self.udp_relay_mode.load()
|
||||
}
|
||||
|
||||
fn is_closed(&self) -> bool {
|
||||
self.inner.close_reason().is_some()
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
self.inner.close(VarInt::from_u32(0), b"");
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_dns(addr: &Address) -> Result<impl Iterator<Item = SocketAddr>, IoError> {
|
||||
match addr {
|
||||
Address::None => Err(IoError::new(ErrorKind::InvalidInput, "empty address")),
|
||||
Address::DomainAddress(domain, port) => Ok(net::lookup_host((domain.as_str(), *port))
|
||||
.await?
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()),
|
||||
Address::SocketAddress(addr) => Ok(vec![*addr].into_iter()),
|
||||
}
|
||||
}
|
||||
|
||||
struct UdpSession {
|
||||
socket_v4: Arc<UdpSocket>,
|
||||
socket_v6: Option<Arc<UdpSocket>>,
|
||||
cancel: Option<Sender<()>>,
|
||||
}
|
||||
|
||||
impl UdpSession {
|
||||
async fn new(assoc_id: u16, conn: Connection, udp_relay_ipv6: bool) -> Result<Self, Error> {
|
||||
let socket_v4 =
|
||||
Arc::new(UdpSocket::bind(SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))).await?);
|
||||
let socket_v6 = if udp_relay_ipv6 {
|
||||
let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
|
||||
socket.set_only_v6(true)?;
|
||||
socket.bind(&SockAddr::from(SocketAddr::from((
|
||||
Ipv6Addr::UNSPECIFIED,
|
||||
0,
|
||||
))))?;
|
||||
Some(Arc::new(UdpSocket::from_std(StdUdpSocket::from(socket))?))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
tokio::spawn(Self::listen_incoming(
|
||||
assoc_id,
|
||||
conn,
|
||||
socket_v4.clone(),
|
||||
socket_v6.clone(),
|
||||
rx,
|
||||
));
|
||||
|
||||
Ok(Self {
|
||||
socket_v4,
|
||||
socket_v6,
|
||||
cancel: Some(tx),
|
||||
})
|
||||
}
|
||||
|
||||
async fn listen_incoming(
|
||||
assoc_id: u16,
|
||||
conn: Connection,
|
||||
socket_v4: Arc<UdpSocket>,
|
||||
socket_v6: Option<Arc<UdpSocket>>,
|
||||
cancel: Receiver<()>,
|
||||
) {
|
||||
async fn send_pkt(conn: Connection, pkt: Bytes, addr: SocketAddr, assoc_id: u16) {
|
||||
let addr = Address::SocketAddress(addr);
|
||||
|
||||
let res = match conn.get_udp_relay_mode() {
|
||||
Some(UdpRelayMode::Native) => conn.model.packet_native(pkt, addr, assoc_id),
|
||||
Some(UdpRelayMode::Quic) => conn.model.packet_quic(pkt, addr, assoc_id).await,
|
||||
None => unreachable!(),
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
eprintln!("{err}");
|
||||
}
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = cancel => {}
|
||||
() = async {
|
||||
loop {
|
||||
match Self::accept(
|
||||
&socket_v4,
|
||||
socket_v6.as_deref(),
|
||||
conn.max_external_pkt_size,
|
||||
).await {
|
||||
Ok((pkt, addr)) => {
|
||||
tokio::spawn(send_pkt(conn.clone(), pkt, addr, assoc_id));
|
||||
}
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
}
|
||||
} => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn accept(
|
||||
socket_v4: &UdpSocket,
|
||||
socket_v6: Option<&UdpSocket>,
|
||||
max_pkt_size: usize,
|
||||
) -> Result<(Bytes, SocketAddr), IoError> {
|
||||
async fn read_pkt(
|
||||
socket: &UdpSocket,
|
||||
max_pkt_size: usize,
|
||||
) -> Result<(Bytes, SocketAddr), IoError> {
|
||||
let mut buf = vec![0u8; max_pkt_size];
|
||||
let (n, addr) = socket.recv_from(&mut buf).await?;
|
||||
buf.truncate(n);
|
||||
Ok((Bytes::from(buf), addr))
|
||||
}
|
||||
|
||||
if let Some(socket_v6) = socket_v6 {
|
||||
tokio::select! {
|
||||
res = read_pkt(socket_v4, max_pkt_size) => res,
|
||||
res = read_pkt(socket_v6, max_pkt_size) => res,
|
||||
}
|
||||
} else {
|
||||
read_pkt(socket_v4, max_pkt_size).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for UdpSession {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.cancel.take().unwrap().send(());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct IsAuthed {
|
||||
is_authed: Arc<AtomicBool>,
|
||||
broadcast: Arc<Mutex<Vec<Waker>>>,
|
||||
}
|
||||
|
||||
impl IsAuthed {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
is_authed: Arc::new(AtomicBool::new(false)),
|
||||
broadcast: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_authed(&self) {
|
||||
self.is_authed.store(true, Ordering::Release);
|
||||
|
||||
for waker in self.broadcast.lock().drain(..) {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
fn is_authed(&self) -> bool {
|
||||
self.is_authed.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for IsAuthed {
|
||||
type Output = ();
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if self.is_authed.load(Ordering::Relaxed) {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
self.broadcast.lock().push(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
69
tuic-server/src/utils.rs
Normal file
69
tuic-server/src/utils.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use rustls::{Certificate, PrivateKey};
|
||||
use rustls_pemfile::Item;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{BufReader, Error as IoError},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
pub fn load_certs(path: PathBuf) -> Result<Vec<Certificate>, IoError> {
|
||||
let mut file = BufReader::new(File::open(&path)?);
|
||||
let mut certs = Vec::new();
|
||||
|
||||
while let Ok(Some(item)) = rustls_pemfile::read_one(&mut file) {
|
||||
if let Item::X509Certificate(cert) = item {
|
||||
certs.push(Certificate(cert));
|
||||
}
|
||||
}
|
||||
|
||||
if certs.is_empty() {
|
||||
certs = vec![Certificate(fs::read(&path)?)];
|
||||
}
|
||||
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
pub fn load_priv_key(path: PathBuf) -> Result<PrivateKey, IoError> {
|
||||
let mut file = BufReader::new(File::open(&path)?);
|
||||
let mut priv_key = None;
|
||||
|
||||
while let Ok(Some(item)) = rustls_pemfile::read_one(&mut file) {
|
||||
if let Item::RSAKey(key) | Item::PKCS8Key(key) | Item::ECKey(key) = item {
|
||||
priv_key = Some(key);
|
||||
}
|
||||
}
|
||||
|
||||
priv_key
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| fs::read(&path))
|
||||
.map(PrivateKey)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum UdpRelayMode {
|
||||
Native,
|
||||
Quic,
|
||||
}
|
||||
|
||||
pub enum CongestionControl {
|
||||
Cubic,
|
||||
NewReno,
|
||||
Bbr,
|
||||
}
|
||||
|
||||
impl FromStr for CongestionControl {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.eq_ignore_ascii_case("cubic") {
|
||||
Ok(Self::Cubic)
|
||||
} else if s.eq_ignore_ascii_case("new_reno") || s.eq_ignore_ascii_case("newreno") {
|
||||
Ok(Self::NewReno)
|
||||
} else if s.eq_ignore_ascii_case("bbr") {
|
||||
Ok(Self::Bbr)
|
||||
} else {
|
||||
Err("invalid congestion control")
|
||||
}
|
||||
}
|
||||
}
|
19
tuic/Cargo.toml
Normal file
19
tuic/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "tuic"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
async_marshal = ["bytes", "futures-util"]
|
||||
marshal = ["bytes"]
|
||||
model = ["parking_lot", "register-count", "thiserror"]
|
||||
|
||||
[dependencies]
|
||||
bytes = { version = "1.4.0", default-features = false, features = ["std"], optional = true }
|
||||
futures-util = { version = "0.3.26", default-features = false, features = ["io", "std"], 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 }
|
||||
thiserror = { version = "1.0.38", default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tuic = { path = ".", features = ["async_marshal", "marshal", "model"] }
|
19
tuic/src/lib.rs
Normal file
19
tuic/src/lib.rs
Normal file
@ -0,0 +1,19 @@
|
||||
//! The TUIC protocol
|
||||
|
||||
mod protocol;
|
||||
|
||||
pub use self::protocol::{
|
||||
Address, Authenticate, Connect, Dissociate, Header, Heartbeat, Packet, VERSION,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "async_marshal", feature = "marshal"))]
|
||||
mod marshal;
|
||||
|
||||
#[cfg(any(feature = "async_marshal", feature = "marshal"))]
|
||||
mod unmarshal;
|
||||
|
||||
#[cfg(any(feature = "async_marshal", feature = "marshal"))]
|
||||
pub use self::unmarshal::UnmarshalError;
|
||||
|
||||
#[cfg(feature = "model")]
|
||||
pub mod model;
|
96
tuic/src/marshal.rs
Normal file
96
tuic/src/marshal.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use crate::protocol::{
|
||||
Address, Authenticate, Connect, Dissociate, Header, Heartbeat, Packet, VERSION,
|
||||
};
|
||||
use bytes::BufMut;
|
||||
use futures_util::{AsyncWrite, AsyncWriteExt};
|
||||
use std::{
|
||||
io::{Error as IoError, Write},
|
||||
net::SocketAddr,
|
||||
};
|
||||
|
||||
impl Header {
|
||||
#[cfg(feature = "async_marshal")]
|
||||
pub async fn async_marshal(&self, s: &mut (impl AsyncWrite + Unpin)) -> Result<(), IoError> {
|
||||
let mut buf = vec![0; self.len()];
|
||||
self.write(&mut buf);
|
||||
s.write_all(&buf).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "marshal")]
|
||||
pub fn marshal(&self, s: &mut impl Write) -> Result<(), IoError> {
|
||||
let mut buf = vec![0; self.len()];
|
||||
self.write(&mut buf);
|
||||
s.write_all(&buf)
|
||||
}
|
||||
|
||||
pub fn write(&self, buf: &mut impl BufMut) {
|
||||
buf.put_u8(VERSION);
|
||||
buf.put_u8(self.type_code());
|
||||
|
||||
match self {
|
||||
Self::Authenticate(auth) => auth.write(buf),
|
||||
Self::Connect(conn) => conn.write(buf),
|
||||
Self::Packet(packet) => packet.write(buf),
|
||||
Self::Dissociate(dissociate) => dissociate.write(buf),
|
||||
Self::Heartbeat(heartbeat) => heartbeat.write(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Address {
|
||||
fn write(&self, buf: &mut impl BufMut) {
|
||||
buf.put_u8(self.type_code());
|
||||
|
||||
match self {
|
||||
Self::None => {}
|
||||
Self::DomainAddress(domain, port) => {
|
||||
buf.put_u8(domain.len() as u8);
|
||||
buf.put_slice(domain.as_bytes());
|
||||
buf.put_u16(*port);
|
||||
}
|
||||
Self::SocketAddress(SocketAddr::V4(addr)) => {
|
||||
buf.put_slice(&addr.ip().octets());
|
||||
buf.put_u16(addr.port());
|
||||
}
|
||||
Self::SocketAddress(SocketAddr::V6(addr)) => {
|
||||
for seg in addr.ip().segments() {
|
||||
buf.put_u16(seg);
|
||||
}
|
||||
buf.put_u16(addr.port());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Authenticate {
|
||||
fn write(&self, buf: &mut impl BufMut) {
|
||||
buf.put_slice(&self.token());
|
||||
}
|
||||
}
|
||||
|
||||
impl Connect {
|
||||
fn write(&self, buf: &mut impl BufMut) {
|
||||
self.addr().write(buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
fn write(&self, buf: &mut impl BufMut) {
|
||||
buf.put_u16(self.assoc_id());
|
||||
buf.put_u16(self.pkt_id());
|
||||
buf.put_u8(self.frag_total());
|
||||
buf.put_u8(self.frag_id());
|
||||
buf.put_u16(self.size());
|
||||
self.addr().write(buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Dissociate {
|
||||
fn write(&self, buf: &mut impl BufMut) {
|
||||
buf.put_u16(self.assoc_id());
|
||||
}
|
||||
}
|
||||
|
||||
impl Heartbeat {
|
||||
fn write(&self, _buf: &mut impl BufMut) {}
|
||||
}
|
45
tuic/src/model/authenticate.rs
Normal file
45
tuic/src/model/authenticate.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use super::side::{self, Side};
|
||||
use crate::protocol::{Authenticate as AuthenticateHeader, Header};
|
||||
|
||||
pub struct Authenticate<M> {
|
||||
inner: Side<Tx, Rx>,
|
||||
_marker: M,
|
||||
}
|
||||
|
||||
pub struct Tx {
|
||||
header: Header,
|
||||
}
|
||||
|
||||
impl Authenticate<side::Tx> {
|
||||
pub(super) fn new(token: [u8; 32]) -> Self {
|
||||
Self {
|
||||
inner: Side::Tx(Tx {
|
||||
header: Header::Authenticate(AuthenticateHeader::new(token)),
|
||||
}),
|
||||
_marker: side::Tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(&self) -> &Header {
|
||||
let Side::Tx(tx) = &self.inner else { unreachable!() };
|
||||
&tx.header
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Rx {
|
||||
token: [u8; 32],
|
||||
}
|
||||
|
||||
impl Authenticate<side::Rx> {
|
||||
pub(super) fn new(token: [u8; 32]) -> Self {
|
||||
Self {
|
||||
inner: Side::Rx(Rx { token }),
|
||||
_marker: side::Rx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token(&self) -> [u8; 32] {
|
||||
let Side::Rx(rx) = &self.inner else { unreachable!() };
|
||||
rx.token
|
||||
}
|
||||
}
|
52
tuic/src/model/connect.rs
Normal file
52
tuic/src/model/connect.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use super::side::{self, Side};
|
||||
use crate::protocol::{Address, Connect as ConnectHeader, Header};
|
||||
use register_count::Register;
|
||||
|
||||
pub struct Connect<M> {
|
||||
inner: Side<Tx, Rx>,
|
||||
_marker: M,
|
||||
}
|
||||
|
||||
struct Tx {
|
||||
header: Header,
|
||||
_task_reg: Register,
|
||||
}
|
||||
|
||||
impl Connect<side::Tx> {
|
||||
pub(super) fn new(task_reg: Register, addr: Address) -> Self {
|
||||
Self {
|
||||
inner: Side::Tx(Tx {
|
||||
header: Header::Connect(ConnectHeader::new(addr)),
|
||||
_task_reg: task_reg,
|
||||
}),
|
||||
_marker: side::Tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(&self) -> &Header {
|
||||
let Side::Tx(tx) = &self.inner else { unreachable!() };
|
||||
&tx.header
|
||||
}
|
||||
}
|
||||
|
||||
struct Rx {
|
||||
addr: Address,
|
||||
_task_reg: Register,
|
||||
}
|
||||
|
||||
impl Connect<side::Rx> {
|
||||
pub(super) fn new(task_reg: Register, addr: Address) -> Self {
|
||||
Self {
|
||||
inner: Side::Rx(Rx {
|
||||
addr,
|
||||
_task_reg: task_reg,
|
||||
}),
|
||||
_marker: side::Rx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn addr(&self) -> &Address {
|
||||
let Side::Rx(rx) = &self.inner else { unreachable!() };
|
||||
&rx.addr
|
||||
}
|
||||
}
|
45
tuic/src/model/dissociate.rs
Normal file
45
tuic/src/model/dissociate.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use super::side::{self, Side};
|
||||
use crate::protocol::{Dissociate as DissociateHeader, Header};
|
||||
|
||||
pub struct Dissociate<M> {
|
||||
inner: Side<Tx, Rx>,
|
||||
_marker: M,
|
||||
}
|
||||
|
||||
pub struct Tx {
|
||||
header: Header,
|
||||
}
|
||||
|
||||
impl Dissociate<side::Tx> {
|
||||
pub(super) fn new(assoc_id: u16) -> Self {
|
||||
Self {
|
||||
inner: Side::Tx(Tx {
|
||||
header: Header::Dissociate(DissociateHeader::new(assoc_id)),
|
||||
}),
|
||||
_marker: side::Tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(&self) -> &Header {
|
||||
let Side::Tx(tx) = &self.inner else { unreachable!() };
|
||||
&tx.header
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Rx {
|
||||
assoc_id: u16,
|
||||
}
|
||||
|
||||
impl Dissociate<side::Rx> {
|
||||
pub(super) fn new(assoc_id: u16) -> Self {
|
||||
Self {
|
||||
inner: Side::Rx(Rx { assoc_id }),
|
||||
_marker: side::Rx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assoc_id(&self) -> u16 {
|
||||
let Side::Rx(rx) = &self.inner else { unreachable!() };
|
||||
rx.assoc_id
|
||||
}
|
||||
}
|
38
tuic/src/model/heartbeat.rs
Normal file
38
tuic/src/model/heartbeat.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use super::side::{self, Side};
|
||||
use crate::protocol::{Header, Heartbeat as HeartbeatHeader};
|
||||
|
||||
pub struct Heartbeat<M> {
|
||||
inner: Side<Tx, Rx>,
|
||||
_marker: M,
|
||||
}
|
||||
|
||||
pub struct Tx {
|
||||
header: Header,
|
||||
}
|
||||
|
||||
impl Heartbeat<side::Tx> {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
inner: Side::Tx(Tx {
|
||||
header: Header::Heartbeat(HeartbeatHeader::new()),
|
||||
}),
|
||||
_marker: side::Tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(&self) -> &Header {
|
||||
let Side::Tx(tx) = &self.inner else { unreachable!() };
|
||||
&tx.header
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Rx;
|
||||
|
||||
impl Heartbeat<side::Rx> {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
inner: Side::Rx(Rx),
|
||||
_marker: side::Rx,
|
||||
}
|
||||
}
|
||||
}
|
450
tuic/src/model/mod.rs
Normal file
450
tuic/src/model/mod.rs
Normal file
@ -0,0 +1,450 @@
|
||||
use crate::protocol::{
|
||||
Address, Authenticate as AuthenticateHeader, Connect as ConnectHeader,
|
||||
Dissociate as DissociateHeader, Heartbeat as HeartbeatHeader, Packet as PacketHeader,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use register_count::{Counter, Register};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
mem,
|
||||
sync::{
|
||||
atomic::{AtomicU16, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
mod authenticate;
|
||||
mod connect;
|
||||
mod dissociate;
|
||||
mod heartbeat;
|
||||
mod packet;
|
||||
|
||||
pub use self::{
|
||||
authenticate::Authenticate,
|
||||
connect::Connect,
|
||||
dissociate::Dissociate,
|
||||
heartbeat::Heartbeat,
|
||||
packet::{Fragments, Packet},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Connection<B> {
|
||||
udp_sessions: Arc<Mutex<UdpSessions<B>>>,
|
||||
task_connect_count: Counter,
|
||||
task_associate_count: Counter,
|
||||
}
|
||||
|
||||
impl<B> Connection<B>
|
||||
where
|
||||
B: AsRef<[u8]>,
|
||||
{
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
let task_associate_count = Counter::new();
|
||||
|
||||
Self {
|
||||
udp_sessions: Arc::new(Mutex::new(UdpSessions::new(task_associate_count.clone()))),
|
||||
task_connect_count: Counter::new(),
|
||||
task_associate_count,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_authenticate(&self, token: [u8; 32]) -> Authenticate<side::Tx> {
|
||||
Authenticate::<side::Tx>::new(token)
|
||||
}
|
||||
|
||||
pub fn recv_authenticate(&self, header: AuthenticateHeader) -> Authenticate<side::Rx> {
|
||||
let (token,) = header.into();
|
||||
Authenticate::<side::Rx>::new(token)
|
||||
}
|
||||
|
||||
pub fn send_connect(&self, addr: Address) -> Connect<side::Tx> {
|
||||
Connect::<side::Tx>::new(self.task_connect_count.reg(), addr)
|
||||
}
|
||||
|
||||
pub fn recv_connect(&self, header: ConnectHeader) -> Connect<side::Rx> {
|
||||
let (addr,) = header.into();
|
||||
Connect::<side::Rx>::new(self.task_connect_count.reg(), addr)
|
||||
}
|
||||
|
||||
pub fn send_packet(
|
||||
&self,
|
||||
assoc_id: u16,
|
||||
addr: Address,
|
||||
max_pkt_size: usize,
|
||||
) -> Packet<side::Tx, B> {
|
||||
self.udp_sessions
|
||||
.lock()
|
||||
.send_packet(assoc_id, addr, max_pkt_size)
|
||||
}
|
||||
|
||||
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();
|
||||
self.udp_sessions.lock().recv_packet(
|
||||
self.udp_sessions.clone(),
|
||||
assoc_id,
|
||||
pkt_id,
|
||||
frag_total,
|
||||
frag_id,
|
||||
size,
|
||||
addr,
|
||||
)
|
||||
}
|
||||
|
||||
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();
|
||||
self.udp_sessions.lock().recv_packet_unrestricted(
|
||||
self.udp_sessions.clone(),
|
||||
assoc_id,
|
||||
pkt_id,
|
||||
frag_total,
|
||||
frag_id,
|
||||
size,
|
||||
addr,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn send_dissociate(&self, assoc_id: u16) -> Dissociate<side::Tx> {
|
||||
self.udp_sessions.lock().send_dissociate(assoc_id)
|
||||
}
|
||||
|
||||
pub fn recv_dissociate(&self, header: DissociateHeader) -> Dissociate<side::Rx> {
|
||||
let (assoc_id,) = header.into();
|
||||
self.udp_sessions.lock().recv_dissociate(assoc_id)
|
||||
}
|
||||
|
||||
pub fn send_heartbeat(&self) -> Heartbeat<side::Tx> {
|
||||
Heartbeat::<side::Tx>::new()
|
||||
}
|
||||
|
||||
pub fn recv_heartbeat(&self, header: HeartbeatHeader) -> Heartbeat<side::Rx> {
|
||||
let () = header.into();
|
||||
Heartbeat::<side::Rx>::new()
|
||||
}
|
||||
|
||||
pub fn task_connect_count(&self) -> usize {
|
||||
self.task_connect_count.count()
|
||||
}
|
||||
|
||||
pub fn task_associate_count(&self) -> usize {
|
||||
self.task_associate_count.count()
|
||||
}
|
||||
|
||||
pub fn collect_garbage(&self, timeout: Duration) {
|
||||
self.udp_sessions.lock().collect_garbage(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
pub mod side {
|
||||
pub struct Tx;
|
||||
pub struct Rx;
|
||||
|
||||
pub(super) enum Side<T, R> {
|
||||
Tx(T),
|
||||
Rx(R),
|
||||
}
|
||||
}
|
||||
|
||||
struct UdpSessions<B> {
|
||||
sessions: HashMap<u16, UdpSession<B>>,
|
||||
task_associate_count: Counter,
|
||||
}
|
||||
|
||||
impl<B> UdpSessions<B>
|
||||
where
|
||||
B: AsRef<[u8]>,
|
||||
{
|
||||
fn new(task_associate_count: Counter) -> Self {
|
||||
Self {
|
||||
sessions: HashMap::new(),
|
||||
task_associate_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_packet(
|
||||
&mut self,
|
||||
assoc_id: u16,
|
||||
addr: Address,
|
||||
max_pkt_size: usize,
|
||||
) -> Packet<side::Tx, B> {
|
||||
self.sessions
|
||||
.entry(assoc_id)
|
||||
.or_insert_with(|| UdpSession::new(self.task_associate_count.reg()))
|
||||
.send_packet(assoc_id, addr, max_pkt_size)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn recv_packet(
|
||||
&mut self,
|
||||
sessions: Arc<Mutex<Self>>,
|
||||
assoc_id: u16,
|
||||
pkt_id: u16,
|
||||
frag_total: u8,
|
||||
frag_id: u8,
|
||||
size: u16,
|
||||
addr: Address,
|
||||
) -> Option<Packet<side::Rx, B>> {
|
||||
self.sessions.get_mut(&assoc_id).map(|session| {
|
||||
session.recv_packet(sessions, assoc_id, pkt_id, frag_total, frag_id, size, addr)
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn recv_packet_unrestricted(
|
||||
&mut self,
|
||||
sessions: Arc<Mutex<Self>>,
|
||||
assoc_id: u16,
|
||||
pkt_id: u16,
|
||||
frag_total: u8,
|
||||
frag_id: u8,
|
||||
size: u16,
|
||||
addr: Address,
|
||||
) -> Packet<side::Rx, B> {
|
||||
self.sessions
|
||||
.entry(assoc_id)
|
||||
.or_insert_with(|| UdpSession::new(self.task_associate_count.reg()))
|
||||
.recv_packet(sessions, assoc_id, pkt_id, frag_total, frag_id, size, addr)
|
||||
}
|
||||
|
||||
fn send_dissociate(&mut self, assoc_id: u16) -> Dissociate<side::Tx> {
|
||||
self.sessions.remove(&assoc_id);
|
||||
Dissociate::<side::Tx>::new(assoc_id)
|
||||
}
|
||||
|
||||
fn recv_dissociate(&mut self, assoc_id: u16) -> Dissociate<side::Rx> {
|
||||
self.sessions.remove(&assoc_id);
|
||||
Dissociate::<side::Rx>::new(assoc_id)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn insert(
|
||||
&mut self,
|
||||
assoc_id: u16,
|
||||
pkt_id: u16,
|
||||
frag_total: u8,
|
||||
frag_id: u8,
|
||||
size: u16,
|
||||
addr: Address,
|
||||
data: B,
|
||||
) -> Result<Option<Assemblable<B>>, AssembleError> {
|
||||
self.sessions
|
||||
.entry(assoc_id)
|
||||
.or_insert_with(|| UdpSession::new(self.task_associate_count.reg()))
|
||||
.insert(assoc_id, pkt_id, frag_total, frag_id, size, addr, data)
|
||||
}
|
||||
|
||||
fn collect_garbage(&mut self, timeout: Duration) {
|
||||
for (_, session) in self.sessions.iter_mut() {
|
||||
session.collect_garbage(timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UdpSession<B> {
|
||||
pkt_buf: HashMap<u16, PacketBuffer<B>>,
|
||||
next_pkt_id: AtomicU16,
|
||||
_task_reg: Register,
|
||||
}
|
||||
|
||||
impl<B> UdpSession<B>
|
||||
where
|
||||
B: AsRef<[u8]>,
|
||||
{
|
||||
fn new(task_reg: Register) -> Self {
|
||||
Self {
|
||||
pkt_buf: HashMap::new(),
|
||||
next_pkt_id: AtomicU16::new(0),
|
||||
_task_reg: task_reg,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_packet(
|
||||
&self,
|
||||
assoc_id: u16,
|
||||
addr: Address,
|
||||
max_pkt_size: usize,
|
||||
) -> Packet<side::Tx, B> {
|
||||
Packet::<side::Tx, B>::new(
|
||||
assoc_id,
|
||||
self.next_pkt_id.fetch_add(1, Ordering::AcqRel),
|
||||
addr,
|
||||
max_pkt_size,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn recv_packet(
|
||||
&self,
|
||||
sessions: Arc<Mutex<UdpSessions<B>>>,
|
||||
assoc_id: u16,
|
||||
pkt_id: u16,
|
||||
frag_total: u8,
|
||||
frag_id: u8,
|
||||
size: u16,
|
||||
addr: Address,
|
||||
) -> Packet<side::Rx, B> {
|
||||
Packet::<side::Rx, B>::new(sessions, assoc_id, pkt_id, frag_total, frag_id, size, addr)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn insert(
|
||||
&mut self,
|
||||
assoc_id: u16,
|
||||
pkt_id: u16,
|
||||
frag_total: u8,
|
||||
frag_id: u8,
|
||||
size: u16,
|
||||
addr: Address,
|
||||
data: B,
|
||||
) -> Result<Option<Assemblable<B>>, AssembleError> {
|
||||
let res = self
|
||||
.pkt_buf
|
||||
.entry(pkt_id)
|
||||
.or_insert_with(|| PacketBuffer::new(frag_total))
|
||||
.insert(assoc_id, frag_total, frag_id, size, addr, data)?;
|
||||
|
||||
if res.is_some() {
|
||||
self.pkt_buf.remove(&pkt_id);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn collect_garbage(&mut self, timeout: Duration) {
|
||||
self.pkt_buf.retain(|_, buf| buf.c_time.elapsed() < timeout);
|
||||
}
|
||||
}
|
||||
|
||||
struct PacketBuffer<B> {
|
||||
buf: Vec<Option<B>>,
|
||||
frag_total: u8,
|
||||
frag_received: u8,
|
||||
addr: Address,
|
||||
c_time: Instant,
|
||||
}
|
||||
|
||||
impl<B> PacketBuffer<B>
|
||||
where
|
||||
B: AsRef<[u8]>,
|
||||
{
|
||||
fn new(frag_total: u8) -> Self {
|
||||
let mut buf = Vec::with_capacity(frag_total as usize);
|
||||
buf.resize_with(frag_total as usize, || None);
|
||||
|
||||
Self {
|
||||
buf,
|
||||
frag_total,
|
||||
frag_received: 0,
|
||||
addr: Address::None,
|
||||
c_time: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn insert(
|
||||
&mut self,
|
||||
assoc_id: u16,
|
||||
frag_total: u8,
|
||||
frag_id: u8,
|
||||
size: u16,
|
||||
addr: Address,
|
||||
data: B,
|
||||
) -> Result<Option<Assemblable<B>>, AssembleError> {
|
||||
assert_eq!(data.as_ref().len(), size as usize);
|
||||
|
||||
if frag_id >= frag_total {
|
||||
return Err(AssembleError::InvalidFragmentId(frag_total, frag_id));
|
||||
}
|
||||
|
||||
if frag_id == 0 && addr.is_none() {
|
||||
return Err(AssembleError::InvalidAddress(
|
||||
"no address in first fragment",
|
||||
));
|
||||
}
|
||||
|
||||
if frag_id != 0 && !addr.is_none() {
|
||||
return Err(AssembleError::InvalidAddress(
|
||||
"address in non-first fragment",
|
||||
));
|
||||
}
|
||||
|
||||
if self.buf[frag_id as usize].is_some() {
|
||||
return Err(AssembleError::DuplicatedFragment(frag_id));
|
||||
}
|
||||
|
||||
self.buf[frag_id as usize] = Some(data);
|
||||
self.frag_received += 1;
|
||||
|
||||
if frag_id == 0 {
|
||||
self.addr = addr;
|
||||
}
|
||||
|
||||
if self.frag_received == self.frag_total {
|
||||
Ok(Some(Assemblable::new(
|
||||
mem::take(&mut self.buf),
|
||||
self.addr.take(),
|
||||
assoc_id,
|
||||
)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Assemblable<B> {
|
||||
buf: Vec<Option<B>>,
|
||||
addr: Address,
|
||||
assoc_id: u16,
|
||||
}
|
||||
|
||||
impl<B> Assemblable<B>
|
||||
where
|
||||
B: AsRef<[u8]>,
|
||||
{
|
||||
fn new(buf: Vec<Option<B>>, addr: Address, assoc_id: u16) -> Self {
|
||||
Self {
|
||||
buf,
|
||||
addr,
|
||||
assoc_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assemble<A>(self, buf: &mut A) -> (Address, u16)
|
||||
where
|
||||
A: Assembler<B>,
|
||||
{
|
||||
let data = self.buf.into_iter().map(|b| b.unwrap());
|
||||
buf.assemble(data);
|
||||
(self.addr, self.assoc_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Assembler<B>
|
||||
where
|
||||
Self: Sized,
|
||||
B: AsRef<[u8]>,
|
||||
{
|
||||
fn assemble(&mut self, data: impl IntoIterator<Item = B>);
|
||||
}
|
||||
|
||||
impl<B> Assembler<B> for Vec<u8>
|
||||
where
|
||||
B: AsRef<[u8]>,
|
||||
{
|
||||
fn assemble(&mut self, data: impl IntoIterator<Item = B>) {
|
||||
for d in data {
|
||||
self.extend_from_slice(d.as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AssembleError {
|
||||
#[error("invalid fragment id {1} in total {0} fragments")]
|
||||
InvalidFragmentId(u8, u8),
|
||||
#[error("{0}")]
|
||||
InvalidAddress(&'static str),
|
||||
#[error("duplicated fragment: {0}")]
|
||||
DuplicatedFragment(u8),
|
||||
}
|
208
tuic/src/model/packet.rs
Normal file
208
tuic/src/model/packet.rs
Normal file
@ -0,0 +1,208 @@
|
||||
use super::{
|
||||
side::{self, Side},
|
||||
Assemblable, AssembleError, UdpSessions,
|
||||
};
|
||||
use crate::protocol::{Address, Header, Packet as PacketHeader};
|
||||
use parking_lot::Mutex;
|
||||
use std::{marker::PhantomData, slice, sync::Arc};
|
||||
|
||||
pub struct Packet<M, B> {
|
||||
inner: Side<Tx, Rx<B>>,
|
||||
_marker: M,
|
||||
}
|
||||
|
||||
pub struct Tx {
|
||||
assoc_id: u16,
|
||||
pkt_id: u16,
|
||||
addr: Address,
|
||||
max_pkt_size: usize,
|
||||
}
|
||||
|
||||
impl<B> Packet<side::Tx, B> {
|
||||
pub(super) fn new(assoc_id: u16, pkt_id: u16, addr: Address, max_pkt_size: usize) -> Self {
|
||||
Self {
|
||||
inner: Side::Tx(Tx {
|
||||
assoc_id,
|
||||
pkt_id,
|
||||
addr,
|
||||
max_pkt_size,
|
||||
}),
|
||||
_marker: side::Tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_fragments<'a, P>(self, payload: P) -> Fragments<'a, P>
|
||||
where
|
||||
P: AsRef<[u8]>,
|
||||
{
|
||||
let Side::Tx(tx) = self.inner else { unreachable!() };
|
||||
Fragments::new(tx.assoc_id, tx.pkt_id, tx.addr, tx.max_pkt_size, payload)
|
||||
}
|
||||
|
||||
pub fn assoc_id(&self) -> u16 {
|
||||
let Side::Tx(tx) = &self.inner else { unreachable!() };
|
||||
tx.assoc_id
|
||||
}
|
||||
|
||||
pub fn addr(&self) -> &Address {
|
||||
let Side::Tx(tx) = &self.inner else { unreachable!() };
|
||||
&tx.addr
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Rx<B> {
|
||||
sessions: Arc<Mutex<UdpSessions<B>>>,
|
||||
assoc_id: u16,
|
||||
pkt_id: u16,
|
||||
frag_total: u8,
|
||||
frag_id: u8,
|
||||
size: u16,
|
||||
addr: Address,
|
||||
}
|
||||
|
||||
impl<B> Packet<side::Rx, B>
|
||||
where
|
||||
B: AsRef<[u8]>,
|
||||
{
|
||||
pub(super) fn new(
|
||||
sessions: Arc<Mutex<UdpSessions<B>>>,
|
||||
assoc_id: u16,
|
||||
pkt_id: u16,
|
||||
frag_total: u8,
|
||||
frag_id: u8,
|
||||
size: u16,
|
||||
addr: Address,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: Side::Rx(Rx {
|
||||
sessions,
|
||||
assoc_id,
|
||||
pkt_id,
|
||||
frag_total,
|
||||
frag_id,
|
||||
size,
|
||||
addr,
|
||||
}),
|
||||
_marker: side::Rx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assemble(self, data: B) -> Result<Option<Assemblable<B>>, AssembleError> {
|
||||
let Side::Rx(rx) = self.inner else { unreachable!() };
|
||||
let mut sessions = rx.sessions.lock();
|
||||
|
||||
sessions.insert(
|
||||
rx.assoc_id,
|
||||
rx.pkt_id,
|
||||
rx.frag_total,
|
||||
rx.frag_id,
|
||||
rx.size,
|
||||
rx.addr,
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn assoc_id(&self) -> u16 {
|
||||
let Side::Rx(rx) = &self.inner else { unreachable!() };
|
||||
rx.assoc_id
|
||||
}
|
||||
|
||||
pub fn addr(&self) -> &Address {
|
||||
let Side::Rx(rx) = &self.inner else { unreachable!() };
|
||||
&rx.addr
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u16 {
|
||||
let Side::Rx(rx) = &self.inner else { unreachable!() };
|
||||
rx.size
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Fragments<'a, P>
|
||||
where
|
||||
P: 'a,
|
||||
{
|
||||
assoc_id: u16,
|
||||
pkt_id: u16,
|
||||
addr: Address,
|
||||
max_pkt_size: usize,
|
||||
frag_total: u8,
|
||||
next_frag_id: u8,
|
||||
next_frag_start: usize,
|
||||
payload: P,
|
||||
_marker: PhantomData<&'a P>,
|
||||
}
|
||||
|
||||
impl<'a, P> Fragments<'a, P>
|
||||
where
|
||||
P: AsRef<[u8]> + 'a,
|
||||
{
|
||||
fn new(assoc_id: u16, pkt_id: u16, addr: Address, max_pkt_size: usize, payload: P) -> Self {
|
||||
let first_frag_size = max_pkt_size - PacketHeader::len_without_addr() - addr.len();
|
||||
let frag_size_addr_none =
|
||||
max_pkt_size - PacketHeader::len_without_addr() - Address::None.len();
|
||||
|
||||
let frag_total = if first_frag_size < payload.as_ref().len() {
|
||||
(1 + (payload.as_ref().len() - first_frag_size) / frag_size_addr_none + 1) as u8
|
||||
} else {
|
||||
1u8
|
||||
};
|
||||
|
||||
Self {
|
||||
assoc_id,
|
||||
pkt_id,
|
||||
addr,
|
||||
max_pkt_size,
|
||||
frag_total,
|
||||
next_frag_id: 0,
|
||||
next_frag_start: 0,
|
||||
payload,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, P> Iterator for Fragments<'a, P>
|
||||
where
|
||||
P: AsRef<[u8]> + 'a,
|
||||
{
|
||||
type Item = (Header, &'a [u8]);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.next_frag_id < self.frag_total {
|
||||
let payload_size =
|
||||
self.max_pkt_size - PacketHeader::len_without_addr() - self.addr.len();
|
||||
let next_frag_end =
|
||||
(self.next_frag_start + payload_size).min(self.payload.as_ref().len());
|
||||
|
||||
let header = Header::Packet(PacketHeader::new(
|
||||
self.assoc_id,
|
||||
self.pkt_id,
|
||||
self.frag_total,
|
||||
self.next_frag_id,
|
||||
(next_frag_end - self.next_frag_start) as u16,
|
||||
self.addr.take(),
|
||||
));
|
||||
|
||||
let payload_ptr = &(self.payload.as_ref()[self.next_frag_start]) as *const u8;
|
||||
let payload =
|
||||
unsafe { slice::from_raw_parts(payload_ptr, next_frag_end - self.next_frag_start) };
|
||||
|
||||
self.next_frag_id += 1;
|
||||
self.next_frag_start = next_frag_end;
|
||||
|
||||
Some((header, payload))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> ExactSizeIterator for Fragments<'_, P>
|
||||
where
|
||||
P: AsRef<[u8]>,
|
||||
{
|
||||
fn len(&self) -> usize {
|
||||
self.frag_total as usize
|
||||
}
|
||||
}
|
36
tuic/src/protocol/authenticate.rs
Normal file
36
tuic/src/protocol/authenticate.rs
Normal file
@ -0,0 +1,36 @@
|
||||
// +-------+
|
||||
// | TOKEN |
|
||||
// +-------+
|
||||
// | 32 |
|
||||
// +-------+
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Authenticate {
|
||||
token: [u8; 32],
|
||||
}
|
||||
|
||||
impl Authenticate {
|
||||
const TYPE_CODE: u8 = 0x00;
|
||||
|
||||
pub const fn new(token: [u8; 32]) -> Self {
|
||||
Self { token }
|
||||
}
|
||||
|
||||
pub fn token(&self) -> [u8; 32] {
|
||||
self.token
|
||||
}
|
||||
|
||||
pub const fn type_code() -> u8 {
|
||||
Self::TYPE_CODE
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
32
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Authenticate> for ([u8; 32],) {
|
||||
fn from(auth: Authenticate) -> Self {
|
||||
(auth.token,)
|
||||
}
|
||||
}
|
38
tuic/src/protocol/connect.rs
Normal file
38
tuic/src/protocol/connect.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use super::Address;
|
||||
|
||||
// +----------+
|
||||
// | ADDR |
|
||||
// +----------+
|
||||
// | Variable |
|
||||
// +----------+
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Connect {
|
||||
addr: Address,
|
||||
}
|
||||
|
||||
impl Connect {
|
||||
const TYPE_CODE: u8 = 0x01;
|
||||
|
||||
pub const fn new(addr: Address) -> Self {
|
||||
Self { addr }
|
||||
}
|
||||
|
||||
pub fn addr(&self) -> &Address {
|
||||
&self.addr
|
||||
}
|
||||
|
||||
pub const fn type_code() -> u8 {
|
||||
Self::TYPE_CODE
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.addr.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Connect> for (Address,) {
|
||||
fn from(conn: Connect) -> Self {
|
||||
(conn.addr,)
|
||||
}
|
||||
}
|
36
tuic/src/protocol/dissociate.rs
Normal file
36
tuic/src/protocol/dissociate.rs
Normal file
@ -0,0 +1,36 @@
|
||||
// +----------+
|
||||
// | ASSOC_ID |
|
||||
// +----------+
|
||||
// | 2 |
|
||||
// +----------+
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Dissociate {
|
||||
assoc_id: u16,
|
||||
}
|
||||
|
||||
impl Dissociate {
|
||||
const TYPE_CODE: u8 = 0x03;
|
||||
|
||||
pub const fn new(assoc_id: u16) -> Self {
|
||||
Self { assoc_id }
|
||||
}
|
||||
|
||||
pub fn assoc_id(&self) -> u16 {
|
||||
self.assoc_id
|
||||
}
|
||||
|
||||
pub const fn type_code() -> u8 {
|
||||
Self::TYPE_CODE
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Dissociate> for (u16,) {
|
||||
fn from(dissoc: Dissociate) -> Self {
|
||||
(dissoc.assoc_id,)
|
||||
}
|
||||
}
|
28
tuic/src/protocol/heartbeat.rs
Normal file
28
tuic/src/protocol/heartbeat.rs
Normal file
@ -0,0 +1,28 @@
|
||||
// +-+
|
||||
// | |
|
||||
// +-+
|
||||
// | |
|
||||
// +-+
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Heartbeat;
|
||||
|
||||
impl Heartbeat {
|
||||
const TYPE_CODE: u8 = 0x04;
|
||||
|
||||
pub const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub const fn type_code() -> u8 {
|
||||
Self::TYPE_CODE
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Heartbeat> for () {
|
||||
fn from(_: Heartbeat) -> Self {}
|
||||
}
|
157
tuic/src/protocol/mod.rs
Normal file
157
tuic/src/protocol/mod.rs
Normal file
@ -0,0 +1,157 @@
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
mem,
|
||||
net::SocketAddr,
|
||||
};
|
||||
|
||||
mod authenticate;
|
||||
mod connect;
|
||||
mod dissociate;
|
||||
mod heartbeat;
|
||||
mod packet;
|
||||
|
||||
pub use self::{
|
||||
authenticate::Authenticate, connect::Connect, dissociate::Dissociate, heartbeat::Heartbeat,
|
||||
packet::Packet,
|
||||
};
|
||||
|
||||
pub const VERSION: u8 = 0x05;
|
||||
|
||||
/// Header
|
||||
///
|
||||
/// ```plain
|
||||
/// +-----+----------+----------+
|
||||
/// | VER | TYPE | OPT |
|
||||
/// +-----+----------+----------+
|
||||
/// | 1 | 1 | Variable |
|
||||
/// +-----+----------+----------+
|
||||
/// ```
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Header {
|
||||
Authenticate(Authenticate),
|
||||
Connect(Connect),
|
||||
Packet(Packet),
|
||||
Dissociate(Dissociate),
|
||||
Heartbeat(Heartbeat),
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub const TYPE_CODE_AUTHENTICATE: u8 = Authenticate::type_code();
|
||||
pub const TYPE_CODE_CONNECT: u8 = Connect::type_code();
|
||||
pub const TYPE_CODE_PACKET: u8 = Packet::type_code();
|
||||
pub const TYPE_CODE_DISSOCIATE: u8 = Dissociate::type_code();
|
||||
pub const TYPE_CODE_HEARTBEAT: u8 = Heartbeat::type_code();
|
||||
|
||||
pub const fn type_code(&self) -> u8 {
|
||||
match self {
|
||||
Self::Authenticate(_) => Authenticate::type_code(),
|
||||
Self::Connect(_) => Connect::type_code(),
|
||||
Self::Packet(_) => Packet::type_code(),
|
||||
Self::Dissociate(_) => Dissociate::type_code(),
|
||||
Self::Heartbeat(_) => Heartbeat::type_code(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
2 + match self {
|
||||
Self::Authenticate(auth) => auth.len(),
|
||||
Self::Connect(conn) => conn.len(),
|
||||
Self::Packet(packet) => packet.len(),
|
||||
Self::Dissociate(dissociate) => dissociate.len(),
|
||||
Self::Heartbeat(heartbeat) => heartbeat.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Address
|
||||
///
|
||||
/// ```plain
|
||||
/// +------+----------+
|
||||
/// | TYPE | ADDR |
|
||||
/// +------+----------+
|
||||
/// | 1 | Variable |
|
||||
/// +------+----------+
|
||||
/// ```
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// The port number is encoded in 2 bytes after the Domain name / IP address.
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum Address {
|
||||
None,
|
||||
DomainAddress(String, u16),
|
||||
SocketAddress(SocketAddr),
|
||||
}
|
||||
|
||||
impl Address {
|
||||
pub const TYPE_CODE_NONE: u8 = 0xff;
|
||||
pub const TYPE_CODE_DOMAIN: u8 = 0x00;
|
||||
pub const TYPE_CODE_IPV4: u8 = 0x01;
|
||||
pub const TYPE_CODE_IPV6: u8 = 0x02;
|
||||
|
||||
pub const fn type_code(&self) -> u8 {
|
||||
match self {
|
||||
Self::None => Self::TYPE_CODE_NONE,
|
||||
Self::DomainAddress(_, _) => Self::TYPE_CODE_DOMAIN,
|
||||
Self::SocketAddress(addr) => match addr {
|
||||
SocketAddr::V4(_) => Self::TYPE_CODE_IPV4,
|
||||
SocketAddr::V6(_) => Self::TYPE_CODE_IPV6,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
1 + match self {
|
||||
Address::None => 0,
|
||||
Address::DomainAddress(addr, _) => 1 + addr.len() + 2,
|
||||
Address::SocketAddress(addr) => match addr {
|
||||
SocketAddr::V4(_) => 4 + 2,
|
||||
SocketAddr::V6(_) => 2 * 8 + 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take(&mut self) -> Self {
|
||||
mem::take(self)
|
||||
}
|
||||
|
||||
pub fn is_none(&self) -> bool {
|
||||
matches!(self, Self::None)
|
||||
}
|
||||
|
||||
pub fn is_domain(&self) -> bool {
|
||||
matches!(self, Self::DomainAddress(_, _))
|
||||
}
|
||||
|
||||
pub fn is_ipv4(&self) -> bool {
|
||||
matches!(self, Self::SocketAddress(SocketAddr::V4(_)))
|
||||
}
|
||||
|
||||
pub fn is_ipv6(&self) -> bool {
|
||||
matches!(self, Self::SocketAddress(SocketAddr::V6(_)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Address {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
Self::None => write!(f, "none"),
|
||||
Self::DomainAddress(addr, port) => write!(f, "{addr}:{port}"),
|
||||
Self::SocketAddress(addr) => write!(f, "{addr}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Address {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
88
tuic/src/protocol/packet.rs
Normal file
88
tuic/src/protocol/packet.rs
Normal file
@ -0,0 +1,88 @@
|
||||
use super::Address;
|
||||
|
||||
// +----------+--------+------------+---------+------+----------+
|
||||
// | ASSOC_ID | PKT_ID | FRAG_TOTAL | FRAG_ID | SIZE | ADDR |
|
||||
// +----------+--------+------------+---------+------+----------+
|
||||
// | 2 | 2 | 1 | 1 | 2 | Variable |
|
||||
// +----------+--------+------------+---------+------+----------+
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Packet {
|
||||
assoc_id: u16,
|
||||
pkt_id: u16,
|
||||
frag_total: u8,
|
||||
frag_id: u8,
|
||||
size: u16,
|
||||
addr: Address,
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
const TYPE_CODE: u8 = 0x02;
|
||||
|
||||
pub const fn new(
|
||||
assoc_id: u16,
|
||||
pkt_id: u16,
|
||||
frag_total: u8,
|
||||
frag_id: u8,
|
||||
size: u16,
|
||||
addr: Address,
|
||||
) -> Self {
|
||||
Self {
|
||||
assoc_id,
|
||||
pkt_id,
|
||||
frag_total,
|
||||
frag_id,
|
||||
size,
|
||||
addr,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assoc_id(&self) -> u16 {
|
||||
self.assoc_id
|
||||
}
|
||||
|
||||
pub fn pkt_id(&self) -> u16 {
|
||||
self.pkt_id
|
||||
}
|
||||
|
||||
pub fn frag_total(&self) -> u8 {
|
||||
self.frag_total
|
||||
}
|
||||
|
||||
pub fn frag_id(&self) -> u8 {
|
||||
self.frag_id
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u16 {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn addr(&self) -> &Address {
|
||||
&self.addr
|
||||
}
|
||||
|
||||
pub const fn type_code() -> u8 {
|
||||
Self::TYPE_CODE
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
Self::len_without_addr() + self.addr.len()
|
||||
}
|
||||
|
||||
pub const fn len_without_addr() -> usize {
|
||||
2 + 2 + 1 + 1 + 2
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Packet> for (u16, u16, u8, u8, u16, Address) {
|
||||
fn from(pkt: Packet) -> Self {
|
||||
(
|
||||
pkt.assoc_id,
|
||||
pkt.pkt_id,
|
||||
pkt.frag_total,
|
||||
pkt.frag_id,
|
||||
pkt.size,
|
||||
pkt.addr,
|
||||
)
|
||||
}
|
||||
}
|
266
tuic/src/unmarshal.rs
Normal file
266
tuic/src/unmarshal.rs
Normal file
@ -0,0 +1,266 @@
|
||||
use crate::protocol::{
|
||||
Address, Authenticate, Connect, Dissociate, Header, Heartbeat, Packet, VERSION,
|
||||
};
|
||||
use futures_util::{AsyncRead, AsyncReadExt};
|
||||
use std::{
|
||||
io::{Error as IoError, Read},
|
||||
net::SocketAddr,
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
impl Header {
|
||||
#[cfg(feature = "async_marshal")]
|
||||
pub async fn async_unmarshal(s: &mut (impl AsyncRead + Unpin)) -> Result<Self, UnmarshalError> {
|
||||
let mut buf = [0; 1];
|
||||
s.read_exact(&mut buf).await?;
|
||||
let ver = buf[0];
|
||||
|
||||
if ver != VERSION {
|
||||
return Err(UnmarshalError::InvalidVersion(ver));
|
||||
}
|
||||
|
||||
let mut buf = [0; 1];
|
||||
s.read_exact(&mut buf).await?;
|
||||
let cmd = buf[0];
|
||||
|
||||
match cmd {
|
||||
Header::TYPE_CODE_AUTHENTICATE => {
|
||||
Authenticate::async_read(s).await.map(Self::Authenticate)
|
||||
}
|
||||
Header::TYPE_CODE_CONNECT => Connect::async_read(s).await.map(Self::Connect),
|
||||
Header::TYPE_CODE_PACKET => Packet::async_read(s).await.map(Self::Packet),
|
||||
Header::TYPE_CODE_DISSOCIATE => Dissociate::async_read(s).await.map(Self::Dissociate),
|
||||
Header::TYPE_CODE_HEARTBEAT => Heartbeat::async_read(s).await.map(Self::Heartbeat),
|
||||
_ => Err(UnmarshalError::InvalidCommand(cmd)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "marshal")]
|
||||
pub fn unmarshal(s: &mut impl Read) -> Result<Self, UnmarshalError> {
|
||||
let mut buf = [0; 1];
|
||||
s.read_exact(&mut buf)?;
|
||||
let ver = buf[0];
|
||||
|
||||
if ver != VERSION {
|
||||
return Err(UnmarshalError::InvalidVersion(ver));
|
||||
}
|
||||
|
||||
let mut buf = [0; 1];
|
||||
s.read_exact(&mut buf)?;
|
||||
let cmd = buf[0];
|
||||
|
||||
match cmd {
|
||||
Header::TYPE_CODE_AUTHENTICATE => Authenticate::read(s).map(Self::Authenticate),
|
||||
Header::TYPE_CODE_CONNECT => Connect::read(s).map(Self::Connect),
|
||||
Header::TYPE_CODE_PACKET => Packet::read(s).map(Self::Packet),
|
||||
Header::TYPE_CODE_DISSOCIATE => Dissociate::read(s).map(Self::Dissociate),
|
||||
Header::TYPE_CODE_HEARTBEAT => Heartbeat::read(s).map(Self::Heartbeat),
|
||||
_ => Err(UnmarshalError::InvalidCommand(cmd)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Address {
|
||||
#[cfg(feature = "async_marshal")]
|
||||
async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result<Self, UnmarshalError> {
|
||||
let mut buf = [0; 1];
|
||||
s.read_exact(&mut buf).await?;
|
||||
let type_code = buf[0];
|
||||
|
||||
match type_code {
|
||||
Address::TYPE_CODE_NONE => Ok(Self::None),
|
||||
Address::TYPE_CODE_DOMAIN => {
|
||||
let mut buf = [0; 1];
|
||||
s.read_exact(&mut buf).await?;
|
||||
let len = buf[0] as usize;
|
||||
|
||||
let mut buf = vec![0; len + 2];
|
||||
s.read_exact(&mut buf).await?;
|
||||
let port = u16::from_be_bytes([buf[len], buf[len + 1]]);
|
||||
buf.truncate(len);
|
||||
let domain = String::from_utf8(buf)?;
|
||||
|
||||
Ok(Self::DomainAddress(domain, port))
|
||||
}
|
||||
Address::TYPE_CODE_IPV4 => {
|
||||
let mut buf = [0; 6];
|
||||
s.read_exact(&mut buf).await?;
|
||||
let ip = [buf[0], buf[1], buf[2], buf[3]];
|
||||
let port = u16::from_be_bytes([buf[4], buf[5]]);
|
||||
Ok(Self::SocketAddress(SocketAddr::from((ip, port))))
|
||||
}
|
||||
Address::TYPE_CODE_IPV6 => {
|
||||
let mut buf = [0; 18];
|
||||
s.read_exact(&mut buf).await?;
|
||||
let ip = [
|
||||
u16::from_be_bytes([buf[0], buf[1]]),
|
||||
u16::from_be_bytes([buf[2], buf[3]]),
|
||||
u16::from_be_bytes([buf[4], buf[5]]),
|
||||
u16::from_be_bytes([buf[6], buf[7]]),
|
||||
u16::from_be_bytes([buf[8], buf[9]]),
|
||||
u16::from_be_bytes([buf[10], buf[11]]),
|
||||
u16::from_be_bytes([buf[12], buf[13]]),
|
||||
u16::from_be_bytes([buf[14], buf[15]]),
|
||||
];
|
||||
let port = u16::from_be_bytes([buf[16], buf[17]]);
|
||||
|
||||
Ok(Self::SocketAddress(SocketAddr::from((ip, port))))
|
||||
}
|
||||
_ => Err(UnmarshalError::InvalidAddressType(type_code)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "marshal")]
|
||||
fn read(s: &mut impl Read) -> Result<Self, UnmarshalError> {
|
||||
let mut buf = [0; 1];
|
||||
s.read_exact(&mut buf)?;
|
||||
let type_code = buf[0];
|
||||
|
||||
match type_code {
|
||||
Address::TYPE_CODE_NONE => Ok(Self::None),
|
||||
Address::TYPE_CODE_DOMAIN => {
|
||||
let mut buf = [0; 1];
|
||||
s.read_exact(&mut buf)?;
|
||||
let len = buf[0] as usize;
|
||||
|
||||
let mut buf = vec![0; len + 2];
|
||||
s.read_exact(&mut buf)?;
|
||||
let port = u16::from_be_bytes([buf[len], buf[len + 1]]);
|
||||
buf.truncate(len);
|
||||
let domain = String::from_utf8(buf)?;
|
||||
|
||||
Ok(Self::DomainAddress(domain, port))
|
||||
}
|
||||
Address::TYPE_CODE_IPV4 => {
|
||||
let mut buf = [0; 6];
|
||||
s.read_exact(&mut buf)?;
|
||||
let ip = [buf[0], buf[1], buf[2], buf[3]];
|
||||
let port = u16::from_be_bytes([buf[4], buf[5]]);
|
||||
Ok(Self::SocketAddress(SocketAddr::from((ip, port))))
|
||||
}
|
||||
Address::TYPE_CODE_IPV6 => {
|
||||
let mut buf = [0; 18];
|
||||
s.read_exact(&mut buf)?;
|
||||
let ip = [
|
||||
u16::from_be_bytes([buf[0], buf[1]]),
|
||||
u16::from_be_bytes([buf[2], buf[3]]),
|
||||
u16::from_be_bytes([buf[4], buf[5]]),
|
||||
u16::from_be_bytes([buf[6], buf[7]]),
|
||||
u16::from_be_bytes([buf[8], buf[9]]),
|
||||
u16::from_be_bytes([buf[10], buf[11]]),
|
||||
u16::from_be_bytes([buf[12], buf[13]]),
|
||||
u16::from_be_bytes([buf[14], buf[15]]),
|
||||
];
|
||||
let port = u16::from_be_bytes([buf[16], buf[17]]);
|
||||
|
||||
Ok(Self::SocketAddress(SocketAddr::from((ip, port))))
|
||||
}
|
||||
_ => Err(UnmarshalError::InvalidAddressType(type_code)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Authenticate {
|
||||
#[cfg(feature = "async_marshal")]
|
||||
async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result<Self, UnmarshalError> {
|
||||
let mut buf = [0; 32];
|
||||
s.read_exact(&mut buf).await?;
|
||||
Ok(Self::new(buf))
|
||||
}
|
||||
|
||||
#[cfg(feature = "marshal")]
|
||||
fn read(s: &mut impl Read) -> Result<Self, UnmarshalError> {
|
||||
let mut buf = [0; 32];
|
||||
s.read_exact(&mut buf)?;
|
||||
Ok(Self::new(buf))
|
||||
}
|
||||
}
|
||||
|
||||
impl Connect {
|
||||
#[cfg(feature = "async_marshal")]
|
||||
async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result<Self, UnmarshalError> {
|
||||
Ok(Self::new(Address::async_read(s).await?))
|
||||
}
|
||||
|
||||
#[cfg(feature = "marshal")]
|
||||
fn read(s: &mut impl Read) -> Result<Self, UnmarshalError> {
|
||||
Ok(Self::new(Address::read(s)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
#[cfg(feature = "async_marshal")]
|
||||
async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result<Self, UnmarshalError> {
|
||||
let mut buf = [0; 8];
|
||||
s.read_exact(&mut buf).await?;
|
||||
|
||||
let assoc_id = u16::from_be_bytes([buf[0], buf[1]]);
|
||||
let pkt_id = u16::from_be_bytes([buf[2], buf[3]]);
|
||||
let frag_total = buf[4];
|
||||
let frag_id = buf[5];
|
||||
let size = u16::from_be_bytes([buf[6], buf[7]]);
|
||||
let addr = Address::async_read(s).await?;
|
||||
|
||||
Ok(Self::new(assoc_id, pkt_id, frag_total, frag_id, size, addr))
|
||||
}
|
||||
|
||||
#[cfg(feature = "marshal")]
|
||||
fn read(s: &mut impl Read) -> Result<Self, UnmarshalError> {
|
||||
let mut buf = [0; 8];
|
||||
s.read_exact(&mut buf)?;
|
||||
|
||||
let assoc_id = u16::from_be_bytes([buf[0], buf[1]]);
|
||||
let pkt_id = u16::from_be_bytes([buf[2], buf[3]]);
|
||||
let frag_total = buf[4];
|
||||
let frag_id = buf[5];
|
||||
let size = u16::from_be_bytes([buf[6], buf[7]]);
|
||||
let addr = Address::read(s)?;
|
||||
|
||||
Ok(Self::new(assoc_id, pkt_id, frag_total, frag_id, size, addr))
|
||||
}
|
||||
}
|
||||
|
||||
impl Dissociate {
|
||||
#[cfg(feature = "async_marshal")]
|
||||
async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result<Self, UnmarshalError> {
|
||||
let mut buf = [0; 2];
|
||||
s.read_exact(&mut buf).await?;
|
||||
let assoc_id = u16::from_be_bytes(buf);
|
||||
Ok(Self::new(assoc_id))
|
||||
}
|
||||
|
||||
#[cfg(feature = "marshal")]
|
||||
fn read(s: &mut impl Read) -> Result<Self, UnmarshalError> {
|
||||
let mut buf = [0; 2];
|
||||
s.read_exact(&mut buf)?;
|
||||
let assoc_id = u16::from_be_bytes(buf);
|
||||
Ok(Self::new(assoc_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl Heartbeat {
|
||||
#[cfg(feature = "async_marshal")]
|
||||
async fn async_read(_s: &mut (impl AsyncRead + Unpin)) -> Result<Self, UnmarshalError> {
|
||||
Ok(Self::new())
|
||||
}
|
||||
|
||||
#[cfg(feature = "marshal")]
|
||||
fn read(_s: &mut impl Read) -> Result<Self, UnmarshalError> {
|
||||
Ok(Self::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UnmarshalError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
#[error("invalid version: {0}")]
|
||||
InvalidVersion(u8),
|
||||
#[error("invalid command: {0}")]
|
||||
InvalidCommand(u8),
|
||||
#[error("invalid address type: {0}")]
|
||||
InvalidAddressType(u8),
|
||||
#[error("address parsing error: {0}")]
|
||||
AddressParse(#[from] FromUtf8Error),
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user