Skip to content

Make an IPv4 address range in a network rule cover only its own addresses - #31

Merged
MarkusPaulsen merged 2 commits into
mainfrom
fix/netblocker-ipv4-prefix
Sep 13, 2026
Merged

MarkusPaulsen merged 2 commits into
mainfrom
fix/netblocker-ipv4-prefix

Conversation

@MarkusPaulsen

@MarkusPaulsen MarkusPaulsen commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A network rule naming a range of IPv4 addresses, such as 10.0.0.0/8, permitted every IPv4 address and the IPv6 loopback instead of only the addresses in the range. A range now covers exactly its own addresses, and a prefix length that cannot belong to an IPv4 range is refused. None of the shipped policies names a range, so they are unaffected.

Linked issues

No linked issues

1. Problem

The network filter compares an IPv4 address in its IPv6 form, ::ffff:a.b.c.d, whose first 96 bits are that fixed prefix. The rule parser kept an IPv4 prefix length as written, so 10.0.0.0/8 compared only the first 8 of those 128 bits. They are zero for every IPv4 address and for ::1, so the range permitted all of them. Prefix lengths up to 128 were accepted for IPv4 too, and an IPv4 address written in IPv6 notation with a short prefix, such as ::ffff:127.0.0.0/8, covered the same far larger space while reading like an IPv4 range.

The layer at fault is the LD_PRELOAD network filter, which decides which hosts a submission may contact. It let a submission reach addresses the policy does not name. Found while reviewing the filter for #30.

2. Improvement from the user's perspective

An instructor who allows a range, for instance a university network, now allows that range and not every IPv4 address. A rule whose prefix cannot describe an IPv4 range, such as /33 or ::ffff:127.0.0.0/8, is ignored instead of permitting everything.

3. Improvement from the maintainer's perspective

The parser now counts an IPv4 prefix from bit 96, named as a constant with the reason beside it, and validates it against 32 rather than 128. An IPv4 range in IPv6 notation needs a prefix of at least 96 bits, as in ::ffff:10.0.0.0/104; a native IPv6 range such as ::/8 keeps its meaning. Ten new checks hold ranges to both directions, IPv4 and IPv6, and they fail against the filter as it was on main, so the defect cannot come back unnoticed.

4. Testing manual

Prerequisites

  1. Docker, and a checkout of this branch. Nothing below uses --privileged, --cap-add or --security-opt.

Steps

  1. The filter's checks, from the repository root:
    docker run --rm -v "$PWD:/repo:ro" ubuntu:26.04 bash -c 'apt-get update -qq && apt-get install -y -qq gcc-14 libc6-dev binutils >/dev/null && cp -a /repo /w && bash /w/tests/network_cache_ports.sh'
  2. The same checks against the filter as it was on main:
    git show origin/main:ld_preloader/netblocker.c > /tmp/old-netblocker.c, then step 1 with -v /tmp/old-netblocker.c:/old.c:ro added and cp /old.c /w/ld_preloader/netblocker.c && placed before bash.
  3. The three acceptance suites exactly as tests/landlock-acceptance/README.md gives them, on a machine that runs the image natively.

Expected result

  1. The section address ranges shows ten ok lines, and the suite ends with 61 passed, 0 failed, 0 skipped.
  2. Five FAIL lines, all refusals the old filter did not make: an address outside an IPv4 range, the neighbour of a /32 (actual: errno:111, meaning the connection reached a port where nothing listens), a /33 prefix, ::ffff:127.0.0.0/8, and ::1 under an IPv4 range. The suite ends with 56 passed, 5 failed, 0 skipped.
  3. bestanden: 11, bestanden: 27 and bestanden: 13, each with fehlgeschlagen: 0.

Negative case (what must still be rejected)

127.0.0.1 under 10.0.0.0/8, 127.0.0.2 under 127.0.0.1/32, 127.0.0.1 under the invalid 127.0.0.0/33 and ::ffff:127.0.0.0/8, ::1 under 127.0.0.0/8, and 127.0.0.1 under ::1/128 are all refused, while 127.0.0.1 under 127.0.0.0/8, 127.0.0.1/32 and ::ffff:127.0.0.0/104, and ::1 under ::1/128, still connect (step 1).

Layers exercised

  • Filesystem layer
  • Network layer
  • Timeout layer
  • All three together, as a run uses them by default

Only the filter's rule parser changes. The acceptance suites in step 3 run all three layers with the rebuilt filter and pass unchanged, but their policies name no range, so they are a regression check rather than a check of this change.

5. Test case coverage regarding this PR

Suite Passed Failed Skipped What it covers regarding this PR
tests/network_cache_ports.sh, Ubuntu 26.04 container, arm64 and amd64 61 0 0 Ten new checks. Permitted: 127.0.0.1 under 127.0.0.0/8, 127.0.0.1/32 and ::ffff:127.0.0.0/104; ::1 under ::1/128. Refused: 127.0.0.1 under 10.0.0.0/8, 127.0.0.2 under 127.0.0.1/32, anything under 127.0.0.0/33 or ::ffff:127.0.0.0/8, ::1 under 127.0.0.0/8, 127.0.0.1 under ::1/128.
The same suite against netblocker.c from main 56 5 0 The five refusals the old filter did not make fail there, as they must.
The same suite against this branch's first commit 60 1 0 Only the ::ffff:127.0.0.0/8 refusal, added in the second commit, fails.
tests/landlock-acceptance/run-tests.sh, arm64 image, --network none 11 0 0 Unchanged, with the rebuilt filter.
tests/landlock-acceptance/extra-tests.sh, same 27 0 0 Unchanged.
tests/landlock-acceptance/phase-test.sh, same 13 0 0 Unchanged.

All runs used kernel 7.0 under Docker Desktop.

Breaking changes and migration

This narrows the network filter. A rule naming an IPv4 range no longer permits addresses outside it or IPv6 addresses. A rule with an IPv4 prefix longer than 32 bits, or an IPv4 address in IPv6 notation with a prefix under 96 bits, is ignored rather than permitting everything. No shipped policy names a range. A policy that relied on the old behaviour has to name the addresses it actually needs, and writes an IPv4 range in IPv6 notation with /96 or more.

Checklist

  • The title of this pull request describes the change, not the implementation.
  • I have self-reviewed the diff of this pull request.
  • Tests were added or updated for the behaviour changed here, in both directions: the forbidden case stays denied and the permitted case still works.
  • The change was exercised in a container started without --privileged, --cap-add or --security-opt, or the manual says why that was not possible.
  • CI is green, or every remaining failure is explained above.
  • No secrets, tokens or absolute local paths are contained in the diff.

Review progress

  • Code review
  • Manual test

The network filter compares an IPv4 address in its IPv6 form ::ffff:a.b.c.d,
whose first 96 bits are that fixed prefix, but the rule parser kept an IPv4
prefix length as written. 10.0.0.0/8 therefore compared the first 8 of 128 bits,
which are zero for every IPv4 address and for ::1, and permitted all of them.
Prefix lengths up to 128 were accepted for IPv4 as well.

An IPv4 prefix is now validated as 1 to 32 and counted from bit 96. An IPv6
prefix keeps its meaning and its limit of 128, and /0 stays invalid. Eight checks
hold ranges to both directions; against the old filter the four refusals fail.

Both committed libnetblocker.so copies are rebuilt with the pinned toolchain.
@MarkusPaulsen
MarkusPaulsen requested a review from a team September 13, 2026 12:45
@MarkusPaulsen
MarkusPaulsen requested review from a team and krusche as code owners September 13, 2026 12:45
A rule such as ::ffff:127.0.0.0/8 reads like an IPv4 range, but in IPv6 notation
the prefix counts from bit 0, so it covered every IPv4 address and ::1. An IPv4
range in that notation needs at least 96 bits, as in ::ffff:127.0.0.0/104, so a
shorter prefix on an IPv4-mapped address is now ignored, which leaves every such
address refused. A native IPv6 range such as ::/8 keeps its meaning.

The check uses ::ffff:127.0.0.0/8 and 127.0.0.1, so it fails whether the rule is
wrongly read as IPv6 /8 or as the IPv4 range 127.0.0.0/8. Both committed
libnetblocker.so copies are rebuilt with the pinned toolchain.
@MarkusPaulsen
MarkusPaulsen merged commit 7eadfa8 into main Sep 13, 2026
1 check passed
@MarkusPaulsen
MarkusPaulsen deleted the fix/netblocker-ipv4-prefix branch September 13, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant