Skip to content

Commit a8ab5f3

Browse files
committed
test: fail on hidden offline egress
Count cgroup-scoped firewall rejects and fail the offline test harness with bounded aggregate diagnostics. Inject the gen-audio GGUF probe so fixture-backed importer tests do not attempt real network access. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com>
1 parent d417776 commit a8ab5f3

3 files changed

Lines changed: 58 additions & 15 deletions

File tree

core/gallery/importers/importers_suite_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ var _ = BeforeSuite(func() {
6565
restoreMTP := importers.SetMTPProbeForTest(func(context.Context, string) (*gguf.GGUFFile, error) {
6666
return nil, errors.New("remote GGUF probing disabled in fixture-backed importer tests")
6767
})
68+
restoreGenAudio := importers.SetGenAudioProbeForTest(func(context.Context, string) (*gguf.GGUFFile, error) {
69+
return nil, errors.New("remote GGUF probing disabled in fixture-backed importer tests")
70+
})
6871
DeferCleanup(func() {
72+
restoreGenAudio()
6973
restoreMTP()
7074
restoreMetadata()
7175
})

core/gallery/importers/llama-cpp.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ var (
2424
parseRemoteGGUF = func(ctx context.Context, url string) (*gguf.GGUFFile, error) {
2525
return gguf.ParseGGUFFileRemote(ctx, url, gguf.SkipLargeMetadata())
2626
}
27+
parseRemoteGenAudioGGUF = func(ctx context.Context, url string) (*gguf.GGUFFile, error) {
28+
return gguf.ParseGGUFFileRemote(ctx, url)
29+
}
2730
)
2831

2932
// SetMTPProbeForTest replaces the remote GGUF header reader and returns a
@@ -34,6 +37,14 @@ func SetMTPProbeForTest(probe func(context.Context, string) (*gguf.GGUFFile, err
3437
return func() { parseRemoteGGUF = previous }
3538
}
3639

40+
// SetGenAudioProbeForTest replaces the remote projector header reader and
41+
// returns a restore function. It must only be called during serial suite setup.
42+
func SetGenAudioProbeForTest(probe func(context.Context, string) (*gguf.GGUFFile, error)) func() {
43+
previous := parseRemoteGenAudioGGUF
44+
parseRemoteGenAudioGGUF = probe
45+
return func() { parseRemoteGenAudioGGUF = previous }
46+
}
47+
3748
type LlamaCPPImporter struct{}
3849

3950
func (i *LlamaCPPImporter) Name() string { return "llama-cpp" }
@@ -465,7 +476,7 @@ func maybeApplyTTSUsecase(modelConfig *config.ModelConfig, cfg *gallery.ModelCon
465476
}
466477
}()
467478

468-
f, err := gguf.ParseGGUFFileRemote(ctx, probeURL)
479+
f, err := parseRemoteGenAudioGGUF(ctx, probeURL)
469480
if err != nil {
470481
xlog.Debug("[tts-importer] failed to read remote mmproj header for gen-audio detection", "uri", probeURL, "error", err)
471482
return

scripts/run-test-linux-offline.sh

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,56 @@ root=$(cd "$(dirname "$0")/.." && pwd)
1515
group="localai-test-$$"
1616
cgroup="/sys/fs/cgroup/$group"
1717
parent_cgroup="/sys/fs/cgroup$(awk -F: '$1 == "0" {print $3}' /proc/self/cgroup)"
18+
chain="LAI_TEST_$$"
1819

1920
sudo mkdir "$cgroup"
2021
cleanup() {
2122
echo $$ | sudo tee "$parent_cgroup/cgroup.procs" >/dev/null 2>&1 || true
22-
sudo iptables -D OUTPUT -m cgroup --path "$group" -j REJECT 2>/dev/null || true
23-
sudo iptables -D OUTPUT -m cgroup --path "$group" -d 192.168.0.0/16 -j ACCEPT 2>/dev/null || true
24-
sudo iptables -D OUTPUT -m cgroup --path "$group" -d 172.16.0.0/12 -j ACCEPT 2>/dev/null || true
25-
sudo iptables -D OUTPUT -m cgroup --path "$group" -d 10.0.0.0/8 -j ACCEPT 2>/dev/null || true
26-
sudo iptables -D OUTPUT -m cgroup --path "$group" -d 127.0.0.0/8 -j ACCEPT 2>/dev/null || true
27-
sudo ip6tables -D OUTPUT -m cgroup --path "$group" -d ::1/128 -j ACCEPT 2>/dev/null || true
28-
sudo ip6tables -D OUTPUT -m cgroup --path "$group" -j REJECT 2>/dev/null || true
23+
sudo iptables -D OUTPUT -m cgroup --path "$group" -j "$chain" 2>/dev/null || true
24+
sudo iptables -F "$chain" 2>/dev/null || true
25+
sudo iptables -X "$chain" 2>/dev/null || true
26+
sudo ip6tables -D OUTPUT -m cgroup --path "$group" -j "$chain" 2>/dev/null || true
27+
sudo ip6tables -F "$chain" 2>/dev/null || true
28+
sudo ip6tables -X "$chain" 2>/dev/null || true
2929
sudo rmdir "$cgroup" 2>/dev/null || true
3030
}
3131
trap cleanup EXIT INT TERM
3232

33-
sudo iptables -I OUTPUT 1 -m cgroup --path "$group" -j REJECT
34-
sudo iptables -I OUTPUT 1 -m cgroup --path "$group" -d 192.168.0.0/16 -j ACCEPT
35-
sudo iptables -I OUTPUT 1 -m cgroup --path "$group" -d 172.16.0.0/12 -j ACCEPT
36-
sudo iptables -I OUTPUT 1 -m cgroup --path "$group" -d 10.0.0.0/8 -j ACCEPT
37-
sudo iptables -I OUTPUT 1 -m cgroup --path "$group" -d 127.0.0.0/8 -j ACCEPT
38-
sudo ip6tables -I OUTPUT 1 -m cgroup --path "$group" -j REJECT
39-
sudo ip6tables -I OUTPUT 1 -m cgroup --path "$group" -d ::1/128 -j ACCEPT
33+
sudo iptables -N "$chain"
34+
sudo iptables -A "$chain" -d 127.0.0.0/8 -j ACCEPT
35+
sudo iptables -A "$chain" -d 10.0.0.0/8 -j ACCEPT
36+
sudo iptables -A "$chain" -d 172.16.0.0/12 -j ACCEPT
37+
sudo iptables -A "$chain" -d 192.168.0.0/16 -j ACCEPT
38+
sudo iptables -A "$chain" -j REJECT
39+
sudo iptables -I OUTPUT 1 -m cgroup --path "$group" -j "$chain"
40+
41+
sudo ip6tables -N "$chain"
42+
sudo ip6tables -A "$chain" -d ::1/128 -j ACCEPT
43+
sudo ip6tables -A "$chain" -j REJECT
44+
sudo ip6tables -I OUTPUT 1 -m cgroup --path "$group" -j "$chain"
4045
echo $$ | sudo tee "$cgroup/cgroup.procs" >/dev/null
4146

47+
set +e
4248
LOCALAI_TEST_KERNEL_ACTIVE=1 "$root/scripts/run-test-offline.sh" "$@"
49+
command_status=$?
50+
set -e
51+
52+
# The final rule in each private chain is the external-egress REJECT. Reading
53+
# its counter makes even an optional probe that swallows its network error a
54+
# hard, visible test failure. Report aggregate counters only: packet-by-packet
55+
# kernel logging can itself exhaust CI logs when a client retries aggressively.
56+
ipv4_blocked=$(sudo iptables -L "$chain" -v -x -n | awk '$3 == "REJECT" { print $1; exit }')
57+
ipv6_blocked=$(sudo ip6tables -L "$chain" -v -x -n | awk '$3 == "REJECT" { print $1; exit }')
58+
ipv4_blocked=${ipv4_blocked:-0}
59+
ipv6_blocked=${ipv6_blocked:-0}
60+
61+
if (( ipv4_blocked > 0 || ipv6_blocked > 0 )); then
62+
{
63+
echo 'offline-test: external network access was attempted and blocked'
64+
echo "offline-test: blocked packets: ipv4=$ipv4_blocked ipv6=$ipv6_blocked"
65+
echo 'offline-test: replace real network clients with fixtures or injected test doubles'
66+
} >&2
67+
exit 1
68+
fi
69+
70+
exit "$command_status"

0 commit comments

Comments
 (0)