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

  1. Use a hostname that resolves to at least one public IP for agent outbound requests
  2. Resolve the domain externally (dig +short @8.8.8.8) to inspect what IPs it returns and fix the DNS records if wrong
  3. If the service must stay internal, it is unreachable by design from this guarded path — expose a public endpoint instead
  4. 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

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


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)