all: fix golangci-lint issues (#3064)
This commit is contained in:
parent
bfb6fd80df
commit
ce580f8245
131 changed files with 3131 additions and 1560 deletions
|
|
@ -91,6 +91,7 @@ func ParseIPSet(arg string, bits *int) (*netipx.IPSet, error) {
|
|||
|
||||
func GetIPPrefixEndpoints(na netip.Prefix) (netip.Addr, netip.Addr) {
|
||||
var network, broadcast netip.Addr
|
||||
|
||||
ipRange := netipx.RangeOfPrefix(na)
|
||||
network = ipRange.From()
|
||||
broadcast = ipRange.To()
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ func Test_parseIPSet(t *testing.T) {
|
|||
arg string
|
||||
bits *int
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
|
|
@ -111,6 +112,7 @@ func Test_parseIPSet(t *testing.T) {
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Errorf("parseIPSet() = (-want +got):\n%s", diff)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,13 +18,27 @@ const (
|
|||
ipv4AddressLength = 32
|
||||
ipv6AddressLength = 128
|
||||
|
||||
// LabelHostnameLength is the maximum length for a DNS label,
|
||||
// value related to RFC 1123 and 952.
|
||||
LabelHostnameLength = 63
|
||||
)
|
||||
|
||||
var invalidDNSRegex = regexp.MustCompile("[^a-z0-9-.]+")
|
||||
|
||||
var ErrInvalidHostName = errors.New("invalid hostname")
|
||||
// DNS validation errors.
|
||||
var (
|
||||
ErrInvalidHostName = errors.New("invalid hostname")
|
||||
ErrUsernameTooShort = errors.New("username must be at least 2 characters long")
|
||||
ErrUsernameMustStartLetter = errors.New("username must start with a letter")
|
||||
ErrUsernameTooManyAt = errors.New("username cannot contain more than one '@'")
|
||||
ErrUsernameInvalidChar = errors.New("username contains invalid character")
|
||||
ErrHostnameTooShort = errors.New("hostname is too short, must be at least 2 characters")
|
||||
ErrHostnameTooLong = errors.New("hostname is too long, must not exceed 63 characters")
|
||||
ErrHostnameMustBeLowercase = errors.New("hostname must be lowercase")
|
||||
ErrHostnameHyphenBoundary = errors.New("hostname cannot start or end with a hyphen")
|
||||
ErrHostnameDotBoundary = errors.New("hostname cannot start or end with a dot")
|
||||
ErrHostnameInvalidChars = errors.New("hostname contains invalid characters")
|
||||
)
|
||||
|
||||
// ValidateUsername checks if a username is valid.
|
||||
// It must be at least 2 characters long, start with a letter, and contain
|
||||
|
|
@ -34,12 +48,12 @@ var ErrInvalidHostName = errors.New("invalid hostname")
|
|||
func ValidateUsername(username string) error {
|
||||
// Ensure the username meets the minimum length requirement
|
||||
if len(username) < 2 {
|
||||
return errors.New("username must be at least 2 characters long")
|
||||
return ErrUsernameTooShort
|
||||
}
|
||||
|
||||
// Ensure the username starts with a letter
|
||||
if !unicode.IsLetter(rune(username[0])) {
|
||||
return errors.New("username must start with a letter")
|
||||
return ErrUsernameMustStartLetter
|
||||
}
|
||||
|
||||
atCount := 0
|
||||
|
|
@ -55,10 +69,10 @@ func ValidateUsername(username string) error {
|
|||
case char == '@':
|
||||
atCount++
|
||||
if atCount > 1 {
|
||||
return errors.New("username cannot contain more than one '@'")
|
||||
return ErrUsernameTooManyAt
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("username contains invalid character: '%c'", char)
|
||||
return fmt.Errorf("%w: '%c'", ErrUsernameInvalidChar, char)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,44 +84,27 @@ func ValidateUsername(username string) error {
|
|||
// The hostname must already be lowercase and contain only valid characters.
|
||||
func ValidateHostname(name string) error {
|
||||
if len(name) < 2 {
|
||||
return fmt.Errorf(
|
||||
"hostname %q is too short, must be at least 2 characters",
|
||||
name,
|
||||
)
|
||||
return fmt.Errorf("%w: %q", ErrHostnameTooShort, name)
|
||||
}
|
||||
|
||||
if len(name) > LabelHostnameLength {
|
||||
return fmt.Errorf(
|
||||
"hostname %q is too long, must not exceed 63 characters",
|
||||
name,
|
||||
)
|
||||
return fmt.Errorf("%w: %q", ErrHostnameTooLong, name)
|
||||
}
|
||||
|
||||
if strings.ToLower(name) != name {
|
||||
return fmt.Errorf(
|
||||
"hostname %q must be lowercase (try %q)",
|
||||
name,
|
||||
strings.ToLower(name),
|
||||
)
|
||||
return fmt.Errorf("%w: %q (try %q)", ErrHostnameMustBeLowercase, name, strings.ToLower(name))
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, "-") || strings.HasSuffix(name, "-") {
|
||||
return fmt.Errorf(
|
||||
"hostname %q cannot start or end with a hyphen",
|
||||
name,
|
||||
)
|
||||
return fmt.Errorf("%w: %q", ErrHostnameHyphenBoundary, name)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
|
||||
return fmt.Errorf(
|
||||
"hostname %q cannot start or end with a dot",
|
||||
name,
|
||||
)
|
||||
return fmt.Errorf("%w: %q", ErrHostnameDotBoundary, name)
|
||||
}
|
||||
|
||||
if invalidDNSRegex.MatchString(name) {
|
||||
return fmt.Errorf(
|
||||
"hostname %q contains invalid characters, only lowercase letters, numbers, hyphens and dots are allowed",
|
||||
name,
|
||||
)
|
||||
return fmt.Errorf("%w: %q", ErrHostnameInvalidChars, name)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -170,6 +167,7 @@ func NormaliseHostname(name string) (string, error) {
|
|||
// and do not make use of RFC2317 ("Classless IN-ADDR.ARPA delegation") - hence generating the entries for the next
|
||||
// class block only.
|
||||
|
||||
// GenerateIPv4DNSRootDomain generates the IPv4 reverse DNS root domains.
|
||||
// From the netmask we can find out the wildcard bits (the bits that are not set in the netmask).
|
||||
// This allows us to then calculate the subnets included in the subsequent class block and generate the entries.
|
||||
func GenerateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
|
||||
|
|
@ -183,25 +181,27 @@ func GenerateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
|
|||
// wildcardBits is the number of bits not under the mask in the lastOctet
|
||||
wildcardBits := ByteSize - maskBits%ByteSize
|
||||
|
||||
// min is the value in the lastOctet byte of the IP
|
||||
// max is basically 2^wildcardBits - i.e., the value when all the wildcardBits are set to 1
|
||||
min := uint(netRange.IP[lastOctet])
|
||||
max := (min + 1<<uint(wildcardBits)) - 1
|
||||
// minVal is the value in the lastOctet byte of the IP
|
||||
// maxVal is basically 2^wildcardBits - i.e., the value when all the wildcardBits are set to 1
|
||||
minVal := uint(netRange.IP[lastOctet])
|
||||
maxVal := (minVal + 1<<uint(wildcardBits)) - 1 //nolint:gosec // wildcardBits is always < 8, no overflow
|
||||
|
||||
// here we generate the base domain (e.g., 100.in-addr.arpa., 16.172.in-addr.arpa., etc.)
|
||||
rdnsSlice := []string{}
|
||||
for i := lastOctet - 1; i >= 0; i-- {
|
||||
rdnsSlice = append(rdnsSlice, strconv.FormatUint(uint64(netRange.IP[i]), 10))
|
||||
}
|
||||
|
||||
rdnsSlice = append(rdnsSlice, "in-addr.arpa.")
|
||||
rdnsBase := strings.Join(rdnsSlice, ".")
|
||||
|
||||
fqdns := make([]dnsname.FQDN, 0, max-min+1)
|
||||
for i := min; i <= max; i++ {
|
||||
fqdns := make([]dnsname.FQDN, 0, maxVal-minVal+1)
|
||||
for i := minVal; i <= maxVal; i++ {
|
||||
fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%d.%s", i, rdnsBase))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fqdns = append(fqdns, fqdn)
|
||||
}
|
||||
|
||||
|
|
@ -226,6 +226,7 @@ func GenerateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
|
|||
// and do not make use of RFC2317 ("Classless IN-ADDR.ARPA delegation") - hence generating the entries for the next
|
||||
// class block only.
|
||||
|
||||
// GenerateIPv6DNSRootDomain generates the IPv6 reverse DNS root domains.
|
||||
// From the netmask we can find out the wildcard bits (the bits that are not set in the netmask).
|
||||
// This allows us to then calculate the subnets included in the subsequent class block and generate the entries.
|
||||
func GenerateIPv6DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
|
||||
|
|
@ -259,18 +260,22 @@ func GenerateIPv6DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN {
|
|||
}
|
||||
|
||||
var fqdns []dnsname.FQDN
|
||||
|
||||
if maskBits%4 == 0 {
|
||||
dom, _ := makeDomain()
|
||||
fqdns = append(fqdns, dom)
|
||||
} else {
|
||||
domCount := 1 << (maskBits % nibbleLen)
|
||||
|
||||
fqdns = make([]dnsname.FQDN, 0, domCount)
|
||||
for i := range domCount {
|
||||
varNibble := fmt.Sprintf("%x", i)
|
||||
|
||||
dom, err := makeDomain(varNibble)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fqdns = append(fqdns, dom)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ func TestNormaliseHostname(t *testing.T) {
|
|||
type args struct {
|
||||
name string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
|
|
@ -90,6 +91,7 @@ func TestNormaliseHostname(t *testing.T) {
|
|||
t.Errorf("NormaliseHostname() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !tt.wantErr && got != tt.want {
|
||||
t.Errorf("NormaliseHostname() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
|
@ -172,6 +174,7 @@ func TestValidateHostname(t *testing.T) {
|
|||
t.Errorf("ValidateHostname() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if tt.wantErr && tt.errorContains != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), tt.errorContains) {
|
||||
t.Errorf("ValidateHostname() error = %v, should contain %q", err, tt.errorContains)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ const (
|
|||
PermissionFallback = 0o700
|
||||
)
|
||||
|
||||
// ErrDirectoryPermission is returned when creating a directory fails due to permission issues.
|
||||
var ErrDirectoryPermission = errors.New("creating directory failed with permission error")
|
||||
|
||||
func AbsolutePathFromConfigPath(path string) string {
|
||||
// If a relative path is provided, prefix it with the directory where
|
||||
// the config file was found.
|
||||
|
|
@ -42,18 +45,15 @@ func GetFileMode(key string) fs.FileMode {
|
|||
return PermissionFallback
|
||||
}
|
||||
|
||||
return fs.FileMode(mode)
|
||||
return fs.FileMode(mode) //nolint:gosec // file mode is bounded by ParseUint
|
||||
}
|
||||
|
||||
func EnsureDir(dir string) error {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) { //nolint:noinlineerr
|
||||
err := os.MkdirAll(dir, PermissionFallback)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
return fmt.Errorf(
|
||||
"creating directory %s, failed with permission error, is it located somewhere Headscale can write?",
|
||||
dir,
|
||||
)
|
||||
return fmt.Errorf("%w: %s", ErrDirectoryPermission, dir)
|
||||
}
|
||||
|
||||
return fmt.Errorf("creating directory %s: %w", dir, err)
|
||||
|
|
|
|||
|
|
@ -87,5 +87,6 @@ func (l *DBLogWrapper) ParamsFilter(ctx context.Context, sql string, params ...a
|
|||
if l.ParameterizedQueries {
|
||||
return sql, nil
|
||||
}
|
||||
|
||||
return sql, params
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,14 @@ func YesNo(msg string) bool {
|
|||
fmt.Fprint(os.Stderr, msg+" [y/n] ")
|
||||
|
||||
var resp string
|
||||
fmt.Scanln(&resp)
|
||||
|
||||
_, _ = fmt.Scanln(&resp)
|
||||
|
||||
resp = strings.ToLower(resp)
|
||||
switch resp {
|
||||
case "y", "yes", "sure":
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,8 @@ func TestYesNo(t *testing.T) {
|
|||
// Write test input
|
||||
go func() {
|
||||
defer w.Close()
|
||||
w.WriteString(tt.input)
|
||||
|
||||
_, _ = w.WriteString(tt.input)
|
||||
}()
|
||||
|
||||
// Call the function
|
||||
|
|
@ -95,6 +96,7 @@ func TestYesNo(t *testing.T) {
|
|||
// Restore stdin and stderr
|
||||
os.Stdin = oldStdin
|
||||
os.Stderr = oldStderr
|
||||
|
||||
stderrW.Close()
|
||||
|
||||
// Check the result
|
||||
|
|
@ -104,10 +106,12 @@ func TestYesNo(t *testing.T) {
|
|||
|
||||
// Check that the prompt was written to stderr
|
||||
var stderrBuf bytes.Buffer
|
||||
io.Copy(&stderrBuf, stderrR)
|
||||
|
||||
_, _ = io.Copy(&stderrBuf, stderrR)
|
||||
stderrR.Close()
|
||||
|
||||
expectedPrompt := "Test question [y/n] "
|
||||
|
||||
actualPrompt := stderrBuf.String()
|
||||
if actualPrompt != expectedPrompt {
|
||||
t.Errorf("Expected prompt %q, got %q", expectedPrompt, actualPrompt)
|
||||
|
|
@ -130,7 +134,8 @@ func TestYesNoPromptMessage(t *testing.T) {
|
|||
// Write test input
|
||||
go func() {
|
||||
defer w.Close()
|
||||
w.WriteString("n\n")
|
||||
|
||||
_, _ = w.WriteString("n\n")
|
||||
}()
|
||||
|
||||
// Call the function with a custom message
|
||||
|
|
@ -140,14 +145,17 @@ func TestYesNoPromptMessage(t *testing.T) {
|
|||
// Restore stdin and stderr
|
||||
os.Stdin = oldStdin
|
||||
os.Stderr = oldStderr
|
||||
|
||||
stderrW.Close()
|
||||
|
||||
// Check that the custom message was included in the prompt
|
||||
var stderrBuf bytes.Buffer
|
||||
io.Copy(&stderrBuf, stderrR)
|
||||
|
||||
_, _ = io.Copy(&stderrBuf, stderrR)
|
||||
stderrR.Close()
|
||||
|
||||
expectedPrompt := customMessage + " [y/n] "
|
||||
|
||||
actualPrompt := stderrBuf.String()
|
||||
if actualPrompt != expectedPrompt {
|
||||
t.Errorf("Expected prompt %q, got %q", expectedPrompt, actualPrompt)
|
||||
|
|
@ -186,7 +194,8 @@ func TestYesNoCaseInsensitive(t *testing.T) {
|
|||
// Write test input
|
||||
go func() {
|
||||
defer w.Close()
|
||||
w.WriteString(tc.input)
|
||||
|
||||
_, _ = w.WriteString(tc.input)
|
||||
}()
|
||||
|
||||
// Call the function
|
||||
|
|
@ -195,10 +204,11 @@ func TestYesNoCaseInsensitive(t *testing.T) {
|
|||
// Restore stdin and stderr
|
||||
os.Stdin = oldStdin
|
||||
os.Stderr = oldStderr
|
||||
|
||||
stderrW.Close()
|
||||
|
||||
// Drain stderr
|
||||
io.Copy(io.Discard, stderrR)
|
||||
_, _ = io.Copy(io.Discard, stderrR)
|
||||
stderrR.Close()
|
||||
|
||||
if result != tc.expected {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ func GenerateRandomBytes(n int) ([]byte, error) {
|
|||
bytes := make([]byte, n)
|
||||
|
||||
// Note that err == nil only if we read len(b) bytes.
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
if _, err := rand.Read(bytes); err != nil { //nolint:noinlineerr
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -33,6 +33,7 @@ func GenerateRandomStringURLSafe(n int) (string, error) {
|
|||
b, err := GenerateRandomBytes(n)
|
||||
|
||||
uenc := base64.RawURLEncoding.EncodeToString(b)
|
||||
|
||||
return uenc[:n], err
|
||||
}
|
||||
|
||||
|
|
@ -42,13 +43,17 @@ func GenerateRandomStringURLSafe(n int) (string, error) {
|
|||
// number generator fails to function correctly, in which
|
||||
// case the caller should not continue.
|
||||
func GenerateRandomStringDNSSafe(size int) (string, error) {
|
||||
var str string
|
||||
var err error
|
||||
var (
|
||||
str string
|
||||
err error
|
||||
)
|
||||
|
||||
for len(str) < size {
|
||||
str, err = GenerateRandomStringURLSafe(size)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
str = strings.ToLower(
|
||||
strings.ReplaceAll(strings.ReplaceAll(str, "_", ""), "-", ""),
|
||||
)
|
||||
|
|
@ -99,6 +104,7 @@ func TailcfgFilterRulesToString(rules []tailcfg.FilterRule) string {
|
|||
DstIPs: %v
|
||||
}
|
||||
`, rule.SrcIPs, rule.DstPorts))
|
||||
|
||||
if index < len(rules)-1 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ var DkeyComparer = cmp.Comparer(func(x, y key.DiscoPublic) bool {
|
|||
return x.String() == y.String()
|
||||
})
|
||||
|
||||
var ViewSliceIPProtoComparer = cmp.Comparer(func(a, b views.Slice[ipproto.Proto]) bool { return views.SliceEqual(a, b) })
|
||||
var ViewSliceIPProtoComparer = cmp.Comparer(views.SliceEqual[ipproto.Proto])
|
||||
|
||||
var Comparers []cmp.Option = []cmp.Option{
|
||||
IPComparer, PrefixComparer, AddrPortComparer, MkeyComparer, NkeyComparer, DkeyComparer, ViewSliceIPProtoComparer,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@ import (
|
|||
"tailscale.com/util/cmpver"
|
||||
)
|
||||
|
||||
// URL parsing errors.
|
||||
var (
|
||||
ErrMultipleURLsFound = errors.New("multiple URLs found")
|
||||
ErrNoURLFound = errors.New("no URL found")
|
||||
ErrEmptyTracerouteOutput = errors.New("empty traceroute output")
|
||||
ErrTracerouteHeaderParse = errors.New("parsing traceroute header")
|
||||
ErrTracerouteDidNotReach = errors.New("traceroute did not reach target")
|
||||
)
|
||||
|
||||
func TailscaleVersionNewerOrEqual(minimum, toCheck string) bool {
|
||||
if cmpver.Compare(minimum, toCheck) <= 0 ||
|
||||
toCheck == "unstable" ||
|
||||
|
|
@ -30,20 +39,22 @@ func TailscaleVersionNewerOrEqual(minimum, toCheck string) bool {
|
|||
// 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)
|
||||
return nil, fmt.Errorf("%w: %s and %s", ErrMultipleURLsFound, urlStr, line)
|
||||
}
|
||||
|
||||
urlStr = line
|
||||
}
|
||||
}
|
||||
|
||||
if urlStr == "" {
|
||||
return nil, errors.New("no URL found")
|
||||
return nil, ErrNoURLFound
|
||||
}
|
||||
|
||||
loginURL, err := url.Parse(urlStr)
|
||||
|
|
@ -89,14 +100,15 @@ type 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")
|
||||
return Traceroute{}, ErrEmptyTracerouteOutput
|
||||
}
|
||||
|
||||
// Parse the header line - handle both 'traceroute' and 'tracert' (Windows)
|
||||
headerRegex := regexp.MustCompile(`(?i)(?:traceroute|tracing route) to ([^ ]+) (?:\[([^\]]+)\]|\(([^)]+)\))`)
|
||||
|
||||
headerMatches := headerRegex.FindStringSubmatch(lines[0])
|
||||
if len(headerMatches) < 2 {
|
||||
return Traceroute{}, fmt.Errorf("parsing traceroute header: %s", lines[0])
|
||||
return Traceroute{}, fmt.Errorf("%w: %s", ErrTracerouteHeaderParse, lines[0])
|
||||
}
|
||||
|
||||
hostname := headerMatches[1]
|
||||
|
|
@ -105,6 +117,7 @@ func ParseTraceroute(output string) (Traceroute, error) {
|
|||
if ipStr == "" {
|
||||
ipStr = headerMatches[3]
|
||||
}
|
||||
|
||||
ip, err := netip.ParseAddr(ipStr)
|
||||
if err != nil {
|
||||
return Traceroute{}, fmt.Errorf("parsing IP address %s: %w", ipStr, err)
|
||||
|
|
@ -144,19 +157,23 @@ func ParseTraceroute(output string) (Traceroute, error) {
|
|||
}
|
||||
|
||||
remainder := strings.TrimSpace(matches[2])
|
||||
var hopHostname string
|
||||
var hopIP netip.Addr
|
||||
var latencies []time.Duration
|
||||
|
||||
var (
|
||||
hopHostname string
|
||||
hopIP netip.Addr
|
||||
latencies []time.Duration
|
||||
)
|
||||
|
||||
// Check for Windows tracert format which has latencies before hostname
|
||||
// Format: " 1 <1 ms <1 ms <1 ms router.local [192.168.1.1]"
|
||||
latencyFirst := false
|
||||
|
||||
if strings.Contains(remainder, " ms ") && !strings.HasPrefix(remainder, "*") {
|
||||
// Check if latencies appear before any hostname/IP
|
||||
firstSpace := strings.Index(remainder, " ")
|
||||
if firstSpace > 0 {
|
||||
firstPart := remainder[:firstSpace]
|
||||
if _, err := strconv.ParseFloat(strings.TrimPrefix(firstPart, "<"), 64); err == nil {
|
||||
if _, err := strconv.ParseFloat(strings.TrimPrefix(firstPart, "<"), 64); err == nil { //nolint:noinlineerr
|
||||
latencyFirst = true
|
||||
}
|
||||
}
|
||||
|
|
@ -171,12 +188,14 @@ func ParseTraceroute(output string) (Traceroute, error) {
|
|||
}
|
||||
// Extract and remove the latency from the beginning
|
||||
latStr := strings.TrimPrefix(remainder[latMatch[2]:latMatch[3]], "<")
|
||||
|
||||
ms, err := strconv.ParseFloat(latStr, 64)
|
||||
if err == nil {
|
||||
// Round to nearest microsecond to avoid floating point precision issues
|
||||
duration := time.Duration(ms * float64(time.Millisecond))
|
||||
latencies = append(latencies, duration.Round(time.Microsecond))
|
||||
}
|
||||
|
||||
remainder = strings.TrimSpace(remainder[latMatch[1]:])
|
||||
}
|
||||
}
|
||||
|
|
@ -202,9 +221,10 @@ func ParseTraceroute(output string) (Traceroute, error) {
|
|||
parts := strings.Fields(remainder)
|
||||
if len(parts) > 0 {
|
||||
hopHostname = parts[0]
|
||||
if ip, err := netip.ParseAddr(parts[0]); err == nil {
|
||||
if ip, err := netip.ParseAddr(parts[0]); err == nil { //nolint:noinlineerr
|
||||
hopIP = ip
|
||||
}
|
||||
|
||||
remainder = strings.TrimSpace(strings.Join(parts[1:], " "))
|
||||
}
|
||||
}
|
||||
|
|
@ -216,6 +236,7 @@ func ParseTraceroute(output string) (Traceroute, error) {
|
|||
if len(match) > 1 {
|
||||
// Remove '<' prefix if present (e.g., "<1 ms")
|
||||
latStr := strings.TrimPrefix(match[1], "<")
|
||||
|
||||
ms, err := strconv.ParseFloat(latStr, 64)
|
||||
if err == nil {
|
||||
// Round to nearest microsecond to avoid floating point precision issues
|
||||
|
|
@ -243,7 +264,7 @@ func ParseTraceroute(output string) (Traceroute, error) {
|
|||
|
||||
// If we didn't reach the target, it's unsuccessful
|
||||
if !result.Success {
|
||||
result.Err = errors.New("traceroute did not reach target")
|
||||
result.Err = ErrTracerouteDidNotReach
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
|
@ -261,11 +282,11 @@ func IsCI() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// SafeHostname extracts a hostname from Hostinfo, providing sensible defaults
|
||||
// EnsureHostname guarantees a valid hostname for node registration.
|
||||
// It extracts a hostname from Hostinfo, providing sensible defaults
|
||||
// if Hostinfo is nil or Hostname is empty. This prevents nil pointer dereferences
|
||||
// and ensures nodes always have a valid hostname.
|
||||
// The hostname is truncated to 63 characters to comply with DNS label length limits (RFC 1123).
|
||||
// EnsureHostname guarantees a valid hostname for node registration.
|
||||
// This function never fails - it always returns a valid hostname.
|
||||
//
|
||||
// Strategy:
|
||||
|
|
@ -280,15 +301,19 @@ func EnsureHostname(hostinfo *tailcfg.Hostinfo, machineKey, nodeKey string) stri
|
|||
if key == "" {
|
||||
return "unknown-node"
|
||||
}
|
||||
|
||||
keyPrefix := key
|
||||
if len(key) > 8 {
|
||||
keyPrefix = key[:8]
|
||||
}
|
||||
return fmt.Sprintf("node-%s", keyPrefix)
|
||||
|
||||
return "node-" + keyPrefix
|
||||
}
|
||||
|
||||
lowercased := strings.ToLower(hostinfo.Hostname)
|
||||
if err := ValidateHostname(lowercased); err == nil {
|
||||
|
||||
err := ValidateHostname(lowercased)
|
||||
if err == nil {
|
||||
return lowercased
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -11,11 +10,14 @@ import (
|
|||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
const testUnknownNode = "unknown-node"
|
||||
|
||||
func TestTailscaleVersionNewerOrEqual(t *testing.T) {
|
||||
type args struct {
|
||||
minimum string
|
||||
toCheck string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
|
|
@ -180,6 +182,7 @@ Success.`,
|
|||
if err != nil {
|
||||
t.Errorf("ParseLoginURLFromCLILogin() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if gotURL.String() != tt.wantURL {
|
||||
t.Errorf("ParseLoginURLFromCLILogin() = %v, want %v", gotURL, tt.wantURL)
|
||||
}
|
||||
|
|
@ -321,7 +324,7 @@ func TestParseTraceroute(t *testing.T) {
|
|||
},
|
||||
},
|
||||
Success: false,
|
||||
Err: errors.New("traceroute did not reach target"),
|
||||
Err: ErrTracerouteDidNotReach,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -489,7 +492,7 @@ over a maximum of 30 hops:
|
|||
},
|
||||
},
|
||||
Success: false,
|
||||
Err: errors.New("traceroute did not reach target"),
|
||||
Err: ErrTracerouteDidNotReach,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -834,7 +837,7 @@ func TestEnsureHostname(t *testing.T) {
|
|||
hostinfo: nil,
|
||||
machineKey: "",
|
||||
nodeKey: "",
|
||||
want: "unknown-node",
|
||||
want: testUnknownNode,
|
||||
},
|
||||
{
|
||||
name: "empty_hostname_with_machine_key",
|
||||
|
|
@ -861,7 +864,7 @@ func TestEnsureHostname(t *testing.T) {
|
|||
},
|
||||
machineKey: "",
|
||||
nodeKey: "",
|
||||
want: "unknown-node",
|
||||
want: testUnknownNode,
|
||||
},
|
||||
{
|
||||
name: "hostname_exactly_63_chars",
|
||||
|
|
@ -902,7 +905,7 @@ func TestEnsureHostname(t *testing.T) {
|
|||
{
|
||||
name: "hostname_with_unicode",
|
||||
hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: "node-ñoño-测试",
|
||||
Hostname: "node-ñoño-测试", //nolint:gosmopolitan
|
||||
},
|
||||
machineKey: "mkey12345678",
|
||||
nodeKey: "nkey12345678",
|
||||
|
|
@ -983,7 +986,7 @@ func TestEnsureHostname(t *testing.T) {
|
|||
{
|
||||
name: "chinese_chars_with_dash_invalid",
|
||||
hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: "server-北京-01",
|
||||
Hostname: "server-北京-01", //nolint:gosmopolitan
|
||||
},
|
||||
machineKey: "mkey12345678",
|
||||
nodeKey: "nkey12345678",
|
||||
|
|
@ -992,7 +995,7 @@ func TestEnsureHostname(t *testing.T) {
|
|||
{
|
||||
name: "chinese_only_invalid",
|
||||
hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: "我的电脑",
|
||||
Hostname: "我的电脑", //nolint:gosmopolitan
|
||||
},
|
||||
machineKey: "mkey12345678",
|
||||
nodeKey: "nkey12345678",
|
||||
|
|
@ -1010,7 +1013,7 @@ func TestEnsureHostname(t *testing.T) {
|
|||
{
|
||||
name: "mixed_chinese_emoji_invalid",
|
||||
hostinfo: &tailcfg.Hostinfo{
|
||||
Hostname: "测试💻机器",
|
||||
Hostname: "测试💻机器", //nolint:gosmopolitan // intentional i18n test data
|
||||
},
|
||||
machineKey: "mkey12345678",
|
||||
nodeKey: "nkey12345678",
|
||||
|
|
@ -1066,6 +1069,7 @@ func TestEnsureHostname(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := EnsureHostname(tt.hostinfo, tt.machineKey, tt.nodeKey)
|
||||
// For invalid hostnames, we just check the prefix since the random part varies
|
||||
if strings.HasPrefix(tt.want, "invalid-") {
|
||||
|
|
@ -1099,13 +1103,15 @@ func TestEnsureHostnameWithHostinfo(t *testing.T) {
|
|||
machineKey: "mkey12345678",
|
||||
nodeKey: "nkey12345678",
|
||||
wantHostname: "test-node",
|
||||
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
||||
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) { //nolint:thelper
|
||||
if hi == nil {
|
||||
t.Error("hostinfo should not be nil")
|
||||
t.Fatal("hostinfo should not be nil")
|
||||
}
|
||||
|
||||
if hi.Hostname != "test-node" {
|
||||
t.Errorf("hostname = %v, want test-node", hi.Hostname)
|
||||
}
|
||||
|
||||
if hi.OS != "linux" {
|
||||
t.Errorf("OS = %v, want linux", hi.OS)
|
||||
}
|
||||
|
|
@ -1143,10 +1149,11 @@ func TestEnsureHostnameWithHostinfo(t *testing.T) {
|
|||
machineKey: "",
|
||||
nodeKey: "nkey12345678",
|
||||
wantHostname: "node-nkey1234",
|
||||
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
||||
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) { //nolint:thelper
|
||||
if hi == nil {
|
||||
t.Error("hostinfo should not be nil")
|
||||
t.Fatal("hostinfo should not be nil")
|
||||
}
|
||||
|
||||
if hi.Hostname != "node-nkey1234" {
|
||||
t.Errorf("hostname = %v, want node-nkey1234", hi.Hostname)
|
||||
}
|
||||
|
|
@ -1157,12 +1164,13 @@ func TestEnsureHostnameWithHostinfo(t *testing.T) {
|
|||
hostinfo: nil,
|
||||
machineKey: "",
|
||||
nodeKey: "",
|
||||
wantHostname: "unknown-node",
|
||||
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
||||
wantHostname: testUnknownNode,
|
||||
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) { //nolint:thelper
|
||||
if hi == nil {
|
||||
t.Error("hostinfo should not be nil")
|
||||
t.Fatal("hostinfo should not be nil")
|
||||
}
|
||||
if hi.Hostname != "unknown-node" {
|
||||
|
||||
if hi.Hostname != testUnknownNode {
|
||||
t.Errorf("hostname = %v, want unknown-node", hi.Hostname)
|
||||
}
|
||||
},
|
||||
|
|
@ -1174,12 +1182,13 @@ func TestEnsureHostnameWithHostinfo(t *testing.T) {
|
|||
},
|
||||
machineKey: "",
|
||||
nodeKey: "",
|
||||
wantHostname: "unknown-node",
|
||||
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
||||
wantHostname: testUnknownNode,
|
||||
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) { //nolint:thelper
|
||||
if hi == nil {
|
||||
t.Error("hostinfo should not be nil")
|
||||
t.Fatal("hostinfo should not be nil")
|
||||
}
|
||||
if hi.Hostname != "unknown-node" {
|
||||
|
||||
if hi.Hostname != testUnknownNode {
|
||||
t.Errorf("hostname = %v, want unknown-node", hi.Hostname)
|
||||
}
|
||||
},
|
||||
|
|
@ -1196,22 +1205,27 @@ func TestEnsureHostnameWithHostinfo(t *testing.T) {
|
|||
machineKey: "mkey12345678",
|
||||
nodeKey: "nkey12345678",
|
||||
wantHostname: "test",
|
||||
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
||||
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) { //nolint:thelper
|
||||
if hi == nil {
|
||||
t.Error("hostinfo should not be nil")
|
||||
}
|
||||
if hi.Hostname != "test" {
|
||||
|
||||
if hi.Hostname != "test" { //nolint:staticcheck // SA5011: nil check is above
|
||||
t.Errorf("hostname = %v, want test", hi.Hostname)
|
||||
}
|
||||
|
||||
if hi.OS != "windows" {
|
||||
t.Errorf("OS = %v, want windows", hi.OS)
|
||||
}
|
||||
|
||||
if hi.OSVersion != "10.0.19044" {
|
||||
t.Errorf("OSVersion = %v, want 10.0.19044", hi.OSVersion)
|
||||
}
|
||||
|
||||
if hi.DeviceModel != "test-device" {
|
||||
t.Errorf("DeviceModel = %v, want test-device", hi.DeviceModel)
|
||||
}
|
||||
|
||||
if hi.BackendLogID != "log123" {
|
||||
t.Errorf("BackendLogID = %v, want log123", hi.BackendLogID)
|
||||
}
|
||||
|
|
@ -1225,11 +1239,12 @@ func TestEnsureHostnameWithHostinfo(t *testing.T) {
|
|||
machineKey: "mkey12345678",
|
||||
nodeKey: "nkey12345678",
|
||||
wantHostname: "123456789012345678901234567890123456789012345678901234567890123",
|
||||
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) {
|
||||
checkHostinfo: func(t *testing.T, hi *tailcfg.Hostinfo) { //nolint:thelper
|
||||
if hi == nil {
|
||||
t.Error("hostinfo should not be nil")
|
||||
}
|
||||
if len(hi.Hostname) != 63 {
|
||||
|
||||
if len(hi.Hostname) != 63 { //nolint:staticcheck // SA5011: nil check is above
|
||||
t.Errorf("hostname length = %v, want 63", len(hi.Hostname))
|
||||
}
|
||||
},
|
||||
|
|
@ -1239,6 +1254,7 @@ func TestEnsureHostnameWithHostinfo(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gotHostname := EnsureHostname(tt.hostinfo, tt.machineKey, tt.nodeKey)
|
||||
// For invalid hostnames, we just check the prefix since the random part varies
|
||||
if strings.HasPrefix(tt.wantHostname, "invalid-") {
|
||||
|
|
@ -1264,7 +1280,10 @@ func TestEnsureHostname_DNSLabelLimit(t *testing.T) {
|
|||
|
||||
for i, hostname := range testCases {
|
||||
t.Run(cmp.Diff("", ""), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hostinfo := &tailcfg.Hostinfo{Hostname: hostname}
|
||||
|
||||
result := EnsureHostname(hostinfo, "mkey", "nkey")
|
||||
if len(result) > 63 {
|
||||
t.Errorf("test case %d: hostname length = %d, want <= 63", i, len(result))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue