Overhaul config, strip out other subcommands, tidy up, add pre-commit with golangci-lint

This commit is contained in:
Matthew Edwards
2019-10-03 15:41:01 +13:00
parent 7c28135168
commit 9f7f20d697
13 changed files with 161 additions and 212 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
/.ztdns.toml
ztdns.yml
ztdns.toml
.vscode/
+4
View File
@@ -0,0 +1,4 @@
linters-settings:
errcheck:
# fmt is default
ignore: fmt:.*,github.com/spf13/viper:BindPFlag
+16
View File
@@ -0,0 +1,16 @@
repos:
- repo: https://github.com/golangci/golangci-lint
rev: v1.19.1
hooks:
- id: golangci-lint
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.2.3
hooks:
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
-29
View File
@@ -1,29 +0,0 @@
# Configuration file for ztDNS
suffix = "zt"
port = 53
interface = "zt0"
# Number of minutes to wait before updating the DNS database again (Default: 30)
DBRefresh = 30
# This section contains information related to your ZeroTier config
[ZT]
# API is used to contact the ZeroTier controller API service.
API = ""
# URL is the url of the ZeroTier controller API
URL = "https://my.zerotier.com/api"
# This section contains one or more ZeroTier networks
# Format is: domain = "NetworkID"
# Domain does not have to match the configured network name
[Networks]
# Match section contains zero or more match pairs to create Round robin dns
# Format is: "regexp to match hosts" = "hostname"
# Example 1:
# "k8s-node-\w" = "k8s-nodes"
# From nodes with names k8s-node-23refw, k8s-node-09sf8g
# will create round robin record k8s-nodes
[RoundRobin]
-42
View File
@@ -1,42 +0,0 @@
// Copyright © 2017 uxbh
package cmd
import (
"fmt"
"net"
"github.com/spf13/cobra"
)
// listinterfacesCmd represents the listinterfaces command
var listinterfacesCmd = &cobra.Command{
Use: "listinterfaces",
Short: "List network interfaces",
Long: `List Interfaces (ztdns listinterfaces) lists the available network interfaces
to start the server on.`,
Run: func(cmd *cobra.Command, args []string) {
// Get a list of interfaces from net
ints, err := net.Interfaces()
if err != nil {
fmt.Printf("error getting interfaces: %s", err)
}
for i, n := range ints {
fmt.Printf("%d: %v\n", i, n.Name)
// Get a list of ip address on the interface
addrs, _ := n.Addrs()
for i, a := range addrs {
ip, _, err := net.ParseCIDR(a.String())
if err != nil {
continue
}
fmt.Printf("\t%d: %s\n", i, ip)
}
}
},
}
func init() {
RootCmd.AddCommand(listinterfacesCmd)
}
-68
View File
@@ -1,68 +0,0 @@
// Copyright © 2017 uxbh
package cmd
import (
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// mkconfigCmd represents the mkconfig command
var mkconfigCmd = &cobra.Command{
Use: "mkconfig",
Short: "Make a new config file",
Long: `mkconfig (ztdns mkconfig) creates a new configuation file.
If you do not specify a filename the default is ./.ztdns.toml
Example: ztdns mkconfig [.filename.toml]`,
Run: func(cmd *cobra.Command, args []string) {
filename := "./.ztdns.toml"
if len(args) > 0 {
filename = args[0]
}
if _, err := os.Stat(filename); os.IsNotExist(err) {
log.Printf("Creating new config file in %s", filename)
file, err := os.Create(filename)
if err != nil {
log.Fatalf("Could not create file: %s", err.Error())
}
defer file.Close()
file.WriteString(`# Configuration file for ztDNS
suffix = "zt"
port = 53
interface = "zt0"
# Number of minutes to wait before updating the DNS database again (Default: 30)
DBRefresh = 30
# This section contains information related to your ZeroTier config
[ZT]
# API is used to contact the ZeroTier controller API service.
API = ""
# URL is the url of the ZeroTier controller API
URL = "https://my.zerotier.com/api"
# This section contains one or more ZeroTier networks
# Format is: domain = "NetworkID"
# Domain does not have to match the configured network name
[Networks]
# Match section contains zero or more match pairs to create Round robin dns
# Format is: "regexp to match hosts" = "hostname"
# Example 1:
# "k8s-node-\w" = "k8s-nodes"
# From nodes with names k8s-node-23refw, k8s-node-09sf8g
# will create round robin record k8s-nodes
[RoundRobin]
`)
}
},
}
func init() {
RootCmd.AddCommand(mkconfigCmd)
}
+12 -13
View File
@@ -1,10 +1,12 @@
// Copyright © 2017 uxbh
// Package cmd implments the ztdns command-line interface.
package cmd
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -12,7 +14,6 @@ import (
var cfgFile string
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "ztdns",
Short: "Zerotier DNS Server",
@@ -21,8 +22,6 @@ This application will serve DNS requests for the members of a ZeroTier
network for both A (IPv4) and AAAA (IPv6) requests`,
}
// Execute adds all child commands to the root command sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
@@ -32,28 +31,28 @@ func Execute() {
func init() {
cobra.OnInitialize(initConfig)
RootCmd.PersistentFlags().Bool("debug", false, "enable debug messages")
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is ztdns.yml)")
viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug"))
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.ztdns.toml)")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" { // enable ability to specify config file via flag
if cfgFile != "" {
// Use specified config file
viper.SetConfigFile(cfgFile)
} else {
viper.SetConfigName(".ztdns") // name of config file (without extension)
viper.AddConfigPath(".") // adding current directory as first search path
viper.AddConfigPath("$HOME") // adding home directory as second search path
// Find config file in current directory or $HOME
viper.SetConfigName("ztdns")
viper.AddConfigPath(".")
viper.AddConfigPath("$HOME")
}
// Enable setting config values with ZTDNS_KEY environment variables
viper.SetEnvPrefix("ztdns")
viper.AutomaticEnv() // read in environment variables that match
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err != nil {
fmt.Println("Can't read config:", err)
os.Exit(1)
+57 -46
View File
@@ -20,27 +20,36 @@ import (
var serverCmd = &cobra.Command{
Use: "server",
Short: "Run ztDNS server",
Long: `Server (ztdns server) will start the DNS server.append
Long: `Server (ztdns server) will start the DNS server.
Example: ztdns server`,
PreRunE: func(cmd *cobra.Command, args []string) error {
// Check config and bail if anything important is missing.
if viper.GetBool("debug") {
log.SetLevel(log.DebugLevel)
log.Debug("Setting Debug Mode")
}
if viper.GetString("ZT.API") == "" {
return fmt.Errorf("no API key provided")
log.Debug("debug: ", viper.GetBool("debug"))
log.Debugf("interface: %s", viper.GetString("interface"))
log.Debug("port: ", viper.GetInt("port"))
log.Debugf("domain: %q", viper.GetString("domain"))
log.Debug("refresh: ", viper.GetInt("refresh"))
log.Debugf("api-key: %q", viper.GetString("api-key"))
log.Debugf("api-url: %q", viper.GetString("api-url"))
log.Debugf("network: %q", viper.GetString("network"))
log.Debugf("networks: %#v", viper.GetStringMapString("networks"))
log.Debugf("round-robin: %#v", viper.GetStringMapString("round-robin"))
if viper.GetString("api-key") == "" {
return fmt.Errorf("No API key provided")
}
if len(viper.GetStringMapString("Networks")) == 0 {
return fmt.Errorf("no Domain / Network ID pairs Provided")
if viper.GetString("network") == "" && !viper.IsSet("networks") {
return fmt.Errorf("No networks configured (specify network or networks)")
}
if viper.GetString("ZT.URL") == "" {
return fmt.Errorf("no URL provided. Run ztdns mkconfig first")
}
if viper.GetString("suffix") == "" {
return fmt.Errorf("no DNS Suffix provided. Run ztdns mkconfig first")
if viper.GetString("network") != "" && viper.IsSet("networks") {
return fmt.Errorf("Conflicting network configuration (specify one of network or networks)")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
@@ -48,12 +57,9 @@ var serverCmd = &cobra.Command{
lastUpdate := updateDNS()
req := make(chan string)
// Start the DNS server
go dnssrv.Start(viper.GetString("interface"), viper.GetInt("port"), viper.GetString("suffix"), req)
go dnssrv.Start(viper.GetString("interface"), viper.GetInt("port"), viper.GetString("domain"), req)
refresh := viper.GetInt("DbRefresh")
if refresh == 0 {
refresh = 30
}
refresh := viper.GetInt("refresh")
for {
// Block until a new request comes in
n := <-req
@@ -69,49 +75,64 @@ var serverCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(serverCmd)
serverCmd.PersistentFlags().String("interface", "", "interface to listen on")
viper.BindPFlag("interface", serverCmd.PersistentFlags().Lookup("interface"))
serverCmd.Flags().String("interface", "zt0", "network interface to bind to")
serverCmd.Flags().Int("port", 53, "port to listen on")
serverCmd.Flags().String("domain", "zt", "domain to serve names under")
serverCmd.Flags().Int("refresh", 30, "how often to poll the ZeroTier controller in minutes")
serverCmd.Flags().String("api-key", "", "ZeroTier API key")
serverCmd.Flags().String("api-url", "https://my.zerotier.com/api", "ZeroTier API URL")
serverCmd.Flags().String("network", "", "ZeroTier Network ID")
viper.BindPFlags(serverCmd.Flags())
}
// TODO: refactor
func updateDNS() time.Time {
// Get config info
API := viper.GetString("ZT.API")
URL := viper.GetString("ZT.URL")
suffix := viper.GetString("suffix")
apiKey := viper.GetString("api-key")
apiUrl := viper.GetString("api-url")
rootDomain := viper.GetString("domain")
rrDNSPatterns := make(map[string]*regexp.Regexp)
rrDNSRecords := make(map[string][]dnssrv.Records)
for re, host := range viper.GetStringMapString("RoundRobin") {
for re, host := range viper.GetStringMapString("round-robin") {
rrDNSPatterns[host] = regexp.MustCompile(re)
log.Debugf("Creating match '%s' for %s host", re, host)
}
// Get all configured networks:
for domain, id := range viper.GetStringMapString("Networks") {
// Get ZeroTier Network info
ztnetwork, err := ztapi.GetNetworkInfo(API, URL, id)
if err != nil {
log.Fatalf("Unable to update DNS entries: %s", err.Error())
networks := viper.GetStringMapString("networks")
if len(networks) == 0 {
networks = map[string]string{"": viper.GetString("network")}
}
// Get list of members in network
log.Infof("Getting Members of Network: %s (%s)", ztnetwork.Config.Name, domain)
lst, err := ztapi.GetMemberList(API, URL, ztnetwork.ID)
if err != nil {
log.Fatalf("Unable to update DNS entries: %s", err.Error())
// Get all configured networks:
for domain, networkID := range networks {
suffix := "." + rootDomain + "."
if domain != "" {
suffix = "." + domain + suffix
}
log.Infof("Got %d members", len(*lst))
ztnetwork, err := ztapi.GetNetworkInfo(apiKey, apiUrl, networkID)
if err != nil {
log.Fatalf("Unable to get network info: %s", err.Error())
}
log.Infof("Getting members of network: %s (%s)", ztnetwork.Config.Name, suffix)
lst, err := ztapi.GetMemberList(apiKey, apiUrl, ztnetwork.ID)
if err != nil {
log.Fatalf("Unable to get member list: %s", err.Error())
}
log.Debugf("Got %d members", len(*lst))
for _, n := range *lst {
// For all online members
if n.Online {
// Sanitize name
// Sanitize member name
name := strings.ToLower(n.Name)
name = strings.ReplaceAll(name, " ", "-")
re := regexp.MustCompile("[^a-zA-Z0-9-]+")
name = re.ReplaceAllString(name, "")
record := name + "." + domain + "." + suffix + "."
record := name + suffix
// Clear current DNS records
dnssrv.DNSDatabase[record] = dnssrv.Records{}
@@ -144,7 +165,6 @@ func updateDNS() time.Time {
for host, re := range rrDNSPatterns {
log.Debugf("Checking matches for %s host", host)
if match := re.FindStringSubmatch(n.Name); match != nil {
// prefix := fmt.Sprintf(host, iface(match[1:]))
rrRecord := host + "." + domain + "." + suffix + "."
log.Infof("Adding ips to RR record %-15s IPv4: %-15s IPv6: %s, from host %s", rrRecord, ip4, ip6, n.Name)
@@ -169,12 +189,3 @@ func updateDNS() time.Time {
// Return the current update time
return time.Now()
}
// Convert slice of string to interface for fmt
func iface(list []string) []interface{} {
vals := make([]interface{}, len(list))
for i, v := range list {
vals[i] = v
}
return vals
}
+15 -9
View File
@@ -29,7 +29,7 @@ var DNSDatabase = map[string]Records{}
var queryChan chan string
// Start brings up a DNS server for the specified suffix on a given port.
func Start(iface string, port int, suffix string, req chan string) error {
func Start(iface string, port int, suffix string, req chan string) {
queryChan = req
if port == 0 {
@@ -58,12 +58,16 @@ func Start(iface string, port int, suffix string, req chan string) error {
log.Printf("Starting server for %s on %s", suffix, server.Addr)
err := server.ListenAndServe()
if err != nil {
log.Fatalf("failed to start DNS server: %s", err.Error())
log.Fatalf("Failed to start DNS server: %s", err.Error())
}
defer server.Shutdown()
defer func () {
err := server.Shutdown()
if err != nil {
log.Fatalf("Failed to stop DNS server: %s", err.Error())
}
}()
}(suffix, addr, port)
}
return nil
}
func getIfaceAddrs(iface string) []net.IP {
@@ -103,7 +107,9 @@ func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
parseQuery(m)
}
w.WriteMsg(m)
if err := w.WriteMsg(m); err != nil {
log.Errorf("Failed to send response: %s", err.Error())
}
}
// parseQuery reads and creates an answer to a DNS query.
@@ -133,15 +139,15 @@ func parseQuery(m *dns.Msg) {
// shuffle ip addresses for Round Robin dns
func shuffle(ips []net.IP) []net.IP {
ipsLenght := len(ips)
ipsLength := len(ips)
if ipsLenght < 2 {
if ipsLength < 2 {
return ips
}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
ret := make([]net.IP, ipsLenght)
perm := r.Perm(ipsLenght)
ret := make([]net.IP, ipsLength)
perm := r.Perm(ipsLength)
for i, randIndex := range perm {
ret[i] = ips[randIndex]
+1
View File
@@ -7,4 +7,5 @@ require (
github.com/sirupsen/logrus v1.4.2
github.com/spf13/cobra v0.0.5
github.com/spf13/viper v1.4.0
golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0 // indirect
)
+7
View File
@@ -116,10 +116,13 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -133,7 +136,11 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd h1:/e+gpKk9r3dJobndpTytxS2gOy6m5uvpg+ISQoEcusQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0 h1:7+F62GGWUowoiJOUDivedlBECd/fTeUDJnCu0JetQO0=
golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+1 -6
View File
@@ -17,10 +17,6 @@ type apiTime struct {
func (t *apiTime) UnmarshalJSON(b []byte) error {
l := len(b)
// if string(b) == "0" {
// t.time = time.Time{}
// return nil
// }
if l < 3 {
n, err := strconv.ParseInt(string(b), 10, 32)
if err != nil {
@@ -52,8 +48,7 @@ func getJSON(url, APIToken string, target interface{}) error {
req.Header.Set("Authorization", "bearer "+APIToken)
r, err := c.Do(req)
if err != nil {
fmt.Errorf("API Error: %q", err)
return err
return fmt.Errorf("API Error: %s", err.Error())
}
if r.StatusCode != 200 {
return fmt.Errorf("API returned error: %s", r.Status)
+48
View File
@@ -0,0 +1,48 @@
# This file contains all available configuration options
# with their default values.
# Network interface to bind to (or "" to bind to). By default, only respond on
# the ZeroTier network.
interface: "zt0"
# Port to listen on.
port: 53
# Domain to serve names under. By default, serve members as
# "<member name>.<network name>.zt".
domain: "zt"
# How often to poll the ZeroTier controller in minutes.
refresh: 30
# Enable debug messages.
debug: false
# An API key for your ZeroTier account (required).
api-key: ""
# The base API URL for the ZeroTier controller.
api-url: "https://my.zerotier.com/api"
# ID of the ZeroTier network to serve. Only one of "network" and "networks" can
# be specified.
# E.g., if there is a network with ID "123abc" this would serve its members as
# "<member name>.zt".
# network: "123abc"
network:
# Mappings between ZeroTier network IDs and domain names. Only one of "network"
# and "networks" can be specified.
networks:
# E.g., if there is a network with ID "123abc" this would serve its members as
# "<member name>.home.zt".
# home: "123abc"
# Mappings between round-robin names and regexps to match members. Names are
# matched within each network (i.e., if there are members matching a mapping in
# multiple networks then the name will be defined separately in each).
round-robin:
# E.g., if the "home" network defined above had members "k8s-node-23refw" and
# "k8s-node-09sf8g" this would create a name "k8s-nodes" returning one of them
# at random.
# k8s-nodes: "k8s-node-\w"