From 279adb8fdd5c5eb6d0f960fb6b65ab543ebb1f9d Mon Sep 17 00:00:00 2001 From: Edward <43848523+thomashacker@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:58:14 +0200 Subject: [PATCH 1/5] [management] Return the reason when custom domain validation fails (#8122) * [management] Return the reason when custom domain validation fails The validate endpoint returned an empty 202 and ran the check in a goroutine, so a failed verification only reached the management log. Run the check in the request and return 204 or 412 with a classified reason. Resolver errors are not forwarded. * [management] Address review feedback on custom domain validation Pass the request context to the lookup and store calls now that the check runs in the request, compare the self-resolved name case-insensitively, document the 412 like the other entries, and check the returned error in the existing tests. * [management] Match custom domain CNAME targets case-insensitively DNS names are case-insensitive, so a CNAME pointing at the cluster in different casing should validate instead of failing as a mismatch. * [management] Add validation timeout --------- Co-authored-by: DevBot NetBird <91386968+netbirddev@users.noreply.github.com> --- .../modules/reverseproxy/domain/interface.go | 2 +- .../reverseproxy/domain/manager/api.go | 7 +- .../domain/manager/expiration_test.go | 8 +- .../reverseproxy/domain/manager/manager.go | 87 +++++----- .../domain/manager/manager_realstore_test.go | 4 +- .../domain/manager/normalization_test.go | 2 +- .../domain/manager/validation_test.go | 151 ++++++++++++++++++ .../modules/reverseproxy/domain/validator.go | 59 ++++++- .../reverseproxy/domain/validator_test.go | 66 ++++++++ shared/management/http/api/openapi.yml | 9 +- 10 files changed, 331 insertions(+), 64 deletions(-) create mode 100644 management/internals/modules/reverseproxy/domain/manager/validation_test.go diff --git a/management/internals/modules/reverseproxy/domain/interface.go b/management/internals/modules/reverseproxy/domain/interface.go index a4bba5841..c729fb7b0 100644 --- a/management/internals/modules/reverseproxy/domain/interface.go +++ b/management/internals/modules/reverseproxy/domain/interface.go @@ -8,6 +8,6 @@ type Manager interface { GetDomains(ctx context.Context, accountID, userID string) ([]*Domain, error) CreateDomain(ctx context.Context, accountID, userID, domainName, targetCluster string) (*Domain, error) DeleteDomain(ctx context.Context, accountID, userID, domainID string) error - ValidateDomain(ctx context.Context, accountID, userID, domainID string) + ValidateDomain(ctx context.Context, accountID, userID, domainID string) error GetClusterDomains() []string } diff --git a/management/internals/modules/reverseproxy/domain/manager/api.go b/management/internals/modules/reverseproxy/domain/manager/api.go index f01329010..ec5daf7d6 100644 --- a/management/internals/modules/reverseproxy/domain/manager/api.go +++ b/management/internals/modules/reverseproxy/domain/manager/api.go @@ -134,7 +134,10 @@ func (h *handler) triggerCustomDomainValidation(w http.ResponseWriter, r *http.R return } - go h.manager.ValidateDomain(r.Context(), userAuth.AccountId, userAuth.UserId, domainID) + if err := h.manager.ValidateDomain(r.Context(), userAuth.AccountId, userAuth.UserId, domainID); err != nil { + util.WriteError(r.Context(), err, w) + return + } - w.WriteHeader(http.StatusAccepted) + w.WriteHeader(http.StatusNoContent) } diff --git a/management/internals/modules/reverseproxy/domain/manager/expiration_test.go b/management/internals/modules/reverseproxy/domain/manager/expiration_test.go index e3b05efbb..3a3bd6e92 100644 --- a/management/internals/modules/reverseproxy/domain/manager/expiration_test.go +++ b/management/internals/modules/reverseproxy/domain/manager/expiration_test.go @@ -29,7 +29,7 @@ func TestValidateDomain_ExpiredRegistration(t *testing.T) { Update("validation_expires_at", expiresAt).Error) env.resolver.set("validation.expired.example.com", testCluster) - env.manager.ValidateDomain(ctx, accountA, accountAUser, d.ID) + assert.Error(t, env.manager.ValidateDomain(ctx, accountA, accountAUser, d.ID), "an expired registration must be refused") stored := storedDomain(t, env.store, accountA, d.Domain) require.NotNil(t, stored) @@ -47,7 +47,7 @@ func TestCreateDomain_ValidationDeadline(t *testing.T) { assert.Equal(t, createdAt.Add(48*time.Hour), *d.ValidationExpiresAt, "new registrations get 48 hours") time.Sleep(time.Hour) - env.manager.ValidateDomain(ctx, accountA, accountAUser, d.ID) + assert.Error(t, env.manager.ValidateDomain(ctx, accountA, accountAUser, d.ID), "a missing CNAME must fail validation") stored := storedDomain(t, env.store, accountA, d.Domain) require.NotNil(t, stored) require.NotNil(t, stored.ValidationExpiresAt) @@ -198,9 +198,10 @@ func TestValidateDomain_DeadlinePassesDuringLookup(t *testing.T) { resolver := blockingDomainResolver{started: make(chan struct{}), release: make(chan struct{})} env.manager.validator.Resolver = resolver done := make(chan struct{}) + var validateErr error go func() { defer close(done) - env.manager.ValidateDomain(ctx, accountA, accountAUser, d.ID) + validateErr = env.manager.ValidateDomain(ctx, accountA, accountAUser, d.ID) }() <-resolver.started time.Sleep(48 * time.Hour) @@ -211,6 +212,7 @@ func TestValidateDomain_DeadlinePassesDuringLookup(t *testing.T) { } close(resolver.release) <-done + assert.Error(t, validateErr, "a validation finishing after the deadline must be refused") owner := accountA if cleanup { assert.Nil(t, storedDomain(t, env.store, accountA, d.Domain), "late validation must not restore the old claim") diff --git a/management/internals/modules/reverseproxy/domain/manager/manager.go b/management/internals/modules/reverseproxy/domain/manager/manager.go index 1e9ecab43..f92ed8201 100644 --- a/management/internals/modules/reverseproxy/domain/manager/manager.go +++ b/management/internals/modules/reverseproxy/domain/manager/manager.go @@ -227,22 +227,19 @@ func (m Manager) DeleteDomain(ctx context.Context, accountID, userID, domainID s return nil } -func (m Manager) ValidateDomain(ctx context.Context, accountID, userID, domainID string) { - ok, _, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Create) +// validationTimeout bounds the DNS check so it ends, and reports why, before a proxy in +// front of management times the request out. +const validationTimeout = 10 * time.Second + +// ValidateDomain checks the domain's validation CNAME and marks it validated. A failure +// the user can act on is returned as PreconditionFailed carrying the reason. +func (m Manager) ValidateDomain(ctx context.Context, accountID, userID, domainID string) error { + ok, ctx, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Create) if err != nil { - log.WithFields(log.Fields{ - "accountID": accountID, - "domainID": domainID, - }).WithError(err).Error("validate domain") - return + return status.NewPermissionValidationError(err) } if !ok { - log.WithFields(log.Fields{ - "accountID": accountID, - "domainID": domainID, - "userID": userID, - }).Error("validate domain: permission denied") - return + return status.NewPermissionDeniedError() } log.WithFields(log.Fields{ @@ -250,32 +247,23 @@ func (m Manager) ValidateDomain(ctx context.Context, accountID, userID, domainID "domainID": domainID, }).Info("starting domain validation") - d, err := m.store.GetCustomDomain(context.Background(), accountID, domainID) + d, err := m.store.GetCustomDomain(ctx, accountID, domainID) if err != nil { - log.WithFields(log.Fields{ - "accountID": accountID, - "domainID": domainID, - }).WithError(err).Error("get custom domain from store") - return + return fmt.Errorf("get domain from store: %w", err) } if d.Validated { - return + return nil } if d.ValidationExpiresAt == nil || !time.Now().Before(*d.ValidationExpiresAt) { - log.WithFields(log.Fields{"accountID": accountID, "domainID": domainID}). - Debug("custom domain validation window has expired") - return + return validationFailed(domain.NewValidationError(domain.ValidationReasonExpired, + "custom domain %s validation window has expired; delete and add the domain again", d.Domain)) } // Validate only against the domain's target cluster targetCluster := d.TargetCluster if targetCluster == "" { - log.WithFields(log.Fields{ - "accountID": accountID, - "domainID": domainID, - "domain": d.Domain, - }).Warn("domain has no target cluster set, skipping validation") - return + return validationFailed(domain.NewValidationError(domain.ValidationReasonNoTargetCluster, + "custom domain %s has no target cluster", d.Domain)) } log.WithFields(log.Fields{ @@ -285,32 +273,35 @@ func (m Manager) ValidateDomain(ctx context.Context, accountID, userID, domainID "targetCluster": targetCluster, }).Info("validating domain against target cluster") - if m.validator.IsValid(context.Background(), d.Domain, []string{targetCluster}) { + lookupCtx, cancel := context.WithTimeout(ctx, validationTimeout) + _, err = m.validator.Validate(lookupCtx, d.Domain, []string{targetCluster}) + cancel() + if err == nil { d.Validated = true - if _, err := m.store.UpdateCustomDomain(context.Background(), accountID, d); err != nil { - entry := log.WithFields(log.Fields{ - "accountID": accountID, - "domainID": domainID, - }).WithError(err) - if sErr, ok := status.FromError(err); ok && sErr.Type() == status.PreconditionFailed { - entry.Debug("custom domain registration is no longer pending validation") - return + if _, err := m.store.UpdateCustomDomain(ctx, accountID, d); err != nil { + // A concurrent request may have validated the domain first. + if current, getErr := m.store.GetCustomDomain(ctx, accountID, domainID); getErr == nil && current.Validated { + return nil } - entry.Error("update custom domain in store") - return + return fmt.Errorf("update domain in store: %w", err) } log.WithFields(log.Fields{"accountID": accountID, "domainID": domainID}). Info("custom domain validated successfully") - m.accountManager.StoreEvent(context.Background(), userID, domainID, accountID, activity.DomainValidated, d.EventMeta()) - } else { - log.WithFields(log.Fields{ - "accountID": accountID, - "domainID": domainID, - "domain": d.Domain, - "targetCluster": targetCluster, - }).Warn("domain validation failed - CNAME does not match target cluster") + m.accountManager.StoreEvent(ctx, userID, domainID, accountID, activity.DomainValidated, d.EventMeta()) + return nil } + + return validationFailed(err) +} + +// validationFailed shows a classified failure to the user and keeps anything else internal. +func validationFailed(err error) error { + var vErr *domain.ValidationError + if errors.As(err, &vErr) { + return status.Errorf(status.PreconditionFailed, "%s", vErr.Message) + } + return fmt.Errorf("validate domain: %w", err) } // GetClusterDomains returns a list of proxy cluster domains. diff --git a/management/internals/modules/reverseproxy/domain/manager/manager_realstore_test.go b/management/internals/modules/reverseproxy/domain/manager/manager_realstore_test.go index fed402498..dfb999fbd 100644 --- a/management/internals/modules/reverseproxy/domain/manager/manager_realstore_test.go +++ b/management/internals/modules/reverseproxy/domain/manager/manager_realstore_test.go @@ -229,7 +229,7 @@ func TestValidateDomain_UnlocksClusterDerivation(t *testing.T) { require.Error(t, err) env.resolver.set("validation.later.example.com", testCluster) - env.manager.ValidateDomain(ctx, accountA, accountAUser, created.ID) + require.NoError(t, env.manager.ValidateDomain(ctx, accountA, accountAUser, created.ID)) require.True(t, storedDomain(t, env.store, accountA, "later.example.com").Validated) @@ -286,7 +286,7 @@ func TestValidateDomain_PermissionDeniedDoesNotValidate(t *testing.T) { // a validated domain is the permission check. env.resolver.set("validation.guarded.example.com", testCluster) - env.manager.ValidateDomain(ctx, accountA, accountAMember, created.ID) + assert.Error(t, env.manager.ValidateDomain(ctx, accountA, accountAMember, created.ID), "a caller without permission must be refused") stored := storedDomain(t, env.store, accountA, "guarded.example.com") require.NotNil(t, stored) diff --git a/management/internals/modules/reverseproxy/domain/manager/normalization_test.go b/management/internals/modules/reverseproxy/domain/manager/normalization_test.go index da5e4bdd1..810454c57 100644 --- a/management/internals/modules/reverseproxy/domain/manager/normalization_test.go +++ b/management/internals/modules/reverseproxy/domain/manager/normalization_test.go @@ -52,7 +52,7 @@ func TestCreateDomain_NormalizedNameCanValidateLater(t *testing.T) { require.False(t, created.Validated, "a missing CNAME must leave the normalized registration pending") env.resolver.set("validation.apps.example.com", testCluster) - env.manager.ValidateDomain(ctx, accountA, accountAUser, created.ID) + require.NoError(t, env.manager.ValidateDomain(ctx, accountA, accountAUser, created.ID)) stored, err := env.store.GetCustomDomain(ctx, accountA, created.ID) require.NoError(t, err) assert.Equal(t, "apps.example.com", stored.Domain, "retrying validation must retain the normalized name") diff --git a/management/internals/modules/reverseproxy/domain/manager/validation_test.go b/management/internals/modules/reverseproxy/domain/manager/validation_test.go new file mode 100644 index 000000000..b7800c6f5 --- /dev/null +++ b/management/internals/modules/reverseproxy/domain/manager/validation_test.go @@ -0,0 +1,151 @@ +package manager + +import ( + "context" + "testing" + "testing/synctest" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain" + nbstore "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/shared/management/status" +) + +// The validator tests cover each DNS outcome. These cover what the manager adds on top: +// the status type the API turns into a response code, and the checks before the lookup. +func TestValidateDomain_ReturnsReason(t *testing.T) { + ctx := context.Background() + tests := []struct { + name string + setup func(t *testing.T, env *domainTestEnv, d *domain.Domain) + user string + want status.Type + message string + }{ + { + name: "cname mismatch", + setup: func(_ *testing.T, env *domainTestEnv, d *domain.Domain) { + env.resolver.set("validation."+d.Domain, "other.example.net") + }, + want: status.PreconditionFailed, + message: "CNAME record validation.test.example.com points to other.example.net; point it to " + testCluster, + }, + { + name: "expired window", + setup: func(t *testing.T, env *domainTestEnv, d *domain.Domain) { + setDomainColumn(t, env, d, "validation_expires_at", time.Now().Add(-time.Second)) + }, + want: status.PreconditionFailed, + message: "custom domain test.example.com validation window has expired; delete and add the domain again", + }, + { + name: "no target cluster", + setup: func(t *testing.T, env *domainTestEnv, d *domain.Domain) { + setDomainColumn(t, env, d, "target_cluster", "") + }, + want: status.PreconditionFailed, + message: "custom domain test.example.com has no target cluster", + }, + { + name: "permission denied", + setup: func(_ *testing.T, env *domainTestEnv, d *domain.Domain) { + env.resolver.set("validation."+d.Domain, testCluster) + }, + user: accountAMember, + want: status.PermissionDenied, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env := setupDomainTest(t) + d, err := env.manager.CreateDomain(ctx, accountA, accountAUser, "test.example.com", testCluster) + require.NoError(t, err) + tt.setup(t, env, d) + user := accountAUser + if tt.user != "" { + user = tt.user + } + + err = env.manager.ValidateDomain(ctx, accountA, user, d.ID) + sErr, ok := status.FromError(err) + require.True(t, ok, "expected a status error, got %v", err) + assert.Equal(t, tt.want, sErr.Type(), "status type") + if tt.message != "" { + assert.Equal(t, tt.message, sErr.Message, "user-facing message") + } + assert.False(t, storedDomain(t, env.store, accountA, d.Domain).Validated, "a failed check must not validate the domain") + }) + } +} + +func TestValidateDomain_SuccessIsIdempotent(t *testing.T) { + ctx := context.Background() + env := setupDomainTest(t) + d, err := env.manager.CreateDomain(ctx, accountA, accountAUser, "ok.example.com", testCluster) + require.NoError(t, err) + env.resolver.set("validation.ok.example.com", testCluster) + + require.NoError(t, env.manager.ValidateDomain(ctx, accountA, accountAUser, d.ID)) + assert.True(t, storedDomain(t, env.store, accountA, d.Domain).Validated, "domain should be validated") + assert.NoError(t, env.manager.ValidateDomain(ctx, accountA, accountAUser, d.ID), "an already validated domain is a success") +} + +func setDomainColumn(t *testing.T, env *domainTestEnv, d *domain.Domain, column string, value any) { + t.Helper() + db := env.store.(*nbstore.SqlStore).GetDB() + require.NoError(t, db.Model(&domain.Domain{}).Where("id = ?", d.ID).Update(column, value).Error) +} + +// Two verify requests can both read the domain as pending. The one that loses the store +// update must still report success when the other one validated the domain. +func TestValidateDomain_ConcurrentValidationSucceeds(t *testing.T) { + ctx := context.Background() + env := setupDomainTest(t) + d, err := env.manager.CreateDomain(ctx, accountA, accountAUser, "twice.example.com", testCluster) + require.NoError(t, err) + resolver := blockingDomainResolver{started: make(chan struct{}), release: make(chan struct{})} + env.manager.validator.Resolver = resolver + + done := make(chan error, 1) + go func() { + done <- env.manager.ValidateDomain(ctx, accountA, accountAUser, d.ID) + }() + <-resolver.started + // The other request completes while this one is still looking up the CNAME. + setDomainColumn(t, env, d, "validated", true) + close(resolver.release) + + assert.NoError(t, <-done, "a domain validated by a concurrent request is a success") +} + +// hangingResolver never answers, so only a deadline on the context ends the lookup. +type hangingResolver struct{} + +func (hangingResolver) LookupCNAME(ctx context.Context, _ string) (string, error) { + <-ctx.Done() + return "", ctx.Err() +} + +// A lookup that hangs must end on the validation timeout and still report the reason, +// rather than leaving the request to whatever timeout sits in front of management. +func TestValidateDomain_LookupTimeout(t *testing.T) { + env := setupClockDomainTest(t) + synctest.Test(t, func(t *testing.T) { + ctx := context.Background() + d, err := env.manager.CreateDomain(ctx, accountA, accountAUser, "slow.example.com", testCluster) + require.NoError(t, err) + env.manager.validator.Resolver = hangingResolver{} + + start := time.Now() + err = env.manager.ValidateDomain(ctx, accountA, accountAUser, d.ID) + sErr, ok := status.FromError(err) + require.True(t, ok, "expected a status error, got %v", err) + assert.Equal(t, status.PreconditionFailed, sErr.Type(), "status type") + assert.Equal(t, "DNS lookup for validation.slow.example.com failed; retry the validation", sErr.Message, "user-facing message") + assert.Equal(t, validationTimeout, time.Since(start), "the lookup must end on the validation timeout") + }) +} diff --git a/management/internals/modules/reverseproxy/domain/validator.go b/management/internals/modules/reverseproxy/domain/validator.go index 9c23c1192..e8d1ea3d0 100644 --- a/management/internals/modules/reverseproxy/domain/validator.go +++ b/management/internals/modules/reverseproxy/domain/validator.go @@ -2,6 +2,8 @@ package domain import ( "context" + "errors" + "fmt" "net" "strings" @@ -16,6 +18,33 @@ type Validator struct { Resolver resolver } +// ValidationReason classifies why a custom domain failed validation. +type ValidationReason string + +const ( + ValidationReasonCNAMENotFound ValidationReason = "cname_not_found" + ValidationReasonCNAMEMismatch ValidationReason = "cname_mismatch" + ValidationReasonLookupFailed ValidationReason = "lookup_failed" + ValidationReasonExpired ValidationReason = "validation_expired" + ValidationReasonNoTargetCluster ValidationReason = "no_target_cluster" +) + +// ValidationError is a validation failure whose message is safe to show to the user. +type ValidationError struct { + Reason ValidationReason + Message string +} + +// Error returns the user-facing message. +func (e *ValidationError) Error() string { + return e.Message +} + +// NewValidationError returns a ValidationError with a formatted message. +func NewValidationError(reason ValidationReason, format string, args ...any) *ValidationError { + return &ValidationError{Reason: reason, Message: fmt.Sprintf(format, args...)} +} + // NewValidator initializes a validator with a specific DNS Resolver. // If a Validator is used without specifying a Resolver, then it will // use the net.DefaultResolver. @@ -39,6 +68,13 @@ func (v *Validator) IsValid(ctx context.Context, domain string, accept []string) // ValidateWithCluster validates a custom domain and returns the matched cluster address. // Returns the cluster address and true if valid, or empty string and false if invalid. func (v *Validator) ValidateWithCluster(ctx context.Context, domain string, accept []string) (string, bool) { + cluster, err := v.Validate(ctx, domain, accept) + return cluster, err == nil +} + +// Validate validates a custom domain and returns the matched cluster address. +// On failure it returns a *ValidationError whose message is safe to show to the user. +func (v *Validator) Validate(ctx context.Context, domain string, accept []string) (string, error) { if v.Resolver == nil { v.Resolver = net.DefaultResolver } @@ -56,10 +92,19 @@ func (v *Validator) ValidateWithCluster(ctx context.Context, domain string, acce "domain": domain, "lookupDomain": lookupDomain, }).WithError(err).Warn("CNAME lookup failed for domain validation") - return "", false + // Never forward the resolver error: its text includes the resolver address. + var dnsErr *net.DNSError + if errors.As(err, &dnsErr) && dnsErr.IsNotFound { + return "", cnameNotFound(lookupDomain, accept) + } + return "", NewValidationError(ValidationReasonLookupFailed, "DNS lookup for %s failed; retry the validation", lookupDomain) } nakedCNAME := strings.TrimSuffix(cname, ".") + // A name without a CNAME record resolves to itself. + if strings.EqualFold(nakedCNAME, lookupDomain) { + return "", cnameNotFound(lookupDomain, accept) + } log.WithFields(log.Fields{ "domain": domain, "cname": cname, @@ -69,13 +114,13 @@ func (v *Validator) ValidateWithCluster(ctx context.Context, domain string, acce for _, acceptDomain := range accept { normalizedAccept := strings.TrimSuffix(acceptDomain, ".") - if nakedCNAME == normalizedAccept { + if strings.EqualFold(nakedCNAME, normalizedAccept) { log.WithFields(log.Fields{ "domain": domain, "cname": nakedCNAME, "cluster": acceptDomain, }).Info("domain CNAME matched cluster") - return acceptDomain, true + return acceptDomain, nil } } @@ -84,5 +129,11 @@ func (v *Validator) ValidateWithCluster(ctx context.Context, domain string, acce "cname": nakedCNAME, "acceptList": accept, }).Warn("domain CNAME does not match any accepted cluster") - return "", false + return "", NewValidationError(ValidationReasonCNAMEMismatch, "CNAME record %s points to %s; point it to %s", + lookupDomain, nakedCNAME, strings.Join(accept, ", ")) +} + +func cnameNotFound(lookupDomain string, accept []string) *ValidationError { + return NewValidationError(ValidationReasonCNAMENotFound, "no CNAME record found for %s; point it to %s", + lookupDomain, strings.Join(accept, ", ")) } diff --git a/management/internals/modules/reverseproxy/domain/validator_test.go b/management/internals/modules/reverseproxy/domain/validator_test.go index 1f9583728..8d47bca32 100644 --- a/management/internals/modules/reverseproxy/domain/validator_test.go +++ b/management/internals/modules/reverseproxy/domain/validator_test.go @@ -2,6 +2,8 @@ package domain_test import ( "context" + "errors" + "net" "testing" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain" @@ -36,6 +38,12 @@ func TestIsValid(t *testing.T) { accept: []string{"bar.example.com"}, expect: false, }, + "match in other case": { + resolver: resolver{"Bar.Example.COM."}, + domain: "foo.example.com", + accept: []string{"bar.example.com"}, + expect: true, + }, "accept trailing dot": { resolver: resolver{"bar.example.com."}, domain: "foo.example.com", @@ -54,3 +62,61 @@ func TestIsValid(t *testing.T) { }) } } + +type errResolver struct { + err error +} + +func (r errResolver) LookupCNAME(context.Context, string) (string, error) { + return "", r.err +} + +func TestValidate_Reason(t *testing.T) { + const notFound = "no CNAME record found for validation.foo.example.com; point it to eu.proxy.example.com" + tests := map[string]struct { + resolver interface { + LookupCNAME(context.Context, string) (string, error) + } + reason domain.ValidationReason + message string + }{ + "mismatch": { + resolver: resolver{"other.example.net."}, + reason: domain.ValidationReasonCNAMEMismatch, + message: "CNAME record validation.foo.example.com points to other.example.net; point it to eu.proxy.example.com", + }, + "no cname resolves to itself": { + resolver: resolver{"validation.foo.example.com."}, + reason: domain.ValidationReasonCNAMENotFound, + message: notFound, + }, + "no cname resolves to itself in other case": { + resolver: resolver{"Validation.Foo.example.com."}, + reason: domain.ValidationReasonCNAMENotFound, + message: notFound, + }, + "not found": { + resolver: errResolver{&net.DNSError{Err: "no such host", Name: "validation.foo.example.com", Server: "10.0.0.2:53", IsNotFound: true}}, + reason: domain.ValidationReasonCNAMENotFound, + message: notFound, + }, + "other error is not forwarded": { + resolver: errResolver{errors.New("dial udp 10.0.0.2:53: connect: network is unreachable")}, + reason: domain.ValidationReasonLookupFailed, + message: "DNS lookup for validation.foo.example.com failed; retry the validation", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + _, err := domain.NewValidator(test.resolver).Validate(t.Context(), "foo.example.com", []string{"eu.proxy.example.com"}) + var vErr *domain.ValidationError + if !errors.As(err, &vErr) { + t.Fatalf("expected a *domain.ValidationError, got %v", err) + } + if vErr.Reason != test.reason || vErr.Message != test.message { + t.Errorf("expect %q %q, actual %q %q", test.reason, test.message, vErr.Reason, vErr.Message) + } + }) + } +} diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index fbf8bf5cd..ca8c1339b 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -12895,7 +12895,7 @@ paths: /api/reverse-proxies/domains/{domainId}/validate: get: summary: Validate a custom domain - description: Trigger domain ownership validation for a custom domain + description: Validate domain ownership for a custom domain tags: [ Services ] security: - BearerAuth: [ ] @@ -12908,8 +12908,8 @@ paths: type: string description: The custom domain ID responses: - '202': - description: Reverse proxy custom domain validation triggered + '204': + description: Reverse proxy custom domain validated '400': "$ref": "#/components/responses/bad_request" '401': @@ -12918,6 +12918,9 @@ paths: "$ref": "#/components/responses/forbidden" '404': "$ref": "#/components/responses/not_found" + '412': + description: Precondition failed - the custom domain could not be validated + content: { } '500': "$ref": "#/components/responses/internal_error" /api/integrations/notifications/types: From f8f335720de0d7f5e4842fd24c42b78bfa042859 Mon Sep 17 00:00:00 2001 From: Riccardo Manfrin <3090891+riccardomanfrin@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:08:15 +0200 Subject: [PATCH 2/5] [client,relay] Bump quic-go to v0.64.0 (#8249) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 79dcd1867..bc8e5e287 100644 --- a/go.mod +++ b/go.mod @@ -105,7 +105,7 @@ require ( github.com/pkg/sftp v1.13.9 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 - github.com/quic-go/quic-go v0.62.0 + github.com/quic-go/quic-go v0.64.0 github.com/redis/go-redis/v9 v9.7.3 github.com/rs/xid v1.3.0 github.com/shirou/gopsutil/v4 v4.25.8 diff --git a/go.sum b/go.sum index 5b9cd5a11..9af59daf5 100644 --- a/go.sum +++ b/go.sum @@ -647,8 +647,8 @@ github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4 github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= -github.com/quic-go/quic-go v0.62.0 h1:ZHDjCk5OacATwGvs8PWE97CTvX7AqZiVoW7++ZOXTf8= -github.com/quic-go/quic-go v0.62.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w= +github.com/quic-go/quic-go v0.64.0 h1:dlUpiyp9+7UrbbuhLvV7CJ9F8uoZxXf/vi2h8tkuuoA= +github.com/quic-go/quic-go v0.64.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= From b1f4efb160071bb04052bfbb736ca0227ff151e3 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:16:59 +0900 Subject: [PATCH 3/5] [client] Fix errcheck findings from the latest golangci-lint (#8251) --- client/iface/iface_close_test.go | 24 ++++++++++++++++-------- client/ssh/client/client.go | 6 ++++-- management/internals/server/server.go | 6 +++++- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/client/iface/iface_close_test.go b/client/iface/iface_close_test.go index ea3115ec0..ecbbf4866 100644 --- a/client/iface/iface_close_test.go +++ b/client/iface/iface_close_test.go @@ -12,6 +12,7 @@ import ( "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/device" + nbnetstack "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/iface/wgproxy" @@ -34,14 +35,14 @@ func (f *fakeTunDevice) Create() (device.WGConfigurer, error) { func (f *fakeTunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) { return nil, errors.New("not implemented") } -func (f *fakeTunDevice) UpdateAddr(wgaddr.Address) error { return nil } -func (f *fakeTunDevice) WgAddress() wgaddr.Address { return wgaddr.Address{} } -func (f *fakeTunDevice) MTU() uint16 { return DefaultMTU } -func (f *fakeTunDevice) DeviceName() string { return "nb-close-test" } +func (f *fakeTunDevice) UpdateAddr(wgaddr.Address) error { return nil } +func (f *fakeTunDevice) WgAddress() wgaddr.Address { return wgaddr.Address{} } +func (f *fakeTunDevice) MTU() uint16 { return DefaultMTU } +func (f *fakeTunDevice) DeviceName() string { return "nb-close-test" } func (f *fakeTunDevice) FilteredDevice() *device.FilteredDevice { return nil } -func (f *fakeTunDevice) Device() *wgdevice.Device { return nil } -func (f *fakeTunDevice) GetNet() *netstack.Net { return nil } -func (f *fakeTunDevice) GetICEBind() device.EndpointManager { return nil } +func (f *fakeTunDevice) Device() *wgdevice.Device { return nil } +func (f *fakeTunDevice) GetNet() *netstack.Net { return nil } +func (f *fakeTunDevice) GetICEBind() device.EndpointManager { return nil } func (f *fakeTunDevice) Close() error { close(f.closeStarted) @@ -62,6 +63,10 @@ func (fakeProxyFactory) Free() error { return nil } // WGIface.GetDevice() which needs the same mutex. The fix is to drop // the lock before tun.Close() returns control. func TestWGIface_CloseReleasesMutexBeforeTunClose(t *testing.T) { + // Netstack mode skips the platform interface cleanup in Close, so only + // the mutex and tun close behavior is exercised. + t.Setenv(nbnetstack.EnvUseNetstackMode, "true") + tun := &fakeTunDevice{ closeStarted: make(chan struct{}), unblockClose: make(chan struct{}), @@ -105,7 +110,10 @@ func TestWGIface_CloseReleasesMutexBeforeTunClose(t *testing.T) { close(tun.unblockClose) select { - case <-closeDone: + case err := <-closeDone: + if err != nil { + t.Errorf("WGIface.Close() returned an error: %v", err) + } case <-time.After(2 * time.Second): t.Fatal("WGIface.Close() never returned after the tun was unblocked") } diff --git a/client/ssh/client/client.go b/client/ssh/client/client.go index 31143a4f4..dc94c33f4 100644 --- a/client/ssh/client/client.go +++ b/client/ssh/client/client.go @@ -166,7 +166,8 @@ func (c *Client) ExecuteCommandWithIO(ctx context.Context, command string) error case <-ctx.Done(): _ = session.Signal(ssh.SIGTERM) select { - case <-done: + case err := <-done: + log.Debugf("session ended after SIGTERM: %v", err) return ctx.Err() case <-time.After(100 * time.Millisecond): return ctx.Err() @@ -205,7 +206,8 @@ func (c *Client) ExecuteCommandWithPTY(ctx context.Context, command string) erro case <-ctx.Done(): _ = session.Signal(ssh.SIGTERM) select { - case <-done: + case err := <-done: + log.Debugf("session ended after SIGTERM: %v", err) return ctx.Err() case <-time.After(100 * time.Millisecond): return ctx.Err() diff --git a/management/internals/server/server.go b/management/internals/server/server.go index bbf0f389d..c95092d70 100644 --- a/management/internals/server/server.go +++ b/management/internals/server/server.go @@ -335,7 +335,11 @@ func (s *BaseServer) Stop() error { } select { - case <-s.Errors(): + case err := <-s.Errors(): + if err != nil { + log.WithContext(ctx).Infof("stopped Management Service: %v", err) + return nil + } log.WithContext(ctx).Infof("stopped Management Service") return nil case <-ctx.Done(): From 6e1065882a453a199a7c3ed7831fe6b6011b9b80 Mon Sep 17 00:00:00 2001 From: DevBot NetBird <91386968+netbirddev@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:24:07 +0200 Subject: [PATCH 4/5] New Crowdin updates (#8101) * New translations common.json (French) [ci skip] * New translations common.json (Spanish) [ci skip] * New translations common.json (German) [ci skip] * New translations common.json (Hebrew) [ci skip] * New translations common.json (Hungarian) [ci skip] * New translations common.json (Italian) [ci skip] * New translations common.json (Japanese) [ci skip] * New translations common.json (Russian) [ci skip] * New translations common.json (Ukrainian) [ci skip] * New translations common.json (Chinese Simplified) [ci skip] * New translations common.json (Portuguese, Brazilian) [ci skip] * New translations common.json (Galician) [ci skip] * Update client/ui/i18n/locales/he/common.json Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * New translations common.json (French) [ci skip] * New translations common.json (Spanish) [ci skip] * New translations common.json (German) [ci skip] * New translations common.json (Hebrew) [ci skip] * New translations common.json (Hungarian) [ci skip] * New translations common.json (Italian) [ci skip] * New translations common.json (Japanese) [ci skip] * New translations common.json (Russian) [ci skip] * New translations common.json (Ukrainian) [ci skip] * New translations common.json (Chinese Simplified) [ci skip] * New translations common.json (Portuguese, Brazilian) [ci skip] * New translations common.json (Galician) [ci skip] --------- Co-authored-by: Edward <43848523+thomashacker@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- client/ui/i18n/locales/de/common.json | 6 +- client/ui/i18n/locales/es/common.json | 6 +- client/ui/i18n/locales/fr/common.json | 6 +- client/ui/i18n/locales/he/common.json | 1397 ++++++++++++++++++++++ client/ui/i18n/locales/hu/common.json | 6 +- client/ui/i18n/locales/it/common.json | 6 +- client/ui/i18n/locales/ja/common.json | 6 +- client/ui/i18n/locales/pt/common.json | 6 +- client/ui/i18n/locales/ru/common.json | 6 +- client/ui/i18n/locales/zh-CN/common.json | 6 +- 10 files changed, 1415 insertions(+), 36 deletions(-) create mode 100644 client/ui/i18n/locales/he/common.json diff --git a/client/ui/i18n/locales/de/common.json b/client/ui/i18n/locales/de/common.json index dcce2f908..105c8c0b3 100644 --- a/client/ui/i18n/locales/de/common.json +++ b/client/ui/i18n/locales/de/common.json @@ -552,12 +552,10 @@ "message": "Ändern des Autostarts fehlgeschlagen" }, "settings.general.keepConnectedOnQuit.label": { - "message": "Nach dem Beenden verbunden bleiben", - "description": "Toggle label: keep the VPN connection up after quitting the UI." + "message": "Nach dem Beenden verbunden bleiben" }, "settings.general.keepConnectedOnQuit.help": { - "message": "Die Verbindung bleibt im Hintergrund bestehen, nachdem Sie NetBird schließen. Sie endet erst, wenn Sie sie selbst trennen.", - "description": "Helper text for the stay-connected-after-quitting toggle." + "message": "Die Verbindung bleibt im Hintergrund bestehen, nachdem Sie NetBird schließen. Sie endet erst, wenn Sie sie selbst trennen." }, "settings.general.language.label": { "message": "Anzeigesprache" diff --git a/client/ui/i18n/locales/es/common.json b/client/ui/i18n/locales/es/common.json index 979879680..f786592a7 100644 --- a/client/ui/i18n/locales/es/common.json +++ b/client/ui/i18n/locales/es/common.json @@ -552,12 +552,10 @@ "message": "Error al cambiar el inicio automático" }, "settings.general.keepConnectedOnQuit.label": { - "message": "Permanecer conectado al salir", - "description": "Toggle label: keep the VPN connection up after quitting the UI." + "message": "Permanecer conectado al salir" }, "settings.general.keepConnectedOnQuit.help": { - "message": "La conexión sigue activa en segundo plano después de cerrar NetBird. Solo se detiene cuando la desconectas tú.", - "description": "Helper text for the stay-connected-after-quitting toggle." + "message": "La conexión sigue activa en segundo plano después de cerrar NetBird. Solo se detiene cuando la desconectas tú." }, "settings.general.language.label": { "message": "Idioma de la interfaz" diff --git a/client/ui/i18n/locales/fr/common.json b/client/ui/i18n/locales/fr/common.json index f9961d864..87e833e3f 100644 --- a/client/ui/i18n/locales/fr/common.json +++ b/client/ui/i18n/locales/fr/common.json @@ -552,12 +552,10 @@ "message": "Échec de la modification du démarrage automatique" }, "settings.general.keepConnectedOnQuit.label": { - "message": "Rester connecté après la fermeture", - "description": "Toggle label: keep the VPN connection up after quitting the UI." + "message": "Rester connecté après la fermeture" }, "settings.general.keepConnectedOnQuit.help": { - "message": "La connexion reste active en arrière-plan après la fermeture de NetBird. Elle ne s'arrête que si vous la coupez vous-même.", - "description": "Helper text for the stay-connected-after-quitting toggle." + "message": "La connexion reste active en arrière-plan après la fermeture de NetBird. Elle ne s'arrête que si vous la coupez vous-même." }, "settings.general.language.label": { "message": "Langue d’affichage" diff --git a/client/ui/i18n/locales/he/common.json b/client/ui/i18n/locales/he/common.json new file mode 100644 index 000000000..640f6b3da --- /dev/null +++ b/client/ui/i18n/locales/he/common.json @@ -0,0 +1,1397 @@ +{ + "tray.tooltip": { + "message": "NetBird" + }, + "tray.status.disconnected": { + "message": "מנותק" + }, + "tray.status.daemonUnavailable": { + "message": "לא פעיל" + }, + "tray.status.error": { + "message": "שגיאה" + }, + "tray.status.connected": { + "message": "מחובר" + }, + "tray.status.connecting": { + "message": "מתחבר" + }, + "tray.status.needsLogin": { + "message": "צריך להיכנס" + }, + "tray.status.loginFailed": { + "message": "הכניסה נכשלה" + }, + "tray.status.sessionExpired": { + "message": "תוקף ההפעלה פג" + }, + "tray.session.expiresIn": { + "message": "תוקף ההפעלה יפוג בעוד {remaining}" + }, + "tray.session.unit.lessThanMinute": { + "message": "פחות מדקה" + }, + "tray.session.unit.minute": { + "message": "דקה" + }, + "tray.session.unit.minutes": { + "message": "{count} דקות" + }, + "tray.session.unit.hour": { + "message": "שעה" + }, + "tray.session.unit.hours": { + "message": "{count} שעות" + }, + "tray.session.unit.day": { + "message": "יום" + }, + "tray.session.unit.days": { + "message": "{count} ימים" + }, + "tray.menu.open": { + "message": "פתיחת NetBird" + }, + "tray.menu.connect": { + "message": "התחברות" + }, + "tray.menu.disconnect": { + "message": "התנתקות" + }, + "tray.menu.exitNode": { + "message": "נקודת יציאה" + }, + "tray.menu.networks": { + "message": "משאבים" + }, + "tray.menu.profiles": { + "message": "פרופילים" + }, + "tray.menu.manageProfiles": { + "message": "ניהול פרופילים" + }, + "tray.menu.settings": { + "message": "הגדרות…" + }, + "tray.menu.debugBundle": { + "message": "יצירת מאגד ניפוי שגיאות" + }, + "tray.menu.about": { + "message": "עזרה ותמיכה" + }, + "tray.menu.github": { + "message": "GitHub" + }, + "tray.menu.documentation": { + "message": "תיעוד" + }, + "tray.menu.troubleshoot": { + "message": "פתרון בעיות" + }, + "tray.menu.downloadLatest": { + "message": "הורדת הגרסה העדכנית ביותר" + }, + "tray.menu.installVersion": { + "message": "התקנת גרסה {version}" + }, + "tray.menu.guiVersion": { + "message": "ממשק משתמש: {version}" + }, + "tray.menu.daemonVersion": { + "message": "סוכן: {version}" + }, + "tray.menu.versionUnknown": { + "message": "—" + }, + "tray.menu.quit": { + "message": "יציאה מ־NetBird" + }, + "notify.daemonOutdated.title": { + "message": "שירות NetBird אינו עדכני" + }, + "notify.daemonOutdated.body": { + "message": "יש לעדכן את שירות NetBird כדי להשתמש ביישום הזה." + }, + "notify.update.title": { + "message": "יש עדכון ל־NetBird" + }, + "notify.update.body": { + "message": "יצא NetBird {version}." + }, + "notify.update.enforcedSuffix": { + "message": " הנהלת המערכת דורשת ממך לעדכן לגרסה הזאת." + }, + "notify.error.title": { + "message": "שגיאה" + }, + "notify.error.connect": { + "message": "החיבור נכשל" + }, + "notify.error.disconnect": { + "message": "הניתוק נכשל" + }, + "notify.error.switchProfile": { + "message": "המעבר לפרופיל {profile} נכשל" + }, + "notify.error.exitNode": { + "message": "עדכון נקודת היציאה {name} נכשל" + }, + "notify.sessionExpired.title": { + "message": "תוקף הפעלת NetBird פג" + }, + "notify.sessionExpired.body": { + "message": "תוקף הפעלת ה־NetBird שלך פג. נא להיכנס למערכת שוב." + }, + "notify.sessionWarning.title": { + "message": "תוקף ההפעלה יפוג בקרוב" + }, + "notify.sessionWarning.body": { + "message": "הפעלת ה־NetBird שלך תפוג בעוד {remaining}. לחיצה על להאריך כעת תחדש אותה." + }, + "notify.sessionWarning.bodyGeneric": { + "message": "תוקף הפעלת ה־NetBird שלך עומד לפוג. לחיצה על להאריך כעת תחדש אותו." + }, + "notify.sessionWarning.extend": { + "message": "להאריך כעת" + }, + "notify.sessionWarning.dismiss": { + "message": "התעלמות" + }, + "notify.sessionWarning.failed": { + "message": "הארכת ההפעלה של NetBird נכשלה" + }, + "notify.sessionWarning.successTitle": { + "message": "ההפעלה של NetBird הוארכה" + }, + "notify.sessionWarning.successBody": { + "message": "ההפעלה שלך התרעננה." + }, + "notify.sessionDeadlineRejected.title": { + "message": "מועד הסיום של ההפעלה נדחה" + }, + "notify.sessionDeadlineRejected.body": { + "message": "השרת שלח מועד סיום שגוי להפעלה. נא להיכנס שוב למערכת." + }, + "notify.mdm.policyApplied.title": { + "message": "ההגדרות של NetBird עודכנו" + }, + "notify.mdm.policyApplied.body": { + "message": "הגדרות ה־NetBird שלך עודכנו על ידי מדיניות צוות המחשוב שלך." + }, + "common.cancel": { + "message": "ביטול" + }, + "common.save": { + "message": "שמירה" + }, + "common.saveChanges": { + "message": "שמירת השינויים" + }, + "common.saving": { + "message": "שמירה…" + }, + "common.close": { + "message": "סגירה" + }, + "common.copy": { + "message": "העתקה" + }, + "common.togglePasswordVisibility": { + "message": "הסתרת/חשיפת סיסמה" + }, + "common.increase": { + "message": "הגדלה" + }, + "common.decrease": { + "message": "הקטנה" + }, + "common.delete": { + "message": "מחיקה" + }, + "common.create": { + "message": "יצירה" + }, + "common.add": { + "message": "הוספה" + }, + "common.remove": { + "message": "הסרה" + }, + "common.refresh": { + "message": "ריענון" + }, + "common.loading": { + "message": "בטעינה…" + }, + "common.netbird": { + "message": "NetBird" + }, + "common.noResults.title": { + "message": "לא ניתן למצוא תוצאות כלשהן." + }, + "common.noResults.description": { + "message": "לא הצלחנו למצוא תוצאות כלשהן. נא לנסות ביטוי חיפוש אחר או לשנות את המסננים שלך." + }, + "notConnected.title": { + "message": "מנותק" + }, + "notConnected.description": { + "message": "יש להתחבר ל־NetBird תחילה כדי לצפות במידע מפורט על העמיתים, משאבי הרשת ונקודות היציאה שלך." + }, + "connect.status.disconnected": { + "message": "מנותק" + }, + "connect.status.connecting": { + "message": "מתחבר…" + }, + "connect.status.connected": { + "message": "מחובר" + }, + "connect.status.disconnecting": { + "message": "מתנתק…" + }, + "connect.status.daemonUnavailable": { + "message": "הסוכן לא זמין" + }, + "connect.status.loginRequired": { + "message": "נדרשת כניסה" + }, + "connect.error.loginTitle": { + "message": "הכניסה נכשלה" + }, + "connect.error.connectTitle": { + "message": "התחברות נכשלה" + }, + "connect.error.disconnectTitle": { + "message": "התנתקות נכשלה" + }, + "nav.peers.title": { + "message": "עמיתים" + }, + "nav.peers.description": { + "message": "{connected} מתוך {total} מחוברים" + }, + "nav.resources.title": { + "message": "משאבים" + }, + "nav.resources.description": { + "message": "{active} מתוך {total} פעילים" + }, + "nav.exitNode.title": { + "message": "נקודות יציאה" + }, + "nav.exitNode.none": { + "message": "לא פעיל" + }, + "nav.exitNode.using": { + "message": "דרך {name}" + }, + "header.openSettings": { + "message": "פתיחת הגדרות" + }, + "header.togglePanel": { + "message": "הצגת/הסתרת לוח צד" + }, + "profile.selector.loading": { + "message": "בטעינה…" + }, + "profile.selector.noProfile": { + "message": "אין פרופיל" + }, + "profile.selector.searchPlaceholder": { + "message": "חיפוש פרופיל לפי שם…" + }, + "profile.selector.emptyTitle": { + "message": "לא נמצאו פרופילים" + }, + "profile.selector.emptyDescription": { + "message": "כדאי לנסות ביטוי אחר לחיפוש או ליצור פרופיל חדש." + }, + "profile.selector.newProfile": { + "message": "פרופיל חדש" + }, + "profile.selector.moreOptions": { + "message": "אפשרויות נוספות" + }, + "profile.selector.deregister": { + "message": "ביטול רישום" + }, + "profile.selector.delete": { + "message": "מחיקה" + }, + "profile.selector.switchTo": { + "message": "החלפה לפרופיל הזה" + }, + "profile.selector.edit": { + "message": "עריכה" + }, + "profile.edit.title": { + "message": "עריכת פרופיל" + }, + "profile.edit.submit": { + "message": "שמירת השינויים" + }, + "profile.dialog.title": { + "message": "נא למלא שם פרופיל" + }, + "profile.dialog.nameLabel": { + "message": "שם פרופיל" + }, + "profile.dialog.description": { + "message": "הגדרת שם שניתן לזהות בקלות לפרופיל שלך." + }, + "profile.dialog.placeholder": { + "message": "למשל: עבודה" + }, + "profile.dialog.submit": { + "message": "הוספת פרופיל" + }, + "profile.dialog.required": { + "message": "נא למלא שם לפרופיל, למשל: עבודה, בית" + }, + "profile.dialog.managementHelp": { + "message": "אפשר להשתמש בענן של NetBird או בשרת משלך." + }, + "profile.dialog.urlUnreachable": { + "message": "לא ניתן להגיע לשרת. נא לבדוק שהכתובת נכונה או להוסיף את הפרופיל בכל זאת אם ברור לך שהיא נכונה." + }, + "header.menu.settings": { + "message": "הגדרות…" + }, + "header.menu.defaultView": { + "message": "תצוגת ברירת מחדל" + }, + "header.menu.advancedView": { + "message": "תצוגה מתקדמת" + }, + "header.menu.updateAvailable": { + "message": "יש עדכון" + }, + "header.menu.open": { + "message": "פתיחת התפריט" + }, + "header.profile.switch": { + "message": "החלפת פרופיל" + }, + "connect.toggle.label": { + "message": "החלפת מצב חיבור NetBird" + }, + "connect.localIp.label": { + "message": "כתובות IP מקומיות" + }, + "common.search": { + "message": "חיפוש" + }, + "common.filter": { + "message": "סינון" + }, + "exitNodes.dropdown.trigger": { + "message": "בחירת נקודת יציאה" + }, + "peers.row.label": { + "message": "פתיחת פרטים עבור {name}, {status}" + }, + "peers.dialog.title": { + "message": "פרטי עמית" + }, + "networks.row.toggle": { + "message": "להחליף את המצב של {name}" + }, + "networks.bulk.label": { + "message": "להחליף את המצב של כל המשאבים הגלויים" + }, + "profile.switch.title": { + "message": "להחליף את הפרופיל כך שיהיה „{name}”?" + }, + "profile.switch.message": { + "message": "להחליף פרופילים?\nהפרופיל הנוכחי שלך יתנתק." + }, + "profile.switch.confirm": { + "message": "אישור" + }, + "profile.deregister.title": { + "message": "לבטל את רישום הפרופיל „{name}”?" + }, + "profile.deregister.message": { + "message": "לבטל את רישום הפרופיל הזה?\nצריך להיכנס שוב כדי להשתמש בו." + }, + "profile.deregister.confirm": { + "message": "לבטל רישום" + }, + "profile.delete.title": { + "message": "למחוק את הפרופיל „{name}”?" + }, + "profile.delete.message": { + "message": "למחוק את הפרופיל הזה?\nזאת פעולה בלתי הפיכה." + }, + "profile.delete.disabledActive": { + "message": "לא ניתן למחוק פרופילים פעילים. יש להחליף לפרופיל אחר בטרם מחיקת הפרופיל הזה." + }, + "profile.delete.disabledDefault": { + "message": "לא ניתן למחוק את פרופיל ברירת המחדל." + }, + "profile.error.switchTitle": { + "message": "החלפת פרופיל נכשלה" + }, + "profile.error.deregisterTitle": { + "message": "ביטול רישום פרופיל נכשל" + }, + "profile.error.deleteTitle": { + "message": "מחיקת פרופיל נכשלה" + }, + "profile.error.createTitle": { + "message": "יצירת פרופיל נכשלה" + }, + "profile.error.editTitle": { + "message": "עריכת פרופיל נכשלה" + }, + "profile.error.loadTitle": { + "message": "טעינת הפרופילים נכשלה" + }, + "profile.dropdown.activeProfile": { + "message": "הפרופיל הפעיל" + }, + "profile.dropdown.switchProfile": { + "message": "החלפת פרופיל" + }, + "profile.dropdown.noEmail": { + "message": "אחר" + }, + "profile.dropdown.addProfile": { + "message": "הוספת פרופיל" + }, + "profile.dropdown.manageProfiles": { + "message": "ניהול פרופילים" + }, + "profile.dropdown.settings": { + "message": "הגדרות" + }, + "settings.profiles.section.profiles": { + "message": "פרופילים" + }, + "settings.profiles.intro": { + "message": "ניתן להחזיק את זהויות ה־NetBird הנפרדות זו לצד זו, למשל חשבון עבודה וחשבון פרטי או שרתי ניהול שונים. אפשר להוסיף, לבטל רישום או למחוק פרופילים להלן." + }, + "settings.profiles.addProfile": { + "message": "הוספת פרופיל" + }, + "settings.profiles.active": { + "message": "פעיל" + }, + "settings.profiles.emptyTitle": { + "message": "אין פרופילים" + }, + "settings.profiles.emptyDescription": { + "message": "יש ליצור פרופיל כדי להתחבר לשרת ניהול של NetBird." + }, + "settings.error.loadTitle": { + "message": "טעינת ההגדרות נכשלה" + }, + "settings.error.saveTitle": { + "message": "שמירת ההגדרות נכשלה" + }, + "settings.error.debugBundleTitle": { + "message": "מאגד ניפוי השגיאות נכשל" + }, + "settings.nav.label": { + "message": "סעיפי הגדרות" + }, + "settings.tabs.general": { + "message": "כללי" + }, + "settings.tabs.network": { + "message": "רשת" + }, + "settings.tabs.security": { + "message": "אבטחה" + }, + "settings.tabs.profiles": { + "message": "פרופילים" + }, + "settings.tabs.ssh": { + "message": "SSH" + }, + "settings.tabs.advanced": { + "message": "מתקדם" + }, + "settings.tabs.troubleshooting": { + "message": "פתרון בעיות" + }, + "settings.tabs.about": { + "message": "על אודות" + }, + "settings.tabs.updateAvailable": { + "message": "יש עדכון" + }, + "settings.general.section.general": { + "message": "כללי" + }, + "settings.general.section.connection": { + "message": "חיבור" + }, + "settings.general.connectOnStartup.label": { + "message": "להתחבר עם העלייה" + }, + "settings.general.connectOnStartup.help": { + "message": "להקים חיבור אוטומטית כשהשירות מופעל." + }, + "settings.general.notifications.label": { + "message": "התראות שולחן עבודה" + }, + "settings.general.notifications.help": { + "message": "להציג התראות שולחן עבודה על עדכונים חדשים ואירועי חיבור." + }, + "settings.general.autostart.label": { + "message": "להפעיל את ממשק המשתמש של NetBird עם הכניסה למערכת" + }, + "settings.general.autostart.help": { + "message": "להפעיל את הממשק של NetBird אוטומטית עם הכניסה. הפעולה הזאת משפיעה על הממשק הגרפי בלבד, לא על שירות הרקע." + }, + "settings.general.autostart.errorTitle": { + "message": "שינוי בהפעלה האוטומטית נכשל" + }, + "settings.general.keepConnectedOnQuit.label": { + "message": "להשאיר את החיבור לאחר היציאה" + }, + "settings.general.keepConnectedOnQuit.help": { + "message": "החיבור נשאר פעיל ברקע לאחר סגירת NetBird. הוא נקטע רק כשבחרת לנתק אותו בעצמך." + }, + "settings.general.language.label": { + "message": "שפת התצוגה" + }, + "settings.general.language.help": { + "message": "בחירת שפת הממשק של NetBird." + }, + "settings.general.language.search": { + "message": "חיפוש שפה…" + }, + "settings.general.language.empty": { + "message": "אין שפות תואמות." + }, + "settings.general.theme.label": { + "message": "ערכת עיצוב" + }, + "settings.general.theme.help": { + "message": "נא לבחור בהירה או כהה או להתאים למערכת." + }, + "settings.general.theme.system": { + "message": "מערכת" + }, + "settings.general.theme.light": { + "message": "בהירה" + }, + "settings.general.theme.dark": { + "message": "כהה" + }, + "settings.general.management.label": { + "message": "שרת ניהול" + }, + "settings.general.management.help": { + "message": "אפשר להתחבר לענן של NetBird או לשרת ניהול באירוח שלך. שינויים יחברו את הלקוח מחדש." + }, + "settings.general.management.cloud": { + "message": "ענן" + }, + "settings.general.management.selfHosted": { + "message": "אירוח עצמי" + }, + "settings.general.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlError": { + "message": "נא למלא כתובת תקפה, למשל, https://netbird.selfhosted.com:443" + }, + "settings.general.management.urlUnreachable": { + "message": "לא ניתן להגיע לשרת הזה. נא לבדוק את הכתובת או לשמור בכל זאת אם ברור לך שהיא נכונה." + }, + "settings.general.management.switchCloudTitle": { + "message": "לעבור לענן NetBird?" + }, + "settings.general.management.switchCloudMessage": { + "message": "מנתק אותך מהשרת שלך באירוח עצמי.\nיתכן שיהיה עליך להיכנס למערכת שוב." + }, + "settings.general.management.switchCloudConfirm": { + "message": "מעבר לענן" + }, + "settings.network.section.connectivity": { + "message": "קישוריות" + }, + "settings.network.section.routingDns": { + "message": "ניתוב ו־DNS" + }, + "settings.network.monitor.label": { + "message": "להתחבר מחדש כשחל שינוי ברשת" + }, + "settings.network.monitor.help": { + "message": "מעקב אחר הרשת כדי להתחבר מחדש עקב שינויים כגון מעבר רשת אלחוטית, שינויים בחיבור הקווי או התעוררות משינה." + }, + "settings.network.dns.label": { + "message": "הפעלת DNS" + }, + "settings.network.dns.help": { + "message": "החלת הגדרות DNS בניהול NetBird לפותר שמות המארחים." + }, + "settings.network.clientRoutes.label": { + "message": "הפעלת ניתובי לקוח" + }, + "settings.network.clientRoutes.help": { + "message": "קבלת ניתובים מעמיתים אחרים כדי להגיע לרשתות שלהם." + }, + "settings.network.serverRoutes.label": { + "message": "הפעלת ניתובי שרת" + }, + "settings.network.serverRoutes.help": { + "message": "לפרסם את הניתובים המקומיים של המארח הזה לעמיתים אחרים." + }, + "settings.network.ipv6.label": { + "message": "הפעלת IPv6" + }, + "settings.network.ipv6.help": { + "message": "להשתמש בחלוקת כתובות IPv6 לרשת שכבת העל של NetBird." + }, + "settings.security.section.firewall": { + "message": "חומת אש" + }, + "settings.security.section.encryption": { + "message": "הצפנה" + }, + "settings.security.blockInbound.label": { + "message": "חסימת תעבורה נכנסת" + }, + "settings.security.blockInbound.help": { + "message": "לדחות חיבורים שלא התבקשו מעמיתים למכשיר הזה ולרשתות כלשהן שהוא מנתב אליהן. אין השפעה על התקשורת היוצאת." + }, + "settings.security.blockLan.label": { + "message": "חסימת גישה לרשת המקומית" + }, + "settings.security.blockLan.help": { + "message": "למנוע מעמיתים להגיע לרשת המקומית שלך או המכשירים שבה כאשר המכשיר הזה מנתב את התעבורה שלהם." + }, + "settings.security.rosenpass.label": { + "message": "הפעלת עמידות לפיענוח צפנים קוונטי" + }, + "settings.security.rosenpass.help": { + "message": "הוספת החלפת מפתחות עמידה לפיענוח צפנים קוונטי דרך Rosenpass על גבי WireGuard®." + }, + "settings.security.rosenpassPermissive.label": { + "message": "הפעלת מצב מתירני" + }, + "settings.security.rosenpassPermissive.help": { + "message": "לאפשר חיבורים לעמיתים ללא תמיכה בעמידות לפיענוח צפנים קוונטי." + }, + "settings.ssh.section.server": { + "message": "שרת" + }, + "settings.ssh.section.capabilities": { + "message": "יכולות" + }, + "settings.ssh.section.authentication": { + "message": "אימות" + }, + "settings.ssh.server.label": { + "message": "הפעלת שרת SSH" + }, + "settings.ssh.server.help": { + "message": "להריץ את שרת ה־SSH של NetBird על המארח הזה כדי שעמיתים אחרים יוכלו להתחבר אליו." + }, + "settings.ssh.root.label": { + "message": "לאפשר כניסת ניהול (root)" + }, + "settings.ssh.root.help": { + "message": "לאפשר לעמיתים להיכנס בתור משתמש הניהול. אפשר להשבית כדי לדרוש שזה יהיה חשבון ללא הרשאות מופלגות." + }, + "settings.ssh.sftp.label": { + "message": "לאפשר SFTP" + }, + "settings.ssh.sftp.help": { + "message": "העברת קבצים בבטחה עם לקוחות מובנים של SFTP או SCP." + }, + "settings.ssh.localForward.label": { + "message": "העברת פתחה מקומית" + }, + "settings.ssh.localForward.help": { + "message": "לאפשר לעמיתים שמתחברים לתעל פתחות מקומיות לשירותים שנגישים למארח הזה." + }, + "settings.ssh.remoteForward.label": { + "message": "העברת פתחה מרוחקת" + }, + "settings.ssh.remoteForward.help": { + "message": "לאפשר לעמיתים שמתחברים לחשוף בפני המארח הזה פתחות שחוזרות למכונות שלהם." + }, + "settings.ssh.jwt.label": { + "message": "הפעלת אימות JWT" + }, + "settings.ssh.jwt.help": { + "message": "לאמת כל הפעלת SSH מול ספק הזהות (IdP) שלך לזהות המשתמש ולפיקוח. יש להשבית כדי לסמוך רק על מדיניויות ה־ACL (רשימת בקרת גישה) של הרשתות בלבד, שימושי כשאין ספק זהות (IdP) זמין." + }, + "settings.ssh.jwtTtl.label": { + "message": "זמן חיי JWT במטמון (TTL)" + }, + "settings.ssh.jwtTtl.help": { + "message": "כמה זמן הלקוח הזה שומר JWT במטמון בטרם הצגת בקשה חוזרת לחיבורי SSH יוצאים. יש להגדיר ל־0 כדי להשבית את המטמון ולעבור אימות עם כל חיבור מחדש." + }, + "settings.ssh.jwtTtl.suffix": { + "message": "שנייה/ות" + }, + "settings.advanced.section.interface": { + "message": "ממשק" + }, + "settings.advanced.section.security": { + "message": "אבטחה" + }, + "settings.advanced.interfaceName.label": { + "message": "שם" + }, + "settings.advanced.interfaceName.error": { + "message": "יש להשתמש ב־1-15 אותיות, ספרות, נקודות, מינוסים או קווים תחתיים." + }, + "settings.advanced.interfaceName.errorMac": { + "message": "חייב להתחיל ב־„utun” ולאחריו מספר (למשל: utun100)." + }, + "settings.advanced.port.label": { + "message": "פתחה" + }, + "settings.advanced.port.error": { + "message": "נא למלא פתחה בין {min} ל־{max}." + }, + "settings.advanced.port.help": { + "message": "אם מוגדר ל־0, ייעשה שימוש בפתחה פנויה אקראית." + }, + "settings.advanced.mtu.label": { + "message": "MTU" + }, + "settings.advanced.mtu.error": { + "message": "נא למלא ערך MTU בין {min} לבין {max}." + }, + "settings.advanced.psk.label": { + "message": "מפתח ששותף מראש" + }, + "settings.advanced.psk.help": { + "message": "מפתח ששותף מראש (PSK) של WireGuard כרשות להצפנה סימטרית בנוסף. לא זהה למפתח התקנה של NetBird. תהיה לך תקשורת רק מול עמיתים שמשתמשים באותו המפתח ששותף מראש." + }, + "settings.troubleshooting.section.title": { + "message": "מאגד ניפוי שגיאות" + }, + "settings.troubleshooting.anonymize.label": { + "message": "הסוואת מידע רגיש עם מידע אלמוני" + }, + "settings.troubleshooting.anonymize.help": { + "message": "הסתרת כתובות IP ציבוריות ושמות תחום שאינם של NetBird מהיומנים." + }, + "settings.troubleshooting.anonymize.info": { + "message": "ברירת המחדל משאירה את כתובות ה־IPv4 הפנימיות ואת שמות העמיתים קריאים למטרות תמיכה. מצב מחמיר מסתיר באלמוניות כתובות פרטיות (RFC 1918), CGNAT ו־link-local (קישור מקומי), שמות עמיתים ומפתחות ציבוריים של WireGuard. ערכים חוזרים ממופים לאותו ממלא המקום, כך שהעמיתים נשארים בלתי ניתנים להבדלה. כדאי להשתמש במחמיר בעת שיתוף המאגד מחוץ לארגון שלך." + }, + "settings.troubleshooting.anonymize.none": { + "message": "ללא" + }, + "settings.troubleshooting.anonymize.default": { + "message": "ברירת מחדל" + }, + "settings.troubleshooting.anonymize.strict": { + "message": "מחמיר" + }, + "settings.troubleshooting.systemInfo.label": { + "message": "לכלול את פרטי המערכת" + }, + "settings.troubleshooting.systemInfo.help": { + "message": "לכלול את מערכת ההפעלה, הליבה, ממשקי הרשת וטבלאות הניתוב." + }, + "settings.troubleshooting.upload.label": { + "message": "להעלות את המאגד לשרתים של NetBird" + }, + "settings.troubleshooting.upload.help": { + "message": "החזרת מפתח שליחה לשיתוף עם התמיכה של NetBird." + }, + "settings.troubleshooting.trace.label": { + "message": "הפעלת יומני מעקב" + }, + "settings.troubleshooting.trace.help": { + "message": "הגברת רמת התיעוד ביומן ל־TRACE (מעקב) ומשחזרת אותה לאחר מכן." + }, + "settings.troubleshooting.capture.label": { + "message": "הפעלת לכידה" + }, + "settings.troubleshooting.capture.help": { + "message": "חיבור מחדש והמתנה כדי לאפשר ליצור את התקלה מחדש." + }, + "settings.troubleshooting.packets.label": { + "message": "לכידת מנות נתונים מהרשת" + }, + "settings.troubleshooting.packets.help": { + "message": "שמירת ‎.pcap של תעבורת רשת בזמן חלון האיסוף." + }, + "settings.troubleshooting.duration.label": { + "message": "משך לכידה" + }, + "settings.troubleshooting.duration.help": { + "message": "למשך כמה זמן הפעלת הלכידה רצה." + }, + "settings.troubleshooting.duration.suffix": { + "message": "דקה/ות" + }, + "settings.troubleshooting.create": { + "message": "יצירת מאגד" + }, + "settings.troubleshooting.progress.description": { + "message": "היומנים, פרטי המערכת ומצב החיבור נאספים. האיסוף בדרך כלל לוקח כמה רגעים. אפשר להמשיך להשתמש ב־NetBird או לסגור את ההגדרות בזמן שהתהליך מסתיים." + }, + "settings.troubleshooting.cancelling": { + "message": "מבוטל…" + }, + "settings.troubleshooting.done.uploadedTitle": { + "message": "מאגד ניפוי השגיאות נשלח בהצלחה!" + }, + "settings.troubleshooting.done.savedTitle": { + "message": "המאגד נשמר" + }, + "settings.troubleshooting.done.uploadedDescription": { + "message": "יש לשתף את מפתח השליחה להלן עם התמיכה של NetBird. עותק מקומי נשמר בנוסף על המכונה שלך." + }, + "settings.troubleshooting.done.savedDescription": { + "message": "מאגד ניפוי השגיאות שלך נשמר מקומית." + }, + "settings.troubleshooting.done.copyKey": { + "message": "העתקת מפתח" + }, + "settings.troubleshooting.done.openFolder": { + "message": "פתיחת תיקייה" + }, + "settings.troubleshooting.done.openFileLocation": { + "message": "פתיחת מקום הקובץ" + }, + "settings.troubleshooting.uploadFailedWithReason": { + "message": "ההעלאה נכשלה: {reason}, המאגד עדיין שמור מקומית." + }, + "settings.troubleshooting.uploadFailed": { + "message": "ההעלאה נכשלה. המאגד עדיין שמור מקומית." + }, + "settings.troubleshooting.stage.reconnecting": { + "message": "NetBird מתחבר מחדש…" + }, + "settings.troubleshooting.stage.capturing": { + "message": "יומני ניפוי השגיאות נלכדים" + }, + "settings.troubleshooting.stage.bundling": { + "message": "מאגד ניפוי שגיאות נוצר…" + }, + "settings.troubleshooting.stage.uploading": { + "message": "נשלח ל־NetBird…" + }, + "settings.troubleshooting.stage.cancelling": { + "message": "מבטל…" + }, + "settings.about.client": { + "message": "לקוח NetBird v{version}" + }, + "settings.about.clientName": { + "message": "לקוח NetBird" + }, + "settings.about.development": { + "message": "[פיתוח]" + }, + "settings.about.gui": { + "message": "ממשק משתמש v{version}" + }, + "settings.about.guiName": { + "message": "ממשק משתמש" + }, + "settings.about.copyright": { + "message": "© {year} NetBird. כל הזכויות שמורות." + }, + "settings.about.links.imprint": { + "message": "הצהרה משפטית" + }, + "settings.about.links.privacy": { + "message": "פרטיות" + }, + "settings.about.links.cla": { + "message": "CLA" + }, + "settings.about.links.terms": { + "message": "תנאי השירות" + }, + "settings.about.community.github": { + "message": "GitHub" + }, + "settings.about.community.slack": { + "message": "Slack" + }, + "settings.about.community.forum": { + "message": "פורום" + }, + "settings.about.community.documentation": { + "message": "תיעוד" + }, + "settings.about.community.feedback": { + "message": "משוב" + }, + "update.banner.message": { + "message": "ההתקנה של NetBird {version} מוכנה." + }, + "update.banner.later": { + "message": "אחר כך" + }, + "update.banner.installNow": { + "message": "להתקין עכשיו" + }, + "update.card.versionAvailableDownload": { + "message": "גרסה {version} זמינה להורדה." + }, + "update.card.versionAvailableInstall": { + "message": "גרסה {version} זמינה להתקנה." + }, + "update.card.whatsNew": { + "message": "מה חדש?" + }, + "update.card.installNow": { + "message": "להתקין עכשיו" + }, + "update.card.getInstaller": { + "message": "הורדה" + }, + "update.card.autoCheckInterval": { + "message": "NetBird בודק ברקע אם יש עדכונים." + }, + "update.card.changelog": { + "message": "יומן שינויים" + }, + "update.card.onLatestVersion": { + "message": "הגרסה המותקנת היא העדכנית ביותר" + }, + "update.header.tooltip": { + "message": "יש עדכון" + }, + "update.overlay.updatingVersion": { + "message": "NetBird מתעדכן ל־v{version}" + }, + "update.overlay.updating": { + "message": "NetBird מתעדכן" + }, + "update.overlay.description": { + "message": "גרסה חדשה יותר זמינה ומותקנת כרגע. NetBird יופעל מחדש אוטומטית לאחר סיום העדכון." + }, + "update.overlay.error.timeoutTitle": { + "message": "העדכון אורך זמן רב מדי" + }, + "update.overlay.error.timeoutDescription": { + "message": "התקנת {target} ארכה זמן רב מדי ולא הסתיימה." + }, + "update.overlay.error.canceledTitle": { + "message": "העדכון נעצר" + }, + "update.overlay.error.canceledDescription": { + "message": "העדכון לגרסה {target} בוטל לפני שהספיק להסתיים." + }, + "update.overlay.error.failTitle": { + "message": "לא ניתן להתקין את העדכון" + }, + "update.overlay.error.failDescription": { + "message": "לא ניתן להתקין את {target}." + }, + "update.overlay.error.unknownMessage": { + "message": "שגיאה לא ידועה" + }, + "update.overlay.error.targetVersion": { + "message": "v{version}" + }, + "update.overlay.error.targetFallback": { + "message": "הגרסה החדשה" + }, + "update.error.loadStateTitle": { + "message": "טעינת מצב העדכון נכשלה" + }, + "update.error.triggerTitle": { + "message": "התחלת העדכון נכשלה" + }, + "update.page.versionLine": { + "message": "הלקוח מתעדכן לגרסה: {version}." + }, + "update.page.versionLineGeneric": { + "message": "הלקוח מתעדכן." + }, + "update.page.outdated": { + "message": "גרסת הלקוח שלך ישנה מגרסת העדכון האוטומטי שהוגדרה בניהול." + }, + "update.page.status.running": { + "message": "מתעדכן" + }, + "update.page.status.timeout": { + "message": "הזמן שהוקצב לעדכון הסתיים. נא לנסות שוב." + }, + "update.page.status.canceled": { + "message": "העדכון בוטל." + }, + "update.page.status.failed": { + "message": "העדכון נכשל: {message}" + }, + "update.page.status.unknownError": { + "message": "שגיאת עדכון לא ידועה" + }, + "update.page.failedTitle": { + "message": "העדכון נכשל" + }, + "update.page.timeoutMessage": { + "message": "הזמן שהוקצב לעדכון תם." + }, + "update.page.dontClose": { + "message": "נא לא לסגור את החלון הזה" + }, + "update.page.updating": { + "message": "מתעדכן…" + }, + "update.page.complete": { + "message": "העדכון הושלם" + }, + "update.page.failed": { + "message": "העדכון נכשל" + }, + "window.title.settings": { + "message": "הגדרות" + }, + "window.title.signIn": { + "message": "כניסה" + }, + "window.title.sessionExpiration": { + "message": "תוקף ההפעלה פג" + }, + "window.title.updating": { + "message": "מתעדכן" + }, + "window.title.welcome": { + "message": "ברוך בואך ל־NetBird" + }, + "window.title.error": { + "message": "שגיאה" + }, + "welcome.title": { + "message": "כדאי לחפש את NetBird בשורת המשימות שלך" + }, + "welcome.titleMac": { + "message": "כדאי לחפש את NetBird בשורת התפריטים שלך" + }, + "welcome.description": { + "message": "NetBird שוכן בשורת המשימות שלך. יש ללחוץ על הסמל כדי להתחבר, להחליף פרופילים או כדי לפתוח את ההגדרות." + }, + "welcome.descriptionMac": { + "message": "NetBird שוכן בסרגל התפריטים שלך. יש ללחוץ על הסמל כדי להתחבר, להחליף פרופילים או כדי לפתוח את ההגדרות." + }, + "welcome.continue": { + "message": "המשך" + }, + "welcome.back": { + "message": "חזרה" + }, + "welcome.management.title": { + "message": "הגדרת NetBird" + }, + "welcome.management.description": { + "message": "יש ללחוץ על המשך כדי להתחיל או לבחור באירוח עצמי אם יש לך שרת NetBird משלך." + }, + "welcome.management.cloud.title": { + "message": "ענן NetBird" + }, + "welcome.management.cloud.description": { + "message": "אפשר להשתמש בשירות באירוח שלנו. לא צריך הגדרות." + }, + "welcome.management.selfHosted.title": { + "message": "אירוח עצמי" + }, + "welcome.management.selfHosted.description": { + "message": "התחברות לשרת ניהול משלך." + }, + "welcome.management.urlLabel": { + "message": "כתובת שרת ניהול" + }, + "welcome.management.urlPlaceholder": { + "message": "https://netbird.selfhosted.com:443" + }, + "welcome.management.urlInvalid": { + "message": "נא למלא כתובת שרת ניהול תקפה, כגון, https://netbird.selfhosted.com:443" + }, + "welcome.management.urlUnreachable": { + "message": "לא ניתן להגיע לשרת הזה. נא לבדוק את הכתובת או את הרשת שלך, לאחר מכן להמשיך אם היא נכונה בוודאות." + }, + "welcome.management.checking": { + "message": "מתבצעת בדיקה…" + }, + "browserLogin.title": { + "message": "יש להשלים את הכניסה בדפדפן שלך" + }, + "browserLogin.notSeeing": { + "message": "פתחנו לשונית בדפדפן כדי לאפשר לך לסיים את הכניסה למערכת. לא רואה אותה?" + }, + "browserLogin.tryAgain": { + "message": "לנסות שוב" + }, + "browserLogin.openFailedTitle": { + "message": "פתיחת הדפדפן נכשלה" + }, + "sessionExpiration.title": { + "message": "תוקף ההפעלה עומד לפוג בקרוב" + }, + "sessionExpiration.titleLater": { + "message": "תוקף ההפעלה שלך יפוג" + }, + "sessionExpiration.description": { + "message": "המכשיר הזה ינותק בהקדם. יש לחדש את הכניסה דרך הדפדפן." + }, + "sessionExpiration.descriptionLater": { + "message": "כניסה עם דפדפן משאירה את המכשיר הזה מחובר לרשת שלך." + }, + "sessionExpiration.stay": { + "message": "חידוש הפעלה" + }, + "sessionExpiration.authenticate": { + "message": "אימות" + }, + "sessionExpiration.logout": { + "message": "יציאה" + }, + "sessionExpiration.expired": { + "message": "תוקף ההפעלה פג" + }, + "sessionExpiration.expiredDescription": { + "message": "המכשיר נותק. יש לעבור אימות עם כניסת דפדפן כדי להתחבר מחדש." + }, + "sessionExpiration.close": { + "message": "סגירה" + }, + "sessionExpiration.extendFailedTitle": { + "message": "הארכת ההפעלה נכשלה" + }, + "sessionExpiration.logoutFailedTitle": { + "message": "היציאה מהחשבון נכשלה" + }, + "peers.search.placeholder": { + "message": "חיפוש לפי שם או IP" + }, + "peers.filter.all": { + "message": "כולם" + }, + "peers.filter.online": { + "message": "מחוברים" + }, + "peers.filter.offline": { + "message": "מנותקים" + }, + "peers.empty.title": { + "message": "אין עמיתים זמינים" + }, + "peers.empty.description": { + "message": "או שאין לך עמיתים זמינים או שאין לך גישה לאף אחד מהם." + }, + "peers.details.domain": { + "message": "שם תחום" + }, + "peers.details.netbirdIp": { + "message": "IP של NetBird" + }, + "peers.details.netbirdIpv6": { + "message": "IPv6 של NetBird" + }, + "peers.details.publicKey": { + "message": "מפתח ציבורי" + }, + "peers.details.connection": { + "message": "חיבור" + }, + "peers.details.latency": { + "message": "עיכוב" + }, + "peers.details.lastHandshake": { + "message": "לחיצת יד אחרונה" + }, + "peers.details.statusSince": { + "message": "עדכון אחרון על החיבור" + }, + "peers.details.bytes": { + "message": "בתים" + }, + "peers.details.bytesSent": { + "message": "נשלחו" + }, + "peers.details.bytesReceived": { + "message": "התקבלו" + }, + "peers.details.localIce": { + "message": "ICE מקומי" + }, + "peers.details.remoteIce": { + "message": "ICE מרוחק" + }, + "peers.details.never": { + "message": "אף פעם לא" + }, + "peers.details.justNow": { + "message": "כרגע" + }, + "peers.details.refresh": { + "message": "ריענון" + }, + "peers.status.connected": { + "message": "מחובר" + }, + "peers.status.connecting": { + "message": "מתחבר" + }, + "peers.status.disconnected": { + "message": "מנותק" + }, + "peers.details.relayAddress": { + "message": "ממסר" + }, + "peers.details.networks": { + "message": "משאבים" + }, + "peers.details.relayed": { + "message": "דרך ממסר" + }, + "peers.details.p2p": { + "message": "P2P" + }, + "peers.details.rosenpass": { + "message": "Rosenpass הופעל" + }, + "networks.search.placeholder": { + "message": "חיפוש לפי רשת או שם תחום" + }, + "networks.filter.all": { + "message": "כולן" + }, + "networks.filter.active": { + "message": "פעילות" + }, + "networks.filter.overlapping": { + "message": "חופפות" + }, + "networks.empty.title": { + "message": "אין משאבים זמינים" + }, + "networks.empty.description": { + "message": "או שאין לך משאבי רשת זמינים או שאין לך גישה לאף אחד מהם." + }, + "networks.selected": { + "message": "נבחרה" + }, + "networks.unselected": { + "message": "לא נבחרה" + }, + "networks.ips.heading": { + "message": "כתובות IP שנפתרו" + }, + "networks.bulk.selectionCount": { + "message": "{selected} מתוך {total} פעילות" + }, + "networks.bulk.enableAll": { + "message": "להפעיל הכול" + }, + "networks.bulk.disableAll": { + "message": "לכבות הכול" + }, + "exitNodes.search.placeholder": { + "message": "חיפוש נקודות יציאה" + }, + "exitNodes.none": { + "message": "אין" + }, + "exitNodes.empty.title": { + "message": "אין נקודות יציאה זמינות" + }, + "exitNodes.empty.description": { + "message": "לא שותפו נקודות יציאה עם העמית הזה." + }, + "exitNodes.card.title": { + "message": "נקודת יציאה" + }, + "exitNodes.card.statusActive": { + "message": "פעילה" + }, + "exitNodes.card.statusInactive": { + "message": "כבויה" + }, + "exitNodes.dropdown.noneTitle": { + "message": "אין" + }, + "exitNodes.dropdown.noneDescription": { + "message": "חיבור ישיר ללא נקודת יציאה" + }, + "quickActions.connect": { + "message": "התחברות" + }, + "quickActions.disconnect": { + "message": "התנתקות" + }, + "daemon.unavailable.title": { + "message": "שירות NetBird לא פעיל" + }, + "daemon.unavailable.description": { + "message": "היישום יתחבר אוטומטית לאחר שהשירות יופעל." + }, + "daemon.unavailable.docsLink": { + "message": "תיעוד" + }, + "daemon.outdated.title": { + "message": "שירות NetBird אינו עדכני" + }, + "daemon.outdated.description": { + "message": "ממשק המשתמש החדש לא תואם ללקוח ישן יותר של NetBird. יש לעדכן את הלקוח שלך כדי להשתמש ביישום החדש" + }, + "daemon.outdated.download": { + "message": "להוריד את העדכני ביותר" + }, + "error.jwt_clock_skew": { + "message": "הכניסה נכשלה: השעון של המכשיר הזה לא תואם במדויק את זה של השרת. נא לכוון את שעון המערכת שלך ולנסות שוב." + }, + "error.jwt_expired": { + "message": "תוקף אסימון הכניסה שלך פג. נא להיכנס שוב." + }, + "error.jwt_signature_invalid": { + "message": "הכניסה נכשלה: חתימת האסימון שגויה. נא ליצור קשר עם הנהלת המערכת שלך." + }, + "error.session_expired": { + "message": "תוקף ההפעלה שלך פג. נא להיכנס שוב." + }, + "error.invalid_setup_key": { + "message": "מפתח ההתקנה חסר או שגוי." + }, + "error.permission_denied": { + "message": "בקשת הכניסה נדחתה על ידי השרת." + }, + "error.daemon_unreachable": { + "message": "הסוכן של NetBird לא מגיב. נא לבדוק שהשירות פעיל." + }, + "error.settings_locked": { + "message": "אי אפשר לשנות את ההגדרות במכשיר הזה: הן ננעלו על ידי ההנהלה." + }, + "error.settings_managed_by_mdm": { + "message": "ההגדרה הזאת מנוהלת על ידי הארגון שלך ואי אפשר לשנות אותה." + }, + "error.unknown": { + "message": "הפעולה נכשלה." + }, + "error.elevation_unavailable": { + "message": "NetBird לא הצליח לבקש מהמערכת הזאת את ההרשאות שהשינוי הזה צריך. יש להריץ את זה במקום:" + }, + "error.elevation_failed": { + "message": "אי אפשר להחיל את השינויים עם הרשאות גבוהות יותר. יש להריץ את זה במקום:" + }, + "settings.ssh.privilege.actorRoot": { + "message": "משתמש על/root" + }, + "settings.ssh.privilege.actorAdministrator": { + "message": "הרשאות ניהול" + }, + "settings.ssh.privilege.hint": { + "message": "דורש {actor}. יש להריץ את זה במקום:" + }, + "settings.ssh.privilege.oneWay": { + "message": "אפשר לכבות את זה, אבל ההפעלה דורשת {actor}." + }, + "settings.ssh.privilege.oneWayInverted": { + "message": "אפשר להפעיל את זה, אבל הכיבוי דורש {actor}." + }, + "settings.ssh.privilege.authorizePending": { + "message": "בהמתנה לאימות…" + } +} diff --git a/client/ui/i18n/locales/hu/common.json b/client/ui/i18n/locales/hu/common.json index 94dcb578c..36705afbc 100644 --- a/client/ui/i18n/locales/hu/common.json +++ b/client/ui/i18n/locales/hu/common.json @@ -552,12 +552,10 @@ "message": "Az automatikus indítás módosítása sikertelen" }, "settings.general.keepConnectedOnQuit.label": { - "message": "Kapcsolat megtartása kilépéskor", - "description": "Toggle label: keep the VPN connection up after quitting the UI." + "message": "Kapcsolat megtartása kilépéskor" }, "settings.general.keepConnectedOnQuit.help": { - "message": "A kapcsolat a háttérben megmarad, miután bezárod a NetBirdöt. Csak akkor szakad meg, ha te magad bontod.", - "description": "Helper text for the stay-connected-after-quitting toggle." + "message": "A kapcsolat a háttérben megmarad, miután bezárod a NetBirdöt. Csak akkor szakad meg, ha te magad bontod." }, "settings.general.language.label": { "message": "Megjelenítési nyelv" diff --git a/client/ui/i18n/locales/it/common.json b/client/ui/i18n/locales/it/common.json index 8c1312535..6590a01fe 100644 --- a/client/ui/i18n/locales/it/common.json +++ b/client/ui/i18n/locales/it/common.json @@ -552,12 +552,10 @@ "message": "Modifica avvio automatico non riuscita" }, "settings.general.keepConnectedOnQuit.label": { - "message": "Resta connesso dopo la chiusura", - "description": "Toggle label: keep the VPN connection up after quitting the UI." + "message": "Resta connesso dopo la chiusura" }, "settings.general.keepConnectedOnQuit.help": { - "message": "La connessione resta attiva in background dopo la chiusura di NetBird. Si interrompe solo quando la disconnetti tu.", - "description": "Helper text for the stay-connected-after-quitting toggle." + "message": "La connessione resta attiva in background dopo la chiusura di NetBird. Si interrompe solo quando la disconnetti tu." }, "settings.general.language.label": { "message": "Lingua dell'interfaccia" diff --git a/client/ui/i18n/locales/ja/common.json b/client/ui/i18n/locales/ja/common.json index a3138de6c..c8918e4ae 100644 --- a/client/ui/i18n/locales/ja/common.json +++ b/client/ui/i18n/locales/ja/common.json @@ -552,12 +552,10 @@ "message": "自動起動の変更に失敗しました" }, "settings.general.keepConnectedOnQuit.label": { - "message": "終了後も接続を維持", - "description": "Toggle label: keep the VPN connection up after quitting the UI." + "message": "終了後も接続を維持" }, "settings.general.keepConnectedOnQuit.help": { - "message": "NetBird を閉じたあとも接続はバックグラウンドで維持されます。自分で切断したときにだけ停止します。", - "description": "Helper text for the stay-connected-after-quitting toggle." + "message": "NetBird を閉じたあとも接続はバックグラウンドで維持されます。自分で切断したときにだけ停止します。" }, "settings.general.language.label": { "message": "表示言語" diff --git a/client/ui/i18n/locales/pt/common.json b/client/ui/i18n/locales/pt/common.json index a75ae3dfc..aca458a4d 100644 --- a/client/ui/i18n/locales/pt/common.json +++ b/client/ui/i18n/locales/pt/common.json @@ -552,12 +552,10 @@ "message": "Falha ao alterar o início automático" }, "settings.general.keepConnectedOnQuit.label": { - "message": "Permanecer conectado ao sair", - "description": "Toggle label: keep the VPN connection up after quitting the UI." + "message": "Permanecer conectado ao sair" }, "settings.general.keepConnectedOnQuit.help": { - "message": "A conexão continua ativa em segundo plano depois de fechar o NetBird. Ela só para quando você mesmo a desconecta.", - "description": "Helper text for the stay-connected-after-quitting toggle." + "message": "A conexão continua ativa em segundo plano depois de fechar o NetBird. Ela só para quando você mesmo a desconecta." }, "settings.general.language.label": { "message": "Idioma de exibição" diff --git a/client/ui/i18n/locales/ru/common.json b/client/ui/i18n/locales/ru/common.json index e11ed26a4..a0795c1da 100644 --- a/client/ui/i18n/locales/ru/common.json +++ b/client/ui/i18n/locales/ru/common.json @@ -552,12 +552,10 @@ "message": "Не удалось изменить автозапуск" }, "settings.general.keepConnectedOnQuit.label": { - "message": "Оставаться подключённым после выхода", - "description": "Toggle label: keep the VPN connection up after quitting the UI." + "message": "Оставаться подключённым после выхода" }, "settings.general.keepConnectedOnQuit.help": { - "message": "Соединение остаётся активным в фоне после закрытия NetBird. Оно прервётся, только когда вы отключите его сами.", - "description": "Helper text for the stay-connected-after-quitting toggle." + "message": "Соединение остаётся активным в фоне после закрытия NetBird. Оно прервётся, только когда вы отключите его сами." }, "settings.general.language.label": { "message": "Язык интерфейса" diff --git a/client/ui/i18n/locales/zh-CN/common.json b/client/ui/i18n/locales/zh-CN/common.json index 64725a69f..9f4713899 100644 --- a/client/ui/i18n/locales/zh-CN/common.json +++ b/client/ui/i18n/locales/zh-CN/common.json @@ -552,12 +552,10 @@ "message": "更改自启动设置失败" }, "settings.general.keepConnectedOnQuit.label": { - "message": "退出后保持连接", - "description": "Toggle label: keep the VPN connection up after quitting the UI." + "message": "退出后保持连接" }, "settings.general.keepConnectedOnQuit.help": { - "message": "关闭 NetBird 后,连接会在后台保持。只有你自己断开时才会停止。", - "description": "Helper text for the stay-connected-after-quitting toggle." + "message": "关闭 NetBird 后,连接会在后台保持。只有你自己断开时才会停止。" }, "settings.general.language.label": { "message": "显示语言" From 0bb98547042986f02a47a07ac70557a8b2893dcf Mon Sep 17 00:00:00 2001 From: Edward <43848523+thomashacker@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:39:17 +0200 Subject: [PATCH 5/5] [client] Add Hebrew to the language picker (#8102) The Hebrew translation is complete and synced from Crowdin, and RTL layout support is in, so list it in _index.json. --- client/ui/i18n/locales/_index.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ui/i18n/locales/_index.json b/client/ui/i18n/locales/_index.json index a52ff5929..4e9b632b5 100644 --- a/client/ui/i18n/locales/_index.json +++ b/client/ui/i18n/locales/_index.json @@ -11,6 +11,7 @@ {"code": "pt", "displayName": "Português", "englishName": "Portuguese"}, {"code": "zh-CN", "displayName": "简体中文", "englishName": "Simplified Chinese"}, {"code": "ja", "displayName": "日本語", "englishName": "Japanese"}, - {"code": "gl", "displayName": "Galego", "englishName": "Galician"} + {"code": "gl", "displayName": "Galego", "englishName": "Galician"}, + {"code": "he", "displayName": "עברית", "englishName": "Hebrew"} ] }