support quic proxy (#993)
Some checks failed
EasyTier Core / pre_job (push) Has been cancelled
EasyTier Core / build_web (push) Has been cancelled
EasyTier Core / build (freebsd-13.2-x86_64, 13.2, ubuntu-22.04, x86_64-unknown-freebsd) (push) Has been cancelled
EasyTier Core / build (linux-aarch64, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-arm, ubuntu-22.04, arm-unknown-linux-musleabi) (push) Has been cancelled
EasyTier Core / build (linux-armhf, ubuntu-22.04, arm-unknown-linux-musleabihf) (push) Has been cancelled
EasyTier Core / build (linux-armv7, ubuntu-22.04, armv7-unknown-linux-musleabi) (push) Has been cancelled
EasyTier Core / build (linux-armv7hf, ubuntu-22.04, armv7-unknown-linux-musleabihf) (push) Has been cancelled
EasyTier Core / build (linux-mips, ubuntu-22.04, mips-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-mipsel, ubuntu-22.04, mipsel-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (macos-aarch64, macos-latest, aarch64-apple-darwin) (push) Has been cancelled
EasyTier Core / build (macos-x86_64, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
EasyTier Core / build (windows-arm64, windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / build (windows-i686, windows-latest, i686-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / build (windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / core-result (push) Has been cancelled
EasyTier Core / magisk_build (push) Has been cancelled
EasyTier GUI / pre_job (push) Has been cancelled
EasyTier GUI / build-gui (linux-aarch64, aarch64-unknown-linux-gnu, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier GUI / build-gui (linux-x86_64, x86_64-unknown-linux-gnu, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Has been cancelled
EasyTier GUI / build-gui (macos-aarch64, aarch64-apple-darwin, macos-latest, aarch64-apple-darwin) (push) Has been cancelled
EasyTier GUI / build-gui (macos-x86_64, x86_64-apple-darwin, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
EasyTier GUI / build-gui (windows-arm64, aarch64-pc-windows-msvc, windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / build-gui (windows-i686, i686-pc-windows-msvc, windows-latest, i686-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / build-gui (windows-x86_64, x86_64-pc-windows-msvc, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / gui-result (push) Has been cancelled
EasyTier Mobile / pre_job (push) Has been cancelled
EasyTier Mobile / build-mobile (android, ubuntu-22.04, android) (push) Has been cancelled
EasyTier Mobile / mobile-result (push) Has been cancelled
EasyTier Test / pre_job (push) Has been cancelled
EasyTier Test / test (push) Has been cancelled

QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC.
QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. 
QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`.
This commit is contained in:
Sijie.Sun
2025-06-15 19:43:45 +08:00
committed by GitHub
parent 5a98fac395
commit 40b5fe9a54
30 changed files with 851 additions and 165 deletions

View File

@@ -175,14 +175,14 @@ jobs:
fi
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier
cargo +nightly build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier
else
if [[ $OS =~ ^windows.*$ ]]; then
SUFFIX=.exe
fi
cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed
cargo build --release --target $TARGET --package=easytier-web --features=embed
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
cargo build --release --verbose --target $TARGET
cargo build --release --target $TARGET
fi
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)

View File

@@ -91,6 +91,7 @@ jobs:
- name: Run tests
run: |
sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose -- --test-threads=1 --nocapture
sudo prlimit --pid $$ --nofile=1048576:1048576
sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose -- --test-threads=1
sudo chown -R $USER:$USER ./target
sudo chown -R $USER:$USER ~/.cargo

145
Cargo.lock generated
View File

@@ -2383,6 +2383,18 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "fastbloom"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27cea6e7f512d43b098939ff4d5a5d6fe3db07971e1d05176fe26c642d33f5b8"
dependencies = [
"getrandom 0.3.2",
"rand 0.9.1",
"siphasher 1.0.1",
"wide",
]
[[package]]
name = "fastrand"
version = "2.1.0"
@@ -3923,20 +3935,6 @@ dependencies = [
"libc",
]
[[package]]
name = "jni"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
dependencies = [
"cesu8",
"combine",
"jni-sys",
"log",
"thiserror 1.0.63",
"walkdir",
]
[[package]]
name = "jni"
version = "0.21.1"
@@ -4118,7 +4116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -4254,6 +4252,12 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "mac"
version = "0.1.1"
@@ -4504,7 +4508,7 @@ dependencies = [
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework 2.11.1",
"security-framework-sys",
"tempfile",
]
@@ -6043,38 +6047,45 @@ dependencies = [
[[package]]
name = "quinn"
version = "0.11.3"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156"
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 1.0.63",
"thiserror 2.0.11",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.6"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd"
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
dependencies = [
"bytes",
"rand 0.8.5",
"fastbloom",
"getrandom 0.3.2",
"lru-slab",
"rand 0.9.1",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"slab",
"thiserror 1.0.63",
"thiserror 2.0.11",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
@@ -6694,9 +6705,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.12"
version = "0.23.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044"
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
dependencies = [
"once_cell",
"ring",
@@ -6708,15 +6719,14 @@ dependencies = [
[[package]]
name = "rustls-native-certs"
version = "0.7.1"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba"
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"rustls-pki-types",
"schannel",
"security-framework",
"security-framework 3.2.0",
]
[[package]]
@@ -6733,26 +6743,29 @@ name = "rustls-pki-types"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
dependencies = [
"web-time",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.3.3"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93bda3f493b9abe5b93b3e7e3ecde0df292f2bd28c0296b90586ee0055ff5123"
checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1"
dependencies = [
"core-foundation 0.9.4",
"core-foundation 0.10.0",
"core-foundation-sys",
"jni 0.19.0",
"jni",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework 3.2.0",
"security-framework-sys",
"webpki-roots",
"winapi",
"webpki-root-certs 0.26.11",
"windows-sys 0.59.0",
]
[[package]]
@@ -6763,9 +6776,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.102.6"
version = "0.103.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e"
checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
dependencies = [
"ring",
"rustls-pki-types",
@@ -7050,15 +7063,27 @@ dependencies = [
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"num-bigint",
"security-framework-sys",
]
[[package]]
name = "security-framework"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
dependencies = [
"bitflags 2.8.0",
"core-foundation 0.10.0",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.11.1"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
@@ -7985,7 +8010,7 @@ dependencies = [
"gdkx11-sys",
"gtk",
"instant",
"jni 0.21.1",
"jni",
"lazy_static",
"libc",
"log",
@@ -8047,7 +8072,7 @@ dependencies = [
"heck 0.5.0",
"http",
"image 0.25.2",
"jni 0.21.1",
"jni",
"libc",
"log",
"mime",
@@ -8288,7 +8313,7 @@ dependencies = [
"dpi",
"gtk",
"http",
"jni 0.21.1",
"jni",
"raw-window-handle",
"serde",
"serde_json",
@@ -8306,7 +8331,7 @@ checksum = "62fa2068e8498ad007b54d5773d03d57c3ff6dd96f8c8ce58beff44d0d5e0d30"
dependencies = [
"gtk",
"http",
"jni 0.21.1",
"jni",
"log",
"objc2",
"objc2-app-kit",
@@ -9445,6 +9470,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webkit2gtk"
version = "2.0.1"
@@ -9499,6 +9534,24 @@ dependencies = [
"untrusted",
]
[[package]]
name = "webpki-root-certs"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
dependencies = [
"webpki-root-certs 1.0.0",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01a83f7e1a9f8712695c03eabe9ed3fbca0feff0152f33f12593e5a6303cb1a4"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "0.26.3"
@@ -10099,7 +10152,7 @@ dependencies = [
"html5ever",
"http",
"javascriptcore-rs",
"jni 0.21.1",
"jni",
"kuchikiki",
"libc",
"ndk",

View File

@@ -147,6 +147,8 @@ const bool_flags: BoolFlag[] = [
{ field: 'use_smoltcp', help: 'use_smoltcp_help' },
{ field: 'enable_kcp_proxy', help: 'enable_kcp_proxy_help' },
{ field: 'disable_kcp_input', help: 'disable_kcp_input_help' },
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
{ field: 'disable_quic_input', help: 'disable_quic_input_help' },
{ field: 'disable_p2p', help: 'disable_p2p_help' },
{ field: 'bind_device', help: 'bind_device_help' },
{ field: 'no_tun', help: 'no_tun_help' },
@@ -200,7 +202,7 @@ const bool_flags: BoolFlag[] = [
<div class="flex flex-col gap-2 basis-5/12 grow">
<label for="network_secret">{{ t('network_secret') }}</label>
<Password id="network_secret" v-model="curNetwork.network_secret"
aria-describedby="network_secret-help" toggleMask :feedback="false"/>
aria-describedby="network_secret-help" toggleMask :feedback="false" />
</div>
</div>
@@ -271,7 +273,7 @@ const bool_flags: BoolFlag[] = [
<div class="flex flex-col gap-2 basis-8/12 grow">
<InputGroup>
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
:placeholder="t('vpn_portal_client_network')" />
:placeholder="t('vpn_portal_client_network')" />
<InputGroupAddon>
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
</InputGroupAddon>
@@ -279,7 +281,7 @@ const bool_flags: BoolFlag[] = [
</div>
<div class="flex flex-col gap-2 basis-3/12 grow">
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false" :format="false"
:min="0" :max="65535" fluid />
:min="0" :max="65535" fluid />
</div>
</div>
</div>
@@ -325,11 +327,10 @@ const bool_flags: BoolFlag[] = [
<div class="flex flex-col gap-2 basis-5/12 grow">
<div class="flex">
<label for="mtu">{{ t('mtu') }}</label>
<span class="pi pi-question-circle ml-2 self-center"
v-tooltip="t('mtu_help')"></span>
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('mtu_help')"></span>
</div>
<InputNumber id="mtu" v-model="curNetwork.mtu" aria-describedby="mtu-help"
:format="false" :placeholder="t('mtu_placeholder')" :min="400" :max="1380" fluid/>
<InputNumber id="mtu" v-model="curNetwork.mtu" aria-describedby="mtu-help" :format="false"
:placeholder="t('mtu_placeholder')" :min="400" :max="1380" fluid />
</div>
</div>
@@ -338,15 +339,15 @@ const bool_flags: BoolFlag[] = [
<div class="flex">
<label for="relay_network_whitelist">{{ t('relay_network_whitelist') }}</label>
<span class="pi pi-question-circle ml-2 self-center"
v-tooltip="t('relay_network_whitelist_help')"></span>
v-tooltip="t('relay_network_whitelist_help')"></span>
</div>
<ToggleButton v-model="curNetwork.enable_relay_network_whitelist" on-icon="pi pi-check" off-icon="pi pi-times"
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
<ToggleButton v-model="curNetwork.enable_relay_network_whitelist" on-icon="pi pi-check"
off-icon="pi pi-times" :on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
<div v-if="curNetwork.enable_relay_network_whitelist" class="items-center flex flex-row gap-x-4">
<div class="min-w-64 w-full">
<AutoComplete id="relay_network_whitelist" v-model="curNetwork.relay_network_whitelist"
:placeholder="t('relay_network_whitelist')" class="w-full" multiple fluid
:suggestions="whitelistSuggestions" @complete="searchWhitelistSuggestions" />
:placeholder="t('relay_network_whitelist')" class="w-full" multiple fluid
:suggestions="whitelistSuggestions" @complete="searchWhitelistSuggestions" />
</div>
</div>
</div>
@@ -359,12 +360,12 @@ const bool_flags: BoolFlag[] = [
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('manual_routes_help')"></span>
</div>
<ToggleButton v-model="curNetwork.enable_manual_routes" on-icon="pi pi-check" off-icon="pi pi-times"
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
<div v-if="curNetwork.enable_manual_routes" class="items-center flex flex-row gap-x-4">
<div class="min-w-64 w-full">
<AutoComplete id="routes" v-model="curNetwork.routes"
:placeholder="t('chips_placeholder', ['192.168.0.0/16'])" class="w-full" multiple fluid
:suggestions="inetSuggestions" @complete="searchInetSuggestions" />
:placeholder="t('chips_placeholder', ['192.168.0.0/16'])" class="w-full" multiple fluid
:suggestions="inetSuggestions" @complete="searchInetSuggestions" />
</div>
</div>
</div>
@@ -377,11 +378,11 @@ const bool_flags: BoolFlag[] = [
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('socks5_help')"></span>
</div>
<ToggleButton v-model="curNetwork.enable_socks5" on-icon="pi pi-check" off-icon="pi pi-times"
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
<div v-if="curNetwork.enable_socks5" class="items-center flex flex-row gap-x-4">
<div class="min-w-64 w-full">
<InputNumber id="socks5_port" v-model="curNetwork.socks5_port" aria-describedby="rpc_port-help"
:format="false" :allow-empty="false" :min="0" :max="65535" class="w-full"/>
:format="false" :allow-empty="false" :min="0" :max="65535" class="w-full" />
</div>
</div>
</div>
@@ -394,8 +395,8 @@ const bool_flags: BoolFlag[] = [
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('exit_nodes_help')"></span>
</div>
<AutoComplete id="exit_nodes" v-model="curNetwork.exit_nodes"
:placeholder="t('chips_placeholder', ['192.168.8.8'])" class="w-full" multiple fluid
:suggestions="exitNodesSuggestions" @complete="searchExitNodesSuggestions" />
:placeholder="t('chips_placeholder', ['192.168.8.8'])" class="w-full" multiple fluid
:suggestions="exitNodesSuggestions" @complete="searchExitNodesSuggestions" />
</div>
</div>
@@ -406,8 +407,8 @@ const bool_flags: BoolFlag[] = [
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('mapped_listeners_help')"></span>
</div>
<AutoComplete id="mapped_listeners" v-model="curNetwork.mapped_listeners"
:placeholder="t('chips_placeholder', ['tcp://123.123.123.123:11223'])" class="w-full"
multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
:placeholder="t('chips_placeholder', ['tcp://123.123.123.123:11223'])" class="w-full" multiple fluid
:suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
</div>
</div>

View File

@@ -85,6 +85,12 @@ enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,
disable_kcp_input: 禁用 KCP 输入
disable_kcp_input_help: 禁用 KCP 入站流量,其他开启 KCP 代理的节点仍然使用 TCP 连接到本节点。
enable_quic_proxy: 启用 QUIC 代理
enable_quic_proxy_help: 将 TCP 流量转为 QUIC 流量,降低传输延迟,提升传输速度。
disable_quic_input: 禁用 QUIC 输入
disable_quic_input_help: 禁用 QUIC 入站流量,其他开启 QUIC 代理的节点仍然使用 TCP 连接到本节点。
disable_p2p: 禁用 P2P
disable_p2p_help: 禁用 P2P 模式,所有流量通过手动指定的服务器中转。

View File

@@ -84,6 +84,12 @@ enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and
disable_kcp_input: Disable KCP Input
disable_kcp_input_help: Disable inbound KCP traffic, while nodes with KCP proxy enabled continue to connect using TCP.
enable_quic_proxy: Enable QUIC Proxy
enable_quic_proxy_help: Convert TCP traffic to QUIC traffic to reduce latency and boost transmission speed.
disable_quic_input: Disable QUIC Input
disable_quic_input_help: Disable inbound QUIC traffic, while nodes with QUIC proxy enabled continue to connect using TCP.
disable_p2p: Disable P2P
disable_p2p_help: Disable P2P mode; route all traffic through a manually specified relay server.

View File

@@ -39,6 +39,8 @@ export interface NetworkConfig {
use_smoltcp?: boolean
enable_kcp_proxy?: boolean
disable_kcp_input?: boolean
enable_quic_proxy?: boolean
disable_quic_input?: boolean
disable_p2p?: boolean
bind_device?: boolean
no_tun?: boolean
@@ -105,6 +107,8 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
use_smoltcp: false,
enable_kcp_proxy: false,
disable_kcp_input: false,
enable_quic_proxy: false,
disable_quic_input: false,
disable_p2p: false,
bind_device: true,
no_tun: false,

View File

@@ -64,7 +64,7 @@ bytes = "1.5.0"
pin-project-lite = "0.2.13"
tachyonix = "0.3.0"
quinn = { version = "0.11.0", optional = true, features = ["ring"] }
quinn = { version = "0.11.8", optional = true, features = ["ring"] }
rustls = { version = "0.23.0", features = [
"ring",
], default-features = false, optional = true }
@@ -280,9 +280,8 @@ tokio-socks = "0.5.2"
[features]
default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun", "socks5"]
default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun", "socks5", "quic"]
full = [
"quic",
"websocket",
"wireguard",
"mimalloc",
@@ -291,7 +290,6 @@ full = [
"tun",
"socks5",
]
mips = ["aes-gcm", "mimalloc", "wireguard", "tun", "smoltcp", "socks5"]
wireguard = ["dep:boringtun", "dep:ring"]
quic = ["dep:quinn", "dep:rustls", "dep:rcgen"]
mimalloc = ["dep:mimalloc"]

View File

@@ -158,6 +158,12 @@ core_clap:
disable_kcp_input:
en: "do not allow other nodes to use kcp to proxy tcp streams to this node. when a node with kcp proxy enabled accesses this node, the original tcp connection is preserved."
zh-CN: "不允许其他节点使用 KCP 代理 TCP 流到此节点。开启 KCP 代理的节点访问此节点时,依然使用原始 TCP 连接。"
enable_quic_proxy:
en: "proxy tcp streams with QUIC, improving the latency and throughput on the network with udp packet loss."
zh-CN: "使用 QUIC 代理 TCP 流,提高在 UDP 丢包网络上的延迟和吞吐量。"
disable_quic_input:
en: "do not allow other nodes to use QUIC to proxy tcp streams to this node. when a node with QUIC proxy enabled accesses this node, the original tcp connection is preserved."
zh-CN: "不允许其他节点使用 QUIC 代理 TCP 流到此节点。开启 QUIC 代理的节点访问此节点时,依然使用原始 TCP 连接。"
port_forward:
en: "forward local port to remote port in virtual network. e.g.: udp://0.0.0.0:12345/10.126.126.1:23456, means forward local udp port 12345 to 10.126.126.1:23456 in the virtual network. can specify multiple."
zh-CN: "将本地端口转发到虚拟网络中的远程端口。例如udp://0.0.0.0:12345/10.126.126.1:23456表示将本地UDP端口12345转发到虚拟网络中的10.126.126.1:23456。可以指定多个。"

View File

@@ -39,6 +39,8 @@ pub fn gen_default_flags() -> Flags {
disable_relay_kcp: true,
accept_dns: false,
private_mode: false,
enable_quic_proxy: false,
disable_quic_input: false,
}
}
@@ -437,7 +439,7 @@ impl ConfigLoader for TomlConfigLoader {
.as_ref()
.unwrap()
.iter()
.any(|c| c.cidr == cidr)
.any(|c| c.cidr == cidr && c.mapped_cidr == mapped_cidr)
{
locked_config
.proxy_network

View File

@@ -75,6 +75,8 @@ pub struct GlobalCtx {
no_tun: bool,
feature_flags: AtomicCell<PeerFeatureFlag>,
quic_proxy_port: AtomicCell<Option<u16>>,
}
impl std::fmt::Debug for GlobalCtx {
@@ -137,6 +139,7 @@ impl GlobalCtx {
no_tun,
feature_flags: AtomicCell::new(feature_flags),
quic_proxy_port: AtomicCell::new(None),
}
}
@@ -281,6 +284,14 @@ impl GlobalCtx {
pub fn set_feature_flags(&self, flags: PeerFeatureFlag) {
self.feature_flags.store(flags);
}
pub fn get_quic_proxy_port(&self) -> Option<u16> {
self.quic_proxy_port.load()
}
pub fn set_quic_proxy_port(&self, port: Option<u16>) {
self.quic_proxy_port.store(port);
}
}
#[cfg(test)]

View File

@@ -1083,7 +1083,8 @@ async fn main() -> Result<(), Error> {
.iter()
.map(|(k, v)| format!("{}: {:?}ms", k, v.latency_ms,))
.collect::<Vec<_>>();
let direct_peers: Vec<_> = v.direct_peers
let direct_peers: Vec<_> = v
.direct_peers
.iter()
.map(|(k, v)| DirectPeerItem {
node_id: k.to_string(),
@@ -1257,23 +1258,14 @@ async fn main() -> Result<(), Error> {
}
SubCommand::Proxy => {
let mut entries = vec![];
let client = handler.get_tcp_proxy_client("tcp").await?;
let ret = client
.list_tcp_proxy_entry(BaseController::default(), Default::default())
.await;
entries.extend(ret.unwrap_or_default().entries);
let client = handler.get_tcp_proxy_client("kcp_src").await?;
let ret = client
.list_tcp_proxy_entry(BaseController::default(), Default::default())
.await;
entries.extend(ret.unwrap_or_default().entries);
let client = handler.get_tcp_proxy_client("kcp_dst").await?;
let ret = client
.list_tcp_proxy_entry(BaseController::default(), Default::default())
.await;
entries.extend(ret.unwrap_or_default().entries);
for client_type in &["tcp", "kcp_src", "kcp_dst", "quic_src", "quic_dst"] {
let client = handler.get_tcp_proxy_client(client_type).await?;
let ret = client
.list_tcp_proxy_entry(BaseController::default(), Default::default())
.await;
entries.extend(ret.unwrap_or_default().entries);
}
if cli.verbose {
println!("{}", serde_json::to_string_pretty(&entries)?);

View File

@@ -433,6 +433,24 @@ struct NetworkOptions {
)]
disable_kcp_input: Option<bool>,
#[arg(
long,
env = "ET_ENABLE_QUIC_PROXY",
help = t!("core_clap.enable_quic_proxy").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
enable_quic_proxy: Option<bool>,
#[arg(
long,
env = "ET_DISABLE_QUIC_INPUT",
help = t!("core_clap.disable_quic_input").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
disable_quic_input: Option<bool>,
#[arg(
long,
env = "ET_PORT_FORWARD",
@@ -773,6 +791,8 @@ impl NetworkOptions {
f.bind_device = self.bind_device.unwrap_or(f.bind_device);
f.enable_kcp_proxy = self.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy);
f.disable_kcp_input = self.disable_kcp_input.unwrap_or(f.disable_kcp_input);
f.enable_quic_proxy = self.enable_quic_proxy.unwrap_or(f.enable_quic_proxy);
f.disable_quic_input = self.disable_quic_input.unwrap_or(f.disable_quic_input);
f.accept_dns = self.accept_dns.unwrap_or(f.accept_dns);
f.private_mode = self.private_mode.unwrap_or(f.private_mode);
cfg.set_flags(f);

View File

@@ -124,19 +124,16 @@ impl NatDstConnector for NatDstKcpConnector {
return Err(anyhow::anyhow!("peer manager is not available").into());
};
let (dst_peers, _) = match nat_dst {
SocketAddr::V4(addr) => {
let ip = addr.ip();
peer_mgr.get_msg_dst_peer(&ip).await
}
let dst_peer_id = match nat_dst {
SocketAddr::V4(addr) => peer_mgr.get_peer_map().get_peer_id_by_ipv4(addr.ip()).await,
SocketAddr::V6(_) => return Err(anyhow::anyhow!("ipv6 is not supported").into()),
};
tracing::trace!("kcp nat dst: {:?}, dst peers: {:?}", nat_dst, dst_peers);
let Some(dst_peer) = dst_peer_id else {
return Err(anyhow::anyhow!("no peer found for nat dst: {}", nat_dst).into());
};
if dst_peers.len() != 1 {
return Err(anyhow::anyhow!("no dst peer found for nat dst: {}", nat_dst).into());
}
tracing::trace!("kcp nat dst: {:?}, dst peers: {:?}", nat_dst, dst_peer);
let mut connect_tasks: JoinSet<std::result::Result<ConnId, anyhow::Error>> = JoinSet::new();
let mut retry_remain = 5;
@@ -167,7 +164,6 @@ impl NatDstConnector for NatDstKcpConnector {
let kcp_endpoint = self.kcp_endpoint.clone();
let my_peer_id = peer_mgr.my_peer_id();
let dst_peer = dst_peers[0];
let conn_data_clone = conn_data.clone();
connect_tasks.spawn(async move {
@@ -200,7 +196,7 @@ impl NatDstConnector for NatDstKcpConnector {
_ipv4: &Ipv4Packet,
_real_dst_ip: &mut Ipv4Addr,
) -> bool {
return hdr.from_peer_id == hdr.to_peer_id;
return hdr.from_peer_id == hdr.to_peer_id && hdr.is_kcp_src_modified();
}
fn transport_type(&self) -> TcpProxyEntryTransportType {
@@ -211,32 +207,41 @@ impl NatDstConnector for NatDstKcpConnector {
#[derive(Clone)]
struct TcpProxyForKcpSrc(Arc<TcpProxy<NatDstKcpConnector>>);
pub struct KcpProxySrc {
kcp_endpoint: Arc<KcpEndpoint>,
peer_manager: Arc<PeerManager>,
tcp_proxy: TcpProxyForKcpSrc,
tasks: JoinSet<()>,
#[async_trait::async_trait]
pub(crate) trait TcpProxyForKcpSrcTrait: Send + Sync + 'static {
type Connector: NatDstConnector;
fn get_tcp_proxy(&self) -> &Arc<TcpProxy<Self::Connector>>;
async fn check_dst_allow_kcp_input(&self, dst_ip: &Ipv4Addr) -> bool;
}
impl TcpProxyForKcpSrc {
#[async_trait::async_trait]
impl TcpProxyForKcpSrcTrait for TcpProxyForKcpSrc {
type Connector = NatDstKcpConnector;
fn get_tcp_proxy(&self) -> &Arc<TcpProxy<Self::Connector>> {
&self.0
}
async fn check_dst_allow_kcp_input(&self, dst_ip: &Ipv4Addr) -> bool {
let peer_map: Arc<crate::peers::peer_map::PeerMap> =
self.0.get_peer_manager().get_peer_map();
let Some(dst_peer_id) = peer_map.get_peer_id_by_ipv4(dst_ip).await else {
return false;
};
let Some(feature_flag) = peer_map.get_peer_feature_flag(dst_peer_id).await else {
let Some(peer_info) = peer_map.get_route_peer_info(dst_peer_id).await else {
return false;
};
feature_flag.kcp_input
peer_info.feature_flag.map(|x| x.kcp_input).unwrap_or(false)
}
}
#[async_trait::async_trait]
impl NicPacketFilter for TcpProxyForKcpSrc {
impl<C: NatDstConnector, T: TcpProxyForKcpSrcTrait<Connector = C>> NicPacketFilter for T {
async fn try_process_packet_from_nic(&self, zc_packet: &mut ZCPacket) -> bool {
let ret = self.0.try_process_packet_from_nic(zc_packet).await;
let ret = self
.get_tcp_proxy()
.try_process_packet_from_nic(zc_packet)
.await;
if ret {
return true;
}
@@ -263,29 +268,45 @@ impl NicPacketFilter for TcpProxyForKcpSrc {
}
} else {
// if not syn packet, only allow established connection
if !self.0.is_tcp_proxy_connection(SocketAddr::new(
IpAddr::V4(ip_packet.get_source()),
tcp_packet.get_source(),
)) {
if !self
.get_tcp_proxy()
.is_tcp_proxy_connection(SocketAddr::new(
IpAddr::V4(ip_packet.get_source()),
tcp_packet.get_source(),
))
{
return false;
}
}
if let Some(my_ipv4) = self.0.get_global_ctx().get_ipv4() {
if let Some(my_ipv4) = self.get_tcp_proxy().get_global_ctx().get_ipv4() {
// this is a net-to-net packet, only allow it when smoltcp is enabled
// because the syn-ack packet will not be through and handled by the tun device when
// the source ip is in the local network
if ip_packet.get_source() != my_ipv4.address() && !self.0.is_smoltcp_enabled() {
if ip_packet.get_source() != my_ipv4.address()
&& !self.get_tcp_proxy().is_smoltcp_enabled()
{
return false;
}
};
zc_packet.mut_peer_manager_header().unwrap().to_peer_id = self.0.get_my_peer_id().into();
let hdr = zc_packet.mut_peer_manager_header().unwrap();
hdr.to_peer_id = self.get_tcp_proxy().get_my_peer_id().into();
if self.get_tcp_proxy().get_transport_type() == TcpProxyEntryTransportType::Kcp {
hdr.set_kcp_src_modified(true);
}
true
}
}
pub struct KcpProxySrc {
kcp_endpoint: Arc<KcpEndpoint>,
peer_manager: Arc<PeerManager>,
tcp_proxy: TcpProxyForKcpSrc,
tasks: JoinSet<()>,
}
impl KcpProxySrc {
pub async fn new(peer_manager: Arc<PeerManager>) -> Self {
let mut kcp_endpoint = create_kcp_endpoint();

View File

@@ -18,6 +18,8 @@ pub mod socks5;
pub mod kcp_proxy;
pub mod quic_proxy;
#[derive(Debug)]
pub(crate) struct CidrSet {
global_ctx: ArcGlobalCtx,

View File

@@ -0,0 +1,443 @@
use std::net::{IpAddr, Ipv4Addr};
use std::sync::{Arc, Mutex, Weak};
use std::{net::SocketAddr, pin::Pin};
use anyhow::Context;
use dashmap::DashMap;
use pnet::packet::ipv4::Ipv4Packet;
use prost::Message as _;
use quinn::{Endpoint, Incoming};
use tokio::io::{copy_bidirectional, AsyncRead, AsyncReadExt, AsyncWrite};
use tokio::net::TcpStream;
use tokio::task::JoinSet;
use tokio::time::timeout;
use crate::common::error::Result;
use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtx};
use crate::common::join_joinset_background;
use crate::defer;
use crate::gateway::kcp_proxy::TcpProxyForKcpSrcTrait;
use crate::gateway::tcp_proxy::{NatDstConnector, NatDstTcpConnector, TcpProxy};
use crate::gateway::CidrSet;
use crate::peers::peer_manager::PeerManager;
use crate::proto::cli::{
ListTcpProxyEntryRequest, ListTcpProxyEntryResponse, TcpProxyEntry, TcpProxyEntryState,
TcpProxyEntryTransportType, TcpProxyRpc,
};
use crate::proto::common::ProxyDstInfo;
use crate::proto::rpc_types;
use crate::proto::rpc_types::controller::BaseController;
use crate::tunnel::packet_def::PeerManagerHeader;
use crate::tunnel::quic::{configure_client, make_server_endpoint};
pub struct QUICStream {
endpoint: Option<quinn::Endpoint>,
connection: Option<quinn::Connection>,
sender: quinn::SendStream,
receiver: quinn::RecvStream,
}
impl AsyncRead for QUICStream {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let this = self.get_mut();
Pin::new(&mut this.receiver).poll_read(cx, buf)
}
}
impl AsyncWrite for QUICStream {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
let this = self.get_mut();
AsyncWrite::poll_write(Pin::new(&mut this.sender), cx, buf)
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let this = self.get_mut();
Pin::new(&mut this.sender).poll_flush(cx)
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let this = self.get_mut();
Pin::new(&mut this.sender).poll_shutdown(cx)
}
}
#[derive(Debug, Clone)]
pub struct NatDstQUICConnector {
pub(crate) peer_mgr: Weak<PeerManager>,
}
#[async_trait::async_trait]
impl NatDstConnector for NatDstQUICConnector {
type DstStream = QUICStream;
#[tracing::instrument(skip(self), level = "debug", name = "NatDstQUICConnector::connect")]
async fn connect(&self, src: SocketAddr, nat_dst: SocketAddr) -> Result<Self::DstStream> {
let Some(peer_mgr) = self.peer_mgr.upgrade() else {
return Err(anyhow::anyhow!("peer manager is not available").into());
};
let IpAddr::V4(dst_ipv4) = nat_dst.ip() else {
return Err(anyhow::anyhow!("src must be an IPv4 address").into());
};
let Some(dst_peer) = peer_mgr.get_peer_map().get_peer_id_by_ipv4(&dst_ipv4).await else {
return Err(anyhow::anyhow!("no peer found for dst: {}", nat_dst).into());
};
let Some(dst_peer_info) = peer_mgr.get_peer_map().get_route_peer_info(dst_peer).await
else {
return Err(anyhow::anyhow!("no peer info found for dst peer: {}", dst_peer).into());
};
let Some(dst_ipv4): Option<Ipv4Addr> = dst_peer_info.ipv4_addr.map(Into::into) else {
return Err(anyhow::anyhow!("no ipv4 found for dst peer: {}", dst_peer).into());
};
let Some(quic_port) = dst_peer_info.quic_port else {
return Err(anyhow::anyhow!("no quic port found for dst peer: {}", dst_peer).into());
};
let mut endpoint = Endpoint::client("0.0.0.0:0".parse().unwrap())
.with_context(|| format!("failed to create QUIC endpoint for src: {}", src))?;
endpoint.set_default_client_config(configure_client());
// connect to server
let connection = {
let _g = peer_mgr.get_global_ctx().net_ns.guard();
endpoint
.connect(
SocketAddr::new(dst_ipv4.into(), quic_port as u16),
"localhost",
)
.unwrap()
.await
.with_context(|| {
format!(
"failed to connect to NAT destination {} from {}, real dst: {}",
nat_dst, src, dst_ipv4
)
})?
};
let (mut w, r) = connection
.open_bi()
.await
.with_context(|| "open_bi failed")?;
let proxy_dst_info = ProxyDstInfo {
dst_addr: Some(nat_dst.into()),
};
let proxy_dst_info_buf = proxy_dst_info.encode_to_vec();
let buf_len = proxy_dst_info_buf.len() as u8;
w.write(&buf_len.to_le_bytes())
.await
.with_context(|| "failed to write proxy dst info buf len to QUIC stream")?;
w.write(&proxy_dst_info_buf)
.await
.with_context(|| "failed to write proxy dst info to QUIC stream")?;
Ok(QUICStream {
endpoint: Some(endpoint),
connection: Some(connection),
sender: w,
receiver: r,
})
}
fn check_packet_from_peer_fast(&self, _cidr_set: &CidrSet, _global_ctx: &GlobalCtx) -> bool {
true
}
fn check_packet_from_peer(
&self,
_cidr_set: &CidrSet,
_global_ctx: &GlobalCtx,
hdr: &PeerManagerHeader,
_ipv4: &Ipv4Packet,
_real_dst_ip: &mut Ipv4Addr,
) -> bool {
return hdr.from_peer_id == hdr.to_peer_id && !hdr.is_kcp_src_modified();
}
fn transport_type(&self) -> TcpProxyEntryTransportType {
TcpProxyEntryTransportType::Quic
}
}
#[derive(Clone)]
struct TcpProxyForQUICSrc(Arc<TcpProxy<NatDstQUICConnector>>);
#[async_trait::async_trait]
impl TcpProxyForKcpSrcTrait for TcpProxyForQUICSrc {
type Connector = NatDstQUICConnector;
fn get_tcp_proxy(&self) -> &Arc<TcpProxy<Self::Connector>> {
&self.0
}
async fn check_dst_allow_kcp_input(&self, dst_ip: &Ipv4Addr) -> bool {
let peer_map: Arc<crate::peers::peer_map::PeerMap> =
self.0.get_peer_manager().get_peer_map();
let Some(dst_peer_id) = peer_map.get_peer_id_by_ipv4(dst_ip).await else {
return false;
};
let Some(peer_info) = peer_map.get_route_peer_info(dst_peer_id).await else {
return false;
};
let Some(quic_port) = peer_info.quic_port else {
return false;
};
quic_port > 0
}
}
pub struct QUICProxySrc {
peer_manager: Arc<PeerManager>,
tcp_proxy: TcpProxyForQUICSrc,
}
impl QUICProxySrc {
pub async fn new(peer_manager: Arc<PeerManager>) -> Self {
let tcp_proxy = TcpProxy::new(
peer_manager.clone(),
NatDstQUICConnector {
peer_mgr: Arc::downgrade(&peer_manager),
},
);
Self {
peer_manager,
tcp_proxy: TcpProxyForQUICSrc(tcp_proxy),
}
}
pub async fn start(&self) {
self.peer_manager
.add_nic_packet_process_pipeline(Box::new(self.tcp_proxy.clone()))
.await;
self.peer_manager
.add_packet_process_pipeline(Box::new(self.tcp_proxy.0.clone()))
.await;
self.tcp_proxy.0.start(false).await.unwrap();
}
pub fn get_tcp_proxy(&self) -> Arc<TcpProxy<NatDstQUICConnector>> {
self.tcp_proxy.0.clone()
}
}
pub struct QUICProxyDst {
global_ctx: Arc<GlobalCtx>,
endpoint: Arc<quinn::Endpoint>,
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
tasks: Arc<Mutex<JoinSet<()>>>,
}
impl QUICProxyDst {
pub fn new(global_ctx: ArcGlobalCtx) -> Result<Self> {
let _g = global_ctx.net_ns.guard();
let (endpoint, _) = make_server_endpoint("0.0.0.0:0".parse().unwrap())
.map_err(|e| anyhow::anyhow!("failed to create QUIC endpoint: {}", e))?;
let tasks = Arc::new(Mutex::new(JoinSet::new()));
join_joinset_background(tasks.clone(), "QUICProxyDst tasks".to_string());
Ok(Self {
global_ctx,
endpoint: Arc::new(endpoint),
proxy_entries: Arc::new(DashMap::new()),
tasks,
})
}
pub async fn start(&self) -> Result<()> {
let endpoint = self.endpoint.clone();
let tasks = Arc::downgrade(&self.tasks.clone());
let ctx = self.global_ctx.clone();
let cidr_set = Arc::new(CidrSet::new(ctx.clone()));
let proxy_entries = self.proxy_entries.clone();
let task = async move {
loop {
match endpoint.accept().await {
Some(conn) => {
let Some(tasks) = tasks.upgrade() else {
tracing::warn!(
"QUICProxyDst tasks is not available, stopping accept loop"
);
return;
};
tasks
.lock()
.unwrap()
.spawn(Self::handle_connection_with_timeout(
conn,
ctx.clone(),
cidr_set.clone(),
proxy_entries.clone(),
));
}
None => {
return;
}
}
}
};
self.tasks.lock().unwrap().spawn(task);
Ok(())
}
pub fn local_addr(&self) -> Result<SocketAddr> {
self.endpoint.local_addr().map_err(Into::into)
}
async fn handle_connection_with_timeout(
conn: Incoming,
ctx: Arc<GlobalCtx>,
cidr_set: Arc<CidrSet>,
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
) {
let remote_addr = conn.remote_address();
defer!(
proxy_entries.remove(&remote_addr);
);
let ret = timeout(
std::time::Duration::from_secs(10),
Self::handle_connection(conn, ctx, cidr_set, remote_addr, proxy_entries.clone()),
)
.await;
match ret {
Ok(Ok((mut quic_stream, mut tcp_stream))) => {
let ret = copy_bidirectional(&mut quic_stream, &mut tcp_stream).await;
tracing::info!(
"QUIC connection handled, result: {:?}, remote addr: {:?}",
ret,
quic_stream.connection.as_ref().map(|c| c.remote_address())
);
}
Ok(Err(e)) => {
tracing::error!("Failed to handle QUIC connection: {}", e);
}
Err(_) => {
tracing::warn!("Timeout while handling QUIC connection");
}
}
}
async fn handle_connection(
incoming: Incoming,
ctx: ArcGlobalCtx,
cidr_set: Arc<CidrSet>,
proxy_entry_key: SocketAddr,
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
) -> Result<(QUICStream, TcpStream)> {
let conn = incoming.await.with_context(|| "accept failed")?;
let addr = conn.remote_address();
tracing::info!("Accepted QUIC connection from {}", addr);
let (w, mut r) = conn.accept_bi().await.with_context(|| "accept_bi failed")?;
let len = r
.read_u8()
.await
.with_context(|| "failed to read proxy dst info buf len")?;
let mut buf = vec![0u8; len as usize];
r.read_exact(&mut buf)
.await
.with_context(|| "failed to read proxy dst info")?;
let proxy_dst_info =
ProxyDstInfo::decode(&buf[..]).with_context(|| "failed to decode proxy dst info")?;
let dst_socket: SocketAddr = proxy_dst_info
.dst_addr
.map(Into::into)
.ok_or_else(|| anyhow::anyhow!("no dst addr in proxy dst info"))?;
let SocketAddr::V4(mut dst_socket) = dst_socket else {
return Err(anyhow::anyhow!("NAT destination must be an IPv4 address").into());
};
let mut real_ip = *dst_socket.ip();
if cidr_set.contains_v4(*dst_socket.ip(), &mut real_ip) {
dst_socket.set_ip(real_ip);
}
if Some(*dst_socket.ip()) == ctx.get_ipv4().map(|ip| ip.address()) && ctx.no_tun() {
dst_socket = format!("127.0.0.1:{}", dst_socket.port()).parse().unwrap();
}
proxy_entries.insert(
proxy_entry_key,
TcpProxyEntry {
src: Some(addr.into()),
dst: Some(SocketAddr::V4(dst_socket).into()),
start_time: chrono::Local::now().timestamp() as u64,
state: TcpProxyEntryState::ConnectingDst.into(),
transport_type: TcpProxyEntryTransportType::Quic.into(),
},
);
let connector = NatDstTcpConnector {};
let dst_stream = {
let _g = ctx.net_ns.guard();
connector
.connect("0.0.0.0:0".parse().unwrap(), dst_socket.into())
.await?
};
if let Some(mut e) = proxy_entries.get_mut(&proxy_entry_key) {
e.state = TcpProxyEntryState::Connected.into();
}
let quic_stream = QUICStream {
endpoint: None,
connection: Some(conn),
sender: w,
receiver: r,
};
Ok((quic_stream, dst_stream))
}
}
#[derive(Clone)]
pub struct QUICProxyDstRpcService(Weak<DashMap<SocketAddr, TcpProxyEntry>>);
impl QUICProxyDstRpcService {
pub fn new(quic_proxy_dst: &QUICProxyDst) -> Self {
Self(Arc::downgrade(&quic_proxy_dst.proxy_entries))
}
}
#[async_trait::async_trait]
impl TcpProxyRpc for QUICProxyDstRpcService {
type Controller = BaseController;
async fn list_tcp_proxy_entry(
&self,
_: BaseController,
_request: ListTcpProxyEntryRequest, // Accept request of type HelloRequest
) -> std::result::Result<ListTcpProxyEntryResponse, rpc_types::error::Error> {
let mut reply = ListTcpProxyEntryResponse::default();
if let Some(tcp_proxy) = self.0.upgrade() {
for item in tcp_proxy.iter() {
reply.entries.push(item.value().clone());
}
}
Ok(reply)
}
}

View File

@@ -902,6 +902,10 @@ impl<C: NatDstConnector> TcpProxy<C> {
}
entries
}
pub fn get_transport_type(&self) -> TcpProxyEntryTransportType {
self.connector.transport_type()
}
}
#[derive(Clone)]

View File

@@ -20,6 +20,7 @@ use crate::connector::manual::{ConnectorManagerRpcService, ManualConnectorManage
use crate::connector::udp_hole_punch::UdpHolePunchConnector;
use crate::gateway::icmp_proxy::IcmpProxy;
use crate::gateway::kcp_proxy::{KcpProxyDst, KcpProxyDstRpcService, KcpProxySrc};
use crate::gateway::quic_proxy::{QUICProxyDst, QUICProxyDstRpcService, QUICProxySrc};
use crate::gateway::tcp_proxy::{NatDstTcpConnector, TcpProxy, TcpProxyRpcService};
use crate::gateway::udp_proxy::UdpProxy;
use crate::peer_center::instance::PeerCenterInstance;
@@ -232,6 +233,9 @@ pub struct Instance {
kcp_proxy_src: Option<KcpProxySrc>,
kcp_proxy_dst: Option<KcpProxyDst>,
quic_proxy_src: Option<QUICProxySrc>,
quic_proxy_dst: Option<QUICProxyDst>,
peer_center: Arc<PeerCenterInstance>,
vpn_portal: Arc<Mutex<Box<dyn VpnPortal>>>,
@@ -312,6 +316,9 @@ impl Instance {
kcp_proxy_src: None,
kcp_proxy_dst: None,
quic_proxy_src: None,
quic_proxy_dst: None,
peer_center,
vpn_portal: Arc::new(Mutex::new(Box::new(vpn_portal_inst))),
@@ -562,6 +569,20 @@ impl Instance {
self.kcp_proxy_dst = Some(dst_proxy);
}
if self.global_ctx.get_flags().enable_quic_proxy {
let quic_src = QUICProxySrc::new(self.get_peer_manager()).await;
quic_src.start().await;
self.quic_proxy_src = Some(quic_src);
}
if !self.global_ctx.get_flags().disable_quic_input {
let quic_dst = QUICProxyDst::new(self.global_ctx.clone())?;
quic_dst.start().await?;
self.global_ctx
.set_quic_proxy_port(Some(quic_dst.local_addr()?.port()));
self.quic_proxy_dst = Some(quic_dst);
}
// run after tun device created, so listener can bind to tun device, which may be required by win 10
self.ip_proxy = Some(IpProxy::new(
self.get_global_ctx(),
@@ -737,6 +758,20 @@ impl Instance {
);
}
if let Some(quic_proxy) = self.quic_proxy_src.as_ref() {
s.registry().register(
TcpProxyRpcServer::new(TcpProxyRpcService::new(quic_proxy.get_tcp_proxy())),
"quic_src",
);
}
if let Some(quic_proxy) = self.quic_proxy_dst.as_ref() {
s.registry().register(
TcpProxyRpcServer::new(QUICProxyDstRpcService::new(quic_proxy)),
"quic_dst",
);
}
s.set_hook(Arc::new(InstanceRpcServerHook::new(
self.global_ctx.config.get_rpc_portal_whitelist(),
)));

View File

@@ -142,6 +142,8 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
if self.global_ctx.config.get_flags().enable_ipv6
&& !is_url_host_ipv6(&l)
&& is_url_host_unspecified(&l)
// quic enables dual-stack by default, may conflict with v4 listener
&& l.scheme() != "quic"
{
let mut ipv6_listener = l.clone();
ipv6_listener

View File

@@ -685,6 +685,14 @@ impl NetworkConfig {
flags.disable_kcp_input = disable_kcp_input;
}
if let Some(enable_quic_proxy) = self.enable_quic_proxy {
flags.enable_quic_proxy = enable_quic_proxy;
}
if let Some(disable_quic_input) = self.disable_quic_input {
flags.disable_quic_input = disable_quic_input;
}
if let Some(disable_p2p) = self.disable_p2p {
flags.disable_p2p = disable_p2p;
}

View File

@@ -10,7 +10,7 @@ use crate::{
global_ctx::{ArcGlobalCtx, GlobalCtxEvent, NetworkIdentity},
PeerId,
},
proto::{cli::PeerConnInfo, common::PeerFeatureFlag},
proto::{cli::PeerConnInfo, peer_rpc::RoutePeerInfo},
tunnel::{packet_def::ZCPacket, TunnelError},
};
@@ -194,12 +194,11 @@ impl PeerMap {
None
}
pub async fn get_peer_feature_flag(&self, peer_id: PeerId) -> Option<PeerFeatureFlag> {
pub async fn get_route_peer_info(&self, peer_id: PeerId) -> Option<RoutePeerInfo> {
for route in self.routes.read().await.iter() {
let feature_flag = route.get_feature_flag(peer_id).await;
if feature_flag.is_some() {
return feature_flag;
};
if let Some(info) = route.get_peer_info(peer_id).await {
return Some(info);
}
}
None
}

View File

@@ -33,7 +33,7 @@ use crate::{
},
peers::route_trait::{Route, RouteInterfaceBox},
proto::{
common::{Ipv4Inet, NatType, PeerFeatureFlag, StunInfo},
common::{Ipv4Inet, NatType, StunInfo},
peer_rpc::{
route_foreign_network_infos, ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey,
OspfRouteRpc, OspfRouteRpcClientFactory, OspfRouteRpcServer, PeerIdVersion,
@@ -124,6 +124,7 @@ impl RoutePeerInfo {
feature_flag: None,
peer_route_id: 0,
network_length: 24,
quic_port: None,
}
}
@@ -162,6 +163,8 @@ impl RoutePeerInfo {
.get_ipv4()
.map(|x| x.network_length() as u32)
.unwrap_or(24),
quic_port: global_ctx.get_quic_proxy_port().map(|x| x as u32),
};
let need_update_periodically = if let Ok(Ok(d)) =
@@ -2317,12 +2320,12 @@ impl Route for PeerRoute {
.map(|x| *x)
}
async fn get_feature_flag(&self, peer_id: PeerId) -> Option<PeerFeatureFlag> {
async fn get_peer_info(&self, peer_id: PeerId) -> Option<RoutePeerInfo> {
self.service_impl
.route_table
.peer_infos
.get(&peer_id)
.and_then(|x| x.feature_flag.clone())
.map(|x| x.clone())
}
async fn get_peer_info_last_update_time(&self) -> Instant {

View File

@@ -4,11 +4,9 @@ use dashmap::DashMap;
use crate::{
common::{global_ctx::NetworkIdentity, PeerId},
proto::{
common::PeerFeatureFlag,
peer_rpc::{
ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, RouteForeignNetworkInfos,
},
proto::peer_rpc::{
ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, RouteForeignNetworkInfos,
RoutePeerInfo,
},
};
@@ -107,7 +105,7 @@ pub trait Route {
async fn set_route_cost_fn(&self, _cost_fn: RouteCostCalculator) {}
async fn get_feature_flag(&self, peer_id: PeerId) -> Option<PeerFeatureFlag>;
async fn get_peer_info(&self, peer_id: PeerId) -> Option<RoutePeerInfo>;
async fn get_peer_info_last_update_time(&self) -> std::time::Instant;

View File

@@ -187,6 +187,7 @@ service VpnPortalRpc {
enum TcpProxyEntryTransportType {
TCP = 0;
KCP = 1;
QUIC = 2;
}
enum TcpProxyEntryState {

View File

@@ -35,6 +35,11 @@ message FlagsInConfig {
bool accept_dns = 22;
// enable private mode
bool private_mode = 23;
// should we convert all tcp streams into quic streams
bool enable_quic_proxy = 24;
// does this peer allow quic input
bool disable_quic_input = 25;
}
message RpcDescriptor {
@@ -171,3 +176,7 @@ message PortForwardConfigPb {
SocketAddr dst_addr = 2;
SocketType socket_type = 3;
}
message ProxyDstInfo {
SocketAddr dst_addr = 1;
}

View File

@@ -22,6 +22,8 @@ message RoutePeerInfo {
uint64 peer_route_id = 12;
uint32 network_length = 13;
optional uint32 quic_port = 14;
}
message PeerIdVersion {

View File

@@ -68,6 +68,9 @@ message NetworkConfig {
optional bool enable_private_mode = 43;
repeated string rpc_portal_whitelists = 44;
optional bool enable_quic_proxy = 45;
optional bool disable_quic_input = 46;
}
message MyNodeInfo {

View File

@@ -222,6 +222,8 @@ async fn ping_test(from_netns: &str, target_ip: &str, payload_size: Option<usize
"1",
target_ip.to_string().as_str(),
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.unwrap();
@@ -391,34 +393,66 @@ async fn subnet_proxy_test_icmp(target_ip: &str) {
.await;
}
#[tokio::test]
pub async fn quic_proxy() {
let insts = init_three_node_ex(
"udp",
|cfg| {
if cfg.get_inst_name() == "inst3" {
cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap(), None);
}
cfg
},
false,
)
.await;
assert_eq!(insts[2].get_global_ctx().config.get_proxy_cidrs().len(), 1);
wait_proxy_route_appear(
&insts[0].get_peer_manager(),
"10.144.144.3/24",
insts[2].peer_id(),
"10.1.2.0/24",
)
.await;
let target_ip = "10.1.2.4";
subnet_proxy_test_icmp(target_ip).await;
subnet_proxy_test_tcp(target_ip).await;
drop_insts(insts).await;
}
#[rstest::rstest]
#[serial_test::serial]
#[tokio::test]
pub async fn subnet_proxy_three_node_test(
#[values("tcp", "udp", "wg")] proto: &str,
#[values(true, false)] no_tun: bool,
#[values(true, false)] relay_by_public_server: bool,
#[values(true, false)] enable_kcp_proxy: bool,
#[values(true, false)] enable_quic_proxy: bool,
#[values(true, false)] disable_kcp_input: bool,
#[values(true, false)] disable_quic_input: bool,
#[values(true, false)] dst_enable_kcp_proxy: bool,
#[values(true, false)] test_mapped_cidr: bool,
#[values(true, false)] dst_enable_quic_proxy: bool,
) {
let insts = init_three_node_ex(
proto,
"udp",
|cfg| {
if cfg.get_inst_name() == "inst3" {
let mut flags = cfg.get_flags();
flags.no_tun = no_tun;
flags.disable_kcp_input = disable_kcp_input;
flags.enable_kcp_proxy = dst_enable_kcp_proxy;
flags.disable_quic_input = disable_quic_input;
flags.enable_quic_proxy = dst_enable_quic_proxy;
cfg.set_flags(flags);
cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap(), None);
cfg.add_proxy_cidr(
"10.1.2.0/24".parse().unwrap(),
if test_mapped_cidr {
Some("10.1.3.0/24".parse().unwrap())
} else {
None
},
Some("10.1.3.0/24".parse().unwrap()),
);
}
@@ -429,9 +463,14 @@ pub async fn subnet_proxy_three_node_test(
));
}
if cfg.get_inst_name() == "inst1" && enable_kcp_proxy {
if cfg.get_inst_name() == "inst1" {
let mut flags = cfg.get_flags();
flags.enable_kcp_proxy = true;
if enable_kcp_proxy {
flags.enable_kcp_proxy = true;
}
if enable_quic_proxy {
flags.enable_quic_proxy = true;
}
cfg.set_flags(flags);
}
@@ -441,29 +480,28 @@ pub async fn subnet_proxy_three_node_test(
)
.await;
assert_eq!(insts[2].get_global_ctx().config.get_proxy_cidrs().len(), 1);
assert_eq!(insts[2].get_global_ctx().config.get_proxy_cidrs().len(), 2);
wait_proxy_route_appear(
&insts[0].get_peer_manager(),
"10.144.144.3/24",
insts[2].peer_id(),
if test_mapped_cidr {
"10.1.3.0/24"
} else {
"10.1.2.0/24"
},
"10.1.2.0/24",
)
.await;
wait_proxy_route_appear(
&insts[0].get_peer_manager(),
"10.144.144.3/24",
insts[2].peer_id(),
"10.1.3.0/24",
)
.await;
let target_ip = if test_mapped_cidr {
"10.1.3.4"
} else {
"10.1.2.4"
};
subnet_proxy_test_icmp(target_ip).await;
subnet_proxy_test_tcp(target_ip).await;
subnet_proxy_test_udp(target_ip).await;
for target_ip in ["10.1.3.4", "10.1.2.4"].iter() {
subnet_proxy_test_icmp(target_ip).await;
subnet_proxy_test_tcp(target_ip).await;
subnet_proxy_test_udp(target_ip).await;
}
drop_insts(insts).await;
}

View File

@@ -81,6 +81,7 @@ bitflags::bitflags! {
const EXIT_NODE = 0b0000_0100;
const NO_PROXY = 0b0000_1000;
const COMPRESSED = 0b0001_0000;
const KCP_SRC_MODIFIED = 0b0010_0000;
const _ = !0;
}
@@ -183,6 +184,23 @@ impl PeerManagerHeader {
self.flags = flags.bits();
self
}
pub fn set_kcp_src_modified(&mut self, modified: bool) -> &mut Self {
let mut flags = PeerManagerHeaderFlags::from_bits(self.flags).unwrap();
if modified {
flags.insert(PeerManagerHeaderFlags::KCP_SRC_MODIFIED);
} else {
flags.remove(PeerManagerHeaderFlags::KCP_SRC_MODIFIED);
}
self.flags = flags.bits();
self
}
pub fn is_kcp_src_modified(&self) -> bool {
PeerManagerHeaderFlags::from_bits(self.flags)
.unwrap()
.contains(PeerManagerHeaderFlags::KCP_SRC_MODIFIED)
}
}
#[repr(C, packed)]

View File

@@ -17,7 +17,7 @@ use super::{
IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelListener,
};
fn configure_client() -> ClientConfig {
pub fn configure_client() -> ClientConfig {
ClientConfig::new(Arc::new(
QuicClientConfig::try_from(get_insecure_tls_client_config()).unwrap(),
))
@@ -38,7 +38,7 @@ pub fn make_server_endpoint(bind_addr: SocketAddr) -> Result<(Endpoint, Vec<u8>)
}
/// Returns default server configuration along with its certificate.
fn configure_server() -> Result<(ServerConfig, Vec<u8>), Box<dyn Error>> {
pub fn configure_server() -> Result<(ServerConfig, Vec<u8>), Box<dyn Error>> {
let (certs, key) = get_insecure_tls_cert();
let mut server_config = ServerConfig::with_single_cert(certs.clone(), key.into())?;