plc4go/tools/plc4xpcapanalyzer/internal/analyzer/analyzer.go (241 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 * * https://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 analyzer import ( "bytes" "context" "encoding/hex" "fmt" "io" "net" "os" "time" "github.com/apache/plc4x/plc4go/spi" "github.com/gopacket/gopacket" "github.com/gopacket/gopacket/layers" "github.com/k0kubun/go-ansi" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/schollz/progressbar/v3" "github.com/apache/plc4x-extras/plc4go/tools/plc4xpcapanalyzer/config" "github.com/apache/plc4x-extras/plc4go/tools/plc4xpcapanalyzer/internal/bacnetanalyzer" "github.com/apache/plc4x-extras/plc4go/tools/plc4xpcapanalyzer/internal/cbusanalyzer" "github.com/apache/plc4x-extras/plc4go/tools/plc4xpcapanalyzer/internal/common" "github.com/apache/plc4x-extras/plc4go/tools/plc4xpcapanalyzer/internal/pcaphandler" ) func Analyze(pcapFile, protocolType string) error { return AnalyzeWithOutput(pcapFile, protocolType, os.Stdout, os.Stderr) } func AnalyzeWithOutput(pcapFile, protocolType string, stdout, stderr io.Writer) error { return AnalyzeWithOutputAndCallback(context.TODO(), pcapFile, protocolType, stdout, stderr, nil) } func AnalyzeWithOutputAndCallback(ctx context.Context, pcapFile, protocolType string, stdout, stderr io.Writer, messageCallback func(parsed spi.Message)) error { var filterExpression = config.AnalyzeConfigInstance.Filter if filterExpression != "" { log.Info().Str("filterExpression", filterExpression).Msg("Using global filter") } var mapPackets = func(in chan gopacket.Packet, packetInformationCreator func(packet gopacket.Packet) common.PacketInformation) chan gopacket.Packet { return in } var packageParse func(common.PacketInformation, []byte) (spi.Message, error) var serializePackage func(spi.Message) ([]byte, error) var prettyPrint = func(item spi.Message) { _, _ = fmt.Fprintf(stdout, "%v\n", item) } var byteOutput = hex.Dump switch protocolType { case "bacnetip": if !config.AnalyzeConfigInstance.NoFilter { if config.AnalyzeConfigInstance.Filter == "" && config.BacnetConfigInstance.BacnetFilter != "" { log.Debug().Str("filter", config.BacnetConfigInstance.Filter).Msg("Setting bacnet filter") filterExpression = config.BacnetConfigInstance.BacnetFilter } } else { log.Info().Msg("All filtering disabled") } packageParse = bacnetanalyzer.PackageParse serializePackage = bacnetanalyzer.SerializePackage case "c-bus": if !config.AnalyzeConfigInstance.NoFilter { if config.AnalyzeConfigInstance.Filter == "" && config.CBusConfigInstance.CBusFilter != "" { log.Debug().Str("filter", config.CBusConfigInstance.Filter).Msg("Setting cbus filter") filterExpression = config.CBusConfigInstance.CBusFilter } } else { log.Info().Msg("All filtering disabled") } analyzer := cbusanalyzer.Analyzer{Client: net.ParseIP(config.AnalyzeConfigInstance.Client)} analyzer.Init() packageParse = analyzer.PackageParse serializePackage = analyzer.SerializePackage mapPackets = analyzer.MapPackets if !config.AnalyzeConfigInstance.NoCustomMapping { byteOutput = analyzer.ByteOutput } else { log.Info().Msg("Custom mapping disabled") } default: return errors.Errorf("Unsupported protocol type %s", protocolType) } log.Info(). Str("pcapFile", pcapFile). Str("protocolType", protocolType). Str("filterExpression", filterExpression). Msg("Analyzing pcap file pcapFile with protocolType and filter filterExpression now") handle, numberOfPackage, timestampToIndexMap, err := pcaphandler.GetIndexedPcapHandle(pcapFile, filterExpression) if err != nil { return errors.Wrap(err, "Error getting handle") } log.Info().Int("numberOfPackage", numberOfPackage).Msg("Starting to analyze numberOfPackage packages") defer handle.Close() log.Debug().Interface("handle", handle).Int("numberOfPackage", numberOfPackage).Msg("got handle") source := pcaphandler.GetPacketSource(handle) bar := progressbar.NewOptions(numberOfPackage, progressbar.OptionSetWriter(ansi.NewAnsiStderr()), progressbar.OptionSetVisibility(!config.RootConfigInstance.HideProgressBar), progressbar.OptionEnableColorCodes(true), progressbar.OptionShowBytes(false), progressbar.OptionSetWidth(15), progressbar.OptionSetDescription("[cyan][1/3][reset] Analyzing packages..."), progressbar.OptionSetTheme(progressbar.Theme{ Saucer: "[green]=[reset]", SaucerHead: "[green]>[reset]", SaucerPadding: " ", BarStart: "[", BarEnd: "]", })) currentPackageNum := uint(0) parseFails := 0 serializeFails := 0 compareFails := 0 for packet := range mapPackets(source.Packets(), func(packet gopacket.Packet) common.PacketInformation { return createPacketInformation(pcapFile, packet, timestampToIndexMap) }) { if err := ctx.Err(); err != nil { log.Info().Err(err).Uint("currentPackageNum", currentPackageNum).Msg("Aborted after currentPackageNum packages") break } currentPackageNum++ if currentPackageNum < config.AnalyzeConfigInstance.StartPackageNumber { log.Debug(). Uint("currentPackageNum", currentPackageNum). Uint("startPackageNum", config.AnalyzeConfigInstance.StartPackageNumber). Msg("Skipping package number currentPackageNum (till no. startPackageNum)") continue } if currentPackageNum > config.AnalyzeConfigInstance.PackageNumberLimit { log.Warn(). Uint("PackageNumberLimit", config.AnalyzeConfigInstance.PackageNumberLimit). Msg("Aborting reading packages because we hit the limit of packageNumberLimit") break } if packet == nil { log.Debug().Msg("Done reading packages. (nil returned)") break } if err := bar.Add(1); err != nil { log.Warn().Err(err).Msg("Error updating progressBar") } packetInformation := createPacketInformation(pcapFile, packet, timestampToIndexMap) realPacketNumber := packetInformation.PacketNumber if filteredPackage, ok := packet.(common.FilteredPackage); ok { log.Info().Err(filteredPackage.FilterReason()). Int("realPacketNumber", realPacketNumber).Msg("No.[realPacketNumber] was filtered") continue } applicationLayer := packet.ApplicationLayer() if applicationLayer == nil { log.Info().Stringer("packetInformation", packetInformation). Int("realPacketNumber", realPacketNumber). Msg("No.[realPacketNumber] No application layer") continue } payload := applicationLayer.Payload() if parsed, err := packageParse(packetInformation, payload); err != nil { switch { case errors.Is(err, common.ErrUnterminatedPackage): log.Info().Stringer("packetInformation", packetInformation). Int("realPacketNumber", realPacketNumber). Msg("No.[realPacketNumber] is unterminated") case errors.Is(err, common.ErrEmptyPackage): log.Info().Stringer("packetInformation", packetInformation). Int("realPacketNumber", realPacketNumber). Msg("No.[realPacketNumber] is empty") case errors.Is(err, common.ErrEcho): log.Info().Stringer("packetInformation", packetInformation). Int("realPacketNumber", realPacketNumber). Msg("No.[realPacketNumber] is echo") default: parseFails++ // TODO: write report to xml or something log.Error(). Err(err). Stringer("packetInformation", packetInformation). Int("realPacketNumber", realPacketNumber). Str("byteOutput", byteOutput(payload)). Msg("No.[realPacketNumber] Error parsing package.") } continue } else { if messageCallback != nil { messageCallback(parsed) } log.Info(). Stringer("packetInformation", packetInformation). Int("realPacketNumber", realPacketNumber). Msg("No.[realPacketNumber] Parsed") if config.AnalyzeConfigInstance.Verbosity > 1 { prettyPrint(parsed) } if config.AnalyzeConfigInstance.OnlyParse { log.Trace().Msg("only parsing") continue } serializedBytes, err := serializePackage(parsed) if err != nil { serializeFails++ // TODO: write report to xml or something log.Warn(). Err(err). Stringer("packetInformation", packetInformation). Int("realPacketNumber", realPacketNumber). Msg("No.[realPacketNumber] Error serializing") continue } if config.AnalyzeConfigInstance.NoBytesCompare { log.Trace().Msg("not comparing bytes") continue } if compareResult := bytes.Compare(payload, serializedBytes); compareResult != 0 { compareFails++ // TODO: write report to xml or something log.Warn(). Stringer("packetInformation", packetInformation). Int("realPacketNumber", realPacketNumber). Str("byteOutputPayload", byteOutput(payload)). Str("byteSerializedBytes", byteOutput(serializedBytes)). Msg("No.[realPacketNumber] Bytes don't match.") if config.AnalyzeConfigInstance.Verbosity > 0 { _, _ = fmt.Fprintf(stdout, "Original bytes\n%s\n%s\n", hex.Dump(payload), hex.Dump(serializedBytes)) } } } } log.Info(). Uint("currentPackageNum", currentPackageNum). Int("numberOfPackage", numberOfPackage). Int("parseFails", parseFails). Int("serializeFails", serializeFails). Int("compareFails", compareFails). Msg("Done evaluating currentPackageNum of numberOfPackage packages (parseFails failed to parse, serializeFails failed to serialize and compareFails failed in byte comparison)") return nil } func createPacketInformation(pcapFile string, packet gopacket.Packet, timestampToIndexMap map[time.Time]int) common.PacketInformation { packetTimestamp := packet.Metadata().Timestamp realPacketNumber := timestampToIndexMap[packetTimestamp] description := fmt.Sprintf("No.[%d] timestamp: %v, %s", realPacketNumber, packetTimestamp, pcapFile) packetInformation := common.PacketInformation{ PacketNumber: realPacketNumber, PacketTimestamp: packetTimestamp, Description: description, } if networkLayer, ok := packet.NetworkLayer().(*layers.IPv4); ok { packetInformation.SrcIp = networkLayer.SrcIP packetInformation.DstIp = networkLayer.DstIP } return packetInformation }