summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--pkg/dhcp/client.go3
-rw-r--r--pkg/dhcp/dhcp_test.go3
-rw-r--r--pkg/dhcp/server.go3
-rw-r--r--pkg/sentry/socket/epsocket/epsocket.go21
-rw-r--r--pkg/syserr/netstack.go2
-rw-r--r--pkg/tcpip/stack/nic.go15
-rw-r--r--pkg/tcpip/stack/transport_demuxer.go70
-rw-r--r--pkg/tcpip/tcpip.go11
-rw-r--r--pkg/tcpip/transport/tcp/endpoint.go20
-rw-r--r--pkg/tcpip/transport/tcp/endpoint_state.go1
-rw-r--r--pkg/tcpip/transport/udp/endpoint.go23
-rw-r--r--test/syscalls/linux/BUILD69
-rw-r--r--test/syscalls/linux/ip_socket_test_util.cc16
-rw-r--r--test/syscalls/linux/ip_socket_test_util.h8
-rw-r--r--test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking.cc66
-rw-r--r--test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking.h30
-rw-r--r--test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking_test.cc35
-rw-r--r--test/syscalls/linux/socket_ipv4_udp_unbound_external_networking.cc231
-rw-r--r--test/syscalls/linux/socket_ipv4_udp_unbound_external_networking.h30
-rw-r--r--test/syscalls/linux/socket_ipv4_udp_unbound_external_networking_test.cc35
-rw-r--r--test/syscalls/linux/socket_test_util.cc12
-rw-r--r--test/syscalls/linux/socket_test_util.h5
22 files changed, 698 insertions, 11 deletions
diff --git a/pkg/dhcp/client.go b/pkg/dhcp/client.go
index 3330c4998..6d48eec7e 100644
--- a/pkg/dhcp/client.go
+++ b/pkg/dhcp/client.go
@@ -141,6 +141,9 @@ func (c *Client) Request(ctx context.Context, requestedAddr tcpip.Address) (cfg
}, nil); err != nil {
return Config{}, fmt.Errorf("dhcp: connect failed: %v", err)
}
+ if err := ep.SetSockOpt(tcpip.BroadcastOption(1)); err != nil {
+ return Config{}, fmt.Errorf("dhcp: setsockopt SO_BROADCAST: %v", err)
+ }
epin, err := c.stack.NewEndpoint(udp.ProtocolNumber, ipv4.ProtocolNumber, &wq)
if err != nil {
diff --git a/pkg/dhcp/dhcp_test.go b/pkg/dhcp/dhcp_test.go
index a21dce6bc..026064394 100644
--- a/pkg/dhcp/dhcp_test.go
+++ b/pkg/dhcp/dhcp_test.go
@@ -287,6 +287,9 @@ func TestTwoServers(t *testing.T) {
if err = ep.Bind(tcpip.FullAddress{Port: ServerPort}, nil); err != nil {
t.Fatalf("dhcp: server bind: %v", err)
}
+ if err = ep.SetSockOpt(tcpip.BroadcastOption(1)); err != nil {
+ t.Fatalf("dhcp: setsockopt: %v", err)
+ }
serverCtx, cancel := context.WithCancel(context.Background())
defer cancel()
diff --git a/pkg/dhcp/server.go b/pkg/dhcp/server.go
index 3e06ab4c7..c72c3b70d 100644
--- a/pkg/dhcp/server.go
+++ b/pkg/dhcp/server.go
@@ -123,6 +123,9 @@ func newEPConnServer(ctx context.Context, stack *stack.Stack, addrs []tcpip.Addr
if err := ep.Bind(tcpip.FullAddress{Port: ServerPort}, nil); err != nil {
return nil, fmt.Errorf("dhcp: server bind: %v", err)
}
+ if err := ep.SetSockOpt(tcpip.BroadcastOption(1)); err != nil {
+ return nil, fmt.Errorf("dhcp: server setsockopt: %v", err)
+ }
c := newEPConn(ctx, wq, ep)
return NewServer(ctx, c, addrs, cfg)
}
diff --git a/pkg/sentry/socket/epsocket/epsocket.go b/pkg/sentry/socket/epsocket/epsocket.go
index a97db5348..e24e58aed 100644
--- a/pkg/sentry/socket/epsocket/epsocket.go
+++ b/pkg/sentry/socket/epsocket/epsocket.go
@@ -582,6 +582,7 @@ func GetSockOpt(t *kernel.Task, s socket.Socket, ep commonEndpoint, family int,
// getSockOptSocket implements GetSockOpt when level is SOL_SOCKET.
func getSockOptSocket(t *kernel.Task, s socket.Socket, ep commonEndpoint, family int, skType transport.SockType, name, outLen int) (interface{}, *syserr.Error) {
+ // TODO: Stop rejecting short optLen values in getsockopt.
switch name {
case linux.SO_TYPE:
if outLen < sizeOfInt32 {
@@ -681,6 +682,18 @@ func getSockOptSocket(t *kernel.Task, s socket.Socket, ep commonEndpoint, family
return int32(v), nil
+ case linux.SO_BROADCAST:
+ if outLen < sizeOfInt32 {
+ return nil, syserr.ErrInvalidArgument
+ }
+
+ var v tcpip.BroadcastOption
+ if err := ep.GetSockOpt(&v); err != nil {
+ return nil, syserr.TranslateNetstackError(err)
+ }
+
+ return int32(v), nil
+
case linux.SO_KEEPALIVE:
if outLen < sizeOfInt32 {
return nil, syserr.ErrInvalidArgument
@@ -982,6 +995,14 @@ func setSockOptSocket(t *kernel.Task, s socket.Socket, ep commonEndpoint, name i
v := usermem.ByteOrder.Uint32(optVal)
return syserr.TranslateNetstackError(ep.SetSockOpt(tcpip.ReusePortOption(v)))
+ case linux.SO_BROADCAST:
+ if len(optVal) < sizeOfInt32 {
+ return syserr.ErrInvalidArgument
+ }
+
+ v := usermem.ByteOrder.Uint32(optVal)
+ return syserr.TranslateNetstackError(ep.SetSockOpt(tcpip.BroadcastOption(v)))
+
case linux.SO_PASSCRED:
if len(optVal) < sizeOfInt32 {
return syserr.ErrInvalidArgument
diff --git a/pkg/syserr/netstack.go b/pkg/syserr/netstack.go
index 20e756edb..05ca475d1 100644
--- a/pkg/syserr/netstack.go
+++ b/pkg/syserr/netstack.go
@@ -43,6 +43,7 @@ var (
ErrQueueSizeNotSupported = New(tcpip.ErrQueueSizeNotSupported.String(), linux.ENOTTY)
ErrNoSuchFile = New(tcpip.ErrNoSuchFile.String(), linux.ENOENT)
ErrInvalidOptionValue = New(tcpip.ErrInvalidOptionValue.String(), linux.EINVAL)
+ ErrBroadcastDisabled = New(tcpip.ErrBroadcastDisabled.String(), linux.EACCES)
)
var netstackErrorTranslations = map[*tcpip.Error]*Error{
@@ -80,6 +81,7 @@ var netstackErrorTranslations = map[*tcpip.Error]*Error{
tcpip.ErrNetworkUnreachable: ErrNetworkUnreachable,
tcpip.ErrMessageTooLong: ErrMessageTooLong,
tcpip.ErrNoBufferSpace: ErrNoBufferSpace,
+ tcpip.ErrBroadcastDisabled: ErrBroadcastDisabled,
}
// TranslateNetstackError converts an error from the tcpip package to a sentry
diff --git a/pkg/tcpip/stack/nic.go b/pkg/tcpip/stack/nic.go
index 586ca873e..43d7c2ec4 100644
--- a/pkg/tcpip/stack/nic.go
+++ b/pkg/tcpip/stack/nic.go
@@ -399,6 +399,21 @@ func (n *NIC) DeliverNetworkPacket(linkEP LinkEndpoint, remote, _ tcpip.LinkAddr
src, dst := netProto.ParseAddresses(vv.First())
+ // If the packet is destined to the IPv4 Broadcast address, then make a
+ // route to each IPv4 network endpoint and let each endpoint handle the
+ // packet.
+ if dst == header.IPv4Broadcast {
+ for _, ref := range n.endpoints {
+ if ref.protocol == header.IPv4ProtocolNumber && ref.tryIncRef() {
+ r := makeRoute(protocol, dst, src, linkEP.LinkAddress(), ref)
+ r.RemoteLinkAddress = remote
+ ref.ep.HandlePacket(&r, vv)
+ ref.decRef()
+ }
+ }
+ return
+ }
+
if ref := n.getRef(protocol, dst); ref != nil {
r := makeRoute(protocol, dst, src, linkEP.LinkAddress(), ref)
r.RemoteLinkAddress = remote
diff --git a/pkg/tcpip/stack/transport_demuxer.go b/pkg/tcpip/stack/transport_demuxer.go
index a5ff2159a..c18208dc0 100644
--- a/pkg/tcpip/stack/transport_demuxer.go
+++ b/pkg/tcpip/stack/transport_demuxer.go
@@ -132,7 +132,22 @@ func (ep *multiPortEndpoint) selectEndpoint(id TransportEndpointID) TransportEnd
// HandlePacket is called by the stack when new packets arrive to this transport
// endpoint.
func (ep *multiPortEndpoint) HandlePacket(r *Route, id TransportEndpointID, vv buffer.VectorisedView) {
- ep.selectEndpoint(id).HandlePacket(r, id, vv)
+ // If this is a broadcast datagram, deliver the datagram to all endpoints
+ // managed by ep.
+ if id.LocalAddress == header.IPv4Broadcast {
+ for i, endpoint := range ep.endpointsArr {
+ // HandlePacket modifies vv, so each endpoint needs its own copy.
+ if i == len(ep.endpointsArr)-1 {
+ endpoint.HandlePacket(r, id, vv)
+ break
+ }
+ vvCopy := buffer.NewView(vv.Size())
+ copy(vvCopy, vv.ToView())
+ endpoint.HandlePacket(r, id, vvCopy.ToVectorisedView())
+ }
+ } else {
+ ep.selectEndpoint(id).HandlePacket(r, id, vv)
+ }
}
// HandleControlPacket implements stack.TransportEndpoint.HandleControlPacket.
@@ -224,20 +239,47 @@ func (d *transportDemuxer) unregisterEndpoint(netProtos []tcpip.NetworkProtocolN
}
}
-// deliverPacket attempts to deliver the given packet. Returns true if it found
-// an endpoint, false otherwise.
+var loopbackSubnet = func() tcpip.Subnet {
+ sn, err := tcpip.NewSubnet("\x7f\x00\x00\x00", "\xff\x00\x00\x00")
+ if err != nil {
+ panic(err)
+ }
+ return sn
+}()
+
+// deliverPacket attempts to find one or more matching transport endpoints, and
+// then, if matches are found, delivers the packet to them. Returns true if it
+// found one or more endpoints, false otherwise.
func (d *transportDemuxer) deliverPacket(r *Route, protocol tcpip.TransportProtocolNumber, vv buffer.VectorisedView, id TransportEndpointID) bool {
eps, ok := d.protocol[protocolIDs{r.NetProto, protocol}]
if !ok {
return false
}
+ // If a sender bound to the Loopback interface sends a broadcast,
+ // that broadcast must not be delivered to the sender.
+ if loopbackSubnet.Contains(r.RemoteAddress) && r.LocalAddress == header.IPv4Broadcast && id.LocalPort == id.RemotePort {
+ return false
+ }
+
+ // If the packet is a broadcast, then find all matching transport endpoints.
+ // Otherwise, try to find a single matching transport endpoint.
+ destEps := make([]TransportEndpoint, 0, 1)
eps.mu.RLock()
- ep := d.findEndpointLocked(eps, vv, id)
+
+ if protocol == header.UDPProtocolNumber && id.LocalAddress == header.IPv4Broadcast {
+ for epID, endpoint := range eps.endpoints {
+ if epID.LocalPort == id.LocalPort {
+ destEps = append(destEps, endpoint)
+ }
+ }
+ } else if ep := d.findEndpointLocked(eps, vv, id); ep != nil {
+ destEps = append(destEps, ep)
+ }
eps.mu.RUnlock()
- // Fail if we didn't find one.
- if ep == nil {
+ // Fail if we didn't find at least one matching transport endpoint.
+ if len(destEps) == 0 {
// UDP packet could not be delivered to an unknown destination port.
if protocol == header.UDPProtocolNumber {
r.Stats().UDP.UnknownPortErrors.Increment()
@@ -246,7 +288,9 @@ func (d *transportDemuxer) deliverPacket(r *Route, protocol tcpip.TransportProto
}
// Deliver the packet.
- ep.HandlePacket(r, id, vv)
+ for _, ep := range destEps {
+ ep.HandlePacket(r, id, vv)
+ }
return true
}
@@ -277,7 +321,7 @@ func (d *transportDemuxer) deliverControlPacket(net tcpip.NetworkProtocolNumber,
func (d *transportDemuxer) findEndpointLocked(eps *transportEndpoints, vv buffer.VectorisedView, id TransportEndpointID) TransportEndpoint {
// Try to find a match with the id as provided.
- if ep := eps.endpoints[id]; ep != nil {
+ if ep, ok := eps.endpoints[id]; ok {
return ep
}
@@ -285,7 +329,7 @@ func (d *transportDemuxer) findEndpointLocked(eps *transportEndpoints, vv buffer
nid := id
nid.LocalAddress = ""
- if ep := eps.endpoints[nid]; ep != nil {
+ if ep, ok := eps.endpoints[nid]; ok {
return ep
}
@@ -293,11 +337,15 @@ func (d *transportDemuxer) findEndpointLocked(eps *transportEndpoints, vv buffer
nid.LocalAddress = id.LocalAddress
nid.RemoteAddress = ""
nid.RemotePort = 0
- if ep := eps.endpoints[nid]; ep != nil {
+ if ep, ok := eps.endpoints[nid]; ok {
return ep
}
// Try to find a match with only the local port.
nid.LocalAddress = ""
- return eps.endpoints[nid]
+ if ep, ok := eps.endpoints[nid]; ok {
+ return ep
+ }
+
+ return nil
}
diff --git a/pkg/tcpip/tcpip.go b/pkg/tcpip/tcpip.go
index a6e47397a..89e9d6741 100644
--- a/pkg/tcpip/tcpip.go
+++ b/pkg/tcpip/tcpip.go
@@ -100,6 +100,7 @@ var (
ErrNetworkUnreachable = &Error{msg: "network is unreachable"}
ErrMessageTooLong = &Error{msg: "message too long"}
ErrNoBufferSpace = &Error{msg: "no buffer space available"}
+ ErrBroadcastDisabled = &Error{msg: "broadcast socket option disabled"}
)
// Errors related to Subnet
@@ -502,6 +503,10 @@ type RemoveMembershipOption MembershipOption
// TCP out-of-band data is delivered along with the normal in-band data.
type OutOfBandInlineOption int
+// BroadcastOption is used by SetSockOpt/GetSockOpt to specify whether
+// datagram sockets are allowed to send packets to a broadcast address.
+type BroadcastOption int
+
// Route is a row in the routing table. It specifies through which NIC (and
// gateway) sets of packets should be routed. A row is considered viable if the
// masked target address matches the destination adddress in the row.
@@ -527,6 +532,12 @@ func (r *Route) Match(addr Address) bool {
return false
}
+ // Using header.Ipv4Broadcast would introduce an import cycle, so
+ // we'll use a literal instead.
+ if addr == "\xff\xff\xff\xff" {
+ return true
+ }
+
for i := 0; i < len(r.Destination); i++ {
if (addr[i] & r.Mask[i]) != r.Destination[i] {
return false
diff --git a/pkg/tcpip/transport/tcp/endpoint.go b/pkg/tcpip/transport/tcp/endpoint.go
index 1ee9f8d25..aa31a78af 100644
--- a/pkg/tcpip/transport/tcp/endpoint.go
+++ b/pkg/tcpip/transport/tcp/endpoint.go
@@ -116,6 +116,9 @@ type endpoint struct {
route stack.Route `state:"manual"`
v6only bool
isConnectNotified bool
+ // TCP should never broadcast but Linux nevertheless supports enabling/
+ // disabling SO_BROADCAST, albeit as a NOOP.
+ broadcast bool
// effectiveNetProtos contains the network protocols actually in use. In
// most cases it will only contain "netProto", but in cases like IPv6
@@ -813,6 +816,12 @@ func (e *endpoint) SetSockOpt(opt interface{}) *tcpip.Error {
e.notifyProtocolGoroutine(notifyKeepaliveChanged)
return nil
+ case tcpip.BroadcastOption:
+ e.mu.Lock()
+ e.broadcast = v != 0
+ e.mu.Unlock()
+ return nil
+
default:
return nil
}
@@ -971,6 +980,17 @@ func (e *endpoint) GetSockOpt(opt interface{}) *tcpip.Error {
*o = 1
return nil
+ case *tcpip.BroadcastOption:
+ e.mu.Lock()
+ v := e.broadcast
+ e.mu.Unlock()
+
+ *o = 0
+ if v {
+ *o = 1
+ }
+ return nil
+
default:
return tcpip.ErrUnknownProtocolOption
}
diff --git a/pkg/tcpip/transport/tcp/endpoint_state.go b/pkg/tcpip/transport/tcp/endpoint_state.go
index 4891c7941..a07cd9011 100644
--- a/pkg/tcpip/transport/tcp/endpoint_state.go
+++ b/pkg/tcpip/transport/tcp/endpoint_state.go
@@ -336,6 +336,7 @@ func loadError(s string) *tcpip.Error {
tcpip.ErrNetworkUnreachable,
tcpip.ErrMessageTooLong,
tcpip.ErrNoBufferSpace,
+ tcpip.ErrBroadcastDisabled,
}
messageToError = make(map[string]*tcpip.Error)
diff --git a/pkg/tcpip/transport/udp/endpoint.go b/pkg/tcpip/transport/udp/endpoint.go
index 9c3881d63..05d35e526 100644
--- a/pkg/tcpip/transport/udp/endpoint.go
+++ b/pkg/tcpip/transport/udp/endpoint.go
@@ -82,6 +82,7 @@ type endpoint struct {
multicastAddr tcpip.Address
multicastNICID tcpip.NICID
reusePort bool
+ broadcast bool
// shutdownFlags represent the current shutdown state of the endpoint.
shutdownFlags tcpip.ShutdownFlags
@@ -347,6 +348,10 @@ func (e *endpoint) Write(p tcpip.Payload, opts tcpip.WriteOptions) (uintptr, <-c
nicid = e.bindNICID
}
+ if to.Addr == header.IPv4Broadcast && !e.broadcast {
+ return 0, nil, tcpip.ErrBroadcastDisabled
+ }
+
r, _, _, err := e.connectRoute(nicid, *to)
if err != nil {
return 0, nil, err
@@ -502,6 +507,13 @@ func (e *endpoint) SetSockOpt(opt interface{}) *tcpip.Error {
e.mu.Lock()
e.reusePort = v != 0
e.mu.Unlock()
+
+ case tcpip.BroadcastOption:
+ e.mu.Lock()
+ e.broadcast = v != 0
+ e.mu.Unlock()
+
+ return nil
}
return nil
}
@@ -581,6 +593,17 @@ func (e *endpoint) GetSockOpt(opt interface{}) *tcpip.Error {
*o = 0
return nil
+ case *tcpip.BroadcastOption:
+ e.mu.RLock()
+ v := e.broadcast
+ e.mu.RUnlock()
+
+ *o = 0
+ if v {
+ *o = 1
+ }
+ return nil
+
default:
return tcpip.ErrUnknownProtocolOption
}
diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD
index 9da5204c1..beece8930 100644
--- a/test/syscalls/linux/BUILD
+++ b/test/syscalls/linux/BUILD
@@ -148,6 +148,7 @@ cc_library(
hdrs = ["ip_socket_test_util.h"],
deps = [
":socket_test_util",
+ "@com_google_absl//absl/strings",
],
)
@@ -1970,6 +1971,42 @@ cc_library(
alwayslink = 1,
)
+cc_library(
+ name = "socket_ipv4_udp_unbound_external_networking_test_cases",
+ testonly = 1,
+ srcs = [
+ "socket_ipv4_udp_unbound_external_networking.cc",
+ ],
+ hdrs = [
+ "socket_ipv4_udp_unbound_external_networking.h",
+ ],
+ deps = [
+ ":ip_socket_test_util",
+ ":socket_test_util",
+ "//test/util:test_util",
+ "@com_google_googletest//:gtest",
+ ],
+ alwayslink = 1,
+)
+
+cc_library(
+ name = "socket_ipv4_tcp_unbound_external_networking_test_cases",
+ testonly = 1,
+ srcs = [
+ "socket_ipv4_tcp_unbound_external_networking.cc",
+ ],
+ hdrs = [
+ "socket_ipv4_tcp_unbound_external_networking.h",
+ ],
+ deps = [
+ ":ip_socket_test_util",
+ ":socket_test_util",
+ "//test/util:test_util",
+ "@com_google_googletest//:gtest",
+ ],
+ alwayslink = 1,
+)
+
cc_binary(
name = "socket_abstract_test",
testonly = 1,
@@ -2148,6 +2185,38 @@ cc_binary(
)
cc_binary(
+ name = "socket_ipv4_udp_unbound_external_networking_test",
+ testonly = 1,
+ srcs = [
+ "socket_ipv4_udp_unbound_external_networking_test.cc",
+ ],
+ linkstatic = 1,
+ deps = [
+ ":ip_socket_test_util",
+ ":socket_ipv4_udp_unbound_external_networking_test_cases",
+ ":socket_test_util",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "socket_ipv4_tcp_unbound_external_networking_test",
+ testonly = 1,
+ srcs = [
+ "socket_ipv4_tcp_unbound_external_networking_test.cc",
+ ],
+ linkstatic = 1,
+ deps = [
+ ":ip_socket_test_util",
+ ":socket_ipv4_tcp_unbound_external_networking_test_cases",
+ ":socket_test_util",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
name = "socket_ip_udp_loopback_non_blocking_test",
testonly = 1,
srcs = [
diff --git a/test/syscalls/linux/ip_socket_test_util.cc b/test/syscalls/linux/ip_socket_test_util.cc
index f8232fc24..4ad787cc0 100644
--- a/test/syscalls/linux/ip_socket_test_util.cc
+++ b/test/syscalls/linux/ip_socket_test_util.cc
@@ -13,7 +13,9 @@
// limitations under the License.
#include <net/if.h>
+#include <netinet/in.h>
#include <sys/ioctl.h>
+#include <sys/socket.h>
#include <cstring>
#include "test/syscalls/linux/ip_socket_test_util.h"
@@ -95,5 +97,19 @@ SocketPairKind IPv4UDPUnboundSocketPair(int type) {
/* dual_stack = */ false)};
}
+SocketKind IPv4UDPUnboundSocket(int type) {
+ std::string description =
+ absl::StrCat(DescribeSocketType(type), "IPv4 UDP socket");
+ return SocketKind{description, UnboundSocketCreator(
+ AF_INET, type | SOCK_DGRAM, IPPROTO_UDP)};
+}
+
+SocketKind IPv4TCPUnboundSocket(int type) {
+ std::string description =
+ absl::StrCat(DescribeSocketType(type), "IPv4 TCP socket");
+ return SocketKind{description, UnboundSocketCreator(
+ AF_INET, type | SOCK_STREAM, IPPROTO_TCP)};
+}
+
} // namespace testing
} // namespace gvisor
diff --git a/test/syscalls/linux/ip_socket_test_util.h b/test/syscalls/linux/ip_socket_test_util.h
index a6721091a..cac790e64 100644
--- a/test/syscalls/linux/ip_socket_test_util.h
+++ b/test/syscalls/linux/ip_socket_test_util.h
@@ -58,6 +58,14 @@ SocketPairKind DualStackUDPBidirectionalBindSocketPair(int type);
// SocketPairs created with AF_INET and the given type.
SocketPairKind IPv4UDPUnboundSocketPair(int type);
+// IPv4UDPUnboundSocketPair returns a SocketKind that represents
+// a SimpleSocket created with AF_INET, SOCK_DGRAM, and the given type.
+SocketKind IPv4UDPUnboundSocket(int type);
+
+// IPv4TCPUnboundSocketPair returns a SocketKind that represents
+// a SimpleSocket created with AF_INET, SOCK_STREAM and the given type.
+SocketKind IPv4TCPUnboundSocket(int type);
+
} // namespace testing
} // namespace gvisor
diff --git a/test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking.cc b/test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking.cc
new file mode 100644
index 000000000..8e1c13ff4
--- /dev/null
+++ b/test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking.cc
@@ -0,0 +1,66 @@
+// Copyright 2019 Google LLC
+//
+// 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.
+
+#include "test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking.h"
+
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <cstdio>
+#include <cstring>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "gtest/gtest.h"
+#include "test/syscalls/linux/socket_test_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+// Verifies that a newly instantiated TCP socket does not have the
+// broadcast socket option enabled.
+TEST_P(IPv4TCPUnboundExternalNetworkingSocketTest, TCPBroadcastDefault) {
+ auto socket = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+
+ int get = -1;
+ socklen_t get_sz = sizeof(get);
+ EXPECT_THAT(
+ getsockopt(socket->get(), SOL_SOCKET, SO_BROADCAST, &get, &get_sz),
+ SyscallSucceedsWithValue(0));
+ EXPECT_EQ(get, kSockOptOff);
+ EXPECT_EQ(get_sz, sizeof(get));
+}
+
+// Verifies that a newly instantiated TCP socket returns true after enabling
+// the broadcast socket option.
+TEST_P(IPv4TCPUnboundExternalNetworkingSocketTest, SetTCPBroadcast) {
+ auto socket = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+
+ EXPECT_THAT(setsockopt(socket->get(), SOL_SOCKET, SO_BROADCAST, &kSockOptOn,
+ sizeof(kSockOptOn)),
+ SyscallSucceedsWithValue(0));
+
+ int get = -1;
+ socklen_t get_sz = sizeof(get);
+ EXPECT_THAT(
+ getsockopt(socket->get(), SOL_SOCKET, SO_BROADCAST, &get, &get_sz),
+ SyscallSucceedsWithValue(0));
+ EXPECT_EQ(get, kSockOptOn);
+ EXPECT_EQ(get_sz, sizeof(get));
+}
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking.h b/test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking.h
new file mode 100644
index 000000000..b23de08d1
--- /dev/null
+++ b/test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking.h
@@ -0,0 +1,30 @@
+// Copyright 2019 Google LLC
+//
+// 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.
+
+#ifndef GVISOR_TEST_SYSCALLS_LINUX_SOCKET_IPV4_TCP_UNBOUND_EXTERNAL_NETWORKING_H_
+#define GVISOR_TEST_SYSCALLS_LINUX_SOCKET_IPV4_TCP_UNBOUND_EXTERNAL_NETWORKING_H_
+
+#include "test/syscalls/linux/socket_test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+// Test fixture for tests that apply to unbound IPv4 TCP sockets in a sandbox
+// with external networking support.
+using IPv4TCPUnboundExternalNetworkingSocketTest = SimpleSocketTest;
+
+} // namespace testing
+} // namespace gvisor
+
+#endif // GVISOR_TEST_SYSCALLS_LINUX_SOCKET_IPV4_TCP_UNBOUND_EXTERNAL_NETWORKING_H_
diff --git a/test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking_test.cc b/test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking_test.cc
new file mode 100644
index 000000000..c6fb42641
--- /dev/null
+++ b/test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking_test.cc
@@ -0,0 +1,35 @@
+// Copyright 2019 Google LLC
+//
+// 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.
+
+#include <vector>
+
+#include "test/syscalls/linux/ip_socket_test_util.h"
+#include "test/syscalls/linux/socket_ipv4_tcp_unbound_external_networking.h"
+#include "test/syscalls/linux/socket_test_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+std::vector<SocketKind> GetSockets() {
+ return ApplyVec<SocketKind>(
+ IPv4TCPUnboundSocket,
+ AllBitwiseCombinations(List<int>{0, SOCK_NONBLOCK}));
+}
+
+INSTANTIATE_TEST_CASE_P(IPv4TCPSockets,
+ IPv4TCPUnboundExternalNetworkingSocketTest,
+ ::testing::ValuesIn(GetSockets()));
+} // namespace testing
+} // namespace gvisor
diff --git a/test/syscalls/linux/socket_ipv4_udp_unbound_external_networking.cc b/test/syscalls/linux/socket_ipv4_udp_unbound_external_networking.cc
new file mode 100644
index 000000000..7d561b991
--- /dev/null
+++ b/test/syscalls/linux/socket_ipv4_udp_unbound_external_networking.cc
@@ -0,0 +1,231 @@
+// Copyright 2019 Google LLC
+//
+// 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.
+
+#include "test/syscalls/linux/socket_ipv4_udp_unbound_external_networking.h"
+
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <cstdio>
+#include <cstring>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "gtest/gtest.h"
+#include "test/syscalls/linux/socket_test_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+// Verifies that a newly instantiated UDP socket does not have the
+// broadcast socket option enabled.
+TEST_P(IPv4UDPUnboundExternalNetworkingSocketTest, UDPBroadcastDefault) {
+ auto socket = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+
+ int get = -1;
+ socklen_t get_sz = sizeof(get);
+ EXPECT_THAT(
+ getsockopt(socket->get(), SOL_SOCKET, SO_BROADCAST, &get, &get_sz),
+ SyscallSucceedsWithValue(0));
+ EXPECT_EQ(get, kSockOptOff);
+ EXPECT_EQ(get_sz, sizeof(get));
+}
+
+// Verifies that a newly instantiated UDP socket returns true after enabling
+// the broadcast socket option.
+TEST_P(IPv4UDPUnboundExternalNetworkingSocketTest, SetUDPBroadcast) {
+ auto socket = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+
+ EXPECT_THAT(setsockopt(socket->get(), SOL_SOCKET, SO_BROADCAST, &kSockOptOn,
+ sizeof(kSockOptOn)),
+ SyscallSucceedsWithValue(0));
+
+ int get = -1;
+ socklen_t get_sz = sizeof(get);
+ EXPECT_THAT(
+ getsockopt(socket->get(), SOL_SOCKET, SO_BROADCAST, &get, &get_sz),
+ SyscallSucceedsWithValue(0));
+ EXPECT_EQ(get, kSockOptOn);
+ EXPECT_EQ(get_sz, sizeof(get));
+}
+
+// Verifies that a broadcast UDP packet will arrive at all UDP sockets with
+// the destination port number.
+TEST_P(IPv4UDPUnboundExternalNetworkingSocketTest,
+ UDPBroadcastReceivedOnAllExpectedEndpoints) {
+ auto sender = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+ auto rcvr1 = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+ auto rcvr2 = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+ auto norcv = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+
+ // Enable SO_BROADCAST on the sending socket.
+ ASSERT_THAT(setsockopt(sender->get(), SOL_SOCKET, SO_BROADCAST, &kSockOptOn,
+ sizeof(kSockOptOn)),
+ SyscallSucceedsWithValue(0));
+
+ // Enable SO_REUSEPORT on the receiving sockets so that they may both be bound
+ // to the broadcast messages destination port.
+ ASSERT_THAT(setsockopt(rcvr1->get(), SOL_SOCKET, SO_REUSEPORT, &kSockOptOn,
+ sizeof(kSockOptOn)),
+ SyscallSucceedsWithValue(0));
+ ASSERT_THAT(setsockopt(rcvr2->get(), SOL_SOCKET, SO_REUSEPORT, &kSockOptOn,
+ sizeof(kSockOptOn)),
+ SyscallSucceedsWithValue(0));
+
+ sockaddr_in rcv_addr = {};
+ socklen_t rcv_addr_sz = sizeof(rcv_addr);
+ rcv_addr.sin_family = AF_INET;
+ rcv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
+ ASSERT_THAT(bind(rcvr1->get(), reinterpret_cast<struct sockaddr*>(&rcv_addr),
+ rcv_addr_sz),
+ SyscallSucceedsWithValue(0));
+ // Retrieve port number from first socket so that it can be bound to the
+ // second socket.
+ rcv_addr = {};
+ ASSERT_THAT(
+ getsockname(rcvr1->get(), reinterpret_cast<struct sockaddr*>(&rcv_addr),
+ &rcv_addr_sz),
+ SyscallSucceedsWithValue(0));
+ ASSERT_THAT(bind(rcvr2->get(), reinterpret_cast<struct sockaddr*>(&rcv_addr),
+ rcv_addr_sz),
+ SyscallSucceedsWithValue(0));
+
+ // Bind the non-receiving socket to an ephemeral port.
+ sockaddr_in norcv_addr = {};
+ norcv_addr.sin_family = AF_INET;
+ norcv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
+ ASSERT_THAT(
+ bind(norcv->get(), reinterpret_cast<struct sockaddr*>(&norcv_addr),
+ sizeof(norcv_addr)),
+ SyscallSucceedsWithValue(0));
+
+ // Broadcast a test message.
+ sockaddr_in dst_addr = {};
+ dst_addr.sin_family = AF_INET;
+ dst_addr.sin_addr.s_addr = htonl(INADDR_BROADCAST);
+ dst_addr.sin_port = rcv_addr.sin_port;
+ constexpr char kTestMsg[] = "hello, world";
+ EXPECT_THAT(
+ sendto(sender->get(), kTestMsg, sizeof(kTestMsg), 0,
+ reinterpret_cast<struct sockaddr*>(&dst_addr), sizeof(dst_addr)),
+ SyscallSucceedsWithValue(sizeof(kTestMsg)));
+
+ // Verify that the receiving sockets received the test message.
+ char buf[sizeof(kTestMsg)] = {};
+ EXPECT_THAT(read(rcvr1->get(), buf, sizeof(buf)),
+ SyscallSucceedsWithValue(sizeof(kTestMsg)));
+ EXPECT_EQ(0, memcmp(buf, kTestMsg, sizeof(kTestMsg)));
+ memset(buf, 0, sizeof(buf));
+ EXPECT_THAT(read(rcvr2->get(), buf, sizeof(buf)),
+ SyscallSucceedsWithValue(sizeof(kTestMsg)));
+ EXPECT_EQ(0, memcmp(buf, kTestMsg, sizeof(kTestMsg)));
+
+ // Verify that the non-receiving socket did not receive the test message.
+ memset(buf, 0, sizeof(buf));
+ EXPECT_THAT(RetryEINTR(recv)(norcv->get(), buf, sizeof(buf), MSG_DONTWAIT),
+ SyscallFailsWithErrno(EAGAIN));
+}
+
+// Verifies that a UDP broadcast sent via the loopback interface is not received
+// by the sender.
+TEST_P(IPv4UDPUnboundExternalNetworkingSocketTest,
+ UDPBroadcastViaLoopbackFails) {
+ auto sender = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+
+ // Enable SO_BROADCAST.
+ ASSERT_THAT(setsockopt(sender->get(), SOL_SOCKET, SO_BROADCAST, &kSockOptOn,
+ sizeof(kSockOptOn)),
+ SyscallSucceedsWithValue(0));
+
+ // Bind the sender to the loopback interface.
+ sockaddr_in src = {};
+ socklen_t src_sz = sizeof(src);
+ src.sin_family = AF_INET;
+ src.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ ASSERT_THAT(
+ bind(sender->get(), reinterpret_cast<struct sockaddr*>(&src), src_sz),
+ SyscallSucceedsWithValue(0));
+ ASSERT_THAT(getsockname(sender->get(),
+ reinterpret_cast<struct sockaddr*>(&src), &src_sz),
+ SyscallSucceedsWithValue(0));
+ ASSERT_EQ(src.sin_addr.s_addr, htonl(INADDR_LOOPBACK));
+
+ // Send the message.
+ sockaddr_in dst = {};
+ dst.sin_family = AF_INET;
+ dst.sin_addr.s_addr = htonl(INADDR_BROADCAST);
+ dst.sin_port = src.sin_port;
+ constexpr char kTestMsg[] = "hello, world";
+ EXPECT_THAT(sendto(sender->get(), kTestMsg, sizeof(kTestMsg), 0,
+ reinterpret_cast<struct sockaddr*>(&dst), sizeof(dst)),
+ SyscallSucceedsWithValue(sizeof(kTestMsg)));
+
+ // Verify that the message was not received by the sender (loopback).
+ char buf[sizeof(kTestMsg)] = {};
+ EXPECT_THAT(RetryEINTR(recv)(sender->get(), buf, sizeof(buf), MSG_DONTWAIT),
+ SyscallFailsWithErrno(EAGAIN));
+}
+
+// Verifies that a UDP broadcast fails to send on a socket with SO_BROADCAST
+// disabled.
+TEST_P(IPv4UDPUnboundExternalNetworkingSocketTest, TestSendBroadcast) {
+ auto sender = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+
+ // Broadcast a test message without having enabled SO_BROADCAST on the sending
+ // socket.
+ sockaddr_in addr = {};
+ socklen_t addr_sz = sizeof(addr);
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(12345);
+ addr.sin_addr.s_addr = htonl(INADDR_BROADCAST);
+ constexpr char kTestMsg[] = "hello, world";
+
+ EXPECT_THAT(sendto(sender->get(), kTestMsg, sizeof(kTestMsg), 0,
+ reinterpret_cast<struct sockaddr*>(&addr), addr_sz),
+ SyscallFailsWithErrno(EACCES));
+}
+
+// Verifies that a UDP unicast on an unbound socket reaches its destination.
+TEST_P(IPv4UDPUnboundExternalNetworkingSocketTest, TestSendUnicastOnUnbound) {
+ auto sender = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+ auto rcvr = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+
+ // Bind the receiver and retrieve its address and port number.
+ sockaddr_in addr = {};
+ addr.sin_family = AF_INET;
+ addr.sin_addr.s_addr = htonl(INADDR_ANY);
+ addr.sin_port = htons(0);
+ ASSERT_THAT(bind(rcvr->get(), reinterpret_cast<struct sockaddr*>(&addr),
+ sizeof(addr)),
+ SyscallSucceedsWithValue(0));
+ memset(&addr, 0, sizeof(addr));
+ socklen_t addr_sz = sizeof(addr);
+ ASSERT_THAT(getsockname(rcvr->get(),
+ reinterpret_cast<struct sockaddr*>(&addr), &addr_sz),
+ SyscallSucceedsWithValue(0));
+
+ // Send a test message to the receiver.
+ constexpr char kTestMsg[] = "hello, world";
+ ASSERT_THAT(sendto(sender->get(), kTestMsg, sizeof(kTestMsg), 0,
+ reinterpret_cast<struct sockaddr*>(&addr), addr_sz),
+ SyscallSucceedsWithValue(sizeof(kTestMsg)));
+ char buf[sizeof(kTestMsg)] = {};
+ ASSERT_THAT(read(rcvr->get(), buf, sizeof(buf)),
+ SyscallSucceedsWithValue(sizeof(kTestMsg)));
+}
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/syscalls/linux/socket_ipv4_udp_unbound_external_networking.h b/test/syscalls/linux/socket_ipv4_udp_unbound_external_networking.h
new file mode 100644
index 000000000..5cf9fa8eb
--- /dev/null
+++ b/test/syscalls/linux/socket_ipv4_udp_unbound_external_networking.h
@@ -0,0 +1,30 @@
+// Copyright 2019 Google LLC
+//
+// 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.
+
+#ifndef GVISOR_TEST_SYSCALLS_LINUX_SOCKET_IPV4_UDP_UNBOUND_EXTERNAL_NETWORKING_H_
+#define GVISOR_TEST_SYSCALLS_LINUX_SOCKET_IPV4_UDP_UNBOUND_EXTERNAL_NETWORKING_H_
+
+#include "test/syscalls/linux/socket_test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+// Test fixture for tests that apply to unbound IPv4 UDP sockets in a sandbox
+// with external networking support.
+using IPv4UDPUnboundExternalNetworkingSocketTest = SimpleSocketTest;
+
+} // namespace testing
+} // namespace gvisor
+
+#endif // GVISOR_TEST_SYSCALLS_LINUX_SOCKET_IPV4_UDP_UNBOUND_EXTERNAL_NETWORKING_H_
diff --git a/test/syscalls/linux/socket_ipv4_udp_unbound_external_networking_test.cc b/test/syscalls/linux/socket_ipv4_udp_unbound_external_networking_test.cc
new file mode 100644
index 000000000..e07385134
--- /dev/null
+++ b/test/syscalls/linux/socket_ipv4_udp_unbound_external_networking_test.cc
@@ -0,0 +1,35 @@
+// Copyright 2019 Google LLC
+//
+// 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.
+
+#include <vector>
+
+#include "test/syscalls/linux/ip_socket_test_util.h"
+#include "test/syscalls/linux/socket_ipv4_udp_unbound_external_networking.h"
+#include "test/syscalls/linux/socket_test_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+std::vector<SocketKind> GetSockets() {
+ return ApplyVec<SocketKind>(
+ IPv4UDPUnboundSocket,
+ AllBitwiseCombinations(List<int>{0, SOCK_NONBLOCK}));
+}
+
+INSTANTIATE_TEST_CASE_P(IPv4UDPSockets,
+ IPv4UDPUnboundExternalNetworkingSocketTest,
+ ::testing::ValuesIn(GetSockets()));
+} // namespace testing
+} // namespace gvisor
diff --git a/test/syscalls/linux/socket_test_util.cc b/test/syscalls/linux/socket_test_util.cc
index 8d19f79ac..035087566 100644
--- a/test/syscalls/linux/socket_test_util.cc
+++ b/test/syscalls/linux/socket_test_util.cc
@@ -22,6 +22,7 @@
#include "absl/memory/memory.h"
#include "absl/strings/str_cat.h"
#include "absl/time/clock.h"
+#include "test/util/file_descriptor.h"
#include "test/util/posix_error.h"
#include "test/util/temp_path.h"
#include "test/util/thread_util.h"
@@ -463,6 +464,17 @@ SocketPairKind Reversed(SocketPairKind const& base) {
}};
}
+Creator<FileDescriptor> UnboundSocketCreator(int domain, int type,
+ int protocol) {
+ return [=]() -> PosixErrorOr<std::unique_ptr<FileDescriptor>> {
+ int sock;
+ RETURN_ERROR_IF_SYSCALL_FAIL(sock = socket(domain, type, protocol));
+ MaybeSave(); // Successful socket creation.
+
+ return absl::make_unique<FileDescriptor>(sock);
+ };
+}
+
std::vector<SocketPairKind> IncludeReversals(std::vector<SocketPairKind> vec) {
return ApplyVecToVec<SocketPairKind>(std::vector<Middleware>{NoOp, Reversed},
vec);
diff --git a/test/syscalls/linux/socket_test_util.h b/test/syscalls/linux/socket_test_util.h
index 906b3e929..dfabdf179 100644
--- a/test/syscalls/linux/socket_test_util.h
+++ b/test/syscalls/linux/socket_test_util.h
@@ -278,6 +278,11 @@ Creator<SocketPair> UDPBidirectionalBindSocketPairCreator(int domain, int type,
Creator<SocketPair> UDPUnboundSocketPairCreator(int domain, int type,
int protocol, bool dual_stack);
+// UnboundSocketCreator returns a Creator<FileDescriptor> that obtains a file
+// descriptor by creating a socket.
+Creator<FileDescriptor> UnboundSocketCreator(int domain, int type,
+ int protocol);
+
// A SocketPairKind couples a human-readable description of a socket pair with
// a function that creates such a socket pair.
struct SocketPairKind {