Skip to content

Commit

Permalink
Implement AF_UNIX sockets on Windows
Browse files Browse the repository at this point in the history
See moby/moby#36442

Signed-off-by: Marat Radchenko <[email protected]>
  • Loading branch information
slonopotamus committed May 14, 2024
1 parent 5df8d2b commit d29df4a
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 63 deletions.
23 changes: 22 additions & 1 deletion sockets/sockets.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
package sockets

import (
"context"
"errors"
"fmt"
"net"
"net/http"
"syscall"
"time"
)

const defaultTimeout = 10 * time.Second
const (
defaultTimeout = 10 * time.Second
maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path)
)

// ErrProtocolNotAvailable is returned when a given transport protocol is not provided by the operating system.
var ErrProtocolNotAvailable = errors.New("protocol not available")
Expand All @@ -35,3 +41,18 @@ func ConfigureTransport(tr *http.Transport, proto, addr string) error {
}
return nil
}

func configureUnixTransport(tr *http.Transport, proto, addr string) error {
if len(addr) > maxUnixSocketPathSize {
return fmt.Errorf("Unix socket path %q is too long", addr)
}
// No need for compression in local communications.
tr.DisableCompression = true
dialer := &net.Dialer{
Timeout: defaultTimeout,
}
tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, proto, addr)
}
return nil
}
19 changes: 0 additions & 19 deletions sockets/sockets_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,12 @@
package sockets

import (
"context"
"fmt"
"net"
"net/http"
"syscall"
"time"
)

const maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path)

func configureUnixTransport(tr *http.Transport, proto, addr string) error {
if len(addr) > maxUnixSocketPathSize {
return fmt.Errorf("unix socket path %q is too long", addr)
}
// No need for compression in local communications.
tr.DisableCompression = true
dialer := &net.Dialer{
Timeout: defaultTimeout,
}
tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, proto, addr)
}
return nil
}

func configureNpipeTransport(tr *http.Transport, proto, addr string) error {
return ErrProtocolNotAvailable
}
Expand Down
4 changes: 0 additions & 4 deletions sockets/sockets_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ import (
"github.com/Microsoft/go-winio"
)

func configureUnixTransport(tr *http.Transport, proto, addr string) error {
return ErrProtocolNotAvailable
}

func configureNpipeTransport(tr *http.Transport, proto, addr string) error {
// No need for compression in local communications.
tr.DisableCompression = true
Expand Down
19 changes: 1 addition & 18 deletions sockets/unix_socket.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
//go:build !windows

/*
Package sockets is a simple unix domain socket wrapper.
Expand Down Expand Up @@ -90,22 +88,7 @@ func NewUnixSocketWithOpts(path string, opts ...SockOption) (net.Listener, error
return nil, err
}

// net.Listen does not allow for permissions to be set. As a result, when
// specifying custom permissions ("WithChmod()"), there is a short time
// between creating the socket and applying the permissions, during which
// the socket permissions are Less restrictive than desired.
//
// To work around this limitation of net.Listen(), we temporarily set the
// umask to 0777, which forces the socket to be created with 000 permissions
// (i.e.: no access for anyone). After that, WithChmod() must be used to set
// the desired permissions.
//
// We don't use "defer" here, to reset the umask to its original value as soon
// as possible. Ideally we'd be able to detect if WithChmod() was passed as
// an option, and skip changing umask if default permissions are used.
origUmask := syscall.Umask(0o777)
l, err := net.Listen("unix", path)
syscall.Umask(origUmask)
l, err := listenUnix(path)
if err != nil {
return nil, err
}
Expand Down
29 changes: 8 additions & 21 deletions sockets/unix_socket_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
//go:build !windows

package sockets

import (
"fmt"
"net"
"os"
"syscall"
"testing"
)

Expand Down Expand Up @@ -52,26 +49,16 @@ func TestNewUnixSocket(t *testing.T) {
}

func TestUnixSocketWithOpts(t *testing.T) {
uid, gid := os.Getuid(), os.Getgid()
perms := os.FileMode(0o660)
path := "/tmp/test.sock"
echoStr := "hello"
l, err := NewUnixSocketWithOpts(path, WithChown(uid, gid), WithChmod(perms))
socketFile, err := os.CreateTemp("", "test*.sock")
if err != nil {
t.Fatal(err)
}
socketFile.Close()
defer os.Remove(socketFile.Name())

l := createTestUnixSocket(t, socketFile.Name())
defer l.Close()
p, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if p.Mode().Perm() != perms {
t.Fatalf("unexpected file permissions: expected: %#o, got: %#o", perms, p.Mode().Perm())
}
if stat, ok := p.Sys().(*syscall.Stat_t); ok {
if stat.Uid != uint32(uid) || stat.Gid != uint32(gid) {
t.Fatalf("unexpected file ownership: expected: %d:%d, got: %d:%d", uid, gid, stat.Uid, stat.Gid)
}
}
runTest(t, path, l, echoStr)

echoStr := "hello"
runTest(t, socketFile.Name(), l, echoStr)
}
32 changes: 32 additions & 0 deletions sockets/unix_socket_test_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//go:build !windows

package sockets

import (
"net"
"os"
"syscall"
"testing"
)

func createTestUnixSocket(t *testing.T, path string) (listener net.Listener) {
uid, gid := os.Getuid(), os.Getgid()
perms := os.FileMode(0660)
l, err := NewUnixSocketWithOpts(path, WithChown(uid, gid), WithChmod(perms))
if err != nil {
t.Fatal(err)
}
p, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if p.Mode().Perm() != perms {
t.Fatalf("unexpected file permissions: expected: %#o, got: %#o", perms, p.Mode().Perm())
}
if stat, ok := p.Sys().(*syscall.Stat_t); ok {
if stat.Uid != uint32(uid) || stat.Gid != uint32(gid) {
t.Fatalf("unexpected file ownership: expected: %d:%d, got: %d:%d", uid, gid, stat.Uid, stat.Gid)
}
}
return l
}
14 changes: 14 additions & 0 deletions sockets/unix_socket_test_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package sockets

import (
"net"
"testing"
)

func createTestUnixSocket(t *testing.T, path string) (listener net.Listener) {
l, err := NewUnixSocketWithOpts(path)
if err != nil {
t.Fatal(err)
}
return l
}
28 changes: 28 additions & 0 deletions sockets/unix_socket_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//go:build !windows

package sockets

import (
"net"
"syscall"
)

func listenUnix(path string) (net.Listener, error) {
// net.Listen does not allow for permissions to be set. As a result, when
// specifying custom permissions ("WithChmod()"), there is a short time
// between creating the socket and applying the permissions, during which
// the socket permissions are Less restrictive than desired.
//
// To work around this limitation of net.Listen(), we temporarily set the
// umask to 0777, which forces the socket to be created with 000 permissions
// (i.e.: no access for anyone). After that, WithChmod() must be used to set
// the desired permissions.
//
// We don't use "defer" here, to reset the umask to its original value as soon
// as possible. Ideally we'd be able to detect if WithChmod() was passed as
// an option, and skip changing umask if default permissions are used.
origUmask := syscall.Umask(0o777)
l, err := net.Listen("unix", path)
syscall.Umask(origUmask)
return l, err
}
7 changes: 7 additions & 0 deletions sockets/unix_socket_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package sockets

import "net"

func listenUnix(path string) (net.Listener, error) {
return net.Listen("unix", path)
}

0 comments on commit d29df4a

Please sign in to comment.