From 8291b8b6a82f226033c79e0a279249c7f3f0e338 Mon Sep 17 00:00:00 2001 From: SticksDev Date: Thu, 11 Jan 2024 15:05:04 -0500 Subject: [PATCH] code upload --- .gitignore | 4 + go.mod | 15 ++ main.go | 513 +++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 31 ++++ 4 files changed, 563 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 main.go create mode 100644 readme.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce9f65a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +go.sum +traceroute_dynamicIngest.txt +traceroute_region.txt +vrcdn-nettest.exe \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b704744 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module sticksdev/vrcdn-nettest + +go 1.21.6 + +require ( + github.com/buger/goterm v1.0.4 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/prometheus-community/pro-bing v0.3.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.16.0 // indirect +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..7a44bc7 --- /dev/null +++ b/main.go @@ -0,0 +1,513 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "runtime" + "time" + + probing "github.com/prometheus-community/pro-bing" +) + +// Setup Color Palette +const ( + Black = "\033[1;30m" + Red = "\033[1;31m" + Green = "\033[1;32m" + Yellow = "\033[1;33m" + Blue = "\033[1;34m" + Magenta = "\033[1;35m" + Cyan = "\033[1;36m" + White = "\033[1;37m" + Reset = "\033[0m" +) + +// Regions +var regions = []string{ + "Europe", + "Americas", + "Asia", + "Australia", +} + +// URLs for each region. Key is the region name, value is a array of objects with the following keys: +// - url: URL to test +// - url_friendlyname: Friendly name for the URL. Used in the output. + +var urls = map[string][]map[string]string{ + "Europe": { + { + "url": "uk.ingest.vrcdn.live", + "url_friendlyname": "UK Ingest (England)", + }, + { + "url": "de.ingest.vrcdn.live", + "url_friendlyname": "DE Ingest (Germany)", + }, + }, + "Americas": { + { + "url": "use.ingest.vrcdn.live", + "url_friendlyname": "USE Ingest (United States East)", + }, + { + "url": "usc.ingest.vrcdn.live", + "url_friendlyname": "USC Ingest (United States Central)", + }, + { + "url": "usw.ingest.vrcdn.live", + "url_friendlyname": "USW Ingest (United States West)", + }, + }, + "Asia": { + { + "url": "jpe.ingest.vrcdn.live", + "url_friendlyname": "JPE Ingest (Japan East)", + }, + { + "url": "jpw.ingest.vrcdn.live", + "url_friendlyname": "JPW Ingest (Japan West)", + }, + }, + "Australia": { + { + "url": "au.ingest.vrcdn.live", + "url_friendlyname": "AUS Ingest (Sydney)", + }, + }, +} + +// Define our structs +type dynamicIngestResult struct { + packetsTransmitted int + packetsReceived int + packetLoss float64 + minPing float64 + maxPing float64 + avgPing float64 +} + +type regionResult struct { + region string + areaName string + packetsTransmitted int + packetsReceived int + packetLoss float64 + minPing float64 + maxPing float64 + avgPing float64 +} + +func testRegionPing(region string) []regionResult { + // Get URLs for the region. + regionUrls := urls[region] + + // Make the results array for the regions, using the regionRequest struct. + var regionResults []regionResult = make([]regionResult, len(regionUrls)) + + // Print region name. + fmt.Printf("%s[ping_test] Testing region %s...%s\n", Yellow, region, Reset) + + // Test each URL. + for _, url := range regionUrls { + fmt.Printf("[ping_test] Testing endpoint %s in region %s with sample rate 10\n", url["url_friendlyname"], region) + + pinger, err := probing.NewPinger(url["url"]) + if err != nil { + panic(err) + } + + // Listen for Ctrl-C. + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + for range c { + pinger.Stop() + fmt.Printf("\n%s[ping_test] Stopped.%s\n", Red, Reset) + os.Exit(0) + } + }() + + pinger.Count = 10 + pinger.Interval = 100 * time.Millisecond + pinger.Timeout = 1 * time.Second + + pinger.OnSend = func(pkt *probing.Packet) { + // Increment the packetsTransmitted counter. + regionResults[len(regionResults)-1].packetsTransmitted++ + } + + pinger.OnRecv = func(pkt *probing.Packet) { + // Increment the packetsReceived counter. + regionResults[len(regionResults)-1].packetsReceived++ + } + + pinger.OnFinish = func(stats *probing.Statistics) { + // Add our results to the regionResults array. + regionResults = append(regionResults, regionResult{ + region: region, + areaName: url["url_friendlyname"], + packetsTransmitted: regionResults[len(regionResults)-1].packetsTransmitted, + packetsReceived: regionResults[len(regionResults)-1].packetsReceived, + packetLoss: stats.PacketLoss, + minPing: stats.MinRtt.Seconds() * 1000, + maxPing: stats.MaxRtt.Seconds() * 1000, + avgPing: stats.AvgRtt.Seconds() * 1000, + }) + } + + // If we are in windows, we need to call pinger.SetPrivileged(true) to use ICMP. + if runtime.GOOS == "windows" { + pinger.SetPrivileged(true) + } + + err = pinger.Run() + if err != nil { + panic(err) + } + } + + return regionResults +} + +func testDynamicIngestPing() dynamicIngestResult { + // Make a new pinger for ingest.vrcdn.live. + pinger, err := probing.NewPinger("ingest.vrcdn.live") + + if err != nil { + panic(err) + } + + // Listen for Ctrl-C. + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + for range c { + pinger.Stop() + fmt.Printf("\n%s[ping_test:dynamicIngestTest] Stopped.%s\n", Red, Reset) + os.Exit(0) + } + }() + + pinger.Count = 10 + pinger.Interval = 100 * time.Millisecond + pinger.Timeout = 1 * time.Second + + // Create a empty dynamicIngestResult struct. + var dynamicIngestResultData dynamicIngestResult + + pinger.OnSend = func(pkt *probing.Packet) { + // Increment the packetsTransmitted counter. + dynamicIngestResultData.packetsTransmitted++ + } + + pinger.OnRecv = func(pkt *probing.Packet) { + // Increment the packetsReceived counter. + dynamicIngestResultData.packetsReceived++ + } + + pinger.OnFinish = func(stats *probing.Statistics) { + // Print results. + fmt.Printf("%s[ping_test:dynamicIngestTest] %d/%d packets received (%.2f%% loss), min/avg/max = %.2f/%.2f/%.2f ms.%s\n", Cyan, stats.PacketsRecv, stats.PacketsSent, stats.PacketLoss, stats.MinRtt.Seconds()*1000, stats.AvgRtt.Seconds()*1000, stats.MaxRtt.Seconds()*1000, Reset) + + // Add our results to the regionResults array. + dynamicIngestResultData = dynamicIngestResult{ + packetsTransmitted: dynamicIngestResultData.packetsTransmitted, + packetsReceived: dynamicIngestResultData.packetsReceived, + packetLoss: stats.PacketLoss, + minPing: stats.MinRtt.Seconds() * 1000, + maxPing: stats.MaxRtt.Seconds() * 1000, + avgPing: stats.AvgRtt.Seconds() * 1000, + } + } + + // If we are in windows, we need to call pinger.SetPrivileged(true) to use ICMP. + if runtime.GOOS == "windows" { + pinger.SetPrivileged(true) + } + + err = pinger.Run() + if err != nil { + panic(err) + } + + return dynamicIngestResultData +} + +func Traceroute(mode string, region string) []string { + // Figure out the binary we need to use based on the OS. + var binary string + if runtime.GOOS == "windows" { + binary = "tracert" + } else { + binary = "traceroute" + } + + // Ensure that the binary exists. + if _, err := exec.LookPath(binary); err != nil { + fmt.Printf("%s[!] Test Failed: %s %s is not installed. Please install it and try again.\n", Red, Reset, binary) + os.Exit(1) + } + + switch mode { + case "region": + // Get URLs for the region. + regionUrls := urls[region] + + // Make the results array for the regions, using the regionRequest struct. + var regionResults []string = make([]string, len(regionUrls)) + + // Print region name. + fmt.Printf("%s[traceroute_test] Testing region %s...%s\n", Yellow, region, Reset) + + // Test each URL. + for _, url := range regionUrls { + fmt.Printf("[traceroute_test] Testing endpoint %s in region %s\n", url["url_friendlyname"], region) + + // Run the traceroute command. + out, err := exec.Command(binary, url["url"]).Output() + + if err != nil { + fmt.Printf("%s[!] Test Failed: %s %s failed to run. Please install it and try again.\n", Red, Reset, binary) + os.Exit(1) + } + + // Add our results to the regionResults array. + regionResults = append(regionResults, string(out)) + } + + return regionResults + case "dynamicIngest": + // Run the traceroute command. + out, err := exec.Command(binary, "ingest.vrcdn.live").Output() + + if err != nil { + fmt.Printf("%s[!] Test Failed: %s %s failed to run. Please install it and try again.\n", Red, Reset, binary) + os.Exit(1) + } + + return []string{string(out)} + } + + return []string{} +} + +func printResults(regionResults []regionResult, dynamicIngestResultData dynamicIngestResult) { + // Find the best area in our selected region. + var bestArea regionResult = regionResult{ + avgPing: 9999999999, + } + + for _, regionResult := range regionResults { + if regionResult.areaName == "" { + continue + } + + // Check if the regionResult is better than the best area. + if regionResult.avgPing < bestArea.avgPing { + bestArea = regionResult + } else if regionResult.avgPing == bestArea.avgPing { + // If the ping is the same, check if the packet loss is better. + if regionResult.packetLoss < bestArea.packetLoss { + bestArea = regionResult + } + } else { + // If the ping is worse, skip it. + continue + } + } + + // Check if dynamic is better than the best area. + if dynamicIngestResultData.avgPing < bestArea.avgPing { + bestArea = regionResult{ // Reconstruct the bestArea struct with the dynamicIngestResultData. + region: "dynamicIngest", + areaName: "Dynamic Ingest", + packetsTransmitted: dynamicIngestResultData.packetsTransmitted, + packetsReceived: dynamicIngestResultData.packetsReceived, + packetLoss: dynamicIngestResultData.packetLoss, + minPing: dynamicIngestResultData.minPing, + maxPing: dynamicIngestResultData.maxPing, + avgPing: dynamicIngestResultData.avgPing, + } + } + + // Print the results. + // ping less then 100ms = green + // ping between 100ms and 200ms = yellow + // ping more then 200ms = red + + bestAreaPingColor := Green + if bestArea.avgPing > 100 && bestArea.avgPing < 200 { + bestAreaPingColor = Yellow + } else if bestArea.avgPing > 200 { + bestAreaPingColor = Red + } + + fmt.Printf("Your best area in your region is %s%s%s with a average ping of %.2fms.%s\n", Green, bestArea.areaName, bestAreaPingColor, bestArea.avgPing, Reset) + + // Print the results for each area in our region. + for _, regionResult := range regionResults { + // ping less then 100ms = green + // ping between 100ms and 200ms = yellow + // ping more then 200ms = red + regionResultPingColor := Green + if regionResult.avgPing > 100 && regionResult.avgPing < 200 { + regionResultPingColor = Yellow + } else if regionResult.avgPing > 200 { + regionResultPingColor = Red + } + + // If no area name, continue. + if regionResult.areaName == "" { + continue + } + + fmt.Printf("%s%s%s: %s%d/%d%s packets received (%s%.2f%%%s loss), min/avg/max = %s%.2f%s/%s%.2f%s/%s%.2f%s ms.%s\n", Cyan, regionResult.areaName, Reset, Cyan, regionResult.packetsReceived, regionResult.packetsTransmitted, Reset, Cyan, regionResult.packetLoss, Reset, Cyan, regionResult.minPing, Reset, regionResultPingColor, regionResult.avgPing, Reset, Cyan, regionResult.maxPing, Reset, Reset) + } + + // Print the results for dynamic ingest. + // ping less then 100ms = green + // ping between 100ms and 200ms = yellow + // ping more then 200ms = red + dynamicIngestResultPingColor := Green + if dynamicIngestResultData.avgPing > 100 && dynamicIngestResultData.avgPing < 200 { + dynamicIngestResultPingColor = Yellow + } else if dynamicIngestResultData.avgPing > 200 { + dynamicIngestResultPingColor = Red + } + + fmt.Printf("%sDynamic Ingest%s: %s%d/%d%s packets received (%s%.2f%%%s loss), min/avg/max = %s%.2f%s/%s%.2f%s/%s%.2f%s ms.%s\n", Cyan, Reset, Cyan, dynamicIngestResultData.packetsReceived, dynamicIngestResultData.packetsTransmitted, Reset, Cyan, dynamicIngestResultData.packetLoss, Reset, Cyan, dynamicIngestResultData.minPing, Reset, dynamicIngestResultPingColor, dynamicIngestResultData.avgPing, Reset, Cyan, dynamicIngestResultData.maxPing, Reset, Reset) + + // Print the traceroute results. + fmt.Printf("\nTraceroute results have been saved to traceroute_region.txt and traceroute_dynamicIngest.txt\n") + + // Print the disclaimer. + fmt.Printf("\nDisclaimer: This tool should not be used as a 100%% accurate way to determine your best region or a possible network issue. Please work with official VRCDN support to determine the cause of any issues you may be having.\n\n") + + // Print warning about traceroute. + fmt.Printf("%s%s%s\n", Yellow, "WARNING: Traceroute results contain SENSITIVE data. Please share with care!", Reset) +} + +func main() { + fmt.Printf("VRCDN Network Test v1.0, built with %s\n", runtime.Version()) + fmt.Print("Created by sticksdev. This tool is not affiliated with VRCDN and is community-made. See LICENSE for more information.\n\n") + + fmt.Print("This tool will test your connection to the VRCDN network. Please select your closest region.\n") + + // Print regions. + for i, region := range regions { + fmt.Printf("%d. %s\n", i+1, region) + } + + // Get user input. + var region int + var errorInput error + + // Keep asking for input until a valid region is selected. + for { + fmt.Print("Please enter the number of your region: ") + fmt.Scan(®ion, &errorInput) + + // If the input is valid, break out of the loop. + if region > 0 && region <= len(regions) { + break + } else { + fmt.Print("\nInvalid region selected. Please try again.") + + // Sleep for .5 seconds + time.Sleep(500 * time.Millisecond) + } + + if errorInput != nil { + fmt.Print(Red + "[!]" + Reset + " a error occurred while reading your input. Please try again.\n") + fmt.Print(errorInput.Error() + "\n") + + // Sleep for .5 seconds + time.Sleep(500 * time.Millisecond) + } + } + + // Test the region. + var regionResults []regionResult = testRegionPing(regions[region-1]) + + if len(regionResults) == 0 { + fmt.Printf("%s[!]%s No results were returned from the previous test. Please try again.\n", Red, Reset) + os.Exit(1) + } + + fmt.Printf("[ping_test] Running dynamic ingest test... (ingest.vrcdn.live)\n") + var dynamicIngestResultData dynamicIngestResult = testDynamicIngestPing() + + if dynamicIngestResultData == (dynamicIngestResult{}) { + fmt.Printf("%s[!]%s No results were returned from the previous test. Please try again.\n", Red, Reset) + os.Exit(1) + } + + var traceRegionResults []string = Traceroute("region", regions[region-1]) + + if len(traceRegionResults) == 0 { + fmt.Printf("%s[!]%s No results were returned from the previous test. Please try again.\n", Red, Reset) + os.Exit(1) + } + + fmt.Printf("[traceroute_test] Running dynamic ingest test... (ingest.vrcdn.live)\n") + var traceDynamicIngestResults []string = Traceroute("dynamicIngest", "") + + if len(traceDynamicIngestResults) == 0 { + fmt.Printf("%s[!]%s No results were returned from the previous test. Please try again.\n", Red, Reset) + os.Exit(1) + } + + fmt.Printf("%s[...]%s Saving traceroute results to traceroute_region.txt and traceroute_dynamicIngest.txt\n", Yellow, Reset) + + // Save the traceroute results to a file. + var traceRegionFile, traceDynamicIngestFile *os.File + + // If we have old results, delete them. + if _, err := os.Stat("traceroute_region.txt"); err == nil { + os.Remove("traceroute_region.txt") + } + + if _, err := os.Stat("traceroute_dynamicIngest.txt"); err == nil { + os.Remove("traceroute_dynamicIngest.txt") + } + + // Define error. + var err error + + traceRegionFile, err = os.OpenFile("traceroute_region.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Printf("%s[!]%s Failed to open traceroute_region.txt. Please try again.\n", Red, Reset) + os.Exit(1) + } + + traceDynamicIngestFile, err = os.OpenFile("traceroute_dynamicIngest.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Printf("%s[!]%s Failed to open traceroute_dynamicIngest.txt. Please try again.\n", Red, Reset) + os.Exit(1) + } + + // Write the results to the files. + for _, traceRegionResult := range traceRegionResults { + traceRegionFile.WriteString(traceRegionResult) + } + + for _, traceDynamicIngestResult := range traceDynamicIngestResults { + traceDynamicIngestFile.WriteString(traceDynamicIngestResult) + } + + // Close the files. + traceRegionFile.Close() + traceDynamicIngestFile.Close() + + fmt.Printf("%s[i]%s Save complete. Showing results!\n", Green, Reset) + printResults(regionResults, dynamicIngestResultData) + + // Sleep for 5 seconds. + time.Sleep(5 * time.Second) + + // Exit. + os.Exit(0) +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..fc075b4 --- /dev/null +++ b/readme.md @@ -0,0 +1,31 @@ +# VRCDN Network Test + +This is a simple tool to help you test your connection to the VRCDNs network. This can be useful to determine if you are having issues connecting to the VRCDN network. + +## How does it work? + +This tool will run two tests to test your connectivity to the VRCDN network. The first is a ping test, which will ping +each of the VRCDN ingest nodes for your area and report the results. The second is traceroute, which will show you the +path your connection takes to reach the VRCDN network. + +Trace route is a useful tool to determine if you are having issues connecting to the VRCDN network. If you see a lot of +packet loss or high latency in the trace route, this could indicate an issue with your connection to the VRCDN network. + +## How do I use it? + +You can download the latest release from the [releases page]() - or you can build it yourself. + +Once you have the binary, you can simply double click it to run it. Results will be shown for 5 seconds, and then the +program will exit. We recommend opening a command prompt and running the program from there so you can see the results +after the program exits, or use one of our pre-built scripts to run the program for you. + +A sample output from the program is shown below: + +![Sample output](https://img.sticks.ovh/9Loapvy8u.png) + +## Compiling from source + +This project uses [Go](https://golang.org/) to compile. You can download Go from [here](https://golang.org/dl/). Once +you have Go installed, you can compile the project by running `go build` in the root directory of the project. This will +create a binary called `vrcdn-nettest` in the root directory of the project that is ready to run. Alternatively, you can +run `go run .` to run the program without compiling it first.