mirror of
https://github.com/juanfont/headscale.git
synced 2025-10-19 11:15:48 +02:00
Initial work on a nodestore which stores all of the nodes and their relations in memory with relationship for peers precalculated. It is a copy-on-write structure, replacing the "snapshot" when a change to the structure occurs. It is optimised for reads, and while batches are not fast, they are grouped together to do less of the expensive peer calculation if there are many changes rapidly. Writes will block until commited, while reads are never blocked. Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
189 lines
4.4 KiB
Go
189 lines
4.4 KiB
Go
package util
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"net/url"
|
|
"os"
|
|
"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 j < len(matches) && 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
|
|
}
|
|
|
|
func IsCI() bool {
|
|
if _, ok := os.LookupEnv("CI"); ok {
|
|
return true
|
|
}
|
|
|
|
if _, ok := os.LookupEnv("GITHUB_RUN_ID"); ok {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|