2025-03-21 11:49:32 +01:00

176 lines
4.2 KiB
Go

package util
import (
"errors"
"fmt"
"net/netip"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"tailscale.com/util/cmpver"
)
func TailscaleVersionNewerOrEqual(minimum, toCheck string) bool {
if cmpver.Compare(minimum, toCheck) <= 0 ||
toCheck == "unstable" ||
toCheck == "head" {
return true
}
return false
}
// ParseLoginURLFromCLILogin parses the output of the tailscale up command to extract the login URL.
// It returns an error if not exactly one URL is found.
func ParseLoginURLFromCLILogin(output string) (*url.URL, error) {
lines := strings.Split(output, "\n")
var urlStr string
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "http://") || strings.HasPrefix(line, "https://") {
if urlStr != "" {
return nil, fmt.Errorf("multiple URLs found: %s and %s", urlStr, line)
}
urlStr = line
}
}
if urlStr == "" {
return nil, errors.New("no URL found")
}
loginURL, err := url.Parse(urlStr)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
return loginURL, nil
}
type TraceroutePath struct {
// Hop is the current jump in the total traceroute.
Hop int
// Hostname is the resolved hostname or IP address identifying the jump
Hostname string
// IP is the IP address of the jump
IP netip.Addr
// Latencies is a list of the latencies for this jump
Latencies []time.Duration
}
type Traceroute struct {
// Hostname is the resolved hostname or IP address identifying the target
Hostname string
// IP is the IP address of the target
IP netip.Addr
// Route is the path taken to reach the target if successful. The list is ordered by the path taken.
Route []TraceroutePath
// Success indicates if the traceroute was successful.
Success bool
// Err contains an error if the traceroute was not successful.
Err error
}
// ParseTraceroute parses the output of the traceroute command and returns a Traceroute struct
func ParseTraceroute(output string) (Traceroute, error) {
lines := strings.Split(strings.TrimSpace(output), "\n")
if len(lines) < 1 {
return Traceroute{}, errors.New("empty traceroute output")
}
// Parse the header line
headerRegex := regexp.MustCompile(`traceroute to ([^ ]+) \(([^)]+)\)`)
headerMatches := headerRegex.FindStringSubmatch(lines[0])
if len(headerMatches) != 3 {
return Traceroute{}, fmt.Errorf("parsing traceroute header: %s", lines[0])
}
hostname := headerMatches[1]
ipStr := headerMatches[2]
ip, err := netip.ParseAddr(ipStr)
if err != nil {
return Traceroute{}, fmt.Errorf("parsing IP address %s: %w", ipStr, err)
}
result := Traceroute{
Hostname: hostname,
IP: ip,
Route: []TraceroutePath{},
Success: false,
}
// Parse each hop line
hopRegex := regexp.MustCompile(`^\s*(\d+)\s+(?:([^ ]+) \(([^)]+)\)|(\*))(?:\s+(\d+\.\d+) ms)?(?:\s+(\d+\.\d+) ms)?(?:\s+(\d+\.\d+) ms)?`)
for i := 1; i < len(lines); i++ {
matches := hopRegex.FindStringSubmatch(lines[i])
if len(matches) == 0 {
continue
}
hop, err := strconv.Atoi(matches[1])
if err != nil {
return Traceroute{}, fmt.Errorf("parsing hop number: %w", err)
}
var hopHostname string
var hopIP netip.Addr
var latencies []time.Duration
// Handle hostname and IP
if matches[2] != "" && matches[3] != "" {
hopHostname = matches[2]
hopIP, err = netip.ParseAddr(matches[3])
if err != nil {
return Traceroute{}, fmt.Errorf("parsing hop IP address %s: %w", matches[3], err)
}
} else if matches[4] == "*" {
hopHostname = "*"
// No IP for timeouts
}
// Parse latencies
for j := 5; j <= 7; j++ {
if matches[j] != "" {
ms, err := strconv.ParseFloat(matches[j], 64)
if err != nil {
return Traceroute{}, fmt.Errorf("parsing latency: %w", err)
}
latencies = append(latencies, time.Duration(ms*float64(time.Millisecond)))
}
}
path := TraceroutePath{
Hop: hop,
Hostname: hopHostname,
IP: hopIP,
Latencies: latencies,
}
result.Route = append(result.Route, path)
// Check if we've reached the target
if hopIP == ip {
result.Success = true
}
}
// If we didn't reach the target, it's unsuccessful
if !result.Success {
result.Err = errors.New("traceroute did not reach target")
}
return result, nil
}