Per-Interface Features
Without network.interfaces, every feature except egress IDS runs on every interface
Synapse picks up from network.iface or network.ifaces; egress IDS has its own list,
ids.egress_ifaces, which does nothing unless SYNAPSE_EGRESS_IDS is set in the
environment as well. With the block, you say which links get which features.
The block is authoritative. Each entry names the links it matches and the features they get, and a link no entry matches is attached by nothing the block governs — no firewall, no IDS, no smart firewall, no XDP program at all. That is the point of the design rather than a side effect of it: an operator who forgets to list the management interface must not find a deny rule on it.
Two settings outside the block can still put a program on a link it never names, and one of them can drop traffic. Both are in What the block does not cover, which anyone relying on the paragraph above should read.
Because the block is the complete list, adding it to a working config narrows what
Synapse attaches to. A link you do not list stops being filtered. There is no "everything
else" entry: pattern: "*" does match every link, but then any second entry matches one
of those links too and the file is refused for the double match. To record a link and
leave it alone, give it features: [].
This page describes Synapse 0.8.8. The block is Linux-only — see
Linux only — and changes to it take effect at startup, so an edit plus a
SIGHUP does not apply it. Restart the agent.
The shape
network:
interfaces:
wan:
match: { name: eth1 }
features: [firewall, ids, smart_firewall, fingerprint]
lan:
match: { pattern: "enp3s*" }
features: [firewall]
oob:
match: { mac: "52:54:00:12:34:56" }
features: [] # listed, nothing enabled
The keys under interfaces: are logical names you choose — wan, lan, oob above.
They appear in the startup log and in error messages, and they are not interface names.
Each entry takes three keys:
| Key | Required | Default | What it does |
|---|---|---|---|
match | yes | — | which links this entry applies to: exactly one of name, mac or pattern |
features | no | [] | the features enabled on the matched links |
require_native | no | false | refuse to attach rather than fall back to generic XDP |
A commented-out example ships in config/config.yaml, under the network: section.
Matching a link
match takes exactly one of three keys. A match with none, or with two, fails the
load.
| Key | Form | Matches |
|---|---|---|
name | eth1 | the link with exactly this kernel name |
mac | 52:54:00:12:34:56 | the link with this hardware address, case-insensitive |
pattern | "enp3s*" | every link whose name matches this glob — *, ? and […] |
Interface names are not stable: udev renames them, predictable naming changes them
between distributions, and swapping a NIC moves them. That is why mac and pattern
exist, and why a config meant to survive hardware changes should prefer them to name.
A pattern entry covers every link it matches, and they all get that entry's
features. A link reporting no hardware address, or an all-zero one — point-to-point and
tunnel links do — never matches a mac.
Three matching rules decide whether the file loads:
- a
nameormacthat matches no link on this host fails the load. It would otherwise silently protect nothing. - a
patternthat matches nothing is accepted in silence. A link matching it may appear later, so it is deliberately not an error — but nothing is printed either, and a pattern that matches nothing looks exactly like one that matches everything you meant. The resolve line below is the only way to tell them apart. - a link matched by two entries fails the load, naming both. Each link belongs to one entry, so that its feature set is unambiguous.
The features
Five features can be scoped to a link. Four of them have a global switch that is
checked: listing one whose switch is off is refused rather than accepted, since it would
otherwise parse, log as enabled and do nothing. fingerprint is the exception — it is
checked against nothing, and what it governs is narrower than its name suggests.
| Feature | What it enables on the link | Global switch | If the switch is off |
|---|---|---|---|
firewall | firewall rules and access-rule IP filtering | firewall.mode | load fails when the mode is none |
ids | IDS on ingress traffic | ids.enabled | load fails |
ids_egress | IDS on egress traffic, via TC-egress capture | ids.enabled, and a build carrying the egress-ids feature | load fails; a build without it fails at startup |
smart_firewall | the smart firewall's pre-block and reactor | firewall.amygdala.enabled | load fails |
fingerprint | which link the TC collectors (JA4S, JA4TS, JA4L, JA4SSH) and the single-interface capture run on | capture.enabled | nothing is said, and those collectors do not run — but read the caution below before concluding the link yields no fingerprints |
fingerprint does not mean "the fingerprints from this link"Two things follow from it not being checked. The first is the ordinary one:
features: [fingerprint] with capture.enabled: false loads cleanly and logs the
feature as enabled, while the TC collectors it selects are skipped — as they are during
a PCAP replay.
The second matters more, because it runs the other way. JA4T does not come from this
feature at all. Those fingerprints are read from the XDP program on every attached
link and written out whenever logging.eventbridge_log.enabled is true, which is its
default. So a link listed features: [firewall] with no fingerprint still produces
JA4T, and turning capture.enabled off does not stop it. The switch for that is
logging.eventbridge_log.enabled.
The shipped config.yaml comment names firewall.mode, ids.enabled and
firewall.amygdala.enabled — neither of the two switches above.
Two more features exist in the schema and are refused under an interface, because each runs host-wide and listing it on one link would read as "only here":
| Feature | Why it cannot be scoped | Where it is controlled |
|---|---|---|
ssl_uprobe | it hooks a library, not a link | capture.enabled |
tls_offload | it sits behind the proxy, after TLS | the IDS post-TLS offload settings |
Two more rules on the set itself: a feature listed twice fails the load, and
smart_firewall requires firewall on the same interface — it enforces through the
firewall datapath and registers the interface for rules on its own, so without firewall
it would install rules on an interface that asked for none.
Listing smart_firewall on two or more fails at startup, naming them. If
firewall.amygdala.iface is also set and names a different link, that fails too — drop
the key and let the block decide. firewall.amygdala.backend must be auto or xdp;
the others are host-wide.
What it replaces, and what it refuses
The block does not merge with the older interface keys, it replaces them. Three combinations fail the load:
| Also set | Why it is refused |
|---|---|
network.iface (other than auto) or network.ifaces, including --iface, --ifaces, NETWORK_IFACE and NETWORK_IFACES | two lists of interfaces, one of them authoritative. List every interface under network.interfaces instead |
ids.egress_ifaces | superseded by the ids_egress feature, which names the links in one place |
firewall.mode: nftables or iptables | those backends install host-wide chains and cannot yet be limited to the listed links, so only auto and xdp are accepted beside the block. (none loads, as long as no entry lists firewall) |
require_native
An interface whose driver has no native XDP support can still be attached in generic
(SKB) mode, which works but costs performance. require_native: true refuses to attach
to such a link rather than running there degraded. It is per-entry, so one uplink can
insist on native XDP while a secondary link accepts generic.
Know what the refusal costs before setting it. Synapse logs an error, skips that interface and keeps running — it is not a startup failure. The link then gets no firewall, no ingress IDS and no XDP program, which is the protection it was listed for. On an uplink that is the difference between a slower uplink and an unprotected one.
It is not quite an unlisted link, though. The TC-attached collectors go by what the
configuration planned rather than by what actually attached, so they can still land on
a refused link: the JA4 collectors when no interface attached XDP at all, and
ids_egress always.
Declaration order and the limit
The block is read in declaration order rather than sorted, and each entry is assigned a slot index from its position. A configuration may declare at most 64 logical interfaces, and declaring more fails the load with the count.
The slot is designed to be what scoped kernel rules key on, and the module describes it that way, but at 0.8.8 nothing in the datapath reads it — the scoped IDS and firewall paths key on the kernel's ifindex instead. So reordering the block is currently inert. The 64-entry limit is enforced regardless.
What a bad file does
| The file says | What happens |
|---|---|
match: {} | load fails: match needs one of name, mac or pattern |
two of name/mac/pattern | load fails: match takes exactly one of the three |
an unknown key under match, such as ifname | load fails on the unknown field |
match: { name: "" } or pattern: "" | load fails; neither may be empty |
| a malformed MAC — wrong length, non-hex, seven groups | load fails naming the expected aa:bb:cc:dd:ee:ff form |
an invalid glob, such as [ | load fails naming the pattern |
features: [waf], or any name not in the five | load fails on the unknown feature |
features: [ssl_uprobe] or [tls_offload] | load fails: host-wide, and the message names where it is controlled |
features: [firewall, firewall] | load fails: listed twice |
features: [smart_firewall] without firewall | load fails |
| an empty logical name | load fails |
| more than 64 entries | load fails with the count |
name or mac matching no link on this host | load fails |
pattern matching no link | accepted, with no startup line at all |
| one link matched by two entries | load fails, naming both entries |
the block together with iface/ifaces, ids.egress_ifaces, or a netfilter firewall.mode | load fails, as above |
smart_firewall on two interfaces | startup fails, naming them |
ids_egress on a build without the egress-ids feature | startup fails |
the block on a host with no /sys/class/net | load fails: cannot enumerate /sys/class/net |
Every link that does resolve is logged at startup, as
interface 'eth1' -> network.interfaces.wan (features: firewall, ids). That line is the
quickest check that a pattern matched what you meant.
What stays quiet
These do not fail the load or the start, and each has surprised someone:
fingerprintwithoutcapture.enabled, and JA4T withoutfingerprint, as above.- Packet capture may not start at all. Under the block, a single-interface capture
runs on a
fingerprintinterface or not at all — never on a default such asautooreth0, either of which could be a link the config deliberately left out. Ifcapture.interfacenames a link with nofingerprintfeature, or no entry hasfingerprint, capture is skipped and the reason is logged at info level aspacket capture not started: …— and only whencapture.enabledis true. Left unset orauto, capture takes the firstfingerprintinterface once physical links have been preferred over virtual ones. - Only one
idslink is covered if the IDS falls back to AF_PACKET. When the XDP IDS is unavailable, the fallback binds to the firstidsinterface and logs an error naming the ones it is not covering. Scopingidsto three NICs does not mean three are inspected on that path. - An unavailable XDP is a degraded start, not a failure. With the block set,
firewall.mode: autoand no XDP program loaded, Synapse logs an error and does not fall back to nftables or iptables — those are the host-wide backends the block cannot scope. It continues with no kernel firewall at all.
What the block does not cover
The guarantee at the top of this page is about the features the block governs. Three things reach an interface by another route, and the first two can reach a link the block never names:
capture.observe_interfacesandcapture.observe_egress_interfaces. These are read straight from the configuration; the block is not consulted. The named link gets a BPF program attached, or a capture socket opened, depending oncapture.observe_mode. And despite the name it is not purely passive: on a smart-firewalldropverdict for an overlay flow it has resolved — pod-to-pod identity, or a destination matched from a snooped domain — the observer drops the packet. A link that appears only here can therefore carry a program and lose traffic without being innetwork.interfacesat all.firewall.urpf.interfaces. Its keys are kernel interface names and are never cross-checked against the block. uRPF is applied only to links that have an XDP dispatcher, which is every link the block attaches — so it reaches a link whose only feature isfingerprint, with nofirewallrequested, at the globalfirewall.urpf.mode, because a name absent from the map falls back to it. A key naming a link the block does not list is simply never applied, and no line at any level says so — not even at debug.- A
require_nativerefusal, which leaves the named link without the protection it was listed for while the TC collectors may still use it — see require_native.
If the management interface must stay untouched, leaving it out of network.interfaces
is necessary and not sufficient: check that it is named in neither capture.observe_*
nor firewall.urpf.interfaces.
veth, bridges and auto-detection
network.iface: auto enumerates the host's links and drops the ones that are virtual or
not up — loopback, veth, bridges and CNI-style prefixes among them. Under
network.interfaces that detection never runs, so none of its exclusions apply: the
block's matchers are the only thing that selects a link.
One exclusion survives, on the attach path rather than in detection. A veth link is
skipped unless firewall.attach_veth is set — or the block matched it, which is read as
the more specific request and attaches it. Matching by pattern counts as matching it.
ids_egress gets no XDP programThe XDP entry program is attached for firewall, ids, smart_firewall and
fingerprint. TC-egress capture does not need it, so an entry whose only feature is
ids_egress is in the plan and carries no XDP.
Linux only
network.interfaces resolves its matchers by reading /sys/class/net, and that read is
not conditional on the platform. On Windows the configuration therefore fails to load
with network.interfaces: cannot enumerate /sys/class/net. Use network.iface or
network.ifaces there; the shipped config_windows.yaml does.
(The exception is synapse --terminal run standalone, which ignores a configuration it
cannot load and starts on defaults. Every other start path propagates the error.)
The features themselves need a Linux build with the bpf feature, which is on by default.
ids_egress cannot be used on a released packageIt needs the egress-ids build feature, and that feature exists only on the inner
synapse-app crate. There is no root-level one, so a plain --features egress-ids is
rejected and only the dependency form --features synapse-app/egress-ids enables it —
and no packaging job passes either. On every published build, features: [ids_egress]
therefore makes the agent refuse to start. Treat it as unavailable unless you build
Synapse yourself.
A worked example
A host with a public uplink, two internal NICs and a management port, where the management port must never be touched:
network:
interfaces:
uplink:
match: { mac: "52:54:00:12:34:56" } # survives a NIC swap
features: [firewall, ids, smart_firewall, fingerprint]
require_native: true # refuse generic XDP — see the caveat above
internal:
match: { pattern: "enp3s*" }
features: [firewall]
firewall:
mode: auto
amygdala:
enabled: true
ids:
enabled: true
capture:
enabled: true
The management port appears nowhere, which is most of what keeps it clear — the rest is
checking it is named in neither capture.observe_* nor firewall.urpf.interfaces.
Note what the file has to get right for the features above to run: firewall.mode is
auto rather than a netfilter backend, and ids.enabled and
firewall.amygdala.enabled are true because ids and smart_firewall are listed —
those three are checked, and the file would be refused without them. capture.enabled is
true because fingerprint is listed, and that one is not checked: with it false the
file still loads and the TC collectors simply do not run.