CoreDNSは、プラグインをチェーンするDNSサーバーです。プラグインは、リクエストを受け取り、クライアントに応答するか、次のプラグインに渡すメソッド`ServeDNS()`として定義されます。どのプラグインもリクエストを処理しない場合、SERVFAILのデフォルト応答が返されます。
このブログ記事では、CoreDNSにプラグインを追加する方法について詳しく説明します。Corefileが指定されていない場合、デフォルトでロードされるCoreDNSプラグインである`whoami`プラグインの例を使用します。
ここで紹介するすべてのコード例は、CoreDNSがGo言語で記述されているため、Go言語です。
まず、「プラグインは何をするべきか?」という疑問があります。`whoami`プラグインの場合、その目的はクライアントのIPアドレス(IPv4またはIPv6)、使用されているトランスポート(「tcp」または「udp」)、およびリクエストポート番号をエコーバックすることです。
次の質問は、「この新しいプラグインの名前は何ですか?」です。短く、説明的な名前を見つけようとします。この例では、すでに(良い)名前がありました:`whoami`。
プラグイン
プラグインはいくつかの部分で構成されています。
- プラグインの登録
- Corefileから`whoami`プラグインと可能な引数を解析する設定関数
- `ServeDNS()`メソッドと`Name()`メソッド
各部分を定義した後、次のことができます。
- 接続する
- 使用する
1. 登録
通常、プラグインには登録を処理する`setup.go`というファイルがあります。その中で、`init`関数は次のようになります。
func init() { plugin.Register("whoami", setup) }
`setup`は、Corefileの解析を処理する設定関数の名前です。その役割は、`plugin.Handler`インターフェースを実装する型を返すことです。
そのため、Corefileパーサーが「whoami」を検出すると、`whoami.setup`が呼び出されます。
2. 設定関数
`whoami`プラグインはオプションを許可しないため、設定関数は比較的シンプルです。
func setupWhoami(c *caddy.Controller) error {
c.Next() // 'whoami'
if c.NextArg() {
return plugin.Error("whoami", c.ArgErr())
}
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
return Whoami{Next: next} // Set the Next field, so the plugin chaining works.
})
return nil
}
Corefileからトークンを受け取り、それらに基づいて処理するために`*caddy.Controller`を使用します。ここでは、トークン`whoami`の後に何も指定されていないかどうかだけを確認します。さらに処理が必要な場合は、`c.Val()`、`c.Args()`、および関連関数があります。
`whoami`の完全な`setup.go`はこちらにあります。
解析もテストする必要があることに注意してください。setup_test.goを参照してください。
3. `ServeDNS()`と`Name()`
他のプラグインが特定のプラグインがロードされているかどうかを確認するために使用される、単純な`Name()`メソッドから始めましょう。このメソッドは、文字列「whoami」を返します。
// Name implements the Handler interface.
func (wh Whoami) Name() string { return "whoami" }
次に、重要な部分である`ServeDNS`メソッドです。このメソッドを1行ずつ見ていきましょう。
// ServeDNS implements the plugin.Handler interface.
func (wh Whoami) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
ご覧のとおり、この関数はいくつかのパラメーターを受け取ります。そのうち`w`はクライアント側です。`w`への書き込みはクライアントへの応答を返します。以下でわかるように、クライアント接続のすべての興味深いプロパティはdns.ResponseWriterから取得できます。`r`は着信クエリです。`ServeDNS`メソッドは整数と/またはエラーを返します。整数が取ることができる値はDNS RCODEs、dns.RcodeServerFailure、dns.RcodeNotImplemented、dns.RcodeSuccessなどです。成功した戻り値は、プラグインがクライアントに書き込んだことを示します。
`request.Request`は、EDNS0レコードやDNSSEC OKビットなど、クライアント側のプロパティを抽象化およびキャッシュするヘルパー構造体です。
次に、返信メッセージを設定します。
a := &dns.Msg{}
a.SetReply(r)
a.Authoritative = true
新しいメッセージを作成し、着信返信から返そうとしているメッセージへの関連ビットをコピーします。いくつかのメッセージビットを調整し、信頼できるものとして設定します。
次に、`state`ヘルパー構造体を使用して着信メッセージを検査し、何を返すかを調べます。
ip := state.IP()
var rr dns.RR
switch state.Family() {
case 1:
rr = &dns.A{}
rr.(*dns.A).Hdr = dns.RR_Header{Name: state.QName(),
Rrtype: dns.TypeA, Class: state.QClass()}
rr.(*dns.A).A = net.ParseIP(ip).To4()
case 2:
rr = &dns.AAAA{}
rr.(*dns.AAAA).Hdr = dns.RR_Header{Name: state.QName(),
Rrtype: dns.TypeAAAA, Class: state.QClass()}
rr.(*dns.AAAA).AAAA = net.ParseIP(ip)
}
`IP()`はクライアントのIPアドレスを返します。`Family()`は使用されているIPバージョンを返します。ファミリに応じて、クライアントのアドレスを持つAレコードまたはAAAAレコードを作成します。TTLを指定しないため、0になります。つまり、これらのレコードはキャッシュされるべきではありません。
次に、クライアントの送信元ポートと使用されているトランスポートプロトコルをエンコードします。
srv := &dns.SRV{}
srv.Hdr = dns.RR_Header{Name: "_" + state.Proto() + "." + state.QName(),
Rrtype: dns.TypeSRV, Class: state.QClass()}
port, _ := strconv.Atoi(state.Port())
srv.Port = uint16(port)
srv.Target = "."
SRVレコードはそれに最適です。ドメイン名は`_tcp`または`_udp`で始まるか、SRVレコードのポート番号がクライアントのポート番号をエコーバックするために再利用されます。つまり、次のようなものが作成されます:`_tcp.example.org. 0 IN SRV 0 0 <portNr>`。
このメソッドの最後の部分では、完全なメッセージを作成して送信します。
a.Extra = []dns.RR{rr, srv}
w.WriteMsg(a)
return 0, nil
まず、作成した2つのリソースレコード(`rr`と`srv`)を回答メッセージの追加セクションに追加します:`a.Extra`。
最後に、`w`に対して`WriteMsg`メソッドを呼び出します。これはメッセージをクライアントに書き戻します。0と`nil`を返すことで、成功した(失敗した可能性がありますが、`WriteMsg`の戻り値を確認していません)書き込みを示します。
4. 接続する
次に、CoreDNSにこの新しいプラグインをコンパイルして使用させる必要があります。プラグインの追加は最近簡素化され、`plugin.cfg`を編集して次の行を追加するだけです。
whoami:whoami
最初の数字はプラグインのソートに使用され(詳細については後述)、次に登録で使用されるプラグインの名前、そしてCoreDNSプラグインディレクトリ内のパッケージが続きます。
各プラグインは、他のすべてのプラグインのリストの中で特定の場所を占めます。たとえば、キャッシングプラグインやメトリクスプラグインは、クエリと応答を「確認」してキャッシュ処理やメトリクス処理を行うことができるように、早期に配置する必要があります。`whoami`プラグインはそれほど特別ではなく、リストの比較的遅い位置に配置できます(したがって、番号が大きくなっています)。
これで、`make`(または`go generate && go build`)を実行して、新しいプラグインで使用できる`coredns`バイナリを取得できます。このバイナリは、`-plugins`で呼び出されたときに`dns.whoami`を含める必要があります。
5. 使用する
Corefileを書き込みます。
. {
whoami
}
これは、ルート`.`とその下に対して権威のあるものになることを意味します。つまり、すべての可能なクエリがこの節にヒットします。そして、各リクエストに対して`whoami`を呼び出します。
次のようにしてCoreDNSを起動します:`coredns -conf Corefile -dns.port 1053`。いくつかの注意事項があります。CoreDNSは現在のディレクトリでCorefileを探しますので、`-conf Corefile`は完全性のためにここでのみ指定されています。`-dns.port`はCoreDNSをポート1053で起動するため、rootとして実行する必要はありません。
CoreDNSは次のように出力します。
.:1053
CoreDNS-1.6.4
linux/amd64, go1.13.1, b139ba3
`.:1053`は、Corefileを解析し、ルート`.`ゾーンとその下のクエリに対してポート1053でリスンしていることを示しています。
そこで、`dig`でクエリを送信してみましょう。
% dig +nocmd @localhost mx example.org -p1053 +noall +additional
example.org. 0 IN A 127.0.0.1
_udp.example.org. 0 IN SRV 0 0 58359 .
素晴らしい。動作しています。応答を確認すると、このリクエストはポート58359からUDP経由でIPv4を使用して送信されました。
TCPを使用してみましょう。
% dig +nocmd @localhost a example.org -p1053 +noall +additional +tcp
example.org. 0 IN A 127.0.0.1
_tcp.example.org. 0 IN SRV 0 0 33435 .
はい、今回はTCPを使用したこと(そしてもちろん別のポートを使用したこと)が正しく認識されています。
CoreDNSにはすでに多くの異なるプラグインがあります。新しくエキサイティングなプラグインは常に歓迎されます。アイデアがあれば、トラッカーで問題を開いてください。
これは、CoreDNSプラグインの記述に関する以前のブログの更新版です。
より単純な、単なる例として機能するプラグインについては、サンプルプラグインも参照してください。