fix(vpn): inherit portal exit nodes for attached clients (#2726)

This commit is contained in:
KKRainbow
2026-10-19 00:04:29 +08:00
committed by GitHub
parent a144597d02
commit 5af90471bf
3 changed files with 195 additions and 3 deletions
+174 -2
View File
@@ -199,7 +199,7 @@ impl AttachedPeerRuntime {
let public_ipv6_runtime =
CorePublicIpv6Runtime::new(runtime_config.clone(), Arc::new(()), Arc::new(()));
let flags = peer_snapshot.flags.clone();
let peer_manager = Arc::new(PeerManagerCore::new(
let mut peer_manager = PeerManagerCore::new(
PortablePeerManagerConfig {
snapshot: peer_snapshot,
route_algo: RouteAlgoType::Ospf,
@@ -214,7 +214,9 @@ impl AttachedPeerRuntime {
Arc::new(()),
None,
Arc::new(()),
)?);
)?;
peer_manager.inherit_exit_nodes(&network_peer_manager);
let peer_manager = Arc::new(peer_manager);
if let Err(error) = peer_manager
.follow_network_policy(network_runtime_config, config.groups)
.await
@@ -621,6 +623,176 @@ mod tests {
peer_manager_with_acl(Vec::new())
}
async fn connect_exit_peer(
network: &PeerManagerCore,
store: &CoreRuntimeConfigStore,
address: Ipv4Addr,
) -> (Arc<PeerManagerCore>, HostPacketReceiver) {
let mut snapshot = store.snapshot().peer.as_ref().clone();
snapshot.runtime.core.routes.ipv4 = Some(IpPrefix {
address: address.into(),
prefix_len: 24,
});
if let Some(secure) = snapshot.runtime.secure_mode.as_mut() {
let private = StaticSecret::from([address.octets()[3]; 32]);
secure.local_private_key = Some(BASE64_STANDARD.encode(private.as_bytes()));
secure.local_public_key =
Some(BASE64_STANDARD.encode(PublicKey::from(&private).as_bytes()));
}
let flags = snapshot.flags.clone();
let store =
CoreRuntimeConfigStore::new(CoreRuntimeConfig::default(), Arc::new(snapshot.clone()));
let (sender, receiver) = host_packet_channel();
let public_ipv6 = CorePublicIpv6Runtime::new(store.clone(), Arc::new(()), Arc::new(()));
let peer = Arc::new(
PeerManagerCore::new(
PortablePeerManagerConfig {
snapshot,
route_algo: RouteAlgoType::Ospf,
exit_nodes: Vec::new(),
foreign_context_default_flags: flags,
},
Vec::new(),
store,
Arc::new(()),
sender,
public_ipv6,
Arc::new(()),
None,
Arc::new(()),
)
.unwrap(),
);
peer.run().await.unwrap();
let (server, client) = create_ring_tunnel_pair();
let (server_result, client_result) = tokio::join!(
network.add_tunnel_as_server(server),
peer.add_client_tunnel(client),
);
server_result.unwrap();
client_result.unwrap();
(peer, receiver)
}
async fn check_attached_peer_exit_nodes(secure: bool) {
let (network, store) = peer_manager_with_acl_and_secure(Vec::new(), secure);
network.run().await.unwrap();
let first_ip = Ipv4Addr::new(10, 82, 0, 10);
let second_ip = Ipv4Addr::new(10, 82, 0, 11);
let (first, mut first_packets) = connect_exit_peer(&network, &store, first_ip).await;
let (second, mut second_packets) = connect_exit_peer(&network, &store, second_ip).await;
network.update_exit_nodes(vec![first_ip.into()]).await;
let client_ip = Ipv4Addr::new(10, 82, 0, 2);
let attached = AttachedPeerRuntime::connect(
network.clone(),
store,
AttachedPeerConfig {
name: "exit-node-client".to_owned(),
virtual_ip: attached_ipv4(client_ip),
groups: vec!["ops".to_owned()],
identity_private_key: [2; 32],
},
)
.await
.unwrap();
for (exit, address) in [(&first, first_ip), (&second, second_ip)] {
wait_for_route(&attached.peer_manager, exit.my_peer_id(), address, true).await;
wait_for_route(exit, attached.peer_id(), client_ip, true).await;
}
let public_ip = Ipv4Addr::new(8, 8, 8, 8);
let mut request = vec![0u8; 28];
request[0] = 0x45;
request[2..4].copy_from_slice(&28u16.to_be_bytes());
request[8] = 64;
request[9] = 1;
request[12..16].copy_from_slice(&client_ip.octets());
request[16..20].copy_from_slice(&public_ip.octets());
request[20] = 8;
for (exit, address, packets) in [
(&first, first_ip, &mut first_packets),
(&second, second_ip, &mut second_packets),
] {
if address == second_ip {
network.update_exit_nodes(vec![second_ip.into()]).await;
}
assert_eq!(
attached
.peer_manager
.get_msg_dst_peer(&public_ip.into())
.await,
(vec![exit.my_peer_id()], true),
);
attached.send_packet(&request).await.unwrap();
let packet = tokio::time::timeout(Duration::from_secs(5), packets.recv())
.await
.expect("exit node did not receive the client packet")
.unwrap()
.into_core_packet();
assert_eq!(packet.payload(), request.as_slice());
let header = packet.peer_manager_header().unwrap();
assert!(header.is_exit_node());
assert_eq!(header.from_peer_id.get(), attached.peer_id());
assert_eq!(header.to_peer_id.get(), exit.my_peer_id());
let mut reply = request.clone();
reply[12..16].copy_from_slice(&public_ip.octets());
reply[16..20].copy_from_slice(&client_ip.octets());
reply[20] = 0;
exit.send_msg_by_ip(ZCPacket::new_with_payload(&reply), client_ip.into(), false)
.await
.unwrap();
let received = tokio::time::timeout(Duration::from_secs(5), attached.recv_packet())
.await
.expect("exit reply did not reach the client")
.unwrap();
assert_eq!(received.payload(), reply.as_slice());
}
network.update_exit_nodes(Vec::new()).await;
assert_eq!(
attached
.peer_manager
.get_msg_dst_peer(&public_ip.into())
.await,
(Vec::new(), false),
);
// Explicit mesh routes must still win, even with an exit configured.
network.update_exit_nodes(vec![second_ip.into()]).await;
assert_eq!(
attached
.peer_manager
.get_msg_dst_peer(&first_ip.into())
.await,
(vec![first.my_peer_id()], false),
);
request[16..20].copy_from_slice(&first_ip.octets());
attached.send_packet(&request).await.unwrap();
let packet = tokio::time::timeout(Duration::from_secs(5), first_packets.recv())
.await
.unwrap()
.unwrap()
.into_core_packet();
assert_eq!(packet.payload(), request.as_slice());
assert!(!packet.peer_manager_header().unwrap().is_exit_node());
attached.close().await;
first.clear_resources().await;
second.clear_resources().await;
network.clear_resources().await;
}
#[tokio::test]
async fn attached_peer_inherits_exit_nodes_and_updates() {
check_attached_peer_exit_nodes(false).await;
}
#[tokio::test]
async fn secure_attached_peer_inherits_exit_nodes_and_updates() {
check_attached_peer_exit_nodes(true).await;
}
async fn wait_for_route(
peer: &PeerManagerCore,
attached_peer_id: PeerId,
+7
View File
@@ -1756,6 +1756,13 @@ impl PeerManagerCore {
*self.exit_nodes.write().await = exit_nodes;
}
/// Shares the network's current exit nodes, including later updates, while
/// keeping this peer's route lookup and packet identity independent.
pub(super) fn inherit_exit_nodes(&mut self, network: &Self) {
self.exit_nodes = network.exit_nodes.clone();
self.outbound_packet_router.exit_nodes = self.exit_nodes.clone();
}
fn reload_acl(&self, acl: Option<&crate::proto::acl::Acl>) {
self.acl_filter.reload_rules(acl);
}
+14 -1
View File
@@ -2075,8 +2075,13 @@ pub async fn wireguard_vpn_portal(#[values(true, false)] test_v6: bool) {
config
.add_proxy_cidr("198.51.100.0/24".parse().unwrap(), None)
.unwrap();
let mut flags = config.get_flags();
flags.enable_exit_node = true;
flags.use_smoltcp = true;
config.set_flags(flags);
}
if config.get_inst_name() == "inst3" {
config.set_exit_nodes(vec!["10.144.144.1".parse().unwrap()]);
config.set_vpn_portal_config(VpnPortalConfig {
enabled: None,
wireguard_listen: "0.0.0.0:22121".parse().unwrap(),
@@ -2137,7 +2142,7 @@ pub async fn wireguard_vpn_portal(#[values(true, false)] test_v6: bool) {
dst_socket_addr,
Key::try_from(server_public.as_slice()).unwrap(),
Key::try_from(client_private.as_slice()).unwrap(),
vec!["10.144.144.0/24".to_string()],
vec!["10.144.144.0/24".to_string(), "10.1.1.1/32".to_string()],
"10.144.144.4".to_owned(),
)
.unwrap();
@@ -2161,6 +2166,14 @@ pub async fn wireguard_vpn_portal(#[values(true, false)] test_v6: bool) {
)
.await;
// This address is outside the mesh and advertised proxy CIDRs, on a
// separate bridge reachable only through the configured exit node.
wait_for_condition(
|| async { ping_test(network.namespace("net_d"), "10.1.1.1", None).await },
Duration::from_secs(5),
)
.await;
drop_insts(insts).await;
}