Skip to content

Commit 9abd1a2

Browse files
committed
Allow LXC traffic through host firewall
1 parent 717fa86 commit 9abd1a2

4 files changed

Lines changed: 46 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
bootstrap. A channel no longer repeatedly creates and cleans up its computer
1717
after starting `apt` during the guest-network race.
1818

19+
- 1Helm's narrow owned LXC forwarding rules are inserted ahead of host firewall
20+
policies such as Docker's `FORWARD=DROP`, allowing resident guests to reach
21+
package mirrors without replacing Docker or Tailscale chains.
22+
1923
- The host installer now prepares the same `/var/lib/1helm-lxc/machines` tree
2024
used by the runtime instead of leaving an obsolete empty `containers` tree.
2125

scripts/1helm-lxc-net

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ set -euo pipefail
77

88
MARKER="/run/1helm-lxc-net-owned"
99
RULE_MARKER="/run/1helm-lxc-net-rules-owned"
10+
INPUT_CHAIN="ONEHELM_LXC_INPUT"
11+
FORWARD_CHAIN="ONEHELM_LXC_FORWARD"
1012
DNSMASQ_PID="/run/lxc/dnsmasq.pid"
1113
DNSMASQ_STATE="/var/lib/1helm-lxc/network"
1214
DNSMASQ_LEASE="$DNSMASQ_STATE/misc/dnsmasq.lxcbr0.leases"
@@ -42,29 +44,45 @@ start_bridge_dns() {
4244
install_rules() {
4345
sysctl -q -w net.ipv4.ip_forward=1 >/dev/null
4446
if [[ -e "$RULE_MARKER" ]]; then
47+
remove_filter_rules
4548
nft delete table inet onehelm_lxc 2>/dev/null || true
4649
nft delete table ip onehelm_lxc 2>/dev/null || true
47-
elif nft list table inet onehelm_lxc >/dev/null 2>&1 || nft list table ip onehelm_lxc >/dev/null 2>&1; then
50+
elif iptables -S "$INPUT_CHAIN" >/dev/null 2>&1 || iptables -S "$FORWARD_CHAIN" >/dev/null 2>&1 \
51+
|| nft list table inet onehelm_lxc >/dev/null 2>&1 || nft list table ip onehelm_lxc >/dev/null 2>&1; then
4852
echo "A non-1Helm nftables ruleset already uses the onehelm_lxc name." >&2
4953
exit 1
5054
fi
55+
# Docker and other host software can set the real FORWARD chain policy to
56+
# DROP. A parallel nftables base chain cannot override a later base-chain
57+
# drop, so jump to 1Helm's exact owned rules before those host policies.
58+
iptables -w -N "$INPUT_CHAIN"
59+
iptables -w -A "$INPUT_CHAIN" -i "$BRIDGE" -p udp -m multiport --dports 53,67 -j ACCEPT
60+
iptables -w -A "$INPUT_CHAIN" -i "$BRIDGE" -p tcp -m multiport --dports 53,67 -j ACCEPT
61+
iptables -w -I INPUT 1 -j "$INPUT_CHAIN"
62+
iptables -w -N "$FORWARD_CHAIN"
63+
iptables -w -A "$FORWARD_CHAIN" -i "$BRIDGE" -j ACCEPT
64+
iptables -w -A "$FORWARD_CHAIN" -o "$BRIDGE" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
65+
iptables -w -I FORWARD 1 -j "$FORWARD_CHAIN"
5166
nft -f - <<'EOF'
52-
add table inet onehelm_lxc
53-
add chain inet onehelm_lxc input { type filter hook input priority -10; policy accept; }
54-
add rule inet onehelm_lxc input iifname "lxcbr0" udp dport { 53, 67 } accept
55-
add rule inet onehelm_lxc input iifname "lxcbr0" tcp dport { 53, 67 } accept
56-
add chain inet onehelm_lxc forward { type filter hook forward priority -10; policy accept; }
57-
add rule inet onehelm_lxc forward iifname "lxcbr0" accept
58-
add rule inet onehelm_lxc forward oifname "lxcbr0" ct state established,related accept
5967
add table ip onehelm_lxc
6068
add chain ip onehelm_lxc postrouting { type nat hook postrouting priority srcnat; policy accept; }
6169
add rule ip onehelm_lxc postrouting ip saddr 10.0.3.0/24 ip daddr != 10.0.3.0/24 masquerade
6270
EOF
6371
install -m 0600 /dev/null "$RULE_MARKER"
6472
}
6573

74+
remove_filter_rules() {
75+
while iptables -w -C INPUT -j "$INPUT_CHAIN" >/dev/null 2>&1; do iptables -w -D INPUT -j "$INPUT_CHAIN"; done
76+
while iptables -w -C FORWARD -j "$FORWARD_CHAIN" >/dev/null 2>&1; do iptables -w -D FORWARD -j "$FORWARD_CHAIN"; done
77+
iptables -w -F "$INPUT_CHAIN" 2>/dev/null || true
78+
iptables -w -X "$INPUT_CHAIN" 2>/dev/null || true
79+
iptables -w -F "$FORWARD_CHAIN" 2>/dev/null || true
80+
iptables -w -X "$FORWARD_CHAIN" 2>/dev/null || true
81+
}
82+
6683
remove_rules() {
6784
if [[ -e "$RULE_MARKER" ]]; then
85+
remove_filter_rules
6886
nft delete table inet onehelm_lxc 2>/dev/null || true
6987
nft delete table ip onehelm_lxc 2>/dev/null || true
7088
rm -f -- "$RULE_MARKER"
@@ -105,14 +123,12 @@ lease_state_writable() {
105123

106124
rules_healthy() {
107125
[[ -e "$RULE_MARKER" ]] || return 1
108-
nft list chain inet onehelm_lxc input 2>/dev/null \
109-
| grep -Eq 'iifname "lxcbr0" udp dport \{ 53, 67 \} accept' || return 1
110-
nft list chain inet onehelm_lxc input 2>/dev/null \
111-
| grep -Eq 'iifname "lxcbr0" tcp dport \{ 53, 67 \} accept' || return 1
112-
nft list chain inet onehelm_lxc forward 2>/dev/null \
113-
| grep -Fq 'iifname "lxcbr0" accept' || return 1
114-
nft list chain inet onehelm_lxc forward 2>/dev/null \
115-
| grep -Eq 'oifname "lxcbr0" ct state (established,related|related,established) accept' || return 1
126+
[[ "$(iptables -S INPUT | sed -n '2p')" == "-A INPUT -j $INPUT_CHAIN" ]] || return 1
127+
[[ "$(iptables -S FORWARD | sed -n '2p')" == "-A FORWARD -j $FORWARD_CHAIN" ]] || return 1
128+
iptables -S "$INPUT_CHAIN" 2>/dev/null | grep -Fxq -- "-A $INPUT_CHAIN -i $BRIDGE -p udp -m multiport --dports 53,67 -j ACCEPT" || return 1
129+
iptables -S "$INPUT_CHAIN" 2>/dev/null | grep -Fxq -- "-A $INPUT_CHAIN -i $BRIDGE -p tcp -m multiport --dports 53,67 -j ACCEPT" || return 1
130+
iptables -S "$FORWARD_CHAIN" 2>/dev/null | grep -Fxq -- "-A $FORWARD_CHAIN -i $BRIDGE -j ACCEPT" || return 1
131+
iptables -S "$FORWARD_CHAIN" 2>/dev/null | grep -Fxq -- "-A $FORWARD_CHAIN -o $BRIDGE -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT" || return 1
116132
nft list chain ip onehelm_lxc postrouting 2>/dev/null \
117133
| grep -Eq 'ip saddr 10\.0\.3\.0/24 ip daddr != 10\.0\.3\.0/24 masquerade' || return 1
118134
}

scripts/1helm-lxc-runtime

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ wait_for_guest_network() {
8989
'
9090
if lxc-attach -P "$LXC_PATH" -n "$name" --clear-env -- /bin/sh -c '
9191
getent ahostsv4 archive.ubuntu.com >/dev/null \
92-
&& getent ahostsv4 security.ubuntu.com >/dev/null
92+
&& getent ahostsv4 security.ubuntu.com >/dev/null \
93+
&& curl -4 -fsS --connect-timeout 2 --max-time 5 \
94+
http://archive.ubuntu.com/ubuntu/dists/noble/InRelease >/dev/null
9395
' >/dev/null 2>&1; then
9496
return
9597
fi
@@ -249,8 +251,8 @@ case "$operation" in
249251
wait_for_guest_network "$name"
250252
lxc-attach -P "$LXC_PATH" -n "$name" --clear-env -- /bin/sh -eu -c '
251253
export DEBIAN_FRONTEND=noninteractive
252-
apt-get update -o APT::Update::Error-Mode=any
253-
apt-get install -y --no-install-recommends bash build-essential ca-certificates coreutils cron curl dbus file findutils git gzip iproute2 iputils-ping jq less locales man-db nano openssh-client procps python3 python3-pip rsync sudo systemd systemd-sysv tar tzdata unzip vim-tiny wget xz-utils zip
254+
apt-get update -o Acquire::ForceIPv4=true -o APT::Update::Error-Mode=any
255+
apt-get install -y -o Acquire::ForceIPv4=true --no-install-recommends bash build-essential ca-certificates coreutils cron curl dbus file findutils git gzip iproute2 iputils-ping jq less locales man-db nano openssh-client procps python3 python3-pip rsync sudo systemd systemd-sysv tar tzdata unzip vim-tiny wget xz-utils zip
254256
apt-get clean
255257
rm -rf /var/lib/apt/lists/*
256258
existing_group="$(getent group 1000 | cut -d: -f1 || true)"

test/site.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -200,16 +200,17 @@ test("installer assets are explicit and syntax-valid", () => {
200200
assert.match(lxcHelper, /ownership marker does not match/);
201201
assert.match(lxcHelper, /cleanup_incomplete_create[\s\S]*rm -rf -- "\$LXC_PATH\/\$name"[\s\S]*created=1[\s\S]*lxc-create/, "a failed create removes only its validated partial container directory");
202202
assert.match(lxcHelper, /wait_for_guest_network[\s\S]*10\\\.0\\\.3\\\.[\s\S]*via 10\\\.0\\\.3\\\.1 dev eth0/, "fresh LXC provisioning waits for its DHCP address and default route");
203-
assert.match(lxcHelper, /archive\.ubuntu\.com[\s\S]*security\.ubuntu\.com/, "fresh LXC provisioning proves DNS before package bootstrap");
204-
assert.match(lxcHelper, /start "\$name"[\s\S]*wait_for_guest_network "\$name"[\s\S]*apt-get update -o APT::Update::Error-Mode=any/, "package bootstrap begins only after bounded guest-network readiness");
203+
assert.match(lxcHelper, /archive\.ubuntu\.com[\s\S]*security\.ubuntu\.com[\s\S]*curl -4[\s\S]*InRelease/, "fresh LXC provisioning proves DNS and outbound IPv4 HTTP before package bootstrap");
204+
assert.match(lxcHelper, /start "\$name"[\s\S]*wait_for_guest_network "\$name"[\s\S]*apt-get update -o Acquire::ForceIPv4=true -o APT::Update::Error-Mode=any/, "package bootstrap begins only after bounded guest-network readiness");
205205
assert.match(lxcHelper, /cpuset\.cpus\.effective/, "LXC CPU limits are selected from the service's actually delegated host CPUs");
206206
assert.match(lxcHelper, /cpu_count/, "LXC inspection counts noncontiguous delegated CPU lists correctly");
207207
assert.match(lxcNetwork, /1helm-lxc-net-owned/, "the bridge wrapper stops only a bridge it started");
208-
assert.match(lxcNetwork, /1helm-lxc-net-rules-owned[\s\S]*table ip onehelm_lxc[\s\S]*masquerade/, "an adopted bridge receives a separately owned, removable outbound NAT contract");
208+
assert.match(lxcNetwork, /1helm-lxc-net-rules-owned[\s\S]*ONEHELM_LXC_INPUT[\s\S]*ONEHELM_LXC_FORWARD[\s\S]*iptables -w -I INPUT 1[\s\S]*iptables -w -I FORWARD 1[\s\S]*table ip onehelm_lxc[\s\S]*masquerade/, "an adopted bridge receives owned filter jumps ahead of host drop policies plus removable outbound NAT");
209209
assert.match(lxcNetwork, /DNSMASQ_LEASE="\$DNSMASQ_STATE\/misc\/dnsmasq\.lxcbr0\.leases"[\s\S]*lease_state_writable[\s\S]*mktemp[\s\S]*bridge_dns_healthy/, "runtime health proves the exact private dnsmasq lease tree is writable from its current mount namespace");
210210
assert.match(lxcNetwork, /start_bridge_dns[\s\S]*dnsmasq[\s\S]*--dhcp-leasefile="\$DNSMASQ_LEASE"/, "1Helm starts its private DHCP server directly instead of inheriting the distro helper's system-wide lease path");
211211
assert.match(lxcNetwork, /--dhcp-range[\s\S]*10\.0\.3\.2,10\.0\.3\.254[\s\S]*--dhcp-lease-max=253[\s\S]*--dhcp-authoritative[\s\S]*--dhcp-leasefile=\$DNSMASQ_LEASE/, "runtime health verifies the exact DHCP and private lease-file process contract");
212-
assert.match(lxcNetwork, /rules_healthy[\s\S]*nft list chain inet onehelm_lxc input[\s\S]*nft list chain inet onehelm_lxc forward[\s\S]*nft list chain ip onehelm_lxc postrouting[\s\S]*10\\\.0\\\.3\\\.0\/24[\s\S]*masquerade/, "runtime health verifies the exact DNS, DHCP, forwarding, and outbound NAT rules instead of accepting table names alone");
212+
assert.match(lxcNetwork, /rules_healthy[\s\S]*iptables -S INPUT[\s\S]*iptables -S FORWARD[\s\S]*nft list chain ip onehelm_lxc postrouting[\s\S]*10\\\.0\\\.3\\\.0\/24[\s\S]*masquerade/, "runtime health verifies first-position host filter jumps and exact outbound NAT instead of accepting an ineffective parallel base chain");
213+
assert.match(lxcNetwork, /FORWARD_CHAIN="ONEHELM_LXC_FORWARD"[\s\S]*-A "\$FORWARD_CHAIN" -i "\$BRIDGE" -j ACCEPT[\s\S]*-A "\$FORWARD_CHAIN" -o "\$BRIDGE"/, "the owned forwarding chain accepts guest egress and only established return traffic");
213214
assert.match(lxcNetwork, /BRIDGE_CIDR="10\.0\.3\.1\/24"[\s\S]*bridge_dns_healthy[\s\S]*state UP[\s\S]*DNSMASQ_PID[\s\S]*--interface=lxcbr0[\s\S]*network_healthy/, "runtime health requires an up/addressed bridge and its exact dnsmasq process");
214215
assert.match(lxcNetwork, /bridge_dns_healthy[\s\S]*ensure_rules[\s\S]*network_healthy/, "a healthy bridge can restore only its owned firewall rules without disrupting containers");
215216
assert.match(lxcNetwork, /"\$LXC_NET" stop force[\s\S]*start_bridge_dns[\s\S]*network_healthy/, "a dead private bridge/DNS stack is rebuilt with 1Helm-owned DHCP state and reverified instead of adopted by interface name");

0 commit comments

Comments
 (0)