puressh 0.0.5

A pure-Rust SSH (Secure Shell) protocol library, in the spirit of libssh, built on purecrypto.
Documentation
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
//! Server-side glue for `tcpip-forward` / `cancel-tcpip-forward`
//! (RFC 4254 §7.1) and the matching `forwarded-tcpip` channel-opens
//! (RFC 4254 §7.2).
//!
//! Implements [`DefaultTcpipForwardHandler`], the in-process backing for
//! the [`crate::server::TcpipForwardHandler`] trait. The handler:
//!
//! - On `bind`, opens a real [`std::net::TcpListener`] for the requested
//!   address and port (`port == 0` picks any free port), and returns the
//!   actually-assigned port back to the server, which echoes it to the
//!   client per the RFC.
//! - Spawns one worker thread per binding. For each accepted TCP
//!   connection on the bound port the worker calls
//!   [`crate::server::ForwardContext::open_forwarded_tcpip`] to ask the
//!   per-connection server loop to open a `forwarded-tcpip` channel back
//!   toward the client, then splices the TCP socket against the resulting
//!   [`crate::server::ChannelStream`] in both directions until either
//!   side hangs up.
//! - On `unbind`, signals the worker thread to stop and drops the
//!   listener.

use std::collections::BTreeMap;
use std::io::{ErrorKind, Read, Write};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;

use crate::error::{Error, Result};
use crate::server::{ChannelEgress, ForwardContext, TcpipForwardHandler};

/// How often the accept-loop polls the non-blocking listener while
/// waiting for either a connection or the stop flag.
const ACCEPT_POLL_INTERVAL: Duration = Duration::from_millis(100);

/// One live `tcpip-forward` binding. Dropping it signals the worker
/// thread to stop and joins it. The thread observes the flag in between
/// `accept()` polls, then exits, releasing the [`TcpListener`].
struct Binding {
    stop: Arc<AtomicBool>,
    /// Carrying an `Option` so the destructor can take ownership of the
    /// `JoinHandle` and call `.join()`.
    handle: Option<JoinHandle<()>>,
}

impl Drop for Binding {
    fn drop(&mut self) {
        self.stop.store(true, Ordering::SeqCst);
        if let Some(h) = self.handle.take() {
            let _ = h.join();
        }
    }
}

/// Filter callback type for [`DefaultTcpipForwardHandler::with_allow_filter`].
type AllowFilter = Box<dyn Fn(&str, &str, u16) -> bool + Send + Sync>;

/// Internal policy describing which bind requests the handler will honour
/// and which bind address it ultimately listens on.
enum Policy {
    /// Refuse every bind. The default constructed by [`DefaultTcpipForwardHandler::new`].
    Deny,
    /// Permit any bind but force the kernel-facing listener onto loopback
    /// (`127.0.0.1` for IPv4, `::1` for IPv6) regardless of what the client
    /// asked for. Matches OpenSSH's `GatewayPorts=no` (the default).
    LocalhostOnly,
    /// Honour the client's bind address verbatim (including `0.0.0.0` /
    /// `::`). Restores the pre-2026-05 default — only safe on
    /// single-tenant servers.
    AllInterfaces,
    /// Defer the decision to a per-request filter. The filter sees the
    /// authenticated user plus the address/port the client asked for. The
    /// bind address is honoured verbatim if the filter returns `true`.
    Filter(AllowFilter),
}

/// Default in-process backing for `tcpip-forward` / `cancel-tcpip-forward`.
///
/// One instance per server typically, registered via
/// [`crate::server::Config::with_tcpip_forward`]. The handler is safe to
/// share across connections — each `bind` opens its own listener and
/// tracks it by the (`bind_address`, returned-port) key.
///
/// # Default-deny
///
/// A bare [`DefaultTcpipForwardHandler::new`] is **default-deny**: every
/// `tcpip-forward` request is refused at the policy layer (the per-
/// connection dispatcher surfaces it as `SSH_MSG_REQUEST_FAILURE` on the
/// wire). This is the safe default on a multi-tenant bastion, where the
/// historical "allow everything, bind on all interfaces" behaviour let any
/// authenticated user publish a tunnel on the host's public IP.
///
/// Operators must explicitly choose one of:
///
/// - [`Self::permit_localhost_only`] — the OpenSSH `GatewayPorts=no`
///   default. Honours the bind request but forces the kernel-facing
///   listener onto `127.0.0.1` (or `::1`) regardless of what the client
///   asked for. Even a client that asks for `0.0.0.0` ends up on loopback
///   — there is no way for an authenticated user to publish on the public
///   IP under this policy.
/// - [`Self::permit_all_interfaces`] — the pre-2026-05 default. Lets the
///   client bind anything, including `0.0.0.0` / `::`. Only safe on
///   single-tenant servers (one trusted operator).
/// - [`Self::with_allow_filter`] — a custom per-request decision (e.g.
///   "alice may bind any port on 127.0.0.1; bob may not bind anything").
///   The bind address is honoured verbatim, so the filter is responsible
///   for refusing `""` / `0.0.0.0` / `::` if the operator doesn't want
///   gateway-port behaviour.
///
/// ```ignore
/// use puressh::forwarding::reverse::DefaultTcpipForwardHandler;
///
/// // OpenSSH GatewayPorts=no equivalent.
/// let h = DefaultTcpipForwardHandler::permit_localhost_only();
/// ```
pub struct DefaultTcpipForwardHandler {
    bindings: Mutex<BTreeMap<(String, u16), Binding>>,
    policy: Policy,
}

impl Default for DefaultTcpipForwardHandler {
    fn default() -> Self {
        Self::new()
    }
}

impl DefaultTcpipForwardHandler {
    /// Build a **default-deny** handler with no active bindings.
    ///
    /// Historically (before 2026-05) `::new()` returned an allow-everything
    /// handler that also honoured `0.0.0.0` binds, letting any authenticated
    /// user publish a tunnel on the bastion's public IP. The constructor was
    /// flipped to default-deny; callers must opt into a permission policy
    /// via [`Self::permit_localhost_only`], [`Self::permit_all_interfaces`],
    /// or [`Self::with_allow_filter`].
    pub fn new() -> Self {
        Self {
            bindings: Mutex::new(BTreeMap::new()),
            policy: Policy::Deny,
        }
    }

    /// Permit every bind request but force the kernel-facing listener onto
    /// loopback (`127.0.0.1` for IPv4 / IPv4-defaulted requests, `::1` for
    /// `::`). Matches OpenSSH's `GatewayPorts=no` (the OpenSSH default).
    ///
    /// Concretely, the bind address the client sent is rewritten before the
    /// `TcpListener` is opened:
    ///
    /// | client requested | actually bound on |
    /// |---|---|
    /// | `""` / `0.0.0.0` / `127.0.0.1` / `localhost` | `127.0.0.1` |
    /// | `::` / `::1` | `::1` |
    /// | any other literal IP | refused with [`Error::Protocol`] |
    ///
    /// The protocol-level `bind_address` field reported back to the peer is
    /// **not** rewritten — the wire reply still names the address the client
    /// asked for, which mirrors OpenSSH's behaviour and lets the client log
    /// the request faithfully.
    pub fn permit_localhost_only() -> Self {
        Self {
            bindings: Mutex::new(BTreeMap::new()),
            policy: Policy::LocalhostOnly,
        }
    }

    /// Permit every bind request and honour the requested bind address
    /// verbatim, including `0.0.0.0` / `::`. Restores the pre-2026-05
    /// default. Only safe on single-tenant servers.
    pub fn permit_all_interfaces() -> Self {
        Self {
            bindings: Mutex::new(BTreeMap::new()),
            policy: Policy::AllInterfaces,
        }
    }

    /// Attach an allow filter. Each `bind` request passes
    /// `(user, bind_address, bind_port)` through the filter; a `false`
    /// return value surfaces to the peer as a `REQUEST_FAILURE` for the
    /// global request (no listener is created).
    ///
    /// Unlike [`Self::permit_localhost_only`] the bind address is **not**
    /// rewritten — if the filter returns `true` for a `0.0.0.0` request the
    /// listener will bind on all interfaces. Filters that want
    /// `GatewayPorts=no` semantics should reject the wildcard addresses
    /// explicitly (or build atop [`Self::permit_localhost_only`] instead).
    pub fn with_allow_filter<F>(mut self, filter: F) -> Self
    where
        F: Fn(&str, &str, u16) -> bool + Send + Sync + 'static,
    {
        self.policy = Policy::Filter(Box::new(filter));
        self
    }

    fn allowed(&self, user: &str, bind_address: &str, bind_port: u16) -> bool {
        match &self.policy {
            Policy::Deny => false,
            Policy::LocalhostOnly | Policy::AllInterfaces => true,
            Policy::Filter(f) => f(user, bind_address, bind_port),
        }
    }

    /// Number of bindings currently held. Useful for tests / monitoring.
    pub fn binding_count(&self) -> usize {
        self.bindings.lock().map(|m| m.len()).unwrap_or(0)
    }
}

/// Bridge a TCP socket against a server-side `ChannelStream`. Each
/// direction runs on its own thread so a slow peer in one direction can't
/// stall the other; when one direction closes we forward EOF/Close on the
/// SSH side and shut down the TCP socket so the other thread unblocks and
/// exits.
fn spawn_splice(tcp: TcpStream, stream: crate::server::ChannelStream) {
    // Peel the channel down to its raw mpsc handles so each direction can
    // be driven independently. This also suppresses the auto-EOF/Close on
    // drop — we emit those explicitly once the TCP→channel direction has
    // finished, which is the canonical splice teardown.
    let (chan_rx, chan_tx) = stream.into_raw();
    let Ok(tcp_in) = tcp.try_clone() else {
        // try_clone shouldn't fail on a freshly-accepted socket; if it
        // does, give up rather than half-spliced.
        let _ = chan_tx.send(ChannelEgress::Eof);
        let _ = chan_tx.send(ChannelEgress::Close);
        return;
    };
    let tcp_out = tcp;

    // Direction A: TCP → channel.
    let chan_tx_a = chan_tx.clone();
    let mut tcp_in_a = tcp_in;
    let a = thread::spawn(move || {
        let mut buf = [0u8; 32 * 1024];
        loop {
            match tcp_in_a.read(&mut buf) {
                Ok(0) => break,
                Ok(n) => {
                    if chan_tx_a
                        .send(ChannelEgress::Data(buf[..n].to_vec()))
                        .is_err()
                    {
                        break;
                    }
                }
                Err(e) if e.kind() == ErrorKind::Interrupted => continue,
                Err(_) => break,
            }
        }
        // Local TCP side hit EOF or error — signal half-close on the SSH
        // side. Don't send Close yet; the channel-side reader may still
        // have bytes flowing the other way.
        let _ = chan_tx_a.send(ChannelEgress::Eof);
    });

    // Direction B: channel → TCP.
    let mut tcp_out_b = tcp_out;
    let b = thread::spawn(move || {
        while let Ok(Some(chunk)) = chan_rx.recv() {
            if tcp_out_b.write_all(&chunk).is_err() {
                break;
            }
        }
        // Channel-side returned None (EOF) or an Err (channel torn down).
        // Stop reading on the local TCP side so the other thread's `read`
        // returns Ok(0) and exits.
        let _ = tcp_out_b.shutdown(std::net::Shutdown::Read);
    });

    // Reaper: when both directions have finished, send Close to drop the
    // channel cleanly.
    thread::spawn(move || {
        let _ = a.join();
        let _ = b.join();
        let _ = chan_tx.send(ChannelEgress::Close);
    });
}

fn resolve_bind(bind_address: &str, port: u16) -> Result<SocketAddr> {
    // RFC 4254 §7.1: "" / "0.0.0.0" → all interfaces; "localhost" →
    // loopback; anything else must parse as a literal IP. We deliberately
    // do not perform DNS resolution here — the SSH server should not
    // open arbitrary outbound DNS lookups based on a client request.
    match bind_address {
        "" | "0.0.0.0" => Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port)),
        "::" => Ok(SocketAddr::new(
            IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED),
            port,
        )),
        "localhost" | "127.0.0.1" => Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)),
        "::1" => Ok(SocketAddr::new(
            IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
            port,
        )),
        other => other
            .parse::<IpAddr>()
            .map(|ip| SocketAddr::new(ip, port))
            .map_err(|_| Error::Protocol("tcpip-forward: invalid bind address")),
    }
}

/// `GatewayPorts=no` rewrite: silently coerce any wildcard / IPv4-loopback
/// request to `127.0.0.1`, any IPv6 wildcard / loopback to `::1`, and
/// refuse any other literal IP (the request is sourced from the
/// authenticated peer, so a "bind on a specific public IP" request from
/// them under a localhost-only policy is a policy violation, not a typo).
fn coerce_to_loopback(bind_address: &str, port: u16) -> Result<SocketAddr> {
    match bind_address {
        "" | "0.0.0.0" | "127.0.0.1" | "localhost" => {
            Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port))
        }
        "::" | "::1" => Ok(SocketAddr::new(
            IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
            port,
        )),
        other => match other.parse::<IpAddr>() {
            Ok(ip) if ip.is_loopback() => Ok(SocketAddr::new(ip, port)),
            // Non-loopback literal under permit_localhost_only is a
            // policy violation; refuse rather than silently widening.
            _ => Err(Error::Protocol(
                "tcpip-forward: permit_localhost_only refuses non-loopback bind",
            )),
        },
    }
}

impl TcpipForwardHandler for DefaultTcpipForwardHandler {
    fn bind(
        &self,
        user: &str,
        bind_address: &str,
        bind_port: u16,
        ctx: ForwardContext,
    ) -> Result<u16> {
        // Filter-first: refuse the request without ever touching the kernel
        // if the operator's policy says no. Surface as a protocol error so
        // the per-connection dispatcher turns it into REQUEST_FAILURE on
        // the wire.
        if !self.allowed(user, bind_address, bind_port) {
            return Err(Error::Protocol("tcpip-forward: bind refused by policy"));
        }
        // `permit_localhost_only` silently rewrites the requested address to
        // loopback before opening the listener; the other policies honour
        // the client's request verbatim (the policy gate above already
        // decided whether to honour it at all).
        let addr = match self.policy {
            Policy::LocalhostOnly => coerce_to_loopback(bind_address, bind_port)?,
            Policy::Deny | Policy::AllInterfaces | Policy::Filter(_) => {
                resolve_bind(bind_address, bind_port)?
            }
        };
        let listener = TcpListener::bind(addr)?;
        let actual_port = listener.local_addr()?.port();
        listener.set_nonblocking(true)?;

        let stop = Arc::new(AtomicBool::new(false));
        let stop_thread = Arc::clone(&stop);
        let bind_address_owned = bind_address.to_string();
        let handle = thread::spawn(move || {
            while !stop_thread.load(Ordering::SeqCst) {
                match listener.accept() {
                    Ok((conn, peer)) => {
                        // Ask the per-connection server loop to open a
                        // `forwarded-tcpip` channel back to the client.
                        // Blocks until OPEN_CONFIRMATION / OPEN_FAILURE
                        // lands. If the client refuses (or the SSH
                        // connection is gone), drop the TCP socket — the
                        // user's app sees ECONNRESET, which matches
                        // OpenSSH's behaviour when no listener answers.
                        let (orig_host, orig_port) = match peer {
                            SocketAddr::V4(a) => (a.ip().to_string(), a.port()),
                            SocketAddr::V6(a) => (a.ip().to_string(), a.port()),
                        };
                        match ctx.open_forwarded_tcpip(
                            &bind_address_owned,
                            actual_port,
                            &orig_host,
                            orig_port,
                        ) {
                            Ok(channel_stream) => {
                                spawn_splice(conn, channel_stream);
                            }
                            Err(_) => {
                                let _ = conn.shutdown(std::net::Shutdown::Both);
                            }
                        }
                    }
                    Err(e) if e.kind() == ErrorKind::WouldBlock => {
                        thread::sleep(ACCEPT_POLL_INTERVAL);
                    }
                    Err(_) => break,
                }
            }
        });

        let mut map = self
            .bindings
            .lock()
            .map_err(|_| Error::Protocol("tcpip-forward: lock poisoned"))?;
        let key = (bind_address.to_string(), actual_port);
        // If somehow we already have a binding under that key (e.g. the
        // peer asked twice), drop the existing one first to keep the map
        // invariant clean.
        if let Some(existing) = map.remove(&key) {
            drop(existing);
        }
        map.insert(
            key,
            Binding {
                stop,
                handle: Some(handle),
            },
        );
        Ok(actual_port)
    }

    fn unbind(&self, _user: &str, bind_address: &str, bind_port: u16) -> Result<()> {
        let mut map = self
            .bindings
            .lock()
            .map_err(|_| Error::Protocol("tcpip-forward: lock poisoned"))?;
        let key = (bind_address.to_string(), bind_port);
        if let Some(binding) = map.remove(&key) {
            // Drop outside the lock to keep `unbind` fast for concurrent
            // callers — the `Drop` impl on `Binding` joins the worker
            // thread, which can take up to one poll interval.
            drop(map);
            drop(binding);
            Ok(())
        } else {
            Err(Error::Protocol("cancel-tcpip-forward: no such binding"))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn bind_port_zero_picks_and_returns_a_port() {
        // Old-default behaviour exercised via the explicit constructor.
        let h = DefaultTcpipForwardHandler::permit_all_interfaces();
        let port = h
            .bind("u", "127.0.0.1", 0, ForwardContext::for_test_no_opens())
            .expect("bind");
        assert!(port > 0, "kernel-assigned port should be non-zero");
        assert_eq!(h.binding_count(), 1);
        h.unbind("u", "127.0.0.1", port).expect("unbind");
        assert_eq!(h.binding_count(), 0);
    }

    #[test]
    fn unbind_releases_the_listener_so_a_fresh_bind_succeeds() {
        let h = DefaultTcpipForwardHandler::permit_all_interfaces();
        let port = h
            .bind("u", "127.0.0.1", 0, ForwardContext::for_test_no_opens())
            .expect("first bind");
        h.unbind("u", "127.0.0.1", port).expect("unbind");
        // Re-binding the *same* port (now released) must succeed.
        let again = h
            .bind("u", "127.0.0.1", port, ForwardContext::for_test_no_opens())
            .expect("rebind released port");
        assert_eq!(again, port);
        h.unbind("u", "127.0.0.1", port).expect("final unbind");
    }

    #[test]
    fn unbind_of_unknown_binding_errors() {
        let h = DefaultTcpipForwardHandler::new();
        assert!(h.unbind("u", "127.0.0.1", 12345).is_err());
    }

    #[test]
    fn invalid_bind_address_is_rejected() {
        let h = DefaultTcpipForwardHandler::permit_all_interfaces();
        // Names that aren't literal IPs (or the documented shortcuts) get
        // refused without ever touching the network. The server then
        // turns that into REQUEST_FAILURE.
        assert!(h
            .bind(
                "u",
                "not-an-ip-or-name",
                0,
                ForwardContext::for_test_no_opens(),
            )
            .is_err());
    }

    #[test]
    fn allow_filter_can_refuse_bind() {
        // Loopback-only policy: 127.0.0.1 accepted, 0.0.0.0 refused.
        let h = DefaultTcpipForwardHandler::new()
            .with_allow_filter(|_user, addr, _port| addr == "127.0.0.1");
        let port = h
            .bind("u", "127.0.0.1", 0, ForwardContext::for_test_no_opens())
            .expect("loopback bind allowed");
        assert!(h
            .bind("u", "0.0.0.0", 0, ForwardContext::for_test_no_opens())
            .is_err());
        // Filter refusal must NOT silently bind something; the binding
        // count must still be 1 (the loopback bind we made above).
        assert_eq!(h.binding_count(), 1);
        h.unbind("u", "127.0.0.1", port).expect("unbind");
    }

    #[test]
    fn allow_filter_sees_user() {
        // Refuse everyone but "alice".
        let h = DefaultTcpipForwardHandler::new()
            .with_allow_filter(|user, _addr, _port| user == "alice");
        assert!(h
            .bind("bob", "127.0.0.1", 0, ForwardContext::for_test_no_opens())
            .is_err());
        let port = h
            .bind("alice", "127.0.0.1", 0, ForwardContext::for_test_no_opens())
            .expect("alice bind allowed");
        h.unbind("alice", "127.0.0.1", port).expect("unbind");
    }

    /// `::new()` is default-deny: every bind is refused at the policy
    /// gate, even loopback ones.
    #[test]
    fn default_constructor_is_deny_all() {
        let h = DefaultTcpipForwardHandler::new();
        assert!(h
            .bind("u", "127.0.0.1", 0, ForwardContext::for_test_no_opens())
            .is_err());
        assert!(h
            .bind("u", "0.0.0.0", 0, ForwardContext::for_test_no_opens())
            .is_err());
        assert_eq!(h.binding_count(), 0);
    }

    /// `permit_localhost_only` rewrites a wildcard request to `127.0.0.1`
    /// rather than refusing it (matches OpenSSH `GatewayPorts=no`). The
    /// `binding_count` is keyed by the *client-requested* address, so it
    /// reflects the original string — but the kernel-facing listener must
    /// be on loopback.
    ///
    /// We can't reliably probe "is this bound on 0.0.0.0 or 127.0.0.1?" from
    /// within a test (a 127.0.0.1 listener still blocks a subsequent
    /// 0.0.0.0:port bind), so this test exercises the success path and
    /// trusts the `coerce_to_loopback` unit test below for the address
    /// rewrite itself.
    #[test]
    fn permit_localhost_only_accepts_wildcard_request() {
        let h = DefaultTcpipForwardHandler::permit_localhost_only();
        let port = h
            .bind("u", "0.0.0.0", 0, ForwardContext::for_test_no_opens())
            .expect("wildcard bind rewritten to loopback");
        // Loopback connect must reach the listener.
        let conn = std::net::TcpStream::connect(("127.0.0.1", port));
        assert!(conn.is_ok(), "loopback should reach the rewritten listener");
        let _ = conn.unwrap().shutdown(std::net::Shutdown::Both);
        h.unbind("u", "0.0.0.0", port).expect("unbind");
    }

    /// Direct unit test for the rewrite table — proves that every
    /// "the user asked for any-interface or loopback" spelling lands on
    /// the canonical loopback addresses.
    #[test]
    fn coerce_to_loopback_rewrite_table() {
        for v4 in ["", "0.0.0.0", "127.0.0.1", "localhost"] {
            let addr = coerce_to_loopback(v4, 22).expect("ipv4-ish should rewrite");
            assert_eq!(
                addr,
                SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 22),
                "spelling {v4:?} should rewrite to 127.0.0.1",
            );
        }
        for v6 in ["::", "::1"] {
            let addr = coerce_to_loopback(v6, 22).expect("ipv6-ish should rewrite");
            assert_eq!(
                addr,
                SocketAddr::new(IpAddr::V6(std::net::Ipv6Addr::LOCALHOST), 22),
                "spelling {v6:?} should rewrite to ::1",
            );
        }
        // A literal non-loopback IP under permit_localhost_only is a
        // policy violation, not a silent widen.
        assert!(coerce_to_loopback("192.0.2.1", 22).is_err());
        assert!(coerce_to_loopback("bogus", 22).is_err());
    }

    /// `permit_localhost_only` refuses a literal non-loopback IP request
    /// (no silent widening; the client meant a specific public IP, we
    /// don't have one to give them under this policy).
    #[test]
    fn permit_localhost_only_refuses_non_loopback_literal() {
        let h = DefaultTcpipForwardHandler::permit_localhost_only();
        assert!(h
            .bind("u", "192.0.2.1", 0, ForwardContext::for_test_no_opens(),)
            .is_err());
    }
}