mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-06 15:51:29 +02:00
Compare commits
12 Commits
agent-netw
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c19b7a30a | ||
|
|
1e2a7aa571 | ||
|
|
98f8a24108 | ||
|
|
b2ae98f0fd | ||
|
|
40ac64bb4e | ||
|
|
19a6cedfff | ||
|
|
6526fc2bec | ||
|
|
2afa69b622 | ||
|
|
2a61eac047 | ||
|
|
f2d13b884a | ||
|
|
564595d283 | ||
|
|
78c1c2fc32 |
133
.github/workflows/release.yml
vendored
133
.github/workflows/release.yml
vendored
@@ -475,6 +475,132 @@ jobs:
|
||||
path: dist/
|
||||
retention-days: 3
|
||||
|
||||
release_ui_gtk3:
|
||||
# Legacy GTK3/WebKit2GTK 4.1 UI build for distros without WebKitGTK 6.0
|
||||
# (Ubuntu 22.04, Debian 12, RHEL 9, Fedora <=39). Runs on ubuntu-22.04 so
|
||||
# the binary links against the oldest supported glibc.
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release_ui_gtk3_artifact_url: ${{ steps.upload_release_ui_gtk3.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||
persist-credentials: false
|
||||
|
||||
- name: Parse semver string
|
||||
id: semver_parser
|
||||
uses: netbirdio/shared-actions/actions/parse-semver@be5df6047383da2236e02243cceb857d8567c27e # v0.0.2
|
||||
|
||||
- name: Set snapshot flag
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
echo "flags=--snapshot" >> $GITHUB_ENV
|
||||
|
||||
- name: Set build vars
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
if [[ "x-${{ steps.semver_parser.outputs.prerelease }}" == "x-" && "x-${{ github.repository }}" == "x-netbirdio/netbird" ]]; then
|
||||
echo "x-${{ github.repository }}"
|
||||
echo "x-${{ steps.semver_parser.outputs.prerelease }}"
|
||||
echo "SKIP_PUBLISH=false" >> $GITHUB_ENV
|
||||
else
|
||||
echo "x-${{ github.repository }}"
|
||||
echo "x-${{ steps.semver_parser.outputs.prerelease }}"
|
||||
fi
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
# Restore-only from the release_ui cache written by trusted runs; the
|
||||
# module cache is identical (same go.sum) and stale build-cache
|
||||
# entries just miss.
|
||||
uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ runner.os }}-ui-go-releaser-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ui-go-releaser-
|
||||
|
||||
- name: Install modules
|
||||
run: go mod tidy
|
||||
|
||||
- name: check git status
|
||||
run: git --no-pager diff --exit-code
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
|
||||
with:
|
||||
version: 11
|
||||
|
||||
- name: Install dependencies
|
||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- name: Decode GPG signing key
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
env:
|
||||
GPG_RPM_PRIVATE_KEY: ${{ secrets.GPG_RPM_PRIVATE_KEY }}
|
||||
run: |
|
||||
echo "$GPG_RPM_PRIVATE_KEY" | base64 -d > /tmp/gpg-rpm-signing-key.asc
|
||||
echo "GPG_RPM_KEY_FILE=/tmp/gpg-rpm-signing-key.asc" >> $GITHUB_ENV
|
||||
|
||||
- name: Install wails3 CLI
|
||||
# Version derived from go.mod so the binding generator always matches
|
||||
# the wails runtime the binary links against.
|
||||
# -tags gtk3: the CLI links the wails runtime's cgo packages, and the
|
||||
# default tags request gtk4/webkitgtk-6.0 pkg-config entries that do
|
||||
# not exist on ubuntu-22.04.
|
||||
run: |
|
||||
WAILS_VERSION=$(go list -m -f '{{.Version}}' github.com/wailsapp/wails/v3)
|
||||
go install -tags gtk3 github.com/wailsapp/wails/v3/cmd/wails3@$WAILS_VERSION
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
|
||||
with:
|
||||
version: ${{ env.GORELEASER_VER }}
|
||||
args: release --config .goreleaser_ui_gtk3.yaml --clean ${{ env.flags }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||
GPG_RPM_KEY_FILE: ${{ env.GPG_RPM_KEY_FILE }}
|
||||
NFPM_NETBIRD_UI_RPM_GTK3_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }}
|
||||
SKIP_PUBLISH: ${{ env.SKIP_PUBLISH }}
|
||||
- name: Verify RPM signatures
|
||||
run: |
|
||||
docker run --rm -v $(pwd)/dist:/dist fedora:41 bash -c '
|
||||
dnf install -y -q rpm-sign curl >/dev/null 2>&1
|
||||
curl -sSL https://pkgs.netbird.io/yum/repodata/repomd.xml.key -o /tmp/rpm-pub.key
|
||||
rpm --import /tmp/rpm-pub.key
|
||||
echo "=== Verifying RPM signatures ==="
|
||||
for rpm_file in /dist/*.rpm; do
|
||||
[ -f "$rpm_file" ] || continue
|
||||
echo "--- $(basename $rpm_file) ---"
|
||||
rpm -K "$rpm_file"
|
||||
done
|
||||
'
|
||||
- name: Clean up GPG key
|
||||
if: always()
|
||||
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
||||
- name: upload non tags for debug purposes
|
||||
id: upload_release_ui_gtk3
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
|
||||
with:
|
||||
name: release-ui-gtk3
|
||||
path: dist/
|
||||
retention-days: 3
|
||||
|
||||
release_ui_darwin:
|
||||
runs-on: macos-latest
|
||||
outputs:
|
||||
@@ -688,7 +814,7 @@ jobs:
|
||||
comment_release_artifacts:
|
||||
name: Comment release artifacts
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release, release_ui, release_ui_darwin]
|
||||
needs: [release, release_ui, release_ui_gtk3, release_ui_darwin]
|
||||
if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -700,12 +826,14 @@ jobs:
|
||||
env:
|
||||
RELEASE_RESULT: ${{ needs.release.result }}
|
||||
RELEASE_UI_RESULT: ${{ needs.release_ui.result }}
|
||||
RELEASE_UI_GTK3_RESULT: ${{ needs.release_ui_gtk3.result }}
|
||||
RELEASE_UI_DARWIN_RESULT: ${{ needs.release_ui_darwin.result }}
|
||||
RELEASE_ARTIFACT_URL: ${{ needs.release.outputs.release_artifact_url }}
|
||||
LINUX_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.linux_packages_artifact_url }}
|
||||
WINDOWS_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.windows_packages_artifact_url }}
|
||||
MACOS_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.macos_packages_artifact_url }}
|
||||
RELEASE_UI_ARTIFACT_URL: ${{ needs.release_ui.outputs.release_ui_artifact_url }}
|
||||
RELEASE_UI_GTK3_ARTIFACT_URL: ${{ needs.release_ui_gtk3.outputs.release_ui_gtk3_artifact_url }}
|
||||
RELEASE_UI_DARWIN_ARTIFACT_URL: ${{ needs.release_ui_darwin.outputs.release_ui_darwin_artifact_url }}
|
||||
GHCR_IMAGES_MARKDOWN: ${{ needs.release.outputs.ghcr_images }}
|
||||
with:
|
||||
@@ -728,6 +856,7 @@ jobs:
|
||||
['Windows packages', process.env.WINDOWS_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
|
||||
['macOS packages', process.env.MACOS_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
|
||||
['UI artifacts', process.env.RELEASE_UI_ARTIFACT_URL, process.env.RELEASE_UI_RESULT],
|
||||
['UI GTK3 artifacts', process.env.RELEASE_UI_GTK3_ARTIFACT_URL, process.env.RELEASE_UI_GTK3_RESULT],
|
||||
['UI macOS artifacts', process.env.RELEASE_UI_DARWIN_ARTIFACT_URL, process.env.RELEASE_UI_DARWIN_RESULT],
|
||||
];
|
||||
|
||||
@@ -784,7 +913,7 @@ jobs:
|
||||
|
||||
trigger_signer:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release, release_ui, release_ui_darwin, test_windows_installer]
|
||||
needs: [release, release_ui, release_ui_gtk3, release_ui_darwin, test_windows_installer]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Trigger binaries sign pipelines
|
||||
|
||||
@@ -257,6 +257,15 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify fresh-install session cookie key hardening
|
||||
run: |
|
||||
grep -Fxq ' SESSION_COOKIE_ENCRYPTION_KEY=$(openssl rand -base64 32)' infrastructure_files/getting-started.sh
|
||||
grep -Fxq ' sessionCookieEncryptionKey: "$SESSION_COOKIE_ENCRYPTION_KEY"' infrastructure_files/getting-started.sh
|
||||
grep -Fxq ' install -m 600 /dev/null config.yaml' infrastructure_files/getting-started.sh
|
||||
grep -Fxq ' openssl rand -base64 32' infrastructure_files/getting-started-enterprise.sh
|
||||
grep -Fxq ' NETBIRD_SESSION_COOKIE_ENCRYPTION_KEY=$(rand_b64_key)' infrastructure_files/getting-started-enterprise.sh
|
||||
grep -Fxq ' sessionCookieEncryptionKey: "${NETBIRD_SESSION_COOKIE_ENCRYPTION_KEY}"' infrastructure_files/getting-started-enterprise.sh
|
||||
|
||||
- name: Verify Dex retirement notice
|
||||
run: |
|
||||
if infrastructure_files/getting-started-with-dex.sh >stdout.txt 2>stderr.txt; then
|
||||
|
||||
131
.goreleaser_ui_gtk3.yaml
Normal file
131
.goreleaser_ui_gtk3.yaml
Normal file
@@ -0,0 +1,131 @@
|
||||
version: 2
|
||||
env:
|
||||
- SKIP_PUBLISH={{ if index .Env "SKIP_PUBLISH" }}{{ .Env.SKIP_PUBLISH }}{{ else }}true{{ end }}
|
||||
project_name: netbird-ui
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# Bindings are gitignored; regenerate before the frontend build so
|
||||
# the @wailsio/runtime Vite plugin can resolve them (vite refuses to
|
||||
# build without them).
|
||||
# -f '-tags gtk3': the generator type-checks client/ui, whose cgo imports
|
||||
# would otherwise resolve gtk4/webkitgtk-6.0 pkg-config entries that do
|
||||
# not exist on ubuntu-22.04.
|
||||
- sh -c 'cd client/ui && wails3 generate bindings -clean=true -ts -f "-tags gtk3"'
|
||||
- sh -c 'cd client/ui/frontend && pnpm install --frozen-lockfile && pnpm build'
|
||||
|
||||
builds:
|
||||
# Legacy GTK3 / WebKit2GTK 4.1 build for distros without WebKitGTK 6.0
|
||||
# (Ubuntu 22.04, Debian 12, RHEL 9, Fedora <=39). The gtk3 tag flips the
|
||||
# Wails Linux backend to the GTK3 stack and swaps our GTK4-only XEmbed
|
||||
# tray host for the pure-Go stub (client/ui/xembed_host_gtk3_linux.go).
|
||||
# Must be built on the oldest supported glibc (ubuntu-22.04 runner).
|
||||
- id: netbird-ui-gtk3
|
||||
dir: client/ui
|
||||
binary: netbird-ui
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
ldflags:
|
||||
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
tags:
|
||||
- production
|
||||
- gtk3
|
||||
|
||||
archives:
|
||||
- id: linux-gtk3-arch
|
||||
name_template: "{{ .ProjectName }}-linux-gtk3_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
builds:
|
||||
- netbird-ui-gtk3
|
||||
|
||||
nfpms:
|
||||
# Same package_name as the GTK4 packages -- the two are mutually-exclusive
|
||||
# alternatives served from separate repo paths (see uploads below); a given
|
||||
# distro points at exactly one of them. The file names must still differ:
|
||||
# the Debian pool is shared storage keyed by file name, so a default-named
|
||||
# gtk3 .deb would overwrite the stable one.
|
||||
- maintainer: Netbird <dev@netbird.io>
|
||||
description: Netbird client UI.
|
||||
homepage: https://netbird.io/
|
||||
license: BSD-3-Clause
|
||||
vendor: NetBird
|
||||
id: netbird_ui_deb_gtk3
|
||||
package_name: netbird-ui
|
||||
file_name_template: "{{ .PackageName }}-gtk3_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
builds:
|
||||
- netbird-ui-gtk3
|
||||
formats:
|
||||
- deb
|
||||
scripts:
|
||||
postinstall: "release_files/ui-post-install.sh"
|
||||
contents:
|
||||
- src: client/ui/build/linux/netbird.desktop
|
||||
dst: /usr/share/applications/org.wails.netbird.desktop
|
||||
- src: client/ui/build/appicon.png
|
||||
dst: /usr/share/pixmaps/netbird.png
|
||||
dependencies:
|
||||
- netbird (>= 0.75.0)
|
||||
- libgtk-3-0
|
||||
- libwebkit2gtk-4.1-0
|
||||
|
||||
- maintainer: Netbird <dev@netbird.io>
|
||||
description: Netbird client UI.
|
||||
homepage: https://netbird.io/
|
||||
license: BSD-3-Clause
|
||||
vendor: NetBird
|
||||
id: netbird_ui_rpm_gtk3
|
||||
package_name: netbird-ui
|
||||
file_name_template: "{{ .PackageName }}-gtk3_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
builds:
|
||||
- netbird-ui-gtk3
|
||||
formats:
|
||||
- rpm
|
||||
scripts:
|
||||
postinstall: "release_files/ui-post-install.sh"
|
||||
contents:
|
||||
- src: client/ui/build/linux/netbird.desktop
|
||||
dst: /usr/share/applications/org.wails.netbird.desktop
|
||||
- src: client/ui/build/appicon.png
|
||||
dst: /usr/share/pixmaps/netbird.png
|
||||
dependencies:
|
||||
- netbird >= 0.75.0
|
||||
- (gtk3 or libgtk-3-0)
|
||||
- (webkit2gtk4.1 or libwebkit2gtk-4_1-0)
|
||||
|
||||
rpm:
|
||||
signature:
|
||||
key_file: '{{ if index .Env "GPG_RPM_KEY_FILE" }}{{ .Env.GPG_RPM_KEY_FILE }}{{ end }}'
|
||||
|
||||
# The GTK4 UI job shares project_name, so the default checksum file name would
|
||||
# collide with it on the shared GitHub release.
|
||||
checksum:
|
||||
name_template: "{{ .ProjectName }}_gtk3_checksums.txt"
|
||||
|
||||
changelog:
|
||||
disable: true
|
||||
|
||||
uploads:
|
||||
# The gtk3 packages reuse the netbird-ui package name, so they live in
|
||||
# dedicated repo paths (deb distribution `gtk3`, yum path `yum-gtk3`) that
|
||||
# legacy distros point their repo config at.
|
||||
- name: debian-gtk3
|
||||
skip: "{{ .Env.SKIP_PUBLISH }}"
|
||||
ids:
|
||||
- netbird_ui_deb_gtk3
|
||||
mode: archive
|
||||
target: https://pkgs.wiretrustee.com/debian/pool/{{ .ArtifactName }};deb.distribution=gtk3;deb.component=main;deb.architecture={{ if .Arm }}armhf{{ else }}{{ .Arch }}{{ end }};deb.package=
|
||||
username: dev@wiretrustee.com
|
||||
method: PUT
|
||||
|
||||
- name: yum-gtk3
|
||||
skip: "{{ .Env.SKIP_PUBLISH }}"
|
||||
ids:
|
||||
- netbird_ui_rpm_gtk3
|
||||
mode: archive
|
||||
target: https://pkgs.wiretrustee.com/yum-gtk3/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
||||
username: dev@wiretrustee.com
|
||||
method: PUT
|
||||
317
AGENTS.md
317
AGENTS.md
@@ -14,20 +14,22 @@ in this file, not duplicated there.
|
||||
|
||||
## Contents
|
||||
|
||||
- [NetBird Agent Guidelines](#netbird-agent-guidelines)
|
||||
- [Contents](#contents)
|
||||
- [STOP and ask the user before](#stop-and-ask-the-user-before)
|
||||
- [Quick reference](#quick-reference)
|
||||
- [Structure](#structure)
|
||||
- [Where to look](#where-to-look)
|
||||
- [Repo-wide principles](#repo-wide-principles)
|
||||
- [Error handling](#error-handling)
|
||||
- [Comments](#comments)
|
||||
- [Testing](#testing)
|
||||
- [Pitfalls](#pitfalls)
|
||||
- [Commits, PRs, releases](#commits-prs-releases)
|
||||
- [After you push: CI and review bots](#after-you-push-ci-and-review-bots)
|
||||
- [Discussion and support](#discussion-and-support)
|
||||
- [STOP and ask the user before](#stop-and-ask-the-user-before)
|
||||
- [Quick reference](#quick-reference)
|
||||
- [Structure](#structure)
|
||||
- [Where to look](#where-to-look)
|
||||
- [Security](#security)
|
||||
- [Agent conventions](#agent-conventions)
|
||||
- [Repo-wide principles](#repo-wide-principles)
|
||||
- [Type safety](#type-safety)
|
||||
- [Concurrency and lifecycle](#concurrency-and-lifecycle)
|
||||
- [Error handling](#error-handling)
|
||||
- [Comments](#comments)
|
||||
- [Testing](#testing)
|
||||
- [Pitfalls](#pitfalls)
|
||||
- [Commits, PRs, releases](#commits-prs-releases)
|
||||
- [After you push: CI and review bots](#after-you-push-ci-and-review-bots)
|
||||
- [Discussion and support](#discussion-and-support)
|
||||
|
||||
## STOP and ask the user before
|
||||
|
||||
@@ -157,11 +159,125 @@ netbird/
|
||||
| LLM routing / Agent Network | `proxy/internal/llm/`, `agent-network/` |
|
||||
| End-to-end tests | `e2e/` |
|
||||
|
||||
## Security
|
||||
|
||||
### Never fail open
|
||||
|
||||
When a security check — access control, an IP restriction, an auth decision —
|
||||
hits an error such as an unparseable value, an unavailable lookup, or a state it
|
||||
does not recognize, it must **deny**. Never skip the check or allow the request
|
||||
through because the check itself failed, and make the `default` and unknown cases
|
||||
of a security-related `switch` deny rather than fall through.
|
||||
|
||||
### Daemon RPC input is untrusted
|
||||
|
||||
The agent runs as root (LocalSystem on Windows), so a daemon RPC crosses a
|
||||
privilege boundary: treat every field as untrusted input rather than as something
|
||||
the UI or CLI validated on the way in.
|
||||
|
||||
When you add or change an RPC, ask what the handler does with caller input while
|
||||
running as root. If the answer touches a filesystem path, a URL or host, or a
|
||||
privileged state change, it needs a gate **in the handler** — a check in the client
|
||||
that normally calls it is not a check at all.
|
||||
|
||||
- **A caller-supplied path the daemon opens.** Never `os.Open` it as root.
|
||||
Constrain it, then open it *as the caller* with `ipcauth.OpenOwnedFile`, which
|
||||
opens `O_NOFOLLOW`, requires a regular file, and refuses a file the caller does
|
||||
not own — so a symlink or hardlink aimed at a root-only file is rejected.
|
||||
- **A caller-supplied URL or host the daemon fetches.** Restrict the scheme and
|
||||
allow only known hosts for unprivileged callers. Prefer a lexical host
|
||||
allowlist plus TLS verification over "resolve the host, then reject private
|
||||
IPs": the resolve-then-trust pattern has a DNS-rebinding race (public IP at
|
||||
check time, attacker IP at connect time), while a name allowlist has no IP
|
||||
check to race. Never accept `http://` where `https://` is expected.
|
||||
- **A privileged state change** (SSH root login, management URL, deregistration)
|
||||
gates on the caller identity from `ipcauth.CallerIdentity(ctx)`.
|
||||
|
||||
Caller identity comes from the kernel — `SO_PEERCRED`, `LOCAL_PEERCRED`, or the
|
||||
named-pipe client token — and never from an RPC field. When
|
||||
`ipcauth.CallerIdentity` reports that it could not determine an identity, **deny**;
|
||||
do not fall back to treating the caller as the transport peer.
|
||||
|
||||
## Agent conventions
|
||||
|
||||
### Three networking modes
|
||||
|
||||
Where packets actually flow depends on the mode the agent is running in. The
|
||||
three are not interchangeable, so establish which one a change applies to — and
|
||||
what it should do in the other two — before you write it.
|
||||
|
||||
- **kernel mode** (Linux only): in-kernel WireGuard®. The kernel handles both
|
||||
peer-to-peer and routed traffic, and ACLs are iptables or nftables rules. The
|
||||
client programs kernel facilities but never sees the traffic itself.
|
||||
- **userspace mode** (wireguard-go with a TUN): wireguard-go runs in-process. The
|
||||
kernel handles peer-to-peer traffic once it leaves the TUN, while routed traffic
|
||||
— exit nodes and network routes — goes through the userspace forwarder, which
|
||||
terminates the connection and re-establishes it over OS sockets. Used on
|
||||
platforms without kernel WireGuard® or when the user opts out.
|
||||
- **netstack mode**: wireguard-go in-process with no TUN and no kernel
|
||||
networking. The forwarder does all routing by stitching userspace sockets, and
|
||||
listeners such as the embedded SSH and DNS servers bind on a gVisor netstack.
|
||||
Used where the process cannot create a TUN device, such as the embedded client
|
||||
(`client/embed/`) and the WASM build.
|
||||
|
||||
### The overlay interface is not "WireGuard"
|
||||
|
||||
Do not put "WireGuard" in identifiers or comments unless the code is genuinely
|
||||
coupled to WireGuard® specifically — a wireguard-go call, a handshake field, a
|
||||
kernel WireGuard® netlink attribute. For the interface, the host, peers, or
|
||||
traffic in general, say "the NetBird interface", "the interface", or "the overlay".
|
||||
Most firewall, routing, and DNS code is transport-agnostic, so a WireGuard®
|
||||
reference there is simply inaccurate and rots as the transports change.
|
||||
|
||||
### IPv6 is a soft feature
|
||||
|
||||
The IPv6 overlay is opt-in dual-stack, and capability can change at runtime. Treat
|
||||
it as soft rather than a requirement:
|
||||
|
||||
- Gate local v6 paths on the interface accessor (`wgIface.Address().HasIPv6()`),
|
||||
not on raw state fields, and skip the v6 path when the host has no v6 rather
|
||||
than returning an error.
|
||||
- Treat an empty or unparseable peer v6 address as "no v6 for that peer" and skip
|
||||
it, keeping the v4 path working.
|
||||
- Never let a missing v6 break v4. Fail-closed is for security checks; a
|
||||
capability mismatch skips the v6 work and carries on.
|
||||
|
||||
### Environment variables
|
||||
|
||||
Name the variable in a constant and parse booleans with `strconv.ParseBool` rather
|
||||
than comparing strings inline, so an unexpected value is logged instead of
|
||||
silently meaning false:
|
||||
|
||||
```go
|
||||
const EnvDisableFeature = "NB_DISABLE_FEATURE"
|
||||
|
||||
func isDisabledByEnv() bool {
|
||||
val := os.Getenv(EnvDisableFeature)
|
||||
if val == "" {
|
||||
return false
|
||||
}
|
||||
disabled, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
log.Warnf("failed to parse %s: %v", EnvDisableFeature, err)
|
||||
return false
|
||||
}
|
||||
return disabled
|
||||
}
|
||||
```
|
||||
|
||||
### Validating against protocol specs
|
||||
|
||||
When a change depends on what a protocol actually mandates, read the specification
|
||||
text from the [IETF datatracker](https://datatracker.ietf.org/) rather than a
|
||||
summary, and check that you have the current RFC — the widely cited one for a
|
||||
protocol is often superseded. Cite the section, not just the document, so a
|
||||
reviewer can jump straight to the rule.
|
||||
|
||||
## Repo-wide principles
|
||||
|
||||
1. **Run `go fmt` on every modified Go file.** Formatting is not optional.
|
||||
2. **Zero unaddressed diagnostics.** Fix IDE and linter warnings on code you
|
||||
touch, and delete imports, helpers, and parameters your refactor orphaned.
|
||||
2. **Zero unaddressed linter warnings.** Fix what `golangci-lint` reports on code
|
||||
you touch, and delete imports, helpers, and parameters your refactor orphaned.
|
||||
Exception: unused parameters in shared code may be consumed by builds outside
|
||||
this repository — do not remove them, ask instead.
|
||||
3. **Function comments are mandatory for exported functions**, written as full
|
||||
@@ -175,9 +291,12 @@ netbird/
|
||||
7. **Avoid LLM-slop tells:** em dashes, hedging narration, restating the diff in
|
||||
prose, trailing summaries. Defaults, not absolute bans. Applies to code,
|
||||
comments, commit messages, and PR descriptions alike.
|
||||
8. **Concurrency: do a two-pass race analysis after every change** that adds
|
||||
shared state. Guard maps and slices with a mutex, keep critical sections
|
||||
short, and run `go test -race` on the touched packages.
|
||||
8. **Concurrency: do a two-pass race analysis after every change** that touches
|
||||
shared state, including reads of existing maps and slices. Guard them with a
|
||||
mutex (or an atomic or channel where that fits better), keep critical
|
||||
sections short, and run `go test -race` on the touched packages. See
|
||||
[Concurrency and lifecycle](#concurrency-and-lifecycle) for the failure modes
|
||||
to check for.
|
||||
9. **Cross-platform builds must keep working.** The agent targets Linux, macOS,
|
||||
Windows, FreeBSD, Android, and iOS. When you add a platform-specific file,
|
||||
add the counterpart or a build-tagged fallback for the others.
|
||||
@@ -185,6 +304,93 @@ netbird/
|
||||
11. **Never log secrets** — private keys, setup keys, tokens, PAT values — and
|
||||
keep peer IPs and hostnames out of logs above debug level.
|
||||
|
||||
## Type safety
|
||||
|
||||
**No bare primitives for domain concepts.** A `string` parameter for an account
|
||||
ID next to a `string` parameter for a peer ID is two bugs waiting to happen,
|
||||
because the compiler cannot catch the swap. Declare the type once and use it
|
||||
throughout, converting only at the boundaries where data enters or leaves —
|
||||
protobuf, gRPC, HTTP, an external library.
|
||||
|
||||
```go
|
||||
type ServiceID string
|
||||
type AccountID string
|
||||
|
||||
// Internal: typed all the way through
|
||||
func (r *Router) RemoveRoute(host SNIHost, svcID ServiceID) { ... }
|
||||
|
||||
// Proto boundary: convert once, on the way in and on the way out
|
||||
svcID := ServiceID(mapping.GetId())
|
||||
req.ServiceId = string(svcID)
|
||||
```
|
||||
|
||||
- **IP addresses are `netip.Addr`**, not `string` and not `net.IP`. Parse at the
|
||||
boundary and pass the typed value inward.
|
||||
- **Always `Unmap()`** after parsing an address, after converting from `net.IP`,
|
||||
and after extracting one from `RemoteAddr()`. This normalizes a v4-mapped v6
|
||||
address (`::ffff:10.1.2.3`) to plain v4 so IPv4 rules match it. A stored or
|
||||
compared mapped address silently fails to match those rules.
|
||||
- **Ports are `uint16`** internally; use `int` only where a library forces it and
|
||||
convert immediately.
|
||||
- **Enums are a typed string with constants**, so the valid set is discoverable
|
||||
and a typo fails to compile.
|
||||
- **Map keys follow the same rule**, and must be a real type (`type ServiceID
|
||||
string`) rather than an alias (`type serviceID = string`) — an alias silently
|
||||
accepts bare strings.
|
||||
|
||||
## Concurrency and lifecycle
|
||||
|
||||
Beyond the mutex hygiene in the principles above, check for these failure
|
||||
modes.
|
||||
|
||||
- **Never read a struct field inside a goroutine** when another goroutine may nil
|
||||
or reassign it. Pass the value as a parameter, or capture it into a local before
|
||||
launching. This matters most when `Stop()` nils a field without waiting for the
|
||||
goroutine to finish.
|
||||
|
||||
```go
|
||||
go func(ifaceName string) { // good: passed in, cannot be nilled underneath
|
||||
m.Start(ctx, ifaceName)
|
||||
}(iface.Name())
|
||||
```
|
||||
|
||||
- **Never wait on a channel while holding a lock the sender needs.** Copy what you
|
||||
need out from under the lock, release it, then wait.
|
||||
|
||||
```go
|
||||
func (m *Manager) Stop() {
|
||||
m.mu.Lock()
|
||||
cancel, done := m.cancel, m.done
|
||||
m.mu.Unlock()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
<-done
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`Stop`/`Close` must be idempotent** — guard on an already-stopped flag or a
|
||||
nil cancel — and must release the state they guarded. Clear maps and caches;
|
||||
a cancelled goroutine holding a live map still pins that memory. Note that a
|
||||
nil map only panics on writes; reads and iteration behave like an empty map,
|
||||
so where post-close use must be rejected, check the stopped flag explicitly.
|
||||
- **Publish coupled state only after every fallible step succeeds.** When several
|
||||
fields form an invariant, build them into locals and assign them to the receiver
|
||||
at the end. Assigning as you go leaves the object half-initialized when a later
|
||||
step fails, so a readiness predicate reports ready while a coupled field is nil.
|
||||
If an earlier step already had an external side effect — a created chain, an
|
||||
opened handle, an inserted rule — roll it back before returning the error.
|
||||
- **Clean up what you own on constructor error paths.** Once a constructor has
|
||||
started something, every later error path must undo it: cancel a goroutine and
|
||||
wait for it to exit, stop a ticker, close a watcher. The object is never
|
||||
returned, so its `Close` will never run.
|
||||
- **A failed `Start` must undo everything it started.** When a component brings up
|
||||
several subsystems in sequence — connection manager, watchers, routing, DNS,
|
||||
flow, persisted state — a failure partway through has to tear down the ones
|
||||
already running, not just close the handle the error came from. Put the
|
||||
already-started guard *before* that teardown path, so a rejected second `Start`
|
||||
cannot dismantle the one that is running.
|
||||
|
||||
## Error handling
|
||||
|
||||
Use single-assignment form when the error is only needed inside the `if`:
|
||||
@@ -248,6 +454,45 @@ Log the errors you choose not to act on:
|
||||
- Close errors may be ignored for read-only operations; log them at debug for
|
||||
writes.
|
||||
|
||||
**Do not log and return the same error.** It gets reported twice, from two places,
|
||||
and the second reader cannot tell whether it happened once or twice. Return it and
|
||||
let the caller decide. The exception is an API handler that has already written a
|
||||
response. Internal helpers return errors rather than logging and swallowing them.
|
||||
|
||||
**Never return a typed nil as an error.** A nil `*MyError` stored in an `error`
|
||||
interface is not nil, so `err != nil` is true and callers take the failure path on
|
||||
success. Return the error only where it is actually set:
|
||||
|
||||
```go
|
||||
if _, err := conn.Write(buf); err != nil { // good
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
```
|
||||
|
||||
**Accumulate with `multierror` when an operation should continue past individual
|
||||
failures** — teardown, cleanup, or setup where partial success is acceptable.
|
||||
`client/errors.FormatErrorOrNil` returns nil for an empty accumulator, so callers
|
||||
still see a plain nil on full success:
|
||||
|
||||
```go
|
||||
func (m *Manager) Cleanup() error {
|
||||
var merr *multierror.Error
|
||||
for _, r := range m.resources {
|
||||
if err := r.Close(); err != nil {
|
||||
merr = multierror.Append(merr, fmt.Errorf("close %s: %w", r.Name, err))
|
||||
}
|
||||
}
|
||||
return nberrors.FormatErrorOrNil(merr)
|
||||
}
|
||||
```
|
||||
|
||||
| Scenario | Approach | Why |
|
||||
| --------------------- | --------------------- | ----------------------------------------- |
|
||||
| Cleanup / teardown | Accumulate | Clean up as much as possible |
|
||||
| Setup with rollback | Abort on first error | Partial state is invalid; undo what stuck |
|
||||
| Setup with partial OK | Accumulate | Degraded operation is still useful |
|
||||
|
||||
## Comments
|
||||
|
||||
Comment the **why**, never the **what**. Default to no comment, and add one only
|
||||
@@ -269,10 +514,14 @@ checksum = updateChecksum(checksum, oldPort, newPort)
|
||||
|
||||
### Length budget
|
||||
|
||||
- **90 characters per line.** Wrap the comment, do not run past it.
|
||||
- **250 characters per comment**, roughly three wrapped lines. Doc comments on
|
||||
exported identifiers may exceed it when the API genuinely needs the
|
||||
explanation; inline comments inside a function body may not.
|
||||
Neither of these is linter-enforced, so they are conventions the surrounding code
|
||||
mostly follows rather than hard limits:
|
||||
|
||||
- **Around 90 characters per line.** Wrap the comment rather than running well past
|
||||
it.
|
||||
- **Roughly 250 characters per comment**, about three wrapped lines. Doc comments
|
||||
on exported identifiers may exceed it when the API genuinely needs the
|
||||
explanation; inline comments inside a function body rarely should.
|
||||
|
||||
The budget is a smell detector, not a rule to game. Do not compress a needed
|
||||
explanation into cryptic shorthand to fit — if a block of code needs more than
|
||||
@@ -329,6 +578,19 @@ up, and the 250-character budget does not apply to them.
|
||||
otherwise.
|
||||
- **Message guidance:** optional for `NoError`/`Error`; always give context for
|
||||
comparison, boolean, and collection assertions.
|
||||
- **Reproduce a bug before fixing it.** Write the test, watch it fail *for the
|
||||
reason you expect* — a test that fails for an unrelated reason proves nothing —
|
||||
then apply the fix and confirm it passes. Add the thin surrounding cases while
|
||||
you are there.
|
||||
- **Use `t.Setenv`** rather than `os.Setenv` so the previous value is restored on
|
||||
cleanup. To test the unset case, call `t.Setenv` first to register the restore,
|
||||
then `os.Unsetenv`.
|
||||
- **Prefer `t.Cleanup` over `defer`** in any test with parallel subtests: the
|
||||
parent function returns, running its `defer`s, while parallel subtests are
|
||||
still suspended. Sequential subtests finish inside `t.Run`, so `defer` is safe
|
||||
there, but `t.Cleanup` works in both cases.
|
||||
- **Explanatory comments in tests are welcome.** Describe the scenario being set
|
||||
up; the comment budget below does not apply to them.
|
||||
|
||||
```go
|
||||
server, err := StartTestServer()
|
||||
@@ -380,7 +642,8 @@ assert.Equal(t, expectedResult, result, "Result should match expected")
|
||||
than replacing it with your own summary: describe the change, link the issue,
|
||||
tick the checklist honestly (including "ran locally" and "single purpose"),
|
||||
and complete the documentation section. Do not tick a box you have not
|
||||
verified, and do not delete rows that do not apply.
|
||||
verified, and do not delete rows that do not apply — the docs gate in CI reads
|
||||
that section and fails when it is missing.
|
||||
|
||||
- **Keep the PR description short.** Under 1000 words on top of the template's
|
||||
own text, and usually far less — a few paragraphs. Reviewers read the diff;
|
||||
@@ -439,6 +702,12 @@ assert.Equal(t, expectedResult, result, "Result should match expected")
|
||||
on their own. Propose that split to the user rather than opening one large PR
|
||||
and hoping.
|
||||
|
||||
Prefer GitHub's stacked pull requests for such a sequence, rather than
|
||||
hand-managing base branches: open each PR against the branch below it instead of
|
||||
`main`, so every PR's diff shows only its own change. Merging a layer retargets
|
||||
the PRs above it, and branch protections and required checks on the base branch
|
||||
still apply to each one.
|
||||
|
||||
- **User-facing changes need a docs PR** in
|
||||
[netbirdio/docs](https://github.com/netbirdio/docs), linked from the PR
|
||||
description.
|
||||
|
||||
@@ -16,17 +16,17 @@ func TestProfileAccountPathFor(t *testing.T) {
|
||||
{
|
||||
name: "default profile",
|
||||
configPath: "/data/data/io.netbird.client/files/netbird.cfg",
|
||||
want: "/data/data/io.netbird.client/files/netbird.account.json",
|
||||
want: filepath.FromSlash("/data/data/io.netbird.client/files/netbird.account.json"),
|
||||
},
|
||||
{
|
||||
name: "id profile",
|
||||
configPath: "/data/data/io.netbird.client/files/profiles/4c5f5c8198c3989cffb5b5394f5a7ae0.json",
|
||||
want: "/data/data/io.netbird.client/files/profiles/4c5f5c8198c3989cffb5b5394f5a7ae0.account.json",
|
||||
want: filepath.FromSlash("/data/data/io.netbird.client/files/profiles/4c5f5c8198c3989cffb5b5394f5a7ae0.account.json"),
|
||||
},
|
||||
{
|
||||
name: "legacy name-keyed profile is handled the same way",
|
||||
configPath: "/data/data/io.netbird.client/files/profiles/work.json",
|
||||
want: "/data/data/io.netbird.client/files/profiles/work.account.json",
|
||||
want: filepath.FromSlash("/data/data/io.netbird.client/files/profiles/work.account.json"),
|
||||
},
|
||||
{
|
||||
name: "empty path is rejected",
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
// agentNetworkAuthToken is the placeholder credential exported for
|
||||
// AI-tool CLIs: the Agent Network proxy authenticates callers by tunnel
|
||||
// peer and injects the real upstream credentials itself, so the
|
||||
// client-side token only needs to satisfy the tool's non-empty check.
|
||||
const agentNetworkAuthToken = "netbird"
|
||||
|
||||
var (
|
||||
agentNetworkProviderFlag string
|
||||
agentNetworkModelFlag string
|
||||
agentNetworkJSONFlag bool
|
||||
)
|
||||
|
||||
var agentNetworkCmd = &cobra.Command{
|
||||
Use: "agent-network",
|
||||
Short: "Show the Agent Network setup available to this peer",
|
||||
Long: `Commands to inspect the Agent Network (AI provider proxy) setup this peer's groups authorize:
|
||||
the proxy endpoint, the reachable providers, and the allowed models.`,
|
||||
}
|
||||
|
||||
var agentNetworkLsCmd = &cobra.Command{
|
||||
Use: "ls",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List the Agent Network endpoint, providers, and allowed models",
|
||||
Example: " netbird agent-network ls",
|
||||
RunE: agentNetworkLs,
|
||||
}
|
||||
|
||||
var agentNetworkEnvCmd = &cobra.Command{
|
||||
Use: "env",
|
||||
Short: "Print shell export lines that point AI tools at the Agent Network",
|
||||
Long: `Print POSIX shell export lines that configure AI tools to use the Agent Network proxy.
|
||||
The variables depend on the provider's API shape — Anthropic API, AWS Bedrock, Google Vertex AI,
|
||||
and OpenAI-compatible providers each get the environment their tools expect (for Claude Code,
|
||||
following its LLM-gateway configuration). Apply them to the current shell with:
|
||||
|
||||
eval "$(netbird agent-network env)"
|
||||
|
||||
When several providers are authorized, pass --provider to pick one; when several models are
|
||||
allowed, pass --model to pin one — nothing is ever guessed.`,
|
||||
Example: " eval \"$(netbird agent-network env)\"\n eval \"$(netbird agent-network env --provider 'Bedrock prod' --model anthropic.claude-sonnet-4-5)\"",
|
||||
RunE: agentNetworkEnv,
|
||||
}
|
||||
|
||||
func init() {
|
||||
agentNetworkLsCmd.PersistentFlags().BoolVar(&agentNetworkJSONFlag, "json", false, "output the setup as JSON")
|
||||
agentNetworkEnvCmd.PersistentFlags().StringVar(&agentNetworkProviderFlag, "provider", "", "provider to configure, by name or catalog id (required when several are authorized)")
|
||||
agentNetworkEnvCmd.PersistentFlags().StringVar(&agentNetworkModelFlag, "model", "", "model to export (required when several models are allowed)")
|
||||
}
|
||||
|
||||
// fetchAgentNetworkSetup asks the daemon for the caller-scoped Agent
|
||||
// Network setup. The daemon relays the request to management over its
|
||||
// existing peer connection, so no elevated permissions are needed.
|
||||
func fetchAgentNetworkSetup(cmd *cobra.Command) (*proto.GetAgentNetworkSetupResponse, error) {
|
||||
conn, err := getClient(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := proto.NewDaemonServiceClient(conn)
|
||||
setup, err := client.GetAgentNetworkSetup(cmd.Context(), &proto.GetAgentNetworkSetupRequest{})
|
||||
if err != nil {
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.Unimplemented {
|
||||
return nil, fmt.Errorf("the running daemon does not support agent-network commands — update the NetBird daemon and restart the service")
|
||||
}
|
||||
return nil, fmt.Errorf("get agent network setup: %v", status.Convert(err).Message())
|
||||
}
|
||||
return setup, nil
|
||||
}
|
||||
|
||||
func agentNetworkLs(cmd *cobra.Command, _ []string) error {
|
||||
setup, err := fetchAgentNetworkSetup(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if agentNetworkJSONFlag {
|
||||
out, err := protojson.MarshalOptions{Multiline: true, Indent: " "}.Marshal(setup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal setup: %v", err)
|
||||
}
|
||||
cmd.Println(string(out))
|
||||
return nil
|
||||
}
|
||||
|
||||
if !setup.Configured {
|
||||
cmd.Println("Agent Network is not available for this peer. Ask your administrator.")
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd.Printf("Agent Network endpoint: %s\n", setup.Endpoint)
|
||||
cmd.Println("(reachable while connected to NetBird)")
|
||||
for _, p := range setup.Providers {
|
||||
cmd.Println()
|
||||
cmd.Printf("%s (%s)\n", sanitizeOutput(p.Name), sanitizeOutput(providerFlavorLabel(p)))
|
||||
switch {
|
||||
case p.AllModelsAllowed && len(p.Models) == 0:
|
||||
cmd.Println(" All models allowed")
|
||||
case p.AllModelsAllowed:
|
||||
cmd.Println(" All models allowed, including:")
|
||||
printModels(cmd, p.Models)
|
||||
default:
|
||||
cmd.Println(" Allowed models:")
|
||||
printModels(cmd, p.Models)
|
||||
}
|
||||
}
|
||||
cmd.Println()
|
||||
cmd.Println("To configure an AI tool in the current shell: eval \"$(netbird agent-network env)\"")
|
||||
return nil
|
||||
}
|
||||
|
||||
func printModels(cmd *cobra.Command, models []string) {
|
||||
if len(models) == 0 {
|
||||
cmd.Println(" (none)")
|
||||
return
|
||||
}
|
||||
for _, m := range models {
|
||||
cmd.Printf(" %s\n", sanitizeOutput(m))
|
||||
}
|
||||
}
|
||||
|
||||
func providerFlavorLabel(p *proto.AgentNetworkProvider) string {
|
||||
if p.ApiFlavor == "" {
|
||||
return p.CatalogId
|
||||
}
|
||||
return fmt.Sprintf("%s · %s-flavor API", p.CatalogId, p.ApiFlavor)
|
||||
}
|
||||
|
||||
func agentNetworkEnv(cmd *cobra.Command, _ []string) error {
|
||||
setup, err := fetchAgentNetworkSetup(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !setup.Configured {
|
||||
// An answer, not an error: print nothing eval-able and say why on
|
||||
// stderr so `eval "$(...)"` stays a harmless no-op.
|
||||
cmd.PrintErrln("Agent Network is not available for this peer. Ask your administrator.")
|
||||
return nil
|
||||
}
|
||||
|
||||
lines, err := buildAgentNetworkEnv(setup, agentNetworkProviderFlag, agentNetworkModelFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, line := range lines {
|
||||
cmd.Println(line)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildAgentNetworkEnv renders the export lines for one selected
|
||||
// provider. Nothing is guessed: an ambiguous provider or model choice
|
||||
// comes back as comment lines instead of exports, and an invalid
|
||||
// --provider/--model is an error.
|
||||
func buildAgentNetworkEnv(setup *proto.GetAgentNetworkSetupResponse, providerFlag, modelFlag string) ([]string, error) {
|
||||
provider, choiceLines, err := selectAgentNetworkProvider(setup.Providers, providerFlag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if provider == nil {
|
||||
return choiceLines, nil
|
||||
}
|
||||
|
||||
model, modelNotes, err := resolveAgentNetworkModel(provider, modelFlag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lines []string
|
||||
switch {
|
||||
case provider.CatalogId == "bedrock_api":
|
||||
// Claude Code's Bedrock-format gateway configuration: the proxy
|
||||
// routes native Bedrock paths and injects the AWS credentials, so
|
||||
// client-side signing is skipped.
|
||||
lines = append(lines,
|
||||
exportLine("CLAUDE_CODE_USE_BEDROCK", "1"),
|
||||
exportLine("ANTHROPIC_BEDROCK_BASE_URL", setup.Endpoint),
|
||||
exportLine("CLAUDE_CODE_SKIP_BEDROCK_AUTH", "1"),
|
||||
)
|
||||
if model != "" {
|
||||
lines = append(lines, exportLine("ANTHROPIC_MODEL", model))
|
||||
}
|
||||
case provider.CatalogId == "vertex_ai_api":
|
||||
// Claude Code's Vertex-format gateway configuration. Vertex
|
||||
// requests carry the GCP project and region in the URL path, which
|
||||
// the proxy forwards to the upstream — those two values belong to
|
||||
// the operator's GCP setup and must come from the administrator.
|
||||
lines = append(lines,
|
||||
exportLine("CLAUDE_CODE_USE_VERTEX", "1"),
|
||||
exportLine("ANTHROPIC_VERTEX_BASE_URL", setup.Endpoint),
|
||||
exportLine("CLAUDE_CODE_SKIP_VERTEX_AUTH", "1"),
|
||||
)
|
||||
if model != "" {
|
||||
lines = append(lines, exportLine("ANTHROPIC_MODEL", model))
|
||||
}
|
||||
lines = append(lines,
|
||||
comment("Vertex requests carry your operator's GCP project and region in the URL."),
|
||||
comment("Ask your administrator for the values, then export:"),
|
||||
comment(" export ANTHROPIC_VERTEX_PROJECT_ID=<project>"),
|
||||
comment(" export CLOUD_ML_REGION=<region>"),
|
||||
)
|
||||
case provider.ApiFlavor == "anthropic":
|
||||
lines = append(lines,
|
||||
exportLine("ANTHROPIC_BASE_URL", setup.Endpoint),
|
||||
exportLine("ANTHROPIC_AUTH_TOKEN", agentNetworkAuthToken),
|
||||
)
|
||||
if model != "" {
|
||||
lines = append(lines, exportLine("ANTHROPIC_MODEL", model))
|
||||
}
|
||||
case provider.ApiFlavor == "openai":
|
||||
lines = append(lines,
|
||||
exportLine("OPENAI_BASE_URL", setup.Endpoint),
|
||||
exportLine("OPENAI_API_KEY", agentNetworkAuthToken),
|
||||
)
|
||||
if model != "" {
|
||||
lines = append(lines, comment(fmt.Sprintf("Configure your tool to use model %s.", model)))
|
||||
}
|
||||
default:
|
||||
lines = append(lines,
|
||||
comment(fmt.Sprintf("Provider %s (%s) is dispatched by URL path; no standard environment", provider.Name, provider.CatalogId)),
|
||||
comment("variables apply. Point your tool at the endpoint below (auth token: netbird):"),
|
||||
comment(" "+setup.Endpoint),
|
||||
)
|
||||
}
|
||||
|
||||
for _, note := range modelNotes {
|
||||
lines = append(lines, comment(note))
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
// selectAgentNetworkProvider picks the provider to configure. An
|
||||
// explicit --provider matches the operator label or catalog id
|
||||
// (case-insensitive); with no flag a single authorized provider is
|
||||
// used, and several come back as comment lines asking for the flag.
|
||||
func selectAgentNetworkProvider(providers []*proto.AgentNetworkProvider, providerFlag string) (*proto.AgentNetworkProvider, []string, error) {
|
||||
if providerFlag != "" {
|
||||
wanted := strings.ToLower(strings.TrimSpace(providerFlag))
|
||||
for _, p := range providers {
|
||||
if strings.ToLower(strings.TrimSpace(p.Name)) == wanted || strings.ToLower(p.CatalogId) == wanted {
|
||||
return p, nil, nil
|
||||
}
|
||||
}
|
||||
names := make([]string, 0, len(providers))
|
||||
for _, p := range providers {
|
||||
names = append(names, fmt.Sprintf("%s (%s)", p.Name, p.CatalogId))
|
||||
}
|
||||
return nil, nil, fmt.Errorf("provider %q is not authorized for this peer — available: %s", providerFlag, strings.Join(names, ", "))
|
||||
}
|
||||
|
||||
if len(providers) == 1 {
|
||||
return providers[0], nil, nil
|
||||
}
|
||||
|
||||
lines := []string{comment("Multiple providers are authorized — none configured. Re-run with --provider to pick one:")}
|
||||
for _, p := range providers {
|
||||
lines = append(lines, comment(fmt.Sprintf(" netbird agent-network env --provider %q (%s)", p.Name, providerFlavorLabel(p))))
|
||||
}
|
||||
return nil, lines, nil
|
||||
}
|
||||
|
||||
// resolveAgentNetworkModel picks the model for the selected provider. A
|
||||
// model is never guessed: --model wins (validated against the allowed
|
||||
// set), a single allowed model is used, and anything ambiguous is
|
||||
// returned as note lines instead.
|
||||
func resolveAgentNetworkModel(provider *proto.AgentNetworkProvider, modelFlag string) (string, []string, error) {
|
||||
if modelFlag != "" {
|
||||
if provider.AllModelsAllowed {
|
||||
return modelFlag, nil, nil
|
||||
}
|
||||
wanted := strings.ToLower(strings.TrimSpace(modelFlag))
|
||||
for _, m := range provider.Models {
|
||||
if strings.ToLower(strings.TrimSpace(m)) == wanted {
|
||||
return modelFlag, nil, nil
|
||||
}
|
||||
}
|
||||
return "", nil, fmt.Errorf("model %q is not allowed on provider %s — run 'netbird agent-network ls' to see the allowed models", modelFlag, provider.Name)
|
||||
}
|
||||
|
||||
if len(provider.Models) == 1 && !provider.AllModelsAllowed {
|
||||
return provider.Models[0], nil, nil
|
||||
}
|
||||
if len(provider.Models) == 0 && provider.AllModelsAllowed {
|
||||
return "", []string{"Any model is allowed; pass --model to pin one."}, nil
|
||||
}
|
||||
|
||||
notes := []string{"Multiple models are allowed — none exported. Re-run with --model to pin one:"}
|
||||
for _, m := range provider.Models {
|
||||
notes = append(notes, " "+m)
|
||||
}
|
||||
if provider.AllModelsAllowed {
|
||||
notes = append(notes, " (any other model the provider serves)")
|
||||
}
|
||||
return "", notes, nil
|
||||
}
|
||||
|
||||
func exportLine(name, value string) string {
|
||||
return fmt.Sprintf("export %s=%s", name, shellQuote(value))
|
||||
}
|
||||
|
||||
func comment(text string) string {
|
||||
return "# " + sanitizeOutput(text)
|
||||
}
|
||||
|
||||
// shellQuote single-quotes a value for safe use in an eval'd export
|
||||
// line, escaping embedded single quotes.
|
||||
func shellQuote(v string) string {
|
||||
return "'" + strings.ReplaceAll(v, "'", `'\''`) + "'"
|
||||
}
|
||||
|
||||
// sanitizeOutput strips control characters (including newlines) from
|
||||
// server-supplied strings so operator-typed values can't break the
|
||||
// line-oriented output or smuggle lines past a `# ` comment prefix.
|
||||
func sanitizeOutput(v string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if r < 0x20 || r == 0x7f {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, v)
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
func anTestSetup(providers ...*proto.AgentNetworkProvider) *proto.GetAgentNetworkSetupResponse {
|
||||
return &proto.GetAgentNetworkSetupResponse{
|
||||
Configured: true,
|
||||
Endpoint: "https://calm-otter.proxy.example.com",
|
||||
Providers: providers,
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentNetworkEnv_AnthropicSingleModel(t *testing.T) {
|
||||
setup := anTestSetup(&proto.AgentNetworkProvider{
|
||||
Name: "Anthropic prod", CatalogId: "anthropic_api", ApiFlavor: "anthropic",
|
||||
Models: []string{"claude-sonnet-4-5"},
|
||||
})
|
||||
|
||||
lines, err := buildAgentNetworkEnv(setup, "", "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{
|
||||
"export ANTHROPIC_BASE_URL='https://calm-otter.proxy.example.com'",
|
||||
"export ANTHROPIC_AUTH_TOKEN='netbird'",
|
||||
"export ANTHROPIC_MODEL='claude-sonnet-4-5'",
|
||||
}, lines)
|
||||
}
|
||||
|
||||
func TestBuildAgentNetworkEnv_MultipleModelsBecomeComments(t *testing.T) {
|
||||
setup := anTestSetup(&proto.AgentNetworkProvider{
|
||||
Name: "Anthropic prod", CatalogId: "anthropic_api", ApiFlavor: "anthropic",
|
||||
Models: []string{"claude-sonnet-4-5", "claude-haiku-4-5"},
|
||||
})
|
||||
|
||||
lines, err := buildAgentNetworkEnv(setup, "", "")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, lines, "export ANTHROPIC_BASE_URL='https://calm-otter.proxy.example.com'")
|
||||
assert.NotContains(t, strings.Join(lines, "\n"), "ANTHROPIC_MODEL=", "no model is ever guessed")
|
||||
assert.Contains(t, strings.Join(lines, "\n"), "# Multiple models are allowed")
|
||||
}
|
||||
|
||||
func TestBuildAgentNetworkEnv_ModelFlagValidated(t *testing.T) {
|
||||
setup := anTestSetup(&proto.AgentNetworkProvider{
|
||||
Name: "Anthropic prod", CatalogId: "anthropic_api", ApiFlavor: "anthropic",
|
||||
Models: []string{"claude-sonnet-4-5", "claude-haiku-4-5"},
|
||||
})
|
||||
|
||||
lines, err := buildAgentNetworkEnv(setup, "", "Claude-Haiku-4-5")
|
||||
require.NoError(t, err, "model match is case-insensitive")
|
||||
assert.Contains(t, lines, "export ANTHROPIC_MODEL='Claude-Haiku-4-5'")
|
||||
|
||||
_, err = buildAgentNetworkEnv(setup, "", "gpt-4o")
|
||||
require.Error(t, err, "a model outside the allowlist is rejected")
|
||||
}
|
||||
|
||||
func TestBuildAgentNetworkEnv_BedrockFlavor(t *testing.T) {
|
||||
setup := anTestSetup(&proto.AgentNetworkProvider{
|
||||
Name: "Bedrock prod", CatalogId: "bedrock_api", ApiFlavor: "",
|
||||
Models: []string{"anthropic.claude-sonnet-4-5"},
|
||||
})
|
||||
|
||||
lines, err := buildAgentNetworkEnv(setup, "", "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{
|
||||
"export CLAUDE_CODE_USE_BEDROCK='1'",
|
||||
"export ANTHROPIC_BEDROCK_BASE_URL='https://calm-otter.proxy.example.com'",
|
||||
"export CLAUDE_CODE_SKIP_BEDROCK_AUTH='1'",
|
||||
"export ANTHROPIC_MODEL='anthropic.claude-sonnet-4-5'",
|
||||
}, lines)
|
||||
}
|
||||
|
||||
func TestBuildAgentNetworkEnv_VertexFlavorNotesProjectAndRegion(t *testing.T) {
|
||||
setup := anTestSetup(&proto.AgentNetworkProvider{
|
||||
Name: "Vertex prod", CatalogId: "vertex_ai_api", ApiFlavor: "",
|
||||
Models: []string{"claude-sonnet-4-5"},
|
||||
})
|
||||
|
||||
lines, err := buildAgentNetworkEnv(setup, "", "")
|
||||
require.NoError(t, err)
|
||||
joined := strings.Join(lines, "\n")
|
||||
assert.Contains(t, lines, "export CLAUDE_CODE_USE_VERTEX='1'")
|
||||
assert.Contains(t, lines, "export ANTHROPIC_VERTEX_BASE_URL='https://calm-otter.proxy.example.com'")
|
||||
assert.Contains(t, lines, "export CLAUDE_CODE_SKIP_VERTEX_AUTH='1'")
|
||||
assert.Contains(t, lines, "export ANTHROPIC_MODEL='claude-sonnet-4-5'")
|
||||
assert.Contains(t, joined, "ANTHROPIC_VERTEX_PROJECT_ID", "project id must be called out as admin-supplied")
|
||||
assert.Contains(t, joined, "CLOUD_ML_REGION", "region must be called out as admin-supplied")
|
||||
}
|
||||
|
||||
func TestBuildAgentNetworkEnv_OpenAIFlavor(t *testing.T) {
|
||||
setup := anTestSetup(&proto.AgentNetworkProvider{
|
||||
Name: "OpenAI prod", CatalogId: "openai_api", ApiFlavor: "openai",
|
||||
Models: []string{"gpt-5.4"},
|
||||
})
|
||||
|
||||
lines, err := buildAgentNetworkEnv(setup, "", "")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, lines, "export OPENAI_BASE_URL='https://calm-otter.proxy.example.com'")
|
||||
assert.Contains(t, lines, "export OPENAI_API_KEY='netbird'")
|
||||
assert.NotContains(t, strings.Join(lines, "\n"), "ANTHROPIC_", "openai flavor must not emit anthropic variables")
|
||||
}
|
||||
|
||||
func TestBuildAgentNetworkEnv_MultipleProvidersRequireFlag(t *testing.T) {
|
||||
anthropic := &proto.AgentNetworkProvider{Name: "Anthropic prod", CatalogId: "anthropic_api", ApiFlavor: "anthropic", Models: []string{"claude-sonnet-4-5"}}
|
||||
bedrock := &proto.AgentNetworkProvider{Name: "Bedrock prod", CatalogId: "bedrock_api", Models: []string{"anthropic.claude-sonnet-4-5"}}
|
||||
setup := anTestSetup(anthropic, bedrock)
|
||||
|
||||
lines, err := buildAgentNetworkEnv(setup, "", "")
|
||||
require.NoError(t, err)
|
||||
joined := strings.Join(lines, "\n")
|
||||
assert.NotContains(t, joined, "export ", "ambiguous provider choice must export nothing")
|
||||
assert.Contains(t, joined, "--provider")
|
||||
assert.Contains(t, joined, "Bedrock prod")
|
||||
|
||||
// Selection by operator label, case-insensitive.
|
||||
lines, err = buildAgentNetworkEnv(setup, "bedrock prod", "")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, lines, "export CLAUDE_CODE_USE_BEDROCK='1'")
|
||||
|
||||
// Selection by catalog id.
|
||||
lines, err = buildAgentNetworkEnv(setup, "anthropic_api", "")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, lines, "export ANTHROPIC_AUTH_TOKEN='netbird'")
|
||||
|
||||
// Unknown provider is an error naming the available ones.
|
||||
_, err = buildAgentNetworkEnv(setup, "vertex", "")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Anthropic prod")
|
||||
}
|
||||
|
||||
func TestBuildAgentNetworkEnv_AllModelsAllowed(t *testing.T) {
|
||||
setup := anTestSetup(&proto.AgentNetworkProvider{
|
||||
Name: "Anthropic prod", CatalogId: "anthropic_api", ApiFlavor: "anthropic",
|
||||
AllModelsAllowed: true, Models: []string{"claude-sonnet-4-5"},
|
||||
})
|
||||
|
||||
// A courtesy-listed single model is still ambiguous when everything is allowed.
|
||||
lines, err := buildAgentNetworkEnv(setup, "", "")
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, strings.Join(lines, "\n"), "ANTHROPIC_MODEL=")
|
||||
|
||||
// --model passes without allowlist validation.
|
||||
lines, err = buildAgentNetworkEnv(setup, "", "claude-opus-4-8")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, lines, "export ANTHROPIC_MODEL='claude-opus-4-8'")
|
||||
}
|
||||
|
||||
func TestBuildAgentNetworkEnv_UnknownFlavorFallsBackToComments(t *testing.T) {
|
||||
setup := anTestSetup(&proto.AgentNetworkProvider{
|
||||
Name: "Kimi", CatalogId: "kimi_api", ApiFlavor: "",
|
||||
Models: []string{"kimi-k3"},
|
||||
})
|
||||
|
||||
lines, err := buildAgentNetworkEnv(setup, "", "")
|
||||
require.NoError(t, err)
|
||||
joined := strings.Join(lines, "\n")
|
||||
assert.NotContains(t, joined, "export ", "unknown API shape must not guess variables")
|
||||
assert.Contains(t, joined, "https://calm-otter.proxy.example.com")
|
||||
}
|
||||
@@ -171,9 +171,6 @@ func init() {
|
||||
rootCmd.AddCommand(debugCmd)
|
||||
rootCmd.AddCommand(profileCmd)
|
||||
rootCmd.AddCommand(exposeCmd)
|
||||
rootCmd.AddCommand(agentNetworkCmd)
|
||||
agentNetworkCmd.AddCommand(agentNetworkLsCmd)
|
||||
agentNetworkCmd.AddCommand(agentNetworkEnvCmd)
|
||||
|
||||
networksCMD.AddCommand(routesListCmd)
|
||||
networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd)
|
||||
|
||||
@@ -2200,19 +2200,6 @@ func (e *Engine) GetExposeManager() *expose.Manager {
|
||||
return e.exposeManager
|
||||
}
|
||||
|
||||
// GetAgentNetworkSetup asks the management server for the Agent Network
|
||||
// connection info this peer's groups authorize, over the engine's
|
||||
// existing management connection.
|
||||
func (e *Engine) GetAgentNetworkSetup(ctx context.Context) (*mgmProto.AgentNetworkSetupResponse, error) {
|
||||
e.syncMsgMux.Lock()
|
||||
mgmClient := e.mgmClient
|
||||
e.syncMsgMux.Unlock()
|
||||
if mgmClient == nil {
|
||||
return nil, errors.New("management client not available")
|
||||
}
|
||||
return mgmClient.GetAgentNetworkSetup(ctx)
|
||||
}
|
||||
|
||||
// IsBlockInbound returns whether inbound connections are blocked.
|
||||
func (e *Engine) IsBlockInbound() bool {
|
||||
return e.config.BlockInbound
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc v7.34.1
|
||||
// protoc v6.33.1
|
||||
// source: daemon.proto
|
||||
|
||||
package proto
|
||||
@@ -6941,190 +6941,6 @@ func (*StopBundleCaptureResponse) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_proto_rawDescGZIP(), []int{106}
|
||||
}
|
||||
|
||||
type GetAgentNetworkSetupRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetAgentNetworkSetupRequest) Reset() {
|
||||
*x = GetAgentNetworkSetupRequest{}
|
||||
mi := &file_daemon_proto_msgTypes[107]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetAgentNetworkSetupRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetAgentNetworkSetupRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetAgentNetworkSetupRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_proto_msgTypes[107]
|
||||
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 GetAgentNetworkSetupRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetAgentNetworkSetupRequest) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_proto_rawDescGZIP(), []int{107}
|
||||
}
|
||||
|
||||
type GetAgentNetworkSetupResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// configured is false when the account has no Agent Network set up or
|
||||
// this peer's groups authorize none of it.
|
||||
Configured bool `protobuf:"varint,1,opt,name=configured,proto3" json:"configured,omitempty"`
|
||||
// endpoint is the account's proxy base URL, reachable over the NetBird
|
||||
// tunnel only. Empty when configured is false.
|
||||
Endpoint string `protobuf:"bytes,2,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||
Providers []*AgentNetworkProvider `protobuf:"bytes,3,rep,name=providers,proto3" json:"providers,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetAgentNetworkSetupResponse) Reset() {
|
||||
*x = GetAgentNetworkSetupResponse{}
|
||||
mi := &file_daemon_proto_msgTypes[108]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetAgentNetworkSetupResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetAgentNetworkSetupResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetAgentNetworkSetupResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_proto_msgTypes[108]
|
||||
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 GetAgentNetworkSetupResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetAgentNetworkSetupResponse) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_proto_rawDescGZIP(), []int{108}
|
||||
}
|
||||
|
||||
func (x *GetAgentNetworkSetupResponse) GetConfigured() bool {
|
||||
if x != nil {
|
||||
return x.Configured
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *GetAgentNetworkSetupResponse) GetEndpoint() string {
|
||||
if x != nil {
|
||||
return x.Endpoint
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetAgentNetworkSetupResponse) GetProviders() []*AgentNetworkProvider {
|
||||
if x != nil {
|
||||
return x.Providers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AgentNetworkProvider struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// name is the operator-assigned provider label, e.g. "Bedrock prod".
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
// catalog_id names the provider type, e.g. "anthropic_api", "bedrock_api".
|
||||
CatalogId string `protobuf:"bytes,2,opt,name=catalog_id,json=catalogId,proto3" json:"catalog_id,omitempty"`
|
||||
// api_flavor is the request-body shape the provider speaks ("anthropic",
|
||||
// "openai"); empty when the proxy dispatches it by URL path instead.
|
||||
ApiFlavor string `protobuf:"bytes,3,opt,name=api_flavor,json=apiFlavor,proto3" json:"api_flavor,omitempty"`
|
||||
// all_models_allowed is true when no model allowlist restricts this
|
||||
// provider for the caller.
|
||||
AllModelsAllowed bool `protobuf:"varint,4,opt,name=all_models_allowed,json=allModelsAllowed,proto3" json:"all_models_allowed,omitempty"`
|
||||
// models is the effective model allowlist (or the declared/catalog models
|
||||
// when all_models_allowed is true).
|
||||
Models []string `protobuf:"bytes,5,rep,name=models,proto3" json:"models,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProvider) Reset() {
|
||||
*x = AgentNetworkProvider{}
|
||||
mi := &file_daemon_proto_msgTypes[109]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProvider) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AgentNetworkProvider) ProtoMessage() {}
|
||||
|
||||
func (x *AgentNetworkProvider) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_proto_msgTypes[109]
|
||||
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 AgentNetworkProvider.ProtoReflect.Descriptor instead.
|
||||
func (*AgentNetworkProvider) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_proto_rawDescGZIP(), []int{109}
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProvider) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProvider) GetCatalogId() string {
|
||||
if x != nil {
|
||||
return x.CatalogId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProvider) GetApiFlavor() string {
|
||||
if x != nil {
|
||||
return x.ApiFlavor
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProvider) GetAllModelsAllowed() bool {
|
||||
if x != nil {
|
||||
return x.AllModelsAllowed
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProvider) GetModels() []string {
|
||||
if x != nil {
|
||||
return x.Models
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type PortInfo_Range struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"`
|
||||
@@ -7135,7 +6951,7 @@ type PortInfo_Range struct {
|
||||
|
||||
func (x *PortInfo_Range) Reset() {
|
||||
*x = PortInfo_Range{}
|
||||
mi := &file_daemon_proto_msgTypes[111]
|
||||
mi := &file_daemon_proto_msgTypes[108]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -7147,7 +6963,7 @@ func (x *PortInfo_Range) String() string {
|
||||
func (*PortInfo_Range) ProtoMessage() {}
|
||||
|
||||
func (x *PortInfo_Range) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_proto_msgTypes[111]
|
||||
mi := &file_daemon_proto_msgTypes[108]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -7761,22 +7577,7 @@ const file_daemon_proto_rawDesc = "" +
|
||||
"\atimeout\x18\x01 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\x1c\n" +
|
||||
"\x1aStartBundleCaptureResponse\"\x1a\n" +
|
||||
"\x18StopBundleCaptureRequest\"\x1b\n" +
|
||||
"\x19StopBundleCaptureResponse\"\x1d\n" +
|
||||
"\x1bGetAgentNetworkSetupRequest\"\x96\x01\n" +
|
||||
"\x1cGetAgentNetworkSetupResponse\x12\x1e\n" +
|
||||
"\n" +
|
||||
"configured\x18\x01 \x01(\bR\n" +
|
||||
"configured\x12\x1a\n" +
|
||||
"\bendpoint\x18\x02 \x01(\tR\bendpoint\x12:\n" +
|
||||
"\tproviders\x18\x03 \x03(\v2\x1c.daemon.AgentNetworkProviderR\tproviders\"\xae\x01\n" +
|
||||
"\x14AgentNetworkProvider\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" +
|
||||
"\n" +
|
||||
"catalog_id\x18\x02 \x01(\tR\tcatalogId\x12\x1d\n" +
|
||||
"\n" +
|
||||
"api_flavor\x18\x03 \x01(\tR\tapiFlavor\x12,\n" +
|
||||
"\x12all_models_allowed\x18\x04 \x01(\bR\x10allModelsAllowed\x12\x16\n" +
|
||||
"\x06models\x18\x05 \x03(\tR\x06models*b\n" +
|
||||
"\x19StopBundleCaptureResponse*b\n" +
|
||||
"\bLogLevel\x12\v\n" +
|
||||
"\aUNKNOWN\x10\x00\x12\t\n" +
|
||||
"\x05PANIC\x10\x01\x12\t\n" +
|
||||
@@ -7794,7 +7595,7 @@ const file_daemon_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"EXPOSE_UDP\x10\x03\x12\x0e\n" +
|
||||
"\n" +
|
||||
"EXPOSE_TLS\x10\x042\x88\x1d\n" +
|
||||
"EXPOSE_TLS\x10\x042\xa3\x1c\n" +
|
||||
"\rDaemonService\x126\n" +
|
||||
"\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" +
|
||||
"\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" +
|
||||
@@ -7844,8 +7645,7 @@ const file_daemon_proto_rawDesc = "" +
|
||||
"\x0eStopCPUProfile\x12\x1d.daemon.StopCPUProfileRequest\x1a\x1e.daemon.StopCPUProfileResponse\"\x00\x12W\n" +
|
||||
"\x12GetInstallerResult\x12\x1e.daemon.InstallerResultRequest\x1a\x1f.daemon.InstallerResultResponse\"\x00\x12M\n" +
|
||||
"\rExposeService\x12\x1c.daemon.ExposeServiceRequest\x1a\x1a.daemon.ExposeServiceEvent\"\x000\x01\x12K\n" +
|
||||
"\fWailsUIReady\x12\x1b.daemon.WailsUIReadyRequest\x1a\x1c.daemon.WailsUIReadyResponse\"\x00\x12c\n" +
|
||||
"\x14GetAgentNetworkSetup\x12#.daemon.GetAgentNetworkSetupRequest\x1a$.daemon.GetAgentNetworkSetupResponse\"\x00B\bZ\x06/protob\x06proto3"
|
||||
"\fWailsUIReady\x12\x1b.daemon.WailsUIReadyRequest\x1a\x1c.daemon.WailsUIReadyResponse\"\x00B\bZ\x06/protob\x06proto3"
|
||||
|
||||
var (
|
||||
file_daemon_proto_rawDescOnce sync.Once
|
||||
@@ -7860,7 +7660,7 @@ func file_daemon_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
|
||||
var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 113)
|
||||
var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 110)
|
||||
var file_daemon_proto_goTypes = []any{
|
||||
(LogLevel)(0), // 0: daemon.LogLevel
|
||||
(ExposeProtocol)(0), // 1: daemon.ExposeProtocol
|
||||
@@ -7973,22 +7773,19 @@ var file_daemon_proto_goTypes = []any{
|
||||
(*StartBundleCaptureResponse)(nil), // 108: daemon.StartBundleCaptureResponse
|
||||
(*StopBundleCaptureRequest)(nil), // 109: daemon.StopBundleCaptureRequest
|
||||
(*StopBundleCaptureResponse)(nil), // 110: daemon.StopBundleCaptureResponse
|
||||
(*GetAgentNetworkSetupRequest)(nil), // 111: daemon.GetAgentNetworkSetupRequest
|
||||
(*GetAgentNetworkSetupResponse)(nil), // 112: daemon.GetAgentNetworkSetupResponse
|
||||
(*AgentNetworkProvider)(nil), // 113: daemon.AgentNetworkProvider
|
||||
nil, // 114: daemon.Network.ResolvedIPsEntry
|
||||
(*PortInfo_Range)(nil), // 115: daemon.PortInfo.Range
|
||||
nil, // 116: daemon.SystemEvent.MetadataEntry
|
||||
(*durationpb.Duration)(nil), // 117: google.protobuf.Duration
|
||||
(*timestamppb.Timestamp)(nil), // 118: google.protobuf.Timestamp
|
||||
nil, // 111: daemon.Network.ResolvedIPsEntry
|
||||
(*PortInfo_Range)(nil), // 112: daemon.PortInfo.Range
|
||||
nil, // 113: daemon.SystemEvent.MetadataEntry
|
||||
(*durationpb.Duration)(nil), // 114: google.protobuf.Duration
|
||||
(*timestamppb.Timestamp)(nil), // 115: google.protobuf.Timestamp
|
||||
}
|
||||
var file_daemon_proto_depIdxs = []int32{
|
||||
117, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
|
||||
114, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
|
||||
25, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
|
||||
118, // 2: daemon.StatusResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
|
||||
118, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
|
||||
118, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
|
||||
117, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration
|
||||
115, // 2: daemon.StatusResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
|
||||
115, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
|
||||
115, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
|
||||
114, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration
|
||||
23, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo
|
||||
20, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
|
||||
19, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState
|
||||
@@ -7999,8 +7796,8 @@ var file_daemon_proto_depIdxs = []int32{
|
||||
57, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent
|
||||
24, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState
|
||||
31, // 15: daemon.ListNetworksResponse.routes:type_name -> daemon.Network
|
||||
114, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry
|
||||
115, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range
|
||||
111, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry
|
||||
112, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range
|
||||
32, // 18: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo
|
||||
32, // 19: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo
|
||||
33, // 20: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule
|
||||
@@ -8011,117 +7808,114 @@ var file_daemon_proto_depIdxs = []int32{
|
||||
54, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage
|
||||
2, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity
|
||||
3, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category
|
||||
118, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
116, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry
|
||||
115, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
113, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry
|
||||
57, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent
|
||||
117, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
|
||||
114, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration
|
||||
72, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile
|
||||
118, // 33: daemon.WaitExtendAuthSessionResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
|
||||
115, // 33: daemon.WaitExtendAuthSessionResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
|
||||
1, // 34: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol
|
||||
104, // 35: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady
|
||||
117, // 36: daemon.StartCaptureRequest.duration:type_name -> google.protobuf.Duration
|
||||
117, // 37: daemon.StartBundleCaptureRequest.timeout:type_name -> google.protobuf.Duration
|
||||
113, // 38: daemon.GetAgentNetworkSetupResponse.providers:type_name -> daemon.AgentNetworkProvider
|
||||
30, // 39: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList
|
||||
5, // 40: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
|
||||
7, // 41: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
|
||||
9, // 42: daemon.DaemonService.Up:input_type -> daemon.UpRequest
|
||||
11, // 43: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
|
||||
11, // 44: daemon.DaemonService.SubscribeStatus:input_type -> daemon.StatusRequest
|
||||
13, // 45: daemon.DaemonService.Down:input_type -> daemon.DownRequest
|
||||
15, // 46: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
|
||||
26, // 47: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest
|
||||
28, // 48: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest
|
||||
28, // 49: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest
|
||||
4, // 50: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest
|
||||
35, // 51: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest
|
||||
37, // 52: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest
|
||||
39, // 53: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest
|
||||
44, // 54: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest
|
||||
46, // 55: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest
|
||||
48, // 56: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest
|
||||
50, // 57: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest
|
||||
53, // 58: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest
|
||||
105, // 59: daemon.DaemonService.StartCapture:input_type -> daemon.StartCaptureRequest
|
||||
107, // 60: daemon.DaemonService.StartBundleCapture:input_type -> daemon.StartBundleCaptureRequest
|
||||
109, // 61: daemon.DaemonService.StopBundleCapture:input_type -> daemon.StopBundleCaptureRequest
|
||||
56, // 62: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest
|
||||
58, // 63: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest
|
||||
41, // 64: daemon.DaemonService.RegisterUILog:input_type -> daemon.RegisterUILogRequest
|
||||
60, // 65: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest
|
||||
62, // 66: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest
|
||||
64, // 67: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest
|
||||
66, // 68: daemon.DaemonService.RenameProfile:input_type -> daemon.RenameProfileRequest
|
||||
68, // 69: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest
|
||||
70, // 70: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest
|
||||
73, // 71: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest
|
||||
75, // 72: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest
|
||||
79, // 73: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest
|
||||
82, // 74: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest
|
||||
84, // 75: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest
|
||||
86, // 76: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest
|
||||
88, // 77: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest
|
||||
90, // 78: daemon.DaemonService.RequestExtendAuthSession:input_type -> daemon.RequestExtendAuthSessionRequest
|
||||
92, // 79: daemon.DaemonService.WaitExtendAuthSession:input_type -> daemon.WaitExtendAuthSessionRequest
|
||||
94, // 80: daemon.DaemonService.DismissSessionWarning:input_type -> daemon.DismissSessionWarningRequest
|
||||
96, // 81: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest
|
||||
98, // 82: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest
|
||||
100, // 83: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest
|
||||
102, // 84: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest
|
||||
77, // 85: daemon.DaemonService.WailsUIReady:input_type -> daemon.WailsUIReadyRequest
|
||||
111, // 86: daemon.DaemonService.GetAgentNetworkSetup:input_type -> daemon.GetAgentNetworkSetupRequest
|
||||
6, // 87: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
|
||||
8, // 88: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
|
||||
10, // 89: daemon.DaemonService.Up:output_type -> daemon.UpResponse
|
||||
12, // 90: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
|
||||
12, // 91: daemon.DaemonService.SubscribeStatus:output_type -> daemon.StatusResponse
|
||||
14, // 92: daemon.DaemonService.Down:output_type -> daemon.DownResponse
|
||||
16, // 93: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
|
||||
27, // 94: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse
|
||||
29, // 95: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse
|
||||
29, // 96: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse
|
||||
34, // 97: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse
|
||||
36, // 98: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse
|
||||
38, // 99: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse
|
||||
40, // 100: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse
|
||||
45, // 101: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse
|
||||
47, // 102: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse
|
||||
49, // 103: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse
|
||||
51, // 104: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse
|
||||
55, // 105: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse
|
||||
106, // 106: daemon.DaemonService.StartCapture:output_type -> daemon.CapturePacket
|
||||
108, // 107: daemon.DaemonService.StartBundleCapture:output_type -> daemon.StartBundleCaptureResponse
|
||||
110, // 108: daemon.DaemonService.StopBundleCapture:output_type -> daemon.StopBundleCaptureResponse
|
||||
57, // 109: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent
|
||||
59, // 110: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse
|
||||
42, // 111: daemon.DaemonService.RegisterUILog:output_type -> daemon.RegisterUILogResponse
|
||||
61, // 112: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse
|
||||
63, // 113: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse
|
||||
65, // 114: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse
|
||||
67, // 115: daemon.DaemonService.RenameProfile:output_type -> daemon.RenameProfileResponse
|
||||
69, // 116: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse
|
||||
71, // 117: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse
|
||||
74, // 118: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse
|
||||
76, // 119: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse
|
||||
80, // 120: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse
|
||||
83, // 121: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse
|
||||
85, // 122: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse
|
||||
87, // 123: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse
|
||||
89, // 124: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse
|
||||
91, // 125: daemon.DaemonService.RequestExtendAuthSession:output_type -> daemon.RequestExtendAuthSessionResponse
|
||||
93, // 126: daemon.DaemonService.WaitExtendAuthSession:output_type -> daemon.WaitExtendAuthSessionResponse
|
||||
95, // 127: daemon.DaemonService.DismissSessionWarning:output_type -> daemon.DismissSessionWarningResponse
|
||||
97, // 128: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse
|
||||
99, // 129: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse
|
||||
101, // 130: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse
|
||||
103, // 131: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent
|
||||
78, // 132: daemon.DaemonService.WailsUIReady:output_type -> daemon.WailsUIReadyResponse
|
||||
112, // 133: daemon.DaemonService.GetAgentNetworkSetup:output_type -> daemon.GetAgentNetworkSetupResponse
|
||||
87, // [87:134] is the sub-list for method output_type
|
||||
40, // [40:87] is the sub-list for method input_type
|
||||
40, // [40:40] is the sub-list for extension type_name
|
||||
40, // [40:40] is the sub-list for extension extendee
|
||||
0, // [0:40] is the sub-list for field type_name
|
||||
114, // 36: daemon.StartCaptureRequest.duration:type_name -> google.protobuf.Duration
|
||||
114, // 37: daemon.StartBundleCaptureRequest.timeout:type_name -> google.protobuf.Duration
|
||||
30, // 38: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList
|
||||
5, // 39: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
|
||||
7, // 40: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
|
||||
9, // 41: daemon.DaemonService.Up:input_type -> daemon.UpRequest
|
||||
11, // 42: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
|
||||
11, // 43: daemon.DaemonService.SubscribeStatus:input_type -> daemon.StatusRequest
|
||||
13, // 44: daemon.DaemonService.Down:input_type -> daemon.DownRequest
|
||||
15, // 45: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
|
||||
26, // 46: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest
|
||||
28, // 47: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest
|
||||
28, // 48: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest
|
||||
4, // 49: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest
|
||||
35, // 50: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest
|
||||
37, // 51: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest
|
||||
39, // 52: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest
|
||||
44, // 53: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest
|
||||
46, // 54: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest
|
||||
48, // 55: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest
|
||||
50, // 56: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest
|
||||
53, // 57: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest
|
||||
105, // 58: daemon.DaemonService.StartCapture:input_type -> daemon.StartCaptureRequest
|
||||
107, // 59: daemon.DaemonService.StartBundleCapture:input_type -> daemon.StartBundleCaptureRequest
|
||||
109, // 60: daemon.DaemonService.StopBundleCapture:input_type -> daemon.StopBundleCaptureRequest
|
||||
56, // 61: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest
|
||||
58, // 62: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest
|
||||
41, // 63: daemon.DaemonService.RegisterUILog:input_type -> daemon.RegisterUILogRequest
|
||||
60, // 64: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest
|
||||
62, // 65: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest
|
||||
64, // 66: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest
|
||||
66, // 67: daemon.DaemonService.RenameProfile:input_type -> daemon.RenameProfileRequest
|
||||
68, // 68: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest
|
||||
70, // 69: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest
|
||||
73, // 70: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest
|
||||
75, // 71: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest
|
||||
79, // 72: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest
|
||||
82, // 73: daemon.DaemonService.TriggerUpdate:input_type -> daemon.TriggerUpdateRequest
|
||||
84, // 74: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest
|
||||
86, // 75: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest
|
||||
88, // 76: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest
|
||||
90, // 77: daemon.DaemonService.RequestExtendAuthSession:input_type -> daemon.RequestExtendAuthSessionRequest
|
||||
92, // 78: daemon.DaemonService.WaitExtendAuthSession:input_type -> daemon.WaitExtendAuthSessionRequest
|
||||
94, // 79: daemon.DaemonService.DismissSessionWarning:input_type -> daemon.DismissSessionWarningRequest
|
||||
96, // 80: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest
|
||||
98, // 81: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest
|
||||
100, // 82: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest
|
||||
102, // 83: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest
|
||||
77, // 84: daemon.DaemonService.WailsUIReady:input_type -> daemon.WailsUIReadyRequest
|
||||
6, // 85: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
|
||||
8, // 86: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
|
||||
10, // 87: daemon.DaemonService.Up:output_type -> daemon.UpResponse
|
||||
12, // 88: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
|
||||
12, // 89: daemon.DaemonService.SubscribeStatus:output_type -> daemon.StatusResponse
|
||||
14, // 90: daemon.DaemonService.Down:output_type -> daemon.DownResponse
|
||||
16, // 91: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
|
||||
27, // 92: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse
|
||||
29, // 93: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse
|
||||
29, // 94: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse
|
||||
34, // 95: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse
|
||||
36, // 96: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse
|
||||
38, // 97: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse
|
||||
40, // 98: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse
|
||||
45, // 99: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse
|
||||
47, // 100: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse
|
||||
49, // 101: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse
|
||||
51, // 102: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse
|
||||
55, // 103: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse
|
||||
106, // 104: daemon.DaemonService.StartCapture:output_type -> daemon.CapturePacket
|
||||
108, // 105: daemon.DaemonService.StartBundleCapture:output_type -> daemon.StartBundleCaptureResponse
|
||||
110, // 106: daemon.DaemonService.StopBundleCapture:output_type -> daemon.StopBundleCaptureResponse
|
||||
57, // 107: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent
|
||||
59, // 108: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse
|
||||
42, // 109: daemon.DaemonService.RegisterUILog:output_type -> daemon.RegisterUILogResponse
|
||||
61, // 110: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse
|
||||
63, // 111: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse
|
||||
65, // 112: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse
|
||||
67, // 113: daemon.DaemonService.RenameProfile:output_type -> daemon.RenameProfileResponse
|
||||
69, // 114: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse
|
||||
71, // 115: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse
|
||||
74, // 116: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse
|
||||
76, // 117: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse
|
||||
80, // 118: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse
|
||||
83, // 119: daemon.DaemonService.TriggerUpdate:output_type -> daemon.TriggerUpdateResponse
|
||||
85, // 120: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse
|
||||
87, // 121: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse
|
||||
89, // 122: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse
|
||||
91, // 123: daemon.DaemonService.RequestExtendAuthSession:output_type -> daemon.RequestExtendAuthSessionResponse
|
||||
93, // 124: daemon.DaemonService.WaitExtendAuthSession:output_type -> daemon.WaitExtendAuthSessionResponse
|
||||
95, // 125: daemon.DaemonService.DismissSessionWarning:output_type -> daemon.DismissSessionWarningResponse
|
||||
97, // 126: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse
|
||||
99, // 127: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse
|
||||
101, // 128: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse
|
||||
103, // 129: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent
|
||||
78, // 130: daemon.DaemonService.WailsUIReady:output_type -> daemon.WailsUIReadyResponse
|
||||
85, // [85:131] is the sub-list for method output_type
|
||||
39, // [39:85] is the sub-list for method input_type
|
||||
39, // [39:39] is the sub-list for extension type_name
|
||||
39, // [39:39] is the sub-list for extension extendee
|
||||
0, // [0:39] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_daemon_proto_init() }
|
||||
@@ -8153,7 +7947,7 @@ func file_daemon_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)),
|
||||
NumEnums: 4,
|
||||
NumMessages: 113,
|
||||
NumMessages: 110,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -1123,30 +1123,6 @@ func local_request_DaemonService_WailsUIReady_0(ctx context.Context, marshaler r
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_DaemonService_GetAgentNetworkSetup_0(ctx context.Context, marshaler runtime.Marshaler, client DaemonServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq GetAgentNetworkSetupRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := client.GetAgentNetworkSetup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_DaemonService_GetAgentNetworkSetup_0(ctx context.Context, marshaler runtime.Marshaler, server DaemonServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq GetAgentNetworkSetupRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.GetAgentNetworkSetup(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
// RegisterDaemonServiceHandlerServer registers the http handlers for service DaemonService to "mux".
|
||||
// UnaryRPC :call DaemonServiceServer directly.
|
||||
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
|
||||
@@ -2021,26 +1997,6 @@ func RegisterDaemonServiceHandlerServer(ctx context.Context, mux *runtime.ServeM
|
||||
}
|
||||
forward_DaemonService_WailsUIReady_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_DaemonService_GetAgentNetworkSetup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/daemon.DaemonService/GetAgentNetworkSetup", runtime.WithHTTPPathPattern("/daemon.DaemonService/GetAgentNetworkSetup"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_DaemonService_GetAgentNetworkSetup_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_DaemonService_GetAgentNetworkSetup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2863,23 +2819,6 @@ func RegisterDaemonServiceHandlerClient(ctx context.Context, mux *runtime.ServeM
|
||||
}
|
||||
forward_DaemonService_WailsUIReady_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_DaemonService_GetAgentNetworkSetup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/daemon.DaemonService/GetAgentNetworkSetup", runtime.WithHTTPPathPattern("/daemon.DaemonService/GetAgentNetworkSetup"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_DaemonService_GetAgentNetworkSetup_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_DaemonService_GetAgentNetworkSetup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2930,7 +2869,6 @@ var (
|
||||
pattern_DaemonService_GetInstallerResult_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "GetInstallerResult"}, ""))
|
||||
pattern_DaemonService_ExposeService_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "ExposeService"}, ""))
|
||||
pattern_DaemonService_WailsUIReady_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "WailsUIReady"}, ""))
|
||||
pattern_DaemonService_GetAgentNetworkSetup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"daemon.DaemonService", "GetAgentNetworkSetup"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -2980,5 +2918,4 @@ var (
|
||||
forward_DaemonService_GetInstallerResult_0 = runtime.ForwardResponseMessage
|
||||
forward_DaemonService_ExposeService_0 = runtime.ForwardResponseStream
|
||||
forward_DaemonService_WailsUIReady_0 = runtime.ForwardResponseMessage
|
||||
forward_DaemonService_GetAgentNetworkSetup_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
|
||||
@@ -156,12 +156,6 @@ service DaemonService {
|
||||
// only cares whether the daemon implements it: an Unimplemented response
|
||||
// means the daemon predates this UI and is too old to drive it.
|
||||
rpc WailsUIReady(WailsUIReadyRequest) returns (WailsUIReadyResponse) {}
|
||||
|
||||
// GetAgentNetworkSetup returns the Agent Network connection info this
|
||||
// peer's groups authorize: proxy endpoint plus effective providers and
|
||||
// models. The daemon relays the request to management over its existing
|
||||
// peer connection, so unprivileged CLI callers get a caller-scoped answer.
|
||||
rpc GetAgentNetworkSetup(GetAgentNetworkSetupRequest) returns (GetAgentNetworkSetupResponse) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -1056,31 +1050,3 @@ message StartBundleCaptureRequest {
|
||||
message StartBundleCaptureResponse {}
|
||||
message StopBundleCaptureRequest {}
|
||||
message StopBundleCaptureResponse {}
|
||||
|
||||
message GetAgentNetworkSetupRequest {}
|
||||
|
||||
message GetAgentNetworkSetupResponse {
|
||||
// configured is false when the account has no Agent Network set up or
|
||||
// this peer's groups authorize none of it.
|
||||
bool configured = 1;
|
||||
// endpoint is the account's proxy base URL, reachable over the NetBird
|
||||
// tunnel only. Empty when configured is false.
|
||||
string endpoint = 2;
|
||||
repeated AgentNetworkProvider providers = 3;
|
||||
}
|
||||
|
||||
message AgentNetworkProvider {
|
||||
// name is the operator-assigned provider label, e.g. "Bedrock prod".
|
||||
string name = 1;
|
||||
// catalog_id names the provider type, e.g. "anthropic_api", "bedrock_api".
|
||||
string catalog_id = 2;
|
||||
// api_flavor is the request-body shape the provider speaks ("anthropic",
|
||||
// "openai"); empty when the proxy dispatches it by URL path instead.
|
||||
string api_flavor = 3;
|
||||
// all_models_allowed is true when no model allowlist restricts this
|
||||
// provider for the caller.
|
||||
bool all_models_allowed = 4;
|
||||
// models is the effective model allowlist (or the declared/catalog models
|
||||
// when all_models_allowed is true).
|
||||
repeated string models = 5;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.1
|
||||
// - protoc v7.34.1
|
||||
// - protoc v6.33.1
|
||||
// source: daemon.proto
|
||||
|
||||
package proto
|
||||
@@ -65,7 +65,6 @@ const (
|
||||
DaemonService_GetInstallerResult_FullMethodName = "/daemon.DaemonService/GetInstallerResult"
|
||||
DaemonService_ExposeService_FullMethodName = "/daemon.DaemonService/ExposeService"
|
||||
DaemonService_WailsUIReady_FullMethodName = "/daemon.DaemonService/WailsUIReady"
|
||||
DaemonService_GetAgentNetworkSetup_FullMethodName = "/daemon.DaemonService/GetAgentNetworkSetup"
|
||||
)
|
||||
|
||||
// DaemonServiceClient is the client API for DaemonService service.
|
||||
@@ -172,11 +171,6 @@ type DaemonServiceClient interface {
|
||||
// only cares whether the daemon implements it: an Unimplemented response
|
||||
// means the daemon predates this UI and is too old to drive it.
|
||||
WailsUIReady(ctx context.Context, in *WailsUIReadyRequest, opts ...grpc.CallOption) (*WailsUIReadyResponse, error)
|
||||
// GetAgentNetworkSetup returns the Agent Network connection info this
|
||||
// peer's groups authorize: proxy endpoint plus effective providers and
|
||||
// models. The daemon relays the request to management over its existing
|
||||
// peer connection, so unprivileged CLI callers get a caller-scoped answer.
|
||||
GetAgentNetworkSetup(ctx context.Context, in *GetAgentNetworkSetupRequest, opts ...grpc.CallOption) (*GetAgentNetworkSetupResponse, error)
|
||||
}
|
||||
|
||||
type daemonServiceClient struct {
|
||||
@@ -683,16 +677,6 @@ func (c *daemonServiceClient) WailsUIReady(ctx context.Context, in *WailsUIReady
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *daemonServiceClient) GetAgentNetworkSetup(ctx context.Context, in *GetAgentNetworkSetupRequest, opts ...grpc.CallOption) (*GetAgentNetworkSetupResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetAgentNetworkSetupResponse)
|
||||
err := c.cc.Invoke(ctx, DaemonService_GetAgentNetworkSetup_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DaemonServiceServer is the server API for DaemonService service.
|
||||
// All implementations must embed UnimplementedDaemonServiceServer
|
||||
// for forward compatibility.
|
||||
@@ -797,11 +781,6 @@ type DaemonServiceServer interface {
|
||||
// only cares whether the daemon implements it: an Unimplemented response
|
||||
// means the daemon predates this UI and is too old to drive it.
|
||||
WailsUIReady(context.Context, *WailsUIReadyRequest) (*WailsUIReadyResponse, error)
|
||||
// GetAgentNetworkSetup returns the Agent Network connection info this
|
||||
// peer's groups authorize: proxy endpoint plus effective providers and
|
||||
// models. The daemon relays the request to management over its existing
|
||||
// peer connection, so unprivileged CLI callers get a caller-scoped answer.
|
||||
GetAgentNetworkSetup(context.Context, *GetAgentNetworkSetupRequest) (*GetAgentNetworkSetupResponse, error)
|
||||
mustEmbedUnimplementedDaemonServiceServer()
|
||||
}
|
||||
|
||||
@@ -950,9 +929,6 @@ func (UnimplementedDaemonServiceServer) ExposeService(*ExposeServiceRequest, grp
|
||||
func (UnimplementedDaemonServiceServer) WailsUIReady(context.Context, *WailsUIReadyRequest) (*WailsUIReadyResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method WailsUIReady not implemented")
|
||||
}
|
||||
func (UnimplementedDaemonServiceServer) GetAgentNetworkSetup(context.Context, *GetAgentNetworkSetupRequest) (*GetAgentNetworkSetupResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetAgentNetworkSetup not implemented")
|
||||
}
|
||||
func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {}
|
||||
func (UnimplementedDaemonServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -1774,24 +1750,6 @@ func _DaemonService_WailsUIReady_Handler(srv interface{}, ctx context.Context, d
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _DaemonService_GetAgentNetworkSetup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetAgentNetworkSetupRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DaemonServiceServer).GetAgentNetworkSetup(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: DaemonService_GetAgentNetworkSetup_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DaemonServiceServer).GetAgentNetworkSetup(ctx, req.(*GetAgentNetworkSetupRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -1967,10 +1925,6 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "WailsUIReady",
|
||||
Handler: _DaemonService_WailsUIReady_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetAgentNetworkSetup",
|
||||
Handler: _DaemonService_GetAgentNetworkSetup_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
gstatus "google.golang.org/grpc/status"
|
||||
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
// GetAgentNetworkSetup relays the peer's Agent Network setup request to the
|
||||
// management server over the engine's existing connection. Running through
|
||||
// the daemon keeps the WireGuard key inside the daemon — unprivileged CLI
|
||||
// callers get a caller-scoped answer without reading the profile config.
|
||||
func (s *Server) GetAgentNetworkSetup(ctx context.Context, _ *proto.GetAgentNetworkSetupRequest) (*proto.GetAgentNetworkSetupResponse, error) {
|
||||
s.mutex.Lock()
|
||||
clientRunning := s.clientRunning
|
||||
connectClient := s.connectClient
|
||||
s.mutex.Unlock()
|
||||
|
||||
if !clientRunning || connectClient == nil {
|
||||
return nil, gstatus.Errorf(codes.FailedPrecondition, "client is not running, run 'netbird up' first")
|
||||
}
|
||||
engine := connectClient.Engine()
|
||||
if engine == nil {
|
||||
return nil, gstatus.Errorf(codes.FailedPrecondition, "engine not initialized")
|
||||
}
|
||||
|
||||
setupCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
setup, err := engine.GetAgentNetworkSetup(setupCtx)
|
||||
if err != nil {
|
||||
return nil, gstatus.Errorf(codes.Internal, "get agent network setup: %v", err)
|
||||
}
|
||||
|
||||
return toDaemonAgentNetworkSetup(setup), nil
|
||||
}
|
||||
|
||||
func toDaemonAgentNetworkSetup(setup *mgmProto.AgentNetworkSetupResponse) *proto.GetAgentNetworkSetupResponse {
|
||||
resp := &proto.GetAgentNetworkSetupResponse{
|
||||
Configured: setup.Configured,
|
||||
Endpoint: setup.Endpoint,
|
||||
Providers: make([]*proto.AgentNetworkProvider, 0, len(setup.Providers)),
|
||||
}
|
||||
for _, p := range setup.Providers {
|
||||
resp.Providers = append(resp.Providers, &proto.AgentNetworkProvider{
|
||||
Name: p.Name,
|
||||
CatalogId: p.CatalogId,
|
||||
ApiFlavor: p.ApiFlavor,
|
||||
AllModelsAllowed: p.AllModelsAllowed,
|
||||
Models: p.Models,
|
||||
})
|
||||
}
|
||||
return resp
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
gstatus "google.golang.org/grpc/status"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
@@ -27,9 +25,9 @@ func TestLogin_ManagementUnreachableIsReturnedInsteadOfDemandingSSO(t *testing.T
|
||||
|
||||
unreachable := errors.New("create connection: dial context: context deadline exceeded")
|
||||
attempts := 0
|
||||
s.loginAttemptFn = func(context.Context, string, string) (internal.StatusType, error) {
|
||||
s.isLoginRequiredFn = func(context.Context) (bool, error) {
|
||||
attempts++
|
||||
return internal.StatusLoginFailed, unreachable
|
||||
return false, unreachable
|
||||
}
|
||||
|
||||
resp, err := s.Login(userCtx(), &proto.LoginRequest{Username: &username})
|
||||
@@ -55,15 +53,12 @@ func TestLogin_AuthRefusalStartsSSOFlow(t *testing.T) {
|
||||
s.rootCtx = internal.CtxInitState(context.Background())
|
||||
breakProfilePrivateKey(t, cfgPath)
|
||||
|
||||
refused := gstatus.Error(codes.PermissionDenied, "peer is not registered")
|
||||
s.loginAttemptFn = func(context.Context, string, string) (internal.StatusType, error) {
|
||||
return internal.StatusNeedsLogin, refused
|
||||
s.isLoginRequiredFn = func(context.Context) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
_, err := s.Login(userCtx(), &proto.LoginRequest{Username: &username})
|
||||
require.Error(t, err)
|
||||
require.NotErrorIs(t, err, refused,
|
||||
"the refusal was handed back to the caller instead of starting the SSO flow")
|
||||
|
||||
status, stateErr := internal.CtxGetState(s.rootCtx).Status()
|
||||
require.NoError(t, stateErr)
|
||||
@@ -71,6 +66,32 @@ func TestLogin_AuthRefusalStartsSSOFlow(t *testing.T) {
|
||||
"the SSO flow setup was never reached with the broken key")
|
||||
}
|
||||
|
||||
func TestLogin_SetupKeyStillRunsWhenPeerNeedsLogin(t *testing.T) {
|
||||
s, _, _, username, _ := setupServerWithProfile(t)
|
||||
s.rootCtx = internal.CtxInitState(context.Background())
|
||||
|
||||
s.isLoginRequiredFn = func(context.Context) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var keysTried []string
|
||||
s.loginAttemptFn = func(_ context.Context, setupKey, _ string) (internal.StatusType, error) {
|
||||
keysTried = append(keysTried, setupKey)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
setupKey := "A2C8E32F-AEB2-4B45-8FD3-8A0C1B2D3E4F"
|
||||
resp, err := s.Login(userCtx(), &proto.LoginRequest{Username: &username, SetupKey: setupKey})
|
||||
require.NoError(t, err, "the probe's outcome leaked out as the login result")
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, []string{setupKey}, keysTried, "the setup key never reached the login attempt")
|
||||
require.Nil(t, s.oauthAuthFlow.flow, "a setup-key login started an SSO flow")
|
||||
|
||||
status, err := internal.CtxGetState(s.rootCtx).Status()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, internal.StatusIdle, status)
|
||||
}
|
||||
|
||||
// breakProfilePrivateKey replaces the profile's private key with an unparseable
|
||||
// one, which makes any attempt to build a Management client fail on the spot.
|
||||
func breakProfilePrivateKey(t *testing.T, cfgPath string) {
|
||||
|
||||
@@ -140,6 +140,8 @@ type Server struct {
|
||||
// it to drive the login outcomes that need a server on the other end;
|
||||
// production leaves it nil, and every login goes through loginAttempt.
|
||||
loginAttemptFn func(ctx context.Context, setupKey, jwtToken string) (internal.StatusType, error)
|
||||
|
||||
isLoginRequiredFn func(ctx context.Context) (bool, error)
|
||||
}
|
||||
|
||||
type oauthAuthFlow struct {
|
||||
@@ -384,6 +386,21 @@ func (s *Server) attemptLogin(ctx context.Context, setupKey, jwtToken string) (i
|
||||
return s.loginAttempt(ctx, setupKey, jwtToken)
|
||||
}
|
||||
|
||||
func (s *Server) isLoginRequired(ctx context.Context) (bool, error) {
|
||||
if s.isLoginRequiredFn != nil {
|
||||
return s.isLoginRequiredFn(ctx)
|
||||
}
|
||||
|
||||
authClient, err := auth.NewAuth(ctx, s.config.PrivateKey, s.config.ManagementURL, s.config)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create auth client: %v", err)
|
||||
return false, err
|
||||
}
|
||||
defer authClient.Close()
|
||||
|
||||
return authClient.IsLoginRequired(ctx)
|
||||
}
|
||||
|
||||
// loginAttempt attempts to login using the provided information. It returns
|
||||
// StatusNeedsLogin when Management refused the peer's credentials and
|
||||
// StatusLoginFailed for every other failure, so callers can tell an
|
||||
@@ -640,22 +657,22 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
|
||||
s.config = config
|
||||
s.mutex.Unlock()
|
||||
|
||||
loginStatus, err := s.attemptLogin(ctx, "", "")
|
||||
if err == nil {
|
||||
state.Set(internal.StatusIdle)
|
||||
return &proto.LoginResponse{}, nil
|
||||
}
|
||||
|
||||
// Only an authentication refusal means the peer has to (re-)authenticate.
|
||||
// Any other failure leaves the login undecided: Management unreachable, a
|
||||
// A probe that errors leaves the login undecided: Management unreachable, a
|
||||
// restart mid-request, an internal error. Those are returned for the caller
|
||||
// to retry, because turning them into an SSO prompt asks the user to solve
|
||||
// something that is not theirs to solve, and a browser login cannot succeed
|
||||
// while Management is unreachable anyway.
|
||||
if loginStatus != internal.StatusNeedsLogin {
|
||||
state.Set(loginStatus)
|
||||
// while Management is unreachable anyway. Only Management refusing the
|
||||
// peer's key is a decision, and IsLoginRequired reports that as
|
||||
// needsLogin=true rather than an error.
|
||||
needsLogin, err := s.isLoginRequired(ctx)
|
||||
if err != nil {
|
||||
state.Set(internal.StatusLoginFailed)
|
||||
return nil, err
|
||||
}
|
||||
if !needsLogin {
|
||||
state.Set(internal.StatusIdle)
|
||||
return &proto.LoginResponse{}, nil
|
||||
}
|
||||
|
||||
if msg.SetupKey == "" {
|
||||
hint := ""
|
||||
@@ -1798,6 +1815,9 @@ func (s *Server) RequestExtendAuthSession(
|
||||
if connectClient == nil {
|
||||
return nil, gstatus.Errorf(codes.FailedPrecondition, "client is not running")
|
||||
}
|
||||
if connectClient.Engine() == nil {
|
||||
return nil, gstatus.Errorf(codes.FailedPrecondition, "session can no longer be extended, log in again to reconnect")
|
||||
}
|
||||
|
||||
hint := ""
|
||||
if msg.Hint != nil {
|
||||
|
||||
35
client/ui/frontend/src/hooks/useKeepConnectedOnQuit.ts
Normal file
35
client/ui/frontend/src/hooks/useKeepConnectedOnQuit.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Preferences } from "@bindings/services";
|
||||
|
||||
export const useKeepConnectedOnQuit = () => {
|
||||
const [keepConnected, setKeepConnected] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Preferences.Get()
|
||||
.then((prefs) => {
|
||||
if (cancelled) return;
|
||||
setKeepConnected(prefs?.keepConnectedOnQuit ?? false);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (cancelled) return;
|
||||
console.warn("[useKeepConnectedOnQuit] load preferences failed", err);
|
||||
setKeepConnected(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setKeepConnectedOnQuit = useCallback(async (keep: boolean) => {
|
||||
setKeepConnected(keep);
|
||||
try {
|
||||
await Preferences.SetKeepConnectedOnQuit(keep);
|
||||
} catch (err: unknown) {
|
||||
setKeepConnected(!keep);
|
||||
console.error("[useKeepConnectedOnQuit] SetKeepConnectedOnQuit failed", err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { keepConnected, setKeepConnectedOnQuit };
|
||||
};
|
||||
@@ -11,7 +11,7 @@ import { DialogHeading } from "@/components/dialog/DialogHeading";
|
||||
import { SquareIcon } from "@/components/SquareIcon";
|
||||
import { Connection, Profiles as ProfilesSvc, Session, WindowManager } from "@bindings/services";
|
||||
import { useAutoSizeWindow } from "@/hooks/useAutoSizeWindow";
|
||||
import { EVENT_BROWSER_LOGIN_CANCEL } from "@/lib/connection";
|
||||
import { EVENT_BROWSER_LOGIN_CANCEL, EVENT_TRIGGER_LOGIN } from "@/lib/connection";
|
||||
import { errorDialog, formatErrorMessage } from "@/lib/errors.ts";
|
||||
import { formatRemaining } from "@/lib/formatters";
|
||||
|
||||
@@ -131,6 +131,21 @@ export default function SessionExpirationDialog() {
|
||||
}
|
||||
}, [busy, t]);
|
||||
|
||||
const authenticate = useCallback(async () => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await Events.Emit(EVENT_TRIGGER_LOGIN);
|
||||
await WindowManager.CloseSessionExpiration();
|
||||
} catch (e) {
|
||||
setBusy(false);
|
||||
await errorDialog({
|
||||
Title: t("connect.error.loginTitle"),
|
||||
Message: formatErrorMessage(e),
|
||||
});
|
||||
}
|
||||
}, [busy, t]);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
@@ -185,7 +200,7 @@ export default function SessionExpirationDialog() {
|
||||
variant={"primary"}
|
||||
size={"md"}
|
||||
className={"w-full"}
|
||||
onClick={stay}
|
||||
onClick={expired ? authenticate : stay}
|
||||
disabled={busy}
|
||||
>
|
||||
{expired ? t("sessionExpiration.authenticate") : t("sessionExpiration.stay")}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ManagementServerSwitch } from "@/components/ManagementServerSwitch.tsx"
|
||||
import { ManagementMode, useManagementUrl } from "@/hooks/useManagementUrl.ts";
|
||||
import { LanguagePicker } from "@/components/LanguagePicker.tsx";
|
||||
import { useRestrictions } from "@/contexts/RestrictionsContext.tsx";
|
||||
import { useKeepConnectedOnQuit } from "@/hooks/useKeepConnectedOnQuit.ts";
|
||||
|
||||
export function SettingsGeneral() {
|
||||
const { t } = useTranslation();
|
||||
@@ -19,6 +20,7 @@ export function SettingsGeneral() {
|
||||
const { mode, setMode, setUrl, displayUrl, showError, canSave, save, checking, unreachable } =
|
||||
useManagementUrl();
|
||||
const { mdm, features } = useRestrictions();
|
||||
const { keepConnected, setKeepConnectedOnQuit } = useKeepConnectedOnQuit();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const managementUrlId = useId();
|
||||
@@ -57,6 +59,15 @@ export function SettingsGeneral() {
|
||||
helpText={t("settings.general.autostart.help")}
|
||||
/>
|
||||
)}
|
||||
<FancyToggleSwitch
|
||||
value={keepConnected ?? false}
|
||||
onChange={(v) => {
|
||||
void setKeepConnectedOnQuit(v);
|
||||
}}
|
||||
loading={keepConnected === null}
|
||||
label={t("settings.general.keepConnectedOnQuit.label")}
|
||||
helpText={t("settings.general.keepConnectedOnQuit.help")}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
{!mdm.managementURL && !features.disableUpdateSettings && (
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Ändern des Autostarts fehlgeschlagen"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Nach dem Beenden verbunden bleiben",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "Die Verbindung bleibt im Hintergrund bestehen, nachdem Sie NetBird schließen. Sie endet erst, wenn Sie sie selbst trennen.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Anzeigesprache"
|
||||
},
|
||||
|
||||
@@ -735,6 +735,14 @@
|
||||
"message": "Autostart Change Failed",
|
||||
"description": "Error-dialog title when changing the autostart setting fails."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Stay Connected After Quitting",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "The connection stays up in the background after you close NetBird. It only stops when you disconnect it yourself.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Display Language",
|
||||
"description": "Label for the display-language picker."
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Error al cambiar el inicio automático"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Permanecer conectado al salir",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "La conexión sigue activa en segundo plano después de cerrar NetBird. Solo se detiene cuando la desconectas tú.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Idioma de la interfaz"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Échec de la modification du démarrage automatique"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Rester connecté après la fermeture",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "La connexion reste active en arrière-plan après la fermeture de NetBird. Elle ne s'arrête que si vous la coupez vous-même.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Langue d’affichage"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Az automatikus indítás módosítása sikertelen"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Kapcsolat megtartása kilépéskor",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "A kapcsolat a háttérben megmarad, miután bezárod a NetBirdöt. Csak akkor szakad meg, ha te magad bontod.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Megjelenítési nyelv"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Modifica avvio automatico non riuscita"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Resta connesso dopo la chiusura",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "La connessione resta attiva in background dopo la chiusura di NetBird. Si interrompe solo quando la disconnetti tu.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Lingua dell'interfaccia"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "自動起動の変更に失敗しました"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "終了後も接続を維持",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "NetBird を閉じたあとも接続はバックグラウンドで維持されます。自分で切断したときにだけ停止します。",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "表示言語"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Falha ao alterar o início automático"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Permanecer conectado ao sair",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "A conexão continua ativa em segundo plano depois de fechar o NetBird. Ela só para quando você mesmo a desconecta.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Idioma de exibição"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "Не удалось изменить автозапуск"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "Оставаться подключённым после выхода",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "Соединение остаётся активным в фоне после закрытия NetBird. Оно прервётся, только когда вы отключите его сами.",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "Язык интерфейса"
|
||||
},
|
||||
|
||||
@@ -551,6 +551,14 @@
|
||||
"settings.general.autostart.errorTitle": {
|
||||
"message": "更改自启动设置失败"
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.label": {
|
||||
"message": "退出后保持连接",
|
||||
"description": "Toggle label: keep the VPN connection up after quitting the UI."
|
||||
},
|
||||
"settings.general.keepConnectedOnQuit.help": {
|
||||
"message": "关闭 NetBird 后,连接会在后台保持。只有你自己断开时才会停止。",
|
||||
"description": "Helper text for the stay-connected-after-quitting toggle."
|
||||
},
|
||||
"settings.general.language.label": {
|
||||
"message": "显示语言"
|
||||
},
|
||||
|
||||
@@ -180,6 +180,7 @@ func main() {
|
||||
WindowManager: windowManager,
|
||||
Session: authSession,
|
||||
Localizer: localizer,
|
||||
Preferences: prefStore,
|
||||
})
|
||||
listenForShowSignal(context.Background(), tray)
|
||||
|
||||
|
||||
@@ -58,6 +58,10 @@ type UIPreferences struct {
|
||||
// decision has run for this OS user. It only ever transitions to true
|
||||
// and is never reset, so the default-on flow runs at most once, ever.
|
||||
AutostartInitialized bool `json:"autostartInitialized"`
|
||||
// KeepConnectedOnQuit leaves the daemon connected when the GUI quits.
|
||||
// Its false zero value preserves the historical disconnect-on-quit
|
||||
// behaviour for preference files written before the field existed.
|
||||
KeepConnectedOnQuit bool `json:"keepConnectedOnQuit"`
|
||||
}
|
||||
|
||||
// LanguageValidator rejects SetLanguage inputs with no shipped bundle.
|
||||
@@ -183,6 +187,26 @@ func (s *Store) SetAutostartInitialized(done bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetKeepConnectedOnQuit persists the disconnect-on-quit opt-out. No-op if unchanged.
|
||||
func (s *Store) SetKeepConnectedOnQuit(keep bool) error {
|
||||
s.mu.Lock()
|
||||
if s.current.KeepConnectedOnQuit == keep {
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
next := s.current
|
||||
next.KeepConnectedOnQuit = keep
|
||||
if err := s.persistLocked(next); err != nil {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("persist preferences: %w", err)
|
||||
}
|
||||
s.current = next
|
||||
s.mu.Unlock()
|
||||
|
||||
s.broadcast(next)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLanguage validates, persists, and broadcasts. No-op if unchanged.
|
||||
func (s *Store) SetLanguage(lang i18n.LanguageCode) error {
|
||||
if lang == "" {
|
||||
|
||||
@@ -238,6 +238,42 @@ func TestStore_SetAutostartInitializedPersistsAcrossReload(t *testing.T) {
|
||||
assert.True(t, reloaded.Get().AutostartInitialized, "marker must survive a reload from disk")
|
||||
}
|
||||
|
||||
func TestStore_SetKeepConnectedOnQuitPersistsAcrossReload(t *testing.T) {
|
||||
withTempConfigDir(t)
|
||||
emitter := &recordingEmitter{}
|
||||
s, err := NewStore(nil, emitter)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, s.Get().KeepConnectedOnQuit, "quitting must disconnect by default")
|
||||
|
||||
require.NoError(t, s.SetKeepConnectedOnQuit(true))
|
||||
assert.True(t, s.Get().KeepConnectedOnQuit, "Get should reflect the persisted opt-out")
|
||||
require.Len(t, emitter.calledWith(EventPreferencesChanged), 1, "first write should broadcast")
|
||||
|
||||
require.NoError(t, s.SetKeepConnectedOnQuit(true))
|
||||
assert.Len(t, emitter.calledWith(EventPreferencesChanged), 1, "idempotent write should not broadcast again")
|
||||
|
||||
reloaded, err := NewStore(nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, reloaded.Get().KeepConnectedOnQuit, "opt-out must survive a reload from disk")
|
||||
}
|
||||
|
||||
func TestStore_KeepConnectedOnQuitDefaultsFalseForPreExistingFile(t *testing.T) {
|
||||
withTempConfigDir(t)
|
||||
|
||||
// A preferences file written before the field existed must keep the
|
||||
// historical disconnect-on-quit behaviour rather than silently opting out.
|
||||
path, err := preferencesPath()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
||||
require.NoError(t, os.WriteFile(path, []byte(`{"language":"en","viewMode":"default"}`), 0o600))
|
||||
|
||||
s, err := NewStore(nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, s.Get().KeepConnectedOnQuit, "a file predating the field must not opt out of disconnect-on-quit")
|
||||
assert.True(t, s.ExistedAtLoad(), "the pre-existing file must be seen on disk")
|
||||
}
|
||||
|
||||
func TestStore_ExistedAtLoad(t *testing.T) {
|
||||
withTempConfigDir(t)
|
||||
|
||||
|
||||
@@ -49,5 +49,11 @@ func getCursorPosition(app *application.App) (application.Point, bool) {
|
||||
if app == nil || app.Screen == nil {
|
||||
return p, true
|
||||
}
|
||||
// The wails GTK3 backend caches screens from the active window; a tray app
|
||||
// has none at startup, so the cache is empty and PhysicalToDipPoint would
|
||||
// dereference a nil nearest screen. Raw pixels are correct there anyway.
|
||||
if app.Screen.ScreenNearestPhysicalPoint(p) == nil {
|
||||
return p, true
|
||||
}
|
||||
return app.Screen.PhysicalToDipPoint(p), true
|
||||
}
|
||||
|
||||
@@ -34,3 +34,7 @@ func (s *Preferences) SetViewMode(_ context.Context, mode preferences.ViewMode)
|
||||
func (s *Preferences) SetOnboardingCompleted(_ context.Context, done bool) error {
|
||||
return s.store.SetOnboardingCompleted(done)
|
||||
}
|
||||
|
||||
func (s *Preferences) SetKeepConnectedOnQuit(_ context.Context, keep bool) error {
|
||||
return s.store.SetKeepConnectedOnQuit(keep)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/netbirdio/netbird/client/ui/authsession"
|
||||
"github.com/netbirdio/netbird/client/ui/i18n"
|
||||
"github.com/netbirdio/netbird/client/ui/preferences"
|
||||
"github.com/netbirdio/netbird/client/ui/services"
|
||||
"github.com/netbirdio/netbird/version"
|
||||
)
|
||||
@@ -50,8 +51,9 @@ type TrayServices struct {
|
||||
WindowManager *services.WindowManager
|
||||
// Session is bound to authsession directly because the services wrapper
|
||||
// only re-exposes the React subset.
|
||||
Session *authsession.Session
|
||||
Localizer *Localizer
|
||||
Session *authsession.Session
|
||||
Localizer *Localizer
|
||||
Preferences *preferences.Store
|
||||
}
|
||||
|
||||
type Tray struct {
|
||||
@@ -461,10 +463,12 @@ func (t *Tray) handleQuit() {
|
||||
t.profileMu.Unlock()
|
||||
t.svc.DaemonFeed.CancelProfileSwitch()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), quitDownTimeout)
|
||||
defer cancel()
|
||||
if err := t.svc.Connection.Down(ctx); err != nil {
|
||||
log.Errorf("disconnect on quit: %v", err)
|
||||
if t.svc.Preferences == nil || !t.svc.Preferences.Get().KeepConnectedOnQuit {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), quitDownTimeout)
|
||||
defer cancel()
|
||||
if err := t.svc.Connection.Down(ctx); err != nil {
|
||||
log.Errorf("disconnect on quit: %v", err)
|
||||
}
|
||||
}
|
||||
t.app.Quit()
|
||||
}
|
||||
|
||||
@@ -4,17 +4,26 @@ package main
|
||||
|
||||
// bindTrayClick wires the tray icon's left-click handler on Linux.
|
||||
//
|
||||
// Both Linux click paths converge on Wails' linuxSystemTray.Activate, which
|
||||
// fires the registered clickHandler:
|
||||
// - Real SNI hosts (KDE Plasma, Waybar, GNOME Shell + AppIndicator) invoke
|
||||
// org.kde.StatusNotifierItem.Activate over D-Bus on left-click.
|
||||
// - The in-process StatusNotifierWatcher + XEmbed host used on minimal WMs
|
||||
// (Fluxbox, i3, dwm, OpenBox) maps a Button1 press to that same Activate
|
||||
// call itself (xembed_host_linux.go), so it routes through the same hook.
|
||||
// Registering OnClick here therefore covers both paths with one handler — no
|
||||
// changes to the watcher or XEmbed C code are needed. Left-click now opens the
|
||||
// main window; right-click still opens the menu via Wails' default
|
||||
// SecondaryActivate→OpenMenu handler (and the XEmbed GTK popup on minimal WMs).
|
||||
// Expected behaviour per tray host:
|
||||
//
|
||||
// Host Left click Right click
|
||||
// KDE Plasma, Waybar main window (Activate) menu (host-rendered)
|
||||
// GNOME Shell + AppIndicator menu only menu only
|
||||
// Minimal WMs via XEmbed host main window (Activate) XEmbed GTK popup
|
||||
//
|
||||
// OnClick fires only on org.kde.StatusNotifierItem.Activate — a real left
|
||||
// click. KDE/Waybar send it over D-Bus; the in-process XEmbed host
|
||||
// (xembed_host_linux.go) maps a Button1 press to the same Activate call.
|
||||
//
|
||||
// GNOME Shell + AppIndicator never sends Activate: it renders the dbusmenu
|
||||
// on ANY click and only reports the menu opening via dbusmenu
|
||||
// Event("opened"). Upstream Wails treated that event as a click, so on GNOME
|
||||
// both buttons raised the main window on top of the menu, and on KDE/Waybar
|
||||
// a right click raised it over the freshly opened menu. The netbirdio/wails
|
||||
// fork (go.mod replace) drops that heuristic: a menu open never fires
|
||||
// OnClick. On GNOME the main window is reached via the "Open NetBird" menu
|
||||
// entry; left-click-opens-window is not achievable there anyway, since the
|
||||
// host always opens the menu itself.
|
||||
//
|
||||
// We do NOT register OnDoubleClick: Wails' Linux SNI backend never fires it
|
||||
// (unlike Windows). And we deliberately skip AttachWindow — it plus Wails3's
|
||||
|
||||
@@ -27,11 +27,10 @@ const (
|
||||
finalWarningCountdownSeconds = 120
|
||||
)
|
||||
|
||||
// handleSessionExpired notifies and brings the window forward so the frontend's /login route drives renewal.
|
||||
// handleSessionExpired notifies and brings the window forward so the user can reconnect.
|
||||
func (t *Tray) handleSessionExpired() {
|
||||
t.notify(t.loc.T("notify.sessionExpired.title"), t.loc.T("notify.sessionExpired.body"), notifyIDSessionExpired)
|
||||
if t.window != nil {
|
||||
t.window.SetURL("/#/login")
|
||||
t.window.Show()
|
||||
t.window.Focus()
|
||||
}
|
||||
@@ -308,11 +307,7 @@ func (t *Tray) openSessionExtendFlow() {
|
||||
}
|
||||
seconds := int(time.Until(deadline).Seconds())
|
||||
if seconds <= 0 {
|
||||
if t.window != nil {
|
||||
t.window.SetURL("/#/login")
|
||||
t.window.Show()
|
||||
t.window.Focus()
|
||||
}
|
||||
t.app.Event.Emit(services.EventTriggerLogin)
|
||||
return
|
||||
}
|
||||
if t.svc.WindowManager == nil {
|
||||
|
||||
40
client/ui/xembed_host_gtk3_linux.go
Normal file
40
client/ui/xembed_host_gtk3_linux.go
Normal file
@@ -0,0 +1,40 @@
|
||||
//go:build linux && gtk3 && !(linux && 386)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
// The legacy GTK3 / WebKit2GTK 4.1 build (-tags gtk3) drops the in-process
|
||||
// XEmbed StatusNotifierWatcher entirely. The real implementation
|
||||
// (xembed_host_linux.go + xembed_tray_linux.c) links GTK4 and uses GTK4-only
|
||||
// popup-menu APIs that have no drop-in GTK3 equivalent, so rather than port the
|
||||
// C layer we stub the host out on gtk3 builds. The tray still works on every
|
||||
// desktop that ships its own StatusNotifierWatcher (KDE, GNOME+AppIndicator,
|
||||
// Cinnamon/xapp, XFCE, …); only the minimal-WM fallback (Fluxbox/OpenBox/i3/
|
||||
// dwm/vanilla GNOME) is unavailable on gtk3 packages. See LINUX-TRAY.md.
|
||||
|
||||
// xembedHost is a placeholder so the package compiles on gtk3 builds; the real
|
||||
// type (with X11/GTK4 state) lives in xembed_host_linux.go. It is never
|
||||
// instantiated here because xembedTrayAvailable always reports false.
|
||||
type xembedHost struct{}
|
||||
|
||||
// run satisfies the call in tray_watcher_linux.go; unreachable on gtk3 because
|
||||
// newXembedHost never returns a non-nil host.
|
||||
func (*xembedHost) run() {}
|
||||
|
||||
// xembedTrayAvailable always reports false on gtk3 builds, so the watcher probe
|
||||
// loop in startStatusNotifierWatcher exits immediately and newXembedHost is
|
||||
// never reached. recenter_linux.go's predicate becomes a harmless no-op too.
|
||||
func xembedTrayAvailable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// newXembedHost exists only to satisfy the reference in tray_watcher_linux.go;
|
||||
// it is unreachable because xembedTrayAvailable returns false on gtk3.
|
||||
func newXembedHost(conn *dbus.Conn, busName string, objPath dbus.ObjectPath) (*xembedHost, error) {
|
||||
return nil, errors.New("xembed host unsupported on gtk3 build")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build linux && !(linux && 386)
|
||||
//go:build linux && !gtk3 && !(linux && 386)
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build linux && !gtk3 && !(linux && 386)
|
||||
|
||||
#include "xembed_tray_linux.h"
|
||||
|
||||
#include <X11/Xatom.h>
|
||||
|
||||
2
go.mod
2
go.mod
@@ -339,3 +339,5 @@ replace github.com/dexidp/dex => github.com/netbirdio/dex v0.244.1-0.20260716205
|
||||
replace github.com/dexidp/dex/api/v2 => github.com/netbirdio/dex/api/v2 v2.0.0-20260512110716-8d70ad8647c1
|
||||
|
||||
replace github.com/mailru/easyjson => github.com/netbirdio/easyjson v0.9.0
|
||||
|
||||
replace github.com/wailsapp/wails/v3 => github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260803205919-ad21e92381f4
|
||||
|
||||
4
go.sum
4
go.sum
@@ -490,6 +490,8 @@ github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9ax
|
||||
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ=
|
||||
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ=
|
||||
github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260803205919-ad21e92381f4 h1:UKztc3QjWvzU5DZk+uYaOWN0x62NSe/pkxuPvzqZIy4=
|
||||
github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260803205919-ad21e92381f4/go.mod h1:BzATbK71VFikMMMCo434wAi0QcaI03P+xeaWgDvQvjw=
|
||||
github.com/netbirdio/wireguard-go v0.0.0-20260628102922-2834bebf6c1a h1:3CWK+yTvRKOcC0Q8VCTGy4l60TEb27CQVS7LkMxwjmw=
|
||||
github.com/netbirdio/wireguard-go v0.0.0-20260628102922-2834bebf6c1a/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
@@ -660,8 +662,6 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-beta.3 h1:BrcZunEBVucncRx+xgkk9TzlXU4qc0ygJuEhKAAGaeA=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-beta.3/go.mod h1:BzATbK71VFikMMMCo434wAi0QcaI03P+xeaWgDvQvjw=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
|
||||
@@ -11,6 +11,10 @@ SED_STRIP_PADDING='s/=//g'
|
||||
|
||||
NETBIRD_EULA_URL="https://netbird.io/self-hosted-EULA"
|
||||
|
||||
# Static IP for Traefik inside the compose bridge network. The management
|
||||
# server trusts X-Forwarded-* headers from this address only.
|
||||
TRAEFIK_IP="172.30.0.10"
|
||||
|
||||
check_docker_compose() {
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
echo "docker-compose"
|
||||
@@ -80,7 +84,7 @@ read_nb_domain() {
|
||||
if ! check_domain_resolves "$value"; then
|
||||
echo "" > /dev/stderr
|
||||
echo "Warning: '$value' does not resolve via DNS from this host." > /dev/stderr
|
||||
echo "Caddy will not be able to issue TLS certificates until it does." > /dev/stderr
|
||||
echo "Traefik will not be able to issue TLS certificates until it does." > /dev/stderr
|
||||
local confirm=""
|
||||
echo -n "Continue anyway? [y/N]: " > /dev/stderr
|
||||
read -r confirm < /dev/tty
|
||||
@@ -92,6 +96,23 @@ read_nb_domain() {
|
||||
echo "$value"
|
||||
}
|
||||
|
||||
read_letsencrypt_email() {
|
||||
if [[ -n "${NETBIRD_LETSENCRYPT_EMAIL:-}" ]]; then
|
||||
echo "$NETBIRD_LETSENCRYPT_EMAIL"
|
||||
return
|
||||
fi
|
||||
local value=""
|
||||
echo "Enter your email for Let's Encrypt certificate notifications." > /dev/stderr
|
||||
echo -n "Email address: " > /dev/stderr
|
||||
read -r value < /dev/tty
|
||||
if [[ -z "$value" ]]; then
|
||||
echo "Email is required for Let's Encrypt." > /dev/stderr
|
||||
read_letsencrypt_email
|
||||
return
|
||||
fi
|
||||
echo "$value"
|
||||
}
|
||||
|
||||
read_required() {
|
||||
local prompt="$1"
|
||||
local value=""
|
||||
@@ -204,11 +225,11 @@ init_environment() {
|
||||
check_openssl
|
||||
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
|
||||
|
||||
if [[ -f .env ]] || [[ -f docker-compose.yml ]] || [[ -f config.yaml ]] || [[ -f Caddyfile ]]; then
|
||||
if [[ -f .env ]] || [[ -f docker-compose.yml ]] || [[ -f config.yaml ]]; then
|
||||
echo "Generated files already exist in $(pwd)."
|
||||
echo "If you want to reinitialize the environment, please remove them first:"
|
||||
echo " $DOCKER_COMPOSE_COMMAND down --volumes # removes all containers and volumes"
|
||||
echo " rm -f .env docker-compose.yml Caddyfile config.yaml"
|
||||
echo " rm -f .env docker-compose.yml config.yaml"
|
||||
echo "Be aware this will remove all data from the database."
|
||||
exit 1
|
||||
fi
|
||||
@@ -230,6 +251,9 @@ init_environment() {
|
||||
echo ""
|
||||
NETBIRD_DOMAIN=$(read_nb_domain)
|
||||
|
||||
echo ""
|
||||
NETBIRD_LETSENCRYPT_EMAIL=$(read_letsencrypt_email)
|
||||
|
||||
echo ""
|
||||
|
||||
NETBIRD_LICENSE_KEY=$(read_secret "Enter license key (input hidden)")
|
||||
@@ -238,6 +262,7 @@ init_environment() {
|
||||
POSTGRES_DB="netbird"
|
||||
POSTGRES_PASSWORD=$(rand_secret)
|
||||
NETBIRD_ENCRYPTION_KEY=$(rand_b64_key)
|
||||
NETBIRD_SESSION_COOKIE_ENCRYPTION_KEY=$(rand_b64_key)
|
||||
NETBIRD_RELAY_AUTH_SECRET=$(rand_secret)
|
||||
|
||||
POSTGRES_DSN="host=postgres user=${POSTGRES_USER} password=${POSTGRES_PASSWORD} dbname=${POSTGRES_DB} port=5432 sslmode=disable TimeZone=UTC"
|
||||
@@ -247,6 +272,7 @@ init_environment() {
|
||||
echo "Selected:"
|
||||
echo " Traffic flow: ${NETBIRD_TRAFFIC_FLOW}"
|
||||
echo " Domain: ${NETBIRD_DOMAIN}"
|
||||
echo " ACME email: ${NETBIRD_LETSENCRYPT_EMAIL}"
|
||||
echo ""
|
||||
echo "Rendering files into $(pwd) ..."
|
||||
install -m 600 /dev/null .env
|
||||
@@ -256,7 +282,6 @@ init_environment() {
|
||||
if [[ -z "${NETBIRD_LICENSE_SERVER_BASE_URL:-}" ]]; then
|
||||
sed -i.bak '/NETBIRD_LICENSE_SERVER_BASE_URL/d' docker-compose.yml && rm -f docker-compose.yml.bak
|
||||
fi
|
||||
render_caddyfile > Caddyfile
|
||||
install -m 600 /dev/null config.yaml
|
||||
render_config_yaml >> config.yaml
|
||||
|
||||
@@ -283,7 +308,7 @@ init_environment() {
|
||||
echo "All configuration and secrets are stored (mode 600) in $(pwd)/.env"
|
||||
echo ""
|
||||
echo "Tail logs:"
|
||||
echo " cd $(pwd) && $DOCKER_COMPOSE_COMMAND logs -f netbird-server caddy"
|
||||
echo " cd $(pwd) && $DOCKER_COMPOSE_COMMAND logs -f netbird-server traefik"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -306,6 +331,11 @@ NETBIRD_TRAFFIC_FLOW_ENABLED=${NETBIRD_TRAFFIC_FLOW}
|
||||
# Domain
|
||||
NETBIRD_DOMAIN=${NETBIRD_DOMAIN}
|
||||
|
||||
# Reverse proxy (Traefik)
|
||||
NETBIRD_LETSENCRYPT_EMAIL=${NETBIRD_LETSENCRYPT_EMAIL}
|
||||
NETBIRD_TRAEFIK_TAG=${NETBIRD_TRAEFIK_TAG:-v3.6}
|
||||
NETBIRD_TRAEFIK_IP=${TRAEFIK_IP}
|
||||
|
||||
# Image tags. Default to "latest"
|
||||
NETBIRD_DASHBOARD_TAG=${NETBIRD_DASHBOARD_TAG:-latest}
|
||||
NETBIRD_SERVER_TAG=${NETBIRD_SERVER_TAG:-latest}
|
||||
@@ -378,26 +408,78 @@ EOF
|
||||
|
||||
render_compose_common() {
|
||||
cat <<'EOF'
|
||||
caddy:
|
||||
# Reverse proxy with automatic TLS via Let's Encrypt. Routes are declared as
|
||||
# labels on the services below and picked up through the Docker provider.
|
||||
traefik:
|
||||
<<: *default
|
||||
image: caddy:2
|
||||
container_name: netbird-caddy
|
||||
networks: [netbird]
|
||||
environment:
|
||||
- CADDY_SECURE_DOMAIN=${NETBIRD_DOMAIN}
|
||||
image: traefik:${NETBIRD_TRAEFIK_TAG}
|
||||
container_name: netbird-traefik
|
||||
networks:
|
||||
netbird:
|
||||
ipv4_address: ${NETBIRD_TRAEFIK_IP}
|
||||
command:
|
||||
# Logging
|
||||
- "--log.level=INFO"
|
||||
- "--accesslog=true"
|
||||
# Docker provider
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--providers.docker.network=netbird"
|
||||
# Entrypoints
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--entrypoints.websecure.allowACMEByPass=true"
|
||||
# readTimeout bounds the whole request, and gRPC streams / relay WebSockets
|
||||
# never end one; idleTimeout would close the keep-alive connection they
|
||||
# are reused over. Entrypoint-wide is the only scope Traefik offers here.
|
||||
# writeTimeout is left alone: it already defaults to 0.
|
||||
- "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0"
|
||||
- "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=0"
|
||||
# HTTP to HTTPS redirect
|
||||
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
|
||||
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
|
||||
# Let's Encrypt ACME
|
||||
- "--certificatesresolvers.letsencrypt.acme.email=${NETBIRD_LETSENCRYPT_EMAIL}"
|
||||
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
|
||||
ports:
|
||||
- '443:443'
|
||||
- '443:443/udp'
|
||||
- '80:80'
|
||||
volumes:
|
||||
- netbird_caddy_data:/data
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- netbird_traefik_letsencrypt:/letsencrypt
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
# Shared security headers, referenced by every NetBird router below. A
|
||||
# label-declared middleware only exists while its container runs, so this
|
||||
# lives on Traefik itself: declaring it on an app container would drop
|
||||
# every router referencing it whenever that container restarts.
|
||||
- traefik.http.middlewares.nb-security.headers.stsSeconds=3600
|
||||
- traefik.http.middlewares.nb-security.headers.stsIncludeSubdomains=true
|
||||
- traefik.http.middlewares.nb-security.headers.contentTypeNosniff=true
|
||||
- traefik.http.middlewares.nb-security.headers.browserXssFilter=true
|
||||
- traefik.http.middlewares.nb-security.headers.referrerPolicy=strict-origin-when-cross-origin
|
||||
- traefik.http.middlewares.nb-security.headers.customResponseHeaders.X-Frame-Options=SAMEORIGIN
|
||||
# Empty value strips the header. Only the dashboard's nginx sets one; the
|
||||
# server emits none. Do not quote it — "" would send a literal Server: "".
|
||||
- traefik.http.middlewares.nb-security.headers.customResponseHeaders.Server=
|
||||
|
||||
dashboard:
|
||||
<<: *default
|
||||
image: ghcr.io/netbirdio/dashboard-cloud:${NETBIRD_DASHBOARD_TAG}
|
||||
container_name: netbird-dashboard
|
||||
networks: [netbird]
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
# Dashboard catch-all: lowest priority so every route below wins
|
||||
- traefik.http.routers.netbird-dashboard.rule=Host(`${NETBIRD_DOMAIN}`)
|
||||
- traefik.http.routers.netbird-dashboard.entrypoints=websecure
|
||||
- traefik.http.routers.netbird-dashboard.tls=true
|
||||
- traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.netbird-dashboard.middlewares=nb-security@docker
|
||||
- traefik.http.routers.netbird-dashboard.service=dashboard
|
||||
- traefik.http.routers.netbird-dashboard.priority=1
|
||||
- traefik.http.services.dashboard.loadbalancer.server.port=80
|
||||
environment:
|
||||
- NETBIRD_MGMT_API_ENDPOINT=https://${NETBIRD_DOMAIN}
|
||||
- NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${NETBIRD_DOMAIN}
|
||||
@@ -435,6 +517,28 @@ render_compose_server() {
|
||||
- netbird_data:/var/lib/netbird
|
||||
- ./config.yaml:/etc/netbird/config.yaml
|
||||
command: ["--config", "/etc/netbird/config.yaml"]
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
# Signal + Management gRPC (needs an h2c backend for HTTP/2 cleartext)
|
||||
- traefik.http.routers.netbird-grpc.rule=Host(`${NETBIRD_DOMAIN}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))
|
||||
- traefik.http.routers.netbird-grpc.entrypoints=websecure
|
||||
- traefik.http.routers.netbird-grpc.tls=true
|
||||
- traefik.http.routers.netbird-grpc.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.netbird-grpc.middlewares=nb-security@docker
|
||||
- traefik.http.routers.netbird-grpc.service=netbird-server-h2c
|
||||
- traefik.http.routers.netbird-grpc.priority=100
|
||||
# Relay WebSocket, management API, and the embedded IdP
|
||||
- traefik.http.routers.netbird-backend.rule=Host(`${NETBIRD_DOMAIN}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))
|
||||
- traefik.http.routers.netbird-backend.entrypoints=websecure
|
||||
- traefik.http.routers.netbird-backend.tls=true
|
||||
- traefik.http.routers.netbird-backend.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.netbird-backend.middlewares=nb-security@docker
|
||||
- traefik.http.routers.netbird-backend.service=netbird-server
|
||||
- traefik.http.routers.netbird-backend.priority=100
|
||||
# Services
|
||||
- traefik.http.services.netbird-server.loadbalancer.server.port=80
|
||||
- traefik.http.services.netbird-server-h2c.loadbalancer.server.port=80
|
||||
- traefik.http.services.netbird-server-h2c.loadbalancer.server.scheme=h2c
|
||||
environment:
|
||||
- NB_LICENSE_KEY=${NETBIRD_LICENSE_KEY}
|
||||
- NETBIRD_LICENSE_SERVER_BASE_URL=${NETBIRD_LICENSE_SERVER_BASE_URL}
|
||||
@@ -497,6 +601,18 @@ render_compose_flow() {
|
||||
- NB_FLOW_NATS_ENDPOINTS=nats://nats:4222
|
||||
- NB_FLOW_NATS_STREAM=traffic-events
|
||||
- NB_FLOW_AUTH_SECRET=${NETBIRD_RELAY_AUTH_SECRET}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
# Flow receiver gRPC (h2c backend)
|
||||
- traefik.http.routers.netbird-flow.rule=Host(`${NETBIRD_DOMAIN}`) && PathPrefix(`/flow.FlowService/`)
|
||||
- traefik.http.routers.netbird-flow.entrypoints=websecure
|
||||
- traefik.http.routers.netbird-flow.tls=true
|
||||
- traefik.http.routers.netbird-flow.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.netbird-flow.middlewares=nb-security@docker
|
||||
- traefik.http.routers.netbird-flow.service=netbird-flow-h2c
|
||||
- traefik.http.routers.netbird-flow.priority=100
|
||||
- traefik.http.services.netbird-flow-h2c.loadbalancer.server.port=80
|
||||
- traefik.http.services.netbird-flow-h2c.loadbalancer.server.scheme=h2c
|
||||
|
||||
EOF
|
||||
}
|
||||
@@ -536,61 +652,16 @@ EOF
|
||||
fi
|
||||
cat <<'EOF'
|
||||
netbird_postgres:
|
||||
netbird_caddy_data:
|
||||
netbird_traefik_letsencrypt:
|
||||
|
||||
networks:
|
||||
netbird:
|
||||
EOF
|
||||
}
|
||||
|
||||
render_caddyfile() {
|
||||
cat <<'EOF'
|
||||
{
|
||||
servers :80,:443 {
|
||||
protocols h1 h2c h2 h3
|
||||
}
|
||||
}
|
||||
|
||||
(security_headers) {
|
||||
header * {
|
||||
Strict-Transport-Security "max-age=3600; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
-Server
|
||||
Referrer-Policy strict-origin-when-cross-origin
|
||||
}
|
||||
}
|
||||
|
||||
:80 {
|
||||
redir https://{$CADDY_SECURE_DOMAIN}{uri} permanent
|
||||
}
|
||||
|
||||
{$CADDY_SECURE_DOMAIN}:443 {
|
||||
import security_headers
|
||||
# Signal (gRPC over h2c)
|
||||
reverse_proxy /signalexchange.SignalExchange/* h2c://netbird-server:80
|
||||
# Management (gRPC over h2c + HTTP)
|
||||
reverse_proxy /management.ManagementService/* h2c://netbird-server:80
|
||||
reverse_proxy /api/* netbird-server:80
|
||||
reverse_proxy /ws-proxy/* netbird-server:80
|
||||
# Embedded IdP (OAuth2 endpoints served by netbird server)
|
||||
reverse_proxy /oauth2/* netbird-server:80
|
||||
# Relay (WebSocket multiplexed on the same port)
|
||||
reverse_proxy /relay* netbird-server:80
|
||||
EOF
|
||||
|
||||
if [[ "$NETBIRD_TRAFFIC_FLOW" == "yes" ]]; then
|
||||
cat <<'EOF'
|
||||
# Flow receiver (gRPC over h2c)
|
||||
reverse_proxy /flow.FlowService/* h2c://receiver:80
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat <<'EOF'
|
||||
# Dashboard
|
||||
reverse_proxy /* dashboard:80
|
||||
}
|
||||
name: netbird
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.30.0.0/24
|
||||
gateway: 172.30.0.1
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -609,7 +680,7 @@ server:
|
||||
logLevel: "info"
|
||||
logFile: "console"
|
||||
|
||||
# TLS is terminated by Caddy in front; leave this block empty.
|
||||
# TLS is terminated by Traefik in front; leave this block empty.
|
||||
tls:
|
||||
certFile: ""
|
||||
keyFile: ""
|
||||
@@ -626,12 +697,23 @@ server:
|
||||
issuer: "https://${NETBIRD_DOMAIN}/oauth2"
|
||||
localAuthDisabled: false
|
||||
signKeyRefreshEnabled: false
|
||||
sessionCookieEncryptionKey: "${NETBIRD_SESSION_COOKIE_ENCRYPTION_KEY}"
|
||||
dashboardRedirectURIs:
|
||||
- "https://${NETBIRD_DOMAIN}/nb-auth"
|
||||
- "https://${NETBIRD_DOMAIN}/nb-silent-auth"
|
||||
cliRedirectURIs:
|
||||
- "http://localhost:53000/"
|
||||
|
||||
# Trust X-Forwarded-* only from the Traefik container's static address. Both
|
||||
# keys must stay in step with the ipv4_address pinned in docker-compose.yml:
|
||||
# trustedPeers decides whether forwarded headers are read at all, and leaving
|
||||
# it unset falls back to 0.0.0.0/0.
|
||||
reverseProxy:
|
||||
trustedPeers:
|
||||
- "${TRAEFIK_IP}/32"
|
||||
trustedHTTPProxies:
|
||||
- "${TRAEFIK_IP}/32"
|
||||
|
||||
store:
|
||||
engine: "postgres"
|
||||
dsn: "${POSTGRES_DSN}"
|
||||
|
||||
@@ -348,6 +348,7 @@ initialize_default_values() {
|
||||
NETBIRD_RELAY_AUTH_SECRET=$(openssl rand -base64 32 | sed "$SED_STRIP_PADDING")
|
||||
# Note: DataStoreEncryptionKey must keep base64 padding (=) for Go's base64.StdEncoding
|
||||
DATASTORE_ENCRYPTION_KEY=$(openssl rand -base64 32)
|
||||
SESSION_COOKIE_ENCRYPTION_KEY=$(openssl rand -base64 32)
|
||||
NETBIRD_STUN_PORT=3478
|
||||
|
||||
# Docker images
|
||||
@@ -527,7 +528,8 @@ generate_configuration_files() {
|
||||
|
||||
# Common files for all configurations
|
||||
render_dashboard_env > dashboard.env
|
||||
render_combined_yaml > config.yaml
|
||||
install -m 600 /dev/null config.yaml
|
||||
render_combined_yaml >> config.yaml
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -911,6 +913,7 @@ server:
|
||||
auth:
|
||||
issuer: "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/oauth2"
|
||||
signKeyRefreshEnabled: true
|
||||
sessionCookieEncryptionKey: "$SESSION_COOKIE_ENCRYPTION_KEY"
|
||||
dashboardRedirectURIs:
|
||||
- "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/nb-auth"
|
||||
- "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/nb-silent-auth"
|
||||
|
||||
@@ -15,7 +15,11 @@ set -o pipefail
|
||||
# 2. Postgres migration — add Postgres, migrate SQLite data via migrate-store.
|
||||
# 3. Traffic flow — add NATS + flow-enricher + flow-receiver.
|
||||
#
|
||||
# To revert:
|
||||
# If any step fails once the stack has been touched, the script rolls itself
|
||||
# back automatically: generated files are removed, the Postgres volume this run
|
||||
# created is dropped, and the original deployment is started again.
|
||||
#
|
||||
# To revert a successful migration:
|
||||
# docker compose down
|
||||
# rm -f docker-compose.override.yml config.yaml.enterprise
|
||||
# # If Postgres migration was done, also restore the SQLite backup printed
|
||||
@@ -25,6 +29,15 @@ set -o pipefail
|
||||
OVERRIDE_FILE="docker-compose.override.yml"
|
||||
ENTERPRISE_CONFIG_FILE="config.yaml.enterprise"
|
||||
|
||||
# Rollback bookkeeping. ROLLBACK_STATE flips to "armed" the moment the script
|
||||
# starts mutating the deployment, and back to "disarmed" once the migration has
|
||||
# completed successfully.
|
||||
ROLLBACK_STATE="disarmed"
|
||||
ENV_EXISTED="unknown"
|
||||
ENV_BACKUP=""
|
||||
PG_VOLUME_NAME=""
|
||||
BACKUP_DIR=""
|
||||
|
||||
NETBIRD_EULA_URL="https://netbird.io/self-hosted-EULA"
|
||||
|
||||
check_docker_compose() {
|
||||
@@ -361,7 +374,77 @@ render_enterprise_config() {
|
||||
# Execution steps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resolve_data_volume() {
|
||||
combined_container_id() {
|
||||
$DOCKER_COMPOSE_COMMAND ps -aq "$COMBINED_SERVICE" 2>/dev/null | head -1
|
||||
}
|
||||
|
||||
container_data_mount() {
|
||||
local container="$1"
|
||||
[[ -n "$container" ]] || return 0
|
||||
docker inspect "$container" --format \
|
||||
'{{range .Mounts}}{{if eq .Destination "/var/lib/netbird"}}{{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}}{{end}}{{end}}' 2>/dev/null
|
||||
}
|
||||
|
||||
# The name comes from the container, so `-v` cannot invent an empty volume here.
|
||||
# 0 = empty, 1 = holds data, 2 = could not determine. A failed listing must not
|
||||
# be reported as empty: that would abort a healthy migration over a pull error
|
||||
# or an unreadable bind mount.
|
||||
data_dir_state() {
|
||||
local src="$1" out
|
||||
if [[ "$src" == /* ]]; then
|
||||
[[ -d "$src" ]] || return 2
|
||||
out=$(ls -A "$src" 2>/dev/null) || return 2
|
||||
else
|
||||
docker volume inspect "$src" &> /dev/null || return 0
|
||||
out=$(docker run --rm -v "${src}:/d:ro" busybox sh -c 'ls -A /d' 2>/dev/null) || return 2
|
||||
fi
|
||||
[[ -z "$out" ]] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
check_data_directory() {
|
||||
[[ "$MIGRATE_POSTGRES" == "yes" ]] || return 0
|
||||
|
||||
local container
|
||||
container=$(combined_container_id)
|
||||
if [[ -z "$container" ]]; then
|
||||
echo "" > /dev/stderr
|
||||
echo "No container found for service '$COMBINED_SERVICE'." > /dev/stderr
|
||||
echo "The migration backs up the store by copying it out of that container," > /dev/stderr
|
||||
echo "so it has to exist. Start the deployment and re-run:" > /dev/stderr
|
||||
echo " $DOCKER_COMPOSE_COMMAND up -d" > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local src
|
||||
src=$(container_data_mount "$container")
|
||||
if [[ -z "$src" ]]; then
|
||||
echo "" > /dev/stderr
|
||||
echo "The '$COMBINED_SERVICE' container has nothing mounted at /var/lib/netbird." > /dev/stderr
|
||||
echo "Cannot locate the NetBird store to back it up." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local state=0
|
||||
data_dir_state "$src" || state=$?
|
||||
if [[ $state -eq 0 ]]; then
|
||||
echo "" > /dev/stderr
|
||||
echo "The NetBird data directory is empty:" > /dev/stderr
|
||||
echo " $src" > /dev/stderr
|
||||
echo "There is nothing to migrate. Check that you are running this from the" > /dev/stderr
|
||||
echo "deployment directory of the NetBird install you mean to migrate." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
if [[ $state -eq 2 ]]; then
|
||||
echo " ⚠ Could not read $src to confirm it holds data — continuing." > /dev/stderr
|
||||
echo " The backup step still fails loudly if it turns out to be empty." > /dev/stderr
|
||||
fi
|
||||
|
||||
echo " Data directory: $src"
|
||||
}
|
||||
|
||||
# Only for the Postgres volume, which has no container to read it off yet.
|
||||
resolve_compose_volume() {
|
||||
local short="$1"
|
||||
local actual
|
||||
# Resolve project-prefixed volume name from Docker Compose config first.
|
||||
@@ -391,18 +474,21 @@ resolve_data_volume() {
|
||||
backup_sqlite() {
|
||||
BACKUP_DIR="$(pwd)/backups/sqlite-pre-enterprise-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
local data_volume_actual
|
||||
data_volume_actual=$(resolve_data_volume "$DATA_VOLUME")
|
||||
echo "Backing up SQLite store from volume '$data_volume_actual' to $BACKUP_DIR ..."
|
||||
docker run --rm \
|
||||
-v "${data_volume_actual}:/var/lib/netbird:ro" \
|
||||
-v "${BACKUP_DIR}:/backup" \
|
||||
busybox \
|
||||
sh -c 'cp -a /var/lib/netbird/. /backup/ 2>/dev/null || true'
|
||||
|
||||
local container
|
||||
container=$(combined_container_id)
|
||||
if [[ -z "$container" ]]; then
|
||||
echo " ⚠ No container found for '$COMBINED_SERVICE' — cannot back up the store." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Backing up the NetBird store to $BACKUP_DIR ..."
|
||||
docker cp "${container}:/var/lib/netbird/." "$BACKUP_DIR/"
|
||||
|
||||
local copied
|
||||
copied=$(find "$BACKUP_DIR" -mindepth 1 | head -1)
|
||||
if [[ -z "$copied" ]]; then
|
||||
echo " ⚠ Backup directory is empty — the volume '$data_volume_actual' didn't contain data. Aborting." > /dev/stderr
|
||||
echo " ⚠ Backup directory is empty — /var/lib/netbird held no data. Aborting." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
echo " done"
|
||||
@@ -414,6 +500,135 @@ run_migrate_store() {
|
||||
echo " done"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rollback — a failed run must not leave the operator with a stopped stack and
|
||||
# half-written artifacts.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Resolve the name Compose would give the Postgres volume before the override
|
||||
# exists, so a leftover volume can be spotted up front.
|
||||
compose_project_name() {
|
||||
local container project
|
||||
container=$($DOCKER_COMPOSE_COMMAND ps -aq 2>/dev/null | head -1)
|
||||
if [[ -n "$container" ]]; then
|
||||
project=$(docker inspect "$container" \
|
||||
--format '{{index .Config.Labels "com.docker.compose.project"}}' 2>/dev/null)
|
||||
if [[ -n "$project" ]]; then
|
||||
echo "$project"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
project=$($DOCKER_COMPOSE_COMMAND config 2>/dev/null | yq eval '.name // ""' - 2>/dev/null)
|
||||
if [[ -n "$project" ]] && [[ "$project" != "null" ]]; then
|
||||
echo "$project"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
postgres_volume_name() {
|
||||
local project
|
||||
project=$(compose_project_name)
|
||||
if [[ -n "$project" ]]; then
|
||||
echo "${project}_netbird_postgres"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Postgres skips initdb when its data directory is non-empty, so a volume left
|
||||
# behind by an interrupted run would keep the old password and old contents,
|
||||
# and migrate-store would fail against it.
|
||||
check_stale_postgres_volume() {
|
||||
[[ "$MIGRATE_POSTGRES" == "yes" ]] || return 0
|
||||
|
||||
PG_VOLUME_NAME=$(postgres_volume_name)
|
||||
if [[ -z "$PG_VOLUME_NAME" ]]; then
|
||||
echo ""
|
||||
echo " ⚠ Could not determine the Compose project name, so a Postgres volume"
|
||||
echo " left over from an earlier attempt cannot be checked for. If a"
|
||||
echo " previous run failed, remove it before continuing:"
|
||||
echo " docker volume ls | grep netbird_postgres"
|
||||
return 0
|
||||
fi
|
||||
docker volume inspect "$PG_VOLUME_NAME" &> /dev/null || return 0
|
||||
|
||||
echo ""
|
||||
echo " ⚠ A Postgres volume from an earlier attempt already exists:"
|
||||
echo " $PG_VOLUME_NAME"
|
||||
echo " Postgres does not re-initialise a non-empty data directory, so the"
|
||||
echo " migration would run against stale credentials and stale data."
|
||||
local remove
|
||||
remove=$(read_yes_no " Remove it and continue?" "y")
|
||||
if [[ "$remove" != "yes" ]]; then
|
||||
echo "" > /dev/stderr
|
||||
echo "Aborted. Remove it manually with: docker volume rm $PG_VOLUME_NAME" > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
docker volume rm "$PG_VOLUME_NAME" > /dev/null
|
||||
echo " Removed."
|
||||
}
|
||||
|
||||
# Undo whatever this run changed and start the previous deployment again.
|
||||
rollback() {
|
||||
ROLLBACK_STATE="done"
|
||||
|
||||
echo ""
|
||||
echo "──────────────────────────────────────────────────────────────────────"
|
||||
echo " Migration failed — restoring the previous deployment"
|
||||
echo "──────────────────────────────────────────────────────────────────────"
|
||||
|
||||
# Resolve while the override is still present; without it Compose no longer
|
||||
# knows about the Postgres volume.
|
||||
local pg_volume="$PG_VOLUME_NAME"
|
||||
if [[ -z "$pg_volume" ]] && [[ "$MIGRATE_POSTGRES" == "yes" ]]; then
|
||||
pg_volume=$(postgres_volume_name)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Stopping services ..."
|
||||
$DOCKER_COMPOSE_COMMAND down || true
|
||||
|
||||
echo "Removing generated files ..."
|
||||
rm -f "$OVERRIDE_FILE" "$ENTERPRISE_CONFIG_FILE"
|
||||
|
||||
# Restore .env to exactly what it was, or remove it if this run created it.
|
||||
if [[ "$ENV_EXISTED" == "yes" ]] && [[ -f "$ENV_BACKUP" ]]; then
|
||||
mv -f "$ENV_BACKUP" .env || echo " ⚠ Could not restore .env from $ENV_BACKUP." > /dev/stderr
|
||||
elif [[ "$ENV_EXISTED" == "no" ]]; then
|
||||
rm -f .env || true
|
||||
fi
|
||||
|
||||
# Only ever the volume this run created — never the NetBird data volume.
|
||||
if [[ -n "$pg_volume" ]] && [[ "$pg_volume" != "null" ]]; then
|
||||
echo "Removing Postgres volume $pg_volume ..."
|
||||
docker volume rm "$pg_volume" &> /dev/null || true
|
||||
fi
|
||||
|
||||
echo "Starting the previous deployment ..."
|
||||
if ! $DOCKER_COMPOSE_COMMAND up -d; then
|
||||
echo ""
|
||||
echo " ⚠ Could not start the previous deployment automatically." > /dev/stderr
|
||||
echo " Run: $DOCKER_COMPOSE_COMMAND up -d" > /dev/stderr
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Rolled back. Your docker-compose.yml, config.yaml and the NetBird data"
|
||||
echo "volume were never modified."
|
||||
if [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then
|
||||
echo "The SQLite backup taken during this run is kept at:"
|
||||
echo " $BACKUP_DIR"
|
||||
fi
|
||||
echo "──────────────────────────────────────────────────────────────────────"
|
||||
}
|
||||
|
||||
on_exit() {
|
||||
local code=$?
|
||||
trap - EXIT
|
||||
if [[ $code -ne 0 ]] && [[ "$ROLLBACK_STATE" == "armed" ]]; then
|
||||
rollback
|
||||
fi
|
||||
exit $code
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -541,9 +756,15 @@ init_migration() {
|
||||
ENABLE_FLOW="no"
|
||||
echo "Step 3 (traffic flow) skipped — requires Postgres."
|
||||
fi
|
||||
|
||||
check_data_directory
|
||||
check_stale_postgres_volume
|
||||
}
|
||||
|
||||
apply_changes() {
|
||||
# From here on a failure must roll the deployment back.
|
||||
ROLLBACK_STATE="armed"
|
||||
|
||||
echo ""
|
||||
echo "Writing $OVERRIDE_FILE ..."
|
||||
install -m 644 /dev/null "$OVERRIDE_FILE"
|
||||
@@ -564,6 +785,14 @@ apply_changes() {
|
||||
# picks it up automatically.
|
||||
echo "Writing .env additions (mode 600) ..."
|
||||
local ENV_FILE=".env"
|
||||
# Snapshot the operator's .env so a rollback can restore it byte for byte.
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
ENV_EXISTED="yes"
|
||||
ENV_BACKUP="${ENV_FILE}.pre-enterprise-$(date +%Y%m%d-%H%M%S)"
|
||||
cp -p "$ENV_FILE" "$ENV_BACKUP"
|
||||
else
|
||||
ENV_EXISTED="no"
|
||||
fi
|
||||
touch "$ENV_FILE"
|
||||
chmod 600 "$ENV_FILE"
|
||||
{
|
||||
@@ -592,11 +821,16 @@ apply_changes() {
|
||||
|
||||
if [[ "$MIGRATE_POSTGRES" == "yes" ]]; then
|
||||
echo ""
|
||||
echo "Stopping existing services (volumes preserved) ..."
|
||||
$DOCKER_COMPOSE_COMMAND down
|
||||
# Stop, but keep the containers: the backup reads the store out of one.
|
||||
echo "Stopping services so the store is quiescent ..."
|
||||
$DOCKER_COMPOSE_COMMAND stop
|
||||
|
||||
backup_sqlite
|
||||
|
||||
echo ""
|
||||
echo "Removing stopped containers (volumes preserved) ..."
|
||||
$DOCKER_COMPOSE_COMMAND down
|
||||
|
||||
echo ""
|
||||
echo "Starting Postgres ..."
|
||||
$DOCKER_COMPOSE_COMMAND up -d postgres
|
||||
@@ -626,6 +860,9 @@ apply_changes() {
|
||||
|
||||
echo ""
|
||||
echo "Migration complete."
|
||||
|
||||
# Nothing left to undo.
|
||||
ROLLBACK_STATE="disarmed"
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
@@ -643,6 +880,7 @@ print_summary() {
|
||||
echo " $OVERRIDE_FILE"
|
||||
[[ "$MIGRATE_POSTGRES" == "yes" ]] && echo " $ENTERPRISE_CONFIG_FILE"
|
||||
echo " .env (license key + secrets, mode 600)"
|
||||
[[ "$ENV_EXISTED" == "yes" ]] && [[ -f "$ENV_BACKUP" ]] && echo " $ENV_BACKUP (.env as it was before this run)"
|
||||
[[ "$MIGRATE_POSTGRES" == "yes" ]] && echo " backups/sqlite-pre-enterprise-*/ (SQLite backup)"
|
||||
echo ""
|
||||
echo " Tail logs:"
|
||||
@@ -651,19 +889,27 @@ print_summary() {
|
||||
echo "──────────────────────────────────────────────────────────────────────"
|
||||
echo " To revert"
|
||||
echo "──────────────────────────────────────────────────────────────────────"
|
||||
echo " $DOCKER_COMPOSE_COMMAND down"
|
||||
if [[ "$MIGRATE_POSTGRES" == "yes" ]]; then
|
||||
# Resolve project-prefixed volume names now (before override is removed).
|
||||
local pg_volume data_volume_actual
|
||||
pg_volume=$(resolve_data_volume "netbird_postgres")
|
||||
data_volume_actual=$(resolve_data_volume "$DATA_VOLUME")
|
||||
echo " # Remove the Postgres volume FIRST, before deleting the override file:"
|
||||
echo " docker volume rm $pg_volume"
|
||||
# Resolve the project-prefixed volume name now, before the override is gone.
|
||||
local pg_volume
|
||||
pg_volume=$(resolve_compose_volume "netbird_postgres")
|
||||
echo " # Stop, but keep the containers so the store can be copied back in:"
|
||||
echo " $DOCKER_COMPOSE_COMMAND stop"
|
||||
echo " # Restore SQLite from the backup created during this run:"
|
||||
echo " docker run --rm -v ${data_volume_actual}:/var/lib/netbird -v ${BACKUP_DIR}:/backup busybox sh -c 'cp -a /backup/. /var/lib/netbird/'"
|
||||
echo " docker cp ${BACKUP_DIR}/. \$($DOCKER_COMPOSE_COMMAND ps -aq $COMBINED_SERVICE):/var/lib/netbird/"
|
||||
echo " $DOCKER_COMPOSE_COMMAND down"
|
||||
echo " docker volume rm $pg_volume"
|
||||
else
|
||||
echo " $DOCKER_COMPOSE_COMMAND down"
|
||||
fi
|
||||
echo " rm -f $OVERRIDE_FILE $ENTERPRISE_CONFIG_FILE"
|
||||
echo " # Remove migrate-to-enterprise.sh additions from .env (search for the timestamp marker)"
|
||||
if [[ "$ENV_EXISTED" == "yes" ]] && [[ -f "$ENV_BACKUP" ]]; then
|
||||
echo " mv $ENV_BACKUP .env # restores .env as it was before this run"
|
||||
elif [[ "$ENV_EXISTED" == "no" ]]; then
|
||||
echo " rm -f .env # created by this run"
|
||||
else
|
||||
echo " # Remove migrate-to-enterprise.sh additions from .env (search for the timestamp marker)"
|
||||
fi
|
||||
echo " $DOCKER_COMPOSE_COMMAND up -d"
|
||||
echo "──────────────────────────────────────────────────────────────────────"
|
||||
}
|
||||
@@ -672,6 +918,10 @@ print_summary() {
|
||||
# Run
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
trap on_exit EXIT
|
||||
# Turn signals into a normal exit so the EXIT trap can roll back.
|
||||
trap 'exit 130' INT TERM
|
||||
|
||||
init_migration
|
||||
apply_changes
|
||||
print_summary
|
||||
|
||||
@@ -82,11 +82,6 @@ type Manager interface {
|
||||
RecordAccountBudgetUsage(ctx context.Context, accountID, userID string, groupIDs []string, tokensIn, tokensOut int64, costUSD float64) error
|
||||
RecordUsage(ctx context.Context, in RecordUsageInput) error
|
||||
SelectPolicyForRequest(ctx context.Context, in PolicySelectionInput) (*PolicySelectionResult, error)
|
||||
|
||||
// GetSetupForPeer returns the Agent Network connection info the peer's
|
||||
// groups authorize (endpoint, providers, effective models). Caller-scoped
|
||||
// by design — no user permission gate; see the implementation.
|
||||
GetSetupForPeer(ctx context.Context, accountID, peerID string) (*types.EffectiveSetup, error)
|
||||
}
|
||||
|
||||
// PolicySelectionInput is the per-request selection envelope. The
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
package agentnetwork
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork/catalog"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
)
|
||||
|
||||
// GetSetupForPeer returns the Agent Network setup the calling peer's
|
||||
// groups authorize. It deliberately performs no user-permission check:
|
||||
// peer RPCs carry no user identity — WireGuard key possession is the
|
||||
// credential (same trust model as SelectPolicyForRequest) — and the
|
||||
// result is caller-scoped, which is strictly tighter than any role gate.
|
||||
// Setup-key/machine peers (no user attached) are fully supported since
|
||||
// the computation runs on peer groups, which is what the proxy enforces.
|
||||
func (m *managerImpl) GetSetupForPeer(ctx context.Context, accountID, peerID string) (*types.EffectiveSetup, error) {
|
||||
groupIDs, err := m.store.GetPeerGroupIDs(ctx, store.LockingStrengthNone, accountID, peerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get peer groups: %w", err)
|
||||
}
|
||||
return m.effectiveSetupForGroups(ctx, accountID, groupIDs)
|
||||
}
|
||||
|
||||
// effectiveSetupForGroups computes the effective Agent Network setup for
|
||||
// a set of caller groups: the account endpoint plus, per authorized
|
||||
// provider, the effective model set. It mirrors what the proxy enforces
|
||||
// at request time — the policy filter matches filterApplicablePolicies,
|
||||
// the model logic matches policyPermitsModel, and orphan providers
|
||||
// (enabled but referenced by no applicable policy) are omitted just like
|
||||
// the router synthesizer omits them — so the answer never advertises
|
||||
// anything the proxy would refuse.
|
||||
//
|
||||
// Every "nothing available" shape returns Configured=false rather than
|
||||
// an error, and "account not set up" is indistinguishable from "caller
|
||||
// has no access" by design: the response must not leak what exists for
|
||||
// others.
|
||||
func (m *managerImpl) effectiveSetupForGroups(ctx context.Context, accountID string, groupIDs []string) (*types.EffectiveSetup, error) {
|
||||
notConfigured := &types.EffectiveSetup{Providers: []types.EffectiveProvider{}}
|
||||
|
||||
settings, err := m.store.GetAgentNetworkSettings(ctx, store.LockingStrengthNone, accountID)
|
||||
switch {
|
||||
case err == nil:
|
||||
case isNotFound(err):
|
||||
return notConfigured, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("get agent network settings: %w", err)
|
||||
}
|
||||
if settings.Endpoint() == "" {
|
||||
return notConfigured, nil
|
||||
}
|
||||
|
||||
policies, err := m.store.GetAccountAgentNetworkPolicies(ctx, store.LockingStrengthNone, accountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list account policies: %w", err)
|
||||
}
|
||||
applicable := filterPoliciesByGroups(policies, groupIDs)
|
||||
if len(applicable) == 0 {
|
||||
return notConfigured, nil
|
||||
}
|
||||
|
||||
providers, err := m.store.GetAccountAgentNetworkProviders(ctx, store.LockingStrengthNone, accountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list account providers: %w", err)
|
||||
}
|
||||
|
||||
var guardrailsByID map[string]*types.Guardrail
|
||||
if anyPolicyHasGuardrails(applicable) {
|
||||
guardrailsByID, err = m.loadGuardrailsByID(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
authorized := make([]*types.Provider, 0, len(providers))
|
||||
for _, p := range providers {
|
||||
if p == nil || !p.Enabled {
|
||||
continue
|
||||
}
|
||||
if len(policiesForProvider(applicable, p.ID)) == 0 {
|
||||
continue
|
||||
}
|
||||
authorized = append(authorized, p)
|
||||
}
|
||||
if len(authorized) == 0 {
|
||||
return notConfigured, nil
|
||||
}
|
||||
// created_at order, ID tiebreak — same deterministic order the router
|
||||
// synthesizer presents.
|
||||
sort.SliceStable(authorized, func(i, j int) bool {
|
||||
if !authorized[i].CreatedAt.Equal(authorized[j].CreatedAt) {
|
||||
return authorized[i].CreatedAt.Before(authorized[j].CreatedAt)
|
||||
}
|
||||
return authorized[i].ID < authorized[j].ID
|
||||
})
|
||||
|
||||
out := &types.EffectiveSetup{
|
||||
Configured: true,
|
||||
Endpoint: "https://" + settings.Endpoint(),
|
||||
Providers: make([]types.EffectiveProvider, 0, len(authorized)),
|
||||
}
|
||||
for _, p := range authorized {
|
||||
allAllowed, models := effectiveModelsForProvider(p, policiesForProvider(applicable, p.ID), guardrailsByID)
|
||||
flavor := ""
|
||||
if entry, ok := catalog.Lookup(p.ProviderID); ok {
|
||||
flavor = entry.ParserID
|
||||
}
|
||||
out.Providers = append(out.Providers, types.EffectiveProvider{
|
||||
Name: p.Name,
|
||||
CatalogID: p.ProviderID,
|
||||
APIFlavor: flavor,
|
||||
AllModelsAllowed: allAllowed,
|
||||
Models: models,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// filterPoliciesByGroups returns the enabled policies whose SourceGroups
|
||||
// intersect the caller's groups. Same group matching as
|
||||
// filterApplicablePolicies, without the per-provider filter — the setup
|
||||
// answer spans every provider the caller can reach.
|
||||
func filterPoliciesByGroups(policies []*types.Policy, groupIDs []string) []*types.Policy {
|
||||
groupSet := make(map[string]struct{}, len(groupIDs))
|
||||
for _, g := range groupIDs {
|
||||
if g != "" {
|
||||
groupSet[g] = struct{}{}
|
||||
}
|
||||
}
|
||||
out := make([]*types.Policy, 0, len(policies))
|
||||
for _, p := range policies {
|
||||
if p == nil || !p.Enabled {
|
||||
continue
|
||||
}
|
||||
if !anyGroupMatches(p.SourceGroups, groupSet) {
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// policiesForProvider returns the subset of policies targeting the
|
||||
// provider, order preserved.
|
||||
func policiesForProvider(policies []*types.Policy, providerID string) []*types.Policy {
|
||||
out := make([]*types.Policy, 0, len(policies))
|
||||
for _, p := range policies {
|
||||
if sliceContains(p.DestinationProviderIDs, providerID) {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// effectiveModelsForProvider derives the caller's effective model set for
|
||||
// one provider from the applicable policies that target it, mirroring
|
||||
// policyPermitsModel: a policy with no allowlist-enabled guardrail is
|
||||
// unrestricted, and one unrestricted policy makes the whole provider
|
||||
// unrestricted (the proxy would admit any model through it). Otherwise
|
||||
// the union of the policies' allowlists applies, intersected with the
|
||||
// provider's declared models when the operator declared any — the router
|
||||
// only claims declared models, so an allowlisted-but-undeclared model is
|
||||
// unreachable and must not be advertised. With no declared models the
|
||||
// router claims every model, so the allowlist union stands alone.
|
||||
func effectiveModelsForProvider(provider *types.Provider, policies []*types.Policy, guardrailsByID map[string]*types.Guardrail) (bool, []string) {
|
||||
restricted := true
|
||||
union := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for _, p := range policies {
|
||||
policyRestricted := false
|
||||
for _, gID := range p.GuardrailIDs {
|
||||
g, ok := guardrailsByID[gID]
|
||||
if !ok || g == nil || !g.Checks.ModelAllowlist.Enabled {
|
||||
continue
|
||||
}
|
||||
policyRestricted = true
|
||||
for _, model := range g.Checks.ModelAllowlist.Models {
|
||||
key := normaliseModelID(model)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, dup := seen[key]; dup {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
union = append(union, key)
|
||||
}
|
||||
}
|
||||
if !policyRestricted {
|
||||
restricted = false
|
||||
}
|
||||
}
|
||||
|
||||
declared := declaredModelIDs(provider)
|
||||
if !restricted {
|
||||
return true, declared
|
||||
}
|
||||
if len(provider.Models) == 0 {
|
||||
// No operator declaration: the router claims every model, so the
|
||||
// allowlist union is the effective set as-is.
|
||||
return false, union
|
||||
}
|
||||
out := make([]string, 0, len(declared))
|
||||
for _, id := range declared {
|
||||
if _, ok := seen[normaliseModelID(id)]; ok {
|
||||
out = append(out, id)
|
||||
}
|
||||
}
|
||||
return false, out
|
||||
}
|
||||
|
||||
// declaredModelIDs returns the models a provider exposes: the operator's
|
||||
// curated list when present, otherwise the catalog entry's models (an
|
||||
// empty operator list means "all catalog models"). Gateway/custom catalog
|
||||
// entries declare no models, so the result may be empty.
|
||||
func declaredModelIDs(provider *types.Provider) []string {
|
||||
if ids := providerModelIDs(provider); len(ids) > 0 {
|
||||
return ids
|
||||
}
|
||||
entry, ok := catalog.Lookup(provider.ProviderID)
|
||||
if !ok {
|
||||
return []string{}
|
||||
}
|
||||
out := make([]string, 0, len(entry.Models))
|
||||
for _, m := range entry.Models {
|
||||
if m.ID != "" {
|
||||
out = append(out, m.ID)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetSetupForPeer on the mock manager reports "not configured" so tests
|
||||
// that don't care about setup still compile.
|
||||
func (*mockManager) GetSetupForPeer(_ context.Context, _, _ string) (*types.EffectiveSetup, error) {
|
||||
return &types.EffectiveSetup{Providers: []types.EffectiveProvider{}}, nil
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
package agentnetwork
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
)
|
||||
|
||||
// These tests drive the effective-setup computation through the real
|
||||
// sqlite store, mirroring the policyselect realstore suite: assert on
|
||||
// observable answers (configured / providers / models), not on which
|
||||
// store methods get called. The computation must agree with what the
|
||||
// proxy enforces — policy filtering matches filterApplicablePolicies,
|
||||
// model logic matches policyPermitsModel, and orphan providers are
|
||||
// omitted like the router synthesizer omits them.
|
||||
|
||||
func newSetupTestMgr(t *testing.T) (*managerImpl, store.Store) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
s, cleanup, err := store.NewTestStoreFromSQL(ctx, "", t.TempDir())
|
||||
require.NoError(t, err, "real sqlite test store must come up")
|
||||
t.Cleanup(cleanup)
|
||||
return &managerImpl{store: s}, s
|
||||
}
|
||||
|
||||
// newSetupTestGuardrail returns an allowlist-enabled guardrail.
|
||||
func newSetupTestGuardrail(id string, models ...string) *types.Guardrail {
|
||||
return &types.Guardrail{
|
||||
ID: id,
|
||||
AccountID: testAccountID,
|
||||
Name: "allowlist " + id,
|
||||
Checks: types.GuardrailChecks{
|
||||
ModelAllowlist: types.GuardrailModelAllowlist{Enabled: true, Models: models},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveSetup_RealStore_NoSettingsRow(t *testing.T) {
|
||||
mgr, _ := newSetupTestMgr(t)
|
||||
|
||||
setup, err := mgr.effectiveSetupForGroups(context.Background(), testAccountID, []string{"grp-eng"})
|
||||
require.NoError(t, err)
|
||||
assert.False(t, setup.Configured, "account without settings must read as not configured")
|
||||
assert.Empty(t, setup.Endpoint)
|
||||
assert.Empty(t, setup.Providers)
|
||||
}
|
||||
|
||||
func TestEffectiveSetup_RealStore_NoApplicablePolicy(t *testing.T) {
|
||||
mgr, s := newSetupTestMgr(t)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings()))
|
||||
provider := newSynthTestProvider()
|
||||
require.NoError(t, s.SaveAgentNetworkProvider(ctx, provider))
|
||||
require.NoError(t, s.SaveAgentNetworkPolicy(ctx, newSynthTestPolicy(provider.ID, "grp-eng", "")))
|
||||
|
||||
setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-other"})
|
||||
require.NoError(t, err)
|
||||
assert.False(t, setup.Configured, "caller outside every policy's source groups must read as not configured")
|
||||
assert.Empty(t, setup.Endpoint, "no-access answer must not leak the endpoint")
|
||||
assert.Empty(t, setup.Providers)
|
||||
}
|
||||
|
||||
func TestEffectiveSetup_RealStore_UnrestrictedPolicyListsDeclaredModels(t *testing.T) {
|
||||
mgr, s := newSetupTestMgr(t)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings()))
|
||||
provider := newSynthTestProvider()
|
||||
require.NoError(t, s.SaveAgentNetworkProvider(ctx, provider))
|
||||
require.NoError(t, s.SaveAgentNetworkPolicy(ctx, newSynthTestPolicy(provider.ID, "grp-eng", "")))
|
||||
|
||||
setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, setup.Configured)
|
||||
assert.Equal(t, "https://"+testEndpoint, setup.Endpoint)
|
||||
require.Len(t, setup.Providers, 1)
|
||||
p := setup.Providers[0]
|
||||
assert.Equal(t, "OpenAI", p.Name)
|
||||
assert.Equal(t, "openai_api", p.CatalogID)
|
||||
assert.Equal(t, "openai", p.APIFlavor)
|
||||
assert.True(t, p.AllModelsAllowed, "policy without allowlist guardrail is unrestricted")
|
||||
assert.Equal(t, []string{"gpt-5.4"}, p.Models, "declared models listed as a courtesy")
|
||||
}
|
||||
|
||||
func TestEffectiveSetup_RealStore_AllowlistIntersectsDeclaredModels(t *testing.T) {
|
||||
mgr, s := newSetupTestMgr(t)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings()))
|
||||
provider := newSynthTestProvider()
|
||||
provider.Models = []types.ProviderModel{{ID: "gpt-5.4"}, {ID: "gpt-4o"}}
|
||||
require.NoError(t, s.SaveAgentNetworkProvider(ctx, provider))
|
||||
// Allowlist admits gpt-5.4 (declared, odd casing/spacing) and gpt-4.1
|
||||
// (NOT declared — the router would never route it, so it must not be
|
||||
// advertised).
|
||||
require.NoError(t, s.SaveAgentNetworkGuardrail(ctx, newSetupTestGuardrail("guard-1", " GPT-5.4 ", "gpt-4.1")))
|
||||
require.NoError(t, s.SaveAgentNetworkPolicy(ctx, newSynthTestPolicy(provider.ID, "grp-eng", "guard-1")))
|
||||
|
||||
setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, setup.Providers, 1)
|
||||
p := setup.Providers[0]
|
||||
assert.False(t, p.AllModelsAllowed)
|
||||
assert.Equal(t, []string{"gpt-5.4"}, p.Models, "allowlist ∩ declared, in declared order and casing")
|
||||
}
|
||||
|
||||
func TestEffectiveSetup_RealStore_UnrestrictedPolicyWinsOverRestricted(t *testing.T) {
|
||||
mgr, s := newSetupTestMgr(t)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings()))
|
||||
provider := newSynthTestProvider()
|
||||
require.NoError(t, s.SaveAgentNetworkProvider(ctx, provider))
|
||||
require.NoError(t, s.SaveAgentNetworkGuardrail(ctx, newSetupTestGuardrail("guard-1", "gpt-5.4")))
|
||||
restricted := newSynthTestPolicy(provider.ID, "grp-eng", "guard-1")
|
||||
require.NoError(t, s.SaveAgentNetworkPolicy(ctx, restricted))
|
||||
open := newSynthTestPolicy(provider.ID, "grp-eng", "")
|
||||
open.ID = "pol-2"
|
||||
require.NoError(t, s.SaveAgentNetworkPolicy(ctx, open))
|
||||
|
||||
setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, setup.Providers, 1)
|
||||
assert.True(t, setup.Providers[0].AllModelsAllowed,
|
||||
"one applicable policy without an allowlist makes the provider unrestricted — the proxy would admit any model through it")
|
||||
}
|
||||
|
||||
func TestEffectiveSetup_RealStore_AllowlistUnionAcrossPolicies(t *testing.T) {
|
||||
mgr, s := newSetupTestMgr(t)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings()))
|
||||
provider := newSynthTestProvider()
|
||||
provider.Models = []types.ProviderModel{{ID: "gpt-5.4"}, {ID: "gpt-4o"}, {ID: "o4-mini"}}
|
||||
require.NoError(t, s.SaveAgentNetworkProvider(ctx, provider))
|
||||
require.NoError(t, s.SaveAgentNetworkGuardrail(ctx, newSetupTestGuardrail("guard-1", "gpt-5.4")))
|
||||
require.NoError(t, s.SaveAgentNetworkGuardrail(ctx, newSetupTestGuardrail("guard-2", "gpt-4o")))
|
||||
p1 := newSynthTestPolicy(provider.ID, "grp-eng", "guard-1")
|
||||
require.NoError(t, s.SaveAgentNetworkPolicy(ctx, p1))
|
||||
p2 := newSynthTestPolicy(provider.ID, "grp-eng", "guard-2")
|
||||
p2.ID = "pol-2"
|
||||
require.NoError(t, s.SaveAgentNetworkPolicy(ctx, p2))
|
||||
|
||||
setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, setup.Providers, 1)
|
||||
p := setup.Providers[0]
|
||||
assert.False(t, p.AllModelsAllowed)
|
||||
assert.ElementsMatch(t, []string{"gpt-5.4", "gpt-4o"}, p.Models, "union of allowlists across applicable policies")
|
||||
}
|
||||
|
||||
func TestEffectiveSetup_RealStore_OrphanAndDisabledProvidersOmitted(t *testing.T) {
|
||||
mgr, s := newSetupTestMgr(t)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings()))
|
||||
// Orphan: enabled but referenced by no policy.
|
||||
orphan := newSynthTestProvider()
|
||||
orphan.ID = "prov-orphan"
|
||||
require.NoError(t, s.SaveAgentNetworkProvider(ctx, orphan))
|
||||
// Disabled but referenced by an applicable policy.
|
||||
disabled := newSynthTestProvider()
|
||||
disabled.ID = "prov-disabled"
|
||||
disabled.Enabled = false
|
||||
require.NoError(t, s.SaveAgentNetworkProvider(ctx, disabled))
|
||||
require.NoError(t, s.SaveAgentNetworkPolicy(ctx, newSynthTestPolicy(disabled.ID, "grp-eng", "")))
|
||||
|
||||
setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"})
|
||||
require.NoError(t, err)
|
||||
assert.False(t, setup.Configured, "neither an orphan nor a disabled provider is reachable, so nothing is configured for the caller")
|
||||
assert.Empty(t, setup.Providers)
|
||||
}
|
||||
|
||||
func TestEffectiveSetup_RealStore_DisabledPolicyIgnored(t *testing.T) {
|
||||
mgr, s := newSetupTestMgr(t)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings()))
|
||||
provider := newSynthTestProvider()
|
||||
require.NoError(t, s.SaveAgentNetworkProvider(ctx, provider))
|
||||
policy := newSynthTestPolicy(provider.ID, "grp-eng", "")
|
||||
policy.Enabled = false
|
||||
require.NoError(t, s.SaveAgentNetworkPolicy(ctx, policy))
|
||||
|
||||
setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"})
|
||||
require.NoError(t, err)
|
||||
assert.False(t, setup.Configured)
|
||||
}
|
||||
|
||||
func TestEffectiveSetup_RealStore_UndeclaredModelsUseAllowlistAsIs(t *testing.T) {
|
||||
mgr, s := newSetupTestMgr(t)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings()))
|
||||
// Gateway-style provider: no declared models — the router claims every
|
||||
// model, so the allowlist union is the effective set on its own.
|
||||
provider := newSynthTestProvider()
|
||||
provider.ProviderID = "litellm_proxy"
|
||||
provider.Name = "LiteLLM"
|
||||
provider.Models = nil
|
||||
require.NoError(t, s.SaveAgentNetworkProvider(ctx, provider))
|
||||
require.NoError(t, s.SaveAgentNetworkGuardrail(ctx, newSetupTestGuardrail("guard-1", "claude-sonnet-4-5")))
|
||||
require.NoError(t, s.SaveAgentNetworkPolicy(ctx, newSynthTestPolicy(provider.ID, "grp-eng", "guard-1")))
|
||||
|
||||
setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, setup.Providers, 1)
|
||||
p := setup.Providers[0]
|
||||
assert.False(t, p.AllModelsAllowed)
|
||||
assert.Equal(t, []string{"claude-sonnet-4-5"}, p.Models)
|
||||
}
|
||||
|
||||
func TestEffectiveSetup_RealStore_ProvidersInCreatedAtOrder(t *testing.T) {
|
||||
mgr, s := newSetupTestMgr(t)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings()))
|
||||
newer := newSynthTestProvider()
|
||||
newer.ID = "prov-newer"
|
||||
newer.Name = "Newer"
|
||||
newer.CreatedAt = time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)
|
||||
require.NoError(t, s.SaveAgentNetworkProvider(ctx, newer))
|
||||
older := newSynthTestProvider()
|
||||
older.ID = "prov-older"
|
||||
older.Name = "Older"
|
||||
older.CreatedAt = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
require.NoError(t, s.SaveAgentNetworkProvider(ctx, older))
|
||||
|
||||
policy := newSynthTestPolicy(newer.ID, "grp-eng", "")
|
||||
policy.DestinationProviderIDs = []string{newer.ID, older.ID}
|
||||
require.NoError(t, s.SaveAgentNetworkPolicy(ctx, policy))
|
||||
|
||||
setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, setup.Providers, 2)
|
||||
assert.Equal(t, "Older", setup.Providers[0].Name)
|
||||
assert.Equal(t, "Newer", setup.Providers[1].Name)
|
||||
}
|
||||
|
||||
// TestGetSetupForPeer_RealStore pins the peer entry point: the peer's
|
||||
// group memberships (not any user identity) scope the answer, which is
|
||||
// exactly what the proxy enforces via AllowedGroupIDs — including for
|
||||
// setup-key/machine peers that have no user attached.
|
||||
func TestGetSetupForPeer_RealStore(t *testing.T) {
|
||||
mgr, s := newSetupTestMgr(t)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings()))
|
||||
provider := newSynthTestProvider()
|
||||
require.NoError(t, s.SaveAgentNetworkProvider(ctx, provider))
|
||||
require.NoError(t, s.SaveAgentNetworkPolicy(ctx, newSynthTestPolicy(provider.ID, "grp-eng", "")))
|
||||
|
||||
require.NoError(t, s.AddPeerToGroup(ctx, testAccountID, "peer-in", "grp-eng"))
|
||||
require.NoError(t, s.AddPeerToGroup(ctx, testAccountID, "peer-out", "grp-other"))
|
||||
|
||||
setupIn, err := mgr.GetSetupForPeer(ctx, testAccountID, "peer-in")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, setupIn.Configured)
|
||||
require.Len(t, setupIn.Providers, 1)
|
||||
|
||||
setupOut, err := mgr.GetSetupForPeer(ctx, testAccountID, "peer-out")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, setupOut.Configured, "peer outside the policy's source groups gets the not-configured answer")
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package types
|
||||
|
||||
// EffectiveSetup is the caller-scoped answer to "what may this caller
|
||||
// use on the Agent Network?" — the account's proxy endpoint plus the
|
||||
// providers and models the caller's groups authorize. It intentionally
|
||||
// carries display metadata only: no keys, no upstream URLs, no policy or
|
||||
// guardrail structure, and no hint of providers the caller cannot reach.
|
||||
type EffectiveSetup struct {
|
||||
// Configured is false when the account has no Agent Network set up or
|
||||
// when nothing is authorized for the caller's groups — the two cases
|
||||
// are deliberately indistinguishable so the response leaks nothing
|
||||
// about what exists for others.
|
||||
Configured bool
|
||||
// Endpoint is the account's proxy base URL
|
||||
// ("https://<subdomain>.<cluster>"), reachable over the NetBird tunnel
|
||||
// only. Empty when Configured is false.
|
||||
Endpoint string
|
||||
// Providers lists the providers at least one applicable policy
|
||||
// authorizes for the caller, in the account's created_at order.
|
||||
Providers []EffectiveProvider
|
||||
}
|
||||
|
||||
// EffectiveProvider is one authorized provider in an EffectiveSetup.
|
||||
type EffectiveProvider struct {
|
||||
// Name is the operator-assigned label, e.g. "Bedrock prod".
|
||||
Name string
|
||||
// CatalogID names the catalog entry, e.g. "anthropic_api".
|
||||
CatalogID string
|
||||
// APIFlavor is the request-body shape the provider speaks — the
|
||||
// catalog entry's parser id ("anthropic", "openai"); empty when the
|
||||
// proxy dispatches the provider by URL path instead.
|
||||
APIFlavor string
|
||||
// AllModelsAllowed is true when no model allowlist restricts this
|
||||
// provider for the caller. Models then lists the declared/catalog
|
||||
// models as a courtesy (possibly none for gateway-style providers).
|
||||
AllModelsAllowed bool
|
||||
// Models is the effective model allowlist for the caller, or the
|
||||
// declared/catalog models when AllModelsAllowed is true.
|
||||
Models []string
|
||||
}
|
||||
@@ -211,12 +211,10 @@ func (s *BaseServer) GRPCServer() *grpc.Server {
|
||||
}
|
||||
serviceMgr := s.ServiceManager()
|
||||
srv.SetReverseProxyManager(serviceMgr)
|
||||
srv.SetAgentNetworkSetupService(s.AgentNetworkManager())
|
||||
if serviceMgr != nil {
|
||||
serviceMgr.StartExposeReaper(context.Background())
|
||||
}
|
||||
mgmtProto.RegisterManagementServiceServer(gRPCAPIHandler, srv)
|
||||
log.Info("ManagementService registered on gRPC server (agent-network setup RPC available)")
|
||||
|
||||
mgmtProto.RegisterProxyServiceServer(gRPCAPIHandler, s.ReverseProxyGRPCServer())
|
||||
log.Info("ProxyService registered on gRPC server")
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
antypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
// AgentNetworkSetupService is the minimal slice of agentnetwork.Manager
|
||||
// the peer-facing setup RPC needs. Narrow on purpose so the gRPC server
|
||||
// never sees the manager's operator-facing surface.
|
||||
type AgentNetworkSetupService interface {
|
||||
GetSetupForPeer(ctx context.Context, accountID, peerID string) (*antypes.EffectiveSetup, error)
|
||||
}
|
||||
|
||||
// GetAgentNetworkSetup handles a peer request for its Agent Network
|
||||
// connection info. The WireGuard key is the credential (same trust model
|
||||
// as the Expose RPCs); the response is scoped to what the calling peer's
|
||||
// own groups authorize and carries display metadata only.
|
||||
func (s *Server) GetAgentNetworkSetup(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
|
||||
setupReq := &proto.AgentNetworkSetupRequest{}
|
||||
peerKey, err := s.parseRequest(ctx, req, setupReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accountID, peer, err := s.authenticateExposePeer(ctx, peerKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setupSvc := s.getAgentNetworkSetupService()
|
||||
if setupSvc == nil {
|
||||
return nil, status.Errorf(codes.Internal, "agent network manager not available")
|
||||
}
|
||||
|
||||
setup, err := setupSvc.GetSetupForPeer(ctx, accountID, peer.ID)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("get agent network setup for peer %s: %v", peer.ID, err)
|
||||
return nil, status.Errorf(codes.Internal, "internal error")
|
||||
}
|
||||
|
||||
return s.encryptResponse(peerKey, toProtoAgentNetworkSetup(setup))
|
||||
}
|
||||
|
||||
func toProtoAgentNetworkSetup(setup *antypes.EffectiveSetup) *proto.AgentNetworkSetupResponse {
|
||||
resp := &proto.AgentNetworkSetupResponse{
|
||||
Configured: setup.Configured,
|
||||
Endpoint: setup.Endpoint,
|
||||
Providers: make([]*proto.AgentNetworkProviderInfo, 0, len(setup.Providers)),
|
||||
}
|
||||
for _, p := range setup.Providers {
|
||||
resp.Providers = append(resp.Providers, &proto.AgentNetworkProviderInfo{
|
||||
Name: p.Name,
|
||||
CatalogId: p.CatalogID,
|
||||
ApiFlavor: p.APIFlavor,
|
||||
AllModelsAllowed: p.AllModelsAllowed,
|
||||
Models: p.Models,
|
||||
})
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *Server) getAgentNetworkSetupService() AgentNetworkSetupService {
|
||||
s.agentNetworkSetupMu.RLock()
|
||||
defer s.agentNetworkSetupMu.RUnlock()
|
||||
return s.agentNetworkSetup
|
||||
}
|
||||
|
||||
// SetAgentNetworkSetupService wires the agent-network setup service on
|
||||
// the server.
|
||||
func (s *Server) SetAgentNetworkSetupService(svc AgentNetworkSetupService) {
|
||||
s.agentNetworkSetupMu.Lock()
|
||||
defer s.agentNetworkSetupMu.Unlock()
|
||||
s.agentNetworkSetup = svc
|
||||
}
|
||||
@@ -87,9 +87,6 @@ type Server struct {
|
||||
|
||||
reverseProxyManager rpservice.Manager
|
||||
reverseProxyMu sync.RWMutex
|
||||
|
||||
agentNetworkSetup AgentNetworkSetupService
|
||||
agentNetworkSetupMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewServer creates a new Management server
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
@@ -744,6 +746,14 @@ func validateDeleteGroup(ctx context.Context, transaction store.Store, group *ty
|
||||
return &GroupLinkError{"network router", linkedRouter.ID}
|
||||
}
|
||||
|
||||
if isLinked, linkedService := isGroupLinkedToReverseProxyService(ctx, transaction, group.AccountID, group.ID); isLinked {
|
||||
return &GroupLinkError{"reverse proxy service", linkedService.Domain}
|
||||
}
|
||||
|
||||
if isLinked, linkedPolicy := isGroupLinkedToAgentNetworkPolicy(ctx, transaction, group.AccountID, group.ID); isLinked {
|
||||
return &GroupLinkError{"agent network policy", linkedPolicy.Name}
|
||||
}
|
||||
|
||||
return checkGroupLinkedToSettings(ctx, transaction, group)
|
||||
}
|
||||
|
||||
@@ -875,6 +885,46 @@ func isGroupLinkedToNetworkRouter(ctx context.Context, transaction store.Store,
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// isGroupLinkedToReverseProxyService checks if a group is used as an access group
|
||||
// of a private reverse proxy service or as a bearer-auth distribution group.
|
||||
func isGroupLinkedToReverseProxyService(ctx context.Context, transaction store.Store, accountID string, groupID string) (bool, *service.Service) {
|
||||
services, err := transaction.GetAccountServices(ctx, store.LockingStrengthNone, accountID)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("error retrieving reverse proxy services while checking group linkage: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, svc := range services {
|
||||
if svc.Private && slices.Contains(svc.AccessGroups, groupID) {
|
||||
return true, svc
|
||||
}
|
||||
if svc.Auth.BearerAuth != nil && svc.Auth.BearerAuth.Enabled && slices.Contains(svc.Auth.BearerAuth.DistributionGroups, groupID) {
|
||||
return true, svc
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// isGroupLinkedToAgentNetworkPolicy checks if a group is used as a source group by any
|
||||
// agent network policy in the account.
|
||||
func isGroupLinkedToAgentNetworkPolicy(ctx context.Context, transaction store.Store, accountID string, groupID string) (bool, *agentNetworkTypes.Policy) {
|
||||
policies, err := transaction.GetAccountAgentNetworkPolicies(ctx, store.LockingStrengthNone, accountID)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("error retrieving agent network policies while checking group linkage: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
if policy == nil {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(policy.SourceGroups, groupID) {
|
||||
return true, policy
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// areGroupChangesAffectPeers checks if any changes to the specified groups will affect peers.
|
||||
// It fetches each collection once and checks all groupIDs against them in memory.
|
||||
func areGroupChangesAffectPeers(ctx context.Context, transaction store.Store, accountID string, groupIDs []string) (bool, error) {
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
|
||||
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/server/groups"
|
||||
"github.com/netbirdio/netbird/management/server/networks"
|
||||
"github.com/netbirdio/netbird/management/server/networks/resources"
|
||||
@@ -125,6 +127,21 @@ func TestDefaultAccountManager_DeleteGroup(t *testing.T) {
|
||||
"grp-for-integration",
|
||||
"only service users with admin power can delete integration group",
|
||||
},
|
||||
{
|
||||
"agent network policy",
|
||||
"grp-for-agent-network-policy",
|
||||
"agent network policy",
|
||||
},
|
||||
{
|
||||
"reverse proxy private service access group",
|
||||
"grp-for-rp-private",
|
||||
"reverse proxy service",
|
||||
},
|
||||
{
|
||||
"reverse proxy bearer distribution group",
|
||||
"grp-for-rp-bearer",
|
||||
"reverse proxy service",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
@@ -218,6 +235,17 @@ func TestDefaultAccountManager_DeleteGroups(t *testing.T) {
|
||||
groupIDs: []string{"grp-for-integration"},
|
||||
expectedReasons: []string{"only service users with admin power can delete integration group"},
|
||||
},
|
||||
{
|
||||
name: "agent network policy",
|
||||
groupIDs: []string{"grp-for-agent-network-policy"},
|
||||
expectedReasons: []string{"agent network policy"},
|
||||
},
|
||||
{
|
||||
name: "reverse proxy services",
|
||||
groupIDs: []string{"grp-for-rp-private", "grp-for-rp-bearer"},
|
||||
expectedReasons: []string{"reverse proxy service", "reverse proxy service"},
|
||||
expectedNotDeleted: []string{"grp-for-rp-private", "grp-for-rp-bearer"},
|
||||
},
|
||||
{
|
||||
name: "successfully delete multiple groups",
|
||||
groupIDs: []string{"group-1", "group-2"},
|
||||
@@ -285,6 +313,65 @@ func TestDefaultAccountManager_DeleteGroups(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAccountManager_DeleteGroupUnlinkedFromReverseProxyService(t *testing.T) {
|
||||
am, _, err := createManager(t)
|
||||
require.NoError(t, err, "Failed to create account manager")
|
||||
|
||||
_, account, err := initTestGroupAccount(am)
|
||||
require.NoError(t, err, "Failed to init testing account")
|
||||
|
||||
deletableGroups := []*types.Group{
|
||||
{
|
||||
ID: "grp-rp-bearer-disabled",
|
||||
AccountID: account.Id,
|
||||
Name: "Group only in a disabled bearer auth",
|
||||
Issued: types.GroupIssuedAPI,
|
||||
Peers: make([]string, 0),
|
||||
},
|
||||
{
|
||||
ID: "grp-rp-nonprivate-access",
|
||||
AccountID: account.Id,
|
||||
Name: "Group only in a non-private service's access groups",
|
||||
Issued: types.GroupIssuedAPI,
|
||||
Peers: make([]string, 0),
|
||||
},
|
||||
}
|
||||
for _, group := range deletableGroups {
|
||||
require.NoError(t, am.CreateGroup(context.Background(), account.Id, groupAdminUserID, group))
|
||||
}
|
||||
|
||||
// Disabled bearer auth and stale access groups on a non-private service
|
||||
// are inert configuration and must not block group deletion.
|
||||
services := []*rpservice.Service{
|
||||
{
|
||||
ID: "rp-svc-bearer-disabled",
|
||||
AccountID: account.Id,
|
||||
Domain: "bearer-disabled.services.example.com",
|
||||
Auth: rpservice.AuthConfig{
|
||||
BearerAuth: &rpservice.BearerAuthConfig{
|
||||
Enabled: false,
|
||||
DistributionGroups: []string{"grp-rp-bearer-disabled"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "rp-svc-nonprivate-access",
|
||||
AccountID: account.Id,
|
||||
Domain: "nonprivate.services.example.com",
|
||||
Private: false,
|
||||
AccessGroups: []string{"grp-rp-nonprivate-access"},
|
||||
},
|
||||
}
|
||||
for _, svc := range services {
|
||||
require.NoError(t, am.Store.CreateService(context.Background(), svc))
|
||||
}
|
||||
|
||||
for _, group := range deletableGroups {
|
||||
err = am.DeleteGroup(context.Background(), account.Id, groupAdminUserID, group.ID)
|
||||
assert.NoError(t, err, "group %s is not referenced by an active reverse proxy gate and should be deletable", group.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAccountManager_DeleteGroupLinkedToFlowGroup(t *testing.T) {
|
||||
am, _, err := createManager(t)
|
||||
require.NoError(t, err)
|
||||
@@ -406,6 +493,30 @@ func initTestGroupAccount(am *DefaultAccountManager) (*DefaultAccountManager, *t
|
||||
Peers: make([]string, 0),
|
||||
}
|
||||
|
||||
groupForAgentNetworkPolicy := &types.Group{
|
||||
ID: "grp-for-agent-network-policy",
|
||||
AccountID: "account-id",
|
||||
Name: "Group for agent network policies",
|
||||
Issued: types.GroupIssuedAPI,
|
||||
Peers: make([]string, 0),
|
||||
}
|
||||
|
||||
groupForRPPrivate := &types.Group{
|
||||
ID: "grp-for-rp-private",
|
||||
AccountID: "account-id",
|
||||
Name: "Group for private reverse proxy service",
|
||||
Issued: types.GroupIssuedAPI,
|
||||
Peers: make([]string, 0),
|
||||
}
|
||||
|
||||
groupForRPBearer := &types.Group{
|
||||
ID: "grp-for-rp-bearer",
|
||||
AccountID: "account-id",
|
||||
Name: "Group for bearer reverse proxy service",
|
||||
Issued: types.GroupIssuedAPI,
|
||||
Peers: make([]string, 0),
|
||||
}
|
||||
|
||||
routeResource := &route.Route{
|
||||
ID: "example route",
|
||||
Groups: []string{groupForRoute.ID},
|
||||
@@ -461,6 +572,66 @@ func initTestGroupAccount(am *DefaultAccountManager) (*DefaultAccountManager, *t
|
||||
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForSetupKeys)
|
||||
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForUsers)
|
||||
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForIntegration)
|
||||
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForAgentNetworkPolicy)
|
||||
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForRPPrivate)
|
||||
_ = am.CreateGroup(context.Background(), accountID, groupAdminUserID, groupForRPBearer)
|
||||
|
||||
agentNetworkPolicy := &agentNetworkTypes.Policy{
|
||||
ID: "example agent network policy",
|
||||
AccountID: accountID,
|
||||
Name: "Example agent network policy",
|
||||
Enabled: true,
|
||||
SourceGroups: []string{groupForAgentNetworkPolicy.ID},
|
||||
}
|
||||
if err := am.Store.SaveAgentNetworkPolicy(context.Background(), agentNetworkPolicy); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// The decoy services are created first so the linkage check has to scan
|
||||
// past services that do not reference the groups under test.
|
||||
rpServices := []*rpservice.Service{
|
||||
{
|
||||
ID: "rp-svc-private-decoy",
|
||||
AccountID: accountID,
|
||||
Domain: "private-decoy.services.example.com",
|
||||
Private: true,
|
||||
AccessGroups: []string{"unrelated-group"},
|
||||
},
|
||||
{
|
||||
ID: "rp-svc-bearer-decoy",
|
||||
AccountID: accountID,
|
||||
Domain: "bearer-decoy.services.example.com",
|
||||
Auth: rpservice.AuthConfig{
|
||||
BearerAuth: &rpservice.BearerAuthConfig{
|
||||
Enabled: true,
|
||||
DistributionGroups: []string{"unrelated-group"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "rp-svc-private",
|
||||
AccountID: accountID,
|
||||
Domain: "private.services.example.com",
|
||||
Private: true,
|
||||
AccessGroups: []string{groupForRPPrivate.ID},
|
||||
},
|
||||
{
|
||||
ID: "rp-svc-bearer",
|
||||
AccountID: accountID,
|
||||
Domain: "bearer.services.example.com",
|
||||
Auth: rpservice.AuthConfig{
|
||||
BearerAuth: &rpservice.BearerAuthConfig{
|
||||
Enabled: true,
|
||||
DistributionGroups: []string{groupForRPBearer.ID},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, svc := range rpServices {
|
||||
if err := am.Store.CreateService(context.Background(), svc); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
acc, err := am.Store.GetAccount(context.Background(), account.Id)
|
||||
if err != nil {
|
||||
|
||||
@@ -1707,14 +1707,34 @@ func (a *Account) injectPrivateServicePolicies(svc *service.Service, proxyPeers
|
||||
if len(proxyPeers) == 0 {
|
||||
return
|
||||
}
|
||||
// A service's AccessGroups can name groups that no longer exist — persisted
|
||||
// services and the agent-network synthesiser both carry the ids verbatim from
|
||||
// their own state. An unresolvable source authorises nothing, so drop it here
|
||||
// rather than let the network-map assembly resolve it to a nil group.
|
||||
sources := a.existingGroupIDs(svc.AccessGroups)
|
||||
if len(sources) == 0 {
|
||||
return
|
||||
}
|
||||
for _, proxyPeer := range proxyPeers {
|
||||
a.Policies = append(a.Policies, a.createPrivateServicePolicy(svc, proxyPeer))
|
||||
a.Policies = append(a.Policies, a.createPrivateServicePolicy(svc, proxyPeer, sources))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Account) createPrivateServicePolicy(svc *service.Service, proxyPeer *nbpeer.Peer) *Policy {
|
||||
// existingGroupIDs returns the subset of groupIDs that resolve to a group in the account,
|
||||
// preserving the input order.
|
||||
func (a *Account) existingGroupIDs(groupIDs []string) []string {
|
||||
out := make([]string, 0, len(groupIDs))
|
||||
for _, groupID := range groupIDs {
|
||||
if _, ok := a.Groups[groupID]; ok {
|
||||
out = append(out, groupID)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *Account) createPrivateServicePolicy(svc *service.Service, proxyPeer *nbpeer.Peer, accessGroups []string) *Policy {
|
||||
policyID := fmt.Sprintf("private-access-%s-%s", svc.ID, proxyPeer.ID)
|
||||
sources := append([]string(nil), svc.AccessGroups...)
|
||||
sources := append([]string(nil), accessGroups...)
|
||||
return &Policy{
|
||||
ID: policyID,
|
||||
Name: fmt.Sprintf("Private Access to %s", svc.Name),
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/netbirdio/netbird/encryption"
|
||||
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
// agentNetworkSetupServer is a minimal ManagementService that answers
|
||||
// GetServerKey and GetAgentNetworkSetup with the real NaCl-box envelope,
|
||||
// so the test pins the full wire path: gRPC method routing, request
|
||||
// encryption, and response decryption.
|
||||
type agentNetworkSetupServer struct {
|
||||
mgmtProto.UnimplementedManagementServiceServer
|
||||
key wgtypes.Key
|
||||
}
|
||||
|
||||
func (s *agentNetworkSetupServer) GetServerKey(_ context.Context, _ *mgmtProto.Empty) (*mgmtProto.ServerKeyResponse, error) {
|
||||
return &mgmtProto.ServerKeyResponse{Key: s.key.PublicKey().String()}, nil
|
||||
}
|
||||
|
||||
func (s *agentNetworkSetupServer) GetAgentNetworkSetup(_ context.Context, msg *mgmtProto.EncryptedMessage) (*mgmtProto.EncryptedMessage, error) {
|
||||
peerKey, err := wgtypes.ParseKey(msg.WgPubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := &mgmtProto.AgentNetworkSetupRequest{}
|
||||
if err := encryption.DecryptMessage(peerKey, s.key, msg.Body, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &mgmtProto.AgentNetworkSetupResponse{
|
||||
Configured: true,
|
||||
Endpoint: "https://violet.eu.proxy.example.com",
|
||||
Providers: []*mgmtProto.AgentNetworkProviderInfo{
|
||||
{
|
||||
Name: "Anthropic prod",
|
||||
CatalogId: "anthropic_api",
|
||||
ApiFlavor: "anthropic",
|
||||
Models: []string{"claude-sonnet-4-5"},
|
||||
},
|
||||
},
|
||||
}
|
||||
body, err := encryption.EncryptMessage(peerKey, s.key, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mgmtProto.EncryptedMessage{WgPubKey: s.key.PublicKey().String(), Body: body}, nil
|
||||
}
|
||||
|
||||
// TestGetAgentNetworkSetup_RoundTrip proves the RPC is routable on any
|
||||
// server built from this tree and that the encrypt→invoke→decrypt path
|
||||
// the CLI uses round-trips. A server answering this call with
|
||||
// codes.Unimplemented is by definition running a binary compiled without
|
||||
// the regenerated management proto.
|
||||
func TestGetAgentNetworkSetup_RoundTrip(t *testing.T) {
|
||||
serverKey, err := wgtypes.GeneratePrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
srv := grpc.NewServer()
|
||||
mgmtProto.RegisterManagementServiceServer(srv, &agentNetworkSetupServer{key: serverKey})
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
t.Cleanup(srv.Stop)
|
||||
|
||||
clientKey, err := wgtypes.GeneratePrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := NewClient(context.Background(), lis.Addr().String(), clientKey, false)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = client.Close() })
|
||||
|
||||
setup, err := client.GetAgentNetworkSetup(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, setup.Configured)
|
||||
assert.Equal(t, "https://violet.eu.proxy.example.com", setup.Endpoint)
|
||||
require.Len(t, setup.Providers, 1)
|
||||
assert.Equal(t, "anthropic_api", setup.Providers[0].CatalogId)
|
||||
assert.Equal(t, []string{"claude-sonnet-4-5"}, setup.Providers[0].Models)
|
||||
}
|
||||
@@ -34,8 +34,4 @@ type Client interface {
|
||||
CreateExpose(ctx context.Context, req ExposeRequest) (*ExposeResponse, error)
|
||||
RenewExpose(ctx context.Context, domain string) error
|
||||
StopExpose(ctx context.Context, domain string) error
|
||||
// GetAgentNetworkSetup returns the Agent Network connection info the
|
||||
// calling peer's groups authorize: proxy endpoint plus effective
|
||||
// providers and models.
|
||||
GetAgentNetworkSetup(ctx context.Context) (*proto.AgentNetworkSetupResponse, error)
|
||||
}
|
||||
|
||||
@@ -914,38 +914,6 @@ func (c *GrpcClient) StopExpose(ctx context.Context, domain string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAgentNetworkSetup asks the management server for the Agent Network
|
||||
// connection info the calling peer's groups authorize.
|
||||
func (c *GrpcClient) GetAgentNetworkSetup(ctx context.Context) (*proto.AgentNetworkSetupResponse, error) {
|
||||
serverPubKey, err := c.getServerPublicKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encReq, err := encryption.EncryptMessage(*serverPubKey, c.key, &proto.AgentNetworkSetupRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encrypt agent network setup request: %w", err)
|
||||
}
|
||||
|
||||
mgmCtx, cancel := context.WithTimeout(ctx, ConnectTimeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.realClient.GetAgentNetworkSetup(mgmCtx, &proto.EncryptedMessage{
|
||||
WgPubKey: c.key.PublicKey().String(),
|
||||
Body: encReq,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setupResp := &proto.AgentNetworkSetupResponse{}
|
||||
if err := encryption.DecryptMessage(*serverPubKey, c.key, resp.Body, setupResp); err != nil {
|
||||
return nil, fmt.Errorf("decrypt agent network setup response: %w", err)
|
||||
}
|
||||
|
||||
return setupResp, nil
|
||||
}
|
||||
|
||||
func fromProtoExposeResponse(resp *proto.ExposeServiceResponse) *ExposeResponse {
|
||||
return &ExposeResponse{
|
||||
ServiceName: resp.ServiceName,
|
||||
|
||||
@@ -25,7 +25,6 @@ type MockClient struct {
|
||||
CreateExposeFunc func(ctx context.Context, req ExposeRequest) (*ExposeResponse, error)
|
||||
RenewExposeFunc func(ctx context.Context, domain string) error
|
||||
StopExposeFunc func(ctx context.Context, domain string) error
|
||||
GetAgentNetworkSetupFunc func(ctx context.Context) (*proto.AgentNetworkSetupResponse, error)
|
||||
}
|
||||
|
||||
func (m *MockClient) IsHealthy() bool {
|
||||
@@ -137,10 +136,3 @@ func (m *MockClient) StopExpose(ctx context.Context, domain string) error {
|
||||
}
|
||||
return m.StopExposeFunc(ctx, domain)
|
||||
}
|
||||
|
||||
func (m *MockClient) GetAgentNetworkSetup(ctx context.Context) (*proto.AgentNetworkSetupResponse, error) {
|
||||
if m.GetAgentNetworkSetupFunc == nil {
|
||||
return &proto.AgentNetworkSetupResponse{}, nil
|
||||
}
|
||||
return m.GetAgentNetworkSetupFunc(ctx)
|
||||
}
|
||||
|
||||
@@ -6653,201 +6653,6 @@ func (x *PeerIndexSet) GetPeerIndexes() []uint32 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type AgentNetworkSetupRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
}
|
||||
|
||||
func (x *AgentNetworkSetupRequest) Reset() {
|
||||
*x = AgentNetworkSetupRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_management_proto_msgTypes[76]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *AgentNetworkSetupRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AgentNetworkSetupRequest) ProtoMessage() {}
|
||||
|
||||
func (x *AgentNetworkSetupRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_management_proto_msgTypes[76]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AgentNetworkSetupRequest.ProtoReflect.Descriptor instead.
|
||||
func (*AgentNetworkSetupRequest) Descriptor() ([]byte, []int) {
|
||||
return file_management_proto_rawDescGZIP(), []int{76}
|
||||
}
|
||||
|
||||
type AgentNetworkSetupResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// configured is false when the account has no Agent Network set up or
|
||||
// the calling peer's groups authorize none of it — the two cases are
|
||||
// deliberately indistinguishable.
|
||||
Configured bool `protobuf:"varint,1,opt,name=configured,proto3" json:"configured,omitempty"`
|
||||
// endpoint is the account's proxy base URL, e.g.
|
||||
// "https://calm-otter.proxy.example.com". Reachable over the NetBird
|
||||
// tunnel only. Empty when configured is false.
|
||||
Endpoint string `protobuf:"bytes,2,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||
Providers []*AgentNetworkProviderInfo `protobuf:"bytes,3,rep,name=providers,proto3" json:"providers,omitempty"`
|
||||
}
|
||||
|
||||
func (x *AgentNetworkSetupResponse) Reset() {
|
||||
*x = AgentNetworkSetupResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_management_proto_msgTypes[77]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *AgentNetworkSetupResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AgentNetworkSetupResponse) ProtoMessage() {}
|
||||
|
||||
func (x *AgentNetworkSetupResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_management_proto_msgTypes[77]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AgentNetworkSetupResponse.ProtoReflect.Descriptor instead.
|
||||
func (*AgentNetworkSetupResponse) Descriptor() ([]byte, []int) {
|
||||
return file_management_proto_rawDescGZIP(), []int{77}
|
||||
}
|
||||
|
||||
func (x *AgentNetworkSetupResponse) GetConfigured() bool {
|
||||
if x != nil {
|
||||
return x.Configured
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *AgentNetworkSetupResponse) GetEndpoint() string {
|
||||
if x != nil {
|
||||
return x.Endpoint
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AgentNetworkSetupResponse) GetProviders() []*AgentNetworkProviderInfo {
|
||||
if x != nil {
|
||||
return x.Providers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AgentNetworkProviderInfo struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// name is the operator-assigned provider label, e.g. "Bedrock prod".
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
// catalog_id names the catalog entry, e.g. "anthropic_api", "bedrock_api".
|
||||
CatalogId string `protobuf:"bytes,2,opt,name=catalog_id,json=catalogId,proto3" json:"catalog_id,omitempty"`
|
||||
// api_flavor is the request-body shape the provider speaks ("anthropic",
|
||||
// "openai"); empty when the proxy dispatches it by URL path instead.
|
||||
ApiFlavor string `protobuf:"bytes,3,opt,name=api_flavor,json=apiFlavor,proto3" json:"api_flavor,omitempty"`
|
||||
// all_models_allowed is true when no model allowlist restricts this
|
||||
// provider for the caller; models then lists the declared/catalog models
|
||||
// as a courtesy (possibly none for gateway-style providers).
|
||||
AllModelsAllowed bool `protobuf:"varint,4,opt,name=all_models_allowed,json=allModelsAllowed,proto3" json:"all_models_allowed,omitempty"`
|
||||
// models is the effective model allowlist for the caller (or the
|
||||
// declared/catalog models when all_models_allowed is true).
|
||||
Models []string `protobuf:"bytes,5,rep,name=models,proto3" json:"models,omitempty"`
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProviderInfo) Reset() {
|
||||
*x = AgentNetworkProviderInfo{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_management_proto_msgTypes[78]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProviderInfo) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AgentNetworkProviderInfo) ProtoMessage() {}
|
||||
|
||||
func (x *AgentNetworkProviderInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_management_proto_msgTypes[78]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AgentNetworkProviderInfo.ProtoReflect.Descriptor instead.
|
||||
func (*AgentNetworkProviderInfo) Descriptor() ([]byte, []int) {
|
||||
return file_management_proto_rawDescGZIP(), []int{78}
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProviderInfo) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProviderInfo) GetCatalogId() string {
|
||||
if x != nil {
|
||||
return x.CatalogId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProviderInfo) GetApiFlavor() string {
|
||||
if x != nil {
|
||||
return x.ApiFlavor
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProviderInfo) GetAllModelsAllowed() bool {
|
||||
if x != nil {
|
||||
return x.AllModelsAllowed
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *AgentNetworkProviderInfo) GetModels() []string {
|
||||
if x != nil {
|
||||
return x.Models
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type PortInfo_Range struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -6860,7 +6665,7 @@ type PortInfo_Range struct {
|
||||
func (x *PortInfo_Range) Reset() {
|
||||
*x = PortInfo_Range{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_management_proto_msgTypes[80]
|
||||
mi := &file_management_proto_msgTypes[77]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -6873,7 +6678,7 @@ func (x *PortInfo_Range) String() string {
|
||||
func (*PortInfo_Range) ProtoMessage() {}
|
||||
|
||||
func (x *PortInfo_Range) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_management_proto_msgTypes[80]
|
||||
mi := &file_management_proto_msgTypes[77]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -7939,127 +7744,99 @@ var file_management_proto_rawDesc = []byte{
|
||||
0x0c, 0x50, 0x65, 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x65, 0x74, 0x12, 0x21, 0x0a,
|
||||
0x0c, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x18, 0x01, 0x20,
|
||||
0x03, 0x28, 0x0d, 0x52, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73,
|
||||
0x22, 0x1a, 0x0a, 0x18, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
|
||||
0x53, 0x65, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x9b, 0x01, 0x0a,
|
||||
0x19, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x53, 0x65, 0x74,
|
||||
0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f,
|
||||
0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a,
|
||||
0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e,
|
||||
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e,
|
||||
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x42, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64,
|
||||
0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
|
||||
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4e, 0x65, 0x74, 0x77,
|
||||
0x6f, 0x72, 0x6b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52,
|
||||
0x09, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0xb2, 0x01, 0x0a, 0x18, 0x41,
|
||||
0x67, 0x65, 0x6e, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x50, 0x72, 0x6f, 0x76, 0x69,
|
||||
0x64, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63,
|
||||
0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x09, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x70,
|
||||
0x69, 0x5f, 0x66, 0x6c, 0x61, 0x76, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
|
||||
0x61, 0x70, 0x69, 0x46, 0x6c, 0x61, 0x76, 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x6c, 0x6c,
|
||||
0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x5f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x61, 0x6c, 0x6c, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x73,
|
||||
0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x6f, 0x64, 0x65, 0x6c,
|
||||
0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x2a,
|
||||
0x3a, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e,
|
||||
0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x10, 0x00,
|
||||
0x12, 0x0d, 0x0a, 0x09, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, 0x01, 0x12,
|
||||
0x0a, 0x0a, 0x06, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x93, 0x01, 0x0a, 0x0e,
|
||||
0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x19,
|
||||
0x0a, 0x15, 0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79,
|
||||
0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x50, 0x65, 0x65,
|
||||
0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x53, 0x6f, 0x75, 0x72, 0x63,
|
||||
0x65, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x50,
|
||||
0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x49, 0x50, 0x76,
|
||||
0x36, 0x4f, 0x76, 0x65, 0x72, 0x6c, 0x61, 0x79, 0x10, 0x02, 0x12, 0x25, 0x0a, 0x21, 0x50, 0x65,
|
||||
0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x43, 0x6f, 0x6d, 0x70,
|
||||
0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x10,
|
||||
0x03, 0x2a, 0x5d, 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, 0x12,
|
||||
0x0f, 0x0a, 0x0b, 0x4e, 0x45, 0x54, 0x42, 0x49, 0x52, 0x44, 0x5f, 0x53, 0x53, 0x48, 0x10, 0x06,
|
||||
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, 0x2a, 0x63, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65,
|
||||
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0f, 0x0a, 0x0b, 0x45, 0x58, 0x50, 0x4f,
|
||||
0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x58, 0x50,
|
||||
0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x45,
|
||||
0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x45,
|
||||
0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x0e, 0x0a, 0x0a, 0x45,
|
||||
0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x32, 0xa6, 0x08, 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,
|
||||
0x2a, 0x3a, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a,
|
||||
0x0e, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x10,
|
||||
0x00, 0x12, 0x0d, 0x0a, 0x09, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, 0x01,
|
||||
0x12, 0x0a, 0x0a, 0x06, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x93, 0x01, 0x0a,
|
||||
0x0e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12,
|
||||
0x19, 0x0a, 0x15, 0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74,
|
||||
0x79, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x50, 0x65,
|
||||
0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x53, 0x6f, 0x75, 0x72,
|
||||
0x63, 0x65, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19,
|
||||
0x50, 0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x49, 0x50,
|
||||
0x76, 0x36, 0x4f, 0x76, 0x65, 0x72, 0x6c, 0x61, 0x79, 0x10, 0x02, 0x12, 0x25, 0x0a, 0x21, 0x50,
|
||||
0x65, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x43, 0x6f, 0x6d,
|
||||
0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70,
|
||||
0x10, 0x03, 0x2a, 0x5d, 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,
|
||||
0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x45, 0x54, 0x42, 0x49, 0x52, 0x44, 0x5f, 0x53, 0x53, 0x48, 0x10,
|
||||
0x06, 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, 0x2a, 0x63, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, 0x73,
|
||||
0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0f, 0x0a, 0x0b, 0x45, 0x58, 0x50,
|
||||
0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x58,
|
||||
0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a,
|
||||
0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a,
|
||||
0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x0e, 0x0a, 0x0a,
|
||||
0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x32, 0xd0, 0x07, 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, 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,
|
||||
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, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
|
||||
0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x47, 0x0a, 0x03, 0x4a,
|
||||
0x6f, 0x62, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
|
||||
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,
|
||||
0x28, 0x01, 0x30, 0x01, 0x12, 0x51, 0x0a, 0x11, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x41, 0x75,
|
||||
0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
|
||||
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, 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, 0x4c, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74,
|
||||
0x65, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 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, 0x4b, 0x0a, 0x0b, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78,
|
||||
0x70, 0x6f, 0x73, 0x65, 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, 0x4a, 0x0a, 0x0a, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65,
|
||||
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, 0x54,
|
||||
0x0a, 0x14, 0x47, 0x65, 0x74, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
|
||||
0x6b, 0x53, 0x65, 0x74, 0x75, 0x70, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
|
||||
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, 0x47, 0x0a, 0x03,
|
||||
0x4a, 0x6f, 0x62, 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, 0x28, 0x01, 0x30, 0x01, 0x12, 0x51, 0x0a, 0x11, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x41,
|
||||
0x75, 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 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, 0x4c, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61,
|
||||
0x74, 0x65, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 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, 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, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0b, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45,
|
||||
0x78, 0x70, 0x6f, 0x73, 0x65, 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, 0x4a, 0x0a, 0x0a, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, 0x6f, 0x73,
|
||||
0x65, 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, 0x42,
|
||||
0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -8075,7 +7852,7 @@ func file_management_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 8)
|
||||
var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 86)
|
||||
var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 83)
|
||||
var file_management_proto_goTypes = []interface{}{
|
||||
(JobStatus)(0), // 0: management.JobStatus
|
||||
(PeerCapability)(0), // 1: management.PeerCapability
|
||||
@@ -8161,18 +7938,15 @@ var file_management_proto_goTypes = []interface{}{
|
||||
(*PolicyIds)(nil), // 81: management.PolicyIds
|
||||
(*UserIDList)(nil), // 82: management.UserIDList
|
||||
(*PeerIndexSet)(nil), // 83: management.PeerIndexSet
|
||||
(*AgentNetworkSetupRequest)(nil), // 84: management.AgentNetworkSetupRequest
|
||||
(*AgentNetworkSetupResponse)(nil), // 85: management.AgentNetworkSetupResponse
|
||||
(*AgentNetworkProviderInfo)(nil), // 86: management.AgentNetworkProviderInfo
|
||||
nil, // 87: management.SSHAuth.MachineUsersEntry
|
||||
(*PortInfo_Range)(nil), // 88: management.PortInfo.Range
|
||||
nil, // 89: management.NetworkMapComponentsFull.RoutersMapEntry
|
||||
nil, // 90: management.NetworkMapComponentsFull.ResourcePoliciesMapEntry
|
||||
nil, // 91: management.NetworkMapComponentsFull.GroupIdToUserIdsEntry
|
||||
nil, // 92: management.NetworkMapComponentsFull.PostureFailedPeersEntry
|
||||
nil, // 93: management.PolicyCompact.AuthorizedGroupsEntry
|
||||
(*timestamppb.Timestamp)(nil), // 94: google.protobuf.Timestamp
|
||||
(*durationpb.Duration)(nil), // 95: google.protobuf.Duration
|
||||
nil, // 84: management.SSHAuth.MachineUsersEntry
|
||||
(*PortInfo_Range)(nil), // 85: management.PortInfo.Range
|
||||
nil, // 86: management.NetworkMapComponentsFull.RoutersMapEntry
|
||||
nil, // 87: management.NetworkMapComponentsFull.ResourcePoliciesMapEntry
|
||||
nil, // 88: management.NetworkMapComponentsFull.GroupIdToUserIdsEntry
|
||||
nil, // 89: management.NetworkMapComponentsFull.PostureFailedPeersEntry
|
||||
nil, // 90: management.PolicyCompact.AuthorizedGroupsEntry
|
||||
(*timestamppb.Timestamp)(nil), // 91: google.protobuf.Timestamp
|
||||
(*durationpb.Duration)(nil), // 92: google.protobuf.Duration
|
||||
}
|
||||
var file_management_proto_depIdxs = []int32{
|
||||
11, // 0: management.JobRequest.bundle:type_name -> management.BundleParameters
|
||||
@@ -8184,7 +7958,7 @@ var file_management_proto_depIdxs = []int32{
|
||||
39, // 6: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig
|
||||
36, // 7: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap
|
||||
54, // 8: management.SyncResponse.Checks:type_name -> management.Checks
|
||||
94, // 9: management.SyncResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
|
||||
91, // 9: management.SyncResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
|
||||
64, // 10: management.SyncResponse.NetworkMapEnvelope:type_name -> management.NetworkMapEnvelope
|
||||
21, // 11: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta
|
||||
21, // 12: management.LoginRequest.meta:type_name -> management.PeerSystemMeta
|
||||
@@ -8197,10 +7971,10 @@ var file_management_proto_depIdxs = []int32{
|
||||
27, // 19: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig
|
||||
34, // 20: management.LoginResponse.peerConfig:type_name -> management.PeerConfig
|
||||
54, // 21: management.LoginResponse.Checks:type_name -> management.Checks
|
||||
94, // 22: management.LoginResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
|
||||
91, // 22: management.LoginResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
|
||||
21, // 23: management.ExtendAuthSessionRequest.meta:type_name -> management.PeerSystemMeta
|
||||
94, // 24: management.ExtendAuthSessionResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
|
||||
94, // 25: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp
|
||||
91, // 24: management.ExtendAuthSessionResponse.sessionExpiresAt:type_name -> google.protobuf.Timestamp
|
||||
91, // 25: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp
|
||||
28, // 26: management.NetbirdConfig.stuns:type_name -> management.HostConfig
|
||||
33, // 27: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig
|
||||
28, // 28: management.NetbirdConfig.signal:type_name -> management.HostConfig
|
||||
@@ -8208,7 +7982,7 @@ var file_management_proto_depIdxs = []int32{
|
||||
30, // 30: management.NetbirdConfig.flow:type_name -> management.FlowConfig
|
||||
31, // 31: management.NetbirdConfig.metrics:type_name -> management.MetricsConfig
|
||||
6, // 32: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol
|
||||
95, // 33: management.FlowConfig.interval:type_name -> google.protobuf.Duration
|
||||
92, // 33: management.FlowConfig.interval:type_name -> google.protobuf.Duration
|
||||
28, // 34: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig
|
||||
40, // 35: management.PeerConfig.sshConfig:type_name -> management.SSHConfig
|
||||
35, // 36: management.PeerConfig.autoUpdate:type_name -> management.AutoUpdateSettings
|
||||
@@ -8221,7 +7995,7 @@ var file_management_proto_depIdxs = []int32{
|
||||
56, // 43: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule
|
||||
57, // 44: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule
|
||||
37, // 45: management.NetworkMap.sshAuth:type_name -> management.SSHAuth
|
||||
87, // 46: management.SSHAuth.machine_users:type_name -> management.SSHAuth.MachineUsersEntry
|
||||
84, // 46: management.SSHAuth.machine_users:type_name -> management.SSHAuth.MachineUsersEntry
|
||||
40, // 47: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig
|
||||
32, // 48: management.SSHConfig.jwtConfig:type_name -> management.JWTConfig
|
||||
7, // 49: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider
|
||||
@@ -8235,7 +8009,7 @@ var file_management_proto_depIdxs = []int32{
|
||||
4, // 57: management.FirewallRule.Action:type_name -> management.RuleAction
|
||||
2, // 58: management.FirewallRule.Protocol:type_name -> management.RuleProtocol
|
||||
55, // 59: management.FirewallRule.PortInfo:type_name -> management.PortInfo
|
||||
88, // 60: management.PortInfo.range:type_name -> management.PortInfo.Range
|
||||
85, // 60: management.PortInfo.range:type_name -> management.PortInfo.Range
|
||||
4, // 61: management.RouteFirewallRule.action:type_name -> management.RuleAction
|
||||
2, // 62: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol
|
||||
55, // 63: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo
|
||||
@@ -8257,10 +8031,10 @@ var file_management_proto_depIdxs = []int32{
|
||||
49, // 79: management.NetworkMapComponentsFull.all_dns_records:type_name -> management.SimpleRecord
|
||||
48, // 80: management.NetworkMapComponentsFull.account_zones:type_name -> management.CustomZone
|
||||
78, // 81: management.NetworkMapComponentsFull.network_resources:type_name -> management.NetworkResourceRaw
|
||||
89, // 82: management.NetworkMapComponentsFull.routers_map:type_name -> management.NetworkMapComponentsFull.RoutersMapEntry
|
||||
90, // 83: management.NetworkMapComponentsFull.resource_policies_map:type_name -> management.NetworkMapComponentsFull.ResourcePoliciesMapEntry
|
||||
91, // 84: management.NetworkMapComponentsFull.group_id_to_user_ids:type_name -> management.NetworkMapComponentsFull.GroupIdToUserIdsEntry
|
||||
92, // 85: management.NetworkMapComponentsFull.posture_failed_peers:type_name -> management.NetworkMapComponentsFull.PostureFailedPeersEntry
|
||||
86, // 82: management.NetworkMapComponentsFull.routers_map:type_name -> management.NetworkMapComponentsFull.RoutersMapEntry
|
||||
87, // 83: management.NetworkMapComponentsFull.resource_policies_map:type_name -> management.NetworkMapComponentsFull.ResourcePoliciesMapEntry
|
||||
88, // 84: management.NetworkMapComponentsFull.group_id_to_user_ids:type_name -> management.NetworkMapComponentsFull.GroupIdToUserIdsEntry
|
||||
89, // 85: management.NetworkMapComponentsFull.posture_failed_peers:type_name -> management.NetworkMapComponentsFull.PostureFailedPeersEntry
|
||||
66, // 86: management.NetworkMapComponentsFull.proxy_patch:type_name -> management.ProxyPatch
|
||||
39, // 87: management.ProxyPatch.peers:type_name -> management.RemotePeerConfig
|
||||
39, // 88: management.ProxyPatch.offline_peers:type_name -> management.RemotePeerConfig
|
||||
@@ -8270,52 +8044,49 @@ var file_management_proto_depIdxs = []int32{
|
||||
57, // 92: management.ProxyPatch.forwarding_rules:type_name -> management.ForwardingRule
|
||||
4, // 93: management.PolicyCompact.action:type_name -> management.RuleAction
|
||||
2, // 94: management.PolicyCompact.protocol:type_name -> management.RuleProtocol
|
||||
88, // 95: management.PolicyCompact.port_ranges:type_name -> management.PortInfo.Range
|
||||
93, // 96: management.PolicyCompact.authorized_groups:type_name -> management.PolicyCompact.AuthorizedGroupsEntry
|
||||
85, // 95: management.PolicyCompact.port_ranges:type_name -> management.PortInfo.Range
|
||||
90, // 96: management.PolicyCompact.authorized_groups:type_name -> management.PolicyCompact.AuthorizedGroupsEntry
|
||||
72, // 97: management.PolicyCompact.source_resource:type_name -> management.ResourceCompact
|
||||
72, // 98: management.PolicyCompact.destination_resource:type_name -> management.ResourceCompact
|
||||
51, // 99: management.NameServerGroupRaw.nameservers:type_name -> management.NameServer
|
||||
80, // 100: management.NetworkRouterList.entries:type_name -> management.NetworkRouterEntry
|
||||
86, // 101: management.AgentNetworkSetupResponse.providers:type_name -> management.AgentNetworkProviderInfo
|
||||
38, // 102: management.SSHAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes
|
||||
79, // 103: management.NetworkMapComponentsFull.RoutersMapEntry.value:type_name -> management.NetworkRouterList
|
||||
81, // 104: management.NetworkMapComponentsFull.ResourcePoliciesMapEntry.value:type_name -> management.PolicyIds
|
||||
82, // 105: management.NetworkMapComponentsFull.GroupIdToUserIdsEntry.value:type_name -> management.UserIDList
|
||||
83, // 106: management.NetworkMapComponentsFull.PostureFailedPeersEntry.value:type_name -> management.PeerIndexSet
|
||||
73, // 107: management.PolicyCompact.AuthorizedGroupsEntry.value:type_name -> management.UserNameList
|
||||
8, // 108: management.ManagementService.Login:input_type -> management.EncryptedMessage
|
||||
8, // 109: management.ManagementService.Sync:input_type -> management.EncryptedMessage
|
||||
26, // 110: management.ManagementService.GetServerKey:input_type -> management.Empty
|
||||
26, // 111: management.ManagementService.isHealthy:input_type -> management.Empty
|
||||
8, // 112: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage
|
||||
8, // 113: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage
|
||||
8, // 114: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage
|
||||
8, // 115: management.ManagementService.Logout:input_type -> management.EncryptedMessage
|
||||
8, // 116: management.ManagementService.Job:input_type -> management.EncryptedMessage
|
||||
8, // 117: management.ManagementService.ExtendAuthSession:input_type -> management.EncryptedMessage
|
||||
8, // 118: management.ManagementService.CreateExpose:input_type -> management.EncryptedMessage
|
||||
8, // 119: management.ManagementService.RenewExpose:input_type -> management.EncryptedMessage
|
||||
8, // 120: management.ManagementService.StopExpose:input_type -> management.EncryptedMessage
|
||||
8, // 121: management.ManagementService.GetAgentNetworkSetup:input_type -> management.EncryptedMessage
|
||||
8, // 122: management.ManagementService.Login:output_type -> management.EncryptedMessage
|
||||
8, // 123: management.ManagementService.Sync:output_type -> management.EncryptedMessage
|
||||
25, // 124: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse
|
||||
26, // 125: management.ManagementService.isHealthy:output_type -> management.Empty
|
||||
8, // 126: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage
|
||||
8, // 127: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage
|
||||
26, // 128: management.ManagementService.SyncMeta:output_type -> management.Empty
|
||||
26, // 129: management.ManagementService.Logout:output_type -> management.Empty
|
||||
8, // 130: management.ManagementService.Job:output_type -> management.EncryptedMessage
|
||||
8, // 131: management.ManagementService.ExtendAuthSession:output_type -> management.EncryptedMessage
|
||||
8, // 132: management.ManagementService.CreateExpose:output_type -> management.EncryptedMessage
|
||||
8, // 133: management.ManagementService.RenewExpose:output_type -> management.EncryptedMessage
|
||||
8, // 134: management.ManagementService.StopExpose:output_type -> management.EncryptedMessage
|
||||
8, // 135: management.ManagementService.GetAgentNetworkSetup:output_type -> management.EncryptedMessage
|
||||
122, // [122:136] is the sub-list for method output_type
|
||||
108, // [108:122] is the sub-list for method input_type
|
||||
108, // [108:108] is the sub-list for extension type_name
|
||||
108, // [108:108] is the sub-list for extension extendee
|
||||
0, // [0:108] is the sub-list for field type_name
|
||||
38, // 101: management.SSHAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes
|
||||
79, // 102: management.NetworkMapComponentsFull.RoutersMapEntry.value:type_name -> management.NetworkRouterList
|
||||
81, // 103: management.NetworkMapComponentsFull.ResourcePoliciesMapEntry.value:type_name -> management.PolicyIds
|
||||
82, // 104: management.NetworkMapComponentsFull.GroupIdToUserIdsEntry.value:type_name -> management.UserIDList
|
||||
83, // 105: management.NetworkMapComponentsFull.PostureFailedPeersEntry.value:type_name -> management.PeerIndexSet
|
||||
73, // 106: management.PolicyCompact.AuthorizedGroupsEntry.value:type_name -> management.UserNameList
|
||||
8, // 107: management.ManagementService.Login:input_type -> management.EncryptedMessage
|
||||
8, // 108: management.ManagementService.Sync:input_type -> management.EncryptedMessage
|
||||
26, // 109: management.ManagementService.GetServerKey:input_type -> management.Empty
|
||||
26, // 110: management.ManagementService.isHealthy:input_type -> management.Empty
|
||||
8, // 111: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage
|
||||
8, // 112: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage
|
||||
8, // 113: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage
|
||||
8, // 114: management.ManagementService.Logout:input_type -> management.EncryptedMessage
|
||||
8, // 115: management.ManagementService.Job:input_type -> management.EncryptedMessage
|
||||
8, // 116: management.ManagementService.ExtendAuthSession:input_type -> management.EncryptedMessage
|
||||
8, // 117: management.ManagementService.CreateExpose:input_type -> management.EncryptedMessage
|
||||
8, // 118: management.ManagementService.RenewExpose:input_type -> management.EncryptedMessage
|
||||
8, // 119: management.ManagementService.StopExpose:input_type -> management.EncryptedMessage
|
||||
8, // 120: management.ManagementService.Login:output_type -> management.EncryptedMessage
|
||||
8, // 121: management.ManagementService.Sync:output_type -> management.EncryptedMessage
|
||||
25, // 122: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse
|
||||
26, // 123: management.ManagementService.isHealthy:output_type -> management.Empty
|
||||
8, // 124: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage
|
||||
8, // 125: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage
|
||||
26, // 126: management.ManagementService.SyncMeta:output_type -> management.Empty
|
||||
26, // 127: management.ManagementService.Logout:output_type -> management.Empty
|
||||
8, // 128: management.ManagementService.Job:output_type -> management.EncryptedMessage
|
||||
8, // 129: management.ManagementService.ExtendAuthSession:output_type -> management.EncryptedMessage
|
||||
8, // 130: management.ManagementService.CreateExpose:output_type -> management.EncryptedMessage
|
||||
8, // 131: management.ManagementService.RenewExpose:output_type -> management.EncryptedMessage
|
||||
8, // 132: management.ManagementService.StopExpose:output_type -> management.EncryptedMessage
|
||||
120, // [120:133] is the sub-list for method output_type
|
||||
107, // [107:120] is the sub-list for method input_type
|
||||
107, // [107:107] is the sub-list for extension type_name
|
||||
107, // [107:107] is the sub-list for extension extendee
|
||||
0, // [0:107] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_management_proto_init() }
|
||||
@@ -9236,43 +9007,7 @@ func file_management_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_management_proto_msgTypes[76].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*AgentNetworkSetupRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_management_proto_msgTypes[77].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*AgentNetworkSetupResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_management_proto_msgTypes[78].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*AgentNetworkProviderInfo); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_management_proto_msgTypes[80].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*PortInfo_Range); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
@@ -9305,7 +9040,7 @@ func file_management_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_management_proto_rawDesc,
|
||||
NumEnums: 8,
|
||||
NumMessages: 86,
|
||||
NumMessages: 83,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -68,13 +68,6 @@ service ManagementService {
|
||||
|
||||
// StopExpose terminates an active expose session
|
||||
rpc StopExpose(EncryptedMessage) returns (EncryptedMessage) {}
|
||||
|
||||
// GetAgentNetworkSetup returns the Agent Network connection info the
|
||||
// calling peer's groups authorize: proxy endpoint plus effective
|
||||
// providers and models. Caller-scoped by the peer's WireGuard key.
|
||||
// EncryptedMessage of the request has a body of AgentNetworkSetupRequest.
|
||||
// EncryptedMessage of the response has a body of AgentNetworkSetupResponse.
|
||||
rpc GetAgentNetworkSetup(EncryptedMessage) returns (EncryptedMessage) {}
|
||||
}
|
||||
|
||||
message EncryptedMessage {
|
||||
@@ -1214,34 +1207,3 @@ message UserIDList {
|
||||
message PeerIndexSet {
|
||||
repeated uint32 peer_indexes = 1;
|
||||
}
|
||||
|
||||
message AgentNetworkSetupRequest {}
|
||||
|
||||
message AgentNetworkSetupResponse {
|
||||
// configured is false when the account has no Agent Network set up or
|
||||
// the calling peer's groups authorize none of it — the two cases are
|
||||
// deliberately indistinguishable.
|
||||
bool configured = 1;
|
||||
// endpoint is the account's proxy base URL, e.g.
|
||||
// "https://calm-otter.proxy.example.com". Reachable over the NetBird
|
||||
// tunnel only. Empty when configured is false.
|
||||
string endpoint = 2;
|
||||
repeated AgentNetworkProviderInfo providers = 3;
|
||||
}
|
||||
|
||||
message AgentNetworkProviderInfo {
|
||||
// name is the operator-assigned provider label, e.g. "Bedrock prod".
|
||||
string name = 1;
|
||||
// catalog_id names the catalog entry, e.g. "anthropic_api", "bedrock_api".
|
||||
string catalog_id = 2;
|
||||
// api_flavor is the request-body shape the provider speaks ("anthropic",
|
||||
// "openai"); empty when the proxy dispatches it by URL path instead.
|
||||
string api_flavor = 3;
|
||||
// all_models_allowed is true when no model allowlist restricts this
|
||||
// provider for the caller; models then lists the declared/catalog models
|
||||
// as a courtesy (possibly none for gateway-style providers).
|
||||
bool all_models_allowed = 4;
|
||||
// models is the effective model allowlist for the caller (or the
|
||||
// declared/catalog models when all_models_allowed is true).
|
||||
repeated string models = 5;
|
||||
}
|
||||
|
||||
@@ -65,12 +65,6 @@ type ManagementServiceClient interface {
|
||||
RenewExpose(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error)
|
||||
// StopExpose terminates an active expose session
|
||||
StopExpose(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error)
|
||||
// GetAgentNetworkSetup returns the Agent Network connection info the
|
||||
// calling peer's groups authorize: proxy endpoint plus effective
|
||||
// providers and models. Caller-scoped by the peer's WireGuard key.
|
||||
// EncryptedMessage of the request has a body of AgentNetworkSetupRequest.
|
||||
// EncryptedMessage of the response has a body of AgentNetworkSetupResponse.
|
||||
GetAgentNetworkSetup(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error)
|
||||
}
|
||||
|
||||
type managementServiceClient struct {
|
||||
@@ -243,15 +237,6 @@ func (c *managementServiceClient) StopExpose(ctx context.Context, in *EncryptedM
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *managementServiceClient) GetAgentNetworkSetup(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) {
|
||||
out := new(EncryptedMessage)
|
||||
err := c.cc.Invoke(ctx, "/management.ManagementService/GetAgentNetworkSetup", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ManagementServiceServer is the server API for ManagementService service.
|
||||
// All implementations must embed UnimplementedManagementServiceServer
|
||||
// for forward compatibility
|
||||
@@ -303,12 +288,6 @@ type ManagementServiceServer interface {
|
||||
RenewExpose(context.Context, *EncryptedMessage) (*EncryptedMessage, error)
|
||||
// StopExpose terminates an active expose session
|
||||
StopExpose(context.Context, *EncryptedMessage) (*EncryptedMessage, error)
|
||||
// GetAgentNetworkSetup returns the Agent Network connection info the
|
||||
// calling peer's groups authorize: proxy endpoint plus effective
|
||||
// providers and models. Caller-scoped by the peer's WireGuard key.
|
||||
// EncryptedMessage of the request has a body of AgentNetworkSetupRequest.
|
||||
// EncryptedMessage of the response has a body of AgentNetworkSetupResponse.
|
||||
GetAgentNetworkSetup(context.Context, *EncryptedMessage) (*EncryptedMessage, error)
|
||||
mustEmbedUnimplementedManagementServiceServer()
|
||||
}
|
||||
|
||||
@@ -355,9 +334,6 @@ func (UnimplementedManagementServiceServer) RenewExpose(context.Context, *Encryp
|
||||
func (UnimplementedManagementServiceServer) StopExpose(context.Context, *EncryptedMessage) (*EncryptedMessage, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method StopExpose not implemented")
|
||||
}
|
||||
func (UnimplementedManagementServiceServer) GetAgentNetworkSetup(context.Context, *EncryptedMessage) (*EncryptedMessage, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetAgentNetworkSetup not implemented")
|
||||
}
|
||||
func (UnimplementedManagementServiceServer) mustEmbedUnimplementedManagementServiceServer() {}
|
||||
|
||||
// UnsafeManagementServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
@@ -616,24 +592,6 @@ func _ManagementService_StopExpose_Handler(srv interface{}, ctx context.Context,
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ManagementService_GetAgentNetworkSetup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(EncryptedMessage)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ManagementServiceServer).GetAgentNetworkSetup(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/management.ManagementService/GetAgentNetworkSetup",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ManagementServiceServer).GetAgentNetworkSetup(ctx, req.(*EncryptedMessage))
|
||||
}
|
||||
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)
|
||||
@@ -685,10 +643,6 @@ var ManagementService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "StopExpose",
|
||||
Handler: _ManagementService_StopExpose_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetAgentNetworkSetup",
|
||||
Handler: _ManagementService_GetAgentNetworkSetup_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
@@ -156,9 +156,11 @@ func (g *Guard) notifyReconnected() {
|
||||
func (g *Guard) exponentTicker(ctx context.Context) *backoff.Ticker {
|
||||
bo := backoff.WithContext(&backoff.ExponentialBackOff{
|
||||
InitialInterval: 2 * time.Second,
|
||||
Multiplier: 2,
|
||||
MaxInterval: g.maxBackoffInterval,
|
||||
Clock: backoff.SystemClock,
|
||||
// Spreads the reconnects of every client that lost the same relay server.
|
||||
RandomizationFactor: backoff.DefaultRandomizationFactor,
|
||||
Multiplier: 2,
|
||||
MaxInterval: g.maxBackoffInterval,
|
||||
Clock: backoff.SystemClock,
|
||||
}, ctx)
|
||||
|
||||
return backoff.NewTicker(bo)
|
||||
|
||||
Reference in New Issue
Block a user