feat: add persistent APT Target adapter

This commit is contained in:
proxyctl coordinator
2026-07-17 03:26:25 +08:00
parent 3003450dce
commit 43b7537955
2 changed files with 803 additions and 0 deletions
+459
View File
@@ -0,0 +1,459 @@
// Package apt implements the persistent Debian-family APT Target.
package apt
import (
"bufio"
"context"
"errors"
"fmt"
"io/fs"
"net"
"net/url"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"proxyctl/internal/profile"
"proxyctl/internal/securefs"
"proxyctl/internal/target"
)
const (
overlayName = "99proxyctl"
overlayHeader = "// Generated by proxyctl. Do not edit.\n"
)
// Configuration names the Debian marker and the isolated APT configuration
// fragment. TrustedRoot anchors all mutations and is injectable for tests.
type Configuration struct {
DebianVersionFile string
ConfigDirectory string
OverlayFile string
TrustedRoot string
}
func DefaultConfiguration() Configuration {
return Configuration{
DebianVersionFile: "/etc/debian_version",
ConfigDirectory: "/etc/apt/apt.conf.d",
OverlayFile: "/etc/apt/apt.conf.d/" + overlayName,
TrustedRoot: "/",
}
}
// FileSystem is the mutation seam used by the Adapter. Availability probes are
// read-only os.Stat calls, while writes remain descriptor-anchored in securefs.
type FileSystem interface {
ReadFile(string) ([]byte, error)
AtomicWrite(string, []byte, os.FileMode) error
Remove(string) error
}
type OSFileSystem struct{ TrustedRoot string }
func NewOSFileSystem(trustedRoot string) OSFileSystem { return OSFileSystem{TrustedRoot: trustedRoot} }
func (f OSFileSystem) store() securefs.Store { return securefs.Store{TrustedRoot: f.TrustedRoot} }
func (f OSFileSystem) ReadFile(path string) ([]byte, error) {
return f.store().ReadFile(path)
}
func (f OSFileSystem) AtomicWrite(path string, data []byte, mode os.FileMode) error {
return f.store().AtomicWrite(path, data, mode)
}
func (f OSFileSystem) Remove(path string) error { return f.store().Remove(path) }
// Runner isolates apt-config discovery and execution from adapter tests.
type Runner interface {
LookPath(string) (string, error)
Output(context.Context, string, ...string) ([]byte, error)
}
type systemRunner struct{}
func (systemRunner) LookPath(name string) (string, error) { return exec.LookPath(name) }
func (systemRunner) Output(ctx context.Context, name string, args ...string) ([]byte, error) {
return exec.CommandContext(ctx, name, args...).Output()
}
type Adapter struct {
configuration Configuration
files FileSystem
runner Runner
}
func New(configuration Configuration, files FileSystem, runner Runner) *Adapter {
defaults := DefaultConfiguration()
if configuration.DebianVersionFile == "" {
configuration.DebianVersionFile = defaults.DebianVersionFile
}
if configuration.ConfigDirectory == "" {
configuration.ConfigDirectory = defaults.ConfigDirectory
}
if configuration.OverlayFile == "" {
configuration.OverlayFile = filepath.Join(configuration.ConfigDirectory, overlayName)
}
if configuration.TrustedRoot == "" {
configuration.TrustedRoot = defaults.TrustedRoot
}
if files == nil {
files = NewOSFileSystem(configuration.TrustedRoot)
}
if runner == nil {
runner = systemRunner{}
}
return &Adapter{configuration: configuration, files: files, runner: runner}
}
func (a *Adapter) ID() string { return "apt" }
func (a *Adapter) Description() target.Description {
return target.Description{
Name: "APT",
Summary: "Persistently configures APT proxying across Shell and privilege boundaries.",
Details: "APT supports proxy environment variables; this integration uses an isolated persistent configuration overlay so package operations remain reliable across privilege boundaries. Writing /etc/apt normally requires pre-existing root authority; proxyctl never escalates privileges.",
}
}
func (a *Adapter) Availability(ctx context.Context) target.Availability {
marker, err := os.Stat(a.configuration.DebianVersionFile)
if err != nil || !marker.Mode().IsRegular() {
return target.Availability{Evidence: "Debian-family marker /etc/debian_version was not detected"}
}
directory, err := os.Stat(a.configuration.ConfigDirectory)
if err != nil || !directory.IsDir() {
return target.Availability{Evidence: "APT configuration directory apt.conf.d was not detected"}
}
command, err := a.runner.LookPath("apt-config")
if err != nil {
return target.Availability{Evidence: "Debian APT tool apt-config was not detected"}
}
if _, err := a.runner.Output(ctx, command, "dump"); err != nil {
return target.Availability{Evidence: "apt-config could not inspect effective APT configuration"}
}
return target.Availability{Available: true, Evidence: "Debian marker, apt-config, and apt.conf.d configuration capability detected"}
}
func (a *Adapter) OverlayPath() string { return a.configuration.OverlayFile }
func (a *Adapter) Inspect(ctx context.Context, active *profile.Profile) target.Inspection {
inspection, _ := a.inspect(ctx, active)
return inspection
}
func (a *Adapter) inspect(ctx context.Context, active *profile.Profile) (target.Inspection, error) {
overlay, overlayErr := a.files.ReadFile(a.configuration.OverlayFile)
managed := overlayErr == nil && isOwnedOverlay(overlay)
if overlayErr != nil && !errors.Is(overlayErr, fs.ErrNotExist) {
return target.Inspection{State: target.Drifted, Evidence: "managed APT overlay cannot be read"}, fmt.Errorf("read managed APT overlay: %w", overlayErr)
}
effective, dumpErr := a.dump(ctx)
if dumpErr != nil {
if managed {
return target.Inspection{State: target.Drifted, Evidence: "managed APT overlay exists but effective configuration cannot be inspected"}, dumpErr
}
return target.Inspection{State: target.Disabled, Evidence: "effective APT configuration could not be positively inspected"}, dumpErr
}
effectiveValues := parseConfiguration(effective)
if managed {
managedValues := parseConfiguration(overlay)
if len(managedValues) == 0 {
return target.Inspection{State: target.Drifted, Evidence: "managed APT overlay contains no recognized proxy settings"}, nil
}
if active == nil {
return target.Inspection{State: target.Drifted, Evidence: "managed APT overlay exists without an Active Profile"}, nil
}
desired, err := translate(*active)
if err == nil {
coverExistingHostRules(desired.settings, effectiveValues)
}
if err == nil && equalSettings(managedValues, desired.settings) && equalSettings(effectiveValues, desired.settings) {
return target.Inspection{State: target.Enabled, Evidence: "effective managed APT values match the Active Profile"}, nil
}
return target.Inspection{State: target.Drifted, Evidence: "effective managed APT values do not match the Active Profile"}, nil
}
if key, ok := recognizedProxy(effectiveValues); ok {
return target.Inspection{State: target.External, Evidence: "recognized non-managed APT proxy setting " + key + " is effective"}, nil
}
return target.Inspection{State: target.Disabled, Evidence: "no recognized effective APT proxy configuration detected"}, nil
}
func (a *Adapter) SetDesired(ctx context.Context, enabled bool, active *profile.Profile) target.OperationResult {
if !a.Availability(ctx).Available {
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: errors.New("APT Target is unavailable: Debian APT tooling or configuration capability was not detected")}
}
if enabled && active == nil {
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: errors.New("no Active Profile; open Profile management to create and activate one")}
}
var mutationErr error
if enabled {
if existing, err := a.files.ReadFile(a.configuration.OverlayFile); err == nil && !isOwnedOverlay(existing) {
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: errors.New("APT overlay path already contains configuration not owned by proxyctl; no changes made")}
} else if err != nil && !errors.Is(err, fs.ErrNotExist) {
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: redactError(fmt.Errorf("inspect managed APT overlay before writing: %w", err), active)}
}
translated, err := translate(*active)
if err != nil {
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: redactError(err, active)}
}
effective, err := a.dump(ctx)
if err != nil {
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: redactError(fmt.Errorf("inspect effective APT configuration before mutation: %w", err), active)}
}
coverExistingHostRules(translated.settings, parseConfiguration(effective))
translated.rendered = render(translated.settings)
mutationErr = a.files.AtomicWrite(a.configuration.OverlayFile, translated.rendered, 0o600)
if mutationErr != nil {
mutationErr = fmt.Errorf("write managed APT overlay: %w", mutationErr)
}
} else {
existing, err := a.files.ReadFile(a.configuration.OverlayFile)
switch {
case errors.Is(err, fs.ErrNotExist):
case err != nil:
mutationErr = fmt.Errorf("inspect managed APT overlay before removal: %w", err)
case !isOwnedOverlay(existing):
// A path collision is user-owned configuration, not managed state.
default:
if err := a.files.Remove(a.configuration.OverlayFile); err != nil && !errors.Is(err, fs.ErrNotExist) {
mutationErr = fmt.Errorf("remove managed APT overlay: %w", err)
}
}
}
inspection, inspectErr := a.inspect(ctx, active)
if mutationErr == nil && inspectErr != nil {
mutationErr = fmt.Errorf("re-inspect effective APT configuration after mutation: %w", inspectErr)
}
if mutationErr == nil && enabled && inspection.State != target.Enabled {
mutationErr = errors.New("managed APT overlay was written but did not become the effective Active Profile configuration")
}
if mutationErr == nil && !enabled && inspection.State == target.Drifted {
mutationErr = errors.New("managed APT overlay removal did not complete")
}
return target.OperationResult{Inspection: inspection, Err: redactError(mutationErr, active)}
}
func (a *Adapter) dump(ctx context.Context) ([]byte, error) {
command, err := a.runner.LookPath("apt-config")
if err != nil {
return nil, fmt.Errorf("locate apt-config: %w", err)
}
output, err := a.runner.Output(ctx, command, "dump")
if err != nil {
return nil, fmt.Errorf("run apt-config dump: %w", err)
}
return output, nil
}
type translation struct {
settings map[string]string
rendered []byte
}
func translate(input profile.Profile) (translation, error) {
p, err := profile.ValidateAndNormalize(input)
if err != nil {
return translation{}, err
}
httpValue, httpsValue := p.HTTPURL, p.HTTPSURL
if httpValue == "" {
httpValue = p.AllURL
}
if httpsValue == "" {
httpsValue = p.AllURL
}
for _, candidate := range []struct {
field, value string
}{{"HTTP proxy", p.HTTPURL}, {"HTTPS proxy", p.HTTPSURL}, {"ALL/SOCKS proxy", p.AllURL}} {
if candidate.value == "" {
continue
}
parsed, _ := url.Parse(candidate.value)
if parsed.Scheme != "http" && parsed.Scheme != "https" && parsed.Scheme != "socks5h" {
return translation{}, fmt.Errorf("APT cannot translate %s value %q: proxy scheme %q is incompatible", candidate.field, profile.ObscureURL(candidate.value), parsed.Scheme)
}
}
settings := make(map[string]string)
if httpValue != "" {
settings["Acquire::http::Proxy"] = httpValue
}
if httpsValue != "" {
settings["Acquire::https::Proxy"] = httpsValue
}
for _, bypass := range p.Bypass {
if strings.HasPrefix(bypass, ".") || strings.Contains(bypass, "/") || strings.Contains(bypass, ":") {
return translation{}, fmt.Errorf("APT cannot translate Bypass entry %q; only exact hostnames and IPv4 addresses are supported", bypass)
}
if net.ParseIP(bypass) == nil && strings.ContainsAny(bypass, " \t\r\n;\"") {
return translation{}, fmt.Errorf("APT cannot translate Bypass entry %q", bypass)
}
settings["Acquire::http::Proxy::"+bypass] = "DIRECT"
settings["Acquire::https::Proxy::"+bypass] = "DIRECT"
}
return translation{settings: settings, rendered: render(settings)}, nil
}
var settingOrder = []string{"Acquire::http::Proxy", "Acquire::https::Proxy"}
func render(settings map[string]string) []byte {
var output strings.Builder
output.WriteString(overlayHeader)
for _, key := range settingOrder {
if value, ok := settings[key]; ok {
fmt.Fprintf(&output, "%s \"%s\";\n", key, aptQuote(value))
}
}
for _, protocol := range []string{"http", "https"} {
prefix := "Acquire::" + protocol + "::Proxy::"
var keys []string
for key := range settings {
if strings.HasPrefix(key, prefix) {
keys = append(keys, key)
}
}
sort.Strings(keys)
for _, key := range keys {
fmt.Fprintf(&output, "%s \"%s\";\n", key, aptQuote(settings[key]))
}
}
return []byte(output.String())
}
func isOwnedOverlay(data []byte) bool { return strings.HasPrefix(string(data), overlayHeader) }
func aptQuote(value string) string {
return strings.NewReplacer("\\", "\\\\", "\"", "\\\"").Replace(value)
}
func parseConfiguration(data []byte) map[string]string {
settings := make(map[string]string)
scanner := bufio.NewScanner(strings.NewReader(string(data)))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "#") {
continue
}
key, rest, ok := strings.Cut(line, " ")
if !ok {
key, rest, ok = strings.Cut(line, "\t")
}
if !ok || !recognizedKey(key) {
continue
}
rest = strings.TrimSpace(rest)
if len(rest) < 3 || rest[0] != '"' || rest[len(rest)-1] != ';' {
continue
}
quoted := strings.TrimSpace(rest[:len(rest)-1])
if len(quoted) < 2 || quoted[len(quoted)-1] != '"' {
continue
}
settings[key] = aptUnquote(quoted[1 : len(quoted)-1])
}
return settings
}
func recognizedKey(key string) bool {
return key == "Acquire::http::Proxy" || key == "Acquire::https::Proxy" ||
strings.HasPrefix(key, "Acquire::http::Proxy::") || strings.HasPrefix(key, "Acquire::https::Proxy::")
}
func recognizedProxy(settings map[string]string) (string, bool) {
keys := make([]string, 0, len(settings))
for key := range settings {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
value := settings[key]
if value != "" && !strings.EqualFold(value, "DIRECT") && !strings.EqualFold(value, "false") {
return key, true
}
}
return "", false
}
func aptUnquote(value string) string {
var output strings.Builder
escaped := false
for _, r := range value {
if escaped {
output.WriteRune(r)
escaped = false
} else if r == '\\' {
escaped = true
} else {
output.WriteRune(r)
}
}
if escaped {
output.WriteByte('\\')
}
return output.String()
}
func coverExistingHostRules(desired, effective map[string]string) {
for key := range effective {
var protocol, prefix string
switch {
case strings.HasPrefix(key, "Acquire::http::Proxy::"):
protocol, prefix = "http", "Acquire::http::Proxy::"
case strings.HasPrefix(key, "Acquire::https::Proxy::"):
protocol, prefix = "https", "Acquire::https::Proxy::"
default:
continue
}
if _, alreadyCovered := desired[key]; alreadyCovered {
continue
}
value := desired["Acquire::"+protocol+"::Proxy"]
if value == "" {
value = "DIRECT"
}
// A later fragment can shadow a more-specific earlier host rule only by
// repeating that host key. The earlier file itself remains untouched and
// becomes effective again when the owned overlay is removed.
desired[prefix+strings.TrimPrefix(key, prefix)] = value
}
}
func equalSettings(left, right map[string]string) bool {
if len(left) != len(right) {
return false
}
for key, value := range left {
if right[key] != value {
return false
}
}
return true
}
type redactedError struct {
message string
class error
}
func (e *redactedError) Error() string { return e.message }
func (e *redactedError) Is(other error) bool { return e.class != nil && errors.Is(e.class, other) }
func redactError(err error, active *profile.Profile) error {
if err == nil {
return nil
}
message := profile.RedactText(err.Error())
if active != nil {
for _, raw := range []string{active.SingleURL, active.HTTPURL, active.HTTPSURL, active.AllURL} {
if raw != "" {
message = strings.ReplaceAll(message, raw, profile.ObscureURL(raw))
}
}
}
var class error
for _, safe := range []error{os.ErrPermission, fs.ErrNotExist, fs.ErrExist, context.Canceled, context.DeadlineExceeded} {
if errors.Is(err, safe) {
class = safe
break
}
}
return &redactedError{message: message, class: class}
}
+344
View File
@@ -0,0 +1,344 @@
package apt_test
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"proxyctl/internal/apt"
"proxyctl/internal/profile"
"proxyctl/internal/target"
"proxyctl/internal/target/targettest"
)
func TestAvailabilityRequiresDebianMarkerAPTToolAndConfigurationDirectory(t *testing.T) {
for _, tc := range []struct {
name string
marker, tool, dir bool
want bool
}{
{"detected", true, true, true, true},
{"not Debian", false, true, true, false},
{"apt-config absent", true, false, true, false},
{"configuration absent", true, true, false, false},
} {
t.Run(tc.name, func(t *testing.T) {
root := t.TempDir()
configDir := filepath.Join(root, "etc", "apt", "apt.conf.d")
if tc.dir {
mustMkdir(t, configDir)
}
if tc.marker {
mustMkdir(t, filepath.Dir(filepath.Join(root, "etc", "debian_version")))
mustWrite(t, filepath.Join(root, "etc", "debian_version"), "12\n")
}
runner := &fakeRunner{available: tc.tool}
a := apt.New(apt.Configuration{DebianVersionFile: filepath.Join(root, "etc", "debian_version"), ConfigDirectory: configDir, OverlayFile: filepath.Join(configDir, "99proxyctl"), TrustedRoot: root}, nil, runner)
got := a.Availability(context.Background())
if got.Available != tc.want || got.Evidence == "" {
t.Fatalf("availability = %#v", got)
}
})
}
}
func TestDescriptionExplainsEnvironmentPersistenceAndPrivilegeBoundary(t *testing.T) {
a, _ := newAdapter(t, nil)
got := a.Description()
combined := got.Summary + " " + got.Details
for _, phrase := range []string{"environment variables", "persistent", "privilege"} {
if !strings.Contains(strings.ToLower(combined), strings.ToLower(phrase)) {
t.Errorf("description does not mention %q: %#v", phrase, got)
}
}
}
func TestAPTAdapterRunsReusableTargetContract(t *testing.T) {
targettest.Run(t, func(t *testing.T) targettest.Fixture {
a, runner := newAdapter(t, nil)
return targettest.Fixture{
Target: a, Active: compatibleProfile(), Secret: "secret",
MakeExternal: func(t *testing.T) { runner.external = `Acquire::http::Proxy "http://prior.example:3128";` },
MakeDrifted: func(t *testing.T) {
contents := readFile(t, a.OverlayPath())
mustWrite(t, a.OverlayPath(), strings.Replace(contents, "http://alice:secret@proxy.example:8080", "http://changed.example:8080", 1))
},
PermissionFailure: func(t *testing.T) target.OperationResult {
failed, _ := newFailingAdapter(t, &failingFS{permission: true})
return failed.SetDesired(context.Background(), true, compatibleProfile())
},
PartialFailure: func(t *testing.T) target.OperationResult {
failed, failedRunner := newAdapter(t, nil)
failedRunner.failDumpAfterWrite = failed.OverlayPath()
return failed.SetDesired(context.Background(), true, compatibleProfile())
},
RedactionFailure: func(t *testing.T) target.OperationResult {
failed, _ := newFailingAdapter(t, &failingFS{writeError: errors.New("write http://alice:secret@proxy.example:8080 failed")})
return failed.SetDesired(context.Background(), true, compatibleProfile())
},
RecoveryRetry: func(t *testing.T) (target.OperationResult, target.OperationResult, target.OperationResult) {
files := &failingFS{failOnce: true}
failed, runner := newFailingAdapter(t, files)
runner.external = `Acquire::http::Proxy "http://prior.example:3128";`
first := failed.SetDesired(context.Background(), true, compatibleProfile())
retry := failed.SetDesired(context.Background(), true, compatibleProfile())
disabled := failed.SetDesired(context.Background(), false, nil)
return first, retry, disabled
},
}
})
}
func TestOwnedOverlayWinsAndDisableRevealsPriorEffectiveConfiguration(t *testing.T) {
a, runner := newAdapter(t, nil)
runner.external = "Acquire::http::Proxy \"http://prior.example:3128\";\n" +
"Acquire::http::Proxy::special.example \"http://special.example:3128\";"
if got := a.Inspect(context.Background(), compatibleProfile()); got.State != target.External || !strings.Contains(got.Evidence, "Acquire::http::Proxy") {
t.Fatalf("external inspection = %#v", got)
}
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil || got.Inspection.State != target.Enabled {
t.Fatalf("enable = %#v", got)
}
managed := readFile(t, a.OverlayPath())
for _, expected := range []string{
`Acquire::http::Proxy "http://alice:secret@proxy.example:8080";`,
`Acquire::https::Proxy "https://proxy.example:8443";`,
`Acquire::http::Proxy::localhost "DIRECT";`,
`Acquire::https::Proxy::10.0.0.1 "DIRECT";`,
`Acquire::http::Proxy::special.example "http://alice:secret@proxy.example:8080";`,
} {
if !strings.Contains(managed, expected) {
t.Errorf("overlay missing %q:\n%s", expected, managed)
}
}
if got := a.SetDesired(context.Background(), false, nil); got.Err != nil || got.Inspection.State != target.External {
t.Fatalf("disable = %#v", got)
}
if _, err := os.Stat(a.OverlayPath()); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("managed overlay remains: %v", err)
}
if !strings.Contains(runner.external, `Acquire::http::Proxy "http://prior.example:3128";`) || !strings.Contains(runner.external, `Acquire::http::Proxy::special.example "http://special.example:3128";`) {
t.Fatal("unrelated APT configuration changed")
}
if got := a.SetDesired(context.Background(), false, nil); got.Err != nil || got.Inspection.State != target.External {
t.Fatalf("idempotent disable = %#v", got)
}
}
func TestOverlayNameCollisionNeverRewritesOrRemovesUnownedConfiguration(t *testing.T) {
a, _ := newAdapter(t, nil)
original := `Acquire::http::Proxy "http://user-owned.example:3128";` + "\n"
mustWrite(t, a.OverlayPath(), original)
if got := a.Inspect(context.Background(), compatibleProfile()); got.State != target.External {
t.Fatalf("unowned collision inspection = %#v", got)
}
got := a.SetDesired(context.Background(), true, compatibleProfile())
if got.Err == nil || !strings.Contains(got.Err.Error(), "not owned") || readFile(t, a.OverlayPath()) != original {
t.Fatalf("enable over collision = %#v, file %q", got, readFile(t, a.OverlayPath()))
}
if got := a.SetDesired(context.Background(), false, nil); got.Err != nil || got.Inspection.State != target.External || readFile(t, a.OverlayPath()) != original {
t.Fatalf("disable over collision = %#v", got)
}
}
func TestEnableReturnsFailureWhenManagedValuesDoNotBecomeEffective(t *testing.T) {
a, runner := newAdapter(t, nil)
runner.override = `Acquire::http::Proxy "http://later.example:8080";`
got := a.SetDesired(context.Background(), true, compatibleProfile())
if got.Err == nil || got.Inspection.State != target.Drifted {
t.Fatalf("ineffective enable = %#v", got)
}
}
func TestInspectionUsesEffectiveAPTConfiguration(t *testing.T) {
a, runner := newAdapter(t, nil)
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil {
t.Fatal(got.Err)
}
runner.override = `Acquire::http::Proxy "http://later.example:8080";`
if got := a.Inspect(context.Background(), compatibleProfile()); got.State != target.Drifted || !strings.Contains(got.Evidence, "effective") {
t.Fatalf("higher-precedence effective config = %#v", got)
}
}
func TestIncompatibleProtocolOrBypassPreventsMutationAndNamesValue(t *testing.T) {
cases := []struct {
name string
mutate func(*profile.Profile)
value string
}{
{"socks5 local DNS", func(p *profile.Profile) { p.HTTPURL = "socks5://unsupported.example:1080" }, "socks5://unsupported.example:1080"},
{"incompatible overridden ALL", func(p *profile.Profile) { p.AllURL = "socks5://all.example:1080" }, "socks5://all.example:1080"},
{"domain suffix", func(p *profile.Profile) { p.Bypass = []string{".example.com"} }, ".example.com"},
{"CIDR", func(p *profile.Profile) { p.Bypass = []string{"10.0.0.0/8"} }, "10.0.0.0/8"},
{"IPv6", func(p *profile.Profile) { p.Bypass = []string{"2001:db8::1"} }, "2001:db8::1"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
a, _ := newAdapter(t, nil)
p := compatibleProfile()
tc.mutate(p)
got := a.SetDesired(context.Background(), true, p)
if got.Err == nil || !strings.Contains(got.Err.Error(), tc.value) || got.Inspection.State != target.Disabled {
t.Fatalf("result = %#v", got)
}
if _, err := os.Stat(a.OverlayPath()); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("incompatible Profile mutated overlay: %v", err)
}
})
}
}
func TestManagedOverlayIsAtomicOwnerOnlyAndRejectsSymlinkVictims(t *testing.T) {
t.Run("owner-only atomic file", func(t *testing.T) {
a, _ := newAdapter(t, nil)
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil {
t.Fatal(got.Err)
}
info, err := os.Stat(a.OverlayPath())
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("overlay mode = %o", info.Mode().Perm())
}
matches, _ := filepath.Glob(filepath.Join(filepath.Dir(a.OverlayPath()), ".proxyctl-*"))
if len(matches) != 0 {
t.Fatalf("atomic temporary files remain: %v", matches)
}
})
t.Run("final symlink", func(t *testing.T) {
a, _ := newAdapter(t, nil)
victim := filepath.Join(t.TempDir(), "victim")
mustWrite(t, victim, "unchanged\n")
if err := os.Symlink(victim, a.OverlayPath()); err != nil {
t.Fatal(err)
}
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err == nil {
t.Fatal("enable followed a symlinked overlay")
}
if got := readFile(t, victim); got != "unchanged\n" {
t.Fatalf("symlink victim changed: %q", got)
}
})
}
func TestAdvancedALLValueSuppliesMissingAPTProtocols(t *testing.T) {
a, _ := newAdapter(t, nil)
p := &profile.Profile{Name: "all", Mode: profile.AdvancedMode, AllURL: "socks5h://proxy.example:1080"}
if got := a.SetDesired(context.Background(), true, p); got.Err != nil || got.Inspection.State != target.Enabled {
t.Fatalf("enable = %#v", got)
}
contents := readFile(t, a.OverlayPath())
if strings.Count(contents, `"socks5h://proxy.example:1080"`) != 2 {
t.Fatalf("ALL translation did not supply both APT protocols:\n%s", contents)
}
}
func TestSingleModeUsesOneSupportedValueWithoutTreatingExpansionAsALL(t *testing.T) {
a, _ := newAdapter(t, nil)
p := &profile.Profile{Name: "single", Mode: profile.SingleMode, SingleURL: "socks5h://proxy.example:1080", Bypass: []string{"localhost"}}
if got := a.SetDesired(context.Background(), true, p); got.Err != nil || got.Inspection.State != target.Enabled {
t.Fatalf("enable = %#v", got)
}
contents := readFile(t, a.OverlayPath())
if !strings.Contains(contents, `Acquire::http::Proxy "socks5h://proxy.example:1080";`) || !strings.Contains(contents, `Acquire::https::Proxy "socks5h://proxy.example:1080";`) {
t.Fatalf("single translation:\n%s", contents)
}
}
func compatibleProfile() *profile.Profile {
return &profile.Profile{Name: "office", Mode: profile.AdvancedMode, HTTPURL: "http://alice:secret@proxy.example:8080", HTTPSURL: "https://proxy.example:8443", Bypass: []string{"localhost", "10.0.0.1"}}
}
func newAdapter(t *testing.T, files apt.FileSystem) (*apt.Adapter, *fakeRunner) {
t.Helper()
root := t.TempDir()
dir := filepath.Join(root, "etc", "apt", "apt.conf.d")
mustMkdir(t, dir)
mustWrite(t, filepath.Join(root, "etc", "debian_version"), "12\n")
runner := &fakeRunner{available: true}
if failure, ok := files.(*failingFS); ok {
failure.delegate = apt.NewOSFileSystem(root)
}
a := apt.New(apt.Configuration{DebianVersionFile: filepath.Join(root, "etc", "debian_version"), ConfigDirectory: dir, OverlayFile: filepath.Join(dir, "99proxyctl"), TrustedRoot: root}, files, runner)
runner.overlay = a.OverlayPath()
return a, runner
}
func newFailingAdapter(t *testing.T, files *failingFS) (*apt.Adapter, *fakeRunner) {
return newAdapter(t, files)
}
type fakeRunner struct {
available bool
overlay, external, override, failDumpAfterWrite string
}
func (r *fakeRunner) LookPath(string) (string, error) {
if !r.available {
return "", os.ErrNotExist
}
return "/usr/bin/apt-config", nil
}
func (r *fakeRunner) Output(context.Context, string, ...string) ([]byte, error) {
if r.failDumpAfterWrite != "" {
if _, err := os.Stat(r.failDumpAfterWrite); err == nil {
return nil, errors.New("apt-config dump failed")
}
}
if r.override != "" {
return []byte(r.override + "\n"), nil
}
if r.overlay != "" {
if data, err := os.ReadFile(r.overlay); err == nil {
return []byte(r.external + "\n" + string(data)), nil
}
}
return []byte(r.external + "\n"), nil
}
type failingFS struct {
delegate apt.FileSystem
permission, failOnce bool
writeError error
}
func (f *failingFS) ReadFile(path string) ([]byte, error) { return f.delegate.ReadFile(path) }
func (f *failingFS) AtomicWrite(path string, data []byte, mode os.FileMode) error {
if f.permission {
return os.ErrPermission
}
if f.writeError != nil {
return f.writeError
}
if f.failOnce {
f.failOnce = false
return os.ErrPermission
}
return f.delegate.AtomicWrite(path, data, mode)
}
func (f *failingFS) Remove(path string) error { return f.delegate.Remove(path) }
func mustMkdir(t *testing.T, path string) {
t.Helper()
if err := os.MkdirAll(path, 0o755); err != nil {
t.Fatal(err)
}
}
func mustWrite(t *testing.T, path, value string) {
t.Helper()
if err := os.WriteFile(path, []byte(value), 0o644); err != nil {
t.Fatal(err)
}
}
func readFile(t *testing.T, path string) string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
return string(data)
}