Files
ctrld/cmd/ctrld_library/main.go
T
2026-09-28 16:50:26 +07:00

139 lines
3.8 KiB
Go

package ctrld_library
import (
"sync"
"time"
"github.com/Control-D-Inc/ctrld/cmd/cli"
)
// stopTimeout bounds how long Stop waits for ctrld to release its resources.
const stopTimeout = 15 * time.Second
var (
runMobile = cli.RunMobile
checkDeactivationPin = cli.CheckDeactivationPin
)
// Controller holds global state. Start, Stop and IsRunning are safe for
// concurrent use. AppCallback and Config must not be accessed concurrently.
type Controller struct {
mu sync.Mutex
pinMu sync.Mutex // Serializes PIN checks, which use CLI global state.
stopCh chan struct{}
doneCh chan struct{}
AppCallback AppCallback
Config cli.AppConfig
}
// NewController provides reference to global state to be managed by android vpn service and iOS network extension.
func NewController(appCallback AppCallback) *Controller {
return &Controller{AppCallback: appCallback}
}
// AppCallback provides access to app instance.
type AppCallback interface {
Hostname() string
LanIp() string
MacAddress() string
Exit(error string)
}
// Start configures utility with config.toml from provided directory.
// This function blocks until the run exits, including teardown after Stop.
// A call made while a run is active or stopping returns immediately.
// Check port availability prior to calling it.
func (c *Controller) Start(CdUID string, ProvisionID string, CustomHostname string, HomeDir string, UpstreamProto string, logLevel int, logPath string) {
c.mu.Lock()
if c.stopCh != nil {
c.mu.Unlock()
return
}
stopCh, doneCh := make(chan struct{}), make(chan struct{})
c.stopCh, c.doneCh = stopCh, doneCh
c.Config = cli.AppConfig{
CdUID: CdUID,
ProvisionID: ProvisionID,
CustomHostname: CustomHostname,
HomeDir: HomeDir,
UpstreamProto: UpstreamProto,
Verbose: logLevel,
LogPath: logPath,
}
appCallback := mapCallback(c.AppCallback)
c.mu.Unlock()
defer func() {
c.mu.Lock()
defer c.mu.Unlock()
// Only the completed run releases the guard, even if Stop timed out.
close(doneCh)
c.stopCh, c.doneCh = nil, nil
}()
runMobile(&c.Config, &appCallback, stopCh)
}
// As workaround to avoid circular dependency between cli and ctrld_library module
func mapCallback(callback AppCallback) cli.AppCallback {
return cli.AppCallback{
HostName: func() string {
return callback.Hostname()
},
LanIp: func() string {
return callback.LanIp()
},
MacAddress: func() string {
return callback.MacAddress()
},
Exit: func(err string) {
callback.Exit(err)
},
}
}
// Stop requests shutdown after validating pin, unless restart bypasses the check.
// A zero result means the stop request was accepted, not that teardown finished.
// After cancellation it waits up to 15 seconds. Callers must check IsRunning
// after Stop: while it remains true, teardown still owns the session and Start
// will be rejected. PIN failure codes are returned unchanged.
func (c *Controller) Stop(restart bool, pin int64) int {
// Keep this request bound to its original run while PIN validation blocks.
c.mu.Lock()
stopCh, doneCh := c.stopCh, c.doneCh
c.mu.Unlock()
// Force disconnect without checking pin.
// In iOS restart is required if vpn detects no connectivity after network change.
if !restart {
c.pinMu.Lock()
errorCode := checkDeactivationPin(pin, stopCh)
c.pinMu.Unlock()
if errorCode != 0 {
return errorCode
}
}
if stopCh != nil {
c.mu.Lock()
select {
case <-stopCh:
default:
close(stopCh)
}
c.mu.Unlock()
timer := time.NewTimer(stopTimeout)
defer timer.Stop()
select {
case <-doneCh:
case <-timer.C:
}
}
return 0
}
// IsRunning remains true until the run has finished releasing its resources.
func (c *Controller) IsRunning() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.stopCh != nil
}