· code

Golang自定义DNS Nameserver

某些情况下我们希望程序通过自定义Nameserver去查询域名,而不希望通过操作系统给定的Nameserver,本文介绍如何在Golang中实现自定义Nameserver

DNS 解析过程

Golang中一般通过net.ResolverLookupHost(ctx context.Context, host string) (addrs []string, err error)去实现域名解析,解析过程如下:

  1. 检查本地hosts文件是否存在解析记录,存在即返回解析地址
  2. 不存在即根据resolv.conf中读取的nameserver发起递归查询
  3. nameserver不断的向上级nameserver发起迭代查询
  4. nameserver最终返回查询结果给请求者

用户可以通过修改/etc/resolv.conf来添加特定的nameserver,但某些场景下我们不希望更改系统配置。比如在kubernetes中,作为sidecar服务需要通过service去访问其他集群内服务,必须更改dnsPolicyClusterFirst,但这可能会影响其他容器的 DNS 查询效率。

自定义 Nameserver

Golang中自定义Nameserver,需要我们自己实现一个Resolver,如果是httpClient需要自定义DialContext()

Resolver实现如下:

// 默认dialer
dialer := &net.Dialer{
		Timeout: 1 * time.Second,
}

// 定义resolver
resolver := &net.Resolver{
	Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
		return dialer.DialContext(ctx, "tcp", nameserver) // 通过tcp请求nameserver解析域名
	},
}

自定义Dialer如下:

type Dialer struct {
	dialer     *net.Dialer
	resolver   *net.Resolver
	nameserver string
}

// NewDialer create a Dialer with user's nameserver.
func NewDialer(dialer *net.Dialer, nameserver string) (*Dialer, error) {
	conn, err := dialer.Dial("tcp", nameserver)
	if err != nil {
		return nil, err
	}
	defer conn.Close()

	return &Dialer{
		dialer: dialer,
		resolver: &net.Resolver{
			Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
				return dialer.DialContext(ctx, "tcp", nameserver)
			},
		},
		nameserver: nameserver, // 用户设置的nameserver
	}, nil
}

// DialContext connects to the address on the named network using
// the provided context.
func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	host, port, err := net.SplitHostPort(address)
	if err != nil {
		return nil, err
	}

	ips, err := d.resolver.LookupHost(ctx, host) // 通过自定义nameserver查询域名
	for _, ip := range ips {
    // 创建链接
		conn, err := d.dialer.DialContext(ctx, network, ip+":"+port)
		if err == nil {
			return conn, nil
		}
	}

	return d.dialer.DialContext(ctx, network, address)
}

httpClient中自定义DialContext()如下:

ndialer, _ := NewDialer(dialer, nameserver)

client := &http.Client{
  Transport: &http.Transport{
    DialContext:         ndialer.DialContext,
    TLSHandshakeTimeout: 10 * time.Second,
  },
  Timeout: timeout,
}

总结

通过以上实现可解决自定义Nameserver,也可以在Dailer中添加缓存,实现 DNS 缓存。