[client] Compare service URLs as endpoints, not as strings

Three places in one request path each had their own notion of "same
management URL": the config layer compared the parsed URLs as strings, the
privileged-change gate compared scheme + host + effective port, and the MDM
conflict check compared strings after filling in the default port. Only the
middle one was right.

A string comparison answers the wrong question. "https://api.netbird.io",
"https://api.netbird.io/" and "https://API.netbird.io:443" are one endpoint
written three ways, so a client restating its own management URL with a
trailing slash — a normal way to write it — was still read as a client asking
to be repointed, and the update-settings gate refused it. The MDM check had
the same flaw against the enforced value.

profilemanager.SameServiceURL is now the single comparison: same scheme, same
host case-insensitively as DNS names are, same effective port. The config
layer, the privileged-change gate and the MDM conflict check all defer to it,
so there is one answer to "did this URL change?" instead of three.
This commit is contained in:
riccardom
2026-09-02 14:31:38 +02:00
parent 0b969e2124
commit 2a17bf0d55
5 changed files with 145 additions and 44 deletions
+15 -20
View File
@@ -3,13 +3,13 @@ package server
import (
"context"
"fmt"
"net/url"
"time"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/mdm"
"github.com/netbirdio/netbird/client/proto"
)
@@ -185,24 +185,11 @@ func conflictBool(key string, p *bool) conflictCheck {
}
}
func canonicalURL(s string) string {
u, err := url.ParseRequestURI(s)
if err != nil {
return s
}
if u.Port() == "" {
switch u.Scheme {
case "https":
u.Host += ":443"
case "http":
u.Host += ":80"
}
}
return u.String()
}
// conflictURL is conflictString for URL-typed keys: both sides are
// normalized via canonicalURL before comparison.
// conflictURL is conflictString for URL-typed keys: both sides are compared as
// endpoints (profilemanager.SameServiceURL), so an implicit default port, a
// trailing slash or a different host case is not read as a divergence from the
// policy. A value that does not parse as a URL falls back to string equality,
// which is the strictest thing left to do with it.
func conflictURL(key, got string) conflictCheck {
return conflictCheck{
key: key,
@@ -211,7 +198,15 @@ func conflictURL(key, got string) conflictCheck {
return true
}
want, ok := pol.GetString(key)
return ok && canonicalURL(want) == canonicalURL(got)
if !ok {
return false
}
wantURL, wantErr := profilemanager.ParseServiceURL(key, want)
gotURL, gotErr := profilemanager.ParseServiceURL(key, got)
if wantErr != nil || gotErr != nil {
return want == got
}
return profilemanager.SameServiceURL(wantURL, gotURL)
},
}
}