siyuan-note/siyuan · warning
host has no public IP
Error message
host has no public IP: %s
What it means
In ssrfSafeDialContext, when a hostname resolves but every resolved IP is private (all candidates skipped), and no prior dial error was recorded, the dial fails with "host has no public IP: %s". This guarantees the agent can only reach genuinely public endpoints, closing DNS-rebinding attacks where a public hostname resolves to internal addresses.
Solutions
- Use a hostname that resolves to at least one public IP for agent outbound requests
- Resolve the domain externally (dig +short @8.8.8.8) to inspect what IPs it returns and fix the DNS records if wrong
- If the service must stay internal, it is unreachable by design from this guarded path — expose a public endpoint instead
- If this appears unexpectedly, check for DNS hijacking/rebinding — treat it as a security signal rather than working around it
Example fix
// before url := "https://internal.company.dev/api" // resolves only to 10.0.0.3 -> host has no public IP // after: publish the service with a public A record (or use the public gateway) url := "https://api.company.com/internal-proxy"
Defensive patterns
Strategy: validation
Validate before calling
// Go: resolve the hostname first and confirm it has a public IP
ips, err := net.DefaultResolver.LookupIPAddr(context.Background(), host)
if err == nil {
hasPublic := false
for _, a := range ips { if !isLikelyPrivate(a.IP) { hasPublic = true } }
if !hasPublic { return fmt.Errorf("%s resolves only to private IPs", host) }
} Prevention
- Resolve hostnames with an external resolver to verify they return public IPs before use
- Avoid internal-only DNS names in agent-facing configuration
- Treat 'host has no public IP' as a possible DNS-rebinding signal and investigate rather than bypass
- Use managed public endpoints for internal services instead of raw LAN DNS names
When it happens
Trigger: Dialing a hostname through ssrfSafeDialContext whose DNS records contain only private IPs — e.g. internal names like myservice.local, split-horizon DNS returning 192.168.x.x for a public-looking name, or DNS rebinding (rebind.tools-style) hostnames.
Common situations: Using a LAN-only DNS name in an agent URL while off the corporate network; a domain configured in split-horizon DNS; a hostname whose A/AAAA records all point to loopback/private ranges; malicious rebinding domains.
Related errors
- access to private/internal IP is prohibited
- failed to resolve host:
- generated image URL resolved to a private or invalid IP
- host has no public IP:
- ip address [ ] is prohibited
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/446d29335e26dbc2.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/util/net.go:228
ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
var lastErr error
for _, ipAddr := range ips {
if isPrivateIP(ipAddr.IP) {
continue
}
conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(ipAddr.IP.String(), port))
if err == nil {
return conn, nil
}
lastErr = err
}
if lastErr != nil {
return nil, lastErr
}
return nil, errors.New("host has no public IP: " + host)
}
}
// isPrivateIP 判断 IP 是否为私网地址,含内嵌私网 IPv4 的 IPv6 过渡地址(NAT64、6to4、Teredo、IPv4 兼容)。
// https://github.com/siyuan-note/siyuan/security/advisories/GHSA-qq8m-8p8v-x4xg
// https://github.com/siyuan-note/siyuan/security/advisories/GHSA-rg26-cg95-gq6p
func isPrivateIP(ip net.IP) bool {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() ||
ip.IsPrivate() || ip.IsUnspecified() || ip.IsMulticast() {
return true
}
// Go 标准库的分类方法不识别 IPv6 过渡地址,需按 RFC 内嵌格式提取其中的 IPv4 后再递归判断。
if ip4 := extractEmbeddedIPv4(ip); nil != ip4 && !ip4.Equal(ip) {
return isPrivateIP(ip4)
}
return false
}
View on GitHub (pinned to 9f775e8a12)