Files
2025-01-29 21:16:51 -08:00

554 lines
16 KiB
Go

// 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)
}
// Suppress 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)
}
// 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)
length += uint16(len(tcpHdr))
// Build IP header.
var ipHdr []byte
if srcIP.Is4() {
length += header.IPv4MinimumSize
hdr := make(header.IPv4, header.IPv4MinimumSize)
hdr.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,
})
ipHdr = hdr
} else {
length += header.IPv6MinimumSize
hdr := make(header.IPv6, header.IPv6MinimumSize)
hdr.Encode(&header.IPv6Fields{
PayloadLength: uint16(len(tcpHdr)),
TransportProtocol: transProto,
SrcAddr: tcpip.AddrFrom16(srcIP.As16()),
DstAddr: tcpip.AddrFrom16(dstIP.As16()),
ExtensionHeaders: header.IPv6ExtHdrSerializer{},
})
ipHdr = hdr
}
// 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, &timestamp)
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
}