From 076008d52d2ecea231169529587ae422ddf4dc2b Mon Sep 17 00:00:00 2001 From: Konstantin Renner Date: Thu, 2 Jul 2026 17:54:56 +0200 Subject: [PATCH 1/5] edge-host: enable Docker IPv6 (sml2mqtt broker connect) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mqtt.renner.uno resolves AAAA-first; the default bridge has no IPv6 and sml2mqtt does not fall back to A records — InitialMqttConnectionFailed loop after the USB cutover. Same daemon-level IPv6+ULA+NAT66 fix as the netbird-proxy role. Co-Authored-By: Claude Fable 5 --- ansible/roles/edge-host/handlers/main.yml | 5 +++++ ansible/roles/edge-host/tasks/main.yml | 14 ++++++++++++++ ansible/roles/edge-host/templates/daemon.json.j2 | 5 +++++ 3 files changed, 24 insertions(+) create mode 100644 ansible/roles/edge-host/templates/daemon.json.j2 diff --git a/ansible/roles/edge-host/handlers/main.yml b/ansible/roles/edge-host/handlers/main.yml index 8962059a..08e373ab 100644 --- a/ansible/roles/edge-host/handlers/main.yml +++ b/ansible/roles/edge-host/handlers/main.yml @@ -1,4 +1,9 @@ --- +- name: Restart docker + ansible.builtin.service: + name: docker + state: restarted + # reload alone only re-reads config files; existing links keep their old # state until reconfigured. - name: Reload networkd diff --git a/ansible/roles/edge-host/tasks/main.yml b/ansible/roles/edge-host/tasks/main.yml index b88c47b0..e3e8b390 100644 --- a/ansible/roles/edge-host/tasks/main.yml +++ b/ansible/roles/edge-host/tasks/main.yml @@ -29,6 +29,20 @@ state: started enabled: true +# mqtt.renner.uno resolves to an IPv6 LB address first; docker's default +# bridge has no IPv6, and sml2mqtt does not fall back to A records +# ("Network unreachable" at startup). Same fix as the netbird-proxy role: +# daemon-level IPv6 + ULA + ip6tables NAT66. +- name: Enable IPv6 in the Docker daemon + ansible.builtin.template: + src: daemon.json.j2 + dest: /etc/docker/daemon.json + mode: "0644" + notify: Restart docker + +- name: Apply Docker daemon config before containers start + ansible.builtin.meta: flush_handlers + # --- Directory layout --- - name: Create service directories ansible.builtin.file: diff --git a/ansible/roles/edge-host/templates/daemon.json.j2 b/ansible/roles/edge-host/templates/daemon.json.j2 new file mode 100644 index 00000000..e5ab7163 --- /dev/null +++ b/ansible/roles/edge-host/templates/daemon.json.j2 @@ -0,0 +1,5 @@ +{ + "ipv6": true, + "fixed-cidr-v6": "fd00:ed9e:1::/64", + "ip6tables": true +} -- 2.54.0 From c660af46855f8f4781698f0aa864600b86908a0f Mon Sep 17 00:00:00 2001 From: Konstantin Renner Date: Thu, 2 Jul 2026 17:57:23 +0200 Subject: [PATCH 2/5] edge-host: create /etc/docker before daemon.json (Arch has none) Co-Authored-By: Claude Fable 5 --- ansible/roles/edge-host/tasks/main.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ansible/roles/edge-host/tasks/main.yml b/ansible/roles/edge-host/tasks/main.yml index e3e8b390..68be35e8 100644 --- a/ansible/roles/edge-host/tasks/main.yml +++ b/ansible/roles/edge-host/tasks/main.yml @@ -33,6 +33,12 @@ # bridge has no IPv6, and sml2mqtt does not fall back to A records # ("Network unreachable" at startup). Same fix as the netbird-proxy role: # daemon-level IPv6 + ULA + ip6tables NAT66. +- name: Ensure /etc/docker exists + ansible.builtin.file: + path: /etc/docker + state: directory + mode: "0755" + - name: Enable IPv6 in the Docker daemon ansible.builtin.template: src: daemon.json.j2 -- 2.54.0 From be3d5d7cb04603c986e13b8ab53a0a5782c98377 Mon Sep 17 00:00:00 2001 From: Konstantin Renner Date: Thu, 2 Jul 2026 18:00:52 +0200 Subject: [PATCH 3/5] edge-host: compose network needs its own enable_ipv6 fixed-cidr-v6 only covers docker0; the compose-created network stayed v4-only and sml2mqtt kept failing to reach the broker. Co-Authored-By: Claude Fable 5 --- .../roles/edge-host/templates/docker-compose.yml.j2 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ansible/roles/edge-host/templates/docker-compose.yml.j2 b/ansible/roles/edge-host/templates/docker-compose.yml.j2 index 53c510aa..13364424 100644 --- a/ansible/roles/edge-host/templates/docker-compose.yml.j2 +++ b/ansible/roles/edge-host/templates/docker-compose.yml.j2 @@ -5,6 +5,16 @@ # No `privileged` needed here: docker's --device handles the device-cgroup # grant that hostPath in k8s could not (that was the only reason the pods # ran privileged). +# The default compose network needs IPv6 declared explicitly — it does +# NOT inherit the daemon's fixed-cidr-v6 (that only covers docker0). +# NAT66 comes from the daemon's ip6tables: true. +networks: + default: + enable_ipv6: true + ipam: + config: + - subnet: fd00:ed9e:2::/64 + services: zwave-js: image: {{ edge_zwavejs_image }} -- 2.54.0 From 180d4ac1c0360c3b50b7f70fd6556c5c19e80255 Mon Sep 17 00:00:00 2001 From: Konstantin Renner Date: Thu, 2 Jul 2026 18:05:18 +0200 Subject: [PATCH 4/5] edge-host: sml2mqtt on host networking (docker NAT66 gap) Bridge-network IPv6 SYNs to the AAAA-first broker LB leave without masquerade and die; sml2mqtt is outbound-only, and the host stack reaches the broker (open3e proves it). Daemon/network v6 kept for future containers. Co-Authored-By: Claude Fable 5 --- ansible/roles/edge-host/templates/docker-compose.yml.j2 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ansible/roles/edge-host/templates/docker-compose.yml.j2 b/ansible/roles/edge-host/templates/docker-compose.yml.j2 index 13364424..2cb20e16 100644 --- a/ansible/roles/edge-host/templates/docker-compose.yml.j2 +++ b/ansible/roles/edge-host/templates/docker-compose.yml.j2 @@ -43,6 +43,11 @@ services: image: {{ edge_sml2mqtt_image }} container_name: sml2mqtt restart: unless-stopped + # Outbound-only (serial in, MQTT out) — host networking sidesteps the + # docker NAT66 gap: the broker LB is IPv6-first and bridge-network v6 + # SYNs leave unmasqueraded and die. The host stack reaches fd99::81 + # fine (open3e proves it continuously). + network_mode: host # Serial exclusivity: this is the ONLY reader of the meter port — # HA's edl21 integration stays disabled (see sml2mqtt-config comment # history in the k8s repo). -- 2.54.0 From d928f9fd9c6cf843fa2f999f4488a385eefed010 Mon Sep 17 00:00:00 2001 From: Konstantin Renner Date: Thu, 2 Jul 2026 18:09:18 +0200 Subject: [PATCH 5/5] =?UTF-8?q?edge-host:=20host=20networking=20for=20both?= =?UTF-8?q?=20containers=20=E2=80=94=20no=20bridge,=20no=20NAT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review: pass real addresses instead of NAT. zwave-js binds 3000/8091 on the host directly; sml2mqtt is outbound-only; the NAT66 daemon config and compose-network v6 experiment are removed. Co-Authored-By: Claude Fable 5 --- .../inventory/group_vars/edge_host/vault.yml | 18 ++++++++++++++++ ansible/roles/edge-host/handlers/main.yml | 5 ----- ansible/roles/edge-host/tasks/main.yml | 21 ++----------------- .../roles/edge-host/templates/daemon.json.j2 | 5 ----- .../edge-host/templates/docker-compose.yml.j2 | 19 ++++++----------- 5 files changed, 26 insertions(+), 42 deletions(-) create mode 100644 ansible/inventory/group_vars/edge_host/vault.yml delete mode 100644 ansible/roles/edge-host/templates/daemon.json.j2 diff --git a/ansible/inventory/group_vars/edge_host/vault.yml b/ansible/inventory/group_vars/edge_host/vault.yml new file mode 100644 index 00000000..a94e2b6f --- /dev/null +++ b/ansible/inventory/group_vars/edge_host/vault.yml @@ -0,0 +1,18 @@ +$ANSIBLE_VAULT;1.1;AES256 +66366335386261303463626230383532323132366232373839643563643531666662383366313336 +6562666333613539396234363135306238323662343132380a636631303364356564656536633164 +62373531663635633139373231343434393035646263373833623636613133653561353266383064 +3261663765316431620a343665653835363433373637616538396163376366353235653561663364 +30353532373061633139623032373263376339616334623931636537633263663237663466636461 +39646165343961316364613431393563356562646438346466653133313264366562393164666633 +64616164333235653734313562303864633534303037306332613934386238313764656465333465 +63363936653566326534333434633631316536336537656334656661663637353830333237356532 +66323566356138633161383334363334663035313762636561626466613165333065623136663537 +62623863373465316636643933323539303232653263636462646363383861353932366138353233 +37303363313362306364303062663435653335343561373934366433306439616431323365636365 +64363837353466396130643962343134376361343431383631336565633866623037613065346364 +61373664653634656135373466373038626533376361373862316562623035306632333533333231 +37353665623738623534353965303634663331613433623661343338666530313863313731316337 +37616364633261356361383937376132333636663464326336343439626666313636373436633338 +63663730323134376536663562633034323339643537393937336362633333373434643037663331 +3634 diff --git a/ansible/roles/edge-host/handlers/main.yml b/ansible/roles/edge-host/handlers/main.yml index 08e373ab..8962059a 100644 --- a/ansible/roles/edge-host/handlers/main.yml +++ b/ansible/roles/edge-host/handlers/main.yml @@ -1,9 +1,4 @@ --- -- name: Restart docker - ansible.builtin.service: - name: docker - state: restarted - # reload alone only re-reads config files; existing links keep their old # state until reconfigured. - name: Reload networkd diff --git a/ansible/roles/edge-host/tasks/main.yml b/ansible/roles/edge-host/tasks/main.yml index 68be35e8..37798fc9 100644 --- a/ansible/roles/edge-host/tasks/main.yml +++ b/ansible/roles/edge-host/tasks/main.yml @@ -29,25 +29,8 @@ state: started enabled: true -# mqtt.renner.uno resolves to an IPv6 LB address first; docker's default -# bridge has no IPv6, and sml2mqtt does not fall back to A records -# ("Network unreachable" at startup). Same fix as the netbird-proxy role: -# daemon-level IPv6 + ULA + ip6tables NAT66. -- name: Ensure /etc/docker exists - ansible.builtin.file: - path: /etc/docker - state: directory - mode: "0755" - -- name: Enable IPv6 in the Docker daemon - ansible.builtin.template: - src: daemon.json.j2 - dest: /etc/docker/daemon.json - mode: "0644" - notify: Restart docker - -- name: Apply Docker daemon config before containers start - ansible.builtin.meta: flush_handlers +# No docker daemon.json needed: both services run with network_mode: host +# (real host addresses, v4+v6, no bridge/NAT) — see docker-compose.yml.j2. # --- Directory layout --- - name: Create service directories diff --git a/ansible/roles/edge-host/templates/daemon.json.j2 b/ansible/roles/edge-host/templates/daemon.json.j2 deleted file mode 100644 index e5ab7163..00000000 --- a/ansible/roles/edge-host/templates/daemon.json.j2 +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ipv6": true, - "fixed-cidr-v6": "fd00:ed9e:1::/64", - "ip6tables": true -} diff --git a/ansible/roles/edge-host/templates/docker-compose.yml.j2 b/ansible/roles/edge-host/templates/docker-compose.yml.j2 index 2cb20e16..cd14d67d 100644 --- a/ansible/roles/edge-host/templates/docker-compose.yml.j2 +++ b/ansible/roles/edge-host/templates/docker-compose.yml.j2 @@ -5,16 +5,9 @@ # No `privileged` needed here: docker's --device handles the device-cgroup # grant that hostPath in k8s could not (that was the only reason the pods # ran privileged). -# The default compose network needs IPv6 declared explicitly — it does -# NOT inherit the daemon's fixed-cidr-v6 (that only covers docker0). -# NAT66 comes from the daemon's ip6tables: true. -networks: - default: - enable_ipv6: true - ipam: - config: - - subnet: fd00:ed9e:2::/64 - +# Both services use host networking: real host addresses (v4+v6), no +# bridge, no NAT. The broker LB is IPv6-first and docker bridge NAT66 is +# a tarpit (unmasqueraded SYNs die silently); the host stack just works. services: zwave-js: image: {{ edge_zwavejs_image }} @@ -35,9 +28,9 @@ services: # settings.json is edited via the UI and persists here; zwave.port # stays /dev/zwave. - ./zwave-js/store:/usr/src/app/store - ports: - - "3000:3000" # Z-Wave JS websocket — HA zwave_js integration - - "8091:8091" # web UI + # Host networking: binds 3000 (Z-Wave JS websocket for HA) and 8091 + # (web UI) directly on the host. + network_mode: host sml2mqtt: image: {{ edge_sml2mqtt_image }} -- 2.54.0