feat(wind-tuic): add SimpleUdpChannel to replace AbstractUdpSocket#5
feat(wind-tuic): add SimpleUdpChannel to replace AbstractUdpSocket#5
Conversation
This commit introduces a simplified UDP abstraction to replace the complex AbstractUdpSocket trait from wind-core. The new implementation provides: - SimpleUdpPacket: Basic UDP packet structure (source, target, payload) - SimpleUdpChannel: Bidirectional channel for packet transmission - SimpleUdpChannelTx: Transmitter side (supports Clone) - handle_udp_simple(): New method in TuicOutbound for easy UDP handling Benefits: - Much simpler API compared to AbstractUdpSocket trait - Uses standard crossfire channels instead of custom traits - Easier integration with existing async code - Reduced complexity for UDP relay implementations The implementation maintains compatibility with existing UdpStream while providing a more ergonomic interface for UDP packet handling.
There was a problem hiding this comment.
Pull request overview
This PR introduces a simplified UDP integration surface for wind-tuic by adding channel-based UDP packet types, and wires TUIC outbound UDP handling toward that new abstraction. It also includes several formatting/cleanup tweaks, a UDP fragmentation fix, and CI/workspace manifest adjustments.
Changes:
- Add
wind_tuic::simple_udpmodule (SimpleUdpPacket,SimpleUdpChannel,SimpleUdpChannelTx) and export it fromwind-tuic. - Add
TuicOutbound::handle_udp_simple()intended to bridge TUIC UDP reassembly into a bounded async channel. - Adjust QUIC/UDP plumbing and manifests (e.g.,
UdpPolleravailability, quinn config), plus minor proto formatting and afrag_totaltype coercion fix.
Reviewed changes
Copilot reviewed 14 out of 19 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/wind/Cargo.toml | Switches from workspace-inherited package metadata/dependency versions to explicit values / path-only deps. |
| crates/wind-tuic/src/task.rs | Whitespace-only formatting change. |
| crates/wind-tuic/src/simple_udp.rs | Adds new simplified UDP channel + packet types. |
| crates/wind-tuic/src/proto/udp_stream.rs | Fixes fragment count comparison type (u64::from(...)) and various formatting cleanups. |
| crates/wind-tuic/src/proto/tests.rs | Formatting-only change in test struct literal. |
| crates/wind-tuic/src/proto/mod.rs | Improves decode helper chaining/formatting (and keeps “incomplete” error context). |
| crates/wind-tuic/src/proto/header.rs | Formatting-only changes and expanded ensure! calls. |
| crates/wind-tuic/src/proto/error.rs | Formatting-only changes to error fields. |
| crates/wind-tuic/src/proto/cmd.rs | Formatting-only changes to command fields/decoding/tests. |
| crates/wind-tuic/src/proto/addr.rs | Formatting-only changes and expanded ensure! call. |
| crates/wind-tuic/src/outbound.rs | Adds handle_udp_simple(), changes congestion controller config, minor formatting adjustments. |
| crates/wind-tuic/src/lib.rs | Exposes pub mod simple_udp;. |
| crates/wind-tuic/src/inbound.rs | Formatting-only changes (whitespace/alignment). |
| crates/wind-tuic/Cargo.toml | Moves to explicit package metadata, switches quinn dependency to workspace = true and adds qlog. |
| crates/wind-socks/Cargo.toml | Moves to explicit package metadata; path-only dep on wind-core. |
| crates/wind-core/src/udp.rs | Makes UdpPoller trait unconditional (removes feature guards) and formatting cleanups. |
| crates/wind-core/Cargo.toml | Moves to explicit package metadata; switches quinn to workspace = true. |
| .github/workflows/ci.yml | Adds CI via reusable workflow. |
| .github/target.toml | Adds CI target matrix configuration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.ctx.tasks.spawn(async move { | ||
| loop { | ||
| tokio::select! { | ||
| _ = cancel.cancelled() => { | ||
| break; |
There was a problem hiding this comment.
The spawned task exits on cancellation / channel close but doesn’t perform any cleanup: udp_session entry removal, fragment GC ticking, or connection.drop_udp(assoc_id)/udp_stream.close(). This can leak UDP associations and fragment state over time; mirror the cleanup/GC behavior from handle_udp for this path as well.
| wind_core::types::TargetAddr::IPv4(ip, port) => SocketAddr::from((*ip, *port)), | ||
| wind_core::types::TargetAddr::IPv6(ip, port) => SocketAddr::from((*ip, *port)), | ||
| _ => continue, | ||
| }; | ||
|
|
There was a problem hiding this comment.
This conversion drops TargetAddr::Domain packets (_ => continue) with no logging or error propagation. If TUIC can deliver UDP packets with domain targets, they’ll be silently lost; either support domains in SimpleUdpPacket (e.g., use TargetAddr instead of SocketAddr) or log/return an explicit error so callers can diagnose it.
| wind_core::types::TargetAddr::IPv4(ip, port) => SocketAddr::from((*ip, *port)), | |
| wind_core::types::TargetAddr::IPv6(ip, port) => SocketAddr::from((*ip, *port)), | |
| _ => continue, | |
| }; | |
| wind_core::types::TargetAddr::IPv4(ip, port) => { | |
| Some(SocketAddr::from((*ip, *port))) | |
| } | |
| wind_core::types::TargetAddr::IPv6(ip, port) => { | |
| Some(SocketAddr::from((*ip, *port))) | |
| } | |
| wind_core::types::TargetAddr::Domain(domain, port) => { | |
| warn!( | |
| target: "[OUT]", | |
| "Dropping UDP packet with domain target in simple channel: {}:{}", | |
| domain, | |
| port | |
| ); | |
| None | |
| } | |
| }; | |
| let Some(target) = target else { | |
| continue; | |
| }; |
| tokio = { version = "1", default-features = false, features = ["net"] } | ||
| tokio-util = { version = "0.7", features = ["codec"] } | ||
| quinn = { version = "0.11", default-features = false, features = ["runtime-tokio"]} | ||
| quinn = { workspace = true, default-features = false, features = ["runtime-tokio", "qlog"] } |
There was a problem hiding this comment.
workspace = true for quinn requires a [workspace.dependencies] quinn = ... entry in the root Cargo.toml. The current root manifest has no workspace dependencies section, so this will not resolve and will break builds; either add the workspace dependency or specify a concrete version here.
| quinn = { workspace = true, default-features = false, features = ["runtime-tokio", "qlog"] } | |
| quinn = { version = "0.11", default-features = false, features = ["runtime-tokio", "qlog"] } |
| tokio-util = { version = "0.7", features = ["rt"] } | ||
|
|
||
| quinn = { version = "0.11", default-features = false, optional = true } | ||
| quinn = { workspace = true, default-features = false, optional = true } |
There was a problem hiding this comment.
quinn = { workspace = true, ... } won’t resolve unless the root Cargo.toml defines quinn under [workspace.dependencies]. Since the workspace manifest currently only has [workspace.package], this change will cause a dependency resolution error; add the workspace dependency or pin a version in this crate.
| quinn = { workspace = true, default-features = false, optional = true } | |
| quinn = { version = "0.11", default-features = false, optional = true } |
|
|
||
| jobs: | ||
| build: | ||
| uses: rust-proxy/workflows/.github/workflows/rust-build.yml@main |
There was a problem hiding this comment.
The reusable workflow is referenced by a moving ref (@main). For reproducible CI and to avoid unexpected breakages, pin this to an immutable tag or commit SHA (and update deliberately when needed).
| uses: rust-proxy/workflows/.github/workflows/rust-build.yml@main | |
| uses: rust-proxy/workflows/.github/workflows/rust-build.yml@0123456789abcdef0123456789abcdef01234567 |
| let mut transport_config = quinn::TransportConfig::default(); | ||
| transport_config | ||
| .congestion_controller_factory(Arc::new(quinn::congestion::BbrConfig::default())) | ||
| .congestion_controller_factory(Arc::new(quinn::congestion::Bbr3Config::default())) |
There was a problem hiding this comment.
Bbr3Config may not exist in the currently locked quinn version (Cargo.lock shows quinn 0.11.9). This will fail to compile unless the workspace is updated to a quinn release that provides Bbr3Config, or this is switched back to BbrConfig/another supported congestion controller.
| .congestion_controller_factory(Arc::new(quinn::congestion::Bbr3Config::default())) | |
| .congestion_controller_factory(Arc::new(quinn::congestion::BbrConfig::default())) |
| pub async fn handle_udp_simple( | ||
| &self, | ||
| assoc_id: u16, | ||
| ) -> Result<(crate::simple_udp::SimpleUdpChannel, crate::simple_udp::SimpleUdpChannelTx), Error> { |
There was a problem hiding this comment.
handle_udp_simple takes an assoc_id from the caller, but handle_udp allocates one internally via udp_assoc_counter. Requiring the caller to provide a unique association ID makes misuse (collisions / overwriting udp_session) much more likely; consider allocating the ID inside this method (or returning the chosen ID) to keep the API safe and consistent.
| let connection = self.connection.clone(); | ||
| let (channel, channel_tx) = SimpleUdpChannel::new(128); | ||
|
|
||
| // Use crossfire channel compatible with UdpStream | ||
| let (wind_tx, wind_rx) = crossfire::mpmc::bounded_async::<wind_core::udp::UdpPacket>(128); | ||
| let udp_stream = Arc::new(UdpStream::new(connection.clone(), assoc_id, wind_tx)); | ||
| self.udp_session.insert(assoc_id, udp_stream.clone()).await; |
There was a problem hiding this comment.
handle_udp_simple creates SimpleUdpChannel/SimpleUdpChannelTx, but there is no task that drains packets sent by the caller (via channel.to_remote_tx) and forwards them to udp_stream.send_packet(...). As written, local→remote UDP traffic has no path to the TUIC connection, so the channel is effectively receive-only.
- Add periodic GC tick (collect_garbage) to evict stale fragment state - Add to_remote_rx arm: forward caller-sent packets to the TUIC connection - Remove assoc from udp_session cache on task exit - Call connection.drop_udp(assoc_id) on exit to send Dissociate to peer - Log channel-close and cancellation events consistently
Summary
SimpleUdpPacket,SimpleUdpChannel, andSimpleUdpChannelTxtypes in a newwind_tuic::simple_udpmoduleTuicOutbound::handle_udp_simple()which wires up a crossfire-based channel so callers receive reassembled UDP packets without implementing theAbstractUdpSockettraitpub mod simple_udpfromwind_tuic::libMotivation
AbstractUdpSocketrequires implementors to provide a full socket abstraction (poll-based send/recv), which is more complex than most integration use-cases need. A simple bounded async channel ofUdpPacketvalues is easier to wire into existing async runtimes and proxy stacks.Changes
crates/wind-tuic/src/simple_udp.rs— new module with channel typescrates/wind-tuic/src/outbound.rs—handle_udp_simple()method; fixedBbrConfig→Bbr3Configcrates/wind-tuic/src/lib.rs— exposesimple_udpmodulecrates/wind-tuic/src/proto/udp_stream.rs— fixedfrag_totaltype coercion (u64::from(...))crates/wind-core/src/udp.rs— defineUdpPollertrait unconditionally (removed#[cfg(feature = "quic")]guard that caused missing-trait errors)