diff --git a/cmd/cli/commands_run.go b/cmd/cli/commands_run.go index abb74bb..9d3260b 100644 --- a/cmd/cli/commands_run.go +++ b/cmd/cli/commands_run.go @@ -50,6 +50,7 @@ func InitRunCmd(rootCmd *cobra.Command) *cobra.Command { runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) _ = runCmd.Flags().MarkHidden("iface") runCmd.Flags().StringVarP(&cdUpstreamProto, "proto", "", ctrld.ResolverTypeDOH, `Control D upstream type, either "doh" or "doh3"`) + runCmd.Flags().BoolVarP(&rfc1918, "rfc1918", "", false, "Listen on RFC1918 addresses when 127.0.0.1 is the only listener") runCmd.FParseErrWhitelist = cobra.FParseErrWhitelist{UnknownFlags: true} rootCmd.AddCommand(runCmd) diff --git a/cmd/cli/commands_service_start.go b/cmd/cli/commands_service_start.go index f8a9d98..0831371 100644 --- a/cmd/cli/commands_service_start.go +++ b/cmd/cli/commands_service_start.go @@ -348,6 +348,7 @@ NOTE: running "ctrld start" without any arguments will start already installed c startCmd.Flags().BoolVarP(&skipSelfChecks, "skip_self_checks", "", false, `Skip self checks after installing ctrld service`) startCmd.Flags().BoolVarP(&startOnly, "start_only", "", false, "Do not install new service") _ = startCmd.Flags().MarkHidden("start_only") + startCmd.Flags().BoolVarP(&rfc1918, "rfc1918", "", false, "Listen on RFC1918 addresses when 127.0.0.1 is the only listener") // Start command alias startCmdAlias := &cobra.Command{ diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 60b316e..9bfa970 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -142,6 +142,8 @@ func (p *prog) startListeners(ctx context.Context, cfg *ctrld.ListenerConfig, ha }) } + // When we spawn a listener on 127.0.0.1, also spawn listeners on the RFC1918 addresses of the machine + // if explicitly set via setting rfc1918 flag, so ctrld could receive queries from LAN clients. if needRFC1918Listeners(cfg) { logger.Debug().Str("protocol", proto).Msg("Starting RFC1918 listeners") g.Go(func() error { @@ -1279,7 +1281,7 @@ func (p *prog) queryFromSelf(ip string) bool { // needRFC1918Listeners reports whether ctrld need to spawn listener for RFC 1918 addresses. // This is helpful for non-desktop platforms to receive queries from LAN clients. func needRFC1918Listeners(lc *ctrld.ListenerConfig) bool { - return lc.IP == "127.0.0.1" && lc.Port == 53 && !ctrld.IsDesktopPlatform() + return rfc1918 && lc.IP == "127.0.0.1" && lc.Port == 53 } // ipFromARPA parses a FQDN arpa domain and return the IP address if valid. diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 95d8356..7581a16 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -41,6 +41,7 @@ var ( skipSelfChecks bool cleanup bool startOnly bool + rfc1918 bool mainLog atomic.Pointer[ctrld.Logger] consoleWriter zapcore.Core diff --git a/nameservers_linux.go b/nameservers_linux.go index 8f877a6..8c93524 100644 --- a/nameservers_linux.go +++ b/nameservers_linux.go @@ -6,9 +6,12 @@ import ( "context" "encoding/hex" "net" + "net/netip" "os" "strings" + "tailscale.com/net/netmon" + "github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile" ) @@ -129,3 +132,25 @@ func virtualInterfaces() set { } return s } + +// validInterfacesMap returns a set containing non virtual interfaces. +// TODO: deduplicated with cmd/cli/net_linux.go in v2. +func validInterfaces() set { + m := make(map[string]struct{}) + vis := virtualInterfaces() + netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) { + if _, existed := vis[i.Name]; existed { + return + } + m[i.Name] = struct{}{} + }) + // Fallback to default route interface if found nothing. + if len(m) == 0 { + defaultRoute, err := netmon.DefaultRoute() + if err != nil { + return m + } + m[defaultRoute.InterfaceName] = struct{}{} + } + return m +} diff --git a/nameservers_windows.go b/nameservers_windows.go index 4ea0422..547aac2 100644 --- a/nameservers_windows.go +++ b/nameservers_windows.go @@ -444,3 +444,7 @@ func ValidInterfaces(ctx context.Context) map[string]struct{} { } return m } + +func validInterfaces() map[string]struct{} { + return ValidInterfaces(context.Background()) +} diff --git a/net_darwin.go b/net_darwin.go new file mode 100644 index 0000000..5b01e9f --- /dev/null +++ b/net_darwin.go @@ -0,0 +1,35 @@ +package ctrld + +import ( + "bufio" + "bytes" + "io" + "os/exec" + "strings" +) + +// validInterfaces returns a set of all valid hardware ports. +// TODO: deduplicated with cmd/cli/net_darwin.go in v2. +func validInterfaces() map[string]struct{} { + b, err := exec.Command("networksetup", "-listallhardwareports").Output() + if err != nil { + return nil + } + return parseListAllHardwarePorts(bytes.NewReader(b)) +} + +// parseListAllHardwarePorts parses output of "networksetup -listallhardwareports" +// and returns map presents all hardware ports. +func parseListAllHardwarePorts(r io.Reader) map[string]struct{} { + m := make(map[string]struct{}) + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + after, ok := strings.CutPrefix(line, "Device: ") + if !ok { + continue + } + m[after] = struct{}{} + } + return m +} diff --git a/cmd/cli/net_darwin_test.go b/net_darwin_test.go similarity index 99% rename from cmd/cli/net_darwin_test.go rename to net_darwin_test.go index 9ef1906..8f9734f 100644 --- a/cmd/cli/net_darwin_test.go +++ b/net_darwin_test.go @@ -1,4 +1,4 @@ -package cli +package ctrld import ( "maps" diff --git a/net_others.go b/net_others.go new file mode 100644 index 0000000..ae7ab8e --- /dev/null +++ b/net_others.go @@ -0,0 +1,15 @@ +//go:build !darwin && !windows && !linux + +package ctrld + +import "tailscale.com/net/netmon" + +// validInterfaces returns a set containing only default route interfaces. +// TODO: deuplicated with cmd/cli/net_others.go in v2. +func validInterfaces() map[string]struct{} { + defaultRoute, err := netmon.DefaultRoute() + if err != nil { + return nil + } + return map[string]struct{}{defaultRoute.InterfaceName: {}} +} diff --git a/resolver.go b/resolver.go index 55dabe6..425786d 100644 --- a/resolver.go +++ b/resolver.go @@ -709,10 +709,15 @@ func newResolverWithNameserver(nameservers []string) *osResolver { return r } -// Rfc1918Addresses returns the list of local interfaces private IP addresses +// Rfc1918Addresses returns the list of local physical interfaces private IP addresses func Rfc1918Addresses() []string { + vis := validInterfaces() var res []string netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) { + // Skip virtual interfaces. + if _, existed := vis[i.Name]; !existed { + return + } addrs, _ := i.Addrs() for _, addr := range addrs { ipNet, ok := addr.(*net.IPNet)