golang使用DoH解析域名

按照RFC 8484 规范,DoH服务器支持GET或POST两种方式。

当使用GET方法,唯一的变量"dns"被赋值为base64url编码的DNS请求内容。

复制代码
   These examples use a DoH service with a URI Template of
   "https://dnsserver.example.net/dns-query{?dns}" to resolve IN A
   records.

   The requests are represented as bodies with media type "application/
   dns-message".

   The first example request uses GET to request "www.example.com".

   :method = GET
   :scheme = https
   :authority = dnsserver.example.net
   :path = /dns-query?dns=AAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB
   accept = application/dns-message

当使用POST方法,DNS查询消息被包含在HTTP 请求的body中,

header需要包含 Content-Type application/dns-message。

复制代码
   The same DNS query for "www.example.com", using the POST method would
   be:

   :method = POST
   :scheme = https
   :authority = dnsserver.example.net
   :path = /dns-query
   accept = application/dns-message
   content-type = application/dns-message
   content-length = 33

   <33 bytes represented by the following hex encoding>
   00 00 01 00 00 01 00 00  00 00 00 00 03 77 77 77
   07 65 78 61 6d 70 6c 65  03 63 6f 6d 00 00 01 00
   01

DNS报文格式,具体请参看 RFC1035 ,本文不再赘述。

golang程序中dns报文封装推荐使用golang.org/x/net/dns/dnsmessage

下面先使用另一个库github.com/miekg/dns简要演示使用opendns DoH解析域名的GET和POST方式。

go 复制代码
package main
import (
	"bytes"
	"encoding/base64"
	"fmt"
	"github.com/miekg/dns"
	"io"
	"net/http"
)
// https://datatracker.ietf.org/doc/html/rfc8484#section-4.1.1
func main() {
	rfc8484Get()
	rfc8484Post()

}
func rfc8484Get() {
	query := dns.Msg{}
	query.SetQuestion("www.github.com.", dns.TypeA)
	dsnReq, _ := query.Pack()
	base64Query := base64.RawURLEncoding.EncodeToString(dsnReq)
	client := &http.Client{}
	//GET请求必须具有 ?dns=查询参数,该参数带有采用 Base64Url编码的DNS消息
	req, err := http.NewRequest("GET", "https://doh.opendns.com/dns-query?dns="+base64Query, nil)
	if err != nil {
		fmt.Printf("Send query error, err:%v\n", err)
		return
	}
	req.Header.Add("Accept", "application/dns-message")
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()
	bodyBytes, _ := io.ReadAll(resp.Body)
	response := dns.Msg{}
	response.Unpack(bodyBytes)
	fmt.Printf("Dns answer is :%v\n", response.String())
}

func rfc8484Post() {
	query := dns.Msg{}
	query.SetQuestion("www.github.com.", dns.TypeA)
	dsnReq, _ := query.Pack()
	client := &http.Client{}
	//POST请求正文为二进制DNS消息
	req, err := http.NewRequest("POST", "https://doh.opendns.com/dns-query", bytes.NewBuffer(dsnReq))
	if err != nil {
		fmt.Printf("Send query error, err:%v\n", err)
	}
	req.Header.Add("Accept", "application/dns-message")
	//POST头需要包含 Content-Type application/dns-message
	req.Header.Add("Content-Type", "application/dns-message")

	resp, err := client.Do(req)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()
	bodyBytes, _ := io.ReadAll(resp.Body)
	response := dns.Msg{}
	response.Unpack(bodyBytes)
	fmt.Printf("Dns answer is :%v\n", response.String())

}

上面response.String()直观显示了返回数据,下面再使用golang.org/x/net/dns/dnsmessage演示读取返回具体数据格式

go 复制代码
package main

import (
	"bytes"
	"fmt"
	"golang.org/x/net/dns/dnsmessage"
	"io"
	"math/rand"
	"net"
	"net/http"
	"os"
	"strings"
)

func main() {
	query := dnsmessage.Message{
		Header: dnsmessage.Header{
			ID:               uint16(rand.Intn(65535) + 1), // Unique identifier for the query
			Response:         false,                        // This is a query, not a response
			RecursionDesired: true,                         // Ask for recursive resolution
		},
		Questions: []dnsmessage.Question{
			{
				Name:  dnsmessage.MustNewName("www.github.com."), // Domain name to query
				Type:  dnsmessage.TypeA,                          // Query type (A record)
				Class: dnsmessage.ClassINET,                      // Internet class
			},
		},
	}

	queryBytes, err := query.Pack()
	if err != nil {
		fmt.Println("Failed to pack DNS query:", err)
		return
	}

	client := &http.Client{}
	//POST请求正文为二进制DNS消息
	req, err := http.NewRequest("POST", "https://doh.opendns.com/dns-query", bytes.NewBuffer(queryBytes))
	if err != nil {
		fmt.Printf("Send query error, err:%v\n", err)
		return
	}
	req.Header.Add("Accept", "application/dns-message")
	//POST头需要包含 Content-Type application/dns-message
	req.Header.Add("Content-Type", "application/dns-message")

	resp, err := client.Do(req)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()
	bodyBytes, _ := io.ReadAll(resp.Body)
	var dnsResponse dnsmessage.Message
	err = dnsResponse.Unpack(bodyBytes)
	if err != nil {
		fmt.Println("Failed to unpack DNS response:", err)
		os.Exit(1)
	}

	// 遍历响应资源记录
	for _, answer := range dnsResponse.Answers {
		switch answer.Header.Type {
		case dnsmessage.TypeA:
			fmt.Println("Type A Record:")
			fmt.Println("IP Address:", net.IP(answer.Body.(*dnsmessage.AResource).A[:]).String())
		case dnsmessage.TypeNS:
			fmt.Println("Type NS Record:")
			fmt.Println("Name Server:", answer.Body.(*dnsmessage.NSResource).NS.String())
		case dnsmessage.TypeCNAME:
			fmt.Println("Type CNAME Record:")
			fmt.Println("Canonical Name:", answer.Body.(*dnsmessage.CNAMEResource).CNAME.String())
		case dnsmessage.TypeSOA:
			fmt.Println("Type SOA Record:")
			soa := answer.Body.(*dnsmessage.SOAResource)
			fmt.Println("Primary Name Server:", soa.NS.String())
			fmt.Println("Responsible Person:", soa.MBox.String())
			fmt.Println("Serial Number:", soa.Serial)
			fmt.Println("Refresh Interval:", soa.Refresh)
			fmt.Println("Retry Interval:", soa.Retry)
			fmt.Println("Expire Limit:", soa.Expire)
			fmt.Println("Minimum TTL:", soa.MinTTL)
		case dnsmessage.TypePTR:
			fmt.Println("Type PTR Record:")
			fmt.Println("Pointer Domain Name:", answer.Body.(*dnsmessage.PTRResource).PTR.String())
		case dnsmessage.TypeMX:
			fmt.Println("Type MX Record:")
			mx := answer.Body.(*dnsmessage.MXResource)
			fmt.Println("Mail Exchange:", mx.MX.String())
			fmt.Println("Preference:", mx.Pref)
		case dnsmessage.TypeTXT:
			fmt.Println("Type TXT Record:")
			fmt.Println("Text Data:", strings.Join(answer.Body.(*dnsmessage.TXTResource).TXT, ""))
		case dnsmessage.TypeAAAA:
			fmt.Println("Type AAAA Record:")
			fmt.Println("IPv6 Address:", net.IP(answer.Body.(*dnsmessage.AAAAResource).AAAA[:]))
		case dnsmessage.TypeSRV:
			fmt.Println("Type SRV Record:")
			srv := answer.Body.(*dnsmessage.SRVResource)
			fmt.Println("Priority:", srv.Priority)
			fmt.Println("Weight:", srv.Weight)
			fmt.Println("Port:", srv.Port)
			fmt.Println("Target:", srv.Target.String())
		case dnsmessage.TypeOPT:
			fmt.Println("Type OPT Record:")
			// 一般情况下,OPT记录用于 DNS 扩展,不是常规资源记录。
		default:
			fmt.Println("Unhandled record type:", answer.Header.Type)
		}
	}
}
相关推荐
007php0071 小时前
Go语言面试:传值与传引用的区别及选择指南
java·开发语言·后端·算法·面试·golang·xcode
q567315233 小时前
手把手教你用Go打造带可视化的网络爬虫
开发语言·爬虫·信息可视化·golang
虚伪的空想家3 小时前
K8S的Pod为什么可以解析访问集群之外的域名地址
云原生·容器·kubernetes·dns·域名解析·pod·coredns
戎码江湖4 小时前
使用CI/CD部署后端项目(gin)
ci/cd·golang·gin·后端自动部署项目·自动化部署项目
二哈不在线5 小时前
代码随想录二刷之“贪心算法”~GO
算法·贪心算法·golang
君万7 小时前
【LeetCode每日一题】94. 二叉树的中序遍历 104. 二叉树的最大深度
算法·leetcode·golang
Craze_rd9 小时前
服务 HTTP 转 SRPC 技术方案
网络·网络协议·http·rpc·golang
尘鹄12 小时前
go 初始化组件最佳实践
后端·设计模式·golang
墩墩分墩12 小时前
【Go语言入门教程】 Go语言的起源与技术特点:从诞生到现代编程利器(一)
开发语言·后端·golang·go
ERP老兵-冷溪虎山16 小时前
Python/JS/Go/Java同步学习(第三篇)四语言“切片“对照表: 财务“小南“纸切片术切凭证到崩溃(附源码/截图/参数表/避坑指南/老板沉默术)
java·javascript·python·golang·中医编程·四语言同步学习·职场生存指南