# Connector > **Security warning:** The Connector authenticates to the Switch with a pre-shared key (`EDRI_SWITCH_SECRET` / `EDRI_SWITCH_SECRET_FILE`) but the link is **not** encrypted, and both sides exchange Python pickle, which is not safe against a compromised peer. Connecting to a non-loopback Switch requires the secret. Keep Switch traffic on a trusted, isolated network and add a VPN, mTLS, or service mesh for anything sensitive. The **Connector** bridges a local {doc}`/router` to the {doc}`/switch` — a TCP/IP hub that connects multiple EDRI instances across a network. Without a Connector, events stay inside a single EDRI process tree. With a Connector, shareable events can travel to other machines. --- ## Where the Connector fits ```mermaid flowchart LR subgraph A["EDRI Instance A"] direction LR RA[Router] <--> CA[Connector] end subgraph B["EDRI Instance B"] direction LR CB[Connector] <--> RB[Router] end CA <-->|"TCP/IP (Switch)"| CB ``` The Connector lives inside the EDRI process tree, started from the main thread like any other component. --- ## How it works internally The Connector runs as its own process. Inside it there is a main loop plus two background threads, all sharing one `Socket` helper that owns the TCP connection to the Switch: ```mermaid flowchart TD subgraph Conn["Connector (process)"] direction TB Main["main loop (run)
connect/reconnect Socket
send UUID + demands
restart receiver if needed"] Recv["receiver thread
reads Switch frames"] Send["sender thread
sender queue → Switch"] end Router["local Router"] -. pipe .-> Main Main -->|"SubscribedNew"| D1["send updated demands to the Switch"] Main -->|"LastEvents / SendFrom"| D2["cache-replay coordination"] Main -->|"other shareable events"| Send Recv -->|"remote demands"| D3["update remote map
SubscribeConnector
to Router"] Recv -->|"remote events"| Router ``` > **Legend:** The dotted arrow (`-.->`) represents a [`Pipe`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Pipe) — the inter-process communication channel between the Router and the Connector process. Solid arrows represent data flow within the same process or across the TCP connection. ### Responsibilities | Part | Purpose | |-------------------|--------------------------------------------------------------------------| | `Socket` | Owns the TCP connection; authenticated handshake, framing, (re)connect, `send_demands`, `send_router`, `receive` | | main loop (`run`) | Connects, propagates local subscription changes as demands, and routes local shareable events to the sender queue | | `receiver` thread | Reads from the Switch: applies remote demands and delivers remote events to the local Router | | `sender` thread | Serializes and sends queued shareable events to the Switch | --- ## Event continuity (best-effort replay) The Connector helps shareable events survive a brief network interruption, backed by the Router's in-memory cache rather than a durable queue: 1. The Router caches every routed event for `EDRI_CACHE_TIMEOUT` seconds, tagged with the originating Router UUID and a stable event ID. 2. On (re)connect, the Connector re-announces its Router UUID and demands, then asks the peer — via a `LastEvents` exchange — for the **ID of the last event it received** from this instance. 3. The peer replays, from its cache, every shareable event recorded **after** that ID (`SendFrom`), then resumes normal flow. This means: * Recovery is **pull-based replay from a marker**, not an acknowledged queue — each side tells the other where to resume. Events delivered after the marker are not re-sent (approximate deduplication), and events before the outage's end are replayed from the cache. * The cache window is the **only** safety net: an outage longer than `EDRI_CACHE_TIMEOUT` (default 30 s) drops the older events permanently. This is why `EDRI_CACHE_TIMEOUT` must be at least 10 s. * Duplication is possible in edge cases: if an event was delivered but its acknowledgement had not yet been replicated before the connection dropped, the marker-based replay may resend it. Handlers should be **idempotent** if duplicate events are unacceptable. * Short outages (seconds) are transparent to the application. --- ## Configuration ### Environment variables | Variable | Default | Description | |---------------------------|-------------|--------------------------------------------------| | `EDRI_SWITCH_HOST` | `localhost` | Switch hostname or IP address | | `EDRI_SWITCH_PORT` | `8899` | Switch TCP port | | `EDRI_SWITCH_SECRET` | *(unset)* | Pre-shared key; must match the Switch. Required unless the Switch is on loopback | | `EDRI_SWITCH_SECRET_FILE` | *(unset)* | Path to a one-key-per-line file; takes precedence over `EDRI_SWITCH_SECRET` | | `EDRI_SWITCH_MAX_MESSAGE` | `67108864` | Largest frame payload the Connector will send, in bytes (64 MiB) | | `EDRI_CACHE_TIMEOUT` | `30` | How long events stay in the local cache (seconds)| | `EDRI_CACHE_INFO_MESSAGE` | `60` | Log interval for cache statistics (seconds) | ### Starting the Connector ```python from uuid import UUID from edri import EDRI edri = EDRI() # Optional: assign a specific Router ID for reconnection router_id = UUID("12345678-1234-5678-1234-567812345678") edri.start_connector(router_id=router_id) edri.run() ``` If no `router_id` is provided, a random UUID is generated. ### Connecting to a remote Switch ```python import os os.environ["EDRI_SWITCH_HOST"] = "192.168.1.100" os.environ["EDRI_SWITCH_PORT"] = "8899" from edri import EDRI edri = EDRI() edri.start_connector() edri.run() ``` --- ## Shareable events Only events marked as **shareable** are forwarded through the Connector to the Switch: ```python @event(shareable=True) class CrossNodeEvent(Event): """This event can leave the local appliance.""" data: str response: CrossNodeResponse ``` An event without `shareable=True` will never leave the local Router, even if a Connector is active. --- ## Multiple Connectors An EDRI instance may have at most one active Connector. For multi-node setups, the topology is typically: ```mermaid flowchart TD SW[Switch] SW <--> E1["EDRI #1
Router + Connector"] SW <--> E2["EDRI #2
Router + Connector"] SW <--> E3["EDRI #3
Router + Connector"] ``` Each router connects through its own Connector to the same Switch. The Switch handles message distribution. --- ## Demand (subscription) propagation The Connector implements **demand propagation** — it tells the Switch which event types the local Router is interested in: 1. When a Manager subscribes to an event type, the Router records it and notifies the Connector (a `SubscribedNew` event). 2. The Connector's main loop detects the change and sends the updated demand list to the Switch (`Socket.send_demands`). 3. The Switch forwards the demands to all other connected routers. 4. Remote routers use this information to decide which events to send. This means: * Remote routers only send events that the local router actually wants. * If no local Manager subscribes to `SomeEvent`, that event type is never forwarded from the Switch. * Demand changes are propagated automatically when Managers start/restart. --- ## Resilience and reconnection ```mermaid stateDiagram-v2 [*] --> IDLE IDLE --> CONNECTING: start() CONNECTING --> ACTIVE: connected CONNECTING --> RETRY: connection failure ACTIVE --> RETRY: connection loss RETRY --> CONNECTING: linear backoff
(+5 s per attempt,
capped at EDRI_CACHE_TIMEOUT) ``` * On connection failure, the Connector retries with **linear** backoff — the delay starts at 5 s and grows by 5 s per attempt, capped at `EDRI_CACHE_TIMEOUT`. * During retry, events destined for the Switch are queued locally. * When reconnected, queued events are sent before new ones. * The Router cache (`EDRI_CACHE_TIMEOUT`) keeps additional backup. --- ## Security The Connector authenticates to the Switch with a pre-shared key and then exchanges pickle-serialized frames over a raw TCP socket. This has the following implications: * **Pre-shared key** — `EDRI_SWITCH_SECRET` (or `EDRI_SWITCH_SECRET_FILE`) must match the Switch's. The mutual handshake proves both sides hold the key and binds the Router UUID into the transcript, so an outsider cannot connect or impersonate a router. The Connector refuses to dial a non-loopback Switch without a secret. Prefer `EDRI_SWITCH_SECRET_FILE` with a mounted secret over an inline environment value, which is readable through `/proc`, `docker inspect`, and pod specs. * **Shared key, not per-peer** — every node in a cluster holds the same key, so authentication is against outsiders, not between members. Restrict which peers are given the key. * **No transport encryption** — frames travel over TCP in the clear. The per-frame MAC authenticates but does not conceal; a network-level attacker can read every event payload. If payloads contain credentials or personal data, use a VPN, mTLS proxy, or service mesh. * **Pickle is not safe against a compromised peer** — an authenticated peer can still send arbitrary pickle, which the other side unpickles. * **Event payloads may appear in logs** — error-level logging includes serialized event contents. Configure log levels appropriately in production. ### Recommended network setup | Environment | Recommended isolation | |-------------|----------------------| | Development | Same machine or private Docker network | | Production | Private subnet + firewall (allow only specific peer IPs) | | Kubernetes | NetworkPolicy restricting ingress to Switch port 8899 | | Multi-datacenter | VPN or WireGuard tunnel between sites | ## Pickle compatibility Both sides must run compatible Python versions, and event class definitions must match. If you change event classes, restart all connected instances to avoid deserialization errors. --- ## Gotchas & Notes * The Connector only forwards **shareable** events (`@event(shareable=True)`). * The Connector must be able to resolve `EDRI_SWITCH_HOST`. Use IP addresses for production to avoid DNS issues. * The Switch must be running and reachable before the Connector starts. * Each EDRI instance can have at most one Connector. --- ## See also * {doc}`/switch` — the Switch server that Connectors connect to * {doc}`/router` — local event dispatching, shareable events * {doc}`/event` — the `shareable` flag on events * {doc}`/env` — environment variables for Switch and cache * {doc}`/deployment` — deploying distributed EDRI