Skip to main content

lonca_net/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3
4use std::collections::HashMap;
5use std::io::{BufRead, BufReader, Read, Write};
6use std::net::{IpAddr, SocketAddr, TcpListener, TcpStream, ToSocketAddrs};
7use std::path::PathBuf;
8use std::process::Command;
9use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
10use std::sync::{Arc, Mutex};
11use std::thread::JoinHandle;
12use std::time::{Duration, Instant};
13
14const MAX_HEAD: usize = 16 * 1024;
15
16/// Connections served at once, by every client together. One past it is
17/// answered 503 and closed: each takes a thread and two descriptors.
18pub const MAX_CONNECTIONS: usize = 256;
19
20/// How much of a tunnel is copied between two reports to
21/// [`Gate::carried`].
22const CHUNK: usize = 16 * 1024;
23
24/// One connection a client asked for.
25#[derive(Debug, Clone, Copy)]
26pub struct Request<'a> {
27    /// Lowercased host name or IP literal, as the client wrote it.
28    pub host: &'a str,
29    pub port: u16,
30    /// The client's end of the connection to the proxy.
31    pub peer: SocketAddr,
32    /// The proxy's end of it.
33    pub local: SocketAddr,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Verdict {
38    pub allow: bool,
39    /// Whether loopback and private addresses may be reached.
40    pub allow_private: bool,
41    /// Shown to the client when the connection is refused.
42    pub reason: String,
43}
44
45impl Verdict {
46    pub fn allow() -> Verdict {
47        Verdict {
48            allow: true,
49            allow_private: false,
50            reason: String::new(),
51        }
52    }
53
54    pub fn deny(reason: impl Into<String>) -> Verdict {
55        Verdict {
56            allow: false,
57            allow_private: false,
58            reason: reason.into(),
59        }
60    }
61}
62
63/// How a connection ended up, after the gate's verdict.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum Outcome {
66    /// The gate refused it.
67    Refused,
68    /// The host did not resolve.
69    Unresolved,
70    /// Every address the host resolved to is private or loopback.
71    Private,
72    /// The upstream did not accept the connection.
73    Unreachable,
74    /// The gate stopped it midway ([`Gate::carried`]).
75    Cut,
76    /// Bytes flowed both ways until one side closed.
77    Tunneled,
78}
79
80pub trait Gate: Send + Sync + 'static {
81    /// Decides whether `request` may go through.
82    fn decide(&self, request: &Request<'_>) -> Verdict;
83
84    /// Told how every request that reached [`Gate::decide`] ended.
85    fn finished(&self, _request: &Request<'_>, _outcome: Outcome) {}
86
87    /// Told of every `bytes` relayed for `request`, either way, before they
88    /// are passed on. `false` cuts the connection: a budget is spent.
89    fn carried(&self, _request: &Request<'_>, _bytes: u64) -> bool {
90        true
91    }
92
93    /// Offered every `CONNECT` tunnel the gate let through, before a byte
94    /// of it is relayed. A gate that serves the tunnel itself (to look
95    /// inside TLS for a host it holds the keys of, say) returns `None` once
96    /// it is done; returning the tunnel has the proxy relay its bytes as
97    /// they are.
98    fn relay(&self, _request: &Request<'_>, tunnel: Tunnel) -> Option<Tunnel> {
99        Some(tunnel)
100    }
101
102    /// Offered every plain (absolute `http://`) request the gate let
103    /// through, before a byte of it reaches the host: its `head` with the
104    /// target cut to the path, and the connection to the host, whose
105    /// `early` is what the client sent after the head. A gate that serves
106    /// it itself (to read each request on the connection, say) returns
107    /// `None` once it is done; handing them back has the proxy send the
108    /// head on and relay the rest as it is.
109    fn relay_plain(
110        &self,
111        _request: &Request<'_>,
112        head: String,
113        tunnel: Tunnel,
114    ) -> Option<(String, Tunnel)> {
115        Some((head, tunnel))
116    }
117}
118
119/// A connection the gate let through: for a `CONNECT` tunnel, the client
120/// has been told it is established; nothing has been relayed yet.
121pub struct Tunnel {
122    pub client: TcpStream,
123    pub upstream: TcpStream,
124    /// What the client sent after its request, which the proxy has already
125    /// read: the start of the tunnel's bytes.
126    pub early: Vec<u8>,
127}
128
129/// Binds the proxy. Only loopback addresses are accepted: the proxy is for
130/// processes on this machine.
131pub fn bind(addr: &str) -> std::io::Result<TcpListener> {
132    let l = TcpListener::bind(addr)?;
133    if !l.local_addr()?.ip().is_loopback() {
134        return Err(std::io::Error::new(
135            std::io::ErrorKind::InvalidInput,
136            "the proxy only listens on loopback",
137        ));
138    }
139    Ok(l)
140}
141
142/// Serves connections until the listener fails, one thread each.
143pub fn serve<G: Gate>(listener: TcpListener, gate: Arc<G>) {
144    accept(listener, gate, &Arc::new(Shared::default()));
145}
146
147/// A proxy serving on its own thread until [`Server::stop`], or until it is
148/// dropped.
149pub struct Server {
150    addr: SocketAddr,
151    shared: Arc<Shared>,
152    thread: Option<JoinHandle<()>>,
153}
154
155#[derive(Default)]
156struct Shared {
157    stopping: AtomicBool,
158    /// Both sides of every connection being handled, by number, so stopping
159    /// can cut tunnels that would otherwise stay open.
160    open: Mutex<HashMap<u64, Vec<TcpStream>>>,
161    next: AtomicU64,
162}
163
164impl Shared {
165    /// Adds `stream` to connection `id`; cuts it at once when stopping has
166    /// begun, since nothing will cut it later.
167    fn track(&self, id: u64, stream: &TcpStream) {
168        let mut open = self.open.lock().unwrap();
169        if self.stopping.load(Ordering::SeqCst) {
170            let _ = stream.shutdown(std::net::Shutdown::Both);
171        } else if let Ok(clone) = stream.try_clone() {
172            open.entry(id).or_default().push(clone);
173        }
174    }
175}
176
177/// Starts serving `listener` on a new thread.
178pub fn spawn<G: Gate>(listener: TcpListener, gate: Arc<G>) -> std::io::Result<Server> {
179    let addr = listener.local_addr()?;
180    let shared = Arc::new(Shared::default());
181    let served = Arc::clone(&shared);
182    let thread = std::thread::spawn(move || accept(listener, gate, &served));
183    Ok(Server {
184        addr,
185        shared,
186        thread: Some(thread),
187    })
188}
189
190impl Server {
191    pub fn local_addr(&self) -> SocketAddr {
192        self.addr
193    }
194
195    /// Stops accepting, closes the listener and cuts every open connection;
196    /// returns once each of them has ended and the gate has heard how
197    /// ([`Gate::finished`]), or after a few seconds at most.
198    pub fn stop(mut self) {
199        self.shut();
200    }
201
202    fn shut(&mut self) {
203        let Some(thread) = self.thread.take() else {
204            return;
205        };
206        {
207            // Set under the lock: a stream tracked from here on sees the flag
208            // and is cut at once, any other is cut below.
209            let _open = self.shared.open.lock().unwrap();
210            self.shared.stopping.store(true, Ordering::SeqCst);
211        }
212        // Wakes the accept loop, which sees the flag and returns.
213        let _ = TcpStream::connect_timeout(&self.addr, Duration::from_secs(1));
214        let _ = thread.join();
215        for stream in self.shared.open.lock().unwrap().values().flatten() {
216            let _ = stream.shutdown(std::net::Shutdown::Both);
217        }
218        // Handlers leave the map once the gate has heard how they ended. One
219        // still connecting upstream (up to its 10 s timeout) is not waited for.
220        let deadline = Instant::now() + Duration::from_secs(3);
221        while !self.shared.open.lock().unwrap().is_empty() && Instant::now() < deadline {
222            std::thread::sleep(Duration::from_millis(5));
223        }
224    }
225}
226
227impl Drop for Server {
228    fn drop(&mut self) {
229        self.shut();
230    }
231}
232
233fn accept<G: Gate>(listener: TcpListener, gate: Arc<G>, shared: &Arc<Shared>) {
234    for conn in listener.incoming() {
235        if shared.stopping.load(Ordering::SeqCst) {
236            return;
237        }
238        let Ok(stream) = conn else { continue };
239        if shared.open.lock().unwrap().len() >= MAX_CONNECTIONS {
240            respond(&stream, 503, "too many connections through the proxy");
241            continue;
242        }
243        let id = shared.next.fetch_add(1, Ordering::Relaxed);
244        shared.track(id, &stream);
245        let (gate, shared) = (gate.clone(), Arc::clone(shared));
246        std::thread::spawn(move || {
247            // Stopping may have come between tracking and now.
248            if !shared.stopping.load(Ordering::SeqCst) {
249                let _ = handle(stream, &*gate, &|upstream| shared.track(id, upstream));
250            }
251            shared.open.lock().unwrap().remove(&id);
252        });
253    }
254}
255
256/// Whether `ip` is anything but the public internet: this computer, its
257/// network, a link, a documentation or benchmarking range, multicast or
258/// reserved, or any of those inside an IPv6 address (mapped, NAT64, or the
259/// old IPv4-compatible form).
260pub fn is_private(ip: IpAddr) -> bool {
261    match ip {
262        IpAddr::V4(v) => private_v4(v),
263        IpAddr::V6(v) => {
264            if let Some(v4) = v.to_ipv4_mapped() {
265                return private_v4(v4);
266            }
267            let s = v.segments();
268            // NAT64 (64:ff9b::/96) carries an IPv4 address at its end.
269            if s[..6] == [0x64, 0xff9b, 0, 0, 0, 0] {
270                let [a, b] = s[6].to_be_bytes();
271                let [c, d] = s[7].to_be_bytes();
272                return private_v4(std::net::Ipv4Addr::new(a, b, c, d));
273            }
274            v.is_unspecified()
275                || v.is_loopback()
276                || v.is_multicast()
277                || (s[0] & 0xfe00) == 0xfc00 // unique local
278                || (s[0] & 0xffc0) == 0xfe80 // link local
279                || (s[0] & 0xffc0) == 0xfec0 // site local
280                || (s[0] == 0x2001 && s[1] == 0x0db8) // documentation
281                || s[..6] == [0, 0, 0, 0, 0, 0] // IPv4-compatible, deprecated
282        }
283    }
284}
285
286/// Cloud metadata endpoints, which hand out a machine's credentials. Never
287/// reachable, whatever a gate allows: `allow_private` opens a network, not
288/// these.
289pub fn is_metadata(ip: IpAddr) -> bool {
290    let v4 = |v: std::net::Ipv4Addr| {
291        matches!(
292            v.octets(),
293            [169, 254, 169, 254] | [169, 254, 170, 2] | [169, 254, 170, 23] | [100, 100, 100, 200]
294        )
295    };
296    match ip {
297        IpAddr::V4(v) => v4(v),
298        IpAddr::V6(v) => {
299            v.to_ipv4_mapped().is_some_and(v4)
300                || v.segments() == [0xfd00, 0x0ec2, 0, 0, 0, 0, 0, 0x0254]
301        }
302    }
303}
304
305fn private_v4(ip: std::net::Ipv4Addr) -> bool {
306    let [a, b, c, _] = ip.octets();
307    ip.is_unspecified()
308        || ip.is_loopback()
309        || ip.is_private()
310        || ip.is_link_local()
311        || ip.is_broadcast()
312        || ip.is_documentation()
313        || ip.is_multicast()
314        || a == 0
315        || (a == 100 && (64..128).contains(&b)) // shared address space
316        || (a == 192 && b == 0 && c == 0) // IETF protocol assignments
317        || (a == 198 && (18..20).contains(&b)) // benchmarking
318        || a >= 240 // reserved
319}
320
321/// Splits `host:port` (with `[v6]` brackets) and checks it.
322fn parse_authority(a: &str, default_port: u16) -> Option<(String, u16)> {
323    let (host, port) = if let Some(rest) = a.strip_prefix('[') {
324        let (h, tail) = rest.split_once(']')?;
325        let port = tail
326            .strip_prefix(':')
327            .map(|p| p.parse().ok())
328            .unwrap_or(Some(default_port))?;
329        (h.to_string(), port)
330    } else {
331        match a.rsplit_once(':') {
332            Some((h, p)) => (h.to_string(), p.parse().ok()?),
333            None => (a.to_string(), default_port),
334        }
335    };
336    let ok = !host.is_empty()
337        && host.len() <= 253
338        && host
339            .chars()
340            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | ':'));
341    ok.then_some((host.to_ascii_lowercase(), port))
342}
343
344/// Working directory of the process on the other end of `peer`, if the
345/// operating system tells us (same user only).
346pub fn peer_cwd(peer: SocketAddr, local: SocketAddr) -> Option<PathBuf> {
347    let me = std::process::id().to_string();
348    let out = Command::new("lsof")
349        .args([
350            "-nP",
351            &format!("-iTCP@{}:{}", peer.ip(), peer.port()),
352            "-sTCP:ESTABLISHED",
353            "-Fpn",
354        ])
355        .output()
356        .ok()?;
357    let text = String::from_utf8_lossy(&out.stdout);
358    // Records: p<pid> then n<local>-><remote>. The client is the process
359    // whose local end is `peer` and whose remote end is our listener.
360    let want = format!(
361        "{}:{}->{}:{}",
362        peer.ip(),
363        peer.port(),
364        local.ip(),
365        local.port()
366    );
367    let mut pid = None;
368    let mut cur = None;
369    for line in text.lines() {
370        if let Some(p) = line.strip_prefix('p') {
371            cur = Some(p.to_string());
372        } else if let Some(n) = line.strip_prefix('n')
373            && n == want
374            && cur.as_deref() != Some(me.as_str())
375        {
376            pid = cur.clone();
377        }
378    }
379    let pid = pid?;
380    let out = Command::new("lsof")
381        .args(["-a", "-p", &pid, "-d", "cwd", "-Fn"])
382        .output()
383        .ok()?;
384    String::from_utf8_lossy(&out.stdout)
385        .lines()
386        .find_map(|l| l.strip_prefix('n').map(PathBuf::from))
387}
388
389/// The first of `addrs` that takes a connection, tried in order within one
390/// budget, as a client does: a name whose first address has no listener
391/// (`localhost` as `::1` on Linux, with the server on `127.0.0.1` only) is
392/// still reached on the next. Only addresses the rules allow are passed in.
393fn connect_any(addrs: &[SocketAddr], within: Duration) -> Option<TcpStream> {
394    let deadline = Instant::now() + within;
395    addrs.iter().find_map(|a| {
396        TcpStream::connect_timeout(a, deadline.saturating_duration_since(Instant::now())).ok()
397    })
398}
399
400fn respond(mut s: &TcpStream, code: u16, reason: &str) {
401    let body = format!("proxy: {reason}\n");
402    let _ = write!(
403        s,
404        "HTTP/1.1 {code} {reason}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
405        body.len()
406    );
407}
408
409/// Serves one client; `track` is shown the upstream connection, if one is
410/// made, so it can be cut from outside.
411fn handle(client: TcpStream, gate: &dyn Gate, track: &dyn Fn(&TcpStream)) -> std::io::Result<()> {
412    client.set_read_timeout(Some(Duration::from_secs(30)))?;
413    let peer = client.peer_addr()?;
414    let local = client.local_addr()?;
415    let mut reader = BufReader::new(client.try_clone()?);
416    let mut head = Vec::new();
417    loop {
418        let mut line = Vec::new();
419        let n = reader
420            .by_ref()
421            .take((MAX_HEAD - head.len()) as u64)
422            .read_until(b'\n', &mut line)?;
423        if n == 0 || head.len() + n >= MAX_HEAD {
424            respond(&client, 400, "bad request");
425            return Ok(());
426        }
427        head.extend_from_slice(&line);
428        if line == b"\r\n" || line == b"\n" {
429            break;
430        }
431    }
432    let head_text = String::from_utf8_lossy(&head).into_owned();
433    let first = head_text.lines().next().unwrap_or("");
434    let mut parts = first.split_whitespace();
435    let (method, target) = (parts.next().unwrap_or(""), parts.next().unwrap_or(""));
436    let (authority, rest_of_request) = if method.eq_ignore_ascii_case("CONNECT") {
437        (parse_authority(target, 443), None)
438    } else if let Some(url) = target.strip_prefix("http://") {
439        let (auth, path) = url.split_at(url.find('/').unwrap_or(url.len()));
440        let path = if path.is_empty() { "/" } else { path };
441        let rewritten = head_text.replacen(target, path, 1);
442        (parse_authority(auth, 80), Some(rewritten))
443    } else {
444        respond(&client, 400, "only CONNECT and absolute http:// requests");
445        return Ok(());
446    };
447    let Some((host, port)) = authority else {
448        respond(&client, 400, "bad target");
449        return Ok(());
450    };
451    let request = Request {
452        host: &host,
453        port,
454        peer,
455        local,
456    };
457    let verdict = gate.decide(&request);
458    if !verdict.allow {
459        respond(&client, 403, &verdict.reason);
460        gate.finished(&request, Outcome::Refused);
461        return Ok(());
462    }
463    let addrs: Vec<SocketAddr> = match (host.as_str(), port).to_socket_addrs() {
464        Ok(a) => a.collect(),
465        Err(_) => {
466            respond(&client, 502, "cannot resolve host");
467            gate.finished(&request, Outcome::Unresolved);
468            return Ok(());
469        }
470    };
471    if !addrs.is_empty() && addrs.iter().all(|a| is_metadata(a.ip())) {
472        respond(&client, 403, "cloud metadata addresses are never reachable");
473        gate.finished(&request, Outcome::Private);
474        return Ok(());
475    }
476    let allowed: Vec<SocketAddr> = addrs
477        .into_iter()
478        .filter(|a| !is_metadata(a.ip()))
479        .filter(|a| verdict.allow_private || !is_private(a.ip()))
480        .collect();
481    if allowed.is_empty() {
482        respond(
483            &client,
484            403,
485            "private and loopback addresses are not reachable",
486        );
487        gate.finished(&request, Outcome::Private);
488        return Ok(());
489    }
490    let Some(upstream) = connect_any(&allowed, Duration::from_secs(10)) else {
491        respond(&client, 502, "cannot connect");
492        gate.finished(&request, Outcome::Unreachable);
493        return Ok(());
494    };
495    track(&upstream);
496    client.set_read_timeout(None)?;
497    // Bytes already buffered after the head belong to the tunnel.
498    let buffered = reader.buffer().to_vec();
499    drop(reader);
500    let (client, upstream, buffered) = match rest_of_request {
501        None => {
502            let mut c = &client;
503            c.write_all(b"HTTP/1.1 200 Connection established\r\n\r\n")?;
504            let tunnel = Tunnel {
505                client,
506                upstream,
507                early: buffered,
508            };
509            match gate.relay(&request, tunnel) {
510                Some(t) => (t.client, t.upstream, t.early),
511                None => {
512                    gate.finished(&request, Outcome::Tunneled);
513                    return Ok(());
514                }
515            }
516        }
517        Some(h) => {
518            let tunnel = Tunnel {
519                client,
520                upstream,
521                early: buffered,
522            };
523            match gate.relay_plain(&request, h, tunnel) {
524                Some((h, t)) => {
525                    (&t.upstream).write_all(h.as_bytes())?;
526                    (t.client, t.upstream, t.early)
527                }
528                None => {
529                    gate.finished(&request, Outcome::Tunneled);
530                    return Ok(());
531                }
532            }
533        }
534    };
535    let mut up_w = upstream.try_clone()?;
536    if !buffered.is_empty() {
537        if !gate.carried(&request, buffered.len() as u64) {
538            gate.finished(&request, Outcome::Cut);
539            return Ok(());
540        }
541        up_w.write_all(&buffered)?;
542    }
543    let cut = AtomicBool::new(false);
544    let (up_r, client_w) = (upstream.try_clone()?, client.try_clone()?);
545    std::thread::scope(|scope| {
546        scope.spawn(|| {
547            if !carry(&up_r, &client_w, gate, &request) {
548                cut.store(true, Ordering::SeqCst);
549                let _ = up_r.shutdown(std::net::Shutdown::Both);
550                let _ = client_w.shutdown(std::net::Shutdown::Both);
551            }
552            let _ = client_w.shutdown(std::net::Shutdown::Write);
553        });
554        if !carry(&client, &up_w, gate, &request) {
555            cut.store(true, Ordering::SeqCst);
556            let _ = client.shutdown(std::net::Shutdown::Both);
557            let _ = up_w.shutdown(std::net::Shutdown::Both);
558        }
559        let _ = up_w.shutdown(std::net::Shutdown::Write);
560    });
561    let how = if cut.load(Ordering::SeqCst) {
562        Outcome::Cut
563    } else {
564        Outcome::Tunneled
565    };
566    gate.finished(&request, how);
567    Ok(())
568}
569
570/// Copies `from` to `to` until the end, telling the gate of every chunk
571/// first; `false` when the gate stopped it.
572fn carry(mut from: &TcpStream, mut to: &TcpStream, gate: &dyn Gate, request: &Request<'_>) -> bool {
573    let mut buf = vec![0u8; CHUNK];
574    loop {
575        let n = match from.read(&mut buf) {
576            Ok(0) | Err(_) => return true,
577            Ok(n) => n,
578        };
579        if !gate.carried(request, n as u64) {
580            return false;
581        }
582        if to.write_all(&buf[..n]).is_err() {
583            return true;
584        }
585    }
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591
592    #[test]
593    fn a_connection_goes_to_the_first_address_that_takes_it() {
594        // A port nobody listens on: bound, then given back.
595        let closed = TcpListener::bind("127.0.0.1:0")
596            .unwrap()
597            .local_addr()
598            .unwrap();
599        let open = TcpListener::bind("127.0.0.1:0").unwrap();
600        let at = open.local_addr().unwrap();
601        let budget = Duration::from_secs(5);
602        let s = connect_any(&[closed, at], budget).expect("the second address");
603        assert_eq!(s.peer_addr().unwrap(), at);
604        assert!(connect_any(&[closed], budget).is_none());
605        assert!(connect_any(&[], budget).is_none());
606        assert!(
607            connect_any(&[at], Duration::ZERO).is_none(),
608            "no budget, no attempt"
609        );
610    }
611
612    #[test]
613    fn metadata_addresses() {
614        for ip in [
615            "169.254.169.254",
616            "169.254.170.2",
617            "100.100.100.200",
618            "fd00:ec2::254",
619            "::ffff:169.254.169.254",
620        ] {
621            assert!(is_metadata(ip.parse().unwrap()), "{ip}");
622        }
623        for ip in ["169.254.169.253", "10.0.0.1", "fd00:ec2::253", "8.8.8.8"] {
624            assert!(!is_metadata(ip.parse().unwrap()), "{ip}");
625        }
626    }
627
628    #[test]
629    fn private_ranges() {
630        for ip in [
631            "127.0.0.1",
632            "10.1.2.3",
633            "192.168.0.1",
634            "172.16.5.4",
635            "169.254.1.1",
636            "100.64.0.1",
637            "0.0.0.0",
638            "::1",
639            "fd00::1",
640            "fe80::1",
641            "::ffff:127.0.0.1",
642            "192.0.0.1",
643            "198.18.0.1",
644            "192.0.2.1",
645            "224.0.0.1",
646            "240.0.0.1",
647            "255.255.255.255",
648            "fec0::1",
649            "ff02::1",
650            "2001:db8::1",
651            "64:ff9b::7f00:1",
652            "::127.0.0.1",
653        ] {
654            assert!(is_private(ip.parse().unwrap()), "{ip}");
655        }
656        for ip in [
657            "93.184.216.34",
658            "1.1.1.1",
659            "2606:4700::1111",
660            "::ffff:8.8.8.8",
661            "64:ff9b::808:808",
662        ] {
663            assert!(!is_private(ip.parse().unwrap()), "{ip}");
664        }
665    }
666
667    #[test]
668    fn authorities() {
669        assert_eq!(
670            parse_authority("Example.com:443", 1),
671            Some(("example.com".into(), 443))
672        );
673        assert_eq!(
674            parse_authority("example.com", 80),
675            Some(("example.com".into(), 80))
676        );
677        assert_eq!(parse_authority("[::1]:8080", 1), Some(("::1".into(), 8080)));
678        assert_eq!(parse_authority("a b:1", 1), None);
679        assert_eq!(parse_authority("x:notaport", 1), None);
680    }
681}