mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-13 08:59:08 +02:00
[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:
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user