mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-05-27 12:52:27 +02:00
feat: add --rfc1918 flag for explicit LAN client support
Make RFC1918 listener spawning opt-in via --rfc1918 flag instead of automatic behavior. This allows users to explicitly control when ctrld listens on private network addresses to receive DNS queries from LAN clients, improving security and configurability. Refactor network interface detection to better distinguish between physical and virtual interfaces, ensuring only real hardware interfaces are used for RFC1918 address binding.
This commit is contained in:
committed by
Cuong Manh Le
parent
52cfb4c302
commit
f24059885f
@@ -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().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
||||||
_ = runCmd.Flags().MarkHidden("iface")
|
_ = runCmd.Flags().MarkHidden("iface")
|
||||||
runCmd.Flags().StringVarP(&cdUpstreamProto, "proto", "", ctrld.ResolverTypeDOH, `Control D upstream type, either "doh" or "doh3"`)
|
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}
|
runCmd.FParseErrWhitelist = cobra.FParseErrWhitelist{UnknownFlags: true}
|
||||||
rootCmd.AddCommand(runCmd)
|
rootCmd.AddCommand(runCmd)
|
||||||
|
|||||||
@@ -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(&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().BoolVarP(&startOnly, "start_only", "", false, "Do not install new service")
|
||||||
_ = startCmd.Flags().MarkHidden("start_only")
|
_ = 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
|
// Start command alias
|
||||||
startCmdAlias := &cobra.Command{
|
startCmdAlias := &cobra.Command{
|
||||||
|
|||||||
@@ -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) {
|
if needRFC1918Listeners(cfg) {
|
||||||
logger.Debug().Str("protocol", proto).Msg("Starting RFC1918 listeners")
|
logger.Debug().Str("protocol", proto).Msg("Starting RFC1918 listeners")
|
||||||
g.Go(func() error {
|
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.
|
// 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.
|
// This is helpful for non-desktop platforms to receive queries from LAN clients.
|
||||||
func needRFC1918Listeners(lc *ctrld.ListenerConfig) bool {
|
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.
|
// ipFromARPA parses a FQDN arpa domain and return the IP address if valid.
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ var (
|
|||||||
skipSelfChecks bool
|
skipSelfChecks bool
|
||||||
cleanup bool
|
cleanup bool
|
||||||
startOnly bool
|
startOnly bool
|
||||||
|
rfc1918 bool
|
||||||
|
|
||||||
mainLog atomic.Pointer[ctrld.Logger]
|
mainLog atomic.Pointer[ctrld.Logger]
|
||||||
consoleWriter zapcore.Core
|
consoleWriter zapcore.Core
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"tailscale.com/net/netmon"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile"
|
"github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,3 +132,25 @@ func virtualInterfaces() set {
|
|||||||
}
|
}
|
||||||
return s
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -444,3 +444,7 @@ func ValidInterfaces(ctx context.Context) map[string]struct{} {
|
|||||||
}
|
}
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validInterfaces() map[string]struct{} {
|
||||||
|
return ValidInterfaces(context.Background())
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cli
|
package ctrld
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
@@ -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: {}}
|
||||||
|
}
|
||||||
+6
-1
@@ -709,10 +709,15 @@ func newResolverWithNameserver(nameservers []string) *osResolver {
|
|||||||
return r
|
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 {
|
func Rfc1918Addresses() []string {
|
||||||
|
vis := validInterfaces()
|
||||||
var res []string
|
var res []string
|
||||||
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
|
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
|
||||||
|
// Skip virtual interfaces.
|
||||||
|
if _, existed := vis[i.Name]; !existed {
|
||||||
|
return
|
||||||
|
}
|
||||||
addrs, _ := i.Addrs()
|
addrs, _ := i.Addrs()
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
ipNet, ok := addr.(*net.IPNet)
|
ipNet, ok := addr.(*net.IPNet)
|
||||||
|
|||||||
Reference in New Issue
Block a user