Allow no-tunnel passthrough mode

This commit is contained in:
Conrad Kramer 2026-03-30 19:30:22 -07:00
parent 450e9c6fcd
commit 7ade60646b
3 changed files with 76 additions and 38 deletions

View file

@ -71,20 +71,19 @@ impl DaemonRPCServer {
self.network_update_chan.0.send(()).map_err(proc_err) self.network_update_chan.0.send(()).map_err(proc_err)
} }
async fn resolve_tunnel(&self) -> Result<Option<ResolvedTunnel>, RspStatus> { async fn resolve_tunnel(&self) -> Result<ResolvedTunnel, RspStatus> {
let conn = self.get_connection()?; let conn = self.get_connection()?;
let networks = list_networks(&conn).map_err(proc_err)?; let networks = list_networks(&conn).map_err(proc_err)?;
ResolvedTunnel::from_networks(&networks).map_err(proc_err) ResolvedTunnel::from_networks(&networks).map_err(proc_err)
} }
async fn current_tunnel_configuration(&self) -> Result<TunnelConfigurationResponse, RspStatus> { async fn current_tunnel_configuration(&self) -> Result<TunnelConfigurationResponse, RspStatus> {
match self.resolve_tunnel().await? { let config = self
Some(config) => { .resolve_tunnel()
let config = config.server_config().map_err(proc_err)?; .await?
Ok(configuration_rsp(config)) .server_config()
} .map_err(proc_err)?;
None => Ok(empty_configuration_rsp()), Ok(configuration_rsp(config))
}
} }
async fn stop_active_tunnel(&self) -> Result<bool, RspStatus> { async fn stop_active_tunnel(&self) -> Result<bool, RspStatus> {
@ -114,10 +113,6 @@ impl DaemonRPCServer {
async fn reconcile_runtime(&self) -> Result<(), RspStatus> { async fn reconcile_runtime(&self) -> Result<(), RspStatus> {
let desired = self.resolve_tunnel().await?; let desired = self.resolve_tunnel().await?;
let Some(desired) = desired else {
let _ = self.stop_active_tunnel().await?;
return Ok(());
};
let needs_restart = { let needs_restart = {
let guard = self.active_tunnel.read().await; let guard = self.active_tunnel.read().await;
guard guard
@ -163,10 +158,7 @@ impl Tunnel for DaemonRPCServer {
} }
async fn tunnel_start(&self, _request: Request<Empty>) -> Result<Response<Empty>, RspStatus> { async fn tunnel_start(&self, _request: Request<Empty>) -> Result<Response<Empty>, RspStatus> {
let desired = self let desired = self.resolve_tunnel().await?;
.resolve_tunnel()
.await?
.ok_or_else(|| RspStatus::failed_precondition("no stored network configured"))?;
let already_running = { let already_running = {
let guard = self.active_tunnel.read().await; let guard = self.active_tunnel.read().await;
guard guard
@ -285,13 +277,6 @@ fn configuration_rsp(config: ServerConfig) -> TunnelConfigurationResponse {
} }
} }
fn empty_configuration_rsp() -> TunnelConfigurationResponse {
TunnelConfigurationResponse {
mtu: 1500,
addresses: Vec::new(),
}
}
fn status_rsp(state: RunState) -> TunnelStatusResponse { fn status_rsp(state: RunState) -> TunnelStatusResponse {
TunnelStatusResponse { TunnelStatusResponse {
state: state.to_rpc().into(), state: state.to_rpc().into(),

View file

@ -72,7 +72,7 @@ mod tests {
client::BurrowClient, client::BurrowClient,
grpc_defs::{ grpc_defs::{
Empty, Network, NetworkListResponse, NetworkReorderRequest, NetworkType, Empty, Network, NetworkListResponse, NetworkReorderRequest, NetworkType,
TunnelConfigurationResponse, TunnelConfigurationResponse, TunnelStatusResponse,
}, },
}; };
@ -111,6 +111,11 @@ mod tests {
.network_list(Empty {}) .network_list(Empty {})
.await? .await?
.into_inner(); .into_inner();
let mut status_stream = client
.tunnel_client
.tunnel_status(Empty {})
.await?
.into_inner();
let initial_config = next_configuration(&mut config_stream).await?; let initial_config = next_configuration(&mut config_stream).await?;
assert!(initial_config.addresses.is_empty()); assert!(initial_config.addresses.is_empty());
@ -119,12 +124,27 @@ mod tests {
let initial_networks = next_networks(&mut network_stream).await?; let initial_networks = next_networks(&mut network_stream).await?;
assert!(initial_networks.network.is_empty()); assert!(initial_networks.network.is_empty());
let start_err = client let initial_status = next_status(&mut status_stream).await?;
.tunnel_client assert_eq!(
.tunnel_start(Empty {}) initial_status.state(),
.await crate::daemon::rpc::grpc_defs::State::Stopped
.expect_err("starting without a stored network should fail"); );
assert_eq!(start_err.code(), tonic::Code::FailedPrecondition);
client.tunnel_client.tunnel_start(Empty {}).await?;
let passthrough_status = next_status(&mut status_stream).await?;
assert_eq!(
passthrough_status.state(),
crate::daemon::rpc::grpc_defs::State::Running
);
client.tunnel_client.tunnel_stop(Empty {}).await?;
let stopped_status = next_status(&mut status_stream).await?;
assert_eq!(
stopped_status.state(),
crate::daemon::rpc::grpc_defs::State::Stopped
);
client client
.networks_client .networks_client
@ -246,6 +266,14 @@ Endpoint = wg.burrow.rs:51820
.ok_or_else(|| anyhow!("network stream ended unexpectedly")) .ok_or_else(|| anyhow!("network stream ended unexpectedly"))
} }
async fn next_status(
stream: &mut tonic::Streaming<TunnelStatusResponse>,
) -> Result<TunnelStatusResponse> {
timeout(Duration::from_secs(5), stream.message())
.await??
.ok_or_else(|| anyhow!("status stream ended unexpectedly"))
}
fn network_ids(response: &NetworkListResponse) -> Vec<(i32, NetworkType)> { fn network_ids(response: &NetworkListResponse) -> Vec<(i32, NetworkType)> {
response response
.network .network

View file

@ -15,6 +15,7 @@ use crate::{
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum RuntimeIdentity { pub enum RuntimeIdentity {
Passthrough,
Network { Network {
id: i32, id: i32,
network_type: NetworkType, network_type: NetworkType,
@ -24,6 +25,9 @@ pub enum RuntimeIdentity {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ResolvedTunnel { pub enum ResolvedTunnel {
Passthrough {
identity: RuntimeIdentity,
},
WireGuard { WireGuard {
identity: RuntimeIdentity, identity: RuntimeIdentity,
config: Config, config: Config,
@ -35,9 +39,11 @@ pub enum ResolvedTunnel {
} }
impl ResolvedTunnel { impl ResolvedTunnel {
pub fn from_networks(networks: &[Network]) -> Result<Option<Self>> { pub fn from_networks(networks: &[Network]) -> Result<Self> {
let Some(network) = networks.first() else { let Some(network) = networks.first() else {
return Ok(None); return Ok(Self::Passthrough {
identity: RuntimeIdentity::Passthrough,
});
}; };
let identity = RuntimeIdentity::Network { let identity = RuntimeIdentity::Network {
@ -51,23 +57,30 @@ impl ResolvedTunnel {
let payload = String::from_utf8(network.payload.clone()) let payload = String::from_utf8(network.payload.clone())
.context("wireguard payload must be valid UTF-8")?; .context("wireguard payload must be valid UTF-8")?;
let config = Config::from_content_fmt(&payload, "ini")?; let config = Config::from_content_fmt(&payload, "ini")?;
Ok(Some(Self::WireGuard { identity, config })) Ok(Self::WireGuard { identity, config })
} }
NetworkType::HackClub => { NetworkType::HackClub => {
let config = HackClubNetworkConfig::from_payload(&network.payload)?; let config = HackClubNetworkConfig::from_payload(&network.payload)?;
Ok(Some(Self::HackClub { identity, config })) Ok(Self::HackClub { identity, config })
} }
} }
} }
pub fn identity(&self) -> &RuntimeIdentity { pub fn identity(&self) -> &RuntimeIdentity {
match self { match self {
Self::WireGuard { identity, .. } | Self::HackClub { identity, .. } => identity, Self::Passthrough { identity }
| Self::WireGuard { identity, .. }
| Self::HackClub { identity, .. } => identity,
} }
} }
pub fn server_config(&self) -> Result<ServerConfig> { pub fn server_config(&self) -> Result<ServerConfig> {
match self { match self {
Self::Passthrough { .. } => Ok(ServerConfig {
address: Vec::new(),
name: None,
mtu: Some(1500),
}),
Self::WireGuard { config, .. } => ServerConfig::try_from(config), Self::WireGuard { config, .. } => ServerConfig::try_from(config),
Self::HackClub { config, .. } => Ok(ServerConfig { Self::HackClub { config, .. } => Ok(ServerConfig {
address: config.local_addresses.clone(), address: config.local_addresses.clone(),
@ -82,6 +95,7 @@ impl ResolvedTunnel {
tun_interface: Arc<RwLock<Option<TunInterface>>>, tun_interface: Arc<RwLock<Option<TunInterface>>>,
) -> Result<ActiveTunnel> { ) -> Result<ActiveTunnel> {
match self { match self {
Self::Passthrough { identity } => Ok(ActiveTunnel::Passthrough { identity }),
Self::WireGuard { identity, config } => { Self::WireGuard { identity, config } => {
let tun = TunOptions::new().open()?; let tun = TunOptions::new().open()?;
tun_interface.write().await.replace(tun); tun_interface.write().await.replace(tun);
@ -118,6 +132,9 @@ impl ResolvedTunnel {
} }
pub enum ActiveTunnel { pub enum ActiveTunnel {
Passthrough {
identity: RuntimeIdentity,
},
WireGuard { WireGuard {
identity: RuntimeIdentity, identity: RuntimeIdentity,
interface: Arc<RwLock<WireGuardInterface>>, interface: Arc<RwLock<WireGuardInterface>>,
@ -132,12 +149,15 @@ pub enum ActiveTunnel {
impl ActiveTunnel { impl ActiveTunnel {
pub fn identity(&self) -> &RuntimeIdentity { pub fn identity(&self) -> &RuntimeIdentity {
match self { match self {
Self::WireGuard { identity, .. } | Self::HackClub { identity, .. } => identity, Self::Passthrough { identity }
| Self::WireGuard { identity, .. }
| Self::HackClub { identity, .. } => identity,
} }
} }
pub async fn shutdown(self, tun_interface: &Arc<RwLock<Option<TunInterface>>>) -> Result<()> { pub async fn shutdown(self, tun_interface: &Arc<RwLock<Option<TunInterface>>>) -> Result<()> {
match self { match self {
Self::Passthrough { .. } => Ok(()),
Self::WireGuard { interface, task, .. } => { Self::WireGuard { interface, task, .. } => {
interface.read().await.remove_tun().await; interface.read().await.remove_tun().await;
let task_result = task.await; let task_result = task.await;
@ -174,7 +194,12 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn no_networks_resolves_to_no_tunnel() { fn no_networks_resolve_to_passthrough() {
assert!(ResolvedTunnel::from_networks(&[]).unwrap().is_none()); let resolved = ResolvedTunnel::from_networks(&[]).unwrap();
assert_eq!(resolved.identity(), &RuntimeIdentity::Passthrough);
assert_eq!(
resolved.server_config().unwrap().address,
Vec::<String>::new()
);
} }
} }