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,21 +71,20 @@ impl DaemonRPCServer {
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 networks = list_networks(&conn).map_err(proc_err)?;
ResolvedTunnel::from_networks(&networks).map_err(proc_err)
}
async fn current_tunnel_configuration(&self) -> Result<TunnelConfigurationResponse, RspStatus> {
match self.resolve_tunnel().await? {
Some(config) => {
let config = config.server_config().map_err(proc_err)?;
let config = self
.resolve_tunnel()
.await?
.server_config()
.map_err(proc_err)?;
Ok(configuration_rsp(config))
}
None => Ok(empty_configuration_rsp()),
}
}
async fn stop_active_tunnel(&self) -> Result<bool, RspStatus> {
let current = { self.active_tunnel.write().await.take() };
@ -114,10 +113,6 @@ impl DaemonRPCServer {
async fn reconcile_runtime(&self) -> Result<(), RspStatus> {
let desired = self.resolve_tunnel().await?;
let Some(desired) = desired else {
let _ = self.stop_active_tunnel().await?;
return Ok(());
};
let needs_restart = {
let guard = self.active_tunnel.read().await;
guard
@ -163,10 +158,7 @@ impl Tunnel for DaemonRPCServer {
}
async fn tunnel_start(&self, _request: Request<Empty>) -> Result<Response<Empty>, RspStatus> {
let desired = self
.resolve_tunnel()
.await?
.ok_or_else(|| RspStatus::failed_precondition("no stored network configured"))?;
let desired = self.resolve_tunnel().await?;
let already_running = {
let guard = self.active_tunnel.read().await;
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 {
TunnelStatusResponse {
state: state.to_rpc().into(),

View file

@ -72,7 +72,7 @@ mod tests {
client::BurrowClient,
grpc_defs::{
Empty, Network, NetworkListResponse, NetworkReorderRequest, NetworkType,
TunnelConfigurationResponse,
TunnelConfigurationResponse, TunnelStatusResponse,
},
};
@ -111,6 +111,11 @@ mod tests {
.network_list(Empty {})
.await?
.into_inner();
let mut status_stream = client
.tunnel_client
.tunnel_status(Empty {})
.await?
.into_inner();
let initial_config = next_configuration(&mut config_stream).await?;
assert!(initial_config.addresses.is_empty());
@ -119,12 +124,27 @@ mod tests {
let initial_networks = next_networks(&mut network_stream).await?;
assert!(initial_networks.network.is_empty());
let start_err = client
.tunnel_client
.tunnel_start(Empty {})
.await
.expect_err("starting without a stored network should fail");
assert_eq!(start_err.code(), tonic::Code::FailedPrecondition);
let initial_status = next_status(&mut status_stream).await?;
assert_eq!(
initial_status.state(),
crate::daemon::rpc::grpc_defs::State::Stopped
);
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
.networks_client
@ -246,6 +266,14 @@ Endpoint = wg.burrow.rs:51820
.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)> {
response
.network

View file

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