mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
Merge pull request #138 from Control-D-Inc/release-branch-v1.3.5
Release branch v1.3.5
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["windows-latest", "ubuntu-latest", "macOS-latest"]
|
||||
go: ["1.20.x"]
|
||||
go: ["1.21.x"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -3,3 +3,12 @@ gon.hcl
|
||||
|
||||
/Build
|
||||
.DS_Store
|
||||
|
||||
# Release folder
|
||||
dist/
|
||||
|
||||
# Binaries
|
||||
ctrld-*
|
||||
|
||||
# generated file
|
||||
cmd/cli/rsrc_*.syso
|
||||
|
||||
@@ -62,7 +62,7 @@ $ docker pull controldns/ctrld
|
||||
Alternatively, if you know what you're doing you can download pre-compiled binaries from the [Releases](https://github.com/Control-D-Inc/ctrld/releases) section for the appropriate platform.
|
||||
|
||||
## Build
|
||||
Lastly, you can build `ctrld` from source which requires `go1.20+`:
|
||||
Lastly, you can build `ctrld` from source which requires `go1.21+`:
|
||||
|
||||
```shell
|
||||
$ go build ./cmd/ctrld
|
||||
|
||||
264
cmd/cli/cli.go
264
cmd/cli/cli.go
@@ -50,9 +50,10 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
v = viper.NewWithOptions(viper.KeyDelimiter("::"))
|
||||
defaultConfigFile = "ctrld.toml"
|
||||
rootCertPool *x509.CertPool
|
||||
v = viper.NewWithOptions(viper.KeyDelimiter("::"))
|
||||
defaultConfigFile = "ctrld.toml"
|
||||
rootCertPool *x509.CertPool
|
||||
errSelfCheckNoAnswer = errors.New("no answer from ctrld listener")
|
||||
)
|
||||
|
||||
var basicModeFlags = []string{"listen", "primary_upstream", "secondary_upstream", "domains"}
|
||||
@@ -258,6 +259,16 @@ func initCLI() {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := s.Status()
|
||||
isCtrldInstalled := !errors.Is(err, service.ErrNotInstalled)
|
||||
|
||||
// If pin code was set, do not allow running start command.
|
||||
if status == service.StatusRunning {
|
||||
if err := checkDeactivationPin(s); isCheckDeactivationPinErr(err) {
|
||||
os.Exit(deactivationPinInvalidExitCode)
|
||||
}
|
||||
}
|
||||
|
||||
if router.Name() != "" && iface != "" {
|
||||
mainLog.Load().Debug().Msg("cleaning up router before installing")
|
||||
_ = p.router.Cleanup()
|
||||
@@ -266,7 +277,22 @@ func initCLI() {
|
||||
tasks := []task{
|
||||
{s.Stop, false},
|
||||
{func() error { return doGenerateNextDNSConfig(nextdns) }, true},
|
||||
{s.Uninstall, false},
|
||||
{func() error { return ensureUninstall(s) }, false},
|
||||
{func() error {
|
||||
// If ctrld is installed, we should not save current DNS settings, because:
|
||||
//
|
||||
// - The DNS settings was being set by ctrld already.
|
||||
// - We could not determine the state of DNS settings before installing ctrld.
|
||||
if isCtrldInstalled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save current DNS so we can restore later.
|
||||
withEachPhysicalInterfaces("", "save DNS settings", func(i *net.Interface) error {
|
||||
return saveCurrentStaticDNS(i)
|
||||
})
|
||||
return nil
|
||||
}, false},
|
||||
{s.Install, false},
|
||||
{s.Start, true},
|
||||
// Note that startCmd do not actually write ControlD config, but the config file was
|
||||
@@ -280,17 +306,40 @@ func initCLI() {
|
||||
return
|
||||
}
|
||||
|
||||
status := selfCheckStatus(s)
|
||||
switch status {
|
||||
case service.StatusRunning:
|
||||
ok, status, err := selfCheckStatus(s)
|
||||
switch {
|
||||
case ok && status == service.StatusRunning:
|
||||
mainLog.Load().Notice().Msg("Service started")
|
||||
default:
|
||||
marker := bytes.Repeat([]byte("="), 32)
|
||||
mainLog.Load().Error().Msg("ctrld service may not have started due to an error or misconfiguration, service log:")
|
||||
_, _ = mainLog.Load().Write(marker)
|
||||
for msg := range runCmdLogCh {
|
||||
_, _ = mainLog.Load().Write([]byte(msg))
|
||||
// If ctrld service is not running, emitting log obtained from ctrld process.
|
||||
if status != service.StatusRunning {
|
||||
mainLog.Load().Error().Msg("ctrld service may not have started due to an error or misconfiguration, service log:")
|
||||
_, _ = mainLog.Load().Write(marker)
|
||||
haveLog := false
|
||||
for msg := range runCmdLogCh {
|
||||
_, _ = mainLog.Load().Write([]byte(msg))
|
||||
haveLog = true
|
||||
}
|
||||
// If we're unable to get log from "ctrld run", notice users about it.
|
||||
if !haveLog {
|
||||
mainLog.Load().Write([]byte(`<no log output is obtained from ctrld process>"`))
|
||||
}
|
||||
}
|
||||
// Report any error if occurred.
|
||||
if err != nil {
|
||||
_, _ = mainLog.Load().Write(marker)
|
||||
msg := fmt.Sprintf("An error occurred while performing test query: %s", err)
|
||||
mainLog.Load().Write([]byte(msg))
|
||||
}
|
||||
// If ctrld service is running but selfCheckStatus failed, it could be related
|
||||
// to user's system firewall configuration, notice users about it.
|
||||
if status == service.StatusRunning {
|
||||
_, _ = mainLog.Load().Write(marker)
|
||||
mainLog.Load().Write([]byte(`ctrld service was running, but a DNS query could not be sent to its listener`))
|
||||
mainLog.Load().Write([]byte(`Please check your system firewall if it is configured to block/intercept/redirect DNS queries`))
|
||||
}
|
||||
|
||||
_, _ = mainLog.Load().Write(marker)
|
||||
uninstall(p, s)
|
||||
os.Exit(1)
|
||||
@@ -364,6 +413,9 @@ func initCLI() {
|
||||
return
|
||||
}
|
||||
initLogging()
|
||||
if err := checkDeactivationPin(s); isCheckDeactivationPinErr(err) {
|
||||
os.Exit(deactivationPinInvalidExitCode)
|
||||
}
|
||||
if doTasks([]task{{s.Stop, true}}) {
|
||||
p.router.Cleanup()
|
||||
p.resetDNS()
|
||||
@@ -372,6 +424,8 @@ func initCLI() {
|
||||
},
|
||||
}
|
||||
stopCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, "auto" means the default interface gateway`)
|
||||
stopCmd.Flags().Int64VarP(&deactivationPin, "pin", "", defaultDeactivationPin, `Pin code for stopping ctrld`)
|
||||
_ = stopCmd.Flags().MarkHidden("pin")
|
||||
|
||||
restartCmd := &cobra.Command{
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
@@ -518,10 +572,15 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
|
||||
if iface == "" {
|
||||
iface = "auto"
|
||||
}
|
||||
if err := checkDeactivationPin(s); isCheckDeactivationPinErr(err) {
|
||||
os.Exit(deactivationPinInvalidExitCode)
|
||||
}
|
||||
uninstall(p, s)
|
||||
},
|
||||
}
|
||||
uninstallCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, use "auto" for the default gateway interface`)
|
||||
uninstallCmd.Flags().Int64VarP(&deactivationPin, "pin", "", defaultDeactivationPin, `Pin code for uninstalling ctrld`)
|
||||
_ = uninstallCmd.Flags().MarkHidden("pin")
|
||||
|
||||
listIfacesCmd := &cobra.Command{
|
||||
Use: "list",
|
||||
@@ -792,6 +851,15 @@ func RunMobile(appConfig *AppConfig, appCallback *AppCallback, stopCh chan struc
|
||||
run(appCallback, stopCh)
|
||||
}
|
||||
|
||||
// CheckDeactivationPin checks if deactivation pin is valid
|
||||
func CheckDeactivationPin(pin int64) int {
|
||||
deactivationPin = pin
|
||||
if err := checkDeactivationPin(nil); isCheckDeactivationPinErr(err) {
|
||||
return deactivationPinInvalidExitCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// run runs ctrld cli with given app callback and stop channel.
|
||||
func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
if stopCh == nil {
|
||||
@@ -1171,6 +1239,18 @@ func processNoConfigFlags(noConfigStart bool) {
|
||||
v.Set("upstream", upstream)
|
||||
}
|
||||
|
||||
// defaultDeactivationPin is the default value for cdDeactivationPin.
|
||||
// If cdDeactivationPin equals to this default, it means the pin code is not set from Control D API.
|
||||
const defaultDeactivationPin = -1
|
||||
|
||||
// cdDeactivationPin is used in cd mode to decide whether stop and uninstall commands can be run.
|
||||
var cdDeactivationPin int64 = defaultDeactivationPin
|
||||
|
||||
// deactivationPinNotSet reports whether cdDeactivationPin was not set by processCDFlags.
|
||||
func deactivationPinNotSet() bool {
|
||||
return cdDeactivationPin == defaultDeactivationPin
|
||||
}
|
||||
|
||||
func processCDFlags(cfg *ctrld.Config) error {
|
||||
logger := mainLog.Load().With().Str("mode", "cd").Logger()
|
||||
logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID)
|
||||
@@ -1195,6 +1275,11 @@ func processCDFlags(cfg *ctrld.Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if resolverConfig.DeactivationPin != nil {
|
||||
logger.Debug().Msg("saving deactivation pin")
|
||||
cdDeactivationPin = *resolverConfig.DeactivationPin
|
||||
}
|
||||
|
||||
logger.Info().Msg("generating ctrld config from Control-D configuration")
|
||||
|
||||
*cfg = ctrld.Config{}
|
||||
@@ -1310,41 +1395,44 @@ func defaultIfaceName() string {
|
||||
return dri
|
||||
}
|
||||
|
||||
func selfCheckStatus(s service.Service) service.Status {
|
||||
// selfCheckStatus performs the end-to-end DNS test by sending query to ctrld listener.
|
||||
// It returns a boolean to indicate whether the check is succeeded, the actual status
|
||||
// of ctrld service, and an additional error if any.
|
||||
func selfCheckStatus(s service.Service) (bool, service.Status, error) {
|
||||
status, err := s.Status()
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not get service status")
|
||||
return status
|
||||
return false, service.StatusUnknown, err
|
||||
}
|
||||
// If ctrld is not running, do nothing, just return the status as-is.
|
||||
if status != service.StatusRunning {
|
||||
return status
|
||||
return false, status, nil
|
||||
}
|
||||
dir, err := socketDir()
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to check ctrld listener status: could not get home directory")
|
||||
return service.StatusUnknown
|
||||
return false, status, err
|
||||
}
|
||||
mainLog.Load().Debug().Msg("waiting for ctrld listener to be ready")
|
||||
cc := newSocketControlClient(s, dir)
|
||||
if cc == nil {
|
||||
return service.StatusUnknown
|
||||
return false, status, errors.New("could not connect to control server")
|
||||
}
|
||||
|
||||
resp, err := cc.post(startedPath, nil)
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to connect to control server")
|
||||
return service.StatusUnknown
|
||||
return false, status, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
mainLog.Load().Error().Msg("ctrld listener is not ready")
|
||||
return service.StatusUnknown
|
||||
return false, status, errors.New("ctrld listener is not ready")
|
||||
}
|
||||
|
||||
// Not a ctrld upstream, return status as-is.
|
||||
if cfg.FirstUpstream().VerifyDomain() == "" {
|
||||
return status
|
||||
return true, status, nil
|
||||
}
|
||||
|
||||
mainLog.Load().Debug().Msg("ctrld listener is ready")
|
||||
@@ -1369,12 +1457,12 @@ func selfCheckStatus(s service.Service) service.Status {
|
||||
domain := cfg.FirstUpstream().VerifyDomain()
|
||||
if domain == "" {
|
||||
// Nothing to do, return the status as-is.
|
||||
return status
|
||||
return true, status, nil
|
||||
}
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("could not watch config change")
|
||||
return service.StatusUnknown
|
||||
return false, status, err
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
@@ -1413,14 +1501,18 @@ func selfCheckStatus(s service.Service) service.Status {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(domain+".", dns.TypeA)
|
||||
m.RecursionDesired = true
|
||||
r, _, err := c.ExchangeContext(ctx, m, net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port)))
|
||||
r, _, exErr := exchangeContextWithTimeout(c, time.Second, m, net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port)))
|
||||
if r != nil && r.Rcode == dns.RcodeSuccess && len(r.Answer) > 0 {
|
||||
mainLog.Load().Debug().Msgf("self-check against %q succeeded", domain)
|
||||
return status
|
||||
return true, status, nil
|
||||
}
|
||||
// Return early if this is a connection refused.
|
||||
if errConnectionRefused(exErr) {
|
||||
return false, status, exErr
|
||||
}
|
||||
lastAnswer = r
|
||||
lastErr = err
|
||||
bo.BackOff(ctx, fmt.Errorf("ExchangeContext: %w", err))
|
||||
lastErr = exErr
|
||||
bo.BackOff(ctx, fmt.Errorf("ExchangeContext: %w", exErr))
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("self-check against %q failed", domain)
|
||||
lc := cfg.FirstListener()
|
||||
@@ -1435,9 +1527,9 @@ func selfCheckStatus(s service.Service) service.Status {
|
||||
for _, s := range strings.Split(lastAnswer.String(), "\n") {
|
||||
mainLog.Load().Debug().Msgf("%s", s)
|
||||
}
|
||||
mainLog.Load().Debug().Msg(marker)
|
||||
return false, status, errSelfCheckNoAnswer
|
||||
}
|
||||
return service.StatusUnknown
|
||||
return false, status, lastErr
|
||||
}
|
||||
|
||||
func userHomeDir() (string, error) {
|
||||
@@ -1664,10 +1756,17 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, infoLogger *zerolog.Logger, fata
|
||||
lcc := make(map[string]*listenerConfigCheck)
|
||||
cdMode := cdUID != ""
|
||||
nextdnsMode := nextdns != ""
|
||||
// For Windows server with local Dns server running, we can only try on random local IP.
|
||||
hasLocalDnsServer := windowsHasLocalDnsServerRunning()
|
||||
for n, listener := range cfg.Listener {
|
||||
lcc[n] = &listenerConfigCheck{}
|
||||
if listener.IP == "" {
|
||||
listener.IP = "0.0.0.0"
|
||||
if hasLocalDnsServer {
|
||||
// Windows Server lies to us that we could listen on 0.0.0.0:53
|
||||
// even there's a process already done that, stick to local IP only.
|
||||
listener.IP = "127.0.0.1"
|
||||
}
|
||||
lcc[n].IP = true
|
||||
}
|
||||
if listener.Port == 0 {
|
||||
@@ -1676,9 +1775,15 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, infoLogger *zerolog.Logger, fata
|
||||
}
|
||||
// In cd mode, we always try to pick an ip:port pair to work.
|
||||
// Same if nextdns resolver is used.
|
||||
//
|
||||
// Except on Windows Server with local Dns running,
|
||||
// we could only listen on random local IP port 53.
|
||||
if cdMode || nextdnsMode {
|
||||
lcc[n].IP = true
|
||||
lcc[n].Port = true
|
||||
if hasLocalDnsServer {
|
||||
lcc[n].Port = false
|
||||
}
|
||||
}
|
||||
updated = updated || lcc[n].IP || lcc[n].Port
|
||||
}
|
||||
@@ -1764,6 +1869,11 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, infoLogger *zerolog.Logger, fata
|
||||
tryAllPort53 := true
|
||||
tryOldIPPort5354 := true
|
||||
tryPort5354 := true
|
||||
if hasLocalDnsServer {
|
||||
tryAllPort53 = false
|
||||
tryOldIPPort5354 = false
|
||||
tryPort5354 = false
|
||||
}
|
||||
attempts := 0
|
||||
maxAttempts := 10
|
||||
for {
|
||||
@@ -2049,3 +2159,103 @@ func noticeWritingControlDConfig() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deactivationPinInvalidExitCode indicates exit code due to invalid pin code.
|
||||
const deactivationPinInvalidExitCode = 126
|
||||
|
||||
// errInvalidDeactivationPin indicates that the deactivation pin is invalid.
|
||||
var errInvalidDeactivationPin = errors.New("deactivation pin is invalid")
|
||||
|
||||
// errRequiredDeactivationPin indicates that the deactivation pin is required but not provided by users.
|
||||
var errRequiredDeactivationPin = errors.New("deactivation pin is required to stop or uninstall the service")
|
||||
|
||||
// checkDeactivationPin validates if the deactivation pin matches one in ControlD config.
|
||||
func checkDeactivationPin(s service.Service) error {
|
||||
dir, err := socketDir()
|
||||
if err != nil {
|
||||
mainLog.Load().Err(err).Msg("could not check deactivation pin")
|
||||
return err
|
||||
}
|
||||
var cc *controlClient
|
||||
if s == nil {
|
||||
cc = newControlClient(filepath.Join(dir, ctrldControlUnixSock))
|
||||
} else {
|
||||
cc = newSocketControlClient(s, dir)
|
||||
}
|
||||
if cc == nil {
|
||||
return nil // ctrld is not running.
|
||||
}
|
||||
data, _ := json.Marshal(&deactivationRequest{Pin: deactivationPin})
|
||||
resp, _ := cc.post(deactivationPath, bytes.NewReader(data))
|
||||
if resp != nil {
|
||||
switch resp.StatusCode {
|
||||
case http.StatusBadRequest:
|
||||
mainLog.Load().Error().Msg(errRequiredDeactivationPin.Error())
|
||||
return errRequiredDeactivationPin // pin is required
|
||||
case http.StatusOK:
|
||||
return nil // valid pin
|
||||
case http.StatusNotFound:
|
||||
return nil // the server is running older version of ctrld
|
||||
}
|
||||
}
|
||||
mainLog.Load().Error().Msg(errInvalidDeactivationPin.Error())
|
||||
return errInvalidDeactivationPin
|
||||
}
|
||||
|
||||
// isCheckDeactivationPinErr reports whether there is an error during check deactivation pin process.
|
||||
func isCheckDeactivationPinErr(err error) bool {
|
||||
return errors.Is(err, errInvalidDeactivationPin) || errors.Is(err, errRequiredDeactivationPin)
|
||||
}
|
||||
|
||||
// ensureUninstall ensures that s.Uninstall will remove ctrld service from system completely.
|
||||
func ensureUninstall(s service.Service) error {
|
||||
maxAttempts := 10
|
||||
var err error
|
||||
for i := 0; i < maxAttempts; i++ {
|
||||
err = s.Uninstall()
|
||||
if _, err := s.Status(); errors.Is(err, service.ErrNotInstalled) {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
return errors.Join(err, errors.New("uninstall failed"))
|
||||
}
|
||||
|
||||
// exchangeContextWithTimeout wraps c.ExchangeContext with the given timeout.
|
||||
func exchangeContextWithTimeout(c *dns.Client, timeout time.Duration, msg *dns.Msg, addr string) (*dns.Msg, time.Duration, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
return c.ExchangeContext(ctx, msg, addr)
|
||||
}
|
||||
|
||||
// powershell runs the given powershell command.
|
||||
func powershell(cmd string) ([]byte, error) {
|
||||
out, err := exec.Command("powershell", "-Command", cmd).CombinedOutput()
|
||||
return bytes.TrimSpace(out), err
|
||||
}
|
||||
|
||||
// windowsHasLocalDnsServerRunning reports whether we are on Windows and having Dns server running.
|
||||
func windowsHasLocalDnsServerRunning() bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
out, _ := powershell("Get-WindowsFeature -Name DNS")
|
||||
if !bytes.Contains(bytes.ToLower(out), []byte("installed")) {
|
||||
return false
|
||||
}
|
||||
|
||||
_, err := powershell("Get-Process -Name DNS")
|
||||
return err == nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// absHomeDir returns the absolute path to given filename using home directory as root dir.
|
||||
func absHomeDir(filename string) string {
|
||||
if homedir != "" {
|
||||
return filepath.Join(homedir, filename)
|
||||
}
|
||||
dir, err := userHomeDir()
|
||||
if err != nil {
|
||||
return filename
|
||||
}
|
||||
return filepath.Join(dir, filename)
|
||||
}
|
||||
|
||||
@@ -27,3 +27,8 @@ func newControlClient(addr string) *controlClient {
|
||||
func (c *controlClient) post(path string, data io.Reader) (*http.Response, error) {
|
||||
return c.c.Post("http://unix"+path, contentTypeJson, data)
|
||||
}
|
||||
|
||||
// deactivationRequest represents request for validating deactivation pin.
|
||||
type deactivationRequest struct {
|
||||
Pin int64 `json:"pin"`
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
contentTypeJson = "application/json"
|
||||
listClientsPath = "/clients"
|
||||
startedPath = "/started"
|
||||
reloadPath = "/reload"
|
||||
contentTypeJson = "application/json"
|
||||
listClientsPath = "/clients"
|
||||
startedPath = "/started"
|
||||
reloadPath = "/reload"
|
||||
deactivationPath = "/deactivation"
|
||||
)
|
||||
|
||||
type controlServer struct {
|
||||
@@ -146,6 +147,30 @@ func (p *prog) registerControlServerHandler() {
|
||||
// Otherwise, reload is done.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
p.cs.register(deactivationPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||
// Non-cd mode or pin code not set, always allowing deactivation.
|
||||
if cdUID == "" || deactivationPinNotSet() {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
var req deactivationRequest
|
||||
if err := json.NewDecoder(request.Body).Decode(&req); err != nil {
|
||||
w.WriteHeader(http.StatusPreconditionFailed)
|
||||
mainLog.Load().Err(err).Msg("invalid deactivation request")
|
||||
return
|
||||
}
|
||||
|
||||
code := http.StatusForbidden
|
||||
switch req.Pin {
|
||||
case cdDeactivationPin:
|
||||
code = http.StatusOK
|
||||
case defaultDeactivationPin:
|
||||
// If the pin code was set, but users do not provide --pin, return proper code to client.
|
||||
code = http.StatusBadRequest
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
}))
|
||||
}
|
||||
|
||||
func jsonResponse(next http.Handler) http.Handler {
|
||||
|
||||
@@ -34,6 +34,7 @@ var (
|
||||
ifaceStartStop string
|
||||
nextdns string
|
||||
cdUpstreamProto string
|
||||
deactivationPin int64
|
||||
|
||||
mainLog atomic.Pointer[zerolog.Logger]
|
||||
consoleWriter zerolog.ConsoleWriter
|
||||
|
||||
34
cmd/cli/net.go
Normal file
34
cmd/cli/net.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package cli
|
||||
|
||||
import "strings"
|
||||
|
||||
// Copied from https://gist.github.com/Ultraporing/fe52981f678be6831f747c206a4861cb
|
||||
|
||||
// Mac Address parts to look for, and identify non-physical devices. There may be more, update me!
|
||||
var macAddrPartsToFilter = []string{
|
||||
"00:03:FF", // Microsoft Hyper-V, Virtual Server, Virtual PC
|
||||
"0A:00:27", // VirtualBox
|
||||
"00:00:00:00:00", // Teredo Tunneling Pseudo-Interface
|
||||
"00:50:56", // VMware ESX 3, Server, Workstation, Player
|
||||
"00:1C:14", // VMware ESX 3, Server, Workstation, Player
|
||||
"00:0C:29", // VMware ESX 3, Server, Workstation, Player
|
||||
"00:05:69", // VMware ESX 3, Server, Workstation, Player
|
||||
"00:1C:42", // Microsoft Hyper-V, Virtual Server, Virtual PC
|
||||
"00:0F:4B", // Virtual Iron 4
|
||||
"00:16:3E", // Red Hat Xen, Oracle VM, XenSource, Novell Xen
|
||||
"08:00:27", // Sun xVM VirtualBox
|
||||
"7A:79", // Hamachi
|
||||
}
|
||||
|
||||
// Filters the possible physical interface address by comparing it to known popular VM Software addresses
|
||||
// and Teredo Tunneling Pseudo-Interface.
|
||||
//
|
||||
//lint:ignore U1000 use in net_windows.go
|
||||
func isPhysicalInterface(addr string) bool {
|
||||
for _, macPart := range macAddrPartsToFilter {
|
||||
if strings.HasPrefix(strings.ToLower(addr), strings.ToLower(macPart)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -42,3 +42,21 @@ func networkServiceName(ifaceName string, r io.Reader) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// validInterface reports whether the *net.Interface is a valid one, which includes:
|
||||
//
|
||||
// - en0: physical wireless
|
||||
// - en1: Thunderbolt 1
|
||||
// - en2: Thunderbolt 2
|
||||
// - en3: Thunderbolt 3
|
||||
// - en4: Thunderbolt 4
|
||||
//
|
||||
// For full list, see: https://unix.stackexchange.com/questions/603506/what-are-these-ifconfig-interfaces-on-macos
|
||||
func validInterface(iface *net.Interface) bool {
|
||||
switch iface.Name {
|
||||
case "en0", "en1", "en2", "en3", "en4":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//go:build !darwin
|
||||
//go:build !darwin && !windows
|
||||
|
||||
package cli
|
||||
|
||||
import "net"
|
||||
|
||||
func patchNetIfaceName(iface *net.Interface) error { return nil }
|
||||
|
||||
func validInterface(iface *net.Interface) bool { return true }
|
||||
|
||||
21
cmd/cli/net_windows.go
Normal file
21
cmd/cli/net_windows.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func patchNetIfaceName(iface *net.Interface) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validInterface reports whether the *net.Interface is a valid one.
|
||||
// On Windows, only physical interfaces are considered valid.
|
||||
func validInterface(iface *net.Interface) bool {
|
||||
if iface == nil {
|
||||
return false
|
||||
}
|
||||
if isPhysicalInterface(iface.HardwareAddr.String()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/exec"
|
||||
|
||||
@@ -34,22 +37,23 @@ func setDNS(iface *net.Interface, nameservers []string) error {
|
||||
cmd := "networksetup"
|
||||
args := []string{"-setdnsservers", iface.Name}
|
||||
args = append(args, nameservers...)
|
||||
|
||||
if err := exec.Command(cmd, args...).Run(); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msgf("setDNS failed, ips = %q", nameservers)
|
||||
return err
|
||||
if out, err := exec.Command(cmd, args...).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("%v: %w", string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(cuonglm): use system API
|
||||
func resetDNS(iface *net.Interface) error {
|
||||
if ns := savedStaticNameservers(iface); len(ns) > 0 {
|
||||
if err := setDNS(iface, ns); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
cmd := "networksetup"
|
||||
args := []string{"-setdnsservers", iface.Name, "empty"}
|
||||
|
||||
if err := exec.Command(cmd, args...).Run(); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msgf("resetDNS failed")
|
||||
return err
|
||||
if out, err := exec.Command(cmd, args...).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("%v: %w", string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -57,3 +61,22 @@ func resetDNS(iface *net.Interface) error {
|
||||
func currentDNS(_ *net.Interface) []string {
|
||||
return resolvconffile.NameServers("")
|
||||
}
|
||||
|
||||
// currentStaticDNS returns the current static DNS settings of given interface.
|
||||
func currentStaticDNS(iface *net.Interface) ([]string, error) {
|
||||
cmd := "networksetup"
|
||||
args := []string{"-getdnsservers", iface.Name}
|
||||
out, err := exec.Command(cmd, args...).Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scanner := bufio.NewScanner(bytes.NewReader(out))
|
||||
var ns []string
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if ip := net.ParseIP(line); ip != nil {
|
||||
ns = append(ns, ip.String())
|
||||
}
|
||||
}
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
@@ -66,3 +66,8 @@ func resetDNS(iface *net.Interface) error {
|
||||
func currentDNS(_ *net.Interface) []string {
|
||||
return resolvconffile.NameServers("")
|
||||
}
|
||||
|
||||
// currentStaticDNS returns the current static DNS settings of given interface.
|
||||
func currentStaticDNS(iface *net.Interface) ([]string, error) {
|
||||
return currentDNS(iface), nil
|
||||
}
|
||||
|
||||
@@ -9,12 +9,10 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6/client6"
|
||||
@@ -25,11 +23,6 @@ import (
|
||||
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
||||
)
|
||||
|
||||
const (
|
||||
resolvConfPath = "/etc/resolv.conf"
|
||||
resolvConfBackupFailedMsg = "open /etc/resolv.pre-ctrld-backup.conf: read-only file system"
|
||||
)
|
||||
|
||||
// allocate loopback ip
|
||||
// sudo ip a add 127.0.0.2/24 dev lo
|
||||
func allocateIP(ip string) error {
|
||||
@@ -69,12 +62,6 @@ func setDNS(iface *net.Interface, nameservers []string) error {
|
||||
Nameservers: ns,
|
||||
SearchDomains: []dnsname.FQDN{},
|
||||
}
|
||||
defer func() {
|
||||
if r.Mode() == "direct" {
|
||||
go watchResolveConf(osConfig)
|
||||
}
|
||||
}()
|
||||
|
||||
trySystemdResolve := false
|
||||
for i := 0; i < maxSetDNSAttempts; i++ {
|
||||
if err := r.SetDNS(osConfig); err != nil {
|
||||
@@ -203,6 +190,11 @@ func currentDNS(iface *net.Interface) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// currentStaticDNS returns the current static DNS settings of given interface.
|
||||
func currentStaticDNS(iface *net.Interface) ([]string, error) {
|
||||
return currentDNS(iface), nil
|
||||
}
|
||||
|
||||
func getDNSByResolvectl(iface string) []string {
|
||||
b, err := exec.Command("resolvectl", "dns", "-i", iface).Output()
|
||||
if err != nil {
|
||||
@@ -309,59 +301,3 @@ func sliceIndex[S ~[]E, E comparable](s S, v E) int {
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// watchResolveConf watches any changes to /etc/resolv.conf file,
|
||||
// and reverting to the original config set by ctrld.
|
||||
func watchResolveConf(oc dns.OSConfig) {
|
||||
mainLog.Load().Debug().Msg("start watching /etc/resolv.conf file")
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not create watcher for /etc/resolv.conf")
|
||||
return
|
||||
}
|
||||
|
||||
// We watch /etc instead of /etc/resolv.conf directly,
|
||||
// see: https://github.com/fsnotify/fsnotify#watching-a-file-doesnt-work-well
|
||||
watchDir := filepath.Dir(resolvConfPath)
|
||||
if err := watcher.Add(watchDir); err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not add /etc/resolv.conf to watcher list")
|
||||
return
|
||||
}
|
||||
|
||||
r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo") // interface name does not matter.
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator")
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.Name != resolvConfPath { // skip if not /etc/resolv.conf changes.
|
||||
continue
|
||||
}
|
||||
if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
|
||||
mainLog.Load().Debug().Msg("/etc/resolv.conf changes detected, reverting to ctrld setting")
|
||||
if err := watcher.Remove(watchDir); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to pause watcher")
|
||||
continue
|
||||
}
|
||||
if err := r.SetDNS(oc); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to revert /etc/resolv.conf changes")
|
||||
}
|
||||
if err := watcher.Add(watchDir); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to continue running watcher")
|
||||
return
|
||||
}
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
mainLog.Load().Err(err).Msg("could not get event for /etc/resolv.conf")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,56 @@ package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
|
||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||
)
|
||||
|
||||
const (
|
||||
forwardersFilename = ".forwarders.txt"
|
||||
v4InterfaceKeyPathFormat = `HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\`
|
||||
v6InterfaceKeyPathFormat = `HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\`
|
||||
)
|
||||
|
||||
var (
|
||||
setDNSOnce sync.Once
|
||||
resetDNSOnce sync.Once
|
||||
)
|
||||
|
||||
func setDNS(iface *net.Interface, nameservers []string) error {
|
||||
if len(nameservers) == 0 {
|
||||
return errors.New("empty DNS nameservers")
|
||||
}
|
||||
setDNSOnce.Do(func() {
|
||||
// If there's a Dns server running, that means we are on AD with Dns feature enabled.
|
||||
// Configuring the Dns server to forward queries to ctrld instead.
|
||||
if windowsHasLocalDnsServerRunning() {
|
||||
file := absHomeDir(forwardersFilename)
|
||||
if data, _ := os.ReadFile(file); len(data) > 0 {
|
||||
if err := removeDnsServerForwarders(strings.Split(string(data), ",")); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("could not remove current forwarders settings")
|
||||
} else {
|
||||
mainLog.Load().Debug().Msg("removed current forwarders settings.")
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(file, []byte(strings.Join(nameservers, ",")), 0600); err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not save forwarders settings")
|
||||
}
|
||||
if err := addDnsServerForwarders(nameservers); err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not set forwarders settings")
|
||||
}
|
||||
}
|
||||
})
|
||||
primaryDNS := nameservers[0]
|
||||
if err := setPrimaryDNS(iface, primaryDNS); err != nil {
|
||||
if err := setPrimaryDNS(iface, primaryDNS, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(nameservers) > 1 {
|
||||
@@ -28,20 +63,64 @@ func setDNS(iface *net.Interface, nameservers []string) error {
|
||||
|
||||
// TODO(cuonglm): should we use system API?
|
||||
func resetDNS(iface *net.Interface) error {
|
||||
resetDNSOnce.Do(func() {
|
||||
// See corresponding comment in setDNS.
|
||||
if windowsHasLocalDnsServerRunning() {
|
||||
file := absHomeDir(forwardersFilename)
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("could not read forwarders settings")
|
||||
return
|
||||
}
|
||||
nameservers := strings.Split(string(content), ",")
|
||||
if err := removeDnsServerForwarders(nameservers); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("could not remove forwarders settings")
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Restoring ipv6 first.
|
||||
if ctrldnet.SupportsIPv6ListenLocal() {
|
||||
if output, err := netsh("interface", "ipv6", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp"); err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msgf("failed to reset ipv6 DNS: %s", string(output))
|
||||
}
|
||||
}
|
||||
// Restoring ipv4 DHCP.
|
||||
output, err := netsh("interface", "ipv4", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp")
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msgf("failed to reset ipv4 DNS: %s", string(output))
|
||||
return err
|
||||
return fmt.Errorf("%s: %w", string(output), err)
|
||||
}
|
||||
// If there's static DNS saved, restoring it.
|
||||
if nss := savedStaticNameservers(iface); len(nss) > 0 {
|
||||
v4ns := make([]string, 0, 2)
|
||||
v6ns := make([]string, 0, 2)
|
||||
for _, ns := range nss {
|
||||
if ctrldnet.IsIPv6(ns) {
|
||||
v6ns = append(v6ns, ns)
|
||||
} else {
|
||||
v4ns = append(v4ns, ns)
|
||||
}
|
||||
}
|
||||
|
||||
for _, ns := range [][]string{v4ns, v6ns} {
|
||||
if len(ns) == 0 {
|
||||
continue
|
||||
}
|
||||
primaryDNS := ns[0]
|
||||
if err := setPrimaryDNS(iface, primaryDNS, false); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ns) > 1 {
|
||||
secondaryDNS := ns[1]
|
||||
_ = addSecondaryDNS(iface, secondaryDNS)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setPrimaryDNS(iface *net.Interface, dns string) error {
|
||||
func setPrimaryDNS(iface *net.Interface, dns string, disablev6 bool) error {
|
||||
ipVer := "ipv4"
|
||||
if ctrldnet.IsIPv6(dns) {
|
||||
ipVer = "ipv6"
|
||||
@@ -52,7 +131,7 @@ func setPrimaryDNS(iface *net.Interface, dns string) error {
|
||||
mainLog.Load().Error().Err(err).Msgf("failed to set primary DNS: %s", string(output))
|
||||
return err
|
||||
}
|
||||
if ipVer == "ipv4" && ctrldnet.SupportsIPv6ListenLocal() {
|
||||
if disablev6 && ipVer == "ipv4" && ctrldnet.SupportsIPv6ListenLocal() {
|
||||
// Disable IPv6 DNS, so the query will be fallback to IPv4.
|
||||
_, _ = netsh("interface", "ipv6", "set", "dnsserver", idx, "static", "::1", "primary")
|
||||
}
|
||||
@@ -93,3 +172,54 @@ func currentDNS(iface *net.Interface) []string {
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
// currentStaticDNS returns the current static DNS settings of given interface.
|
||||
func currentStaticDNS(iface *net.Interface) ([]string, error) {
|
||||
luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
guid, err := luid.GUID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ns []string
|
||||
for _, path := range []string{v4InterfaceKeyPathFormat, v6InterfaceKeyPathFormat} {
|
||||
interfaceKeyPath := path + guid.String()
|
||||
found := false
|
||||
for _, key := range []string{"NameServer", "ProfileNameServer"} {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
cmd := fmt.Sprintf(`Get-ItemPropertyValue -Path "%s" -Name "%s"`, interfaceKeyPath, key)
|
||||
out, err := powershell(cmd)
|
||||
if err == nil && len(out) > 0 {
|
||||
found = true
|
||||
ns = append(ns, strings.Split(string(out), ",")...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
// addDnsServerForwarders adds given nameservers to DNS server forwarders list.
|
||||
func addDnsServerForwarders(nameservers []string) error {
|
||||
for _, ns := range nameservers {
|
||||
cmd := fmt.Sprintf("Add-DnsServerForwarder -IPAddress %s", ns)
|
||||
if out, err := powershell(cmd); err != nil {
|
||||
return fmt.Errorf("%w: %s", err, string(out))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeDnsServerForwarders removes given nameservers from DNS server forwarders list.
|
||||
func removeDnsServerForwarders(nameservers []string) error {
|
||||
for _, ns := range nameservers {
|
||||
cmd := fmt.Sprintf("Remove-DnsServerForwarder -IPAddress %s -Force", ns)
|
||||
if out, err := powershell(cmd); err != nil {
|
||||
return fmt.Errorf("%w: %s", err, string(out))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
119
cmd/cli/prog.go
119
cmd/cli/prog.go
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/netip"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
@@ -417,8 +419,14 @@ func (p *prog) setDNS() {
|
||||
if iface == "" {
|
||||
return
|
||||
}
|
||||
// allIfaces tracks whether we should set DNS for all physical interfaces.
|
||||
allIfaces := false
|
||||
if iface == "auto" {
|
||||
iface = defaultIfaceName()
|
||||
// If iface is "auto", it means user does not specify "--iface" flag.
|
||||
// In this case, ctrld has to set DNS for all physical interfaces, so
|
||||
// thing will still work when user switch from one to the other.
|
||||
allIfaces = requiredMultiNICsConfig()
|
||||
}
|
||||
lc := cfg.FirstListener()
|
||||
if lc == nil {
|
||||
@@ -460,14 +468,29 @@ func (p *prog) setDNS() {
|
||||
return
|
||||
}
|
||||
logger.Debug().Msg("setting DNS successfully")
|
||||
if shouldWatchResolvconf() {
|
||||
servers := make([]netip.Addr, len(nameservers))
|
||||
for i := range nameservers {
|
||||
servers[i] = netip.MustParseAddr(nameservers[i])
|
||||
}
|
||||
go watchResolvConf(netIface, servers, setResolvConf)
|
||||
}
|
||||
if allIfaces {
|
||||
withEachPhysicalInterfaces(netIface.Name, "set DNS", func(i *net.Interface) error {
|
||||
return setDNS(i, nameservers)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (p *prog) resetDNS() {
|
||||
if iface == "" {
|
||||
return
|
||||
}
|
||||
allIfaces := false
|
||||
if iface == "auto" {
|
||||
iface = defaultIfaceName()
|
||||
// See corresponding comments in (*prog).setDNS function.
|
||||
allIfaces = requiredMultiNICsConfig()
|
||||
}
|
||||
logger := mainLog.Load().With().Str("iface", iface).Logger()
|
||||
netIface, err := netInterface(iface)
|
||||
@@ -485,6 +508,9 @@ func (p *prog) resetDNS() {
|
||||
return
|
||||
}
|
||||
logger.Debug().Msg("Restoring DNS successfully")
|
||||
if allIfaces {
|
||||
withEachPhysicalInterfaces(netIface.Name, "reset DNS", resetDNS)
|
||||
}
|
||||
}
|
||||
|
||||
func randomLocalIP() string {
|
||||
@@ -568,6 +594,15 @@ func errNetworkError(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// errConnectionRefused reports whether err is connection refused.
|
||||
func errConnectionRefused(err error) bool {
|
||||
var opErr *net.OpError
|
||||
if !errors.As(err, &opErr) {
|
||||
return false
|
||||
}
|
||||
return errors.Is(opErr.Err, syscall.ECONNREFUSED) || errors.Is(opErr.Err, windowsECONNREFUSED)
|
||||
}
|
||||
|
||||
func ifaceFirstPrivateIP(iface *net.Interface) string {
|
||||
if iface == nil {
|
||||
return ""
|
||||
@@ -649,3 +684,87 @@ func canBeLocalUpstream(addr string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// withEachPhysicalInterfaces runs the function f with each physical interfaces, excluding
|
||||
// the interface that matches excludeIfaceName. The context is used to clarify the
|
||||
// log message when error happens.
|
||||
func withEachPhysicalInterfaces(excludeIfaceName, context string, f func(i *net.Interface) error) {
|
||||
interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
||||
// Skip loopback/virtual interface.
|
||||
if i.IsLoopback() || len(i.HardwareAddr) == 0 {
|
||||
return
|
||||
}
|
||||
// Skip invalid interface.
|
||||
if !validInterface(i.Interface) {
|
||||
return
|
||||
}
|
||||
netIface := i.Interface
|
||||
if err := patchNetIfaceName(netIface); err != nil {
|
||||
mainLog.Load().Debug().Err(err).Msg("failed to patch net interface name")
|
||||
return
|
||||
}
|
||||
// Skip excluded interface.
|
||||
if netIface.Name == excludeIfaceName {
|
||||
return
|
||||
}
|
||||
// TODO: investigate whether we should report this error?
|
||||
if err := f(netIface); err == nil {
|
||||
mainLog.Load().Debug().Msgf("%s for interface %q successfully", context, i.Name)
|
||||
} else if !errors.Is(err, errSaveCurrentStaticDNSNotSupported) {
|
||||
mainLog.Load().Err(err).Msgf("%s for interface %q failed", context, i.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// requiredMultiNicConfig reports whether ctrld needs to set/reset DNS for multiple NICs.
|
||||
func requiredMultiNICsConfig() bool {
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var errSaveCurrentStaticDNSNotSupported = errors.New("saving current DNS is not supported on this platform")
|
||||
|
||||
// saveCurrentStaticDNS saves the current static DNS settings for restoring later.
|
||||
// Only works on Windows and Mac.
|
||||
func saveCurrentStaticDNS(iface *net.Interface) error {
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin":
|
||||
default:
|
||||
return errSaveCurrentStaticDNSNotSupported
|
||||
}
|
||||
file := savedStaticDnsSettingsFilePath(iface)
|
||||
ns, _ := currentStaticDNS(iface)
|
||||
if len(ns) == 0 {
|
||||
_ = os.Remove(file) // removing old static DNS settings
|
||||
return nil
|
||||
}
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not remove old static DNS settings file")
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("DNS settings for %s is static, saving ...", iface.Name)
|
||||
if err := os.WriteFile(file, []byte(strings.Join(ns, ",")), 0600); err != nil {
|
||||
mainLog.Load().Err(err).Msgf("could not save DNS settings for iface: %s", iface.Name)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// savedStaticDnsSettingsFilePath returns the path to saved DNS settings of the given interface.
|
||||
func savedStaticDnsSettingsFilePath(iface *net.Interface) string {
|
||||
return absHomeDir(".dns_" + iface.Name)
|
||||
}
|
||||
|
||||
// savedStaticNameservers returns the static DNS nameservers of the given interface.
|
||||
//
|
||||
//lint:ignore U1000 use in os_windows.go and os_darwin.go
|
||||
func savedStaticNameservers(iface *net.Interface) []string {
|
||||
file := savedStaticDnsSettingsFilePath(iface)
|
||||
if data, _ := os.ReadFile(file); len(data) > 0 {
|
||||
return strings.Split(string(data), ",")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
65
cmd/cli/resolvconf.go
Normal file
65
cmd/cli/resolvconf.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
const (
|
||||
resolvConfPath = "/etc/resolv.conf"
|
||||
resolvConfBackupFailedMsg = "open /etc/resolv.pre-ctrld-backup.conf: read-only file system"
|
||||
)
|
||||
|
||||
// watchResolvConf watches any changes to /etc/resolv.conf file,
|
||||
// and reverting to the original config set by ctrld.
|
||||
func watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn func(iface *net.Interface, ns []netip.Addr) error) {
|
||||
mainLog.Load().Debug().Msg("start watching /etc/resolv.conf file")
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not create watcher for /etc/resolv.conf")
|
||||
return
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
// We watch /etc instead of /etc/resolv.conf directly,
|
||||
// see: https://github.com/fsnotify/fsnotify#watching-a-file-doesnt-work-well
|
||||
watchDir := filepath.Dir(resolvConfPath)
|
||||
if err := watcher.Add(watchDir); err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not add /etc/resolv.conf to watcher list")
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.Name != resolvConfPath { // skip if not /etc/resolv.conf changes.
|
||||
continue
|
||||
}
|
||||
if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
|
||||
mainLog.Load().Debug().Msg("/etc/resolv.conf changes detected, reverting to ctrld setting")
|
||||
if err := watcher.Remove(watchDir); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to pause watcher")
|
||||
continue
|
||||
}
|
||||
if err := setDnsFn(iface, ns); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to revert /etc/resolv.conf changes")
|
||||
}
|
||||
if err := watcher.Add(watchDir); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to continue running watcher")
|
||||
return
|
||||
}
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
mainLog.Load().Err(err).Msg("could not get event for /etc/resolv.conf")
|
||||
}
|
||||
}
|
||||
}
|
||||
20
cmd/cli/resolvconf_darwin.go
Normal file
20
cmd/cli/resolvconf_darwin.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// setResolvConf sets the content of resolv.conf file using the given nameservers list.
|
||||
func setResolvConf(iface *net.Interface, ns []netip.Addr) error {
|
||||
servers := make([]string, len(ns))
|
||||
for i := range ns {
|
||||
servers[i] = ns[i].String()
|
||||
}
|
||||
return setDNS(iface, servers)
|
||||
}
|
||||
|
||||
// shouldWatchResolvconf reports whether ctrld should watch changes to resolv.conf file with given OS configurator.
|
||||
func shouldWatchResolvconf() bool {
|
||||
return true
|
||||
}
|
||||
40
cmd/cli/resolvconf_not_darwin_unix.go
Normal file
40
cmd/cli/resolvconf_not_darwin_unix.go
Normal file
@@ -0,0 +1,40 @@
|
||||
//go:build unix && !darwin
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/util/dnsname"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/dns"
|
||||
)
|
||||
|
||||
// setResolvConf sets the content of resolv.conf file using the given nameservers list.
|
||||
func setResolvConf(iface *net.Interface, ns []netip.Addr) error {
|
||||
r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo") // interface name does not matter.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oc := dns.OSConfig{
|
||||
Nameservers: ns,
|
||||
SearchDomains: []dnsname.FQDN{},
|
||||
}
|
||||
return r.SetDNS(oc)
|
||||
}
|
||||
|
||||
// shouldWatchResolvconf reports whether ctrld should watch changes to resolv.conf file with given OS configurator.
|
||||
func shouldWatchResolvconf() bool {
|
||||
r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo") // interface name does not matter.
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
switch r.Mode() {
|
||||
case "direct", "resolvconf":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
16
cmd/cli/resolvconf_windows.go
Normal file
16
cmd/cli/resolvconf_windows.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// setResolvConf sets the content of resolv.conf file using the given nameservers list.
|
||||
func setResolvConf(_ *net.Interface, _ []netip.Addr) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldWatchResolvconf reports whether ctrld should watch changes to resolv.conf file with given OS configurator.
|
||||
func shouldWatchResolvconf() bool {
|
||||
return false
|
||||
}
|
||||
20
cmd/cli/winres/winres.json
Normal file
20
cmd/cli/winres/winres.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"RT_VERSION": {
|
||||
"#1": {
|
||||
"0000": {
|
||||
"fixed": {
|
||||
"file_version": "0.0.0.1"
|
||||
},
|
||||
"info": {
|
||||
"0409": {
|
||||
"CompanyName": "ControlD Inc",
|
||||
"FileDescription": "Control D DNS daemon",
|
||||
"ProductName": "ctrld",
|
||||
"InternalName": "ctrld",
|
||||
"LegalCopyright": "ControlD Inc 2024"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
4
cmd/cli/winres_windows.go
Normal file
4
cmd/cli/winres_windows.go
Normal file
@@ -0,0 +1,4 @@
|
||||
//go:generate go-winres make --product-version=git-tag --file-version=git-tag
|
||||
package cli
|
||||
|
||||
// Placeholder file for windows builds.
|
||||
@@ -61,13 +61,13 @@ func mapCallback(callback AppCallback) cli.AppCallback {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) Stop() bool {
|
||||
if c.stopCh != nil {
|
||||
func (c *Controller) Stop(Pin int64) int {
|
||||
errorCode := cli.CheckDeactivationPin(Pin)
|
||||
if errorCode == 0 && c.stopCh != nil {
|
||||
close(c.stopCh)
|
||||
c.stopCh = nil
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return errorCode
|
||||
}
|
||||
|
||||
func (c *Controller) IsRunning() bool {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//go:build !qf
|
||||
|
||||
package ctrld
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build qf
|
||||
|
||||
package ctrld
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (uc *UpstreamConfig) setupDOH3Transport() {}
|
||||
|
||||
func (uc *UpstreamConfig) doh3Transport(dnsType uint16) http.RoundTripper { return nil }
|
||||
@@ -224,11 +224,8 @@ DHCP leases file format.
|
||||
- Default: ""
|
||||
|
||||
### client_id_preference
|
||||
Decide how the client ID is generated
|
||||
Decide how the client ID is generated. By default client ID will use both MAC address and Hostname i.e. `hash(mac + host)`. To override this behavior, select one of the 2 allowed values to scope client ID to just MAC address OR Hostname.
|
||||
|
||||
If `host` -> client id will only use the hostname i.e.`hash(hostname)`.
|
||||
If `mac` -> client id will only use the MAC address `hash(mac)`.
|
||||
Else -> client ID will use both Mac and Hostname i.e. `hash(mac + host)
|
||||
- Type: string
|
||||
- Required: no
|
||||
- Valid values: `mac`, `host`
|
||||
@@ -242,7 +239,7 @@ If set to `true`, collect and export the query counters, and show them in `clien
|
||||
- Default: false
|
||||
|
||||
### metrics_listener
|
||||
Specifying the `ip` and `port` of the metrics server.
|
||||
Specifying the `ip` and `port` of the Prometheus metrics server. The Prometheus metrics will be available on: `http://ip:port/metrics`. You can also append `/metrics/json` to get the same data in json format.
|
||||
|
||||
- Type: string
|
||||
- Required: no
|
||||
|
||||
11
go.mod
11
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/Control-D-Inc/ctrld
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
@@ -20,8 +20,9 @@ require (
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pelletier/go-toml/v2 v2.0.8
|
||||
github.com/prometheus/client_golang v1.15.1
|
||||
github.com/prometheus/client_model v0.4.0
|
||||
github.com/prometheus/prom2json v1.3.3
|
||||
github.com/quic-go/quic-go v0.38.0
|
||||
github.com/quic-go/quic-go v0.41.0
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
@@ -43,7 +44,6 @@ require (
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
@@ -66,11 +66,9 @@ require (
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.3.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
@@ -79,10 +77,11 @@ require (
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
go.uber.org/mock v0.3.0 // indirect
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/mod v0.11.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/tools v0.9.1 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
|
||||
25
go.sum
25
go.sum
@@ -51,6 +51,7 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cilium/ebpf v0.10.0 h1:nk5HPMeoBXtOzbkZBWym+ZWq1GIiHUsBFXxwewXAHLQ=
|
||||
github.com/cilium/ebpf v0.10.0/go.mod h1:DPiVdY/kT534dgc9ERmvP8mWA+9gvwgKfRvk4nNWnoE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
@@ -78,6 +79,7 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
@@ -102,8 +104,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -162,6 +162,7 @@ github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
|
||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio=
|
||||
@@ -228,6 +229,7 @@ github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
@@ -251,10 +253,8 @@ github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcET
|
||||
github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.2 h1:rRgN3WfnKbyik4dBV8A6girlJVxGand/d+jVKbQq5GI=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.2/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.38.0 h1:T45lASr5q/TrVwt+jrVccmqHhPL2XuSyoCLVCpfOSLc=
|
||||
github.com/quic-go/quic-go v0.38.0/go.mod h1:MPCuRq7KBK2hNcfKj/1iD1BGuN3eAYMeNxp3T42LRUg=
|
||||
github.com/quic-go/quic-go v0.41.0 h1:aD8MmHfgqTURWNJy48IYFg2OnxwHT3JL7ahGs73lb4k=
|
||||
github.com/quic-go/quic-go v0.41.0/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -304,13 +304,14 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
|
||||
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -358,9 +359,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -393,7 +393,6 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
@@ -416,7 +415,6 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -456,10 +454,8 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -532,7 +528,6 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
|
||||
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -384,6 +384,7 @@ func (t *Table) ListClients() []*Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
clientsByMAC := make(map[string]*Client)
|
||||
for ip := range ipMap {
|
||||
c := ipMap[ip]
|
||||
for _, e := range t.lookupMacAll(ip) {
|
||||
@@ -397,6 +398,7 @@ func (t *Table) ListClients() []*Client {
|
||||
for _, e := range t.lookupHostnameAll(ip, c.Mac) {
|
||||
if c.Hostname == "" && e.name != "" {
|
||||
c.Hostname = e.name
|
||||
clientsByMAC[c.Mac] = c
|
||||
}
|
||||
if e.name != "" {
|
||||
c.Source[e.src] = struct{}{}
|
||||
@@ -405,6 +407,11 @@ func (t *Table) ListClients() []*Client {
|
||||
}
|
||||
clients := make([]*Client, 0, len(ipMap))
|
||||
for _, c := range ipMap {
|
||||
// If we found a client with empty hostname, use hostname from
|
||||
// an existed client which has the same MAC address.
|
||||
if cFromMac := clientsByMAC[c.Mac]; cFromMac != nil && c.Hostname == "" {
|
||||
c.Hostname = cFromMac.Hostname
|
||||
}
|
||||
clients = append(clients, c)
|
||||
}
|
||||
return clients
|
||||
|
||||
@@ -44,3 +44,31 @@ func TestTable_LookupRFC1918IPv4(t *testing.T) {
|
||||
t.Fatalf("unexpected result, want: %s, got: %s", rfc1918IPv4, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTable_ListClients(t *testing.T) {
|
||||
mac := "74:56:3c:44:eb:5e"
|
||||
ipv6_1 := "2405:4803:a04b:4190:fbe9:cd14:d522:bbae"
|
||||
ipv6_2 := "2405:4803:a04b:4190:fbe9:cd14:d522:bbab"
|
||||
table := &Table{}
|
||||
|
||||
// NDP init.
|
||||
table.ndp = &ndpDiscover{}
|
||||
table.ndp.mac.Store(ipv6_1, mac)
|
||||
table.ndp.mac.Store(ipv6_2, mac)
|
||||
table.ndp.ip.Store(mac, ipv6_1)
|
||||
table.ndp.ip.Store(mac, ipv6_2)
|
||||
table.ipResolvers = append(table.ipResolvers, table.ndp)
|
||||
table.macResolvers = append(table.macResolvers, table.ndp)
|
||||
|
||||
hostname := "foo"
|
||||
// mdns init.
|
||||
table.mdns = &mdns{}
|
||||
table.mdns.name.Store(ipv6_2, hostname)
|
||||
table.hostnameResolvers = append(table.hostnameResolvers, table.mdns)
|
||||
|
||||
for _, c := range table.ListClients() {
|
||||
if c.Hostname != hostname {
|
||||
t.Fatalf("missing hostname for client: %v", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,9 @@ type ResolverConfig struct {
|
||||
Ctrld struct {
|
||||
CustomConfig string `json:"custom_config"`
|
||||
} `json:"ctrld"`
|
||||
Exclude []string `json:"exclude"`
|
||||
UID string `json:"uid"`
|
||||
Exclude []string `json:"exclude"`
|
||||
UID string `json:"uid"`
|
||||
DeactivationPin *int64 `json:"deactivation_pin,omitempty"`
|
||||
}
|
||||
|
||||
type utilityResponse struct {
|
||||
|
||||
@@ -199,7 +199,7 @@ func distroName() string {
|
||||
return merlin.Name
|
||||
case haveFile("/etc/openwrt_version"):
|
||||
return openwrt.Name
|
||||
case haveDir("/data/unifi"):
|
||||
case isUbios():
|
||||
return ubios.Name
|
||||
case bytes.HasPrefix(unameU(), []byte("synology")):
|
||||
return synology.Name
|
||||
@@ -234,3 +234,14 @@ 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
|
||||
}
|
||||
|
||||
@@ -72,17 +72,9 @@ build() {
|
||||
if [ "$CGO_ENABLED" = "0" ]; then
|
||||
binary=${binary}-nocgo
|
||||
fi
|
||||
GOOS=${goos} GOARCH=${goarch} GOARM=${3} "$go" generate ./...
|
||||
GOOS=${goos} GOARCH=${goarch} GOARM=${3} "$go" build -ldflags="$ldflags" -o "$binary" ./cmd/ctrld
|
||||
compress "$binary"
|
||||
|
||||
if [ -z "${CTRLD_NO_QF}" ]; then
|
||||
binary_qf=${executable_name}-qf-${goos}-${goarch}v${3}
|
||||
if [ "$CGO_ENABLED" = "0" ]; then
|
||||
binary_qf=${binary_qf}-nocgo
|
||||
fi
|
||||
GOOS=${goos} GOARCH=${goarch} GOARM=${3} "$go" build -ldflags="$ldflags" -tags=qf -o "$binary_qf" ./cmd/ctrld
|
||||
compress "$binary_qf"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# GOMIPS is required for linux/mips: https://nileshgr.com/2020/02/16/golang-on-openwrt-mips/
|
||||
@@ -90,17 +82,9 @@ build() {
|
||||
if [ "$CGO_ENABLED" = "0" ]; then
|
||||
binary=${binary}-nocgo
|
||||
fi
|
||||
GOOS=${goos} GOARCH=${goarch} GOMIPS=softfloat "$go" generate ./...
|
||||
GOOS=${goos} GOARCH=${goarch} GOMIPS=softfloat "$go" build -ldflags="$ldflags" -o "$binary" ./cmd/ctrld
|
||||
compress "$binary"
|
||||
|
||||
if [ -z "${CTRLD_NO_QF}" ]; then
|
||||
binary_qf=${executable_name}-qf-${goos}-${goarch}
|
||||
if [ "$CGO_ENABLED" = "0" ]; then
|
||||
binary_qf=${binary_qf}-nocgo
|
||||
fi
|
||||
GOOS=${goos} GOARCH=${goarch} GOMIPS=softfloat "$go" build -ldflags="$ldflags" -tags=qf -o "$binary_qf" ./cmd/ctrld
|
||||
compress "$binary_qf"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
64
scripts/build_lib.sh
Executable file
64
scripts/build_lib.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script is used to locally build Android .aar library and iOS .xcframework from ctrld source using go mobile tool.
|
||||
|
||||
# Requirements:
|
||||
# - Android NDK (version 23+)
|
||||
# - Android SDK (version 33+)
|
||||
# - Xcode 15 + Build tools
|
||||
# - Go 1.21
|
||||
# - Git
|
||||
# usage: $ ./build_lib.sh v1.3.4
|
||||
|
||||
TAG="$1"
|
||||
# Hacky way to replace version info.
|
||||
update_versionInfo() {
|
||||
local file="$1/ctrld/cmd/cli/cli.go"
|
||||
local tag="$2"
|
||||
local commit="$3"
|
||||
awk -v tag="$tag" -v commit="$commit" '
|
||||
BEGIN { version_updated = 0; commit_updated = 0 }
|
||||
/^\tversion/ {
|
||||
sub(/= ".+"/, "= \"" tag "\"");
|
||||
version_updated = 1;
|
||||
}
|
||||
/^\tcommit/ {
|
||||
sub(/= ".+"/, "= \"" commit "\"");
|
||||
commit_updated = 1;
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (version_updated == 0) {
|
||||
print "\tversion = \"" tag "\"";
|
||||
}
|
||||
if (commit_updated == 0) {
|
||||
print "\tcommit = \"" commit "\"";
|
||||
}
|
||||
}
|
||||
' "$file" > "$file.tmp" && mv "$file.tmp" "$file"
|
||||
}
|
||||
export ANDROID_NDK_HOME=~/Library/Android/sdk/ndk/23.0.7599858
|
||||
mkdir bin
|
||||
cd bin || exit
|
||||
root=$(pwd)
|
||||
# Get source from github and switch to tag
|
||||
git clone --depth 1 --branch "$TAG" https://github.com/Control-D-Inc/ctrld.git
|
||||
# Prepare gomobile tool
|
||||
sourcePath=./ctrld/cmd/ctrld_library
|
||||
cd $sourcePath || exit
|
||||
go mod tidy
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
go get golang.org/x/mobile/bind
|
||||
gomobile init
|
||||
# Prepare build info
|
||||
buildDir=$root/../build
|
||||
mkdir -p "$buildDir"
|
||||
COMMIT=$(git rev-parse HEAD)
|
||||
update_versionInfo "$root" "$TAG" "$COMMIT"
|
||||
ldflags="-s -w"
|
||||
# Build
|
||||
gomobile bind -target ios/arm64 -ldflags="$ldflags" -o "$buildDir"/ctrld-"$TAG".xcframework || exit
|
||||
gomobile bind -ldflags="$ldflags" -o "$buildDir"/ctrld-"$TAG".aar || exit
|
||||
# Clean up
|
||||
rm -r "$root"
|
||||
echo "Successfully built Ctrld library $TAG($COMMIT)."
|
||||
Reference in New Issue
Block a user