Files
netbird/util/serviceurl.go
Riccardo Manfrin d2e62e358a [client] Compare MDM-managed URLs as endpoints, not as strings (#7472)
A policy that enforces a management URL refuses any SetConfig or Login whose
URL differs from it. The comparison normalized only the default port, so
three ways of writing the very endpoint the policy names were reported as
conflicts:

  policy https://mgmt.example.com  vs  https://mgmt.example.com/     refused
                                       https://MGMT.example.com      refused
                                       https://mgmt.example.com:0443 refused

For an MDM-managed deployment whose stored or command-line URL is spelled
differently from the policy's value, that means every settings update is
refused with an MDMManagedFieldsViolation naming a field the caller did not
change. `netbird up --management-url https://MGMT.example.com` reproduces it.

The rules now live in util.SameServiceURL, and ConflictURL delegates: scheme
and host compared case-insensitively, the effective port normalized
numerically, a trailing slash ignored, and a path otherwise still part of the
identity so /other remains a divergence. Unparseable input falls back to
string equality.

util rather than either caller, because comparing two service URLs is
neither device management nor profile storage, and more than one place does
it: an MDM-enforced management URL against a requested one here, a stored
profile URL against a command-line one in profilemanager and the SSH gate.
Every copy of these rules that drifts turns an equivalent URL into a refused
request, which is how this one arose.

CanonicalURL is left alone: besides comparison it is the canonical value
handed to mdm.Restrictions and to the Android and iOS Preferences getters,
and normalizing what those return is a separate decision.
2026-09-08 16:32:16 +02:00

70 lines
2.1 KiB
Go

package util
import (
"net/url"
"strconv"
"strings"
)
// SameServiceURL reports whether two service URLs address the same endpoint.
// One endpoint can be written several ways, and every spelling below reaches
// the same server, so none of them is a divergence from another:
//
// an implicit default port https://mgmt.example.com :443
// a zero-padded port https://mgmt.example.com:0443
// a different host case https://MGMT.example.com
// a trailing slash https://mgmt.example.com/
//
// A path is otherwise part of the identity: https://mgmt.example.com and
// https://mgmt.example.com/other are two endpoints.
//
// It lives here rather than next to any one caller because several of them
// compare the same kind of URL — an MDM-enforced management URL against a
// requested one, a stored profile URL against a command-line one — and every
// copy of these rules that drifts turns an equivalent URL into a refused
// request.
func SameServiceURL(a, b *url.URL) bool {
if a == nil || b == nil {
return a == b
}
return strings.EqualFold(a.Hostname(), b.Hostname()) &&
strings.EqualFold(a.Scheme, b.Scheme) &&
ServiceURLPort(a) == ServiceURLPort(b) &&
strings.TrimSuffix(a.Path, "/") == strings.TrimSuffix(b.Path, "/")
}
// SameServiceURLStrings is SameServiceURL for unparsed input. Input that does
// not parse falls back to string equality, which is the strictest thing left
// to do with it.
func SameServiceURLStrings(a, b string) bool {
ua, errA := url.ParseRequestURI(a)
ub, errB := url.ParseRequestURI(b)
if errA != nil || errB != nil {
return a == b
}
return SameServiceURL(ua, ub)
}
// ServiceURLPort is the port a URL addresses: the one it carries, normalized
// numerically so ":0443" and ":443" are one port, or the scheme's default.
func ServiceURLPort(u *url.URL) string {
port := u.Port()
if port == "" {
switch strings.ToLower(u.Scheme) {
case "https":
return "443"
case "http":
return "80"
default:
return ""
}
}
if n, err := strconv.Atoi(port); err == nil {
return strconv.Itoa(n)
}
return port
}