diff --git a/activation/filesmethod.go b/activation/filesmethod.go new file mode 100644 index 00000000..98f237aa --- /dev/null +++ b/activation/filesmethod.go @@ -0,0 +1,34 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package activation + +// Method decides what happens to the file descriptors that are passed in by systemd. +type Method int + +const ( + // ConsumeFiles is the default, and removes the original file descriptors passed in by + // systemd. This means that new file descriptors created by the program may use the + // file descriptors indices. + ConsumeFiles Method = iota + + // ReserveFiles stores placeholder file descriptors, which point to /dev/null. This + // stops new file descriptors consuming the indices. + ReserveFiles + + // CloneFiles duplicates and leaves the original file descriptors in tack. This + // stops new file descriptors consuming the indices like [ReserveFiles] but + // consider the possible secuirty risk of leaving the sockets exposed. + ConserveFiles +) diff --git a/activation/filesmethod_unix.go b/activation/filesmethod_unix.go new file mode 100644 index 00000000..6b5c9628 --- /dev/null +++ b/activation/filesmethod_unix.go @@ -0,0 +1,68 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows + +package activation + +import ( + "os" + "strconv" + "syscall" +) + +func (m Method) Apply(f *os.File) error { + saveFd := int(f.Fd()) // get the idx before being closed. + + switch m { + case ConsumeFiles: + return f.Close() + case ReserveFiles: + devNull, err := os.OpenFile(os.DevNull, os.O_RDWR, 0o666) + if err != nil { + return errorDevNullSetup{err: err, fd: saveFd} + } + + nullFd := int(devNull.Fd()) + + // "If oldfd equals newfd, then dup3() fails with the error EINVAL." + if saveFd == nullFd { + syscall.CloseOnExec(nullFd) + } else { + // "makes newfd be the copy of oldfd, closing newfd first if necessary" + if err := syscall.Dup3(nullFd, saveFd, syscall.O_CLOEXEC); err != nil { + devNull.Close() // on an error tidy up. + + return errorDevNullSetup{err: err, fd: saveFd} + } + } + case ConserveFiles: + // no action + } + + return nil +} + +type errorDevNullSetup struct { + fd int + err error +} + +func (e errorDevNullSetup) Error() string { + return "setting up " + strconv.Itoa(e.fd) + " fd to /dev/null: " + e.err.Error() +} + +func (e errorDevNullSetup) Unwrap() error { + return e.err +} diff --git a/activation/filesmethod_windows.go b/activation/filesmethod_windows.go new file mode 100644 index 00000000..58119eae --- /dev/null +++ b/activation/filesmethod_windows.go @@ -0,0 +1,21 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package activation + +import "os" + +func (m Method) Apply(f *os.File) error { + return nil +} diff --git a/activation/listeners.go b/activation/listeners.go index 108562bf..1d4aecb6 100644 --- a/activation/listeners.go +++ b/activation/listeners.go @@ -26,37 +26,37 @@ import ( // Nil values are used to fill any gaps. For example if systemd were to return file descriptors // corresponding with "udp, tcp, tcp", then the slice would contain {nil, net.Listener, net.Listener} func Listeners() ([]net.Listener, error) { - files := Files(true) - listeners := make([]net.Listener, len(files)) - - for i, f := range files { - if pc, err := net.FileListener(f); err == nil { - listeners[i] = pc - f.Close() - } - } - return listeners, nil + return ListenersWithOptions() } // ListenersWithNames maps a listener name to a set of net.Listener instances. func ListenersWithNames() (map[string][]net.Listener, error) { - files := Files(true) - listeners := map[string][]net.Listener{} - - for _, f := range files { - if pc, err := net.FileListener(f); err == nil { - listeners[f.Name()] = append(listeners[f.Name()], pc) - f.Close() - } - } - return listeners, nil + return ListenersWithNamesWithOptions() } // TLSListeners returns a slice containing a net.listener for each matching TCP socket type // passed to this process. // It uses default Listeners func and forces TCP sockets handlers to use TLS based on tlsConfig. func TLSListeners(tlsConfig *tls.Config) ([]net.Listener, error) { - listeners, err := Listeners() + return TLSListenersWithOptions(tlsConfig) +} + +// TLSListenersWithNames maps a listener name to a net.Listener with +// the associated TLS configuration. +func TLSListenersWithNames(tlsConfig *tls.Config) (map[string][]net.Listener, error) { + return TLSListenersWithNamesWithOptions(tlsConfig) +} + +// TLSListenersWithOptions returns a slice containing a net.Listener for each matching TCP socket type +// passed to this process. +// It uses ListenersWithOptions func and forces TCP sockets handlers to use TLS based on tlsConfig. +// +// - The socket activation file descriptors are consumed and closed by default. This +// can be changed by passing in the [UseMethod] option. See [Method] for more details. +// - Unsetting the corresponding environment variables is the default behavior. This +// can be changed by passing in the [UnsetEnv] option. +func TLSListenersWithOptions(tlsConfig *tls.Config, opts ...option) ([]net.Listener, error) { + listeners, err := ListenersWithOptions(opts...) if listeners == nil || err != nil { return nil, err @@ -74,10 +74,15 @@ func TLSListeners(tlsConfig *tls.Config) ([]net.Listener, error) { return listeners, err } -// TLSListenersWithNames maps a listener name to a net.Listener with +// TLSListenersWithNamesWithOptions maps a listener name to a net.Listener with // the associated TLS configuration. -func TLSListenersWithNames(tlsConfig *tls.Config) (map[string][]net.Listener, error) { - listeners, err := ListenersWithNames() +// +// - The socket activation file descriptors are consumed and closed by default. This +// can be changed by passing in the [UseMethod] option. See [Method] for more details. +// - Unsetting the corresponding environment variables is the default behavior. This +// can be changed by passing in the [UnsetEnv] option. +func TLSListenersWithNamesWithOptions(tlsConfig *tls.Config, opts ...option) (map[string][]net.Listener, error) { + listeners, err := ListenersWithNamesWithOptions(opts...) if listeners == nil || err != nil { return nil, err @@ -96,3 +101,52 @@ func TLSListenersWithNames(tlsConfig *tls.Config) (map[string][]net.Listener, er return listeners, err } + +// ListenersWithOptions returns a slice containing a net.Listener for each matching socket type +// passed to this process. +// +// - The socket activation file descriptors are consumed and closed by default. This +// can be changed by passing in the [UseMethod] option. See [Method] for more details. +// - Unsetting the corresponding environment variables is the default behavior. This +// can be changed by passing in the [UnsetEnv] option. +func ListenersWithOptions(opts ...option) ([]net.Listener, error) { + o := options{unsetEnv: true} + o.apply(opts...) + + files := Files(o.unsetEnv) + listeners := make([]net.Listener, len(files)) + + for i, f := range files { + if pc, err := net.FileListener(f); err == nil { + listeners[i] = pc + + o.method.Apply(f) + } + } + + return listeners, nil +} + +// ListenersWithNamesWithOptions maps a listener name to a set of net.Listener instances. +// +// - The socket activation file descriptors are consumed and closed by default. This +// can be changed by passing in the [UseMethod] option. See [Method] for more details. +// - Unsetting the corresponding environment variables is the default behavior. This +// can be changed by passing in the [UnsetEnv] option. +func ListenersWithNamesWithOptions(opts ...option) (map[string][]net.Listener, error) { + o := options{unsetEnv: true} + o.apply(opts...) + + files := Files(o.unsetEnv) + listeners := map[string][]net.Listener{} + + for _, f := range files { + if pc, err := net.FileListener(f); err == nil { + listeners[f.Name()] = append(listeners[f.Name()], pc) + + o.method.Apply(f) + } + } + + return listeners, nil +} diff --git a/activation/options.go b/activation/options.go new file mode 100644 index 00000000..f5d37bad --- /dev/null +++ b/activation/options.go @@ -0,0 +1,48 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package activation + +type options struct { + unsetEnv bool + method Method +} + +// apply applies option functions to the options struct. +func (o *options) apply(opts ...option) { + for _, opt := range opts { + opt(o) + } +} + +type option func(*options) + +// UnsetEnv controls if the LISTEN_PID, LISTEN_FDS & LISTEN_FDNAMES environment +// variables are unset. +// +// This is useful to avoid clashes in fd usage and to avoid leaking environment +// flags to child processes. +func UnsetEnv(f bool) option { + return func(o *options) { + o.unsetEnv = f + } +} + +// UseMethod chooses the [Method] applied to the file descriptor passed in by +// systemd socket activation. +func UseMethod(m Method) option { + return func(o *options) { + o.method = m + } +} diff --git a/activation/packetconns.go b/activation/packetconns.go index a9720678..96a1550d 100644 --- a/activation/packetconns.go +++ b/activation/packetconns.go @@ -25,13 +25,28 @@ import ( // Nil values are used to fill any gaps. For example if systemd were to return file descriptors // corresponding with "udp, tcp, udp", then the slice would contain {net.PacketConn, nil, net.PacketConn} func PacketConns() ([]net.PacketConn, error) { - files := Files(true) + return PacketConnsWithOptions(UnsetEnv(true)) +} + +// PacketConnsWithOptions returns a slice containing a net.PacketConn for each matching socket type +// passed to this process. +// +// - The socket activation file descriptors are consumed and closed by default. This +// can be changed by passing in the [UseMethod] option. See [Method] for more details. +// - Unsetting the corresponding environment variables is the default behavior. This +// can be changed by passing in the [UnsetEnv] option. +func PacketConnsWithOptions(opts ...option) ([]net.PacketConn, error) { + o := options{unsetEnv: true} + o.apply(opts...) + + files := Files(o.unsetEnv) conns := make([]net.PacketConn, len(files)) for i, f := range files { if pc, err := net.FilePacketConn(f); err == nil { conns[i] = pc - f.Close() + + o.method.Apply(f) } } return conns, nil