amir20/dozzle · warning · errBlockedAddress
webhook target resolves to a blocked address range
Error message
webhook target resolves to a blocked address range
What it means
errBlockedAddress is returned when a webhook URL's DNS resolution points into a blocked IP range (loopback, link-local, 0.0.0.0/8, etc.). Dozzle refuses these to prevent SSRF against its own host services and cloud metadata endpoints (169.254.169.254), while deliberately allowing RFC1918 private LANs for self-hosted webhooks.
Solutions
- Change the webhook URL to point at a routable address (a real LAN IP in RFC1918 space is allowed, e.g. 192.168.x.x, 10.x.x.x, 172.16-31.x.x).
- If the target genuinely is a local service, expose it on a private/LAN interface and use that address instead of 127.0.0.1/localhost.
- Check DNS: dig <host> and confirm none of the resolved IPs are loopback/link-local/0.0.0.0/8.
- If it was a misconfigured or malicious destination, remove or correct the destination in the notification settings.
Example fix
// before url: http://127.0.0.1:8123/api/webhook // after url: http://192.168.1.50:8123/api/webhook
Defensive patterns
Strategy: validation
Validate before calling
// resolve the webhook host before saving the destination
ips, err := net.LookupIP(host)
if err != nil || len(ips) == 0 { return fmt.Errorf("cannot resolve webhook host") }
for _, ip := range ips {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() {
return fmt.Errorf("webhook host %s resolves to blocked address %s", host, ip)
}
} Try / catch
resp, err := client.SendTest(dest)
if errors.Is(err, errBlockedAddress) {
// surface 'destination resolves to a blocked address range' to the user
} Prevention
- Never point webhooks at 127.0.0.1, localhost, 0.0.0.0, or 169.254.x.x
- Use RFC1918 LAN addresses for self-hosted webhook targets (those are allowed)
- Resolve the hostname and inspect IPs before saving a destination
- Be aware the block is SSRF protection; it will not be relaxed for loopback
When it happens
Trigger: safeDialContext inspects resolved IPs during dialing and isBlockedIP matches (webhook.go:132); SendTest propagates it. Any webhook send to a URL whose hostname resolves to loopback, link-local, or 0.0.0.0/8 triggers it; when every resolved IP is blocked, lastErr falls back to errBlockedAddress (webhook.go:142).
Common situations: Pointing the webhook at localhost/127.0.0.1 or the host's own metadata service (intentionally or via config mistake); a DNS name that resolves to 169.254.x.x; hostname like 'localhost' or 0.0.0.0 in the destination URL; DNS rebinding-ish setups where internal names resolve to loopback.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- invalid webhook URL scheme
- webhook notification failed
- Failed to save alert
- Preview failed
- cloud search failed
AI-assisted analysis of amir20/dozzle@d9463cbe21 (2026-09-07).
Data as JSON: /api/errors/9966f235f26c6674.
Report an issue: GitHub.
Appendix: source
Thrown at internal/notification/dispatcher/webhook.go:28
"net"
"net/http"
"net/url"
"slices"
"strings"
"text/template"
"time"
"github.com/amir20/dozzle/types"
"github.com/rs/zerolog/log"
)
// errBlockedAddress is returned when a webhook URL resolves to a blocked
// address range. Loopback and link-local addresses are refused to prevent SSRF
// against the Dozzle host's own services and cloud metadata endpoints
// (e.g. 169.254.169.254). RFC1918 private ranges are intentionally allowed —
// self-hosted webhooks (Home Assistant, internal Mattermost, etc.) commonly
// live on private LANs.
var errBlockedAddress = errors.New("webhook target resolves to a blocked address range")
// zeroNetV4 covers 0.0.0.0/8 — on Linux these route to the local host.
var zeroNetV4 = &net.IPNet{IP: net.IP{0, 0, 0, 0}, Mask: net.CIDRMask(8, 32)}
func isBlockedIP(ip net.IP) bool {
if isBlockedBaseIP(ip) {
return true
}
// IPv6 transition mechanisms (6to4, NAT64, Teredo, IPv4-compatible) embed an
// arbitrary IPv4 address that none of the checks above look at. Unwrap and
// re-check the embedded address so 2002:7f00:1::1 is treated as 127.0.0.1.
return slices.ContainsFunc(embeddedIPv4(ip), isBlockedBaseIP)
}
func isBlockedBaseIP(ip net.IP) bool {
if ip.IsLoopback() ||
ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() ||View on GitHub (pinned to d9463cbe21)