{"record":{"id":"446d29335e26dbc2","repo":"siyuan-note/siyuan","slug":"host-has-no-public-ip-s","errorCode":null,"errorMessage":"host has no public IP: %s","messagePattern":"host has no public IP: (.+?)","errorType":"exception","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/8641553a1f07374001902d3ce773285db1292b2d/kernel/util/net.go#L191-L227","documentation":"When the host is a hostname (not a literal IP), this dialer resolves all its IPs and requires at least one public address to proceed. If every resolved IP is private/internal (or resolution returned nothing usable), it returns 'host has no public IP' to block SSRF via DNS rebinding.","triggerScenarios":"Fetching from a hostname whose DNS resolves only to private addresses (e.g. internal DNS names, hosts-file entries pointing a domain at 127.0.0.1, split-horizon DNS).","commonSituations":"Developers adding /etc/hosts entries mapping public-looking domains to localhost; self-hosted services reachable only via LAN DNS names; misconfigured DNS returning private IPs.","solutions":["Point the hostname at a genuinely public IP, or use the SafeMode-permitting dialer if private access is intended","Check DNS resolution (nslookup/dig the host) and remove private-only records","If testing locally, run without the hardened dialer or bind via a public-facing reverse proxy"],"exampleFix":"// before\n/etc/hosts: api.example.com 127.0.0.1  // dialer rejects\n// after\n# use the real public DNS record for api.example.com","handlingStrategy":"validation","validationCode":"// resolve first, then decide\nips, err := net.DefaultResolver.LookupIPAddr(ctx, host)\nif err != nil { return err }\nfor _, ip := range ips {\n    if !(ip.IP.IsPrivate() || ip.IP.IsLoopback()) { /* at least one public IP */ }\n}","typeGuard":null,"tryCatchPattern":"if _, err := doFetch(url); err != nil && strings.HasPrefix(err.Error(), \"host has no public IP\") {\n    return fmt.Errorf(\"%s resolves only to private addresses; use a public host\", url)\n}","preventionTips":["Keep /etc/hosts free of entries mapping public domains to 127.0.0.1","Verify DNS records with dig/nslookup when deploying internal names","Use split-horizon aware resolvers or public endpoints for hardened fetches"],"tags":["go","network","security","ssrf","dns"],"backgroundTag":"private-ip-blocked","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}