Block all DDS off the Pi and bring up PipeWire behind the firewall

ROS never leaves the robot, by the owner's decision: the firewall drops domain
42's DDS ports on every interface but loopback (usb0 included), discovery is
pinned to LOCALHOST, and hermes_mcp is the only bridge to the outside. PipeWire
1.0.5 runs as hermes's user services with its network server on TCP 4713; the
firewall, loaded at boot before it, is the protection. Verified from talos: 4713
connects over usb0 and times out over Wi-Fi.

CONTEXT.md brought up to date: the face service, gaze, recognition, the firewall,
and what is left.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs
This commit is contained in:
2026-10-04 23:18:58 -07:00
co-authored by Claude Sonnet 5.5
parent dba756f3a6
commit 880bdd3f44
4 changed files with 63 additions and 19 deletions
+5 -4
View File
@@ -1,9 +1,10 @@
#!/usr/sbin/nft -f
# The robot's audio (4713), MCP (8765) and face webhook (8766) ports are reachable only over the
# USB link, and only from the host at 10.55.0.1. Every other interface, Wi-Fi included, is dropped.
# ROS 2's DDS has no authentication, and /camera/* and /vision/faces carry video and recognised
# names, so its ports (domain 42: 17900-18149 with Fast DDS's default offsets) are dropped on every
# interface but usb0 and loopback too.
# ROS 2 never leaves the robot: its DDS has no authentication, and /camera/* and /vision/faces
# carry video and recognised names. Domain 42's ports (17900-18149 with Fast DDS's default offsets)
# are dropped on every interface but loopback, usb0 included. The only way in or out is hermes_mcp,
# which talks to the ROS graph locally and to the host over MCP.
# SSH and everything else are left alone: that policy is the owner's call.
table inet robot
@@ -15,6 +16,6 @@ table inet robot {
iif "lo" accept
iifname "usb0" ip saddr 10.55.0.1 tcp dport { 4713, 8765, 8766 } accept
tcp dport { 4713, 8765, 8766 } drop
iifname != { "lo", "usb0" } udp dport 17900-18149 drop
iifname != "lo" udp dport 17900-18149 drop
}
}
+6 -4
View File
@@ -1,11 +1,13 @@
# ~/.config/pipewire/pipewire-pulse.conf.d/50-network-server.conf (on the Pi)
#
# Lets your Omarchy machine reach the robot's microphone and speaker over the
# network, using the PulseAudio protocol that pipewire-pulse speaks.
# Lets the host reach the robot's microphone and speaker over the USB link, using the
# PulseAudio protocol that pipewire-pulse speaks.
#
# This replaces the default address list, so "unix:native" must stay in it.
# Anyone on the LAN who can reach port 4713 could listen to the microphone;
# the nftables rules installed alongside restrict it to your machine's IP.
# It listens on every interface and is unauthenticated: anyone who can reach port 4713 could
# listen to the microphone. The protection is the firewall (pi/nftables-robot.nft, loaded at boot
# by robot-firewall.service), which accepts 4713 only on usb0 from 10.55.0.1 and drops it on every
# other interface, Wi-Fi included. Do not enable this without that firewall loaded.
pulse.properties = {
server.address = [
+5 -5
View File
@@ -3,14 +3,14 @@
# - the Raspberry Pi libcamera fork ahead of ROS's own copy (same soname, but ROS's crashes in the
# Pi image plugin: see CONTEXT.md)
# - ~/venv's site-packages first: Blinka/Adafruit for the display, OpenCV 4.10 for YuNet
# - a fixed ROS domain (42), discovery on this machine only. DDS has no authentication and the
# camera and recognised names travel on it: set ROBOT_ROS_DISCOVERY=SUBNET only to
# reach it from a host over the USB link (the firewall drops DDS ports on every other interface)
# - a fixed ROS domain (42), discovery on this machine only. ROS never leaves the robot: DDS has
# no authentication and carries the camera and recognised names, so the firewall drops its ports
# on every interface but loopback. hermes_mcp is the only bridge to the outside world.
source /opt/ros/jazzy/setup.bash
[ -f "$HOME/camera_ws/install/setup.bash" ] && source "$HOME/camera_ws/install/setup.bash"
[ -f "$HOME/ros_ws/install/setup.bash" ] && source "$HOME/ros_ws/install/setup.bash"
export LD_LIBRARY_PATH="/opt/libcamera-rpi/lib/aarch64-linux-gnu:${LD_LIBRARY_PATH:-}"
export PYTHONPATH="$HOME/venv/lib/python3.12/site-packages:${PYTHONPATH:-}"
export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-42}"
# ROS's own setup script exports SUBNET, so a ${VAR:-default} on the ROS variable would never apply
export ROS_AUTOMATIC_DISCOVERY_RANGE="${ROBOT_ROS_DISCOVERY:-LOCALHOST}"
# ROS's own setup script exports SUBNET, so this must be assigned, not defaulted
export ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST