Task 04 added dependencies to toolkit and its git add line left out the lock file, so the driver stopped on an unclean tree. The lock change is folded into task 04's commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
83 lines
3.4 KiB
Markdown
83 lines
3.4 KiB
Markdown
# M3b task 07: which addresses are public
|
|
|
|
**Branch:** `m3b` (run `git switch m3b`; `git status --short` must be empty, otherwise stop)
|
|
**Commit subject:** `toolkit: is_public, the addresses the egress proxy may reach`
|
|
|
|
## Goal
|
|
|
|
The egress proxy (task 08) connects only to **public** addresses. A grant allows a host on the
|
|
internet; if its name resolves into the tailnet (`100.64.0.0/10`), the host (`127.0.0.1`) or a
|
|
private network, the connection is refused even though the name is allowed. This task is the one
|
|
pure function that decides it. Spec section 5, "Public".
|
|
|
|
## Files
|
|
|
|
- Copy: `crates/toolkit/tests/addr.rs`
|
|
- Create: `crates/toolkit/src/addr.rs`
|
|
- Modify: `crates/toolkit/src/lib.rs` (`pub mod addr;`), `docs/implementer-log.md`
|
|
|
|
## Interface
|
|
|
|
```rust
|
|
/// True if `ip` is a public unicast address.
|
|
pub fn is_public(ip: std::net::IpAddr) -> bool;
|
|
```
|
|
|
|
## Refused ranges: every one of these is **not** public
|
|
|
|
IPv4 (with `[a, b, c, _] = ip.octets()`):
|
|
|
|
| Range | Test |
|
|
|---|---|
|
|
| `0.0.0.0/8` | `a == 0` |
|
|
| `10.0.0.0/8` | `a == 10` |
|
|
| `100.64.0.0/10` (the tailnet) | `a == 100 && (64..=127).contains(&b)` |
|
|
| `127.0.0.0/8` | `a == 127` |
|
|
| `169.254.0.0/16` | `a == 169 && b == 254` |
|
|
| `172.16.0.0/12` | `a == 172 && (16..=31).contains(&b)` |
|
|
| `192.0.0.0/24` | `a == 192 && b == 0 && c == 0` |
|
|
| `192.0.2.0/24` | `a == 192 && b == 0 && c == 2` |
|
|
| `192.168.0.0/16` | `a == 192 && b == 168` |
|
|
| `198.18.0.0/15` | `a == 198 && (b == 18 \|\| b == 19)` |
|
|
| `198.51.100.0/24` | `a == 198 && b == 51 && c == 100` |
|
|
| `203.0.113.0/24` | `a == 203 && b == 0 && c == 113` |
|
|
| `224.0.0.0/4` and `240.0.0.0/4` | `a >= 224` |
|
|
|
|
IPv6 (with `s = ip.segments()`, eight `u16`s):
|
|
|
|
| Range | Test |
|
|
|---|---|
|
|
| `::/96` (holds `::`, `::1`, and the old IPv4-compatible form) | the first six segments are all 0 |
|
|
| IPv4-mapped `::ffff:0:0/96` | `s[0..5]` all 0 and `s[5] == 0xffff`: judge the last 32 bits as IPv4 |
|
|
| NAT64 `64:ff9b::/96` | `s[0..6] == [0x64, 0xff9b, 0, 0, 0, 0]`: judge the last 32 bits as IPv4 |
|
|
| `fc00::/7` | `s[0] & 0xfe00 == 0xfc00` |
|
|
| `fe80::/10` | `s[0] & 0xffc0 == 0xfe80` |
|
|
| `ff00::/8` | `s[0] & 0xff00 == 0xff00` |
|
|
| `2001:db8::/32` | `s[0] == 0x2001 && s[1] == 0x0db8` |
|
|
|
|
Check the three "judge as IPv4" rows **before** the others. The last 32 bits as IPv4 are
|
|
`Ipv4Addr::from(((s[6] as u32) << 16) | s[7] as u32)` in spirit, but **no `as` casts**: use
|
|
`u32::from(s[6])` and `u32::from(s[7])`, or `s[6].to_be_bytes()` and `s[7].to_be_bytes()`.
|
|
|
|
Everything else is public. Do not use the standard library's `is_global` (unstable) or add ranges
|
|
that are not in these tables: the test checks public neighbours just outside each range too.
|
|
|
|
## Steps
|
|
|
|
- [ ] **1. Copy.** `git switch m3b`, then
|
|
`cp docs/plans/M3b/files/crates/toolkit/tests/addr.rs crates/toolkit/tests/`
|
|
- [ ] **2. See it fail.** `cargo test -p toolkit --test addr`. Expected: it does not compile.
|
|
- [ ] **3. Write `addr.rs`.** Run `cargo fmt --all`.
|
|
- [ ] **4. See it pass.** `cargo test -p toolkit --test addr`. Expected: 4 passed.
|
|
- [ ] **5. Walk the tables.** Point at the line for each row of both tables.
|
|
- [ ] **6. Run the gate.** `make gate`. Expected last line: `gate: ok`.
|
|
- [ ] **7. Log and commit.** `git add crates/toolkit docs/implementer-log.md Cargo.lock && git commit`
|
|
|
|
## Done when
|
|
|
|
- `cargo test -p toolkit --test addr` reports 4 passed; `make gate` prints `gate: ok`.
|
|
|
|
## Stop and report if
|
|
|
|
- A test's expectation disagrees with these tables.
|