1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! Thin CLI wrapper around `auths-pairing-daemon` for LAN pairing.
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use tokio_util::sync::CancellationToken;
use auths_pairing_daemon::{HostAllowlist, PairingDaemonBuilder, PairingDaemonHandle};
use auths_sdk::pairing::{CreateSessionRequest, SubmitConfirmationRequest, SubmitResponseRequest};
/// Detect the LAN IP address of this machine.
pub fn detect_lan_ip() -> std::io::Result<IpAddr> {
let network = auths_pairing_daemon::IfAddrsNetworkInterfaces;
auths_pairing_daemon::NetworkInterfaces::detect_lan_ip(&network)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::AddrNotAvailable, e.to_string()))
}
/// An ephemeral HTTP server that serves exactly one pairing session.
pub struct LanPairingServer {
addr: SocketAddr,
cancel: CancellationToken,
handle: PairingDaemonHandle,
_task: tokio::task::JoinHandle<()>,
pairing_token_b64: String,
}
impl LanPairingServer {
/// Start the LAN pairing server bound to a specific LAN IP.
///
/// Args:
/// * `session`: The pairing session request data.
/// * `bind_ip`: The LAN IP to bind to (from `detect_lan_ip()`).
pub async fn start(session: CreateSessionRequest, bind_ip: IpAddr) -> anyhow::Result<Self> {
let daemon = PairingDaemonBuilder::new().build(session)?;
let pairing_token_b64 = daemon.token().to_string();
// Bind FIRST so we know the port, then build the router with
// a Host/Origin/Referer allowlist scoped to the bound
// `SocketAddr`. Reversing this order would either leave the
// port unknown (fail-closed allowlist → 421 for every request)
// or require mutable state in the middleware.
let cancel = CancellationToken::new();
let listener = tokio::net::TcpListener::bind(SocketAddr::new(bind_ip, 0))
.await
.map_err(|e| {
anyhow::anyhow!(
"Could not bind to {} — check that your device is on the correct \
network, or use relay-based pairing. ({})",
bind_ip,
e
)
})?;
let addr = listener.local_addr()?;
let allowlist = HostAllowlist::for_bound_addr(addr, None);
let (router, handle) = daemon.into_parts(allowlist);
let cancel_clone = cancel.clone();
let task = tokio::spawn(async move {
let server = axum::serve(
listener,
router.into_make_service_with_connect_info::<SocketAddr>(),
);
tokio::select! {
_ = server => {}
_ = cancel_clone.cancelled() => {}
}
});
Ok(Self {
addr,
cancel,
handle,
_task: task,
pairing_token_b64,
})
}
/// The address the server is listening on.
pub fn addr(&self) -> SocketAddr {
self.addr
}
/// The base64url-encoded pairing token for QR code inclusion.
pub fn pairing_token(&self) -> &str {
&self.pairing_token_b64
}
/// Advertise via mDNS if discovery is available.
pub fn advertise(
&self,
port: u16,
short_code: &str,
controller_did: &str,
) -> Option<
Result<Box<dyn auths_pairing_daemon::AdvertiseHandle>, auths_pairing_daemon::DaemonError>,
> {
self.handle.advertise(port, short_code, controller_did)
}
/// Wait for a pairing response, with a timeout.
///
/// Does NOT shut the listener down — the caller is expected to
/// also `wait_for_confirmation` (or drop the server) afterwards.
pub async fn wait_for_response(
&mut self,
timeout: Duration,
) -> Result<SubmitResponseRequest, auths_sdk::error::PairingError> {
self.handle
.wait_for_response(timeout)
.await
.map_err(|e| match e {
auths_pairing_daemon::DaemonError::Pairing(pe) => pe,
other => auths_sdk::error::PairingError::LocalServerError(other.to_string()),
})
}
/// Wait for the paired device to POST `/confirm { aborted: ... }`.
///
/// Should be called after `wait_for_response` has succeeded. The
/// phone auto-fires `/confirm` as soon as it processes `/response`,
/// so a short timeout (3-5 s) is appropriate. Returns `None` on
/// timeout — the phone side tolerates a silent daemon, so the
/// caller can proceed even if the confirmation never arrived.
pub async fn wait_for_confirmation(
&self,
timeout: Duration,
) -> Option<SubmitConfirmationRequest> {
self.handle.wait_for_confirmation(timeout).await
}
/// Wait for the device to submit a co-authored shared-KEL rotation
/// (`POST /shared-kel-rot`), then take it for replay.
///
/// Should be called after `wait_for_response` has bound the device's
/// signing key. The daemon has already verified the envelope's indexed
/// signatures; the caller still validates it against the registry's
/// prior key state before appending
/// (`auths_sdk::domains::identity::shared_rot::apply_shared_kel_rot`).
pub async fn wait_for_shared_kel_rot(
&self,
timeout: Duration,
) -> Option<auths_sdk::pairing::SubmitSharedKelRotRequest> {
auths_sdk::pairing::lan::wait_for_shared_kel_rot(&self.handle, timeout).await
}
/// Shut the listener down. Consumes `self`.
pub fn shutdown(self) {
self.cancel.cancel();
}
}