Skip to content

[LXC] Enforce the deny-all-except-proxy network policy (model 2) #36

[LXC] Enforce the deny-all-except-proxy network policy (model 2)

[LXC] Enforce the deny-all-except-proxy network policy (model 2) #36

Workflow file for this run

name: LXC E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
lxc-e2e:
name: LXC-Exec Container and Network Policy
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v6
- name: Setup Rust toolchain
run: rustup update stable
- name: Point cargo at the MxcDependencies feed
uses: ./.github/actions/setup-cargo-feed
- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
with:
workspaces: src
- name: Install LXC and firewall tooling
run: |
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
lxc lxc-templates lxc-utils iptables debootstrap uidmap bridge-utils
# A bridged veth only reaches the FORWARD chain while br_netfilter is
# delivering bridged packets to iptables. Without it the firewall rules
# install cleanly and never fire, so the network policy tests would pass
# against a firewall that filters nothing.
- name: Enable bridge netfilter
run: |
sudo modprobe br_netfilter
sudo sysctl -w net.bridge.bridge-nf-call-iptables=1
sudo sysctl -w net.bridge.bridge-nf-call-ip6tables=1
# GitHub-hosted runners ship Docker, and Docker sets the IPv4 FORWARD
# policy to DROP. That breaks these tests twice over.
#
# First, it breaks them outright. MXC hooks its chain on traffic leaving
# the container (`-i <veth>` / `--physdev-in <veth>`), so an allowed
# request is accepted on the way out -- but the reply arrives in the
# opposite direction, matches no MXC rule, falls through to the policy,
# and is dropped. The connection times out and an explicitly allowed
# destination looks unreachable. Observed exactly that: DNS resolved,
# because dnsmasq on lxcbr0 is host-local and never traverses FORWARD,
# and then `wget: can't connect to remote host (140.82.116.5)`.
#
# Second, and worse, it would make the deny cases meaningless. Under a
# DROP policy a container with NO working MXC hook at all is also
# unreachable, so the enforcement and deny-precedence tests would report
# success against a firewall that filters nothing -- which is the precise
# bug this suite exists to detect, and the reason these tests carry
# positive controls.
#
# Setting the policy to ACCEPT restores the condition the tests were
# written for: the host forwards by default, so the ONLY thing that can
# block container traffic is a rule MXC installed. A missing hook then
# shows up as an unexpected success and fails the deny case loudly.
# A narrower conntrack RELATED,ESTABLISHED rule is not an alternative
# here. The chain already carries return rules in both the interface and
# the physdev form, and both were measured inert on this bridged
# topology: a reply is routed toward lxcbr0, so the bridge port is not
# selected when FORWARD runs and neither form matches. Scoping the return
# direction by the container's address is the fix, and it is deferred --
# it needs the address plumbed through to the manager and a live bridged
# measurement, not another untested rule.
- name: Let the host forward, so only MXC rules can block
run: |
sudo iptables -P FORWARD ACCEPT
sudo ip6tables -P FORWARD ACCEPT
sudo iptables -S FORWARD | head -5
- name: Report the environment these tests depend on
run: |
echo "--- kernel ---"
uname -a
echo "--- lxc ---"
lxc-create --version || echo "MISSING lxc-create"
echo "--- iptables ---"
sudo iptables --version || echo "MISSING iptables"
sudo ip6tables --version || echo "MISSING ip6tables"
echo "--- forward policy (must be ACCEPT, or deny cases pass vacuously) ---"
sudo iptables -S FORWARD | head -1
sudo ip6tables -S FORWARD | head -1
echo "--- bridge netfilter ---"
cat /proc/sys/net/bridge/bridge-nf-call-iptables || echo "MISSING bridge-nf-call-iptables"
cat /proc/sys/net/bridge/bridge-nf-call-ip6tables || echo "MISSING bridge-nf-call-ip6tables"
echo "--- host ipv6 ---"
cat /proc/net/if_inet6 || echo "no /proc/net/if_inet6 (IPv6 disabled)"
- name: Build lxc-exec
working-directory: src
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: cargo build --release -p lxc --bin lxc-exec
# MXC_LXC_TESTS_REQUIRE_EXECUTION turns an honest skip into a failure.
# On a developer box a missing ip6tables is a reason to run what you can.
# Here the runner is provisioned specifically to execute this suite, so a
# skip means a prerequisite disappeared and the gate would go green while
# testing nothing.
- name: Run LXC E2E suite
env:
MXC_LXC_TESTS_REQUIRE_EXECUTION: "1"
run: sudo --preserve-env=MXC_LXC_TESTS_REQUIRE_EXECUTION bash tests/scripts/run_lxc_all_tests.sh
- name: Show leftover firewall state on failure
if: failure()
run: |
echo "--- FORWARD chain ---"
sudo iptables -S FORWARD || true
sudo ip6tables -S FORWARD || true
echo "--- MXC chains ---"
sudo iptables -S | grep -E '^-N MXC-' || echo "none"
sudo ip6tables -S | grep -E '^-N MXC-' || echo "none"
- name: Upload logs on failure
if: failure() || cancelled()
uses: actions/upload-artifact@v6
with:
name: lxc-e2e-logs-${{ github.event.pull_request.number || github.run_number }}
retention-days: 7
path: |
logs/
**/*.log