NextDNS 有一个很神奇的匿名 EDNS 客户端子网
功能
他们也写了一篇文章来说明这个功能是怎样实现的,具体可以参考 https://medium.com/nextdns/how-we-made-dns-both-fast-and-private-with-ecs-4970d70401e5
简单来说这个功能可以在不暴露你的 IP 段的情况下提供准确的 ECS,对于追求隐私的人(隐私怪)来说这是一个非常好用的功能
我就在想,能不能自己实现一下这个功能呢
NextDNS 使用的递归解析器是 Unbound,Unbound 是不存在这种功能的,因此他们肯定是在 DNS 转发器上实现了这个功能(不过也有可能是魔改了 Unbound,不过我感觉可能性不大)
于是,在 GPT-4 的帮助下我得到了下面的 Go 代码
package main
import (
"log"
"net"
"github.com/miekg/dns"
"github.com/oschwald/geoip2-golang"
)
const (
upstreamDNS = "8.8.8.8:53"
listenAddr = ":53"
geoIPDBPath = "GeoLite2-City.mmdb"
)
func main() {
geoIP, err := geoip2.Open(geoIPDBPath)
if err != nil {
log.Fatalf("Failed to open GeoIP database: %v", err)
}
defer geoIP.Close()
handler := func(w dns.ResponseWriter, req *dns.Msg) {
clientIP, _, _ := net.SplitHostPort(w.RemoteAddr().String())
log.Printf("Received request from %s", clientIP)
// Log the incoming DNS request
log.Printf("Incoming DNS request: %s", req.String())
opt := req.IsEdns0()
if opt != nil {
log.Printf("EDNS0 options: %v", opt.Option)
} else {
opt = &dns.OPT{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT}}
req.Extra = append(req.Extra, opt)
}
// Lookup client's city from GeoIP database
clientCity := getCityFromIP(geoIP, clientIP)
log.Printf("Client city: %s", clientCity)
// Replace client's EDNS0 options with new subnet
newSubnet, newMask := getNewSubnetForCity(clientCity)
_, ipNet, _ := net.ParseCIDR(newSubnet)
newOpt := &dns.EDNS0_SUBNET{
Code: dns.EDNS0SUBNET,
Family: 1, // IPv4
SourceNetmask: uint8(newMask), // 设置子网掩码
Address: ipNet.IP,
}
opt.Option = append(opt.Option, newOpt)
// Forward the request to upstream
c := new(dns.Client)
res, _, err := c.Exchange(req, upstreamDNS)
if err != nil {
log.Printf("Failed to forward request: %v", err)
dns.HandleFailed(w, req)
return
}
// Log the received DNS response
log.Printf("Received DNS response: %s", res.String())
// Write response back to the client
w.WriteMsg(res)
}
for _, domain := range []string{"com.", "org.", "net.", "io.", "app.", "dev.", "example."} {
dns.HandleFunc(domain, handler)
}
log.Printf("Listening on %s...", listenAddr)
err = dns.ListenAndServe(listenAddr, "udp", nil)
if err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
func getCityFromIP(db *geoip2.Reader, ipStr string) string {
ip := net.ParseIP(ipStr)
record, err := db.City(ip)
if err != nil {
log.Printf("Failed to get city from IP %s: %v", ipStr, err)
return ""
}
city := record.City.Names["en"]
log.Printf("IP %s belongs to city %s", ipStr, city)
return city
}
var cityToSubnetMap = map[string]string{
"Taipei": "220.229.69.0",
"Central": "123.255.88.0",
// Add more cities and subnets here
}
func getNewSubnetForCity(city string) (string, int) {
subnet, ok := cityToSubnetMap[city]
if !ok {
// If the city is not in the map, return a default subnet
return "123.255.88.0/24", 24 // 修改为24位子网掩码
}
return subnet + "/24", 24 // 添加24位子网掩码
}
这段代码实现了从 GeoIP 数据库中获取到客户端的 IP 所对应的城市,然后从cityToSubnetMap
中获取到城市所对应的 ECS,如果匹配不到则将 ECS 变为123.255.88.0/24
(DNS 出口所在的地区)
不过 NextDNS 的匿名 EDNS 可以做到获取到同一 ASN 下的相同地区的 IP 段,应该是专门写了个 API 接口,我这里仅做测试就不弄得那么麻烦了,感兴趣的可以自己修改代码
效果
dig o-o.myaddr.l.google.com TXT
dig www.google.com