summaryrefslogtreecommitdiffhomepage
path: root/test/packetimpact
diff options
context:
space:
mode:
Diffstat (limited to 'test/packetimpact')
-rw-r--r--test/packetimpact/README.md304
-rw-r--r--test/packetimpact/testbench/connections.go270
-rw-r--r--test/packetimpact/testbench/layers.go457
-rw-r--r--test/packetimpact/testbench/layers_test.go80
-rw-r--r--test/packetimpact/tests/BUILD13
-rw-r--r--test/packetimpact/tests/Dockerfile17
-rw-r--r--test/packetimpact/tests/fin_wait2_timeout_test.go4
-rw-r--r--test/packetimpact/tests/icmpv6_param_problem_test.go73
-rwxr-xr-xtest/packetimpact/tests/test_runner.sh41
9 files changed, 1081 insertions, 178 deletions
diff --git a/test/packetimpact/README.md b/test/packetimpact/README.md
index ece4dedc6..a82ad996a 100644
--- a/test/packetimpact/README.md
+++ b/test/packetimpact/README.md
@@ -29,24 +29,24 @@ There are a few ways to write networking tests for gVisor currently:
The right choice depends on the needs of the test.
-Feature | Go unit test | syscall test | packetdrill | packetimpact
-------------- | ------------ | ------------ | ----------- | ------------
-Multiplatform | no | **YES** | **YES** | **YES**
-Concise | no | somewhat | somewhat | **VERY**
-Control-flow | **YES** | **YES** | no | **YES**
-Flexible | **VERY** | no | somewhat | **VERY**
+Feature | Go unit test | syscall test | packetdrill | packetimpact
+-------------- | ------------ | ------------ | ----------- | ------------
+Multi-platform | no | **YES** | **YES** | **YES**
+Concise | no | somewhat | somewhat | **VERY**
+Control-flow | **YES** | **YES** | no | **YES**
+Flexible | **VERY** | no | somewhat | **VERY**
### Go unit tests
If the test depends on the internals of gVisor and doesn't need to run on Linux
or other platforms for comparison purposes, a Go unit test can be appropriate.
They can observe internals of gVisor networking. The downside is that they are
-**not concise** and **not multiplatform**. If you require insight on gVisor
+**not concise** and **not multi-platform**. If you require insight on gVisor
internals, this is the right choice.
### Syscall tests
-Syscall tests are **multiplatform** but cannot examine the internals of gVisor
+Syscall tests are **multi-platform** but cannot examine the internals of gVisor
networking. They are **concise**. They can use **control-flow** structures like
conditionals, for loops, and variables. However, they are limited to only what
the POSIX interface provides so they are **not flexible**. For example, you
@@ -57,7 +57,7 @@ protocols, wrong sequence numbers, etc.
### Packetdrill tests
-Packetdrill tests are **multiplatform** and can run against both Linux and
+Packetdrill tests are **multi-platform** and can run against both Linux and
gVisor. They are **concise** and use a special packetdrill scripting language.
They are **more flexible** than a syscall test in that they can send packets
that a syscall test would have difficulty sending, like a packet with a
@@ -73,7 +73,7 @@ other side supports window scaling, for example.
Packetimpact tests are similar to Packetdrill tests except that they are written
in Go instead of the packetdrill scripting language. That gives them all the
**control-flow** abilities of Go (loops, functions, variables, etc). They are
-**multiplatform** in the same way as packetdrill tests but even more
+**multi-platform** in the same way as packetdrill tests but even more
**flexible** because Go is more expressive than the scripting language of
packetdrill. However, Go is **not as concise** as the packetdrill language. Many
design decisions below are made to mitigate that.
@@ -81,21 +81,27 @@ design decisions below are made to mitigate that.
## How it works
```
- +--------------+ +--------------+
- | | TEST NET | |
- | | <===========> | Device |
- | Test | | Under |
- | Bench | | Test |
- | | <===========> | (DUT) |
- | | CONTROL NET | |
- +--------------+ +--------------+
+ Testbench Device-Under-Test (DUT)
+ +-------------------+ +------------------------+
+ | | TEST NET | |
+ | rawsockets.go <-->| <===========> | <---+ |
+ | ^ | | | |
+ | | | | | |
+ | v | | | |
+ | unittest | | | |
+ | ^ | | | |
+ | | | | | |
+ | v | | v |
+ | dut.go <========gRPC========> posix server |
+ | | CONTROL NET | |
+ +-------------------+ +------------------------+
```
-Two docker containers are created by a script, one for the test bench and the
-other for the device under test (DUT). The script connects the two containers
-with a control network and test network. It also does some other tasks like
-waiting until the DUT is ready before starting the test and disabling Linux
-networking that would interfere with the test bench.
+Two docker containers are created by a "runner" script, one for the testbench
+and the other for the device under test (DUT). The script connects the two
+containers with a control network and test network. It also does some other
+tasks like waiting until the DUT is ready before starting the test and disabling
+Linux networking that would interfere with the test bench.
### DUT
@@ -220,7 +226,8 @@ func (i *Injector) Send(b []byte) {...}
container and in practice, the container doesn't recognize binaries built on
the host if they use cgo.
* Both gVisor and gopacket have the ability to read and write pcap files
- without cgo but that is insufficient here.
+ without cgo but that is insufficient here because we can't just replay pcap
+ files, we need a more dynamic solution.
* The sniffer and injector can't share a socket because they need to be bound
differently.
* Sniffing could have been done asynchronously with channels, obviating the
@@ -270,11 +277,10 @@ but with a pointer for each field that may be `nil`.
* Many functions, one per field, like: `filterByFlag(myBytes, SYN)`,
`filterByLength(myBytes, 20)`, `filterByNextProto(myBytes, 0x8000)`,
etc.
- * Using pointers allows us to combine `Layer`s with a one-line call to
- `mergo.Merge(...)`. So the default `Layers` can be overridden by a
- `Layers` with just the TCP conection's src/dst which can be overridden
- by one with just a test specific TCP window size. Each override is
- specified as just one call to `mergo.Merge`.
+ * Using pointers allows us to combine `Layer`s with reflection. So the
+ default `Layers` can be overridden by a `Layers` with just the TCP
+ conection's src/dst which can be overridden by one with just a test
+ specific TCP window size.
* It's a proven way to separate the details of a packet from the byte
format as shown by scapy's success.
* Use packetgo. It's more general than parsing packets with gVisor. However:
@@ -334,6 +340,14 @@ type Layer interface {
}
```
+The `next` and `prev` make up a link listed so that each layer can get at the
+information in the layer around it. This is necessary for some protocols, like
+TCP that needs the layer before and payload after to compute the checksum. Any
+sequence of `Layer` structs is valid so long as the parser and `toBytes`
+functions can map from type to protool number and vice-versa. When the mapping
+fails, an error is emitted explaining what functionality is missing. The
+solution is either to fix the ordering or implement the missing protocol.
+
For each `Layer` there is also a parsing function. For example, this one is for
Ethernet:
@@ -392,81 +406,217 @@ for {
##### Alternatives considered
* Don't use previous and next pointers.
- * Each layer may need to be able to interrogate the layers aroung it, like
+ * Each layer may need to be able to interrogate the layers around it, like
for computing the next protocol number or total length. So *some*
mechanism is needed for a `Layer` to see neighboring layers.
* We could pass the entire array `Layers` to the `toBytes()` function.
Passing an array to a method that includes in the array the function
receiver itself seems wrong.
-#### Connections
+#### `layerState`
-Using `Layers` above, we can create connection structures to maintain state
-about connections. For example, here is the `TCPIPv4` struct:
+`Layers` represents the different headers of a packet but a connection includes
+more state. For example, a TCP connection needs to keep track of the next
+expected sequence number and also the next sequence number to send. This is
+stored in a `layerState` struct. This is the `layerState` for TCP:
+```go
+// tcpState maintains state about a TCP connection.
+type tcpState struct {
+ out, in TCP
+ localSeqNum, remoteSeqNum *seqnum.Value
+ synAck *TCP
+ portPickerFD int
+ finSent bool
+}
```
-type TCPIPv4 struct {
- outgoing Layers
- incoming Layers
- localSeqNum uint32
- remoteSeqNum uint32
- sniffer Sniffer
- injector Injector
- t *testing.T
+
+The next sequence numbers for each side of the connection are stored. `out` and
+`in` have defaults for the TCP header, such as the expected source and
+destination ports for outgoing packets and incoming packets.
+
+##### `layerState` interface
+
+```go
+// layerState stores the state of a layer of a connection.
+type layerState interface {
+ // outgoing returns an outgoing layer to be sent in a frame.
+ outgoing() Layer
+
+ // incoming creates an expected Layer for comparing against a received Layer.
+ // Because the expectation can depend on values in the received Layer, it is
+ // an input to incoming. For example, the ACK number needs to be checked in a
+ // TCP packet but only if the ACK flag is set in the received packet.
+ incoming(received Layer) Layer
+
+ // sent updates the layerState based on the Layer that was sent. The input is
+ // a Layer with all prev and next pointers populated so that the entire frame
+ // as it was sent is available.
+ sent(sent Layer) error
+
+ // received updates the layerState based on a Layer that is receieved. The
+ // input is a Layer with all prev and next pointers populated so that the
+ // entire frame as it was receieved is available.
+ received(received Layer) error
+
+ // close frees associated resources held by the LayerState.
+ close() error
}
```
-`TCPIPv4` contains an `outgoing Layers` which holds the defaults for the
-connection, such as the source and destination MACs, IPs, and ports. When
-`outgoing.toBytes()` is called a valid packet for this TCPIPv4 flow is built.
+`outgoing` generates the default Layer for an outgoing packet. For TCP, this
+would be a `TCP` with the source and destination ports populated. Because they
+are static, they are stored inside the `out` member of `tcpState`. However, the
+sequence numbers change frequently so the outgoing sequence number is stored in
+the `localSeqNum` and put into the output of outgoing for each call.
+
+`incoming` does the same functions for packets that arrive but instead of
+generating a packet to send, it generates an expect packet for filtering packets
+that arrive. For example, if a `TCP` header arrives with the wrong ports, it can
+be ignored as belonging to a different connection. `incoming` needs the received
+header itself as an input because the filter may depend on the input. For
+example, the expected sequence number depends on the flags in the TCP header.
+
+`sent` and `received` are run for each header that is actually sent or received
+and used to update the internal state. `incoming` and `outgoing` should *not* be
+used for these purpose. For example, `incoming` is called on every packet that
+arrives but only packets that match ought to actually update the state.
+`outgoing` is called to created outgoing packets and those packets are always
+sent, so unlike `incoming`/`received`, there is one `outgoing` call for each
+`sent` call.
+
+`close` cleans up after the layerState. For example, TCP and UDP need to keep a
+port reserved and then release it.
+
+#### Connections
+
+Using `layerState` above, we can create connections.
-It also contains `incoming Layers` which holds filter for incoming packets that
-belong to this flow. `incoming.match(Layers)` is used on received bytes to check
-if they are part of the flow.
+```go
+// Connection holds a collection of layer states for maintaining a connection
+// along with sockets for sniffer and injecting packets.
+type Connection struct {
+ layerStates []layerState
+ injector Injector
+ sniffer Sniffer
+ t *testing.T
+}
+```
-The `sniffer` and `injector` are for receiving and sending raw packet bytes. The
-`localSeqNum` and `remoteSeqNum` are updated by `Send` and `Recv` so that
-outgoing packets will have, by default, the correct sequence number and ack
-number.
+The connection stores an array of `layerState` in the order that the headers
+should be present in the frame to send. For example, Ether then IPv4 then TCP.
+The injector and sniffer are for writing and reading frames. A `*testing.T` is
+stored so that internal errors can be reported directly without code in the unit
+test.
-TCPIPv4 provides some functions:
+The `Connection` has some useful functions:
+```go
+// Close frees associated resources held by the Connection.
+func (conn *Connection) Close() {...}
+// CreateFrame builds a frame for the connection with layer overriding defaults
+// of the innermost layer and additionalLayers added after it.
+func (conn *Connection) CreateFrame(layer Layer, additionalLayers ...Layer) Layers {...}
+// SendFrame sends a frame on the wire and updates the state of all layers.
+func (conn *Connection) SendFrame(frame Layers) {...}
+// Send a packet with reasonable defaults. Potentially override the final layer
+// in the connection with the provided layer and add additionLayers.
+func (conn *Connection) Send(layer Layer, additionalLayers ...Layer) {...}
+// Expect a frame with the final layerStates layer matching the provided Layer
+// within the timeout specified. If it doesn't arrive in time, it returns nil.
+func (conn *Connection) Expect(layer Layer, timeout time.Duration) (Layer, error) {...}
+// ExpectFrame expects a frame that matches the provided Layers within the
+// timeout specified. If it doesn't arrive in time, it returns nil.
+func (conn *Connection) ExpectFrame(layers Layers, timeout time.Duration) (Layers, error) {...}
+// Drain drains the sniffer's receive buffer by receiving packets until there's
+// nothing else to receive.
+func (conn *Connection) Drain() {...}
```
-func (conn *TCPIPv4) Send(tcp TCP) {...}
-func (conn *TCPIPv4) Recv(timeout time.Duration) *TCP {...}
+
+`CreateFrame` uses the `[]layerState` to create a frame to send. The first
+argument is for overriding defaults in the last header of the frame, because
+this is the most common need. For a TCPIPv4 connection, this would be the TCP
+header. Optional additionalLayers can be specified to add to the frame being
+created, such as a `Payload` for `TCP`.
+
+`SendFrame` sends the frame to the DUT. It is combined with `CreateFrame` to
+make `Send`. For unittests with basic sending needs, `Send` can be used. If more
+control is needed over the frame, it can be made with `CreateFrame`, modified in
+the unit test, and then sent with `SendFrame`.
+
+On the receiving side, there is `Expect` and `ExpectFrame`. Like with the
+sending side, there are two forms of each function, one for just the last header
+and one for the whole frame. The expect functions use the `[]layerState` to
+create a template for the expected incoming frame. That frame is then overridden
+by the values in the first argument. Finally, a loop starts sniffing packets on
+the wire for frames. If a matching frame is found before the timeout, it is
+returned without error. If not, nil is returned and the error contains text of
+all the received frames that didn't match. Exactly one of the outputs will be
+non-nil, even if no frames are received at all.
+
+`Drain` sniffs and discards all the frames that have yet to be received. A
+common way to write a test is:
+
+```go
+conn.Drain() // Discard all outstanding frames.
+conn.Send(...) // Send a frame with overrides.
+// Now expect a frame with a certain header and fail if it doesn't arrive.
+if _, err := conn.Expect(...); err != nil { t.Fatal(...) }
```
-`Send(tcp TCP)` uses [mergo](https://github.com/imdario/mergo) to merge the
-provided `TCP` (a `Layer`) into `outgoing`. This way the user can specify
-concisely just which fields of `outgoing` to modify. The packet is sent using
-the `injector`.
+Or for a test where we want to check that no frame arrives:
-`Recv(timeout time.Duration)` reads packets from the sniffer until either the
-timeout has elapsed or a packet that matches `incoming` arrives.
+```go
+if gotOne, _ := conn.Expect(...); gotOne != nil { t.Fatal(...) }
+```
+
+#### Specializing `Connection`
-Using those, we can perform a TCP 3-way handshake without too much code:
+Because there are some common combinations of `layerState` into `Connection`,
+they are defined:
```go
-func (conn *TCPIPv4) Handshake() {
- syn := uint8(header.TCPFlagSyn)
- synack := uint8(header.TCPFlagSyn)
- ack := uint8(header.TCPFlagAck)
- conn.Send(TCP{Flags: &syn}) // Send a packet with all defaults but set TCP-SYN.
-
- // Wait for the SYN-ACK response.
- for {
- newTCP := conn.Recv(time.Second) // This already filters by MAC, IP, and ports.
- if TCP{Flags: &synack}.match(newTCP) {
- break // Only if it's a SYN-ACK proceed.
- }
- }
+// TCPIPv4 maintains the state for all the layers in a TCP/IPv4 connection.
+type TCPIPv4 Connection
+// UDPIPv4 maintains the state for all the layers in a UDP/IPv4 connection.
+type UDPIPv4 Connection
+```
+
+Each has a `NewXxx` function to create a new connection with reasonable
+defaults. They also have functions that call the underlying `Connection`
+functions but with specialization and tighter type-checking. For example:
- conn.Send(TCP{Flags: &ack}) // Send an ACK. The seq and ack numbers are set correctly.
+```go
+func (conn *TCPIPv4) Send(tcp TCP, additionalLayers ...Layer) {
+ (*Connection)(conn).Send(&tcp, additionalLayers...)
+}
+func (conn *TCPIPv4) Drain() {
+ conn.sniffer.Drain()
+}
+```
+
+They may also have some accessors to get or set the internal state of the
+connection:
+
+```go
+func (conn *TCPIPv4) state() *tcpState {
+ state, ok := conn.layerStates[len(conn.layerStates)-1].(*tcpState)
+ if !ok {
+ conn.t.Fatalf("expected final state of %v to be tcpState", conn.layerStates)
+ }
+ return state
+}
+func (conn *TCPIPv4) RemoteSeqNum() *seqnum.Value {
+ return conn.state().remoteSeqNum
+}
+func (conn *TCPIPv4) LocalSeqNum() *seqnum.Value {
+ return conn.state().localSeqNum
}
```
-The handshake code is part of the testbench utilities so tests can share this
-common sequence, making tests even more concise.
+Unittests will in practice use these functions and not the functions on
+`Connection`. For example, `NewTCPIPv4()` and then call `Send` on that rather
+than cast is to a `Connection` and call `Send` on that cast result.
##### Alternatives considered
diff --git a/test/packetimpact/testbench/connections.go b/test/packetimpact/testbench/connections.go
index 952a717e0..2280bd4ee 100644
--- a/test/packetimpact/testbench/connections.go
+++ b/test/packetimpact/testbench/connections.go
@@ -21,7 +21,6 @@ import (
"fmt"
"math/rand"
"net"
- "strings"
"testing"
"time"
@@ -35,45 +34,74 @@ import (
var localIPv4 = flag.String("local_ipv4", "", "local IPv4 address for test packets")
var remoteIPv4 = flag.String("remote_ipv4", "", "remote IPv4 address for test packets")
+var localIPv6 = flag.String("local_ipv6", "", "local IPv6 address for test packets")
+var remoteIPv6 = flag.String("remote_ipv6", "", "remote IPv6 address for test packets")
var localMAC = flag.String("local_mac", "", "local mac address for test packets")
var remoteMAC = flag.String("remote_mac", "", "remote mac address for test packets")
-// pickPort makes a new socket and returns the socket FD and port. The caller
-// must close the FD when done with the port if there is no error.
-func pickPort() (int, uint16, error) {
- fd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, 0)
+// pickPort makes a new socket and returns the socket FD and port. The domain
+// should be AF_INET or AF_INET6. The caller must close the FD when done with
+// the port if there is no error.
+func pickPort(domain, typ int) (fd int, port uint16, err error) {
+ fd, err = unix.Socket(domain, typ, 0)
if err != nil {
return -1, 0, err
}
- var sa unix.SockaddrInet4
- copy(sa.Addr[0:4], net.ParseIP(*localIPv4).To4())
- if err := unix.Bind(fd, &sa); err != nil {
- unix.Close(fd)
+ defer func() {
+ if err != nil {
+ err = multierr.Append(err, unix.Close(fd))
+ }
+ }()
+ var sa unix.Sockaddr
+ switch domain {
+ case unix.AF_INET:
+ var sa4 unix.SockaddrInet4
+ copy(sa4.Addr[:], net.ParseIP(*localIPv4).To4())
+ sa = &sa4
+ case unix.AF_INET6:
+ var sa6 unix.SockaddrInet6
+ copy(sa6.Addr[:], net.ParseIP(*localIPv6).To16())
+ sa = &sa6
+ default:
+ return -1, 0, fmt.Errorf("invalid domain %d, it should be one of unix.AF_INET or unix.AF_INET6", domain)
+ }
+ if err = unix.Bind(fd, sa); err != nil {
return -1, 0, err
}
newSockAddr, err := unix.Getsockname(fd)
if err != nil {
- unix.Close(fd)
return -1, 0, err
}
- newSockAddrInet4, ok := newSockAddr.(*unix.SockaddrInet4)
- if !ok {
- unix.Close(fd)
- return -1, 0, fmt.Errorf("can't cast Getsockname result to SockaddrInet4")
+ switch domain {
+ case unix.AF_INET:
+ newSockAddrInet4, ok := newSockAddr.(*unix.SockaddrInet4)
+ if !ok {
+ return -1, 0, fmt.Errorf("can't cast Getsockname result %T to SockaddrInet4", newSockAddr)
+ }
+ return fd, uint16(newSockAddrInet4.Port), nil
+ case unix.AF_INET6:
+ newSockAddrInet6, ok := newSockAddr.(*unix.SockaddrInet6)
+ if !ok {
+ return -1, 0, fmt.Errorf("can't cast Getsockname result %T to SockaddrInet6", newSockAddr)
+ }
+ return fd, uint16(newSockAddrInet6.Port), nil
+ default:
+ return -1, 0, fmt.Errorf("invalid domain %d, it should be one of unix.AF_INET or unix.AF_INET6", domain)
}
- return fd, uint16(newSockAddrInet4.Port), nil
}
// layerState stores the state of a layer of a connection.
type layerState interface {
- // outgoing returns an outgoing layer to be sent in a frame.
+ // outgoing returns an outgoing layer to be sent in a frame. It should not
+ // update layerState, that is done in layerState.sent.
outgoing() Layer
// incoming creates an expected Layer for comparing against a received Layer.
// Because the expectation can depend on values in the received Layer, it is
// an input to incoming. For example, the ACK number needs to be checked in a
- // TCP packet but only if the ACK flag is set in the received packet. The
- // calles takes ownership of the returned Layer.
+ // TCP packet but only if the ACK flag is set in the received packet. It
+ // should not update layerState, that is done in layerState.received. The
+ // caller takes ownership of the returned Layer.
incoming(received Layer) Layer
// sent updates the layerState based on the Layer that was sent. The input is
@@ -122,7 +150,7 @@ func newEtherState(out, in Ether) (*etherState, error) {
}
func (s *etherState) outgoing() Layer {
- return &s.out
+ return deepcopy.Copy(&s.out).(Layer)
}
// incoming implements layerState.incoming.
@@ -167,7 +195,7 @@ func newIPv4State(out, in IPv4) (*ipv4State, error) {
}
func (s *ipv4State) outgoing() Layer {
- return &s.out
+ return deepcopy.Copy(&s.out).(Layer)
}
// incoming implements layerState.incoming.
@@ -187,6 +215,54 @@ func (*ipv4State) close() error {
return nil
}
+// ipv6State maintains state about an IPv6 connection.
+type ipv6State struct {
+ out, in IPv6
+}
+
+var _ layerState = (*ipv6State)(nil)
+
+// newIPv6State creates a new ipv6State.
+func newIPv6State(out, in IPv6) (*ipv6State, error) {
+ lIP := tcpip.Address(net.ParseIP(*localIPv6).To16())
+ rIP := tcpip.Address(net.ParseIP(*remoteIPv6).To16())
+ s := ipv6State{
+ out: IPv6{SrcAddr: &lIP, DstAddr: &rIP},
+ in: IPv6{SrcAddr: &rIP, DstAddr: &lIP},
+ }
+ if err := s.out.merge(&out); err != nil {
+ return nil, err
+ }
+ if err := s.in.merge(&in); err != nil {
+ return nil, err
+ }
+ return &s, nil
+}
+
+// outgoing returns an outgoing layer to be sent in a frame.
+func (s *ipv6State) outgoing() Layer {
+ return deepcopy.Copy(&s.out).(Layer)
+}
+
+func (s *ipv6State) incoming(Layer) Layer {
+ return deepcopy.Copy(&s.in).(Layer)
+}
+
+func (s *ipv6State) sent(Layer) error {
+ // Nothing to do.
+ return nil
+}
+
+func (s *ipv6State) received(Layer) error {
+ // Nothing to do.
+ return nil
+}
+
+// close cleans up any resources held.
+func (s *ipv6State) close() error {
+ return nil
+}
+
// tcpState maintains state about a TCP connection.
type tcpState struct {
out, in TCP
@@ -205,8 +281,8 @@ func SeqNumValue(v seqnum.Value) *seqnum.Value {
}
// newTCPState creates a new TCPState.
-func newTCPState(out, in TCP) (*tcpState, error) {
- portPickerFD, localPort, err := pickPort()
+func newTCPState(domain int, out, in TCP) (*tcpState, error) {
+ portPickerFD, localPort, err := pickPort(domain, unix.SOCK_STREAM)
if err != nil {
return nil, err
}
@@ -309,8 +385,8 @@ type udpState struct {
var _ layerState = (*udpState)(nil)
// newUDPState creates a new udpState.
-func newUDPState(out, in UDP) (*udpState, error) {
- portPickerFD, localPort, err := pickPort()
+func newUDPState(domain int, out, in UDP) (*udpState, error) {
+ portPickerFD, localPort, err := pickPort(domain, unix.SOCK_DGRAM)
if err != nil {
return nil, err
}
@@ -329,7 +405,7 @@ func newUDPState(out, in UDP) (*udpState, error) {
}
func (s *udpState) outgoing() Layer {
- return &s.out
+ return deepcopy.Copy(&s.out).(Layer)
}
// incoming implements layerState.incoming.
@@ -363,44 +439,33 @@ type Connection struct {
t *testing.T
}
-// match tries to match each Layer in received against the incoming filter. If
-// received is longer than layerStates then that may still count as a match. The
-// reverse is never a match. override overrides the default matchers for each
-// Layer.
-func (conn *Connection) match(override, received Layers) bool {
- var layersToMatch int
- if len(override) < len(conn.layerStates) {
- layersToMatch = len(conn.layerStates)
- } else {
- layersToMatch = len(override)
- }
- if len(received) < layersToMatch {
- return false
- }
- for i := 0; i < layersToMatch; i++ {
- var toMatch Layer
- if i < len(conn.layerStates) {
- s := conn.layerStates[i]
- toMatch = s.incoming(received[i])
- if toMatch == nil {
- return false
- }
- if i < len(override) {
- if err := toMatch.merge(override[i]); err != nil {
- conn.t.Fatalf("failed to merge: %s", err)
- }
- }
- } else {
- toMatch = override[i]
- if toMatch == nil {
- conn.t.Fatalf("expect the overriding layers to be non-nil")
- }
- }
- if !toMatch.match(received[i]) {
- return false
+// Returns the default incoming frame against which to match. If received is
+// longer than layerStates then that may still count as a match. The reverse is
+// never a match and nil is returned.
+func (conn *Connection) incoming(received Layers) Layers {
+ if len(received) < len(conn.layerStates) {
+ return nil
+ }
+ in := Layers{}
+ for i, s := range conn.layerStates {
+ toMatch := s.incoming(received[i])
+ if toMatch == nil {
+ return nil
}
+ in = append(in, toMatch)
+ }
+ return in
+}
+
+func (conn *Connection) match(override, received Layers) bool {
+ toMatch := conn.incoming(received)
+ if toMatch == nil {
+ return false // Not enough layers in gotLayers for matching.
+ }
+ if err := toMatch.merge(override); err != nil {
+ return false // Failing to merge is not matching.
}
- return true
+ return toMatch.match(received)
}
// Close frees associated resources held by the Connection.
@@ -432,7 +497,7 @@ func (conn *Connection) CreateFrame(layer Layer, additionalLayers ...Layer) Laye
// SendFrame sends a frame on the wire and updates the state of all layers.
func (conn *Connection) SendFrame(frame Layers) {
- outBytes, err := frame.toBytes()
+ outBytes, err := frame.ToBytes()
if err != nil {
conn.t.Fatalf("can't build outgoing TCP packet: %s", err)
}
@@ -470,6 +535,16 @@ func (conn *Connection) recvFrame(timeout time.Duration) Layers {
return parse(parseEther, b)
}
+// layersError stores the Layers that we got and the Layers that we wanted to
+// match.
+type layersError struct {
+ got, want Layers
+}
+
+func (e *layersError) Error() string {
+ return e.got.diff(e.want)
+}
+
// Expect a frame with the final layerStates layer matching the provided Layer
// within the timeout specified. If it doesn't arrive in time, it returns nil.
func (conn *Connection) Expect(layer Layer, timeout time.Duration) (Layer, error) {
@@ -485,21 +560,25 @@ func (conn *Connection) Expect(layer Layer, timeout time.Duration) (Layer, error
return gotFrame[len(conn.layerStates)-1], nil
}
conn.t.Fatal("the received frame should be at least as long as the expected layers")
- return nil, fmt.Errorf("the received frame should be at least as long as the expected layers")
+ panic("unreachable")
}
// ExpectFrame expects a frame that matches the provided Layers within the
-// timeout specified. If it doesn't arrive in time, it returns nil.
+// timeout specified. If one arrives in time, the Layers is returned without an
+// error. If it doesn't arrive in time, it returns nil and error is non-nil.
func (conn *Connection) ExpectFrame(layers Layers, timeout time.Duration) (Layers, error) {
deadline := time.Now().Add(timeout)
- var allLayers []string
+ var errs error
for {
var gotLayers Layers
if timeout = time.Until(deadline); timeout > 0 {
gotLayers = conn.recvFrame(timeout)
}
if gotLayers == nil {
- return nil, fmt.Errorf("got %d packets:\n%s", len(allLayers), strings.Join(allLayers, "\n"))
+ if errs == nil {
+ return nil, fmt.Errorf("got no frames matching %v during %s", layers, timeout)
+ }
+ return nil, fmt.Errorf("got no frames matching %v during %s: got %w", layers, timeout, errs)
}
if conn.match(layers, gotLayers) {
for i, s := range conn.layerStates {
@@ -509,7 +588,7 @@ func (conn *Connection) ExpectFrame(layers Layers, timeout time.Duration) (Layer
}
return gotLayers, nil
}
- allLayers = append(allLayers, fmt.Sprintf("%s", gotLayers))
+ errs = multierr.Combine(errs, &layersError{got: gotLayers, want: conn.incoming(gotLayers)})
}
}
@@ -532,7 +611,7 @@ func NewTCPIPv4(t *testing.T, outgoingTCP, incomingTCP TCP) TCPIPv4 {
if err != nil {
t.Fatalf("can't make ipv4State: %s", err)
}
- tcpState, err := newTCPState(outgoingTCP, incomingTCP)
+ tcpState, err := newTCPState(unix.AF_INET, outgoingTCP, incomingTCP)
if err != nil {
t.Fatalf("can't make tcpState: %s", err)
}
@@ -629,6 +708,59 @@ func (conn *TCPIPv4) SynAck() *TCP {
return conn.state().synAck
}
+// IPv6Conn maintains the state for all the layers in a IPv6 connection.
+type IPv6Conn Connection
+
+// NewIPv6Conn creates a new IPv6Conn connection with reasonable defaults.
+func NewIPv6Conn(t *testing.T, outgoingIPv6, incomingIPv6 IPv6) IPv6Conn {
+ etherState, err := newEtherState(Ether{}, Ether{})
+ if err != nil {
+ t.Fatalf("can't make EtherState: %s", err)
+ }
+ ipv6State, err := newIPv6State(outgoingIPv6, incomingIPv6)
+ if err != nil {
+ t.Fatalf("can't make IPv6State: %s", err)
+ }
+
+ injector, err := NewInjector(t)
+ if err != nil {
+ t.Fatalf("can't make injector: %s", err)
+ }
+ sniffer, err := NewSniffer(t)
+ if err != nil {
+ t.Fatalf("can't make sniffer: %s", err)
+ }
+
+ return IPv6Conn{
+ layerStates: []layerState{etherState, ipv6State},
+ injector: injector,
+ sniffer: sniffer,
+ t: t,
+ }
+}
+
+// SendFrame sends a frame on the wire and updates the state of all layers.
+func (conn *IPv6Conn) SendFrame(frame Layers) {
+ (*Connection)(conn).SendFrame(frame)
+}
+
+// CreateFrame builds a frame for the connection with ipv6 overriding the ipv6
+// layer defaults and additionalLayers added after it.
+func (conn *IPv6Conn) CreateFrame(ipv6 IPv6, additionalLayers ...Layer) Layers {
+ return (*Connection)(conn).CreateFrame(&ipv6, additionalLayers...)
+}
+
+// Close to clean up any resources held.
+func (conn *IPv6Conn) Close() {
+ (*Connection)(conn).Close()
+}
+
+// ExpectFrame expects a frame that matches the provided Layers within the
+// timeout specified. If it doesn't arrive in time, it returns nil.
+func (conn *IPv6Conn) ExpectFrame(frame Layers, timeout time.Duration) (Layers, error) {
+ return (*Connection)(conn).ExpectFrame(frame, timeout)
+}
+
// Drain drains the sniffer's receive buffer by receiving packets until there's
// nothing else to receive.
func (conn *TCPIPv4) Drain() {
@@ -648,7 +780,7 @@ func NewUDPIPv4(t *testing.T, outgoingUDP, incomingUDP UDP) UDPIPv4 {
if err != nil {
t.Fatalf("can't make ipv4State: %s", err)
}
- tcpState, err := newUDPState(outgoingUDP, incomingUDP)
+ tcpState, err := newUDPState(unix.AF_INET, outgoingUDP, incomingUDP)
if err != nil {
t.Fatalf("can't make udpState: %s", err)
}
diff --git a/test/packetimpact/testbench/layers.go b/test/packetimpact/testbench/layers.go
index 01e99567d..817f5c261 100644
--- a/test/packetimpact/testbench/layers.go
+++ b/test/packetimpact/testbench/layers.go
@@ -22,6 +22,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
+ "go.uber.org/multierr"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
"gvisor.dev/gvisor/pkg/tcpip/header"
@@ -35,14 +36,14 @@ import (
type Layer interface {
fmt.Stringer
- // toBytes converts the Layer into bytes. In places where the Layer's field
+ // ToBytes converts the Layer into bytes. In places where the Layer's field
// isn't nil, the value that is pointed to is used. When the field is nil, a
// reasonable default for the Layer is used. For example, "64" for IPv4 TTL
// and a calculated checksum for TCP or IP. Some layers require information
// from the previous or next layers in order to compute a default, such as
// TCP's checksum or Ethernet's type, so each Layer has a doubly-linked list
// to the layer's neighbors.
- toBytes() ([]byte, error)
+ ToBytes() ([]byte, error)
// match checks if the current Layer matches the provided Layer. If either
// Layer has a nil in a given field, that field is considered matching.
@@ -173,7 +174,8 @@ func (l *Ether) String() string {
return stringLayer(l)
}
-func (l *Ether) toBytes() ([]byte, error) {
+// ToBytes implements Layer.ToBytes.
+func (l *Ether) ToBytes() ([]byte, error) {
b := make([]byte, header.EthernetMinimumSize)
h := header.Ethernet(b)
fields := &header.EthernetFields{}
@@ -189,8 +191,9 @@ func (l *Ether) toBytes() ([]byte, error) {
switch n := l.next().(type) {
case *IPv4:
fields.Type = header.IPv4ProtocolNumber
+ case *IPv6:
+ fields.Type = header.IPv6ProtocolNumber
default:
- // TODO(b/150301488): Support more protocols, like IPv6.
return nil, fmt.Errorf("ethernet header's next layer is unrecognized: %#v", n)
}
}
@@ -245,6 +248,8 @@ func parseEther(b []byte) (Layer, layerParser) {
switch h.Type() {
case header.IPv4ProtocolNumber:
nextParser = parseIPv4
+ case header.IPv6ProtocolNumber:
+ nextParser = parseIPv6
default:
// Assume that the rest is a payload.
nextParser = parsePayload
@@ -285,7 +290,8 @@ func (l *IPv4) String() string {
return stringLayer(l)
}
-func (l *IPv4) toBytes() ([]byte, error) {
+// ToBytes implements Layer.ToBytes.
+func (l *IPv4) ToBytes() ([]byte, error) {
b := make([]byte, header.IPv4MinimumSize)
h := header.IPv4(b)
fields := &header.IPv4Fields{
@@ -420,6 +426,186 @@ func (l *IPv4) merge(other Layer) error {
return mergeLayer(l, other)
}
+// IPv6 can construct and match an IPv6 encapsulation.
+type IPv6 struct {
+ LayerBase
+ TrafficClass *uint8
+ FlowLabel *uint32
+ PayloadLength *uint16
+ NextHeader *uint8
+ HopLimit *uint8
+ SrcAddr *tcpip.Address
+ DstAddr *tcpip.Address
+}
+
+func (l *IPv6) String() string {
+ return stringLayer(l)
+}
+
+// ToBytes implements Layer.ToBytes.
+func (l *IPv6) ToBytes() ([]byte, error) {
+ b := make([]byte, header.IPv6MinimumSize)
+ h := header.IPv6(b)
+ fields := &header.IPv6Fields{
+ HopLimit: 64,
+ }
+ if l.TrafficClass != nil {
+ fields.TrafficClass = *l.TrafficClass
+ }
+ if l.FlowLabel != nil {
+ fields.FlowLabel = *l.FlowLabel
+ }
+ if l.PayloadLength != nil {
+ fields.PayloadLength = *l.PayloadLength
+ } else {
+ for current := l.next(); current != nil; current = current.next() {
+ fields.PayloadLength += uint16(current.length())
+ }
+ }
+ if l.NextHeader != nil {
+ fields.NextHeader = *l.NextHeader
+ } else {
+ switch n := l.next().(type) {
+ case *TCP:
+ fields.NextHeader = uint8(header.TCPProtocolNumber)
+ case *UDP:
+ fields.NextHeader = uint8(header.UDPProtocolNumber)
+ case *ICMPv6:
+ fields.NextHeader = uint8(header.ICMPv6ProtocolNumber)
+ default:
+ // TODO(b/150301488): Support more protocols as needed.
+ return nil, fmt.Errorf("ToBytes can't deduce the IPv6 header's next protocol: %#v", n)
+ }
+ }
+ if l.HopLimit != nil {
+ fields.HopLimit = *l.HopLimit
+ }
+ if l.SrcAddr != nil {
+ fields.SrcAddr = *l.SrcAddr
+ }
+ if l.DstAddr != nil {
+ fields.DstAddr = *l.DstAddr
+ }
+ h.Encode(fields)
+ return h, nil
+}
+
+// parseIPv6 parses the bytes assuming that they start with an ipv6 header and
+// continues parsing further encapsulations.
+func parseIPv6(b []byte) (Layer, layerParser) {
+ h := header.IPv6(b)
+ tos, flowLabel := h.TOS()
+ ipv6 := IPv6{
+ TrafficClass: &tos,
+ FlowLabel: &flowLabel,
+ PayloadLength: Uint16(h.PayloadLength()),
+ NextHeader: Uint8(h.NextHeader()),
+ HopLimit: Uint8(h.HopLimit()),
+ SrcAddr: Address(h.SourceAddress()),
+ DstAddr: Address(h.DestinationAddress()),
+ }
+ var nextParser layerParser
+ switch h.TransportProtocol() {
+ case header.TCPProtocolNumber:
+ nextParser = parseTCP
+ case header.UDPProtocolNumber:
+ nextParser = parseUDP
+ case header.ICMPv6ProtocolNumber:
+ nextParser = parseICMPv6
+ default:
+ // Assume that the rest is a payload.
+ nextParser = parsePayload
+ }
+ return &ipv6, nextParser
+}
+
+func (l *IPv6) match(other Layer) bool {
+ return equalLayer(l, other)
+}
+
+func (l *IPv6) length() int {
+ return header.IPv6MinimumSize
+}
+
+// merge overrides the values in l with the values from other but only in fields
+// where the value is not nil.
+func (l *IPv6) merge(other Layer) error {
+ return mergeLayer(l, other)
+}
+
+// ICMPv6 can construct and match an ICMPv6 encapsulation.
+type ICMPv6 struct {
+ LayerBase
+ Type *header.ICMPv6Type
+ Code *byte
+ Checksum *uint16
+ NDPPayload []byte
+}
+
+func (l *ICMPv6) String() string {
+ // TODO(eyalsoha): Do something smarter here when *l.Type is ParameterProblem?
+ // We could parse the contents of the Payload as if it were an IPv6 packet.
+ return stringLayer(l)
+}
+
+// ToBytes implements Layer.ToBytes.
+func (l *ICMPv6) ToBytes() ([]byte, error) {
+ b := make([]byte, header.ICMPv6HeaderSize+len(l.NDPPayload))
+ h := header.ICMPv6(b)
+ if l.Type != nil {
+ h.SetType(*l.Type)
+ }
+ if l.Code != nil {
+ h.SetCode(*l.Code)
+ }
+ copy(h.NDPPayload(), l.NDPPayload)
+ if l.Checksum != nil {
+ h.SetChecksum(*l.Checksum)
+ } else {
+ ipv6 := l.prev().(*IPv6)
+ h.SetChecksum(header.ICMPv6Checksum(h, *ipv6.SrcAddr, *ipv6.DstAddr, buffer.VectorisedView{}))
+ }
+ return h, nil
+}
+
+// ICMPv6Type is a helper routine that allocates a new ICMPv6Type value to store
+// v and returns a pointer to it.
+func ICMPv6Type(v header.ICMPv6Type) *header.ICMPv6Type {
+ return &v
+}
+
+// Byte is a helper routine that allocates a new byte value to store
+// v and returns a pointer to it.
+func Byte(v byte) *byte {
+ return &v
+}
+
+// parseICMPv6 parses the bytes assuming that they start with an ICMPv6 header.
+func parseICMPv6(b []byte) (Layer, layerParser) {
+ h := header.ICMPv6(b)
+ icmpv6 := ICMPv6{
+ Type: ICMPv6Type(h.Type()),
+ Code: Byte(h.Code()),
+ Checksum: Uint16(h.Checksum()),
+ NDPPayload: h.NDPPayload(),
+ }
+ return &icmpv6, nil
+}
+
+func (l *ICMPv6) match(other Layer) bool {
+ return equalLayer(l, other)
+}
+
+func (l *ICMPv6) length() int {
+ return header.ICMPv6HeaderSize + len(l.NDPPayload)
+}
+
+// merge overrides the values in l with the values from other but only in fields
+// where the value is not nil.
+func (l *ICMPv6) merge(other Layer) error {
+ return mergeLayer(l, other)
+}
+
// TCP can construct and match a TCP encapsulation.
type TCP struct {
LayerBase
@@ -438,7 +624,8 @@ func (l *TCP) String() string {
return stringLayer(l)
}
-func (l *TCP) toBytes() ([]byte, error) {
+// ToBytes implements Layer.ToBytes.
+func (l *TCP) ToBytes() ([]byte, error) {
b := make([]byte, header.TCPMinimumSize)
h := header.TCP(b)
if l.SrcPort != nil {
@@ -503,7 +690,7 @@ func layerChecksum(l Layer, protoNumber tcpip.TransportProtocolNumber) (uint16,
}
var payloadBytes buffer.VectorisedView
for current := l.next(); current != nil; current = current.next() {
- payload, err := current.toBytes()
+ payload, err := current.ToBytes()
if err != nil {
return 0, fmt.Errorf("can't get bytes for next header: %s", payload)
}
@@ -577,7 +764,8 @@ func (l *UDP) String() string {
return stringLayer(l)
}
-func (l *UDP) toBytes() ([]byte, error) {
+// ToBytes implements Layer.ToBytes.
+func (l *UDP) ToBytes() ([]byte, error) {
b := make([]byte, header.UDPMinimumSize)
h := header.UDP(b)
if l.SrcPort != nil {
@@ -660,7 +848,8 @@ func parsePayload(b []byte) (Layer, layerParser) {
return &payload, nil
}
-func (l *Payload) toBytes() ([]byte, error) {
+// ToBytes implements Layer.ToBytes.
+func (l *Payload) ToBytes() ([]byte, error) {
return l.Bytes, nil
}
@@ -696,11 +885,13 @@ func (ls *Layers) linkLayers() {
}
}
-func (ls *Layers) toBytes() ([]byte, error) {
+// ToBytes converts the Layers into bytes. It creates a linked list of the Layer
+// structs and then concatentates the output of ToBytes on each Layer.
+func (ls *Layers) ToBytes() ([]byte, error) {
ls.linkLayers()
outBytes := []byte{}
for _, l := range *ls {
- layerBytes, err := l.toBytes()
+ layerBytes, err := l.ToBytes()
if err != nil {
return nil, err
}
@@ -720,3 +911,247 @@ func (ls *Layers) match(other Layers) bool {
}
return true
}
+
+// layerDiff stores the diffs for each field along with the label for the Layer.
+// If rows is nil, that means that there was no diff.
+type layerDiff struct {
+ label string
+ rows []layerDiffRow
+}
+
+// layerDiffRow stores the fields and corresponding values for two got and want
+// layers. If the value was nil then the string stored is the empty string.
+type layerDiffRow struct {
+ field, got, want string
+}
+
+// diffLayer extracts all differing fields between two layers.
+func diffLayer(got, want Layer) []layerDiffRow {
+ vGot := reflect.ValueOf(got).Elem()
+ vWant := reflect.ValueOf(want).Elem()
+ if vGot.Type() != vWant.Type() {
+ return nil
+ }
+ t := vGot.Type()
+ var result []layerDiffRow
+ for i := 0; i < t.NumField(); i++ {
+ t := t.Field(i)
+ if t.Anonymous {
+ // Ignore the LayerBase in the Layer struct.
+ continue
+ }
+ vGot := vGot.Field(i)
+ vWant := vWant.Field(i)
+ gotString := ""
+ if !vGot.IsNil() {
+ gotString = fmt.Sprint(reflect.Indirect(vGot))
+ }
+ wantString := ""
+ if !vWant.IsNil() {
+ wantString = fmt.Sprint(reflect.Indirect(vWant))
+ }
+ result = append(result, layerDiffRow{t.Name, gotString, wantString})
+ }
+ return result
+}
+
+// layerType returns a concise string describing the type of the Layer, like
+// "TCP", or "IPv6".
+func layerType(l Layer) string {
+ return reflect.TypeOf(l).Elem().Name()
+}
+
+// diff compares Layers and returns a representation of the difference. Each
+// Layer in the Layers is pairwise compared. If an element in either is nil, it
+// is considered a match with the other Layer. If two Layers have differing
+// types, they don't match regardless of the contents. If two Layers have the
+// same type then the fields in the Layer are pairwise compared. Fields that are
+// nil always match. Two non-nil fields only match if they point to equal
+// values. diff returns an empty string if and only if *ls and other match.
+func (ls *Layers) diff(other Layers) string {
+ var allDiffs []layerDiff
+ // Check the cases where one list is longer than the other, where one or both
+ // elements are nil, where the sides have different types, and where the sides
+ // have the same type.
+ for i := 0; i < len(*ls) || i < len(other); i++ {
+ if i >= len(*ls) {
+ // Matching ls against other where other is longer than ls. missing
+ // matches everything so we just include a label without any rows. Having
+ // no rows is a sign that there was no diff.
+ allDiffs = append(allDiffs, layerDiff{
+ label: "missing matches " + layerType(other[i]),
+ })
+ continue
+ }
+
+ if i >= len(other) {
+ // Matching ls against other where ls is longer than other. missing
+ // matches everything so we just include a label without any rows. Having
+ // no rows is a sign that there was no diff.
+ allDiffs = append(allDiffs, layerDiff{
+ label: layerType((*ls)[i]) + " matches missing",
+ })
+ continue
+ }
+
+ if (*ls)[i] == nil && other[i] == nil {
+ // Matching ls against other where both elements are nil. nil matches
+ // everything so we just include a label without any rows. Having no rows
+ // is a sign that there was no diff.
+ allDiffs = append(allDiffs, layerDiff{
+ label: "nil matches nil",
+ })
+ continue
+ }
+
+ if (*ls)[i] == nil {
+ // Matching ls against other where the element in ls is nil. nil matches
+ // everything so we just include a label without any rows. Having no rows
+ // is a sign that there was no diff.
+ allDiffs = append(allDiffs, layerDiff{
+ label: "nil matches " + layerType(other[i]),
+ })
+ continue
+ }
+
+ if other[i] == nil {
+ // Matching ls against other where the element in other is nil. nil
+ // matches everything so we just include a label without any rows. Having
+ // no rows is a sign that there was no diff.
+ allDiffs = append(allDiffs, layerDiff{
+ label: layerType((*ls)[i]) + " matches nil",
+ })
+ continue
+ }
+
+ if reflect.TypeOf((*ls)[i]) == reflect.TypeOf(other[i]) {
+ // Matching ls against other where both elements have the same type. Match
+ // each field pairwise and only report a diff if there is a mismatch,
+ // which is only when both sides are non-nil and have differring values.
+ diff := diffLayer((*ls)[i], other[i])
+ var layerDiffRows []layerDiffRow
+ for _, d := range diff {
+ if d.got == "" || d.want == "" || d.got == d.want {
+ continue
+ }
+ layerDiffRows = append(layerDiffRows, layerDiffRow{
+ d.field,
+ d.got,
+ d.want,
+ })
+ }
+ if len(layerDiffRows) > 0 {
+ allDiffs = append(allDiffs, layerDiff{
+ label: layerType((*ls)[i]),
+ rows: layerDiffRows,
+ })
+ } else {
+ allDiffs = append(allDiffs, layerDiff{
+ label: layerType((*ls)[i]) + " matches " + layerType(other[i]),
+ // Having no rows is a sign that there was no diff.
+ })
+ }
+ continue
+ }
+ // Neither side is nil and the types are different, so we'll display one
+ // side then the other.
+ allDiffs = append(allDiffs, layerDiff{
+ label: layerType((*ls)[i]) + " doesn't match " + layerType(other[i]),
+ })
+ diff := diffLayer((*ls)[i], (*ls)[i])
+ layerDiffRows := []layerDiffRow{}
+ for _, d := range diff {
+ if len(d.got) == 0 {
+ continue
+ }
+ layerDiffRows = append(layerDiffRows, layerDiffRow{
+ d.field,
+ d.got,
+ "",
+ })
+ }
+ allDiffs = append(allDiffs, layerDiff{
+ label: layerType((*ls)[i]),
+ rows: layerDiffRows,
+ })
+
+ layerDiffRows = []layerDiffRow{}
+ diff = diffLayer(other[i], other[i])
+ for _, d := range diff {
+ if len(d.want) == 0 {
+ continue
+ }
+ layerDiffRows = append(layerDiffRows, layerDiffRow{
+ d.field,
+ "",
+ d.want,
+ })
+ }
+ allDiffs = append(allDiffs, layerDiff{
+ label: layerType(other[i]),
+ rows: layerDiffRows,
+ })
+ }
+
+ output := ""
+ // These are for output formatting.
+ maxLabelLen, maxFieldLen, maxGotLen, maxWantLen := 0, 0, 0, 0
+ foundOne := false
+ for _, l := range allDiffs {
+ if len(l.label) > maxLabelLen && len(l.rows) > 0 {
+ maxLabelLen = len(l.label)
+ }
+ if l.rows != nil {
+ foundOne = true
+ }
+ for _, r := range l.rows {
+ if len(r.field) > maxFieldLen {
+ maxFieldLen = len(r.field)
+ }
+ if l := len(fmt.Sprint(r.got)); l > maxGotLen {
+ maxGotLen = l
+ }
+ if l := len(fmt.Sprint(r.want)); l > maxWantLen {
+ maxWantLen = l
+ }
+ }
+ }
+ if !foundOne {
+ return ""
+ }
+ for _, l := range allDiffs {
+ if len(l.rows) == 0 {
+ output += "(" + l.label + ")\n"
+ continue
+ }
+ for i, r := range l.rows {
+ var label string
+ if i == 0 {
+ label = l.label + ":"
+ }
+ output += fmt.Sprintf(
+ "%*s %*s %*v %*v\n",
+ maxLabelLen+1, label,
+ maxFieldLen+1, r.field+":",
+ maxGotLen, r.got,
+ maxWantLen, r.want,
+ )
+ }
+ }
+ return output
+}
+
+// merge merges the other Layers into ls. If the other Layers is longer, those
+// additional Layer structs are added to ls. The errors from merging are
+// collected and returned.
+func (ls *Layers) merge(other Layers) error {
+ var errs error
+ for i, o := range other {
+ if i < len(*ls) {
+ errs = multierr.Combine(errs, (*ls)[i].merge(o))
+ } else {
+ *ls = append(*ls, o)
+ }
+ }
+ return errs
+}
diff --git a/test/packetimpact/testbench/layers_test.go b/test/packetimpact/testbench/layers_test.go
index f07ec5eb2..96f72de5b 100644
--- a/test/packetimpact/testbench/layers_test.go
+++ b/test/packetimpact/testbench/layers_test.go
@@ -313,3 +313,83 @@ func TestConnectionMatch(t *testing.T) {
})
}
}
+
+func TestLayersDiff(t *testing.T) {
+ for _, tt := range []struct {
+ x, y Layers
+ want string
+ }{
+ {
+ Layers{&Ether{Type: NetworkProtocolNumber(12)}, &TCP{DataOffset: Uint8(5), SeqNum: Uint32(5)}},
+ Layers{&Ether{Type: NetworkProtocolNumber(13)}, &TCP{DataOffset: Uint8(7), SeqNum: Uint32(6)}},
+ "Ether: Type: 12 13\n" +
+ " TCP: SeqNum: 5 6\n" +
+ " DataOffset: 5 7\n",
+ },
+ {
+ Layers{&Ether{Type: NetworkProtocolNumber(12)}, &UDP{SrcPort: Uint16(123)}},
+ Layers{&Ether{Type: NetworkProtocolNumber(13)}, &TCP{DataOffset: Uint8(7), SeqNum: Uint32(6)}},
+ "Ether: Type: 12 13\n" +
+ "(UDP doesn't match TCP)\n" +
+ " UDP: SrcPort: 123 \n" +
+ " TCP: SeqNum: 6\n" +
+ " DataOffset: 7\n",
+ },
+ {
+ Layers{&UDP{SrcPort: Uint16(123)}},
+ Layers{&Ether{Type: NetworkProtocolNumber(13)}, &TCP{DataOffset: Uint8(7), SeqNum: Uint32(6)}},
+ "(UDP doesn't match Ether)\n" +
+ " UDP: SrcPort: 123 \n" +
+ "Ether: Type: 13\n" +
+ "(missing matches TCP)\n",
+ },
+ {
+ Layers{nil, &UDP{SrcPort: Uint16(123)}},
+ Layers{&Ether{Type: NetworkProtocolNumber(13)}, &TCP{DataOffset: Uint8(7), SeqNum: Uint32(6)}},
+ "(nil matches Ether)\n" +
+ "(UDP doesn't match TCP)\n" +
+ "UDP: SrcPort: 123 \n" +
+ "TCP: SeqNum: 6\n" +
+ " DataOffset: 7\n",
+ },
+ {
+ Layers{&Ether{Type: NetworkProtocolNumber(13)}, &IPv4{IHL: Uint8(4)}, &TCP{DataOffset: Uint8(7), SeqNum: Uint32(6)}},
+ Layers{&Ether{Type: NetworkProtocolNumber(13)}, &IPv4{IHL: Uint8(6)}, &TCP{DataOffset: Uint8(7), SeqNum: Uint32(6)}},
+ "(Ether matches Ether)\n" +
+ "IPv4: IHL: 4 6\n" +
+ "(TCP matches TCP)\n",
+ },
+ {
+ Layers{&Payload{Bytes: []byte("foo")}},
+ Layers{&Payload{Bytes: []byte("bar")}},
+ "Payload: Bytes: [102 111 111] [98 97 114]\n",
+ },
+ {
+ Layers{&Payload{Bytes: []byte("")}},
+ Layers{&Payload{}},
+ "",
+ },
+ {
+ Layers{&Payload{Bytes: []byte("")}},
+ Layers{&Payload{Bytes: []byte("")}},
+ "",
+ },
+ {
+ Layers{&UDP{}},
+ Layers{&TCP{}},
+ "(UDP doesn't match TCP)\n" +
+ "(UDP)\n" +
+ "(TCP)\n",
+ },
+ } {
+ if got := tt.x.diff(tt.y); got != tt.want {
+ t.Errorf("%s.diff(%s) = %q, want %q", tt.x, tt.y, got, tt.want)
+ }
+ if tt.x.match(tt.y) != (tt.x.diff(tt.y) == "") {
+ t.Errorf("match and diff of %s and %s disagree", tt.x, tt.y)
+ }
+ if tt.y.match(tt.x) != (tt.y.diff(tt.x) == "") {
+ t.Errorf("match and diff of %s and %s disagree", tt.y, tt.x)
+ }
+ }
+}
diff --git a/test/packetimpact/tests/BUILD b/test/packetimpact/tests/BUILD
index 47c722ccd..42f87e3f3 100644
--- a/test/packetimpact/tests/BUILD
+++ b/test/packetimpact/tests/BUILD
@@ -96,6 +96,19 @@ packetimpact_go_test(
],
)
+packetimpact_go_test(
+ name = "icmpv6_param_problem",
+ srcs = ["icmpv6_param_problem_test.go"],
+ # TODO(b/153485026): Fix netstack then remove the line below.
+ netstack = False,
+ deps = [
+ "//pkg/tcpip",
+ "//pkg/tcpip/header",
+ "//test/packetimpact/testbench",
+ "@org_golang_x_sys//unix:go_default_library",
+ ],
+)
+
sh_binary(
name = "test_runner",
srcs = ["test_runner.sh"],
diff --git a/test/packetimpact/tests/Dockerfile b/test/packetimpact/tests/Dockerfile
deleted file mode 100644
index 9075bc555..000000000
--- a/test/packetimpact/tests/Dockerfile
+++ /dev/null
@@ -1,17 +0,0 @@
-FROM ubuntu:bionic
-
-RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
- # iptables to disable OS native packet processing.
- iptables \
- # nc to check that the posix_server is running.
- netcat \
- # tcpdump to log brief packet sniffing.
- tcpdump \
- # ip link show to display MAC addresses.
- iproute2 \
- # tshark to log verbose packet sniffing.
- tshark \
- # killall for cleanup.
- psmisc
-RUN hash -r
-CMD /bin/bash
diff --git a/test/packetimpact/tests/fin_wait2_timeout_test.go b/test/packetimpact/tests/fin_wait2_timeout_test.go
index b98594f94..99dc77f9a 100644
--- a/test/packetimpact/tests/fin_wait2_timeout_test.go
+++ b/test/packetimpact/tests/fin_wait2_timeout_test.go
@@ -61,8 +61,8 @@ func TestFinWait2Timeout(t *testing.T) {
t.Fatalf("expected a RST packet within a second but got none: %s", err)
}
} else {
- if _, err := conn.Expect(tb.TCP{Flags: tb.Uint8(header.TCPFlagRst)}, 10*time.Second); err == nil {
- t.Fatalf("expected no RST packets within ten seconds but got one: %s", err)
+ if got, err := conn.Expect(tb.TCP{Flags: tb.Uint8(header.TCPFlagRst)}, 10*time.Second); got != nil || err == nil {
+ t.Fatalf("expected no RST packets within ten seconds but got one: %s", got)
}
}
})
diff --git a/test/packetimpact/tests/icmpv6_param_problem_test.go b/test/packetimpact/tests/icmpv6_param_problem_test.go
new file mode 100644
index 000000000..b48e55df4
--- /dev/null
+++ b/test/packetimpact/tests/icmpv6_param_problem_test.go
@@ -0,0 +1,73 @@
+// Copyright 2020 The gVisor Authors.
+//
+// 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 icmpv6_param_problem_test
+
+import (
+ "encoding/binary"
+ "testing"
+ "time"
+
+ "gvisor.dev/gvisor/pkg/tcpip/header"
+ tb "gvisor.dev/gvisor/test/packetimpact/testbench"
+)
+
+// TestICMPv6ParamProblemTest sends a packet with a bad next header. The DUT
+// should respond with an ICMPv6 Parameter Problem message.
+func TestICMPv6ParamProblemTest(t *testing.T) {
+ dut := tb.NewDUT(t)
+ defer dut.TearDown()
+ conn := tb.NewIPv6Conn(t, tb.IPv6{}, tb.IPv6{})
+ defer conn.Close()
+ ipv6 := tb.IPv6{
+ // 254 is reserved and used for experimentation and testing. This should
+ // cause an error.
+ NextHeader: tb.Uint8(254),
+ }
+ icmpv6 := tb.ICMPv6{
+ Type: tb.ICMPv6Type(header.ICMPv6EchoRequest),
+ NDPPayload: []byte("hello world"),
+ }
+
+ toSend := conn.CreateFrame(ipv6, &icmpv6)
+ conn.SendFrame(toSend)
+
+ // Build the expected ICMPv6 payload, which includes an index to the
+ // problematic byte and also the problematic packet as described in
+ // https://tools.ietf.org/html/rfc4443#page-12 .
+ ipv6Sent := toSend[1:]
+ expectedPayload, err := ipv6Sent.ToBytes()
+ if err != nil {
+ t.Fatalf("can't convert %s to bytes: %s", ipv6Sent, err)
+ }
+
+ // The problematic field is the NextHeader.
+ b := make([]byte, 4)
+ binary.BigEndian.PutUint32(b, header.IPv6NextHeaderOffset)
+ expectedPayload = append(b, expectedPayload...)
+ expectedICMPv6 := tb.ICMPv6{
+ Type: tb.ICMPv6Type(header.ICMPv6ParamProblem),
+ NDPPayload: expectedPayload,
+ }
+
+ paramProblem := tb.Layers{
+ &tb.Ether{},
+ &tb.IPv6{},
+ &expectedICMPv6,
+ }
+ timeout := time.Second
+ if _, err := conn.ExpectFrame(paramProblem, timeout); err != nil {
+ t.Errorf("expected %s within %s but got none: %s", paramProblem, timeout, err)
+ }
+}
diff --git a/test/packetimpact/tests/test_runner.sh b/test/packetimpact/tests/test_runner.sh
index e938de782..706441cce 100755
--- a/test/packetimpact/tests/test_runner.sh
+++ b/test/packetimpact/tests/test_runner.sh
@@ -192,6 +192,8 @@ docker pull "${IMAGE_TAG}"
# Create the DUT container and connect to network.
DUT=$(docker create ${RUNTIME_ARG} --privileged --rm \
+ --cap-add NET_ADMIN \
+ --sysctl net.ipv6.conf.all.disable_ipv6=0 \
--stop-timeout ${TIMEOUT} -it ${IMAGE_TAG})
docker network connect "${CTRL_NET}" \
--ip "${CTRL_NET_PREFIX}${DUT_NET_SUFFIX}" "${DUT}" \
@@ -203,6 +205,8 @@ docker start "${DUT}"
# Create the test bench container and connect to network.
TESTBENCH=$(docker create --privileged --rm \
+ --cap-add NET_ADMIN \
+ --sysctl net.ipv6.conf.all.disable_ipv6=0 \
--stop-timeout ${TIMEOUT} -it ${IMAGE_TAG})
docker network connect "${CTRL_NET}" \
--ip "${CTRL_NET_PREFIX}${TESTBENCH_NET_SUFFIX}" "${TESTBENCH}" \
@@ -237,6 +241,32 @@ declare -r REMOTE_MAC=$(docker exec -t "${DUT}" ip link show \
"${TEST_DEVICE}" | tail -1 | cut -d' ' -f6)
declare -r LOCAL_MAC=$(docker exec -t "${TESTBENCH}" ip link show \
"${TEST_DEVICE}" | tail -1 | cut -d' ' -f6)
+declare REMOTE_IPV6=$(docker exec -t "${DUT}" ip addr show scope link \
+ "${TEST_DEVICE}" | grep inet6 | cut -d' ' -f6 | cut -d'/' -f1)
+declare -r LOCAL_IPV6=$(docker exec -t "${TESTBENCH}" ip addr show scope link \
+ "${TEST_DEVICE}" | grep inet6 | cut -d' ' -f6 | cut -d'/' -f1)
+
+# Netstack as DUT doesn't assign IPv6 addresses automatically so do it if
+# needed. Convert the MAC address to an IPv6 link local address as described in
+# RFC 4291 page 20: https://tools.ietf.org/html/rfc4291#page-20
+if [[ -z "${REMOTE_IPV6}" ]]; then
+ # Split the octets of the MAC into an array of strings.
+ IFS=":" read -a REMOTE_OCTETS <<< "${REMOTE_MAC}"
+ # Flip the global bit.
+ REMOTE_OCTETS[0]=$(printf '%x' "$((0x${REMOTE_OCTETS[0]} ^ 2))")
+ # Add the IPv6 address.
+ docker exec "${DUT}" \
+ ip addr add $(printf 'fe80::%02x%02x:%02xff:fe%02x:%02x%02x/64' \
+ "0x${REMOTE_OCTETS[0]}" "0x${REMOTE_OCTETS[1]}" "0x${REMOTE_OCTETS[2]}" \
+ "0x${REMOTE_OCTETS[3]}" "0x${REMOTE_OCTETS[4]}" "0x${REMOTE_OCTETS[5]}") \
+ scope link \
+ dev "${TEST_DEVICE}"
+ # Re-extract the IPv6 address.
+ # TODO(eyalsoha): Add "scope link" below when netstack supports correctly
+ # creating link-local IPv6 addresses.
+ REMOTE_IPV6=$(docker exec -t "${DUT}" ip addr show \
+ "${TEST_DEVICE}" | grep inet6 | cut -d' ' -f6 | cut -d'/' -f1)
+fi
declare -r DOCKER_TESTBENCH_BINARY="/$(basename ${TESTBENCH_BINARY})"
docker cp -L "${TESTBENCH_BINARY}" "${TESTBENCH}:${DOCKER_TESTBENCH_BINARY}"
@@ -245,7 +275,10 @@ if [[ -z "${TSHARK-}" ]]; then
# Run tcpdump in the test bench unbuffered, without dns resolution, just on
# the interface with the test packets.
docker exec -t "${TESTBENCH}" \
- tcpdump -S -vvv -U -n -i "${TEST_DEVICE}" net "${TEST_NET_PREFIX}/24" &
+ tcpdump -S -vvv -U -n -i "${TEST_DEVICE}" \
+ net "${TEST_NET_PREFIX}/24" or \
+ host "${REMOTE_IPV6}" or \
+ host "${LOCAL_IPV6}" &
else
# Run tshark in the test bench unbuffered, without dns resolution, just on the
# interface with the test packets.
@@ -253,7 +286,9 @@ else
tshark -V -l -n -i "${TEST_DEVICE}" \
-o tcp.check_checksum:TRUE \
-o udp.check_checksum:TRUE \
- host "${TEST_NET_PREFIX}${TESTBENCH_NET_SUFFIX}" &
+ net "${TEST_NET_PREFIX}/24" or \
+ host "${REMOTE_IPV6}" or \
+ host "${LOCAL_IPV6}" &
fi
# tcpdump and tshark take time to startup
@@ -272,6 +307,8 @@ docker exec \
--posix_server_port=${CTRL_PORT} \
--remote_ipv4=${TEST_NET_PREFIX}${DUT_NET_SUFFIX} \
--local_ipv4=${TEST_NET_PREFIX}${TESTBENCH_NET_SUFFIX} \
+ --remote_ipv6=${REMOTE_IPV6} \
+ --local_ipv6=${LOCAL_IPV6} \
--remote_mac=${REMOTE_MAC} \
--local_mac=${LOCAL_MAC} \
--device=${TEST_DEVICE}" && true