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

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   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
基于 CoreDNS 和 K8s 构建云原生场景下的企业级 DNS
容器作为近些年最火热的后端技术,加快了很多企业的数字化转型进程。目前的企业,不是在使用云原生技术,就是在转向云原生技术的过程中。在容器化进程中,如何保持业务的平稳迁移,如...【详细内容】
2022-09-21  Search: CoreDNS  点击:(411)  评论:(0)  加入收藏
CoreDNS粗解
如果你厌倦了那些老古董的DNS服务,那么可以试试Coredns, 因为Caddy出色的插件设计, 所以Coredns的骨架基于caddy构建, 也就继承了良好的扩展性, 又因为Go语言是一门开发效率...【详细内容】
2022-09-13  Search: CoreDNS  点击:(387)  评论:(0)  加入收藏
通过一起重启coredns操作引发的故障延伸至dns监控
前言 CoreDNS近日在工作中修改DNS,由于CoreDNS pod数量比较多,习惯性地使用脚本批量重启,随之引发了nginx ingress的告警,有大量超时的请求发生,开始并未意识到是修改CoreDNS的原...【详细内容】
2020-11-05  Search: CoreDNS  点击:(609)  评论:(0)  加入收藏
初试CoreDNS
CoreDNS是SkyDNS的继任者,可以和很多后端(etcd,k8s等)进行通信。CoreDNS非常的灵活,它的灵活性得益于其丰富的插件 (https://coredns.io/plugins/ ),也可以写适合自己的插件。 CoreD...【详细内容】
2020-08-24  Search: CoreDNS  点击:(492)  评论:(0)  加入收藏
▌简易百科推荐
学生偷看“不良网站”,手机上3个痕迹无法清除,网友:咋不早说
众所周知,中国的常规教育中,总是“谈性色变”,但在这个信息爆炸的互联网时代,即便是一些年纪很小的孩子,也能轻易接触到一些所谓的不良网站,因此这一方面的教育缺失,其实是很可怕的...【详细内容】
2024-03-28    叶姐生活指南  Tags:不良网站   点击:(11)  评论:(0)  加入收藏
什么是网络中的路由器?核心功能解释
路由器是互联网连接的核心元素,是一种允许多个设备连接到互联网,并促进将数据包转发到各自的目标地址的设备。使用动态路由技术,路由器检查数据并在各种可用路径中选择最有效的...【详细内容】
2024-03-07    千家网  Tags:路由器   点击:(31)  评论:(0)  加入收藏
过年该不该升级Wi-Fi 7路由?看完就知道
打开电商网站不难发现,从2023年第三季度到现在,Wi-Fi 7路由器新品越来越多。而且价格不再是高高在上,已经基本和Wi-Fi 6路由价格差不多了。看到这些Wi-Fi 7新品路由,不少朋友就...【详细内容】
2024-02-27    中关村在线  Tags:Wi-Fi   点击:(37)  评论:(0)  加入收藏
聊聊 Kubernetes 网络模型综合指南
这篇详细的博文探讨了 Kubernetes 网络的复杂性,提供了关于如何在容器化环境中确保高效和安全通信的见解。译自Navigating the Network: A Comprehensive Guide to Kubernete...【详细内容】
2024-02-19  云云众生s  微信公众号  Tags:Kubernetes   点击:(39)  评论:(0)  加入收藏
SSL协议是什么?关于SSL和TLS的常见问题解答
SSL(安全套接字层)及其后继者TLS(传输层安全)是用于在联网计算机之间建立经过身份验证和加密的链接的协议。尽管SSL协议在 1999年已经随着TLS 1.0的发布而被弃用,但我们仍将这些...【详细内容】
2024-02-06  IDC点评网    Tags:SSL协议   点击:(69)  评论:(0)  加入收藏
从零开始了解网络协议:TCP/IP详解
从零开始了解网络协议:TCP/IP详解 在当今数字化的时代,网络协议已经成为我们生活中不可或缺的一部分。作为互联网的基础,网络协议规定了数据如何在不同的网络设备之间传输。TC...【详细内容】
2024-02-01    简易百科  Tags:TCP/IP   点击:(59)  评论:(0)  加入收藏
BGP路由属性:互联网路由的灵活控制器
在互联网的庞大网络中,边界网关协议(BGP)是确保不同自治系统(AS)间路由信息有效交换的关键协议。然而,BGP的功能远不止于此。其核心组成部分,即BGP路由属性,赋予了BGP强大的灵活性,使...【详细内容】
2024-01-26  诺诺爱生活    Tags:互联网路由   点击:(40)  评论:(0)  加入收藏
简易百科之什么是网络延迟?
简易百科之什么是网络延迟?随着互联网的普及和发展,网络已经成为我们生活中不可或缺的一部分。然而,我们在使用网络时可能会遇到一种情况,那就是网络延迟。那么,什么是网络延迟呢...【详细内容】
2024-01-24    简易百科  Tags:网络延迟   点击:(149)  评论:(0)  加入收藏
网络延迟与网络速度有什么区别?分享具体的答案
通常,许多人抱怨网速测试。速度还是不错的,但是他们玩游戏的时候怎么会卡住,还是断开连接等等问题,这一系列问题始终困扰着大家。那么,网络延迟与网络速度有什么区别呢?请不要担心...【详细内容】
2024-01-24  萌新小郭    Tags:网络延迟   点击:(51)  评论:(0)  加入收藏
揭秘IP地址的网络威胁与攻击类型
在当今数字化时代,网络攻击已经成为网络安全的一大挑战。IP地址,作为互联网通信的基础,也成为网络威胁和攻击的焦点之一。本文将深入探讨不同类型的网络威胁和攻击,以及如何防范...【详细内容】
2024-01-22  IP数据云    Tags:IP地址   点击:(78)  评论:(0)  加入收藏
站内最新
站内热门
站内头条