Help us shape the future of Go! Take the 2026 Go Developer Survey

control

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: BSD-3-Clause Imports: 18 Imported by: 2

README

grpc-transports/control

control

Go Reference CI

The listener for a daemon's gRPC admin API — the port an operator's CLI talks to, as opposed to the one its users do. It listens on a 0600 unix socket, or on TCP with mandatory mutual TLS, and says who called for the audit line. Pure Go, CGO_ENABLED=0.

Why

An admin API can reconfigure, drain or stop the daemon, so "who may reach it" has two acceptable answers and this package accepts no other:

who can connect
unix:///run/mydaemon/admin.sock the daemon's own user: the socket is mode 0600
host:port a client presenting a certificate signed by the CA the daemon names, to a server whose certificate the client verifies in turn

There is no plaintext TCP, and loopback is not an exception: any local account can connect to 127.0.0.1, so an admin API on loopback without client certificates is open to every user of the machine.

Use

cfg := control.Config{Listen: "unix:///run/mydaemon/admin.sock"}
// or: control.Config{Listen: ":8443", TLSCertFile: "srv.crt", TLSKeyFile: "srv.key", ClientCAFile: "ops-ca.pem"}

if err := cfg.Check(); err != nil { // at config load: no network, no files
    log.Fatal(err)
}
lis, opts, err := control.Listen(cfg)
if err != nil {
    log.Fatal(err)
}
gs := grpc.NewServer(opts...)
adminpb.RegisterAdminServer(gs, impl)
go gs.Serve(lis)

In a handler:

log.Printf("admin: %s called Drain", control.Caller(ctx)) // "uid=1000", or "cn=ops-laptop"

A client — a CLI, or the daemon's own tests:

cc, err := control.Dial(control.ClientConfig{Target: "unix:///run/mydaemon/admin.sock"})
cc, err := control.Dial(control.ClientConfig{Target: "admin.example.org:8443",
    CertFile: "me.crt", KeyFile: "me.key", ServerCAFile: "daemon-ca.pem"})

What Check refuses

  • TCP without all three of TLSCertFile, TLSKeyFile, ClientCAFile — loopback included;
  • a unix socket with any of them: its file mode is its access control, and a certificate beside it would suggest otherwise;
  • a relative socket path, and any scheme other than unix:.

The unix socket

It is never reachable by another user, not even for an instant. bind(2) creates the socket file with mode 0777 &^ umask, so with the usual umask it is connectable by everyone until a chmod follows. Narrowing the umask around the bind would close that window but open another: the umask belongs to the process, not the goroutine, and every file other goroutines created meanwhile would silently get the narrower mode. So the socket is bound inside a fresh 0700 directory next to the requested path, chmod'ed to 0600 there, and only then hard-linked into place.

A running daemon's socket is never stolen. A socket file already at the path is removed only if connecting to it is refused — nothing listens. If something answers, Listen fails naming the path. If it cannot tell (another user's socket, say), it fails too. A file at the path that is not a socket is never touched. link(2) refuses to replace a file, so one that appears between that check and the bind is left alone as well.

Closing the listener removes the socket file — if it is still the one this listener created.

The peer uid comes from the kernel at connection time — SO_PEERCRED on Linux, LOCAL_PEERCRED (what getpeereid(3) uses) on darwin and FreeBSD — and a client cannot forge it.

The client checks who is listening. Dial reads the uid of the process behind the socket the same way and refuses to send anything unless it is the client's own uid, root's, or ClientConfig.ServerUID (a uid trusted in addition, for a daemon running as its own account). A socket in a directory others can write could have been bound first by an impostor; the RPC then fails naming the uid found. Where the platform has no peer credentials (Windows, the BSDs other than FreeBSD) the uid is not checked: keep the socket in a directory only trusted accounts can write.

uid := uint32(997) // the daemon's account
cc, err := control.Dial(control.ClientConfig{Target: "unix:///run/mydaemon/admin.sock", ServerUID: &uid})

The private directory costs up to 13 bytes of the socket address limit (104 bytes on darwin, 108 on Linux) while binding; a path that would not fit is refused with that reason rather than an EINVAL.

TCP

The server requires and verifies a client certificate against ClientCAFile (tls.RequireAndVerifyClientCert), accepts TLS 1.2 at the least, and negotiates TLS 1.3 whenever the client offers it. Caller reports the verified certificate's subject CN, or its whole subject when the CN is empty.

Platforms

peer identity socket mode
Linux uid= via SO_PEERCRED 0600, never exposed
darwin, FreeBSD uid= via LOCAL_PEERCRED 0600, never exposed
other unixes unix 0600, never exposed
Windows unix — there is no uid none: who may connect is the ACL the socket inherits from its directory, so put it in one only the daemon's account can open

Sibling

The rest of grpc-transports carries gRPC where TCP cannot go — vsock, websocket, webrtc. This one is about where gRPC should not go.

License

BSD-3-Clause.

Documentation

Overview

Package control is the listener for a daemon's gRPC admin (control-plane) API: the port an operator's CLI talks to, as opposed to the one the daemon's users talk to.

An admin API can reconfigure, drain or stop the daemon, so the question "who may reach it" has only two acceptable answers, and this package accepts no other:

  • a unix socket, whose file mode is its access control: it is left at 0600, owned by the daemon's user, and nobody else can connect;
  • TCP with mutual TLS: the client must present a certificate signed by a CA the daemon names, and the daemon presents one signed by a CA the client names.

There is no plaintext TCP, and loopback is not an exception: any local user can connect to 127.0.0.1, so loopback without client certificates is an admin API open to every account on the machine.

Use

cfg := control.Config{Listen: "unix:///run/mydaemon/admin.sock"}
if err := cfg.Check(); err != nil { ... }   // at config load, no side effects
lis, opts, err := control.Listen(cfg)
gs := grpc.NewServer(opts...)
adminpb.RegisterAdminServer(gs, impl)
go gs.Serve(lis)

and in a handler, for the audit line:

log.Printf("admin: %s called Drain", control.Caller(ctx))

which prints "uid=1000" for a unix caller and "cn=ops-laptop" for an mTLS one.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Caller

func Caller(ctx context.Context) string

Caller says who made the RPC whose context this is, for an audit line: "cn=<subject CN>" for an mTLS client (the whole subject if its CN is empty), "uid=<uid>" for a unix client — read from the kernel when the connection was accepted (SO_PEERCRED on Linux, LOCAL_PEERCRED on darwin and FreeBSD) — or "unix" where the platform cannot say (Windows, and the BSDs without LOCAL_PEERCRED). It returns "unknown" when the context carries neither.

func Dial

func Dial(c ClientConfig, opts ...grpc.DialOption) (*grpc.ClientConn, error)

Dial returns a client connection to an admin API. As with grpc.NewClient, nothing is connected until the first RPC; what fails here is a configuration that could never work, and unreadable files.

For a unix target, each connection is refused unless the process listening on the socket runs as this process's uid, as root, or as ClientConfig.ServerUID: the RPC then fails with an error naming the uid found.

func Listen

func Listen(c Config) (net.Listener, []grpc.ServerOption, error)

Listen binds the admin listener and returns it with the grpc.ServerOptions (transport credentials) to pass to grpc.NewServer. It runs Check first.

For unix, the socket is bound inside a fresh 0700 directory next to the requested path, chmod'ed to 0600 there, and only then hard-linked into place, so it is never reachable by another user, not even for the instant between bind and chmod — see bindUnix. The process umask is not touched: it is process-wide, and changing it around bind would silently change the mode of every file other goroutines create meanwhile. A socket file already at the path is removed only if nothing answers on it; a live one is an error naming the path, and anything that is not a socket is never removed. Closing the listener removes the socket file, if it is still the one this call created.

For TCP, the server presents TLSCertFile/TLSKeyFile, requires and verifies a client certificate against ClientCAFile, and accepts TLS 1.2 at the least (TLS 1.3 is negotiated whenever the client offers it).

Types

type ClientConfig

type ClientConfig struct {
	// Target has the syntax of Config.Listen.
	Target string

	// CertFile and KeyFile are the client's certificate and key, and
	// ServerCAFile the CA that signed the server's certificate. All three are
	// required for TCP and forbidden for unix.
	CertFile, KeyFile, ServerCAFile string

	// ServerName is the name to verify the server's certificate against. It
	// defaults to the host in Target.
	ServerName string

	// ServerUID is, for a unix Target, a uid the server may run as besides
	// this process's own uid and root's, which are always accepted. Dial
	// checks who is listening on the socket (the kernel's peer credentials)
	// and refuses any other uid: a socket in a directory others can write may
	// have been bound by an impostor, which would otherwise receive the CLI's
	// commands. Forbidden for TCP, where the server's certificate is the
	// check. On a platform without peer credentials (Windows, and the BSDs
	// other than FreeBSD) the uid cannot be read and is not checked.
	ServerUID *uint32
}

ClientConfig says how to reach an admin API: for tests, CLIs, and the consumers' own tests.

type Config

type Config struct {
	// Listen is "unix:///abs/path.sock" or "host:port".
	Listen string

	// TLSCertFile and TLSKeyFile are the server's certificate and key, and
	// ClientCAFile the CA bundle client certificates must chain to. All three
	// are required together for TCP and forbidden for unix: a socket's
	// permissions are its access control.
	TLSCertFile, TLSKeyFile, ClientCAFile string
}

Config says where the admin API listens.

func (Config) Check

func (c Config) Check() error

Check refuses a Config that cannot be served safely, without touching the network or the filesystem, so that it can run when configuration is loaded: TCP without all three TLS files (loopback included), unix with any of them, a relative socket path, or an unknown scheme.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL