summaryrefslogtreecommitdiff
path: root/internal/namespace
diff options
context:
space:
mode:
Diffstat (limited to 'internal/namespace')
-rw-r--r--internal/namespace/launcher_src/launcher.c2
-rw-r--r--internal/namespace/lock_linux.go40
-rw-r--r--internal/namespace/lock_stub.go18
-rw-r--r--internal/namespace/namespace.go26
-rw-r--r--internal/namespace/pinning.go5
5 files changed, 84 insertions, 7 deletions
diff --git a/internal/namespace/launcher_src/launcher.c b/internal/namespace/launcher_src/launcher.c
index e108da6..7bbbce7 100644
--- a/internal/namespace/launcher_src/launcher.c
+++ b/internal/namespace/launcher_src/launcher.c
@@ -9,7 +9,7 @@
int main(int argc, char **argv) {
if (argc < 1) {
- fprintf(stderr, "Usage: %s <command> [args...]\n", argv[0]);
+ fprintf(stderr, "Usage: launcher <command> [args...]\n");
return 1;
}
diff --git a/internal/namespace/lock_linux.go b/internal/namespace/lock_linux.go
new file mode 100644
index 0000000..8da98f6
--- /dev/null
+++ b/internal/namespace/lock_linux.go
@@ -0,0 +1,40 @@
+//go:build linux
+
+package namespace
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "git.theodohertyfamily.com/tools/wg-wrap/internal/paths"
+ "golang.org/x/sys/unix"
+)
+
+// AcquireProfileLock locks the profile to prevent concurrent startup races.
+func AcquireProfileLock(pm *paths.PathManager, profile string) (*os.File, error) {
+ lockPath := filepath.Join(pm.RuntimeBaseDir(), "profiles", profile+".lock")
+ if err := os.MkdirAll(filepath.Dir(lockPath), 0755); err != nil {
+ return nil, fmt.Errorf("failed to create lock directory: %w", err)
+ }
+
+ file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open lock file: %w", err)
+ }
+
+ if err := unix.Flock(int(file.Fd()), unix.LOCK_EX); err != nil {
+ _ = file.Close()
+ return nil, fmt.Errorf("failed to lock profile: %w", err)
+ }
+
+ return file, nil
+}
+
+// ReleaseProfileLock unlocks the profile.
+func ReleaseProfileLock(file *os.File) {
+ if file != nil {
+ _ = unix.Flock(int(file.Fd()), unix.LOCK_UN)
+ _ = file.Close()
+ }
+}
diff --git a/internal/namespace/lock_stub.go b/internal/namespace/lock_stub.go
new file mode 100644
index 0000000..2282852
--- /dev/null
+++ b/internal/namespace/lock_stub.go
@@ -0,0 +1,18 @@
+//go:build !linux
+
+package namespace
+
+import (
+ "os"
+
+ "git.theodohertyfamily.com/tools/wg-wrap/internal/paths"
+)
+
+// AcquireProfileLock is a stub for non-Linux platforms.
+func AcquireProfileLock(pm *paths.PathManager, profile string) (*os.File, error) {
+ return nil, nil
+}
+
+// ReleaseProfileLock is a stub for non-Linux platforms.
+func ReleaseProfileLock(file *os.File) {
+}
diff --git a/internal/namespace/namespace.go b/internal/namespace/namespace.go
index e2ef2f1..ab3797d 100644
--- a/internal/namespace/namespace.go
+++ b/internal/namespace/namespace.go
@@ -69,11 +69,20 @@ func VerifyArguments(args []string) error {
// Bootstrap ensures the process is running in an isolated user and network namespace.
// It writes the embedded C launcher to a temporary file and replaces the current process.
-func Bootstrap() error {
+func Bootstrap() (err error) {
if IsIsolated() {
return nil
}
+ var fdsToClose []int
+ defer func() {
+ if err != nil {
+ for _, fd := range fdsToClose {
+ _ = syscall.Close(fd)
+ }
+ }
+ }()
+
// 0. Validate current arguments for null bytes before proceeding.
// If any argument contains a null byte, syscall.Exec will fail with 'invalid argument'.
for i, arg := range os.Args {
@@ -118,6 +127,7 @@ func Bootstrap() error {
_ = os.Remove(launcherPath)
return fmt.Errorf("failed to open launcher for exec: %w", err)
}
+ fdsToClose = append(fdsToClose, execFd)
// Close the write file descriptor (to avoid ETXTBSY)
_ = tmpFile.Close()
@@ -152,6 +162,8 @@ func Bootstrap() error {
if err != nil {
return fmt.Errorf("failed to open host netns: %w", err)
}
+ fdsToClose = append(fdsToClose, hostNetFd)
+
// Clear close-on-exec so it remains open across syscall.Exec
if flags, err := unix.FcntlInt(uintptr(hostNetFd), unix.F_GETFD, 0); err == nil {
_, _ = unix.FcntlInt(uintptr(hostNetFd), unix.F_SETFD, flags&^unix.FD_CLOEXEC)
@@ -160,15 +172,17 @@ func Bootstrap() error {
env := append(os.Environ(), fmt.Sprintf("WG_WRAP_HOST_NETNS_FD=%d", hostNetFd))
// Open a host UDP socket on 0.0.0.0:0 before unsharing network namespace.
- laddr, err := net.ResolveUDPAddr("udp", "0.0.0.0:0")
- if err == nil {
- if conn, err := net.ListenUDP("udp", laddr); err == nil {
- if file, err := conn.File(); err == nil {
+ laddr, errAddr := net.ResolveUDPAddr("udp", "0.0.0.0:0")
+ if errAddr == nil {
+ if conn, errConn := net.ListenUDP("udp", laddr); errConn == nil {
+ if file, errFile := conn.File(); errFile == nil {
hostSocketFd := file.Fd()
- if flags, err := unix.FcntlInt(hostSocketFd, unix.F_GETFD, 0); err == nil {
+ if flags, fcntlErr := unix.FcntlInt(hostSocketFd, unix.F_GETFD, 0); fcntlErr == nil {
_, _ = unix.FcntlInt(hostSocketFd, unix.F_SETFD, flags&^unix.FD_CLOEXEC)
}
env = append(env, fmt.Sprintf("WG_WRAP_HOST_SOCKET_FD=%d", hostSocketFd))
+ fdsToClose = append(fdsToClose, int(hostSocketFd))
+ _ = conn.Close()
}
}
}
diff --git a/internal/namespace/pinning.go b/internal/namespace/pinning.go
index eb0a376..2433203 100644
--- a/internal/namespace/pinning.go
+++ b/internal/namespace/pinning.go
@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "runtime"
"strconv"
"syscall"
@@ -84,6 +85,10 @@ func JoinExistingNamespace(pm *paths.PathManager, profile string) (bool, error)
return false, nil
}
+ // Lock the OS thread before joining namespaces to ensure this goroutine stays on the modified thread,
+ // and that the thread is not reused for other goroutines (since we never unlock it).
+ runtime.LockOSThread()
+
// Join User Namespace first
userNsPath := fmt.Sprintf("/proc/%d/ns/user", activePid)
userFd, err := os.Open(userNsPath)