fix(uptime): allow RFC 1918 IPs for admin-configured monitors
HTTP/HTTPS uptime monitors targeting LAN addresses (192.168.x.x, 10.x.x.x, 172.16.x.x) permanently reported 'down' on fresh installs because SSRF protection rejects RFC 1918 ranges at two independent checkpoints: the URL validator (DNS-resolution layer) and the safe dialer (TCP-connect layer). Fixing only one layer leaves the monitor broken in practice. - Add IsRFC1918() predicate to the network package covering only the three RFC 1918 CIDRs; 169.254.x.x (link-local / cloud metadata) and loopback are intentionally excluded - Add WithAllowRFC1918() functional option to both SafeHTTPClient and ValidationConfig; option defaults to false so existing behaviour is unchanged for every call site except uptime monitors - In uptime_service.go, pass WithAllowRFC1918() to both ValidateExternalURL and NewSafeHTTPClient together; a coordinating comment documents that both layers must be relaxed as a unit - 169.254.169.254 and the full 169.254.0.0/16 link-local range remain unconditionally blocked; the cloud-metadata error path is preserved - 21 new tests across three packages, including an explicit regression guard that confirms RFC 1918 blocks are still applied without the option set (TestValidateExternalURL_RFC1918BlockedByDefault) Fixes issues 6 and 7 from the fresh-install bug report.
This commit is contained in:
@@ -1054,3 +1054,135 @@ func TestIsIPv4MappedIPv6_EdgeCases(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// PR-3: WithAllowRFC1918 validation option tests
|
||||
|
||||
func TestValidateExternalURL_WithAllowRFC1918_Permits10x(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := ValidateExternalURL(
|
||||
"http://10.0.0.1",
|
||||
WithAllowHTTP(),
|
||||
WithAllowRFC1918(),
|
||||
WithTimeout(200*time.Millisecond),
|
||||
)
|
||||
// The key invariant: RFC 1918 bypass must NOT produce the blocking error.
|
||||
// DNS may succeed (returning the IP) or fail (network unavailable) — both acceptable.
|
||||
if err != nil && strings.Contains(err.Error(), "private ip addresses is blocked") {
|
||||
t.Errorf("AllowRFC1918 should skip 10.x.x.x blocking; got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateExternalURL_WithAllowRFC1918_Permits172_16x(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := ValidateExternalURL(
|
||||
"http://172.16.0.1",
|
||||
WithAllowHTTP(),
|
||||
WithAllowRFC1918(),
|
||||
WithTimeout(200*time.Millisecond),
|
||||
)
|
||||
if err != nil && strings.Contains(err.Error(), "private ip addresses is blocked") {
|
||||
t.Errorf("AllowRFC1918 should skip 172.16.x.x blocking; got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateExternalURL_WithAllowRFC1918_Permits192_168x(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := ValidateExternalURL(
|
||||
"http://192.168.1.1",
|
||||
WithAllowHTTP(),
|
||||
WithAllowRFC1918(),
|
||||
WithTimeout(200*time.Millisecond),
|
||||
)
|
||||
if err != nil && strings.Contains(err.Error(), "private ip addresses is blocked") {
|
||||
t.Errorf("AllowRFC1918 should skip 192.168.x.x blocking; got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateExternalURL_WithAllowRFC1918_BlocksMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
// 169.254.169.254 is the cloud metadata endpoint; it must stay blocked even
|
||||
// with AllowRFC1918 because 169.254.0.0/16 is not in rfc1918CIDRs.
|
||||
_, err := ValidateExternalURL(
|
||||
"http://169.254.169.254",
|
||||
WithAllowHTTP(),
|
||||
WithAllowRFC1918(),
|
||||
WithTimeout(200*time.Millisecond),
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected cloud metadata endpoint to be blocked, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateExternalURL_WithAllowRFC1918_BlocksLinkLocal(t *testing.T) {
|
||||
t.Parallel()
|
||||
// 169.254.1.1 is link-local but not the specific metadata IP; still blocked.
|
||||
_, err := ValidateExternalURL(
|
||||
"http://169.254.1.1",
|
||||
WithAllowHTTP(),
|
||||
WithAllowRFC1918(),
|
||||
WithTimeout(200*time.Millisecond),
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected link-local address to be blocked, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateExternalURL_WithAllowRFC1918_BlocksLoopback(t *testing.T) {
|
||||
t.Parallel()
|
||||
// 127.0.0.1 without WithAllowLocalhost must still be blocked.
|
||||
_, err := ValidateExternalURL(
|
||||
"http://127.0.0.1",
|
||||
WithAllowHTTP(),
|
||||
WithAllowRFC1918(),
|
||||
WithTimeout(200*time.Millisecond),
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected loopback to be blocked without AllowLocalhost, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "private ip addresses is blocked") &&
|
||||
!strings.Contains(err.Error(), "dns resolution failed") {
|
||||
t.Errorf("expected loopback blocking error; got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateExternalURL_RFC1918BlockedByDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Without WithAllowRFC1918, RFC 1918 addresses must still fail.
|
||||
_, err := ValidateExternalURL(
|
||||
"http://10.0.0.1",
|
||||
WithAllowHTTP(),
|
||||
WithTimeout(200*time.Millisecond),
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected RFC 1918 address to be blocked by default, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateExternalURL_WithAllowRFC1918_IPv4MappedIPv6Allowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
// ::ffff:192.168.1.1 is an IPv4-mapped IPv6 of an RFC 1918 address.
|
||||
// With AllowRFC1918, the mapped IPv4 is extracted and the RFC 1918 bypass fires.
|
||||
_, err := ValidateExternalURL(
|
||||
"http://[::ffff:192.168.1.1]",
|
||||
WithAllowHTTP(),
|
||||
WithAllowRFC1918(),
|
||||
WithTimeout(200*time.Millisecond),
|
||||
)
|
||||
if err != nil && strings.Contains(err.Error(), "private ip addresses is blocked") {
|
||||
t.Errorf("AllowRFC1918 should permit ::ffff:192.168.1.1; got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateExternalURL_WithAllowRFC1918_IPv4MappedMetadataBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
// ::ffff:169.254.169.254 maps to the cloud metadata IP; must stay blocked.
|
||||
_, err := ValidateExternalURL(
|
||||
"http://[::ffff:169.254.169.254]",
|
||||
WithAllowHTTP(),
|
||||
WithAllowRFC1918(),
|
||||
WithTimeout(200*time.Millisecond),
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected IPv4-mapped metadata address to be blocked, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user