Add root-aware privileged CI test lane
All checks were successful
smoke / smoke (push) Successful in 29s

This commit is contained in:
Vibe Myass
2026-03-16 04:36:14 +00:00
parent 481d70ce72
commit 3b523b78df
5 changed files with 133 additions and 7 deletions

View File

@@ -37,6 +37,43 @@ jobs:
gcc --version | head -n 1
pkg-config --modversion libnftables
- name: Detect runner mode
run: |
set -euo pipefail
uid="$(id -u)"
echo "RUNNER_UID=$uid" >> "$GITHUB_ENV"
cap_net_admin=0
if [ -r /proc/self/status ]; then
cap_eff_hex="$(awk '/^CapEff:/ { print $2 }' /proc/self/status)"
if [ -n "${cap_eff_hex:-}" ]; then
cap_eff_value=$((16#$cap_eff_hex))
if (( (cap_eff_value & (1 << 12)) != 0 )); then
cap_net_admin=1
fi
fi
fi
echo "RUNNER_HAS_CAP_NET_ADMIN=$cap_net_admin" >> "$GITHUB_ENV"
if [ "$uid" -eq 0 ]; then
echo "RUNNER_IS_ROOT=1" >> "$GITHUB_ENV"
echo "Root runner detected (uid 0)."
if [ "$cap_net_admin" -eq 1 ]; then
echo "RUN_PRIVILEGED_TESTS=1" >> "$GITHUB_ENV"
echo "Privileged test lane enabled."
else
echo "RUN_PRIVILEGED_TESTS=0" >> "$GITHUB_ENV"
echo "::warning::Root runner detected, but CAP_NET_ADMIN is unavailable. Privileged tests will be skipped."
fi
else
echo "RUNNER_IS_ROOT=0" >> "$GITHUB_ENV"
echo "RUN_PRIVILEGED_TESTS=0" >> "$GITHUB_ENV"
echo "Non-root runner detected; smoke-only test lane enabled."
fi
- name: Install .NET SDK
run: |
set -euo pipefail
@@ -58,7 +95,13 @@ jobs:
set -euo pipefail
"$HOME/.dotnet/dotnet" build --no-restore
- name: Test
- name: Smoke tests
run: |
set -euo pipefail
"$HOME/.dotnet/dotnet" test LibNftables.slnx --no-build
LIBNFTABLES_RUN_PRIVILEGED_TESTS=0 "$HOME/.dotnet/dotnet" test LibNftables.slnx --no-build --filter "Category!=Privileged"
- name: Privileged tests
if: env.RUN_PRIVILEGED_TESTS == '1'
run: |
set -euo pipefail
LIBNFTABLES_RUN_PRIVILEGED_TESTS=1 "$HOME/.dotnet/dotnet" test LibNftables.slnx --no-build --filter "Category=Privileged"

View File

@@ -61,7 +61,7 @@ The test suite contains:
- Managed/unit tests that do not require a native runtime
- Native integration tests that self-gate when `libnftables` is unavailable
- Capability-dependent tests that only run when `CAP_NET_ADMIN` is available
- Privileged tests that only run when `LIBNFTABLES_RUN_PRIVILEGED_TESTS=1`, `uid == 0`, and `CAP_NET_ADMIN` is available
## Gitea Smoke CI
@@ -69,12 +69,13 @@ The repository includes a Gitea Actions smoke workflow at `.gitea/workflows/smok
- Trigger: push and pull request
- Runner label: `debian-13`
- Job model: non-root smoke verification
- Job model: root-aware smoke verification
- Workflow actions:
- bootstrap the .NET 10 SDK inside the job
- restore
- build
- run `dotnet test`
- always run the non-privileged smoke test lane
- run the privileged test lane when the runner process is `uid 0`
Important runner prerequisites:
@@ -85,7 +86,15 @@ Important runner prerequisites:
- `pkg-config`
- system-installed `libnftables` development/runtime packages discoverable by `pkg-config`
The job does not attempt package-manager installs or privilege escalation. It is intended to catch restore/build/test regressions, not to provide privileged nftables coverage.
The job does not attempt package-manager installs or privilege escalation. It is intended to catch restore/build/test regressions with opportunistic privileged smoke coverage on root runners.
The workflow auto-detects `id -u` at runtime:
- non-root runners stay on the smoke-only path
- root runners enable `LIBNFTABLES_RUN_PRIVILEGED_TESTS=1`
- root runners without effective `CAP_NET_ADMIN` emit a warning and skip the privileged lane
For local runs, `dotnet test` keeps privileged tests disabled by default. To opt in intentionally, set `LIBNFTABLES_RUN_PRIVILEGED_TESTS=1` in an environment where the process is running as root with `CAP_NET_ADMIN`.
## High-Level Example

View File

@@ -4,6 +4,58 @@ namespace LibNftables.Tests;
internal static class NativeTestSupport
{
private const string PrivilegedTestsEnvironmentVariable = "LIBNFTABLES_RUN_PRIVILEGED_TESTS";
internal static bool IsRunningAsRoot()
{
if (!OperatingSystem.IsLinux())
{
return false;
}
try
{
foreach (var line in File.ReadLines("/proc/self/status"))
{
if (!line.StartsWith("Uid:", StringComparison.Ordinal))
{
continue;
}
var parts = line["Uid:".Length..]
.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return parts.Length > 0 && parts[0] == "0";
}
}
catch
{
// If uid probing fails, keep tests conservative.
}
return false;
}
internal static bool PrivilegedTestsRequested()
{
var value = Environment.GetEnvironmentVariable(PrivilegedTestsEnvironmentVariable);
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
return value.Equals("1", StringComparison.Ordinal)
|| value.Equals("true", StringComparison.OrdinalIgnoreCase)
|| value.Equals("yes", StringComparison.OrdinalIgnoreCase);
}
internal static bool ShouldRunPrivilegedTests()
{
return PrivilegedTestsRequested()
&& IsRunningAsRoot()
&& HasCapNetAdmin();
}
internal static bool HasCapNetAdmin()
{
const int capNetAdminBit = 12;

View File

@@ -53,6 +53,7 @@ public sealed class NftContextTests
}
[Fact]
[Trait("Category", "Privileged")]
public void ValidDryRunCommand_CanExecuteAndBufferOutput()
{
if (!NativeTestSupport.TryCreateContext(out var ctx))
@@ -60,7 +61,7 @@ public sealed class NftContextTests
return;
}
if (!NativeTestSupport.HasCapNetAdmin())
if (!NativeTestSupport.ShouldRunPrivilegedTests())
{
return;
}

View File

@@ -71,6 +71,27 @@ public sealed class NftablesClientIntegrationTests
}
}
[Fact]
[Trait("Category", "Privileged")]
public void Snapshot_WithPrivilegedLane_ReturnsRuleset()
{
if (!CanCreateClient())
{
return;
}
if (!NativeTestSupport.ShouldRunPrivilegedTests())
{
return;
}
var client = new NftablesClient();
NftSnapshot snapshot = client.Snapshot();
Assert.False(string.IsNullOrWhiteSpace(snapshot.RulesetText));
}
[Fact]
public void ValidateRuleset_WithTypedSetDefinition_ReturnsValidResult()
{