├── images └── packets-by-protocol.png ├── pkg ├── model │ ├── ping.go │ └── packets.go ├── db │ ├── connection.go │ └── operations.go └── metrics │ ├── ping.go │ └── packets.go ├── docker-compose.yml ├── go.mod ├── .gitignore ├── run.sh ├── cmd └── main.go ├── README.md └── go.sum /images/packets-by-protocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iliasgal/network-monitor/HEAD/images/packets-by-protocol.png -------------------------------------------------------------------------------- /pkg/model/ping.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type PingStats struct { 4 | AvgLatency float64 5 | Jitter float64 6 | PacketLoss float64 7 | } 8 | -------------------------------------------------------------------------------- /pkg/model/packets.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type PacketInfo struct { 4 | PacketType string 5 | SrcIP string 6 | DstIP string 7 | SrcPort string 8 | DstPort string 9 | Size int 10 | } 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | influxdb: 5 | image: influxdb:2.1 6 | ports: 7 | - "8086:8086" 8 | volumes: 9 | - influxdb_data:/var/lib/influxdb2 10 | 11 | volumes: 12 | influxdb_data: 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iliasgal/network-monitor 2 | 3 | go 1.21.1 4 | 5 | require ( 6 | github.com/google/gopacket v1.1.19 7 | github.com/influxdata/influxdb-client-go/v2 v2.13.0 8 | ) 9 | 10 | require ( 11 | github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect 12 | github.com/google/uuid v1.3.1 // indirect 13 | github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect 14 | github.com/oapi-codegen/runtime v1.0.0 // indirect 15 | golang.org/x/net v0.17.0 // indirect 16 | golang.org/x/sys v0.13.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/db/connection.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "os" 5 | 6 | influxdb2 "github.com/influxdata/influxdb-client-go/v2" 7 | ) 8 | 9 | var ( 10 | influxToken = os.Getenv("INFLUXDB_ADMIN_TOKEN") 11 | influxBucket = os.Getenv("INFLUXDB_BUCKET") 12 | influxOrg = os.Getenv("INFLUXDB_ORG") 13 | influxURL = os.Getenv("INFLUXDB_URL") 14 | ) 15 | 16 | var influxClient influxdb2.Client 17 | 18 | func init() { 19 | influxClient = influxdb2.NewClient(influxURL, influxToken) 20 | } 21 | 22 | func CloseInfluxDBClient() { 23 | influxClient.Close() 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Go binaries and executables 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.bin 8 | *.o 9 | *.a 10 | *.out 11 | 12 | # Go build directories 13 | /bin/ 14 | 15 | # Dependency directories (remove the vendor directory if your project uses it) 16 | /vendor/ 17 | 18 | # Error logs 19 | *.log 20 | 21 | # Output of the go coverage tool 22 | *.cover 23 | 24 | # External tool files (e.g., "node_modules" if you're using Node.js in conjunction with Go) 25 | node_modules/ 26 | 27 | # JetBrains IDEs (e.g., GoLand, IntelliJ) 28 | .idea/ 29 | *.iml 30 | 31 | # Visual Studio Code 32 | .vscode/ 33 | *.code-workspace 34 | 35 | # Eclipse 36 | .classpath 37 | .project 38 | .settings/ 39 | 40 | # Vim 41 | *.swp 42 | *.swo 43 | 44 | # Emacs 45 | *~ 46 | 47 | # macOS specific 48 | .DS_Store 49 | 50 | # Windows specific 51 | Thumbs.db 52 | Desktop.ini 53 | 54 | # Linux specific 55 | .directory 56 | 57 | # Other files and directories to ignore 58 | tmp/ 59 | temp/ 60 | .env 61 | local.Dockerfile -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Prompt for sudo password upfront 4 | echo "Please enter your sudo password to continue..." 5 | sudo -v 6 | 7 | # Keep-alive: update existing sudo time stamp if set, otherwise do nothing. 8 | while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null & 9 | 10 | 11 | # Load environment variables from .env file 12 | if [ -f ".env" ]; then 13 | echo "Loading environment variables from .env file..." 14 | set -a # Automatically export all variables 15 | source .env 16 | set +a 17 | else 18 | echo "No .env file found" 19 | fi 20 | 21 | # Define the temporary directory for the build 22 | BUILD_DIR=$(mktemp -d) 23 | 24 | # Ensure the temporary directory is removed on script exit 25 | trap "rm -rf $BUILD_DIR" EXIT 26 | 27 | cd cmd 28 | 29 | echo "Building Go program in $BUILD_DIR..." 30 | go build -o "$BUILD_DIR/main" 31 | echo "Go program built successfully!" 32 | 33 | echo "Starting InfluxDB container..." 34 | docker-compose up -d influxdb 35 | 36 | # Wait for InfluxDB to start 37 | sleep 5 38 | 39 | echo "Running the network monitor program..." 40 | sudo -E "$BUILD_DIR/main" 41 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/iliasgal/network-monitor/pkg/db" 11 | "github.com/iliasgal/network-monitor/pkg/metrics" 12 | ) 13 | 14 | func main() { 15 | // Create a channel to receive errors 16 | errorChan := make(chan error) 17 | 18 | // Setup channel to listen for termination signals 19 | signals := make(chan os.Signal, 1) 20 | // Notify for SIGINT and SIGTERM 21 | signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) 22 | 23 | // Start packet capture in its own goroutine 24 | go metrics.PacketCapture() 25 | 26 | host := "google.com" 27 | count := 4 28 | ticker := time.NewTicker(5 * time.Second) // Ping every 5 seconds 29 | 30 | for { 31 | select { 32 | case <-ticker.C: 33 | pingStats, err := metrics.PingHost(host, count) 34 | if err != nil { 35 | log.Fatal(err) 36 | return 37 | } 38 | db.WritePingMetricsToInfluxDB(pingStats, errorChan) 39 | case info := <-metrics.PacketInfoChan: 40 | db.WritePacketInfoToInfluxDB(info, errorChan) 41 | case err := <-errorChan: 42 | log.Printf("Error occurred: %v", err) 43 | case <-signals: 44 | log.Println("Termination signal received, closing resources.") 45 | db.CloseInfluxDBClient() 46 | ticker.Stop() 47 | return 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pkg/db/operations.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/iliasgal/network-monitor/pkg/model" 7 | influxdb2 "github.com/influxdata/influxdb-client-go/v2" 8 | ) 9 | 10 | func WritePingMetricsToInfluxDB(stats *model.PingStats, errorChan chan<- error) { 11 | // Get non-blocking write client 12 | writeAPI := influxClient.WriteAPI(influxOrg, influxBucket) 13 | 14 | p := influxdb2.NewPointWithMeasurement("ping_metrics"). 15 | AddField("avg_latency_ms", stats.AvgLatency). 16 | AddField("jitter_ms", stats.Jitter). 17 | AddField("packet_loss", stats.PacketLoss). 18 | SetTime(time.Now()) 19 | 20 | // write point asynchronously 21 | writeAPI.WritePoint(p) 22 | 23 | // Flush writes 24 | writeAPI.Flush() 25 | 26 | go func() { 27 | for err := range writeAPI.Errors() { 28 | errorChan <- err 29 | } 30 | }() 31 | } 32 | 33 | func WritePacketInfoToInfluxDB(info *model.PacketInfo, errorChan chan<- error) { 34 | // Get a non-blocking write client 35 | writeAPI := influxClient.WriteAPI(influxOrg, influxBucket) 36 | 37 | p := influxdb2.NewPointWithMeasurement("network_traffic"). 38 | AddTag("packet_type", info.PacketType). 39 | AddTag("src_ip", info.SrcIP). 40 | AddTag("dst_ip", info.DstIP). 41 | AddTag("src_port", info.SrcPort). 42 | AddTag("dst_port", info.DstPort). 43 | AddField("packet_size", info.Size). 44 | SetTime(time.Now()) 45 | 46 | // Write the point asynchronously 47 | writeAPI.WritePoint(p) 48 | 49 | // Ensure all writes are sent 50 | writeAPI.Flush() 51 | 52 | go func() { 53 | for err := range writeAPI.Errors() { 54 | errorChan <- err 55 | } 56 | }() 57 | } 58 | -------------------------------------------------------------------------------- /pkg/metrics/ping.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "regexp" 7 | "strconv" 8 | 9 | "github.com/iliasgal/network-monitor/pkg/model" 10 | ) 11 | 12 | // PingHost executes the ping command and calculates average latency, packet loss, and jitter. 13 | func PingHost(host string, count int) (*model.PingStats, error) { 14 | cmd := exec.Command("ping", "-c", fmt.Sprint(count), host) 15 | output, err := cmd.CombinedOutput() 16 | if err != nil { 17 | return nil, fmt.Errorf("failed to execute ping: %s", err) 18 | } 19 | 20 | latencies, err := extractLatencies(string(output)) 21 | if err != nil { 22 | return nil, fmt.Errorf("failed to extract latencies: %s", err) 23 | } 24 | 25 | avgLatency := calculateAverageLatency(latencies) 26 | jitter := calculateJitter(latencies) 27 | packetLoss, err := calculatePacketLoss(string(output)) 28 | if err != nil { 29 | return nil, fmt.Errorf("failed to calculate packet loss: %s", err) 30 | } 31 | 32 | // write ping metrics to struct 33 | pingMetrics := model.PingStats{ 34 | AvgLatency: avgLatency, 35 | Jitter: jitter, 36 | PacketLoss: packetLoss, 37 | } 38 | 39 | return &pingMetrics, nil 40 | } 41 | 42 | func extractLatencies(output string) ([]float64, error) { 43 | // Regex to extract latencies from ping output 44 | latencyRegex := regexp.MustCompile(`time=(\d+\.\d+)`) 45 | matches := latencyRegex.FindAllStringSubmatch(output, -1) 46 | 47 | var latencies []float64 48 | for _, match := range matches { 49 | latency, err := strconv.ParseFloat(match[1], 64) 50 | if err != nil { 51 | return nil, err 52 | } 53 | latencies = append(latencies, latency) 54 | } 55 | return latencies, nil 56 | } 57 | 58 | func calculateAverageLatency(latencies []float64) float64 { 59 | var totalLatency float64 60 | for _, latency := range latencies { 61 | totalLatency += latency 62 | } 63 | return totalLatency / float64(len(latencies)) 64 | } 65 | 66 | func calculateJitter(latencies []float64) float64 { 67 | // Jitter requires at least two latencies 68 | if len(latencies) < 2 { 69 | return 0.0 70 | } 71 | 72 | // Jitter is the average of the absolute difference between latencies 73 | var totalJitter float64 74 | for i := 1; i < len(latencies); i++ { 75 | diff := latencies[i] - latencies[i-1] 76 | if diff < 0 { 77 | diff = -diff 78 | } 79 | totalJitter += diff 80 | } 81 | return totalJitter / float64(len(latencies)-1) 82 | } 83 | 84 | func calculatePacketLoss(output string) (float64, error) { 85 | // Regex to extract packet loss percentage from ping output 86 | packetLossRegex := regexp.MustCompile(`(\d+)% packet loss`) 87 | match := packetLossRegex.FindStringSubmatch(output) 88 | 89 | if len(match) > 1 { 90 | packetLoss, err := strconv.ParseFloat(match[1], 64) 91 | if err != nil { 92 | return 0, fmt.Errorf("failed to parse packet loss value: %s", err) 93 | } 94 | return packetLoss, nil 95 | } 96 | 97 | return 0, nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/metrics/packets.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/google/gopacket" 8 | "github.com/google/gopacket/layers" 9 | "github.com/google/gopacket/pcap" 10 | "github.com/iliasgal/network-monitor/pkg/model" 11 | ) 12 | 13 | // LayerProcessor defines an interface for processing packet layers 14 | type LayerProcessor interface { 15 | Process(packet gopacket.Packet) *model.PacketInfo 16 | } 17 | 18 | // IPv4Processor processes IPv4 layers 19 | type IPv4Processor struct{} 20 | 21 | func (p IPv4Processor) Process(packet gopacket.Packet) *model.PacketInfo { 22 | ipLayer := packet.Layer(layers.LayerTypeIPv4) 23 | if ipLayer == nil { 24 | return nil 25 | } 26 | 27 | ip, _ := ipLayer.(*layers.IPv4) 28 | return &model.PacketInfo{ 29 | PacketType: "IPv4", 30 | SrcIP: ip.SrcIP.String(), 31 | DstIP: ip.DstIP.String(), 32 | Size: len(packet.Data()), 33 | } 34 | } 35 | 36 | // TCPProcessor processes TCP layers 37 | type TCPProcessor struct{} 38 | 39 | func (p TCPProcessor) Process(packet gopacket.Packet) *model.PacketInfo { 40 | tcpLayer := packet.Layer(layers.LayerTypeTCP) 41 | if tcpLayer == nil { 42 | return nil 43 | } 44 | 45 | tcp, _ := tcpLayer.(*layers.TCP) 46 | return &model.PacketInfo{ 47 | PacketType: "TCP", 48 | SrcPort: tcp.SrcPort.String(), 49 | DstPort: tcp.DstPort.String(), 50 | Size: len(packet.Data()), 51 | } 52 | } 53 | 54 | // UDPProcessor processes UDP layers 55 | type UDPProcessor struct{} 56 | 57 | func (p UDPProcessor) Process(packet gopacket.Packet) *model.PacketInfo { 58 | udpLayer := packet.Layer(layers.LayerTypeUDP) 59 | if udpLayer == nil { 60 | return nil 61 | } 62 | udp, _ := udpLayer.(*layers.UDP) 63 | return &model.PacketInfo{ 64 | PacketType: "UDP", 65 | SrcPort: udp.SrcPort.String(), 66 | DstPort: udp.DstPort.String(), 67 | Size: len(packet.Data()), 68 | } 69 | } 70 | 71 | // PacketInfoChan is a buffered channel for storing packet info 72 | var PacketInfoChan = make(chan *model.PacketInfo, 100) 73 | 74 | func PacketCapture() { 75 | device := "en0" // Change this to your network interface name 76 | var snapshotLen int32 = 1024 77 | var promiscuous bool = false 78 | var timeout time.Duration = 30 * time.Second 79 | 80 | // Open the device for capturing 81 | handle, err := pcap.OpenLive(device, snapshotLen, promiscuous, timeout) 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | defer handle.Close() 86 | 87 | packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) 88 | processors := []LayerProcessor{ 89 | IPv4Processor{}, 90 | TCPProcessor{}, 91 | UDPProcessor{}, 92 | } 93 | 94 | for packet := range packetSource.Packets() { 95 | for _, processor := range processors { 96 | info := processor.Process(packet) 97 | if info != nil { 98 | PacketInfoChan <- info 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Network Monitor 2 | 3 | ## Overview 4 | 5 | Network Monitor is a Go-based tool for capturing and analysing network traffic, designed for efficiency and compatibility with various platforms, including PCs or Raspberry Pi. It utilises InfluxDB for storing network metrics, providing insights into network performance and health. 6 | 7 | ## Features 8 | 9 | - **Comprehensive Packet Capture**: Logs every packet, detailing size, source, destination, and protocol. 10 | - **Performance Metrics**: Measures latency, jitter, and packet loss to assess network health. 11 | - **Data Aggregation and Visualisation**: Stores metrics in InfluxDB, supporting complex analyses and visualisations with tools like Grafana or directly through the InfluxDB dashboards themselves. 12 | 13 | ![network-monitor.png](images/packets-by-protocol.png) 14 | 15 | ## Deployment 16 | 17 | Network Monitor can be run on any computer or a Raspberry Pi, requiring minimal setup. 18 | 19 | ### Prerequisites 20 | 21 | - A computer or Raspberry Pi with network access. 22 | - Docker installed for running InfluxDB in a container. 23 | - Go installed for running the Network Monitor application. 24 | 25 | ### Setting Up InfluxDB 26 | 27 | InfluxDB is used to store and manage the network metrics captured by Network Monitor. It runs in a Docker container, simplifying deployment and management. 28 | 29 | 1. Ensure Docker is installed and running on your system. 30 | 2. Use the provided `docker-compose.yml` file to launch InfluxDB: 31 | 32 | ```bash 33 | docker-compose up -d influxdb 34 | ``` 35 | 36 | ### Configuration 37 | 38 | Before running Network Monitor, configure the connection to InfluxDB through an environment file (`.env`), which is not included in the repository for security reasons. Create this file with the following contents: 39 | 40 | ```plaintext 41 | INFLUXDB_URL=YourInfluxDBURL #e.g. http://localhost:8086 42 | INFLUXDB_ADMIN_TOKEN=YourInfluxDBToken 43 | INFLUXDB_ORG=YourOrganisation 44 | INFLUXDB_BUCKET=YourBucket 45 | ``` 46 | 47 | Replace `YourInfluxDBToken`, `YourOrganisation`, and `YourBucket` with your actual InfluxDB credentials and configuration. 48 | 49 | ## Running Network Monitor 50 | 51 | Use the provided script `run.sh` to build and run the Network Monitor application. Ensure the `.env` file is in the same directory as `run.sh` for the application to read and use the InfluxDB configuration. 52 | 53 | ```bash 54 | ./run.sh 55 | ``` 56 | 57 | This script builds the Go application and starts capturing network metrics, sending them to InfluxDB. 58 | 59 | ## Conclusion 60 | 61 | Network Monitor, with its detailed packet capturing and integration with InfluxDB, offers a powerful and flexible solution for monitoring network performance and health. Its ability to capture all packets, along with their size and protocol, opens up possibilities for in-depth network analysis and visualisation. Whether you're looking to troubleshoot network issues, optimise performance, or simply gain a better understanding of your network traffic, Network Monitor provides the tools and data you need to achieve your goals. 62 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= 2 | github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= 3 | github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= 4 | github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 9 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 10 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 11 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 12 | github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM= 13 | github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4= 14 | github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= 15 | github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= 16 | github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= 17 | github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= 18 | github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= 22 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 23 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 24 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 25 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 26 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 27 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 28 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 29 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 30 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 31 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 32 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 33 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 34 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 35 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 36 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 38 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 40 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 41 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | --------------------------------------------------------------------------------