[client, management] Support DNS Labels for Peer Addressing (#3252)

* [client] Support Extra DNS Labels for Peer Addressing

* [management] Support Extra DNS Labels for Peer Addressing

---------

Co-authored-by: Viktor Liu <17948409+lixmal@users.noreply.github.com>
This commit is contained in:
hakansa
2025-02-20 13:43:20 +03:00
committed by GitHub
co-authored by Viktor Liu
parent 62a0c358f9
commit 39986b0e97
39 changed files with 1504 additions and 1088 deletions
+16
View File
@@ -361,6 +361,12 @@ components:
description: System serial number
type: string
example: "C02XJ0J0JGH7"
extra_dns_labels:
description: Extra DNS labels added to the peer
type: array
items:
type: string
example: "stage-host-1"
required:
- city_name
- connected
@@ -384,6 +390,7 @@ components:
- ui_version
- approval_required
- serial_number
- extra_dns_labels
AccessiblePeer:
allOf:
- $ref: '#/components/schemas/PeerMinimum'
@@ -503,6 +510,10 @@ components:
description: Indicate that the peer will be ephemeral or not
type: boolean
example: true
allow_extra_dns_labels:
description: Allow extra DNS labels to be added to the peer
type: boolean
example: true
required:
- id
- key
@@ -518,6 +529,7 @@ components:
- updated_at
- usage_limit
- ephemeral
- allow_extra_dns_labels
SetupKeyClear:
allOf:
- $ref: '#/components/schemas/SetupKeyBase'
@@ -587,6 +599,10 @@ components:
description: Indicate that the peer will be ephemeral or not
type: boolean
example: true
allow_extra_dns_labels:
description: Allow extra DNS labels to be added to the peer
type: boolean
example: true
required:
- name
- type
+18
View File
@@ -297,6 +297,9 @@ type CountryCode = string
// CreateSetupKeyRequest defines model for CreateSetupKeyRequest.
type CreateSetupKeyRequest struct {
// AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
AllowExtraDnsLabels *bool `json:"allow_extra_dns_labels,omitempty"`
// AutoGroups List of group IDs to auto-assign to peers registered with this key
AutoGroups []string `json:"auto_groups"`
@@ -689,6 +692,9 @@ type Peer struct {
// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
DnsLabel string `json:"dns_label"`
// ExtraDnsLabels Extra DNS labels added to the peer
ExtraDnsLabels []string `json:"extra_dns_labels"`
// GeonameId Unique identifier from the GeoNames database for a specific geographical location.
GeonameId int `json:"geoname_id"`
@@ -767,6 +773,9 @@ type PeerBatch struct {
// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
DnsLabel string `json:"dns_label"`
// ExtraDnsLabels Extra DNS labels added to the peer
ExtraDnsLabels []string `json:"extra_dns_labels"`
// GeonameId Unique identifier from the GeoNames database for a specific geographical location.
GeonameId int `json:"geoname_id"`
@@ -1230,6 +1239,9 @@ type RulePortRange struct {
// SetupKey defines model for SetupKey.
type SetupKey struct {
// AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"`
// AutoGroups List of group IDs to auto-assign to peers registered with this key
AutoGroups []string `json:"auto_groups"`
@@ -1275,6 +1287,9 @@ type SetupKey struct {
// SetupKeyBase defines model for SetupKeyBase.
type SetupKeyBase struct {
// AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"`
// AutoGroups List of group IDs to auto-assign to peers registered with this key
AutoGroups []string `json:"auto_groups"`
@@ -1317,6 +1332,9 @@ type SetupKeyBase struct {
// SetupKeyClear defines model for SetupKeyClear.
type SetupKeyClear struct {
// AllowExtraDnsLabels Allow extra DNS labels to be added to the peer
AllowExtraDnsLabels bool `json:"allow_extra_dns_labels"`
// AutoGroups List of group IDs to auto-assign to peers registered with this key
AutoGroups []string `json:"auto_groups"`
@@ -338,6 +338,7 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD
UserId: peer.UserID,
UiVersion: peer.Meta.UIVersion,
DnsLabel: fqdn(peer, dnsDomain),
ExtraDnsLabels: fqdnList(peer.ExtraDNSLabels, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.GetLastLogin(),
LoginExpired: peer.Status.LoginExpired,
@@ -372,6 +373,7 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn
UserId: peer.UserID,
UiVersion: peer.Meta.UIVersion,
DnsLabel: fqdn(peer, dnsDomain),
ExtraDnsLabels: fqdnList(peer.ExtraDNSLabels, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.GetLastLogin(),
LoginExpired: peer.Status.LoginExpired,
@@ -392,3 +394,11 @@ func fqdn(peer *nbpeer.Peer, dnsDomain string) string {
return fqdn
}
}
func fqdnList(extraLabels []string, dnsDomain string) []string {
fqdnList := make([]string, 0, len(extraLabels))
for _, label := range extraLabels {
fqdn := fmt.Sprintf("%s.%s", label, dnsDomain)
fqdnList = append(fqdnList, fqdn)
}
return fqdnList
}
@@ -2,11 +2,8 @@ package routes
import (
"encoding/json"
"fmt"
"net/http"
"net/netip"
"regexp"
"strings"
"unicode/utf8"
"github.com/gorilla/mux"
@@ -21,7 +18,6 @@ import (
"github.com/netbirdio/netbird/route"
)
const maxDomains = 32
const failedToConvertRoute = "failed to convert route to response: %v"
// handler is the routes handler of the account
@@ -102,7 +98,7 @@ func (h *handler) createRoute(w http.ResponseWriter, r *http.Request) {
var networkType route.NetworkType
var newPrefix netip.Prefix
if req.Domains != nil {
d, err := validateDomains(*req.Domains)
d, err := domain.ValidateDomains(*req.Domains)
if err != nil {
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid domains: %v", err), w)
return
@@ -225,7 +221,7 @@ func (h *handler) updateRoute(w http.ResponseWriter, r *http.Request) {
}
if req.Domains != nil {
d, err := validateDomains(*req.Domains)
d, err := domain.ValidateDomains(*req.Domains)
if err != nil {
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid domains: %v", err), w)
return
@@ -350,34 +346,3 @@ func toRouteResponse(serverRoute *route.Route) (*api.Route, error) {
}
return route, nil
}
// validateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList.
func validateDomains(domains []string) (domain.List, error) {
if len(domains) == 0 {
return nil, fmt.Errorf("domains list is empty")
}
if len(domains) > maxDomains {
return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
}
domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
var domainList domain.List
for _, d := range domains {
d := strings.ToLower(d)
// handles length and idna conversion
punycode, err := domain.FromString(d)
if err != nil {
return domainList, fmt.Errorf("failed to convert domain to punycode: %s: %v", d, err)
}
if !domainRegex.MatchString(string(punycode)) {
return domainList, fmt.Errorf("invalid domain format: %s", d)
}
domainList = append(domainList, punycode)
}
return domainList, nil
}
@@ -561,96 +561,6 @@ func TestRoutesHandlers(t *testing.T) {
}
}
func TestValidateDomains(t *testing.T) {
tests := []struct {
name string
domains []string
expected domain.List
wantErr bool
}{
{
name: "Empty list",
domains: nil,
expected: nil,
wantErr: true,
},
{
name: "Valid ASCII domain",
domains: []string{"sub.ex-ample.com"},
expected: domain.List{"sub.ex-ample.com"},
wantErr: false,
},
{
name: "Valid Unicode domain",
domains: []string{"münchen.de"},
expected: domain.List{"xn--mnchen-3ya.de"},
wantErr: false,
},
{
name: "Valid Unicode, all labels",
domains: []string{"中国.中国.中国"},
expected: domain.List{"xn--fiqs8s.xn--fiqs8s.xn--fiqs8s"},
wantErr: false,
},
{
name: "With underscores",
domains: []string{"_jabber._tcp.gmail.com"},
expected: domain.List{"_jabber._tcp.gmail.com"},
wantErr: false,
},
{
name: "Invalid domain format",
domains: []string{"-example.com"},
expected: nil,
wantErr: true,
},
{
name: "Invalid domain format 2",
domains: []string{"example.com-"},
expected: nil,
wantErr: true,
},
{
name: "Multiple domains valid and invalid",
domains: []string{"google.com", "invalid,nbdomain.com", "münchen.de"},
expected: domain.List{"google.com"},
wantErr: true,
},
{
name: "Valid wildcard domain",
domains: []string{"*.example.com"},
expected: domain.List{"*.example.com"},
wantErr: false,
},
{
name: "Wildcard with dot domain",
domains: []string{".*.example.com"},
expected: nil,
wantErr: true,
},
{
name: "Wildcard with dot domain",
domains: []string{".*.example.com"},
expected: nil,
wantErr: true,
},
{
name: "Invalid wildcard domain",
domains: []string{"a.*.example.com"},
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := validateDomains(tt.domains)
assert.Equal(t, tt.wantErr, err != nil)
assert.Equal(t, got, tt.expected)
})
}
}
func toApiRoute(t *testing.T, r *route.Route) *api.Route {
t.Helper()
@@ -3,6 +3,7 @@ package setup_keys
import (
"context"
"encoding/json"
"net/http"
"time"
@@ -86,8 +87,13 @@ func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) {
ephemeral = *req.Ephemeral
}
var allowExtraDNSLabels bool
if req.AllowExtraDnsLabels != nil {
allowExtraDNSLabels = *req.AllowExtraDnsLabels
}
setupKey, err := h.accountManager.CreateSetupKey(r.Context(), accountID, req.Name, types.SetupKeyType(req.Type), expiresIn,
req.AutoGroups, req.UsageLimit, userID, ephemeral)
req.AutoGroups, req.UsageLimit, userID, ephemeral, allowExtraDNSLabels)
if err != nil {
util.WriteError(r.Context(), err, w)
return
@@ -237,19 +243,20 @@ func ToResponseBody(key *types.SetupKey) *api.SetupKey {
}
return &api.SetupKey{
Id: key.Id,
Key: key.KeySecret,
Name: key.Name,
Expires: key.GetExpiresAt(),
Type: string(key.Type),
Valid: key.IsValid(),
Revoked: key.Revoked,
UsedTimes: key.UsedTimes,
LastUsed: key.GetLastUsed(),
State: state,
AutoGroups: key.AutoGroups,
UpdatedAt: key.UpdatedAt,
UsageLimit: key.UsageLimit,
Ephemeral: key.Ephemeral,
Id: key.Id,
Key: key.KeySecret,
Name: key.Name,
Expires: key.GetExpiresAt(),
Type: string(key.Type),
Valid: key.IsValid(),
Revoked: key.Revoked,
UsedTimes: key.UsedTimes,
LastUsed: key.GetLastUsed(),
State: state,
AutoGroups: key.AutoGroups,
UpdatedAt: key.UpdatedAt,
UsageLimit: key.UsageLimit,
Ephemeral: key.Ephemeral,
AllowExtraDnsLabels: key.AllowExtraDNSLabels,
}
}
@@ -37,11 +37,12 @@ func initSetupKeysTestMetaData(defaultKey *types.SetupKey, newKey *types.SetupKe
return claims.AccountId, claims.UserId, nil
},
CreateSetupKeyFunc: func(_ context.Context, _ string, keyName string, typ types.SetupKeyType, _ time.Duration, _ []string,
_ int, _ string, ephemeral bool,
_ int, _ string, ephemeral bool, allowExtraDNSLabels bool,
) (*types.SetupKey, error) {
if keyName == newKey.Name || typ != newKey.Type {
nk := newKey.Copy()
nk.Ephemeral = ephemeral
nk.AllowExtraDNSLabels = allowExtraDNSLabels
return nk, nil
}
return nil, fmt.Errorf("failed creating setup key")
@@ -94,7 +95,7 @@ func TestSetupKeysHandlers(t *testing.T) {
adminUser := types.NewAdminUser("test_user")
newSetupKey, plainKey := types.GenerateSetupKey(newSetupKeyName, types.SetupKeyReusable, 0, []string{"group-1"},
types.SetupKeyUnlimitedUsage, true)
types.SetupKeyUnlimitedUsage, true, false)
newSetupKey.Key = plainKey
updatedDefaultSetupKey := defaultSetupKey.Copy()
updatedDefaultSetupKey.AutoGroups = []string{"group-1"}