mirror of
https://github.com/docker/cli.git
synced 2026-08-24 10:05:37 -05:00
Merge pull request #7121 from thaJeztah/bump_go_connections
vendor: github.com/docker/go-connections v0.8.0
This commit is contained in:
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user