Skip to content

Commit a465fac

Browse files
committed
sockets: implement WithAdditionalUsersAndGroups for windows
- Implement a WithAdditionalUsersAndGroups (windows daemon allows specifying multiple additional users and groups for named pipes and unix-sockets). - Implement a WithBasePermissions() option for windows - Implement NewUnixSocket that accepts (optional) additional users and groups. Signed-off-by: Sebastiaan van Stijn <[email protected]>
1 parent 9ffab7e commit a465fac

File tree

3 files changed

+206
-4
lines changed

3 files changed

+206
-4
lines changed

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module github.com/docker/go-connections
22

33
go 1.18
44

5-
require github.com/Microsoft/go-winio v0.4.21
6-
7-
require golang.org/x/sys v0.1.0 // indirect
5+
require (
6+
github.com/Microsoft/go-winio v0.4.21
7+
golang.org/x/sys v0.1.0
8+
)

sockets/unix_socket_windows.go

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,128 @@
11
package sockets
22

3-
import "net"
3+
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
"strings"
8+
9+
"github.com/Microsoft/go-winio"
10+
"golang.org/x/sys/windows"
11+
)
12+
13+
// BasePermissions defines the default DACL, which allows Administrators
14+
// and LocalSystem full access (similar to defaults used in [moby]);
15+
//
16+
// - D:P: DACL without inheritance (protected, (P)).
17+
// - (A;;GA;;;BA): Allow full access (GA) for built-in Administrators (BA).
18+
// - (A;;GA;;;SY); Allow full access (GA) for LocalSystem (SY).
19+
// - Any other user is denied access.
20+
//
21+
// [moby]: https://github.com/moby/moby/blob/6b45c76a233b1b8b56465f76c21c09fd7920e82d/daemon/listeners/listeners_windows.go#L53-L59
22+
const BasePermissions = "D:P(A;;GA;;;BA)(A;;GA;;;SY)"
23+
24+
// WithBasePermissions sets a default DACL, which allows Administrators
25+
// and LocalSystem full access (similar to defaults used in [moby]);
26+
//
27+
// - D:P: DACL without inheritance (protected, (P)).
28+
// - (A;;GA;;;BA): Allow full access (GA) for built-in Administrators (BA).
29+
// - (A;;GA;;;SY); Allow full access (GA) for LocalSystem (SY).
30+
// - Any other user is denied access.
31+
//
32+
// [moby]: https://github.com/moby/moby/blob/6b45c76a233b1b8b56465f76c21c09fd7920e82d/daemon/listeners/listeners_windows.go#L53-L59
33+
func WithBasePermissions() SockOption {
34+
return withSDDL(BasePermissions)
35+
}
36+
37+
// WithAdditionalUsersAndGroups modifies the socket file's DACL to grant
38+
// access to additional users and groups.
39+
//
40+
// It sets [BasePermissions] on the socket path and grants the given additional
41+
// users and groups to generic read (GR) and write (GW) access. It returns
42+
// an error if no groups were given, when failing to resolve any of the
43+
// additional users and groups, or when failing to apply the ACL.
44+
func WithAdditionalUsersAndGroups(additionalUsersAndGroups []string) SockOption {
45+
return func(path string) error {
46+
if len(additionalUsersAndGroups) == 0 {
47+
return errors.New("no additional users specified")
48+
}
49+
sd, err := getSecurityDescriptor(additionalUsersAndGroups...)
50+
if err != nil {
51+
return fmt.Errorf("looking up SID: %w", err)
52+
}
53+
return withSDDL(sd)(path)
54+
}
55+
}
56+
57+
// withSDDL applies the given SDDL to the socket. It returns an error
58+
// when failing parse the SDDL, or if the DACL was defaulted.
59+
//
60+
// TODO(thaJeztah); this is not exported yet, as some of the checks may need review if they're not too opinionated.
61+
func withSDDL(sddl string) SockOption {
62+
return func(path string) error {
63+
sd, err := windows.SecurityDescriptorFromString(sddl)
64+
if err != nil {
65+
return fmt.Errorf("parsing SDDL: %w", err)
66+
}
67+
dacl, defaulted, err := sd.DACL()
68+
if err != nil {
69+
return fmt.Errorf("extracting DACL: %w", err)
70+
}
71+
if dacl == nil || defaulted {
72+
// should never be hit with our [DefaultPermissions],
73+
// as it contains "D:" and "P" (protected, don't inherit).
74+
return errors.New("no DACL found in security descriptor or defaulted")
75+
}
76+
return windows.SetNamedSecurityInfo(
77+
path,
78+
windows.SE_FILE_OBJECT,
79+
windows.DACL_SECURITY_INFORMATION|windows.OWNER_SECURITY_INFORMATION,
80+
nil, // do not change the owner
81+
nil, // do not change the owner
82+
dacl,
83+
nil,
84+
)
85+
}
86+
}
87+
88+
// NewUnixSocket creates a new unix socket.
89+
//
90+
// It sets [BasePermissions] on the socket path and grants the given additional
91+
// users and groups to generic read (GR) and write (GW) access. It returns
92+
// an error when failing to resolve any of the additional users and groups,
93+
// or when failing to apply the ACL.
94+
func NewUnixSocket(path string, additionalUsersAndGroups []string) (net.Listener, error) {
95+
var opts []SockOption
96+
if len(additionalUsersAndGroups) > 0 {
97+
opts = append(opts, WithAdditionalUsersAndGroups(additionalUsersAndGroups))
98+
} else {
99+
opts = append(opts, WithBasePermissions())
100+
}
101+
return NewUnixSocketWithOpts(path, opts...)
102+
}
103+
104+
// getSecurityDescriptor returns the DACL for the Unix socket.
105+
//
106+
// By default, it grants [BasePermissions], but allows for additional
107+
// users and groups to get generic read (GR) and write (GW) access. It
108+
// returns an error when failing to resolve any of the additional users
109+
// and groups.
110+
func getSecurityDescriptor(additionalUsersAndGroups ...string) (string, error) {
111+
sddl := BasePermissions
112+
113+
// Grant generic read (GR) and write (GW) access to whatever
114+
// additional users or groups were specified.
115+
//
116+
// TODO(thaJeztah): should we fail on, or remove duplicates?
117+
for _, g := range additionalUsersAndGroups {
118+
sid, err := winio.LookupSidByName(strings.TrimSpace(g))
119+
if err != nil {
120+
return "", fmt.Errorf("looking up SID: %w", err)
121+
}
122+
sddl += fmt.Sprintf("(A;;GRGW;;;%s)", sid)
123+
}
124+
return sddl, nil
125+
}
4126

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

sockets/unix_socket_windows_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,64 @@ package sockets
22

33
import (
44
"os"
5+
"path/filepath"
56
"testing"
67
)
78

9+
func TestGetSecurityDescriptor(t *testing.T) {
10+
t.Run("Default", func(t *testing.T) {
11+
sddl, err := getSecurityDescriptor()
12+
if err != nil {
13+
t.Error(err)
14+
}
15+
expected := BasePermissions
16+
if sddl != expected {
17+
t.Errorf("expected: %s, got: %s", expected, sddl)
18+
}
19+
})
20+
t.Run("Users", func(t *testing.T) {
21+
const name = "Users" // for testing, should always be available
22+
sddl, err := getSecurityDescriptor(name)
23+
if err != nil {
24+
t.Error(err)
25+
}
26+
// FIXME(thaJeztah): this may not be a reproducible SID; probably should do some fuzzy matching.
27+
const expected = "D:P(A;;GA;;;BA)(A;;GA;;;SY)(A;;GRGW;;;S-1-5-32-545)"
28+
if sddl != expected {
29+
t.Errorf("expected: %s, got: %s", expected, sddl)
30+
}
31+
})
32+
33+
// TODO(thaJeztah): should this fail on duplicate users?
34+
t.Run("Users twice", func(t *testing.T) {
35+
const name = "Users" // for testing, should always be available
36+
sddl, err := getSecurityDescriptor(name, name)
37+
if err != nil {
38+
t.Error(err)
39+
}
40+
// FIXME(thaJeztah): this may not be a reproducible SID; probably should do some fuzzy matching.
41+
const expected = "D:P(A;;GA;;;BA)(A;;GA;;;SY)(A;;GRGW;;;S-1-5-32-545)(A;;GRGW;;;S-1-5-32-545)"
42+
if sddl != expected {
43+
t.Errorf("expected: %s, got: %s", expected, sddl)
44+
}
45+
})
46+
t.Run("NoSuchUserOrGroup", func(t *testing.T) {
47+
const name = "NoSuchUserOrGroup" // non-existing user or group
48+
sddl, err := getSecurityDescriptor(name)
49+
if sddl != "" {
50+
t.Errorf("expected an empty sddl, got: %s", sddl)
51+
}
52+
if err == nil {
53+
t.Error("expected error")
54+
}
55+
56+
const expected = "looking up SID: lookup account NoSuchUserOrGroup: not found"
57+
if errMsg := err.Error(); errMsg != expected {
58+
t.Errorf("expected: %s, got: %s", expected, errMsg)
59+
}
60+
})
61+
}
62+
863
func TestUnixSocketWithOpts(t *testing.T) {
964
socketFile, err := os.CreateTemp("", "test*.sock")
1065
if err != nil {
@@ -22,3 +77,27 @@ func TestUnixSocketWithOpts(t *testing.T) {
2277
echoStr := "hello"
2378
runTest(t, socketFile.Name(), l, echoStr)
2479
}
80+
81+
func TestNewUnixSocket(t *testing.T) {
82+
group := "Users" // for testing, should always be available
83+
socketPath := filepath.Join(os.TempDir(), "test.sock")
84+
defer func() { _ = os.Remove(socketPath) }()
85+
t.Logf("socketPath: %s, path length: %d", socketPath, len(socketPath))
86+
87+
l, err := NewUnixSocket(socketPath, []string{group})
88+
if err != nil {
89+
t.Fatal(err)
90+
}
91+
defer func() { _ = l.Close() }()
92+
runTest(t, socketPath, l, "hello")
93+
}
94+
95+
func TestNewUnixSocketUnknownGroup(t *testing.T) {
96+
group := "NoSuchUserOrGroup"
97+
socketPath := filepath.Join(os.TempDir(), "fail.sock")
98+
_, err := NewUnixSocket(socketPath, []string{group})
99+
_ = os.Remove(socketPath)
100+
if err == nil {
101+
t.Errorf("expected error, got nil")
102+
}
103+
}

0 commit comments

Comments
 (0)