diff --git a/.golangci.yml b/.golangci.yml index 05d2320..ac4e65e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,6 +19,10 @@ linters: - ireturn - nolintlint - varnamelen + - depguard + - dupword + - testpackage + - godox linters-settings: misspell: @@ -28,6 +32,23 @@ linters-settings: block-size: 2 exhaustive: default-signifies-exhaustive: true + gosec: + excludes: + - G104 # Duplicated errcheck checks + - G404 # Use of math/rand for RNG + wrapcheck: + ignoreSigs: + - .Errorf( + - errors.New( + - errors.Unwrap( + - .Apply( + revive: + rules: + - name: unused-parameter + severity: warning + disabled: true + arguments: + - allowRegex: "^_" issues: exclude-rules: @@ -35,3 +56,10 @@ issues: - linters: - paralleltest text: "does not use range value in test Run" + - linters: + - errcheck + text: "Error return value of .(tcp|ip|udp).SerializeTo. is not checked" + exclude: + # these should be self-documenting + - "exported const ((TCP|IPv4)(Field|Flag|Option).*|IPFieldVersion) should (have comment|be of the form)" + - "do not define dynamic errors, use wrapped static errors instead:" diff --git a/README.md b/README.md index 97a0af0..9e115e7 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,13 @@ that "branching actions are not supported on inbound trees". Practically, this m actions can only be applied to outbound packets, while the sleep, drop, and tamper actions can apply to packets of either direction. (Note that this package does not currently enforce this; this is also a bug.) +## Disclaimer + +Currently only IPv4 and TCP are supported. There are plans to add support for UDP in the future (although pull requests +are welcome! Look at `TCPTamperAction` and `IPv4TamperAction` in `actions/tamper_action.go` as examples. +`UDPTamperAction` must implement the `actions.Action` interface). There are no plans at the moment to add support +for IPv6. + ## Credits See https://censorship.ai for more information about Geneva itself. diff --git a/actions/action_test.go b/actions/action_test.go index fdbe573..866e40e 100644 --- a/actions/action_test.go +++ b/actions/action_test.go @@ -1,14 +1,15 @@ package actions_test import ( - "encoding/binary" "fmt" "testing" - "github.com/getlantern/geneva/actions" - "github.com/getlantern/geneva/internal/scanner" "github.com/google/gopacket" "github.com/google/gopacket/layers" + + "github.com/getlantern/geneva/actions" + "github.com/getlantern/geneva/common" + "github.com/getlantern/geneva/internal/scanner" ) var ( @@ -43,18 +44,10 @@ func TestIPv4HeaderChecksum(t *testing.T) { } expected := uint16(0xb861) - chksum := actions.ComputeIPv4Checksum(header) + chksum := common.CalculateIPv4Checksum(header) if chksum != expected { t.Fatalf("expected %#04x, got %#04x", expected, chksum) } - - if val := binary.BigEndian.Uint16(header[10:]); val != expected { - t.Fatalf("expected %#04x in header, got %#04x", expected, val) - } - - if !actions.VerifyIPv4Checksum(header) { - t.Fatal("checksum validation failed") - } } func TestSendAction(t *testing.T) { diff --git a/actions/actions.go b/actions/actions.go index 4e9edcb..01bd3d4 100644 --- a/actions/actions.go +++ b/actions/actions.go @@ -4,17 +4,21 @@ package actions import ( + "errors" "fmt" + "github.com/google/gopacket" + "github.com/getlantern/geneva/internal" "github.com/getlantern/geneva/internal/scanner" "github.com/getlantern/geneva/triggers" - "github.com/google/gopacket" // gopacket best practice says import this, too. _ "github.com/google/gopacket/layers" ) +var ErrInvalidAction = errors.New("invalid action") + // ActionTree represents a Geneva (trigger, action) pair. // // Technically, Geneva uses the term "action tree" to refer to the tree of actions in the tuple @@ -122,5 +126,5 @@ func ParseAction(s *scanner.Scanner) (Action, error) { return DefaultSendAction, nil } - return nil, fmt.Errorf("invalid action at %d", s.Pos()) + return nil, fmt.Errorf("%w at %d", ErrInvalidAction, s.Pos()) } diff --git a/actions/duplicate_action.go b/actions/duplicate_action.go index 490f7ed..a2b28ef 100644 --- a/actions/duplicate_action.go +++ b/actions/duplicate_action.go @@ -4,9 +4,10 @@ import ( "errors" "fmt" + "github.com/google/gopacket" + "github.com/getlantern/geneva/internal" "github.com/getlantern/geneva/internal/scanner" - "github.com/google/gopacket" ) // DuplicateAction is a Geneva action that duplicates a packet and applies separate action trees to @@ -114,18 +115,22 @@ func ParseDuplicateAction(s *scanner.Scanner) (Action, error) { } if action.Left, err = ParseAction(s); err != nil { + if !errors.Is(err, ErrInvalidAction) { + return nil, err + } + if c, err2 := s.Peek(); err2 == nil && c == ',' { action.Left = &SendAction{} } else { return nil, fmt.Errorf( - "error parsing first action of duplicate rule: %v", + "error parsing first action of duplicate rule: %w", err) } } if _, err = s.Expect(","); err != nil { return nil, fmt.Errorf( - "unexpected token in duplicate rule: %v", + "unexpected token in duplicate rule: %w", internal.EOFUnexpected(err), ) } @@ -135,14 +140,14 @@ func ParseDuplicateAction(s *scanner.Scanner) (Action, error) { action.Right = &SendAction{} } else { return nil, fmt.Errorf( - "error parsing second action of duplicate rule: %v", + "error parsing second action of duplicate rule: %w", err) } } if _, err = s.Expect(")"); err != nil { return nil, fmt.Errorf( - "unexpected token in duplicate rule: %v", + "unexpected token in duplicate rule: %w", internal.EOFUnexpected(err), ) } diff --git a/actions/fragment_packet.go b/actions/fragment_packet.go index 5c0f14c..db035c9 100644 --- a/actions/fragment_packet.go +++ b/actions/fragment_packet.go @@ -7,10 +7,12 @@ import ( "strconv" "strings" - "github.com/getlantern/geneva/internal" - "github.com/getlantern/geneva/internal/scanner" "github.com/google/gopacket" "github.com/google/gopacket/layers" + + "github.com/getlantern/geneva/common" + "github.com/getlantern/geneva/internal" + "github.com/getlantern/geneva/internal/scanner" ) // FragmentAction is a Geneva action that splits a packet into two fragments and applies separate @@ -63,7 +65,6 @@ func (a *FragmentAction) Apply(packet gopacket.Packet) ([]gopacket.Packet, error case layers.LayerTypeTCP: packets, err = fragmentTCPSegment(packet, a.FragSize) default: - // nolint: godox // TODO: should we log this? packets, err = duplicate(packet) } @@ -138,19 +139,12 @@ func fragmentTCPSegment(packet gopacket.Packet, fragSize int) ([]gopacket.Packet ipv4Buf := buf[ofs:] - // fix up the IP header's Total Length field and checksum + // fix up the IP header's Total Length field binary.BigEndian.PutUint16(ipv4Buf[2:], uint16(f1Len-ofs)) ipHdrLen := uint16(ipv4Buf[0]&0x0f) * 4 - ComputeIPv4Checksum(ipv4Buf[:ipHdrLen]) - - chksum := ComputeTCPChecksum( - ipv4Buf[:ipHdrLen], - ipv4Buf[ipHdrLen:headersLen-ofs], - ipv4Buf[headersLen-ofs:], - ) - binary.BigEndian.PutUint16(ipv4Buf[ipHdrLen+16:], chksum) first := gopacket.NewPacket(buf, packet.Layers()[0].LayerType(), gopacket.NoCopy) + updateChecksums(first) // create the second fragment. f2Len := headersLen + tcpPayloadLen - fragSize @@ -160,9 +154,8 @@ func fragmentTCPSegment(packet gopacket.Packet, fragSize int) ([]gopacket.Packet ipv4Buf = buf[ofs:] - // fix up the IP header's Total Length field and checksum + // fix up the IP header's Total Length field binary.BigEndian.PutUint16(ipv4Buf[2:], uint16(f2Len-ofs)) - ComputeIPv4Checksum(ipv4Buf[:ipHdrLen]) // Fix up the TCP sequence number. // Excitingly, Go does integer wrapping, so we don't have to. @@ -171,15 +164,8 @@ func fragmentTCPSegment(packet gopacket.Packet, fragSize int) ([]gopacket.Packet seqNum += uint32(fragSize) binary.BigEndian.PutUint32(tcp[4:], seqNum) - // fix up the TCP checksum - chksum = ComputeTCPChecksum( - ipv4Buf[:ipHdrLen], - ipv4Buf[ipHdrLen:headersLen-ofs], - ipv4Buf[headersLen-ofs:], - ) - binary.BigEndian.PutUint16(ipv4Buf[ipHdrLen+16:], chksum) - second := gopacket.NewPacket(buf, packet.Layers()[0].LayerType(), gopacket.NoCopy) + updateChecksums(second) return []gopacket.Packet{first, second}, nil } @@ -241,12 +227,13 @@ func FragmentIPPacket(packet gopacket.Packet, fragSize int) ([]gopacket.Packet, flagsAndFrags := (binary.BigEndian.Uint16(ipv4Buf[6:]) | 0x20) & 0xe0 binary.LittleEndian.PutUint16(ipv4Buf[6:], flagsAndFrags) - ComputeIPv4Checksum(ipv4Buf[:hdrLen]) - // slice off everything past the first fragment's end buf = buf[:uint16(ofs)+hdrLen+offset] first := gopacket.NewPacket(buf, packet.Layers()[0].LayerType(), gopacket.NoCopy) + if ipv4, ok := first.Layer(layers.LayerTypeIPv4).(*layers.IPv4); ok && ipv4 != nil { + common.UpdateIPv4Checksum(ipv4) + } // Now start on the second fragment. // First copy the old IP header as-is, then copy just the second fragment's payload right @@ -264,84 +251,32 @@ func FragmentIPPacket(packet gopacket.Packet, fragSize int) ([]gopacket.Packet, flagsAndFrags = (binary.BigEndian.Uint16(ipv4Buf[6:]) & 0x40) + uint16(fragSize) binary.BigEndian.PutUint16(ipv4Buf[6:], flagsAndFrags) - ComputeIPv4Checksum(ipv4Buf[:hdrLen]) - second := gopacket.NewPacket(buf, packet.Layers()[0].LayerType(), gopacket.NoCopy) + if ipv4, _ := second.Layer(layers.LayerTypeIPv4).(*layers.IPv4); ipv4 != nil { + common.UpdateIPv4Checksum(ipv4) + } return []gopacket.Packet{first, second}, nil } -// VerifyIPv4Checksum verifies whether an IPv4 header's checksum field is correct. -func VerifyIPv4Checksum(header []byte) bool { - c := internal.OnesComplementChecksum{} - - for i := 0; i < len(header); i += 2 { - c.Add(binary.BigEndian.Uint16(header[i:])) +func updateChecksums(packet gopacket.Packet) { + if ipv4, _ := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4); ipv4 != nil { + common.UpdateIPv4Checksum(ipv4) } - return c.Finalize() == 0 + if tcp, _ := packet.Layer(layers.LayerTypeTCP).(*layers.TCP); tcp != nil { + common.UpdateTCPChecksum(tcp) + } } -// ComputeIPv4Checksum computes a new checksum for the given IPv4 header and writes it into the -// header. -func ComputeIPv4Checksum(header []byte) uint16 { - // zero out the old checksum - binary.BigEndian.PutUint16(header[10:], 0) - +// VerifyIPv4Checksum verifies whether an IPv4 header's checksum field is correct. +func VerifyIPv4Checksum(header []byte) bool { c := internal.OnesComplementChecksum{} for i := 0; i < len(header); i += 2 { c.Add(binary.BigEndian.Uint16(header[i:])) } - chksum := c.Finalize() - binary.BigEndian.PutUint16(header[10:], chksum) - - return chksum -} - -// ComputeTCPChecksum computes the checksum field of a TCP header. -func ComputeTCPChecksum(ipHeader, tcpHeader, payload []byte) uint16 { - c := internal.OnesComplementChecksum{} - - // pseudo-header - c.Add(binary.BigEndian.Uint16(ipHeader[12:])) // source ip address - c.Add(binary.BigEndian.Uint16(ipHeader[14:])) // destination ip address - c.Add(uint16(ipHeader[6]) << 8) // protocol - tcpLength := binary.BigEndian.Uint16(ipHeader[2:]) - tcpLength -= uint16((ipHeader[0] & 0xf)) * 4 - c.Add(tcpLength) // "TCP Length" from RFC 793 - - // TCP header - for i := 0; i < len(tcpHeader); i += 2 { - if i == 16 { - // don't add existing checksum value - continue - } - - c.Add(binary.BigEndian.Uint16(tcpHeader[i:])) - } - - // TCP payload - for i := 0; i < len(payload); i += 2 { - if len(payload)-i == 1 { - // If there are an odd number of octets in the payload, the last octet must - // be padded on the right with zeros. - c.Add(uint16(payload[i]) << 8) - } else { - c.Add(binary.BigEndian.Uint16(payload[i:])) - } - } - - return c.Finalize() -} - -// VerifyTCPChecksum verifies whether a TCP header's checksum field is correct. -func VerifyTCPChecksum(ipHeader, tcpHeader, payload []byte) bool { - c := internal.OnesComplementChecksum{} - c.Add(ComputeTCPChecksum(ipHeader, tcpHeader, payload)) - c.Add(binary.BigEndian.Uint16(tcpHeader[16:])) - return c.Finalize() == 0 } @@ -439,6 +374,10 @@ func ParseFragmentAction(s *scanner.Scanner) (Action, error) { } if action.FirstFragmentAction, err = ParseAction(s); err != nil { + if !errors.Is(err, ErrInvalidAction) { + return nil, err + } + if c, err2 := s.Peek(); err2 == nil && c == ',' { action.FirstFragmentAction = &SendAction{} } else { @@ -457,13 +396,13 @@ func ParseFragmentAction(s *scanner.Scanner) (Action, error) { if c, err2 := s.Peek(); err2 == nil && c == ')' { action.SecondFragmentAction = &SendAction{} } else { - return nil, fmt.Errorf("error parsing second action of fragment rule: %v", err) + return nil, fmt.Errorf("error parsing second action of fragment rule: %w", err) } } if _, err := s.Expect(")"); err != nil { return nil, fmt.Errorf( - "unexpected token in fragment rule: %v", + "unexpected token in fragment rule: %w", internal.EOFUnexpected(err), ) } diff --git a/actions/tamper_action.go b/actions/tamper_action.go index 10c7288..16e7a35 100644 --- a/actions/tamper_action.go +++ b/actions/tamper_action.go @@ -1,12 +1,34 @@ package actions import ( + "encoding/binary" "errors" "fmt" + "math/rand" + "net" + "strconv" "strings" + "time" - "github.com/getlantern/geneva/internal/scanner" "github.com/google/gopacket" + "github.com/google/gopacket/layers" + + "github.com/getlantern/geneva/common" + "github.com/getlantern/geneva/internal" + "github.com/getlantern/geneva/internal/scanner" +) + +const ( + // TamperReplace replaces the value of a packet field with the given value. + TamperReplace = iota + // TamperCorrupt replaces the value of a packet field with a randomly-generated value. + TamperCorrupt +) + +var ( + ErrInvalidTamperMode = errors.New("invalid tamper mode") + ErrInvalidTamperRule = errors.New("invalid tamper rule") + ErrUDPNotSupported = errors.New("UDP tamper action not currently supported") ) // TamperMode describes the way that the "tamper" action can manipulate a packet. @@ -19,23 +41,22 @@ func (tm TamperMode) String() string { return "replace" case TamperCorrupt: return "corrupt" - case TamperAdd: - return "add" } return "" } -const ( - // TamperReplace replaces the value of a packet field with the given value. - TamperReplace = iota - // TamperCorrupt replaces the value of a packet field with a randomly-generated value. - TamperCorrupt - // TamperAdd adds the value to a packet field. - TamperAdd -) - -// TamperAction is a Geneva action that modifies packets (typically values in the packet header). +// TamperAction is a Geneva action that modifies a given field of a packet while always +// trying to keep the packet valid. This is done by updating the checksums and lengths +// unless the tamper rule is specifically for the checksum or length. If proto is TCP +// and the field is an option, the option will be added if it doesn't exist. +// +// There are two modes for tampering: +// +// "replace" - replace the field with the given value. +// "corrupt" - replace the field with a randomly-generated value of the same bitsize. +// +// Currently, only TCP and IPv4 is supported. UDP support is planned for the future. type TamperAction struct { // Proto is the protocol layer where the modification will occur. Proto string @@ -50,11 +71,6 @@ type TamperAction struct { Action Action } -// Apply applies this action to the given packet. -func (a *TamperAction) Apply(packet gopacket.Packet) ([]gopacket.Packet, error) { - return nil, errors.New("tamper action unimplemented") -} - // String returns a string representation of this Action. func (a *TamperAction) String() string { newValue := "" @@ -69,77 +85,607 @@ func (a *TamperAction) String() string { // ParseTamperAction parses a string representation of a "tamper" action. // // If the string is malformed, an error will be returned instead. +// +//nolint:errorlint func ParseTamperAction(s *scanner.Scanner) (Action, error) { if _, err := s.Expect("tamper{"); err != nil { - return nil, fmt.Errorf("invalid tamper rule: %w", err) + return nil, fmt.Errorf("%s: %w", ErrInvalidTamperRule, err) } str, err := s.Until('}') if err != nil { - return nil, fmt.Errorf("invalid tamper rule: %w", err) + return nil, fmt.Errorf("%s: %w", ErrInvalidTamperRule, err) } _, _ = s.Pop() fields := strings.Split(str, ":") if len(fields) < 3 || len(fields) > 4 { - return nil, fmt.Errorf("invalid fields for tamper rule: %s", str) + return nil, fmt.Errorf("%s: invalid field, %w", ErrInvalidTamperRule, err) } - action := &TamperAction{} - - switch strings.ToLower(fields[0]) { - case "ip": - action.Proto = "IP" - case "tcp": - action.Proto = "TCP" - case "udp": - action.Proto = "UDP" - default: - return nil, fmt.Errorf( - "invalid tamper rule: %q is not a recognized protocol", - fields[0], - ) - } + var ( + proto = strings.ToUpper(fields[0]) + field = strings.ToLower(fields[1]) - action.Field = fields[1] + mode TamperMode + newValue string + ) switch strings.ToLower(fields[2]) { case "replace": - action.Mode = TamperReplace - action.NewValue = fields[3] + mode = TamperReplace + newValue = fields[3] case "corrupt": - action.Mode = TamperCorrupt + mode = TamperCorrupt default: return nil, fmt.Errorf( - "invalid tamper mode: %q must be either 'replace' or 'corrupt'", + "%w: %q must be either 'replace' or 'corrupt'", + ErrInvalidTamperMode, fields[2], ) } - if _, err := s.Expect("("); err != nil { - action.Action = &SendAction{} + tamperAction := TamperAction{ + Proto: proto, + Field: field, + Mode: mode, + NewValue: newValue, + } - return action, nil //nolint:nilerr + if _, err = s.Expect("("); err != nil { + tamperAction.Action = &SendAction{} + return newTamperAction(tamperAction) } - if action.Action, err = ParseAction(s); err != nil { + if tamperAction.Action, err = ParseAction(s); err != nil { + if !errors.Is(err, ErrInvalidAction) { + return nil, err + } + if c, err2 := s.Peek(); err2 == nil && c == ')' { - action.Action = &SendAction{} + tamperAction.Action = &SendAction{} } else { - return nil, fmt.Errorf("invalid action for tamper rule: %w", err) + return nil, fmt.Errorf("%s: invalid action, %w", ErrInvalidTamperRule, err) } } if _, err = s.Expect(","); err == nil { if !s.FindToken(")", true) { - return nil, errors.New("tamper rules can only have one action") + return nil, fmt.Errorf("%w: only one action is allowed", ErrInvalidTamperRule) } } - if _, err := s.Expect(")"); err != nil { - return nil, fmt.Errorf("unexpected token in tamper rule: %w", err) + if _, err = s.Expect(")"); err != nil { + return nil, fmt.Errorf("%s: unexpected token: %w", ErrInvalidTamperRule, internal.EOFUnexpected(err)) + } + + return newTamperAction(tamperAction) +} + +func newTamperAction(ta TamperAction) (Action, error) { + switch ta.Proto { + case "IP": + return NewIPv4TamperAction(ta) + case "TCP": + return NewTCPTamperAction(ta) + case "UDP": + return nil, ErrUDPNotSupported + default: + return nil, fmt.Errorf("%w: %q is not a recognized protocol", ErrInvalidTamperRule, ta.Proto) } +} + +// +// TCP Tamper Action +// + +// TCPField is a TCP field that can be modified by a TCPTamperAction. +type TCPField uint8 + +const ( + // supported TCP options. The other options are apparently obsolete and not used. + TCPOptionEol = layers.TCPOptionKindEndList + TCPOptionNop = layers.TCPOptionKindNop + TCPOptionMss = layers.TCPOptionKindMSS + TCPOptionWscale = layers.TCPOptionKindWindowScale + TCPOptionSackok = layers.TCPOptionKindSACKPermitted + TCPOptionSack = layers.TCPOptionKindSACK + TCPOptionTimestamp = layers.TCPOptionKindTimestamps + + // obsolete TCP options geneva uses and is in the strategies.md document: + // https://github.com/Kkevsterrr/geneva/blob/master/strategies.md + TCPOptionAltCkhsum = 14 + TCPOptionMd5Header = 19 + TCPOptionUto = 28 + + // putting fields after options so that we can use the gopacket.TCPOptionKind constants for options. + // this lets us use the same map for both fields and options and also directly compare + // tcpTamperAction.field == TCPOption when iterating over tcpPacket.Options. + TCPFieldSrcPort = 9 + TCPFieldDstPort = 10 + TCPFieldSeq = 11 + TCPFieldAck = 12 + TCPFieldDataOff = 13 + TCPFieldFlags = 15 + TCPFieldWindow = 16 + TCPFieldUrgent = 17 + TCPFieldChecksum = 18 + TCPLoad = 20 + + // TCP flag string representations for tamper rules. + TCPFlagFin = "f" + TCPFlagSyn = "s" + TCPFlagRst = "r" + TCPFlagPsh = "p" + TCPFlagAck = "a" + TCPFlagUrg = "u" + TCPFlagEce = "e" + TCPFlagCwr = "c" + TCPFlagNop = "n" +) + +var ( + // tcpFields is a map of TCP fields to their corresponding TCPField constants. + // easier to use a map than a switch statement when parsing tamper rules. + tcpFields = map[string]TCPField{ + "srcport": TCPFieldSrcPort, + "dstport": TCPFieldDstPort, + "seq": TCPFieldSeq, + "ack": TCPFieldAck, + "dataofs": TCPFieldDataOff, + "flags": TCPFieldFlags, + "window": TCPFieldWindow, + "urgent": TCPFieldUrgent, + "chksum": TCPFieldChecksum, + "options-eol": TCPOptionEol, + "options-nop": TCPOptionNop, + "options-mss": TCPOptionMss, + "options-wscale": TCPOptionWscale, + "options-sackok": TCPOptionSackok, + "options-sack": TCPOptionSack, + "options-timestamp": TCPOptionTimestamp, + "options-altchksum": TCPOptionTimestamp, + "options-md5header": TCPOptionMd5Header, + "options-uto": TCPOptionUto, + "load": TCPLoad, + } + + // tcpOptionLengths is a map of TCP options to the length of their data field. + tcpOptionLengths = map[TCPField]int{ + TCPOptionEol: 0, + TCPOptionNop: 0, + TCPOptionMss: 2, + TCPOptionWscale: 1, + TCPOptionSackok: 0, // the geneva team has this listed as 0, so at most the data is deleted + TCPOptionSack: 0, // same as above + TCPOptionTimestamp: 8, + TCPOptionAltCkhsum: 3, + TCPOptionMd5Header: 16, + TCPOptionUto: 2, + } +) + +// TCPTamperAction is a Geneva action that modifies TCP packets. +type TCPTamperAction struct { + // TamperAction is the underlying action parsed from the tamper rule. + TamperAction + // field is the TCP field to modify. + field TCPField + // valueGen is the value generator to use when modifying the field. + valueGen tamperValueGen +} + +// NewTCPTamperAction returns a new TCPTamperAction from the given TamperAction. +func NewTCPTamperAction(ta TamperAction) (*TCPTamperAction, error) { + field, ok := tcpFields[ta.Field] + if !ok { + return nil, fmt.Errorf("%w: %q is not a recognized TCP field", ErrInvalidTamperRule, ta.Field) + } + + switch ta.Mode { + case TamperCorrupt: + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + return &TCPTamperAction{ + TamperAction: ta, + field: field, + valueGen: &tamperCorruptGen{r}, + }, nil + case TamperReplace: + gen := &tamperReplaceGen{} + + switch { + case field == TCPFieldFlags: + gen.vUint = tcpFlagsToUint32(ta.NewValue) + case field < TCPFieldSrcPort: + // if field is an option, we need to convert the value to a byte slice + var b []byte + if val, err := strconv.ParseUint(ta.NewValue, 10, 64); err == nil { + b = make([]byte, 8) + binary.BigEndian.PutUint64(b, val) + b = b[8-tcpOptionLengths[field]:] + } else { + b = []byte(ta.NewValue) + } + + gen.vBytes = b + case field == TCPLoad: + gen.vBytes = []byte(ta.NewValue) + default: + val, err := strconv.ParseUint(ta.NewValue, 10, 32) + if err != nil { + return nil, fmt.Errorf( + "%w: %q is not a valid value for field %q", + ErrInvalidTamperRule, + ta.NewValue, + ta.Field, + ) + } + + gen.vUint = uint32(val) + } + + return &TCPTamperAction{ + TamperAction: ta, + field: field, + valueGen: gen, + }, nil + } + + return nil, fmt.Errorf("%w: %q is not a valid tamper mode for TCP", ErrInvalidTamperRule, ta.Mode) +} + +// Apply applies the tamper action to the given packet. +func (a *TCPTamperAction) Apply(packet gopacket.Packet) ([]gopacket.Packet, error) { + tcp, _ := packet.Layer(layers.LayerTypeTCP).(*layers.TCP) + if tcp == nil { + return nil, errors.New("packet does not have a TCP layer") + } + + tamperTCP(tcp, a.field, a.valueGen) + + // if tampering with TCP options, we need to update the data offset and checksum + if strings.HasPrefix(a.Field, "options") { + updateTCPDataOffAndChksum(tcp) + + if ip, _ := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4); ip != nil { + updateIPv4Length(ip) + } + } + + return a.Action.Apply(packet) +} + +// tamperTCP modifies the given TCP field using the given value generator. +func tamperTCP(tcp *layers.TCP, field TCPField, valueGen tamperValueGen) { + switch field { + case TCPFieldSrcPort: + tcp.SrcPort = layers.TCPPort(valueGen.uint(16)) + case TCPFieldDstPort: + tcp.DstPort = layers.TCPPort(valueGen.uint(16)) + case TCPFieldSeq: + tcp.Seq = valueGen.uint(32) + case TCPFieldAck: + tcp.Ack = valueGen.uint(32) + case TCPFieldDataOff: + tcp.DataOffset = uint8(valueGen.uint(8)) + case TCPFieldWindow: + tcp.Window = uint16(valueGen.uint(16)) + case TCPFieldUrgent: + tcp.Urgent = uint16(valueGen.uint(16)) + case TCPFieldChecksum: + tcp.Checksum = uint16(valueGen.uint(16)) + case TCPFieldFlags: + setTCPFlags(tcp, uint16(valueGen.uint(16))) + case TCPLoad: + tcp.Payload = valueGen.bytes(1480) + default: + // find option in TCP header + var opt *layers.TCPOption + + for i, o := range tcp.Options { + if field == TCPField(o.OptionType) { + opt = &tcp.Options[i] + break + } + } + + // create option if it doesn't exist and move options-eol to the end of the list + if opt == nil { + tcp.Options = append(tcp.Options, layers.TCPOption{ + OptionType: layers.TCPOptionKind(field), + }) + + ol := len(tcp.Options) + opt = &tcp.Options[ol-1] + } + + opt.OptionData = valueGen.bytes(tcpOptionLengths[field]) + if field == TCPOptionEol || field == TCPOptionNop { + opt.OptionLength = 1 + } else { + opt.OptionLength = uint8(tcpOptionLengths[field]) + 2 + } + } + + // let gopacket handle converting the modified TCP headers into []byte for us since we changed the struct fields + // instead of the underlying []byte directly. SerializeTo doesn't write the changes to the raw packet + // so we have to copy the formatted bytes back into the packet header. + sb := gopacket.NewSerializeBuffer() + tcp.SerializeTo(sb, gopacket.SerializeOptions{}) + tcp.Contents = make([]byte, len(sb.Bytes())) + copy(tcp.Contents, sb.Bytes()) +} + +// tcpFlagsToUint32 converts a string of TCP flags to a uint32 bitmap. +func tcpFlagsToUint32(flags string) uint32 { + flags = strings.ToLower(flags) + + var f uint32 + for _, c := range flags { //nolint:wsl + switch c { + case 'f': // FIN + f |= 0x0001 + case 's': // SYN + f |= 0x0002 + case 'r': // RST + f |= 0x0004 + case 'p': // PSH + f |= 0x0008 + case 'a': // ACK + f |= 0x0010 + case 'u': // URG + f |= 0x0020 + case 'e': // ECE + f |= 0x0040 + case 'c': // CWR + f |= 0x0080 + case 'n': // NS + f |= 0x0100 + } + } + + return f +} + +// setTCPFlags sets the tcp struct fields using flags bitmap, does not modify the raw packet bytes. +func setTCPFlags(tcp *layers.TCP, flags uint16) { + tcp.FIN = flags&0x0001 != 0 + tcp.SYN = flags&0x0002 != 0 + tcp.RST = flags&0x0004 != 0 + tcp.PSH = flags&0x0008 != 0 + tcp.ACK = flags&0x0010 != 0 + tcp.URG = flags&0x0020 != 0 + tcp.ECE = flags&0x0040 != 0 + tcp.CWR = flags&0x0080 != 0 + tcp.NS = flags&0x0100 != 0 +} + +// updateTCPDataOffAndChksum updates the TCP data offset and checksum fields on the TCP struct +// and in the raw packet bytes. +func updateTCPDataOffAndChksum(tcp *layers.TCP) { + // update data offset + headerLen := len(tcp.Contents) + tcp.DataOffset = uint8(headerLen / 4) + tcp.Contents[12] = tcp.DataOffset << 4 + + // update checksum. + common.UpdateTCPChecksum(tcp) +} + +// +// IPv4 Tamper Action +// + +// IPv4Field is an IPv4 field that can be modified by an IPv4TamperAction. +type IPv4Field uint8 + +const ( + // supported IPv4 fields. + IPv4FieldSrcIP = iota + IPv4FieldDstIP + IPv4FieldVersion + IPv4FieldIHL + IPv4FieldTOS + IPv4FieldLength + IPv4FieldID + IPv4FieldFlags + IPv4FieldFragOffset + IPv4FieldTTL + IPv4FieldProtocol + IPv4FieldChecksum + IPv4Load +) + +var ipv4Fields = map[string]IPv4Field{ + "srcip": IPv4FieldSrcIP, + "dstip": IPv4FieldDstIP, + "verion": IPv4FieldVersion, + "ihl": IPv4FieldIHL, + "tos": IPv4FieldTOS, + "len": IPv4FieldLength, + "id": IPv4FieldID, + // + // I don't know what the flags will look like in a tamper rule + // shouldn't be a problem since there isn't any tamper rules for IP flags currently + // "flags": IPv4FieldFlags, + // + "fragoffset": IPv4FieldFragOffset, + "ttl": IPv4FieldTTL, + "protocol": IPv4FieldProtocol, + "checksum": IPv4FieldChecksum, + "load": IPv4Load, +} + +// IPv4TamperAction is a Geneva action that modifies IPv4 packets. +type IPv4TamperAction struct { + // TamperAction is the underlying action parsed from the tamper rule. + TamperAction + // field is the IPv4 field to modify. + field IPv4Field + // valueGen is the value generator to use when modifying the field. + valueGen tamperValueGen +} + +// NewIPv4TamperAction returns a new IPv4TamperAction from the given TamperAction. +func NewIPv4TamperAction(ta TamperAction) (*IPv4TamperAction, error) { + field, ok := ipv4Fields[ta.Field] + if !ok { + return nil, fmt.Errorf("%w: %q is not a recognized IPv4 field", ErrInvalidTamperRule, ta.Field) + } + + switch ta.Mode { + case TamperCorrupt: + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + return &IPv4TamperAction{ + TamperAction: ta, + field: field, + valueGen: &tamperCorruptGen{r}, + }, nil + case TamperReplace: + gen := &tamperReplaceGen{} + + switch field { + case IPv4FieldSrcIP, IPv4FieldDstIP: + // parse IP address from NewValue and convert to []byte + ip := net.ParseIP(ta.NewValue) + if ip == nil { + return nil, fmt.Errorf("%w: %q is not a valid IPv4 address", ErrInvalidTamperRule, ta.NewValue) + } + + if ip.To4() == nil { + return nil, fmt.Errorf("%w: IPv6 is not supported", ErrInvalidTamperRule) + } + + gen.vBytes = ip + case IPv4Load: + gen.vBytes = []byte(ta.NewValue) + default: + // parse uint from NewValue + val, err := strconv.ParseUint(ta.NewValue, 10, 32) + if err != nil { + return nil, fmt.Errorf("%w: %q is not a valid value for field %q", ErrInvalidTamperRule, ta.NewValue, ta.Field) + } + + gen.vUint = uint32(val) + } + + return &IPv4TamperAction{ + TamperAction: ta, + field: field, + valueGen: gen, + }, nil + } + + return nil, fmt.Errorf("%w: %q is not a valid tamper mode for IPv4", ErrInvalidTamperRule, ta.Mode) +} + +// Apply applies the tamper action to the given packet. +func (a *IPv4TamperAction) Apply(packet gopacket.Packet) ([]gopacket.Packet, error) { + ip, _ := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4) + if ip == nil { + return nil, errors.New("packet does not have a IPv4 layer") + } + + tamperIPv4(ip, a.field, a.valueGen) + common.UpdateIPv4Checksum(ip) + + return a.Action.Apply(packet) +} + +// tamperIPv4 modifies the given IP field using the given value generator. +func tamperIPv4(ip *layers.IPv4, field IPv4Field, valueGen tamperValueGen) { + switch field { + case IPv4FieldSrcIP: + ip.SrcIP = valueGen.bytes(4) + case IPv4FieldDstIP: + ip.DstIP = valueGen.bytes(4) + case IPv4FieldVersion: + ip.Version = uint8(valueGen.uint(8)) + case IPv4FieldIHL: + ip.IHL = uint8(valueGen.uint(8)) + case IPv4FieldTOS: + ip.TOS = uint8(valueGen.uint(8)) + case IPv4FieldLength: + ip.Length = uint16(valueGen.uint(16)) + case IPv4FieldFlags: + // not implemented yet. see comment above. + case IPv4FieldFragOffset: + ip.FragOffset = uint16(valueGen.uint(16)) + case IPv4FieldTTL: + ip.TTL = uint8(valueGen.uint(8)) + case IPv4FieldProtocol: + ip.Protocol = layers.IPProtocol(valueGen.uint(8)) + case IPv4FieldChecksum: + ip.Checksum = uint16(valueGen.uint(16)) + case IPv4Load: + ip.Payload = valueGen.bytes(1480) + } + + // let gopacket handle converting modified packet into []byte again, it's just easier + // again copy the bytes back into the packet header + sb := gopacket.NewSerializeBuffer() + ip.SerializeTo(sb, gopacket.SerializeOptions{}) + ip.Contents = make([]byte, len(sb.Bytes())) + copy(ip.Contents, sb.Bytes()) +} + +// updateIPv4Length updates the IPv4 length. +func updateIPv4Length(ip *layers.IPv4) { + length := len(ip.Contents) + len(ip.Payload) + ip.Length = uint16(length) + binary.BigEndian.PutUint16(ip.Contents[2:4], ip.Length) +} + +// tamperValueGen is a value generator for tamper actions. +type tamperValueGen interface { + uint(bitSize int) uint32 + bytes(n int) []byte +} + +// tamperReplaceGen just returns newValue casted to the appropriate type. +// it assumes that newValue is the correct type. +type tamperReplaceGen struct { + vUint uint32 + vBytes []byte +} + +// uint returns the uint value. bitSize is ignored. +func (g *tamperReplaceGen) uint(_ int) uint32 { + return g.vUint +} + +// bytes returns the byte slice or an empty byte slice if n == 0. +func (g *tamperReplaceGen) bytes(n int) []byte { + if n == 0 { + return []byte{} + } + + return append([]byte{}, g.vBytes...) +} + +// tamperCorruptGen generates random values for tamperCorrupt actions. +type tamperCorruptGen struct { + r *rand.Rand +} + +// uint returns a random value of the given bit size as a uint32. +func (g *tamperCorruptGen) uint(bitSize int) uint32 { + n := g.r.Intn(1< 20 { + n = g.r.Intn(n) + } + + b := make([]byte, n) + g.r.Read(b) - return action, nil + return b } diff --git a/actions/tamper_action_test.go b/actions/tamper_action_test.go new file mode 100644 index 0000000..6bb99fd --- /dev/null +++ b/actions/tamper_action_test.go @@ -0,0 +1,169 @@ +package actions + +import ( + "reflect" + "testing" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + + "github.com/stretchr/testify/assert" + + "github.com/getlantern/geneva/internal/scanner" +) + +func TestParseTamperAction(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + rule string + want Action + wantErr bool + }{ + { + name: "TCP tamper action replace uint", + rule: "tamper{TCP:dataofs:replace:10}", + want: &TCPTamperAction{ + TamperAction: TamperAction{ + Proto: "TCP", + Field: "dataofs", + NewValue: "10", + Mode: TamperReplace, + Action: &SendAction{}, + }, + field: TCPFieldDataOff, + valueGen: &tamperReplaceGen{vUint: 10}, + }, + wantErr: false, + }, { + name: "TCP tamper action replace bytes", + rule: "tamper{TCP:options-mss:replace:15}", + want: &TCPTamperAction{ + TamperAction: TamperAction{ + Proto: "TCP", + Field: "options-mss", + NewValue: "15", + Mode: TamperReplace, + Action: &SendAction{}, + }, + field: TCPOptionMss, + valueGen: &tamperReplaceGen{vBytes: []byte{0x00, 0x0f}}, + }, + wantErr: false, + }, { + name: "IPv4 tamper action replace uint", + rule: "tamper{IP:ttl:replace:15}", + want: &IPv4TamperAction{ + TamperAction: TamperAction{ + Proto: "IP", + Field: "ttl", + NewValue: "15", + Mode: TamperReplace, + Action: &SendAction{}, + }, + field: IPv4FieldTTL, + valueGen: &tamperReplaceGen{vUint: 15}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + s := scanner.NewScanner(tt.rule) + got, err := ParseTamperAction(s) + if (err != nil) != tt.wantErr { + t.Errorf("ParseTamperAction() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseTamperAction() = %#v, want %#v", got, tt.want) + } + }) + } +} + +func TestTamperTCP(t *testing.T) { + t.Parallel() + + type args struct { + tcp *layers.TCP + field TCPField + valueGen tamperValueGen + } + + //nolint:forcetypeassert + tests := []struct { + name string + args args + want []byte + }{ + { + name: "tcp tamper replace existing option", + args: args{ + tcp: testPkt().Layer(layers.LayerTypeTCP).(*layers.TCP), + field: TCPOptionMss, + valueGen: &tamperReplaceGen{vBytes: []byte{0x0f, 0xff}}, + }, + want: []byte{ + 0x30, 0x39, 0xd4, 0x31, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0x00, 0x00, 0x70, 0x02, 0x00, + 0x00, 0x82, 0x9c, 0x00, 0x00, 0x02, 0x04, 0x0f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x54, 0x65, + 0x73, 0x74, + }, + }, { + name: "tcp tamper replace missing option", + args: args{ + tcp: testPkt().Layer(layers.LayerTypeTCP).(*layers.TCP), + field: TCPOptionAltCkhsum, + valueGen: &tamperReplaceGen{vBytes: []byte{0xff, 0xff, 0xff}}, + }, + want: []byte{ + 0x30, 0x39, 0xd4, 0x31, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0x00, 0x00, 0x70, 0x02, 0x00, + 0x00, 0x82, 0x9c, 0x00, 0x00, 0x02, 0x04, 0x20, 0x00, 0x00, 0x0e, 0x05, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x54, 0x65, 0x73, 0x74, + }, + }, { + name: "tcp tamper replace payload", + args: args{ + tcp: testPkt().Layer(layers.LayerTypeTCP).(*layers.TCP), + field: TCPLoad, + valueGen: &tamperReplaceGen{ + vBytes: []byte{ + 0x6d, 0x69, 0x73, 0x73, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x46, 0x61, 0x77, 0x6b, 0x73, + }, + }, + }, + want: []byte{ + 0x30, 0x39, 0xd4, 0x31, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0x00, 0x00, 0x70, 0x02, 0x00, + 0x00, 0x82, 0x9c, 0x00, 0x00, 0x02, 0x04, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, 0x69, + 0x73, 0x73, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x46, 0x61, 0x77, 0x6b, 0x73, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + tamperTCP(tt.args.tcp, tt.args.field, tt.args.valueGen) + + got := append([]byte{}, tt.args.tcp.Contents...) + got = append(got, tt.args.tcp.Payload...) + assert.Equal(t, tt.want, got) + }) + } +} + +func testPkt() gopacket.Packet { + tcpBytes := []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x45, 0x00, + 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x80, 0x06, 0xb9, 0x70, 0xc0, 0xa8, 0x00, 0x01, 0xc0, 0xa8, + 0x00, 0x02, 0x30, 0x39, 0xd4, 0x31, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0x00, 0x00, 0x70, 0x02, + 0x00, 0x00, 0x82, 0x9c, 0x00, 0x00, 0x02, 0x04, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x65, + 0x73, 0x74, + } + + return gopacket.NewPacket(tcpBytes, layers.LinkTypeEthernet, gopacket.Default) +} diff --git a/common/packet.go b/common/packet.go new file mode 100644 index 0000000..1bea0d6 --- /dev/null +++ b/common/packet.go @@ -0,0 +1,49 @@ +// Package common provides common functions for Geneva. +package common + +import ( + "encoding/binary" + + "github.com/google/gopacket/layers" +) + +// UpdateTCPChecksum updates the TCP checksum field and the raw bytes for a gopacket TCP layer. +func UpdateTCPChecksum(tcp *layers.TCP) { + // the ComputeChecksum method requires the checksum bytes in the raw packet to be zeroed out. + tcp.Contents[16] = 0 + tcp.Contents[17] = 0 + + chksum, _ := tcp.ComputeChecksum() + + tcp.Checksum = chksum + binary.BigEndian.PutUint16(tcp.Contents[16:18], chksum) +} + +// UpdateIPv4Checksum updates the IPv4 checksum field and the raw bytes for a gopacket IPv4 layer. +func UpdateIPv4Checksum(ip *layers.IPv4) { + chksum := CalculateIPv4Checksum(ip.Contents) + ip.Checksum = chksum + binary.BigEndian.PutUint16(ip.Contents[10:12], chksum) +} + +// CalculateIPv4Checksum calculates the IPv4 checksum for the given bytes. +// copied from gopacket/layers/ip4.go because they didn't export one. for whatever some reason.. +func CalculateIPv4Checksum(bytes []byte) uint16 { + // Clear checksum bytes + bytes[10] = 0 + bytes[11] = 0 + + // Compute checksum + var csum uint32 + for i := 0; i < len(bytes); i += 2 { + csum += uint32(bytes[i]) << 8 + csum += uint32(bytes[i+1]) + } + + for csum > 0xFFFF { + // Add carry to the sum + csum = (csum >> 16) + uint32(uint16(csum)) + } + // Flip all the bits + return ^uint16(csum) +} diff --git a/geneva.go b/geneva.go index 6e13346..f555a3f 100644 --- a/geneva.go +++ b/geneva.go @@ -10,7 +10,7 @@ // This package aims to implement the same triggers and actions that the Geneva project's canonical // Python package does. // -// Quick Background +// # Quick Background // // Geneva rules are called "strategies". A strategy consists of zero or more "action trees" that can // be applied to inbound or outbound packets. The actions trees define both a "trigger" and a tree @@ -18,11 +18,11 @@ // or more packets that should replace the original packet, which then can be reinjected into the // host OS' network stack. // -// Strategies, Forests, and Action Trees +// # Strategies, Forests, and Action Trees // // Let's work from the top down. A strategy, conceptually, looks like this: // -// outbound-forest \/ inbound-forest +// outbound-forest \/ inbound-forest // // "outbound-forest" and "inbound-forest" are ordered lists of (trigger, action tree) pairs. The // Geneva paper calls these ordered lists "forests". The outbound and inbound forests are separated @@ -39,11 +39,11 @@ // A real example, taken from https://geneva.cs.umd.edu/papers/geneva_ccs19.pdf (pg 2202), would // look like this: // -// [TCP:flags:S]- -// duplicate( -// tamper{TCP:flags:replace:SA}(send), -// send)-| \/ -// [TCP:flags:R]-drop-| +// [TCP:flags:S]- +// duplicate( +// tamper{TCP:flags:replace:SA}(send), +// send)-| \/ +// [TCP:flags:R]-drop-| // // In this example, the outbound forest would trigger on TCP packets that have just the SYN flag set, // and would perform a few different actions on those packets. The inbound forest would only apply @@ -52,7 +52,7 @@ // // In a forest, each action tree must adhere to the syntax "[trigger]-action-|". // -// Triggers +// # Triggers // // A trigger defines a way to match packets so that an action tree can be applied to them. In the // example above, the first trigger is "[TCP:flags:S]". This is a trigger that matches on the TCP @@ -60,29 +60,29 @@ // will not fire for packets that have, i.e., both SYN and ACK set.) If the packet is not a TCP // packet, or the flags do not match exactly, then this trigger will not fire. // -// Actions +// # Actions // // An action simply encodes steps to manipulate a packet. There are a number of actions described in // the Geneva paper: // -// send +// send // // The "send" action simply yields the given packet. (A quirk—or what the paper deems to be // canonical syntax—is to elide any "send" actions in the action tree. For instance, the action // "duplicate(,)" is equivalent to "duplicate(send,send)". Bear this in mind when reading Geneva // strategies!) // -// drop +// drop // // The "drop" action discards the given packet. // -// duplicate(a1, a2) +// duplicate(a1, a2) // // The "duplicate" action copies the original packet, then applies action a1 to the original and a2 // to the copy. For example, if a1 and a2 are both "send" actions, then the action will yield two // packets identical to the first. // -// fragment{protocol:offset:inOrder}(a1, a2) +// fragment{protocol:offset:inOrder}(a1, a2) // // The "fragment" action takes the original packet and fragments it, applying a1 to one of the // fragments and a2 to the other. Since both the IP and TCP layers support fragmentation, the rule @@ -94,7 +94,7 @@ // that the fragments be returned out-of-order; i.e., reversed, by specifying "False" for the // "inOrder" argument.) // -// tamper{protocol:field:mode[:newValue]}(a1) +// tamper{protocol:field:mode[:newValue]}(a1) // // The "tamper" action takes the original packet and modifies it in some fashion, depending on the // protocol, field, and mode given. There are two modes: replace and corrupt. The "replace" mode @@ -121,9 +121,9 @@ import ( // This is a convenience wrapper for strategy.ParseStrategy(). func NewStrategy(st string) (*strategy.Strategy, error) { s, err := strategy.ParseStrategy(st) - if err != nil { return nil, fmt.Errorf("failed to parse strategy: %w", err) } + return s, nil } diff --git a/geneva_test.go b/geneva_test.go index ee4b3d8..3d7f070 100644 --- a/geneva_test.go +++ b/geneva_test.go @@ -36,10 +36,10 @@ var examples = []string{ func TestNewStrategy(t *testing.T) { t.Parallel() - for _, s := range examples { + for i, s := range examples { _, err := geneva.NewStrategy(s) if err != nil { - t.Errorf("failed to parse strategy: %v", err) + t.Errorf("failed to parse strategy %d %q: %v", i, s, err) } } } diff --git a/go.mod b/go.mod index 3046c0b..824d7ef 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,10 @@ module github.com/getlantern/geneva go 1.17 require github.com/google/gopacket v1.1.19 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 29f572a..3dc724f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= @@ -12,3 +18,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/internal.go b/internal/internal.go index 110eb1a..818b41e 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -1,3 +1,4 @@ +// Package internal provides internal Geneva types and functions. package internal import ( diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index bf44e6e..3774734 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -1,4 +1,6 @@ -//nolint:godox +// Package scanner is a token scanner tailored to this library. +// +// BUG(sw) this could probably be almost completely replaced with stdlib's text/scanner. package scanner import ( @@ -7,9 +9,7 @@ import ( "unicode" ) -// Scanner is a token scanner tailored to this library. -// -// BUG(sw) this could probably be almost completely replaced with stdlib's text/scanner. +// Scanner is a token scanner. type Scanner struct { rest []rune currentPosition int @@ -20,6 +20,7 @@ func NewScanner(source string) *Scanner { return &Scanner{[]rune(source), 0} } +// Pos returns the current position of the scanner. func (l *Scanner) Pos() int { return l.currentPosition } diff --git a/strategy/strategy.go b/strategy/strategy.go index 142aa64..f125a36 100644 --- a/strategy/strategy.go +++ b/strategy/strategy.go @@ -4,8 +4,7 @@ // outbound packets. The actions trees encode what actions to take on a packet. A strategy, // conceptually, looks like this: // -// -// outbound-forest \/ inbound-forest +// outbound-forest \/ inbound-forest // // "outbound-forest" and "inbound-forest" are ordered lists of "(trigger, action tree)" pairs. The // Geneva paper calls these ordered lists "forests". The outbound and inbound forests are separated @@ -21,12 +20,12 @@ // // A real example, taken from the [original paper][geneva-paper] (pg 2202), would look like this: // -// [TCP:flags:S]- -// duplicate( -// tamper{TCP:flags:replace:SA}( -// send), -// send)-| \/ -// [TCP:flags:R]-drop-| +// [TCP:flags:S]- +// duplicate( +// tamper{TCP:flags:replace:SA}( +// send), +// send)-| \/ +// [TCP:flags:R]-drop-| // // In this example, the outbound forest would trigger on TCP packets that have just the `SYN` flag // set, and would perform a few different actions on those packets. The inbound forest would only @@ -40,9 +39,10 @@ import ( "io" "strings" + "github.com/google/gopacket" + "github.com/getlantern/geneva/actions" "github.com/getlantern/geneva/internal/scanner" - "github.com/google/gopacket" ) // Forest refers to an ordered list of (trigger, action tree) pairs.