Skip to content

Commit 09fdb24

Browse files
authored
fix: split Linux-only lint paths behind build tags (#181)
1 parent af7895d commit 09fdb24

7 files changed

Lines changed: 142 additions & 85 deletions

File tree

.github/workflows/main.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,7 @@ jobs:
6060
run: make fmt-check
6161

6262
- name: Lint
63-
uses: golangci/golangci-lint-action@v9
64-
with:
65-
install-mode: "none"
63+
run: make lint
6664

6765
test-linux:
6866
name: Test (Linux)

cmd/fence/linux_helpers_linux.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//go:build linux
2+
3+
package main
4+
5+
import (
6+
"os"
7+
8+
"github.com/fencesandbox/fence/internal/fencelog"
9+
"github.com/fencesandbox/fence/internal/sandbox"
10+
)
11+
12+
func runLinuxInternalHelperMode(args []string) bool {
13+
if len(args) >= 2 && args[1] == "--linux-argv-exec-run" {
14+
exitCode, err := sandbox.RunLinuxArgvExecRunnerFromEnv()
15+
if err != nil {
16+
fencelog.Printf("[fence:linux] %v\n", err)
17+
}
18+
os.Exit(exitCode)
19+
}
20+
if len(args) >= 2 && args[1] == "--linux-argv-exec-shim" {
21+
exitCode, err := sandbox.RunLinuxArgvExecShim(args[2:])
22+
if err != nil {
23+
fencelog.Printf("[fence:linux] %v\n", err)
24+
}
25+
os.Exit(exitCode)
26+
}
27+
return false
28+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//go:build !linux
2+
3+
package main
4+
5+
import (
6+
"os"
7+
8+
"github.com/fencesandbox/fence/internal/fencelog"
9+
)
10+
11+
func runLinuxInternalHelperMode(args []string) bool {
12+
if len(args) < 2 {
13+
return false
14+
}
15+
switch args[1] {
16+
case "--linux-argv-exec-run", "--linux-argv-exec-shim":
17+
fencelog.Printf("[fence:linux] %s is only available on Linux\n", args[1])
18+
os.Exit(1)
19+
}
20+
return false
21+
}

cmd/fence/main.go

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,8 @@ func main() {
6666
runLandlockWrapper()
6767
return
6868
}
69-
if len(os.Args) >= 2 && os.Args[1] == "--linux-argv-exec-run" {
70-
exitCode, err := sandbox.RunLinuxArgvExecRunnerFromEnv()
71-
if err != nil {
72-
fencelog.Printf("[fence:linux] %v\n", err)
73-
}
74-
os.Exit(exitCode)
75-
}
76-
if len(os.Args) >= 2 && os.Args[1] == "--linux-argv-exec-shim" {
77-
exitCode, err := sandbox.RunLinuxArgvExecShim(os.Args[2:])
78-
if err != nil {
79-
fencelog.Printf("[fence:linux] %v\n", err)
80-
}
81-
os.Exit(exitCode)
69+
if runLinuxInternalHelperMode(os.Args) {
70+
return
8271
}
8372
if len(os.Args) >= 2 && os.Args[1] == claudePreToolUseMode {
8473
if err := runClaudePreToolUseMode(); err != nil {

internal/sandbox/manager.go

Lines changed: 2 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -203,75 +203,8 @@ func (m *Manager) Initialize() error {
203203
}
204204
m.socksPort = socksPort
205205

206-
// On Linux, set up the socat bridges
207-
if platform.Detect() == platform.Linux {
208-
bridge, err := NewLinuxBridge(m.httpPort, m.socksPort, m.debug)
209-
if err != nil {
210-
_ = m.httpProxy.Stop()
211-
_ = m.socksProxy.Stop()
212-
return fmt.Errorf("failed to initialize Linux bridge: %w", err)
213-
}
214-
m.linuxBridge = bridge
215-
216-
// Set up reverse bridge for exposed ports (inbound connections).
217-
// Only needed when:
218-
// (a) a network namespace is available (otherwise host & sandbox
219-
// share the netns and external traffic reaches listeners directly), and
220-
// (b) the service binds its port INSIDE the sandbox. For
221-
// ServiceBindsOnHost (docker, podman, …), the port is bound by
222-
// an external daemon outside the netns; a reverse bridge on the
223-
// same port would collide with the daemon's bind.
224-
features := DetectLinuxFeatures()
225-
exposures := m.service.resolvedExposures()
226-
switch {
227-
case len(exposures) == 0:
228-
// nothing to do
229-
case m.service.ExecutionModel == ServiceBindsOnHost:
230-
if m.debug {
231-
m.logDebug("Skipping reverse bridge (ServiceBindsOnHost: external daemon binds ports %v outside sandbox netns)", m.service.resolvedPorts())
232-
}
233-
case !features.CanUnshareNet:
234-
if m.debug {
235-
m.logDebug("Skipping reverse bridge (no network namespace, ports accessible directly)")
236-
}
237-
default:
238-
reverseBridge, err := NewReverseBridge(exposures, m.debug)
239-
if err != nil {
240-
m.linuxBridge.Cleanup()
241-
_ = m.httpProxy.Stop()
242-
_ = m.socksProxy.Stop()
243-
return fmt.Errorf("failed to initialize reverse bridge: %w", err)
244-
}
245-
m.reverseBridge = reverseBridge
246-
}
247-
248-
// Set up the localhost-outbound bridge when the user opted into
249-
// host-loopback access. The bridge is only meaningful when we also
250-
// unshare the network namespace (otherwise sandbox 127.0.0.1 already
251-
// is the host's 127.0.0.1 and no forwarding is needed). Wildcard
252-
// relaxed mode drops --unshare-net too, so skip there.
253-
if m.config != nil && m.config.Network.EffectiveAllowLocalOutbound() && features.CanUnshareNet && !hasWildcardAllowedDomain(m.config) {
254-
ports := m.config.Network.AllowLocalOutboundPorts
255-
if len(ports) > 0 {
256-
loBridge, err := NewLocalOutboundBridge(ports, m.debug)
257-
if err != nil {
258-
if m.reverseBridge != nil {
259-
m.reverseBridge.Cleanup()
260-
}
261-
m.linuxBridge.Cleanup()
262-
_ = m.httpProxy.Stop()
263-
_ = m.socksProxy.Stop()
264-
return fmt.Errorf("failed to initialize localhost-outbound bridge: %w", err)
265-
}
266-
m.localOutboundBridge = loBridge
267-
} else {
268-
// Surface the Linux-specific limitation once at startup so
269-
// users do not silently get the pre-fix broken behavior.
270-
fencelog.Printf(
271-
"[fence] network.allowLocalOutbound=true on Linux requires network.allowLocalOutboundPorts to list the host loopback ports to bridge (e.g. [5432, 6379]). Without it, sandbox connections to 127.0.0.1 stay isolated inside the sandbox network namespace.\n",
272-
)
273-
}
274-
}
206+
if err := m.initializePlatformNetworking(); err != nil {
207+
return err
275208
}
276209

277210
m.initialized = true

internal/sandbox/manager_linux.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//go:build linux
2+
3+
package sandbox
4+
5+
import (
6+
"fmt"
7+
8+
"github.com/fencesandbox/fence/internal/fencelog"
9+
)
10+
11+
func (m *Manager) initializePlatformNetworking() error {
12+
bridge, err := NewLinuxBridge(m.httpPort, m.socksPort, m.debug)
13+
if err != nil {
14+
_ = m.httpProxy.Stop()
15+
_ = m.socksProxy.Stop()
16+
return fmt.Errorf("failed to initialize Linux bridge: %w", err)
17+
}
18+
m.linuxBridge = bridge
19+
20+
// Set up reverse bridge for exposed ports (inbound connections).
21+
// Only needed when:
22+
// (a) a network namespace is available (otherwise host & sandbox
23+
// share the netns and external traffic reaches listeners directly), and
24+
// (b) the service binds its port INSIDE the sandbox. For
25+
// ServiceBindsOnHost (docker, podman, ...), the port is bound by
26+
// an external daemon outside the netns; a reverse bridge on the
27+
// same port would collide with the daemon's bind.
28+
features := DetectLinuxFeatures()
29+
exposures := m.service.resolvedExposures()
30+
switch {
31+
case len(exposures) == 0:
32+
// nothing to do
33+
case m.service.ExecutionModel == ServiceBindsOnHost:
34+
if m.debug {
35+
m.logDebug("Skipping reverse bridge (ServiceBindsOnHost: external daemon binds ports %v outside sandbox netns)", m.service.resolvedPorts())
36+
}
37+
case !features.CanUnshareNet:
38+
if m.debug {
39+
m.logDebug("Skipping reverse bridge (no network namespace, ports accessible directly)")
40+
}
41+
default:
42+
reverseBridge, err := NewReverseBridge(exposures, m.debug)
43+
if err != nil {
44+
m.linuxBridge.Cleanup()
45+
_ = m.httpProxy.Stop()
46+
_ = m.socksProxy.Stop()
47+
return fmt.Errorf("failed to initialize reverse bridge: %w", err)
48+
}
49+
m.reverseBridge = reverseBridge
50+
}
51+
52+
// Set up the localhost-outbound bridge when the user opted into
53+
// host-loopback access. The bridge is only meaningful when we also
54+
// unshare the network namespace (otherwise sandbox 127.0.0.1 already
55+
// is the host's 127.0.0.1 and no forwarding is needed). Wildcard
56+
// relaxed mode drops --unshare-net too, so skip there.
57+
if m.config != nil && m.config.Network.EffectiveAllowLocalOutbound() && features.CanUnshareNet && !hasWildcardAllowedDomain(m.config) {
58+
ports := m.config.Network.AllowLocalOutboundPorts
59+
if len(ports) > 0 {
60+
loBridge, err := NewLocalOutboundBridge(ports, m.debug)
61+
if err != nil {
62+
if m.reverseBridge != nil {
63+
m.reverseBridge.Cleanup()
64+
}
65+
m.linuxBridge.Cleanup()
66+
_ = m.httpProxy.Stop()
67+
_ = m.socksProxy.Stop()
68+
return fmt.Errorf("failed to initialize localhost-outbound bridge: %w", err)
69+
}
70+
m.localOutboundBridge = loBridge
71+
} else {
72+
// Surface the Linux-specific limitation once at startup so
73+
// users do not silently get the pre-fix broken behavior.
74+
fencelog.Printf(
75+
"[fence] network.allowLocalOutbound=true on Linux requires network.allowLocalOutboundPorts to list the host loopback ports to bridge (e.g. [5432, 6379]). Without it, sandbox connections to 127.0.0.1 stay isolated inside the sandbox network namespace.\n",
76+
)
77+
}
78+
}
79+
80+
return nil
81+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//go:build !linux
2+
3+
package sandbox
4+
5+
func (m *Manager) initializePlatformNetworking() error {
6+
return nil
7+
}

0 commit comments

Comments
 (0)