├── .golangci.yml ├── go.mod ├── .dockerignore ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── main.go ├── ztapi ├── ztapi.go ├── member_test.go ├── network.go └── member.go ├── cmd ├── root.go └── server.go ├── zerotier-dns.example.yml ├── .github └── workflows │ └── build.yml ├── dnssrv └── dnssrv.go ├── README.md └── go.sum /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | errcheck: 3 | # fmt is default 4 | ignore: fmt:.*,github.com/spf13/viper:BindPFlag 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mje-nz/zerotier-dns 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/miekg/dns v1.1.15 7 | github.com/sirupsen/logrus v1.4.2 8 | github.com/spf13/cobra v0.0.5 9 | github.com/spf13/viper v1.4.0 10 | golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .git 3 | .gitignore 4 | *.md 5 | /.ztdns.toml 6 | 7 | # Binaries for programs and plugins 8 | /ztdns 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, build with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zerotier-dns.yml 2 | zerotier-dns.toml 3 | 4 | .vscode/ 5 | 6 | # Binaries for programs and plugins 7 | /zerotier-dns 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, build with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | .DS_Store 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/golangci/golangci-lint 3 | rev: v1.19.1 4 | hooks: 5 | - id: golangci-lint 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v2.2.3 8 | hooks: 9 | - id: check-byte-order-marker 10 | - id: check-case-conflict 11 | - id: check-executables-have-shebangs 12 | - id: check-merge-conflict 13 | - id: check-symlinks 14 | - id: check-yaml 15 | - id: end-of-file-fixer 16 | - id: trailing-whitespace 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.13 2 | FROM golang:${GO_VERSION} AS builder 3 | 4 | RUN useradd zerotier-dns 5 | 6 | WORKDIR /go/src/github.com/mje-nz/zerotier-dns 7 | 8 | # Fetch and cache dependencies 9 | COPY ./go.mod ./go.sum ./ 10 | RUN go mod download 11 | 12 | # Build static binary and allow it to bind to ports <1000 13 | COPY . . 14 | RUN CGO_ENABLED=0 GOOS=linux go install -ldflags="-w -s" && \ 15 | # NB Only works on BuildKit 16 | # https://github.com/moby/moby/issues/35699 17 | setcap cap_net_bind_service=+ep /go/bin/zerotier-dns 18 | 19 | 20 | 21 | FROM scratch 22 | 23 | # We need ca-certificates for HTTPS, /etc/passwd to log in, and zoneinfo for 24 | # time zones 25 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 26 | COPY --from=builder /etc/passwd /etc/passwd 27 | COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo 28 | 29 | WORKDIR /app 30 | COPY --from=builder /go/bin/zerotier-dns . 31 | 32 | USER zerotier-dns 33 | 34 | ENTRYPOINT ["./zerotier-dns"] 35 | CMD ["server"] 36 | EXPOSE 53/udp 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Matthew Edwards 4 | Copyright (c) 2017 uxbh 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Matthew Edwards 2 | // Copyright © 2017 uxbh 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | package main 23 | 24 | import "github.com/mje-nz/zerotier-dns/cmd" 25 | 26 | func main() { 27 | cmd.Execute() 28 | } 29 | -------------------------------------------------------------------------------- /ztapi/ztapi.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 uxbh 2 | 3 | // Package ztapi implements a (partial) API client to a ZeroTier service. 4 | package ztapi 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "net/http" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | type apiTime struct { 15 | time time.Time 16 | } 17 | 18 | func (t *apiTime) UnmarshalJSON(b []byte) error { 19 | l := len(b) 20 | if l < 3 { 21 | n, err := strconv.ParseInt(string(b), 10, 32) 22 | if err != nil { 23 | return err 24 | } 25 | t.time = time.Unix(0, n) 26 | return nil 27 | } 28 | n, err := strconv.ParseInt(string(b[l-3:]), 10, 32) 29 | if err != nil { 30 | return err 31 | } 32 | s, err := strconv.ParseInt(string(b[:l-3]), 10, 32) 33 | if err != nil { 34 | return err 35 | } 36 | t.time = time.Unix(s, n) 37 | return nil 38 | } 39 | 40 | // getJSON makes a request to the ZeroTier API and returns a JSON object 41 | func getJSON(url, APIToken string, target interface{}) error { 42 | if APIToken == "" { 43 | return fmt.Errorf("API Error: No APIToken provided") 44 | } 45 | 46 | c := &http.Client{} 47 | req, _ := http.NewRequest("GET", url, nil) 48 | req.Header.Set("Authorization", "bearer "+APIToken) 49 | r, err := c.Do(req) 50 | if err != nil { 51 | return fmt.Errorf("API Error: %s", err.Error()) 52 | } 53 | if r.StatusCode != 200 { 54 | return fmt.Errorf("API returned error: %s", r.Status) 55 | } 56 | defer r.Body.Close() 57 | return json.NewDecoder(r.Body).Decode(target) 58 | } 59 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Package cmd implments the zerotier-dns command-line interface. 2 | package cmd 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | var cfgFile string 14 | 15 | var RootCmd = &cobra.Command{ 16 | Use: "zerotier-dns", 17 | Short: "ZeroTier DNS Server", 18 | Long: `zerotier-dns is a DNS server for ZeroTier virtual networks. 19 | This application will serve DNS requests for the members of a ZeroTier 20 | network for both A (IPv4) and AAAA (IPv6) requests`, 21 | } 22 | 23 | func Execute() { 24 | if err := RootCmd.Execute(); err != nil { 25 | fmt.Println(err) 26 | os.Exit(-1) 27 | } 28 | } 29 | 30 | func init() { 31 | cobra.OnInitialize(initConfig) 32 | RootCmd.PersistentFlags().Bool("debug", false, "enable debug messages") 33 | RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is zerotier-dns.yml)") 34 | viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")) 35 | 36 | } 37 | 38 | func initConfig() { 39 | if cfgFile != "" { 40 | // Use specified config file 41 | viper.SetConfigFile(cfgFile) 42 | } else { 43 | // Find config file in current directory or $HOME 44 | viper.SetConfigName("zerotier-dns") 45 | viper.AddConfigPath(".") 46 | viper.AddConfigPath("$HOME") 47 | } 48 | 49 | // Enable setting config values with ZTDNS_KEY environment variables 50 | viper.SetEnvPrefix("ztdns") 51 | viper.AutomaticEnv() 52 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 53 | 54 | if err := viper.ReadInConfig(); err != nil { 55 | fmt.Println("Can't read config:", err) 56 | os.Exit(1) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ztapi/member_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 uxbh 2 | 3 | package ztapi 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | func BenchmarkGet6Plane(b *testing.B) { 10 | member := Member{ 11 | NetworkID: "8056C2E21C24673D", 12 | NodeID: "22c55a1da6", 13 | } 14 | for n := 0; n < b.N; n++ { 15 | member.Get6Plane() 16 | } 17 | } 18 | 19 | func BenchmarkGetRFC4193(b *testing.B) { 20 | member := Member{ 21 | NetworkID: "8056C2E21C24673D", 22 | NodeID: "22c55a1da6", 23 | } 24 | for n := 0; n < b.N; n++ { 25 | member.GetRFC4193() 26 | } 27 | } 28 | 29 | func TestGetRFC4193(t *testing.T) { 30 | var table = []struct { 31 | net string 32 | node string 33 | out string 34 | }{ 35 | {"1", "1", "fd00::199:9300:0:1"}, 36 | {"FFFFFFFFFFFFFFFF", "ffffffffff", "fdff:ffff:ffff:ffff:ff99:93ff:ffff:ffff"}, 37 | {"8056C2E21C24673D", "22c55a1da6", "fd80:56c2:e21c:2467:3d99:9322:c55a:1da6"}, 38 | } 39 | for _, tt := range table { 40 | member := Member{ 41 | NetworkID: tt.net, 42 | NodeID: tt.node, 43 | } 44 | s := member.GetRFC4193().String() 45 | if s != tt.out { 46 | t.Errorf("GetRFC4193\nin(%x,%x)\n got %v\nwant %v", tt.net, tt.node, s, tt.out) 47 | } 48 | } 49 | } 50 | func TestGet6Plane(t *testing.T) { 51 | var table = []struct { 52 | net string 53 | node string 54 | out string 55 | }{ 56 | {"1", "1", "fc00:0:100:0:1::1"}, 57 | {"FFFFFFFFFFFFFFFF", "ffffffffff", "fc00:0:ff:ffff:ffff::1"}, 58 | {"8056C2E21C24673D", "22c55a1da6", "fc9c:72a5:df22:c55a:1da6::1"}, 59 | } 60 | for _, tt := range table { 61 | member := Member{ 62 | NetworkID: tt.net, 63 | NodeID: tt.node, 64 | } 65 | s := member.Get6Plane().String() 66 | if s != tt.out { 67 | t.Errorf("Get6Plane\nin(%x,%x)\n got %v\nwant %v", tt.net, tt.node, s, tt.out) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /zerotier-dns.example.yml: -------------------------------------------------------------------------------- 1 | # This file contains all supported configuration options and their default values. 2 | 3 | # Network interface to bind to (or "" to bind to all interfaces). By default, only 4 | # respond on the ZeroTier interface. On macOS, there are two ZeroTier interfaces 5 | # whose names start with "feth" and you should specify the one with the lower number 6 | # (see https://www.zerotier.com/2019/08/21/how-zerotier-eliminated-kernel-extensions-on-macos/ ). 7 | interface: "zt0" 8 | 9 | # Port to listen on. 10 | port: 53 11 | 12 | # Base domain. Could be a top-level domain for internal use only (e.g., zt) or 13 | # a domain name with one or more subdomains (e.g., internal.yourdomain.com). 14 | # By default, map members to ".zt". 15 | domain: "zt" 16 | 17 | # How often to poll the ZeroTier controller in minutes. 18 | refresh: 30 19 | 20 | # Include members that are currently offline. 21 | include-offline: true 22 | 23 | # Enable debug messages. 24 | debug: false 25 | 26 | # An API key for your ZeroTier account (required). 27 | api-key: "" 28 | 29 | # The base API URL for the ZeroTier controller. 30 | api-url: "https://my.zerotier.com/api" 31 | 32 | # ID of the ZeroTier network. Only one of "network" and "networks" can be 33 | # specified. E.g., if domain="zt" and there is a network with ID "123abc" then 34 | # this would map its members to ".zt": 35 | # network: "123abc" 36 | network: 37 | 38 | # Mappings between subdomains and ZeroTier network IDs. Only one of "network" 39 | # and "networks" can be specified. 40 | networks: 41 | # E.g., if domain="zt" and there is a network with ID "123abc" then this would 42 | # map its members to ".home.zt": 43 | # home: "123abc" 44 | 45 | # Mappings between round-robin names and regexps to match members. Names are 46 | # matched within each network (i.e., if there are members matching a mapping in 47 | # multiple networks then the name will be defined separately in each). 48 | round-robin: 49 | # E.g., if the "home" network defined above had members "k8s-node-23refw" and 50 | # "k8s-node-09sf8g" this would create a name "k8s-nodes.home.zt" returning one 51 | # of them at random: 52 | # k8s-nodes: "k8s-node-\w" 53 | -------------------------------------------------------------------------------- /ztapi/network.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 uxbh 2 | 3 | package ztapi 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | ) 9 | 10 | // GetNetworkInfo returns a Nework containing information about a ZeroTier network 11 | func GetNetworkInfo(API, host, networkID string) (*Network, error) { 12 | resp := new(Network) 13 | url := fmt.Sprintf("%s/network/%s", host, networkID) 14 | err := getJSON(url, API, resp) 15 | if err != nil { 16 | return nil, fmt.Errorf("unable to get network info: %s", err.Error()) 17 | } 18 | return resp, nil 19 | } 20 | 21 | // Network contains the JSON response for a request for a network 22 | type Network struct { 23 | ID string 24 | Type string 25 | Clock apiTime 26 | UI struct { 27 | FlowRulesCollapsed bool 28 | MembersCollapsed bool 29 | MembersHelpCollapsed bool 30 | RulesHelpCollapsed bool 31 | SettingsCollapsed bool 32 | SettingsHelpCollapsed bool 33 | V4EasyMode bool 34 | } 35 | Config struct { 36 | ActiveMemberCount int 37 | AuthorizedMemberCount int 38 | Capabilities []json.RawMessage 39 | Clock apiTime 40 | CreationTime apiTime 41 | EnableBroadcast bool 42 | ID string 43 | IPAssignmentPools []struct { 44 | IPRangeEnd string 45 | IPRangeStart string 46 | } 47 | MulticastLimit int 48 | Name string 49 | Nwid string 50 | Objtype string 51 | Private bool 52 | Revision int 53 | Routes []struct { 54 | Target string 55 | Via string 56 | } 57 | Rules []struct { 58 | EtherType int 59 | Not bool 60 | Or bool 61 | Type string 62 | } 63 | Tags []json.RawMessage 64 | TotalMemberCount int 65 | V4AssignMode struct { 66 | Zt bool 67 | } 68 | V6AssignMode struct { 69 | Sixplane bool `json:"6plane"` 70 | Rfc4193 bool 71 | Zt bool 72 | } 73 | } 74 | RuleSource string 75 | Description string 76 | Permissions map[string]struct { 77 | A bool 78 | D bool 79 | M bool 80 | R bool 81 | T string 82 | } 83 | OnlineMemberCount int 84 | CapabilitesByName map[string]string 85 | TagsByName map[string]json.RawMessage 86 | CircuitTestEvery int 87 | } 88 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # See: 2 | # https://github.com/elgohr/Publish-Docker-Github-Action 3 | # https://andrewlock.net/caching-docker-layers-on-serverless-build-hosts-with-multi-stage-builds---target,-and---cache-from/ 4 | # https://docs.docker.com/docker-hub/access-tokens/ 5 | 6 | # I would use a Docker Hub automated build instead of this, but I need BuildKit 7 | # (https://github.com/moby/moby/issues/35699) 8 | # and the Docker version on Docker Hub is super outdated. Oh well, this is 9 | # faster anyway. 10 | 11 | name: Check, build 12 | on: 13 | push: 14 | release: 15 | types: [published] 16 | 17 | jobs: 18 | Check: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@master 22 | - uses: actions/setup-python@master 23 | - name: Install and run pre-commit 24 | run: | 25 | pip install pre-commit 26 | pre-commit run -a -v 27 | Build: 28 | needs: check 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@master 32 | - name: Install latest Docker 33 | run: | 34 | curl -fsSL https://get.docker.com | sudo sh 35 | - name: Log in to registry 36 | env: 37 | USERNAME: mjenz 38 | PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 39 | run: | 40 | echo ${PASSWORD} | docker login -u ${USERNAME} --password-stdin 41 | - name: Fetch dependencies 42 | run: | 43 | docker pull mjenz/zerotier-dns:builder || true 44 | DOCKER_BUILDKIT=1 docker build \ 45 | --target builder \ 46 | --cache-from mjenz/zerotier-dns:builder \ 47 | -t mjenz/zerotier-dns:builder \ 48 | . 49 | docker push mjenz/zerotier-dns:builder 50 | - name: Construct Docker image name 51 | if: github.event_name == 'push' 52 | run: | 53 | echo "IMAGE=mjenz/zerotier-dns:latest" >> $GITHUB_ENV 54 | - name: Construct Docker image name 55 | if: github.event_name == 'release' 56 | run: | 57 | echo "IMAGE=mjenz/zerotier-dns:${{ github.event.release.tag_name }}" >> $GITHUB_ENV 58 | - name: Build image 59 | run: | 60 | DOCKER_BUILDKIT=1 docker build \ 61 | --cache-from mjenz/zerotier-dns:builder \ 62 | -t $IMAGE \ 63 | . 64 | - name: Push image 65 | if: (github.ref == 'refs/heads/master' || github.event_name == 'release') 66 | run: | 67 | docker push $IMAGE 68 | - name: Update Docker Hub description 69 | if: github.ref == 'refs/heads/master' 70 | uses: peter-evans/dockerhub-description@v2.4.1 71 | with: 72 | username: mjenz 73 | password: ${{ secrets.DOCKER_PASSWORD }} 74 | repository: mjenz/zerotier-dns 75 | -------------------------------------------------------------------------------- /dnssrv/dnssrv.go: -------------------------------------------------------------------------------- 1 | // Package dnssrv implements a simple DNS server. 2 | package dnssrv 3 | 4 | import ( 5 | "fmt" 6 | "math/rand" 7 | "net" 8 | "strings" 9 | "time" 10 | 11 | "github.com/miekg/dns" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // Records contains the types of records the server will respond to. 16 | type Records struct { 17 | A []net.IP 18 | AAAA []net.IP 19 | } 20 | 21 | // DNSUpdate is the last time the DNSDatabase was updated. 22 | var DNSUpdate = time.Time{} 23 | 24 | // DNSDatabase is a map of hostnames to the records associated with it. 25 | var DNSDatabase = map[string]Records{} 26 | 27 | var queryChan chan string 28 | 29 | // Start brings up a DNS server for the specified suffix on a given port. 30 | func Start(iface string, port int, suffix string, req chan string) { 31 | queryChan = req 32 | 33 | if port == 0 { 34 | port = 53 35 | } 36 | 37 | // attach request handler func 38 | dns.HandleFunc(suffix+".", handleDNSRequest) 39 | 40 | for _, addr := range getIfaceAddrs(iface) { 41 | go func(suffix string, addr net.IP, port int) { 42 | var server *dns.Server 43 | if addr.To4().String() == addr.String() { 44 | log.Debugf("Creating IPv4 Server: %s:%d udp", addr, port) 45 | server = &dns.Server{ 46 | Addr: fmt.Sprintf("%s:%d", addr, port), 47 | Net: "udp", 48 | } 49 | } else { 50 | log.Debugf("Creating IPv6 Server: [%s]:%d udp6", addr, port) 51 | server = &dns.Server{ 52 | Addr: fmt.Sprintf("[%s]:%d", addr, port), 53 | Net: "udp6", 54 | } 55 | } 56 | log.Printf("Starting server for %s on %s", suffix, server.Addr) 57 | err := server.ListenAndServe() 58 | if err != nil { 59 | log.Fatalf("Failed to start DNS server: %s", err.Error()) 60 | } 61 | defer func () { 62 | err := server.Shutdown() 63 | if err != nil { 64 | log.Fatalf("Failed to stop DNS server: %s", err.Error()) 65 | } 66 | }() 67 | }(suffix, addr, port) 68 | } 69 | } 70 | 71 | func getIfaceAddrs(iface string) []net.IP { 72 | if iface != "" { 73 | retaddrs := []net.IP{} 74 | netint, err := net.InterfaceByName(iface) 75 | if err != nil { 76 | log.Fatalf("Could not get interface: %s\n", err.Error()) 77 | } 78 | addrs, err := netint.Addrs() 79 | if err != nil { 80 | log.Fatalf("Could not get addresses: %s\n", err.Error()) 81 | } 82 | for _, addr := range addrs { 83 | ip, _, err := net.ParseCIDR(addr.String()) 84 | if err != nil { 85 | continue 86 | } 87 | if !ip.IsLinkLocalUnicast() { 88 | log.Debugf("Found address: %s", ip.String()) 89 | retaddrs = append(retaddrs, ip) 90 | } 91 | } 92 | return retaddrs 93 | } 94 | return []net.IP{net.IPv4zero} 95 | } 96 | 97 | // handleDNSRequest routes an incoming DNS request to a parser. 98 | func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { 99 | m := new(dns.Msg) 100 | m.SetReply(r) 101 | m.Compress = false 102 | m.Authoritative = true 103 | 104 | switch r.Opcode { 105 | case dns.OpcodeQuery: 106 | parseQuery(m) 107 | } 108 | 109 | if err := w.WriteMsg(m); err != nil { 110 | log.Errorf("Failed to send response: %s", err.Error()) 111 | } 112 | } 113 | 114 | // parseQuery reads and creates an answer to a DNS query. 115 | func parseQuery(m *dns.Msg) { 116 | for _, q := range m.Question { 117 | queryChan <- q.Name 118 | if rec, ok := DNSDatabase[strings.ToLower(q.Name)]; ok { 119 | switch q.Qtype { 120 | case dns.TypeA: 121 | for _, ip := range shuffle(rec.A) { 122 | rr, err := dns.NewRR(fmt.Sprintf("%s A %s", q.Name, ip.String())) 123 | if err == nil { 124 | m.Answer = append(m.Answer, rr) 125 | } 126 | } 127 | case dns.TypeAAAA: 128 | for _, ip := range shuffle(rec.AAAA) { 129 | rr, err := dns.NewRR(fmt.Sprintf("%s AAAA %s", q.Name, ip.String())) 130 | if err == nil { 131 | m.Answer = append(m.Answer, rr) 132 | } 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | // shuffle ip addresses for Round Robin dns 140 | func shuffle(ips []net.IP) []net.IP { 141 | ipsLength := len(ips) 142 | 143 | if ipsLength < 2 { 144 | return ips 145 | } 146 | 147 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 148 | ret := make([]net.IP, ipsLength) 149 | perm := r.Perm(ipsLength) 150 | 151 | for i, randIndex := range perm { 152 | ret[i] = ips[randIndex] 153 | } 154 | 155 | return ret 156 | } 157 | -------------------------------------------------------------------------------- /ztapi/member.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 uxbh 2 | 3 | package ztapi 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "net" 9 | "strconv" 10 | ) 11 | 12 | // GetMemberInfo returns a Member containing informationa about a specific member in a ZeroTier network 13 | func GetMemberInfo(API, host, networkID, memberID string) (*Member, error) { 14 | resp := new(Member) 15 | url := fmt.Sprintf("%s/network/%s/member/%s", host, networkID, memberID) 16 | err := getJSON(url, API, resp) 17 | if err != nil { 18 | return nil, fmt.Errorf("unable to get member info: %s", err.Error()) 19 | } 20 | return resp, nil 21 | } 22 | 23 | // GetMemberList gets a Slice of Members in a ZeroTier network 24 | func GetMemberList(API, host, networkID string) (*Members, error) { 25 | resp := new(Members) 26 | url := fmt.Sprintf("%s/network/%s/member", host, networkID) 27 | err := getJSON(url, API, resp) 28 | if err != nil { 29 | return nil, fmt.Errorf("unable to get member list: %s", err.Error()) 30 | } 31 | return resp, nil 32 | } 33 | 34 | // Members is a List of Members 35 | type Members []Member 36 | 37 | // Member contains data from a Member request 38 | type Member struct { 39 | ID string 40 | Type string 41 | Clock apiTime 42 | NetworkID string 43 | NodeID string 44 | ControllerID string 45 | Hidden bool 46 | Name string 47 | Online bool 48 | Description string 49 | Config struct { 50 | ActiveBridge bool 51 | Address string 52 | AuthHistory []struct { 53 | A bool 54 | By string 55 | C string 56 | Ct string 57 | Ts int 58 | } 59 | Authorized bool 60 | Capabilities []json.RawMessage 61 | CreationTime apiTime 62 | ID string 63 | Identity string 64 | IPAssignments []string 65 | LastAuthorizedTime apiTime 66 | LastDeauthorizedTime apiTime 67 | NoAutoAssignIPs bool 68 | Nwid string 69 | Objtype string 70 | PhysicalAddr string 71 | Revision int 72 | Tags []json.RawMessage 73 | VMajor int 74 | VMinor int 75 | VProto int 76 | VRev int 77 | } 78 | LastOnline apiTime 79 | LastOffline apiTime 80 | PhysicalAddress string 81 | PhysicalLocation []float64 82 | ClientVersion string 83 | OfflineNotifyDelay int 84 | ProtocolVersion int 85 | SupportsCircuitTesting bool 86 | SupportsRulesEngine bool 87 | } 88 | 89 | // Get6Plane returns the 6Plane address for a given network member. 90 | // See https://support.zerotier.com/hc/en-us/articles/115001080308-ZeroTier-6PLANE-IPv6-Addressing for details. 91 | // If the ZeroTier network assigns 6Plane addresses, this will be the device's address range. 92 | func (m *Member) Get6Plane() net.IP { 93 | n, _ := strconv.ParseUint(m.NetworkID, 16, 64) 94 | d, _ := strconv.ParseUint(m.NodeID, 16, 64) 95 | s := n&(0xFFFFFFFF<<0x20)>>0x20 ^ n&0xFFFFFFFF 96 | ip := net.IP{ 97 | 0xfc, byte(s >> 0x18), 98 | byte((s >> 0x10) - ((s >> 0x18) << 0x8)), 99 | byte((s >> 0x8) - ((s >> 0x10) << 0x8)), 100 | byte((s) - ((s >> 0x8) << 0x8)), 101 | byte(d >> 0x20), 102 | byte((d >> 0x18) - ((d >> 0x20) << 0x8)), 103 | byte((d >> 0x10) - ((d >> 0x18) << 0x8)), 104 | byte((d >> 0x08) - ((d >> 0x10) << 0x8)), 105 | byte((d >> 0x00) - ((d >> 0x08) << 0x8)), 106 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 107 | } 108 | return ip 109 | } 110 | 111 | // GetRFC4193 returns the RFC4193 address of a given network member. 112 | // If the ZeroTier network assigns RFC4193 addresses, this will be the device's address. 113 | func (m *Member) GetRFC4193() net.IP { 114 | n, _ := strconv.ParseUint(m.NetworkID, 16, 64) 115 | d, _ := strconv.ParseUint(m.NodeID, 16, 64) 116 | ip := net.IP{ 117 | 0xfd, byte(n >> 0x38), 118 | byte((n >> 0x30) - ((n >> 0x38) << 0x8)), 119 | byte((n >> 0x28) - ((n >> 0x30) << 0x8)), 120 | byte((n >> 0x20) - ((n >> 0x28) << 0x8)), 121 | byte((n >> 0x18) - ((n >> 0x20) << 0x8)), 122 | byte((n >> 0x10) - ((n >> 0x18) << 0x8)), 123 | byte((n >> 0x08) - ((n >> 0x10) << 0x8)), 124 | byte((n >> 0x00) - ((n >> 0x08) << 0x8)), 125 | 0x99, 0x93, byte(d >> 0x20), 126 | byte((d >> 0x18) - ((d >> 0x20) << 0x8)), 127 | byte((d >> 0x10) - ((d >> 0x18) << 0x8)), 128 | byte((d >> 0x08) - ((d >> 0x10) << 0x8)), 129 | byte((d >> 0x00) - ((d >> 0x08) << 0x8)), 130 | } 131 | return ip 132 | } 133 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "regexp" 7 | "strings" 8 | "time" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | "github.com/mje-nz/zerotier-dns/dnssrv" 14 | "github.com/mje-nz/zerotier-dns/ztapi" 15 | ) 16 | 17 | // serverCmd represents the server command. 18 | var serverCmd = &cobra.Command{ 19 | Use: "server", 20 | Short: "Run zerotier-dns server", 21 | Long: `Start the zerotier-dns DNS server.`, 22 | PreRunE: func(cmd *cobra.Command, args []string) error { 23 | // Check config and bail if anything important is missing. 24 | if viper.GetBool("debug") { 25 | log.SetLevel(log.DebugLevel) 26 | } 27 | log.Debug("debug: ", viper.GetBool("debug")) 28 | log.Debugf("interface: %s", viper.GetString("interface")) 29 | log.Debug("port: ", viper.GetInt("port")) 30 | log.Debugf("domain: %q", viper.GetString("domain")) 31 | log.Debug("refresh: ", viper.GetInt("refresh")) 32 | log.Debug("include-offline: ", viper.GetBool("include-offline")) 33 | log.Debugf("api-key: %q", viper.GetString("api-key")) 34 | log.Debugf("api-url: %q", viper.GetString("api-url")) 35 | log.Debugf("network: %q", viper.GetString("network")) 36 | log.Debugf("networks: %#v", viper.GetStringMapString("networks")) 37 | log.Debugf("round-robin: %#v", viper.GetStringMapString("round-robin")) 38 | 39 | if viper.GetString("api-key") == "" { 40 | return fmt.Errorf("no API key provided") 41 | } 42 | 43 | if viper.GetString("network") == "" && !viper.IsSet("networks") { 44 | return fmt.Errorf("no networks configured (specify network or networks)") 45 | } 46 | if viper.GetString("network") != "" && viper.IsSet("networks") { 47 | return fmt.Errorf("conflicting network configuration (specify one of network or networks)") 48 | } 49 | 50 | return nil 51 | }, 52 | Run: func(cmd *cobra.Command, args []string) { 53 | // Update the DNSDatabase 54 | lastUpdate := updateDNS() 55 | req := make(chan string) 56 | // Start the DNS server 57 | go dnssrv.Start(viper.GetString("interface"), viper.GetInt("port"), viper.GetString("domain"), req) 58 | 59 | refresh := viper.GetInt("refresh") 60 | for { 61 | // Block until a new request comes in 62 | n := <-req 63 | log.Debugf("Got request for %s", n) 64 | // If the database hasn't been updated in the last "refresh" minutes, update it. 65 | if time.Since(lastUpdate) > time.Duration(refresh)*time.Minute { 66 | log.Infof("DNSDatabase is stale. Refreshing.") 67 | lastUpdate = updateDNS() 68 | } 69 | } 70 | }, 71 | } 72 | 73 | func init() { 74 | log.SetFormatter(&log.TextFormatter{ 75 | TimestampFormat: "2006-01-02 15:04:05 -07:00", 76 | }) 77 | 78 | RootCmd.AddCommand(serverCmd) 79 | serverCmd.Flags().String("interface", "zt0", "network interface to bind to") 80 | serverCmd.Flags().Int("port", 53, "port to listen on") 81 | serverCmd.Flags().String("domain", "zt", "domain to serve names under") 82 | serverCmd.Flags().Int("refresh", 30, "how often to poll the ZeroTier controller in minutes") 83 | serverCmd.Flags().Bool("include-offline", true, "include offline members") 84 | serverCmd.Flags().String("api-key", "", "ZeroTier API key") 85 | serverCmd.Flags().String("api-url", "https://my.zerotier.com/api", "ZeroTier API URL") 86 | serverCmd.Flags().String("network", "", "ZeroTier Network ID") 87 | viper.BindPFlags(serverCmd.Flags()) 88 | } 89 | 90 | // TODO add tests 91 | 92 | // memberNameToDNSLabel converts a ZeroTier member name into a valid DNS label. 93 | // See RFC 1035 section 2.3.1 (https://tools.ietf.org/html/rfc1035). 94 | func memberNameToDNSLabel(name string) string { 95 | // Convert to lower-case so lookup is case-insensitive 96 | name = strings.ToLower(name) 97 | // Labels may consist of letters, digits and hyphens 98 | name = strings.ReplaceAll(name, " ", "-") 99 | re := regexp.MustCompile("[^a-z0-9-]+") 100 | name = re.ReplaceAllString(name, "") 101 | // TODO must start with a letter 102 | // TODO must end with a letter or digit 103 | // TODO must be 63 characters or less 104 | return name 105 | } 106 | 107 | // TODO: refactor 108 | func updateDNS() time.Time { 109 | // Get config info 110 | apiKey := viper.GetString("api-key") 111 | apiUrl := viper.GetString("api-url") 112 | rootDomain := viper.GetString("domain") 113 | includeOffline := viper.GetBool("include-offline") 114 | 115 | roundRobinPatterns := make(map[string]*regexp.Regexp) 116 | roundRobinRecords := make(map[string][]dnssrv.Records) 117 | 118 | for subdomain, re := range viper.GetStringMapString("round-robin") { 119 | roundRobinPatterns[subdomain] = regexp.MustCompile(re) 120 | } 121 | 122 | networks := viper.GetStringMapString("networks") 123 | if len(networks) == 0 { 124 | networks = map[string]string{"": viper.GetString("network")} 125 | } 126 | 127 | // Get all configured networks: 128 | for domain, networkID := range networks { 129 | // TODO: Handle configuration with dots 130 | suffix := "." + rootDomain + "." 131 | if domain != "" { 132 | suffix = "." + domain + suffix 133 | } 134 | 135 | ztnetwork, err := ztapi.GetNetworkInfo(apiKey, apiUrl, networkID) 136 | if err != nil { 137 | log.Fatalf("Unable to get network info: %s", err.Error()) 138 | } 139 | 140 | log.Infof("Getting members of network: %s (%s)", ztnetwork.Config.Name, suffix) 141 | lst, err := ztapi.GetMemberList(apiKey, apiUrl, ztnetwork.ID) 142 | if err != nil { 143 | log.Fatalf("Unable to get member list: %s", err.Error()) 144 | } 145 | log.Debugf("Got %d members", len(*lst)) 146 | 147 | for _, n := range *lst { 148 | if includeOffline || n.Online { 149 | // Sanitize member name 150 | name := memberNameToDNSLabel(n.Name) 151 | fqdn := name + suffix 152 | 153 | // Clear current DNS records 154 | dnssrv.DNSDatabase[fqdn] = dnssrv.Records{} 155 | ip6 := []net.IP{} 156 | ip4 := []net.IP{} 157 | // Get 6Plane address if network has it enabled 158 | if ztnetwork.Config.V6AssignMode.Sixplane { 159 | ip6 = append(ip6, n.Get6Plane()) 160 | } 161 | // Get RFC4193 address if network has it enabled 162 | if ztnetwork.Config.V6AssignMode.Rfc4193 { 163 | ip6 = append(ip6, n.GetRFC4193()) 164 | } 165 | 166 | // Get the rest of the address assigned to the member 167 | for _, a := range n.Config.IPAssignments { 168 | ip4 = append(ip4, net.ParseIP(a)) 169 | } 170 | 171 | dnsRecord := dnssrv.Records{ 172 | A: ip4, 173 | AAAA: ip6, 174 | } 175 | 176 | // Add the FQDN to the database 177 | log.Infof("Updating %-20s IPv4: %-15s IPv6: %s", fqdn, ip4, ip6) 178 | dnssrv.DNSDatabase[fqdn] = dnsRecord 179 | 180 | // Add a record for any round-robin names it matches the pattern for 181 | for subdomain, re := range roundRobinPatterns { 182 | if match := re.FindStringSubmatch(name); match != nil { 183 | roundRobinFQDN := subdomain + suffix 184 | log.Debugf("Adding member %s to RR record %s IPv4: %-15s IPv6: %s", name, roundRobinFQDN, ip4, ip6) 185 | roundRobinRecords[roundRobinFQDN] = append(roundRobinRecords[roundRobinFQDN], dnsRecord) 186 | } 187 | } 188 | } 189 | } 190 | 191 | // Collect round-robin records and add to main database 192 | // TODO: Any reason this needs to be separate? 193 | for roundRobinFQDN, records := range roundRobinRecords { 194 | dnsRecord := dnssrv.Records{} 195 | for _, ips := range records { 196 | dnsRecord.A = append(dnsRecord.A, ips.A...) 197 | dnsRecord.AAAA = append(dnsRecord.AAAA, ips.AAAA...) 198 | } 199 | 200 | log.Infof("Updating %-20s IPv4: %-15s IPv6: %s", roundRobinFQDN, dnsRecord.A, dnsRecord.AAAA) 201 | dnssrv.DNSDatabase[roundRobinFQDN] = dnsRecord 202 | } 203 | } 204 | 205 | // Return the current update time 206 | return time.Now() 207 | } 208 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | zerotier-dns 3 |

4 | 5 |

6 | A DNS server for ZeroTier virtual networks. 7 |

8 | 9 |

10 | 11 | Github Actions 13 | 14 |

15 | 16 | This is a fork of [uxbh/ztdns](https://github.com/uxbh/ztdns). 17 | 18 | 19 | 20 | ## Features 21 | 22 | * Address members of ZeroTier networks by name 23 | * IPv4 and IPv6 support (A and AAAA records) 24 | * Create round-robin DNS names 25 | * Tiny Docker image for easy setup 26 | 27 | 28 | 29 | ## Usage 30 | First, add a new API Access Token to your [ZeroTier account](https://my.zerotier.com/). 31 | 32 | To start the server using the Docker image: 33 | 34 | ```bash 35 | docker run --rm \ 36 | -p 53:53/udp \ 37 | --volume $(pwd)/zerotier-dns.yml:/app/zerotier-dns.yml \ 38 | mjenz/zerotier-dns server --api-key API_KEY --network NETWORK_ID 39 | ``` 40 | 41 | where `API_KEY` is a ZeroTier API token and `NETWORK_ID` is a ZeroTier network ID. 42 | If your ZeroTier network interface is not called `zt0`, then you should add `--interface=` (or `--interface=` to listen on all interfaces). 43 | It is recommended to use a configuration file instead of these command-line arguments (see ["Configuration"](#configuration) section below). 44 | On a machine with `systemd-resolved`, you may need to use `--network=host` instead of `-p 53:53` to prevent the Docker daemon from fighting over port 53. 45 | 46 | Once the server is running you will be able to resolve ZeroTier member names by querying it directly: 47 | 48 | ```bash 49 | dig @ . 50 | 51 | ;; QUESTION SECTION: 52 | ;matthews-mbp.zt. IN A 53 | 54 | ;; ANSWER SECTION: 55 | matthews-mbp.zt. 3600 IN A 192.168.192.120 56 | ``` 57 | 58 | Note that the DNS name is based on the ZeroTier member name (as shown in [ZeroTier Central](https://my.zerotier.com/network)), not the hostname of the member. 59 | 60 | In order to resolve names normally, you need to get the server into the DNS lookup chain on all of your machines. 61 | As of ZeroTier 1.6, you can configure the ZeroTier controller to push a split DNS configuration to clients when they connect. 62 | In the "DNS" box in the "Advanced" section in ZeroTier Central, enter the IP address of the server and the domain you have configured. 63 | ZeroTier Central will not accept top-level domains: "yourdomain.com" is valid but "zt" is not. 64 | Clients must have the "Allow DNS" box checked in "Network Details". 65 | Note that on macOS, some command-line troubleshooting tools like `dig` and `nslookup` use their own DNS resolution logic which won't use the split DNS configuration (see [this Stack Exchange answer](https://superuser.com/a/1177211)). 66 | 67 | To get the server into the DNS lookup chain for older clients, you can either configure the system resolver on each machine to use your `zerotier-dns` instance for your chosen domain (see instructions for [Linux](https://learn.hashicorp.com/consul/security-networking/forwarding#systemd-resolved-setup) or [macOS](https://learn.hashicorp.com/consul/security-networking/forwarding#macos-setup)), or configure the DNS server each machine uses to delegate to your `zerotier-dns` instance for your chosen domain (see instructions for [dnsmasq](https://learn.hashicorp.com/consul/security-networking/forwarding#dnsmasq-setup) or [bind](https://learn.hashicorp.com/consul/security-networking/forwarding#bind-setup)). 68 | 69 | 70 | 71 | ## Building from source 72 | To build from source: 73 | 74 | ``` bash 75 | go get -u github.com/mje-nz/zerotier-dns/ 76 | # or 77 | git clone https://github.com/mje-nz/zerotier-dns.git 78 | cd zerotier-dns 79 | go install 80 | # then 81 | zerotier-dns server --api-key API_KEY --network NETWORK_ID 82 | ``` 83 | 84 | If you are running on Linux, run `sudo setcap cap_net_bind_service=+ep /go/bin/zerotier-dns` to enable non-root users to bind privileged ports. 85 | On other operating systems, `zerotier-dns` may need to be run as an administrator. 86 | This does not apply to the Docker image. 87 | 88 | 89 | 90 | ## Installation as a service 91 | For both of these methods it is assumed your configuration is stored in a configuration file, although this is not compulsory. 92 | 93 | 94 | ### Docker 95 | To install the server as a service using the Docker image: 96 | 97 | ```bash 98 | docker run --detach \ 99 | -p 53:53/udp \ 100 | --volume $(pwd)/zerotier-dns.yml:/app/zerotier-dns.yml \ 101 | --restart=unless-stopped \ 102 | --name=zerotier-dns mjenz/zerotier-dns 103 | ``` 104 | 105 | 106 | ### Systemd 107 | To install the server as a `systemd` service, create a file `/etc/systemd/system/zerotier-dns.service` containing: 108 | 109 | ```ini 110 | [Unit] 111 | Description=ZeroTier DNS Server 112 | After=network-online.target 113 | Wants=network-online.target 114 | 115 | [Service] 116 | WorkingDirectory= 117 | ExecStart=zerotier-dns server 118 | Restart=on-failure 119 | RestartSec=5 120 | 121 | [Install] 122 | WantedBy=multi-user.target 123 | ``` 124 | 125 | Make sure you set `WorkingDirectory` to the directory containing your configuration file. 126 | 127 | Then, to install the service: 128 | 129 | ```bash 130 | # Reload service files 131 | sudo systemctl daemon-reload 132 | # Set zerotier-dns service to start on boot 133 | sudo systemctl enable zerotier-dns.service 134 | # Also start zerotier-dns service right now 135 | sudo systemctl start zerotier-dns.service 136 | ``` 137 | 138 | To uninstall the service: 139 | 140 | ```bash 141 | # Stop zerotier-dns service 142 | sudo systemctl stop zerotier-dns.service 143 | # Stop zerotier-dns service from starting on boot 144 | sudo systemctl disable zerotier-dns.service 145 | ``` 146 | 147 | 148 | 149 | ## Configuration 150 | 151 | 152 | ### Command-line options: 153 | ```bash 154 | $ zerotier-dns server --help 155 | Start the zerotier-dns DNS server. 156 | 157 | Usage: 158 | zerotier-dns server [flags] 159 | 160 | Flags: 161 | --api-key string ZeroTier API key 162 | --api-url string ZeroTier API URL (default "https://my.zerotier.com/api") 163 | --domain string domain to serve names under (default "zt") 164 | -h, --help help for server 165 | --include-offline include offline members (default true) 166 | --interface string network interface to bind to (default "zt0") 167 | --network string ZeroTier Network ID 168 | --port int port to listen on (default 53) 169 | --refresh int how often to poll the ZeroTier controller in minutes (default 30) 170 | 171 | Global Flags: 172 | --config string config file (default is zerotier-dns.yml) 173 | --debug enable debug messages 174 | ``` 175 | 176 | 177 | ### Config file 178 | `zerotier-dns` looks for `zerotier-dns.yml` in the current working directory or `$HOME`. 179 | There is a `zerotier-dns.example.yml` example config file with all supported options and their default values: 180 | 181 | ```yaml 182 | # Network interface to bind to (or "" to bind to all interfaces). By default, only 183 | # respond on the ZeroTier interface. On macOS, there are two ZeroTier interfaces 184 | # whose names start with "feth" and you should specify the one with the lower number 185 | # (see https://www.zerotier.com/2019/08/21/how-zerotier-eliminated-kernel-extensions-on-macos/ ). 186 | interface: "zt0" 187 | 188 | # Port to listen on. 189 | port: 53 190 | 191 | # Base domain. Could be a top-level domain for internal use only (e.g., zt) or 192 | # a domain name with one or more subdomains (e.g., internal.yourdomain.com). 193 | # By default, map members to ".zt". 194 | domain: "zt" 195 | 196 | # How often to poll the ZeroTier controller in minutes. 197 | refresh: 30 198 | 199 | # Include members that are currently offline. 200 | include-offline: true 201 | 202 | # Enable debug messages. 203 | debug: false 204 | 205 | # An API key for your ZeroTier account (required). 206 | api-key: "" 207 | 208 | # The base API URL for the ZeroTier controller. 209 | api-url: "https://my.zerotier.com/api" 210 | 211 | # ID of the ZeroTier network. Only one of "network" and "networks" can be 212 | # specified. E.g., if domain="zt" and there is a network with ID "123abc" then 213 | # this would map its members to ".zt": 214 | # network: "123abc" 215 | network: 216 | 217 | # Mappings between subdomains and ZeroTier network IDs. Only one of "network" 218 | # and "networks" can be specified. 219 | networks: 220 | # E.g., if domain="zt" and there is a network with ID "123abc" then this would 221 | # map its members to ".home.zt": 222 | # home: "123abc" 223 | 224 | # Mappings between round-robin names and regexps to match members. Names are 225 | # matched within each network (i.e., if there are members matching a mapping in 226 | # multiple networks then the name will be defined separately in each). 227 | round-robin: 228 | # E.g., if the "home" network defined above had members "k8s-node-23refw" and 229 | # "k8s-node-09sf8g" this would create a name "k8s-nodes.home.zt" returning one 230 | # of them at random: 231 | # k8s-nodes: "k8s-node-\w" 232 | ``` 233 | 234 | Configuration options can be overridden using environment variables, where the variable name is "ZTDNS_" followed by the option's name in upper-case with hyphens changed to underscores (e.g., the `api-url` option is overridden by the `ZTDNS_API_URL` environment variable). 235 | Command-line options override both. 236 | 237 | 238 | 239 | ## Contributing 240 | 241 | Thanks for considering contributing to the project. 242 | We welcome contributions, issues or requests from anyone, and are grateful for any help. 243 | Problems or questions? 244 | Feel free to open an issue on GitHub. 245 | 246 | Please make sure your contributions adhere to the following guidelines: 247 | 248 | * Code must adhere to the [official Go formatting guidelines](https://golang.org/doc/effective_go.html#formatting) (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). 249 | * Pull requests need to be based on and opened against the `master` branch. 250 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 7 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 8 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 9 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 10 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 11 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 12 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 13 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 14 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 15 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 16 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 17 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 20 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 21 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 22 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 23 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 24 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 25 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 26 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 27 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 28 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 29 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 30 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 31 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 32 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 33 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 34 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 35 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 36 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 37 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 38 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 39 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 40 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 41 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 42 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 43 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 44 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 45 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 46 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 47 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 48 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 49 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 50 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 51 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 52 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 53 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 54 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 55 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 56 | github.com/miekg/dns v1.1.15 h1:CSSIDtllwGLMoA6zjdKnaE6Tx6eVUxQ29LUgGetiDCI= 57 | github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 58 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 59 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 60 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 61 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 62 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 63 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 64 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 65 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 66 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 68 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 69 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 70 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 71 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 72 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 73 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 74 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 75 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 76 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 77 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 78 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 79 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 80 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 81 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 82 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 83 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 84 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 85 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 86 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 87 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 88 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 89 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 90 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 91 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 92 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 93 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 94 | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= 95 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 96 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 97 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 98 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 99 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 100 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 101 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 102 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 103 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 104 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 105 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 106 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 107 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 108 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 109 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 110 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 111 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 112 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 113 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 114 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 115 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 116 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 117 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= 118 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 119 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 120 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 121 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 122 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 123 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 124 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 125 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 126 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 127 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 128 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 129 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 130 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 131 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 132 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 133 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 134 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 135 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 136 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 137 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 138 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 139 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd h1:/e+gpKk9r3dJobndpTytxS2gOy6m5uvpg+ISQoEcusQ= 140 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 141 | golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0 h1:7+F62GGWUowoiJOUDivedlBECd/fTeUDJnCu0JetQO0= 142 | golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 143 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 144 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 145 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 146 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 147 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 148 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 149 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 150 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 151 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 152 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 153 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 154 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 155 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 156 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 157 | --------------------------------------------------------------------------------