mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-05 08:36:37 +00:00
Compare commits
12 Commits
v0.70.2
...
add-steamo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3cf615e51 | ||
|
|
1e11b93e6b | ||
|
|
76838e9170 | ||
|
|
dbd0142b1e | ||
|
|
051d17d01b | ||
|
|
9938da9bbd | ||
|
|
94657c1c80 | ||
|
|
c400d57079 | ||
|
|
ed828b7af4 | ||
|
|
11ac2af2f5 | ||
|
|
df197d5001 | ||
|
|
ad93dcf980 |
@@ -135,7 +135,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
||||
if err != nil {
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -201,6 +201,8 @@ Pop $0
|
||||
|
||||
Function .onInit
|
||||
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
|
||||
; 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 {
|
||||
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 {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package activity
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -18,10 +17,6 @@ import (
|
||||
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
|
||||
type mockEndpointManager struct {
|
||||
endpoints map[netip.Addr]net.Conn
|
||||
@@ -181,10 +176,6 @@ func TestBindListener_Close(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestManager_BindMode(t *testing.T) {
|
||||
if !isBindListenerPlatform() {
|
||||
t.Skip("BindListener only used on Windows/JS platforms")
|
||||
}
|
||||
|
||||
mockEndpointMgr := newMockEndpointManager()
|
||||
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
||||
|
||||
@@ -226,10 +217,6 @@ func TestManager_BindMode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestManager_BindMode_MultiplePeers(t *testing.T) {
|
||||
if !isBindListenerPlatform() {
|
||||
t.Skip("BindListener only used on Windows/JS platforms")
|
||||
}
|
||||
|
||||
mockEndpointMgr := newMockEndpointManager()
|
||||
mockIface := &MockWGIfaceBind{endpointMgr: mockEndpointMgr}
|
||||
|
||||
|
||||
@@ -4,14 +4,12 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"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/internal/lazyconn"
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if !ok {
|
||||
return nil, errors.New("interface claims userspace bind but doesn't implement bindProvider")
|
||||
|
||||
@@ -89,8 +89,16 @@ func (r *SysOps) installScopedDefaultFor(unspec netip.Addr) (bool, error) {
|
||||
return false, fmt.Errorf("unusable default nexthop for %s (no interface)", unspec)
|
||||
}
|
||||
|
||||
reused := false
|
||||
if err := r.addScopedDefault(unspec, nexthop); err != nil {
|
||||
return false, fmt.Errorf("add scoped default on %s: %w", nexthop.Intf.Name, err)
|
||||
if !errors.Is(err, unix.EEXIST) {
|
||||
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
|
||||
@@ -102,7 +110,11 @@ func (r *SysOps) installScopedDefaultFor(unspec netip.Addr) (bool, error) {
|
||||
if nexthop.IP.IsValid() {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
|
||||
<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">
|
||||
<Directory Id="NetbirdInstallDir" Name="Netbird">
|
||||
<Component Id="NetbirdFiles" Guid="db3165de-cc6e-4922-8396-9d892950e23e" Bitness="always64">
|
||||
@@ -63,9 +66,21 @@
|
||||
</Component>
|
||||
</StandardDirectory>
|
||||
|
||||
<StandardDirectory Id="CommonAppDataFolder">
|
||||
<Directory Id="NetbirdAutoStartDir" Name="Netbird">
|
||||
<Component Id="NetbirdAutoStart" Guid="b199eaca-b0dd-4032-af19-679cfad48eb3" Bitness="always64">
|
||||
<Condition>AUTOSTART = "1"</Condition>
|
||||
<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">
|
||||
<ComponentRef Id="NetbirdFiles" />
|
||||
<ComponentRef Id="NetbirdAumidRegistry" />
|
||||
<ComponentRef Id="NetbirdAutoStart" />
|
||||
</ComponentGroup>
|
||||
|
||||
<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 {
|
||||
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 {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ func (s *BaseServer) GRPCServer() *grpc.Server {
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Fatalf("failed to create management server: %v", err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/management-integrations/integrations"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||
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 {
|
||||
audiences := s.Config.GetAuthAudiences()
|
||||
audience := s.Config.HttpConfig.AuthAudience
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
jwtv5 "github.com/golang-jwt/jwt/v5"
|
||||
pb "github.com/golang/protobuf/proto" // nolint
|
||||
"github.com/golang/protobuf/ptypes/timestamp"
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
|
||||
@@ -67,6 +68,7 @@ type Server struct {
|
||||
appMetrics telemetry.AppMetrics
|
||||
peerLocks sync.Map
|
||||
authManager auth.Manager
|
||||
sessionStore *auth.SessionStore
|
||||
|
||||
logBlockedPeers bool
|
||||
blockPeersWithSameConfig bool
|
||||
@@ -98,6 +100,7 @@ func NewServer(
|
||||
integratedPeerValidator integrated_validator.IntegratedValidator,
|
||||
networkMapController network_map.Controller,
|
||||
oAuthConfigProvider idp.OAuthConfigProvider,
|
||||
sessionStore *auth.SessionStore,
|
||||
) (*Server, error) {
|
||||
if appMetrics != nil {
|
||||
// update gauge based on number of connected peers which is equal to open gRPC streams
|
||||
@@ -140,6 +143,7 @@ func NewServer(
|
||||
integratedPeerValidator: integratedPeerValidator,
|
||||
networkMapController: networkMapController,
|
||||
oAuthConfigProvider: oAuthConfigProvider,
|
||||
sessionStore: sessionStore,
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
accountId, _, err := s.accountManager.GetAccountIDFromUserAuth(ctx, userAuth)
|
||||
if err != nil {
|
||||
@@ -828,6 +836,31 @@ func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, ne
|
||||
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
|
||||
// the token is valid.
|
||||
//
|
||||
@@ -838,7 +871,7 @@ func (s *Server) processJwtToken(ctx context.Context, loginReq *proto.LoginReque
|
||||
if loginReq.GetJwtToken() != "" {
|
||||
var err error
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, nil, "", cleanup, err
|
||||
}
|
||||
|
||||
@@ -256,6 +256,7 @@ func startServer(
|
||||
server.MockIntegratedValidator{},
|
||||
networkMapController,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed creating management server: %v", err)
|
||||
|
||||
440
release_files/install-steamos.sh
Executable file
440
release_files/install-steamos.sh
Executable file
@@ -0,0 +1,440 @@
|
||||
#!/bin/bash
|
||||
# NetBird installer for SteamOS (Steam Deck)
|
||||
#
|
||||
# Installs NetBird as a user-level service running entirely from /home.
|
||||
# Uses userspace WireGuard with a real kernel TUN interface for proper
|
||||
# network performance (important for game streaming via Moonlight/Sunshine).
|
||||
# Requires sudo once at install (and per update) to grant file capabilities.
|
||||
# Survives all SteamOS updates without intervention.
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://raw.githubusercontent.com/netbirdio/netbird/main/release_files/install-steamos.sh | bash
|
||||
# bash install-steamos.sh --update
|
||||
# bash install-steamos.sh --uninstall
|
||||
#
|
||||
# Environment variables:
|
||||
# NETBIRD_RELEASE - Version to install (default: "latest")
|
||||
# GITHUB_TOKEN - GitHub token for rate-limited API calls
|
||||
# NB_MANAGEMENT_URL - Custom management server URL
|
||||
# NB_ADMIN_URL - Custom admin dashboard URL
|
||||
# NB_SETUP_KEY - Setup key for automatic authentication
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OWNER="netbirdio"
|
||||
REPO="netbird"
|
||||
BINARY="netbird"
|
||||
|
||||
INSTALL_DIR="${HOME}/.local/bin"
|
||||
CONFIG_DIR="${HOME}/.config/netbird"
|
||||
STATE_DIR="${HOME}/.local/share/netbird"
|
||||
SYSTEMD_DIR="${HOME}/.config/systemd/user"
|
||||
SERVICE_NAME="netbird"
|
||||
|
||||
NETBIRD_RELEASE="${NETBIRD_RELEASE:-latest}"
|
||||
TAG_NAME=""
|
||||
|
||||
# --- Logging ---
|
||||
|
||||
info() { printf '\033[1;32m[netbird]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[netbird]\033[0m %s\n' "$*" >&2; }
|
||||
error() { printf '\033[1;31m[netbird]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# --- Validation ---
|
||||
|
||||
check_steamos() {
|
||||
if [[ ! -f /etc/os-release ]]; then
|
||||
error "Cannot detect OS: /etc/os-release not found"
|
||||
fi
|
||||
|
||||
. /etc/os-release
|
||||
|
||||
# Accept steamos, or allow --force for other immutable Linux distros
|
||||
if [[ "${ID:-}" != "steamos" ]] && [[ "${FORCE:-}" != "true" ]]; then
|
||||
warn "This script is designed for SteamOS (detected: ${ID:-unknown})"
|
||||
warn "Set FORCE=true to install anyway on immutable Linux distros"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Detected ${PRETTY_NAME:-SteamOS}"
|
||||
}
|
||||
|
||||
check_arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) ARCH="amd64" ;;
|
||||
aarch64|arm64) ARCH="arm64" ;;
|
||||
*)
|
||||
error "Unsupported architecture: $(uname -m)"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_dependencies() {
|
||||
local missing=""
|
||||
for cmd in curl tar systemctl sudo sha256sum; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
missing="$missing $cmd"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n "$missing" ]]; then
|
||||
error "Missing required commands:$missing"
|
||||
fi
|
||||
|
||||
# Verify user-level systemd is functional
|
||||
if ! systemctl --user status >/dev/null 2>&1; then
|
||||
error "systemctl --user is not functional. Is systemd user session running?"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Release fetching (adapted from install.sh) ---
|
||||
|
||||
get_release() {
|
||||
local release="$1"
|
||||
if [[ "$release" == "latest" ]]; then
|
||||
local url="https://pkgs.netbird.io/releases/latest"
|
||||
else
|
||||
local url="https://api.github.com/repos/${OWNER}/${REPO}/releases/tags/${release}"
|
||||
fi
|
||||
|
||||
local output=""
|
||||
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
||||
output=$(curl -fsSL -H "Authorization: token ${GITHUB_TOKEN}" "$url")
|
||||
else
|
||||
output=$(curl -fsSL "$url")
|
||||
fi
|
||||
|
||||
TAG_NAME=$(echo "$output" | grep -Eo '"tag_name":\s*"v([0-9]+\.){2}[0-9]+"' | tail -n 1)
|
||||
echo "$TAG_NAME" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+'
|
||||
}
|
||||
|
||||
download_binary() {
|
||||
local dest_dir="${1:-$INSTALL_DIR}"
|
||||
local version
|
||||
version=$(get_release "$NETBIRD_RELEASE")
|
||||
|
||||
if [[ -z "$version" ]]; then
|
||||
error "Failed to determine NetBird version"
|
||||
fi
|
||||
|
||||
local version_num="${version#v}"
|
||||
local tarball="${BINARY}_${version_num}_linux_${ARCH}.tar.gz"
|
||||
local checksums="${BINARY}_${version_num}_checksums.txt"
|
||||
local base_url="https://github.com/${OWNER}/${REPO}/releases/download/${version}"
|
||||
|
||||
info "Downloading NetBird ${version} for ${ARCH}..."
|
||||
|
||||
local tmp_dir
|
||||
tmp_dir=$(mktemp -d)
|
||||
trap "rm -rf '$tmp_dir'" EXIT
|
||||
|
||||
local auth_header=""
|
||||
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
||||
auth_header="Authorization: token ${GITHUB_TOKEN}"
|
||||
fi
|
||||
|
||||
# Download tarball and checksums
|
||||
curl -fsSL ${auth_header:+-H "$auth_header"} -o "${tmp_dir}/${tarball}" "${base_url}/${tarball}"
|
||||
curl -fsSL ${auth_header:+-H "$auth_header"} -o "${tmp_dir}/${checksums}" "${base_url}/${checksums}"
|
||||
|
||||
# Verify checksum
|
||||
info "Verifying checksum..."
|
||||
local expected
|
||||
expected=$(grep " ${tarball}$" "${tmp_dir}/${checksums}" | awk '{print $1}')
|
||||
if [[ -z "$expected" ]]; then
|
||||
error "Checksum for ${tarball} not found in ${checksums}"
|
||||
fi
|
||||
|
||||
local actual
|
||||
actual=$(sha256sum "${tmp_dir}/${tarball}" | awk '{print $1}')
|
||||
if [[ "$expected" != "$actual" ]]; then
|
||||
error "Checksum mismatch for ${tarball}: expected ${expected}, got ${actual}"
|
||||
fi
|
||||
info "Checksum verified"
|
||||
|
||||
tar -xzf "${tmp_dir}/${tarball}" -C "$tmp_dir" "$BINARY"
|
||||
|
||||
mkdir -p "$dest_dir"
|
||||
mv "${tmp_dir}/${BINARY}" "${dest_dir}/${BINARY}"
|
||||
chmod 755 "${dest_dir}/${BINARY}"
|
||||
|
||||
info "Installed ${dest_dir}/${BINARY} (${version})"
|
||||
}
|
||||
|
||||
# --- Network capabilities ---
|
||||
|
||||
apply_capabilities() {
|
||||
local binary_path="${1:-${INSTALL_DIR}/${BINARY}}"
|
||||
|
||||
info "Granting network capabilities (sudo required)..."
|
||||
if ! sudo setcap cap_net_admin,cap_net_raw+eip "$binary_path"; then
|
||||
error "Failed to set capabilities. Is sudo available?"
|
||||
fi
|
||||
info "Capabilities set on ${binary_path}"
|
||||
}
|
||||
|
||||
verify_capabilities() {
|
||||
local binary_path="${1:-${INSTALL_DIR}/${BINARY}}"
|
||||
|
||||
if ! command -v getcap >/dev/null 2>&1; then
|
||||
warn "getcap not found, skipping capability verification"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local caps
|
||||
caps=$(getcap "$binary_path" 2>/dev/null || true)
|
||||
if [[ "$caps" == *"cap_net_admin"* ]] && [[ "$caps" == *"cap_net_raw"* ]]; then
|
||||
info "Verified: ${caps}"
|
||||
return 0
|
||||
else
|
||||
warn "Capabilities not set correctly: ${caps}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Systemd user service ---
|
||||
|
||||
write_service_unit() {
|
||||
mkdir -p "$SYSTEMD_DIR"
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
cat > "${SYSTEMD_DIR}/${SERVICE_NAME}.service" <<EOF
|
||||
[Unit]
|
||||
Description=NetBird Client (SteamOS)
|
||||
Documentation=https://netbird.io/docs
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=NB_CONFIG=${CONFIG_DIR}/config.json
|
||||
Environment=NB_STATE_DIR=${STATE_DIR}
|
||||
Environment=NB_DAEMON_ADDR=unix://${STATE_DIR}/netbird.sock
|
||||
Environment=NB_LOG_FILE=${STATE_DIR}/client.log
|
||||
ExecStart=${INSTALL_DIR}/${BINARY} service run
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStopSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
info "Created systemd user service"
|
||||
}
|
||||
|
||||
enable_service() {
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable "${SERVICE_NAME}.service"
|
||||
systemctl --user start "${SERVICE_NAME}.service"
|
||||
|
||||
# Enable lingering so the service runs even when not logged into a desktop session
|
||||
if command -v loginctl >/dev/null 2>&1; then
|
||||
loginctl enable-linger "$(whoami)" 2>/dev/null || \
|
||||
warn "Could not enable linger. Service will only run while logged in."
|
||||
fi
|
||||
|
||||
info "Service enabled and started"
|
||||
}
|
||||
|
||||
# --- Shell environment ---
|
||||
|
||||
configure_shell_env() {
|
||||
local shell_rc="${HOME}/.bashrc"
|
||||
if [[ -f "${HOME}/.zshrc" ]]; then
|
||||
shell_rc="${HOME}/.zshrc"
|
||||
fi
|
||||
|
||||
local daemon_addr="unix://${STATE_DIR}/netbird.sock"
|
||||
|
||||
if ! grep -qF "# Added by NetBird installer" "$shell_rc" 2>/dev/null; then
|
||||
cat >> "$shell_rc" <<SHELLRC
|
||||
|
||||
# Added by NetBird installer
|
||||
export PATH="${INSTALL_DIR}:\$PATH"
|
||||
export NB_DAEMON_ADDR="${daemon_addr}"
|
||||
export NB_CONFIG="${CONFIG_DIR}/config.json"
|
||||
SHELLRC
|
||||
info "Added NetBird environment to ${shell_rc}"
|
||||
fi
|
||||
|
||||
# Also export for the current script so auto-connect works
|
||||
export PATH="${INSTALL_DIR}:${PATH}"
|
||||
export NB_DAEMON_ADDR="${daemon_addr}"
|
||||
export NB_CONFIG="${CONFIG_DIR}/config.json"
|
||||
}
|
||||
|
||||
# --- Install ---
|
||||
|
||||
do_install() {
|
||||
check_steamos
|
||||
check_arch
|
||||
check_dependencies
|
||||
|
||||
# Check for existing installation
|
||||
if [[ -x "${INSTALL_DIR}/${BINARY}" ]]; then
|
||||
warn "NetBird is already installed at ${INSTALL_DIR}/${BINARY}"
|
||||
warn "Use --update to update or --uninstall to remove first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
download_binary
|
||||
apply_capabilities
|
||||
verify_capabilities
|
||||
write_service_unit
|
||||
enable_service
|
||||
configure_shell_env
|
||||
|
||||
info ""
|
||||
info "NetBird installed successfully!"
|
||||
info ""
|
||||
info "The daemon is running. To connect:"
|
||||
info ""
|
||||
|
||||
if [[ -n "${NB_SETUP_KEY:-}" ]]; then
|
||||
info " Connecting with provided setup key..."
|
||||
"${INSTALL_DIR}/${BINARY}" up --setup-key "$NB_SETUP_KEY" \
|
||||
${NB_MANAGEMENT_URL:+--management-url "$NB_MANAGEMENT_URL"} \
|
||||
${NB_ADMIN_URL:+--admin-url "$NB_ADMIN_URL"} || \
|
||||
warn "Auto-connect failed. Run 'netbird up --setup-key <KEY>' manually."
|
||||
else
|
||||
info " With a setup key (recommended for Steam Deck):"
|
||||
info " netbird up --setup-key <YOUR-SETUP-KEY>"
|
||||
info ""
|
||||
info " With SSO (device flow):"
|
||||
info " netbird up"
|
||||
info " Then open the printed URL on your phone or PC."
|
||||
fi
|
||||
|
||||
info ""
|
||||
info "Check status: netbird status"
|
||||
info "View logs: journalctl --user -u ${SERVICE_NAME} -f"
|
||||
}
|
||||
|
||||
# --- Update ---
|
||||
|
||||
do_update() {
|
||||
if [[ ! -x "${INSTALL_DIR}/${BINARY}" ]]; then
|
||||
error "NetBird is not installed. Run without --update to install."
|
||||
fi
|
||||
|
||||
local installed_version
|
||||
installed_version=$("${INSTALL_DIR}/${BINARY}" version 2>/dev/null || echo "unknown")
|
||||
|
||||
local latest_version
|
||||
latest_version=$(get_release "latest")
|
||||
latest_version="${latest_version#v}"
|
||||
|
||||
if [[ "$installed_version" == "$latest_version" ]]; then
|
||||
info "Already on latest version (${installed_version})"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
info "Updating ${installed_version} -> ${latest_version}"
|
||||
|
||||
check_arch
|
||||
check_dependencies
|
||||
|
||||
# Download and verify new binary to a staging directory before touching the running service
|
||||
local staging_dir
|
||||
staging_dir=$(mktemp -d)
|
||||
trap "rm -rf '$staging_dir'" RETURN
|
||||
|
||||
download_binary "$staging_dir"
|
||||
|
||||
# Apply capabilities to the new binary before swapping
|
||||
apply_capabilities "${staging_dir}/${BINARY}"
|
||||
|
||||
# Regenerate the unit file in case paths or env vars changed
|
||||
write_service_unit
|
||||
|
||||
# Only stop the service after the new binary is ready
|
||||
systemctl --user stop "${SERVICE_NAME}.service" 2>/dev/null || true
|
||||
|
||||
# Atomic swap: move staged binary into place
|
||||
mv "${staging_dir}/${BINARY}" "${INSTALL_DIR}/${BINARY}"
|
||||
chmod 755 "${INSTALL_DIR}/${BINARY}"
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start "${SERVICE_NAME}.service"
|
||||
|
||||
info "Updated to ${latest_version}"
|
||||
}
|
||||
|
||||
# --- Uninstall ---
|
||||
|
||||
do_uninstall() {
|
||||
info "Uninstalling NetBird..."
|
||||
|
||||
# Stop and disable service
|
||||
systemctl --user stop "${SERVICE_NAME}.service" 2>/dev/null || true
|
||||
systemctl --user disable "${SERVICE_NAME}.service" 2>/dev/null || true
|
||||
|
||||
# Remove files
|
||||
rm -f "${SYSTEMD_DIR}/${SERVICE_NAME}.service"
|
||||
rm -f "${INSTALL_DIR}/${BINARY}"
|
||||
|
||||
systemctl --user daemon-reload
|
||||
|
||||
info "Removed binary and service"
|
||||
|
||||
# Ask about config/state
|
||||
if [[ -d "$CONFIG_DIR" ]] || [[ -d "$STATE_DIR" ]]; then
|
||||
info ""
|
||||
info "Config and state directories still exist:"
|
||||
[[ -d "$CONFIG_DIR" ]] && info " ${CONFIG_DIR}"
|
||||
[[ -d "$STATE_DIR" ]] && info " ${STATE_DIR}"
|
||||
info ""
|
||||
info "To remove them (this deletes auth tokens and config):"
|
||||
info " rm -rf ${CONFIG_DIR} ${STATE_DIR}"
|
||||
fi
|
||||
|
||||
info "NetBird uninstalled"
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
|
||||
main() {
|
||||
local action="${1:-}"
|
||||
case "$action" in
|
||||
--update)
|
||||
do_update
|
||||
;;
|
||||
--uninstall)
|
||||
do_uninstall
|
||||
;;
|
||||
--help|-h)
|
||||
cat <<USAGE
|
||||
NetBird installer for SteamOS (Steam Deck)
|
||||
|
||||
Usage:
|
||||
install-steamos.sh Install NetBird
|
||||
install-steamos.sh --update Update to latest version
|
||||
install-steamos.sh --uninstall Remove NetBird
|
||||
|
||||
Environment variables:
|
||||
NETBIRD_RELEASE Version to install (default: latest)
|
||||
GITHUB_TOKEN GitHub token for API rate limits
|
||||
NB_SETUP_KEY Setup key for automatic authentication
|
||||
NB_MANAGEMENT_URL Custom management server URL
|
||||
NB_ADMIN_URL Custom admin dashboard URL
|
||||
FORCE Set to "true" to install on non-SteamOS systems
|
||||
|
||||
Files:
|
||||
${INSTALL_DIR}/${BINARY} Binary
|
||||
${CONFIG_DIR}/config.json Config
|
||||
${STATE_DIR}/ State, socket, logs
|
||||
${SYSTEMD_DIR}/${SERVICE_NAME}.service Systemd unit
|
||||
USAGE
|
||||
;;
|
||||
"")
|
||||
do_install
|
||||
;;
|
||||
*)
|
||||
error "Unknown option: $action (use --help for usage)"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -138,7 +138,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
|
||||
if err != nil {
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user