Merge remote-tracking branch 'origin/main' into embedded-vnc

# Conflicts:
#	client/cmd/up.go
#	client/internal/engine.go
#	client/internal/profilemanager/config.go
#	client/proto/daemon.pb.go
#	client/server/mdm.go
#	client/server/server.go
#	client/status/status.go
#	client/ui/main.go
#	client/ui/services/windowmanager.go
#	management/internals/shared/grpc/conversion.go
#	management/server/store/sql_store.go
#	management/server/store/sql_store_test.go
#	shared/management/networkmap/envelope.go
#	shared/management/proto/management.pb.go
This commit is contained in:
Viktor Liu
2026-10-19 19:49:54 +09:00
804 changed files with 72029 additions and 25943 deletions
@@ -0,0 +1,227 @@
package certposture
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/shared/management/certposture/certtest"
)
var (
secret = []byte("test-secret")
peerKey = []byte("peer-public-key-aaaaaaaaaaaaaaaa")
otherKey = []byte("peer-public-key-bbbbbbbbbbbbbbbb")
now = time.Now().Truncate(Window)
)
func TestChallenger_NonceIsStableWithinWindowAndPerPeer(t *testing.T) {
c := NewChallenger(secret)
assert.Equal(t, c.Nonce(peerKey, now), c.Nonce(peerKey, now.Add(time.Minute)))
assert.NotEqual(t, c.Nonce(peerKey, now), c.Nonce(peerKey, now.Add(Window)))
assert.NotEqual(t, c.Nonce(peerKey, now), c.Nonce(otherKey, now))
assert.NotEqual(t, c.Nonce(peerKey, now), NewChallenger([]byte("other")).Nonce(peerKey, now))
}
func TestChallenger_VerifyNonce(t *testing.T) {
c := NewChallenger(secret)
nonce := c.Nonce(peerKey, now)
tests := []struct {
name string
nonce []byte
peerKey []byte
at time.Time
wantErr error
}{
{"current window", nonce, peerKey, now, nil},
{"previous window still accepted", nonce, peerKey, now.Add(Window), nil},
{"two windows later expired", nonce, peerKey, now.Add(2 * Window), ErrNonceExpired},
// An instance whose clock runs a little ahead issues the next window's nonce early.
{"issued one window ahead accepted", c.Nonce(peerKey, now.Add(Window)), peerKey, now, nil},
{"issued two windows ahead rejected", c.Nonce(peerKey, now.Add(2*Window)), peerKey, now, ErrNonceExpired},
{"other peer", nonce, otherKey, now, ErrNonceMismatch},
{"tampered mac", tamper(nonce, len(nonce)-1), peerKey, now, ErrNonceMismatch},
{"malformed", nonce[:10], peerKey, now, ErrNonceMalformed},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := c.verifyNonce(tt.nonce, tt.peerKey, tt.at)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
// TestNonceAcceptedAlongside must agree with verifyNonce: a proof for a nonce is still
// accepted while the next window's nonce is issued, and no longer two windows on.
func TestNonceAcceptedAlongside(t *testing.T) {
c := NewChallenger(secret)
issued := c.Nonce(peerKey, now)
assert.True(t, NonceAcceptedAlongside(issued, issued), "the current nonce is accepted")
assert.True(t, NonceAcceptedAlongside(issued, c.Nonce(peerKey, now.Add(Window))), "a proof from the previous window is accepted")
assert.False(t, NonceAcceptedAlongside(issued, c.Nonce(peerKey, now.Add(2*Window))), "a proof two windows old is not")
assert.True(t, NonceAcceptedAlongside(c.Nonce(peerKey, now.Add(Window)), issued), "a nonce from an instance one window ahead is accepted")
assert.False(t, NonceAcceptedAlongside(c.Nonce(peerKey, now.Add(2*Window)), issued), "a nonce two windows ahead is not")
assert.False(t, NonceAcceptedAlongside([]byte("short"), issued), "a malformed nonce is not")
for _, tt := range []struct {
later time.Duration
want bool
}{{0, true}, {Window, true}, {2 * Window, false}} {
err := c.verifyNonce(issued, peerKey, now.Add(tt.later))
assert.Equal(t, tt.want, err == nil, "verifyNonce agrees %s later", tt.later)
}
}
func TestSignVerify_RoundTripPerKeyType(t *testing.T) {
ca := certtest.NewCA(t, "root")
keys := map[string]crypto.Signer{
SigAlgECDSASHA256: certtest.ECDSAKey(t),
SigAlgECDSASHA384: p384Key(t),
SigAlgRSAPSSSHA256: certtest.RSAKey(t),
SigAlgEd25519: certtest.Ed25519Key(t),
}
for wantAlg, key := range keys {
t.Run(wantAlg, func(t *testing.T) {
c := NewChallenger(secret)
proof := signedProof(t, c, ca, key)
assert.Equal(t, wantAlg, proof.SigAlg)
chain, err := c.Verify(proof, peerKey, now)
require.NoError(t, err)
require.Len(t, chain, 1)
assert.NoError(t, VerifyChain(chain, mustPool(t, ca.PEM), now))
})
}
}
func p384Key(t *testing.T) crypto.Signer {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
require.NoError(t, err)
return key
}
// TestSign_RefusesCurvesTheProofDoesNotName: a proof names its algorithm from the key, and
// the verifier only knows P-256 and P-384, so P-521 is refused before anything is signed.
func TestSign_RefusesCurvesTheProofDoesNotName(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
require.NoError(t, err)
_, _, err = Sign(key, NewChallenger(secret).Nonce(peerKey, now), peerKey)
assert.ErrorIs(t, err, ErrUnsupportedKey)
}
func TestVerify_Rejections(t *testing.T) {
ca := certtest.NewCA(t, "root")
c := NewChallenger(secret)
good := signedProof(t, c, ca, certtest.ECDSAKey(t))
tests := []struct {
name string
mutate func(p Proof) Proof
peerKey []byte
wantErr error
}{
{"replayed for other peer", identity, otherKey, ErrNonceMismatch},
{"signature tampered", func(p Proof) Proof { p.Signature = tamper(p.Signature, 5); return p }, peerKey, ErrSignatureInvalid},
{"nonce swapped after signing", func(p Proof) Proof { p.Nonce = c.Nonce(peerKey, now.Add(-Window)); return p }, peerKey, ErrSignatureInvalid},
{"foreign leaf presented", func(p Proof) Proof {
p.Chain = [][]byte{ca.Issue(t, certtest.ECDSAKey(t), "other").Raw}
return p
}, peerKey, ErrSignatureInvalid},
{"alg mismatch", func(p Proof) Proof { p.SigAlg = SigAlgEd25519; return p }, peerKey, ErrSigAlgMismatch},
{"empty chain", func(p Proof) Proof { p.Chain = nil; return p }, peerKey, ErrEmptyChain},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := c.Verify(tt.mutate(good), tt.peerKey, now)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestVerify_SecretMismatchAcrossChallengers(t *testing.T) {
ca := certtest.NewCA(t, "root")
proof := signedProof(t, NewChallenger(secret), ca, certtest.ECDSAKey(t))
_, err := NewChallenger([]byte("other-instance-secret")).Verify(proof, peerKey, now)
assert.ErrorIs(t, err, ErrNonceMismatch)
}
func TestChainMatchesCAs(t *testing.T) {
ca := certtest.NewCA(t, "root")
otherCA := certtest.NewCA(t, "other-root")
leaf := ca.Issue(t, certtest.ECDSAKey(t), "device")
chainPEM := EncodeChainPEM([]*x509.Certificate{leaf})
assert.True(t, ChainMatchesCAs(chainPEM, []string{ca.PEM}, now))
assert.True(t, ChainMatchesCAs(chainPEM, []string{otherCA.PEM, ca.PEM}, now))
assert.False(t, ChainMatchesCAs(chainPEM, []string{otherCA.PEM}, now))
assert.False(t, ChainMatchesCAs(chainPEM, []string{ca.PEM}, now.Add(30*24*time.Hour)))
assert.False(t, ChainMatchesCAs(chainPEM, []string{"not a pem"}, now))
assert.False(t, ChainMatchesCAs("not a pem", []string{ca.PEM}, now))
}
func TestChainPEM_RoundTrip(t *testing.T) {
ca := certtest.NewCA(t, "root")
leaf := ca.Issue(t, certtest.ECDSAKey(t), "device")
chain, err := ParseChainPEM(EncodeChainPEM([]*x509.Certificate{leaf, ca.Cert}))
require.NoError(t, err)
require.Len(t, chain, 2)
assert.Equal(t, leaf.Raw, chain[0].Raw)
assert.Equal(t, ca.Cert.Raw, chain[1].Raw)
}
func TestVerify_AcceptsMaximumPSSSalt(t *testing.T) {
// A TPM chooses the PSS salt itself and older firmware uses the largest salt that
// fits, so a proof from such a key carries more salt than Sign asks a software key for.
ca := certtest.NewCA(t, "root")
key := certtest.RSAKey(t).(*rsa.PrivateKey)
leaf := ca.Issue(t, key, "device")
c := NewChallenger(secret)
nonce := c.Nonce(peerKey, now)
digest := sha256.Sum256(proofMessage(nonce, peerKey))
sig, err := rsa.SignPSS(rand.Reader, key, crypto.SHA256, digest[:], &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthAuto})
require.NoError(t, err)
_, err = c.Verify(Proof{Nonce: nonce, Chain: [][]byte{leaf.Raw}, SigAlg: SigAlgRSAPSSSHA256, Signature: sig}, peerKey, now)
assert.NoError(t, err, "a valid PSS signature must verify regardless of salt length")
}
func signedProof(t *testing.T, c *Challenger, ca *certtest.CA, key crypto.Signer) Proof {
t.Helper()
leaf := ca.Issue(t, key, "device")
nonce := c.Nonce(peerKey, now)
sigAlg, sig, err := Sign(key, nonce, peerKey)
require.NoError(t, err)
return Proof{Nonce: nonce, Chain: [][]byte{leaf.Raw}, SigAlg: sigAlg, Signature: sig}
}
func mustPool(t *testing.T, pems ...string) *x509.CertPool {
t.Helper()
pool, err := ParseCAs(pems)
require.NoError(t, err)
return pool
}
func identity(p Proof) Proof { return p }
func tamper(b []byte, i int) []byte {
out := append([]byte(nil), b...)
out[i] ^= 0xff
return out
}
@@ -0,0 +1,109 @@
// Package certtest builds throwaway CAs and leaf certificates for certificate posture tests.
package certtest
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"testing"
"time"
"github.com/stretchr/testify/require"
)
type CA struct {
Cert *x509.Certificate
Key crypto.Signer
PEM string
}
func NewCA(t *testing.T, name string) *CA {
t.Helper()
return newCA(t, name, nil)
}
// NewIntermediate creates a CA signed by parent.
func NewIntermediate(t *testing.T, parent *CA, name string) *CA {
t.Helper()
return newCA(t, name, parent)
}
func newCA(t *testing.T, name string, parent *CA) *CA {
t.Helper()
key := ECDSAKey(t)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{CommonName: name},
NotBefore: time.Now().Add(-24 * time.Hour),
NotAfter: time.Now().Add(48 * time.Hour),
IsCA: true,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
}
issuer, issuerKey := tmpl, key
if parent != nil {
issuer, issuerKey = parent.Cert, parent.Key
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, issuer, key.Public(), issuerKey)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
return &CA{Cert: cert, Key: key, PEM: CertPEM(cert)}
}
// Issue signs a leaf certificate for key with the CA.
func (ca *CA) Issue(t *testing.T, key crypto.Signer, cn string) *x509.Certificate {
t.Helper()
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{CommonName: cn},
NotBefore: time.Now().Add(-24 * time.Hour),
NotAfter: time.Now().Add(48 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, ca.Cert, key.Public(), ca.Key)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
return cert
}
func ECDSAKey(t *testing.T) crypto.Signer {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
return key
}
func RSAKey(t *testing.T) crypto.Signer {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
return key
}
func Ed25519Key(t *testing.T) crypto.Signer {
t.Helper()
_, key, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
return key
}
func CertPEM(cert *x509.Certificate) string {
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))
}
func KeyPEM(t *testing.T, key crypto.Signer) string {
t.Helper()
der, err := x509.MarshalPKCS8PrivateKey(key)
require.NoError(t, err)
return string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}))
}
+121
View File
@@ -0,0 +1,121 @@
package certposture
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"strings"
"time"
)
var ErrNoCertificateInPEM = errors.New("no certificate found in PEM data")
// ParseCAs builds a root pool from PEM encoded CA certificates.
func ParseCAs(pems []string) (*x509.CertPool, error) {
roots := x509.NewCertPool()
for _, p := range pems {
if !roots.AppendCertsFromPEM([]byte(p)) {
return nil, ErrNoCertificateInPEM
}
}
return roots, nil
}
// VerifyChain reports whether the leaf (chain[0]) chains to one of roots using the
// remaining certificates as intermediates.
func VerifyChain(chain []*x509.Certificate, roots *x509.CertPool, now time.Time) error {
if len(chain) == 0 {
return ErrEmptyChain
}
_, err := VerifiedChain(chain[0], chain[1:], roots, now)
return err
}
// VerifiedChain finds a path from leaf to one of roots through any of intermediates,
// trying every candidate issuer rather than the first that matches, and returns it
// without the root, leaf first. It applies the same rules as VerifyChain.
func VerifiedChain(leaf *x509.Certificate, intermediates []*x509.Certificate, roots *x509.CertPool, now time.Time) ([]*x509.Certificate, error) {
pool := x509.NewCertPool()
for _, cert := range intermediates {
pool.AddCert(cert)
}
chains, err := leaf.Verify(x509.VerifyOptions{
Roots: roots,
Intermediates: pool,
CurrentTime: now,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
})
if err != nil {
return nil, err
}
chain := chains[0]
if len(chain) > 1 {
chain = chain[:len(chain)-1]
}
return chain, nil
}
// ChainMatchesCAs is VerifyChain over the PEM forms stored in peer meta and check config.
func ChainMatchesCAs(chainPEM string, caPEMs []string, now time.Time) bool {
roots, err := ParseCAs(caPEMs)
if err != nil {
return false
}
return chainMatchesPool(chainPEM, roots, now)
}
// AnyChainMatchesCAs reports whether at least one of the peer's verified chains
// is anchored in one of the configured CAs.
func AnyChainMatchesCAs(chainPEMs, caPEMs []string, now time.Time) bool {
roots, err := ParseCAs(caPEMs)
if err != nil {
return false
}
for _, chainPEM := range chainPEMs {
if chainMatchesPool(chainPEM, roots, now) {
return true
}
}
return false
}
func chainMatchesPool(chainPEM string, roots *x509.CertPool, now time.Time) bool {
chain, err := ParseChainPEM(chainPEM)
if err != nil {
return false
}
return VerifyChain(chain, roots, now) == nil
}
func EncodeChainPEM(chain []*x509.Certificate) string {
var b strings.Builder
for _, cert := range chain {
_ = pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
}
return b.String()
}
func ParseChainPEM(chainPEM string) ([]*x509.Certificate, error) {
var chain []*x509.Certificate
rest := []byte(chainPEM)
for {
var block *pem.Block
block, rest = pem.Decode(rest)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parse certificate: %w", err)
}
chain = append(chain, cert)
}
if len(chain) == 0 {
return nil, ErrNoCertificateInPEM
}
return chain, nil
}
+139
View File
@@ -0,0 +1,139 @@
package certposture
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"errors"
"os"
"sync"
"time"
log "github.com/sirupsen/logrus"
)
const (
// Window is the default challenge window. A nonce stays valid through the window
// after the one it was issued in, so a peer re-proves possession of its key between
// once and twice per window. One window early is accepted too, for clock skew.
Window = 12 * time.Hour
// EnvWindow overrides Window, for end-to-end tests that cannot wait half a day to
// watch a renewal. Every management instance has to be given the same value: the
// window is part of the nonce, so instances that disagree reject each other's.
EnvWindow = "NB_CERT_CHALLENGE_WINDOW"
// minWindow keeps the renewal period, a third of the window, well above the
// refresher's one-second tick.
minWindow = 30 * time.Second
maxWindow = 24 * time.Hour
challengeDomain = "netbird-cert-challenge-v1"
windowLen = 8
nonceLen = windowLen + sha256.Size
// NonceSize is the length of every nonce a Challenger issues.
NonceSize = nonceLen
)
var effectiveWindow = sync.OnceValue(resolveWindow)
// EffectiveWindow returns the challenge window in force, which is Window unless
// EnvWindow overrides it. Everything timed against the window derives from this, so a
// test that shortens it shortens the renewal that goes with it.
func EffectiveWindow() time.Duration {
return effectiveWindow()
}
func resolveWindow() time.Duration {
val := os.Getenv(EnvWindow)
if val == "" {
return Window
}
window, err := time.ParseDuration(val)
if err != nil {
log.Warnf("failed to parse %s, keeping the %s certificate challenge window: %v", EnvWindow, Window, err)
return Window
}
if window < minWindow || window > maxWindow {
log.Warnf("%s of %s is outside %s..%s, keeping the %s certificate challenge window", EnvWindow, window, minWindow, maxWindow, Window)
return Window
}
if window%time.Second != 0 {
log.Warnf("%s of %s is not a whole number of seconds, keeping the %s certificate challenge window", EnvWindow, window, Window)
return Window
}
// Loud on purpose: this sets how long a device can pass the certificate check after
// its key has gone, and it has to match on every instance.
log.Warnf("certificate challenge window overridden to %s by %s", window, EnvWindow)
return window
}
var (
ErrNonceMalformed = errors.New("certificate challenge nonce is malformed")
ErrNonceExpired = errors.New("certificate challenge nonce is expired")
ErrNonceMismatch = errors.New("certificate challenge nonce was not issued to this peer")
)
// Challenger issues and verifies stateless per-peer nonces. A nonce is bound to the
// peer and to a time window, so any instance sharing the secret can verify it.
type Challenger struct {
secret []byte
window time.Duration
}
func NewChallenger(secret []byte) *Challenger {
return &Challenger{secret: secret, window: EffectiveWindow()}
}
func (c *Challenger) Nonce(peerKey []byte, now time.Time) []byte {
return c.nonceForWindow(peerKey, c.windowOf(now))
}
func (c *Challenger) verifyNonce(nonce, peerKey []byte, now time.Time) error {
if len(nonce) != nonceLen {
return ErrNonceMalformed
}
window := binary.BigEndian.Uint64(nonce[:windowLen])
if !windowAccepted(window, c.windowOf(now)) {
return ErrNonceExpired
}
if !hmac.Equal(nonce, c.nonceForWindow(peerKey, window)) {
return ErrNonceMismatch
}
return nil
}
func (c *Challenger) windowOf(now time.Time) uint64 {
return uint64(now.Unix() / int64(c.window.Seconds()))
}
func (c *Challenger) nonceForWindow(peerKey []byte, window uint64) []byte {
nonce := make([]byte, windowLen, nonceLen)
binary.BigEndian.PutUint64(nonce, window)
mac := hmac.New(sha256.New, c.secret)
mac.Write([]byte(challengeDomain))
mac.Write(peerKey)
mac.Write(nonce[:windowLen])
return mac.Sum(nonce)
}
// NonceAcceptedAlongside reports whether a proof answering nonce is still accepted while
// management issues current to the same peer, by the same window rule verification uses.
func NonceAcceptedAlongside(nonce, current []byte) bool {
if len(nonce) != nonceLen || len(current) != nonceLen {
return false
}
window := binary.BigEndian.Uint64(nonce[:windowLen])
return windowAccepted(window, binary.BigEndian.Uint64(current[:windowLen]))
}
// windowAccepted reports whether a nonce of window is accepted in window current: the
// current window, the previous one so a nonce outlives a rollover, and the next one so an
// instance whose clock runs slightly ahead is not rejected by the others.
func windowAccepted(window, current uint64) bool {
return window == current || window+1 == current || window == current+1
}
@@ -0,0 +1,108 @@
package certposture
import (
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestResolveWindow(t *testing.T) {
// An end-to-end run shortens the window so a renewal can be watched in seconds
// rather than half a day. Anything it cannot make sense of leaves the default in
// place, because a window nobody intended is a security property nobody chose.
tests := []struct {
name string
env string
want time.Duration
}{
{name: "unset keeps the default", env: "", want: Window},
{name: "a test-sized window is taken", env: "30s", want: 30 * time.Second},
{name: "garbage keeps the default", env: "soon", want: Window},
{name: "below the floor keeps the default", env: "10ms", want: Window},
{name: "too short for the refresh tick keeps the default", env: "1s", want: Window},
{name: "a fractional second keeps the default", env: "30500ms", want: Window},
{name: "above the ceiling keeps the default", env: "100h", want: Window},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(EnvWindow, tt.env)
if tt.env == "" {
require.NoError(t, os.Unsetenv(EnvWindow))
}
assert.Equal(t, tt.want, resolveWindow())
})
}
}
func TestChallenger_HonoursAShortenedWindow(t *testing.T) {
// The window is what a nonce is stamped with, so a shortened one has to make a
// nonce expire sooner, not just change a number in a log line.
short := 2 * time.Second
c := &Challenger{secret: []byte("secret"), window: short}
peerKey := []byte("peer-key")
issued := time.Unix(1_790_000_000, 0)
nonce := c.Nonce(peerKey, issued)
require.NoError(t, c.verifyNonce(nonce, peerKey, issued), "a nonce is valid when issued")
require.NoError(t, c.verifyNonce(nonce, peerKey, issued.Add(short)), "and through the window after it")
assert.ErrorIs(t, c.verifyNonce(nonce, peerKey, issued.Add(3*short)), ErrNonceExpired,
"a shortened window must actually expire the nonce sooner")
}
// heldNonceAlwaysAccepted walks ten windows and, at every step, checks the nonce the peer
// would be holding if it were re-stamped every period starting at offset. It returns how
// many of those checks would have rejected the peer.
func heldNonceAlwaysAccepted(c *Challenger, peerKey []byte, period, offset time.Duration) int {
start := time.Unix(0, 0).UTC().Add(offset)
var rejected int
for elapsed := time.Duration(0); elapsed < 10*Window; elapsed += 7 * time.Minute {
periods := int64(elapsed / period)
lastRefresh := start.Add(time.Duration(periods) * period)
if c.verifyNonce(c.Nonce(peerKey, lastRefresh), peerKey, start.Add(elapsed)) != nil {
rejected++
}
}
return rejected
}
// TestNonce_StaysValidWhenRestampedWithinAWindow is the reason management keeps no per-peer
// nonce state. A nonce carries the window it was minted in, not the instant, and is accepted
// for that window and the next. A peer re-stamped at least once per window therefore can
// never be holding one that has fallen outside the accepted pair, whoever it is and whenever
// it was last served. There is nothing to track and nothing to search for: the guarantee
// comes from the cadence alone.
//
// The offsets matter: each account is given a phase of its own so a restart does not fan out
// to every account at once, so the property has to hold off the window boundary too.
func TestNonce_StaysValidWhenRestampedWithinAWindow(t *testing.T) {
c := &Challenger{secret: []byte("secret"), window: Window}
peerKey := []byte("peer-key")
for _, period := range []time.Duration{Window / 3, Window / 2, Window} {
for _, offset := range []time.Duration{0, Window / 7, Window / 2, Window - time.Minute} {
t.Run(period.String()+"+"+offset.String(), func(t *testing.T) {
assert.Zero(t, heldNonceAlwaysAccepted(c, peerKey, period, offset),
"a peer re-stamped every %v is never left holding an expired nonce", period)
})
}
}
}
func TestNonce_ExpiresWhenRestampedTooSlowly(t *testing.T) {
// The counterpart, which is what gives the test above its teeth. Note the aligned case
// does not fail: a cadence of exactly two windows that lands on the boundaries is
// covered by the grace window. Off the boundary it is not, and real refreshes are off
// the boundary by design.
c := &Challenger{secret: []byte("secret"), window: Window}
peerKey := []byte("peer-key")
assert.Zero(t, heldNonceAlwaysAccepted(c, peerKey, 2*Window, 0),
"two windows exactly on the boundary happens to be covered by the grace window")
assert.NotZero(t, heldNonceAlwaysAccepted(c, peerKey, 2*Window, Window/2),
"the same cadence off the boundary must leave the peer rejected for a stretch")
}
+154
View File
@@ -0,0 +1,154 @@
package certposture
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"crypto/x509"
"errors"
"fmt"
"time"
)
const (
SigAlgECDSASHA256 = "ecdsa-sha256"
SigAlgECDSASHA384 = "ecdsa-sha384"
SigAlgRSAPSSSHA256 = "rsa-pss-sha256"
SigAlgEd25519 = "ed25519"
proofDomain = "netbird-posture-cert-v1"
minRSABits = 2048
)
var (
ErrUnsupportedKey = errors.New("unsupported certificate key")
ErrEmptyChain = errors.New("certificate chain is empty")
ErrSignatureInvalid = errors.New("certificate proof signature is invalid")
ErrSigAlgMismatch = errors.New("signature algorithm does not match the certificate key")
)
// Proof is a client's demonstration that it holds the private key of the leaf
// certificate in Chain, made by signing the challenge nonce bound to its peer key.
type Proof struct {
Nonce []byte
Chain [][]byte
SigAlg string
Signature []byte
}
// Sign produces the proof signature for a nonce using the leaf's private key. The key
// never leaves the signer, which may be backed by a file, a TPM or an OS keystore.
func Sign(signer crypto.Signer, nonce, peerKey []byte) (string, []byte, error) {
sigAlg, err := sigAlgFor(signer.Public())
if err != nil {
return "", nil, err
}
msg := proofMessage(nonce, peerKey)
var sig []byte
switch sigAlg {
case SigAlgECDSASHA256:
d := sha256.Sum256(msg)
sig, err = signer.Sign(rand.Reader, d[:], crypto.SHA256)
case SigAlgECDSASHA384:
d := sha512.Sum384(msg)
sig, err = signer.Sign(rand.Reader, d[:], crypto.SHA384)
case SigAlgRSAPSSSHA256:
d := sha256.Sum256(msg)
sig, err = signer.Sign(rand.Reader, d[:], &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash, Hash: crypto.SHA256})
case SigAlgEd25519:
sig, err = signer.Sign(rand.Reader, msg, crypto.Hash(0))
}
if err != nil {
return "", nil, fmt.Errorf("sign certificate proof: %w", err)
}
return sigAlg, sig, nil
}
// Verify checks that the proof's nonce was issued by this challenger to peerKey and is
// still fresh, and that the signature validates against the leaf's public key. It
// returns the parsed chain on success. Chain trust is deliberately not evaluated here.
func (c *Challenger) Verify(proof Proof, peerKey []byte, now time.Time) ([]*x509.Certificate, error) {
if err := c.verifyNonce(proof.Nonce, peerKey, now); err != nil {
return nil, err
}
chain, err := parseChain(proof.Chain)
if err != nil {
return nil, err
}
leaf := chain[0]
sigAlg, err := sigAlgFor(leaf.PublicKey)
if err != nil {
return nil, err
}
if sigAlg != proof.SigAlg {
return nil, ErrSigAlgMismatch
}
if !verifySignature(leaf.PublicKey, sigAlg, proofMessage(proof.Nonce, peerKey), proof.Signature) {
return nil, ErrSignatureInvalid
}
return chain, nil
}
func proofMessage(nonce, peerKey []byte) []byte {
msg := make([]byte, 0, len(proofDomain)+len(nonce)+len(peerKey))
msg = append(msg, proofDomain...)
msg = append(msg, nonce...)
return append(msg, peerKey...)
}
func sigAlgFor(pub crypto.PublicKey) (string, error) {
switch k := pub.(type) {
case *ecdsa.PublicKey:
switch k.Curve {
case elliptic.P256():
return SigAlgECDSASHA256, nil
case elliptic.P384():
return SigAlgECDSASHA384, nil
}
case *rsa.PublicKey:
if k.N.BitLen() >= minRSABits {
return SigAlgRSAPSSSHA256, nil
}
case ed25519.PublicKey:
return SigAlgEd25519, nil
}
return "", ErrUnsupportedKey
}
func verifySignature(pub crypto.PublicKey, sigAlg string, msg, sig []byte) bool {
switch sigAlg {
case SigAlgECDSASHA256:
d := sha256.Sum256(msg)
return ecdsa.VerifyASN1(pub.(*ecdsa.PublicKey), d[:], sig)
case SigAlgECDSASHA384:
d := sha512.Sum384(msg)
return ecdsa.VerifyASN1(pub.(*ecdsa.PublicKey), d[:], sig)
case SigAlgRSAPSSSHA256:
d := sha256.Sum256(msg)
return rsa.VerifyPSS(pub.(*rsa.PublicKey), crypto.SHA256, d[:], sig, &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthAuto}) == nil
case SigAlgEd25519:
return ed25519.Verify(pub.(ed25519.PublicKey), msg, sig)
}
return false
}
func parseChain(der [][]byte) ([]*x509.Certificate, error) {
if len(der) == 0 {
return nil, ErrEmptyChain
}
chain := make([]*x509.Certificate, 0, len(der))
for _, raw := range der {
cert, err := x509.ParseCertificate(raw)
if err != nil {
return nil, fmt.Errorf("parse certificate: %w", err)
}
chain = append(chain, cert)
}
return chain, nil
}
+5 -1
View File
@@ -21,7 +21,11 @@ type Client interface {
// is not eligible for session extension.
ExtendAuthSession(sysInfo *system.Info, jwtToken string) (*proto.ExtendAuthSessionResponse, error)
GetDeviceAuthorizationFlow() (*proto.DeviceAuthorizationFlow, error)
GetPKCEAuthorizationFlow() (*proto.PKCEAuthorizationFlow, error)
// GetPKCEAuthorizationFlow returns the PKCE authorization flow information.
//
// sessionExtend marks the flow as renewing an existing peer's session rather
// than logging one in. See PKCEAuthorizationFlowRequest.
GetPKCEAuthorizationFlow(sessionExtend bool) (*proto.PKCEAuthorizationFlow, error)
GetServerURL() string
// IsHealthy returns the current connection status without blocking.
// Used by the engine to monitor connectivity in the background.
+12 -8
View File
@@ -10,10 +10,10 @@ import (
"testing"
"time"
"go.uber.org/mock/gomock"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
@@ -35,7 +35,6 @@ import (
"github.com/netbirdio/netbird/management/server/activity"
nbcache "github.com/netbirdio/netbird/management/server/cache"
"github.com/netbirdio/netbird/management/server/groups"
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
"github.com/netbirdio/netbird/management/server/mock_server"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/settings"
@@ -128,8 +127,8 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := mgmt.NewAccountRequestBuffer(ctx, store)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManger), config, nil)
accountManager, err := mgmt.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false, cacheStore)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", ephemeral_manager.NewEphemeralManager(store, peersManger), config, nil)
accountManager, err := mgmt.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, settingsMockManager, permissionsManagerMock, false, cacheStore)
if err != nil {
t.Fatal(err)
}
@@ -666,7 +665,12 @@ func Test_GetPKCEAuthorizationFlow(t *testing.T) {
},
}
var gotRequest mgmtProto.PKCEAuthorizationFlowRequest
mgmtMockServer.GetPKCEAuthorizationFlowFunc = func(ctx context.Context, req *mgmtProto.EncryptedMessage) (*mgmtProto.EncryptedMessage, error) {
if err := encryption.DecryptMessage(client.key.PublicKey(), serverKey, req.Body, &gotRequest); err != nil {
return nil, err
}
encryptedResp, err := encryption.EncryptMessage(client.key.PublicKey(), serverKey, expectedFlowInfo)
if err != nil {
return nil, err
@@ -679,10 +683,10 @@ func Test_GetPKCEAuthorizationFlow(t *testing.T) {
}, nil
}
flowInfo, err := client.GetPKCEAuthorizationFlow()
if err != nil {
t.Error("error while retrieving pkce auth flow information")
}
flowInfo, err := client.GetPKCEAuthorizationFlow(true)
require.NoError(t, err)
assert.True(t, gotRequest.GetSessionExtend(), "session extend should reach the server")
assert.Equal(t, expectedFlowInfo.ProviderConfig.ClientID, flowInfo.ProviderConfig.ClientID, "provider configured client ID should match")
assert.Equal(t, expectedFlowInfo.ProviderConfig.ClientSecret, flowInfo.ProviderConfig.ClientSecret, "provider configured client secret should match") //nolint:staticcheck
+29 -5
View File
@@ -621,7 +621,13 @@ func (c *GrpcClient) login(req *proto.LoginRequest) (*proto.LoginResponse, error
err = backoff.Retry(operation, nbgrpc.Backoff(c.ctx))
if err != nil {
log.Errorf("failed to login to Management Service: %v", err)
// PermissionDenied is how management asks an unknown peer to register,
// which the caller does next, so it is not an error worth reporting.
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.PermissionDenied {
log.Debugf("login to Management Service refused: %v", err)
} else {
log.Errorf("failed to login to Management Service: %v", err)
}
return nil, err
}
@@ -738,9 +744,13 @@ func (c *GrpcClient) GetDeviceAuthorizationFlow() (*proto.DeviceAuthorizationFlo
return flowInfoResp, nil
}
// GetPKCEAuthorizationFlow returns a pkce authorization flow information.
// GetPKCEAuthorizationFlow returns the PKCE authorization flow information.
// It also takes care of encrypting and decrypting messages.
func (c *GrpcClient) GetPKCEAuthorizationFlow() (*proto.PKCEAuthorizationFlow, error) {
//
// sessionExtend tells the server the flow will renew an existing peer's session
// rather than log one in, so it can rule out a configuration that would let the
// IdP answer from an unrelated account. See PKCEAuthorizationFlowRequest.
func (c *GrpcClient) GetPKCEAuthorizationFlow(sessionExtend bool) (*proto.PKCEAuthorizationFlow, error) {
if !c.ready() {
return nil, fmt.Errorf("no connection to management in order to get pkce authorization flow")
}
@@ -753,7 +763,7 @@ func (c *GrpcClient) GetPKCEAuthorizationFlow() (*proto.PKCEAuthorizationFlow, e
mgmCtx, cancel := context.WithTimeout(c.ctx, time.Second*2)
defer cancel()
message := &proto.PKCEAuthorizationFlowRequest{}
message := &proto.PKCEAuthorizationFlowRequest{SessionExtend: sessionExtend}
encryptedMSG, err := encryption.EncryptMessage(*serverKey, c.key, message)
if err != nil {
return nil, err
@@ -1014,6 +1024,19 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta {
})
}
proofs := make([]*proto.CertificateProof, 0, len(info.CertificateProofs))
for _, p := range info.CertificateProofs {
proofs = append(proofs, &proto.CertificateProof{
Nonce: p.Nonce,
Chain: p.Chain,
SigAlg: p.SigAlg,
Signature: p.Signature,
})
}
if len(proofs) > 0 {
log.Debugf("peer meta carries %d certificate posture proofs", len(proofs))
}
return &proto.PeerSystemMeta{
Hostname: info.Hostname,
GoOS: info.GoOS,
@@ -1033,7 +1056,8 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta {
Cloud: info.Environment.Cloud,
Platform: info.Environment.Platform,
},
Files: files,
Files: files,
CertificateProofs: proofs,
Flags: &proto.Flags{
RosenpassEnabled: info.RosenpassEnabled,
+24
View File
@@ -12,6 +12,8 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/shared/management/certposture"
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
)
@@ -93,3 +95,25 @@ func TestMaxRecvMsgSizeIntegration(t *testing.T) {
assert.Len(t, resp.Key, payloadSize)
})
}
// TestInfoToMetaData_CarriesCertificateProofs: management verifies the proofs it receives
// in the peer meta, so a field dropped by the mapping fails the certificate check without
// any error on either side.
func TestInfoToMetaData_CarriesCertificateProofs(t *testing.T) {
info := &system.Info{CertificateProofs: []certposture.Proof{
{Nonce: []byte("nonce-1"), Chain: [][]byte{[]byte("leaf"), []byte("intermediate")}, SigAlg: "ecdsa-sha256", Signature: []byte("sig-1")},
{Nonce: []byte("nonce-2"), Chain: [][]byte{[]byte("other")}, SigAlg: "rsa-pss-sha256", Signature: []byte("sig-2")},
}}
meta := infoToMetaData(info)
require.Len(t, meta.GetCertificateProofs(), 2, "every proof reaches the meta")
first := meta.GetCertificateProofs()[0]
assert.Equal(t, []byte("nonce-1"), first.GetNonce())
assert.Equal(t, [][]byte{[]byte("leaf"), []byte("intermediate")}, first.GetChain(), "the whole chain is carried, in order")
assert.Equal(t, "ecdsa-sha256", first.GetSigAlg())
assert.Equal(t, []byte("sig-1"), first.GetSignature())
assert.Equal(t, "rsa-pss-sha256", meta.GetCertificateProofs()[1].GetSigAlg())
assert.Empty(t, infoToMetaData(&system.Info{}).GetCertificateProofs(), "a device without proofs sends none")
}
+4 -3
View File
@@ -16,7 +16,7 @@ type MockClient struct {
LoginFunc func(info *system.Info, sshKey []byte, dnsLabels domain.List) (*proto.LoginResponse, error)
ExtendAuthSessionFunc func(info *system.Info, jwtToken string) (*proto.ExtendAuthSessionResponse, error)
GetDeviceAuthorizationFlowFunc func() (*proto.DeviceAuthorizationFlow, error)
GetPKCEAuthorizationFlowFunc func() (*proto.PKCEAuthorizationFlow, error)
GetPKCEAuthorizationFlowFunc func(sessionExtend bool) (*proto.PKCEAuthorizationFlow, error)
GetServerURLFunc func() string
HealthCheckFunc func() error
SyncMetaFunc func(sysInfo *system.Info) error
@@ -80,11 +80,12 @@ func (m *MockClient) GetDeviceAuthorizationFlow() (*proto.DeviceAuthorizationFlo
return m.GetDeviceAuthorizationFlowFunc()
}
func (m *MockClient) GetPKCEAuthorizationFlow() (*proto.PKCEAuthorizationFlow, error) {
// GetPKCEAuthorizationFlow returns the PKCE authorization flow information.
func (m *MockClient) GetPKCEAuthorizationFlow(sessionExtend bool) (*proto.PKCEAuthorizationFlow, error) {
if m.GetPKCEAuthorizationFlowFunc == nil {
return nil, nil
}
return m.GetPKCEAuthorizationFlowFunc()
return m.GetPKCEAuthorizationFlowFunc(sessionExtend)
}
func (m *MockClient) HealthCheck() error {
-5
View File
@@ -127,10 +127,6 @@ type Client struct {
// see more: https://docs.netbird.io/api/resources/identity-providers
IdentityProviders *IdentityProvidersAPI
// Ingress NetBird Ingress Peers APIs
// see more: https://docs.netbird.io/api/resources/ingress-ports
Ingress *IngressAPI
// Instance NetBird Instance API
// see more: https://docs.netbird.io/api/resources/instance
Instance *InstanceAPI
@@ -207,7 +203,6 @@ func (c *Client) initialize() {
c.OktaScimIDP = &OktaScimIDPAPI{c}
c.EventStreaming = &EventStreamingAPI{c}
c.IdentityProviders = &IdentityProvidersAPI{c}
c.Ingress = &IngressAPI{c}
c.Instance = &InstanceAPI{c}
c.ReverseProxyServices = &ReverseProxyServicesAPI{c}
c.ReverseProxyClusters = &ReverseProxyClustersAPI{c}
-92
View File
@@ -1,92 +0,0 @@
package rest
import (
"bytes"
"context"
"encoding/json"
"github.com/netbirdio/netbird/shared/management/http/api"
)
// IngressAPI APIs for Ingress Peers, do not use directly
type IngressAPI struct {
c *Client
}
// List all ingress peers
// See more: https://docs.netbird.io/api/resources/ingress#list-all-ingress-peers
func (a *IngressAPI) List(ctx context.Context) ([]api.IngressPeer, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/ingress/peers", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.IngressPeer](resp)
return ret, err
}
// Get ingress peer info
// See more: https://docs.netbird.io/api/resources/ingress#retrieve-an-ingress-peer
func (a *IngressAPI) Get(ctx context.Context, ingressPeerID string) (*api.IngressPeer, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/ingress/peers/"+ingressPeerID, nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.IngressPeer](resp)
return &ret, err
}
// Create new ingress peer
// See more: https://docs.netbird.io/api/resources/ingress#create-an-ingress-peer
func (a *IngressAPI) Create(ctx context.Context, request api.PostApiIngressPeersJSONRequestBody) (*api.IngressPeer, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := a.c.NewRequest(ctx, "POST", "/api/ingress/peers", bytes.NewReader(requestBytes), nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.IngressPeer](resp)
return &ret, err
}
// Update update ingress peer
// See more: https://docs.netbird.io/api/resources/ingress#update-an-ingress-peer
func (a *IngressAPI) Update(ctx context.Context, ingressPeerID string, request api.PutApiIngressPeersIngressPeerIdJSONRequestBody) (*api.IngressPeer, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := a.c.NewRequest(ctx, "PUT", "/api/ingress/peers/"+ingressPeerID, bytes.NewReader(requestBytes), nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.IngressPeer](resp)
return &ret, err
}
// Delete delete ingress peer
// See more: https://docs.netbird.io/api/resources/ingress#delete-an-ingress-peer
func (a *IngressAPI) Delete(ctx context.Context, ingressPeerID string) error {
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/ingress/peers/"+ingressPeerID, nil, nil)
if err != nil {
return err
}
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
@@ -1,184 +0,0 @@
//go:build integration
package rest_test
import (
"context"
"encoding/json"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/shared/management/client/rest"
"github.com/netbirdio/netbird/shared/management/http/api"
"github.com/netbirdio/netbird/shared/management/http/util"
)
var testIngressPeer = api.IngressPeer{
Connected: true,
Enabled: true,
Id: "Test",
}
func TestIngress_List_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/ingress/peers", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal([]api.IngressPeer{testIngressPeer})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Ingress.List(context.Background())
require.NoError(t, err)
assert.Len(t, ret, 1)
assert.Equal(t, testIngressPeer, ret[0])
})
}
func TestIngress_List_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/ingress/peers", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Ingress.List(context.Background())
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Empty(t, ret)
})
}
func TestIngress_Get_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(testIngressPeer)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Ingress.Get(context.Background(), "Test")
require.NoError(t, err)
assert.Equal(t, testIngressPeer, *ret)
})
}
func TestIngress_Get_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Ingress.Get(context.Background(), "Test")
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Empty(t, ret)
})
}
func TestIngress_Create_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/ingress/peers", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
reqBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
var req api.PostApiIngressPeersJSONRequestBody
err = json.Unmarshal(reqBytes, &req)
require.NoError(t, err)
assert.Equal(t, "peer-id", req.PeerId)
retBytes, _ := json.Marshal(testIngressPeer)
_, err = w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Ingress.Create(context.Background(), api.PostApiIngressPeersJSONRequestBody{
PeerId: "peer-id",
})
require.NoError(t, err)
assert.Equal(t, testIngressPeer, *ret)
})
}
func TestIngress_Create_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/ingress/peers", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Ingress.Create(context.Background(), api.PostApiIngressPeersJSONRequestBody{
PeerId: "peer-id",
})
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}
func TestIngress_Update_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "PUT", r.Method)
reqBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
var req api.PutApiIngressPeersIngressPeerIdJSONRequestBody
err = json.Unmarshal(reqBytes, &req)
require.NoError(t, err)
assert.Equal(t, true, req.Enabled)
retBytes, _ := json.Marshal(testIngressPeer)
_, err = w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Ingress.Update(context.Background(), "Test", api.PutApiIngressPeersIngressPeerIdJSONRequestBody{
Enabled: true,
})
require.NoError(t, err)
assert.Equal(t, testIngressPeer, *ret)
})
}
func TestIngress_Update_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Ingress.Update(context.Background(), "Test", api.PutApiIngressPeersIngressPeerIdJSONRequestBody{
Enabled: true,
})
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}
func TestIngress_Delete_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "DELETE", r.Method)
w.WriteHeader(200)
})
err := c.Ingress.Delete(context.Background(), "Test")
require.NoError(t, err)
})
}
func TestIngress_Delete_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/ingress/peers/Test", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
err := c.Ingress.Delete(context.Background(), "Test")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
})
}
-92
View File
@@ -125,98 +125,6 @@ func (a *PeersAPI) CreateTemporaryAccess(ctx context.Context, peerID string, req
return &ret, err
}
// PeerIngressPortsAPI APIs for Peer Ingress Ports, do not use directly
type PeerIngressPortsAPI struct {
c *Client
peerID string
}
// IngressPorts APIs for peer ingress ports
func (a *PeersAPI) IngressPorts(peerID string) *PeerIngressPortsAPI {
return &PeerIngressPortsAPI{
c: a.c,
peerID: peerID,
}
}
// List list all ingress port allocations for a peer
// See more: https://docs.netbird.io/api/resources/peers#list-all-ingress-port-allocations
func (a *PeerIngressPortsAPI) List(ctx context.Context) ([]api.IngressPortAllocation, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+a.peerID+"/ingress/ports", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.IngressPortAllocation](resp)
return ret, err
}
// Get get ingress port allocation info
// See more: https://docs.netbird.io/api/resources/peers#retrieve-an-ingress-port-allocation
func (a *PeerIngressPortsAPI) Get(ctx context.Context, allocationID string) (*api.IngressPortAllocation, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+a.peerID+"/ingress/ports/"+allocationID, nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.IngressPortAllocation](resp)
return &ret, err
}
// Create create new ingress port allocation
// See more: https://docs.netbird.io/api/resources/peers#create-an-ingress-port-allocation
func (a *PeerIngressPortsAPI) Create(ctx context.Context, request api.PostApiPeersPeerIdIngressPortsJSONRequestBody) (*api.IngressPortAllocation, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := a.c.NewRequest(ctx, "POST", "/api/peers/"+a.peerID+"/ingress/ports", bytes.NewReader(requestBytes), nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.IngressPortAllocation](resp)
return &ret, err
}
// Update update ingress port allocation
// See more: https://docs.netbird.io/api/resources/peers#update-an-ingress-port-allocation
func (a *PeerIngressPortsAPI) Update(ctx context.Context, allocationID string, request api.PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody) (*api.IngressPortAllocation, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := a.c.NewRequest(ctx, "PUT", "/api/peers/"+a.peerID+"/ingress/ports/"+allocationID, bytes.NewReader(requestBytes), nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.IngressPortAllocation](resp)
return &ret, err
}
// Delete delete ingress port allocation
// See more: https://docs.netbird.io/api/resources/peers#delete-an-ingress-port-allocation
func (a *PeerIngressPortsAPI) Delete(ctx context.Context, allocationID string) error {
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/peers/"+a.peerID+"/ingress/ports/"+allocationID, nil, nil)
if err != nil {
return err
}
if resp.Body != nil {
defer resp.Body.Close()
}
return nil
}
// PeerJobsAPI APIs for Peer Jobs, do not use directly
type PeerJobsAPI struct {
c *Client
-145
View File
@@ -31,11 +31,6 @@ var (
Name: "test-peer",
}
testIngressPortAllocation = api.IngressPortAllocation{
Enabled: true,
Id: "alloc-1",
}
testJobResponse = api.JobResponse{
Id: "job-1",
Status: "pending",
@@ -221,146 +216,6 @@ func TestPeers_CreateTemporaryAccess_Err(t *testing.T) {
})
}
func TestPeerIngressPorts_List_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/peers/Test/ingress/ports", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal([]api.IngressPortAllocation{testIngressPortAllocation})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Peers.IngressPorts("Test").List(context.Background())
require.NoError(t, err)
assert.Len(t, ret, 1)
assert.Equal(t, testIngressPortAllocation, ret[0])
})
}
func TestPeerIngressPorts_List_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/peers/Test/ingress/ports", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Peers.IngressPorts("Test").List(context.Background())
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Empty(t, ret)
})
}
func TestPeerIngressPorts_Get_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(testIngressPortAllocation)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Peers.IngressPorts("Test").Get(context.Background(), "alloc-1")
require.NoError(t, err)
assert.Equal(t, testIngressPortAllocation, *ret)
})
}
func TestPeerIngressPorts_Get_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Peers.IngressPorts("Test").Get(context.Background(), "alloc-1")
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Empty(t, ret)
})
}
func TestPeerIngressPorts_Create_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/peers/Test/ingress/ports", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
retBytes, _ := json.Marshal(testIngressPortAllocation)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Peers.IngressPorts("Test").Create(context.Background(), api.PostApiPeersPeerIdIngressPortsJSONRequestBody{})
require.NoError(t, err)
assert.Equal(t, testIngressPortAllocation, *ret)
})
}
func TestPeerIngressPorts_Create_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/peers/Test/ingress/ports", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Peers.IngressPorts("Test").Create(context.Background(), api.PostApiPeersPeerIdIngressPortsJSONRequestBody{})
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}
func TestPeerIngressPorts_Update_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "PUT", r.Method)
retBytes, _ := json.Marshal(testIngressPortAllocation)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Peers.IngressPorts("Test").Update(context.Background(), "alloc-1", api.PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody{})
require.NoError(t, err)
assert.Equal(t, testIngressPortAllocation, *ret)
})
}
func TestPeerIngressPorts_Update_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Peers.IngressPorts("Test").Update(context.Background(), "alloc-1", api.PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody{})
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}
func TestPeerIngressPorts_Delete_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "DELETE", r.Method)
w.WriteHeader(200)
})
err := c.Peers.IngressPorts("Test").Delete(context.Background(), "alloc-1")
require.NoError(t, err)
})
}
func TestPeerIngressPorts_Delete_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/peers/Test/ingress/ports/alloc-1", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
err := c.Peers.IngressPorts("Test").Delete(context.Background(), "alloc-1")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
})
}
func TestPeerJobs_List_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/peers/Test/jobs", func(w http.ResponseWriter, r *http.Request) {
+79 -559
View File
@@ -31,9 +31,6 @@ tags:
description: View information about the account and network events.
- name: Accounts
description: View information about the accounts.
- name: Ingress Ports
description: Interact with and view information about the ingress peers and ports.
x-cloud-only: true
- name: Identity Providers
description: Interact with and view information about identity providers.
- name: Services
@@ -383,6 +380,10 @@ components:
description: Enables or disables client metrics push for all peers in the account
type: boolean
example: false
peer_hostname_collision_rejected:
description: When enabled, registering or renaming a peer to a hostname already used by another peer in the account fails instead of receiving an IP-based suffix. Applies to ephemeral peers as well.
type: boolean
example: false
agent_network_only:
description: Limits the dashboard to the Agent Network surface for this account. Set for accounts created via netbird.ai signups and can be disabled later. Enabling this requires dashboard_features.agent_network to be true in the same request.
type: boolean
@@ -826,6 +827,20 @@ components:
- ssh_enabled
- login_expiration_enabled
- inactivity_expiration_enabled
NetworkAddress:
type: object
properties:
net_ip:
description: IP address with CIDR of the interface
type: string
example: 192.168.0.11/24
mac:
description: MAC address of the interface
type: string
example: "00:93:37:bd:83:0f"
required:
- net_ip
- mac
Peer:
allOf:
- $ref: '#/components/schemas/PeerMinimum'
@@ -845,6 +860,11 @@ components:
type: string
format: ipv6
example: "fd00:4e42:ab12::1"
network_addresses:
description: Network interfaces (IP + MAC) reported by the peer
type: array
items:
$ref: '#/components/schemas/NetworkAddress'
connection_ip:
description: Peer's public connection IP address
type: string
@@ -1688,6 +1708,8 @@ components:
$ref: '#/components/schemas/PeerNetworkRangeCheck'
process_check:
$ref: '#/components/schemas/ProcessCheck'
certificate_check:
$ref: '#/components/schemas/CertificateCheck'
NBVersionCheck:
description: Posture check for the version of NetBird
type: object
@@ -1805,6 +1827,19 @@ components:
description: Path to the process executable file in a Windows operating system
type: string
example: "C:\ProgramData\NetBird\netbird.exe"
CertificateCheck:
description: Posture check for a certificate held by the peer that chains to one of the given CA certificates. Not supported on mobile devices (iOS and Android), which always fail this check and are denied access
type: object
properties:
ca_certificates:
description: PEM encoded CA certificates the peer's certificate must chain to
type: array
minItems: 1
items:
type: string
example: ["-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----"]
required:
- ca_certificates
Location:
description: Describe geographical location information
type: object
@@ -2433,222 +2468,6 @@ components:
- initiator_email
- target_id
- meta
IngressPeerCreateRequest:
type: object
properties:
peer_id:
description: ID of the peer that is used as an ingress peer
type: string
example: ch8i4ug6lnn4g9hqv7m0
enabled:
description: Defines if an ingress peer is enabled
type: boolean
example: true
fallback:
description: Defines if an ingress peer can be used as a fallback if no ingress peer can be found in the region of the forwarded peer
type: boolean
example: true
required:
- peer_id
- enabled
- fallback
IngressPeerUpdateRequest:
type: object
properties:
enabled:
description: Defines if an ingress peer is enabled
type: boolean
example: true
fallback:
description: Defines if an ingress peer can be used as a fallback if no ingress peer can be found in the region of the forwarded peer
type: boolean
example: true
required:
- enabled
- fallback
IngressPeer:
type: object
properties:
id:
description: ID of the ingress peer
type: string
example: ch8i4ug6lnn4g9hqv7m0
peer_id:
description: ID of the peer that is used as an ingress peer
type: string
example: x7p3kqf2rdd8j5zxw4n9
ingress_ip:
description: Ingress IP address of the ingress peer where the traffic arrives
type: string
example: 192.34.0.123
available_ports:
$ref: '#/components/schemas/AvailablePorts'
enabled:
description: Indicates if an ingress peer is enabled
type: boolean
example: true
connected:
description: Indicates if an ingress peer is connected to the management server
type: boolean
example: true
fallback:
description: Indicates if an ingress peer can be used as a fallback if no ingress peer can be found in the region of the forwarded peer
type: boolean
example: true
region:
description: Region of the ingress peer
type: string
example: germany
required:
- id
- peer_id
- ingress_ip
- available_ports
- enabled
- connected
- fallback
- region
AvailablePorts:
type: object
properties:
tcp:
description: Number of available TCP ports left on the ingress peer
type: integer
example: 45765
udp:
description: Number of available UDP ports left on the ingress peer
type: integer
example: 50000
required:
- tcp
- udp
IngressPortAllocationRequest:
type: object
properties:
name:
description: Name of the ingress port allocation
type: string
example: Ingress Port Allocation 1
enabled:
description: Indicates if an ingress port allocation is enabled
type: boolean
example: true
port_ranges:
description: List of port ranges that are forwarded by the ingress peer
type: array
items:
$ref: '#/components/schemas/IngressPortAllocationRequestPortRange'
direct_port:
description: Direct port allocation
$ref: '#/components/schemas/IngressPortAllocationRequestDirectPort'
required:
- name
- enabled
IngressPortAllocationRequestPortRange:
type: object
properties:
start:
description: The starting port of the range of forwarded ports
type: integer
example: 80
end:
description: The ending port of the range of forwarded ports
type: integer
example: 320
protocol:
description: The protocol accepted by the port range
type: string
enum: [ "tcp", "udp", "tcp/udp" ]
example: tcp
required:
- start
- end
- protocol
IngressPortAllocationRequestDirectPort:
type: object
properties:
count:
description: The number of ports to be forwarded
type: integer
example: 5
protocol:
description: The protocol accepted by the port
type: string
enum: [ "tcp", "udp", "tcp/udp" ]
example: udp
required:
- count
- protocol
IngressPortAllocation:
type: object
properties:
id:
description: ID of the ingress port allocation
type: string
example: ch8i4ug6lnn4g9hqv7m0
name:
description: Name of the ingress port allocation
type: string
example: Ingress Peer Allocation 1
ingress_peer_id:
description: ID of the ingress peer that forwards the ports
type: string
example: x7p3kqf2rdd8j5zxw4n9
region:
description: Region of the ingress peer
type: string
example: germany
enabled:
description: Indicates if an ingress port allocation is enabled
type: boolean
example: true
ingress_ip:
description: Ingress IP address of the ingress peer where the traffic arrives
type: string
example: 192.34.0.123
port_range_mappings:
description: List of port ranges that are allowed to be used by the ingress peer
type: array
items:
$ref: '#/components/schemas/IngressPortAllocationPortMapping'
required:
- id
- name
- ingress_peer_id
- region
- enabled
- ingress_ip
- port_range_mappings
IngressPortAllocationPortMapping:
type: object
properties:
translated_start:
description: The starting port of the translated range of forwarded ports
type: integer
example: 80
translated_end:
description: The ending port of the translated range of forwarded ports
type: integer
example: 320
ingress_start:
description: The starting port of the range of ingress ports mapped to the forwarded ports
type: integer
example: 1080
ingress_end:
description: The ending port of the range of ingress ports mapped to the forwarded ports
type: integer
example: 1320
protocol:
description: Protocol accepted by the ports
type: string
enum: [ "tcp", "udp", "tcp/udp" ]
example: tcp
required:
- translated_start
- translated_end
- ingress_start
- ingress_end
- protocol
NetworkTrafficLocation:
type: object
properties:
@@ -3303,6 +3122,12 @@ components:
type: string
description: URL path prefix for this target (HTTP only)
example: "/"
access_action:
type: string
description: Access action applied after this target's HTTP path prefix is selected. "inherit" uses the service authentication configuration, "bypass" skips service authentication, and "block" denies access. HTTP services only.
enum: [inherit, bypass, block]
default: inherit
example: "inherit"
protocol:
type: string
description: Protocol to use when connecting to the backend
@@ -6466,8 +6291,8 @@ components:
example: "d1m3kebd9pcs0c1pnu7g"
state:
type: string
description: Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure.
enum: [ "provisioning", "ready", "failed" ]
description: Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure, `disabled` while the gateway is turned off and the endpoint is not served.
enum: [ "provisioning", "ready", "failed", "disabled" ]
example: "ready"
endpoint:
type: string
@@ -7529,6 +7354,11 @@ paths:
schema:
type: string
description: Filter peers by IP address
- in: query
name: mac
schema:
type: string
description: Filter peers by MAC address of a network interface
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
@@ -7708,341 +7538,6 @@ paths:
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
/api/peers/{peerId}/ingress/ports:
get:
x-cloud-only: true
summary: List all Port Allocations
description: Returns a list of all ingress port allocations for a peer
tags: [ Ingress Ports ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: peerId
required: true
schema:
type: string
description: The unique identifier of a peer
- in: query
name: name
schema:
type: string
description: Filters ingress port allocations by name
responses:
'200':
description: A JSON Array of Ingress Port Allocations
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/IngressPortAllocation'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
post:
x-cloud-only: true
summary: Create a Port Allocation
description: Creates a new ingress port allocation for a peer
tags: [ Ingress Ports ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: peerId
required: true
schema:
type: string
description: The unique identifier of a peer
requestBody:
description: New Ingress Port Allocation request
content:
'application/json':
schema:
$ref: '#/components/schemas/IngressPortAllocationRequest'
responses:
'200':
description: A Ingress Port Allocation object
content:
application/json:
schema:
$ref: '#/components/schemas/IngressPortAllocation'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
/api/peers/{peerId}/ingress/ports/{allocationId}:
get:
x-cloud-only: true
summary: Retrieve a Port Allocation
description: Get information about an ingress port allocation
tags: [ Ingress Ports ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: peerId
required: true
schema:
type: string
description: The unique identifier of a peer
- in: path
name: allocationId
required: true
schema:
type: string
description: The unique identifier of an ingress port allocation
responses:
'200':
description: A Ingress Port Allocation object
content:
application/json:
schema:
$ref: '#/components/schemas/IngressPortAllocation'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
put:
x-cloud-only: true
summary: Update a Port Allocation
description: Update information about an ingress port allocation
tags: [ Ingress Ports ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: peerId
required: true
schema:
type: string
description: The unique identifier of a peer
- in: path
name: allocationId
required: true
schema:
type: string
description: The unique identifier of an ingress port allocation
requestBody:
description: update an ingress port allocation
content:
application/json:
schema:
$ref: '#/components/schemas/IngressPortAllocationRequest'
responses:
'200':
description: A Ingress Port Allocation object
content:
application/json:
schema:
$ref: '#/components/schemas/IngressPortAllocation'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
delete:
x-cloud-only: true
summary: Delete a Port Allocation
description: Delete an ingress port allocation
tags: [ Ingress Ports ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: peerId
required: true
schema:
type: string
description: The unique identifier of a peer
- in: path
name: allocationId
required: true
schema:
type: string
description: The unique identifier of an ingress port allocation
responses:
'200':
description: Delete status code
content: { }
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
/api/ingress/peers:
get:
x-cloud-only: true
summary: List all Ingress Peers
description: Returns a list of all ingress peers
tags: [ Ingress Ports ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
responses:
'200':
description: A JSON Array of Ingress Peers
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/IngressPeer'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
post:
x-cloud-only: true
summary: Create a Ingress Peer
description: Creates a new ingress peer
tags: [ Ingress Ports ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
requestBody:
description: New Ingress Peer request
content:
'application/json':
schema:
$ref: '#/components/schemas/IngressPeerCreateRequest'
responses:
'200':
description: A Ingress Peer object
content:
application/json:
schema:
$ref: '#/components/schemas/IngressPeer'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
/api/ingress/peers/{ingressPeerId}:
get:
x-cloud-only: true
summary: Retrieve a Ingress Peer
description: Get information about an ingress peer
tags: [ Ingress Ports ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: ingressPeerId
required: true
schema:
type: string
description: The unique identifier of an ingress peer
responses:
'200':
description: A Ingress Peer object
content:
application/json:
schema:
$ref: '#/components/schemas/IngressPeer'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
put:
x-cloud-only: true
summary: Update a Ingress Peer
description: Update information about an ingress peer
tags: [ Ingress Ports ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: ingressPeerId
required: true
schema:
type: string
description: The unique identifier of an ingress peer
requestBody:
description: update an ingress peer
content:
'application/json':
schema:
$ref: '#/components/schemas/IngressPeerUpdateRequest'
responses:
'200':
description: A Ingress Peer object
content:
application/json:
schema:
$ref: '#/components/schemas/IngressPeer'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
delete:
x-cloud-only: true
summary: Delete a Ingress Peer
description: Delete an ingress peer
tags: [ Ingress Ports ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: ingressPeerId
required: true
schema:
type: string
description: The unique identifier of an ingress peer
responses:
'200':
description: Delete status code
content: { }
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
/api/setup-keys:
get:
summary: List all Setup Keys
@@ -10614,6 +10109,28 @@ paths:
$ref: "#/components/responses/requires_authentication"
"500":
$ref: "#/components/responses/internal_error"
delete:
summary: Delete MSP tenant
tags:
- MSP
parameters:
- in: path
name: id
required: true
schema:
type: string
description: The unique identifier of a tenant account
responses:
"200":
description: Successfully deleted the tenant
"400":
$ref: "#/components/responses/bad_request"
"403":
$ref: "#/components/responses/requires_authentication"
"404":
description: The tenant was not found
"500":
$ref: "#/components/responses/internal_error"
/api/integrations/msp/tenants/{id}/unlink:
post:
summary: Unlink a tenant
@@ -13360,7 +12877,7 @@ paths:
/api/reverse-proxies/domains/{domainId}:
delete:
summary: Delete a Custom domain
description: Delete an existing service custom domain
description: Delete an existing service custom domain after removing or moving all services that use it or its subdomains, including disabled services.
tags: [ Services ]
security:
- BearerAuth: [ ]
@@ -13383,6 +12900,9 @@ paths:
"$ref": "#/components/responses/forbidden"
'404':
"$ref": "#/components/responses/not_found"
'412':
description: The domain or one of its subdomains is still used by a service
content: { }
'500':
"$ref": "#/components/responses/internal_error"
/api/reverse-proxies/domains/{domainId}/validate:
@@ -13585,7 +13105,7 @@ paths:
/api/integrations/agent-network/managed-proxy:
post:
summary: Provision a managed Agent Network gateway
description: Starts provisioning of a NetBird-managed Agent Network gateway for the account, allocating its endpoint under the managed zone on the first call. Idempotent — answers 202 when this call started (or, after a failure, restarted) provisioning and 200 when a deployment already exists, reporting current state either way. Returns 409 when the account already has an Agent Network endpoint that managed provisioning does not own, and 503 when endpoint allocation is temporarily exhausted.
description: Starts provisioning of a NetBird-managed Agent Network gateway for the account, allocating its endpoint under the managed zone on the first call. Idempotent — answers 202 when this call started (or, after a failure, restarted) provisioning and 200 when a deployment already exists, reporting current state either way. A disabled deployment answers 200 with state `disabled` and stays disabled. Returns 409 when the account already has an Agent Network endpoint that managed provisioning does not own, and 503 when endpoint allocation is temporarily exhausted.
tags: [ Agent Network ]
security:
- BearerAuth: [ ]
@@ -13623,7 +13143,7 @@ paths:
$ref: '#/components/schemas/ErrorResponse'
get:
summary: Retrieve managed Agent Network gateway status
description: Reports the account's managed gateway deployment and its derived state. Returns 404 when the account has no managed deployment.
description: Reports the account's managed gateway deployment and its derived state, including `disabled` for a deployment that is turned off. Returns 404 when the account has no managed deployment.
tags: [ Agent Network ]
security:
- BearerAuth: [ ]
+61 -224
View File
@@ -79,6 +79,7 @@ func (e AgentNetworkConsumptionDimensionKind) Valid() bool {
// Defines values for AgentNetworkManagedProxyState.
const (
AgentNetworkManagedProxyStateDisabled AgentNetworkManagedProxyState = "disabled"
AgentNetworkManagedProxyStateFailed AgentNetworkManagedProxyState = "failed"
AgentNetworkManagedProxyStateProvisioning AgentNetworkManagedProxyState = "provisioning"
AgentNetworkManagedProxyStateReady AgentNetworkManagedProxyState = "ready"
@@ -87,6 +88,8 @@ const (
// Valid indicates whether the value is a known member of the AgentNetworkManagedProxyState enum.
func (e AgentNetworkManagedProxyState) Valid() bool {
switch e {
case AgentNetworkManagedProxyStateDisabled:
return true
case AgentNetworkManagedProxyStateFailed:
return true
case AgentNetworkManagedProxyStateProvisioning:
@@ -605,69 +608,6 @@ func (e IdentityProviderType) Valid() bool {
}
}
// Defines values for IngressPortAllocationPortMappingProtocol.
const (
IngressPortAllocationPortMappingProtocolTcp IngressPortAllocationPortMappingProtocol = "tcp"
IngressPortAllocationPortMappingProtocolTcpudp IngressPortAllocationPortMappingProtocol = "tcp/udp"
IngressPortAllocationPortMappingProtocolUdp IngressPortAllocationPortMappingProtocol = "udp"
)
// Valid indicates whether the value is a known member of the IngressPortAllocationPortMappingProtocol enum.
func (e IngressPortAllocationPortMappingProtocol) Valid() bool {
switch e {
case IngressPortAllocationPortMappingProtocolTcp:
return true
case IngressPortAllocationPortMappingProtocolTcpudp:
return true
case IngressPortAllocationPortMappingProtocolUdp:
return true
default:
return false
}
}
// Defines values for IngressPortAllocationRequestDirectPortProtocol.
const (
IngressPortAllocationRequestDirectPortProtocolTcp IngressPortAllocationRequestDirectPortProtocol = "tcp"
IngressPortAllocationRequestDirectPortProtocolTcpudp IngressPortAllocationRequestDirectPortProtocol = "tcp/udp"
IngressPortAllocationRequestDirectPortProtocolUdp IngressPortAllocationRequestDirectPortProtocol = "udp"
)
// Valid indicates whether the value is a known member of the IngressPortAllocationRequestDirectPortProtocol enum.
func (e IngressPortAllocationRequestDirectPortProtocol) Valid() bool {
switch e {
case IngressPortAllocationRequestDirectPortProtocolTcp:
return true
case IngressPortAllocationRequestDirectPortProtocolTcpudp:
return true
case IngressPortAllocationRequestDirectPortProtocolUdp:
return true
default:
return false
}
}
// Defines values for IngressPortAllocationRequestPortRangeProtocol.
const (
IngressPortAllocationRequestPortRangeProtocolTcp IngressPortAllocationRequestPortRangeProtocol = "tcp"
IngressPortAllocationRequestPortRangeProtocolTcpudp IngressPortAllocationRequestPortRangeProtocol = "tcp/udp"
IngressPortAllocationRequestPortRangeProtocolUdp IngressPortAllocationRequestPortRangeProtocol = "udp"
)
// Valid indicates whether the value is a known member of the IngressPortAllocationRequestPortRangeProtocol enum.
func (e IngressPortAllocationRequestPortRangeProtocol) Valid() bool {
switch e {
case IngressPortAllocationRequestPortRangeProtocolTcp:
return true
case IngressPortAllocationRequestPortRangeProtocolTcpudp:
return true
case IngressPortAllocationRequestPortRangeProtocolUdp:
return true
default:
return false
}
}
// Defines values for IntegrationResponsePlatform.
const (
IntegrationResponsePlatformDatadog IntegrationResponsePlatform = "datadog"
@@ -1097,6 +1037,27 @@ func (e ServiceRequestMode) Valid() bool {
}
}
// Defines values for ServiceTargetAccessAction.
const (
ServiceTargetAccessActionBlock ServiceTargetAccessAction = "block"
ServiceTargetAccessActionBypass ServiceTargetAccessAction = "bypass"
ServiceTargetAccessActionInherit ServiceTargetAccessAction = "inherit"
)
// Valid indicates whether the value is a known member of the ServiceTargetAccessAction enum.
func (e ServiceTargetAccessAction) Valid() bool {
switch e {
case ServiceTargetAccessActionBlock:
return true
case ServiceTargetAccessActionBypass:
return true
case ServiceTargetAccessActionInherit:
return true
default:
return false
}
}
// Defines values for ServiceTargetProtocol.
const (
ServiceTargetProtocolHttp ServiceTargetProtocol = "http"
@@ -1732,6 +1693,9 @@ type AccountSettings struct {
// PeerExposeGroups Limits which peer groups are allowed to expose services. If empty, all peers are allowed when peer expose is enabled.
PeerExposeGroups []string `json:"peer_expose_groups"`
// PeerHostnameCollisionRejected When enabled, registering or renaming a peer to a hostname already used by another peer in the account fails instead of receiving an IP-based suffix. Applies to ephemeral peers as well.
PeerHostnameCollisionRejected *bool `json:"peer_hostname_collision_rejected,omitempty"`
// PeerInactivityExpiration Period of time of inactivity after which peer session expires (seconds).
PeerInactivityExpiration int `json:"peer_inactivity_expiration"`
@@ -2259,11 +2223,11 @@ type AgentNetworkManagedProxy struct {
// Region Region of the cluster hosting the deployment.
Region *string `json:"region,omitempty"`
// State Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure.
// State Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure, `disabled` while the gateway is turned off and the endpoint is not served.
State AgentNetworkManagedProxyState `json:"state"`
}
// AgentNetworkManagedProxyState Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure.
// AgentNetworkManagedProxyState Derived deployment state. `provisioning` until the gateway is rolled out and connected, `ready` while the gateway actively serves the endpoint, `failed` when the rollout reported a failure, `disabled` while the gateway is turned off and the endpoint is not served.
type AgentNetworkManagedProxyState string
// AgentNetworkManagedProxyConflict Conflict body returned when the account already has an Agent Network endpoint that managed provisioning does not own, naming that endpoint.
@@ -2597,15 +2561,6 @@ type AgentNetworkUsageBucket struct {
TotalTokens int64 `json:"total_tokens"`
}
// AvailablePorts defines model for AvailablePorts.
type AvailablePorts struct {
// Tcp Number of available TCP ports left on the ingress peer
Tcp int `json:"tcp"`
// Udp Number of available UDP ports left on the ingress peer
Udp int `json:"udp"`
}
// AzureIntegration defines model for AzureIntegration.
type AzureIntegration struct {
// ClientId Azure AD application (client) ID
@@ -2701,6 +2656,12 @@ type BypassResponse struct {
PeerId string `json:"peer_id"`
}
// CertificateCheck Posture check for a certificate held by the peer that chains to one of the given CA certificates. Not supported on mobile devices (iOS and Android), which always fail this check and are denied access
type CertificateCheck struct {
// CaCertificates PEM encoded CA certificates the peer's certificate must chain to
CaCertificates []string `json:"ca_certificates"`
}
// CheckoutResponse defines model for CheckoutResponse.
type CheckoutResponse struct {
// SessionId The unique identifier for the checkout session.
@@ -2712,6 +2673,9 @@ type CheckoutResponse struct {
// Checks List of objects that perform the actual checks
type Checks struct {
// CertificateCheck Posture check for a certificate held by the peer that chains to one of the given CA certificates. Not supported on mobile devices (iOS and Android), which always fail this check and are denied access
CertificateCheck *CertificateCheck `json:"certificate_check,omitempty"`
// GeoLocationCheck Posture check for geo location
GeoLocationCheck *GeoLocationCheck `json:"geo_location_check,omitempty"`
@@ -3458,139 +3422,6 @@ type IdpIntegrationSyncLog struct {
Timestamp time.Time `json:"timestamp"`
}
// IngressPeer defines model for IngressPeer.
type IngressPeer struct {
AvailablePorts AvailablePorts `json:"available_ports"`
// Connected Indicates if an ingress peer is connected to the management server
Connected bool `json:"connected"`
// Enabled Indicates if an ingress peer is enabled
Enabled bool `json:"enabled"`
// Fallback Indicates if an ingress peer can be used as a fallback if no ingress peer can be found in the region of the forwarded peer
Fallback bool `json:"fallback"`
// Id ID of the ingress peer
Id string `json:"id"`
// IngressIp Ingress IP address of the ingress peer where the traffic arrives
IngressIp string `json:"ingress_ip"`
// PeerId ID of the peer that is used as an ingress peer
PeerId string `json:"peer_id"`
// Region Region of the ingress peer
Region string `json:"region"`
}
// IngressPeerCreateRequest defines model for IngressPeerCreateRequest.
type IngressPeerCreateRequest struct {
// Enabled Defines if an ingress peer is enabled
Enabled bool `json:"enabled"`
// Fallback Defines if an ingress peer can be used as a fallback if no ingress peer can be found in the region of the forwarded peer
Fallback bool `json:"fallback"`
// PeerId ID of the peer that is used as an ingress peer
PeerId string `json:"peer_id"`
}
// IngressPeerUpdateRequest defines model for IngressPeerUpdateRequest.
type IngressPeerUpdateRequest struct {
// Enabled Defines if an ingress peer is enabled
Enabled bool `json:"enabled"`
// Fallback Defines if an ingress peer can be used as a fallback if no ingress peer can be found in the region of the forwarded peer
Fallback bool `json:"fallback"`
}
// IngressPortAllocation defines model for IngressPortAllocation.
type IngressPortAllocation struct {
// Enabled Indicates if an ingress port allocation is enabled
Enabled bool `json:"enabled"`
// Id ID of the ingress port allocation
Id string `json:"id"`
// IngressIp Ingress IP address of the ingress peer where the traffic arrives
IngressIp string `json:"ingress_ip"`
// IngressPeerId ID of the ingress peer that forwards the ports
IngressPeerId string `json:"ingress_peer_id"`
// Name Name of the ingress port allocation
Name string `json:"name"`
// PortRangeMappings List of port ranges that are allowed to be used by the ingress peer
PortRangeMappings []IngressPortAllocationPortMapping `json:"port_range_mappings"`
// Region Region of the ingress peer
Region string `json:"region"`
}
// IngressPortAllocationPortMapping defines model for IngressPortAllocationPortMapping.
type IngressPortAllocationPortMapping struct {
// IngressEnd The ending port of the range of ingress ports mapped to the forwarded ports
IngressEnd int `json:"ingress_end"`
// IngressStart The starting port of the range of ingress ports mapped to the forwarded ports
IngressStart int `json:"ingress_start"`
// Protocol Protocol accepted by the ports
Protocol IngressPortAllocationPortMappingProtocol `json:"protocol"`
// TranslatedEnd The ending port of the translated range of forwarded ports
TranslatedEnd int `json:"translated_end"`
// TranslatedStart The starting port of the translated range of forwarded ports
TranslatedStart int `json:"translated_start"`
}
// IngressPortAllocationPortMappingProtocol Protocol accepted by the ports
type IngressPortAllocationPortMappingProtocol string
// IngressPortAllocationRequest defines model for IngressPortAllocationRequest.
type IngressPortAllocationRequest struct {
DirectPort *IngressPortAllocationRequestDirectPort `json:"direct_port,omitempty"`
// Enabled Indicates if an ingress port allocation is enabled
Enabled bool `json:"enabled"`
// Name Name of the ingress port allocation
Name string `json:"name"`
// PortRanges List of port ranges that are forwarded by the ingress peer
PortRanges *[]IngressPortAllocationRequestPortRange `json:"port_ranges,omitempty"`
}
// IngressPortAllocationRequestDirectPort defines model for IngressPortAllocationRequestDirectPort.
type IngressPortAllocationRequestDirectPort struct {
// Count The number of ports to be forwarded
Count int `json:"count"`
// Protocol The protocol accepted by the port
Protocol IngressPortAllocationRequestDirectPortProtocol `json:"protocol"`
}
// IngressPortAllocationRequestDirectPortProtocol The protocol accepted by the port
type IngressPortAllocationRequestDirectPortProtocol string
// IngressPortAllocationRequestPortRange defines model for IngressPortAllocationRequestPortRange.
type IngressPortAllocationRequestPortRange struct {
// End The ending port of the range of forwarded ports
End int `json:"end"`
// Protocol The protocol accepted by the port range
Protocol IngressPortAllocationRequestPortRangeProtocol `json:"protocol"`
// Start The starting port of the range of forwarded ports
Start int `json:"start"`
}
// IngressPortAllocationRequestPortRangeProtocol The protocol accepted by the port range
type IngressPortAllocationRequestPortRangeProtocol string
// InstanceStatus Instance status information
type InstanceStatus struct {
// SetupRequired Indicates whether the instance requires initial setup
@@ -3826,6 +3657,15 @@ type Network struct {
RoutingPeersCount int `json:"routing_peers_count"`
}
// NetworkAddress defines model for NetworkAddress.
type NetworkAddress struct {
// Mac MAC address of the interface
Mac string `json:"mac"`
// NetIp IP address with CIDR of the interface
NetIp string `json:"net_ip"`
}
// NetworkRequest defines model for NetworkRequest.
type NetworkRequest struct {
// Description Network description
@@ -4275,6 +4115,9 @@ type Peer struct {
// Name Peer's hostname
Name string `json:"name"`
// NetworkAddresses Network interfaces (IP + MAC) reported by the peer
NetworkAddresses *[]NetworkAddress `json:"network_addresses,omitempty"`
// Os Peer's operating system and version
Os string `json:"os"`
@@ -4369,6 +4212,9 @@ type PeerBatch struct {
// Name Peer's hostname
Name string `json:"name"`
// NetworkAddresses Network interfaces (IP + MAC) reported by the peer
NetworkAddresses *[]NetworkAddress `json:"network_addresses,omitempty"`
// Os Peer's operating system and version
Os string `json:"os"`
@@ -5300,6 +5146,9 @@ type ServiceRequestMode string
// ServiceTarget defines model for ServiceTarget.
type ServiceTarget struct {
// AccessAction Access action applied after this target's HTTP path prefix is selected. "inherit" uses the service authentication configuration, "bypass" skips service authentication, and "block" denies access. HTTP services only.
AccessAction *ServiceTargetAccessAction `json:"access_action,omitempty"`
// Enabled Whether this target is enabled
Enabled bool `json:"enabled"`
@@ -5323,6 +5172,9 @@ type ServiceTarget struct {
TargetType ServiceTargetTargetType `json:"target_type"`
}
// ServiceTargetAccessAction Access action applied after this target's HTTP path prefix is selected. "inherit" uses the service authentication configuration, "bypass" skips service authentication, and "block" denies access. HTTP services only.
type ServiceTargetAccessAction string
// ServiceTargetProtocol Protocol to use when connecting to the backend
type ServiceTargetProtocol string
@@ -6300,12 +6152,9 @@ type GetApiPeersParams struct {
// Ip Filter peers by IP address
Ip *string `form:"ip,omitempty" json:"ip,omitempty"`
}
// GetApiPeersPeerIdIngressPortsParams defines parameters for GetApiPeersPeerIdIngressPorts.
type GetApiPeersPeerIdIngressPortsParams struct {
// Name Filters ingress port allocations by name
Name *string `form:"name,omitempty" json:"name,omitempty"`
// Mac Filter peers by MAC address of a network interface
Mac *string `form:"mac,omitempty" json:"mac,omitempty"`
}
// GetApiUsersParams defines parameters for GetApiUsers.
@@ -6389,12 +6238,6 @@ type PostApiIdentityProvidersJSONRequestBody = IdentityProviderRequest
// PutApiIdentityProvidersIdpIdJSONRequestBody defines body for PutApiIdentityProvidersIdpId for application/json ContentType.
type PutApiIdentityProvidersIdpIdJSONRequestBody = IdentityProviderRequest
// PostApiIngressPeersJSONRequestBody defines body for PostApiIngressPeers for application/json ContentType.
type PostApiIngressPeersJSONRequestBody = IngressPeerCreateRequest
// PutApiIngressPeersIngressPeerIdJSONRequestBody defines body for PutApiIngressPeersIngressPeerId for application/json ContentType.
type PutApiIngressPeersIngressPeerIdJSONRequestBody = IngressPeerUpdateRequest
// CreateAzureIntegrationJSONRequestBody defines body for CreateAzureIntegration for application/json ContentType.
type CreateAzureIntegrationJSONRequestBody = CreateAzureIntegrationRequest
@@ -6503,12 +6346,6 @@ type PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody = NetworkRouterReques
// PutApiPeersPeerIdJSONRequestBody defines body for PutApiPeersPeerId for application/json ContentType.
type PutApiPeersPeerIdJSONRequestBody = PeerRequest
// PostApiPeersPeerIdIngressPortsJSONRequestBody defines body for PostApiPeersPeerIdIngressPorts for application/json ContentType.
type PostApiPeersPeerIdIngressPortsJSONRequestBody = IngressPortAllocationRequest
// PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody defines body for PutApiPeersPeerIdIngressPortsAllocationId for application/json ContentType.
type PutApiPeersPeerIdIngressPortsAllocationIdJSONRequestBody = IngressPortAllocationRequest
// PostApiPeersPeerIdJobsJSONRequestBody defines body for PostApiPeersPeerIdJobs for application/json ContentType.
type PostApiPeersPeerIdJobsJSONRequestBody = JobRequest
@@ -0,0 +1,997 @@
openapi: 3.1.0
servers:
- url: /api/v1alpha1
description: Default server
info:
title: NetBird REST API
description: API to manipulate groups, rules, policies and retrieve information about peers and users
version: 1.0.0-alpha1
tags:
- name: Users
description: Interact with and view information about users.
- name: Tokens
description: Interact with and view information about tokens.
- name: Peers
description: Interact with and view information about peers.
- name: Setup Keys
description: Interact with and view information about setup keys.
- name: Groups
description: Interact with and view information about groups.
- name: Policies
description: Interact with and view information about policies.
- name: Posture Checks
description: Interact with and view information about posture checks.
- name: Routes
description: Interact with and view information about routes.
- name: DNS
description: Interact with and view information about DNS configuration.
- name: DNS Zones
description: Interact with and view information about custom DNS zones.
- name: Events
description: View information about the account and network events.
- name: Accounts
description: View information about the accounts.
- name: Ingress Ports
description: Interact with and view information about the ingress peers and ports.
x-cloud-only: true
- name: Identity Providers
description: Interact with and view information about identity providers.
- name: Services
description: Interact with and view information about reverse proxy services.
- name: Instance
description: Instance setup and status endpoints for initial configuration.
- name: Jobs
description: Interact with and view information about remote jobs.
x-experimental: true
- name: Usage
description: Retrieve current usage statistics for the account.
x-cloud-only: true
- name: Subscription
description: Manage and view information about account subscriptions.
x-cloud-only: true
- name: Plans
description: Retrieve available plans and products.
x-cloud-only: true
- name: Checkout
description: Manage checkout sessions for plan subscriptions.
x-cloud-only: true
- name: AWS Marketplace
description: Manage AWS Marketplace subscriptions.
x-cloud-only: true
- name: Portal
description: Access customer portal for subscription management.
x-cloud-only: true
- name: Invoice
description: Manage and retrieve account invoices.
x-cloud-only: true
- name: MSP
description: MSP portal for Tenant management.
x-cloud-only: true
- name: IDP SCIM Integrations
description: Manage generic SCIM identity provider integrations for user and group sync.
x-cloud-only: true
- name: IDP Google Integrations
description: Manage Google Workspace identity provider integrations for user and group sync.
x-cloud-only: true
- name: IDP Azure Integrations
description: Manage Azure AD identity provider integrations for user and group sync.
x-cloud-only: true
- name: IDP Okta SCIM Integrations
description: Manage Okta SCIM identity provider integrations for user and group sync.
x-cloud-only: true
- name: EDR Intune Integrations
description: Manage Microsoft Intune EDR integrations.
x-cloud-only: true
- name: EDR SentinelOne Integrations
description: Manage SentinelOne EDR integrations.
x-cloud-only: true
- name: EDR Falcon Integrations
description: Manage CrowdStrike Falcon EDR integrations.
x-cloud-only: true
- name: EDR Huntress Integrations
description: Manage Huntress EDR integrations.
x-cloud-only: true
- name: EDR FleetDM Integrations
description: Manage FleetDM EDR integrations.
x-cloud-only: true
- name: EDR Peers
description: Manage EDR compliance bypass for peers.
x-cloud-only: true
- name: Event Streaming Integrations
description: Manage event streaming integrations.
x-cloud-only: true
- name: Notifications
description: Manage notification channels for account event alerts.
x-cloud-only: true
components:
schemas:
CountryCode:
description: 2-letter ISO 3166-1 alpha-2 code that represents the country
type: string
example: "DE"
CityName:
description: Commonly used English name of the city
type: string
example: "Berlin"
Country:
description: Describe country geographical location information
type: object
properties:
country_name:
description: Commonly used English name of the country
type: string
example: "Germany"
country_code:
$ref: '#/components/schemas/CountryCode'
required:
- country_name
- country_code
City:
description: Describe city geographical location information
type: object
properties:
geoname_id:
description: Integer ID of the record in GeoNames database
type: integer
example: 2950158
city_name:
description: Commonly used English name of the city
type: string
example: "Berlin"
required:
- geoname_id
- city_name
ErrorResponse:
type: object
description: Standard error response
properties:
message:
type: string
description: A human-readable error message.
example: "couldn't parse JSON request"
PeerBatch:
allOf:
- $ref: '#/components/schemas/Peer'
- type: object
properties:
created_at:
description: Peer creation date (UTC)
type: string
format: date-time
example: "2023-05-05T09:00:35.477782Z"
accessible_peers_count:
description: Number of accessible peers
type: integer
example: 5
required:
- created_at
- accessible_peers_count
PeerMinimum:
type: object
properties:
id:
description: Peer ID
type: string
example: chacbco6lnnbn6cg5s90
name:
description: Peer's hostname
type: string
example: stage-host-1
required:
- id
- name
GroupMinimum:
type: object
properties:
id:
description: Group ID
type: string
example: ch8i4ug6lnn4g9hqv7m0
name:
description: Group Name identifier
type: string
example: devs
peers_count:
description: Count of peers associated to the group
type: integer
example: 2
resources_count:
description: Count of resources associated to the group
type: integer
example: 5
issued:
description: How the group was issued (api, integration, jwt)
type: string
enum:
- "api"
- "integration"
- "jwt"
example: api
required:
- id
- name
- peers_count
- resources_count
PeerLocalFlags:
type: object
properties:
rosenpass_enabled:
description: Indicates whether Rosenpass is enabled on this peer
type: boolean
example: true
rosenpass_permissive:
description: Indicates whether Rosenpass is in permissive mode or not
type: boolean
example: false
server_ssh_allowed:
description: Indicates whether SSH access this peer is allowed or not
type: boolean
example: true
remote_jobs_allowed:
description: Indicates whether the peer has opted into management-requested remote jobs (e.g. debug bundles)
type: boolean
example: true
disable_client_routes:
description: Indicates whether client routes are disabled on this peer or not
type: boolean
example: false
disable_server_routes:
description: Indicates whether server routes are disabled on this peer or not
type: boolean
example: false
disable_dns:
description: Indicates whether DNS management is disabled on this peer or not
type: boolean
example: false
disable_firewall:
description: Indicates whether firewall management is disabled on this peer or not
type: boolean
example: false
block_lan_access:
description: Indicates whether LAN access is blocked on this peer when used as a routing peer
type: boolean
example: false
block_inbound:
description: Indicates whether inbound traffic is blocked on this peer
type: boolean
example: false
lazy_connection_enabled:
description: Indicates whether lazy connection is enabled on this peer
type: boolean
example: false
Peer:
allOf:
- $ref: '#/components/schemas/PeerMinimum'
- type: object
properties:
created_at:
description: Peer creation date (UTC)
type: string
format: date-time
example: "2023-05-05T09:00:35.477782Z"
ip:
description: Peer's IP address
type: string
example: 10.64.0.1
ipv6:
description: Peer's IPv6 overlay address
type: string
format: ipv6
example: "fd00:4e42:ab12::1"
connection_ip:
description: Peer's public connection IP address
type: string
example: 35.64.0.1
connected:
description: Peer to Management connection status
type: boolean
example: true
last_seen:
description: Last time peer connected to Netbird's management service
type: string
format: date-time
example: "2023-05-05T10:05:26.420578Z"
os:
description: Peer's operating system and version
type: string
example: Darwin 13.2.1
kernel_version:
description: Peer's operating system kernel version
type: string
example: 23.2.0
geoname_id:
description: Unique identifier from the GeoNames database for a specific geographical location.
type: integer
example: 2643743
version:
description: Peer's daemon or cli version
type: string
example: 0.14.0
groups:
description: Groups that the peer belongs to
type: array
items:
$ref: '#/components/schemas/GroupMinimum'
ssh_enabled:
description: Indicates whether SSH server is enabled on this peer
type: boolean
example: true
user_id:
description: User ID of the user that enrolled this peer
type: string
example: google-oauth2|277474792786460067937
hostname:
description: Hostname of the machine
type: string
example: stage-host-1
ui_version:
description: Peer's desktop UI version
type: string
example: 0.14.0
dns_label:
description: Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
type: string
example: stage-host-1.netbird.cloud
login_expiration_enabled:
description: Indicates whether peer login expiration has been enabled or not
type: boolean
example: false
login_expired:
description: Indicates whether peer's login expired or not
type: boolean
example: false
last_login:
description: Last time this peer performed log in (authentication). E.g., user authenticated.
type: string
format: date-time
example: "2023-05-05T09:00:35.477782Z"
inactivity_expiration_enabled:
description: Indicates whether peer inactivity expiration has been enabled or not
type: boolean
example: false
approval_required:
description: (Cloud only) Indicates whether peer needs approval
type: boolean
example: true
disapproval_reason:
description: (Cloud only) Reason why the peer requires approval
type: string
country_code:
$ref: '#/components/schemas/CountryCode'
city_name:
$ref: '#/components/schemas/CityName'
serial_number:
description: System serial number
type: string
example: "C02XJ0J0JGH7"
extra_dns_labels:
description: Extra DNS labels added to the peer
type: array
items:
type: string
example: "stage-host-1"
ephemeral:
description: Indicates whether the peer is ephemeral or not
type: boolean
example: false
local_flags:
$ref: '#/components/schemas/PeerLocalFlags'
required:
- city_name
- connected
- connection_ip
- country_code
- created_at
- dns_label
- geoname_id
- groups
- hostname
- ip
- kernel_version
- last_login
- last_seen
- login_expiration_enabled
- login_expired
- inactivity_expiration_enabled
- os
- ssh_enabled
- user_id
- version
- ui_version
- approval_required
- serial_number
- extra_dns_labels
- ephemeral
PeerRequest:
type: object
properties:
name:
type: string
example: stage-host-1
ssh_enabled:
type: boolean
example: true
login_expiration_enabled:
type: boolean
example: false
inactivity_expiration_enabled:
type: boolean
example: false
approval_required:
description: (Cloud only) Indicates whether peer needs approval
type: boolean
example: true
ip:
description: Peer's IP address
type: string
format: ipv4
example: 100.64.0.15
ipv6:
description: Peer's IPv6 overlay address. Omitted if IPv6 is not enabled for the account.
type: string
format: ipv6
example: "fd00:4e42:ab12::1"
required:
- name
- ssh_enabled
- login_expiration_enabled
- inactivity_expiration_enabled
User:
type: object
properties:
id:
description: User ID
type: string
example: google-oauth2|277474792786460067937
email:
description: User's email address
type: string
example: demo@netbird.io
password:
description: User's password. Only present when user is created (create user endpoint is called) and only when IdP supports user creation with password.
type: string
example: super_secure_password
name:
description: User's name from idp provider
type: string
example: Tom Schulz
role:
description: User's NetBird account role
type: string
example: admin
status:
description: User's status
type: string
enum:
- "active"
- "invited"
- "blocked"
example: active
last_login:
description: Last time this user performed a login to the dashboard
type: string
format: date-time
example: "2023-05-05T09:00:35.477782Z"
auto_groups:
description: Group IDs to auto-assign to peers registered by this user
type: array
items:
type: string
example: ch8i4ug6lnn4g9hqv7m0
is_current:
description: Is true if authenticated user is the same as this user
type: boolean
readOnly: true
example: true
is_service_user:
description: Is true if this user is a service user
type: boolean
readOnly: true
example: false
is_blocked:
description: Is true if this user is blocked. Blocked users can't use the system
type: boolean
example: false
pending_approval:
description: Is true if this user requires approval before being activated. Only applicable for users joining via domain matching when user_approval_required is enabled.
type: boolean
example: false
issued:
description: How user was issued by API or Integration
type: string
example: api
idp_id:
description: Identity provider ID (connector ID) that the user authenticated with. Only populated for users with Dex-encoded user IDs.
type: string
example: okta-abc123
permissions:
$ref: '#/components/schemas/UserPermissions'
required:
- id
- email
- name
- role
- auto_groups
- status
- is_blocked
- pending_approval
UserCreateRequest:
type: object
properties:
email:
description: User's Email to send invite to
type: string
example: demo@netbird.io
name:
description: User's full name
type: string
example: Tom Schulz
role:
description: User's NetBird account role
type: string
example: admin
auto_groups:
description: Group IDs to auto-assign to peers registered by this user
type: array
items:
type: string
example: ch8i4ug6lnn4g9hqv7m0
is_service_user:
description: Is true if this user is a service user
type: boolean
example: false
required:
- role
- auto_groups
- is_service_user
UserRequest:
type: object
properties:
role:
description: User's NetBird account role
type: string
example: admin
auto_groups:
description: Group IDs to auto-assign to peers registered by this user
type: array
items:
type: string
example: ch8i4ug6lnn4g9hqv7m0
is_blocked:
description: If set to true then user is blocked and can't use the system
type: boolean
example: false
required:
- role
- auto_groups
- is_blocked
UserPermissions:
type: object
properties:
is_restricted:
type: boolean
description: Indicates whether this User's Peers view is restricted
modules:
type: object
additionalProperties:
type: object
additionalProperties:
type: boolean
propertyNames:
type: string
description: The operation type
propertyNames:
type: string
description: The module name
example: {"networks": {"read": true, "create": false, "update": false, "delete": false}, "peers": {"read": false, "create": false, "update": false, "delete": false}}
required:
- modules
- is_restricted
responses:
not_found:
description: Resource not found
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: {}
validation_failed_simple:
description: Validation failed
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: {}
bad_request:
description: Bad Request
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: {}
unprocessable:
description: Unprocessable Entity
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content:
application/json:
schema:
type: object
properties:
message:
type: string
code:
type: integer
required:
- message
- code
internal_error:
description: Internal Server Error
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: {}
validation_failed:
description: Validation failed
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: {}
forbidden:
description: Forbidden
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: {}
requires_authentication:
description: Requires authentication
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: {}
conflict:
description: Conflict
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
headers:
X-Request-Id:
description: |
Unique identifier assigned to the request by the server and set on every
response. Useful for correlating client requests with server-side logs.
schema:
type: string
example: cot7r4n3l3vh3qj4qveg
example: cot7r4n3l3vh3qj4qveg
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
TokenAuth:
type: apiKey
in: header
name: Authorization
description: >-
Enter the token with the `Token` prefix, e.g. "Token nbp_F3f0d.....".
pathItems:
peers:
get:
summary: List all Peers
description: Returns a list of all peers
tags:
- Peers
parameters:
- name: page
in: query
description: Page number
required: false
schema:
type: integer
minimum: 1
default: 1
- name: page_size
in: query
description: Number of peers per page
required: false
schema:
type: integer
minimum: 1
maximum: 250
default: 100
- name: connected
in: query
description: Filter by peer connected status
required: false
schema:
type: boolean
- name: approval_required
in: query
description: Filter by approval_required field
required: false
schema:
type: boolean
- name: os
in: query
description: Peer os
required: false
schema:
type: array
items:
type: string
enum:
- linux
- windows
- mac
- android
- ios
- js
- name: search
in: query
description: filter peers by name, dns_label, ip, os, version, serial_number, owner name, owner email, group names, mac
required: false
schema:
type: string
security:
- BearerAuth: []
- TokenAuth: []
responses:
'200':
description: A JSON Array of Peers
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/PeerBatch'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'422':
"$ref": "#/components/responses/unprocessable"
'500':
"$ref": "#/components/responses/internal_error"
peer:
get:
summary: Retrieve a Peer
description: Get information about a peer
tags:
- Peers
security:
- BearerAuth: []
- TokenAuth: []
parameters:
- in: path
name: peerId
required: true
schema:
type: string
description: The unique identifier of a peer
responses:
'200':
description: A Peer object
content:
application/json:
schema:
$ref: '#/components/schemas/Peer'
'400':
"$ref": '#/components/responses/bad_request'
'401':
"$ref": '#/components/responses/requires_authentication'
'403':
"$ref": '#/components/responses/forbidden'
'500':
"$ref": '#/components/responses/internal_error'
put:
summary: Update a Peer
description: Update information about a peer
tags:
- Peers
security:
- BearerAuth: []
- TokenAuth: []
parameters:
- in: path
name: peerId
required: true
schema:
type: string
description: The unique identifier of a peer
requestBody:
description: update a peer
required: true
content:
'application/json':
schema:
$ref: '#/components/schemas/PeerRequest'
responses:
'200':
description: A Peer object
content:
application/json:
schema:
$ref: '#/components/schemas/Peer'
'400':
"$ref": '#/components/responses/bad_request'
'401':
"$ref": '#/components/responses/requires_authentication'
'403':
"$ref": '#/components/responses/forbidden'
'422':
"$ref": "#/components/responses/unprocessable"
'500':
"$ref": '#/components/responses/internal_error'
delete:
summary: Delete a Peer
description: Delete a peer
tags:
- Peers
security:
- BearerAuth: []
- TokenAuth: []
parameters:
- in: path
name: peerId
required: true
schema:
type: string
description: The unique identifier of a peer
responses:
'200':
description: Delete status code
content: {}
'400':
"$ref": '#/components/responses/bad_request'
'401':
"$ref": '#/components/responses/requires_authentication'
'403':
"$ref": '#/components/responses/forbidden'
'500':
"$ref": '#/components/responses/internal_error'
UsersPath:
get:
summary: List all Users
description: Returns a list of all users
tags:
- Users
security:
- BearerAuth: []
- TokenAuth: []
parameters:
- in: query
name: service_user
schema:
type: boolean
description: Filters users and returns either regular users or service users
responses:
'200':
description: A JSON array of Users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'422':
"$ref": "#/components/responses/unprocessable"
'500':
"$ref": "#/components/responses/internal_error"
post:
summary: Create a User
description: Creates a new service user or sends an invite to a regular user
tags:
- Users
security:
- BearerAuth: []
- TokenAuth: []
requestBody:
description: User invite information
required: true
content:
'application/json':
schema:
$ref: '#/components/schemas/UserCreateRequest'
responses:
'200':
description: A User object
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'422':
"$ref": "#/components/responses/unprocessable"
'500':
"$ref": "#/components/responses/internal_error"
UserPath:
put:
summary: Update a User
description: Update information about a User
tags:
- Users
security:
- BearerAuth: []
- TokenAuth: []
parameters:
- in: path
name: userId
required: true
schema:
type: string
description: The unique identifier of a user
requestBody:
description: User update
required: true
content:
'application/json':
schema:
$ref: '#/components/schemas/UserRequest'
responses:
'200':
description: A User object
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'422':
"$ref": "#/components/responses/unprocessable"
'500':
"$ref": "#/components/responses/internal_error"
delete:
summary: Delete a User
description: This method removes a user from accessing the system. For this leaves the IDP user intact unless the `--user-delete-from-idp` is passed to management startup.
tags:
- Users
security:
- BearerAuth: []
- TokenAuth: []
parameters:
- in: path
name: userId
required: true
schema:
type: string
description: The unique identifier of a user
responses:
'200':
description: Delete status code
content: {}
'400':
"$ref": "#/components/responses/bad_request"
'401':
"$ref": "#/components/responses/requires_authentication"
'403':
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
paths:
/peers:
$ref: '#/components/pathItems/peers'
/peers/{peerId}:
$ref: '#/components/pathItems/peer'
/users:
$ref: '#/components/pathItems/UsersPath'
/users/{userId}:
$ref: '#/components/pathItems/UserPath'
@@ -0,0 +1,31 @@
components:
schemas:
GroupMinimum:
type: object
properties:
id:
description: Group ID
type: string
example: ch8i4ug6lnn4g9hqv7m0
name:
description: Group Name identifier
type: string
example: devs
peers_count:
description: Count of peers associated to the group
type: integer
example: 2
resources_count:
description: Count of resources associated to the group
type: integer
example: 5
issued:
description: How the group was issued (api, integration, jwt)
type: string
enum: [ "api", "integration", "jwt" ]
example: api
required:
- id
- name
- peers_count
- resources_count
@@ -0,0 +1,250 @@
openapi: 3.1.0
servers:
- url: /api/v1alpha1
description: Default server
info:
title: NetBird REST API
description: API to manipulate groups, rules, policies and retrieve information about peers and users
version: 1.0.0-alpha1
tags:
- name: Users
description: Interact with and view information about users.
- name: Tokens
description: Interact with and view information about tokens.
- name: Peers
description: Interact with and view information about peers.
- name: Setup Keys
description: Interact with and view information about setup keys.
- name: Groups
description: Interact with and view information about groups.
- name: Policies
description: Interact with and view information about policies.
- name: Posture Checks
description: Interact with and view information about posture checks.
- name: Routes
description: Interact with and view information about routes.
- name: DNS
description: Interact with and view information about DNS configuration.
- name: DNS Zones
description: Interact with and view information about custom DNS zones.
- name: Events
description: View information about the account and network events.
- name: Accounts
description: View information about the accounts.
- name: Ingress Ports
description: Interact with and view information about the ingress peers and ports.
x-cloud-only: true
- name: Identity Providers
description: Interact with and view information about identity providers.
- name: Services
description: Interact with and view information about reverse proxy services.
- name: Instance
description: Instance setup and status endpoints for initial configuration.
- name: Jobs
description: Interact with and view information about remote jobs.
x-experimental: true
- name: Usage
description: Retrieve current usage statistics for the account.
x-cloud-only: true
- name: Subscription
description: Manage and view information about account subscriptions.
x-cloud-only: true
- name: Plans
description: Retrieve available plans and products.
x-cloud-only: true
- name: Checkout
description: Manage checkout sessions for plan subscriptions.
x-cloud-only: true
- name: AWS Marketplace
description: Manage AWS Marketplace subscriptions.
x-cloud-only: true
- name: Portal
description: Access customer portal for subscription management.
x-cloud-only: true
- name: Invoice
description: Manage and retrieve account invoices.
x-cloud-only: true
- name: MSP
description: MSP portal for Tenant management.
x-cloud-only: true
- name: IDP SCIM Integrations
description: Manage generic SCIM identity provider integrations for user and group sync.
x-cloud-only: true
- name: IDP Google Integrations
description: Manage Google Workspace identity provider integrations for user and group sync.
x-cloud-only: true
- name: IDP Azure Integrations
description: Manage Azure AD identity provider integrations for user and group sync.
x-cloud-only: true
- name: IDP Okta SCIM Integrations
description: Manage Okta SCIM identity provider integrations for user and group sync.
x-cloud-only: true
- name: EDR Intune Integrations
description: Manage Microsoft Intune EDR integrations.
x-cloud-only: true
- name: EDR SentinelOne Integrations
description: Manage SentinelOne EDR integrations.
x-cloud-only: true
- name: EDR Falcon Integrations
description: Manage CrowdStrike Falcon EDR integrations.
x-cloud-only: true
- name: EDR Huntress Integrations
description: Manage Huntress EDR integrations.
x-cloud-only: true
- name: EDR FleetDM Integrations
description: Manage FleetDM EDR integrations.
x-cloud-only: true
- name: EDR Peers
description: Manage EDR compliance bypass for peers.
x-cloud-only: true
- name: Event Streaming Integrations
description: Manage event streaming integrations.
x-cloud-only: true
- name: Notifications
description: Manage notification channels for account event alerts.
x-cloud-only: true
components:
schemas:
CountryCode:
description: 2-letter ISO 3166-1 alpha-2 code that represents the country
type: string
example: "DE"
CityName:
description: Commonly used English name of the city
type: string
example: "Berlin"
Country:
description: Describe country geographical location information
type: object
properties:
country_name:
description: Commonly used English name of the country
type: string
example: "Germany"
country_code:
$ref: '#/components/schemas/CountryCode'
required:
- country_name
- country_code
City:
description: Describe city geographical location information
type: object
properties:
geoname_id:
description: Integer ID of the record in GeoNames database
type: integer
example: 2950158
city_name:
description: Commonly used English name of the city
type: string
example: "Berlin"
required:
- geoname_id
- city_name
ErrorResponse:
type: object
description: Standard error response
properties:
message:
type: string
description: A human-readable error message.
example: "couldn't parse JSON request"
responses:
not_found:
description: Resource not found
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: { }
validation_failed_simple:
description: Validation failed
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: { }
bad_request:
description: Bad Request
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: { }
unprocessable:
description: Unprocessable Entity
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content:
application/json:
schema:
type: object
properties:
message:
type: string
code:
type: integer
required:
- message
- code
internal_error:
description: Internal Server Error
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: { }
validation_failed:
description: Validation failed
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: { }
forbidden:
description: Forbidden
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: { }
requires_authentication:
description: Requires authentication
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content: { }
conflict:
description: Conflict
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
headers:
X-Request-Id:
description: |
Unique identifier assigned to the request by the server and set on every
response. Useful for correlating client requests with server-side logs.
schema:
type: string
example: cot7r4n3l3vh3qj4qveg
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
TokenAuth:
type: apiKey
in: header
name: Authorization
description: >-
Enter the token with the `Token` prefix, e.g. "Token nbp_F3f0d.....".
paths:
/peers:
$ref: './peer/peers.yaml'
/peers/{peerId}:
$ref: './peer/peer.yaml'
/users:
$ref: './user/user_paths.yaml#/UsersPath'
/users/{userId}:
$ref: './user/user_paths.yaml#/UserPath'
@@ -0,0 +1,257 @@
components:
schemas:
PeerRequest:
type: object
properties:
name:
type: string
example: stage-host-1
ssh_enabled:
type: boolean
example: true
login_expiration_enabled:
type: boolean
example: false
inactivity_expiration_enabled:
type: boolean
example: false
approval_required:
description: (Cloud only) Indicates whether peer needs approval
type: boolean
example: true
ip:
description: Peer's IP address
type: string
format: ipv4
example: 100.64.0.15
ipv6:
description: Peer's IPv6 overlay address. Omitted if IPv6 is not enabled for the account.
type: string
format: ipv6
example: "fd00:4e42:ab12::1"
required:
- name
- ssh_enabled
- login_expiration_enabled
- inactivity_expiration_enabled
PeerMinimum:
type: object
properties:
id:
description: Peer ID
type: string
example: chacbco6lnnbn6cg5s90
name:
description: Peer's hostname
type: string
example: stage-host-1
required:
- id
- name
Peer:
allOf:
- $ref: '#/components/schemas/PeerMinimum'
- type: object
properties:
created_at:
description: Peer creation date (UTC)
type: string
format: date-time
example: "2023-05-05T09:00:35.477782Z"
ip:
description: Peer's IP address
type: string
example: 10.64.0.1
ipv6:
description: Peer's IPv6 overlay address
type: string
format: ipv6
example: "fd00:4e42:ab12::1"
connection_ip:
description: Peer's public connection IP address
type: string
example: 35.64.0.1
connected:
description: Peer to Management connection status
type: boolean
example: true
last_seen:
description: Last time peer connected to Netbird's management service
type: string
format: date-time
example: "2023-05-05T10:05:26.420578Z"
os:
description: Peer's operating system and version
type: string
example: Darwin 13.2.1
kernel_version:
description: Peer's operating system kernel version
type: string
example: 23.2.0
geoname_id:
description: Unique identifier from the GeoNames database for a specific geographical location.
type: integer
example: 2643743
version:
description: Peer's daemon or cli version
type: string
example: 0.14.0
groups:
description: Groups that the peer belongs to
type: array
items:
$ref: '../group/components.yaml#/components/schemas/GroupMinimum'
ssh_enabled:
description: Indicates whether SSH server is enabled on this peer
type: boolean
example: true
user_id:
description: User ID of the user that enrolled this peer
type: string
example: google-oauth2|277474792786460067937
hostname:
description: Hostname of the machine
type: string
example: stage-host-1
ui_version:
description: Peer's desktop UI version
type: string
example: 0.14.0
dns_label:
description: Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
type: string
example: stage-host-1.netbird.cloud
login_expiration_enabled:
description: Indicates whether peer login expiration has been enabled or not
type: boolean
example: false
login_expired:
description: Indicates whether peer's login expired or not
type: boolean
example: false
last_login:
description: Last time this peer performed log in (authentication). E.g., user authenticated.
type: string
format: date-time
example: "2023-05-05T09:00:35.477782Z"
inactivity_expiration_enabled:
description: Indicates whether peer inactivity expiration has been enabled or not
type: boolean
example: false
approval_required:
description: (Cloud only) Indicates whether peer needs approval
type: boolean
example: true
disapproval_reason:
description: (Cloud only) Reason why the peer requires approval
type: string
country_code:
$ref: '../openapi.yaml#/components/schemas/CountryCode'
city_name:
$ref: '../openapi.yaml#/components/schemas/CityName'
serial_number:
description: System serial number
type: string
example: "C02XJ0J0JGH7"
extra_dns_labels:
description: Extra DNS labels added to the peer
type: array
items:
type: string
example: "stage-host-1"
ephemeral:
description: Indicates whether the peer is ephemeral or not
type: boolean
example: false
local_flags:
$ref: '#/components/schemas/PeerLocalFlags'
required:
- city_name
- connected
- connection_ip
- country_code
- created_at
- dns_label
- geoname_id
- groups
- hostname
- ip
- kernel_version
- last_login
- last_seen
- login_expiration_enabled
- login_expired
- inactivity_expiration_enabled
- os
- ssh_enabled
- user_id
- version
- ui_version
- approval_required
- serial_number
- extra_dns_labels
- ephemeral
PeerLocalFlags:
type: object
properties:
rosenpass_enabled:
description: Indicates whether Rosenpass is enabled on this peer
type: boolean
example: true
rosenpass_permissive:
description: Indicates whether Rosenpass is in permissive mode or not
type: boolean
example: false
server_ssh_allowed:
description: Indicates whether SSH access this peer is allowed or not
type: boolean
example: true
remote_jobs_allowed:
description: Indicates whether the peer has opted into management-requested remote jobs (e.g. debug bundles)
type: boolean
example: true
disable_client_routes:
description: Indicates whether client routes are disabled on this peer or not
type: boolean
example: false
disable_server_routes:
description: Indicates whether server routes are disabled on this peer or not
type: boolean
example: false
disable_dns:
description: Indicates whether DNS management is disabled on this peer or not
type: boolean
example: false
disable_firewall:
description: Indicates whether firewall management is disabled on this peer or not
type: boolean
example: false
block_lan_access:
description: Indicates whether LAN access is blocked on this peer when used as a routing peer
type: boolean
example: false
block_inbound:
description: Indicates whether inbound traffic is blocked on this peer
type: boolean
example: false
lazy_connection_enabled:
description: Indicates whether lazy connection is enabled on this peer
type: boolean
example: false
PeerBatch:
allOf:
- $ref: '#/components/schemas/Peer'
- type: object
properties:
created_at:
description: Peer creation date (UTC)
type: string
format: date-time
example: "2023-05-05T09:00:35.477782Z"
accessible_peers_count:
description: Number of accessible peers
type: integer
example: 5
required:
- created_at
- accessible_peers_count
@@ -0,0 +1,93 @@
get:
summary: Retrieve a Peer
description: Get information about a peer
tags: [ Peers ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: peerId
required: true
schema:
type: string
description: The unique identifier of a peer
responses:
'200':
description: A Peer object
content:
application/json:
schema:
$ref: './components.yaml#/components/schemas/Peer'
'400':
"$ref": '../openapi.yaml#/components/responses/bad_request'
'401':
"$ref": '../openapi.yaml#/components/responses/requires_authentication'
'403':
"$ref": '../openapi.yaml#/components/responses/forbidden'
'500':
"$ref": '../openapi.yaml#/components/responses/internal_error'
put:
summary: Update a Peer
description: Update information about a peer
tags: [ Peers ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: peerId
required: true
schema:
type: string
description: The unique identifier of a peer
requestBody:
description: update a peer
required: true
content:
'application/json':
schema:
$ref: './components.yaml#/components/schemas/PeerRequest'
responses:
'200':
description: A Peer object
content:
application/json:
schema:
$ref: './components.yaml#/components/schemas/Peer'
'400':
"$ref": '../openapi.yaml#/components/responses/bad_request'
'401':
"$ref": '../openapi.yaml#/components/responses/requires_authentication'
'403':
"$ref": '../openapi.yaml#/components/responses/forbidden'
'422':
"$ref": "../openapi.yaml#/components/responses/unprocessable"
'500':
"$ref": '../openapi.yaml#/components/responses/internal_error'
delete:
summary: Delete a Peer
description: Delete a peer
tags: [ Peers ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: peerId
required: true
schema:
type: string
description: The unique identifier of a peer
responses:
'200':
description: Delete status code
content: { }
'400':
"$ref": '../openapi.yaml#/components/responses/bad_request'
'401':
"$ref": '../openapi.yaml#/components/responses/requires_authentication'
'403':
"$ref": '../openapi.yaml#/components/responses/forbidden'
'500':
"$ref": '../openapi.yaml#/components/responses/internal_error'
@@ -0,0 +1,77 @@
get:
summary: List all Peers
description: Returns a list of all peers
tags: [ Peers ]
parameters:
- name: page
in: query
description: Page number
required: false
schema:
type: integer
minimum: 1
default: 1
- name: page_size
in: query
description: Number of peers per page
required: false
schema:
type: integer
minimum: 1
maximum: 250
default: 100
- name: connected
in: query
description: Filter by peer connected status
required: false
schema:
type: boolean
- name: approval_required
in: query
description: Filter by approval_required field
required: false
schema:
type: boolean
- name: os
in: query
description: Peer os
required: false
schema:
type: array
items:
type: string
enum:
- linux
- windows
- mac
- android
- ios
- js
- name: search
in: query
description: filter peers by name, dns_label, ip, os, version, serial_number, owner name, owner email, group names, mac
required: false
schema:
type: string
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
responses:
'200':
description: A JSON Array of Peers
content:
application/json:
schema:
type: array
items:
$ref: './components.yaml#/components/schemas/PeerBatch'
'400':
"$ref": "../openapi.yaml#/components/responses/bad_request"
'401':
"$ref": "../openapi.yaml#/components/responses/requires_authentication"
'403':
"$ref": "../openapi.yaml#/components/responses/forbidden"
'422':
"$ref": "../openapi.yaml#/components/responses/unprocessable"
'500':
"$ref": "../openapi.yaml#/components/responses/internal_error"
@@ -0,0 +1,93 @@
package apiv1alpha1
import (
_ "embed"
"fmt"
"log/slog"
"net/http"
"os"
"strings"
"github.com/netbirdio/netbird/shared/management/http/util"
"github.com/netbirdio/netbird/shared/management/status"
"github.com/pb33f/libopenapi"
validator "github.com/pb33f/libopenapi-validator"
"github.com/pb33f/libopenapi-validator/config"
"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi/datamodel"
log "github.com/sirupsen/logrus"
)
// TODO (dmitri) this needs to be extracted, as it will grow to 500Kb
//
//go:embed bundle.yaml
var bundle []byte
func CreateV1ApiValidatingMiddleware() (*V1ValidatorMiddleware, error) {
doc, err := libopenapi.NewDocumentWithConfiguration(bundle, &datamodel.DocumentConfiguration{
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})),
})
if err != nil {
return nil, err
}
model, err := doc.BuildV3Model()
if err != nil {
return nil, err
}
// TODO figure out document validation: rn it's possible to have a spec
// that's not entirely correct -- parts of it fail to parse, but silently
v := validator.NewValidatorFromV3Model(&model.Model,
config.WithoutSecurityValidation(),
config.WithStandardBodyDecoders(),
config.WithRejectUnsupportedBodyContent(),
config.WithRequestDefaults())
// v.SetDocument(doc)
// v.ValidatePathParams()
// if valid, errs := v.ValidateDocument(); !valid {
// return nil, fmt.Errorf("error validating OpenAPI doc, %s", errs)
// }
return &V1ValidatorMiddleware{Validator: v}, nil
}
type V1ValidatorMiddleware struct {
Validator validator.Validator
}
func (v *V1ValidatorMiddleware) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
valid, errs := v.Validator.ValidateHttpRequestSync(r)
if !valid {
validationErrs := make([]string, 0, len(errs))
for _, err := range errs {
validationErrs = append(validationErrs, validationError(err))
}
log.WithContext(r.Context()).Debugf("error validating request: %s", strings.Join(validationErrs, ", "))
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid request: %s", strings.Join(validationErrs, ", ")), w)
return
}
h.ServeHTTP(w, r)
})
}
func validationError(err *errors.ValidationError) string {
if err.SchemaValidationErrors != nil {
errs := make([]string, 0, len(err.SchemaValidationErrors))
for _, e := range err.SchemaValidationErrors {
errs = append(errs, fmt.Sprintf("field %s: %s", e.FieldPath, e.Reason))
}
return fmt.Sprintf("%s: %s", err.Message, strings.Join(errs, ", "))
} else {
if err.SpecLine > 0 && err.SpecCol > 0 {
return fmt.Sprintf("%s, Line: %d, Column: %d", err.Message, err.SpecLine, err.SpecCol)
} else {
return fmt.Sprint(err.Message)
}
}
}
@@ -0,0 +1,152 @@
package apiv1alpha1
import (
"log/slog"
"os"
"path/filepath"
"strings"
"unicode"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/bundler"
"github.com/pb33f/libopenapi/datamodel"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
"github.com/pb33f/libopenapi/generator/golang"
)
var ApiPath = filepath.Join("shared", "management", "http", "apiv1alpha1", "openapi.yaml")
func GenerateV1ApiBindings(openapipath string) ([]byte, *v3.Document, error) {
specFile, err := os.ReadFile(openapipath)
if err != nil {
return nil, nil, err
}
multiFileDoc, err := libopenapi.NewDocumentWithConfiguration(specFile, &datamodel.DocumentConfiguration{
AllowFileReferences: true,
BasePath: filepath.Dir(openapipath),
SpecFilePath: openapipath,
ExtractRefsSequentially: true,
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelError,
})),
TransformSiblingRefs: true, // enable openapi 3.1 compliance by default
MergeReferencedProperties: true, // enable enhanced resolution by default
PropertyMergeStrategy: datamodel.PreserveLocal, // local properties take precedence
})
if err != nil {
return nil, nil, err
}
multiFileModel, err := multiFileDoc.BuildV3Model()
if err != nil {
return nil, nil, err
}
bundle, err := bundler.BundleDocumentComposed(&multiFileModel.Model, &bundler.BundleCompositionConfig{
StrictValidation: true,
})
if err != nil {
return nil, nil, err
}
doc, err := libopenapi.NewDocumentWithConfiguration(bundle, &datamodel.DocumentConfiguration{
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})),
})
if err != nil {
return nil, nil, err
}
model, err := doc.BuildV3Model()
if err != nil {
return nil, nil, err
}
return bundle, &model.Model, nil
}
func GenerateV1Schema(model *v3.Document) (*golang.GeneratedFile, error) {
// render every schema in components.schemas into one file
gen := golang.NewGenerator(
golang.WithGeneratedComment(true),
golang.WithFormatMapping("date-time", "time.Time", "time"),
golang.WithOptionalFieldsAsPointers(true),
golang.WithEnumConstants(true),
golang.WithNestedTypeNameDelimiter(""),
golang.WithPackageName("apiv1alpha1"),
golang.WithFieldNameResolver(toPublicName))
return gen.RenderSchemas(model.Components.Schemas)
}
// this is to keep existing naming of fields like "id", "url", etc
// libopenapi by default converts them to all-uppercase, like "ID", "URL", etc
func toPublicName(name string) string {
parts := splitIdentifier(name)
if len(parts) == 0 {
return "Value"
}
var b strings.Builder
for _, p := range parts {
rs := []rune(strings.ToLower(p))
rs[0] = unicode.ToUpper(rs[0])
b.WriteString(string(rs))
}
out := b.String()
first := []rune(out)[0]
if unicode.IsDigit(first) {
return "Value" + out
}
return out
}
func splitIdentifier(name string) []string {
var raw []string
var b strings.Builder
flush := func() {
if b.Len() > 0 {
raw = append(raw, b.String())
b.Reset()
}
}
for _, r := range name {
switch {
case unicode.IsLetter(r) || unicode.IsDigit(r):
b.WriteRune(r)
default:
flush()
}
}
flush()
var parts []string
for _, part := range raw {
parts = append(parts, splitCamel(part)...)
}
return parts
}
func splitCamel(value string) []string {
rs := []rune(value)
if len(rs) == 0 {
return nil
}
var parts []string
start := 0
for i := 1; i < len(rs); i++ {
prev := rs[i-1]
cur := rs[i]
var next rune
if i+1 < len(rs) {
next = rs[i+1]
}
lowerToUpper := unicode.IsLower(prev) && unicode.IsUpper(cur)
acronymToWord := unicode.IsUpper(prev) && unicode.IsUpper(cur) && next != 0 && unicode.IsLower(next)
if lowerToUpper || acronymToWord {
parts = append(parts, string(rs[start:i]))
start = i
}
}
parts = append(parts, string(rs[start:]))
return parts
}
@@ -0,0 +1,414 @@
// Code generated by libopenapi generator/golang. DO NOT EDIT.
package apiv1alpha1
import (
"encoding/json"
"time"
)
// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country.
// CountryCode example value is defined in the OpenAPI schema.
type CountryCode string
// CityName Commonly used English name of the city.
// CityName example value is defined in the OpenAPI schema.
type CityName string
// Country Describe country geographical location information.
type Country struct {
// CountryName Commonly used English name of the country.
// CountryName example value is defined in the OpenAPI schema.
CountryName string `json:"country_name"`
CountryCode CountryCode `json:"country_code"`
}
// City Describe city geographical location information.
type City struct {
// GeonameId Integer ID of the record in GeoNames database.
// GeonameId example value is defined in the OpenAPI schema.
GeonameId int `json:"geoname_id"`
// CityName Commonly used English name of the city.
// CityName example value is defined in the OpenAPI schema.
CityName string `json:"city_name"`
}
// ErrorResponse Standard error response.
type ErrorResponse struct {
// Message A human-readable error message.
// Message example value is defined in the OpenAPI schema.
Message *string `json:"message,omitempty"`
}
type PeerBatch struct {
Peer
// CreatedAt Peer creation date (UTC).
// CreatedAt example value is defined in the OpenAPI schema.
CreatedAt time.Time `json:"created_at"`
// AccessiblePeersCount Number of accessible peers.
// AccessiblePeersCount example value is defined in the OpenAPI schema.
AccessiblePeersCount int `json:"accessible_peers_count"`
}
type PeerMinimum struct {
// Id Peer ID.
// Id example value is defined in the OpenAPI schema.
Id string `json:"id"`
// Name Peer's hostname.
// Name example value is defined in the OpenAPI schema.
Name string `json:"name"`
}
// GroupMinimumIssued How the group was issued (api, integration, jwt).
// GroupMinimumIssued example value is defined in the OpenAPI schema.
type GroupMinimumIssued string
const (
GroupMinimumIssuedAPI GroupMinimumIssued = "api"
GroupMinimumIssuedIntegration GroupMinimumIssued = "integration"
GroupMinimumIssuedJWT GroupMinimumIssued = "jwt"
)
type GroupMinimum struct {
// Id Group ID.
// Id example value is defined in the OpenAPI schema.
Id string `json:"id"`
// Name Group Name identifier.
// Name example value is defined in the OpenAPI schema.
Name string `json:"name"`
// PeersCount Count of peers associated to the group.
// PeersCount example value is defined in the OpenAPI schema.
PeersCount int `json:"peers_count"`
// ResourcesCount Count of resources associated to the group.
// ResourcesCount example value is defined in the OpenAPI schema.
ResourcesCount int `json:"resources_count"`
// Issued How the group was issued (api, integration, jwt).
// Issued example value is defined in the OpenAPI schema.
Issued *GroupMinimumIssued `json:"issued,omitempty"`
}
type PeerLocalFlags struct {
// RosenpassEnabled Indicates whether Rosenpass is enabled on this peer.
// RosenpassEnabled example value is defined in the OpenAPI schema.
RosenpassEnabled *bool `json:"rosenpass_enabled,omitempty"`
// RosenpassPermissive Indicates whether Rosenpass is in permissive mode or not.
// RosenpassPermissive example value is defined in the OpenAPI schema.
RosenpassPermissive *bool `json:"rosenpass_permissive,omitempty"`
// ServerSshAllowed Indicates whether SSH access this peer is allowed or not.
// ServerSshAllowed example value is defined in the OpenAPI schema.
ServerSshAllowed *bool `json:"server_ssh_allowed,omitempty"`
// RemoteJobsAllowed Indicates whether the peer has opted into management-requested remote jobs (e.g. debug bundles).
// RemoteJobsAllowed example value is defined in the OpenAPI schema.
RemoteJobsAllowed *bool `json:"remote_jobs_allowed,omitempty"`
// DisableClientRoutes Indicates whether client routes are disabled on this peer or not.
// DisableClientRoutes example value is defined in the OpenAPI schema.
DisableClientRoutes *bool `json:"disable_client_routes,omitempty"`
// DisableServerRoutes Indicates whether server routes are disabled on this peer or not.
// DisableServerRoutes example value is defined in the OpenAPI schema.
DisableServerRoutes *bool `json:"disable_server_routes,omitempty"`
// DisableDns Indicates whether DNS management is disabled on this peer or not.
// DisableDns example value is defined in the OpenAPI schema.
DisableDns *bool `json:"disable_dns,omitempty"`
// DisableFirewall Indicates whether firewall management is disabled on this peer or not.
// DisableFirewall example value is defined in the OpenAPI schema.
DisableFirewall *bool `json:"disable_firewall,omitempty"`
// BlockLanAccess Indicates whether LAN access is blocked on this peer when used as a routing peer.
// BlockLanAccess example value is defined in the OpenAPI schema.
BlockLanAccess *bool `json:"block_lan_access,omitempty"`
// BlockInbound Indicates whether inbound traffic is blocked on this peer.
// BlockInbound example value is defined in the OpenAPI schema.
BlockInbound *bool `json:"block_inbound,omitempty"`
// LazyConnectionEnabled Indicates whether lazy connection is enabled on this peer.
// LazyConnectionEnabled example value is defined in the OpenAPI schema.
LazyConnectionEnabled *bool `json:"lazy_connection_enabled,omitempty"`
}
type Peer struct {
PeerMinimum
// CreatedAt Peer creation date (UTC).
// CreatedAt example value is defined in the OpenAPI schema.
CreatedAt time.Time `json:"created_at"`
// Ip Peer's IP address.
// Ip example value is defined in the OpenAPI schema.
Ip string `json:"ip"`
// Ipv6 Peer's IPv6 overlay address.
// Ipv6 example value is defined in the OpenAPI schema.
Ipv6 *string `json:"ipv6,omitempty"`
// ConnectionIp Peer's public connection IP address.
// ConnectionIp example value is defined in the OpenAPI schema.
ConnectionIp string `json:"connection_ip"`
// Connected Peer to Management connection status.
// Connected example value is defined in the OpenAPI schema.
Connected bool `json:"connected"`
// LastSeen Last time peer connected to Netbird's management service.
// LastSeen example value is defined in the OpenAPI schema.
LastSeen time.Time `json:"last_seen"`
// Os Peer's operating system and version.
// Os example value is defined in the OpenAPI schema.
Os string `json:"os"`
// KernelVersion Peer's operating system kernel version.
// KernelVersion example value is defined in the OpenAPI schema.
KernelVersion string `json:"kernel_version"`
// GeonameId Unique identifier from the GeoNames database for a specific geographical location.
// GeonameId example value is defined in the OpenAPI schema.
GeonameId int `json:"geoname_id"`
// Version Peer's daemon or cli version.
// Version example value is defined in the OpenAPI schema.
Version string `json:"version"`
// Groups Groups that the peer belongs to.
Groups []GroupMinimum `json:"groups"`
// SshEnabled Indicates whether SSH server is enabled on this peer.
// SshEnabled example value is defined in the OpenAPI schema.
SshEnabled bool `json:"ssh_enabled"`
// UserId User ID of the user that enrolled this peer.
// UserId example value is defined in the OpenAPI schema.
UserId string `json:"user_id"`
// Hostname Hostname of the machine.
// Hostname example value is defined in the OpenAPI schema.
Hostname string `json:"hostname"`
// UiVersion Peer's desktop UI version.
// UiVersion example value is defined in the OpenAPI schema.
UiVersion string `json:"ui_version"`
// DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud.
// DnsLabel example value is defined in the OpenAPI schema.
DnsLabel string `json:"dns_label"`
// LoginExpirationEnabled Indicates whether peer login expiration has been enabled or not.
// LoginExpirationEnabled example value is defined in the OpenAPI schema.
LoginExpirationEnabled bool `json:"login_expiration_enabled"`
// LoginExpired Indicates whether peer's login expired or not.
// LoginExpired example value is defined in the OpenAPI schema.
LoginExpired bool `json:"login_expired"`
// LastLogin Last time this peer performed log in (authentication). E.g., user authenticated.
// LastLogin example value is defined in the OpenAPI schema.
LastLogin time.Time `json:"last_login"`
// InactivityExpirationEnabled Indicates whether peer inactivity expiration has been enabled or not.
// InactivityExpirationEnabled example value is defined in the OpenAPI schema.
InactivityExpirationEnabled bool `json:"inactivity_expiration_enabled"`
// ApprovalRequired (Cloud only) Indicates whether peer needs approval.
// ApprovalRequired example value is defined in the OpenAPI schema.
ApprovalRequired bool `json:"approval_required"`
// DisapprovalReason (Cloud only) Reason why the peer requires approval.
DisapprovalReason *string `json:"disapproval_reason,omitempty"`
CountryCode CountryCode `json:"country_code"`
CityName CityName `json:"city_name"`
// SerialNumber System serial number.
// SerialNumber example value is defined in the OpenAPI schema.
SerialNumber string `json:"serial_number"`
// ExtraDnsLabels Extra DNS labels added to the peer.
ExtraDnsLabels []string `json:"extra_dns_labels"`
// Ephemeral Indicates whether the peer is ephemeral or not.
// Ephemeral example value is defined in the OpenAPI schema.
Ephemeral bool `json:"ephemeral"`
LocalFlags *PeerLocalFlags `json:"local_flags,omitempty"`
}
type PeerRequest struct {
// Name example value is defined in the OpenAPI schema.
Name string `json:"name"`
// SshEnabled example value is defined in the OpenAPI schema.
SshEnabled bool `json:"ssh_enabled"`
// LoginExpirationEnabled example value is defined in the OpenAPI schema.
LoginExpirationEnabled bool `json:"login_expiration_enabled"`
// InactivityExpirationEnabled example value is defined in the OpenAPI schema.
InactivityExpirationEnabled bool `json:"inactivity_expiration_enabled"`
// ApprovalRequired (Cloud only) Indicates whether peer needs approval.
// ApprovalRequired example value is defined in the OpenAPI schema.
ApprovalRequired *bool `json:"approval_required,omitempty"`
// Ip Peer's IP address.
// Ip example value is defined in the OpenAPI schema.
Ip *string `json:"ip,omitempty"`
// Ipv6 Peer's IPv6 overlay address. Omitted if IPv6 is not enabled for the account.
// Ipv6 example value is defined in the OpenAPI schema.
Ipv6 *string `json:"ipv6,omitempty"`
}
// UserStatus User's status.
// UserStatus example value is defined in the OpenAPI schema.
type UserStatus string
const (
UserStatusActive UserStatus = "active"
UserStatusInvited UserStatus = "invited"
UserStatusBlocked UserStatus = "blocked"
)
type User struct {
// Id User ID.
// Id example value is defined in the OpenAPI schema.
Id string `json:"id"`
// Email User's email address.
// Email example value is defined in the OpenAPI schema.
Email string `json:"email"`
// Password User's password. Only present when user is created (create user endpoint is called) and only when IdP supports user creation with password.
// Password example value is defined in the OpenAPI schema.
Password *string `json:"password,omitempty"`
// Name User's name from idp provider.
// Name example value is defined in the OpenAPI schema.
Name string `json:"name"`
// Role User's NetBird account role.
// Role example value is defined in the OpenAPI schema.
Role string `json:"role"`
// Status User's status.
// Status example value is defined in the OpenAPI schema.
Status UserStatus `json:"status"`
// LastLogin Last time this user performed a login to the dashboard.
// LastLogin example value is defined in the OpenAPI schema.
LastLogin *time.Time `json:"last_login,omitempty"`
// AutoGroups Group IDs to auto-assign to peers registered by this user.
AutoGroups []string `json:"auto_groups"`
// IsCurrent Is true if authenticated user is the same as this user.
// IsCurrent readOnly.
// IsCurrent example value is defined in the OpenAPI schema.
IsCurrent *bool `json:"is_current,omitempty"`
// IsServiceUser Is true if this user is a service user.
// IsServiceUser readOnly.
// IsServiceUser example value is defined in the OpenAPI schema.
IsServiceUser *bool `json:"is_service_user,omitempty"`
// IsBlocked Is true if this user is blocked. Blocked users can't use the system.
// IsBlocked example value is defined in the OpenAPI schema.
IsBlocked bool `json:"is_blocked"`
// PendingApproval Is true if this user requires approval before being activated. Only applicable for users joining via domain matching when user_approval_required is enabled.
// PendingApproval example value is defined in the OpenAPI schema.
PendingApproval bool `json:"pending_approval"`
// Issued How user was issued by API or Integration.
// Issued example value is defined in the OpenAPI schema.
Issued *string `json:"issued,omitempty"`
// IdpId Identity provider ID (connector ID) that the user authenticated with. Only populated for users with Dex-encoded user IDs.
// IdpId example value is defined in the OpenAPI schema.
IdpId *string `json:"idp_id,omitempty"`
Permissions *UserPermissions `json:"permissions,omitempty"`
}
type UserCreateRequest struct {
// Email User's Email to send invite to.
// Email example value is defined in the OpenAPI schema.
Email *string `json:"email,omitempty"`
// Name User's full name.
// Name example value is defined in the OpenAPI schema.
Name *string `json:"name,omitempty"`
// Role User's NetBird account role.
// Role example value is defined in the OpenAPI schema.
Role string `json:"role"`
// AutoGroups Group IDs to auto-assign to peers registered by this user.
AutoGroups []string `json:"auto_groups"`
// IsServiceUser Is true if this user is a service user.
// IsServiceUser example value is defined in the OpenAPI schema.
IsServiceUser bool `json:"is_service_user"`
}
type UserRequest struct {
// Role User's NetBird account role.
// Role example value is defined in the OpenAPI schema.
Role string `json:"role"`
// AutoGroups Group IDs to auto-assign to peers registered by this user.
AutoGroups []string `json:"auto_groups"`
// IsBlocked If set to true then user is blocked and can't use the system.
// IsBlocked example value is defined in the OpenAPI schema.
IsBlocked bool `json:"is_blocked"`
}
type UserPermissionsModulesAdditionalProperty struct {
AdditionalProperties map[string]bool `json:"-"`
}
func (m *UserPermissionsModulesAdditionalProperty) UnmarshalJSON(data []byte) error {
type Alias UserPermissionsModulesAdditionalProperty
var known Alias
if err := json.Unmarshal(data, &known); err != nil {
return err
}
*m = UserPermissionsModulesAdditionalProperty(known)
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
if len(raw) == 0 {
return nil
}
m.AdditionalProperties = make(map[string]bool, len(raw))
for key, value := range raw {
var decoded bool
if err := json.Unmarshal(value, &decoded); err != nil {
return err
}
m.AdditionalProperties[key] = decoded
}
return nil
}
func (m UserPermissionsModulesAdditionalProperty) MarshalJSON() ([]byte, error) {
type Alias UserPermissionsModulesAdditionalProperty
encoded, err := json.Marshal(Alias(m))
if err != nil {
return nil, err
}
var object map[string]json.RawMessage
if err := json.Unmarshal(encoded, &object); err != nil {
return nil, err
}
for key, value := range m.AdditionalProperties {
encodedValue, err := json.Marshal(value)
if err != nil {
return nil, err
}
object[key] = encodedValue
}
return json.Marshal(object)
}
// UserPermissionsModules example value is defined in the OpenAPI schema.
type UserPermissionsModules struct {
AdditionalProperties map[string]map[string]bool `json:"-"`
}
func (m *UserPermissionsModules) UnmarshalJSON(data []byte) error {
type Alias UserPermissionsModules
var known Alias
if err := json.Unmarshal(data, &known); err != nil {
return err
}
*m = UserPermissionsModules(known)
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
if len(raw) == 0 {
return nil
}
m.AdditionalProperties = make(map[string]map[string]bool, len(raw))
for key, value := range raw {
var decoded map[string]bool
if err := json.Unmarshal(value, &decoded); err != nil {
return err
}
m.AdditionalProperties[key] = decoded
}
return nil
}
func (m UserPermissionsModules) MarshalJSON() ([]byte, error) {
type Alias UserPermissionsModules
encoded, err := json.Marshal(Alias(m))
if err != nil {
return nil, err
}
var object map[string]json.RawMessage
if err := json.Unmarshal(encoded, &object); err != nil {
return nil, err
}
for key, value := range m.AdditionalProperties {
encodedValue, err := json.Marshal(value)
if err != nil {
return nil, err
}
object[key] = encodedValue
}
return json.Marshal(object)
}
type UserPermissions struct {
// IsRestricted Indicates whether this User's Peers view is restricted.
IsRestricted bool `json:"is_restricted"`
// Modules example value is defined in the OpenAPI schema.
Modules map[string]map[string]bool `json:"modules"`
}
@@ -0,0 +1,150 @@
components:
schemas:
User:
type: object
properties:
id:
description: User ID
type: string
example: google-oauth2|277474792786460067937
email:
description: User's email address
type: string
example: demo@netbird.io
password:
description: User's password. Only present when user is created (create user endpoint is called) and only when IdP supports user creation with password.
type: string
example: super_secure_password
name:
description: User's name from idp provider
type: string
example: Tom Schulz
role:
description: User's NetBird account role
type: string
example: admin
status:
description: User's status
type: string
enum: [ "active", "invited", "blocked" ]
example: active
last_login:
description: Last time this user performed a login to the dashboard
type: string
format: date-time
example: "2023-05-05T09:00:35.477782Z"
auto_groups:
description: Group IDs to auto-assign to peers registered by this user
type: array
items:
type: string
example: ch8i4ug6lnn4g9hqv7m0
is_current:
description: Is true if authenticated user is the same as this user
type: boolean
readOnly: true
example: true
is_service_user:
description: Is true if this user is a service user
type: boolean
readOnly: true
example: false
is_blocked:
description: Is true if this user is blocked. Blocked users can't use the system
type: boolean
example: false
pending_approval:
description: Is true if this user requires approval before being activated. Only applicable for users joining via domain matching when user_approval_required is enabled.
type: boolean
example: false
issued:
description: How user was issued by API or Integration
type: string
example: api
idp_id:
description: Identity provider ID (connector ID) that the user authenticated with. Only populated for users with Dex-encoded user IDs.
type: string
example: okta-abc123
permissions:
$ref: '#/components/schemas/UserPermissions'
required:
- id
- email
- name
- role
- auto_groups
- status
- is_blocked
- pending_approval
UserPermissions:
type: object
properties:
is_restricted:
type: boolean
description: Indicates whether this User's Peers view is restricted
modules:
type: object
additionalProperties:
type: object
additionalProperties:
type: boolean
propertyNames:
type: string
description: The operation type
propertyNames:
type: string
description: The module name
example: { "networks": { "read": true, "create": false, "update": false, "delete": false }, "peers": { "read": false, "create": false, "update": false, "delete": false } }
required:
- modules
- is_restricted
UserRequest:
type: object
properties:
role:
description: User's NetBird account role
type: string
example: admin
auto_groups:
description: Group IDs to auto-assign to peers registered by this user
type: array
items:
type: string
example: ch8i4ug6lnn4g9hqv7m0
is_blocked:
description: If set to true then user is blocked and can't use the system
type: boolean
example: false
required:
- role
- auto_groups
- is_blocked
UserCreateRequest:
type: object
properties:
email:
description: User's Email to send invite to
type: string
example: demo@netbird.io
name:
description: User's full name
type: string
example: Tom Schulz
role:
description: User's NetBird account role
type: string
example: admin
auto_groups:
description: Group IDs to auto-assign to peers registered by this user
type: array
items:
type: string
example: ch8i4ug6lnn4g9hqv7m0
is_service_user:
description: Is true if this user is a service user
type: boolean
example: false
required:
- role
- auto_groups
- is_service_user
@@ -0,0 +1,129 @@
UsersPath:
get:
summary: List all Users
description: Returns a list of all users
tags: [ Users ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: query
name: service_user
schema:
type: boolean
description: Filters users and returns either regular users or service users
responses:
'200':
description: A JSON array of Users
content:
application/json:
schema:
type: array
items:
$ref: './user_components.yaml#/components/schemas/User'
'400':
"$ref": "../openapi.yaml#/components/responses/bad_request"
'401':
"$ref": "../openapi.yaml#/components/responses/requires_authentication"
'403':
"$ref": "../openapi.yaml#/components/responses/forbidden"
'422':
"$ref": "../openapi.yaml#/components/responses/unprocessable"
'500':
"$ref": "../openapi.yaml#/components/responses/internal_error"
post:
summary: Create a User
description: Creates a new service user or sends an invite to a regular user
tags: [ Users ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
requestBody:
description: User invite information
required: true
content:
'application/json':
schema:
$ref: './user_components.yaml#/components/schemas/UserCreateRequest'
responses:
'200':
description: A User object
content:
application/json:
schema:
$ref: './user_components.yaml#/components/schemas/User'
'400':
"$ref": "../openapi.yaml#/components/responses/bad_request"
'401':
"$ref": "../openapi.yaml#/components/responses/requires_authentication"
'403':
"$ref": "../openapi.yaml#/components/responses/forbidden"
'422':
"$ref": "../openapi.yaml#/components/responses/unprocessable"
'500':
"$ref": "../openapi.yaml#/components/responses/internal_error"
UserPath:
put:
summary: Update a User
description: Update information about a User
tags: [ Users ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: userId
required: true
schema:
type: string
description: The unique identifier of a user
requestBody:
description: User update
required: true
content:
'application/json':
schema:
$ref: './user_components.yaml#/components/schemas/UserRequest'
responses:
'200':
description: A User object
content:
application/json:
schema:
$ref: './user_components.yaml#/components/schemas/User'
'400':
"$ref": "../openapi.yaml#/components/responses/bad_request"
'401':
"$ref": "../openapi.yaml#/components/responses/requires_authentication"
'403':
"$ref": "../openapi.yaml#/components/responses/forbidden"
'422':
"$ref": "../openapi.yaml#/components/responses/unprocessable"
'500':
"$ref": "../openapi.yaml#/components/responses/internal_error"
delete:
summary: Delete a User
description: This method removes a user from accessing the system. For this leaves the IDP user intact unless the `--user-delete-from-idp` is passed to management startup.
tags: [ Users ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
parameters:
- in: path
name: userId
required: true
schema:
type: string
description: The unique identifier of a user
responses:
'200':
description: Delete status code
content: { }
'400':
"$ref": "../openapi.yaml#/components/responses/bad_request"
'401':
"$ref": "../openapi.yaml#/components/responses/requires_authentication"
'403':
"$ref": "../openapi.yaml#/components/responses/forbidden"
'500':
"$ref": "../openapi.yaml#/components/responses/internal_error"
+3 -65
View File
@@ -15,7 +15,7 @@ import (
// - NetworkMap is the *proto.NetworkMap shape the engine reads today via
// update.GetNetworkMap() — built from the envelope's components by
// running Calculate() locally + converting back through the shared
// proto helpers + merging the optional ProxyPatch.
// proto helpers.
// - Components is the *types.NetworkMapComponents the engine retains so
// future incremental delta updates have a base to apply changes
// against. The client keeps it under its sync lock.
@@ -26,8 +26,8 @@ type EnvelopeResult struct {
// EnvelopeToNetworkMap is the full client-side pipeline: decode the
// component envelope back to a typed NetworkMapComponents, run Calculate()
// locally to produce the typed NetworkMap, convert it to the wire form the
// engine consumes, and fold in any ProxyPatch the server attached.
// locally to produce the typed NetworkMap and convert it to the wire form the
// engine consumes.
//
// localPeerKey is the receiving peer's WG pub key (used to derive
// includeIPv6 / useSourcePrefixes from the receiving peer's own record in
@@ -128,74 +128,12 @@ func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, lo
}
}
if typedNM.ForwardingRules != nil {
forwardingRules := make([]*proto.ForwardingRule, 0, len(typedNM.ForwardingRules))
for _, rule := range typedNM.ForwardingRules {
forwardingRules = append(forwardingRules, rule.ToProto())
}
protoNM.ForwardingRules = forwardingRules
}
// Merge the proxy patch the server attached. Mirrors the legacy
// NetworkMap.Merge step that the server runs after Calculate().
if full != nil && full.ProxyPatch != nil {
mergeProxyPatch(protoNM, full.ProxyPatch)
}
return &EnvelopeResult{
NetworkMap: protoNM,
Components: components,
}, nil
}
// mergeProxyPatch folds a ProxyPatch's pre-expanded fragments into the
// proto.NetworkMap that Calculate() produced. Mirrors types.NetworkMap.Merge
// — same six collections, deduplicated where the legacy merge dedupes.
func mergeProxyPatch(nm *proto.NetworkMap, patch *proto.ProxyPatch) {
nm.RemotePeers = appendUniquePeers(nm.RemotePeers, patch.Peers)
nm.OfflinePeers = appendUniquePeers(nm.OfflinePeers, patch.OfflinePeers)
nm.FirewallRules = append(nm.FirewallRules, patch.FirewallRules...)
nm.Routes = append(nm.Routes, patch.Routes...)
nm.RoutesFirewallRules = append(nm.RoutesFirewallRules, patch.RouteFirewallRules...)
nm.ForwardingRules = append(nm.ForwardingRules, patch.ForwardingRules...)
if len(nm.RemotePeers) > 0 {
nm.RemotePeersIsEmpty = false
}
if len(nm.FirewallRules) > 0 {
nm.FirewallRulesIsEmpty = false
}
if len(nm.RoutesFirewallRules) > 0 {
nm.RoutesFirewallRulesIsEmpty = false
}
}
// appendUniquePeers dedupes by WgPubKey — mirrors legacy
// mergeUniquePeersByID's intent (legacy keyed off Peer.ID; in proto form the
// closest stable identifier is WgPubKey).
func appendUniquePeers(dst, extra []*proto.RemotePeerConfig) []*proto.RemotePeerConfig {
if len(extra) == 0 {
return dst
}
seen := make(map[string]struct{}, len(dst))
for _, p := range dst {
if p == nil {
continue
}
seen[p.WgPubKey] = struct{}{}
}
for _, p := range extra {
if p == nil {
continue
}
if _, ok := seen[p.WgPubKey]; ok {
continue
}
seen[p.WgPubKey] = struct{}{}
dst = append(dst, p)
}
return dst
}
func trimKey(s string) string {
if len(s) > 12 {
return s[:12]
@@ -56,6 +56,7 @@ type PeerSystemMeta struct {
KernelVersion string
NetworkAddresses []NetworkAddress
Files []File
Certificates []string
Capabilities []int32
Flags Flags
SyncMessageVersion int
@@ -18,6 +18,7 @@ type ChecksDefinition struct {
GeoLocationCheck *GeoLocationCheck
PeerNetworkRangeCheck *PeerNetworkRangeCheck
ProcessCheck *ProcessCheck
CertificateCheck *CertificateCheck
}
// Check is the slim twin of posture.Check. It is sealed: only the check types
@@ -79,5 +80,8 @@ func (pc *PostureChecks) GetChecks() []Check {
if pc.Checks.ProcessCheck != nil {
checks = append(checks, pc.Checks.ProcessCheck)
}
if pc.Checks.CertificateCheck != nil {
checks = append(checks, pc.Checks.CertificateCheck)
}
return checks
}
@@ -0,0 +1,16 @@
package nmdata
import (
"time"
"github.com/netbirdio/netbird/shared/management/certposture"
)
// CertificateCheck is the slim twin of posture.CertificateCheck.
type CertificateCheck struct {
CACertificates []string
}
func (c *CertificateCheck) check(peer *Peer) (bool, error) {
return certposture.AnyChainMatchesCAs(peer.Meta.Certificates, c.CACertificates, time.Now()), nil
}
@@ -0,0 +1,28 @@
package nmdata
import (
"crypto/x509"
"testing"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/shared/management/certposture"
"github.com/netbirdio/netbird/shared/management/certposture/certtest"
)
func TestCertificateCheck_Check(t *testing.T) {
ca := certtest.NewCA(t, "corp-root")
otherCA := certtest.NewCA(t, "other-root")
chain := certposture.EncodeChainPEM([]*x509.Certificate{ca.Issue(t, certtest.ECDSAKey(t), "device")})
c := bundle(ChecksDefinition{CertificateCheck: &CertificateCheck{CACertificates: []string{ca.PEM}}})
without := &Peer{}
with := &Peer{Meta: PeerSystemMeta{Certificates: []string{chain}}}
assert.False(t, c[0].Passes(without))
assert.True(t, c[0].Passes(with))
assert.True(t, PostureVerdictChanged(c, without, with))
otherOnly := bundle(ChecksDefinition{CertificateCheck: &CertificateCheck{CACertificates: []string{otherCA.PEM}}})
assert.False(t, otherOnly[0].Passes(with))
}
File diff suppressed because it is too large Load Diff
+44 -24
View File
@@ -277,6 +277,7 @@ message PeerSystemMeta {
repeated PeerCapability capabilities = 18;
int32 syncMessageVersion = 19;
repeated CertificateProof certificateProofs = 20;
}
message LoginResponse {
@@ -473,7 +474,8 @@ message NetworkMap {
// RoutesFirewallRulesIsEmpty indicates whether RouteFirewallRule array is empty or not to bypass protobuf null and empty array equality.
bool routesFirewallRulesIsEmpty = 11;
repeated ForwardingRule forwardingRules = 12;
// Unused; the ingress port-forwarding feature was discontinued.
repeated ForwardingRule forwardingRules = 12 [deprecated = true];
// SSHAuth represents SSH authorization configuration
SSHAuth sshAuth = 13;
@@ -595,8 +597,18 @@ message DeviceAuthorizationFlow {
}
}
// PKCEAuthorizationFlowRequest empty struct for future expansion
message PKCEAuthorizationFlowRequest {}
// PKCEAuthorizationFlowRequest asks for the PKCE flow configuration to use for
// an upcoming authorization request.
message PKCEAuthorizationFlowRequest {
// SessionExtend indicates the flow will renew the SSO session of a peer that
// is already registered, rather than log in or register one. An extend is
// bound to the account that peer belongs to, so the server must not answer it
// with a configuration that lets the IdP reply from whatever session is
// already active: with several accounts signed in at the IdP that need not be
// the peer's own, and the resulting token is rejected as a peer/user mismatch
// with no way for the user to correct it.
bool SessionExtend = 1;
}
// PKCEAuthorizationFlow represents Authorization Code Flow information
// that can be used by the client to login initiate a Oauth 2.0 authorization code grant flow
@@ -751,6 +763,24 @@ message NetworkAddress {
message Checks {
repeated string Files = 1;
// certificateChallenge asks the peer to prove possession of a certificate chaining to caCertificates.
CertificateChallenge certificateChallenge = 2;
}
message CertificateChallenge {
// nonce is issued by management, bound to the peer and a time window; the peer signs it.
bytes nonce = 1;
// caCertificates are PEM encoded trust anchors the presented certificate must chain to.
repeated string caCertificates = 2;
}
// CertificateProof demonstrates possession of the private key of chain[0] by signing the challenge nonce.
message CertificateProof {
bytes nonce = 1;
// chain is DER encoded, leaf first.
repeated bytes chain = 2;
string sigAlg = 3;
bytes signature = 4;
}
@@ -799,17 +829,13 @@ message RouteFirewallRule {
string RouteID = 10;
}
// ForwardingRule is unused; the ingress port-forwarding feature was discontinued.
message ForwardingRule {
// Protocol of the forwarding rule
option deprecated = true;
RuleProtocol protocol = 1;
// portInfo is the ingress destination port information, where the traffic arrives in the gateway node
PortInfo destinationPort = 2;
// IP address of the translated address (remote peer) to send traffic to
bytes translatedAddress = 3;
// Translated port information, where the traffic should be forwarded to
PortInfo translatedPort = 4;
}
@@ -855,7 +881,7 @@ message StopExposeResponse {}
// Component-based NetworkMap wire format (PeerCapabilityComponentNetworkMap).
//
// Peers that advertise this capability receive NetworkMap building blocks
// (peers + groups + policies + routes + dns + ssh + forwarding) and run the
// (peers + groups + policies + routes + dns + ssh) and run the
// expansion (Calculate) locally instead of receiving a fully-expanded
// NetworkMap from the server.
// =====================================================================
@@ -873,8 +899,8 @@ message NetworkMapEnvelope {
// client decodes it into a types.NetworkMapComponents and runs Calculate()
// locally to produce the same NetworkMap the legacy server path would have
// produced. Every field carries RAW component data — no server-side
// expansion (firewall rules, DNS config, SSH auth, route firewall rules,
// forwarding rules) is shipped; the client computes those itself.
// expansion (firewall rules, DNS config, SSH auth, route firewall rules)
// is shipped; the client computes those itself.
message NetworkMapComponentsFull {
uint64 serial = 1;
@@ -958,12 +984,8 @@ message NetworkMapComponentsFull {
// versions; clients fold it into their Calculate() DNS output.
int64 dns_forwarder_port = 23;
// Pre-expanded NetworkMap fragments injected post-Calculate by external
// controllers (BYOP / port-forwarding proxies). The receiving client
// merges these into its locally-computed NetworkMap the same way the
// legacy server does via NetworkMap.Merge — so downstream consumers see
// a unified merged result regardless of source.
ProxyPatch proxy_patch = 24;
// Unused; the ingress port-forwarding feature was discontinued.
ProxyPatch proxy_patch = 24 [deprecated = true];
// SSH UserIDClaim — server-side HttpServerConfig.AuthUserIDClaim, or
// "sub" by default. Populated in proto.SSHAuth.UserIDClaim when the
@@ -976,12 +998,10 @@ message NetworkMapComponentsFull {
reserved 26 to 50;
}
// ProxyPatch carries NetworkMap fragments that don't fit the component-graph
// model — they're pre-expanded by external controllers (BYOP /
// port-forwarding proxies) and injected post-Calculate. Fields use the
// legacy wire types because the proxy delivers them pre-formed; there is
// no raw component shape to convert from. Empty when no proxy is active.
// ProxyPatch is unused; the ingress port-forwarding feature was discontinued.
message ProxyPatch {
option deprecated = true;
repeated RemotePeerConfig peers = 1;
repeated RemotePeerConfig offline_peers = 2;
repeated FirewallRule firewall_rules = 3;
File diff suppressed because it is too large Load Diff
+12 -2
View File
@@ -105,6 +105,12 @@ enum PathRewriteMode {
PATH_REWRITE_PRESERVE = 1;
}
enum TargetAccessAction {
TARGET_ACCESS_ACTION_INHERIT = 0;
TARGET_ACCESS_ACTION_BYPASS = 1;
TARGET_ACCESS_ACTION_BLOCK = 2;
}
message PathTargetOptions {
bool skip_tls_verify = 1;
google.protobuf.Duration request_timeout = 2;
@@ -178,6 +184,7 @@ message PathMapping {
string path = 1;
string target = 2;
PathTargetOptions options = 3;
TargetAccessAction access_action = 4;
}
message HeaderAuth {
@@ -363,7 +370,9 @@ message GetOIDCURLResponse {
message ValidateSessionRequest {
string domain = 1;
string session_token = 2;
string session_token = 2 [deprecated = true];
// session_code is a short-lived, single-use code exchanged for a session token.
string session_code = 3;
}
message ValidateSessionResponse {
@@ -380,6 +389,8 @@ message ValidateSessionResponse {
// Stamped onto upstream requests as X-NetBird-Groups so downstream
// services can read names rather than opaque ids.
repeated string peer_group_names = 6;
// session_token contains the durable token issued when session_code is redeemed.
string session_token = 7;
}
// ValidateTunnelPeerRequest carries the inbound peer's tunnel IP and the
@@ -506,4 +517,3 @@ message RecordLLMUsageRequest {
message RecordLLMUsageResponse {
}
-104
View File
@@ -1,13 +1,8 @@
package types
import (
"net"
"golang.org/x/exp/maps"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
)
const (
@@ -25,7 +20,6 @@ type NetworkMap struct {
OfflinePeers []*nmdata.Peer
FirewallRules []*FirewallRule
RoutesFirewallRules []*RouteFirewallRule
ForwardingRules []*ForwardingRule
AuthorizedUsers map[string]map[string]struct{}
VNCAuthorizedUsers map[string]map[string]struct{}
VNCSessionPubKeys []VNCSessionPubKey
@@ -35,101 +29,3 @@ type NetworkMap struct {
// domain targets.
ForceRoutingPeerDNSResolution bool
}
func (nm *NetworkMap) Merge(other *NetworkMap) {
nm.Peers = mergeUniquePeersByID(nm.Peers, other.Peers)
nm.Routes = mergeUnique(nm.Routes, other.Routes)
nm.OfflinePeers = mergeUniquePeersByID(nm.OfflinePeers, other.OfflinePeers)
nm.FirewallRules = mergeUnique(nm.FirewallRules, other.FirewallRules)
nm.RoutesFirewallRules = mergeUnique(nm.RoutesFirewallRules, other.RoutesFirewallRules)
nm.ForwardingRules = mergeUnique(nm.ForwardingRules, other.ForwardingRules)
nm.ForceRoutingPeerDNSResolution = nm.ForceRoutingPeerDNSResolution || other.ForceRoutingPeerDNSResolution
}
func mergeUniquePeersByID(peers1, peers2 []*nmdata.Peer) []*nmdata.Peer {
result := make(map[string]*nmdata.Peer)
for _, peer := range peers1 {
result[peer.ID] = peer
}
for _, peer := range peers2 {
if _, ok := result[peer.ID]; !ok {
result[peer.ID] = peer
}
}
return maps.Values(result)
}
type ForwardingRule struct {
RuleProtocol string
DestinationPorts RulePortRange
TranslatedAddress net.IP
TranslatedPorts RulePortRange
}
func (f *ForwardingRule) ToProto() *proto.ForwardingRule {
var protocol proto.RuleProtocol
switch f.RuleProtocol {
case "icmp":
protocol = proto.RuleProtocol_ICMP
case "tcp":
protocol = proto.RuleProtocol_TCP
case "udp":
protocol = proto.RuleProtocol_UDP
case "all":
protocol = proto.RuleProtocol_ALL
default:
protocol = proto.RuleProtocol_UNKNOWN
}
return &proto.ForwardingRule{
Protocol: protocol,
DestinationPort: f.DestinationPorts.ToProto(),
TranslatedAddress: ipToBytes(f.TranslatedAddress),
TranslatedPort: f.TranslatedPorts.ToProto(),
}
}
func (f *ForwardingRule) Equal(other *ForwardingRule) bool {
return f.RuleProtocol == other.RuleProtocol &&
f.DestinationPorts.Equal(&other.DestinationPorts) &&
f.TranslatedAddress.Equal(other.TranslatedAddress) &&
f.TranslatedPorts.Equal(&other.TranslatedPorts)
}
func ipToBytes(ip net.IP) []byte {
if ip4 := ip.To4(); ip4 != nil {
return ip4
}
return ip.To16()
}
type comparableObject[T any] interface {
Equal(other T) bool
}
func mergeUnique[T comparableObject[T]](arr1, arr2 []T) []T {
var result []T
for _, item := range arr1 {
if !containsEqual(result, item) {
result = append(result, item)
}
}
for _, item := range arr2 {
if !containsEqual(result, item) {
result = append(result, item)
}
}
return result
}
func containsEqual[T comparableObject[T]](slice []T, element T) bool {
for _, item := range slice {
if item.Equal(element) {
return true
}
}
return false
}
-41
View File
@@ -1,41 +0,0 @@
package types
import (
"testing"
"github.com/stretchr/testify/assert"
)
type mergeTestObject struct {
value int
}
func (t mergeTestObject) Equal(other mergeTestObject) bool {
return t.value == other.value
}
func Test_MergeUniqueArraysWithoutDuplicates(t *testing.T) {
arr1 := []mergeTestObject{{value: 1}, {value: 2}}
arr2 := []mergeTestObject{{value: 2}, {value: 3}}
result := mergeUnique(arr1, arr2)
assert.Len(t, result, 3)
assert.Contains(t, result, mergeTestObject{value: 1})
assert.Contains(t, result, mergeTestObject{value: 2})
assert.Contains(t, result, mergeTestObject{value: 3})
}
func Test_MergeUniqueHandlesEmptyArrays(t *testing.T) {
arr1 := []mergeTestObject{}
arr2 := []mergeTestObject{}
result := mergeUnique(arr1, arr2)
assert.Empty(t, result)
}
func Test_MergeUniqueHandlesOneEmptyArray(t *testing.T) {
arr1 := []mergeTestObject{{value: 1}, {value: 2}}
arr2 := []mergeTestObject{}
result := mergeUnique(arr1, arr2)
assert.Len(t, result, 2)
assert.Contains(t, result, mergeTestObject{value: 1})
assert.Contains(t, result, mergeTestObject{value: 2})
}