mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-05 00:26:39 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fc5a8d4a1 | ||
|
|
57945fc328 | ||
|
|
ed828b7af4 | ||
|
|
11ac2af2f5 | ||
|
|
df197d5001 | ||
|
|
ad93dcf980 |
149
.github/workflows/release.yml
vendored
149
.github/workflows/release.yml
vendored
@@ -114,7 +114,7 @@ jobs:
|
|||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest-m
|
runs-on: ubuntu-24.04-8-core
|
||||||
outputs:
|
outputs:
|
||||||
release_artifact_url: ${{ steps.upload_release.outputs.artifact-url }}
|
release_artifact_url: ${{ steps.upload_release.outputs.artifact-url }}
|
||||||
linux_packages_artifact_url: ${{ steps.upload_linux_packages.outputs.artifact-url }}
|
linux_packages_artifact_url: ${{ steps.upload_linux_packages.outputs.artifact-url }}
|
||||||
@@ -455,6 +455,151 @@ jobs:
|
|||||||
path: dist/
|
path: dist/
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
|
||||||
|
test_windows_installer:
|
||||||
|
name: "Windows Installer / Build Test"
|
||||||
|
runs-on: windows-2022
|
||||||
|
needs: [release, release_ui]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: amd64
|
||||||
|
wintun_arch: amd64
|
||||||
|
- arch: arm64
|
||||||
|
wintun_arch: arm64
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: powershell
|
||||||
|
env:
|
||||||
|
PackageWorkdir: netbird_windows_${{ matrix.arch }}
|
||||||
|
downloadPath: '${{ github.workspace }}\temp'
|
||||||
|
steps:
|
||||||
|
- name: Parse semver string
|
||||||
|
id: semver_parser
|
||||||
|
uses: booxmedialtd/ws-action-parse-semver@v1
|
||||||
|
with:
|
||||||
|
input_string: ${{ (startsWith(github.ref, 'refs/tags/v') && github.ref) || 'refs/tags/v0.0.0' }}
|
||||||
|
version_extractor_regex: '\/v(.*)$'
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Add 7-Zip to PATH
|
||||||
|
run: echo "C:\Program Files\7-Zip" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
|
||||||
|
- name: Download release artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release
|
||||||
|
path: release
|
||||||
|
|
||||||
|
- name: Download UI release artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-ui
|
||||||
|
path: release-ui
|
||||||
|
|
||||||
|
- name: Stage binaries into dist
|
||||||
|
run: |
|
||||||
|
$workdir = "dist\${{ env.PackageWorkdir }}"
|
||||||
|
New-Item -ItemType Directory -Force -Path $workdir | Out-Null
|
||||||
|
$client = Get-ChildItem -Recurse -Path release -Filter "netbird_*_windows_${{ matrix.arch }}.tar.gz" | Select-Object -First 1
|
||||||
|
$ui = Get-ChildItem -Recurse -Path release-ui -Filter "netbird-ui-windows_*_windows_${{ matrix.arch }}.tar.gz" | Select-Object -First 1
|
||||||
|
if (-not $client) { Write-Host "::error::client tarball not found for ${{ matrix.arch }}"; exit 1 }
|
||||||
|
if (-not $ui) { Write-Host "::error::ui tarball not found for ${{ matrix.arch }}"; exit 1 }
|
||||||
|
Write-Host "Client: $($client.FullName)"
|
||||||
|
Write-Host "UI: $($ui.FullName)"
|
||||||
|
tar -zvxf $client.FullName -C $workdir
|
||||||
|
tar -zvxf $ui.FullName -C $workdir
|
||||||
|
Get-ChildItem $workdir
|
||||||
|
|
||||||
|
- name: Download wintun
|
||||||
|
uses: carlosperate/download-file-action@v2
|
||||||
|
id: download-wintun
|
||||||
|
with:
|
||||||
|
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
||||||
|
file-name: wintun.zip
|
||||||
|
location: ${{ env.downloadPath }}
|
||||||
|
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
|
||||||
|
|
||||||
|
- name: Decompress wintun files
|
||||||
|
run: tar -zvxf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
||||||
|
|
||||||
|
- name: Move wintun.dll into dist
|
||||||
|
run: mv ${{ env.downloadPath }}\wintun\bin\${{ matrix.wintun_arch }}\wintun.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
||||||
|
|
||||||
|
- name: Download Mesa3D (amd64 only)
|
||||||
|
uses: carlosperate/download-file-action@v2
|
||||||
|
id: download-mesa3d
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
with:
|
||||||
|
file-url: https://downloads.fdossena.com/Projects/Mesa3D/Builds/MesaForWindows-x64-20.1.8.7z
|
||||||
|
file-name: mesa3d.7z
|
||||||
|
location: ${{ env.downloadPath }}
|
||||||
|
sha256: '71c7cb64ec229a1d6b8d62fa08e1889ed2bd17c0eeede8689daf0f25cb31d6b9'
|
||||||
|
|
||||||
|
- name: Extract Mesa3D driver (amd64 only)
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
run: 7z x -o"${{ env.downloadPath }}" "${{ env.downloadPath }}/mesa3d.7z"
|
||||||
|
|
||||||
|
- name: Move opengl32.dll into dist (amd64 only)
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
run: mv ${{ env.downloadPath }}\opengl32.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
||||||
|
|
||||||
|
- name: Download EnVar plugin for NSIS
|
||||||
|
uses: carlosperate/download-file-action@v2
|
||||||
|
with:
|
||||||
|
file-url: https://nsis.sourceforge.io/mediawiki/images/7/7f/EnVar_plugin.zip
|
||||||
|
file-name: envar_plugin.zip
|
||||||
|
location: ${{ github.workspace }}
|
||||||
|
|
||||||
|
- name: Extract EnVar plugin
|
||||||
|
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/envar_plugin.zip"
|
||||||
|
|
||||||
|
- name: Download ShellExecAsUser plugin for NSIS (amd64 only)
|
||||||
|
uses: carlosperate/download-file-action@v2
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
with:
|
||||||
|
file-url: https://nsis.sourceforge.io/mediawiki/images/6/68/ShellExecAsUser_amd64-Unicode.7z
|
||||||
|
file-name: ShellExecAsUser_amd64-Unicode.7z
|
||||||
|
location: ${{ github.workspace }}
|
||||||
|
|
||||||
|
- name: Extract ShellExecAsUser plugin (amd64 only)
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/ShellExecAsUser_amd64-Unicode.7z"
|
||||||
|
|
||||||
|
- name: Build NSIS installer
|
||||||
|
uses: joncloud/makensis-action@v3.3
|
||||||
|
with:
|
||||||
|
additional-plugin-paths: ${{ github.workspace }}/NSIS_Plugins/Plugins
|
||||||
|
script-file: client/installer.nsis
|
||||||
|
arguments: "/V4 /DARCH=${{ matrix.arch }}"
|
||||||
|
env:
|
||||||
|
APPVER: ${{ steps.semver_parser.outputs.major }}.${{ steps.semver_parser.outputs.minor }}.${{ steps.semver_parser.outputs.patch }}.${{ github.run_id }}
|
||||||
|
|
||||||
|
- name: Rename NSIS installer
|
||||||
|
run: mv netbird-installer.exe netbird_installer_test_windows_${{ matrix.arch }}.exe
|
||||||
|
|
||||||
|
- name: Install WiX
|
||||||
|
run: |
|
||||||
|
dotnet tool install --global wix --version 6.0.2
|
||||||
|
wix extension add WixToolset.Util.wixext/6.0.2
|
||||||
|
|
||||||
|
- name: Build MSI installer
|
||||||
|
env:
|
||||||
|
NETBIRD_VERSION: "${{ steps.semver_parser.outputs.fullversion }}"
|
||||||
|
run: wix build -arch ${{ matrix.arch == 'amd64' && 'x64' || 'arm64' }} -ext WixToolset.Util.wixext -o netbird_installer_test_windows_${{ matrix.arch }}.msi .\client\netbird.wxs -d ProcessorArchitecture=${{ matrix.arch == 'amd64' && 'x64' || 'arm64' }} -d ArchSuffix=${{ matrix.arch }}
|
||||||
|
|
||||||
|
- name: Upload installer artifacts
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-installer-test-${{ matrix.arch }}
|
||||||
|
path: |
|
||||||
|
netbird_installer_test_windows_${{ matrix.arch }}.exe
|
||||||
|
netbird_installer_test_windows_${{ matrix.arch }}.msi
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
comment_release_artifacts:
|
comment_release_artifacts:
|
||||||
name: Comment release artifacts
|
name: Comment release artifacts
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -554,7 +699,7 @@ jobs:
|
|||||||
|
|
||||||
trigger_signer:
|
trigger_signer:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [release, release_ui, release_ui_darwin]
|
needs: [release, release_ui, release_ui_darwin, test_windows_installer]
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger binaries sign pipelines
|
- name: Trigger binaries sign pipelines
|
||||||
|
|||||||
28
.github/workflows/sync-tag.yml
vendored
28
.github/workflows/sync-tag.yml
vendored
@@ -9,6 +9,8 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
# Receiving workflows (cloud sync-tag, mobile bump-netbird) expect the short
|
||||||
|
# tag form (e.g. v0.30.0), not refs/tags/v0.30.0 — github.ref_name, not github.ref.
|
||||||
jobs:
|
jobs:
|
||||||
trigger_sync_tag:
|
trigger_sync_tag:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -21,3 +23,29 @@ jobs:
|
|||||||
repo: ${{ secrets.UPSTREAM_REPO }}
|
repo: ${{ secrets.UPSTREAM_REPO }}
|
||||||
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||||
|
|
||||||
|
trigger_android_bump:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||||
|
steps:
|
||||||
|
- name: Trigger android-client submodule bump
|
||||||
|
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
||||||
|
with:
|
||||||
|
workflow: bump-netbird.yml
|
||||||
|
ref: main
|
||||||
|
repo: netbirdio/android-client
|
||||||
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
|
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||||
|
|
||||||
|
trigger_ios_bump:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||||
|
steps:
|
||||||
|
- name: Trigger ios-client submodule bump
|
||||||
|
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
||||||
|
with:
|
||||||
|
workflow: bump-netbird.yml
|
||||||
|
ref: main
|
||||||
|
repo: netbirdio/ios-client
|
||||||
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
|
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||||
@@ -135,7 +135,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &mgmt.MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &mgmt.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ Pop $0
|
|||||||
|
|
||||||
Function .onInit
|
Function .onInit
|
||||||
StrCpy $INSTDIR "${INSTALL_DIR}"
|
StrCpy $INSTDIR "${INSTALL_DIR}"
|
||||||
|
; Default autostart to enabled so silent installs (/S) match the interactive default
|
||||||
|
StrCpy $AutostartEnabled "1"
|
||||||
|
|
||||||
; Pre-0.70.1 installers ran without SetRegView, so their uninstall keys live
|
; Pre-0.70.1 installers ran without SetRegView, so their uninstall keys live
|
||||||
; in the 32-bit view. Fall back to it so upgrades still find them.
|
; in the 32-bit view. Fall back to it so upgrades still find them.
|
||||||
|
|||||||
@@ -1671,7 +1671,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package activity
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,10 +17,6 @@ import (
|
|||||||
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
|
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isBindListenerPlatform() bool {
|
|
||||||
return runtime.GOOS == "windows" || runtime.GOOS == "js"
|
|
||||||
}
|
|
||||||
|
|
||||||
// mockEndpointManager implements device.EndpointManager for testing
|
// mockEndpointManager implements device.EndpointManager for testing
|
||||||
type mockEndpointManager struct {
|
type mockEndpointManager struct {
|
||||||
endpoints map[netip.Addr]net.Conn
|
endpoints map[netip.Addr]net.Conn
|
||||||
@@ -181,10 +176,6 @@ func TestBindListener_Close(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_BindMode(t *testing.T) {
|
func TestManager_BindMode(t *testing.T) {
|
||||||
if !isBindListenerPlatform() {
|
|
||||||
t.Skip("BindListener only used on Windows/JS platforms")
|
|
||||||
}
|
|
||||||
|
|
||||||
mockEndpointMgr := newMockEndpointManager()
|
mockEndpointMgr := newMockEndpointManager()
|
||||||
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
||||||
|
|
||||||
@@ -226,10 +217,6 @@ func TestManager_BindMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_BindMode_MultiplePeers(t *testing.T) {
|
func TestManager_BindMode_MultiplePeers(t *testing.T) {
|
||||||
if !isBindListenerPlatform() {
|
|
||||||
t.Skip("BindListener only used on Windows/JS platforms")
|
|
||||||
}
|
|
||||||
|
|
||||||
mockEndpointMgr := newMockEndpointManager()
|
mockEndpointMgr := newMockEndpointManager()
|
||||||
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/netstack"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
||||||
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
|
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
|
||||||
@@ -75,16 +73,6 @@ func (m *Manager) createListener(peerCfg lazyconn.PeerConfig) (listener, error)
|
|||||||
return NewUDPListener(m.wgIface, peerCfg)
|
return NewUDPListener(m.wgIface, peerCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindListener is used on Windows, JS, and netstack platforms:
|
|
||||||
// - JS: Cannot listen to UDP sockets
|
|
||||||
// - Windows: IP_UNICAST_IF socket option forces packets out the interface the default
|
|
||||||
// gateway points to, preventing them from reaching the loopback interface.
|
|
||||||
// - Netstack: Allows multiple instances on the same host without port conflicts.
|
|
||||||
// BindListener bypasses these issues by passing data directly through the bind.
|
|
||||||
if runtime.GOOS != "windows" && runtime.GOOS != "js" && !netstack.IsEnabled() {
|
|
||||||
return NewUDPListener(m.wgIface, peerCfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
provider, ok := m.wgIface.(bindProvider)
|
provider, ok := m.wgIface.(bindProvider)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("interface claims userspace bind but doesn't implement bindProvider")
|
return nil, errors.New("interface claims userspace bind but doesn't implement bindProvider")
|
||||||
|
|||||||
@@ -89,9 +89,17 @@ func (r *SysOps) installScopedDefaultFor(unspec netip.Addr) (bool, error) {
|
|||||||
return false, fmt.Errorf("unusable default nexthop for %s (no interface)", unspec)
|
return false, fmt.Errorf("unusable default nexthop for %s (no interface)", unspec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reused := false
|
||||||
if err := r.addScopedDefault(unspec, nexthop); err != nil {
|
if err := r.addScopedDefault(unspec, nexthop); err != nil {
|
||||||
|
if !errors.Is(err, unix.EEXIST) {
|
||||||
return false, fmt.Errorf("add scoped default on %s: %w", nexthop.Intf.Name, err)
|
return false, fmt.Errorf("add scoped default on %s: %w", nexthop.Intf.Name, err)
|
||||||
}
|
}
|
||||||
|
// macOS installs its own RTF_IFSCOPE defaults for primary service
|
||||||
|
// selection on multi-NIC setups, so a route on this ifindex can
|
||||||
|
// already exist before we try. Binding to it via IP[V6]_BOUND_IF
|
||||||
|
// still produces the scoped lookup we need.
|
||||||
|
reused = true
|
||||||
|
}
|
||||||
|
|
||||||
af := unix.AF_INET
|
af := unix.AF_INET
|
||||||
if unspec.Is6() {
|
if unspec.Is6() {
|
||||||
@@ -102,7 +110,11 @@ func (r *SysOps) installScopedDefaultFor(unspec netip.Addr) (bool, error) {
|
|||||||
if nexthop.IP.IsValid() {
|
if nexthop.IP.IsValid() {
|
||||||
via = nexthop.IP.String()
|
via = nexthop.IP.String()
|
||||||
}
|
}
|
||||||
log.Infof("installed scoped default route via %s on %s for %s", via, nexthop.Intf.Name, afOf(unspec))
|
verb := "installed"
|
||||||
|
if reused {
|
||||||
|
verb = "reused existing"
|
||||||
|
}
|
||||||
|
log.Infof("%s scoped default route via %s on %s for %s", verb, via, nexthop.Intf.Name, afOf(unspec))
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
|
|
||||||
<MajorUpgrade AllowSameVersionUpgrades='yes' DowngradeErrorMessage="A newer version of [ProductName] is already installed. Setup will now exit."/>
|
<MajorUpgrade AllowSameVersionUpgrades='yes' DowngradeErrorMessage="A newer version of [ProductName] is already installed. Setup will now exit."/>
|
||||||
|
|
||||||
|
<!-- Autostart: enabled by default, disable with AUTOSTART=0 on the msiexec command line -->
|
||||||
|
<Property Id="AUTOSTART" Value="1" />
|
||||||
|
|
||||||
<StandardDirectory Id="ProgramFiles64Folder">
|
<StandardDirectory Id="ProgramFiles64Folder">
|
||||||
<Directory Id="NetbirdInstallDir" Name="Netbird">
|
<Directory Id="NetbirdInstallDir" Name="Netbird">
|
||||||
<Component Id="NetbirdFiles" Guid="db3165de-cc6e-4922-8396-9d892950e23e" Bitness="always64">
|
<Component Id="NetbirdFiles" Guid="db3165de-cc6e-4922-8396-9d892950e23e" Bitness="always64">
|
||||||
@@ -63,9 +66,20 @@
|
|||||||
</Component>
|
</Component>
|
||||||
</StandardDirectory>
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<StandardDirectory Id="CommonAppDataFolder">
|
||||||
|
<Directory Id="NetbirdAutoStartDir" Name="Netbird">
|
||||||
|
<Component Id="NetbirdAutoStart" Guid="b199eaca-b0dd-4032-af19-679cfad48eb3" Bitness="always64" Condition='AUTOSTART = "1"'>
|
||||||
|
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Run"
|
||||||
|
Name="Netbird" Value=""[NetbirdInstallDir]netbird-ui.exe""
|
||||||
|
Type="string" KeyPath="yes" />
|
||||||
|
</Component>
|
||||||
|
</Directory>
|
||||||
|
</StandardDirectory>
|
||||||
|
|
||||||
<ComponentGroup Id="NetbirdFilesComponent">
|
<ComponentGroup Id="NetbirdFilesComponent">
|
||||||
<ComponentRef Id="NetbirdFiles" />
|
<ComponentRef Id="NetbirdFiles" />
|
||||||
<ComponentRef Id="NetbirdAumidRegistry" />
|
<ComponentRef Id="NetbirdAumidRegistry" />
|
||||||
|
<ComponentRef Id="NetbirdAutoStart" />
|
||||||
</ComponentGroup>
|
</ComponentGroup>
|
||||||
|
|
||||||
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />
|
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &server.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ func (s *BaseServer) GRPCServer() *grpc.Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gRPCAPIHandler := grpc.NewServer(gRPCOpts...)
|
gRPCAPIHandler := grpc.NewServer(gRPCOpts...)
|
||||||
srv, err := nbgrpc.NewServer(s.Config, s.AccountManager(), s.SettingsManager(), s.JobManager(), s.SecretsManager(), s.Metrics(), s.AuthManager(), s.IntegratedValidator(), s.NetworkMapController(), s.OAuthConfigProvider())
|
srv, err := nbgrpc.NewServer(s.Config, s.AccountManager(), s.SettingsManager(), s.JobManager(), s.SecretsManager(), s.Metrics(), s.AuthManager(), s.IntegratedValidator(), s.NetworkMapController(), s.OAuthConfigProvider(), s.SessionStore())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create management server: %v", err)
|
log.Fatalf("failed to create management server: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||||
proxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy/manager"
|
proxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy/manager"
|
||||||
|
|
||||||
@@ -66,6 +67,12 @@ func (s *BaseServer) SecretsManager() grpc.SecretsManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *BaseServer) SessionStore() *auth.SessionStore {
|
||||||
|
return Create(s, func() *auth.SessionStore {
|
||||||
|
return auth.NewSessionStore(s.CacheStore())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *BaseServer) AuthManager() auth.Manager {
|
func (s *BaseServer) AuthManager() auth.Manager {
|
||||||
audiences := s.Config.GetAuthAudiences()
|
audiences := s.Config.GetAuthAudiences()
|
||||||
audience := s.Config.HttpConfig.AuthAudience
|
audience := s.Config.HttpConfig.AuthAudience
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
jwtv5 "github.com/golang-jwt/jwt/v5"
|
||||||
pb "github.com/golang/protobuf/proto" // nolint
|
pb "github.com/golang/protobuf/proto" // nolint
|
||||||
"github.com/golang/protobuf/ptypes/timestamp"
|
"github.com/golang/protobuf/ptypes/timestamp"
|
||||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
|
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
|
||||||
@@ -67,6 +68,7 @@ type Server struct {
|
|||||||
appMetrics telemetry.AppMetrics
|
appMetrics telemetry.AppMetrics
|
||||||
peerLocks sync.Map
|
peerLocks sync.Map
|
||||||
authManager auth.Manager
|
authManager auth.Manager
|
||||||
|
sessionStore *auth.SessionStore
|
||||||
|
|
||||||
logBlockedPeers bool
|
logBlockedPeers bool
|
||||||
blockPeersWithSameConfig bool
|
blockPeersWithSameConfig bool
|
||||||
@@ -98,6 +100,7 @@ func NewServer(
|
|||||||
integratedPeerValidator integrated_validator.IntegratedValidator,
|
integratedPeerValidator integrated_validator.IntegratedValidator,
|
||||||
networkMapController network_map.Controller,
|
networkMapController network_map.Controller,
|
||||||
oAuthConfigProvider idp.OAuthConfigProvider,
|
oAuthConfigProvider idp.OAuthConfigProvider,
|
||||||
|
sessionStore *auth.SessionStore,
|
||||||
) (*Server, error) {
|
) (*Server, error) {
|
||||||
if appMetrics != nil {
|
if appMetrics != nil {
|
||||||
// update gauge based on number of connected peers which is equal to open gRPC streams
|
// update gauge based on number of connected peers which is equal to open gRPC streams
|
||||||
@@ -140,6 +143,7 @@ func NewServer(
|
|||||||
integratedPeerValidator: integratedPeerValidator,
|
integratedPeerValidator: integratedPeerValidator,
|
||||||
networkMapController: networkMapController,
|
networkMapController: networkMapController,
|
||||||
oAuthConfigProvider: oAuthConfigProvider,
|
oAuthConfigProvider: oAuthConfigProvider,
|
||||||
|
sessionStore: sessionStore,
|
||||||
|
|
||||||
loginFilter: newLoginFilter(),
|
loginFilter: newLoginFilter(),
|
||||||
|
|
||||||
@@ -535,7 +539,7 @@ func (s *Server) cancelPeerRoutinesWithoutLock(ctx context.Context, accountID st
|
|||||||
log.WithContext(ctx).Debugf("peer %s has been disconnected", peer.Key)
|
log.WithContext(ctx).Debugf("peer %s has been disconnected", peer.Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) validateToken(ctx context.Context, jwtToken string) (string, error) {
|
func (s *Server) validateToken(ctx context.Context, peerKey, jwtToken string) (string, error) {
|
||||||
if s.authManager == nil {
|
if s.authManager == nil {
|
||||||
return "", status.Errorf(codes.Internal, "missing auth manager")
|
return "", status.Errorf(codes.Internal, "missing auth manager")
|
||||||
}
|
}
|
||||||
@@ -545,6 +549,10 @@ func (s *Server) validateToken(ctx context.Context, jwtToken string) (string, er
|
|||||||
return "", status.Errorf(codes.InvalidArgument, "invalid jwt token, err: %v", err)
|
return "", status.Errorf(codes.InvalidArgument, "invalid jwt token, err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.claimLoginToken(ctx, peerKey, jwtToken, token); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
// we need to call this method because if user is new, we will automatically add it to existing or create a new account
|
// we need to call this method because if user is new, we will automatically add it to existing or create a new account
|
||||||
accountId, _, err := s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth)
|
accountId, _, err := s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -828,6 +836,31 @@ func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, ne
|
|||||||
return loginResp, nil
|
return loginResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) claimLoginToken(ctx context.Context, peerKey, jwtToken string, token *jwtv5.Token) error {
|
||||||
|
if s.sessionStore == nil || token == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
exp, err := token.Claims.GetExpirationTime()
|
||||||
|
if err != nil || exp == nil {
|
||||||
|
log.WithContext(ctx).Warnf("JWT has no usable exp claim for peer %s", peerKey)
|
||||||
|
return status.Error(codes.Unauthenticated, "jwt token has no expiration")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.sessionStore.RegisterToken(ctx, jwtToken, exp.Time)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, auth.ErrTokenAlreadyUsed) || errors.Is(err, auth.ErrTokenExpired) {
|
||||||
|
log.WithContext(ctx).Warnf("%v for peer %s", err, peerKey)
|
||||||
|
return status.Error(codes.Unauthenticated, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithContext(ctx).Warnf("failed to claim JWT for peer %s: %v", peerKey, err)
|
||||||
|
return status.Error(codes.Unavailable, "failed to claim jwt token")
|
||||||
|
}
|
||||||
|
|
||||||
// processJwtToken validates the existence of a JWT token in the login request, and returns the corresponding user ID if
|
// processJwtToken validates the existence of a JWT token in the login request, and returns the corresponding user ID if
|
||||||
// the token is valid.
|
// the token is valid.
|
||||||
//
|
//
|
||||||
@@ -838,7 +871,7 @@ func (s *Server) processJwtToken(ctx context.Context, loginReq *proto.LoginReque
|
|||||||
if loginReq.GetJwtToken() != "" {
|
if loginReq.GetJwtToken() != "" {
|
||||||
var err error
|
var err error
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
userID, err = s.validateToken(ctx, loginReq.GetJwtToken())
|
userID, err = s.validateToken(ctx, peerKey.String(), loginReq.GetJwtToken())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
61
management/server/auth/session.go
Normal file
61
management/server/auth/session.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/eko/gocache/lib/v4/cache"
|
||||||
|
"github.com/eko/gocache/lib/v4/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
usedTokenKeyPrefix = "jwt-used:"
|
||||||
|
usedTokenMarker = "1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrTokenAlreadyUsed = errors.New("JWT already used")
|
||||||
|
ErrTokenExpired = errors.New("JWT expired")
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionStore struct {
|
||||||
|
cache *cache.Cache[string]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionStore(cacheStore store.StoreInterface) *SessionStore {
|
||||||
|
return &SessionStore{cache: cache.New[string](cacheStore)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterToken records a JWT until its exp time and rejects reuse.
|
||||||
|
func (s *SessionStore) RegisterToken(ctx context.Context, token string, expiresAt time.Time) error {
|
||||||
|
ttl := time.Until(expiresAt)
|
||||||
|
if ttl <= 0 {
|
||||||
|
return ErrTokenExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
key := usedTokenKeyPrefix + hashToken(token)
|
||||||
|
_, err := s.cache.Get(ctx, key)
|
||||||
|
if err == nil {
|
||||||
|
return ErrTokenAlreadyUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
var notFound *store.NotFound
|
||||||
|
if !errors.As(err, ¬Found) {
|
||||||
|
return fmt.Errorf("failed to lookup used token entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.cache.Set(ctx, key, usedTokenMarker, store.WithExpiration(ttl)); err != nil {
|
||||||
|
return fmt.Errorf("failed to store used token entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashToken(token string) string {
|
||||||
|
sum := sha256.Sum256([]byte(token))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
82
management/server/auth/session_test.go
Normal file
82
management/server/auth/session_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
nbcache "github.com/netbirdio/netbird/management/server/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestSessionStore(t *testing.T) *SessionStore {
|
||||||
|
t.Helper()
|
||||||
|
cacheStore, err := nbcache.NewStore(context.Background(), time.Hour, time.Hour, 100)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return NewSessionStore(cacheStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionStore_FirstRegisterSucceeds(t *testing.T) {
|
||||||
|
s := newTestSessionStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
require.NoError(t, s.RegisterToken(ctx, "token", time.Now().Add(time.Hour)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionStore_RegisterSameTokenTwiceIsRejected(t *testing.T) {
|
||||||
|
s := newTestSessionStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
token := "token"
|
||||||
|
exp := time.Now().Add(time.Hour)
|
||||||
|
|
||||||
|
require.NoError(t, s.RegisterToken(ctx, token, exp))
|
||||||
|
|
||||||
|
err := s.RegisterToken(ctx, token, exp)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, ErrTokenAlreadyUsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionStore_RegisterDifferentTokensAreIndependent(t *testing.T) {
|
||||||
|
s := newTestSessionStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
exp := time.Now().Add(time.Hour)
|
||||||
|
|
||||||
|
require.NoError(t, s.RegisterToken(ctx, "tokenA", exp))
|
||||||
|
require.NoError(t, s.RegisterToken(ctx, "tokenB", exp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionStore_RegisterWithPastExpiryIsRejected(t *testing.T) {
|
||||||
|
s := newTestSessionStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
token := "token"
|
||||||
|
|
||||||
|
err := s.RegisterToken(ctx, token, time.Now().Add(-time.Second))
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, ErrTokenExpired)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionStore_EntryEvictsAtTTLAndAllowsReRegistration(t *testing.T) {
|
||||||
|
s := newTestSessionStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
token := "token"
|
||||||
|
|
||||||
|
require.NoError(t, s.RegisterToken(ctx, token, time.Now().Add(50*time.Millisecond)))
|
||||||
|
|
||||||
|
err := s.RegisterToken(ctx, token, time.Now().Add(50*time.Millisecond))
|
||||||
|
assert.ErrorIs(t, err, ErrTokenAlreadyUsed)
|
||||||
|
|
||||||
|
time.Sleep(120 * time.Millisecond)
|
||||||
|
|
||||||
|
require.NoError(t, s.RegisterToken(ctx, token, time.Now().Add(time.Hour)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashToken_StableAndDoesNotLeak(t *testing.T) {
|
||||||
|
a := hashToken("tokenA")
|
||||||
|
b := hashToken("tokenB")
|
||||||
|
assert.Equal(t, a, hashToken("tokenA"), "hash must be deterministic")
|
||||||
|
assert.NotEqual(t, a, b, "different tokens must hash differently")
|
||||||
|
assert.Len(t, a, 64, "sha256 hex must be 64 chars")
|
||||||
|
assert.NotContains(t, a, "tokenA", "raw token must not appear in hash")
|
||||||
|
}
|
||||||
@@ -391,7 +391,7 @@ func startManagementForTest(t *testing.T, testFile string, config *config.Config
|
|||||||
return nil, nil, "", cleanup, err
|
return nil, nil, "", cleanup, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, "", cleanup, err
|
return nil, nil, "", cleanup, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ func startServer(
|
|||||||
server.MockIntegratedValidator{},
|
server.MockIntegratedValidator{},
|
||||||
networkMapController,
|
networkMapController,
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed creating management server: %v", err)
|
t.Fatalf("failed creating management server: %v", err)
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, mgmt.MockIntegratedValidator{}, networkMapController, nil)
|
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, mgmt.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user