mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			176 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			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
 | |
| }
 |