Compare commits

...

32 Commits

Author SHA1 Message Date
Maycon Santos
2d350b2522 update protocol 2024-03-14 15:34:40 +01:00
Maycon Santos
d18d2db9ee use config struct 2024-03-13 15:37:56 +01:00
Maycon Santos
c3a1e1ca2c refactor function 2024-03-12 23:58:16 +01:00
Maycon Santos
c9acd2f880 Send ssh and rosenpass config meta 2024-03-12 23:44:27 +01:00
Viktor Liu
4a1aee1ae0 Add routes and dns servers to status command (#1680)
* Add routes (client and server) to status command
* Add DNS servers to status output
2024-03-12 19:06:16 +01:00
Krzysztof Nazarewski
ba33572ec9 add --service/-s flag for specifying system service name (#1691) 2024-03-12 18:29:19 +01:00
Maycon Santos
9d213e0b54 Add fallback retry to daemon (#1690)
This change adds a fallback retry to the daemon service.

this retry has a larger interval with a shorter max retry run time
then others retries
2024-03-12 18:05:41 +01:00
Maycon Santos
5dde044fa5 Check for record not found when searching the store (#1686)
This change returns status.NotFound only on gorm.ErrRecordNotFound and status.Internal on every other DB error
2024-03-10 19:09:45 +01:00
Bethuel Mmbaga
5a3d9e401f Send terminal notification on peer session expiry (#1660)
Send notification through terminal on user session expiration in Linux and macOS, 
unless UI application is installed to handle it instead.
2024-03-08 18:28:13 +01:00
Maycon Santos
fde1a2196c add ansible collections contributions (#1675) 2024-03-06 23:30:16 +01:00
Maycon Santos
0aeb87742a Return 1s when next expiration is too low (#1672)
using the login expired issue could cause problems with ticker used in the scheduler

This change makes 1s the minimum number returned when rescheduling the peer expiration task
2024-03-06 15:18:53 +01:00
Deniz Adrian
6d747b2f83 Do not fail on virtualized windows systems (#1669)
this handles virtualized systems without Win32_ComputerSystemProduct entries by returning 'unknown' for system product name

Co-authored-by: Bjoern Brauer <zaubernerd@zaubernerd.de>
2024-03-06 14:32:34 +01:00
Viktor Liu
199bf73103 Remove usage stats (#1665) 2024-03-05 09:45:32 +01:00
Maycon Santos
17f5abc653 Handle canceling schedule and avoid recursive call (#1636)
Using time.Ticker allows us to avoid recursive calls that may end up in schedule running and possible deadlock if no routine is listening for cancel calls
2024-03-03 10:35:01 +01:00
Maycon Santos
aa935bdae3 Register creation time for peer, user and account (#1654)
This change register creation time for new peers, users and accounts
2024-03-02 13:49:40 +01:00
Bethuel Mmbaga
452419c4c3 Send UI notification on peer connection session expiry (#1646)
notify the user when the peer connection session expires after it switches from connected 
to disconnected and the status is login required
2024-03-01 17:57:17 +01:00
Viktor Liu
17b1099032 Enhance DNS failover reliability (#1637)
* Fix using wrong array index in log to avoid potential panic

* Increase gRPC connection timeout and add the timeout resolv.conf option

This makes sure the dns client is able to failover to a second
configured nameserver, if present. That is the case then when using the
dns `file` manager and a resolv.conf file generated for netbird.

* On file backup restore, remove the first NS if it's the netbird NS

* Bump dns mangager discovery message from debug to info to ease debugging
2024-03-01 15:17:35 +01:00
pascal-fischer
a4b9e93217 add environment meta from grpc to store (#1651) 2024-03-01 15:15:56 +01:00
Maycon Santos
63d7957140 Log version when starting management and signal (#1649)
Output log version at service startup
2024-02-29 16:05:47 +01:00
Maycon Santos
9a6814deff Enable review linter with exported docs check (#1647)
Exported function and methods should have comments. This warns on undocumented exported functions and methods.
2024-02-29 16:05:31 +01:00
Misha Bragin
190698bcf2 Add open-source network security image (#1643) 2024-02-29 14:04:32 +00:00
pascal-fischer
468fa2940b add quotes to all timestamps in openapi spec (#1642) 2024-02-29 13:09:43 +01:00
Maycon Santos
79a0647a26 Update bug-issue-report.md label (#1640) 2024-02-29 10:21:00 +01:00
Maycon Santos
17ceb3bde8 Add reverse proxy settings to management.json template (#1639)
Add grpc_set_header to the nginx example template
2024-02-28 20:24:28 +01:00
Yury Gargay
5a8f1763a6 Implement Auth0 IdP get all connections method (#1613) 2024-02-28 16:57:35 +01:00
Bethuel Mmbaga
f64e73ca70 Fix invalid cross-device link when moving geolocation databases (#1638)
* Fix invalid cross-device link when move geonames db

* Add test for geolocation databases in workflow

This step checks the existence and proper functioning of geolocation databases, including GeoLite2-City.mmdb and Geonames.db. It will help us ensure that geolocation databases are loaded correctly in the management.

* Enable debug mode

* Increase sleep duration in geolocation tests
2024-02-28 16:42:33 +03:00
pascal-fischer
b085419ab8 FIx order when validating account settings (#1632)
* moved extraSettings validation to the end

* moved extraSettings validation directly after permission check
2024-02-27 14:17:22 +01:00
Bethuel Mmbaga
d78b652ff7 Rename PrivateNetworkCheck to PeerNetworkRangeCheck (#1629)
* Rename PrivateNetworkCheck to PeerNetworkRangeCheck

* update description and example

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2024-02-27 11:59:48 +01:00
Viktor Liu
7251150c1c Combine update-available and connected/disconnected tray icon states (#1615)
This PR updates the system tray icons to reflect both connection status and availability of updates. Now, the tray will show distinct icons for the following states: connected, disconnected, update available while connected, and update available while disconnected. This change improves user experience by providing a clear visual status indicator.

- Add new icons for connected and disconnected states with update available.
- Implement logic to switch icons based on connection status and update availability.
- Remove old icon references for default and update states.
2024-02-26 23:28:33 +01:00
Bethuel Mmbaga
b65c2f69b0 Add support for downloading Geo databases to the management service (#1626)
Adds support for downloading Geo databases to the management service. If the Geo databases are not found, the service will automatically attempt to download them during startup.
2024-02-26 22:49:28 +01:00
Yury Gargay
d8ce08d898 Extend bypass middleware with support of wildcard paths (#1628)
---------

Co-authored-by: Viktor Liu <viktor@netbird.io>
2024-02-26 17:54:58 +01:00
Maycon Santos
e1c50248d9 Add support for device flow on getting started with zitadel (#1616) 2024-02-26 12:33:16 +01:00
101 changed files with 3217 additions and 1326 deletions

View File

@@ -2,7 +2,7 @@
name: Bug/Issue report name: Bug/Issue report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: ['triage'] labels: ['triage-needed']
assignees: '' assignees: ''
--- ---

View File

@@ -11,7 +11,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
andrloid_build: android_build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -41,7 +41,7 @@ jobs:
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20230531173138-3c911d8e3eda run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20230531173138-3c911d8e3eda
- name: gomobile init - name: gomobile init
run: gomobile init run: gomobile init
- name: build android nebtird lib - name: build android netbird lib
run: PATH=$PATH:$(go env GOPATH) gomobile bind -o $GITHUB_WORKSPACE/netbird.aar -javapkg=io.netbird.gomobile -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/io.netbird.client/cache/wireguard -X github.com/netbirdio/netbird/version.version=buildtest" $GITHUB_WORKSPACE/client/android run: PATH=$PATH:$(go env GOPATH) gomobile bind -o $GITHUB_WORKSPACE/netbird.aar -javapkg=io.netbird.gomobile -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/io.netbird.client/cache/wireguard -X github.com/netbirdio/netbird/version.version=buildtest" $GITHUB_WORKSPACE/client/android
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0
@@ -59,7 +59,7 @@ jobs:
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20230531173138-3c911d8e3eda run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20230531173138-3c911d8e3eda
- name: gomobile init - name: gomobile init
run: gomobile init run: gomobile init
- name: build iOS nebtird lib - name: build iOS netbird lib
run: PATH=$PATH:$(go env GOPATH) gomobile bind -target=ios -bundleid=io.netbird.framework -ldflags="-X github.com/netbirdio/netbird/version.version=buildtest" -o $GITHUB_WORKSPACE/NetBirdSDK.xcframework $GITHUB_WORKSPACE/client/ios/NetBirdSDK run: PATH=$PATH:$(go env GOPATH) gomobile bind -target=ios -bundleid=io.netbird.framework -ldflags="-X github.com/netbirdio/netbird/version.version=buildtest" -o $GITHUB_WORKSPACE/NetBirdSDK.xcframework $GITHUB_WORKSPACE/client/ios/NetBirdSDK
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0

View File

@@ -162,6 +162,13 @@ jobs:
test $count -eq 4 test $count -eq 4
working-directory: infrastructure_files/artifacts working-directory: infrastructure_files/artifacts
- name: test geolocation databases
working-directory: infrastructure_files/artifacts
run: |
sleep 30
docker compose exec management ls -l /var/lib/netbird/ | grep -i GeoLite2-City.mmdb
docker compose exec management ls -l /var/lib/netbird/ | grep -i geonames.db
test-getting-started-script: test-getting-started-script:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -199,6 +206,6 @@ jobs:
- name: test script - name: test script
run: bash -x infrastructure_files/download-geolite2.sh run: bash -x infrastructure_files/download-geolite2.sh
- name: test mmdb file exists - name: test mmdb file exists
run: ls -l GeoLite2-City_*/GeoLite2-City.mmdb run: test -f GeoLite2-City.mmdb
- name: test geonames file exists - name: test geonames file exists
run: test -f geonames.db run: test -f geonames.db

View File

@@ -63,6 +63,14 @@ linters-settings:
enable: enable:
- nilness - nilness
revive:
rules:
- name: exported
severity: warning
disabled: false
arguments:
- "checkPrivateReceivers"
- "sayRepetitiveInsteadOfStutters"
tenv: tenv:
# The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
# Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
@@ -93,6 +101,7 @@ linters:
- nilerr # finds the code that returns nil even if it checks that the error is not nil - nilerr # finds the code that returns nil even if it checks that the error is not nil
- nilnil # checks that there is no simultaneous return of nil error and an invalid value - nilnil # checks that there is no simultaneous return of nil error and an invalid value
- predeclared # predeclared finds code that shadows one of Go's predeclared identifiers - predeclared # predeclared finds code that shadows one of Go's predeclared identifiers
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.
- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
- thelper # thelper detects Go test helpers without t.Helper() call and checks the consistency of test helpers. - thelper # thelper detects Go test helpers without t.Helper() call and checks the consistency of test helpers.
- wastedassign # wastedassign finds wasted assignment statements - wastedassign # wastedassign finds wasted assignment statements

View File

@@ -54,7 +54,7 @@ nfpms:
contents: contents:
- src: client/ui/netbird.desktop - src: client/ui/netbird.desktop
dst: /usr/share/applications/netbird.desktop dst: /usr/share/applications/netbird.desktop
- src: client/ui/netbird-systemtray-default.png - src: client/ui/netbird-systemtray-connected.png
dst: /usr/share/pixmaps/netbird.png dst: /usr/share/pixmaps/netbird.png
dependencies: dependencies:
- netbird - netbird
@@ -71,7 +71,7 @@ nfpms:
contents: contents:
- src: client/ui/netbird.desktop - src: client/ui/netbird.desktop
dst: /usr/share/applications/netbird.desktop dst: /usr/share/applications/netbird.desktop
- src: client/ui/netbird-systemtray-default.png - src: client/ui/netbird-systemtray-connected.png
dst: /usr/share/pixmaps/netbird.png dst: /usr/share/pixmaps/netbird.png
dependencies: dependencies:
- netbird - netbird

View File

@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<strong>:hatching_chick: New Release! Self-hosting in under 5 min.</strong> <strong>:hatching_chick: New Release! Device Posture Checks.</strong>
<a href="https://github.com/netbirdio/netbird#quickstart-with-self-hosted-netbird"> <a href="https://docs.netbird.io/how-to/manage-posture-checks">
Learn more Learn more
</a> </a>
</p> </p>
@@ -42,25 +42,22 @@
**Secure.** NetBird enables secure remote access by applying granular access policies, while allowing you to manage them intuitively from a single place. Works universally on any infrastructure. **Secure.** NetBird enables secure remote access by applying granular access policies, while allowing you to manage them intuitively from a single place. Works universally on any infrastructure.
### Secure peer-to-peer VPN with SSO and MFA in minutes ### Open-Source Network Security in a Single Platform
https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov ![download (2)](https://github.com/netbirdio/netbird/assets/700848/16210ac2-7265-44c1-8d4e-8fae85534dac)
### Key features ### Key features
| Connectivity | Management | Automation | Platforms | | Connectivity | Management | Security | Automation | Platforms |
|---------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|----------------------------------------------------------------------------|---------------------------------------| |------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
| <ul><li> - \[x] Kernel WireGuard </ul></li> | <ul><li> - \[x] [Admin Web UI](https://github.com/netbirdio/dashboard) </ul></li> | <ul><li> - \[x] [Public API](https://docs.netbird.io/api) </ul></li> | <ul><li> - \[x] Linux </ul></li> | | <ul><li> - \[x] Kernel WireGuard </ul></li> | <ul><li> - \[x] [Admin Web UI](https://github.com/netbirdio/dashboard) </ul></li> | <ul><li> - \[x] [SSO & MFA support](https://docs.netbird.io/how-to/installation#running-net-bird-with-sso-login) </ul></li> | <ul><li> - \[x] [Public API](https://docs.netbird.io/api) </ul></li> | <ul><li> - \[x] Linux </ul></li> |
| <ul><li> - \[x] Peer-to-peer connections </ul></li> | <ul><li> - \[x] Auto peer discovery and configuration </ul></li> | <ul><li> - \[x] [Setup keys for bulk network provisioning](https://docs.netbird.io/how-to/register-machines-using-setup-keys) </ul></li> | <ul><li> - \[x] Mac </ul></li> | | <ul><li> - \[x] Peer-to-peer connections </ul></li> | <ul><li> - \[x] Auto peer discovery and configuration </ul></li> | <ul><li> - \[x] [Access control - groups & rules](https://docs.netbird.io/how-to/manage-network-access) </ul></li> | <ul><li> - \[x] [Setup keys for bulk network provisioning](https://docs.netbird.io/how-to/register-machines-using-setup-keys) </ul></li> | <ul><li> - \[x] Mac </ul></li> |
| <ul><li> - \[x] Peer-to-peer encryption </ul></li> | <ul><li> - \[x] [IdP integrations](https://docs.netbird.io/selfhosted/identity-providers) </ul></li> | <ul><li> - \[x] [Self-hosting quickstart script](https://docs.netbird.io/selfhosted/selfhosted-quickstart) </ul></li> | <ul><li> - \[x] Windows </ul></li> | | <ul><li> - \[x] Connection relay fallback </ul></li> | <ul><li> - \[x] [IdP integrations](https://docs.netbird.io/selfhosted/identity-providers) </ul></li> | <ul><li> - \[x] [Activity logging](https://docs.netbird.io/how-to/monitor-system-and-network-activity) </ul></li> | <ul><li> - \[x] [Self-hosting quickstart script](https://docs.netbird.io/selfhosted/selfhosted-quickstart) </ul></li> | <ul><li> - \[x] Windows </ul></li> |
| <ul><li> - \[x] Connection relay fallback </ul></li> | <ul><li> - \[x] [SSO & MFA support](https://docs.netbird.io/how-to/installation#running-net-bird-with-sso-login) </ul></li> | <ul><li> - \[x] IdP groups sync with JWT </ul></li> | <ul><li> - \[x] Android </ul></li> | | <ul><li> - \[x] [Routes to external networks](https://docs.netbird.io/how-to/routing-traffic-to-private-networks) </ul></li> | <ul><li> - \[x] [Private DNS](https://docs.netbird.io/how-to/manage-dns-in-your-network) </ul></li> | <ul><li> - \[x] [Device posture checks](https://docs.netbird.io/how-to/manage-posture-checks) </ul></li> | <ul><li> - \[x] IdP groups sync with JWT </ul></li> | <ul><li> - \[x] Android </ul></li> |
| <ul><li> - \[x] [Routes to external networks](https://docs.netbird.io/how-to/routing-traffic-to-private-networks) </ul></li> | <ul><li> - \[x] [Access control - groups & rules](https://docs.netbird.io/how-to/manage-network-access) </ul></li> | | <ul><li> - \[x] iOS </ul></li> | | <ul><li> - \[x] NAT traversal with BPF </ul></li> | <ul><li> - \[x] [Multiuser support](https://docs.netbird.io/how-to/add-users-to-your-network) </ul></li> | <ul><li> - \[x] Peer-to-peer encryption </ul></li> | | <ul><li> - \[x] iOS </ul></li> |
| <ul><li> - \[x] NAT traversal with BPF </ul></li> | <ul><li> - \[x] [Private DNS](https://docs.netbird.io/how-to/manage-dns-in-your-network) </ul></li> | | <ul><li> - \[x] Docker </ul></li> | | | | <ul><li> - \[x] [Quantum-resistance with Rosenpass](https://netbird.io/knowledge-hub/the-first-quantum-resistant-mesh-vpn) </ul></li> | | <ul><li> - \[x] OpenWRT </ul></li> |
| <ul><li> - \[x] Post-quantum-secure connection through [Rosenpass](https://rosenpass.eu) </ul></li> | <ul><li> - \[x] [Multiuser support](https://docs.netbird.io/how-to/add-users-to-your-network) </ul></li> | | <ul><li> - \[x] OpenWRT </ul></li> | | | | <ui><li> - \[x] [Periodic re-authentication](https://docs.netbird.io/how-to/enforce-periodic-user-authentication)</ul></li> | | <ul><li> - \[x] [Serverless](https://docs.netbird.io/how-to/netbird-on-faas) </ul></li> |
| | <ul><li> - \[x] [Activity logging](https://docs.netbird.io/how-to/monitor-system-and-network-activity) </ul></li> | | | | | | | | <ul><li> - \[x] Docker </ul></li> |
| | <ul><li> - \[x] SSH access management </ul></li> | | |
### Quickstart with NetBird Cloud ### Quickstart with NetBird Cloud
- Download and install NetBird at [https://app.netbird.io/install](https://app.netbird.io/install) - Download and install NetBird at [https://app.netbird.io/install](https://app.netbird.io/install)
@@ -109,8 +106,8 @@ export NETBIRD_DOMAIN=netbird.example.com; curl -fsSL https://github.com/netbird
See a complete [architecture overview](https://docs.netbird.io/about-netbird/how-netbird-works#architecture) for details. See a complete [architecture overview](https://docs.netbird.io/about-netbird/how-netbird-works#architecture) for details.
### Community projects ### Community projects
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird)
- [NetBird installer script](https://github.com/physk/netbird-installer) - [NetBird installer script](https://github.com/physk/netbird-installer)
- [NetBird ansible collection by Dominion Solutions](https://galaxy.ansible.com/ui/repo/published/dominion_solutions/netbird/)
**Note**: The `main` branch may be in an *unstable or even broken state* during development. **Note**: The `main` branch may be in an *unstable or even broken state* during development.
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases). For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).

View File

@@ -61,6 +61,7 @@ var (
serverSSHAllowed bool serverSSHAllowed bool
interfaceName string interfaceName string
wireguardPort uint16 wireguardPort uint16
serviceName string
autoConnectDisabled bool autoConnectDisabled bool
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "netbird", Use: "netbird",
@@ -100,9 +101,16 @@ func init() {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
defaultDaemonAddr = "tcp://127.0.0.1:41731" defaultDaemonAddr = "tcp://127.0.0.1:41731"
} }
defaultServiceName := "netbird"
if runtime.GOOS == "windows" {
defaultServiceName = "Netbird"
}
rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]") rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL)) rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL))
rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL)) rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL))
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location") rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location")
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level") rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level")
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout") rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout")

View File

@@ -2,8 +2,6 @@ package cmd
import ( import (
"context" "context"
"runtime"
"github.com/kardianos/service" "github.com/kardianos/service"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -24,12 +22,8 @@ func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
} }
func newSVCConfig() *service.Config { func newSVCConfig() *service.Config {
name := "netbird"
if runtime.GOOS == "windows" {
name = "Netbird"
}
return &service.Config{ return &service.Config{
Name: name, Name: serviceName,
DisplayName: "Netbird", DisplayName: "Netbird",
Description: "A WireGuard-based mesh network that connects your devices into a single private network.", Description: "A WireGuard-based mesh network that connects your devices into a single private network.",
Option: make(service.KeyValue), Option: make(service.KeyValue),

View File

@@ -35,6 +35,7 @@ type peerStateDetailOutput struct {
TransferReceived int64 `json:"transferReceived" yaml:"transferReceived"` TransferReceived int64 `json:"transferReceived" yaml:"transferReceived"`
TransferSent int64 `json:"transferSent" yaml:"transferSent"` TransferSent int64 `json:"transferSent" yaml:"transferSent"`
RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"`
Routes []string `json:"routes" yaml:"routes"`
} }
type peersStateOutput struct { type peersStateOutput struct {
@@ -72,19 +73,28 @@ type iceCandidateType struct {
Remote string `json:"remote" yaml:"remote"` Remote string `json:"remote" yaml:"remote"`
} }
type nsServerGroupStateOutput struct {
Servers []string `json:"servers" yaml:"servers"`
Domains []string `json:"domains" yaml:"domains"`
Enabled bool `json:"enabled" yaml:"enabled"`
Error string `json:"error" yaml:"error"`
}
type statusOutputOverview struct { type statusOutputOverview struct {
Peers peersStateOutput `json:"peers" yaml:"peers"` Peers peersStateOutput `json:"peers" yaml:"peers"`
CliVersion string `json:"cliVersion" yaml:"cliVersion"` CliVersion string `json:"cliVersion" yaml:"cliVersion"`
DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"` DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"`
ManagementState managementStateOutput `json:"management" yaml:"management"` ManagementState managementStateOutput `json:"management" yaml:"management"`
SignalState signalStateOutput `json:"signal" yaml:"signal"` SignalState signalStateOutput `json:"signal" yaml:"signal"`
Relays relayStateOutput `json:"relays" yaml:"relays"` Relays relayStateOutput `json:"relays" yaml:"relays"`
IP string `json:"netbirdIp" yaml:"netbirdIp"` IP string `json:"netbirdIp" yaml:"netbirdIp"`
PubKey string `json:"publicKey" yaml:"publicKey"` PubKey string `json:"publicKey" yaml:"publicKey"`
KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"` KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"`
FQDN string `json:"fqdn" yaml:"fqdn"` FQDN string `json:"fqdn" yaml:"fqdn"`
RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"` RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"`
RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"` RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"`
Routes []string `json:"routes" yaml:"routes"`
NSServerGroups []nsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"`
} }
var ( var (
@@ -168,7 +178,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
case yamlFlag: case yamlFlag:
statusOutputString, err = parseToYAML(outputInformationHolder) statusOutputString, err = parseToYAML(outputInformationHolder)
default: default:
statusOutputString = parseGeneralSummary(outputInformationHolder, false, false) statusOutputString = parseGeneralSummary(outputInformationHolder, false, false, false)
} }
if err != nil { if err != nil {
@@ -268,6 +278,8 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv
FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(), FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(),
RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(), RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(),
RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(), RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(),
Routes: pbFullStatus.GetLocalPeerState().GetRoutes(),
NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()),
} }
return overview return overview
@@ -299,6 +311,19 @@ func mapRelays(relays []*proto.RelayState) relayStateOutput {
} }
} }
func mapNSGroups(servers []*proto.NSGroupState) []nsServerGroupStateOutput {
mappedNSGroups := make([]nsServerGroupStateOutput, 0, len(servers))
for _, pbNsGroupServer := range servers {
mappedNSGroups = append(mappedNSGroups, nsServerGroupStateOutput{
Servers: pbNsGroupServer.GetServers(),
Domains: pbNsGroupServer.GetDomains(),
Enabled: pbNsGroupServer.GetEnabled(),
Error: pbNsGroupServer.GetError(),
})
}
return mappedNSGroups
}
func mapPeers(peers []*proto.PeerState) peersStateOutput { func mapPeers(peers []*proto.PeerState) peersStateOutput {
var peersStateDetail []peerStateDetailOutput var peersStateDetail []peerStateDetailOutput
localICE := "" localICE := ""
@@ -352,6 +377,7 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput {
TransferReceived: transferReceived, TransferReceived: transferReceived,
TransferSent: transferSent, TransferSent: transferSent,
RosenpassEnabled: pbPeerState.GetRosenpassEnabled(), RosenpassEnabled: pbPeerState.GetRosenpassEnabled(),
Routes: pbPeerState.GetRoutes(),
} }
peersStateDetail = append(peersStateDetail, peerState) peersStateDetail = append(peersStateDetail, peerState)
@@ -401,8 +427,7 @@ func parseToYAML(overview statusOutputOverview) (string, error) {
return string(yamlBytes), nil return string(yamlBytes), nil
} }
func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays bool) string { func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays bool, showNameServers bool) string {
var managementConnString string var managementConnString string
if overview.ManagementState.Connected { if overview.ManagementState.Connected {
managementConnString = "Connected" managementConnString = "Connected"
@@ -438,7 +463,7 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
interfaceIP = "N/A" interfaceIP = "N/A"
} }
var relayAvailableString string var relaysString string
if showRelays { if showRelays {
for _, relay := range overview.Relays.Details { for _, relay := range overview.Relays.Details {
available := "Available" available := "Available"
@@ -447,15 +472,46 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
available = "Unavailable" available = "Unavailable"
reason = fmt.Sprintf(", reason: %s", relay.Error) reason = fmt.Sprintf(", reason: %s", relay.Error)
} }
relayAvailableString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason) relaysString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason)
} }
} else { } else {
relaysString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total)
relayAvailableString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total)
} }
peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total) routes := "-"
if len(overview.Routes) > 0 {
sort.Strings(overview.Routes)
routes = strings.Join(overview.Routes, ", ")
}
var dnsServersString string
if showNameServers {
for _, nsServerGroup := range overview.NSServerGroups {
enabled := "Available"
if !nsServerGroup.Enabled {
enabled = "Unavailable"
}
errorString := ""
if nsServerGroup.Error != "" {
errorString = fmt.Sprintf(", reason: %s", nsServerGroup.Error)
errorString = strings.TrimSpace(errorString)
}
domainsString := strings.Join(nsServerGroup.Domains, ", ")
if domainsString == "" {
domainsString = "." // Show "." for the default zone
}
dnsServersString += fmt.Sprintf(
"\n [%s] for [%s] is %s%s",
strings.Join(nsServerGroup.Servers, ", "),
domainsString,
enabled,
errorString,
)
}
} else {
dnsServersString = fmt.Sprintf("%d/%d Available", countEnabled(overview.NSServerGroups), len(overview.NSServerGroups))
}
rosenpassEnabledStatus := "false" rosenpassEnabledStatus := "false"
if overview.RosenpassEnabled { if overview.RosenpassEnabled {
@@ -465,26 +521,32 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
} }
} }
peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total)
summary := fmt.Sprintf( summary := fmt.Sprintf(
"Daemon version: %s\n"+ "Daemon version: %s\n"+
"CLI version: %s\n"+ "CLI version: %s\n"+
"Management: %s\n"+ "Management: %s\n"+
"Signal: %s\n"+ "Signal: %s\n"+
"Relays: %s\n"+ "Relays: %s\n"+
"Nameservers: %s\n"+
"FQDN: %s\n"+ "FQDN: %s\n"+
"NetBird IP: %s\n"+ "NetBird IP: %s\n"+
"Interface type: %s\n"+ "Interface type: %s\n"+
"Quantum resistance: %s\n"+ "Quantum resistance: %s\n"+
"Routes: %s\n"+
"Peers count: %s\n", "Peers count: %s\n",
overview.DaemonVersion, overview.DaemonVersion,
version.NetbirdVersion(), version.NetbirdVersion(),
managementConnString, managementConnString,
signalConnString, signalConnString,
relayAvailableString, relaysString,
dnsServersString,
overview.FQDN, overview.FQDN,
interfaceIP, interfaceIP,
interfaceTypeString, interfaceTypeString,
rosenpassEnabledStatus, rosenpassEnabledStatus,
routes,
peersCountString, peersCountString,
) )
return summary return summary
@@ -492,7 +554,7 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays
func parseToFullDetailSummary(overview statusOutputOverview) string { func parseToFullDetailSummary(overview statusOutputOverview) string {
parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive) parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive)
summary := parseGeneralSummary(overview, true, true) summary := parseGeneralSummary(overview, true, true, true)
return fmt.Sprintf( return fmt.Sprintf(
"Peers detail:"+ "Peers detail:"+
@@ -556,6 +618,12 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
} }
} }
routes := "-"
if len(peerState.Routes) > 0 {
sort.Strings(peerState.Routes)
routes = strings.Join(peerState.Routes, ", ")
}
peerString := fmt.Sprintf( peerString := fmt.Sprintf(
"\n %s:\n"+ "\n %s:\n"+
" NetBird IP: %s\n"+ " NetBird IP: %s\n"+
@@ -569,7 +637,8 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
" Last connection update: %s\n"+ " Last connection update: %s\n"+
" Last WireGuard handshake: %s\n"+ " Last WireGuard handshake: %s\n"+
" Transfer status (received/sent) %s/%s\n"+ " Transfer status (received/sent) %s/%s\n"+
" Quantum resistance: %s\n", " Quantum resistance: %s\n"+
" Routes: %s\n",
peerState.FQDN, peerState.FQDN,
peerState.IP, peerState.IP,
peerState.PubKey, peerState.PubKey,
@@ -585,6 +654,7 @@ func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bo
toIEC(peerState.TransferReceived), toIEC(peerState.TransferReceived),
toIEC(peerState.TransferSent), toIEC(peerState.TransferSent),
rosenpassEnabledStatus, rosenpassEnabledStatus,
routes,
) )
peersString += peerString peersString += peerString
@@ -638,3 +708,13 @@ func toIEC(b int64) string {
return fmt.Sprintf("%.1f %ciB", return fmt.Sprintf("%.1f %ciB",
float64(b)/float64(div), "KMGTPE"[exp]) float64(b)/float64(div), "KMGTPE"[exp])
} }
func countEnabled(dnsServers []nsServerGroupStateOutput) int {
count := 0
for _, server := range dnsServers {
if server.Enabled {
count++
}
}
return count
}

View File

@@ -42,6 +42,9 @@ var resp = &proto.StatusResponse{
LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)), LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)),
BytesRx: 200, BytesRx: 200,
BytesTx: 100, BytesTx: 100,
Routes: []string{
"10.1.0.0/24",
},
}, },
{ {
IP: "192.168.178.102", IP: "192.168.178.102",
@@ -87,6 +90,31 @@ var resp = &proto.StatusResponse{
PubKey: "Some-Pub-Key", PubKey: "Some-Pub-Key",
KernelInterface: true, KernelInterface: true,
Fqdn: "some-localhost.awesome-domain.com", Fqdn: "some-localhost.awesome-domain.com",
Routes: []string{
"10.10.0.0/24",
},
},
DnsServers: []*proto.NSGroupState{
{
Servers: []string{
"8.8.8.8:53",
},
Domains: nil,
Enabled: true,
Error: "",
},
{
Servers: []string{
"1.1.1.1:53",
"2.2.2.2:53",
},
Domains: []string{
"example.com",
"example.net",
},
Enabled: false,
Error: "timeout",
},
}, },
}, },
DaemonVersion: "0.14.1", DaemonVersion: "0.14.1",
@@ -116,6 +144,9 @@ var overview = statusOutputOverview{
LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC), LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC),
TransferReceived: 200, TransferReceived: 200,
TransferSent: 100, TransferSent: 100,
Routes: []string{
"10.1.0.0/24",
},
}, },
{ {
IP: "192.168.178.102", IP: "192.168.178.102",
@@ -171,6 +202,31 @@ var overview = statusOutputOverview{
PubKey: "Some-Pub-Key", PubKey: "Some-Pub-Key",
KernelInterface: true, KernelInterface: true,
FQDN: "some-localhost.awesome-domain.com", FQDN: "some-localhost.awesome-domain.com",
NSServerGroups: []nsServerGroupStateOutput{
{
Servers: []string{
"8.8.8.8:53",
},
Domains: nil,
Enabled: true,
Error: "",
},
{
Servers: []string{
"1.1.1.1:53",
"2.2.2.2:53",
},
Domains: []string{
"example.com",
"example.net",
},
Enabled: false,
Error: "timeout",
},
},
Routes: []string{
"10.10.0.0/24",
},
} }
func TestConversionFromFullStatusToOutputOverview(t *testing.T) { func TestConversionFromFullStatusToOutputOverview(t *testing.T) {
@@ -232,7 +288,10 @@ func TestParsingToJSON(t *testing.T) {
"lastWireguardHandshake": "2001-01-01T01:01:02Z", "lastWireguardHandshake": "2001-01-01T01:01:02Z",
"transferReceived": 200, "transferReceived": 200,
"transferSent": 100, "transferSent": 100,
"quantumResistance":false "quantumResistance": false,
"routes": [
"10.1.0.0/24"
]
}, },
{ {
"fqdn": "peer-2.awesome-domain.com", "fqdn": "peer-2.awesome-domain.com",
@@ -253,7 +312,8 @@ func TestParsingToJSON(t *testing.T) {
"lastWireguardHandshake": "2002-02-02T02:02:03Z", "lastWireguardHandshake": "2002-02-02T02:02:03Z",
"transferReceived": 2000, "transferReceived": 2000,
"transferSent": 1000, "transferSent": 1000,
"quantumResistance":false "quantumResistance": false,
"routes": null
} }
] ]
}, },
@@ -289,8 +349,33 @@ func TestParsingToJSON(t *testing.T) {
"publicKey": "Some-Pub-Key", "publicKey": "Some-Pub-Key",
"usesKernelInterface": true, "usesKernelInterface": true,
"fqdn": "some-localhost.awesome-domain.com", "fqdn": "some-localhost.awesome-domain.com",
"quantumResistance":false, "quantumResistance": false,
"quantumResistancePermissive":false "quantumResistancePermissive": false,
"routes": [
"10.10.0.0/24"
],
"dnsServers": [
{
"servers": [
"8.8.8.8:53"
],
"domains": null,
"enabled": true,
"error": ""
},
{
"servers": [
"1.1.1.1:53",
"2.2.2.2:53"
],
"domains": [
"example.com",
"example.net"
],
"enabled": false,
"error": "timeout"
}
]
}` }`
// @formatter:on // @formatter:on
@@ -325,6 +410,8 @@ func TestParsingToYAML(t *testing.T) {
transferReceived: 200 transferReceived: 200
transferSent: 100 transferSent: 100
quantumResistance: false quantumResistance: false
routes:
- 10.1.0.0/24
- fqdn: peer-2.awesome-domain.com - fqdn: peer-2.awesome-domain.com
netbirdIp: 192.168.178.102 netbirdIp: 192.168.178.102
publicKey: Pubkey2 publicKey: Pubkey2
@@ -342,6 +429,7 @@ func TestParsingToYAML(t *testing.T) {
transferReceived: 2000 transferReceived: 2000
transferSent: 1000 transferSent: 1000
quantumResistance: false quantumResistance: false
routes: []
cliVersion: development cliVersion: development
daemonVersion: 0.14.1 daemonVersion: 0.14.1
management: management:
@@ -368,6 +456,22 @@ usesKernelInterface: true
fqdn: some-localhost.awesome-domain.com fqdn: some-localhost.awesome-domain.com
quantumResistance: false quantumResistance: false
quantumResistancePermissive: false quantumResistancePermissive: false
routes:
- 10.10.0.0/24
dnsServers:
- servers:
- 8.8.8.8:53
domains: []
enabled: true
error: ""
- servers:
- 1.1.1.1:53
- 2.2.2.2:53
domains:
- example.com
- example.net
enabled: false
error: timeout
` `
assert.Equal(t, expectedYAML, yaml) assert.Equal(t, expectedYAML, yaml)
@@ -391,6 +495,7 @@ func TestParsingToDetail(t *testing.T) {
Last WireGuard handshake: 2001-01-01 01:01:02 Last WireGuard handshake: 2001-01-01 01:01:02
Transfer status (received/sent) 200 B/100 B Transfer status (received/sent) 200 B/100 B
Quantum resistance: false Quantum resistance: false
Routes: 10.1.0.0/24
peer-2.awesome-domain.com: peer-2.awesome-domain.com:
NetBird IP: 192.168.178.102 NetBird IP: 192.168.178.102
@@ -405,6 +510,7 @@ func TestParsingToDetail(t *testing.T) {
Last WireGuard handshake: 2002-02-02 02:02:03 Last WireGuard handshake: 2002-02-02 02:02:03
Transfer status (received/sent) 2.0 KiB/1000 B Transfer status (received/sent) 2.0 KiB/1000 B
Quantum resistance: false Quantum resistance: false
Routes: -
Daemon version: 0.14.1 Daemon version: 0.14.1
CLI version: development CLI version: development
@@ -413,10 +519,14 @@ Signal: Connected to my-awesome-signal.com:443
Relays: Relays:
[stun:my-awesome-stun.com:3478] is Available [stun:my-awesome-stun.com:3478] is Available
[turns:my-awesome-turn.com:443?transport=tcp] is Unavailable, reason: context: deadline exceeded [turns:my-awesome-turn.com:443?transport=tcp] is Unavailable, reason: context: deadline exceeded
Nameservers:
[8.8.8.8:53] for [.] is Available
[1.1.1.1:53, 2.2.2.2:53] for [example.com, example.net] is Unavailable, reason: timeout
FQDN: some-localhost.awesome-domain.com FQDN: some-localhost.awesome-domain.com
NetBird IP: 192.168.178.100/16 NetBird IP: 192.168.178.100/16
Interface type: Kernel Interface type: Kernel
Quantum resistance: false Quantum resistance: false
Routes: 10.10.0.0/24
Peers count: 2/2 Connected Peers count: 2/2 Connected
` `
@@ -424,7 +534,7 @@ Peers count: 2/2 Connected
} }
func TestParsingToShortVersion(t *testing.T) { func TestParsingToShortVersion(t *testing.T) {
shortVersion := parseGeneralSummary(overview, false, false) shortVersion := parseGeneralSummary(overview, false, false, false)
expectedString := expectedString :=
`Daemon version: 0.14.1 `Daemon version: 0.14.1
@@ -432,10 +542,12 @@ CLI version: development
Management: Connected Management: Connected
Signal: Connected Signal: Connected
Relays: 1/2 Available Relays: 1/2 Available
Nameservers: 1/2 Available
FQDN: some-localhost.awesome-domain.com FQDN: some-localhost.awesome-domain.com
NetBird IP: 192.168.178.100/16 NetBird IP: 192.168.178.100/16
Interface type: Kernel Interface type: Kernel
Quantum resistance: false Quantum resistance: false
Routes: 10.10.0.0/24
Peers count: 2/2 Connected Peers count: 2/2 Connected
` `

View File

@@ -26,7 +26,7 @@ type HTTPClient interface {
} }
// AuthFlowInfo holds information for the OAuth 2.0 authorization flow // AuthFlowInfo holds information for the OAuth 2.0 authorization flow
type AuthFlowInfo struct { type AuthFlowInfo struct { //nolint:revive
DeviceCode string `json:"device_code"` DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"` UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"` VerificationURI string `json:"verification_uri"`

View File

@@ -8,6 +8,7 @@ import (
"net/netip" "net/netip"
"os" "os"
"strings" "strings"
"time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -23,10 +24,16 @@ const (
fileMaxNumberOfSearchDomains = 6 fileMaxNumberOfSearchDomains = 6
) )
const (
dnsFailoverTimeout = 4 * time.Second
dnsFailoverAttempts = 1
)
type fileConfigurator struct { type fileConfigurator struct {
repair *repair repair *repair
originalPerms os.FileMode originalPerms os.FileMode
nbNameserverIP string
} }
func newFileConfigurator() (hostManager, error) { func newFileConfigurator() (hostManager, error) {
@@ -64,7 +71,7 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig) error {
} }
nbSearchDomains := searchDomains(config) nbSearchDomains := searchDomains(config)
nbNameserverIP := config.ServerIP f.nbNameserverIP = config.ServerIP
resolvConf, err := parseBackupResolvConf() resolvConf, err := parseBackupResolvConf()
if err != nil { if err != nil {
@@ -73,11 +80,11 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig) error {
f.repair.stopWatchFileChanges() f.repair.stopWatchFileChanges()
err = f.updateConfig(nbSearchDomains, nbNameserverIP, resolvConf) err = f.updateConfig(nbSearchDomains, f.nbNameserverIP, resolvConf)
if err != nil { if err != nil {
return err return err
} }
f.repair.watchFileChanges(nbSearchDomains, nbNameserverIP) f.repair.watchFileChanges(nbSearchDomains, f.nbNameserverIP)
return nil return nil
} }
@@ -85,10 +92,11 @@ func (f *fileConfigurator) updateConfig(nbSearchDomains []string, nbNameserverIP
searchDomainList := mergeSearchDomains(nbSearchDomains, cfg.searchDomains) searchDomainList := mergeSearchDomains(nbSearchDomains, cfg.searchDomains)
nameServers := generateNsList(nbNameserverIP, cfg) nameServers := generateNsList(nbNameserverIP, cfg)
options := prepareOptionsWithTimeout(cfg.others, int(dnsFailoverTimeout.Seconds()), dnsFailoverAttempts)
buf := prepareResolvConfContent( buf := prepareResolvConfContent(
searchDomainList, searchDomainList,
nameServers, nameServers,
cfg.others) options)
log.Debugf("creating managed file %s", defaultResolvConfPath) log.Debugf("creating managed file %s", defaultResolvConfPath)
err := os.WriteFile(defaultResolvConfPath, buf.Bytes(), f.originalPerms) err := os.WriteFile(defaultResolvConfPath, buf.Bytes(), f.originalPerms)
@@ -131,7 +139,12 @@ func (f *fileConfigurator) backup() error {
} }
func (f *fileConfigurator) restore() error { func (f *fileConfigurator) restore() error {
err := copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath) err := removeFirstNbNameserver(fileDefaultResolvConfBackupLocation, f.nbNameserverIP)
if err != nil {
log.Errorf("Failed to remove netbird nameserver from %s on backup restore: %s", fileDefaultResolvConfBackupLocation, err)
}
err = copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath)
if err != nil { if err != nil {
return fmt.Errorf("restoring %s from %s: %w", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err) return fmt.Errorf("restoring %s from %s: %w", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err)
} }
@@ -157,7 +170,7 @@ func (f *fileConfigurator) restoreUncleanShutdownDNS(storedDNSAddress *netip.Add
currentDNSAddress, err := netip.ParseAddr(resolvConf.nameServers[0]) currentDNSAddress, err := netip.ParseAddr(resolvConf.nameServers[0])
// not a valid first nameserver -> restore // not a valid first nameserver -> restore
if err != nil { if err != nil {
log.Errorf("restoring unclean shutdown: parse dns address %s failed: %s", resolvConf.nameServers[1], err) log.Errorf("restoring unclean shutdown: parse dns address %s failed: %s", resolvConf.nameServers[0], err)
return restoreResolvConfFile() return restoreResolvConfFile()
} }

View File

@@ -5,6 +5,7 @@ package dns
import ( import (
"fmt" "fmt"
"os" "os"
"regexp"
"strings" "strings"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -14,6 +15,9 @@ const (
defaultResolvConfPath = "/etc/resolv.conf" defaultResolvConfPath = "/etc/resolv.conf"
) )
var timeoutRegex = regexp.MustCompile(`timeout:\d+`)
var attemptsRegex = regexp.MustCompile(`attempts:\d+`)
type resolvConf struct { type resolvConf struct {
nameServers []string nameServers []string
searchDomains []string searchDomains []string
@@ -103,3 +107,62 @@ func parseResolvConfFile(resolvConfFile string) (*resolvConf, error) {
} }
return rconf, nil return rconf, nil
} }
// prepareOptionsWithTimeout appends timeout to existing options if it doesn't exist,
// otherwise it adds a new option with timeout and attempts.
func prepareOptionsWithTimeout(input []string, timeout int, attempts int) []string {
configs := make([]string, len(input))
copy(configs, input)
for i, config := range configs {
if strings.HasPrefix(config, "options") {
config = strings.ReplaceAll(config, "rotate", "")
config = strings.Join(strings.Fields(config), " ")
if strings.Contains(config, "timeout:") {
config = timeoutRegex.ReplaceAllString(config, fmt.Sprintf("timeout:%d", timeout))
} else {
config = strings.Replace(config, "options ", fmt.Sprintf("options timeout:%d ", timeout), 1)
}
if strings.Contains(config, "attempts:") {
config = attemptsRegex.ReplaceAllString(config, fmt.Sprintf("attempts:%d", attempts))
} else {
config = strings.Replace(config, "options ", fmt.Sprintf("options attempts:%d ", attempts), 1)
}
configs[i] = config
return configs
}
}
return append(configs, fmt.Sprintf("options timeout:%d attempts:%d", timeout, attempts))
}
// removeFirstNbNameserver removes the given nameserver from the given file if it is in the first position
// and writes the file back to the original location
func removeFirstNbNameserver(filename, nameserverIP string) error {
resolvConf, err := parseResolvConfFile(filename)
if err != nil {
return fmt.Errorf("parse backup resolv.conf: %w", err)
}
content, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("read %s: %w", filename, err)
}
if len(resolvConf.nameServers) > 1 && resolvConf.nameServers[0] == nameserverIP {
newContent := strings.Replace(string(content), fmt.Sprintf("nameserver %s\n", nameserverIP), "", 1)
stat, err := os.Stat(filename)
if err != nil {
return fmt.Errorf("stat %s: %w", filename, err)
}
if err := os.WriteFile(filename, []byte(newContent), stat.Mode()); err != nil {
return fmt.Errorf("write %s: %w", filename, err)
}
}
return nil
}

View File

@@ -6,6 +6,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func Test_parseResolvConf(t *testing.T) { func Test_parseResolvConf(t *testing.T) {
@@ -172,3 +174,131 @@ nameserver 192.168.0.1
t.Errorf("unexpected resolv.conf content: %v", cfg) t.Errorf("unexpected resolv.conf content: %v", cfg)
} }
} }
func TestPrepareOptionsWithTimeout(t *testing.T) {
tests := []struct {
name string
others []string
timeout int
attempts int
expected []string
}{
{
name: "Append new options with timeout and attempts",
others: []string{"some config"},
timeout: 2,
attempts: 2,
expected: []string{"some config", "options timeout:2 attempts:2"},
},
{
name: "Modify existing options to exclude rotate and include timeout and attempts",
others: []string{"some config", "options rotate someother"},
timeout: 3,
attempts: 2,
expected: []string{"some config", "options attempts:2 timeout:3 someother"},
},
{
name: "Existing options with timeout and attempts are updated",
others: []string{"some config", "options timeout:4 attempts:3"},
timeout: 5,
attempts: 4,
expected: []string{"some config", "options timeout:5 attempts:4"},
},
{
name: "Modify existing options, add missing attempts before timeout",
others: []string{"some config", "options timeout:4"},
timeout: 4,
attempts: 3,
expected: []string{"some config", "options attempts:3 timeout:4"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := prepareOptionsWithTimeout(tc.others, tc.timeout, tc.attempts)
assert.Equal(t, tc.expected, result)
})
}
}
func TestRemoveFirstNbNameserver(t *testing.T) {
testCases := []struct {
name string
content string
ipToRemove string
expected string
}{
{
name: "Unrelated nameservers with comments and options",
content: `# This is a comment
options rotate
nameserver 1.1.1.1
# Another comment
nameserver 8.8.4.4
search example.com`,
ipToRemove: "9.9.9.9",
expected: `# This is a comment
options rotate
nameserver 1.1.1.1
# Another comment
nameserver 8.8.4.4
search example.com`,
},
{
name: "First nameserver matches",
content: `search example.com
nameserver 9.9.9.9
# oof, a comment
nameserver 8.8.4.4
options attempts:5`,
ipToRemove: "9.9.9.9",
expected: `search example.com
# oof, a comment
nameserver 8.8.4.4
options attempts:5`,
},
{
name: "Target IP not the first nameserver",
// nolint:dupword
content: `# Comment about the first nameserver
nameserver 8.8.4.4
# Comment before our target
nameserver 9.9.9.9
options timeout:2`,
ipToRemove: "9.9.9.9",
// nolint:dupword
expected: `# Comment about the first nameserver
nameserver 8.8.4.4
# Comment before our target
nameserver 9.9.9.9
options timeout:2`,
},
{
name: "Only nameserver matches",
content: `options debug
nameserver 9.9.9.9
search localdomain`,
ipToRemove: "9.9.9.9",
expected: `options debug
nameserver 9.9.9.9
search localdomain`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tempDir := t.TempDir()
tempFile := filepath.Join(tempDir, "resolv.conf")
err := os.WriteFile(tempFile, []byte(tc.content), 0644)
assert.NoError(t, err)
err = removeFirstNbNameserver(tempFile, tc.ipToRemove)
assert.NoError(t, err)
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, tc.expected, string(content), "The resulting content should match the expected output.")
})
}
}

View File

@@ -65,7 +65,7 @@ func newHostManager(wgInterface string) (hostManager, error) {
return nil, err return nil, err
} }
log.Debugf("discovered mode is: %s", osManager) log.Infof("System DNS manager discovered: %s", osManager)
return newHostManagerFromType(wgInterface, osManager) return newHostManagerFromType(wgInterface, osManager)
} }

View File

@@ -53,10 +53,12 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig) error {
searchDomainList := searchDomains(config) searchDomainList := searchDomains(config)
searchDomainList = mergeSearchDomains(searchDomainList, r.originalSearchDomains) searchDomainList = mergeSearchDomains(searchDomainList, r.originalSearchDomains)
options := prepareOptionsWithTimeout(r.othersConfigs, int(dnsFailoverTimeout.Seconds()), dnsFailoverAttempts)
buf := prepareResolvConfContent( buf := prepareResolvConfContent(
searchDomainList, searchDomainList,
append([]string{config.ServerIP}, r.originalNameServers...), append([]string{config.ServerIP}, r.originalNameServers...),
r.othersConfigs) options)
// create a backup for unclean shutdown detection before the resolv.conf is changed // create a backup for unclean shutdown detection before the resolv.conf is changed
if err := createUncleanShutdownIndicator(defaultResolvConfPath, resolvConfManager, config.ServerIP); err != nil { if err := createUncleanShutdownIndicator(defaultResolvConfPath, resolvConfManager, config.ServerIP); err != nil {

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/netip" "net/netip"
"strings"
"sync" "sync"
"github.com/miekg/dns" "github.com/miekg/dns"
@@ -11,6 +12,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer"
nbdns "github.com/netbirdio/netbird/dns" nbdns "github.com/netbirdio/netbird/dns"
) )
@@ -59,6 +61,8 @@ type DefaultServer struct {
// make sense on mobile only // make sense on mobile only
searchDomainNotifier *notifier searchDomainNotifier *notifier
iosDnsManager IosDnsManager iosDnsManager IosDnsManager
statusRecorder *peer.Status
} }
type handlerWithStop interface { type handlerWithStop interface {
@@ -73,7 +77,12 @@ type muxUpdate struct {
} }
// NewDefaultServer returns a new dns server // NewDefaultServer returns a new dns server
func NewDefaultServer(ctx context.Context, wgInterface WGIface, customAddress string) (*DefaultServer, error) { func NewDefaultServer(
ctx context.Context,
wgInterface WGIface,
customAddress string,
statusRecorder *peer.Status,
) (*DefaultServer, error) {
var addrPort *netip.AddrPort var addrPort *netip.AddrPort
if customAddress != "" { if customAddress != "" {
parsedAddrPort, err := netip.ParseAddrPort(customAddress) parsedAddrPort, err := netip.ParseAddrPort(customAddress)
@@ -90,13 +99,20 @@ func NewDefaultServer(ctx context.Context, wgInterface WGIface, customAddress st
dnsService = newServiceViaListener(wgInterface, addrPort) dnsService = newServiceViaListener(wgInterface, addrPort)
} }
return newDefaultServer(ctx, wgInterface, dnsService), nil return newDefaultServer(ctx, wgInterface, dnsService, statusRecorder), nil
} }
// NewDefaultServerPermanentUpstream returns a new dns server. It optimized for mobile systems // NewDefaultServerPermanentUpstream returns a new dns server. It optimized for mobile systems
func NewDefaultServerPermanentUpstream(ctx context.Context, wgInterface WGIface, hostsDnsList []string, config nbdns.Config, listener listener.NetworkChangeListener) *DefaultServer { func NewDefaultServerPermanentUpstream(
ctx context.Context,
wgInterface WGIface,
hostsDnsList []string,
config nbdns.Config,
listener listener.NetworkChangeListener,
statusRecorder *peer.Status,
) *DefaultServer {
log.Debugf("host dns address list is: %v", hostsDnsList) log.Debugf("host dns address list is: %v", hostsDnsList)
ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface)) ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface), statusRecorder)
ds.permanent = true ds.permanent = true
ds.hostsDnsList = hostsDnsList ds.hostsDnsList = hostsDnsList
ds.addHostRootZone() ds.addHostRootZone()
@@ -108,13 +124,18 @@ func NewDefaultServerPermanentUpstream(ctx context.Context, wgInterface WGIface,
} }
// NewDefaultServerIos returns a new dns server. It optimized for ios // NewDefaultServerIos returns a new dns server. It optimized for ios
func NewDefaultServerIos(ctx context.Context, wgInterface WGIface, iosDnsManager IosDnsManager) *DefaultServer { func NewDefaultServerIos(
ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface)) ctx context.Context,
wgInterface WGIface,
iosDnsManager IosDnsManager,
statusRecorder *peer.Status,
) *DefaultServer {
ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface), statusRecorder)
ds.iosDnsManager = iosDnsManager ds.iosDnsManager = iosDnsManager
return ds return ds
} }
func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService service) *DefaultServer { func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService service, statusRecorder *peer.Status) *DefaultServer {
ctx, stop := context.WithCancel(ctx) ctx, stop := context.WithCancel(ctx)
defaultServer := &DefaultServer{ defaultServer := &DefaultServer{
ctx: ctx, ctx: ctx,
@@ -124,7 +145,8 @@ func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService servi
localResolver: &localResolver{ localResolver: &localResolver{
registeredMap: make(registrationMap), registeredMap: make(registrationMap),
}, },
wgInterface: wgInterface, wgInterface: wgInterface,
statusRecorder: statusRecorder,
} }
return defaultServer return defaultServer
@@ -299,6 +321,8 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
s.searchDomainNotifier.onNewSearchDomains(s.SearchDomains()) s.searchDomainNotifier.onNewSearchDomains(s.SearchDomains())
} }
s.updateNSGroupStates(update.NameServerGroups)
return nil return nil
} }
@@ -338,7 +362,13 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam
continue continue
} }
handler, err := newUpstreamResolver(s.ctx, s.wgInterface.Name(), s.wgInterface.Address().IP, s.wgInterface.Address().Network) handler, err := newUpstreamResolver(
s.ctx,
s.wgInterface.Name(),
s.wgInterface.Address().IP,
s.wgInterface.Address().Network,
s.statusRecorder,
)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to create a new upstream resolver, error: %v", err) return nil, fmt.Errorf("unable to create a new upstream resolver, error: %v", err)
} }
@@ -460,14 +490,14 @@ func getNSHostPort(ns nbdns.NameServer) string {
func (s *DefaultServer) upstreamCallbacks( func (s *DefaultServer) upstreamCallbacks(
nsGroup *nbdns.NameServerGroup, nsGroup *nbdns.NameServerGroup,
handler dns.Handler, handler dns.Handler,
) (deactivate func(), reactivate func()) { ) (deactivate func(error), reactivate func()) {
var removeIndex map[string]int var removeIndex map[string]int
deactivate = func() { deactivate = func(err error) {
s.mux.Lock() s.mux.Lock()
defer s.mux.Unlock() defer s.mux.Unlock()
l := log.WithField("nameservers", nsGroup.NameServers) l := log.WithField("nameservers", nsGroup.NameServers)
l.Info("temporary deactivate nameservers group due timeout") l.Info("Temporarily deactivating nameservers group due to timeout")
removeIndex = make(map[string]int) removeIndex = make(map[string]int)
for _, domain := range nsGroup.Domains { for _, domain := range nsGroup.Domains {
@@ -486,8 +516,11 @@ func (s *DefaultServer) upstreamCallbacks(
} }
} }
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil { if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
l.WithError(err).Error("fail to apply nameserver deactivation on the host") l.Errorf("Failed to apply nameserver deactivation on the host: %v", err)
} }
s.updateNSState(nsGroup, err, false)
} }
reactivate = func() { reactivate = func() {
s.mux.Lock() s.mux.Lock()
@@ -510,12 +543,20 @@ func (s *DefaultServer) upstreamCallbacks(
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil { if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
l.WithError(err).Error("reactivate temporary disabled nameserver group, DNS update apply") l.WithError(err).Error("reactivate temporary disabled nameserver group, DNS update apply")
} }
s.updateNSState(nsGroup, nil, true)
} }
return return
} }
func (s *DefaultServer) addHostRootZone() { func (s *DefaultServer) addHostRootZone() {
handler, err := newUpstreamResolver(s.ctx, s.wgInterface.Name(), s.wgInterface.Address().IP, s.wgInterface.Address().Network) handler, err := newUpstreamResolver(
s.ctx,
s.wgInterface.Name(),
s.wgInterface.Address().IP,
s.wgInterface.Address().Network,
s.statusRecorder,
)
if err != nil { if err != nil {
log.Errorf("unable to create a new upstream resolver, error: %v", err) log.Errorf("unable to create a new upstream resolver, error: %v", err)
return return
@@ -535,7 +576,50 @@ func (s *DefaultServer) addHostRootZone() {
handler.upstreamServers[n] = fmt.Sprintf("%s:53", ipString) handler.upstreamServers[n] = fmt.Sprintf("%s:53", ipString)
} }
handler.deactivate = func() {} handler.deactivate = func(error) {}
handler.reactivate = func() {} handler.reactivate = func() {}
s.service.RegisterMux(nbdns.RootZone, handler) s.service.RegisterMux(nbdns.RootZone, handler)
} }
func (s *DefaultServer) updateNSGroupStates(groups []*nbdns.NameServerGroup) {
var states []peer.NSGroupState
for _, group := range groups {
var servers []string
for _, ns := range group.NameServers {
servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port))
}
state := peer.NSGroupState{
ID: generateGroupKey(group),
Servers: servers,
Domains: group.Domains,
// The probe will determine the state, default enabled
Enabled: true,
Error: nil,
}
states = append(states, state)
}
s.statusRecorder.UpdateDNSStates(states)
}
func (s *DefaultServer) updateNSState(nsGroup *nbdns.NameServerGroup, err error, enabled bool) {
states := s.statusRecorder.GetDNSStates()
id := generateGroupKey(nsGroup)
for i, state := range states {
if state.ID == id {
states[i].Enabled = enabled
states[i].Error = err
break
}
}
s.statusRecorder.UpdateDNSStates(states)
}
func generateGroupKey(nsGroup *nbdns.NameServerGroup) string {
var servers []string
for _, ns := range nsGroup.NameServers {
servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port))
}
return fmt.Sprintf("%s_%s_%s", nsGroup.ID, nsGroup.Name, strings.Join(servers, ","))
}

View File

@@ -15,6 +15,7 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/firewall/uspfilter" "github.com/netbirdio/netbird/client/firewall/uspfilter"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/client/internal/stdnet"
nbdns "github.com/netbirdio/netbird/dns" nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/formatter" "github.com/netbirdio/netbird/formatter"
@@ -274,7 +275,7 @@ func TestUpdateDNSServer(t *testing.T) {
t.Log(err) t.Log(err)
} }
}() }()
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "") dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", &peer.Status{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -375,7 +376,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
return return
} }
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "") dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", &peer.Status{})
if err != nil { if err != nil {
t.Errorf("create DNS server: %v", err) t.Errorf("create DNS server: %v", err)
return return
@@ -470,7 +471,7 @@ func TestDNSServerStartStop(t *testing.T) {
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort) dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort, &peer.Status{})
if err != nil { if err != nil {
t.Fatalf("%v", err) t.Fatalf("%v", err)
} }
@@ -541,6 +542,7 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
{false, "domain2", false}, {false, "domain2", false},
}, },
}, },
statusRecorder: &peer.Status{},
} }
var domainsUpdate string var domainsUpdate string
@@ -563,7 +565,7 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
}, },
}, nil) }, nil)
deactivate() deactivate(nil)
expected := "domain0,domain2" expected := "domain0,domain2"
domains := []string{} domains := []string{}
for _, item := range server.currentConfig.Domains { for _, item := range server.currentConfig.Domains {
@@ -601,7 +603,7 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) {
var dnsList []string var dnsList []string
dnsConfig := nbdns.Config{} dnsConfig := nbdns.Config{}
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil) dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil, &peer.Status{})
err = dnsServer.Initialize() err = dnsServer.Initialize()
if err != nil { if err != nil {
t.Errorf("failed to initialize DNS server: %v", err) t.Errorf("failed to initialize DNS server: %v", err)
@@ -625,7 +627,7 @@ func TestDNSPermanent_updateUpstream(t *testing.T) {
} }
defer wgIFace.Close() defer wgIFace.Close()
dnsConfig := nbdns.Config{} dnsConfig := nbdns.Config{}
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil) dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, &peer.Status{})
err = dnsServer.Initialize() err = dnsServer.Initialize()
if err != nil { if err != nil {
t.Errorf("failed to initialize DNS server: %v", err) t.Errorf("failed to initialize DNS server: %v", err)
@@ -717,7 +719,7 @@ func TestDNSPermanent_matchOnly(t *testing.T) {
} }
defer wgIFace.Close() defer wgIFace.Close()
dnsConfig := nbdns.Config{} dnsConfig := nbdns.Config{}
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil) dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, &peer.Status{})
err = dnsServer.Initialize() err = dnsServer.Initialize()
if err != nil { if err != nil {
t.Errorf("failed to initialize DNS server: %v", err) t.Errorf("failed to initialize DNS server: %v", err)

View File

@@ -11,8 +11,11 @@ import (
"time" "time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/hashicorp/go-multierror"
"github.com/miekg/dns" "github.com/miekg/dns"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/peer"
) )
const ( const (
@@ -45,12 +48,13 @@ type upstreamResolverBase struct {
reactivatePeriod time.Duration reactivatePeriod time.Duration
upstreamTimeout time.Duration upstreamTimeout time.Duration
deactivate func() deactivate func(error)
reactivate func() reactivate func()
statusRecorder *peer.Status
} }
func newUpstreamResolverBase(parentCTX context.Context) *upstreamResolverBase { func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status) *upstreamResolverBase {
ctx, cancel := context.WithCancel(parentCTX) ctx, cancel := context.WithCancel(ctx)
return &upstreamResolverBase{ return &upstreamResolverBase{
ctx: ctx, ctx: ctx,
@@ -58,6 +62,7 @@ func newUpstreamResolverBase(parentCTX context.Context) *upstreamResolverBase {
upstreamTimeout: upstreamTimeout, upstreamTimeout: upstreamTimeout,
reactivatePeriod: reactivatePeriod, reactivatePeriod: reactivatePeriod,
failsTillDeact: failsTillDeact, failsTillDeact: failsTillDeact,
statusRecorder: statusRecorder,
} }
} }
@@ -68,7 +73,10 @@ func (u *upstreamResolverBase) stop() {
// ServeDNS handles a DNS request // ServeDNS handles a DNS request
func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
defer u.checkUpstreamFails() var err error
defer func() {
u.checkUpstreamFails(err)
}()
log.WithField("question", r.Question[0]).Trace("received an upstream question") log.WithField("question", r.Question[0]).Trace("received an upstream question")
@@ -81,7 +89,6 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
for _, upstream := range u.upstreamServers { for _, upstream := range u.upstreamServers {
var rm *dns.Msg var rm *dns.Msg
var t time.Duration var t time.Duration
var err error
func() { func() {
ctx, cancel := context.WithTimeout(u.ctx, u.upstreamTimeout) ctx, cancel := context.WithTimeout(u.ctx, u.upstreamTimeout)
@@ -132,7 +139,7 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
// If fails count is greater that failsTillDeact, upstream resolving // If fails count is greater that failsTillDeact, upstream resolving
// will be disabled for reactivatePeriod, after that time period fails counter // will be disabled for reactivatePeriod, after that time period fails counter
// will be reset and upstream will be reactivated. // will be reset and upstream will be reactivated.
func (u *upstreamResolverBase) checkUpstreamFails() { func (u *upstreamResolverBase) checkUpstreamFails(err error) {
u.mutex.Lock() u.mutex.Lock()
defer u.mutex.Unlock() defer u.mutex.Unlock()
@@ -146,7 +153,7 @@ func (u *upstreamResolverBase) checkUpstreamFails() {
default: default:
} }
u.disable() u.disable(err)
} }
// probeAvailability tests all upstream servers simultaneously and // probeAvailability tests all upstream servers simultaneously and
@@ -165,13 +172,16 @@ func (u *upstreamResolverBase) probeAvailability() {
var mu sync.Mutex var mu sync.Mutex
var wg sync.WaitGroup var wg sync.WaitGroup
var errors *multierror.Error
for _, upstream := range u.upstreamServers { for _, upstream := range u.upstreamServers {
upstream := upstream upstream := upstream
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
if err := u.testNameserver(upstream); err != nil { err := u.testNameserver(upstream)
if err != nil {
errors = multierror.Append(errors, err)
log.Warnf("probing upstream nameserver %s: %s", upstream, err) log.Warnf("probing upstream nameserver %s: %s", upstream, err)
return return
} }
@@ -186,7 +196,7 @@ func (u *upstreamResolverBase) probeAvailability() {
// didn't find a working upstream server, let's disable and try later // didn't find a working upstream server, let's disable and try later
if !success { if !success {
u.disable() u.disable(errors.ErrorOrNil())
} }
} }
@@ -245,15 +255,15 @@ func isTimeout(err error) bool {
return false return false
} }
func (u *upstreamResolverBase) disable() { func (u *upstreamResolverBase) disable(err error) {
if u.disabled { if u.disabled {
return return
} }
// todo test the deactivation logic, it seems to affect the client // todo test the deactivation logic, it seems to affect the client
if runtime.GOOS != "ios" { if runtime.GOOS != "ios" {
log.Warnf("upstream resolving is Disabled for %v", reactivatePeriod) log.Warnf("Upstream resolving is Disabled for %v", reactivatePeriod)
u.deactivate() u.deactivate(err)
u.disabled = true u.disabled = true
go u.waitUntilResponse() go u.waitUntilResponse()
} }

View File

@@ -11,6 +11,8 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/netbirdio/netbird/client/internal/peer"
) )
type upstreamResolverIOS struct { type upstreamResolverIOS struct {
@@ -20,8 +22,14 @@ type upstreamResolverIOS struct {
iIndex int iIndex int
} }
func newUpstreamResolver(parentCTX context.Context, interfaceName string, ip net.IP, net *net.IPNet) (*upstreamResolverIOS, error) { func newUpstreamResolver(
upstreamResolverBase := newUpstreamResolverBase(parentCTX) ctx context.Context,
interfaceName string,
ip net.IP,
net *net.IPNet,
statusRecorder *peer.Status,
) (*upstreamResolverIOS, error) {
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder)
index, err := getInterfaceIndex(interfaceName) index, err := getInterfaceIndex(interfaceName)
if err != nil { if err != nil {

View File

@@ -8,14 +8,22 @@ import (
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/netbirdio/netbird/client/internal/peer"
) )
type upstreamResolverNonIOS struct { type upstreamResolverNonIOS struct {
*upstreamResolverBase *upstreamResolverBase
} }
func newUpstreamResolver(parentCTX context.Context, interfaceName string, ip net.IP, net *net.IPNet) (*upstreamResolverNonIOS, error) { func newUpstreamResolver(
upstreamResolverBase := newUpstreamResolverBase(parentCTX) ctx context.Context,
_ string,
_ net.IP,
_ *net.IPNet,
statusRecorder *peer.Status,
) (*upstreamResolverNonIOS, error) {
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder)
nonIOS := &upstreamResolverNonIOS{ nonIOS := &upstreamResolverNonIOS{
upstreamResolverBase: upstreamResolverBase, upstreamResolverBase: upstreamResolverBase,
} }

View File

@@ -58,7 +58,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.TODO()) ctx, cancel := context.WithCancel(context.TODO())
resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}) resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil)
resolver.upstreamServers = testCase.InputServers resolver.upstreamServers = testCase.InputServers
resolver.upstreamTimeout = testCase.timeout resolver.upstreamTimeout = testCase.timeout
if testCase.cancelCTX { if testCase.cancelCTX {
@@ -131,7 +131,7 @@ func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
} }
failed := false failed := false
resolver.deactivate = func() { resolver.deactivate = func(error) {
failed = true failed = true
} }

View File

@@ -1188,14 +1188,21 @@ func (e *Engine) newDnsServer() ([]*route.Route, dns.Server, error) {
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
dnsServer := dns.NewDefaultServerPermanentUpstream(e.ctx, e.wgInterface, e.mobileDep.HostDNSAddresses, *dnsConfig, e.mobileDep.NetworkChangeListener) dnsServer := dns.NewDefaultServerPermanentUpstream(
e.ctx,
e.wgInterface,
e.mobileDep.HostDNSAddresses,
*dnsConfig,
e.mobileDep.NetworkChangeListener,
e.statusRecorder,
)
go e.mobileDep.DnsReadyListener.OnReady() go e.mobileDep.DnsReadyListener.OnReady()
return routes, dnsServer, nil return routes, dnsServer, nil
case "ios": case "ios":
dnsServer := dns.NewDefaultServerIos(e.ctx, e.wgInterface, e.mobileDep.DnsManager) dnsServer := dns.NewDefaultServerIos(e.ctx, e.wgInterface, e.mobileDep.DnsManager, e.statusRecorder)
return nil, dnsServer, nil return nil, dnsServer, nil
default: default:
dnsServer, err := dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress) dnsServer, err := dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress, e.statusRecorder)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@@ -38,7 +38,7 @@ func IsLoginRequired(ctx context.Context, privateKey string, mgmURL *url.URL, ss
return false, err return false, err
} }
_, err = doMgmLogin(ctx, mgmClient, pubSSHKey) _, err = doMgmLogin(ctx, mgmClient, pubSSHKey, &Config{})
if isLoginNeeded(err) { if isLoginNeeded(err) {
return true, nil return true, nil
} }
@@ -67,7 +67,7 @@ func Login(ctx context.Context, config *Config, setupKey string, jwtToken string
return err return err
} }
serverKey, err := doMgmLogin(ctx, mgmClient, pubSSHKey) serverKey, err := doMgmLogin(ctx, mgmClient, pubSSHKey, config)
if isRegistrationNeeded(err) { if isRegistrationNeeded(err) {
log.Debugf("peer registration required") log.Debugf("peer registration required")
_, err = registerPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey) _, err = registerPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey)
@@ -99,14 +99,14 @@ func getMgmClient(ctx context.Context, privateKey string, mgmURL *url.URL) (*mgm
return mgmClient, err return mgmClient, err
} }
func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte) (*wgtypes.Key, error) { func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte, config *Config) (*wgtypes.Key, error) {
serverKey, err := mgmClient.GetServerPublicKey() serverKey, err := mgmClient.GetServerPublicKey()
if err != nil { if err != nil {
log.Errorf("failed while getting Management Service public key: %v", err) log.Errorf("failed while getting Management Service public key: %v", err)
return nil, err return nil, err
} }
sysInfo := system.GetInfo(ctx) sysInfo := system.GetInfo(ctx, *config)
_, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey) _, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey)
return serverKey, err return serverKey, err
} }
@@ -120,7 +120,7 @@ func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.
} }
log.Debugf("sending peer registration request to Management Service") log.Debugf("sending peer registration request to Management Service")
info := system.GetInfo(ctx) info := system.GetInfo(ctx, Config{})
loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info, pubSSHKey) loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info, pubSSHKey)
if err != nil { if err != nil {
log.Errorf("failed registering peer %v,%s", err, validSetupKey.String()) log.Errorf("failed registering peer %v,%s", err, validSetupKey.String())

View File

@@ -5,6 +5,9 @@ import (
"sync" "sync"
"time" "time"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal/relay" "github.com/netbirdio/netbird/client/internal/relay"
"github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/iface"
) )
@@ -26,6 +29,7 @@ type State struct {
BytesTx int64 BytesTx int64
BytesRx int64 BytesRx int64
RosenpassEnabled bool RosenpassEnabled bool
Routes map[string]struct{}
} }
// LocalPeerState contains the latest state of the local peer // LocalPeerState contains the latest state of the local peer
@@ -34,6 +38,7 @@ type LocalPeerState struct {
PubKey string PubKey string
KernelInterface bool KernelInterface bool
FQDN string FQDN string
Routes map[string]struct{}
} }
// SignalState contains the latest state of a signal connection // SignalState contains the latest state of a signal connection
@@ -56,6 +61,16 @@ type RosenpassState struct {
Permissive bool Permissive bool
} }
// NSGroupState represents the status of a DNS server group, including associated domains,
// whether it's enabled, and the last error message encountered during probing.
type NSGroupState struct {
ID string
Servers []string
Domains []string
Enabled bool
Error error
}
// FullStatus contains the full state held by the Status instance // FullStatus contains the full state held by the Status instance
type FullStatus struct { type FullStatus struct {
Peers []State Peers []State
@@ -64,6 +79,7 @@ type FullStatus struct {
LocalPeerState LocalPeerState LocalPeerState LocalPeerState
RosenpassState RosenpassState RosenpassState RosenpassState
Relays []relay.ProbeResult Relays []relay.ProbeResult
NSGroupStates []NSGroupState
} }
// Status holds a state of peers, signal, management connections and relays // Status holds a state of peers, signal, management connections and relays
@@ -83,6 +99,7 @@ type Status struct {
notifier *notifier notifier *notifier
rosenpassEnabled bool rosenpassEnabled bool
rosenpassPermissive bool rosenpassPermissive bool
nsGroupStates []NSGroupState
// To reduce the number of notification invocation this bool will be true when need to call the notification // To reduce the number of notification invocation this bool will be true when need to call the notification
// Some Peer actions mostly used by in a batch when the network map has been synchronized. In these type of events // Some Peer actions mostly used by in a batch when the network map has been synchronized. In these type of events
@@ -171,6 +188,10 @@ func (d *Status) UpdatePeerState(receivedState State) error {
peerState.IP = receivedState.IP peerState.IP = receivedState.IP
} }
if receivedState.Routes != nil {
peerState.Routes = receivedState.Routes
}
skipNotification := shouldSkipNotify(receivedState, peerState) skipNotification := shouldSkipNotify(receivedState, peerState)
if receivedState.ConnStatus != peerState.ConnStatus { if receivedState.ConnStatus != peerState.ConnStatus {
@@ -275,6 +296,13 @@ func (d *Status) GetPeerStateChangeNotifier(peer string) <-chan struct{} {
return ch return ch
} }
// GetLocalPeerState returns the local peer state
func (d *Status) GetLocalPeerState() LocalPeerState {
d.mux.Lock()
defer d.mux.Unlock()
return d.localPeer
}
// UpdateLocalPeerState updates local peer status // UpdateLocalPeerState updates local peer status
func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) { func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) {
d.mux.Lock() d.mux.Lock()
@@ -361,6 +389,12 @@ func (d *Status) UpdateRelayStates(relayResults []relay.ProbeResult) {
d.relayStates = relayResults d.relayStates = relayResults
} }
func (d *Status) UpdateDNSStates(dnsStates []NSGroupState) {
d.mux.Lock()
defer d.mux.Unlock()
d.nsGroupStates = dnsStates
}
func (d *Status) GetRosenpassState() RosenpassState { func (d *Status) GetRosenpassState() RosenpassState {
return RosenpassState{ return RosenpassState{
d.rosenpassEnabled, d.rosenpassEnabled,
@@ -376,6 +410,24 @@ func (d *Status) GetManagementState() ManagementState {
} }
} }
// IsLoginRequired determines if a peer's login has expired.
func (d *Status) IsLoginRequired() bool {
d.mux.Lock()
defer d.mux.Unlock()
// if peer is connected to the management then login is not expired
if d.managementState {
return false
}
s, ok := gstatus.FromError(d.managementError)
if ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
return true
}
return false
}
func (d *Status) GetSignalState() SignalState { func (d *Status) GetSignalState() SignalState {
return SignalState{ return SignalState{
d.signalAddress, d.signalAddress,
@@ -388,6 +440,10 @@ func (d *Status) GetRelayStates() []relay.ProbeResult {
return d.relayStates return d.relayStates
} }
func (d *Status) GetDNSStates() []NSGroupState {
return d.nsGroupStates
}
// GetFullStatus gets full status // GetFullStatus gets full status
func (d *Status) GetFullStatus() FullStatus { func (d *Status) GetFullStatus() FullStatus {
d.mux.Lock() d.mux.Lock()
@@ -399,6 +455,7 @@ func (d *Status) GetFullStatus() FullStatus {
LocalPeerState: d.localPeer, LocalPeerState: d.localPeer,
Relays: d.GetRelayStates(), Relays: d.GetRelayStates(),
RosenpassState: d.GetRosenpassState(), RosenpassState: d.GetRosenpassState(),
NSGroupStates: d.GetDNSStates(),
} }
for _, status := range d.peers { for _, status := range d.peers {

View File

@@ -160,6 +160,12 @@ func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error {
if err != nil { if err != nil {
return err return err
} }
delete(state.Routes, c.network.String())
if err := c.statusRecorder.UpdatePeerState(state); err != nil {
log.Warnf("Failed to update peer state: %v", err)
}
if state.ConnStatus != peer.StatusConnected { if state.ConnStatus != peer.StatusConnected {
return nil return nil
} }
@@ -225,6 +231,20 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
} }
c.chosenRoute = c.routes[chosen] c.chosenRoute = c.routes[chosen]
state, err := c.statusRecorder.GetPeer(c.chosenRoute.Peer)
if err != nil {
log.Errorf("Failed to get peer state: %v", err)
} else {
if state.Routes == nil {
state.Routes = map[string]struct{}{}
}
state.Routes[c.network.String()] = struct{}{}
if err := c.statusRecorder.UpdatePeerState(state); err != nil {
log.Warnf("Failed to update peer state: %v", err)
}
}
err = c.wgInterface.AddAllowedIP(c.chosenRoute.Peer, c.network.String()) err = c.wgInterface.AddAllowedIP(c.chosenRoute.Peer, c.network.String())
if err != nil { if err != nil {
log.Errorf("couldn't add allowed IP %s added for peer %s, err: %v", log.Errorf("couldn't add allowed IP %s added for peer %s, err: %v",

View File

@@ -58,7 +58,7 @@ func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface,
func (m *DefaultManager) EnableServerRouter(firewall firewall.Manager) error { func (m *DefaultManager) EnableServerRouter(firewall firewall.Manager) error {
var err error var err error
m.serverRouter, err = newServerRouter(m.ctx, m.wgInterface, firewall) m.serverRouter, err = newServerRouter(m.ctx, m.wgInterface, firewall, m.statusRecorder)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -7,9 +7,10 @@ import (
"fmt" "fmt"
firewall "github.com/netbirdio/netbird/client/firewall/manager" firewall "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/iface"
) )
func newServerRouter(context.Context, *iface.WGIface, firewall.Manager) (serverRouter, error) { func newServerRouter(context.Context, *iface.WGIface, firewall.Manager, *peer.Status) (serverRouter, error) {
return nil, fmt.Errorf("server route not supported on this os") return nil, fmt.Errorf("server route not supported on this os")
} }

View File

@@ -10,24 +10,27 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
firewall "github.com/netbirdio/netbird/client/firewall/manager" firewall "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/route"
) )
type defaultServerRouter struct { type defaultServerRouter struct {
mux sync.Mutex mux sync.Mutex
ctx context.Context ctx context.Context
routes map[string]*route.Route routes map[string]*route.Route
firewall firewall.Manager firewall firewall.Manager
wgInterface *iface.WGIface wgInterface *iface.WGIface
statusRecorder *peer.Status
} }
func newServerRouter(ctx context.Context, wgInterface *iface.WGIface, firewall firewall.Manager) (serverRouter, error) { func newServerRouter(ctx context.Context, wgInterface *iface.WGIface, firewall firewall.Manager, statusRecorder *peer.Status) (serverRouter, error) {
return &defaultServerRouter{ return &defaultServerRouter{
ctx: ctx, ctx: ctx,
routes: make(map[string]*route.Route), routes: make(map[string]*route.Route),
firewall: firewall, firewall: firewall,
wgInterface: wgInterface, wgInterface: wgInterface,
statusRecorder: statusRecorder,
}, nil }, nil
} }
@@ -88,6 +91,11 @@ func (m *defaultServerRouter) removeFromServerNetwork(route *route.Route) error
return err return err
} }
delete(m.routes, route.ID) delete(m.routes, route.ID)
state := m.statusRecorder.GetLocalPeerState()
delete(state.Routes, route.Network.String())
m.statusRecorder.UpdateLocalPeerState(state)
return nil return nil
} }
} }
@@ -105,6 +113,14 @@ func (m *defaultServerRouter) addToServerNetwork(route *route.Route) error {
return err return err
} }
m.routes[route.ID] = route m.routes[route.ID] = route
state := m.statusRecorder.GetLocalPeerState()
if state.Routes == nil {
state.Routes = map[string]struct{}{}
}
state.Routes[route.Network.String()] = struct{}{}
m.statusRecorder.UpdateLocalPeerState(state)
return nil return nil
} }
} }
@@ -117,6 +133,10 @@ func (m *defaultServerRouter) cleanUp() {
if err != nil { if err != nil {
log.Warnf("failed to remove clean up route: %s", r.ID) log.Warnf("failed to remove clean up route: %s", r.ID)
} }
state := m.statusRecorder.GetLocalPeerState()
state.Routes = nil
m.statusRecorder.UpdateLocalPeerState(state)
} }
} }

View File

@@ -0,0 +1,82 @@
package internal
import (
"context"
"os/exec"
"strings"
"sync"
"time"
"github.com/netbirdio/netbird/client/internal/peer"
)
type SessionWatcher struct {
ctx context.Context
mutex sync.Mutex
peerStatusRecorder *peer.Status
watchTicker *time.Ticker
sendNotification bool
onExpireListener func()
}
// NewSessionWatcher creates a new instance of SessionWatcher.
func NewSessionWatcher(ctx context.Context, peerStatusRecorder *peer.Status) *SessionWatcher {
s := &SessionWatcher{
ctx: ctx,
peerStatusRecorder: peerStatusRecorder,
watchTicker: time.NewTicker(2 * time.Second),
}
go s.startWatcher()
return s
}
// SetOnExpireListener sets the callback func to be called when the session expires.
func (s *SessionWatcher) SetOnExpireListener(onExpire func()) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.onExpireListener = onExpire
}
// startWatcher continuously checks if the session requires login and
// calls the onExpireListener if login is required.
func (s *SessionWatcher) startWatcher() {
for {
select {
case <-s.ctx.Done():
s.watchTicker.Stop()
return
case <-s.watchTicker.C:
managementState := s.peerStatusRecorder.GetManagementState()
if managementState.Connected {
s.sendNotification = true
}
isLoginRequired := s.peerStatusRecorder.IsLoginRequired()
if isLoginRequired && s.sendNotification && s.onExpireListener != nil {
s.mutex.Lock()
s.onExpireListener()
s.sendNotification = false
s.mutex.Unlock()
}
}
}
}
// CheckUIApp checks whether UI application is running.
func CheckUIApp() bool {
cmd := exec.Command("ps", "-ef")
output, err := cmd.Output()
if err != nil {
return false
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, "netbird-ui") && !strings.Contains(line, "grep") {
return true
}
}
return false
}

View File

@@ -772,6 +772,7 @@ type PeerState struct {
BytesRx int64 `protobuf:"varint,13,opt,name=bytesRx,proto3" json:"bytesRx,omitempty"` BytesRx int64 `protobuf:"varint,13,opt,name=bytesRx,proto3" json:"bytesRx,omitempty"`
BytesTx int64 `protobuf:"varint,14,opt,name=bytesTx,proto3" json:"bytesTx,omitempty"` BytesTx int64 `protobuf:"varint,14,opt,name=bytesTx,proto3" json:"bytesTx,omitempty"`
RosenpassEnabled bool `protobuf:"varint,15,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` RosenpassEnabled bool `protobuf:"varint,15,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"`
Routes []string `protobuf:"bytes,16,rep,name=routes,proto3" json:"routes,omitempty"`
} }
func (x *PeerState) Reset() { func (x *PeerState) Reset() {
@@ -911,18 +912,26 @@ func (x *PeerState) GetRosenpassEnabled() bool {
return false return false
} }
func (x *PeerState) GetRoutes() []string {
if x != nil {
return x.Routes
}
return nil
}
// LocalPeerState contains the latest state of the local peer // LocalPeerState contains the latest state of the local peer
type LocalPeerState struct { type LocalPeerState struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"` IP string `protobuf:"bytes,1,opt,name=IP,proto3" json:"IP,omitempty"`
PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"` PubKey string `protobuf:"bytes,2,opt,name=pubKey,proto3" json:"pubKey,omitempty"`
KernelInterface bool `protobuf:"varint,3,opt,name=kernelInterface,proto3" json:"kernelInterface,omitempty"` KernelInterface bool `protobuf:"varint,3,opt,name=kernelInterface,proto3" json:"kernelInterface,omitempty"`
Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"`
RosenpassEnabled bool `protobuf:"varint,5,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"` RosenpassEnabled bool `protobuf:"varint,5,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"`
RosenpassPermissive bool `protobuf:"varint,6,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"` RosenpassPermissive bool `protobuf:"varint,6,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"`
Routes []string `protobuf:"bytes,7,rep,name=routes,proto3" json:"routes,omitempty"`
} }
func (x *LocalPeerState) Reset() { func (x *LocalPeerState) Reset() {
@@ -999,6 +1008,13 @@ func (x *LocalPeerState) GetRosenpassPermissive() bool {
return false return false
} }
func (x *LocalPeerState) GetRoutes() []string {
if x != nil {
return x.Routes
}
return nil
}
// SignalState contains the latest state of a signal connection // SignalState contains the latest state of a signal connection
type SignalState struct { type SignalState struct {
state protoimpl.MessageState state protoimpl.MessageState
@@ -1191,6 +1207,77 @@ func (x *RelayState) GetError() string {
return "" return ""
} }
type NSGroupState struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Servers []string `protobuf:"bytes,1,rep,name=servers,proto3" json:"servers,omitempty"`
Domains []string `protobuf:"bytes,2,rep,name=domains,proto3" json:"domains,omitempty"`
Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"`
Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"`
}
func (x *NSGroupState) Reset() {
*x = NSGroupState{}
if protoimpl.UnsafeEnabled {
mi := &file_daemon_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NSGroupState) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NSGroupState) ProtoMessage() {}
func (x *NSGroupState) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[17]
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 NSGroupState.ProtoReflect.Descriptor instead.
func (*NSGroupState) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{17}
}
func (x *NSGroupState) GetServers() []string {
if x != nil {
return x.Servers
}
return nil
}
func (x *NSGroupState) GetDomains() []string {
if x != nil {
return x.Domains
}
return nil
}
func (x *NSGroupState) GetEnabled() bool {
if x != nil {
return x.Enabled
}
return false
}
func (x *NSGroupState) GetError() string {
if x != nil {
return x.Error
}
return ""
}
// FullStatus contains the full state held by the Status instance // FullStatus contains the full state held by the Status instance
type FullStatus struct { type FullStatus struct {
state protoimpl.MessageState state protoimpl.MessageState
@@ -1202,12 +1289,13 @@ type FullStatus struct {
LocalPeerState *LocalPeerState `protobuf:"bytes,3,opt,name=localPeerState,proto3" json:"localPeerState,omitempty"` LocalPeerState *LocalPeerState `protobuf:"bytes,3,opt,name=localPeerState,proto3" json:"localPeerState,omitempty"`
Peers []*PeerState `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"` Peers []*PeerState `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"`
Relays []*RelayState `protobuf:"bytes,5,rep,name=relays,proto3" json:"relays,omitempty"` Relays []*RelayState `protobuf:"bytes,5,rep,name=relays,proto3" json:"relays,omitempty"`
DnsServers []*NSGroupState `protobuf:"bytes,6,rep,name=dns_servers,json=dnsServers,proto3" json:"dns_servers,omitempty"`
} }
func (x *FullStatus) Reset() { func (x *FullStatus) Reset() {
*x = FullStatus{} *x = FullStatus{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_daemon_proto_msgTypes[17] mi := &file_daemon_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -1220,7 +1308,7 @@ func (x *FullStatus) String() string {
func (*FullStatus) ProtoMessage() {} func (*FullStatus) ProtoMessage() {}
func (x *FullStatus) ProtoReflect() protoreflect.Message { func (x *FullStatus) ProtoReflect() protoreflect.Message {
mi := &file_daemon_proto_msgTypes[17] mi := &file_daemon_proto_msgTypes[18]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -1233,7 +1321,7 @@ func (x *FullStatus) ProtoReflect() protoreflect.Message {
// Deprecated: Use FullStatus.ProtoReflect.Descriptor instead. // Deprecated: Use FullStatus.ProtoReflect.Descriptor instead.
func (*FullStatus) Descriptor() ([]byte, []int) { func (*FullStatus) Descriptor() ([]byte, []int) {
return file_daemon_proto_rawDescGZIP(), []int{17} return file_daemon_proto_rawDescGZIP(), []int{18}
} }
func (x *FullStatus) GetManagementState() *ManagementState { func (x *FullStatus) GetManagementState() *ManagementState {
@@ -1271,6 +1359,13 @@ func (x *FullStatus) GetRelays() []*RelayState {
return nil return nil
} }
func (x *FullStatus) GetDnsServers() []*NSGroupState {
if x != nil {
return x.DnsServers
}
return nil
}
var File_daemon_proto protoreflect.FileDescriptor var File_daemon_proto protoreflect.FileDescriptor
var file_daemon_proto_rawDesc = []byte{ var file_daemon_proto_rawDesc = []byte{
@@ -1380,7 +1475,7 @@ var file_daemon_proto_rawDesc = []byte{
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65,
0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c,
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c,
0x22, 0x81, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x22, 0x99, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e,
0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16,
0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74,
@@ -1420,20 +1515,23 @@ var file_daemon_proto_rawDesc = []byte{
0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65,
0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01,
0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
0x62, 0x6c, 0x65, 0x64, 0x22, 0xd4, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x10,
0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0xec, 0x01, 0x0a,
0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12,
0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12,
0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65,
0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63,
0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61,
0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52,
0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65,
0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65,
0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13,
0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73,
0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x69, 0x76, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20,
0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53,
0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52,
0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09,
0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
@@ -1449,50 +1547,61 @@ var file_daemon_proto_rawDesc = []byte{
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61,
0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76,
0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x9b, 0x02, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a,
0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a,
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x72, 0x22, 0xd2, 0x02, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d,
0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61,
0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61,
0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73,
0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f,
0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01,
0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x32, 0xf7, 0x02, 0x0a, 0x0d, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61,
0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61,
0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65,
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d,
0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65,
0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20,
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c,
0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12,
0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06,
0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53,
0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53,
0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x32, 0xf7, 0x02, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f,
0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69,
0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e,
0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e,
0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53,
0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e,
0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f,
0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a,
0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52,
0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06,
0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12,
0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f,
0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09,
0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d,
0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74,
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
} }
var ( var (
@@ -1507,7 +1616,7 @@ func file_daemon_proto_rawDescGZIP() []byte {
return file_daemon_proto_rawDescData return file_daemon_proto_rawDescData
} }
var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
var file_daemon_proto_goTypes = []interface{}{ var file_daemon_proto_goTypes = []interface{}{
(*LoginRequest)(nil), // 0: daemon.LoginRequest (*LoginRequest)(nil), // 0: daemon.LoginRequest
(*LoginResponse)(nil), // 1: daemon.LoginResponse (*LoginResponse)(nil), // 1: daemon.LoginResponse
@@ -1526,35 +1635,37 @@ var file_daemon_proto_goTypes = []interface{}{
(*SignalState)(nil), // 14: daemon.SignalState (*SignalState)(nil), // 14: daemon.SignalState
(*ManagementState)(nil), // 15: daemon.ManagementState (*ManagementState)(nil), // 15: daemon.ManagementState
(*RelayState)(nil), // 16: daemon.RelayState (*RelayState)(nil), // 16: daemon.RelayState
(*FullStatus)(nil), // 17: daemon.FullStatus (*NSGroupState)(nil), // 17: daemon.NSGroupState
(*timestamp.Timestamp)(nil), // 18: google.protobuf.Timestamp (*FullStatus)(nil), // 18: daemon.FullStatus
(*timestamp.Timestamp)(nil), // 19: google.protobuf.Timestamp
} }
var file_daemon_proto_depIdxs = []int32{ var file_daemon_proto_depIdxs = []int32{
17, // 0: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus 18, // 0: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus
18, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp 19, // 1: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp
18, // 2: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp 19, // 2: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp
15, // 3: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 15, // 3: daemon.FullStatus.managementState:type_name -> daemon.ManagementState
14, // 4: daemon.FullStatus.signalState:type_name -> daemon.SignalState 14, // 4: daemon.FullStatus.signalState:type_name -> daemon.SignalState
13, // 5: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState 13, // 5: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState
12, // 6: daemon.FullStatus.peers:type_name -> daemon.PeerState 12, // 6: daemon.FullStatus.peers:type_name -> daemon.PeerState
16, // 7: daemon.FullStatus.relays:type_name -> daemon.RelayState 16, // 7: daemon.FullStatus.relays:type_name -> daemon.RelayState
0, // 8: daemon.DaemonService.Login:input_type -> daemon.LoginRequest 17, // 8: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState
2, // 9: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest 0, // 9: daemon.DaemonService.Login:input_type -> daemon.LoginRequest
4, // 10: daemon.DaemonService.Up:input_type -> daemon.UpRequest 2, // 10: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest
6, // 11: daemon.DaemonService.Status:input_type -> daemon.StatusRequest 4, // 11: daemon.DaemonService.Up:input_type -> daemon.UpRequest
8, // 12: daemon.DaemonService.Down:input_type -> daemon.DownRequest 6, // 12: daemon.DaemonService.Status:input_type -> daemon.StatusRequest
10, // 13: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest 8, // 13: daemon.DaemonService.Down:input_type -> daemon.DownRequest
1, // 14: daemon.DaemonService.Login:output_type -> daemon.LoginResponse 10, // 14: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest
3, // 15: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse 1, // 15: daemon.DaemonService.Login:output_type -> daemon.LoginResponse
5, // 16: daemon.DaemonService.Up:output_type -> daemon.UpResponse 3, // 16: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse
7, // 17: daemon.DaemonService.Status:output_type -> daemon.StatusResponse 5, // 17: daemon.DaemonService.Up:output_type -> daemon.UpResponse
9, // 18: daemon.DaemonService.Down:output_type -> daemon.DownResponse 7, // 18: daemon.DaemonService.Status:output_type -> daemon.StatusResponse
11, // 19: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse 9, // 19: daemon.DaemonService.Down:output_type -> daemon.DownResponse
14, // [14:20] is the sub-list for method output_type 11, // 20: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse
8, // [8:14] is the sub-list for method input_type 15, // [15:21] is the sub-list for method output_type
8, // [8:8] is the sub-list for extension type_name 9, // [9:15] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension extendee 9, // [9:9] is the sub-list for extension type_name
0, // [0:8] is the sub-list for field type_name 9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
} }
func init() { file_daemon_proto_init() } func init() { file_daemon_proto_init() }
@@ -1768,6 +1879,18 @@ func file_daemon_proto_init() {
} }
} }
file_daemon_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { file_daemon_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NSGroupState); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_daemon_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*FullStatus); i { switch v := v.(*FullStatus); i {
case 0: case 0:
return &v.state return &v.state
@@ -1787,7 +1910,7 @@ func file_daemon_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_daemon_proto_rawDesc, RawDescriptor: file_daemon_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 18, NumMessages: 19,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@@ -141,6 +141,7 @@ message PeerState {
int64 bytesRx = 13; int64 bytesRx = 13;
int64 bytesTx = 14; int64 bytesTx = 14;
bool rosenpassEnabled = 15; bool rosenpassEnabled = 15;
repeated string routes = 16;
} }
// LocalPeerState contains the latest state of the local peer // LocalPeerState contains the latest state of the local peer
@@ -151,6 +152,7 @@ message LocalPeerState {
string fqdn = 4; string fqdn = 4;
bool rosenpassEnabled = 5; bool rosenpassEnabled = 5;
bool rosenpassPermissive = 6; bool rosenpassPermissive = 6;
repeated string routes = 7;
} }
// SignalState contains the latest state of a signal connection // SignalState contains the latest state of a signal connection
@@ -174,6 +176,13 @@ message RelayState {
string error = 3; string error = 3;
} }
message NSGroupState {
repeated string servers = 1;
repeated string domains = 2;
bool enabled = 3;
string error = 4;
}
// FullStatus contains the full state held by the Status instance // FullStatus contains the full state held by the Status instance
message FullStatus { message FullStatus {
ManagementState managementState = 1; ManagementState managementState = 1;
@@ -181,4 +190,5 @@ message FullStatus {
LocalPeerState localPeerState = 3; LocalPeerState localPeerState = 3;
repeated PeerState peers = 4; repeated PeerState peers = 4;
repeated RelayState relays = 5; repeated RelayState relays = 5;
repeated NSGroupState dns_servers = 6;
} }

View File

@@ -3,9 +3,16 @@ package server
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"os/exec"
"runtime"
"strconv"
"sync" "sync"
"time" "time"
"github.com/cenkalti/backoff/v4"
"golang.org/x/exp/maps"
"github.com/netbirdio/netbird/client/internal/auth" "github.com/netbirdio/netbird/client/internal/auth"
"github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/client/system"
@@ -21,7 +28,17 @@ import (
"github.com/netbirdio/netbird/version" "github.com/netbirdio/netbird/version"
) )
const probeThreshold = time.Second * 5 const (
probeThreshold = time.Second * 5
retryInitialIntervalVar = "NB_CONN_RETRY_INTERVAL_TIME"
maxRetryIntervalVar = "NB_CONN_MAX_RETRY_INTERVAL_TIME"
maxRetryTimeVar = "NB_CONN_MAX_RETRY_TIME_TIME"
retryMultiplierVar = "NB_CONN_RETRY_MULTIPLIER"
defaultInitialRetryTime = 14 * 24 * time.Hour
defaultMaxRetryInterval = 60 * time.Minute
defaultMaxRetryTime = 14 * 24 * time.Hour
defaultRetryMultiplier = 1.7
)
// Server for service control. // Server for service control.
type Server struct { type Server struct {
@@ -39,6 +56,7 @@ type Server struct {
proto.UnimplementedDaemonServiceServer proto.UnimplementedDaemonServiceServer
statusRecorder *peer.Status statusRecorder *peer.Status
sessionWatcher *internal.SessionWatcher
mgmProbe *internal.Probe mgmProbe *internal.Probe
signalProbe *internal.Probe signalProbe *internal.Probe
@@ -116,17 +134,116 @@ func (s *Server) Start() error {
s.statusRecorder.UpdateManagementAddress(config.ManagementURL.String()) s.statusRecorder.UpdateManagementAddress(config.ManagementURL.String())
s.statusRecorder.UpdateRosenpass(config.RosenpassEnabled, config.RosenpassPermissive) s.statusRecorder.UpdateRosenpass(config.RosenpassEnabled, config.RosenpassPermissive)
if s.sessionWatcher == nil {
s.sessionWatcher = internal.NewSessionWatcher(s.rootCtx, s.statusRecorder)
s.sessionWatcher.SetOnExpireListener(s.onSessionExpire)
}
if !config.DisableAutoConnect { if !config.DisableAutoConnect {
go func() { go s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe)
if err := internal.RunClientWithProbes(ctx, config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe); err != nil {
log.Errorf("init connections: %v", err)
}
}()
} }
return nil return nil
} }
// connectWithRetryRuns runs the client connection with a backoff strategy where we retry the operation as additional
// mechanism to keep the client connected even when the connection is lost.
// we cancel retry if the client receive a stop or down command, or if disable auto connect is configured.
func (s *Server) connectWithRetryRuns(ctx context.Context, config *internal.Config, statusRecorder *peer.Status,
mgmProbe *internal.Probe, signalProbe *internal.Probe, relayProbe *internal.Probe, wgProbe *internal.Probe) {
backOff := getConnectWithBackoff(ctx)
retryStarted := false
go func() {
t := time.NewTicker(24 * time.Hour)
for {
select {
case <-ctx.Done():
t.Stop()
return
case <-t.C:
if retryStarted {
mgmtState := statusRecorder.GetManagementState()
signalState := statusRecorder.GetSignalState()
if mgmtState.Connected && signalState.Connected {
log.Tracef("resetting status")
retryStarted = false
} else {
log.Tracef("not resetting status: mgmt: %v, signal: %v", mgmtState.Connected, signalState.Connected)
}
}
}
}
}()
runOperation := func() error {
log.Tracef("running client connection")
err := internal.RunClientWithProbes(ctx, config, statusRecorder, mgmProbe, signalProbe, relayProbe, wgProbe)
if err != nil {
log.Debugf("run client connection exited with error: %v. Will retry in the background", err)
}
if config.DisableAutoConnect {
return backoff.Permanent(err)
}
if !retryStarted {
retryStarted = true
backOff.Reset()
}
log.Tracef("client connection exited")
return fmt.Errorf("client connection exited")
}
err := backoff.Retry(runOperation, backOff)
if s, ok := gstatus.FromError(err); ok && s.Code() != codes.Canceled {
log.Errorf("received an error when trying to connect: %v", err)
} else {
log.Tracef("retry canceled")
}
}
// getConnectWithBackoff returns a backoff with exponential backoff strategy for connection retries
func getConnectWithBackoff(ctx context.Context) backoff.BackOff {
initialInterval := parseEnvDuration(retryInitialIntervalVar, defaultInitialRetryTime)
maxInterval := parseEnvDuration(maxRetryIntervalVar, defaultMaxRetryInterval)
maxElapsedTime := parseEnvDuration(maxRetryTimeVar, defaultMaxRetryTime)
multiplier := defaultRetryMultiplier
if envValue := os.Getenv(retryMultiplierVar); envValue != "" {
// parse the multiplier from the environment variable string value to float64
value, err := strconv.ParseFloat(envValue, 64)
if err != nil {
log.Warnf("unable to parse environment variable %s: %s. using default: %f", retryMultiplierVar, envValue, multiplier)
} else {
multiplier = value
}
}
return backoff.WithContext(&backoff.ExponentialBackOff{
InitialInterval: initialInterval,
RandomizationFactor: 1,
Multiplier: multiplier,
MaxInterval: maxInterval,
MaxElapsedTime: maxElapsedTime, // 14 days
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}, ctx)
}
// parseEnvDuration parses the environment variable and returns the duration
func parseEnvDuration(envVar string, defaultDuration time.Duration) time.Duration {
if envValue := os.Getenv(envVar); envValue != "" {
if duration, err := time.ParseDuration(envValue); err == nil {
return duration
}
log.Warnf("unable to parse environment variable %s: %s. using default: %s", envVar, envValue, defaultDuration)
}
return defaultDuration
}
// loginAttempt attempts to login using the provided information. it returns a status in case something fails // loginAttempt attempts to login using the provided information. it returns a status in case something fails
func (s *Server) loginAttempt(ctx context.Context, setupKey, jwtToken string) (internal.StatusType, error) { func (s *Server) loginAttempt(ctx context.Context, setupKey, jwtToken string) (internal.StatusType, error) {
var status internal.StatusType var status internal.StatusType
@@ -437,12 +554,7 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes
s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String()) s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String())
s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive) s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive)
go func() { go s.connectWithRetryRuns(ctx, s.config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe)
if err := internal.RunClientWithProbes(ctx, s.config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe); err != nil {
log.Errorf("run client connection: %v", err)
return
}
}()
return &proto.UpResponse{}, nil return &proto.UpResponse{}, nil
} }
@@ -542,13 +654,23 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto
}, nil }, nil
} }
func (s *Server) onSessionExpire() {
if runtime.GOOS != "windows" {
isUIActive := internal.CheckUIApp()
if !isUIActive {
if err := sendTerminalNotification(); err != nil {
log.Errorf("send session expire terminal notification: %v", err)
}
}
}
}
func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus { func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus {
pbFullStatus := proto.FullStatus{ pbFullStatus := proto.FullStatus{
ManagementState: &proto.ManagementState{}, ManagementState: &proto.ManagementState{},
SignalState: &proto.SignalState{}, SignalState: &proto.SignalState{},
LocalPeerState: &proto.LocalPeerState{}, LocalPeerState: &proto.LocalPeerState{},
Peers: []*proto.PeerState{}, Peers: []*proto.PeerState{},
Relays: []*proto.RelayState{},
} }
pbFullStatus.ManagementState.URL = fullStatus.ManagementState.URL pbFullStatus.ManagementState.URL = fullStatus.ManagementState.URL
@@ -569,6 +691,7 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus {
pbFullStatus.LocalPeerState.Fqdn = fullStatus.LocalPeerState.FQDN pbFullStatus.LocalPeerState.Fqdn = fullStatus.LocalPeerState.FQDN
pbFullStatus.LocalPeerState.RosenpassPermissive = fullStatus.RosenpassState.Permissive pbFullStatus.LocalPeerState.RosenpassPermissive = fullStatus.RosenpassState.Permissive
pbFullStatus.LocalPeerState.RosenpassEnabled = fullStatus.RosenpassState.Enabled pbFullStatus.LocalPeerState.RosenpassEnabled = fullStatus.RosenpassState.Enabled
pbFullStatus.LocalPeerState.Routes = maps.Keys(fullStatus.LocalPeerState.Routes)
for _, peerState := range fullStatus.Peers { for _, peerState := range fullStatus.Peers {
pbPeerState := &proto.PeerState{ pbPeerState := &proto.PeerState{
@@ -587,6 +710,7 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus {
BytesRx: peerState.BytesRx, BytesRx: peerState.BytesRx,
BytesTx: peerState.BytesTx, BytesTx: peerState.BytesTx,
RosenpassEnabled: peerState.RosenpassEnabled, RosenpassEnabled: peerState.RosenpassEnabled,
Routes: maps.Keys(peerState.Routes),
} }
pbFullStatus.Peers = append(pbFullStatus.Peers, pbPeerState) pbFullStatus.Peers = append(pbFullStatus.Peers, pbPeerState)
} }
@@ -602,5 +726,47 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus {
pbFullStatus.Relays = append(pbFullStatus.Relays, pbRelayState) pbFullStatus.Relays = append(pbFullStatus.Relays, pbRelayState)
} }
for _, dnsState := range fullStatus.NSGroupStates {
var err string
if dnsState.Error != nil {
err = dnsState.Error.Error()
}
pbDnsState := &proto.NSGroupState{
Servers: dnsState.Servers,
Domains: dnsState.Domains,
Enabled: dnsState.Enabled,
Error: err,
}
pbFullStatus.DnsServers = append(pbFullStatus.DnsServers, pbDnsState)
}
return &pbFullStatus return &pbFullStatus
} }
// sendTerminalNotification sends a terminal notification message
// to inform the user that the NetBird connection session has expired.
func sendTerminalNotification() error {
message := "NetBird connection session expired\n\nPlease re-authenticate to connect to the network."
echoCmd := exec.Command("echo", message)
wallCmd := exec.Command("sudo", "wall")
echoCmdStdout, err := echoCmd.StdoutPipe()
if err != nil {
return err
}
wallCmd.Stdin = echoCmdStdout
if err := echoCmd.Start(); err != nil {
return err
}
if err := wallCmd.Start(); err != nil {
return err
}
if err := echoCmd.Wait(); err != nil {
return err
}
return wallCmd.Wait()
}

View File

@@ -0,0 +1,157 @@
package server
import (
"context"
"net"
"testing"
"time"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
mgmtProto "github.com/netbirdio/netbird/management/proto"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/signal/proto"
signalServer "github.com/netbirdio/netbird/signal/server"
)
var (
kaep = keepalive.EnforcementPolicy{
MinTime: 15 * time.Second,
PermitWithoutStream: true,
}
kasp = keepalive.ServerParameters{
MaxConnectionIdle: 15 * time.Second,
MaxConnectionAgeGrace: 5 * time.Second,
Time: 5 * time.Second,
Timeout: 2 * time.Second,
}
)
// TestConnectWithRetryRuns checks that the connectWithRetry function runs and runs the retries according to the times specified via environment variables
// we will use a management server started via to simulate the server and capture the number of retries
func TestConnectWithRetryRuns(t *testing.T) {
// start the signal server
_, signalAddr, err := startSignal()
if err != nil {
t.Fatalf("failed to start signal server: %v", err)
}
counter := 0
// start the management server
_, mgmtAddr, err := startManagement(t, signalAddr, &counter)
if err != nil {
t.Fatalf("failed to start management server: %v", err)
}
ctx := internal.CtxInitState(context.Background())
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(30*time.Second))
defer cancel()
// create new server
s := New(ctx, t.TempDir()+"/config.json", "debug")
s.latestConfigInput.ManagementURL = "http://" + mgmtAddr
config, err := internal.UpdateOrCreateConfig(s.latestConfigInput)
if err != nil {
t.Fatalf("failed to create config: %v", err)
}
s.config = config
s.statusRecorder = peer.NewRecorder(config.ManagementURL.String())
t.Setenv(retryInitialIntervalVar, "1s")
t.Setenv(maxRetryIntervalVar, "2s")
t.Setenv(maxRetryTimeVar, "5s")
t.Setenv(retryMultiplierVar, "1")
s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe)
if counter < 3 {
t.Fatalf("expected counter > 2, got %d", counter)
}
}
type mockServer struct {
mgmtProto.ManagementServiceServer
counter *int
}
func (m *mockServer) Login(ctx context.Context, req *mgmtProto.EncryptedMessage) (*mgmtProto.EncryptedMessage, error) {
*m.counter++
return m.ManagementServiceServer.Login(ctx, req)
}
func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Server, string, error) {
t.Helper()
dataDir := t.TempDir()
config := &server.Config{
Stuns: []*server.Host{},
TURNConfig: &server.TURNConfig{},
Signal: &server.Host{
Proto: "http",
URI: signalAddr,
},
Datadir: dataDir,
HttpConfig: nil,
}
lis, err := net.Listen("tcp", "localhost:0")
if err != nil {
return nil, "", err
}
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
store, err := server.NewStoreFromJson(config.Datadir, nil)
if err != nil {
return nil, "", err
}
peersUpdateManager := server.NewPeersUpdateManager(nil)
eventStore := &activity.InMemoryEventStore{}
if err != nil {
return nil, "", err
}
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "", eventStore, nil, false)
if err != nil {
return nil, "", err
}
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager, nil, nil)
if err != nil {
return nil, "", err
}
mock := &mockServer{
ManagementServiceServer: mgmtServer,
counter: counter,
}
mgmtProto.RegisterManagementServiceServer(s, mock)
go func() {
if err = s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}()
return s, lis.Addr().String(), nil
}
func startSignal() (*grpc.Server, string, error) {
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
lis, err := net.Listen("tcp", "localhost:0")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
proto.RegisterSignalExchangeServer(s, signalServer.NewServer())
go func() {
if err = s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}()
return s, lis.Addr().String(), nil
}

View File

@@ -30,6 +30,12 @@ type Environment struct {
Platform string Platform string
} }
type Config struct {
RosenpassEnabled bool
RosenpassPermissive bool
ServerSSHAllowed bool
}
// Info is an object that contains machine information // Info is an object that contains machine information
// Most of the code is taken from https://github.com/matishsiao/goInfo // Most of the code is taken from https://github.com/matishsiao/goInfo
type Info struct { type Info struct {
@@ -48,6 +54,14 @@ type Info struct {
SystemProductName string SystemProductName string
SystemManufacturer string SystemManufacturer string
Environment Environment Environment Environment
Config Config
}
// GetInfo retrieves and parses the system information
func GetInfo(ctx context.Context, config Config) *Info {
info := getInfo(ctx)
info.Config = config
return info
} }
// extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context // extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context

View File

@@ -15,8 +15,7 @@ import (
"github.com/netbirdio/netbird/version" "github.com/netbirdio/netbird/version"
) )
// GetInfo retrieves and parses the system information func getInfo(ctx context.Context) *Info {
func GetInfo(ctx context.Context) *Info {
kernel := "android" kernel := "android"
osInfo := uname() osInfo := uname()
if len(osInfo) == 2 { if len(osInfo) == 2 {
@@ -28,7 +27,16 @@ func GetInfo(ctx context.Context) *Info {
kernelVersion = osInfo[2] kernelVersion = osInfo[2]
} }
gio := &Info{Kernel: kernel, Platform: "unknown", OS: "android", OSVersion: osVersion(), GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: kernelVersion} gio := &Info{
Kernel: kernel,
Platform: "unknown",
OS: "android",
OSVersion: osVersion(),
GoOS: runtime.GOOS,
CPUs: runtime.NumCPU(),
KernelVersion: kernelVersion,
}
gio.Hostname = extractDeviceName(ctx, "android") gio.Hostname = extractDeviceName(ctx, "android")
gio.WiretrusteeVersion = version.NetbirdVersion() gio.WiretrusteeVersion = version.NetbirdVersion()
gio.UIVersion = extractUserAgent(ctx) gio.UIVersion = extractUserAgent(ctx)

View File

@@ -20,8 +20,7 @@ import (
"github.com/netbirdio/netbird/version" "github.com/netbirdio/netbird/version"
) )
// GetInfo retrieves and parses the system information func getInfo(ctx context.Context) *Info {
func GetInfo(ctx context.Context) *Info {
utsname := unix.Utsname{} utsname := unix.Utsname{}
err := unix.Uname(&utsname) err := unix.Uname(&utsname)
if err != nil { if err != nil {

View File

@@ -15,8 +15,7 @@ import (
"github.com/netbirdio/netbird/version" "github.com/netbirdio/netbird/version"
) )
// GetInfo retrieves and parses the system information func getInfo(ctx context.Context) *Info {
func GetInfo(ctx context.Context) *Info {
out := _getInfo() out := _getInfo()
for strings.Contains(out, "broken pipe") { for strings.Contains(out, "broken pipe") {
out = _getInfo() out = _getInfo()
@@ -31,7 +30,15 @@ func GetInfo(ctx context.Context) *Info {
Platform: detect_platform.Detect(ctx), Platform: detect_platform.Detect(ctx),
} }
gio := &Info{Kernel: osInfo[0], Platform: runtime.GOARCH, OS: osInfo[2], GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: osInfo[1], Environment: env} gio := &Info{
Kernel: osInfo[0],
Platform: runtime.GOARCH,
OS: osInfo[2],
GoOS: runtime.GOOS,
CPUs: runtime.NumCPU(),
KernelVersion: osInfo[1],
Environment: env,
}
systemHostname, _ := os.Hostname() systemHostname, _ := os.Hostname()
gio.Hostname = extractDeviceName(ctx, systemHostname) gio.Hostname = extractDeviceName(ctx, systemHostname)

View File

@@ -10,14 +10,21 @@ import (
"github.com/netbirdio/netbird/version" "github.com/netbirdio/netbird/version"
) )
// GetInfo retrieves and parses the system information func getInfo(ctx context.Context) *Info {
func GetInfo(ctx context.Context) *Info {
// Convert fixed-size byte arrays to Go strings // Convert fixed-size byte arrays to Go strings
sysName := extractOsName(ctx, "sysName") sysName := extractOsName(ctx, "sysName")
swVersion := extractOsVersion(ctx, "swVersion") swVersion := extractOsVersion(ctx, "swVersion")
gio := &Info{Kernel: sysName, OSVersion: swVersion, Platform: "unknown", OS: sysName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: swVersion} gio := &Info{
Kernel: sysName,
OSVersion: swVersion,
Platform: "unknown",
OS: sysName,
GoOS: runtime.GOOS,
CPUs: runtime.NumCPU(),
KernelVersion: swVersion,
}
gio.Hostname = extractDeviceName(ctx, "hostname") gio.Hostname = extractDeviceName(ctx, "hostname")
gio.WiretrusteeVersion = version.NetbirdVersion() gio.WiretrusteeVersion = version.NetbirdVersion()
gio.UIVersion = extractUserAgent(ctx) gio.UIVersion = extractUserAgent(ctx)

View File

@@ -20,8 +20,7 @@ import (
"github.com/netbirdio/netbird/version" "github.com/netbirdio/netbird/version"
) )
// GetInfo retrieves and parses the system information func getInfo(ctx context.Context) *Info {
func GetInfo(ctx context.Context) *Info {
info := _getInfo() info := _getInfo()
for strings.Contains(info, "broken pipe") { for strings.Contains(info, "broken pipe") {
info = _getInfo() info = _getInfo()

View File

@@ -8,7 +8,6 @@ import (
"strings" "strings"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/yusufpapurcu/wmi"
"golang.org/x/sys/windows/registry" "golang.org/x/sys/windows/registry"
"github.com/netbirdio/netbird/client/system/detect_cloud" "github.com/netbirdio/netbird/client/system/detect_cloud"
@@ -32,8 +31,7 @@ type Win32_BIOS struct {
SerialNumber string SerialNumber string
} }
// GetInfo retrieves and parses the system information func getInfo(ctx context.Context) *Info {
func GetInfo(ctx context.Context) *Info {
osName, osVersion := getOSNameAndVersion() osName, osVersion := getOSNameAndVersion()
buildVersion := getBuildVersion() buildVersion := getBuildVersion()
@@ -165,6 +163,10 @@ func sysProductName() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
// `ComputerSystemProduct` could be empty on some virtualized systems
if len(dst) < 1 {
return "unknown", nil
}
return dst[0].Name, nil return dst[0].Name, nil
} }

View File

@@ -61,7 +61,7 @@ func main() {
flag.Parse() flag.Parse()
a := app.New() a := app.NewWithID("NetBird")
a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnectedPNG)) a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnectedPNG))
client := newServiceClient(daemonAddr, a, showSettings) client := newServiceClient(daemonAddr, a, showSettings)
@@ -82,17 +82,23 @@ var iconConnectedICO []byte
//go:embed netbird-systemtray-connected.png //go:embed netbird-systemtray-connected.png
var iconConnectedPNG []byte var iconConnectedPNG []byte
//go:embed netbird-systemtray-default.ico //go:embed netbird-systemtray-disconnected.ico
var iconDisconnectedICO []byte var iconDisconnectedICO []byte
//go:embed netbird-systemtray-default.png //go:embed netbird-systemtray-disconnected.png
var iconDisconnectedPNG []byte var iconDisconnectedPNG []byte
//go:embed netbird-systemtray-update.ico //go:embed netbird-systemtray-update-disconnected.ico
var iconUpdateICO []byte var iconUpdateDisconnectedICO []byte
//go:embed netbird-systemtray-update.png //go:embed netbird-systemtray-update-disconnected.png
var iconUpdatePNG []byte var iconUpdateDisconnectedPNG []byte
//go:embed netbird-systemtray-update-connected.ico
var iconUpdateConnectedICO []byte
//go:embed netbird-systemtray-update-connected.png
var iconUpdateConnectedPNG []byte
//go:embed netbird-systemtray-update-cloud.ico //go:embed netbird-systemtray-update-cloud.ico
var iconUpdateCloudICO []byte var iconUpdateCloudICO []byte
@@ -105,10 +111,11 @@ type serviceClient struct {
addr string addr string
conn proto.DaemonServiceClient conn proto.DaemonServiceClient
icConnected []byte icConnected []byte
icDisconnected []byte icDisconnected []byte
icUpdate []byte icUpdateConnected []byte
icUpdateCloud []byte icUpdateDisconnected []byte
icUpdateCloud []byte
// systray menu items // systray menu items
mStatus *systray.MenuItem mStatus *systray.MenuItem
@@ -123,9 +130,10 @@ type serviceClient struct {
mQuit *systray.MenuItem mQuit *systray.MenuItem
// application with main windows. // application with main windows.
app fyne.App app fyne.App
wSettings fyne.Window wSettings fyne.Window
showSettings bool showSettings bool
sendNotification bool
// input elements for settings form // input elements for settings form
iMngURL *widget.Entry iMngURL *widget.Entry
@@ -139,6 +147,7 @@ type serviceClient struct {
preSharedKey string preSharedKey string
adminURL string adminURL string
connected bool
update *version.Update update *version.Update
daemonVersion string daemonVersion string
updateIndicationLock sync.Mutex updateIndicationLock sync.Mutex
@@ -150,9 +159,10 @@ type serviceClient struct {
// This constructor also builds the UI elements for the settings window. // This constructor also builds the UI elements for the settings window.
func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient { func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient {
s := &serviceClient{ s := &serviceClient{
ctx: context.Background(), ctx: context.Background(),
addr: addr, addr: addr,
app: a, app: a,
sendNotification: false,
showSettings: showSettings, showSettings: showSettings,
update: version.NewUpdate(), update: version.NewUpdate(),
@@ -161,13 +171,15 @@ func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
s.icConnected = iconConnectedICO s.icConnected = iconConnectedICO
s.icDisconnected = iconDisconnectedICO s.icDisconnected = iconDisconnectedICO
s.icUpdate = iconUpdateICO s.icUpdateConnected = iconUpdateConnectedICO
s.icUpdateDisconnected = iconUpdateDisconnectedICO
s.icUpdateCloud = iconUpdateCloudICO s.icUpdateCloud = iconUpdateCloudICO
} else { } else {
s.icConnected = iconConnectedPNG s.icConnected = iconConnectedPNG
s.icDisconnected = iconDisconnectedPNG s.icDisconnected = iconDisconnectedPNG
s.icUpdate = iconUpdatePNG s.icUpdateConnected = iconUpdateConnectedPNG
s.icUpdateDisconnected = iconUpdateDisconnectedPNG
s.icUpdateCloud = iconUpdateCloudPNG s.icUpdateCloud = iconUpdateCloudPNG
} }
@@ -367,9 +379,18 @@ func (s *serviceClient) updateStatus() error {
s.updateIndicationLock.Lock() s.updateIndicationLock.Lock()
defer s.updateIndicationLock.Unlock() defer s.updateIndicationLock.Unlock()
// notify the user when the session has expired
if status.Status == string(internal.StatusNeedsLogin) {
s.onSessionExpire()
}
var systrayIconState bool var systrayIconState bool
if status.Status == string(internal.StatusConnected) && !s.mUp.Disabled() { if status.Status == string(internal.StatusConnected) && !s.mUp.Disabled() {
if !s.isUpdateIconActive { s.connected = true
s.sendNotification = true
if s.isUpdateIconActive {
systray.SetIcon(s.icUpdateConnected)
} else {
systray.SetIcon(s.icConnected) systray.SetIcon(s.icConnected)
} }
systray.SetTooltip("NetBird (Connected)") systray.SetTooltip("NetBird (Connected)")
@@ -378,7 +399,10 @@ func (s *serviceClient) updateStatus() error {
s.mDown.Enable() s.mDown.Enable()
systrayIconState = true systrayIconState = true
} else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() { } else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() {
if !s.isUpdateIconActive { s.connected = false
if s.isUpdateIconActive {
systray.SetIcon(s.icUpdateDisconnected)
} else {
systray.SetIcon(s.icDisconnected) systray.SetIcon(s.icDisconnected)
} }
systray.SetTooltip("NetBird (Disconnected)") systray.SetTooltip("NetBird (Disconnected)")
@@ -605,10 +629,30 @@ func (s *serviceClient) onUpdateAvailable() {
defer s.updateIndicationLock.Unlock() defer s.updateIndicationLock.Unlock()
s.mUpdate.Show() s.mUpdate.Show()
s.mAbout.SetIcon(s.icUpdateCloud)
s.isUpdateIconActive = true s.isUpdateIconActive = true
systray.SetIcon(s.icUpdate)
if s.connected {
systray.SetIcon(s.icUpdateConnected)
} else {
systray.SetIcon(s.icUpdateDisconnected)
}
}
// onSessionExpire sends a notification to the user when the session expires.
func (s *serviceClient) onSessionExpire() {
if s.sendNotification {
title := "Connection session expired"
if runtime.GOOS == "darwin" {
title = "NetBird connection session expired"
}
s.app.SendNotification(
fyne.NewNotification(
title,
"Please re-authenticate to connect to the network",
),
)
s.sendNotification = false
}
} }
func openURL(url string) error { func openURL(url string) error {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

2
go.mod
View File

@@ -48,6 +48,7 @@ require (
github.com/google/gopacket v1.1.19 github.com/google/gopacket v1.1.19
github.com/google/nftables v0.0.0-20220808154552-2eca00135732 github.com/google/nftables v0.0.0-20220808154552-2eca00135732
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 github.com/hashicorp/go-secure-stdlib/base62 v0.1.2
github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/go-version v1.6.0
github.com/libp2p/go-netroute v0.2.0 github.com/libp2p/go-netroute v0.2.0
@@ -123,6 +124,7 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.10.0 // indirect github.com/googleapis/gax-go/v2 v2.10.0 // indirect
github.com/gopacket/gopacket v1.1.1 // indirect github.com/gopacket/gopacket v1.1.1 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect

4
go.sum
View File

@@ -289,6 +289,10 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357 h1:Fkzd8ktnpOR9h47SXHe2AYPwelXLH2GjGsjlAloiWfo= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357 h1:Fkzd8ktnpOR9h47SXHe2AYPwelXLH2GjGsjlAloiWfo=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357/go.mod h1:w9Y7gY31krpLmrVU5ZPG9H7l9fZuRu5/3R3S3FMtVQ4= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357/go.mod h1:w9Y7gY31krpLmrVU5ZPG9H7l9fZuRu5/3R3S3FMtVQ4=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng= github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= github.com/hashicorp/go-secure-stdlib/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=

View File

@@ -8,7 +8,7 @@ import (
"golang.zx2c4.com/wireguard/tun/netstack" "golang.zx2c4.com/wireguard/tun/netstack"
) )
type NetStackTun struct { type NetStackTun struct { //nolint:revive
address string address string
mtu int mtu int
listenAddress string listenAddress string

View File

@@ -43,21 +43,18 @@ download_geolite_mmdb() {
mkdir -p "$EXTRACTION_DIR" mkdir -p "$EXTRACTION_DIR"
tar -xzvf "$DATABASE_FILE" > /dev/null 2>&1 tar -xzvf "$DATABASE_FILE" > /dev/null 2>&1
# Create a SHA256 signature file
MMDB_FILE="GeoLite2-City.mmdb" MMDB_FILE="GeoLite2-City.mmdb"
cd "$EXTRACTION_DIR" cp "$EXTRACTION_DIR"/"$MMDB_FILE" $MMDB_FILE
sha256sum "$MMDB_FILE" > "$MMDB_FILE.sha256"
echo "SHA256 signature created for $MMDB_FILE."
cd - > /dev/null 2>&1
# Remove downloaded files # Remove downloaded files
rm -r "$EXTRACTION_DIR"
rm "$DATABASE_FILE" "$SIGNATURE_FILE" rm "$DATABASE_FILE" "$SIGNATURE_FILE"
# Done. Print next steps # Done. Print next steps
echo "" echo ""
echo "Process completed successfully." echo "Process completed successfully."
echo "Now you can place $EXTRACTION_DIR/$MMDB_FILE to 'datadir' of management service." echo "Now you can place $MMDB_FILE to 'datadir' of management service."
echo -e "Example:\n\tdocker compose cp $EXTRACTION_DIR/$MMDB_FILE management:/var/lib/netbird/" echo -e "Example:\n\tdocker compose cp $MMDB_FILE management:/var/lib/netbird/"
} }

View File

@@ -137,6 +137,13 @@ create_new_application() {
BASE_REDIRECT_URL2=$5 BASE_REDIRECT_URL2=$5
LOGOUT_URL=$6 LOGOUT_URL=$6
ZITADEL_DEV_MODE=$7 ZITADEL_DEV_MODE=$7
DEVICE_CODE=$8
if [[ $DEVICE_CODE == "true" ]]; then
GRANT_TYPES='["OIDC_GRANT_TYPE_AUTHORIZATION_CODE","OIDC_GRANT_TYPE_DEVICE_CODE","OIDC_GRANT_TYPE_REFRESH_TOKEN"]'
else
GRANT_TYPES='["OIDC_GRANT_TYPE_AUTHORIZATION_CODE","OIDC_GRANT_TYPE_REFRESH_TOKEN"]'
fi
RESPONSE=$( RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/projects/$PROJECT_ID/apps/oidc" \ curl -sS -X POST "$INSTANCE_URL/management/v1/projects/$PROJECT_ID/apps/oidc" \
@@ -154,10 +161,7 @@ create_new_application() {
"RESPONSETypes": [ "RESPONSETypes": [
"OIDC_RESPONSE_TYPE_CODE" "OIDC_RESPONSE_TYPE_CODE"
], ],
"grantTypes": [ "grantTypes": '"$GRANT_TYPES"',
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
"OIDC_GRANT_TYPE_REFRESH_TOKEN"
],
"appType": "OIDC_APP_TYPE_USER_AGENT", "appType": "OIDC_APP_TYPE_USER_AGENT",
"authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE", "authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
"version": "OIDC_VERSION_1_0", "version": "OIDC_VERSION_1_0",
@@ -340,10 +344,10 @@ init_zitadel() {
# create zitadel spa applications # create zitadel spa applications
echo "Creating new Zitadel SPA Dashboard application" echo "Creating new Zitadel SPA Dashboard application"
DASHBOARD_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Dashboard" "$BASE_REDIRECT_URL/nb-auth" "$BASE_REDIRECT_URL/nb-silent-auth" "$BASE_REDIRECT_URL/" "$ZITADEL_DEV_MODE") DASHBOARD_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Dashboard" "$BASE_REDIRECT_URL/nb-auth" "$BASE_REDIRECT_URL/nb-silent-auth" "$BASE_REDIRECT_URL/" "$ZITADEL_DEV_MODE" "false")
echo "Creating new Zitadel SPA Cli application" echo "Creating new Zitadel SPA Cli application"
CLI_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Cli" "http://localhost:53000/" "http://localhost:54000/" "http://localhost:53000/" "true") CLI_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Cli" "http://localhost:53000/" "http://localhost:54000/" "http://localhost:53000/" "true" "true")
MACHINE_USER_ID=$(create_service_user "$INSTANCE_URL" "$PAT") MACHINE_USER_ID=$(create_service_user "$INSTANCE_URL" "$PAT")
@@ -561,6 +565,8 @@ renderCaddyfile() {
reverse_proxy /.well-known/openid-configuration h2c://zitadel:8080 reverse_proxy /.well-known/openid-configuration h2c://zitadel:8080
reverse_proxy /openapi/* h2c://zitadel:8080 reverse_proxy /openapi/* h2c://zitadel:8080
reverse_proxy /debug/* h2c://zitadel:8080 reverse_proxy /debug/* h2c://zitadel:8080
reverse_proxy /device/* h2c://zitadel:8080
reverse_proxy /device h2c://zitadel:8080
# Dashboard # Dashboard
reverse_proxy /* dashboard:80 reverse_proxy /* dashboard:80
} }
@@ -629,6 +635,14 @@ renderManagementJson() {
"ManagementEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/management/v1" "ManagementEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/management/v1"
} }
}, },
"DeviceAuthorizationFlow": {
"Provider": "hosted",
"ProviderConfig": {
"Audience": "$NETBIRD_AUTH_CLIENT_ID_CLI",
"ClientID": "$NETBIRD_AUTH_CLIENT_ID_CLI",
"Scope": "openid"
}
},
"PKCEAuthorizationFlow": { "PKCEAuthorizationFlow": {
"ProviderConfig": { "ProviderConfig": {
"Audience": "$NETBIRD_AUTH_CLIENT_ID_CLI", "Audience": "$NETBIRD_AUTH_CLIENT_ID_CLI",

View File

@@ -26,6 +26,13 @@
"Username": "", "Username": "",
"Password": null "Password": null
}, },
"ReverseProxy": {
"TrustedHTTPProxies": [],
"TrustedHTTPProxiesCount": 0,
"TrustedPeers": [
"0.0.0.0/0"
]
},
"Datadir": "", "Datadir": "",
"DataStoreEncryptionKey": "$NETBIRD_DATASTORE_ENC_KEY", "DataStoreEncryptionKey": "$NETBIRD_DATASTORE_ENC_KEY",
"StoreConfig": { "StoreConfig": {

View File

@@ -46,6 +46,7 @@ server {
proxy_set_header X-Scheme $scheme; proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Host $host;
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Proxy dashboard # Proxy dashboard
location / { location / {

View File

@@ -163,7 +163,7 @@ func TestClient_LoginUnregistered_ShouldThrow_401(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
sysInfo := system.GetInfo(context.TODO()) sysInfo := &system.Info{Hostname: "test"}
_, err = client.Login(*key, sysInfo, nil) _, err = client.Login(*key, sysInfo, nil)
if err == nil { if err == nil {
t.Error("expecting err on unregistered login, got nil") t.Error("expecting err on unregistered login, got nil")
@@ -191,7 +191,7 @@ func TestClient_LoginRegistered(t *testing.T) {
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
info := system.GetInfo(context.TODO()) info := &system.Info{Hostname: "test"}
resp, err := client.Register(*key, ValidKey, "", info, nil) resp, err := client.Register(*key, ValidKey, "", info, nil)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@@ -221,7 +221,7 @@ func TestClient_Sync(t *testing.T) {
t.Error(err) t.Error(err)
} }
info := system.GetInfo(context.TODO()) info := &system.Info{Hostname: "test"}
_, err = client.Register(*serverKey, ValidKey, "", info, nil) _, err = client.Register(*serverKey, ValidKey, "", info, nil)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@@ -237,7 +237,6 @@ func TestClient_Sync(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
info = system.GetInfo(context.TODO())
_, err = remoteClient.Register(*serverKey, ValidKey, "", info, nil) _, err = remoteClient.Register(*serverKey, ValidKey, "", info, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -335,7 +334,7 @@ func Test_SystemMetaDataFromClient(t *testing.T) {
}, nil }, nil
} }
info := system.GetInfo(context.TODO()) info := &system.Info{Hostname: "test"}
_, err = testClient.Register(*key, ValidKey, "", info, nil) _, err = testClient.Register(*key, ValidKey, "", info, nil)
if err != nil { if err != nil {
t.Errorf("error while trying to register client: %v", err) t.Errorf("error while trying to register client: %v", err)
@@ -363,10 +362,11 @@ func Test_SystemMetaDataFromClient(t *testing.T) {
WiretrusteeVersion: info.WiretrusteeVersion, WiretrusteeVersion: info.WiretrusteeVersion,
KernelVersion: info.KernelVersion, KernelVersion: info.KernelVersion,
NetworkAddresses: protoNetAddr, NetworkAddresses: protoNetAddr,
SysSerialNumber: info.SystemSerialNumber, SysSerialNumber: info.SystemSerialNumber,
SysProductName: info.SystemProductName, SysProductName: info.SystemProductName,
SysManufacturer: info.SystemManufacturer, SysManufacturer: info.SystemManufacturer,
Environment: &mgmtProto.Environment{Cloud: info.Environment.Cloud, Platform: info.Environment.Platform},
} }
assert.Equal(t, ValidKey, actualValidKey) assert.Equal(t, ValidKey, actualValidKey)
@@ -407,7 +407,9 @@ func isEqual(a, b *mgmtProto.PeerSystemMeta) bool {
a.GetUiVersion() == b.GetUiVersion() && a.GetUiVersion() == b.GetUiVersion() &&
a.GetSysSerialNumber() == b.GetSysSerialNumber() && a.GetSysSerialNumber() == b.GetSysSerialNumber() &&
a.GetSysProductName() == b.GetSysProductName() && a.GetSysProductName() == b.GetSysProductName() &&
a.GetSysManufacturer() == b.GetSysManufacturer() a.GetSysManufacturer() == b.GetSysManufacturer() &&
a.GetEnvironment().Cloud == b.GetEnvironment().Cloud &&
a.GetEnvironment().Platform == b.GetEnvironment().Platform
} }
func Test_GetDeviceAuthorizationFlow(t *testing.T) { func Test_GetDeviceAuthorizationFlow(t *testing.T) {

View File

@@ -26,6 +26,8 @@ import (
"github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/proto"
) )
const ConnectTimeout = 10 * time.Second
// ConnStateNotifier is a wrapper interface of the status recorders // ConnStateNotifier is a wrapper interface of the status recorders
type ConnStateNotifier interface { type ConnStateNotifier interface {
MarkManagementDisconnected(error) MarkManagementDisconnected(error)
@@ -49,7 +51,7 @@ func NewClient(ctx context.Context, addr string, ourPrivateKey wgtypes.Key, tlsE
transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})) transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))
} }
mgmCtx, cancel := context.WithTimeout(ctx, 5*time.Second) mgmCtx, cancel := context.WithTimeout(ctx, ConnectTimeout)
defer cancel() defer cancel()
conn, err := grpc.DialContext( conn, err := grpc.DialContext(
mgmCtx, mgmCtx,
@@ -318,7 +320,7 @@ func (c *GrpcClient) login(serverKey wgtypes.Key, req *proto.LoginRequest) (*pro
log.Errorf("failed to encrypt message: %s", err) log.Errorf("failed to encrypt message: %s", err)
return nil, err return nil, err
} }
mgmCtx, cancel := context.WithTimeout(c.ctx, 5*time.Second) mgmCtx, cancel := context.WithTimeout(c.ctx, ConnectTimeout)
defer cancel() defer cancel()
resp, err := c.realClient.Login(mgmCtx, &proto.EncryptedMessage{ resp, err := c.realClient.Login(mgmCtx, &proto.EncryptedMessage{
WgPubKey: c.key.PublicKey().String(), WgPubKey: c.key.PublicKey().String(),
@@ -474,5 +476,14 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta {
SysSerialNumber: info.SystemSerialNumber, SysSerialNumber: info.SystemSerialNumber,
SysManufacturer: info.SystemManufacturer, SysManufacturer: info.SystemManufacturer,
SysProductName: info.SystemProductName, SysProductName: info.SystemProductName,
Environment: &proto.Environment{
Cloud: info.Environment.Cloud,
Platform: info.Environment.Platform,
},
Config: &proto.Config{
RosenpassEnabled: info.Config.RosenpassEnabled,
RosenpassPermissive: info.Config.RosenpassPermissive,
ServerSSHAllowed: info.Config.ServerSSHAllowed,
},
} }
} }

View File

@@ -43,6 +43,7 @@ import (
"github.com/netbirdio/netbird/management/server/metrics" "github.com/netbirdio/netbird/management/server/metrics"
"github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/telemetry"
"github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/util"
"github.com/netbirdio/netbird/version"
) )
// ManagementLegacyPort is the port that was used before by the Management gRPC server. // ManagementLegacyPort is the port that was used before by the Management gRPC server.
@@ -166,7 +167,7 @@ var (
geo, err := geolocation.NewGeolocation(config.Datadir) geo, err := geolocation.NewGeolocation(config.Datadir)
if err != nil { if err != nil {
log.Warnf("could not initialize geo location service, we proceed without geo support") log.Warnf("could not initialize geo location service: %v, we proceed without geo support", err)
} else { } else {
log.Infof("geo location service has been initialized from %s", config.Datadir) log.Infof("geo location service has been initialized from %s", config.Datadir)
} }
@@ -315,6 +316,7 @@ var (
} }
} }
log.Infof("management server version %s", version.NetbirdVersion())
log.Infof("running HTTP server and gRPC server on the same port: %s", listener.Addr().String()) log.Infof("running HTTP server and gRPC server on the same port: %s", listener.Addr().String())
serveGRPCWithHTTP(listener, rootHandler, tlsEnabled) serveGRPCWithHTTP(listener, rootHandler, tlsEnabled)

File diff suppressed because it is too large Load Diff

View File

@@ -92,6 +92,21 @@ message PeerKeys {
bytes wgPubKey = 2; bytes wgPubKey = 2;
} }
// Environment is part of the PeerSystemMeta and describes the environment the agent is running in.
message Environment {
// cloud is the cloud provider the agent is running in if applicable.
string cloud = 1;
// platform is the platform the agent is running on if applicable.
string platform = 2;
}
// Config is a message with local configuration settings of the peer
message Config {
bool rosenpassEnabled = 1;
bool rosenpassPermissive = 2;
bool serverSSHAllowed = 3;
}
// PeerSystemMeta is machine meta data like OS and version. // PeerSystemMeta is machine meta data like OS and version.
message PeerSystemMeta { message PeerSystemMeta {
string hostname = 1; string hostname = 1;
@@ -108,6 +123,8 @@ message PeerSystemMeta {
string sysSerialNumber = 12; string sysSerialNumber = 12;
string sysProductName = 13; string sysProductName = 13;
string sysManufacturer = 14; string sysManufacturer = 14;
Environment environment = 15;
Config config = 16;
} }
message LoginResponse { message LoginResponse {

View File

@@ -72,7 +72,6 @@ type AccountManager interface {
CheckUserAccessByJWTGroups(claims jwtclaims.AuthorizationClaims) error CheckUserAccessByJWTGroups(claims jwtclaims.AuthorizationClaims) error
GetAccountFromPAT(pat string) (*Account, *User, *PersonalAccessToken, error) GetAccountFromPAT(pat string) (*Account, *User, *PersonalAccessToken, error)
DeleteAccount(accountID, userID string) error DeleteAccount(accountID, userID string) error
GetUsage(ctx context.Context, accountID string, start time.Time, end time.Time) (*AccountUsageStats, error)
MarkPATUsed(tokenID string) error MarkPATUsed(tokenID string) error
GetUser(claims jwtclaims.AuthorizationClaims) (*User, error) GetUser(claims jwtclaims.AuthorizationClaims) (*User, error)
ListUsers(accountID string) ([]*User, error) ListUsers(accountID string) ([]*User, error)
@@ -126,6 +125,7 @@ type AccountManager interface {
SavePostureChecks(accountID, userID string, postureChecks *posture.Checks) error SavePostureChecks(accountID, userID string, postureChecks *posture.Checks) error
DeletePostureChecks(accountID, postureChecksID, userID string) error DeletePostureChecks(accountID, postureChecksID, userID string) error
ListPostureChecks(accountID, userID string) ([]*posture.Checks, error) ListPostureChecks(accountID, userID string) ([]*posture.Checks, error)
GetIdpManager() idp.Manager
} }
type DefaultAccountManager struct { type DefaultAccountManager struct {
@@ -205,6 +205,7 @@ type Account struct {
// User.Id it was created by // User.Id it was created by
CreatedBy string CreatedBy string
CreatedAt time.Time
Domain string `gorm:"index"` Domain string `gorm:"index"`
DomainCategory string DomainCategory string
IsDomainPrimaryAccount bool IsDomainPrimaryAccount bool
@@ -231,14 +232,6 @@ type Account struct {
RulesG []Rule `json:"-" gorm:"-"` RulesG []Rule `json:"-" gorm:"-"`
} }
// AccountUsageStats represents the current usage statistics for an account
type AccountUsageStats struct {
ActiveUsers int64 `json:"active_users"`
TotalUsers int64 `json:"total_users"`
ActivePeers int64 `json:"active_peers"`
TotalPeers int64 `json:"total_peers"`
}
type UserInfo struct { type UserInfo struct {
ID string `json:"id"` ID string `json:"id"`
Email string `json:"email"` Email string `json:"email"`
@@ -462,6 +455,11 @@ func (a *Account) GetNextPeerExpiration() (time.Duration, bool) {
} }
_, duration := peer.LoginExpired(a.Settings.PeerLoginExpiration) _, duration := peer.LoginExpired(a.Settings.PeerLoginExpiration)
if nextExpiry == nil || duration < *nextExpiry { if nextExpiry == nil || duration < *nextExpiry {
// if expiration is below 1s return 1s duration
// this avoids issues with ticker that can't be set to < 0
if duration < time.Second {
return time.Second, true
}
nextExpiry = &duration nextExpiry = &duration
} }
} }
@@ -683,6 +681,7 @@ func (a *Account) Copy() *Account {
return &Account{ return &Account{
Id: a.Id, Id: a.Id,
CreatedBy: a.CreatedBy, CreatedBy: a.CreatedBy,
CreatedAt: a.CreatedAt,
Domain: a.Domain, Domain: a.Domain,
DomainCategory: a.DomainCategory, DomainCategory: a.DomainCategory,
IsDomainPrimaryAccount: a.IsDomainPrimaryAccount, IsDomainPrimaryAccount: a.IsDomainPrimaryAccount,
@@ -900,6 +899,10 @@ func (am *DefaultAccountManager) GetExternalCacheManager() ExternalCacheManager
return am.externalCacheManager return am.externalCacheManager
} }
func (am *DefaultAccountManager) GetIdpManager() idp.Manager {
return am.idpManager
}
// UpdateAccountSettings updates Account settings. // UpdateAccountSettings updates Account settings.
// Only users with role UserRoleAdmin can update the account. // Only users with role UserRoleAdmin can update the account.
// User that performs the update has to belong to the account. // User that performs the update has to belong to the account.
@@ -917,12 +920,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(accountID, userID string,
unlock := am.Store.AcquireAccountLock(accountID) unlock := am.Store.AcquireAccountLock(accountID)
defer unlock() defer unlock()
account, err := am.Store.GetAccountByUser(userID) account, err := am.Store.GetAccount(accountID)
if err != nil {
return nil, err
}
err = additions.ValidateExtraSettings(newSettings.Extra, account.Settings.Extra, account.Peers, userID, accountID, am.eventStore)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -936,6 +934,11 @@ func (am *DefaultAccountManager) UpdateAccountSettings(accountID, userID string,
return nil, status.Errorf(status.PermissionDenied, "user is not allowed to update account") return nil, status.Errorf(status.PermissionDenied, "user is not allowed to update account")
} }
err = additions.ValidateExtraSettings(newSettings.Extra, account.Settings.Extra, account.Peers, userID, accountID, am.eventStore)
if err != nil {
return nil, err
}
oldSettings := account.Settings oldSettings := account.Settings
if oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled { if oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled {
event := activity.AccountPeerLoginExpirationEnabled event := activity.AccountPeerLoginExpirationEnabled
@@ -1114,17 +1117,6 @@ func (am *DefaultAccountManager) DeleteAccount(accountID, userID string) error {
return nil return nil
} }
// GetUsage returns the usage stats for the given account.
// This cannot be used to calculate usage stats for a period in the past as it relies on peers' last seen time.
func (am *DefaultAccountManager) GetUsage(ctx context.Context, accountID string, start time.Time, end time.Time) (*AccountUsageStats, error) {
usageStats, err := am.Store.CalculateUsageStats(ctx, accountID, start, end)
if err != nil {
return nil, fmt.Errorf("failed to calculate usage stats: %w", err)
}
return usageStats, nil
}
// GetAccountByUserOrAccountID looks for an account by user or accountID, if no account is provided and // GetAccountByUserOrAccountID looks for an account by user or accountID, if no account is provided and
// userID doesn't have an account associated with it, one account is created // userID doesn't have an account associated with it, one account is created
// domain is used to create a new account if no account is found // domain is used to create a new account if no account is found
@@ -1870,6 +1862,7 @@ func newAccountWithId(accountID, userID, domain string) *Account {
acc := &Account{ acc := &Account{
Id: accountID, Id: accountID,
CreatedAt: time.Now().UTC(),
SetupKeys: setupKeys, SetupKeys: setupKeys,
Network: network, Network: network,
Peers: peers, Peers: peers,

View File

@@ -94,6 +94,10 @@ func verifyNewAccountHasDefaultFields(t *testing.T, account *Account, createdBy
t.Errorf("expecting newly created account to be created by user %s, got %s", createdBy, account.CreatedBy) t.Errorf("expecting newly created account to be created by user %s, got %s", createdBy, account.CreatedBy)
} }
if account.CreatedAt.IsZero() {
t.Errorf("expecting newly created account to have a non-zero creation time")
}
if account.Domain != domain { if account.Domain != domain {
t.Errorf("expecting newly created account to have domain %s, got %s", domain, account.Domain) t.Errorf("expecting newly created account to have domain %s, got %s", domain, account.Domain)
} }
@@ -1473,6 +1477,7 @@ func TestAccount_Copy(t *testing.T) {
account := &Account{ account := &Account{
Id: "account1", Id: "account1",
CreatedBy: "tester", CreatedBy: "tester",
CreatedAt: time.Now().UTC(),
Domain: "test.com", Domain: "test.com",
DomainCategory: "public", DomainCategory: "public",
IsDomainPrimaryAccount: true, IsDomainPrimaryAccount: true,

View File

@@ -9,7 +9,7 @@ const (
) )
// ActivityDescriber is an interface that describes an activity // ActivityDescriber is an interface that describes an activity
type ActivityDescriber interface { type ActivityDescriber interface { //nolint:revive
StringCode() string StringCode() string
Message() string Message() string
} }

View File

@@ -1,8 +1,6 @@
package server package server
import ( import (
"context"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -664,40 +662,3 @@ func (s *FileStore) Close() error {
func (s *FileStore) GetStoreEngine() StoreEngine { func (s *FileStore) GetStoreEngine() StoreEngine {
return FileStoreEngine return FileStoreEngine
} }
// CalculateUsageStats returns the usage stats for an account
// start and end are inclusive.
func (s *FileStore) CalculateUsageStats(_ context.Context, accountID string, start time.Time, end time.Time) (*AccountUsageStats, error) {
s.mux.Lock()
defer s.mux.Unlock()
account, exists := s.Accounts[accountID]
if !exists {
return nil, fmt.Errorf("account not found")
}
stats := &AccountUsageStats{
TotalUsers: 0,
TotalPeers: int64(len(account.Peers)),
}
for _, user := range account.Users {
if !user.IsServiceUser {
stats.TotalUsers++
}
}
activeUsers := make(map[string]bool)
for _, peer := range account.Peers {
lastSeen := peer.Status.LastSeen
if lastSeen.Compare(start) >= 0 && lastSeen.Compare(end) <= 0 {
if _, exists := account.Users[peer.UserID]; exists && !activeUsers[peer.UserID] {
activeUsers[peer.UserID] = true
stats.ActiveUsers++
}
stats.ActivePeers++
}
}
return stats, nil
}

View File

@@ -1,7 +1,6 @@
package server package server
import ( import (
"context"
"crypto/sha256" "crypto/sha256"
"net" "net"
"path/filepath" "path/filepath"
@@ -658,32 +657,3 @@ func newStore(t *testing.T) *FileStore {
return store return store
} }
func TestFileStore_CalculateUsageStats(t *testing.T) {
storeDir := t.TempDir()
err := util.CopyFileContents("testdata/store_stats.json", filepath.Join(storeDir, "store.json"))
require.NoError(t, err)
store, err := NewFileStore(storeDir, nil)
require.NoError(t, err)
startDate := time.Date(2024, time.February, 1, 0, 0, 0, 0, time.UTC)
endDate := startDate.AddDate(0, 1, 0).Add(-time.Nanosecond)
stats1, err := store.CalculateUsageStats(context.TODO(), "account-1", startDate, endDate)
require.NoError(t, err)
assert.Equal(t, int64(2), stats1.ActiveUsers)
assert.Equal(t, int64(4), stats1.TotalUsers)
assert.Equal(t, int64(3), stats1.ActivePeers)
assert.Equal(t, int64(7), stats1.TotalPeers)
stats2, err := store.CalculateUsageStats(context.TODO(), "account-2", startDate, endDate)
require.NoError(t, err)
assert.Equal(t, int64(1), stats2.ActiveUsers)
assert.Equal(t, int64(2), stats2.TotalUsers)
assert.Equal(t, int64(1), stats2.ActivePeers)
assert.Equal(t, int64(2), stats2.TotalPeers)
}

View File

@@ -0,0 +1,210 @@
package geolocation
import (
"encoding/csv"
"fmt"
"io"
"net/url"
"os"
"path"
"strconv"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
const (
geoLiteCityTarGZURL = "https://pkgs.netbird.io/geolocation-dbs/GeoLite2-City/download?suffix=tar.gz"
geoLiteCityZipURL = "https://pkgs.netbird.io/geolocation-dbs/GeoLite2-City-CSV/download?suffix=zip"
geoLiteCitySha256TarURL = "https://pkgs.netbird.io/geolocation-dbs/GeoLite2-City/download?suffix=tar.gz.sha256"
geoLiteCitySha256ZipURL = "https://pkgs.netbird.io/geolocation-dbs/GeoLite2-City-CSV/download?suffix=zip.sha256"
)
// loadGeolocationDatabases loads the MaxMind databases.
func loadGeolocationDatabases(dataDir string) error {
files := []string{MMDBFileName, GeoSqliteDBFile}
for _, file := range files {
exists, _ := fileExists(path.Join(dataDir, file))
if exists {
continue
}
switch file {
case MMDBFileName:
extractFunc := func(src string, dst string) error {
if err := decompressTarGzFile(src, dst); err != nil {
return err
}
return copyFile(path.Join(dst, MMDBFileName), path.Join(dataDir, MMDBFileName))
}
if err := loadDatabase(
geoLiteCitySha256TarURL,
geoLiteCityTarGZURL,
extractFunc,
); err != nil {
return err
}
case GeoSqliteDBFile:
extractFunc := func(src string, dst string) error {
if err := decompressZipFile(src, dst); err != nil {
return err
}
extractedCsvFile := path.Join(dst, "GeoLite2-City-Locations-en.csv")
return importCsvToSqlite(dataDir, extractedCsvFile)
}
if err := loadDatabase(
geoLiteCitySha256ZipURL,
geoLiteCityZipURL,
extractFunc,
); err != nil {
return err
}
}
}
return nil
}
// loadDatabase downloads a file from the specified URL and verifies its checksum.
// It then calls the extract function to perform additional processing on the extracted files.
func loadDatabase(checksumURL string, fileURL string, extractFunc func(src string, dst string) error) error {
temp, err := os.MkdirTemp(os.TempDir(), "geolite")
if err != nil {
return err
}
defer os.RemoveAll(temp)
checksumFile := path.Join(temp, getDatabaseFileName(checksumURL))
err = downloadFile(checksumURL, checksumFile)
if err != nil {
return err
}
sha256sum, err := loadChecksumFromFile(checksumFile)
if err != nil {
return err
}
dbFile := path.Join(temp, getDatabaseFileName(fileURL))
err = downloadFile(fileURL, dbFile)
if err != nil {
return err
}
if err := verifyChecksum(dbFile, sha256sum); err != nil {
return err
}
return extractFunc(dbFile, temp)
}
// importCsvToSqlite imports a CSV file into a SQLite database.
func importCsvToSqlite(dataDir string, csvFile string) error {
geonames, err := loadGeonamesCsv(csvFile)
if err != nil {
return err
}
db, err := gorm.Open(sqlite.Open(path.Join(dataDir, GeoSqliteDBFile)), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
CreateBatchSize: 1000,
PrepareStmt: true,
})
if err != nil {
return err
}
defer func() {
sql, err := db.DB()
if err != nil {
return
}
sql.Close()
}()
if err := db.AutoMigrate(&GeoNames{}); err != nil {
return err
}
return db.Create(geonames).Error
}
func loadGeonamesCsv(filepath string) ([]GeoNames, error) {
f, err := os.Open(filepath)
if err != nil {
return nil, err
}
defer f.Close()
reader := csv.NewReader(f)
records, err := reader.ReadAll()
if err != nil {
return nil, err
}
var geoNames []GeoNames
for index, record := range records {
if index == 0 {
continue
}
geoNameID, err := strconv.Atoi(record[0])
if err != nil {
return nil, err
}
geoName := GeoNames{
GeoNameID: geoNameID,
LocaleCode: record[1],
ContinentCode: record[2],
ContinentName: record[3],
CountryIsoCode: record[4],
CountryName: record[5],
Subdivision1IsoCode: record[6],
Subdivision1Name: record[7],
Subdivision2IsoCode: record[8],
Subdivision2Name: record[9],
CityName: record[10],
MetroCode: record[11],
TimeZone: record[12],
IsInEuropeanUnion: record[13],
}
geoNames = append(geoNames, geoName)
}
return geoNames, nil
}
// getDatabaseFileName extracts the file name from a given URL string.
func getDatabaseFileName(urlStr string) string {
u, err := url.Parse(urlStr)
if err != nil {
panic(err)
}
ext := u.Query().Get("suffix")
fileName := fmt.Sprintf("%s.%s", path.Base(u.Path), ext)
return fileName
}
// copyFile performs a file copy operation from the source file to the destination.
func copyFile(src string, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
if err != nil {
return err
}
return nil
}

View File

@@ -2,9 +2,7 @@ package geolocation
import ( import (
"bytes" "bytes"
"crypto/sha256"
"fmt" "fmt"
"io"
"net" "net"
"os" "os"
"path" "path"
@@ -54,20 +52,23 @@ type Country struct {
CountryName string CountryName string
} }
func NewGeolocation(datadir string) (*Geolocation, error) { func NewGeolocation(dataDir string) (*Geolocation, error) {
mmdbPath := path.Join(datadir, MMDBFileName) if err := loadGeolocationDatabases(dataDir); err != nil {
return nil, fmt.Errorf("failed to load MaxMind databases: %v", err)
}
mmdbPath := path.Join(dataDir, MMDBFileName)
db, err := openDB(mmdbPath) db, err := openDB(mmdbPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
sha256sum, err := getSha256sum(mmdbPath) sha256sum, err := calculateFileSHA256(mmdbPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
locationDB, err := NewSqliteStore(datadir) locationDB, err := NewSqliteStore(dataDir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -104,21 +105,6 @@ func openDB(mmdbPath string) (*maxminddb.Reader, error) {
return db, nil return db, nil
} }
func getSha256sum(mmdbPath string) ([]byte, error) {
f, err := os.Open(mmdbPath)
if err != nil {
return nil, err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
func (gl *Geolocation) Lookup(ip net.IP) (*Record, error) { func (gl *Geolocation) Lookup(ip net.IP) (*Record, error) {
gl.mux.RLock() gl.mux.RLock()
defer gl.mux.RUnlock() defer gl.mux.RUnlock()
@@ -189,7 +175,7 @@ func (gl *Geolocation) reloader() {
log.Errorf("geonames db reload failed: %s", err) log.Errorf("geonames db reload failed: %s", err)
} }
newSha256sum1, err := getSha256sum(gl.mmdbPath) newSha256sum1, err := calculateFileSHA256(gl.mmdbPath)
if err != nil { if err != nil {
log.Errorf("failed to calculate sha256 sum for '%s': %s", gl.mmdbPath, err) log.Errorf("failed to calculate sha256 sum for '%s': %s", gl.mmdbPath, err)
continue continue
@@ -198,7 +184,7 @@ func (gl *Geolocation) reloader() {
// we check sum twice just to avoid possible case when we reload during update of the file // we check sum twice just to avoid possible case when we reload during update of the file
// considering the frequency of file update (few times a week) checking sum twice should be enough // considering the frequency of file update (few times a week) checking sum twice should be enough
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
newSha256sum2, err := getSha256sum(gl.mmdbPath) newSha256sum2, err := calculateFileSHA256(gl.mmdbPath)
if err != nil { if err != nil {
log.Errorf("failed to calculate sha256 sum for '%s': %s", gl.mmdbPath, err) log.Errorf("failed to calculate sha256 sum for '%s': %s", gl.mmdbPath, err)
continue continue

View File

@@ -20,6 +20,27 @@ const (
GeoSqliteDBFile = "geonames.db" GeoSqliteDBFile = "geonames.db"
) )
type GeoNames struct {
GeoNameID int `gorm:"column:geoname_id"`
LocaleCode string `gorm:"column:locale_code"`
ContinentCode string `gorm:"column:continent_code"`
ContinentName string `gorm:"column:continent_name"`
CountryIsoCode string `gorm:"column:country_iso_code"`
CountryName string `gorm:"column:country_name"`
Subdivision1IsoCode string `gorm:"column:subdivision_1_iso_code"`
Subdivision1Name string `gorm:"column:subdivision_1_name"`
Subdivision2IsoCode string `gorm:"column:subdivision_2_iso_code"`
Subdivision2Name string `gorm:"column:subdivision_2_name"`
CityName string `gorm:"column:city_name"`
MetroCode string `gorm:"column:metro_code"`
TimeZone string `gorm:"column:time_zone"`
IsInEuropeanUnion string `gorm:"column:is_in_european_union"`
}
func (*GeoNames) TableName() string {
return "geonames"
}
// SqliteStore represents a location storage backed by a Sqlite DB. // SqliteStore represents a location storage backed by a Sqlite DB.
type SqliteStore struct { type SqliteStore struct {
db *gorm.DB db *gorm.DB
@@ -37,7 +58,7 @@ func NewSqliteStore(dataDir string) (*SqliteStore, error) {
return nil, err return nil, err
} }
sha256sum, err := getSha256sum(file) sha256sum, err := calculateFileSHA256(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -60,7 +81,7 @@ func (s *SqliteStore) GetAllCountries() ([]Country, error) {
} }
var countries []Country var countries []Country
result := s.db.Table("geonames"). result := s.db.Model(&GeoNames{}).
Select("country_iso_code", "country_name"). Select("country_iso_code", "country_name").
Group("country_name"). Group("country_name").
Scan(&countries) Scan(&countries)
@@ -81,7 +102,7 @@ func (s *SqliteStore) GetCitiesByCountry(countryISOCode string) ([]City, error)
} }
var cities []City var cities []City
result := s.db.Table("geonames"). result := s.db.Model(&GeoNames{}).
Select("geoname_id", "city_name"). Select("geoname_id", "city_name").
Where("country_iso_code = ?", countryISOCode). Where("country_iso_code = ?", countryISOCode).
Group("city_name"). Group("city_name").
@@ -98,7 +119,7 @@ func (s *SqliteStore) reload() error {
s.mux.Lock() s.mux.Lock()
defer s.mux.Unlock() defer s.mux.Unlock()
newSha256sum1, err := getSha256sum(s.filePath) newSha256sum1, err := calculateFileSHA256(s.filePath)
if err != nil { if err != nil {
log.Errorf("failed to calculate sha256 sum for '%s': %s", s.filePath, err) log.Errorf("failed to calculate sha256 sum for '%s': %s", s.filePath, err)
} }
@@ -107,7 +128,7 @@ func (s *SqliteStore) reload() error {
// we check sum twice just to avoid possible case when we reload during update of the file // we check sum twice just to avoid possible case when we reload during update of the file
// considering the frequency of file update (few times a week) checking sum twice should be enough // considering the frequency of file update (few times a week) checking sum twice should be enough
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
newSha256sum2, err := getSha256sum(s.filePath) newSha256sum2, err := calculateFileSHA256(s.filePath)
if err != nil { if err != nil {
return fmt.Errorf("failed to calculate sha256 sum for '%s': %s", s.filePath, err) return fmt.Errorf("failed to calculate sha256 sum for '%s': %s", s.filePath, err)
} }

View File

@@ -0,0 +1,176 @@
package geolocation
import (
"archive/tar"
"archive/zip"
"bufio"
"bytes"
"compress/gzip"
"crypto/sha256"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"strings"
)
// decompressTarGzFile decompresses a .tar.gz file.
func decompressTarGzFile(filepath, destDir string) error {
file, err := os.Open(filepath)
if err != nil {
return err
}
defer file.Close()
gzipReader, err := gzip.NewReader(file)
if err != nil {
return err
}
defer gzipReader.Close()
tarReader := tar.NewReader(gzipReader)
for {
header, err := tarReader.Next()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return err
}
if header.Typeflag == tar.TypeReg {
outFile, err := os.Create(path.Join(destDir, path.Base(header.Name)))
if err != nil {
return err
}
_, err = io.Copy(outFile, tarReader) // #nosec G110
outFile.Close()
if err != nil {
return err
}
}
}
return nil
}
// decompressZipFile decompresses a .zip file.
func decompressZipFile(filepath, destDir string) error {
r, err := zip.OpenReader(filepath)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
if f.FileInfo().IsDir() {
continue
}
outFile, err := os.Create(path.Join(destDir, path.Base(f.Name)))
if err != nil {
return err
}
rc, err := f.Open()
if err != nil {
outFile.Close()
return err
}
_, err = io.Copy(outFile, rc) // #nosec G110
outFile.Close()
rc.Close()
if err != nil {
return err
}
}
return nil
}
// calculateFileSHA256 calculates the SHA256 checksum of a file.
func calculateFileSHA256(filepath string) ([]byte, error) {
file, err := os.Open(filepath)
if err != nil {
return nil, err
}
defer file.Close()
h := sha256.New()
if _, err := io.Copy(h, file); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
// loadChecksumFromFile loads the first checksum from a file.
func loadChecksumFromFile(filepath string) (string, error) {
file, err := os.Open(filepath)
if err != nil {
return "", err
}
defer file.Close()
scanner := bufio.NewScanner(file)
if scanner.Scan() {
parts := strings.Fields(scanner.Text())
if len(parts) > 0 {
return parts[0], nil
}
}
if err := scanner.Err(); err != nil {
return "", err
}
return "", nil
}
// verifyChecksum compares the calculated SHA256 checksum of a file against the expected checksum.
func verifyChecksum(filepath, expectedChecksum string) error {
calculatedChecksum, err := calculateFileSHA256(filepath)
fileCheckSum := fmt.Sprintf("%x", calculatedChecksum)
if err != nil {
return err
}
if fileCheckSum != expectedChecksum {
return fmt.Errorf("checksum mismatch: expected %s, got %s", expectedChecksum, fileCheckSum)
}
return nil
}
// downloadFile downloads a file from a URL and saves it to a local file path.
func downloadFile(url, filepath string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected error occurred while downloading the file: %s", string(bodyBytes))
}
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, bytes.NewBuffer(bodyBytes))
return err
}

View File

@@ -288,6 +288,13 @@ func extractPeerMeta(loginReq *proto.LoginRequest) nbpeer.PeerSystemMeta {
SystemSerialNumber: loginReq.GetMeta().GetSysSerialNumber(), SystemSerialNumber: loginReq.GetMeta().GetSysSerialNumber(),
SystemProductName: loginReq.GetMeta().GetSysProductName(), SystemProductName: loginReq.GetMeta().GetSysProductName(),
SystemManufacturer: loginReq.GetMeta().GetSysManufacturer(), SystemManufacturer: loginReq.GetMeta().GetSysManufacturer(),
Environment: nbpeer.Environment{
Cloud: loginReq.GetMeta().GetEnvironment().GetCloud(),
Platform: loginReq.GetMeta().GetEnvironment().GetPlatform(),
},
RosenpassEnabled: loginReq.GetMeta().GetRosenpassEnabled(),
RosenpassPermissive: loginReq.GetMeta().GetRosenpassPermissive(),
ServerSSHAllowed: loginReq.GetMeta().GetServerSSHAllowed(),
} }
} }

View File

@@ -121,7 +121,7 @@ components:
description: Last time this user performed a login to the dashboard description: Last time this user performed a login to the dashboard
type: string type: string
format: date-time format: date-time
example: 2023-05-05T09:00:35.477782Z example: "2023-05-05T09:00:35.477782Z"
auto_groups: auto_groups:
description: Group IDs to auto-assign to peers registered by this user description: Group IDs to auto-assign to peers registered by this user
type: array type: array
@@ -259,7 +259,7 @@ components:
description: Last time peer connected to Netbird's management service description: Last time peer connected to Netbird's management service
type: string type: string
format: date-time format: date-time
example: 2023-05-05T10:05:26.420578Z example: "2023-05-05T10:05:26.420578Z"
os: os:
description: Peer's operating system and version description: Peer's operating system and version
type: string type: string
@@ -313,7 +313,7 @@ components:
description: Last time this peer performed log in (authentication). E.g., user authenticated. description: Last time this peer performed log in (authentication). E.g., user authenticated.
type: string type: string
format: date-time format: date-time
example: 2023-05-05T09:00:35.477782Z example: "2023-05-05T09:00:35.477782Z"
approval_required: approval_required:
description: (Cloud only) Indicates whether peer needs approval description: (Cloud only) Indicates whether peer needs approval
type: boolean type: boolean
@@ -405,7 +405,7 @@ components:
description: Setup Key expiration date description: Setup Key expiration date
type: string type: string
format: date-time format: date-time
example: 2023-06-01T14:47:22.291057Z example: "2023-06-01T14:47:22.291057Z"
type: type:
description: Setup key type, one-off for single time usage and reusable description: Setup key type, one-off for single time usage and reusable
type: string type: string
@@ -426,7 +426,7 @@ components:
description: Setup key last usage date description: Setup key last usage date
type: string type: string
format: date-time format: date-time
example: 2023-05-05T09:00:35.477782Z example: "2023-05-05T09:00:35.477782Z"
state: state:
description: Setup key status, "valid", "overused","expired" or "revoked" description: Setup key status, "valid", "overused","expired" or "revoked"
type: string type: string
@@ -441,7 +441,7 @@ components:
description: Setup key last update date description: Setup key last update date
type: string type: string
format: date-time format: date-time
example: 2023-05-05T09:00:35.477782Z example: "2023-05-05T09:00:35.477782Z"
usage_limit: usage_limit:
description: A number of times this key can be used. The value of 0 indicates the unlimited usage. description: A number of times this key can be used. The value of 0 indicates the unlimited usage.
type: integer type: integer
@@ -522,7 +522,7 @@ components:
description: Date the token expires description: Date the token expires
type: string type: string
format: date-time format: date-time
example: 2023-05-05T14:38:28.977616Z example: "2023-05-05T14:38:28.977616Z"
created_by: created_by:
description: User ID of the user who created the token description: User ID of the user who created the token
type: string type: string
@@ -531,12 +531,12 @@ components:
description: Date the token was created description: Date the token was created
type: string type: string
format: date-time format: date-time
example: 2023-05-02T14:48:20.465209Z example: "2023-05-02T14:48:20.465209Z"
last_used: last_used:
description: Date the token was last used description: Date the token was last used
type: string type: string
format: date-time format: date-time
example: 2023-05-04T12:45:25.9723616Z example: "2023-05-04T12:45:25.9723616Z"
required: required:
- id - id
- name - name
@@ -862,8 +862,8 @@ components:
$ref: '#/components/schemas/OSVersionCheck' $ref: '#/components/schemas/OSVersionCheck'
geo_location_check: geo_location_check:
$ref: '#/components/schemas/GeoLocationCheck' $ref: '#/components/schemas/GeoLocationCheck'
private_network_check: peer_network_range_check:
$ref: '#/components/schemas/PrivateNetworkCheck' $ref: '#/components/schemas/PeerNetworkRangeCheck'
NBVersionCheck: NBVersionCheck:
description: Posture check for the version of NetBird description: Posture check for the version of NetBird
type: object type: object
@@ -934,16 +934,16 @@ components:
required: required:
- locations - locations
- action - action
PrivateNetworkCheck: PeerNetworkRangeCheck:
description: Posture check for allow or deny private network description: Posture check for allow or deny access based on peer local network addresses
type: object type: object
properties: properties:
ranges: ranges:
description: List of private network ranges in CIDR notation description: List of peer network ranges in CIDR notation
type: array type: array
items: items:
type: string type: string
example: ["192.168.1.0/24", "10.0.0.0/8"] example: ["192.168.1.0/24", "10.0.0.0/8", "2001:db8:1234:1a00::/56"]
action: action:
description: Action to take upon policy match description: Action to take upon policy match
type: string type: string
@@ -979,7 +979,7 @@ components:
type: string type: string
example: "Germany" example: "Germany"
country_code: country_code:
$ref: '#/components/schemas/CountryCode' $ref: '#/components/schemas/CountryCode'
required: required:
- country_name - country_name
- country_code - country_code
@@ -1197,7 +1197,7 @@ components:
description: The date and time when the event occurred description: The date and time when the event occurred
type: string type: string
format: date-time format: date-time
example: 2023-05-05T10:04:37.473542Z example: "2023-05-05T10:04:37.473542Z"
activity: activity:
description: The activity that occurred during the event description: The activity that occurred during the event
type: string type: string

View File

@@ -74,6 +74,12 @@ const (
NameserverNsTypeUdp NameserverNsType = "udp" NameserverNsTypeUdp NameserverNsType = "udp"
) )
// Defines values for PeerNetworkRangeCheckAction.
const (
PeerNetworkRangeCheckActionAllow PeerNetworkRangeCheckAction = "allow"
PeerNetworkRangeCheckActionDeny PeerNetworkRangeCheckAction = "deny"
)
// Defines values for PolicyRuleAction. // Defines values for PolicyRuleAction.
const ( const (
PolicyRuleActionAccept PolicyRuleAction = "accept" PolicyRuleActionAccept PolicyRuleAction = "accept"
@@ -116,12 +122,6 @@ const (
PolicyRuleUpdateProtocolUdp PolicyRuleUpdateProtocol = "udp" PolicyRuleUpdateProtocolUdp PolicyRuleUpdateProtocol = "udp"
) )
// Defines values for PrivateNetworkCheckAction.
const (
PrivateNetworkCheckActionAllow PrivateNetworkCheckAction = "allow"
PrivateNetworkCheckActionDeny PrivateNetworkCheckAction = "deny"
)
// Defines values for UserStatus. // Defines values for UserStatus.
const ( const (
UserStatusActive UserStatus = "active" UserStatusActive UserStatus = "active"
@@ -199,8 +199,8 @@ type Checks struct {
// OsVersionCheck Posture check for the version of operating system // OsVersionCheck Posture check for the version of operating system
OsVersionCheck *OSVersionCheck `json:"os_version_check,omitempty"` OsVersionCheck *OSVersionCheck `json:"os_version_check,omitempty"`
// PrivateNetworkCheck Posture check for allow or deny private network // PeerNetworkRangeCheck Posture check for allow or deny access based on peer local network addresses
PrivateNetworkCheck *PrivateNetworkCheck `json:"private_network_check,omitempty"` PeerNetworkRangeCheck *PeerNetworkRangeCheck `json:"peer_network_range_check,omitempty"`
} }
// City Describe city geographical location information // City Describe city geographical location information
@@ -656,6 +656,18 @@ type PeerMinimum struct {
Name string `json:"name"` Name string `json:"name"`
} }
// PeerNetworkRangeCheck Posture check for allow or deny access based on peer local network addresses
type PeerNetworkRangeCheck struct {
// Action Action to take upon policy match
Action PeerNetworkRangeCheckAction `json:"action"`
// Ranges List of peer network ranges in CIDR notation
Ranges []string `json:"ranges"`
}
// PeerNetworkRangeCheckAction Action to take upon policy match
type PeerNetworkRangeCheckAction string
// PeerRequest defines model for PeerRequest. // PeerRequest defines model for PeerRequest.
type PeerRequest struct { type PeerRequest struct {
// ApprovalRequired (Cloud only) Indicates whether peer needs approval // ApprovalRequired (Cloud only) Indicates whether peer needs approval
@@ -898,18 +910,6 @@ type PostureCheckUpdate struct {
Name string `json:"name"` Name string `json:"name"`
} }
// PrivateNetworkCheck Posture check for allow or deny private network
type PrivateNetworkCheck struct {
// Action Action to take upon policy match
Action PrivateNetworkCheckAction `json:"action"`
// Ranges List of private network ranges in CIDR notation
Ranges []string `json:"ranges"`
}
// PrivateNetworkCheckAction Action to take upon policy match
type PrivateNetworkCheckAction string
// Route defines model for Route. // Route defines model for Route.
type Route struct { type Route struct {
// Description Route description // Description Route description

View File

@@ -177,7 +177,10 @@ func TestAuthMiddleware_Handler(t *testing.T) {
for _, tc := range tt { for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
if tc.shouldBypassAuth { if tc.shouldBypassAuth {
bypass.AddBypassPath(tc.path) err := bypass.AddBypassPath(tc.path)
if err != nil {
t.Fatalf("failed to add bypass path: %v", err)
}
} }
req := httptest.NewRequest("GET", "http://testing"+tc.path, nil) req := httptest.NewRequest("GET", "http://testing"+tc.path, nil)

View File

@@ -1,8 +1,12 @@
package bypass package bypass
import ( import (
"fmt"
"net/http" "net/http"
"path"
"sync" "sync"
log "github.com/sirupsen/logrus"
) )
var byPassMutex sync.RWMutex var byPassMutex sync.RWMutex
@@ -11,10 +15,16 @@ var byPassMutex sync.RWMutex
var bypassPaths = make(map[string]struct{}) var bypassPaths = make(map[string]struct{})
// AddBypassPath adds an exact path to the list of paths that bypass middleware. // AddBypassPath adds an exact path to the list of paths that bypass middleware.
func AddBypassPath(path string) { // Paths can include wildcards, such as /api/*. Paths are matched using path.Match.
// Returns an error if the path has invalid pattern.
func AddBypassPath(path string) error {
byPassMutex.Lock() byPassMutex.Lock()
defer byPassMutex.Unlock() defer byPassMutex.Unlock()
if err := validatePath(path); err != nil {
return fmt.Errorf("validate: %w", err)
}
bypassPaths[path] = struct{}{} bypassPaths[path] = struct{}{}
return nil
} }
// RemovePath removes a path from the list of paths that bypass middleware. // RemovePath removes a path from the list of paths that bypass middleware.
@@ -24,16 +34,41 @@ func RemovePath(path string) {
delete(bypassPaths, path) delete(bypassPaths, path)
} }
// GetList returns a list of all bypass paths.
func GetList() []string {
byPassMutex.RLock()
defer byPassMutex.RUnlock()
list := make([]string, 0, len(bypassPaths))
for k := range bypassPaths {
list = append(list, k)
}
return list
}
// ShouldBypass checks if the request path is one of the auth bypass paths and returns true if the middleware should be bypassed. // ShouldBypass checks if the request path is one of the auth bypass paths and returns true if the middleware should be bypassed.
// This can be used to bypass authz/authn middlewares for certain paths, such as webhooks that implement their own authentication. // This can be used to bypass authz/authn middlewares for certain paths, such as webhooks that implement their own authentication.
func ShouldBypass(requestPath string, h http.Handler, w http.ResponseWriter, r *http.Request) bool { func ShouldBypass(requestPath string, h http.Handler, w http.ResponseWriter, r *http.Request) bool {
byPassMutex.RLock() byPassMutex.RLock()
defer byPassMutex.RUnlock() defer byPassMutex.RUnlock()
if _, ok := bypassPaths[requestPath]; ok { for bypassPath := range bypassPaths {
h.ServeHTTP(w, r) matched, err := path.Match(bypassPath, requestPath)
return true if err != nil {
log.Errorf("Error matching path %s with %s from %s: %v", bypassPath, requestPath, GetList(), err)
continue
}
if matched {
h.ServeHTTP(w, r)
return true
}
} }
return false return false
} }
func validatePath(p string) error {
_, err := path.Match(p, "")
return err
}

View File

@@ -11,6 +11,19 @@ import (
"github.com/netbirdio/netbird/management/server/http/middleware/bypass" "github.com/netbirdio/netbird/management/server/http/middleware/bypass"
) )
func TestGetList(t *testing.T) {
bypassPaths := []string{"/path1", "/path2", "/path3"}
for _, path := range bypassPaths {
err := bypass.AddBypassPath(path)
require.NoError(t, err, "Adding bypass path should not fail")
}
list := bypass.GetList()
assert.ElementsMatch(t, bypassPaths, list, "Bypass path list did not match expected paths")
}
func TestAuthBypass(t *testing.T) { func TestAuthBypass(t *testing.T) {
dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@@ -31,6 +44,13 @@ func TestAuthBypass(t *testing.T) {
expectBypass: true, expectBypass: true,
expectHTTPCode: http.StatusOK, expectHTTPCode: http.StatusOK,
}, },
{
name: "Wildcard path added to bypass",
pathToAdd: "/bypass/*",
testPath: "/bypass/extra",
expectBypass: true,
expectHTTPCode: http.StatusOK,
},
{ {
name: "Path not added to bypass", name: "Path not added to bypass",
testPath: "/no-bypass", testPath: "/no-bypass",
@@ -59,6 +79,13 @@ func TestAuthBypass(t *testing.T) {
expectBypass: false, expectBypass: false,
expectHTTPCode: http.StatusOK, expectHTTPCode: http.StatusOK,
}, },
{
name: "Wildcard subpath does not match bypass",
pathToAdd: "/webhook/*",
testPath: "/webhook/extra/path",
expectBypass: false,
expectHTTPCode: http.StatusOK,
},
{ {
name: "Similar path does not match bypass", name: "Similar path does not match bypass",
pathToAdd: "/webhook", pathToAdd: "/webhook",
@@ -78,7 +105,8 @@ func TestAuthBypass(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
if tc.pathToAdd != "" { if tc.pathToAdd != "" {
bypass.AddBypassPath(tc.pathToAdd) err := bypass.AddBypassPath(tc.pathToAdd)
require.NoError(t, err, "Adding bypass path should not fail")
defer bypass.RemovePath(tc.pathToAdd) defer bypass.RemovePath(tc.pathToAdd)
} }

View File

@@ -213,8 +213,8 @@ func (p *PostureChecksHandler) savePostureChecks(
postureChecks.Checks.GeoLocationCheck = toPostureGeoLocationCheck(geoLocationCheck) postureChecks.Checks.GeoLocationCheck = toPostureGeoLocationCheck(geoLocationCheck)
} }
if privateNetworkCheck := req.Checks.PrivateNetworkCheck; privateNetworkCheck != nil { if peerNetworkRangeCheck := req.Checks.PeerNetworkRangeCheck; peerNetworkRangeCheck != nil {
postureChecks.Checks.PrivateNetworkCheck, err = toPrivateNetworkCheck(privateNetworkCheck) postureChecks.Checks.PeerNetworkRangeCheck, err = toPeerNetworkRangeCheck(peerNetworkRangeCheck)
if err != nil { if err != nil {
util.WriteError(status.Errorf(status.InvalidArgument, "invalid network prefix"), w) util.WriteError(status.Errorf(status.InvalidArgument, "invalid network prefix"), w)
return return
@@ -235,7 +235,7 @@ func validatePostureChecksUpdate(req api.PostureCheckUpdate) error {
} }
if req.Checks == nil || (req.Checks.NbVersionCheck == nil && req.Checks.OsVersionCheck == nil && if req.Checks == nil || (req.Checks.NbVersionCheck == nil && req.Checks.OsVersionCheck == nil &&
req.Checks.GeoLocationCheck == nil && req.Checks.PrivateNetworkCheck == nil) { req.Checks.GeoLocationCheck == nil && req.Checks.PeerNetworkRangeCheck == nil) {
return status.Errorf(status.InvalidArgument, "posture checks shouldn't be empty") return status.Errorf(status.InvalidArgument, "posture checks shouldn't be empty")
} }
@@ -278,17 +278,17 @@ func validatePostureChecksUpdate(req api.PostureCheckUpdate) error {
} }
} }
if privateNetworkCheck := req.Checks.PrivateNetworkCheck; privateNetworkCheck != nil { if peerNetworkRangeCheck := req.Checks.PeerNetworkRangeCheck; peerNetworkRangeCheck != nil {
if privateNetworkCheck.Action == "" { if peerNetworkRangeCheck.Action == "" {
return status.Errorf(status.InvalidArgument, "action for private network check shouldn't be empty") return status.Errorf(status.InvalidArgument, "action for peer network range check shouldn't be empty")
} }
allowedActions := []api.PrivateNetworkCheckAction{api.PrivateNetworkCheckActionAllow, api.PrivateNetworkCheckActionDeny} allowedActions := []api.PeerNetworkRangeCheckAction{api.PeerNetworkRangeCheckActionAllow, api.PeerNetworkRangeCheckActionDeny}
if !slices.Contains(allowedActions, privateNetworkCheck.Action) { if !slices.Contains(allowedActions, peerNetworkRangeCheck.Action) {
return status.Errorf(status.InvalidArgument, "action for private network check is not valid value") return status.Errorf(status.InvalidArgument, "action for peer network range check is not valid value")
} }
if len(privateNetworkCheck.Ranges) == 0 { if len(peerNetworkRangeCheck.Ranges) == 0 {
return status.Errorf(status.InvalidArgument, "network ranges for private network check shouldn't be empty") return status.Errorf(status.InvalidArgument, "network ranges for peer network range check shouldn't be empty")
} }
} }
@@ -318,8 +318,8 @@ func toPostureChecksResponse(postureChecks *posture.Checks) *api.PostureCheck {
checks.GeoLocationCheck = toGeoLocationCheckResponse(postureChecks.Checks.GeoLocationCheck) checks.GeoLocationCheck = toGeoLocationCheckResponse(postureChecks.Checks.GeoLocationCheck)
} }
if postureChecks.Checks.PrivateNetworkCheck != nil { if postureChecks.Checks.PeerNetworkRangeCheck != nil {
checks.PrivateNetworkCheck = toPrivateNetworkCheckResponse(postureChecks.Checks.PrivateNetworkCheck) checks.PeerNetworkRangeCheck = toPeerNetworkRangeCheckResponse(postureChecks.Checks.PeerNetworkRangeCheck)
} }
return &api.PostureCheck{ return &api.PostureCheck{
@@ -369,19 +369,19 @@ func toPostureGeoLocationCheck(apiGeoLocationCheck *api.GeoLocationCheck) *postu
} }
} }
func toPrivateNetworkCheckResponse(check *posture.PrivateNetworkCheck) *api.PrivateNetworkCheck { func toPeerNetworkRangeCheckResponse(check *posture.PeerNetworkRangeCheck) *api.PeerNetworkRangeCheck {
netPrefixes := make([]string, 0, len(check.Ranges)) netPrefixes := make([]string, 0, len(check.Ranges))
for _, netPrefix := range check.Ranges { for _, netPrefix := range check.Ranges {
netPrefixes = append(netPrefixes, netPrefix.String()) netPrefixes = append(netPrefixes, netPrefix.String())
} }
return &api.PrivateNetworkCheck{ return &api.PeerNetworkRangeCheck{
Ranges: netPrefixes, Ranges: netPrefixes,
Action: api.PrivateNetworkCheckAction(check.Action), Action: api.PeerNetworkRangeCheckAction(check.Action),
} }
} }
func toPrivateNetworkCheck(check *api.PrivateNetworkCheck) (*posture.PrivateNetworkCheck, error) { func toPeerNetworkRangeCheck(check *api.PeerNetworkRangeCheck) (*posture.PeerNetworkRangeCheck, error) {
prefixes := make([]netip.Prefix, 0) prefixes := make([]netip.Prefix, 0)
for _, prefix := range check.Ranges { for _, prefix := range check.Ranges {
parsedPrefix, err := netip.ParsePrefix(prefix) parsedPrefix, err := netip.ParsePrefix(prefix)
@@ -391,7 +391,7 @@ func toPrivateNetworkCheck(check *api.PrivateNetworkCheck) (*posture.PrivateNetw
prefixes = append(prefixes, parsedPrefix) prefixes = append(prefixes, parsedPrefix)
} }
return &posture.PrivateNetworkCheck{ return &posture.PeerNetworkRangeCheck{
Ranges: prefixes, Ranges: prefixes,
Action: string(check.Action), Action: string(check.Action),
}, nil }, nil

View File

@@ -131,7 +131,7 @@ func TestGetPostureCheck(t *testing.T) {
ID: "privateNetworkPostureCheck", ID: "privateNetworkPostureCheck",
Name: "privateNetwork", Name: "privateNetwork",
Checks: posture.ChecksDefinition{ Checks: posture.ChecksDefinition{
PrivateNetworkCheck: &posture.PrivateNetworkCheck{ PeerNetworkRangeCheck: &posture.PeerNetworkRangeCheck{
Ranges: []netip.Prefix{ Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"), netip.MustParsePrefix("192.168.0.0/24"),
}, },
@@ -375,7 +375,7 @@ func TestPostureCheckUpdate(t *testing.T) {
}, },
}, },
{ {
name: "Create Posture Checks Private Network", name: "Create Posture Checks Peer Network Range",
requestType: http.MethodPost, requestType: http.MethodPost,
requestPath: "/api/posture-checks", requestPath: "/api/posture-checks",
requestBody: bytes.NewBuffer( requestBody: bytes.NewBuffer(
@@ -383,7 +383,7 @@ func TestPostureCheckUpdate(t *testing.T) {
"name": "default", "name": "default",
"description": "default", "description": "default",
"checks": { "checks": {
"private_network_check": { "peer_network_range_check": {
"action": "allow", "action": "allow",
"ranges": [ "ranges": [
"10.0.0.0/8" "10.0.0.0/8"
@@ -398,11 +398,11 @@ func TestPostureCheckUpdate(t *testing.T) {
Name: "default", Name: "default",
Description: str("default"), Description: str("default"),
Checks: api.Checks{ Checks: api.Checks{
PrivateNetworkCheck: &api.PrivateNetworkCheck{ PeerNetworkRangeCheck: &api.PeerNetworkRangeCheck{
Ranges: []string{ Ranges: []string{
"10.0.0.0/8", "10.0.0.0/8",
}, },
Action: api.PrivateNetworkCheckActionAllow, Action: api.PeerNetworkRangeCheckActionAllow,
}, },
}, },
}, },
@@ -715,14 +715,14 @@ func TestPostureCheckUpdate(t *testing.T) {
expectedBody: false, expectedBody: false,
}, },
{ {
name: "Update Posture Checks Private Network", name: "Update Posture Checks Peer Network Range",
requestType: http.MethodPut, requestType: http.MethodPut,
requestPath: "/api/posture-checks/privateNetworkPostureCheck", requestPath: "/api/posture-checks/peerNetworkRangePostureCheck",
requestBody: bytes.NewBuffer( requestBody: bytes.NewBuffer(
[]byte(`{ []byte(`{
"name": "default", "name": "default",
"checks": { "checks": {
"private_network_check": { "peer_network_range_check": {
"action": "deny", "action": "deny",
"ranges": [ "ranges": [
"192.168.1.0/24" "192.168.1.0/24"
@@ -737,11 +737,11 @@ func TestPostureCheckUpdate(t *testing.T) {
Name: "default", Name: "default",
Description: str(""), Description: str(""),
Checks: api.Checks{ Checks: api.Checks{
PrivateNetworkCheck: &api.PrivateNetworkCheck{ PeerNetworkRangeCheck: &api.PeerNetworkRangeCheck{
Ranges: []string{ Ranges: []string{
"192.168.1.0/24", "192.168.1.0/24",
}, },
Action: api.PrivateNetworkCheckActionDeny, Action: api.PeerNetworkRangeCheckActionDeny,
}, },
}, },
}, },
@@ -784,10 +784,10 @@ func TestPostureCheckUpdate(t *testing.T) {
}, },
}, },
&posture.Checks{ &posture.Checks{
ID: "privateNetworkPostureCheck", ID: "peerNetworkRangePostureCheck",
Name: "privateNetwork", Name: "peerNetworkRange",
Checks: posture.ChecksDefinition{ Checks: posture.ChecksDefinition{
PrivateNetworkCheck: &posture.PrivateNetworkCheck{ PeerNetworkRangeCheck: &posture.PeerNetworkRangeCheck{
Ranges: []netip.Prefix{ Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"), netip.MustParsePrefix("192.168.0.0/24"),
}, },
@@ -891,29 +891,50 @@ func TestPostureCheck_validatePostureChecksUpdate(t *testing.T) {
err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}}) err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}})
assert.NoError(t, err) assert.NoError(t, err)
// valid private network check // valid peer network range check
privateNetworkCheck := api.PrivateNetworkCheck{ peerNetworkRangeCheck := api.PeerNetworkRangeCheck{
Action: api.PrivateNetworkCheckActionAllow, Action: api.PeerNetworkRangeCheckActionAllow,
Ranges: []string{ Ranges: []string{
"192.168.1.0/24", "10.0.0.0/8", "192.168.1.0/24", "10.0.0.0/8",
}, },
} }
err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{PrivateNetworkCheck: &privateNetworkCheck}}) err = validatePostureChecksUpdate(
api.PostureCheckUpdate{
Name: "Default",
Checks: &api.Checks{
PeerNetworkRangeCheck: &peerNetworkRangeCheck,
},
},
)
assert.NoError(t, err) assert.NoError(t, err)
// invalid private network check // invalid peer network range check
privateNetworkCheck = api.PrivateNetworkCheck{ peerNetworkRangeCheck = api.PeerNetworkRangeCheck{
Action: api.PrivateNetworkCheckActionDeny, Action: api.PeerNetworkRangeCheckActionDeny,
Ranges: []string{}, Ranges: []string{},
} }
err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{PrivateNetworkCheck: &privateNetworkCheck}}) err = validatePostureChecksUpdate(
api.PostureCheckUpdate{
Name: "Default",
Checks: &api.Checks{
PeerNetworkRangeCheck: &peerNetworkRangeCheck,
},
},
)
assert.Error(t, err) assert.Error(t, err)
// invalid private network check // invalid peer network range check
privateNetworkCheck = api.PrivateNetworkCheck{ peerNetworkRangeCheck = api.PeerNetworkRangeCheck{
Action: "unknownAction", Action: "unknownAction",
Ranges: []string{}, Ranges: []string{},
} }
err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{PrivateNetworkCheck: &privateNetworkCheck}}) err = validatePostureChecksUpdate(
api.PostureCheckUpdate{
Name: "Default",
Checks: &api.Checks{
PeerNetworkRangeCheck: &peerNetworkRangeCheck,
},
},
)
assert.Error(t, err) assert.Error(t, err)
} }

View File

@@ -114,6 +114,22 @@ type auth0Profile struct {
LastLogin string `json:"last_login"` LastLogin string `json:"last_login"`
} }
// Connections represents a single Auth0 connection
// https://auth0.com/docs/api/management/v2/connections/get-connections
type Connection struct {
Id string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
IsDomainConnection bool `json:"is_domain_connection"`
Realms []string `json:"realms"`
Metadata map[string]string `json:"metadata"`
Options ConnectionOptions `json:"options"`
}
type ConnectionOptions struct {
DomainAliases []string `json:"domain_aliases"`
}
// NewAuth0Manager creates a new instance of the Auth0Manager // NewAuth0Manager creates a new instance of the Auth0Manager
func NewAuth0Manager(config Auth0ClientConfig, appMetrics telemetry.AppMetrics) (*Auth0Manager, error) { func NewAuth0Manager(config Auth0ClientConfig, appMetrics telemetry.AppMetrics) (*Auth0Manager, error) {
httpTransport := http.DefaultTransport.(*http.Transport).Clone() httpTransport := http.DefaultTransport.(*http.Transport).Clone()
@@ -581,13 +597,13 @@ func (am *Auth0Manager) GetAllAccounts() (map[string][]*UserData, error) {
body, err := io.ReadAll(jobResp.Body) body, err := io.ReadAll(jobResp.Body)
if err != nil { if err != nil {
log.Debugf("Coudln't read export job response; %v", err) log.Debugf("Couldn't read export job response; %v", err)
return nil, err return nil, err
} }
err = am.helper.Unmarshal(body, &exportJobResp) err = am.helper.Unmarshal(body, &exportJobResp)
if err != nil { if err != nil {
log.Debugf("Coudln't unmarshal export job response; %v", err) log.Debugf("Couldn't unmarshal export job response; %v", err)
return nil, err return nil, err
} }
@@ -635,7 +651,7 @@ func (am *Auth0Manager) GetUserByEmail(email string) ([]*UserData, error) {
err = am.helper.Unmarshal(body, &userResp) err = am.helper.Unmarshal(body, &userResp)
if err != nil { if err != nil {
log.Debugf("Coudln't unmarshal export job response; %v", err) log.Debugf("Couldn't unmarshal export job response; %v", err)
return nil, err return nil, err
} }
@@ -684,13 +700,13 @@ func (am *Auth0Manager) CreateUser(email, name, accountID, invitedByEmail string
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Debugf("Coudln't read export job response; %v", err) log.Debugf("Couldn't read export job response; %v", err)
return nil, err return nil, err
} }
err = am.helper.Unmarshal(body, &createResp) err = am.helper.Unmarshal(body, &createResp)
if err != nil { if err != nil {
log.Debugf("Coudln't unmarshal export job response; %v", err) log.Debugf("Couldn't unmarshal export job response; %v", err)
return nil, err return nil, err
} }
@@ -777,6 +793,56 @@ func (am *Auth0Manager) DeleteUser(userID string) error {
return nil return nil
} }
// GetAllConnections returns detailed list of all connections filtered by given params.
// Note this method is not part of the IDP Manager interface as this is Auth0 specific.
func (am *Auth0Manager) GetAllConnections(strategy []string) ([]Connection, error) {
var connections []Connection
q := make(url.Values)
q.Set("strategy", strings.Join(strategy, ","))
req, err := am.createRequest(http.MethodGet, "/api/v2/connections?"+q.Encode(), nil)
if err != nil {
return connections, err
}
resp, err := am.httpClient.Do(req)
if err != nil {
log.Debugf("execute get connections request: %v", err)
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountRequestError()
}
return connections, err
}
defer func() {
err = resp.Body.Close()
if err != nil {
log.Errorf("close get connections request body: %v", err)
}
}()
if resp.StatusCode != 200 {
if am.appMetrics != nil {
am.appMetrics.IDPMetrics().CountRequestStatusError()
}
return connections, fmt.Errorf("unable to get connections, statusCode %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Debugf("Couldn't read get connections response; %v", err)
return connections, err
}
err = am.helper.Unmarshal(body, &connections)
if err != nil {
log.Debugf("Couldn't unmarshal get connection response; %v", err)
return connections, err
}
return connections, err
}
// checkExportJobStatus checks the status of the job created at CreateExportUsersJob. // checkExportJobStatus checks the status of the job created at CreateExportUsersJob.
// If the status is "completed", then return the downloadLink // If the status is "completed", then return the downloadLink
func (am *Auth0Manager) checkExportJobStatus(jobID string) (bool, string, error) { func (am *Auth0Manager) checkExportJobStatus(jobID string) (bool, string, error) {

View File

@@ -1,7 +1,6 @@
package mock_server package mock_server
import ( import (
"context"
"net" "net"
"time" "time"
@@ -11,6 +10,7 @@ import (
nbdns "github.com/netbirdio/netbird/dns" nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/jwtclaims"
nbpeer "github.com/netbirdio/netbird/management/server/peer" nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/posture"
@@ -92,7 +92,7 @@ type MockAccountManager struct {
SavePostureChecksFunc func(accountID, userID string, postureChecks *posture.Checks) error SavePostureChecksFunc func(accountID, userID string, postureChecks *posture.Checks) error
DeletePostureChecksFunc func(accountID, postureChecksID, userID string) error DeletePostureChecksFunc func(accountID, postureChecksID, userID string) error
ListPostureChecksFunc func(accountID, userID string) ([]*posture.Checks, error) ListPostureChecksFunc func(accountID, userID string) ([]*posture.Checks, error)
GetUsageFunc func(ctx context.Context, accountID string, start, end time.Time) (*server.AccountUsageStats, error) GetIdpManagerFunc func() idp.Manager
} }
// GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface
@@ -705,10 +705,10 @@ func (am *MockAccountManager) ListPostureChecks(accountID, userID string) ([]*po
return nil, status.Errorf(codes.Unimplemented, "method ListPostureChecks is not implemented") return nil, status.Errorf(codes.Unimplemented, "method ListPostureChecks is not implemented")
} }
// GetUsage mocks GetCurrentUsage of the AccountManager interface // GetIdpManager mocks GetIdpManager of the AccountManager interface
func (am *MockAccountManager) GetUsage(ctx context.Context, accountID string, start time.Time, end time.Time) (*server.AccountUsageStats, error) { func (am *MockAccountManager) GetIdpManager() idp.Manager {
if am.GetUsageFunc != nil { if am.GetIdpManagerFunc != nil {
return am.GetUsageFunc(ctx, accountID, start, end) return am.GetIdpManagerFunc()
} }
return nil, status.Errorf(codes.Unimplemented, "method GetUsage is not implemented") return nil
} }

View File

@@ -410,6 +410,8 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *nbpeer.P
return nil, nil, err return nil, nil, err
} }
registrationTime := time.Now().UTC()
newPeer := &nbpeer.Peer{ newPeer := &nbpeer.Peer{
ID: xid.New().String(), ID: xid.New().String(),
Key: peer.Key, Key: peer.Key,
@@ -419,10 +421,11 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *nbpeer.P
Name: peer.Meta.Hostname, Name: peer.Meta.Hostname,
DNSLabel: newLabel, DNSLabel: newLabel,
UserID: userID, UserID: userID,
Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now().UTC()}, Status: &nbpeer.PeerStatus{Connected: false, LastSeen: registrationTime},
SSHEnabled: false, SSHEnabled: false,
SSHKey: peer.SSHKey, SSHKey: peer.SSHKey,
LastLogin: time.Now().UTC(), LastLogin: registrationTime,
CreatedAt: registrationTime,
LoginExpirationEnabled: addedByUser, LoginExpirationEnabled: addedByUser,
Ephemeral: ephemeral, Ephemeral: ephemeral,
} }

View File

@@ -40,13 +40,15 @@ type Peer struct {
LoginExpirationEnabled bool LoginExpirationEnabled bool
// LastLogin the time when peer performed last login operation // LastLogin the time when peer performed last login operation
LastLogin time.Time LastLogin time.Time
// CreatedAt records the time the peer was created
CreatedAt time.Time
// Indicate ephemeral peer attribute // Indicate ephemeral peer attribute
Ephemeral bool Ephemeral bool
// Geo location based on connection IP // Geo location based on connection IP
Location Location `gorm:"embedded;embeddedPrefix:location_"` Location Location `gorm:"embedded;embeddedPrefix:location_"`
} }
type PeerStatus struct { type PeerStatus struct { //nolint:revive
// LastSeen is the last time peer was connected to the management service // LastSeen is the last time peer was connected to the management service
LastSeen time.Time LastSeen time.Time
// Connected indicates whether peer is connected to the management service or not // Connected indicates whether peer is connected to the management service or not
@@ -71,22 +73,32 @@ type NetworkAddress struct {
Mac string Mac string
} }
// Environment is a system environment information
type Environment struct {
Cloud string
Platform string
}
// PeerSystemMeta is a metadata of a Peer machine system // PeerSystemMeta is a metadata of a Peer machine system
type PeerSystemMeta struct { type PeerSystemMeta struct { //nolint:revive
Hostname string Hostname string
GoOS string GoOS string
Kernel string Kernel string
Core string Core string
Platform string Platform string
OS string OS string
OSVersion string OSVersion string
WtVersion string WtVersion string
UIVersion string UIVersion string
KernelVersion string KernelVersion string
NetworkAddresses []NetworkAddress `gorm:"serializer:json"` NetworkAddresses []NetworkAddress `gorm:"serializer:json"`
SystemSerialNumber string SystemSerialNumber string
SystemProductName string SystemProductName string
SystemManufacturer string SystemManufacturer string
Environment Environment `gorm:"serializer:json"`
RosenpassEnabled bool
RosenpassPermissive bool
ServerSSHAllowed bool
} }
func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool { func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool {
@@ -119,7 +131,12 @@ func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool {
p.UIVersion == other.UIVersion && p.UIVersion == other.UIVersion &&
p.SystemSerialNumber == other.SystemSerialNumber && p.SystemSerialNumber == other.SystemSerialNumber &&
p.SystemProductName == other.SystemProductName && p.SystemProductName == other.SystemProductName &&
p.SystemManufacturer == other.SystemManufacturer p.SystemManufacturer == other.SystemManufacturer &&
p.Environment.Cloud == other.Environment.Cloud &&
p.Environment.Platform == other.Environment.Platform &&
p.RosenpassEnabled == other.RosenpassEnabled &&
p.RosenpassPermissive == other.RosenpassPermissive &&
p.ServerSSHAllowed == other.ServerSSHAllowed
} }
// AddedWithSSOLogin indicates whether this peer has been added with an SSO login by a user. // AddedWithSSOLogin indicates whether this peer has been added with an SSO login by a user.
@@ -148,6 +165,7 @@ func (p *Peer) Copy() *Peer {
SSHEnabled: p.SSHEnabled, SSHEnabled: p.SSHEnabled,
LoginExpirationEnabled: p.LoginExpirationEnabled, LoginExpirationEnabled: p.LoginExpirationEnabled,
LastLogin: p.LastLogin, LastLogin: p.LastLogin,
CreatedAt: p.CreatedAt,
Ephemeral: p.Ephemeral, Ephemeral: p.Ephemeral,
Location: p.Location, Location: p.Location,
} }
@@ -204,7 +222,7 @@ func (p *Peer) FQDN(dnsDomain string) string {
// EventMeta returns activity event meta related to the peer // EventMeta returns activity event meta related to the peer
func (p *Peer) EventMeta(dnsDomain string) map[string]any { func (p *Peer) EventMeta(dnsDomain string) map[string]any {
return map[string]any{"name": p.Name, "fqdn": p.FQDN(dnsDomain), "ip": p.IP} return map[string]any{"name": p.Name, "fqdn": p.FQDN(dnsDomain), "ip": p.IP, "created_at": p.CreatedAt}
} }
// Copy PeerStatus // Copy PeerStatus

View File

@@ -10,10 +10,10 @@ import (
) )
const ( const (
NBVersionCheckName = "NBVersionCheck" NBVersionCheckName = "NBVersionCheck"
OSVersionCheckName = "OSVersionCheck" OSVersionCheckName = "OSVersionCheck"
GeoLocationCheckName = "GeoLocationCheck" GeoLocationCheckName = "GeoLocationCheck"
PrivateNetworkCheckName = "PrivateNetworkCheck" PeerNetworkRangeCheckName = "PeerNetworkRangeCheck"
CheckActionAllow string = "allow" CheckActionAllow string = "allow"
CheckActionDeny string = "deny" CheckActionDeny string = "deny"
@@ -44,10 +44,10 @@ type Checks struct {
// ChecksDefinition contains definition of actual check // ChecksDefinition contains definition of actual check
type ChecksDefinition struct { type ChecksDefinition struct {
NBVersionCheck *NBVersionCheck `json:",omitempty"` NBVersionCheck *NBVersionCheck `json:",omitempty"`
OSVersionCheck *OSVersionCheck `json:",omitempty"` OSVersionCheck *OSVersionCheck `json:",omitempty"`
GeoLocationCheck *GeoLocationCheck `json:",omitempty"` GeoLocationCheck *GeoLocationCheck `json:",omitempty"`
PrivateNetworkCheck *PrivateNetworkCheck `json:",omitempty"` PeerNetworkRangeCheck *PeerNetworkRangeCheck `json:",omitempty"`
} }
// Copy returns a copy of a checks definition. // Copy returns a copy of a checks definition.
@@ -85,13 +85,13 @@ func (cd ChecksDefinition) Copy() ChecksDefinition {
} }
copy(cdCopy.GeoLocationCheck.Locations, geoCheck.Locations) copy(cdCopy.GeoLocationCheck.Locations, geoCheck.Locations)
} }
if cd.PrivateNetworkCheck != nil { if cd.PeerNetworkRangeCheck != nil {
privateNetCheck := cd.PrivateNetworkCheck peerNetRangeCheck := cd.PeerNetworkRangeCheck
cdCopy.PrivateNetworkCheck = &PrivateNetworkCheck{ cdCopy.PeerNetworkRangeCheck = &PeerNetworkRangeCheck{
Action: privateNetCheck.Action, Action: peerNetRangeCheck.Action,
Ranges: make([]netip.Prefix, len(privateNetCheck.Ranges)), Ranges: make([]netip.Prefix, len(peerNetRangeCheck.Ranges)),
} }
copy(cdCopy.PrivateNetworkCheck.Ranges, privateNetCheck.Ranges) copy(cdCopy.PeerNetworkRangeCheck.Ranges, peerNetRangeCheck.Ranges)
} }
return cdCopy return cdCopy
} }
@@ -130,8 +130,8 @@ func (pc *Checks) GetChecks() []Check {
if pc.Checks.GeoLocationCheck != nil { if pc.Checks.GeoLocationCheck != nil {
checks = append(checks, pc.Checks.GeoLocationCheck) checks = append(checks, pc.Checks.GeoLocationCheck)
} }
if pc.Checks.PrivateNetworkCheck != nil { if pc.Checks.PeerNetworkRangeCheck != nil {
checks = append(checks, pc.Checks.PrivateNetworkCheck) checks = append(checks, pc.Checks.PeerNetworkRangeCheck)
} }
return checks return checks
} }

View File

@@ -254,7 +254,7 @@ func TestChecks_Copy(t *testing.T) {
}, },
Action: CheckActionAllow, Action: CheckActionAllow,
}, },
PrivateNetworkCheck: &PrivateNetworkCheck{ PeerNetworkRangeCheck: &PeerNetworkRangeCheck{
Ranges: []netip.Prefix{ Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"), netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("10.0.0.0/8"),

View File

@@ -8,16 +8,16 @@ import (
nbpeer "github.com/netbirdio/netbird/management/server/peer" nbpeer "github.com/netbirdio/netbird/management/server/peer"
) )
type PrivateNetworkCheck struct { type PeerNetworkRangeCheck struct {
Action string Action string
Ranges []netip.Prefix `gorm:"serializer:json"` Ranges []netip.Prefix `gorm:"serializer:json"`
} }
var _ Check = (*PrivateNetworkCheck)(nil) var _ Check = (*PeerNetworkRangeCheck)(nil)
func (p *PrivateNetworkCheck) Check(peer nbpeer.Peer) (bool, error) { func (p *PeerNetworkRangeCheck) Check(peer nbpeer.Peer) (bool, error) {
if len(peer.Meta.NetworkAddresses) == 0 { if len(peer.Meta.NetworkAddresses) == 0 {
return false, fmt.Errorf("peer's does not contain private network addresses") return false, fmt.Errorf("peer's does not contain peer network range addresses")
} }
maskedPrefixes := make([]netip.Prefix, 0, len(p.Ranges)) maskedPrefixes := make([]netip.Prefix, 0, len(p.Ranges))
@@ -34,7 +34,7 @@ func (p *PrivateNetworkCheck) Check(peer nbpeer.Peer) (bool, error) {
case CheckActionAllow: case CheckActionAllow:
return true, nil return true, nil
default: default:
return false, fmt.Errorf("invalid private network check action: %s", p.Action) return false, fmt.Errorf("invalid peer network range check action: %s", p.Action)
} }
} }
} }
@@ -46,9 +46,9 @@ func (p *PrivateNetworkCheck) Check(peer nbpeer.Peer) (bool, error) {
return false, nil return false, nil
} }
return false, fmt.Errorf("invalid private network check action: %s", p.Action) return false, fmt.Errorf("invalid peer network range check action: %s", p.Action)
} }
func (p *PrivateNetworkCheck) Name() string { func (p *PeerNetworkRangeCheck) Name() string {
return PrivateNetworkCheckName return PeerNetworkRangeCheckName
} }

View File

@@ -9,17 +9,17 @@ import (
nbpeer "github.com/netbirdio/netbird/management/server/peer" nbpeer "github.com/netbirdio/netbird/management/server/peer"
) )
func TestPrivateNetworkCheck_Check(t *testing.T) { func TestPeerNetworkRangeCheck_Check(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
check PrivateNetworkCheck check PeerNetworkRangeCheck
peer nbpeer.Peer peer nbpeer.Peer
wantErr bool wantErr bool
isValid bool isValid bool
}{ }{
{ {
name: "Peer private networks matches the allowed range", name: "Peer networks range matches the allowed range",
check: PrivateNetworkCheck{ check: PeerNetworkRangeCheck{
Action: CheckActionAllow, Action: CheckActionAllow,
Ranges: []netip.Prefix{ Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"), netip.MustParsePrefix("192.168.0.0/24"),
@@ -42,8 +42,8 @@ func TestPrivateNetworkCheck_Check(t *testing.T) {
isValid: true, isValid: true,
}, },
{ {
name: "Peer private networks doesn't matches the allowed range", name: "Peer networks range doesn't matches the allowed range",
check: PrivateNetworkCheck{ check: PeerNetworkRangeCheck{
Action: CheckActionAllow, Action: CheckActionAllow,
Ranges: []netip.Prefix{ Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"), netip.MustParsePrefix("192.168.0.0/24"),
@@ -63,8 +63,8 @@ func TestPrivateNetworkCheck_Check(t *testing.T) {
isValid: false, isValid: false,
}, },
{ {
name: "Peer with no privates network in the allow range", name: "Peer with no network range in the allow range",
check: PrivateNetworkCheck{ check: PeerNetworkRangeCheck{
Action: CheckActionAllow, Action: CheckActionAllow,
Ranges: []netip.Prefix{ Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/16"), netip.MustParsePrefix("192.168.0.0/16"),
@@ -76,8 +76,8 @@ func TestPrivateNetworkCheck_Check(t *testing.T) {
isValid: false, isValid: false,
}, },
{ {
name: "Peer private networks matches the denied range", name: "Peer networks range matches the denied range",
check: PrivateNetworkCheck{ check: PeerNetworkRangeCheck{
Action: CheckActionDeny, Action: CheckActionDeny,
Ranges: []netip.Prefix{ Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"), netip.MustParsePrefix("192.168.0.0/24"),
@@ -100,8 +100,8 @@ func TestPrivateNetworkCheck_Check(t *testing.T) {
isValid: false, isValid: false,
}, },
{ {
name: "Peer private networks doesn't matches the denied range", name: "Peer networks range doesn't matches the denied range",
check: PrivateNetworkCheck{ check: PeerNetworkRangeCheck{
Action: CheckActionDeny, Action: CheckActionDeny,
Ranges: []netip.Prefix{ Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"), netip.MustParsePrefix("192.168.0.0/24"),
@@ -121,8 +121,8 @@ func TestPrivateNetworkCheck_Check(t *testing.T) {
isValid: true, isValid: true,
}, },
{ {
name: "Peer with no private networks in the denied range", name: "Peer with no networks range in the denied range",
check: PrivateNetworkCheck{ check: PeerNetworkRangeCheck{
Action: CheckActionDeny, Action: CheckActionDeny,
Ranges: []netip.Prefix{ Ranges: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/16"), netip.MustParsePrefix("192.168.0.0/16"),

View File

@@ -1,9 +1,10 @@
package server package server
import ( import (
log "github.com/sirupsen/logrus"
"sync" "sync"
"time" "time"
log "github.com/sirupsen/logrus"
) )
// Scheduler is an interface which implementations can schedule and cancel jobs // Scheduler is an interface which implementations can schedule and cancel jobs
@@ -55,14 +56,8 @@ func (wm *DefaultScheduler) cancel(ID string) bool {
cancel, ok := wm.jobs[ID] cancel, ok := wm.jobs[ID]
if ok { if ok {
delete(wm.jobs, ID) delete(wm.jobs, ID)
select { close(cancel)
case cancel <- struct{}{}: log.Debugf("cancelled scheduled job %s", ID)
log.Debugf("cancelled scheduled job %s", ID)
default:
log.Warnf("couldn't cancel job %s because there was no routine listening on the cancel event", ID)
return false
}
} }
return ok return ok
} }
@@ -90,25 +85,41 @@ func (wm *DefaultScheduler) Schedule(in time.Duration, ID string, job func() (ne
return return
} }
ticker := time.NewTicker(in)
wm.jobs[ID] = cancel wm.jobs[ID] = cancel
log.Debugf("scheduled a job %s to run in %s. There are %d total jobs scheduled.", ID, in.String(), len(wm.jobs)) log.Debugf("scheduled a job %s to run in %s. There are %d total jobs scheduled.", ID, in.String(), len(wm.jobs))
go func() { go func() {
select { for {
case <-time.After(in): select {
log.Debugf("time to do a scheduled job %s", ID) case <-ticker.C:
runIn, reschedule := job() select {
wm.mu.Lock() case <-cancel:
defer wm.mu.Unlock() log.Debugf("scheduled job %s was canceled, stop timer", ID)
delete(wm.jobs, ID) ticker.Stop()
if reschedule { return
go wm.Schedule(runIn, ID, job) default:
log.Debugf("time to do a scheduled job %s", ID)
}
runIn, reschedule := job()
if !reschedule {
wm.mu.Lock()
defer wm.mu.Unlock()
delete(wm.jobs, ID)
log.Debugf("job %s is not scheduled to run again", ID)
ticker.Stop()
return
}
// we need this comparison to avoid resetting the ticker with the same duration and missing the current elapsesed time
if runIn != in {
ticker.Reset(runIn)
}
case <-cancel:
log.Debugf("job %s was canceled, stopping timer", ID)
ticker.Stop()
return
} }
case <-cancel:
log.Debugf("stopped scheduled job %s ", ID)
wm.mu.Lock()
defer wm.mu.Unlock()
delete(wm.jobs, ID)
return
} }
}() }()
} }

View File

@@ -2,11 +2,12 @@ package server
import ( import (
"fmt" "fmt"
"github.com/stretchr/testify/assert"
"math/rand" "math/rand"
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
) )
func TestScheduler_Performance(t *testing.T) { func TestScheduler_Performance(t *testing.T) {
@@ -36,15 +37,24 @@ func TestScheduler_Cancel(t *testing.T) {
jobID1 := "test-scheduler-job-1" jobID1 := "test-scheduler-job-1"
jobID2 := "test-scheduler-job-2" jobID2 := "test-scheduler-job-2"
scheduler := NewDefaultScheduler() scheduler := NewDefaultScheduler()
scheduler.Schedule(2*time.Second, jobID1, func() (nextRunIn time.Duration, reschedule bool) { tChan := make(chan struct{})
return 0, false p := []string{jobID1, jobID2}
scheduler.Schedule(2*time.Millisecond, jobID1, func() (nextRunIn time.Duration, reschedule bool) {
tt := p[0]
<-tChan
t.Logf("job %s", tt)
return 2 * time.Millisecond, true
}) })
scheduler.Schedule(2*time.Second, jobID2, func() (nextRunIn time.Duration, reschedule bool) { scheduler.Schedule(2*time.Millisecond, jobID2, func() (nextRunIn time.Duration, reschedule bool) {
return 0, false return 2 * time.Millisecond, true
}) })
time.Sleep(4 * time.Millisecond)
assert.Len(t, scheduler.jobs, 2) assert.Len(t, scheduler.jobs, 2)
scheduler.Cancel([]string{jobID1}) scheduler.Cancel([]string{jobID1})
close(tChan)
p = []string{}
time.Sleep(4 * time.Millisecond)
assert.Len(t, scheduler.jobs, 1) assert.Len(t, scheduler.jobs, 1)
assert.NotNil(t, scheduler.jobs[jobID2]) assert.NotNil(t, scheduler.jobs[jobID2])
} }

View File

@@ -1,7 +1,7 @@
package server package server
import ( import (
"context" "errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
"runtime" "runtime"
@@ -256,7 +256,11 @@ func (s *SqliteStore) SavePeerStatus(accountID, peerID string, peerStatus nbpeer
result := s.db.First(&peer, "account_id = ? and id = ?", accountID, peerID) result := s.db.First(&peer, "account_id = ? and id = ?", accountID, peerID)
if result.Error != nil { if result.Error != nil {
return status.Errorf(status.NotFound, "peer %s not found", peerID) if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return status.Errorf(status.NotFound, "peer %s not found", peerID)
}
log.Errorf("error when getting peer from the store: %s", result.Error)
return status.Errorf(status.Internal, "issue getting peer from store")
} }
peer.Status = &peerStatus peer.Status = &peerStatus
@@ -268,7 +272,11 @@ func (s *SqliteStore) SavePeerLocation(accountID string, peerWithLocation *nbpee
var peer nbpeer.Peer var peer nbpeer.Peer
result := s.db.First(&peer, "account_id = ? and id = ?", accountID, peerWithLocation.ID) result := s.db.First(&peer, "account_id = ? and id = ?", accountID, peerWithLocation.ID)
if result.Error != nil { if result.Error != nil {
return status.Errorf(status.NotFound, "peer %s not found", peer.ID) if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return status.Errorf(status.NotFound, "peer %s not found", peer.ID)
}
log.Errorf("error when getting peer from the store: %s", result.Error)
return status.Errorf(status.Internal, "issue getting peer from store")
} }
peer.Location = peerWithLocation.Location peer.Location = peerWithLocation.Location
@@ -292,7 +300,11 @@ func (s *SqliteStore) GetAccountByPrivateDomain(domain string) (*Account, error)
result := s.db.First(&account, "domain = ? and is_domain_primary_account = ? and domain_category = ?", result := s.db.First(&account, "domain = ? and is_domain_primary_account = ? and domain_category = ?",
strings.ToLower(domain), true, PrivateCategory) strings.ToLower(domain), true, PrivateCategory)
if result.Error != nil { if result.Error != nil {
return nil, status.Errorf(status.NotFound, "account not found: provided domain is not registered or is not private") if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.Errorf(status.NotFound, "account not found: provided domain is not registered or is not private")
}
log.Errorf("error when getting account from the store: %s", result.Error)
return nil, status.Errorf(status.Internal, "issue getting account from store")
} }
// TODO: rework to not call GetAccount // TODO: rework to not call GetAccount
@@ -303,7 +315,11 @@ func (s *SqliteStore) GetAccountBySetupKey(setupKey string) (*Account, error) {
var key SetupKey var key SetupKey
result := s.db.Select("account_id").First(&key, "key = ?", strings.ToUpper(setupKey)) result := s.db.Select("account_id").First(&key, "key = ?", strings.ToUpper(setupKey))
if result.Error != nil { if result.Error != nil {
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed")
}
log.Errorf("error when getting setup key from the store: %s", result.Error)
return nil, status.Errorf(status.Internal, "issue getting setup key from store")
} }
if key.AccountID == "" { if key.AccountID == "" {
@@ -317,7 +333,11 @@ func (s *SqliteStore) GetTokenIDByHashedToken(hashedToken string) (string, error
var token PersonalAccessToken var token PersonalAccessToken
result := s.db.First(&token, "hashed_token = ?", hashedToken) result := s.db.First(&token, "hashed_token = ?", hashedToken)
if result.Error != nil { if result.Error != nil {
return "", status.Errorf(status.NotFound, "account not found: index lookup failed") if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return "", status.Errorf(status.NotFound, "account not found: index lookup failed")
}
log.Errorf("error when getting token from the store: %s", result.Error)
return "", status.Errorf(status.Internal, "issue getting account from store")
} }
return token.ID, nil return token.ID, nil
@@ -327,7 +347,11 @@ func (s *SqliteStore) GetUserByTokenID(tokenID string) (*User, error) {
var token PersonalAccessToken var token PersonalAccessToken
result := s.db.First(&token, "id = ?", tokenID) result := s.db.First(&token, "id = ?", tokenID)
if result.Error != nil { if result.Error != nil {
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed")
}
log.Errorf("error when getting token from the store: %s", result.Error)
return nil, status.Errorf(status.Internal, "issue getting account from store")
} }
if token.UserID == "" { if token.UserID == "" {
@@ -371,8 +395,11 @@ func (s *SqliteStore) GetAccount(accountID string) (*Account, error) {
Preload(clause.Associations). Preload(clause.Associations).
First(&account, "id = ?", accountID) First(&account, "id = ?", accountID)
if result.Error != nil { if result.Error != nil {
log.Errorf("when getting account from the store: %s", result.Error) log.Errorf("error when getting account from the store: %s", result.Error)
return nil, status.Errorf(status.NotFound, "account not found") if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.Errorf(status.NotFound, "account not found")
}
return nil, status.Errorf(status.Internal, "issue getting account from store")
} }
// we have to manually preload policy rules as it seems that gorm preloading doesn't do it for us // we have to manually preload policy rules as it seems that gorm preloading doesn't do it for us
@@ -432,7 +459,11 @@ func (s *SqliteStore) GetAccountByUser(userID string) (*Account, error) {
var user User var user User
result := s.db.Select("account_id").First(&user, "id = ?", userID) result := s.db.Select("account_id").First(&user, "id = ?", userID)
if result.Error != nil { if result.Error != nil {
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed")
}
log.Errorf("error when getting user from the store: %s", result.Error)
return nil, status.Errorf(status.Internal, "issue getting account from store")
} }
if user.AccountID == "" { if user.AccountID == "" {
@@ -446,7 +477,11 @@ func (s *SqliteStore) GetAccountByPeerID(peerID string) (*Account, error) {
var peer nbpeer.Peer var peer nbpeer.Peer
result := s.db.Select("account_id").First(&peer, "id = ?", peerID) result := s.db.Select("account_id").First(&peer, "id = ?", peerID)
if result.Error != nil { if result.Error != nil {
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed")
}
log.Errorf("error when getting peer from the store: %s", result.Error)
return nil, status.Errorf(status.Internal, "issue getting account from store")
} }
if peer.AccountID == "" { if peer.AccountID == "" {
@@ -461,7 +496,11 @@ func (s *SqliteStore) GetAccountByPeerPubKey(peerKey string) (*Account, error) {
result := s.db.Select("account_id").First(&peer, "key = ?", peerKey) result := s.db.Select("account_id").First(&peer, "key = ?", peerKey)
if result.Error != nil { if result.Error != nil {
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed")
}
log.Errorf("error when getting peer from the store: %s", result.Error)
return nil, status.Errorf(status.Internal, "issue getting account from store")
} }
if peer.AccountID == "" { if peer.AccountID == "" {
@@ -477,7 +516,11 @@ func (s *SqliteStore) SaveUserLastLogin(accountID, userID string, lastLogin time
result := s.db.First(&user, "account_id = ? and id = ?", accountID, userID) result := s.db.First(&user, "account_id = ? and id = ?", accountID, userID)
if result.Error != nil { if result.Error != nil {
return status.Errorf(status.NotFound, "user %s not found", userID) if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return status.Errorf(status.NotFound, "user %s not found", userID)
}
log.Errorf("error when getting user from the store: %s", result.Error)
return status.Errorf(status.Internal, "issue getting user from store")
} }
user.LastLogin = lastLogin user.LastLogin = lastLogin
@@ -498,48 +541,3 @@ func (s *SqliteStore) Close() error {
func (s *SqliteStore) GetStoreEngine() StoreEngine { func (s *SqliteStore) GetStoreEngine() StoreEngine {
return SqliteStoreEngine return SqliteStoreEngine
} }
// CalculateUsageStats returns the usage stats for an account
// start and end are inclusive.
func (s *SqliteStore) CalculateUsageStats(ctx context.Context, accountID string, start time.Time, end time.Time) (*AccountUsageStats, error) {
stats := &AccountUsageStats{}
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err := tx.Model(&nbpeer.Peer{}).
Where("account_id = ? AND peer_status_last_seen BETWEEN ? AND ?", accountID, start, end).
Distinct("user_id").
Count(&stats.ActiveUsers).Error
if err != nil {
return fmt.Errorf("get active users: %w", err)
}
err = tx.Model(&User{}).
Where("account_id = ? AND is_service_user = ?", accountID, false).
Count(&stats.TotalUsers).Error
if err != nil {
return fmt.Errorf("get total users: %w", err)
}
err = tx.Model(&nbpeer.Peer{}).
Where("account_id = ? AND peer_status_last_seen BETWEEN ? AND ?", accountID, start, end).
Count(&stats.ActivePeers).Error
if err != nil {
return fmt.Errorf("get active peers: %w", err)
}
err = tx.Model(&nbpeer.Peer{}).
Where("account_id = ?", accountID).
Count(&stats.TotalPeers).Error
if err != nil {
return fmt.Errorf("get total peers: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("transaction: %w", err)
}
return stats, nil
}

View File

@@ -1,7 +1,6 @@
package server package server
import ( import (
"context"
"fmt" "fmt"
"net" "net"
"path/filepath" "path/filepath"
@@ -13,6 +12,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/management/server/status"
nbpeer "github.com/netbirdio/netbird/management/server/peer" nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/util"
) )
@@ -175,6 +176,26 @@ func TestSqlite_DeleteAccount(t *testing.T) {
} }
func TestSqlite_GetAccount(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("The SQLite store is not properly supported by Windows yet")
}
store := newSqliteStoreFromFile(t, "testdata/store.json")
id := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
account, err := store.GetAccount(id)
require.NoError(t, err)
require.Equal(t, id, account.Id, "account id should match")
_, err = store.GetAccount("non-existing-account")
assert.Error(t, err)
parsedErr, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
}
func TestSqlite_SavePeerStatus(t *testing.T) { func TestSqlite_SavePeerStatus(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.Skip("The SQLite store is not properly supported by Windows yet") t.Skip("The SQLite store is not properly supported by Windows yet")
@@ -189,6 +210,9 @@ func TestSqlite_SavePeerStatus(t *testing.T) {
newStatus := nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()} newStatus := nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}
err = store.SavePeerStatus(account.Id, "non-existing-peer", newStatus) err = store.SavePeerStatus(account.Id, "non-existing-peer", newStatus)
assert.Error(t, err) assert.Error(t, err)
parsedErr, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
// save new status of existing peer // save new status of existing peer
account.Peers["testpeer"] = &nbpeer.Peer{ account.Peers["testpeer"] = &nbpeer.Peer{
@@ -255,6 +279,13 @@ func TestSqlite_SavePeerLocation(t *testing.T) {
actual := account.Peers[peer.ID].Location actual := account.Peers[peer.ID].Location
assert.Equal(t, peer.Location, actual) assert.Equal(t, peer.Location, actual)
peer.ID = "non-existing-peer"
err = store.SavePeerLocation(account.Id, peer)
assert.Error(t, err)
parsedErr, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
} }
func TestSqlite_TestGetAccountByPrivateDomain(t *testing.T) { func TestSqlite_TestGetAccountByPrivateDomain(t *testing.T) {
@@ -272,6 +303,9 @@ func TestSqlite_TestGetAccountByPrivateDomain(t *testing.T) {
_, err = store.GetAccountByPrivateDomain("missing-domain.com") _, err = store.GetAccountByPrivateDomain("missing-domain.com")
require.Error(t, err, "should return error on domain lookup") require.Error(t, err, "should return error on domain lookup")
parsedErr, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
} }
func TestSqlite_GetTokenIDByHashedToken(t *testing.T) { func TestSqlite_GetTokenIDByHashedToken(t *testing.T) {
@@ -287,6 +321,12 @@ func TestSqlite_GetTokenIDByHashedToken(t *testing.T) {
token, err := store.GetTokenIDByHashedToken(hashed) token, err := store.GetTokenIDByHashedToken(hashed)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, id, token) require.Equal(t, id, token)
_, err = store.GetTokenIDByHashedToken("non-existing-hash")
require.Error(t, err)
parsedErr, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
} }
func TestSqlite_GetUserByTokenID(t *testing.T) { func TestSqlite_GetUserByTokenID(t *testing.T) {
@@ -301,6 +341,12 @@ func TestSqlite_GetUserByTokenID(t *testing.T) {
user, err := store.GetUserByTokenID(id) user, err := store.GetUserByTokenID(id)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, id, user.PATs[id].ID) require.Equal(t, id, user.PATs[id].ID)
_, err = store.GetUserByTokenID("non-existing-id")
require.Error(t, err)
parsedErr, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
} }
func newSqliteStore(t *testing.T) *SqliteStore { func newSqliteStore(t *testing.T) *SqliteStore {
@@ -347,29 +393,3 @@ func newAccount(store Store, id int) error {
return store.SaveAccount(account) return store.SaveAccount(account)
} }
func TestSqliteStore_CalculateUsageStats(t *testing.T) {
store := newSqliteStoreFromFile(t, "testdata/store_stats.json")
t.Cleanup(func() {
require.NoError(t, store.Close())
})
startDate := time.Date(2024, time.February, 1, 0, 0, 0, 0, time.UTC)
endDate := startDate.AddDate(0, 1, 0).Add(-time.Nanosecond)
stats1, err := store.CalculateUsageStats(context.TODO(), "account-1", startDate, endDate)
require.NoError(t, err)
assert.Equal(t, int64(2), stats1.ActiveUsers)
assert.Equal(t, int64(4), stats1.TotalUsers)
assert.Equal(t, int64(3), stats1.ActivePeers)
assert.Equal(t, int64(7), stats1.TotalPeers)
stats2, err := store.CalculateUsageStats(context.TODO(), "account-2", startDate, endDate)
require.NoError(t, err)
assert.Equal(t, int64(1), stats2.ActiveUsers)
assert.Equal(t, int64(2), stats2.TotalUsers)
assert.Equal(t, int64(1), stats2.ActivePeers)
assert.Equal(t, int64(2), stats2.TotalPeers)
}

View File

@@ -1,7 +1,6 @@
package server package server
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -42,7 +41,6 @@ type Store interface {
// GetStoreEngine should return StoreEngine of the current store implementation. // GetStoreEngine should return StoreEngine of the current store implementation.
// This is also a method of metrics.DataSource interface. // This is also a method of metrics.DataSource interface.
GetStoreEngine() StoreEngine GetStoreEngine() StoreEngine
CalculateUsageStats(ctx context.Context, accountID string, start time.Time, end time.Time) (*AccountUsageStats, error)
} }
type StoreEngine string type StoreEngine string

View File

@@ -1,161 +0,0 @@
{
"Accounts": {
"account-1": {
"Id": "account-1",
"Domain": "example.com",
"Network": {
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
"Net": {
"IP": "100.64.0.0",
"Mask": "//8AAA=="
},
"Dns": null
},
"Users": {
"user-1-account-1": {
"Id": "user-1-account-1"
},
"user-2-account-1": {
"Id": "user-2-account-1"
},
"user-3-account-1": {
"Id": "user-3-account-1"
},
"user-4-account-1": {
"Id": "user-4-account-1"
},
"user-5-account-1": {
"Id": "user-5-account-1",
"IsServiceUser": true
}
},
"Peers": {
"peer-1-account-1": {
"ID": "peer-1-account-1",
"UserID": "user-1-account-1",
"Status": {
"LastSeen": "2024-01-01T00:00:00Z"
},
"Name": "Peer One",
"Meta": {
"Hostname": "peer1-host"
}
},
"peer-2-account-1": {
"ID": "peer-2-account-1",
"UserID": "user-2-account-1",
"Status": {
"LastSeen": "2024-02-29T23:59:59Z"
},
"Name": "Peer Two",
"Meta": {
"Hostname": "peer2-host"
}
},
"peer-3-account-1": {
"ID": "peer-3-account-1",
"UserID": "user-2-account-1",
"Status": {
"LastSeen": "2024-02-01T12:00:00Z"
},
"Name": "Peer Three",
"Meta": {
"Hostname": "peer3-host"
}
},
"peer-4-account-1": {
"ID": "peer-4-account-1",
"UserID": "user-3-account-1",
"Status": {
"LastSeen": "2024-02-08T12:00:00Z"
},
"Name": "Peer Four",
"Meta": {
"Hostname": "peer4-host"
}
},
"peer-5-account-1": {
"ID": "peer-5-account-1",
"UserID": "user-3-account-1",
"Status": {
"LastSeen": "2023-06-01T12:00:00Z"
},
"Name": "Peer Five",
"Meta": {
"Hostname": "peer5-host"
}
},
"peer-6-account-1": {
"ID": "peer-6-account-1",
"UserID": "user-4-account-1",
"Status": {
"LastSeen": "2024-01-31T23:59:59Z"
},
"Name": "Peer Six",
"Meta": {
"Hostname": "peer6-host"
}
},
"peer-7-account-1": {
"ID": "peer-7-account-1",
"UserID": "user-4-account-1",
"Status": {
"LastSeen": "2024-03-01T00:00:00Z"
},
"Name": "Peer Seven",
"Meta": {
"Hostname": "peer7-host"
}
}
}
},
"account-2": {
"Id": "account-2",
"Domain": "example.org",
"Network": {
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
"Net": {
"IP": "100.64.0.0",
"Mask": "//8AAA=="
},
"Dns": null
},
"Users": {
"user-1-account-2": {
"Id": "user-1-account-2"
},
"user-2-account-2": {
"Id": "user-1-account-2"
},
"user-3-account-2": {
"Id": "user-3-account-2",
"IsServiceUser": true
}
},
"Peers": {
"peer-1-account-2": {
"ID": "peer-1-account-2",
"UserID": "user-1-account-2",
"Status": {
"LastSeen": "2023-08-30T12:00:00Z"
},
"Name": "Peer One",
"Meta": {
"Hostname": "peer1-host"
}
},
"peer-2-account-2": {
"ID": "peer-2-account-2",
"UserID": "user-1-account-2",
"Status": {
"LastSeen": "2024-02-08T12:00:00Z"
},
"Name": "Peer Two",
"Meta": {
"Hostname": "peer2-host"
}
}
}
}
}
}

View File

@@ -85,6 +85,8 @@ type User struct {
Blocked bool Blocked bool
// LastLogin is the last time the user logged in to IdP // LastLogin is the last time the user logged in to IdP
LastLogin time.Time LastLogin time.Time
// CreatedAt records the time the user was created
CreatedAt time.Time
// Issued of the user // Issued of the user
Issued string `gorm:"default:api"` Issued string `gorm:"default:api"`
@@ -173,6 +175,7 @@ func (u *User) Copy() *User {
PATs: pats, PATs: pats,
Blocked: u.Blocked, Blocked: u.Blocked,
LastLogin: u.LastLogin, LastLogin: u.LastLogin,
CreatedAt: u.CreatedAt,
Issued: u.Issued, Issued: u.Issued,
IntegrationReference: u.IntegrationReference, IntegrationReference: u.IntegrationReference,
} }
@@ -188,6 +191,7 @@ func NewUser(id string, role UserRole, isServiceUser bool, nonDeletable bool, se
ServiceUserName: serviceUserName, ServiceUserName: serviceUserName,
AutoGroups: autoGroups, AutoGroups: autoGroups,
Issued: issued, Issued: issued,
CreatedAt: time.Now().UTC(),
} }
} }
@@ -338,6 +342,7 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite
AutoGroups: invite.AutoGroups, AutoGroups: invite.AutoGroups,
Issued: invite.Issued, Issued: invite.Issued,
IntegrationReference: invite.IntegrationReference, IntegrationReference: invite.IntegrationReference,
CreatedAt: time.Now().UTC(),
} }
account.Users[idpUser.ID] = newUser account.Users[idpUser.ID] = newUser
@@ -414,7 +419,7 @@ func (am *DefaultAccountManager) ListUsers(accountID string) ([]*User, error) {
} }
func (am *DefaultAccountManager) deleteServiceUser(account *Account, initiatorUserID string, targetUser *User) { func (am *DefaultAccountManager) deleteServiceUser(account *Account, initiatorUserID string, targetUser *User) {
meta := map[string]any{"name": targetUser.ServiceUserName} meta := map[string]any{"name": targetUser.ServiceUserName, "created_at": targetUser.CreatedAt}
am.StoreEvent(initiatorUserID, targetUser.Id, account.Id, activity.ServiceUserDeleted, meta) am.StoreEvent(initiatorUserID, targetUser.Id, account.Id, activity.ServiceUserDeleted, meta)
delete(account.Users, targetUser.Id) delete(account.Users, targetUser.Id)
} }
@@ -494,13 +499,23 @@ func (am *DefaultAccountManager) deleteRegularUser(account *Account, initiatorUs
return err return err
} }
u, err := account.FindUser(targetUserID)
if err != nil {
log.Errorf("failed to find user %s for deletion, this should never happen: %s", targetUserID, err)
}
var tuCreatedAt time.Time
if u != nil {
tuCreatedAt = u.CreatedAt
}
delete(account.Users, targetUserID) delete(account.Users, targetUserID)
err = am.Store.SaveAccount(account) err = am.Store.SaveAccount(account)
if err != nil { if err != nil {
return err return err
} }
meta := map[string]any{"name": tuName, "email": tuEmail} meta := map[string]any{"name": tuName, "email": tuEmail, "created_at": tuCreatedAt}
am.StoreEvent(initiatorUserID, targetUserID, account.Id, activity.UserDeleted, meta) am.StoreEvent(initiatorUserID, targetUserID, account.Id, activity.UserDeleted, meta)
am.updateAccountPeers(account) am.updateAccountPeers(account)

View File

@@ -273,7 +273,8 @@ func TestUser_Copy(t *testing.T) {
}, },
}, },
Blocked: false, Blocked: false,
LastLogin: time.Now(), LastLogin: time.Now().UTC(),
CreatedAt: time.Now().UTC(),
Issued: "test", Issued: "test",
IntegrationReference: IntegrationReference{ IntegrationReference: IntegrationReference{
ID: 0, ID: 0,

View File

@@ -21,11 +21,10 @@ import (
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/encryption" "github.com/netbirdio/netbird/encryption"
"github.com/netbirdio/netbird/management/client"
"github.com/netbirdio/netbird/signal/proto" "github.com/netbirdio/netbird/signal/proto"
) )
const defaultSendTimeout = 5 * time.Second
// ConnStateNotifier is a wrapper interface of the status recorder // ConnStateNotifier is a wrapper interface of the status recorder
type ConnStateNotifier interface { type ConnStateNotifier interface {
MarkSignalDisconnected(error) MarkSignalDisconnected(error)
@@ -71,7 +70,7 @@ func NewClient(ctx context.Context, addr string, key wgtypes.Key, tlsEnabled boo
transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})) transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))
} }
sigCtx, cancel := context.WithTimeout(ctx, 5*time.Second) sigCtx, cancel := context.WithTimeout(ctx, client.ConnectTimeout)
defer cancel() defer cancel()
conn, err := grpc.DialContext( conn, err := grpc.DialContext(
sigCtx, sigCtx,
@@ -353,7 +352,7 @@ func (c *GrpcClient) Send(msg *proto.Message) error {
return err return err
} }
attemptTimeout := defaultSendTimeout attemptTimeout := client.ConnectTimeout
for attempt := 0; attempt < 4; attempt++ { for attempt := 0; attempt < 4; attempt++ {
if attempt > 1 { if attempt > 1 {

Some files were not shown because too many files have changed in this diff Show More