Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 24 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,31 +71,37 @@ sudo cocoon-net init \
--pool-size 140
```

#### VM egress isolation (`--drop-internal-access`, `--drop-cidr`)
#### VM isolation

Both flags (accepted by `init` and `adopt`) block VM-originated traffic and are
persisted to `pool.json`, reapplied by the daemon as `FORWARD` DROP rules at the
head of the chain so they win over the default accept rules. Return traffic and
internet egress are unaffected.
**Same-node VM-to-VM** and **anti-spoofing** are handled at L2 by the CNI
`bridge` plugin, baked into the generated conflist (`writeCNIConflist`):

- `--drop-internal-access` blocks **VM-to-VM** traffic within the cocoon subnet.
cocoon-net already knows the subnet from `--subnet`, so there is no CIDR to
restate.
- `--drop-cidr` (repeatable) blocks additional **external** destination ranges,
e.g. internal/VPC management networks.
- `portIsolation: true` — sets the kernel `BR_ISOLATED` flag on every VM's veth,
so same-bridge (same-node) VMs cannot exchange any frames (unicast, ARP,
broadcast) with each other, while still reaching the bridge gateway and
routing out. Pure L2 — no `br_netfilter`, no conntrack.
- `macspoofchk: true` — nftables (bridge family) rule pinning each veth's source
MAC to its assigned address; blocks MAC spoofing / FDB hijack. Stateless.

Same-node VMs share `cni0` and are switched at L2, which bypasses iptables
unless `bridge-nf-call-iptables=1`. When either flag is set, node setup loads
`br_netfilter` and enables that toggle, **failing closed** if it cannot — so the
isolation is never silently a no-op. The DROP rules are tagged `cocoon-net-drop`,
so `teardown` removes exactly them.
**Cross-node VM-to-VM** and **external ranges** are blocked at L3 via `--drop-cidr`:

- `--drop-cidr` (repeatable, `init`/`adopt`) adds `FORWARD -i cni0 -d <CIDR> -j DROP`
at the head of the chain, persisted to `pool.json` and reapplied by the daemon.
Cross-node VM traffic is L3-routed so it traverses `FORWARD` naturally — **no
`br_netfilter` needed**. Pass the fleet VM supernet (e.g. `--drop-cidr
172.22.0.0/16`) to block VM-to-VM across all nodes, plus any management ranges.
- `--drop-internal-access` only adds a `FORWARD` DROP for the node's own
`--subnet`; since same-node VM-to-VM is L2 (off `FORWARD`) and now covered by
`portIsolation`, this flag is largely superseded — prefer `--drop-cidr`.

Return traffic and internet egress are unaffected. DROP rules are tagged
`cocoon-net-drop`, so `teardown` removes exactly them.

```bash
sudo cocoon-net init \
--platform gke --node-name cocoon-pool \
--subnet 172.20.100.0/24 --pool-size 140 \
--drop-internal-access \
--drop-cidr 10.0.0.0/8
--subnet 172.22.0.0/24 --pool-size 140 \
--drop-cidr 172.22.0.0/16
```

> Note: traffic to the node's own address (e.g. a kubelet bound on the cni0
Expand Down
57 changes: 5 additions & 52 deletions node/iptables_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"context"
"fmt"
"net"
"os"
"os/exec"
"slices"
"strings"

Expand Down Expand Up @@ -75,9 +73,9 @@ func ruleDest(fields []string) (string, bool) {
return "", false
}

// setupIPTables installs the FORWARD rules between secondary NICs and the
// bridge, a NAT MASQUERADE rule for outbound VM traffic, and egress DROP rules
// isolating VMs from their own subnet (dropInternal) and from dropCIDRs.
// setupIPTables installs FORWARD rules between secondary NICs and the bridge, a NAT
// MASQUERADE for outbound VM traffic, and egress DROP rules blocking VM traffic to
// dropCIDRs: L3-routed cross-node/external ranges that need no br_netfilter.
func setupIPTables(ctx context.Context, subnetCIDR string, secondaryNICs []string, dropInternal bool, dropCIDRs []string) error {
logger := log.WithFunc("node.setupIPTables")

Expand Down Expand Up @@ -111,14 +109,8 @@ func setupIPTables(ctx context.Context, subnetCIDR string, secondaryNICs []strin
}

if len(dropTargets) > 0 {
// Same-node VM-to-VM is L2-switched on cni0 and bypasses iptables
// unless bridge-nf-call-iptables is on; enable it (fail closed) first.
if err := ensureBridgeNFCall(ctx); err != nil {
return fmt.Errorf("enable bridge netfilter: %w", err)
}

// Insert at FORWARD's head so DROP precedes the ACCEPT rules; the -i
// match spares return traffic, and VM-to-gateway is INPUT, not FORWARD.
// Insert at FORWARD's head so DROP precedes the ACCEPT rules; the -i match
// spares return traffic, VM-to-gateway is INPUT not FORWARD.
for _, dst := range dropTargets {
if err := iptInsert(ipt, "filter", "FORWARD", "-i", BridgeName, "-d", dst, "-m", "comment", "--comment", dropRuleComment, "-j", "DROP"); err != nil {
return fmt.Errorf("iptables FORWARD drop %s: %w", dst, err)
Expand Down Expand Up @@ -161,45 +153,6 @@ func resolveDropTargets(subnetCIDR string, dropInternal bool, dropCIDRs []string
return out, nil
}

// ensureBridgeNFCall loads br_netfilter and turns on bridge-nf-call-iptables so
// same-bridge (same-node VM-to-VM) frames traverse iptables. It verifies the
// toggle stuck, failing closed rather than leaving DROP rules the kernel would
// quietly skip for L2-switched traffic.
func ensureBridgeNFCall(ctx context.Context) error {
logger := log.WithFunc("node.ensureBridgeNFCall")

// modprobe is the authoritative loader for a kernel module and its deps,
// with no stdlib equivalent; it is a no-op when br_netfilter is loaded.
logger.Debug(ctx, "running modprobe br_netfilter (external binary)")
if out, err := exec.CommandContext(ctx, "modprobe", "br_netfilter").CombinedOutput(); err != nil {
return fmt.Errorf("modprobe br_netfilter (%s): %w", strings.TrimSpace(string(out)), err)
}

const key = "net.bridge.bridge-nf-call-iptables"
if err := writeSysctl(key, "1"); err != nil {
return fmt.Errorf("write sysctl %s=1: %w", key, err)
}
got, err := readSysctl(key)
if err != nil {
return fmt.Errorf("read sysctl %s: %w", key, err)
}
if got != "1" {
return fmt.Errorf("sysctl %s is %q after write, want 1", key, got)
}

logger.Info(ctx, "br_netfilter loaded, bridge-nf-call-iptables=1")
return nil
}

// readSysctl reads a sysctl value via /proc/sys, trimming surrounding whitespace.
func readSysctl(key string) (string, error) {
b, err := os.ReadFile(sysctlPath(key)) //nolint:gosec // sysctl read of a known proc path
if err != nil {
return "", err
}
return strings.TrimSpace(string(b)), nil
}

// iptEnsure appends an iptables rule if it does not already exist.
func iptEnsure(ipt *iptables.IPTables, table, chain string, args ...string) error {
exists, err := ipt.Exists(table, chain, args...)
Expand Down
18 changes: 10 additions & 8 deletions node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ type Config struct {
// SkipIPTables omits the iptables FORWARD + NAT MASQUERADE rules.
SkipIPTables bool

// DropInternalAccess blocks VM-to-VM traffic within SubnetCIDR (cocoon's
// own range). Enforced as a FORWARD DROP; node setup enables the
// bridge-nf-call-iptables it relies on. Ignored when SkipIPTables is set.
// DropInternalAccess adds a FORWARD DROP for SubnetCIDR. Same-node VM-to-VM is
// isolated at L2 by portIsolation instead; use DropCIDRs for cross-node.
// Ignored when SkipIPTables is set.
DropInternalAccess bool

// DropCIDRs lists additional external destination CIDRs VM traffic is
Expand Down Expand Up @@ -83,11 +83,13 @@ func writeCNIConflist(ctx context.Context) error {
"name": "cocoon-dhcp",
"plugins": []map[string]any{
{
"type": "bridge",
"bridge": BridgeName,
"isGateway": false,
"ipMasq": false,
"ipam": map[string]any{},
"type": "bridge",
"bridge": BridgeName,
"isGateway": false,
"ipMasq": false,
"portIsolation": true, // block same-node VM-to-VM at L2 (BR_ISOLATED per veth)
"macspoofchk": true, // pin veth source MAC (anti-spoof)
"ipam": map[string]any{},
},
},
}
Expand Down