pax_global_header00006660000000000000000000000064136640467670014535gustar00rootroot0000000000000052 comment=a3253a229765e61998cd26a0b821007ed7b0db81 golang-blitiri-go-systemd-1.1.0/000077500000000000000000000000001366404676700165305ustar00rootroot00000000000000golang-blitiri-go-systemd-1.1.0/.travis.yml000066400000000000000000000003211366404676700206350ustar00rootroot00000000000000# Configuration for https://travis-ci.org/ language: go dist: bionic go_import_path: blitiri.com.ar/go/systemd go: - 1.9 - stable - master script: - go test ./... - go test -race ./... golang-blitiri-go-systemd-1.1.0/LICENSE000066400000000000000000000022041366404676700175330ustar00rootroot00000000000000 Licensed under the MIT licence, which is reproduced below (from https://opensource.org/licenses/MIT). ----- Copyright (c) 2016 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-blitiri-go-systemd-1.1.0/README.md000066400000000000000000000021741366404676700200130ustar00rootroot00000000000000 # blitiri.com.ar/go/systemd [![GoDoc](https://godoc.org/blitiri.com.ar/go/systemd?status.svg)](https://godoc.org/blitiri.com.ar/go/systemd) [![Build Status](https://travis-ci.org/albertito/systemd.svg?branch=master)](https://travis-ci.org/albertito/systemd) [systemd](https://godoc.org/blitiri.com.ar/go/systemd) is a Go package implementing a way to get network listeners from systemd, similar to C's `sd_listen_fds()` and `sd_listen_fds_with_names()` ([man](https://www.freedesktop.org/software/systemd/man/sd_listen_fds.html)). Supports named file descriptors, which is useful if your daemon needs to be able to tell the different ports apart (e.g. http vs https). It is used by daemons such as [chasquid](https://blitiri.com.ar/p/chasquid/) to listen on privileged ports without needing to run as root. ## Example ```go listeners, err := systemd.Listeners() for _, l := range listeners["service.socket"] { go serve(l) } ``` ## Status The API should be considered stable, and no backwards-incompatible changes are expected. ## Contact If you have any questions, comments or patches please send them to albertito@blitiri.com.ar. golang-blitiri-go-systemd-1.1.0/example_test.go000066400000000000000000000007711366404676700215560ustar00rootroot00000000000000package systemd_test import ( "fmt" "net" "blitiri.com.ar/go/systemd" ) func serve(l net.Listener) { // Serve over the listener. } func Example() { listeners, err := systemd.Listeners() if err != nil { fmt.Printf("error getting listeners: %v", err) return } // Iterate over the listeners of a particular systemd socket. // The name comes from the FileDescriptorName option, defaults to the name // of the socket unit. for _, l := range listeners["service.socket"] { go serve(l) } } golang-blitiri-go-systemd-1.1.0/go.mod000066400000000000000000000000521366404676700176330ustar00rootroot00000000000000module blitiri.com.ar/go/systemd go 1.14 golang-blitiri-go-systemd-1.1.0/systemd.go000066400000000000000000000153641366404676700205600ustar00rootroot00000000000000// Package systemd implements a way to get network listeners from systemd, // similar to C's sd_listen_fds(3) and sd_listen_fds_with_names(3). package systemd // import "blitiri.com.ar/go/systemd" import ( "errors" "fmt" "net" "os" "strconv" "strings" "sync" "syscall" ) var ( // Error to return when $LISTEN_PID does not refer to us. ErrPIDMismatch = errors.New("$LISTEN_PID != our PID") // First FD for listeners. // It's 3 by definition, but using a variable simplifies testing. firstFD = 3 ) // Keep a single global map of files/listeners, to avoid repeatedly parsing // which can be problematic (see parse). var files map[string][]*os.File var listeners map[string][]net.Listener var parseError error var listenError error var mutex sync.Mutex // parse files, updating the global state. // This function messes with file descriptors and environment, so it is not // idempotent and must be called only once. For the callers' convenience, we // save the files and listener maps globally, and reuse them on the // user-visible functions. func parse() { mutex.Lock() defer mutex.Unlock() if files != nil { return } pidStr := os.Getenv("LISTEN_PID") nfdsStr := os.Getenv("LISTEN_FDS") fdNamesStr := os.Getenv("LISTEN_FDNAMES") fdNames := strings.Split(fdNamesStr, ":") // Nothing to do if the variables are not set. if pidStr == "" || nfdsStr == "" { return } pid, err := strconv.Atoi(pidStr) if err != nil { parseError = fmt.Errorf( "error converting $LISTEN_PID=%q: %v", pidStr, err) return } else if pid != os.Getpid() { parseError = ErrPIDMismatch return } nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS")) if err != nil { parseError = fmt.Errorf( "error reading $LISTEN_FDS=%q: %v", nfdsStr, err) return } // If LISTEN_FDNAMES is set at all, it should have as many names as we // have descriptors. If it isn't set, then we map them all to "". if fdNamesStr == "" { fdNames = []string{} for i := 0; i < nfds; i++ { fdNames = append(fdNames, "") } } else { if nfds > 0 && len(fdNames) != nfds { parseError = fmt.Errorf( "Incorrect LISTEN_FDNAMES, have you set FileDescriptorName?") return } } files = map[string][]*os.File{} listeners = map[string][]net.Listener{} for i := 0; i < nfds; i++ { fd := firstFD + i // We don't want childs to inherit these file descriptors. syscall.CloseOnExec(fd) name := fdNames[i] sysName := fmt.Sprintf("[systemd-fd-%d-%v]", fd, name) f := os.NewFile(uintptr(fd), sysName) files[name] = append(files[name], f) // Note this can fail for non-TCP listeners, so we put the error in a // separate variable. lis, err := net.FileListener(f) if err != nil { listenError = fmt.Errorf( "Error making listener out of fd %d: %v", fd, err) } else { listeners[name] = append(listeners[name], lis) } } // Remove them from the environment, to prevent accidental reuse (by // us or children processes). os.Unsetenv("LISTEN_PID") os.Unsetenv("LISTEN_FDS") os.Unsetenv("LISTEN_FDNAMES") } // Listeners returns net.Listeners corresponding to the file descriptors // passed by systemd via environment variables. // // It returns a map of the form (file descriptor name -> []net.Listener). // // The file descriptor name comes from the "FileDescriptorName=" option in the // systemd socket unit. Multiple socket units can have the same name, hence // the slice of listeners for each name. // // If the "FileDescriptorName=" option is not used, then all file descriptors // are mapped to the "" name. // // Ideally you should not need to call this more than once. If you do, the // same listeners will be returned, as repeated calls to this function will // return the same results: the parsing is done only once, and the results are // saved and reused. // // See sd_listen_fds(3) and sd_listen_fds_with_names(3) for more details on // how the passing works. func Listeners() (map[string][]net.Listener, error) { parse() if parseError != nil { return listeners, parseError } return listeners, listenError } // OneListener returns a net.Listener for the first systemd socket with the // given name. If there are none, the listener and error will both be nil. An // error will be returned only if there were issues parsing the file // descriptors. // // This function can be convenient for simple callers where you know there's // only one file descriptor being passed with the given name. // // This is a convenience function built on top of Listeners(). func OneListener(name string) (net.Listener, error) { parse() if parseError != nil { return nil, parseError } if listenError != nil { return nil, listenError } lis := listeners[name] if len(lis) < 1 { return nil, nil } return lis[0], nil } // Listen returns a net.Listener for the given address, similar to net.Listen. // // If the address begins with "&" it is interpreted as a systemd socket being // passed. For example, using "&http" would mean we expect a systemd socket // passed to us, named with "FileDescriptorName=http" in its unit. // // Otherwise, it uses net.Listen to create a new listener with the given net // and local address. // // This function can be convenient for simple callers where you get the // address from a user, and want to let them specify either "use systemd" or a // normal address without too much additional complexity. // // This is a convenience function built on top of Listeners(). func Listen(netw, laddr string) (net.Listener, error) { if strings.HasPrefix(laddr, "&") { name := laddr[1:] lis, err := OneListener(name) if lis == nil && err == nil { err = fmt.Errorf("systemd socket %q not found", name) } return lis, err } else { return net.Listen(netw, laddr) } } // Files returns the open files passed by systemd via environment variables. // // It returns a map of the form (file descriptor name -> []*os.File). // // The file descriptor name comes from the "FileDescriptorName=" option in the // systemd socket unit. Multiple socket units can have the same name, hence // the slice of listeners for each name. // // If the "FileDescriptorName=" option is not used, then all file descriptors // are mapped to the "" name. // // Ideally you should not need to call this more than once. If you do, the // same files will be returned, as repeated calls to this function will return // the same results: the parsing is done only once, and the results are saved // and reused. // // See sd_listen_fds(3) and sd_listen_fds_with_names(3) for more details on // how the passing works. // // Normally you would use Listeners instead; however, access to the file // descriptors can be useful if you need more fine-grained control over // listener creation, for example if you need to create packet connections // from them. func Files() (map[string][]*os.File, error) { parse() return files, parseError } golang-blitiri-go-systemd-1.1.0/systemd_test.go000066400000000000000000000212221366404676700216050ustar00rootroot00000000000000package systemd import ( "fmt" "math/rand" "net" "os" "strconv" "strings" "testing" ) // setenv prepares the environment and resets the internal state. func setenv(pid, fds string, names ...string) { os.Setenv("LISTEN_PID", pid) os.Setenv("LISTEN_FDS", fds) os.Setenv("LISTEN_FDNAMES", strings.Join(names, ":")) files = nil listeners = nil parseError = nil listenError = nil } // newListener creates a TCP listener. func newListener(t *testing.T) *net.TCPListener { t.Helper() addr := &net.TCPAddr{ Port: 0, } l, err := net.ListenTCP("tcp", addr) if err != nil { t.Fatalf("Could not create TCP listener: %v", err) } return l } // listenerFd returns a file descriptor for the listener. // Note it is a NEW file descriptor, not the original one. func listenerFd(t *testing.T, l *net.TCPListener) int { t.Helper() f, err := l.File() if err != nil { t.Fatalf("Could not get TCP listener file: %v", err) } return int(f.Fd()) } func sameAddr(a, b net.Addr) bool { return a.Network() == b.Network() && a.String() == b.String() } func TestEmptyEnvironment(t *testing.T) { cases := []struct{ pid, fds string }{ {"", ""}, {"123", ""}, {"", "4"}, } for _, c := range cases { setenv(c.pid, c.fds) if ls, err := Listeners(); ls != nil || err != nil { t.Logf("Case: LISTEN_PID=%q LISTEN_FDS=%q", c.pid, c.fds) t.Errorf("Unexpected result: %v // %v", ls, err) } if fs, err := Files(); fs != nil || err != nil { t.Logf("Case: LISTEN_PID=%q LISTEN_FDS=%q", c.pid, c.fds) t.Errorf("Unexpected result: %v // %v", fs, err) } } } func TestBadEnvironment(t *testing.T) { // Create a listener so we have something to reference. l := newListener(t) defer l.Close() firstFD = listenerFd(t, l) ourPID := strconv.Itoa(os.Getpid()) cases := []struct { pid, fds string names []string }{ {"a", "1", []string{"name"}}, // Invalid PID. {ourPID, "a", []string{"name"}}, // Invalid number of fds. {"1", "1", []string{"name"}}, // PID != ourselves. {ourPID, "1", []string{"name1", "name2"}}, // Too many names. } for _, c := range cases { setenv(c.pid, c.fds, c.names...) if ls, err := Listeners(); err == nil { t.Logf("Case: LISTEN_PID=%q LISTEN_FDS=%q LISTEN_FDNAMES=%q", c.pid, c.fds, c.names) t.Errorf("Unexpected result: %v // %v", ls, err) } if ls, err := OneListener("name"); err == nil { t.Errorf("Unexpected result: %v // %v", ls, err) } if fs, err := Files(); err == nil { t.Logf("Case: LISTEN_PID=%q LISTEN_FDS=%q LISTEN_FDNAMES=%q", c.pid, c.fds, c.names) t.Errorf("Unexpected result: %v // %v", fs, err) } } } func TestWrongPID(t *testing.T) { // Find a pid != us. 1 should always work in practice. pid := 1 for pid == os.Getpid() { pid = rand.Int() } setenv(strconv.Itoa(pid), "4") if _, err := Listeners(); err != ErrPIDMismatch { t.Errorf("Did not fail with PID mismatch: %v", err) } if _, err := OneListener("name"); err != ErrPIDMismatch { t.Errorf("Did not fail with PID mismatch: %v", err) } if _, err := Files(); err != ErrPIDMismatch { t.Errorf("Did not fail with PID mismatch: %v", err) } } func TestNoFDs(t *testing.T) { setenv(strconv.Itoa(os.Getpid()), "0") if ls, err := Listeners(); len(ls) != 0 || err != nil { t.Errorf("Got a non-empty result: %v // %v", ls, err) } if ls, err := Files(); len(ls) != 0 || err != nil { t.Errorf("Got a non-empty result: %v // %v", ls, err) } if l, err := OneListener("nothing"); l != nil || err != nil { t.Errorf("Unexpected result: %v // %v", l, err) } if l, err := Listen("tcp", "¬hing"); l != nil || err == nil { t.Errorf("Unexpected result: %v // %v", l, err) } } func TestBadFDs(t *testing.T) { f, err := os.Open(os.DevNull) if err != nil { t.Fatalf("Failed to open /dev/null: %v", err) } defer f.Close() setenv(strconv.Itoa(os.Getpid()), "1") firstFD = int(f.Fd()) if ls, err := Listeners(); len(ls) != 0 || err == nil { t.Errorf("Got a non-empty result: %v // %v", ls, err) } if l, err := OneListener(""); l != nil || err == nil { t.Errorf("Got a non-empty result: %v // %v", l, err) } // It's not a bad FD as far as Files() is concerned. fs, err := Files() if err != nil { t.Errorf("Unexpected error: %v", err) } if len(fs) != 1 || len(fs[""]) != 1 { t.Errorf("Unexpected result: %v", fs) } if got := fs[""][0]; got.Fd() != f.Fd() { t.Errorf("File descriptor %d != expected %d (%v)", got.Fd(), f.Fd(), got) } } func TestOneSocket(t *testing.T) { l := newListener(t) defer l.Close() firstFD = listenerFd(t, l) setenv(strconv.Itoa(os.Getpid()), "1", "name") { lsMap, err := Listeners() if err != nil || len(lsMap) != 1 { t.Fatalf("Got an invalid result: %v // %v", lsMap, err) } ls := lsMap["name"] if !sameAddr(ls[0].Addr(), l.Addr()) { t.Errorf("Listener 0 address mismatch, expected %#v, got %#v", l.Addr(), ls[0].Addr()) } oneL, err := OneListener("name") if err != nil { t.Errorf("OneListener error: %v", err) } if !sameAddr(oneL.Addr(), l.Addr()) { t.Errorf("OneListener address mismatch, expected %#v, got %#v", l.Addr(), ls[0].Addr()) } } { fsMap, err := Files() if err != nil || len(fsMap) != 1 || len(fsMap["name"]) != 1 { t.Fatalf("Got an invalid result: %v // %v", fsMap, err) } f := fsMap["name"][0] flis, err := net.FileListener(f) if err != nil { t.Errorf("File was not a listener: %v", err) } if !sameAddr(flis.Addr(), l.Addr()) { t.Errorf("Listener 0 address mismatch, expected %#v, got %#v", l.Addr(), flis.Addr()) } } if os.Getenv("LISTEN_PID") != "" || os.Getenv("LISTEN_FDS") != "" { t.Errorf("Failed to reset the environment") } } func TestManySockets(t *testing.T) { // Create two contiguous listeners. // The test environment does not guarantee us that they are contiguous, so // keep going until they are. var l0, l1 *net.TCPListener var f0, f1 int = -1, -3 for f0+1 != f1 { // We have to be careful with the order of these operations, because // listenerFd will create *new* file descriptors. l0 = newListener(t) l1 = newListener(t) f0 = listenerFd(t, l0) f1 = listenerFd(t, l1) defer l0.Close() defer l1.Close() t.Logf("Looping for FDs: %d %d", f0, f1) } expected := []*net.TCPListener{l0, l1} firstFD = f0 setenv(strconv.Itoa(os.Getpid()), "2", "name1", "name2") { lsMap, err := Listeners() if err != nil || len(lsMap) != 2 { t.Fatalf("Got an invalid result: %v // %v", lsMap, err) } ls := []net.Listener{ lsMap["name1"][0], lsMap["name2"][0], } for i := 0; i < 2; i++ { if !sameAddr(ls[i].Addr(), expected[i].Addr()) { t.Errorf("Listener %d address mismatch, expected %#v, got %#v", i, ls[i].Addr(), expected[i].Addr()) } } oneL, _ := OneListener("name1") if !sameAddr(oneL.Addr(), expected[0].Addr()) { t.Errorf("OneListener address mismatch, expected %#v, got %#v", oneL.Addr(), expected[0].Addr()) } } { fsMap, err := Files() if err != nil || len(fsMap) != 2 { t.Fatalf("Got an invalid result: %v // %v", fsMap, err) } for i := 0; i < 2; i++ { name := fmt.Sprintf("name%d", i+1) fs := fsMap[name] if len(fs) != 1 { t.Errorf("fsMap[%q] = %v had %d entries, expected 1", name, fs, len(fs)) } flis, err := net.FileListener(fs[0]) if err != nil { t.Errorf("File was not a listener: %v", err) } if !sameAddr(flis.Addr(), expected[i].Addr()) { t.Errorf("Listener %d address mismatch, expected %#v, got %#v", i, flis.Addr(), expected[i].Addr()) } } } if os.Getenv("LISTEN_PID") != "" || os.Getenv("LISTEN_FDS") != "" || os.Getenv("LISTEN_FDNAMES") != "" { t.Errorf("Failed to reset the environment") } // Test that things also work with LISTEN_FDNAMES unset. setenv(strconv.Itoa(os.Getpid()), "2") os.Unsetenv("LISTEN_FDNAMES") { lsMap, err := Listeners() if err != nil || len(lsMap) != 1 || len(lsMap[""]) != 2 { t.Fatalf("Got an invalid result: %v // %v", lsMap, err) } ls := []net.Listener{ lsMap[""][0], lsMap[""][1], } for i := 0; i < 2; i++ { if !sameAddr(ls[i].Addr(), expected[i].Addr()) { t.Errorf("Listener %d address mismatch, expected %#v, got %#v", i, ls[i].Addr(), expected[i].Addr()) } } } } func TestListen(t *testing.T) { orig := newListener(t) defer orig.Close() firstFD = listenerFd(t, orig) setenv(strconv.Itoa(os.Getpid()), "1", "name") l, err := Listen("tcp", "&name") if err != nil { t.Errorf("Listen failed: %v", err) } if !sameAddr(l.Addr(), orig.Addr()) { t.Errorf("Listener 0 address mismatch, expected %#v, got %#v", l.Addr(), orig.Addr()) } l, err = Listen("tcp", ":0") if err != nil { t.Errorf("Listen failed: %v", err) } t.Logf("listener created at %v", l.Addr()) }