From 3073d18039a040800b690bd41f4a05ddcce35282 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sun, 20 Sep 2026 20:24:01 +0200 Subject: [PATCH 1/9] [proxy] Close the client connection on private service denials (#7590) A client that hits a private service before its peer joins the overlay gets a 403 from the tunnel-peer check. After it connects to NetBird, the browser reuses the warm socket to the public listener, so the request never traverses the tunnel and keeps failing until the 120s idle timeout closes it. Private service denials now set Connection: close and Cache-Control: no-store before the 403, both at the tunnel-peer check and at IP restriction denials on a private domain. Go's HTTP/1.1 server closes after the response; its HTTP/2 server turns the exact lowercase close token into a GOAWAY, which retires the stale connection for h2 clients. Public services and allowed private traffic keep their keep-alive behaviour. Tests cover HTTP/1.1 and HTTP/2 denials over a real listener (retry lands on a new connection), public denials and allowed private requests (connection reused), and both IP restriction paths. --- proxy/internal/auth/middleware.go | 26 ++- proxy/internal/auth/private_deny_test.go | 272 +++++++++++++++++++++++ 2 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 proxy/internal/auth/private_deny_test.go diff --git a/proxy/internal/auth/middleware.go b/proxy/internal/auth/middleware.go index 1d46fd824..25ff68010 100644 --- a/proxy/internal/auth/middleware.go +++ b/proxy/internal/auth/middleware.go @@ -133,7 +133,7 @@ func (mw *Middleware) Protect(next http.Handler) http.Handler { if mw.forwardWithTunnelPeer(w, r, host, config, next) { return } - http.Error(w, "Forbidden", http.StatusForbidden) + denyPrivate(w) return } @@ -228,7 +228,7 @@ func (mw *Middleware) checkIPRestrictions(w http.ResponseWriter, r *http.Request clientIP := mw.resolveClientIP(r) if !clientIP.IsValid() { mw.logger.Debugf("IP restriction: cannot resolve client address for %q, denying", r.RemoteAddr) - http.Error(w, "Forbidden", http.StatusForbidden) + denyForbidden(w, config) return false } @@ -263,10 +263,30 @@ func (mw *Middleware) checkIPRestrictions(w http.ResponseWriter, r *http.Request reason := verdict.String() mw.blockIPRestriction(r, reason) - http.Error(w, "Forbidden", http.StatusForbidden) + denyForbidden(w, config) return false } +// denyForbidden writes a 403, dropping the client connection when the +// domain is private so a later retry cannot reuse it. +func denyForbidden(w http.ResponseWriter, config DomainConfig) { + if config.Private { + denyPrivate(w) + return + } + http.Error(w, "Forbidden", http.StatusForbidden) +} + +// denyPrivate writes a 403 and closes the connection, so a client refused +// before joining the overlay cannot keep retrying on the same warm socket. +// Go's HTTP/2 server turns the exact lowercase "close" token into a GOAWAY. +func denyPrivate(w http.ResponseWriter) { + h := w.Header() + h.Set("Connection", "close") + h.Set("Cache-Control", "no-store") + http.Error(w, "Forbidden", http.StatusForbidden) +} + // resolveClientIP extracts the real client IP from CapturedData, falling back to r.RemoteAddr. func (mw *Middleware) resolveClientIP(r *http.Request) netip.Addr { if cd := proxy.CapturedDataFromContext(r.Context()); cd != nil { diff --git a/proxy/internal/auth/private_deny_test.go b/proxy/internal/auth/private_deny_test.go new file mode 100644 index 000000000..5bec67168 --- /dev/null +++ b/proxy/internal/auth/private_deny_test.go @@ -0,0 +1,272 @@ +package auth + +import ( + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "net/http/httptrace" + "net/netip" + "sync" + "testing" + "time" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + + "github.com/netbirdio/netbird/proxy/internal/proxy" + "github.com/netbirdio/netbird/proxy/internal/restrict" + "github.com/netbirdio/netbird/shared/management/proto" +) + +// switchableTunnelValidator flips the ValidateTunnelPeer verdict between requests. +type switchableTunnelValidator struct { + mu sync.Mutex + valid bool +} + +func (s *switchableTunnelValidator) setValid(v bool) { + s.mu.Lock() + defer s.mu.Unlock() + s.valid = v +} + +func (s *switchableTunnelValidator) ValidateSession(context.Context, *proto.ValidateSessionRequest, ...grpc.CallOption) (*proto.ValidateSessionResponse, error) { + return nil, errors.New("not used in this test") +} + +func (s *switchableTunnelValidator) ValidateTunnelPeer(context.Context, *proto.ValidateTunnelPeerRequest, ...grpc.CallOption) (*proto.ValidateTunnelPeerResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + if !s.valid { + return &proto.ValidateTunnelPeerResponse{Valid: false, DeniedReason: "not_in_group"}, nil + } + return &proto.ValidateTunnelPeerResponse{ + Valid: true, + UserId: "user-1", + SessionToken: "tunnel-session-token", + }, nil +} + +// testServerHost is the domain key Protect derives from the httptest listener. +const testServerHost = "127.0.0.1" + +var testTunnelIP = netip.MustParseAddr("100.90.1.14") + +// startProtectedServer serves mw.Protect and stamps requests as overlay traffic. +func startProtectedServer(t *testing.T, mw *Middleware, clientIP netip.Addr, lookup TunnelLookupFunc, h2 bool) *httptest.Server { + t.Helper() + protected := mw.Protect(newPassthroughHandler()) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cd := proxy.NewCapturedData("") + cd.SetClientIP(clientIP) + ctx := proxy.WithCapturedData(r.Context(), cd) + ctx = WithTunnelLookup(ctx, lookup) + protected.ServeHTTP(w, r.WithContext(ctx)) + }) + + srv := httptest.NewUnstartedServer(handler) + if h2 { + srv.EnableHTTP2 = true + srv.StartTLS() + } else { + srv.Start() + } + t.Cleanup(srv.Close) + return srv +} + +// tracedResponse is what a test observes from one client round trip. +type tracedResponse struct { + status int + protoMajor int + close bool + connection string + cacheControl string + reused bool +} + +// doTraced GETs url and reports whether the connection that served it was reused. +func doTraced(t *testing.T, client *http.Client, url string) tracedResponse { + t.Helper() + var reused bool + trace := &httptrace.ClientTrace{ + GotConn: func(info httptrace.GotConnInfo) { reused = info.Reused }, + } + req, err := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), http.MethodGet, url, nil) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + defer func() { require.NoError(t, resp.Body.Close()) }() + _, err = io.Copy(io.Discard, resp.Body) + require.NoError(t, err) + return tracedResponse{ + status: resp.StatusCode, + protoMajor: resp.ProtoMajor, + close: resp.Close, + connection: resp.Header.Get("Connection"), + cacheControl: resp.Header.Get("Cache-Control"), + reused: reused, + } +} + +func acceptAllLookup(_ netip.Addr) (PeerIdentity, bool) { + return PeerIdentity{TunnelIP: testTunnelIP}, true +} + +func newPrivateMiddleware(t *testing.T, validator SessionValidator, ipRestrictions *restrict.Filter) *Middleware { + t.Helper() + mw := NewMiddleware(log.StandardLogger(), validator, nil) + kp := generateTestKeyPair(t) + require.NoError(t, mw.AddDomain(testServerHost, nil, kp.PublicKey, time.Hour, "acct-1", "svc-1", ipRestrictions, true, nil)) + return mw +} + +// A rejected tunnel peer must emit the exact lowercase "close" token h2 matches on. +func TestProtect_PrivateService_DeniedSetsCloseHeaders(t *testing.T) { + mw := newPrivateMiddleware(t, &switchableTunnelValidator{}, nil) + handler := mw.Protect(newPassthroughHandler()) + + cd := proxy.NewCapturedData("") + cd.SetClientIP(testTunnelIP) + req := httptest.NewRequest(http.MethodGet, "http://"+testServerHost+"/", nil) + req.RemoteAddr = testTunnelIP.String() + ":5000" + req = req.WithContext(WithTunnelLookup(proxy.WithCapturedData(req.Context(), cd), acceptAllLookup)) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusForbidden, rec.Code) + assert.Equal(t, "close", rec.Header().Get("Connection"), "private denial must ask the client to drop the connection") + assert.Equal(t, "no-store", rec.Header().Get("Cache-Control"), "private denial must not be cacheable") +} + +// A denied client must not keep reusing the warm socket after joining the overlay. +func TestPrivateDeny_HTTP1_ClosesConnection(t *testing.T) { + validator := &switchableTunnelValidator{} + mw := newPrivateMiddleware(t, validator, nil) + srv := startProtectedServer(t, mw, testTunnelIP, acceptAllLookup, false) + client := srv.Client() + + resp := doTraced(t, client, srv.URL) + assert.Equal(t, http.StatusForbidden, resp.status) + assert.Equal(t, 1, resp.protoMajor, "plain httptest server must speak HTTP/1.1") + // The Go client folds "Connection: close" into resp.close and drops the header. + assert.True(t, resp.close, "private denial must make the client mark the connection as not reusable") + assert.Equal(t, "no-store", resp.cacheControl, "private denial must not be cacheable") + + validator.setValid(true) + resp2 := doTraced(t, client, srv.URL) + assert.Equal(t, http.StatusOK, resp2.status, "the retry must reach the upstream once the peer is valid") + assert.False(t, resp2.reused, "the retry must open a new connection") +} + +// On HTTP/2 the header becomes a GOAWAY and the retry must use a new connection. +func TestPrivateDeny_HTTP2_SendsGoAway(t *testing.T) { + validator := &switchableTunnelValidator{} + mw := newPrivateMiddleware(t, validator, nil) + srv := startProtectedServer(t, mw, testTunnelIP, acceptAllLookup, true) + client := srv.Client() + + resp := doTraced(t, client, srv.URL) + require.Equal(t, 2, resp.protoMajor, "test client must negotiate HTTP/2") + assert.Equal(t, http.StatusForbidden, resp.status) + assert.Empty(t, resp.connection, "HTTP/2 must not carry a Connection header on the wire") + assert.Equal(t, "no-store", resp.cacheControl) + + validator.setValid(true) + resp2 := doTraced(t, client, srv.URL) + assert.Equal(t, 2, resp2.protoMajor) + assert.Equal(t, http.StatusOK, resp2.status, "the retry must reach the upstream once the peer is valid") + assert.False(t, resp2.reused, "GOAWAY must retire the connection so the retry opens a new one") +} + +// Legitimate private traffic keeps its keep-alive connection. +func TestPrivateAllow_KeepsConnection(t *testing.T) { + validator := &switchableTunnelValidator{valid: true} + mw := newPrivateMiddleware(t, validator, nil) + srv := startProtectedServer(t, mw, testTunnelIP, acceptAllLookup, false) + client := srv.Client() + + resp := doTraced(t, client, srv.URL) + assert.Equal(t, http.StatusOK, resp.status) + assert.Empty(t, resp.connection, "an allowed private request must not close the connection") + + resp2 := doTraced(t, client, srv.URL) + assert.Equal(t, http.StatusOK, resp2.status) + assert.True(t, resp2.reused, "allowed private traffic must keep reusing the connection") +} + +// Public denials keep the connection open; only private services change. +func TestPublicDeny_KeepsConnection(t *testing.T) { + mw := NewMiddleware(log.StandardLogger(), nil, nil) + filter := restrict.ParseFilter(restrict.FilterConfig{AllowedCIDRs: []string{"10.0.0.0/8"}}) + require.NoError(t, mw.AddDomain(testServerHost, nil, "", 0, "acct-1", "svc-1", filter, false, nil)) + srv := startProtectedServer(t, mw, netip.MustParseAddr("192.168.1.1"), nil, false) + client := srv.Client() + + resp := doTraced(t, client, srv.URL) + assert.Equal(t, http.StatusForbidden, resp.status) + assert.Empty(t, resp.connection, "public denial must not close the connection") + assert.Empty(t, resp.cacheControl, "public denial must not gain cache headers") + + resp2 := doTraced(t, client, srv.URL) + assert.Equal(t, http.StatusForbidden, resp2.status) + assert.True(t, resp2.reused, "public denials must keep reusing the connection") +} + +// IP restriction denials on a private service must close the connection too. +func TestCheckIPRestrictions_PrivateDenialClosesConnection(t *testing.T) { + filter := restrict.ParseFilter(restrict.FilterConfig{AllowedCIDRs: []string{"100.64.0.0/16"}}) + mw := newPrivateMiddleware(t, &switchableTunnelValidator{valid: true}, filter) + handler := mw.Protect(newPassthroughHandler()) + + tests := []struct { + name string + remoteAddr string + }{ + {"denied by CIDR", "100.65.5.6:5000"}, + {"unresolvable client address", "not-an-ip:1234"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "http://"+testServerHost+"/", nil) + req.RemoteAddr = tt.remoteAddr + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusForbidden, rec.Code) + assert.Equal(t, "close", rec.Header().Get("Connection"), "private IP-restriction denial must close the connection") + assert.Equal(t, "no-store", rec.Header().Get("Cache-Control"), "private IP-restriction denial must not be cacheable") + }) + } +} + +func TestCheckIPRestrictions_PublicDenialKeepsConnection(t *testing.T) { + mw := NewMiddleware(log.StandardLogger(), nil, nil) + filter := restrict.ParseFilter(restrict.FilterConfig{AllowedCIDRs: []string{"10.0.0.0/8"}}) + require.NoError(t, mw.AddDomain(testServerHost, nil, "", 0, "acct-1", "svc-1", filter, false, nil)) + handler := mw.Protect(newPassthroughHandler()) + + tests := []struct { + name string + remoteAddr string + }{ + {"denied by CIDR", "192.168.1.1:5000"}, + {"unresolvable client address", "not-an-ip:1234"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "http://"+testServerHost+"/", nil) + req.RemoteAddr = tt.remoteAddr + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusForbidden, rec.Code) + assert.Empty(t, rec.Header().Get("Connection"), "public IP-restriction denial must not close the connection") + assert.Empty(t, rec.Header().Get("Cache-Control"), "public IP-restriction denial must not gain cache headers") + }) + } +} From 314d88252d7d0f2549714b3cc25640bd612b7a88 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Mon, 21 Sep 2026 10:34:04 +0200 Subject: [PATCH 2/9] [management] Name the account owner in the pending approval error (#7533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Name the account owner in the pending approval error A user refused because their account is pending approval had no way to learn who could approve them. The refusal now carries the account owner's address, masked, so a caller can name someone to contact without being handed the address itself. Resolving the owner is best effort: a lookup failure, or an account predating the stored email, falls back to the refusal as it was. * Name only the caller's own owner in the pending approval error The refusal is raised before ValidateAccountAccess has established that the caller belongs to the account the request asked about, and the user is loaded by ID alone. Resolving the owner of the requested account therefore disclosed that owner's address to a pending user with no claim to it, reachable through any handler that takes an account ID from the caller — DELETE /api/accounts/{accountId} passes one straight through. The owner who can approve a pending user is the owner of their own account, so resolve that one. The requested account is never read. * Mask short local parts whole in MaskedEmail Keeping the first two characters and the last hides nothing until the local part is four long: at three or fewer they are the whole of it, so "abc@example.com" masked to "ab****c@example.com" and a pending user could recover the owner's address in full from what is meant to conceal it. Short local parts are now replaced entirely. * Name the owner from GetCurrentUserInfo instead of the permission gate The gate could only read the stored user row, which carries no address when an external IdP owns the identities — the usual case — so it named no one in practice. It also had no way to reach the IdP without being handed the account manager, which meant restoring bootstrap wiring that a refactor had dropped. GetCurrentUserInfo already holds that account manager, so it answers for a pending user itself and reuses GetOwnerInfo, the same lookup /msp uses to resolve an owner's address. The gate returns to exactly what it was, and with it goes the risk of naming the owner of an account the caller only asked about. MaskedEmail becomes MaskEmail: with a UserInfo in hand there is no stored row to hang it off. * [management] Cover the pending approval refusal in GetCurrentUserInfo The branch that names the owner had no coverage at the manager level, so neither the named refusal nor the fallback for an owner without a resolvable address was pinned down. * [management] Cover the failed owner lookup in the pending approval refusal The generic fallback has two ways in: no address on the resolved owner, and no owner to resolve at all. Only the first was pinned down. * [management] Pin the owner lookup to the caller's own account A mismatched account claim must not steer which owner the refusal names, and a blocked user is still answered before the claim is validated. Both are load bearing and neither was covered. --- management/server/types/user.go | 19 ++++ management/server/types/user_test.go | 141 +++++++++++++++++++++++++++ management/server/user.go | 27 +++++ management/server/user_test.go | 64 ++++++++++++ shared/management/status/error.go | 5 + 5 files changed, 256 insertions(+) diff --git a/management/server/types/user.go b/management/server/types/user.go index 02358ebc2..bebc20ea4 100644 --- a/management/server/types/user.go +++ b/management/server/types/user.go @@ -285,6 +285,25 @@ func (u *User) EncryptSensitiveData(enc *crypt.FieldEncrypt) error { return nil } +func MaskEmail(email string) string { + local, domain, found := strings.Cut(email, "@") + if !found || local == "" || domain == "" { + return "" + } + + // Runes, not bytes, so a non-ASCII local part is not cut mid-character. + runes := []rune(local) + + // Keeping the first two and the last needs a local part of at least four to + // hide anything at all: at three or fewer those are the whole of it, and the + // address would be recoverable in full from what is meant to conceal it. + if len(runes) < 4 { + return "****@" + domain + } + + return string(runes[:2]) + "****" + string(runes[len(runes)-1]) + "@" + domain +} + // DecryptSensitiveData decrypts the user's sensitive fields (Email and Name) in place. func (u *User) DecryptSensitiveData(enc *crypt.FieldEncrypt) error { if enc == nil { diff --git a/management/server/types/user_test.go b/management/server/types/user_test.go index e11df96aa..1b3ce7ce6 100644 --- a/management/server/types/user_test.go +++ b/management/server/types/user_test.go @@ -296,3 +296,144 @@ func TestUser_EncryptDecryptRoundTrip(t *testing.T) { }) } } + +func TestMaskEmail(t *testing.T) { + testCases := []struct { + name string + email string + expected string + }{ + { + name: "ordinary address keeps the first two, the last, and the domain", + email: "admin@example.com", + expected: "ad****n@example.com", + }, + { + name: "four characters is the shortest local part that reveals anything", + email: "abcd@example.com", + expected: "ab****d@example.com", + }, + { + name: "three character local part is masked whole, since a lead and tail would be all of it", + email: "abc@example.com", + expected: "****@example.com", + }, + { + name: "two character local part is masked whole", + email: "ab@example.com", + expected: "****@example.com", + }, + { + name: "single character local part is masked whole", + email: "a@b.co", + expected: "****@b.co", + }, + { + name: "mask width does not report the length it stands in for", + email: "a.very.long.local.part@example.com", + expected: "a.****t@example.com", + }, + { + name: "a local part far longer than the mask is still reduced to three characters", + email: "finance.department.notifications.owner.account@example.com", + expected: "fi****t@example.com", + }, + { + name: "plus addressing is masked along with the rest of the local part", + email: "admin+netbird@example.com", + expected: "ad****d@example.com", + }, + { + name: "separators inside the local part are not treated specially", + email: "first.last-name_x@example.com", + expected: "fi****x@example.com", + }, + { + name: "case is preserved rather than normalised", + email: "Admin@Example.COM", + expected: "Ad****n@Example.COM", + }, + { + name: "subdomains stay intact", + email: "owner@mail.corp.example.com", + expected: "ow****r@mail.corp.example.com", + }, + { + name: "german umlauts count as single characters", + email: "müller@example.de", + expected: "mü****r@example.de", + }, + { + name: "cyrillic local part is cut on runes", + email: "иванов@example.ru", + expected: "ив****в@example.ru", + }, + { + name: "cjk local part of three runes is masked whole, counted in runes not bytes", + email: "用户名@example.cn", + expected: "****@example.cn", + }, + { + name: "cjk local part of four runes reveals the first two and the last", + email: "用户名字@example.cn", + expected: "用户****字@example.cn", + }, + { + name: "arabic local part is cut on runes", + email: "مستخدم@example.sa", + expected: "مس****م@example.sa", + }, + { + name: "two rune non-ascii local part is masked whole", + email: "ää@example.de", + expected: "****@example.de", + }, + { + name: "astral plane runes are not split into surrogates", + email: "a🎉bc@example.com", + expected: "a🎉****c@example.com", + }, + { + name: "a non-ascii domain is left alone", + email: "admin@münchen.example", + expected: "ad****n@münchen.example", + }, + { + name: "only the first separator splits, so a second stays in the domain", + email: "a@b@example.com", + expected: "****@b@example.com", + }, + { + name: "empty email has nothing to mask", + email: "", + expected: "", + }, + { + name: "value without a separator is not an address", + email: "not-an-email", + expected: "", + }, + { + name: "missing local part is not an address", + email: "@example.com", + expected: "", + }, + { + name: "missing domain is not an address", + email: "admin@", + expected: "", + }, + { + name: "a bare separator is not an address", + email: "@", + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, MaskEmail(tc.email)) + }) + } + +} diff --git a/management/server/user.go b/management/server/user.go index 823c1b2e4..3510a624b 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -1448,6 +1448,25 @@ func (am *DefaultAccountManager) deleteRegularUser(ctx context.Context, accountI return updateAccountPeers, nil } +// pendingApprovalError refuses a user awaiting approval, naming the owner who +// can approve them when their address resolves. Failing to resolve one is not a +// reason to withhold the refusal, so the lookup is best effort. +func (am *DefaultAccountManager) pendingApprovalError(ctx context.Context, accountID string) error { + owner, err := am.GetOwnerInfo(ctx, accountID) + if err != nil { + log.WithContext(ctx).Debugf("pending approval refusal: owner of account %s did not resolve: %v", accountID, err) + return status.NewUserPendingApprovalError() + } + + masked := types.MaskEmail(owner.Email) + if masked == "" { + log.WithContext(ctx).Debugf("pending approval refusal: no address found for the owner of account %s", accountID) + return status.NewUserPendingApprovalError() + } + + return status.NewUserPendingApprovalByOwnerError(masked) +} + // GetOwnerInfo retrieves the owner information for a given account ID. func (am *DefaultAccountManager) GetOwnerInfo(ctx context.Context, accountID string) (*types.UserInfo, error) { owner, err := am.Store.GetAccountOwner(ctx, store.LockingStrengthNone, accountID) @@ -1505,6 +1524,14 @@ func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, userAut return nil, err } + // A user pending approval is blocked too, and the dashboard needs to tell + // the two apart: one is a dead end, the other resolves by itself once the + // owner acts. Naming that owner needs the address the IdP holds, which is + // why this is answered here rather than in the permission gate. + if user.IsBlocked() && user.PendingApproval { + return nil, am.pendingApprovalError(ctx, user.AccountID) + } + if user.IsBlocked() { return nil, status.NewUserBlockedError() } diff --git a/management/server/user_test.go b/management/server/user_test.go index ec0bbc54e..2d1a5f1e9 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -1779,6 +1779,42 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) { } require.NoError(t, store.SaveAccount(context.Background(), account2)) + account3 := newAccountWithId(context.Background(), "account3", "account3Owner", "", "owner@example.com", "", false) + account3.Users["pending-user"] = &types.User{ + Id: "pending-user", + AccountID: account3.Id, + Role: types.UserRoleUser, + Blocked: true, + PendingApproval: true, + } + require.NoError(t, store.SaveAccount(context.Background(), account3)) + + // The owner has no address to name, so the refusal falls back to the generic one. + account4 := newAccountWithId(context.Background(), "account4", "account4Owner", "", "", "", false) + account4.Users["pending-user-without-owner-email"] = &types.User{ + Id: "pending-user-without-owner-email", + AccountID: account4.Id, + Role: types.UserRoleUser, + Blocked: true, + PendingApproval: true, + } + require.NoError(t, store.SaveAccount(context.Background(), account4)) + + // No user holds the owner role, so the owner lookup itself fails. + account5 := newAccountWithId(context.Background(), "account5", "account5Admin", "", "", "", false) + account5.Users["account5Admin"].Role = types.UserRoleAdmin + account5.Users["pending-user-without-owner"] = &types.User{ + Id: "pending-user-without-owner", + AccountID: account5.Id, + Role: types.UserRoleUser, + Blocked: true, + PendingApproval: true, + } + require.NoError(t, store.SaveAccount(context.Background(), account5)) + + account6 := newAccountWithId(context.Background(), "account6", "account6Owner", "", "stranger@example.com", "", false) + require.NoError(t, store.SaveAccount(context.Background(), account6)) + permissionsManager := permissions.NewManager(store) am := DefaultAccountManager{ Store: store, @@ -1812,6 +1848,34 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) { userAuth: auth.UserAuth{AccountId: account1.Id, UserId: "service-user"}, expectedErr: status.NewPermissionDeniedError(), }, + { + name: "pending approval names the owner", + userAuth: auth.UserAuth{AccountId: account3.Id, UserId: "pending-user"}, + expectedErr: status.NewUserPendingApprovalByOwnerError("ow****r@example.com"), + }, + { + name: "pending approval without an owner address", + userAuth: auth.UserAuth{AccountId: account4.Id, UserId: "pending-user-without-owner-email"}, + expectedErr: status.NewUserPendingApprovalError(), + }, + { + name: "pending approval without an owner", + userAuth: auth.UserAuth{AccountId: account5.Id, UserId: "pending-user-without-owner"}, + expectedErr: status.NewUserPendingApprovalError(), + }, + { + // The account claim points at an account the caller is not in. The + // owner named has to be the one of the account holding the caller's + // own record, never the one the claim asks for. + name: "pending approval ignores a mismatched account claim", + userAuth: auth.UserAuth{AccountId: account6.Id, UserId: "pending-user"}, + expectedErr: status.NewUserPendingApprovalByOwnerError("ow****r@example.com"), + }, + { + name: "blocked user answers before the account claim is validated", + userAuth: auth.UserAuth{AccountId: account6.Id, UserId: "blocked-user"}, + expectedErr: status.NewUserBlockedError(), + }, { name: "owner user", userAuth: auth.UserAuth{AccountId: account1.Id, UserId: "account1Owner"}, diff --git a/shared/management/status/error.go b/shared/management/status/error.go index e31663450..4249f27de 100644 --- a/shared/management/status/error.go +++ b/shared/management/status/error.go @@ -135,6 +135,11 @@ func NewUserPendingApprovalError() error { return Errorf(PermissionDenied, "user is pending approval") } +// NewUserPendingApprovalByOwnerError creates a new Error with PermissionDenied type for a blocked user pending approval, naming the masked address of the owner who can approve them +func NewUserPendingApprovalByOwnerError(ownerEmail string) error { + return Errorf(PermissionDenied, "user is pending approval by owner %s", ownerEmail) +} + // NewPeerNotRegisteredError creates a new Error with Unauthenticated type unregistered peer func NewPeerNotRegisteredError() error { return Errorf(Unauthenticated, "peer is not registered") From 6c6298f2ab52e8ec7f831a00ad9de6a0e3a28014 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:55:20 +0200 Subject: [PATCH 3/9] [proxy] add proxy rate limiter (#7568) --- proxy/internal/auth/README.md | 26 +++ proxy/internal/auth/credential.go | 100 +++++++++ proxy/internal/auth/credential_limiter.go | 169 +++++++++++++++ .../internal/auth/credential_limiter_test.go | 190 +++++++++++++++++ proxy/internal/auth/credential_test.go | 196 ++++++++++++++++++ proxy/internal/auth/middleware.go | 14 +- proxy/internal/auth/password.go | 2 +- proxy/internal/auth/pin.go | 2 +- proxy/web/dist/assets/index.js | 12 +- proxy/web/src/App.tsx | 6 + 10 files changed, 701 insertions(+), 16 deletions(-) create mode 100644 proxy/internal/auth/README.md create mode 100644 proxy/internal/auth/credential.go create mode 100644 proxy/internal/auth/credential_limiter.go create mode 100644 proxy/internal/auth/credential_limiter_test.go create mode 100644 proxy/internal/auth/credential_test.go diff --git a/proxy/internal/auth/README.md b/proxy/internal/auth/README.md new file mode 100644 index 000000000..5ebf75cee --- /dev/null +++ b/proxy/internal/auth/README.md @@ -0,0 +1,26 @@ +# PIN and password authentication limits + +PIN and password credentials are accepted only in a POST form body. Query-string +credentials and credentials on other HTTP methods are ignored. + +The proxy permits a burst of five credential checks per account and service, +then replenishes one check every six seconds (ten per minute). PIN and password +checks share the same budget. Five failed checks from one client IP in a +rolling five-minute window block that source for fifteen minutes. In-flight checks +reserve failure slots; blocked requests do not extend the cooldown. Successful +authentication clears that source's failure history. Infrastructure failures +consume the service budget without counting as incorrect credentials. + +Throttled requests return HTTP 429 with a `Retry-After` delay in seconds. The +login page displays that delay. Existing authenticated sessions and other +authentication methods do not consume these credential budgets. + +The client IP comes from the existing trusted-proxy resolution. Deployments +behind a load balancer must configure trusted proxies correctly; otherwise +visitors share the load balancer's source budget. Visitors behind the same NAT +also share a source budget for a service. + +State is held in memory per proxy process and resets on restart. Multiple +replicas have independent budgets. State is bounded to 16,384 source entries and +4,096 service entries; when capacity is exhausted, new checks are denied until +idle entries expire. Active blocks are never evicted to admit a new source. diff --git a/proxy/internal/auth/credential.go b/proxy/internal/auth/credential.go new file mode 100644 index 000000000..0e93891fb --- /dev/null +++ b/proxy/internal/auth/credential.go @@ -0,0 +1,100 @@ +package auth + +import ( + "errors" + "math" + "net/http" + "strconv" + "time" + + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/proxy/auth" + "github.com/netbirdio/netbird/proxy/internal/proxy" +) + +var errCredentialClientIP = errors.New("invalid client address") + +type credentialLimitError struct { + retryAfter time.Duration +} + +func (e *credentialLimitError) Error() string { + return "too many authentication attempts" +} + +func credentialFormValue(r *http.Request, field string) string { + if r.Method != http.MethodPost { + return "" + } + return r.PostFormValue(field) +} + +func (mw *Middleware) authenticateScheme(r *http.Request, config DomainConfig, scheme Scheme) (string, string, error) { + method := scheme.Type() + if (method != auth.MethodPIN && method != auth.MethodPassword) || !wasCredentialSubmitted(r, method) { + return scheme.Authenticate(r) + } + ip := mw.resolveClientIP(r).Unmap() + if !ip.IsValid() { + return "", "", errCredentialClientIP + } + source, retry := mw.credentials.begin(credentialSourceKey{ + service: credentialServiceKey{accountID: config.AccountID, serviceID: config.ServiceID}, + ip: ip, + }) + if retry > 0 { + return "", "", &credentialLimitError{retryAfter: retry} + } + token, prompt, err := scheme.Authenticate(r) + outcome := credentialUnavailable + if err == nil { + outcome = credentialRejected + if token != "" { + outcome = credentialAccepted + } + } + mw.credentials.finish(source, outcome) + return token, prompt, err +} + +func credentialRetryAfter(err error) time.Duration { + var limitErr *credentialLimitError + if errors.As(err, &limitErr) { + return limitErr.retryAfter + } + s := status.Convert(err) + if s.Code() != codes.ResourceExhausted { + return 0 + } + for _, detail := range s.Details() { + if info, ok := detail.(*errdetails.RetryInfo); ok && info.RetryDelay != nil && info.RetryDelay.CheckValid() == nil { + if delay := info.RetryDelay.AsDuration(); delay > 0 { + return delay + } + } + } + return credentialCheckInterval +} + +func (mw *Middleware) writeAuthenticationError(w http.ResponseWriter, r *http.Request, method auth.Method, err error) { + if cd := proxy.CapturedDataFromContext(r.Context()); cd != nil { + cd.SetOrigin(proxy.OriginAuth) + cd.SetAuthMethod(method.String()) + } + if retry := credentialRetryAfter(err); retry > 0 { + // RFC 6585 section 4 forbids caching 429 responses. + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Retry-After", strconv.FormatInt(int64(math.Ceil(retry.Seconds())), 10)) + http.Error(w, "too many authentication attempts; try again later", http.StatusTooManyRequests) + return + } + if errors.Is(err, errCredentialClientIP) { + http.Error(w, "invalid client address", http.StatusBadRequest) + return + } + mw.logger.WithField("scheme", method.String()).Warnf("authentication infrastructure error: %v", err) + http.Error(w, "authentication service unavailable", http.StatusBadGateway) +} diff --git a/proxy/internal/auth/credential_limiter.go b/proxy/internal/auth/credential_limiter.go new file mode 100644 index 000000000..be9e70413 --- /dev/null +++ b/proxy/internal/auth/credential_limiter.go @@ -0,0 +1,169 @@ +package auth + +import ( + "net/netip" + "sync" + "time" + + "golang.org/x/time/rate" + + "github.com/netbirdio/netbird/proxy/internal/types" +) + +const ( + credentialFailureLimit = 5 + credentialFailureWindow = 5 * time.Minute + credentialBlockDuration = 15 * time.Minute + credentialCheckInterval = 6 * time.Second + credentialCheckBurst = 5 + credentialMaxSources = 16384 + credentialMaxServices = 4096 + credentialCleanupInterval = time.Minute +) + +type credentialServiceKey struct { + accountID types.AccountID + serviceID types.ServiceID +} + +type credentialSourceKey struct { + service credentialServiceKey + ip netip.Addr +} + +type credentialSource struct { + failures []time.Time + pending int + expiresAt time.Time + blockedUntil time.Time +} + +type credentialService struct { + limiter *rate.Limiter + lastUsed time.Time +} + +type credentialOutcome string + +const ( + credentialUnavailable credentialOutcome = "unavailable" + credentialRejected credentialOutcome = "rejected" + credentialAccepted credentialOutcome = "accepted" +) + +// State is local to this proxy process. Active blocks are never evicted to +// make room for a new source; exhausting capacity denies new checks. +type credentialLimiter struct { + mu sync.Mutex + now func() time.Time + sources map[credentialSourceKey]*credentialSource + services map[credentialServiceKey]*credentialService + nextCleanup time.Time +} + +func newCredentialLimiter() *credentialLimiter { + return &credentialLimiter{ + now: time.Now, + sources: make(map[credentialSourceKey]*credentialSource), + services: make(map[credentialServiceKey]*credentialService), + } +} + +func (l *credentialLimiter) begin(key credentialSourceKey) (*credentialSource, time.Duration) { + l.mu.Lock() + defer l.mu.Unlock() + now := l.now() + l.cleanup(now) + source := l.sources[key] + if source != nil { + if now.Before(source.blockedUntil) { + return nil, source.blockedUntil.Sub(now) + } + if source.pending == 0 && !now.Before(source.expiresAt) { + *source = credentialSource{} + } + source.expireFailures(now) + // Reserve the failure budget before verification so concurrent guesses + // cannot all pass a check against the same completed failure count. + if len(source.failures)+source.pending >= credentialFailureLimit { + return nil, time.Second + } + } else if len(l.sources) >= credentialMaxSources { + return nil, credentialCleanupInterval + } + if retry := l.allowService(key.service, now); retry > 0 { + return nil, retry + } + if source == nil { + source = &credentialSource{} + l.sources[key] = source + } + if source.expiresAt.IsZero() { + source.expiresAt = now.Add(credentialFailureWindow) + } + source.pending++ + return source, 0 +} + +func (l *credentialLimiter) allowService(key credentialServiceKey, now time.Time) time.Duration { + service := l.services[key] + if service == nil { + if len(l.services) >= credentialMaxServices { + return credentialCleanupInterval + } + service = &credentialService{limiter: rate.NewLimiter(rate.Every(credentialCheckInterval), credentialCheckBurst)} + l.services[key] = service + } + service.lastUsed = now + if service.limiter.AllowN(now, 1) { + return 0 + } + return max(time.Nanosecond, time.Duration((1-service.limiter.TokensAt(now))*float64(credentialCheckInterval))) +} + +func (l *credentialLimiter) finish(source *credentialSource, outcome credentialOutcome) { + l.mu.Lock() + defer l.mu.Unlock() + source.pending-- + now := l.now() + source.expireFailures(now) + switch outcome { + case credentialRejected: + source.failures = append(source.failures, now) + source.expiresAt = now.Add(credentialFailureWindow) + if len(source.failures) >= credentialFailureLimit && source.blockedUntil.IsZero() { + source.blockedUntil = now.Add(credentialBlockDuration) + source.expiresAt = source.blockedUntil + } + case credentialAccepted: + if !now.Before(source.blockedUntil) { + source.failures = nil + source.expiresAt = now.Add(credentialFailureWindow) + } + case credentialUnavailable: + // Transport failures consume the service budget, but are not bad guesses. + } +} + +func (s *credentialSource) expireFailures(now time.Time) { + for len(s.failures) > 0 && !now.Before(s.failures[0].Add(credentialFailureWindow)) { + s.failures = s.failures[1:] + } +} + +func (l *credentialLimiter) cleanup(now time.Time) { + if now.Before(l.nextCleanup) { + return + } + l.nextCleanup = now.Add(credentialCleanupInterval) + for key, source := range l.sources { + if source.pending == 0 && !now.Before(source.expiresAt) { + delete(l.sources, key) + } + } + for key, service := range l.services { + if now.Sub(service.lastUsed) >= credentialBlockDuration { + delete(l.services, key) + } + } +} diff --git a/proxy/internal/auth/credential_limiter_test.go b/proxy/internal/auth/credential_limiter_test.go new file mode 100644 index 000000000..dfa346baf --- /dev/null +++ b/proxy/internal/auth/credential_limiter_test.go @@ -0,0 +1,190 @@ +package auth + +import ( + "net/netip" + "strconv" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/proxy/internal/types" +) + +func TestCredentialLimiterCooldown(t *testing.T) { + l := newCredentialLimiter() + now := time.Now() + l.now = func() time.Time { return now } + key := credentialSourceKey{service: credentialServiceKey{"account", "service"}, ip: netip.MustParseAddr("192.0.2.1")} + for range credentialFailureLimit { + attempt, retry := l.begin(key) + require.Zero(t, retry, "initial guesses must reach verification") + l.finish(attempt, credentialRejected) + } + _, retry := l.begin(key) + assert.Equal(t, credentialBlockDuration, retry, "five failures must start a fifteen-minute block") + now = now.Add(credentialBlockDuration - time.Second) + _, retry = l.begin(key) + assert.Equal(t, time.Second, retry, "blocked requests must not extend the deadline") + now = now.Add(time.Second) + attempt, retry := l.begin(key) + require.Zero(t, retry, "the source must recover when its block expires") + l.finish(attempt, credentialAccepted) +} + +func TestCredentialLimiterFailureWindowAndSuccess(t *testing.T) { + for _, outcome := range []credentialOutcome{credentialAccepted, credentialUnavailable} { + t.Run(map[credentialOutcome]string{credentialAccepted: "success", credentialUnavailable: "infrastructure error"}[outcome], func(t *testing.T) { + l := newCredentialLimiter() + now := time.Now() + l.now = func() time.Time { return now } + key := credentialSourceKey{service: credentialServiceKey{"account", "service"}, ip: netip.MustParseAddr("192.0.2.1")} + for range 4 { + attempt, retry := l.begin(key) + require.Zero(t, retry, "four failures must fit the budget") + l.finish(attempt, credentialRejected) + } + attempt, retry := l.begin(key) + require.Zero(t, retry, "fifth check must be allowed") + l.finish(attempt, outcome) + now = now.Add(credentialCheckInterval) + attempt, retry = l.begin(key) + require.Zero(t, retry, "success or infrastructure error must not start a block") + l.finish(attempt, credentialRejected) + now = now.Add(credentialCheckInterval) + attempt, retry = l.begin(key) + if outcome == credentialUnavailable { + assert.Greater(t, retry, time.Duration(0), "infrastructure errors must preserve earlier failures") + return + } + require.Zero(t, retry, "success must clear earlier failures") + l.finish(attempt, credentialRejected) + now = now.Add(credentialFailureWindow) + for range credentialFailureLimit { + attempt, retry = l.begin(key) + require.Zero(t, retry, "old failures must expire") + l.finish(attempt, credentialRejected) + } + }) + } +} + +func TestCredentialLimiterRollingWindow(t *testing.T) { + l := newCredentialLimiter() + now := time.Now() + l.now = func() time.Time { return now } + key := credentialSourceKey{service: credentialServiceKey{"account", "service"}, ip: netip.MustParseAddr("192.0.2.1")} + attempt, retry := l.begin(key) + require.Zero(t, retry, "the first failure starts the history") + l.finish(attempt, credentialRejected) + now = now.Add(4 * time.Minute) + for range 3 { + attempt, retry = l.begin(key) + require.Zero(t, retry, "three more failures must fit the budget") + l.finish(attempt, credentialRejected) + } + now = now.Add(time.Minute + time.Second) + for range 2 { + attempt, retry = l.begin(key) + require.Zero(t, retry, "only the oldest failure must have expired") + l.finish(attempt, credentialRejected) + } + _, retry = l.begin(key) + assert.Equal(t, credentialBlockDuration, retry, "five recent failures must block even across the first window boundary") +} + +func TestCredentialLimiterServiceBudget(t *testing.T) { + l := newCredentialLimiter() + now := time.Now() + l.now = func() time.Time { return now } + key := credentialSourceKey{service: credentialServiceKey{"account", "service"}, ip: netip.MustParseAddr("192.0.2.1")} + for range credentialCheckBurst { + attempt, retry := l.begin(key) + require.Zero(t, retry, "initial checks must fit the service burst") + l.finish(attempt, credentialAccepted) + key.ip = key.ip.Next() + } + _, retry := l.begin(key) + assert.Equal(t, credentialCheckInterval, retry, "changing IP must not bypass the service budget") + other := key + other.service.accountID = "another-account" + attempt, retry := l.begin(other) + require.Zero(t, retry, "accounts must have separate budgets") + l.finish(attempt, credentialAccepted) + other = key + other.service.serviceID = "another-service" + attempt, retry = l.begin(other) + require.Zero(t, retry, "services must have separate budgets") + l.finish(attempt, credentialAccepted) + now = now.Add(credentialCheckInterval) + attempt, retry = l.begin(key) + require.Zero(t, retry, "one check must refill every six seconds") + l.finish(attempt, credentialAccepted) + _, retry = l.begin(key) + assert.Equal(t, credentialCheckInterval, retry, "refill must only grant one new check") +} + +func TestCredentialLimiterConcurrentReservations(t *testing.T) { + l := newCredentialLimiter() + now := time.Now() + l.now = func() time.Time { return now } + key := credentialSourceKey{service: credentialServiceKey{"account", "service"}, ip: netip.MustParseAddr("192.0.2.1")} + var attempts []*credentialSource + for range credentialFailureLimit { + attempt, retry := l.begin(key) + require.Zero(t, retry, "initial requests must reserve the failure budget") + attempts = append(attempts, attempt) + } + // Refill the service budget while earlier verification calls are still running. + now = now.Add(time.Minute) + var admitted atomic.Int32 + var wg sync.WaitGroup + for range 100 { + wg.Go(func() { + attempt, retry := l.begin(key) + if retry == 0 { + admitted.Add(1) + l.finish(attempt, credentialRejected) + } + }) + } + wg.Wait() + assert.Zero(t, admitted.Load(), "in-flight guesses must reserve the failure budget despite a refilled service budget") + for _, attempt := range attempts { + wg.Go(func() { l.finish(attempt, credentialRejected) }) + } + wg.Wait() + _, retry := l.begin(key) + assert.Equal(t, credentialBlockDuration, retry, "concurrent failures must activate the block") +} + +func TestCredentialLimiterCapacityAndCleanup(t *testing.T) { + for _, fullSources := range []bool{true, false} { + t.Run(map[bool]string{true: "sources", false: "services"}[fullSources], func(t *testing.T) { + l := newCredentialLimiter() + now := time.Now() + l.now = func() time.Time { return now } + key := credentialSourceKey{service: credentialServiceKey{"account", "service"}, ip: netip.MustParseAddr("192.0.2.1")} + if fullSources { + ip := netip.MustParseAddr("198.18.0.1") + for range credentialMaxSources { + l.sources[credentialSourceKey{service: key.service, ip: ip}] = &credentialSource{expiresAt: now.Add(credentialBlockDuration), blockedUntil: now.Add(credentialBlockDuration)} + ip = ip.Next() + } + } else { + for i := range credentialMaxServices { + l.services[credentialServiceKey{serviceID: key.service.serviceID, accountID: types.AccountID(strconv.Itoa(i))}] = &credentialService{lastUsed: now} + } + } + _, retry := l.begin(key) + assert.Positive(t, retry, "full state must deny new checks without evicting active entries") + now = now.Add(credentialBlockDuration) + attempt, retry := l.begin(key) + require.Zero(t, retry, "expired state must release capacity") + l.finish(attempt, credentialAccepted) + }) + } +} diff --git a/proxy/internal/auth/credential_test.go b/proxy/internal/auth/credential_test.go new file mode 100644 index 000000000..cae2cff69 --- /dev/null +++ b/proxy/internal/auth/credential_test.go @@ -0,0 +1,196 @@ +package auth + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/netip" + "net/url" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" + servicemanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service/manager" + "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/sessionkey" + nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc" + "github.com/netbirdio/netbird/management/server/store" + mgmttypes "github.com/netbirdio/netbird/management/server/types" + proxyauth "github.com/netbirdio/netbird/proxy/auth" + "github.com/netbirdio/netbird/proxy/internal/proxy" + "github.com/netbirdio/netbird/shared/management/proto" +) + +// localCredentialClient replaces the transport while keeping the real service +// store, credential verification, and session signing. +type localCredentialClient struct { + server *nbgrpc.ProxyServiceServer +} + +func (c localCredentialClient) Authenticate(ctx context.Context, req *proto.AuthenticateRequest, _ ...grpc.CallOption) (*proto.AuthenticateResponse, error) { + return c.server.Authenticate(ctx, req) +} + +func credentialHandler(t *testing.T, field string) (*Middleware, http.Handler) { + t.Helper() + ctx := context.Background() + s, err := store.NewStore(ctx, mgmttypes.SqliteStoreEngine, t.TempDir(), nil, false) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, s.Close(ctx)) }) + require.NoError(t, s.SaveAccount(ctx, &mgmttypes.Account{Id: "account"})) + keys := generateTestKeyPair(t) + svc := &service.Service{ + ID: "service", AccountID: "account", Name: "test", Domain: "example.com", + Enabled: true, SessionPrivateKey: keys.PrivateKey, SessionPublicKey: keys.PublicKey, + Auth: service.AuthConfig{ + PinAuth: &service.PINAuthConfig{Enabled: true, Pin: "842716"}, + PasswordAuth: &service.PasswordAuthConfig{Enabled: true, Password: "842716"}, + }, + } + require.NoError(t, svc.Auth.HashSecrets()) + require.NoError(t, s.CreateService(ctx, svc)) + server := nbgrpc.NewProxyServiceServer(nil, nil, nil, nbgrpc.ProxyOIDCConfig{}, nil, nil, nil, nil, nil) + t.Cleanup(server.Close) + server.SetServiceManager(servicemanager.NewManager(s, nil, nil, nil, nil, nil)) + client := localCredentialClient{server: server} + var scheme Scheme = NewPin(client, "service", "account") + if field == "password" { + scheme = NewPassword(client, "service", "account") + } + mw := NewMiddleware(nil, nil, nil) + require.NoError(t, mw.AddDomain("example.com", []Scheme{scheme}, keys.PublicKey, time.Hour, "account", "service", nil, false, nil)) + return mw, mw.Protect(newPassthroughHandler()) +} + +func credentialRequest(method, field, value string) *http.Request { + r := httptest.NewRequest(method, "https://example.com/", strings.NewReader(url.Values{field: {value}}.Encode())) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + r.RemoteAddr = "198.51.100.25:12345" + return r +} + +func TestCredentialAuthPOSTOnly(t *testing.T) { + for _, field := range []string{"pin", "password"} { + t.Run(field, func(t *testing.T) { + _, handler := credentialHandler(t, field) + for _, method := range []string{http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodPost} { + r := credentialRequest(method, field, "") + r.URL.RawQuery = url.Values{field: {"842716"}}.Encode() + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, r) + assert.Equal(t, http.StatusUnauthorized, resp.Code, "%s query credentials must not authenticate", method) + assert.Empty(t, resp.Result().Cookies(), "query credentials must not issue a session") + } + for _, method := range []string{http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodDelete} { + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, credentialRequest(method, field, "842716")) + assert.Equal(t, http.StatusUnauthorized, resp.Code, "%s body credentials must not authenticate", method) + } + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, credentialRequest(http.MethodPost, field, "842716")) + assert.Equal(t, http.StatusSeeOther, resp.Code, "POST body credentials must authenticate") + }) + } +} + +func TestCredentialAuthThrottling(t *testing.T) { + for _, field := range []string{"pin", "password"} { + t.Run(field, func(t *testing.T) { + _, handler := credentialHandler(t, field) + for range 5 { + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, credentialRequest(http.MethodPost, field, "000000")) + require.Equal(t, http.StatusUnauthorized, resp.Code, "initial wrong credentials must be rejected") + } + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, credentialRequest(http.MethodPost, field, "842716")) + assert.Equal(t, http.StatusTooManyRequests, resp.Code, "even correct credentials must wait for the block to expire") + assert.Equal(t, "900", resp.Header().Get("Retry-After"), "five failures must block the source for fifteen minutes") + assert.Empty(t, resp.Result().Cookies(), "blocked credentials must not issue a session") + }) + } +} + +func TestCredentialAuthSessionAndClientIP(t *testing.T) { + keys := generateTestKeyPair(t) + token, err := sessionkey.SignToken(keys.PrivateKey, "pin-user", "", "example.com", proxyauth.MethodPIN, nil, nil, time.Hour) + require.NoError(t, err) + mw := NewMiddleware(nil, nil, nil) + now := time.Now() + mw.credentials.now = func() time.Time { return now } + scheme := &stubScheme{method: proxyauth.MethodPIN, promptID: "pin"} + require.NoError(t, mw.AddDomain("example.com", []Scheme{scheme}, keys.PublicKey, 0, "account", "service", nil, false, nil)) + handler := mw.Protect(newPassthroughHandler()) + for range credentialFailureLimit { + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, credentialRequest(http.MethodPost, "pin", "000000")) + require.Equal(t, http.StatusUnauthorized, resp.Code, "bad PIN must consume the failure budget") + } + now = now.Add(credentialCheckInterval) + require.NoError(t, mw.AddDomain("example.com", []Scheme{scheme}, keys.PublicKey, 0, "account", "service", nil, false, nil)) + r := credentialRequest(http.MethodPost, "pin", "000000") + r.RemoteAddr = "[::ffff:198.51.100.25]:45678" + r.Header.Set("X-Forwarded-For", "192.0.2.5") + r.Header.Set("X-Real-IP", "192.0.2.6") + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, r) + assert.Equal(t, http.StatusTooManyRequests, resp.Code, "mapped addresses and untrusted forwarding headers must not bypass the source block") + assert.Equal(t, "no-store", resp.Header().Get("Cache-Control"), "rate limits must not be cached") + r.AddCookie(&http.Cookie{Name: proxyauth.SessionCookieName, Value: token}) + resp = httptest.NewRecorder() + handler.ServeHTTP(resp, r) + assert.Equal(t, http.StatusOK, resp.Code, "an existing session must pass even with credentials in the request") + assert.Equal(t, "backend", resp.Body.String(), "the authenticated request must reach the application") + r = credentialRequest(http.MethodPost, "pin", "000000") + cd := proxy.NewCapturedData("test") + cd.SetClientIP(netip.MustParseAddr("192.0.2.9")) + r = r.WithContext(proxy.WithCapturedData(r.Context(), cd)) + resp = httptest.NewRecorder() + handler.ServeHTTP(resp, r) + assert.Equal(t, http.StatusUnauthorized, resp.Code, "a client resolved by the trusted-proxy middleware must get its own source budget") + r = credentialRequest(http.MethodPost, "pin", "000000") + r.RemoteAddr = "invalid" + resp = httptest.NewRecorder() + handler.ServeHTTP(resp, r) + assert.Equal(t, http.StatusBadRequest, resp.Code, "an unresolvable client address must fail closed") + now = now.Add(credentialBlockDuration) + scheme.token = token + resp = httptest.NewRecorder() + handler.ServeHTTP(resp, credentialRequest(http.MethodPost, "pin", "842716")) + assert.Equal(t, http.StatusSeeOther, resp.Code, "credentials must work again after cooldown") +} + +func TestCredentialAuthManagementThrottling(t *testing.T) { + s, err := status.New(codes.ResourceExhausted, "rate limited").WithDetails(&errdetails.RetryInfo{RetryDelay: durationpb.New(2500 * time.Millisecond)}) + require.NoError(t, err) + for _, tc := range []struct { + name string + err error + code int + retry string + }{ + {"retry info", fmt.Errorf("authenticate PIN: %w", s.Err()), http.StatusTooManyRequests, "3"}, + {"missing retry info", status.Error(codes.ResourceExhausted, "rate limited"), http.StatusTooManyRequests, "6"}, + {"unavailable", status.Error(codes.Unavailable, "unavailable"), http.StatusBadGateway, ""}, + } { + t.Run(tc.name, func(t *testing.T) { + keys := generateTestKeyPair(t) + mw := NewMiddleware(nil, nil, nil) + scheme := &stubScheme{method: proxyauth.MethodPIN, authFn: func(*http.Request) (string, string, error) { return "", "", tc.err }} + require.NoError(t, mw.AddDomain("example.com", []Scheme{scheme}, keys.PublicKey, 0, "account", "service", nil, false, nil)) + resp := httptest.NewRecorder() + mw.Protect(newPassthroughHandler()).ServeHTTP(resp, credentialRequest(http.MethodPost, "pin", "000000")) + assert.Equal(t, tc.code, resp.Code, "management errors must keep their HTTP meaning") + assert.Equal(t, tc.retry, resp.Header().Get("Retry-After"), "retry hints must round up to whole seconds") + }) + } +} diff --git a/proxy/internal/auth/middleware.go b/proxy/internal/auth/middleware.go index 25ff68010..311dd2cbb 100644 --- a/proxy/internal/auth/middleware.go +++ b/proxy/internal/auth/middleware.go @@ -87,6 +87,7 @@ type Middleware struct { sessionValidator SessionValidator geo restrict.GeoResolver tunnelCache *tunnelValidationCache + credentials *credentialLimiter } // NewMiddleware creates a new authentication middleware. The sessionValidator is @@ -101,6 +102,7 @@ func NewMiddleware(logger *log.Logger, sessionValidator SessionValidator, geo re sessionValidator: sessionValidator, geo: geo, tunnelCache: newTunnelValidationCache(), + credentials: newCredentialLimiter(), } } @@ -543,13 +545,9 @@ func (mw *Middleware) authenticateWithSchemes(w http.ResponseWriter, r *http.Req var attemptedMethod string for _, scheme := range config.Schemes { - token, promptData, err := scheme.Authenticate(r) + token, promptData, err := mw.authenticateScheme(r, config, scheme) if err != nil { - mw.logger.WithField("scheme", scheme.Type().String()).Warnf("authentication infrastructure error: %v", err) - if cd := proxy.CapturedDataFromContext(r.Context()); cd != nil { - cd.SetOrigin(proxy.OriginAuth) - } - http.Error(w, "authentication service unavailable", http.StatusBadGateway) + mw.writeAuthenticationError(w, r, scheme.Type(), err) return } @@ -650,9 +648,9 @@ func setSessionCookie(w http.ResponseWriter, token string, expiration time.Durat func wasCredentialSubmitted(r *http.Request, method auth.Method) bool { switch method { case auth.MethodPIN: - return r.FormValue("pin") != "" + return credentialFormValue(r, pinFormId) != "" case auth.MethodPassword: - return r.FormValue("password") != "" + return credentialFormValue(r, passwordFormId) != "" case auth.MethodOIDC: return r.URL.Query().Get("session_token") != "" } diff --git a/proxy/internal/auth/password.go b/proxy/internal/auth/password.go index 6a7eda3e1..c43e8e3af 100644 --- a/proxy/internal/auth/password.go +++ b/proxy/internal/auth/password.go @@ -35,7 +35,7 @@ func (Password) Type() auth.Method { // so that it can be injected into a request from the UI so that // authentication may be successful. func (p Password) Authenticate(r *http.Request) (string, string, error) { - password := r.FormValue(passwordFormId) + password := credentialFormValue(r, passwordFormId) if password == "" { // No password submitted; return the form ID so the UI can prompt the user. diff --git a/proxy/internal/auth/pin.go b/proxy/internal/auth/pin.go index 4d08f3dc6..180f2648d 100644 --- a/proxy/internal/auth/pin.go +++ b/proxy/internal/auth/pin.go @@ -35,7 +35,7 @@ func (Pin) Type() auth.Method { // so that it can be injected into a request from the UI so that // authentication may be successful. func (p Pin) Authenticate(r *http.Request) (string, string, error) { - pin := r.FormValue(pinFormId) + pin := credentialFormValue(r, pinFormId) if pin == "" { // No PIN submitted; return the form ID so the UI can prompt the user. diff --git a/proxy/web/dist/assets/index.js b/proxy/web/dist/assets/index.js index 9ce3e4394..0a34a21d4 100644 --- a/proxy/web/dist/assets/index.js +++ b/proxy/web/dist/assets/index.js @@ -1,9 +1,9 @@ -(function(){const v=document.createElement("link").relList;if(v&&v.supports&&v.supports("modulepreload"))return;for(const _ of document.querySelectorAll('link[rel="modulepreload"]'))f(_);new MutationObserver(_=>{for(const O of _)if(O.type==="childList")for(const D of O.addedNodes)D.tagName==="LINK"&&D.rel==="modulepreload"&&f(D)}).observe(document,{childList:!0,subtree:!0});function S(_){const O={};return _.integrity&&(O.integrity=_.integrity),_.referrerPolicy&&(O.referrerPolicy=_.referrerPolicy),_.crossOrigin==="use-credentials"?O.credentials="include":_.crossOrigin==="anonymous"?O.credentials="omit":O.credentials="same-origin",O}function f(_){if(_.ep)return;_.ep=!0;const O=S(_);fetch(_.href,O)}})();var Sf={exports:{}},Du={};var Yd;function jm(){if(Yd)return Du;Yd=1;var r=Symbol.for("react.transitional.element"),v=Symbol.for("react.fragment");function S(f,_,O){var D=null;if(O!==void 0&&(D=""+O),_.key!==void 0&&(D=""+_.key),"key"in _){O={};for(var U in _)U!=="key"&&(O[U]=_[U])}else O=_;return _=O.ref,{$$typeof:r,type:f,key:D,ref:_!==void 0?_:null,props:O}}return Du.Fragment=v,Du.jsx=S,Du.jsxs=S,Du}var Gd;function Rm(){return Gd||(Gd=1,Sf.exports=jm()),Sf.exports}var A=Rm(),xf={exports:{}},K={};var Xd;function Hm(){if(Xd)return K;Xd=1;var r=Symbol.for("react.transitional.element"),v=Symbol.for("react.portal"),S=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),_=Symbol.for("react.profiler"),O=Symbol.for("react.consumer"),D=Symbol.for("react.context"),U=Symbol.for("react.forward_ref"),N=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),R=Symbol.for("react.lazy"),H=Symbol.for("react.activity"),V=Symbol.iterator;function st(s){return s===null||typeof s!="object"?null:(s=V&&s[V]||s["@@iterator"],typeof s=="function"?s:null)}var ct={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},G=Object.assign,Q={};function L(s,M,j){this.props=s,this.context=M,this.refs=Q,this.updater=j||ct}L.prototype.isReactComponent={},L.prototype.setState=function(s,M){if(typeof s!="object"&&typeof s!="function"&&s!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,s,M,"setState")},L.prototype.forceUpdate=function(s){this.updater.enqueueForceUpdate(this,s,"forceUpdate")};function gt(){}gt.prototype=L.prototype;function zt(s,M,j){this.props=s,this.context=M,this.refs=Q,this.updater=j||ct}var _t=zt.prototype=new gt;_t.constructor=zt,G(_t,L.prototype),_t.isPureReactComponent=!0;var it=Array.isArray;function Ot(){}var J={H:null,A:null,T:null,S:null},Rt=Object.prototype.hasOwnProperty;function It(s,M,j){var q=j.ref;return{$$typeof:r,type:s,key:M,ref:q!==void 0?q:null,props:j}}function jl(s,M){return It(s.type,M,s.props)}function Pt(s){return typeof s=="object"&&s!==null&&s.$$typeof===r}function I(s){var M={"=":"=0",":":"=2"};return"$"+s.replace(/[=:]/g,function(j){return M[j]})}var Rl=/\/+/g;function tl(s,M){return typeof s=="object"&&s!==null&&s.key!=null?I(""+s.key):M.toString(36)}function ll(s){switch(s.status){case"fulfilled":return s.value;case"rejected":throw s.reason;default:switch(typeof s.status=="string"?s.then(Ot,Ot):(s.status="pending",s.then(function(M){s.status==="pending"&&(s.status="fulfilled",s.value=M)},function(M){s.status==="pending"&&(s.status="rejected",s.reason=M)})),s.status){case"fulfilled":return s.value;case"rejected":throw s.reason}}throw s}function x(s,M,j,q,k){var P=typeof s;(P==="undefined"||P==="boolean")&&(s=null);var yt=!1;if(s===null)yt=!0;else switch(P){case"bigint":case"string":case"number":yt=!0;break;case"object":switch(s.$$typeof){case r:case v:yt=!0;break;case R:return yt=s._init,x(yt(s._payload),M,j,q,k)}}if(yt)return k=k(s),yt=q===""?"."+tl(s,0):q,it(k)?(j="",yt!=null&&(j=yt.replace(Rl,"$&/")+"/"),x(k,M,j,"",function(qa){return qa})):k!=null&&(Pt(k)&&(k=jl(k,j+(k.key==null||s&&s.key===k.key?"":(""+k.key).replace(Rl,"$&/")+"/")+yt)),M.push(k)),1;yt=0;var Wt=q===""?".":q+":";if(it(s))for(var Ut=0;Ut>>1,dt=x[nt];if(0<_(dt,C))x[nt]=C,x[Z]=dt,Z=nt;else break t}}function S(x){return x.length===0?null:x[0]}function f(x){if(x.length===0)return null;var C=x[0],Z=x.pop();if(Z!==C){x[0]=Z;t:for(var nt=0,dt=x.length,s=dt>>>1;nt_(j,Z))q_(k,j)?(x[nt]=k,x[q]=Z,nt=q):(x[nt]=j,x[M]=Z,nt=M);else if(q_(k,Z))x[nt]=k,x[q]=Z,nt=q;else break t}}return C}function _(x,C){var Z=x.sortIndex-C.sortIndex;return Z!==0?Z:x.id-C.id}if(r.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var O=performance;r.unstable_now=function(){return O.now()}}else{var D=Date,U=D.now();r.unstable_now=function(){return D.now()-U}}var N=[],p=[],R=1,H=null,V=3,st=!1,ct=!1,G=!1,Q=!1,L=typeof setTimeout=="function"?setTimeout:null,gt=typeof clearTimeout=="function"?clearTimeout:null,zt=typeof setImmediate<"u"?setImmediate:null;function _t(x){for(var C=S(p);C!==null;){if(C.callback===null)f(p);else if(C.startTime<=x)f(p),C.sortIndex=C.expirationTime,v(N,C);else break;C=S(p)}}function it(x){if(G=!1,_t(x),!ct)if(S(N)!==null)ct=!0,Ot||(Ot=!0,I());else{var C=S(p);C!==null&&ll(it,C.startTime-x)}}var Ot=!1,J=-1,Rt=5,It=-1;function jl(){return Q?!0:!(r.unstable_now()-Itx&&jl());){var nt=H.callback;if(typeof nt=="function"){H.callback=null,V=H.priorityLevel;var dt=nt(H.expirationTime<=x);if(x=r.unstable_now(),typeof dt=="function"){H.callback=dt,_t(x),C=!0;break l}H===S(N)&&f(N),_t(x)}else f(N);H=S(N)}if(H!==null)C=!0;else{var s=S(p);s!==null&&ll(it,s.startTime-x),C=!1}}break t}finally{H=null,V=Z,st=!1}C=void 0}}finally{C?I():Ot=!1}}}var I;if(typeof zt=="function")I=function(){zt(Pt)};else if(typeof MessageChannel<"u"){var Rl=new MessageChannel,tl=Rl.port2;Rl.port1.onmessage=Pt,I=function(){tl.postMessage(null)}}else I=function(){L(Pt,0)};function ll(x,C){J=L(function(){x(r.unstable_now())},C)}r.unstable_IdlePriority=5,r.unstable_ImmediatePriority=1,r.unstable_LowPriority=4,r.unstable_NormalPriority=3,r.unstable_Profiling=null,r.unstable_UserBlockingPriority=2,r.unstable_cancelCallback=function(x){x.callback=null},r.unstable_forceFrameRate=function(x){0>x||125nt?(x.sortIndex=Z,v(p,x),S(N)===null&&x===S(p)&&(G?(gt(J),J=-1):G=!0,ll(it,Z-nt))):(x.sortIndex=dt,v(N,x),ct||st||(ct=!0,Ot||(Ot=!0,I()))),x},r.unstable_shouldYield=jl,r.unstable_wrapCallback=function(x){var C=V;return function(){var Z=V;V=C;try{return x.apply(this,arguments)}finally{V=Z}}}})(Ef)),Ef}var wd;function qm(){return wd||(wd=1,Tf.exports=Bm()),Tf.exports}var Af={exports:{}},kt={};var Ld;function Ym(){if(Ld)return kt;Ld=1;var r=Rf();function v(N){var p="https://react.dev/errors/"+N;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(r)}catch(v){console.error(v)}}return r(),Af.exports=Ym(),Af.exports}var Kd;function Xm(){if(Kd)return Uu;Kd=1;var r=qm(),v=Rf(),S=Gm();function f(t){var l="https://react.dev/errors/"+t;if(1dt||(t.current=nt[dt],nt[dt]=null,dt--)}function j(t,l){dt++,nt[dt]=t.current,t.current=l}var q=s(null),k=s(null),P=s(null),yt=s(null);function Wt(t,l){switch(j(P,l),j(k,t),j(q,null),l.nodeType){case 9:case 11:t=(t=l.documentElement)&&(t=t.namespaceURI)?cd(t):0;break;default:if(t=l.tagName,l=l.namespaceURI)l=cd(l),t=fd(l,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}M(q),j(q,t)}function Ut(){M(q),M(k),M(P)}function qa(t){t.memoizedState!==null&&j(yt,t);var l=q.current,e=fd(l,t.type);l!==e&&(j(k,t),j(q,e))}function Hu(t){k.current===t&&(M(q),M(k)),yt.current===t&&(M(yt),Mu._currentValue=Z)}var li,Bf;function Ue(t){if(li===void 0)try{throw Error()}catch(e){var l=e.stack.trim().match(/\n( *(at )?)/);li=l&&l[1]||"",Bf=-1{for(const O of _)if(O.type==="childList")for(const D of O.addedNodes)D.tagName==="LINK"&&D.rel==="modulepreload"&&f(D)}).observe(document,{childList:!0,subtree:!0});function S(_){const O={};return _.integrity&&(O.integrity=_.integrity),_.referrerPolicy&&(O.referrerPolicy=_.referrerPolicy),_.crossOrigin==="use-credentials"?O.credentials="include":_.crossOrigin==="anonymous"?O.credentials="omit":O.credentials="same-origin",O}function f(_){if(_.ep)return;_.ep=!0;const O=S(_);fetch(_.href,O)}})();var Sf={exports:{}},Du={};var Yd;function jm(){if(Yd)return Du;Yd=1;var r=Symbol.for("react.transitional.element"),v=Symbol.for("react.fragment");function S(f,_,O){var D=null;if(O!==void 0&&(D=""+O),_.key!==void 0&&(D=""+_.key),"key"in _){O={};for(var U in _)U!=="key"&&(O[U]=_[U])}else O=_;return _=O.ref,{$$typeof:r,type:f,key:D,ref:_!==void 0?_:null,props:O}}return Du.Fragment=v,Du.jsx=S,Du.jsxs=S,Du}var Gd;function Rm(){return Gd||(Gd=1,Sf.exports=jm()),Sf.exports}var E=Rm(),xf={exports:{}},K={};var Xd;function Hm(){if(Xd)return K;Xd=1;var r=Symbol.for("react.transitional.element"),v=Symbol.for("react.portal"),S=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),_=Symbol.for("react.profiler"),O=Symbol.for("react.consumer"),D=Symbol.for("react.context"),U=Symbol.for("react.forward_ref"),N=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),R=Symbol.for("react.lazy"),H=Symbol.for("react.activity"),L=Symbol.iterator;function ot(o){return o===null||typeof o!="object"?null:(o=L&&o[L]||o["@@iterator"],typeof o=="function"?o:null)}var ct={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},G=Object.assign,Q={};function V(o,M,j){this.props=o,this.context=M,this.refs=Q,this.updater=j||ct}V.prototype.isReactComponent={},V.prototype.setState=function(o,M){if(typeof o!="object"&&typeof o!="function"&&o!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,o,M,"setState")},V.prototype.forceUpdate=function(o){this.updater.enqueueForceUpdate(this,o,"forceUpdate")};function gt(){}gt.prototype=V.prototype;function zt(o,M,j){this.props=o,this.context=M,this.refs=Q,this.updater=j||ct}var _t=zt.prototype=new gt;_t.constructor=zt,G(_t,V.prototype),_t.isPureReactComponent=!0;var nt=Array.isArray;function Ot(){}var J={H:null,A:null,T:null,S:null},Nt=Object.prototype.hasOwnProperty;function Xt(o,M,j){var q=j.ref;return{$$typeof:r,type:o,key:M,ref:q!==void 0?q:null,props:j}}function pl(o,M){return Xt(o.type,M,o.props)}function Pt(o){return typeof o=="object"&&o!==null&&o.$$typeof===r}function I(o){var M={"=":"=0",":":"=2"};return"$"+o.replace(/[=:]/g,function(j){return M[j]})}var Rl=/\/+/g;function tl(o,M){return typeof o=="object"&&o!==null&&o.key!=null?I(""+o.key):M.toString(36)}function ll(o){switch(o.status){case"fulfilled":return o.value;case"rejected":throw o.reason;default:switch(typeof o.status=="string"?o.then(Ot,Ot):(o.status="pending",o.then(function(M){o.status==="pending"&&(o.status="fulfilled",o.value=M)},function(M){o.status==="pending"&&(o.status="rejected",o.reason=M)})),o.status){case"fulfilled":return o.value;case"rejected":throw o.reason}}throw o}function x(o,M,j,q,k){var P=typeof o;(P==="undefined"||P==="boolean")&&(o=null);var yt=!1;if(o===null)yt=!0;else switch(P){case"bigint":case"string":case"number":yt=!0;break;case"object":switch(o.$$typeof){case r:case v:yt=!0;break;case R:return yt=o._init,x(yt(o._payload),M,j,q,k)}}if(yt)return k=k(o),yt=q===""?"."+tl(o,0):q,nt(k)?(j="",yt!=null&&(j=yt.replace(Rl,"$&/")+"/"),x(k,M,j,"",function(qa){return qa})):k!=null&&(Pt(k)&&(k=pl(k,j+(k.key==null||o&&o.key===k.key?"":(""+k.key).replace(Rl,"$&/")+"/")+yt)),M.push(k)),1;yt=0;var $t=q===""?".":q+":";if(nt(o))for(var Ct=0;Ct>>1,dt=x[it];if(0<_(dt,C))x[it]=C,x[Z]=dt,Z=it;else break t}}function S(x){return x.length===0?null:x[0]}function f(x){if(x.length===0)return null;var C=x[0],Z=x.pop();if(Z!==C){x[0]=Z;t:for(var it=0,dt=x.length,o=dt>>>1;it_(j,Z))q_(k,j)?(x[it]=k,x[q]=Z,it=q):(x[it]=j,x[M]=Z,it=M);else if(q_(k,Z))x[it]=k,x[q]=Z,it=q;else break t}}return C}function _(x,C){var Z=x.sortIndex-C.sortIndex;return Z!==0?Z:x.id-C.id}if(r.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var O=performance;r.unstable_now=function(){return O.now()}}else{var D=Date,U=D.now();r.unstable_now=function(){return D.now()-U}}var N=[],p=[],R=1,H=null,L=3,ot=!1,ct=!1,G=!1,Q=!1,V=typeof setTimeout=="function"?setTimeout:null,gt=typeof clearTimeout=="function"?clearTimeout:null,zt=typeof setImmediate<"u"?setImmediate:null;function _t(x){for(var C=S(p);C!==null;){if(C.callback===null)f(p);else if(C.startTime<=x)f(p),C.sortIndex=C.expirationTime,v(N,C);else break;C=S(p)}}function nt(x){if(G=!1,_t(x),!ct)if(S(N)!==null)ct=!0,Ot||(Ot=!0,I());else{var C=S(p);C!==null&&ll(nt,C.startTime-x)}}var Ot=!1,J=-1,Nt=5,Xt=-1;function pl(){return Q?!0:!(r.unstable_now()-Xtx&&pl());){var it=H.callback;if(typeof it=="function"){H.callback=null,L=H.priorityLevel;var dt=it(H.expirationTime<=x);if(x=r.unstable_now(),typeof dt=="function"){H.callback=dt,_t(x),C=!0;break l}H===S(N)&&f(N),_t(x)}else f(N);H=S(N)}if(H!==null)C=!0;else{var o=S(p);o!==null&&ll(nt,o.startTime-x),C=!1}}break t}finally{H=null,L=Z,ot=!1}C=void 0}}finally{C?I():Ot=!1}}}var I;if(typeof zt=="function")I=function(){zt(Pt)};else if(typeof MessageChannel<"u"){var Rl=new MessageChannel,tl=Rl.port2;Rl.port1.onmessage=Pt,I=function(){tl.postMessage(null)}}else I=function(){V(Pt,0)};function ll(x,C){J=V(function(){x(r.unstable_now())},C)}r.unstable_IdlePriority=5,r.unstable_ImmediatePriority=1,r.unstable_LowPriority=4,r.unstable_NormalPriority=3,r.unstable_Profiling=null,r.unstable_UserBlockingPriority=2,r.unstable_cancelCallback=function(x){x.callback=null},r.unstable_forceFrameRate=function(x){0>x||125it?(x.sortIndex=Z,v(p,x),S(N)===null&&x===S(p)&&(G?(gt(J),J=-1):G=!0,ll(nt,Z-it))):(x.sortIndex=dt,v(N,x),ct||ot||(ct=!0,Ot||(Ot=!0,I()))),x},r.unstable_shouldYield=pl,r.unstable_wrapCallback=function(x){var C=L;return function(){var Z=L;L=C;try{return x.apply(this,arguments)}finally{L=Z}}}})(Af)),Af}var wd;function qm(){return wd||(wd=1,Tf.exports=Bm()),Tf.exports}var Ef={exports:{}},Wt={};var Ld;function Ym(){if(Ld)return Wt;Ld=1;var r=Rf();function v(N){var p="https://react.dev/errors/"+N;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(r)}catch(v){console.error(v)}}return r(),Ef.exports=Ym(),Ef.exports}var Kd;function Xm(){if(Kd)return Uu;Kd=1;var r=qm(),v=Rf(),S=Gm();function f(t){var l="https://react.dev/errors/"+t;if(1dt||(t.current=it[dt],it[dt]=null,dt--)}function j(t,l){dt++,it[dt]=t.current,t.current=l}var q=o(null),k=o(null),P=o(null),yt=o(null);function $t(t,l){switch(j(P,l),j(k,t),j(q,null),l.nodeType){case 9:case 11:t=(t=l.documentElement)&&(t=t.namespaceURI)?cd(t):0;break;default:if(t=l.tagName,l=l.namespaceURI)l=cd(l),t=fd(l,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}M(q),j(q,t)}function Ct(){M(q),M(k),M(P)}function qa(t){t.memoizedState!==null&&j(yt,t);var l=q.current,e=fd(l,t.type);l!==e&&(j(k,t),j(q,e))}function Hu(t){k.current===t&&(M(q),M(k)),yt.current===t&&(M(yt),Mu._currentValue=Z)}var li,Bf;function Ue(t){if(li===void 0)try{throw Error()}catch(e){var l=e.stack.trim().match(/\n( *(at )?)/);li=l&&l[1]||"",Bf=-1)":-1u||o[a]!==h[u]){var z=` -`+o[a].replace(" at new "," at ");return t.displayName&&z.includes("")&&(z=z.replace("",t.displayName)),z}while(1<=a&&0<=u);break}}}finally{ei=!1,Error.prepareStackTrace=e}return(e=t?t.displayName||t.name:"")?Ue(e):""}function o0(t,l){switch(t.tag){case 26:case 27:case 5:return Ue(t.type);case 16:return Ue("Lazy");case 13:return t.child!==l&&l!==null?Ue("Suspense Fallback"):Ue("Suspense");case 19:return Ue("SuspenseList");case 0:case 15:return ai(t.type,!1);case 11:return ai(t.type.render,!1);case 1:return ai(t.type,!0);case 31:return Ue("Activity");default:return""}}function qf(t){try{var l="",e=null;do l+=o0(t,e),e=t,t=t.return;while(t);return l}catch(a){return` +`);for(u=a=0;au||s[a]!==h[u]){var z=` +`+s[a].replace(" at new "," at ");return t.displayName&&z.includes("")&&(z=z.replace("",t.displayName)),z}while(1<=a&&0<=u);break}}}finally{ei=!1,Error.prepareStackTrace=e}return(e=t?t.displayName||t.name:"")?Ue(e):""}function s0(t,l){switch(t.tag){case 26:case 27:case 5:return Ue(t.type);case 16:return Ue("Lazy");case 13:return t.child!==l&&l!==null?Ue("Suspense Fallback"):Ue("Suspense");case 19:return Ue("SuspenseList");case 0:case 15:return ai(t.type,!1);case 11:return ai(t.type.render,!1);case 1:return ai(t.type,!0);case 31:return Ue("Activity");default:return""}}function qf(t){try{var l="",e=null;do l+=s0(t,e),e=t,t=t.return;while(t);return l}catch(a){return` Error generating stack: `+a.message+` -`+a.stack}}var ui=Object.prototype.hasOwnProperty,ni=r.unstable_scheduleCallback,ii=r.unstable_cancelCallback,s0=r.unstable_shouldYield,d0=r.unstable_requestPaint,rl=r.unstable_now,y0=r.unstable_getCurrentPriorityLevel,Yf=r.unstable_ImmediatePriority,Gf=r.unstable_UserBlockingPriority,Bu=r.unstable_NormalPriority,m0=r.unstable_LowPriority,Xf=r.unstable_IdlePriority,h0=r.log,g0=r.unstable_setDisableYieldValue,Ya=null,ol=null;function ue(t){if(typeof h0=="function"&&g0(t),ol&&typeof ol.setStrictMode=="function")try{ol.setStrictMode(Ya,t)}catch{}}var sl=Math.clz32?Math.clz32:p0,v0=Math.log,b0=Math.LN2;function p0(t){return t>>>=0,t===0?32:31-(v0(t)/b0|0)|0}var qu=256,Yu=262144,Gu=4194304;function Ce(t){var l=t&42;if(l!==0)return l;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Xu(t,l,e){var a=t.pendingLanes;if(a===0)return 0;var u=0,n=t.suspendedLanes,i=t.pingedLanes;t=t.warmLanes;var c=a&134217727;return c!==0?(a=c&~n,a!==0?u=Ce(a):(i&=c,i!==0?u=Ce(i):e||(e=c&~t,e!==0&&(u=Ce(e))))):(c=a&~n,c!==0?u=Ce(c):i!==0?u=Ce(i):e||(e=a&~t,e!==0&&(u=Ce(e)))),u===0?0:l!==0&&l!==u&&(l&n)===0&&(n=u&-u,e=l&-l,n>=e||n===32&&(e&4194048)!==0)?l:u}function Ga(t,l){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&l)===0}function S0(t,l){switch(t){case 1:case 2:case 4:case 8:case 64:return l+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return l+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Qf(){var t=Gu;return Gu<<=1,(Gu&62914560)===0&&(Gu=4194304),t}function ci(t){for(var l=[],e=0;31>e;e++)l.push(t);return l}function Xa(t,l){t.pendingLanes|=l,l!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function x0(t,l,e,a,u,n){var i=t.pendingLanes;t.pendingLanes=e,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=e,t.entangledLanes&=e,t.errorRecoveryDisabledLanes&=e,t.shellSuspendCounter=0;var c=t.entanglements,o=t.expirationTimes,h=t.hiddenUpdates;for(e=i&~e;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var _0=/[\n"\\]/g;function Sl(t){return t.replace(_0,function(l){return"\\"+l.charCodeAt(0).toString(16)+" "})}function yi(t,l,e,a,u,n,i,c){t.name="",i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"?t.type=i:t.removeAttribute("type"),l!=null?i==="number"?(l===0&&t.value===""||t.value!=l)&&(t.value=""+pl(l)):t.value!==""+pl(l)&&(t.value=""+pl(l)):i!=="submit"&&i!=="reset"||t.removeAttribute("value"),l!=null?mi(t,i,pl(l)):e!=null?mi(t,i,pl(e)):a!=null&&t.removeAttribute("value"),u==null&&n!=null&&(t.defaultChecked=!!n),u!=null&&(t.checked=u&&typeof u!="function"&&typeof u!="symbol"),c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"?t.name=""+pl(c):t.removeAttribute("name")}function tr(t,l,e,a,u,n,i,c){if(n!=null&&typeof n!="function"&&typeof n!="symbol"&&typeof n!="boolean"&&(t.type=n),l!=null||e!=null){if(!(n!=="submit"&&n!=="reset"||l!=null)){di(t);return}e=e!=null?""+pl(e):"",l=l!=null?""+pl(l):e,c||l===t.value||(t.value=l),t.defaultValue=l}a=a??u,a=typeof a!="function"&&typeof a!="symbol"&&!!a,t.checked=c?t.checked:!!a,t.defaultChecked=!!a,i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"&&(t.name=i),di(t)}function mi(t,l,e){l==="number"&&wu(t.ownerDocument)===t||t.defaultValue===""+e||(t.defaultValue=""+e)}function ea(t,l,e,a){if(t=t.options,l){l={};for(var u=0;u"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),pi=!1;if(Ql)try{var La={};Object.defineProperty(La,"passive",{get:function(){pi=!0}}),window.addEventListener("test",La,La),window.removeEventListener("test",La,La)}catch{pi=!1}var ie=null,Si=null,Vu=null;function cr(){if(Vu)return Vu;var t,l=Si,e=l.length,a,u="value"in ie?ie.value:ie.textContent,n=u.length;for(t=0;t=Ja),yr=" ",mr=!1;function hr(t,l){switch(t){case"keyup":return ly.indexOf(l.keyCode)!==-1;case"keydown":return l.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function gr(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var ia=!1;function ay(t,l){switch(t){case"compositionend":return gr(l);case"keypress":return l.which!==32?null:(mr=!0,yr);case"textInput":return t=l.data,t===yr&&mr?null:t;default:return null}}function uy(t,l){if(ia)return t==="compositionend"||!Ai&&hr(t,l)?(t=cr(),Vu=Si=ie=null,ia=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(l.ctrlKey||l.altKey||l.metaKey)||l.ctrlKey&&l.altKey){if(l.char&&1=l)return{node:e,offset:l-t};t=a}t:{for(;e;){if(e.nextSibling){e=e.nextSibling;break t}e=e.parentNode}e=void 0}e=Er(e)}}function Mr(t,l){return t&&l?t===l?!0:t&&t.nodeType===3?!1:l&&l.nodeType===3?Mr(t,l.parentNode):"contains"in t?t.contains(l):t.compareDocumentPosition?!!(t.compareDocumentPosition(l)&16):!1:!1}function _r(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var l=wu(t.document);l instanceof t.HTMLIFrameElement;){try{var e=typeof l.contentWindow.location.href=="string"}catch{e=!1}if(e)t=l.contentWindow;else break;l=wu(t.document)}return l}function Oi(t){var l=t&&t.nodeName&&t.nodeName.toLowerCase();return l&&(l==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||l==="textarea"||t.contentEditable==="true")}var dy=Ql&&"documentMode"in document&&11>=document.documentMode,ca=null,Ni=null,Fa=null,Di=!1;function Or(t,l,e){var a=e.window===e?e.document:e.nodeType===9?e:e.ownerDocument;Di||ca==null||ca!==wu(a)||(a=ca,"selectionStart"in a&&Oi(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),Fa&&$a(Fa,a)||(Fa=a,a=Gn(Ni,"onSelect"),0>=i,u-=i,Hl=1<<32-sl(l)+u|e<$?(at=Y,Y=null):at=Y.sibling;var rt=g(y,Y,m[$],T);if(rt===null){Y===null&&(Y=at);break}t&&Y&&rt.alternate===null&&l(y,Y),d=n(rt,d,$),ft===null?X=rt:ft.sibling=rt,ft=rt,Y=at}if($===m.length)return e(y,Y),ut&&wl(y,$),X;if(Y===null){for(;$$?(at=Y,Y=null):at=Y.sibling;var Oe=g(y,Y,rt.value,T);if(Oe===null){Y===null&&(Y=at);break}t&&Y&&Oe.alternate===null&&l(y,Y),d=n(Oe,d,$),ft===null?X=Oe:ft.sibling=Oe,ft=Oe,Y=at}if(rt.done)return e(y,Y),ut&&wl(y,$),X;if(Y===null){for(;!rt.done;$++,rt=m.next())rt=E(y,rt.value,T),rt!==null&&(d=n(rt,d,$),ft===null?X=rt:ft.sibling=rt,ft=rt);return ut&&wl(y,$),X}for(Y=a(Y);!rt.done;$++,rt=m.next())rt=b(Y,y,$,rt.value,T),rt!==null&&(t&&rt.alternate!==null&&Y.delete(rt.key===null?$:rt.key),d=n(rt,d,$),ft===null?X=rt:ft.sibling=rt,ft=rt);return t&&Y.forEach(function(Cm){return l(y,Cm)}),ut&&wl(y,$),X}function pt(y,d,m,T){if(typeof m=="object"&&m!==null&&m.type===G&&m.key===null&&(m=m.props.children),typeof m=="object"&&m!==null){switch(m.$$typeof){case st:t:{for(var X=m.key;d!==null;){if(d.key===X){if(X=m.type,X===G){if(d.tag===7){e(y,d.sibling),T=u(d,m.props.children),T.return=y,y=T;break t}}else if(d.elementType===X||typeof X=="object"&&X!==null&&X.$$typeof===Rt&&we(X)===d.type){e(y,d.sibling),T=u(d,m.props),au(T,m),T.return=y,y=T;break t}e(y,d);break}else l(y,d);d=d.sibling}m.type===G?(T=Ye(m.props.children,y.mode,T,m.key),T.return=y,y=T):(T=ln(m.type,m.key,m.props,null,y.mode,T),au(T,m),T.return=y,y=T)}return i(y);case ct:t:{for(X=m.key;d!==null;){if(d.key===X)if(d.tag===4&&d.stateNode.containerInfo===m.containerInfo&&d.stateNode.implementation===m.implementation){e(y,d.sibling),T=u(d,m.children||[]),T.return=y,y=T;break t}else{e(y,d);break}else l(y,d);d=d.sibling}T=qi(m,y.mode,T),T.return=y,y=T}return i(y);case Rt:return m=we(m),pt(y,d,m,T)}if(ll(m))return B(y,d,m,T);if(I(m)){if(X=I(m),typeof X!="function")throw Error(f(150));return m=X.call(m),w(y,d,m,T)}if(typeof m.then=="function")return pt(y,d,rn(m),T);if(m.$$typeof===zt)return pt(y,d,un(y,m),T);on(y,m)}return typeof m=="string"&&m!==""||typeof m=="number"||typeof m=="bigint"?(m=""+m,d!==null&&d.tag===6?(e(y,d.sibling),T=u(d,m),T.return=y,y=T):(e(y,d),T=Bi(m,y.mode,T),T.return=y,y=T),i(y)):e(y,d)}return function(y,d,m,T){try{eu=0;var X=pt(y,d,m,T);return ba=null,X}catch(Y){if(Y===va||Y===cn)throw Y;var ft=yl(29,Y,null,y.mode);return ft.lanes=T,ft.return=y,ft}}}var Ve=Fr(!0),Ir=Fr(!1),se=!1;function Wi(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function $i(t,l){t=t.updateQueue,l.updateQueue===t&&(l.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function de(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function ye(t,l,e){var a=t.updateQueue;if(a===null)return null;if(a=a.shared,(ot&2)!==0){var u=a.pending;return u===null?l.next=l:(l.next=u.next,u.next=l),a.pending=l,l=tn(t),Hr(t,null,e),l}return Pu(t,a,l,e),tn(t)}function uu(t,l,e){if(l=l.updateQueue,l!==null&&(l=l.shared,(e&4194048)!==0)){var a=l.lanes;a&=t.pendingLanes,e|=a,l.lanes=e,wf(t,e)}}function Fi(t,l){var e=t.updateQueue,a=t.alternate;if(a!==null&&(a=a.updateQueue,e===a)){var u=null,n=null;if(e=e.firstBaseUpdate,e!==null){do{var i={lane:e.lane,tag:e.tag,payload:e.payload,callback:null,next:null};n===null?u=n=i:n=n.next=i,e=e.next}while(e!==null);n===null?u=n=l:n=n.next=l}else u=n=l;e={baseState:a.baseState,firstBaseUpdate:u,lastBaseUpdate:n,shared:a.shared,callbacks:a.callbacks},t.updateQueue=e;return}t=e.lastBaseUpdate,t===null?e.firstBaseUpdate=l:t.next=l,e.lastBaseUpdate=l}var Ii=!1;function nu(){if(Ii){var t=ga;if(t!==null)throw t}}function iu(t,l,e,a){Ii=!1;var u=t.updateQueue;se=!1;var n=u.firstBaseUpdate,i=u.lastBaseUpdate,c=u.shared.pending;if(c!==null){u.shared.pending=null;var o=c,h=o.next;o.next=null,i===null?n=h:i.next=h,i=o;var z=t.alternate;z!==null&&(z=z.updateQueue,c=z.lastBaseUpdate,c!==i&&(c===null?z.firstBaseUpdate=h:c.next=h,z.lastBaseUpdate=o))}if(n!==null){var E=u.baseState;i=0,z=h=o=null,c=n;do{var g=c.lane&-536870913,b=g!==c.lane;if(b?(et&g)===g:(a&g)===g){g!==0&&g===ha&&(Ii=!0),z!==null&&(z=z.next={lane:0,tag:c.tag,payload:c.payload,callback:null,next:null});t:{var B=t,w=c;g=l;var pt=e;switch(w.tag){case 1:if(B=w.payload,typeof B=="function"){E=B.call(pt,E,g);break t}E=B;break t;case 3:B.flags=B.flags&-65537|128;case 0:if(B=w.payload,g=typeof B=="function"?B.call(pt,E,g):B,g==null)break t;E=H({},E,g);break t;case 2:se=!0}}g=c.callback,g!==null&&(t.flags|=64,b&&(t.flags|=8192),b=u.callbacks,b===null?u.callbacks=[g]:b.push(g))}else b={lane:g,tag:c.tag,payload:c.payload,callback:c.callback,next:null},z===null?(h=z=b,o=E):z=z.next=b,i|=g;if(c=c.next,c===null){if(c=u.shared.pending,c===null)break;b=c,c=b.next,b.next=null,u.lastBaseUpdate=b,u.shared.pending=null}}while(!0);z===null&&(o=E),u.baseState=o,u.firstBaseUpdate=h,u.lastBaseUpdate=z,n===null&&(u.shared.lanes=0),be|=i,t.lanes=i,t.memoizedState=E}}function Pr(t,l){if(typeof t!="function")throw Error(f(191,t));t.call(l)}function to(t,l){var e=t.callbacks;if(e!==null)for(t.callbacks=null,t=0;tn?n:8;var i=x.T,c={};x.T=c,vc(t,!1,l,e);try{var o=u(),h=x.S;if(h!==null&&h(c,o),o!==null&&typeof o=="object"&&typeof o.then=="function"){var z=xy(o,a);ru(t,l,z,bl(t))}else ru(t,l,a,bl(t))}catch(E){ru(t,l,{then:function(){},status:"rejected",reason:E},bl())}finally{C.p=n,i!==null&&c.types!==null&&(i.types=c.types),x.T=i}}function _y(){}function hc(t,l,e,a){if(t.tag!==5)throw Error(f(476));var u=jo(t).queue;Co(t,u,l,Z,e===null?_y:function(){return Ro(t),e(a)})}function jo(t){var l=t.memoizedState;if(l!==null)return l;l={memoizedState:Z,baseState:Z,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Jl,lastRenderedState:Z},next:null};var e={};return l.next={memoizedState:e,baseState:e,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Jl,lastRenderedState:e},next:null},t.memoizedState=l,t=t.alternate,t!==null&&(t.memoizedState=l),l}function Ro(t){var l=jo(t);l.next===null&&(l=t.alternate.memoizedState),ru(t,l.next.queue,{},bl())}function gc(){return Vt(Mu)}function Ho(){return jt().memoizedState}function Bo(){return jt().memoizedState}function Oy(t){for(var l=t.return;l!==null;){switch(l.tag){case 24:case 3:var e=bl();t=de(e);var a=ye(l,t,e);a!==null&&(fl(a,l,e),uu(a,l,e)),l={cache:Vi()},t.payload=l;return}l=l.return}}function Ny(t,l,e){var a=bl();e={lane:a,revertLane:0,gesture:null,action:e,hasEagerState:!1,eagerState:null,next:null},Sn(t)?Yo(l,e):(e=Ri(t,l,e,a),e!==null&&(fl(e,t,a),Go(e,l,a)))}function qo(t,l,e){var a=bl();ru(t,l,e,a)}function ru(t,l,e,a){var u={lane:a,revertLane:0,gesture:null,action:e,hasEagerState:!1,eagerState:null,next:null};if(Sn(t))Yo(l,u);else{var n=t.alternate;if(t.lanes===0&&(n===null||n.lanes===0)&&(n=l.lastRenderedReducer,n!==null))try{var i=l.lastRenderedState,c=n(i,e);if(u.hasEagerState=!0,u.eagerState=c,dl(c,i))return Pu(t,l,u,0),St===null&&Iu(),!1}catch{}if(e=Ri(t,l,u,a),e!==null)return fl(e,t,a),Go(e,l,a),!0}return!1}function vc(t,l,e,a){if(a={lane:2,revertLane:Wc(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},Sn(t)){if(l)throw Error(f(479))}else l=Ri(t,e,a,2),l!==null&&fl(l,t,2)}function Sn(t){var l=t.alternate;return t===W||l!==null&&l===W}function Yo(t,l){Sa=yn=!0;var e=t.pending;e===null?l.next=l:(l.next=e.next,e.next=l),t.pending=l}function Go(t,l,e){if((e&4194048)!==0){var a=l.lanes;a&=t.pendingLanes,e|=a,l.lanes=e,wf(t,e)}}var ou={readContext:Vt,use:gn,useCallback:Nt,useContext:Nt,useEffect:Nt,useImperativeHandle:Nt,useLayoutEffect:Nt,useInsertionEffect:Nt,useMemo:Nt,useReducer:Nt,useRef:Nt,useState:Nt,useDebugValue:Nt,useDeferredValue:Nt,useTransition:Nt,useSyncExternalStore:Nt,useId:Nt,useHostTransitionStatus:Nt,useFormState:Nt,useActionState:Nt,useOptimistic:Nt,useMemoCache:Nt,useCacheRefresh:Nt};ou.useEffectEvent=Nt;var Xo={readContext:Vt,use:gn,useCallback:function(t,l){return $t().memoizedState=[t,l===void 0?null:l],t},useContext:Vt,useEffect:To,useImperativeHandle:function(t,l,e){e=e!=null?e.concat([t]):null,bn(4194308,4,_o.bind(null,l,t),e)},useLayoutEffect:function(t,l){return bn(4194308,4,t,l)},useInsertionEffect:function(t,l){bn(4,2,t,l)},useMemo:function(t,l){var e=$t();l=l===void 0?null:l;var a=t();if(Ke){ue(!0);try{t()}finally{ue(!1)}}return e.memoizedState=[a,l],a},useReducer:function(t,l,e){var a=$t();if(e!==void 0){var u=e(l);if(Ke){ue(!0);try{e(l)}finally{ue(!1)}}}else u=l;return a.memoizedState=a.baseState=u,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:u},a.queue=t,t=t.dispatch=Ny.bind(null,W,t),[a.memoizedState,t]},useRef:function(t){var l=$t();return t={current:t},l.memoizedState=t},useState:function(t){t=oc(t);var l=t.queue,e=qo.bind(null,W,l);return l.dispatch=e,[t.memoizedState,e]},useDebugValue:yc,useDeferredValue:function(t,l){var e=$t();return mc(e,t,l)},useTransition:function(){var t=oc(!1);return t=Co.bind(null,W,t.queue,!0,!1),$t().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,l,e){var a=W,u=$t();if(ut){if(e===void 0)throw Error(f(407));e=e()}else{if(e=l(),St===null)throw Error(f(349));(et&127)!==0||io(a,l,e)}u.memoizedState=e;var n={value:e,getSnapshot:l};return u.queue=n,To(fo.bind(null,a,n,t),[t]),a.flags|=2048,za(9,{destroy:void 0},co.bind(null,a,n,e,l),null),e},useId:function(){var t=$t(),l=St.identifierPrefix;if(ut){var e=Bl,a=Hl;e=(a&~(1<<32-sl(a)-1)).toString(32)+e,l="_"+l+"R_"+e,e=mn++,0<\/script>",n=n.removeChild(n.firstChild);break;case"select":n=typeof a.is=="string"?i.createElement("select",{is:a.is}):i.createElement("select"),a.multiple?n.multiple=!0:a.size&&(n.size=a.size);break;default:n=typeof a.is=="string"?i.createElement(u,{is:a.is}):i.createElement(u)}}n[wt]=l,n[el]=a;t:for(i=l.child;i!==null;){if(i.tag===5||i.tag===6)n.appendChild(i.stateNode);else if(i.tag!==4&&i.tag!==27&&i.child!==null){i.child.return=i,i=i.child;continue}if(i===l)break t;for(;i.sibling===null;){if(i.return===null||i.return===l)break t;i=i.return}i.sibling.return=i.return,i=i.sibling}l.stateNode=n;t:switch(Jt(n,u,a),u){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break t;case"img":a=!0;break t;default:a=!1}a&&Wl(l)}}return Et(l),Uc(l,l.type,t===null?null:t.memoizedProps,l.pendingProps,e),null;case 6:if(t&&l.stateNode!=null)t.memoizedProps!==a&&Wl(l);else{if(typeof a!="string"&&l.stateNode===null)throw Error(f(166));if(t=P.current,ya(l)){if(t=l.stateNode,e=l.memoizedProps,a=null,u=Lt,u!==null)switch(u.tag){case 27:case 5:a=u.memoizedProps}t[wt]=l,t=!!(t.nodeValue===e||a!==null&&a.suppressHydrationWarning===!0||nd(t.nodeValue,e)),t||re(l,!0)}else t=Xn(t).createTextNode(a),t[wt]=l,l.stateNode=t}return Et(l),null;case 31:if(e=l.memoizedState,t===null||t.memoizedState!==null){if(a=ya(l),e!==null){if(t===null){if(!a)throw Error(f(318));if(t=l.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(f(557));t[wt]=l}else Ge(),(l.flags&128)===0&&(l.memoizedState=null),l.flags|=4;Et(l),t=!1}else e=Qi(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=e),t=!0;if(!t)return l.flags&256?(hl(l),l):(hl(l),null);if((l.flags&128)!==0)throw Error(f(558))}return Et(l),null;case 13:if(a=l.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(u=ya(l),a!==null&&a.dehydrated!==null){if(t===null){if(!u)throw Error(f(318));if(u=l.memoizedState,u=u!==null?u.dehydrated:null,!u)throw Error(f(317));u[wt]=l}else Ge(),(l.flags&128)===0&&(l.memoizedState=null),l.flags|=4;Et(l),u=!1}else u=Qi(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=u),u=!0;if(!u)return l.flags&256?(hl(l),l):(hl(l),null)}return hl(l),(l.flags&128)!==0?(l.lanes=e,l):(e=a!==null,t=t!==null&&t.memoizedState!==null,e&&(a=l.child,u=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(u=a.alternate.memoizedState.cachePool.pool),n=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(n=a.memoizedState.cachePool.pool),n!==u&&(a.flags|=2048)),e!==t&&e&&(l.child.flags|=8192),An(l,l.updateQueue),Et(l),null);case 4:return Ut(),t===null&&Pc(l.stateNode.containerInfo),Et(l),null;case 10:return Vl(l.type),Et(l),null;case 19:if(M(Ct),a=l.memoizedState,a===null)return Et(l),null;if(u=(l.flags&128)!==0,n=a.rendering,n===null)if(u)du(a,!1);else{if(Dt!==0||t!==null&&(t.flags&128)!==0)for(t=l.child;t!==null;){if(n=dn(t),n!==null){for(l.flags|=128,du(a,!1),t=n.updateQueue,l.updateQueue=t,An(l,t),l.subtreeFlags=0,t=e,e=l.child;e!==null;)Br(e,t),e=e.sibling;return j(Ct,Ct.current&1|2),ut&&wl(l,a.treeForkCount),l.child}t=t.sibling}a.tail!==null&&rl()>Dn&&(l.flags|=128,u=!0,du(a,!1),l.lanes=4194304)}else{if(!u)if(t=dn(n),t!==null){if(l.flags|=128,u=!0,t=t.updateQueue,l.updateQueue=t,An(l,t),du(a,!0),a.tail===null&&a.tailMode==="hidden"&&!n.alternate&&!ut)return Et(l),null}else 2*rl()-a.renderingStartTime>Dn&&e!==536870912&&(l.flags|=128,u=!0,du(a,!1),l.lanes=4194304);a.isBackwards?(n.sibling=l.child,l.child=n):(t=a.last,t!==null?t.sibling=n:l.child=n,a.last=n)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=rl(),t.sibling=null,e=Ct.current,j(Ct,u?e&1|2:e&1),ut&&wl(l,a.treeForkCount),t):(Et(l),null);case 22:case 23:return hl(l),tc(),a=l.memoizedState!==null,t!==null?t.memoizedState!==null!==a&&(l.flags|=8192):a&&(l.flags|=8192),a?(e&536870912)!==0&&(l.flags&128)===0&&(Et(l),l.subtreeFlags&6&&(l.flags|=8192)):Et(l),e=l.updateQueue,e!==null&&An(l,e.retryQueue),e=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(e=t.memoizedState.cachePool.pool),a=null,l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(a=l.memoizedState.cachePool.pool),a!==e&&(l.flags|=2048),t!==null&&M(Ze),null;case 24:return e=null,t!==null&&(e=t.memoizedState.cache),l.memoizedState.cache!==e&&(l.flags|=2048),Vl(Ht),Et(l),null;case 25:return null;case 30:return null}throw Error(f(156,l.tag))}function Ry(t,l){switch(Gi(l),l.tag){case 1:return t=l.flags,t&65536?(l.flags=t&-65537|128,l):null;case 3:return Vl(Ht),Ut(),t=l.flags,(t&65536)!==0&&(t&128)===0?(l.flags=t&-65537|128,l):null;case 26:case 27:case 5:return Hu(l),null;case 31:if(l.memoizedState!==null){if(hl(l),l.alternate===null)throw Error(f(340));Ge()}return t=l.flags,t&65536?(l.flags=t&-65537|128,l):null;case 13:if(hl(l),t=l.memoizedState,t!==null&&t.dehydrated!==null){if(l.alternate===null)throw Error(f(340));Ge()}return t=l.flags,t&65536?(l.flags=t&-65537|128,l):null;case 19:return M(Ct),null;case 4:return Ut(),null;case 10:return Vl(l.type),null;case 22:case 23:return hl(l),tc(),t!==null&&M(Ze),t=l.flags,t&65536?(l.flags=t&-65537|128,l):null;case 24:return Vl(Ht),null;case 25:return null;default:return null}}function os(t,l){switch(Gi(l),l.tag){case 3:Vl(Ht),Ut();break;case 26:case 27:case 5:Hu(l);break;case 4:Ut();break;case 31:l.memoizedState!==null&&hl(l);break;case 13:hl(l);break;case 19:M(Ct);break;case 10:Vl(l.type);break;case 22:case 23:hl(l),tc(),t!==null&&M(Ze);break;case 24:Vl(Ht)}}function yu(t,l){try{var e=l.updateQueue,a=e!==null?e.lastEffect:null;if(a!==null){var u=a.next;e=u;do{if((e.tag&t)===t){a=void 0;var n=e.create,i=e.inst;a=n(),i.destroy=a}e=e.next}while(e!==u)}}catch(c){ht(l,l.return,c)}}function ge(t,l,e){try{var a=l.updateQueue,u=a!==null?a.lastEffect:null;if(u!==null){var n=u.next;a=n;do{if((a.tag&t)===t){var i=a.inst,c=i.destroy;if(c!==void 0){i.destroy=void 0,u=l;var o=e,h=c;try{h()}catch(z){ht(u,o,z)}}}a=a.next}while(a!==n)}}catch(z){ht(l,l.return,z)}}function ss(t){var l=t.updateQueue;if(l!==null){var e=t.stateNode;try{to(l,e)}catch(a){ht(t,t.return,a)}}}function ds(t,l,e){e.props=Je(t.type,t.memoizedProps),e.state=t.memoizedState;try{e.componentWillUnmount()}catch(a){ht(t,l,a)}}function mu(t,l){try{var e=t.ref;if(e!==null){switch(t.tag){case 26:case 27:case 5:var a=t.stateNode;break;case 30:a=t.stateNode;break;default:a=t.stateNode}typeof e=="function"?t.refCleanup=e(a):e.current=a}}catch(u){ht(t,l,u)}}function ql(t,l){var e=t.ref,a=t.refCleanup;if(e!==null)if(typeof a=="function")try{a()}catch(u){ht(t,l,u)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof e=="function")try{e(null)}catch(u){ht(t,l,u)}else e.current=null}function ys(t){var l=t.type,e=t.memoizedProps,a=t.stateNode;try{t:switch(l){case"button":case"input":case"select":case"textarea":e.autoFocus&&a.focus();break t;case"img":e.src?a.src=e.src:e.srcSet&&(a.srcset=e.srcSet)}}catch(u){ht(t,t.return,u)}}function Cc(t,l,e){try{var a=t.stateNode;em(a,t.type,e,l),a[el]=l}catch(u){ht(t,t.return,u)}}function ms(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&Te(t.type)||t.tag===4}function jc(t){t:for(;;){for(;t.sibling===null;){if(t.return===null||ms(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&Te(t.type)||t.flags&2||t.child===null||t.tag===4)continue t;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function Rc(t,l,e){var a=t.tag;if(a===5||a===6)t=t.stateNode,l?(e.nodeType===9?e.body:e.nodeName==="HTML"?e.ownerDocument.body:e).insertBefore(t,l):(l=e.nodeType===9?e.body:e.nodeName==="HTML"?e.ownerDocument.body:e,l.appendChild(t),e=e._reactRootContainer,e!=null||l.onclick!==null||(l.onclick=Xl));else if(a!==4&&(a===27&&Te(t.type)&&(e=t.stateNode,l=null),t=t.child,t!==null))for(Rc(t,l,e),t=t.sibling;t!==null;)Rc(t,l,e),t=t.sibling}function Mn(t,l,e){var a=t.tag;if(a===5||a===6)t=t.stateNode,l?e.insertBefore(t,l):e.appendChild(t);else if(a!==4&&(a===27&&Te(t.type)&&(e=t.stateNode),t=t.child,t!==null))for(Mn(t,l,e),t=t.sibling;t!==null;)Mn(t,l,e),t=t.sibling}function hs(t){var l=t.stateNode,e=t.memoizedProps;try{for(var a=t.type,u=l.attributes;u.length;)l.removeAttributeNode(u[0]);Jt(l,a,e),l[wt]=t,l[el]=e}catch(n){ht(t,t.return,n)}}var $l=!1,Yt=!1,Hc=!1,gs=typeof WeakSet=="function"?WeakSet:Set,Qt=null;function Hy(t,l){if(t=t.containerInfo,ef=Jn,t=_r(t),Oi(t)){if("selectionStart"in t)var e={start:t.selectionStart,end:t.selectionEnd};else t:{e=(e=t.ownerDocument)&&e.defaultView||window;var a=e.getSelection&&e.getSelection();if(a&&a.rangeCount!==0){e=a.anchorNode;var u=a.anchorOffset,n=a.focusNode;a=a.focusOffset;try{e.nodeType,n.nodeType}catch{e=null;break t}var i=0,c=-1,o=-1,h=0,z=0,E=t,g=null;l:for(;;){for(var b;E!==e||u!==0&&E.nodeType!==3||(c=i+u),E!==n||a!==0&&E.nodeType!==3||(o=i+a),E.nodeType===3&&(i+=E.nodeValue.length),(b=E.firstChild)!==null;)g=E,E=b;for(;;){if(E===t)break l;if(g===e&&++h===u&&(c=i),g===n&&++z===a&&(o=i),(b=E.nextSibling)!==null)break;E=g,g=E.parentNode}E=b}e=c===-1||o===-1?null:{start:c,end:o}}else e=null}e=e||{start:0,end:0}}else e=null;for(af={focusedElem:t,selectionRange:e},Jn=!1,Qt=l;Qt!==null;)if(l=Qt,t=l.child,(l.subtreeFlags&1028)!==0&&t!==null)t.return=l,Qt=t;else for(;Qt!==null;){switch(l=Qt,n=l.alternate,t=l.flags,l.tag){case 0:if((t&4)!==0&&(t=l.updateQueue,t=t!==null?t.events:null,t!==null))for(e=0;e title"))),Jt(n,a,e),n[wt]=t,Xt(n),a=n;break t;case"link":var i=zd("link","href",u).get(a+(e.href||""));if(i){for(var c=0;cpt&&(i=pt,pt=w,w=i);var y=Ar(c,w),d=Ar(c,pt);if(y&&d&&(b.rangeCount!==1||b.anchorNode!==y.node||b.anchorOffset!==y.offset||b.focusNode!==d.node||b.focusOffset!==d.offset)){var m=E.createRange();m.setStart(y.node,y.offset),b.removeAllRanges(),w>pt?(b.addRange(m),b.extend(d.node,d.offset)):(m.setEnd(d.node,d.offset),b.addRange(m))}}}}for(E=[],b=c;b=b.parentNode;)b.nodeType===1&&E.push({element:b,left:b.scrollLeft,top:b.scrollTop});for(typeof c.focus=="function"&&c.focus(),c=0;ce?32:e,x.T=null,e=Zc,Zc=null;var n=Se,i=le;if(Gt=0,_a=Se=null,le=0,(ot&6)!==0)throw Error(f(331));var c=ot;if(ot|=4,_s(n.current),Es(n,n.current,i,e),ot=c,Su(0,!1),ol&&typeof ol.onPostCommitFiberRoot=="function")try{ol.onPostCommitFiberRoot(Ya,n)}catch{}return!0}finally{C.p=u,x.T=a,Vs(t,l)}}function Js(t,l,e){l=zl(e,l),l=xc(t.stateNode,l,2),t=ye(t,l,2),t!==null&&(Xa(t,2),Yl(t))}function ht(t,l,e){if(t.tag===3)Js(t,t,e);else for(;l!==null;){if(l.tag===3){Js(l,t,e);break}else if(l.tag===1){var a=l.stateNode;if(typeof l.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(pe===null||!pe.has(a))){t=zl(e,t),e=ko(2),a=ye(l,e,2),a!==null&&(Wo(e,a,l,t),Xa(a,2),Yl(a));break}}l=l.return}}function Kc(t,l,e){var a=t.pingCache;if(a===null){a=t.pingCache=new Yy;var u=new Set;a.set(l,u)}else u=a.get(l),u===void 0&&(u=new Set,a.set(l,u));u.has(e)||(Yc=!0,u.add(e),t=wy.bind(null,t,l,e),l.then(t,t))}function wy(t,l,e){var a=t.pingCache;a!==null&&a.delete(l),t.pingedLanes|=t.suspendedLanes&e,t.warmLanes&=~e,St===t&&(et&e)===e&&(Dt===4||Dt===3&&(et&62914560)===et&&300>rl()-Nn?(ot&2)===0&&Oa(t,0):Gc|=e,Ma===et&&(Ma=0)),Yl(t)}function ks(t,l){l===0&&(l=Qf()),t=qe(t,l),t!==null&&(Xa(t,l),Yl(t))}function Ly(t){var l=t.memoizedState,e=0;l!==null&&(e=l.retryLane),ks(t,e)}function Vy(t,l){var e=0;switch(t.tag){case 31:case 13:var a=t.stateNode,u=t.memoizedState;u!==null&&(e=u.retryLane);break;case 19:a=t.stateNode;break;case 22:a=t.stateNode._retryCache;break;default:throw Error(f(314))}a!==null&&a.delete(l),ks(t,e)}function Ky(t,l){return ni(t,l)}var Bn=null,Da=null,Jc=!1,qn=!1,kc=!1,ze=0;function Yl(t){t!==Da&&t.next===null&&(Da===null?Bn=Da=t:Da=Da.next=t),qn=!0,Jc||(Jc=!0,ky())}function Su(t,l){if(!kc&&qn){kc=!0;do for(var e=!1,a=Bn;a!==null;){if(t!==0){var u=a.pendingLanes;if(u===0)var n=0;else{var i=a.suspendedLanes,c=a.pingedLanes;n=(1<<31-sl(42|t)+1)-1,n&=u&~(i&~c),n=n&201326741?n&201326741|1:n?n|2:0}n!==0&&(e=!0,Is(a,n))}else n=et,n=Xu(a,a===St?n:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(n&3)===0||Ga(a,n)||(e=!0,Is(a,n));a=a.next}while(e);kc=!1}}function Jy(){Ws()}function Ws(){qn=Jc=!1;var t=0;ze!==0&&um()&&(t=ze);for(var l=rl(),e=null,a=Bn;a!==null;){var u=a.next,n=$s(a,l);n===0?(a.next=null,e===null?Bn=u:e.next=u,u===null&&(Da=e)):(e=a,(t!==0||(n&3)!==0)&&(qn=!0)),a=u}Gt!==0&&Gt!==5||Su(t),ze!==0&&(ze=0)}function $s(t,l){for(var e=t.suspendedLanes,a=t.pingedLanes,u=t.expirationTimes,n=t.pendingLanes&-62914561;0c)break;var z=o.transferSize,E=o.initiatorType;z&&id(E)&&(o=o.responseEnd,i+=z*(o"u"?null:document;function bd(t,l,e){var a=Ua;if(a&&typeof l=="string"&&l){var u=Sl(l);u='link[rel="'+t+'"][href="'+u+'"]',typeof e=="string"&&(u+='[crossorigin="'+e+'"]'),vd.has(u)||(vd.add(u),t={rel:t,crossOrigin:e,href:l},a.querySelector(u)===null&&(l=a.createElement("link"),Jt(l,"link",t),Xt(l),a.head.appendChild(l)))}}function ym(t){ee.D(t),bd("dns-prefetch",t,null)}function mm(t,l){ee.C(t,l),bd("preconnect",t,l)}function hm(t,l,e){ee.L(t,l,e);var a=Ua;if(a&&t&&l){var u='link[rel="preload"][as="'+Sl(l)+'"]';l==="image"&&e&&e.imageSrcSet?(u+='[imagesrcset="'+Sl(e.imageSrcSet)+'"]',typeof e.imageSizes=="string"&&(u+='[imagesizes="'+Sl(e.imageSizes)+'"]')):u+='[href="'+Sl(t)+'"]';var n=u;switch(l){case"style":n=Ca(t);break;case"script":n=ja(t)}Ol.has(n)||(t=H({rel:"preload",href:l==="image"&&e&&e.imageSrcSet?void 0:t,as:l},e),Ol.set(n,t),a.querySelector(u)!==null||l==="style"&&a.querySelector(Eu(n))||l==="script"&&a.querySelector(Au(n))||(l=a.createElement("link"),Jt(l,"link",t),Xt(l),a.head.appendChild(l)))}}function gm(t,l){ee.m(t,l);var e=Ua;if(e&&t){var a=l&&typeof l.as=="string"?l.as:"script",u='link[rel="modulepreload"][as="'+Sl(a)+'"][href="'+Sl(t)+'"]',n=u;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":n=ja(t)}if(!Ol.has(n)&&(t=H({rel:"modulepreload",href:t},l),Ol.set(n,t),e.querySelector(u)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(e.querySelector(Au(n)))return}a=e.createElement("link"),Jt(a,"link",t),Xt(a),e.head.appendChild(a)}}}function vm(t,l,e){ee.S(t,l,e);var a=Ua;if(a&&t){var u=ta(a).hoistableStyles,n=Ca(t);l=l||"default";var i=u.get(n);if(!i){var c={loading:0,preload:null};if(i=a.querySelector(Eu(n)))c.loading=5;else{t=H({rel:"stylesheet",href:t,"data-precedence":l},e),(e=Ol.get(n))&&sf(t,e);var o=i=a.createElement("link");Xt(o),Jt(o,"link",t),o._p=new Promise(function(h,z){o.onload=h,o.onerror=z}),o.addEventListener("load",function(){c.loading|=1}),o.addEventListener("error",function(){c.loading|=2}),c.loading|=4,Zn(i,l,a)}i={type:"stylesheet",instance:i,count:1,state:c},u.set(n,i)}}}function bm(t,l){ee.X(t,l);var e=Ua;if(e&&t){var a=ta(e).hoistableScripts,u=ja(t),n=a.get(u);n||(n=e.querySelector(Au(u)),n||(t=H({src:t,async:!0},l),(l=Ol.get(u))&&df(t,l),n=e.createElement("script"),Xt(n),Jt(n,"link",t),e.head.appendChild(n)),n={type:"script",instance:n,count:1,state:null},a.set(u,n))}}function pm(t,l){ee.M(t,l);var e=Ua;if(e&&t){var a=ta(e).hoistableScripts,u=ja(t),n=a.get(u);n||(n=e.querySelector(Au(u)),n||(t=H({src:t,async:!0,type:"module"},l),(l=Ol.get(u))&&df(t,l),n=e.createElement("script"),Xt(n),Jt(n,"link",t),e.head.appendChild(n)),n={type:"script",instance:n,count:1,state:null},a.set(u,n))}}function pd(t,l,e,a){var u=(u=P.current)?Qn(u):null;if(!u)throw Error(f(446));switch(t){case"meta":case"title":return null;case"style":return typeof e.precedence=="string"&&typeof e.href=="string"?(l=Ca(e.href),e=ta(u).hoistableStyles,a=e.get(l),a||(a={type:"style",instance:null,count:0,state:null},e.set(l,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(e.rel==="stylesheet"&&typeof e.href=="string"&&typeof e.precedence=="string"){t=Ca(e.href);var n=ta(u).hoistableStyles,i=n.get(t);if(i||(u=u.ownerDocument||u,i={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},n.set(t,i),(n=u.querySelector(Eu(t)))&&!n._p&&(i.instance=n,i.state.loading=5),Ol.has(t)||(e={rel:"preload",as:"style",href:e.href,crossOrigin:e.crossOrigin,integrity:e.integrity,media:e.media,hrefLang:e.hrefLang,referrerPolicy:e.referrerPolicy},Ol.set(t,e),n||Sm(u,t,e,i.state))),l&&a===null)throw Error(f(528,""));return i}if(l&&a!==null)throw Error(f(529,""));return null;case"script":return l=e.async,e=e.src,typeof e=="string"&&l&&typeof l!="function"&&typeof l!="symbol"?(l=ja(e),e=ta(u).hoistableScripts,a=e.get(l),a||(a={type:"script",instance:null,count:0,state:null},e.set(l,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(f(444,t))}}function Ca(t){return'href="'+Sl(t)+'"'}function Eu(t){return'link[rel="stylesheet"]['+t+"]"}function Sd(t){return H({},t,{"data-precedence":t.precedence,precedence:null})}function Sm(t,l,e,a){t.querySelector('link[rel="preload"][as="style"]['+l+"]")?a.loading=1:(l=t.createElement("link"),a.preload=l,l.addEventListener("load",function(){return a.loading|=1}),l.addEventListener("error",function(){return a.loading|=2}),Jt(l,"link",e),Xt(l),t.head.appendChild(l))}function ja(t){return'[src="'+Sl(t)+'"]'}function Au(t){return"script[async]"+t}function xd(t,l,e){if(l.count++,l.instance===null)switch(l.type){case"style":var a=t.querySelector('style[data-href~="'+Sl(e.href)+'"]');if(a)return l.instance=a,Xt(a),a;var u=H({},e,{"data-href":e.href,"data-precedence":e.precedence,href:null,precedence:null});return a=(t.ownerDocument||t).createElement("style"),Xt(a),Jt(a,"style",u),Zn(a,e.precedence,t),l.instance=a;case"stylesheet":u=Ca(e.href);var n=t.querySelector(Eu(u));if(n)return l.state.loading|=4,l.instance=n,Xt(n),n;a=Sd(e),(u=Ol.get(u))&&sf(a,u),n=(t.ownerDocument||t).createElement("link"),Xt(n);var i=n;return i._p=new Promise(function(c,o){i.onload=c,i.onerror=o}),Jt(n,"link",a),l.state.loading|=4,Zn(n,e.precedence,t),l.instance=n;case"script":return n=ja(e.src),(u=t.querySelector(Au(n)))?(l.instance=u,Xt(u),u):(a=e,(u=Ol.get(n))&&(a=H({},e),df(a,u)),t=t.ownerDocument||t,u=t.createElement("script"),Xt(u),Jt(u,"link",a),t.head.appendChild(u),l.instance=u);case"void":return null;default:throw Error(f(443,l.type))}else l.type==="stylesheet"&&(l.state.loading&4)===0&&(a=l.instance,l.state.loading|=4,Zn(a,e.precedence,t));return l.instance}function Zn(t,l,e){for(var a=e.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),u=a.length?a[a.length-1]:null,n=u,i=0;i title"):null)}function xm(t,l,e){if(e===1||l.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof l.precedence!="string"||typeof l.href!="string"||l.href==="")break;return!0;case"link":if(typeof l.rel!="string"||typeof l.href!="string"||l.href===""||l.onLoad||l.onError)break;return l.rel==="stylesheet"?(t=l.disabled,typeof l.precedence=="string"&&t==null):!0;case"script":if(l.async&&typeof l.async!="function"&&typeof l.async!="symbol"&&!l.onLoad&&!l.onError&&l.src&&typeof l.src=="string")return!0}return!1}function Ed(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function zm(t,l,e,a){if(e.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(e.state.loading&4)===0){if(e.instance===null){var u=Ca(a.href),n=l.querySelector(Eu(u));if(n){l=n._p,l!==null&&typeof l=="object"&&typeof l.then=="function"&&(t.count++,t=Ln.bind(t),l.then(t,t)),e.state.loading|=4,e.instance=n,Xt(n);return}n=l.ownerDocument||l,a=Sd(a),(u=Ol.get(u))&&sf(a,u),n=n.createElement("link"),Xt(n);var i=n;i._p=new Promise(function(c,o){i.onload=c,i.onerror=o}),Jt(n,"link",a),e.instance=n}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(e,l),(l=e.state.preload)&&(e.state.loading&3)===0&&(t.count++,e=Ln.bind(t),l.addEventListener("load",e),l.addEventListener("error",e))}}var yf=0;function Tm(t,l){return t.stylesheets&&t.count===0&&Kn(t,t.stylesheets),0yf?50:800)+l);return t.unsuspend=e,function(){t.unsuspend=null,clearTimeout(a),clearTimeout(u)}}:null}function Ln(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Kn(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var Vn=null;function Kn(t,l){t.stylesheets=null,t.unsuspend!==null&&(t.count++,Vn=new Map,l.forEach(Em,t),Vn=null,Ln.call(t))}function Em(t,l){if(!(l.state.loading&4)){var e=Vn.get(t);if(e)var a=e.get(null);else{e=new Map,Vn.set(t,e);for(var u=t.querySelectorAll("link[data-precedence],style[data-precedence]"),n=0;n"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(r)}catch(v){console.error(v)}}return r(),zf.exports=Xm(),zf.exports}var Zm=Qm();const wm=r=>r.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Pd=(...r)=>r.filter((v,S,f)=>!!v&&v.trim()!==""&&f.indexOf(v)===S).join(" ").trim();var Lm={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};const Vm=xt.forwardRef(({color:r="currentColor",size:v=24,strokeWidth:S=2,absoluteStrokeWidth:f,className:_="",children:O,iconNode:D,...U},N)=>xt.createElement("svg",{ref:N,...Lm,width:v,height:v,stroke:r,strokeWidth:f?Number(S)*24/Number(v):S,className:Pd("lucide",_),...U},[...D.map(([p,R])=>xt.createElement(p,R)),...Array.isArray(O)?O:[O]]));const Nl=(r,v)=>{const S=xt.forwardRef(({className:f,..._},O)=>xt.createElement(Vm,{ref:O,iconNode:v,className:Pd(`lucide-${wm(r)}`,f),..._}));return S.displayName=`${r}`,S};const Km=Nl("Binary",[["rect",{x:"14",y:"14",width:"4",height:"6",rx:"2",key:"p02svl"}],["rect",{x:"6",y:"4",width:"4",height:"6",rx:"2",key:"xm4xkj"}],["path",{d:"M6 20h4",key:"1i6q5t"}],["path",{d:"M14 10h4",key:"ru81e7"}],["path",{d:"M6 14h2v6",key:"16z9wg"}],["path",{d:"M14 4h2v6",key:"1idq9u"}]]);const Jm=Nl("BookText",[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20",key:"k3hazp"}],["path",{d:"M8 11h8",key:"vwpz6n"}],["path",{d:"M8 7h6",key:"1f0q6e"}]]);const km=Nl("EyeOff",[["path",{d:"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49",key:"ct8e1f"}],["path",{d:"M14.084 14.158a3 3 0 0 1-4.242-4.242",key:"151rxh"}],["path",{d:"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143",key:"13bj9a"}],["path",{d:"m2 2 20 20",key:"1ooewy"}]]);const Wm=Nl("Eye",[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",key:"1nclc0"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);const $m=Nl("Globe",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",key:"13o1zl"}],["path",{d:"M2 12h20",key:"9i4pu4"}]]);const kd=Nl("LoaderCircle",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);const Fm=Nl("Lock",[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]]);const Im=Nl("LogIn",[["path",{d:"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4",key:"u53s6r"}],["polyline",{points:"10 17 15 12 10 7",key:"1ail0h"}],["line",{x1:"15",x2:"3",y1:"12",y2:"12",key:"v6grx8"}]]);const Pm=Nl("RotateCw",[["path",{d:"M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8",key:"1p45f6"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}]]);const th=Nl("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);const lh=Nl("Waypoints",[["circle",{cx:"12",cy:"4.5",r:"2.5",key:"r5ysbb"}],["path",{d:"m10.2 6.3-3.9 3.9",key:"1nzqf6"}],["circle",{cx:"4.5",cy:"12",r:"2.5",key:"jydg6v"}],["path",{d:"M7 12h10",key:"b7w52i"}],["circle",{cx:"19.5",cy:"12",r:"2.5",key:"1piiel"}],["path",{d:"m13.8 17.7 3.9-3.9",key:"1wyg1y"}],["circle",{cx:"12",cy:"19.5",r:"2.5",key:"13o1pw"}]]);const eh=Nl("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]);function t0(){return globalThis.__DATA__??{}}function l0(r){var v,S,f="";if(typeof r=="string"||typeof r=="number")f+=r;else if(typeof r=="object")if(Array.isArray(r)){var _=r.length;for(v=0;v<_;v++)r[v]&&(S=l0(r[v]))&&(f&&(f+=" "),f+=S)}else for(S in r)r[S]&&(f&&(f+=" "),f+=S);return f}function ah(){for(var r,v,S=0,f="",_=arguments.length;S<_;S++)(r=arguments[S])&&(v=l0(r))&&(f&&(f+=" "),f+=v);return f}const Hf="-",uh=r=>{const v=ih(r),{conflictingClassGroups:S,conflictingClassGroupModifiers:f}=r;return{getClassGroupId:D=>{const U=D.split(Hf);return U[0]===""&&U.length!==1&&U.shift(),e0(U,v)||nh(D)},getConflictingClassGroupIds:(D,U)=>{const N=S[D]||[];return U&&f[D]?[...N,...f[D]]:N}}},e0=(r,v)=>{if(r.length===0)return v.classGroupId;const S=r[0],f=v.nextPart.get(S),_=f?e0(r.slice(1),f):void 0;if(_)return _;if(v.validators.length===0)return;const O=r.join(Hf);return v.validators.find(({validator:D})=>D(O))?.classGroupId},Wd=/^\[(.+)\]$/,nh=r=>{if(Wd.test(r)){const v=Wd.exec(r)[1],S=v?.substring(0,v.indexOf(":"));if(S)return"arbitrary.."+S}},ih=r=>{const{theme:v,prefix:S}=r,f={nextPart:new Map,validators:[]};return fh(Object.entries(r.classGroups),S).forEach(([O,D])=>{Df(D,f,O,v)}),f},Df=(r,v,S,f)=>{r.forEach(_=>{if(typeof _=="string"){const O=_===""?v:$d(v,_);O.classGroupId=S;return}if(typeof _=="function"){if(ch(_)){Df(_(f),v,S,f);return}v.validators.push({validator:_,classGroupId:S});return}Object.entries(_).forEach(([O,D])=>{Df(D,$d(v,O),S,f)})})},$d=(r,v)=>{let S=r;return v.split(Hf).forEach(f=>{S.nextPart.has(f)||S.nextPart.set(f,{nextPart:new Map,validators:[]}),S=S.nextPart.get(f)}),S},ch=r=>r.isThemeGetter,fh=(r,v)=>v?r.map(([S,f])=>{const _=f.map(O=>typeof O=="string"?v+O:typeof O=="object"?Object.fromEntries(Object.entries(O).map(([D,U])=>[v+D,U])):O);return[S,_]}):r,rh=r=>{if(r<1)return{get:()=>{},set:()=>{}};let v=0,S=new Map,f=new Map;const _=(O,D)=>{S.set(O,D),v++,v>r&&(v=0,f=S,S=new Map)};return{get(O){let D=S.get(O);if(D!==void 0)return D;if((D=f.get(O))!==void 0)return _(O,D),D},set(O,D){S.has(O)?S.set(O,D):_(O,D)}}},a0="!",oh=r=>{const{separator:v,experimentalParseClassName:S}=r,f=v.length===1,_=v[0],O=v.length,D=U=>{const N=[];let p=0,R=0,H;for(let Q=0;QR?H-R:void 0;return{modifiers:N,hasImportantModifier:st,baseClassName:ct,maybePostfixModifierPosition:G}};return S?U=>S({className:U,parseClassName:D}):D},sh=r=>{if(r.length<=1)return r;const v=[];let S=[];return r.forEach(f=>{f[0]==="["?(v.push(...S.sort(),f),S=[]):S.push(f)}),v.push(...S.sort()),v},dh=r=>({cache:rh(r.cacheSize),parseClassName:oh(r),...uh(r)}),yh=/\s+/,mh=(r,v)=>{const{parseClassName:S,getClassGroupId:f,getConflictingClassGroupIds:_}=v,O=[],D=r.trim().split(yh);let U="";for(let N=D.length-1;N>=0;N-=1){const p=D[N],{modifiers:R,hasImportantModifier:H,baseClassName:V,maybePostfixModifierPosition:st}=S(p);let ct=!!st,G=f(ct?V.substring(0,st):V);if(!G){if(!ct){U=p+(U.length>0?" "+U:U);continue}if(G=f(V),!G){U=p+(U.length>0?" "+U:U);continue}ct=!1}const Q=sh(R).join(":"),L=H?Q+a0:Q,gt=L+G;if(O.includes(gt))continue;O.push(gt);const zt=_(G,ct);for(let _t=0;_t0?" "+U:U)}return U};function hh(){let r=0,v,S,f="";for(;r{if(typeof r=="string")return r;let v,S="";for(let f=0;fH(R),r());return S=dh(p),f=S.cache.get,_=S.cache.set,O=U,U(N)}function U(N){const p=f(N);if(p)return p;const R=mh(N,S);return _(N,R),R}return function(){return O(hh.apply(null,arguments))}}const At=r=>{const v=S=>S[r]||[];return v.isThemeGetter=!0,v},n0=/^\[(?:([a-z-]+):)?(.+)\]$/i,vh=/^\d+\/\d+$/,bh=new Set(["px","full","screen"]),ph=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,Sh=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,xh=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,zh=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,Th=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,ae=r=>Ha(r)||bh.has(r)||vh.test(r),Ne=r=>Ba(r,"length",Uh),Ha=r=>!!r&&!Number.isNaN(Number(r)),Mf=r=>Ba(r,"number",Ha),Cu=r=>!!r&&Number.isInteger(Number(r)),Eh=r=>r.endsWith("%")&&Ha(r.slice(0,-1)),F=r=>n0.test(r),De=r=>ph.test(r),Ah=new Set(["length","size","percentage"]),Mh=r=>Ba(r,Ah,i0),_h=r=>Ba(r,"position",i0),Oh=new Set(["image","url"]),Nh=r=>Ba(r,Oh,jh),Dh=r=>Ba(r,"",Ch),ju=()=>!0,Ba=(r,v,S)=>{const f=n0.exec(r);return f?f[1]?typeof v=="string"?f[1]===v:v.has(f[1]):S(f[2]):!1},Uh=r=>Sh.test(r)&&!xh.test(r),i0=()=>!1,Ch=r=>zh.test(r),jh=r=>Th.test(r),Rh=()=>{const r=At("colors"),v=At("spacing"),S=At("blur"),f=At("brightness"),_=At("borderColor"),O=At("borderRadius"),D=At("borderSpacing"),U=At("borderWidth"),N=At("contrast"),p=At("grayscale"),R=At("hueRotate"),H=At("invert"),V=At("gap"),st=At("gradientColorStops"),ct=At("gradientColorStopPositions"),G=At("inset"),Q=At("margin"),L=At("opacity"),gt=At("padding"),zt=At("saturate"),_t=At("scale"),it=At("sepia"),Ot=At("skew"),J=At("space"),Rt=At("translate"),It=()=>["auto","contain","none"],jl=()=>["auto","hidden","clip","visible","scroll"],Pt=()=>["auto",F,v],I=()=>[F,v],Rl=()=>["",ae,Ne],tl=()=>["auto",Ha,F],ll=()=>["bottom","center","left","left-bottom","left-top","right","right-bottom","right-top","top"],x=()=>["solid","dashed","dotted","double","none"],C=()=>["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"],Z=()=>["start","end","center","between","around","evenly","stretch"],nt=()=>["","0",F],dt=()=>["auto","avoid","all","avoid-page","page","left","right","column"],s=()=>[Ha,F];return{cacheSize:500,separator:":",theme:{colors:[ju],spacing:[ae,Ne],blur:["none","",De,F],brightness:s(),borderColor:[r],borderRadius:["none","","full",De,F],borderSpacing:I(),borderWidth:Rl(),contrast:s(),grayscale:nt(),hueRotate:s(),invert:nt(),gap:I(),gradientColorStops:[r],gradientColorStopPositions:[Eh,Ne],inset:Pt(),margin:Pt(),opacity:s(),padding:I(),saturate:s(),scale:s(),sepia:nt(),skew:s(),space:I(),translate:I()},classGroups:{aspect:[{aspect:["auto","square","video",F]}],container:["container"],columns:[{columns:[De]}],"break-after":[{"break-after":dt()}],"break-before":[{"break-before":dt()}],"break-inside":[{"break-inside":["auto","avoid","avoid-page","avoid-column"]}],"box-decoration":[{"box-decoration":["slice","clone"]}],box:[{box:["border","content"]}],display:["block","inline-block","inline","flex","inline-flex","table","inline-table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row-group","table-row","flow-root","grid","inline-grid","contents","list-item","hidden"],float:[{float:["right","left","none","start","end"]}],clear:[{clear:["left","right","both","none","start","end"]}],isolation:["isolate","isolation-auto"],"object-fit":[{object:["contain","cover","fill","none","scale-down"]}],"object-position":[{object:[...ll(),F]}],overflow:[{overflow:jl()}],"overflow-x":[{"overflow-x":jl()}],"overflow-y":[{"overflow-y":jl()}],overscroll:[{overscroll:It()}],"overscroll-x":[{"overscroll-x":It()}],"overscroll-y":[{"overscroll-y":It()}],position:["static","fixed","absolute","relative","sticky"],inset:[{inset:[G]}],"inset-x":[{"inset-x":[G]}],"inset-y":[{"inset-y":[G]}],start:[{start:[G]}],end:[{end:[G]}],top:[{top:[G]}],right:[{right:[G]}],bottom:[{bottom:[G]}],left:[{left:[G]}],visibility:["visible","invisible","collapse"],z:[{z:["auto",Cu,F]}],basis:[{basis:Pt()}],"flex-direction":[{flex:["row","row-reverse","col","col-reverse"]}],"flex-wrap":[{flex:["wrap","wrap-reverse","nowrap"]}],flex:[{flex:["1","auto","initial","none",F]}],grow:[{grow:nt()}],shrink:[{shrink:nt()}],order:[{order:["first","last","none",Cu,F]}],"grid-cols":[{"grid-cols":[ju]}],"col-start-end":[{col:["auto",{span:["full",Cu,F]},F]}],"col-start":[{"col-start":tl()}],"col-end":[{"col-end":tl()}],"grid-rows":[{"grid-rows":[ju]}],"row-start-end":[{row:["auto",{span:[Cu,F]},F]}],"row-start":[{"row-start":tl()}],"row-end":[{"row-end":tl()}],"grid-flow":[{"grid-flow":["row","col","dense","row-dense","col-dense"]}],"auto-cols":[{"auto-cols":["auto","min","max","fr",F]}],"auto-rows":[{"auto-rows":["auto","min","max","fr",F]}],gap:[{gap:[V]}],"gap-x":[{"gap-x":[V]}],"gap-y":[{"gap-y":[V]}],"justify-content":[{justify:["normal",...Z()]}],"justify-items":[{"justify-items":["start","end","center","stretch"]}],"justify-self":[{"justify-self":["auto","start","end","center","stretch"]}],"align-content":[{content:["normal",...Z(),"baseline"]}],"align-items":[{items:["start","end","center","baseline","stretch"]}],"align-self":[{self:["auto","start","end","center","stretch","baseline"]}],"place-content":[{"place-content":[...Z(),"baseline"]}],"place-items":[{"place-items":["start","end","center","baseline","stretch"]}],"place-self":[{"place-self":["auto","start","end","center","stretch"]}],p:[{p:[gt]}],px:[{px:[gt]}],py:[{py:[gt]}],ps:[{ps:[gt]}],pe:[{pe:[gt]}],pt:[{pt:[gt]}],pr:[{pr:[gt]}],pb:[{pb:[gt]}],pl:[{pl:[gt]}],m:[{m:[Q]}],mx:[{mx:[Q]}],my:[{my:[Q]}],ms:[{ms:[Q]}],me:[{me:[Q]}],mt:[{mt:[Q]}],mr:[{mr:[Q]}],mb:[{mb:[Q]}],ml:[{ml:[Q]}],"space-x":[{"space-x":[J]}],"space-x-reverse":["space-x-reverse"],"space-y":[{"space-y":[J]}],"space-y-reverse":["space-y-reverse"],w:[{w:["auto","min","max","fit","svw","lvw","dvw",F,v]}],"min-w":[{"min-w":[F,v,"min","max","fit"]}],"max-w":[{"max-w":[F,v,"none","full","min","max","fit","prose",{screen:[De]},De]}],h:[{h:[F,v,"auto","min","max","fit","svh","lvh","dvh"]}],"min-h":[{"min-h":[F,v,"min","max","fit","svh","lvh","dvh"]}],"max-h":[{"max-h":[F,v,"min","max","fit","svh","lvh","dvh"]}],size:[{size:[F,v,"auto","min","max","fit"]}],"font-size":[{text:["base",De,Ne]}],"font-smoothing":["antialiased","subpixel-antialiased"],"font-style":["italic","not-italic"],"font-weight":[{font:["thin","extralight","light","normal","medium","semibold","bold","extrabold","black",Mf]}],"font-family":[{font:[ju]}],"fvn-normal":["normal-nums"],"fvn-ordinal":["ordinal"],"fvn-slashed-zero":["slashed-zero"],"fvn-figure":["lining-nums","oldstyle-nums"],"fvn-spacing":["proportional-nums","tabular-nums"],"fvn-fraction":["diagonal-fractions","stacked-fractions"],tracking:[{tracking:["tighter","tight","normal","wide","wider","widest",F]}],"line-clamp":[{"line-clamp":["none",Ha,Mf]}],leading:[{leading:["none","tight","snug","normal","relaxed","loose",ae,F]}],"list-image":[{"list-image":["none",F]}],"list-style-type":[{list:["none","disc","decimal",F]}],"list-style-position":[{list:["inside","outside"]}],"placeholder-color":[{placeholder:[r]}],"placeholder-opacity":[{"placeholder-opacity":[L]}],"text-alignment":[{text:["left","center","right","justify","start","end"]}],"text-color":[{text:[r]}],"text-opacity":[{"text-opacity":[L]}],"text-decoration":["underline","overline","line-through","no-underline"],"text-decoration-style":[{decoration:[...x(),"wavy"]}],"text-decoration-thickness":[{decoration:["auto","from-font",ae,Ne]}],"underline-offset":[{"underline-offset":["auto",ae,F]}],"text-decoration-color":[{decoration:[r]}],"text-transform":["uppercase","lowercase","capitalize","normal-case"],"text-overflow":["truncate","text-ellipsis","text-clip"],"text-wrap":[{text:["wrap","nowrap","balance","pretty"]}],indent:[{indent:I()}],"vertical-align":[{align:["baseline","top","middle","bottom","text-top","text-bottom","sub","super",F]}],whitespace:[{whitespace:["normal","nowrap","pre","pre-line","pre-wrap","break-spaces"]}],break:[{break:["normal","words","all","keep"]}],hyphens:[{hyphens:["none","manual","auto"]}],content:[{content:["none",F]}],"bg-attachment":[{bg:["fixed","local","scroll"]}],"bg-clip":[{"bg-clip":["border","padding","content","text"]}],"bg-opacity":[{"bg-opacity":[L]}],"bg-origin":[{"bg-origin":["border","padding","content"]}],"bg-position":[{bg:[...ll(),_h]}],"bg-repeat":[{bg:["no-repeat",{repeat:["","x","y","round","space"]}]}],"bg-size":[{bg:["auto","cover","contain",Mh]}],"bg-image":[{bg:["none",{"gradient-to":["t","tr","r","br","b","bl","l","tl"]},Nh]}],"bg-color":[{bg:[r]}],"gradient-from-pos":[{from:[ct]}],"gradient-via-pos":[{via:[ct]}],"gradient-to-pos":[{to:[ct]}],"gradient-from":[{from:[st]}],"gradient-via":[{via:[st]}],"gradient-to":[{to:[st]}],rounded:[{rounded:[O]}],"rounded-s":[{"rounded-s":[O]}],"rounded-e":[{"rounded-e":[O]}],"rounded-t":[{"rounded-t":[O]}],"rounded-r":[{"rounded-r":[O]}],"rounded-b":[{"rounded-b":[O]}],"rounded-l":[{"rounded-l":[O]}],"rounded-ss":[{"rounded-ss":[O]}],"rounded-se":[{"rounded-se":[O]}],"rounded-ee":[{"rounded-ee":[O]}],"rounded-es":[{"rounded-es":[O]}],"rounded-tl":[{"rounded-tl":[O]}],"rounded-tr":[{"rounded-tr":[O]}],"rounded-br":[{"rounded-br":[O]}],"rounded-bl":[{"rounded-bl":[O]}],"border-w":[{border:[U]}],"border-w-x":[{"border-x":[U]}],"border-w-y":[{"border-y":[U]}],"border-w-s":[{"border-s":[U]}],"border-w-e":[{"border-e":[U]}],"border-w-t":[{"border-t":[U]}],"border-w-r":[{"border-r":[U]}],"border-w-b":[{"border-b":[U]}],"border-w-l":[{"border-l":[U]}],"border-opacity":[{"border-opacity":[L]}],"border-style":[{border:[...x(),"hidden"]}],"divide-x":[{"divide-x":[U]}],"divide-x-reverse":["divide-x-reverse"],"divide-y":[{"divide-y":[U]}],"divide-y-reverse":["divide-y-reverse"],"divide-opacity":[{"divide-opacity":[L]}],"divide-style":[{divide:x()}],"border-color":[{border:[_]}],"border-color-x":[{"border-x":[_]}],"border-color-y":[{"border-y":[_]}],"border-color-s":[{"border-s":[_]}],"border-color-e":[{"border-e":[_]}],"border-color-t":[{"border-t":[_]}],"border-color-r":[{"border-r":[_]}],"border-color-b":[{"border-b":[_]}],"border-color-l":[{"border-l":[_]}],"divide-color":[{divide:[_]}],"outline-style":[{outline:["",...x()]}],"outline-offset":[{"outline-offset":[ae,F]}],"outline-w":[{outline:[ae,Ne]}],"outline-color":[{outline:[r]}],"ring-w":[{ring:Rl()}],"ring-w-inset":["ring-inset"],"ring-color":[{ring:[r]}],"ring-opacity":[{"ring-opacity":[L]}],"ring-offset-w":[{"ring-offset":[ae,Ne]}],"ring-offset-color":[{"ring-offset":[r]}],shadow:[{shadow:["","inner","none",De,Dh]}],"shadow-color":[{shadow:[ju]}],opacity:[{opacity:[L]}],"mix-blend":[{"mix-blend":[...C(),"plus-lighter","plus-darker"]}],"bg-blend":[{"bg-blend":C()}],filter:[{filter:["","none"]}],blur:[{blur:[S]}],brightness:[{brightness:[f]}],contrast:[{contrast:[N]}],"drop-shadow":[{"drop-shadow":["","none",De,F]}],grayscale:[{grayscale:[p]}],"hue-rotate":[{"hue-rotate":[R]}],invert:[{invert:[H]}],saturate:[{saturate:[zt]}],sepia:[{sepia:[it]}],"backdrop-filter":[{"backdrop-filter":["","none"]}],"backdrop-blur":[{"backdrop-blur":[S]}],"backdrop-brightness":[{"backdrop-brightness":[f]}],"backdrop-contrast":[{"backdrop-contrast":[N]}],"backdrop-grayscale":[{"backdrop-grayscale":[p]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[R]}],"backdrop-invert":[{"backdrop-invert":[H]}],"backdrop-opacity":[{"backdrop-opacity":[L]}],"backdrop-saturate":[{"backdrop-saturate":[zt]}],"backdrop-sepia":[{"backdrop-sepia":[it]}],"border-collapse":[{border:["collapse","separate"]}],"border-spacing":[{"border-spacing":[D]}],"border-spacing-x":[{"border-spacing-x":[D]}],"border-spacing-y":[{"border-spacing-y":[D]}],"table-layout":[{table:["auto","fixed"]}],caption:[{caption:["top","bottom"]}],transition:[{transition:["none","all","","colors","opacity","shadow","transform",F]}],duration:[{duration:s()}],ease:[{ease:["linear","in","out","in-out",F]}],delay:[{delay:s()}],animate:[{animate:["none","spin","ping","pulse","bounce",F]}],transform:[{transform:["","gpu","none"]}],scale:[{scale:[_t]}],"scale-x":[{"scale-x":[_t]}],"scale-y":[{"scale-y":[_t]}],rotate:[{rotate:[Cu,F]}],"translate-x":[{"translate-x":[Rt]}],"translate-y":[{"translate-y":[Rt]}],"skew-x":[{"skew-x":[Ot]}],"skew-y":[{"skew-y":[Ot]}],"transform-origin":[{origin:["center","top","top-right","right","bottom-right","bottom","bottom-left","left","top-left",F]}],accent:[{accent:["auto",r]}],appearance:[{appearance:["none","auto"]}],cursor:[{cursor:["auto","default","pointer","wait","text","move","help","not-allowed","none","context-menu","progress","cell","crosshair","vertical-text","alias","copy","no-drop","grab","grabbing","all-scroll","col-resize","row-resize","n-resize","e-resize","s-resize","w-resize","ne-resize","nw-resize","se-resize","sw-resize","ew-resize","ns-resize","nesw-resize","nwse-resize","zoom-in","zoom-out",F]}],"caret-color":[{caret:[r]}],"pointer-events":[{"pointer-events":["none","auto"]}],resize:[{resize:["none","y","x",""]}],"scroll-behavior":[{scroll:["auto","smooth"]}],"scroll-m":[{"scroll-m":I()}],"scroll-mx":[{"scroll-mx":I()}],"scroll-my":[{"scroll-my":I()}],"scroll-ms":[{"scroll-ms":I()}],"scroll-me":[{"scroll-me":I()}],"scroll-mt":[{"scroll-mt":I()}],"scroll-mr":[{"scroll-mr":I()}],"scroll-mb":[{"scroll-mb":I()}],"scroll-ml":[{"scroll-ml":I()}],"scroll-p":[{"scroll-p":I()}],"scroll-px":[{"scroll-px":I()}],"scroll-py":[{"scroll-py":I()}],"scroll-ps":[{"scroll-ps":I()}],"scroll-pe":[{"scroll-pe":I()}],"scroll-pt":[{"scroll-pt":I()}],"scroll-pr":[{"scroll-pr":I()}],"scroll-pb":[{"scroll-pb":I()}],"scroll-pl":[{"scroll-pl":I()}],"snap-align":[{snap:["start","end","center","align-none"]}],"snap-stop":[{snap:["normal","always"]}],"snap-type":[{snap:["none","x","y","both"]}],"snap-strictness":[{snap:["mandatory","proximity"]}],touch:[{touch:["auto","none","manipulation"]}],"touch-x":[{"touch-pan":["x","left","right"]}],"touch-y":[{"touch-pan":["y","up","down"]}],"touch-pz":["touch-pinch-zoom"],select:[{select:["none","text","all","auto"]}],"will-change":[{"will-change":["auto","scroll","contents","transform",F]}],fill:[{fill:[r,"none"]}],"stroke-w":[{stroke:[ae,Ne,Mf]}],stroke:[{stroke:[r,"none"]}],sr:["sr-only","not-sr-only"],"forced-color-adjust":[{"forced-color-adjust":["auto","none"]}]},conflictingClassGroups:{overflow:["overflow-x","overflow-y"],overscroll:["overscroll-x","overscroll-y"],inset:["inset-x","inset-y","start","end","top","right","bottom","left"],"inset-x":["right","left"],"inset-y":["top","bottom"],flex:["basis","grow","shrink"],gap:["gap-x","gap-y"],p:["px","py","ps","pe","pt","pr","pb","pl"],px:["pr","pl"],py:["pt","pb"],m:["mx","my","ms","me","mt","mr","mb","ml"],mx:["mr","ml"],my:["mt","mb"],size:["w","h"],"font-size":["leading"],"fvn-normal":["fvn-ordinal","fvn-slashed-zero","fvn-figure","fvn-spacing","fvn-fraction"],"fvn-ordinal":["fvn-normal"],"fvn-slashed-zero":["fvn-normal"],"fvn-figure":["fvn-normal"],"fvn-spacing":["fvn-normal"],"fvn-fraction":["fvn-normal"],"line-clamp":["display","overflow"],rounded:["rounded-s","rounded-e","rounded-t","rounded-r","rounded-b","rounded-l","rounded-ss","rounded-se","rounded-ee","rounded-es","rounded-tl","rounded-tr","rounded-br","rounded-bl"],"rounded-s":["rounded-ss","rounded-es"],"rounded-e":["rounded-se","rounded-ee"],"rounded-t":["rounded-tl","rounded-tr"],"rounded-r":["rounded-tr","rounded-br"],"rounded-b":["rounded-br","rounded-bl"],"rounded-l":["rounded-tl","rounded-bl"],"border-spacing":["border-spacing-x","border-spacing-y"],"border-w":["border-w-s","border-w-e","border-w-t","border-w-r","border-w-b","border-w-l"],"border-w-x":["border-w-r","border-w-l"],"border-w-y":["border-w-t","border-w-b"],"border-color":["border-color-s","border-color-e","border-color-t","border-color-r","border-color-b","border-color-l"],"border-color-x":["border-color-r","border-color-l"],"border-color-y":["border-color-t","border-color-b"],"scroll-m":["scroll-mx","scroll-my","scroll-ms","scroll-me","scroll-mt","scroll-mr","scroll-mb","scroll-ml"],"scroll-mx":["scroll-mr","scroll-ml"],"scroll-my":["scroll-mt","scroll-mb"],"scroll-p":["scroll-px","scroll-py","scroll-ps","scroll-pe","scroll-pt","scroll-pr","scroll-pb","scroll-pl"],"scroll-px":["scroll-pr","scroll-pl"],"scroll-py":["scroll-pt","scroll-pb"],touch:["touch-x","touch-y","touch-pz"],"touch-x":["touch"],"touch-y":["touch"],"touch-pz":["touch"]},conflictingClassGroupModifiers:{"font-size":["leading"]}}},Hh=gh(Rh);function Zt(...r){return Hh(ah(r))}const Bh=["relative cursor-pointer","text-sm focus:z-10 focus:ring-2 font-medium focus:outline-none whitespace-nowrap shadow-sm","inline-flex gap-2 items-center justify-center transition-colors focus:ring-offset-1","disabled:opacity-40 disabled:cursor-not-allowed disabled:text-nb-gray-300 ring-offset-neutral-950/50"],qh={default:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900","dark:focus:ring-zinc-800/50 dark:bg-nb-gray dark:text-gray-400 dark:border-gray-700/30 dark:hover:text-white dark:hover:bg-zinc-800/50"],primary:["dark:focus:ring-netbird-600/50 dark:ring-offset-neutral-950/50 enabled:dark:bg-netbird disabled:dark:bg-nb-gray-910 dark:text-gray-100 enabled:dark:hover:text-white enabled:dark:hover:bg-netbird-500/80","enabled:bg-netbird enabled:text-white enabled:focus:ring-netbird-400/50 enabled:hover:bg-netbird-500"],secondary:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900","dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20","dark:bg-nb-gray-920 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-nb-gray-910"],secondaryLighter:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900","dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20","dark:bg-nb-gray-900/70 dark:text-gray-400 dark:border-gray-700/70 dark:hover:text-white dark:hover:bg-nb-gray-800/60"],input:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900","dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20","dark:bg-nb-gray-900 dark:text-gray-400 dark:border-nb-gray-700 dark:hover:bg-nb-gray-900/80"],dropdown:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900","dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20","dark:bg-nb-gray-900/40 dark:text-gray-400 dark:border-nb-gray-900 dark:hover:bg-nb-gray-900/50"],dotted:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900 border-dashed","dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20","dark:bg-nb-gray-900/30 dark:text-gray-400 dark:border-gray-500/40 dark:hover:text-white dark:hover:bg-zinc-800/50"],tertiary:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900","dark:focus:ring-zinc-800/50 dark:bg-white dark:text-gray-800 dark:border-gray-700/40 dark:hover:bg-neutral-200 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300"],white:["focus:ring-white/50 bg-white text-gray-800 border-white outline-none hover:bg-neutral-200 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300","disabled:dark:bg-nb-gray-900 disabled:dark:text-nb-gray-300 disabled:dark:border-nb-gray-900"],outline:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900","dark:focus:ring-zinc-800/50 dark:bg-transparent dark:text-netbird dark:border-netbird dark:hover:bg-nb-gray-900/30"],"danger-outline":["enabled:dark:focus:ring-red-800/20 enabled:dark:focus:bg-red-950/40 enabled:hover:dark:bg-red-950/50 enabled:dark:hover:border-red-800/50 dark:bg-transparent dark:text-red-500"],"danger-text":["dark:bg-transparent dark:text-red-500 dark:hover:text-red-600 dark:border-transparent !px-0 !shadow-none !py-0 focus:ring-red-500/30 dark:ring-offset-neutral-950/50"],"default-outline":["dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20","dark:bg-transparent dark:text-nb-gray-400 dark:border-transparent dark:hover:text-white dark:hover:bg-nb-gray-900/30 dark:hover:border-nb-gray-800/50","data-[state=open]:dark:text-white data-[state=open]:dark:bg-nb-gray-900/30 data-[state=open]:dark:border-nb-gray-800/50"],danger:["dark:focus:ring-red-700/20 dark:focus:bg-red-700 hover:dark:bg-red-700 dark:hover:border-red-800/50 dark:bg-red-600 dark:text-red-100"]},Yh={xs:"text-xs py-2 px-4",xs2:"text-[0.78rem] py-2 px-4",sm:"text-sm py-2.5 px-4",md:"text-sm py-2.5 px-4",lg:"text-base py-2.5 px-4"},Gh={0:"border",1:"border border-transparent",2:"border border-t-0 border-b-0"},Ru=xt.forwardRef(({variant:r="default",rounded:v=!0,border:S=1,size:f="md",stopPropagation:_=!0,className:O,onClick:D,children:U,...N},p)=>A.jsx("button",{type:"button",...N,ref:p,className:Zt(Bh,qh[r],Yh[f],Gh[S?1:0],v&&"rounded-md",O),onClick:R=>{_&&R.stopPropagation(),D?.(R)},children:U}));Ru.displayName="Button";const Xh={default:["bg-nb-gray-900 placeholder:text-neutral-400/70 border-nb-gray-700","ring-offset-neutral-950/50 focus-visible:ring-neutral-500/20"],darker:["bg-nb-gray-920 placeholder:text-neutral-400/70 border-nb-gray-800","ring-offset-neutral-950/50 focus-visible:ring-neutral-500/20"],error:["bg-nb-gray-900 placeholder:text-neutral-400/70 border-red-500 text-red-500","ring-offset-red-500/10 focus-visible:ring-red-500/10"]},Qh={default:"bg-nb-gray-900 border-nb-gray-700 text-nb-gray-300",error:"bg-nb-gray-900 border-red-500 text-nb-gray-300 text-red-500"},c0=xt.forwardRef(({className:r,type:v,customSuffix:S,customPrefix:f,icon:_,maxWidthClass:O="",error:D,variant:U="default",prefixClassName:N,showPasswordToggle:p=!1,...R},H)=>{const[V,st]=xt.useState(!1),ct=v==="password",G=ct&&V?"text":v,L=(ct&&p?A.jsx("button",{type:"button",onClick:()=>st(!V),className:"hover:text-white transition-all","aria-label":"Toggle password visibility",children:V?A.jsx(km,{size:18}):A.jsx(Wm,{size:18})}):null)||S,gt=D?"error":U;return A.jsxs(A.Fragment,{children:[A.jsxs("div",{className:Zt("flex relative h-[42px]",O),children:[f&&A.jsx("div",{className:Zt(Qh[D?"error":"default"],"flex h-[42px] w-auto rounded-l-md px-3 py-2 text-sm","border items-center whitespace-nowrap",R.disabled&&"opacity-40",N),children:f}),A.jsx("div",{className:Zt("absolute left-0 top-0 h-full flex items-center text-xs text-nb-gray-300 pl-3 leading-[0]",R.disabled&&"opacity-40"),children:_}),A.jsx("input",{type:G,ref:H,...R,className:Zt(Xh[gt],"flex h-[42px] w-full rounded-md px-3 py-2 text-sm","file:bg-transparent file:text-sm file:font-medium file:border-0","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2","disabled:cursor-not-allowed disabled:opacity-40","border",f&&"!border-l-0 !rounded-l-none",L&&"!pr-16",_&&"!pl-10",r)}),A.jsx("div",{className:Zt("absolute right-0 top-0 h-full flex items-center text-xs text-nb-gray-300 pr-4 leading-[0] select-none",R.disabled&&"opacity-30"),children:L})]}),D&&A.jsx("p",{className:"text-xs text-red-500 mt-2",children:D})]})});c0.displayName="Input";const Zh=xt.forwardRef(function({value:v,onChange:S,length:f=6,disabled:_=!1,className:O,autoFocus:D=!1},U){const N=xt.useRef([]);xt.useImperativeHandle(U,()=>({focus:()=>{N.current[0]?.focus()}}));const p=v.split("").concat(new Array(f).fill("")).slice(0,f),R=Array.from({length:f},(G,Q)=>`pin-${Q}`),H=(G,Q)=>{if(!/^\d*$/.test(Q))return;const L=[...p];L[G]=Q.slice(-1);const gt=L.join("").replaceAll(/\s/g,"");S(gt),Q&&G{Q.key==="Backspace"&&!p[G]&&G>0&&N.current[G-1]?.focus(),Q.key==="ArrowLeft"&&G>0&&N.current[G-1]?.focus(),Q.key==="ArrowRight"&&G{G.preventDefault();const Q=G.clipboardData.getData("text").replaceAll(/\D/g,"").slice(0,f);S(Q);const L=Math.min(Q.length,f-1);N.current[L]?.focus()},ct=G=>{G.target.select()};return A.jsx("div",{className:Zt("flex gap-2 w-full min-w-0",O),children:p.map((G,Q)=>A.jsx("input",{id:R[Q],ref:L=>{N.current[Q]=L},type:"text",inputMode:"numeric",maxLength:1,value:G,onChange:L=>H(Q,L.target.value),onKeyDown:L=>V(Q,L),onPaste:st,onFocus:ct,disabled:_,autoFocus:D&&Q===0,className:Zt("flex-1 min-w-0 h-[42px] text-center text-sm rounded-md","dark:bg-nb-gray-900 border dark:border-nb-gray-700","dark:placeholder:text-neutral-400/70","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2","ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20","disabled:cursor-not-allowed disabled:opacity-40")},R[Q]))})}),f0=xt.createContext({value:"",onChange:()=>{}}),r0=()=>xt.useContext(f0);function $e({value:r,defaultValue:v,onChange:S,children:f}){const[_,O]=xt.useState(v??""),D=r??_,U=xt.useCallback(p=>{r===void 0&&O(p),S?.(p)},[r,S]),N=xt.useMemo(()=>({value:D,onChange:U}),[D,U]);return A.jsx(f0.Provider,{value:N,children:A.jsx("div",{children:typeof f=="function"?f({value:D,onChange:U}):f})})}function wh({children:r,className:v}){return A.jsx("div",{role:"tablist",className:Zt("bg-nb-gray-930/70 p-1.5 flex justify-center gap-1 border-nb-gray-900",v),children:r})}function Lh({children:r,value:v,disabled:S=!1,className:f,selected:_,onClick:O}){const D=r0(),U=_??v===D.value;let N="";U?N="bg-nb-gray-900 text-white":S||(N="text-nb-gray-400 hover:bg-nb-gray-900/50");const p=()=>{D.onChange(v),O?.()};return A.jsx("button",{role:"tab",type:"button",disabled:S,"aria-selected":U,onClick:p,className:Zt("px-4 py-2 text-sm rounded-md w-full transition-all cursor-pointer",S&&"opacity-30 cursor-not-allowed",N,f),children:A.jsx("div",{className:"flex items-center w-full justify-center gap-2",children:r})})}function Vh({children:r,value:v,className:S,visible:f}){const _=r0();return f??v===_.value?A.jsx("div",{role:"tabpanel",className:Zt("bg-nb-gray-930/70 px-4 pt-4 pb-5 rounded-b-md border border-t-0 border-nb-gray-900",S),children:r}):null}$e.List=wh;$e.Trigger=Lh;$e.Content=Vh;const Kh="/__netbird__/assets/netbird-full.svg",Jh="data:image/svg+xml,%3csvg%20width='31'%20height='23'%20viewBox='0%200%2031%2023'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M21.4631%200.523438C17.8173%200.857913%2016.0028%202.95675%2015.3171%204.01871L4.66406%2022.4734H17.5163L30.1929%200.523438H21.4631Z'%20fill='%23F68330'/%3e%3cpath%20d='M17.5265%2022.4737L0%203.88525C0%203.88525%2019.8177%20-1.44128%2021.7493%2015.1738L17.5265%2022.4737Z'%20fill='%23F68330'/%3e%3cpath%20d='M14.9236%204.70563L9.54688%2014.0208L17.5158%2022.4747L21.7385%2015.158C21.0696%209.44682%2018.2851%206.32784%2014.9236%204.69727'%20fill='%23F05252'/%3e%3c/svg%3e",ti={small:{desktop:14,mobile:20},default:{desktop:22,mobile:30},large:{desktop:24,mobile:40}},kh=({size:r="default",mobile:v=!0})=>A.jsxs(A.Fragment,{children:[A.jsx("img",{src:Kh,height:ti[r].desktop,style:{height:ti[r].desktop},alt:"NetBird Logo",className:Zt(v&&"hidden md:block","group-hover:opacity-80 transition-all")}),v&&A.jsx("img",{src:Jh,width:ti[r].mobile,style:{width:ti[r].mobile},alt:"NetBird Logo",className:Zt(v&&"md:hidden ml-4")})]});function Uf(){return A.jsxs("a",{href:"https://netbird.io?utm_source=netbird-proxy&utm_medium=web&utm_campaign=powered_by",target:"_blank",rel:"noopener noreferrer",className:"flex items-center justify-center mt-8 gap-2 group cursor-pointer",children:[A.jsx("span",{className:"text-sm text-nb-gray-400 font-light text-center group-hover:opacity-80 transition-all",children:"Powered by"}),A.jsx(kh,{size:"small",mobile:!1})]})}const Wh=({className:r})=>A.jsx("div",{className:Zt("h-full w-full absolute left-0 top-0 rounded-md overflow-hidden z-0 pointer-events-none",r),children:A.jsx("div",{className:"bg-linear-to-b from-nb-gray-900/10 via-transparent to-transparent w-full h-full rounded-md"})}),Fd=({children:r,className:v})=>A.jsxs("div",{className:Zt("px-6 sm:px-10 py-10 pt-8","bg-nb-gray-940 border border-nb-gray-910 rounded-lg relative",v),children:[A.jsx(Wh,{}),r]});function Cf({children:r,className:v}){return A.jsx("h1",{className:Zt("text-xl! text-center z-10 relative",v),children:r})}function jf({children:r,className:v}){return A.jsx("div",{className:Zt("text-sm text-nb-gray-300 font-light mt-2 block text-center z-10 relative",v),children:r})}const $h=()=>A.jsxs("div",{className:"flex items-center justify-center relative my-4",children:[A.jsx("span",{className:"bg-nb-gray-940 relative z-10 px-4 text-xs text-nb-gray-400 font-medium",children:"OR"}),A.jsx("span",{className:"h-px bg-nb-gray-900 w-full absolute z-0"})]}),Fh=({error:r})=>A.jsx("div",{className:"text-red-400 bg-red-800/20 border border-red-800/50 rounded-lg px-4 py-3 whitespace-break-spaces text-sm",children:r});function Id({className:r,htmlFor:v,...S}){return A.jsx("label",{htmlFor:v,className:Zt("text-sm font-medium tracking-wider leading-none","peer-disabled:cursor-not-allowed peer-disabled:opacity-70","mb-2.5 inline-block text-nb-gray-200","flex items-center gap-2 select-none",r),...S})}const _f=t0(),Ft=_f.methods&&Object.keys(_f.methods).length>0?_f.methods:{password:"password",pin:"pin",oidc:"/auth/oidc"};function Ih(){xt.useEffect(()=>{document.title="Authentication Required - NetBird Service"},[]);const[r,v]=xt.useState(null),[S,f]=xt.useState(null),[_,O]=xt.useState(""),[D,U]=xt.useState(""),N=xt.useRef(null),p=xt.useRef(null),[R,H]=xt.useState(Ft.password?"password":"pin"),V=(it,Ot)=>{v(Ot),f(null),it==="password"?(U(""),setTimeout(()=>N.current?.focus(),200)):(O(""),setTimeout(()=>p.current?.focus(),200))},st=(it,Ot)=>{v(null),f(it);const J=new FormData;it==="password"?J.append(Ft.password,Ot):J.append(Ft.pin,Ot),fetch(globalThis.location.href,{method:"POST",body:J,redirect:"manual"}).then(Rt=>{Rt.type==="opaqueredirect"||Rt.status===0?(f("redirect"),globalThis.location.reload()):V(it,"Authentication failed. Please try again.")}).catch(()=>{V(it,"An error occurred. Please try again.")})},ct=it=>{O(it),it.length===6&&st("pin",it)},G=_.length===6,Q=D.length>0,L=S!==null||R==="password"&&!Q||R==="pin"&&!G,gt=Ft.password||Ft.pin,zt=Ft.password&&Ft.pin,_t=R==="password"?"Sign in":"Submit";return S==="redirect"?A.jsxs("main",{className:"mt-20",children:[A.jsxs(Fd,{className:"max-w-105 mx-auto",children:[A.jsx(Cf,{children:"Authenticated"}),A.jsx(jf,{children:"Loading service..."}),A.jsx("div",{className:"flex justify-center mt-7",children:A.jsx(kd,{className:"animate-spin",size:24})})]}),A.jsx(Uf,{})]}):A.jsxs("main",{className:"mt-20",children:[A.jsxs(Fd,{className:"max-w-105 mx-auto",children:[A.jsx(Cf,{children:"Authentication Required"}),A.jsx(jf,{children:"The service you are trying to access is protected. Please authenticate to continue."}),A.jsxs("div",{className:"flex flex-col gap-4 mt-7 z-10 relative",children:[r&&A.jsx(Fh,{error:r}),Ft.oidc&&A.jsxs(Ru,{variant:"primary",className:"w-full",onClick:()=>{globalThis.location.href=Ft.oidc},children:[A.jsx(Im,{size:16}),"Sign in with SSO"]}),Ft.oidc&>&&A.jsx($h,{}),gt&&A.jsxs("form",{onSubmit:it=>{it.preventDefault(),st(R,R==="password"?D:_)},children:[zt&&A.jsx($e,{value:R,onChange:it=>{H(it),setTimeout(()=>{it==="password"?N.current?.focus():p.current?.focus()},0)},children:A.jsxs($e.List,{className:"rounded-lg border mb-4",children:[A.jsxs($e.Trigger,{value:"password",children:[A.jsx(Fm,{size:14}),"Password"]}),A.jsxs($e.Trigger,{value:"pin",children:[A.jsx(Km,{size:14}),"PIN"]})]})}),A.jsxs("div",{className:"mb-4",children:[Ft.password&&(R==="password"||!Ft.pin)&&A.jsxs(A.Fragment,{children:[!zt&&A.jsx(Id,{htmlFor:"password",children:"Password"}),A.jsx(c0,{ref:N,type:"password",id:"password",placeholder:"Enter password",disabled:S!==null,showPasswordToggle:!0,autoFocus:!0,value:D,onChange:it=>U(it.target.value)})]}),Ft.pin&&(R==="pin"||!Ft.password)&&A.jsxs(A.Fragment,{children:[!zt&&A.jsx(Id,{htmlFor:"pin-0",children:"Enter PIN Code"}),A.jsx(Zh,{ref:p,value:_,onChange:ct,disabled:S!==null,autoFocus:!Ft.password})]})]}),A.jsx(Ru,{type:"submit",disabled:L,variant:"secondary",className:"w-full",children:S===null?_t:A.jsxs(A.Fragment,{children:[A.jsx(kd,{className:"animate-spin",size:16}),"Verifying..."]})})]})]})]}),A.jsx(Uf,{})]})}function Ph({success:r=!0}){return r?A.jsx("div",{className:"flex-1 flex items-center justify-center h-12 w-full px-5",children:A.jsx("div",{className:"w-full border-t-2 border-dashed border-green-500"})}):A.jsxs("div",{className:"flex-1 flex items-center justify-center h-12 min-w-10 px-5 relative",children:[A.jsx("div",{className:"w-full border-t-2 border-dashed border-nb-gray-900"}),A.jsx("div",{className:"absolute inset-0 flex items-center justify-center",children:A.jsx("div",{className:"w-8 h-8 rounded-full flex items-center justify-center",children:A.jsx(eh,{size:18,className:"text-netbird"})})})]})}function Of({icon:r,label:v,detail:S,success:f=!0,line:_=!0}){return A.jsxs(A.Fragment,{children:[_&&A.jsx(Ph,{success:f}),A.jsxs("div",{className:"flex flex-col items-center gap-2",children:[A.jsx("div",{className:"w-14 h-14 rounded-md flex items-center justify-center from-nb-gray-940 to-nb-gray-930/70 bg-gradient-to-br border border-nb-gray-910",children:A.jsx(r,{size:20,className:"text-nb-gray-200"})}),A.jsx("span",{className:"text-sm text-nb-gray-200 font-normal mt-1",children:v}),A.jsx("span",{className:`text-xs font-medium uppercase ${f?"text-green-500":"text-netbird"}`,children:f?"Connected":"Unreachable"}),S&&A.jsx("span",{className:"text-xs text-nb-gray-400 truncate text-center",children:S})]})]})}function tg({code:r,title:v,message:S,proxy:f=!0,destination:_=!0,requestId:O,simple:D=!1,retryUrl:U}){xt.useEffect(()=>{document.title=`${v} - NetBird Service`},[v]);const[N]=xt.useState(()=>new Date().toISOString());return A.jsxs("main",{className:"flex flex-col items-center mt-24 px-4 max-w-3xl mx-auto",children:[A.jsxs("div",{className:"text-sm text-netbird font-normal font-mono mb-3 z-10 relative",children:["Error ",r]}),A.jsx(Cf,{className:"text-3xl!",children:v}),A.jsx(jf,{className:"mt-2 mb-8 max-w-md",children:S}),!D&&A.jsxs("div",{className:"hidden sm:flex items-start justify-center w-full mt-6 mb-16 z-10 relative",children:[A.jsx(Of,{icon:th,label:"You",line:!1}),A.jsx(Of,{icon:lh,label:"Proxy",success:f}),A.jsx(Of,{icon:$m,label:"Destination",success:_})]}),A.jsxs("div",{className:"flex gap-3 justify-center items-center mb-6 z-10 relative",children:[A.jsxs(Ru,{variant:"primary",onClick:()=>{U?globalThis.location.href=U:globalThis.location.reload()},children:[A.jsx(Pm,{size:16}),"Refresh Page"]}),A.jsxs(Ru,{variant:"secondary",onClick:()=>globalThis.open("https://docs.netbird.io","_blank","noopener,noreferrer"),children:[A.jsx(Jm,{size:16}),"Documentation"]})]}),A.jsxs("div",{className:"text-center text-xs text-nb-gray-300 uppercase z-10 relative font-mono flex flex-col sm:flex-row gap-2 sm:gap-10 mt-4 mb-3",children:[A.jsxs("div",{children:[A.jsx("span",{className:"text-nb-gray-400",children:"REQUEST-ID:"})," ",O]}),A.jsxs("div",{children:[A.jsx("span",{className:"text-nb-gray-400",children:"TIMESTAMP:"})," ",N]})]}),A.jsx(Uf,{})]})}const Nf=t0();Zm.createRoot(document.getElementById("root")).render(A.jsx(xt.StrictMode,{children:Nf.page==="error"&&Nf.error?A.jsx(tg,{...Nf.error}):A.jsx(Ih,{})})); +`+a.stack}}var ui=Object.prototype.hasOwnProperty,ni=r.unstable_scheduleCallback,ii=r.unstable_cancelCallback,o0=r.unstable_shouldYield,d0=r.unstable_requestPaint,rl=r.unstable_now,y0=r.unstable_getCurrentPriorityLevel,Yf=r.unstable_ImmediatePriority,Gf=r.unstable_UserBlockingPriority,Bu=r.unstable_NormalPriority,m0=r.unstable_LowPriority,Xf=r.unstable_IdlePriority,h0=r.log,g0=r.unstable_setDisableYieldValue,Ya=null,sl=null;function ue(t){if(typeof h0=="function"&&g0(t),sl&&typeof sl.setStrictMode=="function")try{sl.setStrictMode(Ya,t)}catch{}}var ol=Math.clz32?Math.clz32:p0,v0=Math.log,b0=Math.LN2;function p0(t){return t>>>=0,t===0?32:31-(v0(t)/b0|0)|0}var qu=256,Yu=262144,Gu=4194304;function Ce(t){var l=t&42;if(l!==0)return l;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Xu(t,l,e){var a=t.pendingLanes;if(a===0)return 0;var u=0,n=t.suspendedLanes,i=t.pingedLanes;t=t.warmLanes;var c=a&134217727;return c!==0?(a=c&~n,a!==0?u=Ce(a):(i&=c,i!==0?u=Ce(i):e||(e=c&~t,e!==0&&(u=Ce(e))))):(c=a&~n,c!==0?u=Ce(c):i!==0?u=Ce(i):e||(e=a&~t,e!==0&&(u=Ce(e)))),u===0?0:l!==0&&l!==u&&(l&n)===0&&(n=u&-u,e=l&-l,n>=e||n===32&&(e&4194048)!==0)?l:u}function Ga(t,l){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&l)===0}function S0(t,l){switch(t){case 1:case 2:case 4:case 8:case 64:return l+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return l+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Qf(){var t=Gu;return Gu<<=1,(Gu&62914560)===0&&(Gu=4194304),t}function ci(t){for(var l=[],e=0;31>e;e++)l.push(t);return l}function Xa(t,l){t.pendingLanes|=l,l!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function x0(t,l,e,a,u,n){var i=t.pendingLanes;t.pendingLanes=e,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=e,t.entangledLanes&=e,t.errorRecoveryDisabledLanes&=e,t.shellSuspendCounter=0;var c=t.entanglements,s=t.expirationTimes,h=t.hiddenUpdates;for(e=i&~e;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var _0=/[\n"\\]/g;function xl(t){return t.replace(_0,function(l){return"\\"+l.charCodeAt(0).toString(16)+" "})}function yi(t,l,e,a,u,n,i,c){t.name="",i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"?t.type=i:t.removeAttribute("type"),l!=null?i==="number"?(l===0&&t.value===""||t.value!=l)&&(t.value=""+Sl(l)):t.value!==""+Sl(l)&&(t.value=""+Sl(l)):i!=="submit"&&i!=="reset"||t.removeAttribute("value"),l!=null?mi(t,i,Sl(l)):e!=null?mi(t,i,Sl(e)):a!=null&&t.removeAttribute("value"),u==null&&n!=null&&(t.defaultChecked=!!n),u!=null&&(t.checked=u&&typeof u!="function"&&typeof u!="symbol"),c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"?t.name=""+Sl(c):t.removeAttribute("name")}function tr(t,l,e,a,u,n,i,c){if(n!=null&&typeof n!="function"&&typeof n!="symbol"&&typeof n!="boolean"&&(t.type=n),l!=null||e!=null){if(!(n!=="submit"&&n!=="reset"||l!=null)){di(t);return}e=e!=null?""+Sl(e):"",l=l!=null?""+Sl(l):e,c||l===t.value||(t.value=l),t.defaultValue=l}a=a??u,a=typeof a!="function"&&typeof a!="symbol"&&!!a,t.checked=c?t.checked:!!a,t.defaultChecked=!!a,i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"&&(t.name=i),di(t)}function mi(t,l,e){l==="number"&&wu(t.ownerDocument)===t||t.defaultValue===""+e||(t.defaultValue=""+e)}function ea(t,l,e,a){if(t=t.options,l){l={};for(var u=0;u"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),pi=!1;if(Ql)try{var La={};Object.defineProperty(La,"passive",{get:function(){pi=!0}}),window.addEventListener("test",La,La),window.removeEventListener("test",La,La)}catch{pi=!1}var ie=null,Si=null,Vu=null;function cr(){if(Vu)return Vu;var t,l=Si,e=l.length,a,u="value"in ie?ie.value:ie.textContent,n=u.length;for(t=0;t=Ja),yr=" ",mr=!1;function hr(t,l){switch(t){case"keyup":return ly.indexOf(l.keyCode)!==-1;case"keydown":return l.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function gr(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var ia=!1;function ay(t,l){switch(t){case"compositionend":return gr(l);case"keypress":return l.which!==32?null:(mr=!0,yr);case"textInput":return t=l.data,t===yr&&mr?null:t;default:return null}}function uy(t,l){if(ia)return t==="compositionend"||!Ei&&hr(t,l)?(t=cr(),Vu=Si=ie=null,ia=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(l.ctrlKey||l.altKey||l.metaKey)||l.ctrlKey&&l.altKey){if(l.char&&1=l)return{node:e,offset:l-t};t=a}t:{for(;e;){if(e.nextSibling){e=e.nextSibling;break t}e=e.parentNode}e=void 0}e=Ar(e)}}function Mr(t,l){return t&&l?t===l?!0:t&&t.nodeType===3?!1:l&&l.nodeType===3?Mr(t,l.parentNode):"contains"in t?t.contains(l):t.compareDocumentPosition?!!(t.compareDocumentPosition(l)&16):!1:!1}function _r(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var l=wu(t.document);l instanceof t.HTMLIFrameElement;){try{var e=typeof l.contentWindow.location.href=="string"}catch{e=!1}if(e)t=l.contentWindow;else break;l=wu(t.document)}return l}function Oi(t){var l=t&&t.nodeName&&t.nodeName.toLowerCase();return l&&(l==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||l==="textarea"||t.contentEditable==="true")}var dy=Ql&&"documentMode"in document&&11>=document.documentMode,ca=null,Ni=null,Fa=null,Di=!1;function Or(t,l,e){var a=e.window===e?e.document:e.nodeType===9?e:e.ownerDocument;Di||ca==null||ca!==wu(a)||(a=ca,"selectionStart"in a&&Oi(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),Fa&&$a(Fa,a)||(Fa=a,a=Gn(Ni,"onSelect"),0>=i,u-=i,Hl=1<<32-ol(l)+u|e<$?(at=Y,Y=null):at=Y.sibling;var rt=g(y,Y,m[$],T);if(rt===null){Y===null&&(Y=at);break}t&&Y&&rt.alternate===null&&l(y,Y),d=n(rt,d,$),ft===null?X=rt:ft.sibling=rt,ft=rt,Y=at}if($===m.length)return e(y,Y),ut&&wl(y,$),X;if(Y===null){for(;$$?(at=Y,Y=null):at=Y.sibling;var Oe=g(y,Y,rt.value,T);if(Oe===null){Y===null&&(Y=at);break}t&&Y&&Oe.alternate===null&&l(y,Y),d=n(Oe,d,$),ft===null?X=Oe:ft.sibling=Oe,ft=Oe,Y=at}if(rt.done)return e(y,Y),ut&&wl(y,$),X;if(Y===null){for(;!rt.done;$++,rt=m.next())rt=A(y,rt.value,T),rt!==null&&(d=n(rt,d,$),ft===null?X=rt:ft.sibling=rt,ft=rt);return ut&&wl(y,$),X}for(Y=a(Y);!rt.done;$++,rt=m.next())rt=b(Y,y,$,rt.value,T),rt!==null&&(t&&rt.alternate!==null&&Y.delete(rt.key===null?$:rt.key),d=n(rt,d,$),ft===null?X=rt:ft.sibling=rt,ft=rt);return t&&Y.forEach(function(Cm){return l(y,Cm)}),ut&&wl(y,$),X}function pt(y,d,m,T){if(typeof m=="object"&&m!==null&&m.type===G&&m.key===null&&(m=m.props.children),typeof m=="object"&&m!==null){switch(m.$$typeof){case ot:t:{for(var X=m.key;d!==null;){if(d.key===X){if(X=m.type,X===G){if(d.tag===7){e(y,d.sibling),T=u(d,m.props.children),T.return=y,y=T;break t}}else if(d.elementType===X||typeof X=="object"&&X!==null&&X.$$typeof===Nt&&we(X)===d.type){e(y,d.sibling),T=u(d,m.props),au(T,m),T.return=y,y=T;break t}e(y,d);break}else l(y,d);d=d.sibling}m.type===G?(T=Ye(m.props.children,y.mode,T,m.key),T.return=y,y=T):(T=ln(m.type,m.key,m.props,null,y.mode,T),au(T,m),T.return=y,y=T)}return i(y);case ct:t:{for(X=m.key;d!==null;){if(d.key===X)if(d.tag===4&&d.stateNode.containerInfo===m.containerInfo&&d.stateNode.implementation===m.implementation){e(y,d.sibling),T=u(d,m.children||[]),T.return=y,y=T;break t}else{e(y,d);break}else l(y,d);d=d.sibling}T=qi(m,y.mode,T),T.return=y,y=T}return i(y);case Nt:return m=we(m),pt(y,d,m,T)}if(ll(m))return B(y,d,m,T);if(I(m)){if(X=I(m),typeof X!="function")throw Error(f(150));return m=X.call(m),w(y,d,m,T)}if(typeof m.then=="function")return pt(y,d,rn(m),T);if(m.$$typeof===zt)return pt(y,d,un(y,m),T);sn(y,m)}return typeof m=="string"&&m!==""||typeof m=="number"||typeof m=="bigint"?(m=""+m,d!==null&&d.tag===6?(e(y,d.sibling),T=u(d,m),T.return=y,y=T):(e(y,d),T=Bi(m,y.mode,T),T.return=y,y=T),i(y)):e(y,d)}return function(y,d,m,T){try{eu=0;var X=pt(y,d,m,T);return ba=null,X}catch(Y){if(Y===va||Y===cn)throw Y;var ft=yl(29,Y,null,y.mode);return ft.lanes=T,ft.return=y,ft}}}var Ve=Fr(!0),Ir=Fr(!1),oe=!1;function Wi(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function $i(t,l){t=t.updateQueue,l.updateQueue===t&&(l.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function de(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function ye(t,l,e){var a=t.updateQueue;if(a===null)return null;if(a=a.shared,(st&2)!==0){var u=a.pending;return u===null?l.next=l:(l.next=u.next,u.next=l),a.pending=l,l=tn(t),Hr(t,null,e),l}return Pu(t,a,l,e),tn(t)}function uu(t,l,e){if(l=l.updateQueue,l!==null&&(l=l.shared,(e&4194048)!==0)){var a=l.lanes;a&=t.pendingLanes,e|=a,l.lanes=e,wf(t,e)}}function Fi(t,l){var e=t.updateQueue,a=t.alternate;if(a!==null&&(a=a.updateQueue,e===a)){var u=null,n=null;if(e=e.firstBaseUpdate,e!==null){do{var i={lane:e.lane,tag:e.tag,payload:e.payload,callback:null,next:null};n===null?u=n=i:n=n.next=i,e=e.next}while(e!==null);n===null?u=n=l:n=n.next=l}else u=n=l;e={baseState:a.baseState,firstBaseUpdate:u,lastBaseUpdate:n,shared:a.shared,callbacks:a.callbacks},t.updateQueue=e;return}t=e.lastBaseUpdate,t===null?e.firstBaseUpdate=l:t.next=l,e.lastBaseUpdate=l}var Ii=!1;function nu(){if(Ii){var t=ga;if(t!==null)throw t}}function iu(t,l,e,a){Ii=!1;var u=t.updateQueue;oe=!1;var n=u.firstBaseUpdate,i=u.lastBaseUpdate,c=u.shared.pending;if(c!==null){u.shared.pending=null;var s=c,h=s.next;s.next=null,i===null?n=h:i.next=h,i=s;var z=t.alternate;z!==null&&(z=z.updateQueue,c=z.lastBaseUpdate,c!==i&&(c===null?z.firstBaseUpdate=h:c.next=h,z.lastBaseUpdate=s))}if(n!==null){var A=u.baseState;i=0,z=h=s=null,c=n;do{var g=c.lane&-536870913,b=g!==c.lane;if(b?(et&g)===g:(a&g)===g){g!==0&&g===ha&&(Ii=!0),z!==null&&(z=z.next={lane:0,tag:c.tag,payload:c.payload,callback:null,next:null});t:{var B=t,w=c;g=l;var pt=e;switch(w.tag){case 1:if(B=w.payload,typeof B=="function"){A=B.call(pt,A,g);break t}A=B;break t;case 3:B.flags=B.flags&-65537|128;case 0:if(B=w.payload,g=typeof B=="function"?B.call(pt,A,g):B,g==null)break t;A=H({},A,g);break t;case 2:oe=!0}}g=c.callback,g!==null&&(t.flags|=64,b&&(t.flags|=8192),b=u.callbacks,b===null?u.callbacks=[g]:b.push(g))}else b={lane:g,tag:c.tag,payload:c.payload,callback:c.callback,next:null},z===null?(h=z=b,s=A):z=z.next=b,i|=g;if(c=c.next,c===null){if(c=u.shared.pending,c===null)break;b=c,c=b.next,b.next=null,u.lastBaseUpdate=b,u.shared.pending=null}}while(!0);z===null&&(s=A),u.baseState=s,u.firstBaseUpdate=h,u.lastBaseUpdate=z,n===null&&(u.shared.lanes=0),be|=i,t.lanes=i,t.memoizedState=A}}function Pr(t,l){if(typeof t!="function")throw Error(f(191,t));t.call(l)}function ts(t,l){var e=t.callbacks;if(e!==null)for(t.callbacks=null,t=0;tn?n:8;var i=x.T,c={};x.T=c,vc(t,!1,l,e);try{var s=u(),h=x.S;if(h!==null&&h(c,s),s!==null&&typeof s=="object"&&typeof s.then=="function"){var z=xy(s,a);ru(t,l,z,bl(t))}else ru(t,l,a,bl(t))}catch(A){ru(t,l,{then:function(){},status:"rejected",reason:A},bl())}finally{C.p=n,i!==null&&c.types!==null&&(i.types=c.types),x.T=i}}function _y(){}function hc(t,l,e,a){if(t.tag!==5)throw Error(f(476));var u=Cs(t).queue;Us(t,u,l,Z,e===null?_y:function(){return js(t),e(a)})}function Cs(t){var l=t.memoizedState;if(l!==null)return l;l={memoizedState:Z,baseState:Z,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Jl,lastRenderedState:Z},next:null};var e={};return l.next={memoizedState:e,baseState:e,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Jl,lastRenderedState:e},next:null},t.memoizedState=l,t=t.alternate,t!==null&&(t.memoizedState=l),l}function js(t){var l=Cs(t);l.next===null&&(l=t.alternate.memoizedState),ru(t,l.next.queue,{},bl())}function gc(){return Kt(Mu)}function Rs(){return Rt().memoizedState}function Hs(){return Rt().memoizedState}function Oy(t){for(var l=t.return;l!==null;){switch(l.tag){case 24:case 3:var e=bl();t=de(e);var a=ye(l,t,e);a!==null&&(fl(a,l,e),uu(a,l,e)),l={cache:Vi()},t.payload=l;return}l=l.return}}function Ny(t,l,e){var a=bl();e={lane:a,revertLane:0,gesture:null,action:e,hasEagerState:!1,eagerState:null,next:null},Sn(t)?qs(l,e):(e=Ri(t,l,e,a),e!==null&&(fl(e,t,a),Ys(e,l,a)))}function Bs(t,l,e){var a=bl();ru(t,l,e,a)}function ru(t,l,e,a){var u={lane:a,revertLane:0,gesture:null,action:e,hasEagerState:!1,eagerState:null,next:null};if(Sn(t))qs(l,u);else{var n=t.alternate;if(t.lanes===0&&(n===null||n.lanes===0)&&(n=l.lastRenderedReducer,n!==null))try{var i=l.lastRenderedState,c=n(i,e);if(u.hasEagerState=!0,u.eagerState=c,dl(c,i))return Pu(t,l,u,0),St===null&&Iu(),!1}catch{}if(e=Ri(t,l,u,a),e!==null)return fl(e,t,a),Ys(e,l,a),!0}return!1}function vc(t,l,e,a){if(a={lane:2,revertLane:Wc(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},Sn(t)){if(l)throw Error(f(479))}else l=Ri(t,e,a,2),l!==null&&fl(l,t,2)}function Sn(t){var l=t.alternate;return t===W||l!==null&&l===W}function qs(t,l){Sa=yn=!0;var e=t.pending;e===null?l.next=l:(l.next=e.next,e.next=l),t.pending=l}function Ys(t,l,e){if((e&4194048)!==0){var a=l.lanes;a&=t.pendingLanes,e|=a,l.lanes=e,wf(t,e)}}var su={readContext:Kt,use:gn,useCallback:Dt,useContext:Dt,useEffect:Dt,useImperativeHandle:Dt,useLayoutEffect:Dt,useInsertionEffect:Dt,useMemo:Dt,useReducer:Dt,useRef:Dt,useState:Dt,useDebugValue:Dt,useDeferredValue:Dt,useTransition:Dt,useSyncExternalStore:Dt,useId:Dt,useHostTransitionStatus:Dt,useFormState:Dt,useActionState:Dt,useOptimistic:Dt,useMemoCache:Dt,useCacheRefresh:Dt};su.useEffectEvent=Dt;var Gs={readContext:Kt,use:gn,useCallback:function(t,l){return Ft().memoizedState=[t,l===void 0?null:l],t},useContext:Kt,useEffect:zs,useImperativeHandle:function(t,l,e){e=e!=null?e.concat([t]):null,bn(4194308,4,Ms.bind(null,l,t),e)},useLayoutEffect:function(t,l){return bn(4194308,4,t,l)},useInsertionEffect:function(t,l){bn(4,2,t,l)},useMemo:function(t,l){var e=Ft();l=l===void 0?null:l;var a=t();if(Ke){ue(!0);try{t()}finally{ue(!1)}}return e.memoizedState=[a,l],a},useReducer:function(t,l,e){var a=Ft();if(e!==void 0){var u=e(l);if(Ke){ue(!0);try{e(l)}finally{ue(!1)}}}else u=l;return a.memoizedState=a.baseState=u,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:u},a.queue=t,t=t.dispatch=Ny.bind(null,W,t),[a.memoizedState,t]},useRef:function(t){var l=Ft();return t={current:t},l.memoizedState=t},useState:function(t){t=sc(t);var l=t.queue,e=Bs.bind(null,W,l);return l.dispatch=e,[t.memoizedState,e]},useDebugValue:yc,useDeferredValue:function(t,l){var e=Ft();return mc(e,t,l)},useTransition:function(){var t=sc(!1);return t=Us.bind(null,W,t.queue,!0,!1),Ft().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,l,e){var a=W,u=Ft();if(ut){if(e===void 0)throw Error(f(407));e=e()}else{if(e=l(),St===null)throw Error(f(349));(et&127)!==0||is(a,l,e)}u.memoizedState=e;var n={value:e,getSnapshot:l};return u.queue=n,zs(fs.bind(null,a,n,t),[t]),a.flags|=2048,za(9,{destroy:void 0},cs.bind(null,a,n,e,l),null),e},useId:function(){var t=Ft(),l=St.identifierPrefix;if(ut){var e=Bl,a=Hl;e=(a&~(1<<32-ol(a)-1)).toString(32)+e,l="_"+l+"R_"+e,e=mn++,0<\/script>",n=n.removeChild(n.firstChild);break;case"select":n=typeof a.is=="string"?i.createElement("select",{is:a.is}):i.createElement("select"),a.multiple?n.multiple=!0:a.size&&(n.size=a.size);break;default:n=typeof a.is=="string"?i.createElement(u,{is:a.is}):i.createElement(u)}}n[Lt]=l,n[el]=a;t:for(i=l.child;i!==null;){if(i.tag===5||i.tag===6)n.appendChild(i.stateNode);else if(i.tag!==4&&i.tag!==27&&i.child!==null){i.child.return=i,i=i.child;continue}if(i===l)break t;for(;i.sibling===null;){if(i.return===null||i.return===l)break t;i=i.return}i.sibling.return=i.return,i=i.sibling}l.stateNode=n;t:switch(kt(n,u,a),u){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break t;case"img":a=!0;break t;default:a=!1}a&&Wl(l)}}return At(l),Uc(l,l.type,t===null?null:t.memoizedProps,l.pendingProps,e),null;case 6:if(t&&l.stateNode!=null)t.memoizedProps!==a&&Wl(l);else{if(typeof a!="string"&&l.stateNode===null)throw Error(f(166));if(t=P.current,ya(l)){if(t=l.stateNode,e=l.memoizedProps,a=null,u=Vt,u!==null)switch(u.tag){case 27:case 5:a=u.memoizedProps}t[Lt]=l,t=!!(t.nodeValue===e||a!==null&&a.suppressHydrationWarning===!0||nd(t.nodeValue,e)),t||re(l,!0)}else t=Xn(t).createTextNode(a),t[Lt]=l,l.stateNode=t}return At(l),null;case 31:if(e=l.memoizedState,t===null||t.memoizedState!==null){if(a=ya(l),e!==null){if(t===null){if(!a)throw Error(f(318));if(t=l.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(f(557));t[Lt]=l}else Ge(),(l.flags&128)===0&&(l.memoizedState=null),l.flags|=4;At(l),t=!1}else e=Qi(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=e),t=!0;if(!t)return l.flags&256?(hl(l),l):(hl(l),null);if((l.flags&128)!==0)throw Error(f(558))}return At(l),null;case 13:if(a=l.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(u=ya(l),a!==null&&a.dehydrated!==null){if(t===null){if(!u)throw Error(f(318));if(u=l.memoizedState,u=u!==null?u.dehydrated:null,!u)throw Error(f(317));u[Lt]=l}else Ge(),(l.flags&128)===0&&(l.memoizedState=null),l.flags|=4;At(l),u=!1}else u=Qi(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=u),u=!0;if(!u)return l.flags&256?(hl(l),l):(hl(l),null)}return hl(l),(l.flags&128)!==0?(l.lanes=e,l):(e=a!==null,t=t!==null&&t.memoizedState!==null,e&&(a=l.child,u=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(u=a.alternate.memoizedState.cachePool.pool),n=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(n=a.memoizedState.cachePool.pool),n!==u&&(a.flags|=2048)),e!==t&&e&&(l.child.flags|=8192),En(l,l.updateQueue),At(l),null);case 4:return Ct(),t===null&&Pc(l.stateNode.containerInfo),At(l),null;case 10:return Vl(l.type),At(l),null;case 19:if(M(jt),a=l.memoizedState,a===null)return At(l),null;if(u=(l.flags&128)!==0,n=a.rendering,n===null)if(u)du(a,!1);else{if(Ut!==0||t!==null&&(t.flags&128)!==0)for(t=l.child;t!==null;){if(n=dn(t),n!==null){for(l.flags|=128,du(a,!1),t=n.updateQueue,l.updateQueue=t,En(l,t),l.subtreeFlags=0,t=e,e=l.child;e!==null;)Br(e,t),e=e.sibling;return j(jt,jt.current&1|2),ut&&wl(l,a.treeForkCount),l.child}t=t.sibling}a.tail!==null&&rl()>Dn&&(l.flags|=128,u=!0,du(a,!1),l.lanes=4194304)}else{if(!u)if(t=dn(n),t!==null){if(l.flags|=128,u=!0,t=t.updateQueue,l.updateQueue=t,En(l,t),du(a,!0),a.tail===null&&a.tailMode==="hidden"&&!n.alternate&&!ut)return At(l),null}else 2*rl()-a.renderingStartTime>Dn&&e!==536870912&&(l.flags|=128,u=!0,du(a,!1),l.lanes=4194304);a.isBackwards?(n.sibling=l.child,l.child=n):(t=a.last,t!==null?t.sibling=n:l.child=n,a.last=n)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=rl(),t.sibling=null,e=jt.current,j(jt,u?e&1|2:e&1),ut&&wl(l,a.treeForkCount),t):(At(l),null);case 22:case 23:return hl(l),tc(),a=l.memoizedState!==null,t!==null?t.memoizedState!==null!==a&&(l.flags|=8192):a&&(l.flags|=8192),a?(e&536870912)!==0&&(l.flags&128)===0&&(At(l),l.subtreeFlags&6&&(l.flags|=8192)):At(l),e=l.updateQueue,e!==null&&En(l,e.retryQueue),e=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(e=t.memoizedState.cachePool.pool),a=null,l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(a=l.memoizedState.cachePool.pool),a!==e&&(l.flags|=2048),t!==null&&M(Ze),null;case 24:return e=null,t!==null&&(e=t.memoizedState.cache),l.memoizedState.cache!==e&&(l.flags|=2048),Vl(Ht),At(l),null;case 25:return null;case 30:return null}throw Error(f(156,l.tag))}function Ry(t,l){switch(Gi(l),l.tag){case 1:return t=l.flags,t&65536?(l.flags=t&-65537|128,l):null;case 3:return Vl(Ht),Ct(),t=l.flags,(t&65536)!==0&&(t&128)===0?(l.flags=t&-65537|128,l):null;case 26:case 27:case 5:return Hu(l),null;case 31:if(l.memoizedState!==null){if(hl(l),l.alternate===null)throw Error(f(340));Ge()}return t=l.flags,t&65536?(l.flags=t&-65537|128,l):null;case 13:if(hl(l),t=l.memoizedState,t!==null&&t.dehydrated!==null){if(l.alternate===null)throw Error(f(340));Ge()}return t=l.flags,t&65536?(l.flags=t&-65537|128,l):null;case 19:return M(jt),null;case 4:return Ct(),null;case 10:return Vl(l.type),null;case 22:case 23:return hl(l),tc(),t!==null&&M(Ze),t=l.flags,t&65536?(l.flags=t&-65537|128,l):null;case 24:return Vl(Ht),null;case 25:return null;default:return null}}function ro(t,l){switch(Gi(l),l.tag){case 3:Vl(Ht),Ct();break;case 26:case 27:case 5:Hu(l);break;case 4:Ct();break;case 31:l.memoizedState!==null&&hl(l);break;case 13:hl(l);break;case 19:M(jt);break;case 10:Vl(l.type);break;case 22:case 23:hl(l),tc(),t!==null&&M(Ze);break;case 24:Vl(Ht)}}function yu(t,l){try{var e=l.updateQueue,a=e!==null?e.lastEffect:null;if(a!==null){var u=a.next;e=u;do{if((e.tag&t)===t){a=void 0;var n=e.create,i=e.inst;a=n(),i.destroy=a}e=e.next}while(e!==u)}}catch(c){ht(l,l.return,c)}}function ge(t,l,e){try{var a=l.updateQueue,u=a!==null?a.lastEffect:null;if(u!==null){var n=u.next;a=n;do{if((a.tag&t)===t){var i=a.inst,c=i.destroy;if(c!==void 0){i.destroy=void 0,u=l;var s=e,h=c;try{h()}catch(z){ht(u,s,z)}}}a=a.next}while(a!==n)}}catch(z){ht(l,l.return,z)}}function so(t){var l=t.updateQueue;if(l!==null){var e=t.stateNode;try{ts(l,e)}catch(a){ht(t,t.return,a)}}}function oo(t,l,e){e.props=Je(t.type,t.memoizedProps),e.state=t.memoizedState;try{e.componentWillUnmount()}catch(a){ht(t,l,a)}}function mu(t,l){try{var e=t.ref;if(e!==null){switch(t.tag){case 26:case 27:case 5:var a=t.stateNode;break;case 30:a=t.stateNode;break;default:a=t.stateNode}typeof e=="function"?t.refCleanup=e(a):e.current=a}}catch(u){ht(t,l,u)}}function ql(t,l){var e=t.ref,a=t.refCleanup;if(e!==null)if(typeof a=="function")try{a()}catch(u){ht(t,l,u)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof e=="function")try{e(null)}catch(u){ht(t,l,u)}else e.current=null}function yo(t){var l=t.type,e=t.memoizedProps,a=t.stateNode;try{t:switch(l){case"button":case"input":case"select":case"textarea":e.autoFocus&&a.focus();break t;case"img":e.src?a.src=e.src:e.srcSet&&(a.srcset=e.srcSet)}}catch(u){ht(t,t.return,u)}}function Cc(t,l,e){try{var a=t.stateNode;em(a,t.type,e,l),a[el]=l}catch(u){ht(t,t.return,u)}}function mo(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&Te(t.type)||t.tag===4}function jc(t){t:for(;;){for(;t.sibling===null;){if(t.return===null||mo(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&Te(t.type)||t.flags&2||t.child===null||t.tag===4)continue t;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function Rc(t,l,e){var a=t.tag;if(a===5||a===6)t=t.stateNode,l?(e.nodeType===9?e.body:e.nodeName==="HTML"?e.ownerDocument.body:e).insertBefore(t,l):(l=e.nodeType===9?e.body:e.nodeName==="HTML"?e.ownerDocument.body:e,l.appendChild(t),e=e._reactRootContainer,e!=null||l.onclick!==null||(l.onclick=Xl));else if(a!==4&&(a===27&&Te(t.type)&&(e=t.stateNode,l=null),t=t.child,t!==null))for(Rc(t,l,e),t=t.sibling;t!==null;)Rc(t,l,e),t=t.sibling}function Mn(t,l,e){var a=t.tag;if(a===5||a===6)t=t.stateNode,l?e.insertBefore(t,l):e.appendChild(t);else if(a!==4&&(a===27&&Te(t.type)&&(e=t.stateNode),t=t.child,t!==null))for(Mn(t,l,e),t=t.sibling;t!==null;)Mn(t,l,e),t=t.sibling}function ho(t){var l=t.stateNode,e=t.memoizedProps;try{for(var a=t.type,u=l.attributes;u.length;)l.removeAttributeNode(u[0]);kt(l,a,e),l[Lt]=t,l[el]=e}catch(n){ht(t,t.return,n)}}var $l=!1,Yt=!1,Hc=!1,go=typeof WeakSet=="function"?WeakSet:Set,Zt=null;function Hy(t,l){if(t=t.containerInfo,ef=Jn,t=_r(t),Oi(t)){if("selectionStart"in t)var e={start:t.selectionStart,end:t.selectionEnd};else t:{e=(e=t.ownerDocument)&&e.defaultView||window;var a=e.getSelection&&e.getSelection();if(a&&a.rangeCount!==0){e=a.anchorNode;var u=a.anchorOffset,n=a.focusNode;a=a.focusOffset;try{e.nodeType,n.nodeType}catch{e=null;break t}var i=0,c=-1,s=-1,h=0,z=0,A=t,g=null;l:for(;;){for(var b;A!==e||u!==0&&A.nodeType!==3||(c=i+u),A!==n||a!==0&&A.nodeType!==3||(s=i+a),A.nodeType===3&&(i+=A.nodeValue.length),(b=A.firstChild)!==null;)g=A,A=b;for(;;){if(A===t)break l;if(g===e&&++h===u&&(c=i),g===n&&++z===a&&(s=i),(b=A.nextSibling)!==null)break;A=g,g=A.parentNode}A=b}e=c===-1||s===-1?null:{start:c,end:s}}else e=null}e=e||{start:0,end:0}}else e=null;for(af={focusedElem:t,selectionRange:e},Jn=!1,Zt=l;Zt!==null;)if(l=Zt,t=l.child,(l.subtreeFlags&1028)!==0&&t!==null)t.return=l,Zt=t;else for(;Zt!==null;){switch(l=Zt,n=l.alternate,t=l.flags,l.tag){case 0:if((t&4)!==0&&(t=l.updateQueue,t=t!==null?t.events:null,t!==null))for(e=0;e title"))),kt(n,a,e),n[Lt]=t,Qt(n),a=n;break t;case"link":var i=zd("link","href",u).get(a+(e.href||""));if(i){for(var c=0;cpt&&(i=pt,pt=w,w=i);var y=Er(c,w),d=Er(c,pt);if(y&&d&&(b.rangeCount!==1||b.anchorNode!==y.node||b.anchorOffset!==y.offset||b.focusNode!==d.node||b.focusOffset!==d.offset)){var m=A.createRange();m.setStart(y.node,y.offset),b.removeAllRanges(),w>pt?(b.addRange(m),b.extend(d.node,d.offset)):(m.setEnd(d.node,d.offset),b.addRange(m))}}}}for(A=[],b=c;b=b.parentNode;)b.nodeType===1&&A.push({element:b,left:b.scrollLeft,top:b.scrollTop});for(typeof c.focus=="function"&&c.focus(),c=0;ce?32:e,x.T=null,e=Zc,Zc=null;var n=Se,i=le;if(Gt=0,_a=Se=null,le=0,(st&6)!==0)throw Error(f(331));var c=st;if(st|=4,_o(n.current),Ao(n,n.current,i,e),st=c,Su(0,!1),sl&&typeof sl.onPostCommitFiberRoot=="function")try{sl.onPostCommitFiberRoot(Ya,n)}catch{}return!0}finally{C.p=u,x.T=a,Vo(t,l)}}function Jo(t,l,e){l=Tl(e,l),l=xc(t.stateNode,l,2),t=ye(t,l,2),t!==null&&(Xa(t,2),Yl(t))}function ht(t,l,e){if(t.tag===3)Jo(t,t,e);else for(;l!==null;){if(l.tag===3){Jo(l,t,e);break}else if(l.tag===1){var a=l.stateNode;if(typeof l.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(pe===null||!pe.has(a))){t=Tl(e,t),e=Js(2),a=ye(l,e,2),a!==null&&(ks(e,a,l,t),Xa(a,2),Yl(a));break}}l=l.return}}function Kc(t,l,e){var a=t.pingCache;if(a===null){a=t.pingCache=new Yy;var u=new Set;a.set(l,u)}else u=a.get(l),u===void 0&&(u=new Set,a.set(l,u));u.has(e)||(Yc=!0,u.add(e),t=wy.bind(null,t,l,e),l.then(t,t))}function wy(t,l,e){var a=t.pingCache;a!==null&&a.delete(l),t.pingedLanes|=t.suspendedLanes&e,t.warmLanes&=~e,St===t&&(et&e)===e&&(Ut===4||Ut===3&&(et&62914560)===et&&300>rl()-Nn?(st&2)===0&&Oa(t,0):Gc|=e,Ma===et&&(Ma=0)),Yl(t)}function ko(t,l){l===0&&(l=Qf()),t=qe(t,l),t!==null&&(Xa(t,l),Yl(t))}function Ly(t){var l=t.memoizedState,e=0;l!==null&&(e=l.retryLane),ko(t,e)}function Vy(t,l){var e=0;switch(t.tag){case 31:case 13:var a=t.stateNode,u=t.memoizedState;u!==null&&(e=u.retryLane);break;case 19:a=t.stateNode;break;case 22:a=t.stateNode._retryCache;break;default:throw Error(f(314))}a!==null&&a.delete(l),ko(t,e)}function Ky(t,l){return ni(t,l)}var Bn=null,Da=null,Jc=!1,qn=!1,kc=!1,ze=0;function Yl(t){t!==Da&&t.next===null&&(Da===null?Bn=Da=t:Da=Da.next=t),qn=!0,Jc||(Jc=!0,ky())}function Su(t,l){if(!kc&&qn){kc=!0;do for(var e=!1,a=Bn;a!==null;){if(t!==0){var u=a.pendingLanes;if(u===0)var n=0;else{var i=a.suspendedLanes,c=a.pingedLanes;n=(1<<31-ol(42|t)+1)-1,n&=u&~(i&~c),n=n&201326741?n&201326741|1:n?n|2:0}n!==0&&(e=!0,Io(a,n))}else n=et,n=Xu(a,a===St?n:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(n&3)===0||Ga(a,n)||(e=!0,Io(a,n));a=a.next}while(e);kc=!1}}function Jy(){Wo()}function Wo(){qn=Jc=!1;var t=0;ze!==0&&um()&&(t=ze);for(var l=rl(),e=null,a=Bn;a!==null;){var u=a.next,n=$o(a,l);n===0?(a.next=null,e===null?Bn=u:e.next=u,u===null&&(Da=e)):(e=a,(t!==0||(n&3)!==0)&&(qn=!0)),a=u}Gt!==0&&Gt!==5||Su(t),ze!==0&&(ze=0)}function $o(t,l){for(var e=t.suspendedLanes,a=t.pingedLanes,u=t.expirationTimes,n=t.pendingLanes&-62914561;0c)break;var z=s.transferSize,A=s.initiatorType;z&&id(A)&&(s=s.responseEnd,i+=z*(s"u"?null:document;function bd(t,l,e){var a=Ua;if(a&&typeof l=="string"&&l){var u=xl(l);u='link[rel="'+t+'"][href="'+u+'"]',typeof e=="string"&&(u+='[crossorigin="'+e+'"]'),vd.has(u)||(vd.add(u),t={rel:t,crossOrigin:e,href:l},a.querySelector(u)===null&&(l=a.createElement("link"),kt(l,"link",t),Qt(l),a.head.appendChild(l)))}}function ym(t){ee.D(t),bd("dns-prefetch",t,null)}function mm(t,l){ee.C(t,l),bd("preconnect",t,l)}function hm(t,l,e){ee.L(t,l,e);var a=Ua;if(a&&t&&l){var u='link[rel="preload"][as="'+xl(l)+'"]';l==="image"&&e&&e.imageSrcSet?(u+='[imagesrcset="'+xl(e.imageSrcSet)+'"]',typeof e.imageSizes=="string"&&(u+='[imagesizes="'+xl(e.imageSizes)+'"]')):u+='[href="'+xl(t)+'"]';var n=u;switch(l){case"style":n=Ca(t);break;case"script":n=ja(t)}Nl.has(n)||(t=H({rel:"preload",href:l==="image"&&e&&e.imageSrcSet?void 0:t,as:l},e),Nl.set(n,t),a.querySelector(u)!==null||l==="style"&&a.querySelector(Au(n))||l==="script"&&a.querySelector(Eu(n))||(l=a.createElement("link"),kt(l,"link",t),Qt(l),a.head.appendChild(l)))}}function gm(t,l){ee.m(t,l);var e=Ua;if(e&&t){var a=l&&typeof l.as=="string"?l.as:"script",u='link[rel="modulepreload"][as="'+xl(a)+'"][href="'+xl(t)+'"]',n=u;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":n=ja(t)}if(!Nl.has(n)&&(t=H({rel:"modulepreload",href:t},l),Nl.set(n,t),e.querySelector(u)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(e.querySelector(Eu(n)))return}a=e.createElement("link"),kt(a,"link",t),Qt(a),e.head.appendChild(a)}}}function vm(t,l,e){ee.S(t,l,e);var a=Ua;if(a&&t){var u=ta(a).hoistableStyles,n=Ca(t);l=l||"default";var i=u.get(n);if(!i){var c={loading:0,preload:null};if(i=a.querySelector(Au(n)))c.loading=5;else{t=H({rel:"stylesheet",href:t,"data-precedence":l},e),(e=Nl.get(n))&&of(t,e);var s=i=a.createElement("link");Qt(s),kt(s,"link",t),s._p=new Promise(function(h,z){s.onload=h,s.onerror=z}),s.addEventListener("load",function(){c.loading|=1}),s.addEventListener("error",function(){c.loading|=2}),c.loading|=4,Zn(i,l,a)}i={type:"stylesheet",instance:i,count:1,state:c},u.set(n,i)}}}function bm(t,l){ee.X(t,l);var e=Ua;if(e&&t){var a=ta(e).hoistableScripts,u=ja(t),n=a.get(u);n||(n=e.querySelector(Eu(u)),n||(t=H({src:t,async:!0},l),(l=Nl.get(u))&&df(t,l),n=e.createElement("script"),Qt(n),kt(n,"link",t),e.head.appendChild(n)),n={type:"script",instance:n,count:1,state:null},a.set(u,n))}}function pm(t,l){ee.M(t,l);var e=Ua;if(e&&t){var a=ta(e).hoistableScripts,u=ja(t),n=a.get(u);n||(n=e.querySelector(Eu(u)),n||(t=H({src:t,async:!0,type:"module"},l),(l=Nl.get(u))&&df(t,l),n=e.createElement("script"),Qt(n),kt(n,"link",t),e.head.appendChild(n)),n={type:"script",instance:n,count:1,state:null},a.set(u,n))}}function pd(t,l,e,a){var u=(u=P.current)?Qn(u):null;if(!u)throw Error(f(446));switch(t){case"meta":case"title":return null;case"style":return typeof e.precedence=="string"&&typeof e.href=="string"?(l=Ca(e.href),e=ta(u).hoistableStyles,a=e.get(l),a||(a={type:"style",instance:null,count:0,state:null},e.set(l,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(e.rel==="stylesheet"&&typeof e.href=="string"&&typeof e.precedence=="string"){t=Ca(e.href);var n=ta(u).hoistableStyles,i=n.get(t);if(i||(u=u.ownerDocument||u,i={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},n.set(t,i),(n=u.querySelector(Au(t)))&&!n._p&&(i.instance=n,i.state.loading=5),Nl.has(t)||(e={rel:"preload",as:"style",href:e.href,crossOrigin:e.crossOrigin,integrity:e.integrity,media:e.media,hrefLang:e.hrefLang,referrerPolicy:e.referrerPolicy},Nl.set(t,e),n||Sm(u,t,e,i.state))),l&&a===null)throw Error(f(528,""));return i}if(l&&a!==null)throw Error(f(529,""));return null;case"script":return l=e.async,e=e.src,typeof e=="string"&&l&&typeof l!="function"&&typeof l!="symbol"?(l=ja(e),e=ta(u).hoistableScripts,a=e.get(l),a||(a={type:"script",instance:null,count:0,state:null},e.set(l,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(f(444,t))}}function Ca(t){return'href="'+xl(t)+'"'}function Au(t){return'link[rel="stylesheet"]['+t+"]"}function Sd(t){return H({},t,{"data-precedence":t.precedence,precedence:null})}function Sm(t,l,e,a){t.querySelector('link[rel="preload"][as="style"]['+l+"]")?a.loading=1:(l=t.createElement("link"),a.preload=l,l.addEventListener("load",function(){return a.loading|=1}),l.addEventListener("error",function(){return a.loading|=2}),kt(l,"link",e),Qt(l),t.head.appendChild(l))}function ja(t){return'[src="'+xl(t)+'"]'}function Eu(t){return"script[async]"+t}function xd(t,l,e){if(l.count++,l.instance===null)switch(l.type){case"style":var a=t.querySelector('style[data-href~="'+xl(e.href)+'"]');if(a)return l.instance=a,Qt(a),a;var u=H({},e,{"data-href":e.href,"data-precedence":e.precedence,href:null,precedence:null});return a=(t.ownerDocument||t).createElement("style"),Qt(a),kt(a,"style",u),Zn(a,e.precedence,t),l.instance=a;case"stylesheet":u=Ca(e.href);var n=t.querySelector(Au(u));if(n)return l.state.loading|=4,l.instance=n,Qt(n),n;a=Sd(e),(u=Nl.get(u))&&of(a,u),n=(t.ownerDocument||t).createElement("link"),Qt(n);var i=n;return i._p=new Promise(function(c,s){i.onload=c,i.onerror=s}),kt(n,"link",a),l.state.loading|=4,Zn(n,e.precedence,t),l.instance=n;case"script":return n=ja(e.src),(u=t.querySelector(Eu(n)))?(l.instance=u,Qt(u),u):(a=e,(u=Nl.get(n))&&(a=H({},e),df(a,u)),t=t.ownerDocument||t,u=t.createElement("script"),Qt(u),kt(u,"link",a),t.head.appendChild(u),l.instance=u);case"void":return null;default:throw Error(f(443,l.type))}else l.type==="stylesheet"&&(l.state.loading&4)===0&&(a=l.instance,l.state.loading|=4,Zn(a,e.precedence,t));return l.instance}function Zn(t,l,e){for(var a=e.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),u=a.length?a[a.length-1]:null,n=u,i=0;i title"):null)}function xm(t,l,e){if(e===1||l.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof l.precedence!="string"||typeof l.href!="string"||l.href==="")break;return!0;case"link":if(typeof l.rel!="string"||typeof l.href!="string"||l.href===""||l.onLoad||l.onError)break;return l.rel==="stylesheet"?(t=l.disabled,typeof l.precedence=="string"&&t==null):!0;case"script":if(l.async&&typeof l.async!="function"&&typeof l.async!="symbol"&&!l.onLoad&&!l.onError&&l.src&&typeof l.src=="string")return!0}return!1}function Ad(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function zm(t,l,e,a){if(e.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(e.state.loading&4)===0){if(e.instance===null){var u=Ca(a.href),n=l.querySelector(Au(u));if(n){l=n._p,l!==null&&typeof l=="object"&&typeof l.then=="function"&&(t.count++,t=Ln.bind(t),l.then(t,t)),e.state.loading|=4,e.instance=n,Qt(n);return}n=l.ownerDocument||l,a=Sd(a),(u=Nl.get(u))&&of(a,u),n=n.createElement("link"),Qt(n);var i=n;i._p=new Promise(function(c,s){i.onload=c,i.onerror=s}),kt(n,"link",a),e.instance=n}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(e,l),(l=e.state.preload)&&(e.state.loading&3)===0&&(t.count++,e=Ln.bind(t),l.addEventListener("load",e),l.addEventListener("error",e))}}var yf=0;function Tm(t,l){return t.stylesheets&&t.count===0&&Kn(t,t.stylesheets),0yf?50:800)+l);return t.unsuspend=e,function(){t.unsuspend=null,clearTimeout(a),clearTimeout(u)}}:null}function Ln(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Kn(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var Vn=null;function Kn(t,l){t.stylesheets=null,t.unsuspend!==null&&(t.count++,Vn=new Map,l.forEach(Am,t),Vn=null,Ln.call(t))}function Am(t,l){if(!(l.state.loading&4)){var e=Vn.get(t);if(e)var a=e.get(null);else{e=new Map,Vn.set(t,e);for(var u=t.querySelectorAll("link[data-precedence],style[data-precedence]"),n=0;n"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(r)}catch(v){console.error(v)}}return r(),zf.exports=Xm(),zf.exports}var Zm=Qm();const wm=r=>r.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Pd=(...r)=>r.filter((v,S,f)=>!!v&&v.trim()!==""&&f.indexOf(v)===S).join(" ").trim();var Lm={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};const Vm=xt.forwardRef(({color:r="currentColor",size:v=24,strokeWidth:S=2,absoluteStrokeWidth:f,className:_="",children:O,iconNode:D,...U},N)=>xt.createElement("svg",{ref:N,...Lm,width:v,height:v,stroke:r,strokeWidth:f?Number(S)*24/Number(v):S,className:Pd("lucide",_),...U},[...D.map(([p,R])=>xt.createElement(p,R)),...Array.isArray(O)?O:[O]]));const Dl=(r,v)=>{const S=xt.forwardRef(({className:f,..._},O)=>xt.createElement(Vm,{ref:O,iconNode:v,className:Pd(`lucide-${wm(r)}`,f),..._}));return S.displayName=`${r}`,S};const Km=Dl("Binary",[["rect",{x:"14",y:"14",width:"4",height:"6",rx:"2",key:"p02svl"}],["rect",{x:"6",y:"4",width:"4",height:"6",rx:"2",key:"xm4xkj"}],["path",{d:"M6 20h4",key:"1i6q5t"}],["path",{d:"M14 10h4",key:"ru81e7"}],["path",{d:"M6 14h2v6",key:"16z9wg"}],["path",{d:"M14 4h2v6",key:"1idq9u"}]]);const Jm=Dl("BookText",[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20",key:"k3hazp"}],["path",{d:"M8 11h8",key:"vwpz6n"}],["path",{d:"M8 7h6",key:"1f0q6e"}]]);const km=Dl("EyeOff",[["path",{d:"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49",key:"ct8e1f"}],["path",{d:"M14.084 14.158a3 3 0 0 1-4.242-4.242",key:"151rxh"}],["path",{d:"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143",key:"13bj9a"}],["path",{d:"m2 2 20 20",key:"1ooewy"}]]);const Wm=Dl("Eye",[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",key:"1nclc0"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);const $m=Dl("Globe",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",key:"13o1zl"}],["path",{d:"M2 12h20",key:"9i4pu4"}]]);const kd=Dl("LoaderCircle",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);const Fm=Dl("Lock",[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]]);const Im=Dl("LogIn",[["path",{d:"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4",key:"u53s6r"}],["polyline",{points:"10 17 15 12 10 7",key:"1ail0h"}],["line",{x1:"15",x2:"3",y1:"12",y2:"12",key:"v6grx8"}]]);const Pm=Dl("RotateCw",[["path",{d:"M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8",key:"1p45f6"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}]]);const th=Dl("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);const lh=Dl("Waypoints",[["circle",{cx:"12",cy:"4.5",r:"2.5",key:"r5ysbb"}],["path",{d:"m10.2 6.3-3.9 3.9",key:"1nzqf6"}],["circle",{cx:"4.5",cy:"12",r:"2.5",key:"jydg6v"}],["path",{d:"M7 12h10",key:"b7w52i"}],["circle",{cx:"19.5",cy:"12",r:"2.5",key:"1piiel"}],["path",{d:"m13.8 17.7 3.9-3.9",key:"1wyg1y"}],["circle",{cx:"12",cy:"19.5",r:"2.5",key:"13o1pw"}]]);const eh=Dl("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]);function t0(){return globalThis.__DATA__??{}}function l0(r){var v,S,f="";if(typeof r=="string"||typeof r=="number")f+=r;else if(typeof r=="object")if(Array.isArray(r)){var _=r.length;for(v=0;v<_;v++)r[v]&&(S=l0(r[v]))&&(f&&(f+=" "),f+=S)}else for(S in r)r[S]&&(f&&(f+=" "),f+=S);return f}function ah(){for(var r,v,S=0,f="",_=arguments.length;S<_;S++)(r=arguments[S])&&(v=l0(r))&&(f&&(f+=" "),f+=v);return f}const Hf="-",uh=r=>{const v=ih(r),{conflictingClassGroups:S,conflictingClassGroupModifiers:f}=r;return{getClassGroupId:D=>{const U=D.split(Hf);return U[0]===""&&U.length!==1&&U.shift(),e0(U,v)||nh(D)},getConflictingClassGroupIds:(D,U)=>{const N=S[D]||[];return U&&f[D]?[...N,...f[D]]:N}}},e0=(r,v)=>{if(r.length===0)return v.classGroupId;const S=r[0],f=v.nextPart.get(S),_=f?e0(r.slice(1),f):void 0;if(_)return _;if(v.validators.length===0)return;const O=r.join(Hf);return v.validators.find(({validator:D})=>D(O))?.classGroupId},Wd=/^\[(.+)\]$/,nh=r=>{if(Wd.test(r)){const v=Wd.exec(r)[1],S=v?.substring(0,v.indexOf(":"));if(S)return"arbitrary.."+S}},ih=r=>{const{theme:v,prefix:S}=r,f={nextPart:new Map,validators:[]};return fh(Object.entries(r.classGroups),S).forEach(([O,D])=>{Df(D,f,O,v)}),f},Df=(r,v,S,f)=>{r.forEach(_=>{if(typeof _=="string"){const O=_===""?v:$d(v,_);O.classGroupId=S;return}if(typeof _=="function"){if(ch(_)){Df(_(f),v,S,f);return}v.validators.push({validator:_,classGroupId:S});return}Object.entries(_).forEach(([O,D])=>{Df(D,$d(v,O),S,f)})})},$d=(r,v)=>{let S=r;return v.split(Hf).forEach(f=>{S.nextPart.has(f)||S.nextPart.set(f,{nextPart:new Map,validators:[]}),S=S.nextPart.get(f)}),S},ch=r=>r.isThemeGetter,fh=(r,v)=>v?r.map(([S,f])=>{const _=f.map(O=>typeof O=="string"?v+O:typeof O=="object"?Object.fromEntries(Object.entries(O).map(([D,U])=>[v+D,U])):O);return[S,_]}):r,rh=r=>{if(r<1)return{get:()=>{},set:()=>{}};let v=0,S=new Map,f=new Map;const _=(O,D)=>{S.set(O,D),v++,v>r&&(v=0,f=S,S=new Map)};return{get(O){let D=S.get(O);if(D!==void 0)return D;if((D=f.get(O))!==void 0)return _(O,D),D},set(O,D){S.has(O)?S.set(O,D):_(O,D)}}},a0="!",sh=r=>{const{separator:v,experimentalParseClassName:S}=r,f=v.length===1,_=v[0],O=v.length,D=U=>{const N=[];let p=0,R=0,H;for(let Q=0;QR?H-R:void 0;return{modifiers:N,hasImportantModifier:ot,baseClassName:ct,maybePostfixModifierPosition:G}};return S?U=>S({className:U,parseClassName:D}):D},oh=r=>{if(r.length<=1)return r;const v=[];let S=[];return r.forEach(f=>{f[0]==="["?(v.push(...S.sort(),f),S=[]):S.push(f)}),v.push(...S.sort()),v},dh=r=>({cache:rh(r.cacheSize),parseClassName:sh(r),...uh(r)}),yh=/\s+/,mh=(r,v)=>{const{parseClassName:S,getClassGroupId:f,getConflictingClassGroupIds:_}=v,O=[],D=r.trim().split(yh);let U="";for(let N=D.length-1;N>=0;N-=1){const p=D[N],{modifiers:R,hasImportantModifier:H,baseClassName:L,maybePostfixModifierPosition:ot}=S(p);let ct=!!ot,G=f(ct?L.substring(0,ot):L);if(!G){if(!ct){U=p+(U.length>0?" "+U:U);continue}if(G=f(L),!G){U=p+(U.length>0?" "+U:U);continue}ct=!1}const Q=oh(R).join(":"),V=H?Q+a0:Q,gt=V+G;if(O.includes(gt))continue;O.push(gt);const zt=_(G,ct);for(let _t=0;_t0?" "+U:U)}return U};function hh(){let r=0,v,S,f="";for(;r{if(typeof r=="string")return r;let v,S="";for(let f=0;fH(R),r());return S=dh(p),f=S.cache.get,_=S.cache.set,O=U,U(N)}function U(N){const p=f(N);if(p)return p;const R=mh(N,S);return _(N,R),R}return function(){return O(hh.apply(null,arguments))}}const Et=r=>{const v=S=>S[r]||[];return v.isThemeGetter=!0,v},n0=/^\[(?:([a-z-]+):)?(.+)\]$/i,vh=/^\d+\/\d+$/,bh=new Set(["px","full","screen"]),ph=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,Sh=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,xh=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,zh=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,Th=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,ae=r=>Ha(r)||bh.has(r)||vh.test(r),Ne=r=>Ba(r,"length",Uh),Ha=r=>!!r&&!Number.isNaN(Number(r)),Mf=r=>Ba(r,"number",Ha),Cu=r=>!!r&&Number.isInteger(Number(r)),Ah=r=>r.endsWith("%")&&Ha(r.slice(0,-1)),F=r=>n0.test(r),De=r=>ph.test(r),Eh=new Set(["length","size","percentage"]),Mh=r=>Ba(r,Eh,i0),_h=r=>Ba(r,"position",i0),Oh=new Set(["image","url"]),Nh=r=>Ba(r,Oh,jh),Dh=r=>Ba(r,"",Ch),ju=()=>!0,Ba=(r,v,S)=>{const f=n0.exec(r);return f?f[1]?typeof v=="string"?f[1]===v:v.has(f[1]):S(f[2]):!1},Uh=r=>Sh.test(r)&&!xh.test(r),i0=()=>!1,Ch=r=>zh.test(r),jh=r=>Th.test(r),Rh=()=>{const r=Et("colors"),v=Et("spacing"),S=Et("blur"),f=Et("brightness"),_=Et("borderColor"),O=Et("borderRadius"),D=Et("borderSpacing"),U=Et("borderWidth"),N=Et("contrast"),p=Et("grayscale"),R=Et("hueRotate"),H=Et("invert"),L=Et("gap"),ot=Et("gradientColorStops"),ct=Et("gradientColorStopPositions"),G=Et("inset"),Q=Et("margin"),V=Et("opacity"),gt=Et("padding"),zt=Et("saturate"),_t=Et("scale"),nt=Et("sepia"),Ot=Et("skew"),J=Et("space"),Nt=Et("translate"),Xt=()=>["auto","contain","none"],pl=()=>["auto","hidden","clip","visible","scroll"],Pt=()=>["auto",F,v],I=()=>[F,v],Rl=()=>["",ae,Ne],tl=()=>["auto",Ha,F],ll=()=>["bottom","center","left","left-bottom","left-top","right","right-bottom","right-top","top"],x=()=>["solid","dashed","dotted","double","none"],C=()=>["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"],Z=()=>["start","end","center","between","around","evenly","stretch"],it=()=>["","0",F],dt=()=>["auto","avoid","all","avoid-page","page","left","right","column"],o=()=>[Ha,F];return{cacheSize:500,separator:":",theme:{colors:[ju],spacing:[ae,Ne],blur:["none","",De,F],brightness:o(),borderColor:[r],borderRadius:["none","","full",De,F],borderSpacing:I(),borderWidth:Rl(),contrast:o(),grayscale:it(),hueRotate:o(),invert:it(),gap:I(),gradientColorStops:[r],gradientColorStopPositions:[Ah,Ne],inset:Pt(),margin:Pt(),opacity:o(),padding:I(),saturate:o(),scale:o(),sepia:it(),skew:o(),space:I(),translate:I()},classGroups:{aspect:[{aspect:["auto","square","video",F]}],container:["container"],columns:[{columns:[De]}],"break-after":[{"break-after":dt()}],"break-before":[{"break-before":dt()}],"break-inside":[{"break-inside":["auto","avoid","avoid-page","avoid-column"]}],"box-decoration":[{"box-decoration":["slice","clone"]}],box:[{box:["border","content"]}],display:["block","inline-block","inline","flex","inline-flex","table","inline-table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row-group","table-row","flow-root","grid","inline-grid","contents","list-item","hidden"],float:[{float:["right","left","none","start","end"]}],clear:[{clear:["left","right","both","none","start","end"]}],isolation:["isolate","isolation-auto"],"object-fit":[{object:["contain","cover","fill","none","scale-down"]}],"object-position":[{object:[...ll(),F]}],overflow:[{overflow:pl()}],"overflow-x":[{"overflow-x":pl()}],"overflow-y":[{"overflow-y":pl()}],overscroll:[{overscroll:Xt()}],"overscroll-x":[{"overscroll-x":Xt()}],"overscroll-y":[{"overscroll-y":Xt()}],position:["static","fixed","absolute","relative","sticky"],inset:[{inset:[G]}],"inset-x":[{"inset-x":[G]}],"inset-y":[{"inset-y":[G]}],start:[{start:[G]}],end:[{end:[G]}],top:[{top:[G]}],right:[{right:[G]}],bottom:[{bottom:[G]}],left:[{left:[G]}],visibility:["visible","invisible","collapse"],z:[{z:["auto",Cu,F]}],basis:[{basis:Pt()}],"flex-direction":[{flex:["row","row-reverse","col","col-reverse"]}],"flex-wrap":[{flex:["wrap","wrap-reverse","nowrap"]}],flex:[{flex:["1","auto","initial","none",F]}],grow:[{grow:it()}],shrink:[{shrink:it()}],order:[{order:["first","last","none",Cu,F]}],"grid-cols":[{"grid-cols":[ju]}],"col-start-end":[{col:["auto",{span:["full",Cu,F]},F]}],"col-start":[{"col-start":tl()}],"col-end":[{"col-end":tl()}],"grid-rows":[{"grid-rows":[ju]}],"row-start-end":[{row:["auto",{span:[Cu,F]},F]}],"row-start":[{"row-start":tl()}],"row-end":[{"row-end":tl()}],"grid-flow":[{"grid-flow":["row","col","dense","row-dense","col-dense"]}],"auto-cols":[{"auto-cols":["auto","min","max","fr",F]}],"auto-rows":[{"auto-rows":["auto","min","max","fr",F]}],gap:[{gap:[L]}],"gap-x":[{"gap-x":[L]}],"gap-y":[{"gap-y":[L]}],"justify-content":[{justify:["normal",...Z()]}],"justify-items":[{"justify-items":["start","end","center","stretch"]}],"justify-self":[{"justify-self":["auto","start","end","center","stretch"]}],"align-content":[{content:["normal",...Z(),"baseline"]}],"align-items":[{items:["start","end","center","baseline","stretch"]}],"align-self":[{self:["auto","start","end","center","stretch","baseline"]}],"place-content":[{"place-content":[...Z(),"baseline"]}],"place-items":[{"place-items":["start","end","center","baseline","stretch"]}],"place-self":[{"place-self":["auto","start","end","center","stretch"]}],p:[{p:[gt]}],px:[{px:[gt]}],py:[{py:[gt]}],ps:[{ps:[gt]}],pe:[{pe:[gt]}],pt:[{pt:[gt]}],pr:[{pr:[gt]}],pb:[{pb:[gt]}],pl:[{pl:[gt]}],m:[{m:[Q]}],mx:[{mx:[Q]}],my:[{my:[Q]}],ms:[{ms:[Q]}],me:[{me:[Q]}],mt:[{mt:[Q]}],mr:[{mr:[Q]}],mb:[{mb:[Q]}],ml:[{ml:[Q]}],"space-x":[{"space-x":[J]}],"space-x-reverse":["space-x-reverse"],"space-y":[{"space-y":[J]}],"space-y-reverse":["space-y-reverse"],w:[{w:["auto","min","max","fit","svw","lvw","dvw",F,v]}],"min-w":[{"min-w":[F,v,"min","max","fit"]}],"max-w":[{"max-w":[F,v,"none","full","min","max","fit","prose",{screen:[De]},De]}],h:[{h:[F,v,"auto","min","max","fit","svh","lvh","dvh"]}],"min-h":[{"min-h":[F,v,"min","max","fit","svh","lvh","dvh"]}],"max-h":[{"max-h":[F,v,"min","max","fit","svh","lvh","dvh"]}],size:[{size:[F,v,"auto","min","max","fit"]}],"font-size":[{text:["base",De,Ne]}],"font-smoothing":["antialiased","subpixel-antialiased"],"font-style":["italic","not-italic"],"font-weight":[{font:["thin","extralight","light","normal","medium","semibold","bold","extrabold","black",Mf]}],"font-family":[{font:[ju]}],"fvn-normal":["normal-nums"],"fvn-ordinal":["ordinal"],"fvn-slashed-zero":["slashed-zero"],"fvn-figure":["lining-nums","oldstyle-nums"],"fvn-spacing":["proportional-nums","tabular-nums"],"fvn-fraction":["diagonal-fractions","stacked-fractions"],tracking:[{tracking:["tighter","tight","normal","wide","wider","widest",F]}],"line-clamp":[{"line-clamp":["none",Ha,Mf]}],leading:[{leading:["none","tight","snug","normal","relaxed","loose",ae,F]}],"list-image":[{"list-image":["none",F]}],"list-style-type":[{list:["none","disc","decimal",F]}],"list-style-position":[{list:["inside","outside"]}],"placeholder-color":[{placeholder:[r]}],"placeholder-opacity":[{"placeholder-opacity":[V]}],"text-alignment":[{text:["left","center","right","justify","start","end"]}],"text-color":[{text:[r]}],"text-opacity":[{"text-opacity":[V]}],"text-decoration":["underline","overline","line-through","no-underline"],"text-decoration-style":[{decoration:[...x(),"wavy"]}],"text-decoration-thickness":[{decoration:["auto","from-font",ae,Ne]}],"underline-offset":[{"underline-offset":["auto",ae,F]}],"text-decoration-color":[{decoration:[r]}],"text-transform":["uppercase","lowercase","capitalize","normal-case"],"text-overflow":["truncate","text-ellipsis","text-clip"],"text-wrap":[{text:["wrap","nowrap","balance","pretty"]}],indent:[{indent:I()}],"vertical-align":[{align:["baseline","top","middle","bottom","text-top","text-bottom","sub","super",F]}],whitespace:[{whitespace:["normal","nowrap","pre","pre-line","pre-wrap","break-spaces"]}],break:[{break:["normal","words","all","keep"]}],hyphens:[{hyphens:["none","manual","auto"]}],content:[{content:["none",F]}],"bg-attachment":[{bg:["fixed","local","scroll"]}],"bg-clip":[{"bg-clip":["border","padding","content","text"]}],"bg-opacity":[{"bg-opacity":[V]}],"bg-origin":[{"bg-origin":["border","padding","content"]}],"bg-position":[{bg:[...ll(),_h]}],"bg-repeat":[{bg:["no-repeat",{repeat:["","x","y","round","space"]}]}],"bg-size":[{bg:["auto","cover","contain",Mh]}],"bg-image":[{bg:["none",{"gradient-to":["t","tr","r","br","b","bl","l","tl"]},Nh]}],"bg-color":[{bg:[r]}],"gradient-from-pos":[{from:[ct]}],"gradient-via-pos":[{via:[ct]}],"gradient-to-pos":[{to:[ct]}],"gradient-from":[{from:[ot]}],"gradient-via":[{via:[ot]}],"gradient-to":[{to:[ot]}],rounded:[{rounded:[O]}],"rounded-s":[{"rounded-s":[O]}],"rounded-e":[{"rounded-e":[O]}],"rounded-t":[{"rounded-t":[O]}],"rounded-r":[{"rounded-r":[O]}],"rounded-b":[{"rounded-b":[O]}],"rounded-l":[{"rounded-l":[O]}],"rounded-ss":[{"rounded-ss":[O]}],"rounded-se":[{"rounded-se":[O]}],"rounded-ee":[{"rounded-ee":[O]}],"rounded-es":[{"rounded-es":[O]}],"rounded-tl":[{"rounded-tl":[O]}],"rounded-tr":[{"rounded-tr":[O]}],"rounded-br":[{"rounded-br":[O]}],"rounded-bl":[{"rounded-bl":[O]}],"border-w":[{border:[U]}],"border-w-x":[{"border-x":[U]}],"border-w-y":[{"border-y":[U]}],"border-w-s":[{"border-s":[U]}],"border-w-e":[{"border-e":[U]}],"border-w-t":[{"border-t":[U]}],"border-w-r":[{"border-r":[U]}],"border-w-b":[{"border-b":[U]}],"border-w-l":[{"border-l":[U]}],"border-opacity":[{"border-opacity":[V]}],"border-style":[{border:[...x(),"hidden"]}],"divide-x":[{"divide-x":[U]}],"divide-x-reverse":["divide-x-reverse"],"divide-y":[{"divide-y":[U]}],"divide-y-reverse":["divide-y-reverse"],"divide-opacity":[{"divide-opacity":[V]}],"divide-style":[{divide:x()}],"border-color":[{border:[_]}],"border-color-x":[{"border-x":[_]}],"border-color-y":[{"border-y":[_]}],"border-color-s":[{"border-s":[_]}],"border-color-e":[{"border-e":[_]}],"border-color-t":[{"border-t":[_]}],"border-color-r":[{"border-r":[_]}],"border-color-b":[{"border-b":[_]}],"border-color-l":[{"border-l":[_]}],"divide-color":[{divide:[_]}],"outline-style":[{outline:["",...x()]}],"outline-offset":[{"outline-offset":[ae,F]}],"outline-w":[{outline:[ae,Ne]}],"outline-color":[{outline:[r]}],"ring-w":[{ring:Rl()}],"ring-w-inset":["ring-inset"],"ring-color":[{ring:[r]}],"ring-opacity":[{"ring-opacity":[V]}],"ring-offset-w":[{"ring-offset":[ae,Ne]}],"ring-offset-color":[{"ring-offset":[r]}],shadow:[{shadow:["","inner","none",De,Dh]}],"shadow-color":[{shadow:[ju]}],opacity:[{opacity:[V]}],"mix-blend":[{"mix-blend":[...C(),"plus-lighter","plus-darker"]}],"bg-blend":[{"bg-blend":C()}],filter:[{filter:["","none"]}],blur:[{blur:[S]}],brightness:[{brightness:[f]}],contrast:[{contrast:[N]}],"drop-shadow":[{"drop-shadow":["","none",De,F]}],grayscale:[{grayscale:[p]}],"hue-rotate":[{"hue-rotate":[R]}],invert:[{invert:[H]}],saturate:[{saturate:[zt]}],sepia:[{sepia:[nt]}],"backdrop-filter":[{"backdrop-filter":["","none"]}],"backdrop-blur":[{"backdrop-blur":[S]}],"backdrop-brightness":[{"backdrop-brightness":[f]}],"backdrop-contrast":[{"backdrop-contrast":[N]}],"backdrop-grayscale":[{"backdrop-grayscale":[p]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[R]}],"backdrop-invert":[{"backdrop-invert":[H]}],"backdrop-opacity":[{"backdrop-opacity":[V]}],"backdrop-saturate":[{"backdrop-saturate":[zt]}],"backdrop-sepia":[{"backdrop-sepia":[nt]}],"border-collapse":[{border:["collapse","separate"]}],"border-spacing":[{"border-spacing":[D]}],"border-spacing-x":[{"border-spacing-x":[D]}],"border-spacing-y":[{"border-spacing-y":[D]}],"table-layout":[{table:["auto","fixed"]}],caption:[{caption:["top","bottom"]}],transition:[{transition:["none","all","","colors","opacity","shadow","transform",F]}],duration:[{duration:o()}],ease:[{ease:["linear","in","out","in-out",F]}],delay:[{delay:o()}],animate:[{animate:["none","spin","ping","pulse","bounce",F]}],transform:[{transform:["","gpu","none"]}],scale:[{scale:[_t]}],"scale-x":[{"scale-x":[_t]}],"scale-y":[{"scale-y":[_t]}],rotate:[{rotate:[Cu,F]}],"translate-x":[{"translate-x":[Nt]}],"translate-y":[{"translate-y":[Nt]}],"skew-x":[{"skew-x":[Ot]}],"skew-y":[{"skew-y":[Ot]}],"transform-origin":[{origin:["center","top","top-right","right","bottom-right","bottom","bottom-left","left","top-left",F]}],accent:[{accent:["auto",r]}],appearance:[{appearance:["none","auto"]}],cursor:[{cursor:["auto","default","pointer","wait","text","move","help","not-allowed","none","context-menu","progress","cell","crosshair","vertical-text","alias","copy","no-drop","grab","grabbing","all-scroll","col-resize","row-resize","n-resize","e-resize","s-resize","w-resize","ne-resize","nw-resize","se-resize","sw-resize","ew-resize","ns-resize","nesw-resize","nwse-resize","zoom-in","zoom-out",F]}],"caret-color":[{caret:[r]}],"pointer-events":[{"pointer-events":["none","auto"]}],resize:[{resize:["none","y","x",""]}],"scroll-behavior":[{scroll:["auto","smooth"]}],"scroll-m":[{"scroll-m":I()}],"scroll-mx":[{"scroll-mx":I()}],"scroll-my":[{"scroll-my":I()}],"scroll-ms":[{"scroll-ms":I()}],"scroll-me":[{"scroll-me":I()}],"scroll-mt":[{"scroll-mt":I()}],"scroll-mr":[{"scroll-mr":I()}],"scroll-mb":[{"scroll-mb":I()}],"scroll-ml":[{"scroll-ml":I()}],"scroll-p":[{"scroll-p":I()}],"scroll-px":[{"scroll-px":I()}],"scroll-py":[{"scroll-py":I()}],"scroll-ps":[{"scroll-ps":I()}],"scroll-pe":[{"scroll-pe":I()}],"scroll-pt":[{"scroll-pt":I()}],"scroll-pr":[{"scroll-pr":I()}],"scroll-pb":[{"scroll-pb":I()}],"scroll-pl":[{"scroll-pl":I()}],"snap-align":[{snap:["start","end","center","align-none"]}],"snap-stop":[{snap:["normal","always"]}],"snap-type":[{snap:["none","x","y","both"]}],"snap-strictness":[{snap:["mandatory","proximity"]}],touch:[{touch:["auto","none","manipulation"]}],"touch-x":[{"touch-pan":["x","left","right"]}],"touch-y":[{"touch-pan":["y","up","down"]}],"touch-pz":["touch-pinch-zoom"],select:[{select:["none","text","all","auto"]}],"will-change":[{"will-change":["auto","scroll","contents","transform",F]}],fill:[{fill:[r,"none"]}],"stroke-w":[{stroke:[ae,Ne,Mf]}],stroke:[{stroke:[r,"none"]}],sr:["sr-only","not-sr-only"],"forced-color-adjust":[{"forced-color-adjust":["auto","none"]}]},conflictingClassGroups:{overflow:["overflow-x","overflow-y"],overscroll:["overscroll-x","overscroll-y"],inset:["inset-x","inset-y","start","end","top","right","bottom","left"],"inset-x":["right","left"],"inset-y":["top","bottom"],flex:["basis","grow","shrink"],gap:["gap-x","gap-y"],p:["px","py","ps","pe","pt","pr","pb","pl"],px:["pr","pl"],py:["pt","pb"],m:["mx","my","ms","me","mt","mr","mb","ml"],mx:["mr","ml"],my:["mt","mb"],size:["w","h"],"font-size":["leading"],"fvn-normal":["fvn-ordinal","fvn-slashed-zero","fvn-figure","fvn-spacing","fvn-fraction"],"fvn-ordinal":["fvn-normal"],"fvn-slashed-zero":["fvn-normal"],"fvn-figure":["fvn-normal"],"fvn-spacing":["fvn-normal"],"fvn-fraction":["fvn-normal"],"line-clamp":["display","overflow"],rounded:["rounded-s","rounded-e","rounded-t","rounded-r","rounded-b","rounded-l","rounded-ss","rounded-se","rounded-ee","rounded-es","rounded-tl","rounded-tr","rounded-br","rounded-bl"],"rounded-s":["rounded-ss","rounded-es"],"rounded-e":["rounded-se","rounded-ee"],"rounded-t":["rounded-tl","rounded-tr"],"rounded-r":["rounded-tr","rounded-br"],"rounded-b":["rounded-br","rounded-bl"],"rounded-l":["rounded-tl","rounded-bl"],"border-spacing":["border-spacing-x","border-spacing-y"],"border-w":["border-w-s","border-w-e","border-w-t","border-w-r","border-w-b","border-w-l"],"border-w-x":["border-w-r","border-w-l"],"border-w-y":["border-w-t","border-w-b"],"border-color":["border-color-s","border-color-e","border-color-t","border-color-r","border-color-b","border-color-l"],"border-color-x":["border-color-r","border-color-l"],"border-color-y":["border-color-t","border-color-b"],"scroll-m":["scroll-mx","scroll-my","scroll-ms","scroll-me","scroll-mt","scroll-mr","scroll-mb","scroll-ml"],"scroll-mx":["scroll-mr","scroll-ml"],"scroll-my":["scroll-mt","scroll-mb"],"scroll-p":["scroll-px","scroll-py","scroll-ps","scroll-pe","scroll-pt","scroll-pr","scroll-pb","scroll-pl"],"scroll-px":["scroll-pr","scroll-pl"],"scroll-py":["scroll-pt","scroll-pb"],touch:["touch-x","touch-y","touch-pz"],"touch-x":["touch"],"touch-y":["touch"],"touch-pz":["touch"]},conflictingClassGroupModifiers:{"font-size":["leading"]}}},Hh=gh(Rh);function wt(...r){return Hh(ah(r))}const Bh=["relative cursor-pointer","text-sm focus:z-10 focus:ring-2 font-medium focus:outline-none whitespace-nowrap shadow-sm","inline-flex gap-2 items-center justify-center transition-colors focus:ring-offset-1","disabled:opacity-40 disabled:cursor-not-allowed disabled:text-nb-gray-300 ring-offset-neutral-950/50"],qh={default:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900","dark:focus:ring-zinc-800/50 dark:bg-nb-gray dark:text-gray-400 dark:border-gray-700/30 dark:hover:text-white dark:hover:bg-zinc-800/50"],primary:["dark:focus:ring-netbird-600/50 dark:ring-offset-neutral-950/50 enabled:dark:bg-netbird disabled:dark:bg-nb-gray-910 dark:text-gray-100 enabled:dark:hover:text-white enabled:dark:hover:bg-netbird-500/80","enabled:bg-netbird enabled:text-white enabled:focus:ring-netbird-400/50 enabled:hover:bg-netbird-500"],secondary:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900","dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20","dark:bg-nb-gray-920 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-nb-gray-910"],secondaryLighter:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900","dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20","dark:bg-nb-gray-900/70 dark:text-gray-400 dark:border-gray-700/70 dark:hover:text-white dark:hover:bg-nb-gray-800/60"],input:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900","dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20","dark:bg-nb-gray-900 dark:text-gray-400 dark:border-nb-gray-700 dark:hover:bg-nb-gray-900/80"],dropdown:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900","dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20","dark:bg-nb-gray-900/40 dark:text-gray-400 dark:border-nb-gray-900 dark:hover:bg-nb-gray-900/50"],dotted:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900 border-dashed","dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20","dark:bg-nb-gray-900/30 dark:text-gray-400 dark:border-gray-500/40 dark:hover:text-white dark:hover:bg-zinc-800/50"],tertiary:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900","dark:focus:ring-zinc-800/50 dark:bg-white dark:text-gray-800 dark:border-gray-700/40 dark:hover:bg-neutral-200 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300"],white:["focus:ring-white/50 bg-white text-gray-800 border-white outline-none hover:bg-neutral-200 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300","disabled:dark:bg-nb-gray-900 disabled:dark:text-nb-gray-300 disabled:dark:border-nb-gray-900"],outline:["bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900","dark:focus:ring-zinc-800/50 dark:bg-transparent dark:text-netbird dark:border-netbird dark:hover:bg-nb-gray-900/30"],"danger-outline":["enabled:dark:focus:ring-red-800/20 enabled:dark:focus:bg-red-950/40 enabled:hover:dark:bg-red-950/50 enabled:dark:hover:border-red-800/50 dark:bg-transparent dark:text-red-500"],"danger-text":["dark:bg-transparent dark:text-red-500 dark:hover:text-red-600 dark:border-transparent !px-0 !shadow-none !py-0 focus:ring-red-500/30 dark:ring-offset-neutral-950/50"],"default-outline":["dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20","dark:bg-transparent dark:text-nb-gray-400 dark:border-transparent dark:hover:text-white dark:hover:bg-nb-gray-900/30 dark:hover:border-nb-gray-800/50","data-[state=open]:dark:text-white data-[state=open]:dark:bg-nb-gray-900/30 data-[state=open]:dark:border-nb-gray-800/50"],danger:["dark:focus:ring-red-700/20 dark:focus:bg-red-700 hover:dark:bg-red-700 dark:hover:border-red-800/50 dark:bg-red-600 dark:text-red-100"]},Yh={xs:"text-xs py-2 px-4",xs2:"text-[0.78rem] py-2 px-4",sm:"text-sm py-2.5 px-4",md:"text-sm py-2.5 px-4",lg:"text-base py-2.5 px-4"},Gh={0:"border",1:"border border-transparent",2:"border border-t-0 border-b-0"},Ru=xt.forwardRef(({variant:r="default",rounded:v=!0,border:S=1,size:f="md",stopPropagation:_=!0,className:O,onClick:D,children:U,...N},p)=>E.jsx("button",{type:"button",...N,ref:p,className:wt(Bh,qh[r],Yh[f],Gh[S?1:0],v&&"rounded-md",O),onClick:R=>{_&&R.stopPropagation(),D?.(R)},children:U}));Ru.displayName="Button";const Xh={default:["bg-nb-gray-900 placeholder:text-neutral-400/70 border-nb-gray-700","ring-offset-neutral-950/50 focus-visible:ring-neutral-500/20"],darker:["bg-nb-gray-920 placeholder:text-neutral-400/70 border-nb-gray-800","ring-offset-neutral-950/50 focus-visible:ring-neutral-500/20"],error:["bg-nb-gray-900 placeholder:text-neutral-400/70 border-red-500 text-red-500","ring-offset-red-500/10 focus-visible:ring-red-500/10"]},Qh={default:"bg-nb-gray-900 border-nb-gray-700 text-nb-gray-300",error:"bg-nb-gray-900 border-red-500 text-nb-gray-300 text-red-500"},c0=xt.forwardRef(({className:r,type:v,customSuffix:S,customPrefix:f,icon:_,maxWidthClass:O="",error:D,variant:U="default",prefixClassName:N,showPasswordToggle:p=!1,...R},H)=>{const[L,ot]=xt.useState(!1),ct=v==="password",G=ct&&L?"text":v,V=(ct&&p?E.jsx("button",{type:"button",onClick:()=>ot(!L),className:"hover:text-white transition-all","aria-label":"Toggle password visibility",children:L?E.jsx(km,{size:18}):E.jsx(Wm,{size:18})}):null)||S,gt=D?"error":U;return E.jsxs(E.Fragment,{children:[E.jsxs("div",{className:wt("flex relative h-[42px]",O),children:[f&&E.jsx("div",{className:wt(Qh[D?"error":"default"],"flex h-[42px] w-auto rounded-l-md px-3 py-2 text-sm","border items-center whitespace-nowrap",R.disabled&&"opacity-40",N),children:f}),E.jsx("div",{className:wt("absolute left-0 top-0 h-full flex items-center text-xs text-nb-gray-300 pl-3 leading-[0]",R.disabled&&"opacity-40"),children:_}),E.jsx("input",{type:G,ref:H,...R,className:wt(Xh[gt],"flex h-[42px] w-full rounded-md px-3 py-2 text-sm","file:bg-transparent file:text-sm file:font-medium file:border-0","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2","disabled:cursor-not-allowed disabled:opacity-40","border",f&&"!border-l-0 !rounded-l-none",V&&"!pr-16",_&&"!pl-10",r)}),E.jsx("div",{className:wt("absolute right-0 top-0 h-full flex items-center text-xs text-nb-gray-300 pr-4 leading-[0] select-none",R.disabled&&"opacity-30"),children:V})]}),D&&E.jsx("p",{className:"text-xs text-red-500 mt-2",children:D})]})});c0.displayName="Input";const Zh=xt.forwardRef(function({value:v,onChange:S,length:f=6,disabled:_=!1,className:O,autoFocus:D=!1},U){const N=xt.useRef([]);xt.useImperativeHandle(U,()=>({focus:()=>{N.current[0]?.focus()}}));const p=v.split("").concat(new Array(f).fill("")).slice(0,f),R=Array.from({length:f},(G,Q)=>`pin-${Q}`),H=(G,Q)=>{if(!/^\d*$/.test(Q))return;const V=[...p];V[G]=Q.slice(-1);const gt=V.join("").replaceAll(/\s/g,"");S(gt),Q&&G{Q.key==="Backspace"&&!p[G]&&G>0&&N.current[G-1]?.focus(),Q.key==="ArrowLeft"&&G>0&&N.current[G-1]?.focus(),Q.key==="ArrowRight"&&G{G.preventDefault();const Q=G.clipboardData.getData("text").replaceAll(/\D/g,"").slice(0,f);S(Q);const V=Math.min(Q.length,f-1);N.current[V]?.focus()},ct=G=>{G.target.select()};return E.jsx("div",{className:wt("flex gap-2 w-full min-w-0",O),children:p.map((G,Q)=>E.jsx("input",{id:R[Q],ref:V=>{N.current[Q]=V},type:"text",inputMode:"numeric",maxLength:1,value:G,onChange:V=>H(Q,V.target.value),onKeyDown:V=>L(Q,V),onPaste:ot,onFocus:ct,disabled:_,autoFocus:D&&Q===0,className:wt("flex-1 min-w-0 h-[42px] text-center text-sm rounded-md","dark:bg-nb-gray-900 border dark:border-nb-gray-700","dark:placeholder:text-neutral-400/70","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2","ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20","disabled:cursor-not-allowed disabled:opacity-40")},R[Q]))})}),f0=xt.createContext({value:"",onChange:()=>{}}),r0=()=>xt.useContext(f0);function $e({value:r,defaultValue:v,onChange:S,children:f}){const[_,O]=xt.useState(v??""),D=r??_,U=xt.useCallback(p=>{r===void 0&&O(p),S?.(p)},[r,S]),N=xt.useMemo(()=>({value:D,onChange:U}),[D,U]);return E.jsx(f0.Provider,{value:N,children:E.jsx("div",{children:typeof f=="function"?f({value:D,onChange:U}):f})})}function wh({children:r,className:v}){return E.jsx("div",{role:"tablist",className:wt("bg-nb-gray-930/70 p-1.5 flex justify-center gap-1 border-nb-gray-900",v),children:r})}function Lh({children:r,value:v,disabled:S=!1,className:f,selected:_,onClick:O}){const D=r0(),U=_??v===D.value;let N="";U?N="bg-nb-gray-900 text-white":S||(N="text-nb-gray-400 hover:bg-nb-gray-900/50");const p=()=>{D.onChange(v),O?.()};return E.jsx("button",{role:"tab",type:"button",disabled:S,"aria-selected":U,onClick:p,className:wt("px-4 py-2 text-sm rounded-md w-full transition-all cursor-pointer",S&&"opacity-30 cursor-not-allowed",N,f),children:E.jsx("div",{className:"flex items-center w-full justify-center gap-2",children:r})})}function Vh({children:r,value:v,className:S,visible:f}){const _=r0();return f??v===_.value?E.jsx("div",{role:"tabpanel",className:wt("bg-nb-gray-930/70 px-4 pt-4 pb-5 rounded-b-md border border-t-0 border-nb-gray-900",S),children:r}):null}$e.List=wh;$e.Trigger=Lh;$e.Content=Vh;const Kh="/__netbird__/assets/netbird-full.svg",Jh="data:image/svg+xml,%3csvg%20width='31'%20height='23'%20viewBox='0%200%2031%2023'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M21.4631%200.523438C17.8173%200.857913%2016.0028%202.95675%2015.3171%204.01871L4.66406%2022.4734H17.5163L30.1929%200.523438H21.4631Z'%20fill='%23F68330'/%3e%3cpath%20d='M17.5265%2022.4737L0%203.88525C0%203.88525%2019.8177%20-1.44128%2021.7493%2015.1738L17.5265%2022.4737Z'%20fill='%23F68330'/%3e%3cpath%20d='M14.9236%204.70563L9.54688%2014.0208L17.5158%2022.4747L21.7385%2015.158C21.0696%209.44682%2018.2851%206.32784%2014.9236%204.69727'%20fill='%23F05252'/%3e%3c/svg%3e",ti={small:{desktop:14,mobile:20},default:{desktop:22,mobile:30},large:{desktop:24,mobile:40}},kh=({size:r="default",mobile:v=!0})=>E.jsxs(E.Fragment,{children:[E.jsx("img",{src:Kh,height:ti[r].desktop,style:{height:ti[r].desktop},alt:"NetBird Logo",className:wt(v&&"hidden md:block","group-hover:opacity-80 transition-all")}),v&&E.jsx("img",{src:Jh,width:ti[r].mobile,style:{width:ti[r].mobile},alt:"NetBird Logo",className:wt(v&&"md:hidden ml-4")})]});function Uf(){return E.jsxs("a",{href:"https://netbird.io?utm_source=netbird-proxy&utm_medium=web&utm_campaign=powered_by",target:"_blank",rel:"noopener noreferrer",className:"flex items-center justify-center mt-8 gap-2 group cursor-pointer",children:[E.jsx("span",{className:"text-sm text-nb-gray-400 font-light text-center group-hover:opacity-80 transition-all",children:"Powered by"}),E.jsx(kh,{size:"small",mobile:!1})]})}const Wh=({className:r})=>E.jsx("div",{className:wt("h-full w-full absolute left-0 top-0 rounded-md overflow-hidden z-0 pointer-events-none",r),children:E.jsx("div",{className:"bg-linear-to-b from-nb-gray-900/10 via-transparent to-transparent w-full h-full rounded-md"})}),Fd=({children:r,className:v})=>E.jsxs("div",{className:wt("px-6 sm:px-10 py-10 pt-8","bg-nb-gray-940 border border-nb-gray-910 rounded-lg relative",v),children:[E.jsx(Wh,{}),r]});function Cf({children:r,className:v}){return E.jsx("h1",{className:wt("text-xl! text-center z-10 relative",v),children:r})}function jf({children:r,className:v}){return E.jsx("div",{className:wt("text-sm text-nb-gray-300 font-light mt-2 block text-center z-10 relative",v),children:r})}const $h=()=>E.jsxs("div",{className:"flex items-center justify-center relative my-4",children:[E.jsx("span",{className:"bg-nb-gray-940 relative z-10 px-4 text-xs text-nb-gray-400 font-medium",children:"OR"}),E.jsx("span",{className:"h-px bg-nb-gray-900 w-full absolute z-0"})]}),Fh=({error:r})=>E.jsx("div",{className:"text-red-400 bg-red-800/20 border border-red-800/50 rounded-lg px-4 py-3 whitespace-break-spaces text-sm",children:r});function Id({className:r,htmlFor:v,...S}){return E.jsx("label",{htmlFor:v,className:wt("text-sm font-medium tracking-wider leading-none","peer-disabled:cursor-not-allowed peer-disabled:opacity-70","mb-2.5 inline-block text-nb-gray-200","flex items-center gap-2 select-none",r),...S})}const _f=t0(),It=_f.methods&&Object.keys(_f.methods).length>0?_f.methods:{password:"password",pin:"pin",oidc:"/auth/oidc"};function Ih(){xt.useEffect(()=>{document.title="Authentication Required - NetBird Service"},[]);const[r,v]=xt.useState(null),[S,f]=xt.useState(null),[_,O]=xt.useState(""),[D,U]=xt.useState(""),N=xt.useRef(null),p=xt.useRef(null),[R,H]=xt.useState(It.password?"password":"pin"),L=(nt,Ot)=>{v(Ot),f(null),nt==="password"?(U(""),setTimeout(()=>N.current?.focus(),200)):(O(""),setTimeout(()=>p.current?.focus(),200))},ot=(nt,Ot)=>{v(null),f(nt);const J=new FormData;nt==="password"?J.append(It.password,Ot):J.append(It.pin,Ot),fetch(globalThis.location.href,{method:"POST",body:J,redirect:"manual"}).then(Nt=>{if(Nt.type==="opaqueredirect"||Nt.status===0)f("redirect"),globalThis.location.reload();else if(Nt.status===429){const Xt=Number(Nt.headers.get("Retry-After")),pl=Number.isFinite(Xt)&&Xt>0?` Try again in ${Math.ceil(Xt)} seconds.`:" Please try again later.";L(nt,`Too many authentication attempts.${pl}`)}else L(nt,"Authentication failed. Please try again.")}).catch(()=>{L(nt,"An error occurred. Please try again.")})},ct=nt=>{O(nt),nt.length===6&&ot("pin",nt)},G=_.length===6,Q=D.length>0,V=S!==null||R==="password"&&!Q||R==="pin"&&!G,gt=It.password||It.pin,zt=It.password&&It.pin,_t=R==="password"?"Sign in":"Submit";return S==="redirect"?E.jsxs("main",{className:"mt-20",children:[E.jsxs(Fd,{className:"max-w-105 mx-auto",children:[E.jsx(Cf,{children:"Authenticated"}),E.jsx(jf,{children:"Loading service..."}),E.jsx("div",{className:"flex justify-center mt-7",children:E.jsx(kd,{className:"animate-spin",size:24})})]}),E.jsx(Uf,{})]}):E.jsxs("main",{className:"mt-20",children:[E.jsxs(Fd,{className:"max-w-105 mx-auto",children:[E.jsx(Cf,{children:"Authentication Required"}),E.jsx(jf,{children:"The service you are trying to access is protected. Please authenticate to continue."}),E.jsxs("div",{className:"flex flex-col gap-4 mt-7 z-10 relative",children:[r&&E.jsx(Fh,{error:r}),It.oidc&&E.jsxs(Ru,{variant:"primary",className:"w-full",onClick:()=>{globalThis.location.href=It.oidc},children:[E.jsx(Im,{size:16}),"Sign in with SSO"]}),It.oidc&>&&E.jsx($h,{}),gt&&E.jsxs("form",{onSubmit:nt=>{nt.preventDefault(),ot(R,R==="password"?D:_)},children:[zt&&E.jsx($e,{value:R,onChange:nt=>{H(nt),setTimeout(()=>{nt==="password"?N.current?.focus():p.current?.focus()},0)},children:E.jsxs($e.List,{className:"rounded-lg border mb-4",children:[E.jsxs($e.Trigger,{value:"password",children:[E.jsx(Fm,{size:14}),"Password"]}),E.jsxs($e.Trigger,{value:"pin",children:[E.jsx(Km,{size:14}),"PIN"]})]})}),E.jsxs("div",{className:"mb-4",children:[It.password&&(R==="password"||!It.pin)&&E.jsxs(E.Fragment,{children:[!zt&&E.jsx(Id,{htmlFor:"password",children:"Password"}),E.jsx(c0,{ref:N,type:"password",id:"password",placeholder:"Enter password",disabled:S!==null,showPasswordToggle:!0,autoFocus:!0,value:D,onChange:nt=>U(nt.target.value)})]}),It.pin&&(R==="pin"||!It.password)&&E.jsxs(E.Fragment,{children:[!zt&&E.jsx(Id,{htmlFor:"pin-0",children:"Enter PIN Code"}),E.jsx(Zh,{ref:p,value:_,onChange:ct,disabled:S!==null,autoFocus:!It.password})]})]}),E.jsx(Ru,{type:"submit",disabled:V,variant:"secondary",className:"w-full",children:S===null?_t:E.jsxs(E.Fragment,{children:[E.jsx(kd,{className:"animate-spin",size:16}),"Verifying..."]})})]})]})]}),E.jsx(Uf,{})]})}function Ph({success:r=!0}){return r?E.jsx("div",{className:"flex-1 flex items-center justify-center h-12 w-full px-5",children:E.jsx("div",{className:"w-full border-t-2 border-dashed border-green-500"})}):E.jsxs("div",{className:"flex-1 flex items-center justify-center h-12 min-w-10 px-5 relative",children:[E.jsx("div",{className:"w-full border-t-2 border-dashed border-nb-gray-900"}),E.jsx("div",{className:"absolute inset-0 flex items-center justify-center",children:E.jsx("div",{className:"w-8 h-8 rounded-full flex items-center justify-center",children:E.jsx(eh,{size:18,className:"text-netbird"})})})]})}function Of({icon:r,label:v,detail:S,success:f=!0,line:_=!0}){return E.jsxs(E.Fragment,{children:[_&&E.jsx(Ph,{success:f}),E.jsxs("div",{className:"flex flex-col items-center gap-2",children:[E.jsx("div",{className:"w-14 h-14 rounded-md flex items-center justify-center from-nb-gray-940 to-nb-gray-930/70 bg-gradient-to-br border border-nb-gray-910",children:E.jsx(r,{size:20,className:"text-nb-gray-200"})}),E.jsx("span",{className:"text-sm text-nb-gray-200 font-normal mt-1",children:v}),E.jsx("span",{className:`text-xs font-medium uppercase ${f?"text-green-500":"text-netbird"}`,children:f?"Connected":"Unreachable"}),S&&E.jsx("span",{className:"text-xs text-nb-gray-400 truncate text-center",children:S})]})]})}function tg({code:r,title:v,message:S,proxy:f=!0,destination:_=!0,requestId:O,simple:D=!1,retryUrl:U}){xt.useEffect(()=>{document.title=`${v} - NetBird Service`},[v]);const[N]=xt.useState(()=>new Date().toISOString());return E.jsxs("main",{className:"flex flex-col items-center mt-24 px-4 max-w-3xl mx-auto",children:[E.jsxs("div",{className:"text-sm text-netbird font-normal font-mono mb-3 z-10 relative",children:["Error ",r]}),E.jsx(Cf,{className:"text-3xl!",children:v}),E.jsx(jf,{className:"mt-2 mb-8 max-w-md",children:S}),!D&&E.jsxs("div",{className:"hidden sm:flex items-start justify-center w-full mt-6 mb-16 z-10 relative",children:[E.jsx(Of,{icon:th,label:"You",line:!1}),E.jsx(Of,{icon:lh,label:"Proxy",success:f}),E.jsx(Of,{icon:$m,label:"Destination",success:_})]}),E.jsxs("div",{className:"flex gap-3 justify-center items-center mb-6 z-10 relative",children:[E.jsxs(Ru,{variant:"primary",onClick:()=>{U?globalThis.location.href=U:globalThis.location.reload()},children:[E.jsx(Pm,{size:16}),"Refresh Page"]}),E.jsxs(Ru,{variant:"secondary",onClick:()=>globalThis.open("https://docs.netbird.io","_blank","noopener,noreferrer"),children:[E.jsx(Jm,{size:16}),"Documentation"]})]}),E.jsxs("div",{className:"text-center text-xs text-nb-gray-300 uppercase z-10 relative font-mono flex flex-col sm:flex-row gap-2 sm:gap-10 mt-4 mb-3",children:[E.jsxs("div",{children:[E.jsx("span",{className:"text-nb-gray-400",children:"REQUEST-ID:"})," ",O]}),E.jsxs("div",{children:[E.jsx("span",{className:"text-nb-gray-400",children:"TIMESTAMP:"})," ",N]})]}),E.jsx(Uf,{})]})}const Nf=t0();Zm.createRoot(document.getElementById("root")).render(E.jsx(xt.StrictMode,{children:Nf.page==="error"&&Nf.error?E.jsx(tg,{...Nf.error}):E.jsx(Ih,{})})); diff --git a/proxy/web/src/App.tsx b/proxy/web/src/App.tsx index ab453aa3e..3f09aacfb 100644 --- a/proxy/web/src/App.tsx +++ b/proxy/web/src/App.tsx @@ -68,6 +68,12 @@ function App() { if (res.type === "opaqueredirect" || res.status === 0) { setSubmitting("redirect"); globalThis.location.reload(); + } else if (res.status === 429) { + const seconds = Number(res.headers.get("Retry-After")); + const wait = Number.isFinite(seconds) && seconds > 0 + ? ` Try again in ${Math.ceil(seconds)} seconds.` + : " Please try again later."; + handleAuthError(method, `Too many authentication attempts.${wait}`); } else { handleAuthError(method, "Authentication failed. Please try again."); } From 771d81b72ab8be9da5857d0e2e1f734dc4a70b11 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:55:56 +0200 Subject: [PATCH 4/9] [management] Add proxy credentials limiter on management (#7569) --- management/internals/shared/grpc/proxy.go | 15 +- .../shared/grpc/proxy_credential_limiter.go | 101 ++++++++++++++ .../grpc/proxy_credential_limiter_test.go | 79 +++++++++++ .../shared/grpc/proxy_credentials.md | 18 +++ .../shared/grpc/proxy_credentials_test.go | 131 ++++++++++++++++++ 5 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 management/internals/shared/grpc/proxy_credential_limiter.go create mode 100644 management/internals/shared/grpc/proxy_credential_limiter_test.go create mode 100644 management/internals/shared/grpc/proxy_credentials.md create mode 100644 management/internals/shared/grpc/proxy_credentials_test.go diff --git a/management/internals/shared/grpc/proxy.go b/management/internals/shared/grpc/proxy.go index 2fc969ad0..b1527f0ab 100644 --- a/management/internals/shared/grpc/proxy.go +++ b/management/internals/shared/grpc/proxy.go @@ -102,7 +102,8 @@ type ProxyServiceServer struct { mu sync.RWMutex // Manager for reverse proxy operations - serviceManager rpservice.Manager + serviceManager rpservice.Manager + credentialLimits credentialVerificationLimiter // agentNetworkSynth produces synthesised reverse-proxy services from // Agent Network state. Optional — when nil the snapshot path only ships // persisted services. @@ -242,9 +243,10 @@ func (s *ProxyServiceServer) cleanupStaleProxies(ctx context.Context) { } } -// Close stops background goroutines. +// Close stops background goroutines and releases credential verification state. func (s *ProxyServiceServer) Close() { s.cancel() + s.credentialLimits.close() } // SetServiceManager sets the service manager. Must be called before serving. @@ -1223,6 +1225,7 @@ func shallowCloneMapping(m *proto.ProxyMapping) *proto.ProxyMapping { } } +// Authenticate verifies service credentials and issues a session token. func (s *ProxyServiceServer) Authenticate(ctx context.Context, req *proto.AuthenticateRequest) (*proto.AuthenticateResponse, error) { if err := enforceAccountScope(ctx, req.GetAccountId()); err != nil { return nil, err @@ -1234,6 +1237,14 @@ func (s *ProxyServiceServer) Authenticate(ctx context.Context, req *proto.Authen return nil, status.Errorf(codes.FailedPrecondition, "get service from store: %v", err) } + switch req.GetRequest().(type) { + case *proto.AuthenticateRequest_Pin, *proto.AuthenticateRequest_Password: + key := credentialVerificationKey{accountID: credentialAccountID(service.AccountID), serviceID: credentialServiceID(service.ID)} + if err := s.credentialLimits.allow(key); err != nil { + return nil, err + } + } + authenticated, userId, method := s.authenticateRequest(ctx, req, service) // Non-OIDC schemes (PIN/Password/Header) authenticate against per-service diff --git a/management/internals/shared/grpc/proxy_credential_limiter.go b/management/internals/shared/grpc/proxy_credential_limiter.go new file mode 100644 index 000000000..2a3a02347 --- /dev/null +++ b/management/internals/shared/grpc/proxy_credential_limiter.go @@ -0,0 +1,101 @@ +package grpc + +import ( + "sync" + "time" + + "golang.org/x/time/rate" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" +) + +const ( + credentialVerificationInterval = 6 * time.Second + credentialVerificationBurst = 5 + credentialVerificationMaxServices = 4096 + credentialVerificationIdleTimeout = 15 * time.Minute + credentialVerificationCleanupInterval = time.Minute +) + +type credentialAccountID string +type credentialServiceID string + +type credentialVerificationKey struct { + accountID credentialAccountID + serviceID credentialServiceID +} + +type credentialVerificationBudget struct { + limiter *rate.Limiter + lastUsed time.Time +} + +// The zero value is ready to use. Budgets are local to this Management process; +// proxy replicas reaching this process share a service's verification budget. +type credentialVerificationLimiter struct { + mu sync.Mutex + now func() time.Time + services map[credentialVerificationKey]*credentialVerificationBudget + nextCleanup time.Time + closed bool +} + +func (l *credentialVerificationLimiter) allow(key credentialVerificationKey) error { + l.mu.Lock() + defer l.mu.Unlock() + if l.closed { + return status.Error(codes.Unavailable, "credential verification is closed") + } + now := time.Now() + if l.now != nil { + now = l.now() + } + l.cleanup(now) + budget := l.services[key] + if budget == nil { + if len(l.services) >= credentialVerificationMaxServices { + return credentialVerificationThrottled(credentialVerificationCleanupInterval) + } + if l.services == nil { + l.services = make(map[credentialVerificationKey]*credentialVerificationBudget) + } + budget = &credentialVerificationBudget{limiter: rate.NewLimiter(rate.Every(credentialVerificationInterval), credentialVerificationBurst)} + l.services[key] = budget + } + budget.lastUsed = now + if budget.limiter.AllowN(now, 1) { + return nil + } + delay := max(time.Nanosecond, time.Duration((1-budget.limiter.TokensAt(now))*float64(credentialVerificationInterval))) + return credentialVerificationThrottled(delay) +} + +func (l *credentialVerificationLimiter) cleanup(now time.Time) { + if now.Before(l.nextCleanup) { + return + } + l.nextCleanup = now.Add(credentialVerificationCleanupInterval) + for key, budget := range l.services { + if now.Sub(budget.lastUsed) >= credentialVerificationIdleTimeout { + delete(l.services, key) + } + } +} + +func (l *credentialVerificationLimiter) close() { + l.mu.Lock() + defer l.mu.Unlock() + l.closed = true + l.services = nil +} + +func credentialVerificationThrottled(delay time.Duration) error { + s := status.New(codes.ResourceExhausted, "too many credential verification attempts") + withRetry, err := s.WithDetails(&errdetails.RetryInfo{RetryDelay: durationpb.New(delay)}) + if err != nil { + return s.Err() + } + return withRetry.Err() +} diff --git a/management/internals/shared/grpc/proxy_credential_limiter_test.go b/management/internals/shared/grpc/proxy_credential_limiter_test.go new file mode 100644 index 000000000..565b14889 --- /dev/null +++ b/management/internals/shared/grpc/proxy_credential_limiter_test.go @@ -0,0 +1,79 @@ +package grpc + +import ( + "strconv" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestCredentialVerificationRefillAndIsolation(t *testing.T) { + now := time.Now() + l := credentialVerificationLimiter{now: func() time.Time { return now }} + key := credentialVerificationKey{accountID: "account", serviceID: "service"} + for range credentialVerificationBurst { + require.NoError(t, l.allow(key)) + } + err := l.allow(key) + require.Equal(t, codes.ResourceExhausted, status.Code(err), "the burst must be bounded") + now = now.Add(3 * time.Second) + err = l.allow(key) + require.Equal(t, codes.ResourceExhausted, status.Code(err), "a partially refilled token must not permit a check") + details := status.Convert(err).Details() + require.Len(t, details, 1, "throttling must provide RetryInfo") + retry, ok := details[0].(*errdetails.RetryInfo) + require.True(t, ok, "retry details must use the standard message") + assert.Equal(t, 3*time.Second, retry.RetryDelay.AsDuration(), "retry hint must reflect time until the next check") + now = now.Add(3 * time.Second) + require.NoError(t, l.allow(key)) + assert.Equal(t, codes.ResourceExhausted, status.Code(l.allow(key)), "only one check must refill every six seconds") + require.NoError(t, l.allow(credentialVerificationKey{accountID: "other-account", serviceID: key.serviceID})) + require.NoError(t, l.allow(credentialVerificationKey{accountID: key.accountID, serviceID: "other-service"})) +} + +func TestCredentialVerificationCapacityAndExpiry(t *testing.T) { + now := time.Now() + l := credentialVerificationLimiter{now: func() time.Time { return now }} + for i := range credentialVerificationMaxServices { + require.NoError(t, l.allow(credentialVerificationKey{accountID: "account", serviceID: credentialServiceID(strconv.Itoa(i))})) + } + key := credentialVerificationKey{accountID: "account", serviceID: "new-service"} + assert.Equal(t, codes.ResourceExhausted, status.Code(l.allow(key)), "capacity exhaustion must deny new checks") + now = now.Add(credentialVerificationIdleTimeout) + for range credentialVerificationBurst { + require.NoError(t, l.allow(key)) + } + assert.Equal(t, codes.ResourceExhausted, status.Code(l.allow(key)), "expiry must retain the normal burst bound") +} + +func TestCredentialVerificationConcurrentChecksAndClose(t *testing.T) { + var l credentialVerificationLimiter + key := credentialVerificationKey{accountID: "account", serviceID: "service"} + var admitted atomic.Int32 + var wg sync.WaitGroup + for range 100 { + wg.Go(func() { + if err := l.allow(key); err == nil { + admitted.Add(1) + } else { + assert.Equal(t, codes.ResourceExhausted, status.Code(err), "excess checks must be throttled") + } + }) + } + wg.Wait() + assert.EqualValues(t, credentialVerificationBurst, admitted.Load(), "concurrent checks must share the burst") + for range 10 { + wg.Go(l.close) + wg.Go(func() { assert.Error(t, l.allow(key)) }) + } + wg.Wait() + assert.Empty(t, l.services, "closing must release retained budgets") + assert.Equal(t, codes.Unavailable, status.Code(l.allow(key)), "checks after close must fail closed") +} diff --git a/management/internals/shared/grpc/proxy_credentials.md b/management/internals/shared/grpc/proxy_credentials.md new file mode 100644 index 000000000..1caf432e5 --- /dev/null +++ b/management/internals/shared/grpc/proxy_credentials.md @@ -0,0 +1,18 @@ +# Reverse proxy credential verification + +The `ProxyService.Authenticate` RPC limits PIN and password checks before +verifying their Argon2 hashes. Both methods share one budget per account and +service: a burst of five checks, replenishing one check every six seconds +(ten per minute). Successful and failed checks consume the budget. Account +scope and service lookup run before the limiter. + +Excess checks receive gRPC `ResourceExhausted` with a standard `RetryInfo` delay. +Updated proxies translate it to HTTP 429 and `Retry-After`. Older proxies show +an authentication-service error but cannot bypass the Management limit. + +Budgets are held in memory per Management process and reset on restart. Proxy +replicas reaching the same Management process share its budgets. Multiple +Management processes have independent budgets; this is not a cluster-wide +limit. At most 4,096 service budgets are retained, with idle entries expiring +after fifteen minutes. Capacity exhaustion denies new checks until entries +expire. Closing the server releases the retained state. diff --git a/management/internals/shared/grpc/proxy_credentials_test.go b/management/internals/shared/grpc/proxy_credentials_test.go new file mode 100644 index 000000000..2e144bd69 --- /dev/null +++ b/management/internals/shared/grpc/proxy_credentials_test.go @@ -0,0 +1,131 @@ +package grpc_test + +import ( + "context" + "net" + "net/netip" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" + servicemanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service/manager" + "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/sessionkey" + nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc" + "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/proto" +) + +func credentialServer(t *testing.T) (*nbgrpc.ProxyServiceServer, context.Context, grpc.UnaryServerInterceptor) { + t.Helper() + ctx := context.Background() + s, err := store.NewStore(ctx, types.SqliteStoreEngine, t.TempDir(), nil, false) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, s.Close(ctx)) }) + require.NoError(t, s.SaveAccount(ctx, &types.Account{Id: "account"})) + keys, err := sessionkey.GenerateKeyPair() + require.NoError(t, err) + for _, id := range []string{"service", "other-service"} { + svc := &service.Service{ + ID: id, AccountID: "account", Name: id, Domain: id + ".example.com", + Enabled: true, SessionPrivateKey: keys.PrivateKey, SessionPublicKey: keys.PublicKey, + Auth: service.AuthConfig{ + PinAuth: &service.PINAuthConfig{Enabled: true, Pin: "842716"}, + PasswordAuth: &service.PasswordAuthConfig{Enabled: true, Password: "test-password"}, + }, + } + require.NoError(t, svc.Auth.HashSecrets()) + require.NoError(t, s.CreateService(ctx, svc)) + } + account := "account" + token, err := types.CreateNewProxyAccessToken("test proxy", time.Hour, &account, "admin") + require.NoError(t, err) + require.NoError(t, s.SaveProxyAccessToken(ctx, &token.ProxyAccessToken)) + ctx = metadata.NewIncomingContext(ctx, metadata.Pairs("authorization", "Bearer "+string(token.PlainToken))) + ctx = peer.NewContext(ctx, &peer.Peer{Addr: net.TCPAddrFromAddrPort(netip.MustParseAddrPort("192.0.2.1:443"))}) + server := nbgrpc.NewProxyServiceServer(nil, nil, nil, nbgrpc.ProxyOIDCConfig{}, nil, nil, nil, nil, nil) + t.Cleanup(server.Close) + server.SetServiceManager(servicemanager.NewManager(s, nil, nil, nil, nil, nil)) + interceptor, _, closeInterceptor := nbgrpc.NewProxyAuthInterceptors(s) + t.Cleanup(closeInterceptor) + return server, ctx, interceptor +} + +func TestAuthenticateCredentialRateLimit(t *testing.T) { + server, ctx, interceptor := credentialServer(t) + authenticate := func(req *proto.AuthenticateRequest) (*proto.AuthenticateResponse, error) { + response, err := interceptor(ctx, req, &grpc.UnaryServerInfo{FullMethod: "/management.ProxyService/Authenticate"}, func(ctx context.Context, req any) (any, error) { + return server.Authenticate(ctx, req.(*proto.AuthenticateRequest)) + }) + if err != nil { + return nil, err + } + return response.(*proto.AuthenticateResponse), nil + } + for i := range 5 { + req := &proto.AuthenticateRequest{AccountId: "account", Id: "service"} + if i%2 == 0 { + req.Request = &proto.AuthenticateRequest_Pin{Pin: &proto.PinRequest{Pin: "000000"}} + } else { + req.Request = &proto.AuthenticateRequest_Password{Password: &proto.PasswordRequest{Password: "wrong-password"}} + } + resp, err := authenticate(req) + require.NoError(t, err) + assert.False(t, resp.GetSuccess(), "incorrect PINs and passwords must be denied") + assert.Empty(t, resp.GetSessionToken(), "incorrect credentials must not issue a token") + } + req := &proto.AuthenticateRequest{AccountId: "account", Id: "service", Request: &proto.AuthenticateRequest_Pin{Pin: &proto.PinRequest{Pin: "842716"}}} + resp, err := authenticate(req) + assert.Nil(t, resp, "a throttled verification must not return a session") + require.Equal(t, codes.ResourceExhausted, status.Code(err), "PIN and password checks must share a service budget even with a valid proxy token") + details := status.Convert(err).Details() + require.Len(t, details, 1, "throttled responses must include a retry hint") + retry, ok := details[0].(*errdetails.RetryInfo) + require.True(t, ok, "the hint must use the standard RetryInfo message") + assert.Positive(t, retry.RetryDelay.AsDuration(), "the retry delay must be positive") + assert.LessOrEqual(t, retry.RetryDelay.AsDuration(), 6*time.Second, "the service must replenish one verification every six seconds") + req.AccountId = "another-account" + _, err = authenticate(req) + assert.Equal(t, codes.PermissionDenied, status.Code(err), "account scope must still be enforced before throttling") + req.AccountId = "account" + req.Id = "other-service" + resp, err = authenticate(req) + require.NoError(t, err) + assert.True(t, resp.GetSuccess(), "one service's throttle must not block another service") + assert.NotEmpty(t, resp.GetSessionToken(), "valid credentials on another service must issue a session") +} + +func TestAuthenticateCredentialConcurrentLimit(t *testing.T) { + server, _, _ := credentialServer(t) + req := &proto.AuthenticateRequest{AccountId: "account", Id: "service", Request: &proto.AuthenticateRequest_Pin{Pin: &proto.PinRequest{Pin: "000000"}}} + var checked, throttled atomic.Int32 + var wg sync.WaitGroup + for range 20 { + wg.Go(func() { + resp, err := server.Authenticate(context.Background(), req) + switch status.Code(err) { + case codes.OK: + checked.Add(1) + assert.False(t, resp.GetSuccess(), "incorrect credentials must be denied") + case codes.ResourceExhausted: + throttled.Add(1) + default: + assert.NoError(t, err) + } + }) + } + wg.Wait() + assert.EqualValues(t, 5, checked.Load(), "only the burst budget may reach concurrent credential verification") + assert.EqualValues(t, 15, throttled.Load(), "excess concurrent checks must be throttled") +} From bc0671fd213459b2f9c1a102b8005af750c0ea0b Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Mon, 21 Sep 2026 17:00:37 +0200 Subject: [PATCH 5/9] [client] Fix peers not being notified when the relay connection drops (#7490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [relay] Signal relay disconnects through the conn context AddCloseListener deduplicated listeners by comparing reflect.ValueOf(callback).Pointer(). For a method value that pointer is the address of the compiler-generated wrapper, not an identity bound to the receiver, so every peer's w.onRelayClientDisconnected compared equal. All peers on the home relay register under the same connectionURL key, so only the first registration survived and the rest were silently dropped. On a relay disconnect those peers were never notified: statusRelay stayed connected and the reconnect guard never fired. The relayed net.Conn itself was closed by closeAllConns, so nothing leaked, but the peer state machine did not learn about it. Foreign relays had the same defect scoped to the peers sharing that server. Rather than fixing the deduplication, drop the peer-level listener registry entirely. A relayed Conn now exposes Context(), cancelled when the connection is torn down, with a cancellation cause naming the reason. This is the same shape quic-go uses for its Conn and Stream types, and it removes the whole class of problems around listener identity, lifetime and deregistration: the signal belongs to the resource instead of a side table. WorkerRelay watches that context in a goroutine whose lifetime matches the connection. A watcher that wakes up for a superseded connection compares the conn pointer against the current one and returns without touching the state machine, so a fast relay reconnect cannot have a stale watcher tear down the connection that replaced it. Client.SetOnDisconnectListener stays: it is server-level and drives the reconnect guard and foreign relay eviction, unrelated to peers. handleRelayReady also checks the conn context, closing the race where the relay dies between OpenConn and the readiness handoff and the peer would otherwise build a WireGuard endpoint over a dead connection. TestNotifierDoubleAdd covered the removed mechanism and is gone. TestForeignAutoClose asserted nothing (both branches logged); it now waits for the relay to leave the client map and fails if it does not. * [relay] Fix build: return the concrete conn from Client.OpenConn OpenConn now returns *Conn, but it still went through connContainer.netConn(), which widens to net.Conn. The helper had one caller and only existed to produce the interface value the signature no longer wants, so return container.conn directly and drop it. * [relay] Assert the local-close cancellation cause explicitly The local-close test only rejected ErrServerDisconnected, so it would also have passed for ErrPeerDisconnected or a bare context.Canceled. closeConn cancels with net.ErrClosed, so assert that. * [client] Ignore relay disconnects from superseded connections The relayed conn watcher compared the conn pointer under relayLock, released it, and only then tore the connection down. A new offer could install its replacement in that window, so a watcher that validated the old pointer went on to close the proxy of the connection that had already replaced it and report the peer as disconnected while it was up. Move the decision to where the teardown happens. Conn records which relayed connection the current proxy was built from, and onRelayDisconnected takes the connection the signal belongs to and drops it under conn.mu when it is no longer the current one. Check and effect are now in the same critical section, so the verdict cannot go stale before it is acted on. This also covers the proxy read loops, whose disconnect listener took no argument and had the same defect: it now names the connection it belongs to. The WG timeout path keeps passing nil, since it deliberately tears down whatever is current. * [client] Bind the relayed conn reference to the proxy swap relayedConnRef was set at the top of the readiness path, but wgProxyRelay only changes at the end, in setRelayedProxy. The two failure returns in between — newProxy and ConfigureWGEndpoint — left the reference pointing at a connection that never became active while the old proxy was still installed. A disconnect of that old, live relay would then be dismissed as belonging to a superseded connection and never cleaned up. Set the reference in setRelayedProxy, next to the proxy it belongs to. Both success paths go through it and neither failure path does, so no failure branch has to remember to roll anything back. --- client/internal/peer/conn.go | 34 ++++-- client/internal/peer/worker_relay.go | 27 +++-- shared/relay/client/client.go | 35 +++--- shared/relay/client/conn.go | 10 ++ shared/relay/client/manager.go | 82 ++------------ shared/relay/client/manager_test.go | 157 ++++++++++++++++++--------- 6 files changed, 182 insertions(+), 163 deletions(-) diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 83089606f..d73144773 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -135,9 +135,10 @@ type Conn struct { // used to store the remote Rosenpass key for Relayed connection in case of connection update from ice rosenpassRemoteKey []byte - wgProxyICE wgproxy.Proxy - wgProxyRelay wgproxy.Proxy - handshaker *Handshaker + wgProxyICE wgproxy.Proxy + wgProxyRelay wgproxy.Proxy + relayedConnRef *relayClient.Conn + handshaker *Handshaker guard *guard.Guard wg sync.WaitGroup @@ -560,7 +561,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { conn.mu.Lock() defer conn.mu.Unlock() - if conn.ctx.Err() != nil { + if conn.ctx.Err() != nil || rci.relayedConn.Context().Err() != nil { if err := rci.relayedConn.Close(); err != nil { conn.Log.Warnf("failed to close unnecessary relayed connection: %v", err) } @@ -575,7 +576,9 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { conn.Log.Errorf("failed to add relayed net.Conn to local proxy: %v", err) return } - wgProxy.SetDisconnectListener(conn.onRelayDisconnected) + wgProxy.SetDisconnectListener(func() { + conn.onRelayDisconnected(rci.relayedConn) + }) conn.dumpState.NewLocalProxy() @@ -583,7 +586,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { if conn.isICEActive() { conn.Log.Debugf("do not switch to relay because current priority is: %s", conn.currentConnPriority.String()) - conn.setRelayedProxy(wgProxy) + conn.setRelayedProxy(wgProxy, rci.relayedConn) conn.statusRelay.SetConnected() conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey, time.Now()) return @@ -614,15 +617,26 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { conn.rosenpassRemoteKey = rci.rosenpassPubKey conn.currentConnPriority = conntype.Relay conn.statusRelay.SetConnected() - conn.setRelayedProxy(wgProxy) + conn.setRelayedProxy(wgProxy, rci.relayedConn) conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey, updateTime) conn.Log.Infof("start to communicate with peer via relay") conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr, updateTime) } -func (conn *Conn) onRelayDisconnected() { +// onRelayDisconnected reports the teardown of a relayed connection. relayedConn +// names the connection the signal belongs to, so a signal that arrives after +// its connection was replaced is ignored instead of tearing down its successor. +// A nil relayedConn means the caller does not track generations and the current +// connection is always torn down. +func (conn *Conn) onRelayDisconnected(relayedConn *relayClient.Conn) { conn.mu.Lock() defer conn.mu.Unlock() + + if relayedConn != nil && conn.relayedConnRef != relayedConn { + conn.Log.Debugf("ignoring relay disconnect of a superseded connection") + return + } + conn.handleRelayDisconnectedLocked() } @@ -646,6 +660,7 @@ func (conn *Conn) handleRelayDisconnectedLocked() { _ = conn.wgProxyRelay.CloseConn() conn.wgProxyRelay = nil } + conn.relayedConnRef = nil changed := conn.statusRelay.Get() != worker.StatusDisconnected if changed { @@ -930,13 +945,14 @@ func (conn *Conn) logTraceConnState() { } } -func (conn *Conn) setRelayedProxy(proxy wgproxy.Proxy) { +func (conn *Conn) setRelayedProxy(proxy wgproxy.Proxy, relayedConn *relayClient.Conn) { if conn.wgProxyRelay != nil { if err := conn.wgProxyRelay.CloseConn(); err != nil { conn.Log.Warnf("failed to close deprecated wg proxy conn: %v", err) } } conn.wgProxyRelay = proxy + conn.relayedConnRef = relayedConn } // onWGHandshakeSuccess is called when the first WireGuard handshake is detected diff --git a/client/internal/peer/worker_relay.go b/client/internal/peer/worker_relay.go index 0402992c9..fc3489992 100644 --- a/client/internal/peer/worker_relay.go +++ b/client/internal/peer/worker_relay.go @@ -3,7 +3,6 @@ package peer import ( "context" "errors" - "net" "net/netip" "sync" "sync/atomic" @@ -14,7 +13,7 @@ import ( ) type RelayConnInfo struct { - relayedConn net.Conn + relayedConn *relayClient.Conn rosenpassPubKey []byte rosenpassAddr string } @@ -27,7 +26,7 @@ type WorkerRelay struct { conn *Conn relayManager *relayClient.Manager - relayedConn net.Conn + relayedConn *relayClient.Conn relayLock sync.Mutex relaySupportedOnRemotePeer atomic.Bool @@ -80,12 +79,7 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) { w.relayedConn = relayedConn w.relayLock.Unlock() - err = w.relayManager.AddCloseListener(srv, w.onRelayClientDisconnected) - if err != nil { - log.Errorf("failed to add close listener: %s", err) - _ = relayedConn.Close() - return - } + go w.watchRelayedConn(relayedConn) w.log.Debugf("peer conn opened via Relay: %s", srv) go w.conn.onRelayConnectionIsReady(RelayConnInfo{ @@ -109,12 +103,15 @@ func (w *WorkerRelay) RelayIsSupportedLocally() bool { func (w *WorkerRelay) CloseConn() { w.relayLock.Lock() - defer w.relayLock.Unlock() - if w.relayedConn == nil { + conn := w.relayedConn + w.relayedConn = nil + w.relayLock.Unlock() + + if conn == nil { return } - if err := w.relayedConn.Close(); err != nil { + if err := conn.Close(); err != nil { w.log.Warnf("failed to close relay connection: %v", err) } } @@ -133,6 +130,8 @@ func (w *WorkerRelay) preferredRelayServer(myRelayAddress, remoteRelayAddress st return remoteRelayAddress } -func (w *WorkerRelay) onRelayClientDisconnected() { - go w.conn.onRelayDisconnected() +func (w *WorkerRelay) watchRelayedConn(relayedConn *relayClient.Conn) { + <-relayedConn.Context().Done() + + w.conn.onRelayDisconnected(relayedConn) } diff --git a/shared/relay/client/client.go b/shared/relay/client/client.go index 7171b40ad..9bb061f6e 100644 --- a/shared/relay/client/client.go +++ b/shared/relay/client/client.go @@ -30,6 +30,12 @@ const ( var ( ErrConnAlreadyExists = fmt.Errorf("connection already exists") + // ErrServerDisconnected is the cancellation cause of a relayed Conn when the + // client lost the connection to the relay server. + ErrServerDisconnected = fmt.Errorf("relay server disconnected") + // ErrPeerDisconnected is the cancellation cause of a relayed Conn when the + // remote peer went offline. + ErrPeerDisconnected = fmt.Errorf("remote peer disconnected") ) type internalStopFlag struct { @@ -74,16 +80,17 @@ type connContainer struct { msgChanLock sync.Mutex closed bool // flag to check if channel is closed ctx context.Context - cancel context.CancelFunc + cancel context.CancelCauseFunc } func newConnContainer(log *log.Entry, c *Client, peerID messages.PeerID, instanceURL *RelayAddr) *connContainer { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancelCause(context.Background()) msgChan := make(chan Msg, connChannelSize) cn := &Conn{ dstID: peerID, messageChan: msgChan, instanceURL: instanceURL, + ctx: ctx, } cc := &connContainer{ log: log, @@ -106,10 +113,6 @@ func newConnContainer(log *log.Entry, c *Client, peerID messages.PeerID, instanc return cc } -func (cc *connContainer) netConn() net.Conn { - return cc.conn -} - func (cc *connContainer) writeMsg(msg Msg) { cc.msgChanLock.Lock() defer cc.msgChanLock.Unlock() @@ -128,8 +131,8 @@ func (cc *connContainer) writeMsg(msg Msg) { } } -func (cc *connContainer) close() { - cc.cancel() +func (cc *connContainer) close(cause error) { + cc.cancel(cause) cc.msgChanLock.Lock() defer cc.msgChanLock.Unlock() @@ -293,12 +296,12 @@ func (c *Client) Connect(ctx context.Context) error { return nil } -// OpenConn create a new net.Conn for the destination peer ID. In case if the connection is in progress +// OpenConn create a new Conn for the destination peer ID. In case if the connection is in progress // to the relay server, the function will block until the connection is established or timed out. Otherwise, // it will return immediately. // It block until the server confirm the peer is online. // todo: what should happen if call with the same peerID with multiple times? -func (c *Client) OpenConn(ctx context.Context, dstPeerID string) (net.Conn, error) { +func (c *Client) OpenConn(ctx context.Context, dstPeerID string) (*Conn, error) { peerID := messages.HashID(dstPeerID) c.mu.Lock() @@ -335,7 +338,7 @@ func (c *Client) OpenConn(ctx context.Context, dstPeerID string) (net.Conn, erro delete(c.conns, peerID) } c.mu.Unlock() - container.close() + container.close(err) return nil, err } @@ -345,13 +348,13 @@ func (c *Client) OpenConn(ctx context.Context, dstPeerID string) (net.Conn, erro delete(c.conns, peerID) } c.mu.Unlock() - container.close() + container.close(ErrServerDisconnected) return nil, fmt.Errorf("relay connection is not established") } c.mu.Unlock() c.log.Infof("remote peer is available: %s", peerID) - return container.netConn(), nil + return container.conn, nil } // ServerInstanceURL returns the address of the relay server. It could change after the close and reopen the connection. @@ -773,7 +776,7 @@ func (c *Client) serverInstanceAddress() (string, netip.Addr, error) { func (c *Client) closeAllConns() { for _, container := range c.conns { - container.close() + container.close(ErrServerDisconnected) } c.conns = make(map[messages.PeerID]*connContainer) @@ -793,7 +796,7 @@ func (c *Client) closeConnsByPeerID(peerIDs []messages.PeerID) { } container.log.Infof("remote peer has been disconnected, free up connection: %s", peerID) - container.close() + container.close(ErrPeerDisconnected) delete(c.conns, peerID) } @@ -821,7 +824,7 @@ func (c *Client) closeConn(containerRef *connContainer, id messages.PeerID) erro c.log.Infof("free up connection to peer: %s", id) delete(c.conns, id) - current.close() + current.close(net.ErrClosed) return nil } diff --git a/shared/relay/client/conn.go b/shared/relay/client/conn.go index 9e2279790..67767a2b9 100644 --- a/shared/relay/client/conn.go +++ b/shared/relay/client/conn.go @@ -1,6 +1,7 @@ package client import ( + "context" "net" "time" @@ -12,11 +13,20 @@ type Conn struct { dstID messages.PeerID messageChan chan Msg instanceURL *RelayAddr + ctx context.Context writeFn func(messages.PeerID, []byte) (int, error) closeFn func(messages.PeerID) error localAddrFn func() net.Addr } +// Context returns a context that is cancelled when the connection is torn down, +// either by Close or by the relay client losing the server connection. The +// cancellation cause carries the reason, see ErrServerDisconnected and +// ErrPeerDisconnected. +func (c *Conn) Context() context.Context { + return c.ctx +} + func (c *Conn) Write(p []byte) (n int, err error) { return c.writeFn(c.dstID, p) } diff --git a/shared/relay/client/manager.go b/shared/relay/client/manager.go index 367c6dfc5..fc69e8fea 100644 --- a/shared/relay/client/manager.go +++ b/shared/relay/client/manager.go @@ -1,12 +1,9 @@ package client import ( - "container/list" "context" "fmt" - "net" "net/netip" - "reflect" "sync" "time" @@ -43,8 +40,6 @@ func NewRelayTrack() *RelayTrack { } } -type OnServerCloseListener func() - // ManagerOption configures a Manager at construction time. type ManagerOption func(*Manager) @@ -91,7 +86,6 @@ type Manager struct { relayClients map[string]*RelayTrack relayClientsMutex sync.RWMutex - onDisconnectedListeners map[string]*list.List onReconnectedListenerFn func() listenerLock sync.Mutex @@ -126,10 +120,9 @@ func NewManager(ctx context.Context, serverURLs []string, peerID string, mtu uin ConnectionTimeout: defaultConnectionTimeout, TransportFallback: tf, }, - relayClients: make(map[string]*RelayTrack), - onDisconnectedListeners: make(map[string]*list.List), - cleanupInterval: relayCleanupInterval, - keepUnusedServerTime: keepUnusedServerTime, + relayClients: make(map[string]*RelayTrack), + cleanupInterval: relayCleanupInterval, + keepUnusedServerTime: keepUnusedServerTime, } for _, opt := range opts { opt(m) @@ -168,11 +161,11 @@ func (m *Manager) Serve() error { // OpenConn opens a connection to the given peer key. If the peer is on the same relay server, the connection will be // established via the relay server. If the peer is on a different relay server, the manager will establish a new -// connection to the relay server. It returns back with a net.Conn what represent the remote peer connection. +// connection to the relay server. It returns the relayed connection to the remote peer. // // serverIP, when valid and serverAddress is foreign, is used as a dial target if the FQDN-based dial fails. // Ignored for the local home-server path. TLS verification still uses the FQDN via SNI. -func (m *Manager) OpenConn(ctx context.Context, serverAddress, peerKey string, serverIP netip.Addr) (net.Conn, error) { +func (m *Manager) OpenConn(ctx context.Context, serverAddress, peerKey string, serverIP netip.Addr) (*Conn, error) { m.relayClientMu.RLock() defer m.relayClientMu.RUnlock() @@ -185,9 +178,7 @@ func (m *Manager) OpenConn(ctx context.Context, serverAddress, peerKey string, s return nil, err } - var ( - netConn net.Conn - ) + var netConn *Conn if !foreign { log.Debugf("open peer connection via permanent server: %s", peerKey) netConn, err = m.relayClient.OpenConn(ctx, peerKey) @@ -220,31 +211,6 @@ func (m *Manager) SetOnReconnectedListener(f func()) { m.onReconnectedListenerFn = f } -// AddCloseListener adds a listener to the given server instance address. The listener will be called if the connection -// closed. -func (m *Manager) AddCloseListener(serverAddress string, onClosedListener OnServerCloseListener) error { - m.relayClientMu.RLock() - defer m.relayClientMu.RUnlock() - - if m.relayClient == nil { - return ErrRelayClientNotConnected - } - - foreign, err := m.isForeignServer(serverAddress) - if err != nil { - return err - } - - var listenerAddr string - if foreign { - listenerAddr = serverAddress - } else { - listenerAddr = m.relayClient.connectionURL - } - m.addListener(listenerAddr, onClosedListener) - return nil -} - // RelayInstanceAddress returns the address and resolved IP of the permanent relay server. It could change if the // network connection is lost. The address is sent to the target peer to choose the common relay server for the // communication; the IP is sent alongside so remote peers can dial directly without their own DNS lookup. Both @@ -330,7 +296,7 @@ func (m *Manager) UpdateToken(token *relayAuth.Token) error { return m.tokenStore.UpdateToken(token) } -func (m *Manager) openConnVia(ctx context.Context, serverAddress, peerKey string, serverIP netip.Addr) (net.Conn, error) { +func (m *Manager) openConnVia(ctx context.Context, serverAddress, peerKey string, serverIP netip.Addr) (*Conn, error) { // check if already has a connection to the desired relay server m.relayClientsMutex.RLock() rt, ok := m.relayClients[serverAddress] @@ -383,7 +349,7 @@ func (m *Manager) openConnVia(ctx context.Context, serverAddress, peerKey string // waiting for the dial started by another openConnVia call to finish. It waits // on rt.ready rather than the track lock, so it neither holds nor contends the // track lock across the dial. -func (m *Manager) openConnOnTrack(ctx context.Context, rt *RelayTrack, peerKey string) (net.Conn, error) { +func (m *Manager) openConnOnTrack(ctx context.Context, rt *RelayTrack, peerKey string) (*Conn, error) { select { case <-rt.ready: case <-ctx.Done(): @@ -428,8 +394,6 @@ func (m *Manager) onServerDisconnected(serverAddress string) { if !isHome { m.evictForeignRelay(serverAddress) } - - m.notifyOnDisconnectListeners(serverAddress) } func (m *Manager) evictForeignRelay(serverAddress string) { @@ -523,36 +487,6 @@ func (m *Manager) cleanUpUnusedRelays() { } } -func (m *Manager) addListener(serverAddress string, onClosedListener OnServerCloseListener) { - m.listenerLock.Lock() - defer m.listenerLock.Unlock() - l, ok := m.onDisconnectedListeners[serverAddress] - if !ok { - l = list.New() - } - for e := l.Front(); e != nil; e = e.Next() { - if reflect.ValueOf(e.Value).Pointer() == reflect.ValueOf(onClosedListener).Pointer() { - return - } - } - l.PushBack(onClosedListener) - m.onDisconnectedListeners[serverAddress] = l -} - -func (m *Manager) notifyOnDisconnectListeners(serverAddress string) { - m.listenerLock.Lock() - defer m.listenerLock.Unlock() - - l, ok := m.onDisconnectedListeners[serverAddress] - if !ok { - return - } - for e := l.Front(); e != nil; e = e.Next() { - go e.Value.(OnServerCloseListener)() - } - delete(m.onDisconnectedListeners, serverAddress) -} - func relayConnState(c *Client) RelayConnState { addr, err := c.ServerInstanceURL() if err != nil { diff --git a/shared/relay/client/manager_test.go b/shared/relay/client/manager_test.go index 9e964f688..4a7840dd7 100644 --- a/shared/relay/client/manager_test.go +++ b/shared/relay/client/manager_test.go @@ -2,7 +2,9 @@ package client import ( "context" + "errors" "fmt" + "net" "net/netip" "testing" "time" @@ -291,35 +293,29 @@ func TestForeignAutoClose(t *testing.T) { t.Fatalf("failed to serve manager: %s", err) } - // Set up a disconnect listener to track when foreign server disconnects foreignServerURL := toURL(srvCfg2)[0] - disconnected := make(chan struct{}) - onDisconnect := func() { - select { - case disconnected <- struct{}{}: - default: - } - } t.Log("open connection to another peer") if _, err = mgr.OpenConn(ctx, foreignServerURL, "anotherpeer", netip.Addr{}); err == nil { t.Fatalf("should have failed to open connection to another peer") } - // Add the disconnect listener after the connection attempt - if err := mgr.AddCloseListener(foreignServerURL, onDisconnect); err != nil { - t.Logf("failed to add close listener (expected if connection failed): %s", err) - } - - // Wait for cleanup to happen timeout := relayCleanupInterval + keepUnusedServerTime + 2*time.Second t.Logf("waiting for relay cleanup: %s", timeout) - - select { - case <-disconnected: - t.Log("foreign relay connection cleaned up successfully") - case <-time.After(timeout): - t.Log("timeout waiting for cleanup - this might be expected if connection never established") + deadline := time.After(timeout) + for { + mgr.relayClientsMutex.RLock() + _, tracked := mgr.relayClients[foreignServerURL] + mgr.relayClientsMutex.RUnlock() + if !tracked { + t.Log("foreign relay connection cleaned up successfully") + break + } + select { + case <-deadline: + t.Fatal("foreign relay was not cleaned up") + case <-time.After(200 * time.Millisecond): + } } t.Logf("closing manager") @@ -413,23 +409,24 @@ func waitForReady(ctx context.Context, m *Manager, timeout time.Duration) error return fmt.Errorf("manager not ready within %s", timeout) } -func TestNotifierDoubleAdd(t *testing.T) { +func toURL(address server.ListenerConfig) []string { + return []string{"rel://" + address.Address} +} + +func TestConnContextCancelledOnServerDisconnect(t *testing.T) { ctx := context.Background() - listenerCfg1 := server.ListenerConfig{ - Address: "localhost:52501", - } - srv, err := server.NewServer(newManagerTestServerConfig(listenerCfg1.Address)) + srvCfg := server.ListenerConfig{Address: "localhost:52601"} + srv, err := server.NewServer(newManagerTestServerConfig(srvCfg.Address)) if err != nil { t.Fatalf("failed to create server: %s", err) } errChan := make(chan error, 1) go func() { - if err := srv.Listen(listenerCfg1); err != nil { + if err := srv.Listen(srvCfg); err != nil { errChan <- err } }() - defer func() { if err := srv.Shutdown(ctx); err != nil { t.Errorf("failed to close server: %s", err) @@ -440,46 +437,106 @@ func TestNotifierDoubleAdd(t *testing.T) { t.Fatalf("failed to start server: %s", err) } - log.Debugf("connect by alice") mCtx, cancel := context.WithCancel(ctx) defer cancel() - clientBob := NewManager(mCtx, toURL(listenerCfg1), "bob", iface.DefaultMTU) - if err = clientBob.Serve(); err != nil { + mgrBob := NewManager(mCtx, toURL(srvCfg), "bob", iface.DefaultMTU) + if err := mgrBob.Serve(); err != nil { + t.Fatalf("failed to serve bob manager: %s", err) + } + + mgr := NewManager(mCtx, toURL(srvCfg), "alice", iface.DefaultMTU) + if err := mgr.Serve(); err != nil { t.Fatalf("failed to serve manager: %s", err) } - clientAlice := NewManager(mCtx, toURL(listenerCfg1), "alice", iface.DefaultMTU) - if err = clientAlice.Serve(); err != nil { + ra, _, err := mgr.RelayInstanceAddress() + if err != nil { + t.Fatalf("failed to get relay address: %s", err) + } + + relayedConn, err := mgr.OpenConn(ctx, ra, "bob", netip.Addr{}) + if err != nil { + t.Fatalf("failed to open conn: %s", err) + } + + select { + case <-relayedConn.Context().Done(): + t.Fatal("conn context cancelled while the relay is still up") + default: + } + + _ = mgr.relayClient.relayConn.Close() + + select { + case <-relayedConn.Context().Done(): + case <-time.After(15 * time.Second): + t.Fatal("conn context was not cancelled after the relay connection dropped") + } + + if cause := context.Cause(relayedConn.Context()); !errors.Is(cause, ErrServerDisconnected) { + t.Errorf("unexpected cancellation cause: %v, want %v", cause, ErrServerDisconnected) + } +} + +func TestConnContextCauseOnLocalClose(t *testing.T) { + ctx := context.Background() + + srvCfg := server.ListenerConfig{Address: "localhost:52602"} + srv, err := server.NewServer(newManagerTestServerConfig(srvCfg.Address)) + if err != nil { + t.Fatalf("failed to create server: %s", err) + } + errChan := make(chan error, 1) + go func() { + if err := srv.Listen(srvCfg); err != nil { + errChan <- err + } + }() + defer func() { + if err := srv.Shutdown(ctx); err != nil { + t.Errorf("failed to close server: %s", err) + } + }() + + if err := waitForServerToStart(errChan); err != nil { + t.Fatalf("failed to start server: %s", err) + } + + mCtx, cancel := context.WithCancel(ctx) + defer cancel() + + mgrBob := NewManager(mCtx, toURL(srvCfg), "bob", iface.DefaultMTU) + if err := mgrBob.Serve(); err != nil { + t.Fatalf("failed to serve bob manager: %s", err) + } + + mgr := NewManager(mCtx, toURL(srvCfg), "alice", iface.DefaultMTU) + if err := mgr.Serve(); err != nil { t.Fatalf("failed to serve manager: %s", err) } - conn1, err := clientAlice.OpenConn(ctx, clientAlice.ServerURLs()[0], "bob", netip.Addr{}) + ra, _, err := mgr.RelayInstanceAddress() if err != nil { - t.Fatalf("failed to bind channel: %s", err) + t.Fatalf("failed to get relay address: %s", err) } - fnCloseListener := OnServerCloseListener(func() { - log.Infof("close listener") - }) - - err = clientAlice.AddCloseListener(clientAlice.ServerURLs()[0], fnCloseListener) + relayedConn, err := mgr.OpenConn(ctx, ra, "bob", netip.Addr{}) if err != nil { - t.Fatalf("failed to add close listener: %s", err) + t.Fatalf("failed to open conn: %s", err) } - err = clientAlice.AddCloseListener(clientAlice.ServerURLs()[0], fnCloseListener) - if err != nil { - t.Fatalf("failed to add close listener: %s", err) + if err := relayedConn.Close(); err != nil { + t.Fatalf("failed to close conn: %s", err) } - err = conn1.Close() - if err != nil { - t.Errorf("failed to close connection: %s", err) + select { + case <-relayedConn.Context().Done(): + case <-time.After(5 * time.Second): + t.Fatal("conn context was not cancelled after a local close") } -} - -func toURL(address server.ListenerConfig) []string { - return []string{"rel://" + address.Address} + if cause := context.Cause(relayedConn.Context()); !errors.Is(cause, net.ErrClosed) { + t.Errorf("unexpected cancellation cause after a local close: %v, want %v", cause, net.ErrClosed) + } } From ee2344502eaa139636afd97483b414f1f8d58145 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:26:52 +0200 Subject: [PATCH 6/9] [management] fix group resource validation (#7608) --- .../http/handlers/groups/groups_handler.go | 45 ++++++++++++------- .../handlers/groups/groups_handler_test.go | 43 +++++++++++++++++- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/management/server/http/handlers/groups/groups_handler.go b/management/server/http/handlers/groups/groups_handler.go index f8d161a87..ed01e7c3d 100644 --- a/management/server/http/handlers/groups/groups_handler.go +++ b/management/server/http/handlers/groups/groups_handler.go @@ -148,13 +148,10 @@ func (h *handler) updateGroup(w http.ResponseWriter, r *http.Request) { peers = *req.Peers } - resources := make([]types.Resource, 0) - if req.Resources != nil { - for _, res := range *req.Resources { - resource := types.Resource{} - resource.FromAPIRequest(&res) - resources = append(resources, resource) - } + resources, err := resourcesFromAPIRequest(req.Resources) + if err != nil { + util.WriteError(r.Context(), err, w) + return } group := types.Group{ @@ -210,13 +207,10 @@ func (h *handler) createGroup(w http.ResponseWriter, r *http.Request) { peers = *req.Peers } - resources := make([]types.Resource, 0) - if req.Resources != nil { - for _, res := range *req.Resources { - resource := types.Resource{} - resource.FromAPIRequest(&res) - resources = append(resources, resource) - } + resources, err := resourcesFromAPIRequest(req.Resources) + if err != nil { + util.WriteError(r.Context(), err, w) + return } group := types.Group{ @@ -335,11 +329,30 @@ func toGroupResponse(peers []*nbpeer.Peer, group *types.Group) *api.Group { gr.PeersCount = len(gr.Peers) for _, res := range group.Resources { - resResp := res.ToAPIResponse() - gr.Resources = append(gr.Resources, *resResp) + if resResp := res.ToAPIResponse(); resResp != nil { + gr.Resources = append(gr.Resources, *resResp) + } } gr.ResourcesCount = len(gr.Resources) return &gr } + +func resourcesFromAPIRequest(req *[]api.Resource) ([]types.Resource, error) { + resources := make([]types.Resource, 0) + if req == nil { + return resources, nil + } + + for _, res := range *req { + if res.Id == "" || !types.ResourceType(res.Type).Valid() { + return nil, status.Errorf(status.InvalidArgument, "resource id shouldn't be empty and type must be one of: peer, domain, host, subnet") + } + resource := types.Resource{} + resource.FromAPIRequest(&res) + resources = append(resources, resource) + } + + return resources, nil +} diff --git a/management/server/http/handlers/groups/groups_handler_test.go b/management/server/http/handlers/groups/groups_handler_test.go index 57e238630..78e4a2578 100644 --- a/management/server/http/handlers/groups/groups_handler_test.go +++ b/management/server/http/handlers/groups/groups_handler_test.go @@ -8,8 +8,8 @@ import ( "fmt" "io" "net/http" - "net/netip" "net/http/httptest" + "net/netip" "strings" "testing" @@ -208,6 +208,33 @@ func TestWriteGroup(t *testing.T) { expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, + { + name: "Write Group POST Empty Resource", + requestType: http.MethodPost, + requestPath: "/api/groups", + requestBody: bytes.NewBuffer( + []byte(`{"name":"With Resource","resources":[{}]}`)), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "Write Group PUT Empty Resource", + requestType: http.MethodPut, + requestPath: "/api/groups/id-existed", + requestBody: bytes.NewBuffer( + []byte(`{"name":"With Resource","resources":[{"id":"","type":"host"}]}`)), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "Write Group POST Unknown Resource Type", + requestType: http.MethodPost, + requestPath: "/api/groups", + requestBody: bytes.NewBuffer( + []byte(`{"name":"With Resource","resources":[{"id":"res-1","type":"banana"}]}`)), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, { name: "Write Group PUT OK", requestType: http.MethodPut, @@ -376,6 +403,20 @@ func TestGetAllGroups(t *testing.T) { } } +func TestToGroupResponseSkipsEmptyResource(t *testing.T) { + group := &types.Group{ + ID: "id-resources", + Name: "Resources", + Issued: types.GroupIssuedAPI, + Resources: []types.Resource{{}, {ID: "res-1", Type: types.ResourceTypeHost}}, + } + + got := toGroupResponse(nil, group) + + assert.Equal(t, 1, got.ResourcesCount) + assert.Equal(t, []api.Resource{{Id: "res-1", Type: api.ResourceType(types.ResourceTypeHost)}}, got.Resources) +} + func TestDeleteGroup(t *testing.T) { tt := []struct { name string From f6109a3395f9ce34649cc2af7dc2638d28e1d98e Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:44:10 +0900 Subject: [PATCH 7/9] [client] Remove the empty GPO DNS policy store on Windows teardown (#7563) --- client/internal/dns/host_windows.go | 98 ++++++++++++++--- client/internal/dns/host_windows_test.go | 129 +++++++++++++++++++++++ 2 files changed, 213 insertions(+), 14 deletions(-) diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 948000a3d..6462d0c37 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -124,19 +124,9 @@ func newHostManager(wgInterface WGIface) (*registryConfigurator, error) { return nil, err } - var useGPO bool - k, err := registry.OpenKey(registry.LOCAL_MACHINE, GPODNSPolicyConfigRoot, registry.QUERY_VALUE) - if err != nil { - log.Debugf("failed to open GPO DNS policy root: %v", err) - } else { - closer(k) - useGPO = true - log.Infof("detected GPO DNS policy configuration, using policy store") - } - configurator := ®istryConfigurator{ guid: guid, - gpo: useGPO, + gpo: useGPOPolicyStore(), } origNameservers, err := configurator.captureOriginalNameservers() @@ -576,14 +566,22 @@ func (r *registryConfigurator) setInterfaceRegistryKeyStringValue(key, value str return nil } +// deleteInterfaceRegistryKeyProperty removes a value from the interface key. +// A value that is already gone, or an interface key that is, is not an error: +// the caller asked for the value not to be there, and a cleanup that runs twice +// has to reach its later steps on the second run as well. func (r *registryConfigurator) deleteInterfaceRegistryKeyProperty(propertyKey string) error { regKey, err := r.getInterfaceRegistryKey() - if err != nil { + switch { + case errors.Is(err, registry.ErrNotExist), errors.Is(err, syscall.ERROR_PATH_NOT_FOUND): + log.Debugf("interface key of %s does not exist, nothing to delete %s from", r.guid, propertyKey) + return nil + case err != nil: return fmt.Errorf("get interface registry key: %w", err) } defer closer(regKey) - if err := regKey.DeleteValue(propertyKey); err != nil { + if err := regKey.DeleteValue(propertyKey); err != nil && !errors.Is(err, registry.ErrNotExist) { return fmt.Errorf("delete registry key %s: %w", propertyKey, err) } return nil @@ -612,7 +610,12 @@ func (r *registryConfigurator) restoreHostDNS() error { go r.flushDNSCache() - return nil + // Last, and only on the way out, once no rule of ours is left: during a + // session the store is where the rules of this run live, and emptying it + // mid-session would have the next rule recreate it anyway. Propagated so a + // failure keeps the shutdown state for the next run to retry, rather than + // leaving the store to hold up every rule change from here on. + return removeEmptyGPOPolicyStore() } // removeDNSMatchPolicies deletes every NRPT rule this client may have created, @@ -651,6 +654,73 @@ func (r *registryConfigurator) restoreUncleanShutdownDNS() error { return r.restoreHostDNS() } +// useGPOPolicyStore reports whether NRPT rules have to go into the group policy +// store, and clears an empty one out of the way first. +// +// The order is the point. A store left empty by an earlier run would otherwise +// decide this run too, sending its rules somewhere the resolver only reads when +// the policy engine next applies DNS client policy. Removing it before the +// choice is made leaves the local store authoritative for the whole session, +// including the first one after an upgrade. +func useGPOPolicyStore() bool { + if err := removeEmptyGPOPolicyStore(); err != nil { + // Nothing to retry against here: the worst case is the run going + // through the group policy store, which is where it would have gone + // before this check existed. + log.Warnf("%v", err) + } + + k, err := registry.OpenKey(registry.LOCAL_MACHINE, GPODNSPolicyConfigRoot, registry.QUERY_VALUE) + if err != nil { + log.Debugf("failed to open GPO DNS policy root: %v", err) + return false + } + closer(k) + + log.Infof("detected GPO DNS policy configuration, using policy store") + return true +} + +// removeEmptyGPOPolicyStore deletes the group policy DnsPolicyConfig key once +// nothing is left in it. The key survives the deletion of the last rule it +// held, and the client treats its presence as "group policy configures the +// NRPT", so an empty one left behind keeps every later run writing rules there. +// Rules in that store reach the resolver only when the policy engine next +// applies DNS client policy, and a rule this client writes belongs to no GPO, +// so nothing schedules that application: both adding and removing a rule are +// held up by a minute or more, and for a removal that is a catch-all rule +// resolving every name over an interface that no longer exists. With the store +// absent the local one is authoritative and a change applies at once. +// +// A store that still holds rules, values or subkeys of somebody else's is left +// alone. +func removeEmptyGPOPolicyStore() error { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, GPODNSPolicyConfigRoot, registry.QUERY_VALUE) + switch { + case errors.Is(err, registry.ErrNotExist), errors.Is(err, syscall.ERROR_PATH_NOT_FOUND): + return nil + case err != nil: + return fmt.Errorf("open HKEY_LOCAL_MACHINE\\%s: %w", GPODNSPolicyConfigRoot, err) + } + + info, err := k.Stat() + closer(k) + if err != nil { + return fmt.Errorf("stat HKEY_LOCAL_MACHINE\\%s: %w", GPODNSPolicyConfigRoot, err) + } + + if info.SubKeyCount != 0 || info.ValueCount != 0 { + return nil + } + + if err := registry.DeleteKey(registry.LOCAL_MACHINE, GPODNSPolicyConfigRoot); err != nil { + return fmt.Errorf("delete empty HKEY_LOCAL_MACHINE\\%s: %w", GPODNSPolicyConfigRoot, err) + } + + log.Infof("removed the empty GPO DNS policy store, leaving the local one authoritative") + return nil +} + // listNRPTRuleKeys returns the names of our NRPT rule keys under a policy store // root. An absent root holds nothing to clean up, which is the normal state of // the GPO store on a machine without DNS Client policy. diff --git a/client/internal/dns/host_windows_test.go b/client/internal/dns/host_windows_test.go index 7aef64590..353f6adbc 100644 --- a/client/internal/dns/host_windows_test.go +++ b/client/internal/dns/host_windows_test.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sys/windows/registry" + + "github.com/netbirdio/netbird/client/internal/winregistry" ) // TestNRPTEntriesCleanupOnConfigChange tests that old NRPT entries are properly cleaned up @@ -405,3 +407,130 @@ func TestNRPTDomainBatching(t *testing.T) { }) } } + +// TestRemoveEmptyGPOPolicyStore verifies that cleanup takes the GPO policy +// store itself with it once our rules are gone, since the store existing keeps +// the local one from being applied, and that a store with somebody else's rule +// in it is left alone. +func TestRemoveEmptyGPOPolicyStore(t *testing.T) { + if testing.Short() { + t.Skip("skipping registry integration test in short mode") + } + + t.Cleanup(func() { cleanupRegistryKeys(t) }) + cleanupRegistryKeys(t) + + testIP := netip.MustParseAddr("100.64.0.1") + cfg := ®istryConfigurator{gpo: true} + + // a store holding a rule of ours is kept, because the rule is still applied + require.NoError(t, cfg.addDNSMatchPolicy([]string{".example.com"}, testIP)) + exists, err := registryKeyExists(gpoDnsPolicyConfigMatchPath + "-0") + require.NoError(t, err) + require.True(t, exists, "Should write the rule to the GPO policy store") + + require.NoError(t, removeEmptyGPOPolicyStore()) + exists, err = registryKeyExists(GPODNSPolicyConfigRoot) + require.NoError(t, err) + assert.True(t, exists, "Should keep a policy store that still holds a rule") + + // once the rules are gone the store goes with them + require.NoError(t, cfg.removeDNSMatchPolicies()) + require.NoError(t, removeEmptyGPOPolicyStore()) + + exists, err = registryKeyExists(GPODNSPolicyConfigRoot) + require.NoError(t, err) + assert.False(t, exists, "Should remove the GPO policy store once it is empty") + + // A store is not ours to remove while somebody else has a rule in it. The + // rule is written volatile like our own: the rules above created the parent + // chain volatile, and Windows refuses a stable subkey under a volatile + // parent. + foreignRule := GPODNSPolicyConfigRoot + `\{2A3B4C5D-6E7F-4041-8283-84858687888A}` + foreignKey, _, err := winregistry.CreateVolatileKey(registry.LOCAL_MACHINE, foreignRule, registry.SET_VALUE) + require.NoError(t, err, "Should create a foreign GPO rule") + foreignKey.Close() + t.Cleanup(func() { + _ = registry.DeleteKey(registry.LOCAL_MACHINE, foreignRule) + _ = registry.DeleteKey(registry.LOCAL_MACHINE, GPODNSPolicyConfigRoot) + }) + + require.NoError(t, cfg.removeDNSMatchPolicies()) + require.NoError(t, removeEmptyGPOPolicyStore()) + + exists, err = registryKeyExists(foreignRule) + require.NoError(t, err) + assert.True(t, exists, "Should not remove a foreign rule") + exists, err = registryKeyExists(GPODNSPolicyConfigRoot) + require.NoError(t, err) + assert.True(t, exists, "Should keep a policy store that still holds a foreign rule") +} + +// TestDeleteInterfaceRegistryKeyPropertyTwice verifies that removing a value +// that is already gone, or one on an interface key that is, reports success. +// Teardown runs again after a failed cleanup, and the steps that follow this +// one have to be reached on that second run. +func TestDeleteInterfaceRegistryKeyPropertyTwice(t *testing.T) { + if testing.Short() { + t.Skip("skipping registry integration test in short mode") + } + + testGUID := "{12345678-1234-1234-1234-123456789ABC}" + interfacePath := InterfaceConfigPath + `\` + testGUID + testKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, interfacePath, registry.SET_VALUE) + require.NoError(t, err, "Should create test interface registry key") + testKey.Close() + t.Cleanup(func() { + _ = registry.DeleteKey(registry.LOCAL_MACHINE, interfacePath) + }) + + cfg := ®istryConfigurator{guid: testGUID} + + require.NoError(t, cfg.setInterfaceRegistryKeyStringValue(interfaceConfigSearchListKey, "example.com")) + require.NoError(t, cfg.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey)) + assert.NoError(t, cfg.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey), + "Should report success for a value that is already gone") + + // and with the interface key itself gone, as it is once the adapter is + require.NoError(t, registry.DeleteKey(registry.LOCAL_MACHINE, interfacePath)) + assert.NoError(t, cfg.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey), + "Should report success when the interface key does not exist") +} + +// TestUseGPOPolicyStoreClearsEmptyStore verifies that the store is cleared +// before it is consulted, so an empty one left by an earlier run does not send +// this run's rules to the group policy store. A store somebody else has a rule +// in still decides where the rules go. +func TestUseGPOPolicyStoreClearsEmptyStore(t *testing.T) { + if testing.Short() { + t.Skip("skipping registry integration test in short mode") + } + + t.Cleanup(func() { cleanupRegistryKeys(t) }) + cleanupRegistryKeys(t) + + // the leftover an earlier run used to keep, which the client read as + // "group policy configures the NRPT" for every run after it + emptyStore, _, err := winregistry.CreateVolatileKey(registry.LOCAL_MACHINE, GPODNSPolicyConfigRoot, registry.SET_VALUE) + require.NoError(t, err, "Should create the GPO policy store") + emptyStore.Close() + + assert.False(t, useGPOPolicyStore(), "An empty store should not decide where the rules go") + exists, err := registryKeyExists(GPODNSPolicyConfigRoot) + require.NoError(t, err) + assert.False(t, exists, "Should clear the empty store before consulting it") + + foreignRule := GPODNSPolicyConfigRoot + `\{2A3B4C5D-6E7F-4041-8283-84858687888A}` + foreignKey, _, err := winregistry.CreateVolatileKey(registry.LOCAL_MACHINE, foreignRule, registry.SET_VALUE) + require.NoError(t, err, "Should create a foreign GPO rule") + foreignKey.Close() + t.Cleanup(func() { + _ = registry.DeleteKey(registry.LOCAL_MACHINE, foreignRule) + _ = registry.DeleteKey(registry.LOCAL_MACHINE, GPODNSPolicyConfigRoot) + }) + + assert.True(t, useGPOPolicyStore(), "A store holding a rule should decide where the rules go") + exists, err = registryKeyExists(GPODNSPolicyConfigRoot) + require.NoError(t, err) + assert.True(t, exists, "Should keep a store that holds a rule") +} From 9a5395d3140956389433410192ca8e2753baf7f4 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:16:57 +0200 Subject: [PATCH 8/9] [management] add store support to filter by public id (#7208) --- .../networks/resources/types/resource.go | 2 +- management/server/store/sql_store.go | 68 ++++++++++++++++ management/server/store/sql_store_test.go | 78 +++++++++++++++++++ management/server/store/store.go | 3 + management/server/store/store_mock.go | 45 +++++++++++ management/server/types/policy.go | 2 +- route/route.go | 2 +- 7 files changed, 197 insertions(+), 3 deletions(-) diff --git a/management/server/networks/resources/types/resource.go b/management/server/networks/resources/types/resource.go index 4cf7f7ea3..bb33e00eb 100644 --- a/management/server/networks/resources/types/resource.go +++ b/management/server/networks/resources/types/resource.go @@ -32,7 +32,7 @@ type NetworkResource struct { ID string `gorm:"primaryKey"` NetworkID string `gorm:"index"` AccountID string `gorm:"index"` - PublicID string `json:"-"` + PublicID string `json:"-" gorm:"index"` Name string Description string Type NetworkResourceType diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 1e99251b2..b32be5af9 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -58,6 +58,7 @@ const ( keyQueryCondition = "key = ?" mysqlKeyQueryCondition = "`key` = ?" accountAndIDQueryCondition = "account_id = ? and id = ?" + accountAndAnyIDQueryCondition = "account_id = ? and (id = ? or public_id = ?)" accountAndPeerIDQueryCondition = "account_id = ? and peer_id = ?" accountAndIDsQueryCondition = "account_id = ? AND id IN ?" accountIDCondition = "account_id = ?" @@ -4063,6 +4064,30 @@ func (s *SqlStore) GetPolicyByID(ctx context.Context, lockStrength LockingStreng return policy, nil } +// GetPolicyByIDOrPublicID retrieves a policy by either its ID or its PublicID. Peers report +// whichever of the two the network map they were served carries, so callers resolving a +// peer-reported reference cannot know upfront which namespace it belongs to. +func (s *SqlStore) GetPolicyByIDOrPublicID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*types.Policy, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + + var policy *types.Policy + + result := tx.Preload(clause.Associations). + Take(&policy, accountAndAnyIDQueryCondition, accountID, policyID, policyID) + if err := result.Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, status.NewPolicyNotFoundError(policyID) + } + log.WithContext(ctx).Errorf("failed to get policy from store: %s", err) + return nil, status.Errorf(status.Internal, "failed to get policy from store") + } + + return policy, nil +} + func (s *SqlStore) CreatePolicy(ctx context.Context, policy *types.Policy) error { result := s.db.Create(policy) if result.Error != nil { @@ -4248,6 +4273,27 @@ func (s *SqlStore) GetRouteByID(ctx context.Context, lockStrength LockingStrengt return route, nil } +// GetRouteByIDOrPublicID retrieves a route by either its ID or its PublicID. See +// GetPolicyByIDOrPublicID for why peer-reported references need both. +func (s *SqlStore) GetRouteByIDOrPublicID(ctx context.Context, lockStrength LockingStrength, accountID string, routeID string) (*route.Route, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + + var route *route.Route + result := tx.Take(&route, accountAndAnyIDQueryCondition, accountID, routeID, routeID) + if err := result.Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, status.NewRouteNotFoundError(routeID) + } + log.WithContext(ctx).Errorf("failed to get route from the store: %s", err) + return nil, status.Errorf(status.Internal, "failed to get route from store") + } + + return route, nil +} + // SaveRoute saves a route to the database. func (s *SqlStore) SaveRoute(ctx context.Context, route *route.Route) error { result := s.db.Save(route) @@ -4642,6 +4688,28 @@ func (s *SqlStore) GetNetworkResourceByID(ctx context.Context, lockStrength Lock return netResources, nil } +// GetNetworkResourceByIDOrPublicID retrieves a network resource by either its ID or its +// PublicID. See GetPolicyByIDOrPublicID for why peer-reported references need both. +func (s *SqlStore) GetNetworkResourceByIDOrPublicID(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) (*resourceTypes.NetworkResource, error) { + tx := s.db + if lockStrength != LockingStrengthNone { + tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)}) + } + + var netResources *resourceTypes.NetworkResource + result := tx. + Take(&netResources, accountAndAnyIDQueryCondition, accountID, resourceID, resourceID) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, status.NewNetworkResourceNotFoundError(resourceID) + } + log.WithContext(ctx).Errorf("failed to get network resource from store: %v", result.Error) + return nil, status.Errorf(status.Internal, "failed to get network resource from store") + } + + return netResources, nil +} + func (s *SqlStore) GetNetworkResourceByName(ctx context.Context, lockStrength LockingStrength, accountID, resourceName string) (*resourceTypes.NetworkResource, error) { tx := s.db if lockStrength != LockingStrengthNone { diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index fbcff5257..73132bf75 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -1972,6 +1972,32 @@ func TestSqlStore_GetPolicyByID(t *testing.T) { } } +func TestSqlStore_GetPolicyByIDOrPublicID(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + policyID := "cs1tnh0hhcjnqoiuebf0" + + policy, err := store.GetPolicyByID(context.Background(), LockingStrengthNone, accountID, policyID) + require.NoError(t, err) + require.NotEmpty(t, policy.PublicID) + + for _, id := range []string{policyID, policy.PublicID} { + policy, err := store.GetPolicyByIDOrPublicID(context.Background(), LockingStrengthNone, accountID, id) + require.NoError(t, err) + require.Equal(t, policyID, policy.ID) + } + + policy, err = store.GetPolicyByIDOrPublicID(context.Background(), LockingStrengthNone, accountID, "non-existing") + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Nil(t, policy) +} + func TestSqlStore_CreatePolicy(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) t.Cleanup(cleanup) @@ -2631,6 +2657,32 @@ func TestSqlStore_GetNetworkResourceByID(t *testing.T) { } } +func TestSqlStore_GetNetworkResourceByIDOrPublicID(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + netResourceID := "ctc4nci7qv9061u6ilfg" + + netResource, err := store.GetNetworkResourceByID(context.Background(), LockingStrengthNone, accountID, netResourceID) + require.NoError(t, err) + require.NotEmpty(t, netResource.PublicID) + + for _, id := range []string{netResourceID, netResource.PublicID} { + netResource, err := store.GetNetworkResourceByIDOrPublicID(context.Background(), LockingStrengthNone, accountID, id) + require.NoError(t, err) + require.Equal(t, netResourceID, netResource.ID) + } + + netResource, err = store.GetNetworkResourceByIDOrPublicID(context.Background(), LockingStrengthNone, accountID, "non-existing") + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Nil(t, netResource) +} + func TestSqlStore_SaveNetworkResource(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) t.Cleanup(cleanup) @@ -3756,6 +3808,32 @@ func TestSqlStore_GetRouteByID(t *testing.T) { } } +func TestSqlStore_GetRouteByIDOrPublicID(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + routeID := "ct03t427qv97vmtmglog" + + route, err := store.GetRouteByID(context.Background(), LockingStrengthNone, accountID, routeID) + require.NoError(t, err) + require.NotEmpty(t, route.PublicID) + + for _, id := range []string{routeID, route.PublicID} { + route, err := store.GetRouteByIDOrPublicID(context.Background(), LockingStrengthNone, accountID, id) + require.NoError(t, err) + require.Equal(t, routeID, string(route.ID)) + } + + route, err = store.GetRouteByIDOrPublicID(context.Background(), LockingStrengthNone, accountID, "non-existing") + require.Error(t, err) + sErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, sErr.Type(), status.NotFound) + require.Nil(t, route) +} + func TestSqlStore_SaveRoute(t *testing.T) { store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) t.Cleanup(cleanup) diff --git a/management/server/store/store.go b/management/server/store/store.go index 97da95b4c..b6368f47f 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -138,6 +138,7 @@ type Store interface { GetAccountPolicies(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Policy, error) GetPolicyByID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*types.Policy, error) + GetPolicyByIDOrPublicID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*types.Policy, error) CreatePolicy(ctx context.Context, policy *types.Policy) error SavePolicy(ctx context.Context, policy *types.Policy) error DeletePolicy(ctx context.Context, accountID, policyID string) error @@ -208,6 +209,7 @@ type Store interface { GetAccountRoutes(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*route.Route, error) GetRouteByID(ctx context.Context, lockStrength LockingStrength, accountID, routeID string) (*route.Route, error) + GetRouteByIDOrPublicID(ctx context.Context, lockStrength LockingStrength, accountID, routeID string) (*route.Route, error) SaveRoute(ctx context.Context, route *route.Route) error DeleteRoute(ctx context.Context, accountID, routeID string) error @@ -248,6 +250,7 @@ type Store interface { GetNetworkResourcesByNetID(ctx context.Context, lockStrength LockingStrength, accountID, netID string) ([]*resourceTypes.NetworkResource, error) GetNetworkResourcesByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*resourceTypes.NetworkResource, error) GetNetworkResourceByID(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) (*resourceTypes.NetworkResource, error) + GetNetworkResourceByIDOrPublicID(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) (*resourceTypes.NetworkResource, error) GetNetworkResourceByName(ctx context.Context, lockStrength LockingStrength, accountID, resourceName string) (*resourceTypes.NetworkResource, error) SaveNetworkResource(ctx context.Context, resource *resourceTypes.NetworkResource) error DeleteNetworkResource(ctx context.Context, accountID, resourceID string) error diff --git a/management/server/store/store_mock.go b/management/server/store/store_mock.go index 399a07a19..460ba712b 100644 --- a/management/server/store/store_mock.go +++ b/management/server/store/store_mock.go @@ -2166,6 +2166,21 @@ func (mr *MockStoreMockRecorder) GetNetworkResourceByID(ctx, lockStrength, accou return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkResourceByID", reflect.TypeOf((*MockStore)(nil).GetNetworkResourceByID), ctx, lockStrength, accountID, resourceID) } +// GetNetworkResourceByIDOrPublicID mocks base method. +func (m *MockStore) GetNetworkResourceByIDOrPublicID(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) (*types0.NetworkResource, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNetworkResourceByIDOrPublicID", ctx, lockStrength, accountID, resourceID) + ret0, _ := ret[0].(*types0.NetworkResource) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNetworkResourceByIDOrPublicID indicates an expected call of GetNetworkResourceByIDOrPublicID. +func (mr *MockStoreMockRecorder) GetNetworkResourceByIDOrPublicID(ctx, lockStrength, accountID, resourceID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkResourceByIDOrPublicID", reflect.TypeOf((*MockStore)(nil).GetNetworkResourceByIDOrPublicID), ctx, lockStrength, accountID, resourceID) +} + // GetNetworkResourceByName mocks base method. func (m *MockStore) GetNetworkResourceByName(ctx context.Context, lockStrength LockingStrength, accountID, resourceName string) (*types0.NetworkResource, error) { m.ctrl.T.Helper() @@ -2496,6 +2511,21 @@ func (mr *MockStoreMockRecorder) GetPolicyByID(ctx, lockStrength, accountID, pol return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPolicyByID", reflect.TypeOf((*MockStore)(nil).GetPolicyByID), ctx, lockStrength, accountID, policyID) } +// GetPolicyByIDOrPublicID mocks base method. +func (m *MockStore) GetPolicyByIDOrPublicID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*types3.Policy, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPolicyByIDOrPublicID", ctx, lockStrength, accountID, policyID) + ret0, _ := ret[0].(*types3.Policy) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPolicyByIDOrPublicID indicates an expected call of GetPolicyByIDOrPublicID. +func (mr *MockStoreMockRecorder) GetPolicyByIDOrPublicID(ctx, lockStrength, accountID, policyID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPolicyByIDOrPublicID", reflect.TypeOf((*MockStore)(nil).GetPolicyByIDOrPublicID), ctx, lockStrength, accountID, policyID) +} + // GetPolicyRulesByResourceID mocks base method. func (m *MockStore) GetPolicyRulesByResourceID(ctx context.Context, lockStrength LockingStrength, accountID, peerID string) ([]*types3.PolicyRule, error) { m.ctrl.T.Helper() @@ -2676,6 +2706,21 @@ func (mr *MockStoreMockRecorder) GetRouteByID(ctx, lockStrength, accountID, rout return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRouteByID", reflect.TypeOf((*MockStore)(nil).GetRouteByID), ctx, lockStrength, accountID, routeID) } +// GetRouteByIDOrPublicID mocks base method. +func (m *MockStore) GetRouteByIDOrPublicID(ctx context.Context, lockStrength LockingStrength, accountID, routeID string) (*route.Route, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRouteByIDOrPublicID", ctx, lockStrength, accountID, routeID) + ret0, _ := ret[0].(*route.Route) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRouteByIDOrPublicID indicates an expected call of GetRouteByIDOrPublicID. +func (mr *MockStoreMockRecorder) GetRouteByIDOrPublicID(ctx, lockStrength, accountID, routeID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRouteByIDOrPublicID", reflect.TypeOf((*MockStore)(nil).GetRouteByIDOrPublicID), ctx, lockStrength, accountID, routeID) +} + // GetRoutingPeerNetworks mocks base method. func (m *MockStore) GetRoutingPeerNetworks(ctx context.Context, accountID, peerID string) ([]string, error) { m.ctrl.T.Helper() diff --git a/management/server/types/policy.go b/management/server/types/policy.go index 0f7298d18..9786d17b6 100644 --- a/management/server/types/policy.go +++ b/management/server/types/policy.go @@ -29,7 +29,7 @@ type Policy struct { // ID of the policy' ID string `gorm:"primaryKey"` - PublicID string `json:"-"` + PublicID string `json:"-" gorm:"index"` // AccountID is a reference to Account that this object belongs AccountID string `json:"-" gorm:"index"` diff --git a/route/route.go b/route/route.go index 3bdb0a3a1..ef9a39ef7 100644 --- a/route/route.go +++ b/route/route.go @@ -95,7 +95,7 @@ type Route struct { ID ID `gorm:"primaryKey"` // AccountID is a reference to Account that this object belongs AccountID string `gorm:"index"` - PublicID string `json:"-"` + PublicID string `json:"-" gorm:"index"` // Network and Domains are mutually exclusive Network netip.Prefix `gorm:"serializer:json"` Domains domain.List `gorm:"serializer:json"` From cb7ca8ef3f02cf747d35412362598c57784eb79c Mon Sep 17 00:00:00 2001 From: Riccardo Manfrin <3090891+riccardomanfrin@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:50:44 +0200 Subject: [PATCH 9/9] [client,management] Skip route firewall rule computation when no firewall (#7624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [client,management] Skip route firewall rule computation when no firewall A peer that runs with the firewall disabled has no ACL manager and no firewall to program, so nothing ever reads RoutesFirewallRules: the only consumers are acl.Manager, which is reached solely when e.acl is set, and the legacy-management probe in updateNetworkMap, which is guarded by a non-nil firewall. Building those rules is the most expensive part of a sync on a peer that routes many network resources. On a 15k-peer deployment a debug bundle showed getPeerNetworkResourceFirewallRules accounting for 62% of the allocations of Calculate, and Calculate for effectively all of the allocations of handleSync, which was taking 3.2s on average and holding the engine lock for the duration. Let the caller ask Calculate to leave the rules out. The client passes its existing DisableFirewall setting; the management server keeps the default and still produces them. RoutesFirewallRulesIsEmpty is set from the resulting empty list, so a receiver that would otherwise infer legacy management from an empty rule set does not misread the skip. * [client,management] Cover the skip flag through the envelope Review feedback on #7624. The components test compared only the length of the peer firewall rules, so a change to their content would have passed while the message claimed they came out unchanged. Compare the slices. The skip path was also only exercised by setting the field directly on the components, which bypasses the envelope conversion where RoutesFirewallRulesIsEmpty is derived. That bit is what keeps the client from reading skipped rules as a legacy management server, so it gets a test that goes through EnvelopeToNetworkMap with the flag set. * [management] Give the router a peer ACL so the rule comparison bites Review feedback on #7624. peer-router-1 appears in no peer ACL in the shared fixture, so its FirewallRules came out empty and the equality assertion compared two empty slices — it would have passed even if the peer rules were dropped entirely. Add a policy covering the router and require the baseline to be non-empty before comparing. --- client/internal/engine.go | 6 +- .../network_map/nmaptest/runner.go | 2 +- .../types/networkmap_components_test.go | 57 ++++++++ shared/management/networkmap/envelope.go | 8 +- shared/management/networkmap/envelope_test.go | 124 ++++++++++++++++-- .../management/types/networkmap_components.go | 15 ++- 6 files changed, 199 insertions(+), 13 deletions(-) diff --git a/client/internal/engine.go b/client/internal/engine.go index d517d1d68..1613bbbc9 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1061,7 +1061,11 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { // back to empty if the FQDN doesn't have the expected shape. dnsName = extractDNSDomainFromFQDN(pc.GetFqdn()) } - result, err := nbnetworkmap.EnvelopeToNetworkMap(e.ctx, envelope, localKey, dnsName) + // With the firewall disabled there is no ACL manager to program, so + // RoutesFirewallRules would be built and then dropped. On a peer that + // routes many network resources that is the single most expensive + // step of the sync. + result, err := nbnetworkmap.EnvelopeToNetworkMap(e.ctx, envelope, localKey, dnsName, e.config.DisableFirewall) if err != nil { return fmt.Errorf("decode network map envelope: %w", err) } diff --git a/management/internals/controllers/network_map/nmaptest/runner.go b/management/internals/controllers/network_map/nmaptest/runner.go index 0d6ac9c18..ffce6483e 100644 --- a/management/internals/controllers/network_map/nmaptest/runner.go +++ b/management/internals/controllers/network_map/nmaptest/runner.go @@ -245,7 +245,7 @@ func computeMode(t *testing.T, ctx context.Context, mode Mode, nmData *networkma peerGroups := maps.Keys(nmData.GetPeerGroups(peerID)) resp := mgmtgrpc.ToComponentSyncResponse(ctx, nil, nil, nil, peer, nil, nil, components, nil, dnsDomain, nil, nmData.AccountSettings, nil, peerGroups, dnsFwdPort) - res, err := networkmap.EnvelopeToNetworkMap(ctx, resp.NetworkMapEnvelope, peer.Key, dnsDomain) + res, err := networkmap.EnvelopeToNetworkMap(ctx, resp.NetworkMapEnvelope, peer.Key, dnsDomain, false) require.NoError(t, err, "expand envelope") return res.NetworkMap default: diff --git a/management/server/types/networkmap_components_test.go b/management/server/types/networkmap_components_test.go index f6d542609..9e76de775 100644 --- a/management/server/types/networkmap_components_test.go +++ b/management/server/types/networkmap_components_test.go @@ -175,6 +175,63 @@ func TestNetworkMapComponents_NetworkResourceRoutes_RouterPeer(t *testing.T) { assert.NotEmpty(t, nm.RoutesFirewallRules, "router peer should have route firewall rules for the resource") } +// A receiver without a firewall asks Calculate to skip the route firewall +// rules. Everything the rest of the sync consumes — routes, peers, peer +// firewall rules — must come out unchanged. +func TestNetworkMapComponents_SkipRouteFirewallRules(t *testing.T) { + ctx := context.Background() + account := createComponentTestAccount() + + // The shared fixture leaves peer-router-1 out of every peer ACL, so its + // FirewallRules would be empty and the comparison below vacuous. Give the + // router a policy of its own. + account.Policies = append(account.Policies, &types.Policy{ + ID: "policy-router", Name: "Router connectivity", Enabled: true, + Rules: []*types.PolicyRule{{ + ID: "rule-router", Name: "Allow all <-> router", Enabled: true, + Action: types.PolicyTrafficActionAccept, Protocol: types.PolicyRuleProtocolALL, + Bidirectional: true, + Sources: []string{"group-all"}, Destinations: []string{"group-all"}, + }}, + }) + + validated := allPeersValidated(account) + + components := account.GetPeerNetworkMapComponents( + ctx, + "peer-router-1", + account.GetPeersCustomZone(ctx, "netbird.io"), + nil, + validated, + account.GetResourcePoliciesMap(), + account.GetResourceRoutersMap(), + account.GetActiveGroupUsers(), + ) + + full := components.Calculate(ctx) + require.NotEmpty(t, full.RoutesFirewallRules, "baseline: router peer must get route firewall rules") + require.NotEmpty(t, full.FirewallRules, "baseline: router peer must get peer firewall rules") + + components.SkipRouteFirewallRules = true + skipped := components.Calculate(ctx) + + assert.Empty(t, skipped.RoutesFirewallRules, "route firewall rules must not be computed when skipped") + assert.ElementsMatch(t, routeNetworks(full.Routes), routeNetworks(skipped.Routes), + "skipping route firewall rules must not change the routes") + assert.ElementsMatch(t, peerIDs(full.Peers), peerIDs(skipped.Peers), + "skipping route firewall rules must not change the peers to connect") + assert.Equal(t, full.FirewallRules, skipped.FirewallRules, + "peer firewall rules are unrelated and must come out unchanged") +} + +func routeNetworks(routes []*nmdata.Route) []string { + networks := make([]string, 0, len(routes)) + for _, r := range routes { + networks = append(networks, r.Network.String()) + } + return networks +} + func TestNetworkMapComponents_NetworkResourceRoutes_UnrelatedPeer(t *testing.T) { account := createComponentTestAccount() validated := allPeersValidated(account) diff --git a/shared/management/networkmap/envelope.go b/shared/management/networkmap/envelope.go index e7961fd7b..fd9dd6bbd 100644 --- a/shared/management/networkmap/envelope.go +++ b/shared/management/networkmap/envelope.go @@ -35,7 +35,12 @@ type EnvelopeResult struct { // // dnsName is the account's DNS domain ("netbird.cloud" etc.); used when // rebuilding the per-peer FQDNs that proto.RemotePeerConfig carries. -func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, localPeerKey, dnsName string) (*EnvelopeResult, error) { +// +// skipRouteFirewallRules leaves RoutesFirewallRules empty. Callers that have +// no firewall to program pass true: the rules are the most expensive part of +// Calculate on a peer that routes many network resources, and nothing reads +// them afterwards. +func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, localPeerKey, dnsName string, skipRouteFirewallRules bool) (*EnvelopeResult, error) { components, err := DecodeEnvelope(ctx, env) if err != nil { return nil, fmt.Errorf("decode envelope: %w", err) @@ -53,6 +58,7 @@ func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, lo return nil, fmt.Errorf("receiving peer (wg_key prefix %q) not found among %d decoded peers — components have no PeerID, Calculate would return empty", trimKey(localPeerKey), len(components.Peers)) } components.PeerID = canonicalKey + components.SkipRouteFirewallRules = skipRouteFirewallRules includeIPv6 := localPeer.SupportsIPv6() && localPeer.IPv6.IsValid() useSourcePrefixes := localPeer.SupportsSourcePrefixes() diff --git a/shared/management/networkmap/envelope_test.go b/shared/management/networkmap/envelope_test.go index 7fe2a5277..98333a3c8 100644 --- a/shared/management/networkmap/envelope_test.go +++ b/shared/management/networkmap/envelope_test.go @@ -9,6 +9,7 @@ import ( "net/netip" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" goproto "google.golang.org/protobuf/proto" @@ -37,7 +38,7 @@ func TestEnvelopeToNetworkMap_RoundTrip(t *testing.T) { var decoded proto.NetworkMapEnvelope require.NoError(t, goproto.Unmarshal(wire, &decoded), "unmarshal envelope") - result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud") + result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud", false) require.NoError(t, err, "EnvelopeToNetworkMap") require.NotNil(t, result) require.NotNil(t, result.NetworkMap, "decoded NetworkMap must be non-nil") @@ -78,7 +79,7 @@ func TestCalculate_FirewallRuleProtocol_NeverNetbirdSSH(t *testing.T) { var decoded proto.NetworkMapEnvelope require.NoError(t, goproto.Unmarshal(wire, &decoded)) - result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud") + result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud", false) require.NoError(t, err) require.NotEmpty(t, result.NetworkMap.FirewallRules, "ssh policy should produce firewall rules") for i, fr := range result.NetworkMap.FirewallRules { @@ -88,13 +89,13 @@ func TestCalculate_FirewallRuleProtocol_NeverNetbirdSSH(t *testing.T) { } func TestEnvelopeToNetworkMap_NilEnvelope(t *testing.T) { - _, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), nil, "key", "netbird.cloud") + _, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), nil, "key", "netbird.cloud", false) require.Error(t, err, "nil envelope must produce an error rather than panic") } func TestEnvelopeToNetworkMap_FullPayloadMissing(t *testing.T) { env := &proto.NetworkMapEnvelope{} - _, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), env, "key", "netbird.cloud") + _, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), env, "key", "netbird.cloud", false) require.Error(t, err, "envelope with no Full payload must produce an error") } @@ -126,7 +127,7 @@ func TestDecodeEnvelope_MalformedWgKeyPeerSkipped(t *testing.T) { var decoded proto.NetworkMapEnvelope require.NoError(t, goproto.Unmarshal(wire, &decoded), "unmarshal envelope") - result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud") + result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud", false) require.NoError(t, err, "EnvelopeToNetworkMap must tolerate one bad peer key") require.NotNil(t, result) require.NotNil(t, result.Components) @@ -195,7 +196,7 @@ func TestEnvelopeRoundTrip_AllGroupShortCircuitParity(t *testing.T) { var decodedEnv proto.NetworkMapEnvelope require.NoError(t, goproto.Unmarshal(wire, &decodedEnv), "unmarshal envelope") - result, err := nbnetworkmap.EnvelopeToNetworkMap(ctx, &decodedEnv, peers["peer-T"].Key, "netbird.cloud") + result, err := nbnetworkmap.EnvelopeToNetworkMap(ctx, &decodedEnv, peers["peer-T"].Key, "netbird.cloud", false) require.NoError(t, err, "EnvelopeToNetworkMap") clientNM := result.NetworkMap @@ -253,7 +254,7 @@ func TestEnvelopeToNetworkMap_EmptyComponents(t *testing.T) { var decoded proto.NetworkMapEnvelope require.NoError(t, goproto.Unmarshal(wire, &decoded), "unmarshal envelope") - result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud") + result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud", false) require.NoError(t, err, "EnvelopeToNetworkMap must degrade gracefully on empty components") require.Equal(t, uint64(7), result.NetworkMap.Serial) require.Empty(t, result.NetworkMap.RemotePeers, "unvalidated peer connects to nobody") @@ -276,7 +277,7 @@ func TestEnvelopeToNetworkMap_MissingNetwork(t *testing.T) { var decoded proto.NetworkMapEnvelope require.NoError(t, goproto.Unmarshal(wire, &decoded), "unmarshal envelope") - result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud") + result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud", false) require.NoError(t, err, "a missing AccountNetwork must not panic the client") require.NotNil(t, result.Components.Network) require.NotEmpty(t, result.NetworkMap.RemotePeers, "the rest of the snapshot stays usable") @@ -353,3 +354,110 @@ func randomWgKey(t *testing.T) string { require.NoError(t, err) return base64.StdEncoding.EncodeToString(raw[:]) } + +// TestEnvelopeToNetworkMap_SkipRouteFirewallRules covers the flag end to end, +// through the envelope rather than by poking Calculate directly. The +// RoutesFirewallRulesIsEmpty derivation is the part that matters: the client's +// legacy-management probe reads an empty rule list together with that bit, so +// skipping the rules must set it rather than leave it false. +func TestEnvelopeToNetworkMap_SkipRouteFirewallRules(t *testing.T) { + ctx := context.Background() + c, routerKey := buildRoutedResourceComponents(t) + + envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{ + Components: c, + DNSDomain: "netbird.cloud", + }) + wire, err := goproto.Marshal(envelope) + require.NoError(t, err, "marshal envelope") + var decoded proto.NetworkMapEnvelope + require.NoError(t, goproto.Unmarshal(wire, &decoded), "unmarshal envelope") + + full, err := nbnetworkmap.EnvelopeToNetworkMap(ctx, &decoded, routerKey, "netbird.cloud", false) + require.NoError(t, err, "EnvelopeToNetworkMap without skip") + require.NotEmpty(t, full.NetworkMap.RoutesFirewallRules, + "baseline: the router peer must receive route firewall rules") + require.False(t, full.NetworkMap.RoutesFirewallRulesIsEmpty, + "baseline: the empty bit must be false when rules are present") + + var decodedSkip proto.NetworkMapEnvelope + require.NoError(t, goproto.Unmarshal(wire, &decodedSkip), "unmarshal envelope") + skipped, err := nbnetworkmap.EnvelopeToNetworkMap(ctx, &decodedSkip, routerKey, "netbird.cloud", true) + require.NoError(t, err, "EnvelopeToNetworkMap with skip") + + assert.Empty(t, skipped.NetworkMap.RoutesFirewallRules, + "route firewall rules must not be computed when skipped") + assert.True(t, skipped.NetworkMap.RoutesFirewallRulesIsEmpty, + "the empty bit must be derived from the skipped list, or the client misreads it as legacy management") + assert.Len(t, skipped.NetworkMap.Routes, len(full.NetworkMap.Routes), + "skipping route firewall rules must not change the routes") + assert.Len(t, skipped.NetworkMap.RemotePeers, len(full.NetworkMap.RemotePeers), + "skipping route firewall rules must not change the remote peers") +} + +// buildRoutedResourceComponents returns components in which the local peer is +// the routing peer for one enabled network resource, reachable by a second +// peer through a resource policy — the minimum shape that yields a non-empty +// RoutesFirewallRules. It also returns the local peer's WG key. +func buildRoutedResourceComponents(t *testing.T) (*types.NetworkMapComponents, string) { + t.Helper() + + routerKey := randomWgKey(t) + peers := map[string]*nmdata.Peer{ + "peer-R": { + ID: "peer-R", Key: routerKey, DNSLabel: "router", + IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}), + Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"}, + }, + "peer-S": { + ID: "peer-S", Key: randomWgKey(t), DNSLabel: "source", + IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}), + Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"}, + }, + } + + resourcePolicy := &nmdata.Policy{ + ID: "pol-res", PublicID: "10", Enabled: true, + Rules: []*nmdata.PolicyRule{{ + ID: "rule-res", + Enabled: true, + Action: string(types.PolicyTrafficActionAccept), + Protocol: string(types.PolicyRuleProtocolALL), + Sources: []string{"g-src"}, + }}, + } + + c := &types.NetworkMapComponents{ + PeerID: "peer-R", + Network: &nmdata.Network{ + Identifier: "net-routed-resource", + Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}, + Serial: 1, + }, + AccountSettings: &nmdata.AccountSettingsInfo{}, + DNSSettings: &nmdata.DNSSettings{}, + Peers: peers, + Groups: map[string]*nmdata.Group{ + "g-src": {PublicID: "1", Name: "sources", Peers: []string{"peer-S"}}, + "g-routers": {PublicID: "2", Name: "routers", Peers: []string{"peer-R"}}, + }, + NetworkResources: []*nmdata.NetworkResource{{ + ID: "res-1", NetworkID: "netid-1", PublicID: "100", Name: "res1", + Type: "subnet", + Prefix: netip.MustParsePrefix("10.200.0.0/24"), + Enabled: true, + }}, + RoutersMap: map[string]map[string]*nmdata.NetworkRouter{ + "netid-1": {"peer-R": { + PublicID: "200", PeerGroups: []string{"g-routers"}, Metric: 9999, Enabled: true, + }}, + }, + ResourcePoliciesMap: map[string][]*nmdata.Policy{ + "res-1": {resourcePolicy}, + }, + Policies: []*nmdata.Policy{resourcePolicy}, + NetworkXIDToPublicID: map[string]string{"netid-1": "1"}, + } + + return c, routerKey +} diff --git a/shared/management/types/networkmap_components.go b/shared/management/types/networkmap_components.go index e18db4ec0..7742ca244 100644 --- a/shared/management/types/networkmap_components.go +++ b/shared/management/types/networkmap_components.go @@ -58,6 +58,13 @@ type NetworkMapComponents struct { // domain targets. ForceRoutingPeerDNSResolution bool + // SkipRouteFirewallRules drops the route firewall rule computation from + // Calculate. A receiver without a firewall manager never reads + // RoutesFirewallRules, and on a routing peer with many network resources + // building them dominates the cost of a sync. Defaults to false so the + // management server keeps producing them. + SkipRouteFirewallRules bool + routesByPeerOnce sync.Once routesByPeerIdx map[string][]routeIndexEntry @@ -149,11 +156,15 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap { includeIPv6 = p.SupportsIPv6() && p.IPv6.IsValid() } routesUpdate := filterAndExpandRoutes(c.getRoutesToSync(targetPeerID, peersToConnect, peerGroups), includeIPv6) - routesFirewallRules := c.getPeerRoutesFirewallRules(ctx, targetPeerID, includeIPv6) + + var routesFirewallRules []*RouteFirewallRule + if !c.SkipRouteFirewallRules { + routesFirewallRules = c.getPeerRoutesFirewallRules(ctx, targetPeerID, includeIPv6) + } isRouter, networkResourcesRoutes, sourcePeers := c.getNetworkResourcesRoutesToSync(targetPeerID) var networkResourcesFirewallRules []*RouteFirewallRule - if isRouter { + if isRouter && !c.SkipRouteFirewallRules { networkResourcesFirewallRules = c.getPeerNetworkResourceFirewallRules(ctx, targetPeerID, networkResourcesRoutes, includeIPv6) }