openlatch-client 0.3.3

OpenLatch runtime enforcement node — the capture-and-enforce adapter that evaluates every covered action against a coding agent's Autonomy Zone before it runs
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
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
//! `no_proxy` bypass matching, in Go's `golang.org/x/net/http/httpproxy` grammar.
//!
//! The grammar is normative: it is what Go's `net/http`, and therefore the bulk of
//! the cloud tooling an enterprise already runs, means by `NO_PROXY`. curl's dialect
//! differs on the case that matters most in the field -- curl treats a bare `foo.com`
//! as a suffix-only rule, Go matches the apex *and* every subdomain -- so an operator
//! who wrote one list for their Go tooling would silently get a narrower bypass here
//! if we followed curl. We follow Go.
//!
//! Loopback is not negotiable. [`HARD_BYPASS`] is prepended to whatever the operator
//! supplied and cannot be shortened, reordered or removed by any user input: a proxy
//! misconfiguration must never route the hook's POST to the daemon, or a CLI call to
//! an admin endpoint, through a corporate egress. That mirrors the two hardcoded
//! checks Go performs before it consults `NO_PROXY` at all (`host == "localhost"`,
//! and `ip.IsLoopback()`), expressed as entries rather than as branches so that the
//! guarantee is visible in one `const` instead of buried in the matcher.
//!
//! Everything here is pure: no environment reads, no I/O. The caller owns where the
//! list came from.

use std::net::IpAddr;

/// Bypass entries prepended to every list, ahead of and inseparable from user input.
///
/// Together these reproduce Go's pre-`NO_PROXY` loopback checks exactly:
/// `127.0.0.0/8` and `::1` cover `ip.IsLoopback()`, and `localhost` covers the
/// literal host comparison. See [`NoProxyMatcher::new`] for the one place where a
/// hard-bypass entry is read more narrowly than the same string typed by a user.
pub const HARD_BYPASS: &[&str] = &["127.0.0.0/8", "::1", "localhost"];

/// One parsed `no_proxy` entry.
///
/// The split mirrors Go's three matcher types plus its `allMatch`. Go keeps IP and
/// domain matchers in two separate vectors and consults the IP one only when the host
/// parses as an address; we keep a single ordered vector and let each variant decline
/// the inputs it does not serve, which keeps entry order (and so the hard-bypass
/// prepend) observable in one place.
#[derive(Debug, Clone)]
enum Matcher {
    /// `*` -- matches every host on every port.
    All,
    /// A literal address, optionally pinned to one port.
    Ip { ip: IpAddr, port: Option<u16> },
    /// A CIDR block. Go's `net.IPNet.Contains` ignores the port entirely, and so do we.
    Cidr { network: IpAddr, prefix: u8 },
    /// A domain rule, optionally pinned to one port.
    ///
    /// Go stores a dot-prefixed `host` plus a `matchHost` bool; the two fields here say
    /// the same thing without the coupling, which is what lets a hard-bypass entry be
    /// exact-only:
    ///
    /// | Entry | `suffix` | `exact` | Matches |
    /// | ----- | -------- | ------- | ------- |
    /// | `foo.com` | `.foo.com` | `foo.com` | apex and subdomains |
    /// | `.foo.com` | `.foo.com` | -- | subdomains only |
    /// | `*.foo.com` | `.foo.com` | -- | subdomains only (Go rewrites `*.x` to `.x`) |
    /// | `localhost` (hard bypass) | -- | `localhost` | that host and nothing else |
    Domain {
        suffix: Option<String>,
        exact: Option<String>,
        port: Option<u16>,
    },
}

impl Matcher {
    /// Does this entry bypass `host` on `port`?
    ///
    /// `ip` is `Some` when `host` parsed as an address literal; the split is what
    /// implements Go's rule that a domain entry never matches an IP host (its
    /// `domainMatch.match` returns early on `ip != nil`) and that an IP entry never
    /// matches a name.
    fn matches(&self, host: &str, port: u16, ip: Option<&IpAddr>) -> bool {
        match self {
            Self::All => true,
            Self::Ip { ip: entry, port: p } => {
                ip == Some(entry) && p.is_none_or(|want| want == port)
            }
            Self::Cidr { network, prefix } => ip.is_some_and(|ip| contains(network, *prefix, ip)),
            Self::Domain {
                suffix,
                exact,
                port: p,
            } => {
                if ip.is_some() {
                    return false;
                }
                let hit = suffix.as_deref().is_some_and(|s| host.ends_with(s))
                    || exact.as_deref() == Some(host);
                hit && p.is_none_or(|want| want == port)
            }
        }
    }

    /// Whether this entry can only ever bypass loopback.
    ///
    /// Drives [`NoProxyMatcher::has_non_loopback_entry`], so it answers the question
    /// that rule actually asks -- "did the operator ask for a bypass we would not have
    /// applied anyway?" -- rather than "was this string one of [`HARD_BYPASS`]". A user
    /// who re-types `127.0.0.1` has added nothing; one who types `.internal` has.
    fn is_loopback_only(&self) -> bool {
        match self {
            Self::All => false,
            Self::Ip { ip, .. } => ip.is_loopback(),
            // A block is loopback-only when it sits inside 127.0.0.0/8 (v4) or is
            // exactly ::1/128 (v6); anything shorter reaches past loopback.
            Self::Cidr { network, prefix } => match network {
                IpAddr::V4(v4) => v4.is_loopback() && *prefix >= 8,
                IpAddr::V6(v6) => v6.is_loopback() && *prefix == 128,
            },
            Self::Domain { suffix, exact, .. } => {
                let loopback_name = |n: &str| n == "localhost" || n.ends_with(".localhost");
                suffix
                    .as_deref()
                    .is_none_or(|s| loopback_name(s.trim_start_matches('.')))
                    && exact.as_deref().is_none_or(loopback_name)
            }
        }
    }
}

/// Is `ip` inside the block `network`/`prefix`?
///
/// Hand-rolled rather than pulled from a CIDR crate: the whole operation is one mask
/// and one comparison per family, and `ipnet` is currently a transitive dependency
/// only -- promoting it to a direct one would put a crate in the hot-path graph to
/// save eight lines. Families never cross, matching `net.IPNet.Contains`; the caller
/// has already folded IPv4-mapped IPv6 hosts down to IPv4 so that `::ffff:127.0.0.1`
/// is caught by `127.0.0.0/8` the way Go catches it.
fn contains(network: &IpAddr, prefix: u8, ip: &IpAddr) -> bool {
    match (network, ip) {
        (IpAddr::V4(net), IpAddr::V4(ip)) => {
            // A shift by the full width is undefined, so /0 is spelled out.
            let mask = if prefix == 0 {
                0
            } else {
                u32::MAX << (32 - u32::from(prefix))
            };
            u32::from(*net) & mask == u32::from(*ip) & mask
        }
        (IpAddr::V6(net), IpAddr::V6(ip)) => {
            let mask = if prefix == 0 {
                0
            } else {
                u128::MAX << (128 - u32::from(prefix))
            };
            u128::from(*net) & mask == u128::from(*ip) & mask
        }
        _ => false,
    }
}

/// Zero the host bits, as `net.ParseCIDR` does before handing back the network.
fn mask(addr: IpAddr, prefix: u8) -> IpAddr {
    match addr {
        IpAddr::V4(v4) => {
            let bits = if prefix == 0 {
                0
            } else {
                u32::from(v4) & (u32::MAX << (32 - u32::from(prefix)))
            };
            IpAddr::V4(bits.into())
        }
        IpAddr::V6(v6) => {
            let bits = if prefix == 0 {
                0
            } else {
                u128::from(v6) & (u128::MAX << (128 - u32::from(prefix)))
            };
            IpAddr::V6(bits.into())
        }
    }
}

/// Fold an IPv4-mapped IPv6 address (`::ffff:a.b.c.d`) down to its IPv4 form.
///
/// Go's `IP.Equal` and `IPNet.Contains` both do this internally, so without it an
/// entry of `127.0.0.0/8` would fail to catch a host written as `::ffff:127.0.0.1`.
fn normalize(ip: IpAddr) -> IpAddr {
    match ip {
        IpAddr::V6(v6) => v6.to_ipv4_mapped().map_or(ip, IpAddr::V4),
        IpAddr::V4(_) => ip,
    }
}

/// Split `host:port`, in the shapes Go's `net.SplitHostPort` accepts.
///
/// Returns `None` for Go's error cases -- no port at all, and the bare IPv6 literal
/// whose colons Go rejects as "too many colons" -- because both fall through to the
/// same place: the entry is reconsidered whole.
fn split_host_port(entry: &str) -> Option<(&str, &str)> {
    if let Some(rest) = entry.strip_prefix('[') {
        let close = rest.find(']')?;
        let port = rest[close + 1..].strip_prefix(':')?;
        return Some((&rest[..close], port));
    }
    let colon = entry.find(':')?;
    let (host, port) = (&entry[..colon], &entry[colon + 1..]);
    // More than one colon and no brackets: an IPv6 literal, not a host:port pair.
    if port.contains(':') {
        return None;
    }
    Some((host, port))
}

/// A compiled `no_proxy` list.
///
/// Build it once per configuration and query it per request; matching allocates only
/// the lowercased host.
#[derive(Debug, Clone)]
pub struct NoProxyMatcher {
    matchers: Vec<Matcher>,
    has_non_loopback: bool,
}

impl NoProxyMatcher {
    /// Parse a comma-separated `no_proxy` list.
    ///
    /// [`HARD_BYPASS`] is parsed first and prepended, so loopback survives any user
    /// input -- including an empty string, and including a list that contradicts it.
    /// The hard-bypass entries are read under one deliberately narrower rule: a bare
    /// domain there matches that host **only**, never its subdomains. `localhost` is
    /// the only such entry, and the narrow reading is what Go's hardcoded
    /// `host == "localhost"` means. Reading it under the ordinary bare-domain rule
    /// would silently bypass `db.localhost` for every operator on earth, which is a
    /// policy decision this layer has no business making on their behalf; an operator
    /// who wants it writes `.localhost`.
    ///
    /// The returned `Vec` carries one message per entry that could not be compiled.
    /// Empty and whitespace-only entries are skipped in silence -- a trailing comma is
    /// not an operator error -- but nothing else is ever dropped without a word,
    /// because a `no_proxy` entry that quietly did nothing is how a request ends up on
    /// a proxy that cannot reach it.
    pub fn new(user_entries: &str) -> (Self, Vec<String>) {
        let mut matchers = Vec::new();
        let mut warnings = Vec::new();
        let mut has_non_loopback = false;

        for entry in HARD_BYPASS {
            if let Ok(matcher) = parse_entry(entry, true) {
                matchers.push(matcher);
            }
        }

        for raw in user_entries.split(',') {
            let entry = raw.trim().to_ascii_lowercase();
            if entry.is_empty() {
                continue;
            }
            match parse_entry(&entry, false) {
                Ok(matcher) => {
                    has_non_loopback |= !matcher.is_loopback_only();
                    matchers.push(matcher);
                }
                Err(reason) => warnings.push(format!("no_proxy entry {raw:?} ignored: {reason}")),
            }
        }

        (
            Self {
                matchers,
                has_non_loopback,
            },
            warnings,
        )
    }

    /// Whether `host`:`port` must bypass the proxy.
    ///
    /// `host` may arrive bracketed (`[::1]`) because that is how a URL authority
    /// carries an IPv6 literal; entries never are, because that is how Go's grammar
    /// spells them. Both sides are lowercased -- DNS names are case-insensitive, and
    /// an operator who typed `Internal.Corp` meant the same thing the resolver did.
    pub fn matches(&self, host: &str, port: u16) -> bool {
        let trimmed = host.trim();
        let unbracketed = trimmed
            .strip_prefix('[')
            .and_then(|h| h.strip_suffix(']'))
            .unwrap_or(trimmed);
        let host = unbracketed.to_ascii_lowercase();
        let ip = host.parse::<IpAddr>().ok().map(normalize);

        self.matchers
            .iter()
            .any(|m| m.matches(&host, port, ip.as_ref()))
    }

    /// Whether the operator asked for a bypass beyond loopback.
    ///
    /// Read by the `allow_direct = false` rule elsewhere: refusing every direct
    /// connection is coherent only while the sole exception is the loopback we would
    /// have taken anyway. A user list that adds a real destination is a contradiction
    /// the caller has to surface rather than silently resolve.
    pub fn has_non_loopback_entry(&self) -> bool {
        self.has_non_loopback
    }
}

/// Compile one already-trimmed, already-lowercased entry.
///
/// `hard` marks a [`HARD_BYPASS`] entry, whose bare domains are exact-only; see
/// [`NoProxyMatcher::new`] for why.
///
/// The `Err` is a rendered operator message rather than a typed error, and this is the
/// one place in the module where that is the right shape: a bad `no_proxy` entry is not
/// a failure that anything upstream can branch on -- the list still compiles, the client
/// is still built -- so there is no variant for a caller to match. The string is
/// produced, formatted and consumed inside [`NoProxyMatcher::new`], which turns it into
/// the warning that is this module's actual contract; nothing string-typed escapes.
fn parse_entry(entry: &str, hard: bool) -> Result<Matcher, String> {
    if entry == "*" {
        return Ok(Matcher::All);
    }

    // CIDR first, as Go does. Restricting the attempt to entries that actually contain
    // a slash is what lets a malformed block be reported: Go falls through and compiles
    // `10.0.0.0/99` into a domain rule named `.10.0.0.0/99`, which can never match and
    // never explains itself.
    if let Some((addr, len)) = entry.split_once('/') {
        let addr: IpAddr = addr
            .parse()
            .map_err(|_| format!("{addr:?} is not an IP address"))?;
        let prefix: u8 = len
            .parse()
            .map_err(|_| format!("{len:?} is not a prefix length"))?;
        let width: u8 = if addr.is_ipv4() { 32 } else { 128 };
        if prefix > width {
            return Err(format!("/{prefix} exceeds the {width}-bit address width"));
        }
        return Ok(Matcher::Cidr {
            network: mask(addr, prefix),
            prefix,
        });
    }

    let (host, port) = match split_host_port(entry) {
        Some((host, port)) => {
            if host.is_empty() {
                return Err("no host before the port".to_string());
            }
            let port = port
                .parse::<u16>()
                .map_err(|_| format!("{port:?} is not a port number"))?;
            (host, Some(port))
        }
        // No port, or a bare IPv6 literal whose colons are part of the address.
        None => (entry, None),
    };

    if let Ok(ip) = host.parse::<IpAddr>() {
        return Ok(Matcher::Ip {
            ip: normalize(ip),
            port,
        });
    }

    // Brackets only ever wrap an address. Go would compile `[::1]` (no port) into a
    // domain rule named `.[::1]`; say so instead.
    if host.starts_with('[') {
        return Err("bracketed hosts are only valid as [ipv6]:port".to_string());
    }
    if host.is_empty() {
        return Err("no host".to_string());
    }

    // `*.foo.com` is `.foo.com` -- Go drops the star and keeps the dot, which is what
    // makes the wildcard form subdomain-only rather than a third kind of rule.
    let host = if host.starts_with("*.") {
        &host[1..]
    } else {
        host
    };

    if let Some(sub) = host.strip_prefix('.') {
        if sub.is_empty() {
            return Err("no domain after the leading dot".to_string());
        }
        return Ok(Matcher::Domain {
            suffix: Some(host.to_string()),
            exact: None,
            port,
        });
    }

    Ok(Matcher::Domain {
        // A hard-bypass bare domain is the host itself and nothing under it.
        suffix: (!hard).then(|| format!(".{host}")),
        exact: Some(host.to_string()),
        port,
    })
}

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

    /// Go's `no_proxy` fixture from `httpproxy/proxy_test.go`, verbatim.
    const GO_NO_PROXY: &str = "foobar.com, .barbaz.net, *.wildcard.io, 192.168.1.1, \
         192.168.1.2:81, 192.168.1.3:80, 10.0.0.0/30, 2001:db8::52:0:1, \
         [2001:db8::52:0:2]:443, [2001:db8::52:0:3]:80, 2002:db8:a::45/64";

    /// Go's `UseProxyTests`, ported row for row.
    ///
    /// Go's `match` column is the result of `useProxy` -- `true` means *send it to the
    /// proxy*. Ours is the complement, so every row is inverted here and the Go comment
    /// is kept alongside to make the inversion auditable. Every row is queried on port
    /// 80, as `TestUseProxy` does (`test.host+":80"`).
    ///
    /// One row is ours rather than Go's: Go has no `localhost` entry in its fixture and
    /// relies on the hardcoded check, so `local.localhost` proxies. Our exact-only
    /// reading of the hard-bypass `localhost` preserves that; the bare-domain reading
    /// would not, which is the whole reason for the narrower rule.
    const GO_ROWS: &[(&str, bool, &str)] = &[
        // Never proxy localhost.
        ("localhost", true, "hardcoded in Go, HARD_BYPASS here"),
        ("127.0.0.1", true, "loopback"),
        ("127.0.0.2", true, "loopback, and 127.0.0.0/8 covers it"),
        (
            "[::1]",
            true,
            "loopback, bracketed as a URL authority carries it",
        ),
        ("[::2]", false, "not a loopback address"),
        ("192.168.1.1", true, "matches exact IPv4"),
        ("192.168.1.2", false, "ports do not match"),
        ("192.168.1.3", true, "matches exact IPv4:port"),
        ("192.168.1.4", false, "no match"),
        ("10.0.0.2", true, "matches IPv4/CIDR"),
        ("[2001:db8::52:0:1]", true, "matches exact IPv6"),
        ("[2001:db8::52:0:2]", false, "no match"),
        ("[2001:db8::52:0:3]", true, "matches exact [IPv6]:port"),
        ("[2002:db8:a::123]", true, "matches IPv6/CIDR"),
        ("[fe80::424b:c8be:1643:a1b6]", false, "no match"),
        ("barbaz.net", false, "does not match as .barbaz.net"),
        ("www.barbaz.net", true, "does match as .barbaz.net"),
        ("foobar.com", true, "does match as foobar.com"),
        (
            "www.foobar.com",
            true,
            "match because no_proxy includes foobar.com",
        ),
        ("foofoobar.com", false, "not match as a part of foobar.com"),
        ("baz.com", false, "not match as a part of barbaz.com"),
        ("localhost.net", false, "not match as suffix of address"),
        ("local.localhost", false, "not match as prefix as address"),
        ("barbarbaz.net", false, "not match, wrong domain"),
        ("wildcard.io", false, "does not match as *.wildcard.io"),
        ("nested.wildcard.io", true, "match as *.wildcard.io"),
        ("awildcard.io", false, "not a match because of '*'"),
    ];

    #[test]
    fn go_use_proxy_table() {
        let (matcher, warnings) = NoProxyMatcher::new(GO_NO_PROXY);
        assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
        for (host, want, why) in GO_ROWS {
            assert_eq!(
                matcher.matches(host, 80),
                *want,
                "matches({host}, 80) -- {why}"
            );
        }
    }

    /// Go's `TestAllNoProxy`: `*` bypasses every row in the same table.
    #[test]
    fn wildcard_bypasses_everything() {
        let (matcher, warnings) = NoProxyMatcher::new("*");
        assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
        for (host, _, _) in GO_ROWS {
            assert!(matcher.matches(host, 80), "* must bypass {host}");
        }
        assert!(matcher.matches("anything.example", 9999));
    }

    /// Go's `TestInvalidNoProxy`: `:1` must not panic and must not bypass. Go drops it
    /// in silence; we report it, which is the point of the warnings channel.
    #[test]
    fn portless_entry_is_reported_not_applied() {
        let (matcher, warnings) = NoProxyMatcher::new(":1");
        assert!(!matcher.matches("example.com", 80));
        assert_eq!(warnings.len(), 1, "{warnings:?}");
        assert!(warnings[0].contains("no host"), "{warnings:?}");
    }

    /// The prepend is proved with an EMPTY list and again beside an unrelated entry --
    /// never with `*`, which bypasses everything by grammar and would prove nothing.
    #[test]
    fn loopback_bypasses_with_an_empty_user_list() {
        let (matcher, warnings) = NoProxyMatcher::new("");
        assert!(warnings.is_empty(), "{warnings:?}");
        assert!(matcher.matches("127.0.0.1", 443));
        assert!(matcher.matches("127.0.0.1", 8080));
        assert!(matcher.matches("127.0.0.53", 1));
        assert!(matcher.matches("[::1]", 443));
        assert!(matcher.matches("::1", 443));
        assert!(matcher.matches("localhost", 7443));
        assert!(matcher.matches("LOCALHOST", 7443));
    }

    #[test]
    fn loopback_bypasses_alongside_an_unrelated_user_entry() {
        let (matcher, warnings) = NoProxyMatcher::new("internal.example.com");
        assert!(warnings.is_empty(), "{warnings:?}");
        assert!(matcher.matches("127.0.0.1", 4317));
        assert!(matcher.matches("[::1]", 4317));
        assert!(matcher.matches("localhost", 7443));
        assert!(matcher.matches("internal.example.com", 443));
    }

    #[test]
    fn a_non_loopback_host_is_not_bypassed_with_an_empty_list() {
        let (matcher, warnings) = NoProxyMatcher::new("");
        assert!(warnings.is_empty(), "{warnings:?}");
        assert!(!matcher.matches("api.openlatch.ai", 443));
        assert!(!matcher.matches("10.0.0.1", 443));
        assert!(!matcher.matches("[2001:db8::1]", 443));
        assert!(!matcher.matches("localhost.example.com", 443));
    }

    /// A user list can only ever widen the bypass, never narrow it below loopback.
    #[test]
    fn user_input_cannot_remove_the_hard_bypass() {
        for list in ["", "example.com", "10.0.0.0/8", "  ,  , ", ".foo.com:8080"] {
            let (matcher, _) = NoProxyMatcher::new(list);
            assert!(matcher.matches("127.0.0.1", 1), "list {list:?}");
            assert!(matcher.matches("[::1]", 1), "list {list:?}");
            assert!(matcher.matches("localhost", 1), "list {list:?}");
        }
    }

    /// A bare domain is apex + subdomains; a leading dot and a star are subdomains only.
    #[test]
    fn bare_dotted_and_star_forms() {
        let (bare, _) = NoProxyMatcher::new("foo.com");
        assert!(bare.matches("foo.com", 443));
        assert!(bare.matches("bar.foo.com", 443));
        assert!(bare.matches("a.b.foo.com", 443));
        assert!(!bare.matches("barfoo.com", 443));
        assert!(!bare.matches("foo.com.evil.net", 443));

        let (dotted, _) = NoProxyMatcher::new(".foo.com");
        assert!(!dotted.matches("foo.com", 443));
        assert!(dotted.matches("bar.foo.com", 443));

        let (star, _) = NoProxyMatcher::new("*.foo.com");
        assert!(!star.matches("foo.com", 443));
        assert!(star.matches("bar.foo.com", 443));
    }

    /// Ports participate only for entries that carry one.
    #[test]
    fn a_bare_host_entry_matches_any_port() {
        let (bare, _) = NoProxyMatcher::new("foo.com, 10.1.2.3");
        assert!(bare.matches("foo.com", 80));
        assert!(bare.matches("foo.com", 65535));
        assert!(bare.matches("10.1.2.3", 1));

        let (pinned, _) = NoProxyMatcher::new("foo.com:8080, 10.1.2.3:9000");
        assert!(pinned.matches("foo.com", 8080));
        assert!(!pinned.matches("foo.com", 8081));
        assert!(pinned.matches("10.1.2.3", 9000));
        assert!(!pinned.matches("10.1.2.3", 9001));
    }

    /// An IPv6 entry is written bare; the host being matched may be bracketed.
    #[test]
    fn ipv6_brackets_are_stripped_on_the_host_side_only() {
        let (matcher, warnings) = NoProxyMatcher::new("fe80::1, [2001:db8::2]:8443, fe80::/10");
        assert!(warnings.is_empty(), "{warnings:?}");
        assert!(matcher.matches("[fe80::1]", 443));
        assert!(matcher.matches("fe80::1", 443));
        assert!(matcher.matches("[2001:db8::2]", 8443));
        assert!(!matcher.matches("[2001:db8::2]", 8444));
        assert!(matcher.matches("[fe80::abcd]", 443));
        assert!(!matcher.matches("[2001:db8::3]", 443));
    }

    /// CIDR blocks, including the boundaries Go's `ParseCIDR` normalizes away.
    #[test]
    fn cidr_blocks() {
        let (matcher, warnings) = NoProxyMatcher::new("10.0.0.0/8, 172.16.5.9/16, fe80::/10");
        assert!(warnings.is_empty(), "{warnings:?}");
        assert!(matcher.matches("10.255.255.254", 443));
        assert!(!matcher.matches("11.0.0.1", 443));
        // The host bits of 172.16.5.9/16 are masked off, exactly as ParseCIDR does.
        assert!(matcher.matches("172.16.99.1", 443));
        assert!(!matcher.matches("172.17.0.1", 443));
        assert!(matcher.matches("[fe80::1]", 443));
        assert!(!matcher.matches("[fec0::1]", 443));
        // The network address itself is inside its own block.
        assert!(matcher.matches("10.0.0.0", 443));
        // A CIDR entry never matches a name.
        assert!(!matcher.matches("ten.example.com", 443));
    }

    /// An IPv4-mapped IPv6 host folds to IPv4, as Go's Contains/Equal do.
    #[test]
    fn ipv4_mapped_hosts_fold_to_ipv4() {
        let (matcher, _) = NoProxyMatcher::new("");
        assert!(matcher.matches("[::ffff:127.0.0.1]", 443));
        assert!(!matcher.matches("[::ffff:8.8.8.8]", 443));
    }

    /// Both sides are folded to lowercase.
    #[test]
    fn matching_is_case_insensitive() {
        let (matcher, _) = NoProxyMatcher::new("Internal.Corp, .Cache.Corp");
        assert!(matcher.matches("INTERNAL.CORP", 443));
        assert!(matcher.matches("api.Internal.Corp", 443));
        assert!(matcher.matches("NODE.cache.corp", 443));
    }

    /// Empty and whitespace-only entries are skipped without a word.
    #[test]
    fn blank_entries_are_skipped_silently() {
        let (matcher, warnings) = NoProxyMatcher::new(" , foo.com ,,   ,");
        assert!(warnings.is_empty(), "{warnings:?}");
        assert!(matcher.matches("foo.com", 443));
    }

    /// Everything that cannot be compiled is reported, never dropped.
    #[test]
    fn unparsable_entries_are_reported() {
        let cases = [
            ("10.0.0.0/99", "exceeds"),
            ("10.0.0.0/abc", "prefix length"),
            ("not-an-ip/24", "IP address"),
            ("foo.com:http", "port number"),
            ("foo.com:99999", "port number"),
            ("[::1]", "bracketed"),
            (":8080", "no host"),
            (".", "leading dot"),
        ];
        for (entry, needle) in cases {
            let (_, warnings) = NoProxyMatcher::new(entry);
            assert_eq!(warnings.len(), 1, "entry {entry:?} -> {warnings:?}");
            assert!(
                warnings[0].contains(needle),
                "entry {entry:?} -> {warnings:?}"
            );
        }
    }

    /// A bad entry never takes its neighbours down with it.
    #[test]
    fn a_bad_entry_does_not_invalidate_the_list() {
        let (matcher, warnings) = NoProxyMatcher::new("foo.com, 10.0.0.0/99, bar.com");
        assert_eq!(warnings.len(), 1, "{warnings:?}");
        assert!(matcher.matches("foo.com", 443));
        assert!(matcher.matches("bar.com", 443));
        assert!(matcher.matches("127.0.0.1", 443));
    }

    #[test]
    fn has_non_loopback_entry() {
        for list in [
            "",
            "  ",
            ",,",
            "127.0.0.1",
            "::1",
            "127.0.0.0/8",
            "localhost",
            "10.0.0.0/99",
        ] {
            let (matcher, _) = NoProxyMatcher::new(list);
            assert!(
                !matcher.has_non_loopback_entry(),
                "list {list:?} adds nothing beyond loopback"
            );
        }
        for list in [
            "*",
            "example.com",
            "10.0.0.0/8",
            "0.0.0.0/0",
            "::/0",
            "127.0.0.0/4",
        ] {
            let (matcher, _) = NoProxyMatcher::new(list);
            assert!(
                matcher.has_non_loopback_entry(),
                "list {list:?} reaches past loopback"
            );
        }
    }

    /// No input may panic: the client must never fall over on a malformed env var.
    #[test]
    fn hostile_input_never_panics() {
        let long = "a".repeat(4096);
        let lists = [
            "*",
            "**",
            "*.",
            ".",
            "..",
            ":",
            "::",
            ":::",
            "[",
            "]",
            "[]",
            "[]:",
            "[]:80",
            "[::1",
            "::1]",
            "/",
            "//",
            "/32",
            "10.0.0.0/",
            "/24",
            "a:b:c",
            "-",
            "%",
            "\u{1f600}",
            "\u{1f600}.com",
            "foo..com",
            "foo.com:",
            "foo.com::80",
            "0.0.0.0/0",
            "::/0",
            long.as_str(),
        ];
        for list in lists {
            let (matcher, _) = NoProxyMatcher::new(list);
            for host in [
                "",
                "foo.com",
                "127.0.0.1",
                "[::1]",
                "[",
                "]",
                "::",
                "\u{1f600}",
            ] {
                let _ = matcher.matches(host, 0);
                let _ = matcher.matches(host, 65535);
            }
        }
    }
}