Golang DNS解析

我们平时都会使用func Dial(network, address string) (Conn, error)去创建一个连接,包括golang的httpclient也是调用这样一个函数去创建连接。这个方法里使用了net.Dialer

net.Dialer

type Dialer struct {
    Timeout time.Duration  //连接超时
    Deadline time.Time
    LocalAddr Addr
    DualStack bool
    FallbackDelay time.Duration 
    KeepAlive time.Duration
    Resolver *Resolver     //DNS解析器
    Cancel <-chan struct{}
    Control func(network, address string, c syscall.RawConn) error
}
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)

Dialer里有一个DialContext方法,它先使用Resolver *Resolver解析DNS,然后在创建连接。

Resolver *Resolver是一个struct,不是个interface

type Resolver struct {
    PreferGo bool
    StrictErrors bool
    Dial func(ctx context.Context, network, address string) (Conn, error) //创建到nameserver的连接
}

Resolver的工作流程如下:

使用cgo还是purego去解析

在图中步骤4中,会计算出解析方式的顺序,顺序有5种。分为两类

var lookupOrderName = map[hostLookupOrder]string{
    hostLookupCgo:      "cgo",
    hostLookupFilesDNS: "files,dns",
    hostLookupDNSFiles: "dns,files",
    hostLookupFiles:    "files",
    hostLookupDNS:      "dns",
}

其中hostLookupCgo是一类,表示直接调用libc的getaddrinfo方法去解析。

其它四个是另一类,表示go去读取文件/etc/hosts和/etc/resolv.conf去解析,files表示先看看/etc/hosts有没有对应的记录,dns表示通过/etc/resolv.conf的server去解析。四个指定了不同的解析方式顺序

这个顺序计算是依赖平台类型,/etc/nsswitch.conf等的。比如goos为darwin和android的平台都是用cgo解析,这可能是因为这两个平台上找不到/etc/hosts和/etc/resolv.conf文件吧。然后先files还是先dns的关系是使用/etc/nsswitch.conf的配置的。对应linux都是使用purego去解析的。

如果你确定你机器上有/etc/hosts和/etc/resolv.conf这两个文件,而且格式正确,应用程序有访问权限,那个你可以设置Resovler.PreferGo=true,强制使用purego。

DNS缓存

但你通过net.Dial或者net.DialContext去创建连接,或者使用Resolver去解析DNS时,都是没有缓存的。这个意思就是说你每创建一个连接,Resolver都回去解析一次。那我们肯定需要一个dns缓存,如果dns server挂掉了不会影响到我们呀。

但有个特别坑爹的地方, Resolver是个struct,不是个interface,这样我们就没办法直接实现一个Resolver,然后替换掉Dialer里的Resolver。

还有个坑爹地方,我们没法调用Resolver.resolverAddrList方法,因为这是个内部方法。只能调用图中3步骤的Resolver.LookupIPAddr, 这个方法你只能给一个host,不能带port,也不能是unxi socket,不然报错。

下面我自己做了个DialContext, 这个方法可以用来设置到http.Transport等地方。代码中的dialer.Dialer.DialContext就是net库下的Dialer.DialContext

func (dialer *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
    host, port, err := net.SplitHostPort(address)
    if err != nil {
        return nil, err
    }

    ips, _ := dialer.Resolver.Get(ctx, host)//这里自己实现了一个带缓存的Resolver,但是这个Resolver没有识别unix socket的功能,如果host里有port也不能识别,所以host不能带port
    for _, ip := range ips {
        conn, err := dialer.Dialer.DialContext(ctx, network, ip+":"+port)//这里我们已经解析出来了ip和port,那么net.Dialer判断出来是个ip就不会再去解析了
        if err == nil {
            return conn, nil
        }
    }
    return dialer.Dialer.DialContext(ctx, network, address) 如果前面解析失败了就老老实实用原address去调用吧,可能address是个unix socket呢。这里是个兜底,前面都失败了那么我们还可以用原来的方式去做
}

带缓存的Resolver,很简单,就是定时去解析一下,你也可以实现一个带有过期时间的缓存。

type Resolver struct {
    lock            sync.RWMutex
    cache           map[string][]string
    ResolverTimeout time.Duration
}

func NewResolver(refreshRate time.Duration) *Resolver {
    resolver := &Resolver{
        cache:           make(map[string][]string, 64),
        ResolverTimeout: 30 * time.Second,
    }
    if refreshRate > 0 {
        go resolver.autoRefresh(refreshRate)
    }
    return resolver
}

func (r *Resolver) Get(ctx context.Context, host string) ([]string, error) {
    r.lock.RLock()
    ips, exists := r.cache[host]
    r.lock.RUnlock()
    if exists {
        return ips, nil
    }

    return r.Lookup(ctx, host)
}

func (r *Resolver) Refresh() {
    i := 0
    r.lock.RLock()
    addresses := make([]string, len(r.cache))
    for key, _ := range r.cache {
        addresses[i] = key
        i++
    }
    r.lock.RUnlock()

    for _, host := range addresses {
        ctx, _ := context.WithTimeout(context.Background(), r.ResolverTimeout)
        r.Lookup(ctx, host)
        time.Sleep(time.Second)
    }
}

func (r *Resolver) Lookup(ctx context.Context, host string) ([]string, error) {
    ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) //调用默认的resolver
    if err != nil {
        return nil, err
    }
    if len(ips) == 0 {
        return nil, nil
    }
    strIPs := make([]string, len(ips))
    for index, ip := range ips{
        strIPs[index] = ip.String()
    }
    r.lock.Lock()
    r.cache[host] = strIPs
    r.lock.Unlock()
    return strIPs, nil
}

func (r *Resolver) autoRefresh(rate time.Duration) {
    for {
        time.Sleep(rate)
        r.Refresh()
    }
}


go的Dialer和Resover需要改进的地方

现在的Dialer和Resover太过于耦合了

  1. Dialer的Resolver是个struct, 我要实现我自己的Resolver呢
  2. Resover的resolveAddrList、internetAddrList方法里包含了对addr检查和过滤的逻辑,但是没有暴露给用户。比如是否为unxi检查,是否有端口的检查,还有一些地址的校验逻辑。