cmd/ctrld: add --iface for setting DNS on specific interface

This commit is contained in:
Cuong Manh Le
2023-01-06 00:55:21 +07:00
committed by Cuong Manh Le
parent d5344aea52
commit b00a7c34ee
13 changed files with 473 additions and 45 deletions

View File

@@ -3,6 +3,7 @@ package main
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"log"
"net"
@@ -12,6 +13,7 @@ import (
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/go-playground/validator/v10"
"github.com/kardianos/service"
@@ -24,10 +26,16 @@ import (
"github.com/Control-D-Inc/ctrld/internal/controld"
)
const (
tailscaleDevName = "tailscale0"
tailscaleDNS = "100.100.100.100"
)
var (
v = viper.NewWithOptions(viper.KeyDelimiter("::"))
defaultConfigWritten = false
defaultConfigFile = "ctrld.toml"
tailscaleIface *net.Interface
)
var basicModeFlags = []string{"listen", "primary_upstream", "secondary_upstream", "domains", "log", "cache_size"}
@@ -41,6 +49,15 @@ func isNoConfigStart(cmd *cobra.Command) bool {
return false
}
const rootShortDesc = `
__ .__ .___
_____/ |________| | __| _/
_/ ___\ __\_ __ \ | / __ |
\ \___| | | | \/ |__/ /_/ |
\___ >__| |__| |____/\____ |
\/ dns forwarding proxy \/
`
func initCLI() {
// Enable opening via explorer.exe on Windows.
// See: https://github.com/spf13/cobra/issues/844.
@@ -48,15 +65,17 @@ func initCLI() {
rootCmd := &cobra.Command{
Use: "ctrld",
Short: "Running Control-D DNS proxy server",
Short: strings.TrimLeft(rootShortDesc, "\n"),
Version: "1.0.1",
}
rootCmd.PersistentFlags().CountVarP(
&verbose,
"verbose",
"v",
`verbose log output, "-v" means query logging enabled, "-vv" means debug level logging enabled`,
`verbose log output, "-v" basic logging, "-vv" debug level logging`,
)
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
rootCmd.CompletionOptions.HiddenDefaultCmd = true
runCmd := &cobra.Command{
Use: "run",
@@ -96,6 +115,15 @@ func initCLI() {
}
initLogging()
initCache()
if iface == "auto" {
dri, err := interfaces.DefaultRouteInterface()
if err != nil {
mainLog.Error().Err(err).Msg("failed to get default route interface")
}
iface = dri
}
if daemon {
exe, err := os.Executable()
if err != nil {
@@ -138,16 +166,18 @@ 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")
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")
runCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
runCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
runCmd.Flags().StringVarP(&homedir, "homedir", "", "", "")
_ = runCmd.Flags().MarkHidden("homedir")
runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
_ = runCmd.Flags().MarkHidden("iface")
rootCmd.AddCommand(runCmd)
@@ -204,14 +234,15 @@ func initCLI() {
}
// Keep these flags in sync with runCmd above, except for "-d".
startCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file")
startCmd.Flags().StringVarP(&configBase64, "base64_config", "", "", "base64 encoded config")
startCmd.Flags().StringVarP(&listenAddress, "listen", "", "", "listener address and port, in format: address:port")
startCmd.Flags().StringVarP(&primaryUpstream, "primary_upstream", "", "", "primary upstream endpoint")
startCmd.Flags().StringVarP(&secondaryUpstream, "secondary_upstream", "", "", "secondary upstream endpoint")
startCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "list of domain to apply in a split DNS policy")
startCmd.Flags().StringVarP(&logPath, "log", "", "", "path to log file")
startCmd.Flags().StringVarP(&configBase64, "base64_config", "", "", "Base64 encoded config")
startCmd.Flags().StringVarP(&listenAddress, "listen", "", "", "Listener address and port, in format: address:port")
startCmd.Flags().StringVarP(&primaryUpstream, "primary_upstream", "", "", "Primary upstream endpoint")
startCmd.Flags().StringVarP(&secondaryUpstream, "secondary_upstream", "", "", "Secondary upstream endpoint")
startCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "List of domain to apply in a split DNS policy")
startCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
startCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
stopCmd := &cobra.Command{
Use: "stop",
@@ -228,6 +259,7 @@ func initCLI() {
}
},
}
stopCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, "auto" means the default interface gateway`)
restartCmd := &cobra.Command{
Use: "restart",
@@ -347,13 +379,24 @@ func initCLI() {
rootCmd.AddCommand(serviceCmd)
startCmdAlias := &cobra.Command{
Use: "start",
Short: "Alias for service start",
Short: "Quick start service and configure DNS on interface",
Run: func(cmd *cobra.Command, args []string) {
startCmd.Run(cmd, args)
},
}
startCmdAlias.Flags().StringVarP(&iface, "iface", "", "auto", `Update DNS setting for iface, "auto" means the default interface gateway`)
startCmdAlias.Flags().AddFlagSet(startCmd.Flags())
rootCmd.AddCommand(startCmdAlias)
stopCmdAlias := &cobra.Command{
Use: "stop",
Short: "Quick stop service and remove DNS from interface",
Run: func(cmd *cobra.Command, args []string) {
startCmd.Run(cmd, args)
},
}
stopCmdAlias.Flags().StringVarP(&iface, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`)
stopCmdAlias.Flags().AddFlagSet(stopCmd.Flags())
rootCmd.AddCommand(stopCmdAlias)
if err := rootCmd.Execute(); err != nil {
stderrMsg(err.Error())
@@ -461,6 +504,9 @@ func processCDFlags() {
if cdUID == "" {
return
}
if iface == "" {
iface = "auto"
}
resolverConfig, err := controld.FetchResolverConfig(cdUID)
if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode {
s, err := service.New(&prog{}, svcConfig)
@@ -546,3 +592,19 @@ func processLogAndCacheFlags() {
}
v.Set("service", sc)
}
func netIfaceFromName(ifaceName string) (*net.Interface, error) {
var iface *net.Interface
err := interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
if i.Name == ifaceName {
iface = i.Interface
}
if i.Name == tailscaleDevName {
tailscaleIface = i.Interface
}
})
if iface == nil {
return nil, errors.New("interface not found")
}
return iface, err
}

3
cmd/ctrld/dns.go Normal file
View File

@@ -0,0 +1,3 @@
package main
type getDNS func(iface string) []string

View File

@@ -34,6 +34,7 @@ var (
proxyLog = rootLogger
cdUID string
iface string
)
func main() {

View File

@@ -22,3 +22,9 @@ func supportsIPv6() bool {
stackOnce.Do(probeStack)
return ipv6Enabled
}
// isIPv6 checks if the provided IP is v6.
func isIPv6(ip string) bool {
parsedIP := net.ParseIP(ip)
return parsedIP != nil && parsedIP.To4() == nil && parsedIP.To16() != nil
}

View File

@@ -1,7 +1,18 @@
package main
import (
"bufio"
"bytes"
"fmt"
"net"
"net/netip"
"os/exec"
"strings"
"tailscale.com/net/dns"
"tailscale.com/util/dnsname"
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
)
// allocate loopback ip
@@ -23,3 +34,84 @@ func deAllocateIP(ip string) error {
}
return nil
}
// set the dns server for the provided network interface
func setDNS(iface *net.Interface, nameservers []string) error {
logf := func(format string, args ...any) {
mainLog.Debug().Msgf(format, args...)
}
r, err := dns.NewOSConfigurator(logf, iface.Name)
if err != nil {
mainLog.Error().Err(err).Msg("failed to create DNS OS configurator")
return err
}
ns := make([]netip.Addr, 0, len(nameservers))
for _, nameserver := range nameservers {
ns = append(ns, netip.MustParseAddr(nameserver))
}
return r.SetDNS(dns.OSConfig{
Nameservers: ns,
SearchDomains: []dnsname.FQDN{},
})
}
func resetDNS(iface *net.Interface, nameservers []string) error {
if err := setDNS(iface, nameservers); err != nil {
mainLog.Error().Err(err).Msg("resetDNS failed.")
return err
}
return nil
}
func currentDNS(iface *net.Interface) []string {
for _, fn := range []getDNS{getDNSByResolvectl, getDNSByNmcli, resolvconffile.NameServers} {
if ns := fn(iface.Name); len(ns) > 0 {
return ns
}
}
return nil
}
func getDNSByResolvectl(iface string) []string {
b, err := exec.Command("resolvectl", "dns", "-i", iface).Output()
if err != nil {
return nil
}
parts := strings.SplitN(string(b), "%", 2)
if len(parts) != 2 {
return nil
}
parts = strings.Fields(parts[0])
if len(parts) > 2 {
fmt.Println(parts)
return parts[3:]
}
return nil
}
func getDNSByNmcli(iface string) []string {
b, err := exec.Command("nmcli", "dev", "show", iface).Output()
if err != nil {
return nil
}
s := bufio.NewScanner(bytes.NewReader(b))
var dns []string
do := func(line string) {
parts := strings.SplitN(line, ":", 2)
if len(parts) > 1 {
dns = append(dns, strings.TrimSpace(parts[1]))
}
}
for s.Scan() {
line := s.Text()
switch {
case strings.HasPrefix(line, "IP4.DNS"):
fallthrough
case strings.HasPrefix(line, "IP6.DNS"):
do(line)
}
}
return dns
}

View File

@@ -4,6 +4,7 @@
package main
import (
"net"
"os/exec"
)
@@ -26,3 +27,34 @@ func deAllocateIP(ip string) error {
}
return nil
}
// set the dns server for the provided network interface
// networksetup -setdnsservers Wi-Fi 8.8.8.8 1.1.1.1
// TODO(cuonglm): use system API
func setDNS(iface *net.Interface, nameservers []string) error {
cmd := "networksetup"
args := []string{"-setdnsservers", iface.Name}
args = append(args, nameservers...)
if err := exec.Command(cmd, args...).Run(); err != nil {
mainLog.Error().Err(err).Msgf("setDNS failed, ips = %q", nameservers)
return err
}
return nil
}
// TODO(cuonglm): use system API
func resetDNS(iface *net.Interface, _ []string) error {
cmd := "networksetup"
args := []string{"-setdnsservers", iface.Name, "empty"}
if err := exec.Command(cmd, args...).Run(); err != nil {
mainLog.Error().Err(err).Msgf("resetDNS failed")
return err
}
return nil
}
func currentDNS(_ *net.Interface) []string {
return nil
}

View File

@@ -3,6 +3,15 @@
package main
import (
"errors"
"net"
"os/exec"
"strconv"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
)
// TODO(cuonglm): implement.
func allocateIP(ip string) error {
return nil
@@ -12,3 +21,93 @@ func allocateIP(ip string) error {
func deAllocateIP(ip string) error {
return nil
}
func setDNS(iface *net.Interface, nameservers []string) error {
if len(nameservers) == 0 {
return errors.New("empty DNS nameservers")
}
primaryDNS := nameservers[0]
if err := setPrimaryDNS(iface, primaryDNS); err != nil {
return err
}
if len(nameservers) > 1 {
secondaryDNS := nameservers[1]
_ = addSecondaryDNS(iface, secondaryDNS)
}
return nil
}
// TODO(cuonglm): should we use system API?
func resetDNS(iface *net.Interface, nameservers []string) error {
if err := resetDNSUseDHCP(iface); err != nil {
mainLog.Debug().Err(err).Msg("could not reset DNS using DHCP")
}
return setDNS(iface, nameservers)
}
func setPrimaryDNS(iface *net.Interface, dns string) error {
ipVer := "ipv4"
if isIPv6(dns) {
ipVer = "ipv6"
}
idx := strconv.Itoa(iface.Index)
output, err := netsh("interface", ipVer, "set", "dnsserver", idx, "static", dns)
if err != nil {
mainLog.Error().Err(err).Msgf("failed to set primary DNS: %s", string(output))
return err
}
if ipVer == "ipv4" {
// Disable IPv6 DNS, so the query will be fallback to IPv4.
_, _ = netsh("interface", "ipv6", "set", "dnsserver", idx, "static", "::1", "primary")
}
return nil
}
func addSecondaryDNS(iface *net.Interface, dns string) error {
ipVer := "ipv4"
if isIPv6(dns) {
ipVer = "ipv6"
}
output, err := netsh("interface", ipVer, "add", "dns", strconv.Itoa(iface.Index), dns, "index=2")
if err != nil {
mainLog.Warn().Err(err).Msgf("failed to add secondary DNS: %s", string(output))
}
return nil
}
func resetDNSUseDHCP(iface *net.Interface) error {
if supportsIPv6() {
if output, err := netsh("interface", "ipv6", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp"); err != nil {
mainLog.Warn().Err(err).Msgf("failed to reset ipv6 DNS: %s", string(output))
}
}
output, err := netsh("interface", "ipv4", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp")
if err != nil {
mainLog.Error().Err(err).Msgf("failed to reset ipv4 DNS: %s", string(output))
return err
}
return nil
}
func netsh(args ...string) ([]byte, error) {
return exec.Command("netsh", args...).Output()
}
func currentDNS(iface *net.Interface) []string {
luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
if err != nil {
mainLog.Error().Err(err).Msg("failed to get interface LUID")
return nil
}
nameservers, err := luid.DNS()
if err != nil {
mainLog.Error().Err(err).Msg("failed to get interface DNS")
return nil
}
ns := make([]string, 0, len(nameservers))
for _, nameserver := range nameservers {
ns = append(ns, nameserver.String())
}
return ns
}

View File

@@ -23,8 +23,9 @@ var svcConfig = &service.Config{
}
type prog struct {
cfg *ctrld.Config
cache dnscache.Cacher
cfg *ctrld.Config
cache dnscache.Cacher
origDNS []string
}
func (p *prog) Start(s service.Service) error {
@@ -34,6 +35,24 @@ func (p *prog) Start(s service.Service) error {
}
func (p *prog) run() {
if iface != "" {
netIface, err := netIfaceFromName(iface)
if err != nil {
mainLog.Error().Err(err).Msg("could not get interface")
} else {
p.origDNS = currentDNS(netIface)
if err := setDNS(netIface, []string{cfg.Listener["0"].IP}); err != nil {
mainLog.Error().Err(err).Str("iface", iface).Msgf("could not set DNS for interface")
}
}
}
// Sorry, tailscale!
if tailscaleIface != nil {
if err := setDNS(tailscaleIface, []string{cfg.Listener["0"].IP}); err != nil {
mainLog.Warn().Err(err).Msg("could not set DNS for tailscale interface")
}
}
if p.cfg.Service.CacheEnable {
cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize)
if err != nil {
@@ -164,6 +183,15 @@ func (p *prog) Stop(s service.Service) error {
mainLog.Error().Err(err).Msg("de-allocate ip failed")
return err
}
if iface != "" {
if netIface, err := netIfaceFromName(iface); err == nil {
if err := resetDNS(netIface, p.origDNS); err != nil {
mainLog.Error().Err(err).Str("iface", iface).Msgf("could not reset DNS")
}
} else {
mainLog.Error().Err(err).Msg("could not get interface")
}
}
return nil
}