Add bridge provisioning for networks (root-only)
Commit 3/6 of the network isolation feature (docs/networking-design.md).
network_bridge.{h,cpp}: ensure_network_provisioned() stands up a
network's real bridge -- idempotent (checks `ip link show` first), so
this doubles as the reboot-reconciliation mechanism, no separate code
path. extern's bridge lives in the host's own root namespace with
net.ipv4.ip_forward + an iptables MASQUERADE rule for the subnet (+
IPv6 equivalents if enabled); intern's bridge lives inside its own
dedicated persistent namespace (persistent_netns.h) with no forwarding
or NAT at all -- a structural isolation boundary, not just a missing
rule. Bridge names are a deterministic FNV-1a hash of the network name
(not std::hash, whose value isn't guaranteed stable across a rebuild),
kept under Linux's 15-char interface name limit.
network_subnet.{h,cpp} gains ipv4_gateway_address()/
ipv6_gateway_address() (mask a CIDR to its network address, +1 for the
bridge's own ".1"). create_network_command() now calls
ensure_network_provisioned() before persisting the config entry -- a
network that fails to provision isn't saved.
Verified end-to-end as root (via a scoped doas rule): a real extern
network's bridge/gateway IPs/forwarding/NAT rule, and a real intern
network's isolated bridge with neither, both came up correctly; test
networks removed via --delete-network afterward.
This commit is contained in:
@@ -135,19 +135,28 @@ Source layout (all under `src/`):
|
||||
`create_network_command()`/`list_networks_command()`/`delete_network_command()`
|
||||
are the direct network equivalents (`Mode::network`/`Mode::list_networks`/
|
||||
`Mode::delete_network`) — see `docs/networking-design.md` for the full feature
|
||||
design and `config_file.{h,cpp}` below for `NetworkEntry`. This commit is
|
||||
config-only: no bridge, namespace, or iptables state is created yet, only the
|
||||
config entry (later commits in the design doc's sequence wire up the actual
|
||||
host-side networking). `create_network_command()` rejects a duplicate name
|
||||
first, then resolves `subnet`/`subnet6`: an explicit `--subnet`/`--subnet6`
|
||||
is validated (`is_valid_ipv4_cidr()`/`is_valid_ipv6_cidr()`) and checked for
|
||||
overlap against every existing network's subnet (`ipv4_cidrs_overlap()`/
|
||||
design and `config_file.{h,cpp}` below for `NetworkEntry`. Joining a network
|
||||
from `-r/--run` isn't wired up yet (a later commit in the design doc's
|
||||
sequence). `create_network_command()` rejects a duplicate name first, then
|
||||
resolves `subnet`/`subnet6`: an explicit `--subnet`/`--subnet6` is validated
|
||||
(`is_valid_ipv4_cidr()`/`is_valid_ipv6_cidr()`) and checked for overlap
|
||||
against every existing network's subnet (`ipv4_cidrs_overlap()`/
|
||||
`ipv6_cidrs_overlap()`, `network_subnet.{h,cpp}`, see below); otherwise the
|
||||
next free block is auto-allocated (`allocate_ipv4_subnet()`/
|
||||
`allocate_ipv6_subnet()`). `list_networks_command()` reuses the same
|
||||
`allocate_ipv6_subnet()`). Once a `subnet`/`subnet6` is resolved,
|
||||
`create_network_command()` calls `ensure_network_provisioned()`
|
||||
(`network_bridge.h`, see below) to actually stand up the network's
|
||||
host-side state (bridge, sysctls, iptables rules for `extern`; a dedicated
|
||||
persistent namespace + bridge for `intern`) — only once that succeeds is
|
||||
the entry appended to `config.networks` and persisted; a network that
|
||||
fails to provision isn't saved. `list_networks_command()` reuses the same
|
||||
independently-per-column tab-alignment scheme as `list_processes_command()`
|
||||
(name/kind/subnet each aligned, then the IPv6 subnet — or `"(no ipv6)"` —
|
||||
appended unaligned as the trailing column, nothing follows it).
|
||||
`delete_network_command()` currently only removes the config entry, the
|
||||
same as `delete_volume_command()`'s default (non-`-full`) behavior — it
|
||||
does not tear down the network's live bridge/namespace/iptables state (no
|
||||
`--delete-network-full` analog exists yet).
|
||||
`write_config_command()` implements `-w/--write-config`: unlike
|
||||
`create_volume_command()`/`delete_volume_command()`'s use of
|
||||
`write_config_file()` (which only ever persists `AppConfig` fields that are
|
||||
@@ -464,9 +473,9 @@ Source layout (all under `src/`):
|
||||
left untouched and not reported.
|
||||
- `persistent_netns.{h,cpp}` — generic, narrow infrastructure for keeping a
|
||||
network namespace alive with no process in it, the way `ip netns add` does;
|
||||
no `intern`/`extern` policy or bridge logic here (that's a later commit,
|
||||
`network_bridge.{h,cpp}`, per `docs/networking-design.md`'s commit
|
||||
sequence), and not yet wired into `-n/--network` at all. `persistent_netns_path()`
|
||||
no `intern`/`extern` policy or bridge logic here (that's `network_bridge.{h,cpp}`,
|
||||
see below, which is what actually calls `create_persistent_netns()` for an
|
||||
`intern` network). `persistent_netns_path()`
|
||||
resolves `xdg_state_dir() / "netns" / sanitize_for_filename(name)`
|
||||
(`pid_file.h`, see above). `persistent_netns_exists()` checks whether that
|
||||
path is actually a live bind-mounted namespace, not just a stale/never-
|
||||
@@ -485,6 +494,54 @@ Source layout (all under `src/`):
|
||||
host-state primitives (session locks, cgroups): logs and returns `false` on
|
||||
any failure (already exists, fork/unshare/mount failure) rather than
|
||||
throwing. `remove_persistent_netns()` unmounts then removes the file.
|
||||
- `network_bridge.{h,cpp}` — stands up (or confirms already-standing) a
|
||||
network's actual host-side state: `ensure_network_provisioned()` is
|
||||
idempotent by design (checks `ip link show <bridge>` first and does nothing
|
||||
further if it's already there) — this is deliberately also the reboot-
|
||||
reconciliation mechanism, not a separate code path: nothing about a
|
||||
network's live state (bridge, veths, iptables rules; the persistent
|
||||
namespace itself for `intern`) survives a reboot except its `config.yaml`
|
||||
entry, so calling this again after one just recreates whatever's missing.
|
||||
`bridge_name()` derives a stable, `.cpp`-local interface name from the
|
||||
network's own name via a hand-rolled 32-bit FNV-1a (`"slk" + 8 hex chars`,
|
||||
11 characters, comfortably under Linux's `IFNAMSIZ - 1` = 15-character
|
||||
limit regardless of how long the network name is) — deliberately not
|
||||
`std::hash<std::string>`, whose exact value is implementation-defined and
|
||||
not guaranteed stable across a rebuild with a different standard library,
|
||||
which would silently orphan an already-provisioned bridge a rebuilt binary
|
||||
can no longer find by the name it now computes. For `extern`, every command
|
||||
runs directly (the bridge lives in the host's own root namespace, and this
|
||||
whole feature is root-only for now — `docs/networking-design.md` — so
|
||||
`slocker-lite`'s own current namespace already is the right one). For
|
||||
`intern`, every command is wrapped through `nsenter --net=<persistent path>`
|
||||
(`wrap_for_network()`, `.cpp`-local) into the network's own dedicated
|
||||
namespace (`persistent_netns.h`, created here first if it doesn't exist
|
||||
yet) — the same pattern `wrap_for_root_namespace()` (`bwrap.cpp`) already
|
||||
uses for the rootless `containers-storage` mount's namespace, just
|
||||
targeting a persistent bind-mounted path instead of a live pid's `/proc`
|
||||
entry. `provision_bridge()` (`.cpp`-local): creates the bridge, assigns it
|
||||
the gateway address from `network_subnet.h`'s `ipv4_gateway_address()`
|
||||
(and `ipv6_gateway_address()` if `network.ipv6`), brings it up, then —
|
||||
`extern` only — `sysctl -w net.ipv4.ip_forward=1` (+ the IPv6 equivalent if
|
||||
`ipv6`, both idempotent global host sysctls, not per-bridge, so no separate
|
||||
"already enabled" tracking is needed) and one `iptables`/`ip6tables`
|
||||
`POSTROUTING`/`MASQUERADE` rule for the subnet (`! -o <bridge>`, the same
|
||||
`docker0` shape, so bridge-local inter-container traffic isn't
|
||||
unnecessarily NAT'd). `check_network_dependencies()` gates on the tool set
|
||||
each kind actually needs (`ip` always; `iptables`/`sysctl` [+`ip6tables` if
|
||||
`ipv6`] for `extern`; `nsenter` for `intern`) — same shape/spirit as
|
||||
`commands.cpp`'s own `check_required_dependencies()`, kept separate since
|
||||
the tool set here depends on the network's own kind/`ipv6` setting.
|
||||
`create_network_command()` (`commands.cpp`, see above) calls
|
||||
`ensure_network_provisioned()` after resolving subnets but *before*
|
||||
persisting the config entry — a network that fails to provision isn't
|
||||
saved, so a later join doesn't find a config entry for something that
|
||||
doesn't actually exist on the host. **Verified end-to-end on this dev
|
||||
machine (root, via a scoped `doas` rule)**: a real `extern` network's
|
||||
bridge, gateway IPv4/IPv6 addresses, `ip_forward`, and `MASQUERADE` rules
|
||||
all came up correctly; a real `intern` network's bridge came up inside its
|
||||
own dedicated namespace with neither forwarding nor a NAT rule, confirming
|
||||
the structural (not merely policy) isolation the design calls for.
|
||||
- `session_cgroup.{h,cpp}` — gives `--kill` (`kill_session.{h,cpp}`, see
|
||||
below) a reliable way to find every process a session ever started, however
|
||||
deeply forked/daemonized/reparented, by putting it in a dedicated cgroup v2
|
||||
@@ -820,8 +877,8 @@ Source layout (all under `src/`):
|
||||
`networks`, `ipv6` re-serialized as canonical `"true"`/`"false"` like the
|
||||
`unshare-*` keys.
|
||||
- `network_subnet.{h,cpp}` — pure CIDR arithmetic backing `-n/--network`'s
|
||||
subnet allocation, no kernel/`ip`/`iptables` calls (those come in a later
|
||||
commit per `docs/networking-design.md`'s sequence). `is_valid_ipv4_cidr()`/
|
||||
subnet allocation and `network_bridge.{h,cpp}`'s (see below) gateway-address
|
||||
computation; no kernel/`ip`/`iptables` calls of its own. `is_valid_ipv4_cidr()`/
|
||||
`is_valid_ipv6_cidr()` and `ipv4_cidrs_overlap()`/`ipv6_cidrs_overlap()` all
|
||||
build on one `.cpp`-local `parse_cidr()` (via `inet_pton()`, not hand-rolled
|
||||
parsing) producing a plain byte-vector address (4 bytes for IPv4, 16 for
|
||||
@@ -838,7 +895,14 @@ Source layout (all under `src/`):
|
||||
since IPv6 hextets are hexadecimal, `n >= 10` renders as a valid but
|
||||
numerically-different-from-`n` address (e.g. `n=15` becomes `...:15::/64`,
|
||||
which is hex `0x15` = 21) — purely cosmetic, allocation correctness doesn't
|
||||
depend on the two matching numerically.
|
||||
depend on the two matching numerically. `ipv4_gateway_address()`/
|
||||
`ipv6_gateway_address()` (`network_bridge.cpp`'s `provision_bridge()`)
|
||||
return a network's bridge gateway address within a CIDR: masked down to its
|
||||
network address first (a shared `.cpp`-local `mask_to_network()`, in case
|
||||
the CIDR given — e.g. a manual `--subnet`/`--subnet6` — wasn't already a
|
||||
canonical network address), then `| 1` in the last bit for the `.1`
|
||||
convention this project's bridges use, reusing the same `parse_cidr()` as
|
||||
the validation/overlap functions above.
|
||||
- `volume_mount.{h,cpp}` — `is_valid_volume_name()` (no `/`, checked by both
|
||||
`create_volume_command()` and to tell a `-v` spec's name/path apart) and
|
||||
`resolve_volume_mount()`, called once per `-v` occurrence from `run_container()`
|
||||
|
||||
@@ -92,7 +92,7 @@ slocker-lite -V|--version
|
||||
| `--list-volumes` | List all named volumes (see `-v/--volume`) with their host directory. |
|
||||
| `--delete-volume <name>` | Remove a named volume from the config. The host directory is left untouched. |
|
||||
| `--delete-volume-full <name>` | Like `--delete-volume`, but also recursively deletes the volume's host directory. |
|
||||
| `-n, --network <name>` | Create/manage a persistent named network: requires exactly one of `--extern` (has a path to the host's real network) or `--intern` (only reachable by other containers on the same network). `--subnet <cidr>` overrides the auto-allocated IPv4 range (`10.168.0.0/24`, incrementing per network); `--no-ipv6` disables (and `--subnet6 <cidr>` overrides) the auto-allocated IPv6 range, on by default. With `--run`, instead joins `<name>` to the container; repeatable, no membership limit. See [`docs/networking-design.md`](docs/networking-design.md) — as of this, network creation is config-only: no bridge/namespace/iptables state exists yet. |
|
||||
| `-n, --network <name>` | Create/manage a persistent named network: requires exactly one of `--extern` (a real Linux bridge in the host's own namespace, with NAT/forwarding set up so containers on it reach the host's real network) or `--intern` (a bridge inside its own dedicated, routeless namespace, only reachable by other containers on the same network). `--subnet <cidr>` overrides the auto-allocated IPv4 range (`10.168.0.0/24`, incrementing per network); `--no-ipv6` disables (and `--subnet6 <cidr>` overrides) the auto-allocated IPv6 range, on by default. With `--run`, instead joins `<name>` to the container; repeatable, no membership limit (not wired up yet, see below). Root-only for now. See [`docs/networking-design.md`](docs/networking-design.md). |
|
||||
| `--list-networks` | List all named networks (see `-n/--network`) with their kind, IPv4 subnet, and IPv6 subnet (or `(no ipv6)`). |
|
||||
| `--delete-network <name>` | Remove a named network from the config. |
|
||||
| `--list-processes` | List running `--run` sessions found by their pid files under `$XDG_STATE_HOME/slocker-lite/run/`, with their pid, container name, and status (`running` or `exited`). |
|
||||
@@ -203,12 +203,12 @@ managed by `-v/--volume` (see above) rather than hand-edited — it's what
|
||||
`-r/--run`'s own `-v` usage looks named volumes up in. The `networks` section
|
||||
is likewise managed by `-n/--network` rather than hand-edited — see
|
||||
[`docs/networking-design.md`](docs/networking-design.md) for the full
|
||||
persistent-network feature design (as of this, network creation is
|
||||
config-only: no bridge/namespace/iptables state exists yet, so
|
||||
`global.unshare-net` above is still the only thing actually affecting a
|
||||
sandboxed container's network access). A missing config file is fine either
|
||||
way (nothing is overridden, and one gets created the first time
|
||||
`-v/--volume`/`-n/--network` is used).
|
||||
persistent-network feature design. `-n/--network` creating a network does
|
||||
stand up its real bridge/iptables state (root-only) as of this, but joining
|
||||
one from `-r/--run` isn't wired up yet, so `global.unshare-net` above is
|
||||
still the only thing actually affecting a sandboxed container's own network
|
||||
access. A missing config file is fine either way (nothing is overridden, and
|
||||
one gets created the first time `-v/--volume`/`-n/--network` is used).
|
||||
|
||||
Run `-w/--write-config` to bootstrap a config file: it writes out every
|
||||
supported option explicitly (filling in the current or default value for
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ slocker_lite = executable('slocker-lite',
|
||||
'src/bwrap.cpp', 'src/user_spec.cpp', 'src/config_file.cpp', 'src/volume_mount.cpp',
|
||||
'src/pid_file.cpp', 'src/exec_session.cpp', 'src/env_spec.cpp', 'src/daemonize.cpp',
|
||||
'src/sandbox_process.cpp', 'src/session_cgroup.cpp', 'src/kill_session.cpp',
|
||||
'src/network_subnet.cpp', 'src/persistent_netns.cpp'],
|
||||
'src/network_subnet.cpp', 'src/persistent_netns.cpp', 'src/network_bridge.cpp'],
|
||||
include_directories : include_directories('.'),
|
||||
dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep],
|
||||
install : true)
|
||||
|
||||
+15
-4
@@ -38,6 +38,7 @@
|
||||
#include "env_spec.h"
|
||||
#include "exec_session.h"
|
||||
#include "kill_session.h"
|
||||
#include "network_bridge.h"
|
||||
#include "network_subnet.h"
|
||||
#include "oci_image.h"
|
||||
#include "pid_file.h"
|
||||
@@ -367,9 +368,13 @@ int delete_volume_command(const std::string& name, const std::filesystem::path&
|
||||
return 0;
|
||||
}
|
||||
|
||||
// -n/--network's standalone (create) use. Config-only for now -- no bridge,
|
||||
// namespace, or iptables state is created yet (see docs/networking-design.md's
|
||||
// commit sequence; that lands in later commits).
|
||||
// -n/--network's standalone (create) use. Resolves subnet(s), provisions the
|
||||
// network's actual host-side state (ensure_network_provisioned(),
|
||||
// network_bridge.h), and only persists the config entry once that succeeds --
|
||||
// a network that failed to provision isn't saved, so a later -r/--run join
|
||||
// doesn't find a config entry for something that doesn't actually exist on
|
||||
// the host. Joining a network from -r/--run isn't wired up yet (see
|
||||
// docs/networking-design.md's commit sequence; that lands in a later commit).
|
||||
int create_network_command(const std::string& name, NetworkKind kind,
|
||||
const std::optional<std::string>& subnet_override, bool ipv6,
|
||||
const std::optional<std::string>& subnet6_override,
|
||||
@@ -429,7 +434,13 @@ int create_network_command(const std::string& name, NetworkKind kind,
|
||||
}
|
||||
}
|
||||
|
||||
config.networks.push_back({name, kind, subnet, ipv6, subnet6});
|
||||
NetworkEntry entry{name, kind, subnet, ipv6, subnet6};
|
||||
if (!ensure_network_provisioned(entry)) {
|
||||
spdlog::error("failed to provision network '{}'; not saving it to the config", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
config.networks.push_back(entry);
|
||||
if (!write_config_file(config_path, config)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright (C) 2026 Viorel Munteanu
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#include "network_bridge.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "network_subnet.h"
|
||||
#include "persistent_netns.h"
|
||||
#include "process.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Deterministic, portable 32-bit FNV-1a -- not std::hash<std::string>(),
|
||||
// whose exact value is implementation-defined and isn't guaranteed stable
|
||||
// across a rebuild with a different standard library, which would silently
|
||||
// "orphan" an already-provisioned bridge (a rebuilt binary computing a
|
||||
// different name for the same network could no longer find it).
|
||||
uint32_t fnv1a(std::string_view s) {
|
||||
uint32_t hash = 0x811c9dc5u;
|
||||
for (unsigned char c : s) {
|
||||
hash ^= c;
|
||||
hash *= 0x01000193u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
// "slk" + 8 hex chars = 11 characters, comfortably under Linux's 15-character
|
||||
// (IFNAMSIZ - 1) interface name limit regardless of how long `network_name`
|
||||
// is. Not human-recognizable, but doesn't need to be -- the network's own
|
||||
// identity lives in config.yaml by name; this only needs to be stable (the
|
||||
// same network always maps to the same bridge, so re-provisioning finds the
|
||||
// existing one) and collision-free at this project's expected scale (a
|
||||
// handful of networks, not thousands).
|
||||
std::string bridge_name(const std::string& network_name) { return fmt::format("slk{:08x}", fnv1a(network_name)); }
|
||||
|
||||
// For `extern` networks, commands run directly: the bridge lives in the
|
||||
// host's own root namespace, and this whole feature is root-only for now
|
||||
// (docs/networking-design.md), so slocker-lite's own current namespace
|
||||
// already *is* the right one, no wrapping needed. For `intern`, every
|
||||
// command is wrapped through nsenter into the network's own dedicated
|
||||
// persistent namespace (persistent_netns.h) -- the same pattern
|
||||
// wrap_for_root_namespace() (bwrap.cpp) already uses for reaching the
|
||||
// rootless containers-storage mount's namespace, just targeting a persistent
|
||||
// bind-mounted path instead of a live pid's /proc entry.
|
||||
std::vector<std::string> wrap_for_network(const NetworkEntry& network, std::vector<std::string> argv) {
|
||||
if (network.kind == NetworkKind::extern_) {
|
||||
return argv;
|
||||
}
|
||||
std::vector<std::string> wrapped = {"nsenter", fmt::format("--net={}", persistent_netns_path(network.name).string()),
|
||||
"--"};
|
||||
wrapped.insert(wrapped.end(), argv.begin(), argv.end());
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
bool run_admin_command(const NetworkEntry& network, std::vector<std::string> argv, std::string_view what) {
|
||||
auto result = run_process(wrap_for_network(network, std::move(argv)));
|
||||
if (result.exit_code != 0) {
|
||||
spdlog::error("failed to {} for network '{}' (exit code {})", what, network.name, result.exit_code);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool bridge_exists(const NetworkEntry& network, const std::string& bridge) {
|
||||
return run_process(wrap_for_network(network, {"ip", "link", "show", bridge})).exit_code == 0;
|
||||
}
|
||||
|
||||
bool provision_bridge(const NetworkEntry& network, const std::string& bridge) {
|
||||
if (!run_admin_command(network, {"ip", "link", "add", bridge, "type", "bridge"}, "create bridge")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto gateway = ipv4_gateway_address(network.subnet);
|
||||
if (!gateway) {
|
||||
spdlog::error("invalid IPv4 subnet '{}' for network '{}'", network.subnet, network.name);
|
||||
return false;
|
||||
}
|
||||
if (!run_admin_command(network, {"ip", "addr", "add", *gateway, "dev", bridge}, "assign bridge IPv4 address")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (network.ipv6) {
|
||||
auto gateway6 = ipv6_gateway_address(network.subnet6);
|
||||
if (!gateway6) {
|
||||
spdlog::error("invalid IPv6 subnet '{}' for network '{}'", network.subnet6, network.name);
|
||||
return false;
|
||||
}
|
||||
if (!run_admin_command(network, {"ip", "-6", "addr", "add", *gateway6, "dev", bridge},
|
||||
"assign bridge IPv6 address")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!run_admin_command(network, {"ip", "link", "set", bridge, "up"}, "bring bridge up")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (network.kind == NetworkKind::extern_) {
|
||||
// Global host sysctls, not per-bridge -- idempotent to re-apply, so
|
||||
// no need to track "already enabled" separately.
|
||||
if (!run_admin_command(network, {"sysctl", "-w", "net.ipv4.ip_forward=1"}, "enable IPv4 forwarding")) {
|
||||
return false;
|
||||
}
|
||||
if (!run_admin_command(network, {"iptables", "-t", "nat", "-A", "POSTROUTING", "-s", network.subnet, "!",
|
||||
"-o", bridge, "-j", "MASQUERADE"},
|
||||
"add IPv4 MASQUERADE rule")) {
|
||||
return false;
|
||||
}
|
||||
if (network.ipv6) {
|
||||
if (!run_admin_command(network, {"sysctl", "-w", "net.ipv6.conf.all.forwarding=1"},
|
||||
"enable IPv6 forwarding")) {
|
||||
return false;
|
||||
}
|
||||
if (!run_admin_command(network, {"ip6tables", "-t", "nat", "-A", "POSTROUTING", "-s", network.subnet6,
|
||||
"!", "-o", bridge, "-j", "MASQUERADE"},
|
||||
"add IPv6 MASQUERADE rule")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool check_network_dependencies(const NetworkEntry& network) {
|
||||
std::vector<std::string_view> tools = {"ip"};
|
||||
if (network.kind == NetworkKind::extern_) {
|
||||
tools.push_back("iptables");
|
||||
tools.push_back("sysctl");
|
||||
if (network.ipv6) {
|
||||
tools.push_back("ip6tables");
|
||||
}
|
||||
} else {
|
||||
tools.push_back("nsenter");
|
||||
}
|
||||
|
||||
bool all_found = true;
|
||||
for (auto name : tools) {
|
||||
if (!find_in_path(name)) {
|
||||
spdlog::error("required dependency not found in PATH: {}", name);
|
||||
all_found = false;
|
||||
}
|
||||
}
|
||||
return all_found;
|
||||
}
|
||||
|
||||
bool ensure_network_provisioned(const NetworkEntry& network) {
|
||||
if (!check_network_dependencies(network)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (network.kind == NetworkKind::intern && !persistent_netns_exists(network.name)) {
|
||||
if (!create_persistent_netns(network.name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::string bridge = bridge_name(network.name);
|
||||
if (bridge_exists(network, bridge)) {
|
||||
return true; // already provisioned
|
||||
}
|
||||
|
||||
return provision_bridge(network, bridge);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (C) 2026 Viorel Munteanu
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "config_file.h"
|
||||
|
||||
// Checks that the external tools provisioning `network` needs are found in
|
||||
// PATH, logging which are missing: always `ip`; `iptables`/`sysctl` (+
|
||||
// `ip6tables` if `network.ipv6`) for `extern`; `nsenter` for `intern` (to
|
||||
// reach its dedicated persistent namespace). Same shape/spirit as
|
||||
// commands.cpp's own check_required_dependencies(), kept separate since this
|
||||
// file's tool set depends on the network's own kind/ipv6 setting.
|
||||
bool check_network_dependencies(const NetworkEntry& network);
|
||||
|
||||
// Ensures `network`'s bridge (and, for `intern`, its dedicated persistent
|
||||
// namespace -- persistent_netns.h) exists and is configured, creating
|
||||
// whatever's missing:
|
||||
// - extern: bridge in the host's own root namespace (this whole feature is
|
||||
// root-only for now, see docs/networking-design.md, so no nsenter
|
||||
// wrapping is needed to reach it); net.ipv4.ip_forward=1 (+ the IPv6
|
||||
// forwarding sysctl if network.ipv6) and one iptables MASQUERADE rule
|
||||
// (+ ip6tables if network.ipv6) for the subnet.
|
||||
// - intern: bridge inside network's own dedicated persistent namespace
|
||||
// (created here if it doesn't exist yet); no forwarding sysctl, no NAT
|
||||
// rule -- no route out at all.
|
||||
// Idempotent: a no-op (true) if the bridge already exists. This doubles as
|
||||
// the mechanism that transparently recreates a network's host-side state
|
||||
// after a reboot (nothing about it survives one except the config.yaml
|
||||
// entry) -- there's no separate "reconcile" path; calling this again just
|
||||
// recreates whatever's missing.
|
||||
bool ensure_network_provisioned(const NetworkEntry& network);
|
||||
@@ -80,6 +80,39 @@ bool bytes_overlap(const std::vector<uint8_t>& a, int prefix_a, const std::vecto
|
||||
return true;
|
||||
}
|
||||
|
||||
// Masks `addr` (modified in place) down to its network address for the given
|
||||
// prefix length, zeroing every bit outside the prefix -- shared by
|
||||
// ipv4_gateway_address()/ipv6_gateway_address() so a manually-given --subnet
|
||||
// that wasn't already a canonical network address still yields a correct
|
||||
// gateway.
|
||||
void mask_to_network(std::vector<uint8_t>& addr, int prefix) {
|
||||
int full_bytes = prefix / 8;
|
||||
int rem_bits = prefix % 8;
|
||||
if (rem_bits > 0) {
|
||||
auto mask = static_cast<uint8_t>(0xFF << (8 - rem_bits));
|
||||
addr[static_cast<size_t>(full_bytes)] &= mask;
|
||||
++full_bytes;
|
||||
}
|
||||
for (size_t i = static_cast<size_t>(full_bytes); i < addr.size(); ++i) {
|
||||
addr[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> gateway_address(int af, const std::string& cidr) {
|
||||
auto parsed = parse_cidr(af, cidr);
|
||||
if (!parsed) {
|
||||
return std::nullopt;
|
||||
}
|
||||
mask_to_network(parsed->addr, parsed->prefix);
|
||||
parsed->addr.back() = static_cast<uint8_t>(parsed->addr.back() | 1);
|
||||
|
||||
char buf[INET6_ADDRSTRLEN] = {};
|
||||
if (!inet_ntop(af, parsed->addr.data(), buf, sizeof(buf))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return fmt::format("{}/{}", buf, parsed->prefix);
|
||||
}
|
||||
|
||||
// Highest index this project's own auto-allocation ever hands out --
|
||||
// 10.168.0.0/24..10.168.255.0/24 and fd00:168:0:0::/64..fd00:168:0:255::/64,
|
||||
// deliberately kept in sync between the two so the common case (no manual
|
||||
@@ -143,3 +176,7 @@ std::optional<std::string> allocate_ipv6_subnet(const std::vector<NetworkEntry>&
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> ipv4_gateway_address(const std::string& cidr) { return gateway_address(AF_INET, cidr); }
|
||||
|
||||
std::optional<std::string> ipv6_gateway_address(const std::string& cidr) { return gateway_address(AF_INET6, cidr); }
|
||||
|
||||
@@ -49,3 +49,13 @@ std::optional<std::string> allocate_ipv4_subnet(const std::vector<NetworkEntry>&
|
||||
// Same idea for the paired IPv6 ULA block: "fd00:168:0:<n>::/64", checked
|
||||
// against every existing network with ipv6 enabled.
|
||||
std::optional<std::string> allocate_ipv6_subnet(const std::vector<NetworkEntry>& existing);
|
||||
|
||||
// The bridge's own gateway address within `cidr`: the CIDR masked down to its
|
||||
// network address (in case `cidr` wasn't already one -- e.g. a manually
|
||||
// given --subnet), plus 1 in the last bit (the ".1" convention this
|
||||
// project's bridges use), formatted with the same prefix length (e.g.
|
||||
// "10.168.0.0/24" -> "10.168.0.1/24"). nullopt if `cidr` doesn't parse.
|
||||
std::optional<std::string> ipv4_gateway_address(const std::string& cidr);
|
||||
|
||||
// Same idea for IPv6.
|
||||
std::optional<std::string> ipv6_gateway_address(const std::string& cidr);
|
||||
|
||||
Reference in New Issue
Block a user