Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f009928
feat(tuic-server): wire GeoIP/GeoSite database into routing end-to-end
Itsusinn Jul 2, 2026
ab2e43c
fix(wind-quic): surface quiche stream resets as errors and propagate …
Itsusinn Jul 2, 2026
34df770
fix(wind): include wind_acl/wind_geodata/wind_quic in the log filter
Itsusinn Jul 2, 2026
21c7a60
chore(wind): remove dead target_addr_to_socket_addr util
Itsusinn Jul 2, 2026
70242bd
chore(tuic-server): remove unused compat::QuicClient
Itsusinn Jul 2, 2026
964bd0d
fix(wind-naive): derive SNI correctly from a bracketed IPv6 server ad…
Itsusinn Jul 2, 2026
41904b5
fix(tuic-client): parse IPv6 server addresses correctly, reject unbra…
Itsusinn Jul 2, 2026
dfaaa98
fix(tuic-client): return an error on double socks5 set_config instead…
Itsusinn Jul 2, 2026
4692198
fix(tuic-client): update log filter targets to the wind-tuic split
Itsusinn Jul 2, 2026
452c359
fix(wind-geodata): write the cache file atomically
Itsusinn Jul 2, 2026
93da068
fix(wind-acme): regenerate a self-signed cert when it has expired
Itsusinn Jul 2, 2026
8378160
fix(wind-acme): bind the HTTP-01 challenge server dual-stack
Itsusinn Jul 2, 2026
57934b7
style: cargo +nightly fmt
Itsusinn Jul 2, 2026
68c1d9a
style: reword comment to satisfy the typos check
Itsusinn Jul 2, 2026
bc518ca
test(tuic-tests): isolate client-using integration tests into their o…
Itsusinn Jul 2, 2026
350d254
fix(tuic-client): only set IPV6_V6ONLY on IPv6 UDP-associate sockets
Itsusinn Jul 2, 2026
9a6c9e3
fix(wind-quic): prevent a clean FIN from racing a quiche stream reset
Itsusinn Jul 2, 2026
523792e
ci: replace musl Linux targets with glibc + static-CRT
Itsusinn Jul 2, 2026
8bed8e2
test(wind-quic): skip quiche bulk-transfer on 32-bit (quiche PRR panics)
Itsusinn Jul 2, 2026
091a161
ci: shrink sccache to 400M to avoid runner disk exhaustion
Itsusinn Jul 2, 2026
a212e7d
Revert "ci: replace musl Linux targets with glibc + static-CRT"
Itsusinn Jul 2, 2026
3f5e948
fix(deps): pin quinn fork to ce60e5b5 to fix the musl build
Itsusinn Jul 2, 2026
2ded734
test(wind-quic): scope quiche bulk-transfer to x86_64
Itsusinn Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ jobs:
rustflags: "--cfg reqwest_unstable"
enable-tmate: false
only-clippy-tests-on-pr: false
# GitHub caps the per-repo Actions cache at 10 GB and each matrix target
# keeps its own sccache directory, so size each one at 10 GB / number of
# active targets in .github/target.toml (currently 12) ≈ 850M. Re-derive
# when targets are added or removed.
sccache-max-size: "850M"
# Two constraints:
# 1. GitHub caps the per-repo Actions cache at 10 GB, shared across all
# matrix targets (currently 12) → 10 GB / 12 ≈ 850M each.
# 2. The sccache directory also consumes runner-local disk *during* the
# build, on top of the (large) `target/` tree. The release build with
# the `quiche`/boringssl stack was exhausting the ~14 GB runner disk
# ("No space left on device") at 850M, so keep it well below the cache
# cap to leave build headroom.
# Re-derive if targets are added/removed or the dependency footprint grows.
sccache-max-size: "400M"

release:
name: Release
Expand Down
53 changes: 27 additions & 26 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ license = "MIT OR Apache-2.0"


[workspace.dependencies]
# NOTE: the `bbrv3` branch is intentionally pinned in Cargo.lock to the rev
# ce60e5b5 (the tip *before* the branch was rebased onto a newer upstream quinn).
# That newer upstream's `quinn-udp` receive-timestamps code (RECVERR cmsg
# handling) fails to compile on musl targets (a `u32`/`usize` mismatch on
# `cmsg_len`). Keeping the `branch` spec (rather than a `rev`) lets cargo unify
# this with `quinn-congestions`, which also depends on the fork via `bbrv3`;
# `cargo update` on quinn must re-pin with `--precise ce60e5b5...` until the
# fork's musl build is fixed upstream.
quinn = { branch = "bbrv3", git = "https://github.com/Tipuch/quinn.git", default-features = false }
quinn-proto = { branch = "bbrv3", git = "https://github.com/Tipuch/quinn.git", default-features = false }
quinn-congestions = { git = "https://github.com/proxy-rs/quinn-congestions.git", default-features = false }
Expand Down
50 changes: 41 additions & 9 deletions crates/tuic-client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,19 +359,31 @@ pub fn deserialize_server<'de, D>(deserializer: D) -> Result<(String, u16), D::E
where
D: Deserializer<'de>,
{
let mut s = String::deserialize(deserializer)?;

let (domain, port) = s.rsplit_once(':').ok_or(DeError::custom("invalid server address"))?;
let s = String::deserialize(deserializer)?;

let port = port.parse().map_err(DeError::custom)?;
s.truncate(domain.len());
// Bracketed IPv6: "[host]:port" — the host may itself contain colons.
if let Some(rest) = s.strip_prefix('[') {
let (host, after) = rest
.split_once(']')
.ok_or_else(|| DeError::custom("unterminated '[' in IPv6 server address"))?;
let port = after
.strip_prefix(':')
.ok_or_else(|| DeError::custom("expected ':port' after ']' in server address"))?;
let port = port.parse().map_err(DeError::custom)?;
return Ok((host.to_string(), port));
}

// Strip brackets from IPv6 addresses (e.g., "[::1]" -> "::1")
if s.starts_with('[') && s.ends_with(']') {
s = s[1..s.len() - 1].to_string();
let (host, port) = s
.rsplit_once(':')
.ok_or_else(|| DeError::custom("server address must be 'host:port'"))?;
// A leftover colon in the host means an unbracketed IPv6 literal, which is
// ambiguous (`rsplit_once` would treat part of the address as the port).
if host.contains(':') {
return Err(DeError::custom("IPv6 server address must be bracketed as '[addr]:port'"));
}
let port = port.parse().map_err(DeError::custom)?;

Ok((s, port))
Ok((host.to_string(), port))
}

pub fn deserialize_password<'de, D>(deserializer: D) -> Result<Arc<[u8]>, D::Error>
Expand Down Expand Up @@ -444,6 +456,26 @@ impl From<toml::de::Error> for ConfigError {
mod tests {
use super::*;

fn parse_server(s: &str) -> Result<(String, u16), serde::de::value::Error> {
use serde::de::IntoDeserializer;
deserialize_server(s.into_deserializer())
}

#[test]
fn deserialize_server_handles_ipv6_domains_and_rejects_ambiguous() {
assert_eq!(parse_server("example.com:443").unwrap(), ("example.com".to_string(), 443));
assert_eq!(parse_server("1.2.3.4:8443").unwrap(), ("1.2.3.4".to_string(), 8443));
assert_eq!(parse_server("[2001:db8::1]:443").unwrap(), ("2001:db8::1".to_string(), 443));
assert_eq!(parse_server("[::1]:8443").unwrap(), ("::1".to_string(), 8443));

// Unbracketed IPv6 is ambiguous and must be rejected rather than split wrong.
assert!(parse_server("2001:db8::1").is_err());
assert!(parse_server("::1").is_err());
// Missing port / malformed.
assert!(parse_server("example.com").is_err());
assert!(parse_server("[2001:db8::1]").is_err());
}

// Helper function for testing config file parsing
fn test_parse_config(config_content: &str, extension: &str) -> eyre::Result<Config> {
test_parse_config_with_env(config_content, extension, EnvState::default())
Expand Down
15 changes: 14 additions & 1 deletion crates/tuic-client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,21 @@ async fn main() -> eyre::Result<()> {
}
};
let level = tracing::Level::from_str(&cfg.log_level)?;
// Cover the crates and custom targets the client actually logs under. The
// old list (`tuic`, `tuic_quinn`) predated the wind-tuic split, so relay
// debug (emitted under `wind_tuic` / the `tuic_out` and `udp` targets) fell
// through to the default INFO filter and `log_level = "debug"` was
// ineffective for it.
let filter = tracing_subscriber::filter::Targets::new()
.with_targets(vec![("tuic", level), ("tuic_quinn", level), ("tuic_client", level)])
.with_targets(vec![
("tuic_client", level),
("tuic_core", level),
("tuic_out", level),
("udp", level),
("wind_core", level),
("wind_quic", level),
("wind_tuic", level),
])
.with_default(LevelFilter::INFO);
let registry = tracing_subscriber::registry();
registry
Expand Down
5 changes: 3 additions & 2 deletions crates/tuic-client/src/socks5/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ impl Server {
cfg.username,
cfg.password,
)?)
.map_err(|_| "failed initializing socks5 server")
.unwrap();
// Called more than once (SERVER already initialized): return an error
// instead of `unwrap()`-panicking the caller.
.map_err(|_| Error::Socks5("socks5 server already initialized".to_string()))?;

Ok(())
}
Expand Down
8 changes: 7 additions & 1 deletion crates/tuic-client/src/socks5/udp_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ impl UdpSession {
let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))
.map_err(|err| Error::Socket("failed to create socks5 server UDP associate socket", err))?;

if let Some(dual_stack) = dual_stack {
// `IPV6_V6ONLY` only exists on IPv6 sockets. Setting it on an IPv4 socket
// fails with ENOPROTOOPT ("Protocol not available"), which broke every
// UDP ASSOCIATE on an IPv4 SOCKS5 listener. Mirror `Server::new`: only
// apply the dual-stack option when this socket is actually IPv6.
if local_ip.is_ipv6()
&& let Some(dual_stack) = dual_stack
{
socket
.set_only_v6(!dual_stack)
.map_err(|err| Error::Socket("socks5 server UDP associate dual-stack socket setting error", err))?;
Expand Down
4 changes: 3 additions & 1 deletion crates/tuic-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ wind-socks = { path = "../wind-socks" }
wind-tuic = { path = "../wind-tuic", features = ["server"] }
wind-acme = { path = "../wind-acme" }
wind-acl = { path = "../wind-acl" }
wind-geodata = { path = "../wind-geodata" }

toml = "1.0"
clap = { version = "4", features = ["derive"] }
Expand Down Expand Up @@ -91,4 +92,5 @@ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"
ip_network = "0.4"
tempfile = "3"
tokio = { version = "1", features = ["full", "test-util"] }
tokio-test = "0.4"
tokio-test = "0.4"
geosite-rs = "0.1.6"
Loading
Loading