Compare commits

...

1 Commits

Author SHA1 Message Date
KKRainbow 8e1d079142 feat: add Windows UDP broadcast relay (#2222)
This may helps games to find rooms in virtual network.

- add opt-in Windows UDP broadcast relay config flag and CLI/env plumbing
- capture local UDP broadcasts with Windows raw sockets, normalize packets, and inject them via PeerManager
2026-05-09 09:56:31 +08:00
19 changed files with 1271 additions and 13 deletions
@@ -99,6 +99,7 @@ const bool_flags: BoolFlag[] = [
{ field: 'disable_encryption', help: 'disable_encryption_help' },
{ field: 'disable_tcp_hole_punching', help: 'disable_tcp_hole_punching_help' },
{ field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
{ field: 'enable_udp_broadcast_relay', help: 'enable_udp_broadcast_relay_help' },
{ field: 'disable_upnp', help: 'disable_upnp_help' },
{ field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' },
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
@@ -160,6 +160,9 @@ disable_tcp_hole_punching_help: 禁用TCP打洞功能
disable_udp_hole_punching: 禁用UDP打洞
disable_udp_hole_punching_help: 禁用UDP打洞功能
enable_udp_broadcast_relay: UDP 广播中继
enable_udp_broadcast_relay_help: "仅 Windows:捕获物理网卡上的本机 UDP 广播包并转发给 EasyTier 对等节点,帮助局域网游戏发现房间。需要管理员权限。"
disable_upnp: 禁用 UPnP
disable_upnp_help: 禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启。
@@ -260,6 +263,7 @@ event:
DhcpIpv4Conflicted: DHCP IPv4地址冲突
PortForwardAdded: 端口转发添加
ProxyCidrsUpdated: 子网代理CIDR更新
UdpBroadcastRelayStartResult: UDP广播中继启动结果
web:
login:
@@ -159,6 +159,9 @@ disable_tcp_hole_punching_help: Disable tcp hole punching
disable_udp_hole_punching: Disable UDP Hole Punching
disable_udp_hole_punching_help: Disable udp hole punching
enable_udp_broadcast_relay: UDP Broadcast Relay
enable_udp_broadcast_relay_help: "Windows only: capture local UDP broadcast packets from physical interfaces and forward them to EasyTier peers. Helps games to find rooms in local network. Requires administrator privileges."
disable_upnp: Disable UPnP
disable_upnp_help: Disable runtime UPnP/NAT-PMP port mapping for eligible listeners; automatic port mapping is enabled by default.
@@ -260,6 +263,7 @@ event:
DhcpIpv4Conflicted: DhcpIpv4Conflicted
PortForwardAdded: PortForwardAdded
ProxyCidrsUpdated: ProxyCidrsUpdated
UdpBroadcastRelayStartResult: UDP Broadcast Relay Start Result
web:
login:
@@ -134,6 +134,7 @@ export interface NetworkConfig {
disable_tcp_hole_punching?: boolean
disable_udp_hole_punching?: boolean
disable_upnp?: boolean
enable_udp_broadcast_relay?: boolean
disable_sym_hole_punching?: boolean
enable_relay_network_whitelist?: boolean
@@ -211,6 +212,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
disable_tcp_hole_punching: false,
disable_udp_hole_punching: false,
disable_upnp: false,
enable_udp_broadcast_relay: false,
disable_sym_hole_punching: false,
enable_relay_network_whitelist: false,
relay_network_whitelist: [],
@@ -447,4 +449,6 @@ export enum EventType {
PortForwardAdded = 'PortForwardAdded', // PortForwardConfigPb
ProxyCidrsUpdated = 'ProxyCidrsUpdated', // string[], string[]
UdpBroadcastRelayStartResult = 'UdpBroadcastRelayStartResult', // { capture_backend?: string, error?: string }
}
+3
View File
@@ -184,6 +184,9 @@ core_clap:
disable_upnp:
en: "disable runtime UPnP/NAT-PMP port mapping for eligible listeners; automatic port mapping is enabled by default"
zh-CN: "禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启"
enable_udp_broadcast_relay:
en: "Windows only: capture local UDP broadcast packets from physical interfaces and forward them to EasyTier peers. Helps games to find rooms in local network. Requires administrator privileges."
zh-CN: "仅 Windows:捕获物理网卡上的本机 UDP 广播包并转发给 EasyTier 对等节点,帮助局域网游戏发现房间。需要管理员权限。"
relay_all_peer_rpc:
en: "relay all peer rpc packets, even if the peer is not in the relay network whitelist. this can help peers not in relay network whitelist to establish p2p connection."
zh-CN: "转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。这可以帮助白名单外网络中的对等节点建立P2P连接。"
+2 -3
View File
@@ -11,9 +11,8 @@ use windows::{
NET_FW_RULE_DIR_OUT,
},
Networking::WinSock::{
IP_UNICAST_IF, IPPROTO_IP, IPPROTO_IPV6, IPV6_UNICAST_IF, SIO_UDP_CONNRESET,
SO_EXCLUSIVEADDRUSE, SOCKET, SOCKET_ERROR, SOL_SOCKET, WSAGetLastError, WSAIoctl,
htonl, setsockopt,
IP_UNICAST_IF, IPPROTO_IP, IPPROTO_IPV6, IPV6_UNICAST_IF, SIO_UDP_CONNRESET, SOCKET,
SOCKET_ERROR, WSAGetLastError, WSAIoctl, htonl, setsockopt,
},
System::Com::{
CLSCTX_ALL, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize,
+1
View File
@@ -72,6 +72,7 @@ pub fn gen_default_flags() -> Flags {
instance_recv_bps_limit: u64::MAX,
disable_upnp: false,
disable_relay_data: false,
enable_udp_broadcast_relay: false,
}
}
+5
View File
@@ -77,6 +77,11 @@ pub enum GlobalCtxEvent {
ProxyCidrsUpdated(Vec<cidr::Ipv4Cidr>, Vec<cidr::Ipv4Cidr>), // (added, removed)
UdpBroadcastRelayStartResult {
capture_backend: Option<String>,
error: Option<String>,
},
CredentialChanged,
}
+22
View File
@@ -85,6 +85,15 @@ pub enum MetricName {
/// Traffic packets forwarded for foreign network, forward
TrafficPacketsForeignForwardForwarded,
/// UDP broadcast relay packets captured from the raw socket
UdpBroadcastRelayPacketsCaptured,
/// UDP broadcast relay packets ignored before forwarding
UdpBroadcastRelayPacketsIgnored,
/// UDP broadcast relay packets forwarded
UdpBroadcastRelayPacketsForwarded,
/// UDP broadcast relay packets that failed to forward
UdpBroadcastRelayPacketsForwardFailed,
/// Compression bytes before compression
CompressionBytesRxBefore,
/// Compression bytes after compression
@@ -167,6 +176,19 @@ impl fmt::Display for MetricName {
write!(f, "traffic_packets_foreign_forward_forwarded")
}
MetricName::UdpBroadcastRelayPacketsCaptured => {
write!(f, "udp_broadcast_relay_packets_captured")
}
MetricName::UdpBroadcastRelayPacketsIgnored => {
write!(f, "udp_broadcast_relay_packets_ignored")
}
MetricName::UdpBroadcastRelayPacketsForwarded => {
write!(f, "udp_broadcast_relay_packets_forwarded")
}
MetricName::UdpBroadcastRelayPacketsForwardFailed => {
write!(f, "udp_broadcast_relay_packets_forward_failed")
}
MetricName::CompressionBytesRxBefore => write!(f, "compression_bytes_rx_before"),
MetricName::CompressionBytesRxAfter => write!(f, "compression_bytes_rx_after"),
MetricName::CompressionBytesTxBefore => write!(f, "compression_bytes_tx_before"),
+12
View File
@@ -484,6 +484,15 @@ struct NetworkOptions {
)]
disable_upnp: Option<bool>,
#[arg(
long,
env = "ET_ENABLE_UDP_BROADCAST_RELAY",
help = t!("core_clap.enable_udp_broadcast_relay").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
enable_udp_broadcast_relay: Option<bool>,
#[arg(
long,
env = "ET_RELAY_ALL_PEER_RPC",
@@ -1142,6 +1151,9 @@ impl NetworkOptions {
.disable_sym_hole_punching
.unwrap_or(f.disable_sym_hole_punching);
f.disable_upnp = self.disable_upnp.unwrap_or(f.disable_upnp);
f.enable_udp_broadcast_relay = self
.enable_udp_broadcast_relay
.unwrap_or(f.enable_udp_broadcast_relay);
// Configure tld_dns_zone: use provided value if set
if let Some(tld_dns_zone) = &self.tld_dns_zone {
f.tld_dns_zone = tld_dns_zone.clone();
+3
View File
@@ -10,3 +10,6 @@ pub mod proxy_cidrs_monitor;
#[cfg(feature = "tun")]
pub mod virtual_nic;
#[cfg(any(windows, test))]
pub(crate) mod windows_udp_broadcast;
@@ -1,5 +1,8 @@
use std::{path::Path, sync::Arc};
#[cfg(target_os = "linux")]
use std::path::Path;
use std::sync::Arc;
#[cfg(target_os = "linux")]
use anyhow::Context;
use cidr::{Ipv6Cidr, Ipv6Inet};
#[cfg(target_os = "linux")]
@@ -321,7 +324,7 @@ async fn resolve_public_ipv6_provider_runtime_state_linux(
}
async fn resolve_public_ipv6_provider_runtime_state(
global_ctx: &ArcGlobalCtx,
_global_ctx: &ArcGlobalCtx,
config: PublicIpv6ProviderConfigSnapshot,
) -> PublicIpv6ProviderRuntimeState {
if !config.provider_enabled {
@@ -331,7 +334,7 @@ async fn resolve_public_ipv6_provider_runtime_state(
#[cfg(target_os = "linux")]
{
return resolve_public_ipv6_provider_runtime_state_linux(
global_ctx,
_global_ctx,
config.configured_prefix,
)
.await;
+35
View File
@@ -35,6 +35,8 @@ use tokio::{
task::JoinSet,
};
use tokio_util::bytes::Bytes;
#[cfg(target_os = "windows")]
use tokio_util::task::AbortOnDropHandle;
use tun::{AbstractDevice, AsyncDevice, Configuration, Layer};
use zerocopy::{NativeEndian, NetworkEndian};
@@ -801,6 +803,9 @@ pub struct NicCtx {
nic: Arc<Mutex<VirtualNic>>,
tasks: JoinSet<()>,
#[cfg(target_os = "windows")]
windows_udp_broadcast_relay: Option<AbortOnDropHandle<()>>,
}
impl NicCtx {
@@ -819,6 +824,9 @@ impl NicCtx {
nic: Arc::new(Mutex::new(VirtualNic::new(global_ctx))),
tasks: JoinSet::new(),
#[cfg(target_os = "windows")]
windows_udp_broadcast_relay: None,
}
}
@@ -1005,6 +1013,31 @@ impl NicCtx {
});
}
#[cfg(target_os = "windows")]
fn start_windows_udp_broadcast_relay(&mut self, virtual_ipv4: Ipv4Inet) {
if !self.global_ctx.get_flags().enable_udp_broadcast_relay {
return;
}
let Some(peer_manager) = self.peer_mgr.upgrade() else {
tracing::warn!("peer manager is dropped, skip Windows UDP broadcast relay");
return;
};
match super::windows_udp_broadcast::start(peer_manager, virtual_ipv4) {
Ok(handle) => {
self.windows_udp_broadcast_relay = Some(handle);
tracing::info!("Windows UDP broadcast relay started");
}
Err(err) => {
tracing::warn!(
?err,
"failed to start Windows UDP broadcast relay; administrator privileges are required"
);
}
}
}
async fn apply_route_changes(
ifcfg: &impl IfConfiguerTrait,
ifname: &str,
@@ -1347,6 +1380,8 @@ impl NicCtx {
// Assign IPv4 address if provided
if let Some(ipv4_addr) = ipv4_addr {
self.assign_ipv4_to_tun_device(ipv4_addr).await?;
#[cfg(target_os = "windows")]
self.start_windows_udp_broadcast_relay(ipv4_addr);
}
// Assign IPv6 address if provided
File diff suppressed because it is too large Load Diff
+22
View File
@@ -474,6 +474,28 @@ fn handle_event(
);
}
GlobalCtxEvent::UdpBroadcastRelayStartResult {
capture_backend,
error,
} => {
if let Some(error) = error {
event!(
warn,
?capture_backend,
%error,
"[{}] UDP broadcast relay start failed",
instance_id
);
} else {
event!(
info,
?capture_backend,
"[{}] UDP broadcast relay started",
instance_id
);
}
}
GlobalCtxEvent::CredentialChanged => {
event!(info, "[{}] credential changed", instance_id);
}
+6
View File
@@ -820,6 +820,10 @@ impl NetworkConfig {
flags.disable_relay_data = disable_relay_data;
}
if let Some(enable_udp_broadcast_relay) = self.enable_udp_broadcast_relay {
flags.enable_udp_broadcast_relay = enable_udp_broadcast_relay;
}
if let Some(disable_sym_hole_punching) = self.disable_sym_hole_punching {
flags.disable_sym_hole_punching = disable_sym_hole_punching;
}
@@ -995,6 +999,7 @@ impl NetworkConfig {
result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching);
result.disable_upnp = Some(flags.disable_upnp);
result.disable_relay_data = Some(flags.disable_relay_data);
result.enable_udp_broadcast_relay = Some(flags.enable_udp_broadcast_relay);
result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching);
result.enable_magic_dns = Some(flags.accept_dns);
result.mtu = Some(flags.mtu as i32);
@@ -1263,6 +1268,7 @@ mod tests {
flags.disable_tcp_hole_punching = rng.gen_bool(0.2);
flags.disable_udp_hole_punching = rng.gen_bool(0.2);
flags.disable_upnp = rng.gen_bool(0.2);
flags.enable_udp_broadcast_relay = rng.gen_bool(0.2);
flags.accept_dns = rng.gen_bool(0.6);
flags.mtu = rng.gen_range(1200..1500);
flags.private_mode = rng.gen_bool(0.3);
+42 -7
View File
@@ -1569,17 +1569,26 @@ impl PeerManager {
ipv6_addr.is_multicast() || *ipv6_addr == ipv6_inet.last_address()
}
fn select_ipv4_broadcast_peers<'a>(
routes: impl IntoIterator<Item = &'a instance::Route>,
my_peer_id: PeerId,
) -> Vec<PeerId> {
routes
.into_iter()
.filter_map(|route| {
(route.peer_id != my_peer_id && route.ipv4_addr.is_some()).then_some(route.peer_id)
})
.collect()
}
pub async fn get_msg_dst_peer_ipv4(&self, ipv4_addr: &Ipv4Addr) -> (Vec<PeerId>, bool) {
let mut is_exit_node = false;
let mut dst_peers = vec![];
if self.is_all_peers_broadcast_ipv4(ipv4_addr) {
dst_peers.extend(self.peers.list_routes().await.iter().filter_map(|x| {
if *x.key() != self.my_peer_id {
Some(*x.key())
} else {
None
}
}));
dst_peers.extend(Self::select_ipv4_broadcast_peers(
&self.peers.list_route_infos().await,
self.my_peer_id,
));
} else if let Some(peer_id) = self.peers.get_peer_id_by_ipv4(ipv4_addr).await {
dst_peers.push(peer_id);
} else if !self
@@ -2199,6 +2208,32 @@ mod tests {
assert!(!PeerManager::should_mark_recent_traffic_for_fanout(2));
}
fn route_with_ipv4(
peer_id: u32,
ipv4_addr: Option<std::net::Ipv4Addr>,
) -> crate::proto::api::instance::Route {
crate::proto::api::instance::Route {
peer_id,
ipv4_addr: ipv4_addr.map(|addr| cidr::Ipv4Inet::new(addr, 24).unwrap().into()),
..Default::default()
}
}
#[test]
fn ipv4_broadcast_peer_selection_skips_peers_without_ipv4() {
let routes = vec![
route_with_ipv4(1, Some(std::net::Ipv4Addr::new(10, 126, 126, 1))),
route_with_ipv4(2, None),
route_with_ipv4(3, Some(std::net::Ipv4Addr::new(10, 126, 126, 3))),
route_with_ipv4(4, None),
];
assert_eq!(
PeerManager::select_ipv4_broadcast_peers(&routes, 3),
vec![1]
);
}
#[test]
fn gc_recent_traffic_removes_expired_and_connected_entries() {
let stale_peer = 1;
+1
View File
@@ -100,6 +100,7 @@ message NetworkConfig {
optional bool ipv6_public_addr_auto = 63;
optional string ipv6_public_addr_prefix = 64;
optional bool disable_relay_data = 65;
optional bool enable_udp_broadcast_relay = 66;
}
message PortForwardConfig {
+1
View File
@@ -76,6 +76,7 @@ message FlagsInConfig {
uint64 instance_recv_bps_limit = 39;
bool disable_upnp = 40;
bool disable_relay_data = 41;
bool enable_udp_broadcast_relay = 42;
}
message RpcDescriptor {