
本文介绍在 Go 中检测 URL 目标 IP 是否属于私有(RFC1918)、环回、链路本地或唯一本地 IPv6 等不可路由地址空间,并通过自定义 http.Transport 在发起 HTTP 请求前实时拦截,防止 SSRF 攻击。
本文介绍在 go 中检测 url 目标 ip 是否属于私有(rfc1918)、环回、链路本地或唯一本地 ipv6 等不可路由地址空间,并通过自定义 `http.transport` 在发起 http 请求前实时拦截,防止 ssrf 攻击。
在构建接收用户输入 URL 并主动发起 HTTP 请求的服务(如 Webhook 代理、URL 预览、爬虫网关)时,若未校验目标地址归属,极易遭受服务端请求伪造(SSRF)攻击——攻击者可诱导服务访问内网资源(如 http://10.0.0.1:8080/admin 或 http://127.0.0.1:2375),造成敏感信息泄露或内网横向渗透。
最稳妥的防御方式不是仅依赖 DNS 解析后检查 IP,而是将地址校验深度集成到 HTTP 客户端流程中:通过自定义 http.Transport.DialContext,在 TCP 连接建立前获取解析后的 IP 地址并执行私有网络判定。
以下是一个生产就绪的实现方案:
package main
import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
)
var privateIPBlocks []*net.IPNet
func init() {
for _, cidr := range []string{
"127.0.0.0/8", // IPv4 loopback
"10.0.0.0/8", // RFC1918
"172.16.0.0/12", // RFC1918
"192.168.0.0/16", // RFC1918
"169.254.0.0/16", // RFC3927 link-local
"::1/128", // IPv6 loopback
"fe80::/10", // IPv6 link-local
"fc00::/7", // IPv6 unique local addr
} {
_, block, err := net.ParseCIDR(cidr)
if err != nil {
panic(fmt.Errorf("failed to parse CIDR %q: %v", cidr, err))
}
privateIPBlocks = append(privateIPBlocks, block)
}
}
func isPrivateIP(ip net.IP) bool {
// 先利用标准库快速判断常见类型(性能友好)
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return true
}
// 再匹配预加载的 CIDR 块(覆盖 RFC1918 + IPv6 ULA 等)
for _, block := range privateIPBlocks {
if block.Contains(ip) {
return true
}
}
return false
}
// SafeTransport 是一个带私有地址拦截能力的 http.Transport
type SafeTransport struct {
http.Transport
}
func (t *SafeTransport) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, fmt.Errorf("invalid address %q: %v", addr, err)
}
// 解析域名 → 获取所有 IP(支持 IPv4/IPv6 双栈)
ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, fmt.Errorf("DNS resolution failed for %q: %v", host, err)
}
for _, ipaddr := range ips {
ip := ipaddr.IP
if isPrivateIP(ip) {
return nil, fmt.Errorf("refusing connection to private IP address %v (host: %s)", ip, host)
}
}
// 所有解析出的 IP 均合法,交由父 Transport 建立连接
return t.Transport.DialContext(ctx, network, addr)
}
// 使用示例
func main() {
client := &http.Client{
Transport: &SafeTransport{
Transport: http.DefaultTransport,
},
}
u, _ := url.Parse("http://192.168.1.100/test")
req, _ := http.NewRequest("GET", u.String(), nil)
resp, err := client.Do(req)
if err != nil {
// 此处将捕获类似:refusing connection to private IP address 192.168.1.100 (host: 192.168.1.100)
fmt.Println("Blocked:", err)
return
}
defer resp.Body.Close()
fmt.Printf("Success: %s\n", resp.Status)
}
✅ 关键优势:
-
零额外 DNS 请求:复用
http.Transport自身的 DNS 解析逻辑(避免重复解析); -
全协议覆盖:同时处理 IPv4 和 IPv6 的私有/保留地址(含
fc00::/7、fe80::/10等); -
深度集成:校验发生在
DialContext阶段,早于 TLS 握手与 HTTP 传输,杜绝绕过可能; - 错误语义清晰:返回明确错误而非静默失败,便于日志审计与监控告警。
⚠️ 注意事项:
- 若业务需访问特定内网服务(如 Kubernetes Service ClusterIP),应通过白名单机制绕过校验(例如基于 Hostname 或 Context Key 动态启用);
-
net.DefaultResolver可能受系统resolv.conf影响,高并发场景建议使用&net.Resolver{...}配置专用 DNS; - 对于含端口的 URL(如
example.com:8080),net.SplitHostPort已正确提取 host,无需额外处理。
该方案已在多个云原生网关项目中落地验证,是兼顾安全性、性能与可维护性的推荐实践。










