Skip to content

Commit

Permalink
vsock: add net.OpError for Listener, passing x/net/nettest
Browse files Browse the repository at this point in the history
  • Loading branch information
mdlayher committed Mar 22, 2019
1 parent 7e331fa commit 90b56a8
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 82 deletions.
14 changes: 11 additions & 3 deletions fd_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,17 @@ func (cfd *sysConnFD) SetDeadline(t time.Time, typ deadlineType) error {
}
}

// isENOTCONN determines if an error is unix.ENOTCONN.
func isENOTCONN(err error) bool {
return err == unix.ENOTCONN
// isErrno determines if an error a matches UNIX error number.
func isErrno(err error, errno int) bool {
switch errno {
case ebadf:
return err == unix.EBADF
case enotconn:
return err == unix.ENOTCONN
default:
panicf("vsock: isErrno called with unhandled error number parameter: %d", errno)
return false
}
}

func panicf(format string, a ...interface{}) {
Expand Down
20 changes: 10 additions & 10 deletions integration_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,20 @@ func TestIntegrationListenerUnblockAcceptAfterClose(t *testing.T) {
_, err := vsutil.Accept(l, 10*time.Second)
t.Log("after accept")

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
t.Errorf("expected permanent error, but got temporary one: %v", err)
nerr, ok := err.(*net.OpError)
if !ok {
t.Errorf("expected a net.OpError, but got: %#v", err)
}

// Go1.11:
if strings.Contains(err.Error(), "bad file descriptor") {
// All is well, the file descriptor was closed.
return
if nerr.Temporary() {
t.Errorf("expected permanent error, but got temporary one: %v", err)
}

// Go 1.12+:
// TODO(mdlayher): wrap string error in net.OpError or similar.
if !strings.Contains(err.Error(), "use of closed file") {
t.Errorf("unexpected accept error: %v", err)
// We mimic what net.TCPConn does and return an error with the same
// string as internal/poll, so string matching is the best we can do
// for now.
if !strings.Contains(nerr.Err.Error(), "use of closed") {
t.Errorf("expected close network connection error, but got: %v", nerr.Err)
}
}()

Expand Down
6 changes: 6 additions & 0 deletions internal/vsutil/vsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ func IsHypervisor(t *testing.T) bool {
// SkipDeviceError skips this test if err is related to a failure to access the
// /dev/vsock device.
func SkipDeviceError(t *testing.T, err error) {
// Unwrap net.OpError if needed.
// TODO(mdlayher): errors.Unwrap in Go 1.13.
if nerr, ok := err.(*net.OpError); ok {
err = nerr.Err
}

if os.IsNotExist(err) {
t.Skipf("skipping, vsock device does not exist (try: 'modprobe vhost_vsock'): %v", err)
}
Expand Down
174 changes: 106 additions & 68 deletions vsock.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ const (
shutRd = 0 // unix.SHUT_RD
shutWr = 1 // unix.SHUT_WR

// Error numbers we recognize, copied here to avoid importing x/sys/unix in
// cross-platform code.
ebadf = 9
enotconn = 107

// network is the vsock network reported in net.OpError.
network = "vsock"

// Operation names which may be returned in net.OpError.
opAccept = "accept"
opClose = "close"
opDial = "dial"
opListen = "listen"
Expand All @@ -48,7 +54,15 @@ const (
//
// When the Listener is no longer needed, Close must be called to free resources.
func Listen(port uint32) (*Listener, error) {
return listenStream(port)
l, err := listenStream(port)
if err != nil {
// No addresses available, and we don't parse the context ID for this
// machine on init.
// TODO(mdlayher): figure out a way to plumb in the local address.
return nil, opError(opListen, err, nil, nil)
}

return l, nil
}

var _ net.Listener = &Listener{}
Expand All @@ -61,21 +75,39 @@ type Listener struct {
// Accept implements the Accept method in the net.Listener interface; it waits
// for the next call and returns a generic net.Conn. The returned net.Conn will
// always be of type *Conn.
func (l *Listener) Accept() (net.Conn, error) { return l.l.Accept() }
func (l *Listener) Accept() (net.Conn, error) {
c, err := l.l.Accept()
if err != nil {
return nil, l.opError(opAccept, err)
}

return c, nil
}

// Addr returns the listener's network address, a *Addr. The Addr returned is
// shared by all invocations of Addr, so do not modify it.
func (l *Listener) Addr() net.Addr { return l.l.Addr() }

// Close stops listening on the VM sockets address. Already Accepted connections
// are not closed.
func (l *Listener) Close() error { return l.l.Close() }
func (l *Listener) Close() error {
return l.opError(opClose, l.l.Close())
}

// SetDeadline sets the deadline associated with the listener. A zero time value
// disables the deadline.
//
// SetDeadline only works with Go 1.12+.
func (l *Listener) SetDeadline(t time.Time) error { return l.l.SetDeadline(t) }
func (l *Listener) SetDeadline(t time.Time) error {
return l.opError(opSet, l.l.SetDeadline(t))
}

// opError is a convenience for the function opError that also passes the local
// address of the Listener.
func (l *Listener) opError(op string, err error) error {
// No remote address for a Listener.
return opError(op, err, l.Addr(), nil)
}

// Dial dials a connection-oriented net.Conn to a VM sockets server.
// The contextID and port parameters specify the address of the server.
Expand All @@ -91,15 +123,11 @@ func (l *Listener) SetDeadline(t time.Time) error { return l.l.SetDeadline(t) }
func Dial(contextID, port uint32) (*Conn, error) {
c, err := dialStream(contextID, port)
if err != nil {
// Create a minimal Conn to generate a proper net.OpError.
c := &Conn{
remote: &Addr{
ContextID: contextID,
Port: port,
},
}

return nil, c.opError(opDial, err)
// No local address, but we have a remote address we can return.
return nil, opError(opDial, err, nil, &Addr{
ContextID: contextID,
Port: port,
})
}

return c, nil
Expand Down Expand Up @@ -188,62 +216,10 @@ func (c *Conn) SetWriteDeadline(t time.Time) error {
return c.opError(opSet, c.fd.SetDeadline(t, writeDeadline))
}

// opError unpacks err if possible, producing a net.OpError with op and err in
// order to implement net.Conn. As a convenience, opError returns nil if the
// input error is nil.
// opError is a convenience for the function opError that also passes the local
// and remote addresses of the Conn.
func (c *Conn) opError(op string, err error) error {
if err == nil {
return nil
}

// Unwrap inner errors from error types.
//
// TODO(mdlayher): errors.Cause or similar in Go 1.13.
switch xerr := err.(type) {
// os.PathError produced by os.File method calls.
case *os.PathError:
// Although we could make use of xerr.Op here, we're passing it manually
// for consistency, since some of the Conn calls we are making don't
// wrap an os.File, which would return an Op for us.
err = xerr.Err
}

switch {
case isENOTCONN(err):
// "transport not connected" means io.EOF in Go.
return io.EOF
case err == os.ErrClosed, strings.Contains(err.Error(), "use of closed"):
// net.TCPConn uses an error with this text from internal/poll for the
// backing file already being closed.
err = errors.New("use of closed network connection")
default:
// Nothing to do, return this directly.
}

// Determine source and addr using the rules defined by net.OpError's
// documentation: https://golang.org/pkg/net/#OpError.
var source, addr net.Addr
switch op {
case opClose, opDial, opRead, opWrite:
if c.local != nil {
source = c.local
}
if c.remote != nil {
addr = c.remote
}
case opSet:
if c.local != nil {
addr = c.local
}
}

return &net.OpError{
Op: op,
Net: network,
Source: source,
Addr: addr,
Err: err,
}
return opError(op, err, c.local, c.remote)
}

// TODO(mdlayher): ListenPacket and DialPacket (or maybe another parameter for Dial?).
Expand Down Expand Up @@ -292,3 +268,65 @@ func (a *Addr) fileName() string {
func ContextID() (uint32, error) {
return contextID()
}

// opError unpacks err if possible, producing a net.OpError with the input
// parameters in order to implement net.Conn. As a convenience, opError returns
// nil if the input error is nil.
func opError(op string, err error, local, remote net.Addr) error {
if err == nil {
return nil
}

// Unwrap inner errors from error types.
//
// TODO(mdlayher): errors.Cause or similar in Go 1.13.
switch xerr := err.(type) {
// os.PathError produced by os.File method calls.
case *os.PathError:
// Although we could make use of xerr.Op here, we're passing it manually
// for consistency, since some of the Conn calls we are making don't
// wrap an os.File, which would return an Op for us.
err = xerr.Err
}

switch {
case err == io.EOF, isErrno(err, enotconn):
// We may see a literal io.EOF as happens with x/net/nettest, but
// "transport not connected" also means io.EOF in Go.
return io.EOF
case err == os.ErrClosed, isErrno(err, ebadf), strings.Contains(err.Error(), "use of closed"):
// Different operations may return different errors that all effectively
// indicate a closed file.
//
// To rectify the differences, net.TCPConn uses an error with this text
// from internal/poll for the backing file already being closed.
err = errors.New("use of closed network connection")
default:
// Nothing to do, return this directly.
}

// Determine source and addr using the rules defined by net.OpError's
// documentation: https://golang.org/pkg/net/#OpError.
var source, addr net.Addr
switch op {
case opClose, opDial, opRead, opWrite:
if local != nil {
source = local
}
if remote != nil {
addr = remote
}
case opAccept, opListen, opSet:
if local != nil {
addr = local
}
}

return &net.OpError{
Op: op,
Net: network,
Source: source,
Addr: addr,
Err: err,
}
}
2 changes: 1 addition & 1 deletion vsock_others.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ func (*connFD) Shutdown(_ int) error { return errUnimpl

func contextID() (uint32, error) { return 0, errUnimplemented }

func isENOTCONN(err error) bool { return false }
func isErrno(_ error, _ int) bool { return false }

0 comments on commit 90b56a8

Please sign in to comment.