WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 7c22d0d

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 c296721 commit 7c22d0d

File tree

3 files changed

+216
-6
lines changed

3 files changed

+216
-6
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: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,137 @@
11
package sockets
22

3-
import "net"
3+
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
"path/filepath"
8+
"strings"
49

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

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)