diff --git a/kubernetes/apps/otbr/deployment.yaml b/kubernetes/apps/otbr/deployment.yaml index 82b011e5..5d2aed3c 100644 --- a/kubernetes/apps/otbr/deployment.yaml +++ b/kubernetes/apps/otbr/deployment.yaml @@ -29,49 +29,48 @@ spec: # RCP is reached over the network (slzb-thread.renner.uno). nodeSelector: net.renner.uno/iot-vlan: "true" + initContainers: + # The Thread RCP is a network-attached radio (spinel over TCP). + # otbr-agent speaks spinel+hdlc over a Unix stream socket, which this + # native sidecar serves on the shared emptyDir and bridges to the + # radio, one TCP connection per agent connection. A PTY would not + # work here: devpts is per-container. + - name: radio-bridge + image: alpine/socat:1.8.1.3@sha256:466c8a0383e16463fabdb7df01008292fb78d07da60224a46cb5d3142ce122ed + imagePullPolicy: IfNotPresent + restartPolicy: Always + args: + - -d + - UNIX-LISTEN:/run/thread/spinel.sock,fork,unlink-early,mode=600 + - TCP:slzb-thread.renner.uno:6638 + startupProbe: + exec: + command: ["test", "-S", "/run/thread/spinel.sock"] + periodSeconds: 1 + failureThreshold: 30 + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: radio + mountPath: /run/thread containers: - # The Thread RCP is a network-attached radio (Serial-over-IP), so the - # spinel transport needs a TCP-to-PTY bridge. Two obvious approaches - # do not work and should not be retried: - # - # - `spinel+hdlc+forkpty://` needs socat, which the upstream image - # does not ship; the URL fails with a silent ENOENT and otbr-agent - # dies right after logging it. - # - A sidecar cannot provide it either: devpts is per-container, so - # a PTY created next door is invisible here and the symlink dangles. - # - # Hence socat is installed at startup and run inside this container. - name: otbr - image: openthread/otbr:latest@sha256:51f9af250252f4faae85d0197574a7c17ada873c78799854461a93e3e42cf324 + image: openthread/otbr:latest@sha256:cada244b39d1f43cb12d5f93694b4621db3b51c28219a05bbffac0f898d1403e command: - bash - -c - | set -euo pipefail - if ! command -v socat >/dev/null; then - echo "otbr: installing socat from apt" - apt-get update -qq - apt-get install -y -qq socat - fi - echo "otbr: starting TCP↔PTY bridge to ${SLZB_HOST}:${SLZB_PORT}" - # Match the HA addon's socat invocation byte-for-byte - # (rootfs/etc/s6-overlay/s6-rc.d/socat-otbr-tcp/run): the - # known-working reference for SMLight network-attached - # Thread radios. ignoreeof keeps the data loop alive when - # otbr-agent transiently closes the PTY (e.g. on agent - # restart from livenessProbe), instead of socat exiting - # and racing to recreate the PTY symlink. - ( while true; do - socat -d \ - "pty,raw,echo=0,link=/tmp/ttyThread,ignoreeof" \ - "tcp:${SLZB_HOST}:${SLZB_PORT}" - echo "otbr: socat bridge exited; restarting in 2s" >&2 - sleep 2 - done ) & - until [ -e /tmp/ttyThread ]; do - echo "otbr: waiting for serial bridge PTY at /tmp/ttyThread" - sleep 1 - done # SLZB-06P7 firmware crashes otbr-agent if a saved dataset # is loaded before the radio's PHY is enabled: dataset # commit triggers KeyManager → otPlatRadioSetMacKey, which @@ -94,8 +93,8 @@ spec: /app/etc/docker/test/docker_entrypoint.sh & ENTRYPOINT_PID=$! # Wait for the daemon socket to come up. Up to 90s of - # attempts at 2s intervals — covers slow apt installs, - # spinel handshake, and agent init on this radio. + # attempts at 2s intervals — covers the spinel handshake + # and agent init on this radio. for i in $(seq 1 45); do if ot-ctl state >/dev/null 2>&1; then break @@ -167,16 +166,9 @@ spec: fi wait "$ENTRYPOINT_PID" env: - - name: SLZB_HOST - value: "slzb-thread.renner.uno" - - name: SLZB_PORT - value: "6638" - # Match the HA addon's RADIO_URL byte-for-byte. baud is - # omitted because PTYs ignore termios speed; uart-flow- - # control is a no-op on a PTY but stays in to mirror the - # known-working reference exactly. + # The radio-bridge sidecar's socket. - name: RADIO_URL - value: "spinel+hdlc+uart:///tmp/ttyThread?uart-flow-control" + value: "spinel+hdlc+unix:///run/thread/spinel.sock" # Use the macvlan attachment as the backbone interface. mDNS # advertisements, SRP server listening, and RA reception all # happen on this NIC. eth0 (the Cilium pod CIDR) is the @@ -249,6 +241,8 @@ spec: # and re-election on every restart). - name: data mountPath: /var/lib/thread + - name: radio + mountPath: /run/thread # Probes must watch the *protocol*, not just the process. # # The upstream entrypoint backgrounds otbr-agent and ends in @@ -266,8 +260,7 @@ spec: # from this image, so probes go through ot-ctl, not REST. # # startupProbe: the agent's control socket answers. Covers the - # apt install, the socat TCP↔PTY bridge to the SLZB radio, and - # the spinel handshake. Generous: 10s × 60 = 10 minutes. While + # spinel handshake with the SLZB radio and agent init. Generous: 10s × 60 = 10 minutes. While # it runs, the other two probes are suspended, so the entrypoint # gets to finish activating Thread before liveness may act. startupProbe: @@ -340,3 +333,5 @@ spec: - name: data persistentVolumeClaim: claimName: otbr-thread + - name: radio + emptyDir: {} diff --git a/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml b/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml index 3b974389..d9012319 100644 --- a/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml +++ b/kubernetes/infrastructure/network-policies/home-assistant-ns.yaml @@ -541,6 +541,38 @@ spec: - port: "443" protocol: TCP --- +# otbr: the radio-bridge sidecar's spinel link to the SLZB-06P7 Thread radio. +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: otbr-egress + namespace: home-assistant +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: otbr + egress: + # rules.dns lets Cilium learn the toFQDNs mapping + - toEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": kube-system + k8s-app: kube-dns + toPorts: + - ports: + - port: "53" + protocol: UDP + - port: "53" + protocol: TCP + rules: + dns: + - matchPattern: "*" + - toFQDNs: + - matchName: "slzb-thread.renner.uno" + toPorts: + - ports: + - port: "6638" + protocol: TCP +--- # mqtt-account-controller: credential delivery to deliver.esp32 targets in mqtt-accounts.yaml. apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy