package dns
import (
"encoding/base64"
"errors"
"fmt"
"github.com/miekg/dns"
"io"
"net"
"net/http"
"time"
)
type Result struct {
Res * dns . Msg
Str string
}
type DnsClient interface {
Exchange ( req * dns . Msg , address string ) ( r * dns . Msg , rtt time . Duration , err error )
}
func QueryDns ( domain string , queryType string , serverType int , dnsServer string ) ( Result , error ) {
var c DnsClient
c = new ( dns . Client )
m := new ( dns . Msg )
if dnsServer == "" {
dnsServer = "223.5.5.5:53"
}
switch serverType {
case 1 :
c . ( * dns . Client ) . Net = "tcp"
case 2 :
c = & dns . Client {
Net : "tcp-tls" ,
Dialer : & net . Dialer {
Resolver : net . DefaultResolver ,
} ,
}
case 3 :
c = NewDoHClient ( WithTimeout ( 10 * time . Second ) )
}
switch queryType {
case "A" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeA )
case "CNAME" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeCNAME )
case "MX" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeMX )
case "NS" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeNS )
case "TXT" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeTXT )
case "SOA" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeSOA )
case "SRV" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeSRV )
case "AAAA" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeAAAA )
case "PTR" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypePTR )
case "ANY" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeANY )
case "CAA" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeCAA )
case "TLSA" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeTLSA )
case "DS" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeDS )
case "DNSKEY" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeDNSKEY )
case "NSEC" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeNSEC )
case "NSEC3" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeNSEC3 )
case "NSEC3PARAM" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeNSEC3PARAM )
case "RRSIG" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeRRSIG )
case "SPF" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeSPF )
case "SSHFP" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeSSHFP )
case "TKEY" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeTKEY )
case "TSIG" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeTSIG )
case "URI" :
m . SetQuestion ( dns . Fqdn ( domain ) , dns . TypeURI )
default :
return Result { } , errors . New ( "not support query type,only support A,CNAME,MX,NS,SOA,SRV,AAAA,PTR,ANY,CAA,TLSA,DS,DNSKEY,NSEC,NSEC3,NSEC3PARAM,RRSIG,SPF,SSHFP,TKEY,TSIG,URI" )
}
r , rtt , err := c . Exchange ( m , dnsServer )
if err != nil {
return Result { } , err
}
return Result {
Res : r ,
Str : r . String ( ) + "\n" + ";; RTT:\n" + fmt . Sprintf ( "%v milliseconds" , rtt . Milliseconds ( ) ) ,
} , nil
}
const DoHMediaType = "application/dns-message"
type clientOptions struct {
Timeout time . Duration // Timeout for one DNS query
}
type ClientOption func ( * clientOptions ) error
func WithTimeout ( t time . Duration ) ClientOption {
return func ( o * clientOptions ) error {
o . Timeout = t
return nil
}
}
type DoHClient struct {
opt * clientOptions
cli * http . Client
}
func NewDoHClient ( opts ... ClientOption ) * DoHClient {
o := new ( clientOptions )
for _ , f := range opts {
f ( o )
}
return & DoHClient {
opt : o ,
cli : & http . Client {
Timeout : o . Timeout ,
} ,
}
}
func ( c * DoHClient ) Exchange ( req * dns . Msg , address string ) ( r * dns . Msg , rtt time . Duration , err error ) {
var (
buf , b64 [ ] byte
begin = time . Now ( )
origID = req . Id
)
// Set DNS ID as zero accoreding to RFC8484 (cache friendly)
req . Id = 0
buf , err = req . Pack ( )
b64 = make ( [ ] byte , base64 . RawURLEncoding . EncodedLen ( len ( buf ) ) )
if err != nil {
return
}
base64 . RawURLEncoding . Encode ( b64 , buf )
// No need to use hreq.URL.Query()
hreq , _ := http . NewRequest ( "GET" , address + "?dns=" + string ( b64 ) , nil )
hreq . Header . Set ( "User-Agent" , "B612 DoH Client" )
hreq . Header . Add ( "Accept" , DoHMediaType )
resp , err := c . cli . Do ( hreq )
if err != nil {
return
}
defer resp . Body . Close ( )
content , err := io . ReadAll ( resp . Body )
if err != nil {
return
}
if resp . StatusCode != http . StatusOK {
err = errors . New ( "DoH query failed: " + string ( content ) )
return
}
r = new ( dns . Msg )
err = r . Unpack ( content )
r . Id = origID
rtt = time . Since ( begin )
return
}