mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-07-16 13:17:19 +02:00
Removing router platforms support
This commit is contained in:
committed by
Cuong Manh Le
parent
af1a6e9f3a
commit
ba9057e466
@@ -11,7 +11,6 @@ A highly configurable DNS forwarding proxy with support for:
|
|||||||
- Multiple upstreams with fallbacks
|
- Multiple upstreams with fallbacks
|
||||||
- Multiple network policy driven DNS query steering (via network cidr, MAC address or FQDN)
|
- Multiple network policy driven DNS query steering (via network cidr, MAC address or FQDN)
|
||||||
- Policy driven domain based "split horizon" DNS with wildcard support
|
- Policy driven domain based "split horizon" DNS with wildcard support
|
||||||
- Integrations with common router vendors and firmware
|
|
||||||
- LAN client discovery via DHCP, mDNS, ARP, NDP, hosts file parsing
|
- LAN client discovery via DHCP, mDNS, ARP, NDP, hosts file parsing
|
||||||
- Prometheus metrics exporter
|
- Prometheus metrics exporter
|
||||||
|
|
||||||
@@ -26,11 +25,10 @@ All DNS protocols are supported, including:
|
|||||||
- `DNS-over-QUIC`
|
- `DNS-over-QUIC`
|
||||||
|
|
||||||
# Use Cases
|
# Use Cases
|
||||||
1. Use secure DNS protocols on networks and devices that don't natively support them (legacy routers, legacy OSes, TVs, smart toasters).
|
1. Use secure DNS protocols on networks and devices that don't natively support them (legacy OSes, TVs, smart toasters).
|
||||||
2. Create source IP based DNS routing policies with variable secure DNS upstreams. Subnet 1 (admin) uses upstream resolver A, while Subnet 2 (employee) uses upstream resolver B.
|
2. Create source IP based DNS routing policies with variable secure DNS upstreams. Subnet 1 (admin) uses upstream resolver A, while Subnet 2 (employee) uses upstream resolver B.
|
||||||
3. Create destination IP based DNS routing policies with variable secure DNS upstreams. Listener 1 uses upstream resolver C, while Listener 2 uses upstream resolver D.
|
3. Create destination IP based DNS routing policies with variable secure DNS upstreams. Listener 1 uses upstream resolver C, while Listener 2 uses upstream resolver D.
|
||||||
4. Create domain level "split horizon" DNS routing policies to send internal domains (*.company.int) to a local DNS server, while everything else goes to another upstream.
|
4. Create domain level "split horizon" DNS routing policies to send internal domains (*.company.int) to a local DNS server, while everything else goes to another upstream.
|
||||||
5. Deploy on a router and create LAN client specific DNS routing policies from a web GUI (When using ControlD.com).
|
|
||||||
|
|
||||||
|
|
||||||
## OS Support
|
## OS Support
|
||||||
@@ -39,22 +37,6 @@ All DNS protocols are supported, including:
|
|||||||
- MacOS (amd64, arm64)
|
- MacOS (amd64, arm64)
|
||||||
- Linux (386, amd64, arm, mips)
|
- Linux (386, amd64, arm, mips)
|
||||||
- FreeBSD (386, amd64, arm)
|
- FreeBSD (386, amd64, arm)
|
||||||
- Common routers (See below)
|
|
||||||
|
|
||||||
|
|
||||||
### Supported Routers
|
|
||||||
You can run `ctrld` on any supported router. The list of supported routers and firmware includes:
|
|
||||||
- Asus Merlin
|
|
||||||
- DD-WRT
|
|
||||||
- Firewalla
|
|
||||||
- FreshTomato
|
|
||||||
- GL.iNet
|
|
||||||
- OpenWRT
|
|
||||||
- pfSense / OPNsense
|
|
||||||
- Synology
|
|
||||||
- Ubiquiti (UniFi, EdgeOS)
|
|
||||||
|
|
||||||
`ctrld` will attempt to interface with dnsmasq (or Windows Server) whenever possible and set itself as the upstream, while running on port 5354. On FreeBSD based OSes, `ctrld` will terminate dnsmasq and unbound in order to be able to listen on port 53 directly.
|
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
There are several ways to download and install `ctrld`.
|
There are several ways to download and install `ctrld`.
|
||||||
@@ -161,9 +143,7 @@ You can then run a test query using a DNS client, for example, `dig`:
|
|||||||
If `verify.controld.com` resolves, you're successfully using the default Control D upstream. From here, you can start editing the config file that was generated. To enforce a new config, restart the server.
|
If `verify.controld.com` resolves, you're successfully using the default Control D upstream. From here, you can start editing the config file that was generated. To enforce a new config, restart the server.
|
||||||
|
|
||||||
## Service Mode
|
## Service Mode
|
||||||
This mode will run the application as a background system service on any Windows, MacOS, Linux, FreeBSD distribution or supported router. This will create a generic `ctrld.toml` file in the **C:\ControlD** directory (on Windows) or `/etc/controld/` (almost everywhere else), start the system service, and **configure the listener on all physical network interface**. Service will start on OS boot.
|
This mode will run the application as a background system service on any Windows, MacOS, Linux or FreeBSD distribution. This will create a generic `ctrld.toml` file in the **C:\ControlD** directory (on Windows) or `/etc/controld/` (almost everywhere else), start the system service, and **configure the listener on all physical network interface**. Service will start on OS boot.
|
||||||
|
|
||||||
When Control D upstreams are used on a router type device, `ctrld` will [relay your network topology](https://docs.controld.com/docs/device-clients) to Control D (LAN IPs, MAC addresses, and hostnames), and you will be able to see your LAN devices in the web panel, view analytics and apply unique profiles to them.
|
|
||||||
|
|
||||||
### Command
|
### Command
|
||||||
|
|
||||||
|
|||||||
+8
-70
@@ -40,7 +40,6 @@ import (
|
|||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/controld"
|
"github.com/Control-D-Inc/ctrld/internal/controld"
|
||||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// selfCheckInternalTestDomain is used for testing ctrld self response to clients.
|
// selfCheckInternalTestDomain is used for testing ctrld self response to clients.
|
||||||
@@ -290,21 +289,12 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
|||||||
p.Fatal().Msg("network is not up yet")
|
p.Fatal().Msg("network is not up yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
p.router = router.New(&cfg, cdUID != "")
|
|
||||||
cs, err := newControlServer(filepath.Join(sockDir, ControlSocketName()))
|
cs, err := newControlServer(filepath.Join(sockDir, ControlSocketName()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.Warn().Err(err).Msg("could not create control server")
|
p.Warn().Err(err).Msg("could not create control server")
|
||||||
}
|
}
|
||||||
p.cs = cs
|
p.cs = cs
|
||||||
|
|
||||||
// Processing --cd flag require connecting to ControlD API, which needs valid
|
|
||||||
// time for validating server certificate. Some routers need NTP synchronization
|
|
||||||
// to set the current time, so this check must happen before processCDFlags.
|
|
||||||
if err := p.router.PreRun(); err != nil {
|
|
||||||
notifyExitToLogServer()
|
|
||||||
p.Fatal().Err(err).Msg("failed to perform router pre-run check")
|
|
||||||
}
|
|
||||||
|
|
||||||
oldLogPath := cfg.Service.LogPath
|
oldLogPath := cfg.Service.LogPath
|
||||||
if uid := cdUIDFromProvToken(); uid != "" {
|
if uid := cdUIDFromProvToken(); uid != "" {
|
||||||
cdUID = uid
|
cdUID = uid
|
||||||
@@ -413,25 +403,6 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if platform := router.Name(); platform != "" {
|
|
||||||
if cp := router.CertPool(); cp != nil {
|
|
||||||
rootCertPool = cp
|
|
||||||
}
|
|
||||||
if iface != "" {
|
|
||||||
p.onStarted = append(p.onStarted, func() {
|
|
||||||
p.Debug().Msg("router setup on start")
|
|
||||||
if err := p.router.Setup(); err != nil {
|
|
||||||
p.Error().Err(err).Msg("could not configure router")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
p.onStopped = append(p.onStopped, func() {
|
|
||||||
p.Debug().Msg("router cleanup on stop")
|
|
||||||
if err := p.router.Cleanup(); err != nil {
|
|
||||||
p.Error().Err(err).Msg("could not cleanup router")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.onStopped = append(p.onStopped, func() {
|
p.onStopped = append(p.onStopped, func() {
|
||||||
// restore static DNS settings or DHCP
|
// restore static DNS settings or DHCP
|
||||||
p.resetDNS(false, true)
|
p.resetDNS(false, true)
|
||||||
@@ -809,9 +780,6 @@ func netInterface(ifaceName string) (*net.Interface, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultIfaceName() string {
|
func defaultIfaceName() string {
|
||||||
if ifaceName := router.DefaultInterfaceName(); ifaceName != "" {
|
|
||||||
return ifaceName
|
|
||||||
}
|
|
||||||
dri, err := netmon.DefaultRouteInterface()
|
dri, err := netmon.DefaultRouteInterface()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// On WSL 1, the route table does not have any default route. But the fact that
|
// On WSL 1, the route table does not have any default route. But the fact that
|
||||||
@@ -962,13 +930,6 @@ func selfCheckResolveDomain(ctx context.Context, addr, scope string, domain stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func userHomeDir() (string, error) {
|
func userHomeDir() (string, error) {
|
||||||
dir, err := router.HomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if dir != "" {
|
|
||||||
return dir, nil
|
|
||||||
}
|
|
||||||
// Mobile platform should provide a rw dir path for this.
|
// Mobile platform should provide a rw dir path for this.
|
||||||
if isMobile() {
|
if isMobile() {
|
||||||
return homedir, nil
|
return homedir, nil
|
||||||
@@ -1051,13 +1012,6 @@ func uninstall(p *prog, s service.Service) {
|
|||||||
}
|
}
|
||||||
initInteractiveLogging()
|
initInteractiveLogging()
|
||||||
if doTasks(tasks) {
|
if doTasks(tasks) {
|
||||||
if err := p.router.ConfigureService(svcConfig); err != nil {
|
|
||||||
mainLog.Load().Fatal().Err(err).Msg("could not configure service")
|
|
||||||
}
|
|
||||||
if err := p.router.Uninstall(svcConfig); err != nil {
|
|
||||||
mainLog.Load().Warn().Err(err).Msg("post uninstallation failed, please check system/service log for details error")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// restore static DNS settings or DHCP
|
// restore static DNS settings or DHCP
|
||||||
p.resetDNS(false, true)
|
p.resetDNS(false, true)
|
||||||
|
|
||||||
@@ -1078,12 +1032,6 @@ func uninstall(p *prog, s service.Service) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if router.Name() != "" {
|
|
||||||
mainLog.Load().Debug().Msg("Router cleanup")
|
|
||||||
}
|
|
||||||
// Stop already did router.Cleanup and report any error if happens,
|
|
||||||
// ignoring error here to prevent false positive.
|
|
||||||
_ = p.router.Cleanup()
|
|
||||||
mainLog.Load().Notice().Msg("Service uninstalled")
|
mainLog.Load().Notice().Msg("Service uninstalled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1201,7 +1149,6 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, notifyFunc func(), fatal bool) (
|
|||||||
nextdnsMode := nextdns != ""
|
nextdnsMode := nextdns != ""
|
||||||
// For Windows server with local Dns server running, we can only try on random local IP.
|
// For Windows server with local Dns server running, we can only try on random local IP.
|
||||||
hasLocalDnsServer := hasLocalDnsServerRunning()
|
hasLocalDnsServer := hasLocalDnsServerRunning()
|
||||||
notRouter := router.Name() == ""
|
|
||||||
isDesktop := ctrld.IsDesktopPlatform()
|
isDesktop := ctrld.IsDesktopPlatform()
|
||||||
for n, listener := range cfg.Listener {
|
for n, listener := range cfg.Listener {
|
||||||
lcc[n] = &listenerConfigCheck{}
|
lcc[n] = &listenerConfigCheck{}
|
||||||
@@ -1309,21 +1256,19 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, notifyFunc func(), fatal bool) (
|
|||||||
|
|
||||||
// On firewalla, we don't need to check localhost, because the lo interface is excluded in dnsmasq
|
// On firewalla, we don't need to check localhost, because the lo interface is excluded in dnsmasq
|
||||||
// config, so we can always listen on localhost port 53, but no traffic could be routed there.
|
// config, so we can always listen on localhost port 53, but no traffic could be routed there.
|
||||||
tryLocalhost := !isLoopback(listener.IP) && router.CanListenLocalhost()
|
tryLocalhost := !isLoopback(listener.IP)
|
||||||
tryAllPort53 := true
|
tryAllPort53 := true
|
||||||
tryOldIPPort5354 := true
|
// We should not try to listen on any port other than 53,
|
||||||
tryPort5354 := true
|
// if we do, this will break the dns resolution for the system.
|
||||||
|
// TODO: cleanup these codes when refactoring this function.
|
||||||
|
tryOldIPPort5354 := false
|
||||||
|
tryPort5354 := false
|
||||||
if hasLocalDnsServer {
|
if hasLocalDnsServer {
|
||||||
tryAllPort53 = false
|
tryAllPort53 = false
|
||||||
tryOldIPPort5354 = false
|
tryOldIPPort5354 = false
|
||||||
tryPort5354 = false
|
tryPort5354 = false
|
||||||
}
|
}
|
||||||
// if not running on a router, we should not try to listen on any port other than 53
|
|
||||||
// if we do, this will break the dns resolution for the system.
|
|
||||||
if notRouter {
|
|
||||||
tryOldIPPort5354 = false
|
|
||||||
tryPort5354 = false
|
|
||||||
}
|
|
||||||
attempts := 0
|
attempts := 0
|
||||||
maxAttempts := 10
|
maxAttempts := 10
|
||||||
for {
|
for {
|
||||||
@@ -1400,9 +1345,7 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, notifyFunc func(), fatal bool) (
|
|||||||
} else {
|
} else {
|
||||||
listener.IP = oldIP
|
listener.IP = oldIP
|
||||||
}
|
}
|
||||||
// if we are not running on a router, we should not try to listen on any port other than 53
|
if check.Port {
|
||||||
// if we do, this will break the dns resolution for the system.
|
|
||||||
if check.Port && !notRouter {
|
|
||||||
listener.Port = randomPort()
|
listener.Port = randomPort()
|
||||||
} else {
|
} else {
|
||||||
listener.Port = oldPort
|
listener.Port = oldPort
|
||||||
@@ -1738,11 +1681,6 @@ func exchangeContextWithTimeout(c *dns.Client, timeout time.Duration, msg *dns.M
|
|||||||
return c.ExchangeContext(ctx, msg, addr)
|
return c.ExchangeContext(ctx, msg, addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// runInCdMode reports whether ctrld service is running in cd mode.
|
|
||||||
func runInCdMode() bool {
|
|
||||||
return curCdUID() != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// curCdUID returns the current ControlD UID used by running ctrld process.
|
// curCdUID returns the current ControlD UID used by running ctrld process.
|
||||||
func curCdUID() string {
|
func curCdUID() string {
|
||||||
if s, _ := newService(&prog{}, svcConfig); s != nil {
|
if s, _ := newService(&prog{}, svcConfig); s != nil {
|
||||||
|
|||||||
+11
-116
@@ -23,11 +23,9 @@ import (
|
|||||||
"github.com/minio/selfupdate"
|
"github.com/minio/selfupdate"
|
||||||
"github.com/olekukonko/tablewriter"
|
"github.com/olekukonko/tablewriter"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/clientinfo"
|
"github.com/Control-D-Inc/ctrld/internal/clientinfo"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// dialSocketControlServerTimeout is the default timeout to wait when ping control server.
|
// dialSocketControlServerTimeout is the default timeout to wait when ping control server.
|
||||||
@@ -47,7 +45,7 @@ func initLogCmd() *cobra.Command {
|
|||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
p := &prog{router: router.New(&cfg, false)}
|
p := &prog{}
|
||||||
s, _ := newService(p, svcConfig)
|
s, _ := newService(p, svcConfig)
|
||||||
|
|
||||||
status, err := s.Status()
|
status, err := s.Status()
|
||||||
@@ -100,7 +98,7 @@ func initLogCmd() *cobra.Command {
|
|||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
p := &prog{router: router.New(&cfg, false)}
|
p := &prog{}
|
||||||
s, _ := newService(p, svcConfig)
|
s, _ := newService(p, svcConfig)
|
||||||
|
|
||||||
status, err := s.Status()
|
status, err := s.Status()
|
||||||
@@ -225,10 +223,7 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
|||||||
setDependencies(sc)
|
setDependencies(sc)
|
||||||
sc.Arguments = append([]string{"run"}, osArgs...)
|
sc.Arguments = append([]string{"run"}, osArgs...)
|
||||||
|
|
||||||
p := &prog{
|
p := &prog{cfg: &cfg}
|
||||||
router: router.New(&cfg, cdUID != ""),
|
|
||||||
cfg: &cfg,
|
|
||||||
}
|
|
||||||
s, err := newService(p, sc)
|
s, err := newService(p, sc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Load().Error().Msg(err.Error())
|
mainLog.Load().Error().Msg(err.Error())
|
||||||
@@ -400,10 +395,6 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
|||||||
validateCdUpstreamProtocol()
|
validateCdUpstreamProtocol()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.router.ConfigureService(sc); err != nil {
|
|
||||||
mainLog.Load().Fatal().Err(err).Msg("failed to configure service on router")
|
|
||||||
}
|
|
||||||
|
|
||||||
if configPath != "" {
|
if configPath != "" {
|
||||||
v.SetConfigFile(configPath)
|
v.SetConfigFile(configPath)
|
||||||
}
|
}
|
||||||
@@ -427,11 +418,6 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
|||||||
sc.Arguments = append(sc.Arguments, "--config="+defaultConfigFile)
|
sc.Arguments = append(sc.Arguments, "--config="+defaultConfigFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if router.Name() != "" && iface != "" {
|
|
||||||
mainLog.Load().Debug().Msg("cleaning up router before installing")
|
|
||||||
_ = p.router.Cleanup()
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks := []task{
|
tasks := []task{
|
||||||
{s.Stop, false, "Stop"},
|
{s.Stop, false, "Stop"},
|
||||||
{func() error { return doGenerateNextDNSConfig(nextdns) }, true, "Checking config"},
|
{func() error { return doGenerateNextDNSConfig(nextdns) }, true, "Checking config"},
|
||||||
@@ -458,11 +444,6 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
|||||||
}
|
}
|
||||||
mainLog.Load().Notice().Msg("Starting service")
|
mainLog.Load().Notice().Msg("Starting service")
|
||||||
if doTasks(tasks) {
|
if doTasks(tasks) {
|
||||||
if err := p.router.Install(sc); err != nil {
|
|
||||||
mainLog.Load().Warn().Err(err).Msg("post installation failed, please check system/service log for details error")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// add a small delay to ensure the service is started and did not crash
|
// add a small delay to ensure the service is started and did not crash
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
@@ -529,33 +510,6 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
|||||||
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")
|
||||||
|
|
||||||
routerCmd := &cobra.Command{
|
|
||||||
Use: "setup",
|
|
||||||
Run: func(cmd *cobra.Command, _ []string) {
|
|
||||||
exe, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Load().Fatal().Msgf("could not find executable path: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
flags := make([]string, 0)
|
|
||||||
cmd.Flags().Visit(func(flag *pflag.Flag) {
|
|
||||||
flags = append(flags, fmt.Sprintf("--%s=%s", flag.Name, flag.Value))
|
|
||||||
})
|
|
||||||
cmdArgs := []string{"start"}
|
|
||||||
cmdArgs = append(cmdArgs, flags...)
|
|
||||||
command := exec.Command(exe, cmdArgs...)
|
|
||||||
command.Stdout = os.Stdout
|
|
||||||
command.Stderr = os.Stderr
|
|
||||||
command.Stdin = os.Stdin
|
|
||||||
if err := command.Run(); err != nil {
|
|
||||||
mainLog.Load().Fatal().Msg(err.Error())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
routerCmd.Flags().AddFlagSet(startCmd.Flags())
|
|
||||||
routerCmd.Hidden = true
|
|
||||||
rootCmd.AddCommand(routerCmd)
|
|
||||||
|
|
||||||
startCmdAlias := &cobra.Command{
|
startCmdAlias := &cobra.Command{
|
||||||
PreRun: func(cmd *cobra.Command, args []string) {
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
checkHasElevatedPrivilege()
|
checkHasElevatedPrivilege()
|
||||||
@@ -601,7 +555,7 @@ func initStopCmd() *cobra.Command {
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
readConfig(false)
|
readConfig(false)
|
||||||
v.Unmarshal(&cfg)
|
v.Unmarshal(&cfg)
|
||||||
p := &prog{router: router.New(&cfg, runInCdMode())}
|
p := &prog{}
|
||||||
s, err := newService(p, svcConfig)
|
s, err := newService(p, svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Load().Error().Msg(err.Error())
|
mainLog.Load().Error().Msg(err.Error())
|
||||||
@@ -629,23 +583,6 @@ func initStopCmd() *cobra.Command {
|
|||||||
os.Exit(deactivationPinInvalidExitCode)
|
os.Exit(deactivationPinInvalidExitCode)
|
||||||
}
|
}
|
||||||
if doTasks([]task{{s.Stop, true, "Stop"}}) {
|
if doTasks([]task{{s.Stop, true, "Stop"}}) {
|
||||||
if router.WaitProcessExited() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
mainLog.Load().Error().Msg("timeout while waiting for service to stop")
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
if status, _ := s.Status(); status == service.StatusStopped {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mainLog.Load().Notice().Msg("Service stopped")
|
mainLog.Load().Notice().Msg("Service stopped")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -689,7 +626,7 @@ func initRestartCmd() *cobra.Command {
|
|||||||
cdUID = curCdUID()
|
cdUID = curCdUID()
|
||||||
cdMode := cdUID != ""
|
cdMode := cdUID != ""
|
||||||
|
|
||||||
p := &prog{router: router.New(&cfg, cdMode)}
|
p := &prog{}
|
||||||
s, err := newService(p, svcConfig)
|
s, err := newService(p, svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Load().Error().Msg(err.Error())
|
mainLog.Load().Error().Msg(err.Error())
|
||||||
@@ -723,7 +660,6 @@ func initRestartCmd() *cobra.Command {
|
|||||||
tasks := []task{
|
tasks := []task{
|
||||||
{s.Stop, true, "Stop"},
|
{s.Stop, true, "Stop"},
|
||||||
{func() error {
|
{func() error {
|
||||||
p.router.Cleanup()
|
|
||||||
// restore static DNS settings or DHCP
|
// restore static DNS settings or DHCP
|
||||||
p.resetDNS(false, true)
|
p.resetDNS(false, true)
|
||||||
return nil
|
return nil
|
||||||
@@ -733,27 +669,7 @@ func initRestartCmd() *cobra.Command {
|
|||||||
return nil
|
return nil
|
||||||
}, false, "Waiting for service to stop"},
|
}, false, "Waiting for service to stop"},
|
||||||
}
|
}
|
||||||
if doTasks(tasks) {
|
if !doTasks(tasks) {
|
||||||
|
|
||||||
if router.WaitProcessExited() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
mainLog.Load().Error().Msg("timeout while waiting for service to stop")
|
|
||||||
break loop
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
if status, _ := s.Status(); status == service.StatusStopped {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -814,7 +730,7 @@ func initReloadCmd(restartCmd *cobra.Command) *cobra.Command {
|
|||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
p := &prog{router: router.New(&cfg, false)}
|
p := &prog{}
|
||||||
s, _ := newService(p, svcConfig)
|
s, _ := newService(p, svcConfig)
|
||||||
|
|
||||||
status, err := s.Status()
|
status, err := s.Status()
|
||||||
@@ -939,7 +855,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
readConfig(false)
|
readConfig(false)
|
||||||
v.Unmarshal(&cfg)
|
v.Unmarshal(&cfg)
|
||||||
p := &prog{router: router.New(&cfg, runInCdMode())}
|
p := &prog{}
|
||||||
s, err := newService(p, svcConfig)
|
s, err := newService(p, svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Load().Error().Msg(err.Error())
|
mainLog.Load().Error().Msg(err.Error())
|
||||||
@@ -1115,7 +1031,7 @@ func initClientsCmd() *cobra.Command {
|
|||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
p := &prog{router: router.New(&cfg, false)}
|
p := &prog{}
|
||||||
s, _ := newService(p, svcConfig)
|
s, _ := newService(p, svcConfig)
|
||||||
|
|
||||||
status, err := s.Status()
|
status, err := s.Status()
|
||||||
@@ -1228,7 +1144,7 @@ func initUpgradeCmd() *cobra.Command {
|
|||||||
sc.Executable = bin
|
sc.Executable = bin
|
||||||
readConfig(false)
|
readConfig(false)
|
||||||
v.Unmarshal(&cfg)
|
v.Unmarshal(&cfg)
|
||||||
p := &prog{router: router.New(&cfg, runInCdMode())}
|
p := &prog{}
|
||||||
s, err := newService(p, sc)
|
s, err := newService(p, sc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Load().Error().Msg(err.Error())
|
mainLog.Load().Error().Msg(err.Error())
|
||||||
@@ -1285,7 +1201,6 @@ func initUpgradeCmd() *cobra.Command {
|
|||||||
tasks := []task{
|
tasks := []task{
|
||||||
{s.Stop, true, "Stop"},
|
{s.Stop, true, "Stop"},
|
||||||
{func() error {
|
{func() error {
|
||||||
p.router.Cleanup()
|
|
||||||
// restore static DNS settings or DHCP
|
// restore static DNS settings or DHCP
|
||||||
p.resetDNS(false, true)
|
p.resetDNS(false, true)
|
||||||
return nil
|
return nil
|
||||||
@@ -1295,27 +1210,7 @@ func initUpgradeCmd() *cobra.Command {
|
|||||||
return nil
|
return nil
|
||||||
}, false, "Waiting for service to stop"},
|
}, false, "Waiting for service to stop"},
|
||||||
}
|
}
|
||||||
if doTasks(tasks) {
|
doTasks(tasks)
|
||||||
|
|
||||||
if router.WaitProcessExited() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
mainLog.Load().Error().Msg("timeout while waiting for service to stop")
|
|
||||||
break loop
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
if status, _ := s.Status(); status == service.StatusStopped {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks = []task{
|
tasks = []task{
|
||||||
{s.Start, true, "Start"},
|
{s.Start, true, "Start"},
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import (
|
|||||||
"github.com/Control-D-Inc/ctrld/internal/controld"
|
"github.com/Control-D-Inc/ctrld/internal/controld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
||||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -1405,10 +1404,7 @@ func (p *prog) monitorNetworkChanges(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
p.Debug().Msgf("Set default local IPv4: %s, IPv6: %s", selfIP, ipv6)
|
p.Debug().Msgf("Set default local IPv4: %s, IPv6: %s", selfIP, ipv6)
|
||||||
|
|
||||||
// we only trigger recovery flow for network changes on non router devices
|
p.handleRecovery(RecoveryReasonNetworkChange)
|
||||||
if router.Name() == "" {
|
|
||||||
p.handleRecovery(RecoveryReasonNetworkChange)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
mon.Start()
|
mon.Start()
|
||||||
|
|||||||
+1
-16
@@ -34,8 +34,6 @@ import (
|
|||||||
"github.com/Control-D-Inc/ctrld/internal/clientinfo"
|
"github.com/Control-D-Inc/ctrld/internal/clientinfo"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/controld"
|
"github.com/Control-D-Inc/ctrld/internal/controld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -120,7 +118,6 @@ type prog struct {
|
|||||||
sema semaphore
|
sema semaphore
|
||||||
ciTable *clientinfo.Table
|
ciTable *clientinfo.Table
|
||||||
um *upstreamMonitor
|
um *upstreamMonitor
|
||||||
router router.Router
|
|
||||||
ptrLoopGuard *loopGuard
|
ptrLoopGuard *loopGuard
|
||||||
lanLoopGuard *loopGuard
|
lanLoopGuard *loopGuard
|
||||||
metricsQueryStats atomic.Bool
|
metricsQueryStats atomic.Bool
|
||||||
@@ -612,12 +609,6 @@ func (p *prog) setupClientInfoDiscover() {
|
|||||||
format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat)
|
format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat)
|
||||||
p.ciTable.AddLeaseFile(leaseFile, format)
|
p.ciTable.AddLeaseFile(leaseFile, format)
|
||||||
}
|
}
|
||||||
if leaseFiles := dnsmasq.AdditionalLeaseFiles(); len(leaseFiles) > 0 {
|
|
||||||
mainLog.Load().Debug().Msgf("watching additional lease files: %v", leaseFiles)
|
|
||||||
for _, leaseFile := range leaseFiles {
|
|
||||||
p.ciTable.AddLeaseFile(leaseFile, ctrld.Dnsmasq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// runClientInfoDiscover runs the client info discover.
|
// runClientInfoDiscover runs the client info discover.
|
||||||
@@ -724,9 +715,6 @@ func (p *prog) setDNS() {
|
|||||||
ns = "127.0.0.1"
|
ns = "127.0.0.1"
|
||||||
case lc.Port != 53:
|
case lc.Port != 53:
|
||||||
ns = "127.0.0.1"
|
ns = "127.0.0.1"
|
||||||
if resolver := router.LocalResolverIP(); resolver != "" {
|
|
||||||
ns = resolver
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
// If we ever reach here, it means ctrld is running on lc.IP port 53,
|
// If we ever reach here, it means ctrld is running on lc.IP port 53,
|
||||||
// so we could just use lc.IP as nameserver.
|
// so we could just use lc.IP as nameserver.
|
||||||
@@ -1493,10 +1481,7 @@ func (p *prog) leakOnUpstreamFailure() bool {
|
|||||||
if ptr := p.cfg.Service.LeakOnUpstreamFailure; ptr != nil {
|
if ptr := p.cfg.Service.LeakOnUpstreamFailure; ptr != nil {
|
||||||
return *ptr
|
return *ptr
|
||||||
}
|
}
|
||||||
// Default is false on routers, since this leaking is only useful for devices that move between networks.
|
|
||||||
if router.Name() != "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// if we are running on ADDC, we should not leak on upstream failure
|
// if we are running on ADDC, we should not leak on upstream failure
|
||||||
if p.runningOnDomainController {
|
if p.runningOnDomainController {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -37,9 +35,6 @@ func setDependencies(svc *service.Config) {
|
|||||||
svc.Dependencies = append(svc.Dependencies, "Wants=systemd-networkd-wait-online.service")
|
svc.Dependencies = append(svc.Dependencies, "Wants=systemd-networkd-wait-online.service")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if routerDeps := router.ServiceDependencies(); len(routerDeps) > 0 {
|
|
||||||
svc.Dependencies = append(svc.Dependencies, routerDeps...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setWorkingDirectory(svc *service.Config, dir string) {
|
func setWorkingDirectory(svc *service.Config, dir string) {
|
||||||
|
|||||||
+1
-46
@@ -11,9 +11,6 @@ import (
|
|||||||
|
|
||||||
"github.com/coreos/go-systemd/v22/unit"
|
"github.com/coreos/go-systemd/v22/unit"
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/openwrt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// newService wraps service.New call to return service.Service
|
// newService wraps service.New call to return service.Service
|
||||||
@@ -24,10 +21,6 @@ func newService(i service.Interface, c *service.Config) (service.Service, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case router.IsOldOpenwrt(), router.IsNetGearOrbi():
|
|
||||||
return &procd{sysV: &sysV{s}, svcConfig: c}, nil
|
|
||||||
case router.IsGLiNet():
|
|
||||||
return &sysV{s}, nil
|
|
||||||
case s.Platform() == "unix-systemv":
|
case s.Platform() == "unix-systemv":
|
||||||
return &sysV{s}, nil
|
return &sysV{s}, nil
|
||||||
case s.Platform() == "linux-systemd":
|
case s.Platform() == "linux-systemd":
|
||||||
@@ -42,7 +35,7 @@ func newService(i service.Interface, c *service.Config) (service.Service, error)
|
|||||||
// sysV wraps a service.Service, and provide start/stop/status command
|
// sysV wraps a service.Service, and provide start/stop/status command
|
||||||
// base on "/etc/init.d/<service_name>".
|
// base on "/etc/init.d/<service_name>".
|
||||||
//
|
//
|
||||||
// Use this on system where "service" command is not available, like GL.iNET router.
|
// Use this on system where "service" command is not available.
|
||||||
type sysV struct {
|
type sysV struct {
|
||||||
service.Service
|
service.Service
|
||||||
}
|
}
|
||||||
@@ -89,37 +82,6 @@ func (s *sysV) Status() (service.Status, error) {
|
|||||||
return unixSystemVServiceStatus()
|
return unixSystemVServiceStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
// procd wraps a service.Service, and provide start/stop command
|
|
||||||
// base on "/etc/init.d/<service_name>", status command base on parsing "ps" command output.
|
|
||||||
//
|
|
||||||
// Use this on system where "/etc/init.d/<service_name> status" command is not available,
|
|
||||||
// like old GL.iNET Opal router.
|
|
||||||
type procd struct {
|
|
||||||
*sysV
|
|
||||||
svcConfig *service.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *procd) Status() (service.Status, error) {
|
|
||||||
if !s.installed() {
|
|
||||||
return service.StatusUnknown, service.ErrNotInstalled
|
|
||||||
}
|
|
||||||
bin := s.svcConfig.Executable
|
|
||||||
if bin == "" {
|
|
||||||
exe, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return service.StatusUnknown, nil
|
|
||||||
}
|
|
||||||
bin = exe
|
|
||||||
}
|
|
||||||
|
|
||||||
// Looking for something like "/sbin/ctrld run ".
|
|
||||||
shellCmd := fmt.Sprintf("ps | grep -q %q", bin+" [r]un ")
|
|
||||||
if err := exec.Command("sh", "-c", shellCmd).Run(); err != nil {
|
|
||||||
return service.StatusStopped, nil
|
|
||||||
}
|
|
||||||
return service.StatusRunning, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// systemd wraps a service.Service, and provide status command to
|
// systemd wraps a service.Service, and provide status command to
|
||||||
// report the status correctly.
|
// report the status correctly.
|
||||||
type systemd struct {
|
type systemd struct {
|
||||||
@@ -249,13 +211,6 @@ func checkHasElevatedPrivilege() {
|
|||||||
func unixSystemVServiceStatus() (service.Status, error) {
|
func unixSystemVServiceStatus() (service.Status, error) {
|
||||||
out, err := exec.Command("/etc/init.d/ctrld", "status").CombinedOutput()
|
out, err := exec.Command("/etc/init.d/ctrld", "status").CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Specific case for openwrt >= 24.10, it returns non-success code
|
|
||||||
// for above status command, which may not right.
|
|
||||||
if router.Name() == openwrt.Name {
|
|
||||||
if string(bytes.ToLower(bytes.TrimSpace(out))) == "inactive" {
|
|
||||||
return service.StatusStopped, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return service.StatusUnknown, nil
|
return service.StatusUnknown, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -18,10 +18,6 @@ The config file allows for advanced configuration of the `ctrld` utility to cove
|
|||||||
|
|
||||||
- `/etc/controld` on *nix.
|
- `/etc/controld` on *nix.
|
||||||
- User's home directory on Windows.
|
- User's home directory on Windows.
|
||||||
- Same directory with `ctrld` binary on these routers:
|
|
||||||
- `ddwrt`
|
|
||||||
- `merlin`
|
|
||||||
- `freshtomato`
|
|
||||||
- Current directory.
|
- Current directory.
|
||||||
|
|
||||||
The user can choose to override default value using command line `--config` or `-c`:
|
The user can choose to override default value using command line `--config` or `-c`:
|
||||||
@@ -293,7 +289,7 @@ If a remote upstream fails to resolve a query or is unreachable, `ctrld` will fo
|
|||||||
|
|
||||||
- Type: boolean
|
- Type: boolean
|
||||||
- Required: no
|
- Required: no
|
||||||
- Default: true on Windows, MacOS and non-router Linux.
|
- Default: true on Windows, MacOS and Linux.
|
||||||
|
|
||||||
## Upstream
|
## Upstream
|
||||||
The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to.
|
The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to.
|
||||||
|
|||||||
@@ -82,8 +82,6 @@ type Table struct {
|
|||||||
logger *ctrld.Logger
|
logger *ctrld.Logger
|
||||||
|
|
||||||
dhcp *dhcp
|
dhcp *dhcp
|
||||||
merlin *merlinDiscover
|
|
||||||
ubios *ubiosDiscover
|
|
||||||
arp *arpDiscover
|
arp *arpDiscover
|
||||||
ndp *ndpDiscover
|
ndp *ndpDiscover
|
||||||
ptr *ptrDiscover
|
ptr *ptrDiscover
|
||||||
@@ -206,30 +204,6 @@ func (t *Table) init() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, process all possible sources in order, that means
|
|
||||||
// the first result of IP/MAC/Hostname lookup will be used.
|
|
||||||
//
|
|
||||||
// Routers custom clients:
|
|
||||||
// - Merlin
|
|
||||||
// - Ubios
|
|
||||||
if t.discoverDHCP() || t.discoverARP() {
|
|
||||||
t.merlin = &merlinDiscover{logger: t.logger}
|
|
||||||
t.ubios = &ubiosDiscover{}
|
|
||||||
discovers := map[string]interface {
|
|
||||||
refresher
|
|
||||||
HostnameResolver
|
|
||||||
}{
|
|
||||||
"Merlin": t.merlin,
|
|
||||||
"Ubios": t.ubios,
|
|
||||||
}
|
|
||||||
for platform, discover := range discovers {
|
|
||||||
if err := discover.refresh(); err != nil {
|
|
||||||
t.logger.Warn().Err(err).Msgf("failed to init %s discover", platform)
|
|
||||||
}
|
|
||||||
t.hostnameResolvers = append(t.hostnameResolvers, discover)
|
|
||||||
t.refreshers = append(t.refreshers, discover)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Hosts file mapping.
|
// Hosts file mapping.
|
||||||
if t.discoverHosts() {
|
if t.discoverHosts() {
|
||||||
t.hf = &hostsFile{logger: t.logger}
|
t.hf = &hostsFile{logger: t.logger}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
"tailscale.com/util/lineread"
|
"tailscale.com/util/lineread"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type dhcp struct {
|
type dhcp struct {
|
||||||
@@ -39,10 +38,6 @@ func (d *dhcp) init() error {
|
|||||||
}
|
}
|
||||||
d.addSelf()
|
d.addSelf()
|
||||||
d.watcher = watcher
|
d.watcher = watcher
|
||||||
for file, format := range clientInfoFiles {
|
|
||||||
// Ignore errors for default lease files.
|
|
||||||
_ = d.addLeaseFile(file, format)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,11 +45,7 @@ func (d *dhcp) watchChanges() {
|
|||||||
if d.watcher == nil {
|
if d.watcher == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if dir := router.LeaseFilesDir(); dir != "" {
|
|
||||||
if err := d.watcher.Add(dir); err != nil {
|
|
||||||
d.logger.Err(err).Str("dir", dir).Msg("could not watch lease dir")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event, ok := <-d.watcher.Events:
|
case event, ok := <-d.watcher.Events:
|
||||||
@@ -390,22 +381,4 @@ func (d *dhcp) addSelf() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
for _, netIface := range router.SelfInterfaces() {
|
|
||||||
mac := netIface.HardwareAddr.String()
|
|
||||||
if mac == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.mac2name.Store(mac, hostname)
|
|
||||||
addrs, _ := netIface.Addrs()
|
|
||||||
for _, addr := range addrs {
|
|
||||||
ipNet, ok := addr.(*net.IPNet)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ip := ipNet.IP
|
|
||||||
d.mac.LoadOrStore(ip.String(), mac)
|
|
||||||
d.ip.LoadOrStore(mac, ip.String())
|
|
||||||
d.ip2name.Store(ip.String(), hostname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,5 @@ package clientinfo
|
|||||||
import "github.com/Control-D-Inc/ctrld"
|
import "github.com/Control-D-Inc/ctrld"
|
||||||
|
|
||||||
// clientInfoFiles specifies client info files and how to read them on supported platforms.
|
// clientInfoFiles specifies client info files and how to read them on supported platforms.
|
||||||
var clientInfoFiles = map[string]ctrld.LeaseFileFormat{
|
// TODO: cleanup this after server support removal.
|
||||||
"/tmp/dnsmasq.leases": ctrld.Dnsmasq, // ddwrt
|
var clientInfoFiles = map[string]ctrld.LeaseFileFormat{}
|
||||||
"/tmp/dhcp.leases": ctrld.Dnsmasq, // openwrt
|
|
||||||
"/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // merlin
|
|
||||||
"/mnt/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDM Pro
|
|
||||||
"/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDR
|
|
||||||
"/etc/dhcpd/dhcpd-leases.log": ctrld.Dnsmasq, // Synology
|
|
||||||
"/tmp/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // Tomato
|
|
||||||
"/run/dnsmasq-dhcp.leases": ctrld.Dnsmasq, // EdgeOS
|
|
||||||
"/run/dhcpd.leases": ctrld.IscDhcpd, // EdgeOS
|
|
||||||
"/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense
|
|
||||||
"/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla
|
|
||||||
"/var/lib/kea/dhcp4.leases": ctrld.KeaDHCP4, // Pfsense
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
package clientinfo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/merlin"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
|
||||||
)
|
|
||||||
|
|
||||||
const merlinNvramCustomClientListKey = "custom_clientlist"
|
|
||||||
|
|
||||||
type merlinDiscover struct {
|
|
||||||
hostname sync.Map // mac => hostname
|
|
||||||
logger *ctrld.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *merlinDiscover) refresh() error {
|
|
||||||
if router.Name() != merlin.Name {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out, err := nvram.Run("get", merlinNvramCustomClientListKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.logger.Debug().Msg("reading Merlin custom client list")
|
|
||||||
m.parseMerlinCustomClientList(out)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *merlinDiscover) LookupHostnameByIP(ip string) string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *merlinDiscover) LookupHostnameByMac(mac string) string {
|
|
||||||
val, ok := m.hostname.Load(mac)
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return val.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// "nvram get custom_clientlist" output:
|
|
||||||
//
|
|
||||||
// <client 1>00:00:00:00:00:01>0>4>><client 2>00:00:00:00:00:02>0>24>>...
|
|
||||||
//
|
|
||||||
// So to parse it, do the following steps:
|
|
||||||
//
|
|
||||||
// - Split by "<" => entries
|
|
||||||
// - For each entry, split by ">" => parts
|
|
||||||
// - Empty parts => skip
|
|
||||||
// - Empty parts[0] => skip empty hostname
|
|
||||||
// - Empty parts[1] => skip empty MAC
|
|
||||||
func (m *merlinDiscover) parseMerlinCustomClientList(data string) {
|
|
||||||
entries := strings.Split(data, "<")
|
|
||||||
for _, entry := range entries {
|
|
||||||
parts := strings.SplitN(string(entry), ">", 3)
|
|
||||||
if len(parts) < 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
hostname := normalizeHostname(parts[0])
|
|
||||||
mac := strings.ToLower(parts[1])
|
|
||||||
m.hostname.Store(mac, hostname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *merlinDiscover) String() string {
|
|
||||||
return "merlin"
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package clientinfo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseMerlinCustomClientList(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
clientList string
|
|
||||||
macList []string
|
|
||||||
hostnameList []string
|
|
||||||
macNotPresentList []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"normal",
|
|
||||||
"<client1>00:00:00:00:00:01>0>4>>",
|
|
||||||
[]string{"00:00:00:00:00:01"},
|
|
||||||
[]string{"client1"},
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"multiple clients",
|
|
||||||
"<client1>00:00:00:00:00:01>0>4>><client2>00:00:00:00:00:02>0>24>>",
|
|
||||||
[]string{"00:00:00:00:00:01", "00:00:00:00:00:02"},
|
|
||||||
[]string{"client1", "client2"},
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"empty hostname",
|
|
||||||
"<client1>00:00:00:00:00:01>0>4>><>00:00:00:00:00:02>0>24>>",
|
|
||||||
[]string{"00:00:00:00:00:01"},
|
|
||||||
[]string{"client1"},
|
|
||||||
[]string{"00:00:00:00:00:02"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"empty dhcp",
|
|
||||||
"<client1>00:00:00:00:00:01>0>4>><client 1>>>",
|
|
||||||
[]string{"00:00:00:00:00:01"},
|
|
||||||
[]string{"client1"},
|
|
||||||
[]string{""},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"invalid",
|
|
||||||
"qwerty",
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"empty",
|
|
||||||
"",
|
|
||||||
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range tests {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
m := &merlinDiscover{}
|
|
||||||
m.parseMerlinCustomClientList(tc.clientList)
|
|
||||||
for i, mac := range tc.macList {
|
|
||||||
val, ok := m.hostname.Load(mac)
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("missing hostname: %s", mac)
|
|
||||||
}
|
|
||||||
hostname := val.(string)
|
|
||||||
if hostname != tc.hostnameList[i] {
|
|
||||||
t.Errorf("hostname mismatch, want: %q, got: %q", tc.hostnameList[i], hostname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, mac := range tc.macNotPresentList {
|
|
||||||
if _, ok := m.hostname.Load(mac); ok {
|
|
||||||
t.Errorf("mac2name address %q should not be present", mac)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package clientinfo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/ubios"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ubiosDiscover provides client discovery functionality on Ubios routers.
|
|
||||||
type ubiosDiscover struct {
|
|
||||||
hostname sync.Map // mac => hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
// refresh reloads unifi devices from database.
|
|
||||||
func (u *ubiosDiscover) refresh() error {
|
|
||||||
if router.Name() != ubios.Name {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return u.refreshDevices()
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupHostnameByIP returns hostname for given IP.
|
|
||||||
func (u *ubiosDiscover) LookupHostnameByIP(ip string) string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupHostnameByMac returns unifi device custom hostname for the given MAC address.
|
|
||||||
func (u *ubiosDiscover) LookupHostnameByMac(mac string) string {
|
|
||||||
val, ok := u.hostname.Load(mac)
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return val.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// refreshDevices updates unifi devices name from local mongodb.
|
|
||||||
func (u *ubiosDiscover) refreshDevices() error {
|
|
||||||
cmd := exec.Command("/usr/bin/mongo", "localhost:27117/ace", "--quiet", "--eval", `
|
|
||||||
DBQuery.shellBatchSize = 256;
|
|
||||||
db.user.find({name: {$exists: true, $ne: ""}}, {_id:0, mac:1, name:1});`)
|
|
||||||
b, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("out: %s, err: %w", string(b), err)
|
|
||||||
}
|
|
||||||
return u.storeDevices(bytes.NewReader(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
// storeDevices saves unifi devices name for caching.
|
|
||||||
func (u *ubiosDiscover) storeDevices(r io.Reader) error {
|
|
||||||
decoder := json.NewDecoder(r)
|
|
||||||
device := struct {
|
|
||||||
MAC string
|
|
||||||
Name string
|
|
||||||
}{}
|
|
||||||
for {
|
|
||||||
err := decoder.Decode(&device)
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mac := strings.ToLower(device.MAC)
|
|
||||||
u.hostname.Store(mac, normalizeHostname(device.Name))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns human-readable format of ubiosDiscover.
|
|
||||||
func (u *ubiosDiscover) String() string {
|
|
||||||
return "ubios"
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package clientinfo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_ubiosDiscover_storeDevices(t *testing.T) {
|
|
||||||
ud := &ubiosDiscover{}
|
|
||||||
r := strings.NewReader(`{ "mac": "00:00:00:00:00:01", "name": "device 1" }
|
|
||||||
{ "mac": "00:00:00:00:00:02", "name": "device 2" }
|
|
||||||
`)
|
|
||||||
if err := ud.storeDevices(r); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
mac string
|
|
||||||
hostname string
|
|
||||||
}{
|
|
||||||
{"device 1", "00:00:00:00:00:01", "device 1"},
|
|
||||||
{"device 2", "00:00:00:00:00:02", "device 2"},
|
|
||||||
{"non-existed", "00:00:00:00:00:03", ""},
|
|
||||||
}
|
|
||||||
for _, tc := range tests {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
if got := ud.LookupHostnameByMac(tc.mac); got != tc.hostname {
|
|
||||||
t.Errorf("hostname mismatched, want: %q, got: %q", tc.hostname, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for invalid input.
|
|
||||||
r = strings.NewReader(`{ "mac": "00:00:00:00:00:01", "name": "device 1"`)
|
|
||||||
if err := ud.storeDevices(r); err == nil {
|
|
||||||
t.Fatal("expected error, got nil")
|
|
||||||
} else {
|
|
||||||
t.Log(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,8 +18,6 @@ import (
|
|||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/certs"
|
"github.com/Control-D-Inc/ctrld/internal/certs"
|
||||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/ddwrt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -271,7 +269,7 @@ func apiTransport(loggerCtx context.Context, cdDev bool) *http.Transport {
|
|||||||
// Fallback to direct IPv6
|
// Fallback to direct IPv6
|
||||||
return dial(ctx, "tcp6", addrsFromPort(apiIpsV6, port))
|
return dial(ctx, "tcp6", addrsFromPort(apiIpsV6, port))
|
||||||
}
|
}
|
||||||
if router.Name() == ddwrt.Name || runtime.GOOS == "android" {
|
if runtime.GOOS == "android" {
|
||||||
transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()}
|
transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()}
|
||||||
}
|
}
|
||||||
return transport
|
return transport
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
package ddwrt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/ntp"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
|
||||||
)
|
|
||||||
|
|
||||||
const Name = "ddwrt"
|
|
||||||
|
|
||||||
//lint:ignore ST1005 This error is for human.
|
|
||||||
var errDdwrtJffs2NotEnabled = errors.New(`could not install service without jffs, follow this guide to enable:
|
|
||||||
|
|
||||||
https://wiki.dd-wrt.com/wiki/index.php/Journalling_Flash_File_System
|
|
||||||
`)
|
|
||||||
|
|
||||||
var nvramKvMap = map[string]string{
|
|
||||||
"dns_dnsmasq": "1", // Make dnsmasq running but disable DNS ability, ctrld will replace it.
|
|
||||||
"dnsmasq_options": "", // Configuration of dnsmasq set by ctrld, filled by setupDDWrt.
|
|
||||||
"dns_crypt": "0", // Disable DNSCrypt.
|
|
||||||
"dnssec": "0", // Disable DNSSEC.
|
|
||||||
}
|
|
||||||
|
|
||||||
type Ddwrt struct {
|
|
||||||
cfg *ctrld.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a router.Router for configuring/setup/run ctrld on ddwrt routers.
|
|
||||||
func New(cfg *ctrld.Config) *Ddwrt {
|
|
||||||
return &Ddwrt{cfg: cfg}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Ddwrt) ConfigureService(config *service.Config) error {
|
|
||||||
if !ddwrtJff2Enabled() {
|
|
||||||
return errDdwrtJffs2NotEnabled
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Ddwrt) Install(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Ddwrt) Uninstall(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Ddwrt) PreRun() error {
|
|
||||||
_ = d.Cleanup()
|
|
||||||
return ntp.WaitNvram()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Ddwrt) Setup() error {
|
|
||||||
if d.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Already setup.
|
|
||||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, d.cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
nvramKvMap["dnsmasq_options"] = data
|
|
||||||
if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Ddwrt) Cleanup() error {
|
|
||||||
if d.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" {
|
|
||||||
return nil // was restored, nothing to do.
|
|
||||||
}
|
|
||||||
|
|
||||||
nvramKvMap["dnsmasq_options"] = ""
|
|
||||||
// Restore old configs.
|
|
||||||
if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func restartDNSMasq() error {
|
|
||||||
if out, err := exec.Command("restart_dns").CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("restart_dns: %s, %w", string(out), err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ddwrtJff2Enabled() bool {
|
|
||||||
out, _ := nvram.Run("get", "enable_jffs2")
|
|
||||||
return out == "1"
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package dnsmasq
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func InterfaceNameFromConfig(filename string) (string, error) {
|
|
||||||
buf, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return interfaceNameFromReader(bytes.NewReader(buf))
|
|
||||||
}
|
|
||||||
|
|
||||||
func interfaceNameFromReader(r io.Reader) (string, error) {
|
|
||||||
scanner := bufio.NewScanner(r)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
after, found := strings.CutPrefix(line, "interface=")
|
|
||||||
if found {
|
|
||||||
return after, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", errors.New("not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdditionalConfigFiles returns a list of Dnsmasq configuration files found in the "/tmp/etc" directory.
|
|
||||||
func AdditionalConfigFiles() []string {
|
|
||||||
if paths, err := filepath.Glob("/tmp/etc/dnsmasq-*.conf"); err == nil {
|
|
||||||
return paths
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdditionalLeaseFiles returns a list of lease file paths corresponding to the Dnsmasq configuration files.
|
|
||||||
func AdditionalLeaseFiles() []string {
|
|
||||||
cfgFiles := AdditionalConfigFiles()
|
|
||||||
if len(cfgFiles) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
leaseFiles := make([]string, 0, len(cfgFiles))
|
|
||||||
for _, cfgFile := range cfgFiles {
|
|
||||||
if leaseFile := leaseFileFromConfigFileName(cfgFile); leaseFile != "" {
|
|
||||||
leaseFiles = append(leaseFiles, leaseFile)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
leaseFiles = append(leaseFiles, defaultLeaseFileFromConfigPath(cfgFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return leaseFiles
|
|
||||||
}
|
|
||||||
|
|
||||||
// leaseFileFromConfigFileName retrieves the DHCP lease file path by reading and parsing the provided configuration file.
|
|
||||||
func leaseFileFromConfigFileName(cfgFile string) string {
|
|
||||||
if f, err := os.Open(cfgFile); err == nil {
|
|
||||||
return leaseFileFromReader(f)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// leaseFileFromReader parses the given io.Reader for the "dhcp-leasefile" configuration and returns its value as a string.
|
|
||||||
func leaseFileFromReader(r io.Reader) string {
|
|
||||||
scanner := bufio.NewScanner(r)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
before, after, found := strings.Cut(line, "=")
|
|
||||||
if !found {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if before == "dhcp-leasefile" {
|
|
||||||
return after
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaultLeaseFileFromConfigPath generates the default lease file path based on the provided configuration file path.
|
|
||||||
func defaultLeaseFileFromConfigPath(path string) string {
|
|
||||||
name := filepath.Base(path)
|
|
||||||
return filepath.Join("/var/lib/misc", strings.TrimSuffix(name, ".conf")+".leases")
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package dnsmasq
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_interfaceNameFromReader(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
in string
|
|
||||||
wantIface string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"good",
|
|
||||||
`interface=lo`,
|
|
||||||
"lo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"multiple",
|
|
||||||
`interface=lo
|
|
||||||
interface=eth0
|
|
||||||
`,
|
|
||||||
"lo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"no iface",
|
|
||||||
`cache-size=100`,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range tests {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
ifaceName, err := interfaceNameFromReader(strings.NewReader(tc.in))
|
|
||||||
if tc.wantIface != "" && err != nil {
|
|
||||||
t.Errorf("unexpected error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tc.wantIface != ifaceName {
|
|
||||||
t.Errorf("mismatched, want: %q, got: %q", tc.wantIface, ifaceName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_leaseFileFromReader(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
in io.Reader
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"default",
|
|
||||||
strings.NewReader(`
|
|
||||||
dhcp-script=/sbin/dhcpc_lease
|
|
||||||
dhcp-leasefile=/var/lib/misc/dnsmasq-1.leases
|
|
||||||
script-arp
|
|
||||||
`),
|
|
||||||
"/var/lib/misc/dnsmasq-1.leases",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"non-default",
|
|
||||||
strings.NewReader(`
|
|
||||||
dhcp-script=/sbin/dhcpc_lease
|
|
||||||
dhcp-leasefile=/tmp/var/lib/misc/dnsmasq-1.leases
|
|
||||||
script-arp
|
|
||||||
`),
|
|
||||||
"/tmp/var/lib/misc/dnsmasq-1.leases",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"missing",
|
|
||||||
strings.NewReader(`
|
|
||||||
dhcp-script=/sbin/dhcpc_lease
|
|
||||||
script-arp
|
|
||||||
`),
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
if got := leaseFileFromReader(tc.in); got != tc.expected {
|
|
||||||
t.Errorf("leaseFileFromReader() = %v, want %v", got, tc.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
package dnsmasq
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"html/template"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
)
|
|
||||||
|
|
||||||
const CtrldMarker = `# GENERATED BY ctrld - DO NOT MODIFY`
|
|
||||||
|
|
||||||
const ConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
|
||||||
no-resolv
|
|
||||||
{{- range .Upstreams}}
|
|
||||||
server={{ .IP }}#{{ .Port }}
|
|
||||||
{{- end}}
|
|
||||||
add-mac
|
|
||||||
add-subnet=32,128
|
|
||||||
{{- if .CacheDisabled}}
|
|
||||||
cache-size=0
|
|
||||||
{{- else}}
|
|
||||||
max-cache-ttl=0
|
|
||||||
{{- end}}
|
|
||||||
`
|
|
||||||
|
|
||||||
const (
|
|
||||||
MerlinConfPath = "/tmp/etc/dnsmasq.conf"
|
|
||||||
MerlinJffsConfDir = "/jffs/configs"
|
|
||||||
MerlinJffsConfPath = "/jffs/configs/dnsmasq.conf"
|
|
||||||
MerlinPostConfPath = "/jffs/scripts/dnsmasq.postconf"
|
|
||||||
)
|
|
||||||
|
|
||||||
const MerlinPostConfMarker = `# GENERATED BY ctrld - EOF`
|
|
||||||
const MerlinPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
|
||||||
|
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
config_file="$1"
|
|
||||||
. /usr/sbin/helper.sh
|
|
||||||
|
|
||||||
pid=$(cat /tmp/ctrld.pid 2>/dev/null)
|
|
||||||
if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then
|
|
||||||
pc_delete "servers-file" "$config_file" # no WAN DNS settings
|
|
||||||
pc_append "no-resolv" "$config_file" # do not read /etc/resolv.conf
|
|
||||||
# use ctrld as upstream
|
|
||||||
pc_delete "server=" "$config_file"
|
|
||||||
{{- range .Upstreams}}
|
|
||||||
pc_append "server={{ .IP }}#{{ .Port }}" "$config_file"
|
|
||||||
{{- end}}
|
|
||||||
pc_delete "add-mac" "$config_file"
|
|
||||||
pc_delete "add-subnet" "$config_file"
|
|
||||||
pc_append "add-mac" "$config_file" # add client mac
|
|
||||||
pc_append "add-subnet=32,128" "$config_file" # add client ip
|
|
||||||
pc_delete "dnssec" "$config_file" # disable DNSSEC
|
|
||||||
pc_delete "trust-anchor=" "$config_file" # disable DNSSEC
|
|
||||||
pc_delete "cache-size=" "$config_file"
|
|
||||||
pc_append "cache-size=0" "$config_file" # disable cache
|
|
||||||
|
|
||||||
# For John fork
|
|
||||||
pc_delete "resolv-file" "$config_file" # no WAN DNS settings
|
|
||||||
|
|
||||||
# Change /etc/resolv.conf, which may be changed by WAN DNS setup
|
|
||||||
pc_delete "nameserver" /etc/resolv.conf
|
|
||||||
pc_append "nameserver 127.0.0.1" /etc/resolv.conf
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
`
|
|
||||||
|
|
||||||
type Upstream struct {
|
|
||||||
IP string
|
|
||||||
Port int
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfTmpl generates dnsmasq configuration from ctrld config.
|
|
||||||
func ConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) {
|
|
||||||
return ConfTmplWithCacheDisabled(tmplText, cfg, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfTmplWithCacheDisabled is like ConfTmpl, but the caller can control whether
|
|
||||||
// dnsmasq cache is disabled using cacheDisabled parameter.
|
|
||||||
//
|
|
||||||
// Generally, the caller should use ConfTmpl, but on some routers which dnsmasq config may be changed
|
|
||||||
// after ctrld started (like EdgeOS/Ubios, Firewalla ...), dnsmasq cache should not be disabled because
|
|
||||||
// the cache-size=0 generated by ctrld will conflict with router's generated config.
|
|
||||||
func ConfTmplWithCacheDisabled(tmplText string, cfg *ctrld.Config, cacheDisabled bool) (string, error) {
|
|
||||||
listener := cfg.FirstListener()
|
|
||||||
if listener == nil {
|
|
||||||
return "", errors.New("missing listener")
|
|
||||||
}
|
|
||||||
ip := listener.IP
|
|
||||||
if ip == "0.0.0.0" || ip == "::" || ip == "" {
|
|
||||||
ip = "127.0.0.1"
|
|
||||||
}
|
|
||||||
upstreams := []Upstream{{IP: ip, Port: listener.Port}}
|
|
||||||
return confTmpl(tmplText, upstreams, cacheDisabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FirewallaConfTmpl generates dnsmasq config for Firewalla routers.
|
|
||||||
func FirewallaConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) {
|
|
||||||
// If ctrld listen on all interfaces, generating config for all of them.
|
|
||||||
if lc := cfg.FirstListener(); lc != nil && (lc.IP == "0.0.0.0" || lc.IP == "") {
|
|
||||||
return confTmpl(tmplText, firewallaUpstreams(lc.Port), false)
|
|
||||||
}
|
|
||||||
// Otherwise, generating config for the specific listener from ctrld's config.
|
|
||||||
return ConfTmplWithCacheDisabled(tmplText, cfg, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func confTmpl(tmplText string, upstreams []Upstream, cacheDisabled bool) (string, error) {
|
|
||||||
tmpl := template.Must(template.New("").Parse(tmplText))
|
|
||||||
var to = &struct {
|
|
||||||
Upstreams []Upstream
|
|
||||||
CacheDisabled bool
|
|
||||||
}{
|
|
||||||
Upstreams: upstreams,
|
|
||||||
CacheDisabled: cacheDisabled,
|
|
||||||
}
|
|
||||||
var sb strings.Builder
|
|
||||||
if err := tmpl.Execute(&sb, to); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return sb.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func firewallaUpstreams(port int) []Upstream {
|
|
||||||
ifaces := FirewallaSelfInterfaces()
|
|
||||||
upstreams := make([]Upstream, 0, len(ifaces))
|
|
||||||
for _, netIface := range ifaces {
|
|
||||||
addrs, _ := netIface.Addrs()
|
|
||||||
for _, addr := range addrs {
|
|
||||||
if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil {
|
|
||||||
upstreams = append(upstreams, Upstream{
|
|
||||||
IP: netIP.IP.To4().String(),
|
|
||||||
Port: port,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return upstreams
|
|
||||||
}
|
|
||||||
|
|
||||||
// firewallaDnsmasqConfFiles returns dnsmasq config files of all firewalla interfaces.
|
|
||||||
func firewallaDnsmasqConfFiles() ([]string, error) {
|
|
||||||
return filepath.Glob("/home/pi/firerouter/etc/dnsmasq.dns.*.conf")
|
|
||||||
}
|
|
||||||
|
|
||||||
// FirewallaSelfInterfaces returns list of interfaces that will be configured with default dnsmasq setup on Firewalla.
|
|
||||||
func FirewallaSelfInterfaces() []*net.Interface {
|
|
||||||
matches, err := firewallaDnsmasqConfFiles()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ifaces := make([]*net.Interface, 0, len(matches))
|
|
||||||
for _, match := range matches {
|
|
||||||
// Trim prefix and suffix to get the iface name only.
|
|
||||||
ifaceName := strings.TrimSuffix(strings.TrimPrefix(match, "/home/pi/firerouter/etc/dnsmasq.dns."), ".conf")
|
|
||||||
if netIface, _ := net.InterfaceByName(ifaceName); netIface != nil {
|
|
||||||
ifaces = append(ifaces, netIface)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ifaces
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ubios43ConfPath = "/run/dnsmasq.dhcp.conf.d"
|
|
||||||
ubios42ConfPath = "/run/dnsmasq.conf.d"
|
|
||||||
ubios43PidFile = "/run/dnsmasq-main.pid"
|
|
||||||
ubios42PidFile = "/run/dnsmasq.pid"
|
|
||||||
UbiosConfName = "zzzctrld.conf"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UbiosConfPath returns the appropriate configuration path based on the system's directory structure.
|
|
||||||
func UbiosConfPath() string {
|
|
||||||
if st, _ := os.Stat(ubios43ConfPath); st != nil && st.IsDir() {
|
|
||||||
return ubios43ConfPath
|
|
||||||
}
|
|
||||||
return ubios42ConfPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// UbiosPidFile returns the appropriate dnsmasq pid file based on the system's directory structure.
|
|
||||||
func UbiosPidFile() string {
|
|
||||||
if st, _ := os.Stat(ubios43PidFile); st != nil && !st.IsDir() {
|
|
||||||
return ubios43PidFile
|
|
||||||
}
|
|
||||||
return ubios42PidFile
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
package edgeos
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Name = "edgeos"
|
|
||||||
edgeOSDNSMasqConfigPath = "/etc/dnsmasq.d/dnsmasq-zzz-ctrld.conf"
|
|
||||||
usgDNSMasqConfigPath = "/etc/dnsmasq.conf"
|
|
||||||
usgDNSMasqBackupConfigPath = "/etc/dnsmasq.conf.bak"
|
|
||||||
toggleContentFilteringLink = "https://community.ui.com/questions/UDM-Pro-disable-enable-DNS-filtering/e2cc4060-e56a-4139-b200-62d7f773ff8f"
|
|
||||||
toggleDnsShieldLink = "https://community.ui.com/questions/UniFi-OS-3-2-7-DNS-Shield-Missing/d3a85905-4ce0-4fe4-8bf0-6cb04f21371d"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrContentFilteringEnabled = fmt.Errorf(`the "Content Filtering" feature" is enabled, which is conflicted with ctrld.\n
|
|
||||||
To disable it, folowing instruction here: %s`, toggleContentFilteringLink)
|
|
||||||
|
|
||||||
var ErrDnsShieldEnabled = fmt.Errorf(`the "DNS Shield" feature" is enabled, which is conflicted with ctrld.\n
|
|
||||||
To disable it, folowing screenshot here: %s`, toggleDnsShieldLink)
|
|
||||||
|
|
||||||
type EdgeOS struct {
|
|
||||||
cfg *ctrld.Config
|
|
||||||
isUSG bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a router.Router for configuring/setup/run ctrld on EdgeOS routers.
|
|
||||||
func New(cfg *ctrld.Config) *EdgeOS {
|
|
||||||
e := &EdgeOS{cfg: cfg}
|
|
||||||
e.isUSG = checkUSG()
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EdgeOS) ConfigureService(config *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EdgeOS) Install(_ *service.Config) error {
|
|
||||||
// If "Content Filtering" is enabled, UniFi OS will create firewall rules to intercept all DNS queries
|
|
||||||
// from outside, and route those queries to separated interfaces (e.g: dnsfilter-2@if79) created by UniFi OS.
|
|
||||||
// Thus, those queries will never reach ctrld listener. UniFi OS does not provide any mechanism to toggle this
|
|
||||||
// feature via command line, so there's nothing ctrld can do to disable this feature. For now, reporting an
|
|
||||||
// error and guiding users to disable the feature using UniFi OS web UI.
|
|
||||||
if ContentFilteringEnabled() {
|
|
||||||
return ErrContentFilteringEnabled
|
|
||||||
}
|
|
||||||
// If "DNS Shield" is enabled, UniFi OS will spawn dnscrypt-proxy process, and route all DNS queries to it. So
|
|
||||||
// reporting an error and guiding users to disable the feature using UniFi OS web UI.
|
|
||||||
if DnsShieldEnabled() {
|
|
||||||
return ErrDnsShieldEnabled
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EdgeOS) Uninstall(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EdgeOS) PreRun() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EdgeOS) Setup() error {
|
|
||||||
if e.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if e.isUSG {
|
|
||||||
return e.setupUSG()
|
|
||||||
}
|
|
||||||
return e.setupUDM()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EdgeOS) Cleanup() error {
|
|
||||||
if e.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if e.isUSG {
|
|
||||||
return e.cleanupUSG()
|
|
||||||
}
|
|
||||||
return e.cleanupUDM()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EdgeOS) setupUSG() error {
|
|
||||||
// On USG, dnsmasq is configured to forward queries to external provider by default.
|
|
||||||
// So instead of generating config in /etc/dnsmasq.d, we need to create a backup of
|
|
||||||
// the config, then modify it to forward queries to ctrld listener.
|
|
||||||
|
|
||||||
// Creating a backup.
|
|
||||||
buf, err := os.ReadFile(usgDNSMasqConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("setupUSG: reading current config: %w", err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(usgDNSMasqBackupConfigPath, buf, 0600); err != nil {
|
|
||||||
return fmt.Errorf("setupUSG: backup current config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removing all configured upstreams and cache config.
|
|
||||||
var sb strings.Builder
|
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(buf))
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if strings.HasPrefix(line, "server=") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(line, "all-servers") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sb.WriteString(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, e.cfg, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sb.WriteString("\n")
|
|
||||||
sb.WriteString(data)
|
|
||||||
if err := os.WriteFile(usgDNSMasqConfigPath, []byte(sb.String()), 0644); err != nil {
|
|
||||||
return fmt.Errorf("setupUSG: writing dnsmasq config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return fmt.Errorf("setupUSG: restartDNSMasq: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EdgeOS) setupUDM() error {
|
|
||||||
data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, e.cfg, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(edgeOSDNSMasqConfigPath, []byte(data), 0600); err != nil {
|
|
||||||
return fmt.Errorf("setupUDM: generating dnsmasq config: %w", err)
|
|
||||||
}
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return fmt.Errorf("setupUDM: restartDNSMasq: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EdgeOS) cleanupUSG() error {
|
|
||||||
if err := os.Rename(usgDNSMasqBackupConfigPath, usgDNSMasqConfigPath); err != nil {
|
|
||||||
return fmt.Errorf("cleanupUSG: os.Rename: %w", err)
|
|
||||||
}
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return fmt.Errorf("cleanupUSG: restartDNSMasq: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EdgeOS) cleanupUDM() error {
|
|
||||||
// Remove the custom dnsmasq config
|
|
||||||
if err := os.Remove(edgeOSDNSMasqConfigPath); err != nil {
|
|
||||||
return fmt.Errorf("cleanupUDM: os.Remove: %w", err)
|
|
||||||
}
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return fmt.Errorf("cleanupUDM: restartDNSMasq: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ContentFilteringEnabled() bool {
|
|
||||||
st, err := os.Stat("/run/dnsfilter/dnsfilter")
|
|
||||||
return err == nil && !st.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DnsShieldEnabled reports whether DNS Shield is enabled.
|
|
||||||
// See: https://community.ui.com/releases/UniFi-OS-Dream-Machines-3-2-7/251dfc1e-f4dd-4264-a080-3be9d8b9e02b
|
|
||||||
func DnsShieldEnabled() bool {
|
|
||||||
buf, err := os.ReadFile(filepath.Join(dnsmasq.UbiosConfPath(), "dns.conf"))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return bytes.Contains(buf, []byte("server=127.0.0.1#5053"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func LeaseFileDir() string {
|
|
||||||
if checkUSG() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "/run"
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkUSG() bool {
|
|
||||||
out, _ := os.ReadFile("/etc/version")
|
|
||||||
return bytes.HasPrefix(out, []byte("UniFiSecurityGateway."))
|
|
||||||
}
|
|
||||||
|
|
||||||
func restartDNSMasq() error {
|
|
||||||
if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("edgeosRestartDNSMasq: %s, %w", string(out), err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package firewalla
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Name = "firewalla"
|
|
||||||
|
|
||||||
firewallaDNSMasqConfigPath = "/home/pi/.firewalla/config/dnsmasq_local/ctrld"
|
|
||||||
firewallaConfigPostMainDir = "/home/pi/.firewalla/config/post_main.d"
|
|
||||||
firewallaCtrldInitScriptPath = "/home/pi/.firewalla/config/post_main.d/start_ctrld.sh"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Firewalla struct {
|
|
||||||
cfg *ctrld.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a router.Router for configuring/setup/run ctrld on Firewalla routers.
|
|
||||||
func New(cfg *ctrld.Config) *Firewalla {
|
|
||||||
return &Firewalla{cfg: cfg}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Firewalla) ConfigureService(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Firewalla) Install(_ *service.Config) error {
|
|
||||||
// Writing startup script.
|
|
||||||
if err := writeFirewallStartupScript(); err != nil {
|
|
||||||
return fmt.Errorf("writing startup script: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Firewalla) Uninstall(_ *service.Config) error {
|
|
||||||
// Removing startup script.
|
|
||||||
if err := os.Remove(firewallaCtrldInitScriptPath); err != nil {
|
|
||||||
return fmt.Errorf("removing startup script: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Firewalla) PreRun() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Firewalla) Setup() error {
|
|
||||||
if f.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
data, err := dnsmasq.FirewallaConfTmpl(dnsmasq.ConfigContentTmpl, f.cfg)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("generating dnsmasq config: %w", err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(firewallaDNSMasqConfigPath, []byte(data), 0600); err != nil {
|
|
||||||
return fmt.Errorf("writing ctrld config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return fmt.Errorf("restartDNSMasq: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Firewalla) Cleanup() error {
|
|
||||||
if f.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Removing current config.
|
|
||||||
if err := os.Remove(firewallaDNSMasqConfigPath); err != nil {
|
|
||||||
return fmt.Errorf("removing ctrld config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return fmt.Errorf("restartDNSMasq: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeFirewallStartupScript() error {
|
|
||||||
if err := os.MkdirAll(firewallaConfigPostMainDir, 0775); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
exe, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// This is called when "ctrld start ..." runs, so recording
|
|
||||||
// the same command line arguments to use in startup script.
|
|
||||||
argStr := strings.Join(os.Args[1:], " ")
|
|
||||||
script := fmt.Sprintf("#!/bin/bash\n\nsudo %q %s\n", exe, argStr)
|
|
||||||
return os.WriteFile(firewallaCtrldInitScriptPath, []byte(script), 0755)
|
|
||||||
}
|
|
||||||
|
|
||||||
func restartDNSMasq() error {
|
|
||||||
return exec.Command("systemctl", "restart", "firerouter_dns").Run()
|
|
||||||
}
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
package merlin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/ntp"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
|
||||||
)
|
|
||||||
|
|
||||||
const Name = "merlin"
|
|
||||||
|
|
||||||
// nvramKvMap is a map of NVRAM key-value pairs used to configure and manage Merlin-specific settings.
|
|
||||||
var nvramKvMap = map[string]string{
|
|
||||||
"dnspriv_enable": "0", // Ensure Merlin native DoT disabled.
|
|
||||||
}
|
|
||||||
|
|
||||||
// dnsmasqConfig represents configuration paths for dnsmasq operations in Merlin firmware.
|
|
||||||
type dnsmasqConfig struct {
|
|
||||||
confPath string
|
|
||||||
jffsConfPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merlin represents a configuration handler for setting up and managing ctrld on Merlin routers.
|
|
||||||
type Merlin struct {
|
|
||||||
cfg *ctrld.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a router.Router for configuring/setup/run ctrld on Merlin routers.
|
|
||||||
func New(cfg *ctrld.Config) *Merlin {
|
|
||||||
return &Merlin{cfg: cfg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigureService configures the service based on the provided configuration. It returns an error if the configuration fails.
|
|
||||||
func (m *Merlin) ConfigureService(config *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install sets up the necessary configurations and services required for the Merlin instance to function properly.
|
|
||||||
func (m *Merlin) Install(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uninstall removes the ctrld-related configurations and services from the Merlin router and reverts to the original state.
|
|
||||||
func (m *Merlin) Uninstall(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreRun prepares the Merlin instance for operation by waiting for essential services and directories to become available.
|
|
||||||
func (m *Merlin) PreRun() error {
|
|
||||||
// Wait NTP ready.
|
|
||||||
_ = m.Cleanup()
|
|
||||||
if err := ntp.WaitNvram(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Wait until directories mounted.
|
|
||||||
for _, dir := range []string{"/tmp", "/proc"} {
|
|
||||||
waitDirExists(dir)
|
|
||||||
}
|
|
||||||
// Wait dnsmasq started.
|
|
||||||
for {
|
|
||||||
out, _ := exec.Command("pidof", "dnsmasq").CombinedOutput()
|
|
||||||
if len(bytes.TrimSpace(out)) > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup initializes and configures the Merlin instance for use, including setting up dnsmasq and necessary nvram settings.
|
|
||||||
func (m *Merlin) Setup() error {
|
|
||||||
if m.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Already setup.
|
|
||||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.writeDnsmasqPostconf(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, cfg := range getDnsmasqConfigs() {
|
|
||||||
if err := m.setupDnsmasq(cfg); err != nil {
|
|
||||||
return fmt.Errorf("failed to setup dnsmasq: config: %s, error: %w", cfg.confPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup restores the original dnsmasq and nvram configurations and restarts dnsmasq if necessary.
|
|
||||||
func (m *Merlin) Cleanup() error {
|
|
||||||
if m.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" {
|
|
||||||
return nil // was restored, nothing to do.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore old configs.
|
|
||||||
if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Restore dnsmasq post conf file.
|
|
||||||
if err := os.WriteFile(dnsmasq.MerlinPostConfPath, merlinParsePostConf(buf), 0750); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, cfg := range getDnsmasqConfigs() {
|
|
||||||
if err := m.cleanupDnsmasqJffs(cfg); err != nil {
|
|
||||||
return fmt.Errorf("failed to cleanup jffs dnsmasq: config: %s, error: %w", cfg.confPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupDnsmasq sets up dnsmasq configuration by writing postconf, copying configuration, and running a postconf script.
|
|
||||||
func (m *Merlin) setupDnsmasq(cfg *dnsmasqConfig) error {
|
|
||||||
src, err := os.Open(cfg.confPath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil // nothing to do if conf file does not exist.
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open dnsmasq config: %w", err)
|
|
||||||
}
|
|
||||||
defer src.Close()
|
|
||||||
|
|
||||||
// Copy current dnsmasq config to cfg.jffsConfPath,
|
|
||||||
// Then we will run postconf script on this file.
|
|
||||||
//
|
|
||||||
// Normally, adding postconf script is enough. However, we see
|
|
||||||
// reports on some Merlin devices that postconf scripts does not
|
|
||||||
// work, but manipulating the config directly via /jffs/configs does.
|
|
||||||
dst, err := os.Create(cfg.jffsConfPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create %s: %w", cfg.jffsConfPath, err)
|
|
||||||
}
|
|
||||||
defer dst.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(dst, src); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy current dnsmasq config: %w", err)
|
|
||||||
}
|
|
||||||
if err := dst.Close(); err != nil {
|
|
||||||
return fmt.Errorf("failed to save %s: %w", cfg.jffsConfPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run postconf script on cfg.jffsConfPath directly.
|
|
||||||
cmd := exec.Command("/bin/sh", dnsmasq.MerlinPostConfPath, cfg.jffsConfPath)
|
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("failed to run post conf: %s: %w", string(out), err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanupDnsmasqJffs removes the JFFS configuration file specified in the given dnsmasqConfig, if it exists.
|
|
||||||
func (m *Merlin) cleanupDnsmasqJffs(cfg *dnsmasqConfig) error {
|
|
||||||
// Remove cfg.jffsConfPath file.
|
|
||||||
if err := os.Remove(cfg.jffsConfPath); err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeDnsmasqPostconf writes the requireddnsmasqConfigs post-configuration for dnsmasq to enable custom DNS settings with ctrld.
|
|
||||||
func (m *Merlin) writeDnsmasqPostconf() error {
|
|
||||||
buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath)
|
|
||||||
// Already setup.
|
|
||||||
if bytes.Contains(buf, []byte(dnsmasq.MerlinPostConfMarker)) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := dnsmasq.ConfTmpl(dnsmasq.MerlinPostConfTmpl, m.cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
data = strings.Join([]string{
|
|
||||||
data,
|
|
||||||
"\n",
|
|
||||||
dnsmasq.MerlinPostConfMarker,
|
|
||||||
"\n",
|
|
||||||
string(buf),
|
|
||||||
}, "\n")
|
|
||||||
// Write dnsmasq post conf file.
|
|
||||||
return os.WriteFile(dnsmasq.MerlinPostConfPath, []byte(data), 0750)
|
|
||||||
}
|
|
||||||
|
|
||||||
// restartDNSMasq restarts the dnsmasq service by executing the appropriate system command using "service".
|
|
||||||
// Returns an error if the command fails or if there is an issue processing the command output.
|
|
||||||
func restartDNSMasq() error {
|
|
||||||
if out, err := exec.Command("service", "restart_dnsmasq").CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("restart_dnsmasq: %s, %w", string(out), err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getDnsmasqConfigs retrieves a list of dnsmasqConfig containing configuration and JFFS paths for dnsmasq operations.
|
|
||||||
func getDnsmasqConfigs() []*dnsmasqConfig {
|
|
||||||
cfgs := []*dnsmasqConfig{
|
|
||||||
{dnsmasq.MerlinConfPath, dnsmasq.MerlinJffsConfPath},
|
|
||||||
}
|
|
||||||
for _, path := range dnsmasq.AdditionalConfigFiles() {
|
|
||||||
jffsConfPath := filepath.Join(dnsmasq.MerlinJffsConfDir, filepath.Base(path))
|
|
||||||
cfgs = append(cfgs, &dnsmasqConfig{path, jffsConfPath})
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfgs
|
|
||||||
}
|
|
||||||
|
|
||||||
// merlinParsePostConf parses the dnsmasq post configuration by removing content after the MerlinPostConfMarker, if present.
|
|
||||||
// If no marker is found, the original buffer is returned unmodified.
|
|
||||||
// Returns nil if the input buffer is empty.
|
|
||||||
func merlinParsePostConf(buf []byte) []byte {
|
|
||||||
if len(buf) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
parts := bytes.Split(buf, []byte(dnsmasq.MerlinPostConfMarker))
|
|
||||||
if len(parts) != 1 {
|
|
||||||
return bytes.TrimLeftFunc(parts[1], unicode.IsSpace)
|
|
||||||
}
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// waitDirExists waits until the specified directory exists, polling its existence every second.
|
|
||||||
func waitDirExists(dir string) {
|
|
||||||
for {
|
|
||||||
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package merlin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_merlinParsePostConf(t *testing.T) {
|
|
||||||
origContent := "# foo"
|
|
||||||
data := strings.Join([]string{
|
|
||||||
dnsmasq.MerlinPostConfTmpl,
|
|
||||||
"\n",
|
|
||||||
dnsmasq.MerlinPostConfMarker,
|
|
||||||
"\n",
|
|
||||||
}, "\n")
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
data string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{"empty", "", ""},
|
|
||||||
{"no ctrld", origContent, origContent},
|
|
||||||
{"ctrld with data", data + origContent, origContent},
|
|
||||||
{"ctrld without data", data, ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
//t.Parallel()
|
|
||||||
if got := merlinParsePostConf([]byte(tc.data)); !bytes.Equal(got, []byte(tc.expected)) {
|
|
||||||
t.Errorf("unexpected result, want: %q, got: %q", tc.expected, string(got))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package netgear
|
|
||||||
|
|
||||||
const openWrtScript = `#!/bin/sh /etc/rc.common
|
|
||||||
USE_PROCD=1
|
|
||||||
# After dnsmasq starts
|
|
||||||
START=61
|
|
||||||
# Before network stops
|
|
||||||
STOP=89
|
|
||||||
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
|
|
||||||
name="{{.Name}}"
|
|
||||||
pid_file="/var/run/${name}.pid"
|
|
||||||
|
|
||||||
start_service() {
|
|
||||||
echo "Starting ${name}"
|
|
||||||
procd_open_instance
|
|
||||||
procd_set_param command ${cmd}
|
|
||||||
procd_set_param respawn # respawn automatically if something died
|
|
||||||
procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop
|
|
||||||
procd_close_instance
|
|
||||||
echo "${name} has been started"
|
|
||||||
}
|
|
||||||
`
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
package netgear
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Name = "netgear_orbi_voxel"
|
|
||||||
netgearOrbiVoxelDNSMasqConfigPath = "/etc/dnsmasq.conf"
|
|
||||||
netgearOrbiVoxelHomedir = "/mnt/bitdefender"
|
|
||||||
netgearOrbiVoxelStartupScript = "/mnt/bitdefender/rc.user"
|
|
||||||
netgearOrbiVoxelStartupScriptBackup = "/mnt/bitdefender/rc.user.bak"
|
|
||||||
netgearOrbiVoxelStartupScriptMarker = "\n# GENERATED BY ctrld"
|
|
||||||
)
|
|
||||||
|
|
||||||
var nvramKvMap = map[string]string{
|
|
||||||
"dns_hijack": "0", // Disable dns hijacking
|
|
||||||
}
|
|
||||||
|
|
||||||
type NetgearOrbiVoxel struct {
|
|
||||||
cfg *ctrld.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a router.Router for configuring/setup/run ctrld on ddwrt routers.
|
|
||||||
func New(cfg *ctrld.Config) *NetgearOrbiVoxel {
|
|
||||||
return &NetgearOrbiVoxel{cfg: cfg}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NetgearOrbiVoxel) ConfigureService(svc *service.Config) error {
|
|
||||||
if err := d.checkInstalledDir(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
svc.Option["SysvScript"] = openWrtScript
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NetgearOrbiVoxel) Install(_ *service.Config) error {
|
|
||||||
// Ignoring error here at this moment is ok, since everything will be wiped out on reboot.
|
|
||||||
_ = exec.Command("/etc/init.d/ctrld", "enable").Run()
|
|
||||||
if err := d.checkInstalledDir(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := backupVoxelStartupScript(); err != nil {
|
|
||||||
return fmt.Errorf("backup startup script: %w", err)
|
|
||||||
}
|
|
||||||
if err := writeVoxelStartupScript(); err != nil {
|
|
||||||
return fmt.Errorf("writing startup script: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NetgearOrbiVoxel) Uninstall(_ *service.Config) error {
|
|
||||||
if err := os.Remove(netgearOrbiVoxelStartupScript); err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err := os.Rename(netgearOrbiVoxelStartupScriptBackup, netgearOrbiVoxelStartupScript)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NetgearOrbiVoxel) PreRun() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NetgearOrbiVoxel) Setup() error {
|
|
||||||
if d.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Already setup.
|
|
||||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, d.cfg, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
currentConfig, _ := os.ReadFile(netgearOrbiVoxelDNSMasqConfigPath)
|
|
||||||
configContent := append(currentConfig, data...)
|
|
||||||
if err := os.WriteFile(netgearOrbiVoxelDNSMasqConfigPath, configContent, 0600); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NetgearOrbiVoxel) Cleanup() error {
|
|
||||||
if d.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" {
|
|
||||||
return nil // was restored, nothing to do.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore old configs.
|
|
||||||
if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore dnsmasq config.
|
|
||||||
if err := restoreDnsmasqConf(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkInstalledDir checks that ctrld binary was installed in the correct directory.
|
|
||||||
func (d *NetgearOrbiVoxel) checkInstalledDir() error {
|
|
||||||
exePath, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("checkHomeDir: failed to get binary path %w", err)
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(filepath.Dir(exePath), netgearOrbiVoxelHomedir) {
|
|
||||||
return fmt.Errorf("checkHomeDir: could not install service outside %s", netgearOrbiVoxelHomedir)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// backupVoxelStartupScript creates a backup of original startup script if existed.
|
|
||||||
func backupVoxelStartupScript() error {
|
|
||||||
// Do nothing if the startup script was modified by ctrld.
|
|
||||||
script, _ := os.ReadFile(netgearOrbiVoxelStartupScript)
|
|
||||||
if bytes.Contains(script, []byte(netgearOrbiVoxelStartupScriptMarker)) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := os.Rename(netgearOrbiVoxelStartupScript, netgearOrbiVoxelStartupScriptBackup)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("backupVoxelStartupScript: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeVoxelStartupScript writes startup script to re-install ctrld upon reboot.
|
|
||||||
// See: https://github.com/SVoxel/ORBI-RBK50/pull/7
|
|
||||||
func writeVoxelStartupScript() error {
|
|
||||||
exe, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("configure service: failed to get binary path %w", err)
|
|
||||||
}
|
|
||||||
// This is called when "ctrld start ..." runs, so recording
|
|
||||||
// the same command line arguments to use in startup script.
|
|
||||||
argStr := strings.Join(os.Args[1:], " ")
|
|
||||||
script, _ := os.ReadFile(netgearOrbiVoxelStartupScriptBackup)
|
|
||||||
script = append(script, fmt.Sprintf("%s\n%q %s\n", netgearOrbiVoxelStartupScriptMarker, exe, argStr)...)
|
|
||||||
f, err := os.Create(netgearOrbiVoxelStartupScript)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create startup script: %w", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
if _, err := f.Write(script); err != nil {
|
|
||||||
return fmt.Errorf("failed to write startup script: %w", err)
|
|
||||||
}
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
return fmt.Errorf("failed to save startup script: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// restoreDnsmasqConf restores original dnsmasq configuration.
|
|
||||||
func restoreDnsmasqConf() error {
|
|
||||||
f, err := os.Open(netgearOrbiVoxelDNSMasqConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
var bs []byte
|
|
||||||
buf := bytes.NewBuffer(bs)
|
|
||||||
|
|
||||||
removed := false
|
|
||||||
scanner := bufio.NewScanner(f)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if line == dnsmasq.CtrldMarker {
|
|
||||||
removed = true
|
|
||||||
}
|
|
||||||
if !removed {
|
|
||||||
_, err := buf.WriteString(line + "\n")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return os.WriteFile(netgearOrbiVoxelDNSMasqConfigPath, buf.Bytes(), 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func restartDNSMasq() error {
|
|
||||||
if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("restartDNSMasq: %s, %w", string(out), err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package ntp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"tailscale.com/logtail/backoff"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WaitNvram waits NTP synced by checking "ntp_ready" value using nvram.
|
|
||||||
func WaitNvram() error {
|
|
||||||
// Wait until `ntp_ready=1` set.
|
|
||||||
b := backoff.NewBackoff("ntp.Wait", func(format string, args ...any) {}, 10*time.Second)
|
|
||||||
for {
|
|
||||||
// ddwrt use "ntp_done": https://github.com/mirror/dd-wrt/blob/a08c693527ab3204bf7bebd408a7c9a83b6ede47/src/router/rc/ntp.c#L100
|
|
||||||
for _, key := range []string{"ntp_ready", "ntp_done"} {
|
|
||||||
out, err := nvram.Run("get", key)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("PreStart: nvram: %w", err)
|
|
||||||
}
|
|
||||||
if out == "1" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.BackOff(context.Background(), errors.New("ntp not ready"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitUpstart waits NTP synced by checking upstart task "ntpsync" is in "stop/waiting" state.
|
|
||||||
func WaitUpstart() error {
|
|
||||||
// Wait until `initctl status ntpsync` returns stop state.
|
|
||||||
b := backoff.NewBackoff("ntp.WaitUpstart", func(format string, args ...any) {}, 10*time.Second)
|
|
||||||
for {
|
|
||||||
out, err := exec.Command("initctl", "status", "ntpsync").CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("exec.Command: %w", err)
|
|
||||||
}
|
|
||||||
if bytes.Contains(out, []byte("stop/waiting")) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
b.BackOff(context.Background(), errors.New("ntp not ready"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package nvram
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
CtrldKeyPrefix = "ctrld_"
|
|
||||||
CtrldSetupKey = "ctrld_setup"
|
|
||||||
CtrldInstallKey = "ctrld_install"
|
|
||||||
RCStartupKey = "rc_startup"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Run runs the given nvram command.
|
|
||||||
func Run(args ...string) (string, error) {
|
|
||||||
cmd := exec.Command("nvram", args...)
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return "", fmt.Errorf("%s:%w", stderr.String(), err)
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(stdout.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
NOTE:
|
|
||||||
- For Openwrt, DNSSEC is not included in default dnsmasq (require dnsmasq-full).
|
|
||||||
- For Merlin, DNSSEC is configured during postconf script (see merlinDNSMasqPostConfTmpl).
|
|
||||||
- For Ubios UDM Pro/Dream Machine, DNSSEC is not included in their dnsmasq package:
|
|
||||||
+https://community.ui.com/questions/Implement-DNSSEC-into-UniFi/951c72b0-4d88-4c86-9174-45417bd2f9ca
|
|
||||||
+https://community.ui.com/questions/Enable-DNSSEC-for-Unifi-Dream-Machine-FW-updates/e68e367c-d09b-4459-9444-18908f7c1ea1
|
|
||||||
*/
|
|
||||||
|
|
||||||
// SetKV writes the given key/value from map to nvram.
|
|
||||||
// The given setupKey is set to 1 to indicates key/value set.
|
|
||||||
func SetKV(m map[string]string, setupKey string) error {
|
|
||||||
// Backup current value, store ctrld's configs.
|
|
||||||
for key, value := range m {
|
|
||||||
old, err := Run("get", key)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", old, err)
|
|
||||||
}
|
|
||||||
if out, err := Run("set", CtrldKeyPrefix+key+"="+old); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", out, err)
|
|
||||||
}
|
|
||||||
if out, err := Run("set", key+"="+value); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", out, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if out, err := Run("set", setupKey+"=1"); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", out, err)
|
|
||||||
}
|
|
||||||
// Commit.
|
|
||||||
if out, err := Run("commit"); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", out, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore restores the old value of given key from map m.
|
|
||||||
// The given setupKey is set to 0 to indicates key/value restored.
|
|
||||||
func Restore(m map[string]string, setupKey string) error {
|
|
||||||
// Restore old configs.
|
|
||||||
for key := range m {
|
|
||||||
ctrldKey := CtrldKeyPrefix + key
|
|
||||||
old, err := Run("get", ctrldKey)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", old, err)
|
|
||||||
}
|
|
||||||
_, _ = Run("unset", ctrldKey)
|
|
||||||
if out, err := Run("set", key+"="+old); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", out, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if out, err := Run("unset", setupKey); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", out, err)
|
|
||||||
}
|
|
||||||
// Commit.
|
|
||||||
if out, err := Run("commit"); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", out, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
package openwrt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Name = "openwrt"
|
|
||||||
openwrtDNSMasqConfigName = "ctrld.conf"
|
|
||||||
openwrtDNSMasqDefaultConfigDir = "/tmp/dnsmasq.d"
|
|
||||||
)
|
|
||||||
|
|
||||||
var openwrtDnsmasqDefaultConfigPath = filepath.Join(openwrtDNSMasqDefaultConfigDir, openwrtDNSMasqConfigName)
|
|
||||||
|
|
||||||
type Openwrt struct {
|
|
||||||
cfg *ctrld.Config
|
|
||||||
dnsmasqCacheSize string
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a router.Router for configuring/setup/run ctrld on Openwrt routers.
|
|
||||||
func New(cfg *ctrld.Config) *Openwrt {
|
|
||||||
return &Openwrt{cfg: cfg}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Openwrt) ConfigureService(svc *service.Config) error {
|
|
||||||
svc.Option["SysvScript"] = openWrtScript
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Openwrt) Install(config *service.Config) error {
|
|
||||||
return exec.Command("/etc/init.d/ctrld", "enable").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Openwrt) Uninstall(config *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Openwrt) PreRun() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Openwrt) Setup() error {
|
|
||||||
if o.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save current dnsmasq config cache size if present.
|
|
||||||
if cs, err := uci("get", "dhcp.@dnsmasq[0].cachesize"); err == nil {
|
|
||||||
o.dnsmasqCacheSize = cs
|
|
||||||
if _, err := uci("delete", "dhcp.@dnsmasq[0].cachesize"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Commit.
|
|
||||||
if _, err := uci("commit", "dhcp"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, o.cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(dnsmasqConfPathFromUbus(), []byte(data), 0600); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Openwrt) Cleanup() error {
|
|
||||||
if o.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Remove the custom dnsmasq config
|
|
||||||
if err := os.Remove(dnsmasqConfPathFromUbus()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore original value if present.
|
|
||||||
if o.dnsmasqCacheSize != "" {
|
|
||||||
if _, err := uci("set", fmt.Sprintf("dhcp.@dnsmasq[0].cachesize=%s", o.dnsmasqCacheSize)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Commit.
|
|
||||||
if _, err := uci("commit", "dhcp"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func restartDNSMasq() error {
|
|
||||||
if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", string(out), err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var errUCIEntryNotFound = errors.New("uci: Entry not found")
|
|
||||||
|
|
||||||
func uci(args ...string) (string, error) {
|
|
||||||
cmd := exec.Command("uci", args...)
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
if strings.HasPrefix(stderr.String(), errUCIEntryNotFound.Error()) {
|
|
||||||
return "", errUCIEntryNotFound
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("%s:%w", stderr.String(), err)
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(stdout.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// openwrtServiceList represents openwrt services config.
|
|
||||||
type openwrtServiceList struct {
|
|
||||||
Dnsmasq dnsmasqConf `json:"dnsmasq"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// dnsmasqConf represents dnsmasq config.
|
|
||||||
type dnsmasqConf struct {
|
|
||||||
Instances map[string]confInstances `json:"instances"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// confInstances represents an instance config of a service.
|
|
||||||
type confInstances struct {
|
|
||||||
Mount map[string]string `json:"mount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// dnsmasqConfPath returns the dnsmasq config path.
|
|
||||||
//
|
|
||||||
// Since version 24.10, openwrt makes some changes to dnsmasq to support
|
|
||||||
// multiple instances of dnsmasq. This change causes breaking changes to
|
|
||||||
// software which depends on the default dnsmasq path.
|
|
||||||
//
|
|
||||||
// There are some discussion/PRs in openwrt repo to address this:
|
|
||||||
//
|
|
||||||
// - https://github.com/openwrt/openwrt/pull/16806
|
|
||||||
// - https://github.com/openwrt/openwrt/pull/16890
|
|
||||||
//
|
|
||||||
// In the meantime, workaround this problem by querying the actual config path
|
|
||||||
// by querying ubus service list.
|
|
||||||
func dnsmasqConfPath(r io.Reader) string {
|
|
||||||
var svc openwrtServiceList
|
|
||||||
if err := json.NewDecoder(r).Decode(&svc); err != nil {
|
|
||||||
return openwrtDnsmasqDefaultConfigPath
|
|
||||||
}
|
|
||||||
for _, inst := range svc.Dnsmasq.Instances {
|
|
||||||
for mount := range inst.Mount {
|
|
||||||
dirName := filepath.Base(mount)
|
|
||||||
parts := strings.Split(dirName, ".")
|
|
||||||
if len(parts) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if parts[0] == "dnsmasq" && parts[len(parts)-1] == "d" {
|
|
||||||
return filepath.Join(mount, openwrtDNSMasqConfigName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return openwrtDnsmasqDefaultConfigPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// dnsmasqConfPathFromUbus get dnsmasq config path from ubus service list.
|
|
||||||
func dnsmasqConfPathFromUbus() string {
|
|
||||||
output, err := exec.Command("ubus", "call", "service", "list").Output()
|
|
||||||
if err != nil {
|
|
||||||
return openwrtDnsmasqDefaultConfigPath
|
|
||||||
}
|
|
||||||
return dnsmasqConfPath(bytes.NewReader(output))
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package openwrt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sample output from https://github.com/openwrt/openwrt/pull/16806#issuecomment-2448255734
|
|
||||||
const ubusDnsmasqBefore2410 = `{
|
|
||||||
"dnsmasq": {
|
|
||||||
"instances": {
|
|
||||||
"guest_dns": {
|
|
||||||
"mount": {
|
|
||||||
"/tmp/dnsmasq.d": "0",
|
|
||||||
"/var/run/dnsmasq/": "1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
const ubusDnsmasq2410 = `{
|
|
||||||
"dnsmasq": {
|
|
||||||
"instances": {
|
|
||||||
"guest_dns": {
|
|
||||||
"mount": {
|
|
||||||
"/tmp/dnsmasq.guest_dns.d": "0",
|
|
||||||
"/var/run/dnsmasq/": "1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
func Test_dnsmasqConfPath(t *testing.T) {
|
|
||||||
var dnsmasq2410expected = filepath.Join("/tmp/dnsmasq.guest_dns.d", openwrtDNSMasqConfigName)
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
in io.Reader
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{"empty", strings.NewReader(""), openwrtDnsmasqDefaultConfigPath},
|
|
||||||
{"invalid", strings.NewReader("}}"), openwrtDnsmasqDefaultConfigPath},
|
|
||||||
{"before 24.10", strings.NewReader(ubusDnsmasqBefore2410), openwrtDnsmasqDefaultConfigPath},
|
|
||||||
{"24.10", strings.NewReader(ubusDnsmasq2410), dnsmasq2410expected},
|
|
||||||
}
|
|
||||||
for _, tc := range tests {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
if got := dnsmasqConfPath(tc.in); got != tc.expected {
|
|
||||||
t.Errorf("dnsmasqConfPath() = %v, want %v", got, tc.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package openwrt
|
|
||||||
|
|
||||||
const openWrtScript = `#!/bin/sh /etc/rc.common
|
|
||||||
USE_PROCD=1
|
|
||||||
# After network starts
|
|
||||||
START=21
|
|
||||||
# Before network stops
|
|
||||||
STOP=89
|
|
||||||
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
|
|
||||||
name="{{.Name}}"
|
|
||||||
pid_file="/var/run/${name}.pid"
|
|
||||||
|
|
||||||
start_service() {
|
|
||||||
echo "Starting ${name}"
|
|
||||||
procd_open_instance
|
|
||||||
procd_set_param command ${cmd}
|
|
||||||
procd_set_param respawn # respawn automatically if something died
|
|
||||||
procd_set_param stdout 1 # forward stdout of the command to logd
|
|
||||||
procd_set_param stderr 1 # same for stderr
|
|
||||||
procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop
|
|
||||||
procd_set_param term_timeout 10
|
|
||||||
procd_close_instance
|
|
||||||
echo "${name} has been started"
|
|
||||||
}
|
|
||||||
`
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config represents /conf/config.xml file found on pfsense/opnsense.
|
|
||||||
type Config struct {
|
|
||||||
PfsenseUnbound *string `xml:"unbound>enable,omitempty"`
|
|
||||||
OPNsenseUnbound *string `xml:"OPNsense>unboundplus>general>enabled,omitempty"`
|
|
||||||
Dnsmasq *string `xml:"dnsmasq>enable,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DnsmasqEnabled reports whether dnsmasq is enabled.
|
|
||||||
func (c *Config) DnsmasqEnabled() bool {
|
|
||||||
if isPfsense() { // pfsense only set the attribute if dnsmasq is enabled.
|
|
||||||
return c.Dnsmasq != nil
|
|
||||||
}
|
|
||||||
return c.Dnsmasq != nil && *c.Dnsmasq == "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnboundEnabled reports whether unbound is enabled.
|
|
||||||
func (c *Config) UnboundEnabled() bool {
|
|
||||||
if isPfsense() { // pfsense only set the attribute if unbound is enabled.
|
|
||||||
return c.PfsenseUnbound != nil
|
|
||||||
}
|
|
||||||
return c.OPNsenseUnbound != nil && *c.OPNsenseUnbound == "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
// currentConfig does unmarshalling /conf/config.xml file,
|
|
||||||
// return the corresponding *Config represent it.
|
|
||||||
func currentConfig() (*Config, error) {
|
|
||||||
buf, _ := os.ReadFile("/conf/config.xml")
|
|
||||||
c := Config{}
|
|
||||||
if err := xml.Unmarshal(buf, &c); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &c, nil
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
osName = "freebsd"
|
|
||||||
rcPath = "/usr/local/etc/rc.d"
|
|
||||||
rcConfPath = "/etc/rc.conf.d/"
|
|
||||||
unboundRcPath = rcPath + "/unbound"
|
|
||||||
dnsmasqRcPath = rcPath + "/dnsmasq"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newOsRouter(cfg *ctrld.Config, cdMode bool) Router {
|
|
||||||
return &osRouter{cfg: cfg, cdMode: cdMode}
|
|
||||||
}
|
|
||||||
|
|
||||||
type osRouter struct {
|
|
||||||
cfg *ctrld.Config
|
|
||||||
svcName string
|
|
||||||
// cdMode indicates whether the router will configure ctrld in cd mode (aka --cd=<uid>).
|
|
||||||
// When ctrld is running on freebsd-like routers, and there's process running on port 53
|
|
||||||
// in cd mode, ctrld will attempt to kill the process and become direct listener.
|
|
||||||
// See details implemenation in osRouter.PreRun method.
|
|
||||||
cdMode bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (or *osRouter) ConfigureService(svc *service.Config) error {
|
|
||||||
svc.Option["SysvScript"] = bsdInitScript
|
|
||||||
or.svcName = svc.Name
|
|
||||||
rcFile := filepath.Join(rcConfPath, or.svcName)
|
|
||||||
var to = &struct {
|
|
||||||
Name string
|
|
||||||
}{
|
|
||||||
or.svcName,
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create(rcFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("os.Create: %w", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
if err := template.Must(template.New("").Parse(rcConfTmpl)).Execute(f, to); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (or *osRouter) Install(_ *service.Config) error {
|
|
||||||
if isPfsense() {
|
|
||||||
// pfsense need ".sh" extension for script to be run at boot.
|
|
||||||
// See: https://docs.netgate.com/pfsense/en/latest/development/boot-commands.html#shell-script-option
|
|
||||||
oldname := filepath.Join(rcPath, or.svcName)
|
|
||||||
newname := filepath.Join(rcPath, or.svcName+".sh")
|
|
||||||
_ = os.Remove(newname)
|
|
||||||
if err := os.Symlink(oldname, newname); err != nil {
|
|
||||||
return fmt.Errorf("os.Symlink: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (or *osRouter) Uninstall(_ *service.Config) error {
|
|
||||||
rcFiles := []string{filepath.Join(rcConfPath, or.svcName)}
|
|
||||||
if isPfsense() {
|
|
||||||
rcFiles = append(rcFiles, filepath.Join(rcPath, or.svcName+".sh"))
|
|
||||||
}
|
|
||||||
for _, filename := range rcFiles {
|
|
||||||
if err := os.Remove(filename); err != nil {
|
|
||||||
return fmt.Errorf("os.Remove: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (or *osRouter) PreRun() error {
|
|
||||||
if or.cdMode {
|
|
||||||
addr := "0.0.0.0:53"
|
|
||||||
udpLn, udpErr := net.ListenPacket("udp", addr)
|
|
||||||
if udpLn != nil {
|
|
||||||
udpLn.Close()
|
|
||||||
}
|
|
||||||
tcpLn, tcpErr := net.Listen("tcp", addr)
|
|
||||||
if tcpLn != nil {
|
|
||||||
tcpLn.Close()
|
|
||||||
}
|
|
||||||
// If we could not listen on :53 for any reason, try killing unbound/dnsmasq, become direct listener
|
|
||||||
if udpErr != nil || tcpErr != nil {
|
|
||||||
_ = exec.Command("killall", "unbound").Run()
|
|
||||||
_ = exec.Command("killall", "dnsmasq").Run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (or *osRouter) Setup() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (or *osRouter) Cleanup() error {
|
|
||||||
if or.cdMode {
|
|
||||||
c, err := currentConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if c.UnboundEnabled() {
|
|
||||||
_ = exec.Command(unboundRcPath, "onerestart").Run()
|
|
||||||
}
|
|
||||||
if c.DnsmasqEnabled() {
|
|
||||||
_ = exec.Command(dnsmasqRcPath, "onerestart").Run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPfsense() bool {
|
|
||||||
b, err := os.ReadFile("/etc/platform")
|
|
||||||
return err == nil && bytes.HasPrefix(b, []byte("pfSense"))
|
|
||||||
}
|
|
||||||
|
|
||||||
const bsdInitScript = `#!/bin/sh
|
|
||||||
|
|
||||||
# PROVIDE: {{.Name}}
|
|
||||||
# REQUIRE: SERVERS
|
|
||||||
# REQUIRE: unbound dnsmasq securelevel
|
|
||||||
# KEYWORD: shutdown
|
|
||||||
|
|
||||||
. /etc/rc.subr
|
|
||||||
|
|
||||||
name="{{.Name}}"
|
|
||||||
rcvar="${name}_enable"
|
|
||||||
{{.Name}}_env="IS_DAEMON=1"
|
|
||||||
pidfile="/var/run/${name}.pid"
|
|
||||||
child_pidfile="/var/run/${name}_child.pid"
|
|
||||||
command="/usr/sbin/daemon"
|
|
||||||
daemon_args="-r -P ${pidfile} -p ${child_pidfile} -t \"${name}: daemon\"{{if .WorkingDirectory}} -c {{.WorkingDirectory}}{{end}}"
|
|
||||||
command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
|
||||||
|
|
||||||
load_rc_config "${name}"
|
|
||||||
run_rc_command "$1"
|
|
||||||
`
|
|
||||||
|
|
||||||
var rcConfTmpl = `# {{.Name}}
|
|
||||||
{{.Name}}_enable="YES"
|
|
||||||
`
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
//go:build !freebsd
|
|
||||||
|
|
||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
)
|
|
||||||
|
|
||||||
const osName = ""
|
|
||||||
|
|
||||||
func newOsRouter(cfg *ctrld.Config, cdMode bool) Router {
|
|
||||||
return &osRouter{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type osRouter struct{}
|
|
||||||
|
|
||||||
func (d *osRouter) ConfigureService(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *osRouter) Install(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *osRouter) Uninstall(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *osRouter) PreRun() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *osRouter) Setup() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *osRouter) Cleanup() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/x509"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/certs"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/ddwrt"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/edgeos"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/firewalla"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/merlin"
|
|
||||||
netgear "github.com/Control-D-Inc/ctrld/internal/router/netgear_orbi_voxel"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/openwrt"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/synology"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/tomato"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/ubios"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service is the interface to manage ctrld service on router.
|
|
||||||
type Service interface {
|
|
||||||
// ConfigureService performs works for installing ctrla as a service on router.
|
|
||||||
ConfigureService(*service.Config) error
|
|
||||||
// Install performs necessary works after service.Install done.
|
|
||||||
Install(*service.Config) error
|
|
||||||
// Uninstall performs necessary works after service.Uninstallation done.
|
|
||||||
Uninstall(*service.Config) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Router is the interface for managing ctrld running on router.
|
|
||||||
type Router interface {
|
|
||||||
Service
|
|
||||||
|
|
||||||
// PreRun performs works need to be done before ctrld being run on router.
|
|
||||||
// Implementation should only return if the pre-condition was met (e.g: ntp synced).
|
|
||||||
PreRun() error
|
|
||||||
// Setup configures ctrld to be run on the router.
|
|
||||||
Setup() error
|
|
||||||
// Cleanup cleans up works setup on router by ctrld.
|
|
||||||
Cleanup() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns new Router interface.
|
|
||||||
func New(cfg *ctrld.Config, cdMode bool) Router {
|
|
||||||
switch Name() {
|
|
||||||
case ddwrt.Name:
|
|
||||||
return ddwrt.New(cfg)
|
|
||||||
case merlin.Name:
|
|
||||||
return merlin.New(cfg)
|
|
||||||
case openwrt.Name:
|
|
||||||
return openwrt.New(cfg)
|
|
||||||
case edgeos.Name:
|
|
||||||
return edgeos.New(cfg)
|
|
||||||
case ubios.Name:
|
|
||||||
return ubios.New(cfg)
|
|
||||||
case synology.Name:
|
|
||||||
return synology.New(cfg)
|
|
||||||
case tomato.Name:
|
|
||||||
return tomato.New(cfg)
|
|
||||||
case firewalla.Name:
|
|
||||||
return firewalla.New(cfg)
|
|
||||||
case netgear.Name:
|
|
||||||
return netgear.New(cfg)
|
|
||||||
}
|
|
||||||
return newOsRouter(cfg, cdMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsNetGearOrbi reports whether the router is a Netgear Orbi router.
|
|
||||||
func IsNetGearOrbi() bool {
|
|
||||||
return Name() == netgear.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsGLiNet reports whether the router is an GL.iNet router.
|
|
||||||
func IsGLiNet() bool {
|
|
||||||
if Name() != openwrt.Name {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
buf, _ := os.ReadFile("/proc/version")
|
|
||||||
// The output of /proc/version contains "(glinet@glinet)".
|
|
||||||
return bytes.Contains(buf, []byte(" (glinet"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsOldOpenwrt reports whether the router is an "old" version of Openwrt,
|
|
||||||
// aka versions which don't have "service" command.
|
|
||||||
func IsOldOpenwrt() bool {
|
|
||||||
if Name() != openwrt.Name {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
cmd, _ := exec.LookPath("service")
|
|
||||||
return cmd == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitProcessExited reports whether the "ctrld stop" command have to wait until ctrld process exited.
|
|
||||||
func WaitProcessExited() bool {
|
|
||||||
return Name() == openwrt.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
var routerPlatform atomic.Pointer[router]
|
|
||||||
|
|
||||||
type router struct {
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns name of the router platform.
|
|
||||||
func Name() string {
|
|
||||||
if r := routerPlatform.Load(); r != nil {
|
|
||||||
return r.name
|
|
||||||
}
|
|
||||||
r := &router{}
|
|
||||||
r.name = distroName()
|
|
||||||
routerPlatform.Store(r)
|
|
||||||
return r.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultInterfaceName returns the default interface name of the current router.
|
|
||||||
func DefaultInterfaceName() string {
|
|
||||||
switch Name() {
|
|
||||||
case ubios.Name:
|
|
||||||
return "lo"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// LocalResolverIP returns the IP that could be used as nameserver in /etc/resolv.conf file.
|
|
||||||
func LocalResolverIP() string {
|
|
||||||
var iface string
|
|
||||||
switch Name() {
|
|
||||||
case edgeos.Name:
|
|
||||||
// On EdgeOS, dnsmasq is run with "--local-service", so we need to get
|
|
||||||
// the proper interface from dnsmasq config.
|
|
||||||
if name, _ := dnsmasq.InterfaceNameFromConfig("/etc/dnsmasq.conf"); name != "" {
|
|
||||||
iface = name
|
|
||||||
}
|
|
||||||
case firewalla.Name:
|
|
||||||
// On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces.
|
|
||||||
// Thus, we use "br0" as the nameserver in /etc/resolv.conf file.
|
|
||||||
iface = "br0"
|
|
||||||
}
|
|
||||||
if netIface, _ := net.InterfaceByName(iface); netIface != nil {
|
|
||||||
addrs, _ := netIface.Addrs()
|
|
||||||
for _, addr := range addrs {
|
|
||||||
if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil {
|
|
||||||
return netIP.IP.To4().String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// HomeDir returns the home directory of ctrld on current router.
|
|
||||||
func HomeDir() (string, error) {
|
|
||||||
switch Name() {
|
|
||||||
case ddwrt.Name, firewalla.Name, merlin.Name, netgear.Name, tomato.Name:
|
|
||||||
exe, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return filepath.Dir(exe), nil
|
|
||||||
case edgeos.Name:
|
|
||||||
exe, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// Using binary directory as home dir if it is located in /config.
|
|
||||||
// Otherwise, fallback to old behavior for compatibility.
|
|
||||||
if strings.HasPrefix(exe, "/config/") {
|
|
||||||
return filepath.Dir(exe), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CertPool returns the system certificate pool of the current router.
|
|
||||||
func CertPool() *x509.CertPool {
|
|
||||||
if Name() == ddwrt.Name {
|
|
||||||
return certs.CACertPool()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanListenLocalhost reports whether the ctrld can listen on localhost with current host.
|
|
||||||
func CanListenLocalhost() bool {
|
|
||||||
switch {
|
|
||||||
case Name() == firewalla.Name:
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SelfInterfaces return list of *net.Interface that will be source of requests from router itself.
|
|
||||||
func SelfInterfaces() []*net.Interface {
|
|
||||||
switch Name() {
|
|
||||||
case firewalla.Name:
|
|
||||||
return dnsmasq.FirewallaSelfInterfaces()
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LeaseFilesDir is the directory which contains lease files.
|
|
||||||
func LeaseFilesDir() string {
|
|
||||||
if Name() == edgeos.Name {
|
|
||||||
edgeos.LeaseFileDir()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceDependencies returns list of dependencies that ctrld services needs on this router.
|
|
||||||
// See https://pkg.go.dev/github.com/kardianos/service#Config for list format.
|
|
||||||
func ServiceDependencies() []string {
|
|
||||||
if Name() == ubios.Name {
|
|
||||||
// On Ubios, ctrld needs to start after unifi-mongodb,
|
|
||||||
// so it can query custom client info mapping.
|
|
||||||
return []string{
|
|
||||||
"Wants=unifi-mongodb.service",
|
|
||||||
"After=unifi-mongodb.service",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func distroName() string {
|
|
||||||
switch {
|
|
||||||
case bytes.HasPrefix(unameO(), []byte("DD-WRT")):
|
|
||||||
return ddwrt.Name
|
|
||||||
case bytes.HasPrefix(unameO(), []byte("ASUSWRT-Merlin")):
|
|
||||||
return merlin.Name
|
|
||||||
case haveFile("/etc/openwrt_version"):
|
|
||||||
if haveFile("/bin/config") { // TODO: is there any more reliable way?
|
|
||||||
return netgear.Name
|
|
||||||
}
|
|
||||||
return openwrt.Name
|
|
||||||
case isUbios():
|
|
||||||
return ubios.Name
|
|
||||||
case bytes.HasPrefix(unameU(), []byte("synology")):
|
|
||||||
return synology.Name
|
|
||||||
case bytes.HasPrefix(unameO(), []byte("Tomato")):
|
|
||||||
return tomato.Name
|
|
||||||
case haveDir("/config/scripts/post-config.d"):
|
|
||||||
return edgeos.Name
|
|
||||||
case haveFile("/etc/ubnt/init/vyatta-router"):
|
|
||||||
return edgeos.Name // For 2.x
|
|
||||||
case haveFile("/etc/firewalla_release"):
|
|
||||||
return firewalla.Name
|
|
||||||
}
|
|
||||||
return osName
|
|
||||||
}
|
|
||||||
|
|
||||||
func haveFile(file string) bool {
|
|
||||||
_, err := os.Stat(file)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func haveDir(dir string) bool {
|
|
||||||
fi, _ := os.Stat(dir)
|
|
||||||
return fi != nil && fi.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
func unameO() []byte {
|
|
||||||
out, _ := exec.Command("uname", "-o").Output()
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func unameU() []byte {
|
|
||||||
out, _ := exec.Command("uname", "-u").Output()
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// isUbios reports whether the current machine is running on Ubios.
|
|
||||||
func isUbios() bool {
|
|
||||||
if haveDir("/data/unifi") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if err := exec.Command("ubnt-device-info", "firmware").Run(); err == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/ddwrt"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/merlin"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/tomato"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/ubios"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
systems := []service.System{
|
|
||||||
&linuxSystemService{
|
|
||||||
name: "ddwrt",
|
|
||||||
detect: func() bool { return Name() == ddwrt.Name },
|
|
||||||
interactive: func() bool {
|
|
||||||
is, _ := isInteractive()
|
|
||||||
return is
|
|
||||||
},
|
|
||||||
new: newddwrtService,
|
|
||||||
},
|
|
||||||
&linuxSystemService{
|
|
||||||
name: "merlin",
|
|
||||||
detect: func() bool { return Name() == merlin.Name },
|
|
||||||
interactive: func() bool {
|
|
||||||
is, _ := isInteractive()
|
|
||||||
return is
|
|
||||||
},
|
|
||||||
new: newMerlinService,
|
|
||||||
},
|
|
||||||
&linuxSystemService{
|
|
||||||
name: "ubios",
|
|
||||||
detect: func() bool {
|
|
||||||
if Name() != ubios.Name {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
out, err := exec.Command("ubnt-device-info", "firmware").CombinedOutput()
|
|
||||||
if err == nil {
|
|
||||||
// For v2/v3, UbiOS use a Debian base with systemd, so it is not
|
|
||||||
// necessary to use custom implementation for supporting init system.
|
|
||||||
return bytes.HasPrefix(out, []byte("1."))
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
interactive: func() bool {
|
|
||||||
is, _ := isInteractive()
|
|
||||||
return is
|
|
||||||
},
|
|
||||||
new: newUbiosService,
|
|
||||||
},
|
|
||||||
&linuxSystemService{
|
|
||||||
name: "tomato",
|
|
||||||
detect: func() bool { return Name() == tomato.Name },
|
|
||||||
interactive: func() bool {
|
|
||||||
is, _ := isInteractive()
|
|
||||||
return is
|
|
||||||
},
|
|
||||||
new: newTomatoService,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
systems = append(systems, service.AvailableSystems()...)
|
|
||||||
service.ChooseSystem(systems...)
|
|
||||||
}
|
|
||||||
|
|
||||||
type linuxSystemService struct {
|
|
||||||
name string
|
|
||||||
detect func() bool
|
|
||||||
interactive func() bool
|
|
||||||
new func(i service.Interface, platform string, c *service.Config) (service.Service, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sc linuxSystemService) String() string {
|
|
||||||
return sc.name
|
|
||||||
}
|
|
||||||
func (sc linuxSystemService) Detect() bool {
|
|
||||||
return sc.detect()
|
|
||||||
}
|
|
||||||
func (sc linuxSystemService) Interactive() bool {
|
|
||||||
return sc.interactive()
|
|
||||||
}
|
|
||||||
func (sc linuxSystemService) New(i service.Interface, c *service.Config) (service.Service, error) {
|
|
||||||
return sc.new(i, sc.String(), c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isInteractive() (bool, error) {
|
|
||||||
ppid := os.Getppid()
|
|
||||||
if ppid == 1 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ddwrtSvc struct {
|
|
||||||
i service.Interface
|
|
||||||
platform string
|
|
||||||
*service.Config
|
|
||||||
rcStartup string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newddwrtService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
|
|
||||||
s := &ddwrtSvc{
|
|
||||||
i: i,
|
|
||||||
platform: platform,
|
|
||||||
Config: c,
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll("/jffs/etc/config", 0644); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) String() string {
|
|
||||||
if len(s.DisplayName) > 0 {
|
|
||||||
return s.DisplayName
|
|
||||||
}
|
|
||||||
return s.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) Platform() string {
|
|
||||||
return s.platform
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) configPath() string {
|
|
||||||
return fmt.Sprintf("/jffs/etc/config/%s.startup", s.Config.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) template() *template.Template {
|
|
||||||
return template.Must(template.New("").Parse(ddwrtSvcScript))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) Install() error {
|
|
||||||
confPath := s.configPath()
|
|
||||||
if _, err := os.Stat(confPath); err == nil {
|
|
||||||
return fmt.Errorf("already installed: %s", confPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
path, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(path, "/jffs/") {
|
|
||||||
return errors.New("could not install service outside /jffs")
|
|
||||||
}
|
|
||||||
|
|
||||||
var to = &struct {
|
|
||||||
*service.Config
|
|
||||||
Path string
|
|
||||||
}{
|
|
||||||
s.Config,
|
|
||||||
path,
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create(confPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
if err := s.template().Execute(f, to); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = os.Chmod(confPath, 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
if err := template.Must(template.New("").Parse(ddwrtStartupCmd)).Execute(&sb, to); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.rcStartup = sb.String()
|
|
||||||
curVal, err := nvram.Run("get", nvram.RCStartupKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := nvram.Run("set", nvram.CtrldKeyPrefix+nvram.RCStartupKey+"="+curVal); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
val := strings.Join([]string{curVal, s.rcStartup + " &", fmt.Sprintf(`echo $! > "/tmp/%s.pid"`, s.Config.Name)}, "\n")
|
|
||||||
|
|
||||||
if _, err := nvram.Run("set", nvram.RCStartupKey+"="+val); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if out, err := nvram.Run("commit"); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", out, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) Uninstall() error {
|
|
||||||
if err := os.Remove(s.configPath()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrldStartupKey := nvram.CtrldKeyPrefix + nvram.RCStartupKey
|
|
||||||
rcStartup, err := nvram.Run("get", ctrldStartupKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, _ = nvram.Run("unset", ctrldStartupKey)
|
|
||||||
if _, err := nvram.Run("set", nvram.RCStartupKey+"="+rcStartup); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if out, err := nvram.Run("commit"); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", out, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) Logger(errs chan<- error) (service.Logger, error) {
|
|
||||||
if service.Interactive() {
|
|
||||||
return service.ConsoleLogger, nil
|
|
||||||
}
|
|
||||||
return s.SystemLogger(errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
|
|
||||||
// TODO(cuonglm): detect syslog enable and return proper logger?
|
|
||||||
// this at least works with default configuration.
|
|
||||||
if service.Interactive() {
|
|
||||||
return service.ConsoleLogger, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
return &noopLogger{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) Run() (err error) {
|
|
||||||
err = s.i.Start(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if interactice, _ := isInteractive(); !interactice {
|
|
||||||
signal.Ignore(syscall.SIGHUP)
|
|
||||||
}
|
|
||||||
var sigChan = make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
|
||||||
<-sigChan
|
|
||||||
|
|
||||||
return s.i.Stop(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) Status() (service.Status, error) {
|
|
||||||
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
|
|
||||||
return service.StatusUnknown, service.ErrNotInstalled
|
|
||||||
}
|
|
||||||
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return service.StatusUnknown, err
|
|
||||||
}
|
|
||||||
switch string(bytes.TrimSpace(out)) {
|
|
||||||
case "running":
|
|
||||||
return service.StatusRunning, nil
|
|
||||||
default:
|
|
||||||
return service.StatusStopped, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) Start() error {
|
|
||||||
return exec.Command(s.configPath(), "start").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) Stop() error {
|
|
||||||
return exec.Command(s.configPath(), "stop").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ddwrtSvc) Restart() error {
|
|
||||||
err := s.Stop()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
type noopLogger struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c noopLogger) Error(v ...interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (c noopLogger) Warning(v ...interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (c noopLogger) Info(v ...interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (c noopLogger) Errorf(format string, a ...interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (c noopLogger) Warningf(format string, a ...interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (c noopLogger) Infof(format string, a ...interface{}) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ddwrtStartupCmd = `{{.Path}}{{range .Arguments}} {{.}}{{end}}`
|
|
||||||
const ddwrtSvcScript = `#!/bin/sh
|
|
||||||
|
|
||||||
name="{{.Name}}"
|
|
||||||
cmd="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
|
||||||
pid_file="/tmp/$name.pid"
|
|
||||||
|
|
||||||
get_pid() {
|
|
||||||
cat "$pid_file"
|
|
||||||
}
|
|
||||||
|
|
||||||
is_running() {
|
|
||||||
[ -f "$pid_file" ] && ps | grep -q "^ *$(get_pid) "
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
start)
|
|
||||||
if is_running; then
|
|
||||||
echo "Already started"
|
|
||||||
else
|
|
||||||
echo "Starting $name"
|
|
||||||
$cmd &
|
|
||||||
echo $! > "$pid_file"
|
|
||||||
chmod 600 "$pid_file"
|
|
||||||
if ! is_running; then
|
|
||||||
echo "Failed to start $name"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
if is_running; then
|
|
||||||
echo -n "Stopping $name..."
|
|
||||||
kill "$(get_pid)"
|
|
||||||
for _ in 1 2 3 4 5; do
|
|
||||||
if ! is_running; then
|
|
||||||
echo "stopped"
|
|
||||||
if [ -f "$pid_file" ]; then
|
|
||||||
rm "$pid_file"
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
printf "."
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
echo "failed to stop $name"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
$0 stop
|
|
||||||
$0 start
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
if is_running; then
|
|
||||||
echo "running"
|
|
||||||
else
|
|
||||||
echo "stopped"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: $0 {start|stop|restart|status}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
exit 0
|
|
||||||
`
|
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
merlinJFFSScriptPath = "/jffs/scripts/services-start"
|
|
||||||
merlinJFFSServiceEventScriptPath = "/jffs/scripts/service-event"
|
|
||||||
)
|
|
||||||
|
|
||||||
type merlinSvc struct {
|
|
||||||
i service.Interface
|
|
||||||
platform string
|
|
||||||
*service.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMerlinService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
|
|
||||||
s := &merlinSvc{
|
|
||||||
i: i,
|
|
||||||
platform: platform,
|
|
||||||
Config: c,
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) String() string {
|
|
||||||
if len(s.DisplayName) > 0 {
|
|
||||||
return s.DisplayName
|
|
||||||
}
|
|
||||||
return s.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) Platform() string {
|
|
||||||
return s.platform
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) configPath() string {
|
|
||||||
bin := s.Config.Executable
|
|
||||||
if bin == "" {
|
|
||||||
path, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
bin = path
|
|
||||||
}
|
|
||||||
return bin + ".startup"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) template() *template.Template {
|
|
||||||
return template.Must(template.New("").Parse(merlinSvcScript))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) Install() error {
|
|
||||||
exePath, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(exePath, "/jffs/") {
|
|
||||||
return errors.New("could not install service outside /jffs")
|
|
||||||
}
|
|
||||||
if _, err := nvram.Run("set", "jffs2_scripts=1"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := nvram.Run("commit"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
confPath := s.configPath()
|
|
||||||
if _, err := os.Stat(confPath); err == nil {
|
|
||||||
return fmt.Errorf("already installed: %s", confPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
var to = &struct {
|
|
||||||
*service.Config
|
|
||||||
Path string
|
|
||||||
}{
|
|
||||||
s.Config,
|
|
||||||
exePath,
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create(confPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("os.Create: %w", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
if err := s.template().Execute(f, to); err != nil {
|
|
||||||
return fmt.Errorf("s.template.Execute: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = os.Chmod(confPath, 0755); err != nil {
|
|
||||||
return fmt.Errorf("os.Chmod: startup script: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(merlinJFFSScriptPath), 0755); err != nil {
|
|
||||||
return fmt.Errorf("os.MkdirAll: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpScript, err := os.CreateTemp("", "ctrld_install")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("os.CreateTemp: %w", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(tmpScript.Name())
|
|
||||||
defer tmpScript.Close()
|
|
||||||
|
|
||||||
if _, err := tmpScript.WriteString(merlinAddLineToScript); err != nil {
|
|
||||||
return fmt.Errorf("tmpScript.WriteString: %w", err)
|
|
||||||
}
|
|
||||||
if err := tmpScript.Close(); err != nil {
|
|
||||||
return fmt.Errorf("tmpScript.Close: %w", err)
|
|
||||||
}
|
|
||||||
addLineToScript := func(line, script string) error {
|
|
||||||
if _, err := os.Stat(script); os.IsNotExist(err) {
|
|
||||||
if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := os.Chmod(script, 0755); err != nil {
|
|
||||||
return fmt.Errorf("os.Chmod: jffs script: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil {
|
|
||||||
return fmt.Errorf("exec.Command: add startup script: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for script, line := range map[string]string{
|
|
||||||
merlinJFFSScriptPath: s.configPath() + " start",
|
|
||||||
merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`,
|
|
||||||
} {
|
|
||||||
if err := addLineToScript(line, script); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) Uninstall() error {
|
|
||||||
if err := os.Remove(s.configPath()); err != nil {
|
|
||||||
return fmt.Errorf("os.Remove: %w", err)
|
|
||||||
}
|
|
||||||
tmpScript, err := os.CreateTemp("", "ctrld_uninstall")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("os.CreateTemp: %w", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(tmpScript.Name())
|
|
||||||
defer tmpScript.Close()
|
|
||||||
|
|
||||||
if _, err := tmpScript.WriteString(merlinRemoveLineFromScript); err != nil {
|
|
||||||
return fmt.Errorf("tmpScript.WriteString: %w", err)
|
|
||||||
}
|
|
||||||
if err := tmpScript.Close(); err != nil {
|
|
||||||
return fmt.Errorf("tmpScript.Close: %w", err)
|
|
||||||
}
|
|
||||||
removeLineFromScript := func(line, script string) error {
|
|
||||||
if _, err := os.Stat(script); os.IsNotExist(err) {
|
|
||||||
if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := os.Chmod(script, 0755); err != nil {
|
|
||||||
return fmt.Errorf("os.Chmod: jffs script: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil {
|
|
||||||
return fmt.Errorf("exec.Command: add startup script: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for script, line := range map[string]string{
|
|
||||||
merlinJFFSScriptPath: s.configPath() + " start",
|
|
||||||
merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`,
|
|
||||||
} {
|
|
||||||
if err := removeLineFromScript(line, script); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) Logger(errs chan<- error) (service.Logger, error) {
|
|
||||||
if service.Interactive() {
|
|
||||||
return service.ConsoleLogger, nil
|
|
||||||
}
|
|
||||||
return s.SystemLogger(errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
|
|
||||||
return newSysLogger(s.Name, errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) Run() (err error) {
|
|
||||||
err = s.i.Start(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if interactice, _ := isInteractive(); !interactice {
|
|
||||||
signal.Ignore(syscall.SIGHUP)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sigChan = make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
|
||||||
<-sigChan
|
|
||||||
|
|
||||||
return s.i.Stop(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) Status() (service.Status, error) {
|
|
||||||
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
|
|
||||||
return service.StatusUnknown, service.ErrNotInstalled
|
|
||||||
}
|
|
||||||
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return service.StatusUnknown, err
|
|
||||||
}
|
|
||||||
switch string(bytes.TrimSpace(out)) {
|
|
||||||
case "running":
|
|
||||||
return service.StatusRunning, nil
|
|
||||||
default:
|
|
||||||
return service.StatusStopped, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) Start() error {
|
|
||||||
return exec.Command(s.configPath(), "start").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) Stop() error {
|
|
||||||
return exec.Command(s.configPath(), "stop").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *merlinSvc) Restart() error {
|
|
||||||
err := s.Stop()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
const merlinSvcScript = `#!/bin/sh
|
|
||||||
|
|
||||||
name="{{.Name}}"
|
|
||||||
cmd="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
|
||||||
pid_file="/tmp/$name.pid"
|
|
||||||
|
|
||||||
get_pid() {
|
|
||||||
cat "$pid_file"
|
|
||||||
}
|
|
||||||
|
|
||||||
is_running() {
|
|
||||||
[ -f "$pid_file" ] && ps | grep -q "^ *$(get_pid) "
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
start)
|
|
||||||
if is_running; then
|
|
||||||
logger -c "Already started"
|
|
||||||
else
|
|
||||||
logger -c "Starting $name"
|
|
||||||
if [ -f /rom/ca-bundle.crt ]; then
|
|
||||||
# For John’s fork
|
|
||||||
export SSL_CERT_FILE=/rom/ca-bundle.crt
|
|
||||||
fi
|
|
||||||
$cmd &
|
|
||||||
echo $! > "$pid_file"
|
|
||||||
chmod 600 "$pid_file"
|
|
||||||
if ! is_running; then
|
|
||||||
logger -c "Failed to start $name"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
if is_running; then
|
|
||||||
logger -c "Stopping $name..."
|
|
||||||
kill "$(get_pid)"
|
|
||||||
for _ in 1 2 3 4 5; do
|
|
||||||
if ! is_running; then
|
|
||||||
logger -c "stopped"
|
|
||||||
if [ -f "$pid_file" ]; then
|
|
||||||
rm "$pid_file"
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
printf "."
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
logger -c "failed to stop $name"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
$0 stop
|
|
||||||
$0 start
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
if is_running; then
|
|
||||||
echo "running"
|
|
||||||
else
|
|
||||||
echo "stopped"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
service_event)
|
|
||||||
event=$2
|
|
||||||
svc=$3
|
|
||||||
dnsmasq_pid_file=$(sed -n '/pid-file=/s///p' /etc/dnsmasq.conf)
|
|
||||||
|
|
||||||
if [ "$event" = "restart" ] && [ "$svc" = "diskmon" ]; then
|
|
||||||
kill "$(cat "$dnsmasq_pid_file")" >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: $0 {start|stop|restart|status}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
exit 0
|
|
||||||
`
|
|
||||||
|
|
||||||
const merlinAddLineToScript = `#!/bin/sh
|
|
||||||
|
|
||||||
line=$1
|
|
||||||
file=$2
|
|
||||||
|
|
||||||
. /usr/sbin/helper.sh
|
|
||||||
|
|
||||||
pc_append "$line" "$file"
|
|
||||||
`
|
|
||||||
|
|
||||||
const merlinRemoveLineFromScript = `#!/bin/sh
|
|
||||||
|
|
||||||
line=$1
|
|
||||||
file=$2
|
|
||||||
|
|
||||||
. /usr/sbin/helper.sh
|
|
||||||
|
|
||||||
pc_delete "$line" "$file"
|
|
||||||
`
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
|
||||||
)
|
|
||||||
|
|
||||||
const tomatoNvramScriptWanupKey = "script_wanup"
|
|
||||||
|
|
||||||
type tomatoSvc struct {
|
|
||||||
i service.Interface
|
|
||||||
platform string
|
|
||||||
*service.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTomatoService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
|
|
||||||
s := &tomatoSvc{
|
|
||||||
i: i,
|
|
||||||
platform: platform,
|
|
||||||
Config: c,
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) String() string {
|
|
||||||
if len(s.DisplayName) > 0 {
|
|
||||||
return s.DisplayName
|
|
||||||
}
|
|
||||||
return s.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) Platform() string {
|
|
||||||
return s.platform
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) configPath() string {
|
|
||||||
bin := s.Config.Executable
|
|
||||||
if bin == "" {
|
|
||||||
path, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
bin = path
|
|
||||||
}
|
|
||||||
return bin + ".startup"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) template() *template.Template {
|
|
||||||
return template.Must(template.New("").Parse(tomatoSvcScript))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) Install() error {
|
|
||||||
exePath, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(exePath, "/jffs/") {
|
|
||||||
return errors.New("could not install service outside /jffs")
|
|
||||||
}
|
|
||||||
if _, err := nvram.Run("set", "jffs2_on=1"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := nvram.Run("commit"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
confPath := s.configPath()
|
|
||||||
if _, err := os.Stat(confPath); err == nil {
|
|
||||||
return fmt.Errorf("already installed: %s", confPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
var to = &struct {
|
|
||||||
*service.Config
|
|
||||||
Path string
|
|
||||||
}{
|
|
||||||
s.Config,
|
|
||||||
exePath,
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create(confPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("os.Create: %w", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
if err := s.template().Execute(f, to); err != nil {
|
|
||||||
return fmt.Errorf("s.template.Execute: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = os.Chmod(confPath, 0755); err != nil {
|
|
||||||
return fmt.Errorf("os.Chmod: startup script: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nvramKvMap := map[string]string{
|
|
||||||
tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method.
|
|
||||||
}
|
|
||||||
old, err := nvram.Run("get", tomatoNvramScriptWanupKey)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("nvram: %w", err)
|
|
||||||
}
|
|
||||||
nvramKvMap[tomatoNvramScriptWanupKey] = strings.Join([]string{old, s.configPath() + " start"}, "\n")
|
|
||||||
if err := nvram.SetKV(nvramKvMap, nvram.CtrldInstallKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) Uninstall() error {
|
|
||||||
if err := os.Remove(s.configPath()); err != nil {
|
|
||||||
return fmt.Errorf("os.Remove: %w", err)
|
|
||||||
}
|
|
||||||
nvramKvMap := map[string]string{
|
|
||||||
tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method.
|
|
||||||
}
|
|
||||||
// Restore old configs.
|
|
||||||
if err := nvram.Restore(nvramKvMap, nvram.CtrldInstallKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) Logger(errs chan<- error) (service.Logger, error) {
|
|
||||||
if service.Interactive() {
|
|
||||||
return service.ConsoleLogger, nil
|
|
||||||
}
|
|
||||||
return s.SystemLogger(errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
|
|
||||||
return newSysLogger(s.Name, errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) Run() (err error) {
|
|
||||||
err = s.i.Start(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if interactice, _ := isInteractive(); !interactice {
|
|
||||||
signal.Ignore(syscall.SIGHUP)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sigChan = make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
|
||||||
<-sigChan
|
|
||||||
|
|
||||||
return s.i.Stop(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) Status() (service.Status, error) {
|
|
||||||
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
|
|
||||||
return service.StatusUnknown, service.ErrNotInstalled
|
|
||||||
}
|
|
||||||
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return service.StatusUnknown, err
|
|
||||||
}
|
|
||||||
switch string(bytes.TrimSpace(out)) {
|
|
||||||
case "running":
|
|
||||||
return service.StatusRunning, nil
|
|
||||||
default:
|
|
||||||
return service.StatusStopped, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) Start() error {
|
|
||||||
return exec.Command(s.configPath(), "start").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) Stop() error {
|
|
||||||
return exec.Command(s.configPath(), "stop").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tomatoSvc) Restart() error {
|
|
||||||
return exec.Command(s.configPath(), "restart").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://wiki.freshtomato.org/doku.php/freshtomato_zerotier?s[]=%2Aservice%2A
|
|
||||||
const tomatoSvcScript = `#!/bin/sh
|
|
||||||
|
|
||||||
|
|
||||||
NAME="{{.Name}}"
|
|
||||||
CMD="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
|
||||||
LOG_FILE="/var/log/${NAME}.log"
|
|
||||||
PID_FILE="/tmp/$NAME.pid"
|
|
||||||
|
|
||||||
|
|
||||||
alias elog="logger -t $NAME -s"
|
|
||||||
|
|
||||||
|
|
||||||
COND=$1
|
|
||||||
[ $# -eq 0 ] && COND="start"
|
|
||||||
|
|
||||||
get_pid() {
|
|
||||||
cat "$PID_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
is_running() {
|
|
||||||
[ -f "$PID_FILE" ] && ps | grep -q "^ *$(get_pid) "
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
if is_running; then
|
|
||||||
elog "$NAME is already running."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
elog "Starting $NAME Services: "
|
|
||||||
$CMD &
|
|
||||||
echo $! > "$PID_FILE"
|
|
||||||
chmod 600 "$PID_FILE"
|
|
||||||
if is_running; then
|
|
||||||
elog "succeeded."
|
|
||||||
else
|
|
||||||
elog "failed."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
if ! is_running; then
|
|
||||||
elog "$NAME is not running."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
elog "Shutting down $NAME Services: "
|
|
||||||
kill -SIGTERM "$(get_pid)"
|
|
||||||
for _ in 1 2 3 4 5; do
|
|
||||||
if ! is_running; then
|
|
||||||
if [ -f "$pid_file" ]; then
|
|
||||||
rm "$pid_file"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
printf "."
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
if ! is_running; then
|
|
||||||
elog "succeeded."
|
|
||||||
else
|
|
||||||
elog "failed."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
do_restart() {
|
|
||||||
stop
|
|
||||||
start
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
do_status() {
|
|
||||||
if ! is_running; then
|
|
||||||
echo "stopped"
|
|
||||||
else
|
|
||||||
echo "running"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
case "$COND" in
|
|
||||||
start)
|
|
||||||
start
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
stop
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
do_restart
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
do_status
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
elog "Usage: $0 (start|stop|restart|status)"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
exit 0
|
|
||||||
`
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is a copy of https://github.com/kardianos/service/blob/v1.2.1/service_sysv_linux.go,
|
|
||||||
// with modification for supporting ubios v1 init system.
|
|
||||||
|
|
||||||
type ubiosSvc struct {
|
|
||||||
i service.Interface
|
|
||||||
platform string
|
|
||||||
*service.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUbiosService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
|
|
||||||
s := &ubiosSvc{
|
|
||||||
i: i,
|
|
||||||
platform: platform,
|
|
||||||
Config: c,
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) String() string {
|
|
||||||
if len(s.DisplayName) > 0 {
|
|
||||||
return s.DisplayName
|
|
||||||
}
|
|
||||||
return s.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) Platform() string {
|
|
||||||
return s.platform
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) configPath() string {
|
|
||||||
return "/etc/init.d/" + s.Config.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) execPath() (string, error) {
|
|
||||||
if len(s.Executable) != 0 {
|
|
||||||
return filepath.Abs(s.Executable)
|
|
||||||
}
|
|
||||||
return os.Executable()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) template() *template.Template {
|
|
||||||
return template.Must(template.New("").Funcs(tf).Parse(ubiosSvcScript))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) Install() error {
|
|
||||||
confPath := s.configPath()
|
|
||||||
if _, err := os.Stat(confPath); err == nil {
|
|
||||||
return fmt.Errorf("init already exists: %s", confPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create(confPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create config path: %w", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
path, err := s.execPath()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get exec path: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var to = &struct {
|
|
||||||
*service.Config
|
|
||||||
Path string
|
|
||||||
DnsMasqConfPath string
|
|
||||||
}{
|
|
||||||
s.Config,
|
|
||||||
path,
|
|
||||||
filepath.Join(dnsmasq.UbiosConfPath(), dnsmasq.UbiosConfName),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.template().Execute(f, to); err != nil {
|
|
||||||
return fmt.Errorf("failed to create init script: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
return fmt.Errorf("failed to save init script: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = os.Chmod(confPath, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to set init script executable: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable on boot
|
|
||||||
script, err := os.CreateTemp("", "ctrld_boot.service")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create boot service tmp file: %w", err)
|
|
||||||
}
|
|
||||||
defer script.Close()
|
|
||||||
|
|
||||||
svcConfig := *to.Config
|
|
||||||
svcConfig.Arguments = os.Args[1:]
|
|
||||||
to.Config = &svcConfig
|
|
||||||
if err := template.Must(template.New("").Funcs(tf).Parse(ubiosBootSystemdService)).Execute(script, &to); err != nil {
|
|
||||||
return fmt.Errorf("failed to create boot service file: %w", err)
|
|
||||||
}
|
|
||||||
if err := script.Close(); err != nil {
|
|
||||||
return fmt.Errorf("failed to save boot service file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the boot script to container and start.
|
|
||||||
cmd := exec.Command("podman", "cp", "--pause=false", script.Name(), "unifi-os:/lib/systemd/system/ctrld-boot.service")
|
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy boot script, out: %s, err: %v", string(out), err)
|
|
||||||
}
|
|
||||||
cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "enable", "--now", "ctrld-boot.service")
|
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("failed to start ctrld boot script, out: %s, err: %v", string(out), err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) Uninstall() error {
|
|
||||||
if err := os.Remove(s.configPath()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Remove ctrld-boot service inside unifi-os container.
|
|
||||||
cmd := exec.Command("podman", "exec", "unifi-os", "systemctl", "disable", "ctrld-boot.service")
|
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("failed to disable ctrld-boot service, out: %s, err: %v", string(out), err)
|
|
||||||
}
|
|
||||||
cmd = exec.Command("podman", "exec", "unifi-os", "rm", "/lib/systemd/system/ctrld-boot.service")
|
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove ctrld-boot service file, out: %s, err: %v", string(out), err)
|
|
||||||
}
|
|
||||||
cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "daemon-reload")
|
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("failed to reload systemd service, out: %s, err: %v", string(out), err)
|
|
||||||
}
|
|
||||||
cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "reset-failed")
|
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("failed to reset-failed systemd service, out: %s, err: %v", string(out), err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) Logger(errs chan<- error) (service.Logger, error) {
|
|
||||||
if service.Interactive() {
|
|
||||||
return service.ConsoleLogger, nil
|
|
||||||
}
|
|
||||||
return s.SystemLogger(errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
|
|
||||||
return newSysLogger(s.Name, errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) Run() (err error) {
|
|
||||||
err = s.i.Start(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if interactice, _ := isInteractive(); !interactice {
|
|
||||||
signal.Ignore(syscall.SIGHUP)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sigChan = make(chan os.Signal, 3)
|
|
||||||
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
|
||||||
<-sigChan
|
|
||||||
|
|
||||||
return s.i.Stop(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) Status() (service.Status, error) {
|
|
||||||
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
|
|
||||||
return service.StatusUnknown, service.ErrNotInstalled
|
|
||||||
}
|
|
||||||
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return service.StatusUnknown, err
|
|
||||||
}
|
|
||||||
switch string(bytes.TrimSpace(out)) {
|
|
||||||
case "Running":
|
|
||||||
return service.StatusRunning, nil
|
|
||||||
default:
|
|
||||||
return service.StatusStopped, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) Start() error {
|
|
||||||
return exec.Command(s.configPath(), "start").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) Stop() error {
|
|
||||||
return exec.Command(s.configPath(), "stop").Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ubiosSvc) Restart() error {
|
|
||||||
err := s.Stop()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
return s.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
const ubiosBootSystemdService = `[Unit]
|
|
||||||
Description=Run ctrld On Startup UDM
|
|
||||||
Wants=network-online.target
|
|
||||||
After=network-online.target
|
|
||||||
Wants=unifi-mongodb
|
|
||||||
After=unifi-mongodb
|
|
||||||
StartLimitIntervalSec=500
|
|
||||||
StartLimitBurst=5
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5s
|
|
||||||
ExecStart=/sbin/ssh-proxy '[ -f "{{.DnsMasqConfPath}}" ] || {{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}'
|
|
||||||
RemainAfterExit=true
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
`
|
|
||||||
|
|
||||||
const ubiosSvcScript = `#!/bin/sh
|
|
||||||
# For RedHat and cousins:
|
|
||||||
# chkconfig: - 99 01
|
|
||||||
# description: {{.Description}}
|
|
||||||
# processname: {{.Path}}
|
|
||||||
|
|
||||||
### BEGIN INIT INFO
|
|
||||||
# Provides: {{.Path}}
|
|
||||||
# Required-Start:
|
|
||||||
# Required-Stop:
|
|
||||||
# Default-Start: 2 3 4 5
|
|
||||||
# Default-Stop: 0 1 6
|
|
||||||
# Short-Description: {{.DisplayName}}
|
|
||||||
# Description: {{.Description}}
|
|
||||||
### END INIT INFO
|
|
||||||
|
|
||||||
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
|
|
||||||
|
|
||||||
name=$(basename $(readlink -f $0))
|
|
||||||
pid_file="/var/run/$name.pid"
|
|
||||||
stdout_log="/var/log/$name.log"
|
|
||||||
stderr_log="/var/log/$name.err"
|
|
||||||
|
|
||||||
[ -e /etc/sysconfig/$name ] && . /etc/sysconfig/$name
|
|
||||||
|
|
||||||
get_pid() {
|
|
||||||
cat "$pid_file"
|
|
||||||
}
|
|
||||||
|
|
||||||
is_running() {
|
|
||||||
[ -f "$pid_file" ] && cat /proc/$(get_pid)/stat > /dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
start)
|
|
||||||
if is_running; then
|
|
||||||
echo "Already started"
|
|
||||||
else
|
|
||||||
echo "Starting $name"
|
|
||||||
{{if .WorkingDirectory}}cd '{{.WorkingDirectory}}'{{end}}
|
|
||||||
$cmd >> "$stdout_log" 2>> "$stderr_log" &
|
|
||||||
echo $! > "$pid_file"
|
|
||||||
if ! is_running; then
|
|
||||||
echo "Unable to start, see $stdout_log and $stderr_log"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
if is_running; then
|
|
||||||
echo -n "Stopping $name.."
|
|
||||||
kill $(get_pid)
|
|
||||||
for i in $(seq 1 10)
|
|
||||||
do
|
|
||||||
if ! is_running; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo -n "."
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo
|
|
||||||
if is_running; then
|
|
||||||
echo "Not stopped; may still be shutting down or shutdown may have failed"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "Stopped"
|
|
||||||
if [ -f "$pid_file" ]; then
|
|
||||||
rm "$pid_file"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Not running"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
$0 stop
|
|
||||||
if is_running; then
|
|
||||||
echo "Unable to stop, will not attempt to start"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
$0 start
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
if is_running; then
|
|
||||||
echo "Running"
|
|
||||||
else
|
|
||||||
echo "Stopped"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: $0 {start|stop|restart|status}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
exit 0
|
|
||||||
`
|
|
||||||
|
|
||||||
var tf = map[string]interface{}{
|
|
||||||
"cmd": func(s string) string {
|
|
||||||
return `"` + strings.Replace(s, `"`, `\"`, -1) + `"`
|
|
||||||
},
|
|
||||||
"cmdEscape": func(s string) string {
|
|
||||||
return strings.Replace(s, " ", `\x20`, -1)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
package synology
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
"tailscale.com/logtail/backoff"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/ntp"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Name = "synology"
|
|
||||||
|
|
||||||
synologyDNSMasqConfigPath = "/etc/dhcpd/dhcpd-zzz-ctrld.conf"
|
|
||||||
synologyDhcpdInfoPath = "/etc/dhcpd/dhcpd-zzz-ctrld.info"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Synology struct {
|
|
||||||
cfg *ctrld.Config
|
|
||||||
useUpstart bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a router.Router for configuring/setup/run ctrld on Ubios routers.
|
|
||||||
func New(cfg *ctrld.Config) *Synology {
|
|
||||||
return &Synology{
|
|
||||||
cfg: cfg,
|
|
||||||
useUpstart: service.Platform() == "linux-upstart",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Synology) ConfigureService(svc *service.Config) error {
|
|
||||||
svc.Option["LogOutput"] = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Synology) Install(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Synology) Uninstall(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Synology) PreRun() error {
|
|
||||||
if s.useUpstart {
|
|
||||||
if err := ntp.WaitUpstart(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return waitDhcpServer()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Synology) Setup() error {
|
|
||||||
if s.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, s.cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(synologyDNSMasqConfigPath, []byte(data), 0600); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(synologyDhcpdInfoPath, []byte(`enable="yes"`), 0600); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Synology) Cleanup() error {
|
|
||||||
if s.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Remove the custom config files.
|
|
||||||
for _, f := range []string{synologyDNSMasqConfigPath, synologyDhcpdInfoPath} {
|
|
||||||
if err := os.Remove(f); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func restartDNSMasq() error {
|
|
||||||
if out, err := exec.Command("/etc/rc.network", "nat-restart-dhcp").CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("synologyRestartDNSMasq: %s - %w", string(out), err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitDhcpServer() error {
|
|
||||||
// Wait until `initctl status dhcpserver` returns running state.
|
|
||||||
b := backoff.NewBackoff("waitDhcpServer", func(format string, args ...any) {}, 10*time.Second)
|
|
||||||
for {
|
|
||||||
out, err := exec.Command("initctl", "status", "dhcpserver").CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "Unknown job") {
|
|
||||||
// dhcpserver service does not exist.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("exec.Command: %w", err)
|
|
||||||
}
|
|
||||||
if bytes.Contains(out, []byte("start/running")) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
b.BackOff(context.Background(), errors.New("ntp not ready"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
//go:build linux || darwin || freebsd
|
|
||||||
|
|
||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log/syslog"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newSysLogger(name string, errs chan<- error) (service.Logger, error) {
|
|
||||||
w, err := syslog.New(syslog.LOG_INFO, name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return sysLogger{w, errs}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type sysLogger struct {
|
|
||||||
*syslog.Writer
|
|
||||||
errs chan<- error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s sysLogger) send(err error) error {
|
|
||||||
if err != nil && s.errs != nil {
|
|
||||||
s.errs <- err
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s sysLogger) Error(v ...interface{}) error {
|
|
||||||
return s.send(s.Writer.Err(fmt.Sprint(v...)))
|
|
||||||
}
|
|
||||||
func (s sysLogger) Warning(v ...interface{}) error {
|
|
||||||
return s.send(s.Writer.Warning(fmt.Sprint(v...)))
|
|
||||||
}
|
|
||||||
func (s sysLogger) Info(v ...interface{}) error {
|
|
||||||
return s.send(s.Writer.Info(fmt.Sprint(v...)))
|
|
||||||
}
|
|
||||||
func (s sysLogger) Errorf(format string, a ...interface{}) error {
|
|
||||||
return s.send(s.Writer.Err(fmt.Sprintf(format, a...)))
|
|
||||||
}
|
|
||||||
func (s sysLogger) Warningf(format string, a ...interface{}) error {
|
|
||||||
return s.send(s.Writer.Warning(fmt.Sprintf(format, a...)))
|
|
||||||
}
|
|
||||||
func (s sysLogger) Infof(format string, a ...interface{}) error {
|
|
||||||
return s.send(s.Writer.Info(fmt.Sprintf(format, a...)))
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import "github.com/kardianos/service"
|
|
||||||
|
|
||||||
func newSysLogger(name string, errs chan<- error) (service.Logger, error) {
|
|
||||||
return service.ConsoleLogger, nil
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package tomato
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/ntp"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Name = "freshtomato"
|
|
||||||
|
|
||||||
tomatoDnsCryptProxySvcName = "dnscrypt-proxy"
|
|
||||||
tomatoStubbySvcName = "stubby"
|
|
||||||
tomatoDNSMasqSvcName = "dnsmasq"
|
|
||||||
)
|
|
||||||
|
|
||||||
var nvramKvMap = map[string]string{
|
|
||||||
"dnsmasq_custom": "", // Configuration of dnsmasq set by ctrld, filled by setupTomato.
|
|
||||||
"dnscrypt_proxy": "0", // Disable DNSCrypt.
|
|
||||||
"dnssec_enable": "0", // Disable DNSSEC.
|
|
||||||
"stubby_proxy": "0", // Disable Stubby
|
|
||||||
}
|
|
||||||
|
|
||||||
type FreshTomato struct {
|
|
||||||
cfg *ctrld.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a router.Router for configuring/setup/run ctrld on Ubios routers.
|
|
||||||
func New(cfg *ctrld.Config) *FreshTomato {
|
|
||||||
return &FreshTomato{cfg: cfg}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FreshTomato) ConfigureService(config *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FreshTomato) Install(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FreshTomato) Uninstall(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FreshTomato) PreRun() error {
|
|
||||||
_ = f.Cleanup()
|
|
||||||
return ntp.WaitNvram()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FreshTomato) Setup() error {
|
|
||||||
if f.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Already setup.
|
|
||||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, f.cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
nvramKvMap["dnsmasq_custom"] = data
|
|
||||||
if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart dnscrypt-proxy service.
|
|
||||||
if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Restart stubby service.
|
|
||||||
if err := tomatoRestartService(tomatoStubbySvcName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FreshTomato) Cleanup() error {
|
|
||||||
if f.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" {
|
|
||||||
return nil // was restored, nothing to do.
|
|
||||||
}
|
|
||||||
|
|
||||||
nvramKvMap["dnsmasq_custom"] = ""
|
|
||||||
// Restore old configs.
|
|
||||||
if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart dnscrypt-proxy service.
|
|
||||||
if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Restart stubby service.
|
|
||||||
if err := tomatoRestartService(tomatoStubbySvcName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func tomatoRestartService(name string) error {
|
|
||||||
return tomatoRestartServiceWithKill(name, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tomatoRestartServiceWithKill(name string, killBeforeRestart bool) error {
|
|
||||||
if killBeforeRestart {
|
|
||||||
_, _ = exec.Command("killall", name).CombinedOutput()
|
|
||||||
}
|
|
||||||
if out, err := exec.Command("service", name, "restart").CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("service restart %s: %s, %w", name, string(out), err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func restartDNSMasq() error {
|
|
||||||
return tomatoRestartService(tomatoDNSMasqSvcName)
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package ubios
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/edgeos"
|
|
||||||
)
|
|
||||||
|
|
||||||
const Name = "ubios"
|
|
||||||
|
|
||||||
type Ubios struct {
|
|
||||||
cfg *ctrld.Config
|
|
||||||
dnsmasqConfPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a router.Router for configuring/setup/run ctrld on Ubios routers.
|
|
||||||
func New(cfg *ctrld.Config) *Ubios {
|
|
||||||
return &Ubios{
|
|
||||||
cfg: cfg,
|
|
||||||
dnsmasqConfPath: filepath.Join(dnsmasq.UbiosConfPath(), dnsmasq.UbiosConfName),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Ubios) ConfigureService(config *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Ubios) Install(config *service.Config) error {
|
|
||||||
// See comment in (*edgeos.EdgeOS).Install method.
|
|
||||||
if edgeos.ContentFilteringEnabled() {
|
|
||||||
return edgeos.ErrContentFilteringEnabled
|
|
||||||
}
|
|
||||||
// See comment in (*edgeos.EdgeOS).Install method.
|
|
||||||
if edgeos.DnsShieldEnabled() {
|
|
||||||
return edgeos.ErrDnsShieldEnabled
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Ubios) Uninstall(_ *service.Config) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Ubios) PreRun() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Ubios) Setup() error {
|
|
||||||
if u.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, u.cfg, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(u.dnsmasqConfPath, []byte(data), 0600); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Ubios) Cleanup() error {
|
|
||||||
if u.cfg.FirstListener().IsDirectDnsListener() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Remove the custom dnsmasq config
|
|
||||||
if err := os.Remove(u.dnsmasqConfPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Restart dnsmasq service.
|
|
||||||
if err := restartDNSMasq(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func restartDNSMasq() error {
|
|
||||||
buf, err := os.ReadFile(dnsmasq.UbiosPidFile())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pid, err := strconv.ParseUint(string(bytes.TrimSpace(buf)), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
proc, err := os.FindProcess(int(pid))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return proc.Kill()
|
|
||||||
}
|
|
||||||
+2
-2
@@ -44,11 +44,11 @@ compress() {
|
|||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
*-linux-armv*)
|
*-linux-armv*)
|
||||||
echo >&2 "upx does not work on arm routers"
|
echo >&2 "upx does not work on arm platforms"
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
*-linux-mips*)
|
*-linux-mips*)
|
||||||
echo >&2 "upx does not work on mips routers"
|
echo >&2 "upx does not work on mips platforms"
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user