diff --git a/opts/swarmopts/port_test.go b/opts/swarmopts/port_test.go index 12eb7b653418..5ccf28052cb0 100644 --- a/opts/swarmopts/port_test.go +++ b/opts/swarmopts/port_test.go @@ -318,8 +318,7 @@ func TestPortOptInvalidSimpleSyntax(t *testing.T) { }, { value: "", - expectedError: "invalid proto: ", - // expectedError: "no port specified: ", // FIXME(thaJeztah): re-enable once https://github.com/docker/go-connections/pull/143 is in a go-connections release. + expectedError: "no port specified: ", }, { value: "1.1.1.1:80:80", diff --git a/vendor.mod b/vendor.mod index 0aed9952bf7d..350508089a42 100644 --- a/vendor.mod +++ b/vendor.mod @@ -19,7 +19,7 @@ require ( github.com/docker/cli-docs-tool v0.11.0 github.com/docker/distribution v2.8.3+incompatible github.com/docker/docker-credential-helpers v0.9.5 - github.com/docker/go-connections v0.6.0 + github.com/docker/go-connections v0.7.0 github.com/docker/go-units v0.5.0 github.com/fvbommel/sortorder v1.1.0 github.com/go-jose/go-jose/v4 v4.1.4 diff --git a/vendor.sum b/vendor.sum index f42edf7f558e..42eed653af9e 100644 --- a/vendor.sum +++ b/vendor.sum @@ -42,8 +42,8 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-events v0.0.0-20250808211157-605354379745 h1:yOn6Ze6IbYI/KAw2lw/83ELYvZh6hvsygTVkD0dzMC4= github.com/docker/go-events v0.0.0-20250808211157-605354379745/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= diff --git a/vendor/github.com/docker/go-connections/nat/nat.go b/vendor/github.com/docker/go-connections/nat/nat.go index 1ffe0355dc15..d46c06a27ef5 100644 --- a/vendor/github.com/docker/go-connections/nat/nat.go +++ b/vendor/github.com/docker/go-connections/nat/nat.go @@ -27,19 +27,15 @@ type PortSet map[Port]struct{} type Port string // NewPort creates a new instance of a Port given a protocol and port number or port range -func NewPort(proto, port string) (Port, error) { - // Check for parsing issues on "port" now so we can avoid having - // to check it later on. - - portStartInt, portEndInt, err := ParsePortRangeToInt(port) +func NewPort(proto, portOrRange string) (Port, error) { + start, end, err := parsePortRange(portOrRange) if err != nil { return "", err } - - if portStartInt == portEndInt { - return Port(fmt.Sprintf("%d/%s", portStartInt, proto)), nil + if start == end { + return Port(fmt.Sprintf("%d/%s", start, proto)), nil } - return Port(fmt.Sprintf("%d-%d/%s", portStartInt, portEndInt, proto)), nil + return Port(fmt.Sprintf("%d-%d/%s", start, end, proto)), nil } // ParsePort parses the port number string and returns an int @@ -47,49 +43,53 @@ func ParsePort(rawPort string) (int, error) { if rawPort == "" { return 0, nil } - port, err := strconv.ParseUint(rawPort, 10, 16) + port, err := parsePortNumber(rawPort) if err != nil { - return 0, fmt.Errorf("invalid port '%s': %w", rawPort, errors.Unwrap(err)) + return 0, fmt.Errorf("invalid port '%s': %w", rawPort, err) } - return int(port), nil + return port, nil } // ParsePortRangeToInt parses the port range string and returns start/end ints -func ParsePortRangeToInt(rawPort string) (int, int, error) { +func ParsePortRangeToInt(rawPort string) (startPort, endPort int, _ error) { if rawPort == "" { + // TODO(thaJeztah): consider making this an error; this was kept to keep existing behavior. return 0, 0, nil } - start, end, err := ParsePortRange(rawPort) - if err != nil { - return 0, 0, err - } - return int(start), int(end), nil + return parsePortRange(rawPort) } // Proto returns the protocol of a Port func (p Port) Proto() string { - proto, _ := SplitProtoPort(string(p)) + _, proto, _ := strings.Cut(string(p), "/") + if proto == "" { + proto = "tcp" + } return proto } // Port returns the port number of a Port func (p Port) Port() string { - _, port := SplitProtoPort(string(p)) + port, _, _ := strings.Cut(string(p), "/") return port } -// Int returns the port number of a Port as an int +// Int returns the port number of a Port as an int. It assumes [Port] +// is valid, and returns 0 otherwise. func (p Port) Int() int { - portStr := p.Port() // We don't need to check for an error because we're going to - // assume that any error would have been found, and reported, in NewPort() - port, _ := ParsePort(portStr) + // assume that any error would have been found, and reported, in [NewPort] + port, _ := parsePortNumber(p.Port()) return port } // Range returns the start/end port numbers of a Port range as ints func (p Port) Range() (int, int, error) { - return ParsePortRangeToInt(p.Port()) + portRange := p.Port() + if portRange == "" { + return 0, 0, nil + } + return parsePortRange(portRange) } // SplitProtoPort splits a port(range) and protocol, formatted as "/[]" @@ -173,6 +173,10 @@ func splitParts(rawport string) (hostIP, hostPort, containerPort string) { func ParsePortSpec(rawPort string) ([]PortMapping, error) { ip, hostPort, containerPort := splitParts(rawPort) proto, containerPort := SplitProtoPort(containerPort) + if containerPort == "" { + return nil, fmt.Errorf("no port specified: %s", rawPort) + } + proto = strings.ToLower(proto) if err := validateProto(proto); err != nil { return nil, err @@ -189,18 +193,15 @@ func ParsePortSpec(rawPort string) ([]PortMapping, error) { if ip != "" && net.ParseIP(ip) == nil { return nil, errors.New("invalid IP address: " + ip) } - if containerPort == "" { - return nil, fmt.Errorf("no port specified: %s", rawPort) - } - startPort, endPort, err := ParsePortRange(containerPort) + startPort, endPort, err := parsePortRange(containerPort) if err != nil { return nil, errors.New("invalid containerPort: " + containerPort) } - var startHostPort, endHostPort uint64 + var startHostPort, endHostPort int if hostPort != "" { - startHostPort, endHostPort, err = ParsePortRange(hostPort) + startHostPort, endHostPort, err = parsePortRange(hostPort) if err != nil { return nil, errors.New("invalid hostPort: " + hostPort) } @@ -217,19 +218,18 @@ func ParsePortSpec(rawPort string) ([]PortMapping, error) { count := endPort - startPort + 1 ports := make([]PortMapping, 0, count) - for i := uint64(0); i < count; i++ { - cPort := Port(strconv.FormatUint(startPort+i, 10) + "/" + proto) + for i := range count { hPort := "" if hostPort != "" { - hPort = strconv.FormatUint(startHostPort+i, 10) + hPort = strconv.Itoa(startHostPort + i) // Set hostPort to a range only if there is a single container port // and a dynamic host port. if count == 1 && startHostPort != endHostPort { - hPort += "-" + strconv.FormatUint(endHostPort, 10) + hPort += "-" + strconv.Itoa(endHostPort) } } ports = append(ports, PortMapping{ - Port: cPort, + Port: Port(strconv.Itoa(startPort+i) + "/" + proto), Binding: PortBinding{HostIP: ip, HostPort: hPort}, }) } diff --git a/vendor/github.com/docker/go-connections/nat/parse.go b/vendor/github.com/docker/go-connections/nat/parse.go index 64affa2a904c..f6f86bd04fe3 100644 --- a/vendor/github.com/docker/go-connections/nat/parse.go +++ b/vendor/github.com/docker/go-connections/nat/parse.go @@ -2,32 +2,59 @@ package nat import ( "errors" + "fmt" "strconv" "strings" ) -// ParsePortRange parses and validates the specified string as a port-range (8000-9000) -func ParsePortRange(ports string) (uint64, uint64, error) { +// ParsePortRange parses and validates the specified string as a port range (e.g., "8000-9000"). +func ParsePortRange(ports string) (startPort, endPort uint64, _ error) { + start, end, err := parsePortRange(ports) + return uint64(start), uint64(end), err +} + +// parsePortRange parses and validates the specified string as a port range (e.g., "8000-9000"). +func parsePortRange(ports string) (startPort, endPort int, _ error) { if ports == "" { return 0, 0, errors.New("empty string specified for ports") } - if !strings.Contains(ports, "-") { - start, err := strconv.ParseUint(ports, 10, 16) - end := start - return start, end, err + start, end, ok := strings.Cut(ports, "-") + + startPort, err := parsePortNumber(start) + if err != nil { + return 0, 0, fmt.Errorf("invalid start port '%s': %w", start, err) + } + if !ok || start == end { + return startPort, startPort, nil } - parts := strings.Split(ports, "-") - start, err := strconv.ParseUint(parts[0], 10, 16) + endPort, err = parsePortNumber(end) if err != nil { - return 0, 0, err + return 0, 0, fmt.Errorf("invalid end port '%s': %w", end, err) } - end, err := strconv.ParseUint(parts[1], 10, 16) + if endPort < startPort { + return 0, 0, errors.New("invalid port range: " + ports) + } + return startPort, endPort, nil +} + +// parsePortNumber parses rawPort into an int, unwrapping strconv errors +// and returning a single "out of range" error for any value outside 0–65535. +func parsePortNumber(rawPort string) (int, error) { + if rawPort == "" { + return 0, errors.New("value is empty") + } + port, err := strconv.ParseInt(rawPort, 10, 0) if err != nil { - return 0, 0, err + var numErr *strconv.NumError + if errors.As(err, &numErr) { + err = numErr.Err + } + return 0, err } - if end < start { - return 0, 0, errors.New("invalid range specified for port: " + ports) + if port < 0 || port > 65535 { + return 0, errors.New("value out of range (0–65535)") } - return start, end, nil + + return int(port), nil } diff --git a/vendor/github.com/docker/go-connections/nat/sort.go b/vendor/github.com/docker/go-connections/nat/sort.go index b6eed145e1cc..b983d011485b 100644 --- a/vendor/github.com/docker/go-connections/nat/sort.go +++ b/vendor/github.com/docker/go-connections/nat/sort.go @@ -34,8 +34,10 @@ func Sort(ports []Port, predicate func(i, j Port) bool) { } type portMapEntry struct { - port Port - binding PortBinding + port Port + binding *PortBinding + portInt int + portProto string } type portMapSorter []portMapEntry @@ -48,23 +50,36 @@ func (s portMapSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // 2. larger port // 3. port with tcp protocol func (s portMapSorter) Less(i, j int) bool { - pi, pj := s[i].port, s[j].port - hpi, hpj := toInt(s[i].binding.HostPort), toInt(s[j].binding.HostPort) - return hpi > hpj || pi.Int() > pj.Int() || (pi.Int() == pj.Int() && strings.ToLower(pi.Proto()) == "tcp") + pi, pj := s[i].portInt, s[j].portInt + var hpi, hpj int + if s[i].binding != nil { + hpi = toInt(s[i].binding.HostPort) + } + if s[j].binding != nil { + hpj = toInt(s[j].binding.HostPort) + } + return hpi > hpj || pi > pj || (pi == pj && strings.EqualFold(s[i].portProto, "tcp")) } // SortPortMap sorts the list of ports and their respected mapping. The ports // will explicit HostPort will be placed first. -func SortPortMap(ports []Port, bindings PortMap) { +func SortPortMap(ports []Port, bindings map[Port][]PortBinding) { s := portMapSorter{} for _, p := range ports { + portInt, portProto := p.Int(), p.Proto() if binding, ok := bindings[p]; ok && len(binding) > 0 { for _, b := range binding { - s = append(s, portMapEntry{port: p, binding: b}) + s = append(s, portMapEntry{ + port: p, binding: &b, + portInt: portInt, portProto: portProto, + }) } bindings[p] = []PortBinding{} } else { - s = append(s, portMapEntry{port: p}) + s = append(s, portMapEntry{ + port: p, + portInt: portInt, portProto: portProto, + }) } } @@ -81,16 +96,13 @@ func SortPortMap(ports []Port, bindings PortMap) { i++ } // reorder bindings for this port - if _, ok := bindings[entry.port]; ok { - bindings[entry.port] = append(bindings[entry.port], entry.binding) + if entry.binding != nil { + bindings[entry.port] = append(bindings[entry.port], *entry.binding) } } } -func toInt(s string) uint64 { - i, _, err := ParsePortRange(s) - if err != nil { - i = 0 - } +func toInt(s string) int { + i, _, _ := parsePortRange(s) return i } diff --git a/vendor/github.com/docker/go-connections/sockets/inmem_socket.go b/vendor/github.com/docker/go-connections/sockets/inmem_socket.go index 99846ffddb1a..06fcf747ac4b 100644 --- a/vendor/github.com/docker/go-connections/sockets/inmem_socket.go +++ b/vendor/github.com/docker/go-connections/sockets/inmem_socket.go @@ -1,48 +1,57 @@ package sockets import ( - "errors" "net" "sync" ) -var errClosed = errors.New("use of closed network connection") +// dummyAddr is used to satisfy net.Addr for the in-mem socket +// it is just stored as a string and returns the string for all calls +type dummyAddr string + +// Network returns the addr string, satisfies net.Addr +func (a dummyAddr) Network() string { + return string(a) +} -// InmemSocket implements net.Listener using in-memory only connections. +// String returns the string form +func (a dummyAddr) String() string { + return string(a) +} + +// InmemSocket implements [net.Listener] using in-memory only connections. type InmemSocket struct { chConn chan net.Conn chClose chan struct{} - addr string + addr dummyAddr mu sync.Mutex } -// dummyAddr is used to satisfy net.Addr for the in-mem socket -// it is just stored as a string and returns the string for all calls -type dummyAddr string - -// NewInmemSocket creates an in-memory only net.Listener -// The addr argument can be any string, but is used to satisfy the `Addr()` part -// of the net.Listener interface +// NewInmemSocket creates an in-memory only [net.Listener]. The addr argument +// can be any string, but is used to satisfy the [net.Listener.Addr] part +// of the [net.Listener] interface func NewInmemSocket(addr string, bufSize int) *InmemSocket { return &InmemSocket{ chConn: make(chan net.Conn, bufSize), chClose: make(chan struct{}), - addr: addr, + addr: dummyAddr(addr), } } // Addr returns the socket's addr string to satisfy net.Listener func (s *InmemSocket) Addr() net.Addr { - return dummyAddr(s.addr) + return s.addr } -// Accept implements the Accept method in the Listener interface; it waits for the next call and returns a generic Conn. +// Accept implements the Accept method in the Listener interface; it waits +// for the next call and returns a generic Conn. It returns a [net.ErrClosed] +// if the connection is already closed. func (s *InmemSocket) Accept() (net.Conn, error) { select { case conn := <-s.chConn: return conn, nil case <-s.chClose: - return nil, errClosed + return nil, net.ErrClosed } } @@ -58,24 +67,15 @@ func (s *InmemSocket) Close() error { return nil } -// Dial is used to establish a connection with the in-mem server +// Dial is used to establish a connection with the in-mem server. +// It returns a [net.ErrClosed] if the connection is already closed. func (s *InmemSocket) Dial(network, addr string) (net.Conn, error) { srvConn, clientConn := net.Pipe() select { case s.chConn <- srvConn: case <-s.chClose: - return nil, errClosed + return nil, net.ErrClosed } return clientConn, nil } - -// Network returns the addr string, satisfies net.Addr -func (a dummyAddr) Network() string { - return string(a) -} - -// String returns the string form -func (a dummyAddr) String() string { - return string(a) -} diff --git a/vendor/github.com/docker/go-connections/sockets/proxy.go b/vendor/github.com/docker/go-connections/sockets/proxy.go deleted file mode 100644 index f04980e40a5a..000000000000 --- a/vendor/github.com/docker/go-connections/sockets/proxy.go +++ /dev/null @@ -1,31 +0,0 @@ -package sockets - -import ( - "net" - "os" - "strings" -) - -// GetProxyEnv allows access to the uppercase and the lowercase forms of -// proxy-related variables. See the Go specification for details on these -// variables. https://golang.org/pkg/net/http/ -// -// Deprecated: this function was used as helper for [DialerFromEnvironment] and is no longer used. It will be removed in the next release. -func GetProxyEnv(key string) string { - proxyValue := os.Getenv(strings.ToUpper(key)) - if proxyValue == "" { - return os.Getenv(strings.ToLower(key)) - } - return proxyValue -} - -// DialerFromEnvironment was previously used to configure a net.Dialer to route -// connections through a SOCKS proxy. -// -// Deprecated: SOCKS proxies are now supported by configuring only -// http.Transport.Proxy, and no longer require changing http.Transport.Dial. -// Therefore, only [sockets.ConfigureTransport] needs to be called, and any -// [sockets.DialerFromEnvironment] calls can be dropped. -func DialerFromEnvironment(direct *net.Dialer) (*net.Dialer, error) { - return direct, nil -} diff --git a/vendor/github.com/docker/go-connections/sockets/sockets.go b/vendor/github.com/docker/go-connections/sockets/sockets.go index 6117297860db..0d7789bbdbda 100644 --- a/vendor/github.com/docker/go-connections/sockets/sockets.go +++ b/vendor/github.com/docker/go-connections/sockets/sockets.go @@ -27,11 +27,19 @@ var ErrProtocolNotAvailable = errors.New("protocol not available") // make sure you do it _after_ any subsequent calls to ConfigureTransport is made against the same // [http.Transport]. func ConfigureTransport(tr *http.Transport, proto, addr string) error { + if tr.MaxIdleConns == 0 { + // prevent long-lived processes from leaking connections + // due to idle connections not being released. + // + // TODO: see if we can also address this from the server side; see: https://github.com/moby/moby/issues/45539 + tr.MaxIdleConns = 6 + tr.IdleConnTimeout = 30 * time.Second + } switch proto { case "unix": - return configureUnixTransport(tr, proto, addr) + return configureUnixTransport(tr, addr) case "npipe": - return configureNpipeTransport(tr, proto, addr) + return configureNpipeTransport(tr, addr) default: tr.Proxy = http.ProxyFromEnvironment tr.DisableCompression = false @@ -42,15 +50,7 @@ func ConfigureTransport(tr *http.Transport, proto, addr string) error { return nil } -// DialPipe connects to a Windows named pipe. It is not supported on -// non-Windows platforms. -// -// Deprecated: use [github.com/Microsoft/go-winio.DialPipe] or [github.com/Microsoft/go-winio.DialPipeContext]. -func DialPipe(addr string, timeout time.Duration) (net.Conn, error) { - return dialPipe(addr, timeout) -} - -func configureUnixTransport(tr *http.Transport, proto, addr string) error { +func configureUnixTransport(tr *http.Transport, addr string) error { if len(addr) > maxUnixSocketPathSize { return fmt.Errorf("unix socket path %q is too long", addr) } @@ -60,7 +60,7 @@ func configureUnixTransport(tr *http.Transport, proto, addr string) error { Timeout: defaultTimeout, } tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { - return dialer.DialContext(ctx, proto, addr) + return dialer.DialContext(ctx, "unix", addr) } return nil } diff --git a/vendor/github.com/docker/go-connections/sockets/sockets_unix.go b/vendor/github.com/docker/go-connections/sockets/sockets_unix.go index 913d2f00dd2f..b37c39eab828 100644 --- a/vendor/github.com/docker/go-connections/sockets/sockets_unix.go +++ b/vendor/github.com/docker/go-connections/sockets/sockets_unix.go @@ -2,17 +2,6 @@ package sockets -import ( - "net" - "net/http" - "syscall" - "time" -) - -func configureNpipeTransport(tr *http.Transport, proto, addr string) error { +func configureNpipeTransport(any, string) error { return ErrProtocolNotAvailable } - -func dialPipe(_ string, _ time.Duration) (net.Conn, error) { - return nil, syscall.EAFNOSUPPORT -} diff --git a/vendor/github.com/docker/go-connections/sockets/sockets_windows.go b/vendor/github.com/docker/go-connections/sockets/sockets_windows.go index 6d6beb3855c0..0863fc36a792 100644 --- a/vendor/github.com/docker/go-connections/sockets/sockets_windows.go +++ b/vendor/github.com/docker/go-connections/sockets/sockets_windows.go @@ -4,12 +4,11 @@ import ( "context" "net" "net/http" - "time" "github.com/Microsoft/go-winio" ) -func configureNpipeTransport(tr *http.Transport, proto, addr string) error { +func configureNpipeTransport(tr *http.Transport, addr string) error { // No need for compression in local communications. tr.DisableCompression = true tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { @@ -17,7 +16,3 @@ func configureNpipeTransport(tr *http.Transport, proto, addr string) error { } return nil } - -func dialPipe(addr string, timeout time.Duration) (net.Conn, error) { - return winio.DialPipe(addr, &timeout) -} diff --git a/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go b/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go index 5ec29e059e78..01aee5f11a5b 100644 --- a/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go +++ b/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go @@ -1,6 +1,128 @@ package sockets -import "net" +import ( + "errors" + "fmt" + "net" + "strings" + + "github.com/Microsoft/go-winio" + "golang.org/x/sys/windows" +) + +// BasePermissions defines the default DACL, which allows Administrators +// and LocalSystem full access (similar to defaults used in [moby]); +// +// - D:P: DACL without inheritance (protected, (P)). +// - (A;;GA;;;BA): Allow full access (GA) for built-in Administrators (BA). +// - (A;;GA;;;SY); Allow full access (GA) for LocalSystem (SY). +// - Any other user is denied access. +// +// [moby]: https://github.com/moby/moby/blob/6b45c76a233b1b8b56465f76c21c09fd7920e82d/daemon/listeners/listeners_windows.go#L53-L59 +const BasePermissions = "D:P(A;;GA;;;BA)(A;;GA;;;SY)" + +// WithBasePermissions sets a default DACL, which allows Administrators +// and LocalSystem full access (similar to defaults used in [moby]); +// +// - D:P: DACL without inheritance (protected, (P)). +// - (A;;GA;;;BA): Allow full access (GA) for built-in Administrators (BA). +// - (A;;GA;;;SY); Allow full access (GA) for LocalSystem (SY). +// - Any other user is denied access. +// +// [moby]: https://github.com/moby/moby/blob/6b45c76a233b1b8b56465f76c21c09fd7920e82d/daemon/listeners/listeners_windows.go#L53-L59 +func WithBasePermissions() SockOption { + return withSDDL(BasePermissions) +} + +// WithAdditionalUsersAndGroups modifies the socket file's DACL to grant +// access to additional users and groups. +// +// It sets [BasePermissions] on the socket path and grants the given additional +// users and groups to generic read (GR) and write (GW) access. It returns +// an error if no groups were given, when failing to resolve any of the +// additional users and groups, or when failing to apply the ACL. +func WithAdditionalUsersAndGroups(additionalUsersAndGroups []string) SockOption { + return func(path string) error { + if len(additionalUsersAndGroups) == 0 { + return errors.New("no additional users specified") + } + sd, err := getSecurityDescriptor(additionalUsersAndGroups...) + if err != nil { + return fmt.Errorf("looking up SID: %w", err) + } + return withSDDL(sd)(path) + } +} + +// withSDDL applies the given SDDL to the socket. It returns an error +// when failing parse the SDDL, or if the DACL was defaulted. +// +// TODO(thaJeztah); this is not exported yet, as some of the checks may need review if they're not too opinionated. +func withSDDL(sddl string) SockOption { + return func(path string) error { + sd, err := windows.SecurityDescriptorFromString(sddl) + if err != nil { + return fmt.Errorf("parsing SDDL: %w", err) + } + dacl, defaulted, err := sd.DACL() + if err != nil { + return fmt.Errorf("extracting DACL: %w", err) + } + if dacl == nil || defaulted { + // should never be hit with our [DefaultPermissions], + // as it contains "D:" and "P" (protected, don't inherit). + return errors.New("no DACL found in security descriptor or defaulted") + } + return windows.SetNamedSecurityInfo( + path, + windows.SE_FILE_OBJECT, + windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION, + nil, // do not change the owner + nil, // do not change the owner + dacl, + nil, + ) + } +} + +// NewUnixSocket creates a new unix socket. +// +// It sets [BasePermissions] on the socket path and grants the given additional +// users and groups to generic read (GR) and write (GW) access. It returns +// an error when failing to resolve any of the additional users and groups, +// or when failing to apply the ACL. +func NewUnixSocket(path string, additionalUsersAndGroups []string) (net.Listener, error) { + var opts []SockOption + if len(additionalUsersAndGroups) > 0 { + opts = append(opts, WithAdditionalUsersAndGroups(additionalUsersAndGroups)) + } else { + opts = append(opts, WithBasePermissions()) + } + return NewUnixSocketWithOpts(path, opts...) +} + +// getSecurityDescriptor returns the DACL for the Unix socket. +// +// By default, it grants [BasePermissions], but allows for additional +// users and groups to get generic read (GR) and write (GW) access. It +// returns an error when failing to resolve any of the additional users +// and groups. +func getSecurityDescriptor(additionalUsersAndGroups ...string) (string, error) { + sddl := BasePermissions + + // Grant generic read (GR) and write (GW) access to whatever + // additional users or groups were specified. + // + // TODO(thaJeztah): should we fail on, or remove duplicates? + for _, g := range additionalUsersAndGroups { + sid, err := winio.LookupSidByName(strings.TrimSpace(g)) + if err != nil { + return "", fmt.Errorf("looking up SID: %w", err) + } + sddl += fmt.Sprintf("(A;;GRGW;;;%s)", sid) + } + return sddl, nil +} func listenUnix(path string) (net.Listener, error) { return net.Listen("unix", path) diff --git a/vendor/github.com/docker/go-connections/tlsconfig/certpool.go b/vendor/github.com/docker/go-connections/tlsconfig/certpool.go index f84c624ba0ba..803f1e122ead 100644 --- a/vendor/github.com/docker/go-connections/tlsconfig/certpool.go +++ b/vendor/github.com/docker/go-connections/tlsconfig/certpool.go @@ -1,16 +1,12 @@ package tlsconfig -import ( - "crypto/x509" - "runtime" -) +import "crypto/x509" -// SystemCertPool returns a copy of the system cert pool, -// returns an error if failed to load or empty pool on windows. +// SystemCertPool returns a copy of the system cert pool. +// +// Deprecated: use [x509.SystemCertPool] instead. +// +//go:fix inline func SystemCertPool() (*x509.CertPool, error) { - certpool, err := x509.SystemCertPool() - if err != nil && runtime.GOOS == "windows" { - return x509.NewCertPool(), nil - } - return certpool, err + return x509.SystemCertPool() } diff --git a/vendor/github.com/docker/go-connections/tlsconfig/config.go b/vendor/github.com/docker/go-connections/tlsconfig/config.go index 8b0264f68b75..761b36bb825e 100644 --- a/vendor/github.com/docker/go-connections/tlsconfig/config.go +++ b/vendor/github.com/docker/go-connections/tlsconfig/config.go @@ -34,6 +34,9 @@ type Options struct { // the system pool will be used. ExclusiveRootPools bool MinVersion uint16 + + // systemCertPool allows mocking the system cert-pool for testing. + systemCertPool func() (*x509.CertPool, error) } // DefaultServerAcceptedCiphers should be uses by code which already has a crypto/tls @@ -47,6 +50,8 @@ var defaultCipherSuites = []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, } // ServerDefault returns a secure-enough TLS configuration for the server TLS configuration. @@ -75,26 +80,33 @@ func defaultConfig(ops ...func(*tls.Config)) *tls.Config { } // certPool returns an X.509 certificate pool from `caFile`, the certificate file. -func certPool(caFile string, exclusivePool bool) (*x509.CertPool, error) { +func certPool(opts Options) (*x509.CertPool, error) { // If we should verify the server, we need to load a trusted ca var ( pool *x509.CertPool err error ) - if exclusivePool { + if opts.ExclusiveRootPools { pool = x509.NewCertPool() } else { - pool, err = SystemCertPool() + if opts.systemCertPool != nil { + pool, err = opts.systemCertPool() + } else { + pool, err = x509.SystemCertPool() + } if err != nil { return nil, fmt.Errorf("failed to read system certificates: %v", err) } } - pemData, err := os.ReadFile(caFile) + if opts.CAFile == "" { + return pool, nil + } + pemData, err := os.ReadFile(opts.CAFile) if err != nil { - return nil, fmt.Errorf("could not read CA certificate %q: %v", caFile, err) + return nil, fmt.Errorf("could not read CA certificate %q: %v", opts.CAFile, err) } if !pool.AppendCertsFromPEM(pemData) { - return nil, fmt.Errorf("failed to append certificates from PEM file: %q", caFile) + return nil, fmt.Errorf("failed to append certificates from PEM file: %q", opts.CAFile) } return pool, nil } @@ -197,7 +209,7 @@ func Client(options Options) (*tls.Config, error) { tlsConfig := defaultConfig() tlsConfig.InsecureSkipVerify = options.InsecureSkipVerify if !options.InsecureSkipVerify && options.CAFile != "" { - CAs, err := certPool(options.CAFile, options.ExclusiveRootPools) + CAs, err := certPool(options) if err != nil { return nil, err } @@ -230,7 +242,7 @@ func Server(options Options) (*tls.Config, error) { } tlsConfig.Certificates = []tls.Certificate{tlsCert} if options.ClientAuth >= tls.VerifyClientCertIfGiven && options.CAFile != "" { - CAs, err := certPool(options.CAFile, options.ExclusiveRootPools) + CAs, err := certPool(options) if err != nil { return nil, err } diff --git a/vendor/modules.txt b/vendor/modules.txt index 8f23ed417f94..473202dcd40a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -72,8 +72,8 @@ github.com/docker/distribution/registry/storage/cache/memory ## explicit; go 1.21 github.com/docker/docker-credential-helpers/client github.com/docker/docker-credential-helpers/credentials -# github.com/docker/go-connections v0.6.0 -## explicit; go 1.18 +# github.com/docker/go-connections v0.7.0 +## explicit; go 1.23 github.com/docker/go-connections/nat github.com/docker/go-connections/sockets github.com/docker/go-connections/tlsconfig