Skip to content

Commit

Permalink
socket: implement FileConn for systemd socket activation
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Layher <[email protected]>
  • Loading branch information
mdlayher committed Feb 12, 2022
1 parent 66d61f5 commit 6e912a6
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 0 deletions.
36 changes: 36 additions & 0 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,42 @@ func socket(domain, typ, proto int, name string) (*Conn, error) {
}
}

// FileConn returns a copy of the network connection corresponding to the open
// file. It is the caller's responsibility to close the file when finished.
// Closing the Conn does not affect the File, and closing the File does not
// affect the Conn.
func FileConn(f *os.File, name string) (*Conn, error) {
// First we'll try to do fctnl(2) with F_DUPFD_CLOEXEC because we can dup
// the file descriptor and set the flag in one syscall.
fd, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0)
switch err {
case nil:
// OK, ready to set up non-blocking I/O.
return newConn(fd, name)
case unix.EINVAL:
// The kernel rejected our fcntl(2), fall back to separate dup(2) and
// setting close on exec.
//
// Mirror what the standard library does when creating file descriptors:
// avoid racing a fork/exec with the creation of new file descriptors,
// so that child processes do not inherit socket file descriptors
// unexpectedly.
syscall.ForkLock.RLock()
fd, err := unix.Dup(fd)
if err != nil {
syscall.ForkLock.RUnlock()
return nil, os.NewSyscallError("dup", err)
}
unix.CloseOnExec(fd)
syscall.ForkLock.RUnlock()

return newConn(fd, name)
default:
// Any other errors.
return nil, os.NewSyscallError("fcntl", err)
}
}

// TODO(mdlayher): consider exporting newConn as New?

// newConn wraps an existing file descriptor to create a Conn. name should be a
Expand Down
57 changes: 57 additions & 0 deletions conn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package socket_test

import (
"bytes"
"fmt"
"io"
"io/ioutil"
"math"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/mdlayher/socket/internal/sockettest"
"golang.org/x/net/nettest"
"golang.org/x/sync/errgroup"
"golang.org/x/sys/unix"
)

Expand Down Expand Up @@ -43,6 +45,61 @@ func TestDialTCPNoListener(t *testing.T) {
}
}

func TestFileConn(t *testing.T) {
// Use raw system calls to set up the socket since we assume anything being
// passed into a FileConn is set up by another system, such as systemd's
// socket activation.
fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, 0)
if err != nil {
t.Fatalf("failed to open socket: %v", err)
}

// Bind to loopback, any available port.
sa := &unix.SockaddrInet6{Addr: [16]byte{15: 0x01}}
if err := unix.Bind(fd, sa); err != nil {
t.Fatalf("failed to bind: %v", err)
}

if err := unix.Listen(fd, unix.SOMAXCONN); err != nil {
t.Fatalf("failed to listen: %v", err)
}

// The socket should be ready, create a blocking file which is ready to be
// passed into FileConn via the FileListener helper.
f := os.NewFile(uintptr(fd), "tcpv6-listener")
defer f.Close()

l, err := sockettest.FileListener(f)
if err != nil {
t.Fatalf("failed to open file listener: %v", err)
}
defer l.Close()

// To exercise the listener, attempt to accept and then immediately close a
// single TCPv6 connection. Dial to the listener from the main goroutine and
// wait for everything to finish.
var eg errgroup.Group
eg.Go(func() error {
c, err := l.Accept()
if err != nil {
return fmt.Errorf("failed to accept: %v", err)
}

_ = c.Close()
return nil
})

c, err := net.Dial(l.Addr().Network(), l.Addr().String())
if err != nil {
t.Fatalf("failed to dial listener: %v", err)
}
_ = c.Close()

if err := eg.Wait(); err != nil {
t.Fatalf("failed to wait for listener goroutine: %v", err)
}
}

// Use our TCP net.Listener and net.Conn implementations backed by *socket.Conn
// and run compliance tests with nettest.TestConn.
//
Expand Down
21 changes: 21 additions & 0 deletions internal/sockettest/sockettest.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net"
"os"
"time"

"github.com/mdlayher/socket"
Expand Down Expand Up @@ -50,6 +51,26 @@ func Listen(port int, cfg *socket.Config) (net.Listener, error) {
}, nil
}

// FileListener creates an IPv6 TCP net.Listener backed by a *socket.Conn from
// the input file.
func FileListener(f *os.File) (net.Listener, error) {
c, err := socket.FileConn(f, "tcpv6-server")
if err != nil {
return nil, fmt.Errorf("failed to open file conn: %v", err)
}

sa, err := c.Getsockname()
if err != nil {
_ = c.Close()
return nil, fmt.Errorf("failed to getsockname: %v", err)
}

return &listener{
addr: newTCPAddr(sa),
c: c,
}, nil
}

func (l *listener) Addr() net.Addr { return l.addr }
func (l *listener) Close() error { return l.c.Close() }
func (l *listener) Accept() (net.Conn, error) {
Expand Down

0 comments on commit 6e912a6

Please sign in to comment.