您当前的位置:首页 > 电脑百科 > 网络技术 > 网络知识

CoreDNS粗解

时间:2022-09-13 13:43:35  来源:今日头条  作者:邓big胖

如果你厌倦了那些老古董的DNS服务,那么可以试试Coredns, 因为Caddy出色的插件设计, 所以Coredns的骨架基于caddy构建, 也就继承了良好的扩展性, 又因为Go语言是一门开发效率比较高的语言,所以开发一个自定义的插件是比较简单的事情,但是大多数使用都不需要自己编写插件,因为默认的插件以及外部的插件足够大多数场景了。

本文主要分为四个部分

  • 源码阅读
  • 自定义插件编写
  • 一些非常有用的工具函数

源码阅读

如果你不确定,那就阅读源代码吧,代码中存在准确无误的答案。

这里假设启动命令为

./coredns

并且当前工作目录有一个名称是Corefile的文本文件, 内容如下

. {
  forward . 8.8.8.8 1.1.1.1
  log
  errors
  cache
}

首先看看coredns的函数入口

// coredns.go
func main() {
 coremain.Run()
}

// coremainrun.go
func Run() 
 flag.StringVar(&conf, "conf", "", "Corefile to load (default ""+caddy.DefaultConfigFile+"")")
 // 注册一个加载配置文件函数, 如果指定了-conf参数, confLoader就能加载参数对应的配置文件
 caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
    // 如果不指定,自然也没关系, 那就在注册一个默认的配置文件加载函数
 caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))

 if version {
  showVersion()
  os.Exit(0)
 }
 if plugins {
  fmt.Println(caddy.DescribePlugins())
  os.Exit(0)
 }
 // Get Corefile input
 corefile, err := caddy.LoadCaddyfile(serverType)
 // Start your engines
 instance, err := caddy.Start(corefile)
 // Twiddle your thumbs
 instance.Wait()
}

coredns的启动流程还是比较简洁的,, 可以看到coredns没有太多参数选项, 除了打印插件列表, 显示版本的命令参数之外,就是启动流程了,而启动流程概括起来也不负载,加载配置文件,基于配置文件启动,但是在在深入caddy.LoadCaddyfile和caddy.Start之前要先看看在此之前运行的init方法.

// corednsserverregister.go
func init() {
 caddy.RegisterServerType(serverType, caddy.ServerType{
  Directives: func() []string { return Directives },
  DefaultInput: func() caddy.Input {
            // 如果配置文件找不到会加载配置了whoami, log插件的配置文件
   return caddy.CaddyfileInput{
    Filepath:       "Corefile",
    Contents:       []byte(".:" + Port + " {nwhoaminlogn}n"),
    ServerTypeName: serverType,
   }
  },
  NewContext: newContext,
 })
}

因为caddy是一个http/s web服务器,而不是一个dns服务器,所以我们需要在调用caddy启动流程之前注入一个用于dns的ServerType,后续创建的Server等对象都是基于此,并且这个ServerType设置了一个静态的配置文件".:" + Port + " {nwhoaminlogn}n", 也就是说在没有指定配置文件路径以及本地没有Corefile的情况下,还是能够启动一个默认的dns服务器,这个服务加载了whoami, log两个插件。

加载配置文件

在入口函数可以知道, 注入了以下两个配置文件加载函数

// 该函数比较简单, 就是将函数追加到caddyfileLoaders切片中
caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
// 如果caddyfileLoaders切片中所有函数都没有加载到配置文件, 就会使用默认加载函数
caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))

前者的加载函数就是读取参数-conf指定的配置文件。

后者就是读取本地的Corefile, 如果存在的话。

confLoader的代码如下:

func confLoader(serverType string) (caddy.Input, error) {
 if conf == "" {
  return nil, nil
 }

 if conf == "stdin" {
  return caddy.CaddyfileFromPipe(os.Stdin, serverType)
 }

 contents, err := os.ReadFile(filepath.Clean(conf))
 return caddy.CaddyfileInput{
  Contents:       contents,
  Filepath:       conf,
  ServerTypeName: serverType,
 }, nil
}

可以看到逻辑比较简单, 如果指定了-conf参数就通过参数值找到对应的配置文件并返回

因为我们没有指定-conf参数, 所以调用默认加载函数。

LoadCaddyfile加载逻辑如下:

// vendorgithub.comcorednscaddycaddy.go
func LoadCaddyfile(serverType string) (Input, error) {
 // 通过注册的配置文件加载函数, 默认加载函数加载配置文件
 cdyfile, err := loadCaddyfileInput(serverType)
    // 函数找不到就用
 if cdyfile == nil {
  cdyfile = DefaultInput(serverType)
 }

 return cdyfile, nil
}

// vendorgithub.comcorednscaddyplugins.go
func loadCaddyfileInput(serverType string) (Input, error) {
 var loadedBy string
 var caddyfileToUse Input
    // 这里只注册一个从命令行参数加载的loader
    // caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
    // 因为没有指定参数, 所以没哟配置文件会加载
 for _, l := range caddyfileLoaders {
  cdyfile, err := l.loader.Load(serverType)
 }
    // 继而调用默认的加载函数
 if caddyfileToUse == nil && defaultCaddyfileLoader.loader != nil {
  cdyfile, err := defaultCaddyfileLoader.loader.Load(serverType)

  if cdyfile != nil {
   loaderUsed = defaultCaddyfileLoader
   caddyfileToUse = cdyfile
  }
 }
 return caddyfileToUse, nil
}

上面的主要逻辑就是首先判断注册的加载函数能不能加载到配置文件,如果不能,就调用默认加载函数。

而默认加载函数如下

func defaultLoader(serverType string) (caddy.Input, error) {
    // caddy.DefaultConfigFile = Corefile
 contents, err := os.ReadFile(caddy.DefaultConfigFile)

 return caddy.CaddyfileInput{
  Contents:       contents,
  Filepath:       caddy.DefaultConfigFile,
  ServerTypeName: serverType,
 }, nil
}

因为本地有Corefile文件, 所以读取并构造CaddyfileInput对象。

至此配置文件加载完成。

小结

通过代码我们知道配置文件的加载顺序依次是

命令行参数指定的配置文件 > 当前工作目录的Corefile > 静态设置的Corefile内容(".:" + Port + " {nwhoaminlogn}n"))

启动服务

再次粘贴一下前面的启动代码:

// 解析配置文件
instance, err := caddy.Start(corefile)
// 主进程等待退出信号
instance.Wait()

基于上一步加载的配置文件开始服务.

代码如下:

func Start(cdyfile Input) (*Instance, error) {
 inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})}
    // 启动监听进程
 err := startWithListenerFds(cdyfile, inst, nil)
    
    // 用于重启, 告诉父进程是否重启成功
 signalSuccessToParent()

 // 执行on之类的相关命令, 比如
    // on startup /etc/init.d/php-fpm start
 EmitEvent(InstanceStartupEvent, inst)

 return inst, nil
}


func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartTriple) error {
 instances = Append(instances, inst)

    // 验证配置文件并解析
 err = ValidateAndExecuteDirectives(cdyfile, inst, false)
 // 创建Server对象用于后续监听服务
 slist, err := inst.context.MakeServers()
 
    // 依次调用各个插件注册的启动函数
 for _, startupFunc := range inst.OnStartup {
  err = startupFunc()
  if err != nil {
   return err
  }
 }

    // 开始监听
 err = startServers(slist, inst, restartFds)
 started = true

 return nil
}

启动服务大概可以分为两步

  • 解析配置文件 加载配置的各个插件并按插件的顺序执行setup函数, 而不是配置文件插件名出现的顺序加载插件
  • 启动监听服务

解析配置文件

func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bool) error {
    // dns
 stypeName := cdyfile.ServerType()
 // 获取dns的serverType, 在corednsserverregister.go中注册
 stype, err := getServerType(stypeName)
 inst.caddyfileInput = cdyfile
 // 将配置文件加载成一个个ServerBlock对象
 sblocks, err := loadServerBlocks(stypeName, cdyfile.Path(), bytes.NewReader(cdyfile.Body()))
 // 还是corednsserverregister.go中注册的Context
 inst.context = stype.NewContext(inst)

 sblocks, err = inst.context.InspectServerBlocks(cdyfile.Path(), sblocks)
 return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate)
}



func executeDirectives(inst *Instance, filename string,
 directives []string, sblocks []caddyfile.ServerBlock, justValidate bool) error {
 storages := make(map[int]map[string]interface{})

 // 最外层的循环是治理,所以配置文件的指令顺序不重要,重要的代码里面的顺序
    // 插件的顺序是根据plugin.cfg文件生成的
 for _, dir := range directives {
  for i, sb := range sblocks {
   var once sync.Once
   // 依次去serverBlocks中检查是否存在该指令
            // keys是 .:53, .:1053之类的监听地址
   for j, key := range sb.Keys {
    if tokens, ok := sb.Tokens[dir]; ok {
     controller := &Controller{
      instance:  inst,
      Key:       key,
      Dispenser: caddyfile.NewDispenserTokens(filename, tokens),
      OncePerServerBlock: func(f func() error) error {
       var err error
       once.Do(func() {
        err = f()
       })
       return err
      },
      ServerBlockIndex:    i,
      ServerBlockKeyIndex: j,
      ServerBlockKeys:     sb.Keys,
      ServerBlockStorage:  storages[i][dir],
     }
                    // 因为各个key都是公用同一个ServerBlocks, 所以没比较初始化两遍
                    if j > 0 {
                        continue
                    }
     
                    // 调用插件的setup方法
                    // 这里只是注册插件,还没将插件构造成pluginChain
     setup, err := DirectiveAction(inst.serverType, dir)
     err = setup(controller)
    }
   }
  }
 }

 return nil
}

解析配置文件的主要工作就是解析配置文件的指令,然后根据代码中指令的顺序依次调用对应的setup方法。

setup方法会在自定义插件编写的段落着重介绍,这里只需要知道,setup方法是一个将插件集成到调用链的一个方法就行了。

至此配置文件解析完成,各个插件也加载完成了。是时候启动服务了

之所以按照插件在代码顺序执行而不是在配置文件中出现的顺序配置,这是为了避免一些奇怪的问题,比如log和cache放在forward之后缓存和日志就不生效了,这会让人很恼火。

启动监听服务

启动服务的入口大致如下:

// 创建server对象并启动
// 这里的context来自corednsserverregister.go
slist, err := inst.context.MakeServers()
err = startServers(slist, inst, restartFds)

MakeServers代码如下:

func (h *dnsContext) MakeServers() ([]caddy.Server, error) {

 // 检查配置文件是否有冲突, 比如监听域名是否有重复等
 errValid := h.validateZonesAndListeningAddresses()

 // 共享第一个配置的相关值
 for _, c := range h.configs {
  c.Plugin = c.firstConfigInBlock.Plugin
  c.ListenHosts = c.firstConfigInBlock.ListenHosts
  c.Debug = c.firstConfigInBlock.Debug
  c.TLSConfig = c.firstConfigInBlock.TLSConfig
 }

 // 将监听的端口聚合起来
 groups, err := groupConfigsByListenAddr(h.configs)
    
 // 开始创建caddy.Server
 var servers []caddy.Server
 for addr, group := range groups {
  // switch on addr
  switch tr, _ := parse.Transport(addr); tr {
        // 默认就是DNS
  case transport.DNS:
   s, err := NewServer(addr, group)
   if err != nil {
    return nil, err
   }
   servers = append(servers, s)
        // 还有TLS,GRPC,HTTPS
 }

 return servers, nil
}
    
func NewServer(addr string, group []*Config) (*Server, error) {
 s := &Server{
  Addr:         addr,
  zones:        make(map[string]*Config),
  graceTimeout: 5 * time.Second,
 }

    // 为每个zone构建一个site对象以及对应的stack对象, 并注册各指令,然后赋值pluginChain
    // pluginChain就是后面的响应函数
 for _, site := range group {
  if site.Debug {
   s.debug = true
   log.D.Set()
  }
  s.zones[site.Zone] = site

  var stack plugin.Handler
        // 将插件列表按倒叙依次传给优先级高的插件
        // 这样一层套一层就可以让优先级高的插件在最外层,也就是优先执行。
  for i := len(site.Plugin) - 1; i >= 0; i-- {
   stack = site.Plugin[i](stack)
   site.registerHandler(stack)
   }
  }
  site.pluginChain = stack
 }

 return s, nil
}

至此构造好了配置文件中的各个Server对象,然后开始监听服务

startServers代码如下:

func startServers(serverList []Server, inst *Instance, restartFds map[string]restartTriple) error {
 for _, s := range serverList {
  var (
   ln .NET.Listener
   pc  net.PacketConn
   err error
  )
  // 可以看到默认tcp和udp同时监听
  if ln == nil {
   ln, err = s.Listen()
   if err != nil {
    return fmt.Errorf("Listen: %v", err)
   }
  }
  if pc == nil {
   pc, err = s.ListenPacket()
   if err != nil {
    return fmt.Errorf("ListenPacket: %v", err)
   }
  }

  inst.servers = append(inst.servers, ServerListener{server: s, listener: ln, packet: pc})
 }

    // 一起启动监听服务
    // tcp接口调用Serve
    // udp接口调用ServePacket
 for _, s := range inst.servers {
  inst.wg.Add(2)
  stopWg.Add(2)
  func(s Server, ln net.Listener, pc net.PacketConn, inst *Instance) {
   go func() {
    defer func() {
     inst.wg.Done()
     stopWg.Done()
    }()
    errChan <- s.Serve(ln)
   }()

   go func() {
    defer func() {
     inst.wg.Done()
     stopWg.Done()
    }()
    errChan <- s.ServePacket(pc)
   }()
  }(s.server, s.listener, s.packet, inst)
 }

 return nil
}

至此监听服务启动起来了,可以接口客户端的请求。

这里看看udp的处理逻辑把

// corednsserverserver.go
func (s *Server) ServePacket(p net.PacketConn) error {
 s.m.Lock()
    // 这里的Handler实现了ServeDNS接口, 会直接动用传入的函数
 s.server[udp] = &dns.Server{PacketConn: p, Net: "udp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
  ctx := context.WithValue(context.Background(), Key{}, s)
  ctx = context.WithValue(ctx, LoopKey{}, 0)
        // ServeDNS是每个插件都要实现的函数接口, 超级重要的流量入口
        // 最终将数据导向h.pluginChain.ServeDNS
  s.ServeDNS(ctx, w, r)
 })}
 s.m.Unlock()

 return s.server[udp].ActivateAndServe()
}

// 后面的调用链比较长,大家了解一下就行,其实最终还是调用上面的HandlerFunc里面传入的匿名函数
func (srv *Server) ActivateAndServe() error {
 srv.init()

 if srv.PacketConn != nil {
  srv.started = true
  unlock()
  return srv.serveUDP(srv.PacketConn)
 }
 return &Error{err: "bad listeners"}
}

func (srv *Server) serveUDP(l net.PacketConn) error {
 defer l.Close()

 reader := Reader(defaultReader{srv})
 lUDP, isUDP := l.(*net.UDPConn)
 readerPC, canPacketConn := reader.(PacketConnReader)

 rtimeout := srv.getReadTimeout()
 // 不断读取udp包
 for srv.isStarted() {
  var (
   m    []byte
   sPC  net.Addr
   sUDP *SessionUDP
   err  error
  )
  if isUDP {
   m, sUDP, err = reader.ReadUDP(lUDP, rtimeout)
  } else {
   m, sPC, err = readerPC.ReadPacketConn(l, rtimeout)
  }
  wg.Add(1)
        // 每个包创建一个协程专门处理
  go srv.serveUDPPacket(&wg, m, l, sUDP, sPC)
 }

 return nil
}

func (srv *Server) serveUDPPacket(wg *sync.WaitGroup, m []byte, u net.PacketConn, udpSession *SessionUDP, pcSession net.Addr) {
 w := &response{tsigProvider: srv.tsigProvider(), udp: u, udpSession: udpSession, pcSession: pcSession}
 srv.serveDNS(m, w)
 wg.Done()
}

func (srv *Server) serveDNS(m []byte, w *response) {
    // udp包解析
 dh, off, err := unpackMsgHdr(m, 0)

 req := new(Msg)
 req.setHdr(dh)

 switch action := srv.MsgAcceptFunc(dh); action {
 case MsgAccept:
  // 处理细节
 case MsgReject, MsgRejectNotImplemented:
  // 处理细节
 case MsgIgnore:
  // 处理细节
 }
 
    // 这里就是最上面的那个handlerfunc
 srv.Handler.ServeDNS(w, req) // Writes back to the client
}

可以看到监听的处理调用链还是比较长的。

小结

Coredns默认会同时监听tcp和udp, 基于解析的配置文件会构造一个pluginChain, 而这个pluginChain就如它的名字那样直白,将插件包装成一个链条依次执行以完成dns解析。

自定义插件编写

因为caddy的优秀的插件系统,所以扩展起来很方便。

编写插件大致分为一下几步

  • 复制代码框架(模板)
  • 注入插件代码
  • 重新生成插件列表
  • 编译运行

复制代码框架(模板)

要开发自己的插件首先要下载coredns的源代码, coredns的代码结构如下:

tree .
├── ADOPTERS.md
├── CODE_OF_CONDUCT.md -> .github/CODE_OF_CONDUCT.md
├── CODEOWNERS
├── CONTRIBUTING.md -> .github/CONTRIBUTING.md
├── core
├── coredns.1.md
├── coredns.go
├── corefile.5.md
├── coremain
├── directives_generate.go
├── Dockerfile
├── go.mod
├── go.sum
├── GOVERNANCE.md
├── LICENSE
├── Makefile
├── Makefile.doc
├── Makefile.docker
├── Makefile.release
├── man
├── notes
├── owners_generate.go
├── pb
├── plugin
├── plugin.cfg
├── plugin.md
├── README.md
├── request
├── SECURITY.md -> .github/SECURITY.md
└── test

官方提供了一个可以直接运行的示例:
https://github.com/coredns/example, 你可以拷贝下来做成自己的一个模块, 比如:

go mod init github.com/{github账号}/{你的仓库名}

或者将其直接复制到coredns的plugin目录

这里使用第二种方法, 本文的插件名叫dforward

所以目录结构如下:

.
├── coredns.go
├── directives_generate.go
├── Dockerfile
├── go.mod
├── go.sum
├── plugin
│   ├── acl
│   ├── example  # 自定插件在这
│   # 省略其他插件名
└── test
    ├── auto_test.go

63 directories, 196 files

注入插件代码

为了让我们的插件集成到coredns里面,我们需要编辑plugin.cfg文件以及重新生成代码。

从上面的源代码阅读我们知道插件的顺序很重要,所以不能将插件的顺序放在太前,假设我们的插件放在第一位,那么log, errors等插件就都不会在我们自定义的插件前被调用,还有就是cache插件就不能正常缓存了,当然了,如果你的插件就是类似于log, errors, cache等插件的功能,那么你可以将其放在较前面的顺序,具体顺序应该具体分析。

效果如下:

loop:loop
example:example  # 你的插件在这个位置
forward:forward
grpc:grpc

如果你是使用第一种方法注入编写插件,那么效果如下:

loop:loop
dforward:github.com/{github账号}/{你的仓库名}  # 你的插件在这个位置
forward:forward
grpc:grpc

重新生成插件列表

重新生成相关代码,并不复杂,只需要在coredns代码根目录执行一条命令即可。

go generate

如果没有出现错误, 你会发现corednsserverzdirectives.go 和corepluginzplugin.go两个文件出现了dforward的相关信息。

编译运行

然后就可以在Corefile文件里面使用example的指令了。

example这个指令的唯一功能就是在日志中输出一个example。

小结

编写Coredns的插件并不复杂,唯一要考虑的是,是不是需要编写自己的插件,可以先了解内置的插件列表以及外部的插件列表中的各个插件功能在决定是否需要编写,很多时候是不需要,如果你真的需要编写一个自己的插件来满足特定的功能,也不需要自己实现各种功能,比如转发你可以直接调用forward插件,缓存可以直接调用cache插件等,再者就是coredns有许多常用的工具函数,这些放在下一个段落。

一些常用的工具函数

一些在域名解析中常用到的函数等

判断一个域名是否是一个zone(可以简单认为是域名)列表的子域名。

import (
 "fmt"

 "github.com/coredns/coredns/plugin"
)

func main() {
 zones := []string{"a.com.", "b.com."}
 fmt.Println(plugin.Zones(zones).Matches("xx.a.com."))
 fmt.Println(plugin.Zones(zones).Matches("xx.c.com.")) // 没匹配上就会输出空字符串
}

输出如下:

a.com.

如果是很多域名的话,建议搞一个前缀树来匹配,因为这里的Matches方法是便利列表来匹配的。

解析dns各种协议

coredns支持dns, tls(dot), https(doh), grpc等多种传输协议

package main

import (
 "fmt"

 "github.com/coredns/coredns/plugin/pkg/parse"
)

func main() {
 hosts := []string{
  "tls://127.0.0.1:853",
  "127.0.0.1:53",
  "grpc://127.0.0.1:999",
  "https://127.0.0.1:1443",
 }
 for _, host := range hosts {
  trans, addr := parse.Transport(host)
  fmt.Println(host, "->", trans, addr)
 }
}

输出如下:

tls://127.0.0.1:853 -> tls 127.0.0.1:853
127.0.0.1:53 -> dns 127.0.0.1:53
grpc://127.0.0.1:999 -> grpc 127.0.0.1:999
https://127.0.0.1:1443 -> https 127.0.0.1:1443

各种协议的请求

根据需要发起各种类型的请求

我们可以不用到处找支持这些协议的dns服务器,用coredns自身监听即可,下面是配置文件

.:53 tls://.:853 https://.:1043 {
  tls plugin/tls/test_cert.pem plugin/tls/test_key.pem plugin/tls/test_ca.pem
  log
  errors
  hosts example.hosts
}

example.hosts就只有一行

127.0.0.1 example.com

coredns有一个小小的坑, 如果你没有显式的设置证书,那么即使是指定了tls, https等协议,最终还是以tcp和http的方式来监听。

个人觉得更好的体验是自动在本地创建一个证书或者在日志中发出警告, 但是coredns暗搓搓的把tls的那层给去掉了。。。。。

原生的DNS请求

package main

import (
 "fmt"
 "net"
 "os"
 "time"

 "github.com/miekg/dns"
)

func main() {
 if len(os.Args) == 1 {
  fmt.Println("必须指定一个域名")
  os.Exit(1)
 }
 // 将域名标准化, 比如example.com 变成 example.com. 最后面加了一个点
 query := dns.Fqdn(os.Args[1])
 m1 := new(dns.Msg)
 m1.Id = dns.Id()
 m1.RecursionDesired = true
 m1.Question = make([]dns.Question, 1)
 m1.Question[0] = dns.Question{Name: query, Qtype: dns.TypeA, Qclass: dns.ClassINET}
 c := new(dns.Client)
 in, rtt, err := c.Exchange(m1, "127.0.0.1:53")
 if err != nil {
  panic(err)
 }

 if len(in.Answer) == 0 {
  fmt.Println("没有查询到任何结果")
  os.Exit(1)
 }

 if a, ok := in.Answer[0].(*dns.A); ok {
  fmt.Println("获取到ip地址:", a.A.String())
 } else {
  fmt.Println("返回结果不是A记录:", in)
 }
 fmt.Println("耗时: ", rtt)

 c2 := new(dns.Client)
 laddr := net.UDPAddr{
  IP:   net.ParseIP("[::1]"),
  Port: 12345,
  Zone: "",
 }

 fmt.Println("设置超时的客户端的响应结果:")
 c2.Dialer = &net.Dialer{
  Timeout:   200 * time.Millisecond,
  LocalAddr: &laddr,
 }
 in2, rtt2, err2 := c2.Exchange(m1, "127.0.0.1:53")
 if err2 != nil {
  panic(err)
 }

 if a, ok := in2.Answer[0].(*dns.A); ok {
  fmt.Println("获取到ip地址:", a.A.String())
 } else {
  fmt.Println("返回结果不是A记录:", in)
 }
 fmt.Println("耗时: ", rtt2)
}

输出如下:

获取到ip地址: 127.0.0.1
耗时:  512µs
设置超时的客户端的响应结果:
获取到ip地址: 127.0.0.1
耗时:  0s

DNS Over TLS请求(DOT)

package main

import (
 "crypto/tls"
 "fmt"
 "log"
 "os"

 "github.com/miekg/dns"
)

func main() {
 tlsConfig := new(tls.Config)
 tlsConfig.InsecureSkipVerify = true
 conn, err := dns.DialWithTLS("tcp", "127.0.0.1:853", tlsConfig)
 if err != nil {
  log.Fatalln("连接出错:", err)
 }
 defer conn.Close()

 query := dns.Fqdn(os.Args[1])
 m1 := new(dns.Msg)
 m1.Id = dns.Id()
 m1.RecursionDesired = true
 m1.Question = make([]dns.Question, 1)
 m1.Question[0] = dns.Question{Name: query, Qtype: dns.TypeA, Qclass: dns.ClassINET}
 err = conn.WriteMsg(m1)
 if err != nil {
  log.Fatal("发送请求失败:", err)
 }
 ret, err := conn.ReadMsg()
 if err != nil {
  log.Fatal("读取响应失败:", err)
 }
 fmt.Println(ret)
}

输出如下:

获取到ip地址: 127.0.0.1

DNS Over HTTPS请求(DOH)

package main

import (
 "crypto/tls"
 "fmt"
 "os"

 "bytes"
 "encoding/base64"
 "io"
 "net/http"

 "github.com/miekg/dns"
)

func main() {

 query := dns.Fqdn(os.Args[1])
 m1 := new(dns.Msg)
 m1.Id = dns.Id()
 m1.RecursionDesired = true
 m1.Question = make([]dns.Question, 1)
 m1.Question[0] = dns.Question{Name: query, Qtype: dns.TypeA, Qclass: dns.ClassINET}
 req, err := NewRequest("GET", "127.0.0.1:1043", m1)
 if err != nil {
  panic("创建请求失败: " + err.Error())
 }

 httpClient := http.Client{}
 httpClient.Transport = &http.Transport{
  TLSClientConfig: &tls.Config{
   InsecureSkipVerify: true,
  },
 }
 resp, err := httpClient.Do(req)
 if err != nil {
  panic("读取响应失败: " + err.Error())
 }

 in, err := ResponseToMsg(resp)
 if err != nil {
  panic("解析响应结果失败: " + err.Error())
 }
 if len(in.Answer) == 0 {
  fmt.Println("没有查询到任何结果")
  os.Exit(1)
 }

 if a, ok := in.Answer[0].(*dns.A); ok {
  fmt.Println("获取到ip地址:", a.A.String())
 } else {
  fmt.Println("返回结果不是A记录:", in)
 }
}

// 以下代码复制自pluginpkgdoh
// MimeType is the DoH mimetype that should be used.
const MimeType = "application/dns-message"

// Path is the URL path that should be used.
const Path = "/dns-query"

// NewRequest returns a new DoH request given a method, URL (without any paths, so exclude /dns-query) and dns.Msg.
func NewRequest(method, url string, m *dns.Msg) (*http.Request, error) {
 buf, err := m.Pack()
 if err != nil {
  return nil, err
 }

 switch method {
 case http.MethodGet:
  b64 := base64.RawURLEncoding.EncodeToString(buf)

  req, err := http.NewRequest(http.MethodGet, "https://"+url+Path+"?dns="+b64, nil)
  if err != nil {
   return req, err
  }

  req.Header.Set("content-type", MimeType)
  req.Header.Set("accept", MimeType)
  return req, nil

 case http.MethodPost:
  req, err := http.NewRequest(http.MethodPost, "https://"+url+Path+"?bla=foo:443", bytes.NewReader(buf))
  if err != nil {
   return req, err
  }

  req.Header.Set("content-type", MimeType)
  req.Header.Set("accept", MimeType)
  return req, nil

 default:
  return nil, fmt.Errorf("method not allowed: %s", method)
 }
}

// ResponseToMsg converts a http.Response to a dns message.
func ResponseToMsg(resp *http.Response) (*dns.Msg, error) {
 defer resp.Body.Close()

 return toMsg(resp.Body)
}

// RequestToMsg converts a http.Request to a dns message.
func RequestToMsg(req *http.Request) (*dns.Msg, error) {
 switch req.Method {
 case http.MethodGet:
  return requestToMsgGet(req)

 case http.MethodPost:
  return requestToMsgPost(req)

 default:
  return nil, fmt.Errorf("method not allowed: %s", req.Method)
 }
}

// requestToMsgPost extracts the dns message from the request body.
func requestToMsgPost(req *http.Request) (*dns.Msg, error) {
 defer req.Body.Close()
 return toMsg(req.Body)
}

// requestToMsgGet extract the dns message from the GET request.
func requestToMsgGet(req *http.Request) (*dns.Msg, error) {
 values := req.URL.Query()
 b64, ok := values["dns"]
 if !ok {
  return nil, fmt.Errorf("no 'dns' query parameter found")
 }
 if len(b64) != 1 {
  return nil, fmt.Errorf("multiple 'dns' query values found")
 }
 return base64ToMsg(b64[0])
}

func toMsg(r io.ReadCloser) (*dns.Msg, error) {
 buf, err := io.ReadAll(http.MaxBytesReader(nil, r, 65536))
 if err != nil {
  return nil, err
 }
 m := new(dns.Msg)
 err = m.Unpack(buf)
 return m, err
}

func base64ToMsg(b64 string) (*dns.Msg, error) {
 buf, err := b64Enc.DecodeString(b64)
 if err != nil {
  return nil, err
 }

 m := new(dns.Msg)
 err = m.Unpack(buf)

 return m, err
}

var b64Enc = base64.RawURLEncoding

输出结果如下:

获取到ip地址: 127.0.0.1

总结

coredns还是很棒的, 因为是GO写的所以可以交叉编译各个平台的可执行文件,这样部署很方便,又因为coredns是一个发展不错的项目,所以资源和插件都很丰富,又因为借鉴了caddy的插件体系,所以扩展起来很方便。

GitHub地址参考:
https://github.com/youerning/blog/tree/master/coredns_code



Tags:CoreDNS   点击:( )  评论:( )
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:[email protected]),我们将及时更正、删除,谢谢。
▌相关推荐
如果你厌倦了那些老古董的DNS服务,那么可以试试Coredns, 因为Caddy出色的插件设计, 所以Coredns的骨架基于caddy构建, 也就继承了良好的扩展性, 又因为Go语言是一门开发效率...【详细内容】
2022-09-13  Tags: CoreDNS  点击:(0)  评论:(0)  加入收藏
前言 CoreDNS近日在工作中修改DNS,由于CoreDNS pod数量比较多,习惯性地使用脚本批量重启,随之引发了nginx ingress的告警,有大量超时的请求发生,开始并未意识到是修改CoreDNS的原...【详细内容】
2020-11-05  Tags: CoreDNS  点击:(405)  评论:(0)  加入收藏
CoreDNS是SkyDNS的继任者,可以和很多后端(etcd,k8s等)进行通信。CoreDNS非常的灵活,它的灵活性得益于其丰富的插件 (https://coredns.io/plugins/ ),也可以写适合自己的插件。 CoreD...【详细内容】
2020-08-24  Tags: CoreDNS  点击:(242)  评论:(0)  加入收藏
▌哈哈电竞推荐
如果你厌倦了那些老古董的DNS服务,那么可以试试Coredns, 因为Caddy出色的插件设计, 所以Coredns的骨架基于caddy构建, 也就继承了良好的扩展性, 又因为Go语言是一门开发效率...【详细内容】
2022-09-13  邓big胖  今日头条  Tags:CoreDNS   点击:(0)  评论:(0)  加入收藏
法国云提供商OVHcloud推出了一项self-IP服务,允许客户在发生中断时重复使用现有的公共IPv4块作为故障转移地址。名为BYOIP的“自带IP导入”服务允许客户通过其OVHcloud控制面...【详细内容】
2022-09-08  慕先生科技馆    Tags:OVHcloud   点击:(17)  评论:(0)  加入收藏
1.常规的网络交互过程是从客户端发起网络请求,用户态的应用程序(浏览器)会生成 HTTP 请求报文、并通过 DNS 协议查找到对应的远端 IP 地址。2.在套接字生成之后进入内核态,浏览...【详细内容】
2022-09-06  微笑橙子mR  今日头条  Tags:IP数据包   点击:(23)  评论:(0)  加入收藏
在三类云资源(计算、存储和网络)中,网络似乎最经得起云原生非功能性需求的考验。例如,计算弹性是通过虚拟机、容器和编配器合理分配的,并通过 CI/CD 管道进行管理。网络在实现方面似乎缺乏弹性。在本文中,我们将试图通过云...【详细内容】
2022-09-06  InfoQ   企鹅号  Tags:SDN   点击:(18)  评论:(0)  加入收藏
先说串口之前写过一篇UART,通用串行异步通讯协议,感兴趣可以参考一下《 我打赌!你还不会UART 》; 因为UART没有时钟信号,无法控制何时发送数据,也无法保证双发按照完全相同的速度...【详细内容】
2022-09-05  嵌入式胖胖  今日头条  Tags:SPI协议   点击:(13)  评论:(0)  加入收藏
网络覆盖设计涉及到规划网络覆盖范围和范围内信号强度,所以先介绍无线网络覆盖范围的概念,引出衡量覆盖范围的指标:覆盖半径和覆盖距离。覆盖范围AP通过天线发射无线信号,在天线...【详细内容】
2022-09-05  弱电智能网  网易号  Tags:无线网络   点击:(17)  评论:(0)  加入收藏
功率和信号强度基本概念在无线网络中,使用AP设备和天线来实现有线和无线信号互相转换。如图1所示,有线网络侧的数据从AP设备的有线接口进入AP后,经AP处理为射频信号,从AP的发送...【详细内容】
2022-09-05  弱电智能网    Tags:功率   点击:(29)  评论:(0)  加入收藏
软件定义网络 (SDN) 是一种架构,它抽象了网络的不同、可区分的层,使网络变得敏捷和灵活,SDN 的目标是通过使企业和服务提供商能够快速响应不断变化的业务需求来改进网络控制。...【详细内容】
2022-08-30  太阁IT认证闫辉  搜狐号  Tags:SDN   点击:(7)  评论:(0)  加入收藏
随着我国宽带网络的飞速发展,无论城市乡村,安装宽带几乎已经成了各家各户生活中必不可少的一部分。宽带网速从几十兆发展到上千兆,速度越来越快。可是有的用户却说,我家安装的已...【详细内容】
2022-08-29  Sudo  今日头条  Tags:网络质量   点击:(41)  评论:(0)  加入收藏
以太网首部目地MAC地址(8字节)源MAC地址(8字节)类型(2字节)1、IP头的结构 (1)字节和数字的存储顺序是从右到左,依次是从低位到高位,而网络存储顺序是从左到右,依次从低位到高位。(2)版本:...【详细内容】
2022-08-29  网络技术干货  今日头条  Tags:协议   点击:(45)  评论:(0)  加入收藏
站内最新
站内热门
站内头条