EventBridge
EventBridge is the local interface into a running Synapse agent. Your own software can subscribe to what the agent is seeing, or ask it a direct question about one connection and get an answer back.
That closes a gap nothing else does. Your application knows the socket it is serving. It does not know the client's TLS fingerprint, its SNI, or how its TCP stack behaved — the agent does, and this is how you get it without implementing any capture yourself.
Two interfaces
| What it is | Reach for it when | |
|---|---|---|
| Event stream | Events pushed as they happen — HTTP requests and packets, with their fingerprints | You want a live feed to consume or display |
| Control socket | A request/response query about a specific connection | You have a connection in hand and want to know what the agent saw |
Both are local sockets on the host — a Unix domain socket on Linux and macOS, a loopback address on Windows. Neither is exposed to the network, so the interface is available to processes on the box and to nothing else.
Asking about a connection
You identify a connection by its source address, optionally narrowing with source port, destination address and port, or a fingerprint you already hold. The agent answers from its cache of recent connections.
What comes back is everything it captured for that connection:
| TLS | JA4 and JA4S, plus the raw JA4 string |
| TCP | JA4T and JA4TS, with the hash form |
| Latency | JA4L and JA4LS |
| HTTP | JA4H |
| Certificate | JA4X |
| Handshake detail | SNI, ALPN, and the negotiated TLS version |
| Context | The connection four-tuple and when it was captured |
You can ask for a summary or the full event detail.
The SNI and ALPN are worth calling out. Your application sees the request; it does not see what hostname the client asked for at the TLS layer, or what protocol it offered. Those are frequently the interesting part of a mismatch.
Try it
Both sockets are off unless you give them a path. Set them, and restart:
# /etc/synapse/config.yaml
daemon:
control_socket: "/var/run/synapse-control.sock"
event_socket: "/var/run/synapse-events.sock"
On Windows these are loopback TCP instead — 127.0.0.1:19198 for control and
127.0.0.1:19199 for events.
Ask about one connection
The control socket speaks HTTP/1.1 with a single endpoint, POST /query, so curl is
enough to test it:
curl --unix-socket /var/run/synapse-control.sock \
-X POST http://localhost/query \
-H 'Content-Type: application/json' \
-d '{"src_ip":"203.0.113.42","src_port":54321}'
{
"status": "found",
"event": {
"src_ip": "203.0.113.42",
"src_port": 54321,
"dst_ip": "10.0.0.5",
"dst_port": 443,
"ja4": "t13d311200_e8f1e7e78f70_d339722ba4af",
"ja4t": "t64320_2_1-3-8-nop_nop,sackOK,ts,nop,ws,eol_",
"ja4s": "t301600_c02bc02f002fc030_h2,http/1.1_",
"sni": "example.com",
"alpn": "h2",
"tls_version": "TLS 1.3"
}
}
Only fields actually captured for that connection appear — absent ones are omitted rather
than returned as null. A connection the cache has evicted returns 404 with
{"status":"not_found"}.
src_ip and src_port are required. Narrow the match with dst_ip, dst_port, ja4 or
ja4t when you need to.
Set "aggregate": true and drop src_port to get every entry seen under that address:
the response becomes {"status":"found_many","events":[…]}. Combine it with
expect_ja4_prefix to keep only one fingerprint family.
Enrich a request in your own handler
The point of the query interface is that your application already knows the socket it is serving, and can ask what the agent saw on it:
import http.client, json, socket
class UnixConn(http.client.HTTPConnection):
def connect(self):
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect("/var/run/synapse-control.sock")
def fingerprint(src_ip: str, src_port: int) -> dict | None:
c = UnixConn("localhost")
c.request("POST", "/query",
json.dumps({"src_ip": src_ip, "src_port": src_port}),
{"Content-Type": "application/json"})
body = json.loads(c.getresponse().read())
return body.get("event") if body.get("status") == "found" else None
# In a request handler, from the peer address you already have:
fp = fingerprint("203.0.113.42", 54321)
if fp:
log.info("client ja4=%s sni=%s alpn=%s",
fp.get("ja4"), fp.get("sni"), fp.get("alpn"))
The SNI and ALPN are the part worth having: your application sees the request, but not what hostname the client asked for at the TLS layer or which protocols it offered.
Watch the stream
The event socket needs no handshake — connect and events start arriving as newline-delimited JSON, one object per line:
socat -u UNIX-CONNECT:/var/run/synapse-events.sock - \
| jq -c 'select(.type == "Packet") | {src_ip, ja4, sni}'
{"src_ip":"203.0.113.42","ja4":"t13d311200_e8f1e7e78f70_d339722ba4af","sni":"example.com"}
{"src_ip":"198.51.100.7","ja4":"t13d1516h2_8daaf6152771_b0da82dd1658","sni":"api.example.com"}
Two event types are emitted: Packet for TCP/TLS connection fingerprints, and Http for
request fingerprints. In agent mode you will see Packet almost exclusively, because
Http needs the proxy to have parsed a request.
Up to 64 clients can be connected at once.
What you can build with it
- Enrich a request in your own handler — look up the connection you are serving and attach the client's fingerprint to your own logs or risk scoring.
- Step up authentication on an unfamiliar client by treating the fingerprint as one factor.
- Confirm during an incident that the connection you are looking at is the one you think it is, rather than inferring from timestamps.
- Build your own view on the event stream — the terminal client that ships with Synapse is itself a consumer of this interface.
Limits worth knowing
- The cache is bounded and recent. A connection old enough to have been evicted returns nothing. This is a live interface, not a history — for anything retrospective use Security Event Export.
- Local only, by design. There is no network listener to secure, and equally no remote access; anything off-host has to go through the export path instead.
- Answers are best-effort. Fields are absent rather than wrong when a view was never captured — a connection that never completed a TLS handshake has no JA4, and says so by omitting it.
See also
- Live Traffic View — the terminal client built on this interface
- Security Event Export — the durable, off-host counterpart
- JA4+ — what each fingerprint in the answer means
- Configuration — socket paths and enablement