address review: connect only to loopback addresses for a plain ws://localhost endpoint

This commit is contained in:
Dylan M. Taylor
2026-11-11 03:05:22 +00:00
parent 22ddc42f97
commit b59015d9ee
+34 -4
View File
@@ -242,18 +242,30 @@ pub fn shutdown(s: &TcpStream) {
/// Connect to `server` (through TLS for `wss://`): the reader's and the writer's handles, and the
/// socket to shut it down with.
pub fn open(server: &Endpoint, timeout: Duration, limits: &NetLimits) -> Result<(Duplex, Duplex, TcpStream), String> {
let stream = dial(&server.host, timeout, limits)?;
// A plain `ws://localhost` is trusted as this machine (a key may go over it), so only loopback
// addresses are connected to: a resolver mapping the name elsewhere must not carry it off-machine.
let stream = dial(&server.host, !server.secure && server.loopback(), timeout, limits)?;
let tls = if server.secure { Some(tls::handshake(&stream, &server.host)?) } else { None };
Duplex::split(stream, tls).map_err(|e| e.to_string())
}
/// Connect to `host` (`name:port`), trying each of its addresses within `timeout`, with Nagle off
/// and reads and writes bounded by `limits`' hello and write timeouts.
fn dial(host: &str, timeout: Duration, limits: &NetLimits) -> Result<TcpStream, String> {
/// Whether `a` may be connected to: any address, or with `loopback_only` just this machine's.
fn dialable(a: &std::net::SocketAddr, loopback_only: bool) -> bool {
!loopback_only || a.ip().is_loopback()
}
/// Connect to `host` (`name:port`), trying each of its addresses within `timeout` (only loopback
/// ones with `loopback_only`), with Nagle off and reads and writes bounded by `limits`' hello and
/// write timeouts.
fn dial(host: &str, loopback_only: bool, timeout: Duration, limits: &NetLimits) -> Result<TcpStream, String> {
use std::net::ToSocketAddrs;
let addrs = host.to_socket_addrs().map_err(|e| format!("can't find {host}: {e}"))?;
let mut last = format!("can't find {host}");
for a in addrs {
if !dialable(&a, loopback_only) {
last = format!("{host} resolves to {a}, which is not this machine");
continue;
}
match TcpStream::connect_timeout(&a, timeout) {
Ok(s) => {
let _ = s.set_nodelay(true);
@@ -318,3 +330,21 @@ impl Bucket {
self.tokens = (self.tokens + n).min(self.burst);
}
}
#[cfg(test)]
mod dial_tests {
use super::dialable;
use std::net::SocketAddr;
#[test]
fn a_loopback_only_dial_skips_addresses_off_this_machine() {
let at = |s: &str| s.parse::<SocketAddr>().unwrap();
for a in ["127.0.0.1:9", "[::1]:9", "127.5.5.5:9"] {
assert!(dialable(&at(a), true), "{a}");
}
for a in ["10.0.0.5:9", "203.0.113.7:9", "[2001:db8::1]:9"] {
assert!(!dialable(&at(a), true), "{a}");
assert!(dialable(&at(a), false), "{a} when anywhere is allowed");
}
}
}