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
3 changes: 1 addition & 2 deletions crates/cli/src/bin/wallhack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
//! Invoked as `wallhack` (no args, slim build): starts daemon engine directly.
//! Invoked as `wallhack --connect HOST [...]`: daemon in auto-negotiated mode.
//! Invoked as `wallhack --listen ADDR [...]`: daemon in auto-negotiated mode.
//! Invoked as `wallhack --role ROLE [...]`: daemon with fixed role hint.
//! Invoked as `wallhack entry/exit/relay [...]`: daemon with explicit role override.
//! Invoked as `wallhack --role ROLE [...]`: daemon with fixed role override.
//! Invoked as `wallhackd` (or `wallhack daemon`): daemon engine directly.
//! Invoked as `wallhackctl`: IPC control client only; fails if daemon not running.
//! Invoked as `wallhack <control-subcommand>`: IPC control client.
Expand Down
4 changes: 2 additions & 2 deletions crates/cli/src/daemon_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ use wallhackd::{

/// Network pivoting and tunneling tool.
///
/// Auto-negotiates role from `--connect` / `--listen` flags. Use a subcommand
/// (`entry`, `exit`, `relay`) to override with an explicit role.
/// Auto-negotiates role from `--connect` / `--listen` flags. Use `--role` to
/// override with an explicit role.
#[allow(clippy::struct_excessive_bools)] // Independent CLI flags, not related state
#[derive(FromArgs, Debug, Clone)]
pub struct WallhackCli {
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/control/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ impl Handler {
fn do_disconnect(&self, id: &str) {
self.peers.send_disconnect(id, "disconnected by API");
let _ = self.peers.unregister(id);
tracing::info!("Peer disconnected: {id} (via API)");
}
}

Expand Down
33 changes: 10 additions & 23 deletions crates/daemon/src/mode/auto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,18 +412,17 @@ async fn run_auto_connect_session_dispatch(
NegotiationResult::Indeterminate { .. } => NodeRole::Indeterminate,
};
node_state.update_role(negotiated_role);
let peer_role = super::peer_role_from_capabilities(peer_hs.capabilities.unwrap_or_default());
tracing::info!(
"Role resolved: peer={} addr={peer_addr} local_role={negotiated_role} peer_role={peer_role}",
peer_hs.name,
);

match result {
NegotiationResult::Resolved {
role: NodeRole::Entry,
..
} => {
tracing::info!(
"Role resolved: name={} addr={peer_addr} role=entry",
peer_hs.name,
);
node_state.update_role(NodeRole::Entry);

// Install routes advertised by the exit peer. The inner function
// applies routes from the table when it creates the TUN, so they
// must be in the table before we call it.
Expand Down Expand Up @@ -477,11 +476,6 @@ async fn run_auto_connect_session_dispatch(
role: NodeRole::Exit,
..
} => {
tracing::info!(
"Role resolved: name={} addr={peer_addr} role=exit",
peer_hs.name,
);
node_state.update_role(NodeRole::Exit);
let peer_caps = peer_hs.capabilities.unwrap_or_default();
let peer_role = super::peer_role_from_capabilities(peer_caps);
let peer_name = if peer_hs.name.is_empty() {
Expand Down Expand Up @@ -870,18 +864,17 @@ async fn run_auto_accept_session_inner(
NegotiationResult::Indeterminate { .. } => NodeRole::Indeterminate,
};
node_state.update_role(negotiated_role);
let peer_role = super::peer_role_from_capabilities(peer_hs.capabilities.unwrap_or_default());
tracing::info!(
"Role resolved: peer={} addr={peer_addr} local_role={negotiated_role} peer_role={peer_role}",
peer_hs.name,
);

match result {
NegotiationResult::Resolved {
role: NodeRole::Entry,
..
} => {
tracing::info!(
"Role resolved: name={} addr={peer_addr} role=entry",
peer_hs.name,
);
node_state.update_role(NodeRole::Entry);

// Spawn data tasks: incoming (peer→instructions/responses) + outgoing (instructions→peer).
super::entry::spawn_data_tasks(
&transport,
Expand Down Expand Up @@ -1040,12 +1033,6 @@ async fn run_auto_accept_session_inner(
role: NodeRole::Exit,
..
} => {
tracing::info!(
"Role resolved: name={} addr={peer_addr} role=exit",
peer_hs.name,
);
node_state.update_role(NodeRole::Exit);

// Spawn data tasks for exit: incoming (peer→broadcasts) + outgoing (responses→peer).
{
let transport = Arc::clone(&transport);
Expand Down
38 changes: 15 additions & 23 deletions crates/daemon/src/netlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,10 @@ pub(crate) fn add_os_route(cidr: &str, dev: &str) -> Result<(), String> {

/// Receive and check the Netlink ACK/error response.
///
/// `NLMSG_ERROR` (type 2) carries a 4-byte `i32` error code at the start of its
/// payload. Error 0 = success (pure ACK), negative = errno.
/// `-3` (ESRCH) after route delete and `-17` (EEXIST) after route add are
/// treated as success (idempotent operations).
/// neli 0.7 parses `NLMSG_ERROR` into `NlPayload::Ack` (error=0) or
/// `NlPayload::Err` (error<0). We check the error code and treat
/// `-3` (ESRCH, already gone) and `-17` (EEXIST, already present) as
/// success for idempotent route operations.
fn recv_netlink_ack(socket: &mut NlSocketHandle, op: &str) -> Result<(), String> {
let (mut iter, _groups) = socket
.recv::<u16, neli::types::Buffer>()
Expand All @@ -158,28 +158,20 @@ fn recv_netlink_ack(socket: &mut NlSocketHandle, op: &str) -> Result<(), String>
};
let msg = msg_result.map_err(|e| format!("Netlink recv error: {e}"))?;

// NLMSG_ERROR = 2
if *msg.nl_type() == 2 {
if let NlPayload::Payload(buf) = msg.nl_payload() {
let bytes: &[u8] = buf.as_ref();
if bytes.len() >= 4 {
let error = i32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
// 0 = success, -3 = ESRCH (already gone), -17 = EEXIST (already present)
if error == 0 || error == -3 || error == -17 {
Ok(())
} else {
let err_msg = format!("Netlink error: {error}");
tracing::warn!("Failed to {op}: {err_msg}");
Err(err_msg)
}
match msg.nl_payload() {
NlPayload::Ack(_) | NlPayload::Empty => Ok(()),
NlPayload::Err(err) => {
let code = *err.error();
// -3 = ESRCH (route already gone), -17 = EEXIST (route already present)
if code == -3 || code == -17 {
Ok(())
} else {
Err("Netlink ACK payload too short".into())
let err_msg = format!("Netlink error: {code}");
tracing::warn!("Failed to {op}: {err_msg}");
Err(err_msg)
}
} else {
Err("Unexpected payload in ACK".into())
}
} else {
Err(format!("Unexpected message type: {}", msg.nl_type()))
_ => Err(format!("Unexpected Netlink response for {op}")),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/mcp/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub fn format_response(resp: &ManagementResponse) -> Result<String, String> {
let mut out = String::new();
for route in &r.routes {
let tag = if route.auto_managed { " (auto)" } else { "" };
let _ = writeln!(out, "{} {}{tag}", route.cidr, route.peer);
let _ = writeln!(out, "{} via {}{tag}", route.cidr, route.peer);
}
Ok(out)
}
Expand Down
2 changes: 1 addition & 1 deletion crates/mcp/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async fn ipc_call(request: management_request::Request) -> Result<String, rmcp::
.await
.map_err(|e| rmcp::ErrorData::internal_error(e.to_string(), None))?;

convert::format_response(&resp).map_err(|msg| rmcp::ErrorData::internal_error(msg, None))
convert::format_response(&resp).map_err(|msg| rmcp::ErrorData::invalid_params(msg, None))
}

#[rmcp::tool_router(vis = "pub")]
Expand Down
Loading
Loading