用 Go 找到 Cloudflare 背后的源站 IP
用 Go 找到 Cloudflare 背后的源站 IP
你好,我是 Bobby。
cfsearch 是一个 Go 写的命令行工具,用来在网站接入 Cloudflare 或类似 CDN 后,从一批候选 IP 里筛出可能的源站地址。
Cloudflare 挡在目标站点前面时,DNS 查到的是边缘节点,而不是源站。cfsearch 做的事情,就是直接请求候选 IP,同时把 HTTP Host 头设置成目标域名,再根据响应内容判断这个 IP 是否值得继续验证。
它的实现很简单。核心代码在一个 main.go 里,主要依赖 Go 标准库,net/http 发请求,net 解析 IP 和 CIDR,sync 控制并发,time 处理超时。
这篇文章整理一下它的实现思路。
CDN 后面的源站为什么还能被找到
网站接入 Cloudflare 以后,普通 DNS 查询拿到的是 Cloudflare 的边缘节点地址。源站 IP 不会直接出现在 DNS 结果里。
但有些源站服务器仍然暴露在公网。只要你知道它的 IP,并且请求里带上正确的域名,服务器可能照常返回目标网站的内容。
这里用到的是 HTTP Host 头。
假设目标域名是 example.com,候选 IP 是 1.2.3.4。请求实际连接的是:
1 | |
但 HTTP 请求里带的是:
1 | |
很多 Web 服务器会根据 Host 决定把请求交给哪个虚拟主机处理。于是客户端虽然连的是裸 IP,服务端仍然可能按 example.com 的站点配置返回页面。
cfsearch 自动化的就是这个过程:
- 读取一批 IP 地址或 CIDR 网段。
- 对每个 IP 发起 HTTP 或 HTTPS 请求。
- 请求时把
Host设置成目标域名。 - 如果返回结果里出现目标域名,就把这个 IP 记录为可能的源站。
它不是在证明某个 IP 一定是源站,只是在大量地址里筛出值得继续验证的候选项。
单文件结构适合这种小工具
cfsearch 没有拆出 cmd/、internal/、pkg/ 这类目录,所有逻辑都放在一个 main.go 文件里。
对这种只做一件事的 CLI 来说,这个选择并不奇怪。它没有复杂领域模型,也没有多种运行模式。用户传入目标域名、IP 文件或 CIDR,程序并发扫描,最后把命中的结果输出。
如果为了”项目结构完整”硬拆包,反而会把阅读成本抬高。
cfsearch 支持的参数也比较贴近扫描过程本身,比如:
1 | |
这些参数对应几个关键问题:扫哪个域名,用 HTTP 还是 HTTPS,开多少并发,请求等多久,从文件读 IP 还是直接展开 CIDR。
并发扫描靠 goroutine 和 WaitGroup
逐个 IP 顺序请求会很慢。大部分地址不会响应,或者会等到超时。cfsearch 用 goroutine 把请求并发发出去,再用 WaitGroup 等待扫描结束。
它还用了一个带缓冲的 channel 当并发限制:
1 | |
这个写法很常见。workers 控制同时进行的请求数量,避免一下子启动过多 goroutine,把本机文件描述符、网络连接或对端防护打爆。
工具类程序通常还要处理进度、输出文件、去重、错误打印这些细节。
关键点是 req.Host
整套探测方法里,最关键的一行是:
1 | |
Go 的 http.Request 里有 URL,也有单独的 Host 字段。请求 URL 可以是某个 IP,但 Host 字段可以覆盖实际发出去的 Host 头。
Go 官方 net/http 文档也明确写到,客户端请求里,Host 可以用来覆盖要发送的 Host header。
所以 cfsearch 的请求构造大概是这样:
1 | |
这段代码把两个概念分开了,网络连接的目标是 IP,HTTP 层声明的目标站点是域名。
平时写业务代码时,URL.Host 和请求里的 Host 通常一致,开发者很少需要单独改 req.Host。
怎么判断一个 IP 是否是源站
cfsearch 收到响应后,会先看状态码。
如果返回 200,它读取响应 body 的一小段内容,然后检查里面是否包含目标域名:
1 | |
这里用了 io.LimitReader,只读前 256 字节。它不需要下载整个页面,只想快速判断返回内容里有没有目标域名。
如果遇到跳转,工具会看 Location 头里是否包含目标域名:
1 | |
CIDR 展开用 net.ParseCIDR
除了单个 IP,cfsearch 也支持传入 CIDR 网段。比如你给它一个 /24,它会把这个网段里的地址逐个送进扫描队列。
Go 标准库已经提供了 net.ParseCIDR:
1 | |
这里有一个小细节,代码会复制一份 ip 再发送到 channel。因为循环里的 ip 会不断自增,如果直接把同一个底层切片传出去,后面的 goroutine 可能看到已经变化的地址。复制一份可以避免这个问题。
自增 IP 的函数也很短:
1 | |
写这种网络工具时,IP 解析和 CIDR 判断可以交给 net,HTTP 请求和超时控制可以交给 net/http。
超时和连接配置不能省
扫描大量 IP 时,大多数请求会失败。失败并不麻烦,麻烦的是连接或读取一直等不到结果。
cfsearch 创建了一个自定义 http.Client,给整个请求设置超时,也给底层拨号设置超时:
1 | |
Timeout 用来限制单次请求生命周期。DialContext 里的超时限制 TCP 连接建立过程。CheckRedirect 返回 http.ErrUseLastResponse,表示不要自动跟随跳转,这样程序可以自己读取 Location 头。
HTTPS 扫描还有一个额外问题,你直接连接的是 IP,但证书通常签给域名。证书校验会失败,所以代码里设置了 InsecureSkipVerify: true。
这个配置在普通业务代码里应该非常谨慎,很多场景不能用。放在这类授权安全测试工具里,它解决的是“我知道域名和 IP 不匹配,但我仍然要观察服务响应”的问题。
使用时要注意什么
这类工具只能用于你自己拥有或得到授权测试的系统。它适合检查自己的源站是否错误暴露在公网,或者在授权渗透测试中验证 CDN 配置。未经授权地寻找别人的源站 IP,性质完全不同。实现上,cfsearch 用 workers 参数控制并发,避免触发对端滥用检测;HTTPS 直连 IP 时会跳过证书验证,因为证书签给域名而不是 IP,这个配置只是为了让请求能发出去,不能当作正常浏览器访问的安全判断。
把网站接到 Cloudflare 之后,源站最好只允许 Cloudflare 的回源 IP 段访问,或者放到私有网络后面。
否则 CDN 隐藏了 DNS 层入口,但源站本身仍然可能被直接访问。
值得看的地方
cfsearch 生成候选 IP,带着伪造的 Host 发请求,再根据响应做简单匹配。
单文件结构让读者可以从参数解析一直读到请求发送和结果输出,标准库覆盖了大部分工作,goroutine 和 channel 用在最直接的位置,没有为了”工程化”拆出一堆暂时用不上的抽象。
如果你平时写的是 HTTP 服务端代码,也可以看看这种客户端工具。它会让你重新注意到几个平时容易忽略的点,req.Host 和 URL.Host 可以不同,http.Client 的超时要明确设置,响应 body 要关闭,CIDR 展开时要注意 net.IP 的底层切片。
资料:
- cfsearch:https://github.com/internetkafe/cfsearch
- Go
net/http文档:https://pkg.go.dev/net/http#Request