From 7a1a7e79ca82479ab608fc62658013e9acffb11a Mon Sep 17 00:00:00 2001 From: Kevin Krakauer Date: Mon, 9 Sep 2024 13:53:55 -0700 Subject: [PATCH] netstack: add the gvisor2pcap binary gvisor2pcap accepts via `--in` a gVisor log file containing packet logs. It writes to `--out` a `.pcap` file based on those logs. This makes debugging some issues much simpler than before, as rather than trawling through inscrutable text representing 20 different connections, we can import the file into tools like Wireshark that perform packet analysis, graphing, etc. PiperOrigin-RevId: 672662760 --- tools/gvisor2pcap/BUILD | 40 ++ tools/gvisor2pcap/gvisor2pcap_test.go | 189 +++++++++ tools/gvisor2pcap/main.go | 541 ++++++++++++++++++++++++++ tools/gvisor2pcap/wget.log | 47 +++ 4 files changed, 817 insertions(+) create mode 100644 tools/gvisor2pcap/BUILD create mode 100644 tools/gvisor2pcap/gvisor2pcap_test.go create mode 100644 tools/gvisor2pcap/main.go create mode 100644 tools/gvisor2pcap/wget.log diff --git a/tools/gvisor2pcap/BUILD b/tools/gvisor2pcap/BUILD new file mode 100644 index 000000000..1c4d27062 --- /dev/null +++ b/tools/gvisor2pcap/BUILD @@ -0,0 +1,40 @@ +load("//tools:defs.bzl", "go_binary", "go_test") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +go_binary( + name = "gvisor2pcap", + srcs = [ + "main.go", + ], + static = True, + deps = [ + "//pkg/buffer", + "//pkg/log", + "//pkg/tcpip", + "//pkg/tcpip/header", + "//pkg/tcpip/link/sniffer", + "//pkg/tcpip/stack", + ], +) + +go_test( + name = "gvisor2pcap_test", + srcs = ["gvisor2pcap_test.go"], + data = [ + "wget.log", + ":gvisor2pcap", + ], + deps = [ + "//pkg/buffer", + "//pkg/log", + "//pkg/tcpip", + "//pkg/tcpip/header", + "//pkg/tcpip/link/sniffer", + "//pkg/tcpip/stack", + "//pkg/test/testutil", + ], +) diff --git a/tools/gvisor2pcap/gvisor2pcap_test.go b/tools/gvisor2pcap/gvisor2pcap_test.go new file mode 100644 index 000000000..c46082f57 --- /dev/null +++ b/tools/gvisor2pcap/gvisor2pcap_test.go @@ -0,0 +1,189 @@ +// Copyright 2024 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 gvisor2pcap_test + +import ( + "os" + "os/exec" + "path" + "regexp" + "strings" + "testing" + + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/link/sniffer" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/test/testutil" +) + +// TestGvisor2PCAP ensures that gVisor packet logs remain compatible with +// gvisor2pcap. +func TestGvisor2PCAP(t *testing.T) { + tempDir := t.TempDir() + tempIn := path.Join(tempDir, "in.log") + in, err := os.Create(tempIn) + if err != nil { + t.Fatalf("couldn't create input file: %v", err) + } + + // Capture sniffer output. + log.SetTarget(log.GoogleEmitter{Writer: &log.Writer{Next: in}}) + + // Run packets through the sniffer generate output. + snfr := sniffer.New(nil) + pkts := buildPackets() + defer pkts.Reset() + for _, pkt := range pkts.AsSlice() { + snfr.DumpPacket(sniffer.DirectionRecv, pkt.NetworkProtocolNumber, pkt, nil) + } + for _, pkt := range pkts.AsSlice() { + snfr.DumpPacket(sniffer.DirectionSend, pkt.NetworkProtocolNumber, pkt, nil) + } + + testBinary(t, tempIn, tempDir) +} + +func buildPackets() stack.PacketBufferList { + // The various sizes and fields of generated packets are arbitrary. + var pbl stack.PacketBufferList + for _, ipGen := range []func([]byte, tcpip.TransportProtocolNumber) *stack.PacketBuffer{genIPv4, genIPv6} { + for _, transportGen := range []func() ([]byte, tcpip.TransportProtocolNumber){genTCP, genUDP} { + transHdr, proto := transportGen() + pbl.PushBack(ipGen(transHdr, proto)) + } + } + return pbl +} + +func genTCP() ([]byte, tcpip.TransportProtocolNumber) { + tcp := header.TCP(make([]byte, header.TCPMinimumSize)) + tcp.Encode(&header.TCPFields{ + SeqNum: 800, + AckNum: 902, + DataOffset: header.TCPMinimumSize, + Flags: header.TCPFlagSyn, + WindowSize: 4096, + }) + return []byte(tcp), header.TCPProtocolNumber +} + +func genUDP() ([]byte, tcpip.TransportProtocolNumber) { + udp := header.UDP(make([]byte, header.UDPMinimumSize)) + udp.Encode(&header.UDPFields{ + SrcPort: 343, + DstPort: 2401, + Length: uint16(len(udp)), + Checksum: 0, + }) + return []byte(udp), header.UDPProtocolNumber +} + +func genIPv4(payload []byte, proto tcpip.TransportProtocolNumber) *stack.PacketBuffer { + pktLen := uint16(header.IPv4MinimumSize + len(payload)) + pktData := make([]byte, header.IPv4MinimumSize) + ip := header.IPv4(pktData) + ip.Encode(&header.IPv4Fields{ + TOS: 0, + TotalLength: pktLen, + ID: 1, + Flags: 0, + FragmentOffset: 0, + TTL: 48, + Protocol: uint8(proto), + SrcAddr: tcpip.AddrFromSlice([]byte("\x01\x02\x03\x04")), + DstAddr: tcpip.AddrFromSlice([]byte("\x05\x06\x07\x08")), + }) + ip.SetChecksum(42) + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(append(pktData, payload...)), + }) + pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber + return pkt +} + +func genIPv6(payload []byte, proto tcpip.TransportProtocolNumber) *stack.PacketBuffer { + pktData := make([]byte, header.IPv6MinimumSize) + ip := header.IPv6(pktData) + ip.Encode(&header.IPv6Fields{ + TrafficClass: 1, + FlowLabel: 2, + PayloadLength: uint16(len(payload)), + TransportProtocol: proto, + HopLimit: 78, + SrcAddr: tcpip.AddrFromSlice([]byte("\x01\x02\x03\x04\x01\x02\x03\x04\x01\x02\x03\x04\x01\x02\x03\x04")), + DstAddr: tcpip.AddrFromSlice([]byte("\x05\x06\x07\x08\x05\x06\x07\x08\x05\x06\x07\x08\x05\x06\x07\x08")), + ExtensionHeaders: header.IPv6ExtHdrSerializer{}, + }) + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(append(pktData, payload...)), + }) + pkt.NetworkProtocolNumber = header.IPv6ProtocolNumber + return pkt +} + +// TestGolden runs against the packet logs from `wget`. This is a larger, +// more realistic log than we could reasonably generate ourselves. +// +// One way to generate a new golden is via docker configured to run `runsc` +// with `--log-packets`. Build an image with wget and run +// `docker run --runtime runsc wget-image wget -4 google.com` +func TestGolden(t *testing.T) { + tempDir := t.TempDir() + packetLogFile, err := testutil.FindFile("tools/gvisor2pcap/wget.log") + if err != nil { + t.Fatalf("couldn't get log path: %v", err) + } + + testBinary(t, packetLogFile, tempDir) +} + +// allowedErrors matches output for packets that are known to be unhandled. +var allowedErrors = regexp.MustCompile(strings.Join([]string{ + "unhandled protocol (udp|arp|icmp)", + "IPv6 packet", +}, "|")) + +// testBinary runs gvisor2pcap with an input and checks that packets are either +// handled or in the set of known unhandled packet types. +func testBinary(t *testing.T, inputPath, outDir string) { + // Run gvisor2pcap. + gvisor2pcap, err := testutil.FindFile("tools/gvisor2pcap/gvisor2pcap") + if err != nil { + t.Fatalf("couldn't get binary: %v", err) + } + + tempOut := path.Join(outDir, "out.pcap") + cmd := exec.Command(gvisor2pcap, "--out", tempOut, "--in", inputPath) + binaryOutput, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("couldn't start command %v: %v: %v", cmd, err, string(binaryOutput)) + } + + // Ensure we generated an output file. + if output, err := os.ReadFile(tempOut); err != nil { + t.Errorf("couldn't read output file %q: %v", tempOut, err) + } else if len(output) == 0 { + t.Errorf("generated no output in %q", tempOut) + } + + for _, line := range strings.Split(string(binaryOutput), "\n") { + if len(line) > 0 && !allowedErrors.MatchString(line) { + t.Errorf("unexpected error output: %q", line) + } + } +} diff --git a/tools/gvisor2pcap/main.go b/tools/gvisor2pcap/main.go new file mode 100644 index 000000000..447df9185 --- /dev/null +++ b/tools/gvisor2pcap/main.go @@ -0,0 +1,541 @@ +// Copyright 2024 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. + +// Binary gvisor2pcap converts gVisor packet sniffer output to .pcap files that +// can be fed to Wireshark or tcpdump. +// +// gvisor2pcap currently supports only TCP on IPv4. It doesn't handle SACK +// blocks. +package main + +import ( + "bufio" + "flag" + "fmt" + "math/bits" + "net/netip" + "os" + "regexp" + "strconv" + "strings" + "time" + + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/link/sniffer" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +// packetTruncateSize is the maximum per-packet size to store in pcap output. +// We reconstruct packet headers during conversion, but have no access to their +// payloads. So there's never a reason to store more than their headers. +const packetTruncateSize = header.IPv4MaximumHeaderSize + header.TCPHeaderMaximumSize + +// Flags. +var ( + inFileName = flag.String("in", "/dev/stdin", "log file containing sniffer output to be parsed (default: stdin)") + outFileName = flag.String("out", "/dev/stdout", "file to write to (default: stdout)") +) + +// Regular expression for matching sniffer output. +var snifferOutput = regexp.MustCompile(`^.*sniffer\.go.*(send|recv).*$`) + +func main() { + if err := run(); err != nil { + log.Warningf("%v", err) + os.Exit(1) + } +} + +// run is basically a main function that can return errors. +func run() error { + flag.Parse() + + // Open the input log file. + input, err := os.Open(*inFileName) + if err != nil { + return fmt.Errorf("failed to open input file %s: %w", *inFileName, err) + } + defer input.Close() + + // Open the output pcap file. + output, err := os.Create(*outFileName) + if err != nil { + return fmt.Errorf("failed to open output file %s: %w", *outFileName, err) + } + defer output.Close() + + // Write the pcap header. + ep, err := sniffer.NewWithWriter(nil, output, packetTruncateSize) + if err != nil { + return fmt.Errorf("failed to create sniffer: %w", err) + } + // Supress text output. + sniffer.LogPackets.Store(0) + + scanner := bufio.NewScanner(input) + for scanner.Scan() { + if err := processLine(ep, scanner); err != nil { + log.Infof("skipping line: %v: %q", err, scanner.Text()) + } + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("encountered scanner error: %w", err) + } + + return nil +} + +func processLine(ep *sniffer.Endpoint, scanner *bufio.Scanner) error { + // Filter for sniffer output. + line := scanner.Text() + if !snifferOutput.MatchString(line) { + return nil + } + + // Timestamp has the form "I0829 21:58:10.197938". + tokens := strings.Fields(line) + if len(tokens) < 2 { + return fmt.Errorf("not enough tokens for timestamp") + } + tsStr := fmt.Sprintf("%s %s", tokens[0], tokens[1]) + timestamp, err := time.Parse("I0102 15:04:05.000000", tsStr) + if err != nil { + return fmt.Errorf("couldn't parse timestamp: %w", err) + } + + // Discard text leading up to "send" or "recv". + for i, token := range tokens { + if token == "send" || token == "recv" { + tokens = tokens[i:] + break + } + } + + // Get the direction. + var direction sniffer.Direction + switch tokens[0] { + case "send": + direction = sniffer.DirectionSend + case "recv": + direction = sniffer.DirectionRecv + default: + panic(fmt.Sprintf("unknown direction %q", tokens[0])) + } + tokens = tokens[1:] + + // Get the transport protocol. We only support TCP for now. + if len(tokens) == 0 { + return fmt.Errorf("not enough tokens for transport protocol") + } + var transProto tcpip.TransportProtocolNumber + switch tokens[0] { + case "tcp": + transProto = header.TCPProtocolNumber + default: + return fmt.Errorf("unhandled protocol %s", tokens[0]) + } + tokens = tokens[1:] + + // The next few tokens look like "1.2.3.4:500 -> 6.7.8.9:1000". + srcIP, srcPort, err := consumeIPPort(&tokens) + if err != nil { + return fmt.Errorf("bad source: %w", err) + } + if len(tokens) == 0 { + return fmt.Errorf("not enough tokens for '->'") + } + if tokens[0] != "->" { + return fmt.Errorf("'->' was expected but not found") + } + tokens = tokens[1:] + dstIP, dstPort, err := consumeIPPort(&tokens) + if err != nil { + return fmt.Errorf("bad source: %w", err) + } + + if srcIP.Is6() || dstIP.Is6() { + return fmt.Errorf("IPv6 packet") + } + + // Packet length is of the form "len:200". It describes the + // payload length not including headers. + length, err := consumeFieldUint[uint16](&tokens, "len") + if err != nil { + return fmt.Errorf("bad length: %w", err) + } + + // Packet ID is of the form "id:0xa20f". + id, err := consumeFieldUint[uint16](&tokens, "id") + if err != nil { + return fmt.Errorf("bad id: %w", err) + } + + // TCP flags are trickier. If every flag is set, we'll see + // "flags:FSRPAUEC". Each unset flag becomes whitespace, so + // flags are a variable number of tokens in length. We look for + // the tokens before and after the flags ("flags:" and + // "seqnum:") to find them. + var ok bool + if len(tokens) == 0 { + return fmt.Errorf("not enough tokens for 'flags:'") + } + tokens[0], ok = strings.CutPrefix(tokens[0], "flags:") + if !ok { + return fmt.Errorf("missing \"flags:\"") + } + if len(tokens[0]) == 0 { + tokens = tokens[1:] + } + + flagsEnd := -1 + for i, token := range tokens { + if strings.HasPrefix(token, "seqnum:") { + flagsEnd = i + break + } + } + if flagsEnd == -1 { + return fmt.Errorf("bad flags/seqnum formatting") + } + var flags header.TCPFlags + for _, token := range tokens[:flagsEnd] { + // Tokens may be a single flag e.g. "S", or multiple e.g. "PA". + for _, flag := range token { + switch flag { + case 'F': + flags |= header.TCPFlagFin + case 'S': + flags |= header.TCPFlagSyn + case 'R': + flags |= header.TCPFlagRst + case 'P': + flags |= header.TCPFlagPsh + case 'A': + flags |= header.TCPFlagAck + case 'U': + flags |= header.TCPFlagUrg + case 'E': + flags |= header.TCPFlagEce + case 'C': + flags |= header.TCPFlagCwr + default: + return fmt.Errorf("unknown TCP flag %v", flag) + } + } + } + tokens = tokens[flagsEnd:] + + // Sequence number. + seqnum, err := consumeFieldUint[uint32](&tokens, "seqnum") + if err != nil { + return fmt.Errorf("bad sequence number: %w", err) + } + + // Acknowledgement number. + ack, err := consumeFieldUint[uint32](&tokens, "ack") + if err != nil { + return fmt.Errorf("bad ack number: %w", err) + } + + // Window size. + window, err := consumeFieldUint[uint16](&tokens, "win") + if err != nil { + return fmt.Errorf("bad window size: %w", err) + } + + // Checksum. + csum, err := consumeFieldUint[uint16](&tokens, "xsum") + if err != nil { + return fmt.Errorf("bad id: %w", err) + } + + // Options can be one of two types. + if len(tokens) == 0 { + return fmt.Errorf("not enough tokens for 'options:'") + } + firstOpt, hasOptions := strings.CutPrefix(tokens[0], "options:{") + if !hasOptions { + return fmt.Errorf("bad \"options:\" field") + } + tokens[0] = firstOpt + var options any + switch strings.Split(firstOpt, ":")[0] { + case "MSS": + mss, err := consumeFieldUint[uint16](&tokens, "MSS") + if err != nil { + return fmt.Errorf("bad mss: %w", err) + } + windowScale, err := consumeFieldUint[uint16](&tokens, "WS") + if err != nil { + // -1 is special output for "no window scaling". + if tokens[0] != "WS:-1" { + return fmt.Errorf("bad window scale: %w", err) + } + tokens = tokens[1:] + } + timestamp, err := consumeFieldBool(&tokens, "TS") + if err != nil { + return fmt.Errorf("bad timestamp: %w", err) + } + tsVal, err := consumeFieldUint[uint32](&tokens, "TSVal") + if err != nil { + return fmt.Errorf("bad timestamp value: %w", err) + } + tsEcr, err := consumeFieldUint[uint32](&tokens, "TSEcr") + if err != nil { + return fmt.Errorf("bad timestamp echo reply: %w", err) + } + sackPermitted, err := consumeFieldBool(&tokens, "SACKPermitted") + if err != nil { + return fmt.Errorf("bad SACKPermitted: %w", err) + } + // "Flags:" can be ignored, as it's used internally only. + if !strings.HasPrefix(tokens[0], "Flags:") { + return fmt.Errorf("bad \"Flags:\"") + } + tokens = tokens[1:] + if len(tokens) == 0 { + return fmt.Errorf("not enough tokens for 'options:' closing brace") + } + if tokens[0] != "}" { + return fmt.Errorf("'}' was expected but not found") + } + tokens = tokens[1:] + options = header.TCPSynOptions{ + MSS: mss, + WS: int(windowScale), + TS: timestamp, + TSVal: tsVal, + TSEcr: tsEcr, + SACKPermitted: sackPermitted, + } + case "TS": + timestamp, err := consumeFieldBool(&tokens, "TS") + if err != nil { + return fmt.Errorf("bad timestamp: %w", err) + } + tsVal, err := consumeFieldUint[uint32](&tokens, "TSVal") + if err != nil { + return fmt.Errorf("bad timestamp value: %w", err) + } + tsEcr, err := consumeFieldUint[uint32](&tokens, "TSEcr") + if err != nil { + return fmt.Errorf("bad timestamp echo reply: %w", err) + } + + // For now, just skip SACK blocks by looking for a "]". + if !strings.HasPrefix(tokens[0], "SACKBlocks:[") { + return fmt.Errorf("bad missing SACKBlocks field") + } + for i, token := range tokens { + if strings.Contains(token, "]") { + tokens = tokens[i+1:] + } + } + + options = header.TCPOptions{ + TS: timestamp, + TSVal: tsVal, + TSEcr: tsEcr, + SACKBlocks: nil, + } + + default: + return fmt.Errorf("bad option: %q", firstOpt) + } + + // There may be a GSO field. + if len(tokens) != 0 { + if !strings.HasPrefix(tokens[0], "gso:") { + return fmt.Errorf("unknown option %q", tokens[0]) + } + // At time of writing there are 6 fields in this struct + // (GSOOptions). We don't need them, but we want to log if + // we've encountered something we don't understand. + if len(tokens) != 6 { + return fmt.Errorf("extra options") + } + } + + // TODO(krakauer): Tons of copied code from tcp package. + optsBuf := make([]byte, 40) + switch options := options.(type) { + case header.TCPSynOptions: + offset := header.EncodeMSSOption(uint32(options.MSS), optsBuf) + + // Special ordering is required here. If both TS and SACK are enabled, + // then the SACK option precedes TS, with no padding. If they are + // enabled individually, then we see padding before the option. + if options.TS && options.SACKPermitted { + offset += header.EncodeSACKPermittedOption(optsBuf[offset:]) + offset += header.EncodeTSOption(options.TSVal, options.TSEcr, optsBuf[offset:]) + } else if options.TS { + offset += header.EncodeNOP(optsBuf[offset:]) + offset += header.EncodeNOP(optsBuf[offset:]) + offset += header.EncodeTSOption(options.TSVal, options.TSEcr, optsBuf[offset:]) + } else if options.SACKPermitted { + offset += header.EncodeNOP(optsBuf[offset:]) + offset += header.EncodeNOP(optsBuf[offset:]) + offset += header.EncodeSACKPermittedOption(optsBuf[offset:]) + } + + // Initialize the WS option. + if options.WS >= 0 { + offset += header.EncodeNOP(optsBuf[offset:]) + offset += header.EncodeWSOption(options.WS, optsBuf[offset:]) + } + + optsBuf = optsBuf[:offset] + + // Padding to the end; note that this never apply unless we add a + // fastopen option, we always expect the offset to remain the same. + if delta := header.AddTCPOptionPadding(optsBuf, offset); delta != 0 { + panic("unexpected option encoding") + } + + case header.TCPOptions: + offset := 0 + if options.TS { + offset += header.EncodeNOP(optsBuf[offset:]) + offset += header.EncodeNOP(optsBuf[offset:]) + offset += header.EncodeTSOption(options.TSVal, options.TSEcr, optsBuf[offset:]) + } + optsBuf = optsBuf[:offset] + + // We expect the above to produce an aligned offset. + if delta := header.AddTCPOptionPadding(optsBuf, offset); delta != 0 { + panic("unexpected option encoding") + } + + default: + panic(fmt.Sprintf("bad options type %T", options)) + } + + // Build TCP header. + tcpHdr := header.TCP(make([]byte, header.TCPMinimumSize+len(optsBuf))) + tcpHdr.Encode(&header.TCPFields{ + SrcPort: srcPort, + DstPort: dstPort, + SeqNum: seqnum, + AckNum: ack, + DataOffset: uint8(header.TCPMinimumSize + len(optsBuf)), + Flags: flags, + WindowSize: window, + }) + copy(tcpHdr[header.TCPMinimumSize:], optsBuf) + + // Build IP header. + length += header.IPv4MinimumSize + uint16(len(tcpHdr)) + ipHdr := header.IPv4(make([]byte, header.IPv4MinimumSize)) + ipHdr.Encode(&header.IPv4Fields{ + TotalLength: length, + ID: id, + TTL: 10, // Made up. + TOS: 0, // Made up. + Protocol: uint8(transProto), + Checksum: csum, + SrcAddr: tcpip.AddrFrom4(srcIP.As4()), + DstAddr: tcpip.AddrFrom4(dstIP.As4()), + Options: nil, + }) + + // Build PacketBuffer. + data := make([]byte, length) + copy(data, ipHdr) + copy(data[len(ipHdr):], tcpHdr) + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(data), + }) + pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber + pkt.TransportProtocolNumber = header.TCPProtocolNumber + + // Inject packet into sniffer to generate pcap output. + ep.DumpPacket(direction, header.IPv4ProtocolNumber, pkt, ×tamp) + pkt.DecRef() + + return nil +} + +// consumeFieldUint returns the int value of a field of the form: "name:123" or +// "name:0xabc". +func consumeFieldUint[T uint16 | uint32](tokens *[]string, name string) (T, error) { + if len(*tokens) == 0 { + return 0, fmt.Errorf("no tokens available to find uint") + } + field := strings.Split((*tokens)[0], ":") + if len(field) != 2 || field[0] != name { + return 0, fmt.Errorf("bad field: %q", field) + } + base := 10 + if len(field[1]) >= 2 && strings.HasPrefix(field[1], "0x") { + field[1] = field[1][2:] + base = 16 + } + + // Get the bit length of T so we can pass it to ParseUint. + bitLen := bits.OnesCount64(uint64(T(0) - 1)) + + value, err := strconv.ParseUint(field[1], base, bitLen) + if err != nil { + return 0, fmt.Errorf("field has bad value %q: %w", field[1], err) + } + *tokens = (*tokens)[1:] + return T(value), nil +} + +// consumeFieldBool returns the bool value of a field of the form: "name:true" +// or "name:false". +func consumeFieldBool(tokens *[]string, name string) (bool, error) { + if len(*tokens) == 0 { + return false, fmt.Errorf("no tokens available to find bool") + } + field := strings.Split((*tokens)[0], ":") + if len(field) != 2 || field[0] != name { + return false, fmt.Errorf("bad field: %q", field) + } + defer func() { *tokens = (*tokens)[1:] }() + switch field[1] { + case "true": + return true, nil + case "false": + return false, nil + default: + return false, fmt.Errorf("unrecognized boolean value %q", (*tokens)[0]) + } +} + +func consumeIPPort(tokens *[]string) (netip.Addr, uint16, error) { + if len(*tokens) == 0 { + return netip.Addr{}, 0, fmt.Errorf("no tokens available to find address:port") + } + token := (*tokens)[0] + lastColon := strings.LastIndex(token, ":") + if lastColon < 0 { + return netip.Addr{}, 0, fmt.Errorf("bad address:port %q", token) + } + address, err := netip.ParseAddr(token[:lastColon]) + if err != nil { + return netip.Addr{}, 0, fmt.Errorf("bad source address %q", token[:lastColon]) + } + port, err := strconv.ParseUint(token[lastColon+1:], 10, 16) + if err != nil { + return netip.Addr{}, 0, fmt.Errorf("bad source port %q", token[lastColon+1:]) + } + *tokens = (*tokens)[1:] + return address, uint16(port), nil +} diff --git a/tools/gvisor2pcap/wget.log b/tools/gvisor2pcap/wget.log new file mode 100644 index 000000000..a770e27b8 --- /dev/null +++ b/tools/gvisor2pcap/wget.log @@ -0,0 +1,47 @@ +I0903 14:40:00.408531 1 sniffer.go:241] send arp 192.168.9.2 (02:42:c0:a8:09:02) -> 192.168.9.1 (00:00:00:00:00:00) valid:true +I0903 14:40:00.408684 1 sniffer.go:241] recv arp 192.168.9.1 (02:42:8c:56:45:30) -> 192.168.9.2 (02:42:c0:a8:09:02) valid:true +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28}) +I0903 14:40:00.408758 1 sniffer.go:388] send udp 192.168.9.2:51688 -> 8.8.8.8:53 len:39 id:0xca32 xsum: 0xa30 +I0903 14:40:00.408784 1 sniffer.go:388] send udp 192.168.9.2:51688 -> 8.8.8.8:53 len:39 id:0x6f5e xsum: 0x2998 +I0903 14:40:00.423904 1 sniffer.go:388] recv udp 8.8.8.8:53 -> 192.168.9.2:51688 len:55 id:0x45f6 xsum: 0xbac8 +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28}) +I0903 14:40:00.424864 1 sniffer.go:388] recv udp 8.8.8.8:53 -> 192.168.9.2:51688 len:67 id:0xe7b5 xsum: 0x10ce +I0903 14:40:00.426682 1 sniffer.go:388] send tcp 192.168.9.2:45751 -> 142.251.46.174:80 len:0 id:0xaa35 flags: S seqnum:4174770378 ack:0 win:29184 xsum:0xdca4 options:{MSS:1460 WS:7 TS:true TSVal:2699900017 TSEcr:0 SACKPermitted:true Flags: } +I0903 14:40:00.429014 1 sniffer.go:388] recv tcp 142.251.46.174:80 -> 192.168.9.2:45751 len:0 id:0x0000 flags: S A seqnum:2608070130 ack:4174770379 win:65535 xsum:0x6f5c options:{MSS:1398 WS:8 TS:true TSVal:123223734 TSEcr:2699900017 SACKPermitted:true Flags: } +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28}) +I0903 14:40:00.429095 1 sniffer.go:388] send tcp 192.168.9.2:45751 -> 142.251.46.174:80 len:0 id:0x0000 flags: A seqnum:4174770379 ack:2608070131 win:228 xsum:0x877a options:{TS:true TSVal:2699900019 TSEcr:123223734 SACKBlocks:[]} gso:stack.GSO{Type:1, NeedsCsum:true, CsumOffset:0x10, MSS:0x54e, L3HdrLen:0x14, MaxSize:0x10000} +I0903 14:40:00.429414 1 sniffer.go:388] send tcp 192.168.9.2:45751 -> 142.251.46.174:80 len:125 id:0x0000 flags: PA seqnum:4174770379 ack:2608070131 win:4096 xsum:0x87f7 options:{TS:true TSVal:2699900019 TSEcr:123223734 SACKBlocks:[]} gso:stack.GSO{Type:1, NeedsCsum:true, CsumOffset:0x10, MSS:0x54e, L3HdrLen:0x14, MaxSize:0x10000} +I0903 14:40:00.431188 1 sniffer.go:388] recv tcp 142.251.46.174:80 -> 192.168.9.2:45751 len:0 id:0x5fa7 flags: A seqnum:2608070131 ack:4174770504 win:256 xsum:0x9c6a options:{TS:true TSVal:123223736 TSEcr:2699900019 SACKBlocks:[]} +I0903 14:40:00.449410 1 sniffer.go:388] recv tcp 142.251.46.174:80 -> 192.168.9.2:45751 len:773 id:0x5fa8 flags: PA seqnum:2608070131 ack:4174770504 win:256 xsum:0x6b7e options:{TS:true TSVal:123223754 TSEcr:2699900019 SACKBlocks:[]} +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28}) +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28}) +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28}) +I0903 14:40:00.449478 1 sniffer.go:388] send tcp 192.168.9.2:45751 -> 142.251.46.174:80 len:0 id:0x0000 flags: A seqnum:4174770504 ack:2608070904 win:4089 xsum:0x877a options:{TS:true TSVal:2699900040 TSEcr:123223754 SACKBlocks:[]} gso:stack.GSO{Type:1, NeedsCsum:true, CsumOffset:0x10, MSS:0x54e, L3HdrLen:0x14, MaxSize:0x10000} +I0903 14:40:00.451395 1 sniffer.go:388] send udp 192.168.9.2:42277 -> 8.8.8.8:53 len:43 id:0xed2a xsum: 0x57a6 +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28}) +I0903 14:40:00.451409 1 sniffer.go:388] send udp 192.168.9.2:42277 -> 8.8.8.8:53 len:43 id:0x493e xsum: 0x8354 +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28}) +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28}) +I0903 14:40:00.468378 1 sniffer.go:388] recv udp 8.8.8.8:53 -> 192.168.9.2:42277 len:59 id:0x5c50 xsum: 0x7949 +I0903 14:40:00.470102 1 sniffer.go:388] recv udp 8.8.8.8:53 -> 192.168.9.2:42277 len:71 id:0x294b xsum: 0x6a8a +I0903 14:40:00.471609 1 sniffer.go:388] send tcp 192.168.9.2:31258 -> 142.250.189.164:80 len:0 id:0x41e5 flags: S seqnum:2976118385 ack:0 win:29184 xsum:0xcfb5 options:{MSS:1460 WS:7 TS:true TSVal:384415447 TSEcr:0 SACKPermitted:true Flags: } +I0903 14:40:00.472869 1 sniffer.go:388] recv tcp 142.250.189.164:80 -> 192.168.9.2:31258 len:0 id:0x0000 flags: S A seqnum:3464048916 ack:2976118386 win:65535 xsum:0x8c45 options:{MSS:1460 WS:8 TS:true TSVal:1559451869 TSEcr:384415447 SACKPermitted:true Flags: } +I0903 14:40:00.473020 1 sniffer.go:388] send tcp 192.168.9.2:31258 -> 142.250.189.164:80 len:0 id:0x0000 flags: A seqnum:2976118386 ack:3464048917 win:228 xsum:0x1670 options:{TS:true TSVal:384415448 TSEcr:1559451869 SACKBlocks:[]} gso:stack.GSO{Type:1, NeedsCsum:true, CsumOffset:0x10, MSS:0x58c, L3HdrLen:0x14, MaxSize:0x10000} +I0903 14:40:00.473336 1 sniffer.go:388] send tcp 192.168.9.2:31258 -> 142.250.189.164:80 len:129 id:0x0000 flags: PA seqnum:2976118386 ack:3464048917 win:4096 xsum:0x16f1 options:{TS:true TSVal:384415448 TSEcr:1559451869 SACKBlocks:[]} gso:stack.GSO{Type:1, NeedsCsum:true, CsumOffset:0x10, MSS:0x58c, L3HdrLen:0x14, MaxSize:0x10000} +I0903 14:40:00.474499 1 sniffer.go:388] recv tcp 142.250.189.164:80 -> 192.168.9.2:31258 len:0 id:0x07a6 flags: A seqnum:3464048917 ack:2976118515 win:261 xsum:0xb989 options:{TS:true TSVal:1559451871 TSEcr:384415448 SACKBlocks:[]} +I0903 14:40:00.517157 1 sniffer.go:388] recv tcp 142.250.189.164:80 -> 192.168.9.2:31258 len:7240 id:0x07a7 flags: PA seqnum:3464048917 ack:2976118515 win:261 xsum:0x32b8 options:{TS:true TSVal:1559451914 TSEcr:384415448 SACKBlocks:[]} +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28}) +I0903 14:40:00.517212 1 sniffer.go:388] recv tcp 142.250.189.164:80 -> 192.168.9.2:31258 len:7240 id:0x07ac flags: PA seqnum:3464056157 ack:2976118515 win:261 xsum:0x32b8 options:{TS:true TSVal:1559451914 TSEcr:384415448 SACKBlocks:[]} +I0903 14:40:00.517260 1 sniffer.go:388] send tcp 192.168.9.2:31258 -> 142.250.189.164:80 len:0 id:0x0000 flags: A seqnum:2976118515 ack:3464063397 win:3982 xsum:0x1670 options:{TS:true TSVal:384415492 TSEcr:1559451914 SACKBlocks:[]} gso:stack.GSO{Type:1, NeedsCsum:true, CsumOffset:0x10, MSS:0x58c, L3HdrLen:0x14, MaxSize:0x10000} +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28}) +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28}) +I0903 14:40:00.518643 1 sniffer.go:388] recv tcp 142.250.189.164:80 -> 192.168.9.2:31258 len:14480 id:0x07b1 flags: PA seqnum:3464063397 ack:2976118515 win:261 xsum:0x4f00 options:{TS:true TSVal:1559451915 TSEcr:384415492 SACKBlocks:[]} +I0903 14:40:00.518689 1 sniffer.go:388] recv tcp 142.250.189.164:80 -> 192.168.9.2:31258 len:3287 id:0x07bb flags: PA seqnum:3464077877 ack:2976118515 win:261 xsum:0x2347 options:{TS:true TSVal:1559451915 TSEcr:384415492 SACKBlocks:[]} +I0903 14:40:00.518751 1 sniffer.go:388] send tcp 192.168.9.2:31258 -> 142.250.189.164:80 len:0 id:0x0000 flags: A seqnum:2976118515 ack:3464081164 win:3932 xsum:0x1670 options:{TS:true TSVal:384415494 TSEcr:1559451915 SACKBlocks:[]} gso:stack.GSO{Type:1, NeedsCsum:true, CsumOffset:0x10, MSS:0x58c, L3HdrLen:0x14, MaxSize:0x10000} +I0903 14:40:00.520165 1 sniffer.go:388] send tcp 192.168.9.2:45751 -> 142.251.46.174:80 len:0 id:0x0000 flags:F A seqnum:4174770504 ack:2608070904 win:4096 xsum:0x877a options:{TS:true TSVal:2699900110 TSEcr:123223754 SACKBlocks:[]} gso:stack.GSO{Type:1, NeedsCsum:true, CsumOffset:0x10, MSS:0x54e, L3HdrLen:0x14, MaxSize:0x10000} +I0903 14:40:00.525019 1 sniffer.go:388] send tcp 192.168.9.2:31258 -> 142.250.189.164:80 len:0 id:0x0000 flags:F A seqnum:2976118515 ack:3464081164 win:4096 xsum:0x1670 options:{TS:true TSVal:384415500 TSEcr:1559451915 SACKBlocks:[]} gso:stack.GSO{Type:1, NeedsCsum:true, CsumOffset:0x10, MSS:0x58c, L3HdrLen:0x14, MaxSize:0x10000} +I0903 14:40:00.525363 1 sniffer.go:388] recv tcp 142.251.46.174:80 -> 192.168.9.2:45751 len:0 id:0x5fa9 flags:F A seqnum:2608070904 ack:4174770505 win:256 xsum:0x98aa options:{TS:true TSVal:123223830 TSEcr:2699900110 SACKBlocks:[]} +I0903 14:40:00.525482 1 sniffer.go:388] send tcp 192.168.9.2:45751 -> 142.251.46.174:80 len:0 id:0x0000 flags: A seqnum:4174770505 ack:2608070905 win:4095 xsum:0x877a options:{TS:true TSVal:2699900115 TSEcr:123223754 SACKBlocks:[]} gso:stack.GSO{Type:1, NeedsCsum:true, CsumOffset:0x10, MSS:0x54e, L3HdrLen:0x14, MaxSize:0x10000} +I0903 14:40:00.526064 1 sniffer.go:388] send tcp 192.168.9.2:45751 -> 142.251.46.174:80 len:0 id:0x0000 flags: R A seqnum:4174770505 ack:2608070905 win:0 xsum:0x877a options:{TS:true TSVal:2699900116 TSEcr:123223830 SACKBlocks:[]} gso:stack.GSO{Type:1, NeedsCsum:true, CsumOffset:0x10, MSS:0x54e, L3HdrLen:0x14, MaxSize:0x10000} +I0903 14:40:00.526082 1 sniffer.go:388] send tcp 192.168.9.2:31258 -> 142.250.189.164:80 len:0 id:0x0000 flags: R A seqnum:2976118516 ack:3464081164 win:0 xsum:0x1670 options:{TS:true TSVal:384415501 TSEcr:1559451915 SACKBlocks:[]} gso:stack.GSO{Type:1, NeedsCsum:true, CsumOffset:0x10, MSS:0x58c, L3HdrLen:0x14, MaxSize:0x10000} +I0903 14:40:00.425033 1 strace.go:576] [ 1: 1] wget E recvfrom(0x3 socket:[3], 0x559039f02bd0, 0x10000, 0x0, 0x7ee9163d2f80, 0x7ee9163d2f60 {length=28})