diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d0cfafa..0110bfd 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -25,6 +25,10 @@ jobs: with: go-version: ${{ matrix.version }} + - name: Install libpcap-dev (Linux only) + if: matrix.os == 'ubuntu-latest' + run: sudo apt update && sudo apt-get install -y libpcap-dev + - name: Build run: go build -v . diff --git a/embed/linux/misc/install-libpcap.sh b/embed/linux/misc/install-libpcap.sh new file mode 100644 index 0000000..5937101 --- /dev/null +++ b/embed/linux/misc/install-libpcap.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -e + +echo "[*] Detecting distribution..." + +if [ -f /etc/debian_version ]; then + echo "[*] Debian/Ubuntu detected" + apt update + apt install -y libpcap0.8 libpcap-dev + +elif [ -f /etc/redhat-release ]; then + echo "[*] RHEL/CentOS/Fedora detected" + yum install -y libpcap libpcap-devel || dnf install -y libpcap libpcap-devel + +elif [ -f /etc/arch-release ]; then + echo "[*] Arch Linux detected" + pacman -Sy --noconfirm libpcap + +else + echo "[!] Unknown distro. Please install libpcap manually." + exit 1 +fi + +echo "[*] Verifying installation..." +ldconfig -p | grep libpcap || echo "[!] libpcap not found in ldconfig cache" + +echo "[+] libpcap installation complete." +echo "[!] Reminder: Packet capture requires root or CAP_NET_RAW capability." diff --git a/embed/windows/misc/install-npcap.ps1 b/embed/windows/misc/install-npcap.ps1 new file mode 100644 index 0000000..8b6d899 --- /dev/null +++ b/embed/windows/misc/install-npcap.ps1 @@ -0,0 +1,25 @@ +$ErrorActionPreference = "Stop" + +Write-Host "[*] Checking for Npcap installation..." +try { + Get-Service -Name "npcap" -ErrorAction Stop | Out-Null + Write-Host "[+] Npcap is already installed." + exit 0 +} catch { + Write-Host "[*] Npcap is not installed." +} + +Write-Host "[*] Downloading Npcap..." + +$npcapUrl = "https://npcap.com/dist/npcap-1.79.exe" +$installerPath = "$env:TEMP\npcap-installer.exe" + +Invoke-WebRequest -Uri $npcapUrl -OutFile $installerPath + +Write-Host "[*] Installing Npcap (requires GUI)..." + +Start-Process -FilePath $installerPath ` + -ArgumentList "/winpcap_mode=yes", "/loopback_support=yes" ` + -Wait -Verb RunAs + +Write-Host "[+] Npcap installation complete." diff --git a/go.mod b/go.mod index 772cc27..6b485c6 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( require ( github.com/charmbracelet/huh v0.6.0 github.com/charmbracelet/x/term v0.2.1 + github.com/google/gopacket v1.1.19 ) require ( @@ -52,6 +53,7 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect diff --git a/go.sum b/go.sum index f2cc772..3cef82f 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= @@ -105,15 +107,26 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +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/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220921155015-db77216a4ee9/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -126,11 +139,14 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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/misc/misc_linux.go b/misc/misc.go similarity index 91% rename from misc/misc_linux.go rename to misc/misc.go index acb399c..a79d1d7 100644 --- a/misc/misc_linux.go +++ b/misc/misc.go @@ -9,6 +9,7 @@ import ( func SetupCommand(cmd *cobra.Command) { setupToolsCommand(cmd) setupExtractCommand(cmd) + setupNetflowCommand(cmd) } func Run(cmd *cobra.Command) { diff --git a/misc/misc_windows.go b/misc/misc_windows.go deleted file mode 100644 index acb399c..0000000 --- a/misc/misc_windows.go +++ /dev/null @@ -1,18 +0,0 @@ -package misc - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -func SetupCommand(cmd *cobra.Command) { - setupToolsCommand(cmd) - setupExtractCommand(cmd) -} - -func Run(cmd *cobra.Command) { - fmt.Println("Error: No subcommand specified") - fmt.Println() - _ = cmd.Usage() -} diff --git a/misc/netflow.go b/misc/netflow.go new file mode 100644 index 0000000..c509d88 --- /dev/null +++ b/misc/netflow.go @@ -0,0 +1,400 @@ +package misc + +import ( + "bufio" + "fmt" + "log" + "net" + "os" + "sort" + "strconv" + "strings" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" + "github.com/spf13/cobra" +) + +type FlowKey struct { + SrcIP string + SrcPort int + DstIP string + DstPort int + Proto string +} + +type FilteredFlowKey struct { + IP string + Port int + Proto string +} + +type FlowStat struct { + Packets int + Bytes int +} + +type PacketSummary struct { + SrcIP string + DstIP string + SrcPort int + DstPort int + Proto string + Length int +} + +var knownPorts = map[int]string{ + 21: "FTP", + 22: "SSH", + 25: "SMTP", + 53: "DNS", + 80: "HTTP", + 88: "Kerberos", + 110: "POP3", + 123: "NTP", + 135: "RPC", + 139: "NetBIOS", + 143: "IMAP", + 161: "SNMP", + 389: "LDAP", + 443: "HTTPS", + 445: "SMB", + 465: "SMTPS", + 587: "SMTP", + 636: "LDAPS", + 993: "IMAPS", + 995: "POP3S", + 1433: "MS SQL", + 3268: "LDAP GC", + 3269: "LDAPS GC", + 3306: "SQL", + 3389: "RDP", + 5900: "VNC", + 5985: "WinRM", + 5986: "WinRM SSL", + 6001: "MAPI", + 8000: "HTTP Alt", + 8008: "HTTP Alt", + 8080: "HTTP Alt", + 8081: "HTTP Alt", + 8443: "HTTPS Alt", + 8888: "HTTP Alt", + 9389: "AD WS", +} + +var netflowFlags struct { + duration int + subnetStr string + ifaceName string + forceBackup bool +} + +func setupNetflowCommand(cmd *cobra.Command) { + netflowCmd := &cobra.Command{ + Use: "netflow", + Short: "Capture and analyze network flows", + Run: func(cmd *cobra.Command, args []string) { + analyzeNetflow(netflowFlags.duration, netflowFlags.subnetStr, netflowFlags.ifaceName, netflowFlags.forceBackup) + }, + } + + netflowCmd.Flags().IntVarP(&netflowFlags.duration, "duration", "d", 60, "Capture duration in seconds") + netflowCmd.Flags().StringVarP(&netflowFlags.subnetStr, "subnet", "s", "0.0.0.0/0", "Subnet(s) in CIDR format (comma-separated, e.g., 192.168.1.0/24,10.0.0.0/8)") + netflowCmd.Flags().StringVarP(&netflowFlags.ifaceName, "iface", "i", "", "Interface to capture on (optional)") + netflowCmd.Flags().BoolVarP(&netflowFlags.forceBackup, "backup", "b", false, "Force use of backup capture method (only on Windows)") + + cmd.AddCommand(netflowCmd) +} + +func getLocalIPs() map[string]bool { + localIPs := make(map[string]bool) + + ifaces, _ := net.Interfaces() + for _, iface := range ifaces { + addrs, _ := iface.Addrs() + for _, addr := range addrs { + ip, _, err := net.ParseCIDR(addr.String()) + if err == nil { + localIPs[ip.String()] = true + } + } + } + return localIPs +} + +func selectInterface(provided string) string { + if provided != "" { + return provided + } + + devices, err := pcap.FindAllDevs() + if err != nil { + log.Fatal(err) + } + + if len(devices) == 0 { + log.Fatal("No network devices found") + } + + fmt.Println("Available interfaces:") + for i, dev := range devices { + desc := dev.Description + if desc == "" { + desc = "(no description)" + } + fmt.Printf("[%d] %s — %s\n", i+1, dev.Name, desc) + } + + reader := bufio.NewReader(os.Stdin) + for { + fmt.Print("Enter interface number: ") + line, err := reader.ReadString('\n') + if err != nil { + log.Fatalf("Failed to read input: %v", err) + } + line = strings.TrimSpace(line) + if line == "" { + continue + } + n, err := strconv.Atoi(line) + if err != nil || n < 1 || n > len(devices) { + fmt.Println("Invalid selection, try again.") + continue + } + return devices[n-1].Name + } +} + +func parseSubnets(s string) ([]*net.IPNet, error) { + if strings.TrimSpace(s) == "" { + return nil, fmt.Errorf("empty subnet list") + } + parts := strings.Split(s, ",") + var out []*net.IPNet + for _, p := range parts { + p = strings.TrimSpace(p) + if p == "" { + continue + } + _, ipnet, err := net.ParseCIDR(p) + if err != nil { + return nil, fmt.Errorf("invalid CIDR '%s': %v", p, err) + } + out = append(out, ipnet) + } + if len(out) == 0 { + return nil, fmt.Errorf("no valid CIDRs provided") + } + return out, nil +} + +func containsAny(nets []*net.IPNet, ip net.IP) bool { + for _, n := range nets { + if n.Contains(ip) { + return true + } + } + return false +} + +func defaultPacketCapture(ifaceName string, duration int) ([]PacketSummary, error) { + iface := selectInterface(ifaceName) + fmt.Printf("Starting packet capturing on interface %s...\n", iface) + h, err := pcap.OpenLive(iface, 65535, true, pcap.BlockForever) + if err != nil { + return nil, err + } + defer h.Close() + if err := h.SetBPFFilter("tcp or udp"); err != nil { + return nil, err + } + + packetSource := gopacket.NewPacketSource(h, h.LinkType()) + out := make([]PacketSummary, 0) + timeout := time.After(time.Duration(duration) * time.Second) + + for { + select { + case packet := <-packetSource.Packets(): + if packet == nil { + continue + } + ipLayer := packet.Layer(layers.LayerTypeIPv4) + if ipLayer == nil { + continue + } + ip := ipLayer.(*layers.IPv4) + + var srcPort, dstPort int + var proto string + if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil { + tcp := tcpLayer.(*layers.TCP) + srcPort = int(tcp.SrcPort) + dstPort = int(tcp.DstPort) + proto = "TCP" + } else if udpLayer := packet.Layer(layers.LayerTypeUDP); udpLayer != nil { + udp := udpLayer.(*layers.UDP) + srcPort = int(udp.SrcPort) + dstPort = int(udp.DstPort) + proto = "UDP" + } else { + continue + } + + out = append(out, PacketSummary{ + SrcIP: ip.SrcIP.String(), + DstIP: ip.DstIP.String(), + SrcPort: srcPort, + DstPort: dstPort, + Proto: proto, + Length: len(packet.Data()), + }) + + case <-timeout: + return out, nil + } + } +} + +func filterAndAggregate(packets []PacketSummary, subnets []*net.IPNet, localIPs map[string]bool) (map[FlowKey]*FlowStat, map[FlowKey]*FlowStat) { + inbound := make(map[FlowKey]*FlowStat) + outbound := make(map[FlowKey]*FlowStat) + + for _, p := range packets { + src := net.ParseIP(p.SrcIP) + dst := net.ParseIP(p.DstIP) + if src == nil || dst == nil { + continue + } + + if !((containsAny(subnets, src) || localIPs[src.String()]) && (containsAny(subnets, dst) || localIPs[dst.String()])) { + continue + } + + key := FlowKey{ + SrcIP: p.SrcIP, + SrcPort: p.SrcPort, + DstIP: p.DstIP, + DstPort: p.DstPort, + Proto: p.Proto, + } + + if localIPs[dst.String()] { + if inbound[key] == nil { + inbound[key] = &FlowStat{} + } + inbound[key].Packets++ + inbound[key].Bytes += p.Length + } else if localIPs[src.String()] { + if outbound[key] == nil { + outbound[key] = &FlowStat{} + } + outbound[key].Packets++ + outbound[key].Bytes += p.Length + } + } + + return inbound, outbound +} + +func analyzeNetflow(duration int, subnetStr string, ifaceName string, forceBackup bool) { + if subnetStr == "" { + fmt.Println("Usage: netflow -duration 60 -subnet 192.168.1.0/24[,10.0.0.0/8] [-iface eth0]") + os.Exit(1) + } + + subnets, err := parseSubnets(subnetStr) + if err != nil { + log.Fatal("Invalid subnet(s):", err) + } + + localIPs := getLocalIPs() + + var packets []PacketSummary + + if !forceBackup && verifyPacketCapture() { + fmt.Println("Default packet capture available") + packets, err = defaultPacketCapture(ifaceName, duration) + if err != nil { + log.Fatalf("Default packet capture failed: %v", err) + } + } else { + fmt.Println("Default packet capture not available, using backup method") + packets, err = backupPacketCapture(duration) + if err != nil { + log.Fatalf("Backup packet capture failed: %v", err) + } + } + + inbound, outbound := filterAndAggregate(packets, subnets, localIPs) + + printTable("Inbound Flows", inbound) + printTable("Outbound Flows", outbound) + printFiltered("Filtered Inbound (known dest ports)", inbound, true) + printFiltered("Filtered Outbound (known dest ports)", outbound, false) +} + +func printTable(title string, flows map[FlowKey]*FlowStat) { + fmt.Println("\n====", title, "====") + + type row struct { + Key FlowKey + Stat *FlowStat + } + + var rows []row + for k, v := range flows { + rows = append(rows, row{k, v}) + } + + sort.Slice(rows, func(i, j int) bool { + return rows[i].Stat.Bytes > rows[j].Stat.Bytes + }) + + fmt.Printf("%-15s %-6s %-15s %-6s %-5s %-8s %-8s\n", + "SRC IP", "SPORT", "DST IP", "DPORT", "PROTO", "PACKETS", "BYTES") + + for _, r := range rows { + fmt.Printf("%-15s %-6d %-15s %-6d %-5s %-8d %-8d\n", + r.Key.SrcIP, + r.Key.SrcPort, + r.Key.DstIP, + r.Key.DstPort, + r.Key.Proto, + r.Stat.Packets, + r.Stat.Bytes, + ) + } +} + +func printFiltered(title string, flows map[FlowKey]*FlowStat, inbound bool) { + filteredFlows := make(map[FilteredFlowKey]*FlowStat) + for k, v := range flows { + var ip string + if inbound { + ip = k.SrcIP + } else { + ip = k.DstIP + } + if _, ok := knownPorts[k.DstPort]; ok { + fk := FilteredFlowKey{ + IP: ip, + Port: k.DstPort, + Proto: k.Proto, + } + if filteredFlows[fk] == nil { + filteredFlows[fk] = &FlowStat{} + } + filteredFlows[fk].Packets += v.Packets + filteredFlows[fk].Bytes += v.Bytes + } + } + fmt.Println("\n====", title, "====") + fmt.Printf("%-15s %-6s %-20s %-8s %-8s\n", "IP", "PORT", "SERVICE", "PACKETS", "BYTES") + for k, v := range filteredFlows { + svc := knownPorts[k.Port] + fmt.Printf("%-15s %-6d %-20s %-8d %-8d\n", k.IP, k.Port, svc, v.Packets, v.Bytes) + } +} diff --git a/misc/packet_capture_linux.go b/misc/packet_capture_linux.go new file mode 100644 index 0000000..0f3af77 --- /dev/null +++ b/misc/packet_capture_linux.go @@ -0,0 +1,16 @@ +package misc + +import ( + "fmt" + + "github.com/google/gopacket/pcap" +) + +func verifyPacketCapture() bool { + devices, err := pcap.FindAllDevs() + return err == nil && len(devices) > 0 +} + +func backupPacketCapture(duration int) ([]PacketSummary, error) { + return nil, fmt.Errorf("pktmon is only available on Windows") +} diff --git a/misc/packet_capture_windows.go b/misc/packet_capture_windows.go new file mode 100644 index 0000000..cd85d3e --- /dev/null +++ b/misc/packet_capture_windows.go @@ -0,0 +1,102 @@ +package misc + +import ( + "fmt" + "io" + "os" + "os/exec" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" + "github.com/google/gopacket/pcapgo" +) + +func verifyPacketCapture() bool { + devices, err := pcap.FindAllDevs() + return err == nil && len(devices) > 0 +} + +func backupPacketCapture(duration int) ([]PacketSummary, error) { + fmt.Println("Starting pktmon capture...") + + etl := "PktMon.etl" + cmdStart := exec.Command("pktmon", "start", "--capture", "--pkt-size", "0", "--file-size", "4096", "--file-name", etl) + cmdStart.Stdout = io.Discard + cmdStart.Stderr = io.Discard + if err := cmdStart.Run(); err != nil { + return nil, fmt.Errorf("Failed to start pktmon capture: %v", err) + } + + time.Sleep(time.Duration(duration) * time.Second) + + cmdStop := exec.Command("pktmon", "stop") + cmdStop.Stdout = io.Discard + cmdStop.Stderr = io.Discard + if err := cmdStop.Run(); err != nil { + return nil, fmt.Errorf("Failed to stop pktmon capture: %v", err) + } + + pcapOut := "pktmon.pcap" + cmdFmt := exec.Command("pktmon", "etl2pcap", etl, "-o", pcapOut) + cmdFmt.Stdout = io.Discard + cmdFmt.Stderr = io.Discard + if err := cmdFmt.Run(); err != nil { + return nil, fmt.Errorf("Failed to format pktmon ETL to pcap: %v", err) + } + defer os.Remove(etl) + defer os.Remove(pcapOut) + + f, err := os.Open(pcapOut) + if err != nil { + return nil, fmt.Errorf("Failed to open pktmon pcap file: %v", err) + } + defer f.Close() + + reader, err := pcapgo.NewNgReader(f, pcapgo.DefaultNgReaderOptions) + if err != nil { + return nil, fmt.Errorf("Failed to create pcapng reader: %v", err) + } + ps := gopacket.NewPacketSource(reader, reader.LinkType()) + + out := make([]PacketSummary, 0) + + for packet := range ps.Packets() { + if packet == nil { + continue + } + ipLayer := packet.Layer(layers.LayerTypeIPv4) + if ipLayer == nil { + continue + } + ip := ipLayer.(*layers.IPv4) + + var srcPort, dstPort int + var proto string + if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil { + tcp := tcpLayer.(*layers.TCP) + srcPort = int(tcp.SrcPort) + dstPort = int(tcp.DstPort) + proto = "TCP" + } else if udpLayer := packet.Layer(layers.LayerTypeUDP); udpLayer != nil { + udp := udpLayer.(*layers.UDP) + srcPort = int(udp.SrcPort) + dstPort = int(udp.DstPort) + proto = "UDP" + } else { + continue + } + + out = append(out, PacketSummary{ + SrcIP: ip.SrcIP.String(), + DstIP: ip.DstIP.String(), + SrcPort: srcPort, + DstPort: dstPort, + Proto: proto, + Length: len(packet.Data()), + }) + } + + return out, nil +}