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})