{"record":{"id":"d55c3f11d16f37f8","repo":"siyuan-note/siyuan","slug":"host-has-no-public-ip","errorCode":null,"errorMessage":"host has no public IP: ","messagePattern":"host has no public IP: ","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/net.go","lineNumber":209,"sourceCode":"\t\tips, err := net.DefaultResolver.LookupIPAddr(ctx, host)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar lastErr error\n\t\tfor _, ipAddr := range ips {\n\t\t\tif isPrivateIP(ipAddr.IP) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconn, err := dialer.DialContext(ctx, network, net.JoinHostPort(ipAddr.IP.String(), port))\n\t\t\tif err == nil {\n\t\t\t\treturn conn, nil\n\t\t\t}\n\t\t\tlastErr = err\n\t\t}\n\t\tif lastErr != nil {\n\t\t\treturn nil, lastErr\n\t\t}\n\t\treturn nil, errors.New(\"host has no public IP: \" + host)\n\t}\n}\n\n// isPrivateIP 判断 IP 是否为私网地址，含内嵌私网 IPv4 的 IPv6 过渡地址（NAT64、6to4、Teredo、IPv4 兼容）。\n// https://github.com/siyuan-note/siyuan/security/advisories/GHSA-qq8m-8p8v-x4xg\n// https://github.com/siyuan-note/siyuan/security/advisories/GHSA-rg26-cg95-gq6p\nfunc isPrivateIP(ip net.IP) bool {\n\tif ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() ||\n\t\tip.IsPrivate() || ip.IsUnspecified() || ip.IsMulticast() {\n\t\treturn true\n\t}\n\t// Go 标准库的分类方法不识别 IPv6 过渡地址，需按 RFC 内嵌格式提取其中的 IPv4 后再递归判断。\n\tif ip4 := extractEmbeddedIPv4(ip); nil != ip4 && !ip4.Equal(ip) {\n\t\treturn isPrivateIP(ip4)\n\t}\n\treturn false\n}\n","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/util/net.go#L191-L227","documentation":"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.)","triggerScenarios":"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.","commonSituations":"Corporate intranet hostnames; docker-compose service discovery; VPN split DNS; captive portals rewriting DNS; hosts-file overrides during testing.","solutions":["Verify resolution from this machine (dig +short <host>) and use a name with at least one public record","Expose the internal service publicly (tunnel, reverse proxy) if the agent genuinely needs it","Check /etc/hosts and VPN DNS for private overrides of the name","Accept the restriction: pointing the agent at private networks is blocked by design (GHSA advisories)"],"exampleFix":"// before\nresp, err := ssrfClient.Get(\"http://intranet-wiki.corp/page\") // 'host has no public IP: intranet-wiki.corp'\n\n// after — pre-check and use a public mirror\nif err := util.CheckHostSSRF(\"intranet-wiki.corp\"); err != nil {\n    return errors.New(\"target resolves only to private IPs; use the public mirror\")\n}\nresp, err := ssrfClient.Get(\"https://wiki.example.com/page\")","handlingStrategy":"validation","validationCode":"u, err := url.Parse(rawURL)\nif err != nil {\n    return err\n}\nips, err := net.LookupIP(u.Hostname())\nif err != nil {\n    return err\n}\nhasPublic := false\nfor _, ip := range ips {\n    if !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() {\n        hasPublic = true\n    }\n}\nif !hasPublic {\n    return fmt.Errorf(\"%s resolves only to private IPs; agent access blocked\", u.Hostname())\n}","typeGuard":"func resolvesToPublicIP(host string) bool {\n    ips, err := net.LookupIP(host)\n    if err != nil {\n        return false\n    }\n    for _, ip := range ips {\n        if !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !ip.IsUnspecified() {\n            return true\n        }\n    }\n    return false\n}","tryCatchPattern":"conn := ssrfSafeClient.Get(url)\nif err != nil {\n    if strings.Contains(err.Error(), \"no public IP\") {\n        // DNS has no public record: fix the hostname or expose the service; do not retry\n    }\n}","preventionTips":["Check DNS from the kernel machine (dig +short) for every host agents will fetch","Avoid intranet hostnames, docker service names, and *.local in agent URLs","Watch for /etc/hosts and VPN split-DNS overrides turning public names private","Publish internal services through tunnels when agents must consume them"],"tags":["ssrf","dns","network","security","agent"],"backgroundTag":"ssrf-protection","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}