Remote EPICS Access via CA Tunnel¶
How Lightfall connects to beamline EPICS IOCs from anywhere.
The Problem¶
EPICS Channel Access (CA) uses UDP broadcast for PV name resolution.
When a client wants to read BL7ANDOR1:cam1:Acquire, it broadcasts a
UDP search packet on port 5064. Every IOC on the subnet checks if it
owns that PV and replies.
This works great on a local network. It completely breaks for remote access because:
SSH tunnels only forward TCP, not UDP
SOCKS proxies only handle TCP
Firewalls typically block UDP broadcast across subnets
VPNs may carry UDP but add latency and complexity
After the UDP search, CA switches to TCP for the actual data channel. So the data path is fine — it’s only the discovery phase that’s broken.
The Solution: Two Components¶
1. CA Gateway (server-side, on the beamline network)¶
The EPICS CA Gateway
runs on a machine inside the beamline network (e.g., suzume). It’s
both a CA client and a CA server:
Client side: searches for PVs via normal UDP broadcast on the beamline subnet — this works because it’s local
Server side: listens on a TCP port (5099) for client connections
The gateway is the bridge between “CA over local network” and “CA over TCP to a single endpoint.”
[Beamline Network]
IOC (.44) ──UDP──┐
IOC (.43) ──UDP──┤
IOC (.50) ──UDP──┼── CA Gateway (suzume:5099/tcp)
│
[UDP broadcast on 192.168.10.0/24]
2. CA Tunnel (client-side, built into Lightfall)¶
Lightfall’s CATunnelService runs locally on the user’s machine. It solves
the “CA needs UDP but SSH only does TCP” problem with a relay:
caproto (ophyd) CATunnelService SSH Tunnel CA Gateway
│ │ │ │
│──UDP search──────────────────>│ │ │
│ "where is PV:NAME?" │ │ │
│ │──TCP connect────────────────────────────────────>│
│ │ version + host + user │ (SSH -L 5099) │
│ │ + search request │ │
│ │ │ │──UDP search──>IOCs
│ │ │ │<─UDP reply────IOCs
│ │<─TCP search response────────────────────────────│
│ │ "found at port 5099" │ │
│<─UDP reply────────────────────│ │ │
│ "connect to localhost:5099" │ │ │
│ │ │ │
│══TCP circuit═══════════════════════════════════════════════════════════════════>│
│ CreateChan, Read, Write, │ │ (SSH -L 5099) │──CA──>IOC
│ Subscribe (all standard CA) │ │ │<─CA──IOC
│<══════════════════════════════════════════════════════════════════════════════════│
How the relay works¶
caproto sends a UDP search to
localhost:5099(configured viaEPICS_CA_ADDR_LIST). Since SSH-Lonly binds the TCP port, the UDP port is free for our tunnel to bind.CATunnelService receives the UDP packet, opens a TCP connection to
localhost:5099(which SSH forwards to the gateway), and sends:CA version handshake
Hostname + username identification
The search request (with
DO_REPLYflag forced on)
The CA Gateway receives the TCP search, does a real UDP broadcast search on the beamline network, and responds with FOUND or NOT_FOUND.
CATunnelService rewrites FOUND responses: changes the server address to
127.0.0.1:5099so caproto will connect through the tunnel. NOT_FOUND responses are forwarded as-is.CATunnelService sends the response back as UDP to caproto.
caproto opens a TCP circuit to
localhost:5099— this goes through the SSH tunnel to the gateway, which proxies it to the real IOC. All subsequent CA operations (read, write, subscribe, monitor) flow over this TCP circuit.
Why it works¶
The key insight: after the initial search, all CA communication is TCP. The tunnel only needs to bridge the UDP search phase. Once caproto knows where to connect (localhost:5099 → SSH → gateway), the standard CA TCP protocol handles everything — subscriptions, monitors, puts, gets — all through the encrypted SSH tunnel.
Setup¶
Server side (one-time)¶
Build the CA Gateway on a beamline network machine:
# Build PCAS (required for EPICS 7) cd ~/epics-gateway git clone https://github.com/epics-modules/pcas.git echo "EPICS_BASE = /usr/local/epics/R7.0.8.1/base" > pcas/configure/RELEASE.local make -C pcas -j4 # Build CA Gateway git clone https://github.com/epics-extensions/ca-gateway.git printf "EPICS_BASE = /usr/local/epics/R7.0.8.1/base\nPCAS = $(pwd)/pcas\n" > ca-gateway/configure/RELEASE.local make -C ca-gateway -j4Configure (
config/pvlist.txtandconfig/access.txt):# pvlist.txt — allow all PVs EVALUATION ORDER ALLOW, DENY .* ALLOW# access.txt — read/write from tunnel clients HAG(tunnel) { localhost } ASG(DEFAULT) { RULE(1, READ) RULE(1, WRITE) { HAG(tunnel) } }Install as a systemd service (
/etc/systemd/system/ca-gateway.service):[Unit] Description=EPICS Channel Access Gateway After=network.target [Service] Type=simple User=rp Environment=LD_LIBRARY_PATH=/home/rp/epics-gateway/pcas/lib/linux-x86_64:/usr/local/epics/R7.0.8.1/base/lib/linux-x86_64 ExecStart=/home/rp/epics-gateway/ca-gateway/bin/linux-x86_64/gateway \ -pvlist /home/rp/epics-gateway/config/pvlist.txt \ -access /home/rp/epics-gateway/config/access.txt \ -log /home/rp/epics-gateway/gateway.log \ -sip localhost -sport 5099 \ -cip "192.168.10.255 localhost" -cport 5064 \ -debug 1 Restart=on-failure [Install] WantedBy=multi-user.targetNote:
-cipmust include the broadcast address of the beamline subnet so the gateway can discover IOCs on other hosts.
Client side¶
SSH tunnel (add
-L 5099:localhost:5099to your SSH command):ssh -D 1080 -L 5099:localhost:5099 rp@suzume.lbl.govLightfall settings (Settings → Devices → Connection Settings):
Enable “CA tunnel for remote access”
Gateway address:
localhost:5099
Restart Lightfall. Devices connect automatically.
Limitations¶
Latency: Each PV search requires a TCP round-trip through the SSH tunnel (~50-200ms depending on network). Batch searches help but initial connection takes 10-30 seconds for many devices.
IOC must be reachable from the gateway host: If the gateway can’t reach an IOC via UDP broadcast, the PV won’t be found. Check firewall rules (both on the gateway host and the IOC host).
One gateway per network segment: The gateway searches via broadcast on its local subnet. IOCs on other subnets need additional
-cipentries or a separate gateway.Not suitable for end users: Requires SSH access and tunnel setup. For production remote access, see the WebSocket PV Bridge proposal which will provide Keycloak-authenticated access over standard HTTPS.
Architecture Decision: Why Not Just Use PV Access?¶
PV Access (pvAccess, EPICS 7) is TCP-native and would tunnel perfectly. However:
Most ALS IOCs run CA, not pvAccess
ophyd uses caproto (pure Python CA) by default, not p4p
Switching the entire control layer is a larger change than bridging CA
The CA tunnel is a pragmatic solution that works with the existing infrastructure. The WebSocket PV Bridge is the long-term answer.
Files¶
src/lightfall/services/ca_tunnel.py— CATunnelService implementationsrc/lightfall/main.py— Tunnel startup, timeout patching, auto-retrysrc/lightfall/ui/preferences/device_settings.py— Settings UI