vendor: github.com/docker/go-connections v0.8.0

- sockets: set socket permissions without overriding umask
- sockets: improve abstract Unix socket handling
- sockets: InmemSocket: add DialContext
- sockets: remove double error decoration
- sockets: test-enhancements and improve coverage

full diff: https://github.com/docker/go-connections/compare/v0.7.0...v0.8.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2026-07-25 19:47:59 +02:00
parent 9391c9889c
commit 4e6e8fe5c8
9 changed files with 292 additions and 64 deletions
+1 -1
View File
@@ -21,7 +21,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.8
github.com/docker/go-connections v0.7.0
github.com/docker/go-connections v0.8.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
+2 -2
View File
@@ -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.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q=
github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
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-connections v0.8.0 h1:T9UlP76qPLA/HaLrcC+s4Doqqv5XsWMMUGPF5Aih/k0=
github.com/docker/go-connections v0.8.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-events v0.0.0-20260608200158-dbf6103125a4 h1:Bj+mzWc7MJqqD0UzTaPmwszW3ttOVjSFi84ZU5l+2I0=
github.com/docker/go-events v0.0.0-20260608200158-dbf6103125a4/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
+37 -13
View File
@@ -1,21 +1,19 @@
package sockets
import (
"context"
"net"
"sync"
)
// 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
// inmemAddr is used to satisfy net.Addr for the in-memory socket.
type inmemAddr string
// Network returns the addr string, satisfies net.Addr
func (a dummyAddr) Network() string {
return string(a)
}
func (a inmemAddr) Network() string { return "inmem" }
// String returns the string form
func (a dummyAddr) String() string {
func (a inmemAddr) String() string {
return string(a)
}
@@ -23,7 +21,7 @@ func (a dummyAddr) String() string {
type InmemSocket struct {
chConn chan net.Conn
chClose chan struct{}
addr dummyAddr
addr inmemAddr
mu sync.Mutex
}
@@ -34,7 +32,7 @@ func NewInmemSocket(addr string, bufSize int) *InmemSocket {
return &InmemSocket{
chConn: make(chan net.Conn, bufSize),
chClose: make(chan struct{}),
addr: dummyAddr(addr),
addr: inmemAddr(addr),
}
}
@@ -67,15 +65,41 @@ func (s *InmemSocket) Close() error {
return nil
}
// Dial is used to establish a connection with the in-mem server.
// It returns a [net.ErrClosed] if the connection is already closed.
// Dial establishes a connection with the in-memory listener.
//
// The network and addr parameters are accepted for compatibility with
// conventional dialer APIs but are currently ignored.
//
// It is equivalent to calling DialContext with context.Background().
// It returns [net.ErrClosed] if the listener has already been closed.
func (s *InmemSocket) Dial(network, addr string) (net.Conn, error) {
return s.DialContext(context.Background(), network, addr)
}
// DialContext establishes a connection with the in-memory listener.
//
// The network and addr parameters are accepted for compatibility with
// conventional dialer APIs but are currently ignored.
//
// If ctx is canceled before the connection is established, DialContext
// returns the context error. It returns [net.ErrClosed] if the listener
// has already been closed.
func (s *InmemSocket) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
srvConn, clientConn := net.Pipe()
select {
case s.chConn <- srvConn:
return clientConn, nil
case <-ctx.Done():
_ = srvConn.Close()
_ = clientConn.Close()
return nil, ctx.Err()
case <-s.chClose:
_ = srvConn.Close()
_ = clientConn.Close()
return nil, net.ErrClosed
}
return clientConn, nil
}
+50 -19
View File
@@ -47,38 +47,69 @@ For example:
package sockets
import (
"errors"
"fmt"
"net"
"os"
"runtime"
"syscall"
)
const supportsAbstractSockets = runtime.GOOS == "linux"
// SockOption sets up socket file's creating option
type SockOption func(string) error
// NewUnixSocketWithOpts creates a unix socket with the specified options.
// By default, socket permissions are 0000 (i.e.: no access for anyone); pass
// WithChmod() and WithChown() to set the desired ownership and permissions.
// NewUnixSocketWithOpts creates a Unix socket with the specified options.
//
// This function temporarily changes the system's "umask" to 0777 to work around
// a race condition between creating the socket and setting its permissions. While
// this should only be for a short duration, it may affect other processes that
// create files/directories during that period.
// On Unix platforms, socket permissions are 0000 by default, i.e. no access
// for anyone. Pass WithChmod() and WithChown() to set the desired permissions
// and ownership.
//
// On Windows, the socket uses Windows ACLs. Pass WithBasePermissions() to allow
// Administrators and LocalSystem full access, or WithAdditionalUsersAndGroups()
// to also grant generic read and write access to additional users or groups.
//
// Abstract Unix sockets (Go's Linux-specific "@" shorthand and the native
// leading-NUL representation) are supported only on Linux. On other platforms,
// attempts to use abstract socket addresses return an error. Because abstract
// sockets have no filesystem representation, filesystem-specific socket
// options are not supported.
//
// On platforms without abstract Unix socket support, attempts to use abstract
// socket addresses return an error wrapping [errors.ErrUnsupported].
func NewUnixSocketWithOpts(path string, opts ...SockOption) (net.Listener, error) {
if isAbstractSocket(path) {
if !supportsAbstractSockets {
return nil, fmt.Errorf("abstract Unix socket %q is not supported on %s: %w", path, runtime.GOOS, errors.ErrUnsupported)
}
for _, opt := range opts {
if err := opt(path); err != nil {
return nil, err
}
}
return net.Listen("unix", path)
}
if err := syscall.Unlink(path); err != nil && !os.IsNotExist(err) {
return nil, err
}
l, err := listenUnix(path)
if err != nil {
return nil, err
}
return listenUnix(path, opts...)
}
for _, op := range opts {
if err := op(path); err != nil {
_ = l.Close()
return nil, err
}
}
return l, nil
// isAbstractSocket reports whether path is an abstract Unix socket address.
//
// Go recognizes two representations of abstract socket addresses:
//
// - On Linux, a path beginning with '@' is translated by the standard library
// to the kernel's native leading-NUL representation.
// See https://pkg.go.dev/net@go1.27rc2#UnixAddr.
//
// - A path beginning with a NUL byte uses the kernel's native representation
// directly. See https://github.com/golang/go/issues/78615.
//
// The interpretation of these addresses is platform-dependent; this helper only
// recognizes the syntax.
func isAbstractSocket(path string) bool {
return len(path) > 0 && (path[0] == '@' || path[0] == 0)
}
+25
View File
@@ -0,0 +1,25 @@
package sockets
import (
"os"
"strconv"
"strings"
"syscall"
)
// maxListenerBacklog returns the maximum length of the queue of pending
// connections for a listening socket.
//
// It is similar to in stdlib, but without the fallbacks for Kernel < 4.1.0;
// https://github.com/golang/go/blob/go1.26.3/src/net/sock_linux.go#L33-L53
func maxListenerBacklog() int {
b, err := os.ReadFile("/proc/sys/net/core/somaxconn")
if err != nil {
return syscall.SOMAXCONN
}
n, err := strconv.Atoi(strings.TrimSpace(string(b)))
if err != nil || n <= 0 {
return syscall.SOMAXCONN
}
return n
}
+39
View File
@@ -0,0 +1,39 @@
//go:build !linux && !windows
package sockets
import (
"runtime"
"syscall"
)
// maxListenerBacklog is similar to the equivalent in stdlib;
// https://github.com/golang/go/blob/go1.26.3/src/net/sock_bsd.go#L14-L39
func maxListenerBacklog() int {
var (
n uint32
err error
)
switch runtime.GOOS {
case "darwin", "ios":
n, err = syscall.SysctlUint32("kern.ipc.somaxconn")
case "freebsd":
n, err = syscall.SysctlUint32("kern.ipc.soacceptqueue")
case "netbsd":
// NOTE: NetBSD has no somaxconn-like kernel state so far
case "openbsd":
n, err = syscall.SysctlUint32("kern.somaxconn")
default:
return syscall.SOMAXCONN
}
if n == 0 || err != nil {
return syscall.SOMAXCONN
}
// FreeBSD stores the backlog in a uint16, as does Linux.
// Assume the other BSDs do too. Truncate number to avoid wrapping.
// See issue 5030.
if n > 1<<16-1 {
n = 1<<16 - 1
}
return int(n)
}
+113 -18
View File
@@ -3,14 +3,35 @@
package sockets
import (
"errors"
"fmt"
"net"
"os"
"sync"
"syscall"
)
// WithChown modifies the socket file's uid and gid
// defaultSocketPerms is the default permission mode applied to newly created
// Unix sockets. Sockets are created inaccessible by default; callers can
// override this by passing [WithChmod].
//
// TODO(thaJeztah): Consider changing the default to 0o600, making the socket usable by its owner by default.
const defaultSocketPerms os.FileMode = 0o000
// WithChown modifies the socket file's uid and gid.
//
// Abstract Unix sockets have no filesystem representation, so this option
// returns an error wrapping [errors.ErrUnsupported] when used with an abstract
// socket.
func WithChown(uid, gid int) SockOption {
return func(path string) error {
if isAbstractSocket(path) {
return &os.PathError{
Op: "chown",
Path: path,
Err: fmt.Errorf("abstract Unix sockets do not support filesystem permissions: %w", errors.ErrUnsupported),
}
}
if err := os.Chown(path, uid, gid); err != nil {
return err
}
@@ -19,8 +40,19 @@ func WithChown(uid, gid int) SockOption {
}
// WithChmod modifies socket file's access mode.
//
// Abstract Unix sockets have no filesystem representation, so this option
// returns an error wrapping [errors.ErrUnsupported] when used with an abstract
// socket.
func WithChmod(mask os.FileMode) SockOption {
return func(path string) error {
if isAbstractSocket(path) {
return &os.PathError{
Op: "chmod",
Path: path,
Err: fmt.Errorf("abstract Unix sockets do not support filesystem permissions: %w", errors.ErrUnsupported),
}
}
if err := os.Chmod(path, mask); err != nil {
return err
}
@@ -28,27 +60,90 @@ func WithChmod(mask os.FileMode) SockOption {
}
}
// NewUnixSocket creates a unix socket with the specified path and group.
// NewUnixSocket creates a Unix socket with the specified path and group.
//
// On Unix platforms, the socket is owned by root:gid and has permissions 0660.
//
// Abstract Unix sockets are not supported by this helper. Use [NewUnixSocketWithOpts]
// without filesystem permission options instead.
func NewUnixSocket(path string, gid int) (net.Listener, error) {
return NewUnixSocketWithOpts(path, WithChown(0, gid), WithChmod(0o660))
}
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.
func listenUnix(path string, opts ...SockOption) (_ net.Listener, retErr error) {
// net.Listen does not allow permissions or ownership to be set between
// bind(2), which creates the socket path, and listen(2), which makes it
// possible for clients to connect.
//
// 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.
// Creating the socket manually lets us apply options after bind(2), but
// before listen(2). This avoids temporarily relaxing the process umask while
// still preventing a socket from becoming connectable before the requested
// permissions are applied.
//
// 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
// See https://github.com/golang/go/issues/11822
// Similar to sysSocket in stdlib, but without the fast path for Linux.
// https://github.com/golang/go/blob/go1.26.3/src/net/sys_cloexec.go#L18-L36
syscall.ForkLock.RLock()
fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
if err == nil {
syscall.CloseOnExec(fd) // No syscall.SOCK_CLOEXEC on macOS.
}
syscall.ForkLock.RUnlock()
if err != nil {
return nil, os.NewSyscallError("socket", err)
}
defer func() {
if fd >= 0 {
_ = syscall.Close(fd)
}
}()
if err := syscall.Bind(fd, &syscall.SockaddrUnix{Name: path}); err != nil {
return nil, os.NewSyscallError("bind", err)
}
defer func() {
if retErr != nil {
_ = syscall.Unlink(path)
}
}()
// Secure by default: the socket is not accessible at all
// unless permission options are set through WithChmod.
if err := os.Chmod(path, defaultSocketPerms); err != nil {
return nil, err
}
for _, op := range opts {
if err := op(path); err != nil {
return nil, err
}
}
if err := syscall.Listen(fd, listenerBacklog()); err != nil {
return nil, os.NewSyscallError("listen", err)
}
f := os.NewFile(uintptr(fd), "unix:"+path)
fd = -1 // f now owns the original fd; prevent the defer from closing it.
// FileListener duplicates f, sets the duplicate close-on-exec and nonblocking,
// and returns a net.Listener backed by that duplicate. The temporary *os.File
// is no longer needed after this point.
l, err := net.FileListener(f)
_ = f.Close()
if err != nil {
return nil, err
}
if ul, ok := l.(*net.UnixListener); ok {
ul.SetUnlinkOnClose(true)
}
return l, nil
}
// listenerBacklog is a caching wrapper around maxListenerBacklog.
var listenerBacklog = sync.OnceValue(maxListenerBacklog)
+24 -10
View File
@@ -48,7 +48,7 @@ func WithAdditionalUsersAndGroups(additionalUsersAndGroups []string) SockOption
}
sd, err := getSecurityDescriptor(additionalUsersAndGroups...)
if err != nil {
return fmt.Errorf("looking up SID: %w", err)
return err
}
return withSDDL(sd)(path)
}
@@ -85,12 +85,15 @@ func withSDDL(sddl string) SockOption {
}
}
// NewUnixSocket creates a new unix socket.
// 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.
//
// Abstract Unix sockets are not supported by this helper. Attempts to use
// abstract socket addresses return an error wrapping [errors.ErrUnsupported].
func NewUnixSocket(path string, additionalUsersAndGroups []string) (net.Listener, error) {
var opts []SockOption
if len(additionalUsersAndGroups) > 0 {
@@ -103,27 +106,38 @@ func NewUnixSocket(path string, additionalUsersAndGroups []string) (net.Listener
// 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.
// By default, it grants [BasePermissions]. Additional users and groups
// are granted generic read (GR) and write (GW) access. It returns an
// error if any name cannot be resolved to a SID.
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?
// We keep duplicates; two identical allow ACEs are redundant,
// but they do not create conflicting permissions, so should not error.
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/20233ed8-a6c6-4097-aafa-dd545ed24428
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)
sddl += "(A;;GRGW;;;" + sid + ")"
}
return sddl, nil
}
func listenUnix(path string) (net.Listener, error) {
return net.Listen("unix", path)
func listenUnix(path string, opts ...SockOption) (net.Listener, error) {
l, err := net.Listen("unix", path)
if err != nil {
return nil, err
}
for _, op := range opts {
if err := op(path); err != nil {
_ = l.Close()
return nil, err
}
}
return l, nil
}
+1 -1
View File
@@ -72,7 +72,7 @@ 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.7.0
# github.com/docker/go-connections v0.8.0
## explicit; go 1.23
github.com/docker/go-connections/nat
github.com/docker/go-connections/sockets