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:
2026-08-30 12:50:55 +00:00
parent db3a9d82c7
commit 24b8ddcce7
8 changed files with 380 additions and 26 deletions
+78 -14
View File
@@ -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()`
+7 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+187
View File
@@ -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);
}
+45
View File
@@ -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);
+37
View File
@@ -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); }
+10
View File
@@ -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);