01.08 如何編寫一個 CoreDNS 插件

目前測試環境中有很多個 DNS 服務器,不同項目組使用的 DNS 服務器不同,但是不可避免的他們會訪問一些公共域名;老的 DNS 服務器都是 dnsmasq,改起來很麻煩,最近研究了一下 CoreDNS,通過編寫插件的方式可以實現讓多個 CoreDNS 實例實現分佈式的統一控制,以下記錄了插件編寫過程

一、CoreDNS 簡介

CoreDNS 目前是 CNCF 旗下的項目(已畢業),為 Kubernetes 等雲原生環境提供可靠的 DNS 服務發現等功能;官網的描述只有一句話: CoreDNS: DNS and Service Discovery,而實際上分析源碼以後發現 CoreDNS 實際上是基於 Caddy (一個現代化的負載均衡器)而開發的,通過插件式注入,並監聽 TCP/UDP 端口提供 DNS 服務;得益於 Caddy 的插件機制,CoreDNS 支持自行編寫插件,攔截 DNS 請求然後處理,通過這個插件機制你可以在 CoreDNS 上實現各種功能,比如構建分佈式一致性的 DNS 集群、動態的 DNS 負載均衡等等

二、CoreDNS 插件規範

2.1、插件模式

CoreDNS 插件編寫目前有兩種方式:

  • 深度耦合 CoreDNS,使用 Go 編寫插件,直接編譯進 CoreDNS 二進制文件
  • 通過 GRPC 解耦,任意語言編寫 GRPC 接口實現,CoreDNS 通過 GRPC 與插件交互

由於 GRPC 鏈接實際上藉助於 CoreDNS 的 GRPC 插件,同時 GRPC 會有網絡開銷,TCP 鏈接不穩定可能造成 DNS 響應過慢等問題,所以本文只介紹如何使用 Go 編寫 CoreDNS 的插件,這種插件將直接編譯進 CoreDNS 二進制文件中

2.2、插件註冊

在通常情況下,插件中應當包含一個 setup.go 文件,這個文件的 init 方法調用插件註冊,類似這樣

<code>
func init() { plugin.Register("gdns", setup) }/<code>

註冊方法的第一個參數是插件名稱,第二個是一個 func,func 簽名如下

<code>
// SetupFunc is used to set up a plugin, or in other words,// execute a directive. It will be called>// each server block it appears in.type SetupFunc func(c *Controller) error/<code>

在這個 SetupFunc 中,插件編寫者應當通過 *Controller 拿到 CoreDNS 的配置並解析它,從而完成自己插件的初始化配置;比如你的插件需要連接 Etcd,那麼在這個方法裡你要通過 *Controller 遍歷配置,拿到 Etcd 的地址、證書、用戶名密碼配置等信息;

如果配置信息沒有問題,該插件應當初始化完成;如果有問題就報錯退出,然後整個 CoreDNS 啟動失敗;如果插件初始化完成,最後不要忘記將自己的插件加入到整個插件鏈路中(CoreDNS 根據情況逐個調用)

<code>
func setup(c *caddy.Controller) error { e, err := etcdParse(c) if err != nil { return plugin.Error("gdns", err) }
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { e.Next = next return e })
return nil}/<code>

2.3、插件結構體

一般來說,每一個插件都會定義一個結構體,結構體中包含必要的 CoreDNS 內置屬性,以及當前插件特性的相關配置;一個樣例的插件結構體如下所示

<code>
type GDNS struct { // Next 屬性在 Setup 之後會被設置到下一個插件的引用,以便在本插件解析失敗後可以交由下面的插件繼續解析 Next plugin.Handler // Fall 列表用來控制哪些域名的請求解析失敗後可以繼續穿透到下一個插件重新處理 Fall fall.F // Zones 表示當前插件應該 case 哪些域名的 DNS 請求 Zones []string
// PathPrefix 和 Client 就是插件本身的業務屬性了,由於插件要連 Etcd // PathPrefix 就是 Etcd 目錄前綴,Client 是一個 Etcd 的 client // endpoints 是 Etcd api 端點的地址 PathPrefix string Client *etcdcv3.Client endpoints []string // Stored here as well, to aid in testing.}/<code>

2.4、插件接口

一個 Go 編寫的 CoreDNS 插件實際上只需要實現一個 Handler 接口既可,接口定義如下

<code>
// Handler is like dns.Handler except ServeDNS may return an rcode// and/or error.//// If ServeDNS writes to the response body, it should return a status// code. CoreDNS assumes *no* reply has yet been written if the status// code is>//// * SERVFAIL (dns.RcodeServerFailure)//// * REFUSED (dns.RecodeRefused)//// * FORMERR (dns.RcodeFormatError)//// * NOTIMP (dns.RcodeNotImplemented)//// All other response codes signal other handlers above it that the// response message is already written, and that they should not write// to it also.//// If ServeDNS encounters an error, it should return the error value// so it can be logged by designated error-handling plugin.//// If writing a response after calling another ServeDNS method, the// returned rcode SHOULD be used when writing the response.//// If handling errors after calling another ServeDNS method, the// returned error value SHOULD be logged or handled accordingly.//// Otherwise, return values should be propagated down the plugin// chain by returning them unchanged.Handler interface { ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error) Name() string}/<code>
  • ServeDNS 方法是插件需要實現的主要邏輯方法,DNS 請求接受後會從這個方法傳入,插件編寫者需要實現查詢並返回結果
  • Name 方法只返回一個插件名稱標識,具體作用記不太清楚,好像是為了判斷插件命名唯一性然後做鏈式順序調用的,原則只要你不跟系統插件重名就行

基本邏輯就是在 setup 階段通過配置文件創建你的插件結構體對象;然後插件結構體實現這個 Handler 接口,運行期 CoreDNS 會調用接口的 ServeDNS 方法來向插件查詢 DNS 請求

2.5、ServeDNS 方法

ServeDNS 方法入參有 3 個:

  • context.Context 用來控制超時等情況的 context
  • dns.ResponseWriter 插件通過這個對象寫入對 Client DNS 請求的響應結果
  • *dns.Msg 這個是 Client 發起的 DNS 請求,插件負責處理它,比如當你發現請求類型是 AAAA 而你的插件又不想去支持時要如何返回結果

對於返回結果,插件編寫者應當通過 dns.ResponseWriter.WriteMsg 方法寫入返回結果,基本代碼如下

<code>
// ServeDNS implements the plugin.Handler interface.func (gDNS *GDNS) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
// ...... 這裡應當實現你的業務邏輯,查找相應的 DNS 記錄
// 最後通過 new 一個 dns.Msg 作為返回結果 resp := new(dns.Msg) resp.SetReply(r) resp.Authoritative = true
// records 是真正的記錄結果,應當在業務邏輯區準備好 resp.Answer = append(resp.Answer, records...)
// 返回結果 err = w.WriteMsg(resp) if err != nil { log.Error(err) }
// 告訴 CoreDNS 是否處理成功 return dns.RcodeSuccess, nil}/<code>

需要注意的是,無論根據業務邏輯是否查詢到 DNS 記錄,都要返回響應結果(沒有就返回空),錯誤或者未返回將會導致 Client 端查詢 DNS 超時,然後不斷重試,最終可能導致 Client 端服務故障

2.6、Name 方法

Name 方法非常簡單,只需要返回當前插件名稱既可;該方法的作用是為了其他插件判斷本插件是否加載等情況

<code>
// Name implements the Handler interface.func (gDNS *GDNS) Name() string { return "gdns" }/<code>

三、CoreDNS 插件處理

對於實際的業務處理,可以通過 case 請求 QType 來做具體的業務實現

<code>
// ServeDNS implements the plugin.Handler interface.func (gDNS *GDNS) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { state := request.Request{W: w, Req: r} zone := plugin.Zones(gDNS.Zones).Matches(state.Name()) if zone == "" { return plugin.NextOrFailure(gDNS.Name(), gDNS.Next, ctx, w, r) }
// ...業務處理 switch state.QType() { case dns.TypeA: // A 記錄查詢業務邏輯 case dns.TypeAAAA: // AAAA 記錄查詢業務邏輯 default: return false
resp := new(dns.Msg) resp.SetReply(r) resp.Authoritative = true resp.Answer = append(resp.Answer, records...) err = w.WriteMsg(resp) if err != nil { log.Error(err) }
return dns.RcodeSuccess, nil}/<code>

四、插件編譯及測試

4.1、官方標準操作

根據官方文檔的描述,當你編寫好插件以後,你的插件應當提交到一個 Git 倉庫中,可以使 Github 等(保證可以 go get 拉取就行),然後修改 plugin.cfg,最後執行 make 既可;具體修改如下所示

如何編寫一個 CoreDNS 插件

值得注意的是: 插件配置在 plugin.cfg 內的順序決定了插件的執行順序;通俗的講,如果 Client 的一個 DNS 請求進來,CoreDNS 根據你在 plugin.cfg 內書寫的順序依次調用,而並非 Corefile 內的配置順序

配置好以後直接執行 make 既可編譯成功一個包含自定義插件的 CoreDNS 二進制文件(編譯過程的 go mod 下載加速問題不在本文討論範圍內);你可以直接通過這個二進制測試插件的處理情況,當然這種測試不夠直觀,而且頻繁修改由於 go mod 緩存等原因並不一定能保證每次編譯的都包含最新插件代碼,所以另一種方式請看下一章節

4.2、經驗性的操作

根據個人測試以及對源碼的分析,在修改 plugin.cfg 然後執行 make 命令後,實際上是進行了代碼生成;當你通過 git 命令查看相關修改文件時,整個插件加載體系便沒什麼秘密可言了;在整個插件體系中,插件加載是通過 init 方法註冊的,那麼既然用 go 寫插件,那麼應該清楚 init 方法只有在包引用之後才會執行,所以整個插件體系實際上是這樣事兒的:

首先 make 以後會修改 core/plugin/zplugin.go 文件,這個文件啥也不幹,就是 import 來實現調用對應包的 init 方法

如何編寫一個 CoreDNS 插件

當 init 執行後你去追源碼,實際上就是 Caddy 維護了一個 map[string]Plugin,init 會把你的插件 func 塞進去然後後面再調用,實現一個懶加載或者說延遲初始化

如何編寫一個 CoreDNS 插件

接著修改了一下 core/dnsserver/zdirectives.go,這個裡面也沒啥,就是一個 []string,但是 []string 這玩意有順序啊,這就是為什麼你在 plugin.cfg 裡寫的順序決定了插件處理順序的原因(因為生成的這個切片有順序)

如何編寫一個 CoreDNS 插件

綜上所述,實際上 make 命令一共修改了兩個文件,如果想在 IDE 內直接 debug CoreDNS + Plugin 源碼,那麼只需要這樣做:

複製自己編寫的插件目錄到 plugin 目錄,類似這樣

如何編寫一個 CoreDNS 插件

手動修改 core/plugin/zplugin.go,加入自己插件的 import(此時你直接複製系統其他插件,改一下目錄名既可)

如何編寫一個 CoreDNS 插件

手動修改 core/dnsserver/zdirectives.go 把自己插件名稱寫進去(自己控制順序),然後 debug 啟動 coredns.go 裡面的 main 方法測試既可

如何編寫一個 CoreDNS 插件

五、本文參考

  • Writing Plugins for CoreDNS: https://coredns.io/2016/12/19/writing-plugins-for-coredns
  • how-to-add-plugins.md: https://github.com/coredns/coredns.io/blob/master/content/blog/how-to-add-plugins.md
  • example plugin: https://github.com/coredns/example


原文:https://mritd.me/2019/11/05/writing-plugin-for-coredns/



分享到:


相關文章: