mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-12 19:59:56 +00:00
Compare commits
14 Commits
v0.64.2
...
deploy/sec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5a52711d7 | ||
|
|
b20d484972 | ||
|
|
8931293343 | ||
|
|
7b830d8f72 | ||
|
|
3a0cf230a1 | ||
|
|
0c990ab662 | ||
|
|
101c813e98 | ||
|
|
5333e55a81 | ||
|
|
81c11df103 | ||
|
|
f74bc48d16 | ||
|
|
0169e4540f | ||
|
|
cead3f38ee | ||
|
|
b55262d4a2 | ||
|
|
2248ff392f |
@@ -60,8 +60,8 @@
|
|||||||
|
|
||||||
https://github.com/user-attachments/assets/10cec749-bb56-4ab3-97af-4e38850108d2
|
https://github.com/user-attachments/assets/10cec749-bb56-4ab3-97af-4e38850108d2
|
||||||
|
|
||||||
### NetBird on Lawrence Systems (Video)
|
### Self-Host NetBird (Video)
|
||||||
[](https://www.youtube.com/watch?v=Kwrff6h0rEw)
|
[](https://youtu.be/bZAgpT6nzaQ)
|
||||||
|
|
||||||
### Key features
|
### Key features
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ type Options struct {
|
|||||||
StatePath string
|
StatePath string
|
||||||
// DisableClientRoutes disables the client routes
|
// DisableClientRoutes disables the client routes
|
||||||
DisableClientRoutes bool
|
DisableClientRoutes bool
|
||||||
|
// BlockInbound blocks all inbound connections from peers
|
||||||
|
BlockInbound bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateCredentials checks that exactly one credential type is provided
|
// validateCredentials checks that exactly one credential type is provided
|
||||||
@@ -137,6 +139,7 @@ func New(opts Options) (*Client, error) {
|
|||||||
PreSharedKey: &opts.PreSharedKey,
|
PreSharedKey: &opts.PreSharedKey,
|
||||||
DisableServerRoutes: &t,
|
DisableServerRoutes: &t,
|
||||||
DisableClientRoutes: &opts.DisableClientRoutes,
|
DisableClientRoutes: &opts.DisableClientRoutes,
|
||||||
|
BlockInbound: &opts.BlockInbound,
|
||||||
}
|
}
|
||||||
if opts.ConfigPath != "" {
|
if opts.ConfigPath != "" {
|
||||||
config, err = profilemanager.UpdateOrCreateConfig(input)
|
config, err = profilemanager.UpdateOrCreateConfig(input)
|
||||||
|
|||||||
@@ -558,7 +558,7 @@ func parseStatus(deviceName, ipcStr string) (*Stats, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
host, portStr, err := net.SplitHostPort(strings.Trim(val, "[]"))
|
host, portStr, err := net.SplitHostPort(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to parse endpoint: %v", err)
|
log.Errorf("failed to parse endpoint: %v", err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/google/gopacket"
|
|
||||||
"github.com/google/gopacket/layers"
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/pion/transport/v3"
|
"github.com/pion/transport/v3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -26,16 +24,6 @@ const (
|
|||||||
loopbackAddr = "127.0.0.1"
|
loopbackAddr = "127.0.0.1"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
localHostNetIPv4 = net.ParseIP("127.0.0.1")
|
|
||||||
localHostNetIPv6 = net.ParseIP("::1")
|
|
||||||
|
|
||||||
serializeOpts = gopacket.SerializeOptions{
|
|
||||||
ComputeChecksums: true,
|
|
||||||
FixLengths: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// WGEBPFProxy definition for proxy with EBPF support
|
// WGEBPFProxy definition for proxy with EBPF support
|
||||||
type WGEBPFProxy struct {
|
type WGEBPFProxy struct {
|
||||||
localWGListenPort int
|
localWGListenPort int
|
||||||
@@ -253,63 +241,3 @@ generatePort:
|
|||||||
}
|
}
|
||||||
return p.lastUsedPort, nil
|
return p.lastUsedPort, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *WGEBPFProxy) sendPkg(data []byte, endpointAddr *net.UDPAddr) error {
|
|
||||||
|
|
||||||
var ipH gopacket.SerializableLayer
|
|
||||||
var networkLayer gopacket.NetworkLayer
|
|
||||||
var dstIP net.IP
|
|
||||||
var rawConn net.PacketConn
|
|
||||||
|
|
||||||
if endpointAddr.IP.To4() != nil {
|
|
||||||
// IPv4 path
|
|
||||||
ipv4 := &layers.IPv4{
|
|
||||||
DstIP: localHostNetIPv4,
|
|
||||||
SrcIP: endpointAddr.IP,
|
|
||||||
Version: 4,
|
|
||||||
TTL: 64,
|
|
||||||
Protocol: layers.IPProtocolUDP,
|
|
||||||
}
|
|
||||||
ipH = ipv4
|
|
||||||
networkLayer = ipv4
|
|
||||||
dstIP = localHostNetIPv4
|
|
||||||
rawConn = p.rawConnIPv4
|
|
||||||
} else {
|
|
||||||
// IPv6 path
|
|
||||||
if p.rawConnIPv6 == nil {
|
|
||||||
return fmt.Errorf("IPv6 raw socket not available")
|
|
||||||
}
|
|
||||||
ipv6 := &layers.IPv6{
|
|
||||||
DstIP: localHostNetIPv6,
|
|
||||||
SrcIP: endpointAddr.IP,
|
|
||||||
Version: 6,
|
|
||||||
HopLimit: 64,
|
|
||||||
NextHeader: layers.IPProtocolUDP,
|
|
||||||
}
|
|
||||||
ipH = ipv6
|
|
||||||
networkLayer = ipv6
|
|
||||||
dstIP = localHostNetIPv6
|
|
||||||
rawConn = p.rawConnIPv6
|
|
||||||
}
|
|
||||||
|
|
||||||
udpH := &layers.UDP{
|
|
||||||
SrcPort: layers.UDPPort(endpointAddr.Port),
|
|
||||||
DstPort: layers.UDPPort(p.localWGListenPort),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := udpH.SetNetworkLayerForChecksum(networkLayer); err != nil {
|
|
||||||
return fmt.Errorf("set network layer for checksum: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
layerBuffer := gopacket.NewSerializeBuffer()
|
|
||||||
payload := gopacket.Payload(data)
|
|
||||||
|
|
||||||
if err := gopacket.SerializeLayers(layerBuffer, serializeOpts, ipH, udpH, payload); err != nil {
|
|
||||||
return fmt.Errorf("serialize layers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := rawConn.WriteTo(layerBuffer.Bytes(), &net.IPAddr{IP: dstIP}); err != nil {
|
|
||||||
return fmt.Errorf("write to raw conn: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,12 +10,89 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/google/gopacket"
|
||||||
|
"github.com/google/gopacket/layers"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bufsize"
|
"github.com/netbirdio/netbird/client/iface/bufsize"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgproxy/listener"
|
"github.com/netbirdio/netbird/client/iface/wgproxy/listener"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errIPv6ConnNotAvailable = errors.New("IPv6 endpoint but rawConnIPv6 is not available")
|
||||||
|
errIPv4ConnNotAvailable = errors.New("IPv4 endpoint but rawConnIPv4 is not available")
|
||||||
|
|
||||||
|
localHostNetIPv4 = net.ParseIP("127.0.0.1")
|
||||||
|
localHostNetIPv6 = net.ParseIP("::1")
|
||||||
|
|
||||||
|
serializeOpts = gopacket.SerializeOptions{
|
||||||
|
ComputeChecksums: true,
|
||||||
|
FixLengths: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// PacketHeaders holds pre-created headers and buffers for efficient packet sending
|
||||||
|
type PacketHeaders struct {
|
||||||
|
ipH gopacket.SerializableLayer
|
||||||
|
udpH *layers.UDP
|
||||||
|
layerBuffer gopacket.SerializeBuffer
|
||||||
|
localHostAddr net.IP
|
||||||
|
isIPv4 bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPacketHeaders(localWGListenPort int, endpoint *net.UDPAddr) (*PacketHeaders, error) {
|
||||||
|
var ipH gopacket.SerializableLayer
|
||||||
|
var networkLayer gopacket.NetworkLayer
|
||||||
|
var localHostAddr net.IP
|
||||||
|
var isIPv4 bool
|
||||||
|
|
||||||
|
// Check if source address is IPv4 or IPv6
|
||||||
|
if endpoint.IP.To4() != nil {
|
||||||
|
// IPv4 path
|
||||||
|
ipv4 := &layers.IPv4{
|
||||||
|
DstIP: localHostNetIPv4,
|
||||||
|
SrcIP: endpoint.IP,
|
||||||
|
Version: 4,
|
||||||
|
TTL: 64,
|
||||||
|
Protocol: layers.IPProtocolUDP,
|
||||||
|
}
|
||||||
|
ipH = ipv4
|
||||||
|
networkLayer = ipv4
|
||||||
|
localHostAddr = localHostNetIPv4
|
||||||
|
isIPv4 = true
|
||||||
|
} else {
|
||||||
|
// IPv6 path
|
||||||
|
ipv6 := &layers.IPv6{
|
||||||
|
DstIP: localHostNetIPv6,
|
||||||
|
SrcIP: endpoint.IP,
|
||||||
|
Version: 6,
|
||||||
|
HopLimit: 64,
|
||||||
|
NextHeader: layers.IPProtocolUDP,
|
||||||
|
}
|
||||||
|
ipH = ipv6
|
||||||
|
networkLayer = ipv6
|
||||||
|
localHostAddr = localHostNetIPv6
|
||||||
|
isIPv4 = false
|
||||||
|
}
|
||||||
|
|
||||||
|
udpH := &layers.UDP{
|
||||||
|
SrcPort: layers.UDPPort(endpoint.Port),
|
||||||
|
DstPort: layers.UDPPort(localWGListenPort),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := udpH.SetNetworkLayerForChecksum(networkLayer); err != nil {
|
||||||
|
return nil, fmt.Errorf("set network layer for checksum: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PacketHeaders{
|
||||||
|
ipH: ipH,
|
||||||
|
udpH: udpH,
|
||||||
|
layerBuffer: gopacket.NewSerializeBuffer(),
|
||||||
|
localHostAddr: localHostAddr,
|
||||||
|
isIPv4: isIPv4,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProxyWrapper help to keep the remoteConn instance for net.Conn.Close function call
|
// ProxyWrapper help to keep the remoteConn instance for net.Conn.Close function call
|
||||||
type ProxyWrapper struct {
|
type ProxyWrapper struct {
|
||||||
wgeBPFProxy *WGEBPFProxy
|
wgeBPFProxy *WGEBPFProxy
|
||||||
@@ -24,8 +101,10 @@ type ProxyWrapper struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
wgRelayedEndpointAddr *net.UDPAddr
|
wgRelayedEndpointAddr *net.UDPAddr
|
||||||
wgEndpointCurrentUsedAddr *net.UDPAddr
|
headers *PacketHeaders
|
||||||
|
headerCurrentUsed *PacketHeaders
|
||||||
|
rawConn net.PacketConn
|
||||||
|
|
||||||
paused bool
|
paused bool
|
||||||
pausedCond *sync.Cond
|
pausedCond *sync.Cond
|
||||||
@@ -41,15 +120,32 @@ func NewProxyWrapper(proxy *WGEBPFProxy) *ProxyWrapper {
|
|||||||
closeListener: listener.NewCloseListener(),
|
closeListener: listener.NewCloseListener(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProxyWrapper) AddTurnConn(ctx context.Context, _ *net.UDPAddr, remoteConn net.Conn) error {
|
func (p *ProxyWrapper) AddTurnConn(ctx context.Context, _ *net.UDPAddr, remoteConn net.Conn) error {
|
||||||
addr, err := p.wgeBPFProxy.AddTurnConn(remoteConn)
|
addr, err := p.wgeBPFProxy.AddTurnConn(remoteConn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add turn conn: %w", err)
|
return fmt.Errorf("add turn conn: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
headers, err := NewPacketHeaders(p.wgeBPFProxy.localWGListenPort, addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create packet sender: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if required raw connection is available
|
||||||
|
if !headers.isIPv4 && p.wgeBPFProxy.rawConnIPv6 == nil {
|
||||||
|
return errIPv6ConnNotAvailable
|
||||||
|
}
|
||||||
|
if headers.isIPv4 && p.wgeBPFProxy.rawConnIPv4 == nil {
|
||||||
|
return errIPv4ConnNotAvailable
|
||||||
|
}
|
||||||
|
|
||||||
p.remoteConn = remoteConn
|
p.remoteConn = remoteConn
|
||||||
p.ctx, p.cancel = context.WithCancel(ctx)
|
p.ctx, p.cancel = context.WithCancel(ctx)
|
||||||
p.wgRelayedEndpointAddr = addr
|
p.wgRelayedEndpointAddr = addr
|
||||||
return err
|
p.headers = headers
|
||||||
|
p.rawConn = p.selectRawConn(headers)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProxyWrapper) EndpointAddr() *net.UDPAddr {
|
func (p *ProxyWrapper) EndpointAddr() *net.UDPAddr {
|
||||||
@@ -68,7 +164,8 @@ func (p *ProxyWrapper) Work() {
|
|||||||
p.pausedCond.L.Lock()
|
p.pausedCond.L.Lock()
|
||||||
p.paused = false
|
p.paused = false
|
||||||
|
|
||||||
p.wgEndpointCurrentUsedAddr = p.wgRelayedEndpointAddr
|
p.headerCurrentUsed = p.headers
|
||||||
|
p.rawConn = p.selectRawConn(p.headerCurrentUsed)
|
||||||
|
|
||||||
if !p.isStarted {
|
if !p.isStarted {
|
||||||
p.isStarted = true
|
p.isStarted = true
|
||||||
@@ -95,10 +192,28 @@ func (p *ProxyWrapper) RedirectAs(endpoint *net.UDPAddr) {
|
|||||||
log.Errorf("failed to start package redirection, endpoint is nil")
|
log.Errorf("failed to start package redirection, endpoint is nil")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header, err := NewPacketHeaders(p.wgeBPFProxy.localWGListenPort, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create packet headers: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if required raw connection is available
|
||||||
|
if !header.isIPv4 && p.wgeBPFProxy.rawConnIPv6 == nil {
|
||||||
|
log.Error(errIPv6ConnNotAvailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if header.isIPv4 && p.wgeBPFProxy.rawConnIPv4 == nil {
|
||||||
|
log.Error(errIPv4ConnNotAvailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
p.pausedCond.L.Lock()
|
p.pausedCond.L.Lock()
|
||||||
p.paused = false
|
p.paused = false
|
||||||
|
|
||||||
p.wgEndpointCurrentUsedAddr = endpoint
|
p.headerCurrentUsed = header
|
||||||
|
p.rawConn = p.selectRawConn(header)
|
||||||
|
|
||||||
p.pausedCond.Signal()
|
p.pausedCond.Signal()
|
||||||
p.pausedCond.L.Unlock()
|
p.pausedCond.L.Unlock()
|
||||||
@@ -140,7 +255,7 @@ func (p *ProxyWrapper) proxyToLocal(ctx context.Context) {
|
|||||||
p.pausedCond.Wait()
|
p.pausedCond.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.wgeBPFProxy.sendPkg(buf[:n], p.wgEndpointCurrentUsedAddr)
|
err = p.sendPkg(buf[:n], p.headerCurrentUsed)
|
||||||
p.pausedCond.L.Unlock()
|
p.pausedCond.L.Unlock()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -166,3 +281,29 @@ func (p *ProxyWrapper) readFromRemote(ctx context.Context, buf []byte) (int, err
|
|||||||
}
|
}
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *ProxyWrapper) sendPkg(data []byte, header *PacketHeaders) error {
|
||||||
|
defer func() {
|
||||||
|
if err := header.layerBuffer.Clear(); err != nil {
|
||||||
|
log.Errorf("failed to clear layer buffer: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
payload := gopacket.Payload(data)
|
||||||
|
|
||||||
|
if err := gopacket.SerializeLayers(header.layerBuffer, serializeOpts, header.ipH, header.udpH, payload); err != nil {
|
||||||
|
return fmt.Errorf("serialize layers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := p.rawConn.WriteTo(header.layerBuffer.Bytes(), &net.IPAddr{IP: header.localHostAddr}); err != nil {
|
||||||
|
return fmt.Errorf("write to raw conn: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProxyWrapper) selectRawConn(header *PacketHeaders) net.PacketConn {
|
||||||
|
if header.isIPv4 {
|
||||||
|
return p.wgeBPFProxy.rawConnIPv4
|
||||||
|
}
|
||||||
|
return p.wgeBPFProxy.rawConnIPv6
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,6 +112,54 @@ func TestHandlerChain_ServeDNS_DomainMatching(t *testing.T) {
|
|||||||
matchSubdomains: false,
|
matchSubdomains: false,
|
||||||
shouldMatch: false,
|
shouldMatch: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "single letter TLD exact match",
|
||||||
|
handlerDomain: "example.x.",
|
||||||
|
queryDomain: "example.x.",
|
||||||
|
isWildcard: false,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single letter TLD subdomain match",
|
||||||
|
handlerDomain: "example.x.",
|
||||||
|
queryDomain: "sub.example.x.",
|
||||||
|
isWildcard: false,
|
||||||
|
matchSubdomains: true,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single letter TLD wildcard match",
|
||||||
|
handlerDomain: "*.example.x.",
|
||||||
|
queryDomain: "sub.example.x.",
|
||||||
|
isWildcard: true,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two letter domain labels",
|
||||||
|
handlerDomain: "a.b.",
|
||||||
|
queryDomain: "a.b.",
|
||||||
|
isWildcard: false,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single character domain",
|
||||||
|
handlerDomain: "x.",
|
||||||
|
queryDomain: "x.",
|
||||||
|
isWildcard: false,
|
||||||
|
matchSubdomains: false,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single character domain with subdomain match",
|
||||||
|
handlerDomain: "x.",
|
||||||
|
queryDomain: "sub.x.",
|
||||||
|
isWildcard: false,
|
||||||
|
matchSubdomains: true,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
@@ -38,6 +40,9 @@ const (
|
|||||||
type systemConfigurator struct {
|
type systemConfigurator struct {
|
||||||
createdKeys map[string]struct{}
|
createdKeys map[string]struct{}
|
||||||
systemDNSSettings SystemDNSSettings
|
systemDNSSettings SystemDNSSettings
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
origNameservers []netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHostManager() (*systemConfigurator, error) {
|
func newHostManager() (*systemConfigurator, error) {
|
||||||
@@ -218,6 +223,7 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dnsSettings SystemDNSSettings
|
var dnsSettings SystemDNSSettings
|
||||||
|
var serverAddresses []netip.Addr
|
||||||
inSearchDomainsArray := false
|
inSearchDomainsArray := false
|
||||||
inServerAddressesArray := false
|
inServerAddressesArray := false
|
||||||
|
|
||||||
@@ -244,9 +250,12 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) {
|
|||||||
dnsSettings.Domains = append(dnsSettings.Domains, searchDomain)
|
dnsSettings.Domains = append(dnsSettings.Domains, searchDomain)
|
||||||
} else if inServerAddressesArray {
|
} else if inServerAddressesArray {
|
||||||
address := strings.Split(line, " : ")[1]
|
address := strings.Split(line, " : ")[1]
|
||||||
if ip, err := netip.ParseAddr(address); err == nil && ip.Is4() {
|
if ip, err := netip.ParseAddr(address); err == nil && !ip.IsUnspecified() {
|
||||||
dnsSettings.ServerIP = ip.Unmap()
|
ip = ip.Unmap()
|
||||||
inServerAddressesArray = false // Stop reading after finding the first IPv4 address
|
serverAddresses = append(serverAddresses, ip)
|
||||||
|
if !dnsSettings.ServerIP.IsValid() && ip.Is4() {
|
||||||
|
dnsSettings.ServerIP = ip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,9 +267,19 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) {
|
|||||||
// default to 53 port
|
// default to 53 port
|
||||||
dnsSettings.ServerPort = DefaultPort
|
dnsSettings.ServerPort = DefaultPort
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
s.origNameservers = serverAddresses
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
return dnsSettings, nil
|
return dnsSettings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *systemConfigurator) getOriginalNameservers() []netip.Addr {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return slices.Clone(s.origNameservers)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *systemConfigurator) addSearchDomains(key, domains string, ip netip.Addr, port int) error {
|
func (s *systemConfigurator) addSearchDomains(key, domains string, ip netip.Addr, port int) error {
|
||||||
err := s.addDNSState(key, domains, ip, port, true)
|
err := s.addDNSState(key, domains, ip, port, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -109,3 +109,169 @@ func removeTestDNSKey(key string) error {
|
|||||||
_, err := cmd.CombinedOutput()
|
_, err := cmd.CombinedOutput()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetOriginalNameservers(t *testing.T) {
|
||||||
|
configurator := &systemConfigurator{
|
||||||
|
createdKeys: make(map[string]struct{}),
|
||||||
|
origNameservers: []netip.Addr{
|
||||||
|
netip.MustParseAddr("8.8.8.8"),
|
||||||
|
netip.MustParseAddr("1.1.1.1"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
servers := configurator.getOriginalNameservers()
|
||||||
|
assert.Len(t, servers, 2)
|
||||||
|
assert.Equal(t, netip.MustParseAddr("8.8.8.8"), servers[0])
|
||||||
|
assert.Equal(t, netip.MustParseAddr("1.1.1.1"), servers[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOriginalNameserversFromSystem(t *testing.T) {
|
||||||
|
configurator := &systemConfigurator{
|
||||||
|
createdKeys: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := configurator.getSystemDNSSettings()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
servers := configurator.getOriginalNameservers()
|
||||||
|
|
||||||
|
require.NotEmpty(t, servers, "expected at least one DNS server from system configuration")
|
||||||
|
|
||||||
|
for _, server := range servers {
|
||||||
|
assert.True(t, server.IsValid(), "server address should be valid")
|
||||||
|
assert.False(t, server.IsUnspecified(), "server address should not be unspecified")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("found %d original nameservers: %v", len(servers), servers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTestConfigurator(t *testing.T) (*systemConfigurator, *statemanager.Manager, func()) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
stateFile := filepath.Join(tmpDir, "state.json")
|
||||||
|
sm := statemanager.New(stateFile)
|
||||||
|
sm.RegisterState(&ShutdownState{})
|
||||||
|
sm.Start()
|
||||||
|
|
||||||
|
configurator := &systemConfigurator{
|
||||||
|
createdKeys: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
searchKey := getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix)
|
||||||
|
matchKey := getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)
|
||||||
|
localKey := getKeyWithInput(netbirdDNSStateKeyFormat, localSuffix)
|
||||||
|
|
||||||
|
cleanup := func() {
|
||||||
|
_ = sm.Stop(context.Background())
|
||||||
|
for _, key := range []string{searchKey, matchKey, localKey} {
|
||||||
|
_ = removeTestDNSKey(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configurator, sm, cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOriginalNameserversNoTransition(t *testing.T) {
|
||||||
|
netbirdIP := netip.MustParseAddr("100.64.0.1")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
routeAll bool
|
||||||
|
}{
|
||||||
|
{"routeall_false", false},
|
||||||
|
{"routeall_true", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
configurator, sm, cleanup := setupTestConfigurator(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
_, err := configurator.getSystemDNSSettings()
|
||||||
|
require.NoError(t, err)
|
||||||
|
initialServers := configurator.getOriginalNameservers()
|
||||||
|
t.Logf("Initial servers: %v", initialServers)
|
||||||
|
require.NotEmpty(t, initialServers)
|
||||||
|
|
||||||
|
for _, srv := range initialServers {
|
||||||
|
require.NotEqual(t, netbirdIP, srv, "initial servers should not contain NetBird IP")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := HostDNSConfig{
|
||||||
|
ServerIP: netbirdIP,
|
||||||
|
ServerPort: 53,
|
||||||
|
RouteAll: tc.routeAll,
|
||||||
|
Domains: []DomainConfig{{Domain: "example.com", MatchOnly: true}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i <= 2; i++ {
|
||||||
|
err = configurator.applyDNSConfig(config, sm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
servers := configurator.getOriginalNameservers()
|
||||||
|
t.Logf("After apply %d (RouteAll=%v): %v", i, tc.routeAll, servers)
|
||||||
|
assert.Equal(t, initialServers, servers)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOriginalNameserversRouteAllTransition(t *testing.T) {
|
||||||
|
netbirdIP := netip.MustParseAddr("100.64.0.1")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
initialRoute bool
|
||||||
|
}{
|
||||||
|
{"start_with_routeall_false", false},
|
||||||
|
{"start_with_routeall_true", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
configurator, sm, cleanup := setupTestConfigurator(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
_, err := configurator.getSystemDNSSettings()
|
||||||
|
require.NoError(t, err)
|
||||||
|
initialServers := configurator.getOriginalNameservers()
|
||||||
|
t.Logf("Initial servers: %v", initialServers)
|
||||||
|
require.NotEmpty(t, initialServers)
|
||||||
|
|
||||||
|
config := HostDNSConfig{
|
||||||
|
ServerIP: netbirdIP,
|
||||||
|
ServerPort: 53,
|
||||||
|
RouteAll: tc.initialRoute,
|
||||||
|
Domains: []DomainConfig{{Domain: "example.com", MatchOnly: true}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// First apply
|
||||||
|
err = configurator.applyDNSConfig(config, sm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
servers := configurator.getOriginalNameservers()
|
||||||
|
t.Logf("After first apply (RouteAll=%v): %v", tc.initialRoute, servers)
|
||||||
|
assert.Equal(t, initialServers, servers)
|
||||||
|
|
||||||
|
// Toggle RouteAll
|
||||||
|
config.RouteAll = !tc.initialRoute
|
||||||
|
err = configurator.applyDNSConfig(config, sm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
servers = configurator.getOriginalNameservers()
|
||||||
|
t.Logf("After toggle (RouteAll=%v): %v", config.RouteAll, servers)
|
||||||
|
assert.Equal(t, initialServers, servers)
|
||||||
|
|
||||||
|
// Toggle back
|
||||||
|
config.RouteAll = tc.initialRoute
|
||||||
|
err = configurator.applyDNSConfig(config, sm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
servers = configurator.getOriginalNameservers()
|
||||||
|
t.Logf("After toggle back (RouteAll=%v): %v", config.RouteAll, servers)
|
||||||
|
assert.Equal(t, initialServers, servers)
|
||||||
|
|
||||||
|
for _, srv := range servers {
|
||||||
|
assert.NotEqual(t, netbirdIP, srv, "servers should not contain NetBird IP")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -615,7 +615,7 @@ func (s *DefaultServer) applyHostConfig() {
|
|||||||
s.registerFallback(config)
|
s.registerFallback(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerFallback registers original nameservers as low-priority fallback handlers
|
// registerFallback registers original nameservers as low-priority fallback handlers.
|
||||||
func (s *DefaultServer) registerFallback(config HostDNSConfig) {
|
func (s *DefaultServer) registerFallback(config HostDNSConfig) {
|
||||||
hostMgrWithNS, ok := s.hostManager.(hostManagerWithOriginalNS)
|
hostMgrWithNS, ok := s.hostManager.(hostManagerWithOriginalNS)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -624,6 +624,7 @@ func (s *DefaultServer) registerFallback(config HostDNSConfig) {
|
|||||||
|
|
||||||
originalNameservers := hostMgrWithNS.getOriginalNameservers()
|
originalNameservers := hostMgrWithNS.getOriginalNameservers()
|
||||||
if len(originalNameservers) == 0 {
|
if len(originalNameservers) == 0 {
|
||||||
|
s.deregisterHandler([]string{nbdns.RootZone}, PriorityFallback)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,21 @@ import (
|
|||||||
|
|
||||||
type MockResponseWriter struct {
|
type MockResponseWriter struct {
|
||||||
WriteMsgFunc func(m *dns.Msg) error
|
WriteMsgFunc func(m *dns.Msg) error
|
||||||
|
lastResponse *dns.Msg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rw *MockResponseWriter) WriteMsg(m *dns.Msg) error {
|
func (rw *MockResponseWriter) WriteMsg(m *dns.Msg) error {
|
||||||
|
rw.lastResponse = m
|
||||||
if rw.WriteMsgFunc != nil {
|
if rw.WriteMsgFunc != nil {
|
||||||
return rw.WriteMsgFunc(m)
|
return rw.WriteMsgFunc(m)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rw *MockResponseWriter) GetLastResponse() *dns.Msg {
|
||||||
|
return rw.lastResponse
|
||||||
|
}
|
||||||
|
|
||||||
func (rw *MockResponseWriter) LocalAddr() net.Addr { return nil }
|
func (rw *MockResponseWriter) LocalAddr() net.Addr { return nil }
|
||||||
func (rw *MockResponseWriter) RemoteAddr() net.Addr { return nil }
|
func (rw *MockResponseWriter) RemoteAddr() net.Addr { return nil }
|
||||||
func (rw *MockResponseWriter) Write([]byte) (int, error) { return 0, nil }
|
func (rw *MockResponseWriter) Write([]byte) (int, error) { return 0, nil }
|
||||||
|
|||||||
@@ -573,9 +573,11 @@ func (e *Engine) createFirewall() error {
|
|||||||
|
|
||||||
var err error
|
var err error
|
||||||
e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager, e.flowManager.GetLogger(), e.config.DisableServerRoutes, e.config.MTU)
|
e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager, e.flowManager.GetLogger(), e.config.DisableServerRoutes, e.config.MTU)
|
||||||
if err != nil || e.firewall == nil {
|
if err != nil {
|
||||||
log.Errorf("failed creating firewall manager: %s", err)
|
return fmt.Errorf("create firewall manager: %w", err)
|
||||||
return nil
|
}
|
||||||
|
if e.firewall == nil {
|
||||||
|
return fmt.Errorf("create firewall manager: received nil manager")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := e.initFirewall(); err != nil {
|
if err := e.initFirewall(); err != nil {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,6 +38,11 @@ func New() *NetworkMonitor {
|
|||||||
|
|
||||||
// Listen begins monitoring network changes. When a change is detected, this function will return without error.
|
// Listen begins monitoring network changes. When a change is detected, this function will return without error.
|
||||||
func (nw *NetworkMonitor) Listen(ctx context.Context) (err error) {
|
func (nw *NetworkMonitor) Listen(ctx context.Context) (err error) {
|
||||||
|
if netstack.IsEnabled() {
|
||||||
|
log.Debugf("Network monitor: skipping in netstack mode")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
nw.mu.Lock()
|
nw.mu.Lock()
|
||||||
if nw.cancel != nil {
|
if nw.cancel != nil {
|
||||||
nw.mu.Unlock()
|
nw.mu.Unlock()
|
||||||
|
|||||||
@@ -390,6 +390,8 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn.Log.Infof("configure WireGuard endpoint to: %s", ep.String())
|
conn.Log.Infof("configure WireGuard endpoint to: %s", ep.String())
|
||||||
|
conn.enableWgWatcherIfNeeded()
|
||||||
|
|
||||||
presharedKey := conn.presharedKey(iceConnInfo.RosenpassPubKey)
|
presharedKey := conn.presharedKey(iceConnInfo.RosenpassPubKey)
|
||||||
if err = conn.endpointUpdater.ConfigureWGEndpoint(ep, presharedKey); err != nil {
|
if err = conn.endpointUpdater.ConfigureWGEndpoint(ep, presharedKey); err != nil {
|
||||||
conn.handleConfigurationFailure(err, wgProxy)
|
conn.handleConfigurationFailure(err, wgProxy)
|
||||||
@@ -402,8 +404,6 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn
|
|||||||
conn.wgProxyRelay.RedirectAs(ep)
|
conn.wgProxyRelay.RedirectAs(ep)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.enableWgWatcherIfNeeded()
|
|
||||||
|
|
||||||
conn.currentConnPriority = priority
|
conn.currentConnPriority = priority
|
||||||
conn.statusICE.SetConnected()
|
conn.statusICE.SetConnected()
|
||||||
conn.updateIceState(iceConnInfo)
|
conn.updateIceState(iceConnInfo)
|
||||||
@@ -501,6 +501,9 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
|
|||||||
|
|
||||||
wgProxy.Work()
|
wgProxy.Work()
|
||||||
presharedKey := conn.presharedKey(rci.rosenpassPubKey)
|
presharedKey := conn.presharedKey(rci.rosenpassPubKey)
|
||||||
|
|
||||||
|
conn.enableWgWatcherIfNeeded()
|
||||||
|
|
||||||
if err := conn.endpointUpdater.ConfigureWGEndpoint(wgProxy.EndpointAddr(), presharedKey); err != nil {
|
if err := conn.endpointUpdater.ConfigureWGEndpoint(wgProxy.EndpointAddr(), presharedKey); err != nil {
|
||||||
if err := wgProxy.CloseConn(); err != nil {
|
if err := wgProxy.CloseConn(); err != nil {
|
||||||
conn.Log.Warnf("Failed to close relay connection: %v", err)
|
conn.Log.Warnf("Failed to close relay connection: %v", err)
|
||||||
@@ -509,8 +512,6 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.enableWgWatcherIfNeeded()
|
|
||||||
|
|
||||||
wgConfigWorkaround()
|
wgConfigWorkaround()
|
||||||
conn.rosenpassRemoteKey = rci.rosenpassPubKey
|
conn.rosenpassRemoteKey = rci.rosenpassPubKey
|
||||||
conn.currentConnPriority = conntype.Relay
|
conn.currentConnPriority = conntype.Relay
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WGIfaceMonitor monitors the WireGuard interface lifecycle and restarts the engine
|
// WGIfaceMonitor monitors the WireGuard interface lifecycle and restarts the engine
|
||||||
@@ -35,6 +37,11 @@ func (m *WGIfaceMonitor) Start(ctx context.Context, ifaceName string) (shouldRes
|
|||||||
return false, errors.New("not supported on mobile platforms")
|
return false, errors.New("not supported on mobile platforms")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if netstack.IsEnabled() {
|
||||||
|
log.Debugf("Interface monitor: skipped in netstack mode")
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
if ifaceName == "" {
|
if ifaceName == "" {
|
||||||
log.Debugf("Interface monitor: empty interface name, skipping monitor")
|
log.Debugf("Interface monitor: empty interface name, skipping monitor")
|
||||||
return false, errors.New("empty interface name")
|
return false, errors.New("empty interface name")
|
||||||
|
|||||||
@@ -327,6 +327,60 @@ func ensureLocalConnector(ctx context.Context, stor storage.Storage) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasNonLocalConnectors checks if there are any connectors other than the local connector.
|
||||||
|
func (p *Provider) HasNonLocalConnectors(ctx context.Context) (bool, error) {
|
||||||
|
connectors, err := p.storage.ListConnectors(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to list connectors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.logger.Info("checking for non-local connectors", "total_connectors", len(connectors))
|
||||||
|
for _, conn := range connectors {
|
||||||
|
p.logger.Info("found connector in storage", "id", conn.ID, "type", conn.Type, "name", conn.Name)
|
||||||
|
if conn.ID != "local" || conn.Type != "local" {
|
||||||
|
p.logger.Info("found non-local connector", "id", conn.ID)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.logger.Info("no non-local connectors found")
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableLocalAuth removes the local (password) connector.
|
||||||
|
// Returns an error if no other connectors are configured.
|
||||||
|
func (p *Provider) DisableLocalAuth(ctx context.Context) error {
|
||||||
|
hasOthers, err := p.HasNonLocalConnectors(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !hasOthers {
|
||||||
|
return fmt.Errorf("cannot disable local authentication: no other identity providers configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if local connector exists
|
||||||
|
_, err = p.storage.GetConnector(ctx, "local")
|
||||||
|
if errors.Is(err, storage.ErrNotFound) {
|
||||||
|
// Already disabled
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check local connector: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the local connector
|
||||||
|
if err := p.storage.DeleteConnector(ctx, "local"); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete local connector: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.logger.Info("local authentication disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableLocalAuth creates the local (password) connector if it doesn't exist.
|
||||||
|
func (p *Provider) EnableLocalAuth(ctx context.Context) error {
|
||||||
|
return ensureLocalConnector(ctx, p.storage)
|
||||||
|
}
|
||||||
|
|
||||||
// ensureStaticConnectors creates or updates static connectors in storage
|
// ensureStaticConnectors creates or updates static connectors in storage
|
||||||
func ensureStaticConnectors(ctx context.Context, stor storage.Storage, connectors []Connector) error {
|
func ensureStaticConnectors(ctx context.Context, stor storage.Storage, connectors []Connector) error {
|
||||||
for _, conn := range connectors {
|
for _, conn := range connectors {
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/formatter/hook"
|
"github.com/netbirdio/netbird/formatter/hook"
|
||||||
"github.com/netbirdio/netbird/management/internals/server"
|
"github.com/netbirdio/netbird/management/internals/server"
|
||||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
|
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
"github.com/netbirdio/netbird/util/crypt"
|
"github.com/netbirdio/netbird/util/crypt"
|
||||||
)
|
)
|
||||||
@@ -78,9 +78,8 @@ var (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, valid := dns.IsDomainName(dnsDomain)
|
if !nbdomain.IsValidDomainNoWildcard(dnsDomain) {
|
||||||
if !valid || len(dnsDomain) > 192 {
|
return fmt.Errorf("invalid dns-domain: %s", dnsDomain)
|
||||||
return fmt.Errorf("failed parsing the provided dns-domain. Valid status: %t, Length: %d", valid, len(dnsDomain))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -187,10 +187,10 @@ func (e *EphemeralManager) cleanup(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for accountID, peerIDs := range peerIDsPerAccount {
|
for accountID, peerIDs := range peerIDsPerAccount {
|
||||||
log.WithContext(ctx).Debugf("delete ephemeral peers for account: %s", accountID)
|
log.WithContext(ctx).Tracef("cleanup: deleting %d ephemeral peers for account %s", len(peerIDs), accountID)
|
||||||
err := e.peersManager.DeletePeers(ctx, accountID, peerIDs, activity.SystemInitiator, true)
|
err := e.peersManager.DeletePeers(ctx, accountID, peerIDs, activity.SystemInitiator, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithContext(ctx).Errorf("failed to delete ephemeral peer: %s", err)
|
log.WithContext(ctx).Errorf("failed to delete ephemeral peers: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,10 +108,19 @@ func (m *managerImpl) DeletePeers(ctx context.Context, accountID string, peerIDs
|
|||||||
err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||||
peer, err := transaction.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID)
|
peer, err := transaction.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if e, ok := status.FromError(err); ok && e.Type() == status.NotFound {
|
||||||
|
log.WithContext(ctx).Tracef("DeletePeers: peer %s not found, skipping", peerID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if checkConnected && (peer.Status.Connected || peer.Status.LastSeen.After(time.Now().Add(-(ephemeral.EphemeralLifeTime - 10*time.Second)))) {
|
if checkConnected && (peer.Status.Connected || peer.Status.LastSeen.After(time.Now().Add(-(ephemeral.EphemeralLifeTime - 10*time.Second)))) {
|
||||||
|
log.WithContext(ctx).Tracef("DeletePeers: peer %s skipped (connected=%t, lastSeen=%s, threshold=%s, ephemeral=%t)",
|
||||||
|
peerID, peer.Status.Connected,
|
||||||
|
peer.Status.LastSeen.Format(time.RFC3339),
|
||||||
|
time.Now().Add(-(ephemeral.EphemeralLifeTime - 10*time.Second)).Format(time.RFC3339),
|
||||||
|
peer.Ephemeral)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +159,8 @@ func (m *managerImpl) DeletePeers(ctx context.Context, accountID string, peerIDs
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.WithContext(ctx).Errorf("DeletePeers: failed to delete peer %s: %v", peerID, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.integratedPeerValidator != nil {
|
if m.integratedPeerValidator != nil {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/util"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ func (r *Record) Validate() error {
|
|||||||
return errors.New("record name is required")
|
return errors.New("record name is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !util.IsValidDomain(r.Name) {
|
if !domain.IsValidDomain(r.Name) {
|
||||||
return errors.New("invalid record name format")
|
return errors.New("invalid record name format")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +81,8 @@ func (r *Record) Validate() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case RecordTypeCNAME:
|
case RecordTypeCNAME:
|
||||||
if !util.IsValidDomain(r.Content) {
|
if !domain.IsValidDomainNoWildcard(r.Content) {
|
||||||
return errors.New("invalid CNAME record format")
|
return errors.New("invalid CNAME target format")
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return errors.New("invalid record type, must be A, AAAA, or CNAME")
|
return errors.New("invalid record type, must be A, AAAA, or CNAME")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
|
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
|
||||||
"github.com/netbirdio/netbird/management/server/util"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ func (z *Zone) Validate() error {
|
|||||||
return errors.New("zone name exceeds maximum length of 255 characters")
|
return errors.New("zone name exceeds maximum length of 255 characters")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !util.IsValidDomain(z.Domain) {
|
if !domain.IsValidDomainNoWildcard(z.Domain) {
|
||||||
return errors.New("invalid zone domain format")
|
return errors.New("invalid zone domain format")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ func (s *BaseServer) ProxyController() port_forwarding.Controller {
|
|||||||
|
|
||||||
func (s *BaseServer) SecretsManager() grpc.SecretsManager {
|
func (s *BaseServer) SecretsManager() grpc.SecretsManager {
|
||||||
return Create(s, func() grpc.SecretsManager {
|
return Create(s, func() grpc.SecretsManager {
|
||||||
|
log.Debugf("Initializing secrets manager")
|
||||||
secretsManager, err := grpc.NewTimeBasedAuthSecretsManager(s.PeersUpdateManager(), s.Config.TURNConfig, s.Config.Relay, s.SettingsManager(), s.GroupsManager())
|
secretsManager, err := grpc.NewTimeBasedAuthSecretsManager(s.PeersUpdateManager(), s.Config.TURNConfig, s.Config.Relay, s.SettingsManager(), s.GroupsManager())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create secrets manager: %v", err)
|
log.Fatalf("failed to create secrets manager: %v", err)
|
||||||
|
|||||||
@@ -69,7 +69,14 @@ func (s *BaseServer) UsersManager() users.Manager {
|
|||||||
func (s *BaseServer) SettingsManager() settings.Manager {
|
func (s *BaseServer) SettingsManager() settings.Manager {
|
||||||
return Create(s, func() settings.Manager {
|
return Create(s, func() settings.Manager {
|
||||||
extraSettingsManager := integrations.NewManager(s.EventStore())
|
extraSettingsManager := integrations.NewManager(s.EventStore())
|
||||||
return settings.NewManager(s.Store(), s.UsersManager(), extraSettingsManager, s.PermissionsManager())
|
|
||||||
|
idpConfig := settings.IdpConfig{}
|
||||||
|
if s.Config.EmbeddedIdP != nil && s.Config.EmbeddedIdP.Enabled {
|
||||||
|
idpConfig.EmbeddedIdpEnabled = true
|
||||||
|
idpConfig.LocalAuthDisabled = s.Config.EmbeddedIdP.LocalAuthDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings.NewManager(s.Store(), s.UsersManager(), extraSettingsManager, s.PermissionsManager(), idpConfig)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ import (
|
|||||||
pb "github.com/golang/protobuf/proto" // nolint
|
pb "github.com/golang/protobuf/proto" // nolint
|
||||||
"github.com/golang/protobuf/ptypes/timestamp"
|
"github.com/golang/protobuf/ptypes/timestamp"
|
||||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
|
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
|
||||||
"github.com/netbirdio/netbird/shared/management/client/common"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/peer"
|
"google.golang.org/grpc/peer"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/shared/management/client/common"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
"github.com/netbirdio/netbird/management/server/idp"
|
"github.com/netbirdio/netbird/management/server/idp"
|
||||||
@@ -76,8 +77,9 @@ type Server struct {
|
|||||||
|
|
||||||
oAuthConfigProvider idp.OAuthConfigProvider
|
oAuthConfigProvider idp.OAuthConfigProvider
|
||||||
|
|
||||||
syncSem atomic.Int32
|
syncSem atomic.Int32
|
||||||
syncLim int32
|
syncLimEnabled bool
|
||||||
|
syncLim int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new Management server
|
// NewServer creates a new Management server
|
||||||
@@ -107,6 +109,7 @@ func NewServer(
|
|||||||
blockPeersWithSameConfig := strings.ToLower(os.Getenv(envBlockPeers)) == "true"
|
blockPeersWithSameConfig := strings.ToLower(os.Getenv(envBlockPeers)) == "true"
|
||||||
|
|
||||||
syncLim := int32(defaultSyncLim)
|
syncLim := int32(defaultSyncLim)
|
||||||
|
syncLimEnabled := true
|
||||||
if syncLimStr := os.Getenv(envConcurrentSyncs); syncLimStr != "" {
|
if syncLimStr := os.Getenv(envConcurrentSyncs); syncLimStr != "" {
|
||||||
syncLimParsed, err := strconv.Atoi(syncLimStr)
|
syncLimParsed, err := strconv.Atoi(syncLimStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -114,6 +117,9 @@ func NewServer(
|
|||||||
} else {
|
} else {
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
syncLim = int32(syncLimParsed)
|
syncLim = int32(syncLimParsed)
|
||||||
|
if syncLim < 0 {
|
||||||
|
syncLimEnabled = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +139,8 @@ func NewServer(
|
|||||||
|
|
||||||
loginFilter: newLoginFilter(),
|
loginFilter: newLoginFilter(),
|
||||||
|
|
||||||
syncLim: syncLim,
|
syncLim: syncLim,
|
||||||
|
syncLimEnabled: syncLimEnabled,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +218,7 @@ func (s *Server) Job(srv proto.ManagementService_JobServer) error {
|
|||||||
// Sync validates the existence of a connecting peer, sends an initial state (all available for the connecting peers) and
|
// Sync validates the existence of a connecting peer, sends an initial state (all available for the connecting peers) and
|
||||||
// notifies the connected peer of any updates (e.g. new peers under the same account)
|
// notifies the connected peer of any updates (e.g. new peers under the same account)
|
||||||
func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_SyncServer) error {
|
func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_SyncServer) error {
|
||||||
if s.syncSem.Load() >= s.syncLim {
|
if s.syncLimEnabled && s.syncSem.Load() >= s.syncLim {
|
||||||
return status.Errorf(codes.ResourceExhausted, "too many concurrent sync requests, please try again later")
|
return status.Errorf(codes.ResourceExhausted, "too many concurrent sync requests, please try again later")
|
||||||
}
|
}
|
||||||
s.syncSem.Add(1)
|
s.syncSem.Add(1)
|
||||||
@@ -304,6 +311,7 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithContext(ctx).Debugf("error while sending initial sync for %s: %v", peerKey.String(), err)
|
log.WithContext(ctx).Debugf("error while sending initial sync for %s: %v", peerKey.String(), err)
|
||||||
s.syncSem.Add(-1)
|
s.syncSem.Add(-1)
|
||||||
|
s.cancelPeerRoutinesWithoutLock(ctx, accountID, peer)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +319,7 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithContext(ctx).Debugf("error while notify peer connected for %s: %v", peerKey.String(), err)
|
log.WithContext(ctx).Debugf("error while notify peer connected for %s: %v", peerKey.String(), err)
|
||||||
s.syncSem.Add(-1)
|
s.syncSem.Add(-1)
|
||||||
s.cancelPeerRoutines(ctx, accountID, peer)
|
s.cancelPeerRoutinesWithoutLock(ctx, accountID, peer)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,6 +490,10 @@ func (s *Server) cancelPeerRoutines(ctx context.Context, accountID string, peer
|
|||||||
unlock := s.acquirePeerLockByUID(ctx, peer.Key)
|
unlock := s.acquirePeerLockByUID(ctx, peer.Key)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
|
s.cancelPeerRoutinesWithoutLock(ctx, accountID, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) cancelPeerRoutinesWithoutLock(ctx context.Context, accountID string, peer *nbpeer.Peer) {
|
||||||
err := s.accountManager.OnPeerDisconnected(ctx, accountID, peer.Key)
|
err := s.accountManager.OnPeerDisconnected(ctx, accountID, peer.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithContext(ctx).Errorf("failed to disconnect peer %s properly: %v", peer.Key, err)
|
log.WithContext(ctx).Errorf("failed to disconnect peer %s properly: %v", peer.Key, err)
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ func NewTimeBasedAuthSecretsManager(updateManager network_map.PeersUpdateManager
|
|||||||
|
|
||||||
// GetWGKey returns WireGuard private key used to generate peer keys
|
// GetWGKey returns WireGuard private key used to generate peer keys
|
||||||
func (m *TimeBasedAuthSecretsManager) GetWGKey() (wgtypes.Key, error) {
|
func (m *TimeBasedAuthSecretsManager) GetWGKey() (wgtypes.Key, error) {
|
||||||
|
log.Debug("returning wg key from secrets manager")
|
||||||
return m.wgKey, nil
|
return m.wgKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
"github.com/netbirdio/netbird/management/server/util"
|
"github.com/netbirdio/netbird/management/server/util"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
|
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
|
||||||
"github.com/netbirdio/netbird/shared/management/status"
|
"github.com/netbirdio/netbird/shared/management/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -231,7 +232,7 @@ func BuildManager(
|
|||||||
// enable single account mode only if configured by user and number of existing accounts is not grater than 1
|
// enable single account mode only if configured by user and number of existing accounts is not grater than 1
|
||||||
am.singleAccountMode = singleAccountModeDomain != "" && accountsCounter <= 1
|
am.singleAccountMode = singleAccountModeDomain != "" && accountsCounter <= 1
|
||||||
if am.singleAccountMode {
|
if am.singleAccountMode {
|
||||||
if !isDomainValid(singleAccountModeDomain) {
|
if !nbdomain.IsValidDomainNoWildcard(singleAccountModeDomain) {
|
||||||
return nil, status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for a single account mode. Please review your input for --single-account-mode-domain", singleAccountModeDomain)
|
return nil, status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for a single account mode. Please review your input for --single-account-mode-domain", singleAccountModeDomain)
|
||||||
}
|
}
|
||||||
am.singleAccountModeDomain = singleAccountModeDomain
|
am.singleAccountModeDomain = singleAccountModeDomain
|
||||||
@@ -402,7 +403,7 @@ func (am *DefaultAccountManager) validateSettingsUpdate(ctx context.Context, tra
|
|||||||
return status.Errorf(status.InvalidArgument, "peer login expiration can't be smaller than one hour")
|
return status.Errorf(status.InvalidArgument, "peer login expiration can't be smaller than one hour")
|
||||||
}
|
}
|
||||||
|
|
||||||
if newSettings.DNSDomain != "" && !isDomainValid(newSettings.DNSDomain) {
|
if newSettings.DNSDomain != "" && !nbdomain.IsValidDomainNoWildcard(newSettings.DNSDomain) {
|
||||||
return status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for DNS domain", newSettings.DNSDomain)
|
return status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for DNS domain", newSettings.DNSDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,6 +795,19 @@ func IsEmbeddedIdp(i idp.Manager) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsLocalAuthDisabled checks if local (email/password) authentication is disabled.
|
||||||
|
// Returns true only when using embedded IDP with local auth disabled in config.
|
||||||
|
func IsLocalAuthDisabled(ctx context.Context, i idp.Manager) bool {
|
||||||
|
if isNil(i) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
embeddedIdp, ok := i.(*idp.EmbeddedIdPManager)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return embeddedIdp.IsLocalAuthDisabled()
|
||||||
|
}
|
||||||
|
|
||||||
// addAccountIDToIDPAppMeta update user's app metadata in idp manager
|
// addAccountIDToIDPAppMeta update user's app metadata in idp manager
|
||||||
func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(ctx context.Context, userID string, accountID string) error {
|
func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(ctx context.Context, userID string, accountID string) error {
|
||||||
if !isNil(am.idpManager) && !IsEmbeddedIdp(am.idpManager) {
|
if !isNil(am.idpManager) && !IsEmbeddedIdp(am.idpManager) {
|
||||||
@@ -1691,10 +1705,12 @@ func (am *DefaultAccountManager) SyncPeerMeta(ctx context.Context, peerPubKey st
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var invalidDomainRegexp = regexp.MustCompile(`^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`)
|
// isDomainValid validates public/IDP domains using stricter rules than internal DNS domains.
|
||||||
|
// Requires at least 2-char alphabetic TLD and no single-label domains.
|
||||||
|
var publicDomainRegexp = regexp.MustCompile(`^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`)
|
||||||
|
|
||||||
func isDomainValid(domain string) bool {
|
func isDomainValid(domain string) bool {
|
||||||
return invalidDomainRegexp.MatchString(domain)
|
return publicDomainRegexp.MatchString(domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *DefaultAccountManager) onPeersInvalidated(ctx context.Context, accountID string, peerIDs []string) {
|
func (am *DefaultAccountManager) onPeersInvalidated(ctx context.Context, accountID string, peerIDs []string) {
|
||||||
|
|||||||
@@ -129,14 +129,14 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks
|
|||||||
return nil, fmt.Errorf("register integrations endpoints: %w", err)
|
return nil, fmt.Errorf("register integrations endpoints: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if embedded IdP is enabled
|
// Check if embedded IdP is enabled for instance manager
|
||||||
embeddedIdP, embeddedIdpEnabled := idpManager.(*idpmanager.EmbeddedIdPManager)
|
embeddedIdP, embeddedIdpEnabled := idpManager.(*idpmanager.EmbeddedIdPManager)
|
||||||
instanceManager, err := nbinstance.NewManager(ctx, accountManager.GetStore(), embeddedIdP)
|
instanceManager, err := nbinstance.NewManager(ctx, accountManager.GetStore(), embeddedIdP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create instance manager: %w", err)
|
return nil, fmt.Errorf("failed to create instance manager: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts.AddEndpoints(accountManager, settingsManager, embeddedIdpEnabled, router)
|
accounts.AddEndpoints(accountManager, settingsManager, router)
|
||||||
peers.AddEndpoints(accountManager, router, networkMapController)
|
peers.AddEndpoints(accountManager, router, networkMapController)
|
||||||
users.AddEndpoints(accountManager, router)
|
users.AddEndpoints(accountManager, router)
|
||||||
users.AddInvitesEndpoints(accountManager, router)
|
users.AddInvitesEndpoints(accountManager, router)
|
||||||
|
|||||||
@@ -36,24 +36,22 @@ const (
|
|||||||
|
|
||||||
// handler is a handler that handles the server.Account HTTP endpoints
|
// handler is a handler that handles the server.Account HTTP endpoints
|
||||||
type handler struct {
|
type handler struct {
|
||||||
accountManager account.Manager
|
accountManager account.Manager
|
||||||
settingsManager settings.Manager
|
settingsManager settings.Manager
|
||||||
embeddedIdpEnabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddEndpoints(accountManager account.Manager, settingsManager settings.Manager, embeddedIdpEnabled bool, router *mux.Router) {
|
func AddEndpoints(accountManager account.Manager, settingsManager settings.Manager, router *mux.Router) {
|
||||||
accountsHandler := newHandler(accountManager, settingsManager, embeddedIdpEnabled)
|
accountsHandler := newHandler(accountManager, settingsManager)
|
||||||
router.HandleFunc("/accounts/{accountId}", accountsHandler.updateAccount).Methods("PUT", "OPTIONS")
|
router.HandleFunc("/accounts/{accountId}", accountsHandler.updateAccount).Methods("PUT", "OPTIONS")
|
||||||
router.HandleFunc("/accounts/{accountId}", accountsHandler.deleteAccount).Methods("DELETE", "OPTIONS")
|
router.HandleFunc("/accounts/{accountId}", accountsHandler.deleteAccount).Methods("DELETE", "OPTIONS")
|
||||||
router.HandleFunc("/accounts", accountsHandler.getAllAccounts).Methods("GET", "OPTIONS")
|
router.HandleFunc("/accounts", accountsHandler.getAllAccounts).Methods("GET", "OPTIONS")
|
||||||
}
|
}
|
||||||
|
|
||||||
// newHandler creates a new handler HTTP handler
|
// newHandler creates a new handler HTTP handler
|
||||||
func newHandler(accountManager account.Manager, settingsManager settings.Manager, embeddedIdpEnabled bool) *handler {
|
func newHandler(accountManager account.Manager, settingsManager settings.Manager) *handler {
|
||||||
return &handler{
|
return &handler{
|
||||||
accountManager: accountManager,
|
accountManager: accountManager,
|
||||||
settingsManager: settingsManager,
|
settingsManager: settingsManager,
|
||||||
embeddedIdpEnabled: embeddedIdpEnabled,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +163,7 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := toAccountResponse(accountID, settings, meta, onboarding, h.embeddedIdpEnabled)
|
resp := toAccountResponse(accountID, settings, meta, onboarding)
|
||||||
util.WriteJSONObject(r.Context(), w, []*api.Account{resp})
|
util.WriteJSONObject(r.Context(), w, []*api.Account{resp})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,7 +290,7 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := toAccountResponse(accountID, updatedSettings, meta, updatedOnboarding, h.embeddedIdpEnabled)
|
resp := toAccountResponse(accountID, updatedSettings, meta, updatedOnboarding)
|
||||||
|
|
||||||
util.WriteJSONObject(r.Context(), w, &resp)
|
util.WriteJSONObject(r.Context(), w, &resp)
|
||||||
}
|
}
|
||||||
@@ -321,7 +319,7 @@ func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
|
|||||||
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
|
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func toAccountResponse(accountID string, settings *types.Settings, meta *types.AccountMeta, onboarding *types.AccountOnboarding, embeddedIdpEnabled bool) *api.Account {
|
func toAccountResponse(accountID string, settings *types.Settings, meta *types.AccountMeta, onboarding *types.AccountOnboarding) *api.Account {
|
||||||
jwtAllowGroups := settings.JWTAllowGroups
|
jwtAllowGroups := settings.JWTAllowGroups
|
||||||
if jwtAllowGroups == nil {
|
if jwtAllowGroups == nil {
|
||||||
jwtAllowGroups = []string{}
|
jwtAllowGroups = []string{}
|
||||||
@@ -341,7 +339,8 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
|
|||||||
LazyConnectionEnabled: &settings.LazyConnectionEnabled,
|
LazyConnectionEnabled: &settings.LazyConnectionEnabled,
|
||||||
DnsDomain: &settings.DNSDomain,
|
DnsDomain: &settings.DNSDomain,
|
||||||
AutoUpdateVersion: &settings.AutoUpdateVersion,
|
AutoUpdateVersion: &settings.AutoUpdateVersion,
|
||||||
EmbeddedIdpEnabled: &embeddedIdpEnabled,
|
EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled,
|
||||||
|
LocalAuthDisabled: &settings.LocalAuthDisabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.NetworkRange.IsValid() {
|
if settings.NetworkRange.IsValid() {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ func initAccountsTestData(t *testing.T, account *types.Account) *handler {
|
|||||||
AnyTimes()
|
AnyTimes()
|
||||||
|
|
||||||
return &handler{
|
return &handler{
|
||||||
embeddedIdpEnabled: false,
|
|
||||||
accountManager: &mock_server.MockAccountManager{
|
accountManager: &mock_server.MockAccountManager{
|
||||||
GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
|
GetAccountSettingsFunc: func(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
|
||||||
return account.Settings, nil
|
return account.Settings, nil
|
||||||
@@ -124,6 +123,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
EmbeddedIdpEnabled: br(false),
|
EmbeddedIdpEnabled: br(false),
|
||||||
|
LocalAuthDisabled: br(false),
|
||||||
},
|
},
|
||||||
expectedArray: true,
|
expectedArray: true,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
@@ -148,6 +148,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
EmbeddedIdpEnabled: br(false),
|
EmbeddedIdpEnabled: br(false),
|
||||||
|
LocalAuthDisabled: br(false),
|
||||||
},
|
},
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
@@ -172,6 +173,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
AutoUpdateVersion: sr("latest"),
|
AutoUpdateVersion: sr("latest"),
|
||||||
EmbeddedIdpEnabled: br(false),
|
EmbeddedIdpEnabled: br(false),
|
||||||
|
LocalAuthDisabled: br(false),
|
||||||
},
|
},
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
@@ -196,6 +198,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
EmbeddedIdpEnabled: br(false),
|
EmbeddedIdpEnabled: br(false),
|
||||||
|
LocalAuthDisabled: br(false),
|
||||||
},
|
},
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
@@ -220,6 +223,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
EmbeddedIdpEnabled: br(false),
|
EmbeddedIdpEnabled: br(false),
|
||||||
|
LocalAuthDisabled: br(false),
|
||||||
},
|
},
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
@@ -244,6 +248,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
DnsDomain: sr(""),
|
DnsDomain: sr(""),
|
||||||
AutoUpdateVersion: sr(""),
|
AutoUpdateVersion: sr(""),
|
||||||
EmbeddedIdpEnabled: br(false),
|
EmbeddedIdpEnabled: br(false),
|
||||||
|
LocalAuthDisabled: br(false),
|
||||||
},
|
},
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func (h *handler) getInstanceStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
util.WriteErrorResponse("failed to check instance status", http.StatusInternalServerError, w)
|
util.WriteErrorResponse("failed to check instance status", http.StatusInternalServerError, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.WithContext(r.Context()).Infof("instance setup status: %v", setupRequired)
|
||||||
util.WriteJSONObject(r.Context(), w, api.InstanceStatus{
|
util.WriteJSONObject(r.Context(), w, api.InstanceStatus{
|
||||||
SetupRequired: setupRequired,
|
SetupRequired: setupRequired,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -205,6 +205,14 @@ func TestCreateInvite(t *testing.T) {
|
|||||||
return nil, status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
return nil, status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "local auth disabled",
|
||||||
|
requestBody: `{"email":"test@example.com","name":"Test User","role":"user","auto_groups":[]}`,
|
||||||
|
expectedStatus: http.StatusPreconditionFailed,
|
||||||
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID string, invite *types.UserInfo, expiresIn int) (*types.UserInvite, error) {
|
||||||
|
return nil, status.Errorf(status.PreconditionFailed, "local user creation is disabled - use an external identity provider")
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "invalid JSON",
|
name: "invalid JSON",
|
||||||
requestBody: `{invalid json}`,
|
requestBody: `{invalid json}`,
|
||||||
@@ -376,6 +384,15 @@ func TestAcceptInvite(t *testing.T) {
|
|||||||
return status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
return status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "local auth disabled",
|
||||||
|
token: testInviteToken,
|
||||||
|
requestBody: `{"password":"SecurePass123!"}`,
|
||||||
|
expectedStatus: http.StatusPreconditionFailed,
|
||||||
|
mockFunc: func(ctx context.Context, token, password string) error {
|
||||||
|
return status.Errorf(status.PreconditionFailed, "local user creation is disabled - use an external identity provider")
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "missing token",
|
name: "missing token",
|
||||||
token: "",
|
token: "",
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
|
|||||||
proxyController := integrations.NewController(store)
|
proxyController := integrations.NewController(store)
|
||||||
userManager := users.NewManager(store)
|
userManager := users.NewManager(store)
|
||||||
permissionsManager := permissions.NewManager(store)
|
permissionsManager := permissions.NewManager(store)
|
||||||
settingsManager := settings.NewManager(store, userManager, integrations.NewManager(&activity.InMemoryEventStore{}), permissionsManager)
|
settingsManager := settings.NewManager(store, userManager, integrations.NewManager(&activity.InMemoryEventStore{}), permissionsManager, settings.IdpConfig{})
|
||||||
peersManager := peers.NewManager(store, permissionsManager)
|
peersManager := peers.NewManager(store, permissionsManager)
|
||||||
|
|
||||||
jobManager := job.NewJobManager(nil, store, peersManager)
|
jobManager := job.NewJobManager(nil, store, peersManager)
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ type EmbeddedIdPConfig struct {
|
|||||||
Owner *OwnerConfig
|
Owner *OwnerConfig
|
||||||
// SignKeyRefreshEnabled enables automatic key rotation for signing keys
|
// SignKeyRefreshEnabled enables automatic key rotation for signing keys
|
||||||
SignKeyRefreshEnabled bool
|
SignKeyRefreshEnabled bool
|
||||||
|
// LocalAuthDisabled disables the local (email/password) authentication connector.
|
||||||
|
// When true, users cannot authenticate via email/password, only via external identity providers.
|
||||||
|
// Existing local users are preserved and will be able to login again if re-enabled.
|
||||||
|
// Cannot be enabled if no external identity provider connectors are configured.
|
||||||
|
LocalAuthDisabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmbeddedStorageConfig holds storage configuration for the embedded IdP.
|
// EmbeddedStorageConfig holds storage configuration for the embedded IdP.
|
||||||
@@ -105,6 +110,8 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
|
|||||||
Issuer: "NetBird",
|
Issuer: "NetBird",
|
||||||
Theme: "light",
|
Theme: "light",
|
||||||
},
|
},
|
||||||
|
// Always enable password DB initially - we disable the local connector after startup if needed.
|
||||||
|
// This ensures Dex has at least one connector during initialization.
|
||||||
EnablePasswordDB: true,
|
EnablePasswordDB: true,
|
||||||
StaticClients: []storage.Client{
|
StaticClients: []storage.Client{
|
||||||
{
|
{
|
||||||
@@ -192,11 +199,32 @@ func NewEmbeddedIdPManager(ctx context.Context, config *EmbeddedIdPConfig, appMe
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.WithContext(ctx).Debugf("initializing embedded Dex IDP with config: %+v", config)
|
||||||
|
|
||||||
provider, err := dex.NewProviderFromYAML(ctx, yamlConfig)
|
provider, err := dex.NewProviderFromYAML(ctx, yamlConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create embedded IdP provider: %w", err)
|
return nil, fmt.Errorf("failed to create embedded IdP provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If local auth is disabled, validate that other connectors exist
|
||||||
|
if config.LocalAuthDisabled {
|
||||||
|
hasOthers, err := provider.HasNonLocalConnectors(ctx)
|
||||||
|
if err != nil {
|
||||||
|
_ = provider.Stop(ctx)
|
||||||
|
return nil, fmt.Errorf("failed to check connectors: %w", err)
|
||||||
|
}
|
||||||
|
if !hasOthers {
|
||||||
|
_ = provider.Stop(ctx)
|
||||||
|
return nil, fmt.Errorf("cannot disable local authentication: no other identity providers configured")
|
||||||
|
}
|
||||||
|
// Ensure local connector is removed (it might exist from a previous run)
|
||||||
|
if err := provider.DisableLocalAuth(ctx); err != nil {
|
||||||
|
_ = provider.Stop(ctx)
|
||||||
|
return nil, fmt.Errorf("failed to disable local auth: %w", err)
|
||||||
|
}
|
||||||
|
log.WithContext(ctx).Info("local authentication disabled - only external identity providers can be used")
|
||||||
|
}
|
||||||
|
|
||||||
log.WithContext(ctx).Infof("embedded Dex IDP initialized with issuer: %s", yamlConfig.Issuer)
|
log.WithContext(ctx).Infof("embedded Dex IDP initialized with issuer: %s", yamlConfig.Issuer)
|
||||||
|
|
||||||
return &EmbeddedIdPManager{
|
return &EmbeddedIdPManager{
|
||||||
@@ -281,6 +309,8 @@ func (m *EmbeddedIdPManager) GetAllAccounts(ctx context.Context) (map[string][]*
|
|||||||
return nil, fmt.Errorf("failed to list users: %w", err)
|
return nil, fmt.Errorf("failed to list users: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.WithContext(ctx).Debugf("retrieved %d users from embedded IdP", len(users))
|
||||||
|
|
||||||
indexedUsers := make(map[string][]*UserData)
|
indexedUsers := make(map[string][]*UserData)
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], &UserData{
|
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], &UserData{
|
||||||
@@ -290,11 +320,17 @@ func (m *EmbeddedIdPManager) GetAllAccounts(ctx context.Context) (map[string][]*
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.WithContext(ctx).Debugf("retrieved %d users from embedded IdP", len(indexedUsers[UnsetAccountID]))
|
||||||
|
|
||||||
return indexedUsers, nil
|
return indexedUsers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in the embedded IdP.
|
// CreateUser creates a new user in the embedded IdP.
|
||||||
func (m *EmbeddedIdPManager) CreateUser(ctx context.Context, email, name, accountID, invitedByEmail string) (*UserData, error) {
|
func (m *EmbeddedIdPManager) CreateUser(ctx context.Context, email, name, accountID, invitedByEmail string) (*UserData, error) {
|
||||||
|
if m.config.LocalAuthDisabled {
|
||||||
|
return nil, fmt.Errorf("local user creation is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
if m.appMetrics != nil {
|
if m.appMetrics != nil {
|
||||||
m.appMetrics.IDPMetrics().CountCreateUser()
|
m.appMetrics.IDPMetrics().CountCreateUser()
|
||||||
}
|
}
|
||||||
@@ -364,6 +400,10 @@ func (m *EmbeddedIdPManager) GetUserByEmail(ctx context.Context, email string) (
|
|||||||
// Unlike CreateUser which auto-generates a password, this method uses the provided password.
|
// Unlike CreateUser which auto-generates a password, this method uses the provided password.
|
||||||
// This is useful for instance setup where the user provides their own password.
|
// This is useful for instance setup where the user provides their own password.
|
||||||
func (m *EmbeddedIdPManager) CreateUserWithPassword(ctx context.Context, email, password, name string) (*UserData, error) {
|
func (m *EmbeddedIdPManager) CreateUserWithPassword(ctx context.Context, email, password, name string) (*UserData, error) {
|
||||||
|
if m.config.LocalAuthDisabled {
|
||||||
|
return nil, fmt.Errorf("local user creation is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
if m.appMetrics != nil {
|
if m.appMetrics != nil {
|
||||||
m.appMetrics.IDPMetrics().CountCreateUser()
|
m.appMetrics.IDPMetrics().CountCreateUser()
|
||||||
}
|
}
|
||||||
@@ -553,3 +593,13 @@ func (m *EmbeddedIdPManager) GetClientIDs() []string {
|
|||||||
func (m *EmbeddedIdPManager) GetUserIDClaim() string {
|
func (m *EmbeddedIdPManager) GetUserIDClaim() string {
|
||||||
return defaultUserIDClaim
|
return defaultUserIDClaim
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsLocalAuthDisabled returns whether local authentication is disabled based on configuration.
|
||||||
|
func (m *EmbeddedIdPManager) IsLocalAuthDisabled() bool {
|
||||||
|
return m.config.LocalAuthDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNonLocalConnectors checks if there are any identity provider connectors other than local.
|
||||||
|
func (m *EmbeddedIdPManager) HasNonLocalConnectors(ctx context.Context) (bool, error) {
|
||||||
|
return m.provider.HasNonLocalConnectors(ctx)
|
||||||
|
}
|
||||||
|
|||||||
@@ -370,3 +370,234 @@ func TestEmbeddedIdPManager_GetLocalKeysLocation(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("cannot start with local auth disabled without other connectors", func(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
config := &EmbeddedIdPConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Issuer: "http://localhost:5556/dex",
|
||||||
|
LocalAuthDisabled: true,
|
||||||
|
Storage: EmbeddedStorageConfig{
|
||||||
|
Type: "sqlite3",
|
||||||
|
Config: EmbeddedStorageTypeConfig{
|
||||||
|
File: filepath.Join(tmpDir, "dex.db"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = NewEmbeddedIdPManager(ctx, config, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "no other identity providers configured")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("local auth enabled by default", func(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
config := &EmbeddedIdPConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Issuer: "http://localhost:5556/dex",
|
||||||
|
Storage: EmbeddedStorageConfig{
|
||||||
|
Type: "sqlite3",
|
||||||
|
Config: EmbeddedStorageTypeConfig{
|
||||||
|
File: filepath.Join(tmpDir, "dex.db"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := NewEmbeddedIdPManager(ctx, config, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = manager.Stop(ctx) }()
|
||||||
|
|
||||||
|
// Verify local auth is enabled by default
|
||||||
|
assert.False(t, manager.IsLocalAuthDisabled())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("start with local auth disabled when connector exists", func(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
dbFile := filepath.Join(tmpDir, "dex.db")
|
||||||
|
|
||||||
|
// First, create a manager with local auth enabled and add a connector
|
||||||
|
config1 := &EmbeddedIdPConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Issuer: "http://localhost:5556/dex",
|
||||||
|
Storage: EmbeddedStorageConfig{
|
||||||
|
Type: "sqlite3",
|
||||||
|
Config: EmbeddedStorageTypeConfig{
|
||||||
|
File: dbFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager1, err := NewEmbeddedIdPManager(ctx, config1, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a user
|
||||||
|
userData, err := manager1.CreateUser(ctx, "preserved@example.com", "Preserved User", "account1", "admin@example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
userID := userData.ID
|
||||||
|
|
||||||
|
// Add an external connector (Google doesn't require OIDC discovery)
|
||||||
|
_, err = manager1.CreateConnector(ctx, &dex.ConnectorConfig{
|
||||||
|
ID: "google-test",
|
||||||
|
Name: "Google Test",
|
||||||
|
Type: "google",
|
||||||
|
ClientID: "test-client-id",
|
||||||
|
ClientSecret: "test-client-secret",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Stop the first manager
|
||||||
|
err = manager1.Stop(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Now create a new manager with local auth disabled
|
||||||
|
config2 := &EmbeddedIdPConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Issuer: "http://localhost:5556/dex",
|
||||||
|
LocalAuthDisabled: true,
|
||||||
|
Storage: EmbeddedStorageConfig{
|
||||||
|
Type: "sqlite3",
|
||||||
|
Config: EmbeddedStorageTypeConfig{
|
||||||
|
File: dbFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager2, err := NewEmbeddedIdPManager(ctx, config2, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = manager2.Stop(ctx) }()
|
||||||
|
|
||||||
|
// Verify local auth is disabled via config
|
||||||
|
assert.True(t, manager2.IsLocalAuthDisabled())
|
||||||
|
|
||||||
|
// Verify the user still exists in storage (just can't login via local)
|
||||||
|
lookedUp, err := manager2.GetUserDataByID(ctx, userID, AppMetadata{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "preserved@example.com", lookedUp.Email)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateUser fails when local auth is disabled", func(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
dbFile := filepath.Join(tmpDir, "dex.db")
|
||||||
|
|
||||||
|
// First, create a manager and add an external connector
|
||||||
|
config1 := &EmbeddedIdPConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Issuer: "http://localhost:5556/dex",
|
||||||
|
Storage: EmbeddedStorageConfig{
|
||||||
|
Type: "sqlite3",
|
||||||
|
Config: EmbeddedStorageTypeConfig{
|
||||||
|
File: dbFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager1, err := NewEmbeddedIdPManager(ctx, config1, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = manager1.CreateConnector(ctx, &dex.ConnectorConfig{
|
||||||
|
ID: "google-test",
|
||||||
|
Name: "Google Test",
|
||||||
|
Type: "google",
|
||||||
|
ClientID: "test-client-id",
|
||||||
|
ClientSecret: "test-client-secret",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = manager1.Stop(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create manager with local auth disabled
|
||||||
|
config2 := &EmbeddedIdPConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Issuer: "http://localhost:5556/dex",
|
||||||
|
LocalAuthDisabled: true,
|
||||||
|
Storage: EmbeddedStorageConfig{
|
||||||
|
Type: "sqlite3",
|
||||||
|
Config: EmbeddedStorageTypeConfig{
|
||||||
|
File: dbFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager2, err := NewEmbeddedIdPManager(ctx, config2, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = manager2.Stop(ctx) }()
|
||||||
|
|
||||||
|
// Try to create a user - should fail
|
||||||
|
_, err = manager2.CreateUser(ctx, "newuser@example.com", "New User", "account1", "admin@example.com")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "local user creation is disabled")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateUserWithPassword fails when local auth is disabled", func(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
dbFile := filepath.Join(tmpDir, "dex.db")
|
||||||
|
|
||||||
|
// First, create a manager and add an external connector
|
||||||
|
config1 := &EmbeddedIdPConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Issuer: "http://localhost:5556/dex",
|
||||||
|
Storage: EmbeddedStorageConfig{
|
||||||
|
Type: "sqlite3",
|
||||||
|
Config: EmbeddedStorageTypeConfig{
|
||||||
|
File: dbFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager1, err := NewEmbeddedIdPManager(ctx, config1, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = manager1.CreateConnector(ctx, &dex.ConnectorConfig{
|
||||||
|
ID: "google-test",
|
||||||
|
Name: "Google Test",
|
||||||
|
Type: "google",
|
||||||
|
ClientID: "test-client-id",
|
||||||
|
ClientSecret: "test-client-secret",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = manager1.Stop(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create manager with local auth disabled
|
||||||
|
config2 := &EmbeddedIdPConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Issuer: "http://localhost:5556/dex",
|
||||||
|
LocalAuthDisabled: true,
|
||||||
|
Storage: EmbeddedStorageConfig{
|
||||||
|
Type: "sqlite3",
|
||||||
|
Config: EmbeddedStorageTypeConfig{
|
||||||
|
File: dbFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager2, err := NewEmbeddedIdPManager(ctx, config2, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = manager2.Stop(ctx) }()
|
||||||
|
|
||||||
|
// Try to create a user with password - should fail
|
||||||
|
_, err = manager2.CreateUserWithPassword(ctx, "newuser@example.com", "SecurePass123!", "New User")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "local user creation is disabled")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,13 +104,22 @@ func NewManager(ctx context.Context, store store.Store, idpManager idp.Manager)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *DefaultManager) loadSetupRequired(ctx context.Context) error {
|
func (m *DefaultManager) loadSetupRequired(ctx context.Context) error {
|
||||||
|
// Check if there are any accounts in the NetBird store
|
||||||
|
numAccounts, err := m.store.GetAccountsCounter(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hasAccounts := numAccounts > 0
|
||||||
|
|
||||||
|
// Check if there are any users in the embedded IdP (Dex)
|
||||||
users, err := m.embeddedIdpManager.GetAllAccounts(ctx)
|
users, err := m.embeddedIdpManager.GetAllAccounts(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
hasLocalUsers := len(users) > 0
|
||||||
|
|
||||||
m.setupMu.Lock()
|
m.setupMu.Lock()
|
||||||
m.setupRequired = len(users) == 0
|
m.setupRequired = !(hasAccounts || hasLocalUsers)
|
||||||
m.setupMu.Unlock()
|
m.setupMu.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"regexp"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
@@ -15,11 +15,10 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
|
||||||
"github.com/netbirdio/netbird/shared/management/status"
|
"github.com/netbirdio/netbird/shared/management/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
const domainPattern = `^(?i)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*[*.a-z]{1,}$`
|
|
||||||
|
|
||||||
var errInvalidDomainName = errors.New("invalid domain name")
|
var errInvalidDomainName = errors.New("invalid domain name")
|
||||||
|
|
||||||
// GetNameServerGroup gets a nameserver group object from account and nameserver group IDs
|
// GetNameServerGroup gets a nameserver group object from account and nameserver group IDs
|
||||||
@@ -305,16 +304,18 @@ func validateGroups(list []string, groups map[string]*types.Group) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var domainMatcher = regexp.MustCompile(domainPattern)
|
// validateDomain validates a nameserver match domain.
|
||||||
|
// Converts unicode to punycode. Wildcards are not allowed for nameservers.
|
||||||
func validateDomain(domain string) error {
|
func validateDomain(d string) error {
|
||||||
if !domainMatcher.MatchString(domain) {
|
if strings.HasPrefix(d, "*.") {
|
||||||
return errors.New("domain should consists of only letters, numbers, and hyphens with no leading, trailing hyphens, or spaces")
|
return errors.New("wildcards not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, valid := dns.IsDomainName(domain)
|
// Nameservers allow trailing dot (FQDN format)
|
||||||
if !valid {
|
toValidate := strings.TrimSuffix(d, ".")
|
||||||
return errInvalidDomainName
|
|
||||||
|
if _, err := nbdomain.ValidateDomains([]string{toValidate}); err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", errInvalidDomainName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -901,82 +901,53 @@ func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*types.Account,
|
|||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestValidateDomain tests nameserver-specific domain validation.
|
||||||
|
// Core domain validation is tested in shared/management/domain/validate_test.go.
|
||||||
|
// This test only covers nameserver-specific behavior: wildcard rejection and unicode support.
|
||||||
func TestValidateDomain(t *testing.T) {
|
func TestValidateDomain(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
domain string
|
domain string
|
||||||
errFunc require.ErrorAssertionFunc
|
errFunc require.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
|
// Nameserver-specific: wildcards not allowed
|
||||||
{
|
{
|
||||||
name: "Valid domain name with multiple labels",
|
name: "Wildcard prefix rejected",
|
||||||
domain: "123.example.com",
|
domain: "*.example.com",
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wildcard in middle rejected",
|
||||||
|
domain: "a.*.example.com",
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
// Nameserver-specific: unicode converted to punycode
|
||||||
|
{
|
||||||
|
name: "Unicode domain converted to punycode",
|
||||||
|
domain: "münchen.de",
|
||||||
errFunc: require.NoError,
|
errFunc: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Valid domain name with hyphen",
|
name: "Unicode domain all labels",
|
||||||
domain: "test-example.com",
|
domain: "中国.中国",
|
||||||
|
errFunc: require.NoError,
|
||||||
|
},
|
||||||
|
// Basic validation still works (delegates to shared validation)
|
||||||
|
{
|
||||||
|
name: "Valid multi-label domain",
|
||||||
|
domain: "example.com",
|
||||||
errFunc: require.NoError,
|
errFunc: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Valid domain name with only one label",
|
name: "Valid single label",
|
||||||
domain: "example",
|
domain: "internal",
|
||||||
errFunc: require.NoError,
|
errFunc: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Valid domain name with trailing dot",
|
name: "Invalid leading hyphen",
|
||||||
domain: "example.",
|
|
||||||
errFunc: require.NoError,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid wildcard domain name",
|
|
||||||
domain: "*.example",
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid domain name with leading dot",
|
|
||||||
domain: ".com",
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid domain name with dot only",
|
|
||||||
domain: ".",
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid domain name with double hyphen",
|
|
||||||
domain: "test--example.com",
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid domain name with a label exceeding 63 characters",
|
|
||||||
domain: "dnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdns.com",
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid domain name starting with a hyphen",
|
|
||||||
domain: "-example.com",
|
domain: "-example.com",
|
||||||
errFunc: require.Error,
|
errFunc: require.Error,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Invalid domain name ending with a hyphen",
|
|
||||||
domain: "example.com-",
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid domain with unicode",
|
|
||||||
domain: "example?,.com",
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid domain with space before top-level domain",
|
|
||||||
domain: "space .example.com",
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid domain with trailing space",
|
|
||||||
domain: "example.com ",
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ func Test_CreateResourceFailsWithInvalidAddress(t *testing.T) {
|
|||||||
NetworkID: "testNetworkId",
|
NetworkID: "testNetworkId",
|
||||||
Name: "testResourceId",
|
Name: "testResourceId",
|
||||||
Description: "description",
|
Description: "description",
|
||||||
Address: "invalid-address",
|
Address: "-invalid",
|
||||||
}
|
}
|
||||||
|
|
||||||
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
|
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
|
||||||
@@ -227,9 +227,9 @@ func Test_CreateResourceFailsWithUsedName(t *testing.T) {
|
|||||||
resource := &types.NetworkResource{
|
resource := &types.NetworkResource{
|
||||||
AccountID: "testAccountId",
|
AccountID: "testAccountId",
|
||||||
NetworkID: "testNetworkId",
|
NetworkID: "testNetworkId",
|
||||||
Name: "testResourceId",
|
Name: "used-name",
|
||||||
Description: "description",
|
Description: "description",
|
||||||
Address: "invalid-address",
|
Address: "example.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
|
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "../../testdata/networks.sql", t.TempDir())
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
|
|
||||||
@@ -166,8 +165,7 @@ func GetResourceType(address string) (NetworkResourceType, string, netip.Prefix,
|
|||||||
return Host, "", netip.PrefixFrom(ip, ip.BitLen()), nil
|
return Host, "", netip.PrefixFrom(ip, ip.BitLen()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
domainRegex := regexp.MustCompile(`^(\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$`)
|
if _, err := nbDomain.ValidateDomains([]string{address}); err == nil {
|
||||||
if domainRegex.MatchString(address) {
|
|
||||||
return Domain, address, netip.Prefix{}, nil
|
return Domain, address, netip.Prefix{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ func TestGetResourceType(t *testing.T) {
|
|||||||
{"example.com", Domain, false, "example.com", netip.Prefix{}},
|
{"example.com", Domain, false, "example.com", netip.Prefix{}},
|
||||||
{"*.example.com", Domain, false, "*.example.com", netip.Prefix{}},
|
{"*.example.com", Domain, false, "*.example.com", netip.Prefix{}},
|
||||||
{"sub.example.com", Domain, false, "sub.example.com", netip.Prefix{}},
|
{"sub.example.com", Domain, false, "sub.example.com", netip.Prefix{}},
|
||||||
|
{"example.x", Domain, false, "example.x", netip.Prefix{}},
|
||||||
|
{"internal", Domain, false, "internal", netip.Prefix{}},
|
||||||
// Invalid inputs
|
// Invalid inputs
|
||||||
{"invalid", "", true, "", netip.Prefix{}},
|
|
||||||
{"1.1.1.1/abc", "", true, "", netip.Prefix{}},
|
{"1.1.1.1/abc", "", true, "", netip.Prefix{}},
|
||||||
{"1234", "", true, "", netip.Prefix{}},
|
{"-invalid.com", "", true, "", netip.Prefix{}},
|
||||||
|
{"", "", true, "", netip.Prefix{}},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -728,11 +728,6 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
|
|||||||
return fmt.Errorf("failed adding peer to All group: %w", err)
|
return fmt.Errorf("failed adding peer to All group: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if temporary {
|
|
||||||
// we should track ephemeral peers to be able to clean them if the peer don't sync and be marked as connected
|
|
||||||
am.networkMapController.TrackEphemeralPeer(ctx, newPeer)
|
|
||||||
}
|
|
||||||
|
|
||||||
if addedByUser {
|
if addedByUser {
|
||||||
err := transaction.SaveUserLastLogin(ctx, accountID, userID, newPeer.GetLastLogin())
|
err := transaction.SaveUserLastLogin(ctx, accountID, userID, newPeer.GetLastLogin())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -760,6 +755,11 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
|
|||||||
return fmt.Errorf("failed to increment network serial: %w", err)
|
return fmt.Errorf("failed to increment network serial: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ephemeral {
|
||||||
|
// we should track ephemeral peers to be able to clean them if the peer doesn't sync and isn't marked as connected
|
||||||
|
am.networkMapController.TrackEphemeralPeer(ctx, newPeer)
|
||||||
|
}
|
||||||
|
|
||||||
log.WithContext(ctx).Debugf("Peer %s added to account %s", newPeer.ID, accountID)
|
log.WithContext(ctx).Debugf("Peer %s added to account %s", newPeer.ID, accountID)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,19 +24,28 @@ type Manager interface {
|
|||||||
UpdateExtraSettings(ctx context.Context, accountID, userID string, extraSettings *types.ExtraSettings) (bool, error)
|
UpdateExtraSettings(ctx context.Context, accountID, userID string, extraSettings *types.ExtraSettings) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IdpConfig holds IdP-related configuration that is set at runtime
|
||||||
|
// and not stored in the database.
|
||||||
|
type IdpConfig struct {
|
||||||
|
EmbeddedIdpEnabled bool
|
||||||
|
LocalAuthDisabled bool
|
||||||
|
}
|
||||||
|
|
||||||
type managerImpl struct {
|
type managerImpl struct {
|
||||||
store store.Store
|
store store.Store
|
||||||
extraSettingsManager extra_settings.Manager
|
extraSettingsManager extra_settings.Manager
|
||||||
userManager users.Manager
|
userManager users.Manager
|
||||||
permissionsManager permissions.Manager
|
permissionsManager permissions.Manager
|
||||||
|
idpConfig IdpConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(store store.Store, userManager users.Manager, extraSettingsManager extra_settings.Manager, permissionsManager permissions.Manager) Manager {
|
func NewManager(store store.Store, userManager users.Manager, extraSettingsManager extra_settings.Manager, permissionsManager permissions.Manager, idpConfig IdpConfig) Manager {
|
||||||
return &managerImpl{
|
return &managerImpl{
|
||||||
store: store,
|
store: store,
|
||||||
extraSettingsManager: extraSettingsManager,
|
extraSettingsManager: extraSettingsManager,
|
||||||
userManager: userManager,
|
userManager: userManager,
|
||||||
permissionsManager: permissionsManager,
|
permissionsManager: permissionsManager,
|
||||||
|
idpConfig: idpConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +83,10 @@ func (m *managerImpl) GetSettings(ctx context.Context, accountID, userID string)
|
|||||||
settings.Extra.FlowDnsCollectionEnabled = extraSettings.FlowDnsCollectionEnabled
|
settings.Extra.FlowDnsCollectionEnabled = extraSettings.FlowDnsCollectionEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill in IdP-related runtime settings
|
||||||
|
settings.EmbeddedIdpEnabled = m.idpConfig.EmbeddedIdpEnabled
|
||||||
|
settings.LocalAuthDisabled = m.idpConfig.LocalAuthDisabled
|
||||||
|
|
||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ type Settings struct {
|
|||||||
|
|
||||||
// AutoUpdateVersion client auto-update version
|
// AutoUpdateVersion client auto-update version
|
||||||
AutoUpdateVersion string `gorm:"default:'disabled'"`
|
AutoUpdateVersion string `gorm:"default:'disabled'"`
|
||||||
|
|
||||||
|
// EmbeddedIdpEnabled indicates if the embedded identity provider is enabled.
|
||||||
|
// This is a runtime-only field, not stored in the database.
|
||||||
|
EmbeddedIdpEnabled bool `gorm:"-"`
|
||||||
|
|
||||||
|
// LocalAuthDisabled indicates if local (email/password) authentication is disabled.
|
||||||
|
// This is a runtime-only field, not stored in the database.
|
||||||
|
LocalAuthDisabled bool `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy copies the Settings struct
|
// Copy copies the Settings struct
|
||||||
@@ -76,6 +84,8 @@ func (s *Settings) Copy() *Settings {
|
|||||||
DNSDomain: s.DNSDomain,
|
DNSDomain: s.DNSDomain,
|
||||||
NetworkRange: s.NetworkRange,
|
NetworkRange: s.NetworkRange,
|
||||||
AutoUpdateVersion: s.AutoUpdateVersion,
|
AutoUpdateVersion: s.AutoUpdateVersion,
|
||||||
|
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
|
||||||
|
LocalAuthDisabled: s.LocalAuthDisabled,
|
||||||
}
|
}
|
||||||
if s.Extra != nil {
|
if s.Extra != nil {
|
||||||
settings.Extra = s.Extra.Copy()
|
settings.Extra = s.Extra.Copy()
|
||||||
|
|||||||
@@ -191,6 +191,10 @@ func (am *DefaultAccountManager) createNewIdpUser(ctx context.Context, accountID
|
|||||||
// Unlike createNewIdpUser, this method fetches user data directly from the database
|
// Unlike createNewIdpUser, this method fetches user data directly from the database
|
||||||
// since the embedded IdP usage ensures the username and email are stored locally in the User table.
|
// since the embedded IdP usage ensures the username and email are stored locally in the User table.
|
||||||
func (am *DefaultAccountManager) createEmbeddedIdpUser(ctx context.Context, accountID string, inviterID string, invite *types.UserInfo) (*idp.UserData, error) {
|
func (am *DefaultAccountManager) createEmbeddedIdpUser(ctx context.Context, accountID string, inviterID string, invite *types.UserInfo) (*idp.UserData, error) {
|
||||||
|
if IsLocalAuthDisabled(ctx, am.idpManager) {
|
||||||
|
return nil, status.Errorf(status.PreconditionFailed, "local user creation is disabled - use an external identity provider")
|
||||||
|
}
|
||||||
|
|
||||||
inviter, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, inviterID)
|
inviter, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, inviterID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get inviter user: %w", err)
|
return nil, fmt.Errorf("failed to get inviter user: %w", err)
|
||||||
@@ -1462,6 +1466,10 @@ func (am *DefaultAccountManager) CreateUserInvite(ctx context.Context, accountID
|
|||||||
return nil, status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
return nil, status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if IsLocalAuthDisabled(ctx, am.idpManager) {
|
||||||
|
return nil, status.Errorf(status.PreconditionFailed, "local user creation is disabled - use an external identity provider")
|
||||||
|
}
|
||||||
|
|
||||||
if err := validateUserInvite(invite); err != nil {
|
if err := validateUserInvite(invite); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1621,6 +1629,10 @@ func (am *DefaultAccountManager) AcceptUserInvite(ctx context.Context, token, pa
|
|||||||
return status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
return status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if IsLocalAuthDisabled(ctx, am.idpManager) {
|
||||||
|
return status.Errorf(status.PreconditionFailed, "local user creation is disabled - use an external identity provider")
|
||||||
|
}
|
||||||
|
|
||||||
if password == "" {
|
if password == "" {
|
||||||
return status.Errorf(status.InvalidArgument, "password is required")
|
return status.Errorf(status.InvalidArgument, "password is required")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import "regexp"
|
|
||||||
|
|
||||||
var domainRegex = regexp.MustCompile(`^(\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$`)
|
|
||||||
|
|
||||||
// Difference returns the elements in `a` that aren't in `b`.
|
// Difference returns the elements in `a` that aren't in `b`.
|
||||||
func Difference(a, b []string) []string {
|
func Difference(a, b []string) []string {
|
||||||
mb := make(map[string]struct{}, len(b))
|
mb := make(map[string]struct{}, len(b))
|
||||||
@@ -55,9 +51,3 @@ func contains[T comparableObject[T]](slice []T, element T) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsValidDomain(domain string) bool {
|
|
||||||
if domain == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return domainRegex.MatchString(domain)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,30 @@ const maxDomains = 32
|
|||||||
|
|
||||||
var 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 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])?$`)
|
||||||
|
|
||||||
// ValidateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList.
|
// IsValidDomain checks if a single domain string is valid.
|
||||||
|
// Does not convert unicode to punycode - domain must already be ASCII/punycode.
|
||||||
|
// Allows wildcard prefix (*.example.com).
|
||||||
|
func IsValidDomain(domain string) bool {
|
||||||
|
if domain == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return domainRegex.MatchString(strings.ToLower(domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidDomainNoWildcard checks if a single domain string is valid without wildcard prefix.
|
||||||
|
// Use for zone domains and CNAME targets where wildcards are not allowed.
|
||||||
|
func IsValidDomainNoWildcard(domain string) bool {
|
||||||
|
if domain == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(domain, "*.") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return domainRegex.MatchString(strings.ToLower(domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDomains validates domains and converts unicode to punycode.
|
||||||
|
// Allows wildcard prefix (*.example.com). Maximum 32 domains.
|
||||||
func ValidateDomains(domains []string) (List, error) {
|
func ValidateDomains(domains []string) (List, error) {
|
||||||
if len(domains) == 0 {
|
if len(domains) == 0 {
|
||||||
return nil, fmt.Errorf("domains list is empty")
|
return nil, fmt.Errorf("domains list is empty")
|
||||||
@@ -37,7 +60,10 @@ func ValidateDomains(domains []string) (List, error) {
|
|||||||
return domainList, nil
|
return domainList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateDomainsList checks if each domain in the list is valid
|
// ValidateDomainsList validates domains without punycode conversion.
|
||||||
|
// Use this for domains that must already be in ASCII/punycode format (e.g., extra DNS labels).
|
||||||
|
// Unlike ValidateDomains, this does not convert unicode to punycode - unicode domains will fail.
|
||||||
|
// Allows wildcard prefix (*.example.com). Maximum 32 domains.
|
||||||
func ValidateDomainsList(domains []string) error {
|
func ValidateDomainsList(domains []string) error {
|
||||||
if len(domains) == 0 {
|
if len(domains) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ package domain
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateDomains(t *testing.T) {
|
func TestValidateDomains(t *testing.T) {
|
||||||
|
label63 := strings.Repeat("a", 63)
|
||||||
|
label64 := strings.Repeat("a", 64)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
domains []string
|
domains []string
|
||||||
@@ -26,6 +30,48 @@ func TestValidateDomains(t *testing.T) {
|
|||||||
expected: List{"sub.ex-ample.com"},
|
expected: List{"sub.ex-ample.com"},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Valid uppercase domain normalized to lowercase",
|
||||||
|
domains: []string{"EXAMPLE.COM"},
|
||||||
|
expected: List{"example.com"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid mixed case domain",
|
||||||
|
domains: []string{"ExAmPlE.CoM"},
|
||||||
|
expected: List{"example.com"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single letter TLD",
|
||||||
|
domains: []string{"example.x"},
|
||||||
|
expected: List{"example.x"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Two letter domain labels",
|
||||||
|
domains: []string{"a.b"},
|
||||||
|
expected: List{"a.b"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single character domain",
|
||||||
|
domains: []string{"x"},
|
||||||
|
expected: List{"x"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wildcard with single letter TLD",
|
||||||
|
domains: []string{"*.x"},
|
||||||
|
expected: List{"*.x"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multi-level with single letter labels",
|
||||||
|
domains: []string{"a.b.c"},
|
||||||
|
expected: List{"a.b.c"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Valid Unicode domain",
|
name: "Valid Unicode domain",
|
||||||
domains: []string{"münchen.de"},
|
domains: []string{"münchen.de"},
|
||||||
@@ -45,17 +91,92 @@ func TestValidateDomains(t *testing.T) {
|
|||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid domain format",
|
name: "Valid domain starting with digit",
|
||||||
|
domains: []string{"123.example.com"},
|
||||||
|
expected: List{"123.example.com"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
// Numeric TLDs are allowed for internal/private DNS use cases.
|
||||||
|
// While ICANN doesn't issue all-numeric gTLDs, the DNS protocol permits them
|
||||||
|
// and resolvers like systemd-resolved handle them correctly.
|
||||||
|
{
|
||||||
|
name: "Numeric TLD allowed",
|
||||||
|
domains: []string{"example.123"},
|
||||||
|
expected: List{"example.123"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single digit TLD allowed",
|
||||||
|
domains: []string{"example.1"},
|
||||||
|
expected: List{"example.1"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "All numeric labels allowed",
|
||||||
|
domains: []string{"123.456"},
|
||||||
|
expected: List{"123.456"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single numeric label allowed",
|
||||||
|
domains: []string{"123"},
|
||||||
|
expected: List{"123"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid domain with double hyphen",
|
||||||
|
domains: []string{"test--example.com"},
|
||||||
|
expected: List{"test--example.com"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid leading hyphen",
|
||||||
domains: []string{"-example.com"},
|
domains: []string{"-example.com"},
|
||||||
expected: nil,
|
expected: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid domain format 2",
|
name: "Invalid trailing hyphen",
|
||||||
domains: []string{"example.com-"},
|
domains: []string{"example.com-"},
|
||||||
expected: nil,
|
expected: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid leading dot",
|
||||||
|
domains: []string{".com"},
|
||||||
|
expected: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid dot only",
|
||||||
|
domains: []string{"."},
|
||||||
|
expected: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid double dot",
|
||||||
|
domains: []string{"example..com"},
|
||||||
|
expected: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid special characters",
|
||||||
|
domains: []string{"example?,.com"},
|
||||||
|
expected: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid space in domain",
|
||||||
|
domains: []string{"space .example.com"},
|
||||||
|
expected: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid trailing space",
|
||||||
|
domains: []string{"example.com "},
|
||||||
|
expected: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Multiple domains valid and invalid",
|
name: "Multiple domains valid and invalid",
|
||||||
domains: []string{"google.com", "invalid,nbdomain.com", "münchen.de"},
|
domains: []string{"google.com", "invalid,nbdomain.com", "münchen.de"},
|
||||||
@@ -86,6 +207,30 @@ func TestValidateDomains(t *testing.T) {
|
|||||||
expected: nil,
|
expected: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Valid 63 char label (max)",
|
||||||
|
domains: []string{label63 + ".com"},
|
||||||
|
expected: List{Domain(label63 + ".com")},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid 64 char label (exceeds max)",
|
||||||
|
domains: []string{label64 + ".com"},
|
||||||
|
expected: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid 253 char domain (max)",
|
||||||
|
domains: []string{strings.Repeat("a.", 126) + "a"},
|
||||||
|
expected: List{Domain(strings.Repeat("a.", 126) + "a")},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid 254+ char domain (exceeds max)",
|
||||||
|
domains: []string{strings.Repeat("ab.", 85)},
|
||||||
|
expected: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -118,6 +263,57 @@ func TestValidateDomainsList(t *testing.T) {
|
|||||||
domains: []string{"sub.ex-ample.com"},
|
domains: []string{"sub.ex-ample.com"},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Uppercase domain accepted",
|
||||||
|
domains: []string{"EXAMPLE.COM"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single letter TLD",
|
||||||
|
domains: []string{"example.x"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Two letter domain labels",
|
||||||
|
domains: []string{"a.b"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single character domain",
|
||||||
|
domains: []string{"x"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wildcard with single letter TLD",
|
||||||
|
domains: []string{"*.x"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multi-level with single letter labels",
|
||||||
|
domains: []string{"a.b.c"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
// Numeric TLDs are allowed for internal/private DNS use cases.
|
||||||
|
{
|
||||||
|
name: "Numeric TLD allowed",
|
||||||
|
domains: []string{"example.123"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single digit TLD allowed",
|
||||||
|
domains: []string{"example.1"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "All numeric labels allowed",
|
||||||
|
domains: []string{"123.456"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single numeric label allowed",
|
||||||
|
domains: []string{"123"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Underscores in labels",
|
name: "Underscores in labels",
|
||||||
domains: []string{"_jabber._tcp.gmail.com"},
|
domains: []string{"_jabber._tcp.gmail.com"},
|
||||||
|
|||||||
@@ -294,6 +294,11 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
readOnly: true
|
readOnly: true
|
||||||
example: false
|
example: false
|
||||||
|
local_auth_disabled:
|
||||||
|
description: Indicates whether local (email/password) authentication is disabled. When true, users can only authenticate via external identity providers. This is a read-only field.
|
||||||
|
type: boolean
|
||||||
|
readOnly: true
|
||||||
|
example: false
|
||||||
required:
|
required:
|
||||||
- peer_login_expiration_enabled
|
- peer_login_expiration_enabled
|
||||||
- peer_login_expiration
|
- peer_login_expiration
|
||||||
|
|||||||
@@ -415,6 +415,9 @@ type AccountSettings struct {
|
|||||||
// LazyConnectionEnabled Enables or disables experimental lazy connection
|
// LazyConnectionEnabled Enables or disables experimental lazy connection
|
||||||
LazyConnectionEnabled *bool `json:"lazy_connection_enabled,omitempty"`
|
LazyConnectionEnabled *bool `json:"lazy_connection_enabled,omitempty"`
|
||||||
|
|
||||||
|
// LocalAuthDisabled Indicates whether local (email/password) authentication is disabled. When true, users can only authenticate via external identity providers. This is a read-only field.
|
||||||
|
LocalAuthDisabled *bool `json:"local_auth_disabled,omitempty"`
|
||||||
|
|
||||||
// NetworkRange Allows to define a custom network range for the account in CIDR format
|
// NetworkRange Allows to define a custom network range for the account in CIDR format
|
||||||
NetworkRange *string `json:"network_range,omitempty"`
|
NetworkRange *string `json:"network_range,omitempty"`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user