Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions crates/api/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,11 +392,15 @@ pub async fn add_route(

match resp {
Ok(r) => match r.response {
Some(management_response::Response::Ok(_)) => (
Some(management_response::Response::Ok(ok)) => (
StatusCode::CREATED,
Json(SuccessResponse {
success: true,
message: None,
message: if ok.warning.is_empty() {
None
} else {
Some(ok.warning)
},
}),
),
Some(management_response::Response::Error(e)) => (
Expand Down
5 changes: 4 additions & 1 deletion crates/cli/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,11 @@ pub fn print_response(resp: &ManagementResponse) -> Result<(), CtlError> {
println!("Fingerprint: {}", l.fingerprint);
}
}
Some(management_response::Response::Ok(_)) => {
Some(management_response::Response::Ok(ok)) => {
println!("OK");
if !ok.warning.is_empty() {
eprintln!("warning: {}", ok.warning);
}
}
Some(management_response::Response::Error(e)) => {
return Err(CtlError::Daemon(e.message.clone()));
Expand Down
14 changes: 10 additions & 4 deletions crates/cli/src/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ fn print_help() {

let mut tw = TabWriter::new(std::io::stdout());
let _ = writeln!(tw, "Commands:");
let _ = writeln!(tw, " ping [<peer>]\tPing a peer");
let _ = writeln!(tw, " ping\tPing the daemon (peer ping not yet supported)");
let _ = writeln!(tw, " info\tShow daemon info");
let _ = writeln!(tw, " version\tShow version");
let _ = writeln!(tw, " stats\tShow traffic statistics");
Expand All @@ -265,14 +265,20 @@ fn print_help() {
let _ = writeln!(tw, " route del <cidr>\tRemove a route");
let _ = writeln!(tw, " connect <addr>\tConnect to a peer");
let _ = writeln!(tw, " listen <addr>\tStart listening for connections");
let _ = writeln!(tw, " disconnect [peer]\tDisconnect peer");
let _ = writeln!(
tw,
" disconnect [peer]\tDrop peer session (peer may reconnect)"
);
let _ = writeln!(tw, " role\tShow current role");
let _ = writeln!(tw, " role <entry|exit|relay>\tSet role hint");
let _ = writeln!(
tw,
" hint <prefer|exclude|fixed> <role>\tApply a role hint"
" hint <prefer|exclude|fixed> <role>\tSet hint to influence auto-negotiation"
);
let _ = writeln!(
tw,
" hint auto\tRemove all hints, return to automatic negotiation"
);
let _ = writeln!(tw, " hint auto\tReturn to capability-based negotiation");
let _ = writeln!(tw, " shutdown\tShut down the daemon");
let _ = writeln!(tw, " help / ?\tShow this help");
let _ = writeln!(tw, " quit \tQuit the REPL");
Expand Down
23 changes: 20 additions & 3 deletions crates/core/src/control/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,15 +430,32 @@ impl crate::node_api::NodeApi for Handler {
))
}

fn add_route(&self, cidr: crate::Cidr, peer: String) -> crate::node_api::Result<()> {
fn add_route(
&self,
cidr: crate::Cidr,
peer: String,
) -> crate::node_api::Result<Option<String>> {
// Resolve peer name by prefix (will error if not found or ambiguous)
let peer_info = self.peers.find_by_prefix(&peer)?;

let (_, new_entry) = self.routes.add(cidr, peer_info.name);
let (_, new_entry) = self.routes.add(cidr, peer_info.name.clone());
let _ = self
.route_updates
.send(super::routes::RouteUpdate::Add(new_entry));
Ok(())

// Warn if the peer advertises routes but none of them cover the new CIDR.
// If the peer has no auto-routes at all, it may not advertise routes at all
// (e.g. an explicit-mode peer), so silence the warning in that case.
let auto_routes = self.routes.auto_routes_for_peer(&peer_info.name);
if !auto_routes.is_empty() && !auto_routes.iter().any(|r| r.cidr.contains(&cidr)) {
let warning = format!(
"peer {} does not advertise a route covering {cidr}; traffic may not reach the destination",
peer_info.name,
);
return Ok(Some(warning));
}

Ok(None)
}

fn route_del(&self, cidr: &crate::Cidr) -> crate::node_api::Result<()> {
Expand Down
42 changes: 42 additions & 0 deletions crates/core/src/control/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,21 @@ impl RouteTable {
removed
}

/// List all auto-managed routes for a specific peer.
///
/// Returns routes that were installed from the peer's handshake
/// advertisement. Used to check whether a manually added route is
/// covered by what the peer actually advertises.
#[must_use]
pub fn auto_routes_for_peer(&self, peer: &str) -> Vec<RouteEntry> {
self.routes
.load()
.values()
.filter(|entry| entry.peer == peer && entry.auto_managed)
.cloned()
.collect()
}

/// Look up a route by CIDR.
#[must_use]
pub fn get(&self, cidr: &Cidr) -> Option<RouteEntry> {
Expand Down Expand Up @@ -258,4 +273,31 @@ mod tests {
let removed = table.remove_by_peer("no-such-peer");
assert!(removed.is_empty());
}

#[test]
fn test_auto_routes_for_peer() {
let table = RouteTable::new();
let cidr_auto: Cidr = "10.99.1.0/24".parse().unwrap();
let cidr_manual: Cidr = "10.99.3.0/24".parse().unwrap();
let cidr_other: Cidr = "10.99.2.0/24".parse().unwrap();

table.add_auto(cidr_auto, "peer-1".into());
table.add(cidr_manual, "peer-1".into());
table.add_auto(cidr_other, "peer-2".into());

let peer1_auto = table.auto_routes_for_peer("peer-1");
assert_eq!(
peer1_auto.len(),
1,
"only auto routes for peer-1 should be returned"
);
assert_eq!(peer1_auto[0].cidr, cidr_auto);

let peer2_auto = table.auto_routes_for_peer("peer-2");
assert_eq!(peer2_auto.len(), 1);
assert_eq!(peer2_auto[0].cidr, cidr_other);

let no_auto = table.auto_routes_for_peer("unknown-peer");
assert!(no_auto.is_empty());
}
}
2 changes: 1 addition & 1 deletion crates/core/src/entry/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ impl<D: Device + Send + 'static> ConnectionManager<D> {
let transport = Arc::clone(&self.transport);
let metrics = self.metrics.clone();
tokio::spawn(async move {
if let Err(e) = run_tcp_session(stream, transport).await {
if let Err(e) = run_tcp_session(stream, transport, metrics.clone()).await {
tracing::debug!("TCP session ended: {e}");
}
metrics.dec_active_connections();
Expand Down
8 changes: 7 additions & 1 deletion crates/core/src/entry/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use wallhack_entry_stack::async_stack::tcp_stream::TcpStream;
use wallhack_transport::{BiStream as _, ErasedTransport, TransportError};
use wallhack_wire::data::{ResponseStatus, SessionProtocol, TcpStreamHeader, TcpStreamStatus};

use crate::transport::protocol::{AsyncProtoRead as _, AsyncProtoWrite as _};
use crate::{
control::metrics::SharedMetrics,
transport::protocol::{AsyncProtoRead as _, AsyncProtoWrite as _},
};

#[derive(Debug, thiserror::Error)]
pub enum Error {
Expand All @@ -19,6 +22,7 @@ pub enum Error {
pub async fn run_tcp_session<D>(
mut local: TcpStream<D>,
transport: Arc<dyn ErasedTransport>,
metrics: SharedMetrics,
) -> Result<(), Error>
where
D: smoltcp::phy::Device + Send + 'static,
Expand Down Expand Up @@ -64,6 +68,8 @@ where
match copy_bidirectional_with_sizes(&mut local, &mut remote, 64 * 1024, 64 * 1024).await {
Ok((to_remote, to_local)) => {
tracing::debug!(?target, to_remote, to_local, "copy_bidirectional completed");
metrics.inc_bytes_out(to_remote);
metrics.inc_bytes_in(to_local);
}
Err(e) => {
tracing::debug!(?target, error = %e, "copy_bidirectional failed");
Expand Down
33 changes: 26 additions & 7 deletions crates/core/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,14 @@ fn dispatch_request(request: &ManagementRequest, api: &dyn NodeApi) -> Managemen

Some(management_request::Request::RouteAdd(req)) => match req.cidr.parse() {
Ok(cidr) => match api.add_route(cidr, req.peer.clone()) {
Ok(()) => management_response::Response::Ok(OkResponse {}),
Ok(warning) => {
if let Some(ref msg) = warning {
tracing::warn!("{msg}");
}
management_response::Response::Ok(OkResponse {
warning: warning.unwrap_or_default(),
})
}
Err(e) => error_response(&e),
},
Err(_) => management_response::Response::Error(ErrorResponse {
Expand All @@ -367,7 +374,9 @@ fn dispatch_request(request: &ManagementRequest, api: &dyn NodeApi) -> Managemen

Some(management_request::Request::RouteDel(req)) => match req.cidr.parse() {
Ok(cidr) => match api.route_del(&cidr) {
Ok(()) => management_response::Response::Ok(OkResponse {}),
Ok(()) => management_response::Response::Ok(OkResponse {
warning: String::new(),
}),
Err(e) => error_response(&e),
},
Err(_) => management_response::Response::Error(ErrorResponse {
Expand All @@ -383,7 +392,9 @@ fn dispatch_request(request: &ManagementRequest, api: &dyn NodeApi) -> Managemen
api.peer_disconnect(req.peer.clone())
};
match result {
Ok(()) => management_response::Response::Ok(OkResponse {}),
Ok(()) => management_response::Response::Ok(OkResponse {
warning: String::new(),
}),
Err(ref e) => error_response(e),
}
}
Expand Down Expand Up @@ -412,15 +423,19 @@ fn dispatch_request(request: &ManagementRequest, api: &dyn NodeApi) -> Managemen
},

Some(management_request::Request::Disconnect(_)) => match api.disconnect() {
Ok(()) => management_response::Response::Ok(OkResponse {}),
Ok(()) => management_response::Response::Ok(OkResponse {
warning: String::new(),
}),
Err(e) => error_response(&e),
},

Some(management_request::Request::Shutdown(_)) => {
// Shutdown is handled by the caller via DaemonHandle, not NodeApi.
// Return Ok here — the daemon layer should intercept ShutdownRequest
// before it reaches dispatch, or handle it after dispatch returns.
management_response::Response::Ok(OkResponse {})
management_response::Response::Ok(OkResponse {
warning: String::new(),
})
}

Some(management_request::Request::HintSet(req)) => {
Expand All @@ -431,13 +446,17 @@ fn dispatch_request(request: &ManagementRequest, api: &dyn NodeApi) -> Managemen
target: target.into(),
};
match api.hint_set(hint) {
Ok(()) => management_response::Response::Ok(OkResponse {}),
Ok(()) => management_response::Response::Ok(OkResponse {
warning: String::new(),
}),
Err(e) => error_response(&e),
}
}

Some(management_request::Request::HintSetAuto(_)) => match api.hint_set_auto() {
Ok(()) => management_response::Response::Ok(OkResponse {}),
Ok(()) => management_response::Response::Ok(OkResponse {
warning: String::new(),
}),
Err(e) => error_response(&e),
},

Expand Down
6 changes: 5 additions & 1 deletion crates/core/src/node_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,11 @@ pub trait NodeApi: Send + Sync {
///
/// Only supported on entry nodes. Returns error for exit/relay nodes.
/// Peer must be directly connected.
fn add_route(&self, cidr: Cidr, peer: String) -> Result<()>;
///
/// Returns `Ok(Some(warning))` when the route was added but the peer's
/// advertised routes do not cover the requested CIDR, meaning traffic
/// may be silently dropped. Returns `Ok(None)` on clean success.
fn add_route(&self, cidr: Cidr, peer: String) -> Result<Option<String>>;

/// Delete a route by CIDR.
///
Expand Down
Loading