#8 Go DNS 解析

2022-10-30

A 记录

package main

import (
    "fmt"
    "net"
)

func main() {
    // 方法 1
    // func ResolveIPAddr(network, address string) (*IPAddr, error)
    ipAddr, err := net.ResolveIPAddr("ip", "www.google.com")
    if err != nil {
        fmt.Println("解析IP地址失败:", err)

    } else {
        fmt.Println("IP地址是:", ipAddr.IP)
    }

    // 方法 2
    // func LookupMX(name string) ([]*MX, error)
    // func LookupTXT(name string) ([]string, error)
    // func LookupIP(host string) ([]IP, error)
    // func LookupHost(host string) ([]string, error) // only IPv4
    ips, err := net.LookupHost("www.google.com")
    if err != nil {
        fmt.Println("解析主机名失败:", err)
    } else {
        for _, ip := range ips {
            fmt.Println("IP地址是:", ip)
        }
    }
}

MX 记录

package main

import (
    "fmt"
    "net"
)

func main() {
    records, err := net.LookupMX("qq.com")
    if err != nil {
        fmt.Println("解析MX记录失败:", err)
    } else {
        fmt.Printf("%#v", records)
    }
}

指定 DNS 服务器

func queryMX(dns string, domain string) ([]*net.MX, error) {
    resolver := &net.Resolver{
        PreferGo: true,
        Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
            dialer := &net.Dialer{}
            return dialer.DialContext(ctx, "udp", dns)
        },
    }
    return resolver.LookupMX(context.Background(), domain)
}

完整版本

package main

import (
    "context"
    "fmt"
    "net"
    "strings"
    "time"
)

var dnsServers = []string{
    "223.5.5.5:53",
    "114.114.114.114:53",
    "8.8.8.8:53",
}

type IPType int

const (
    NotIP IPType = iota
    IPv4
    IPv6
)

const (
    DNSLookupTimeout = 500 * time.Millisecond // time.Duration
)

func GetIPType(host string) IPType {
    ip := net.ParseIP(host)
    if ip == nil {
        return NotIP
    }
    if ip.To4() != nil {
        return IPv4
    }
    if ip.To16() != nil {
        return IPv6
    }
    return NotIP
}

type Resolver struct {
    DNS      string
    Resolver *net.Resolver
}

func NewResolver(dns string) Resolver {
    resolver := Resolver{}
    resolver.DNS = dns
    resolver.Resolver = &net.Resolver{
        PreferGo: true,
        Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
            dialer := &net.Dialer{}
            return dialer.DialContext(ctx, "udp", dns)
        },
    }
    return resolver
}

func (r Resolver) QueryA(domain string) ([]string, error) {
    return r.Resolver.LookupHost(context.Background(), domain)
}

func (r Resolver) QueryMX(domain string) ([]*net.MX, error) {
    ctx, cancel := context.WithTimeout(context.Background(), DNSLookupTimeout)
    defer cancel()
    return r.Resolver.LookupMX(ctx, domain)
}

func (r Resolver) Query(domain string) ([]*net.MX, error) {
    addrs, err := r.QueryMX(domain)
    if err != nil {
        return nil, err
    }
    var result []*net.MX
    for _, addr := range addrs {
        // hostname := strings.TrimRight(addr.Host, ".")
        ipType := GetIPType(addr.Host)
        fmt.Printf("%s: %#v\n", addr.Host, ipType)
        if ipType == NotIP {
            ips, err := r.QueryA(addr.Host)
            if err != nil {
                continue
            }
            for _, ip := range ips {
                result = append(result, &net.MX{Host: ip, Pref: addr.Pref})
            }
        }
    }
    return result, nil
}

var resolvers map[string]Resolver

func Query(domain string) ([]*net.MX, error) {
    var addrs []*net.MX
    var err error
    for _, dns := range dnsServers {
        var r Resolver
        if r, ok := resolvers[dns]; !ok {
            r = NewResolver(dns)
            resolvers[dns] = r
        }
        addrs, err = r.Query(domain)
        if err == nil {
            break
        }
        fmt.Printf("Error: %s: %s: %#v\n", dns, domain, err)
    }
    return addrs, err
}

func QueryAsync(domain string, ch chan<- []*net.MX, errCh chan<- error) {
    addrs, err := Query(domain)
    if err != nil {
        errCh <- err
        return
    }
    ch <- addrs
}

func init() {
    resolvers = make(map[string]Resolver)
}

func main() {
    {
        domains := []string{"qq.com", "gmail.com", "google.com"}
        for _, domain := range domains {
            fmt.Println(strings.Repeat("=", 100))
            addrs, err := Query(domain)
            if err != nil {
                fmt.Printf("Error: %#v\n", err)
            } else {
                for _, addr := range addrs {
                    fmt.Printf("%#v\n", addr)
                }
            }
        }
    }

    {
        fmt.Println(strings.Repeat("=", 100))
        ch := make(chan []*net.MX)
        errCh := make(chan error)
        go QueryAsync("google.com", ch, errCh)
        select {
        case addrs := <-ch:
            fmt.Println("MX Records:")
            for _, addr := range addrs {
                fmt.Printf("%#v\n", addr)
            }
        case err := <-errCh:
            fmt.Println("Error:", err)
        }
    }
}

#4 DNS_PROBE_FINISHED_NXDOMAIN

2021-12-14
  1. 浏览器突然打不开 zhihu.com, 报 DNS_PROBE_FINISHED_NXDOMAIN
  2. Windows 网络诊断之后说是 DNS 不可用。
  3. 经过检查,使用 DHCP 获取到的 DNS 172.16.0.1
  4. 改成 AliDNS: 223.5.5.5, 223.6.6.6 之后就好了。
  5. 然后再改回默认的 DNS 发现也能正常访问了。

我应该在出现问题的时候先尝试 nslookup 一下,看看 DNS 解析出来的到底是个什么结果。
下次遇到再继续更新。

C:\Users\Administrator>ipconfig /all | findstr DNS
   主 DNS 后缀 . . . . . . . . . . . :
   连接特定的 DNS 后缀 . . . . . . . :
   DNS 服务器  . . . . . . . . . . . : 172.16.0.1
   连接特定的 DNS 后缀 . . . . . . . :
   DNS 服务器  . . . . . . . . . . . : fec0:0:0:ffff::1%1
   连接特定的 DNS 后缀 . . . . . . . :
   连接特定的 DNS 后缀 . . . . . . . :
   连接特定的 DNS 后缀 . . . . . . . :
   连接特定的 DNS 后缀 . . . . . . . :

C:\Users\Administrator>nslookup zhihu.com
服务器:  UnKnown
Address:  172.16.0.1

非权威应答:
名称:    zhihu.com
Address:  103.41.167.234

#3 DNS 基础

2019-07-22

域名

Class rdataclass

RESERVED0 = 0
IN = 1
INTERNET = 1
CH = 3
CHAOS = 3
HESIOD = 4
HS = 4
NONE = 254
ANY = 255

Type rdatatype

  • A IPv4 Address
  • AAAA IPv4 Address
  • CNAME Canonical Name,“规范名称”,其实就是别名,指向另一个域名。MX 记录和 NS 记录不能指向一个 CNAME 记录。
  • NS Name Server,权威 DNS 服务器位置。
  • MX Mail Exchange,邮件服务位置。
  • SRV 通用的服务类型记录,取代 MX 这种专用记录。
_xmpp-client._tcp.example.net. 86400 IN SRV 5        0      5222 example.net.
_xmpp-server._tcp.example.net. 86400 IN SRV 5        0      5269 example.net.
#                              TTL          Priority Weight Port
  • TXT
  • SOA Start Of Authority,权威记录起始
  • PTR 反解记录

反解记录

记录名称:IP 翻转过来,加上 .in-addr.arpa 后缀
记录类型:PTR

dig -x 106.75.80.125
dig +noall +answer -tPTR 125.80.75.106.in-addr.arpa

完整的 DNS 解析过程

  1. 找到权威服务器

Windows

nslookup
ipconfig /displaydns
ipconfig /flushdns

Linux

dig markjour.com A
dig @8.8.8.8 markjour.com

dig qq.com MX
dig MX qq.com
dig -t MX qq.com
dig +noall +answer qq.com
dig +short qq.com
dig qq.com ANY

dig +trace +nssearch markjour.com

DNS 请求和响应

  • header 2Bytes
Field Bits Desc
QR 1 query 0, reply 1
OPCODE 4 QUERY 0, IQUERY 1, STATUS 2
AA 1 Authoritative Answer
TC 1 TrunCation
RD 1 Recursion Desired
RA 1 Recursion Available
Z 3 Zero 填零,保留
RCODE 4 Response Code

RCODE:

  • NOERROR (0)
  • FORMERR (1, Format error)
  • SERVFAIL (2)
  • NXDOMAIN (3, Nonexistent domain)

  • body

  • question

    Field Bits Desc
    NAME 可变 资源名称
    TYPE 2 记录类型
    CLASS 2 Class Code

    CLASS Code rdata.class

    RESERVED0 = 0
    IN = 1
    INTERNET = 1
    CH = 3
    CHAOS = 3
    HESIOD = 4
    HS = 4
    NONE = 254
    ANY = 255
    
  • answer

  • authority
  • additional space

请求

响应

参考资料与拓展阅读

#2 关于 DNS(历史和未来)

2018-05-02

历史

从阿帕网 (ARPANET) 时代一直到互联网的早期,网络节点比较少,都是通过本地 hosts 文件来实现主机名到 IP 地址的映射。
根据维基百科的信息,斯坦福研究所负责维护了一个公共 hosts 文件,大家会找他同步 (rfc606, rfc608)。
PS: 这个时候如果有主机名重复了谁来管?打电话过去让他们改名?

这套机制一直运行了十几年,公共 hosts 文件已经变的很大了,变化也很频繁(IP 可能已经不再那么固定了),需要经常同步。这个时候,斯坦福研究所的网络压力也越来越大了。

后来人们开始设计域名和域名相关的公共设施 (rfc805, rfc830)。最后,在 1983 年,形成了下面两个 RFC 文档:

  • RFC 882, DOMAIN NAMES - CONCEPTS and FACILITIES
  • RFC 883, DOMAIN NAMES - IMPLEMENTATION and SPECIFICATION

几年后(1987),正式的 DNS 标准 RFC 1034 和 RFC 1035 推出。
这套标准一直运行到现在,可能有对其进行拓展(比如 DNS 记录类型不断添加,Unicode 字符引入),但是基本技术设计没有改变。

DNS 的管理权问题

https://zhidao.baidu.com/question/1386069665602139980.html

基本流程

比如本站域名 www.markjour.com, 其完整形式应该是 www.markjour.com. (后面多一个小数点)

DNS 软件

  • BIND
  • PowerDNS
  • dnsmasq
  • Unbound
  • CoreDNS
  • SmartDNS

Cache-Only DNS Server

新的发展

  1. 标准的 DNS 是运行在 UDP 53 端口上的。后来的 RFC 1123 增加了 TCP 的支持, 这个方案叫做 DNS over TCP, 还是在 53 端口。
  2. DNSCrypt, 2011 年设计的, 实现 DNS 的加密和验证,运行于 443 端口。注意:存在于 IETF 框架之外,但是好像有很多服务器支持。
  3. DNS over TLS (DoT), 2016 年 5 月成为规范。
    RFC 7858 Specification for DNS over Transport Layer Security (TLS)
    主要作用是加密传输,防止窃听。
  4. DNS over HTTPS (DoH), 2018 年 10 月成为规范。
    RFC 8484 DNS Queries over HTTPS (DoH)
    作用和 DoT 一样。
  5. DNS over TOR, 2019 年。
  6. Oblivious DNS-over-HTTPS (ODoH), 透过代理的方式,让 DoH 服务器无法获取客户端的真实 IP。同时代理无法获取 DNS 请求的内容。

参考资料与拓展阅读

#1 中文域名

2016-06-06
$ curl -v "http://成都大运会.网址"
* Input domain encoded as `UTF-8'
* About to connect() to xn--6oqv8vrnhtp3c7hb.xn--ses554g port 80 (#0)
*   Trying 202.173.11.233... connected
* Connected to 成都大运会.网址 (202.173.11.233) port 80 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
> Host: xn--6oqv8vrnhtp3c7hb.xn--ses554g
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Server: openresty/1.21.4.3
< Date: Sat, 16 Dec 2023 06:09:33 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 61
< Connection: keep-alive
< Location: http://www.2021chengdu.com
<
<a href="http://www.2021chengdu.com">Moved Permanently</a>.

* Connection #0 to host 成都大运会.网址 left intact
* Closing connection #0

中文域名,中文顶级域名都已经支持很多年了,虽然看到不多。

上面示例中的域名 成都大运会.网址 实际上在域名系统中是以 xn--6oqv8vrnhtp3c7hb.xn--ses554g 形式存在的。
这种编码方式叫做 Punycode,非 ASCII 字符会被按照 Unicode 编号转换成 ASCII 字符。

国际化域名

域名系统中允许的字符集基于 ASCII,不允许以母语或字母表示多种语言的名称和单词。
ICANN 批准了国际化域名(IDNA)系统,该系统通过一种称为 Punycode 的编码将应用程序用户界面中使用的 Unicode 字符串映射到有效的 DNS 字符集。

Example of Greek IDN with domain name in non-Latin alphabet: ουτοπία.δπθ.gr (Punycode is xn--kxae4bafwg.xn--pxaix.gr)

Punycode

'I❤️U'.encode('punycode')
b'IU-ony8085h'

'baidu.com'.encode('idna').decode()
# baidu.com
'中国.com'.encode('idna').decode()
# 'xn--fiqs8s.com'
'编程.中国'.encode('idna').decode()
# 'xn--9nz56h.xn--fiqs8s'

先提取 ASCII 字符,再编码非 ASCII 字符。