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.
This commit is contained in:
Cuong Manh Le
2022-12-21 19:08:19 +07:00
committed by Cuong Manh Le
parent 30fefe7ab9
commit b93970ccfd
6 changed files with 205 additions and 30 deletions

View File

@@ -1,11 +1,15 @@
package main package main
import ( import (
"bytes"
"encoding/base64"
"fmt" "fmt"
"log" "log"
"net"
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"strconv"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/kardianos/service" "github.com/kardianos/service"
@@ -38,6 +42,7 @@ func initCLI() {
`verbose log output, "-v" means query logging enabled, "-vv" means debug level logging enabled`, `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{ runCmd := &cobra.Command{
Use: "run", Use: "run",
Short: "Run the DNS proxy server", Short: "Run the DNS proxy server",
@@ -49,14 +54,18 @@ func initCLI() {
if configPath != "" { if configPath != "" {
v.SetConfigFile(configPath) v.SetConfigFile(configPath)
} }
if err := v.ReadInConfig(); err != nil { noConfigStart := func() bool {
if _, ok := err.(viper.ConfigFileNotFoundError); ok { for _, flagName := range basicModeFlags {
writeConfigFile() if cmd.Flags().Lookup(flagName).Changed {
defaultConfigWritten = true return true
} else { }
log.Fatalf("failed to decode config file: %v", err)
} }
} return false
}()
readConfigFile(!noConfigStart && configBase64 == "")
readBase64Config()
processNoConfigFlags(noConfigStart)
if err := v.Unmarshal(&cfg); err != nil { if err := v.Unmarshal(&cfg); err != nil {
log.Fatalf("failed to unmarshal config: %v", err) 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().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon")
runCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file") 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) rootCmd.AddCommand(runCmd)
@@ -125,3 +140,78 @@ func writeConfigFile() {
log.Printf("failed to write config file: %v\n", err) 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})
}
}

View File

@@ -12,10 +12,16 @@ import (
) )
var ( var (
configPath string configPath string
daemon bool configBase64 string
cfg ctrld.Config daemon bool
verbose int listenAddress string
primaryUpstream string
secondaryUpstream string
domains []string
logPath string
cfg ctrld.Config
verbose int
bootstrapDNS = "76.76.2.0" bootstrapDNS = "76.76.2.0"

View File

@@ -40,14 +40,14 @@ func InitConfig(v *viper.Viper, name string) {
"0": { "0": {
BootstrapIP: "76.76.2.11", BootstrapIP: "76.76.2.11",
Name: "Control D - Anti-Malware", Name: "Control D - Anti-Malware",
Type: "doh", Type: ResolverTypeDOH,
Endpoint: "https://freedns.controld.com/p1", Endpoint: "https://freedns.controld.com/p1",
Timeout: 5000, Timeout: 5000,
}, },
"1": { "1": {
BootstrapIP: "76.76.2.11", BootstrapIP: "76.76.2.11",
Name: "Control D - No Ads", Name: "Control D - No Ads",
Type: "doq", Type: ResolverTypeDOQ,
Endpoint: "p2.freedns.controld.com", Endpoint: "p2.freedns.controld.com",
Timeout: 3000, Timeout: 3000,
}, },
@@ -139,9 +139,9 @@ func (uc *UpstreamConfig) Init() {
// For now, only DoH upstream is supported. // For now, only DoH upstream is supported.
func (uc *UpstreamConfig) SetupTransport() { func (uc *UpstreamConfig) SetupTransport() {
switch uc.Type { switch uc.Type {
case resolverTypeDOH: case ResolverTypeDOH:
uc.setupDOHTransport() uc.setupDOHTransport()
case resolverTypeDOH3: case ResolverTypeDOH3:
uc.setupDOH3Transport() uc.setupDOH3Transport()
} }
} }
@@ -231,11 +231,11 @@ func validateDnsRcode(fl validator.FieldLevel) bool {
func defaultPortFor(typ string) string { func defaultPortFor(typ string) string {
switch typ { switch typ {
case resolverTypeDOH, resolverTypeDOH3: case ResolverTypeDOH, ResolverTypeDOH3:
return "443" return "443"
case resolverTypeDOQ, resolverTypeDOT: case ResolverTypeDOQ, ResolverTypeDOT:
return "853" return "853"
case resolverTypeLegacy: case ResolverTypeLegacy:
return "53" return "53"
} }
return "53" return "53"

79
docs/basic_mode.md Normal file
View File

@@ -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.

2
doh.go
View File

@@ -14,7 +14,7 @@ import (
func newDohResolver(uc *UpstreamConfig) *dohResolver { func newDohResolver(uc *UpstreamConfig) *dohResolver {
r := &dohResolver{ r := &dohResolver{
endpoint: uc.Endpoint, endpoint: uc.Endpoint,
isDoH3: uc.Type == resolverTypeDOH3, isDoH3: uc.Type == ResolverTypeDOH3,
transport: uc.transport, transport: uc.transport,
http3RoundTripper: uc.http3RoundTripper, http3RoundTripper: uc.http3RoundTripper,
} }

View File

@@ -11,12 +11,12 @@ import (
) )
const ( const (
resolverTypeDOH = "doh" ResolverTypeDOH = "doh"
resolverTypeDOH3 = "doh3" ResolverTypeDOH3 = "doh3"
resolverTypeDOT = "dot" ResolverTypeDOT = "dot"
resolverTypeDOQ = "doq" ResolverTypeDOQ = "doq"
resolverTypeOS = "os" ResolverTypeOS = "os"
resolverTypeLegacy = "legacy" ResolverTypeLegacy = "legacy"
) )
var bootstrapDNS = "76.76.2.0" var bootstrapDNS = "76.76.2.0"
@@ -34,15 +34,15 @@ var errUnknownResolver = errors.New("unknown resolver")
func NewResolver(uc *UpstreamConfig) (Resolver, error) { func NewResolver(uc *UpstreamConfig) (Resolver, error) {
typ, endpoint := uc.Type, uc.Endpoint typ, endpoint := uc.Type, uc.Endpoint
switch typ { switch typ {
case resolverTypeDOH, resolverTypeDOH3: case ResolverTypeDOH, ResolverTypeDOH3:
return newDohResolver(uc), nil return newDohResolver(uc), nil
case resolverTypeDOT: case ResolverTypeDOT:
return &dotResolver{uc: uc}, nil return &dotResolver{uc: uc}, nil
case resolverTypeDOQ: case ResolverTypeDOQ:
return &doqResolver{uc: uc}, nil return &doqResolver{uc: uc}, nil
case resolverTypeOS: case ResolverTypeOS:
return &osResolver{}, nil return &osResolver{}, nil
case resolverTypeLegacy: case ResolverTypeLegacy:
return &legacyResolver{endpoint: endpoint}, nil return &legacyResolver{endpoint: endpoint}, nil
} }
return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ) return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ)