mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
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:
committed by
Cuong Manh Le
parent
30fefe7ab9
commit
b93970ccfd
104
cmd/ctrld/cli.go
104
cmd/ctrld/cli.go
@@ -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})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
14
config.go
14
config.go
@@ -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
79
docs/basic_mode.md
Normal 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
2
doh.go
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
22
resolver.go
22
resolver.go
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user