From b93970ccfd891718e1f1e8deed258eefedb14112 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 21 Dec 2022 19:08:19 +0700 Subject: [PATCH] all: add CLI flags for no config start This commit adds the ability to start `ctrld` without config file. All necessary information can be provided via command line flags, either in base64 encoded config or launch arguments. --- cmd/ctrld/cli.go | 104 ++++++++++++++++++++++++++++++++++++++++++--- cmd/ctrld/main.go | 14 ++++-- config.go | 14 +++--- docs/basic_mode.md | 79 ++++++++++++++++++++++++++++++++++ doh.go | 2 +- resolver.go | 22 +++++----- 6 files changed, 205 insertions(+), 30 deletions(-) create mode 100644 docs/basic_mode.md diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 6eb65f6..602e3f5 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -1,11 +1,15 @@ package main import ( + "bytes" + "encoding/base64" "fmt" "log" + "net" "os" "os/exec" "runtime" + "strconv" "github.com/go-playground/validator/v10" "github.com/kardianos/service" @@ -38,6 +42,7 @@ func initCLI() { `verbose log output, "-v" means query logging enabled, "-vv" means debug level logging enabled`, ) + basicModeFlags := []string{"listen", "primary_upstream", "secondary_upstream", "domains", "log"} runCmd := &cobra.Command{ Use: "run", Short: "Run the DNS proxy server", @@ -49,14 +54,18 @@ func initCLI() { if configPath != "" { v.SetConfigFile(configPath) } - if err := v.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - writeConfigFile() - defaultConfigWritten = true - } else { - log.Fatalf("failed to decode config file: %v", err) + noConfigStart := func() bool { + for _, flagName := range basicModeFlags { + if cmd.Flags().Lookup(flagName).Changed { + return true + } } - } + return false + }() + + readConfigFile(!noConfigStart && configBase64 == "") + readBase64Config() + processNoConfigFlags(noConfigStart) if err := v.Unmarshal(&cfg); err != nil { log.Fatalf("failed to unmarshal config: %v", err) } @@ -106,6 +115,12 @@ func initCLI() { } runCmd.Flags().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon") runCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file") + runCmd.Flags().StringVarP(&configBase64, "base64_config", "", "", "base64 encoded config") + runCmd.Flags().StringVarP(&listenAddress, "listen", "", "", "listener address and port, in format: address:port") + runCmd.Flags().StringVarP(&primaryUpstream, "primary_upstream", "", "", "primary upstream endpoint") + runCmd.Flags().StringVarP(&secondaryUpstream, "secondary_upstream", "", "", "secondary upstream endpoint") + runCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "list of domain to apply in a split DNS policy") + runCmd.Flags().StringVarP(&logPath, "log", "", "", "path to log file") rootCmd.AddCommand(runCmd) @@ -125,3 +140,78 @@ func writeConfigFile() { log.Printf("failed to write config file: %v\n", err) } } + +func readConfigFile(configWritten bool) { + err := v.ReadInConfig() + if err == nil || !configWritten { + return + } + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + writeConfigFile() + defaultConfigWritten = true + return + } + log.Fatalf("failed to decode config file: %v", err) +} + +func readBase64Config() { + if configBase64 == "" { + return + } + configStr, err := base64.StdEncoding.DecodeString(configBase64) + if err != nil { + log.Fatalf("invalid base64 config: %v", err) + } + if err := v.ReadConfig(bytes.NewReader(configStr)); err != nil { + log.Fatalf("failed to read base64 config: %v", err) + } +} + +func processNoConfigFlags(noConfigStart bool) { + if !noConfigStart { + return + } + if listenAddress == "" || primaryUpstream == "" { + log.Fatal(`"listen" and "primary_upstream" flags must be set in no config mode`) + } + host, portStr, err := net.SplitHostPort(listenAddress) + if err != nil { + log.Fatalf("invalid listener address: %v", err) + } + port, err := strconv.Atoi(portStr) + if err != nil { + log.Fatalf("invalid port number: %v", err) + } + lc := &ctrld.ListenerConfig{ + IP: host, + Port: port, + } + v.Set("listener", map[string]*ctrld.ListenerConfig{ + "0": lc, + }) + + upstream := map[string]*ctrld.UpstreamConfig{ + "0": { + Name: primaryUpstream, + Endpoint: primaryUpstream, + Type: ctrld.ResolverTypeDOH, + }, + } + if secondaryUpstream != "" { + upstream["1"] = &ctrld.UpstreamConfig{ + Name: secondaryUpstream, + Endpoint: secondaryUpstream, + Type: ctrld.ResolverTypeLegacy, + } + rules := make([]ctrld.Rule, 0, len(domains)) + for _, domain := range domains { + rules = append(rules, ctrld.Rule{domain: []string{"upstream.1"}}) + } + lc.Policy = &ctrld.ListenerPolicyConfig{Name: "My Policy", Rules: rules} + } + v.Set("upstream", upstream) + + if logPath != "" { + v.Set("service", ctrld.ServiceConfig{LogLevel: "debug", LogPath: logPath}) + } +} diff --git a/cmd/ctrld/main.go b/cmd/ctrld/main.go index b916ee7..1fa5c22 100644 --- a/cmd/ctrld/main.go +++ b/cmd/ctrld/main.go @@ -12,10 +12,16 @@ import ( ) var ( - configPath string - daemon bool - cfg ctrld.Config - verbose int + configPath string + configBase64 string + daemon bool + listenAddress string + primaryUpstream string + secondaryUpstream string + domains []string + logPath string + cfg ctrld.Config + verbose int bootstrapDNS = "76.76.2.0" diff --git a/config.go b/config.go index 650b371..a88fdaa 100644 --- a/config.go +++ b/config.go @@ -40,14 +40,14 @@ func InitConfig(v *viper.Viper, name string) { "0": { BootstrapIP: "76.76.2.11", Name: "Control D - Anti-Malware", - Type: "doh", + Type: ResolverTypeDOH, Endpoint: "https://freedns.controld.com/p1", Timeout: 5000, }, "1": { BootstrapIP: "76.76.2.11", Name: "Control D - No Ads", - Type: "doq", + Type: ResolverTypeDOQ, Endpoint: "p2.freedns.controld.com", Timeout: 3000, }, @@ -139,9 +139,9 @@ func (uc *UpstreamConfig) Init() { // For now, only DoH upstream is supported. func (uc *UpstreamConfig) SetupTransport() { switch uc.Type { - case resolverTypeDOH: + case ResolverTypeDOH: uc.setupDOHTransport() - case resolverTypeDOH3: + case ResolverTypeDOH3: uc.setupDOH3Transport() } } @@ -231,11 +231,11 @@ func validateDnsRcode(fl validator.FieldLevel) bool { func defaultPortFor(typ string) string { switch typ { - case resolverTypeDOH, resolverTypeDOH3: + case ResolverTypeDOH, ResolverTypeDOH3: return "443" - case resolverTypeDOQ, resolverTypeDOT: + case ResolverTypeDOQ, ResolverTypeDOT: return "853" - case resolverTypeLegacy: + case ResolverTypeLegacy: return "53" } return "53" diff --git a/docs/basic_mode.md b/docs/basic_mode.md new file mode 100644 index 0000000..dda3801 --- /dev/null +++ b/docs/basic_mode.md @@ -0,0 +1,79 @@ +# basic mode + +`ctrld` can operate in `basic` mode, which requires no configuration file. All necessary information is provided +via command line flags, and be translated to corresponding config. `ctrld` will start with that config but do not +write anything to disk. + +## Base64 encoded config + +`ctrld` can read a base64 encoded config via command line flag: + +```shell +ctrld run --base64_config="CltsaXN0ZW5lcl0KCiAgW2xpc3RlbmVyLjBdCiAgICBpcCA9ICIxMjcuMC4wLjEiCiAgICBwb3J0ID0gNTMKICAgIHJlc3RyaWN0ZWQgPSBmYWxzZQoKW25ldHdvcmtdCgogIFtuZXR3b3JrLjBdCiAgICBjaWRycyA9IFsiMC4wLjAuMC8wIl0KICAgIG5hbWUgPSAiTmV0d29yayAwIgoKW3Vwc3RyZWFtXQoKICBbdXBzdHJlYW0uMF0KICAgIGJvb3RzdHJhcF9pcCA9ICI3Ni43Ni4yLjExIgogICAgZW5kcG9pbnQgPSAiaHR0cHM6Ly9mcmVlZG5zLmNvbnRyb2xkLmNvbS9wMSIKICAgIG5hbWUgPSAiQ29udHJvbCBEIC0gQW50aS1NYWx3YXJlIgogICAgdGltZW91dCA9IDUwMDAKICAgIHR5cGUgPSAiZG9oIgoKICBbdXBzdHJlYW0uMV0KICAgIGJvb3RzdHJhcF9pcCA9ICI3Ni43Ni4yLjExIgogICAgZW5kcG9pbnQgPSAicDIuZnJlZWRucy5jb250cm9sZC5jb20iCiAgICBuYW1lID0gIkNvbnRyb2wgRCAtIE5vIEFkcyIKICAgIHRpbWVvdXQgPSAzMDAwCiAgICB0eXBlID0gImRvcSIK" +``` + +## Launch arguments + +A set of arguments can be provided via command line flags. + +```shell +$ ctrld run --help +Run the DNS proxy server + +Usage: + ctrld run [flags] + +Flags: + --base64_config string base64 encoded config + -c, --config string Path to config file + -d, --daemon Run as daemon + --domains strings list of domain to apply in a split DNS policy + -h, --help help for run + --listen string listener address and port, in format: address:port + --log string path to log file + --primary_upstream string primary upstream endpoint + --secondary_upstream string secondary upstream endpoint + +Global Flags: + -v, --verbose count verbose log output, "-v" means query logging enabled, "-vv" means debug level logging enabled +``` + +For example: + +```shell +ctrld run --listen=127.0.0.1:53 --primary_upstream=https://freedns.controld.com/p2 --secondary_upstream=8.8.8.8:53 --domains=*.company.int,*.net --log /path/to/log.log +``` + +Above command will be translated roughly to this config: + +```toml +[service] + log_level = "debug" + log_path = "/path/to/log.log" + +[network.0] + name = "Network 0" + cidrs = ["0.0.0.0/0"] + +[upstream.0] + name = "https://freedns.controld.com/p2" + endpoint = "https://freedns.controld.com/p2" + type = "doh" + +[upstream.1] + name = "8.8.8.8:53" + endpoint = "8.8.8.8:53" + type = "legacy" + +[listener.0] + ip = "127.0.0.1" + port = 53 + + [listener.0.policy] + rules = [ + {"*.company.int" = ["upstream.1"]}, + {"*.net" = ["upstream.1"]}, + ] +``` + +`secondary_upstream`, `domains`, and `log` flags are optional. diff --git a/doh.go b/doh.go index 546f8c7..2c68512 100644 --- a/doh.go +++ b/doh.go @@ -14,7 +14,7 @@ import ( func newDohResolver(uc *UpstreamConfig) *dohResolver { r := &dohResolver{ endpoint: uc.Endpoint, - isDoH3: uc.Type == resolverTypeDOH3, + isDoH3: uc.Type == ResolverTypeDOH3, transport: uc.transport, http3RoundTripper: uc.http3RoundTripper, } diff --git a/resolver.go b/resolver.go index ac065ce..72f2177 100644 --- a/resolver.go +++ b/resolver.go @@ -11,12 +11,12 @@ import ( ) const ( - resolverTypeDOH = "doh" - resolverTypeDOH3 = "doh3" - resolverTypeDOT = "dot" - resolverTypeDOQ = "doq" - resolverTypeOS = "os" - resolverTypeLegacy = "legacy" + ResolverTypeDOH = "doh" + ResolverTypeDOH3 = "doh3" + ResolverTypeDOT = "dot" + ResolverTypeDOQ = "doq" + ResolverTypeOS = "os" + ResolverTypeLegacy = "legacy" ) var bootstrapDNS = "76.76.2.0" @@ -34,15 +34,15 @@ var errUnknownResolver = errors.New("unknown resolver") func NewResolver(uc *UpstreamConfig) (Resolver, error) { typ, endpoint := uc.Type, uc.Endpoint switch typ { - case resolverTypeDOH, resolverTypeDOH3: + case ResolverTypeDOH, ResolverTypeDOH3: return newDohResolver(uc), nil - case resolverTypeDOT: + case ResolverTypeDOT: return &dotResolver{uc: uc}, nil - case resolverTypeDOQ: + case ResolverTypeDOQ: return &doqResolver{uc: uc}, nil - case resolverTypeOS: + case ResolverTypeOS: return &osResolver{}, nil - case resolverTypeLegacy: + case ResolverTypeLegacy: return &legacyResolver{endpoint: endpoint}, nil } return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ)