开发过程中,好多域名是内网域名,直接改/etc/host是一个选择,但是如果不及时改回去,在切换环境的时候会给我们排查问题带来很大干扰,如果能够实现一个代理,在运行的时候走指定代理服务器,代理服务器内部将域名解析发送到自定义的域名服务器上,如果自定义域名服务器解析不了,再走默认的域名服务器,是不是很爽?
先贴地址然后分析下如何实现:https://github.com/xiazemin/dns_proxy
首先我们需要定义一个dns服务器
package dns/*dig命令主要用来从 DNS 域名服务器查询主机地址信息。查找www.baidu.com的ip (A记录):命令:dig @127.0.0.1 www.baidu.com根据ip查找对应域名 (PTR记录):命令:dig @127.0.0.1 -x 220.181.38.150*/import ("fmt""net""golang.org/x/net/dns/dnsmessage")func Serve() {conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: 53})if err != nil {panic(err)}defer conn.Close()fmt.Println("Listing ...")for {buf := make([]byte, 512)_, addr, _ := conn.ReadFromUDP(buf)var msg dnsmessage.Messageif err := msg.Unpack(buf); err != nil {fmt.Println(err)continue}go ServerDNS(addr, conn, msg)}}// ServerDNS servefunc ServerDNS(addr *net.UDPAddr, conn *net.UDPConn, msg dnsmessage.Message) {// query infoif len(msg.Questions) < 1 {return}question := msg.Questions[0]var (queryTypeStr = question.Type.String()queryNameStr = question.Name.String()queryType = question.TypequeryName, _ = dnsmessage.NewName(queryNameStr))fmt.Printf("[%s] queryName: [%s]\n", queryTypeStr, queryNameStr)// find recordvar resource dnsmessage.Resourceswitch queryType {case dnsmessage.TypeA, dnsmessage.TypeAAAA:if rst, ok := addressBookOfA[queryNameStr]; ok {resource = NewAResource(queryName, rst)} else {fmt.Printf("not fount A record queryName: [%s] \n", queryNameStr)Response(addr, conn, msg)return}case dnsmessage.TypePTR:if rst, ok := addressBookOfPTR[queryName.String()]; ok {resource = NewPTRResource(queryName, rst)} else {fmt.Printf("not fount PTR record queryName: [%s] \n", queryNameStr)Response(addr, conn, msg)return}default:fmt.Printf("not support dns queryType: [%s] \n", queryTypeStr)return}// send responsemsg.Response = truemsg.Answers = append(msg.Answers, resource)Response(addr, conn, msg)}// Response returnfunc Response(addr *net.UDPAddr, conn *net.UDPConn, msg dnsmessage.Message) {packed, err := msg.Pack()if err != nil {fmt.Println(err)return}if _, err := conn.WriteToUDP(packed, addr); err != nil {fmt.Println(err)}}// NewAResource A recordfunc NewAResource(query dnsmessage.Name, a [4]byte) dnsmessage.Resource {return dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: query,Class: dnsmessage.ClassINET,TTL: 600,},Body: &dnsmessage.AResource{A: a,},}}// NewPTRResource PTR recordfunc NewPTRResource(query dnsmessage.Name, ptr string) dnsmessage.Resource {name, _ := dnsmessage.NewName(ptr)return dnsmessage.Resource{Header: dnsmessage.ResourceHeader{Name: query,Class: dnsmessage.ClassINET,},Body: &dnsmessage.PTRResource{PTR: name,},}}
用dig命令测试下
% dig @127.0.0.1 www.baidu.com; <<>> DiG 9.10.6 <<>> @127.0.0.1 www.baidu.com; (1 server found);; global options: +cmd;; Got answer:;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 30165;; flags: qr rd ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1;; WARNING: recursion requested but not available;; OPT PSEUDOSECTION:; EDNS: version: 0, flags:; udp: 4096;; QUESTION SECTION:;www.baidu.com. IN A;; ANSWER SECTION:www.baidu.com. 600 IN A 127.0.0.8;; Query time: 1 msec;; SERVER: 127.0.0.1#53(127.0.0.1);; WHEN: Sun Nov 13 18:38:20 CST 2022;; MSG SIZE rcvd: 58% dig @127.0.0.1 -x 127.0.0.8;; Warning: query response not set; <<>> DiG 9.10.6 <<>> @127.0.0.1 -x 127.0.0.8; (1 server found);; global options: +cmd;; Got answer:;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 34734;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1;; WARNING: recursion requested but not available;; OPT PSEUDOSECTION:; EDNS: version: 0, flags:; udp: 4096;; QUESTION SECTION:;8.0.0.127.in-addr.arpa. IN PTR;; Query time: 0 msec;; SERVER: 127.0.0.1#53(127.0.0.1);; WHEN: Sun Nov 13 18:38:38 CST 2022;; MSG SIZE rcvd: 51% dig @127.0.0.1 -x 127.0.0.9;; Warning: query response not set; <<>> DiG 9.10.6 <<>> @127.0.0.1 -x 127.0.0.9; (1 server found);; global options: +cmd;; Got answer:;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 28830;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1;; WARNING: recursion requested but not available;; OPT PSEUDOSECTION:; EDNS: version: 0, flags:; udp: 4096;; QUESTION SECTION:;9.0.0.127.in-addr.arpa. IN PTR;; Query time: 0 msec;; SERVER: 127.0.0.1#53(127.0.0.1);; WHEN: Sun Nov 13 18:38:42 CST 2022;; MSG SIZE rcvd: 51
有了dns服务器,首先我们要考虑如何在client请求里指定dns服务器,我们可以在创建链接的时候定义dialer,指定resolver的Dial方法
package mainimport ("context""fmt""net""net/http""net/url""os""time")func main() {fmt.Println(os.Getenv("HTTP_PROXY"))u, err := url.Parse(os.Getenv("HTTP_PROXY"))if err != nil {fmt.Println(err, u)}resolver := &net.Resolver{PreferGo: true, //否则不生效Dial: func(ctx context.Context, network, address string) (net.Conn, error) {return net.DialUDP("udp", nil, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 53})},}fmt.Println(resolver.LookupAddr(context.TODO(), net.ParseIP("127.0.0.8").String()))//[xiazemin.com.] <nil>fmt.Println(resolver.LookupAddr(context.TODO(), net.ParseIP("127.0.0.1").String()))//[localhost kubernetes.docker.internal.] <nil>dialer := &net.Dialer{Timeout: 1 * time.Second,Resolver: resolver,}client := http.Client{Transport: &http.Transport{DialContext: dialer.DialContext,TLSHandshakeTimeout: 10 * time.Second,// Proxy: http.ProxyURL(u),Proxy: http.ProxyFromEnvironment,},Timeout: 1 * time.Second,}req, _ := http.NewRequest("GET", "http://xiazemin.com:8080", &net.Buffers{[]byte("xiazemin http get")})fmt.Println(client.Do(req))}
可以定义一个简单的服务器测试下
package mainimport ("fmt""net/http")func main() {http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {fmt.Println("request from client and host :", r.Host)fmt.Fprintln(w, r.Body)})http.ListenAndServe(":8080", nil)}
% go run learn/dns/http/server/main.gorequest from client and host : xiazemin.com:8080% go run learn/dns/server/main.goListing ...[TypeAAAA] queryName: [xiazemin.com.][TypeA] queryName: [xiazemin.com.]% go run learn/dns/http/client/main.go&{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[87] Content-Type:[text/plain; charset=utf-8] Date:[Sun, 13 Nov 2022 13:01:18 GMT]] 0xc000028320 87 [] false false map[] 0xc00013c100 <nil>} <nil>
发现我们的dns服务器已经能解析我自己定义的域名xiazemin.com了,当然,我们的域名服务器也实现了反向查域名的能力,这里有个细节需要注意的是:in-addr.arpa里表达的ip就是反过来表达的,即
d.c.b.a.in-addr.arpa (IP地址是a.b.c.d)
这么骚的设计还是挺耐人寻味的。
然后我们可以定义一个tcp代理,在做转发之前嵌入我们的域名服务器解析地址,如果解析失败,尝试系统默认的解析方法
package tcpproxyimport ("bytes""context""fmt""io""log""net""net/url""strings""time")//https://www.51sjk.com/b123b258404/func Serve() {log.SetFlags(log.LstdFlags | log.Lshortfile)l, err := net.Listen("tcp", ":8081")if err != nil {log.Panic(err)}for {client, err := l.Accept()if err != nil {log.Panic(err)}go handleclientrequest(client)}}func handleclientrequest(client net.Conn) {if client == nil {return}defer client.Close()var b [1024]byten, err := client.Read(b[:])if err != nil {log.Println(err)return}var method, host, address stringfmt.Sscanf(string(b[:bytes.IndexByte(b[:], '\n')]), "%s%s", &method, &host)fmt.Println(method, host, address)hostporturl, err := url.Parse(host)fmt.Println(hostporturl)if err != nil {log.Println(err)return}if hostporturl.Opaque == "443" { //https访问address = hostporturl.Scheme + ":443"} else { //http访问if strings.Index(hostporturl.Host, ":") == -1 { //host不带端口, 默认80address = hostporturl.Host + ":80"} else {address = hostporturl.Host}}//获得了请求的host和port,就开始拨号吧dialer := net.Dialer{Resolver: &net.Resolver{PreferGo: true, //否则不生效Dial: func(ctx context.Context, network, address string) (net.Conn, error) {return net.DialUDP("udp", nil, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 53})},},Timeout: 1 * time.Second,}//net.Dialserver, err := dialer.Dial("tcp", address)if err != nil {log.Println(err, "retry default")server, err = net.Dial("tcp", address)if err != nil {log.Println(err)return}}if method == "connect" {fmt.Fprint(client, "http/1.1 200 connection established\r\n")} else {server.Write(b[:n])}//进行转发go io.Copy(server, client)io.Copy(client, server)}
做完以后尝试下
% HTTP_PROXY="http://localhost:8081" curl http://www.baidu.com

