golang源码分析:将域名解析代理到自定义域名服务器

2022-12-17 16:31:11 浏览数 (1)

开发过程中,好多域名是内网域名,直接改/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

0 人点赞