feat(esh-matter): Matter server (matter.js 1.4.0) on a VLAN-90-only LXC for Home Assistant

For ha-dev (operator-approved 2026-09-26). CT 111 on esh-pve at 10.0.90.20:
Matter/Thread IPv6 (Echo ULA + RA route-information) is link-only, so the
server sits on esh-iot and HA reaches it over routed IPv4 ws :5580.
- playbooks/esh-matter-lxc.yaml: kernel RA (accept_ra=1,
  rt_info_max_plen=64), forwarding off, Docker ip-forward/iptables off;
  nftables admits 5580 from HA 10.0.50.46 only and SSH from mgmt ranges;
  the CT is added to esh-pve's vzdump job (fabric credentials).
- stacks/matter-server: ghcr.io/matter-js/matterjs-server:1.4.0 (digest),
  host networking, /data on the CT.
- Acceptance: fdad:: SLAAC, ping6 thermostat, 2 Thread RIO routes learned, ws
  server_info from inside the HA container; 5580 refused from 10.0.50.45,
  nh3-dev and a temporary VLAN 90 netns vantage.
This commit is contained in:
vh
2026-09-26 13:07:22 -07:00
parent 141ff653ef
commit 8e7ae0675d
10 changed files with 482 additions and 0 deletions
+1
View File
@@ -367,6 +367,7 @@ Observed and standardized across servers:
| esh-pve-nas | 10.0.50.55 | ESH home lab | Proxmox VE hypervisor (storage / media) | `servers/esh-pve-nas/README.md` | | esh-pve-nas | 10.0.50.55 | ESH home lab | Proxmox VE hypervisor (storage / media) | `servers/esh-pve-nas/README.md` |
| esh-vm-db | 10.0.50.60 | ESH home lab | DB VM — PostgreSQL (paperless-ng) + MongoDB; bare-metal VM, no Docker | `servers/esh-vm-db/README.md` | | esh-vm-db | 10.0.50.60 | ESH home lab | DB VM — PostgreSQL (paperless-ng) + MongoDB; bare-metal VM, no Docker | `servers/esh-vm-db/README.md` |
| esh-ml1 | 10.0.50.80 | ESH home lab | GPU LXC (CT 110 on esh-pve, RTX 2000E Ada 16 GB, host-side NVIDIA driver) — **the fleet's embedding + reranking service** (TEI), sole gateway backend since 2026-09-25 | `servers/esh-ml1/README.md` | | esh-ml1 | 10.0.50.80 | ESH home lab | GPU LXC (CT 110 on esh-pve, RTX 2000E Ada 16 GB, host-side NVIDIA driver) — **the fleet's embedding + reranking service** (TEI), sole gateway backend since 2026-09-25 | `servers/esh-ml1/README.md` |
| esh-matter | 10.0.90.20 | ESH home lab (VLAN 90, esh-iot) | LXC (CT 111 on esh-pve), VLAN 90 only — Matter server (matter.js) for Home Assistant; :5580 firewalled to HA; in vzdump (fabric credentials) | `servers/esh-matter/README.md` |
| nh3-ml1 | 10.100.50.80 | NH3 | GPU LXC (CT 109 on nh3-pve, RTX 2000E Ada 16 GB, host-side NVIDIA driver) — second embed/rerank (TEI) backend, twin of esh-ml1; not yet behind the gateway (2026-09-25) | `servers/nh3-ml1/README.md` | | nh3-ml1 | 10.100.50.80 | NH3 | GPU LXC (CT 109 on nh3-pve, RTX 2000E Ada 16 GB, host-side NVIDIA driver) — second embed/rerank (TEI) backend, twin of esh-ml1; not yet behind the gateway (2026-09-25) | `servers/nh3-ml1/README.md` |
**Placement rules:** **Placement rules:**
+1
View File
@@ -101,6 +101,7 @@ hosts:
- {name: esh-pve-nas, site: esh, v4: 10.0.50.55, note: Proxmox hypervisor — storage/media} - {name: esh-pve-nas, site: esh, v4: 10.0.50.55, note: Proxmox hypervisor — storage/media}
- {name: esh-vm-db, site: esh, v4: 10.0.50.60, note: PostgreSQL + MongoDB} - {name: esh-vm-db, site: esh, v4: 10.0.50.60, note: PostgreSQL + MongoDB}
- {name: esh-ml1, site: esh, v4: 10.0.50.80, note: CT110 on esh-pve — RTX 2000E Ada, embedding + reranking (TEI) + reward seat} - {name: esh-ml1, site: esh, v4: 10.0.50.80, note: CT110 on esh-pve — RTX 2000E Ada, embedding + reranking (TEI) + reward seat}
- {name: esh-matter, site: esh, v4: 10.0.90.20, note: CT111 on esh-pve — VLAN 90 (esh-iot) only; Matter server (matter.js) for HA; :5580 open to HA 10.0.50.46 only}
- {name: vm-esh-nas, site: esh, v4: 10.0.50.154, note: NAS-adjacent docker host} - {name: vm-esh-nas, site: esh, v4: 10.0.50.154, note: NAS-adjacent docker host}
- {name: esh-filebot, site: esh, v4: 10.0.50.70, note: restic / file-sync VM} - {name: esh-filebot, site: esh, v4: 10.0.50.70, note: restic / file-sync VM}
- {name: esh-gw, site: esh, v4: 10.0.250.1, note: esh-gw} - {name: esh-gw, site: esh, v4: 10.0.250.1, note: esh-gw}
+1
View File
@@ -218,6 +218,7 @@ in `servers/pfi-gx10/README.md`. → `persistent-memory.d/2026-09-24-gx10-ac-res
## Recent decisions ## Recent decisions
- `[2026-09-26]` **esh-matter LIVE: a Matter server (matter.js 1.4.0) on CT 111 @ 10.0.90.20, VLAN 90 only**, for ha-dev (operator-approved, relayed). It is on the IoT link because Matter/Thread IPv6 (Echo ULA + RIO routes) is link-only. Kernel RA with rt_info_max_plen=64, forwarding off; Docker ip-forward/iptables off. The in-CT nftables admits :5580 from HA 10.0.50.46 only and SSH from mgmt ranges only, and accepts everything else so Matter UDP never meets conntrack. The CT is in esh-pve vzdump (fabric creds). Acceptance all green: fdad:: SLAAC; ping6 thermostat; 2 Thread routes learned (Aqara + Echo); ws OK from inside the HA container; refused from 10.0.50.45, nh3-dev and a temp VLAN 90 netns. → `servers/esh-matter/README.md`
- `[2026-09-26]` **Embed/rerank LOAD-SHARED across esh-ml1 + nh3-ml1 (Prime).** Second deployments were added for qwen3-embedding and reranker (config) and for reranker-a3-bge-v2-m3 (DB, `/model/new`). Without failover, a dead backend failed 7/40 embeds; `router_settings.enable_weighted_failover: true` (affects only multi-deployment groups) brought it to rerank 80/80 and embed 38/40 at onset, 60/60 sustained. In LiteLLM 1.97 the embedding path does not stamp the failed deployment, so it relies on cooldown. LiteLLM restarted 3× tonight, ~36 s each. - `[2026-09-26]` **Embed/rerank LOAD-SHARED across esh-ml1 + nh3-ml1 (Prime).** Second deployments were added for qwen3-embedding and reranker (config) and for reranker-a3-bge-v2-m3 (DB, `/model/new`). Without failover, a dead backend failed 7/40 embeds; `router_settings.enable_weighted_failover: true` (affects only multi-deployment groups) brought it to rerank 80/80 and embed 38/40 at onset, 60/60 sustained. In LiteLLM 1.97 the embedding path does not stamp the failed deployment, so it relies on cooldown. LiteLLM restarted 3× tonight, ~36 s each.
- `[2026-09-26]` **Two dataset-foundry utility seats LIVE on nh3-ml1 (brokkr; operator approval relayed):** LFM2.5-VL-3B on llama.cpp `:8030` (gateway `lfm25-vl-3b`, LiteLLM restarted 36 s at 0039) and VibeVoice-ASR-Streaming-1.5B on **audio.cpp** `:8031`, not vibevoice.cpp as specced; the GGUF card names audio.cpp. Controls: VL read a synthetic image exactly, but hallucinates when no image is sent; ASR WER 3/69 on the bundled LibriSpeech clips. libsoxr added (linear resampling misheard a word). First ASR request ~31 s cold. Answered brokkr: the old 27B image seat (10.250.50.54:8015) was retired 09-14; baseline = gateway `image-judge` (Flash-Next). - `[2026-09-26]` **Two dataset-foundry utility seats LIVE on nh3-ml1 (brokkr; operator approval relayed):** LFM2.5-VL-3B on llama.cpp `:8030` (gateway `lfm25-vl-3b`, LiteLLM restarted 36 s at 0039) and VibeVoice-ASR-Streaming-1.5B on **audio.cpp** `:8031`, not vibevoice.cpp as specced; the GGUF card names audio.cpp. Controls: VL read a synthetic image exactly, but hallucinates when no image is sent; ASR WER 3/69 on the bundled LibriSpeech clips. libsoxr added (linear resampling misheard a word). First ASR request ~31 s cold. Answered brokkr: the old 27B image seat (10.250.50.54:8015) was retired 09-14; baseline = gateway `image-judge` (Flash-Next).
- `[2026-09-26]` **Coder seat STAYS on fv-ml1 (Prime).** The nh3-ml1 copy gave the same quality (teacher-forced true-code logprob diff +0.008 ± 0.019) but ran ~5× slower (64-tok FIM ~1.0 s vs 0.2 s; 63 vs 338 tok/s), and freeing 6.3 GB on fv-ml1 GPU 1 (20 GB spare) bought little. Copy removed; recipe kept in `stacks/coder-seat/`. The RTX 2000E suits embed/rerank/classify, not latency-sensitive generation. Only coder was a candidate: parakeet stays (earlier ruling), voices is a generation seat. - `[2026-09-26]` **Coder seat STAYS on fv-ml1 (Prime).** The nh3-ml1 copy gave the same quality (teacher-forced true-code logprob diff +0.008 ± 0.019) but ran ~5× slower (64-tok FIM ~1.0 s vs 0.2 s; 63 vs 338 tok/s), and freeing 6.3 GB on fv-ml1 GPU 1 (20 GB spare) bought little. Copy removed; recipe kept in `stacks/coder-seat/`. The RTX 2000E suits embed/rerank/classify, not latency-sensitive generation. Only coder was a candidate: parakeet stays (earlier ruling), voices is a generation seat.
+204
View File
@@ -0,0 +1,204 @@
# esh-matter — a small LXC on esh-pve whose ONLY network leg is VLAN 90 (esh-iot),
# running the Matter server (matter.js, stacks/matter-server) for Home Assistant.
# Requested by ha-dev, operator-approved 2026-09-26.
#
# WHY IT SITS ON VLAN 90: Matter's operational traffic is IPv6, and VLAN 90's only
# IPv6 is the Thread ULA fdad:29e:d492:fd87::/64. It is advertised by an Echo
# border router, not the UDM, and is visible only on that link. The Thread routes
# (RA route-information options from the border routers) are likewise link-only.
# HA core reaches the server over an IPv4 websocket (routed VLAN 50 → 90; the UDM
# policy InternalToIOT already allows it), so HA itself does not change.
#
# WHY AN LXC, NOT A MACVLAN ON esh-docker-vm: it keeps the untrusted IoT leg off the
# host that runs HA and everything else (a VM with a history of wedges). It gets
# its own firewall and its own vzdump backup.
#
# IPv6: kernel RA processing (no NetworkManager/networkd; the Debian 12 template
# uses ifupdown). accept_ra=1 plus accept_ra_rt_info_max_plen=64 learns Thread
# routes. IPv6 forwarding stays OFF (matter.js os_requirements: forwarding
# disables RFC 4191 reachability probing). Docker is told not to touch forwarding
# or iptables; the server runs with host networking, so it needs neither.
#
# FIREWALL (in-CT nftables): 5580 (the websocket and dashboard, UNAUTHENTICATED) is
# accepted only from HA at {{ ha_ip }}, then dropped for everyone else, v4 and v6.
# SSH is accepted only from non-IoT management ranges. Everything else, Matter
# UDP 5540 and mDNS 5353 included, is left alone on purpose. That keeps conntrack
# out of the Matter path, so sleepy-device reports are not dropped by a 120 s UDP
# timeout (matter.js "Stateful firewalls" note).
#
# BACKUP: the CT is added to esh-pve's vzdump job (PBS-ANA, mirrored to PBS-NH3).
# The data dir holds the Matter fabric root credentials; losing it means
# re-commissioning every device.
#
# Run: scripts/elway root@esh-pve --playbook playbooks/esh-matter-lxc.yaml
# Then: scripts/deploy-stack.sh esh-matter matter-server (+ docker compose up -d)
vars:
ctid: 111
hostname: esh-matter
ip_cidr: 10.0.90.20/24
ip_addr: 10.0.90.20
gateway: 10.0.90.1
vlan: 90
rootfs_storage: local-lvm
rootfs_gb: 8
cores: 2
memory_mb: 1024
swap_mb: 512
startup_order: 30
template: debian-12-standard_12.12-1_amd64.tar.zst
ha_ip: 10.0.50.46
# Non-IoT sources allowed to SSH: esh-server, esh-userland, esh-mgmt, NH3, mesh.
ssh_sources: "10.0.50.0/24, 10.0.10.0/24, 10.0.250.0/24, 10.100.0.0/16, 100.64.0.0/10"
infra_ops_pubkey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN+1HBwfXrkfTYWdcnWCjLJ6VLAGC87gxH5h5vKaaA3c infra-ops@pfi-fleet"
steps:
- name: Create CT {{ ctid }} ({{ hostname }}) with its only leg on VLAN {{ vlan }}, IPv6 SLAAC
shell: |
pct create {{ ctid }} local:vztmpl/{{ template }} \
--hostname {{ hostname }} --unprivileged 1 --features nesting=1,keyctl=1 \
--cores {{ cores }} --memory {{ memory_mb }} --swap {{ swap_mb }} \
--rootfs {{ rootfs_storage }}:{{ rootfs_gb }} \
--net0 name=eth0,bridge=vmbr0,firewall=0,gw={{ gateway }},ip={{ ip_cidr }},ip6=auto,tag={{ vlan }},type=veth \
--nameserver {{ gateway }} \
--onboot 1 --startup order={{ startup_order }},up=10 \
--description "{{ hostname }} — Matter server (matter.js) on VLAN {{ vlan }} for Home Assistant. Built by eshpfi playbooks/esh-matter-lxc.yaml; stack stacks/matter-server. IN vzdump (fabric credentials)."
when: "! pct status {{ ctid }} >/dev/null 2>&1"
# esh-pve's job names its vmids explicitly. Append ours; leave the rest alone.
- name: Add CT {{ ctid }} to esh-pve's vzdump job
shell: |
set -e
CT={{ ctid }} JOBS="$(pvesh get /cluster/backup --output-format json)" python3 - <<'PY'
import json, os, subprocess
ct = os.environ["CT"]
for j in json.loads(os.environ["JOBS"]):
ids = [x for x in str(j.get("vmid", "")).split(",") if x]
if not ids or ct in ids:
continue
subprocess.run(["pvesh", "set", "/cluster/backup/" + j["id"], "--vmid", ",".join(ids + [ct])], check=True)
print("added", ct, "to", j["id"])
PY
when: "! grep -E '^\\s+vmid .*\\b{{ ctid }}\\b' /etc/pve/jobs.cfg"
- name: Start the container
shell: pct start {{ ctid }} && sleep 6
when: "! pct status {{ ctid }} | grep -q running"
- name: IPv6 RA processing with Thread route-information options, forwarding off
shell: |
pct exec {{ ctid }} -- bash -s <<'EOF'
set -euo pipefail
cat > /etc/sysctl.d/60-matter-ipv6.conf <<'EOC'
# Matter server — see eshpfi playbooks/esh-matter-lxc.yaml
net.ipv6.conf.all.forwarding = 0
net.ipv6.conf.eth0.accept_ra = 1
net.ipv6.conf.eth0.accept_ra_rt_info_max_plen = 64
EOC
sysctl -q -p /etc/sysctl.d/60-matter-ipv6.conf
EOF
when: "! pct exec {{ ctid }} -- sh -c 'test $(cat /proc/sys/net/ipv6/conf/eth0/accept_ra_rt_info_max_plen) = 64'"
- name: Base packages + bookworm point upgrade (+ nftables)
shell: |
pct exec {{ ctid }} -- bash -s <<'EOF'
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
for i in $(seq 1 30); do getent hosts deb.debian.org >/dev/null && break; sleep 1; done
apt-get update -qq
apt-get -y -qq full-upgrade
apt-get install -y -qq --no-install-recommends ca-certificates curl gnupg sudo jq less rsync locales nftables iputils-ping iproute2
sed -i 's/^# *en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen >/dev/null
EOF
when: "! pct exec {{ ctid }} -- sh -c 'command -v nft && command -v rsync && command -v jq && locale -a | grep -qi en_US.utf8' >/dev/null 2>&1"
- name: Fleet identities (docker 851, infra-ops 850, vh 1000) + /opt/docker tree
shell: |
pct exec {{ ctid }} -- bash -s <<'EOF'
set -euo pipefail
getent group docker >/dev/null || groupadd -g 851 docker
getent group infra-ops >/dev/null || groupadd -g 850 infra-ops
id infra-ops >/dev/null 2>&1 || useradd -u 850 -g 850 -G docker -m -s /bin/bash infra-ops
chmod 0700 /home/infra-ops
install -d -m 0700 -o infra-ops -g infra-ops /home/infra-ops/.ssh
echo '{{ infra_ops_pubkey }}' > /home/infra-ops/.ssh/authorized_keys
chown infra-ops:infra-ops /home/infra-ops/.ssh/authorized_keys
chmod 0600 /home/infra-ops/.ssh/authorized_keys
echo 'infra-ops ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/infra-ops
chmod 0440 /etc/sudoers.d/infra-ops
id vh >/dev/null 2>&1 || useradd -u 1000 -U -G docker,sudo -m -s /bin/bash vh
chmod 0700 /home/vh
install -d -m 2775 -o root -g docker /opt/docker /opt/docker/compose /opt/docker/conf
EOF
when: "! pct exec {{ ctid }} -- sh -c 'test \"$(id -u infra-ops)\" = 850 && test \"$(getent group docker | cut -d: -f3)\" = 851 && test -d /opt/docker/compose'"
# Applied BEFORE Docker, so it is already in force when dockerd first starts.
- name: Firewall — 5580 from HA only, SSH from management ranges only
shell: |
pct exec {{ ctid }} -- bash -s <<'EOF'
set -euo pipefail
cat > /etc/nftables.conf <<'EOC'
#!/usr/sbin/nft -f
# esh-matter guard — eshpfi playbooks/esh-matter-lxc.yaml. Policy ACCEPT on
# purpose: only the two ports below are filtered, so Matter UDP and mDNS never
# touch conntrack-based rules.
flush ruleset
table inet matter_guard {
chain input {
type filter hook input priority 0; policy accept;
iif "lo" accept
tcp dport 5580 ip saddr {{ ha_ip }} accept
tcp dport 5580 counter drop
tcp dport 22 ip saddr { {{ ssh_sources }} } accept
tcp dport 22 counter drop
}
}
EOC
systemctl enable -q nftables
systemctl restart nftables
EOF
when: "! pct exec {{ ctid }} -- sh -c 'nft list table inet matter_guard 2>/dev/null | grep -q \"5580 ip saddr {{ ha_ip }} accept\"'"
- name: docker-ce, hands off forwarding and iptables (host networking only)
shell: |
pct exec {{ ctid }} -- bash -s <<'EOF'
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
install -d /etc/docker
printf '{\n "ip-forward": false,\n "iptables": false,\n "ip6tables": false\n}\n' > /etc/docker/daemon.json
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update -qq
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin
EOF
when: "! pct exec {{ ctid }} -- sh -c 'command -v docker' >/dev/null 2>&1"
- name: Matter data dir (fabric credentials) owned by the container's uid 1000
shell: pct exec {{ ctid }} -- install -d -m 0750 -o 1000 -g 1000 /opt/docker/data /opt/docker/data/matter-server
when: "! pct exec {{ ctid }} -- test -d /opt/docker/data/matter-server"
verify:
- name: Running, onboot, in the vzdump job
shell: "pct status {{ ctid }} | grep -q running && pct config {{ ctid }} | grep -q '^onboot: 1' && grep -Eq '^\\s+vmid .*\\b{{ ctid }}\\b' /etc/pve/jobs.cfg"
changed_when: "false"
- name: IPv6 — RA route-info on, forwarding off, a SLAAC address on eth0
shell: |
pct exec {{ ctid }} -- sh -c 'test $(cat /proc/sys/net/ipv6/conf/eth0/accept_ra_rt_info_max_plen) = 64 && test $(cat /proc/sys/net/ipv6/conf/all/forwarding) = 0 && ip -6 addr show dev eth0 scope global | grep -q inet6'
changed_when: "false"
- name: Firewall loaded and persistent
shell: pct exec {{ ctid }} -- sh -c 'nft list table inet matter_guard | grep -Eq "dport 5580 counter .*drop" && systemctl is-enabled --quiet nftables'
changed_when: "false"
- name: Docker left forwarding alone
shell: pct exec {{ ctid }} -- sh -c 'docker info >/dev/null && test $(cat /proc/sys/net/ipv6/conf/all/forwarding) = 0'
changed_when: "false"
- name: Fleet identities are the pinned ids
shell: |
pct exec {{ ctid }} -- sh -c 'test "$(id -u infra-ops)" = 850 && test "$(id -u vh)" = 1000 && test "$(getent group docker | cut -d: -f3)" = 851'
changed_when: "false"
+62
View File
@@ -0,0 +1,62 @@
# esh-matter
LXC **CT 111 on esh-pve** whose **only network leg is VLAN 90 (esh-iot)**. It runs
the **Matter server** (`stacks/matter-server`, matter.js 1.4.0) for Home Assistant.
Built 2026-09-26 for ha-dev (operator-approved) by
[`playbooks/esh-matter-lxc.yaml`](../../playbooks/esh-matter-lxc.yaml).
| | |
|---|---|
| **IPv4** | `10.0.90.20/24`, gateway `10.0.90.1`. Static, below the UDM's `.40–.250` DHCP pool |
| **IPv6** | SLAAC from the Echo's RA: `fdad:29e:d492:fd87:be24:11ff:fe3d:c6c7` (Thread ULA) + link-local. No global prefix exists on VLAN 90 |
| **DNS** | `esh-matter.esh.internal` (HA uses the IP: its container can't resolve `.internal`) |
| **SSH** | `ssh esh-matter` → `infra-ops@10.0.90.20`, or `pct enter 111` on esh-pve |
| **OS** | Debian 12, unprivileged, `nesting=1,keyctl=1` (Docker), 2 cores / 1 GB / 8 GB |
| **Backup** | **in esh-pve's vzdump job** (PBS-ANA 03:30, synced to PBS-NH3). `/opt/docker/data/matter-server` holds the Matter **fabric root credentials**; losing it means re-commissioning every device |
## Why it is built this way
- **It must be on the devices' link.** Matter's operational traffic is IPv6, and
VLAN 90's only IPv6 is the Thread ULA advertised by an Echo border router.
Thread routes arrive as RA route-information options, which are link-only. HA
(VLAN 50) reaches the server over IPv4 `ws://10.0.90.20:5580/ws`, routed by the
UDM (policy `InternalToIOT`), so the HA container did not change.
- **Kernel RA processing, forwarding off.** Set in `/etc/sysctl.d/60-matter-ipv6.conf`:
`accept_ra=1`, `accept_ra_rt_info_max_plen=64`, `all.forwarding=0`. With
forwarding on, RFC 4191 reachability probing stops (matter.js OS requirements).
Docker is configured with `ip-forward/iptables/ip6tables: false`; the server
uses host networking.
- **Firewall (in-CT nftables, `/etc/nftables.conf`):**
- TCP **5580** (websocket + dashboard, **unauthenticated**) is accepted only
from **HA at 10.0.50.46**, then dropped.
- SSH is accepted only from 10.0.50/24, 10.0.10/24, 10.0.250/24, 10.100/16 and
100.64/10.
- Everything else is accepted on purpose, so Matter UDP and mDNS never meet
conntrack. The matter.js docs warn that a stateful firewall's 120 s UDP
timeout drops sleepy-device reports.
- The PVE firewall is disabled on esh-pve, so this in-CT table is the control.
- **An LXC, not a macvlan on esh-docker-vm:** the untrusted IoT leg stays off the
VM that runs HA and everything else.
## Acceptance (2026-09-26 ~1305)
- `ip -6 addr`: link-local + `fdad:29e:d492:fd87:…` ✓. `ping -6` to the Aqara
thermostat `fdad:29e:d492:fd87:56ef:44ff:fea1:7a36`: 3/3 ✓.
- `ip -6 route` learned **two Thread routes by RA**:
- `fd0e:3007:2ebd::/64 via fe80::56ef:44ff:fea1:7a36` (the Aqara W200, Apple
Thread network);
- `fdb6:ee32:f4c2:1::/64 via fe80::5a9a:3eff:fe49:6d36` (the Echo).
- `ws://10.0.90.20:5580/ws` from **inside the homeassistant container**: answered
`server_info` (schema 13, `matter-server/1.4.0 (matter.js/0.17.9)`, fabric 1) ✓.
- **Negative controls:** 5580 is refused from the esh-docker-vm host (10.0.50.45),
from nh3-dev, and from a **VLAN 90 vantage** (a temporary netns on esh-pve at
10.0.90.39, which can ping the CT). SSH is also refused from that VLAN 90
vantage. The drop counters incremented.
## Not yet wired
- **Monitoring.** Kuma can't probe 5580 (only HA may reach it), and there is no
Beszel agent yet. HA's Matter integration surfaces an outage.
- **UniFi mDNS reflector is ON for esh-iot** (`mdns_enabled`). matter.js warns
mDNS forwarders can hinder Matter. It is left alone because other devices may
rely on it; revisit only if commissioning or discovery misbehaves.
+1
View File
@@ -0,0 +1 @@
infra-ops@10.0.90.20
File diff suppressed because one or more lines are too long
+4
View File
@@ -0,0 +1,4 @@
# matter-server tunables (esh-matter). Copy to `.env` on the server.
# matter.js server 1.4.0 (stable, 2026-08-07), pinned by digest.
MATTER_IMAGE=ghcr.io/matter-js/matterjs-server:1.4.0@sha256:54232d0d3e7dff5a54759469d2753399270412b4c30c55b31750a4595e4cb236
MATTER_LOG_LEVEL=info
+18
View File
@@ -0,0 +1,18 @@
# matter-server
The **Matter controller for Home Assistant**: matter.js server
(`ghcr.io/matter-js/matterjs-server`, **1.4.0**, digest-pinned) on **esh-matter**
(CT 111, VLAN 90 only, `10.0.90.20`), with host networking. HA's `matter`
integration connects to `ws://10.0.90.20:5580/ws`.
- matter.js server is the drop-in successor to python-matter-server (HA has used
it since 2026.2), with the same websocket API.
- `/data` → `/opt/docker/data/matter-server` (owned by uid 1000, the image's
unprivileged user) holds the **fabric root credentials**. It is covered by the
CT's vzdump backup. Never commit it.
- :5580 is unauthenticated, so the CT firewall admits HA (10.0.50.46) only.
- Host requirements (IPv6 RA with route-info, forwarding off) and acceptance
results: [`servers/esh-matter/README.md`](../../servers/esh-matter/README.md).
Deploy: `scripts/deploy-stack.sh esh-matter matter-server`, then on the host
`cp -n .env.example .env && docker compose config -q && docker compose up -d`.
+30
View File
@@ -0,0 +1,30 @@
# matter-server — the Matter controller for Home Assistant, on esh-matter (CT 111
# on esh-pve, whose ONLY network leg is VLAN 90 / esh-iot, 10.0.90.20).
# Requested by ha-dev, operator-approved 2026-09-26.
#
# matter.js server (matter-js/matterjs-server), the drop-in successor to
# python-matter-server that HA has used since 2026.2. Same websocket API on
# :5580/ws. Pinned: 1.4.0 (stable, 2026-08-07) by digest.
#
# host networking is REQUIRED: Matter is IPv6 link-local and mDNS. The server must
# see eth0's Thread ULA and the border routers' RAs directly.
#
# ⚠ :5580 (websocket + dashboard) is UNAUTHENTICATED. The CT's nftables admits it
# only from HA at 10.0.50.46 (playbooks/esh-matter-lxc.yaml).
# ⚠ /data holds the Matter fabric ROOT CREDENTIALS. Losing it means re-commissioning
# every device. The whole CT is in esh-pve's vzdump job (PBS-ANA → PBS-NH3). Never
# put it in git.
name: matter-server
services:
matter-server:
image: ${MATTER_IMAGE}
container_name: matter-server
restart: unless-stopped
network_mode: host
volumes:
- /opt/docker/data/matter-server:/data
environment:
- STORAGE_PATH=/data
- PRIMARY_INTERFACE=eth0
- TZ=America/Los_Angeles
- LOG_LEVEL=${MATTER_LOG_LEVEL:-info}