Skip to content

Commit 951cd84

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 951cd84

File tree

3 files changed

+199
-4
lines changed

3 files changed

+199
-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: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,56 @@ import (
55
"testing"
66
)
77

8+
func TestGetSecurityDescriptor(t *testing.T) {
9+
t.Run("Default", func(t *testing.T) {
10+
sddl, err := getSecurityDescriptor()
11+
if err != nil {
12+
t.Error(err)
13+
}
14+
expected := BasePermissions
15+
if sddl != expected {
16+
t.Errorf("expected: %s, got: %s", expected, sddl)
17+
}
18+
})
19+
t.Run("Users", func(t *testing.T) {
20+
const name = "Users" // for testing, should always be available
21+
sddl, err := getSecurityDescriptor(name)
22+
if err != nil {
23+
t.Error(err)
24+
}
25+
// FIXME(thaJeztah): this may not be a reproducible SID; probably should do some fuzzy matching.
26+
expected := "D:P(A;;GA;;;BA)(A;;GA;;;SY)(A;;GRGW;;;S-1-5-32-545"
27+
if sddl != expected {
28+
t.Errorf("expected: %s, got: %s", expected, sddl)
29+
}
30+
})
31+
32+
// TODO(thaJeztah): should this fail on duplicate users?
33+
t.Run("Users twice", func(t *testing.T) {
34+
const name = "Users" // for testing, should always be available
35+
sddl, err := getSecurityDescriptor(name, name)
36+
if err != nil {
37+
t.Error(err)
38+
}
39+
// FIXME(thaJeztah): this may not be a reproducible SID; probably should do some fuzzy matching.
40+
expected := "D:P(A;;GA;;;BA)(A;;GA;;;SY)(A;;GRGW;;;S-1-5-32-545)(A;;GRGW;;;S-1-5-32-545)"
41+
if sddl != expected {
42+
t.Errorf("expected: %s, got: %s", expected, sddl)
43+
}
44+
})
45+
t.Run("NoSuchUserOrGroup", func(t *testing.T) {
46+
const name = "NoSuchUserOrGroup" // non-existing user or group
47+
sddl, err := getSecurityDescriptor(name)
48+
if err != nil {
49+
t.Error(err)
50+
}
51+
expected := "hello"
52+
if sddl != expected {
53+
t.Errorf("expected: %s, got: %s", expected, sddl)
54+
}
55+
})
56+
}
57+
858
func TestUnixSocketWithOpts(t *testing.T) {
959
socketFile, err := os.CreateTemp("", "test*.sock")
1060
if err != nil {
@@ -22,3 +72,25 @@ func TestUnixSocketWithOpts(t *testing.T) {
2272
echoStr := "hello"
2373
runTest(t, socketFile.Name(), l, echoStr)
2474
}
75+
76+
func TestNewUnixSocket(t *testing.T) {
77+
group := "Users" // for testing, should always be available
78+
path := "/tmp/test.sock"
79+
echoStr := "hello"
80+
l, err := NewUnixSocket(path, []string{group})
81+
if err != nil {
82+
t.Fatal(err)
83+
}
84+
defer func() { _ = l.Close() }()
85+
runTest(t, path, l, echoStr)
86+
}
87+
88+
func TestNewUnixSocketUnknownGroup(t *testing.T) {
89+
group := "NoSuchUserOrGroup"
90+
path := "/tmp/fail.sock"
91+
_, err := NewUnixSocket(path, []string{group})
92+
if err == nil {
93+
t.Errorf("expected error, got nil")
94+
}
95+
_ = os.Remove(path)
96+
}

0 commit comments

Comments
 (0)