Add root-aware privileged CI test lane
All checks were successful
smoke / smoke (push) Successful in 29s
All checks were successful
smoke / smoke (push) Successful in 29s
This commit is contained in:
@@ -37,6 +37,43 @@ jobs:
|
|||||||
gcc --version | head -n 1
|
gcc --version | head -n 1
|
||||||
pkg-config --modversion libnftables
|
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
|
- name: Install .NET SDK
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -58,7 +95,13 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
"$HOME/.dotnet/dotnet" build --no-restore
|
"$HOME/.dotnet/dotnet" build --no-restore
|
||||||
|
|
||||||
- name: Test
|
- name: Smoke tests
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
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"
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -61,7 +61,7 @@ The test suite contains:
|
|||||||
|
|
||||||
- Managed/unit tests that do not require a native runtime
|
- Managed/unit tests that do not require a native runtime
|
||||||
- Native integration tests that self-gate when `libnftables` is unavailable
|
- 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
|
## Gitea Smoke CI
|
||||||
|
|
||||||
@@ -69,12 +69,13 @@ The repository includes a Gitea Actions smoke workflow at `.gitea/workflows/smok
|
|||||||
|
|
||||||
- Trigger: push and pull request
|
- Trigger: push and pull request
|
||||||
- Runner label: `debian-13`
|
- Runner label: `debian-13`
|
||||||
- Job model: non-root smoke verification
|
- Job model: root-aware smoke verification
|
||||||
- Workflow actions:
|
- Workflow actions:
|
||||||
- bootstrap the .NET 10 SDK inside the job
|
- bootstrap the .NET 10 SDK inside the job
|
||||||
- restore
|
- restore
|
||||||
- build
|
- 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:
|
Important runner prerequisites:
|
||||||
|
|
||||||
@@ -85,7 +86,15 @@ Important runner prerequisites:
|
|||||||
- `pkg-config`
|
- `pkg-config`
|
||||||
- system-installed `libnftables` development/runtime packages discoverable by `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
|
## High-Level Example
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,58 @@ namespace LibNftables.Tests;
|
|||||||
|
|
||||||
internal static class NativeTestSupport
|
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()
|
internal static bool HasCapNetAdmin()
|
||||||
{
|
{
|
||||||
const int capNetAdminBit = 12;
|
const int capNetAdminBit = 12;
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ public sealed class NftContextTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
[Trait("Category", "Privileged")]
|
||||||
public void ValidDryRunCommand_CanExecuteAndBufferOutput()
|
public void ValidDryRunCommand_CanExecuteAndBufferOutput()
|
||||||
{
|
{
|
||||||
if (!NativeTestSupport.TryCreateContext(out var ctx))
|
if (!NativeTestSupport.TryCreateContext(out var ctx))
|
||||||
@@ -60,7 +61,7 @@ public sealed class NftContextTests
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!NativeTestSupport.HasCapNetAdmin())
|
if (!NativeTestSupport.ShouldRunPrivilegedTests())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
[Fact]
|
||||||
public void ValidateRuleset_WithTypedSetDefinition_ReturnsValidResult()
|
public void ValidateRuleset_WithTypedSetDefinition_ReturnsValidResult()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user