egress anti-spoof: drop by default again
This commit is contained in:
parent
8c4541400f
commit
60aede950b
4 changed files with 33 additions and 29 deletions
|
|
@ -803,7 +803,7 @@ Every container on the agent bridge is root in its own network namespace and
|
||||||
shares one L2 segment with the others, so it can send with another container's
|
shares one L2 segment with the others, so it can send with another container's
|
||||||
MAC or address. `services.hyperhive.network.egress.antiSpoof` sets what the
|
MAC or address. `services.hyperhive.network.egress.antiSpoof` sets what the
|
||||||
`bridge hive_egress` table (`nix/host-modules/hive-egress.nix`) does about it. It
|
`bridge hive_egress` table (`nix/host-modules/hive-egress.nix`) does about it. It
|
||||||
takes `drop` or `log` (the default, until the renewal fix has run on a live hive). Two tables, not one: the bridge carries traffic between two
|
takes `drop` (the default) or `log`. Two tables, not one: the bridge carries traffic between two
|
||||||
ports, and the `inet` hooks never see it as coming from a port. Only a
|
ports, and the `inet` hooks never see it as coming from a port. Only a
|
||||||
`bridge`-family hook at `prerouting` sees which veth a frame came in on, so the
|
`bridge`-family hook at `prerouting` sees which veth a frame came in on, so the
|
||||||
source check lives in that table and the host-side filtering stays in
|
source check lives in that table and the host-side filtering stays in
|
||||||
|
|
@ -859,12 +859,14 @@ source check lives in that table and the host-side filtering stays in
|
||||||
sent; the port names the sender, because a bound port sends only its own MAC.
|
sent; the port names the sender, because a bound port sends only its own MAC.
|
||||||
The counters in `nft list chain bridge hive_egress spoof` count every refused
|
The counters in `nft list chain bridge hive_egress spoof` count every refused
|
||||||
frame.
|
frame.
|
||||||
- **Why `log` is the default.** A container that sends from its leased address
|
- **Reading the log.** A container that sends from its leased address and its
|
||||||
and its own MAC is never refused by `drop`, but only while the pair stays in
|
own MAC never gets a line, so a line is a frame to look at. A container that
|
||||||
the table. Until a live hive has run through many lease renewals with
|
has no lease yet logs a few lines in its first seconds: DNS queries from
|
||||||
`script-on-renewal` set without a `hive-egress-spoof` line for a legitimate
|
`0.0.0.0`, and the DHCP replies and ICMP redirects its own stack reflects
|
||||||
container, `log` keeps the lines and the counters and blocks nothing. `drop`
|
while the address isn't configured. They stop at the first lease, and `drop`
|
||||||
is what the table is for: without it the egress log can't tell agents apart.
|
loses nothing by refusing them. Set `antiSpoof = "log"` to let the frames
|
||||||
|
through while you check whether a refusal is a false positive. `drop` is what
|
||||||
|
the table is for: without it the egress log can't tell agents apart.
|
||||||
A binding that failed shows as `hive-egress-pin` errors in the journal and
|
A binding that failed shows as `hive-egress-pin` errors in the journal and
|
||||||
`IN=` lines for that port.
|
`IN=` lines for that port.
|
||||||
- **What it doesn't cover.** A container can still send from any IPv6
|
- **What it doesn't cover.** A container can still send from any IPv6
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,7 @@ in
|
||||||
"drop"
|
"drop"
|
||||||
"log"
|
"log"
|
||||||
];
|
];
|
||||||
default = "log";
|
default = "drop";
|
||||||
description = ''
|
description = ''
|
||||||
What the `bridge hive_egress` table does with a frame whose source is not
|
What the `bridge hive_egress` table does with a frame whose source is not
|
||||||
the sending container's own. Every port of the agent bridge is checked:
|
the sending container's own. Every port of the agent bridge is checked:
|
||||||
|
|
@ -345,15 +345,13 @@ in
|
||||||
`0.0.0.0` whose `chaddr` is the port's MAC, ARP probes, and IPv6 link-local
|
`0.0.0.0` whose `chaddr` is the port's MAC, ARP probes, and IPv6 link-local
|
||||||
sources (and `::` in neighbour discovery and listener reports).
|
sources (and `::` in neighbour discovery and listener reports).
|
||||||
|
|
||||||
- `drop`: the frame is dropped and logged with the prefix
|
- `drop` (the default): the frame is dropped and logged with the prefix
|
||||||
`hive-egress-spoof `, rate limited per port. A packet the per-port limit
|
`hive-egress-spoof `, rate limited per port. A packet the per-port limit
|
||||||
cannot log gets a line with the prefix `hive-egress-spoof-unmetered `
|
cannot log gets a line with the prefix `hive-egress-spoof-unmetered `
|
||||||
from a limit shared by all ports. A container sending from its leased
|
from a limit shared by all ports. A container sending from its leased
|
||||||
address is never affected, so nothing legitimate breaks.
|
address is never affected, so nothing legitimate breaks.
|
||||||
- `log` (the default): the same lines and counters, and the frame goes
|
- `log`: the same lines and counters, and the frame goes through. For
|
||||||
through. For finding out why a legitimate container is refused on a
|
finding out whether a suspected false positive of `drop` is one.
|
||||||
live hive. The default goes back to `drop` once the renewal fix
|
|
||||||
(dnsmasq runs the lease script on a renewal) is proven live.
|
|
||||||
|
|
||||||
With `log`, the identity squid takes from the source of an intercepted
|
With `log`, the identity squid takes from the source of an intercepted
|
||||||
flow (`egress.intercept`) can be spoofed: use it to diagnose, not to run.
|
flow (`egress.intercept`) can be spoofed: use it to diagnose, not to run.
|
||||||
|
|
|
||||||
|
|
@ -73,12 +73,12 @@ let
|
||||||
# The two settings of `egress.forward` the ordinary host (`log`) does not
|
# The two settings of `egress.forward` the ordinary host (`log`) does not
|
||||||
# render, for ../host-modules/hive-egress.nix: `accept` leaves the table
|
# render, for ../host-modules/hive-egress.nix: `accept` leaves the table
|
||||||
# without a forward chain, `reject` renders the chain that refuses. The
|
# without a forward chain, `reject` renders the chain that refuses. The
|
||||||
# `accept` host also sets `egress.antiSpoof = "drop"`, the one setting the
|
# `accept` host also sets `egress.antiSpoof = "log"`, the one setting the
|
||||||
# ordinary host (`log`) does not render.
|
# ordinary host (`drop`) does not render.
|
||||||
egressForwardAccept = hive {
|
egressForwardAccept = hive {
|
||||||
network.enable = true;
|
network.enable = true;
|
||||||
network.egress.forward = "accept";
|
network.egress.forward = "accept";
|
||||||
network.egress.antiSpoof = "drop";
|
network.egress.antiSpoof = "log";
|
||||||
};
|
};
|
||||||
egressForwardReject = hiveWithController {
|
egressForwardReject = hiveWithController {
|
||||||
network.egress.forward = "reject";
|
network.egress.forward = "reject";
|
||||||
|
|
@ -1587,17 +1587,17 @@ let
|
||||||
# its own, and logs it. The ruleset is read as text: the spoof chain
|
# its own, and logs it. The ruleset is read as text: the spoof chain
|
||||||
# ends every path in `drop`, and the accepts that remain are the ones that
|
# ends every path in `drop`, and the accepts that remain are the ones that
|
||||||
# let the leased source through, which is the presence arm.
|
# let the leased source through, which is the presence arm.
|
||||||
name = "egress.antiSpoof drop renders the bridge table that drops frames whose source is not the port's own";
|
name = "egress.antiSpoof defaults to drop and renders the bridge table that drops frames whose source is not the port's own";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
text = egressForwardAccept.services.hyperhive.network.egress.ruleset;
|
text = bare.services.hyperhive.network.egress.ruleset;
|
||||||
bridgeText = lib.last (lib.splitString "table bridge hive_egress" text);
|
bridgeText = lib.last (lib.splitString "table bridge hive_egress" text);
|
||||||
section =
|
section =
|
||||||
name: lib.head (lib.splitString "chain" (lib.last (lib.splitString "chain ${name} {" bridgeText)));
|
name: lib.head (lib.splitString "chain" (lib.last (lib.splitString "chain ${name} {" bridgeText)));
|
||||||
spoof = lib.filter (l: l != "" && l != "}") (map lib.trim (lib.splitString "\n" (section "spoof")));
|
spoof = lib.filter (l: l != "" && l != "}") (map lib.trim (lib.splitString "\n" (section "spoof")));
|
||||||
ip4 = section "l3_v4";
|
ip4 = section "l3_v4";
|
||||||
in
|
in
|
||||||
egressForwardAccept.services.hyperhive.network.egress.antiSpoof == "drop"
|
bare.services.hyperhive.network.egress.antiSpoof == "drop"
|
||||||
&& lib.hasInfix "hook prerouting priority -300" bridgeText
|
&& lib.hasInfix "hook prerouting priority -300" bridgeText
|
||||||
&& lib.hasInfix ''meta ibrname != "hive-br0" accept'' bridgeText
|
&& lib.hasInfix ''meta ibrname != "hive-br0" accept'' bridgeText
|
||||||
&& lib.hasInfix "iifname . ether saddr != @port_mac goto spoof" bridgeText
|
&& lib.hasInfix "iifname . ether saddr != @port_mac goto spoof" bridgeText
|
||||||
|
|
@ -1614,10 +1614,10 @@ let
|
||||||
&& lib.hasInfix "goto spoof" ip4;
|
&& lib.hasInfix "goto spoof" ip4;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# `log`, the default, renders the same chains with `accept` where `drop`
|
# `log` renders the same chains with `accept` where `drop`
|
||||||
# was: the lines and counters stay, the frame goes through. The `accept`
|
# was: the lines and counters stay, the frame goes through. The `accept`
|
||||||
# host sets `drop` and is the control that the two differ.
|
# host sets `log` and `bare` is the control that the two differ.
|
||||||
name = "egress.antiSpoof defaults to log and renders the spoof chain with accept and no drop";
|
name = "egress.antiSpoof log renders the spoof chain with accept and no drop";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
textOf = m: m.services.hyperhive.network.egress.ruleset;
|
textOf = m: m.services.hyperhive.network.egress.ruleset;
|
||||||
|
|
@ -1629,14 +1629,14 @@ let
|
||||||
lib.filter (l: l != "" && l != "}") (
|
lib.filter (l: l != "" && l != "}") (
|
||||||
map lib.trim (lib.splitString "\n" (lib.head (lib.splitString "chain" t)))
|
map lib.trim (lib.splitString "\n" (lib.head (lib.splitString "chain" t)))
|
||||||
);
|
);
|
||||||
logged = spoofOf bare;
|
logged = spoofOf egressForwardAccept;
|
||||||
in
|
in
|
||||||
bare.services.hyperhive.network.egress.antiSpoof == "log"
|
egressForwardAccept.services.hyperhive.network.egress.antiSpoof == "log"
|
||||||
&& lib.length logged == 3
|
&& lib.length logged == 3
|
||||||
&& lib.all (l: lib.hasSuffix " accept" l) logged
|
&& lib.all (l: lib.hasSuffix " accept" l) logged
|
||||||
&& lib.hasInfix ''log prefix "hive-egress-spoof "'' (textOf bare)
|
&& lib.hasInfix ''log prefix "hive-egress-spoof "'' (textOf egressForwardAccept)
|
||||||
&& !(lib.any (l: lib.hasSuffix " drop" l) logged)
|
&& !(lib.any (l: lib.hasSuffix " drop" l) logged)
|
||||||
&& lib.all (l: lib.hasSuffix " drop" l) (spoofOf egressForwardAccept);
|
&& lib.all (l: lib.hasSuffix " drop" l) (spoofOf bare);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# The sets are filled at run time, from four places: the unit at start and
|
# The sets are filled at run time, from four places: the unit at start and
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@
|
||||||
# stand-in bridge is one NIC, so every agent shares one port.
|
# stand-in bridge is one NIC, so every agent shares one port.
|
||||||
#
|
#
|
||||||
# What it pins:
|
# What it pins:
|
||||||
# - each port is bound to its container's MAC and each lease is in the
|
# - the default verdict is `drop`, and each container takes its first lease
|
||||||
|
# (DHCP from `0.0.0.0`, the reply, the first ARP and IP traffic) with it in
|
||||||
|
# place; each port is bound to its container's MAC and each lease is in the
|
||||||
# table, without the test adding either;
|
# table, without the test adding either;
|
||||||
# - a container sending from its own address reaches the host and the other
|
# - a container sending from its own address reaches the host and the other
|
||||||
# container (the control for every drop below);
|
# container (the control for every drop below);
|
||||||
|
|
@ -15,7 +17,7 @@
|
||||||
# dropped, and each leaves a `hive-egress-spoof ` line that names the port;
|
# dropped, and each leaves a `hive-egress-spoof ` line that names the port;
|
||||||
# - a container that keeps talking across several renewals of a 2-minute lease
|
# - a container that keeps talking across several renewals of a 2-minute lease
|
||||||
# is never dropped, and its pair is still in `lease_v4` after the first
|
# is never dropped, and its pair is still in `lease_v4` after the first
|
||||||
# lease and its margin would have ended, with `antiSpoof = "drop"`;
|
# lease and its margin would have ended;
|
||||||
# - a DHCP renewal, a link-local IPv6 packet and a restored MAC go through;
|
# - a DHCP renewal, a link-local IPv6 packet and a restored MAC go through;
|
||||||
# - a port with no bound MAC sends nothing, and the timer binds it again;
|
# - a port with no bound MAC sends nothing, and the timer binds it again;
|
||||||
# - `antiSpoof = "log"`, reached by a reload of the unit, brings the bindings
|
# - `antiSpoof = "log"`, reached by a reload of the unit, brings the bindings
|
||||||
|
|
@ -98,7 +100,6 @@ pkgs.testers.runNixOSTest {
|
||||||
../host-modules/hive-priv.nix
|
../host-modules/hive-priv.nix
|
||||||
];
|
];
|
||||||
virtualisation.memorySize = 2048;
|
virtualisation.memorySize = 2048;
|
||||||
services.hyperhive.network.egress.antiSpoof = "drop";
|
|
||||||
networking.bridges.${bridge}.interfaces = [ ];
|
networking.bridges.${bridge}.interfaces = [ ];
|
||||||
networking.interfaces.${bridge}.ipv4.addresses = [
|
networking.interfaces.${bridge}.ipv4.addresses = [
|
||||||
{
|
{
|
||||||
|
|
@ -190,6 +191,9 @@ pkgs.testers.runNixOSTest {
|
||||||
host.wait_for_unit("multi-user.target")
|
host.wait_for_unit("multi-user.target")
|
||||||
host.wait_for_unit("dnsmasq.service")
|
host.wait_for_unit("dnsmasq.service")
|
||||||
host.wait_for_unit("hive-egress.service")
|
host.wait_for_unit("hive-egress.service")
|
||||||
|
# The default verdict, loaded before either container exists: the first
|
||||||
|
# lease of each container below is taken with the drop rule in place.
|
||||||
|
assert "counter drop" in host.succeed(f"nft list chain {TABLE} spoof"), "the default is drop"
|
||||||
for name in ("a", "b"):
|
for name in ("a", "b"):
|
||||||
host.wait_for_unit(f"container@{name}.service")
|
host.wait_for_unit(f"container@{name}.service")
|
||||||
host.wait_until_succeeds(
|
host.wait_until_succeeds(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue