开发过程中,好多域名是内网域名,直接改/etc/host是一个选择,但是如果不及时改回去,在切换环境的时候会给我们排查问题带来很大干扰,如果能够实现一个代理,在运行的时候走指定代理服务器,代理服务器内部将域名解析发送到自定义的域名服务器上,如果自定义域名服务器解析不了,再走默认的域名服务器,是不是很爽?
先贴地址然后分析下如何实现:https://github.com/xiazemin/dns_proxy
首先我们需要定义一个dns服务器
代码语言:javascript复制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.Message
if err := msg.Unpack(buf); err != nil {
fmt.Println(err)
continue
}
go ServerDNS(addr, conn, msg)
}
}
// ServerDNS serve
func ServerDNS(addr *net.UDPAddr, conn *net.UDPConn, msg dnsmessage.Message) {
// query info
if len(msg.Questions) < 1 {
return
}
question := msg.Questions[0]
var (
queryTypeStr = question.Type.String()
queryNameStr = question.Name.String()
queryType = question.Type
queryName, _ = dnsmessage.NewName(queryNameStr)
)
fmt.Printf("[%s] queryName: [%s]n", queryTypeStr, queryNameStr)
// find record
var resource dnsmessage.Resource
switch 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 response
msg.Response = true
msg.Answers = append(msg.Answers, resource)
Response(addr, conn, msg)
}
// Response return
func 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 record
func 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 record
func 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命令测试下
代码语言:javascript复制% 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方法
代码语言:javascript复制package main
import (
"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))
}
可以定义一个简单的服务器测试下
代码语言:javascript复制package main
import (
"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)
}
代码语言:javascript复制 % go run learn/dns/http/server/main.go
request from client and host : xiazemin.com:8080
% go run learn/dns/server/main.go
Listing ...
[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就是反过来表达的,即
代码语言:javascript复制d.c.b.a.in-addr.arpa (IP地址是a.b.c.d)
这么骚的设计还是挺耐人寻味的。
然后我们可以定义一个tcp代理,在做转发之前嵌入我们的域名服务器解析地址,如果解析失败,尝试系统默认的解析方法
代码语言:javascript复制package tcpproxy
import (
"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]byte
n, err := client.Read(b[:])
if err != nil {
log.Println(err)
return
}
var method, host, address string
fmt.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不带端口, 默认80
address = 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.Dial
server, 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 establishedrn")
} else {
server.Write(b[:n])
}
//进行转发
go io.Copy(server, client)
io.Copy(client, server)
}
做完以后尝试下
代码语言:javascript复制 % HTTP_PROXY="http://localhost:8081" curl http://www.baidu.com