Add comprehensive documentation to CLI components and core functionality

This commit extends the documentation effort by adding detailed explanatory
comments to key CLI components and core functionality throughout the cmd/
directory. The changes focus on explaining WHY certain logic is needed,
not just WHAT the code does, improving code maintainability and helping
developers understand complex business decisions.

Key improvements:
- Main entry points: Document CLI initialization, logging setup, and cache
  configuration with reasoning for design decisions
- DNS proxy core: Explain DNS proxy constants, data structures, and core
  processing pipeline for handling DNS queries
- Service management: Document service command structure, configuration
  patterns, and platform-specific service handling
- Logging infrastructure: Explain log buffer management, level encoders,
  and log formatting decisions for different use cases
- Metrics and monitoring: Document Prometheus metrics structure, HTTP
  endpoints, and conditional metric collection for performance
- Network handling: Explain Linux-specific network interface filtering,
  virtual interface detection, and DNS configuration management
- Hostname validation: Document RFC1123 compliance and DNS naming
  standards for system compatibility
- Mobile integration: Explain HTTP retry logic, fallback mechanisms, and
  mobile platform integration patterns
- Connection management: Document connection wrapper design to prevent
  log pollution during process lifecycle

Technical details:
- Added explanatory comments to 11 additional files in cmd/cli/
- Maintained consistent documentation style and format
- Preserved all existing functionality while improving code clarity
- Enhanced understanding of complex business logic and platform-specific
  behavior

These comments help future developers understand the reasoning behind
complex decisions, making the codebase more maintainable and reducing
the risk of incorrect modifications during maintenance.
This commit is contained in:
Cuong Manh Le
2025-08-07 15:49:20 +07:00
committed by Cuong Manh Le
parent d88c860cac
commit 4792183c0d
39 changed files with 249 additions and 22 deletions

View File

@@ -67,6 +67,7 @@ var (
var basicModeFlags = []string{"listen", "primary_upstream", "secondary_upstream", "domains"}
// isNoConfigStart checks if the command is using no-config start mode
func isNoConfigStart(cmd *cobra.Command) bool {
for _, flagName := range basicModeFlags {
if cmd.Flags().Lookup(flagName).Changed {
@@ -85,6 +86,7 @@ _/ ___\ __\_ __ \ | / __ |
\/ dns forwarding proxy \/
`
// curVersion returns the current version string
func curVersion() string {
// Ensure version has proper "v" prefix for semantic versioning
// This is needed because some build systems may provide version without the "v" prefix
@@ -429,6 +431,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
<-stopCh
}
// writeConfigFile writes the configuration to a file
func writeConfigFile(cfg *ctrld.Config) error {
if cfu := v.ConfigFileUsed(); cfu != "" {
defaultConfigFile = cfu
@@ -544,6 +547,7 @@ func readBase64Config(configBase64 string) error {
return v.ReadConfig(bytes.NewReader(configStr))
}
// processNoConfigFlags processes flags for no-config mode
func processNoConfigFlags(noConfigStart bool) {
if !noConfigStart {
return
@@ -607,6 +611,7 @@ func deactivationPinSet() bool {
return cdDeactivationPin.Load() != defaultDeactivationPin
}
// processCDFlags processes Control D related flags
func processCDFlags(cfg *ctrld.Config) (*controld.ResolverConfig, error) {
logger := mainLog.Load().With().Str("mode", "cd")
logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID)
@@ -743,6 +748,7 @@ func validateCdRemoteConfig(rc *controld.ResolverConfig, cfg *ctrld.Config) erro
return v.Unmarshal(&cfg)
}
// processListenFlag processes the listen flag
func processListenFlag() {
if listenAddress == "" {
return
@@ -764,6 +770,7 @@ func processListenFlag() {
})
}
// processLogAndCacheFlags processes log and cache related flags
func processLogAndCacheFlags() {
if logPath != "" {
cfg.Service.LogPath = logPath
@@ -779,6 +786,7 @@ func processLogAndCacheFlags() {
v.Set("service", cfg.Service)
}
// netInterface returns the network interface by name
func netInterface(ifaceName string) (*net.Interface, error) {
if ifaceName == "auto" {
ifaceName = defaultIfaceName()
@@ -798,6 +806,7 @@ func netInterface(ifaceName string) (*net.Interface, error) {
return iface, err
}
// defaultIfaceName returns the default interface name
func defaultIfaceName() string {
dri, err := netmon.DefaultRouteInterface()
if err != nil {
@@ -948,6 +957,7 @@ func selfCheckResolveDomain(ctx context.Context, addr, scope string, domain stri
return errSelfCheckNoAnswer
}
// userHomeDir returns the user's home directory
func userHomeDir() (string, error) {
// Mobile platform should provide a rw dir path for this.
if isMobile() {
@@ -1394,6 +1404,7 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, notifyFunc func(), fatal bool) (
return
}
// dirWritable checks if a directory is writable
func dirWritable(dir string) (bool, error) {
f, err := os.CreateTemp(dir, "")
if err != nil {
@@ -1403,6 +1414,7 @@ func dirWritable(dir string) (bool, error) {
return true, f.Close()
}
// osVersion returns the operating system version
func osVersion() string {
oi := osinfo.New()
if runtime.GOOS == "freebsd" {
@@ -1544,6 +1556,7 @@ func checkStrFlagEmpty(cmd *cobra.Command, flagName string) {
}
}
// validateCdUpstreamProtocol validates the Control D upstream protocol
func validateCdUpstreamProtocol() {
if cdUID == "" {
return
@@ -1555,6 +1568,7 @@ func validateCdUpstreamProtocol() {
}
}
// validateCdAndNextDNSFlags validates that Control D and NextDNS flags are not used together
func validateCdAndNextDNSFlags() {
if (cdUID != "" || cdOrg != "") && nextdns != "" {
mainLog.Load().Fatal().Msgf("--%s/--%s could not be used with --%s", cdUidFlagName, cdOrgFlagName, nextdnsFlagName)
@@ -1595,6 +1609,7 @@ func doGenerateNextDNSConfig(uid string) error {
return writeConfigFile(&cfg)
}
// noticeWritingControlDConfig logs on notice level that a Control D config is being written
func noticeWritingControlDConfig() error {
if cdUID != "" {
mainLog.Load().Notice().Msgf("Generating controld config: %s", defaultConfigFile)

View File

@@ -10,6 +10,7 @@ import (
)
// filterEmptyStrings removes empty strings from a slice
// This is used to clean up command line arguments and configuration values
func filterEmptyStrings(slice []string) []string {
var result []string
for _, s := range slice {
@@ -21,17 +22,20 @@ func filterEmptyStrings(slice []string) []string {
}
// ServiceCommand handles service-related operations
// This encapsulates all service management functionality for the CLI
type ServiceCommand struct {
serviceManager *ServiceManager
}
// initializeServiceManager creates a service manager with default configuration
// This sets up the basic service infrastructure needed for all service operations
func (sc *ServiceCommand) initializeServiceManager() (service.Service, *prog, error) {
svcConfig := sc.createServiceConfig()
return sc.initializeServiceManagerWithServiceConfig(svcConfig)
}
// initializeServiceManagerWithServiceConfig creates a service manager with the given configuration
// This allows for custom service configuration while maintaining the same initialization pattern
func (sc *ServiceCommand) initializeServiceManagerWithServiceConfig(svcConfig *service.Config) (service.Service, *prog, error) {
p := &prog{}
@@ -45,6 +49,7 @@ func (sc *ServiceCommand) initializeServiceManagerWithServiceConfig(svcConfig *s
}
// newService creates a new service instance using the provided program and configuration.
// This abstracts the service creation process for different operating systems
func (sc *ServiceCommand) newService(p *prog, svcConfig *service.Config) (service.Service, error) {
s, err := newService(p, svcConfig)
if err != nil {
@@ -54,11 +59,13 @@ func (sc *ServiceCommand) newService(p *prog, svcConfig *service.Config) (servic
}
// NewServiceCommand creates a new service command handler
// This provides a clean factory method for creating service command instances
func NewServiceCommand() *ServiceCommand {
return &ServiceCommand{}
}
// createServiceConfig creates a properly initialized service configuration
// This ensures consistent service naming and description across all platforms
func (sc *ServiceCommand) createServiceConfig() *service.Config {
return &service.Config{
Name: ctrldServiceName,
@@ -69,6 +76,7 @@ func (sc *ServiceCommand) createServiceConfig() *service.Config {
}
// InitServiceCmd creates the service command with proper logic and aliases
// This sets up all service-related subcommands with appropriate permissions and flags
func InitServiceCmd(rootCmd *cobra.Command) *cobra.Command {
// Create service command handlers
sc := NewServiceCommand()

View File

@@ -8,44 +8,60 @@ import (
// logConn wraps a net.Conn, override the Write behavior.
// runCmd uses this wrapper, so as long as startCmd finished,
// ctrld log won't be flushed with un-necessary write errors.
// This prevents log pollution when the parent process closes the connection
type logConn struct {
conn net.Conn
}
// Read delegates to the underlying connection
// This maintains normal read behavior for the wrapped connection
func (lc *logConn) Read(b []byte) (n int, err error) {
return lc.conn.Read(b)
}
// Close delegates to the underlying connection
// This ensures proper cleanup of the wrapped connection
func (lc *logConn) Close() error {
return lc.conn.Close()
}
// LocalAddr delegates to the underlying connection
// This provides access to local address information
func (lc *logConn) LocalAddr() net.Addr {
return lc.conn.LocalAddr()
}
// RemoteAddr delegates to the underlying connection
// This provides access to remote address information
func (lc *logConn) RemoteAddr() net.Addr {
return lc.conn.RemoteAddr()
}
// SetDeadline delegates to the underlying connection
// This maintains timeout functionality for the wrapped connection
func (lc *logConn) SetDeadline(t time.Time) error {
return lc.conn.SetDeadline(t)
}
// SetReadDeadline delegates to the underlying connection
// This maintains read timeout functionality for the wrapped connection
func (lc *logConn) SetReadDeadline(t time.Time) error {
return lc.conn.SetReadDeadline(t)
}
// SetWriteDeadline delegates to the underlying connection
// This maintains write timeout functionality for the wrapped connection
func (lc *logConn) SetWriteDeadline(t time.Time) error {
return lc.conn.SetWriteDeadline(t)
}
// Write performs writes with underlying net.Conn, ignore any errors happen.
// "ctrld run" command use this wrapper to report errors to "ctrld start".
// If no error occurred, "ctrld start" may finish before "ctrld run" attempt
// to close the connection, so ignore errors conservatively here, prevent
// un-necessary error "write to closed connection" flushed to ctrld log.
// This prevents log pollution when the parent process closes the connection prematurely
func (lc *logConn) Write(b []byte) (int, error) {
// Write performs writes with underlying net.Conn, ignore any errors happen.
// "ctrld run" command use this wrapper to report errors to "ctrld start".
// If no error occurred, "ctrld start" may finish before "ctrld run" attempt
// to close the connection, so ignore errors conservatively here, prevent
// un-necessary error "write to closed connection" flushed to ctrld log.
_, _ = lc.conn.Write(b)
return len(b), nil
}

View File

@@ -8,10 +8,12 @@ import (
"time"
)
// controlClient represents an HTTP client for communicating with the control server
type controlClient struct {
c *http.Client
}
// newControlClient creates a new control client with Unix socket transport
func newControlClient(addr string) *controlClient {
return &controlClient{c: &http.Client{
Transport: &http.Transport{

View File

@@ -37,12 +37,14 @@ type ifaceResponse struct {
OK bool `json:"ok"`
}
// controlServer represents an HTTP server for handling control requests
type controlServer struct {
server *http.Server
mux *http.ServeMux
addr string
}
// newControlServer creates a new control server instance
func newControlServer(addr string) (*controlServer, error) {
mux := http.NewServeMux()
s := &controlServer{
@@ -338,6 +340,7 @@ func (p *prog) registerControlServerHandler() {
}))
}
// jsonResponse wraps an HTTP handler to set JSON content type
func jsonResponse(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

View File

@@ -27,24 +27,37 @@ import (
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
)
// DNS proxy constants for configuration and behavior control
const (
// staleTTL is the TTL for stale cache entries
// This allows serving cached responses even when upstreams are temporarily unavailable
staleTTL = 60 * time.Second
// localTTL is the TTL for local network responses
// Longer TTL for local queries reduces unnecessary repeated lookups
localTTL = 3600 * time.Second
// EDNS0_OPTION_MAC is dnsmasq EDNS0 code for adding mac option.
// https://thekelleys.org.uk/gitweb/?p=dnsmasq.git;a=blob;f=src/dns-protocol.h;h=76ac66a8c28317e9c121a74ab5fd0e20f6237dc8;hb=HEAD#l81
// This is also dns.EDNS0LOCALSTART, but define our own constant here for clarification.
// This enables MAC address-based client identification for policy routing
EDNS0_OPTION_MAC = 0xFDE9
// selfUninstallMaxQueries is number of REFUSED queries seen before checking for self-uninstallation.
// This prevents premature self-uninstallation due to temporary network issues
selfUninstallMaxQueries = 32
)
// osUpstreamConfig defines the default OS resolver configuration
// This is used as a fallback when all configured upstreams fail
var osUpstreamConfig = &ctrld.UpstreamConfig{
Name: "OS resolver",
Type: ctrld.ResolverTypeOS,
Timeout: 3000,
}
// privateUpstreamConfig defines the default private resolver configuration
// This is used for internal network queries that should not go to public resolvers
var privateUpstreamConfig = &ctrld.UpstreamConfig{
Name: "Private resolver",
Type: ctrld.ResolverTypePrivate,
@@ -52,6 +65,7 @@ var privateUpstreamConfig = &ctrld.UpstreamConfig{
}
// proxyRequest contains data for proxying a DNS query to upstream.
// This structure encapsulates all the information needed to process a DNS request
type proxyRequest struct {
msg *dns.Msg
ci *ctrld.ClientInfo
@@ -63,6 +77,7 @@ type proxyRequest struct {
}
// proxyResponse contains data for proxying a DNS response from upstream.
// This structure encapsulates the response and metadata for logging and metrics
type proxyResponse struct {
answer *dns.Msg
upstream string
@@ -72,6 +87,7 @@ type proxyResponse struct {
}
// upstreamForResult represents the result of processing rules for a request.
// This contains the matched policy information for logging and debugging
type upstreamForResult struct {
upstreams []string
matchedPolicy string
@@ -81,7 +97,9 @@ type upstreamForResult struct {
srcAddr string
}
func (p *prog) serveDNS(mainCtx context.Context, listenerNum string) error {
// serveDNS sets up and starts a DNS server on the specified listener, handling DNS queries and network monitoring.
// This is the main entry point for DNS server functionality
func (p *prog) serveDNS(ctx context.Context, listenerNum string) error {
listenerConfig := p.cfg.Listener[listenerNum]
if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil {
p.Error().Err(allocErr).Str("ip", listenerConfig.IP).Msg("serveUDP: failed to allocate listen ip")
@@ -92,11 +110,12 @@ func (p *prog) serveDNS(mainCtx context.Context, listenerNum string) error {
p.handleDNSQuery(w, m, listenerNum, listenerConfig)
})
return p.startListeners(mainCtx, listenerConfig, handler)
return p.startListeners(ctx, listenerConfig, handler)
}
// startListeners starts DNS listeners on specified configurations, supporting UDP and TCP protocols.
// It handles local IPv6, RFC 1918, and specified IP listeners, reacting to stop signals or errors.
// This function manages the lifecycle of DNS server listeners
func (p *prog) startListeners(ctx context.Context, cfg *ctrld.ListenerConfig, handler dns.Handler) error {
g, gctx := errgroup.WithContext(ctx)
@@ -153,6 +172,7 @@ func (p *prog) startListeners(ctx context.Context, cfg *ctrld.ListenerConfig, ha
}
// handleDNSQuery processes incoming DNS queries, validates client access, and routes the query to appropriate handlers.
// This is the main entry point for all DNS query processing
func (p *prog) handleDNSQuery(w dns.ResponseWriter, m *dns.Msg, listenerNum string, listenerConfig *ctrld.ListenerConfig) {
p.sema.acquire()
defer p.sema.release()
@@ -191,6 +211,7 @@ func (p *prog) handleDNSQuery(w dns.ResponseWriter, m *dns.Msg, listenerNum stri
}
// handleSpecialDomains processes special domain queries, handles errors, purges cache if necessary, and returns a bool status.
// This handles internal test domains and cache management commands
func (p *prog) handleSpecialDomains(ctx context.Context, w dns.ResponseWriter, m *dns.Msg, domain string) bool {
switch {
case domain == "":
@@ -211,6 +232,7 @@ func (p *prog) handleSpecialDomains(ctx context.Context, w dns.ResponseWriter, m
}
// standardQueryRequest represents a standard DNS query request with associated context and configuration.
// This encapsulates all the data needed to process a standard DNS query
type standardQueryRequest struct {
ctx context.Context
writer dns.ResponseWriter
@@ -221,6 +243,7 @@ type standardQueryRequest struct {
}
// processStandardQuery handles a standard DNS query by routing it through appropriate upstreams and writing a DNS response.
// This is the main processing pipeline for normal DNS queries
func (p *prog) processStandardQuery(req *standardQueryRequest) {
remoteIP, _, _ := net.SplitHostPort(req.writer.RemoteAddr().String())
ci := p.getClientInfo(remoteIP, req.msg)

View File

@@ -4,11 +4,15 @@ import "regexp"
// validHostname reports whether hostname is a valid hostname.
// A valid hostname contains 3 -> 64 characters and conform to RFC1123.
// This function validates hostnames to ensure they meet DNS naming standards
// and prevents invalid hostnames from being used in DNS configurations
func validHostname(hostname string) bool {
hostnameLen := len(hostname)
if hostnameLen < 3 || hostnameLen > 64 {
return false
}
// RFC1123 regex pattern ensures hostnames follow DNS naming conventions
// This prevents issues with DNS resolution and system compatibility
validHostnameRfc1123 := regexp.MustCompile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
return validHostnameRfc1123.MatchString(hostname)
}

View File

@@ -9,6 +9,7 @@ import (
// AppCallback provides hooks for injecting certain functionalities
// from mobile platforms to main ctrld cli.
// This allows mobile applications to customize behavior without modifying core CLI code
type AppCallback struct {
HostName func() string
LanIp func() string
@@ -17,6 +18,7 @@ type AppCallback struct {
}
// AppConfig allows overwriting ctrld cli flags from mobile platforms.
// This provides a clean interface for mobile apps to configure ctrld behavior
type AppConfig struct {
CdUID string
HomeDir string
@@ -25,18 +27,29 @@ type AppConfig struct {
LogPath string
}
// Network and HTTP configuration constants
const (
// defaultHTTPTimeout provides reasonable timeout for HTTP operations
// This prevents hanging requests while allowing sufficient time for network delays
defaultHTTPTimeout = 30 * time.Second
defaultMaxRetries = 3
downloadServerIp = "23.171.240.151"
// defaultMaxRetries provides retry attempts for failed HTTP requests
// This improves reliability in unstable network conditions
defaultMaxRetries = 3
// downloadServerIp is the fallback IP for download operations
// This ensures downloads work even when DNS resolution fails
downloadServerIp = "23.171.240.151"
)
// httpClientWithFallback returns an HTTP client configured with timeout and IPv4 fallback
// This ensures reliable HTTP operations by preferring IPv4 and handling timeouts gracefully
func httpClientWithFallback(timeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
Transport: &http.Transport{
// Prefer IPv4 over IPv6
// This improves compatibility with networks that have IPv6 issues
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
@@ -47,6 +60,7 @@ func httpClientWithFallback(timeout time.Duration) *http.Client {
}
// doWithRetry performs an HTTP request with retries
// This improves reliability by automatically retrying failed requests with exponential backoff
func doWithRetry(req *http.Request, maxRetries int, ip string) (*http.Response, error) {
var lastErr error
client := httpClientWithFallback(defaultHTTPTimeout)
@@ -58,7 +72,8 @@ func doWithRetry(req *http.Request, maxRetries int, ip string) (*http.Response,
}
for attempt := 0; attempt < maxRetries; attempt++ {
if attempt > 0 {
time.Sleep(time.Second * time.Duration(attempt+1)) // Exponential backoff
// Linear backoff reduces server load and improves success rate
time.Sleep(time.Second * time.Duration(attempt+1))
}
resp, err := client.Do(req)
@@ -84,6 +99,7 @@ func doWithRetry(req *http.Request, maxRetries int, ip string) (*http.Response,
}
// Helper for making GET requests with retries
// This provides a simplified interface for common GET operations with built-in retry logic
func getWithRetry(url string, ip string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {

View File

@@ -16,12 +16,30 @@ import (
"github.com/Control-D-Inc/ctrld"
)
// Log writer constants for buffer management and log formatting
const (
// logWriterSize is the default buffer size for log writers
// This provides sufficient space for runtime logs without excessive memory usage
logWriterSize = 1024 * 1024 * 5 // 5 MB
// logWriterSmallSize is used for memory-constrained environments
// This reduces memory footprint while still maintaining log functionality
logWriterSmallSize = 1024 * 1024 * 1 // 1 MB
// logWriterInitialSize is the initial buffer allocation
// This provides immediate space for early log entries
logWriterInitialSize = 32 * 1024 // 32 KB
// logWriterSentInterval controls how often logs are sent to external systems
// This balances real-time logging with system performance
logWriterSentInterval = time.Minute
// logWriterInitEndMarker marks the end of initialization logs
// This helps separate startup logs from runtime logs
logWriterInitEndMarker = "\n\n=== INIT_END ===\n\n"
// logWriterLogEndMarker marks the end of log sections
// This provides clear boundaries for log parsing and analysis
logWriterLogEndMarker = "\n\n=== LOG_END ===\n\n"
)
@@ -31,6 +49,8 @@ const (
// Note: WARN messages will also display as "NOTICE" because they share the same level value.
// This is the intended behavior for visual distinction.
// noticeLevelEncoder provides custom level encoding for NOTICE level
// This ensures NOTICE messages are clearly distinguished from other log levels
func noticeLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
switch l {
case ctrld.NoticeLevel:
@@ -40,6 +60,8 @@ func noticeLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
}
}
// noticeColorLevelEncoder provides colored level encoding for NOTICE level
// This uses cyan color to make NOTICE messages visually distinct in terminal output
func noticeColorLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
switch l {
case ctrld.NoticeLevel:
@@ -49,21 +71,28 @@ func noticeColorLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder)
}
}
// logViewResponse represents the response structure for log viewing requests
// This provides a consistent JSON format for log data retrieval
type logViewResponse struct {
Data string `json:"data"`
}
// logSentResponse represents the response structure for log sending operations
// This includes size information and error details for debugging
type logSentResponse struct {
Size int64 `json:"size"`
Error string `json:"error"`
}
// logReader provides read access to log data with size information
// This encapsulates the log reading functionality for external consumers
type logReader struct {
r io.ReadCloser
size int64
}
// logWriter is an internal buffer to keep track of runtime log when no logging is enabled.
// This provides in-memory log storage for debugging and monitoring purposes
type logWriter struct {
mu sync.Mutex
buf bytes.Buffer
@@ -71,30 +100,37 @@ type logWriter struct {
}
// newLogWriter creates an internal log writer.
// This provides the default log writer with standard buffer size
func newLogWriter() *logWriter {
return newLogWriterWithSize(logWriterSize)
}
// newSmallLogWriter creates an internal log writer with small buffer size.
// This is used in memory-constrained environments or for temporary logging
func newSmallLogWriter() *logWriter {
return newLogWriterWithSize(logWriterSmallSize)
}
// newLogWriterWithSize creates an internal log writer with a given buffer size.
// This allows customization of log buffer size based on specific requirements
func newLogWriterWithSize(size int) *logWriter {
lw := &logWriter{size: size}
return lw
}
// Write implements io.Writer interface for logWriter
// This manages buffer overflow by discarding old data while preserving important markers
func (lw *logWriter) Write(p []byte) (int, error) {
lw.mu.Lock()
defer lw.mu.Unlock()
// If writing p causes overflows, discard old data.
// This prevents unbounded memory growth while maintaining recent logs
if lw.buf.Len()+len(p) > lw.size {
buf := lw.buf.Bytes()
haveEndMarker := false
// If there's init end marker already, preserve the data til the marker.
// This ensures initialization logs are always available for debugging
if idx := bytes.LastIndex(buf, []byte(logWriterInitEndMarker)); idx >= 0 {
buf = buf[:idx+len(logWriterInitEndMarker)]
haveEndMarker = true

View File

@@ -138,7 +138,7 @@ func (p *prog) checkDnsLoopTicker(ctx context.Context) {
}
}
// loopTestMsg generates DNS message for checking loop.
// loopTestMsg creates a DNS test message for loop detection
func loopTestMsg(uid string) *dns.Msg {
msg := new(dns.Msg)
msg.SetQuestion(dns.Fqdn(uid+loopTestDomain), loopTestQtype)

View File

@@ -13,6 +13,8 @@ import (
"github.com/Control-D-Inc/ctrld"
)
// Global variables for CLI configuration and state management
// These are used across multiple commands and need to persist throughout the application lifecycle
var (
configPath string
configBase64 string
@@ -46,6 +48,8 @@ var (
noConfigStart bool
)
// Flag name constants for consistent reference across the codebase
// Using constants prevents typos and makes refactoring easier
const (
cdUidFlagName = "cd"
cdOrgFlagName = "cd-org"
@@ -53,11 +57,15 @@ const (
nextdnsFlagName = "nextdns"
)
// init initializes the default logger before any CLI commands are executed
// This ensures logging is available even during early initialization phases
func init() {
l := zap.NewNop()
mainLog.Store(&ctrld.Logger{Logger: l})
}
// Main is the entry point for the CLI application
// It initializes configuration, sets up the CLI structure, and executes the root command
func Main() {
ctrld.InitConfig(v, "ctrld")
rootCmd := initCLI()
@@ -67,6 +75,8 @@ func Main() {
}
}
// normalizeLogFilePath converts relative log file paths to absolute paths
// This ensures log files are created in predictable locations regardless of working directory
func normalizeLogFilePath(logFilePath string) string {
if logFilePath == "" || filepath.IsAbs(logFilePath) || service.Interactive() {
return logFilePath
@@ -82,18 +92,19 @@ func normalizeLogFilePath(logFilePath string) string {
}
// initConsoleLogging initializes console logging, then storing to mainLog.
// This sets up human-readable logging output for interactive use
func initConsoleLogging() {
consoleWriterLevel = ctrld.NoticeLevel
switch {
case silent:
// For silent mode, use a no-op logger
// For silent mode, use a no-op logger to suppress all output
l := zap.NewNop()
mainLog.Store(&ctrld.Logger{Logger: l})
case verbose == 1:
// Info level
// Info level provides basic operational information
consoleWriterLevel = zapcore.InfoLevel
case verbose > 1:
// Debug level
// Debug level provides detailed diagnostic information
consoleWriterLevel = zapcore.DebugLevel
}
consoleWriter = newHumanReadableZapCore(os.Stdout, consoleWriterLevel)
@@ -105,6 +116,7 @@ func initConsoleLogging() {
// to be used for all interactive commands.
//
// Current log file config will also be ignored.
// This prevents log file conflicts during interactive command execution
func initInteractiveLogging() {
old := cfg.Service.LogPath
cfg.Service.LogPath = ""
@@ -122,19 +134,23 @@ func initLoggingWithBackup(doBackup bool) []zapcore.Core {
var writers []io.Writer
if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" {
// Create parent directory if necessary.
// This ensures log files can be created even if the directory doesn't exist
if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil {
mainLog.Load().Error().Msgf("failed to create log path: %v", err)
os.Exit(1)
}
// Default open log file in append mode.
// This preserves existing log entries across restarts
flags := os.O_CREATE | os.O_RDWR | os.O_APPEND
if doBackup {
// Backup old log file with .1 suffix.
// This prevents log file corruption during rotation
if err := os.Rename(logFilePath, logFilePath+oldLogSuffix); err != nil && !os.IsNotExist(err) {
mainLog.Load().Error().Msgf("could not backup old log file: %v", err)
} else {
// Backup was created, set flags for truncating old log file.
// This ensures a clean start for the new log file
flags = os.O_CREATE | os.O_RDWR
}
}
@@ -147,14 +163,16 @@ func initLoggingWithBackup(doBackup bool) []zapcore.Core {
}
// Create zap cores for different writers
// Multiple cores allow logging to both console and file simultaneously
var cores []zapcore.Core
cores = append(cores, consoleWriter)
// Determine log level
// Determine log level based on verbosity and configuration
// This provides flexible logging control for different use cases
logLevel := cfg.Service.LogLevel
switch {
case silent:
// For silent mode, use a no-op logger
// For silent mode, use a no-op logger to suppress all output
l := zap.NewNop()
mainLog.Store(&ctrld.Logger{Logger: l})
return cores
@@ -164,7 +182,8 @@ func initLoggingWithBackup(doBackup bool) []zapcore.Core {
logLevel = "debug"
}
// Parse log level
// Parse log level string to zapcore.Level
// This provides human-readable log level configuration
var level zapcore.Level
switch logLevel {
case "debug":
@@ -183,12 +202,14 @@ func initLoggingWithBackup(doBackup bool) []zapcore.Core {
consoleWriter.Enabled(level)
// Add cores for all writers
// This enables multi-destination logging (console + file)
for _, writer := range writers {
core := newMachineFriendlyZapCore(writer, level)
cores = append(cores, core)
}
// Create a multi-core logger
// This allows simultaneous logging to multiple destinations
multiCore := zapcore.NewTee(cores...)
logger := zap.New(multiCore)
mainLog.Store(&ctrld.Logger{Logger: logger})
@@ -196,11 +217,14 @@ func initLoggingWithBackup(doBackup bool) []zapcore.Core {
return cores
}
// initCache initializes DNS cache configuration
// This improves performance by caching frequently requested DNS responses
func initCache() {
if !cfg.Service.CacheEnable {
return
}
if cfg.Service.CacheSize == 0 {
// Default cache size provides good balance between memory usage and performance
cfg.Service.CacheSize = 4096
}
}

View File

@@ -15,6 +15,7 @@ import (
)
// metricsServer represents a server to expose Prometheus metrics via HTTP.
// This provides monitoring and observability for the DNS proxy service
type metricsServer struct {
server *http.Server
mux *http.ServeMux
@@ -24,6 +25,7 @@ type metricsServer struct {
}
// newMetricsServer returns new metrics server.
// This initializes the HTTP server for exposing Prometheus metrics
func newMetricsServer(addr string, reg *prometheus.Registry) (*metricsServer, error) {
mux := http.NewServeMux()
ms := &metricsServer{
@@ -37,11 +39,13 @@ func newMetricsServer(addr string, reg *prometheus.Registry) (*metricsServer, er
}
// register adds handlers for given pattern.
// This provides a clean interface for adding HTTP endpoints to the metrics server
func (ms *metricsServer) register(pattern string, handler http.Handler) {
ms.mux.Handle(pattern, handler)
}
// registerMetricsServerHandler adds handlers for metrics server.
// This sets up both Prometheus format and JSON format endpoints for metrics
func (ms *metricsServer) registerMetricsServerHandler() {
ms.register("/metrics", promhttp.HandlerFor(
ms.reg,
@@ -74,6 +78,7 @@ func (ms *metricsServer) registerMetricsServerHandler() {
}
// start runs the metricsServer.
// This starts the HTTP server for metrics exposure
func (ms *metricsServer) start() error {
listener, err := net.Listen("tcp", ms.addr)
if err != nil {
@@ -85,6 +90,7 @@ func (ms *metricsServer) start() error {
}
// stop shutdowns the metricsServer within 2 seconds timeout.
// This ensures graceful shutdown of the metrics server
func (ms *metricsServer) stop() error {
if !ms.started {
return nil
@@ -95,6 +101,7 @@ func (ms *metricsServer) stop() error {
}
// runMetricsServer initializes metrics stats and runs the metrics server if enabled.
// This sets up the complete metrics infrastructure including Prometheus collectors
func (p *prog) runMetricsServer(ctx context.Context, reloadCh chan struct{}) {
if !p.metricsEnabled() {
return

View File

@@ -12,16 +12,20 @@ import (
"github.com/Control-D-Inc/ctrld"
)
// patchNetIfaceName patches network interface names on Linux
// This is a no-op on Linux as interface names don't need special handling
func patchNetIfaceName(iface *net.Interface) (bool, error) { return true, nil }
// validInterface reports whether the *net.Interface is a valid one.
// Only non-virtual interfaces are considered valid.
// This prevents DNS configuration on virtual interfaces like docker, veth, etc.
func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bool {
_, ok := validIfacesMap[iface.Name]
return ok
}
// validInterfacesMap returns a set containing non virtual interfaces.
// This filters out virtual interfaces to ensure DNS is only configured on physical interfaces
func validInterfacesMap(ctx context.Context) map[string]struct{} {
m := make(map[string]struct{})
vis := virtualInterfaces(ctx)
@@ -32,6 +36,7 @@ func validInterfacesMap(ctx context.Context) map[string]struct{} {
m[i.Name] = struct{}{}
})
// Fallback to the default route interface if found nothing.
// This ensures we always have at least one interface to configure
if len(m) == 0 {
defaultRoute, err := netmon.DefaultRoute()
if err != nil {
@@ -43,6 +48,8 @@ func validInterfacesMap(ctx context.Context) map[string]struct{} {
}
// virtualInterfaces returns a map of virtual interfaces on the current machine.
// This reads from /sys/devices/virtual/net to identify virtual network interfaces
// Virtual interfaces should not have DNS configured as they don't represent physical network connections
func virtualInterfaces(ctx context.Context) map[string]struct{} {
logger := ctrld.LoggerFromCtx(ctx)
s := make(map[string]struct{})

View File

@@ -9,8 +9,10 @@ import (
"tailscale.com/net/netmon"
)
// patchNetIfaceName patches network interface names on non-Linux/Darwin platforms
func patchNetIfaceName(iface *net.Interface) (bool, error) { return true, nil }
// validInterface checks if an interface is valid on non-Linux/Darwin platforms
func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bool { return true }
// validInterfacesMap returns a set containing only default route interfaces.

View File

@@ -23,6 +23,7 @@ systemd-resolved=false
var networkManagerCtrldConfFile = filepath.Join(nmConfDir, nmCtrldConfFilename)
// hasNetworkManager reports whether NetworkManager executable found.
// hasNetworkManager checks if NetworkManager is available on the system
func hasNetworkManager() bool {
exe, _ := exec.LookPath("NetworkManager")
return exe != ""

View File

@@ -8,6 +8,7 @@ import (
const nextdnsURL = "https://dns.nextdns.io"
// generateNextDNSConfig generates NextDNS configuration for the given UID
func generateNextDNSConfig(uid string) {
if uid == "" {
return

View File

@@ -11,7 +11,7 @@ import (
"github.com/Control-D-Inc/ctrld"
)
// allocate loopback ip
// allocateIP allocates an IP address on the specified interface
// sudo ifconfig lo0 alias 127.0.0.2 up
func allocateIP(ip string) error {
cmd := exec.Command("ifconfig", "lo0", "alias", ip, "up")
@@ -22,6 +22,7 @@ func allocateIP(ip string) error {
return nil
}
// deAllocateIP deallocates an IP address from the specified interface
func deAllocateIP(ip string) error {
cmd := exec.Command("ifconfig", "lo0", "-alias", ip)
if err := cmd.Run(); err != nil {
@@ -90,6 +91,7 @@ func restoreDNS(iface *net.Interface) (err error) {
return err
}
// currentDNS returns the current DNS servers for the specified interface
func currentDNS(_ *net.Interface) []string {
return ctrld.CurrentNameserversFromResolvconf()
}

View File

@@ -13,7 +13,7 @@ import (
"github.com/Control-D-Inc/ctrld/internal/dns"
)
// allocate loopback ip
// allocateIP allocates an IP address on the specified interface
// sudo ifconfig lo0 127.0.0.53 alias
func allocateIP(ip string) error {
cmd := exec.Command("ifconfig", "lo0", ip, "alias")
@@ -24,6 +24,7 @@ func allocateIP(ip string) error {
return nil
}
// deAllocateIP deallocates an IP address from the specified interface
func deAllocateIP(ip string) error {
cmd := exec.Command("ifconfig", "lo0", ip, "-alias")
if err := cmd.Run(); err != nil {
@@ -73,6 +74,7 @@ func resetDnsIgnoreUnusableInterface(iface *net.Interface) error {
return resetDNS(iface)
}
// resetDNS resets DNS servers for the specified interface
func resetDNS(iface *net.Interface) error {
r, err := dns.NewOSConfigurator(logf, &health.Tracker{}, &controlknobs.Knobs{}, iface.Name)
if err != nil {
@@ -93,6 +95,7 @@ func restoreDNS(iface *net.Interface) (err error) {
return err
}
// currentDNS returns the current DNS servers for the specified interface
func currentDNS(_ *net.Interface) []string {
return ctrld.CurrentNameserversFromResolvconf()
}

View File

@@ -2,12 +2,12 @@
package cli
// TODO(cuonglm): implement.
// allocateIP allocates an IP address on the specified interface
func allocateIP(ip string) error {
return nil
}
// TODO(cuonglm): implement.
// deAllocateIP deallocates an IP address from the specified interface
func deAllocateIP(ip string) error {
return nil
}

View File

@@ -75,6 +75,7 @@ func resetDnsIgnoreUnusableInterface(iface *net.Interface) error {
return resetDNS(iface)
}
// resetDNS resets DNS servers for the specified interface
func resetDNS(iface *net.Interface) error {
luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
if err != nil {
@@ -136,6 +137,7 @@ func restoreDNS(iface *net.Interface) (err error) {
return err
}
// currentDNS returns the current DNS servers for the specified interface
func currentDNS(iface *net.Interface) []string {
luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
if err != nil {

View File

@@ -4,8 +4,10 @@ import (
"github.com/kardianos/service"
)
// setDependencies sets service dependencies for Darwin
func setDependencies(svc *service.Config) {}
// setWorkingDirectory sets the working directory for the service
func setWorkingDirectory(svc *service.Config, dir string) {
svc.WorkingDirectory = dir
}

View File

@@ -6,9 +6,11 @@ import (
"github.com/kardianos/service"
)
// setDependencies sets service dependencies for FreeBSD
func setDependencies(svc *service.Config) {
// TODO(cuonglm): remove once https://github.com/kardianos/service/issues/359 fixed.
_ = os.MkdirAll("/usr/local/etc/rc.d", 0755)
}
// setWorkingDirectory sets the working directory for the service
func setWorkingDirectory(svc *service.Config, dir string) {}

View File

@@ -21,6 +21,7 @@ func init() {
}
}
// setDependencies sets service dependencies for Linux
func setDependencies(svc *service.Config) {
svc.Dependencies = []string{
"Wants=network-online.target",
@@ -37,6 +38,7 @@ func setDependencies(svc *service.Config) {
}
}
// setWorkingDirectory sets the working directory for the service
func setWorkingDirectory(svc *service.Config, dir string) {
svc.WorkingDirectory = dir
}

View File

@@ -4,8 +4,10 @@ package cli
import "github.com/kardianos/service"
// setDependencies sets service dependencies for other platforms
func setDependencies(svc *service.Config) {}
// setWorkingDirectory sets the working directory for the service
func setWorkingDirectory(svc *service.Config, dir string) {
// WorkingDirectory is not supported on Windows.
svc.WorkingDirectory = dir

View File

@@ -2,8 +2,10 @@ package cli
import "github.com/kardianos/service"
// setDependencies sets service dependencies for Windows
func setDependencies(svc *service.Config) {}
// setWorkingDirectory sets the working directory for the service
func setWorkingDirectory(svc *service.Config, dir string) {
// WorkingDirectory is not supported on Windows.
svc.WorkingDirectory = dir

View File

@@ -2,6 +2,8 @@ package cli
import "github.com/prometheus/client_golang/prometheus"
// Prometheus metrics label constants for consistent labeling across all metrics
// These ensure standardized metric labeling for monitoring and alerting
const (
metricsLabelListener = "listener"
metricsLabelClientSourceIP = "client_source_ip"
@@ -13,17 +15,21 @@ const (
)
// statsVersion represent ctrld version.
// This metric provides version information for monitoring and debugging
var statsVersion = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "ctrld_build_info",
Help: "Version of ctrld process.",
}, []string{"gitref", "goversion", "version"})
// statsTimeStart represents start time of ctrld service.
// This metric tracks service uptime and helps with monitoring service restarts
var statsTimeStart = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "ctrld_time_seconds",
Help: "Start time of the ctrld process since unix epoch in seconds.",
})
// statsQueriesCountLabels defines the labels for query count metrics
// These labels provide detailed breakdown of DNS query statistics
var statsQueriesCountLabels = []string{
metricsLabelListener,
metricsLabelClientSourceIP,
@@ -35,6 +41,7 @@ var statsQueriesCountLabels = []string{
}
// statsQueriesCount counts total number of queries.
// This provides comprehensive DNS query statistics for monitoring and alerting
var statsQueriesCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "ctrld_queries_count",
Help: "Total number of queries.",
@@ -44,12 +51,14 @@ var statsQueriesCount = prometheus.NewCounterVec(prometheus.CounterOpts{
//
// The labels "client_source_ip", "client_mac", "client_hostname" are unbounded,
// thus this stat is highly inefficient if there are many devices.
// This metric should be used carefully in high-client environments
var statsClientQueriesCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "ctrld_client_queries_count",
Help: "Total number queries of a client.",
}, []string{metricsLabelClientSourceIP, metricsLabelClientMac, metricsLabelClientHostname})
// WithLabelValuesInc increases prometheus counter by 1 if query stats is enabled.
// This provides conditional metric collection to avoid performance impact when metrics are disabled
func (p *prog) WithLabelValuesInc(c *prometheus.CounterVec, lvs ...string) {
if p.metricsQueryStats.Load() {
c.WithLabelValues(lvs...).Inc()

View File

@@ -8,10 +8,12 @@ import (
"syscall"
)
// notifyReloadSigCh sends reload signal to the channel
func notifyReloadSigCh(ch chan os.Signal) {
signal.Notify(ch, syscall.SIGUSR1)
}
// sendReloadSignal sends a reload signal to the current process
func (p *prog) sendReloadSignal() error {
return syscall.Kill(syscall.Getpid(), syscall.SIGUSR1)
}

View File

@@ -6,8 +6,10 @@ import (
"time"
)
// notifyReloadSigCh is a no-op on Windows platforms
func notifyReloadSigCh(ch chan os.Signal) {}
// sendReloadSignal sends a reload signal to the program
func (p *prog) sendReloadSignal() error {
select {
case p.reloadCh <- struct{}{}:

View File

@@ -13,15 +13,18 @@ import (
// parseResolvConfNameservers reads the resolv.conf file and returns the nameservers found.
// Returns nil if no nameservers are found.
// This function parses the system DNS configuration to understand current nameserver settings
func (p *prog) parseResolvConfNameservers(path string) ([]string, error) {
return resolvconffile.NameserversFromFile(path)
}
// watchResolvConf watches any changes to /etc/resolv.conf file,
// and reverting to the original config set by ctrld.
// This ensures that DNS settings are not overridden by other applications or system processes
func (p *prog) watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn func(iface *net.Interface, ns []netip.Addr) error) {
resolvConfPath := "/etc/resolv.conf"
// Evaluating symbolics link to watch the target file that /etc/resolv.conf point to.
// This handles systems where resolv.conf is a symlink to another location
if rp, _ := filepath.EvalSymlinks(resolvConfPath); rp != "" {
resolvConfPath = rp
}
@@ -35,6 +38,7 @@ func (p *prog) watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn f
// We watch /etc instead of /etc/resolv.conf directly,
// see: https://github.com/fsnotify/fsnotify#watching-a-file-doesnt-work-well
// This is necessary because some systems don't properly notify on file changes
watchDir := filepath.Dir(resolvConfPath)
if err := watcher.Add(watchDir); err != nil {
p.Warn().Err(err).Msgf("could not add %s to watcher list", watchDir)
@@ -62,6 +66,7 @@ func (p *prog) watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn f
p.Debug().Msgf("/etc/resolv.conf changes detected, reading changes...")
// Convert expected nameservers to strings for comparison
// This allows us to detect when the resolv.conf has been modified
expectedNS := make([]string, len(ns))
for i, addr := range ns {
expectedNS[i] = addr.String()
@@ -79,11 +84,13 @@ func (p *prog) watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn f
}
// If we found nameservers, break out of retry loop
// This handles cases where the file is being written but not yet complete
if len(foundNS) > 0 {
break
}
// Only retry if we found no nameservers
// This handles temporary file states during updates
if retry < maxRetries-1 {
p.Debug().Msgf("resolv.conf has no nameserver entries, retry %d/%d in 2 seconds", retry+1, maxRetries)
select {

View File

@@ -4,4 +4,5 @@ package cli
var supportedSelfDelete = true
// selfDeleteExe performs self-deletion on non-Windows platforms
func selfDeleteExe() error { return nil }

View File

@@ -33,6 +33,7 @@ type FILE_DISPOSITION_INFO struct {
DeleteFile bool
}
// dsOpenHandle opens a handle to the specified file with DELETE access
func dsOpenHandle(pwPath *uint16) (windows.Handle, error) {
handle, err := windows.CreateFile(
pwPath,
@@ -51,6 +52,7 @@ func dsOpenHandle(pwPath *uint16) (windows.Handle, error) {
return handle, nil
}
// dsRenameHandle renames a file handle to a stream name
func dsRenameHandle(hHandle windows.Handle) error {
var fRename FILE_RENAME_INFO
DS_STREAM_RENAME, err := windows.UTF16FromString(":deadbeef")
@@ -82,6 +84,7 @@ func dsRenameHandle(hHandle windows.Handle) error {
return nil
}
// dsDepositeHandle marks a file handle for deletion
func dsDepositeHandle(hHandle windows.Handle) error {
var fDelete FILE_DISPOSITION_INFO
fDelete.DeleteFile = true
@@ -100,6 +103,7 @@ func dsDepositeHandle(hHandle windows.Handle) error {
return nil
}
// selfDeleteExe performs self-deletion on Windows platforms
func selfDeleteExe() error {
var wcPath [windows.MAX_PATH + 1]uint16
var hCurrent windows.Handle

View File

@@ -8,6 +8,7 @@ import (
"github.com/Control-D-Inc/ctrld"
)
// selfUninstall performs self-uninstallation on non-Unix platforms
func selfUninstall(p *prog, logger *ctrld.Logger) {
if uninstallInvalidCdUID(p, logger, false) {
logger.Warn().Msgf("service was uninstalled because device %q does not exist", cdUID)

View File

@@ -12,6 +12,7 @@ import (
"github.com/Control-D-Inc/ctrld"
)
// selfUninstall performs self-uninstallation on Unix platforms
func selfUninstall(p *prog, logger *ctrld.Logger) {
if runtime.GOOS == "linux" {
selfUninstallLinux(p, logger)
@@ -37,6 +38,7 @@ func selfUninstall(p *prog, logger *ctrld.Logger) {
os.Exit(0)
}
// selfUninstallLinux performs self-uninstallation on Linux platforms
func selfUninstallLinux(p *prog, logger *ctrld.Logger) {
if uninstallInvalidCdUID(p, logger, true) {
logger.Warn().Msgf("service was uninstalled because device %q does not exist", cdUID)

View File

@@ -1,24 +1,31 @@
package cli
// semaphore provides a simple synchronization mechanism
type semaphore interface {
acquire()
release()
}
// noopSemaphore is a no-operation implementation of semaphore
type noopSemaphore struct{}
// acquire performs a no-operation for the noop semaphore
func (n noopSemaphore) acquire() {}
// release performs a no-operation for the noop semaphore
func (n noopSemaphore) release() {}
// chanSemaphore is a channel-based implementation of semaphore
type chanSemaphore struct {
ready chan struct{}
}
// acquire blocks until a slot is available in the semaphore
func (c *chanSemaphore) acquire() {
c.ready <- struct{}{}
}
// release signals that a slot has been freed in the semaphore
func (c *chanSemaphore) release() {
<-c.ready
}

View File

@@ -149,6 +149,7 @@ func ensureSystemdKillMode(r io.Reader) (opts []*unit.UnitOption, change bool) {
return opts, change
}
// newLaunchd creates a new launchd service wrapper
func newLaunchd(s service.Service) *launchd {
return &launchd{
Service: s,
@@ -178,6 +179,7 @@ type task struct {
Name string
}
// doTasks executes a list of tasks and returns success status
func doTasks(tasks []task) bool {
for _, task := range tasks {
mainLog.Load().Debug().Msgf("Running task %s", task.Name)
@@ -196,6 +198,7 @@ func doTasks(tasks []task) bool {
return true
}
// checkHasElevatedPrivilege checks if the process has elevated privileges and exits if not
func checkHasElevatedPrivilege() {
ok, err := hasElevatedPrivilege()
if err != nil {
@@ -208,6 +211,7 @@ func checkHasElevatedPrivilege() {
}
}
// unixSystemVServiceStatus checks the status of a Unix System V service
func unixSystemVServiceStatus() (service.Status, error) {
out, err := exec.Command("/etc/init.d/ctrld", "status").CombinedOutput()
if err != nil {

View File

@@ -6,12 +6,15 @@ import (
"os"
)
// hasElevatedPrivilege checks if the current process has elevated privileges
func hasElevatedPrivilege() (bool, error) {
return os.Geteuid() == 0, nil
}
// openLogFile opens a log file with the specified flags
func openLogFile(path string, flags int) (*os.File, error) {
return os.OpenFile(path, flags, os.FileMode(0o600))
}
// ConfigureWindowsServiceFailureActions is a no-op on non-Windows platforms
func ConfigureWindowsServiceFailureActions(serviceName string) error { return nil }

View File

@@ -11,6 +11,7 @@ import (
"golang.org/x/sys/windows/svc/mgr"
)
// hasElevatedPrivilege checks if the current process has elevated privileges on Windows
func hasElevatedPrivilege() (bool, error) {
var sid *windows.SID
if err := windows.AllocateAndInitializeSid(
@@ -93,6 +94,7 @@ func ConfigureWindowsServiceFailureActions(serviceName string) error {
return nil
}
// openLogFile opens a log file with the specified mode on Windows
func openLogFile(path string, mode int) (*os.File, error) {
if len(path) == 0 {
return nil, &os.PathError{Path: path, Op: "open", Err: syscall.ERROR_FILE_NOT_FOUND}

View File

@@ -30,6 +30,7 @@ type upstreamMonitor struct {
failureTimerActive map[string]bool
}
// newUpstreamMonitor creates a new upstream monitor instance
func newUpstreamMonitor(cfg *ctrld.Config, logger *ctrld.Logger) *upstreamMonitor {
um := &upstreamMonitor{
cfg: cfg,

View File

@@ -43,7 +43,7 @@ func (c *Controller) Start(CdUID string, HomeDir string, UpstreamProto string, l
}
}
// As workaround to avoid circular dependency between cli and ctrld_library module
// mapCallback maps the AppCallback interface to cli.AppCallback to avoid circular dependency
func mapCallback(callback AppCallback) cli.AppCallback {
return cli.AppCallback{
HostName: func() string {