# ESH IPv6 naming scheme

Every ESH LAN carries an **eight-hex-digit phrase** as the first half of the
interface identifier. Picked 2026-08-18/19. This file is the canonical record.

> **Why this file exists.** The scheme originally lived as a single line in
> `persistent-memory.md` and was silently deleted by a `memory: snapshot`
> commit (`837fa36`). Recovering it took a hunt through session transcripts to
> find the commit that had held it. A naming convention is not temporal state —
> it belongs in a document, so it now is one.

## The names

| network | hex | reads as |
|---|---|---|
| `Default` | **`4BA5:3417`** | A BASE FOR IT |
| `esh-mgmt` | **`15DA:B055`** | IS DA BOSS |
| `esh-server` | **`4411:B105`** | FOR ALL BIOS |
| `esh-userland` | **`CAFE:4411`** | CAFE FOR ALL |
| `esh-iot` | **`4DBA:D107`** | FOR DA BAD IOT |
| `esh-cameras` | **`1533:FACE5`** | I SEE FACES |
| *(reserved)* DMZ | **`4411:DBAD`** | FOR ALL DA BAD |

The DMZ name is **claimed against a network that does not exist yet** — there is
no DMZ on the ESH UDM. Do not reuse it.

Substitutions are the standard hexspeak set: `0`→O, `1`→I/L, `5`→S, plus letters
that are already native hex (`A`–`F`). Anything outside `0-9a-f` is not
expressible — `b0ss` and `c00l` do **not** work, which is why the set above uses
`B055` and avoids `c00l` entirely.

House style, arrived at rather than designed: **eight digits, and a complete
phrase rather than a single word.** Words are allowed to straddle the group
boundary (`4DBA:D107` is `4·D·BAD·107`); the phrase reads through the colon.

## Address structure

```
2607:73c0:402:1d02 : 4411:b105 : 50 : 45
└──── ISP /64 ────┘ └ segment ─┘ └ 10.0.50.45 ┘
```

- **Prefix** — Cityside's, not ours to name. ESH holds a `/56`
  (`2607:73c0:402:1d00::/56`, 256 × /64); the subnet id is assigned by UniFi's
  `ipv6_pd_prefixid`. `esh-cameras` is `1d00`, `esh-server` is `1d02`.
- **Segment word pair** — 32 bits, from the table above.
- **Host** — the last two IPv4 octets, written as literal digits so they read
  straight off the address. `10.0.50.45` → `:50:45`.

The scheme lives entirely in the **interface identifier**, so it is
**delegation-size independent**. It works identically on a `/56`, a `/48`, or
NH3's single `/64`. It never competes with the subnet id, which is far too small
to hold a word (8 bits at ESH — two hex digits).

Note: `4411:b105:50:45` fills all four host groups, so there is **no `::`** in
these addresses. Writing `...::4411:b105:50:45` is malformed and will be
rejected.

## What can and cannot carry a name

| slot | nameable? |
|---|---|
| `/64` subnet id (`ipv6_pd_prefixid`) | **No** — 8 bits at ESH, two hex digits, no room for a word |
| the gateway's own address | **No** — fixed at `::1` by UniFi, no field for it |
| a UniFi *client* reservation | **No** — UniFi has no IPv6 equivalent of `use_fixedip` |
| **a host taking its own address** | **Yes** — this is the one that works |

⚠ **The original note concluded these names could never appear in a `dig` or
`ip -6` output. That is wrong.** The first three rows are correct, but they only
establish that *UniFi* cannot assign the address. A Linux host can simply take
one within its own advertised prefix, and the router gets no vote. Appliances
with no shell — cameras, most IoT — genuinely cannot, so `1533:FACE5` and
`4DBA:D107` are likely to stay documentation-only.

## Applying it to a host

Do **not** use an `iface … inet6 static` stanza: on Debian that sets
`accept_ra=0`, killing SLAAC and the IPv6 default route — a good way to strand a
headless box. Use an `if-up.d` hook that derives the live prefix instead.

Live example, `/etc/network/if-up.d/ipv6-scheme-addr` on `esh-docker-vm`:

```sh
#!/bin/sh
[ "$IFACE" = ens18 ] || exit 0
(
  i=0
  while [ $i -lt 30 ]; do
    PFX=$(ip -6 -o addr show dev "$IFACE" scope global 2>/dev/null \
          | awk '{print $4}' | cut -d/ -f1 | head -1 | cut -d: -f1-4)
    if [ -n "$PFX" ]; then
      ip -6 addr replace "${PFX}:4411:b105:50:45/64" dev "$IFACE" && exit 0
    fi
    sleep 2
    i=$((i + 1))
  done
) >/dev/null 2>&1 &
exit 0
```

Three deliberate properties:

- **The prefix is derived, never hardcoded** — self-heals if Cityside
  re-delegates.
- **Backgrounded with a retry** — SLAAC may not have landed when `if-up.d` runs,
  and a hook that blocks or fails would stall interface bring-up.
- **Additive** — `/etc/network/interfaces` already sources `interfaces.d/`;
  nothing existing is edited, and removal is one `rm`.

Remaining gap: a *mid-life* prefix change is only picked up at the next
interface-up. A timer would close it; not worth building until the prefix is
observed to actually move.

## Deployed

All three Linux hosts on `esh-server` now carry the segment name, with the last
two groups reading straight off their IPv4 address:

| host | address | v4 | applied via |
|---|---|---|---|
| `esh-docker-vm` (AdGuard) | `2607:73c0:402:1d02:4411:b105:50:45` | 10.0.50.45 | `if-up.d` on `ens18` |
| `esh-pve-nas` | `2607:73c0:402:1d02:4411:b105:50:55` | 10.0.50.55 | `if-up.d` on `vmbr0` |
| `esh-vm-db` | `2607:73c0:402:1d02:4411:b105:50:60` | 10.0.50.60 | `if-up.d` on `ens18` |

`esh-docker-vm`'s is load-bearing, not decorative: the ESH UDM advertises an
IPv6 resolver to clients via RDNSS, macOS prefers it over the DHCPv4-supplied
one, so whatever sits there is what resolves `*.internal` for every Mac on the
network. It previously pointed at AdGuard's **MAC-derived SLAAC address**, which
would have broken if that VM's NIC ever changed. Both `esh-userland` and
`esh-server` now advertise the scheme address instead
(`dhcpdv6_dns_auto=false` + `dhcpdv6_dns_1=<address>`), verified on the wire by
soliciting an RA and parsing option type 25.

### ⚠ Proxmox bridges need `accept_ra=2` or SLAAC never runs

`esh-pve-nas` had **link-local only** despite `accept_ra=1`, `autoconf=1` and
IPv6 enabled — every sysctl looked correct. The cause: **`vmbr0.forwarding = 1`**
(Proxmox sets per-interface forwarding on bridges), and the kernel ignores RAs on
a forwarding interface unless `accept_ra` is explicitly **`2`**. `accept_ra=1`
means "accept only if not forwarding", so it silently did nothing.

Fixed in `/etc/sysctl.d/60-ipv6-accept-ra.conf` on that host:

```
net.ipv6.conf.vmbr0.accept_ra = 2
net.ipv6.conf.vmbr0.accept_ra_defrtr = 0
```

`accept_ra_defrtr=0` is deliberate — it takes the advertised **prefix** (so
SLAAC configures an address) while **declining the default route**, so a
hypervisor gains an IPv6 identity with no change to its routing behaviour.
Verified after: SLAAC address present, v6 default routes still **0**, v4 intact.

Expect the same on any other Proxmox node when its LAN gets IPv6.

### Getting into a host with no direct root

`esh-vm-db` refuses key auth for `root` and `infra-ops`, and `lkraven`'s sudo
wants a password. It is VMID 101 on `esh-pve`, and the **QEMU guest agent** runs
as uid 0 inside it, so the hook was installed with:

```
qm guest exec 101 -- /bin/sh -c 'echo <base64> | base64 -d > /etc/network/if-up.d/... '
```

base64 because quoting a multi-line script through two SSH layers mangles it.
Worth remembering as the general path for guests whose credentials are not
vaulted.

Every Linux host on `esh-server` now carries its name. The remaining ESH
segments have no eligible hosts: `esh-cameras` and `esh-iot` are appliances
with no shell, and `esh-mgmt`, `esh-userland` and `Default` are still
`ipv6_interface_type: none` pending the firewall-policy pass — enabling SLAAC
there gives every client a globally reachable address.
