From 7108d580de7d36fe60f8e02ff1a711258b454839 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 27 May 2026 10:04:21 +0100 Subject: [PATCH 1/4] feat: replace TCP loopback with secure named pipes --- go.mod | 1 + go.sum | 2 ++ server.go | 32 +++++++++++++++++++------------- transport.go | 38 ++++++++++++++++++++++++++++++++++++++ transport_other.go | 12 ++++++++++++ transport_windows.go | 21 +++++++++++++++++++++ 6 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 transport.go create mode 100644 transport_other.go create mode 100644 transport_windows.go diff --git a/go.mod b/go.mod index edd9bf22..514d45b8 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( ) require ( + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/bufbuild/protocompile v0.14.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect diff --git a/go.sum b/go.sum index dc443178..0d96df94 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/server.go b/server.go index 0f4b8e56..a158a64f 100644 --- a/server.go +++ b/server.go @@ -8,14 +8,12 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" - "errors" "fmt" "io" "net" "os" "os/signal" "os/user" - "runtime" "sort" "strconv" "strings" @@ -525,13 +523,15 @@ func Serve(opts *ServeConfig) { } } -func serverListener(unixSocketCfg UnixSocketConfig) (net.Listener, error) { - if runtime.GOOS == "windows" { - return serverListener_tcp() - } +//func serverListener(unixSocketCfg UnixSocketConfig) (net.Listener, error) { +//if runtime.GOOS == "windows" { +// return serverListener_tcp() +//} - return serverListener_unix(unixSocketCfg) -} +// return serverListener_unix(unixSocketCfg) +//} + +/* func serverListener_tcp() (net.Listener, error) { envMinPort := os.Getenv("PLUGIN_MIN_PORT") @@ -565,17 +565,21 @@ func serverListener_tcp() (net.Listener, error) { } for port := minPort; port <= maxPort; port++ { - address := fmt.Sprintf("127.0.0.1:%d", port) - listener, err := net.Listen("tcp", address) + //address := fmt.Sprintf("127.0.0.1:%d", port) + //listener, err := net.Listen("tcp", address) + //listener, err := secureListen(c.pipeName) + if err == nil { - return listener, nil + return nil, nil } } return nil, errors.New("couldn't bind plugin TCP listener") } -func serverListener_unix(unixSocketCfg UnixSocketConfig) (net.Listener, error) { +*/ + +func serverListener(unixSocketCfg UnixSocketConfig) (net.Listener, error) { tf, err := os.CreateTemp(unixSocketCfg.socketDir, "plugin") if err != nil { return nil, err @@ -591,7 +595,9 @@ func serverListener_unix(unixSocketCfg UnixSocketConfig) (net.Listener, error) { return nil, err } - l, err := net.Listen("unix", path) + l, err := secureListen(path) + + //l, err := net.Listen("unix", path) if err != nil { return nil, err } diff --git a/transport.go b/transport.go new file mode 100644 index 00000000..03373f58 --- /dev/null +++ b/transport.go @@ -0,0 +1,38 @@ +// go-plugin/transport.go +package plugin + +import ( + "fmt" + "net" + "os" + "runtime" + "sync" + "syscall" +) + +var umaskMu sync.Mutex + +// secureListen creates a platform-appropriate secure listener. +// Windows: named pipe restricted to owner via ACL. +// Unix: unix socket created atomically with 0600 permissions. +func secureListen(name string) (net.Listener, error) { + switch runtime.GOOS { + case "windows": + return secureListenWindows(name) + default: + return secureListenUnix(name) + } +} + +func secureListenUnix(name string) (net.Listener, error) { + path := fmt.Sprintf("/tmp/%s.sock", name) + os.Remove(path) + + umaskMu.Lock() + oldUmask := syscall.Umask(0177) + l, err := net.Listen("unix", path) + syscall.Umask(oldUmask) + umaskMu.Unlock() + + return l, err +} diff --git a/transport_other.go b/transport_other.go new file mode 100644 index 00000000..2308d19b --- /dev/null +++ b/transport_other.go @@ -0,0 +1,12 @@ +// go-plugin/transport_other.go +//go:build !windows + +package plugin + +import "net" + +// secureListenWindows is only implemented on Windows +// this stub satisfies the compiler on other platforms +func secureListenWindows(name string) (net.Listener, error) { + return nil, nil // never called on non-Windows +} diff --git a/transport_windows.go b/transport_windows.go new file mode 100644 index 00000000..e8a59292 --- /dev/null +++ b/transport_windows.go @@ -0,0 +1,21 @@ +// go-plugin/transport_windows.go +//go:build windows + +package plugin + +import ( + "fmt" + "net" + + winio "github.com/Microsoft/go-winio" +) + +func secureListenWindows(name string) (net.Listener, error) { + pipe := fmt.Sprintf(`\\.\pipe\%s`, name) + return winio.ListenPipe(pipe, &winio.PipeConfig{ + SecurityDescriptor: "D:P(A;;GA;;;OW)", // owner full access only + MessageMode: false, + InputBufferSize: 65536, + OutputBufferSize: 65536, + }) +} From 341479803d7ffe7cef4dc27ed7ee6a206ccf3c8b Mon Sep 17 00:00:00 2001 From: James Date: Wed, 27 May 2026 10:11:08 +0100 Subject: [PATCH 2/4] feat: secure named pipe transport --- server.go | 41 +++++++++++++++++++++++++++++++++++++++++ transport.go | 16 +++++----------- transport_other.go | 8 +++++--- transport_windows.go | 8 +++++++- 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/server.go b/server.go index a158a64f..490522f5 100644 --- a/server.go +++ b/server.go @@ -14,6 +14,7 @@ import ( "os" "os/signal" "os/user" + "runtime" "sort" "strconv" "strings" @@ -579,6 +580,45 @@ func serverListener_tcp() (net.Listener, error) { */ +func serverListener(unixSocketCfg UnixSocketConfig) (net.Listener, error) { + switch runtime.GOOS { + case "windows": + // Windows: use a named pipe — no temp file needed + name := fmt.Sprintf("go-plugin-%d", os.Getpid()) + return secureListenWindows(name) + + default: + // Unix: create a temp file to get a unique path, then replace with socket + tf, err := os.CreateTemp(unixSocketCfg.socketDir, "plugin") + if err != nil { + return nil, err + } + path := tf.Name() + if err := tf.Close(); err != nil { + return nil, err + } + if err := os.Remove(path); err != nil { + return nil, err + } + + l, err := secureListenUnix(path) + if err != nil { + return nil, err + } + + // set group permissions if configured + if unixSocketCfg.Group != "" { + if err = setGroupWritable(path, unixSocketCfg.Group, 0o660); err != nil { + return nil, err + } + } + + // wrap so socket file is deleted on close + return newDeleteFileListener(l, path), nil + } +} + +/* func serverListener(unixSocketCfg UnixSocketConfig) (net.Listener, error) { tf, err := os.CreateTemp(unixSocketCfg.socketDir, "plugin") if err != nil { @@ -615,6 +655,7 @@ func serverListener(unixSocketCfg UnixSocketConfig) (net.Listener, error) { // is removed on close. return newDeleteFileListener(l, path), nil } +*/ func setGroupWritable(path, groupString string, mode os.FileMode) error { groupID, err := strconv.Atoi(groupString) diff --git a/transport.go b/transport.go index 03373f58..555f8595 100644 --- a/transport.go +++ b/transport.go @@ -1,4 +1,4 @@ -// go-plugin/transport.go +// transport.go package plugin import ( @@ -12,27 +12,21 @@ import ( var umaskMu sync.Mutex -// secureListen creates a platform-appropriate secure listener. -// Windows: named pipe restricted to owner via ACL. -// Unix: unix socket created atomically with 0600 permissions. -func secureListen(name string) (net.Listener, error) { +func secureListen(path string) (net.Listener, error) { switch runtime.GOOS { case "windows": + name := fmt.Sprintf("go-plugin-%d", os.Getpid()) return secureListenWindows(name) default: - return secureListenUnix(name) + return secureListenUnix(path) } } -func secureListenUnix(name string) (net.Listener, error) { - path := fmt.Sprintf("/tmp/%s.sock", name) - os.Remove(path) - +func secureListenUnix(path string) (net.Listener, error) { umaskMu.Lock() oldUmask := syscall.Umask(0177) l, err := net.Listen("unix", path) syscall.Umask(oldUmask) umaskMu.Unlock() - return l, err } diff --git a/transport_other.go b/transport_other.go index 2308d19b..ebc2c2b0 100644 --- a/transport_other.go +++ b/transport_other.go @@ -1,12 +1,14 @@ -// go-plugin/transport_other.go +// transport_other.go //go:build !windows package plugin import "net" -// secureListenWindows is only implemented on Windows -// this stub satisfies the compiler on other platforms func secureListenWindows(name string) (net.Listener, error) { return nil, nil // never called on non-Windows } + +func pipeName() string { + return "" // never called on non-Windows +} diff --git a/transport_windows.go b/transport_windows.go index e8a59292..0bc43db7 100644 --- a/transport_windows.go +++ b/transport_windows.go @@ -1,4 +1,4 @@ -// go-plugin/transport_windows.go +// transport_windows.go //go:build windows package plugin @@ -6,6 +6,7 @@ package plugin import ( "fmt" "net" + "os" winio "github.com/Microsoft/go-winio" ) @@ -19,3 +20,8 @@ func secureListenWindows(name string) (net.Listener, error) { OutputBufferSize: 65536, }) } + +// pipeName returns the named pipe path for the client to connect to +func pipeName() string { + return fmt.Sprintf(`\\.\pipe\go-plugin-%d`, os.Getpid()) +} From f910d3fd34eb18be0457d08649a7e0bad1ebdca5 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 27 May 2026 10:24:17 +0100 Subject: [PATCH 3/4] fix: correct function call to serverListener in Unix socket group permissions test --- server_unix_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_unix_test.go b/server_unix_test.go index 6a98448a..6f946271 100644 --- a/server_unix_test.go +++ b/server_unix_test.go @@ -25,7 +25,7 @@ func TestUnixSocketGroupPermissions(t *testing.T) { "as name": {group.Name}, } { t.Run(name, func(t *testing.T) { - ln, err := serverListener_unix(UnixSocketConfig{Group: tc.group}) + ln, err := serverListener(UnixSocketConfig{Group: tc.group}) if err != nil { t.Fatal(err) } From 6eba7389153ba41fba2c2adee85e313464dbefff Mon Sep 17 00:00:00 2001 From: James Date: Wed, 27 May 2026 11:33:40 +0100 Subject: [PATCH 4/4] chore: update module path to fork --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 514d45b8..22aa9fd9 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/hashicorp/go-plugin +module github.com/james-barrow/go-plugin go 1.24