Skip to content

Refuse connections to non-public addresses - #73

Merged
beetlebugorg merged 1 commit into
mainfrom
feat/network-guard
Aug 28, 2026
Merged

Refuse connections to non-public addresses#73
beetlebugorg merged 1 commit into
mainfrom
feat/network-guard

Conversation

@beetlebugorg

Copy link
Copy Markdown
Owner

What

Every fetch now passes a network guard before a socket opens. src/netguard.c answers three questions: is the scheme one this service speaks, is the resolved address one a real image origin can hold, and is the host one the operator named.

The guard has three tiers.

Tier one always applies, on every endpoint. A resolved address that is loopback, link local, multicast, unspecified, or reserved is refused. So is any scheme other than HTTP and HTTPS. So is a redirect chain longer than three hops. No deployment fetches an image from one of these, so refusing them cannot break an upgrade. The address check is a CURLOPT_OPENSOCKETFUNCTION callback, which runs once per connection, after the name resolves. A name that resolves to a refused address is refused whatever it is called, and every redirect hop arrives at the check on its own.

Tier two is DimsAllowPrivateAddresses, default On. It covers 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, and IPv6 unique local. Some deployments serve images from a private origin, so refusing these by default would break them.

Tier three is DimsAllowlistSigned, values log and enforce, default log. It decides whether the host allowlist applies to a signed request and to a redirect target. In log mode the guard records what enforcing would refuse and lets the request through, so an operator can fill the allowlist from the log before changing the value.

The sizer and the watermark overlay reach the fetch without passing the handler, so both now route through the guard. The sizer also gains the allowlist check the handler has always made.

The request record moves from apr_palloc to apr_pcalloc. The field list that follows it sets one field at a time, so a field it does not name held whatever the pool handed over.

Why

C1: the service passed the caller's URL to libcurl with no restriction. There was no protocol set, no redirect cap, and no address check. The host allowlist was the only gate, and the handler turned it off for every signed request, which is the path that carries the traffic. A caller reaching 169.254.169.254 gets IAM credentials on EC2 and ECS.

C2: the allowlist ran once, against the hostname in the request. CURLOPT_FOLLOWLOCATION was then set with no limit and no re-check, so an allowlisted origin returning 302 Location: http://127.0.0.1:11211/ sent the module to loopback.

M1: the sizer parsed the URL and fetched it directly. It consulted no allowlist and validated no signature, so any caller could ask the service to connect to any address and learn whether the response decoded as an image.

H8, in part: the request record was allocated uninitialized and filled in field by field.

Verify

make -C test test

166 cases pass, 155 before. No golden file changes.

The suite gains three servers. Port 8003 sets DimsAllowlistSigned enforce; port 8004 sets DimsAllowPrivateAddresses off, which refuses every host on the compose network and shows the check runs at the socket. The cases on port 8000 assert the shipped defaults, including the two that keep this a drop-in upgrade: a signed request still reaches a host outside the allowlist, and a redirect off the allowlist is still followed.

test/unit/test_netguard.c ports TestIsPublicAddress, TestHostAllowed, and TestValidateImageURL from ../go-dims/internal/core/network_test.go. The fetch cases in test/http/test_allowlist.c port the rest from ../go-dims/internal/source/http_test.go and lose their expected failure markers.

A refusal answers 400, which is the status go-dims returns for the same refusal.

Breaking

Nothing that a correct deployment does today stops working. Both new directives default to the behavior the module had before.

A deployment that fetches images from loopback, link local, or a reserved range now fails. So does one that uses a scheme other than HTTP or HTTPS, or that relies on a redirect chain longer than three hops.

DimsHostnameNotInWhitelist still answers 500 under DimsAllowlistSigned enforce, because a host outside the allowlist has reported that way since the module was written. The status for every refusal moves together, behind its own directive.

The configuration pages do not exist yet, so neither directive is documented outside the httpd -L help text. DimsMaxSourceBytes is in the same position.

- Reject a resolved address that is loopback, link local, multicast, unspecified, or reserved.
- Restrict the protocol set to HTTP and HTTPS.
- Cap the redirect chain at three hops.
- Add DimsAllowPrivateAddresses and DimsAllowlistSigned.
- Route the sizer and the watermark overlay through the same guard.
- Allocate the request record with apr_pcalloc.
@beetlebugorg
beetlebugorg merged commit 936a3ab into main Aug 28, 2026
4 checks passed
@beetlebugorg
beetlebugorg deleted the feat/network-guard branch August 28, 2026 02:08
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