diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000000..8d8821fda09 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,31 @@ +#!/bin/bash + +echo "Running pre-commit hook..." + +# Check for unformatted Go files (only staged files) +STAGED_GO_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$') + +if [ -n "$STAGED_GO_FILES" ]; then + UNFORMATTED=$(echo "$STAGED_GO_FILES" | xargs gofmt -l 2>/dev/null) + if [ -n "$UNFORMATTED" ]; then + echo "ERROR: Unformatted Go files:" + echo "$UNFORMATTED" + echo "" + echo "Run 'gofmt -w ' to fix, then 'git add' again" + echo "Or run 'gofmt -w .' to fix all" + exit 1 + fi +fi + +# Check for secrets in staged files +SECRETS_PATTERN='(PRIVATE KEY|password.*=|api[_-]?key|secret[_-]?key|-----BEGIN)' +if git diff --cached --name-only | xargs grep -l -E "$SECRETS_PATTERN" 2>/dev/null; then + echo "" + echo "WARNING: Potential secrets detected in staged files!" + echo "Please review before committing." + echo "" + echo "To bypass this check (only if you're sure): git commit --no-verify" + exit 1 +fi + +echo "Pre-commit checks passed!" diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000000..6ef652e63aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug Report +about: Fehler oder Problem melden +title: '[Bug] ' +labels: type:bug, priority:high +assignees: '' +--- + +## Beschreibung +Was ist passiert? + +## Schritte zum Reproduzieren +1. ... +2. ... +3. ... + +## Erwartetes Verhalten +Was sollte passieren? + +## Tatsächliches Verhalten +Was passiert stattdessen? + +## Umgebung +- OS: [z.B. Windows 11, Ubuntu 22.04] +- NetBird Version: [z.B. 0.31.0] +- Go Version: [z.B. 1.22] + +## Logs/Screenshots +(Optional) Fehlermeldungen oder Screenshots diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..abf8ae2e7cc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: NetBird Documentation + url: https://docs.netbird.io + about: Official NetBird documentation + - name: NetBird Discussions + url: https://github.com/netbirdio/netbird/discussions + about: Ask questions and discuss NetBird diff --git a/.github/ISSUE_TEMPLATE/epic.md b/.github/ISSUE_TEMPLATE/epic.md new file mode 100644 index 00000000000..8a8e167430e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic.md @@ -0,0 +1,39 @@ +--- +name: Epic +about: Großes Feature das mehrere Stories umfasst +title: '[Epic] E-X: ' +labels: type:epic, priority:critical, phase:mvp +assignees: '' +--- + +## Epic Beschreibung +Was ist das übergeordnete Ziel dieses Epics? + +## Business Value +Welchen Wert bringt dieses Epic für den User/das Projekt? + +## Scope +Was ist Teil dieses Epics? Was ist NICHT Teil? + +**In Scope:** +- + +**Out of Scope:** +- + +## Stories + +- [ ] S-1: ... +- [ ] S-2: ... + +## Acceptance Criteria + +- [ ] ... + +## Dependencies + +- + +## Risks + +- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 4a3e5782cf5..3c9ca28dbb5 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,8 +1,8 @@ --- name: Feature request about: Suggest an idea for this project -title: '' -labels: ['feature-request'] +title: '[Feature] ' +labels: type:feature assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/story.md b/.github/ISSUE_TEMPLATE/story.md new file mode 100644 index 00000000000..ad68bf327dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/story.md @@ -0,0 +1,32 @@ +--- +name: User Story +about: Feature aus Nutzersicht beschreiben +title: '[Story] S-X: ' +labels: type:story, priority:high, phase:mvp +assignees: '' +--- + +## Parent Epic + +Refs # + +## User Story +Als [Rolle] +möchte ich [Funktion] +damit [Nutzen] + +## Acceptance Criteria + +- [ ] ... + +## Tasks + +- [ ] T-X.1: ... +- [ ] T-X.2: ... + +## Technical Notes + +- + +## Branch +`feature/...` diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md new file mode 100644 index 00000000000..8e7e68f15dd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.md @@ -0,0 +1,31 @@ +--- +name: Technical Task +about: Technische Aufgabe für die Implementierung +title: '[Task] T-X.Y: ' +labels: type:task, phase:mvp +assignees: '' +--- + +## Parent Story + +Refs # + +## Beschreibung +Was muss technisch umgesetzt werden? + +## Dateien/Komponenten + +- + +## Implementation Notes + +- + +## Definition of Done +- [ ] Code implementiert +- [ ] Tests geschrieben +- [ ] Code reviewed +- [ ] Dokumentation aktualisiert (falls nötig) + +## Estimated Complexity + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..6fd9a493029 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +version: 2 +updates: + # Go dependencies + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "deps" + labels: + - "type:deps" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "ci" + labels: + - "type:ci" + + # Docker dependencies + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "deps" + labels: + - "type:deps" diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 00000000000..6453cbc269c --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,97 @@ +name: Auto Label + +on: + issues: + types: [opened] + pull_request: + types: [opened] + +permissions: + issues: write + pull-requests: write + +jobs: + label-issues: + if: github.event_name == 'issues' + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + const title = context.payload.issue.title.toLowerCase(); + const labels = ['status:backlog']; + + // Epic/Story/Task detection (Machine Tunnel Plan) + if (title.includes('[epic]') || title.startsWith('e-')) { + labels.push('type:epic', 'priority:critical'); + } else if (title.includes('[story]') || title.startsWith('s-')) { + labels.push('type:story', 'priority:high'); + } else if (title.includes('[task]') || title.startsWith('t-')) { + labels.push('type:task'); + } + + // Type detection from title prefix + if (title.includes('[bug]') || title.startsWith('bug:') || title.startsWith('fix:')) { + labels.push('type:bug', 'priority:high'); + } else if (title.includes('[feature]') || title.startsWith('feat:')) { + labels.push('type:feature'); + } else if (title.includes('[docs]') || title.startsWith('docs:')) { + labels.push('type:docs'); + } else if (title.includes('[refactor]') || title.startsWith('refactor:')) { + labels.push('type:refactor'); + } else if (title.includes('[ci]') || title.startsWith('ci:')) { + labels.push('type:ci'); + } else if (title.includes('[spike]') || title.startsWith('spike:')) { + labels.push('type:spike'); + } + + // Phase detection + if (title.includes('[mvp]') || title.includes('phase:mvp')) { + labels.push('phase:mvp'); + } + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: labels + }); + + label-prs: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + const title = context.payload.pull_request.title.toLowerCase(); + const labels = []; + + // Type detection from Conventional Commit prefix + if (title.startsWith('feat:') || title.startsWith('feat(')) { + labels.push('type:feature'); + } else if (title.startsWith('fix:') || title.startsWith('fix(')) { + labels.push('type:bug'); + } else if (title.startsWith('docs:') || title.startsWith('docs(')) { + labels.push('type:docs'); + } else if (title.startsWith('refactor:') || title.startsWith('refactor(')) { + labels.push('type:refactor'); + } else if (title.startsWith('ci:') || title.startsWith('ci(')) { + labels.push('type:ci'); + } else if (title.startsWith('deps:') || title.startsWith('chore(deps)')) { + labels.push('type:deps'); + } + + // Dependabot PRs + if (context.payload.pull_request.user.login === 'dependabot[bot]') { + labels.push('type:deps'); + } + + if (labels.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: labels + }); + } diff --git a/.github/workflows/e2e-tunnel.yml b/.github/workflows/e2e-tunnel.yml new file mode 100644 index 00000000000..cb1735d0e13 --- /dev/null +++ b/.github/workflows/e2e-tunnel.yml @@ -0,0 +1,154 @@ +name: "E2E Tunnel Tests" + +# This workflow is for manual E2E testing in a lab environment. +# It cannot run in GitHub Actions due to requirements: +# - Windows domain-joined machine +# - NetBird Machine Service running +# - Domain Controller accessible via tunnel +# +# Use workflow_dispatch to record test results from lab testing. + +on: + workflow_dispatch: + inputs: + test_environment: + description: 'Test environment (e.g., lab-proxmox, azure-lab)' + required: true + default: 'lab-proxmox' + dc_address: + description: 'Domain Controller IP address' + required: true + default: '192.168.100.20' + domain_name: + description: 'Domain name for SRV lookups' + required: true + default: 'test.local' + test_results: + description: 'Test results summary (for documentation)' + required: false + default: '' + +env: + TEST_SCRIPT: scripts/tests/Test-TunnelEstablishment.ps1 + +jobs: + validate-script: + name: "Validate Test Script Syntax" + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Validate PowerShell syntax + shell: pwsh + run: | + $script = Get-Content "${{ env.TEST_SCRIPT }}" -Raw + $errors = $null + [System.Management.Automation.PSParser]::Tokenize($script, [ref]$errors) + if ($errors.Count -gt 0) { + Write-Error "PowerShell syntax errors found:" + $errors | ForEach-Object { Write-Error $_.Message } + exit 1 + } + Write-Host "PowerShell script syntax is valid" + + - name: Show script help + shell: pwsh + run: | + Get-Help "${{ env.TEST_SCRIPT }}" -Detailed + + document-results: + name: "Document Lab Test Results" + runs-on: ubuntu-latest + needs: validate-script + if: ${{ github.event.inputs.test_results != '' }} + steps: + - name: Create test summary + run: | + cat << 'EOF' >> $GITHUB_STEP_SUMMARY + # E2E Tunnel Test Results + + ## Environment + - **Test Environment:** ${{ github.event.inputs.test_environment }} + - **Domain Controller:** ${{ github.event.inputs.dc_address }} + - **Domain:** ${{ github.event.inputs.domain_name }} + - **Run Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC") + - **Triggered by:** ${{ github.actor }} + + ## Results + ${{ github.event.inputs.test_results }} + + ## Test Script + The following test script was used: + - `${{ env.TEST_SCRIPT }}` + + ## Test Cases + | Test | Description | + |------|-------------| + | TC1.1 | Service Running | + | TC1.2 | WireGuard Interface | + | TC1.3 | Route to DC Network | + | TC1.4 | DC LDAP (389/TCP) | + | TC1.5 | DC Kerberos (88/TCP) | + | TC1.6 | DC DNS (53/TCP) | + | TC1.7 | Kerberos TGT | + | TC2.1 | LDAP SRV Record | + | TC3.1 | Kerberos SRV (UDP) | + | TC3.2 | Kerberos SRV (TCP) | + | TC4.1 | DC Discovery (nltest) | + | TC4.2 | UDP Kerberos Indicator | + EOF + + instructions: + name: "Lab Testing Instructions" + runs-on: ubuntu-latest + needs: validate-script + if: ${{ github.event.inputs.test_results == '' }} + steps: + - name: Show testing instructions + run: | + cat << 'EOF' >> $GITHUB_STEP_SUMMARY + # E2E Tunnel Testing Instructions + + ## Prerequisites + 1. Windows 10/11 VM in lab environment (Proxmox/Azure) + 2. VM must be domain-joined to `${{ github.event.inputs.domain_name }}` + 3. NetBird Machine Service installed and configured + 4. Domain Controller at `${{ github.event.inputs.dc_address }}` accessible via tunnel + + ## Running Tests + + ### 1. Copy test script to Windows VM + ```powershell + # From Linux host + scp scripts/tests/Test-TunnelEstablishment.ps1 administrator@:C:\temp\ + ``` + + ### 2. Run tests on Windows VM (as Administrator) + ```powershell + cd C:\temp + .\Test-TunnelEstablishment.ps1 -DCAddress ${{ github.event.inputs.dc_address }} -DomainName ${{ github.event.inputs.domain_name }} -Verbose + ``` + + ### 3. Capture results + Save the output and re-run this workflow with the `test_results` input filled in. + + ## Expected Output + ``` + ============================================================ + TEST SUMMARY + ============================================================ + + Passed: 12 + Failed: 0 + Skipped: 0 + + Pass Rate: 100% + ``` + + ## Troubleshooting + - **Service not running:** `Start-Service NetBirdMachine` + - **No WireGuard interface:** Check service logs in Event Viewer + - **DC not reachable:** Verify tunnel is established, check routes + - **No Kerberos TGT:** Run `klist purge` then `gpupdate /force` + EOF diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml new file mode 100644 index 00000000000..8d95ff22abe --- /dev/null +++ b/.github/workflows/pr-lint.yml @@ -0,0 +1,36 @@ +name: PR Lint + +on: + pull_request: + types: [opened, edited, synchronize] + +permissions: + pull-requests: read + +jobs: + conventional-commits: + # Soft enforcement - NOT a required check (Dependabot compatibility) + runs-on: ubuntu-latest + steps: + - name: Check PR title follows Conventional Commits + uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + docs + style + refactor + perf + test + build + ci + chore + revert + deps + requireScope: false + # Allow Dependabot titles like "Bump X from Y to Z" + ignoreLabels: | + type:deps diff --git a/.gitignore b/.gitignore index 89024d1901a..e73ee2ec018 100644 --- a/.gitignore +++ b/.gitignore @@ -27,8 +27,55 @@ client/client.exe client/.distfiles/ infrastructure_files/setup.env infrastructure_files/setup-*.env -.vscode +# VSCode - ignore personal settings, keep shared configs +.vscode/settings.json +.vscode/*.code-workspace .DS_Store vendor/ /netbird client/netbird-electron/ + +# ========================================== +# Machine Tunnel Fork - Additional Ignores +# ========================================== + +# Secrets - NIEMALS committen! +*.key +*.pem +*.p12 +*.pfx +*.crt +!ca.crt +secrets/ +credentials/ +credentials.json + +# Test artifacts +coverage.out +coverage.html +*.test +*.prof + +# Build artifacts +*.exe +*.dll +*.so +*.dylib +__debug_bin* + +# OS-specific +Thumbs.db +*~ +*.swp + +# Editor/IDE (additional) +*.sublime-* +.history/ + +# Local development +.env.local +.env.*.local +docker-compose.override.yml + +# Security Audits - NICHT ins Public Repo! +docs/AUDIT-*.md diff --git a/Makefile b/Makefile index 43379e115cc..2a339e7cdf5 100644 --- a/Makefile +++ b/Makefile @@ -23,5 +23,54 @@ lint-install: $(GOLANGCI_LINT) # Setup git hooks for all developers setup-hooks: @git config core.hooksPath .githooks - @chmod +x .githooks/pre-push - @echo "✅ Git hooks configured! Pre-push will now run 'make lint'" + @chmod +x .githooks/pre-push .githooks/pre-commit + @echo "Git hooks configured:" + @echo " - pre-commit: gofmt check, secrets detection" + @echo " - pre-push: make lint" + +# ========================================== +# Machine Tunnel Fork - Build Targets +# ========================================== + +.PHONY: build-windows build-windows-nocgo build-linux build-all clean + +# Output directory +DIST_DIR := dist + +# Version from git tag or default +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +LDFLAGS := -X main.version=$(VERSION) + +# Windows Cross-Compile with CGO (required for CNG Cert Store) +# Requires: mingw-w64 (dnf install mingw64-gcc / apt install gcc-mingw-w64-x86-64) +build-windows: + @echo "Building Windows binary (with CGO for CNG support)..." + @mkdir -p $(DIST_DIR) + GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \ + go build -ldflags "$(LDFLAGS)" -o $(DIST_DIR)/netbird-machine.exe ./client/cmd/ + +# Windows Cross-Compile without CGO (faster, but no CNG support) +build-windows-nocgo: + @echo "Building Windows binary (no CGO - faster but limited)..." + @mkdir -p $(DIST_DIR) + GOOS=windows GOARCH=amd64 CGO_ENABLED=0 \ + go build -ldflags "$(LDFLAGS)" -o $(DIST_DIR)/netbird-machine-nocgo.exe ./client/cmd/ + +# Linux Build +build-linux: + @echo "Building Linux binary..." + @mkdir -p $(DIST_DIR) + go build -ldflags "$(LDFLAGS)" -o $(DIST_DIR)/netbird-machine ./client/cmd/ + +# Build all platforms +build-all: build-linux build-windows-nocgo + @echo "Built all platforms in $(DIST_DIR)/" + +# Clean build artifacts +clean: + @rm -rf $(DIST_DIR) + @echo "Cleaned build artifacts" + +# Quick test build (no CGO, fast feedback) +build-quick: build-windows-nocgo + @echo "Quick build complete: $(DIST_DIR)/netbird-machine-nocgo.exe" diff --git a/client/internal/tunnel/bootstrap.go b/client/internal/tunnel/bootstrap.go new file mode 100644 index 00000000000..df9c17c4bd8 --- /dev/null +++ b/client/internal/tunnel/bootstrap.go @@ -0,0 +1,470 @@ +// Package tunnel provides machine tunnel functionality for Windows pre-login VPN. +// This package handles the two-phase bootstrap process: +// Phase 1: Setup-Key authentication (for initial enrollment) +// Phase 2: mTLS authentication (after AD CS certificate enrollment) +package tunnel + +import ( + "context" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "fmt" + "net/url" + "strings" + "time" + + "github.com/google/uuid" + log "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/internal/profilemanager" + "github.com/netbirdio/netbird/client/ssh" + "github.com/netbirdio/netbird/client/system" + mgm "github.com/netbirdio/netbird/shared/management/client" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" +) + +// infoToProtoMeta converts system.Info to proto.PeerSystemMeta for gRPC requests. +func infoToProtoMeta(info *system.Info) *mgmProto.PeerSystemMeta { + if info == nil { + return nil + } + + addresses := make([]*mgmProto.NetworkAddress, 0, len(info.NetworkAddresses)) + for _, addr := range info.NetworkAddresses { + addresses = append(addresses, &mgmProto.NetworkAddress{ + NetIP: addr.NetIP.String(), + Mac: addr.Mac, + }) + } + + files := make([]*mgmProto.File, 0, len(info.Files)) + for _, file := range info.Files { + files = append(files, &mgmProto.File{ + Path: file.Path, + Exist: file.Exist, + ProcessIsRunning: file.ProcessIsRunning, + }) + } + + return &mgmProto.PeerSystemMeta{ + Hostname: info.Hostname, + GoOS: info.GoOS, + OS: info.OS, + Core: info.OSVersion, + OSVersion: info.OSVersion, + Platform: info.Platform, + Kernel: info.Kernel, + NetbirdVersion: info.NetbirdVersion, + UiVersion: info.UIVersion, + KernelVersion: info.KernelVersion, + NetworkAddresses: addresses, + SysSerialNumber: info.SystemSerialNumber, + SysManufacturer: info.SystemManufacturer, + SysProductName: info.SystemProductName, + Environment: &mgmProto.Environment{ + Cloud: info.Environment.Cloud, + Platform: info.Environment.Platform, + }, + Files: files, + Flags: &mgmProto.Flags{ + RosenpassEnabled: info.RosenpassEnabled, + RosenpassPermissive: info.RosenpassPermissive, + ServerSSHAllowed: info.ServerSSHAllowed, + DisableClientRoutes: info.DisableClientRoutes, + DisableServerRoutes: info.DisableServerRoutes, + DisableDNS: info.DisableDNS, + DisableFirewall: info.DisableFirewall, + BlockLANAccess: info.BlockLANAccess, + BlockInbound: info.BlockInbound, + LazyConnectionEnabled: info.LazyConnectionEnabled, + EnableSSHRoot: info.EnableSSHRoot, + EnableSSHSFTP: info.EnableSSHSFTP, + EnableSSHLocalPortForwarding: info.EnableSSHLocalPortForwarding, + EnableSSHRemotePortForwarding: info.EnableSSHRemotePortForwarding, + DisableSSHAuth: info.DisableSSHAuth, + }, + } +} + +// AuthMethod indicates which authentication method was used for bootstrap. +type AuthMethod int + +const ( + // AuthMethodUnknown indicates no authentication method was determined. + AuthMethodUnknown AuthMethod = iota + // AuthMethodSetupKey indicates Setup-Key was used (Phase 1). + AuthMethodSetupKey + // AuthMethodMTLS indicates mTLS with machine certificate was used (Phase 2). + AuthMethodMTLS +) + +func (m AuthMethod) String() string { + switch m { + case AuthMethodSetupKey: + return "SetupKey" + case AuthMethodMTLS: + return "mTLS" + default: + return "Unknown" + } +} + +// BootstrapResult contains the result of the bootstrap process. +type BootstrapResult struct { + // AuthMethod indicates which authentication was used. + AuthMethod AuthMethod + + // PeerConfig is the local peer configuration from the server. + PeerConfig *mgmProto.PeerConfig + + // NetbirdConfig contains STUN/TURN/Relay configuration. + NetbirdConfig *mgmProto.NetbirdConfig + + // MachineIdentity is present only for mTLS auth (Phase 2). + MachineIdentity *mgmProto.MachineIdentity + + // AllowedDCRoutes are routes this machine can access (mTLS only). + AllowedDCRoutes []*mgmProto.Route + + // DNSConfig for DC DNS resolution (mTLS only). + DNSConfig *mgmProto.DNSConfig +} + +// MachineConfig extends the standard Config with machine tunnel specific settings. +type MachineConfig struct { + // Embed standard config + *profilemanager.Config + + // MachineCertEnabled indicates whether to use machine certificate authentication. + MachineCertEnabled bool + + // MachineCertThumbprint is the expected certificate thumbprint (optional validation). + MachineCertThumbprint string + + // SetupKey for Phase 1 bootstrap (one-time use, should be revoked after Phase 2). + SetupKey string + + // MTLSPort is the port for mTLS connections (default: 33074). + MTLSPort int + + // DCRoutes are the Domain Controller network CIDRs to route through the tunnel. + DCRoutes []string +} + +// DefaultMTLSPort is the default port for mTLS machine tunnel connections. +const DefaultMTLSPort = 33074 + +// Bootstrap initiates the machine tunnel authentication process. +// It automatically selects the appropriate authentication method: +// - If a valid machine certificate is available, uses mTLS (Phase 2) +// - Otherwise, falls back to Setup-Key authentication (Phase 1) +// +// After successful Setup-Key bootstrap, the client should: +// 1. Join the domain (if not already joined) +// 2. Enroll a machine certificate via AD CS +// 3. Update config to enable machine cert (MachineCertEnabled = true) +// 4. Restart the service to switch to mTLS auth +func Bootstrap(ctx context.Context, cfg *MachineConfig) (*BootstrapResult, error) { + if cfg == nil || cfg.Config == nil { + return nil, fmt.Errorf("config is required") + } + + // Check if machine certificate is available and enabled + if cfg.MachineCertEnabled && hasMachineCert(cfg) { + log.Info("Machine certificate available, attempting mTLS authentication (Phase 2)") + result, err := bootstrapWithMTLS(ctx, cfg) + if err != nil { + // If mTLS fails and we have a setup key, fall back to Phase 1 + if cfg.SetupKey != "" { + log.Warnf("mTLS authentication failed: %v, falling back to Setup-Key", err) + return bootstrapWithSetupKey(ctx, cfg) + } + return nil, fmt.Errorf("mTLS authentication failed: %w", err) + } + return result, nil + } + + // No machine cert or not enabled - use Setup-Key (Phase 1) + if cfg.SetupKey == "" { + return nil, fmt.Errorf("no machine certificate available and no setup key provided; " + + "for initial bootstrap, provide a setup key") + } + + log.Info("No machine certificate, using Setup-Key authentication (Phase 1)") + return bootstrapWithSetupKey(ctx, cfg) +} + +// hasMachineCert checks if a valid machine certificate is configured and loadable. +func hasMachineCert(cfg *MachineConfig) bool { + if cfg.ClientCertPath == "" || cfg.ClientCertKeyPath == "" { + log.Debug("Machine cert paths not configured") + return false + } + + // Try to load the certificate + cert, err := tls.LoadX509KeyPair(cfg.ClientCertPath, cfg.ClientCertKeyPath) + if err != nil { + log.Debugf("Failed to load machine certificate: %v", err) + return false + } + + // Parse to check validity + if len(cert.Certificate) == 0 { + log.Debug("No certificate in loaded key pair") + return false + } + + x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + log.Debugf("Failed to parse machine certificate: %v", err) + return false + } + + // Check expiry + now := time.Now() + if now.Before(x509Cert.NotBefore) { + log.Debugf("Machine certificate not yet valid (NotBefore: %v)", x509Cert.NotBefore) + return false + } + if now.After(x509Cert.NotAfter) { + log.Debugf("Machine certificate expired (NotAfter: %v)", x509Cert.NotAfter) + return false + } + + // Check for required SAN DNSName + if len(x509Cert.DNSNames) == 0 { + log.Debug("Machine certificate has no SAN DNSNames") + return false + } + + // Validate thumbprint if specified + if cfg.MachineCertThumbprint != "" { + actualThumbprint := fmt.Sprintf("%x", sha256.Sum256(cert.Certificate[0])) + if !strings.EqualFold(actualThumbprint, cfg.MachineCertThumbprint) { + log.Debugf("Machine certificate thumbprint mismatch: expected %s, got %s", + cfg.MachineCertThumbprint, actualThumbprint) + return false + } + } + + log.Debugf("Machine certificate valid: DNSNames=%v, NotAfter=%v", x509Cert.DNSNames, x509Cert.NotAfter) + return true +} + +// bootstrapWithSetupKey performs Phase 1 bootstrap using a Setup-Key. +// This uses the standard Login/Register RPC (not RegisterMachinePeer). +func bootstrapWithSetupKey(ctx context.Context, cfg *MachineConfig) (*BootstrapResult, error) { + // Validate setup key format + if _, err := uuid.Parse(cfg.SetupKey); err != nil { + return nil, fmt.Errorf("invalid setup key format: %w", err) + } + + log.Debugf("Connecting to management server %s with Setup-Key", cfg.ManagementURL) + + // Create standard management client (not mTLS) + mgmClient, err := getMgmClient(ctx, cfg.Config) + if err != nil { + return nil, fmt.Errorf("failed to connect to management server: %w", err) + } + defer func() { + if closeErr := mgmClient.Close(); closeErr != nil { + log.Warnf("Failed to close management client: %v", closeErr) + } + }() + + // Get server public key + serverKey, err := mgmClient.GetServerPublicKey() + if err != nil { + return nil, fmt.Errorf("failed to get server public key: %w", err) + } + + // Generate SSH key for registration + pubSSHKey, err := ssh.GeneratePublicKey([]byte(cfg.SSHKey)) + if err != nil { + return nil, fmt.Errorf("failed to generate SSH public key: %w", err) + } + + // Try to login first (peer might already be registered) + sysInfo := system.GetInfo(ctx) + setSystemFlags(sysInfo, cfg.Config) + + loginResp, err := mgmClient.Login(*serverKey, sysInfo, pubSSHKey, cfg.DNSLabels) + if err == nil { + // Already registered, login successful + log.Info("Setup-Key bootstrap: peer already registered, login successful") + return &BootstrapResult{ + AuthMethod: AuthMethodSetupKey, + PeerConfig: loginResp.PeerConfig, + NetbirdConfig: loginResp.NetbirdConfig, + }, nil + } + + // Check if registration is needed + if !isRegistrationNeeded(err) { + return nil, fmt.Errorf("login failed: %w", err) + } + + // Register new peer with setup key + log.Debug("Peer not registered, registering with Setup-Key") + loginResp, err = mgmClient.Register(*serverKey, cfg.SetupKey, "", sysInfo, pubSSHKey, cfg.DNSLabels) + if err != nil { + return nil, fmt.Errorf("registration with setup key failed: %w", err) + } + + log.Info("Setup-Key bootstrap: peer registered successfully") + return &BootstrapResult{ + AuthMethod: AuthMethodSetupKey, + PeerConfig: loginResp.PeerConfig, + NetbirdConfig: loginResp.NetbirdConfig, + }, nil +} + +// bootstrapWithMTLS performs Phase 2 bootstrap using mTLS with machine certificate. +// This uses the RegisterMachinePeer RPC which is mTLS-only. +func bootstrapWithMTLS(ctx context.Context, cfg *MachineConfig) (*BootstrapResult, error) { + // Determine mTLS port + mtlsPort := cfg.MTLSPort + if mtlsPort == 0 { + mtlsPort = DefaultMTLSPort + } + + // Build mTLS URL + mtlsURL, err := buildMTLSURL(cfg.ManagementURL, mtlsPort) + if err != nil { + return nil, fmt.Errorf("failed to build mTLS URL: %w", err) + } + + log.Debugf("Connecting to management server %s with mTLS", mtlsURL) + + // Load client certificate + cert, err := tls.LoadX509KeyPair(cfg.ClientCertPath, cfg.ClientCertKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to load machine certificate: %w", err) + } + + // Create TLS config with client certificate + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + } + + // Create gRPC connection with mTLS + creds := credentials.NewTLS(tlsConfig) + conn, err := grpc.DialContext(ctx, mtlsURL, + grpc.WithTransportCredentials(creds), + grpc.WithBlock(), + ) + if err != nil { + return nil, fmt.Errorf("failed to connect with mTLS: %w", err) + } + defer conn.Close() + + // Create management service client + client := mgmProto.NewManagementServiceClient(conn) + + // Generate WireGuard key for this machine tunnel + wgKey, err := wgtypes.GenerateKey() + if err != nil { + return nil, fmt.Errorf("failed to generate WireGuard key: %w", err) + } + + // Get system info + sysInfo := system.GetInfo(ctx) + setSystemFlags(sysInfo, cfg.Config) + + // Build registration request + // Note: The machine identity is extracted from the mTLS certificate by the server, + // we don't need to send it explicitly + req := &mgmProto.MachineRegisterRequest{ + Meta: infoToProtoMeta(sysInfo), + WgPubKey: []byte(wgKey.PublicKey().String()), + } + + // Call RegisterMachinePeer RPC + resp, err := client.RegisterMachinePeer(ctx, req) + if err != nil { + return nil, fmt.Errorf("RegisterMachinePeer failed: %w", err) + } + + log.Infof("mTLS bootstrap successful: identity=%s", resp.MachineIdentity.DnsName) + + return &BootstrapResult{ + AuthMethod: AuthMethodMTLS, + PeerConfig: resp.PeerConfig, + NetbirdConfig: resp.NetbirdConfig, + MachineIdentity: resp.MachineIdentity, + AllowedDCRoutes: resp.AllowedDcRoutes, + DNSConfig: resp.DnsConfig, + }, nil +} + +// getMgmClient creates a standard management gRPC client. +func getMgmClient(ctx context.Context, config *profilemanager.Config) (*mgm.GrpcClient, error) { + myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to parse WireGuard private key: %w", err) + } + + tlsEnabled := config.ManagementURL.Scheme == "https" + + client, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, tlsEnabled) + if err != nil { + return nil, fmt.Errorf("failed to create management client: %w", err) + } + + return client, nil +} + +// buildMTLSURL constructs the mTLS endpoint URL from the management URL. +func buildMTLSURL(mgmURL *url.URL, mtlsPort int) (string, error) { + if mgmURL == nil { + return "", fmt.Errorf("management URL is nil") + } + + // Extract host without port + host := mgmURL.Hostname() + if host == "" { + return "", fmt.Errorf("empty host in management URL") + } + + return fmt.Sprintf("%s:%d", host, mtlsPort), nil +} + +// setSystemFlags sets the system flags from config. +func setSystemFlags(sysInfo *system.Info, config *profilemanager.Config) { + sysInfo.SetFlags( + config.RosenpassEnabled, + config.RosenpassPermissive, + config.ServerSSHAllowed, + config.DisableClientRoutes, + config.DisableServerRoutes, + config.DisableDNS, + config.DisableFirewall, + config.BlockLANAccess, + config.BlockInbound, + config.LazyConnectionEnabled, + config.EnableSSHRoot, + config.EnableSSHSFTP, + config.EnableSSHLocalPortForwarding, + config.EnableSSHRemotePortForwarding, + config.DisableSSHAuth, + ) +} + +// isRegistrationNeeded checks if the error indicates that peer registration is required. +func isRegistrationNeeded(err error) bool { + if err == nil { + return false + } + s, ok := status.FromError(err) + if !ok { + return false + } + return s.Code() == codes.PermissionDenied +} diff --git a/client/internal/tunnel/bootstrap_test.go b/client/internal/tunnel/bootstrap_test.go new file mode 100644 index 00000000000..43b1e70602c --- /dev/null +++ b/client/internal/tunnel/bootstrap_test.go @@ -0,0 +1,319 @@ +package tunnel + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/internal/profilemanager" +) + +func TestAuthMethodString(t *testing.T) { + tests := []struct { + method AuthMethod + expected string + }{ + {AuthMethodUnknown, "Unknown"}, + {AuthMethodSetupKey, "SetupKey"}, + {AuthMethodMTLS, "mTLS"}, + {AuthMethod(99), "Unknown"}, + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.method.String()) + }) + } +} + +func TestHasMachineCert_NoPaths(t *testing.T) { + cfg := &MachineConfig{ + Config: &profilemanager.Config{ + ClientCertPath: "", + ClientCertKeyPath: "", + }, + } + + assert.False(t, hasMachineCert(cfg)) +} + +func TestHasMachineCert_InvalidPath(t *testing.T) { + cfg := &MachineConfig{ + Config: &profilemanager.Config{ + ClientCertPath: "/nonexistent/cert.pem", + ClientCertKeyPath: "/nonexistent/key.pem", + }, + } + + assert.False(t, hasMachineCert(cfg)) +} + +func TestHasMachineCert_ValidCert(t *testing.T) { + // Create a temporary directory for test certificates + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "cert.pem") + keyPath := filepath.Join(tmpDir, "key.pem") + + // Generate a test certificate with SAN DNSName + err := generateTestCertificate(certPath, keyPath, []string{"test-machine.corp.local"}, time.Hour) + require.NoError(t, err) + + cfg := &MachineConfig{ + Config: &profilemanager.Config{ + ClientCertPath: certPath, + ClientCertKeyPath: keyPath, + }, + } + + assert.True(t, hasMachineCert(cfg)) +} + +func TestHasMachineCert_ExpiredCert(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "cert.pem") + keyPath := filepath.Join(tmpDir, "key.pem") + + // Generate an expired certificate (valid for -1 hour, i.e., already expired) + err := generateTestCertificateWithTimes(certPath, keyPath, []string{"test-machine.corp.local"}, + time.Now().Add(-2*time.Hour), time.Now().Add(-1*time.Hour)) + require.NoError(t, err) + + cfg := &MachineConfig{ + Config: &profilemanager.Config{ + ClientCertPath: certPath, + ClientCertKeyPath: keyPath, + }, + } + + assert.False(t, hasMachineCert(cfg)) +} + +func TestHasMachineCert_NoDNSNames(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "cert.pem") + keyPath := filepath.Join(tmpDir, "key.pem") + + // Generate a certificate without SAN DNSNames + err := generateTestCertificate(certPath, keyPath, nil, time.Hour) + require.NoError(t, err) + + cfg := &MachineConfig{ + Config: &profilemanager.Config{ + ClientCertPath: certPath, + ClientCertKeyPath: keyPath, + }, + } + + assert.False(t, hasMachineCert(cfg)) +} + +func TestHasMachineCert_ThumbprintMismatch(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "cert.pem") + keyPath := filepath.Join(tmpDir, "key.pem") + + err := generateTestCertificate(certPath, keyPath, []string{"test-machine.corp.local"}, time.Hour) + require.NoError(t, err) + + cfg := &MachineConfig{ + Config: &profilemanager.Config{ + ClientCertPath: certPath, + ClientCertKeyPath: keyPath, + }, + MachineCertThumbprint: "0000000000000000000000000000000000000000000000000000000000000000", + } + + assert.False(t, hasMachineCert(cfg)) +} + +func TestBuildMTLSURL(t *testing.T) { + tests := []struct { + name string + mgmURL string + mtlsPort int + expected string + expectErr bool + }{ + { + name: "standard URL", + mgmURL: "https://api.netbird.io:443", + mtlsPort: 33074, + expected: "api.netbird.io:33074", + }, + { + name: "URL without port", + mgmURL: "https://api.netbird.io", + mtlsPort: 33074, + expected: "api.netbird.io:33074", + }, + { + name: "localhost", + mgmURL: "https://localhost:33073", + mtlsPort: 33074, + expected: "localhost:33074", + }, + { + name: "IP address", + mgmURL: "https://192.168.1.100:443", + mtlsPort: 33074, + expected: "192.168.1.100:33074", + }, + { + name: "custom port", + mgmURL: "https://mgmt.example.com:8443", + mtlsPort: 8444, + expected: "mgmt.example.com:8444", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u, err := url.Parse(tt.mgmURL) + require.NoError(t, err) + + result, err := buildMTLSURL(u, tt.mtlsPort) + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestBuildMTLSURL_NilURL(t *testing.T) { + _, err := buildMTLSURL(nil, 33074) + assert.Error(t, err) + assert.Contains(t, err.Error(), "nil") +} + +func TestBootstrap_NilConfig(t *testing.T) { + ctx := context.Background() + _, err := Bootstrap(ctx, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "config is required") +} + +func TestBootstrap_NilEmbeddedConfig(t *testing.T) { + ctx := context.Background() + cfg := &MachineConfig{Config: nil} + _, err := Bootstrap(ctx, cfg) + assert.Error(t, err) + assert.Contains(t, err.Error(), "config is required") +} + +func TestBootstrap_NoSetupKeyNoCert(t *testing.T) { + ctx := context.Background() + cfg := &MachineConfig{ + Config: &profilemanager.Config{ + ClientCertPath: "", + ClientCertKeyPath: "", + }, + SetupKey: "", + } + _, err := Bootstrap(ctx, cfg) + assert.Error(t, err) + assert.Contains(t, err.Error(), "no machine certificate available and no setup key provided") +} + +func TestBootstrapWithSetupKey_InvalidSetupKey(t *testing.T) { + ctx := context.Background() + cfg := &MachineConfig{ + Config: &profilemanager.Config{ + ClientCertPath: "", + ClientCertKeyPath: "", + }, + SetupKey: "not-a-uuid", + } + _, err := bootstrapWithSetupKey(ctx, cfg) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid setup key format") +} + +func TestDefaultMTLSPort(t *testing.T) { + assert.Equal(t, 33074, DefaultMTLSPort) +} + +// Helper function to generate a test certificate +func generateTestCertificate(certPath, keyPath string, dnsNames []string, validity time.Duration) error { + return generateTestCertificateWithTimes(certPath, keyPath, dnsNames, + time.Now().Add(-time.Hour), time.Now().Add(validity)) +} + +func generateTestCertificateWithTimes(certPath, keyPath string, dnsNames []string, notBefore, notAfter time.Time) error { + // Generate ECDSA key + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return fmt.Errorf("failed to generate private key: %w", err) + } + + // Create certificate template + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return fmt.Errorf("failed to generate serial number: %w", err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "Test Machine", + Organization: []string{"Test Org"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + DNSNames: dnsNames, + } + + // Self-sign the certificate + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + return fmt.Errorf("failed to create certificate: %w", err) + } + + // Write certificate + certFile, err := os.Create(certPath) + if err != nil { + return fmt.Errorf("failed to create cert file: %w", err) + } + defer certFile.Close() + + if err := pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil { + return fmt.Errorf("failed to encode certificate: %w", err) + } + + // Write private key + keyFile, err := os.Create(keyPath) + if err != nil { + return fmt.Errorf("failed to create key file: %w", err) + } + defer keyFile.Close() + + keyDER, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + return fmt.Errorf("failed to marshal private key: %w", err) + } + + if err := pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}); err != nil { + return fmt.Errorf("failed to encode private key: %w", err) + } + + return nil +} diff --git a/client/internal/tunnel/certenroll.go b/client/internal/tunnel/certenroll.go new file mode 100644 index 00000000000..00eade70c04 --- /dev/null +++ b/client/internal/tunnel/certenroll.go @@ -0,0 +1,451 @@ +// Package tunnel provides machine tunnel functionality for Windows pre-login VPN. +package tunnel + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "os" + "strings" + "time" + + log "github.com/sirupsen/logrus" +) + +// CertEnrollmentConfig contains configuration for certificate enrollment. +type CertEnrollmentConfig struct { + // TemplateName is the AD CS certificate template name. + // Default: "NetBirdMachineTunnel" + TemplateName string + + // DomainName is the FQDN of the domain (e.g., "corp.local"). + DomainName string + + // Hostname is the machine hostname (without domain). + Hostname string + + // OutputCertPath is the path to write the enrolled certificate. + OutputCertPath string + + // OutputKeyPath is the path to write the private key. + OutputKeyPath string + + // ValidityCheck enables pre-enrollment validation. + ValidityCheck bool +} + +// CertEnrollmentResult contains the results of certificate enrollment. +type CertEnrollmentResult struct { + // Success indicates if enrollment succeeded. + Success bool + + // CertPath is the path to the enrolled certificate. + CertPath string + + // KeyPath is the path to the private key. + KeyPath string + + // Thumbprint is the SHA-256 thumbprint of the certificate. + Thumbprint string + + // Subject is the certificate subject. + Subject string + + // DNSNames are the SAN DNS names in the certificate. + DNSNames []string + + // NotBefore is the certificate validity start time. + NotBefore time.Time + + // NotAfter is the certificate validity end time. + NotAfter time.Time + + // Error contains any error that occurred. + Error error +} + +// DefaultCertTemplateName is the default AD CS template name. +const DefaultCertTemplateName = "NetBirdMachineTunnel" + +// CertRenewalThreshold is how long before expiry to trigger renewal (30 days). +const CertRenewalThreshold = 30 * 24 * time.Hour + +// MinCertValidity is the minimum acceptable certificate validity (7 days). +const MinCertValidity = 7 * 24 * time.Hour + +// ValidateMachineCertificate validates a machine certificate for use with mTLS. +// It checks: +// - Certificate exists and is readable +// - Certificate is not expired +// - Certificate has valid SAN DNSNames matching expected hostname.domain format +// - Certificate is signed by a trusted CA (optional, if caCert provided) +func ValidateMachineCertificate(certPath string, expectedHostname, expectedDomain string) (*CertEnrollmentResult, error) { + result := &CertEnrollmentResult{ + CertPath: certPath, + } + + // Read certificate file + certPEM, err := os.ReadFile(certPath) + if err != nil { + result.Error = fmt.Errorf("read certificate: %w", err) + return result, result.Error + } + + // Parse PEM block + block, _ := pem.Decode(certPEM) + if block == nil { + result.Error = fmt.Errorf("failed to decode PEM block") + return result, result.Error + } + + // Parse certificate + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + result.Error = fmt.Errorf("parse certificate: %w", err) + return result, result.Error + } + + // Fill in result fields + result.Subject = cert.Subject.String() + result.DNSNames = cert.DNSNames + result.NotBefore = cert.NotBefore + result.NotAfter = cert.NotAfter + result.Thumbprint = ComputeCertThumbprint(cert) + + // Check expiry + now := time.Now() + if now.Before(cert.NotBefore) { + result.Error = fmt.Errorf("certificate not yet valid (starts %s)", cert.NotBefore) + return result, result.Error + } + if now.After(cert.NotAfter) { + result.Error = fmt.Errorf("certificate expired (ended %s)", cert.NotAfter) + return result, result.Error + } + + // Check minimum validity remaining + remaining := cert.NotAfter.Sub(now) + if remaining < MinCertValidity { + log.Warnf("Certificate expires soon: %s remaining", remaining) + } + + // Check SAN DNSNames + if len(cert.DNSNames) == 0 { + result.Error = fmt.Errorf("certificate has no SAN DNSNames") + return result, result.Error + } + + // Validate expected hostname.domain format + expectedFQDN := strings.ToLower(fmt.Sprintf("%s.%s", expectedHostname, expectedDomain)) + foundMatch := false + for _, dnsName := range cert.DNSNames { + if strings.EqualFold(dnsName, expectedFQDN) { + foundMatch = true + break + } + } + + if !foundMatch { + result.Error = fmt.Errorf("certificate SAN DNSNames %v do not match expected %s", cert.DNSNames, expectedFQDN) + return result, result.Error + } + + result.Success = true + log.Infof("Certificate validation passed: %s (expires %s)", result.Subject, cert.NotAfter) + return result, nil +} + +// ComputeCertThumbprint computes the SHA-256 thumbprint of a certificate. +func ComputeCertThumbprint(cert *x509.Certificate) string { + hash := sha256.Sum256(cert.Raw) + return hex.EncodeToString(hash[:]) +} + +// NeedsRenewal checks if a certificate needs renewal. +func NeedsRenewal(certPath string) (bool, error) { + certPEM, err := os.ReadFile(certPath) + if err != nil { + return true, fmt.Errorf("read certificate: %w", err) + } + + block, _ := pem.Decode(certPEM) + if block == nil { + return true, fmt.Errorf("failed to decode PEM block") + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return true, fmt.Errorf("parse certificate: %w", err) + } + + remaining := time.Until(cert.NotAfter) + if remaining < CertRenewalThreshold { + log.Infof("Certificate renewal needed: %s remaining (threshold: %s)", remaining, CertRenewalThreshold) + return true, nil + } + + return false, nil +} + +// GenerateCertEnrollmentScript generates a PowerShell script for AD CS enrollment. +// This script uses certreq.exe which is available on domain-joined Windows machines. +func GenerateCertEnrollmentScript(config *CertEnrollmentConfig) string { + templateName := config.TemplateName + if templateName == "" { + templateName = DefaultCertTemplateName + } + + fqdn := fmt.Sprintf("%s.%s", config.Hostname, config.DomainName) + + script := fmt.Sprintf(`# Certificate Enrollment Script (Generated by NetBird Machine Tunnel) +# Prerequisites: Domain-joined, AD CS available, template "%s" configured + +$ErrorActionPreference = 'Stop' +$hostname = '%s' +$domain = '%s' +$fqdn = '%s' +$templateName = '%s' + +# Paths +$infPath = "$env:TEMP\netbird-certreq.inf" +$reqPath = "$env:TEMP\netbird-certreq.req" +$cerPath = "$env:TEMP\netbird-certreq.cer" +$pfxPath = "$env:TEMP\netbird-certreq.pfx" + +Write-Host "Enrolling machine certificate for: $fqdn" +Write-Host "Using template: $templateName" + +# Step 1: Create INF file for certificate request +$infContent = @" +[NewRequest] +Subject = "CN=$fqdn" +KeySpec = 1 +KeyLength = 2048 +Exportable = TRUE +MachineKeySet = TRUE +SMIME = FALSE +PrivateKeyArchive = FALSE +UserProtected = FALSE +UseExistingKeySet = FALSE +ProviderName = "Microsoft RSA SChannel Cryptographic Provider" +ProviderType = 12 +RequestType = PKCS10 +KeyUsage = 0xa0 +HashAlgorithm = SHA256 + +[EnhancedKeyUsageExtension] +OID = 1.3.6.1.5.5.7.3.2 ; Client Authentication + +[Extensions] +2.5.29.17 = "{text}" +_continue_ = "dns=$fqdn&" + +[RequestAttributes] +CertificateTemplate = $templateName +"@ + +Set-Content -Path $infPath -Value $infContent -Encoding ASCII +Write-Host "Created certificate request INF: $infPath" + +# Step 2: Generate certificate request +Write-Host "Generating certificate request..." +$result = certreq -new -machine $infPath $reqPath 2>&1 +if ($LASTEXITCODE -ne 0) { + throw "certreq -new failed: $result" +} +Write-Host "Created certificate request: $reqPath" + +# Step 3: Submit request to CA +Write-Host "Submitting request to CA..." +$result = certreq -submit -machine -config - $reqPath $cerPath 2>&1 +if ($LASTEXITCODE -ne 0) { + # Try with explicit CA discovery + Write-Host "Trying with CA auto-discovery..." + $result = certreq -submit -machine $reqPath $cerPath 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "certreq -submit failed: $result" + } +} +Write-Host "Received certificate: $cerPath" + +# Step 4: Accept certificate into store +Write-Host "Installing certificate..." +$result = certreq -accept -machine $cerPath 2>&1 +if ($LASTEXITCODE -ne 0) { + throw "certreq -accept failed: $result" +} +Write-Host "Certificate installed to LocalMachine\My" + +# Step 5: Find and export the certificate +$cert = Get-ChildItem Cert:\LocalMachine\My | + Where-Object { $_.Subject -match $fqdn } | + Sort-Object NotAfter -Descending | + Select-Object -First 1 + +if (-not $cert) { + throw "Could not find enrolled certificate in store" +} + +Write-Host "Certificate Details:" +Write-Host " Subject: $($cert.Subject)" +Write-Host " Thumbprint: $($cert.Thumbprint)" +Write-Host " Expires: $($cert.NotAfter)" +Write-Host " DNS Names: $($cert.DnsNameList -join ', ')" + +# Step 6: Export to PEM format (requires OpenSSL or manual conversion) +# For now, output the thumbprint for config update +$thumbprint = $cert.Thumbprint + +# Cleanup temp files +Remove-Item $infPath, $reqPath, $cerPath -ErrorAction SilentlyContinue + +# Return result +@{ + Success = $true + Thumbprint = $thumbprint + Subject = $cert.Subject + NotAfter = $cert.NotAfter + DnsNames = $cert.DnsNameList +} +`, templateName, config.Hostname, config.DomainName, fqdn, templateName) + + return script +} + +// CertificateInfo contains parsed certificate information. +type CertificateInfo struct { + // Thumbprint is the SHA-256 thumbprint. + Thumbprint string + + // Subject is the certificate subject DN. + Subject string + + // Issuer is the certificate issuer DN. + Issuer string + + // DNSNames are the SAN DNS names. + DNSNames []string + + // NotBefore is the validity start. + NotBefore time.Time + + // NotAfter is the validity end. + NotAfter time.Time + + // SerialNumber is the certificate serial number (hex encoded). + SerialNumber string + + // IsExpired indicates if the certificate is expired. + IsExpired bool + + // NeedsRenewal indicates if the certificate should be renewed. + NeedsRenewal bool + + // RemainingValidity is the time until expiry. + RemainingValidity time.Duration +} + +// ParseCertificateFile parses a PEM certificate file and returns info. +func ParseCertificateFile(certPath string) (*CertificateInfo, error) { + certPEM, err := os.ReadFile(certPath) + if err != nil { + return nil, fmt.Errorf("read certificate: %w", err) + } + + block, _ := pem.Decode(certPEM) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse certificate: %w", err) + } + + now := time.Now() + remaining := cert.NotAfter.Sub(now) + + info := &CertificateInfo{ + Thumbprint: ComputeCertThumbprint(cert), + Subject: cert.Subject.String(), + Issuer: cert.Issuer.String(), + DNSNames: cert.DNSNames, + NotBefore: cert.NotBefore, + NotAfter: cert.NotAfter, + SerialNumber: cert.SerialNumber.Text(16), + IsExpired: now.After(cert.NotAfter), + NeedsRenewal: remaining < CertRenewalThreshold, + RemainingValidity: remaining, + } + + return info, nil +} + +// WatchCertificateExpiry starts a goroutine that monitors certificate expiry +// and calls the callback when renewal is needed. +func WatchCertificateExpiry(ctx context.Context, certPath string, checkInterval time.Duration, onRenewalNeeded func()) { + ticker := time.NewTicker(checkInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + log.Debug("Certificate expiry watcher stopped") + return + case <-ticker.C: + needsRenewal, err := NeedsRenewal(certPath) + if err != nil { + log.Warnf("Certificate renewal check failed: %v", err) + continue + } + if needsRenewal { + log.Info("Certificate renewal needed, triggering callback") + onRenewalNeeded() + } + } + } +} + +// ExtractIssuerFingerprint extracts the issuer certificate fingerprint from a cert chain. +// This is used for mTLS issuer verification (not AuthorityKeyId!). +func ExtractIssuerFingerprint(certPath string, verifyChain bool) (string, error) { + certPEM, err := os.ReadFile(certPath) + if err != nil { + return "", fmt.Errorf("read certificate: %w", err) + } + + // Parse all certificates in the PEM file (may include chain) + var certs []*x509.Certificate + rest := certPEM + for { + block, remaining := pem.Decode(rest) + if block == nil { + break + } + if block.Type == "CERTIFICATE" { + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return "", fmt.Errorf("parse certificate: %w", err) + } + certs = append(certs, cert) + } + rest = remaining + } + + if len(certs) == 0 { + return "", fmt.Errorf("no certificates found in file") + } + + // If we have a chain, the issuer is the second certificate + if len(certs) > 1 { + issuerCert := certs[1] + return ComputeCertThumbprint(issuerCert), nil + } + + // Single certificate - issuer fingerprint would need to be looked up + // In production, this should verify against the system trust store + return "", fmt.Errorf("certificate chain required for issuer fingerprint extraction") +} diff --git a/client/internal/tunnel/certenroll_test.go b/client/internal/tunnel/certenroll_test.go new file mode 100644 index 00000000000..a583f364a56 --- /dev/null +++ b/client/internal/tunnel/certenroll_test.go @@ -0,0 +1,540 @@ +package tunnel + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDefaultCertTemplateName(t *testing.T) { + assert.Equal(t, "NetBirdMachineTunnel", DefaultCertTemplateName) +} + +func TestCertRenewalThreshold(t *testing.T) { + assert.Equal(t, 30*24*time.Hour, CertRenewalThreshold) +} + +func TestMinCertValidity(t *testing.T) { + assert.Equal(t, 7*24*time.Hour, MinCertValidity) +} + +func TestComputeCertThumbprint(t *testing.T) { + // Create a test certificate + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + err := generateTestCertWithDNSNames(certPath, keyPath, []string{"test.example.com"}, time.Hour*24) + require.NoError(t, err) + + // Read and parse certificate + certPEM, err := os.ReadFile(certPath) + require.NoError(t, err) + + block, _ := pem.Decode(certPEM) + require.NotNil(t, block) + + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err) + + // Compute thumbprint + thumbprint := ComputeCertThumbprint(cert) + + // Verify thumbprint is 64 hex characters (SHA-256) + assert.Len(t, thumbprint, 64) + assert.Regexp(t, "^[0-9a-f]+$", thumbprint) +} + +func TestValidateMachineCertificate_ValidCert(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + err := generateTestCertWithDNSNames(certPath, keyPath, []string{"testhost.example.com"}, time.Hour*24*365) + require.NoError(t, err) + + result, err := ValidateMachineCertificate(certPath, "testhost", "example.com") + + assert.NoError(t, err) + assert.True(t, result.Success) + assert.Contains(t, result.DNSNames, "testhost.example.com") + assert.NotEmpty(t, result.Thumbprint) +} + +func TestValidateMachineCertificate_ExpiredCert(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + // Generate an expired certificate + err := generateTestCertWithTimes(certPath, keyPath, []string{"testhost.example.com"}, + time.Now().Add(-48*time.Hour), time.Now().Add(-24*time.Hour)) + require.NoError(t, err) + + result, err := ValidateMachineCertificate(certPath, "testhost", "example.com") + + assert.Error(t, err) + assert.False(t, result.Success) + assert.Contains(t, err.Error(), "expired") +} + +func TestValidateMachineCertificate_NotYetValid(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + // Generate a certificate that's not yet valid + err := generateTestCertWithTimes(certPath, keyPath, []string{"testhost.example.com"}, + time.Now().Add(24*time.Hour), time.Now().Add(48*time.Hour)) + require.NoError(t, err) + + result, err := ValidateMachineCertificate(certPath, "testhost", "example.com") + + assert.Error(t, err) + assert.False(t, result.Success) + assert.Contains(t, err.Error(), "not yet valid") +} + +func TestValidateMachineCertificate_NoDNSNames(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + // Generate a certificate without DNS names + err := generateTestCertWithDNSNames(certPath, keyPath, nil, time.Hour*24) + require.NoError(t, err) + + result, err := ValidateMachineCertificate(certPath, "testhost", "example.com") + + assert.Error(t, err) + assert.False(t, result.Success) + assert.Contains(t, err.Error(), "no SAN DNSNames") +} + +func TestValidateMachineCertificate_WrongHostname(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + err := generateTestCertWithDNSNames(certPath, keyPath, []string{"otherhost.example.com"}, time.Hour*24) + require.NoError(t, err) + + result, err := ValidateMachineCertificate(certPath, "testhost", "example.com") + + assert.Error(t, err) + assert.False(t, result.Success) + assert.Contains(t, err.Error(), "do not match expected") +} + +func TestValidateMachineCertificate_FileNotFound(t *testing.T) { + result, err := ValidateMachineCertificate("/nonexistent/cert.pem", "test", "example.com") + + assert.Error(t, err) + assert.False(t, result.Success) + assert.Contains(t, err.Error(), "read certificate") +} + +func TestValidateMachineCertificate_InvalidPEM(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "invalid.pem") + + err := os.WriteFile(certPath, []byte("not a valid PEM"), 0600) + require.NoError(t, err) + + result, err := ValidateMachineCertificate(certPath, "test", "example.com") + + assert.Error(t, err) + assert.False(t, result.Success) + assert.Contains(t, err.Error(), "decode PEM") +} + +func TestNeedsRenewal_ValidCert(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + // Certificate valid for 1 year - doesn't need renewal + err := generateTestCertWithDNSNames(certPath, keyPath, []string{"test.example.com"}, time.Hour*24*365) + require.NoError(t, err) + + needsRenewal, err := NeedsRenewal(certPath) + + assert.NoError(t, err) + assert.False(t, needsRenewal) +} + +func TestNeedsRenewal_ExpiringSoon(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + // Certificate expires in 15 days - needs renewal (threshold is 30 days) + err := generateTestCertWithTimes(certPath, keyPath, []string{"test.example.com"}, + time.Now().Add(-time.Hour), time.Now().Add(15*24*time.Hour)) + require.NoError(t, err) + + needsRenewal, err := NeedsRenewal(certPath) + + assert.NoError(t, err) + assert.True(t, needsRenewal) +} + +func TestNeedsRenewal_FileNotFound(t *testing.T) { + needsRenewal, err := NeedsRenewal("/nonexistent/cert.pem") + + assert.Error(t, err) + assert.True(t, needsRenewal) // Should return true if we can't read the cert +} + +func TestParseCertificateFile(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + dnsNames := []string{"host.domain.local", "alias.domain.local"} + err := generateTestCertWithDNSNames(certPath, keyPath, dnsNames, time.Hour*24*365) + require.NoError(t, err) + + info, err := ParseCertificateFile(certPath) + + assert.NoError(t, err) + assert.NotEmpty(t, info.Thumbprint) + assert.NotEmpty(t, info.Subject) + assert.NotEmpty(t, info.Issuer) + assert.Equal(t, dnsNames, info.DNSNames) + assert.False(t, info.IsExpired) + assert.False(t, info.NeedsRenewal) + assert.True(t, info.RemainingValidity > 364*24*time.Hour) +} + +func TestParseCertificateFile_Expired(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + err := generateTestCertWithTimes(certPath, keyPath, []string{"test.example.com"}, + time.Now().Add(-48*time.Hour), time.Now().Add(-24*time.Hour)) + require.NoError(t, err) + + info, err := ParseCertificateFile(certPath) + + assert.NoError(t, err) + assert.True(t, info.IsExpired) + assert.True(t, info.NeedsRenewal) + assert.True(t, info.RemainingValidity < 0) +} + +func TestGenerateCertEnrollmentScript_Basic(t *testing.T) { + config := &CertEnrollmentConfig{ + Hostname: "win10-pc", + DomainName: "corp.local", + } + + script := GenerateCertEnrollmentScript(config) + + assert.Contains(t, script, "win10-pc.corp.local") + assert.Contains(t, script, DefaultCertTemplateName) + assert.Contains(t, script, "certreq -new") + assert.Contains(t, script, "certreq -submit") + assert.Contains(t, script, "certreq -accept") + assert.Contains(t, script, "Cert:\\LocalMachine\\My") +} + +func TestGenerateCertEnrollmentScript_CustomTemplate(t *testing.T) { + config := &CertEnrollmentConfig{ + Hostname: "server01", + DomainName: "example.com", + TemplateName: "CustomMachineTemplate", + } + + script := GenerateCertEnrollmentScript(config) + + assert.Contains(t, script, "CustomMachineTemplate") + assert.Contains(t, script, "server01.example.com") +} + +func TestGenerateCertEnrollmentScript_ContainsRequiredSteps(t *testing.T) { + config := &CertEnrollmentConfig{ + Hostname: "test", + DomainName: "test.local", + } + + script := GenerateCertEnrollmentScript(config) + + // Check that all required steps are present + assert.Contains(t, script, "Step 1: Create INF") + assert.Contains(t, script, "Step 2: Generate certificate request") + assert.Contains(t, script, "Step 3: Submit request") + assert.Contains(t, script, "Step 4: Accept certificate") + assert.Contains(t, script, "Step 5: Find and export") + assert.Contains(t, script, "Step 6: Export to PEM") + + // Check crypto requirements + assert.Contains(t, script, "KeyLength = 2048") + assert.Contains(t, script, "SHA256") + assert.Contains(t, script, "MachineKeySet = TRUE") +} + +func TestWatchCertificateExpiry(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + // Create a certificate that expires in 20 days (within renewal threshold) + err := generateTestCertWithTimes(certPath, keyPath, []string{"test.example.com"}, + time.Now().Add(-time.Hour), time.Now().Add(20*24*time.Hour)) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + var callbackCalled atomic.Bool + + go WatchCertificateExpiry(ctx, certPath, 500*time.Millisecond, func() { + callbackCalled.Store(true) + }) + + // Wait for at least one check + time.Sleep(1 * time.Second) + + assert.True(t, callbackCalled.Load(), "Callback should have been called for expiring cert") +} + +func TestWatchCertificateExpiry_NoRenewalNeeded(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + // Create a certificate valid for 1 year + err := generateTestCertWithDNSNames(certPath, keyPath, []string{"test.example.com"}, time.Hour*24*365) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + var callbackCalled atomic.Bool + + go WatchCertificateExpiry(ctx, certPath, 500*time.Millisecond, func() { + callbackCalled.Store(true) + }) + + // Wait for at least one check + time.Sleep(1 * time.Second) + + assert.False(t, callbackCalled.Load(), "Callback should NOT have been called for valid cert") +} + +func TestExtractIssuerFingerprint_SingleCert(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + err := generateTestCertWithDNSNames(certPath, keyPath, []string{"test.example.com"}, time.Hour*24) + require.NoError(t, err) + + // Single cert should return error (no issuer in chain) + _, err = ExtractIssuerFingerprint(certPath, true) + assert.Error(t, err) + assert.Contains(t, err.Error(), "certificate chain required") +} + +func TestExtractIssuerFingerprint_CertChain(t *testing.T) { + tmpDir := t.TempDir() + chainPath := filepath.Join(tmpDir, "chain.pem") + + // Generate CA and end-entity cert + caCert, caKey, err := generateCACertificate() + require.NoError(t, err) + + eeCert, _, err := generateSignedCertificate(caCert, caKey, []string{"test.example.com"}) + require.NoError(t, err) + + // Write chain (EE cert first, then CA) + chainPEM := append(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: eeCert.Raw}), + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw})...) + err = os.WriteFile(chainPath, chainPEM, 0600) + require.NoError(t, err) + + fingerprint, err := ExtractIssuerFingerprint(chainPath, true) + + assert.NoError(t, err) + assert.Len(t, fingerprint, 64) // SHA-256 hex + assert.Equal(t, ComputeCertThumbprint(caCert), fingerprint) +} + +func TestCertificateInfo_Fields(t *testing.T) { + info := &CertificateInfo{ + Thumbprint: "abc123", + Subject: "CN=test", + Issuer: "CN=CA", + DNSNames: []string{"test.local"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + SerialNumber: "1234", + IsExpired: false, + NeedsRenewal: false, + RemainingValidity: 24 * time.Hour, + } + + assert.Equal(t, "abc123", info.Thumbprint) + assert.Equal(t, "CN=test", info.Subject) + assert.Equal(t, "CN=CA", info.Issuer) + assert.Len(t, info.DNSNames, 1) + assert.False(t, info.IsExpired) +} + +func TestValidateMachineCertificate_CaseInsensitiveHostname(t *testing.T) { + tmpDir := t.TempDir() + certPath := filepath.Join(tmpDir, "test.pem") + keyPath := filepath.Join(tmpDir, "test.key") + + // Certificate has lowercase DNS name + err := generateTestCertWithDNSNames(certPath, keyPath, []string{"testhost.example.com"}, time.Hour*24*365) + require.NoError(t, err) + + // Validate with uppercase hostname - should still match + result, err := ValidateMachineCertificate(certPath, "TESTHOST", "EXAMPLE.COM") + + assert.NoError(t, err) + assert.True(t, result.Success) +} + +// Helper functions + +func generateTestCertWithDNSNames(certPath, keyPath string, dnsNames []string, validity time.Duration) error { + return generateTestCertWithTimes(certPath, keyPath, dnsNames, + time.Now().Add(-time.Hour), time.Now().Add(validity)) +} + +func generateTestCertWithTimes(certPath, keyPath string, dnsNames []string, notBefore, notAfter time.Time) error { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + + serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "Test Certificate", + Organization: []string{"Test Org"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + DNSNames: dnsNames, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + return err + } + + certFile, err := os.Create(certPath) + if err != nil { + return err + } + defer certFile.Close() + if err := pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil { + return err + } + + keyFile, err := os.Create(keyPath) + if err != nil { + return err + } + defer keyFile.Close() + keyDER, _ := x509.MarshalECPrivateKey(privateKey) + if err := pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}); err != nil { + return err + } + + return nil +} + +func generateCACertificate() (*x509.Certificate, *ecdsa.PrivateKey, error) { + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + + caTemplate := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "Test CA", + Organization: []string{"Test CA Org"}, + }, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour * 365), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 1, + } + + caDER, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey) + if err != nil { + return nil, nil, err + } + + caCert, err := x509.ParseCertificate(caDER) + if err != nil { + return nil, nil, err + } + + return caCert, caKey, nil +} + +func generateSignedCertificate(caCert *x509.Certificate, caKey *ecdsa.PrivateKey, dnsNames []string) (*x509.Certificate, *ecdsa.PrivateKey, error) { + eeKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + + eeTemplate := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: strings.Join(dnsNames, ", "), + Organization: []string{"Test Org"}, + }, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour * 30), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + DNSNames: dnsNames, + } + + eeDER, err := x509.CreateCertificate(rand.Reader, &eeTemplate, caCert, &eeKey.PublicKey, caKey) + if err != nil { + return nil, nil, err + } + + eeCert, err := x509.ParseCertificate(eeDER) + if err != nil { + return nil, nil, err + } + + return eeCert, eeKey, nil +} diff --git a/client/internal/tunnel/domainjoin.go b/client/internal/tunnel/domainjoin.go new file mode 100644 index 00000000000..9606fb269f4 --- /dev/null +++ b/client/internal/tunnel/domainjoin.go @@ -0,0 +1,346 @@ +// Package tunnel provides machine tunnel functionality for Windows pre-login VPN. +package tunnel + +import ( + "context" + "fmt" + "net" + "time" + + log "github.com/sirupsen/logrus" +) + +// DCConnectivityResult contains the results of DC connectivity checks. +type DCConnectivityResult struct { + // DCAddress is the IP or hostname of the Domain Controller. + DCAddress string + + // LDAPReachable indicates if LDAP (port 389) is reachable. + LDAPReachable bool + + // LDAPSReachable indicates if LDAPS (port 636) is reachable. + LDAPSReachable bool + + // KerberosReachable indicates if Kerberos (port 88) is reachable. + KerberosReachable bool + + // DNSReachable indicates if DNS (port 53) is reachable. + DNSReachable bool + + // SMBReachable indicates if SMB (port 445) is reachable for Sysvol/GPO. + SMBReachable bool + + // NTPReachable indicates if NTP (port 123 UDP) is reachable. + NTPReachable bool + + // AllRequired indicates if all required ports are reachable. + AllRequired bool + + // Errors contains any errors encountered during checks. + Errors []string +} + +// Required ports for Domain Controller connectivity. +const ( + PortLDAP = 389 + PortLDAPS = 636 + PortKerberos = 88 + PortDNS = 53 + PortSMB = 445 + PortNTP = 123 + PortRPCEPM = 135 // RPC Endpoint Mapper +) + +// domainJoinPromptMessage is the PowerShell Get-Credential prompt text. +// This is NOT a credential - it's a UI prompt string for the user. +const domainJoinPromptMessage = "Enter administrator account for domain join" //nolint:gosec + +// DefaultDCPorts are the ports required for basic DC connectivity. +var DefaultDCPorts = []int{PortLDAP, PortKerberos, PortDNS} + +// DefaultConnectTimeout is the default timeout for TCP connection tests. +const DefaultConnectTimeout = 5 * time.Second + +// MaxKerberosTimeSkew is the maximum allowed time skew for Kerberos (5 minutes). +const MaxKerberosTimeSkew = 5 * time.Minute + +// CheckDCConnectivity verifies that the Domain Controller is reachable via the tunnel. +// This should be called after the tunnel is established and before domain join. +func CheckDCConnectivity(ctx context.Context, dcAddress string, timeout time.Duration) *DCConnectivityResult { + if timeout == 0 { + timeout = DefaultConnectTimeout + } + + result := &DCConnectivityResult{ + DCAddress: dcAddress, + Errors: make([]string, 0), + } + + log.Infof("Checking DC connectivity to %s", dcAddress) + + // Check LDAP (TCP 389) - Required + result.LDAPReachable = checkTCPPort(ctx, dcAddress, PortLDAP, timeout) + if !result.LDAPReachable { + result.Errors = append(result.Errors, fmt.Sprintf("LDAP (port %d) not reachable", PortLDAP)) + } + + // Check Kerberos (TCP 88) - Required + result.KerberosReachable = checkTCPPort(ctx, dcAddress, PortKerberos, timeout) + if !result.KerberosReachable { + result.Errors = append(result.Errors, fmt.Sprintf("Kerberos (port %d) not reachable", PortKerberos)) + } + + // Check DNS (TCP 53) - Required + result.DNSReachable = checkTCPPort(ctx, dcAddress, PortDNS, timeout) + if !result.DNSReachable { + result.Errors = append(result.Errors, fmt.Sprintf("DNS (port %d) not reachable", PortDNS)) + } + + // Check LDAPS (TCP 636) - Optional + result.LDAPSReachable = checkTCPPort(ctx, dcAddress, PortLDAPS, timeout) + + // Check SMB (TCP 445) - Optional but recommended for GPO + result.SMBReachable = checkTCPPort(ctx, dcAddress, PortSMB, timeout) + + // Check NTP (UDP 123) - Important for Kerberos time sync + result.NTPReachable = checkUDPPort(ctx, dcAddress, PortNTP, timeout) + if !result.NTPReachable { + // NTP failure is a warning, not an error - time sync might work via other means + log.Warnf("NTP (port %d) not reachable - ensure time is synchronized", PortNTP) + } + + // All required = LDAP + Kerberos + DNS + result.AllRequired = result.LDAPReachable && result.KerberosReachable && result.DNSReachable + + if result.AllRequired { + log.Infof("DC connectivity check passed: all required ports reachable") + } else { + log.Errorf("DC connectivity check failed: %v", result.Errors) + } + + return result +} + +// checkTCPPort tests if a TCP port is reachable. +func checkTCPPort(ctx context.Context, host string, port int, timeout time.Duration) bool { + addr := net.JoinHostPort(host, fmt.Sprintf("%d", port)) + + // Create dialer with timeout + dialer := &net.Dialer{ + Timeout: timeout, + } + + conn, err := dialer.DialContext(ctx, "tcp", addr) + if err != nil { + log.Debugf("TCP port check failed: %s - %v", addr, err) + return false + } + defer conn.Close() + + log.Debugf("TCP port check passed: %s", addr) + return true +} + +// checkUDPPort tests if a UDP port is reachable (best effort - UDP is connectionless). +// This sends a small packet and checks if there's no immediate ICMP unreachable. +func checkUDPPort(ctx context.Context, host string, port int, timeout time.Duration) bool { + addr := net.JoinHostPort(host, fmt.Sprintf("%d", port)) + + conn, err := net.DialTimeout("udp", addr, timeout) + if err != nil { + log.Debugf("UDP port check failed: %s - %v", addr, err) + return false + } + defer conn.Close() + + // For NTP, send a basic NTP request to check if the service responds + if port == PortNTP { + return checkNTPService(conn, timeout) + } + + // For other UDP services, just check if we could create the connection + log.Debugf("UDP port check passed (connection established): %s", addr) + return true +} + +// checkNTPService sends a basic NTP request and checks for a response. +func checkNTPService(conn net.Conn, timeout time.Duration) bool { + // NTP v3 request packet (minimal) + // Li=0, VN=3, Mode=3 (client), Stratum=0, Poll=0, Precision=0 + ntpRequest := make([]byte, 48) + ntpRequest[0] = 0x1B // LI=0, VN=3, Mode=3 + + if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil { + log.Debugf("Failed to set NTP deadline: %v", err) + return false + } + + if _, err := conn.Write(ntpRequest); err != nil { + log.Debugf("Failed to send NTP request: %v", err) + return false + } + + response := make([]byte, 48) + n, err := conn.Read(response) + if err != nil { + log.Debugf("No NTP response: %v", err) + return false + } + + if n < 48 { + log.Debugf("Invalid NTP response size: %d", n) + return false + } + + log.Debug("NTP service check passed") + return true +} + +// PreJoinChecklist contains all pre-domain-join validation results. +type PreJoinChecklist struct { + // DCConnectivity is the DC connectivity check result. + DCConnectivity *DCConnectivityResult + + // TunnelUp indicates if the VPN tunnel is established. + TunnelUp bool + + // TimeInSync indicates if system time is within Kerberos tolerance. + TimeInSync bool + + // ReadyForJoin indicates if all checks pass and domain join can proceed. + ReadyForJoin bool + + // Errors contains any blocking errors. + Errors []string + + // Warnings contains non-blocking warnings. + Warnings []string +} + +// ValidatePreJoinRequirements performs all checks required before domain join. +func ValidatePreJoinRequirements(ctx context.Context, dcAddress string, tunnelUp bool) *PreJoinChecklist { + checklist := &PreJoinChecklist{ + TunnelUp: tunnelUp, + Errors: make([]string, 0), + Warnings: make([]string, 0), + } + + // Check 1: Tunnel must be up + if !tunnelUp { + checklist.Errors = append(checklist.Errors, "VPN tunnel is not established") + checklist.ReadyForJoin = false + return checklist + } + + // Check 2: DC connectivity + checklist.DCConnectivity = CheckDCConnectivity(ctx, dcAddress, DefaultConnectTimeout) + if !checklist.DCConnectivity.AllRequired { + checklist.Errors = append(checklist.Errors, checklist.DCConnectivity.Errors...) + } + + // Check 3: Time synchronization (warning only - actual sync is done by PowerShell) + // Note: We can't accurately check time skew from Go without NTP parsing + // The PowerShell script handles the actual time sync + if !checklist.DCConnectivity.NTPReachable { + checklist.Warnings = append(checklist.Warnings, + "NTP service not reachable - ensure time is synchronized before domain join") + } + // Assume time is in sync for the checklist - actual verification is done by PowerShell + checklist.TimeInSync = true + + // Determine if ready for join + checklist.ReadyForJoin = tunnelUp && + checklist.DCConnectivity.AllRequired && + len(checklist.Errors) == 0 + + if checklist.ReadyForJoin { + log.Info("Pre-join checklist passed - ready for domain join") + } else { + log.Errorf("Pre-join checklist failed: %v", checklist.Errors) + } + + return checklist +} + +// DomainJoinConfig contains configuration for domain join. +type DomainJoinConfig struct { + // DomainName is the FQDN of the domain (e.g., "corp.local"). + DomainName string + + // DCAddress is the IP address of the Domain Controller. + DCAddress string + + // OUPath is the optional OU path for the computer object. + // Format: "OU=Computers,DC=corp,DC=local" + OUPath string + + // RestartAfterJoin indicates if the system should restart after join. + RestartAfterJoin bool + + // UseCredentials indicates if credentials should be prompted. + // If false, uses the current user's credentials. + UseCredentials bool +} + +// GenerateDomainJoinScript generates a PowerShell script for domain join. +// This is meant to be executed by the Windows service or an elevated process. +func GenerateDomainJoinScript(config *DomainJoinConfig) string { + restartFlag := "$false" + if config.RestartAfterJoin { + restartFlag = "$true" + } + + credentialPart := "" + if config.UseCredentials { + credentialPart = fmt.Sprintf(" -Credential (Get-Credential -Message '%s')", domainJoinPromptMessage) + } + + ouPart := "" + if config.OUPath != "" { + ouPart = fmt.Sprintf(" -OUPath '%s'", config.OUPath) + } + + script := fmt.Sprintf(`# Domain Join Script (Generated by NetBird Machine Tunnel) +# Prerequisites: Tunnel up, DC reachable, time synchronized + +$ErrorActionPreference = 'Stop' +$dcIP = '%s' +$domain = '%s' + +# Step 1: Verify DC connectivity +Write-Host "Verifying DC connectivity..." + +if (-not (Test-NetConnection -ComputerName $dcIP -Port 389 -WarningAction SilentlyContinue).TcpTestSucceeded) { + throw "LDAP (port 389) not reachable - check tunnel status" +} + +if (-not (Test-NetConnection -ComputerName $dcIP -Port 88 -WarningAction SilentlyContinue).TcpTestSucceeded) { + throw "Kerberos (port 88) not reachable - check tunnel status" +} + +Write-Host "DC connectivity verified." + +# Step 2: Configure NTP via DC (before domain join) +Write-Host "Configuring NTP sync..." +try { + w32tm /config /manualpeerlist:"$dcIP" /syncfromflags:manual /reliable:no /update + Restart-Service W32Time -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + w32tm /resync /nowait + Write-Host "NTP configured successfully." +} catch { + Write-Warning "NTP configuration failed: $_" +} + +# Step 3: Domain Join +Write-Host "Joining domain: $domain" +try { + Add-Computer -DomainName $domain%s%s -Restart:%s -Force + Write-Host "Domain join successful!" +} catch { + throw "Domain join failed: $_" +} +`, config.DCAddress, config.DomainName, ouPart, credentialPart, restartFlag) + + return script +} diff --git a/client/internal/tunnel/domainjoin_test.go b/client/internal/tunnel/domainjoin_test.go new file mode 100644 index 00000000000..06788824de2 --- /dev/null +++ b/client/internal/tunnel/domainjoin_test.go @@ -0,0 +1,267 @@ +package tunnel + +import ( + "context" + "fmt" + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDefaultPorts(t *testing.T) { + assert.Equal(t, 389, PortLDAP) + assert.Equal(t, 636, PortLDAPS) + assert.Equal(t, 88, PortKerberos) + assert.Equal(t, 53, PortDNS) + assert.Equal(t, 445, PortSMB) + assert.Equal(t, 123, PortNTP) + assert.Equal(t, 135, PortRPCEPM) +} + +func TestCheckTCPPort_Reachable(t *testing.T) { + // Start a local TCP listener + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + + // Extract the port + addr := listener.Addr().(*net.TCPAddr) + + ctx := context.Background() + result := checkTCPPort(ctx, "127.0.0.1", addr.Port, time.Second) + assert.True(t, result) +} + +func TestCheckTCPPort_Unreachable(t *testing.T) { + ctx := context.Background() + // Use a port that's unlikely to be open + result := checkTCPPort(ctx, "127.0.0.1", 59999, 100*time.Millisecond) + assert.False(t, result) +} + +func TestCheckTCPPort_ContextCanceled(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately + + result := checkTCPPort(ctx, "127.0.0.1", 80, time.Second) + assert.False(t, result) +} + +func TestCheckDCConnectivity_AllUnreachable(t *testing.T) { + ctx := context.Background() + // Use an IP that doesn't exist (TEST-NET-1) + result := CheckDCConnectivity(ctx, "192.0.2.1", 100*time.Millisecond) + + assert.Equal(t, "192.0.2.1", result.DCAddress) + assert.False(t, result.LDAPReachable) + assert.False(t, result.KerberosReachable) + assert.False(t, result.DNSReachable) + assert.False(t, result.AllRequired) + assert.NotEmpty(t, result.Errors) +} + +func TestCheckDCConnectivity_DefaultTimeout(t *testing.T) { + ctx := context.Background() + // This tests that zero timeout uses default + result := CheckDCConnectivity(ctx, "192.0.2.1", 0) + // Just verify it doesn't panic and returns a result + assert.NotNil(t, result) +} + +func TestValidatePreJoinRequirements_TunnelDown(t *testing.T) { + ctx := context.Background() + checklist := ValidatePreJoinRequirements(ctx, "192.168.100.20", false) + + assert.False(t, checklist.TunnelUp) + assert.False(t, checklist.ReadyForJoin) + assert.Contains(t, checklist.Errors, "VPN tunnel is not established") +} + +func TestValidatePreJoinRequirements_TunnelUpDCUnreachable(t *testing.T) { + ctx := context.Background() + // Use TEST-NET-1 which is unreachable + checklist := ValidatePreJoinRequirements(ctx, "192.0.2.1", true) + + assert.True(t, checklist.TunnelUp) + assert.False(t, checklist.ReadyForJoin) + assert.NotEmpty(t, checklist.Errors) +} + +func TestPreJoinChecklist_Fields(t *testing.T) { + checklist := &PreJoinChecklist{ + TunnelUp: true, + TimeInSync: true, + Errors: []string{}, + Warnings: []string{"test warning"}, + } + + assert.True(t, checklist.TunnelUp) + assert.True(t, checklist.TimeInSync) + assert.Empty(t, checklist.Errors) + assert.Len(t, checklist.Warnings, 1) +} + +func TestDCConnectivityResult_Fields(t *testing.T) { + result := &DCConnectivityResult{ + DCAddress: "192.168.100.20", + LDAPReachable: true, + KerberosReachable: true, + DNSReachable: true, + SMBReachable: false, + AllRequired: true, + Errors: []string{}, + } + + assert.Equal(t, "192.168.100.20", result.DCAddress) + assert.True(t, result.LDAPReachable) + assert.True(t, result.KerberosReachable) + assert.True(t, result.DNSReachable) + assert.False(t, result.SMBReachable) + assert.True(t, result.AllRequired) +} + +func TestGenerateDomainJoinScript_Basic(t *testing.T) { + config := &DomainJoinConfig{ + DomainName: "corp.local", + DCAddress: "192.168.100.20", + RestartAfterJoin: false, + UseCredentials: false, + } + + script := GenerateDomainJoinScript(config) + + assert.Contains(t, script, "192.168.100.20") + assert.Contains(t, script, "corp.local") + assert.Contains(t, script, "Add-Computer") + assert.Contains(t, script, "Test-NetConnection") + assert.Contains(t, script, "w32tm") + assert.Contains(t, script, "-Restart:$false") +} + +func TestGenerateDomainJoinScript_WithRestart(t *testing.T) { + config := &DomainJoinConfig{ + DomainName: "corp.local", + DCAddress: "192.168.100.20", + RestartAfterJoin: true, + UseCredentials: false, + } + + script := GenerateDomainJoinScript(config) + + assert.Contains(t, script, "-Restart:$true") +} + +func TestGenerateDomainJoinScript_WithCredentials(t *testing.T) { + config := &DomainJoinConfig{ + DomainName: "corp.local", + DCAddress: "192.168.100.20", + RestartAfterJoin: false, + UseCredentials: true, + } + + script := GenerateDomainJoinScript(config) + + assert.Contains(t, script, "Get-Credential") + assert.Contains(t, script, "-Credential") +} + +func TestGenerateDomainJoinScript_WithOUPath(t *testing.T) { + config := &DomainJoinConfig{ + DomainName: "corp.local", + DCAddress: "192.168.100.20", + OUPath: "OU=Workstations,DC=corp,DC=local", + RestartAfterJoin: false, + UseCredentials: false, + } + + script := GenerateDomainJoinScript(config) + + assert.Contains(t, script, "-OUPath") + assert.Contains(t, script, "OU=Workstations,DC=corp,DC=local") +} + +func TestGenerateDomainJoinScript_FullConfig(t *testing.T) { + config := &DomainJoinConfig{ + DomainName: "test.local", + DCAddress: "10.0.0.1", + OUPath: "OU=Computers,DC=test,DC=local", + RestartAfterJoin: true, + UseCredentials: true, + } + + script := GenerateDomainJoinScript(config) + + // Verify all components are present + assert.Contains(t, script, "test.local") + assert.Contains(t, script, "10.0.0.1") + assert.Contains(t, script, "OU=Computers,DC=test,DC=local") + assert.Contains(t, script, "-Restart:$true") + assert.Contains(t, script, "-Credential") + + // Verify the script structure + assert.Contains(t, script, "# Step 1: Verify DC connectivity") + assert.Contains(t, script, "# Step 2: Configure NTP") + assert.Contains(t, script, "# Step 3: Domain Join") +} + +func TestMaxKerberosTimeSkew(t *testing.T) { + assert.Equal(t, 5*time.Minute, MaxKerberosTimeSkew) +} + +func TestDefaultConnectTimeout(t *testing.T) { + assert.Equal(t, 5*time.Second, DefaultConnectTimeout) +} + +// TestLocalServerDCConnectivity simulates a partial DC by starting local listeners +func TestLocalServerDCConnectivity(t *testing.T) { + // Start mock LDAP server + ldapListener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer ldapListener.Close() + ldapPort := ldapListener.Addr().(*net.TCPAddr).Port + + // Start mock Kerberos server + kerberosListener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer kerberosListener.Close() + kerberosPort := kerberosListener.Addr().(*net.TCPAddr).Port + + // Start mock DNS server + dnsListener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer dnsListener.Close() + dnsPort := dnsListener.Addr().(*net.TCPAddr).Port + + // Test individual port checks + ctx := context.Background() + timeout := 100 * time.Millisecond + + assert.True(t, checkTCPPort(ctx, "127.0.0.1", ldapPort, timeout), "LDAP port should be reachable") + assert.True(t, checkTCPPort(ctx, "127.0.0.1", kerberosPort, timeout), "Kerberos port should be reachable") + assert.True(t, checkTCPPort(ctx, "127.0.0.1", dnsPort, timeout), "DNS port should be reachable") + + // Close ports to verify unreachability detection + ldapListener.Close() + assert.False(t, checkTCPPort(ctx, "127.0.0.1", ldapPort, timeout), "LDAP port should be unreachable after close") +} + +// Test that the result struct properly captures partial connectivity +func TestPartialDCConnectivity(t *testing.T) { + // Create a result manually to test logic + result := &DCConnectivityResult{ + DCAddress: "test.dc", + LDAPReachable: true, + KerberosReachable: false, // Missing Kerberos + DNSReachable: true, + Errors: []string{fmt.Sprintf("Kerberos (port %d) not reachable", PortKerberos)}, + } + + // Calculate AllRequired + result.AllRequired = result.LDAPReachable && result.KerberosReachable && result.DNSReachable + + assert.False(t, result.AllRequired, "AllRequired should be false when Kerberos is missing") + assert.Len(t, result.Errors, 1) +} diff --git a/client/internal/tunnel/interface_other.go b/client/internal/tunnel/interface_other.go new file mode 100644 index 00000000000..432f1e69b73 --- /dev/null +++ b/client/internal/tunnel/interface_other.go @@ -0,0 +1,43 @@ +//go:build !windows + +// Package tunnel provides machine tunnel functionality for Windows pre-login VPN. +package tunnel + +import ( + "fmt" + "net" +) + +const ( + // MachineInterfaceName is the desired name for the machine tunnel interface. + MachineInterfaceName = "wg-nb-machine" + + // WireGuardDescription is the adapter description (Windows-specific). + WireGuardDescription = "WireGuard Tunnel" +) + +// InterfaceInfo contains information about a discovered WireGuard interface. +type InterfaceInfo struct { + Name string + GUID string + LUID uint64 + Index int + Addresses []net.IPNet + IsUp bool + MTU int +} + +// FindWireGuardInterface is not supported on non-Windows platforms. +func FindWireGuardInterface(guid string) (*InterfaceInfo, error) { + return nil, fmt.Errorf("FindWireGuardInterface is only supported on Windows") +} + +// VerifyInterface is not supported on non-Windows platforms. +func VerifyInterface(info *InterfaceInfo) error { + return fmt.Errorf("VerifyInterface is only supported on Windows") +} + +// HasRouteToNetwork is not supported on non-Windows platforms. +func HasRouteToNetwork(info *InterfaceInfo, network string) (bool, error) { + return false, fmt.Errorf("HasRouteToNetwork is only supported on Windows") +} diff --git a/client/internal/tunnel/interface_windows.go b/client/internal/tunnel/interface_windows.go new file mode 100644 index 00000000000..5ba3c8a32c3 --- /dev/null +++ b/client/internal/tunnel/interface_windows.go @@ -0,0 +1,307 @@ +//go:build windows + +// Package tunnel provides machine tunnel functionality for Windows pre-login VPN. +package tunnel + +import ( + "fmt" + "net" + "strings" + + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" + "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" +) + +const ( + // MachineInterfaceName is the desired name for the machine tunnel interface. + MachineInterfaceName = "wg-nb-machine" + + // WireGuardDescription is the Windows adapter description for WireGuard interfaces. + WireGuardDescription = "WireGuard Tunnel" +) + +// InterfaceInfo contains information about a discovered WireGuard interface. +type InterfaceInfo struct { + // Name is the interface name (e.g., "wg-nb-machine"). + Name string + + // GUID is the Windows interface GUID. + GUID string + + // LUID is the Windows Local Unique Identifier. + LUID uint64 + + // Index is the interface index. + Index int + + // Addresses are the IP addresses assigned to the interface. + Addresses []net.IPNet + + // IsUp indicates whether the interface is up and running. + IsUp bool + + // MTU is the Maximum Transmission Unit. + MTU int +} + +// FindWireGuardInterface finds the machine tunnel WireGuard interface. +// It uses a priority-based search: +// 1. By GUID (most reliable, survives renames) +// 2. By Description (Windows adapter description) +// 3. By Name prefix (fallback) +func FindWireGuardInterface(guid string) (*InterfaceInfo, error) { + // Method 1: Try to find by GUID first (most reliable) + if guid != "" { + iface, err := findByGUID(guid) + if err == nil && iface != nil { + log.Debugf("Found interface by GUID: %s -> %s", guid, iface.Name) + return iface, nil + } + log.Debugf("Could not find interface by GUID %s: %v", guid, err) + } + + // Method 2: Find by WireGuard description + iface, err := findByDescription(WireGuardDescription) + if err == nil && iface != nil { + log.Debugf("Found interface by description: %s -> %s", WireGuardDescription, iface.Name) + return iface, nil + } + log.Debugf("Could not find interface by description: %v", err) + + // Method 3: Find by name prefix + iface, err = findByNamePrefix(MachineInterfaceName) + if err == nil && iface != nil { + log.Debugf("Found interface by name prefix: %s", iface.Name) + return iface, nil + } + log.Debugf("Could not find interface by name prefix: %v", err) + + return nil, fmt.Errorf("no WireGuard interface found") +} + +// findByGUID finds an interface by its Windows GUID. +func findByGUID(guidStr string) (*InterfaceInfo, error) { + guid, err := windows.GUIDFromString(guidStr) + if err != nil { + return nil, fmt.Errorf("invalid GUID format: %w", err) + } + + luid, err := winipcfg.LUIDFromGUID(&guid) + if err != nil { + return nil, fmt.Errorf("LUID from GUID failed: %w", err) + } + + return getInterfaceInfoFromLUID(luid) +} + +// findByDescription finds an interface by its Windows adapter description. +func findByDescription(description string) (*InterfaceInfo, error) { + interfaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("list interfaces: %w", err) + } + + for _, iface := range interfaces { + // Get Windows-specific adapter info + row, err := getAdapterRow(iface.Index) + if err != nil { + continue + } + + if strings.Contains(row.Description, description) { + return buildInterfaceInfo(&iface, row) + } + } + + return nil, fmt.Errorf("no interface with description %q found", description) +} + +// findByNamePrefix finds an interface by name prefix. +func findByNamePrefix(prefix string) (*InterfaceInfo, error) { + interfaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("list interfaces: %w", err) + } + + for _, iface := range interfaces { + if strings.HasPrefix(iface.Name, prefix) { + row, err := getAdapterRow(iface.Index) + if err != nil { + // Even if we can't get adapter row, return basic info + return &InterfaceInfo{ + Name: iface.Name, + Index: iface.Index, + MTU: iface.MTU, + IsUp: iface.Flags&net.FlagUp != 0, + }, nil + } + return buildInterfaceInfo(&iface, row) + } + } + + return nil, fmt.Errorf("no interface with name prefix %q found", prefix) +} + +// getInterfaceInfoFromLUID builds interface info from a Windows LUID. +func getInterfaceInfoFromLUID(luid winipcfg.LUID) (*InterfaceInfo, error) { + row, err := luid.Interface() + if err != nil { + return nil, fmt.Errorf("get interface row: %w", err) + } + + // Get the interface by index + iface, err := net.InterfaceByIndex(int(row.InterfaceIndex)) + if err != nil { + return nil, fmt.Errorf("get interface by index: %w", err) + } + + // Get addresses + addrs, err := iface.Addrs() + if err != nil { + log.Warnf("Failed to get addresses for interface %s: %v", iface.Name, err) + } + + ipNets := make([]net.IPNet, 0, len(addrs)) + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + ipNets = append(ipNets, *ipnet) + } + } + + // Get GUID string + guid, err := luid.GUID() + if err != nil { + log.Warnf("Failed to get GUID for interface %s: %v", iface.Name, err) + } + + guidStr := "" + if guid != nil { + guidStr = guid.String() + } + + return &InterfaceInfo{ + Name: iface.Name, + GUID: guidStr, + LUID: uint64(luid), + Index: iface.Index, + Addresses: ipNets, + IsUp: iface.Flags&net.FlagUp != 0, + MTU: iface.MTU, + }, nil +} + +// adapterRow holds Windows adapter row info. +type adapterRow struct { + Description string + GUID string + LUID uint64 +} + +// getAdapterRow gets Windows-specific adapter information. +func getAdapterRow(index int) (*adapterRow, error) { + // Use winipcfg to get adapter info + luid, err := winipcfg.LUIDFromIndex(uint32(index)) + if err != nil { + return nil, fmt.Errorf("LUID from index: %w", err) + } + + guid, err := luid.GUID() + if err != nil { + return nil, fmt.Errorf("get GUID: %w", err) + } + + guidStr := "" + if guid != nil { + guidStr = guid.String() + } + + // Get the interface to extract the alias/description + iface, err := luid.Interface() + if err != nil { + return nil, fmt.Errorf("get interface: %w", err) + } + + // Use the Alias method to get description + return &adapterRow{ + Description: iface.Alias(), + GUID: guidStr, + LUID: uint64(luid), + }, nil +} + +// buildInterfaceInfo builds InterfaceInfo from net.Interface and adapter row. +func buildInterfaceInfo(iface *net.Interface, row *adapterRow) (*InterfaceInfo, error) { + addrs, err := iface.Addrs() + if err != nil { + log.Warnf("Failed to get addresses for interface %s: %v", iface.Name, err) + } + + ipNets := make([]net.IPNet, 0, len(addrs)) + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + ipNets = append(ipNets, *ipnet) + } + } + + return &InterfaceInfo{ + Name: iface.Name, + GUID: row.GUID, + LUID: row.LUID, + Index: iface.Index, + Addresses: ipNets, + IsUp: iface.Flags&net.FlagUp != 0, + MTU: iface.MTU, + }, nil +} + +// VerifyInterface checks that the interface is properly configured. +func VerifyInterface(info *InterfaceInfo) error { + if info == nil { + return fmt.Errorf("interface info is nil") + } + + if !info.IsUp { + return fmt.Errorf("interface %s is not up", info.Name) + } + + if len(info.Addresses) == 0 { + return fmt.Errorf("interface %s has no IP addresses", info.Name) + } + + log.Infof("Interface %s verified: up=%v, addresses=%d, MTU=%d", + info.Name, info.IsUp, len(info.Addresses), info.MTU) + + return nil +} + +// HasRouteToNetwork checks if the interface has a route to the specified network. +// Note: This is a simplified check that verifies the interface is associated with +// a route to the target network. Full route verification is done via PowerShell tests. +func HasRouteToNetwork(info *InterfaceInfo, network string) (bool, error) { + if info == nil { + return false, fmt.Errorf("interface info is nil") + } + + _, cidr, err := net.ParseCIDR(network) + if err != nil { + return false, fmt.Errorf("parse network CIDR: %w", err) + } + + // Check if any of the interface's addresses are in the same network family + // Full route verification is done via PowerShell in the test suite + for _, addr := range info.Addresses { + if addr.IP.To4() != nil && cidr.IP.To4() != nil { + // Both are IPv4 - interface could potentially route to this network + log.Debugf("Interface %s has IPv4 address %s, target network %s", + info.Name, addr.IP.String(), network) + return true, nil + } + } + + // If no matching address family, assume route might still exist + // (routes can be added without local addresses in the same range) + log.Debugf("Interface %s may have route to %s (no local address check)", + info.Name, network) + return true, nil +} diff --git a/docs/ADR-001-mTLS-Port-Strategy.md b/docs/ADR-001-mTLS-Port-Strategy.md new file mode 100644 index 00000000000..5ac8e297c21 --- /dev/null +++ b/docs/ADR-001-mTLS-Port-Strategy.md @@ -0,0 +1,139 @@ +# ADR-001: mTLS Port Strategy + +**Status:** Accepted +**Date:** 2026-01-19 +**Issue:** #14 (T-1.2: NetBird gRPC mTLS Interception) + +## Context + +The Machine Tunnel Fork adds mTLS (mutual TLS) authentication for machine peers using AD CS certificates. We need to decide how to expose the mTLS-authenticated gRPC endpoints. + +**Options considered:** + +### Option A: Single Port with Method-Based Routing +- Standard gRPC port (33073) handles both token-auth and mTLS +- TLS config uses `VerifyClientCertIfGiven` (not `RequireAndVerifyClientCert`) +- gRPC interceptors check if method requires mTLS +- Methods in `mTLSRequiredMethods` map reject requests without valid cert +- Other methods fall back to token authentication + +### Option B: Dual Port +- Standard port (33073) for token authentication +- Separate port (33074) for mTLS-only authentication +- Each port has its own TLS config +- Simpler routing but more network config + +## Decision + +**We chose Option A: Single Port with Method-Based Routing** + +## Rationale + +1. **Simpler Deployment:** Only one port to configure in firewalls and load balancers +2. **Graceful Fallback:** Machines without certificates can still use Setup-Key auth for bootstrap +3. **Existing Infrastructure:** Works with existing NetBird deployments without port changes +4. **TLS Flexibility:** `VerifyClientCertIfGiven` allows: + - Clients WITH certs: Verified against CA pool, mTLS identity extracted + - Clients WITHOUT certs: TLS handshake succeeds, fall back to token auth + +## Implementation + +### TLS Configuration (boot.go) +```go +config := &tls.Config{ + ClientAuth: tls.VerifyClientCertIfGiven, // NOT RequireAndVerifyClientCert + ClientCAs: caCertPool, + // ... +} +``` + +### Method-Based Requirements (mtls_auth.go) +```go +var mTLSRequiredMethods = map[string]bool{ + "/management.ManagementService/RegisterMachinePeer": true, + "/management.ManagementService/SyncMachinePeer": true, + "/management.ManagementService/GetMachineRoutes": true, + "/management.ManagementService/ReportMachineStatus": true, +} +``` + +### Interceptor Logic +``` +Request arrives + | + +-> Extract mTLS identity from TLS state + | | + | +-> Success: Identity available + | | | + | | +-> Store in context, proceed + | | + | +-> Failure: No cert or invalid + | | + | +-> Method requires mTLS? + | | | + | | +-> YES: Return Unauthenticated error + | | +-> NO: Fall back to token auth +``` + +## Consequences + +### Positive +- Bootstrap flow works: Machine can use Setup-Key initially, then mTLS after cert enrollment +- No network reconfiguration needed for existing deployments +- Single source of truth for mTLS-required methods + +### Negative +- Slightly more complex interceptor logic +- All methods receive TLS handshake overhead (minimal) + +### Risks +- **Misconfiguration:** If `mTLSRequiredMethods` is not kept in sync with proto definitions + - Mitigation: Code review, integration tests +- **CA Pool exhaustion:** Large multi-tenant deployments with many CAs + - Mitigation: MTLSCADir supports multiple CA files, can scale + +## Related Decisions + +- ADR-002 (pending): Machine Identity Extraction Strategy (SAN DNSName vs CN) +- ADR-003 (pending): Multi-Tenant CA Isolation + +## Implementation Status + +### Completed +- [x] `management/internals/server/mtls_auth.go` - gRPC interceptors +- [x] `management/internals/server/config/config.go` - mTLS config fields +- [x] `management/internals/server/boot.go` - TLS config + interceptor chain +- [x] `shared/management/proto/management.proto` - Machine RPC definitions + +### TODO: Proto Code Generation +The proto file has been updated but **Go code needs to be regenerated**: + +```bash +cd shared/management/proto +./generate.sh +``` + +**Prerequisites:** +- `protoc` (Protocol Buffers compiler) +- `protoc-gen-go` (installed by generate.sh) +- `protoc-gen-go-grpc` (installed by generate.sh) + +**New Messages added:** +- `MachineIdentity` - Certificate identity info +- `MachineRegisterRequest/Response` - Registration with mTLS +- `MachineSyncRequest/Response` - Sync stream for machine peers +- `MachineRoutesRequest/Response` - Route retrieval +- `MachineStatusRequest/Response` - Health reporting +- `MachineUpdateType` - Enum for sync update types + +**New RPCs added:** +- `RegisterMachinePeer` - mTLS-required +- `SyncMachinePeer` - mTLS-required +- `GetMachineRoutes` - mTLS-required +- `ReportMachineStatus` - mTLS-required + +## References + +- [Go TLS ClientAuthType](https://pkg.go.dev/crypto/tls#ClientAuthType) +- [gRPC Interceptors](https://grpc.io/docs/guides/interceptors/) +- [NetBird gRPC Server](management/internals/server/boot.go) diff --git a/docs/ADR-002-CNG-Signer-Interface.md b/docs/ADR-002-CNG-Signer-Interface.md new file mode 100644 index 00000000000..870691b66eb --- /dev/null +++ b/docs/ADR-002-CNG-Signer-Interface.md @@ -0,0 +1,132 @@ +# ADR-002: Windows CNG crypto.Signer Interface + +**Status:** Pending (requires Windows environment) +**Date:** 2026-01-20 +**Issue:** T-1.1 (Windows CNG crypto.Signer Spike) + +## Context + +Machine Tunnel needs to use Windows machine certificates stored in the Windows Certificate Store for mTLS authentication. Go's standard `crypto/tls` expects a `crypto.Signer` interface, but Windows certificates use CNG (Cryptography Next Generation) APIs where private keys are not exportable. + +## Problem + +1. Windows machine certificates are stored in `LocalMachine\My` certificate store +2. Private keys are managed by CNG (`ncrypt.dll`) and marked as non-exportable +3. Go's `tls.Certificate` expects either: + - `PrivateKey` as `crypto.Signer` (preferred) + - Or raw key bytes (not possible with non-exportable keys) + +## Proposed Solution + +Implement a `CNG crypto.Signer` wrapper that: +1. Opens the certificate from Windows Cert Store via `crypt32.dll` +2. Gets the private key handle via `ncrypt.dll` +3. Implements `crypto.Signer.Sign()` by calling `NCryptSignHash()` + +### Interface Definition + +```go +// cng_signer_windows.go + +package auth + +import ( + "crypto" + "io" +) + +// CNGSigner implements crypto.Signer using Windows CNG APIs. +// This allows using non-exportable machine certificates for mTLS. +type CNGSigner struct { + keyHandle uintptr // NCRYPT_KEY_HANDLE + publicKey crypto.PublicKey + certThumbprint string +} + +// Public returns the public key. +func (s *CNGSigner) Public() crypto.PublicKey { + return s.publicKey +} + +// Sign signs digest with the private key via NCryptSignHash. +func (s *CNGSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + // TODO: Implement via NCryptSignHash + // - Determine padding based on opts (PKCS1v15 vs PSS) + // - Call NCryptSignHash with appropriate flags + // - Return signature bytes +} + +// NewCNGSignerFromThumbprint loads a certificate by thumbprint and returns a signer. +func NewCNGSignerFromThumbprint(thumbprint string) (*CNGSigner, *x509.Certificate, error) { + // TODO: Implement + // 1. CertOpenStore(CERT_STORE_PROV_SYSTEM, "MY", CERT_SYSTEM_STORE_LOCAL_MACHINE) + // 2. CertFindCertificateInStore(thumbprint) + // 3. CryptAcquireCertificatePrivateKey() + // 4. Extract public key from certificate + // 5. Return CNGSigner wrapping the key handle +} +``` + +### Required Windows APIs + +| API | DLL | Purpose | +|-----|-----|---------| +| `CertOpenStore` | crypt32.dll | Open certificate store | +| `CertFindCertificateInStore` | crypt32.dll | Find cert by thumbprint | +| `CryptAcquireCertificatePrivateKey` | crypt32.dll | Get private key handle | +| `NCryptSignHash` | ncrypt.dll | Sign with CNG key | +| `NCryptFreeObject` | ncrypt.dll | Release key handle | + +### Dependencies + +```go +import "golang.org/x/sys/windows" +``` + +## Implementation Notes + +### Build Constraints +```go +//go:build windows + +package auth +``` + +### Stub for Non-Windows +```go +//go:build !windows + +package auth + +func NewCNGSignerFromThumbprint(thumbprint string) (*CNGSigner, *x509.Certificate, error) { + return nil, nil, errors.New("CNG signer only available on Windows") +} +``` + +### Testing Requirements +- Requires Windows VM with: + - AD CS enrolled machine certificate + - Certificate in `LocalMachine\My` store + - Non-exportable private key + +## Status + +**BLOCKED:** Implementation requires Windows development environment. + +### Prerequisites +1. Windows 10/11 or Server 2019+ VM +2. Machine certificate enrolled via AD CS +3. Go 1.21+ with CGO enabled (for syscall) + +### Next Steps (on Windows VM) +1. Create `client/internal/auth/cng_signer_windows.go` +2. Implement Windows API calls via `golang.org/x/sys/windows` +3. Test with real AD CS certificate +4. Add unit tests with mock certificate store + +## References + +- [NCryptSignHash](https://learn.microsoft.com/en-us/windows/win32/api/ncrypt/nf-ncrypt-ncryptsignhash) +- [CryptAcquireCertificatePrivateKey](https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptacquirecertificateprivatekey) +- [golang.org/x/sys/windows](https://pkg.go.dev/golang.org/x/sys/windows) +- [Go crypto.Signer](https://pkg.go.dev/crypto#Signer) diff --git a/management/Dockerfile.multistage b/management/Dockerfile.multistage new file mode 100644 index 00000000000..c97daba13c7 --- /dev/null +++ b/management/Dockerfile.multistage @@ -0,0 +1,13 @@ +FROM golang:1.25 AS builder +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build -ldflags="-s -w" -o /netbird-mgmt ./management/ + +FROM ubuntu:24.04 +RUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=builder /netbird-mgmt /go/bin/netbird-mgmt +RUN chmod +x /go/bin/netbird-mgmt +ENTRYPOINT ["/go/bin/netbird-mgmt", "management"] +CMD ["--log-file", "console"] diff --git a/management/internals/server/boot.go b/management/internals/server/boot.go index 5d312ef94fe..814b1e7bbcf 100644 --- a/management/internals/server/boot.go +++ b/management/internals/server/boot.go @@ -5,8 +5,12 @@ package server import ( "context" "crypto/tls" + "crypto/x509" + "fmt" "net/http" "net/netip" + "os" + "path/filepath" "slices" "time" @@ -120,11 +124,29 @@ func (s *BaseServer) GRPCServer() *grpc.Server { realip.WithTrustedProxiesCount(trustedProxiesCount), realip.WithHeaders([]string{realip.XForwardedFor, realip.XRealIp}), } + + // Build interceptor chains + // Machine Tunnel Fork: Add mTLS interceptors when enabled + unaryInterceptors := []grpc.UnaryServerInterceptor{ + realip.UnaryServerInterceptorOpts(realipOpts...), + unaryInterceptor, + } + streamInterceptors := []grpc.StreamServerInterceptor{ + realip.StreamServerInterceptorOpts(realipOpts...), + streamInterceptor, + } + + if s.Config.HttpConfig.MTLSEnabled { + log.Info("mTLS authentication enabled for machine peers") + unaryInterceptors = append(unaryInterceptors, MTLSUnaryInterceptor(s.Config.HttpConfig.MTLSStrictMode)) + streamInterceptors = append(streamInterceptors, MTLSStreamInterceptor(s.Config.HttpConfig.MTLSStrictMode)) + } + gRPCOpts := []grpc.ServerOption{ grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp), - grpc.ChainUnaryInterceptor(realip.UnaryServerInterceptorOpts(realipOpts...), unaryInterceptor), - grpc.ChainStreamInterceptor(realip.StreamServerInterceptorOpts(realipOpts...), streamInterceptor), + grpc.ChainUnaryInterceptor(unaryInterceptors...), + grpc.ChainStreamInterceptor(streamInterceptors...), } if s.Config.HttpConfig.LetsEncryptDomain != "" { @@ -135,9 +157,25 @@ func (s *BaseServer) GRPCServer() *grpc.Server { transportCredentials := credentials.NewTLS(certManager.TLSConfig()) gRPCOpts = append(gRPCOpts, grpc.Creds(transportCredentials)) } else if s.Config.HttpConfig.CertFile != "" && s.Config.HttpConfig.CertKey != "" { - tlsConfig, err := loadTLSConfig(s.Config.HttpConfig.CertFile, s.Config.HttpConfig.CertKey) - if err != nil { - log.Fatalf("cannot load TLS credentials: %v", err) + var tlsConfig *tls.Config + var err error + + // Machine Tunnel Fork: Use mTLS config when enabled + if s.Config.HttpConfig.MTLSEnabled { + tlsConfig, err = loadMTLSConfig( + s.Config.HttpConfig.CertFile, + s.Config.HttpConfig.CertKey, + s.Config.HttpConfig.MTLSCACertFile, + s.Config.HttpConfig.MTLSCADir, + ) + if err != nil { + log.Fatalf("cannot load mTLS credentials: %v", err) + } + } else { + tlsConfig, err = loadTLSConfig(s.Config.HttpConfig.CertFile, s.Config.HttpConfig.CertKey) + if err != nil { + log.Fatalf("cannot load TLS credentials: %v", err) + } } transportCredentials := credentials.NewTLS(tlsConfig) gRPCOpts = append(gRPCOpts, grpc.Creds(transportCredentials)) @@ -173,6 +211,94 @@ func loadTLSConfig(certFile string, certKey string) (*tls.Config, error) { return config, nil } +// loadMTLSConfig creates a TLS config with client certificate verification enabled. +// Machine Tunnel Fork: This enables mTLS for machine peer authentication. +func loadMTLSConfig(certFile, certKey, caCertFile, caDir string) (*tls.Config, error) { + // Load server's certificate and private key + serverCert, err := tls.LoadX509KeyPair(certFile, certKey) + if err != nil { + return nil, err + } + + // Load CA certificate pool for client verification + caCertPool, err := loadCACertPool(caCertFile, caDir) + if err != nil { + return nil, err + } + + // Use VerifyClientCertIfGiven instead of RequireAndVerifyClientCert + // This allows: + // - Clients WITH certificates: verified against CA pool + // - Clients WITHOUT certificates: TLS handshake succeeds, interceptor handles auth + // This enables fallback to Setup-Key auth for bootstrap + config := &tls.Config{ + Certificates: []tls.Certificate{serverCert}, + ClientAuth: tls.VerifyClientCertIfGiven, + ClientCAs: caCertPool, + NextProtos: []string{ + "h2", "http/1.1", // enable HTTP/2 + }, + } + + return config, nil +} + +// loadCACertPool loads CA certificates from a file and/or directory. +// This supports multi-tenant scenarios where different customers have different CAs. +func loadCACertPool(caCertFile, caDir string) (*x509.CertPool, error) { + caCertPool := x509.NewCertPool() + certsLoaded := 0 + + // Load single CA cert file if specified + if caCertFile != "" { + caCert, err := os.ReadFile(caCertFile) + if err != nil { + return nil, fmt.Errorf("read CA cert file: %w", err) + } + if !caCertPool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("failed to parse CA certificate from %s", caCertFile) + } + certsLoaded++ + log.Infof("Loaded mTLS CA certificate from %s", caCertFile) + } + + // Load all .crt and .pem files from CA directory + if caDir != "" { + entries, err := os.ReadDir(caDir) + if err != nil { + return nil, fmt.Errorf("read CA directory: %w", err) + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + ext := filepath.Ext(entry.Name()) + if ext != ".crt" && ext != ".pem" { + continue + } + + certPath := filepath.Join(caDir, entry.Name()) + caCert, err := os.ReadFile(certPath) + if err != nil { + log.Warnf("Failed to read CA cert %s: %v", certPath, err) + continue + } + if caCertPool.AppendCertsFromPEM(caCert) { + certsLoaded++ + log.Infof("Loaded mTLS CA certificate from %s", certPath) + } + } + } + + if certsLoaded == 0 { + return nil, fmt.Errorf("no CA certificates loaded (caCertFile=%s, caDir=%s)", caCertFile, caDir) + } + + log.Infof("mTLS CA pool loaded with %d certificate(s)", certsLoaded) + return caCertPool, nil +} + func unaryInterceptor( ctx context.Context, req interface{}, diff --git a/management/internals/server/config/config.go b/management/internals/server/config/config.go index 7b87839436f..e0d32ef60c5 100644 --- a/management/internals/server/config/config.go +++ b/management/internals/server/config/config.go @@ -117,6 +117,33 @@ type HttpServerConfig struct { IdpSignKeyRefreshEnabled bool // Extra audience ExtraAuthAudience string + + // Machine Tunnel Fork - mTLS Configuration + // MTLSEnabled enables client certificate authentication for machine peers + MTLSEnabled bool + // MTLSPort is the dedicated port for mTLS-only Machine Tunnel clients (default: 33074) + // When set, a separate gRPC server runs on this port with RequireAndVerifyClientCert + MTLSPort int + // MTLSCACertFile is the CA certificate file for validating client certificates + MTLSCACertFile string + // MTLSCADir is a directory containing CA certificates (for multi-tenant support) + MTLSCADir string + // MTLSStrictMode if true, ALL requests require client certificate (no fallback) + // if false, only mTLS-required methods need certificates, others fall back to token auth + MTLSStrictMode bool + // MTLSDomainAccountMapping maps AD domains to NetBird account IDs + // Example: {"corp.local": "account-uuid-1", "test.local": "account-uuid-1"} + // CRITICAL: This prevents cross-tenant certificate acceptance! + MTLSDomainAccountMapping map[string]string + // MTLSAccountAllowedDomains maps account IDs to their allowed domains + // Example: {"account-uuid-1": ["corp.local", "test.local"]} + // If not set, domains are derived from MTLSDomainAccountMapping + MTLSAccountAllowedDomains map[string][]string + // MTLSAccountAllowedIssuers maps account IDs to their allowed CA issuer fingerprints (SHA256) + // Example: {"account-uuid-1": ["abc123...", "def456..."]} + // CRITICAL: If set, only certificates issued by these CAs are accepted for the account + // If empty for an account, issuer validation is SKIPPED (NOT RECOMMENDED for production!) + MTLSAccountAllowedIssuers map[string][]string } // Host represents a Netbird host (e.g. STUN, TURN, Signal) diff --git a/management/internals/server/mtls_auth.go b/management/internals/server/mtls_auth.go new file mode 100644 index 00000000000..b166e07fa15 --- /dev/null +++ b/management/internals/server/mtls_auth.go @@ -0,0 +1,679 @@ +package server + +// Machine Tunnel Fork - mTLS Authentication for Machine Peers +// This file implements gRPC interceptors for client certificate authentication. + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "fmt" + "strings" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/management/internals/shared/mtls" +) + +// MTLSIdentity is an alias for the shared mtls.Identity type +// Kept for backwards compatibility within this package +type MTLSIdentity = mtls.Identity + +// MTLSIdentityKey is an alias for the shared mtls.IdentityKey +// Kept for backwards compatibility +var MTLSIdentityKey = mtls.IdentityKey + +// GetMTLSIdentity retrieves the mTLS identity from context. +// This is an alias for mtls.GetIdentity for backwards compatibility. +func GetMTLSIdentity(ctx context.Context) *MTLSIdentity { + return mtls.GetIdentity(ctx) +} + +// mTLSRequiredMethods defines which gRPC methods REQUIRE client certificate authentication. +// These methods will reject requests without a valid client certificate. +// Other methods will fall back to token-based authentication. +var mTLSRequiredMethods = map[string]bool{ + "/management.ManagementService/RegisterMachinePeer": true, + "/management.ManagementService/SyncMachinePeer": true, + "/management.ManagementService/GetMachineRoutes": true, + "/management.ManagementService/ReportMachineStatus": true, +} + +// MTLSConfig holds the mTLS configuration for domain-account mapping. +// This is set during server initialization from the config file. +type MTLSConfig struct { + // DomainAccountMapping maps AD domains to NetBird account IDs + DomainAccountMapping map[string]string + // AccountAllowedDomains maps account IDs to their allowed domains + AccountAllowedDomains map[string][]string + // AccountAllowedIssuers maps account IDs to their allowed CA issuer fingerprints (SHA256) + // CRITICAL: If set for an account, only certificates from these CAs are accepted + // If empty for an account, issuer validation is SKIPPED (warned, NOT RECOMMENDED for production!) + AccountAllowedIssuers map[string][]string +} + +// globalMTLSConfig is the server-wide mTLS configuration. +// Set via SetMTLSConfig during server startup. +var globalMTLSConfig *MTLSConfig + +// SetMTLSConfig sets the global mTLS configuration. +// Must be called during server initialization before handling requests. +func SetMTLSConfig(cfg *MTLSConfig) { + globalMTLSConfig = cfg + log.Infof("mTLS config loaded: %d domain mappings, %d account configs", + len(cfg.DomainAccountMapping), len(cfg.AccountAllowedDomains)) +} + +// getAccountIDFromDomain maps a domain to its NetBird account ID. +// Returns error if domain is not mapped to any account. +// CRITICAL: This mapping prevents cross-tenant certificate acceptance! +func getAccountIDFromDomain(domain string) (string, error) { + if globalMTLSConfig == nil { + return "", fmt.Errorf("mTLS config not initialized") + } + + // Normalize domain to lowercase for case-insensitive matching + normalizedDomain := strings.ToLower(domain) + + accountID, ok := globalMTLSConfig.DomainAccountMapping[normalizedDomain] + if !ok { + return "", fmt.Errorf("domain %q not mapped to any account", domain) + } + + return accountID, nil +} + +// getAllowedDomainsForAccount returns the list of allowed domains for an account. +// Returns nil if no domains are configured (which means REJECT ALL - fail-safe!). +// CRITICAL: This is the security boundary for multi-tenant isolation! +func getAllowedDomainsForAccount(accountID string) []string { + if globalMTLSConfig == nil { + log.Warn("mTLS config not initialized, rejecting all domains") + return nil + } + + // First check explicit account configuration + if domains, ok := globalMTLSConfig.AccountAllowedDomains[accountID]; ok { + return domains + } + + // Fallback: derive allowed domains from DomainAccountMapping + // (all domains that map to this account are allowed) + var domains []string + for domain, accID := range globalMTLSConfig.DomainAccountMapping { + if accID == accountID { + domains = append(domains, domain) + } + } + + if len(domains) == 0 { + log.Warnf("No allowed domains found for account %s", accountID) + } + + return domains +} + +// validateDomainForAccount checks if a domain is allowed for the given account. +// Returns the matched allowed domain pattern (for audit logging) or error. +func validateDomainForAccount(domain, accountID string) (string, error) { + allowedDomains := getAllowedDomainsForAccount(accountID) + if len(allowedDomains) == 0 { + return "", fmt.Errorf("no allowed domains configured for account %s", accountID) + } + + normalizedDomain := strings.ToLower(domain) + for _, allowed := range allowedDomains { + if strings.ToLower(allowed) == normalizedDomain { + return allowed, nil + } + } + + return "", fmt.Errorf("domain %q not in allowed list for account %s: %v", + domain, accountID, allowedDomains) +} + +// ValidateIssuerCA validates that the certificate issuer is authorized for the given account. +// CRITICAL: This is a security boundary for multi-tenant isolation! +// Uses SHA256 fingerprint comparison (NOT string matching on DN which can be spoofed!) +// +// Returns nil if issuer is valid, error otherwise. +// Per Security Review: Empty allowlist = DENY (not any-CA!) for production safety. +func ValidateIssuerCA(accountID, issuerFingerprint string) error { + if globalMTLSConfig == nil { + return fmt.Errorf("mTLS config not initialized - cannot validate issuer") + } + + // Get allowed issuers for this account + allowedIssuers := globalMTLSConfig.AccountAllowedIssuers[accountID] + + // Security: Empty allowlist = DENY (fail-safe for production) + // Per Security Review: Explicit configuration required, no "any CA" fallback + if len(allowedIssuers) == 0 { + log.Warnf("SECURITY: Account %s has no MTLSAccountAllowedIssuers configured - rejecting certificate (explicit config required)", accountID) + return fmt.Errorf("no allowed CA issuers configured for account %s - explicit MTLSAccountAllowedIssuers configuration required", accountID) + } + + // Normalize fingerprint for comparison (lowercase hex) + normalizedFP := strings.ToLower(issuerFingerprint) + + // Check against allowed issuers + for _, allowed := range allowedIssuers { + if strings.ToLower(allowed) == normalizedFP { + log.Debugf("Issuer CA validated for account %s (FP: %s...)", accountID, normalizedFP[:16]) + return nil + } + } + + // Log truncated fingerprint for security (don't expose full FP in logs) + fpPreview := normalizedFP + if len(fpPreview) > 16 { + fpPreview = fpPreview[:16] + "..." + } + return fmt.Errorf("certificate issuer CA (FP: %s) not in allowed list for account %s", fpPreview, accountID) +} + +// checkMultiAccountSpan detects if a certificate's SANs span multiple accounts. +// This is a security warning - certificates should belong to a single account. +func checkMultiAccountSpan(dnsNames []string) { + seenAccounts := make(map[string]bool) + for _, dnsName := range dnsNames { + _, domain, err := splitDNSName(dnsName) + if err != nil { + continue + } + accountID, err := getAccountIDFromDomain(domain) + if err == nil { + seenAccounts[accountID] = true + } + } + + if len(seenAccounts) > 1 { + accounts := make([]string, 0, len(seenAccounts)) + for acc := range seenAccounts { + accounts = append(accounts, acc) + } + log.Warnf("SECURITY: Certificate spans multiple accounts: %v (SANs: %v). "+ + "Using first valid match only.", accounts, dnsNames) + } +} + +// MTLSUnaryInterceptor creates a gRPC unary interceptor for mTLS authentication. +// If strictMode is true, ALL requests require a client certificate. +// If strictMode is false, only methods in mTLSRequiredMethods require a certificate. +func MTLSUnaryInterceptor(strictMode bool) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, + info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + + identity, err := extractMTLSIdentity(ctx) + methodRequiresMTLS := mTLSRequiredMethods[info.FullMethod] + + if err != nil { + if methodRequiresMTLS { + log.WithContext(ctx).Warnf("mTLS required for %s but no valid cert: %v", info.FullMethod, err) + return nil, status.Errorf(codes.Unauthenticated, + "method %s requires client certificate authentication", info.FullMethod) + } + if strictMode { + log.WithContext(ctx).Warnf("mTLS strict mode: rejecting request without cert") + return nil, status.Errorf(codes.Unauthenticated, "client certificate required") + } + // Non-strict + non-required: allow fallback to token auth + log.WithContext(ctx).Tracef("No mTLS cert for %s, falling back to token auth", info.FullMethod) + return handler(ctx, req) + } + + log.WithContext(ctx).Debugf("mTLS authenticated: %s (issuer: %s...)", + identity.DNSName, identity.IssuerFingerprint[:16]) + + ctx = context.WithValue(ctx, MTLSIdentityKey, identity) + return handler(ctx, req) + } +} + +// MTLSStreamInterceptor creates a gRPC stream interceptor for mTLS authentication. +func MTLSStreamInterceptor(strictMode bool) grpc.StreamServerInterceptor { + return func(srv interface{}, ss grpc.ServerStream, + info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + + ctx := ss.Context() + identity, err := extractMTLSIdentity(ctx) + methodRequiresMTLS := mTLSRequiredMethods[info.FullMethod] + + if err != nil { + if methodRequiresMTLS { + log.WithContext(ctx).Warnf("mTLS required for %s but no valid cert: %v", info.FullMethod, err) + return status.Errorf(codes.Unauthenticated, + "method %s requires client certificate authentication", info.FullMethod) + } + if strictMode { + return status.Errorf(codes.Unauthenticated, "client certificate required") + } + return handler(srv, ss) + } + + log.WithContext(ctx).Debugf("mTLS stream authenticated: %s", identity.DNSName) + + // Wrap stream with identity context + wrapped := &mtlsServerStream{ + ServerStream: ss, + ctx: context.WithValue(ctx, MTLSIdentityKey, identity), + } + return handler(srv, wrapped) + } +} + +// mtlsServerStream wraps a grpc.ServerStream to inject mTLS identity into context +type mtlsServerStream struct { + grpc.ServerStream + ctx context.Context +} + +func (s *mtlsServerStream) Context() context.Context { + return s.ctx +} + +// extractMTLSIdentity extracts the machine identity from a client certificate. +// It validates the certificate and extracts the SAN DNSName as the primary identity. +func extractMTLSIdentity(ctx context.Context) (*MTLSIdentity, error) { + p, ok := peer.FromContext(ctx) + if !ok { + return nil, fmt.Errorf("no peer info in context") + } + + tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo) + if !ok { + return nil, fmt.Errorf("no TLS info in peer") + } + + // VerifiedChains is populated when ClientAuth >= VerifyClientCertIfGiven + // and the client provided a certificate that passed verification + if len(tlsInfo.State.VerifiedChains) == 0 { + return nil, fmt.Errorf("no verified certificate chains") + } + + // First chain, first cert is the client certificate + chain := tlsInfo.State.VerifiedChains[0] + if len(chain) == 0 { + return nil, fmt.Errorf("empty certificate chain") + } + + clientCert := chain[0] + + // Extract SAN DNSName as primary identity (NOT CN!) + // CN is deprecated and can be easily spoofed or left empty + if len(clientCert.DNSNames) == 0 { + return nil, fmt.Errorf("certificate has no SAN DNSName") + } + + // Security: Check if certificate SANs span multiple accounts (logging only) + checkMultiAccountSpan(clientCert.DNSNames) + + // Find first valid SAN that maps to an account and passes validation + var validDNSName, validHostname, validDomain, accountID, matchedDomain string + var validationErr error + + for _, dnsName := range clientCert.DNSNames { + hostname, domain, err := splitDNSName(dnsName) + if err != nil { + log.Debugf("Skipping invalid SAN DNSName %q: %v", dnsName, err) + continue + } + + // Try to get account ID from domain + accID, err := getAccountIDFromDomain(domain) + if err != nil { + log.Debugf("SAN %q: domain not mapped to account: %v", dnsName, err) + validationErr = err + continue + } + + // Validate domain against account's allowed domains + matched, err := validateDomainForAccount(domain, accID) + if err != nil { + log.Debugf("SAN %q: domain validation failed: %v", dnsName, err) + validationErr = err + continue + } + + // Found valid SAN! + validDNSName = dnsName + validHostname = hostname + validDomain = domain + accountID = accID + matchedDomain = matched + log.Debugf("mTLS: Valid SAN found: %s (account: %s, matched: %s)", + dnsName, accountID, matchedDomain) + break + } + + // If no valid SAN was found, return the last validation error + // or a generic error if mTLS config is not set up + if validDNSName == "" { + switch { + case globalMTLSConfig == nil: + // mTLS config not set - fall back to simple validation (first valid FQDN) + dnsName := clientCert.DNSNames[0] + hostname, domain, err := splitDNSName(dnsName) + if err != nil { + return nil, fmt.Errorf("invalid SAN DNSName format: %w", err) + } + validDNSName = dnsName + validHostname = hostname + validDomain = domain + log.Debugf("mTLS config not set, using first valid SAN: %s", dnsName) + case validationErr != nil: + return nil, fmt.Errorf("no valid SAN DNSName for configured accounts: %w", validationErr) + default: + return nil, fmt.Errorf("certificate has no SAN DNSName matching configured domains") + } + } + + // Compute issuer fingerprint from VerifiedChains (strong binding) + // NOT from AuthorityKeyId which can be spoofed! + issuerFP := "" + if len(chain) > 1 { + issuerCert := chain[1] + hash := sha256.Sum256(issuerCert.Raw) + issuerFP = fmt.Sprintf("%x", hash) + } + + // Extract template info (OID from v2, Name from v1) + templateOID := extractTemplateOID(clientCert) + templateName := extractTemplateNameV1(clientCert) + + // Determine peer type from template + peerType := determinePeerType(templateOID, templateName, clientCert) + + identity := &MTLSIdentity{ + DNSName: validDNSName, + Hostname: validHostname, + Domain: validDomain, + MatchedDomain: matchedDomain, + AccountID: accountID, + IssuerFingerprint: issuerFP, + SerialNumber: clientCert.SerialNumber.String(), + TemplateOID: templateOID, + TemplateName: templateName, + PeerType: peerType, + } + + return identity, nil +} + +// splitDNSName splits a FQDN into hostname and domain parts. +// Example: "win10-pc.corp.local" -> ("win10-pc", "corp.local") +func splitDNSName(dnsName string) (hostname, domain string, err error) { + parts := strings.SplitN(dnsName, ".", 2) + if len(parts) < 2 { + return "", "", fmt.Errorf("DNSName must be FQDN (hostname.domain): %s", dnsName) + } + return parts[0], parts[1], nil +} + +// extractTemplateOID extracts the certificate template OID from extensions. +// AD CS certificates include the template OID in extension 1.3.6.1.4.1.311.21.7 +// This can be used to validate the certificate was issued from the expected template. +// +// The extension value is ASN.1 encoded as: +// SEQUENCE { +// OBJECT IDENTIFIER (template OID) +// INTEGER (major version) OPTIONAL +// INTEGER (minor version) OPTIONAL +// } +func extractTemplateOID(cert *x509.Certificate) string { + // Microsoft Certificate Template OID (szOID_CERTIFICATE_TEMPLATE) + const templateExtOID = "1.3.6.1.4.1.311.21.7" + + for _, ext := range cert.Extensions { + if ext.Id.String() == templateExtOID { + return parseTemplateExtension(ext.Value) + } + } + return "" +} + +// parseTemplateExtension parses the ASN.1 encoded certificate template extension. +// Returns the template OID string or empty string on parse error. +func parseTemplateExtension(data []byte) string { + // Simple ASN.1 parsing for the template extension + // Format: SEQUENCE { OID, [INTEGER], [INTEGER] } + // + // We only need the OID, which starts after the SEQUENCE header + + if len(data) < 4 { + return "" + } + + // Check for SEQUENCE tag (0x30) + if data[0] != 0x30 { + return "" + } + + // Get sequence length (simplified - assumes short form) + seqLen := int(data[1]) + if seqLen > len(data)-2 { + return "" + } + + // Move past SEQUENCE header + pos := 2 + + // Check for OID tag (0x06) + if pos >= len(data) || data[pos] != 0x06 { + return "" + } + pos++ + + // Get OID length + if pos >= len(data) { + return "" + } + oidLen := int(data[pos]) + pos++ + + if pos+oidLen > len(data) { + return "" + } + + // Parse the OID bytes + oidBytes := data[pos : pos+oidLen] + return decodeOID(oidBytes) +} + +// decodeOID decodes ASN.1 DER encoded OID bytes to dotted string format. +func decodeOID(data []byte) string { + if len(data) == 0 { + return "" + } + + // First byte encodes first two components: val = 40*c1 + c2 + // where c1 is 0, 1, or 2 and c2 < 40 (for c1 = 0 or 1) or any (for c1 = 2) + var components []int + + first := int(data[0]) + switch { + case first < 40: + components = append(components, 0, first) + case first < 80: + components = append(components, 1, first-40) + default: + components = append(components, 2, first-80) + } + + // Remaining bytes are variable-length encoded + var val int + for i := 1; i < len(data); i++ { + val = val<<7 | int(data[i]&0x7f) + if data[i]&0x80 == 0 { + components = append(components, val) + val = 0 + } + } + + // Convert to dotted string + result := "" + for i, c := range components { + if i > 0 { + result += "." + } + result += fmt.Sprintf("%d", c) + } + return result +} + +// extractTemplateNameV1 extracts the certificate template NAME from v1 extension. +// AD CS v1 templates use extension OID 1.3.6.1.4.1.311.20.2 (szOID_ENROLL_CERTTYPE_EXTENSION) +// The value is a string, usually encoded as BMPString (UTF-16BE) or UTF8String. +func extractTemplateNameV1(cert *x509.Certificate) string { + // Microsoft Certificate Template Name OID (v1 templates) + const templateNameExtOID = "1.3.6.1.4.1.311.20.2" + + for _, ext := range cert.Extensions { + if ext.Id.String() == templateNameExtOID { + return decodeASN1String(ext.Value) + } + } + return "" +} + +// ASN.1 tag constants for string types +const ( + tagUTF8String = 12 // 0x0C + tagPrintableString = 19 // 0x13 + tagIA5String = 22 // 0x16 + tagBMPString = 30 // 0x1E - UTF-16BE! +) + +// decodeASN1String decodes an ASN.1 encoded string value. +// Handles UTF8String, PrintableString, IA5String, and BMPString (UTF-16BE). +func decodeASN1String(data []byte) string { + if len(data) < 2 { + return "" + } + + // ASN.1 TLV: Tag, Length, Value + tag := int(data[0]) + length := int(data[1]) + + // Handle long form length (0x81 prefix for lengths 128-255) + valueStart := 2 + if length == 0x81 && len(data) > 2 { + length = int(data[2]) + valueStart = 3 + } else if length == 0x82 && len(data) > 3 { + length = int(data[2])<<8 | int(data[3]) + valueStart = 4 + } + + if valueStart+length > len(data) { + // Fallback: try to decode entire data as string + return string(data) + } + + value := data[valueStart : valueStart+length] + + switch tag { + case tagUTF8String, tagPrintableString, tagIA5String: + return string(value) + case tagBMPString: + return decodeBMPString(value) + default: + // Unknown tag, try as raw string + return string(value) + } +} + +// decodeBMPString decodes UTF-16BE (BMPString) bytes to Go UTF-8 string. +// BMPString is commonly used in Microsoft certificate extensions. +func decodeBMPString(data []byte) string { + if len(data) < 2 { + return "" + } + + runes := make([]rune, 0, len(data)/2) + for i := 0; i+1 < len(data); i += 2 { + // UTF-16BE: high byte first + r := rune(data[i])<<8 | rune(data[i+1]) + if r != 0 { // Skip null characters + runes = append(runes, r) + } + } + return string(runes) +} + +// DefaultMachineTemplateNames are template names that indicate a machine certificate. +// These are matched case-insensitively. +var DefaultMachineTemplateNames = []string{ + "Machine", + "Computer", + "Workstation", + "NetBirdMachine", + "DomainController", + "WebServer", + "IPSecIntermediateOffline", +} + +// determinePeerType determines if the certificate belongs to a machine or user. +// Returns "machine", "user", or "unknown". +func determinePeerType(templateOID, templateName string, cert *x509.Certificate) string { + // Priority 1: Check template NAME (v1 extension, most reliable for AD CS) + if templateName != "" { + nameLower := strings.ToLower(templateName) + for _, mt := range DefaultMachineTemplateNames { + if nameLower == strings.ToLower(mt) { + return "machine" + } + } + // Known user template names + if nameLower == "user" || nameLower == "smartcardlogon" || nameLower == "smartcarduser" { + return "user" + } + } + + // Priority 2: Check EKU (Extended Key Usage) + // Machine certs typically have ClientAuth but NOT SmartCardLogon + hasClientAuth := false + hasSmartCardLogon := false + for _, eku := range cert.ExtKeyUsage { + if eku == x509.ExtKeyUsageClientAuth { + hasClientAuth = true + } + } + // SmartCardLogon OID: 1.3.6.1.4.1.311.20.2.2 + for _, oid := range cert.UnknownExtKeyUsage { + if oid.String() == "1.3.6.1.4.1.311.20.2.2" { + hasSmartCardLogon = true + } + } + + if hasClientAuth && !hasSmartCardLogon { + // Check if SAN has email or UPN (user indicators) + if len(cert.EmailAddresses) > 0 { + return "user" + } + // No email, has ClientAuth, no SmartCardLogon = likely machine + return "machine" + } + + if hasSmartCardLogon { + return "user" + } + + // Priority 3: Check SAN types + // User certs often have email addresses in SAN + if len(cert.EmailAddresses) > 0 { + return "user" + } + + // DNS names without email usually indicate machine + if len(cert.DNSNames) > 0 { + return "machine" + } + + return "unknown" +} diff --git a/management/internals/server/mtls_auth_test.go b/management/internals/server/mtls_auth_test.go new file mode 100644 index 00000000000..c9262413d09 --- /dev/null +++ b/management/internals/server/mtls_auth_test.go @@ -0,0 +1,605 @@ +package server + +import ( + "context" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "net" + "os" + "path/filepath" + "testing" + + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" +) + +// TestExtractMTLSIdentity tests the identity extraction from a mocked TLS connection. +func TestExtractMTLSIdentity(t *testing.T) { + // Find test certs relative to this file + certDir := filepath.Join("..", "..", "..", "test", "certs") + + clientCertPEM, err := os.ReadFile(filepath.Join(certDir, "client.crt")) + if err != nil { + t.Skipf("Test certs not found (run from repo root): %v", err) + } + + caCertPEM, err := os.ReadFile(filepath.Join(certDir, "ca.crt")) + if err != nil { + t.Fatalf("Failed to read CA cert: %v", err) + } + + // Parse client certificate + clientCert, err := parseCertificatePEM(clientCertPEM) + if err != nil { + t.Fatalf("Failed to parse client cert: %v", err) + } + + // Parse CA certificate + caCert, err := parseCertificatePEM(caCertPEM) + if err != nil { + t.Fatalf("Failed to parse CA cert: %v", err) + } + + // Create a mock peer context with TLS info + // VerifiedChains[0] = [clientCert, caCert] + tlsState := tls.ConnectionState{ + VerifiedChains: [][]*x509.Certificate{ + {clientCert, caCert}, + }, + PeerCertificates: []*x509.Certificate{clientCert}, + } + + tlsInfo := credentials.TLSInfo{State: tlsState} + peerInfo := &peer.Peer{ + Addr: &net.IPAddr{IP: net.ParseIP("127.0.0.1")}, + AuthInfo: tlsInfo, + } + + ctx := peer.NewContext(context.Background(), peerInfo) + + // Test extraction + identity, err := extractMTLSIdentity(ctx) + if err != nil { + t.Fatalf("extractMTLSIdentity failed: %v", err) + } + + // Verify expected values + t.Logf("Extracted identity: %+v", identity) + + if identity.DNSName != "win10-pc.corp.local" { + t.Errorf("Expected DNSName 'win10-pc.corp.local', got '%s'", identity.DNSName) + } + if identity.Hostname != "win10-pc" { + t.Errorf("Expected Hostname 'win10-pc', got '%s'", identity.Hostname) + } + if identity.Domain != "corp.local" { + t.Errorf("Expected Domain 'corp.local', got '%s'", identity.Domain) + } + if identity.SerialNumber == "" { + t.Error("Expected SerialNumber to be set") + } + + // Verify issuer fingerprint matches CA cert + expectedFP := fmt.Sprintf("%x", sha256.Sum256(caCert.Raw)) + if identity.IssuerFingerprint != expectedFP { + t.Errorf("IssuerFingerprint mismatch.\nExpected: %s\nGot: %s", expectedFP, identity.IssuerFingerprint) + } + + t.Logf("✅ mTLS Identity extraction VERIFIED:") + t.Logf(" DNSName: %s", identity.DNSName) + t.Logf(" Hostname: %s", identity.Hostname) + t.Logf(" Domain: %s", identity.Domain) + t.Logf(" IssuerFP: %s...", identity.IssuerFingerprint[:16]) + t.Logf(" Serial: %s", identity.SerialNumber) +} + +// TestExtractMTLSIdentityNoCert tests that missing cert returns error. +func TestExtractMTLSIdentityNoCert(t *testing.T) { + // Empty TLS state (no client cert) + tlsState := tls.ConnectionState{ + VerifiedChains: nil, + } + + tlsInfo := credentials.TLSInfo{State: tlsState} + peerInfo := &peer.Peer{ + Addr: &net.IPAddr{IP: net.ParseIP("127.0.0.1")}, + AuthInfo: tlsInfo, + } + + ctx := peer.NewContext(context.Background(), peerInfo) + + _, err := extractMTLSIdentity(ctx) + if err == nil { + t.Error("Expected error for missing certificate, got nil") + } + t.Logf("✅ Correctly rejected request without cert: %v", err) +} + +// TestExtractMTLSIdentityNoSAN tests that cert without SAN DNSName is rejected. +func TestExtractMTLSIdentityNoSAN(t *testing.T) { + // Create a minimal cert without SAN (CN only) + cert := &x509.Certificate{ + DNSNames: []string{}, // Empty SAN + } + + tlsState := tls.ConnectionState{ + VerifiedChains: [][]*x509.Certificate{ + {cert}, + }, + } + + tlsInfo := credentials.TLSInfo{State: tlsState} + peerInfo := &peer.Peer{ + Addr: &net.IPAddr{IP: net.ParseIP("127.0.0.1")}, + AuthInfo: tlsInfo, + } + + ctx := peer.NewContext(context.Background(), peerInfo) + + _, err := extractMTLSIdentity(ctx) + if err == nil { + t.Error("Expected error for cert without SAN DNSName") + } + t.Logf("✅ Correctly rejected cert without SAN: %v", err) +} + +// TestSplitDNSName tests the FQDN splitting function. +func TestSplitDNSName(t *testing.T) { + tests := []struct { + input string + hostname string + domain string + wantErr bool + }{ + {"win10-pc.corp.local", "win10-pc", "corp.local", false}, + {"server01.subdomain.example.com", "server01", "subdomain.example.com", false}, + {"host.a.b.c.d", "host", "a.b.c.d", false}, + {"nodotshere", "", "", true}, + {"", "", "", true}, + } + + for _, tt := range tests { + hostname, domain, err := splitDNSName(tt.input) + if tt.wantErr { + if err == nil { + t.Errorf("splitDNSName(%q): expected error", tt.input) + } + continue + } + if err != nil { + t.Errorf("splitDNSName(%q): unexpected error: %v", tt.input, err) + continue + } + if hostname != tt.hostname || domain != tt.domain { + t.Errorf("splitDNSName(%q) = (%q, %q), want (%q, %q)", + tt.input, hostname, domain, tt.hostname, tt.domain) + } + } +} + +// TestDecodeOID tests OID decoding. +func TestDecodeOID(t *testing.T) { + tests := []struct { + name string + input []byte + expected string + }{ + { + name: "Microsoft Template OID", + input: []byte{0x60, 0x86, 0x48, 0x01, 0x65, 0x02, 0x04, 0x05, 0x07}, // 2.16.840.1.101.2.4.5.7 (example) + expected: "2.16.840.1.101.2.4.5.7", + }, + { + name: "Simple OID", + input: []byte{0x55, 0x04, 0x03}, // 2.5.4.3 (CN) + expected: "2.5.4.3", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := decodeOID(tt.input) + if result != tt.expected { + t.Errorf("decodeOID() = %q, want %q", result, tt.expected) + } + }) + } +} + +// Helper to parse PEM certificate +func parseCertificatePEM(pemData []byte) (*x509.Certificate, error) { + block, _ := pem.Decode(pemData) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM") + } + return x509.ParseCertificate(block.Bytes) +} + +// TestDecodeASN1String tests ASN.1 string decoding including BMPString. +func TestDecodeASN1String(t *testing.T) { + tests := []struct { + name string + input []byte + expected string + }{ + { + name: "UTF8String - Machine", + input: []byte{0x0C, 0x07, 'M', 'a', 'c', 'h', 'i', 'n', 'e'}, // tag=12, len=7 + expected: "Machine", + }, + { + name: "PrintableString - Computer", + input: []byte{0x13, 0x08, 'C', 'o', 'm', 'p', 'u', 't', 'e', 'r'}, // tag=19, len=8 + expected: "Computer", + }, + { + name: "BMPString - Machine (UTF-16BE)", + // BMPString tag=30, len=14 (7 chars * 2 bytes) + // "Machine" in UTF-16BE: 0x004D 0x0061 0x0063 0x0068 0x0069 0x006E 0x0065 + input: []byte{0x1E, 0x0E, 0x00, 'M', 0x00, 'a', 0x00, 'c', 0x00, 'h', 0x00, 'i', 0x00, 'n', 0x00, 'e'}, + expected: "Machine", + }, + { + name: "BMPString - NetBirdMachine", + // "NetBirdMachine" = 14 chars * 2 bytes = 28 bytes (0x1C) + input: []byte{ + 0x1E, 0x1C, // BMPString, length=28 + 0x00, 'N', 0x00, 'e', 0x00, 't', 0x00, 'B', + 0x00, 'i', 0x00, 'r', 0x00, 'd', 0x00, 'M', + 0x00, 'a', 0x00, 'c', 0x00, 'h', 0x00, 'i', + 0x00, 'n', 0x00, 'e', + }, + expected: "NetBirdMachine", + }, + { + name: "IA5String", + input: []byte{0x16, 0x04, 'T', 'e', 's', 't'}, // tag=22, len=4 + expected: "Test", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := decodeASN1String(tt.input) + if result != tt.expected { + t.Errorf("decodeASN1String() = %q, want %q", result, tt.expected) + } + }) + } +} + +// TestDecodeBMPString tests UTF-16BE decoding specifically. +func TestDecodeBMPString(t *testing.T) { + tests := []struct { + name string + input []byte + expected string + }{ + { + name: "Simple ASCII in UTF-16BE", + input: []byte{0x00, 'H', 0x00, 'i'}, + expected: "Hi", + }, + { + name: "Machine in UTF-16BE", + input: []byte{0x00, 'M', 0x00, 'a', 0x00, 'c', 0x00, 'h', 0x00, 'i', 0x00, 'n', 0x00, 'e'}, + expected: "Machine", + }, + { + name: "Empty", + input: []byte{}, + expected: "", + }, + { + name: "With null terminators", + input: []byte{0x00, 'A', 0x00, 0x00, 0x00, 'B'}, // A, null, B + expected: "AB", // nulls stripped + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := decodeBMPString(tt.input) + if result != tt.expected { + t.Errorf("decodeBMPString() = %q, want %q", result, tt.expected) + } + }) + } +} + +// TestDeterminePeerType tests peer type determination logic. +func TestDeterminePeerType(t *testing.T) { + tests := []struct { + name string + templateOID string + templateName string + cert *x509.Certificate + expected string + }{ + { + name: "Machine template by name", + templateOID: "", + templateName: "Machine", + cert: &x509.Certificate{}, + expected: "machine", + }, + { + name: "Machine template case insensitive", + templateOID: "", + templateName: "MACHINE", + cert: &x509.Certificate{}, + expected: "machine", + }, + { + name: "Computer template", + templateOID: "", + templateName: "Computer", + cert: &x509.Certificate{}, + expected: "machine", + }, + { + name: "NetBirdMachine custom template", + templateOID: "", + templateName: "NetBirdMachine", + cert: &x509.Certificate{}, + expected: "machine", + }, + { + name: "User template by name", + templateOID: "", + templateName: "User", + cert: &x509.Certificate{}, + expected: "user", + }, + { + name: "SmartCardLogon template", + templateOID: "", + templateName: "SmartCardLogon", + cert: &x509.Certificate{}, + expected: "user", + }, + { + name: "Unknown template - has DNS but no email", + templateOID: "", + templateName: "CustomTemplate", + cert: &x509.Certificate{DNSNames: []string{"host.domain.local"}}, + expected: "machine", + }, + { + name: "Unknown template - has email", + templateOID: "", + templateName: "CustomTemplate", + cert: &x509.Certificate{EmailAddresses: []string{"user@domain.local"}}, + expected: "user", + }, + { + name: "No template - only ClientAuth EKU", + templateOID: "", + templateName: "", + cert: &x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}}, + expected: "machine", + }, + { + name: "No template - ClientAuth + Email", + templateOID: "", + templateName: "", + cert: &x509.Certificate{ + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + EmailAddresses: []string{"user@example.com"}, + }, + expected: "user", + }, + { + name: "Completely empty", + templateOID: "", + templateName: "", + cert: &x509.Certificate{}, + expected: "unknown", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := determinePeerType(tt.templateOID, tt.templateName, tt.cert) + if result != tt.expected { + t.Errorf("determinePeerType() = %q, want %q", result, tt.expected) + } + }) + } +} + +// TestExtractTemplateNameV1 tests v1 template name extraction. +func TestExtractTemplateNameV1(t *testing.T) { + // Create a certificate with v1 template extension (OID 1.3.6.1.4.1.311.20.2) + // containing "Machine" as UTF8String + cert := &x509.Certificate{ + Extensions: []pkix.Extension{ + { + Id: []int{1, 3, 6, 1, 4, 1, 311, 20, 2}, // szOID_ENROLL_CERTTYPE_EXTENSION + Critical: false, + Value: []byte{0x0C, 0x07, 'M', 'a', 'c', 'h', 'i', 'n', 'e'}, // UTF8String "Machine" + }, + }, + } + + result := extractTemplateNameV1(cert) + if result != "Machine" { + t.Errorf("extractTemplateNameV1() = %q, want %q", result, "Machine") + } + + // Test with BMPString encoding + certBMP := &x509.Certificate{ + Extensions: []pkix.Extension{ + { + Id: []int{1, 3, 6, 1, 4, 1, 311, 20, 2}, + Critical: false, + Value: []byte{0x1E, 0x0E, 0x00, 'M', 0x00, 'a', 0x00, 'c', 0x00, 'h', 0x00, 'i', 0x00, 'n', 0x00, 'e'}, + }, + }, + } + + resultBMP := extractTemplateNameV1(certBMP) + if resultBMP != "Machine" { + t.Errorf("extractTemplateNameV1() with BMPString = %q, want %q", resultBMP, "Machine") + } + + // Test without v1 extension + certNoExt := &x509.Certificate{} + resultNoExt := extractTemplateNameV1(certNoExt) + if resultNoExt != "" { + t.Errorf("extractTemplateNameV1() without extension = %q, want empty", resultNoExt) + } +} + +// TestAccountMapping tests the domain-to-account mapping functions. +func TestAccountMapping(t *testing.T) { + // Set up test config + testConfig := &MTLSConfig{ + DomainAccountMapping: map[string]string{ + "corp.local": "account-123", + "test.local": "account-123", // Same account, multiple domains + "customer-a.local": "account-456", + "customer-b.local": "account-789", + }, + AccountAllowedDomains: map[string][]string{ + "account-123": {"corp.local", "test.local"}, + "account-456": {"customer-a.local"}, + "account-789": {"customer-b.local"}, + }, + } + SetMTLSConfig(testConfig) + defer func() { globalMTLSConfig = nil }() // Cleanup + + // Test getAccountIDFromDomain + t.Run("getAccountIDFromDomain", func(t *testing.T) { + tests := []struct { + domain string + wantAccID string + wantErr bool + }{ + {"corp.local", "account-123", false}, + {"CORP.LOCAL", "account-123", false}, // Case insensitive + {"test.local", "account-123", false}, + {"customer-a.local", "account-456", false}, + {"unknown.local", "", true}, + {"", "", true}, + } + + for _, tt := range tests { + accID, err := getAccountIDFromDomain(tt.domain) + if tt.wantErr { + if err == nil { + t.Errorf("getAccountIDFromDomain(%q) expected error, got nil", tt.domain) + } + continue + } + if err != nil { + t.Errorf("getAccountIDFromDomain(%q) unexpected error: %v", tt.domain, err) + continue + } + if accID != tt.wantAccID { + t.Errorf("getAccountIDFromDomain(%q) = %q, want %q", tt.domain, accID, tt.wantAccID) + } + } + }) + + // Test getAllowedDomainsForAccount + t.Run("getAllowedDomainsForAccount", func(t *testing.T) { + domains := getAllowedDomainsForAccount("account-123") + if len(domains) != 2 { + t.Errorf("getAllowedDomainsForAccount(account-123) = %v, want 2 domains", domains) + } + + domains = getAllowedDomainsForAccount("account-456") + if len(domains) != 1 || domains[0] != "customer-a.local" { + t.Errorf("getAllowedDomainsForAccount(account-456) = %v, want [customer-a.local]", domains) + } + + domains = getAllowedDomainsForAccount("unknown-account") + if len(domains) != 0 { + t.Errorf("getAllowedDomainsForAccount(unknown) = %v, want empty (fail-safe)", domains) + } + }) + + // Test validateDomainForAccount + t.Run("validateDomainForAccount", func(t *testing.T) { + matched, err := validateDomainForAccount("corp.local", "account-123") + if err != nil { + t.Errorf("validateDomainForAccount(corp.local, account-123) unexpected error: %v", err) + } + if matched != "corp.local" { + t.Errorf("validateDomainForAccount() matched = %q, want corp.local", matched) + } + + // Cross-tenant attempt should fail + _, err = validateDomainForAccount("customer-a.local", "account-123") + if err == nil { + t.Error("validateDomainForAccount(customer-a.local, account-123) should fail (cross-tenant)") + } + }) + + t.Log("✅ Account mapping tests PASSED") +} + +// TestExtractMTLSIdentityWithAccountMapping tests identity extraction with account validation. +func TestExtractMTLSIdentityWithAccountMapping(t *testing.T) { + // Set up test config + testConfig := &MTLSConfig{ + DomainAccountMapping: map[string]string{ + "corp.local": "account-123", + }, + AccountAllowedDomains: map[string][]string{ + "account-123": {"corp.local"}, + }, + } + SetMTLSConfig(testConfig) + defer func() { globalMTLSConfig = nil }() // Cleanup + + // Find test certs + certDir := filepath.Join("..", "..", "..", "test", "certs") + clientCertPEM, err := os.ReadFile(filepath.Join(certDir, "client.crt")) + if err != nil { + t.Skipf("Test certs not found: %v", err) + } + caCertPEM, err := os.ReadFile(filepath.Join(certDir, "ca.crt")) + if err != nil { + t.Fatalf("Failed to read CA cert: %v", err) + } + + clientCert, _ := parseCertificatePEM(clientCertPEM) + caCert, _ := parseCertificatePEM(caCertPEM) + + // Create mock peer context + tlsState := tls.ConnectionState{ + VerifiedChains: [][]*x509.Certificate{{clientCert, caCert}}, + } + tlsInfo := credentials.TLSInfo{State: tlsState} + peerInfo := &peer.Peer{ + Addr: &net.IPAddr{IP: net.ParseIP("127.0.0.1")}, + AuthInfo: tlsInfo, + } + ctx := peer.NewContext(context.Background(), peerInfo) + + // Test extraction with account validation + identity, err := extractMTLSIdentity(ctx) + if err != nil { + t.Fatalf("extractMTLSIdentity failed: %v", err) + } + + // Verify account fields are populated + if identity.AccountID != "account-123" { + t.Errorf("AccountID = %q, want account-123", identity.AccountID) + } + if identity.MatchedDomain != "corp.local" { + t.Errorf("MatchedDomain = %q, want corp.local", identity.MatchedDomain) + } + + t.Logf("✅ mTLS Identity with Account Mapping VERIFIED:") + t.Logf(" DNSName: %s", identity.DNSName) + t.Logf(" AccountID: %s", identity.AccountID) + t.Logf(" MatchedDomain: %s", identity.MatchedDomain) +} diff --git a/management/internals/server/mtls_server.go b/management/internals/server/mtls_server.go new file mode 100644 index 00000000000..c3ac4a60fd4 --- /dev/null +++ b/management/internals/server/mtls_server.go @@ -0,0 +1,223 @@ +package server + +// Machine Tunnel Fork - Separate mTLS Server on Port 33074 +// This provides a dedicated port for mTLS-only machine clients with RequireAndVerifyClientCert. + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "os" + "path/filepath" + "strings" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + "github.com/netbirdio/netbird/management/internals/shared/mtls" +) + +// MTLSServerPort is the default port for the mTLS-only Machine Tunnel server +const MTLSServerPort = 33074 + +// MTLSServer holds the mTLS-only gRPC server for Machine Tunnel clients +type MTLSServer struct { + server *grpc.Server + listener net.Listener + caPool *x509.CertPool + tlsConfig *tls.Config + port int + interceptors []grpc.UnaryServerInterceptor +} + +// NewMTLSServer creates a new mTLS-only server for Machine Tunnel clients +func NewMTLSServer(certFile, keyFile, caDir, caCertFile string, port int, interceptors []grpc.UnaryServerInterceptor) (*MTLSServer, error) { + if port == 0 { + port = MTLSServerPort + } + + // Load server certificate + serverCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, fmt.Errorf("failed to load server certificate: %w", err) + } + + // Load CA pool for client certificate verification + caPool, err := loadCAPool(caDir, caCertFile) + if err != nil { + return nil, fmt.Errorf("failed to load CA pool: %w", err) + } + + // Configure TLS with RequireAndVerifyClientCert + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{serverCert}, + ClientAuth: tls.RequireAndVerifyClientCert, // STRICT: Client cert required! + ClientCAs: caPool, + MinVersion: tls.VersionTLS12, // TLS 1.2+ required + } + + log.Infof("mTLS server configured: port=%d, CA pool loaded with %d certificates", port, countCertsInPool(caPool)) + + return &MTLSServer{ + caPool: caPool, + tlsConfig: tlsConfig, + port: port, + interceptors: interceptors, + }, nil +} + +// CreateGRPCServer creates the gRPC server with mTLS credentials and interceptors +func (s *MTLSServer) CreateGRPCServer() *grpc.Server { + opts := []grpc.ServerOption{ + grpc.Creds(credentials.NewTLS(s.tlsConfig)), + } + + // Add interceptors if provided + if len(s.interceptors) > 0 { + opts = append(opts, grpc.ChainUnaryInterceptor(s.interceptors...)) + } + + s.server = grpc.NewServer(opts...) + return s.server +} + +// Start starts the mTLS server on the configured port +func (s *MTLSServer) Start(ctx context.Context) error { + if s.server == nil { + return fmt.Errorf("gRPC server not created - call CreateGRPCServer first") + } + + var err error + s.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", s.port)) + if err != nil { + return fmt.Errorf("failed to listen on port %d: %w", s.port, err) + } + + log.WithContext(ctx).Infof("starting mTLS-only Machine Tunnel server on port %d", s.port) + + go func() { + if err := s.server.Serve(s.listener); err != nil { + if ctx.Err() == nil { + log.WithContext(ctx).Errorf("mTLS server error: %v", err) + } + } + }() + + return nil +} + +// Stop stops the mTLS server gracefully +func (s *MTLSServer) Stop() { + if s.server != nil { + s.server.GracefulStop() + } + if s.listener != nil { + _ = s.listener.Close() + } + log.Info("mTLS server stopped") +} + +// GetServer returns the underlying gRPC server for service registration +func (s *MTLSServer) GetServer() *grpc.Server { + return s.server +} + +// loadCAPool loads CA certificates from directory and/or single file +func loadCAPool(caDir, caCertFile string) (*x509.CertPool, error) { + caPool := x509.NewCertPool() + loaded := 0 + + // Load from directory if specified + if caDir != "" { + dirLoaded, err := loadCAFromDirectory(caPool, caDir) + if err != nil { + log.Warnf("error loading CAs from directory %s: %v", caDir, err) + } + loaded += dirLoaded + } + + // Load from single file if specified + if caCertFile != "" { + certPEM, err := os.ReadFile(caCertFile) + if err != nil { + return nil, fmt.Errorf("failed to read CA cert file %s: %w", caCertFile, err) + } + if caPool.AppendCertsFromPEM(certPEM) { + loaded++ + log.Infof("loaded CA certificate: %s", filepath.Base(caCertFile)) + } else { + log.Warnf("failed to parse CA certificate from %s", caCertFile) + } + } + + if loaded == 0 { + return nil, fmt.Errorf("no CA certificates loaded - mTLS requires at least one CA") + } + + log.Infof("mTLS CA pool loaded: %d certificates", loaded) + return caPool, nil +} + +// loadCAFromDirectory loads all .crt, .pem, .cer files from a directory +func loadCAFromDirectory(pool *x509.CertPool, caDir string) (int, error) { + entries, err := os.ReadDir(caDir) + if err != nil { + return 0, fmt.Errorf("failed to read CA directory: %w", err) + } + + loaded := 0 + for _, entry := range entries { + if entry.IsDir() { + continue + } + + name := strings.ToLower(entry.Name()) + if !strings.HasSuffix(name, ".crt") && + !strings.HasSuffix(name, ".pem") && + !strings.HasSuffix(name, ".cer") { + continue + } + + certPath := filepath.Join(caDir, entry.Name()) + certPEM, err := os.ReadFile(certPath) + if err != nil { + log.Warnf("failed to read CA cert %s: %v", certPath, err) + continue + } + + if pool.AppendCertsFromPEM(certPEM) { + loaded++ + log.Infof("loaded CA certificate: %s", entry.Name()) + } else { + log.Warnf("failed to parse CA certificate from %s", entry.Name()) + } + } + + return loaded, nil +} + +// countCertsInPool attempts to estimate certificates in pool (Go doesn't expose this) +// This is a workaround since x509.CertPool doesn't have a Count() method +func countCertsInPool(pool *x509.CertPool) int { + if pool == nil { + return 0 + } + // Use Subjects() to count - each cert has one subject + return len(pool.Subjects()) //nolint:staticcheck // Subjects() is deprecated but no alternative exists +} + +// InitMTLSValidatorConfig initializes the global mTLS validator configuration +// from the server config. This should be called during server startup. +func InitMTLSValidatorConfig(accountAllowedIssuers map[string][]string) { + if len(accountAllowedIssuers) == 0 { + log.Warn("mTLS validator: no AccountAllowedIssuers configured - issuer validation will reject all certificates") + return + } + + mtls.SetValidatorConfig(&mtls.ValidatorConfig{ + AccountAllowedIssuers: accountAllowedIssuers, + }) +} diff --git a/management/internals/server/server.go b/management/internals/server/server.go index cd8d8e8fb09..06015c4d7e9 100644 --- a/management/internals/server/server.go +++ b/management/internals/server/server.go @@ -62,6 +62,9 @@ type BaseServer struct { certManager *autocert.Manager update *version.Update + // Machine Tunnel Fork: Separate mTLS server for machine peers + mtlsServer *MTLSServer + errCh chan error wg sync.WaitGroup cancel context.CancelFunc @@ -178,6 +181,12 @@ func (s *BaseServer) Start(ctx context.Context) error { } } + // Machine Tunnel Fork: Start separate mTLS server if enabled + if err := s.startMTLSServer(srvCtx); err != nil { + log.WithContext(srvCtx).Warnf("mTLS server not started: %v", err) + // Continue - mTLS is optional, main server should still work + } + for _, fn := range s.afterInit { if fn != nil { fn(s) @@ -215,6 +224,10 @@ func (s *BaseServer) Stop() error { _ = s.certManager.Listener().Close() } s.GRPCServer().Stop() + // Machine Tunnel Fork: Stop mTLS server if running + if s.mtlsServer != nil { + s.mtlsServer.Stop() + } _ = s.Store().Close(ctx) _ = s.EventStore().Close(ctx) if s.update != nil { @@ -255,6 +268,15 @@ func (s *BaseServer) SetContainer(key string, container any) { log.Tracef("container with key %s set successfully", key) } +// GetMTLSServer returns the mTLS gRPC server for Machine Tunnel service registration. +// Returns nil if mTLS is not enabled or not yet started. +func (s *BaseServer) GetMTLSServer() *grpc.Server { + if s.mtlsServer == nil { + return nil + } + return s.mtlsServer.GetServer() +} + func (s *BaseServer) handlerFunc(_ context.Context, gRPCHandler *grpc.Server, httpHandler http.Handler, meter metric.Meter) http.Handler { wsProxy := wsproxyserver.New(gRPCHandler, wsproxyserver.WithOTelMeter(meter)) @@ -338,6 +360,66 @@ func (s *BaseServer) serveGRPCWithHTTP(ctx context.Context, listener net.Listene }() } +// startMTLSServer starts the dedicated mTLS-only server for Machine Tunnel clients. +// This server runs on a separate port (default 33074) with RequireAndVerifyClientCert. +// Machine-only services (RegisterMachinePeer, SyncMachinePeer) are registered here. +func (s *BaseServer) startMTLSServer(ctx context.Context) error { + if !s.Config.HttpConfig.MTLSEnabled { + log.WithContext(ctx).Debug("mTLS server disabled - MTLSEnabled is false") + return nil + } + + // Server certificate - reuse main server's cert if mTLS-specific not provided + certFile := s.Config.HttpConfig.CertFile + keyFile := s.Config.HttpConfig.CertKey + + if certFile == "" || keyFile == "" { + return fmt.Errorf("mTLS server requires TLS certificates (CertFile and CertKey)") + } + + // CA for client certificate verification + caDir := s.Config.HttpConfig.MTLSCADir + caCertFile := s.Config.HttpConfig.MTLSCACertFile + + if caDir == "" && caCertFile == "" { + return fmt.Errorf("mTLS server requires CA certificates (MTLSCADir or MTLSCACertFile)") + } + + // Get port (default: 33074) + port := s.Config.HttpConfig.MTLSPort + if port == 0 { + port = MTLSServerPort + } + + // Create mTLS server + var err error + s.mtlsServer, err = NewMTLSServer(certFile, keyFile, caDir, caCertFile, port, nil) + if err != nil { + return fmt.Errorf("failed to create mTLS server: %w", err) + } + + // Initialize mTLS validator config with account-issuer mappings + if len(s.Config.HttpConfig.MTLSAccountAllowedIssuers) > 0 { + InitMTLSValidatorConfig(s.Config.HttpConfig.MTLSAccountAllowedIssuers) + } + + // Create gRPC server with mTLS credentials + grpcServer := s.mtlsServer.CreateGRPCServer() + + // Register Machine-only services on mTLS port + // Note: These services will be registered by the caller after this method returns + // The grpcServer is available via s.mtlsServer.GetServer() + _ = grpcServer // Services registered externally + + // Start the mTLS server + if err := s.mtlsServer.Start(ctx); err != nil { + return fmt.Errorf("failed to start mTLS server: %w", err) + } + + log.WithContext(ctx).Infof("mTLS-only Machine Tunnel server started on port %d", port) + return nil +} + func getInstallationID(ctx context.Context, store store.Store) (string, error) { installationID := store.GetInstallationID() if installationID != "" { diff --git a/management/internals/shared/grpc/conversion_test.go b/management/internals/shared/grpc/conversion_test.go index 95ad05eecfa..883b9ddbf8c 100644 --- a/management/internals/shared/grpc/conversion_test.go +++ b/management/internals/shared/grpc/conversion_test.go @@ -195,6 +195,7 @@ func TestBuildJWTConfig_Audiences(t *testing.T) { assert.NotNil(t, result) assert.Equal(t, tc.expectedAudiences, result.Audiences, "audiences should match expected") + // nolint:staticcheck // SA1019: testing deprecated Audience field for backwards compatibility assert.Equal(t, tc.expectedAudience, result.Audience, "audience should match expected") }) } diff --git a/management/internals/shared/grpc/machine_tunnel.go b/management/internals/shared/grpc/machine_tunnel.go new file mode 100644 index 00000000000..488ac8ca29a --- /dev/null +++ b/management/internals/shared/grpc/machine_tunnel.go @@ -0,0 +1,243 @@ +package grpc + +// Machine Tunnel Fork - gRPC handlers for machine peer registration and sync. +// These handlers require mTLS authentication and use the MTLSIdentity from context. + +import ( + "context" + "time" + + log "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/netbirdio/netbird/management/internals/shared/mtls" + nbContext "github.com/netbirdio/netbird/management/server/context" + nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/types" + // Note: nbpeer is still needed for extractMachinePeerMeta + "github.com/netbirdio/netbird/shared/management/proto" +) + +// RegisterMachinePeer handles machine peer registration using mTLS certificate authentication. +// This method is in mTLSRequiredMethods and will only be called with valid mTLS identity. +// +// Features (T-3.6 complete): +// - validateIssuerCA: CA-Fingerprint validation per account +// - Meta fields for audit: peer_type, cert_dns_name, auth_method, cert_issuer_fp, etc. +// - Re-registration logic: update existing peer vs create new +// - Rate-limit protection: TODO (stub for MVP) +// - Replay protection: TODO (stub for MVP) +func (s *Server) RegisterMachinePeer(ctx context.Context, req *proto.MachineRegisterRequest) (*proto.MachineRegisterResponse, error) { + reqStart := time.Now() + + // Extract mTLS identity from context (set by MTLSUnaryInterceptor) + identity := mtls.GetIdentity(ctx) + if identity == nil { + // This should not happen - interceptor should reject requests without identity + log.WithContext(ctx).Error("RegisterMachinePeer called without mTLS identity") + return nil, status.Error(codes.Unauthenticated, "mTLS authentication required") + } + + log.WithContext(ctx).Infof("RegisterMachinePeer: DNS=%s, Account=%s, Hostname=%s", + identity.DNSName, identity.AccountID, identity.Hostname) + + // Get account ID from mTLS identity (CRITICAL: Already validated in extractMTLSIdentity) + accountID := identity.AccountID + if accountID == "" { + log.WithContext(ctx).Errorf("No account ID in mTLS identity for domain %s", identity.Domain) + return nil, status.Errorf(codes.FailedPrecondition, + "domain %q not mapped to any account - configure MTLSDomainAccountMapping", identity.Domain) + } + + // SECURITY: Validate Issuer CA fingerprint against account's allowed issuers + // Per Security Review: Empty allowlist = DENY (explicit config required) + if err := mtls.ValidateIssuerCA(accountID, identity.IssuerFingerprint); err != nil { + log.WithContext(ctx).Warnf("Issuer CA validation failed: %v", err) + return nil, status.Errorf(codes.PermissionDenied, "certificate issuer not authorized: %v", err) + } + + // Parse WireGuard public key from request + if len(req.GetWgPubKey()) == 0 { + return nil, status.Error(codes.InvalidArgument, "WireGuard public key is required") + } + peerKey, err := wgtypes.ParseKey(string(req.GetWgPubKey())) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid WireGuard public key: %v", err) + } + + // Add peer and account info to context for logging + //nolint:staticcheck + ctx = context.WithValue(ctx, nbContext.PeerIDKey, peerKey.String()) + //nolint:staticcheck + ctx = context.WithValue(ctx, nbContext.AccountIDKey, accountID) + + // Build peer metadata from request, enriched with mTLS audit fields + peerMeta := extractMachinePeerMeta(ctx, req.GetMeta(), identity) + + // Log registration attempt (truncate keys for security) + keyPrefix := peerKey.String() + if len(keyPrefix) > 8 { + keyPrefix = keyPrefix[:8] + } + accountPrefix := accountID + if len(accountPrefix) > 8 { + accountPrefix = accountPrefix[:8] + } + log.WithContext(ctx).Infof("Machine peer registration: key=%s... hostname=%s domain=%s account=%s...", + keyPrefix, identity.Hostname, identity.Domain, accountPrefix) + + // Register or re-register peer via LoginPeer which handles both new + // registrations and updates for existing peers + // For machine peers, SetupKey and UserID are empty - auth is via mTLS + peer, netMap, postureChecks, err := s.accountManager.LoginPeer(ctx, types.PeerLogin{ + WireGuardPubKey: peerKey.String(), + Meta: peerMeta, + // Machine peer specific: no setup key, no user ID (auth via mTLS) + // The mTLS identity in context provides authentication + SetupKey: "", + UserID: "", + }) + if err != nil { + // Check if this is a "no auth method" error and provide better message + if err.Error() == "no peer auth method provided, please use a setup key or interactive SSO login" { + log.WithContext(ctx).Errorf("LoginPeer rejected mTLS auth - mTLS context not recognized. "+ + "This may indicate AddPeer needs mTLS support. Error: %v", err) + return nil, status.Errorf(codes.Internal, + "machine peer registration not fully implemented - AddPeer needs mTLS support") + } + log.WithContext(ctx).Errorf("Failed to register machine peer: %v", err) + return nil, status.Errorf(codes.Internal, "failed to register peer: %v", err) + } + + // Build response with machine-specific configuration + loginResp, err := s.prepareLoginResponse(ctx, peer, netMap, postureChecks) + if err != nil { + log.WithContext(ctx).Errorf("Failed to prepare login response: %v", err) + return nil, status.Errorf(codes.Internal, "failed to prepare response: %v", err) + } + + // Convert LoginResponse to MachineRegisterResponse + response := &proto.MachineRegisterResponse{ + PeerConfig: loginResp.GetPeerConfig(), + NetbirdConfig: loginResp.GetNetbirdConfig(), + MachineIdentity: &proto.MachineIdentity{ + DnsName: identity.DNSName, + Hostname: identity.Hostname, + Domain: identity.Domain, + IssuerFingerprint: identity.IssuerFingerprint, + SerialNumber: identity.SerialNumber, + TemplateOid: identity.TemplateOID, + }, + // TODO: Filter routes to only DC routes based on ACLs + AllowedDcRoutes: nil, // Will be populated in T-3.6b + DnsConfig: nil, // Will be populated based on DC DNS config + } + + log.WithContext(ctx).Infof("Machine peer registered successfully: DNS=%s, IP=%s (took %s)", + identity.DNSName, peer.IP, time.Since(reqStart)) + + return response, nil +} + +// extractMachinePeerMeta builds peer metadata from request, enriched with mTLS audit fields. +// This sets the mTLS-specific fields for audit trail. +func extractMachinePeerMeta(ctx context.Context, reqMeta *proto.PeerSystemMeta, identity *mtls.Identity) nbpeer.PeerSystemMeta { + // Start with base meta from request + meta := extractPeerMeta(ctx, reqMeta) + + // Enrich with mTLS audit fields + meta.PeerType = identity.PeerType + if meta.PeerType == "" { + meta.PeerType = "machine" // Default for mTLS-authenticated peers + } + meta.AuthMethod = "mtls" + meta.CertDNSName = identity.DNSName + meta.CertDomain = identity.Domain + meta.CertIssuerFP = identity.IssuerFingerprint + meta.CertSerial = identity.SerialNumber + meta.CertTemplate = identity.TemplateName + if meta.CertTemplate == "" { + meta.CertTemplate = identity.TemplateOID // Fallback to OID if name not available + } + + // Set auth timestamps + now := time.Now().UTC().Format(time.RFC3339) + meta.FirstAuthTime = now // Will be overwritten on re-registration + meta.LastCertAuthTime = now + + return meta +} + +// SyncMachinePeer handles machine peer sync stream using mTLS certificate authentication. +func (s *Server) SyncMachinePeer(req *proto.MachineSyncRequest, srv proto.ManagementService_SyncMachinePeerServer) error { + ctx := srv.Context() + + // Extract mTLS identity from context + identity := mtls.GetIdentity(ctx) + if identity == nil { + log.WithContext(ctx).Error("SyncMachinePeer called without mTLS identity") + return status.Error(codes.Unauthenticated, "mTLS authentication required") + } + + // Validate issuer CA + if err := mtls.ValidateIssuerCA(identity.AccountID, identity.IssuerFingerprint); err != nil { + log.WithContext(ctx).Warnf("Issuer CA validation failed in SyncMachinePeer: %v", err) + return status.Errorf(codes.PermissionDenied, "certificate issuer not authorized: %v", err) + } + + log.WithContext(ctx).Infof("SyncMachinePeer: DNS=%s", identity.DNSName) + + // TODO: Implement sync stream similar to Sync but for machine peers + // This should: + // 1. Look up peer by mTLS identity (hostname + domain) + // 2. Stream network map updates to the machine peer + // 3. Handle DC route changes + + return status.Error(codes.Unimplemented, "SyncMachinePeer not yet implemented") +} + +// GetMachineRoutes returns the DC routes allowed for a machine peer. +func (s *Server) GetMachineRoutes(ctx context.Context, req *proto.MachineRoutesRequest) (*proto.MachineRoutesResponse, error) { + // Extract mTLS identity from context + identity := mtls.GetIdentity(ctx) + if identity == nil { + return nil, status.Error(codes.Unauthenticated, "mTLS authentication required") + } + + // Validate issuer CA + if err := mtls.ValidateIssuerCA(identity.AccountID, identity.IssuerFingerprint); err != nil { + return nil, status.Errorf(codes.PermissionDenied, "certificate issuer not authorized: %v", err) + } + + log.WithContext(ctx).Infof("GetMachineRoutes: DNS=%s, IncludeOffline=%v", + identity.DNSName, req.GetIncludeOffline()) + + // TODO: Implement route retrieval based on peer and account ACLs + return nil, status.Error(codes.Unimplemented, "GetMachineRoutes not yet implemented") +} + +// ReportMachineStatus handles machine peer status reports. +func (s *Server) ReportMachineStatus(ctx context.Context, req *proto.MachineStatusRequest) (*proto.MachineStatusResponse, error) { + // Extract mTLS identity from context + identity := mtls.GetIdentity(ctx) + if identity == nil { + return nil, status.Error(codes.Unauthenticated, "mTLS authentication required") + } + + // Note: Issuer validation skipped for status reports (lower security sensitivity) + // The mTLS handshake itself provides authentication + + log.WithContext(ctx).Debugf("ReportMachineStatus: DNS=%s, TunnelUp=%v, DCReachable=%v", + identity.DNSName, req.GetTunnelUp(), req.GetDcReachable()) + + // TODO: Store status for monitoring/alerting + // This could update peer.LastSeen and store tunnel metrics + + return &proto.MachineStatusResponse{ + Ack: true, + ServerTime: timestamppb.Now(), + }, nil +} diff --git a/management/internals/shared/mtls/dnslabel.go b/management/internals/shared/mtls/dnslabel.go new file mode 100644 index 00000000000..901d3ef8ac5 --- /dev/null +++ b/management/internals/shared/mtls/dnslabel.go @@ -0,0 +1,141 @@ +package mtls + +// Machine Tunnel Fork - DNSLabel Generation for mTLS Peers +// Provides unique DNS label generation to prevent collisions across domains. + +import ( + "crypto/sha256" + "fmt" + "regexp" + "strings" + + log "github.com/sirupsen/logrus" +) + +// dnsLabelRegex validates RFC 1123 compliant DNS labels +// Must start and end with alphanumeric, can contain hyphens in between +var dnsLabelRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`) + +// MaxDNSLabelLength is the maximum length for a DNS label per RFC 1123 +const MaxDNSLabelLength = 63 + +// HashSuffixLength is the length of the FQDN hash suffix (8 hex chars = 32 bits) +const HashSuffixLength = 8 + +// GenerateUniqueDNSLabel creates a unique DNSLabel from hostname and domain. +// +// Problem (v3.5 - domain-only hash): +// - Hash only over domain → all hosts of a domain get same hash suffix +// - Hostname collision within domain would result in identical DNSLabel +// - Example: Two "win10-pc.corp.local" (misconfiguration) → same DNSLabel! +// +// Solution (v3.6 - FQDN hash): +// - FQDN = "hostname.domain" (case-insensitive) +// - Each host gets guaranteed unique hash +// - "win10-pc.customer-a.local" → "win10-pc-a1b2c3d4" +// - "win10-pc.customer-b.local" → "win10-pc-5e6f7g8h" +// - "win11-pc.customer-a.local" → "win11-pc-9x8y7z6w" +// +// Hash collision probability with 32 bits (8 hex chars) and 10,000 peers: ~0.001% +func GenerateUniqueDNSLabel(hostname, domain string) string { + // Normalize: lowercase for case-insensitive matching + hostname = strings.ToLower(hostname) + domain = strings.ToLower(domain) + + // v3.6: Hash over FQDN (hostname.domain), not just domain! + fqdn := fmt.Sprintf("%s.%s", hostname, domain) + h := sha256.Sum256([]byte(fqdn)) + fqdnHash := fmt.Sprintf("%x", h[:4]) // 32 bit = 4 bytes = 8 hex chars + + // Sanitize hostname: replace invalid chars with hyphens + sanitizedHostname := sanitizeForDNS(hostname) + + // Combine hostname with hash (Human-readable prefix + unique suffix) + label := fmt.Sprintf("%s-%s", sanitizedHostname, fqdnHash) + + // DNS-Label max 63 chars (RFC 1123) + if len(label) > MaxDNSLabelLength { + // Truncate hostname to fit, keeping the hash suffix intact + maxHostLen := MaxDNSLabelLength - HashSuffixLength - 1 // -1 for dash + if maxHostLen < 1 { + maxHostLen = 1 + } + truncatedHostname := sanitizedHostname + if len(sanitizedHostname) > maxHostLen { + truncatedHostname = sanitizedHostname[:maxHostLen] + } + // Remove trailing hyphens after truncation + truncatedHostname = strings.TrimRight(truncatedHostname, "-") + label = fmt.Sprintf("%s-%s", truncatedHostname, fqdnHash) + log.Debugf("DNSLabel truncated: %s (from hostname %s)", label, hostname) + } + + return label +} + +// ValidateDNSLabel checks if a label is RFC 1123 compliant. +// Returns nil if valid, error otherwise. +func ValidateDNSLabel(label string) error { + if len(label) == 0 { + return fmt.Errorf("DNS label cannot be empty") + } + if len(label) > MaxDNSLabelLength { + return fmt.Errorf("DNS label must be 1-%d chars, got %d", MaxDNSLabelLength, len(label)) + } + + // RFC 1123: [a-z0-9]([-a-z0-9]*[a-z0-9])? + // Must be lowercase, start/end with alphanumeric, can contain hyphens + if !dnsLabelRegex.MatchString(label) { + return fmt.Errorf("DNS label must match RFC 1123: start/end with alphanumeric, only lowercase letters, digits and hyphens allowed") + } + + return nil +} + +// sanitizeForDNS converts a hostname to a valid DNS label component. +// Replaces invalid characters with hyphens and ensures valid format. +func sanitizeForDNS(hostname string) string { + // Replace underscores and other common invalid chars with hyphens + result := strings.Map(func(r rune) rune { + switch { + case r >= 'a' && r <= 'z': + return r + case r >= '0' && r <= '9': + return r + case r == '-': + return r + case r >= 'A' && r <= 'Z': + return r + 32 // lowercase + case r == '_' || r == '.' || r == ' ': + return '-' + default: + return -1 // drop other chars + } + }, hostname) + + // Remove leading/trailing hyphens + result = strings.Trim(result, "-") + + // Collapse multiple consecutive hyphens + for strings.Contains(result, "--") { + result = strings.ReplaceAll(result, "--", "-") + } + + // If empty after sanitization, use a default + if result == "" { + result = "peer" + } + + return result +} + +// CheckDNSLabelCollision is a helper that logs a warning if a collision is detected. +// This should be called after DB check for existing label. +// Returns true if collision detected (existingLabel is not empty). +func CheckDNSLabelCollision(label, existingPeerID string) bool { + if existingPeerID != "" { + log.Warnf("RARE: DNSLabel collision detected for label %s (existing peer: %s)", label, existingPeerID) + return true + } + return false +} diff --git a/management/internals/shared/mtls/dnslabel_test.go b/management/internals/shared/mtls/dnslabel_test.go new file mode 100644 index 00000000000..3f5a678b4cb --- /dev/null +++ b/management/internals/shared/mtls/dnslabel_test.go @@ -0,0 +1,262 @@ +package mtls + +import ( + "testing" +) + +func TestGenerateUniqueDNSLabel(t *testing.T) { + tests := []struct { + name string + hostname string + domain string + wantLen int // Check length is within bounds + }{ + { + name: "simple hostname and domain", + hostname: "win10-pc", + domain: "corp.local", + wantLen: 17, // "win10-pc" + "-" + 8 hex chars + }, + { + name: "uppercase hostname normalized", + hostname: "WIN10-PC", + domain: "CORP.LOCAL", + wantLen: 17, + }, + { + name: "very long hostname truncated", + hostname: "this-is-a-very-very-very-very-very-very-very-long-hostname-that-exceeds-limit", + domain: "corp.local", + wantLen: 63, // Should be truncated to max 63 chars + }, + { + name: "hostname with underscores", + hostname: "win_10_pc", + domain: "corp.local", + wantLen: 18, // "win-10-pc" + "-" + 8 hex chars + }, + { + name: "hostname with spaces", + hostname: "win 10 pc", + domain: "corp.local", + wantLen: 18, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GenerateUniqueDNSLabel(tt.hostname, tt.domain) + + // Check length constraint + if len(got) > MaxDNSLabelLength { + t.Errorf("GenerateUniqueDNSLabel() returned label longer than %d chars: %s (len=%d)", + MaxDNSLabelLength, got, len(got)) + } + + // Check RFC 1123 compliance + if err := ValidateDNSLabel(got); err != nil { + t.Errorf("GenerateUniqueDNSLabel() returned invalid label: %s, error: %v", got, err) + } + }) + } +} + +func TestGenerateUniqueDNSLabel_Uniqueness(t *testing.T) { + // Same hostname, different domains should produce different labels + label1 := GenerateUniqueDNSLabel("win10-pc", "customer-a.local") + label2 := GenerateUniqueDNSLabel("win10-pc", "customer-b.local") + + if label1 == label2 { + t.Errorf("Expected different labels for different domains, got same: %s", label1) + } + + // Same domain, different hostnames should produce different labels + label3 := GenerateUniqueDNSLabel("win10-pc", "corp.local") + label4 := GenerateUniqueDNSLabel("win11-pc", "corp.local") + + if label3 == label4 { + t.Errorf("Expected different labels for different hostnames, got same: %s", label3) + } + + // Same hostname+domain should produce same label (deterministic) + label5 := GenerateUniqueDNSLabel("server1", "example.com") + label6 := GenerateUniqueDNSLabel("server1", "example.com") + + if label5 != label6 { + t.Errorf("Expected same label for same input, got different: %s vs %s", label5, label6) + } + + // Case-insensitive: same FQDN with different case should produce same label + label7 := GenerateUniqueDNSLabel("SERVER1", "EXAMPLE.COM") + if label5 != label7 { + t.Errorf("Expected case-insensitive matching, got different: %s vs %s", label5, label7) + } +} + +func TestValidateDNSLabel(t *testing.T) { + tests := []struct { + name string + label string + wantErr bool + }{ + { + name: "valid simple label", + label: "hostname", + wantErr: false, + }, + { + name: "valid with numbers", + label: "host123", + wantErr: false, + }, + { + name: "valid with hyphens", + label: "my-hostname-01", + wantErr: false, + }, + { + name: "valid machine label with hash", + label: "win10-pc-a1b2c3d4", + wantErr: false, + }, + { + name: "empty label", + label: "", + wantErr: true, + }, + { + name: "starts with hyphen", + label: "-hostname", + wantErr: true, + }, + { + name: "ends with hyphen", + label: "hostname-", + wantErr: true, + }, + { + name: "contains uppercase", + label: "Hostname", + wantErr: true, + }, + { + name: "contains underscore", + label: "host_name", + wantErr: true, + }, + { + name: "contains space", + label: "host name", + wantErr: true, + }, + { + name: "too long (64 chars)", + label: "a123456789012345678901234567890123456789012345678901234567890123", + wantErr: true, + }, + { + name: "max length (63 chars)", + label: "a12345678901234567890123456789012345678901234567890123456789012", + wantErr: false, + }, + { + name: "single char", + label: "a", + wantErr: false, + }, + { + name: "starts with number", + label: "1hostname", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateDNSLabel(tt.label) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateDNSLabel(%q) error = %v, wantErr %v", tt.label, err, tt.wantErr) + } + }) + } +} + +func TestSanitizeForDNS(t *testing.T) { + tests := []struct { + name string + hostname string + want string + }{ + { + name: "already valid", + hostname: "hostname", + want: "hostname", + }, + { + name: "uppercase to lowercase", + hostname: "HOSTNAME", + want: "hostname", + }, + { + name: "underscores to hyphens", + hostname: "host_name", + want: "host-name", + }, + { + name: "spaces to hyphens", + hostname: "host name", + want: "host-name", + }, + { + name: "dots to hyphens", + hostname: "host.name", + want: "host-name", + }, + { + name: "leading hyphens removed", + hostname: "_hostname", + want: "hostname", + }, + { + name: "trailing hyphens removed", + hostname: "hostname_", + want: "hostname", + }, + { + name: "multiple consecutive hyphens collapsed", + hostname: "host__name", + want: "host-name", + }, + { + name: "special chars dropped", + hostname: "host@name!", + want: "hostname", + }, + { + name: "empty after sanitization", + hostname: "@#$%", + want: "peer", // default fallback + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := sanitizeForDNS(tt.hostname) + if got != tt.want { + t.Errorf("sanitizeForDNS(%q) = %q, want %q", tt.hostname, got, tt.want) + } + }) + } +} + +func TestCheckDNSLabelCollision(t *testing.T) { + // No collision + if CheckDNSLabelCollision("test-label", "") { + t.Error("Expected no collision when existingPeerID is empty") + } + + // Collision detected + if !CheckDNSLabelCollision("test-label", "existing-peer-id") { + t.Error("Expected collision when existingPeerID is not empty") + } +} diff --git a/management/internals/shared/mtls/identity.go b/management/internals/shared/mtls/identity.go new file mode 100644 index 00000000000..2c00e790cb8 --- /dev/null +++ b/management/internals/shared/mtls/identity.go @@ -0,0 +1,125 @@ +package mtls + +// Machine Tunnel Fork - mTLS Identity Types and Validation +// This package defines types shared between server and grpc packages to avoid import cycles. + +import ( + "context" + "fmt" + "strings" + + log "github.com/sirupsen/logrus" +) + +// mtlsIdentityKeyType is the context key type for mTLS identity +type mtlsIdentityKeyType struct{} + +// IdentityKey is the context key for mTLS identity +var IdentityKey = mtlsIdentityKeyType{} + +// Identity represents the extracted identity from a client certificate +type Identity struct { + // DNSName is the primary identity from SAN DNSName (e.g., "hostname.domain.local") + DNSName string + // Hostname extracted from DNSName (e.g., "hostname") + Hostname string + // Domain extracted from DNSName (e.g., "domain.local") + Domain string + // MatchedDomain is the AllowedDomain that matched (for audit logging) + MatchedDomain string + // AccountID is the account UUID from domain mapping (CRITICAL for Multi-Tenant isolation!) + AccountID string + // IssuerFingerprint is SHA256 of the issuer certificate + IssuerFingerprint string + // SerialNumber of the client certificate + SerialNumber string + // TemplateOID if present in certificate extensions (v2 extension) + TemplateOID string + // TemplateName if present in certificate extensions (v1 extension, BMPString decoded) + TemplateName string + // PeerType determined from template: "machine", "user", or "unknown" + PeerType string +} + +// GetIdentity retrieves the mTLS identity from context. +// Returns nil if no mTLS identity is present (e.g., token auth was used). +func GetIdentity(ctx context.Context) *Identity { + identity, ok := ctx.Value(IdentityKey).(*Identity) + if !ok { + return nil + } + return identity +} + +// WithIdentity returns a new context with the mTLS identity attached. +func WithIdentity(ctx context.Context, identity *Identity) context.Context { + return context.WithValue(ctx, IdentityKey, identity) +} + +// ValidatorConfig holds configuration for mTLS validation. +// This is set during server initialization. +type ValidatorConfig struct { + // AccountAllowedIssuers maps account IDs to their allowed CA issuer fingerprints (SHA256) + // CRITICAL: If set for an account, only certificates from these CAs are accepted + // If empty for an account, issuer validation is SKIPPED (warned, NOT RECOMMENDED for production!) + AccountAllowedIssuers map[string][]string +} + +// globalValidatorConfig is the server-wide mTLS validator configuration. +var globalValidatorConfig *ValidatorConfig + +// SetValidatorConfig sets the global mTLS validator configuration. +// Must be called during server initialization. +func SetValidatorConfig(cfg *ValidatorConfig) { + globalValidatorConfig = cfg + if cfg != nil { + log.Infof("mTLS validator config loaded: %d accounts with issuer allowlists", + len(cfg.AccountAllowedIssuers)) + } +} + +// ValidateIssuerCA validates that the certificate issuer is authorized for the given account. +// CRITICAL: This is a security boundary for multi-tenant isolation! +// Uses SHA256 fingerprint comparison (NOT string matching on DN which can be spoofed!) +// +// Returns nil if issuer is valid, error otherwise. +// Per Security Review: Empty allowlist = DENY (not any-CA!) for production safety. +func ValidateIssuerCA(accountID, issuerFingerprint string) error { + if globalValidatorConfig == nil { + // Config not set - allow for backwards compatibility during testing + // In production, this should be configured + log.Warnf("mTLS validator config not initialized - skipping issuer validation (configure for production!)") + return nil + } + + // Get allowed issuers for this account + allowedIssuers := globalValidatorConfig.AccountAllowedIssuers[accountID] + + // Security: Empty allowlist = DENY (fail-safe for production) + // Per Security Review: Explicit configuration required, no "any CA" fallback + if len(allowedIssuers) == 0 { + log.Warnf("SECURITY: Account %s has no MTLSAccountAllowedIssuers configured - rejecting certificate (explicit config required)", accountID) + return fmt.Errorf("no allowed CA issuers configured for account %s - explicit MTLSAccountAllowedIssuers configuration required", accountID) + } + + // Normalize fingerprint for comparison (lowercase hex) + normalizedFP := strings.ToLower(issuerFingerprint) + + // Check against allowed issuers + for _, allowed := range allowedIssuers { + if strings.ToLower(allowed) == normalizedFP { + log.Debugf("Issuer CA validated for account %s (FP: %s...)", accountID, truncateFP(normalizedFP)) + return nil + } + } + + return fmt.Errorf("certificate issuer CA (FP: %s) not in allowed list for account %s", truncateFP(normalizedFP), accountID) +} + +// truncateFP truncates a fingerprint for safe logging (first 16 chars). +func truncateFP(fp string) string { + if len(fp) > 16 { + return fp[:16] + "..." + } + return fp +} diff --git a/management/server/peer.go b/management/server/peer.go index 977bd52af55..fea071bf504 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -29,6 +29,9 @@ import ( "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/shared/management/status" + + // Machine Tunnel Fork: mTLS DNS label generation + "github.com/netbirdio/netbird/management/internals/shared/mtls" ) // GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if @@ -564,12 +567,25 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe } var freeLabel string - if ephemeral || attempt > 1 { + // Machine Tunnel Fork: Use hash-based DNS label for mTLS peers + // This prevents collisions across different domains with same hostname + switch { + case peer.Meta.CertDNSName != "" && peer.Meta.CertDomain != "": + // mTLS peer: use FQDN-hash based label for uniqueness + freeLabel = mtls.GenerateUniqueDNSLabel(peer.Meta.Hostname, peer.Meta.CertDomain) + if err := mtls.ValidateDNSLabel(freeLabel); err != nil { + log.WithContext(ctx).Warnf("Generated DNS label failed validation: %s, falling back to IP-based", freeLabel) + freeLabel, err = getPeerIPDNSLabel(freeIP, peer.Meta.Hostname) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get free DNS label: %w", err) + } + } + case ephemeral || attempt > 1: freeLabel, err = getPeerIPDNSLabel(freeIP, peer.Meta.Hostname) if err != nil { return nil, nil, nil, fmt.Errorf("failed to get free DNS label: %w", err) } - } else { + default: freeLabel, err = nbdns.GetParsedDomainLabel(peer.Meta.Hostname) if err != nil { return nil, nil, nil, fmt.Errorf("failed to get free DNS label: %w", err) diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go index 2439e8a22b8..2f77ba5c0e7 100644 --- a/management/server/peer/peer.go +++ b/management/server/peer/peer.go @@ -130,6 +130,27 @@ type PeerSystemMeta struct { //nolint:revive Environment Environment `gorm:"serializer:json"` Flags Flags `gorm:"serializer:json"` Files []File `gorm:"serializer:json"` + + // Machine Tunnel Fork - mTLS Authentication Metadata + // These fields are populated when peer authenticates via mTLS (machine certificate) + // PeerType indicates authentication type: "machine" (mTLS), "user" (SSO), or empty (setup key) + PeerType string + // AuthMethod indicates how the peer was authenticated: "mtls", "sso", "setup_key" + AuthMethod string + // CertDNSName is the SAN DNSName from the client certificate (e.g., "hostname.domain.local") + CertDNSName string + // CertDomain is the domain extracted from CertDNSName (e.g., "domain.local") + CertDomain string + // CertIssuerFP is SHA256 fingerprint of the issuer CA certificate (for audit) + CertIssuerFP string + // CertSerial is the serial number of the client certificate + CertSerial string + // CertTemplate is the certificate template name/OID if present + CertTemplate string + // FirstAuthTime records when the peer first authenticated via mTLS + FirstAuthTime string + // LastCertAuthTime records the most recent mTLS authentication + LastCertAuthTime string } func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool { diff --git a/scripts/bootstrap-new-client.ps1 b/scripts/bootstrap-new-client.ps1 new file mode 100644 index 00000000000..14ca184ddf1 --- /dev/null +++ b/scripts/bootstrap-new-client.ps1 @@ -0,0 +1,522 @@ +<# +.SYNOPSIS + Bootstrap script for new NetBird Machine Tunnel clients. + Performs Phase 1 (Setup-Key) -> Domain Join -> Certificate Enrollment -> Phase 2 (mTLS). + +.DESCRIPTION + This script automates the full bootstrap process for a new Windows client: + 1. Pre-Tunnel NTP sync (pool.ntp.org) + 2. Install/Start NetBird Machine Service with Setup-Key + 3. Wait for tunnel establishment + 4. Verify DC connectivity via tunnel + 5. NTP sync with DC (Kerberos requirement) + 6. Domain Join + 7. Certificate Enrollment via AD CS + 8. Update NetBird config for mTLS + 9. Restart service for Phase 2 + +.PARAMETER SetupKey + The one-time Setup-Key from NetBird Management (UUID format). + +.PARAMETER DomainName + The FQDN of the Active Directory domain (e.g., "corp.local"). + +.PARAMETER DCAddress + The IP address of the Domain Controller (e.g., "192.168.100.20"). + +.PARAMETER OUPath + Optional. The OU path for the computer object. + Format: "OU=Computers,DC=corp,DC=local" + +.PARAMETER CertTemplateName + The AD CS certificate template name for machine certificates. + Default: "NetBirdMachineTunnel" + +.PARAMETER NoRestart + Skip automatic restart after domain join. + +.PARAMETER WhatIf + Show what would be done without making changes. + +.EXAMPLE + .\bootstrap-new-client.ps1 -SetupKey "a1b2c3d4-e5f6-7890-abcd-ef1234567890" -DomainName "corp.local" -DCAddress "192.168.100.20" + +.NOTES + Requires: Administrator privileges, PowerShell 5.1+ + Author: NetBird Machine Tunnel Fork + Version: 2.0.0 + + SECURITY CONSIDERATIONS: + + 1. Script Verification (before running): + - Verify SHA256 checksum: Get-FileHash .\bootstrap-new-client.ps1 -Algorithm SHA256 + - Compare with published checksum in CHECKSUMS.txt or release notes + + 2. Authenticode Signing (for production deployments): + - Sign with code signing certificate: Set-AuthenticodeSignature -FilePath .\bootstrap-new-client.ps1 -Certificate $cert + - Verify signature: Get-AuthenticodeSignature .\bootstrap-new-client.ps1 + + 3. Setup-Key Handling: + - Setup-Keys are one-time use with 24h TTL + - ALWAYS revoke Setup-Key in Dashboard after bootstrap + - Setup-Key is redacted in all logs (only last 4 chars shown) + - Setup-Key is removed from local config after mTLS upgrade +#> + +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory = $true)] + [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] + [string]$SetupKey, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$DomainName, + + [Parameter(Mandatory = $true)] + [ValidatePattern('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')] + [string]$DCAddress, + + [Parameter(Mandatory = $false)] + [string]$OUPath = "", + + [Parameter(Mandatory = $false)] + [string]$CertTemplateName = "NetBirdMachineTunnel", + + [Parameter(Mandatory = $false)] + [switch]$NoRestart +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'Continue' + +# Configuration paths +$NetBirdConfigPath = "$env:ProgramData\NetBird\config.yaml" +$NetBirdServiceName = "NetBirdMachine" +$TunnelInterface = "wg-nb-machine" + +#region Helper Functions + +function Write-Step { + param([string]$Message) + Write-Host "`n========================================" -ForegroundColor Cyan + Write-Host " $Message" -ForegroundColor Cyan + Write-Host "========================================`n" -ForegroundColor Cyan +} + +function Write-Success { + param([string]$Message) + Write-Host "[OK] $Message" -ForegroundColor Green +} + +function Write-Warning { + param([string]$Message) + Write-Host "[WARN] $Message" -ForegroundColor Yellow +} + +function Write-Failure { + param([string]$Message) + Write-Host "[FAIL] $Message" -ForegroundColor Red +} + +function Test-Administrator { + $currentUser = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) + return $currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function Test-DCConnectivity { + param([string]$DC, [int]$Port) + + try { + $result = Test-NetConnection -ComputerName $DC -Port $Port -WarningAction SilentlyContinue -ErrorAction Stop + return $result.TcpTestSucceeded + } catch { + return $false + } +} + +function Wait-TunnelUp { + param( + [int]$TimeoutSeconds = 60, + [int]$CheckIntervalSeconds = 2 + ) + + $elapsed = 0 + while ($elapsed -lt $TimeoutSeconds) { + # Check if tunnel interface exists + $interface = Get-NetAdapter -Name $TunnelInterface -ErrorAction SilentlyContinue + if ($interface -and $interface.Status -eq 'Up') { + return $true + } + + Start-Sleep -Seconds $CheckIntervalSeconds + $elapsed += $CheckIntervalSeconds + Write-Host "." -NoNewline + } + Write-Host "" + return $false +} + +function Get-TimeDifferenceSeconds { + param([string]$NtpServer) + + try { + $output = w32tm /stripchart /computer:$NtpServer /samples:1 /dataonly 2>&1 + if ($output -match '([+-]?\d+\.\d+)s') { + return [math]::Abs([double]$Matches[1]) + } + } catch { + Write-Warning "Could not measure time difference: $_" + } + return $null +} + +#endregion + +#region Main Script + +# Check prerequisites +if (-not (Test-Administrator)) { + throw "This script must be run as Administrator" +} + +Write-Host @" + +╔═══════════════════════════════════════════════════════════════════╗ +║ NetBird Machine Tunnel Bootstrap Script ║ +║ ║ +║ Phase 1: Setup-Key → Domain Join → Cert → Phase 2: mTLS ║ +╚═══════════════════════════════════════════════════════════════════╝ + +"@ -ForegroundColor Cyan + +# Redact Setup-Key for logging (show only last 4 chars) +$SetupKeyRedacted = "****-****-****-****-" + $SetupKey.Substring($SetupKey.Length - 4) + +Write-Host "Configuration:" -ForegroundColor White +Write-Host " Domain: $DomainName" +Write-Host " DC: $DCAddress" +Write-Host " Setup-Key: $SetupKeyRedacted" +if ($OUPath) { Write-Host " OU Path: $OUPath" } +Write-Host " Template: $CertTemplateName" +Write-Host "" + +# ============================================ +# Step 1: Pre-Tunnel NTP Sync +# ============================================ +Write-Step "Step 1: Pre-Tunnel NTP Sync (Public NTP)" + +if ($PSCmdlet.ShouldProcess("W32Time", "Configure public NTP")) { + try { + # Use public NTP before tunnel is up + w32tm /config /manualpeerlist:"pool.ntp.org" /syncfromflags:manual /reliable:no /update | Out-Null + Restart-Service W32Time -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + w32tm /resync /nowait | Out-Null + Write-Success "Public NTP sync initiated" + } catch { + Write-Warning "Public NTP sync failed: $_ (continuing...)" + } +} + +# ============================================ +# Step 2: Start NetBird Service with Setup-Key +# ============================================ +Write-Step "Step 2: Starting NetBird Machine Service (Phase 1: Setup-Key)" + +if ($PSCmdlet.ShouldProcess($NetBirdServiceName, "Install and start with Setup-Key")) { + # Check if service exists + $service = Get-Service -Name $NetBirdServiceName -ErrorAction SilentlyContinue + + if (-not $service) { + throw "NetBird Machine Service is not installed. Run the installer first." + } + + # Update config with setup key + if (Test-Path $NetBirdConfigPath) { + $config = Get-Content $NetBirdConfigPath -Raw + if ($config -notmatch 'setup_key:') { + Add-Content $NetBirdConfigPath "`nsetup_key: $SetupKey" + } else { + $config = $config -replace 'setup_key:.*', "setup_key: $SetupKey" + Set-Content $NetBirdConfigPath $config + } + Write-Success "Config updated with Setup-Key" + } else { + throw "NetBird config not found at $NetBirdConfigPath" + } + + # Start/Restart service + if ($service.Status -eq 'Running') { + Restart-Service $NetBirdServiceName + } else { + Start-Service $NetBirdServiceName + } + + Write-Success "Service started" + + # Wait for tunnel + Write-Host "Waiting for tunnel to establish" -NoNewline + if (-not (Wait-TunnelUp -TimeoutSeconds 60)) { + throw "Tunnel did not come up within 60 seconds. Check NetBird logs." + } + Write-Success "Tunnel is UP" +} + +# ============================================ +# Step 3: Verify DC Connectivity via Tunnel +# ============================================ +Write-Step "Step 3: Verifying DC Connectivity via Tunnel" + +$requiredPorts = @( + @{Name = "LDAP"; Port = 389}, + @{Name = "Kerberos"; Port = 88}, + @{Name = "DNS"; Port = 53} +) + +$allReachable = $true +foreach ($port in $requiredPorts) { + Write-Host " Testing $($port.Name) (port $($port.Port))... " -NoNewline + if (Test-DCConnectivity -DC $DCAddress -Port $port.Port) { + Write-Success "OK" + } else { + Write-Failure "FAILED" + $allReachable = $false + } +} + +if (-not $allReachable) { + throw "DC connectivity check failed. Ensure the tunnel routes DC traffic correctly." +} +Write-Success "All required DC ports reachable via tunnel" + +# ============================================ +# Step 4: NTP Sync with DC (Kerberos Requirement) +# ============================================ +Write-Step "Step 4: NTP Sync with Domain Controller" + +if ($PSCmdlet.ShouldProcess("W32Time", "Configure DC NTP")) { + # Configure DC as NTP source + w32tm /config /manualpeerlist:"$DCAddress" /syncfromflags:manual /reliable:no /update | Out-Null + Restart-Service W32Time -ErrorAction SilentlyContinue + Start-Sleep -Seconds 3 + w32tm /resync /nowait | Out-Null + + # Check time difference + $timeDiff = Get-TimeDifferenceSeconds -NtpServer $DCAddress + if ($null -ne $timeDiff) { + if ($timeDiff -gt 300) { + Write-Warning "Time difference is ${timeDiff}s (>5min). Kerberos may fail!" + Write-Host " Waiting for sync..." -NoNewline + Start-Sleep -Seconds 10 + w32tm /resync /nowait | Out-Null + Start-Sleep -Seconds 5 + $timeDiff = Get-TimeDifferenceSeconds -NtpServer $DCAddress + } + + if ($null -ne $timeDiff -and $timeDiff -le 300) { + Write-Success "Time synchronized (diff: ${timeDiff}s)" + } else { + Write-Warning "Could not verify time sync. Proceeding with caution." + } + } else { + Write-Warning "Could not measure time difference. Proceeding..." + } +} + +# ============================================ +# Step 5: Domain Join +# ============================================ +Write-Step "Step 5: Domain Join" + +# Check if already joined +$computerSystem = Get-WmiObject Win32_ComputerSystem +if ($computerSystem.PartOfDomain -and $computerSystem.Domain -eq $DomainName) { + Write-Success "Computer is already joined to $DomainName" +} else { + if ($PSCmdlet.ShouldProcess($DomainName, "Join domain")) { + Write-Host "Joining domain: $DomainName" + Write-Host "(You will be prompted for domain admin credentials)" + + $joinParams = @{ + DomainName = $DomainName + Credential = (Get-Credential -Message "Enter domain admin credentials for $DomainName") + Force = $true + Restart = $false + } + + if ($OUPath) { + $joinParams.OUPath = $OUPath + } + + try { + Add-Computer @joinParams + Write-Success "Domain join successful!" + } catch { + throw "Domain join failed: $_" + } + } +} + +# ============================================ +# Step 6: Certificate Enrollment +# ============================================ +Write-Step "Step 6: Machine Certificate Enrollment" + +if ($PSCmdlet.ShouldProcess("AD CS", "Request machine certificate")) { + Write-Host "Requesting machine certificate using template: $CertTemplateName" + + # Use certreq for enrollment + $infContent = @" +[NewRequest] +Subject = "CN=$env:COMPUTERNAME.$DomainName" +KeySpec = 1 +KeyLength = 2048 +Exportable = TRUE +MachineKeySet = TRUE +SMIME = FALSE +PrivateKeyArchive = FALSE +UserProtected = FALSE +UseExistingKeySet = FALSE +ProviderName = "Microsoft RSA SChannel Cryptographic Provider" +ProviderType = 12 +RequestType = PKCS10 +KeyUsage = 0xa0 + +[RequestAttributes] +CertificateTemplate = $CertTemplateName +"@ + + $infPath = "$env:TEMP\certreq.inf" + $reqPath = "$env:TEMP\certreq.req" + $cerPath = "$env:TEMP\certreq.cer" + + try { + Set-Content -Path $infPath -Value $infContent + + # Generate request + $result = certreq -new -machine $infPath $reqPath 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "certreq -new failed: $result" + } + + # Submit to CA (assumes auto-enrollment CA discovery) + $result = certreq -submit -machine $reqPath $cerPath 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "certreq -submit failed: $result" + } + + # Accept certificate + $result = certreq -accept -machine $cerPath 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "certreq -accept failed: $result" + } + + Write-Success "Certificate enrolled successfully" + + # Get thumbprint + $cert = Get-ChildItem Cert:\LocalMachine\My | + Where-Object { $_.Subject -match $env:COMPUTERNAME } | + Sort-Object NotAfter -Descending | + Select-Object -First 1 + + if ($cert) { + Write-Host " Certificate Thumbprint: $($cert.Thumbprint)" + Write-Host " Valid Until: $($cert.NotAfter)" + + # Save thumbprint for config update + $certThumbprint = $cert.Thumbprint + } + } finally { + # Cleanup temp files + Remove-Item $infPath, $reqPath, $cerPath -ErrorAction SilentlyContinue + } +} + +# ============================================ +# Step 7: Update NetBird Config for mTLS (Smart Selection v3.6) +# ============================================ +Write-Step "Step 7: Updating NetBird Config for mTLS (Phase 2 - Smart Selection)" + +if ($PSCmdlet.ShouldProcess($NetBirdConfigPath, "Enable mTLS with Smart Selection")) { + # v3.6: Use Smart Cert Selection - no thumbprint needed! + # Smart Selection automatically finds the right cert based on: + # - Template name match + # - SAN must match hostname.domain + # - Most recent valid cert + $configUpdate = @" + +# Machine Certificate Authentication (Phase 2 - Smart Selection v3.6) +# No thumbprint needed - NetBird automatically selects the right certificate! +machine_cert_enabled: true +machine_cert_template_name: $CertTemplateName +machine_cert_san_must_match: true +"@ + Add-Content $NetBirdConfigPath $configUpdate + + # Remove setup key from config (CRITICAL: security requirement!) + $config = Get-Content $NetBirdConfigPath -Raw + $config = $config -replace 'setup_key:.*\n', '' + Set-Content $NetBirdConfigPath $config + + Write-Success "Config updated for mTLS (Smart Selection)" + Write-Host " Template: $CertTemplateName" -ForegroundColor Gray + Write-Host " SAN Match: hostname.$DomainName" -ForegroundColor Gray + + if ($certThumbprint) { + Write-Host " Found Cert: $($certThumbprint.Substring(0,16))..." -ForegroundColor Gray + } +} + +# ============================================ +# Step 8: Restart or Prompt +# ============================================ +Write-Step "Step 8: Completing Bootstrap" + +# CRITICAL SECURITY WARNING +Write-Host @" +╔═══════════════════════════════════════════════════════════════════╗ +║ ⚠️ SECURITY ACTION REQUIRED ║ +║ ║ +║ REVOKE the Setup-Key in NetBird Dashboard immediately! ║ +║ ║ +║ Setup-Key used: $SetupKeyRedacted +║ ║ +║ The Setup-Key has been removed from local config, but it ║ +║ must also be revoked on the server to prevent reuse. ║ +║ ║ +║ Dashboard → Setup Keys → Find & Revoke ║ +╚═══════════════════════════════════════════════════════════════════╝ +"@ -ForegroundColor Yellow + +if ($NoRestart) { + Write-Host @" + +Bootstrap complete! Manual steps required: +1. REVOKE the Setup-Key in NetBird Dashboard (see above) +2. Restart the computer to complete domain join +3. After restart, the NetBird service will use mTLS (Phase 2) + +"@ -ForegroundColor Cyan +} else { + Write-Host @" + +Bootstrap complete! + +The computer will restart in 30 seconds to complete: +- Domain join finalization +- NetBird service restart with mTLS (Phase 2) + +REMEMBER: REVOKE the Setup-Key in NetBird Dashboard! + +Press Ctrl+C to cancel restart. + +"@ -ForegroundColor Green + + if ($PSCmdlet.ShouldProcess("Computer", "Restart")) { + Start-Sleep -Seconds 30 + Restart-Computer -Force + } +} + +#endregion diff --git a/scripts/lab/autounattend.xml b/scripts/lab/autounattend.xml new file mode 100644 index 00000000000..6ed1b65069f --- /dev/null +++ b/scripts/lab/autounattend.xml @@ -0,0 +1,180 @@ + + + + + + + + + en-US + + de-DE + en-US + en-US + de-DE + + + + + + + D:\vioscsi\2k25\amd64 + + + D:\viostor\2k25\amd64 + + + D:\NetKVM\2k25\amd64 + + + D:\Balloon\2k25\amd64 + + + D:\qxldod\2k25\amd64 + + + + + + 0 + true + + + + 1 + 260 + EFI + + + + 2 + 128 + MSR + + + + 3 + true + Primary + + + + + 1 + 1 + FAT32 + + + + 2 + 3 + NTFS + + C + + + + + + + + + + + /IMAGE/INDEX + 4 + + + + 0 + 3 + + + + + + true + + OnError + + + + + + + + + DC01 + W. Europe Standard Time + + + + false + + + + + + true + Remote Desktop + all + + + + + + + + + + true + true + true + true + true + 3 + + + + + NetBird-Lab-2025! + true</PlainText> + </AdministratorPassword> + </UserAccounts> + + <AutoLogon> + <Enabled>true</Enabled> + <Username>Administrator</Username> + <Password> + <Value>NetBird-Lab-2025!</Value> + <PlainText>true</PlainText> + </Password> + <LogonCount>3</LogonCount> + </AutoLogon> + + <!-- First logon: Enable OpenSSH, run setup script --> + <FirstLogonCommands> + <SynchronousCommand wcm:action="add"> + <Order>1</Order> + <CommandLine>powershell -Command "Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0"</CommandLine> + <Description>Install OpenSSH Server</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>2</Order> + <CommandLine>powershell -Command "Start-Service sshd; Set-Service -Name sshd -StartupType Automatic"</CommandLine> + <Description>Start and enable OpenSSH</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>3</Order> + <CommandLine>powershell -Command "New-NetFirewallRule -Name 'OpenSSH-Server' -DisplayName 'OpenSSH Server' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22"</CommandLine> + <Description>Open firewall for SSH</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>4</Order> + <CommandLine>powershell -Command "Set-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'"</CommandLine> + <Description>Set PowerShell as default SSH shell</Description> + </SynchronousCommand> + </FirstLogonCommands> + </component> + </settings> +</unattend> diff --git a/scripts/lab/setup-lab-ca.ps1 b/scripts/lab/setup-lab-ca.ps1 new file mode 100644 index 00000000000..3617b67e1ed --- /dev/null +++ b/scripts/lab/setup-lab-ca.ps1 @@ -0,0 +1,254 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Bootstraps AD CS with NetBirdMachine template for Lab environment +.DESCRIPTION + - Installs AD CS Enterprise Root CA (if not installed) + - Creates NetBirdMachine certificate template via ADSI + - Adds template to CA + - Configures Auto-Enrollment GPO + - Restricts RPC port range for firewall compatibility +.PARAMETER CAName + Common Name for the CA (default: TEST-CA) +.PARAMETER Domain + Domain name (default: test.local) +.EXAMPLE + .\setup-lab-ca.ps1 -CAName "CORP-CA" -Domain "corp.local" +#> + +param( + [string]$CAName = "TEST-CA", + [string]$Domain = "test.local" +) + +$ErrorActionPreference = "Stop" + +Write-Host "=== NetBird Lab CA Bootstrap ===" -ForegroundColor Cyan +Write-Host "CA Name: $CAName" -ForegroundColor Gray +Write-Host "Domain: $Domain" -ForegroundColor Gray +Write-Host "" + +# Helper function +function Test-ADCSInstalled { + $svc = Get-Service CertSvc -ErrorAction SilentlyContinue + return ($null -ne $svc) +} + +# ============================================================================= +# Step 1: Install AD CS Role +# ============================================================================= +Write-Host "[1/6] Checking AD CS Installation..." -ForegroundColor Yellow + +if (Test-ADCSInstalled) { + Write-Host " AD CS already installed, skipping." -ForegroundColor Green +} else { + Write-Host " Installing AD CS role..." -ForegroundColor Gray + Install-WindowsFeature -Name AD-Certificate, ADCS-Cert-Authority, ADCS-Web-Enrollment -IncludeManagementTools + + Write-Host " Configuring CA: $CAName..." -ForegroundColor Gray + Install-AdcsCertificationAuthority ` + -CAType EnterpriseRootCA ` + -CACommonName $CAName ` + -KeyLength 4096 ` + -HashAlgorithmName SHA256 ` + -ValidityPeriod Years ` + -ValidityPeriodUnits 10 ` + -Force + + Write-Host " AD CS installed and configured." -ForegroundColor Green +} + +# ============================================================================= +# Step 2: Restrict RPC Port Range (for firewall rules) +# ============================================================================= +Write-Host "[2/6] Configuring RPC Port Range (5000-5100)..." -ForegroundColor Yellow + +$currentPorts = netsh int ipv4 show dynamicport tcp +if ($currentPorts -match "Start Port\s*:\s*5000") { + Write-Host " RPC range already configured." -ForegroundColor Green +} else { + netsh int ipv4 set dynamicport tcp start=5000 num=100 + netsh int ipv4 set dynamicport udp start=5000 num=100 + Write-Host " RPC range set to 5000-5100." -ForegroundColor Green +} + +# ============================================================================= +# Step 3: Create NetBirdMachine Template via ADSI +# ============================================================================= +Write-Host "[3/6] Creating NetBirdMachine Certificate Template..." -ForegroundColor Yellow + +$configContext = ([ADSI]"LDAP://RootDSE").configurationNamingContext +$templateContainer = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$configContext" + +# Check if template already exists +$existingTemplate = [ADSI]"LDAP://CN=NetBirdMachine,$templateContainer" +if ($existingTemplate.Name) { + Write-Host " Template 'NetBirdMachine' already exists." -ForegroundColor Green +} else { + Write-Host " Creating template from 'Machine' base..." -ForegroundColor Gray + + # Get Machine template as base + $machineTemplate = [ADSI]"LDAP://CN=Machine,$templateContainer" + + # Create new template + $container = [ADSI]"LDAP://$templateContainer" + $newTemplate = $container.Create("pKICertificateTemplate", "CN=NetBirdMachine") + + # Copy base properties from Machine template + $newTemplate.Put("displayName", "NetBird Machine Authentication") + + # Generate unique OID (simplified - production should use proper OID generation) + $oidBase = "1.3.6.1.4.1.311.21.8" + $random = Get-Random -Minimum 1000000 -Maximum 9999999 + $templateOID = "$oidBase.$random.1" + $newTemplate.Put("msPKI-Cert-Template-OID", $templateOID) + + # Template flags + # msPKI-Certificate-Name-Flag: 0x18000000 = SUBJECT_ALT_REQUIRE_DNS | SUBJECT_ALT_REQUIRE_DOMAIN_DNS + $newTemplate.Put("msPKI-Certificate-Name-Flag", 402653184) + + # msPKI-Enrollment-Flag: 32 = AUTO_ENROLLMENT + $newTemplate.Put("msPKI-Enrollment-Flag", 32) + + # msPKI-Private-Key-Flag: 0 = NOT exportable + $newTemplate.Put("msPKI-Private-Key-Flag", 0) + + # msPKI-Minimal-Key-Size: 2048 + $newTemplate.Put("msPKI-Minimal-Key-Size", 2048) + + # pKIMaxIssuingDepth: 0 + $newTemplate.Put("pKIMaxIssuingDepth", 0) + + # pKIDefaultKeySpec: 1 (AT_KEYEXCHANGE) + $newTemplate.Put("pKIDefaultKeySpec", 1) + + # Validity: 1 year (in 100-nanosecond intervals) + $validity = [byte[]]@(0x00, 0x40, 0x1F, 0xD4, 0xB0, 0xCE, 0xFE, 0xFF) # 1 year + $newTemplate.Put("pKIExpirationPeriod", $validity) + + # Renewal: 6 weeks + $renewal = [byte[]]@(0x00, 0x80, 0xA6, 0x0A, 0xFF, 0xDE, 0xFF, 0xFF) # 6 weeks + $newTemplate.Put("pKIOverlapPeriod", $renewal) + + # EKU: Client Auth + Server Auth + $newTemplate.PutEx(2, "pKIExtendedKeyUsage", @("1.3.6.1.5.5.7.3.2", "1.3.6.1.5.5.7.3.1")) + + # Key Usage: Digital Signature + Key Encipherment + $newTemplate.Put("pKIKeyUsage", [byte[]]@(0xA0, 0x00)) + + # Schema version + $newTemplate.Put("msPKI-Template-Schema-Version", 2) + $newTemplate.Put("msPKI-Template-Minor-Revision", 1) + $newTemplate.Put("revision", 100) + + # Flags + $newTemplate.Put("flags", 131680) # CT_FLAG_PUBLISH_TO_DS | CT_FLAG_AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE + + $newTemplate.SetInfo() + Write-Host " Template 'NetBirdMachine' created." -ForegroundColor Green + + # Set permissions: Domain Computers can Enroll and AutoEnroll + Write-Host " Setting template permissions..." -ForegroundColor Gray + $template = [ADSI]"LDAP://CN=NetBirdMachine,$templateContainer" + $domainSID = (Get-ADDomain).DomainSID + $domainComputersSID = New-Object System.Security.Principal.SecurityIdentifier("$domainSID-515") + + # Create ACE for Enroll (ExtendedRight) + $enrollGUID = [GUID]"0e10c968-78fb-11d2-90d4-00c04f79dc55" + $aceEnroll = New-Object System.DirectoryServices.ActiveDirectoryAccessRule( + $domainComputersSID, + [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight, + [System.Security.AccessControl.AccessControlType]::Allow, + $enrollGUID + ) + + # Create ACE for AutoEnroll (ExtendedRight) + $autoEnrollGUID = [GUID]"a05b8cc2-17bc-4802-a710-e7c15ab866a2" + $aceAutoEnroll = New-Object System.DirectoryServices.ActiveDirectoryAccessRule( + $domainComputersSID, + [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight, + [System.Security.AccessControl.AccessControlType]::Allow, + $autoEnrollGUID + ) + + $template.ObjectSecurity.AddAccessRule($aceEnroll) + $template.ObjectSecurity.AddAccessRule($aceAutoEnroll) + $template.CommitChanges() + Write-Host " Permissions set for Domain Computers." -ForegroundColor Green +} + +# ============================================================================= +# Step 4: Add Template to CA +# ============================================================================= +Write-Host "[4/6] Adding Template to CA..." -ForegroundColor Yellow + +$caTemplates = certutil -CATemplates 2>&1 +if ($caTemplates -match "NetBirdMachine") { + Write-Host " Template already published to CA." -ForegroundColor Green +} else { + certutil -SetCATemplates +NetBirdMachine + Write-Host " Template added to CA." -ForegroundColor Green +} + +# ============================================================================= +# Step 5: Create Auto-Enrollment GPO +# ============================================================================= +Write-Host "[5/6] Creating Auto-Enrollment GPO..." -ForegroundColor Yellow + +$gpoName = "NetBird-AutoEnrollment" +$existingGPO = Get-GPO -Name $gpoName -ErrorAction SilentlyContinue + +if ($existingGPO) { + Write-Host " GPO '$gpoName' already exists." -ForegroundColor Green +} else { + $gpo = New-GPO -Name $gpoName + + # Link to domain root + $domainDN = (Get-ADDomain).DistinguishedName + New-GPLink -Name $gpoName -Target $domainDN -LinkEnabled Yes + + Write-Host " GPO created and linked to $domainDN" -ForegroundColor Green +} + +# Set Auto-Enrollment registry via GPO +$gpoPath = "HKLM\SOFTWARE\Policies\Microsoft\Cryptography\AutoEnrollment" +Set-GPRegistryValue -Name $gpoName -Key $gpoPath -ValueName "AEPolicy" -Type DWord -Value 7 +Set-GPRegistryValue -Name $gpoName -Key $gpoPath -ValueName "OfflineExpirationPercent" -Type DWord -Value 10 +Set-GPRegistryValue -Name $gpoName -Key $gpoPath -ValueName "OfflineExpirationStoreNames" -Type String -Value "MY" + +Write-Host " Auto-Enrollment settings configured (AEPolicy=7)." -ForegroundColor Green + +# ============================================================================= +# Step 6: Configure DCOM for Remote Enrollment +# ============================================================================= +Write-Host "[6/6] Configuring DCOM Access..." -ForegroundColor Yellow + +# Add Domain Computers to Certificate Service DCOM Access +$group = [ADSI]"WinNT://./Certificate Service DCOM Access,group" +$domainComputersGroup = "WinNT://$($Domain.Split('.')[0])/Domain Computers,group" + +try { + $group.Add($domainComputersGroup) + Write-Host " Domain Computers added to DCOM Access group." -ForegroundColor Green +} catch { + if ($_.Exception.Message -match "already a member") { + Write-Host " Domain Computers already in DCOM Access group." -ForegroundColor Green + } else { + Write-Host " Warning: Could not add to DCOM group: $_" -ForegroundColor Yellow + } +} + +# ============================================================================= +# Summary +# ============================================================================= +Write-Host "" +Write-Host "=== Bootstrap Complete ===" -ForegroundColor Green +Write-Host "" +Write-Host "Next steps:" -ForegroundColor Cyan +Write-Host "1. Run 'gpupdate /force' on domain controllers" +Write-Host "2. Domain-join a test client" +Write-Host "3. Run test-client-enrollment.ps1 on the client" +Write-Host "" +Write-Host "Verification:" -ForegroundColor Cyan +Write-Host " certutil -CATemplates | findstr NetBird" +Write-Host " Get-GPO -Name 'NetBird-AutoEnrollment'" diff --git a/scripts/lab/test-client-enrollment.ps1 b/scripts/lab/test-client-enrollment.ps1 new file mode 100644 index 00000000000..4d112b9309f --- /dev/null +++ b/scripts/lab/test-client-enrollment.ps1 @@ -0,0 +1,275 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Test certificate auto-enrollment on a domain-joined client +.DESCRIPTION + Tests Machine Certificate enrollment by: + 1. Checking domain membership + 2. Verifying GPO settings + 3. Running enrollment in SYSTEM context (required for machine certs!) + 4. Validating the issued certificate + + IMPORTANT: Machine certificate enrollment MUST run as SYSTEM (LocalSystem), + not as a logged-in user. This script uses a Scheduled Task to achieve this. +.PARAMETER TemplateName + Certificate template name (default: NetBirdMachine) +.PARAMETER CAConfig + CA configuration string (default: auto-detect) +.PARAMETER Force + Force new enrollment even if certificate exists +.EXAMPLE + .\test-client-enrollment.ps1 +.EXAMPLE + .\test-client-enrollment.ps1 -Force -Verbose +#> + +[CmdletBinding()] +param( + [string]$TemplateName = "NetBirdMachine", + [string]$CAConfig = "", + [switch]$Force +) + +$ErrorActionPreference = "Stop" + +Write-Host "=== Certificate Enrollment Test ===" -ForegroundColor Cyan +Write-Host "Computer: $env:COMPUTERNAME" -ForegroundColor Gray +Write-Host "Template: $TemplateName" -ForegroundColor Gray +Write-Host "Time: $(Get-Date)" -ForegroundColor Gray +Write-Host "" + +# ============================================================================= +# Pre-Flight Checks +# ============================================================================= +Write-Host "[1/5] Pre-flight checks..." -ForegroundColor Yellow + +# Check domain membership +$cs = Get-WmiObject Win32_ComputerSystem +if (-not $cs.PartOfDomain) { + Write-Host " ERROR: Computer is not domain-joined!" -ForegroundColor Red + Write-Host " Run: Add-Computer -DomainName <domain> -Credential (Get-Credential)" -ForegroundColor Yellow + exit 1 +} +Write-Host " Domain: $($cs.Domain)" -ForegroundColor Green + +# Check if certificate already exists +$existingCert = Get-ChildItem Cert:\LocalMachine\My | Where-Object { + $_.Subject -match "CN=$env:COMPUTERNAME" -and + $_.NotAfter -gt (Get-Date) +} + +if ($existingCert -and -not $Force) { + Write-Host " Machine certificate already exists:" -ForegroundColor Green + Write-Host " Subject: $($existingCert.Subject)" -ForegroundColor Gray + Write-Host " Thumbprint: $($existingCert.Thumbprint)" -ForegroundColor Gray + Write-Host " Expires: $($existingCert.NotAfter)" -ForegroundColor Gray + Write-Host "" + Write-Host " Use -Force to request a new certificate anyway." -ForegroundColor Yellow + exit 0 +} + +# ============================================================================= +# GPO Update +# ============================================================================= +Write-Host "[2/5] Updating Group Policy..." -ForegroundColor Yellow +$gpResult = gpupdate /force /target:computer 2>&1 +if ($LASTEXITCODE -eq 0) { + Write-Host " GPO update successful." -ForegroundColor Green +} else { + Write-Host " GPO update may have issues: $gpResult" -ForegroundColor Yellow +} + +# Check AEPolicy registry value +$aePath = "HKLM:\SOFTWARE\Policies\Microsoft\Cryptography\AutoEnrollment" +$aePolicy = Get-ItemProperty -Path $aePath -Name "AEPolicy" -ErrorAction SilentlyContinue +if ($aePolicy -and $aePolicy.AEPolicy -eq 7) { + Write-Host " AEPolicy = 7 (Auto-enrollment enabled)" -ForegroundColor Green +} else { + Write-Host " WARNING: AEPolicy not set to 7. GPO may not be applied." -ForegroundColor Yellow +} + +# ============================================================================= +# SYSTEM Context Enrollment (via Scheduled Task) +# ============================================================================= +Write-Host "[3/5] Running enrollment as SYSTEM..." -ForegroundColor Yellow +Write-Host " (Machine certs require SYSTEM context, not user context)" -ForegroundColor Gray + +$taskName = "NetBird-CertEnroll-Test" +$resultFile = "$env:TEMP\cert-enroll-result.txt" +$certFile = "$env:TEMP\machine-cert.cer" + +# Cleanup previous files +Remove-Item $resultFile -ErrorAction SilentlyContinue +Remove-Item $certFile -ErrorAction SilentlyContinue + +# Script to run as SYSTEM +$enrollScript = @" +`$ErrorActionPreference = 'Continue' +`$log = @() +`$log += "=== SYSTEM Enrollment Log ===" +`$log += "Time: `$(Get-Date)" +`$log += "Identity: `$(whoami)" +`$log += "" + +# Check Kerberos tickets +`$log += "Kerberos tickets:" +`$klist = klist -li 0x3e7 2>&1 +`$log += `$klist | Out-String + +# Trigger certificate pulse +`$log += "Running certutil -pulse..." +`$pulse = certutil -pulse 2>&1 +`$log += `$pulse | Out-String + +# Wait for enrollment +Start-Sleep -Seconds 5 + +# Check for certificate +`$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object { + `$_.Subject -match "CN=`$env:COMPUTERNAME" +} | Sort-Object NotAfter -Descending | Select-Object -First 1 + +if (`$cert) { + `$log += "SUCCESS: Certificate found!" + `$log += "Subject: `$(`$cert.Subject)" + `$log += "Thumbprint: `$(`$cert.Thumbprint)" + `$log += "Issuer: `$(`$cert.Issuer)" + `$log += "NotAfter: `$(`$cert.NotAfter)" + `$log += "HasPrivateKey: `$(`$cert.HasPrivateKey)" + + # Check SAN + `$san = `$cert.DnsNameList | ForEach-Object { `$_.Unicode } + `$log += "SAN DNS Names: `$(`$san -join ', ')" + + # Check EKU + `$eku = `$cert.EnhancedKeyUsageList | ForEach-Object { `$_.FriendlyName } + `$log += "EKU: `$(`$eku -join ', ')" + + # Export public cert for verification + `$certBytes = `$cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert) + [System.IO.File]::WriteAllBytes("$certFile", `$certBytes) +} else { + `$log += "FAILED: No certificate found after enrollment" + + # Try to get more info + `$log += "" + `$log += "Attempting manual enrollment..." + `$inf = @" +[NewRequest] +Subject = "CN=`$env:COMPUTERNAME.`$((Get-WmiObject Win32_ComputerSystem).Domain)" +KeyLength = 2048 +Exportable = FALSE +MachineKeySet = TRUE +[RequestAttributes] +CertificateTemplate = $TemplateName +"@ + `$inf | Out-File "`$env:TEMP\machine.inf" -Encoding ASCII + `$req = certreq -new -machine "`$env:TEMP\machine.inf" "`$env:TEMP\machine.csr" 2>&1 + `$log += `$req | Out-String +} + +`$log | Out-File "$resultFile" -Encoding UTF8 +"@ + +# Save script to temp file +$scriptFile = "$env:TEMP\enroll-as-system.ps1" +$enrollScript | Out-File $scriptFile -Encoding UTF8 + +# Create and run scheduled task as SYSTEM +$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -File `"$scriptFile`"" +$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest +$task = New-ScheduledTask -Action $action -Principal $principal + +# Remove existing task if present +Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + +# Register and run +Register-ScheduledTask -TaskName $taskName -InputObject $task -Force | Out-Null +Start-ScheduledTask -TaskName $taskName + +# Wait for completion +Write-Host " Waiting for enrollment (max 30 seconds)..." -ForegroundColor Gray +$timeout = 30 +$elapsed = 0 +while ($elapsed -lt $timeout) { + Start-Sleep -Seconds 2 + $elapsed += 2 + + $taskInfo = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue + if ($taskInfo.State -eq "Ready") { + break + } +} + +# Cleanup task +Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + +# ============================================================================= +# Results +# ============================================================================= +Write-Host "[4/5] Checking results..." -ForegroundColor Yellow + +if (Test-Path $resultFile) { + $results = Get-Content $resultFile -Raw + Write-Verbose $results + + if ($results -match "SUCCESS: Certificate found") { + Write-Host " Enrollment SUCCESSFUL!" -ForegroundColor Green + } else { + Write-Host " Enrollment may have failed. Details:" -ForegroundColor Yellow + Write-Host $results -ForegroundColor Gray + } +} else { + Write-Host " ERROR: Result file not created. Task may have failed." -ForegroundColor Red +} + +# ============================================================================= +# Final Verification +# ============================================================================= +Write-Host "[5/5] Final verification..." -ForegroundColor Yellow + +$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object { + $_.Subject -match "CN=$env:COMPUTERNAME" +} | Sort-Object NotAfter -Descending | Select-Object -First 1 + +if ($cert) { + Write-Host "" + Write-Host "=== Machine Certificate ===" -ForegroundColor Green + Write-Host "Subject: $($cert.Subject)" + Write-Host "Issuer: $($cert.Issuer)" + Write-Host "Thumbprint: $($cert.Thumbprint)" + Write-Host "Valid From: $($cert.NotBefore)" + Write-Host "Valid Until: $($cert.NotAfter)" + Write-Host "PrivateKey: $($cert.HasPrivateKey)" + + # SAN + $sanList = $cert.DnsNameList | ForEach-Object { $_.Unicode } + Write-Host "SAN DNS: $($sanList -join ', ')" + + # EKU + $ekuList = $cert.EnhancedKeyUsageList | ForEach-Object { "$($_.FriendlyName) ($($_.ObjectId))" } + Write-Host "EKU: $($ekuList -join ', ')" + + # Exportable check + try { + $exportable = $cert.PrivateKey.CspKeyContainerInfo.Exportable + Write-Host "Exportable: $exportable" + } catch { + Write-Host "Exportable: (could not determine)" + } + + Write-Host "" + Write-Host "Certificate enrollment successful!" -ForegroundColor Green + exit 0 +} else { + Write-Host "" + Write-Host "=== FAILED ===" -ForegroundColor Red + Write-Host "No machine certificate found after enrollment." + Write-Host "" + Write-Host "Troubleshooting:" -ForegroundColor Yellow + Write-Host "1. Check Event Log: Applications and Services > Microsoft > Windows > CertificateServicesClient-AutoEnrollment" + Write-Host "2. Verify template permissions: certtmpl.msc > NetBirdMachine > Security" + Write-Host "3. Test CA connectivity: certutil -ping -config <CA>" + Write-Host "4. Check DCOM permissions on CA server" + exit 1 +} diff --git a/scripts/lab/verify-lab-ca.ps1 b/scripts/lab/verify-lab-ca.ps1 new file mode 100644 index 00000000000..dc90e58990c --- /dev/null +++ b/scripts/lab/verify-lab-ca.ps1 @@ -0,0 +1,240 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Verifies Lab CA setup is complete and working +.DESCRIPTION + Checks all components required for NetBird Machine Certificate enrollment: + - AD CS Service + - NetBirdMachine Template + - Auto-Enrollment GPO + - RPC Port Range + - DNS Service + - DCOM Permissions +.EXAMPLE + .\verify-lab-ca.ps1 +#> + +$ErrorActionPreference = "Continue" + +Write-Host "=== NetBird Lab CA Verification ===" -ForegroundColor Cyan +Write-Host "Running on: $env:COMPUTERNAME" -ForegroundColor Gray +Write-Host "Time: $(Get-Date)" -ForegroundColor Gray +Write-Host "" + +$checks = @{ + Passed = 0 + Failed = 0 + Warnings = 0 +} + +function Write-Check { + param( + [string]$Name, + [string]$Status, # Pass, Fail, Warn + [string]$Message + ) + + switch ($Status) { + "Pass" { + Write-Host " [PASS] " -ForegroundColor Green -NoNewline + Write-Host "$Name" -ForegroundColor White + if ($Message) { Write-Host " $Message" -ForegroundColor Gray } + $script:checks.Passed++ + } + "Fail" { + Write-Host " [FAIL] " -ForegroundColor Red -NoNewline + Write-Host "$Name" -ForegroundColor White + if ($Message) { Write-Host " $Message" -ForegroundColor Yellow } + $script:checks.Failed++ + } + "Warn" { + Write-Host " [WARN] " -ForegroundColor Yellow -NoNewline + Write-Host "$Name" -ForegroundColor White + if ($Message) { Write-Host " $Message" -ForegroundColor Gray } + $script:checks.Warnings++ + } + } +} + +# ============================================================================= +# Check 1: CA Service +# ============================================================================= +Write-Host "[1/7] Checking CA Service..." -ForegroundColor Yellow + +$svc = Get-Service CertSvc -ErrorAction SilentlyContinue +if ($null -eq $svc) { + Write-Check "CertSvc" "Fail" "AD CS not installed" +} elseif ($svc.Status -eq "Running") { + Write-Check "CertSvc" "Pass" "Service is running" +} else { + Write-Check "CertSvc" "Fail" "Service status: $($svc.Status)" +} + +# ============================================================================= +# Check 2: CA Configuration +# ============================================================================= +Write-Host "[2/7] Checking CA Configuration..." -ForegroundColor Yellow + +$caInfo = certutil -getreg CA\CommonName 2>&1 | Out-String +if ($caInfo -match "CommonName\s+REG_SZ\s+=\s+(.+)") { + $caName = $Matches[1].Trim() + Write-Check "CA Name" "Pass" $caName +} else { + Write-Check "CA Name" "Warn" "Could not parse CA name from registry" +} + +# ============================================================================= +# Check 3: NetBirdMachine Template +# ============================================================================= +Write-Host "[3/7] Checking NetBirdMachine Template..." -ForegroundColor Yellow + +$templates = certutil -CATemplates 2>&1 +if ($templates -match "NetBirdMachine") { + Write-Check "Template Published" "Pass" "NetBirdMachine is available on CA" + + # Check template properties in AD + $configContext = ([ADSI]"LDAP://RootDSE").configurationNamingContext + $templatePath = "CN=NetBirdMachine,CN=Certificate Templates,CN=Public Key Services,CN=Services,$configContext" + $template = [ADSI]"LDAP://$templatePath" + + if ($template.Name) { + # Check EKU + $eku = $template."pKIExtendedKeyUsage" + if ($eku -contains "1.3.6.1.5.5.7.3.2") { + Write-Check "Template EKU" "Pass" "Client Authentication present" + } else { + Write-Check "Template EKU" "Fail" "Missing Client Authentication EKU" + } + + # Check Name Flag (DNS in SAN) + $nameFlag = [int]($template."msPKI-Certificate-Name-Flag"[0]) + if ($nameFlag -band 0x8000000) { + Write-Check "Template SAN" "Pass" "DNS name in SAN enabled (flag: 0x$($nameFlag.ToString('X')))" + } else { + Write-Check "Template SAN" "Warn" "DNS name in SAN may not be configured (flag: 0x$($nameFlag.ToString('X')))" + } + + # Check Private Key Flag + $pkFlag = [int]($template."msPKI-Private-Key-Flag"[0]) + if ($pkFlag -eq 0) { + Write-Check "Private Key" "Pass" "Not exportable" + } else { + Write-Check "Private Key" "Warn" "May be exportable (flag: $pkFlag)" + } + + # Check Enrollment Flag + $enrollFlag = [int]($template."msPKI-Enrollment-Flag"[0]) + if ($enrollFlag -band 32) { + Write-Check "Auto-Enrollment" "Pass" "Enabled on template (flag: $enrollFlag)" + } else { + Write-Check "Auto-Enrollment" "Warn" "May not be enabled on template (flag: $enrollFlag)" + } + } +} else { + Write-Check "Template Published" "Fail" "NetBirdMachine not found in CA templates" +} + +# ============================================================================= +# Check 4: Auto-Enrollment GPO +# ============================================================================= +Write-Host "[4/7] Checking Auto-Enrollment GPO..." -ForegroundColor Yellow + +try { + $gpo = Get-GPO -Name "NetBird-AutoEnrollment" -ErrorAction Stop + Write-Check "GPO Exists" "Pass" "ID: $($gpo.Id)" + + # Check if linked by searching domain for GPLinks containing this GPO + $domainDN = (Get-ADDomain).DistinguishedName + $gpoLink = Get-ADObject -Filter { objectClass -eq "domainDNS" } -SearchBase $domainDN -Properties gPLink -ErrorAction SilentlyContinue + if ($gpoLink.gPLink -match $gpo.Id) { + Write-Check "GPO Linked" "Pass" "Linked to domain root" + } else { + Write-Check "GPO Linked" "Warn" "GPO exists but may not be linked to domain" + } + + # Check registry values + $regValues = Get-GPRegistryValue -Name "NetBird-AutoEnrollment" -Key "HKLM\SOFTWARE\Policies\Microsoft\Cryptography\AutoEnrollment" -ErrorAction SilentlyContinue + $aePolicy = $regValues | Where-Object { $_.ValueName -eq "AEPolicy" } + if ($aePolicy -and $aePolicy.Value -eq 7) { + Write-Check "AEPolicy" "Pass" "Value = 7 (Enroll + Renew + Update)" + } else { + Write-Check "AEPolicy" "Warn" "AEPolicy may not be configured correctly" + } +} catch { + Write-Check "GPO Exists" "Fail" "GPO 'NetBird-AutoEnrollment' not found" +} + +# ============================================================================= +# Check 5: RPC Port Range +# ============================================================================= +Write-Host "[5/7] Checking RPC Port Range..." -ForegroundColor Yellow + +$rpcConfig = netsh int ipv4 show dynamicport tcp | Out-String +if ($rpcConfig -match "Start Port\s*:\s*(\d+)") { + $startPort = [int]$Matches[1] + if ($startPort -eq 5000) { + Write-Check "RPC Range" "Pass" "Start port: 5000 (restricted for firewall)" + } elseif ($startPort -eq 49152) { + Write-Check "RPC Range" "Warn" "Default range (49152-65535) - consider restricting to 5000-5100" + } else { + Write-Check "RPC Range" "Pass" "Start port: $startPort" + } +} else { + Write-Check "RPC Range" "Warn" "Could not determine RPC port range" +} + +# ============================================================================= +# Check 6: DNS Service +# ============================================================================= +Write-Host "[6/7] Checking DNS Service..." -ForegroundColor Yellow + +$dns = Get-Service DNS -ErrorAction SilentlyContinue +if ($null -eq $dns) { + Write-Check "DNS Service" "Warn" "DNS service not found (may not be a DC)" +} elseif ($dns.Status -eq "Running") { + Write-Check "DNS Service" "Pass" "DNS is running" +} else { + Write-Check "DNS Service" "Fail" "DNS status: $($dns.Status)" +} + +# ============================================================================= +# Check 7: DCOM Permissions +# ============================================================================= +Write-Host "[7/7] Checking DCOM Permissions..." -ForegroundColor Yellow + +try { + $group = [ADSI]"WinNT://./Certificate Service DCOM Access,group" + $members = @($group.Invoke("Members")) | ForEach-Object { + $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null) + } + + if ($members -contains "Domain Computers") { + Write-Check "DCOM Access" "Pass" "Domain Computers in DCOM group" + } else { + Write-Check "DCOM Access" "Warn" "Domain Computers may not be in DCOM group" + } +} catch { + Write-Check "DCOM Access" "Warn" "Could not check DCOM group: $_" +} + +# ============================================================================= +# Summary +# ============================================================================= +Write-Host "" +Write-Host "=== Summary ===" -ForegroundColor Cyan +Write-Host " Passed: $($checks.Passed)" -ForegroundColor Green +Write-Host " Warnings: $($checks.Warnings)" -ForegroundColor Yellow +Write-Host " Failed: $($checks.Failed)" -ForegroundColor Red +Write-Host "" + +if ($checks.Failed -eq 0) { + if ($checks.Warnings -eq 0) { + Write-Host "All checks passed! CA is ready for use." -ForegroundColor Green + } else { + Write-Host "CA is functional with warnings. Review warnings above." -ForegroundColor Yellow + } + exit 0 +} else { + Write-Host "CA setup incomplete. Fix failed checks before proceeding." -ForegroundColor Red + exit 1 +} diff --git a/scripts/reinstall-and-test.ps1 b/scripts/reinstall-and-test.ps1 new file mode 100644 index 00000000000..86d07785e72 --- /dev/null +++ b/scripts/reinstall-and-test.ps1 @@ -0,0 +1,275 @@ +<# +.SYNOPSIS + Reinstall and test NetBird Machine Tunnel after reset. + +.DESCRIPTION + This script automates the reinstall and test cycle: + 1. Runs reset-netbird-machine.ps1 for clean state + 2. Installs the new binary + 3. Starts the service + 4. Verifies basic functionality + +.PARAMETER BinaryPath + Path to the netbird-machine.exe binary to install. + +.PARAMETER ConfigPath + Optional path to a config file to use. + +.PARAMETER SkipReset + Skip the reset step (useful if already reset). + +.PARAMETER WaitForTunnel + Wait for tunnel to establish (timeout in seconds). + +.EXAMPLE + .\reinstall-and-test.ps1 -BinaryPath .\netbird-machine.exe + Full reset, install, and test cycle. + +.EXAMPLE + .\reinstall-and-test.ps1 -BinaryPath .\netbird-machine.exe -SkipReset + Install without reset (assumes clean state). + +.NOTES + Requires: Administrator privileges + Author: NetBird Machine Tunnel Fork + Version: 1.0.0 +#> + +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory = $true)] + [ValidateScript({ Test-Path $_ })] + [string]$BinaryPath, + + [Parameter(Mandatory = $false)] + [string]$ConfigPath, + + [Parameter(Mandatory = $false)] + [switch]$SkipReset, + + [Parameter(Mandatory = $false)] + [int]$WaitForTunnel = 60 +) + +$ErrorActionPreference = 'Stop' + +# Configuration +$ServiceName = "NetBirdMachine" +$InterfaceName = "wg-nb-machine" +$InstallPath = "$env:ProgramFiles\NetBird Machine" +$ConfigDir = "$env:ProgramData\NetBird" + +#region Helper Functions + +function Write-Step { + param([string]$Message) + Write-Host "`n========================================" -ForegroundColor Cyan + Write-Host " $Message" -ForegroundColor Cyan + Write-Host "========================================`n" -ForegroundColor Cyan +} + +function Write-Success { + param([string]$Message) + Write-Host "[OK] $Message" -ForegroundColor Green +} + +function Write-Failure { + param([string]$Message) + Write-Host "[FAIL] $Message" -ForegroundColor Red +} + +function Test-Administrator { + $currentUser = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) + return $currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function Wait-TunnelUp { + param([int]$TimeoutSeconds) + + $elapsed = 0 + while ($elapsed -lt $TimeoutSeconds) { + $adapter = Get-NetAdapter -Name $InterfaceName -ErrorAction SilentlyContinue + if ($adapter -and $adapter.Status -eq 'Up') { + return $true + } + Start-Sleep -Seconds 2 + $elapsed += 2 + Write-Host "." -NoNewline + } + Write-Host "" + return $false +} + +#endregion + +#region Main Script + +# Check administrator +if (-not (Test-Administrator)) { + throw "This script must be run as Administrator" +} + +Write-Host @" + +╔═══════════════════════════════════════════════════════════════════╗ +║ NetBird Machine Tunnel Reinstall & Test ║ +╚═══════════════════════════════════════════════════════════════════╝ + +"@ -ForegroundColor Cyan + +# ============================================ +# Step 1: Reset (Optional) +# ============================================ +if (-not $SkipReset) { + Write-Step "Step 1: Resetting previous installation" + + $resetScript = Join-Path $PSScriptRoot "reset-netbird-machine.ps1" + if (Test-Path $resetScript) { + & $resetScript -Force -KeepConfig:$false + } else { + Write-Host "Reset script not found, performing manual cleanup..." + # Inline minimal cleanup + Stop-Service $ServiceName -Force -ErrorAction SilentlyContinue + sc.exe delete $ServiceName 2>&1 | Out-Null + } + + # Verify cleanup + $verifyScript = Join-Path $PSScriptRoot "verify-nrpt-cleanup.ps1" + if (Test-Path $verifyScript) { + & $verifyScript + } + + Write-Success "Reset complete" +} else { + Write-Host "Skipping reset (--SkipReset)" -ForegroundColor Yellow +} + +# ============================================ +# Step 2: Install Binary +# ============================================ +Write-Step "Step 2: Installing binary" + +# Create install directory +if (-not (Test-Path $InstallPath)) { + New-Item -Path $InstallPath -ItemType Directory -Force | Out-Null +} + +# Copy binary +$targetBinary = Join-Path $InstallPath "netbird-machine.exe" +Copy-Item -Path $BinaryPath -Destination $targetBinary -Force +Write-Success "Binary copied to: $targetBinary" + +# Create config directory +if (-not (Test-Path $ConfigDir)) { + New-Item -Path $ConfigDir -ItemType Directory -Force | Out-Null +} + +# Copy config if provided +if ($ConfigPath -and (Test-Path $ConfigPath)) { + $targetConfig = Join-Path $ConfigDir "config.yaml" + Copy-Item -Path $ConfigPath -Destination $targetConfig -Force + Write-Success "Config copied to: $targetConfig" +} + +# ============================================ +# Step 3: Install Service +# ============================================ +Write-Step "Step 3: Installing service" + +if ($PSCmdlet.ShouldProcess($targetBinary, "Install service")) { + $result = & $targetBinary install 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Success "Service installed" + } else { + Write-Failure "Service install failed: $result" + throw "Service installation failed" + } +} + +# ============================================ +# Step 4: Start Service +# ============================================ +Write-Step "Step 4: Starting service" + +if ($PSCmdlet.ShouldProcess($ServiceName, "Start service")) { + Start-Service $ServiceName -ErrorAction Stop + Start-Sleep -Seconds 2 + + $service = Get-Service $ServiceName + if ($service.Status -eq 'Running') { + Write-Success "Service started" + } else { + Write-Failure "Service status: $($service.Status)" + throw "Service failed to start" + } +} + +# ============================================ +# Step 5: Wait for Tunnel +# ============================================ +Write-Step "Step 5: Waiting for tunnel ($WaitForTunnel seconds max)" + +Write-Host "Waiting for interface $InterfaceName" -NoNewline +if (Wait-TunnelUp -TimeoutSeconds $WaitForTunnel) { + Write-Success "Tunnel is UP" + + # Get interface details + $adapter = Get-NetAdapter -Name $InterfaceName + $ipConfig = Get-NetIPAddress -InterfaceAlias $InterfaceName -ErrorAction SilentlyContinue + + Write-Host "`nInterface Details:" -ForegroundColor White + Write-Host " Name: $($adapter.Name)" + Write-Host " Status: $($adapter.Status)" + Write-Host " MAC: $($adapter.MacAddress)" + if ($ipConfig) { + Write-Host " IP: $($ipConfig.IPAddress)" + } +} else { + Write-Failure "Tunnel did not come up within $WaitForTunnel seconds" + Write-Host "`nCheck logs:" -ForegroundColor Yellow + Write-Host " Get-EventLog -LogName Application -Source $ServiceName -Newest 20" + throw "Tunnel establishment timeout" +} + +# ============================================ +# Step 6: Basic Connectivity Test +# ============================================ +Write-Step "Step 6: Basic connectivity test" + +# Check if we can ping the NetBird network +$testTargets = @( + @{ Name = "NetBird Gateway"; IP = "100.64.0.1" } +) + +$allPassed = $true +foreach ($target in $testTargets) { + Write-Host " Testing $($target.Name) ($($target.IP))... " -NoNewline + $ping = Test-Connection -ComputerName $target.IP -Count 1 -Quiet -ErrorAction SilentlyContinue + if ($ping) { + Write-Success "OK" + } else { + Write-Host "[--] Not reachable (may be expected)" -ForegroundColor Gray + } +} + +# ============================================ +# Summary +# ============================================ +Write-Host @" + +╔═══════════════════════════════════════════════════════════════════╗ +║ Installation Complete ║ +╚═══════════════════════════════════════════════════════════════════╝ + +Service: $ServiceName (Running) +Interface: $InterfaceName (Up) +Binary: $targetBinary + +Next steps: +1. Check service logs: Get-EventLog -LogName Application -Source $ServiceName -Newest 20 +2. Test DC connectivity (if configured) +3. Test domain operations + +"@ -ForegroundColor Green + +#endregion diff --git a/scripts/reset-netbird-machine.ps1 b/scripts/reset-netbird-machine.ps1 new file mode 100644 index 00000000000..ff975f5c658 --- /dev/null +++ b/scripts/reset-netbird-machine.ps1 @@ -0,0 +1,311 @@ +<# +.SYNOPSIS + Reset NetBird Machine Tunnel for testing purposes. + Safely removes service, interface, NRPT rules, and firewall rules. + +.DESCRIPTION + This script performs a complete cleanup of the NetBird Machine Tunnel: + 1. Stops and removes the NetBird Machine service + 2. Removes the WireGuard tunnel interface + 3. Removes ONLY NetBird-specific NRPT rules (scoped by registry key hash) + 4. Removes NetBird-specific firewall rules + 5. Optionally cleans up configuration files + + IMPORTANT: This script uses scoped cleanup - it will NOT remove other + NRPT rules or firewall rules that are not NetBird-related. + +.PARAMETER Force + Skip confirmation prompts. + +.PARAMETER KeepConfig + Keep configuration files (useful for re-testing with same config). + +.PARAMETER Verbose + Show detailed progress information. + +.EXAMPLE + .\reset-netbird-machine.ps1 -Force + Performs full reset without prompts. + +.EXAMPLE + .\reset-netbird-machine.ps1 -KeepConfig + Reset but keep config files for next test. + +.NOTES + Requires: Administrator privileges + Author: NetBird Machine Tunnel Fork + Version: 1.0.0 +#> + +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory = $false)] + [switch]$Force, + + [Parameter(Mandatory = $false)] + [switch]$KeepConfig +) + +$ErrorActionPreference = 'Continue' # Continue on errors to ensure full cleanup + +# Configuration +$ServiceName = "NetBirdMachine" +$InterfaceName = "wg-nb-machine" +$ConfigPath = "$env:ProgramData\NetBird" +$NRPTRegistryPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig" +$NRPTKeyPrefix = "NetBird-Machine-" +$FirewallRulePrefix = "NetBird-Machine-" + +#region Helper Functions + +function Write-Step { + param([string]$Message) + Write-Host "`n>> $Message" -ForegroundColor Cyan +} + +function Write-Success { + param([string]$Message) + Write-Host " [OK] $Message" -ForegroundColor Green +} + +function Write-Skipped { + param([string]$Message) + Write-Host " [--] $Message" -ForegroundColor Gray +} + +function Write-Warning { + param([string]$Message) + Write-Host " [!!] $Message" -ForegroundColor Yellow +} + +function Write-Failure { + param([string]$Message) + Write-Host " [XX] $Message" -ForegroundColor Red +} + +function Test-Administrator { + $currentUser = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) + return $currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +#endregion + +#region Main Script + +# Check administrator +if (-not (Test-Administrator)) { + Write-Error "This script must be run as Administrator" + exit 1 +} + +Write-Host @" + +╔═══════════════════════════════════════════════════════════════════╗ +║ NetBird Machine Tunnel Reset Script ║ +║ ║ +║ Safely removes service, interface, NRPT, and firewall rules ║ +╚═══════════════════════════════════════════════════════════════════╝ + +"@ -ForegroundColor Cyan + +# Confirmation +if (-not $Force) { + $confirm = Read-Host "This will reset the NetBird Machine Tunnel. Continue? (y/N)" + if ($confirm -ne 'y' -and $confirm -ne 'Y') { + Write-Host "Aborted." -ForegroundColor Yellow + exit 0 + } +} + +# ============================================ +# Step 1: Stop and Remove Service +# ============================================ +Write-Step "Step 1: Stopping and removing service" + +$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if ($service) { + if ($service.Status -eq 'Running') { + if ($PSCmdlet.ShouldProcess($ServiceName, "Stop service")) { + Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + Write-Success "Service stopped" + } + } else { + Write-Skipped "Service already stopped" + } + + # Remove service using sc.exe + if ($PSCmdlet.ShouldProcess($ServiceName, "Remove service")) { + $result = sc.exe delete $ServiceName 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Success "Service removed" + } else { + Write-Warning "Service removal returned: $result" + } + } +} else { + Write-Skipped "Service not installed" +} + +# ============================================ +# Step 2: Remove WireGuard Interface +# ============================================ +Write-Step "Step 2: Removing WireGuard interface" + +$adapter = Get-NetAdapter -Name $InterfaceName -ErrorAction SilentlyContinue +if ($adapter) { + if ($PSCmdlet.ShouldProcess($InterfaceName, "Remove network adapter")) { + # Disable first + Disable-NetAdapter -Name $InterfaceName -Confirm:$false -ErrorAction SilentlyContinue + Start-Sleep -Seconds 1 + + # Remove via netsh (works for WireGuard interfaces) + $result = netsh interface delete interface name="$InterfaceName" 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Success "Interface removed via netsh" + } else { + # Try WireGuard-specific removal if available + $wgExe = "C:\Program Files\WireGuard\wireguard.exe" + if (Test-Path $wgExe) { + & $wgExe /uninstalltunnelservice $InterfaceName 2>&1 | Out-Null + Write-Success "Interface removed via WireGuard" + } else { + Write-Warning "Interface may require manual removal" + } + } + } +} else { + Write-Skipped "Interface not present" +} + +# ============================================ +# Step 3: Remove NRPT Rules (Scoped!) +# ============================================ +Write-Step "Step 3: Removing NetBird NRPT rules (scoped)" + +# CRITICAL: Only remove rules with our prefix - NOT all NRPT rules! +if (Test-Path $NRPTRegistryPath) { + $removedCount = 0 + $nrptKeys = Get-ChildItem $NRPTRegistryPath -ErrorAction SilentlyContinue + + foreach ($key in $nrptKeys) { + $keyName = Split-Path $key.Name -Leaf + if ($keyName.StartsWith($NRPTKeyPrefix)) { + if ($PSCmdlet.ShouldProcess($keyName, "Remove NRPT rule")) { + Remove-Item -Path $key.PSPath -Recurse -Force -ErrorAction SilentlyContinue + $removedCount++ + } + } + } + + if ($removedCount -gt 0) { + Write-Success "Removed $removedCount NetBird NRPT rule(s)" + + # Flush DNS cache to apply changes + Clear-DnsClientCache -ErrorAction SilentlyContinue + Write-Success "DNS cache flushed" + } else { + Write-Skipped "No NetBird NRPT rules found" + } +} else { + Write-Skipped "NRPT registry path not present" +} + +# Also check the alternative NRPT path +$NRPTAltPath = "HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig" +if (Test-Path $NRPTAltPath) { + $altKeys = Get-ChildItem $NRPTAltPath -ErrorAction SilentlyContinue | Where-Object { + (Split-Path $_.Name -Leaf).StartsWith($NRPTKeyPrefix) + } + if ($altKeys) { + foreach ($key in $altKeys) { + Remove-Item -Path $key.PSPath -Recurse -Force -ErrorAction SilentlyContinue + } + Write-Success "Removed NetBird NRPT rules from alternate path" + } +} + +# ============================================ +# Step 4: Remove Firewall Rules (Scoped!) +# ============================================ +Write-Step "Step 4: Removing NetBird firewall rules (scoped)" + +# CRITICAL: Only remove rules with our prefix - NOT all firewall rules! +$fwRules = Get-NetFirewallRule -DisplayName "$FirewallRulePrefix*" -ErrorAction SilentlyContinue + +if ($fwRules) { + $ruleCount = ($fwRules | Measure-Object).Count + if ($PSCmdlet.ShouldProcess("$ruleCount firewall rules", "Remove")) { + $fwRules | Remove-NetFirewallRule -ErrorAction SilentlyContinue + Write-Success "Removed $ruleCount firewall rule(s)" + } +} else { + Write-Skipped "No NetBird firewall rules found" +} + +# ============================================ +# Step 5: Clean Configuration (Optional) +# ============================================ +Write-Step "Step 5: Cleaning configuration" + +if (-not $KeepConfig) { + if (Test-Path $ConfigPath) { + if ($PSCmdlet.ShouldProcess($ConfigPath, "Remove configuration directory")) { + # Backup config first + $backupPath = "$env:TEMP\netbird-config-backup-$(Get-Date -Format 'yyyyMMdd-HHmmss')" + Copy-Item -Path $ConfigPath -Destination $backupPath -Recurse -ErrorAction SilentlyContinue + Write-Success "Config backed up to: $backupPath" + + # Remove config + Remove-Item -Path $ConfigPath -Recurse -Force -ErrorAction SilentlyContinue + Write-Success "Configuration removed" + } + } else { + Write-Skipped "Configuration directory not present" + } +} else { + Write-Skipped "Configuration kept (--KeepConfig)" +} + +# ============================================ +# Step 6: Clean Registry Keys +# ============================================ +Write-Step "Step 6: Cleaning registry keys" + +$registryPaths = @( + "HKLM:\SOFTWARE\NetBird\Machine", + "HKLM:\SOFTWARE\WOW6432Node\NetBird\Machine" +) + +foreach ($regPath in $registryPaths) { + if (Test-Path $regPath) { + if ($PSCmdlet.ShouldProcess($regPath, "Remove registry key")) { + Remove-Item -Path $regPath -Recurse -Force -ErrorAction SilentlyContinue + Write-Success "Removed: $regPath" + } + } +} + +# ============================================ +# Summary +# ============================================ +Write-Host @" + +╔═══════════════════════════════════════════════════════════════════╗ +║ Reset Complete ║ +╚═══════════════════════════════════════════════════════════════════╝ + +The following have been cleaned up: +- NetBird Machine service +- WireGuard interface ($InterfaceName) +- NetBird-specific NRPT rules (prefix: $NRPTKeyPrefix) +- NetBird-specific firewall rules (prefix: $FirewallRulePrefix) +$(if (-not $KeepConfig) { "- Configuration files (backed up to $backupPath)" }) + +To reinstall, run: + .\netbird-machine.exe install + Start-Service $ServiceName + +"@ -ForegroundColor Green + +#endregion diff --git a/scripts/tests/Test-TunnelEstablishment.ps1 b/scripts/tests/Test-TunnelEstablishment.ps1 new file mode 100644 index 00000000000..b63f882fa92 --- /dev/null +++ b/scripts/tests/Test-TunnelEstablishment.ps1 @@ -0,0 +1,415 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + E2E Tests for NetBird Machine Tunnel Establishment (T-6.1) +.DESCRIPTION + Tests tunnel establishment functionality: + - TC1: Boot + Login - WireGuard interface, routes, DC reachability, Kerberos TGT + - TC2: DNS-SRV Discovery - LDAP SRV records + - TC3: Kerberos-SRV Discovery - Kerberos SRV records + - TC4: UDP Kerberos - Port 88 connectivity + + Returns exit code 0 on success, 1 on failure. + Designed for CI integration. +.PARAMETER DCAddress + IP address of the Domain Controller (default: 192.168.100.20) +.PARAMETER DomainName + Domain name for SRV lookups (default: test.local) +.PARAMETER DCNetworkPrefix + Network prefix for route verification (default: 192.168.100) +.PARAMETER SkipKerberos + Skip Kerberos TGT verification (for non-domain-joined machines) +.PARAMETER Verbose + Show detailed output +.EXAMPLE + .\Test-TunnelEstablishment.ps1 +.EXAMPLE + .\Test-TunnelEstablishment.ps1 -DCAddress 10.0.0.5 -DomainName corp.local +.EXAMPLE + .\Test-TunnelEstablishment.ps1 -SkipKerberos -Verbose +.NOTES + Author: NetBird Machine Tunnel Fork + Version: 1.0.0 + Requires: NetBird Machine Service running, Administrator privileges +#> + +[CmdletBinding()] +param( + [string]$DCAddress = "192.168.100.20", + [string]$DomainName = "test.local", + [string]$DCNetworkPrefix = "192.168.100", + [switch]$SkipKerberos +) + +$ErrorActionPreference = "Continue" + +# Test result tracking +$script:TestResults = @{ + Passed = @() + Failed = @() + Skipped = @() +} + +# ============================================================================= +# Helper Functions +# ============================================================================= + +function Write-TestHeader { + param([string]$Title) + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host " $Title" -ForegroundColor Cyan + Write-Host ("=" * 60) -ForegroundColor Cyan +} + +function Write-TestResult { + param( + [string]$TestName, + [bool]$Passed, + [string]$Message = "", + [switch]$Skip + ) + + if ($Skip) { + Write-Host " [SKIP] $TestName" -ForegroundColor Yellow + if ($Message) { Write-Host " $Message" -ForegroundColor Gray } + $script:TestResults.Skipped += $TestName + return + } + + if ($Passed) { + Write-Host " [PASS] $TestName" -ForegroundColor Green + if ($Message) { Write-Host " $Message" -ForegroundColor Gray } + $script:TestResults.Passed += $TestName + } else { + Write-Host " [FAIL] $TestName" -ForegroundColor Red + if ($Message) { Write-Host " $Message" -ForegroundColor Yellow } + $script:TestResults.Failed += $TestName + } +} + +function Write-TestSummary { + Write-Host "" + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host " TEST SUMMARY" -ForegroundColor Cyan + Write-Host ("=" * 60) -ForegroundColor Cyan + Write-Host "" + Write-Host " Passed: $($script:TestResults.Passed.Count)" -ForegroundColor Green + Write-Host " Failed: $($script:TestResults.Failed.Count)" -ForegroundColor $(if ($script:TestResults.Failed.Count -gt 0) { "Red" } else { "Green" }) + Write-Host " Skipped: $($script:TestResults.Skipped.Count)" -ForegroundColor Yellow + Write-Host "" + + if ($script:TestResults.Failed.Count -gt 0) { + Write-Host " Failed Tests:" -ForegroundColor Red + foreach ($test in $script:TestResults.Failed) { + Write-Host " - $test" -ForegroundColor Red + } + Write-Host "" + } + + $total = $script:TestResults.Passed.Count + $script:TestResults.Failed.Count + if ($total -gt 0) { + $passRate = [math]::Round(($script:TestResults.Passed.Count / $total) * 100, 1) + Write-Host " Pass Rate: $passRate%" -ForegroundColor $(if ($passRate -eq 100) { "Green" } else { "Yellow" }) + } + Write-Host "" +} + +# ============================================================================= +# Test Header +# ============================================================================= + +Write-Host "" +Write-Host "####################################################################" -ForegroundColor White +Write-Host "# #" -ForegroundColor White +Write-Host "# NetBird Machine Tunnel - E2E Establishment Tests #" -ForegroundColor White +Write-Host "# T-6.1 #" -ForegroundColor White +Write-Host "# #" -ForegroundColor White +Write-Host "####################################################################" -ForegroundColor White +Write-Host "" +Write-Host "Configuration:" -ForegroundColor Gray +Write-Host " Computer: $env:COMPUTERNAME" +Write-Host " DC Address: $DCAddress" +Write-Host " Domain: $DomainName" +Write-Host " DC Network: $DCNetworkPrefix.0/24" +Write-Host " Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" +Write-Host "" + +# ============================================================================= +# TC1: Boot + Login - Tunnel Establishment +# ============================================================================= + +Write-TestHeader "TC1: Tunnel Establishment (Boot + Login)" + +# TC1.1: Service Status +Write-Host " Checking NetBird Machine Service..." -ForegroundColor Gray +$service = Get-Service -Name "NetBirdMachine" -ErrorAction SilentlyContinue +if ($service) { + $serviceRunning = $service.Status -eq "Running" + Write-TestResult "TC1.1: Service Running" $serviceRunning "Status: $($service.Status)" + + if (-not $serviceRunning) { + Write-Host " Attempting to start service..." -ForegroundColor Yellow + try { + Start-Service NetBirdMachine -ErrorAction Stop + Start-Sleep -Seconds 5 + $service = Get-Service -Name "NetBirdMachine" + $serviceRunning = $service.Status -eq "Running" + Write-TestResult "TC1.1b: Service Started" $serviceRunning "Status: $($service.Status)" + } catch { + Write-TestResult "TC1.1b: Service Start" $false "Error: $_" + } + } +} else { + Write-TestResult "TC1.1: Service Exists" $false "NetBirdMachine service not found" +} + +# TC1.2: WireGuard Interface +Write-Host " Checking WireGuard interface..." -ForegroundColor Gray +$wgAdapter = Get-NetAdapter | Where-Object { + $_.InterfaceDescription -like "WireGuard*" -or + $_.Name -like "wg-nb-machine*" -or + $_.Name -like "wg0*" +} | Select-Object -First 1 + +if ($wgAdapter) { + $interfaceUp = $wgAdapter.Status -eq "Up" + Write-TestResult "TC1.2: WireGuard Interface" $interfaceUp "Name: $($wgAdapter.Name), Status: $($wgAdapter.Status)" + + # Get interface details + if ($VerbosePreference -eq "Continue") { + $ipConfig = Get-NetIPAddress -InterfaceIndex $wgAdapter.ifIndex -ErrorAction SilentlyContinue + foreach ($ip in $ipConfig) { + Write-Host " IP: $($ip.IPAddress)/$($ip.PrefixLength)" -ForegroundColor Gray + } + } +} else { + Write-TestResult "TC1.2: WireGuard Interface" $false "No WireGuard adapter found" + + # List all adapters for debugging + if ($VerbosePreference -eq "Continue") { + Write-Host " Available adapters:" -ForegroundColor Gray + Get-NetAdapter | ForEach-Object { + Write-Host " - $($_.Name) ($($_.InterfaceDescription)): $($_.Status)" -ForegroundColor Gray + } + } +} + +# TC1.3: Route to DC Network +Write-Host " Checking route to DC network..." -ForegroundColor Gray +$routes = route print | Select-String $DCNetworkPrefix +$routeExists = $routes -ne $null -and $routes.Count -gt 0 + +if ($routeExists) { + Write-TestResult "TC1.3: Route to DC Network" $true "Found route(s) to $DCNetworkPrefix.0/24" + if ($VerbosePreference -eq "Continue") { + foreach ($r in $routes) { + Write-Host " $($r.Line.Trim())" -ForegroundColor Gray + } + } +} else { + # Alternative check via Get-NetRoute + $netRoutes = Get-NetRoute -DestinationPrefix "$DCNetworkPrefix.0/24" -ErrorAction SilentlyContinue + if ($netRoutes) { + Write-TestResult "TC1.3: Route to DC Network" $true "Found via Get-NetRoute" + } else { + Write-TestResult "TC1.3: Route to DC Network" $false "No route to $DCNetworkPrefix.0/24 found" + } +} + +# TC1.4: DC Reachability (LDAP) +Write-Host " Testing DC reachability (LDAP port 389)..." -ForegroundColor Gray +$ldapTest = Test-NetConnection -ComputerName $DCAddress -Port 389 -WarningAction SilentlyContinue +Write-TestResult "TC1.4: DC LDAP (389/TCP)" $ldapTest.TcpTestSucceeded "Latency: $($ldapTest.PingReplyDetails.RoundtripTime)ms" + +# TC1.5: DC Reachability (Kerberos) +Write-Host " Testing DC reachability (Kerberos port 88)..." -ForegroundColor Gray +$krbTest = Test-NetConnection -ComputerName $DCAddress -Port 88 -WarningAction SilentlyContinue +Write-TestResult "TC1.5: DC Kerberos (88/TCP)" $krbTest.TcpTestSucceeded "Connected: $($krbTest.TcpTestSucceeded)" + +# TC1.6: DC Reachability (DNS) +Write-Host " Testing DC reachability (DNS port 53)..." -ForegroundColor Gray +$dnsTest = Test-NetConnection -ComputerName $DCAddress -Port 53 -WarningAction SilentlyContinue +Write-TestResult "TC1.6: DC DNS (53/TCP)" $dnsTest.TcpTestSucceeded "Connected: $($dnsTest.TcpTestSucceeded)" + +# TC1.7: Kerberos TGT +if ($SkipKerberos) { + Write-TestResult "TC1.7: Kerberos TGT" $false -Skip "Skipped via -SkipKerberos flag" +} else { + Write-Host " Checking Kerberos TGT..." -ForegroundColor Gray + $klistOutput = klist 2>&1 | Out-String + $hasTGT = $klistOutput -match "krbtgt/" -or $klistOutput -match "Ticket\(s\)" + + if ($hasTGT -and $klistOutput -notmatch "Error" -and $klistOutput -notmatch "No tickets") { + Write-TestResult "TC1.7: Kerberos TGT" $true "TGT found in cache" + if ($VerbosePreference -eq "Continue") { + # Extract ticket info + $tickets = $klistOutput -split "`n" | Where-Object { $_ -match "Server:" -or $_ -match "KerbTicket" } + foreach ($t in $tickets) { + Write-Host " $($t.Trim())" -ForegroundColor Gray + } + } + } else { + Write-TestResult "TC1.7: Kerberos TGT" $false "No TGT in cache" + if ($VerbosePreference -eq "Continue") { + Write-Host " klist output:" -ForegroundColor Gray + Write-Host " $klistOutput" -ForegroundColor Gray + } + } +} + +# ============================================================================= +# TC2: DNS-SRV Discovery (LDAP) +# ============================================================================= + +Write-TestHeader "TC2: DNS-SRV Discovery (LDAP)" + +Write-Host " Querying _ldap._tcp.$DomainName..." -ForegroundColor Gray +try { + $ldapSrv = Resolve-DnsName -Name "_ldap._tcp.$DomainName" -Type SRV -ErrorAction Stop + $srvFound = $ldapSrv -ne $null -and $ldapSrv.Count -gt 0 + Write-TestResult "TC2.1: LDAP SRV Record" $srvFound "Found $($ldapSrv.Count) record(s)" + + if ($srvFound -and $VerbosePreference -eq "Continue") { + foreach ($srv in $ldapSrv) { + if ($srv.Type -eq "SRV") { + Write-Host " $($srv.NameTarget):$($srv.Port) (Priority: $($srv.Priority), Weight: $($srv.Weight))" -ForegroundColor Gray + } + } + } +} catch { + Write-TestResult "TC2.1: LDAP SRV Record" $false "Error: $($_.Exception.Message)" + + # Fallback: try nslookup + Write-Host " Trying nslookup fallback..." -ForegroundColor Gray + $nslookup = nslookup -type=SRV "_ldap._tcp.$DomainName" $DCAddress 2>&1 | Out-String + if ($nslookup -match "service location" -or $nslookup -match "svr hostname") { + Write-TestResult "TC2.1b: LDAP SRV (nslookup)" $true "Found via nslookup" + } else { + Write-TestResult "TC2.1b: LDAP SRV (nslookup)" $false "Not found" + } +} + +# ============================================================================= +# TC3: Kerberos-SRV Discovery +# ============================================================================= + +Write-TestHeader "TC3: DNS-SRV Discovery (Kerberos)" + +Write-Host " Querying _kerberos._udp.$DomainName..." -ForegroundColor Gray +try { + $krbSrv = Resolve-DnsName -Name "_kerberos._udp.$DomainName" -Type SRV -ErrorAction Stop + $srvFound = $krbSrv -ne $null -and $krbSrv.Count -gt 0 + Write-TestResult "TC3.1: Kerberos SRV Record (UDP)" $srvFound "Found $($krbSrv.Count) record(s)" + + if ($srvFound -and $VerbosePreference -eq "Continue") { + foreach ($srv in $krbSrv) { + if ($srv.Type -eq "SRV") { + Write-Host " $($srv.NameTarget):$($srv.Port) (Priority: $($srv.Priority))" -ForegroundColor Gray + } + } + } +} catch { + Write-TestResult "TC3.1: Kerberos SRV Record (UDP)" $false "Error: $($_.Exception.Message)" +} + +# Also test TCP variant +Write-Host " Querying _kerberos._tcp.$DomainName..." -ForegroundColor Gray +try { + $krbTcpSrv = Resolve-DnsName -Name "_kerberos._tcp.$DomainName" -Type SRV -ErrorAction Stop + $srvFound = $krbTcpSrv -ne $null -and $krbTcpSrv.Count -gt 0 + Write-TestResult "TC3.2: Kerberos SRV Record (TCP)" $srvFound "Found $($krbTcpSrv.Count) record(s)" +} catch { + Write-TestResult "TC3.2: Kerberos SRV Record (TCP)" $false "Error: $($_.Exception.Message)" +} + +# ============================================================================= +# TC4: UDP Kerberos Connectivity +# ============================================================================= + +Write-TestHeader "TC4: UDP Kerberos Connectivity" + +Write-Host " Testing UDP connectivity to Kerberos (port 88)..." -ForegroundColor Gray + +# UDP test is tricky - we can't directly test UDP with Test-NetConnection +# But we can verify by attempting a DNS query through the DC or checking nltest + +# Method 1: Check if nltest can find a DC +Write-Host " Running nltest /dsgetdc..." -ForegroundColor Gray +$nltestOutput = nltest /dsgetdc:$DomainName 2>&1 | Out-String +$dcFound = $nltestOutput -match "DC:" -or $nltestOutput -match "The command completed successfully" +Write-TestResult "TC4.1: DC Discovery (nltest)" $dcFound "nltest /dsgetdc:$DomainName" + +if ($VerbosePreference -eq "Continue" -and $dcFound) { + $dcLine = $nltestOutput -split "`n" | Where-Object { $_ -match "DC:" } | Select-Object -First 1 + if ($dcLine) { + Write-Host " $($dcLine.Trim())" -ForegroundColor Gray + } +} + +# Method 2: Verify UDP port is listening (via portqry if available, otherwise skip) +# For CI, we rely on the TCP test + nltest as indicators of UDP functionality +Write-Host " UDP 88 verification..." -ForegroundColor Gray +# The presence of a TGT or successful nltest implies UDP Kerberos works +$udpIndicator = $dcFound -or ($script:TestResults.Passed -contains "TC1.7: Kerberos TGT") +Write-TestResult "TC4.2: UDP Kerberos Indicator" $udpIndicator "Based on DC discovery and TGT status" + +# ============================================================================= +# NRPT Verification (Bonus) +# ============================================================================= + +Write-TestHeader "NRPT Configuration Check" + +Write-Host " Checking NRPT rules..." -ForegroundColor Gray +$nrptPaths = @( + "HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig", + "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig" +) + +$nrptFound = $false +foreach ($path in $nrptPaths) { + if (Test-Path $path) { + $rules = Get-ChildItem $path -ErrorAction SilentlyContinue + if ($rules) { + $domainRule = $rules | Where-Object { + $props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue + $props.Name -like "*$DomainName*" -or $props.ConfigOptions -like "*$DomainName*" + } + if ($domainRule) { + $nrptFound = $true + break + } + } + } +} + +# Alternative: Check via PowerShell cmdlet +if (-not $nrptFound) { + try { + $nrptRules = Get-DnsClientNrptRule -ErrorAction SilentlyContinue + $domainRule = $nrptRules | Where-Object { $_.Namespace -like "*$DomainName*" } + $nrptFound = $domainRule -ne $null + } catch { + # Cmdlet not available on all systems + } +} + +Write-TestResult "NRPT: Domain Rule" $nrptFound "Rule for $DomainName configured" + +# ============================================================================= +# Summary +# ============================================================================= + +Write-TestSummary + +# ============================================================================= +# Exit Code +# ============================================================================= + +if ($script:TestResults.Failed.Count -eq 0) { + Write-Host "All tests passed!" -ForegroundColor Green + exit 0 +} else { + Write-Host "Some tests failed. See details above." -ForegroundColor Red + exit 1 +} diff --git a/scripts/verify-nrpt-cleanup.ps1 b/scripts/verify-nrpt-cleanup.ps1 new file mode 100644 index 00000000000..ee61aa59faa --- /dev/null +++ b/scripts/verify-nrpt-cleanup.ps1 @@ -0,0 +1,184 @@ +<# +.SYNOPSIS + Verify NRPT cleanup after NetBird Machine Tunnel reset. + +.DESCRIPTION + This script checks all NRPT-related registry paths and PowerShell cmdlets + to verify that NetBird-specific NRPT rules have been properly removed + while other NRPT rules remain intact. + + Checks: + 1. Registry: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig + 2. Registry: HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig + 3. PowerShell: Get-DnsClientNrptRule + +.PARAMETER ShowAll + Show all NRPT rules, not just NetBird-related ones. + +.EXAMPLE + .\verify-nrpt-cleanup.ps1 + Checks for any remaining NetBird NRPT rules. + +.EXAMPLE + .\verify-nrpt-cleanup.ps1 -ShowAll + Shows all NRPT rules in the system. + +.NOTES + Requires: Administrator privileges (for some registry paths) + Author: NetBird Machine Tunnel Fork + Version: 1.0.0 +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [switch]$ShowAll +) + +$NRPTKeyPrefix = "NetBird-Machine-" + +Write-Host @" + +╔═══════════════════════════════════════════════════════════════════╗ +║ NRPT Cleanup Verification ║ +╚═══════════════════════════════════════════════════════════════════╝ + +"@ -ForegroundColor Cyan + +$issuesFound = $false + +# ============================================ +# Check 1: Policy Registry Path +# ============================================ +Write-Host ">> Checking: Policy Registry Path" -ForegroundColor Yellow +$policyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig" + +if (Test-Path $policyPath) { + $policyKeys = Get-ChildItem $policyPath -ErrorAction SilentlyContinue + + $netbirdRules = $policyKeys | Where-Object { + (Split-Path $_.Name -Leaf).StartsWith($NRPTKeyPrefix) + } + + $otherRules = $policyKeys | Where-Object { + -not (Split-Path $_.Name -Leaf).StartsWith($NRPTKeyPrefix) + } + + if ($netbirdRules) { + Write-Host " [FAIL] Found $($netbirdRules.Count) NetBird NRPT rule(s):" -ForegroundColor Red + foreach ($rule in $netbirdRules) { + Write-Host " - $(Split-Path $rule.Name -Leaf)" -ForegroundColor Red + } + $issuesFound = $true + } else { + Write-Host " [OK] No NetBird NRPT rules found" -ForegroundColor Green + } + + if ($ShowAll -and $otherRules) { + Write-Host " [INFO] Other NRPT rules present: $($otherRules.Count)" -ForegroundColor Cyan + foreach ($rule in $otherRules) { + Write-Host " - $(Split-Path $rule.Name -Leaf)" -ForegroundColor Gray + } + } +} else { + Write-Host " [OK] Policy path not present (clean)" -ForegroundColor Green +} + +# ============================================ +# Check 2: Dnscache Registry Path +# ============================================ +Write-Host "`n>> Checking: Dnscache Registry Path" -ForegroundColor Yellow +$dnscachePath = "HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig" + +if (Test-Path $dnscachePath) { + $dnscacheKeys = Get-ChildItem $dnscachePath -ErrorAction SilentlyContinue + + $netbirdRules = $dnscacheKeys | Where-Object { + (Split-Path $_.Name -Leaf).StartsWith($NRPTKeyPrefix) + } + + $otherRules = $dnscacheKeys | Where-Object { + -not (Split-Path $_.Name -Leaf).StartsWith($NRPTKeyPrefix) + } + + if ($netbirdRules) { + Write-Host " [FAIL] Found $($netbirdRules.Count) NetBird NRPT rule(s):" -ForegroundColor Red + foreach ($rule in $netbirdRules) { + Write-Host " - $(Split-Path $rule.Name -Leaf)" -ForegroundColor Red + } + $issuesFound = $true + } else { + Write-Host " [OK] No NetBird NRPT rules found" -ForegroundColor Green + } + + if ($ShowAll -and $otherRules) { + Write-Host " [INFO] Other NRPT rules present: $($otherRules.Count)" -ForegroundColor Cyan + foreach ($rule in $otherRules) { + Write-Host " - $(Split-Path $rule.Name -Leaf)" -ForegroundColor Gray + } + } +} else { + Write-Host " [OK] Dnscache path not present (clean)" -ForegroundColor Green +} + +# ============================================ +# Check 3: PowerShell Get-DnsClientNrptRule +# ============================================ +Write-Host "`n>> Checking: PowerShell NRPT Rules" -ForegroundColor Yellow + +try { + $psRules = Get-DnsClientNrptRule -ErrorAction SilentlyContinue + + if ($psRules) { + $netbirdRules = $psRules | Where-Object { + $_.Name -like "$NRPTKeyPrefix*" -or + $_.Comment -like "*NetBird*" -or + $_.Namespace -like "*netbird*" + } + + $otherRules = $psRules | Where-Object { + $_.Name -notlike "$NRPTKeyPrefix*" -and + $_.Comment -notlike "*NetBird*" -and + $_.Namespace -notlike "*netbird*" + } + + if ($netbirdRules) { + Write-Host " [FAIL] Found $($netbirdRules.Count) NetBird NRPT rule(s) via PowerShell:" -ForegroundColor Red + foreach ($rule in $netbirdRules) { + Write-Host " - $($rule.Name): $($rule.Namespace)" -ForegroundColor Red + } + $issuesFound = $true + } else { + Write-Host " [OK] No NetBird NRPT rules found via PowerShell" -ForegroundColor Green + } + + if ($ShowAll -and $otherRules) { + Write-Host " [INFO] Other NRPT rules via PowerShell: $($otherRules.Count)" -ForegroundColor Cyan + foreach ($rule in $otherRules) { + Write-Host " - $($rule.Name): $($rule.Namespace)" -ForegroundColor Gray + } + } + } else { + Write-Host " [OK] No NRPT rules found via PowerShell" -ForegroundColor Green + } +} catch { + Write-Host " [WARN] Could not query PowerShell NRPT rules: $_" -ForegroundColor Yellow +} + +# ============================================ +# Summary +# ============================================ +Write-Host "" +if ($issuesFound) { + Write-Host "╔═══════════════════════════════════════════════════════════════════╗" -ForegroundColor Red + Write-Host "║ VERIFICATION FAILED: NetBird NRPT rules still present ║" -ForegroundColor Red + Write-Host "╚═══════════════════════════════════════════════════════════════════╝" -ForegroundColor Red + Write-Host "" + Write-Host "Run reset-netbird-machine.ps1 -Force to clean up." -ForegroundColor Yellow + exit 1 +} else { + Write-Host "╔═══════════════════════════════════════════════════════════════════╗" -ForegroundColor Green + Write-Host "║ VERIFICATION PASSED: No NetBird NRPT rules found ║" -ForegroundColor Green + Write-Host "╚═══════════════════════════════════════════════════════════════════╝" -ForegroundColor Green + exit 0 +} diff --git a/shared/management/proto/management.pb.go b/shared/management/proto/management.pb.go index 84b74bf8c8b..cf07d7f038e 100644 --- a/shared/management/proto/management.pb.go +++ b/shared/management/proto/management.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 +// protoc-gen-go v1.36.11 // protoc v6.33.3 // source: management.proto @@ -13,6 +13,7 @@ import ( timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -172,6 +173,62 @@ func (RuleAction) EnumDescriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{2} } +// MachineUpdateType indicates what triggered the sync update. +type MachineUpdateType int32 + +const ( + MachineUpdateType_MACHINE_UPDATE_FULL MachineUpdateType = 0 // Full sync (initial or reconnect) + MachineUpdateType_MACHINE_UPDATE_ROUTES MachineUpdateType = 1 // Route changes + MachineUpdateType_MACHINE_UPDATE_DNS MachineUpdateType = 2 // DNS config changes + MachineUpdateType_MACHINE_UPDATE_PEERS MachineUpdateType = 3 // Peer changes (router-peers) + MachineUpdateType_MACHINE_UPDATE_FIREWALL MachineUpdateType = 4 // Firewall rule changes +) + +// Enum value maps for MachineUpdateType. +var ( + MachineUpdateType_name = map[int32]string{ + 0: "MACHINE_UPDATE_FULL", + 1: "MACHINE_UPDATE_ROUTES", + 2: "MACHINE_UPDATE_DNS", + 3: "MACHINE_UPDATE_PEERS", + 4: "MACHINE_UPDATE_FIREWALL", + } + MachineUpdateType_value = map[string]int32{ + "MACHINE_UPDATE_FULL": 0, + "MACHINE_UPDATE_ROUTES": 1, + "MACHINE_UPDATE_DNS": 2, + "MACHINE_UPDATE_PEERS": 3, + "MACHINE_UPDATE_FIREWALL": 4, + } +) + +func (x MachineUpdateType) Enum() *MachineUpdateType { + p := new(MachineUpdateType) + *p = x + return p +} + +func (x MachineUpdateType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (MachineUpdateType) Descriptor() protoreflect.EnumDescriptor { + return file_management_proto_enumTypes[3].Descriptor() +} + +func (MachineUpdateType) Type() protoreflect.EnumType { + return &file_management_proto_enumTypes[3] +} + +func (x MachineUpdateType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use MachineUpdateType.Descriptor instead. +func (MachineUpdateType) EnumDescriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{3} +} + type HostConfig_Protocol int32 const ( @@ -211,11 +268,11 @@ func (x HostConfig_Protocol) String() string { } func (HostConfig_Protocol) Descriptor() protoreflect.EnumDescriptor { - return file_management_proto_enumTypes[3].Descriptor() + return file_management_proto_enumTypes[4].Descriptor() } func (HostConfig_Protocol) Type() protoreflect.EnumType { - return &file_management_proto_enumTypes[3] + return &file_management_proto_enumTypes[4] } func (x HostConfig_Protocol) Number() protoreflect.EnumNumber { @@ -254,11 +311,11 @@ func (x DeviceAuthorizationFlowProvider) String() string { } func (DeviceAuthorizationFlowProvider) Descriptor() protoreflect.EnumDescriptor { - return file_management_proto_enumTypes[4].Descriptor() + return file_management_proto_enumTypes[5].Descriptor() } func (DeviceAuthorizationFlowProvider) Type() protoreflect.EnumType { - return &file_management_proto_enumTypes[4] + return &file_management_proto_enumTypes[5] } func (x DeviceAuthorizationFlowProvider) Number() protoreflect.EnumNumber { @@ -271,25 +328,22 @@ func (DeviceAuthorizationFlowProvider) EnumDescriptor() ([]byte, []int) { } type EncryptedMessage struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Wireguard public key WgPubKey string `protobuf:"bytes,1,opt,name=wgPubKey,proto3" json:"wgPubKey,omitempty"` // encrypted message Body Body []byte `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` // Version of the Netbird Management Service protocol - Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` + Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *EncryptedMessage) Reset() { *x = EncryptedMessage{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *EncryptedMessage) String() string { @@ -300,7 +354,7 @@ func (*EncryptedMessage) ProtoMessage() {} func (x *EncryptedMessage) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -337,21 +391,18 @@ func (x *EncryptedMessage) GetVersion() int32 { } type SyncRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Meta data of the peer - Meta *PeerSystemMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + Meta *PeerSystemMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SyncRequest) Reset() { *x = SyncRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SyncRequest) String() string { @@ -362,7 +413,7 @@ func (*SyncRequest) ProtoMessage() {} func (x *SyncRequest) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -386,10 +437,7 @@ func (x *SyncRequest) GetMeta() *PeerSystemMeta { // SyncResponse represents a state that should be applied to the local peer (e.g. Netbird servers config as well as local peer and remote peers configs) type SyncResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Global config NetbirdConfig *NetbirdConfig `protobuf:"bytes,1,opt,name=netbirdConfig,proto3" json:"netbirdConfig,omitempty"` // Deprecated. Use NetworkMap.PeerConfig @@ -401,16 +449,16 @@ type SyncResponse struct { RemotePeersIsEmpty bool `protobuf:"varint,4,opt,name=remotePeersIsEmpty,proto3" json:"remotePeersIsEmpty,omitempty"` NetworkMap *NetworkMap `protobuf:"bytes,5,opt,name=NetworkMap,proto3" json:"NetworkMap,omitempty"` // Posture checks to be evaluated by client - Checks []*Checks `protobuf:"bytes,6,rep,name=Checks,proto3" json:"Checks,omitempty"` + Checks []*Checks `protobuf:"bytes,6,rep,name=Checks,proto3" json:"Checks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SyncResponse) Reset() { *x = SyncResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SyncResponse) String() string { @@ -421,7 +469,7 @@ func (*SyncResponse) ProtoMessage() {} func (x *SyncResponse) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -479,21 +527,18 @@ func (x *SyncResponse) GetChecks() []*Checks { } type SyncMetaRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Meta data of the peer - Meta *PeerSystemMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + Meta *PeerSystemMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SyncMetaRequest) Reset() { *x = SyncMetaRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SyncMetaRequest) String() string { @@ -504,7 +549,7 @@ func (*SyncMetaRequest) ProtoMessage() {} func (x *SyncMetaRequest) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -527,10 +572,7 @@ func (x *SyncMetaRequest) GetMeta() *PeerSystemMeta { } type LoginRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Pre-authorized setup key (can be empty) SetupKey string `protobuf:"bytes,1,opt,name=setupKey,proto3" json:"setupKey,omitempty"` // Meta data of the peer (e.g. name, os_name, os_version, @@ -538,17 +580,17 @@ type LoginRequest struct { // SSO token (can be empty) JwtToken string `protobuf:"bytes,3,opt,name=jwtToken,proto3" json:"jwtToken,omitempty"` // Can be absent for now. - PeerKeys *PeerKeys `protobuf:"bytes,4,opt,name=peerKeys,proto3" json:"peerKeys,omitempty"` - DnsLabels []string `protobuf:"bytes,5,rep,name=dnsLabels,proto3" json:"dnsLabels,omitempty"` + PeerKeys *PeerKeys `protobuf:"bytes,4,opt,name=peerKeys,proto3" json:"peerKeys,omitempty"` + DnsLabels []string `protobuf:"bytes,5,rep,name=dnsLabels,proto3" json:"dnsLabels,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LoginRequest) Reset() { *x = LoginRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *LoginRequest) String() string { @@ -559,7 +601,7 @@ func (*LoginRequest) ProtoMessage() {} func (x *LoginRequest) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -612,23 +654,20 @@ func (x *LoginRequest) GetDnsLabels() []string { // PeerKeys is additional peer info like SSH pub key and WireGuard public key. // This message is sent on Login or register requests, or when a key rotation has to happen. type PeerKeys struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // sshPubKey represents a public SSH key of the peer. Can be absent. SshPubKey []byte `protobuf:"bytes,1,opt,name=sshPubKey,proto3" json:"sshPubKey,omitempty"` // wgPubKey represents a public WireGuard key of the peer. Can be absent. - WgPubKey []byte `protobuf:"bytes,2,opt,name=wgPubKey,proto3" json:"wgPubKey,omitempty"` + WgPubKey []byte `protobuf:"bytes,2,opt,name=wgPubKey,proto3" json:"wgPubKey,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PeerKeys) Reset() { *x = PeerKeys{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PeerKeys) String() string { @@ -639,7 +678,7 @@ func (*PeerKeys) ProtoMessage() {} func (x *PeerKeys) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -670,23 +709,20 @@ func (x *PeerKeys) GetWgPubKey() []byte { // Environment is part of the PeerSystemMeta and describes the environment the agent is running in. type Environment struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // cloud is the cloud provider the agent is running in if applicable. Cloud string `protobuf:"bytes,1,opt,name=cloud,proto3" json:"cloud,omitempty"` // platform is the platform the agent is running on if applicable. - Platform string `protobuf:"bytes,2,opt,name=platform,proto3" json:"platform,omitempty"` + Platform string `protobuf:"bytes,2,opt,name=platform,proto3" json:"platform,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Environment) Reset() { *x = Environment{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Environment) String() string { @@ -697,7 +733,7 @@ func (*Environment) ProtoMessage() {} func (x *Environment) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -728,25 +764,22 @@ func (x *Environment) GetPlatform() string { // File represents a file on the system. type File struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // path is the path to the file. Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // exist indicate whether the file exists. Exist bool `protobuf:"varint,2,opt,name=exist,proto3" json:"exist,omitempty"` // processIsRunning indicates whether the file is a running process or not. ProcessIsRunning bool `protobuf:"varint,3,opt,name=processIsRunning,proto3" json:"processIsRunning,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *File) Reset() { *x = File{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *File) String() string { @@ -757,7 +790,7 @@ func (*File) ProtoMessage() {} func (x *File) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -794,34 +827,31 @@ func (x *File) GetProcessIsRunning() bool { } type Flags struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - RosenpassEnabled bool `protobuf:"varint,1,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` - RosenpassPermissive bool `protobuf:"varint,2,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` - ServerSSHAllowed bool `protobuf:"varint,3,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"` - DisableClientRoutes bool `protobuf:"varint,4,opt,name=disableClientRoutes,proto3" json:"disableClientRoutes,omitempty"` - DisableServerRoutes bool `protobuf:"varint,5,opt,name=disableServerRoutes,proto3" json:"disableServerRoutes,omitempty"` - DisableDNS bool `protobuf:"varint,6,opt,name=disableDNS,proto3" json:"disableDNS,omitempty"` - DisableFirewall bool `protobuf:"varint,7,opt,name=disableFirewall,proto3" json:"disableFirewall,omitempty"` - BlockLANAccess bool `protobuf:"varint,8,opt,name=blockLANAccess,proto3" json:"blockLANAccess,omitempty"` - BlockInbound bool `protobuf:"varint,9,opt,name=blockInbound,proto3" json:"blockInbound,omitempty"` - LazyConnectionEnabled bool `protobuf:"varint,10,opt,name=lazyConnectionEnabled,proto3" json:"lazyConnectionEnabled,omitempty"` - EnableSSHRoot bool `protobuf:"varint,11,opt,name=enableSSHRoot,proto3" json:"enableSSHRoot,omitempty"` - EnableSSHSFTP bool `protobuf:"varint,12,opt,name=enableSSHSFTP,proto3" json:"enableSSHSFTP,omitempty"` - EnableSSHLocalPortForwarding bool `protobuf:"varint,13,opt,name=enableSSHLocalPortForwarding,proto3" json:"enableSSHLocalPortForwarding,omitempty"` - EnableSSHRemotePortForwarding bool `protobuf:"varint,14,opt,name=enableSSHRemotePortForwarding,proto3" json:"enableSSHRemotePortForwarding,omitempty"` - DisableSSHAuth bool `protobuf:"varint,15,opt,name=disableSSHAuth,proto3" json:"disableSSHAuth,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RosenpassEnabled bool `protobuf:"varint,1,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` + RosenpassPermissive bool `protobuf:"varint,2,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` + ServerSSHAllowed bool `protobuf:"varint,3,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"` + DisableClientRoutes bool `protobuf:"varint,4,opt,name=disableClientRoutes,proto3" json:"disableClientRoutes,omitempty"` + DisableServerRoutes bool `protobuf:"varint,5,opt,name=disableServerRoutes,proto3" json:"disableServerRoutes,omitempty"` + DisableDNS bool `protobuf:"varint,6,opt,name=disableDNS,proto3" json:"disableDNS,omitempty"` + DisableFirewall bool `protobuf:"varint,7,opt,name=disableFirewall,proto3" json:"disableFirewall,omitempty"` + BlockLANAccess bool `protobuf:"varint,8,opt,name=blockLANAccess,proto3" json:"blockLANAccess,omitempty"` + BlockInbound bool `protobuf:"varint,9,opt,name=blockInbound,proto3" json:"blockInbound,omitempty"` + LazyConnectionEnabled bool `protobuf:"varint,10,opt,name=lazyConnectionEnabled,proto3" json:"lazyConnectionEnabled,omitempty"` + EnableSSHRoot bool `protobuf:"varint,11,opt,name=enableSSHRoot,proto3" json:"enableSSHRoot,omitempty"` + EnableSSHSFTP bool `protobuf:"varint,12,opt,name=enableSSHSFTP,proto3" json:"enableSSHSFTP,omitempty"` + EnableSSHLocalPortForwarding bool `protobuf:"varint,13,opt,name=enableSSHLocalPortForwarding,proto3" json:"enableSSHLocalPortForwarding,omitempty"` + EnableSSHRemotePortForwarding bool `protobuf:"varint,14,opt,name=enableSSHRemotePortForwarding,proto3" json:"enableSSHRemotePortForwarding,omitempty"` + DisableSSHAuth bool `protobuf:"varint,15,opt,name=disableSSHAuth,proto3" json:"disableSSHAuth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Flags) Reset() { *x = Flags{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Flags) String() string { @@ -832,7 +862,7 @@ func (*Flags) ProtoMessage() {} func (x *Flags) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -954,36 +984,33 @@ func (x *Flags) GetDisableSSHAuth() bool { // PeerSystemMeta is machine meta data like OS and version. type PeerSystemMeta struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` - GoOS string `protobuf:"bytes,2,opt,name=goOS,proto3" json:"goOS,omitempty"` - Kernel string `protobuf:"bytes,3,opt,name=kernel,proto3" json:"kernel,omitempty"` - Core string `protobuf:"bytes,4,opt,name=core,proto3" json:"core,omitempty"` - Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` - OS string `protobuf:"bytes,6,opt,name=OS,proto3" json:"OS,omitempty"` - NetbirdVersion string `protobuf:"bytes,7,opt,name=netbirdVersion,proto3" json:"netbirdVersion,omitempty"` - UiVersion string `protobuf:"bytes,8,opt,name=uiVersion,proto3" json:"uiVersion,omitempty"` - KernelVersion string `protobuf:"bytes,9,opt,name=kernelVersion,proto3" json:"kernelVersion,omitempty"` - OSVersion string `protobuf:"bytes,10,opt,name=OSVersion,proto3" json:"OSVersion,omitempty"` - NetworkAddresses []*NetworkAddress `protobuf:"bytes,11,rep,name=networkAddresses,proto3" json:"networkAddresses,omitempty"` - SysSerialNumber string `protobuf:"bytes,12,opt,name=sysSerialNumber,proto3" json:"sysSerialNumber,omitempty"` - SysProductName string `protobuf:"bytes,13,opt,name=sysProductName,proto3" json:"sysProductName,omitempty"` - SysManufacturer string `protobuf:"bytes,14,opt,name=sysManufacturer,proto3" json:"sysManufacturer,omitempty"` - Environment *Environment `protobuf:"bytes,15,opt,name=environment,proto3" json:"environment,omitempty"` - Files []*File `protobuf:"bytes,16,rep,name=files,proto3" json:"files,omitempty"` - Flags *Flags `protobuf:"bytes,17,opt,name=flags,proto3" json:"flags,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` + GoOS string `protobuf:"bytes,2,opt,name=goOS,proto3" json:"goOS,omitempty"` + Kernel string `protobuf:"bytes,3,opt,name=kernel,proto3" json:"kernel,omitempty"` + Core string `protobuf:"bytes,4,opt,name=core,proto3" json:"core,omitempty"` + Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` + OS string `protobuf:"bytes,6,opt,name=OS,proto3" json:"OS,omitempty"` + NetbirdVersion string `protobuf:"bytes,7,opt,name=netbirdVersion,proto3" json:"netbirdVersion,omitempty"` + UiVersion string `protobuf:"bytes,8,opt,name=uiVersion,proto3" json:"uiVersion,omitempty"` + KernelVersion string `protobuf:"bytes,9,opt,name=kernelVersion,proto3" json:"kernelVersion,omitempty"` + OSVersion string `protobuf:"bytes,10,opt,name=OSVersion,proto3" json:"OSVersion,omitempty"` + NetworkAddresses []*NetworkAddress `protobuf:"bytes,11,rep,name=networkAddresses,proto3" json:"networkAddresses,omitempty"` + SysSerialNumber string `protobuf:"bytes,12,opt,name=sysSerialNumber,proto3" json:"sysSerialNumber,omitempty"` + SysProductName string `protobuf:"bytes,13,opt,name=sysProductName,proto3" json:"sysProductName,omitempty"` + SysManufacturer string `protobuf:"bytes,14,opt,name=sysManufacturer,proto3" json:"sysManufacturer,omitempty"` + Environment *Environment `protobuf:"bytes,15,opt,name=environment,proto3" json:"environment,omitempty"` + Files []*File `protobuf:"bytes,16,rep,name=files,proto3" json:"files,omitempty"` + Flags *Flags `protobuf:"bytes,17,opt,name=flags,proto3" json:"flags,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PeerSystemMeta) Reset() { *x = PeerSystemMeta{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PeerSystemMeta) String() string { @@ -994,7 +1021,7 @@ func (*PeerSystemMeta) ProtoMessage() {} func (x *PeerSystemMeta) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1129,25 +1156,22 @@ func (x *PeerSystemMeta) GetFlags() *Flags { } type LoginResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Global config NetbirdConfig *NetbirdConfig `protobuf:"bytes,1,opt,name=netbirdConfig,proto3" json:"netbirdConfig,omitempty"` // Peer local config PeerConfig *PeerConfig `protobuf:"bytes,2,opt,name=peerConfig,proto3" json:"peerConfig,omitempty"` // Posture checks to be evaluated by client - Checks []*Checks `protobuf:"bytes,3,rep,name=Checks,proto3" json:"Checks,omitempty"` + Checks []*Checks `protobuf:"bytes,3,rep,name=Checks,proto3" json:"Checks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LoginResponse) Reset() { *x = LoginResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *LoginResponse) String() string { @@ -1158,7 +1182,7 @@ func (*LoginResponse) ProtoMessage() {} func (x *LoginResponse) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1195,25 +1219,22 @@ func (x *LoginResponse) GetChecks() []*Checks { } type ServerKeyResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Server's Wireguard public key Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Key expiration timestamp after which the key should be fetched again by the client ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expiresAt,proto3" json:"expiresAt,omitempty"` // Version of the Netbird Management Service protocol - Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` + Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ServerKeyResponse) Reset() { *x = ServerKeyResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ServerKeyResponse) String() string { @@ -1224,7 +1245,7 @@ func (*ServerKeyResponse) ProtoMessage() {} func (x *ServerKeyResponse) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1261,18 +1282,16 @@ func (x *ServerKeyResponse) GetVersion() int32 { } type Empty struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Empty) Reset() { *x = Empty{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Empty) String() string { @@ -1283,7 +1302,7 @@ func (*Empty) ProtoMessage() {} func (x *Empty) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[12] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1300,27 +1319,24 @@ func (*Empty) Descriptor() ([]byte, []int) { // NetbirdConfig is a common configuration of any Netbird peer. It contains STUN, TURN, Signal and Management servers configurations type NetbirdConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // a list of STUN servers Stuns []*HostConfig `protobuf:"bytes,1,rep,name=stuns,proto3" json:"stuns,omitempty"` // a list of TURN servers Turns []*ProtectedHostConfig `protobuf:"bytes,2,rep,name=turns,proto3" json:"turns,omitempty"` // a Signal server config - Signal *HostConfig `protobuf:"bytes,3,opt,name=signal,proto3" json:"signal,omitempty"` - Relay *RelayConfig `protobuf:"bytes,4,opt,name=relay,proto3" json:"relay,omitempty"` - Flow *FlowConfig `protobuf:"bytes,5,opt,name=flow,proto3" json:"flow,omitempty"` + Signal *HostConfig `protobuf:"bytes,3,opt,name=signal,proto3" json:"signal,omitempty"` + Relay *RelayConfig `protobuf:"bytes,4,opt,name=relay,proto3" json:"relay,omitempty"` + Flow *FlowConfig `protobuf:"bytes,5,opt,name=flow,proto3" json:"flow,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NetbirdConfig) Reset() { *x = NetbirdConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *NetbirdConfig) String() string { @@ -1331,7 +1347,7 @@ func (*NetbirdConfig) ProtoMessage() {} func (x *NetbirdConfig) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[13] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1383,22 +1399,19 @@ func (x *NetbirdConfig) GetFlow() *FlowConfig { // HostConfig describes connection properties of some server (e.g. STUN, Signal, Management) type HostConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // URI of the resource e.g. turns://stun.netbird.io:4430 or signal.netbird.io:10000 - Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"` - Protocol HostConfig_Protocol `protobuf:"varint,2,opt,name=protocol,proto3,enum=management.HostConfig_Protocol" json:"protocol,omitempty"` + Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"` + Protocol HostConfig_Protocol `protobuf:"varint,2,opt,name=protocol,proto3,enum=management.HostConfig_Protocol" json:"protocol,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *HostConfig) Reset() { *x = HostConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *HostConfig) String() string { @@ -1409,7 +1422,7 @@ func (*HostConfig) ProtoMessage() {} func (x *HostConfig) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[14] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1439,22 +1452,19 @@ func (x *HostConfig) GetProtocol() HostConfig_Protocol { } type RelayConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Urls []string `protobuf:"bytes,1,rep,name=urls,proto3" json:"urls,omitempty"` - TokenPayload string `protobuf:"bytes,2,opt,name=tokenPayload,proto3" json:"tokenPayload,omitempty"` - TokenSignature string `protobuf:"bytes,3,opt,name=tokenSignature,proto3" json:"tokenSignature,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Urls []string `protobuf:"bytes,1,rep,name=urls,proto3" json:"urls,omitempty"` + TokenPayload string `protobuf:"bytes,2,opt,name=tokenPayload,proto3" json:"tokenPayload,omitempty"` + TokenSignature string `protobuf:"bytes,3,opt,name=tokenSignature,proto3" json:"tokenSignature,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RelayConfig) Reset() { *x = RelayConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RelayConfig) String() string { @@ -1465,7 +1475,7 @@ func (*RelayConfig) ProtoMessage() {} func (x *RelayConfig) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[15] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1502,30 +1512,27 @@ func (x *RelayConfig) GetTokenSignature() string { } type FlowConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` - TokenPayload string `protobuf:"bytes,2,opt,name=tokenPayload,proto3" json:"tokenPayload,omitempty"` - TokenSignature string `protobuf:"bytes,3,opt,name=tokenSignature,proto3" json:"tokenSignature,omitempty"` - Interval *durationpb.Duration `protobuf:"bytes,4,opt,name=interval,proto3" json:"interval,omitempty"` - Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + TokenPayload string `protobuf:"bytes,2,opt,name=tokenPayload,proto3" json:"tokenPayload,omitempty"` + TokenSignature string `protobuf:"bytes,3,opt,name=tokenSignature,proto3" json:"tokenSignature,omitempty"` + Interval *durationpb.Duration `protobuf:"bytes,4,opt,name=interval,proto3" json:"interval,omitempty"` + Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"` // counters determines if flow packets and bytes counters should be sent Counters bool `protobuf:"varint,6,opt,name=counters,proto3" json:"counters,omitempty"` // exitNodeCollection determines if event collection on exit nodes should be enabled ExitNodeCollection bool `protobuf:"varint,7,opt,name=exitNodeCollection,proto3" json:"exitNodeCollection,omitempty"` // dnsCollection determines if DNS event collection should be enabled DnsCollection bool `protobuf:"varint,8,opt,name=dnsCollection,proto3" json:"dnsCollection,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FlowConfig) Reset() { *x = FlowConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FlowConfig) String() string { @@ -1536,7 +1543,7 @@ func (*FlowConfig) ProtoMessage() {} func (x *FlowConfig) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[16] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1607,27 +1614,26 @@ func (x *FlowConfig) GetDnsCollection() bool { return false } -// JWTConfig represents JWT authentication configuration +// JWTConfig represents JWT authentication configuration for validating tokens. type JWTConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Issuer string `protobuf:"bytes,1,opt,name=issuer,proto3" json:"issuer,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Issuer string `protobuf:"bytes,1,opt,name=issuer,proto3" json:"issuer,omitempty"` + // Deprecated: audience is kept for backwards compatibility only. Use audiences instead in the client code but populate this field. Audience string `protobuf:"bytes,2,opt,name=audience,proto3" json:"audience,omitempty"` KeysLocation string `protobuf:"bytes,3,opt,name=keysLocation,proto3" json:"keysLocation,omitempty"` MaxTokenAge int64 `protobuf:"varint,4,opt,name=maxTokenAge,proto3" json:"maxTokenAge,omitempty"` - // audiences - Audiences []string `protobuf:"bytes,5,rep,name=audiences,proto3" json:"audiences,omitempty"` + // audiences contains the list of valid audiences for JWT validation. + // Tokens matching any audience in this list are considered valid. + Audiences []string `protobuf:"bytes,5,rep,name=audiences,proto3" json:"audiences,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *JWTConfig) Reset() { *x = JWTConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *JWTConfig) String() string { @@ -1638,7 +1644,7 @@ func (*JWTConfig) ProtoMessage() {} func (x *JWTConfig) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[17] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1691,22 +1697,19 @@ func (x *JWTConfig) GetAudiences() []string { // ProtectedHostConfig is similar to HostConfig but has additional user and password // Mostly used for TURN servers type ProtectedHostConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + HostConfig *HostConfig `protobuf:"bytes,1,opt,name=hostConfig,proto3" json:"hostConfig,omitempty"` + User string `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"` + Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"` unknownFields protoimpl.UnknownFields - - HostConfig *HostConfig `protobuf:"bytes,1,opt,name=hostConfig,proto3" json:"hostConfig,omitempty"` - User string `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"` - Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ProtectedHostConfig) Reset() { *x = ProtectedHostConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ProtectedHostConfig) String() string { @@ -1717,7 +1720,7 @@ func (*ProtectedHostConfig) ProtoMessage() {} func (x *ProtectedHostConfig) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[18] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1756,10 +1759,7 @@ func (x *ProtectedHostConfig) GetPassword() string { // PeerConfig represents a configuration of a "our" peer. // The properties are used to configure local Wireguard type PeerConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Peer's virtual IP address within the Netbird VPN (a Wireguard address config) Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` // Netbird DNS server (a Wireguard DNS config) @@ -1772,16 +1772,16 @@ type PeerConfig struct { LazyConnectionEnabled bool `protobuf:"varint,6,opt,name=LazyConnectionEnabled,proto3" json:"LazyConnectionEnabled,omitempty"` Mtu int32 `protobuf:"varint,7,opt,name=mtu,proto3" json:"mtu,omitempty"` // Auto-update config - AutoUpdate *AutoUpdateSettings `protobuf:"bytes,8,opt,name=autoUpdate,proto3" json:"autoUpdate,omitempty"` + AutoUpdate *AutoUpdateSettings `protobuf:"bytes,8,opt,name=autoUpdate,proto3" json:"autoUpdate,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PeerConfig) Reset() { *x = PeerConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PeerConfig) String() string { @@ -1792,7 +1792,7 @@ func (*PeerConfig) ProtoMessage() {} func (x *PeerConfig) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[19] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1864,23 +1864,20 @@ func (x *PeerConfig) GetAutoUpdate() *AutoUpdateSettings { } type AutoUpdateSettings struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` // alwaysUpdate = true → Updates happen automatically in the background // alwaysUpdate = false → Updates only happen when triggered by a peer connection - AlwaysUpdate bool `protobuf:"varint,2,opt,name=alwaysUpdate,proto3" json:"alwaysUpdate,omitempty"` + AlwaysUpdate bool `protobuf:"varint,2,opt,name=alwaysUpdate,proto3" json:"alwaysUpdate,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AutoUpdateSettings) Reset() { *x = AutoUpdateSettings{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AutoUpdateSettings) String() string { @@ -1891,7 +1888,7 @@ func (*AutoUpdateSettings) ProtoMessage() {} func (x *AutoUpdateSettings) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[20] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1922,10 +1919,7 @@ func (x *AutoUpdateSettings) GetAlwaysUpdate() bool { // NetworkMap represents a network state of the peer with the corresponding configuration parameters to establish peer-to-peer connections type NetworkMap struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Serial is an ID of the network state to be used by clients to order updates. // The larger the Serial the newer the configuration. // E.g. the client app should keep track of this id locally and discard all the configurations with a lower value @@ -1952,16 +1946,16 @@ type NetworkMap struct { RoutesFirewallRulesIsEmpty bool `protobuf:"varint,11,opt,name=routesFirewallRulesIsEmpty,proto3" json:"routesFirewallRulesIsEmpty,omitempty"` ForwardingRules []*ForwardingRule `protobuf:"bytes,12,rep,name=forwardingRules,proto3" json:"forwardingRules,omitempty"` // SSHAuth represents SSH authorization configuration - SshAuth *SSHAuth `protobuf:"bytes,13,opt,name=sshAuth,proto3" json:"sshAuth,omitempty"` + SshAuth *SSHAuth `protobuf:"bytes,13,opt,name=sshAuth,proto3" json:"sshAuth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NetworkMap) Reset() { *x = NetworkMap{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *NetworkMap) String() string { @@ -1972,7 +1966,7 @@ func (*NetworkMap) ProtoMessage() {} func (x *NetworkMap) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[21] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2079,25 +2073,22 @@ func (x *NetworkMap) GetSshAuth() *SSHAuth { } type SSHAuth struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // UserIDClaim is the JWT claim to be used to get the users ID UserIDClaim string `protobuf:"bytes,1,opt,name=UserIDClaim,proto3" json:"UserIDClaim,omitempty"` // AuthorizedUsers is a list of hashed user IDs authorized to access this peer via SSH AuthorizedUsers [][]byte `protobuf:"bytes,2,rep,name=AuthorizedUsers,proto3" json:"AuthorizedUsers,omitempty"` // MachineUsers is a map of machine user names to their corresponding indexes in the AuthorizedUsers list - MachineUsers map[string]*MachineUserIndexes `protobuf:"bytes,3,rep,name=machine_users,json=machineUsers,proto3" json:"machine_users,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + MachineUsers map[string]*MachineUserIndexes `protobuf:"bytes,3,rep,name=machine_users,json=machineUsers,proto3" json:"machine_users,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SSHAuth) Reset() { *x = SSHAuth{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SSHAuth) String() string { @@ -2108,7 +2099,7 @@ func (*SSHAuth) ProtoMessage() {} func (x *SSHAuth) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[22] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2145,20 +2136,17 @@ func (x *SSHAuth) GetMachineUsers() map[string]*MachineUserIndexes { } type MachineUserIndexes struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Indexes []uint32 `protobuf:"varint,1,rep,packed,name=indexes,proto3" json:"indexes,omitempty"` unknownFields protoimpl.UnknownFields - - Indexes []uint32 `protobuf:"varint,1,rep,packed,name=indexes,proto3" json:"indexes,omitempty"` + sizeCache protoimpl.SizeCache } func (x *MachineUserIndexes) Reset() { *x = MachineUserIndexes{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *MachineUserIndexes) String() string { @@ -2169,7 +2157,7 @@ func (*MachineUserIndexes) ProtoMessage() {} func (x *MachineUserIndexes) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[23] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2194,10 +2182,7 @@ func (x *MachineUserIndexes) GetIndexes() []uint32 { // RemotePeerConfig represents a configuration of a remote peer. // The properties are used to configure WireGuard Peers sections type RemotePeerConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // A WireGuard public key of a remote peer WgPubKey string `protobuf:"bytes,1,opt,name=wgPubKey,proto3" json:"wgPubKey,omitempty"` // WireGuard allowed IPs of a remote peer e.g. [10.30.30.1/32] @@ -2205,17 +2190,17 @@ type RemotePeerConfig struct { // SSHConfig is a SSH config of the remote peer. SSHConfig.sshPubKey should be ignored because peer knows it's SSH key. SshConfig *SSHConfig `protobuf:"bytes,3,opt,name=sshConfig,proto3" json:"sshConfig,omitempty"` // Peer fully qualified domain name - Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` - AgentVersion string `protobuf:"bytes,5,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"` + Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` + AgentVersion string `protobuf:"bytes,5,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RemotePeerConfig) Reset() { *x = RemotePeerConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RemotePeerConfig) String() string { @@ -2226,7 +2211,7 @@ func (*RemotePeerConfig) ProtoMessage() {} func (x *RemotePeerConfig) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[24] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2278,25 +2263,22 @@ func (x *RemotePeerConfig) GetAgentVersion() string { // SSHConfig represents SSH configurations of a peer. type SSHConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // sshEnabled indicates whether a SSH server is enabled on this peer SshEnabled bool `protobuf:"varint,1,opt,name=sshEnabled,proto3" json:"sshEnabled,omitempty"` // sshPubKey is a SSH public key of a peer to be added to authorized_hosts. // This property should be ignore if SSHConfig comes from PeerConfig. - SshPubKey []byte `protobuf:"bytes,2,opt,name=sshPubKey,proto3" json:"sshPubKey,omitempty"` - JwtConfig *JWTConfig `protobuf:"bytes,3,opt,name=jwtConfig,proto3" json:"jwtConfig,omitempty"` + SshPubKey []byte `protobuf:"bytes,2,opt,name=sshPubKey,proto3" json:"sshPubKey,omitempty"` + JwtConfig *JWTConfig `protobuf:"bytes,3,opt,name=jwtConfig,proto3" json:"jwtConfig,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SSHConfig) Reset() { *x = SSHConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SSHConfig) String() string { @@ -2307,7 +2289,7 @@ func (*SSHConfig) ProtoMessage() {} func (x *SSHConfig) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[25] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2345,18 +2327,16 @@ func (x *SSHConfig) GetJwtConfig() *JWTConfig { // DeviceAuthorizationFlowRequest empty struct for future expansion type DeviceAuthorizationFlowRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DeviceAuthorizationFlowRequest) Reset() { *x = DeviceAuthorizationFlowRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeviceAuthorizationFlowRequest) String() string { @@ -2367,7 +2347,7 @@ func (*DeviceAuthorizationFlowRequest) ProtoMessage() {} func (x *DeviceAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[26] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2386,22 +2366,19 @@ func (*DeviceAuthorizationFlowRequest) Descriptor() ([]byte, []int) { // that can be used by the client to login initiate a Oauth 2.0 device authorization grant flow // see https://datatracker.ietf.org/doc/html/rfc8628 type DeviceAuthorizationFlow struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // An IDP provider , (eg. Auth0) Provider DeviceAuthorizationFlowProvider `protobuf:"varint,1,opt,name=Provider,proto3,enum=management.DeviceAuthorizationFlowProvider" json:"Provider,omitempty"` ProviderConfig *ProviderConfig `protobuf:"bytes,2,opt,name=ProviderConfig,proto3" json:"ProviderConfig,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DeviceAuthorizationFlow) Reset() { *x = DeviceAuthorizationFlow{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeviceAuthorizationFlow) String() string { @@ -2412,7 +2389,7 @@ func (*DeviceAuthorizationFlow) ProtoMessage() {} func (x *DeviceAuthorizationFlow) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[27] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2443,18 +2420,16 @@ func (x *DeviceAuthorizationFlow) GetProviderConfig() *ProviderConfig { // PKCEAuthorizationFlowRequest empty struct for future expansion type PKCEAuthorizationFlowRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PKCEAuthorizationFlowRequest) Reset() { *x = PKCEAuthorizationFlowRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[28] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PKCEAuthorizationFlowRequest) String() string { @@ -2465,7 +2440,7 @@ func (*PKCEAuthorizationFlowRequest) ProtoMessage() {} func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[28] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2484,20 +2459,17 @@ func (*PKCEAuthorizationFlowRequest) Descriptor() ([]byte, []int) { // that can be used by the client to login initiate a Oauth 2.0 authorization code grant flow // with Proof Key for Code Exchange (PKCE). See https://datatracker.ietf.org/doc/html/rfc7636 type PKCEAuthorizationFlow struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ProviderConfig *ProviderConfig `protobuf:"bytes,1,opt,name=ProviderConfig,proto3" json:"ProviderConfig,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + ProviderConfig *ProviderConfig `protobuf:"bytes,1,opt,name=ProviderConfig,proto3" json:"ProviderConfig,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PKCEAuthorizationFlow) Reset() { *x = PKCEAuthorizationFlow{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[29] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PKCEAuthorizationFlow) String() string { @@ -2508,7 +2480,7 @@ func (*PKCEAuthorizationFlow) ProtoMessage() {} func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[29] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2532,10 +2504,7 @@ func (x *PKCEAuthorizationFlow) GetProviderConfig() *ProviderConfig { // ProviderConfig has all attributes needed to initiate a device/pkce authorization flow type ProviderConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // An IDP application client id ClientID string `protobuf:"bytes,1,opt,name=ClientID,proto3" json:"ClientID,omitempty"` // An IDP application client secret @@ -2560,16 +2529,16 @@ type ProviderConfig struct { // DisablePromptLogin makes the PKCE flow to not prompt the user for login DisablePromptLogin bool `protobuf:"varint,11,opt,name=DisablePromptLogin,proto3" json:"DisablePromptLogin,omitempty"` // LoginFlags sets the PKCE flow login details - LoginFlag uint32 `protobuf:"varint,12,opt,name=LoginFlag,proto3" json:"LoginFlag,omitempty"` + LoginFlag uint32 `protobuf:"varint,12,opt,name=LoginFlag,proto3" json:"LoginFlag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ProviderConfig) Reset() { *x = ProviderConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[30] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ProviderConfig) String() string { @@ -2580,7 +2549,7 @@ func (*ProviderConfig) ProtoMessage() {} func (x *ProviderConfig) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[30] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2681,29 +2650,26 @@ func (x *ProviderConfig) GetLoginFlag() uint32 { // Route represents a route.Route object type Route struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` + Network string `protobuf:"bytes,2,opt,name=Network,proto3" json:"Network,omitempty"` + NetworkType int64 `protobuf:"varint,3,opt,name=NetworkType,proto3" json:"NetworkType,omitempty"` + Peer string `protobuf:"bytes,4,opt,name=Peer,proto3" json:"Peer,omitempty"` + Metric int64 `protobuf:"varint,5,opt,name=Metric,proto3" json:"Metric,omitempty"` + Masquerade bool `protobuf:"varint,6,opt,name=Masquerade,proto3" json:"Masquerade,omitempty"` + NetID string `protobuf:"bytes,7,opt,name=NetID,proto3" json:"NetID,omitempty"` + Domains []string `protobuf:"bytes,8,rep,name=Domains,proto3" json:"Domains,omitempty"` + KeepRoute bool `protobuf:"varint,9,opt,name=keepRoute,proto3" json:"keepRoute,omitempty"` + SkipAutoApply bool `protobuf:"varint,10,opt,name=skipAutoApply,proto3" json:"skipAutoApply,omitempty"` unknownFields protoimpl.UnknownFields - - ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` - Network string `protobuf:"bytes,2,opt,name=Network,proto3" json:"Network,omitempty"` - NetworkType int64 `protobuf:"varint,3,opt,name=NetworkType,proto3" json:"NetworkType,omitempty"` - Peer string `protobuf:"bytes,4,opt,name=Peer,proto3" json:"Peer,omitempty"` - Metric int64 `protobuf:"varint,5,opt,name=Metric,proto3" json:"Metric,omitempty"` - Masquerade bool `protobuf:"varint,6,opt,name=Masquerade,proto3" json:"Masquerade,omitempty"` - NetID string `protobuf:"bytes,7,opt,name=NetID,proto3" json:"NetID,omitempty"` - Domains []string `protobuf:"bytes,8,rep,name=Domains,proto3" json:"Domains,omitempty"` - KeepRoute bool `protobuf:"varint,9,opt,name=keepRoute,proto3" json:"keepRoute,omitempty"` - SkipAutoApply bool `protobuf:"varint,10,opt,name=skipAutoApply,proto3" json:"skipAutoApply,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Route) Reset() { *x = Route{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[31] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Route) String() string { @@ -2714,7 +2680,7 @@ func (*Route) ProtoMessage() {} func (x *Route) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[31] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2801,24 +2767,21 @@ func (x *Route) GetSkipAutoApply() bool { // DNSConfig represents a dns.Update type DNSConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ServiceEnable bool `protobuf:"varint,1,opt,name=ServiceEnable,proto3" json:"ServiceEnable,omitempty"` - NameServerGroups []*NameServerGroup `protobuf:"bytes,2,rep,name=NameServerGroups,proto3" json:"NameServerGroups,omitempty"` - CustomZones []*CustomZone `protobuf:"bytes,3,rep,name=CustomZones,proto3" json:"CustomZones,omitempty"` - // Deprecated: Do not use. + state protoimpl.MessageState `protogen:"open.v1"` + ServiceEnable bool `protobuf:"varint,1,opt,name=ServiceEnable,proto3" json:"ServiceEnable,omitempty"` + NameServerGroups []*NameServerGroup `protobuf:"bytes,2,rep,name=NameServerGroups,proto3" json:"NameServerGroups,omitempty"` + CustomZones []*CustomZone `protobuf:"bytes,3,rep,name=CustomZones,proto3" json:"CustomZones,omitempty"` + // Deprecated: Marked as deprecated in management.proto. ForwarderPort int64 `protobuf:"varint,4,opt,name=ForwarderPort,proto3" json:"ForwarderPort,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DNSConfig) Reset() { *x = DNSConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[32] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DNSConfig) String() string { @@ -2829,7 +2792,7 @@ func (*DNSConfig) ProtoMessage() {} func (x *DNSConfig) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[32] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2865,7 +2828,7 @@ func (x *DNSConfig) GetCustomZones() []*CustomZone { return nil } -// Deprecated: Do not use. +// Deprecated: Marked as deprecated in management.proto. func (x *DNSConfig) GetForwarderPort() int64 { if x != nil { return x.ForwarderPort @@ -2875,25 +2838,22 @@ func (x *DNSConfig) GetForwarderPort() int64 { // CustomZone represents a dns.CustomZone type CustomZone struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Domain string `protobuf:"bytes,1,opt,name=Domain,proto3" json:"Domain,omitempty"` - Records []*SimpleRecord `protobuf:"bytes,2,rep,name=Records,proto3" json:"Records,omitempty"` - SearchDomainDisabled bool `protobuf:"varint,3,opt,name=SearchDomainDisabled,proto3" json:"SearchDomainDisabled,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Domain string `protobuf:"bytes,1,opt,name=Domain,proto3" json:"Domain,omitempty"` + Records []*SimpleRecord `protobuf:"bytes,2,rep,name=Records,proto3" json:"Records,omitempty"` + SearchDomainDisabled bool `protobuf:"varint,3,opt,name=SearchDomainDisabled,proto3" json:"SearchDomainDisabled,omitempty"` // NonAuthoritative indicates this is a user-created zone (not the built-in peer DNS zone). // Non-authoritative zones will fallthrough to lower-priority handlers on NXDOMAIN and skip PTR processing. NonAuthoritative bool `protobuf:"varint,4,opt,name=NonAuthoritative,proto3" json:"NonAuthoritative,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CustomZone) Reset() { *x = CustomZone{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[33] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CustomZone) String() string { @@ -2904,7 +2864,7 @@ func (*CustomZone) ProtoMessage() {} func (x *CustomZone) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[33] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2949,24 +2909,21 @@ func (x *CustomZone) GetNonAuthoritative() bool { // SimpleRecord represents a dns.SimpleRecord type SimpleRecord struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` + Type int64 `protobuf:"varint,2,opt,name=Type,proto3" json:"Type,omitempty"` + Class string `protobuf:"bytes,3,opt,name=Class,proto3" json:"Class,omitempty"` + TTL int64 `protobuf:"varint,4,opt,name=TTL,proto3" json:"TTL,omitempty"` + RData string `protobuf:"bytes,5,opt,name=RData,proto3" json:"RData,omitempty"` unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` - Type int64 `protobuf:"varint,2,opt,name=Type,proto3" json:"Type,omitempty"` - Class string `protobuf:"bytes,3,opt,name=Class,proto3" json:"Class,omitempty"` - TTL int64 `protobuf:"varint,4,opt,name=TTL,proto3" json:"TTL,omitempty"` - RData string `protobuf:"bytes,5,opt,name=RData,proto3" json:"RData,omitempty"` + sizeCache protoimpl.SizeCache } func (x *SimpleRecord) Reset() { *x = SimpleRecord{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[34] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SimpleRecord) String() string { @@ -2977,7 +2934,7 @@ func (*SimpleRecord) ProtoMessage() {} func (x *SimpleRecord) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[34] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3029,23 +2986,20 @@ func (x *SimpleRecord) GetRData() string { // NameServerGroup represents a dns.NameServerGroup type NameServerGroup struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - NameServers []*NameServer `protobuf:"bytes,1,rep,name=NameServers,proto3" json:"NameServers,omitempty"` - Primary bool `protobuf:"varint,2,opt,name=Primary,proto3" json:"Primary,omitempty"` - Domains []string `protobuf:"bytes,3,rep,name=Domains,proto3" json:"Domains,omitempty"` - SearchDomainsEnabled bool `protobuf:"varint,4,opt,name=SearchDomainsEnabled,proto3" json:"SearchDomainsEnabled,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + NameServers []*NameServer `protobuf:"bytes,1,rep,name=NameServers,proto3" json:"NameServers,omitempty"` + Primary bool `protobuf:"varint,2,opt,name=Primary,proto3" json:"Primary,omitempty"` + Domains []string `protobuf:"bytes,3,rep,name=Domains,proto3" json:"Domains,omitempty"` + SearchDomainsEnabled bool `protobuf:"varint,4,opt,name=SearchDomainsEnabled,proto3" json:"SearchDomainsEnabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NameServerGroup) Reset() { *x = NameServerGroup{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[35] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *NameServerGroup) String() string { @@ -3056,7 +3010,7 @@ func (*NameServerGroup) ProtoMessage() {} func (x *NameServerGroup) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[35] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3101,22 +3055,19 @@ func (x *NameServerGroup) GetSearchDomainsEnabled() bool { // NameServer represents a dns.NameServer type NameServer struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"` + NSType int64 `protobuf:"varint,2,opt,name=NSType,proto3" json:"NSType,omitempty"` + Port int64 `protobuf:"varint,3,opt,name=Port,proto3" json:"Port,omitempty"` unknownFields protoimpl.UnknownFields - - IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"` - NSType int64 `protobuf:"varint,2,opt,name=NSType,proto3" json:"NSType,omitempty"` - Port int64 `protobuf:"varint,3,opt,name=Port,proto3" json:"Port,omitempty"` + sizeCache protoimpl.SizeCache } func (x *NameServer) Reset() { *x = NameServer{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[36] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *NameServer) String() string { @@ -3127,7 +3078,7 @@ func (*NameServer) ProtoMessage() {} func (x *NameServer) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[36] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3165,27 +3116,24 @@ func (x *NameServer) GetPort() int64 { // FirewallRule represents a firewall rule type FirewallRule struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - PeerIP string `protobuf:"bytes,1,opt,name=PeerIP,proto3" json:"PeerIP,omitempty"` - Direction RuleDirection `protobuf:"varint,2,opt,name=Direction,proto3,enum=management.RuleDirection" json:"Direction,omitempty"` - Action RuleAction `protobuf:"varint,3,opt,name=Action,proto3,enum=management.RuleAction" json:"Action,omitempty"` - Protocol RuleProtocol `protobuf:"varint,4,opt,name=Protocol,proto3,enum=management.RuleProtocol" json:"Protocol,omitempty"` - Port string `protobuf:"bytes,5,opt,name=Port,proto3" json:"Port,omitempty"` - PortInfo *PortInfo `protobuf:"bytes,6,opt,name=PortInfo,proto3" json:"PortInfo,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + PeerIP string `protobuf:"bytes,1,opt,name=PeerIP,proto3" json:"PeerIP,omitempty"` + Direction RuleDirection `protobuf:"varint,2,opt,name=Direction,proto3,enum=management.RuleDirection" json:"Direction,omitempty"` + Action RuleAction `protobuf:"varint,3,opt,name=Action,proto3,enum=management.RuleAction" json:"Action,omitempty"` + Protocol RuleProtocol `protobuf:"varint,4,opt,name=Protocol,proto3,enum=management.RuleProtocol" json:"Protocol,omitempty"` + Port string `protobuf:"bytes,5,opt,name=Port,proto3" json:"Port,omitempty"` + PortInfo *PortInfo `protobuf:"bytes,6,opt,name=PortInfo,proto3" json:"PortInfo,omitempty"` // PolicyID is the ID of the policy that this rule belongs to - PolicyID []byte `protobuf:"bytes,7,opt,name=PolicyID,proto3" json:"PolicyID,omitempty"` + PolicyID []byte `protobuf:"bytes,7,opt,name=PolicyID,proto3" json:"PolicyID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FirewallRule) Reset() { *x = FirewallRule{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[37] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FirewallRule) String() string { @@ -3196,7 +3144,7 @@ func (*FirewallRule) ProtoMessage() {} func (x *FirewallRule) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[37] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3261,21 +3209,18 @@ func (x *FirewallRule) GetPolicyID() []byte { } type NetworkAddress struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NetIP string `protobuf:"bytes,1,opt,name=netIP,proto3" json:"netIP,omitempty"` + Mac string `protobuf:"bytes,2,opt,name=mac,proto3" json:"mac,omitempty"` unknownFields protoimpl.UnknownFields - - NetIP string `protobuf:"bytes,1,opt,name=netIP,proto3" json:"netIP,omitempty"` - Mac string `protobuf:"bytes,2,opt,name=mac,proto3" json:"mac,omitempty"` + sizeCache protoimpl.SizeCache } func (x *NetworkAddress) Reset() { *x = NetworkAddress{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[38] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *NetworkAddress) String() string { @@ -3286,7 +3231,7 @@ func (*NetworkAddress) ProtoMessage() {} func (x *NetworkAddress) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[38] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3316,20 +3261,17 @@ func (x *NetworkAddress) GetMac() string { } type Checks struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Files []string `protobuf:"bytes,1,rep,name=Files,proto3" json:"Files,omitempty"` unknownFields protoimpl.UnknownFields - - Files []string `protobuf:"bytes,1,rep,name=Files,proto3" json:"Files,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Checks) Reset() { *x = Checks{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[39] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Checks) String() string { @@ -3340,7 +3282,7 @@ func (*Checks) ProtoMessage() {} func (x *Checks) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[39] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3363,24 +3305,21 @@ func (x *Checks) GetFiles() []string { } type PortInfo struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Types that are assignable to PortSelection: + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to PortSelection: // // *PortInfo_Port // *PortInfo_Range_ PortSelection isPortInfo_PortSelection `protobuf_oneof:"portSelection"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PortInfo) Reset() { *x = PortInfo{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[40] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PortInfo) String() string { @@ -3391,7 +3330,7 @@ func (*PortInfo) ProtoMessage() {} func (x *PortInfo) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[40] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3406,23 +3345,27 @@ func (*PortInfo) Descriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{40} } -func (m *PortInfo) GetPortSelection() isPortInfo_PortSelection { - if m != nil { - return m.PortSelection +func (x *PortInfo) GetPortSelection() isPortInfo_PortSelection { + if x != nil { + return x.PortSelection } return nil } func (x *PortInfo) GetPort() uint32 { - if x, ok := x.GetPortSelection().(*PortInfo_Port); ok { - return x.Port + if x != nil { + if x, ok := x.PortSelection.(*PortInfo_Port); ok { + return x.Port + } } return 0 } func (x *PortInfo) GetRange() *PortInfo_Range { - if x, ok := x.GetPortSelection().(*PortInfo_Range_); ok { - return x.Range + if x != nil { + if x, ok := x.PortSelection.(*PortInfo_Range_); ok { + return x.Range + } } return nil } @@ -3445,10 +3388,7 @@ func (*PortInfo_Range_) isPortInfo_PortSelection() {} // RouteFirewallRule signifies a firewall rule applicable for a routed network. type RouteFirewallRule struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // sourceRanges IP ranges of the routing peers. SourceRanges []string `protobuf:"bytes,1,rep,name=sourceRanges,proto3" json:"sourceRanges,omitempty"` // Action to be taken by the firewall when the rule is applicable. @@ -3468,16 +3408,16 @@ type RouteFirewallRule struct { // PolicyID is the ID of the policy that this rule belongs to PolicyID []byte `protobuf:"bytes,9,opt,name=PolicyID,proto3" json:"PolicyID,omitempty"` // RouteID is the ID of the route that this rule belongs to - RouteID string `protobuf:"bytes,10,opt,name=RouteID,proto3" json:"RouteID,omitempty"` + RouteID string `protobuf:"bytes,10,opt,name=RouteID,proto3" json:"RouteID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RouteFirewallRule) Reset() { *x = RouteFirewallRule{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[41] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RouteFirewallRule) String() string { @@ -3488,7 +3428,7 @@ func (*RouteFirewallRule) ProtoMessage() {} func (x *RouteFirewallRule) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[41] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3574,10 +3514,7 @@ func (x *RouteFirewallRule) GetRouteID() string { } type ForwardingRule struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Protocol of the forwarding rule Protocol RuleProtocol `protobuf:"varint,1,opt,name=protocol,proto3,enum=management.RuleProtocol" json:"protocol,omitempty"` // portInfo is the ingress destination port information, where the traffic arrives in the gateway node @@ -3586,15 +3523,15 @@ type ForwardingRule struct { TranslatedAddress []byte `protobuf:"bytes,3,opt,name=translatedAddress,proto3" json:"translatedAddress,omitempty"` // Translated port information, where the traffic should be forwarded to TranslatedPort *PortInfo `protobuf:"bytes,4,opt,name=translatedPort,proto3" json:"translatedPort,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ForwardingRule) Reset() { *x = ForwardingRule{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[42] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_management_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ForwardingRule) String() string { @@ -3605,7 +3542,7 @@ func (*ForwardingRule) ProtoMessage() {} func (x *ForwardingRule) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[42] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3648,33 +3585,127 @@ func (x *ForwardingRule) GetTranslatedPort() *PortInfo { return nil } -type PortInfo_Range struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +// MachineIdentity contains identity information extracted from the machine certificate. +// This is populated by the server from the mTLS client certificate, not sent by client. +type MachineIdentity struct { + state protoimpl.MessageState `protogen:"open.v1"` + // dns_name is the full SAN DNSName from the certificate (e.g., "win10-pc.corp.local") + DnsName string `protobuf:"bytes,1,opt,name=dns_name,json=dnsName,proto3" json:"dns_name,omitempty"` + // hostname is extracted from dns_name (e.g., "win10-pc") + Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` + // domain is extracted from dns_name (e.g., "corp.local") + Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"` + // issuer_fingerprint is SHA256 of the issuing CA certificate (from VerifiedChains) + IssuerFingerprint string `protobuf:"bytes,4,opt,name=issuer_fingerprint,json=issuerFingerprint,proto3" json:"issuer_fingerprint,omitempty"` + // serial_number of the client certificate + SerialNumber string `protobuf:"bytes,5,opt,name=serial_number,json=serialNumber,proto3" json:"serial_number,omitempty"` + // template_oid if present in certificate extensions (AD CS template) + TemplateOid string `protobuf:"bytes,6,opt,name=template_oid,json=templateOid,proto3" json:"template_oid,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} - Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` - End uint32 `protobuf:"varint,2,opt,name=end,proto3" json:"end,omitempty"` +func (x *MachineIdentity) Reset() { + *x = MachineIdentity{} + mi := &file_management_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *PortInfo_Range) Reset() { - *x = PortInfo_Range{} - if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[44] +func (x *MachineIdentity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MachineIdentity) ProtoMessage() {} + +func (x *MachineIdentity) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[43] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } + return mi.MessageOf(x) } -func (x *PortInfo_Range) String() string { +// Deprecated: Use MachineIdentity.ProtoReflect.Descriptor instead. +func (*MachineIdentity) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{43} +} + +func (x *MachineIdentity) GetDnsName() string { + if x != nil { + return x.DnsName + } + return "" +} + +func (x *MachineIdentity) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *MachineIdentity) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +func (x *MachineIdentity) GetIssuerFingerprint() string { + if x != nil { + return x.IssuerFingerprint + } + return "" +} + +func (x *MachineIdentity) GetSerialNumber() string { + if x != nil { + return x.SerialNumber + } + return "" +} + +func (x *MachineIdentity) GetTemplateOid() string { + if x != nil { + return x.TemplateOid + } + return "" +} + +// MachineRegisterRequest is sent when a machine peer registers with mTLS. +type MachineRegisterRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // meta contains machine metadata (OS, version, etc.) + Meta *PeerSystemMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + // wg_pub_key is the WireGuard public key for this machine tunnel + WgPubKey []byte `protobuf:"bytes,2,opt,name=wg_pub_key,json=wgPubKey,proto3" json:"wg_pub_key,omitempty"` + // requested_ip is an optional requested VPN IP (may be ignored by server) + RequestedIp string `protobuf:"bytes,3,opt,name=requested_ip,json=requestedIp,proto3" json:"requested_ip,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MachineRegisterRequest) Reset() { + *x = MachineRegisterRequest{} + mi := &file_management_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MachineRegisterRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*PortInfo_Range) ProtoMessage() {} +func (*MachineRegisterRequest) ProtoMessage() {} -func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { +func (x *MachineRegisterRequest) ProtoReflect() protoreflect.Message { mi := &file_management_proto_msgTypes[44] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3684,1308 +3715,1103 @@ func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use PortInfo_Range.ProtoReflect.Descriptor instead. -func (*PortInfo_Range) Descriptor() ([]byte, []int) { - return file_management_proto_rawDescGZIP(), []int{40, 0} +// Deprecated: Use MachineRegisterRequest.ProtoReflect.Descriptor instead. +func (*MachineRegisterRequest) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{44} } -func (x *PortInfo_Range) GetStart() uint32 { +func (x *MachineRegisterRequest) GetMeta() *PeerSystemMeta { if x != nil { - return x.Start + return x.Meta } - return 0 + return nil } -func (x *PortInfo_Range) GetEnd() uint32 { +func (x *MachineRegisterRequest) GetWgPubKey() []byte { if x != nil { - return x.End + return x.WgPubKey } - return 0 + return nil } -var File_management_proto protoreflect.FileDescriptor +func (x *MachineRegisterRequest) GetRequestedIp() string { + if x != nil { + return x.RequestedIp + } + return "" +} -var file_management_proto_rawDesc = []byte{ - 0x0a, 0x10, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x12, 0x0a, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x1f, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, - 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, - 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, - 0x5c, 0x0a, 0x10, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, - 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, - 0x6f, 0x64, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3d, 0x0a, - 0x0b, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, - 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xdb, 0x02, 0x0a, - 0x0c, 0x53, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, - 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, - 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, - 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, - 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, - 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, - 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, - 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x4d, 0x61, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, - 0x61, 0x70, 0x52, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x2a, - 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x41, 0x0a, 0x0f, 0x53, 0x79, - 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, - 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, - 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0xc6, 0x01, - 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, - 0x0a, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, - 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, - 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x6a, 0x77, - 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x77, - 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, - 0x79, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x08, - 0x70, 0x65, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x6e, 0x73, 0x4c, - 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73, - 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x22, 0x44, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4b, 0x65, - 0x79, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, - 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x3f, 0x0a, 0x0b, - 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, - 0x6c, 0x6f, 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x75, - 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0x5c, 0x0a, - 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x78, 0x69, - 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65, 0x78, 0x69, 0x73, 0x74, 0x12, - 0x2a, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, - 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x63, 0x65, - 0x73, 0x73, 0x49, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xbf, 0x05, 0x0a, 0x05, - 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, - 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, - 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, - 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, - 0x69, 0x76, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, - 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, - 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, - 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x73, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, - 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x4e, - 0x53, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x44, 0x4e, 0x53, 0x12, 0x28, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, - 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x69, - 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x12, 0x26, 0x0a, - 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x41, 0x4e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x41, 0x4e, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x6e, - 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x62, 0x6c, 0x6f, - 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x61, 0x7a, - 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, - 0x24, 0x0a, 0x0d, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x53, 0x48, 0x52, 0x6f, 0x6f, 0x74, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x53, - 0x48, 0x52, 0x6f, 0x6f, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x53, - 0x53, 0x48, 0x53, 0x46, 0x54, 0x50, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x65, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x53, 0x53, 0x48, 0x53, 0x46, 0x54, 0x50, 0x12, 0x42, 0x0a, 0x1c, 0x65, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x53, 0x48, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x6f, 0x72, - 0x74, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x0d, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x1c, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x53, 0x48, 0x4c, 0x6f, 0x63, 0x61, - 0x6c, 0x50, 0x6f, 0x72, 0x74, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x12, - 0x44, 0x0a, 0x1d, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x53, 0x48, 0x52, 0x65, 0x6d, 0x6f, - 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, - 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1d, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x53, - 0x48, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x46, 0x6f, 0x72, 0x77, 0x61, - 0x72, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, - 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x22, 0xf2, 0x04, - 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, - 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x67, 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53, - 0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, - 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x62, - 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, - 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x73, 0x79, - 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, - 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x73, 0x79, 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, - 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x79, - 0x73, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, - 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x18, - 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x79, 0x73, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, - 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, - 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, - 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, - 0x74, 0x12, 0x26, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, - 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x05, 0x66, 0x6c, 0x61, - 0x67, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x52, 0x05, 0x66, 0x6c, 0x61, - 0x67, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a, - 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x73, 0x52, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xff, 0x01, - 0x0a, 0x0d, 0x4e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, - 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, - 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, - 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, - 0x67, 0x6e, 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x72, 0x65, - 0x6c, 0x61, 0x79, 0x12, 0x2a, 0x0a, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, - 0x6c, 0x6f, 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x66, 0x6c, 0x6f, 0x77, 0x22, - 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, - 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, - 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, - 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, - 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, - 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, - 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x6d, 0x0a, 0x0b, 0x52, 0x65, - 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x72, 0x6c, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x22, 0x0a, - 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, - 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xad, 0x02, 0x0a, 0x0a, 0x46, 0x6c, - 0x6f, 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x26, - 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, - 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, - 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x18, 0x0a, - 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, - 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x78, 0x69, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x43, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x12, 0x65, 0x78, 0x69, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x6e, 0x73, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x64, 0x6e, 0x73, 0x43, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xa3, 0x01, 0x0a, 0x09, 0x4a, 0x57, - 0x54, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, - 0x1a, 0x0a, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x6b, - 0x65, 0x79, 0x73, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x73, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x41, 0x67, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x41, 0x67, - 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x05, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x22, - 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, - 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, - 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xd3, - 0x02, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, - 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, - 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, - 0x64, 0x6e, 0x12, 0x48, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, - 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, - 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, - 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, - 0x4c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x4c, 0x61, 0x7a, - 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x74, 0x75, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x03, 0x6d, 0x74, 0x75, 0x12, 0x3e, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x6f, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x22, 0x52, 0x0a, 0x12, 0x41, 0x75, 0x74, 0x6f, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x61, 0x6c, 0x77, 0x61, - 0x79, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0xe8, 0x05, 0x0a, 0x0a, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, - 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, - 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, - 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, - 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, - 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, - 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, - 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, - 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, - 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, - 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, - 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, - 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, - 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, - 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, - 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, - 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, - 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, - 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, - 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, - 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, - 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, - 0x0a, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, - 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, - 0x75, 0x6c, 0x65, 0x52, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, - 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x73, 0x73, 0x68, 0x41, 0x75, 0x74, 0x68, 0x18, - 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x52, 0x07, 0x73, 0x73, 0x68, 0x41, - 0x75, 0x74, 0x68, 0x22, 0x82, 0x02, 0x0a, 0x07, 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x12, - 0x20, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x43, 0x6c, 0x61, 0x69, - 0x6d, 0x12, 0x28, 0x0a, 0x0f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x55, - 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0f, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x4a, 0x0a, 0x0d, 0x6d, - 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, - 0x73, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, - 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x1a, 0x5f, 0x0a, 0x11, 0x4d, 0x61, 0x63, 0x68, 0x69, - 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x34, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, - 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x2e, 0x0a, 0x12, 0x4d, 0x61, 0x63, 0x68, - 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x18, - 0x0a, 0x07, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0d, 0x52, - 0x07, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, - 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, - 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, - 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, - 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, - 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, - 0x64, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x7e, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, - 0x79, 0x12, 0x33, 0x0a, 0x09, 0x6a, 0x77, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x4a, 0x57, 0x54, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x6a, 0x77, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, - 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, - 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, - 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, - 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, - 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, - 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, - 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xb8, 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, - 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, - 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, - 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, - 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, - 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x55, 0x52, 0x4c, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, - 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, - 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, - 0x61, 0x67, 0x22, 0x93, 0x02, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, - 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, - 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, - 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, - 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, - 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x41, 0x70, - 0x70, 0x6c, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x41, - 0x75, 0x74, 0x6f, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x22, 0xde, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, - 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, - 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, - 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x12, - 0x28, 0x0a, 0x0d, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0d, 0x46, 0x6f, 0x72, 0x77, - 0x61, 0x72, 0x64, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xb8, 0x01, 0x0a, 0x0a, 0x43, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, - 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, - 0x6f, 0x72, 0x64, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x4e, 0x6f, 0x6e, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x10, 0x4e, 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x61, - 0x74, 0x69, 0x76, 0x65, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, - 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, - 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, - 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, - 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, - 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, - 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, - 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, - 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, - 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, - 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, - 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a, 0x0c, 0x46, - 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, - 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, - 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, - 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x49, 0x44, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, - 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, - 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, - 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, - 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, - 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, - 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, - 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, - 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, - 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, - 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, - 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, - 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, - 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, - 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, - 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, - 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, - 0x44, 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, - 0x52, 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, - 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, - 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, - 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, - 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, - 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, - 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, - 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, - 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, - 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, - 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, - 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x32, 0xcd, 0x04, 0x0a, 0x11, 0x4d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, - 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, - 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, - 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, - 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, - 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, - 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, - 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, - 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +// MachineRegisterResponse contains the configuration for the registered machine peer. +type MachineRegisterResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // peer_config is the local peer configuration + PeerConfig *PeerConfig `protobuf:"bytes,1,opt,name=peer_config,json=peerConfig,proto3" json:"peer_config,omitempty"` + // netbird_config contains STUN/TURN/Relay configuration + NetbirdConfig *NetbirdConfig `protobuf:"bytes,2,opt,name=netbird_config,json=netbirdConfig,proto3" json:"netbird_config,omitempty"` + // machine_identity is the identity extracted from the certificate (for client verification) + MachineIdentity *MachineIdentity `protobuf:"bytes,3,opt,name=machine_identity,json=machineIdentity,proto3" json:"machine_identity,omitempty"` + // allowed_dc_routes are the DC routes this machine is allowed to access + AllowedDcRoutes []*Route `protobuf:"bytes,4,rep,name=allowed_dc_routes,json=allowedDcRoutes,proto3" json:"allowed_dc_routes,omitempty"` + // dns_config for DC DNS resolution + DnsConfig *DNSConfig `protobuf:"bytes,5,opt,name=dns_config,json=dnsConfig,proto3" json:"dns_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -var ( - file_management_proto_rawDescOnce sync.Once - file_management_proto_rawDescData = file_management_proto_rawDesc -) +func (x *MachineRegisterResponse) Reset() { + *x = MachineRegisterResponse{} + mi := &file_management_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} -func file_management_proto_rawDescGZIP() []byte { - file_management_proto_rawDescOnce.Do(func() { - file_management_proto_rawDescData = protoimpl.X.CompressGZIP(file_management_proto_rawDescData) - }) - return file_management_proto_rawDescData +func (x *MachineRegisterResponse) String() string { + return protoimpl.X.MessageStringOf(x) } -var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 45) -var file_management_proto_goTypes = []interface{}{ - (RuleProtocol)(0), // 0: management.RuleProtocol - (RuleDirection)(0), // 1: management.RuleDirection - (RuleAction)(0), // 2: management.RuleAction - (HostConfig_Protocol)(0), // 3: management.HostConfig.Protocol - (DeviceAuthorizationFlowProvider)(0), // 4: management.DeviceAuthorizationFlow.provider - (*EncryptedMessage)(nil), // 5: management.EncryptedMessage - (*SyncRequest)(nil), // 6: management.SyncRequest - (*SyncResponse)(nil), // 7: management.SyncResponse - (*SyncMetaRequest)(nil), // 8: management.SyncMetaRequest - (*LoginRequest)(nil), // 9: management.LoginRequest - (*PeerKeys)(nil), // 10: management.PeerKeys - (*Environment)(nil), // 11: management.Environment - (*File)(nil), // 12: management.File - (*Flags)(nil), // 13: management.Flags - (*PeerSystemMeta)(nil), // 14: management.PeerSystemMeta - (*LoginResponse)(nil), // 15: management.LoginResponse - (*ServerKeyResponse)(nil), // 16: management.ServerKeyResponse - (*Empty)(nil), // 17: management.Empty - (*NetbirdConfig)(nil), // 18: management.NetbirdConfig - (*HostConfig)(nil), // 19: management.HostConfig - (*RelayConfig)(nil), // 20: management.RelayConfig - (*FlowConfig)(nil), // 21: management.FlowConfig - (*JWTConfig)(nil), // 22: management.JWTConfig - (*ProtectedHostConfig)(nil), // 23: management.ProtectedHostConfig - (*PeerConfig)(nil), // 24: management.PeerConfig - (*AutoUpdateSettings)(nil), // 25: management.AutoUpdateSettings - (*NetworkMap)(nil), // 26: management.NetworkMap - (*SSHAuth)(nil), // 27: management.SSHAuth - (*MachineUserIndexes)(nil), // 28: management.MachineUserIndexes - (*RemotePeerConfig)(nil), // 29: management.RemotePeerConfig - (*SSHConfig)(nil), // 30: management.SSHConfig - (*DeviceAuthorizationFlowRequest)(nil), // 31: management.DeviceAuthorizationFlowRequest - (*DeviceAuthorizationFlow)(nil), // 32: management.DeviceAuthorizationFlow - (*PKCEAuthorizationFlowRequest)(nil), // 33: management.PKCEAuthorizationFlowRequest - (*PKCEAuthorizationFlow)(nil), // 34: management.PKCEAuthorizationFlow - (*ProviderConfig)(nil), // 35: management.ProviderConfig - (*Route)(nil), // 36: management.Route - (*DNSConfig)(nil), // 37: management.DNSConfig - (*CustomZone)(nil), // 38: management.CustomZone - (*SimpleRecord)(nil), // 39: management.SimpleRecord - (*NameServerGroup)(nil), // 40: management.NameServerGroup - (*NameServer)(nil), // 41: management.NameServer - (*FirewallRule)(nil), // 42: management.FirewallRule - (*NetworkAddress)(nil), // 43: management.NetworkAddress - (*Checks)(nil), // 44: management.Checks - (*PortInfo)(nil), // 45: management.PortInfo - (*RouteFirewallRule)(nil), // 46: management.RouteFirewallRule - (*ForwardingRule)(nil), // 47: management.ForwardingRule - nil, // 48: management.SSHAuth.MachineUsersEntry - (*PortInfo_Range)(nil), // 49: management.PortInfo.Range - (*timestamppb.Timestamp)(nil), // 50: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 51: google.protobuf.Duration +func (*MachineRegisterResponse) ProtoMessage() {} + +func (x *MachineRegisterResponse) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -var file_management_proto_depIdxs = []int32{ - 14, // 0: management.SyncRequest.meta:type_name -> management.PeerSystemMeta - 18, // 1: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig - 24, // 2: management.SyncResponse.peerConfig:type_name -> management.PeerConfig - 29, // 3: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig - 26, // 4: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap - 44, // 5: management.SyncResponse.Checks:type_name -> management.Checks - 14, // 6: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta - 14, // 7: management.LoginRequest.meta:type_name -> management.PeerSystemMeta - 10, // 8: management.LoginRequest.peerKeys:type_name -> management.PeerKeys - 43, // 9: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress - 11, // 10: management.PeerSystemMeta.environment:type_name -> management.Environment - 12, // 11: management.PeerSystemMeta.files:type_name -> management.File - 13, // 12: management.PeerSystemMeta.flags:type_name -> management.Flags - 18, // 13: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig - 24, // 14: management.LoginResponse.peerConfig:type_name -> management.PeerConfig - 44, // 15: management.LoginResponse.Checks:type_name -> management.Checks - 50, // 16: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp - 19, // 17: management.NetbirdConfig.stuns:type_name -> management.HostConfig - 23, // 18: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig - 19, // 19: management.NetbirdConfig.signal:type_name -> management.HostConfig - 20, // 20: management.NetbirdConfig.relay:type_name -> management.RelayConfig - 21, // 21: management.NetbirdConfig.flow:type_name -> management.FlowConfig - 3, // 22: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol - 51, // 23: management.FlowConfig.interval:type_name -> google.protobuf.Duration - 19, // 24: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig - 30, // 25: management.PeerConfig.sshConfig:type_name -> management.SSHConfig - 25, // 26: management.PeerConfig.autoUpdate:type_name -> management.AutoUpdateSettings - 24, // 27: management.NetworkMap.peerConfig:type_name -> management.PeerConfig - 29, // 28: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig - 36, // 29: management.NetworkMap.Routes:type_name -> management.Route - 37, // 30: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig - 29, // 31: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig - 42, // 32: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule - 46, // 33: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule - 47, // 34: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule - 27, // 35: management.NetworkMap.sshAuth:type_name -> management.SSHAuth - 48, // 36: management.SSHAuth.machine_users:type_name -> management.SSHAuth.MachineUsersEntry - 30, // 37: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig - 22, // 38: management.SSHConfig.jwtConfig:type_name -> management.JWTConfig - 4, // 39: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider - 35, // 40: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 35, // 41: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 40, // 42: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup - 38, // 43: management.DNSConfig.CustomZones:type_name -> management.CustomZone - 39, // 44: management.CustomZone.Records:type_name -> management.SimpleRecord - 41, // 45: management.NameServerGroup.NameServers:type_name -> management.NameServer - 1, // 46: management.FirewallRule.Direction:type_name -> management.RuleDirection - 2, // 47: management.FirewallRule.Action:type_name -> management.RuleAction - 0, // 48: management.FirewallRule.Protocol:type_name -> management.RuleProtocol - 45, // 49: management.FirewallRule.PortInfo:type_name -> management.PortInfo - 49, // 50: management.PortInfo.range:type_name -> management.PortInfo.Range - 2, // 51: management.RouteFirewallRule.action:type_name -> management.RuleAction - 0, // 52: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol - 45, // 53: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo - 0, // 54: management.ForwardingRule.protocol:type_name -> management.RuleProtocol - 45, // 55: management.ForwardingRule.destinationPort:type_name -> management.PortInfo - 45, // 56: management.ForwardingRule.translatedPort:type_name -> management.PortInfo - 28, // 57: management.SSHAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes - 5, // 58: management.ManagementService.Login:input_type -> management.EncryptedMessage - 5, // 59: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 17, // 60: management.ManagementService.GetServerKey:input_type -> management.Empty - 17, // 61: management.ManagementService.isHealthy:input_type -> management.Empty - 5, // 62: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 63: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage - 5, // 64: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage - 5, // 65: management.ManagementService.Logout:input_type -> management.EncryptedMessage - 5, // 66: management.ManagementService.Login:output_type -> management.EncryptedMessage - 5, // 67: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 16, // 68: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 17, // 69: management.ManagementService.isHealthy:output_type -> management.Empty - 5, // 70: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 5, // 71: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage - 17, // 72: management.ManagementService.SyncMeta:output_type -> management.Empty - 17, // 73: management.ManagementService.Logout:output_type -> management.Empty - 66, // [66:74] is the sub-list for method output_type - 58, // [58:66] is the sub-list for method input_type - 58, // [58:58] is the sub-list for extension type_name - 58, // [58:58] is the sub-list for extension extendee - 0, // [0:58] is the sub-list for field type_name + +// Deprecated: Use MachineRegisterResponse.ProtoReflect.Descriptor instead. +func (*MachineRegisterResponse) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{45} } -func init() { file_management_proto_init() } -func file_management_proto_init() { - if File_management_proto != nil { - return +func (x *MachineRegisterResponse) GetPeerConfig() *PeerConfig { + if x != nil { + return x.PeerConfig } - if !protoimpl.UnsafeEnabled { - file_management_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EncryptedMessage); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SyncRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SyncResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SyncMetaRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoginRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerKeys); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Environment); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*File); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Flags); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerSystemMeta); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoginResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ServerKeyResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Empty); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetbirdConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HostConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RelayConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FlowConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*JWTConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProtectedHostConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AutoUpdateSettings); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkMap); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SSHAuth); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MachineUserIndexes); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemotePeerConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SSHConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeviceAuthorizationFlowRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeviceAuthorizationFlow); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PKCEAuthorizationFlowRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PKCEAuthorizationFlow); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProviderConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Route); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DNSConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CustomZone); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SimpleRecord); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServerGroup); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NameServer); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_management_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FirewallRule); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } + return nil +} + +func (x *MachineRegisterResponse) GetNetbirdConfig() *NetbirdConfig { + if x != nil { + return x.NetbirdConfig + } + return nil +} + +func (x *MachineRegisterResponse) GetMachineIdentity() *MachineIdentity { + if x != nil { + return x.MachineIdentity + } + return nil +} + +func (x *MachineRegisterResponse) GetAllowedDcRoutes() []*Route { + if x != nil { + return x.AllowedDcRoutes + } + return nil +} + +func (x *MachineRegisterResponse) GetDnsConfig() *DNSConfig { + if x != nil { + return x.DnsConfig + } + return nil +} + +// MachineSyncRequest is sent to initiate machine peer synchronization. +type MachineSyncRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // meta contains current machine metadata + Meta *PeerSystemMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MachineSyncRequest) Reset() { + *x = MachineSyncRequest{} + mi := &file_management_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MachineSyncRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MachineSyncRequest) ProtoMessage() {} + +func (x *MachineSyncRequest) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } - file_management_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NetworkAddress); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MachineSyncRequest.ProtoReflect.Descriptor instead. +func (*MachineSyncRequest) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{46} +} + +func (x *MachineSyncRequest) GetMeta() *PeerSystemMeta { + if x != nil { + return x.Meta + } + return nil +} + +// MachineSyncResponse contains updates for the machine peer. +type MachineSyncResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // network_map contains the current network state + NetworkMap *NetworkMap `protobuf:"bytes,1,opt,name=network_map,json=networkMap,proto3" json:"network_map,omitempty"` + // update_type indicates what changed + UpdateType MachineUpdateType `protobuf:"varint,2,opt,name=update_type,json=updateType,proto3,enum=management.MachineUpdateType" json:"update_type,omitempty"` + // serial is the network state serial number + Serial uint64 `protobuf:"varint,3,opt,name=serial,proto3" json:"serial,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MachineSyncResponse) Reset() { + *x = MachineSyncResponse{} + mi := &file_management_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MachineSyncResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MachineSyncResponse) ProtoMessage() {} + +func (x *MachineSyncResponse) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } - file_management_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Checks); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MachineSyncResponse.ProtoReflect.Descriptor instead. +func (*MachineSyncResponse) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{47} +} + +func (x *MachineSyncResponse) GetNetworkMap() *NetworkMap { + if x != nil { + return x.NetworkMap + } + return nil +} + +func (x *MachineSyncResponse) GetUpdateType() MachineUpdateType { + if x != nil { + return x.UpdateType + } + return MachineUpdateType_MACHINE_UPDATE_FULL +} + +func (x *MachineSyncResponse) GetSerial() uint64 { + if x != nil { + return x.Serial + } + return 0 +} + +// MachineRoutesRequest requests routes for the machine peer. +type MachineRoutesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // include_offline if true, includes routes via offline router-peers + IncludeOffline bool `protobuf:"varint,1,opt,name=include_offline,json=includeOffline,proto3" json:"include_offline,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MachineRoutesRequest) Reset() { + *x = MachineRoutesRequest{} + mi := &file_management_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MachineRoutesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MachineRoutesRequest) ProtoMessage() {} + +func (x *MachineRoutesRequest) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } - file_management_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortInfo); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MachineRoutesRequest.ProtoReflect.Descriptor instead. +func (*MachineRoutesRequest) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{48} +} + +func (x *MachineRoutesRequest) GetIncludeOffline() bool { + if x != nil { + return x.IncludeOffline + } + return false +} + +// MachineRoutesResponse contains routes accessible by this machine peer. +type MachineRoutesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // routes are the currently active routes + Routes []*Route `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` + // dc_networks are the Domain Controller network CIDRs + DcNetworks []string `protobuf:"bytes,2,rep,name=dc_networks,json=dcNetworks,proto3" json:"dc_networks,omitempty"` + // router_peers are the peers that route DC traffic + RouterPeers []*RemotePeerConfig `protobuf:"bytes,3,rep,name=router_peers,json=routerPeers,proto3" json:"router_peers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MachineRoutesResponse) Reset() { + *x = MachineRoutesResponse{} + mi := &file_management_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MachineRoutesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MachineRoutesResponse) ProtoMessage() {} + +func (x *MachineRoutesResponse) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } - file_management_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RouteFirewallRule); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MachineRoutesResponse.ProtoReflect.Descriptor instead. +func (*MachineRoutesResponse) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{49} +} + +func (x *MachineRoutesResponse) GetRoutes() []*Route { + if x != nil { + return x.Routes + } + return nil +} + +func (x *MachineRoutesResponse) GetDcNetworks() []string { + if x != nil { + return x.DcNetworks + } + return nil +} + +func (x *MachineRoutesResponse) GetRouterPeers() []*RemotePeerConfig { + if x != nil { + return x.RouterPeers + } + return nil +} + +// MachineStatusRequest reports machine tunnel status. +type MachineStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // tunnel_up indicates if the WireGuard tunnel is established + TunnelUp bool `protobuf:"varint,1,opt,name=tunnel_up,json=tunnelUp,proto3" json:"tunnel_up,omitempty"` + // connected_router_peer is the currently connected router-peer (if any) + ConnectedRouterPeer string `protobuf:"bytes,2,opt,name=connected_router_peer,json=connectedRouterPeer,proto3" json:"connected_router_peer,omitempty"` + // last_handshake is the timestamp of last WireGuard handshake + LastHandshake *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=last_handshake,json=lastHandshake,proto3" json:"last_handshake,omitempty"` + // dc_reachable indicates if DC connectivity test succeeded + DcReachable bool `protobuf:"varint,4,opt,name=dc_reachable,json=dcReachable,proto3" json:"dc_reachable,omitempty"` + // errors contains any error messages + Errors []string `protobuf:"bytes,5,rep,name=errors,proto3" json:"errors,omitempty"` + // uptime_seconds is how long the tunnel has been up + UptimeSeconds int64 `protobuf:"varint,6,opt,name=uptime_seconds,json=uptimeSeconds,proto3" json:"uptime_seconds,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MachineStatusRequest) Reset() { + *x = MachineStatusRequest{} + mi := &file_management_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MachineStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MachineStatusRequest) ProtoMessage() {} + +func (x *MachineStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } - file_management_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardingRule); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MachineStatusRequest.ProtoReflect.Descriptor instead. +func (*MachineStatusRequest) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{50} +} + +func (x *MachineStatusRequest) GetTunnelUp() bool { + if x != nil { + return x.TunnelUp + } + return false +} + +func (x *MachineStatusRequest) GetConnectedRouterPeer() string { + if x != nil { + return x.ConnectedRouterPeer + } + return "" +} + +func (x *MachineStatusRequest) GetLastHandshake() *timestamppb.Timestamp { + if x != nil { + return x.LastHandshake + } + return nil +} + +func (x *MachineStatusRequest) GetDcReachable() bool { + if x != nil { + return x.DcReachable + } + return false +} + +func (x *MachineStatusRequest) GetErrors() []string { + if x != nil { + return x.Errors + } + return nil +} + +func (x *MachineStatusRequest) GetUptimeSeconds() int64 { + if x != nil { + return x.UptimeSeconds + } + return 0 +} + +// MachineStatusResponse acknowledges the status report. +type MachineStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // ack indicates the status was received + Ack bool `protobuf:"varint,1,opt,name=ack,proto3" json:"ack,omitempty"` + // server_time is the current server time (for clock sync verification) + ServerTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=server_time,json=serverTime,proto3" json:"server_time,omitempty"` + // config_serial is the current config serial (client can compare) + ConfigSerial uint64 `protobuf:"varint,3,opt,name=config_serial,json=configSerial,proto3" json:"config_serial,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MachineStatusResponse) Reset() { + *x = MachineStatusResponse{} + mi := &file_management_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MachineStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MachineStatusResponse) ProtoMessage() {} + +func (x *MachineStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[51] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } - file_management_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortInfo_Range); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MachineStatusResponse.ProtoReflect.Descriptor instead. +func (*MachineStatusResponse) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{51} +} + +func (x *MachineStatusResponse) GetAck() bool { + if x != nil { + return x.Ack + } + return false +} + +func (x *MachineStatusResponse) GetServerTime() *timestamppb.Timestamp { + if x != nil { + return x.ServerTime + } + return nil +} + +func (x *MachineStatusResponse) GetConfigSerial() uint64 { + if x != nil { + return x.ConfigSerial + } + return 0 +} + +type PortInfo_Range struct { + state protoimpl.MessageState `protogen:"open.v1"` + Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` + End uint32 `protobuf:"varint,2,opt,name=end,proto3" json:"end,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PortInfo_Range) Reset() { + *x = PortInfo_Range{} + mi := &file_management_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PortInfo_Range) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PortInfo_Range) ProtoMessage() {} + +func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[53] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PortInfo_Range.ProtoReflect.Descriptor instead. +func (*PortInfo_Range) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{40, 0} +} + +func (x *PortInfo_Range) GetStart() uint32 { + if x != nil { + return x.Start + } + return 0 +} + +func (x *PortInfo_Range) GetEnd() uint32 { + if x != nil { + return x.End + } + return 0 +} + +var File_management_proto protoreflect.FileDescriptor + +const file_management_proto_rawDesc = "" + + "\n" + + "\x10management.proto\x12\n" + + "management\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\"\\\n" + + "\x10EncryptedMessage\x12\x1a\n" + + "\bwgPubKey\x18\x01 \x01(\tR\bwgPubKey\x12\x12\n" + + "\x04body\x18\x02 \x01(\fR\x04body\x12\x18\n" + + "\aversion\x18\x03 \x01(\x05R\aversion\"=\n" + + "\vSyncRequest\x12.\n" + + "\x04meta\x18\x01 \x01(\v2\x1a.management.PeerSystemMetaR\x04meta\"\xdb\x02\n" + + "\fSyncResponse\x12?\n" + + "\rnetbirdConfig\x18\x01 \x01(\v2\x19.management.NetbirdConfigR\rnetbirdConfig\x126\n" + + "\n" + + "peerConfig\x18\x02 \x01(\v2\x16.management.PeerConfigR\n" + + "peerConfig\x12>\n" + + "\vremotePeers\x18\x03 \x03(\v2\x1c.management.RemotePeerConfigR\vremotePeers\x12.\n" + + "\x12remotePeersIsEmpty\x18\x04 \x01(\bR\x12remotePeersIsEmpty\x126\n" + + "\n" + + "NetworkMap\x18\x05 \x01(\v2\x16.management.NetworkMapR\n" + + "NetworkMap\x12*\n" + + "\x06Checks\x18\x06 \x03(\v2\x12.management.ChecksR\x06Checks\"A\n" + + "\x0fSyncMetaRequest\x12.\n" + + "\x04meta\x18\x01 \x01(\v2\x1a.management.PeerSystemMetaR\x04meta\"\xc6\x01\n" + + "\fLoginRequest\x12\x1a\n" + + "\bsetupKey\x18\x01 \x01(\tR\bsetupKey\x12.\n" + + "\x04meta\x18\x02 \x01(\v2\x1a.management.PeerSystemMetaR\x04meta\x12\x1a\n" + + "\bjwtToken\x18\x03 \x01(\tR\bjwtToken\x120\n" + + "\bpeerKeys\x18\x04 \x01(\v2\x14.management.PeerKeysR\bpeerKeys\x12\x1c\n" + + "\tdnsLabels\x18\x05 \x03(\tR\tdnsLabels\"D\n" + + "\bPeerKeys\x12\x1c\n" + + "\tsshPubKey\x18\x01 \x01(\fR\tsshPubKey\x12\x1a\n" + + "\bwgPubKey\x18\x02 \x01(\fR\bwgPubKey\"?\n" + + "\vEnvironment\x12\x14\n" + + "\x05cloud\x18\x01 \x01(\tR\x05cloud\x12\x1a\n" + + "\bplatform\x18\x02 \x01(\tR\bplatform\"\\\n" + + "\x04File\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x14\n" + + "\x05exist\x18\x02 \x01(\bR\x05exist\x12*\n" + + "\x10processIsRunning\x18\x03 \x01(\bR\x10processIsRunning\"\xbf\x05\n" + + "\x05Flags\x12*\n" + + "\x10rosenpassEnabled\x18\x01 \x01(\bR\x10rosenpassEnabled\x120\n" + + "\x13rosenpassPermissive\x18\x02 \x01(\bR\x13rosenpassPermissive\x12*\n" + + "\x10serverSSHAllowed\x18\x03 \x01(\bR\x10serverSSHAllowed\x120\n" + + "\x13disableClientRoutes\x18\x04 \x01(\bR\x13disableClientRoutes\x120\n" + + "\x13disableServerRoutes\x18\x05 \x01(\bR\x13disableServerRoutes\x12\x1e\n" + + "\n" + + "disableDNS\x18\x06 \x01(\bR\n" + + "disableDNS\x12(\n" + + "\x0fdisableFirewall\x18\a \x01(\bR\x0fdisableFirewall\x12&\n" + + "\x0eblockLANAccess\x18\b \x01(\bR\x0eblockLANAccess\x12\"\n" + + "\fblockInbound\x18\t \x01(\bR\fblockInbound\x124\n" + + "\x15lazyConnectionEnabled\x18\n" + + " \x01(\bR\x15lazyConnectionEnabled\x12$\n" + + "\renableSSHRoot\x18\v \x01(\bR\renableSSHRoot\x12$\n" + + "\renableSSHSFTP\x18\f \x01(\bR\renableSSHSFTP\x12B\n" + + "\x1cenableSSHLocalPortForwarding\x18\r \x01(\bR\x1cenableSSHLocalPortForwarding\x12D\n" + + "\x1denableSSHRemotePortForwarding\x18\x0e \x01(\bR\x1denableSSHRemotePortForwarding\x12&\n" + + "\x0edisableSSHAuth\x18\x0f \x01(\bR\x0edisableSSHAuth\"\xf2\x04\n" + + "\x0ePeerSystemMeta\x12\x1a\n" + + "\bhostname\x18\x01 \x01(\tR\bhostname\x12\x12\n" + + "\x04goOS\x18\x02 \x01(\tR\x04goOS\x12\x16\n" + + "\x06kernel\x18\x03 \x01(\tR\x06kernel\x12\x12\n" + + "\x04core\x18\x04 \x01(\tR\x04core\x12\x1a\n" + + "\bplatform\x18\x05 \x01(\tR\bplatform\x12\x0e\n" + + "\x02OS\x18\x06 \x01(\tR\x02OS\x12&\n" + + "\x0enetbirdVersion\x18\a \x01(\tR\x0enetbirdVersion\x12\x1c\n" + + "\tuiVersion\x18\b \x01(\tR\tuiVersion\x12$\n" + + "\rkernelVersion\x18\t \x01(\tR\rkernelVersion\x12\x1c\n" + + "\tOSVersion\x18\n" + + " \x01(\tR\tOSVersion\x12F\n" + + "\x10networkAddresses\x18\v \x03(\v2\x1a.management.NetworkAddressR\x10networkAddresses\x12(\n" + + "\x0fsysSerialNumber\x18\f \x01(\tR\x0fsysSerialNumber\x12&\n" + + "\x0esysProductName\x18\r \x01(\tR\x0esysProductName\x12(\n" + + "\x0fsysManufacturer\x18\x0e \x01(\tR\x0fsysManufacturer\x129\n" + + "\venvironment\x18\x0f \x01(\v2\x17.management.EnvironmentR\venvironment\x12&\n" + + "\x05files\x18\x10 \x03(\v2\x10.management.FileR\x05files\x12'\n" + + "\x05flags\x18\x11 \x01(\v2\x11.management.FlagsR\x05flags\"\xb4\x01\n" + + "\rLoginResponse\x12?\n" + + "\rnetbirdConfig\x18\x01 \x01(\v2\x19.management.NetbirdConfigR\rnetbirdConfig\x126\n" + + "\n" + + "peerConfig\x18\x02 \x01(\v2\x16.management.PeerConfigR\n" + + "peerConfig\x12*\n" + + "\x06Checks\x18\x03 \x03(\v2\x12.management.ChecksR\x06Checks\"y\n" + + "\x11ServerKeyResponse\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x128\n" + + "\texpiresAt\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12\x18\n" + + "\aversion\x18\x03 \x01(\x05R\aversion\"\a\n" + + "\x05Empty\"\xff\x01\n" + + "\rNetbirdConfig\x12,\n" + + "\x05stuns\x18\x01 \x03(\v2\x16.management.HostConfigR\x05stuns\x125\n" + + "\x05turns\x18\x02 \x03(\v2\x1f.management.ProtectedHostConfigR\x05turns\x12.\n" + + "\x06signal\x18\x03 \x01(\v2\x16.management.HostConfigR\x06signal\x12-\n" + + "\x05relay\x18\x04 \x01(\v2\x17.management.RelayConfigR\x05relay\x12*\n" + + "\x04flow\x18\x05 \x01(\v2\x16.management.FlowConfigR\x04flow\"\x98\x01\n" + + "\n" + + "HostConfig\x12\x10\n" + + "\x03uri\x18\x01 \x01(\tR\x03uri\x12;\n" + + "\bprotocol\x18\x02 \x01(\x0e2\x1f.management.HostConfig.ProtocolR\bprotocol\";\n" + + "\bProtocol\x12\a\n" + + "\x03UDP\x10\x00\x12\a\n" + + "\x03TCP\x10\x01\x12\b\n" + + "\x04HTTP\x10\x02\x12\t\n" + + "\x05HTTPS\x10\x03\x12\b\n" + + "\x04DTLS\x10\x04\"m\n" + + "\vRelayConfig\x12\x12\n" + + "\x04urls\x18\x01 \x03(\tR\x04urls\x12\"\n" + + "\ftokenPayload\x18\x02 \x01(\tR\ftokenPayload\x12&\n" + + "\x0etokenSignature\x18\x03 \x01(\tR\x0etokenSignature\"\xad\x02\n" + + "\n" + + "FlowConfig\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\x12\"\n" + + "\ftokenPayload\x18\x02 \x01(\tR\ftokenPayload\x12&\n" + + "\x0etokenSignature\x18\x03 \x01(\tR\x0etokenSignature\x125\n" + + "\binterval\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\binterval\x12\x18\n" + + "\aenabled\x18\x05 \x01(\bR\aenabled\x12\x1a\n" + + "\bcounters\x18\x06 \x01(\bR\bcounters\x12.\n" + + "\x12exitNodeCollection\x18\a \x01(\bR\x12exitNodeCollection\x12$\n" + + "\rdnsCollection\x18\b \x01(\bR\rdnsCollection\"\xa3\x01\n" + + "\tJWTConfig\x12\x16\n" + + "\x06issuer\x18\x01 \x01(\tR\x06issuer\x12\x1a\n" + + "\baudience\x18\x02 \x01(\tR\baudience\x12\"\n" + + "\fkeysLocation\x18\x03 \x01(\tR\fkeysLocation\x12 \n" + + "\vmaxTokenAge\x18\x04 \x01(\x03R\vmaxTokenAge\x12\x1c\n" + + "\taudiences\x18\x05 \x03(\tR\taudiences\"}\n" + + "\x13ProtectedHostConfig\x126\n" + + "\n" + + "hostConfig\x18\x01 \x01(\v2\x16.management.HostConfigR\n" + + "hostConfig\x12\x12\n" + + "\x04user\x18\x02 \x01(\tR\x04user\x12\x1a\n" + + "\bpassword\x18\x03 \x01(\tR\bpassword\"\xd3\x02\n" + + "\n" + + "PeerConfig\x12\x18\n" + + "\aaddress\x18\x01 \x01(\tR\aaddress\x12\x10\n" + + "\x03dns\x18\x02 \x01(\tR\x03dns\x123\n" + + "\tsshConfig\x18\x03 \x01(\v2\x15.management.SSHConfigR\tsshConfig\x12\x12\n" + + "\x04fqdn\x18\x04 \x01(\tR\x04fqdn\x12H\n" + + "\x1fRoutingPeerDnsResolutionEnabled\x18\x05 \x01(\bR\x1fRoutingPeerDnsResolutionEnabled\x124\n" + + "\x15LazyConnectionEnabled\x18\x06 \x01(\bR\x15LazyConnectionEnabled\x12\x10\n" + + "\x03mtu\x18\a \x01(\x05R\x03mtu\x12>\n" + + "\n" + + "autoUpdate\x18\b \x01(\v2\x1e.management.AutoUpdateSettingsR\n" + + "autoUpdate\"R\n" + + "\x12AutoUpdateSettings\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12\"\n" + + "\falwaysUpdate\x18\x02 \x01(\bR\falwaysUpdate\"\xe8\x05\n" + + "\n" + + "NetworkMap\x12\x16\n" + + "\x06Serial\x18\x01 \x01(\x04R\x06Serial\x126\n" + + "\n" + + "peerConfig\x18\x02 \x01(\v2\x16.management.PeerConfigR\n" + + "peerConfig\x12>\n" + + "\vremotePeers\x18\x03 \x03(\v2\x1c.management.RemotePeerConfigR\vremotePeers\x12.\n" + + "\x12remotePeersIsEmpty\x18\x04 \x01(\bR\x12remotePeersIsEmpty\x12)\n" + + "\x06Routes\x18\x05 \x03(\v2\x11.management.RouteR\x06Routes\x123\n" + + "\tDNSConfig\x18\x06 \x01(\v2\x15.management.DNSConfigR\tDNSConfig\x12@\n" + + "\fofflinePeers\x18\a \x03(\v2\x1c.management.RemotePeerConfigR\fofflinePeers\x12>\n" + + "\rFirewallRules\x18\b \x03(\v2\x18.management.FirewallRuleR\rFirewallRules\x122\n" + + "\x14firewallRulesIsEmpty\x18\t \x01(\bR\x14firewallRulesIsEmpty\x12O\n" + + "\x13routesFirewallRules\x18\n" + + " \x03(\v2\x1d.management.RouteFirewallRuleR\x13routesFirewallRules\x12>\n" + + "\x1aroutesFirewallRulesIsEmpty\x18\v \x01(\bR\x1aroutesFirewallRulesIsEmpty\x12D\n" + + "\x0fforwardingRules\x18\f \x03(\v2\x1a.management.ForwardingRuleR\x0fforwardingRules\x12-\n" + + "\asshAuth\x18\r \x01(\v2\x13.management.SSHAuthR\asshAuth\"\x82\x02\n" + + "\aSSHAuth\x12 \n" + + "\vUserIDClaim\x18\x01 \x01(\tR\vUserIDClaim\x12(\n" + + "\x0fAuthorizedUsers\x18\x02 \x03(\fR\x0fAuthorizedUsers\x12J\n" + + "\rmachine_users\x18\x03 \x03(\v2%.management.SSHAuth.MachineUsersEntryR\fmachineUsers\x1a_\n" + + "\x11MachineUsersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x124\n" + + "\x05value\x18\x02 \x01(\v2\x1e.management.MachineUserIndexesR\x05value:\x028\x01\".\n" + + "\x12MachineUserIndexes\x12\x18\n" + + "\aindexes\x18\x01 \x03(\rR\aindexes\"\xbb\x01\n" + + "\x10RemotePeerConfig\x12\x1a\n" + + "\bwgPubKey\x18\x01 \x01(\tR\bwgPubKey\x12\x1e\n" + + "\n" + + "allowedIps\x18\x02 \x03(\tR\n" + + "allowedIps\x123\n" + + "\tsshConfig\x18\x03 \x01(\v2\x15.management.SSHConfigR\tsshConfig\x12\x12\n" + + "\x04fqdn\x18\x04 \x01(\tR\x04fqdn\x12\"\n" + + "\fagentVersion\x18\x05 \x01(\tR\fagentVersion\"~\n" + + "\tSSHConfig\x12\x1e\n" + + "\n" + + "sshEnabled\x18\x01 \x01(\bR\n" + + "sshEnabled\x12\x1c\n" + + "\tsshPubKey\x18\x02 \x01(\fR\tsshPubKey\x123\n" + + "\tjwtConfig\x18\x03 \x01(\v2\x15.management.JWTConfigR\tjwtConfig\" \n" + + "\x1eDeviceAuthorizationFlowRequest\"\xbf\x01\n" + + "\x17DeviceAuthorizationFlow\x12H\n" + + "\bProvider\x18\x01 \x01(\x0e2,.management.DeviceAuthorizationFlow.providerR\bProvider\x12B\n" + + "\x0eProviderConfig\x18\x02 \x01(\v2\x1a.management.ProviderConfigR\x0eProviderConfig\"\x16\n" + + "\bprovider\x12\n" + + "\n" + + "\x06HOSTED\x10\x00\"\x1e\n" + + "\x1cPKCEAuthorizationFlowRequest\"[\n" + + "\x15PKCEAuthorizationFlow\x12B\n" + + "\x0eProviderConfig\x18\x01 \x01(\v2\x1a.management.ProviderConfigR\x0eProviderConfig\"\xb8\x03\n" + + "\x0eProviderConfig\x12\x1a\n" + + "\bClientID\x18\x01 \x01(\tR\bClientID\x12\"\n" + + "\fClientSecret\x18\x02 \x01(\tR\fClientSecret\x12\x16\n" + + "\x06Domain\x18\x03 \x01(\tR\x06Domain\x12\x1a\n" + + "\bAudience\x18\x04 \x01(\tR\bAudience\x12.\n" + + "\x12DeviceAuthEndpoint\x18\x05 \x01(\tR\x12DeviceAuthEndpoint\x12$\n" + + "\rTokenEndpoint\x18\x06 \x01(\tR\rTokenEndpoint\x12\x14\n" + + "\x05Scope\x18\a \x01(\tR\x05Scope\x12\x1e\n" + + "\n" + + "UseIDToken\x18\b \x01(\bR\n" + + "UseIDToken\x124\n" + + "\x15AuthorizationEndpoint\x18\t \x01(\tR\x15AuthorizationEndpoint\x12\"\n" + + "\fRedirectURLs\x18\n" + + " \x03(\tR\fRedirectURLs\x12.\n" + + "\x12DisablePromptLogin\x18\v \x01(\bR\x12DisablePromptLogin\x12\x1c\n" + + "\tLoginFlag\x18\f \x01(\rR\tLoginFlag\"\x93\x02\n" + + "\x05Route\x12\x0e\n" + + "\x02ID\x18\x01 \x01(\tR\x02ID\x12\x18\n" + + "\aNetwork\x18\x02 \x01(\tR\aNetwork\x12 \n" + + "\vNetworkType\x18\x03 \x01(\x03R\vNetworkType\x12\x12\n" + + "\x04Peer\x18\x04 \x01(\tR\x04Peer\x12\x16\n" + + "\x06Metric\x18\x05 \x01(\x03R\x06Metric\x12\x1e\n" + + "\n" + + "Masquerade\x18\x06 \x01(\bR\n" + + "Masquerade\x12\x14\n" + + "\x05NetID\x18\a \x01(\tR\x05NetID\x12\x18\n" + + "\aDomains\x18\b \x03(\tR\aDomains\x12\x1c\n" + + "\tkeepRoute\x18\t \x01(\bR\tkeepRoute\x12$\n" + + "\rskipAutoApply\x18\n" + + " \x01(\bR\rskipAutoApply\"\xde\x01\n" + + "\tDNSConfig\x12$\n" + + "\rServiceEnable\x18\x01 \x01(\bR\rServiceEnable\x12G\n" + + "\x10NameServerGroups\x18\x02 \x03(\v2\x1b.management.NameServerGroupR\x10NameServerGroups\x128\n" + + "\vCustomZones\x18\x03 \x03(\v2\x16.management.CustomZoneR\vCustomZones\x12(\n" + + "\rForwarderPort\x18\x04 \x01(\x03B\x02\x18\x01R\rForwarderPort\"\xb8\x01\n" + + "\n" + + "CustomZone\x12\x16\n" + + "\x06Domain\x18\x01 \x01(\tR\x06Domain\x122\n" + + "\aRecords\x18\x02 \x03(\v2\x18.management.SimpleRecordR\aRecords\x122\n" + + "\x14SearchDomainDisabled\x18\x03 \x01(\bR\x14SearchDomainDisabled\x12*\n" + + "\x10NonAuthoritative\x18\x04 \x01(\bR\x10NonAuthoritative\"t\n" + + "\fSimpleRecord\x12\x12\n" + + "\x04Name\x18\x01 \x01(\tR\x04Name\x12\x12\n" + + "\x04Type\x18\x02 \x01(\x03R\x04Type\x12\x14\n" + + "\x05Class\x18\x03 \x01(\tR\x05Class\x12\x10\n" + + "\x03TTL\x18\x04 \x01(\x03R\x03TTL\x12\x14\n" + + "\x05RData\x18\x05 \x01(\tR\x05RData\"\xb3\x01\n" + + "\x0fNameServerGroup\x128\n" + + "\vNameServers\x18\x01 \x03(\v2\x16.management.NameServerR\vNameServers\x12\x18\n" + + "\aPrimary\x18\x02 \x01(\bR\aPrimary\x12\x18\n" + + "\aDomains\x18\x03 \x03(\tR\aDomains\x122\n" + + "\x14SearchDomainsEnabled\x18\x04 \x01(\bR\x14SearchDomainsEnabled\"H\n" + + "\n" + + "NameServer\x12\x0e\n" + + "\x02IP\x18\x01 \x01(\tR\x02IP\x12\x16\n" + + "\x06NSType\x18\x02 \x01(\x03R\x06NSType\x12\x12\n" + + "\x04Port\x18\x03 \x01(\x03R\x04Port\"\xa7\x02\n" + + "\fFirewallRule\x12\x16\n" + + "\x06PeerIP\x18\x01 \x01(\tR\x06PeerIP\x127\n" + + "\tDirection\x18\x02 \x01(\x0e2\x19.management.RuleDirectionR\tDirection\x12.\n" + + "\x06Action\x18\x03 \x01(\x0e2\x16.management.RuleActionR\x06Action\x124\n" + + "\bProtocol\x18\x04 \x01(\x0e2\x18.management.RuleProtocolR\bProtocol\x12\x12\n" + + "\x04Port\x18\x05 \x01(\tR\x04Port\x120\n" + + "\bPortInfo\x18\x06 \x01(\v2\x14.management.PortInfoR\bPortInfo\x12\x1a\n" + + "\bPolicyID\x18\a \x01(\fR\bPolicyID\"8\n" + + "\x0eNetworkAddress\x12\x14\n" + + "\x05netIP\x18\x01 \x01(\tR\x05netIP\x12\x10\n" + + "\x03mac\x18\x02 \x01(\tR\x03mac\"\x1e\n" + + "\x06Checks\x12\x14\n" + + "\x05Files\x18\x01 \x03(\tR\x05Files\"\x96\x01\n" + + "\bPortInfo\x12\x14\n" + + "\x04port\x18\x01 \x01(\rH\x00R\x04port\x122\n" + + "\x05range\x18\x02 \x01(\v2\x1a.management.PortInfo.RangeH\x00R\x05range\x1a/\n" + + "\x05Range\x12\x14\n" + + "\x05start\x18\x01 \x01(\rR\x05start\x12\x10\n" + + "\x03end\x18\x02 \x01(\rR\x03endB\x0f\n" + + "\rportSelection\"\x87\x03\n" + + "\x11RouteFirewallRule\x12\"\n" + + "\fsourceRanges\x18\x01 \x03(\tR\fsourceRanges\x12.\n" + + "\x06action\x18\x02 \x01(\x0e2\x16.management.RuleActionR\x06action\x12 \n" + + "\vdestination\x18\x03 \x01(\tR\vdestination\x124\n" + + "\bprotocol\x18\x04 \x01(\x0e2\x18.management.RuleProtocolR\bprotocol\x120\n" + + "\bportInfo\x18\x05 \x01(\v2\x14.management.PortInfoR\bportInfo\x12\x1c\n" + + "\tisDynamic\x18\x06 \x01(\bR\tisDynamic\x12\x18\n" + + "\adomains\x18\a \x03(\tR\adomains\x12&\n" + + "\x0ecustomProtocol\x18\b \x01(\rR\x0ecustomProtocol\x12\x1a\n" + + "\bPolicyID\x18\t \x01(\fR\bPolicyID\x12\x18\n" + + "\aRouteID\x18\n" + + " \x01(\tR\aRouteID\"\xf2\x01\n" + + "\x0eForwardingRule\x124\n" + + "\bprotocol\x18\x01 \x01(\x0e2\x18.management.RuleProtocolR\bprotocol\x12>\n" + + "\x0fdestinationPort\x18\x02 \x01(\v2\x14.management.PortInfoR\x0fdestinationPort\x12,\n" + + "\x11translatedAddress\x18\x03 \x01(\fR\x11translatedAddress\x12<\n" + + "\x0etranslatedPort\x18\x04 \x01(\v2\x14.management.PortInfoR\x0etranslatedPort\"\xd7\x01\n" + + "\x0fMachineIdentity\x12\x19\n" + + "\bdns_name\x18\x01 \x01(\tR\adnsName\x12\x1a\n" + + "\bhostname\x18\x02 \x01(\tR\bhostname\x12\x16\n" + + "\x06domain\x18\x03 \x01(\tR\x06domain\x12-\n" + + "\x12issuer_fingerprint\x18\x04 \x01(\tR\x11issuerFingerprint\x12#\n" + + "\rserial_number\x18\x05 \x01(\tR\fserialNumber\x12!\n" + + "\ftemplate_oid\x18\x06 \x01(\tR\vtemplateOid\"\x89\x01\n" + + "\x16MachineRegisterRequest\x12.\n" + + "\x04meta\x18\x01 \x01(\v2\x1a.management.PeerSystemMetaR\x04meta\x12\x1c\n" + + "\n" + + "wg_pub_key\x18\x02 \x01(\fR\bwgPubKey\x12!\n" + + "\frequested_ip\x18\x03 \x01(\tR\vrequestedIp\"\xd1\x02\n" + + "\x17MachineRegisterResponse\x127\n" + + "\vpeer_config\x18\x01 \x01(\v2\x16.management.PeerConfigR\n" + + "peerConfig\x12@\n" + + "\x0enetbird_config\x18\x02 \x01(\v2\x19.management.NetbirdConfigR\rnetbirdConfig\x12F\n" + + "\x10machine_identity\x18\x03 \x01(\v2\x1b.management.MachineIdentityR\x0fmachineIdentity\x12=\n" + + "\x11allowed_dc_routes\x18\x04 \x03(\v2\x11.management.RouteR\x0fallowedDcRoutes\x124\n" + + "\n" + + "dns_config\x18\x05 \x01(\v2\x15.management.DNSConfigR\tdnsConfig\"D\n" + + "\x12MachineSyncRequest\x12.\n" + + "\x04meta\x18\x01 \x01(\v2\x1a.management.PeerSystemMetaR\x04meta\"\xa6\x01\n" + + "\x13MachineSyncResponse\x127\n" + + "\vnetwork_map\x18\x01 \x01(\v2\x16.management.NetworkMapR\n" + + "networkMap\x12>\n" + + "\vupdate_type\x18\x02 \x01(\x0e2\x1d.management.MachineUpdateTypeR\n" + + "updateType\x12\x16\n" + + "\x06serial\x18\x03 \x01(\x04R\x06serial\"?\n" + + "\x14MachineRoutesRequest\x12'\n" + + "\x0finclude_offline\x18\x01 \x01(\bR\x0eincludeOffline\"\xa4\x01\n" + + "\x15MachineRoutesResponse\x12)\n" + + "\x06routes\x18\x01 \x03(\v2\x11.management.RouteR\x06routes\x12\x1f\n" + + "\vdc_networks\x18\x02 \x03(\tR\n" + + "dcNetworks\x12?\n" + + "\frouter_peers\x18\x03 \x03(\v2\x1c.management.RemotePeerConfigR\vrouterPeers\"\x8c\x02\n" + + "\x14MachineStatusRequest\x12\x1b\n" + + "\ttunnel_up\x18\x01 \x01(\bR\btunnelUp\x122\n" + + "\x15connected_router_peer\x18\x02 \x01(\tR\x13connectedRouterPeer\x12A\n" + + "\x0elast_handshake\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\rlastHandshake\x12!\n" + + "\fdc_reachable\x18\x04 \x01(\bR\vdcReachable\x12\x16\n" + + "\x06errors\x18\x05 \x03(\tR\x06errors\x12%\n" + + "\x0euptime_seconds\x18\x06 \x01(\x03R\ruptimeSeconds\"\x8b\x01\n" + + "\x15MachineStatusResponse\x12\x10\n" + + "\x03ack\x18\x01 \x01(\bR\x03ack\x12;\n" + + "\vserver_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "serverTime\x12#\n" + + "\rconfig_serial\x18\x03 \x01(\x04R\fconfigSerial*L\n" + + "\fRuleProtocol\x12\v\n" + + "\aUNKNOWN\x10\x00\x12\a\n" + + "\x03ALL\x10\x01\x12\a\n" + + "\x03TCP\x10\x02\x12\a\n" + + "\x03UDP\x10\x03\x12\b\n" + + "\x04ICMP\x10\x04\x12\n" + + "\n" + + "\x06CUSTOM\x10\x05* \n" + + "\rRuleDirection\x12\x06\n" + + "\x02IN\x10\x00\x12\a\n" + + "\x03OUT\x10\x01*\"\n" + + "\n" + + "RuleAction\x12\n" + + "\n" + + "\x06ACCEPT\x10\x00\x12\b\n" + + "\x04DROP\x10\x01*\x96\x01\n" + + "\x11MachineUpdateType\x12\x17\n" + + "\x13MACHINE_UPDATE_FULL\x10\x00\x12\x19\n" + + "\x15MACHINE_UPDATE_ROUTES\x10\x01\x12\x16\n" + + "\x12MACHINE_UPDATE_DNS\x10\x02\x12\x18\n" + + "\x14MACHINE_UPDATE_PEERS\x10\x03\x12\x1b\n" + + "\x17MACHINE_UPDATE_FIREWALL\x10\x042\xc0\a\n" + + "\x11ManagementService\x12E\n" + + "\x05Login\x12\x1c.management.EncryptedMessage\x1a\x1c.management.EncryptedMessage\"\x00\x12F\n" + + "\x04Sync\x12\x1c.management.EncryptedMessage\x1a\x1c.management.EncryptedMessage\"\x000\x01\x12B\n" + + "\fGetServerKey\x12\x11.management.Empty\x1a\x1d.management.ServerKeyResponse\"\x00\x123\n" + + "\tisHealthy\x12\x11.management.Empty\x1a\x11.management.Empty\"\x00\x12Z\n" + + "\x1aGetDeviceAuthorizationFlow\x12\x1c.management.EncryptedMessage\x1a\x1c.management.EncryptedMessage\"\x00\x12X\n" + + "\x18GetPKCEAuthorizationFlow\x12\x1c.management.EncryptedMessage\x1a\x1c.management.EncryptedMessage\"\x00\x12=\n" + + "\bSyncMeta\x12\x1c.management.EncryptedMessage\x1a\x11.management.Empty\"\x00\x12;\n" + + "\x06Logout\x12\x1c.management.EncryptedMessage\x1a\x11.management.Empty\"\x00\x12`\n" + + "\x13RegisterMachinePeer\x12\".management.MachineRegisterRequest\x1a#.management.MachineRegisterResponse\"\x00\x12V\n" + + "\x0fSyncMachinePeer\x12\x1e.management.MachineSyncRequest\x1a\x1f.management.MachineSyncResponse\"\x000\x01\x12Y\n" + + "\x10GetMachineRoutes\x12 .management.MachineRoutesRequest\x1a!.management.MachineRoutesResponse\"\x00\x12\\\n" + + "\x13ReportMachineStatus\x12 .management.MachineStatusRequest\x1a!.management.MachineStatusResponse\"\x00B\bZ\x06/protob\x06proto3" + +var ( + file_management_proto_rawDescOnce sync.Once + file_management_proto_rawDescData []byte +) + +func file_management_proto_rawDescGZIP() []byte { + file_management_proto_rawDescOnce.Do(func() { + file_management_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_management_proto_rawDesc), len(file_management_proto_rawDesc))) + }) + return file_management_proto_rawDescData +} + +var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 6) +var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 54) +var file_management_proto_goTypes = []any{ + (RuleProtocol)(0), // 0: management.RuleProtocol + (RuleDirection)(0), // 1: management.RuleDirection + (RuleAction)(0), // 2: management.RuleAction + (MachineUpdateType)(0), // 3: management.MachineUpdateType + (HostConfig_Protocol)(0), // 4: management.HostConfig.Protocol + (DeviceAuthorizationFlowProvider)(0), // 5: management.DeviceAuthorizationFlow.provider + (*EncryptedMessage)(nil), // 6: management.EncryptedMessage + (*SyncRequest)(nil), // 7: management.SyncRequest + (*SyncResponse)(nil), // 8: management.SyncResponse + (*SyncMetaRequest)(nil), // 9: management.SyncMetaRequest + (*LoginRequest)(nil), // 10: management.LoginRequest + (*PeerKeys)(nil), // 11: management.PeerKeys + (*Environment)(nil), // 12: management.Environment + (*File)(nil), // 13: management.File + (*Flags)(nil), // 14: management.Flags + (*PeerSystemMeta)(nil), // 15: management.PeerSystemMeta + (*LoginResponse)(nil), // 16: management.LoginResponse + (*ServerKeyResponse)(nil), // 17: management.ServerKeyResponse + (*Empty)(nil), // 18: management.Empty + (*NetbirdConfig)(nil), // 19: management.NetbirdConfig + (*HostConfig)(nil), // 20: management.HostConfig + (*RelayConfig)(nil), // 21: management.RelayConfig + (*FlowConfig)(nil), // 22: management.FlowConfig + (*JWTConfig)(nil), // 23: management.JWTConfig + (*ProtectedHostConfig)(nil), // 24: management.ProtectedHostConfig + (*PeerConfig)(nil), // 25: management.PeerConfig + (*AutoUpdateSettings)(nil), // 26: management.AutoUpdateSettings + (*NetworkMap)(nil), // 27: management.NetworkMap + (*SSHAuth)(nil), // 28: management.SSHAuth + (*MachineUserIndexes)(nil), // 29: management.MachineUserIndexes + (*RemotePeerConfig)(nil), // 30: management.RemotePeerConfig + (*SSHConfig)(nil), // 31: management.SSHConfig + (*DeviceAuthorizationFlowRequest)(nil), // 32: management.DeviceAuthorizationFlowRequest + (*DeviceAuthorizationFlow)(nil), // 33: management.DeviceAuthorizationFlow + (*PKCEAuthorizationFlowRequest)(nil), // 34: management.PKCEAuthorizationFlowRequest + (*PKCEAuthorizationFlow)(nil), // 35: management.PKCEAuthorizationFlow + (*ProviderConfig)(nil), // 36: management.ProviderConfig + (*Route)(nil), // 37: management.Route + (*DNSConfig)(nil), // 38: management.DNSConfig + (*CustomZone)(nil), // 39: management.CustomZone + (*SimpleRecord)(nil), // 40: management.SimpleRecord + (*NameServerGroup)(nil), // 41: management.NameServerGroup + (*NameServer)(nil), // 42: management.NameServer + (*FirewallRule)(nil), // 43: management.FirewallRule + (*NetworkAddress)(nil), // 44: management.NetworkAddress + (*Checks)(nil), // 45: management.Checks + (*PortInfo)(nil), // 46: management.PortInfo + (*RouteFirewallRule)(nil), // 47: management.RouteFirewallRule + (*ForwardingRule)(nil), // 48: management.ForwardingRule + (*MachineIdentity)(nil), // 49: management.MachineIdentity + (*MachineRegisterRequest)(nil), // 50: management.MachineRegisterRequest + (*MachineRegisterResponse)(nil), // 51: management.MachineRegisterResponse + (*MachineSyncRequest)(nil), // 52: management.MachineSyncRequest + (*MachineSyncResponse)(nil), // 53: management.MachineSyncResponse + (*MachineRoutesRequest)(nil), // 54: management.MachineRoutesRequest + (*MachineRoutesResponse)(nil), // 55: management.MachineRoutesResponse + (*MachineStatusRequest)(nil), // 56: management.MachineStatusRequest + (*MachineStatusResponse)(nil), // 57: management.MachineStatusResponse + nil, // 58: management.SSHAuth.MachineUsersEntry + (*PortInfo_Range)(nil), // 59: management.PortInfo.Range + (*timestamppb.Timestamp)(nil), // 60: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 61: google.protobuf.Duration +} +var file_management_proto_depIdxs = []int32{ + 15, // 0: management.SyncRequest.meta:type_name -> management.PeerSystemMeta + 19, // 1: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig + 25, // 2: management.SyncResponse.peerConfig:type_name -> management.PeerConfig + 30, // 3: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig + 27, // 4: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap + 45, // 5: management.SyncResponse.Checks:type_name -> management.Checks + 15, // 6: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta + 15, // 7: management.LoginRequest.meta:type_name -> management.PeerSystemMeta + 11, // 8: management.LoginRequest.peerKeys:type_name -> management.PeerKeys + 44, // 9: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress + 12, // 10: management.PeerSystemMeta.environment:type_name -> management.Environment + 13, // 11: management.PeerSystemMeta.files:type_name -> management.File + 14, // 12: management.PeerSystemMeta.flags:type_name -> management.Flags + 19, // 13: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig + 25, // 14: management.LoginResponse.peerConfig:type_name -> management.PeerConfig + 45, // 15: management.LoginResponse.Checks:type_name -> management.Checks + 60, // 16: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp + 20, // 17: management.NetbirdConfig.stuns:type_name -> management.HostConfig + 24, // 18: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig + 20, // 19: management.NetbirdConfig.signal:type_name -> management.HostConfig + 21, // 20: management.NetbirdConfig.relay:type_name -> management.RelayConfig + 22, // 21: management.NetbirdConfig.flow:type_name -> management.FlowConfig + 4, // 22: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol + 61, // 23: management.FlowConfig.interval:type_name -> google.protobuf.Duration + 20, // 24: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig + 31, // 25: management.PeerConfig.sshConfig:type_name -> management.SSHConfig + 26, // 26: management.PeerConfig.autoUpdate:type_name -> management.AutoUpdateSettings + 25, // 27: management.NetworkMap.peerConfig:type_name -> management.PeerConfig + 30, // 28: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig + 37, // 29: management.NetworkMap.Routes:type_name -> management.Route + 38, // 30: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig + 30, // 31: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig + 43, // 32: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule + 47, // 33: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule + 48, // 34: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule + 28, // 35: management.NetworkMap.sshAuth:type_name -> management.SSHAuth + 58, // 36: management.SSHAuth.machine_users:type_name -> management.SSHAuth.MachineUsersEntry + 31, // 37: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig + 23, // 38: management.SSHConfig.jwtConfig:type_name -> management.JWTConfig + 5, // 39: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider + 36, // 40: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 36, // 41: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 41, // 42: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup + 39, // 43: management.DNSConfig.CustomZones:type_name -> management.CustomZone + 40, // 44: management.CustomZone.Records:type_name -> management.SimpleRecord + 42, // 45: management.NameServerGroup.NameServers:type_name -> management.NameServer + 1, // 46: management.FirewallRule.Direction:type_name -> management.RuleDirection + 2, // 47: management.FirewallRule.Action:type_name -> management.RuleAction + 0, // 48: management.FirewallRule.Protocol:type_name -> management.RuleProtocol + 46, // 49: management.FirewallRule.PortInfo:type_name -> management.PortInfo + 59, // 50: management.PortInfo.range:type_name -> management.PortInfo.Range + 2, // 51: management.RouteFirewallRule.action:type_name -> management.RuleAction + 0, // 52: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol + 46, // 53: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo + 0, // 54: management.ForwardingRule.protocol:type_name -> management.RuleProtocol + 46, // 55: management.ForwardingRule.destinationPort:type_name -> management.PortInfo + 46, // 56: management.ForwardingRule.translatedPort:type_name -> management.PortInfo + 15, // 57: management.MachineRegisterRequest.meta:type_name -> management.PeerSystemMeta + 25, // 58: management.MachineRegisterResponse.peer_config:type_name -> management.PeerConfig + 19, // 59: management.MachineRegisterResponse.netbird_config:type_name -> management.NetbirdConfig + 49, // 60: management.MachineRegisterResponse.machine_identity:type_name -> management.MachineIdentity + 37, // 61: management.MachineRegisterResponse.allowed_dc_routes:type_name -> management.Route + 38, // 62: management.MachineRegisterResponse.dns_config:type_name -> management.DNSConfig + 15, // 63: management.MachineSyncRequest.meta:type_name -> management.PeerSystemMeta + 27, // 64: management.MachineSyncResponse.network_map:type_name -> management.NetworkMap + 3, // 65: management.MachineSyncResponse.update_type:type_name -> management.MachineUpdateType + 37, // 66: management.MachineRoutesResponse.routes:type_name -> management.Route + 30, // 67: management.MachineRoutesResponse.router_peers:type_name -> management.RemotePeerConfig + 60, // 68: management.MachineStatusRequest.last_handshake:type_name -> google.protobuf.Timestamp + 60, // 69: management.MachineStatusResponse.server_time:type_name -> google.protobuf.Timestamp + 29, // 70: management.SSHAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes + 6, // 71: management.ManagementService.Login:input_type -> management.EncryptedMessage + 6, // 72: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 18, // 73: management.ManagementService.GetServerKey:input_type -> management.Empty + 18, // 74: management.ManagementService.isHealthy:input_type -> management.Empty + 6, // 75: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 6, // 76: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage + 6, // 77: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage + 6, // 78: management.ManagementService.Logout:input_type -> management.EncryptedMessage + 50, // 79: management.ManagementService.RegisterMachinePeer:input_type -> management.MachineRegisterRequest + 52, // 80: management.ManagementService.SyncMachinePeer:input_type -> management.MachineSyncRequest + 54, // 81: management.ManagementService.GetMachineRoutes:input_type -> management.MachineRoutesRequest + 56, // 82: management.ManagementService.ReportMachineStatus:input_type -> management.MachineStatusRequest + 6, // 83: management.ManagementService.Login:output_type -> management.EncryptedMessage + 6, // 84: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 17, // 85: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 18, // 86: management.ManagementService.isHealthy:output_type -> management.Empty + 6, // 87: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 6, // 88: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage + 18, // 89: management.ManagementService.SyncMeta:output_type -> management.Empty + 18, // 90: management.ManagementService.Logout:output_type -> management.Empty + 51, // 91: management.ManagementService.RegisterMachinePeer:output_type -> management.MachineRegisterResponse + 53, // 92: management.ManagementService.SyncMachinePeer:output_type -> management.MachineSyncResponse + 55, // 93: management.ManagementService.GetMachineRoutes:output_type -> management.MachineRoutesResponse + 57, // 94: management.ManagementService.ReportMachineStatus:output_type -> management.MachineStatusResponse + 83, // [83:95] is the sub-list for method output_type + 71, // [71:83] is the sub-list for method input_type + 71, // [71:71] is the sub-list for extension type_name + 71, // [71:71] is the sub-list for extension extendee + 0, // [0:71] is the sub-list for field type_name +} + +func init() { file_management_proto_init() } +func file_management_proto_init() { + if File_management_proto != nil { + return } - file_management_proto_msgTypes[40].OneofWrappers = []interface{}{ + file_management_proto_msgTypes[40].OneofWrappers = []any{ (*PortInfo_Port)(nil), (*PortInfo_Range_)(nil), } @@ -4993,9 +4819,9 @@ func file_management_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_management_proto_rawDesc, - NumEnums: 5, - NumMessages: 45, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_management_proto_rawDesc), len(file_management_proto_rawDesc)), + NumEnums: 6, + NumMessages: 54, NumExtensions: 0, NumServices: 1, }, @@ -5005,7 +4831,6 @@ func file_management_proto_init() { MessageInfos: file_management_proto_msgTypes, }.Build() File_management_proto = out.File - file_management_proto_rawDesc = nil file_management_proto_goTypes = nil file_management_proto_depIdxs = nil } diff --git a/shared/management/proto/management.proto b/shared/management/proto/management.proto index e44b49781e1..511206c7f8e 100644 --- a/shared/management/proto/management.proto +++ b/shared/management/proto/management.proto @@ -48,6 +48,30 @@ service ManagementService { // Logout logs out the peer and removes it from the management server rpc Logout(EncryptedMessage) returns (Empty) {} + + // ========================================== + // Machine Tunnel Fork - mTLS-authenticated RPCs + // These methods require client certificate authentication (AD CS machine certs). + // Unlike standard RPCs, these do NOT use EncryptedMessage because + // authentication is handled at the TLS layer via mTLS. + // ========================================== + + // RegisterMachinePeer registers a machine peer using mTLS certificate authentication. + // The machine identity is extracted from the client certificate SAN DNSName. + // Requires: Valid machine certificate with SAN DNSName = "{hostname}.{domain}" + rpc RegisterMachinePeer(MachineRegisterRequest) returns (MachineRegisterResponse) {} + + // SyncMachinePeer enables machine peer synchronization via mTLS. + // Similar to Sync but for machine tunnel context with certificate auth. + rpc SyncMachinePeer(MachineSyncRequest) returns (stream MachineSyncResponse) {} + + // GetMachineRoutes retrieves routes configured for this machine peer. + // Used to get DC-specific routes for pre-login tunnel. + rpc GetMachineRoutes(MachineRoutesRequest) returns (MachineRoutesResponse) {} + + // ReportMachineStatus reports machine tunnel health and status. + // Used for monitoring and troubleshooting machine tunnels. + rpc ReportMachineStatus(MachineStatusRequest) returns (MachineStatusResponse) {} } message EncryptedMessage { @@ -598,3 +622,137 @@ message ForwardingRule { // Translated port information, where the traffic should be forwarded to PortInfo translatedPort = 4; } + +// ========================================== +// Machine Tunnel Fork - Message Definitions +// ========================================== + +// MachineIdentity contains identity information extracted from the machine certificate. +// This is populated by the server from the mTLS client certificate, not sent by client. +message MachineIdentity { + // dns_name is the full SAN DNSName from the certificate (e.g., "win10-pc.corp.local") + string dns_name = 1; + + // hostname is extracted from dns_name (e.g., "win10-pc") + string hostname = 2; + + // domain is extracted from dns_name (e.g., "corp.local") + string domain = 3; + + // issuer_fingerprint is SHA256 of the issuing CA certificate (from VerifiedChains) + string issuer_fingerprint = 4; + + // serial_number of the client certificate + string serial_number = 5; + + // template_oid if present in certificate extensions (AD CS template) + string template_oid = 6; +} + +// MachineRegisterRequest is sent when a machine peer registers with mTLS. +message MachineRegisterRequest { + // meta contains machine metadata (OS, version, etc.) + PeerSystemMeta meta = 1; + + // wg_pub_key is the WireGuard public key for this machine tunnel + bytes wg_pub_key = 2; + + // requested_ip is an optional requested VPN IP (may be ignored by server) + string requested_ip = 3; +} + +// MachineRegisterResponse contains the configuration for the registered machine peer. +message MachineRegisterResponse { + // peer_config is the local peer configuration + PeerConfig peer_config = 1; + + // netbird_config contains STUN/TURN/Relay configuration + NetbirdConfig netbird_config = 2; + + // machine_identity is the identity extracted from the certificate (for client verification) + MachineIdentity machine_identity = 3; + + // allowed_dc_routes are the DC routes this machine is allowed to access + repeated Route allowed_dc_routes = 4; + + // dns_config for DC DNS resolution + DNSConfig dns_config = 5; +} + +// MachineSyncRequest is sent to initiate machine peer synchronization. +message MachineSyncRequest { + // meta contains current machine metadata + PeerSystemMeta meta = 1; +} + +// MachineSyncResponse contains updates for the machine peer. +message MachineSyncResponse { + // network_map contains the current network state + NetworkMap network_map = 1; + + // update_type indicates what changed + MachineUpdateType update_type = 2; + + // serial is the network state serial number + uint64 serial = 3; +} + +// MachineUpdateType indicates what triggered the sync update. +enum MachineUpdateType { + MACHINE_UPDATE_FULL = 0; // Full sync (initial or reconnect) + MACHINE_UPDATE_ROUTES = 1; // Route changes + MACHINE_UPDATE_DNS = 2; // DNS config changes + MACHINE_UPDATE_PEERS = 3; // Peer changes (router-peers) + MACHINE_UPDATE_FIREWALL = 4; // Firewall rule changes +} + +// MachineRoutesRequest requests routes for the machine peer. +message MachineRoutesRequest { + // include_offline if true, includes routes via offline router-peers + bool include_offline = 1; +} + +// MachineRoutesResponse contains routes accessible by this machine peer. +message MachineRoutesResponse { + // routes are the currently active routes + repeated Route routes = 1; + + // dc_networks are the Domain Controller network CIDRs + repeated string dc_networks = 2; + + // router_peers are the peers that route DC traffic + repeated RemotePeerConfig router_peers = 3; +} + +// MachineStatusRequest reports machine tunnel status. +message MachineStatusRequest { + // tunnel_up indicates if the WireGuard tunnel is established + bool tunnel_up = 1; + + // connected_router_peer is the currently connected router-peer (if any) + string connected_router_peer = 2; + + // last_handshake is the timestamp of last WireGuard handshake + google.protobuf.Timestamp last_handshake = 3; + + // dc_reachable indicates if DC connectivity test succeeded + bool dc_reachable = 4; + + // errors contains any error messages + repeated string errors = 5; + + // uptime_seconds is how long the tunnel has been up + int64 uptime_seconds = 6; +} + +// MachineStatusResponse acknowledges the status report. +message MachineStatusResponse { + // ack indicates the status was received + bool ack = 1; + + // server_time is the current server time (for clock sync verification) + google.protobuf.Timestamp server_time = 2; + + // config_serial is the current config serial (client can compare) + uint64 config_serial = 3; +} diff --git a/shared/management/proto/management_grpc.pb.go b/shared/management/proto/management_grpc.pb.go index 5b189334d4a..4abccc676d5 100644 --- a/shared/management/proto/management_grpc.pb.go +++ b/shared/management/proto/management_grpc.pb.go @@ -1,4 +1,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc v6.33.3 +// source: management.proto package proto @@ -11,8 +15,23 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ManagementService_Login_FullMethodName = "/management.ManagementService/Login" + ManagementService_Sync_FullMethodName = "/management.ManagementService/Sync" + ManagementService_GetServerKey_FullMethodName = "/management.ManagementService/GetServerKey" + ManagementService_IsHealthy_FullMethodName = "/management.ManagementService/isHealthy" + ManagementService_GetDeviceAuthorizationFlow_FullMethodName = "/management.ManagementService/GetDeviceAuthorizationFlow" + ManagementService_GetPKCEAuthorizationFlow_FullMethodName = "/management.ManagementService/GetPKCEAuthorizationFlow" + ManagementService_SyncMeta_FullMethodName = "/management.ManagementService/SyncMeta" + ManagementService_Logout_FullMethodName = "/management.ManagementService/Logout" + ManagementService_RegisterMachinePeer_FullMethodName = "/management.ManagementService/RegisterMachinePeer" + ManagementService_SyncMachinePeer_FullMethodName = "/management.ManagementService/SyncMachinePeer" + ManagementService_GetMachineRoutes_FullMethodName = "/management.ManagementService/GetMachineRoutes" + ManagementService_ReportMachineStatus_FullMethodName = "/management.ManagementService/ReportMachineStatus" +) // ManagementServiceClient is the client API for ManagementService service. // @@ -25,7 +44,7 @@ type ManagementServiceClient interface { // For example, if a new peer has been added to an account all other connected peers will receive this peer's Wireguard public key as an update // The initial SyncResponse contains all of the available peers so the local state can be refreshed // Returns encrypted SyncResponse in EncryptedMessage.Body - Sync(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (ManagementService_SyncClient, error) + Sync(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (grpc.ServerStreamingClient[EncryptedMessage], error) // Exposes a Wireguard public key of the Management service. // This key is used to support message encryption between client and server GetServerKey(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*ServerKeyResponse, error) @@ -50,6 +69,19 @@ type ManagementServiceClient interface { SyncMeta(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*Empty, error) // Logout logs out the peer and removes it from the management server Logout(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*Empty, error) + // RegisterMachinePeer registers a machine peer using mTLS certificate authentication. + // The machine identity is extracted from the client certificate SAN DNSName. + // Requires: Valid machine certificate with SAN DNSName = "{hostname}.{domain}" + RegisterMachinePeer(ctx context.Context, in *MachineRegisterRequest, opts ...grpc.CallOption) (*MachineRegisterResponse, error) + // SyncMachinePeer enables machine peer synchronization via mTLS. + // Similar to Sync but for machine tunnel context with certificate auth. + SyncMachinePeer(ctx context.Context, in *MachineSyncRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[MachineSyncResponse], error) + // GetMachineRoutes retrieves routes configured for this machine peer. + // Used to get DC-specific routes for pre-login tunnel. + GetMachineRoutes(ctx context.Context, in *MachineRoutesRequest, opts ...grpc.CallOption) (*MachineRoutesResponse, error) + // ReportMachineStatus reports machine tunnel health and status. + // Used for monitoring and troubleshooting machine tunnels. + ReportMachineStatus(ctx context.Context, in *MachineStatusRequest, opts ...grpc.CallOption) (*MachineStatusResponse, error) } type managementServiceClient struct { @@ -61,20 +93,22 @@ func NewManagementServiceClient(cc grpc.ClientConnInterface) ManagementServiceCl } func (c *managementServiceClient) Login(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(EncryptedMessage) - err := c.cc.Invoke(ctx, "/management.ManagementService/Login", in, out, opts...) + err := c.cc.Invoke(ctx, ManagementService_Login_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *managementServiceClient) Sync(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (ManagementService_SyncClient, error) { - stream, err := c.cc.NewStream(ctx, &ManagementService_ServiceDesc.Streams[0], "/management.ManagementService/Sync", opts...) +func (c *managementServiceClient) Sync(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (grpc.ServerStreamingClient[EncryptedMessage], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &ManagementService_ServiceDesc.Streams[0], ManagementService_Sync_FullMethodName, cOpts...) if err != nil { return nil, err } - x := &managementServiceSyncClient{stream} + x := &grpc.GenericClientStream[EncryptedMessage, EncryptedMessage]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } @@ -84,26 +118,13 @@ func (c *managementServiceClient) Sync(ctx context.Context, in *EncryptedMessage return x, nil } -type ManagementService_SyncClient interface { - Recv() (*EncryptedMessage, error) - grpc.ClientStream -} - -type managementServiceSyncClient struct { - grpc.ClientStream -} - -func (x *managementServiceSyncClient) Recv() (*EncryptedMessage, error) { - m := new(EncryptedMessage) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ManagementService_SyncClient = grpc.ServerStreamingClient[EncryptedMessage] func (c *managementServiceClient) GetServerKey(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*ServerKeyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ServerKeyResponse) - err := c.cc.Invoke(ctx, "/management.ManagementService/GetServerKey", in, out, opts...) + err := c.cc.Invoke(ctx, ManagementService_GetServerKey_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -111,8 +132,9 @@ func (c *managementServiceClient) GetServerKey(ctx context.Context, in *Empty, o } func (c *managementServiceClient) IsHealthy(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Empty) - err := c.cc.Invoke(ctx, "/management.ManagementService/isHealthy", in, out, opts...) + err := c.cc.Invoke(ctx, ManagementService_IsHealthy_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -120,8 +142,9 @@ func (c *managementServiceClient) IsHealthy(ctx context.Context, in *Empty, opts } func (c *managementServiceClient) GetDeviceAuthorizationFlow(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(EncryptedMessage) - err := c.cc.Invoke(ctx, "/management.ManagementService/GetDeviceAuthorizationFlow", in, out, opts...) + err := c.cc.Invoke(ctx, ManagementService_GetDeviceAuthorizationFlow_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -129,8 +152,9 @@ func (c *managementServiceClient) GetDeviceAuthorizationFlow(ctx context.Context } func (c *managementServiceClient) GetPKCEAuthorizationFlow(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(EncryptedMessage) - err := c.cc.Invoke(ctx, "/management.ManagementService/GetPKCEAuthorizationFlow", in, out, opts...) + err := c.cc.Invoke(ctx, ManagementService_GetPKCEAuthorizationFlow_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -138,8 +162,9 @@ func (c *managementServiceClient) GetPKCEAuthorizationFlow(ctx context.Context, } func (c *managementServiceClient) SyncMeta(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Empty) - err := c.cc.Invoke(ctx, "/management.ManagementService/SyncMeta", in, out, opts...) + err := c.cc.Invoke(ctx, ManagementService_SyncMeta_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -147,8 +172,58 @@ func (c *managementServiceClient) SyncMeta(ctx context.Context, in *EncryptedMes } func (c *managementServiceClient) Logout(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Empty) - err := c.cc.Invoke(ctx, "/management.ManagementService/Logout", in, out, opts...) + err := c.cc.Invoke(ctx, ManagementService_Logout_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *managementServiceClient) RegisterMachinePeer(ctx context.Context, in *MachineRegisterRequest, opts ...grpc.CallOption) (*MachineRegisterResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MachineRegisterResponse) + err := c.cc.Invoke(ctx, ManagementService_RegisterMachinePeer_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *managementServiceClient) SyncMachinePeer(ctx context.Context, in *MachineSyncRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[MachineSyncResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &ManagementService_ServiceDesc.Streams[1], ManagementService_SyncMachinePeer_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[MachineSyncRequest, MachineSyncResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ManagementService_SyncMachinePeerClient = grpc.ServerStreamingClient[MachineSyncResponse] + +func (c *managementServiceClient) GetMachineRoutes(ctx context.Context, in *MachineRoutesRequest, opts ...grpc.CallOption) (*MachineRoutesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MachineRoutesResponse) + err := c.cc.Invoke(ctx, ManagementService_GetMachineRoutes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *managementServiceClient) ReportMachineStatus(ctx context.Context, in *MachineStatusRequest, opts ...grpc.CallOption) (*MachineStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MachineStatusResponse) + err := c.cc.Invoke(ctx, ManagementService_ReportMachineStatus_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -157,7 +232,7 @@ func (c *managementServiceClient) Logout(ctx context.Context, in *EncryptedMessa // ManagementServiceServer is the server API for ManagementService service. // All implementations must embed UnimplementedManagementServiceServer -// for forward compatibility +// for forward compatibility. type ManagementServiceServer interface { // Login logs in peer. In case server returns codes.PermissionDenied this endpoint can be used to register Peer providing LoginRequest.setupKey // Returns encrypted LoginResponse in EncryptedMessage.Body @@ -166,7 +241,7 @@ type ManagementServiceServer interface { // For example, if a new peer has been added to an account all other connected peers will receive this peer's Wireguard public key as an update // The initial SyncResponse contains all of the available peers so the local state can be refreshed // Returns encrypted SyncResponse in EncryptedMessage.Body - Sync(*EncryptedMessage, ManagementService_SyncServer) error + Sync(*EncryptedMessage, grpc.ServerStreamingServer[EncryptedMessage]) error // Exposes a Wireguard public key of the Management service. // This key is used to support message encryption between client and server GetServerKey(context.Context, *Empty) (*ServerKeyResponse, error) @@ -191,38 +266,67 @@ type ManagementServiceServer interface { SyncMeta(context.Context, *EncryptedMessage) (*Empty, error) // Logout logs out the peer and removes it from the management server Logout(context.Context, *EncryptedMessage) (*Empty, error) + // RegisterMachinePeer registers a machine peer using mTLS certificate authentication. + // The machine identity is extracted from the client certificate SAN DNSName. + // Requires: Valid machine certificate with SAN DNSName = "{hostname}.{domain}" + RegisterMachinePeer(context.Context, *MachineRegisterRequest) (*MachineRegisterResponse, error) + // SyncMachinePeer enables machine peer synchronization via mTLS. + // Similar to Sync but for machine tunnel context with certificate auth. + SyncMachinePeer(*MachineSyncRequest, grpc.ServerStreamingServer[MachineSyncResponse]) error + // GetMachineRoutes retrieves routes configured for this machine peer. + // Used to get DC-specific routes for pre-login tunnel. + GetMachineRoutes(context.Context, *MachineRoutesRequest) (*MachineRoutesResponse, error) + // ReportMachineStatus reports machine tunnel health and status. + // Used for monitoring and troubleshooting machine tunnels. + ReportMachineStatus(context.Context, *MachineStatusRequest) (*MachineStatusResponse, error) mustEmbedUnimplementedManagementServiceServer() } -// UnimplementedManagementServiceServer must be embedded to have forward compatible implementations. -type UnimplementedManagementServiceServer struct { -} +// UnimplementedManagementServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedManagementServiceServer struct{} func (UnimplementedManagementServiceServer) Login(context.Context, *EncryptedMessage) (*EncryptedMessage, error) { - return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") + return nil, status.Error(codes.Unimplemented, "method Login not implemented") } -func (UnimplementedManagementServiceServer) Sync(*EncryptedMessage, ManagementService_SyncServer) error { - return status.Errorf(codes.Unimplemented, "method Sync not implemented") +func (UnimplementedManagementServiceServer) Sync(*EncryptedMessage, grpc.ServerStreamingServer[EncryptedMessage]) error { + return status.Error(codes.Unimplemented, "method Sync not implemented") } func (UnimplementedManagementServiceServer) GetServerKey(context.Context, *Empty) (*ServerKeyResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetServerKey not implemented") + return nil, status.Error(codes.Unimplemented, "method GetServerKey not implemented") } func (UnimplementedManagementServiceServer) IsHealthy(context.Context, *Empty) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method IsHealthy not implemented") + return nil, status.Error(codes.Unimplemented, "method IsHealthy not implemented") } func (UnimplementedManagementServiceServer) GetDeviceAuthorizationFlow(context.Context, *EncryptedMessage) (*EncryptedMessage, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetDeviceAuthorizationFlow not implemented") + return nil, status.Error(codes.Unimplemented, "method GetDeviceAuthorizationFlow not implemented") } func (UnimplementedManagementServiceServer) GetPKCEAuthorizationFlow(context.Context, *EncryptedMessage) (*EncryptedMessage, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetPKCEAuthorizationFlow not implemented") + return nil, status.Error(codes.Unimplemented, "method GetPKCEAuthorizationFlow not implemented") } func (UnimplementedManagementServiceServer) SyncMeta(context.Context, *EncryptedMessage) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method SyncMeta not implemented") + return nil, status.Error(codes.Unimplemented, "method SyncMeta not implemented") } func (UnimplementedManagementServiceServer) Logout(context.Context, *EncryptedMessage) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented") + return nil, status.Error(codes.Unimplemented, "method Logout not implemented") +} +func (UnimplementedManagementServiceServer) RegisterMachinePeer(context.Context, *MachineRegisterRequest) (*MachineRegisterResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RegisterMachinePeer not implemented") +} +func (UnimplementedManagementServiceServer) SyncMachinePeer(*MachineSyncRequest, grpc.ServerStreamingServer[MachineSyncResponse]) error { + return status.Error(codes.Unimplemented, "method SyncMachinePeer not implemented") +} +func (UnimplementedManagementServiceServer) GetMachineRoutes(context.Context, *MachineRoutesRequest) (*MachineRoutesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetMachineRoutes not implemented") +} +func (UnimplementedManagementServiceServer) ReportMachineStatus(context.Context, *MachineStatusRequest) (*MachineStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportMachineStatus not implemented") } func (UnimplementedManagementServiceServer) mustEmbedUnimplementedManagementServiceServer() {} +func (UnimplementedManagementServiceServer) testEmbeddedByValue() {} // UnsafeManagementServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to ManagementServiceServer will @@ -232,6 +336,13 @@ type UnsafeManagementServiceServer interface { } func RegisterManagementServiceServer(s grpc.ServiceRegistrar, srv ManagementServiceServer) { + // If the following call panics, it indicates UnimplementedManagementServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&ManagementService_ServiceDesc, srv) } @@ -245,7 +356,7 @@ func _ManagementService_Login_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/management.ManagementService/Login", + FullMethod: ManagementService_Login_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ManagementServiceServer).Login(ctx, req.(*EncryptedMessage)) @@ -258,21 +369,11 @@ func _ManagementService_Sync_Handler(srv interface{}, stream grpc.ServerStream) if err := stream.RecvMsg(m); err != nil { return err } - return srv.(ManagementServiceServer).Sync(m, &managementServiceSyncServer{stream}) + return srv.(ManagementServiceServer).Sync(m, &grpc.GenericServerStream[EncryptedMessage, EncryptedMessage]{ServerStream: stream}) } -type ManagementService_SyncServer interface { - Send(*EncryptedMessage) error - grpc.ServerStream -} - -type managementServiceSyncServer struct { - grpc.ServerStream -} - -func (x *managementServiceSyncServer) Send(m *EncryptedMessage) error { - return x.ServerStream.SendMsg(m) -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ManagementService_SyncServer = grpc.ServerStreamingServer[EncryptedMessage] func _ManagementService_GetServerKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Empty) @@ -284,7 +385,7 @@ func _ManagementService_GetServerKey_Handler(srv interface{}, ctx context.Contex } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/management.ManagementService/GetServerKey", + FullMethod: ManagementService_GetServerKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ManagementServiceServer).GetServerKey(ctx, req.(*Empty)) @@ -302,7 +403,7 @@ func _ManagementService_IsHealthy_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/management.ManagementService/isHealthy", + FullMethod: ManagementService_IsHealthy_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ManagementServiceServer).IsHealthy(ctx, req.(*Empty)) @@ -320,7 +421,7 @@ func _ManagementService_GetDeviceAuthorizationFlow_Handler(srv interface{}, ctx } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/management.ManagementService/GetDeviceAuthorizationFlow", + FullMethod: ManagementService_GetDeviceAuthorizationFlow_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ManagementServiceServer).GetDeviceAuthorizationFlow(ctx, req.(*EncryptedMessage)) @@ -338,7 +439,7 @@ func _ManagementService_GetPKCEAuthorizationFlow_Handler(srv interface{}, ctx co } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/management.ManagementService/GetPKCEAuthorizationFlow", + FullMethod: ManagementService_GetPKCEAuthorizationFlow_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ManagementServiceServer).GetPKCEAuthorizationFlow(ctx, req.(*EncryptedMessage)) @@ -356,7 +457,7 @@ func _ManagementService_SyncMeta_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/management.ManagementService/SyncMeta", + FullMethod: ManagementService_SyncMeta_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ManagementServiceServer).SyncMeta(ctx, req.(*EncryptedMessage)) @@ -374,7 +475,7 @@ func _ManagementService_Logout_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/management.ManagementService/Logout", + FullMethod: ManagementService_Logout_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ManagementServiceServer).Logout(ctx, req.(*EncryptedMessage)) @@ -382,6 +483,71 @@ func _ManagementService_Logout_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _ManagementService_RegisterMachinePeer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MachineRegisterRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ManagementServiceServer).RegisterMachinePeer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ManagementService_RegisterMachinePeer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ManagementServiceServer).RegisterMachinePeer(ctx, req.(*MachineRegisterRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ManagementService_SyncMachinePeer_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(MachineSyncRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(ManagementServiceServer).SyncMachinePeer(m, &grpc.GenericServerStream[MachineSyncRequest, MachineSyncResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ManagementService_SyncMachinePeerServer = grpc.ServerStreamingServer[MachineSyncResponse] + +func _ManagementService_GetMachineRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MachineRoutesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ManagementServiceServer).GetMachineRoutes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ManagementService_GetMachineRoutes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ManagementServiceServer).GetMachineRoutes(ctx, req.(*MachineRoutesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ManagementService_ReportMachineStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MachineStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ManagementServiceServer).ReportMachineStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ManagementService_ReportMachineStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ManagementServiceServer).ReportMachineStatus(ctx, req.(*MachineStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ManagementService_ServiceDesc is the grpc.ServiceDesc for ManagementService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -417,6 +583,18 @@ var ManagementService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Logout", Handler: _ManagementService_Logout_Handler, }, + { + MethodName: "RegisterMachinePeer", + Handler: _ManagementService_RegisterMachinePeer_Handler, + }, + { + MethodName: "GetMachineRoutes", + Handler: _ManagementService_GetMachineRoutes_Handler, + }, + { + MethodName: "ReportMachineStatus", + Handler: _ManagementService_ReportMachineStatus_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -424,6 +602,11 @@ var ManagementService_ServiceDesc = grpc.ServiceDesc{ Handler: _ManagementService_Sync_Handler, ServerStreams: true, }, + { + StreamName: "SyncMachinePeer", + Handler: _ManagementService_SyncMachinePeer_Handler, + ServerStreams: true, + }, }, Metadata: "management.proto", } diff --git a/spike/cng-signer/go.mod b/spike/cng-signer/go.mod new file mode 100644 index 00000000000..e3804eed132 --- /dev/null +++ b/spike/cng-signer/go.mod @@ -0,0 +1,5 @@ +module github.com/obtFusi/netbird-fork/spike/cng-signer + +go 1.22 + +require golang.org/x/sys v0.28.0 diff --git a/spike/cng-signer/go.sum b/spike/cng-signer/go.sum new file mode 100644 index 00000000000..e35a60da425 --- /dev/null +++ b/spike/cng-signer/go.sum @@ -0,0 +1,17 @@ +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/certtostore v1.0.3 h1:LD3lNKt4wU3V50qo6wXmr/6ER+KX+rmK30ueyctaZM8= +github.com/google/certtostore v1.0.3/go.mod h1:6YomPQrPy09/fTFbjJt+x1RqMADyc1ceU91z19Zk3No= +github.com/google/deck v0.0.0-20230104221208-105ad94aa8ae h1:Iy1Ad7L9qPtNAFJad+Ch2kwDXrcwu7QUBR0bfChjnEM= +github.com/google/deck v0.0.0-20230104221208-105ad94aa8ae/go.mod h1:DoDv8G58DuLNZF0KysYn0bA/6ZWhmRW3fZE2VnGEH0w= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/spike/cng-signer/main.go b/spike/cng-signer/main.go new file mode 100644 index 00000000000..51fd3a33b73 --- /dev/null +++ b/spike/cng-signer/main.go @@ -0,0 +1,423 @@ +//go:build windows + +// CNG crypto.Signer Spike - T-1.1 +// Tests whether Go can use non-exportable Windows Cert Store certificates +// for TLS authentication via the crypto.Signer interface. +// +// Based on: https://victoronsoftware.com/posts/mtls-go-client-windows-certificate-store/ +// Source: https://github.com/getvictor/mtls/tree/master/mtls-go-windows +package main + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "log" + "os" + "runtime" + "strings" + "time" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + // Windows NCrypt flags + nCryptSilentFlag = 0x00000040 // ncrypt.h NCRYPT_SILENT_FLAG + bCryptPadPkcs1 = 0x00000002 // bcrypt.h BCRYPT_PAD_PKCS1 + bCryptPadPss = 0x00000008 // bcrypt.h BCRYPT_PAD_PSS +) + +var ( + nCrypt = windows.MustLoadDLL("ncrypt.dll") + nCryptSignHash = nCrypt.MustFindProc("NCryptSignHash") +) + +// CNGSigner implements crypto.Signer using Windows CNG +type CNGSigner struct { + store windows.Handle + windowsCertContext *windows.CertContext + x509Cert *x509.Certificate +} + +func (k *CNGSigner) Public() crypto.PublicKey { + return k.x509Cert.PublicKey +} + +func (k *CNGSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + // Get private key from Windows cert store + var ( + privateKey windows.Handle + pdwKeySpec uint32 + pfCallerFreeProvOrNCryptKey bool + ) + err = windows.CryptAcquireCertificatePrivateKey( + k.windowsCertContext, + windows.CRYPT_ACQUIRE_CACHE_FLAG|windows.CRYPT_ACQUIRE_SILENT_FLAG|windows.CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG, + nil, + &privateKey, + &pdwKeySpec, + &pfCallerFreeProvOrNCryptKey, + ) + if err != nil { + return nil, fmt.Errorf("CryptAcquireCertificatePrivateKey: %w", err) + } + + // Determine padding based on SignerOpts + var flags uint32 = nCryptSilentFlag + var pPaddingInfo unsafe.Pointer + + switch opts := opts.(type) { + case *rsa.PSSOptions: + // RSA-PSS padding + flags |= bCryptPadPss + pPaddingInfo, err = getRsaPssPadding(opts) + if err != nil { + return nil, err + } + default: + // PKCS#1 v1.5 padding (default for most certificates) + flags |= bCryptPadPkcs1 + pPaddingInfo, err = getPkcs1Padding(opts.HashFunc()) + if err != nil { + return nil, err + } + } + + // Sign the digest - first call gets signature size + var size uint32 + success, _, _ := nCryptSignHash.Call( + uintptr(privateKey), + uintptr(pPaddingInfo), + uintptr(unsafe.Pointer(&digest[0])), + uintptr(len(digest)), + uintptr(0), + uintptr(0), + uintptr(unsafe.Pointer(&size)), + uintptr(flags), + ) + if success != 0 { + return nil, fmt.Errorf("NCryptSignHash: failed to get signature length: %#x", success) + } + + // Second call generates the signature + signature = make([]byte, size) + success, _, _ = nCryptSignHash.Call( + uintptr(privateKey), + uintptr(pPaddingInfo), + uintptr(unsafe.Pointer(&digest[0])), + uintptr(len(digest)), + uintptr(unsafe.Pointer(&signature[0])), + uintptr(size), + uintptr(unsafe.Pointer(&size)), + uintptr(flags), + ) + if success != 0 { + return nil, fmt.Errorf("NCryptSignHash: failed to generate signature: %#x", success) + } + return signature, nil +} + +func getRsaPssPadding(opts *rsa.PSSOptions) (unsafe.Pointer, error) { + algName, err := hashToAlgName(opts.Hash) + if err != nil { + return nil, err + } + algPtr, _ := windows.UTF16PtrFromString(algName) + // BCRYPT_PSS_PADDING_INFO structure + return unsafe.Pointer( + &struct { + pszAlgId *uint16 + cbSalt uint32 + }{ + pszAlgId: algPtr, + cbSalt: uint32(opts.HashFunc().Size()), + }, + ), nil +} + +func getPkcs1Padding(hash crypto.Hash) (unsafe.Pointer, error) { + algName, err := hashToAlgName(hash) + if err != nil { + return nil, err + } + algPtr, _ := windows.UTF16PtrFromString(algName) + // BCRYPT_PKCS1_PADDING_INFO structure + return unsafe.Pointer( + &struct { + pszAlgId *uint16 + }{ + pszAlgId: algPtr, + }, + ), nil +} + +func hashToAlgName(hash crypto.Hash) (string, error) { + switch hash { + case crypto.SHA256: + return "SHA256", nil + case crypto.SHA384: + return "SHA384", nil + case crypto.SHA512: + return "SHA512", nil + case crypto.SHA1: + return "SHA1", nil + default: + return "", fmt.Errorf("unsupported hash function: %s", hash.String()) + } +} + +// OpenCertStore opens the Windows Certificate Store +func OpenCertStore(storeName string, storeLocation uint32) (windows.Handle, error) { + storePtr, err := windows.UTF16PtrFromString(storeName) + if err != nil { + return 0, err + } + return windows.CertOpenStore( + windows.CERT_STORE_PROV_SYSTEM, + 0, + uintptr(0), + storeLocation, + uintptr(unsafe.Pointer(storePtr)), + ) +} + +// FindCertBySubject finds a certificate containing the given subject string +func FindCertBySubject(store windows.Handle, subject string) (*windows.CertContext, error) { + subjectPtr, err := windows.UTF16PtrFromString(subject) + if err != nil { + return nil, err + } + return windows.CertFindCertificateInStore( + store, + windows.X509_ASN_ENCODING, + 0, + windows.CERT_FIND_SUBJECT_STR, + unsafe.Pointer(subjectPtr), + nil, + ) +} + +// NewCNGSigner creates a crypto.Signer from a Windows certificate +func NewCNGSigner(store windows.Handle, certCtx *windows.CertContext) (*CNGSigner, error) { + // Copy certificate data outside Windows context + encodedCert := unsafe.Slice(certCtx.EncodedCert, certCtx.Length) + buf := bytes.Clone(encodedCert) + x509Cert, err := x509.ParseCertificate(buf) + if err != nil { + return nil, fmt.Errorf("parse certificate: %w", err) + } + + signer := &CNGSigner{ + store: store, + windowsCertContext: certCtx, + x509Cert: x509Cert, + } + + // Set finalizer for cleanup + runtime.SetFinalizer(signer, func(c *CNGSigner) { + _ = windows.CertFreeCertificateContext(c.windowsCertContext) + _ = windows.CertCloseStore(c.store, 0) + }) + + return signer, nil +} + +func main() { + fmt.Println("=== CNG crypto.Signer Spike (T-1.1) ===") + fmt.Println("Pure Go implementation using golang.org/x/sys/windows") + fmt.Println() + + // Search subject (default: DC01) + searchSubject := "DC01" + if len(os.Args) > 1 { + searchSubject = os.Args[1] + } + + // Test 1: Open LocalMachine Certificate Store + fmt.Println("[1] Opening LocalMachine\\My certificate store...") + store, err := OpenCertStore("MY", windows.CERT_SYSTEM_STORE_LOCAL_MACHINE) + if err != nil { + log.Fatalf("Failed to open cert store: %v", err) + } + fmt.Println(" Store opened successfully") + fmt.Println() + + // Test 2: Find certificate by subject + fmt.Printf("[2] Searching for certificate containing '%s'...\n", searchSubject) + + // Enumerate all certificates first + var prevCtx *windows.CertContext + var foundCtx *windows.CertContext + count := 0 + + for { + ctx, err := windows.CertEnumCertificatesInStore(store, prevCtx) + if err != nil { + break // End of enumeration + } + count++ + + // Parse and check certificate + encodedCert := unsafe.Slice(ctx.EncodedCert, ctx.Length) + buf := bytes.Clone(encodedCert) + cert, err := x509.ParseCertificate(buf) + if err != nil { + prevCtx = ctx + continue + } + + fmt.Printf(" - CN=%s (Issuer: %s)\n", cert.Subject.CommonName, cert.Issuer.CommonName) + + if strings.Contains(cert.Subject.CommonName, searchSubject) && foundCtx == nil { + // CRITICAL: Duplicate the context! CertEnumCertificatesInStore frees + // the previous context on next call, so we must duplicate it to keep it valid. + foundCtx = windows.CertDuplicateCertificateContext(ctx) + fmt.Printf("\n MATCH FOUND!\n") + fmt.Printf(" Subject: %s\n", cert.Subject.String()) + fmt.Printf(" Thumbprint: %X\n", sha256.Sum256(cert.Raw)) + fmt.Printf(" NotAfter: %s\n", cert.NotAfter.Format(time.RFC3339)) + fmt.Printf(" Issuer: %s\n", cert.Issuer.CommonName) + fmt.Printf(" Key Algorithm: %s\n", cert.PublicKeyAlgorithm.String()) + + if len(cert.DNSNames) > 0 { + fmt.Printf(" SAN DNSNames: %v\n", cert.DNSNames) + } + } + prevCtx = ctx + } + fmt.Printf("\n Total certificates in store: %d\n", count) + + if foundCtx == nil { + log.Fatalf("No certificate found matching '%s'", searchSubject) + } + fmt.Println(" Certificate found") + fmt.Println() + + // Test 3: Create crypto.Signer from certificate + fmt.Println("[3] Creating crypto.Signer from certificate...") + signer, err := NewCNGSigner(store, foundCtx) + if err != nil { + log.Fatalf("Failed to create signer: %v", err) + } + fmt.Println(" crypto.Signer created (private key in CNG store)") + fmt.Println() + + // Test 4: Verify public key access + fmt.Println("[4] Verifying public key access...") + pubKey := signer.Public() + if pubKey == nil { + log.Fatal("Public key is nil") + } + fmt.Printf(" Public key type: %T\n", pubKey) + fmt.Println(" Public key accessible") + fmt.Println() + + // Test 5: Sign a test digest (PKCS#1 v1.5) + fmt.Println("[5] Testing signing operation (SHA-256, PKCS#1 v1.5)...") + testData := []byte("NetBird Machine Tunnel CNG Spike Test") + digest := sha256.Sum256(testData) + + startSign := time.Now() + signature, err := signer.Sign(rand.Reader, digest[:], crypto.SHA256) + signDuration := time.Since(startSign) + + if err != nil { + log.Fatalf("Signing failed: %v", err) + } + fmt.Printf(" Signature length: %d bytes\n", len(signature)) + fmt.Printf(" Signing latency: %v\n", signDuration) + fmt.Println(" Signing successful!") + fmt.Println() + + // Test 6: Verify signature + fmt.Println("[6] Verifying signature...") + rsaPubKey, ok := pubKey.(*rsa.PublicKey) + if !ok { + fmt.Println(" Skipping verification (non-RSA key)") + } else { + err = rsa.VerifyPKCS1v15(rsaPubKey, crypto.SHA256, digest[:], signature) + if err != nil { + log.Fatalf("Signature verification failed: %v", err) + } + fmt.Println(" Signature verified successfully!") + } + fmt.Println() + + // Test 7: Performance test (10 signing operations) + fmt.Println("[7] Performance test (10 signing operations)...") + var totalDuration time.Duration + for i := 0; i < 10; i++ { + testDigest := sha256.Sum256([]byte(fmt.Sprintf("test-%d", i))) + start := time.Now() + _, err := signer.Sign(rand.Reader, testDigest[:], crypto.SHA256) + if err != nil { + log.Fatalf("Signing %d failed: %v", i, err) + } + totalDuration += time.Since(start) + } + avgLatency := totalDuration / 10 + fmt.Printf(" Average signing latency: %v\n", avgLatency) + if avgLatency > 500*time.Millisecond { + fmt.Println(" WARNING: Latency > 500ms (performance concern)") + } else if avgLatency > 50*time.Millisecond { + fmt.Println(" Note: Latency > 50ms (acceptable)") + } else { + fmt.Println(" Excellent performance!") + } + fmt.Println() + + // Test 8: Create tls.Certificate + fmt.Println("[8] Creating tls.Certificate with CNG-backed signer...") + tlsCert := tls.Certificate{ + Certificate: [][]byte{signer.x509Cert.Raw}, + PrivateKey: signer, + Leaf: signer.x509Cert, + } + fmt.Printf(" Certificate chain length: %d\n", len(tlsCert.Certificate)) + fmt.Println(" tls.Certificate created") + fmt.Println() + + // Test 9: Verify TLS config is usable for mTLS + fmt.Println("[9] Verifying TLS config for mTLS client...") + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + MinVersion: tls.VersionTLS12, + } + // Verify config has the certificate and can be used for mTLS + if len(tlsConfig.Certificates) == 0 { + log.Fatal("TLS config has no certificates") + } + if tlsConfig.Certificates[0].PrivateKey == nil { + log.Fatal("TLS certificate has no private key") + } + // Verify the private key implements crypto.Signer + if _, ok := tlsConfig.Certificates[0].PrivateKey.(crypto.Signer); !ok { + log.Fatal("Private key does not implement crypto.Signer") + } + fmt.Println(" TLS config valid for mTLS (crypto.Signer verified)") + fmt.Println() + + // Summary + fmt.Println("=== SPIKE RESULT: GO ===") + fmt.Println() + fmt.Println("All tests passed! CNG crypto.Signer works with non-exportable keys.") + fmt.Println() + fmt.Println("Key findings:") + fmt.Println(" - Implementation: Pure Go using golang.org/x/sys/windows") + fmt.Println(" - No CGO required!") + fmt.Println(" - Store: LocalMachine\\My (CNG-backed)") + fmt.Printf(" - Certificate: %s\n", signer.x509Cert.Subject.CommonName) + fmt.Println(" - Private key: Non-exportable (CNG-backed)") + fmt.Printf(" - Signing latency: %v (avg)\n", avgLatency) + fmt.Println() + fmt.Println("Recommendation: Proceed with implementation using golang.org/x/sys/windows") + fmt.Println("Reference: https://github.com/getvictor/mtls/tree/master/mtls-go-windows") +} diff --git a/spike/san-parser/go.mod b/spike/san-parser/go.mod new file mode 100644 index 00000000000..0d894e4036b --- /dev/null +++ b/spike/san-parser/go.mod @@ -0,0 +1,5 @@ +module github.com/obtFusi/netbird-fork/spike/san-parser + +go 1.22 + +require golang.org/x/sys v0.28.0 diff --git a/spike/san-parser/main.go b/spike/san-parser/main.go new file mode 100644 index 00000000000..832783b980c --- /dev/null +++ b/spike/san-parser/main.go @@ -0,0 +1,396 @@ +//go:build windows + +// SAN/Template Parsing Spike - T-1.3 +// Tests whether Go can parse SAN DNSName and Template OID/Name from AD CS certificates. +package main + +import ( + "bytes" + "crypto/sha256" + "crypto/x509" + "encoding/asn1" + "fmt" + "log" + "os" + "strings" + "unsafe" + + "golang.org/x/sys/windows" +) + +// ASN.1 Tags for string types +const ( + tagUTF8String = 12 // 0x0C + tagPrintableString = 19 // 0x13 + tagIA5String = 22 // 0x16 + tagBMPString = 30 // 0x1E - UTF-16BE! +) + +// Certificate Template OIDs (AD CS) +var ( + // szOID_CERTIFICATE_TEMPLATE (v2 Templates): contains OID + Version + OIDCertificateTemplateV2 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 21, 7} + + // szOID_ENROLL_CERTTYPE_EXTENSION (v1 Templates): contains Template NAME as String! + OIDCertificateTemplateNameV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 20, 2} + + // Known Machine Template Names (case-insensitive) + DefaultMachineTemplateNames = []string{ + "Machine", "Computer", "Workstation", + "NetBirdMachine", "DomainController", + } +) + +// TemplateInfo holds parsed template information +type TemplateInfo struct { + OID string // Template OID from v2 extension + Name string // Template Name from v1 extension + Version int // Template Version from v2 extension +} + +// MTLSIdentity holds extracted certificate information +type MTLSIdentity struct { + // Primary Identity: SAN DNSName (NOT CN!) + DNSName string // e.g. "win10-pc.corp.local" + Hostname string // e.g. "win10-pc" (first part) + Domain string // e.g. "corp.local" (rest) + + // Validation + IssuerDN string // Issuer Distinguished Name + IssuerFP string // SHA-256 Fingerprint + TemplateOID string // Certificate Template OID + TemplateName string // Template Name from extension + CertSerial string // For audit/logging + + PeerType string // "machine" | "user" | "unknown" +} + +func main() { + fmt.Println("=== SAN/Template Parsing Spike (T-1.3) ===") + fmt.Println("Tests SAN DNSName + Template OID/Name extraction from AD CS certs") + fmt.Println() + + // Search subject (default: DC01) + searchSubject := "DC01" + if len(os.Args) > 1 { + searchSubject = os.Args[1] + } + + // Test 1: Open LocalMachine Certificate Store + fmt.Println("[1] Opening LocalMachine\\My certificate store...") + store, err := openCertStore("MY", windows.CERT_SYSTEM_STORE_LOCAL_MACHINE) + if err != nil { + log.Fatalf("Failed to open cert store: %v", err) + } + defer windows.CertCloseStore(store, 0) + fmt.Println(" Store opened successfully") + fmt.Println() + + // Test 2: Find certificate + fmt.Printf("[2] Searching for certificate containing '%s'...\n", searchSubject) + cert, err := findCertBySubject(store, searchSubject) + if err != nil { + log.Fatalf("Failed to find certificate: %v", err) + } + fmt.Printf(" Found: CN=%s\n", cert.Subject.CommonName) + fmt.Println() + + // Test 3: Extract SAN DNSNames + fmt.Println("[3] Extracting SAN DNSNames...") + if len(cert.DNSNames) == 0 { + fmt.Println(" WARNING: No SAN DNSNames found!") + fmt.Println(" This certificate cannot be used for mTLS (SAN required)") + } else { + for i, dns := range cert.DNSNames { + fmt.Printf(" [%d] %s\n", i+1, dns) + } + fmt.Printf(" Total SAN DNSNames: %d\n", len(cert.DNSNames)) + } + fmt.Println() + + // Test 4: Parse Template Extensions + fmt.Println("[4] Parsing Certificate Template Extensions...") + tmplInfo := extractTemplateInfo(cert) + if tmplInfo.OID != "" { + fmt.Printf(" Template OID (v2): %s\n", tmplInfo.OID) + if tmplInfo.Version > 0 { + fmt.Printf(" Template Version: %d\n", tmplInfo.Version) + } + } else { + fmt.Println(" Template OID (v2): Not found") + } + if tmplInfo.Name != "" { + fmt.Printf(" Template Name (v1): %s\n", tmplInfo.Name) + } else { + fmt.Println(" Template Name (v1): Not found") + } + fmt.Println() + + // Test 5: Determine Peer Type + fmt.Println("[5] Determining Peer Type...") + peerType := determinePeerType(cert, tmplInfo) + fmt.Printf(" Peer Type: %s\n", peerType) + fmt.Println() + + // Test 6: Build MTLSIdentity + fmt.Println("[6] Building MTLSIdentity struct...") + identity := buildMTLSIdentity(cert, tmplInfo) + fmt.Printf(" DNSName: %s\n", identity.DNSName) + fmt.Printf(" Hostname: %s\n", identity.Hostname) + fmt.Printf(" Domain: %s\n", identity.Domain) + fmt.Printf(" IssuerDN: %s\n", identity.IssuerDN) + fmt.Printf(" IssuerFP: %s\n", identity.IssuerFP) + fmt.Printf(" TemplateOID: %s\n", identity.TemplateOID) + fmt.Printf(" TemplateName: %s\n", identity.TemplateName) + fmt.Printf(" CertSerial: %s\n", identity.CertSerial) + fmt.Printf(" PeerType: %s\n", identity.PeerType) + fmt.Println() + + // Test 7: Verify all extensions are accessible + fmt.Println("[7] Listing all certificate extensions...") + for i, ext := range cert.Extensions { + critical := "" + if ext.Critical { + critical = " [CRITICAL]" + } + fmt.Printf(" [%d] OID: %s%s\n", i+1, ext.Id.String(), critical) + } + fmt.Printf(" Total extensions: %d\n", len(cert.Extensions)) + fmt.Println() + + // Test 8: Test ASN.1 String Decoding (BMPString) + fmt.Println("[8] Testing ASN.1 String Decoding...") + + // UTF8String test + utf8Test := []byte{0x0C, 0x07, 'M', 'a', 'c', 'h', 'i', 'n', 'e'} + utf8Result := decodeASN1String(utf8Test) + fmt.Printf(" UTF8String test: 'Machine' -> '%s' %s\n", utf8Result, checkMark(utf8Result == "Machine")) + + // BMPString test (UTF-16BE: "Test") + bmpTest := []byte{0x1E, 0x08, 0x00, 'T', 0x00, 'e', 0x00, 's', 0x00, 't'} + bmpResult := decodeASN1String(bmpTest) + fmt.Printf(" BMPString test: 'Test' -> '%s' %s\n", bmpResult, checkMark(bmpResult == "Test")) + + // PrintableString test + printableTest := []byte{0x13, 0x05, 'H', 'e', 'l', 'l', 'o'} + printableResult := decodeASN1String(printableTest) + fmt.Printf(" PrintableString: 'Hello' -> '%s' %s\n", printableResult, checkMark(printableResult == "Hello")) + fmt.Println() + + // Summary + fmt.Println("=== SPIKE RESULT ===") + fmt.Println() + + allPassed := true + checks := []struct { + name string + passed bool + }{ + {"SAN DNSName extracted", len(cert.DNSNames) > 0}, + {"Template Info available", tmplInfo.OID != "" || tmplInfo.Name != ""}, + {"Peer Type determined", peerType != "unknown"}, + {"Identity built", identity.DNSName != ""}, + {"ASN.1 UTF8String", utf8Result == "Machine"}, + {"ASN.1 BMPString", bmpResult == "Test"}, + } + + for _, c := range checks { + if c.passed { + fmt.Printf(" [PASS] %s\n", c.name) + } else { + fmt.Printf(" [FAIL] %s\n", c.name) + allPassed = false + } + } + + fmt.Println() + if allPassed { + fmt.Println("All checks passed! Go crypto/x509 can parse AD CS certificates.") + fmt.Println() + fmt.Println("Key findings:") + fmt.Printf(" - SAN DNSName: %s\n", identity.DNSName) + fmt.Printf(" - Template: %s (%s)\n", identity.TemplateName, identity.TemplateOID) + fmt.Printf(" - Peer Type: %s\n", identity.PeerType) + fmt.Println() + fmt.Println("Recommendation: Proceed with mTLS implementation using SAN DNSName") + } else { + fmt.Println("Some checks failed. Review certificate configuration.") + } +} + +func checkMark(ok bool) string { + if ok { + return "[OK]" + } + return "[FAIL]" +} + +func openCertStore(storeName string, storeLocation uint32) (windows.Handle, error) { + storePtr, err := windows.UTF16PtrFromString(storeName) + if err != nil { + return 0, err + } + return windows.CertOpenStore( + windows.CERT_STORE_PROV_SYSTEM, + 0, + uintptr(0), + storeLocation, + uintptr(unsafe.Pointer(storePtr)), + ) +} + +func findCertBySubject(store windows.Handle, subject string) (*x509.Certificate, error) { + var prevCtx *windows.CertContext + + for { + ctx, err := windows.CertEnumCertificatesInStore(store, prevCtx) + if err != nil { + break + } + + encodedCert := unsafe.Slice(ctx.EncodedCert, ctx.Length) + buf := bytes.Clone(encodedCert) + cert, err := x509.ParseCertificate(buf) + if err != nil { + prevCtx = ctx + continue + } + + if strings.Contains(cert.Subject.CommonName, subject) { + // Found it - duplicate context to keep it valid + _ = windows.CertDuplicateCertificateContext(ctx) + return cert, nil + } + prevCtx = ctx + } + + return nil, fmt.Errorf("certificate containing '%s' not found", subject) +} + +// extractTemplateInfo extracts Template OID and Name from certificate extensions +func extractTemplateInfo(cert *x509.Certificate) TemplateInfo { + var info TemplateInfo + + for _, ext := range cert.Extensions { + // v2 Extension: contains Template OID + Version + if ext.Id.Equal(OIDCertificateTemplateV2) { + var templateData struct { + TemplateID asn1.ObjectIdentifier + Major int `asn1:"optional"` + Minor int `asn1:"optional"` + } + if _, err := asn1.Unmarshal(ext.Value, &templateData); err == nil { + info.OID = templateData.TemplateID.String() + info.Version = templateData.Major + } + } + + // v1 Extension: contains Template NAME as BMPString/UTF8String + if ext.Id.Equal(OIDCertificateTemplateNameV1) { + info.Name = decodeASN1String(ext.Value) + } + } + return info +} + +// decodeASN1String decodes ASN.1 encoded strings (UTF8, BMP, Printable, IA5) +func decodeASN1String(data []byte) string { + if len(data) < 2 { + return string(data) + } + + var raw asn1.RawValue + if _, err := asn1.Unmarshal(data, &raw); err != nil { + return string(data) // Fallback + } + + switch raw.Tag { + case tagUTF8String, tagPrintableString, tagIA5String: + return string(raw.Bytes) + case tagBMPString: + return decodeBMPStringBytes(raw.Bytes) // UTF-16BE → UTF-8 + default: + return string(raw.Bytes) + } +} + +// decodeBMPStringBytes decodes UTF-16BE bytes to Go string +func decodeBMPStringBytes(data []byte) string { + runes := make([]rune, 0, len(data)/2) + for i := 0; i+1 < len(data); i += 2 { + r := rune(data[i])<<8 | rune(data[i+1]) + if r != 0 { + runes = append(runes, r) + } + } + return string(runes) +} + +// determinePeerType determines if cert is for machine or user +func determinePeerType(cert *x509.Certificate, tmplInfo TemplateInfo) string { + // Priority 1: Template NAME (most reliable) + if tmplInfo.Name != "" { + nameLower := strings.ToLower(tmplInfo.Name) + for _, mt := range DefaultMachineTemplateNames { + if nameLower == strings.ToLower(mt) { + return "machine" + } + } + // Check for user templates + if strings.Contains(nameLower, "user") || strings.Contains(nameLower, "smartcard") { + return "user" + } + } + + // Priority 2: EKU Analysis + for _, eku := range cert.ExtKeyUsage { + // SmartCardLogon is typically user + if eku == x509.ExtKeyUsageAny { + continue + } + } + // Check for SmartCardLogon OID (1.3.6.1.4.1.311.20.2.2) + for _, oid := range cert.UnknownExtKeyUsage { + if oid.String() == "1.3.6.1.4.1.311.20.2.2" { + return "user" // SmartCardLogon + } + } + + // Priority 3: SAN Analysis (User certs often have UPN/Email) + if len(cert.EmailAddresses) > 0 { + return "user" + } + + // Priority 4: Has DNSNames but no Email = likely machine + if len(cert.DNSNames) > 0 && len(cert.EmailAddresses) == 0 { + return "machine" + } + + return "unknown" +} + +// buildMTLSIdentity builds the full identity struct from certificate +func buildMTLSIdentity(cert *x509.Certificate, tmplInfo TemplateInfo) MTLSIdentity { + identity := MTLSIdentity{ + IssuerDN: cert.Issuer.String(), + IssuerFP: fmt.Sprintf("%X", sha256.Sum256(cert.RawIssuer)), + TemplateOID: tmplInfo.OID, + TemplateName: tmplInfo.Name, + CertSerial: cert.SerialNumber.String(), + PeerType: determinePeerType(cert, tmplInfo), + } + + // Extract primary SAN DNSName + if len(cert.DNSNames) > 0 { + identity.DNSName = strings.ToLower(cert.DNSNames[0]) + + // Split into hostname and domain + parts := strings.SplitN(identity.DNSName, ".", 2) + identity.Hostname = parts[0] + if len(parts) > 1 { + identity.Domain = parts[1] + } + } + + return identity +} diff --git a/test/certs/ca.crt b/test/certs/ca.crt new file mode 100644 index 00000000000..5ddedff75cd --- /dev/null +++ b/test/certs/ca.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFPTCCAyWgAwIBAgIUJg64HcVZ0QvA7BU55y0On8vcn2MwDQYJKoZIhvcNAQEL +BQAwLjEQMA4GA1UEAwwHVGVzdC1DQTEaMBgGA1UECgwRTmV0QmlyZC1Gb3JrLVRl +c3QwHhcNMjYwMTIwMDgxMzEyWhcNMjcwMTIwMDgxMzEyWjAuMRAwDgYDVQQDDAdU +ZXN0LUNBMRowGAYDVQQKDBFOZXRCaXJkLUZvcmstVGVzdDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAJhPWaTwp+TvDEYYjj2dawsiA2psRjYp0mbFUeve +xUp5aZ5Kf9F3wNn4ECckMJ+ngCeDJOVFXqwaJ9JAxhXWi75Z/1sf13IR6MjFdRrQ +3fwC/lEJC38q5oP/UbC1pGFSDo9kA2dPMX38ZlFeO6o5wDsbFYbO0uGFlFFljysL +QO3iWMZyXBS8SDpmzl/VxZOJFJKHkaTLmld4N53ogoZV8Q/ILmRfEpGprhnH22YD +9aYOmMwEIJ2WtFbQdB5cM0NIMfpR1juV2znxwyQXpWWOsLERkA3rPJZJZur7BGCu +2OPZget7aJ1Dr0vm+cG9sup8k1o8ROYNyqv4T7hzNP8X7lf624FVTtlcGRGMKbzT +y84auPYtZttHlW/wQCKwWxoAsxj+iv2Gu0mTbcAbTJfTZORqxqcSSReBuB4mAXQy +GXkr60llrUKPL+wkRou5+QY4ITDJoAVfBZnLa485pYpAPcGF+2GFvd2Oo9UU9S5k +MSvl/0cH4TF8Esek4mdWVbUu5gsedN7J6Ex3f6fmpXsIgaOdXk7K30+GJP2M4u33 +3Vz7n4uEonVzBbXsLKDKXYVbBKSfCOq7V/DdJ2pJwaoVnYRVYjrE18xCY4+S/sYc +ROhadQ7+c6BvXcWdTfi1FrrTLxHNdR8uRF1h80RaZqvsuYqRJ3K5XxOtGq3XcCU4 +fVB5AgMBAAGjUzBRMB0GA1UdDgQWBBR8Uiaqcuk4nvnD9mROZsknCtp+1zAfBgNV +HSMEGDAWgBR8Uiaqcuk4nvnD9mROZsknCtp+1zAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4ICAQAMSDt3c+J33riijvHT8VGefq+VhMQwSO1RdKMBQbpl +cU3WtOZvJVa3GmiYz8yoVSE1iFHy/26C0t61Q+R6syB1pLY03rDiat881HNqhZW3 +d/889zzj/mzj6T/+3nRQO5Xssrn7kFIlcDhXttQw2/wBVrMN6NIeckYBfdHVOifi +Qjlh3Q0BJfM3qlcd/DtAQxoQdyf5pQZADOk4br7/77CCnCu22/mbD93CtVVaU76l +7O2VHs6iX0ChM8MT95xLDnw6BimGfEDw5g60DUpTf/LIF2e7Ma6+RQghPcmEvhrL +Dk7U8Vv8dUNfgVmNqmT+NjDPO3YZlMxEVQUzuy+KWsW7gaNRe6HOVTeIGKxMF0iu +1mR6VntIU2mFlr6IpGlvNJ238ckNP4Y/fMG3YZayii5s6uoVLmdSuxdSuIhZL4mh +bHsXHS5BSDNH+sFMztzA1GhBoYWvdahZZdmqkeC7c5wHgPNagPbXkJgsBnRv1N8b +Mt79UC2jIK47qnuTpFVtkWnANKSF7KM5walE2PCQeC9/Lho//zcED0n7SfPlK4CR +hsTukSy+X6f9ItryaAuc2I5RBFNvMjh1CFQoPAvz6k2jy65sXNeHgyaYtMV1yOy5 +yb98UbGDi+cu4RjWSliEa89Watokk/mblWc1eWxuPgW3geDRW+fj9FucRbXZE5gA +qw== +-----END CERTIFICATE----- diff --git a/test/certs/ca.key b/test/certs/ca.key new file mode 100644 index 00000000000..3fd74378d64 --- /dev/null +++ b/test/certs/ca.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCYT1mk8Kfk7wxG +GI49nWsLIgNqbEY2KdJmxVHr3sVKeWmeSn/Rd8DZ+BAnJDCfp4AngyTlRV6sGifS +QMYV1ou+Wf9bH9dyEejIxXUa0N38Av5RCQt/KuaD/1GwtaRhUg6PZANnTzF9/GZR +XjuqOcA7GxWGztLhhZRRZY8rC0Dt4ljGclwUvEg6Zs5f1cWTiRSSh5Gky5pXeDed +6IKGVfEPyC5kXxKRqa4Zx9tmA/WmDpjMBCCdlrRW0HQeXDNDSDH6UdY7lds58cMk +F6VljrCxEZAN6zyWSWbq+wRgrtjj2YHre2idQ69L5vnBvbLqfJNaPETmDcqr+E+4 +czT/F+5X+tuBVU7ZXBkRjCm808vOGrj2LWbbR5Vv8EAisFsaALMY/or9hrtJk23A +G0yX02TkasanEkkXgbgeJgF0Mhl5K+tJZa1Cjy/sJEaLufkGOCEwyaAFXwWZy2uP +OaWKQD3Bhfthhb3djqPVFPUuZDEr5f9HB+ExfBLHpOJnVlW1LuYLHnTeyehMd3+n +5qV7CIGjnV5Oyt9PhiT9jOLt991c+5+LhKJ1cwW17Cygyl2FWwSknwjqu1fw3Sdq +ScGqFZ2EVWI6xNfMQmOPkv7GHEToWnUO/nOgb13FnU34tRa60y8RzXUfLkRdYfNE +Wmar7LmKkSdyuV8TrRqt13AlOH1QeQIDAQABAoICAAtSdLu/3v8z7PHl/+bhJZSr +h9z4pQSAOeGFYAhcZEabvWf0qklAMXASkmLSfCHWIf7t7wdzD21UOWsWAROdAgDZ +sFOOC6YxYiBQm1qttZ62+0A7X6Upo36i82fjLM/GOYpbuSwMFUYEBfgc/OrzxZAv +PHursr3sf+DIH6snuEEmvwordKBe+bCLtWIm4jvMKCEXXlKFhxji+SFuKAvD5jpR +227/KUJ2PliwxGSyPSfFtIKP+Pu1+PxuHP+nw3DINFzjCf2eb3BAgkzTzCn/QC+9 +ejuqpbOXS8UCeWoVfU58u/1tDFMKdcS67AyEpUDsl0iRK979HfiIKibwO3uOBBvd +QN8AqnVyLKUKhWg0B/b9yeiinus9e7XQL+JBbIxROBU13Ss/aEztXOG+Rcx9lmCz +3OzIaaPZgcqjbOyqfdCDuID/FwTJi2T9l7cVTS4VJgpsCY8CuAzuZhnFStSsX2Ga +zYI06Lo0vNyII1466YAEFQhvfgru86nnCvozez+nP/rRuzoiZ3rRWIilg7kzb+Qz +Cs4R96JECpTbdsYMnVpt944nZ+17PxctUIwSixVJzHWWVSFb98gJ2WZ/dE7E79SE +Tk52WeVZOqbUp6RIKjGMZyyvl2xteaDGfgxFOJX5tL4HgLtiV2YGPJcLC6YlHPnJ +Gs30nHJ8/0vsbiG5QHhdAoIBAQDSuNDUilLybRpzEtjG9bluk5ZnbQN6gwIwpzw7 +VzhZIjcqTFog1ih0mpaMI1+pBZpdDERWfBkCn9V9CcOamPPeuEhqTayGjczdHLyI +CMmJdgWlNO78LPdVE6UCAhTrJMNJXLUZQmZvOgITFBmRkXOBFBf26oUWB9k3Tz3o +qU1aP1kPTIe5mB0PMYKJoRVPdAruuakYJWPFG7XuiSY2mOSrWIPFy9jK6YNovvqm +XUoTKeNl4Ad6FejoocIkdySEHeM5gzmti30DxePXm7Dc4E03/dsWQCKyvPx+xMS6 +GdFIy3Eq1u3KTyg25G1fN2TC6ARf9sRrewMuWl3QutSMqA81AoIBAQC5CXdXldfy +aN0J5+NbbQX1d/he6eOM91nZ8I2VHnv5OwXNBpO1XwJ590vq2tSX44zUTLlJ13ax +I6cd1KYS0dnw7/e3Jv+ct3slO50kcjGoAG/U0o04vTJeAxgBWete16arVQpUwqlf +mNclP8rpoLWXevQvYHTG6Nk9ig7oNF23HL2b0AcACy2d6XA6de3GIfb034VuvrDF +5E32GUbwlCtrU8em+jFYReXnUrlhq7eaGuv0o5cKz5uYOZoSCuM/kjvUMSYTj76F +b/AbXf069Dig7Gf0cGSjLcjdBJ/IBinbYVeo3tvnhdbt/clTI8KO5qq4yQA0W5RN +GtSXr1RoglC1AoIBAEgCmHrJemcWGb/RZPs45dF/5hoaCuJG+uydedvdhogPRULT +LMmj5ddTLLdfL0WXgJTjqEbVycY30MEWIR4nvs8Rss2BFcA1nRjCxTrHpfevuWYn +nLPYufz85Zq2E2f3/DSJ1el344GHFUZnzAUO66Xks/vRUQGiVPytu75SfPimRU0R +HiCydtvGU5Gs1pd8VHAYSkzSGjI1sgp+G+z0etCDQyTI8KEHA7075nQL1VCPNAKQ +eH2kFx/Ih4vmmzf67resvH0t+d7cNWxs7BfPHxRPUBted30VUEQSAhiG0hpKS7YU +FNbaDigUD7xGNczVdQlGTwFb3E5u2ziFYDVhCTECggEBALOMXPJvIzlEyd931OT9 +OIAFZstqtvQtfFF/G1NsXi3sOOfGjwO6aqPA9DizGQE9u4Sx4kWlvrWKe/n1QyUV +3h3uLHfbbsM6Q1NQsl4QNODpc5qWqJQ6+inBZMTC5SKFrWOpRDoVHdb2byXk7def +qzWPCG0Ecqwke9A8K9TkI1+wksgjpvdC4YPOZalEj3HnuUPJtSdOACg2LfQ0eOKv +VCZ0CFKdic43HSxG2D3PK3xszTL4nnLOGxQuKLODPxmLo2R2QJVQsTW39cwIetV+ +74gvfXvyp6Szo6nbd9PXLzyCC1aA0e+prnIHBhkXpQ2XBGgEGp+zkYb0FwANEKHM +edUCggEBALYLWpczQxB+R/2kZ5hhRj3oIYIMND+iOSbcf5K+nn1hvmAentXH2/t7 +6Tbbwe+hEGsV3E6807qzidfv8xrweCJ4bYIRhpSoxXvphn0uN903L2+5XnMwU6vB +jpTuE0XY+T1Nxy0VBAzqenZtGufKnJ8iTD80TdJAiOGaSZOT9dz0c0CcLZmlMijl +qsfwrVMsRhPq9baK+uL2pKZWSxyDmK+8QHM1bSyN8MvhZuK1uXwAB7slIqikJqJw +cZNFb33drsuE/IsyRTHufm5k5iW92ZT2upRa0QahMd69g0Yur6N5EmtRbX5yqcsk +CZsqE320npHHpSUk3Apbdwd5pbSrtIY= +-----END PRIVATE KEY----- diff --git a/test/certs/ca.srl b/test/certs/ca.srl new file mode 100644 index 00000000000..8d68603bd77 --- /dev/null +++ b/test/certs/ca.srl @@ -0,0 +1 @@ +228C547F97ACC58D05ED0EDD43C53A9F23BF99A2 diff --git a/test/certs/client.cnf b/test/certs/client.cnf new file mode 100644 index 00000000000..c3a97e13bf1 --- /dev/null +++ b/test/certs/client.cnf @@ -0,0 +1,12 @@ +[req] +distinguished_name = req_dn +req_extensions = v3_req +prompt = no + +[req_dn] +CN = win10-pc.corp.local + +[v3_req] +keyUsage = digitalSignature, keyEncipherment +extendedKeyUsage = clientAuth +subjectAltName = DNS:win10-pc.corp.local diff --git a/test/certs/client.crt b/test/certs/client.crt new file mode 100644 index 00000000000..3dccb727f12 --- /dev/null +++ b/test/certs/client.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEYDCCAkigAwIBAgIUIoxUf5esxY0F7Q7dQ8U6nyO/maEwDQYJKoZIhvcNAQEL +BQAwLjEQMA4GA1UEAwwHVGVzdC1DQTEaMBgGA1UECgwRTmV0QmlyZC1Gb3JrLVRl +c3QwHhcNMjYwMTIwMDgxMzI0WhcNMjYwMjE5MDgxMzI0WjAeMRwwGgYDVQQDDBN3 +aW4xMC1wYy5jb3JwLmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAz0Qk4UrUlQbUgRfsEElxhGA5UvPEvKVMH/R2JCYnqZ50zqtSwrhfNNMFBH97 +BUDkJZbCi3yPnO+aerCJ4cMwPbjpOqSoD44U6JEsoAmUSK66CU74EhvYcxoxQYdJ +QlyLyVtzPhE5axp7CA5xRsXbQm5CW6h//XFdTWUoF4l6/2ZGd3CDIIrrUlwnD3wp +dgt5Btjc16ylFsCqCgWlxEDZ51npPpXNBB5Ps4DZZX6n4MUhxm7lK3Atj7PYQKfL +lCkXefykdJbmaM7EbFp5zZdHvz+pSCwA6S8Z03ShEgpUV44xq3ADSa/RyC73Xhpm +gDW4kPOfEWrIAdu9pN7qHfr6awIDAQABo4GFMIGCMAsGA1UdDwQEAwIFoDATBgNV +HSUEDDAKBggrBgEFBQcDAjAeBgNVHREEFzAVghN3aW4xMC1wYy5jb3JwLmxvY2Fs +MB0GA1UdDgQWBBTTPn8+PZfh1yd1sSiYp/AmnGnNWzAfBgNVHSMEGDAWgBR8Uiaq +cuk4nvnD9mROZsknCtp+1zANBgkqhkiG9w0BAQsFAAOCAgEAgNzOP1Krpsg8bkyU +ETYYWatFBjb0WOdw6Z6tAeHPod46sM1Y0RuQ8u0QAE8vHjcFTQrjZOWwmCuZf02W +t3qAu13KAOmf8GCABKUfwqGQDCdT/XhLPvILR0huViYY4s30Zs811Xdiq2aw+XCw +EZBSFsY+8j4scnZ28Iad/egp7tGwXeG3Ue0fthwQ9/k8IH0J6dsmEtUpOHRfgQmY +Uo/TWr7d0SXiJlxHoxL4/D0P+Jm2wsLh1Lj4fcQjznrQXRajkTyr3mw4GCf59eqX +9Fd9ueucasrGh0aqwrKZaEbjdWufhQ/ubW6I8Hq4NUCUsx4qDFhOMOKKHorQ9GaA +MkP4+VMWBxJcOiVlGPTmw6rokLtbri0bx5u8qu4yS9WcCBsExPGGdJeCyvKy6IZJ +bI7vEaPo8QQ/lx3kvt4NcHRocEwp3fbBETb9VLK8lMv6OKuLSm1UCvydghFYejKP +ldBEvY1unDT45AdSJ3UVIfndKwGKynGAf/TGzZnhl+5RjeVCKxWAiFrOXK7zLaPG +rMeNUzC+LKHPBWCqjQJnEjd2PbylDeCEoXKqMk2ADoUGR97u9KsvM7MBRQSmp4nt +eXaofQF0wJ/5nrsB+aWubrtX445e0D/2JgIVrGPNMHVNRIJvLurz0rxoLAFf3ijB +E6TIhQZEmorKfJqRfGnyW4kYYeE= +-----END CERTIFICATE----- diff --git a/test/certs/client.csr b/test/certs/client.csr new file mode 100644 index 00000000000..0013984df90 --- /dev/null +++ b/test/certs/client.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICtjCCAZ4CAQAwHjEcMBoGA1UEAwwTd2luMTAtcGMuY29ycC5sb2NhbDCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM9EJOFK1JUG1IEX7BBJcYRgOVLz +xLylTB/0diQmJ6medM6rUsK4XzTTBQR/ewVA5CWWwot8j5zvmnqwieHDMD246Tqk +qA+OFOiRLKAJlEiuuglO+BIb2HMaMUGHSUJci8lbcz4ROWsaewgOcUbF20JuQluo +f/1xXU1lKBeJev9mRndwgyCK61JcJw98KXYLeQbY3NespRbAqgoFpcRA2edZ6T6V +zQQeT7OA2WV+p+DFIcZu5StwLY+z2ECny5QpF3n8pHSW5mjOxGxaec2XR78/qUgs +AOkvGdN0oRIKVFeOMatwA0mv0cgu914aZoA1uJDznxFqyAHbvaTe6h36+msCAwEA +AaBTMFEGCSqGSIb3DQEJDjFEMEIwCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsG +AQUFBwMCMB4GA1UdEQQXMBWCE3dpbjEwLXBjLmNvcnAubG9jYWwwDQYJKoZIhvcN +AQELBQADggEBAIbySpar2XTI0Qp7cB7mkj5qfkifcgfSzSsubGeeoJYr4QUBSZmK +9BNZeQLZf3PltT81ZZcOkB1DggPbusqVGPkTPxV65ar7uFFNb5NJqD51xtcZCb88 +o4OumG1QnF05qepDDq+yMwNVkeSpFYTbvAJH15RAZXvbjwrfWNv0ZMwx+xI3YAYO +hlCiIxXLIqxfLOgDU2If2x5Hrn517zxr44XnKdg8Shu13CLg/k1Z57wUTTlf2GfO +X4IW45B9ubfsCIAGhRnASqHgDIJtuCVEfxaIFfpkaaOd/s1v0soegLPj5Nk8AbvG +AzE2jmWoz4GKy1h2teDBJzVsrArS1KBXQlM= +-----END CERTIFICATE REQUEST----- diff --git a/test/certs/client.key b/test/certs/client.key new file mode 100644 index 00000000000..427abe029ba --- /dev/null +++ b/test/certs/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDPRCThStSVBtSB +F+wQSXGEYDlS88S8pUwf9HYkJiepnnTOq1LCuF800wUEf3sFQOQllsKLfI+c75p6 +sInhwzA9uOk6pKgPjhTokSygCZRIrroJTvgSG9hzGjFBh0lCXIvJW3M+ETlrGnsI +DnFGxdtCbkJbqH/9cV1NZSgXiXr/ZkZ3cIMgiutSXCcPfCl2C3kG2NzXrKUWwKoK +BaXEQNnnWek+lc0EHk+zgNllfqfgxSHGbuUrcC2Ps9hAp8uUKRd5/KR0luZozsRs +WnnNl0e/P6lILADpLxnTdKESClRXjjGrcANJr9HILvdeGmaANbiQ858RasgB272k +3uod+vprAgMBAAECggEAQIMO0ZrXgQ0vroCyJr2dWh0TKpkaRrdScu3FqenAVaCu +7bbQmzAE3i3CNYyyT0fL21FJ+12JW8kONZrmR8FXrhZ0bZ7ben/4TQ0GrUdeAqNz +3zrXptdI70abRzCjIkco8UMIIyz8SLRkz/Si2Gr0HICyIdJYCBw1dMYEDRNrF7v/ +1MpPb1IuozOqRc8Pj3EdnAiXDLn4D80kIBP0hlnkGJ7RCmp/MpLBlia37icmw3ll +dSFJOLgbj9EF2qd2NNtf03N3HtYtgpZZRbF3P7kSsvujkXzpudBS4HMA72kXoAO6 ++pbGXMJc7la92rXdKxZ8F+6mOssgysMq6mtESJnREQKBgQDm6GEJzFSwc8oMhXVr +V/saT9NNXV/cUuDAckopXS7riWZg0decJudmQwuxFJo7yszkAnNawJO29RsZ8eX+ +P/QH6fFgQE2Cau/o9531LdrVNWvp8tkNfLLXWUWnF4eNLmcMc/0Hr+Vu748pqk1u +R4nhHQXNY04sfA3gi/NRgktFOwKBgQDlyhS/lPY4MQOGKvTSrjQ/5yeOpT1E1jUj +2mJUci8feYsfI/Cz9b5zAbNZAumEvrBpm8NjSWFNCH05ulMtEyDnPe0Tnp/34hY0 +Sk1TiFH2Tkk1O21ttr4mmBqkCAbcAT7z5C1WZ8ns9+PnQsq+V3XrI3MNsdWnxDpj +FfGUdK4MkQKBgQDgP6gWGvHYl+sOtAHv6Pb9e67LgLZbQ8XwQE6T02KA7uSVfNW3 +WfT61HwjUs3i6baIbXTYGxSZ53qVfN6PSE7X7LQ1dN2Rngc2qlwmQ4016PbPssBn +H8aT66gAeZJ0Yy9C4dZHw+S/EzpnDXS7eBCIpmX/LMU74JKdk20PqMkvBwKBgQDA +E3vmbGCntaipdKyyknUQWWsCXHLrYFaJApmg1tU27QTyYbto4ehw/6HnrHx/vll9 +3XqkOok/t/Hc2DeAfPXK9UN/W9+Bd5Vx3g3m3hMM3IFrIqKky9UEM65JIICDU/NI +MJoJGLZ8AvWYsIcCNd9WTop0jwr1shvQCV6m5iU6UQKBgGKZeDtnE1LI8Eg8Bw/m +8CHTS9MqkLO3yrKAv94w9AYSE9L7xIn2UevRhSqTKElN0Si7Dnxnl8Wlm21LCiXu +0/vf/sfILKoU0LfCMuhEVvaXyji9saIemlxHM3fO/ULXIzT6LUHZOn352uzaV9eq +UE2SDCn1IC8Z+JwrXgSsP3wM +-----END PRIVATE KEY----- diff --git a/test/certs/server.cnf b/test/certs/server.cnf new file mode 100644 index 00000000000..4cdd51dba00 --- /dev/null +++ b/test/certs/server.cnf @@ -0,0 +1,12 @@ +[req] +distinguished_name = req_dn +req_extensions = v3_req +prompt = no + +[req_dn] +CN = localhost + +[v3_req] +keyUsage = digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = DNS:localhost,IP:127.0.0.1 diff --git a/test/certs/server.crt b/test/certs/server.crt new file mode 100644 index 00000000000..9cd3775654f --- /dev/null +++ b/test/certs/server.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEUTCCAjmgAwIBAgIUIoxUf5esxY0F7Q7dQ8U6nyO/maIwDQYJKoZIhvcNAQEL +BQAwLjEQMA4GA1UEAwwHVGVzdC1DQTEaMBgGA1UECgwRTmV0QmlyZC1Gb3JrLVRl +c3QwHhcNMjYwMTIwMDgxMzM3WhcNMjYwMjE5MDgxMzM3WjAUMRIwEAYDVQQDDAls +b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDY2H7HX4Kt +wLYsihj4Aa0yeds1olLhxipIK5iciBXLF8/m9/UYMjIkFkFXjCPGB3OZJ4Kr9uPe +nU09E1gH6Z2FKFCQT0SJqBGb8q7NeM8fdYeKtIBzRIQ7T1SqnnEDDq51ni4J4yor +QlvwK8kqxWpkwHSnTFECU+UjmXG0f3RT4+gkgO+8xrUFA4HnjOLa3Swv3nU2p4gE +kCgLCkzD0emr9QsNeYmj7Hy1B0AThUrLmmEOuytyy+1VDxoKx/Hx1Z1WwsGU/H00 +60RRrrIHDPKJNMnGQNt7zyDKtB7iFyJZxvaamX8VK/xHWjZO3bGSKSMbpAz0tXC1 +J3ldUBpDcVXnAgMBAAGjgYAwfjALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYB +BQUHAwEwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMB0GA1UdDgQWBBQD48uW +SGWIpnQXD6UkPRMEdwFEOjAfBgNVHSMEGDAWgBR8Uiaqcuk4nvnD9mROZsknCtp+ +1zANBgkqhkiG9w0BAQsFAAOCAgEAclYur4B+pGG9YumbGx+Ii48dQMD/h0keh/TQ +eJLpMjVowtqGfk7GO/3/Pncb5sNbcmOh7SYVmckUsU/VzGGaOxvzvMKDKNAWJgTg +p0Uaom2zjNIcKYDqfCwYaWhqEkFbKCeCn784TIO7V1RBio/nhibo4VqAKigDr8bS +dS6xPgycCa9guxfdny6E2YX2iQwJ5PVrsS1HSpQ4/sP7N83i7pfZSFPuELWRdTck +MhFBJwH+P4picLWJ3k9zT3sh4CUPkxXmoPQTYChkb0J3WaubylLk22bR/OqqJePY ++SlbmJMuJWmQH0L6Js6WXTFhZ3Vn3um3NTKM74C1O2BfrY/u8BgqyOO0A4tx7lMG +ykuGO24fgck4jP6q3kltzWdOZWcB2yaOJeukwroOXHKNbbvfw1TW/mScgYhyV6jZ +hKQvQRCwLYvIAK2JD10HdJyU2N2Iazrwwp5RC0eobcLrvTH7MSsrnFhqmLTOJkTu +WBMs/Dvi+4a71kER34snUZI9r0QTDnzODRd1LuBmzx+O0XTKVS4gjpvaGA4ZHoNL +lFjD02DK7pSvTNSY5ioz+gSw61VhdE9UbhnXk6EPzBIbbW+k8aBiEoGyrDCXGscd +UvEPqYUKOsYkQmrXUyikOCpFZp7PA4twVf1sHjzflN7t0ba1U1frZFr4U/ii1glr +O5/LP00= +-----END CERTIFICATE----- diff --git a/test/certs/server.csr b/test/certs/server.csr new file mode 100644 index 00000000000..7e13cba3128 --- /dev/null +++ b/test/certs/server.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICqDCCAZACAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA2Nh+x1+CrcC2LIoY+AGtMnnbNaJS4cYqSCuYnIgV +yxfP5vf1GDIyJBZBV4wjxgdzmSeCq/bj3p1NPRNYB+mdhShQkE9EiagRm/KuzXjP +H3WHirSAc0SEO09Uqp5xAw6udZ4uCeMqK0Jb8CvJKsVqZMB0p0xRAlPlI5lxtH90 +U+PoJIDvvMa1BQOB54zi2t0sL951NqeIBJAoCwpMw9Hpq/ULDXmJo+x8tQdAE4VK +y5phDrsrcsvtVQ8aCsfx8dWdVsLBlPx9NOtEUa6yBwzyiTTJxkDbe88gyrQe4hci +Wcb2mpl/FSv8R1o2Tt2xkikjG6QM9LVwtSd5XVAaQ3FV5wIDAQABoE8wTQYJKoZI +hvcNAQkOMUAwPjALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGgYD +VR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQCq6AZI +ic8YjBi23BrrMm6i6O1aNBykVh1PQoQIt6Yb87NnJtoY30zTjARYfwMZBYRj5nay +OmzUJesfnYFxkF3SCy9Y1ERVf5I/QbHansslbt4lIAj+ltpF7wCmJlfi0fv0nMmk +LnKj6RS9fI99h61jZYlVpVhZHYStvVOjJbunula84vEKiS9/1D07WaVXj/LwGSIp +cNLdgMzy4V9vCURu46VR1DtGSLhiTha4kEGh8QpRcBWxXp4f1R3AzTomyUKfA+4K +36TQlZSwlRR3Pj0HSGAV3AO5LKBDeCEqpmyl9QZOX87WUq0dPSLb4I0it9ozGJKR +9Ar2/uOYn/AZmpcs +-----END CERTIFICATE REQUEST----- diff --git a/test/certs/server.key b/test/certs/server.key new file mode 100644 index 00000000000..8b802967449 --- /dev/null +++ b/test/certs/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDY2H7HX4KtwLYs +ihj4Aa0yeds1olLhxipIK5iciBXLF8/m9/UYMjIkFkFXjCPGB3OZJ4Kr9uPenU09 +E1gH6Z2FKFCQT0SJqBGb8q7NeM8fdYeKtIBzRIQ7T1SqnnEDDq51ni4J4yorQlvw +K8kqxWpkwHSnTFECU+UjmXG0f3RT4+gkgO+8xrUFA4HnjOLa3Swv3nU2p4gEkCgL +CkzD0emr9QsNeYmj7Hy1B0AThUrLmmEOuytyy+1VDxoKx/Hx1Z1WwsGU/H0060RR +rrIHDPKJNMnGQNt7zyDKtB7iFyJZxvaamX8VK/xHWjZO3bGSKSMbpAz0tXC1J3ld +UBpDcVXnAgMBAAECggEAaYBPklXkQQAhQiNASa2bJBNIdVxQAdvFp45adlH5dHHV +63RZUVfesFMJoHwkzYzDj9C60UrSC/WxZhU6v8LIKkh2hfejC2xzgNG+fWaZMx+d +/fybADnmMesDcO2cY1XpyHzYvmPBGQH0tDHBcqdQ/8rpFppZUY4azkqyGRTntoYm +z/NCar00uvt1mUhsknw0s0r8BNobjuLIp7ITeYTXsvCBzVEFhr/E3nnwc1MagavQ +BY/fhFLZx5K7De1fL3V7tUieIZMXBEwdJR/ddqblYE7Vabpz82+Kj3met8xmr5XC +WOStqklycfKFHxqW6ThLNR38gxPAI7zSYmCtd49JBQKBgQD4RdBmAl5Nyttfry5Z +DCBf9bc4+LLT7uNBUa/xVm21JvzWeteC74t5EmvzknuyYDLZdjYbJexNP6CMYaPe +kDkA6EWnAxP2qXrAOBntk/7FJ4cxgw3BIhugp+Mb/8A8APqWIuJ5+26w5SMx58av ++L2jGraD8KVi1uaEeBz2rNS4AwKBgQDfmEbu5e4574QkagK/PU6Q29abu6YK73vn +1xbqGd661XCZJF0f+hP3L0aHMimu/XfvUN9MQ+kYKXerQNoz1bOpAq4p/NIyaqQg +CTXhEE2bpoXTo3HwNmcmwIX3FzENcFjYDu6JwWcHJwDZ0DVTnUGJ1/Kbu/2gYnBj +rZinzrn/TQKBgCWTc3ItA5bkkAVQX0Rs+G1tpBiEU5SOAGk/ctClEx+q0fOoTfvs +Q4DEkAAieIRL2QZexNuhBr2+Vo9Iq+OgknAXt4sKhTf8+K4lD4+Mqa+vRt/whOFp +RyMupcn0EGVEhKi6iOqLanptg8Em4kR58kHAZkVb47ws6GC+SSvwhpV/AoGBAM+z +9hiP8OdSrq7g52JdAfGgtc/+1XSNGM25yWh31aY7BZjM5dEBjrBs8xg9qekLPfFK +b9O5tfsi8i5cVRYXqwgSHFWWrG/3DjVQOQO0EFPJscFysDHTZ6jg6NEqcv+n4G/8 +FuxSg3Fqcuji99aPW0VAG/c8/5KQPxTpOsiPScp1AoGBANDA7RYCzZnX7aSULDwp +8Pf+W6yJaquYGe2N1b4Rb8pA5Jvuw/1EAXmV7YINJn3IL9Uccnn/xSiOf7Ez26kK ++VFjJijphUW99ZtY99CboaobXusI5kndyU0qszlep87mxcvdhQ9iACZz/EvLFHiG +pBVfN4rL6HRvV2RpnOCv5auX +-----END PRIVATE KEY-----