Files
Jack Nagy 1a35cd59a1 feat(protocol): add DTLS ClientHello liveness probe + wire it into the bridge
A stateless-by-default DTLS ClientHello probe that classifies a host:port
as DEAD/LIVE/COMPLETED/REJECTED in ~1 RTT off the server's first flight,
sitting in front of the full handshake.

Probe (smartthings_local/protocol/dtls_probe.py):
- Stateless liveness mode (default): stops at HelloVerifyRequest and never
  sends the cookie'd second ClientHello, so by RFC 6347 §4.2.1 it leaves
  no association on the device — safe to run before a real connect.
- Diagnostic mode (stateless=False): drives the handshake further to
  capture cipher/cert-chain/CertificateRequest or a fatal Alert, for
  OCF-PKI-wall characterization (#16). Kept out of hot reconnect paths.
- Retransmit + retries: services OpenSSL's DTLS retransmit timer so a
  single dropped ClientHello no longer reads as a false DEAD.

MQTT bridge (mqtt_demo):
- Stateless pre-flight gate in session_once() rejects a silent/rebooting
  device or wrong port in ~3s (retries=1) instead of eating the 12s
  HANDSHAKE_TIMEOUT_S per reconnect.
- OCF-band port autodiscovery when OCF_PORT is unset: races the band in
  parallel and returns on the first port to answer LIVE (~1 RTT, abandoning
  the dead-port probes), cached across reconnects; the stateless gate
  leaves no orphan, preserving the fixed-source-port §4.2.8 invariant.

Validated on real hardware (dryer 49155 / oven 49154): parallel discovery
resolves both ports in <1s, connect with no orphan cooldown, and a wrong
pinned port rejected in ~3s.

Tests: probe behaviour (retransmit recovery, stateless single-flight
guard, silent-port flight budget, diagnostic continuation) and bridge
port-resolution (pinned gate, parallel discovery early-exit, cache).
2026-08-01 10:57:54 +01:00

64 lines
2.2 KiB
Bash

# SmartThings-Local Bridge config.
# Copy to `.env` and fill in. Never commit `.env`.
# =============================================================
# Appliances — one process supervises N appliances over DTLS.
# =============================================================
# APPLIANCE_COUNT defines how many entries to read. Per-appliance
# keys are 1-indexed (APPLIANCE_1_*, APPLIANCE_2_*, …).
APPLIANCE_COUNT=1
# Appliance 1 — Samsung dryer
APPLIANCE_1_CLASS=dryer
APPLIANCE_1_IP=192.168.1.100
# OCF_PORT is optional. Leave it blank to auto-discover the live DTLS
# port each connect (a stateless ClientHello races the OCF band
# 49153-49156); set it to pin a specific port and skip discovery.
APPLIANCE_1_OCF_PORT=
APPLIANCE_1_TOPIC=samsung_dryer
APPLIANCE_1_NAME=Samsung Dryer
# Future:
# APPLIANCE_2_CLASS=oven
# APPLIANCE_2_IP=192.168.1.101
# APPLIANCE_2_OCF_PORT=
# APPLIANCE_2_TOPIC=samsung_oven
# APPLIANCE_2_NAME=Samsung Oven
# (Don't forget to bump APPLIANCE_COUNT=2.)
# --- Cert paths ---
# Defaults work for Docker (mount as /config) and bare-metal (drop
# into ./certs). The client cert + key are built by setup_cert.py.
# CERT_PATH=./certs/client_fullchain.pem
# KEY_PATH=./certs/client.key
# --- MQTT broker (HA Mosquitto add-on or any broker) ---
MQTT_BROKER=192.168.1.5
MQTT_PORT=1883
MQTT_USER=samsung_bridge
MQTT_PASS=
# HA discovery prefix — must match the MQTT integration's setting in HA
# (default `homeassistant`).
HA_DISCOVERY_PREFIX=homeassistant
# Bridge timers (seconds).
# HEALTH_INTERVAL_S — how often <prefix>/bridge/health republishes.
# PING_INTERVAL_S — CoAP empty-CON ping cadence (DTLS-layer
# liveness). Three consecutive failures publish
# availability=offline.
# State freshness itself comes from the in-bridge PollScheduler whose
# tier cadences are declared in the appliance descriptor — there is
# no top-level heartbeat env var to tune.
HEALTH_INTERVAL_S=60
PING_INTERVAL_S=25
# Container TZ.
TZ=Europe/London
# --- Deploy (deploy.sh — tar + ssh docker compose) ---
SSH_HOST=user@your-server
REMOTE_DIR=/mnt/user/compose/smartthings-local
APPDATA_DIR=/mnt/user/appdata/smartthings-local