mirror of https://github.com/kubernetes/kops.git
Merge pull request #2343 from vmware/vsphere-vendor-code
Add vSphere vendor codes
This commit is contained in:
commit
0294a90f6a
|
|
@ -274,3 +274,15 @@
|
|||
[submodule "_vendor/github.com/golang/protobuf"]
|
||||
path = _vendor/github.com/golang/protobuf
|
||||
url = https://github.com/golang/protobuf
|
||||
[submodule "_vendor/github.com/vmware/govmomi"]
|
||||
path = _vendor/github.com/vmware/govmomi
|
||||
url = https://github.com/vmware/govmomi.git
|
||||
[submodule "_vendor/github.com/coreos/go-semver"]
|
||||
path = _vendor/github.com/coreos/go-semver
|
||||
url = https://github.com/coreos/go-semver.git
|
||||
[submodule "_vendor/github.com/miekg/coredns"]
|
||||
path = _vendor/github.com/miekg/coredns
|
||||
url = https://github.com/miekg/coredns.git
|
||||
[submodule "_vendor/github.com/miekg/dns"]
|
||||
path = _vendor/github.com/miekg/dns
|
||||
url = https://github.com/miekg/dns.git
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 5e3acbb5668c4c3deb4842615c4098eb61fb6b1e
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 757f49d8ff2687d289468b00835f360614357252
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 25ac7f171497271bc74ad3c6b5e1f86b4bab54fa
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 2d7d7b3702fddc23a76ac283c3a3d56bb0375e62
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- tip
|
||||
script: cd semver && go test
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# go-semver - Semantic Versioning Library
|
||||
|
||||
[](https://travis-ci.org/coreos/go-semver)
|
||||
[](https://godoc.org/github.com/coreos/go-semver/semver)
|
||||
|
||||
go-semver is a [semantic versioning][semver] library for Go. It lets you parse
|
||||
and compare two semantic version strings.
|
||||
|
||||
[semver]: http://semver.org/
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
vA := semver.New("1.2.3")
|
||||
vB := semver.New("3.2.1")
|
||||
|
||||
fmt.Printf("%s < %s == %t\n", vA, vB, vA.LessThan(*vB))
|
||||
```
|
||||
|
||||
## Example Application
|
||||
|
||||
```
|
||||
$ go run example.go 1.2.3 3.2.1
|
||||
1.2.3 < 3.2.1 == true
|
||||
|
||||
$ go run example.go 5.2.3 3.2.1
|
||||
5.2.3 < 3.2.1 == false
|
||||
```
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/coreos/go-semver/semver"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
vA, err := semver.NewVersion(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
vB, err := semver.NewVersion(os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
|
||||
fmt.Printf("%s < %s == %t\n", vA, vB, vA.LessThan(*vB))
|
||||
}
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
// Copyright 2013-2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Semantic Versions http://semver.org
|
||||
package semver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Version struct {
|
||||
Major int64
|
||||
Minor int64
|
||||
Patch int64
|
||||
PreRelease PreRelease
|
||||
Metadata string
|
||||
}
|
||||
|
||||
type PreRelease string
|
||||
|
||||
func splitOff(input *string, delim string) (val string) {
|
||||
parts := strings.SplitN(*input, delim, 2)
|
||||
|
||||
if len(parts) == 2 {
|
||||
*input = parts[0]
|
||||
val = parts[1]
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func New(version string) *Version {
|
||||
return Must(NewVersion(version))
|
||||
}
|
||||
|
||||
func NewVersion(version string) (*Version, error) {
|
||||
v := Version{}
|
||||
|
||||
if err := v.Set(version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
// Must is a helper for wrapping NewVersion and will panic if err is not nil.
|
||||
func Must(v *Version, err error) *Version {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Set parses and updates v from the given version string. Implements flag.Value
|
||||
func (v *Version) Set(version string) error {
|
||||
metadata := splitOff(&version, "+")
|
||||
preRelease := PreRelease(splitOff(&version, "-"))
|
||||
dotParts := strings.SplitN(version, ".", 3)
|
||||
|
||||
if len(dotParts) != 3 {
|
||||
return fmt.Errorf("%s is not in dotted-tri format", version)
|
||||
}
|
||||
|
||||
parsed := make([]int64, 3, 3)
|
||||
|
||||
for i, v := range dotParts[:3] {
|
||||
val, err := strconv.ParseInt(v, 10, 64)
|
||||
parsed[i] = val
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
v.Metadata = metadata
|
||||
v.PreRelease = preRelease
|
||||
v.Major = parsed[0]
|
||||
v.Minor = parsed[1]
|
||||
v.Patch = parsed[2]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
fmt.Fprintf(&buffer, "%d.%d.%d", v.Major, v.Minor, v.Patch)
|
||||
|
||||
if v.PreRelease != "" {
|
||||
fmt.Fprintf(&buffer, "-%s", v.PreRelease)
|
||||
}
|
||||
|
||||
if v.Metadata != "" {
|
||||
fmt.Fprintf(&buffer, "+%s", v.Metadata)
|
||||
}
|
||||
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (v *Version) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var data string
|
||||
if err := unmarshal(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
return v.Set(data)
|
||||
}
|
||||
|
||||
func (v Version) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"` + v.String() + `"`), nil
|
||||
}
|
||||
|
||||
func (v *Version) UnmarshalJSON(data []byte) error {
|
||||
l := len(data)
|
||||
if l == 0 || string(data) == `""` {
|
||||
return nil
|
||||
}
|
||||
if l < 2 || data[0] != '"' || data[l-1] != '"' {
|
||||
return errors.New("invalid semver string")
|
||||
}
|
||||
return v.Set(string(data[1 : l-1]))
|
||||
}
|
||||
|
||||
// Compare tests if v is less than, equal to, or greater than versionB,
|
||||
// returning -1, 0, or +1 respectively.
|
||||
func (v Version) Compare(versionB Version) int {
|
||||
if cmp := recursiveCompare(v.Slice(), versionB.Slice()); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return preReleaseCompare(v, versionB)
|
||||
}
|
||||
|
||||
// Equal tests if v is equal to versionB.
|
||||
func (v Version) Equal(versionB Version) bool {
|
||||
return v.Compare(versionB) == 0
|
||||
}
|
||||
|
||||
// LessThan tests if v is less than versionB.
|
||||
func (v Version) LessThan(versionB Version) bool {
|
||||
return v.Compare(versionB) < 0
|
||||
}
|
||||
|
||||
// Slice converts the comparable parts of the semver into a slice of integers.
|
||||
func (v Version) Slice() []int64 {
|
||||
return []int64{v.Major, v.Minor, v.Patch}
|
||||
}
|
||||
|
||||
func (p PreRelease) Slice() []string {
|
||||
preRelease := string(p)
|
||||
return strings.Split(preRelease, ".")
|
||||
}
|
||||
|
||||
func preReleaseCompare(versionA Version, versionB Version) int {
|
||||
a := versionA.PreRelease
|
||||
b := versionB.PreRelease
|
||||
|
||||
/* Handle the case where if two versions are otherwise equal it is the
|
||||
* one without a PreRelease that is greater */
|
||||
if len(a) == 0 && (len(b) > 0) {
|
||||
return 1
|
||||
} else if len(b) == 0 && (len(a) > 0) {
|
||||
return -1
|
||||
}
|
||||
|
||||
// If there is a prerelease, check and compare each part.
|
||||
return recursivePreReleaseCompare(a.Slice(), b.Slice())
|
||||
}
|
||||
|
||||
func recursiveCompare(versionA []int64, versionB []int64) int {
|
||||
if len(versionA) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
a := versionA[0]
|
||||
b := versionB[0]
|
||||
|
||||
if a > b {
|
||||
return 1
|
||||
} else if a < b {
|
||||
return -1
|
||||
}
|
||||
|
||||
return recursiveCompare(versionA[1:], versionB[1:])
|
||||
}
|
||||
|
||||
func recursivePreReleaseCompare(versionA []string, versionB []string) int {
|
||||
// A larger set of pre-release fields has a higher precedence than a smaller set,
|
||||
// if all of the preceding identifiers are equal.
|
||||
if len(versionA) == 0 {
|
||||
if len(versionB) > 0 {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
} else if len(versionB) == 0 {
|
||||
// We're longer than versionB so return 1.
|
||||
return 1
|
||||
}
|
||||
|
||||
a := versionA[0]
|
||||
b := versionB[0]
|
||||
|
||||
aInt := false
|
||||
bInt := false
|
||||
|
||||
aI, err := strconv.Atoi(versionA[0])
|
||||
if err == nil {
|
||||
aInt = true
|
||||
}
|
||||
|
||||
bI, err := strconv.Atoi(versionB[0])
|
||||
if err == nil {
|
||||
bInt = true
|
||||
}
|
||||
|
||||
// Numeric identifiers always have lower precedence than non-numeric identifiers.
|
||||
if aInt && !bInt {
|
||||
return -1
|
||||
} else if !aInt && bInt {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Handle Integer Comparison
|
||||
if aInt && bInt {
|
||||
if aI > bI {
|
||||
return 1
|
||||
} else if aI < bI {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
// Handle String Comparison
|
||||
if a > b {
|
||||
return 1
|
||||
} else if a < b {
|
||||
return -1
|
||||
}
|
||||
|
||||
return recursivePreReleaseCompare(versionA[1:], versionB[1:])
|
||||
}
|
||||
|
||||
// BumpMajor increments the Major field by 1 and resets all other fields to their default values
|
||||
func (v *Version) BumpMajor() {
|
||||
v.Major += 1
|
||||
v.Minor = 0
|
||||
v.Patch = 0
|
||||
v.PreRelease = PreRelease("")
|
||||
v.Metadata = ""
|
||||
}
|
||||
|
||||
// BumpMinor increments the Minor field by 1 and resets all other fields to their default values
|
||||
func (v *Version) BumpMinor() {
|
||||
v.Minor += 1
|
||||
v.Patch = 0
|
||||
v.PreRelease = PreRelease("")
|
||||
v.Metadata = ""
|
||||
}
|
||||
|
||||
// BumpPatch increments the Patch field by 1 and resets all other fields to their default values
|
||||
func (v *Version) BumpPatch() {
|
||||
v.Patch += 1
|
||||
v.PreRelease = PreRelease("")
|
||||
v.Metadata = ""
|
||||
}
|
||||
|
|
@ -0,0 +1,371 @@
|
|||
// Copyright 2013-2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package semver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type fixture struct {
|
||||
GreaterVersion string
|
||||
LesserVersion string
|
||||
}
|
||||
|
||||
var fixtures = []fixture{
|
||||
fixture{"0.0.0", "0.0.0-foo"},
|
||||
fixture{"0.0.1", "0.0.0"},
|
||||
fixture{"1.0.0", "0.9.9"},
|
||||
fixture{"0.10.0", "0.9.0"},
|
||||
fixture{"0.99.0", "0.10.0"},
|
||||
fixture{"2.0.0", "1.2.3"},
|
||||
fixture{"0.0.0", "0.0.0-foo"},
|
||||
fixture{"0.0.1", "0.0.0"},
|
||||
fixture{"1.0.0", "0.9.9"},
|
||||
fixture{"0.10.0", "0.9.0"},
|
||||
fixture{"0.99.0", "0.10.0"},
|
||||
fixture{"2.0.0", "1.2.3"},
|
||||
fixture{"0.0.0", "0.0.0-foo"},
|
||||
fixture{"0.0.1", "0.0.0"},
|
||||
fixture{"1.0.0", "0.9.9"},
|
||||
fixture{"0.10.0", "0.9.0"},
|
||||
fixture{"0.99.0", "0.10.0"},
|
||||
fixture{"2.0.0", "1.2.3"},
|
||||
fixture{"1.2.3", "1.2.3-asdf"},
|
||||
fixture{"1.2.3", "1.2.3-4"},
|
||||
fixture{"1.2.3", "1.2.3-4-foo"},
|
||||
fixture{"1.2.3-5-foo", "1.2.3-5"},
|
||||
fixture{"1.2.3-5", "1.2.3-4"},
|
||||
fixture{"1.2.3-5-foo", "1.2.3-5-Foo"},
|
||||
fixture{"3.0.0", "2.7.2+asdf"},
|
||||
fixture{"3.0.0+foobar", "2.7.2"},
|
||||
fixture{"1.2.3-a.10", "1.2.3-a.5"},
|
||||
fixture{"1.2.3-a.b", "1.2.3-a.5"},
|
||||
fixture{"1.2.3-a.b", "1.2.3-a"},
|
||||
fixture{"1.2.3-a.b.c.10.d.5", "1.2.3-a.b.c.5.d.100"},
|
||||
fixture{"1.0.0", "1.0.0-rc.1"},
|
||||
fixture{"1.0.0-rc.2", "1.0.0-rc.1"},
|
||||
fixture{"1.0.0-rc.1", "1.0.0-beta.11"},
|
||||
fixture{"1.0.0-beta.11", "1.0.0-beta.2"},
|
||||
fixture{"1.0.0-beta.2", "1.0.0-beta"},
|
||||
fixture{"1.0.0-beta", "1.0.0-alpha.beta"},
|
||||
fixture{"1.0.0-alpha.beta", "1.0.0-alpha.1"},
|
||||
fixture{"1.0.0-alpha.1", "1.0.0-alpha"},
|
||||
fixture{"1.2.3-rc.1-1-1hash", "1.2.3-rc.2"},
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
for _, v := range fixtures {
|
||||
gt, err := NewVersion(v.GreaterVersion)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
lt, err := NewVersion(v.LesserVersion)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if gt.LessThan(*lt) {
|
||||
t.Errorf("%s should not be less than %s", gt, lt)
|
||||
}
|
||||
if gt.Equal(*lt) {
|
||||
t.Errorf("%s should not be equal to %s", gt, lt)
|
||||
}
|
||||
if gt.Compare(*lt) <= 0 {
|
||||
t.Errorf("%s should be greater than %s", gt, lt)
|
||||
}
|
||||
if !lt.LessThan(*gt) {
|
||||
t.Errorf("%s should be less than %s", lt, gt)
|
||||
}
|
||||
if !lt.Equal(*lt) {
|
||||
t.Errorf("%s should be equal to %s", lt, lt)
|
||||
}
|
||||
if lt.Compare(*gt) > 0 {
|
||||
t.Errorf("%s should not be greater than %s", lt, gt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testString(t *testing.T, orig string, version *Version) {
|
||||
if orig != version.String() {
|
||||
t.Errorf("%s != %s", orig, version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
for _, v := range fixtures {
|
||||
gt, err := NewVersion(v.GreaterVersion)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
testString(t, v.GreaterVersion, gt)
|
||||
|
||||
lt, err := NewVersion(v.LesserVersion)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
testString(t, v.LesserVersion, lt)
|
||||
}
|
||||
}
|
||||
|
||||
func shuffleStringSlice(src []string) []string {
|
||||
dest := make([]string, len(src))
|
||||
rand.Seed(time.Now().Unix())
|
||||
perm := rand.Perm(len(src))
|
||||
for i, v := range perm {
|
||||
dest[v] = src[i]
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
sortedVersions := []string{"1.0.0", "1.0.2", "1.2.0", "3.1.1"}
|
||||
unsortedVersions := shuffleStringSlice(sortedVersions)
|
||||
|
||||
semvers := []*Version{}
|
||||
for _, v := range unsortedVersions {
|
||||
sv, err := NewVersion(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
semvers = append(semvers, sv)
|
||||
}
|
||||
|
||||
Sort(semvers)
|
||||
|
||||
for idx, sv := range semvers {
|
||||
if sv.String() != sortedVersions[idx] {
|
||||
t.Fatalf("incorrect sort at index %v", idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBumpMajor(t *testing.T) {
|
||||
version, _ := NewVersion("1.0.0")
|
||||
version.BumpMajor()
|
||||
if version.Major != 2 {
|
||||
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
|
||||
}
|
||||
|
||||
version, _ = NewVersion("1.5.2")
|
||||
version.BumpMajor()
|
||||
if version.Minor != 0 && version.Patch != 0 {
|
||||
t.Fatalf("bumping major on 1.5.2 resulted in %v", version)
|
||||
}
|
||||
|
||||
version, _ = NewVersion("1.0.0+build.1-alpha.1")
|
||||
version.BumpMajor()
|
||||
if version.PreRelease != "" && version.Metadata != "" {
|
||||
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBumpMinor(t *testing.T) {
|
||||
version, _ := NewVersion("1.0.0")
|
||||
version.BumpMinor()
|
||||
|
||||
if version.Major != 1 {
|
||||
t.Fatalf("bumping minor on 1.0.0 resulted in %v", version)
|
||||
}
|
||||
|
||||
if version.Minor != 1 {
|
||||
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
|
||||
}
|
||||
|
||||
version, _ = NewVersion("1.0.0+build.1-alpha.1")
|
||||
version.BumpMinor()
|
||||
if version.PreRelease != "" && version.Metadata != "" {
|
||||
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBumpPatch(t *testing.T) {
|
||||
version, _ := NewVersion("1.0.0")
|
||||
version.BumpPatch()
|
||||
|
||||
if version.Major != 1 {
|
||||
t.Fatalf("bumping minor on 1.0.0 resulted in %v", version)
|
||||
}
|
||||
|
||||
if version.Minor != 0 {
|
||||
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
|
||||
}
|
||||
|
||||
if version.Patch != 1 {
|
||||
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
|
||||
}
|
||||
|
||||
version, _ = NewVersion("1.0.0+build.1-alpha.1")
|
||||
version.BumpPatch()
|
||||
if version.PreRelease != "" && version.Metadata != "" {
|
||||
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMust(t *testing.T) {
|
||||
tests := []struct {
|
||||
versionStr string
|
||||
|
||||
version *Version
|
||||
recov interface{}
|
||||
}{
|
||||
{
|
||||
versionStr: "1.0.0",
|
||||
version: &Version{Major: 1},
|
||||
},
|
||||
{
|
||||
versionStr: "version number",
|
||||
recov: errors.New("version number is not in dotted-tri format"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
func() {
|
||||
defer func() {
|
||||
recov := recover()
|
||||
if !reflect.DeepEqual(tt.recov, recov) {
|
||||
t.Fatalf("incorrect panic for %q: want %v, got %v", tt.versionStr, tt.recov, recov)
|
||||
}
|
||||
}()
|
||||
|
||||
version := Must(NewVersion(tt.versionStr))
|
||||
if !reflect.DeepEqual(tt.version, version) {
|
||||
t.Fatalf("incorrect version for %q: want %+v, got %+v", tt.versionStr, tt.version, version)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
type fixtureJSON struct {
|
||||
GreaterVersion *Version
|
||||
LesserVersion *Version
|
||||
}
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
fj := make([]fixtureJSON, len(fixtures))
|
||||
for i, v := range fixtures {
|
||||
var err error
|
||||
fj[i].GreaterVersion, err = NewVersion(v.GreaterVersion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fj[i].LesserVersion, err = NewVersion(v.LesserVersion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
fromStrings, err := json.Marshal(fixtures)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fromVersions, err := json.Marshal(fj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(fromStrings, fromVersions) {
|
||||
t.Errorf("Expected: %s", fromStrings)
|
||||
t.Errorf("Unexpected: %s", fromVersions)
|
||||
}
|
||||
|
||||
fromJson := make([]fixtureJSON, 0, len(fj))
|
||||
err = json.Unmarshal(fromStrings, &fromJson)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(fromJson, fj) {
|
||||
t.Error("Expected: ", fj)
|
||||
t.Error("Unexpected: ", fromJson)
|
||||
}
|
||||
}
|
||||
|
||||
func TestYAML(t *testing.T) {
|
||||
document, err := yaml.Marshal(fixtures)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := make([]fixtureJSON, len(fixtures))
|
||||
for i, v := range fixtures {
|
||||
var err error
|
||||
expected[i].GreaterVersion, err = NewVersion(v.GreaterVersion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected[i].LesserVersion, err = NewVersion(v.LesserVersion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
fromYAML := make([]fixtureJSON, 0, len(fixtures))
|
||||
err = yaml.Unmarshal(document, &fromYAML)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(fromYAML, expected) {
|
||||
t.Error("Expected: ", expected)
|
||||
t.Error("Unexpected: ", fromYAML)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadInput(t *testing.T) {
|
||||
bad := []string{
|
||||
"1.2",
|
||||
"1.2.3x",
|
||||
"0x1.3.4",
|
||||
"-1.2.3",
|
||||
"1.2.3.4",
|
||||
}
|
||||
for _, b := range bad {
|
||||
if _, err := NewVersion(b); err == nil {
|
||||
t.Error("Improperly accepted value: ", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlag(t *testing.T) {
|
||||
v := Version{}
|
||||
f := flag.NewFlagSet("version", flag.ContinueOnError)
|
||||
f.Var(&v, "version", "set version")
|
||||
|
||||
if err := f.Set("version", "1.2.3"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if v.String() != "1.2.3" {
|
||||
t.Errorf("Set wrong value %q", v)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleVersion_LessThan() {
|
||||
vA := New("1.2.3")
|
||||
vB := New("3.2.1")
|
||||
|
||||
fmt.Printf("%s < %s == %t\n", vA, vB, vA.LessThan(*vB))
|
||||
// Output:
|
||||
// 1.2.3 < 3.2.1 == true
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2013-2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package semver
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
type Versions []*Version
|
||||
|
||||
func (s Versions) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s Versions) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
func (s Versions) Less(i, j int) bool {
|
||||
return s[i].LessThan(*s[j])
|
||||
}
|
||||
|
||||
// Sort sorts the given slice of Version
|
||||
func Sort(versions []*Version) {
|
||||
sort.Sort(Versions(versions))
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
query.log
|
||||
Corefile
|
||||
*.swp
|
||||
coredns
|
||||
kubectl
|
||||
go-test-tmpfile*
|
||||
coverage.txt
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
sudo: required
|
||||
# Trusty distribution is much faster when sudo is required
|
||||
dist: trusty
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
language: go
|
||||
go:
|
||||
- 1.8
|
||||
|
||||
go_import_path: github.com/coredns/coredns
|
||||
|
||||
env:
|
||||
- ETCD_VERSION=2.3.1 K8S_VERSION=1.3.7 KUBECTL="docker exec hyperkube /hyperkube kubectl" DNS_ARGUMENTS=""
|
||||
|
||||
# In the Travis VM-based build environment, IPv6 networking is not
|
||||
# enabled by default. The sysctl operations below enable IPv6.
|
||||
# IPv6 is needed by some of the CoreDNS test cases. The VM environment
|
||||
# is needed to have access to sudo in the test environment. Sudo is
|
||||
# needed to have docker in the test environment. Docker is needed to
|
||||
# launch a kubernetes instance in the test environment.
|
||||
# (Dependencies are fun! :) )
|
||||
before_install:
|
||||
- cat /proc/net/if_inet6
|
||||
- uname -a
|
||||
- sudo bash -c 'if [ `cat /proc/net/if_inet6 | wc -l` = "0" ]; then echo "Enabling IPv6" ; sysctl net.ipv6.conf.all.disable_ipv6=0 ; sysctl net.ipv6.conf.default.disable_ipv6=0 ; sysctl net.ipv6.conf.lo.disable_ipv6=0 ; fi'
|
||||
- cat /proc/net/if_inet6
|
||||
- env
|
||||
|
||||
before_script:
|
||||
- docker run -d --net=host --name=etcd quay.io/coreos/etcd:v$ETCD_VERSION
|
||||
- docker run -d --volume=/:/rootfs:ro --volume=/sys:/sys:ro --volume=/var/lib/docker/:/var/lib/docker:rw --volume=/var/lib/kubelet/:/var/lib/kubelet:rw --volume=/var/run:/var/run:rw --volume=`pwd`/.travis:/travis --net=host --pid=host --privileged --name=hyperkube gcr.io/google_containers/hyperkube-amd64:v$K8S_VERSION /hyperkube kubelet --containerized --hostname-override=127.0.0.1 --api-servers=http://localhost:8080 --config=/etc/kubernetes/manifests $DNS_ARGUMENTS --allow-privileged --v=2
|
||||
# Wait until kubectl is ready
|
||||
- for i in {1..10}; do $KUBECTL version && break || sleep 5; done
|
||||
- $KUBECTL version
|
||||
- $KUBECTL config set-cluster test-doc --server=http://localhost:8080
|
||||
- $KUBECTL config set-context test-doc --cluster=test-doc
|
||||
- $KUBECTL config use-context test-doc
|
||||
# Wait until k8s is ready
|
||||
- for i in {1..30}; do $KUBECTL get nodes && break || sleep 5; done
|
||||
- $KUBECTL create -f /travis/kubernetes/dns-test.yaml
|
||||
- docker ps -a
|
||||
|
||||
script:
|
||||
- make coverage
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: test-1
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: test-2
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: de-1-a
|
||||
namespace: test-1
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: app-1-a
|
||||
spec:
|
||||
containers:
|
||||
- name: app-1-a-c
|
||||
image: gcr.io/google_containers/pause-amd64:3.0
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
protocol: TCP
|
||||
- containerPort: 443
|
||||
name: https
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: de-1-b
|
||||
namespace: test-1
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: app-1-b
|
||||
spec:
|
||||
containers:
|
||||
- name: app-1-b-c
|
||||
image: gcr.io/google_containers/pause-amd64:3.0
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: de-c
|
||||
namespace: test-1
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: app-c
|
||||
spec:
|
||||
containers:
|
||||
- name: app-c-c
|
||||
image: gcr.io/google_containers/pause-amd64:3.0
|
||||
ports:
|
||||
- containerPort: 1234
|
||||
name: c-port
|
||||
protocol: UDP
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: de-c
|
||||
namespace: test-2
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: app-c
|
||||
spec:
|
||||
containers:
|
||||
- name: app-c-c
|
||||
image: gcr.io/google_containers/pause-amd64:3.0
|
||||
ports:
|
||||
- containerPort: 1234
|
||||
name: c-port
|
||||
protocol: UDP
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: de-d1
|
||||
namespace: test-1
|
||||
spec:
|
||||
replicas: 2
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: app-d
|
||||
spec:
|
||||
containers:
|
||||
- name: app-d-c
|
||||
image: gcr.io/google_containers/pause-amd64:3.0
|
||||
ports:
|
||||
- containerPort: 1234
|
||||
name: c-port
|
||||
protocol: UDP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: svc-1-a
|
||||
namespace: test-1
|
||||
spec:
|
||||
selector:
|
||||
app: app-1-a
|
||||
clusterIP: 10.0.0.100
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
protocol: TCP
|
||||
- name: https
|
||||
port: 443
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: svc-1-b
|
||||
namespace: test-1
|
||||
spec:
|
||||
selector:
|
||||
app: app-1-b
|
||||
clusterIP: 10.0.0.110
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: svc-c
|
||||
namespace: test-1
|
||||
spec:
|
||||
selector:
|
||||
app: app-c
|
||||
clusterIP: 10.0.0.115
|
||||
ports:
|
||||
- name: c-port
|
||||
port: 1234
|
||||
protocol: UDP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: svc-c
|
||||
namespace: test-2
|
||||
spec:
|
||||
selector:
|
||||
app: app-c
|
||||
clusterIP: 10.0.0.120
|
||||
ports:
|
||||
- name: c-port
|
||||
port: 1234
|
||||
protocol: UDP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: headless-svc
|
||||
namespace: test-1
|
||||
spec:
|
||||
selector:
|
||||
app: app-d
|
||||
clusterIP: None
|
||||
ports:
|
||||
- name: c-port
|
||||
port: 1234
|
||||
protocol: UDP
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
CNCF Community Code of Conduct
|
||||
|
||||
### Contributor Code of Conduct
|
||||
|
||||
As contributors and maintainers of this project, and in the interest of fostering
|
||||
an open and welcoming community, we pledge to respect all people who contribute
|
||||
through reporting issues, posting feature requests, updating documentation,
|
||||
submitting pull requests or patches, and other activities.
|
||||
|
||||
We are committed to making participation in this project a harassment-free experience for
|
||||
everyone, regardless of level of experience, gender, gender identity and expression,
|
||||
sexual orientation, disability, personal appearance, body size, race, ethnicity, age,
|
||||
religion, or nationality.
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery
|
||||
* Personal attacks
|
||||
* Trolling or insulting/derogatory comments
|
||||
* Public or private harassment
|
||||
* Publishing other's private information, such as physical or electronic addresses,
|
||||
without explicit permission
|
||||
* Other unethical or unprofessional conduct.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are not
|
||||
aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers
|
||||
commit themselves to fairly and consistently applying these principles to every aspect
|
||||
of managing this project. Project maintainers who do not follow or enforce the Code of
|
||||
Conduct may be permanently removed from the project team.
|
||||
|
||||
This code of conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a CNCF project maintainer, Sarah Novotny <sarahnovotny@google.com>, and/or Dan Kohn <dan@linuxfoundation.org>.
|
||||
|
||||
This Code of Conduct is adapted from the Contributor Covenant
|
||||
(http://contributor-covenant.org), version 1.2.0, available at
|
||||
http://contributor-covenant.org/version/1/2/0/
|
||||
|
||||
### CNCF Events Code of Conduct
|
||||
|
||||
CNCF events are governed by the Linux Foundation [Code of Conduct](http://events.linuxfoundation.org/events/cloudnativecon/attend/code-of-conduct) available on the event page. This is designed to be compatible with the above policy and also includes more details on responding to incidents.
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
## Contributing to CoreDNS
|
||||
|
||||
Welcome! Our community focuses on helping others and making CoreDNS the best it
|
||||
can be. We gladly accept contributions and encourage you to get involved!
|
||||
|
||||
### Bug reports
|
||||
|
||||
First, please [search this repository](https://github.com/coredns/coredns/search?q=&type=Issues&utf8=%E2%9C%93)
|
||||
with a variety of keywords to ensure your bug is not already reported.
|
||||
|
||||
If not, [open an issue](https://github.com/coredns/coredns/issues) and answer the
|
||||
questions so we can understand and reproduce the problematic behavior.
|
||||
|
||||
The burden is on you to convince us that it is actually a bug in CoreDNS. This is
|
||||
easiest to do when you write clear, concise instructions so we can reproduce
|
||||
the behavior (even if it seems obvious). The more detailed and specific you are,
|
||||
the faster we will be able to help you. Check out
|
||||
[How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
|
||||
|
||||
Please be kind. :smile: Remember that CoreDNS comes at no cost to you, and you're
|
||||
getting free help.
|
||||
|
||||
|
||||
### Minor improvements and new tests
|
||||
|
||||
Submit [pull requests](https://github.com/coredns/coredns/pulls) at any time. Make
|
||||
sure to write tests to assert your change is working properly and is thoroughly
|
||||
covered.
|
||||
|
||||
|
||||
### Proposals, suggestions, ideas, new features
|
||||
|
||||
First, please [search](https://github.com/coredns/coredns/search?q=&type=Issues&utf8=%E2%9C%93)
|
||||
with a variety of keywords to ensure your suggestion/proposal is new.
|
||||
|
||||
If so, you may open either an issue or a pull request for discussion and
|
||||
feedback.
|
||||
|
||||
The advantage of issues is that you don't have to spend time actually
|
||||
implementing your idea, but you should still describe it thoroughly. The
|
||||
advantage of a pull request is that we can immediately see the impact the change
|
||||
will have on the project, what the code will look like, and how to improve it.
|
||||
The disadvantage of pull requests is that they are unlikely to get accepted
|
||||
without significant changes, or it may be rejected entirely. Don't worry, that
|
||||
won't happen without an open discussion first.
|
||||
|
||||
If you are going to spend significant time implementing code for a pull request,
|
||||
best to open an issue first and "claim" it and get feedback before you invest
|
||||
a lot of time.
|
||||
|
||||
|
||||
### Vulnerabilities
|
||||
|
||||
If you've found a vulnerability that is serious, please email me: <miek@miek.nl>.
|
||||
If it's not a big deal, a pull request will probably be faster.
|
||||
|
||||
## Thank you
|
||||
|
||||
Thanks for your help! CoreDNS would not be what it is today without your contributions.
|
||||
|
||||
## Git Hook
|
||||
|
||||
We use `golint` and `go vet` as tools to warn use about things (noted golint is obnoxious sometimes,
|
||||
but still helpful). Add the following script as a git `post-commit` in `.git/hooks/post-commit` and
|
||||
make it executable.
|
||||
|
||||
~~~ sh
|
||||
#!/bin/bash
|
||||
|
||||
# <https://git-scm.com/docs/githooks>:
|
||||
# The script takes no parameters and its exit status does not affect the commit in any way. You can
|
||||
# use git # rev-parse HEAD to get the new commit’s SHA1 hash, or you can use git log -l HEAD to get
|
||||
# all of its # information.
|
||||
|
||||
for d in *; do
|
||||
if [[ "$d" == "vendor" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ "$d" == "logo" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ ! -d "$d" ]]; then
|
||||
continue
|
||||
fi
|
||||
golint "$d"/...
|
||||
done
|
||||
~~~
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
FROM alpine:latest
|
||||
MAINTAINER Miek Gieben <miek@miek.nl> @miekg
|
||||
|
||||
RUN apk --update add bind-tools && rm -rf /var/cache/apk/*
|
||||
|
||||
ADD coredns /coredns
|
||||
|
||||
EXPOSE 53 53/udp
|
||||
ENTRYPOINT ["/coredns"]
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
BUILD_VERBOSE := -v
|
||||
|
||||
TEST_VERBOSE := -v
|
||||
|
||||
all: coredns
|
||||
|
||||
# Phony this to ensure we always build the binary.
|
||||
# TODO: Add .go file dependencies.
|
||||
.PHONY: coredns
|
||||
coredns: check
|
||||
go build $(BUILD_VERBOSE) -ldflags="-s -w"
|
||||
|
||||
.PHONY: deps
|
||||
deps: core/zmiddleware.go core/dnsserver/zdirectives.go
|
||||
go get ${BUILD_VERBOSE}
|
||||
go get -u github.com/golang/lint/golint
|
||||
|
||||
.PHONY: check
|
||||
check: fmt deps
|
||||
|
||||
.PHONY: test
|
||||
test: check
|
||||
go test -race $(TEST_VERBOSE) ./test ./middleware/...
|
||||
|
||||
.PHONY: testk8s
|
||||
testk8s: check
|
||||
go test -race $(TEST_VERBOSE) -tags=k8s -run 'TestKubernetes' ./test ./middleware/kubernetes/...
|
||||
|
||||
.PHONY: coverage
|
||||
coverage: check
|
||||
set -e -x
|
||||
echo "" > coverage.txt
|
||||
for d in `go list ./... | grep -v vendor`; do \
|
||||
go test $(TEST_VERBOSE) -tags 'etcd k8s' -race -coverprofile=cover.out -covermode=atomic -bench=. $$d || exit 1; \
|
||||
if [ -f cover.out ]; then \
|
||||
cat cover.out >> coverage.txt; \
|
||||
rm cover.out; \
|
||||
fi; \
|
||||
done
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
go clean
|
||||
rm -f coredns
|
||||
|
||||
core/zmiddleware.go core/dnsserver/zdirectives.go: middleware.cfg
|
||||
go generate coredns.go
|
||||
|
||||
.PHONY: gen
|
||||
gen:
|
||||
go generate coredns.go
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
## run go fmt
|
||||
@test -z "$$(gofmt -s -l . | grep -v vendor/ | tee /dev/stderr)" || \
|
||||
(echo "please format Go code with 'gofmt -s -w'" && false)
|
||||
|
||||
.PHONY: lint
|
||||
lint: deps
|
||||
## run go lint, suggestion only (not enforced)
|
||||
@test -z "$$(golint ./... | grep -v vendor/ | grep -v ".pb.go:" | grep -vE "context\.Context should be the first parameter of a function" | tee /dev/stderr)"
|
||||
|
||||
.PHONY: distclean
|
||||
distclean: clean
|
||||
# Clean all dependencies and build artifacts
|
||||
find $(GOPATH)/pkg -maxdepth 1 -mindepth 1 | xargs rm -rf
|
||||
find $(GOPATH)/bin -maxdepth 1 -mindepth 1 | xargs rm -rf
|
||||
|
||||
find $(GOPATH)/src -maxdepth 1 -mindepth 1 | grep -v github | xargs rm -rf
|
||||
find $(GOPATH)/src -maxdepth 2 -mindepth 2 | grep -v miekg | xargs rm -rf
|
||||
find $(GOPATH)/src/github.com/miekg -maxdepth 1 -mindepth 1 \! -name \*coredns\* | xargs rm -rf
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
# Makefile for releasing CoreDNS
|
||||
#
|
||||
# The release is controlled from coremain/version.go. The version found
|
||||
# there is used to tag the git repo and to build the assets that are
|
||||
# uploaded to github (after some sanity checks).
|
||||
#
|
||||
# The release should be accompanied by release notes published on
|
||||
# blog.coredns.io. For example:
|
||||
# https://blog.coredns.io/2016/09/18/coredns-001-release/
|
||||
#
|
||||
# We use https://github.com/progrium/gh-release to automate github stuff
|
||||
# be sure to have that binary in your path.
|
||||
#
|
||||
# Get a list of authors for this release with:
|
||||
#
|
||||
# git log --pretty=format:'%an' v001..master | sort -u
|
||||
# (where v001 is the previous release, obviously you'll need to adjust this)
|
||||
#
|
||||
# Steps:
|
||||
# * Get an access token: https://help.github.com/articles/creating-an-access-token-for-command-line-use/
|
||||
# * export GITHUB_ACCESS_TOKEN=<token>
|
||||
# * Up the version in coremain/version.go
|
||||
# * Run: make -f Makefile.release
|
||||
# * will commit your change with 'Release $VERSION'
|
||||
# * push to github
|
||||
# * build the release and do all that fluff.
|
||||
#
|
||||
# Steps for docker
|
||||
# * Login into docker: docker login (should have push creds for coredns registry)
|
||||
# * Run: make -f Makefile.release docker
|
||||
#
|
||||
# Docker push should happen after you make the new release and uploaded it to Github.
|
||||
|
||||
NAME:=coredns
|
||||
VERSION:=$(shell grep 'coreVersion' coremain/version.go | awk '{ print $$3 }' | tr -d '"')
|
||||
ARCH:=$(shell uname -m)
|
||||
GITHUB:=coredns
|
||||
DOCKER:=coredns
|
||||
DOCKER_IMAGE_NAME:=$(DOCKER)/$(NAME)
|
||||
|
||||
all: commit push build tar release
|
||||
|
||||
docker: docker-build docker-release
|
||||
|
||||
.PHONY: push
|
||||
push:
|
||||
@echo Pushing release to master
|
||||
git push
|
||||
|
||||
.PHONY: commit
|
||||
commit:
|
||||
@echo Committing
|
||||
git commit -am"Release $(VERSION)"
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
@echo Building: $(VERSION)
|
||||
mkdir -p build/Darwin && CGO_ENABLED=0 GOOS=darwin go build -ldflags="-s -w" -o build/Darwin/$(NAME)
|
||||
mkdir -p build/Linux/Arm && CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags="-s -w" -o build/Linux/Arm/$(NAME)
|
||||
mkdir -p build/Linux && CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o build/Linux/$(NAME)
|
||||
|
||||
|
||||
.PHONY: tar
|
||||
tar:
|
||||
rm -rf release && mkdir release
|
||||
tar -zcf release/$(NAME)_$(VERSION)_linux_$(ARCH).tgz -C build/Linux $(NAME)
|
||||
tar -zcf release/$(NAME)_$(VERSION)_linux_armv6l.tgz -C build/Linux/Arm $(NAME)
|
||||
tar -zcf release/$(NAME)_$(VERSION)_darwin_$(ARCH).tgz -C build/Darwin $(NAME)
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
@echo Releasing: $(VERSION)
|
||||
gh-release create $(GITHUB)/$(NAME) $(VERSION)
|
||||
|
||||
.PHONY: docker-build
|
||||
docker:
|
||||
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w"
|
||||
docker build -t $(DOCKER_IMAGE_NAME) .
|
||||
docker tag $(DOCKER_IMAGE_NAME):latest $(DOCKER_IMAGE_NAME):$(VERSION)
|
||||
|
||||
.PHONY: docker-release
|
||||
docker-release:
|
||||
@echo Pushing: $(VERSION)
|
||||
docker tag $(DOCKER_IMAGE_NAME):latest $(DOCKER_IMAGE_NAME):$(VERSION)
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf release
|
||||
rm -rf build
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
# CoreDNS
|
||||
|
||||
[](https://godoc.org/github.com/coredns/coredns)
|
||||
[](https://travis-ci.org/coredns/coredns)
|
||||
[](https://codecov.io/github/coredns/coredns?branch=master)
|
||||
[](https://goreportcard.com/report/coredns/coredns)
|
||||
|
||||
CoreDNS is a DNS server that started as a fork of [Caddy](https://github.com/mholt/caddy/). It has
|
||||
the same model: it chains middleware. In fact it's so similar that CoreDNS is now a server type
|
||||
plugin for Caddy.
|
||||
|
||||
CoreDNS is also a [Cloud Native Computing Foundation](https://cncf.io) inception level project.
|
||||
|
||||
CoreDNS is the successor to [SkyDNS](https://github.com/skynetservices/skydns). SkyDNS is a thin
|
||||
layer that exposes services in etcd in the DNS. CoreDNS builds on this idea and is a generic DNS
|
||||
server that can talk to multiple backends (etcd, kubernetes, etc.).
|
||||
|
||||
CoreDNS aims to be a fast and flexible DNS server. The keyword here is *flexible*: with CoreDNS you
|
||||
are able to do what you want with your DNS data. And if not: write some middleware!
|
||||
|
||||
CoreDNS can listen for DNS request coming in over UDP/TCP (go'old DNS), TLS ([RFC
|
||||
7858](https://tools.ietf.org/html/rfc7858)) and gRPC (not a standard.
|
||||
|
||||
Currently CoreDNS is able to:
|
||||
|
||||
* Serve zone data from a file; both DNSSEC (NSEC only) and DNS are supported (*file*).
|
||||
* Retrieve zone data from primaries, i.e., act as a secondary server (AXFR only) (*secondary*).
|
||||
* Sign zone data on-the-fly (*dnssec*).
|
||||
* Load balancing of responses (*loadbalance*).
|
||||
* Allow for zone transfers, i.e., act as a primary server (*file*).
|
||||
* Automatically load zone files from disk (*auto*)
|
||||
* Caching (*cache*).
|
||||
* Health checking endpoint (*health*).
|
||||
* Use etcd as a backend, i.e., a 101.5% replacement for
|
||||
[SkyDNS](https://github.com/skynetservices/skydns) (*etcd*).
|
||||
* Use k8s (kubernetes) as a backend (*kubernetes*).
|
||||
* Serve as a proxy to forward queries to some other (recursive) nameserver (*proxy*).
|
||||
* Provide metrics (by using Prometheus) (*metrics*).
|
||||
* Provide query (*log*) and error (*error*) logging.
|
||||
* Support the CH class: `version.bind` and friends (*chaos*).
|
||||
* Profiling support (*pprof*).
|
||||
* Rewrite queries (qtype, qclass and qname) (*rewrite*).
|
||||
* Echo back the IP address, transport and port number used (*whoami*).
|
||||
|
||||
Each of the middlewares has a README.md of its own.
|
||||
|
||||
## Status
|
||||
|
||||
CoreDNS can be used as an authoritative nameserver for your domains, and should be stable enough to
|
||||
provide you with good DNS(SEC) service.
|
||||
|
||||
There are still a few known [issues](https://github.com/coredns/coredns/issues), and work is ongoing
|
||||
on making things fast and to reduce the memory usage.
|
||||
|
||||
All in all, CoreDNS should be able to provide you with enough functionality to replace parts of BIND
|
||||
9, Knot, NSD or PowerDNS and SkyDNS. Most documentation is in the source and some blog articles can
|
||||
be [found here](https://blog.coredns.io). If you do want to use CoreDNS in production, please
|
||||
let us know and how we can help.
|
||||
|
||||
<https://caddyserver.com/> is also full of examples on how to structure a Corefile (renamed from
|
||||
Caddyfile when forked).
|
||||
|
||||
## Compilation
|
||||
|
||||
CoreDNS (as a servertype plugin for Caddy) has a dependency on Caddy, but this is not different than
|
||||
any other Go dependency. If you have the source of CoreDNS, get all dependencies:
|
||||
|
||||
go get ./...
|
||||
|
||||
And then `go build` as you would normally do:
|
||||
|
||||
go build
|
||||
|
||||
This should yield a `coredns` binary.
|
||||
|
||||
## Examples
|
||||
|
||||
When starting CoreDNS without any configuration, it loads the `whoami` middleware and starts
|
||||
listening on port 53 (override with `-dns.port`), it should show the following:
|
||||
|
||||
~~~ txt
|
||||
.:53
|
||||
2016/09/18 09:20:50 [INFO] CoreDNS-001
|
||||
CoreDNS-001
|
||||
~~~
|
||||
|
||||
Any query send to port 53 should return some information; your sending address, port and protocol
|
||||
used.
|
||||
|
||||
If you have a Corefile without a port number specified it will, by default, use port 53, but you
|
||||
can override the port with the `-dns.port` flag:
|
||||
|
||||
~~~ txt
|
||||
.: {
|
||||
proxy . 8.8.8.8:53
|
||||
log stdout
|
||||
}
|
||||
~~~
|
||||
|
||||
`./coredns -dns.port 1053`, runs the server on port 1053.
|
||||
|
||||
Start a simple proxy, you'll need to be root to start listening on port 53.
|
||||
|
||||
`Corefile` contains:
|
||||
|
||||
~~~ txt
|
||||
.:53 {
|
||||
proxy . 8.8.8.8:53
|
||||
log stdout
|
||||
}
|
||||
~~~
|
||||
|
||||
Just start CoreDNS: `./coredns`.
|
||||
And then just query on that port (53). The query should be forwarded to 8.8.8.8 and the response
|
||||
will be returned. Each query should also show up in the log.
|
||||
|
||||
Serve the (NSEC) DNSSEC-signed `example.org` on port 1053, with errors and logging sent to stdout.
|
||||
Allow zone transfers to everybody, but specically mention 1 IP address so that CoreDNS can send
|
||||
notifies to it.
|
||||
|
||||
~~~ txt
|
||||
example.org:1053 {
|
||||
file /var/lib/coredns/example.org.signed {
|
||||
transfer to *
|
||||
transfer to 2001:500:8f::53
|
||||
}
|
||||
errors stdout
|
||||
log stdout
|
||||
}
|
||||
~~~
|
||||
|
||||
Serve `example.org` on port 1053, but forward everything that does *not* match `example.org` to a recursive
|
||||
nameserver *and* rewrite ANY queries to HINFO.
|
||||
|
||||
~~~ txt
|
||||
.:1053 {
|
||||
rewrite ANY HINFO
|
||||
proxy . 8.8.8.8:53
|
||||
|
||||
file /var/lib/coredns/example.org.signed example.org {
|
||||
transfer to *
|
||||
transfer to 2001:500:8f::53
|
||||
}
|
||||
errors stdout
|
||||
log stdout
|
||||
}
|
||||
~~~
|
||||
|
||||
### Zone Specification
|
||||
|
||||
The following Corefile fragment is legal, but does not explicitly define a zone to listen on:
|
||||
|
||||
~~~ txt
|
||||
{
|
||||
# ...
|
||||
}
|
||||
~~~
|
||||
|
||||
This defaults to `.:53` (or whatever `-dns.port` is).
|
||||
|
||||
The next one only defines a port:
|
||||
~~~ txt
|
||||
:123 {
|
||||
# ...
|
||||
}
|
||||
~~~
|
||||
This defaults to the root zone `.`, but can't be overruled with the `-dns.port` flag.
|
||||
|
||||
Just specifying a zone, default to listening on port 53 (can still be overridden with `-dns.port`:
|
||||
|
||||
~~~ txt
|
||||
example.org {
|
||||
# ...
|
||||
}
|
||||
~~~
|
||||
|
||||
Listening on TLS and for gRPC? Use:
|
||||
|
||||
~~~ txt
|
||||
tls://example.org grpc://example.org {
|
||||
# ...
|
||||
}
|
||||
~~~
|
||||
|
||||
Specifying ports works in the same way:
|
||||
|
||||
~~~ txt
|
||||
grpc://example.org:1443 {
|
||||
# ...
|
||||
}
|
||||
~~~
|
||||
|
||||
When no transport protocol is specified the default `dns://` is assumed.
|
||||
|
||||
## Blog and Contact
|
||||
|
||||
Website: <https://coredns.io>
|
||||
Twitter: [@corednsio](https://twitter.com/corednsio)
|
||||
Docs: <https://miek.nl/tags/coredns/>
|
||||
Github: <https://github.com/coredns/coredns>
|
||||
|
||||
|
||||
## Systemd Service File
|
||||
|
||||
Use this as a systemd service file. It defaults to a coredns with a homedir of /home/coredns
|
||||
and the binary lives in /opt/bin and the config in `/etc/coredns/Corefile`:
|
||||
|
||||
~~~ txt
|
||||
[Unit]
|
||||
Description=CoreDNS DNS server
|
||||
Documentation=https://coredns.io
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
PermissionsStartOnly=true
|
||||
LimitNOFILE=8192
|
||||
User=coredns
|
||||
WorkingDirectory=/home/coredns
|
||||
ExecStartPre=/sbin/setcap cap_net_bind_service=+ep /opt/bin/coredns
|
||||
ExecStart=/opt/bin/coredns -conf=/etc/coredns/Corefile
|
||||
ExecReload=/bin/kill -SIGUSR1 $MAINPID
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
~~~
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Package core registers the server and all plugins we support.
|
||||
package core
|
||||
|
||||
import (
|
||||
// plug in the server
|
||||
_ "github.com/coredns/coredns/core/dnsserver"
|
||||
|
||||
// plug in the standard directives (sorted)
|
||||
_ "github.com/coredns/coredns/middleware/auto"
|
||||
_ "github.com/coredns/coredns/middleware/bind"
|
||||
_ "github.com/coredns/coredns/middleware/cache"
|
||||
_ "github.com/coredns/coredns/middleware/chaos"
|
||||
_ "github.com/coredns/coredns/middleware/dnssec"
|
||||
_ "github.com/coredns/coredns/middleware/erratic"
|
||||
_ "github.com/coredns/coredns/middleware/errors"
|
||||
_ "github.com/coredns/coredns/middleware/etcd"
|
||||
_ "github.com/coredns/coredns/middleware/file"
|
||||
_ "github.com/coredns/coredns/middleware/health"
|
||||
_ "github.com/coredns/coredns/middleware/kubernetes"
|
||||
_ "github.com/coredns/coredns/middleware/loadbalance"
|
||||
_ "github.com/coredns/coredns/middleware/log"
|
||||
_ "github.com/coredns/coredns/middleware/metrics"
|
||||
_ "github.com/coredns/coredns/middleware/pprof"
|
||||
_ "github.com/coredns/coredns/middleware/proxy"
|
||||
_ "github.com/coredns/coredns/middleware/reverse"
|
||||
_ "github.com/coredns/coredns/middleware/rewrite"
|
||||
_ "github.com/coredns/coredns/middleware/root"
|
||||
_ "github.com/coredns/coredns/middleware/secondary"
|
||||
_ "github.com/coredns/coredns/middleware/trace"
|
||||
_ "github.com/coredns/coredns/middleware/whoami"
|
||||
)
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type zoneAddr struct {
|
||||
Zone string
|
||||
Port string
|
||||
Transport string // dns, tls or grpc
|
||||
}
|
||||
|
||||
// String return the string represenation of z.
|
||||
func (z zoneAddr) String() string { return z.Transport + "://" + z.Zone + ":" + z.Port }
|
||||
|
||||
// Transport returns the protocol of the string s
|
||||
func Transport(s string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(s, TransportTLS+"://"):
|
||||
return TransportTLS
|
||||
case strings.HasPrefix(s, TransportDNS+"://"):
|
||||
return TransportDNS
|
||||
case strings.HasPrefix(s, TransportGRPC+"://"):
|
||||
return TransportGRPC
|
||||
}
|
||||
return TransportDNS
|
||||
}
|
||||
|
||||
// normalizeZone parses an zone string into a structured format with separate
|
||||
// host, and port portions, as well as the original input string.
|
||||
//
|
||||
// TODO(miek): possibly move this to middleware/normalize.go
|
||||
func normalizeZone(str string) (zoneAddr, error) {
|
||||
var err error
|
||||
|
||||
// Default to DNS if there isn't a transport protocol prefix.
|
||||
trans := TransportDNS
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(str, TransportTLS+"://"):
|
||||
trans = TransportTLS
|
||||
str = str[len(TransportTLS+"://"):]
|
||||
case strings.HasPrefix(str, TransportDNS+"://"):
|
||||
trans = TransportDNS
|
||||
str = str[len(TransportDNS+"://"):]
|
||||
case strings.HasPrefix(str, TransportGRPC+"://"):
|
||||
trans = TransportGRPC
|
||||
str = str[len(TransportGRPC+"://"):]
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(str)
|
||||
if err != nil {
|
||||
host, port, err = net.SplitHostPort(str + ":")
|
||||
// no error check here; return err at end of function
|
||||
}
|
||||
|
||||
if len(host) > 255 { // TODO(miek): this should take escaping into account.
|
||||
return zoneAddr{}, fmt.Errorf("specified zone is too long: %d > 255", len(host))
|
||||
}
|
||||
_, d := dns.IsDomainName(host)
|
||||
if !d {
|
||||
return zoneAddr{}, fmt.Errorf("zone is not a valid domain name: %s", host)
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
if trans == TransportDNS {
|
||||
port = Port
|
||||
}
|
||||
if trans == TransportTLS {
|
||||
port = TLSPort
|
||||
}
|
||||
if trans == TransportGRPC {
|
||||
port = GRPCPort
|
||||
}
|
||||
}
|
||||
|
||||
return zoneAddr{Zone: strings.ToLower(dns.Fqdn(host)), Port: port, Transport: trans}, err
|
||||
}
|
||||
|
||||
// Supported transports.
|
||||
const (
|
||||
TransportDNS = "dns"
|
||||
TransportTLS = "tls"
|
||||
TransportGRPC = "grpc"
|
||||
)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package dnsserver
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeZone(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
expected string
|
||||
shouldErr bool
|
||||
}{
|
||||
{".", "dns://.:53", false},
|
||||
{".:54", "dns://.:54", false},
|
||||
{"..", "://:", true},
|
||||
{"..", "://:", true},
|
||||
} {
|
||||
addr, err := normalizeZone(test.input)
|
||||
actual := addr.String()
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error, but there wasn't any", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but there was one: %v", i, err)
|
||||
}
|
||||
if actual != test.expected {
|
||||
t.Errorf("Test %d: Expected %s but got %s", i, test.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
// Config configuration for a single server.
|
||||
type Config struct {
|
||||
// The zone of the site.
|
||||
Zone string
|
||||
|
||||
// The hostname to bind listener to, defaults to the wildcard address
|
||||
ListenHost string
|
||||
|
||||
// The port to listen on.
|
||||
Port string
|
||||
|
||||
// Root points to a base directory we we find user defined "things".
|
||||
// First consumer is the file middleware to looks for zone files in this place.
|
||||
Root string
|
||||
|
||||
// The transport we implement, normally just "dns" over TCP/UDP, but could be
|
||||
// DNS-over-TLS or DNS-over-gRPC.
|
||||
Transport string
|
||||
|
||||
// TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS).
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// Middleware stack.
|
||||
Middleware []middleware.Middleware
|
||||
|
||||
// Compiled middleware stack.
|
||||
middlewareChain middleware.Handler
|
||||
}
|
||||
|
||||
// GetConfig gets the Config that corresponds to c.
|
||||
// If none exist nil is returned.
|
||||
func GetConfig(c *caddy.Controller) *Config {
|
||||
ctx := c.Context().(*dnsContext)
|
||||
if cfg, ok := ctx.keysToConfigs[c.Key]; ok {
|
||||
return cfg
|
||||
}
|
||||
// we should only get here during tests because directive
|
||||
// actions typically skip the server blocks where we make
|
||||
// the configs.
|
||||
ctx.saveConfig(c.Key, &Config{})
|
||||
return GetConfig(c)
|
||||
}
|
||||
|
||||
// GetMiddleware returns the middleware handler that has been added to the config under name.
|
||||
// This is useful to inspect if a certain middleware is active in this server.
|
||||
// Note that this is order dependent and the order is defined in directives.go, i.e. if your middleware
|
||||
// comes before the middleware you are checking; it will not be there (yet).
|
||||
func GetMiddleware(c *caddy.Controller, name string) middleware.Handler {
|
||||
conf := GetConfig(c)
|
||||
for _, h := range conf.Middleware {
|
||||
x := h(nil)
|
||||
if name == x.Name() {
|
||||
return x
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RegisterDevDirective splices name into the list of directives
|
||||
// immediately before another directive. This function is ONLY
|
||||
// for plugin development purposes! NEVER use it for a plugin
|
||||
// that you are not currently building. If before is empty,
|
||||
// the directive will be appended to the end of the list.
|
||||
//
|
||||
// It is imperative that directives execute in the proper
|
||||
// order, and hard-coding the list of directives guarantees
|
||||
// a correct, absolute order every time. This function is
|
||||
// convenient when developing a plugin, but it does not
|
||||
// guarantee absolute ordering. Multiple plugins registering
|
||||
// directives with this function will lead to non-
|
||||
// deterministic builds and buggy software.
|
||||
//
|
||||
// Directive names must be lower-cased and unique. Any errors
|
||||
// here are fatal, and even successful calls print a message
|
||||
// to stdout as a reminder to use it only in development.
|
||||
func RegisterDevDirective(name, before string) {
|
||||
if name == "" {
|
||||
fmt.Println("[FATAL] Cannot register empty directive name")
|
||||
os.Exit(1)
|
||||
}
|
||||
if strings.ToLower(name) != name {
|
||||
fmt.Printf("[FATAL] %s: directive name must be lowercase\n", name)
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, dir := range directives {
|
||||
if dir == name {
|
||||
fmt.Printf("[FATAL] %s: directive name already exists\n", name)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if before == "" {
|
||||
directives = append(directives, name)
|
||||
} else {
|
||||
var found bool
|
||||
for i, dir := range directives {
|
||||
if dir == before {
|
||||
directives = append(directives[:i], append([]string{name}, directives[i:]...)...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
fmt.Printf("[FATAL] %s: directive not found\n", before)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
msg := fmt.Sprintf("Registered directive '%s' ", name)
|
||||
if before == "" {
|
||||
msg += "at end of list"
|
||||
} else {
|
||||
msg += fmt.Sprintf("before '%s'", before)
|
||||
}
|
||||
fmt.Printf("[INFO] %s\n", msg)
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
)
|
||||
|
||||
const serverType = "dns"
|
||||
|
||||
// Any flags defined here, need to be namespaced to the serverType other
|
||||
// wise they potentially clash with other server types.
|
||||
func init() {
|
||||
flag.StringVar(&Port, serverType+".port", DefaultPort, "Default port")
|
||||
|
||||
caddy.RegisterServerType(serverType, caddy.ServerType{
|
||||
Directives: func() []string { return directives },
|
||||
DefaultInput: func() caddy.Input {
|
||||
return caddy.CaddyfileInput{
|
||||
Filepath: "Corefile",
|
||||
Contents: []byte(".:" + Port + " {\nwhoami\n}\n"),
|
||||
ServerTypeName: serverType,
|
||||
}
|
||||
},
|
||||
NewContext: newContext,
|
||||
})
|
||||
}
|
||||
|
||||
func newContext() caddy.Context {
|
||||
return &dnsContext{keysToConfigs: make(map[string]*Config)}
|
||||
}
|
||||
|
||||
type dnsContext struct {
|
||||
keysToConfigs map[string]*Config
|
||||
|
||||
// configs is the master list of all site configs.
|
||||
configs []*Config
|
||||
}
|
||||
|
||||
func (h *dnsContext) saveConfig(key string, cfg *Config) {
|
||||
h.configs = append(h.configs, cfg)
|
||||
h.keysToConfigs[key] = cfg
|
||||
}
|
||||
|
||||
// InspectServerBlocks make sure that everything checks out before
|
||||
// executing directives and otherwise prepares the directives to
|
||||
// be parsed and executed.
|
||||
func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
|
||||
// Normalize and check all the zone names and check for duplicates
|
||||
dups := map[string]string{}
|
||||
for _, s := range serverBlocks {
|
||||
for i, k := range s.Keys {
|
||||
za, err := normalizeZone(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.Keys[i] = za.String()
|
||||
if v, ok := dups[za.String()]; ok {
|
||||
return nil, fmt.Errorf("cannot serve %s - zone already defined for %v", za, v)
|
||||
}
|
||||
dups[za.String()] = za.String()
|
||||
|
||||
// Save the config to our master list, and key it for lookups
|
||||
cfg := &Config{
|
||||
Zone: za.Zone,
|
||||
Port: za.Port,
|
||||
Transport: za.Transport,
|
||||
}
|
||||
h.saveConfig(za.String(), cfg)
|
||||
}
|
||||
}
|
||||
return serverBlocks, nil
|
||||
}
|
||||
|
||||
// MakeServers uses the newly-created siteConfigs to create and return a list of server instances.
|
||||
func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
|
||||
|
||||
// we must map (group) each config to a bind address
|
||||
groups, err := groupConfigsByListenAddr(h.configs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// then we create a server for each group
|
||||
var servers []caddy.Server
|
||||
for addr, group := range groups {
|
||||
// switch on addr
|
||||
switch Transport(addr) {
|
||||
case TransportDNS:
|
||||
s, err := NewServer(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
|
||||
case TransportTLS:
|
||||
s, err := NewServerTLS(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
|
||||
case TransportGRPC:
|
||||
s, err := NewServergRPC(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// AddMiddleware adds a middleware to a site's middleware stack.
|
||||
func (c *Config) AddMiddleware(m middleware.Middleware) {
|
||||
c.Middleware = append(c.Middleware, m)
|
||||
}
|
||||
|
||||
// groupSiteConfigsByListenAddr groups site configs by their listen
|
||||
// (bind) address, so sites that use the same listener can be served
|
||||
// on the same server instance. The return value maps the listen
|
||||
// address (what you pass into net.Listen) to the list of site configs.
|
||||
// This function does NOT vet the configs to ensure they are compatible.
|
||||
func groupConfigsByListenAddr(configs []*Config) (map[string][]*Config, error) {
|
||||
groups := make(map[string][]*Config)
|
||||
|
||||
for _, conf := range configs {
|
||||
addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.ListenHost, conf.Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrstr := conf.Transport + "://" + addr.String()
|
||||
groups[addrstr] = append(groups[addrstr], conf)
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultPort is the default port.
|
||||
DefaultPort = "53"
|
||||
// TLSPort is the default port for DNS-over-TLS.
|
||||
TLSPort = "853"
|
||||
// GRPCPort is the default port for DNS-over-gRPC.
|
||||
GRPCPort = "443"
|
||||
)
|
||||
|
||||
// These "soft defaults" are configurable by
|
||||
// command line flags, etc.
|
||||
var (
|
||||
// Port is the port we listen on by default.
|
||||
Port = DefaultPort
|
||||
|
||||
// GracefulTimeout is the maximum duration of a graceful shutdown.
|
||||
GracefulTimeout time.Duration
|
||||
)
|
||||
|
||||
var _ caddy.GracefulServer = new(Server)
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/peer"
|
||||
|
||||
"github.com/coredns/coredns/pb"
|
||||
)
|
||||
|
||||
// servergRPC represents an instance of a DNS-over-gRPC server.
|
||||
type servergRPC struct {
|
||||
*Server
|
||||
grpcServer *grpc.Server
|
||||
|
||||
listenAddr net.Addr
|
||||
}
|
||||
|
||||
// NewGRPCServer returns a new CoreDNS GRPC server and compiles all middleware in to it.
|
||||
func NewServergRPC(addr string, group []*Config) (*servergRPC, error) {
|
||||
|
||||
s, err := NewServer(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gs := &servergRPC{Server: s}
|
||||
gs.grpcServer = grpc.NewServer()
|
||||
// trace foo... TODO(miek)
|
||||
pb.RegisterDnsServiceServer(gs.grpcServer, gs)
|
||||
|
||||
return gs, nil
|
||||
}
|
||||
|
||||
// Serve implements caddy.TCPServer interface.
|
||||
func (s *servergRPC) Serve(l net.Listener) error {
|
||||
s.m.Lock()
|
||||
s.listenAddr = l.Addr()
|
||||
s.m.Unlock()
|
||||
|
||||
return s.grpcServer.Serve(l)
|
||||
}
|
||||
|
||||
// ServePacket implements caddy.UDPServer interface.
|
||||
func (s *servergRPC) ServePacket(p net.PacketConn) error { return nil }
|
||||
|
||||
// Listen implements caddy.TCPServer interface.
|
||||
func (s *servergRPC) Listen() (net.Listener, error) {
|
||||
|
||||
// The *tls* middleware must make sure that multiple conflicting
|
||||
// TLS configuration return an error: it can only be specified once.
|
||||
tlsConfig := new(tls.Config)
|
||||
for _, conf := range s.zones {
|
||||
// Should we error if some configs *don't* have TLS?
|
||||
tlsConfig = conf.TLSConfig
|
||||
}
|
||||
|
||||
var (
|
||||
l net.Listener
|
||||
err error
|
||||
)
|
||||
|
||||
if tlsConfig == nil {
|
||||
l, err = net.Listen("tcp", s.Addr[len(TransportGRPC+"://"):])
|
||||
} else {
|
||||
l, err = tls.Listen("tcp", s.Addr[len(TransportGRPC+"://"):], tlsConfig)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ListenPacket implements caddy.UDPServer interface.
|
||||
func (s *servergRPC) ListenPacket() (net.PacketConn, error) { return nil, nil }
|
||||
|
||||
// OnStartupComplete lists the sites served by this server
|
||||
// and any relevant information, assuming Quiet is false.
|
||||
func (s *servergRPC) OnStartupComplete() {
|
||||
if Quiet {
|
||||
return
|
||||
}
|
||||
|
||||
for zone, config := range s.zones {
|
||||
fmt.Println(TransportGRPC + "://" + zone + ":" + config.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *servergRPC) Stop() (err error) {
|
||||
s.m.Lock()
|
||||
defer s.m.Unlock()
|
||||
if s.grpcServer != nil {
|
||||
s.grpcServer.GracefulStop()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Query is the main entry-point into the gRPC server. From here we call ServeDNS like
|
||||
// any normal server. We use a custom responseWriter to pick up the bytes we need to write
|
||||
// back to the client as a protobuf.
|
||||
func (s *servergRPC) Query(ctx context.Context, in *pb.DnsPacket) (*pb.DnsPacket, error) {
|
||||
msg := new(dns.Msg)
|
||||
err := msg.Unpack(in.Msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p, ok := peer.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("no peer in gRPC context")
|
||||
}
|
||||
|
||||
a, ok := p.Addr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no TCP peer in gRPC context: %v", p.Addr)
|
||||
}
|
||||
|
||||
r := &net.IPAddr{IP: a.IP}
|
||||
w := &gRPCresponse{localAddr: s.listenAddr, remoteAddr: r, Msg: msg}
|
||||
|
||||
s.ServeDNS(ctx, w, msg)
|
||||
|
||||
packed, err := w.Msg.Pack()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.DnsPacket{Msg: packed}, nil
|
||||
}
|
||||
|
||||
func (g *servergRPC) Shutdown() error {
|
||||
if g.grpcServer != nil {
|
||||
g.grpcServer.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type gRPCresponse struct {
|
||||
localAddr net.Addr
|
||||
remoteAddr net.Addr
|
||||
Msg *dns.Msg
|
||||
}
|
||||
|
||||
// Write is the hack that makes this work. It does not actually write the message
|
||||
// but returns the bytes we need to to write in r. We can then pick this up in Query
|
||||
// and write a proper protobuf back to the client.
|
||||
func (r *gRPCresponse) Write(b []byte) (int, error) {
|
||||
r.Msg = new(dns.Msg)
|
||||
return len(b), r.Msg.Unpack(b)
|
||||
}
|
||||
|
||||
// These methods implement the dns.ResponseWriter interface from Go DNS.
|
||||
func (r *gRPCresponse) Close() error { return nil }
|
||||
func (r *gRPCresponse) TsigStatus() error { return nil }
|
||||
func (r *gRPCresponse) TsigTimersOnly(b bool) { return }
|
||||
func (r *gRPCresponse) Hijack() { return }
|
||||
func (r *gRPCresponse) LocalAddr() net.Addr { return r.localAddr }
|
||||
func (r *gRPCresponse) RemoteAddr() net.Addr { return r.remoteAddr }
|
||||
func (r *gRPCresponse) WriteMsg(m *dns.Msg) error { r.Msg = m; return nil }
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// serverTLS represents an instance of a TLS-over-DNS-server.
|
||||
type serverTLS struct {
|
||||
*Server
|
||||
}
|
||||
|
||||
// NewTLSServer returns a new CoreDNS TLS server and compiles all middleware in to it.
|
||||
func NewServerTLS(addr string, group []*Config) (*serverTLS, error) {
|
||||
|
||||
s, err := NewServer(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &serverTLS{Server: s}, nil
|
||||
}
|
||||
|
||||
// Serve implements caddy.TCPServer interface.
|
||||
func (s *serverTLS) Serve(l net.Listener) error {
|
||||
s.m.Lock()
|
||||
|
||||
// Only fill out the TCP server for this one.
|
||||
s.server[tcp] = &dns.Server{Listener: l, Net: "tcp-tls", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
ctx := context.Background()
|
||||
s.ServeDNS(ctx, w, r)
|
||||
})}
|
||||
s.m.Unlock()
|
||||
|
||||
return s.server[tcp].ActivateAndServe()
|
||||
}
|
||||
|
||||
// ServePacket implements caddy.UDPServer interface.
|
||||
func (s *serverTLS) ServePacket(p net.PacketConn) error { return nil }
|
||||
|
||||
// Listen implements caddy.TCPServer interface.
|
||||
func (s *serverTLS) Listen() (net.Listener, error) {
|
||||
// The *tls* middleware must make sure that multiple conflicting
|
||||
// TLS configuration return an error: it can only be specified once.
|
||||
tlsConfig := new(tls.Config)
|
||||
for _, conf := range s.zones {
|
||||
// Should we error if some configs *don't* have TLS?
|
||||
tlsConfig = conf.TLSConfig
|
||||
}
|
||||
|
||||
var (
|
||||
l net.Listener
|
||||
err error
|
||||
)
|
||||
|
||||
if tlsConfig == nil {
|
||||
l, err = net.Listen("tcp", s.Addr[len(TransportTLS+"://"):])
|
||||
} else {
|
||||
l, err = tls.Listen("tcp", s.Addr[len(TransportTLS+"://"):], tlsConfig)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ListenPacket implements caddy.UDPServer interface.
|
||||
func (s *serverTLS) ListenPacket() (net.PacketConn, error) { return nil, nil }
|
||||
|
||||
// OnStartupComplete lists the sites served by this server
|
||||
// and any relevant information, assuming Quiet is false.
|
||||
func (s *serverTLS) OnStartupComplete() {
|
||||
if Quiet {
|
||||
return
|
||||
}
|
||||
|
||||
for zone, config := range s.zones {
|
||||
fmt.Println(TransportTLS + "://" + zone + ":" + config.Port)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
// Package dnsserver implements all the interfaces from Caddy, so that CoreDNS can be a servertype plugin.
|
||||
package dnsserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/middleware/metrics/vars"
|
||||
"github.com/coredns/coredns/middleware/pkg/edns"
|
||||
"github.com/coredns/coredns/middleware/pkg/rcode"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Server represents an instance of a server, which serves
|
||||
// DNS requests at a particular address (host and port). A
|
||||
// server is capable of serving numerous zones on
|
||||
// the same address and the listener may be stopped for
|
||||
// graceful termination (POSIX only).
|
||||
type Server struct {
|
||||
Addr string // Address we listen on
|
||||
|
||||
server [2]*dns.Server // 0 is a net.Listener, 1 is a net.PacketConn (a *UDPConn) in our case.
|
||||
m sync.Mutex // protects the servers
|
||||
|
||||
zones map[string]*Config // zones keyed by their address
|
||||
dnsWg sync.WaitGroup // used to wait on outstanding connections
|
||||
connTimeout time.Duration // the maximum duration of a graceful shutdown
|
||||
}
|
||||
|
||||
// NewServer returns a new CoreDNS server and compiles all middleware in to it.
|
||||
func NewServer(addr string, group []*Config) (*Server, error) {
|
||||
|
||||
s := &Server{
|
||||
Addr: addr,
|
||||
zones: make(map[string]*Config),
|
||||
connTimeout: 5 * time.Second, // TODO(miek): was configurable
|
||||
}
|
||||
|
||||
// We have to bound our wg with one increment
|
||||
// to prevent a "race condition" that is hard-coded
|
||||
// into sync.WaitGroup.Wait() - basically, an add
|
||||
// with a positive delta must be guaranteed to
|
||||
// occur before Wait() is called on the wg.
|
||||
// In a way, this kind of acts as a safety barrier.
|
||||
s.dnsWg.Add(1)
|
||||
|
||||
for _, site := range group {
|
||||
// set the config per zone
|
||||
s.zones[site.Zone] = site
|
||||
// compile custom middleware for everything
|
||||
var stack middleware.Handler
|
||||
for i := len(site.Middleware) - 1; i >= 0; i-- {
|
||||
stack = site.Middleware[i](stack)
|
||||
}
|
||||
site.middlewareChain = stack
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Serve starts the server with an existing listener. It blocks until the server stops.
|
||||
// This implements caddy.TCPServer interface.
|
||||
func (s *Server) Serve(l net.Listener) error {
|
||||
s.m.Lock()
|
||||
s.server[tcp] = &dns.Server{Listener: l, Net: "tcp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
ctx := context.Background()
|
||||
s.ServeDNS(ctx, w, r)
|
||||
})}
|
||||
s.m.Unlock()
|
||||
|
||||
return s.server[tcp].ActivateAndServe()
|
||||
}
|
||||
|
||||
// ServePacket starts the server with an existing packetconn. It blocks until the server stops.
|
||||
// This implements caddy.UDPServer interface.
|
||||
func (s *Server) ServePacket(p net.PacketConn) error {
|
||||
s.m.Lock()
|
||||
s.server[udp] = &dns.Server{PacketConn: p, Net: "udp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
ctx := context.Background()
|
||||
s.ServeDNS(ctx, w, r)
|
||||
})}
|
||||
s.m.Unlock()
|
||||
|
||||
return s.server[udp].ActivateAndServe()
|
||||
}
|
||||
|
||||
// Listen implements caddy.TCPServer interface.
|
||||
func (s *Server) Listen() (net.Listener, error) {
|
||||
l, err := net.Listen("tcp", s.Addr[len(TransportDNS+"://"):])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ListenPacket implements caddy.UDPServer interface.
|
||||
func (s *Server) ListenPacket() (net.PacketConn, error) {
|
||||
p, err := net.ListenPacket("udp", s.Addr[len(TransportDNS+"://"):])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Stop stops the server. It blocks until the server is
|
||||
// totally stopped. On POSIX systems, it will wait for
|
||||
// connections to close (up to a max timeout of a few
|
||||
// seconds); on Windows it will close the listener
|
||||
// immediately.
|
||||
// This implements Caddy.Stopper interface.
|
||||
func (s *Server) Stop() (err error) {
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
// force connections to close after timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
s.dnsWg.Done() // decrement our initial increment used as a barrier
|
||||
s.dnsWg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Wait for remaining connections to finish or
|
||||
// force them all to close after timeout
|
||||
select {
|
||||
case <-time.After(s.connTimeout):
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
|
||||
// Close the listener now; this stops the server without delay
|
||||
s.m.Lock()
|
||||
for _, s1 := range s.server {
|
||||
// We might not have started and initialized the full set of servers
|
||||
if s1 != nil {
|
||||
err = s1.Shutdown()
|
||||
}
|
||||
}
|
||||
s.m.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Address together with Stop() implement caddy.GracefulServer.
|
||||
func (s *Server) Address() string { return s.Addr }
|
||||
|
||||
// ServeDNS is the entry point for every request to the address that s
|
||||
// is bound to. It acts as a multiplexer for the requests zonename as
|
||||
// defined in the request so that the correct zone
|
||||
// (configuration and middleware stack) will handle the request.
|
||||
func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
|
||||
defer func() {
|
||||
// In case the user doesn't enable error middleware, we still
|
||||
// need to make sure that we stay alive up here
|
||||
if rec := recover(); rec != nil {
|
||||
DefaultErrorFunc(w, r, dns.RcodeServerFailure)
|
||||
}
|
||||
}()
|
||||
|
||||
if m, err := edns.Version(r); err != nil { // Wrong EDNS version, return at once.
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
|
||||
q := r.Question[0].Name
|
||||
b := make([]byte, len(q))
|
||||
off, end := 0, false
|
||||
|
||||
var dshandler *Config
|
||||
|
||||
for {
|
||||
l := len(q[off:])
|
||||
for i := 0; i < l; i++ {
|
||||
b[i] = q[off+i]
|
||||
// normalize the name for the lookup
|
||||
if b[i] >= 'A' && b[i] <= 'Z' {
|
||||
b[i] |= ('a' - 'A')
|
||||
}
|
||||
}
|
||||
|
||||
if h, ok := s.zones[string(b[:l])]; ok {
|
||||
if r.Question[0].Qtype != dns.TypeDS {
|
||||
rcode, _ := h.middlewareChain.ServeDNS(ctx, w, r)
|
||||
if rcodeNoClientWrite(rcode) {
|
||||
DefaultErrorFunc(w, r, rcode)
|
||||
}
|
||||
return
|
||||
}
|
||||
// The type is DS, keep the handler, but keep on searching as maybe we are serving
|
||||
// the parent as well and the DS should be routed to it - this will probably *misroute* DS
|
||||
// queries to a possibly grand parent, but there is no way for us to know at this point
|
||||
// if there is an actually delegation from grandparent -> parent -> zone.
|
||||
// In all fairness: direct DS queries should not be needed.
|
||||
dshandler = h
|
||||
}
|
||||
off, end = dns.NextLabel(q, off)
|
||||
if end {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if dshandler != nil {
|
||||
// DS request, and we found a zone, use the handler for the query
|
||||
rcode, _ := dshandler.middlewareChain.ServeDNS(ctx, w, r)
|
||||
if rcodeNoClientWrite(rcode) {
|
||||
DefaultErrorFunc(w, r, rcode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Wildcard match, if we have found nothing try the root zone as a last resort.
|
||||
if h, ok := s.zones["."]; ok {
|
||||
rcode, _ := h.middlewareChain.ServeDNS(ctx, w, r)
|
||||
if rcodeNoClientWrite(rcode) {
|
||||
DefaultErrorFunc(w, r, rcode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Still here? Error out with REFUSED and some logging
|
||||
remoteHost := w.RemoteAddr().String()
|
||||
DefaultErrorFunc(w, r, dns.RcodeRefused)
|
||||
log.Printf("[INFO] \"%s %s %s\" - No such zone at %s (Remote: %s)", dns.Type(r.Question[0].Qtype), dns.Class(r.Question[0].Qclass), q, s.Addr, remoteHost)
|
||||
}
|
||||
|
||||
// OnStartupComplete lists the sites served by this server
|
||||
// and any relevant information, assuming Quiet is false.
|
||||
func (s *Server) OnStartupComplete() {
|
||||
if Quiet {
|
||||
return
|
||||
}
|
||||
|
||||
for zone, config := range s.zones {
|
||||
fmt.Println(zone + ":" + config.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultErrorFunc responds to an DNS request with an error.
|
||||
func DefaultErrorFunc(w dns.ResponseWriter, r *dns.Msg, rc int) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(r, rc)
|
||||
|
||||
state.SizeAndDo(answer)
|
||||
|
||||
vars.Report(state, vars.Dropped, rcode.ToString(rc), answer.Len(), time.Now())
|
||||
|
||||
w.WriteMsg(answer)
|
||||
}
|
||||
|
||||
func rcodeNoClientWrite(rcode int) bool {
|
||||
switch rcode {
|
||||
case dns.RcodeServerFailure:
|
||||
fallthrough
|
||||
case dns.RcodeRefused:
|
||||
fallthrough
|
||||
case dns.RcodeFormatError:
|
||||
fallthrough
|
||||
case dns.RcodeNotImplemented:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
tcp = 0
|
||||
udp = 1
|
||||
)
|
||||
|
||||
var (
|
||||
// Quiet mode will not show any informative output on initialization.
|
||||
Quiet bool
|
||||
)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// generated by directives_generate.go; DO NOT EDIT
|
||||
|
||||
package dnsserver
|
||||
|
||||
// Directives are registered in the order they should be
|
||||
// executed.
|
||||
//
|
||||
// Ordering is VERY important. Every middleware will
|
||||
// feel the effects of all other middleware below
|
||||
// (after) them during a request, but they must not
|
||||
// care what middleware above them are doing.
|
||||
|
||||
var directives = []string{
|
||||
"tls",
|
||||
"root",
|
||||
"bind",
|
||||
"trace",
|
||||
"health",
|
||||
"pprof",
|
||||
"prometheus",
|
||||
"errors",
|
||||
"log",
|
||||
"chaos",
|
||||
"cache",
|
||||
"rewrite",
|
||||
"loadbalance",
|
||||
"dnssec",
|
||||
"reverse",
|
||||
"file",
|
||||
"auto",
|
||||
"secondary",
|
||||
"etcd",
|
||||
"kubernetes",
|
||||
"proxy",
|
||||
"whoami",
|
||||
"erratic",
|
||||
"startup",
|
||||
"shutdown",
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// generated by directives_generate.go; DO NOT EDIT
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
// Include all middleware.
|
||||
_ "github.com/coredns/coredns/middleware/auto"
|
||||
_ "github.com/coredns/coredns/middleware/bind"
|
||||
_ "github.com/coredns/coredns/middleware/cache"
|
||||
_ "github.com/coredns/coredns/middleware/chaos"
|
||||
_ "github.com/coredns/coredns/middleware/dnssec"
|
||||
_ "github.com/coredns/coredns/middleware/erratic"
|
||||
_ "github.com/coredns/coredns/middleware/errors"
|
||||
_ "github.com/coredns/coredns/middleware/etcd"
|
||||
_ "github.com/coredns/coredns/middleware/file"
|
||||
_ "github.com/coredns/coredns/middleware/health"
|
||||
_ "github.com/coredns/coredns/middleware/kubernetes"
|
||||
_ "github.com/coredns/coredns/middleware/loadbalance"
|
||||
_ "github.com/coredns/coredns/middleware/log"
|
||||
_ "github.com/coredns/coredns/middleware/metrics"
|
||||
_ "github.com/coredns/coredns/middleware/pprof"
|
||||
_ "github.com/coredns/coredns/middleware/proxy"
|
||||
_ "github.com/coredns/coredns/middleware/reverse"
|
||||
_ "github.com/coredns/coredns/middleware/rewrite"
|
||||
_ "github.com/coredns/coredns/middleware/root"
|
||||
_ "github.com/coredns/coredns/middleware/secondary"
|
||||
_ "github.com/coredns/coredns/middleware/tls"
|
||||
_ "github.com/coredns/coredns/middleware/trace"
|
||||
_ "github.com/coredns/coredns/middleware/whoami"
|
||||
_ "github.com/mholt/caddy/startupshutdown"
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
//go:generate go run directives_generate.go
|
||||
|
||||
import "github.com/coredns/coredns/coremain"
|
||||
|
||||
func main() {
|
||||
coremain.Run()
|
||||
}
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
// Package coremain contains the functions for starting CoreDNS.
|
||||
package coremain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
|
||||
// Plug in CoreDNS
|
||||
_ "github.com/coredns/coredns/core"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Reset flag.CommandLine to get rid of unwanted flags for instance from glog (used in kubernetes).
|
||||
// And readd the once we want to keep.
|
||||
flag.VisitAll(func(f *flag.Flag) {
|
||||
if _, ok := flagsBlacklist[f.Name]; ok {
|
||||
return
|
||||
}
|
||||
flagsToKeep = append(flagsToKeep, f)
|
||||
})
|
||||
|
||||
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
||||
for _, f := range flagsToKeep {
|
||||
flag.Var(f.Value, f.Name, f.Usage)
|
||||
}
|
||||
|
||||
caddy.TrapSignals()
|
||||
caddy.DefaultConfigFile = "Corefile"
|
||||
caddy.Quiet = true // don't show init stuff from caddy
|
||||
setVersion()
|
||||
|
||||
flag.StringVar(&conf, "conf", "", "Corefile to load (default \""+caddy.DefaultConfigFile+"\")")
|
||||
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
|
||||
flag.BoolVar(&plugins, "plugins", false, "List installed plugins")
|
||||
flag.StringVar(&logfile, "log", "", "Process log file")
|
||||
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
|
||||
flag.BoolVar(&version, "version", false, "Show version")
|
||||
flag.BoolVar(&dnsserver.Quiet, "quiet", false, "Quiet mode (no initialization output)")
|
||||
|
||||
caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
|
||||
caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))
|
||||
}
|
||||
|
||||
// Run is CoreDNS's main() function.
|
||||
func Run() {
|
||||
|
||||
flag.Parse()
|
||||
|
||||
caddy.AppName = coreName
|
||||
caddy.AppVersion = coreVersion
|
||||
|
||||
// Set up process log before anything bad happens
|
||||
switch logfile {
|
||||
case "stdout":
|
||||
log.SetOutput(os.Stdout)
|
||||
case "stderr":
|
||||
log.SetOutput(os.Stderr)
|
||||
default:
|
||||
log.SetOutput(os.Stdout)
|
||||
}
|
||||
log.SetFlags(log.LstdFlags)
|
||||
|
||||
if version {
|
||||
showVersion()
|
||||
os.Exit(0)
|
||||
}
|
||||
if plugins {
|
||||
fmt.Println(caddy.DescribePlugins())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Set CPU cap
|
||||
if err := setCPU(cpu); err != nil {
|
||||
mustLogFatal(err)
|
||||
}
|
||||
|
||||
// Get Corefile input
|
||||
corefile, err := caddy.LoadCaddyfile(serverType)
|
||||
if err != nil {
|
||||
mustLogFatal(err)
|
||||
}
|
||||
|
||||
// Start your engines
|
||||
instance, err := caddy.Start(corefile)
|
||||
if err != nil {
|
||||
mustLogFatal(err)
|
||||
}
|
||||
|
||||
logVersion()
|
||||
if !dnsserver.Quiet {
|
||||
showVersion()
|
||||
}
|
||||
|
||||
// Twiddle your thumbs
|
||||
instance.Wait()
|
||||
}
|
||||
|
||||
// mustLogFatal wraps log.Fatal() in a way that ensures the
|
||||
// output is always printed to stderr so the user can see it
|
||||
// if the user is still there, even if the process log was not
|
||||
// enabled. If this process is an upgrade, however, and the user
|
||||
// might not be there anymore, this just logs to the process
|
||||
// log and exits.
|
||||
func mustLogFatal(args ...interface{}) {
|
||||
if !caddy.IsUpgrade() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}
|
||||
log.Fatal(args...)
|
||||
}
|
||||
|
||||
// confLoader loads the Caddyfile using the -conf flag.
|
||||
func confLoader(serverType string) (caddy.Input, error) {
|
||||
if conf == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if conf == "stdin" {
|
||||
return caddy.CaddyfileFromPipe(os.Stdin, "dns")
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: conf,
|
||||
ServerTypeName: serverType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// defaultLoader loads the Corefile from the current working directory.
|
||||
func defaultLoader(serverType string) (caddy.Input, error) {
|
||||
contents, err := ioutil.ReadFile(caddy.DefaultConfigFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: caddy.DefaultConfigFile,
|
||||
ServerTypeName: serverType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// logVersion logs the version that is starting.
|
||||
func logVersion() { log.Print("[INFO] " + versionString()) }
|
||||
|
||||
// showVersion prints the version that is starting.
|
||||
func showVersion() {
|
||||
fmt.Print(versionString())
|
||||
if devBuild && gitShortStat != "" {
|
||||
fmt.Printf("%s\n%s\n", gitShortStat, gitFilesModified)
|
||||
}
|
||||
}
|
||||
|
||||
// versionString returns the CoreDNS version as a string.
|
||||
func versionString() string {
|
||||
return fmt.Sprintf("%s-%s\n", caddy.AppName, caddy.AppVersion)
|
||||
}
|
||||
|
||||
// setVersion figures out the version information
|
||||
// based on variables set by -ldflags.
|
||||
func setVersion() {
|
||||
// A development build is one that's not at a tag or has uncommitted changes
|
||||
devBuild = gitTag == "" || gitShortStat != ""
|
||||
|
||||
// Only set the appVersion if -ldflags was used
|
||||
if gitNearestTag != "" || gitTag != "" {
|
||||
if devBuild && gitNearestTag != "" {
|
||||
appVersion = fmt.Sprintf("%s (+%s %s)",
|
||||
strings.TrimPrefix(gitNearestTag, "v"), gitCommit, buildDate)
|
||||
} else if gitTag != "" {
|
||||
appVersion = strings.TrimPrefix(gitTag, "v")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setCPU parses string cpu and sets GOMAXPROCS
|
||||
// according to its value. It accepts either
|
||||
// a number (e.g. 3) or a percent (e.g. 50%).
|
||||
func setCPU(cpu string) error {
|
||||
var numCPU int
|
||||
|
||||
availCPU := runtime.NumCPU()
|
||||
|
||||
if strings.HasSuffix(cpu, "%") {
|
||||
// Percent
|
||||
var percent float32
|
||||
pctStr := cpu[:len(cpu)-1]
|
||||
pctInt, err := strconv.Atoi(pctStr)
|
||||
if err != nil || pctInt < 1 || pctInt > 100 {
|
||||
return errors.New("invalid CPU value: percentage must be between 1-100")
|
||||
}
|
||||
percent = float32(pctInt) / 100
|
||||
numCPU = int(float32(availCPU) * percent)
|
||||
} else {
|
||||
// Number
|
||||
num, err := strconv.Atoi(cpu)
|
||||
if err != nil || num < 1 {
|
||||
return errors.New("invalid CPU value: provide a number or percent greater than 0")
|
||||
}
|
||||
numCPU = num
|
||||
}
|
||||
|
||||
if numCPU > availCPU {
|
||||
numCPU = availCPU
|
||||
}
|
||||
|
||||
runtime.GOMAXPROCS(numCPU)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flags that control program flow or startup
|
||||
var (
|
||||
conf string
|
||||
cpu string
|
||||
logfile string
|
||||
version bool
|
||||
plugins bool
|
||||
)
|
||||
|
||||
// Build information obtained with the help of -ldflags
|
||||
var (
|
||||
appVersion = "(untracked dev build)" // inferred at startup
|
||||
devBuild = true // inferred at startup
|
||||
|
||||
buildDate string // date -u
|
||||
gitTag string // git describe --exact-match HEAD 2> /dev/null
|
||||
gitNearestTag string // git describe --abbrev=0 --tags HEAD
|
||||
gitCommit string // git rev-parse HEAD
|
||||
gitShortStat string // git diff-index --shortstat
|
||||
gitFilesModified string // git diff-index --name-only HEAD
|
||||
)
|
||||
|
||||
// flagsBlacklist removes flags with these names from our flagset.
|
||||
var flagsBlacklist = map[string]bool{
|
||||
"logtostderr": true,
|
||||
"alsologtostderr": true,
|
||||
"v": true,
|
||||
"stderrthreshold": true,
|
||||
"vmodule": true,
|
||||
"log_backtrace_at": true,
|
||||
"log_dir": true,
|
||||
}
|
||||
|
||||
var flagsToKeep []*flag.Flag
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package coremain
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetCPU(t *testing.T) {
|
||||
currentCPU := runtime.GOMAXPROCS(-1)
|
||||
maxCPU := runtime.NumCPU()
|
||||
halfCPU := int(0.5 * float32(maxCPU))
|
||||
if halfCPU < 1 {
|
||||
halfCPU = 1
|
||||
}
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
output int
|
||||
shouldErr bool
|
||||
}{
|
||||
{"1", 1, false},
|
||||
{"-1", currentCPU, true},
|
||||
{"0", currentCPU, true},
|
||||
{"100%", maxCPU, false},
|
||||
{"50%", halfCPU, false},
|
||||
{"110%", currentCPU, true},
|
||||
{"-10%", currentCPU, true},
|
||||
{"invalid input", currentCPU, true},
|
||||
{"invalid input%", currentCPU, true},
|
||||
{"9999", maxCPU, false}, // over available CPU
|
||||
} {
|
||||
err := setCPU(test.input)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error, but there wasn't any", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but there was one: %v", i, err)
|
||||
}
|
||||
if actual, expected := runtime.GOMAXPROCS(-1), test.output; actual != expected {
|
||||
t.Errorf("Test %d: GOMAXPROCS was %d but expected %d", i, actual, expected)
|
||||
}
|
||||
// teardown
|
||||
runtime.GOMAXPROCS(currentCPU)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package coremain
|
||||
|
||||
const (
|
||||
coreName = "CoreDNS"
|
||||
coreVersion = "006"
|
||||
|
||||
serverType = "dns"
|
||||
)
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
//+build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"go/format"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mi := make(map[string]string, 0)
|
||||
md := make(map[int]string, 0)
|
||||
|
||||
file, err := os.Open(middlewareFile)
|
||||
fatalIfErr(err)
|
||||
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
items := strings.Split(line, ":")
|
||||
if len(items) != 3 {
|
||||
// ignore
|
||||
continue
|
||||
}
|
||||
priority, err := strconv.Atoi(items[0])
|
||||
fatalIfErr(err)
|
||||
|
||||
if v, ok := md[priority]; ok {
|
||||
log.Fatalf("Duplicate priority '%d', slot already taken by %q", priority, v)
|
||||
}
|
||||
md[priority] = items[1]
|
||||
mi[items[1]] = middlewarePath + items[2] // Default, unless overriden by 3rd arg
|
||||
|
||||
if strings.Contains(items[2], "/") { // External package has been given
|
||||
mi[items[1]] = items[2]
|
||||
}
|
||||
}
|
||||
|
||||
genImports("core/zmiddleware.go", "core", mi)
|
||||
genDirectives("core/dnsserver/zdirectives.go", "dnsserver", md)
|
||||
}
|
||||
|
||||
func genImports(file, pack string, mi map[string]string) {
|
||||
outs := header + "package " + pack + "\n\n" + "import ("
|
||||
|
||||
if len(mi) > 0 {
|
||||
outs += "\n"
|
||||
}
|
||||
|
||||
outs += "// Include all middleware.\n"
|
||||
for _, v := range mi {
|
||||
outs += `_ "` + v + `"` + "\n"
|
||||
}
|
||||
outs += ")\n"
|
||||
|
||||
res, err := format.Source([]byte(outs))
|
||||
fatalIfErr(err)
|
||||
|
||||
err = ioutil.WriteFile(file, res, 0644)
|
||||
fatalIfErr(err)
|
||||
}
|
||||
|
||||
func genDirectives(file, pack string, md map[int]string) {
|
||||
|
||||
outs := header + "package " + pack + "\n\n"
|
||||
outs += `
|
||||
// Directives are registered in the order they should be
|
||||
// executed.
|
||||
//
|
||||
// Ordering is VERY important. Every middleware will
|
||||
// feel the effects of all other middleware below
|
||||
// (after) them during a request, but they must not
|
||||
// care what middleware above them are doing.
|
||||
|
||||
var directives = []string{
|
||||
`
|
||||
|
||||
var orders []int
|
||||
for k := range md {
|
||||
orders = append(orders, k)
|
||||
}
|
||||
sort.Ints(orders)
|
||||
|
||||
for _, k := range orders {
|
||||
outs += `"` + md[k] + `",` + "\n"
|
||||
}
|
||||
|
||||
outs += "}\n"
|
||||
|
||||
res, err := format.Source([]byte(outs))
|
||||
fatalIfErr(err)
|
||||
|
||||
err = ioutil.WriteFile(file, res, 0644)
|
||||
fatalIfErr(err)
|
||||
}
|
||||
|
||||
func fatalIfErr(err error) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
middlewarePath = "github.com/coredns/coredns/middleware/"
|
||||
middlewareFile = "middleware.cfg"
|
||||
header = "// generated by directives_generate.go; DO NOT EDIT\n\n"
|
||||
)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# Directives are registered in the order they should be
|
||||
# executed.
|
||||
#
|
||||
# Ordering is VERY important. Every middleware will
|
||||
# feel the effects of all other middleware below
|
||||
# (after) them during a request, but they must not
|
||||
# care what middleware above them are doing.
|
||||
|
||||
# How to rebuild with updated middleware configurations:
|
||||
# Modify the list below and run `go gen && go build`
|
||||
|
||||
# The parser takes the input format of
|
||||
# <order>:<middleware-name>:<package-name>
|
||||
# Or
|
||||
# <order>:<middleware-name>:<fully-qualified-package-name>
|
||||
#
|
||||
# External middleware example:
|
||||
# 80:log:github.com/coredns/coredns/middleware/log
|
||||
# Local middleware example:
|
||||
# 80:log:log
|
||||
|
||||
1:tls:tls
|
||||
10:root:root
|
||||
20:bind:bind
|
||||
30:trace:trace
|
||||
40:health:health
|
||||
50:pprof:pprof
|
||||
60:prometheus:metrics
|
||||
70:errors:errors
|
||||
80:log:log
|
||||
90:chaos:chaos
|
||||
100:cache:cache
|
||||
110:rewrite:rewrite
|
||||
120:loadbalance:loadbalance
|
||||
130:dnssec:dnssec
|
||||
140:reverse:reverse
|
||||
150:file:file
|
||||
160:auto:auto
|
||||
170:secondary:secondary
|
||||
180:etcd:etcd
|
||||
190:kubernetes:kubernetes
|
||||
200:proxy:proxy
|
||||
210:whoami:whoami
|
||||
220:erratic:erratic
|
||||
500:startup:github.com/mholt/caddy/startupshutdown
|
||||
510:shutdown:github.com/mholt/caddy/startupshutdown
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
# Middleware
|
||||
|
||||
## Writing Middleware
|
||||
|
||||
From the Caddy docs:
|
||||
|
||||
> Oh yes, those pesky return values on ServeHTTP(). You read the documentation so you already know
|
||||
> what they mean. But what does that imply for the behavior of your middleware?
|
||||
>
|
||||
> Basically, return a status code only if you did NOT write to the response body. If you DO write to
|
||||
> the response body, return a status code of 0. Return an error value if your middleware encountered
|
||||
> an error that you want logged. It is common to return an error status and an error value together,
|
||||
> so that the error handler up the chain can write the correct error page.
|
||||
>
|
||||
> The returned status code is not logged directly; rather, it tells middleware higher up the chain
|
||||
> what status code to use if/when the response body is written. Again, return a 0 status if you've
|
||||
> already written a body!
|
||||
|
||||
In the DNS status codes are called rcodes and it's slightly harder to return the correct
|
||||
answer in case of failure.
|
||||
|
||||
So CoreDNS treats:
|
||||
|
||||
* SERVFAIL (dns.RcodeServerFailure)
|
||||
* REFUSED (dns.RecodeRefused)
|
||||
* FORMERR (dns.RcodeFormatError)
|
||||
* NOTIMP (dns.RcodeNotImplemented)
|
||||
|
||||
as special and will then assume nothing has written to the client. In all other cases it is assumes
|
||||
something has been written to the client (by the middleware).
|
||||
|
||||
## Hooking It Up
|
||||
|
||||
See a couple of blog posts on how to write and add middleware to CoreDNS:
|
||||
|
||||
* <https://blog.coredns.io/#> TO BE PUBLISHED.
|
||||
* <https://blog.coredns.io/2016/12/19/writing-middleware-for-coredns/>, slightly older, but useful.
|
||||
|
||||
## Metrics
|
||||
|
||||
When exporting metrics the *Namespace* should be `middleware.Namespace` (="coredns"), and the
|
||||
*Subsystem* should be the name of the middleware. The README.md for the middleware should then
|
||||
also contain a *Metrics* section detailing the metrics.
|
||||
|
||||
## Documentation
|
||||
|
||||
Each middleware should have a README.md explaining what the middleware does and how it is
|
||||
configured. The file should have the following layout:
|
||||
|
||||
* Title: use the middleware's name
|
||||
* Subsection titled: "Syntax"
|
||||
* Subsection titled: "Examples"
|
||||
|
||||
More sections are of course possible.
|
||||
|
||||
### Style
|
||||
|
||||
We use the Unix manual page style:
|
||||
|
||||
* The name of middleware in the running text should be italic: *middleware*.
|
||||
* all CAPITAL: user supplied argument, in the running text references this use strong text: `**`:
|
||||
**EXAMPLE**.
|
||||
* Optional text: in block quotes: `[optional]`.
|
||||
* Use three dots to indicate multiple options are allowed: `arg...`.
|
||||
* Item used literal: `literal`.
|
||||
|
||||
### Example Domain Names
|
||||
|
||||
Please be sure to use `example.org` or `example.net` in any examples you provide. These are the
|
||||
standard domain names created for this purpose.
|
||||
|
||||
## Fallthrough
|
||||
|
||||
In a perfect world the following would be true for middleware: "Either you are responsible for
|
||||
a zone or not". If the answer is "not", the middleware should call the next middleware in the chain.
|
||||
If "yes" it should handle *all* names that fall in this zone and the names below - i.e. it should
|
||||
handle the entire domain.
|
||||
|
||||
~~~ txt
|
||||
. {
|
||||
file example.org db.example
|
||||
}
|
||||
~~~
|
||||
In this example the *file* middleware is handling all names below (and including) `example.org`. If
|
||||
a query comes in that is not a subdomain (or equal to) `example.org` the next middleware is called.
|
||||
|
||||
Now, the world isn't perfect, and there are good reasons to "fallthrough" to the next middlware,
|
||||
meaning a middleware is only responsible for a subset of names within the zone. The first of these
|
||||
to appear was the *reverse* middleware that synthesis PTR and A/AAAA responses (useful with IPv6).
|
||||
|
||||
The nature of the *reverse* middleware is such that it only deals with A,AAAA and PTR and then only
|
||||
for a subset of the names. Ideally you would want to layer *reverse* **in front off** another
|
||||
middleware such as *file* or *auto* (or even *proxy*). This means *reverse* handles some special
|
||||
reverse cases and **all other** request are handled by the backing middleware. This is exactly what
|
||||
"fallthrough" does. To keep things explicit we've opted that middlewares implement such behavior
|
||||
should implement a `fallthrough` keyword.
|
||||
|
||||
### Example Fallthrough Usage
|
||||
|
||||
The following Corefile example, sets up the *reverse* middleware, but disables fallthrough. It
|
||||
also defines a zonefile for use with the *file* middleware for other names in the `compute.internal`.
|
||||
|
||||
~~~ txt
|
||||
arpa compute.internal {
|
||||
reverse 10.32.0.0/16 {
|
||||
hostname ip-{ip}.{zone[2]}
|
||||
#fallthrough
|
||||
}
|
||||
file db.compute.internal compute.internal
|
||||
}
|
||||
~~~
|
||||
|
||||
This works for returning a response to a PTR request:
|
||||
|
||||
~~~ sh
|
||||
% dig +nocmd @localhost +noall +ans -x 10.32.0.1
|
||||
1.0.32.10.in-addr.arpa. 3600 IN PTR ip-10-32-0-1.compute.internal.
|
||||
~~~
|
||||
|
||||
And for the forward:
|
||||
|
||||
~~~ sh
|
||||
% dig +nocmd @localhost +noall +ans A ip-10-32-0-1.compute.internal
|
||||
ip-10-32-0-1.compute.internal. 3600 IN A 10.32.0.1
|
||||
~~~
|
||||
|
||||
But a query for `mx compute.internal` will return SERVFAIL. Now when we remove the '#' from
|
||||
fallthrough and reload (on Unix: `kill -SIGUSR1 $(pidof coredns)`) CoreDNS, we *should* get an
|
||||
answer for the MX query:
|
||||
|
||||
~~~ sh
|
||||
% dig +nocmd @localhost +noall +ans MX compute.internal
|
||||
compute.internal. 3600 IN MX 10 mx.compute.internal.
|
||||
~~~
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
# auto
|
||||
|
||||
*auto* enables serving zone data from an RFC 1035-style master file which is automatically picked
|
||||
up from disk.
|
||||
|
||||
The *auto* middleware is used for an "old-style" DNS server. It serves from a preloaded file that exists
|
||||
on disk. If the zone file contains signatures (i.e. is signed, i.e. DNSSEC) correct DNSSEC answers
|
||||
are returned. Only NSEC is supported! If you use this setup *you* are responsible for resigning the
|
||||
zonefile. New zones or changed zone are automatically picked up from disk.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
auto [ZONES...] {
|
||||
directory DIR [REGEXP ORIGIN_TEMPLATE [TIMEOUT]]
|
||||
no_reload
|
||||
upstream ADDRESS...
|
||||
}
|
||||
~~~
|
||||
|
||||
**ZONES** zones it should be authoritative for. If empty, the zones from the configuration block
|
||||
are used.
|
||||
|
||||
* `directory` loads zones from the speficied **DIR**. If a file name matches **REGEXP** it will be
|
||||
used to extract the origin. **ORIGIN_TEMPLATE** will be used as a template for the origin. Strings
|
||||
like `{<number>}` are replaced with the respective matches in the file name, i.e. `{1}` is the
|
||||
first match, `{2}` is the second, etc.. The default is: `db\.(.*) {1}` e.g. from a file with the
|
||||
name `db.example.com`, the extracted origin will be `example.com`. **TIMEOUT** specifies how often
|
||||
CoreDNS should scan the directory, the default is every 60 seconds. This value is in seconds.
|
||||
The minimum value is 1 second.
|
||||
* `no_reload` by default CoreDNS will reload a zone from disk whenever it detects a change to the
|
||||
file. This option disables that behavior.
|
||||
* `upstream` defines upstream resolvers to be used resolve external names found (think CNAMEs)
|
||||
pointing to external names.
|
||||
|
||||
All directives from the *file* middleware are supported. Note that *auto* will load all zones found,
|
||||
even though the directive might only receive queries for a specific zone. I.e:
|
||||
|
||||
~~~
|
||||
auto example.org {
|
||||
directory /etc/coredns/zones
|
||||
}
|
||||
~~~
|
||||
Will happily pick up a zone for `example.COM`, except it will never be queried, because the *auto*
|
||||
directive only is authoritative for `example.ORG`.
|
||||
|
||||
## Examples
|
||||
|
||||
Load `org` domains from `/etc/coredns/zones/org` and allow transfers to the internet, but send
|
||||
notifies to 10.240.1.1
|
||||
|
||||
~~~
|
||||
auto org {
|
||||
directory /etc/coredns/zones/org
|
||||
transfer to *
|
||||
transfer to 10.240.1.1
|
||||
}
|
||||
~~~
|
||||
|
||||
Load `org` domains from `/etc/coredns/zones/org` and looks for file names as `www.db.example.org`,
|
||||
where `example.org` is the origin. Scan every 45 seconds.
|
||||
|
||||
~~~
|
||||
auto org {
|
||||
directory /etc/coredns/zones/org www\.db\.(.*) {1} 45
|
||||
}
|
||||
~~~
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
// Package auto implements an on-the-fly loading file backend.
|
||||
package auto
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/middleware/file"
|
||||
"github.com/coredns/coredns/middleware/metrics"
|
||||
"github.com/coredns/coredns/middleware/proxy"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type (
|
||||
// Auto holds the zones and the loader configuration for automatically loading zones.
|
||||
Auto struct {
|
||||
Next middleware.Handler
|
||||
*Zones
|
||||
|
||||
metrics *metrics.Metrics
|
||||
loader
|
||||
}
|
||||
|
||||
loader struct {
|
||||
directory string
|
||||
template string
|
||||
re *regexp.Regexp
|
||||
|
||||
// In the future this should be something like ZoneMeta that contains all this stuff.
|
||||
transferTo []string
|
||||
noReload bool
|
||||
proxy proxy.Proxy // Proxy for looking up names during the resolution process
|
||||
|
||||
duration time.Duration
|
||||
}
|
||||
)
|
||||
|
||||
// ServeDNS implements the middleware.Handle interface.
|
||||
func (a Auto) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
if state.QClass() != dns.ClassINET {
|
||||
return dns.RcodeServerFailure, middleware.Error(a.Name(), errors.New("can only deal with ClassINET"))
|
||||
}
|
||||
qname := state.Name()
|
||||
|
||||
// TODO(miek): match the qname better in the map
|
||||
|
||||
// Precheck with the origins, i.e. are we allowed to looks here.
|
||||
zone := middleware.Zones(a.Zones.Origins()).Matches(qname)
|
||||
if zone == "" {
|
||||
return middleware.NextOrFailure(a.Name(), a.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
// Now the real zone.
|
||||
zone = middleware.Zones(a.Zones.Names()).Matches(qname)
|
||||
|
||||
a.Zones.RLock()
|
||||
z, ok := a.Zones.Z[zone]
|
||||
a.Zones.RUnlock()
|
||||
|
||||
if !ok || z == nil {
|
||||
return dns.RcodeServerFailure, nil
|
||||
}
|
||||
|
||||
if state.QType() == dns.TypeAXFR || state.QType() == dns.TypeIXFR {
|
||||
xfr := file.Xfr{Zone: z}
|
||||
return xfr.ServeDNS(ctx, w, r)
|
||||
}
|
||||
|
||||
answer, ns, extra, result := z.Lookup(state, qname)
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
|
||||
m.Answer, m.Ns, m.Extra = answer, ns, extra
|
||||
|
||||
switch result {
|
||||
case file.Success:
|
||||
case file.NoData:
|
||||
case file.NameError:
|
||||
m.Rcode = dns.RcodeNameError
|
||||
case file.Delegation:
|
||||
m.Authoritative = false
|
||||
case file.ServerFailure:
|
||||
return dns.RcodeServerFailure, nil
|
||||
}
|
||||
|
||||
state.SizeAndDo(m)
|
||||
m, _ = state.Scrub(m)
|
||||
w.WriteMsg(m)
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (a Auto) Name() string { return "auto" }
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package auto
|
||||
|
||||
// rewriteToExpand rewrites our template string to one that we can give to regexp.ExpandString. This basically
|
||||
// involves prefixing any '{' with a '$'.
|
||||
func rewriteToExpand(s string) string {
|
||||
// Pretty dumb at the moment, every { will get a $ prefixed.
|
||||
// Also wasteful as we build the string with +=. This is OKish
|
||||
// as we do this during config parsing.
|
||||
|
||||
copy := ""
|
||||
|
||||
for _, c := range s {
|
||||
if c == '{' {
|
||||
copy += "$"
|
||||
}
|
||||
copy += string(c)
|
||||
}
|
||||
|
||||
return copy
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package auto
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRewriteToExpand(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
expected string
|
||||
}{
|
||||
{in: "", expected: ""},
|
||||
{in: "{1}", expected: "${1}"},
|
||||
{in: "{1", expected: "${1"},
|
||||
}
|
||||
for i, tc := range tests {
|
||||
got := rewriteToExpand(tc.in)
|
||||
if got != tc.expected {
|
||||
t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
package auto
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/middleware/file"
|
||||
"github.com/coredns/coredns/middleware/metrics"
|
||||
"github.com/coredns/coredns/middleware/pkg/dnsutil"
|
||||
"github.com/coredns/coredns/middleware/proxy"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("auto", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
a, err := autoParse(c)
|
||||
if err != nil {
|
||||
return middleware.Error("auto", err)
|
||||
}
|
||||
|
||||
// If we have enabled prometheus we should add newly discovered zones to it.
|
||||
met := dnsserver.GetMiddleware(c, "prometheus")
|
||||
if met != nil {
|
||||
a.metrics = met.(*metrics.Metrics)
|
||||
}
|
||||
|
||||
walkChan := make(chan bool)
|
||||
|
||||
c.OnStartup(func() error {
|
||||
err := a.Walk()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(a.loader.duration)
|
||||
for {
|
||||
select {
|
||||
case <-walkChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
a.Walk()
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
|
||||
c.OnShutdown(func() error {
|
||||
close(walkChan)
|
||||
return nil
|
||||
})
|
||||
|
||||
dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler {
|
||||
a.Next = next
|
||||
return a
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoParse(c *caddy.Controller) (Auto, error) {
|
||||
var a = Auto{
|
||||
loader: loader{template: "${1}", re: regexp.MustCompile(`db\.(.*)`), duration: time.Duration(60 * time.Second)},
|
||||
Zones: &Zones{},
|
||||
}
|
||||
|
||||
config := dnsserver.GetConfig(c)
|
||||
|
||||
for c.Next() {
|
||||
if c.Val() == "auto" {
|
||||
// auto [ZONES...]
|
||||
a.Zones.origins = make([]string, len(c.ServerBlockKeys))
|
||||
copy(a.Zones.origins, c.ServerBlockKeys)
|
||||
|
||||
args := c.RemainingArgs()
|
||||
if len(args) > 0 {
|
||||
a.Zones.origins = args
|
||||
}
|
||||
for i := range a.Zones.origins {
|
||||
a.Zones.origins[i] = middleware.Host(a.Zones.origins[i]).Normalize()
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "directory": // directory DIR [REGEXP [TEMPLATE] [DURATION]]
|
||||
if !c.NextArg() {
|
||||
return a, c.ArgErr()
|
||||
}
|
||||
a.loader.directory = c.Val()
|
||||
if !path.IsAbs(a.loader.directory) && config.Root != "" {
|
||||
a.loader.directory = path.Join(config.Root, a.loader.directory)
|
||||
}
|
||||
_, err := os.Stat(a.loader.directory)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("[WARNING] Directory does not exist: %s", a.loader.directory)
|
||||
} else {
|
||||
return a, c.Errf("Unable to access root path '%s': %v", a.loader.directory, err)
|
||||
}
|
||||
}
|
||||
|
||||
// regexp
|
||||
if c.NextArg() {
|
||||
a.loader.re, err = regexp.Compile(c.Val())
|
||||
if err != nil {
|
||||
return a, err
|
||||
}
|
||||
if a.loader.re.NumSubexp() == 0 {
|
||||
return a, c.Errf("Need at least one sub expression")
|
||||
}
|
||||
}
|
||||
|
||||
// template
|
||||
if c.NextArg() {
|
||||
a.loader.template = rewriteToExpand(c.Val())
|
||||
}
|
||||
|
||||
// duration
|
||||
if c.NextArg() {
|
||||
i, err := strconv.Atoi(c.Val())
|
||||
if err != nil {
|
||||
return a, err
|
||||
}
|
||||
if i < 1 {
|
||||
i = 1
|
||||
}
|
||||
a.loader.duration = time.Duration(i) * time.Second
|
||||
}
|
||||
|
||||
case "no_reload":
|
||||
a.loader.noReload = true
|
||||
|
||||
case "upstream":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return a, c.ArgErr()
|
||||
}
|
||||
ups, err := dnsutil.ParseHostPortOrFile(args...)
|
||||
if err != nil {
|
||||
return a, err
|
||||
}
|
||||
a.loader.proxy = proxy.NewLookup(ups)
|
||||
|
||||
default:
|
||||
t, _, e := file.TransferParse(c, false)
|
||||
if e != nil {
|
||||
return a, e
|
||||
}
|
||||
if t != nil {
|
||||
a.loader.transferTo = append(a.loader.transferTo, t...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package auto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestAutoParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputFileRules string
|
||||
shouldErr bool
|
||||
expectedDirectory string
|
||||
expectedTempl string
|
||||
expectedRe string
|
||||
expectedTo []string
|
||||
}{
|
||||
{
|
||||
`auto example.org {
|
||||
directory /tmp
|
||||
transfer to 127.0.0.1
|
||||
}`,
|
||||
false, "/tmp", "${1}", `db\.(.*)`, []string{"127.0.0.1:53"},
|
||||
},
|
||||
{
|
||||
`auto {
|
||||
directory /tmp
|
||||
}`,
|
||||
false, "/tmp", "${1}", `db\.(.*)`, nil,
|
||||
},
|
||||
{
|
||||
`auto {
|
||||
directory /tmp (.*) bliep
|
||||
}`,
|
||||
false, "/tmp", "bliep", `(.*)`, nil,
|
||||
},
|
||||
{
|
||||
`auto {
|
||||
directory /tmp (.*) bliep
|
||||
transfer to 127.0.0.1
|
||||
transfer to 127.0.0.2
|
||||
upstream 8.8.8.8
|
||||
}`,
|
||||
false, "/tmp", "bliep", `(.*)`, []string{"127.0.0.1:53", "127.0.0.2:53"},
|
||||
},
|
||||
// errors
|
||||
{
|
||||
`auto example.org {
|
||||
directory
|
||||
}`,
|
||||
true, "", "${1}", `db\.(.*)`, nil,
|
||||
},
|
||||
{
|
||||
`auto example.org {
|
||||
directory /tmp * {1}
|
||||
}`,
|
||||
true, "", "${1}", ``, nil,
|
||||
},
|
||||
{
|
||||
`auto example.org {
|
||||
directory /tmp .* {1}
|
||||
}`,
|
||||
true, "", "${1}", ``, nil,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.inputFileRules)
|
||||
a, err := autoParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Fatalf("Test %d expected errors, but got no error", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Fatalf("Test %d expected no errors, but got '%v'", i, err)
|
||||
} else if !test.shouldErr {
|
||||
if a.loader.directory != test.expectedDirectory {
|
||||
t.Fatalf("Test %d expected %v, got %v", i, test.expectedDirectory, a.loader.directory)
|
||||
}
|
||||
if a.loader.template != test.expectedTempl {
|
||||
t.Fatalf("Test %d expected %v, got %v", i, test.expectedTempl, a.loader.template)
|
||||
}
|
||||
if a.loader.re.String() != test.expectedRe {
|
||||
t.Fatalf("Test %d expected %v, got %v", i, test.expectedRe, a.loader.re)
|
||||
}
|
||||
if test.expectedTo != nil {
|
||||
for j, got := range a.loader.transferTo {
|
||||
if got != test.expectedTo[j] {
|
||||
t.Fatalf("Test %d expected %v, got %v", i, test.expectedTo[j], got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package auto
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/coredns/coredns/middleware/file"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Walk will recursively walk of the file under l.directory and adds the one that match l.re.
|
||||
func (a Auto) Walk() error {
|
||||
|
||||
// TODO(miek): should add something so that we don't stomp on each other.
|
||||
|
||||
toDelete := make(map[string]bool)
|
||||
for _, n := range a.Zones.Names() {
|
||||
toDelete[n] = true
|
||||
}
|
||||
|
||||
filepath.Walk(a.loader.directory, func(path string, info os.FileInfo, err error) error {
|
||||
if info == nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
match, origin := matches(a.loader.re, info.Name(), a.loader.template)
|
||||
if !match {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, ok := a.Zones.Z[origin]; ok {
|
||||
// we already have this zone
|
||||
toDelete[origin] = false
|
||||
return nil
|
||||
}
|
||||
|
||||
reader, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Opening %s failed: %s", path, err)
|
||||
return nil
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
zo, err := file.Parse(reader, origin, path)
|
||||
if err != nil {
|
||||
// Parse barfs warning by itself...
|
||||
return nil
|
||||
}
|
||||
|
||||
zo.NoReload = a.loader.noReload
|
||||
zo.Proxy = a.loader.proxy
|
||||
zo.TransferTo = a.loader.transferTo
|
||||
|
||||
a.Zones.Add(zo, origin)
|
||||
|
||||
if a.metrics != nil {
|
||||
a.metrics.AddZone(origin)
|
||||
}
|
||||
|
||||
zo.Notify()
|
||||
|
||||
log.Printf("[INFO] Inserting zone `%s' from: %s", origin, path)
|
||||
|
||||
toDelete[origin] = false
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
for origin, ok := range toDelete {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if a.metrics != nil {
|
||||
a.metrics.RemoveZone(origin)
|
||||
}
|
||||
|
||||
a.Zones.Remove(origin)
|
||||
|
||||
log.Printf("[INFO] Deleting zone `%s'", origin)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// matches matches re to filename, if is is a match, the subexpression will be used to expand
|
||||
// template to an origin. When match is true that origin is returned. Origin is fully qualified.
|
||||
func matches(re *regexp.Regexp, filename, template string) (match bool, origin string) {
|
||||
base := path.Base(filename)
|
||||
|
||||
matches := re.FindStringSubmatchIndex(base)
|
||||
if matches == nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
by := re.ExpandString(nil, template, base, matches)
|
||||
if by == nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
origin = dns.Fqdn(string(by))
|
||||
|
||||
return true, origin
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package auto
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var dbFiles = []string{"db.example.org", "aa.example.org"}
|
||||
|
||||
const zoneContent = `; testzone
|
||||
@ IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082534 7200 3600 1209600 3600
|
||||
NS a.iana-servers.net.
|
||||
NS b.iana-servers.net.
|
||||
|
||||
www IN A 127.0.0.1
|
||||
`
|
||||
|
||||
func TestWalk(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
|
||||
tempdir, err := createFiles()
|
||||
if err != nil {
|
||||
if tempdir != "" {
|
||||
os.RemoveAll(tempdir)
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempdir)
|
||||
|
||||
ldr := loader{
|
||||
directory: tempdir,
|
||||
re: regexp.MustCompile(`db\.(.*)`),
|
||||
template: `${1}`,
|
||||
}
|
||||
|
||||
a := Auto{
|
||||
loader: ldr,
|
||||
Zones: &Zones{},
|
||||
}
|
||||
|
||||
a.Walk()
|
||||
|
||||
// db.example.org and db.example.com should be here (created in createFiles)
|
||||
for _, name := range []string{"example.com.", "example.org."} {
|
||||
if _, ok := a.Zones.Z[name]; !ok {
|
||||
t.Errorf("%s should have been added", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkNonExistent(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
|
||||
nonExistingDir := "highly_unlikely_to_exist_dir"
|
||||
|
||||
ldr := loader{
|
||||
directory: nonExistingDir,
|
||||
re: regexp.MustCompile(`db\.(.*)`),
|
||||
template: `${1}`,
|
||||
}
|
||||
|
||||
a := Auto{
|
||||
loader: ldr,
|
||||
Zones: &Zones{},
|
||||
}
|
||||
|
||||
a.Walk()
|
||||
}
|
||||
|
||||
func createFiles() (string, error) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "coredns")
|
||||
if err != nil {
|
||||
return dir, err
|
||||
}
|
||||
|
||||
for _, name := range dbFiles {
|
||||
if err := ioutil.WriteFile(path.Join(dir, name), []byte(zoneContent), 0644); err != nil {
|
||||
return dir, err
|
||||
}
|
||||
}
|
||||
// symlinks
|
||||
if err = os.Symlink(path.Join(dir, "db.example.org"), path.Join(dir, "db.example.com")); err != nil {
|
||||
return dir, err
|
||||
}
|
||||
if err = os.Symlink(path.Join(dir, "db.example.org"), path.Join(dir, "aa.example.com")); err != nil {
|
||||
return dir, err
|
||||
}
|
||||
|
||||
return dir, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package auto
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWatcher(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
|
||||
tempdir, err := createFiles()
|
||||
if err != nil {
|
||||
if tempdir != "" {
|
||||
os.RemoveAll(tempdir)
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempdir)
|
||||
|
||||
ldr := loader{
|
||||
directory: tempdir,
|
||||
re: regexp.MustCompile(`db\.(.*)`),
|
||||
template: `${1}`,
|
||||
}
|
||||
|
||||
a := Auto{
|
||||
loader: ldr,
|
||||
Zones: &Zones{},
|
||||
}
|
||||
|
||||
a.Walk()
|
||||
|
||||
// example.org and example.com should exist
|
||||
if x := len(a.Zones.Z["example.org."].All()); x != 4 {
|
||||
t.Fatalf("Expected 4 RRs, got %d", x)
|
||||
}
|
||||
if x := len(a.Zones.Z["example.com."].All()); x != 4 {
|
||||
t.Fatalf("Expected 4 RRs, got %d", x)
|
||||
}
|
||||
|
||||
// Now remove one file, rescan and see if it's gone.
|
||||
if err := os.Remove(path.Join(tempdir, "db.example.com")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
a.Walk()
|
||||
|
||||
if _, ok := a.Zones.Z["example.com."]; ok {
|
||||
t.Errorf("Expected %q to be gone.", "example.com.")
|
||||
}
|
||||
if _, ok := a.Zones.Z["example.org."]; !ok {
|
||||
t.Errorf("Expected %q to still be there.", "example.org.")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// Package auto implements a on-the-fly loading file backend.
|
||||
package auto
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/coredns/coredns/middleware/file"
|
||||
)
|
||||
|
||||
// Zones maps zone names to a *Zone. This keep track of what we zones we have loaded at
|
||||
// any one time.
|
||||
type Zones struct {
|
||||
Z map[string]*file.Zone // A map mapping zone (origin) to the Zone's data.
|
||||
names []string // All the keys from the map Z as a string slice.
|
||||
|
||||
origins []string // Any origins from the server block.
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// Names returns the names from z.
|
||||
func (z *Zones) Names() []string {
|
||||
z.RLock()
|
||||
n := z.names
|
||||
z.RUnlock()
|
||||
return n
|
||||
}
|
||||
|
||||
// Origins returns the origins from z.
|
||||
func (z *Zones) Origins() []string {
|
||||
// doesn't need locking, because there aren't multiple Go routines accessing it.
|
||||
return z.origins
|
||||
}
|
||||
|
||||
// Zones returns a zone with origin name from z, nil when not found.
|
||||
func (z *Zones) Zones(name string) *file.Zone {
|
||||
z.RLock()
|
||||
zo := z.Z[name]
|
||||
z.RUnlock()
|
||||
return zo
|
||||
}
|
||||
|
||||
// Add adds a new zone into z. If zo.NoReload is false, the
|
||||
// reload goroutine is started.
|
||||
func (z *Zones) Add(zo *file.Zone, name string) {
|
||||
z.Lock()
|
||||
|
||||
if z.Z == nil {
|
||||
z.Z = make(map[string]*file.Zone)
|
||||
}
|
||||
|
||||
z.Z[name] = zo
|
||||
z.names = append(z.names, name)
|
||||
zo.Reload()
|
||||
|
||||
z.Unlock()
|
||||
}
|
||||
|
||||
// Remove removes the zone named name from z. It also stop the the zone's reload goroutine.
|
||||
func (z *Zones) Remove(name string) {
|
||||
z.Lock()
|
||||
|
||||
if zo, ok := z.Z[name]; ok && !zo.NoReload {
|
||||
zo.ReloadShutdown <- true
|
||||
}
|
||||
|
||||
delete(z.Z, name)
|
||||
|
||||
// TODO(miek): just regenerate Names (might be bad if you have a lot of zones...)
|
||||
z.names = []string{}
|
||||
for n := range z.Z {
|
||||
z.names = append(z.names, n)
|
||||
}
|
||||
|
||||
z.Unlock()
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/middleware/etcd/msg"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// ServiceBackend defines a (dynamic) backend that returns a slice of service definitions.
|
||||
type ServiceBackend interface {
|
||||
// Services communicates with the backend to retrieve the service definition. Exact indicates
|
||||
// on exact much are that we are allowed to recurs.
|
||||
Services(state request.Request, exact bool, opt Options) ([]msg.Service, []msg.Service, error)
|
||||
|
||||
// Reverse communicates with the backend to retrieve service definition based on a IP address
|
||||
// instead of a name. I.e. a reverse DNS lookup.
|
||||
Reverse(state request.Request, exact bool, opt Options) ([]msg.Service, []msg.Service, error)
|
||||
|
||||
// Lookup is used to find records else where.
|
||||
Lookup(state request.Request, name string, typ uint16) (*dns.Msg, error)
|
||||
|
||||
// IsNameError return true if err indicated a record not found condition
|
||||
IsNameError(err error) bool
|
||||
|
||||
// Debug returns a string used when returning debug services.
|
||||
Debug() string
|
||||
}
|
||||
|
||||
// Options are extra options that can be specified for a lookup.
|
||||
type Options struct {
|
||||
Debug string // This is a debug query. A query prefixed with debug.o-o
|
||||
}
|
||||
|
|
@ -0,0 +1,451 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware/etcd/msg"
|
||||
"github.com/coredns/coredns/middleware/pkg/dnsutil"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// A returns A records from Backend or an error.
|
||||
func A(b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, debug []msg.Service, err error) {
|
||||
services, debug, err := b.Services(state, false, opt)
|
||||
if err != nil {
|
||||
return nil, debug, err
|
||||
}
|
||||
|
||||
for _, serv := range services {
|
||||
ip := net.ParseIP(serv.Host)
|
||||
switch {
|
||||
case ip == nil:
|
||||
if Name(state.Name()).Matches(dns.Fqdn(serv.Host)) {
|
||||
// x CNAME x is a direct loop, don't add those
|
||||
continue
|
||||
}
|
||||
|
||||
newRecord := serv.NewCNAME(state.QName(), serv.Host)
|
||||
if len(previousRecords) > 7 {
|
||||
// don't add it, and just continue
|
||||
continue
|
||||
}
|
||||
if dnsutil.DuplicateCNAME(newRecord, previousRecords) {
|
||||
continue
|
||||
}
|
||||
|
||||
state1 := state.NewWithQuestion(serv.Host, state.QType())
|
||||
nextRecords, nextDebug, err := A(b, zone, state1, append(previousRecords, newRecord), opt)
|
||||
|
||||
if err == nil {
|
||||
// Not only have we found something we should add the CNAME and the IP addresses.
|
||||
if len(nextRecords) > 0 {
|
||||
records = append(records, newRecord)
|
||||
records = append(records, nextRecords...)
|
||||
debug = append(debug, nextDebug...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// This means we can not complete the CNAME, try to look else where.
|
||||
target := newRecord.Target
|
||||
if dns.IsSubDomain(zone, target) {
|
||||
// We should already have found it
|
||||
continue
|
||||
}
|
||||
// Lookup
|
||||
m1, e1 := b.Lookup(state, target, state.QType())
|
||||
if e1 != nil {
|
||||
debugMsg := msg.Service{Key: msg.Path(target, b.Debug()), Host: target, Text: " IN " + state.Type() + ": " + e1.Error()}
|
||||
debug = append(debug, debugMsg)
|
||||
continue
|
||||
}
|
||||
// Len(m1.Answer) > 0 here is well?
|
||||
records = append(records, newRecord)
|
||||
records = append(records, m1.Answer...)
|
||||
continue
|
||||
case ip.To4() != nil:
|
||||
records = append(records, serv.NewA(state.QName(), ip.To4()))
|
||||
case ip.To4() == nil:
|
||||
// nodata?
|
||||
}
|
||||
}
|
||||
return records, debug, nil
|
||||
}
|
||||
|
||||
// AAAA returns AAAA records from Backend or an error.
|
||||
func AAAA(b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, debug []msg.Service, err error) {
|
||||
services, debug, err := b.Services(state, false, opt)
|
||||
if err != nil {
|
||||
return nil, debug, err
|
||||
}
|
||||
|
||||
for _, serv := range services {
|
||||
ip := net.ParseIP(serv.Host)
|
||||
switch {
|
||||
case ip == nil:
|
||||
// Try to resolve as CNAME if it's not an IP, but only if we don't create loops.
|
||||
if Name(state.Name()).Matches(dns.Fqdn(serv.Host)) {
|
||||
// x CNAME x is a direct loop, don't add those
|
||||
continue
|
||||
}
|
||||
|
||||
newRecord := serv.NewCNAME(state.QName(), serv.Host)
|
||||
if len(previousRecords) > 7 {
|
||||
// don't add it, and just continue
|
||||
continue
|
||||
}
|
||||
if dnsutil.DuplicateCNAME(newRecord, previousRecords) {
|
||||
continue
|
||||
}
|
||||
|
||||
state1 := state.NewWithQuestion(serv.Host, state.QType())
|
||||
nextRecords, nextDebug, err := AAAA(b, zone, state1, append(previousRecords, newRecord), opt)
|
||||
|
||||
if err == nil {
|
||||
// Not only have we found something we should add the CNAME and the IP addresses.
|
||||
if len(nextRecords) > 0 {
|
||||
records = append(records, newRecord)
|
||||
records = append(records, nextRecords...)
|
||||
debug = append(debug, nextDebug...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// This means we can not complete the CNAME, try to look else where.
|
||||
target := newRecord.Target
|
||||
if dns.IsSubDomain(zone, target) {
|
||||
// We should already have found it
|
||||
continue
|
||||
}
|
||||
m1, e1 := b.Lookup(state, target, state.QType())
|
||||
if e1 != nil {
|
||||
debugMsg := msg.Service{Key: msg.Path(target, b.Debug()), Host: target, Text: " IN " + state.Type() + ": " + e1.Error()}
|
||||
debug = append(debug, debugMsg)
|
||||
continue
|
||||
}
|
||||
// Len(m1.Answer) > 0 here is well?
|
||||
records = append(records, newRecord)
|
||||
records = append(records, m1.Answer...)
|
||||
continue
|
||||
// both here again
|
||||
case ip.To4() != nil:
|
||||
// nada?
|
||||
case ip.To4() == nil:
|
||||
records = append(records, serv.NewAAAA(state.QName(), ip.To16()))
|
||||
}
|
||||
}
|
||||
return records, debug, nil
|
||||
}
|
||||
|
||||
// SRV returns SRV records from the Backend.
|
||||
// If the Target is not a name but an IP address, a name is created on the fly.
|
||||
func SRV(b ServiceBackend, zone string, state request.Request, opt Options) (records, extra []dns.RR, debug []msg.Service, err error) {
|
||||
services, debug, err := b.Services(state, false, opt)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Looping twice to get the right weight vs priority
|
||||
w := make(map[int]int)
|
||||
for _, serv := range services {
|
||||
weight := 100
|
||||
if serv.Weight != 0 {
|
||||
weight = serv.Weight
|
||||
}
|
||||
if _, ok := w[serv.Priority]; !ok {
|
||||
w[serv.Priority] = weight
|
||||
continue
|
||||
}
|
||||
w[serv.Priority] += weight
|
||||
}
|
||||
lookup := make(map[string]bool)
|
||||
for _, serv := range services {
|
||||
w1 := 100.0 / float64(w[serv.Priority])
|
||||
if serv.Weight == 0 {
|
||||
w1 *= 100
|
||||
} else {
|
||||
w1 *= float64(serv.Weight)
|
||||
}
|
||||
weight := uint16(math.Floor(w1))
|
||||
ip := net.ParseIP(serv.Host)
|
||||
switch {
|
||||
case ip == nil:
|
||||
srv := serv.NewSRV(state.QName(), weight)
|
||||
records = append(records, srv)
|
||||
|
||||
if _, ok := lookup[srv.Target]; ok {
|
||||
break
|
||||
}
|
||||
|
||||
lookup[srv.Target] = true
|
||||
|
||||
if !dns.IsSubDomain(zone, srv.Target) {
|
||||
m1, e1 := b.Lookup(state, srv.Target, dns.TypeA)
|
||||
if e1 == nil {
|
||||
extra = append(extra, m1.Answer...)
|
||||
} else {
|
||||
debugMsg := msg.Service{Key: msg.Path(srv.Target, b.Debug()), Host: srv.Target, Text: " IN A: " + e1.Error()}
|
||||
debug = append(debug, debugMsg)
|
||||
}
|
||||
|
||||
m1, e1 = b.Lookup(state, srv.Target, dns.TypeAAAA)
|
||||
if e1 == nil {
|
||||
// If we have seen CNAME's we *assume* that they are already added.
|
||||
for _, a := range m1.Answer {
|
||||
if _, ok := a.(*dns.CNAME); !ok {
|
||||
extra = append(extra, a)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugMsg := msg.Service{Key: msg.Path(srv.Target, b.Debug()), Host: srv.Target, Text: " IN AAAA: " + e1.Error()}
|
||||
debug = append(debug, debugMsg)
|
||||
}
|
||||
break
|
||||
}
|
||||
// Internal name, we should have some info on them, either v4 or v6
|
||||
// Clients expect a complete answer, because we are a recursor in their view.
|
||||
state1 := state.NewWithQuestion(srv.Target, dns.TypeA)
|
||||
addr, debugAddr, e1 := A(b, zone, state1, nil, Options(opt))
|
||||
if e1 == nil {
|
||||
extra = append(extra, addr...)
|
||||
debug = append(debug, debugAddr...)
|
||||
}
|
||||
// IPv6 lookups here as well? AAAA(zone, state1, nil).
|
||||
case ip.To4() != nil:
|
||||
serv.Host = msg.Domain(serv.Key)
|
||||
srv := serv.NewSRV(state.QName(), weight)
|
||||
|
||||
records = append(records, srv)
|
||||
extra = append(extra, serv.NewA(srv.Target, ip.To4()))
|
||||
case ip.To4() == nil:
|
||||
serv.Host = msg.Domain(serv.Key)
|
||||
srv := serv.NewSRV(state.QName(), weight)
|
||||
|
||||
records = append(records, srv)
|
||||
extra = append(extra, serv.NewAAAA(srv.Target, ip.To16()))
|
||||
}
|
||||
}
|
||||
return records, extra, debug, nil
|
||||
}
|
||||
|
||||
// MX returns MX records from the Backend. If the Target is not a name but an IP address, a name is created on the fly.
|
||||
func MX(b ServiceBackend, zone string, state request.Request, opt Options) (records, extra []dns.RR, debug []msg.Service, err error) {
|
||||
services, debug, err := b.Services(state, false, opt)
|
||||
if err != nil {
|
||||
return nil, nil, debug, err
|
||||
}
|
||||
|
||||
lookup := make(map[string]bool)
|
||||
for _, serv := range services {
|
||||
if !serv.Mail {
|
||||
continue
|
||||
}
|
||||
ip := net.ParseIP(serv.Host)
|
||||
switch {
|
||||
case ip == nil:
|
||||
mx := serv.NewMX(state.QName())
|
||||
records = append(records, mx)
|
||||
if _, ok := lookup[mx.Mx]; ok {
|
||||
break
|
||||
}
|
||||
|
||||
lookup[mx.Mx] = true
|
||||
|
||||
if !dns.IsSubDomain(zone, mx.Mx) {
|
||||
m1, e1 := b.Lookup(state, mx.Mx, dns.TypeA)
|
||||
if e1 == nil {
|
||||
extra = append(extra, m1.Answer...)
|
||||
} else {
|
||||
debugMsg := msg.Service{Key: msg.Path(mx.Mx, b.Debug()), Host: mx.Mx, Text: " IN A: " + e1.Error()}
|
||||
debug = append(debug, debugMsg)
|
||||
}
|
||||
m1, e1 = b.Lookup(state, mx.Mx, dns.TypeAAAA)
|
||||
if e1 == nil {
|
||||
// If we have seen CNAME's we *assume* that they are already added.
|
||||
for _, a := range m1.Answer {
|
||||
if _, ok := a.(*dns.CNAME); !ok {
|
||||
extra = append(extra, a)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugMsg := msg.Service{Key: msg.Path(mx.Mx, b.Debug()), Host: mx.Mx, Text: " IN AAAA: " + e1.Error()}
|
||||
debug = append(debug, debugMsg)
|
||||
}
|
||||
break
|
||||
}
|
||||
// Internal name
|
||||
state1 := state.NewWithQuestion(mx.Mx, dns.TypeA)
|
||||
addr, debugAddr, e1 := A(b, zone, state1, nil, opt)
|
||||
if e1 == nil {
|
||||
extra = append(extra, addr...)
|
||||
debug = append(debug, debugAddr...)
|
||||
}
|
||||
// e.AAAA as well
|
||||
case ip.To4() != nil:
|
||||
serv.Host = msg.Domain(serv.Key)
|
||||
records = append(records, serv.NewMX(state.QName()))
|
||||
extra = append(extra, serv.NewA(serv.Host, ip.To4()))
|
||||
case ip.To4() == nil:
|
||||
serv.Host = msg.Domain(serv.Key)
|
||||
records = append(records, serv.NewMX(state.QName()))
|
||||
extra = append(extra, serv.NewAAAA(serv.Host, ip.To16()))
|
||||
}
|
||||
}
|
||||
return records, extra, debug, nil
|
||||
}
|
||||
|
||||
// CNAME returns CNAME records from the backend or an error.
|
||||
func CNAME(b ServiceBackend, zone string, state request.Request, opt Options) (records []dns.RR, debug []msg.Service, err error) {
|
||||
services, debug, err := b.Services(state, true, opt)
|
||||
if err != nil {
|
||||
return nil, debug, err
|
||||
}
|
||||
|
||||
if len(services) > 0 {
|
||||
serv := services[0]
|
||||
if ip := net.ParseIP(serv.Host); ip == nil {
|
||||
records = append(records, serv.NewCNAME(state.QName(), serv.Host))
|
||||
}
|
||||
}
|
||||
return records, debug, nil
|
||||
}
|
||||
|
||||
// TXT returns TXT records from Backend or an error.
|
||||
func TXT(b ServiceBackend, zone string, state request.Request, opt Options) (records []dns.RR, debug []msg.Service, err error) {
|
||||
services, debug, err := b.Services(state, false, opt)
|
||||
if err != nil {
|
||||
return nil, debug, err
|
||||
}
|
||||
|
||||
for _, serv := range services {
|
||||
if serv.Text == "" {
|
||||
continue
|
||||
}
|
||||
records = append(records, serv.NewTXT(state.QName()))
|
||||
}
|
||||
return records, debug, nil
|
||||
}
|
||||
|
||||
// PTR returns the PTR records from the backend, only services that have a domain name as host are included.
|
||||
func PTR(b ServiceBackend, zone string, state request.Request, opt Options) (records []dns.RR, debug []msg.Service, err error) {
|
||||
services, debug, err := b.Reverse(state, true, opt)
|
||||
if err != nil {
|
||||
return nil, debug, err
|
||||
}
|
||||
|
||||
for _, serv := range services {
|
||||
if ip := net.ParseIP(serv.Host); ip == nil {
|
||||
records = append(records, serv.NewPTR(state.QName(), serv.Host))
|
||||
}
|
||||
}
|
||||
return records, debug, nil
|
||||
}
|
||||
|
||||
// NS returns NS records from the backend
|
||||
func NS(b ServiceBackend, zone string, state request.Request, opt Options) (records, extra []dns.RR, debug []msg.Service, err error) {
|
||||
// NS record for this zone live in a special place, ns.dns.<zone>. Fake our lookup.
|
||||
// only a tad bit fishy...
|
||||
old := state.QName()
|
||||
|
||||
state.Clear()
|
||||
state.Req.Question[0].Name = "ns.dns." + zone
|
||||
services, debug, err := b.Services(state, false, opt)
|
||||
if err != nil {
|
||||
return nil, nil, debug, err
|
||||
}
|
||||
// ... and reset
|
||||
state.Req.Question[0].Name = old
|
||||
|
||||
for _, serv := range services {
|
||||
ip := net.ParseIP(serv.Host)
|
||||
switch {
|
||||
case ip == nil:
|
||||
return nil, nil, debug, fmt.Errorf("NS record must be an IP address: %s", serv.Host)
|
||||
case ip.To4() != nil:
|
||||
serv.Host = msg.Domain(serv.Key)
|
||||
records = append(records, serv.NewNS(state.QName()))
|
||||
extra = append(extra, serv.NewA(serv.Host, ip.To4()))
|
||||
case ip.To4() == nil:
|
||||
serv.Host = msg.Domain(serv.Key)
|
||||
records = append(records, serv.NewNS(state.QName()))
|
||||
extra = append(extra, serv.NewAAAA(serv.Host, ip.To16()))
|
||||
}
|
||||
}
|
||||
return records, extra, debug, nil
|
||||
}
|
||||
|
||||
// SOA returns a SOA record from the backend.
|
||||
func SOA(b ServiceBackend, zone string, state request.Request, opt Options) ([]dns.RR, []msg.Service, error) {
|
||||
header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: 300, Class: dns.ClassINET}
|
||||
|
||||
soa := &dns.SOA{Hdr: header,
|
||||
Mbox: hostmaster + "." + zone,
|
||||
Ns: "ns.dns." + zone,
|
||||
Serial: uint32(time.Now().Unix()),
|
||||
Refresh: 7200,
|
||||
Retry: 1800,
|
||||
Expire: 86400,
|
||||
Minttl: minTTL,
|
||||
}
|
||||
// TODO(miek): fake some msg.Service here when returning?
|
||||
return []dns.RR{soa}, nil, nil
|
||||
}
|
||||
|
||||
// BackendError writes an error response to the client.
|
||||
func BackendError(b ServiceBackend, zone string, rcode int, state request.Request, debug []msg.Service, err error, opt Options) (int, error) {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(state.Req, rcode)
|
||||
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
|
||||
m.Ns, _, _ = SOA(b, zone, state, opt)
|
||||
if opt.Debug != "" {
|
||||
m.Extra = ServicesToTxt(debug)
|
||||
txt := ErrorToTxt(err)
|
||||
if txt != nil {
|
||||
m.Extra = append(m.Extra, ErrorToTxt(err))
|
||||
}
|
||||
}
|
||||
state.SizeAndDo(m)
|
||||
state.W.WriteMsg(m)
|
||||
// Return success as the rcode to signal we have written to the client.
|
||||
return dns.RcodeSuccess, err
|
||||
}
|
||||
|
||||
// ServicesToTxt puts debug in TXT RRs.
|
||||
func ServicesToTxt(debug []msg.Service) []dns.RR {
|
||||
if debug == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rr := make([]dns.RR, len(debug))
|
||||
for i, d := range debug {
|
||||
rr[i] = d.RR()
|
||||
}
|
||||
return rr
|
||||
}
|
||||
|
||||
// ErrorToTxt puts in error's text into an TXT RR.
|
||||
func ErrorToTxt(err error) dns.RR {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
msg := err.Error()
|
||||
if len(msg) > 255 {
|
||||
msg = msg[:255]
|
||||
}
|
||||
t := new(dns.TXT)
|
||||
t.Hdr.Class = dns.ClassCHAOS
|
||||
t.Hdr.Ttl = 0
|
||||
t.Hdr.Rrtype = dns.TypeTXT
|
||||
t.Hdr.Name = "."
|
||||
|
||||
t.Txt = []string{msg}
|
||||
return t
|
||||
}
|
||||
|
||||
const (
|
||||
minTTL = 60
|
||||
hostmaster = "hostmaster"
|
||||
)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# bind
|
||||
|
||||
*bind* overrides the host to which the server should bind. Normally, the listener binds to the
|
||||
wildcard host. However, you may force the listener to bind to another IP instead. This
|
||||
directive accepts only an address, not a port.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~ txt
|
||||
bind ADDRESS
|
||||
~~~
|
||||
|
||||
**ADDRESS** is the IP address to bind to.
|
||||
|
||||
## Examples
|
||||
|
||||
To make your socket accessible only to that machine, bind to IP 127.0.0.1 (localhost):
|
||||
|
||||
~~~ txt
|
||||
bind 127.0.0.1
|
||||
~~~
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// Package bind allows binding to a specific interface instead of bind to all of them.
|
||||
package bind
|
||||
|
||||
import "github.com/mholt/caddy"
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("bind", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setupBind,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package bind
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestSetupBind(t *testing.T) {
|
||||
c := caddy.NewTestController("dns", `bind 1.2.3.4`)
|
||||
err := setupBind(c)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
|
||||
cfg := dnsserver.GetConfig(c)
|
||||
if got, want := cfg.ListenHost, "1.2.3.4"; got != want {
|
||||
t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindAddress(t *testing.T) {
|
||||
c := caddy.NewTestController("dns", `bind 1.2.3.bla`)
|
||||
err := setupBind(c)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected errors, but got none")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package bind
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/middleware"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func setupBind(c *caddy.Controller) error {
|
||||
config := dnsserver.GetConfig(c)
|
||||
for c.Next() {
|
||||
if !c.Args(&config.ListenHost) {
|
||||
return middleware.Error("bind", c.ArgErr())
|
||||
}
|
||||
}
|
||||
if net.ParseIP(config.ListenHost) == nil {
|
||||
return middleware.Error("bind", fmt.Errorf("not a valid IP address: %s", config.ListenHost))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
# cache
|
||||
|
||||
*cache* enables a frontend cache. It will cache all records except zone transfers and metadata records.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~ txt
|
||||
cache [TTL] [ZONES...]
|
||||
~~~
|
||||
|
||||
* **TTL** max TTL in seconds. If not specified, the maximum TTL will be used which is 3600 for
|
||||
noerror responses and 1800 for denial of existence ones.
|
||||
A set TTL of 300 *cache 300* would cache the record up to 300 seconds.
|
||||
Smaller record provided TTLs will take precedence.
|
||||
* **ZONES** zones it should cache for. If empty, the zones from the configuration block are used.
|
||||
|
||||
Each element in the cache is cached according to its TTL (with **TTL** as the max).
|
||||
For the negative cache, the SOA's MinTTL value is used. A cache can contain up to 10,000 items by
|
||||
default. A TTL of zero is not allowed. No cache invalidation triggered by other middlewares is available. Therefore even reloaded items might still be cached for the duration of the TTL.
|
||||
|
||||
If you want more control:
|
||||
|
||||
~~~ txt
|
||||
cache [TTL] [ZONES...] {
|
||||
success CAPACITY [TTL]
|
||||
denial CAPACITY [TTL]
|
||||
}
|
||||
~~~
|
||||
|
||||
* **TTL** and **ZONES** as above.
|
||||
* `success`, override the settings for caching successful responses, **CAPACITY** indicates the maximum
|
||||
number of packets we cache before we start evicting (LRU). **TTL** overrides the cache maximum TTL.
|
||||
* `denial`, override the settings for caching denial of existence responses, **CAPACITY** indicates the maximum
|
||||
number of packets we cache before we start evicting (LRU). **TTL** overrides the cache maximum TTL.
|
||||
|
||||
There is a third category (`error`) but those responses are never cached.
|
||||
|
||||
The minimum TTL allowed on resource records is 5 seconds.
|
||||
|
||||
## Metrics
|
||||
|
||||
If monitoring is enabled (via the *prometheus* directive) then the following metrics are exported:
|
||||
|
||||
* coredns_cache_size{type} - Total elements in the cache by cache type.
|
||||
* coredns_cache_capacity{type} - Total capacity of the cache by cache type.
|
||||
* coredns_cache_hits_total{type} - Counter of cache hits by cache type.
|
||||
* coredns_cache_misses_total - Counter of cache misses.
|
||||
|
||||
Cache types are either "denial" or "success".
|
||||
|
||||
## Examples
|
||||
|
||||
Enable caching for all zones, but cap everything to a TTL of 10 seconds:
|
||||
|
||||
~~~
|
||||
cache 10
|
||||
~~~
|
||||
|
||||
Proxy to Google Public DNS and only cache responses for example.org (or below).
|
||||
|
||||
~~~
|
||||
proxy . 8.8.8.8:53
|
||||
cache example.org
|
||||
~~~
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
// Package cache implements a cache.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/middleware/pkg/response"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Cache is middleware that looks up responses in a cache and caches replies.
|
||||
// It has a success and a denial of existence cache.
|
||||
type Cache struct {
|
||||
Next middleware.Handler
|
||||
Zones []string
|
||||
|
||||
ncache *lru.Cache
|
||||
ncap int
|
||||
nttl time.Duration
|
||||
|
||||
pcache *lru.Cache
|
||||
pcap int
|
||||
pttl time.Duration
|
||||
}
|
||||
|
||||
// Return key under which we store the item. The empty string is returned
|
||||
// when we don't want to cache the message. Currently we do not cache Truncated, errors
|
||||
// zone transfers or dynamic update messages.
|
||||
func key(m *dns.Msg, t response.Type, do bool) string {
|
||||
// We don't store truncated responses.
|
||||
if m.Truncated {
|
||||
return ""
|
||||
}
|
||||
// Nor errors or Meta or Update
|
||||
if t == response.OtherError || t == response.Meta || t == response.Update {
|
||||
return ""
|
||||
}
|
||||
|
||||
qtype := m.Question[0].Qtype
|
||||
qname := strings.ToLower(m.Question[0].Name)
|
||||
return rawKey(qname, qtype, do)
|
||||
}
|
||||
|
||||
func rawKey(qname string, qtype uint16, do bool) string {
|
||||
if do {
|
||||
return "1" + qname + "." + strconv.Itoa(int(qtype))
|
||||
}
|
||||
return "0" + qname + "." + strconv.Itoa(int(qtype))
|
||||
}
|
||||
|
||||
// ResponseWriter is a response writer that caches the reply message.
|
||||
type ResponseWriter struct {
|
||||
dns.ResponseWriter
|
||||
*Cache
|
||||
}
|
||||
|
||||
// WriteMsg implements the dns.ResponseWriter interface.
|
||||
func (c *ResponseWriter) WriteMsg(res *dns.Msg) error {
|
||||
do := false
|
||||
mt, opt := response.Typify(res)
|
||||
if opt != nil {
|
||||
do = opt.Do()
|
||||
}
|
||||
|
||||
// key returns empty string for anything we don't want to cache.
|
||||
key := key(res, mt, do)
|
||||
|
||||
duration := c.pttl
|
||||
if mt == response.NameError || mt == response.NoData {
|
||||
duration = c.nttl
|
||||
}
|
||||
|
||||
msgTTL := minMsgTTL(res, mt)
|
||||
if msgTTL < duration {
|
||||
duration = msgTTL
|
||||
}
|
||||
|
||||
if key != "" {
|
||||
c.set(res, key, mt, duration)
|
||||
|
||||
cacheSize.WithLabelValues(Success).Set(float64(c.pcache.Len()))
|
||||
cacheSize.WithLabelValues(Denial).Set(float64(c.ncache.Len()))
|
||||
}
|
||||
|
||||
setMsgTTL(res, uint32(duration.Seconds()))
|
||||
|
||||
return c.ResponseWriter.WriteMsg(res)
|
||||
}
|
||||
|
||||
func (c *ResponseWriter) set(m *dns.Msg, key string, mt response.Type, duration time.Duration) {
|
||||
if key == "" {
|
||||
log.Printf("[ERROR] Caching called with empty cache key")
|
||||
return
|
||||
}
|
||||
|
||||
switch mt {
|
||||
case response.NoError, response.Delegation:
|
||||
i := newItem(m, duration)
|
||||
c.pcache.Add(key, i)
|
||||
|
||||
case response.NameError, response.NoData:
|
||||
i := newItem(m, duration)
|
||||
c.ncache.Add(key, i)
|
||||
|
||||
case response.OtherError:
|
||||
// don't cache these
|
||||
default:
|
||||
log.Printf("[WARNING] Caching called with unknown classification: %d", mt)
|
||||
}
|
||||
}
|
||||
|
||||
// Write implements the dns.ResponseWriter interface.
|
||||
func (c *ResponseWriter) Write(buf []byte) (int, error) {
|
||||
log.Printf("[WARNING] Caching called with Write: not caching reply")
|
||||
n, err := c.ResponseWriter.Write(buf)
|
||||
return n, err
|
||||
}
|
||||
|
||||
const (
|
||||
maxTTL = 1 * time.Hour
|
||||
maxNTTL = 30 * time.Minute
|
||||
|
||||
minTTL = 5 // seconds
|
||||
|
||||
defaultCap = 10000 // default capacity of the cache.
|
||||
|
||||
// Success is the class for caching positive caching.
|
||||
Success = "success"
|
||||
// Denial is the class defined for negative caching.
|
||||
Denial = "denial"
|
||||
)
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/middleware/pkg/response"
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type cacheTestCase struct {
|
||||
test.Case
|
||||
in test.Case
|
||||
AuthenticatedData bool
|
||||
Authoritative bool
|
||||
RecursionAvailable bool
|
||||
Truncated bool
|
||||
}
|
||||
|
||||
var cacheTestCases = []cacheTestCase{
|
||||
{
|
||||
RecursionAvailable: true, AuthenticatedData: true, Authoritative: true,
|
||||
Case: test.Case{
|
||||
Qname: "miek.nl.", Qtype: dns.TypeMX,
|
||||
Answer: []dns.RR{
|
||||
test.MX("miek.nl. 3600 IN MX 1 aspmx.l.google.com."),
|
||||
test.MX("miek.nl. 3600 IN MX 10 aspmx2.googlemail.com."),
|
||||
},
|
||||
},
|
||||
in: test.Case{
|
||||
Qname: "miek.nl.", Qtype: dns.TypeMX,
|
||||
Answer: []dns.RR{
|
||||
test.MX("miek.nl. 3601 IN MX 1 aspmx.l.google.com."),
|
||||
test.MX("miek.nl. 3601 IN MX 10 aspmx2.googlemail.com."),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
RecursionAvailable: true, AuthenticatedData: true, Authoritative: true,
|
||||
Case: test.Case{
|
||||
Qname: "mIEK.nL.", Qtype: dns.TypeMX,
|
||||
Answer: []dns.RR{
|
||||
test.MX("mIEK.nL. 3600 IN MX 1 aspmx.l.google.com."),
|
||||
test.MX("mIEK.nL. 3600 IN MX 10 aspmx2.googlemail.com."),
|
||||
},
|
||||
},
|
||||
in: test.Case{
|
||||
Qname: "mIEK.nL.", Qtype: dns.TypeMX,
|
||||
Answer: []dns.RR{
|
||||
test.MX("mIEK.nL. 3601 IN MX 1 aspmx.l.google.com."),
|
||||
test.MX("mIEK.nL. 3601 IN MX 10 aspmx2.googlemail.com."),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Truncated: true,
|
||||
Case: test.Case{
|
||||
Qname: "miek.nl.", Qtype: dns.TypeMX,
|
||||
Answer: []dns.RR{test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com.")},
|
||||
},
|
||||
in: test.Case{},
|
||||
},
|
||||
{
|
||||
RecursionAvailable: true, Authoritative: true,
|
||||
Case: test.Case{
|
||||
Rcode: dns.RcodeNameError,
|
||||
Qname: "example.org.", Qtype: dns.TypeA,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600"),
|
||||
},
|
||||
},
|
||||
in: test.Case{
|
||||
Rcode: dns.RcodeNameError,
|
||||
Qname: "example.org.", Qtype: dns.TypeA,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func cacheMsg(m *dns.Msg, tc cacheTestCase) *dns.Msg {
|
||||
m.RecursionAvailable = tc.RecursionAvailable
|
||||
m.AuthenticatedData = tc.AuthenticatedData
|
||||
m.Authoritative = tc.Authoritative
|
||||
m.Rcode = tc.Rcode
|
||||
m.Truncated = tc.Truncated
|
||||
m.Answer = tc.in.Answer
|
||||
m.Ns = tc.in.Ns
|
||||
// m.Extra = tc.in.Extra , not the OPT record!
|
||||
return m
|
||||
}
|
||||
|
||||
func newTestCache(ttl time.Duration) (*Cache, *ResponseWriter) {
|
||||
c := &Cache{Zones: []string{"."}, pcap: defaultCap, ncap: defaultCap, pttl: ttl, nttl: ttl}
|
||||
c.pcache, _ = lru.New(c.pcap)
|
||||
c.ncache, _ = lru.New(c.ncap)
|
||||
|
||||
crr := &ResponseWriter{nil, c}
|
||||
return c, crr
|
||||
}
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
c, crr := newTestCache(maxTTL)
|
||||
|
||||
log.SetOutput(ioutil.Discard)
|
||||
|
||||
for _, tc := range cacheTestCases {
|
||||
m := tc.in.Msg()
|
||||
m = cacheMsg(m, tc)
|
||||
do := tc.in.Do
|
||||
|
||||
mt, _ := response.Typify(m)
|
||||
k := key(m, mt, do)
|
||||
crr.set(m, k, mt, c.pttl)
|
||||
|
||||
name := middleware.Name(m.Question[0].Name).Normalize()
|
||||
qtype := m.Question[0].Qtype
|
||||
i, ok, _ := c.get(name, qtype, do)
|
||||
if ok && m.Truncated {
|
||||
t.Errorf("Truncated message should not have been cached")
|
||||
continue
|
||||
}
|
||||
|
||||
if ok {
|
||||
resp := i.toMsg(m)
|
||||
|
||||
if !test.Header(t, tc.Case, resp) {
|
||||
t.Logf("%v\n", resp)
|
||||
continue
|
||||
}
|
||||
|
||||
if !test.Section(t, tc.Case, test.Answer, resp.Answer) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc.Case, test.Ns, resp.Ns) {
|
||||
t.Logf("%v\n", resp)
|
||||
|
||||
}
|
||||
if !test.Section(t, tc.Case, test.Extra, resp.Extra) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ServeDNS implements the middleware.Handler interface.
|
||||
func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
|
||||
qname := state.Name()
|
||||
qtype := state.QType()
|
||||
zone := middleware.Zones(c.Zones).Matches(qname)
|
||||
if zone == "" {
|
||||
return c.Next.ServeDNS(ctx, w, r)
|
||||
}
|
||||
|
||||
do := state.Do() // TODO(): might need more from OPT record? Like the actual bufsize?
|
||||
|
||||
if i, ok, expired := c.get(qname, qtype, do); ok && !expired {
|
||||
resp := i.toMsg(r)
|
||||
state.SizeAndDo(resp)
|
||||
resp, _ = state.Scrub(resp)
|
||||
w.WriteMsg(resp)
|
||||
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
crr := &ResponseWriter{w, c}
|
||||
return middleware.NextOrFailure(c.Name(), c.Next, ctx, crr, r)
|
||||
}
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (c *Cache) Name() string { return "cache" }
|
||||
|
||||
func (c *Cache) get(qname string, qtype uint16, do bool) (*item, bool, bool) {
|
||||
k := rawKey(qname, qtype, do)
|
||||
|
||||
if i, ok := c.ncache.Get(k); ok {
|
||||
cacheHits.WithLabelValues(Denial).Inc()
|
||||
return i.(*item), ok, i.(*item).expired(time.Now())
|
||||
}
|
||||
|
||||
if i, ok := c.pcache.Get(k); ok {
|
||||
cacheHits.WithLabelValues(Success).Inc()
|
||||
return i.(*item), ok, i.(*item).expired(time.Now())
|
||||
}
|
||||
cacheMisses.Inc()
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
var (
|
||||
cacheSize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: middleware.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "size",
|
||||
Help: "The number of elements in the cache.",
|
||||
}, []string{"type"})
|
||||
|
||||
cacheCapacity = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: middleware.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "capacity",
|
||||
Help: "The cache's capacity.",
|
||||
}, []string{"type"})
|
||||
|
||||
cacheHits = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: middleware.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "hits_total",
|
||||
Help: "The count of cache hits.",
|
||||
}, []string{"type"})
|
||||
|
||||
cacheMisses = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: middleware.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "misses_total",
|
||||
Help: "The count of cache misses.",
|
||||
})
|
||||
)
|
||||
|
||||
const subsystem = "cache"
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(cacheSize)
|
||||
prometheus.MustRegister(cacheCapacity)
|
||||
prometheus.MustRegister(cacheHits)
|
||||
prometheus.MustRegister(cacheMisses)
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware/pkg/response"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type item struct {
|
||||
Rcode int
|
||||
Authoritative bool
|
||||
AuthenticatedData bool
|
||||
RecursionAvailable bool
|
||||
Answer []dns.RR
|
||||
Ns []dns.RR
|
||||
Extra []dns.RR
|
||||
|
||||
origTTL uint32
|
||||
stored time.Time
|
||||
}
|
||||
|
||||
func newItem(m *dns.Msg, d time.Duration) *item {
|
||||
i := new(item)
|
||||
i.Rcode = m.Rcode
|
||||
i.Authoritative = m.Authoritative
|
||||
i.AuthenticatedData = m.AuthenticatedData
|
||||
i.RecursionAvailable = m.RecursionAvailable
|
||||
i.Answer = m.Answer
|
||||
i.Ns = m.Ns
|
||||
i.Extra = make([]dns.RR, len(m.Extra))
|
||||
// Don't copy OPT record as these are hop-by-hop.
|
||||
j := 0
|
||||
for _, e := range m.Extra {
|
||||
if e.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
i.Extra[j] = e
|
||||
j++
|
||||
}
|
||||
i.Extra = i.Extra[:j]
|
||||
|
||||
i.origTTL = uint32(d.Seconds())
|
||||
i.stored = time.Now().UTC()
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// toMsg turns i into a message, it tailers the reply to m.
|
||||
// The Authoritative bit is always set to 0, because the answer is from the cache.
|
||||
func (i *item) toMsg(m *dns.Msg) *dns.Msg {
|
||||
m1 := new(dns.Msg)
|
||||
m1.SetReply(m)
|
||||
|
||||
m1.Authoritative = false
|
||||
m1.AuthenticatedData = i.AuthenticatedData
|
||||
m1.RecursionAvailable = i.RecursionAvailable
|
||||
m1.Rcode = i.Rcode
|
||||
m1.Compress = true
|
||||
|
||||
m1.Answer = i.Answer
|
||||
m1.Ns = i.Ns
|
||||
m1.Extra = i.Extra
|
||||
|
||||
ttl := int(i.origTTL) - int(time.Now().UTC().Sub(i.stored).Seconds())
|
||||
setMsgTTL(m1, uint32(ttl))
|
||||
return m1
|
||||
}
|
||||
|
||||
func (i *item) expired(now time.Time) bool {
|
||||
ttl := int(i.origTTL) - int(now.UTC().Sub(i.stored).Seconds())
|
||||
return ttl < 0
|
||||
}
|
||||
|
||||
// setMsgTTL sets the ttl on all RRs in all sections. If ttl is smaller than minTTL
|
||||
// that value is used.
|
||||
func setMsgTTL(m *dns.Msg, ttl uint32) {
|
||||
if ttl < minTTL {
|
||||
ttl = minTTL
|
||||
}
|
||||
|
||||
for _, r := range m.Answer {
|
||||
r.Header().Ttl = ttl
|
||||
}
|
||||
for _, r := range m.Ns {
|
||||
r.Header().Ttl = ttl
|
||||
}
|
||||
for _, r := range m.Extra {
|
||||
if r.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
r.Header().Ttl = ttl
|
||||
}
|
||||
}
|
||||
|
||||
func minMsgTTL(m *dns.Msg, mt response.Type) time.Duration {
|
||||
if mt != response.NoError && mt != response.NameError && mt != response.NoData {
|
||||
return 0
|
||||
}
|
||||
|
||||
minTTL := maxTTL
|
||||
for _, r := range append(m.Answer, m.Ns...) {
|
||||
switch mt {
|
||||
case response.NameError, response.NoData:
|
||||
if r.Header().Rrtype == dns.TypeSOA {
|
||||
return time.Duration(r.(*dns.SOA).Minttl) * time.Second
|
||||
}
|
||||
case response.NoError, response.Delegation:
|
||||
if r.Header().Ttl < uint32(minTTL.Seconds()) {
|
||||
minTTL = time.Duration(r.Header().Ttl) * time.Second
|
||||
}
|
||||
}
|
||||
}
|
||||
return minTTL
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestKey(t *testing.T) {
|
||||
if x := rawKey("miek.nl.", dns.TypeMX, false); x != "0miek.nl..15" {
|
||||
t.Errorf("failed to create correct key, got %s", x)
|
||||
}
|
||||
if x := rawKey("miek.nl.", dns.TypeMX, true); x != "1miek.nl..15" {
|
||||
t.Errorf("failed to create correct key, got %s", x)
|
||||
}
|
||||
// rawKey does not lowercase.
|
||||
if x := rawKey("miEK.nL.", dns.TypeMX, true); x != "1miEK.nL..15" {
|
||||
t.Errorf("failed to create correct key, got %s", x)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/middleware"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("cache", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
ca, err := cacheParse(c)
|
||||
if err != nil {
|
||||
return middleware.Error("cache", err)
|
||||
}
|
||||
dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler {
|
||||
ca.Next = next
|
||||
return ca
|
||||
})
|
||||
|
||||
// Export the capacity for the metrics. This only happens once, because this is a re-load change only.
|
||||
cacheCapacity.WithLabelValues(Success).Set(float64(ca.pcap))
|
||||
cacheCapacity.WithLabelValues(Denial).Set(float64(ca.ncap))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cacheParse(c *caddy.Controller) (*Cache, error) {
|
||||
|
||||
ca := &Cache{pcap: defaultCap, ncap: defaultCap, pttl: maxTTL, nttl: maxNTTL}
|
||||
|
||||
for c.Next() {
|
||||
// cache [ttl] [zones..]
|
||||
origins := make([]string, len(c.ServerBlockKeys))
|
||||
copy(origins, c.ServerBlockKeys)
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) > 0 {
|
||||
// first args may be just a number, then it is the ttl, if not it is a zone
|
||||
ttl, err := strconv.Atoi(args[0])
|
||||
if err == nil {
|
||||
// Reserve 0 (and smaller for future things)
|
||||
if ttl <= 0 {
|
||||
return nil, fmt.Errorf("cache TTL can not be zero or negative: %d", ttl)
|
||||
}
|
||||
ca.pttl = time.Duration(ttl) * time.Second
|
||||
ca.nttl = time.Duration(ttl) * time.Second
|
||||
args = args[1:]
|
||||
}
|
||||
if len(args) > 0 {
|
||||
copy(origins, args)
|
||||
}
|
||||
}
|
||||
|
||||
// Refinements? In an extra block.
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
// first number is cap, second is an new ttl
|
||||
case Success:
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
pcap, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ca.pcap = pcap
|
||||
if len(args) > 1 {
|
||||
pttl, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Reserve 0 (and smaller for future things)
|
||||
if pttl <= 0 {
|
||||
return nil, fmt.Errorf("cache TTL can not be zero or negative: %d", pttl)
|
||||
}
|
||||
ca.pttl = time.Duration(pttl) * time.Second
|
||||
}
|
||||
case Denial:
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
ncap, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ca.ncap = ncap
|
||||
if len(args) > 1 {
|
||||
nttl, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Reserve 0 (and smaller for future things)
|
||||
if nttl <= 0 {
|
||||
return nil, fmt.Errorf("cache TTL can not be zero or negative: %d", nttl)
|
||||
}
|
||||
ca.nttl = time.Duration(nttl) * time.Second
|
||||
}
|
||||
default:
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
for i := range origins {
|
||||
origins[i] = middleware.Host(origins[i]).Normalize()
|
||||
}
|
||||
|
||||
var err error
|
||||
ca.Zones = origins
|
||||
|
||||
ca.pcache, err = lru.New(ca.pcap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ca.ncache, err = lru.New(ca.ncap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ca, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedNcap int
|
||||
expectedPcap int
|
||||
expectedNttl time.Duration
|
||||
expectedPttl time.Duration
|
||||
}{
|
||||
{`cache`, false, defaultCap, defaultCap, maxNTTL, maxTTL},
|
||||
{`cache {}`, false, defaultCap, defaultCap, maxNTTL, maxTTL},
|
||||
{`cache example.nl {
|
||||
success 10
|
||||
}`, false, defaultCap, 10, maxNTTL, maxTTL},
|
||||
{`cache example.nl {
|
||||
success 10
|
||||
denial 10 15
|
||||
}`, false, 10, 10, 15 * time.Second, maxTTL},
|
||||
{`cache 25 example.nl {
|
||||
success 10
|
||||
denial 10 15
|
||||
}`, false, 10, 10, 15 * time.Second, 25 * time.Second},
|
||||
{`cache aaa example.nl`, false, defaultCap, defaultCap, maxNTTL, maxTTL},
|
||||
|
||||
// fails
|
||||
{`cache example.nl {
|
||||
success
|
||||
denial 10 15
|
||||
}`, true, defaultCap, defaultCap, maxTTL, maxTTL},
|
||||
{`cache example.nl {
|
||||
success 15
|
||||
denial aaa
|
||||
}`, true, defaultCap, defaultCap, maxTTL, maxTTL},
|
||||
{`cache example.nl {
|
||||
positive 15
|
||||
negative aaa
|
||||
}`, true, defaultCap, defaultCap, maxTTL, maxTTL},
|
||||
{`cache 0 example.nl`, true, defaultCap, defaultCap, maxTTL, maxTTL},
|
||||
{`cache -1 example.nl`, true, defaultCap, defaultCap, maxTTL, maxTTL},
|
||||
{`cache 1 example.nl {
|
||||
positive 0
|
||||
}`, true, defaultCap, defaultCap, maxTTL, maxTTL},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.input)
|
||||
ca, err := cacheParse(c)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %v: Expected error but found nil", i)
|
||||
continue
|
||||
} else if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
|
||||
continue
|
||||
}
|
||||
if test.shouldErr && err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if ca.ncap != test.expectedNcap {
|
||||
t.Errorf("Test %v: Expected ncap %v but found: %v", i, test.expectedNcap, ca.ncap)
|
||||
}
|
||||
if ca.pcap != test.expectedPcap {
|
||||
t.Errorf("Test %v: Expected pcap %v but found: %v", i, test.expectedPcap, ca.pcap)
|
||||
}
|
||||
if ca.nttl != test.expectedNttl {
|
||||
t.Errorf("Test %v: Expected nttl %v but found: %v", i, test.expectedNttl, ca.nttl)
|
||||
}
|
||||
if ca.pttl != test.expectedPttl {
|
||||
t.Errorf("Test %v: Expected pttl %v but found: %v", i, test.expectedPttl, ca.pttl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# chaos
|
||||
|
||||
The *chaos* middleware allows CoreDNS to respond to TXT queries in the CH class.
|
||||
This is useful for retrieving version or author information from the server.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
chaos [VERSION] [AUTHORS...]
|
||||
~~~
|
||||
|
||||
* **VERSION** is the version to return. Defaults to `CoreDNS-<version>`, if not set.
|
||||
* **AUTHORS** is what authors to return. No default.
|
||||
|
||||
Note that you have to make sure that this middleware will get actual queries for the
|
||||
following zones: `version.bind`, `version.server`, `authors.bind`, `hostname.bind` and
|
||||
`id.server`.
|
||||
|
||||
## Examples
|
||||
|
||||
~~~
|
||||
chaos CoreDNS-001 "Miek Gieben" miek@miek.nl
|
||||
~~~
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// Package chaos implements a middleware that answer to 'CH version.bind TXT' type queries.
|
||||
package chaos
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Chaos allows CoreDNS to reply to CH TXT queries and return author or
|
||||
// version information.
|
||||
type Chaos struct {
|
||||
Next middleware.Handler
|
||||
Version string
|
||||
Authors map[string]bool
|
||||
}
|
||||
|
||||
// ServeDNS implements the middleware.Handler interface.
|
||||
func (c Chaos) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
if state.QClass() != dns.ClassCHAOS || state.QType() != dns.TypeTXT {
|
||||
return middleware.NextOrFailure(c.Name(), c.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
|
||||
hdr := dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeTXT, Class: dns.ClassCHAOS, Ttl: 0}
|
||||
switch state.Name() {
|
||||
default:
|
||||
return c.Next.ServeDNS(ctx, w, r)
|
||||
case "authors.bind.":
|
||||
for a := range c.Authors {
|
||||
m.Answer = append(m.Answer, &dns.TXT{Hdr: hdr, Txt: []string{trim(a)}})
|
||||
}
|
||||
case "version.bind.", "version.server.":
|
||||
m.Answer = []dns.RR{&dns.TXT{Hdr: hdr, Txt: []string{trim(c.Version)}}}
|
||||
case "hostname.bind.", "id.server.":
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
hostname = "localhost"
|
||||
}
|
||||
m.Answer = []dns.RR{&dns.TXT{Hdr: hdr, Txt: []string{trim(hostname)}}}
|
||||
}
|
||||
state.SizeAndDo(m)
|
||||
w.WriteMsg(m)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (c Chaos) Name() string { return "chaos" }
|
||||
|
||||
func trim(s string) string {
|
||||
if len(s) < 256 {
|
||||
return s
|
||||
}
|
||||
return s[:255]
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package chaos
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/middleware/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestChaos(t *testing.T) {
|
||||
em := Chaos{
|
||||
Version: version,
|
||||
Authors: map[string]bool{"Miek Gieben": true},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
next middleware.Handler
|
||||
qname string
|
||||
qtype uint16
|
||||
expectedCode int
|
||||
expectedReply string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
next: test.NextHandler(dns.RcodeSuccess, nil),
|
||||
qname: "version.bind",
|
||||
expectedCode: dns.RcodeSuccess,
|
||||
expectedReply: version,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
next: test.NextHandler(dns.RcodeSuccess, nil),
|
||||
qname: "authors.bind",
|
||||
expectedCode: dns.RcodeSuccess,
|
||||
expectedReply: "Miek Gieben",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
next: test.NextHandler(dns.RcodeSuccess, nil),
|
||||
qname: "authors.bind",
|
||||
qtype: dns.TypeSRV,
|
||||
expectedCode: dns.RcodeSuccess,
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
for i, tc := range tests {
|
||||
req := new(dns.Msg)
|
||||
if tc.qtype == 0 {
|
||||
tc.qtype = dns.TypeTXT
|
||||
}
|
||||
req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype)
|
||||
req.Question[0].Qclass = dns.ClassCHAOS
|
||||
em.Next = tc.next
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
code, err := em.ServeDNS(ctx, rec, req)
|
||||
|
||||
if err != tc.expectedErr {
|
||||
t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expectedErr, err)
|
||||
}
|
||||
if code != int(tc.expectedCode) {
|
||||
t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code)
|
||||
}
|
||||
if tc.expectedReply != "" {
|
||||
answer := rec.Msg.Answer[0].(*dns.TXT).Txt[0]
|
||||
if answer != tc.expectedReply {
|
||||
t.Errorf("Test %d: Expected answer %s, but got %s", i, tc.expectedReply, answer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const version = "CoreDNS-001"
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package chaos
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/middleware"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("chaos", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
version, authors, err := chaosParse(c)
|
||||
if err != nil {
|
||||
return middleware.Error("chaos", err)
|
||||
}
|
||||
|
||||
dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler {
|
||||
return Chaos{Next: next, Version: version, Authors: authors}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func chaosParse(c *caddy.Controller) (string, map[string]bool, error) {
|
||||
version := ""
|
||||
authors := make(map[string]bool)
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return defaultVersion, nil, nil
|
||||
}
|
||||
if len(args) == 1 {
|
||||
return args[0], nil, nil
|
||||
}
|
||||
version = args[0]
|
||||
for _, a := range args[1:] {
|
||||
authors[a] = true
|
||||
}
|
||||
return version, authors, nil
|
||||
}
|
||||
return version, authors, nil
|
||||
}
|
||||
|
||||
var defaultVersion = caddy.AppName + "-" + caddy.AppVersion
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package chaos
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestSetupChaos(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedVersion string // expected version.
|
||||
expectedAuthor string // expected author (string, although we get a map).
|
||||
expectedErrContent string // substring from the expected error. Empty for positive cases.
|
||||
}{
|
||||
// positive
|
||||
{
|
||||
`chaos`, false, defaultVersion, "", "",
|
||||
},
|
||||
{
|
||||
`chaos v2`, false, "v2", "", "",
|
||||
},
|
||||
{
|
||||
`chaos v3 "Miek Gieben"`, false, "v3", "Miek Gieben", "",
|
||||
},
|
||||
{
|
||||
fmt.Sprintf(`chaos {
|
||||
%s
|
||||
}`, defaultVersion), false, defaultVersion, "", "",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.input)
|
||||
version, authors, err := chaosParse(c)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if !test.shouldErr {
|
||||
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), test.expectedErrContent) {
|
||||
t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input)
|
||||
}
|
||||
}
|
||||
|
||||
if !test.shouldErr && version != test.expectedVersion {
|
||||
t.Errorf("Chaos not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedVersion, version)
|
||||
}
|
||||
if !test.shouldErr && authors != nil {
|
||||
if _, ok := authors[test.expectedAuthor]; !ok {
|
||||
t.Errorf("Chaos not correctly set for input %s. Expected: '%s', actual: '%s'", test.input, test.expectedAuthor, "Miek Gieben")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# dnssec
|
||||
|
||||
*dnssec* enables on-the-fly DNSSEC signing of served data.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
dnssec [ZONES...]
|
||||
~~~
|
||||
|
||||
* **ZONES** zones that should be signed. If empty, the zones from the configuration block
|
||||
are used.
|
||||
|
||||
If keys are not specified (see below), a key is generated and used for all signing operations. The
|
||||
DNSSEC signing will treat this key a CSK (common signing key), forgoing the ZSK/KSK split. All
|
||||
signing operations are done online. Authenticated denial of existence is implemented with NSEC black
|
||||
lies. Using ECDSA as an algorithm is preferred as this leads to smaller signatures (compared to
|
||||
RSA). NSEC3 is *not* supported.
|
||||
|
||||
A single signing key can be specified by using the `key` directive.
|
||||
|
||||
NOTE: Key generation has not been implemented yet.
|
||||
|
||||
TODO(miek): think about key rollovers, and how to do them automatically.
|
||||
|
||||
~~~
|
||||
dnssec [ZONES... ] {
|
||||
key file KEY...
|
||||
cache_capacity CAPACITY
|
||||
}
|
||||
~~~
|
||||
|
||||
* `key file` indicates that key file(s) should be read from disk. When multiple keys are specified, RRsets
|
||||
will be signed with all keys. Generating a key can be done with `dnssec-keygen`: `dnssec-keygen -a
|
||||
ECDSAP256SHA256 <zonename>`. A key created for zone *A* can be safely used for zone *B*.
|
||||
|
||||
* `cache_capacity` indicates the capacity of the LRU cache. The dnssec middleware uses LRU cache to manage
|
||||
objects and the default capacity is 10000.
|
||||
|
||||
## Metrics
|
||||
|
||||
If monitoring is enabled (via the *prometheus* directive) then the following metrics are exported:
|
||||
|
||||
* coredns_dnssec_cache_size{type} - total elements in the cache, type is "signature".
|
||||
* coredns_dnssec_cache_capacity{type} - total capacity of the cache, type is "signature".
|
||||
* coredns_dnssec_cache_hits_total - Counter of cache hits.
|
||||
* coredns_dnssec_cache_misses_total - Counter of cache misses.
|
||||
|
||||
## Examples
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package dnssec
|
||||
|
||||
import "github.com/miekg/dns"
|
||||
|
||||
// nsec returns an NSEC useful for NXDOMAIN respsones.
|
||||
// See https://tools.ietf.org/html/draft-valsorda-dnsop-black-lies-00
|
||||
// For example, a request for the non-existing name a.example.com would
|
||||
// cause the following NSEC record to be generated:
|
||||
// a.example.com. 3600 IN NSEC \000.a.example.com. ( RRSIG NSEC )
|
||||
// This inturn makes every NXDOMAIN answer a NODATA one, don't forget to flip
|
||||
// the header rcode to NOERROR.
|
||||
func (d Dnssec) nsec(name, zone string, ttl, incep, expir uint32) ([]dns.RR, error) {
|
||||
nsec := &dns.NSEC{}
|
||||
nsec.Hdr = dns.RR_Header{Name: name, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeNSEC}
|
||||
nsec.NextDomain = "\\000." + name
|
||||
nsec.TypeBitMap = []uint16{dns.TypeRRSIG, dns.TypeNSEC}
|
||||
|
||||
sigs, err := d.sign([]dns.RR{nsec}, zone, ttl, incep, expir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(sigs, nsec), nil
|
||||
}
|
||||
49
vendor/github.com/miekg/coredns/middleware/dnssec/black_lies_test.go
generated
vendored
Normal file
49
vendor/github.com/miekg/coredns/middleware/dnssec/black_lies_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package dnssec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestZoneSigningBlackLies(t *testing.T) {
|
||||
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
|
||||
defer rm1()
|
||||
defer rm2()
|
||||
|
||||
m := testNxdomainMsg()
|
||||
state := request.Request{Req: m}
|
||||
m = d.Sign(state, "miek.nl.", time.Now().UTC())
|
||||
if !section(m.Ns, 2) {
|
||||
t.Errorf("authority section should have 2 sig")
|
||||
}
|
||||
var nsec *dns.NSEC
|
||||
for _, r := range m.Ns {
|
||||
if r.Header().Rrtype == dns.TypeNSEC {
|
||||
nsec = r.(*dns.NSEC)
|
||||
}
|
||||
}
|
||||
if m.Rcode != dns.RcodeSuccess {
|
||||
t.Errorf("expected rcode %d, got %d", dns.RcodeSuccess, m.Rcode)
|
||||
}
|
||||
if nsec == nil {
|
||||
t.Fatalf("expected NSEC, got none")
|
||||
}
|
||||
if nsec.Hdr.Name != "ww.miek.nl." {
|
||||
t.Errorf("expected %s, got %s", "ww.miek.nl.", nsec.Hdr.Name)
|
||||
}
|
||||
if nsec.NextDomain != "\\000.ww.miek.nl." {
|
||||
t.Errorf("expected %s, got %s", "\\000.ww.miek.nl.", nsec.NextDomain)
|
||||
}
|
||||
}
|
||||
|
||||
func testNxdomainMsg() *dns.Msg {
|
||||
return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeNameError},
|
||||
Question: []dns.Question{{Name: "ww.miek.nl.", Qclass: dns.ClassINET, Qtype: dns.TypeTXT}},
|
||||
Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1461471181 14400 3600 604800 14400")},
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package dnssec
|
||||
|
||||
import (
|
||||
"hash/fnv"
|
||||
"strconv"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Key serializes the RRset and return a signature cache key.
|
||||
func key(rrs []dns.RR) string {
|
||||
h := fnv.New64()
|
||||
buf := make([]byte, 256)
|
||||
for _, r := range rrs {
|
||||
off, err := dns.PackRR(r, buf, 0, nil, false)
|
||||
if err == nil {
|
||||
h.Write(buf[:off])
|
||||
}
|
||||
}
|
||||
|
||||
i := h.Sum64()
|
||||
return strconv.FormatUint(i, 10)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package dnssec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
)
|
||||
|
||||
func TestCacheSet(t *testing.T) {
|
||||
fPriv, rmPriv, _ := test.TempFile(".", privKey)
|
||||
fPub, rmPub, _ := test.TempFile(".", pubKey)
|
||||
defer rmPriv()
|
||||
defer rmPub()
|
||||
|
||||
dnskey, err := ParseKeyFile(fPub, fPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse key: %v\n", err)
|
||||
}
|
||||
|
||||
cache, _ := lru.New(defaultCap)
|
||||
m := testMsg()
|
||||
state := request.Request{Req: m}
|
||||
k := key(m.Answer) // calculate *before* we add the sig
|
||||
d := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, nil, cache)
|
||||
m = d.Sign(state, "miek.nl.", time.Now().UTC())
|
||||
|
||||
_, ok := d.get(k)
|
||||
if !ok {
|
||||
t.Errorf("signature was not added to the cache")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package dnssec
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// DNSKEY holds a DNSSEC public and private key used for on-the-fly signing.
|
||||
type DNSKEY struct {
|
||||
K *dns.DNSKEY
|
||||
s crypto.Signer
|
||||
keytag uint16
|
||||
}
|
||||
|
||||
// ParseKeyFile read a DNSSEC keyfile as generated by dnssec-keygen or other
|
||||
// utilities. It adds ".key" for the public key and ".private" for the private key.
|
||||
func ParseKeyFile(pubFile, privFile string) (*DNSKEY, error) {
|
||||
f, e := os.Open(pubFile)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
k, e := dns.ReadRR(f, pubFile)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
f, e = os.Open(privFile)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
p, e := k.(*dns.DNSKEY).ReadPrivateKey(f, privFile)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
if v, ok := p.(*rsa.PrivateKey); ok {
|
||||
return &DNSKEY{k.(*dns.DNSKEY), v, k.(*dns.DNSKEY).KeyTag()}, nil
|
||||
}
|
||||
if v, ok := p.(*ecdsa.PrivateKey); ok {
|
||||
return &DNSKEY{k.(*dns.DNSKEY), v, k.(*dns.DNSKEY).KeyTag()}, nil
|
||||
}
|
||||
return &DNSKEY{k.(*dns.DNSKEY), nil, 0}, errors.New("no known? private key found")
|
||||
}
|
||||
|
||||
// getDNSKEY returns the correct DNSKEY to the client. Signatures are added when do is true.
|
||||
func (d Dnssec) getDNSKEY(state request.Request, zone string, do bool) *dns.Msg {
|
||||
keys := make([]dns.RR, len(d.keys))
|
||||
for i, k := range d.keys {
|
||||
keys[i] = dns.Copy(k.K)
|
||||
keys[i].Header().Name = zone
|
||||
}
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(state.Req)
|
||||
m.Answer = keys
|
||||
if !do {
|
||||
return m
|
||||
}
|
||||
|
||||
incep, expir := incepExpir(time.Now().UTC())
|
||||
if sigs, err := d.sign(keys, zone, 3600, incep, expir); err == nil {
|
||||
m.Answer = append(m.Answer, sigs...)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
// Package dnssec implements a middleware that signs responses on-the-fly using
|
||||
// NSEC black lies.
|
||||
package dnssec
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/middleware/pkg/response"
|
||||
"github.com/coredns/coredns/middleware/pkg/singleflight"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Dnssec signs the reply on-the-fly.
|
||||
type Dnssec struct {
|
||||
Next middleware.Handler
|
||||
|
||||
zones []string
|
||||
keys []*DNSKEY
|
||||
inflight *singleflight.Group
|
||||
cache *lru.Cache
|
||||
}
|
||||
|
||||
// New returns a new Dnssec.
|
||||
func New(zones []string, keys []*DNSKEY, next middleware.Handler, cache *lru.Cache) Dnssec {
|
||||
return Dnssec{Next: next,
|
||||
zones: zones,
|
||||
keys: keys,
|
||||
cache: cache,
|
||||
inflight: new(singleflight.Group),
|
||||
}
|
||||
}
|
||||
|
||||
// Sign signs the message in state. it takes care of negative or nodata responses. It
|
||||
// uses NSEC black lies for authenticated denial of existence. Signatures
|
||||
// creates will be cached for a short while. By default we sign for 8 days,
|
||||
// starting 3 hours ago.
|
||||
func (d Dnssec) Sign(state request.Request, zone string, now time.Time) *dns.Msg {
|
||||
req := state.Req
|
||||
|
||||
mt, _ := response.Typify(req) // TODO(miek): need opt record here?
|
||||
if mt == response.Delegation {
|
||||
return req
|
||||
}
|
||||
|
||||
incep, expir := incepExpir(now)
|
||||
|
||||
if mt == response.NameError {
|
||||
if req.Ns[0].Header().Rrtype != dns.TypeSOA || len(req.Ns) > 1 {
|
||||
return req
|
||||
}
|
||||
|
||||
ttl := req.Ns[0].Header().Ttl
|
||||
|
||||
if sigs, err := d.sign(req.Ns, zone, ttl, incep, expir); err == nil {
|
||||
req.Ns = append(req.Ns, sigs...)
|
||||
}
|
||||
if sigs, err := d.nsec(state.Name(), zone, ttl, incep, expir); err == nil {
|
||||
req.Ns = append(req.Ns, sigs...)
|
||||
}
|
||||
if len(req.Ns) > 1 { // actually added nsec and sigs, reset the rcode
|
||||
req.Rcode = dns.RcodeSuccess
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
for _, r := range rrSets(req.Answer) {
|
||||
ttl := r[0].Header().Ttl
|
||||
if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil {
|
||||
req.Answer = append(req.Answer, sigs...)
|
||||
}
|
||||
}
|
||||
for _, r := range rrSets(req.Ns) {
|
||||
ttl := r[0].Header().Ttl
|
||||
if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil {
|
||||
req.Ns = append(req.Ns, sigs...)
|
||||
}
|
||||
}
|
||||
for _, r := range rrSets(req.Extra) {
|
||||
ttl := r[0].Header().Ttl
|
||||
if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil {
|
||||
req.Extra = append(sigs, req.Extra...) // prepend to leave OPT alone
|
||||
}
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func (d Dnssec) sign(rrs []dns.RR, signerName string, ttl, incep, expir uint32) ([]dns.RR, error) {
|
||||
k := key(rrs)
|
||||
sgs, ok := d.get(k)
|
||||
if ok {
|
||||
return sgs, nil
|
||||
}
|
||||
|
||||
sigs, err := d.inflight.Do(k, func() (interface{}, error) {
|
||||
sigs := make([]dns.RR, len(d.keys))
|
||||
var e error
|
||||
for i, k := range d.keys {
|
||||
sig := k.newRRSIG(signerName, ttl, incep, expir)
|
||||
e = sig.Sign(k.s, rrs)
|
||||
sigs[i] = sig
|
||||
}
|
||||
d.set(k, sigs)
|
||||
return sigs, e
|
||||
})
|
||||
return sigs.([]dns.RR), err
|
||||
}
|
||||
|
||||
func (d Dnssec) set(key string, sigs []dns.RR) {
|
||||
d.cache.Add(key, sigs)
|
||||
}
|
||||
|
||||
func (d Dnssec) get(key string) ([]dns.RR, bool) {
|
||||
if s, ok := d.cache.Get(key); ok {
|
||||
cacheHits.Inc()
|
||||
return s.([]dns.RR), true
|
||||
}
|
||||
cacheMisses.Inc()
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func incepExpir(now time.Time) (uint32, uint32) {
|
||||
incep := uint32(now.Add(-3 * time.Hour).Unix()) // -(2+1) hours, be sure to catch daylight saving time and such
|
||||
expir := uint32(now.Add(eightDays).Unix()) // sign for 8 days
|
||||
return incep, expir
|
||||
}
|
||||
|
||||
const (
|
||||
eightDays = 8 * 24 * time.Hour
|
||||
defaultCap = 10000 // default capacity of the cache.
|
||||
)
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package dnssec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestZoneSigning(t *testing.T) {
|
||||
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
|
||||
defer rm1()
|
||||
defer rm2()
|
||||
|
||||
m := testMsg()
|
||||
state := request.Request{Req: m}
|
||||
|
||||
m = d.Sign(state, "miek.nl.", time.Now().UTC())
|
||||
if !section(m.Answer, 1) {
|
||||
t.Errorf("answer section should have 1 sig")
|
||||
}
|
||||
if !section(m.Ns, 1) {
|
||||
t.Errorf("authority section should have 1 sig")
|
||||
}
|
||||
}
|
||||
|
||||
func TestZoneSigningDouble(t *testing.T) {
|
||||
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
|
||||
defer rm1()
|
||||
defer rm2()
|
||||
|
||||
fPriv1, rmPriv1, _ := test.TempFile(".", privKey1)
|
||||
fPub1, rmPub1, _ := test.TempFile(".", pubKey1)
|
||||
defer rmPriv1()
|
||||
defer rmPub1()
|
||||
|
||||
key1, err := ParseKeyFile(fPub1, fPriv1)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse key: %v\n", err)
|
||||
}
|
||||
d.keys = append(d.keys, key1)
|
||||
|
||||
m := testMsg()
|
||||
state := request.Request{Req: m}
|
||||
m = d.Sign(state, "miek.nl.", time.Now().UTC())
|
||||
if !section(m.Answer, 2) {
|
||||
t.Errorf("answer section should have 1 sig")
|
||||
}
|
||||
if !section(m.Ns, 2) {
|
||||
t.Errorf("authority section should have 1 sig")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSigningDifferentZone tests if a key for miek.nl and be used for example.org.
|
||||
func TestSigningDifferentZone(t *testing.T) {
|
||||
fPriv, rmPriv, _ := test.TempFile(".", privKey)
|
||||
fPub, rmPub, _ := test.TempFile(".", pubKey)
|
||||
defer rmPriv()
|
||||
defer rmPub()
|
||||
|
||||
key, err := ParseKeyFile(fPub, fPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse key: %v\n", err)
|
||||
}
|
||||
|
||||
m := testMsgEx()
|
||||
state := request.Request{Req: m}
|
||||
cache, _ := lru.New(defaultCap)
|
||||
d := New([]string{"example.org."}, []*DNSKEY{key}, nil, cache)
|
||||
m = d.Sign(state, "example.org.", time.Now().UTC())
|
||||
if !section(m.Answer, 1) {
|
||||
t.Errorf("answer section should have 1 sig")
|
||||
t.Logf("%+v\n", m)
|
||||
}
|
||||
if !section(m.Ns, 1) {
|
||||
t.Errorf("authority section should have 1 sig")
|
||||
t.Logf("%+v\n", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigningCname(t *testing.T) {
|
||||
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
|
||||
defer rm1()
|
||||
defer rm2()
|
||||
|
||||
m := testMsgCname()
|
||||
state := request.Request{Req: m}
|
||||
m = d.Sign(state, "miek.nl.", time.Now().UTC())
|
||||
if !section(m.Answer, 1) {
|
||||
t.Errorf("answer section should have 1 sig")
|
||||
}
|
||||
}
|
||||
|
||||
func TestZoneSigningDelegation(t *testing.T) {
|
||||
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
|
||||
defer rm1()
|
||||
defer rm2()
|
||||
|
||||
m := testDelegationMsg()
|
||||
state := request.Request{Req: m}
|
||||
m = d.Sign(state, "miek.nl.", time.Now().UTC())
|
||||
if !section(m.Ns, 0) {
|
||||
t.Errorf("authority section should have 0 sig")
|
||||
t.Logf("%v\n", m)
|
||||
}
|
||||
if !section(m.Extra, 0) {
|
||||
t.Errorf("answer section should have 0 sig")
|
||||
t.Logf("%v\n", m)
|
||||
}
|
||||
}
|
||||
|
||||
func section(rss []dns.RR, nrSigs int) bool {
|
||||
i := 0
|
||||
for _, r := range rss {
|
||||
if r.Header().Rrtype == dns.TypeRRSIG {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return nrSigs == i
|
||||
}
|
||||
|
||||
func testMsg() *dns.Msg {
|
||||
// don't care about the message header
|
||||
return &dns.Msg{
|
||||
Answer: []dns.RR{test.MX("miek.nl. 1703 IN MX 1 aspmx.l.google.com.")},
|
||||
Ns: []dns.RR{test.NS("miek.nl. 1703 IN NS omval.tednet.nl.")},
|
||||
}
|
||||
}
|
||||
func testMsgEx() *dns.Msg {
|
||||
return &dns.Msg{
|
||||
Answer: []dns.RR{test.MX("example.org. 1703 IN MX 1 aspmx.l.google.com.")},
|
||||
Ns: []dns.RR{test.NS("example.org. 1703 IN NS omval.tednet.nl.")},
|
||||
}
|
||||
}
|
||||
|
||||
func testMsgCname() *dns.Msg {
|
||||
return &dns.Msg{
|
||||
Answer: []dns.RR{test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl.")},
|
||||
}
|
||||
}
|
||||
|
||||
func testDelegationMsg() *dns.Msg {
|
||||
return &dns.Msg{
|
||||
Ns: []dns.RR{
|
||||
test.NS("miek.nl. 3600 IN NS linode.atoom.net."),
|
||||
test.NS("miek.nl. 3600 IN NS ns-ext.nlnetlabs.nl."),
|
||||
test.NS("miek.nl. 3600 IN NS omval.tednet.nl."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("omval.tednet.nl. 3600 IN A 185.49.141.42"),
|
||||
test.AAAA("omval.tednet.nl. 3600 IN AAAA 2a04:b900:0:100::42"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) {
|
||||
k, rm1, rm2 := newKey(t)
|
||||
cache, _ := lru.New(defaultCap)
|
||||
d := New(zones, []*DNSKEY{k}, nil, cache)
|
||||
return d, rm1, rm2
|
||||
}
|
||||
|
||||
func newKey(t *testing.T) (*DNSKEY, func(), func()) {
|
||||
fPriv, rmPriv, _ := test.TempFile(".", privKey)
|
||||
fPub, rmPub, _ := test.TempFile(".", pubKey)
|
||||
|
||||
key, err := ParseKeyFile(fPub, fPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse key: %v\n", err)
|
||||
}
|
||||
return key, rmPriv, rmPub
|
||||
}
|
||||
|
||||
const (
|
||||
pubKey = `miek.nl. IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4BXPP3gwhetiOUMnGA+x09nqzgF5IY OyjWB7N3rXqQbnOSILhH1hnuyh7mmA==`
|
||||
privKey = `Private-key-format: v1.3
|
||||
Algorithm: 13 (ECDSAP256SHA256)
|
||||
PrivateKey: /4BZk8AFvyW5hL3cOLSVxIp1RTqHSAEloWUxj86p3gs=
|
||||
Created: 20160423195532
|
||||
Publish: 20160423195532
|
||||
Activate: 20160423195532
|
||||
`
|
||||
pubKey1 = `example.org. IN DNSKEY 257 3 13 tVRWNSGpHZbCi7Pr7OmbADVUO3MxJ0Lb8Lk3o/HBHqCxf5K/J50lFqRa 98lkdAIiFOVRy8LyMvjwmxZKwB5MNw==`
|
||||
privKey1 = `Private-key-format: v1.3
|
||||
Algorithm: 13 (ECDSAP256SHA256)
|
||||
PrivateKey: i8j4OfDGT8CQt24SDwLz2hg9yx4qKOEOh1LvbAuSp1c=
|
||||
Created: 20160423211746
|
||||
Publish: 20160423211746
|
||||
Activate: 20160423211746
|
||||
`
|
||||
)
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package dnssec
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ServeDNS implements the middleware.Handler interface.
|
||||
func (d Dnssec) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
|
||||
do := state.Do()
|
||||
qname := state.Name()
|
||||
qtype := state.QType()
|
||||
zone := middleware.Zones(d.zones).Matches(qname)
|
||||
if zone == "" {
|
||||
return middleware.NextOrFailure(d.Name(), d.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
// Intercept queries for DNSKEY, but only if one of the zones matches the qname, otherwise we let
|
||||
// the query through.
|
||||
if qtype == dns.TypeDNSKEY {
|
||||
for _, z := range d.zones {
|
||||
if qname == z {
|
||||
resp := d.getDNSKEY(state, z, do)
|
||||
resp.Authoritative = true
|
||||
state.SizeAndDo(resp)
|
||||
w.WriteMsg(resp)
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drr := &ResponseWriter{w, d}
|
||||
return middleware.NextOrFailure(d.Name(), d.Next, ctx, drr, r)
|
||||
}
|
||||
|
||||
var (
|
||||
cacheSize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: middleware.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "cache_size",
|
||||
Help: "The number of elements in the dnssec cache.",
|
||||
}, []string{"type"})
|
||||
|
||||
cacheCapacity = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: middleware.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "cache_capacity",
|
||||
Help: "The dnssec cache's capacity.",
|
||||
}, []string{"type"})
|
||||
|
||||
cacheHits = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: middleware.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "cache_hits_total",
|
||||
Help: "The count of cache hits.",
|
||||
})
|
||||
|
||||
cacheMisses = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: middleware.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "cache_misses_total",
|
||||
Help: "The count of cache misses.",
|
||||
})
|
||||
)
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (d Dnssec) Name() string { return "dnssec" }
|
||||
|
||||
const subsystem = "dnssec"
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(cacheSize)
|
||||
prometheus.MustRegister(cacheCapacity)
|
||||
prometheus.MustRegister(cacheHits)
|
||||
prometheus.MustRegister(cacheMisses)
|
||||
}
|
||||
188
vendor/github.com/miekg/coredns/middleware/dnssec/handler_test.go
generated
vendored
Normal file
188
vendor/github.com/miekg/coredns/middleware/dnssec/handler_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
package dnssec
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/middleware/file"
|
||||
"github.com/coredns/coredns/middleware/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var dnssecTestCases = []test.Case{
|
||||
{
|
||||
Qname: "miek.nl.", Qtype: dns.TypeDNSKEY,
|
||||
Answer: []dns.RR{
|
||||
test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "miek.nl.", Qtype: dns.TypeDNSKEY, Do: true,
|
||||
Answer: []dns.RR{
|
||||
test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"),
|
||||
test.RRSIG("miek.nl. 3600 IN RRSIG DNSKEY 13 2 3600 20160503150844 20160425120844 18512 miek.nl. Iw/kNOyM"),
|
||||
},
|
||||
Extra: []dns.RR{test.OPT(4096, true)},
|
||||
},
|
||||
}
|
||||
|
||||
var dnsTestCases = []test.Case{
|
||||
{
|
||||
Qname: "miek.nl.", Qtype: dns.TypeDNSKEY,
|
||||
Answer: []dns.RR{
|
||||
test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "miek.nl.", Qtype: dns.TypeMX,
|
||||
Answer: []dns.RR{
|
||||
test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."),
|
||||
},
|
||||
Ns: []dns.RR{
|
||||
test.NS("miek.nl. 1800 IN NS linode.atoom.net."),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "miek.nl.", Qtype: dns.TypeMX, Do: true,
|
||||
Answer: []dns.RR{
|
||||
test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."),
|
||||
test.RRSIG("miek.nl. 1800 IN RRSIG MX 13 2 3600 20160503192428 20160425162428 18512 miek.nl. 4nxuGKitXjPVA9zP1JIUvA09"),
|
||||
},
|
||||
Ns: []dns.RR{
|
||||
test.NS("miek.nl. 1800 IN NS linode.atoom.net."),
|
||||
test.RRSIG("miek.nl. 1800 IN RRSIG NS 13 2 3600 20161217114912 20161209084912 18512 miek.nl. ad9gA8VWgF1H8ze9/0Rk2Q=="),
|
||||
},
|
||||
Extra: []dns.RR{test.OPT(4096, true)},
|
||||
},
|
||||
{
|
||||
Qname: "www.miek.nl.", Qtype: dns.TypeAAAA, Do: true,
|
||||
Answer: []dns.RR{
|
||||
test.AAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
|
||||
test.RRSIG("a.miek.nl. 1800 IN RRSIG AAAA 13 3 3600 20160503193047 20160425163047 18512 miek.nl. UAyMG+gcnoXW3"),
|
||||
test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."),
|
||||
test.RRSIG("www.miek.nl. 1800 IN RRSIG CNAME 13 3 3600 20160503193047 20160425163047 18512 miek.nl. E3qGZn"),
|
||||
},
|
||||
Ns: []dns.RR{
|
||||
test.NS("miek.nl. 1800 IN NS linode.atoom.net."),
|
||||
test.RRSIG("miek.nl. 1800 IN RRSIG NS 13 2 3600 20161217114912 20161209084912 18512 miek.nl. ad9gA8VWgF1H8ze9/0Rk2Q=="),
|
||||
},
|
||||
Extra: []dns.RR{test.OPT(4096, true)},
|
||||
},
|
||||
{
|
||||
Qname: "www.example.org.", Qtype: dns.TypeAAAA, Do: true,
|
||||
Rcode: dns.RcodeServerFailure,
|
||||
// Extra: []dns.RR{test.OPT(4096, true)}, // test.ErrorHandler is a simple handler that does not do EDNS.
|
||||
},
|
||||
}
|
||||
|
||||
func TestLookupZone(t *testing.T) {
|
||||
zone, err := file.Parse(strings.NewReader(dbMiekNL), "miek.nl.", "stdin")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fm := file.File{Next: test.ErrorHandler(), Zones: file.Zones{Z: map[string]*file.Zone{"miek.nl.": zone}, Names: []string{"miek.nl."}}}
|
||||
dnskey, rm1, rm2 := newKey(t)
|
||||
defer rm1()
|
||||
defer rm2()
|
||||
cache, _ := lru.New(defaultCap)
|
||||
dh := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, fm, cache)
|
||||
ctx := context.TODO()
|
||||
|
||||
for _, tc := range dnsTestCases {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
_, err := dh.ServeDNS(ctx, rec, m)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := rec.Msg
|
||||
sort.Sort(test.RRSet(resp.Answer))
|
||||
sort.Sort(test.RRSet(resp.Ns))
|
||||
sort.Sort(test.RRSet(resp.Extra))
|
||||
|
||||
if !test.Header(t, tc, resp) {
|
||||
t.Logf("%v\n", resp)
|
||||
continue
|
||||
}
|
||||
if !test.Section(t, tc, test.Answer, resp.Answer) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Ns, resp.Ns) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Extra, resp.Extra) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupDNSKEY(t *testing.T) {
|
||||
dnskey, rm1, rm2 := newKey(t)
|
||||
defer rm1()
|
||||
defer rm2()
|
||||
cache, _ := lru.New(defaultCap)
|
||||
dh := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, test.ErrorHandler(), cache)
|
||||
ctx := context.TODO()
|
||||
|
||||
for _, tc := range dnssecTestCases {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
_, err := dh.ServeDNS(ctx, rec, m)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := rec.Msg
|
||||
if !resp.Authoritative {
|
||||
t.Errorf("Authoritative Answer should be true, got false")
|
||||
}
|
||||
|
||||
sort.Sort(test.RRSet(resp.Answer))
|
||||
sort.Sort(test.RRSet(resp.Ns))
|
||||
sort.Sort(test.RRSet(resp.Extra))
|
||||
|
||||
if !test.Header(t, tc, resp) {
|
||||
t.Logf("%v\n", resp)
|
||||
continue
|
||||
}
|
||||
if !test.Section(t, tc, test.Answer, resp.Answer) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Ns, resp.Ns) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Extra, resp.Extra) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dbMiekNL = `
|
||||
$TTL 30M
|
||||
$ORIGIN miek.nl.
|
||||
@ IN SOA linode.atoom.net. miek.miek.nl. (
|
||||
1282630057 ; Serial
|
||||
4H ; Refresh
|
||||
1H ; Retry
|
||||
7D ; Expire
|
||||
4H ) ; Negative Cache TTL
|
||||
IN NS linode.atoom.net.
|
||||
|
||||
IN MX 1 aspmx.l.google.com.
|
||||
|
||||
IN A 139.162.196.78
|
||||
IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
|
||||
|
||||
a IN A 139.162.196.78
|
||||
IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
|
||||
www IN CNAME a`
|
||||
49
vendor/github.com/miekg/coredns/middleware/dnssec/responsewriter.go
generated
vendored
Normal file
49
vendor/github.com/miekg/coredns/middleware/dnssec/responsewriter.go
generated
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package dnssec
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// ResponseWriter sign the response on the fly.
|
||||
type ResponseWriter struct {
|
||||
dns.ResponseWriter
|
||||
d Dnssec
|
||||
}
|
||||
|
||||
// WriteMsg implements the dns.ResponseWriter interface.
|
||||
func (d *ResponseWriter) WriteMsg(res *dns.Msg) error {
|
||||
// By definition we should sign anything that comes back, we should still figure out for
|
||||
// which zone it should be.
|
||||
state := request.Request{W: d.ResponseWriter, Req: res}
|
||||
|
||||
qname := state.Name()
|
||||
zone := middleware.Zones(d.d.zones).Matches(qname)
|
||||
if zone == "" {
|
||||
return d.ResponseWriter.WriteMsg(res)
|
||||
}
|
||||
|
||||
if state.Do() {
|
||||
res = d.d.Sign(state, zone, time.Now().UTC())
|
||||
|
||||
cacheSize.WithLabelValues("signature").Set(float64(d.d.cache.Len()))
|
||||
}
|
||||
state.SizeAndDo(res)
|
||||
|
||||
return d.ResponseWriter.WriteMsg(res)
|
||||
}
|
||||
|
||||
// Write implements the dns.ResponseWriter interface.
|
||||
func (d *ResponseWriter) Write(buf []byte) (int, error) {
|
||||
log.Printf("[WARNING] Dnssec called with Write: not signing reply")
|
||||
n, err := d.ResponseWriter.Write(buf)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Hijack implements the dns.ResponseWriter interface.
|
||||
func (d *ResponseWriter) Hijack() { d.ResponseWriter.Hijack() }
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package dnssec
|
||||
|
||||
import "github.com/miekg/dns"
|
||||
|
||||
// newRRSIG return a new RRSIG, with all fields filled out, except the signed data.
|
||||
func (k *DNSKEY) newRRSIG(signerName string, ttl, incep, expir uint32) *dns.RRSIG {
|
||||
sig := new(dns.RRSIG)
|
||||
|
||||
sig.Hdr.Rrtype = dns.TypeRRSIG
|
||||
sig.Algorithm = k.K.Algorithm
|
||||
sig.KeyTag = k.keytag
|
||||
sig.SignerName = signerName
|
||||
sig.Hdr.Ttl = ttl
|
||||
sig.OrigTtl = origTTL
|
||||
|
||||
sig.Inception = incep
|
||||
sig.Expiration = expir
|
||||
|
||||
return sig
|
||||
}
|
||||
|
||||
type rrset struct {
|
||||
qname string
|
||||
qtype uint16
|
||||
}
|
||||
|
||||
// rrSets returns rrs as a map of RRsets. It skips RRSIG and OPT records as those don't need to be signed.
|
||||
func rrSets(rrs []dns.RR) map[rrset][]dns.RR {
|
||||
m := make(map[rrset][]dns.RR)
|
||||
|
||||
for _, r := range rrs {
|
||||
if r.Header().Rrtype == dns.TypeRRSIG || r.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
|
||||
if s, ok := m[rrset{r.Header().Name, r.Header().Rrtype}]; ok {
|
||||
s = append(s, r)
|
||||
m[rrset{r.Header().Name, r.Header().Rrtype}] = s
|
||||
continue
|
||||
}
|
||||
|
||||
s := make([]dns.RR, 1, 3)
|
||||
s[0] = r
|
||||
m[rrset{r.Header().Name, r.Header().Rrtype}] = s
|
||||
}
|
||||
|
||||
if len(m) > 0 {
|
||||
return m
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const origTTL = 3600
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package dnssec
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/middleware"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("dnssec", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
zones, keys, capacity, err := dnssecParse(c)
|
||||
if err != nil {
|
||||
return middleware.Error("dnssec", err)
|
||||
}
|
||||
|
||||
cache, err := lru.New(capacity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler {
|
||||
return New(zones, keys, next, cache)
|
||||
})
|
||||
|
||||
// Export the capacity for the metrics. This only happens once, because this is a re-load change only.
|
||||
cacheCapacity.WithLabelValues("signature").Set(float64(capacity))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, error) {
|
||||
zones := []string{}
|
||||
|
||||
keys := []*DNSKEY{}
|
||||
|
||||
capacity := defaultCap
|
||||
for c.Next() {
|
||||
if c.Val() == "dnssec" {
|
||||
// dnssec [zones...]
|
||||
zones = make([]string, len(c.ServerBlockKeys))
|
||||
copy(zones, c.ServerBlockKeys)
|
||||
args := c.RemainingArgs()
|
||||
if len(args) > 0 {
|
||||
zones = args
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "key":
|
||||
k, e := keyParse(c)
|
||||
if e != nil {
|
||||
return nil, nil, 0, e
|
||||
}
|
||||
keys = append(keys, k...)
|
||||
case "cache_capacity":
|
||||
if !c.NextArg() {
|
||||
return nil, nil, 0, c.ArgErr()
|
||||
}
|
||||
value := c.Val()
|
||||
cacheCap, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
capacity = cacheCap
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range zones {
|
||||
zones[i] = middleware.Host(zones[i]).Normalize()
|
||||
}
|
||||
return zones, keys, capacity, nil
|
||||
}
|
||||
|
||||
func keyParse(c *caddy.Controller) ([]*DNSKEY, error) {
|
||||
keys := []*DNSKEY{}
|
||||
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
value := c.Val()
|
||||
if value == "file" {
|
||||
ks := c.RemainingArgs()
|
||||
for _, k := range ks {
|
||||
base := k
|
||||
// Kmiek.nl.+013+26205.key, handle .private or without extension: Kmiek.nl.+013+26205
|
||||
if strings.HasSuffix(k, ".key") {
|
||||
base = k[:len(k)-4]
|
||||
}
|
||||
if strings.HasSuffix(k, ".private") {
|
||||
base = k[:len(k)-8]
|
||||
}
|
||||
k, err := ParseKeyFile(base+".key", base+".private")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package dnssec
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestSetupDnssec(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedZones []string
|
||||
expectedKeys []string
|
||||
expectedCapacity int
|
||||
expectedErrContent string
|
||||
}{
|
||||
{
|
||||
`dnssec`, false, nil, nil, defaultCap, "",
|
||||
},
|
||||
{
|
||||
`dnssec miek.nl`, false, []string{"miek.nl."}, nil, defaultCap, "",
|
||||
},
|
||||
{
|
||||
`dnssec miek.nl {
|
||||
cache_capacity 100
|
||||
}`, false, []string{"miek.nl."}, nil, 100, "",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.input)
|
||||
zones, keys, capacity, err := dnssecParse(c)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if !test.shouldErr {
|
||||
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), test.expectedErrContent) {
|
||||
t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input)
|
||||
}
|
||||
}
|
||||
if !test.shouldErr {
|
||||
for i, z := range test.expectedZones {
|
||||
if zones[i] != z {
|
||||
t.Errorf("Dnssec not correctly set for input %s. Expected: %s, actual: %s", test.input, z, zones[i])
|
||||
}
|
||||
}
|
||||
for i, k := range test.expectedKeys {
|
||||
if k != keys[i].K.Header().Name {
|
||||
t.Errorf("Dnssec not correctly set for input %s. Expected: '%s', actual: '%s'", test.input, k, keys[i].K.Header().Name)
|
||||
}
|
||||
}
|
||||
if capacity != test.expectedCapacity {
|
||||
t.Errorf("Dnssec not correctly set capacity for input '%s' Expected: '%d', actual: '%d'", test.input, capacity, test.expectedCapacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# erratic
|
||||
|
||||
*erratic* is a middleware useful for testing client behavior. It returns a static response to all
|
||||
queries, but the responses can be delayed by a random amount of time or dropped all together, i.e.
|
||||
no answer at all.
|
||||
|
||||
~~~ txt
|
||||
._<transport>.qname. 0 IN SRV 0 0 <port> .
|
||||
~~~
|
||||
|
||||
The *erratic* middleware will respond to every A or AAAA query. For any other type it will return
|
||||
a SERVFAIL response. The reply for A will return 192.0.2.53 (see RFC 5737), for AAAA it returns
|
||||
2001:DB8::53 (see RFC 3849).
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~ txt
|
||||
erratic {
|
||||
drop AMOUNT
|
||||
}
|
||||
~~~
|
||||
|
||||
* **AMOUNT** drop 1 per **AMOUNT** of the queries, the default is 2.
|
||||
|
||||
## Examples
|
||||
|
||||
~~~ txt
|
||||
.:53 {
|
||||
erratic {
|
||||
drop 3
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
Or even shorter if the defaults suits you:
|
||||
|
||||
~~~ txt
|
||||
. {
|
||||
erratic
|
||||
}
|
||||
~~~
|
||||
|
||||
## Bugs
|
||||
|
||||
Delaying answers is not implemented.
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// Package erratic implements a middleware that returns erratic answers (delayed, dropped).
|
||||
package erratic
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Erratic is a middleware that returns erratic repsonses to each client.
|
||||
type Erratic struct {
|
||||
amount uint64
|
||||
|
||||
q uint64 // counter of queries
|
||||
}
|
||||
|
||||
// ServeDNS implements the middleware.Handler interface.
|
||||
func (e *Erratic) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
|
||||
drop := false
|
||||
if e.amount > 0 {
|
||||
queryNr := atomic.LoadUint64(&e.q)
|
||||
|
||||
if queryNr%e.amount == 0 {
|
||||
drop = true
|
||||
}
|
||||
|
||||
atomic.AddUint64(&e.q, 1)
|
||||
}
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Compress = true
|
||||
m.Authoritative = true
|
||||
|
||||
// small dance to copy rrA or rrAAAA into a non-pointer var that allows us to overwrite the ownername
|
||||
// in a non-racy way.
|
||||
switch state.QType() {
|
||||
case dns.TypeA:
|
||||
rr := *(rrA.(*dns.A))
|
||||
rr.Header().Name = state.QName()
|
||||
m.Answer = append(m.Answer, &rr)
|
||||
case dns.TypeAAAA:
|
||||
rr := *(rrAAAA.(*dns.AAAA))
|
||||
rr.Header().Name = state.QName()
|
||||
m.Answer = append(m.Answer, &rr)
|
||||
default:
|
||||
if !drop {
|
||||
// coredns will return error.
|
||||
return dns.RcodeServerFailure, nil
|
||||
}
|
||||
}
|
||||
|
||||
if drop {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
state.SizeAndDo(m)
|
||||
w.WriteMsg(m)
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (e *Erratic) Name() string { return "erratic" }
|
||||
|
||||
var (
|
||||
rrA, _ = dns.NewRR(". IN 0 A 192.0.2.53")
|
||||
rrAAAA, _ = dns.NewRR(". IN 0 AAAA 2001:DB8::53")
|
||||
)
|
||||
45
vendor/github.com/miekg/coredns/middleware/erratic/erratic_test.go
generated
vendored
Normal file
45
vendor/github.com/miekg/coredns/middleware/erratic/erratic_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package erratic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/middleware/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestErraticDrop(t *testing.T) {
|
||||
e := &Erratic{amount: 2} // 50% drops
|
||||
|
||||
tests := []struct {
|
||||
expectedCode int
|
||||
expectedErr error
|
||||
drop bool
|
||||
}{
|
||||
{expectedCode: dns.RcodeSuccess, expectedErr: nil, drop: true},
|
||||
{expectedCode: dns.RcodeSuccess, expectedErr: nil, drop: false},
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
for i, tc := range tests {
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.org.", dns.TypeA)
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
code, err := e.ServeDNS(ctx, rec, req)
|
||||
|
||||
if err != tc.expectedErr {
|
||||
t.Errorf("Test %d: Expected error %q, but got %q", i, tc.expectedErr, err)
|
||||
}
|
||||
if code != int(tc.expectedCode) {
|
||||
t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code)
|
||||
}
|
||||
|
||||
if tc.drop && rec.Msg != nil {
|
||||
t.Errorf("Test %d: Expected dropped packet, but got %q", i, rec.Msg.Question[0].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package erratic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/middleware"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("erratic", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setupErratic,
|
||||
})
|
||||
}
|
||||
|
||||
func setupErratic(c *caddy.Controller) error {
|
||||
e, err := parseErratic(c)
|
||||
if err != nil {
|
||||
return middleware.Error("erratic", err)
|
||||
}
|
||||
|
||||
dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler {
|
||||
return e
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseErratic(c *caddy.Controller) (*Erratic, error) {
|
||||
e := &Erratic{amount: 2}
|
||||
for c.Next() { // 'erratic'
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "drop":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) > 1 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
amount, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if amount < 0 {
|
||||
return nil, fmt.Errorf("illegal amount value given %q", args[0])
|
||||
}
|
||||
e.amount = uint64(amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package erratic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestSetupWhoami(t *testing.T) {
|
||||
c := caddy.NewTestController("dns", `erratic {
|
||||
drop
|
||||
}`)
|
||||
if err := setupErratic(c); err != nil {
|
||||
t.Fatalf("Test 1, expected no errors, but got: %q", err)
|
||||
}
|
||||
|
||||
c = caddy.NewTestController("dns", `erratic`)
|
||||
if err := setupErratic(c); err != nil {
|
||||
t.Fatalf("Test 2, expected no errors, but got: %q", err)
|
||||
}
|
||||
|
||||
c = caddy.NewTestController("dns", `erratic {
|
||||
drop -1
|
||||
}`)
|
||||
if err := setupErratic(c); err == nil {
|
||||
t.Fatalf("Test 4, expected errors, but got: %q", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# errors
|
||||
|
||||
*errors* enables error logging.
|
||||
TODO: what are errors.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
errors [LOGFILE]
|
||||
~~~
|
||||
|
||||
* **LOGFILE** is the path to the error log file to create (or append to), relative to the current
|
||||
working directory. It can also be `stdout` or `stderr` to write to the console, syslog to write to the
|
||||
system log (except on Windows), or visible to write the error (including full stack trace, if
|
||||
applicable) to the response. Writing errors to the response is NOT advised except in local debug
|
||||
situations. The default is stderr. The above syntax will simply enable error reporting on the
|
||||
server. To specify custom error pages, open a block:
|
||||
|
||||
~~~
|
||||
errors {
|
||||
what where
|
||||
}
|
||||
~~~
|
||||
|
||||
* `what` can only be `log`.
|
||||
* `where` is the path to the log file (as described above) and you can enable rotation to manage the log files.
|
||||
|
||||
## Examples
|
||||
|
||||
Log errors into a file in the parent directory:
|
||||
|
||||
~~~
|
||||
errors ../error.log
|
||||
~~~
|
||||
|
||||
Make errors visible to the client (for debugging only):
|
||||
|
||||
~~~
|
||||
errors visible
|
||||
~~~
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
// Package errors implements an HTTP error handling middleware.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// errorHandler handles DNS errors (and errors from other middleware).
|
||||
type errorHandler struct {
|
||||
Next middleware.Handler
|
||||
LogFile string
|
||||
Log *log.Logger
|
||||
Debug bool // if true, errors are written out to client rather than to a log
|
||||
}
|
||||
|
||||
// ServeDNS implements the middleware.Handler interface.
|
||||
func (h errorHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
defer h.recovery(ctx, w, r)
|
||||
|
||||
rcode, err := middleware.NextOrFailure(h.Name(), h.Next, ctx, w, r)
|
||||
|
||||
if err != nil {
|
||||
state := request.Request{W: w, Req: r}
|
||||
errMsg := fmt.Sprintf("%s [ERROR %d %s %s] %v", time.Now().Format(timeFormat), rcode, state.Name(), state.Type(), err)
|
||||
|
||||
if h.Debug {
|
||||
// Write error to response as a txt message instead of to log
|
||||
answer := debugMsg(rcode, r)
|
||||
txt, _ := dns.NewRR(". IN 0 TXT " + errMsg)
|
||||
answer.Answer = append(answer.Answer, txt)
|
||||
state.SizeAndDo(answer)
|
||||
w.WriteMsg(answer)
|
||||
return 0, err
|
||||
}
|
||||
h.Log.Println(errMsg)
|
||||
}
|
||||
|
||||
return rcode, err
|
||||
}
|
||||
|
||||
func (h errorHandler) Name() string { return "errors" }
|
||||
|
||||
func (h errorHandler) recovery(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
|
||||
rec := recover()
|
||||
if rec == nil {
|
||||
return
|
||||
}
|
||||
|
||||
state := request.Request{W: w, Req: r}
|
||||
// Obtain source of panic
|
||||
// From: https://gist.github.com/swdunlop/9629168
|
||||
var name, file string // function name, file name
|
||||
var line int
|
||||
var pc [16]uintptr
|
||||
n := runtime.Callers(3, pc[:])
|
||||
for _, pc := range pc[:n] {
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
continue
|
||||
}
|
||||
file, line = fn.FileLine(pc)
|
||||
name = fn.Name()
|
||||
if !strings.HasPrefix(name, "runtime.") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Trim file path
|
||||
delim := "/coredns/"
|
||||
pkgPathPos := strings.Index(file, delim)
|
||||
if pkgPathPos > -1 && len(file) > pkgPathPos+len(delim) {
|
||||
file = file[pkgPathPos+len(delim):]
|
||||
}
|
||||
|
||||
panicMsg := fmt.Sprintf("%s [PANIC %s %s] %s:%d - %v", time.Now().Format(timeFormat), r.Question[0].Name, dns.Type(r.Question[0].Qtype), file, line, rec)
|
||||
if h.Debug {
|
||||
// Write error and stack trace to the response rather than to a log
|
||||
var stackBuf [4096]byte
|
||||
stack := stackBuf[:runtime.Stack(stackBuf[:], false)]
|
||||
answer := debugMsg(dns.RcodeServerFailure, r)
|
||||
// add stack buf in TXT, limited to 255 chars for now.
|
||||
txt, _ := dns.NewRR(". IN 0 TXT " + string(stack[:255]))
|
||||
answer.Answer = append(answer.Answer, txt)
|
||||
state.SizeAndDo(answer)
|
||||
w.WriteMsg(answer)
|
||||
} else {
|
||||
// Currently we don't use the function name, since file:line is more conventional
|
||||
h.Log.Printf(panicMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// debugMsg creates a debug message that gets send back to the client.
|
||||
func debugMsg(rcode int, r *dns.Msg) *dns.Msg {
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(r, rcode)
|
||||
return answer
|
||||
}
|
||||
|
||||
const timeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/middleware/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
em := errorHandler{Log: log.New(&buf, "", 0)}
|
||||
|
||||
testErr := errors.New("test error")
|
||||
tests := []struct {
|
||||
next middleware.Handler
|
||||
expectedCode int
|
||||
expectedLog string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
next: genErrorHandler(dns.RcodeSuccess, nil),
|
||||
expectedCode: dns.RcodeSuccess,
|
||||
expectedLog: "",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
next: genErrorHandler(dns.RcodeNotAuth, testErr),
|
||||
expectedCode: dns.RcodeNotAuth,
|
||||
expectedLog: fmt.Sprintf("[ERROR %d %s] %v\n", dns.RcodeNotAuth, "example.org. A", testErr),
|
||||
expectedErr: testErr,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.org.", dns.TypeA)
|
||||
|
||||
for i, tc := range tests {
|
||||
em.Next = tc.next
|
||||
buf.Reset()
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
code, err := em.ServeDNS(ctx, rec, req)
|
||||
|
||||
if err != tc.expectedErr {
|
||||
t.Errorf("Test %d: Expected error %v, but got %v",
|
||||
i, tc.expectedErr, err)
|
||||
}
|
||||
if code != tc.expectedCode {
|
||||
t.Errorf("Test %d: Expected status code %d, but got %d",
|
||||
i, tc.expectedCode, code)
|
||||
}
|
||||
if log := buf.String(); !strings.Contains(log, tc.expectedLog) {
|
||||
t.Errorf("Test %d: Expected log %q, but got %q",
|
||||
i, tc.expectedLog, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisibleErrorWithPanic(t *testing.T) {
|
||||
const panicMsg = "I'm a panic"
|
||||
eh := errorHandler{
|
||||
Debug: true,
|
||||
Next: middleware.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
panic(panicMsg)
|
||||
}),
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.org.", dns.TypeA)
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
|
||||
code, err := eh.ServeDNS(ctx, rec, req)
|
||||
if code != 0 {
|
||||
t.Errorf("Expected error handler to return 0 (it should write to response), got status %d", code)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Expected error handler to return nil error (it should panic!), but got '%v'", err)
|
||||
}
|
||||
}
|
||||
|
||||
func genErrorHandler(rcode int, err error) middleware.Handler {
|
||||
return middleware.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
return rcode, err
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/middleware"
|
||||
|
||||
"github.com/hashicorp/go-syslog"
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("errors", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
handler, err := errorsParse(c)
|
||||
if err != nil {
|
||||
return middleware.Error("errors", err)
|
||||
}
|
||||
|
||||
var writer io.Writer
|
||||
|
||||
switch handler.LogFile {
|
||||
case "visible":
|
||||
handler.Debug = true
|
||||
case "stdout":
|
||||
writer = os.Stdout
|
||||
case "stderr":
|
||||
writer = os.Stderr
|
||||
case "syslog":
|
||||
writer, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "coredns")
|
||||
if err != nil {
|
||||
return middleware.Error("errors", err)
|
||||
}
|
||||
default:
|
||||
if handler.LogFile == "" {
|
||||
writer = os.Stderr // default
|
||||
break
|
||||
}
|
||||
|
||||
var file *os.File
|
||||
file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return middleware.Error("errors", err)
|
||||
}
|
||||
writer = file
|
||||
}
|
||||
handler.Log = log.New(writer, "", 0)
|
||||
|
||||
dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler {
|
||||
handler.Next = next
|
||||
return handler
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func errorsParse(c *caddy.Controller) (errorHandler, error) {
|
||||
handler := errorHandler{}
|
||||
|
||||
optionalBlock := func() (bool, error) {
|
||||
var hadBlock bool
|
||||
|
||||
for c.NextBlock() {
|
||||
hadBlock = true
|
||||
|
||||
what := c.Val()
|
||||
if !c.NextArg() {
|
||||
return hadBlock, c.ArgErr()
|
||||
}
|
||||
where := c.Val()
|
||||
|
||||
if what == "log" {
|
||||
if where == "visible" {
|
||||
handler.Debug = true
|
||||
} else {
|
||||
handler.LogFile = where
|
||||
}
|
||||
}
|
||||
}
|
||||
return hadBlock, nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
// Configuration may be in a block
|
||||
hadBlock, err := optionalBlock()
|
||||
if err != nil {
|
||||
return handler, err
|
||||
}
|
||||
|
||||
// Otherwise, the only argument would be an error log file name or 'visible'
|
||||
if !hadBlock {
|
||||
if c.NextArg() {
|
||||
if c.Val() == "visible" {
|
||||
handler.Debug = true
|
||||
} else {
|
||||
handler.LogFile = c.Val()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestErrorsParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputErrorsRules string
|
||||
shouldErr bool
|
||||
expectedErrorHandler errorHandler
|
||||
}{
|
||||
{`errors`, false, errorHandler{
|
||||
LogFile: "",
|
||||
}},
|
||||
{`errors errors.txt`, false, errorHandler{
|
||||
LogFile: "errors.txt",
|
||||
}},
|
||||
{`errors visible`, false, errorHandler{
|
||||
LogFile: "",
|
||||
Debug: true,
|
||||
}},
|
||||
{`errors { log visible }`, false, errorHandler{
|
||||
LogFile: "",
|
||||
Debug: true,
|
||||
}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.inputErrorsRules)
|
||||
actualErrorsRule, err := errorsParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if actualErrorsRule.LogFile != test.expectedErrorHandler.LogFile {
|
||||
t.Errorf("Test %d expected LogFile to be %s, but got %s",
|
||||
i, test.expectedErrorHandler.LogFile, actualErrorsRule.LogFile)
|
||||
}
|
||||
if actualErrorsRule.Debug != test.expectedErrorHandler.Debug {
|
||||
t.Errorf("Test %d expected Debug to be %v, but got %v",
|
||||
i, test.expectedErrorHandler.Debug, actualErrorsRule.Debug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
# etcd
|
||||
|
||||
*etcd* enables reading zone data from an etcd instance. The data in etcd has to be encoded as
|
||||
a [message](https://github.com/skynetservices/skydns/blob/2fcff74cdc9f9a7dd64189a447ef27ac354b725f/msg/service.go#L26)
|
||||
like [SkyDNS](https//github.com/skynetservices/skydns). It should also work just like SkyDNS.
|
||||
|
||||
The etcd middleware makes extensive use of the proxy middleware to forward and query other servers
|
||||
in the network.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
etcd [ZONES...]
|
||||
~~~
|
||||
|
||||
* **ZONES** zones etcd should be authoritative for.
|
||||
|
||||
The path will default to `/skydns` the local etcd proxy (http://localhost:2379).
|
||||
If no zones are specified the block's zone will be used as the zone.
|
||||
|
||||
If you want to `round robin` A and AAAA responses look at the `loadbalance` middleware.
|
||||
|
||||
~~~
|
||||
etcd [ZONES...] {
|
||||
stubzones
|
||||
path PATH
|
||||
endpoint ENDPOINT...
|
||||
upstream ADDRESS...
|
||||
tls CERT KEY CACERt
|
||||
debug
|
||||
}
|
||||
~~~
|
||||
|
||||
* `stubzones` enables the stub zones feature. The stubzone is *only* done in the etcd tree located
|
||||
under the *first* zone specified.
|
||||
* **PATH** the path inside etcd. Defaults to "/skydns".
|
||||
* **ENDPOINT** the etcd endpoints. Defaults to "http://localhost:2397".
|
||||
* `upstream` upstream resolvers to be used resolve external names found in etcd (think CNAMEs)
|
||||
pointing to external names. If you want CoreDNS to act as a proxy for clients, you'll need to add
|
||||
the proxy middleware. **ADDRESS** can be an IP address, and IP:port or a string pointing to a file
|
||||
that is structured as /etc/resolv.conf.
|
||||
* `tls` followed by:
|
||||
* no arguments, if the server certificate is signed by a system-installed CA and no client cert is needed
|
||||
* a single argument that is the CA PEM file, if the server cert is not signed by a system CA and no client cert is needed
|
||||
* two arguments - path to cert PEM file, the path to private key PEM file - if the server certificate is signed by a system-installed CA and a client certificate is needed
|
||||
* three arguments - path to cert PEM file, path to client private key PEM file, path to CA PEM file - if the server certificate is not signed by a system-installed CA and client certificate is needed
|
||||
* `debug` allows for debug queries. Prefix the name with `o-o.debug.` to retrieve extra information in the
|
||||
additional section of the reply in the form of TXT records.
|
||||
|
||||
## Examples
|
||||
|
||||
This is the default SkyDNS setup, with everying specified in full:
|
||||
|
||||
~~~
|
||||
.:53 {
|
||||
etcd skydns.local {
|
||||
stubzones
|
||||
path /skydns
|
||||
endpoint http://localhost:2379
|
||||
upstream 8.8.8.8:53 8.8.4.4:53
|
||||
}
|
||||
prometheus
|
||||
cache 160 skydns.local
|
||||
loadbalance
|
||||
proxy . 8.8.8.8:53 8.8.4.4:53
|
||||
}
|
||||
~~~
|
||||
|
||||
Or a setup where we use `/etc/resolv.conf` as the basis for the proxy and the upstream
|
||||
when resolving external pointing CNAMEs.
|
||||
|
||||
~~~
|
||||
.:53 {
|
||||
etcd skydns.local {
|
||||
path /skydns
|
||||
upstream /etc/resolv.conf
|
||||
}
|
||||
cache 160 skydns.local
|
||||
proxy . /etc/resolv.conf
|
||||
}
|
||||
~~~
|
||||
|
||||
|
||||
### Reverse zones
|
||||
|
||||
Reverse zones are supported. You need to make CoreDNS aware of the fact that you are also
|
||||
authoritative for the reverse. For instance if you want to add the reverse for 10.0.0.0/24, you'll
|
||||
need to add the zone `0.0.10.in-addr.arpa` to the list of zones. (The fun starts with IPv6 reverse zones
|
||||
in the ip6.arpa domain.) Showing a snippet of a Corefile:
|
||||
|
||||
~~~
|
||||
etcd skydns.local 0.0.10.in-addr.arpa {
|
||||
stubzones
|
||||
...
|
||||
~~~
|
||||
|
||||
Next you'll need to populate the zone with reverse records, here we add a reverse for
|
||||
10.0.0.127 pointing to reverse.skydns.local.
|
||||
|
||||
~~~
|
||||
% curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/arpa/in-addr/10/0/0/127 \
|
||||
-d value='{"host":"reverse.skydns.local."}'
|
||||
~~~
|
||||
|
||||
Querying with dig:
|
||||
|
||||
~~~
|
||||
% dig @localhost -x 10.0.0.127 +short
|
||||
reverse.atoom.net.
|
||||
~~~
|
||||
|
||||
Or with *debug* queries enabled:
|
||||
|
||||
~~~
|
||||
% dig @localhost -p 1053 o-o.debug.127.0.0.10.in-addr.arpa. PTR
|
||||
|
||||
;; OPT PSEUDOSECTION:
|
||||
; EDNS: version: 0, flags:; udp: 4096
|
||||
;; QUESTION SECTION:
|
||||
;o-o.debug.127.0.0.10.in-addr.arpa. IN PTR
|
||||
|
||||
;; ANSWER SECTION:
|
||||
127.0.0.10.in-addr.arpa. 300 IN PTR reverse.atoom.net.
|
||||
|
||||
;; ADDITIONAL SECTION:
|
||||
127.0.0.10.in-addr.arpa. 300 CH TXT "reverse.atoom.net.:0(10,0,,false)[0,]"
|
||||
~~~
|
||||
|
||||
## Debug queries
|
||||
|
||||
When debug queries are enabled CoreDNS will return errors and etcd records encountered during the resolution
|
||||
process in the response. The general form looks like this:
|
||||
|
||||
skydns.test.skydns.dom.a. 0 CH TXT "127.0.0.1:0(10,0,,false)[0,]"
|
||||
|
||||
This shows the complete key as the owername, the rdata of the TXT record has:
|
||||
`host:port(priority,weight,txt content,mail)[targetstrip,group]`.
|
||||
|
||||
Errors when communicating with an upstream will be returned as: `host:0(0,0,error message,false)[0,]`.
|
||||
|
||||
An example:
|
||||
|
||||
www.example.org. 0 CH TXT "www.example.org.:0(0,0, IN A: unreachable backend,false)[0,]"
|
||||
|
||||
Signalling that an A record for www.example.org. was sought, but it failed with that error.
|
||||
|
||||
Any errors seen doing parsing will show up like this:
|
||||
|
||||
. 0 CH TXT "/skydns/local/skydns/r/a: invalid character '.' after object key:value pair"
|
||||
|
||||
which shows `a.r.skydns.local.` has a json encoding problem.
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
// +build etcd
|
||||
|
||||
package etcd
|
||||
|
||||
// etcd needs to be running on http://localhost:2379
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/middleware/etcd/msg"
|
||||
"github.com/coredns/coredns/middleware/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Check the ordering of returned cname.
|
||||
func TestCnameLookup(t *testing.T) {
|
||||
etc := newEtcdMiddleware()
|
||||
|
||||
for _, serv := range servicesCname {
|
||||
set(t, etc, serv.Key, 0, serv)
|
||||
defer delete(t, etc, serv.Key)
|
||||
}
|
||||
for _, tc := range dnsTestCasesCname {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
_, err := etc.ServeDNS(ctxt, rec, m)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := rec.Msg
|
||||
if !test.Header(t, tc, resp) {
|
||||
t.Logf("%v\n", resp)
|
||||
continue
|
||||
}
|
||||
if !test.Section(t, tc, test.Answer, resp.Answer) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Ns, resp.Ns) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Extra, resp.Extra) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var servicesCname = []*msg.Service{
|
||||
{Host: "cname1.region2.skydns.test", Key: "a.server1.dev.region1.skydns.test."},
|
||||
{Host: "cname2.region2.skydns.test", Key: "cname1.region2.skydns.test."},
|
||||
{Host: "cname3.region2.skydns.test", Key: "cname2.region2.skydns.test."},
|
||||
{Host: "cname4.region2.skydns.test", Key: "cname3.region2.skydns.test."},
|
||||
{Host: "cname5.region2.skydns.test", Key: "cname4.region2.skydns.test."},
|
||||
{Host: "cname6.region2.skydns.test", Key: "cname5.region2.skydns.test."},
|
||||
{Host: "endpoint.region2.skydns.test", Key: "cname6.region2.skydns.test."},
|
||||
{Host: "10.240.0.1", Key: "endpoint.region2.skydns.test."},
|
||||
}
|
||||
|
||||
var dnsTestCasesCname = []test.Case{
|
||||
{
|
||||
Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("a.server1.dev.region1.skydns.test. 300 IN SRV 10 100 0 cname1.region2.skydns.test."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.CNAME("cname1.region2.skydns.test. 300 IN CNAME cname2.region2.skydns.test."),
|
||||
test.CNAME("cname2.region2.skydns.test. 300 IN CNAME cname3.region2.skydns.test."),
|
||||
test.CNAME("cname3.region2.skydns.test. 300 IN CNAME cname4.region2.skydns.test."),
|
||||
test.CNAME("cname4.region2.skydns.test. 300 IN CNAME cname5.region2.skydns.test."),
|
||||
test.CNAME("cname5.region2.skydns.test. 300 IN CNAME cname6.region2.skydns.test."),
|
||||
test.CNAME("cname6.region2.skydns.test. 300 IN CNAME endpoint.region2.skydns.test."),
|
||||
test.A("endpoint.region2.skydns.test. 300 IN A 10.240.0.1"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
// +build etcd
|
||||
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/middleware/etcd/msg"
|
||||
"github.com/coredns/coredns/middleware/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestDebugLookup(t *testing.T) {
|
||||
etc := newEtcdMiddleware()
|
||||
etc.Debugging = true
|
||||
|
||||
for _, serv := range servicesDebug {
|
||||
set(t, etc, serv.Key, 0, serv)
|
||||
defer delete(t, etc, serv.Key)
|
||||
}
|
||||
|
||||
for _, tc := range dnsTestCasesDebug {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
etc.ServeDNS(ctxt, rec, m)
|
||||
|
||||
resp := rec.Msg
|
||||
sort.Sort(test.RRSet(resp.Answer))
|
||||
sort.Sort(test.RRSet(resp.Ns))
|
||||
sort.Sort(test.RRSet(resp.Extra))
|
||||
|
||||
if !test.Header(t, tc, resp) {
|
||||
t.Logf("%v\n", resp)
|
||||
continue
|
||||
}
|
||||
if !test.Section(t, tc, test.Answer, resp.Answer) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Ns, resp.Ns) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Extra, resp.Extra) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugLookupFalse(t *testing.T) {
|
||||
etc := newEtcdMiddleware()
|
||||
|
||||
for _, serv := range servicesDebug {
|
||||
set(t, etc, serv.Key, 0, serv)
|
||||
defer delete(t, etc, serv.Key)
|
||||
}
|
||||
for _, tc := range dnsTestCasesDebugFalse {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
etc.ServeDNS(ctxt, rec, m)
|
||||
|
||||
resp := rec.Msg
|
||||
sort.Sort(test.RRSet(resp.Answer))
|
||||
sort.Sort(test.RRSet(resp.Ns))
|
||||
sort.Sort(test.RRSet(resp.Extra))
|
||||
|
||||
if !test.Header(t, tc, resp) {
|
||||
t.Logf("%v\n", resp)
|
||||
continue
|
||||
}
|
||||
if !test.Section(t, tc, test.Answer, resp.Answer) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Ns, resp.Ns) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Extra, resp.Extra) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var servicesDebug = []*msg.Service{
|
||||
{Host: "127.0.0.1", Key: "a.dom.skydns.test."},
|
||||
{Host: "127.0.0.2", Key: "b.sub.dom.skydns.test."},
|
||||
}
|
||||
|
||||
var dnsTestCasesDebug = []test.Case{
|
||||
{
|
||||
Qname: "o-o.debug.dom.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("dom.skydns.test. 300 IN A 127.0.0.1"),
|
||||
test.A("dom.skydns.test. 300 IN A 127.0.0.2"),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.TXT(`a.dom.skydns.test. 300 CH TXT "127.0.0.1:0(10,0,,false)[0,]"`),
|
||||
test.TXT(`b.sub.dom.skydns.test. 300 CH TXT "127.0.0.2:0(10,0,,false)[0,]"`),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "o-o.debug.dom.skydns.test.", Qtype: dns.TypeTXT,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("skydns.test. 300 IN SOA ns.dns.skydns.test. hostmaster.skydns.test. 1463943291 7200 1800 86400 60"),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.TXT(`a.dom.skydns.test. 300 CH TXT "127.0.0.1:0(10,0,,false)[0,]"`),
|
||||
test.TXT(`b.sub.dom.skydns.test. 300 CH TXT "127.0.0.2:0(10,0,,false)[0,]"`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var dnsTestCasesDebugFalse = []test.Case{
|
||||
{
|
||||
Qname: "o-o.debug.dom.skydns.test.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("skydns.test. 300 IN SOA ns.dns.skydns.test. hostmaster.skydns.test. 1463943291 7200 1800 86400 60"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "o-o.debug.dom.skydns.test.", Qtype: dns.TypeTXT,
|
||||
Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("skydns.test. 300 IN SOA ns.dns.skydns.test. hostmaster.skydns.test. 1463943291 7200 1800 86400 60"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
// Package etcd provides the etcd backend middleware.
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/middleware/etcd/msg"
|
||||
"github.com/coredns/coredns/middleware/pkg/singleflight"
|
||||
"github.com/coredns/coredns/middleware/proxy"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
etcdc "github.com/coreos/etcd/client"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Etcd is a middleware talks to an etcd cluster.
|
||||
type Etcd struct {
|
||||
Next middleware.Handler
|
||||
Zones []string
|
||||
PathPrefix string
|
||||
Proxy proxy.Proxy // Proxy for looking up names during the resolution process
|
||||
Client etcdc.KeysAPI
|
||||
Ctx context.Context
|
||||
Inflight *singleflight.Group
|
||||
Stubmap *map[string]proxy.Proxy // list of proxies for stub resolving.
|
||||
Debugging bool // Do we allow debug queries.
|
||||
|
||||
endpoints []string // Stored here as well, to aid in testing.
|
||||
}
|
||||
|
||||
// Services implements the ServiceBackend interface.
|
||||
func (e *Etcd) Services(state request.Request, exact bool, opt middleware.Options) (services, debug []msg.Service, err error) {
|
||||
services, err = e.Records(state.Name(), exact)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if opt.Debug != "" {
|
||||
debug = services
|
||||
}
|
||||
services = msg.Group(services)
|
||||
return
|
||||
}
|
||||
|
||||
// Reverse implements the ServiceBackend interface.
|
||||
func (e *Etcd) Reverse(state request.Request, exact bool, opt middleware.Options) (services, debug []msg.Service, err error) {
|
||||
return e.Services(state, exact, opt)
|
||||
}
|
||||
|
||||
// Lookup implements the ServiceBackend interface.
|
||||
func (e *Etcd) Lookup(state request.Request, name string, typ uint16) (*dns.Msg, error) {
|
||||
return e.Proxy.Lookup(state, name, typ)
|
||||
}
|
||||
|
||||
// IsNameError implements the ServiceBackend interface.
|
||||
func (e *Etcd) IsNameError(err error) bool {
|
||||
if ee, ok := err.(etcdc.Error); ok && ee.Code == etcdc.ErrorCodeKeyNotFound {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Debug implements the ServiceBackend interface.
|
||||
func (e *Etcd) Debug() string {
|
||||
return e.PathPrefix
|
||||
}
|
||||
|
||||
// Records looks up records in etcd. If exact is true, it will lookup just this
|
||||
// name. This is used when find matches when completing SRV lookups for instance.
|
||||
func (e *Etcd) Records(name string, exact bool) ([]msg.Service, error) {
|
||||
path, star := msg.PathWithWildcard(name, e.PathPrefix)
|
||||
r, err := e.get(path, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
segments := strings.Split(msg.Path(name, e.PathPrefix), "/")
|
||||
switch {
|
||||
case exact && r.Node.Dir:
|
||||
return nil, nil
|
||||
case r.Node.Dir:
|
||||
return e.loopNodes(r.Node.Nodes, segments, star, nil)
|
||||
default:
|
||||
return e.loopNodes([]*etcdc.Node{r.Node}, segments, false, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// get is a wrapper for client.Get that uses SingleInflight to suppress multiple outstanding queries.
|
||||
func (e *Etcd) get(path string, recursive bool) (*etcdc.Response, error) {
|
||||
resp, err := e.Inflight.Do(path, func() (interface{}, error) {
|
||||
ctx, cancel := context.WithTimeout(e.Ctx, etcdTimeout)
|
||||
defer cancel()
|
||||
r, e := e.Client.Get(ctx, path, &etcdc.GetOptions{Sort: false, Recursive: recursive})
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return r, e
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.(*etcdc.Response), err
|
||||
}
|
||||
|
||||
// skydns/local/skydns/east/staging/web
|
||||
// skydns/local/skydns/west/production/web
|
||||
//
|
||||
// skydns/local/skydns/*/*/web
|
||||
// skydns/local/skydns/*/web
|
||||
|
||||
// loopNodes recursively loops through the nodes and returns all the values. The nodes' keyname
|
||||
// will be match against any wildcards when star is true.
|
||||
func (e *Etcd) loopNodes(ns []*etcdc.Node, nameParts []string, star bool, bx map[msg.Service]bool) (sx []msg.Service, err error) {
|
||||
if bx == nil {
|
||||
bx = make(map[msg.Service]bool)
|
||||
}
|
||||
Nodes:
|
||||
for _, n := range ns {
|
||||
if n.Dir {
|
||||
nodes, err := e.loopNodes(n.Nodes, nameParts, star, bx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sx = append(sx, nodes...)
|
||||
continue
|
||||
}
|
||||
if star {
|
||||
keyParts := strings.Split(n.Key, "/")
|
||||
for i, n := range nameParts {
|
||||
if i > len(keyParts)-1 {
|
||||
// name is longer than key
|
||||
continue Nodes
|
||||
}
|
||||
if n == "*" || n == "any" {
|
||||
continue
|
||||
}
|
||||
if keyParts[i] != n {
|
||||
continue Nodes
|
||||
}
|
||||
}
|
||||
}
|
||||
serv := new(msg.Service)
|
||||
if err := json.Unmarshal([]byte(n.Value), serv); err != nil {
|
||||
return nil, fmt.Errorf("%s: %s", n.Key, err.Error())
|
||||
}
|
||||
b := msg.Service{Host: serv.Host, Port: serv.Port, Priority: serv.Priority, Weight: serv.Weight, Text: serv.Text, Key: n.Key}
|
||||
if _, ok := bx[b]; ok {
|
||||
continue
|
||||
}
|
||||
bx[b] = true
|
||||
|
||||
serv.Key = n.Key
|
||||
serv.TTL = e.TTL(n, serv)
|
||||
if serv.Priority == 0 {
|
||||
serv.Priority = priority
|
||||
}
|
||||
sx = append(sx, *serv)
|
||||
}
|
||||
return sx, nil
|
||||
}
|
||||
|
||||
// TTL returns the smaller of the etcd TTL and the service's
|
||||
// TTL. If neither of these are set (have a zero value), a default is used.
|
||||
func (e *Etcd) TTL(node *etcdc.Node, serv *msg.Service) uint32 {
|
||||
etcdTTL := uint32(node.TTL)
|
||||
|
||||
if etcdTTL == 0 && serv.TTL == 0 {
|
||||
return ttl
|
||||
}
|
||||
if etcdTTL == 0 {
|
||||
return serv.TTL
|
||||
}
|
||||
if serv.TTL == 0 {
|
||||
return etcdTTL
|
||||
}
|
||||
if etcdTTL < serv.TTL {
|
||||
return etcdTTL
|
||||
}
|
||||
return serv.TTL
|
||||
}
|
||||
|
||||
const (
|
||||
priority = 10 // default priority when nothing is set
|
||||
ttl = 300 // default ttl when nothing is set
|
||||
etcdTimeout = 5 * time.Second
|
||||
)
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
// +build etcd
|
||||
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/middleware/etcd/msg"
|
||||
"github.com/coredns/coredns/middleware/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestGroupLookup(t *testing.T) {
|
||||
etc := newEtcdMiddleware()
|
||||
|
||||
for _, serv := range servicesGroup {
|
||||
set(t, etc, serv.Key, 0, serv)
|
||||
defer delete(t, etc, serv.Key)
|
||||
}
|
||||
for _, tc := range dnsTestCasesGroup {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
_, err := etc.ServeDNS(ctxt, rec, m)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
resp := rec.Msg
|
||||
sort.Sort(test.RRSet(resp.Answer))
|
||||
sort.Sort(test.RRSet(resp.Ns))
|
||||
sort.Sort(test.RRSet(resp.Extra))
|
||||
|
||||
if !test.Header(t, tc, resp) {
|
||||
t.Logf("%v\n", resp)
|
||||
continue
|
||||
}
|
||||
if !test.Section(t, tc, test.Answer, resp.Answer) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Ns, resp.Ns) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
if !test.Section(t, tc, test.Extra, resp.Extra) {
|
||||
t.Logf("%v\n", resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note the key is encoded as DNS name, while in "reality" it is a etcd path.
|
||||
var servicesGroup = []*msg.Service{
|
||||
{Host: "127.0.0.1", Key: "a.dom.skydns.test.", Group: "g1"},
|
||||
{Host: "127.0.0.2", Key: "b.sub.dom.skydns.test.", Group: "g1"},
|
||||
|
||||
{Host: "127.0.0.1", Key: "a.dom2.skydns.test.", Group: "g1"},
|
||||
{Host: "127.0.0.2", Key: "b.sub.dom2.skydns.test.", Group: ""},
|
||||
|
||||
{Host: "127.0.0.1", Key: "a.dom1.skydns.test.", Group: "g1"},
|
||||
{Host: "127.0.0.2", Key: "b.sub.dom1.skydns.test.", Group: "g2"},
|
||||
}
|
||||
|
||||
var dnsTestCasesGroup = []test.Case{
|
||||
// Groups
|
||||
{
|
||||
// hits the group 'g1' and only includes those records
|
||||
Qname: "dom.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("dom.skydns.test. 300 IN A 127.0.0.1"),
|
||||
test.A("dom.skydns.test. 300 IN A 127.0.0.2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// One has group, the other has not... Include the non-group always.
|
||||
Qname: "dom2.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("dom2.skydns.test. 300 IN A 127.0.0.1"),
|
||||
test.A("dom2.skydns.test. 300 IN A 127.0.0.2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// The groups differ.
|
||||
Qname: "dom1.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("dom1.skydns.test. 300 IN A 127.0.0.1"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package etcd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/middleware/etcd/msg"
|
||||
"github.com/coredns/coredns/middleware/pkg/debug"
|
||||
"github.com/coredns/coredns/middleware/pkg/dnsutil"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ServeDNS implements the middleware.Handler interface.
|
||||
func (e *Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
opt := middleware.Options{}
|
||||
state := request.Request{W: w, Req: r}
|
||||
if state.QClass() != dns.ClassINET {
|
||||
return dns.RcodeServerFailure, middleware.Error(e.Name(), errors.New("can only deal with ClassINET"))
|
||||
}
|
||||
name := state.Name()
|
||||
if e.Debugging {
|
||||
if bug := debug.IsDebug(name); bug != "" {
|
||||
opt.Debug = r.Question[0].Name
|
||||
state.Clear()
|
||||
state.Req.Question[0].Name = bug
|
||||
}
|
||||
}
|
||||
|
||||
// We need to check stubzones first, because we may get a request for a zone we
|
||||
// are not auth. for *but* do have a stubzone forward for. If we do the stubzone
|
||||
// handler will handle the request.
|
||||
if e.Stubmap != nil && len(*e.Stubmap) > 0 {
|
||||
for zone := range *e.Stubmap {
|
||||
if middleware.Name(zone).Matches(name) {
|
||||
stub := Stub{Etcd: e, Zone: zone}
|
||||
return stub.ServeDNS(ctx, w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zone := middleware.Zones(e.Zones).Matches(state.Name())
|
||||
if zone == "" {
|
||||
if opt.Debug != "" {
|
||||
r.Question[0].Name = opt.Debug
|
||||
}
|
||||
return middleware.NextOrFailure(e.Name(), e.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
var (
|
||||
records, extra []dns.RR
|
||||
debug []msg.Service
|
||||
err error
|
||||
)
|
||||
switch state.Type() {
|
||||
case "A":
|
||||
records, debug, err = middleware.A(e, zone, state, nil, opt)
|
||||
case "AAAA":
|
||||
records, debug, err = middleware.AAAA(e, zone, state, nil, opt)
|
||||
case "TXT":
|
||||
records, debug, err = middleware.TXT(e, zone, state, opt)
|
||||
case "CNAME":
|
||||
records, debug, err = middleware.CNAME(e, zone, state, opt)
|
||||
case "PTR":
|
||||
records, debug, err = middleware.PTR(e, zone, state, opt)
|
||||
case "MX":
|
||||
records, extra, debug, err = middleware.MX(e, zone, state, opt)
|
||||
case "SRV":
|
||||
records, extra, debug, err = middleware.SRV(e, zone, state, opt)
|
||||
case "SOA":
|
||||
records, debug, err = middleware.SOA(e, zone, state, opt)
|
||||
case "NS":
|
||||
if state.Name() == zone {
|
||||
records, extra, debug, err = middleware.NS(e, zone, state, opt)
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
// Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN
|
||||
_, debug, err = middleware.A(e, zone, state, nil, opt)
|
||||
}
|
||||
|
||||
if opt.Debug != "" {
|
||||
// Substitute this name with the original when we return the request.
|
||||
state.Clear()
|
||||
state.Req.Question[0].Name = opt.Debug
|
||||
}
|
||||
|
||||
if e.IsNameError(err) {
|
||||
// Make err nil when returning here, so we don't log spam for NXDOMAIN.
|
||||
return middleware.BackendError(e, zone, dns.RcodeNameError, state, debug, nil /* err */, opt)
|
||||
}
|
||||
if err != nil {
|
||||
return middleware.BackendError(e, zone, dns.RcodeServerFailure, state, debug, err, opt)
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return middleware.BackendError(e, zone, dns.RcodeSuccess, state, debug, err, opt)
|
||||
}
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
|
||||
m.Answer = append(m.Answer, records...)
|
||||
m.Extra = append(m.Extra, extra...)
|
||||
if opt.Debug != "" {
|
||||
m.Extra = append(m.Extra, middleware.ServicesToTxt(debug)...)
|
||||
}
|
||||
|
||||
m = dnsutil.Dedup(m)
|
||||
state.SizeAndDo(m)
|
||||
m, _ = state.Scrub(m)
|
||||
w.WriteMsg(m)
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (e *Etcd) Name() string { return "etcd" }
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
// +build etcd
|
||||
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/middleware/etcd/msg"
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Note the key is encoded as DNS name, while in "reality" it is a etcd path.
|
||||
var services = []*msg.Service{
|
||||
{Host: "dev.server1", Port: 8080, Key: "a.server1.dev.region1.skydns.test."},
|
||||
{Host: "10.0.0.1", Port: 8080, Key: "a.server1.prod.region1.skydns.test."},
|
||||
{Host: "10.0.0.2", Port: 8080, Key: "b.server1.prod.region1.skydns.test."},
|
||||
{Host: "::1", Port: 8080, Key: "b.server6.prod.region1.skydns.test."},
|
||||
// Unresolvable internal name.
|
||||
{Host: "unresolvable.skydns.test", Key: "cname.prod.region1.skydns.test."},
|
||||
// Priority.
|
||||
{Host: "priority.server1", Priority: 333, Port: 8080, Key: "priority.skydns.test."},
|
||||
// Subdomain.
|
||||
{Host: "sub.server1", Port: 0, Key: "a.sub.region1.skydns.test."},
|
||||
{Host: "sub.server2", Port: 80, Key: "b.sub.region1.skydns.test."},
|
||||
{Host: "10.0.0.1", Port: 8080, Key: "c.sub.region1.skydns.test."},
|
||||
// Cname loop.
|
||||
{Host: "a.cname.skydns.test", Key: "b.cname.skydns.test."},
|
||||
{Host: "b.cname.skydns.test", Key: "a.cname.skydns.test."},
|
||||
// Nameservers.
|
||||
{Host: "10.0.0.2", Key: "a.ns.dns.skydns.test."},
|
||||
{Host: "10.0.0.3", Key: "b.ns.dns.skydns.test."},
|
||||
// Reverse.
|
||||
{Host: "reverse.example.com", Key: "1.0.0.10.in-addr.arpa."}, // 10.0.0.1
|
||||
}
|
||||
|
||||
var dnsTestCases = []test.Case{
|
||||
// SRV Test
|
||||
{
|
||||
Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{test.SRV("a.server1.dev.region1.skydns.test. 300 SRV 10 100 8080 dev.server1.")},
|
||||
},
|
||||
// SRV Test (case test)
|
||||
{
|
||||
Qname: "a.SERVer1.dEv.region1.skydns.tEst.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{test.SRV("a.SERVer1.dEv.region1.skydns.tEst. 300 SRV 10 100 8080 dev.server1.")},
|
||||
},
|
||||
// NXDOMAIN Test
|
||||
{
|
||||
Qname: "doesnotexist.skydns.test.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0"),
|
||||
},
|
||||
},
|
||||
// A Test
|
||||
{
|
||||
Qname: "a.server1.prod.region1.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{test.A("a.server1.prod.region1.skydns.test. 300 A 10.0.0.1")},
|
||||
},
|
||||
// SRV Test where target is IP address
|
||||
{
|
||||
Qname: "a.server1.prod.region1.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{test.SRV("a.server1.prod.region1.skydns.test. 300 SRV 10 100 8080 a.server1.prod.region1.skydns.test.")},
|
||||
Extra: []dns.RR{test.A("a.server1.prod.region1.skydns.test. 300 A 10.0.0.1")},
|
||||
},
|
||||
// AAAA Test
|
||||
{
|
||||
Qname: "b.server6.prod.region1.skydns.test.", Qtype: dns.TypeAAAA,
|
||||
Answer: []dns.RR{test.AAAA("b.server6.prod.region1.skydns.test. 300 AAAA ::1")},
|
||||
},
|
||||
// Multiple A Record Test
|
||||
{
|
||||
Qname: "server1.prod.region1.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("server1.prod.region1.skydns.test. 300 A 10.0.0.1"),
|
||||
test.A("server1.prod.region1.skydns.test. 300 A 10.0.0.2"),
|
||||
},
|
||||
},
|
||||
// Priority Test
|
||||
{
|
||||
Qname: "priority.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{test.SRV("priority.skydns.test. 300 SRV 333 100 8080 priority.server1.")},
|
||||
},
|
||||
// Subdomain Test
|
||||
{
|
||||
Qname: "sub.region1.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("sub.region1.skydns.test. 300 IN SRV 10 33 0 sub.server1."),
|
||||
test.SRV("sub.region1.skydns.test. 300 IN SRV 10 33 80 sub.server2."),
|
||||
test.SRV("sub.region1.skydns.test. 300 IN SRV 10 33 8080 c.sub.region1.skydns.test."),
|
||||
},
|
||||
Extra: []dns.RR{test.A("c.sub.region1.skydns.test. 300 IN A 10.0.0.1")},
|
||||
},
|
||||
// CNAME (unresolvable internal name)
|
||||
{
|
||||
Qname: "cname.prod.region1.skydns.test.", Qtype: dns.TypeA,
|
||||
Ns: []dns.RR{test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")},
|
||||
},
|
||||
// Wildcard Test
|
||||
{
|
||||
Qname: "*.region1.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 0 sub.server1."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 0 unresolvable.skydns.test."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 80 sub.server2."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 a.server1.prod.region1.skydns.test."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 b.server1.prod.region1.skydns.test."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 b.server6.prod.region1.skydns.test."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 c.sub.region1.skydns.test."),
|
||||
test.SRV("*.region1.skydns.test. 300 IN SRV 10 12 8080 dev.server1."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("a.server1.prod.region1.skydns.test. 300 IN A 10.0.0.1"),
|
||||
test.A("b.server1.prod.region1.skydns.test. 300 IN A 10.0.0.2"),
|
||||
test.AAAA("b.server6.prod.region1.skydns.test. 300 IN AAAA ::1"),
|
||||
test.A("c.sub.region1.skydns.test. 300 IN A 10.0.0.1"),
|
||||
},
|
||||
},
|
||||
// Wildcard Test
|
||||
{
|
||||
Qname: "prod.*.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{
|
||||
|
||||
test.SRV("prod.*.skydns.test. 300 IN SRV 10 25 0 unresolvable.skydns.test."),
|
||||
test.SRV("prod.*.skydns.test. 300 IN SRV 10 25 8080 a.server1.prod.region1.skydns.test."),
|
||||
test.SRV("prod.*.skydns.test. 300 IN SRV 10 25 8080 b.server1.prod.region1.skydns.test."),
|
||||
test.SRV("prod.*.skydns.test. 300 IN SRV 10 25 8080 b.server6.prod.region1.skydns.test."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("a.server1.prod.region1.skydns.test. 300 IN A 10.0.0.1"),
|
||||
test.A("b.server1.prod.region1.skydns.test. 300 IN A 10.0.0.2"),
|
||||
test.AAAA("b.server6.prod.region1.skydns.test. 300 IN AAAA ::1"),
|
||||
},
|
||||
},
|
||||
// Wildcard Test
|
||||
{
|
||||
Qname: "prod.any.skydns.test.", Qtype: dns.TypeSRV,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("prod.any.skydns.test. 300 IN SRV 10 25 0 unresolvable.skydns.test."),
|
||||
test.SRV("prod.any.skydns.test. 300 IN SRV 10 25 8080 a.server1.prod.region1.skydns.test."),
|
||||
test.SRV("prod.any.skydns.test. 300 IN SRV 10 25 8080 b.server1.prod.region1.skydns.test."),
|
||||
test.SRV("prod.any.skydns.test. 300 IN SRV 10 25 8080 b.server6.prod.region1.skydns.test."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("a.server1.prod.region1.skydns.test. 300 IN A 10.0.0.1"),
|
||||
test.A("b.server1.prod.region1.skydns.test. 300 IN A 10.0.0.2"),
|
||||
test.AAAA("b.server6.prod.region1.skydns.test. 300 IN AAAA ::1"),
|
||||
},
|
||||
},
|
||||
// CNAME loop detection
|
||||
{
|
||||
Qname: "a.cname.skydns.test.", Qtype: dns.TypeA,
|
||||
Ns: []dns.RR{test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 1407441600 28800 7200 604800 60")},
|
||||
},
|
||||
// NODATA Test
|
||||
{
|
||||
Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeTXT,
|
||||
Ns: []dns.RR{test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")},
|
||||
},
|
||||
// NODATA Test
|
||||
{
|
||||
Qname: "a.server1.dev.region1.skydns.test.", Qtype: dns.TypeHINFO,
|
||||
Ns: []dns.RR{test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")},
|
||||
},
|
||||
// NXDOMAIN Test
|
||||
{
|
||||
Qname: "a.server1.nonexistent.region1.skydns.test.", Qtype: dns.TypeHINFO, Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{test.SOA("skydns.test. 300 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")},
|
||||
},
|
||||
{
|
||||
Qname: "skydns.test.", Qtype: dns.TypeSOA,
|
||||
Answer: []dns.RR{test.SOA("skydns.test. 300 IN SOA ns.dns.skydns.test. hostmaster.skydns.test. 1460498836 14400 3600 604800 60")},
|
||||
},
|
||||
// NS Record Test
|
||||
{
|
||||
Qname: "skydns.test.", Qtype: dns.TypeNS,
|
||||
Answer: []dns.RR{
|
||||
test.NS("skydns.test. 300 NS a.ns.dns.skydns.test."),
|
||||
test.NS("skydns.test. 300 NS b.ns.dns.skydns.test."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("a.ns.dns.skydns.test. 300 A 10.0.0.2"),
|
||||
test.A("b.ns.dns.skydns.test. 300 A 10.0.0.3"),
|
||||
},
|
||||
},
|
||||
// NS Record Test
|
||||
{
|
||||
Qname: "a.skydns.test.", Qtype: dns.TypeNS, Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{test.SOA("skydns.test. 300 IN SOA ns.dns.skydns.test. hostmaster.skydns.test. 1460498836 14400 3600 604800 60")},
|
||||
},
|
||||
// A Record For NS Record Test
|
||||
{
|
||||
Qname: "ns.dns.skydns.test.", Qtype: dns.TypeA,
|
||||
Answer: []dns.RR{
|
||||
test.A("ns.dns.skydns.test. 300 A 10.0.0.2"),
|
||||
test.A("ns.dns.skydns.test. 300 A 10.0.0.3"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "skydns_extra.test.", Qtype: dns.TypeSOA,
|
||||
Answer: []dns.RR{test.SOA("skydns_extra.test. 300 IN SOA ns.dns.skydns_extra.test. hostmaster.skydns_extra.test. 1460498836 14400 3600 604800 60")},
|
||||
},
|
||||
// Reverse lookup
|
||||
{
|
||||
Qname: "1.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
|
||||
Answer: []dns.RR{test.PTR("1.0.0.10.in-addr.arpa. 300 PTR reverse.example.com.")},
|
||||
},
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue