siyuan-note/siyuan · error

host has no public IP:

Error message

host has no public IP: 

What it means

When the agent dialer gets a hostname, it resolves all IPs, silently skips private ones, and dials only public records. If every resolved record is private, zero dial attempts happen and this 'host has no public IP' error is returned. (If public records existed but every dial failed, the last dial error is returned instead — this message specifically means no public record existed.)

Source

Thrown at kernel/util/net.go:209

		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 afa823b6b4)

Solutions

  1. Verify resolution from this machine (dig +short <host>) and use a name with at least one public record
  2. Expose the internal service publicly (tunnel, reverse proxy) if the agent genuinely needs it
  3. Check /etc/hosts and VPN DNS for private overrides of the name
  4. Accept the restriction: pointing the agent at private networks is blocked by design (GHSA advisories)

Example fix

// before
resp, err := ssrfClient.Get("http://intranet-wiki.corp/page") // 'host has no public IP: intranet-wiki.corp'

// after — pre-check and use a public mirror
if err := util.CheckHostSSRF("intranet-wiki.corp"); err != nil {
    return errors.New("target resolves only to private IPs; use the public mirror")
}
resp, err := ssrfClient.Get("https://wiki.example.com/page")
Defensive patterns

Strategy: validation

Validate before calling

u, err := url.Parse(rawURL)
if err != nil {
    return err
}
ips, err := net.LookupIP(u.Hostname())
if err != nil {
    return err
}
hasPublic := false
for _, ip := range ips {
    if !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() {
        hasPublic = true
    }
}
if !hasPublic {
    return fmt.Errorf("%s resolves only to private IPs; agent access blocked", u.Hostname())
}

Type guard

func resolvesToPublicIP(host string) bool {
    ips, err := net.LookupIP(host)
    if err != nil {
        return false
    }
    for _, ip := range ips {
        if !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !ip.IsUnspecified() {
            return true
        }
    }
    return false
}

Try / catch

conn := ssrfSafeClient.Get(url)
if err != nil {
    if strings.Contains(err.Error(), "no public IP") {
        // DNS has no public record: fix the hostname or expose the service; do not retry
    }
}

Prevention

When it happens

Trigger: Agent fetching a hostname whose DNS returns only private A/AAAA records: intranet names, docker-service names like http://db:5432, *.local, split-horizon DNS, a public name overridden to a LAN IP by /etc/hosts or VPN DNS, or 'localhost' itself.

Common situations: Corporate intranet hostnames; docker-compose service discovery; VPN split DNS; captive portals rewriting DNS; hosts-file overrides during testing.

Related errors


AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18). Data as JSON: /api/errors/d55c3f11d16f37f8. Report an issue: GitHub.