Compare commits

...

31 Commits

Author SHA1 Message Date
Bethuel Mmbaga
ac0b7dc8cb Enhance linux client authentication (#1093)
The change clarifies the message usage, 
indicating that setup keys can alternatively be used 
in the authentication process. 
This approach adds flexibility in scenarios 
where automated authentication is unachievable, 
especially in non-desktop Linux environments.
2023-08-23 20:03:34 +02:00
Yury Gargay
e586eca16c Improve account copying (#1069)
With this fix, all nested slices and pointers will be copied by value.
Also, this fixes tests to compare the original and copy account by their
values by marshaling them to JSON strings.

Before that, they were copying the pointers that also passed the simple `=` compassion
(as the addresses match).
2023-08-22 17:56:39 +02:00
Misha Bragin
892db25021 docs: change get started link (#1098) 2023-08-21 09:11:52 +02:00
pascal-fischer
da75a76d41 Adding dashboard login activity (#1092)
For better auditing this PR adds a dashboard login event to the management service.

For that the user object was extended with a field for last login that is not actively saved to the database but kept in memory until next write. The information about the last login can be extracted from the JWT claims nb_last_login. This timestamp will be stored and compared on each API request. If the value changes we generate an event to inform about a login.
2023-08-18 19:23:11 +02:00
Givi Khojanashvili
3ac32fd78a Send network update when propagate user auto-groups (#1084)
For peer propagation this commit triggers
network map update in two cases:
  1) peer login
  2) user AutoGroups update

Also it issues new activity message about new user group
for peer login process.

Previous implementation only adds JWT groups to user. This fix also
removes JWT groups from user auto assign groups.

Pelase note, it also happen when user works with dashboard.
2023-08-18 15:36:05 +02:00
Bethuel Mmbaga
3aa657599b Switch OAuth flow initialization order (#1089)
Switches the order of initialization in the OAuth flow within 
the NewOAuthFlow method. Instead of initializing the 
Device Authorization Flow first, it now initializes 
the PKCE Authorization Flow first, and falls back 
to the Device Authorization Flow if the PKCE initialization fails.
2023-08-17 14:10:03 +02:00
Misha Bragin
d4e9087f94 Add peer login and expiration activity events (#1090)
Track the even of a user logging in their peer.
Track the event of a peer login expiration.
2023-08-17 14:04:04 +02:00
Zoltan Papp
da8447a67d Update the link to the doc page (#1088) 2023-08-17 12:27:04 +02:00
Misha Bragin
8e3bcd57a2 Specify invited by email when inviting a user (#1087) 2023-08-16 23:05:22 +02:00
Maycon Santos
4572c6c1f8 Avoid categorization on incoming claim (#1086)
This prevents domain categorization on claims of invited users
2023-08-16 16:11:26 +02:00
Maycon Santos
01f2b0ecb7 Add support to force using binary install (#1082)
Check if the USE_BIN_INSTALL variable is set to true and skip package manager discovery
2023-08-16 15:10:57 +02:00
Bethuel Mmbaga
442ba7cbc8 Add domain validation for nameserver groups (#1077)
This change ensures that domain names with uppercase 
letters are also considered valid, 
providing more flexibility in domain naming.
2023-08-16 11:25:38 +02:00
Maycon Santos
6c2b364966 Update client Dockerfile to use Alpine as base image and install necessary packages (#1078) 2023-08-12 16:12:09 +02:00
Zoltan Papp
0f0c7ec2ed Routemgr error handling (#1073)
In case the route management feature is not supported 
then do not create unnecessary firewall and manager instances. 
This can happen if the nftables nor iptables is not available on the host OS.

- Move the error handling to upper layer
- Remove fake, useless implementations of interfaces
- Update go-iptables because In Docker the old version can not 
determine well the path of executable file
- update lib to 0.70
2023-08-12 11:42:36 +02:00
Zoltan Papp
2dec016201 Fix/always on boot (#1062)
In case of 'always-on' feature has switched on, after the reboot the service do not start properly in all cases.
If the device is in offline state (no internet connection) the auth login steps will fail and the service will stop.
For the auth steps make no sense in this case because if the OS start the service we do not have option for
the user interaction.
2023-08-11 11:51:39 +02:00
Misha Bragin
06125acb8d Update new release banner (#1072) 2023-08-10 21:10:12 +02:00
Maycon Santos
a9b9b3fa0a Fix input reading for NetBird domain in getting-started-with-zitadel.sh (#1064) 2023-08-08 20:10:14 +02:00
Zoltan Papp
cdf57275b7 Rename eBPF program to reflect better to NetBird (#1063)
Rename program name and map name
2023-08-08 19:53:51 +02:00
Givi Khojanashvili
e5e69b1f75 Autopropagate peers by JWT groups (#1037)
Enhancements to Peer Group Assignment:

1. Auto-assigned groups are now applied to all peers every time a user logs into the network.
2. Feature activation is available in the account settings.
3. API modifications included to support these changes for account settings updates.
4. If propagation is enabled, updates to a user's auto-assigned groups are immediately reflected across all user peers.
5. With the JWT group sync feature active, auto-assigned groups are forcefully updated whenever a peer logs in using user credentials.
2023-08-07 19:44:51 +04:00
Zoltan Papp
8eca83f3cb Fix/ebpf free (#1057)
* Fix ebpf free call

* Add debug logs
2023-08-07 11:43:32 +02:00
Maycon Santos
973316d194 Validate input of expiration time for setup-keys (#1053)
So far we accepted any value for setup keys, including negative values

Now we are checking if it is less than 1 day or greater than 365 days
2023-08-04 23:54:51 +02:00
Zoltan Papp
a0a6ced148 After add listener automatically trigger peer list change event (#1044)
In case of alway-on start the peer list was invalid on Android UI.
2023-08-04 14:14:08 +02:00
Misha Bragin
0fc6c477a9 Add features links to the features table in README (#1052) 2023-08-04 11:52:11 +02:00
Misha Bragin
401a462398 Update getting started docs (#1049) 2023-08-04 11:05:05 +02:00
Zoltan Papp
a3839a6ef7 Fix error handling in iptables initialization (#1051)
* Fix error handling in iptables initialization

* Change log level
2023-08-03 22:12:36 +02:00
Maycon Santos
8aa4f240c7 Add getting started script with Zitadel (#1005)
add getting started script with zitadel

limit tests for infrastructure file workflow

limit release workflow based on relevant files
2023-08-03 19:19:17 +02:00
Zoltan Papp
d9686bae92 Handle conn store in thread safe way (#1047)
* Handle conn store in thread safe way

* Change log line

* Fix proper error handling
2023-08-03 18:24:23 +02:00
pascal-fischer
24e19ae287 revert systemd changes (#1046) 2023-08-03 00:05:13 +02:00
Maycon Santos
74fde0ea2c Update setup key auto_groups description (#1042)
* Update setup key auto_groups description

* Update setup key auto_groups description
2023-08-02 17:50:00 +02:00
pascal-fischer
890e09b787 Keep confiured nameservers as fallback (#1036)
* keep existing nameserver as fallback when adding netbird resolver

* fix resolvconf

* fix imports
2023-08-01 17:45:44 +02:00
Bethuel Mmbaga
48098c994d Handle authentication errors in PKCE flow (#1039)
* handle authentication errors in PKCE flow

* remove shadowing and replace TokenEndpoint for PKCE config

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-07-31 14:22:38 +02:00
84 changed files with 1911 additions and 869 deletions

View File

@@ -0,0 +1,35 @@
name: Test installation
on:
push:
branches:
- main
pull_request:
paths:
- "release_files/install.sh"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
jobs:
test-install-script:
strategy:
max-parallel: 2
matrix:
os: [ubuntu-latest, macos-latest]
skip_ui_mode: [true, false]
install_binary: [true, false]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: run install script
env:
SKIP_UI_APP: ${{ matrix.skip_ui_mode }}
USE_BIN_INSTALL: ${{ matrix.install_binary }}
run: |
[ "$SKIP_UI_APP" == "false" ] && export XDG_CURRENT_DESKTOP="none"
cat release_files/install.sh | sh -x
- name: check cli binary
run: command -v netbird

View File

@@ -1,60 +0,0 @@
name: Test installation Darwin
on:
push:
branches:
- main
pull_request:
paths:
- "release_files/install.sh"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
jobs:
install-cli-only:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Rename brew package
if: ${{ matrix.check_bin_install }}
run: mv /opt/homebrew/bin/brew /opt/homebrew/bin/brew.bak
- name: Run install script
run: |
sh ./release_files/install.sh
env:
SKIP_UI_APP: true
- name: Run tests
run: |
if ! command -v netbird &> /dev/null; then
echo "Error: netbird is not installed"
exit 1
fi
install-all:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Rename brew package
if: ${{ matrix.check_bin_install }}
run: mv /opt/homebrew/bin/brew /opt/homebrew/bin/brew.bak
- name: Run install script
run: |
sh ./release_files/install.sh
- name: Run tests
run: |
if ! command -v netbird &> /dev/null; then
echo "Error: netbird is not installed"
exit 1
fi
if [[ $(mdfind "kMDItemContentType == 'com.apple.application-bundle' && kMDItemFSName == '*NetBird UI.app'") ]]; then
echo "Error: NetBird UI is not installed"
exit 1
fi

View File

@@ -1,38 +0,0 @@
name: Test installation Linux
on:
push:
branches:
- main
pull_request:
paths:
- "release_files/install.sh"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
jobs:
install-cli-only:
runs-on: ubuntu-latest
strategy:
matrix:
check_bin_install: [true, false]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Rename apt package
if: ${{ matrix.check_bin_install }}
run: |
sudo mv /usr/bin/apt /usr/bin/apt.bak
sudo mv /usr/bin/apt-get /usr/bin/apt-get.bak
- name: Run install script
run: |
sh ./release_files/install.sh
- name: Run tests
run: |
if ! command -v netbird &> /dev/null; then
echo "Error: netbird is not installed"
exit 1
fi

View File

@@ -7,6 +7,16 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
paths:
- 'go.mod'
- 'go.sum'
- '.goreleaser.yml'
- '.goreleaser_ui.yaml'
- '.goreleaser_ui_darwin.yaml'
- '.github/workflows/release.yml'
- 'release_files/**'
- '**/Dockerfile'
- '**/Dockerfile.*'
env: env:
SIGN_PIPE_VER: "v0.0.8" SIGN_PIPE_VER: "v0.0.8"

View File

@@ -1,18 +1,20 @@
name: Test Docker Compose Linux name: Test Infrastructure files
on: on:
push: push:
branches: branches:
- main - main
pull_request: pull_request:
paths:
- 'infrastructure_files/**'
- '.github/workflows/test-infrastructure-files.yml'
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }} group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
test: test-docker-compose:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Install jq - name: Install jq
@@ -35,7 +37,7 @@ jobs:
${{ runner.os }}-go- ${{ runner.os }}-go-
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: cp setup.env - name: cp setup.env
run: cp infrastructure_files/tests/setup.env infrastructure_files/ run: cp infrastructure_files/tests/setup.env infrastructure_files/
@@ -121,3 +123,28 @@ jobs:
count=$(docker compose ps --format json | jq '.[] | select(.Project | contains("infrastructure_files")) | .State' | grep -c running) count=$(docker compose ps --format json | jq '.[] | select(.Project | contains("infrastructure_files")) | .State' | grep -c running)
test $count -eq 4 test $count -eq 4
working-directory: infrastructure_files working-directory: infrastructure_files
test-getting-started-script:
runs-on: ubuntu-latest
steps:
- name: Install jq
run: sudo apt-get install -y jq
- name: Checkout code
uses: actions/checkout@v3
- name: run script
run: NETBIRD_DOMAIN=use-ip bash -x infrastructure_files/getting-started-with-zitadel.sh
- name: test Caddy file gen
run: test -f Caddyfile
- name: test docker-compose file gen
run: test -f docker-compose.yml
- name: test management.json file gen
run: test -f management.json
- name: test turnserver.conf file gen
run: test -f turnserver.conf
- name: test zitadel.env file gen
run: test -f zitadel.env
- name: test dashboard.env file gen
run: test -f dashboard.env

View File

@@ -377,3 +377,13 @@ uploads:
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }} target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
username: dev@wiretrustee.com username: dev@wiretrustee.com
method: PUT method: PUT
checksum:
extra_files:
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
- glob: ./release_files/install.sh
release:
extra_files:
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
- glob: ./release_files/install.sh

View File

@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<strong>:hatching_chick: New Release! Peer expiration.</strong> <strong>:hatching_chick: New Release! Self-hosting in under 5 min.</strong>
<a href="https://github.com/netbirdio/netbird/releases"> <a href="https://github.com/netbirdio/netbird#quickstart-with-self-hosted-netbird">
Learn more Learn more
</a> </a>
</p> </p>
@@ -24,7 +24,7 @@
<p align="center"> <p align="center">
<strong> <strong>
Start using NetBird at <a href="https://app.netbird.io/">app.netbird.io</a> Start using NetBird at <a href="https://netbird.io/pricing">netbird.io</a>
<br/> <br/>
See <a href="https://netbird.io/docs/">Documentation</a> See <a href="https://netbird.io/docs/">Documentation</a>
<br/> <br/>
@@ -36,47 +36,62 @@
<br> <br>
**NetBird is an open-source VPN management platform built on top of WireGuard® making it easy to create secure private networks for your organization or home.** **NetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, making it easy to create secure private networks for your organization or home.**
It requires zero configuration effort leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth. **Connect.** NetBird creates a WireGuard-based overlay network that automatically connects your machines over an encrypted tunnel, leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
NetBird uses [NAT traversal techniques](https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment) to automatically create an overlay peer-to-peer network connecting machines regardless of location (home, office, data center, container, cloud, or edge environments), unifying virtual private network management experience. **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.
**Key features:**
- \[x] Automatic IP allocation and network management with a Web UI ([separate repo](https://github.com/netbirdio/dashboard))
- \[x] Automatic WireGuard peer (machine) discovery and configuration.
- \[x] Encrypted peer-to-peer connections without a central VPN gateway.
- \[x] Connection relay fallback in case a peer-to-peer connection is not possible.
- \[x] Desktop client applications for Linux, MacOS, and Windows (systray).
- \[x] Multiuser support - sharing network between multiple users.
- \[x] SSO and MFA support.
- \[x] Multicloud and hybrid-cloud support.
- \[x] Kernel WireGuard usage when possible.
- \[x] Access Controls - groups & rules.
- \[x] Remote SSH access without managing SSH keys.
- \[x] Network Routes.
- \[x] Private DNS.
- \[x] Network Activity Monitoring.
**Coming soon:**
- \[ ] Mobile clients.
### Secure peer-to-peer VPN with SSO and MFA in minutes ### Secure peer-to-peer VPN with SSO and MFA in minutes
https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov
**Note**: The `main` branch may be in an *unstable or even broken state* during development. ### Key features
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
### Start using NetBird | Connectivity | Management | Automation | Platforms |
- Hosted version: [https://app.netbird.io/](https://app.netbird.io/). |-------------------------------------------------------------------|--------------------------------------------------------------------------|----------------------------------------------------------------------------|---------------------------------------|
- See our documentation for [Quickstart Guide](https://docs.netbird.io/how-to/getting-started). | <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> |
- If you are looking to self-host NetBird, check our [Self-Hosting Guide](https://docs.netbird.io/selfhosted/selfhosted-guide). | <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> |
- Step-by-step [Installation Guide](https://docs.netbird.io/how-to/getting-started#installation) for different platforms. | <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> |
- Web UI [repository](https://github.com/netbirdio/dashboard). | <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> |
- 5 min [demo video](https://youtu.be/Tu9tPsUWaY0) on YouTube. | <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> - \[ ] 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] [Multiuser support](https://docs.netbird.io/how-to/add-users-to-your-network) </ul></li> | | <ul><li> - \[x] OpenWRT </ul></li> |
| | <ul><li> - \[x] [Activity logging](https://docs.netbird.io/how-to/monitor-system-and-network-activity) </ul></li> | | |
| | <ul><li> - \[x] SSH access management </ul></li> | | |
### Quickstart with NetBird Cloud
- Download and install NetBird at [https://app.netbird.io/install](https://app.netbird.io/install)
- Follow the steps to sign-up with Google, Microsoft, GitHub or your email address.
- Check NetBird [admin UI](https://app.netbird.io/).
- Add more machines.
### Quickstart with self-hosted NetBird
> This is the quickest way to try self-hosted NetBird. It should take around 5 minutes to get started if you already have a public domain and a VM.
Follow the [Advanced guide with a custom identity provider](https://docs.netbird.io/selfhosted/selfhosted-guide#advanced-guide-with-a-custom-identity-provider) for installations with different IDPs.
**Infrastructure requirements:**
- A Linux VM with at least **1CPU** and **2GB** of memory.
- The VM should be publicly accessible on TCP ports **80** and **443** and UDP ports: **3478**, **49152-65535**.
- **Public domain** name pointing to the VM.
**Software requirements:**
- Docker installed on the VM with the docker compose plugin ([Docker installation guide](https://docs.docker.com/engine/install/)) or docker with docker-compose in version 2 or higher.
- [jq](https://jqlang.github.io/jq/) installed. In most distributions
Usually available in the official repositories and can be installed with `sudo apt install jq` or `sudo yum install jq`
- [curl](https://curl.se/) installed.
Usually available in the official repositories and can be installed with `sudo apt install curl` or `sudo yum install curl`
**Steps**
- Download and run the installation script:
```bash
export NETBIRD_DOMAIN=netbird.example.com; curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started-with-zitadel.sh | bash
```
- Once finished, you can manage the resources via `docker-compose`
### A bit on NetBird internals ### A bit on NetBird internals
- Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard. - Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
- Every agent connects to [Management Service](management/) that holds network state, manages peer IPs, and distributes network updates to agents (peers). - Every agent connects to [Management Service](management/) that holds network state, manages peer IPs, and distributes network updates to agents (peers).
@@ -88,18 +103,18 @@ For stable versions, see [releases](https://github.com/netbirdio/netbird/release
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in NetBird setups. [Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in NetBird setups.
<p float="left" align="middle"> <p float="left" align="middle">
<img src="https://netbird.io/docs/img/architecture/high-level-dia.png" width="700"/> <img src="https://docs.netbird.io/docs-static/img/architecture/high-level-dia.png" width="700"/>
</p> </p>
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.
### Roadmap
- [Public Roadmap](https://github.com/netbirdio/netbird/projects/2)
### Community projects ### Community projects
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird) - [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)
**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).
### Support acknowledgement ### Support acknowledgement
In November 2022, NetBird joined the [StartUpSecure program](https://www.forschung-it-sicherheit-kommunikationssysteme.de/foerderung/bekanntmachungen/startup-secure) sponsored by The Federal Ministry of Education and Research of The Federal Republic of Germany. Together with [CISPA Helmholtz Center for Information Security](https://cispa.de/en) NetBird brings the security best practices and simplicity to private networking. In November 2022, NetBird joined the [StartUpSecure program](https://www.forschung-it-sicherheit-kommunikationssysteme.de/foerderung/bekanntmachungen/startup-secure) sponsored by The Federal Ministry of Education and Research of The Federal Republic of Germany. Together with [CISPA Helmholtz Center for Information Security](https://cispa.de/en) NetBird brings the security best practices and simplicity to private networking.
@@ -107,7 +122,7 @@ In November 2022, NetBird joined the [StartUpSecure program](https://www.forschu
![CISPA_Logo_BLACK_EN_RZ_RGB (1)](https://user-images.githubusercontent.com/700848/203091324-c6d311a0-22b5-4b05-a288-91cbc6cdcc46.png) ![CISPA_Logo_BLACK_EN_RZ_RGB (1)](https://user-images.githubusercontent.com/700848/203091324-c6d311a0-22b5-4b05-a288-91cbc6cdcc46.png)
### Testimonials ### Testimonials
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), and [Coturn](https://github.com/coturn/coturn). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution). We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), [Coturn](https://github.com/coturn/coturn), and [Rosenpass](https://rosenpass.eu). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
### Legal ### Legal
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld. _WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.

View File

@@ -1,7 +1,5 @@
FROM gcr.io/distroless/base:debug FROM alpine:3
RUN apk add --no-cache ca-certificates iptables ip6tables
ENV NB_FOREGROUND_MODE=true ENV NB_FOREGROUND_MODE=true
ENV PATH=/sbin:/usr/sbin:/bin:/usr/bin:/busybox
SHELL ["/busybox/sh","-c"]
RUN sed -i -E 's/(^root:.+)\/sbin\/nologin/\1\/busybox\/sh/g' /etc/passwd
ENTRYPOINT [ "/go/bin/netbird","up"] ENTRYPOINT [ "/go/bin/netbird","up"]
COPY netbird /go/bin/netbird COPY netbird /go/bin/netbird

View File

@@ -55,7 +55,6 @@ type Client struct {
ctxCancelLock *sync.Mutex ctxCancelLock *sync.Mutex
deviceName string deviceName string
routeListener routemanager.RouteListener routeListener routemanager.RouteListener
onHostDnsFn func([]string)
} }
// NewClient instantiate a new Client // NewClient instantiate a new Client
@@ -97,7 +96,30 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead
// todo do not throw error in case of cancelled context // todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx) ctx = internal.CtxInitState(ctx)
c.onHostDnsFn = func([]string) {} return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener, dns.items, dnsReadyListener)
}
// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
// In this case make no sense handle registration steps.
func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener) error {
cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
ConfigPath: c.cfgFile,
})
if err != nil {
return err
}
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
var ctx context.Context
//nolint
ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
c.ctxCancelLock.Lock()
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
defer c.ctxCancel()
c.ctxCancelLock.Unlock()
// todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx)
return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener, dns.items, dnsReadyListener) return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener, dns.items, dnsReadyListener)
} }

View File

@@ -3,21 +3,21 @@ package cmd
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/netbirdio/netbird/client/internal/auth" "os"
"runtime"
"strings" "strings"
"time" "time"
"github.com/skratchdot/open-golang/open" "github.com/skratchdot/open-golang/open"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status" gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/util"
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/auth"
"github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/util"
) )
var loginCmd = &cobra.Command{ var loginCmd = &cobra.Command{
@@ -191,17 +191,63 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string) { func openURL(cmd *cobra.Command, verificationURIComplete, userCode string) {
var codeMsg string var codeMsg string
if userCode != "" { if userCode != "" && !strings.Contains(verificationURIComplete, userCode) {
if !strings.Contains(verificationURIComplete, userCode) {
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode) codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
} }
browserAuthMsg := "Please do the SSO login in your browser. \n" +
"If your browser didn't open automatically, use this URL to log in:\n\n" +
verificationURIComplete + " " + codeMsg
setupKeyAuthMsg := "\nAlternatively, you may want to use a setup key, see:\n\n" +
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
authenticateUsingBrowser := func() {
cmd.Println(browserAuthMsg)
if err := open.Run(verificationURIComplete); err != nil {
cmd.Println(setupKeyAuthMsg)
}
} }
err := open.Run(verificationURIComplete) switch runtime.GOOS {
cmd.Printf("Please do the SSO login in your browser. \n" + case "windows", "darwin":
"If your browser didn't open automatically, use this URL to log in:\n\n" + authenticateUsingBrowser()
" " + verificationURIComplete + " " + codeMsg + " \n\n") case "linux":
if err != nil { if isLinuxRunningDesktop() {
cmd.Printf("Alternatively, you may want to use a setup key, see:\n\n https://www.netbird.io/docs/overview/setup-keys\n") authenticateUsingBrowser()
} else {
// If current flow is PKCE, it implies the server is anticipating the redirect to localhost.
// Devices lacking browser support are incompatible with this flow.Therefore,
// these devices will need to resort to setup keys instead.
if isPKCEFlow(verificationURIComplete) {
cmd.Println("Please proceed with setting up this device using setup keys, see:\n\n" +
"https://docs.netbird.io/how-to/register-machines-using-setup-keys")
} else {
cmd.Println(browserAuthMsg)
} }
} }
}
}
// isLinuxRunningDesktop checks if a Linux OS is running desktop environment.
func isLinuxRunningDesktop() bool {
for _, env := range os.Environ() {
values := strings.Split(env, "=")
if len(values) == 2 {
key, value := values[0], values[1]
if key == "XDG_CURRENT_DESKTOP" && value != "" {
return true
}
}
}
return false
}
// isPKCEFlow determines if the PKCE flow is active or not,
// by checking the existence of redirect_uri inside the verification URL.
func isPKCEFlow(verificationURL string) bool {
if verificationURL == "" {
return false
}
return strings.Contains(verificationURL, "redirect_uri")
}

View File

@@ -120,7 +120,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
" netbird up \n\n"+ " netbird up \n\n"+
"If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n"+ "If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n"+
"you can use a setup-key:\n\n netbird up --management-url <YOUR_MANAGEMENT_URL> --setup-key <YOUR_SETUP_KEY>\n\n"+ "you can use a setup-key:\n\n netbird up --management-url <YOUR_MANAGEMENT_URL> --setup-key <YOUR_SETUP_KEY>\n\n"+
"More info: https://www.netbird.io/docs/overview/setup-keys\n\n", "More info: https://docs.netbird.io/how-to/register-machines-using-setup-keys\n\n",
resp.GetStatus(), resp.GetStatus(),
) )
return nil return nil

View File

@@ -59,19 +59,17 @@ func (t TokenInfo) GetTokenToUse() string {
// NewOAuthFlow initializes and returns the appropriate OAuth flow based on the management configuration. // NewOAuthFlow initializes and returns the appropriate OAuth flow based on the management configuration.
func NewOAuthFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) { func NewOAuthFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) {
log.Debug("getting device authorization flow info") log.Debug("loading pkce authorization flow info")
// Try to initialize the Device Authorization Flow pkceFlowInfo, err := internal.GetPKCEAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
if err == nil { if err == nil {
return NewDeviceAuthorizationFlow(deviceFlowInfo.ProviderConfig) return NewPKCEAuthorizationFlow(pkceFlowInfo.ProviderConfig)
} }
log.Debugf("getting device authorization flow info failed with error: %v", err) log.Debugf("loading pkce authorization flow info failed with error: %v", err)
log.Debugf("falling back to pkce authorization flow info") log.Debugf("falling back to device authorization flow info")
// If Device Authorization Flow failed, try the PKCE Authorization Flow deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
pkceFlowInfo, err := internal.GetPKCEAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
if err != nil { if err != nil {
s, ok := gstatus.FromError(err) s, ok := gstatus.FromError(err)
if ok && s.Code() == codes.NotFound { if ok && s.Code() == codes.NotFound {
@@ -82,9 +80,9 @@ func NewOAuthFlow(ctx context.Context, config *internal.Config) (OAuthFlow, erro
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+ return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
"please update your server or use Setup Keys to login", config.ManagementURL) "please update your server or use Setup Keys to login", config.ManagementURL)
} else { } else {
return nil, fmt.Errorf("getting pkce authorization flow info failed with error: %v", err) return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err)
} }
} }
return NewPKCEAuthorizationFlow(pkceFlowInfo.ProviderConfig) return NewDeviceAuthorizationFlow(deviceFlowInfo.ProviderConfig)
} }

View File

@@ -25,6 +25,8 @@ var _ OAuthFlow = &PKCEAuthorizationFlow{}
const ( const (
queryState = "state" queryState = "state"
queryCode = "code" queryCode = "code"
queryError = "error"
queryErrorDesc = "error_description"
defaultPKCETimeoutSeconds = 300 defaultPKCETimeoutSeconds = 300
) )
@@ -141,9 +143,13 @@ func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errC
tokenValidatorFunc := func() (*oauth2.Token, error) { tokenValidatorFunc := func() (*oauth2.Token, error) {
query := req.URL.Query() query := req.URL.Query()
state := query.Get(queryState) if authError := query.Get(queryError); authError != "" {
authErrorDesc := query.Get(queryErrorDesc)
return nil, fmt.Errorf("%s.%s", authError, authErrorDesc)
}
// Prevent timing attacks on state // Prevent timing attacks on state
if subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 { if state := query.Get(queryState); subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 {
return nil, fmt.Errorf("invalid state") return nil, fmt.Errorf("invalid state")
} }
@@ -161,12 +167,13 @@ func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errC
token, err := tokenValidatorFunc() token, err := tokenValidatorFunc()
if err != nil { if err != nil {
errChan <- fmt.Errorf("PKCE authorization flow failed: %v", err)
renderPKCEFlowTmpl(w, err) renderPKCEFlowTmpl(w, err)
errChan <- fmt.Errorf("PKCE authorization flow failed: %v", err)
return
} }
tokenChan <- token
renderPKCEFlowTmpl(w, nil) renderPKCEFlowTmpl(w, nil)
tokenChan <- token
}) })
if err := server.ListenAndServe(); err != nil { if err := server.ListenAndServe(); err != nil {

View File

@@ -179,8 +179,6 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
log.Print("Netbird engine started, my IP is: ", peerConfig.Address) log.Print("Netbird engine started, my IP is: ", peerConfig.Address)
state.Set(StatusConnected) state.Set(StatusConnected)
statusRecorder.ClientStart()
<-engineCtx.Done() <-engineCtx.Done()
statusRecorder.ClientTeardown() statusRecorder.ClientTeardown()
@@ -201,6 +199,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
return nil return nil
} }
statusRecorder.ClientStart()
err = backoff.Retry(operation, backOff) err = backoff.Retry(operation, backOff)
if err != nil { if err != nil {
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err) log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)

View File

@@ -15,7 +15,8 @@ const (
fileGeneratedResolvConfSearchBeginContent = "search " fileGeneratedResolvConfSearchBeginContent = "search "
fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader + fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader +
"\n# If needed you can restore the original file by copying back %s\n\nnameserver %s\n" + "\n# If needed you can restore the original file by copying back %s\n\nnameserver %s\n" +
fileGeneratedResolvConfSearchBeginContent + "%s\n" fileGeneratedResolvConfSearchBeginContent + "%s\n\n" +
"%s\n"
) )
const ( const (
@@ -91,7 +92,12 @@ func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error {
searchDomains += " " + dConf.domain searchDomains += " " + dConf.domain
appendedDomains++ appendedDomains++
} }
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains)
originalContent, err := os.ReadFile(fileDefaultResolvConfBackupLocation)
if err != nil {
log.Errorf("Could not read existing resolv.conf")
}
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains, string(originalContent))
err = writeDNSConfig(content, defaultResolvConfPath, f.originalPerms) err = writeDNSConfig(content, defaultResolvConfPath, f.originalPerms)
if err != nil { if err != nil {
err = f.restore() err = f.restore()

View File

@@ -182,12 +182,11 @@ func (s *systemConfigurator) addDNSState(state, domains, dnsServer string, port
} }
func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error { func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error {
primaryServiceKey := s.getPrimaryService() primaryServiceKey, existingNameserver := s.getPrimaryService()
if primaryServiceKey == "" { if primaryServiceKey == "" {
return fmt.Errorf("couldn't find the primary service key") return fmt.Errorf("couldn't find the primary service key")
} }
err := s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port, existingNameserver)
err := s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port)
if err != nil { if err != nil {
return err return err
} }
@@ -196,27 +195,32 @@ func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error
return nil return nil
} }
func (s *systemConfigurator) getPrimaryService() string { func (s *systemConfigurator) getPrimaryService() (string, string) {
line := buildCommandLine("show", globalIPv4State, "") line := buildCommandLine("show", globalIPv4State, "")
stdinCommands := wrapCommand(line) stdinCommands := wrapCommand(line)
b, err := runSystemConfigCommand(stdinCommands) b, err := runSystemConfigCommand(stdinCommands)
if err != nil { if err != nil {
log.Error("got error while sending the command: ", err) log.Error("got error while sending the command: ", err)
return "" return "", ""
} }
scanner := bufio.NewScanner(bytes.NewReader(b)) scanner := bufio.NewScanner(bytes.NewReader(b))
primaryService := ""
router := ""
for scanner.Scan() { for scanner.Scan() {
text := scanner.Text() text := scanner.Text()
if strings.Contains(text, "PrimaryService") { if strings.Contains(text, "PrimaryService") {
return strings.TrimSpace(strings.Split(text, ":")[1]) primaryService = strings.TrimSpace(strings.Split(text, ":")[1])
}
if strings.Contains(text, "Router") {
router = strings.TrimSpace(strings.Split(text, ":")[1])
} }
} }
return "" return primaryService, router
} }
func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int) error { func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int, existingDNSServer string) error {
lines := buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+strconv.Itoa(0)) lines := buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+strconv.Itoa(0))
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer) lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer+" "+existingDNSServer)
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port)) lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
addDomainCommand := buildCreateStateWithOperation(setupKey, lines) addDomainCommand := buildCreateStateWithOperation(setupKey, lines)
stdinCommands := wrapCommand(addDomainCommand) stdinCommands := wrapCommand(addDomainCommand)

View File

@@ -4,6 +4,7 @@ package dns
import ( import (
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"strings" "strings"
@@ -59,7 +60,11 @@ func (r *resolvconf) applyDNSConfig(config hostDNSConfig) error {
appendedDomains++ appendedDomains++
} }
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains) originalContent, err := os.ReadFile(fileDefaultResolvConfBackupLocation)
if err != nil {
log.Errorf("Could not read existing resolv.conf")
}
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains, string(originalContent))
err = r.applyConfig(content) err = r.applyConfig(content)
if err != nil { if err != nil {

View File

@@ -238,7 +238,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
hostUpdate := s.currentConfig hostUpdate := s.currentConfig
if s.service.RuntimePort() != defaultPort && !s.hostManager.supportCustomPort() { if s.service.RuntimePort() != defaultPort && !s.hostManager.supportCustomPort() {
log.Warnf("the DNS manager of this peer doesn't support custom port. Disabling primary DNS setup. " + log.Warnf("the DNS manager of this peer doesn't support custom port. Disabling primary DNS setup. " +
"Learn more at: https://netbird.io/docs/how-to-guides/nameservers#local-resolver") "Learn more at: https://docs.netbird.io/how-to/manage-dns-in-your-network#local-resolver")
hostUpdate.routeAll = false hostUpdate.routeAll = false
} }

View File

@@ -17,6 +17,7 @@ type notifier struct {
listener Listener listener Listener
currentClientState bool currentClientState bool
lastNotification int lastNotification int
lastNumberOfPeers int
} }
func newNotifier() *notifier { func newNotifier() *notifier {
@@ -29,6 +30,7 @@ func (n *notifier) setListener(listener Listener) {
n.serverStateLock.Lock() n.serverStateLock.Lock()
n.notifyListener(listener, n.lastNotification) n.notifyListener(listener, n.lastNotification)
listener.OnPeersListChanged(n.lastNumberOfPeers)
n.serverStateLock.Unlock() n.serverStateLock.Unlock()
n.listener = listener n.listener = listener
@@ -59,7 +61,7 @@ func (n *notifier) clientStart() {
n.serverStateLock.Lock() n.serverStateLock.Lock()
defer n.serverStateLock.Unlock() defer n.serverStateLock.Unlock()
n.currentClientState = true n.currentClientState = true
n.lastNotification = stateConnected n.lastNotification = stateConnecting
n.notify(n.lastNotification) n.notify(n.lastNotification)
} }
@@ -112,7 +114,7 @@ func (n *notifier) calculateState(managementConn, signalConn bool) int {
return stateConnected return stateConnected
} }
if !managementConn && !signalConn { if !managementConn && !signalConn && !n.currentClientState {
return stateDisconnected return stateDisconnected
} }
@@ -124,6 +126,7 @@ func (n *notifier) calculateState(managementConn, signalConn bool) int {
} }
func (n *notifier) peerListChanged(numOfPeers int) { func (n *notifier) peerListChanged(numOfPeers int) {
n.lastNumberOfPeers = numOfPeers
n.listenersLock.Lock() n.listenersLock.Lock()
defer n.listenersLock.Unlock() defer n.listenersLock.Unlock()
if n.listener == nil { if n.listener == nil {

View File

@@ -353,9 +353,13 @@ func (d *Status) onConnectionChanged() {
} }
func (d *Status) notifyPeerListChanged() { func (d *Status) notifyPeerListChanged() {
d.notifier.peerListChanged(len(d.peers) + len(d.offlinePeers)) d.notifier.peerListChanged(d.numOfPeers())
} }
func (d *Status) notifyAddressChanged() { func (d *Status) notifyAddressChanged() {
d.notifier.localAddressChanged(d.localPeer.FQDN, d.localPeer.IP) d.notifier.localAddressChanged(d.localPeer.FQDN, d.localPeer.IP)
} }
func (d *Status) numOfPeers() int {
return len(d.peers) + len(d.offlinePeers)
}

View File

@@ -27,14 +27,19 @@ func genKey(format string, input string) string {
} }
// NewFirewall if supported, returns an iptables manager, otherwise returns a nftables manager // NewFirewall if supported, returns an iptables manager, otherwise returns a nftables manager
func NewFirewall(parentCTX context.Context) firewallManager { func NewFirewall(parentCTX context.Context) (firewallManager, error) {
manager, err := newNFTablesManager(parentCTX) manager, err := newNFTablesManager(parentCTX)
if err == nil { if err == nil {
log.Debugf("nftables firewall manager will be used") log.Debugf("nftables firewall manager will be used")
return manager return manager, nil
} }
log.Debugf("fallback to iptables firewall manager: %s", err) fMgr, err := newIptablesManager(parentCTX)
return newIptablesManager(parentCTX) if err != nil {
log.Debugf("failed to initialize iptables for root mgr: %s", err)
return nil, err
}
log.Debugf("iptables firewall manager will be used")
return fMgr, nil
} }
func getInPair(pair routerPair) routerPair { func getInPair(pair routerPair) routerPair {

View File

@@ -3,24 +3,12 @@
package routemanager package routemanager
import "context" import (
"context"
"fmt"
)
type unimplementedFirewall struct{} // NewFirewall returns a nil manager
func NewFirewall(context.Context) (firewallManager, error) {
func (unimplementedFirewall) RestoreOrCreateContainers() error { return nil, fmt.Errorf("firewall not supported on this OS")
return nil
}
func (unimplementedFirewall) InsertRoutingRules(pair routerPair) error {
return nil
}
func (unimplementedFirewall) RemoveRoutingRules(pair routerPair) error {
return nil
}
func (unimplementedFirewall) CleanRoutingRules() {
}
// NewFirewall returns an unimplemented Firewall manager
func NewFirewall(parentCtx context.Context) firewallManager {
return unimplementedFirewall{}
} }

View File

@@ -49,26 +49,29 @@ type iptablesManager struct {
mux sync.Mutex mux sync.Mutex
} }
func newIptablesManager(parentCtx context.Context) *iptablesManager { func newIptablesManager(parentCtx context.Context) (*iptablesManager, error) {
ctx, cancel := context.WithCancel(parentCtx) ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4) if err != nil {
if !isIptablesClientAvailable(ipv4Client) { return nil, err
log.Infof("iptables is missing for ipv4") } else if !isIptablesClientAvailable(ipv4Client) {
ipv4Client = nil return nil, fmt.Errorf("iptables is missing for ipv4")
} }
ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6) ipv6Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
if !isIptablesClientAvailable(ipv6Client) { if err != nil {
log.Debugf("failed to initialize iptables for ipv6: %s", err)
} else if !isIptablesClientAvailable(ipv6Client) {
log.Infof("iptables is missing for ipv6") log.Infof("iptables is missing for ipv6")
ipv6Client = nil ipv6Client = nil
} }
ctx, cancel := context.WithCancel(parentCtx)
return &iptablesManager{ return &iptablesManager{
ctx: ctx, ctx: ctx,
stop: cancel, stop: cancel,
ipv4Client: ipv4Client, ipv4Client: ipv4Client,
ipv6Client: ipv6Client, ipv6Client: ipv6Client,
rules: make(map[string]map[string][]string), rules: make(map[string]map[string][]string),
} }, nil
} }
// CleanRoutingRules cleans existing iptables resources that we created by the agent // CleanRoutingRules cleans existing iptables resources that we created by the agent
@@ -391,6 +394,10 @@ func (i *iptablesManager) insertRoutingRule(keyFormat, table, chain, jump string
ipVersion = ipv6 ipVersion = ipv6
} }
if iptablesClient == nil {
return fmt.Errorf("unable to insert iptables routing rules. Iptables client is not initialized")
}
ruleKey := genKey(keyFormat, pair.ID) ruleKey := genKey(keyFormat, pair.ID)
rule := genRuleSpec(jump, ruleKey, pair.source, pair.destination) rule := genRuleSpec(jump, ruleKey, pair.source, pair.destination)
existingRule, found := i.rules[ipVersion][ruleKey] existingRule, found := i.rules[ipVersion][ruleKey]
@@ -455,6 +462,10 @@ func (i *iptablesManager) removeRoutingRule(keyFormat, table, chain string, pair
ipVersion = ipv6 ipVersion = ipv6
} }
if iptablesClient == nil {
return fmt.Errorf("unable to remove iptables routing rules. Iptables client is not initialized")
}
ruleKey := genKey(keyFormat, pair.ID) ruleKey := genKey(keyFormat, pair.ID)
existingRule, found := i.rules[ipVersion][ruleKey] existingRule, found := i.rules[ipVersion][ruleKey]
if found { if found {

View File

@@ -16,7 +16,7 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
t.SkipNow() t.SkipNow()
} }
manager := newIptablesManager(context.TODO()) manager, _ := newIptablesManager(context.TODO())
defer manager.CleanRoutingRules() defer manager.CleanRoutingRules()

View File

@@ -27,7 +27,7 @@ type DefaultManager struct {
stop context.CancelFunc stop context.CancelFunc
mux sync.Mutex mux sync.Mutex
clientNetworks map[string]*clientNetwork clientNetworks map[string]*clientNetwork
serverRouter *serverRouter serverRouter serverRouter
statusRecorder *peer.Status statusRecorder *peer.Status
wgInterface *iface.WGIface wgInterface *iface.WGIface
pubKey string pubKey string
@@ -36,13 +36,17 @@ type DefaultManager struct {
// NewManager returns a new route manager // NewManager returns a new route manager
func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, statusRecorder *peer.Status, initialRoutes []*route.Route) *DefaultManager { func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, statusRecorder *peer.Status, initialRoutes []*route.Route) *DefaultManager {
mCTX, cancel := context.WithCancel(ctx) serverRouter, err := newServerRouter(ctx, wgInterface)
if err != nil {
log.Errorf("server router is not supported: %s", err)
}
mCTX, cancel := context.WithCancel(ctx)
dm := &DefaultManager{ dm := &DefaultManager{
ctx: mCTX, ctx: mCTX,
stop: cancel, stop: cancel,
clientNetworks: make(map[string]*clientNetwork), clientNetworks: make(map[string]*clientNetwork),
serverRouter: newServerRouter(ctx, wgInterface), serverRouter: serverRouter,
statusRecorder: statusRecorder, statusRecorder: statusRecorder,
wgInterface: wgInterface, wgInterface: wgInterface,
pubKey: pubKey, pubKey: pubKey,
@@ -59,7 +63,9 @@ func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface,
// Stop stops the manager watchers and clean firewall rules // Stop stops the manager watchers and clean firewall rules
func (m *DefaultManager) Stop() { func (m *DefaultManager) Stop() {
m.stop() m.stop()
if m.serverRouter != nil {
m.serverRouter.cleanUp() m.serverRouter.cleanUp()
}
m.ctx = nil m.ctx = nil
} }
@@ -77,10 +83,13 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro
m.updateClientNetworks(updateSerial, newClientRoutesIDMap) m.updateClientNetworks(updateSerial, newClientRoutesIDMap)
m.notifier.onNewRoutes(newClientRoutesIDMap) m.notifier.onNewRoutes(newClientRoutesIDMap)
if m.serverRouter != nil {
err := m.serverRouter.updateRoutes(newServerRoutesMap) err := m.serverRouter.updateRoutes(newServerRoutesMap)
if err != nil { if err != nil {
return err return err
} }
}
return nil return nil
} }

View File

@@ -30,7 +30,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
inputInitRoutes []*route.Route inputInitRoutes []*route.Route
inputRoutes []*route.Route inputRoutes []*route.Route
inputSerial uint64 inputSerial uint64
shouldCheckServerRoutes bool
serverRoutesExpected int serverRoutesExpected int
clientNetworkWatchersExpected int clientNetworkWatchersExpected int
}{ }{
@@ -87,7 +86,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
}, },
}, },
inputSerial: 1, inputSerial: 1,
shouldCheckServerRoutes: runtime.GOOS == "linux",
serverRoutesExpected: 2, serverRoutesExpected: 2,
clientNetworkWatchersExpected: 0, clientNetworkWatchersExpected: 0,
}, },
@@ -116,7 +114,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
}, },
}, },
inputSerial: 1, inputSerial: 1,
shouldCheckServerRoutes: runtime.GOOS == "linux",
serverRoutesExpected: 1, serverRoutesExpected: 1,
clientNetworkWatchersExpected: 1, clientNetworkWatchersExpected: 1,
}, },
@@ -174,25 +171,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
inputSerial: 1, inputSerial: 1,
clientNetworkWatchersExpected: 0, clientNetworkWatchersExpected: 0,
}, },
{
name: "No Server Routes Should Be Added To Non Linux",
inputRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: localPeerKey,
Network: netip.MustParsePrefix("1.2.3.4/32"),
NetworkType: route.IPv4Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputSerial: 1,
shouldCheckServerRoutes: runtime.GOOS != "linux",
serverRoutesExpected: 0,
clientNetworkWatchersExpected: 0,
},
{ {
name: "Remove 1 Client Route", name: "Remove 1 Client Route",
inputInitRoutes: []*route.Route{ inputInitRoutes: []*route.Route{
@@ -335,7 +313,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
}, },
inputRoutes: []*route.Route{}, inputRoutes: []*route.Route{},
inputSerial: 1, inputSerial: 1,
shouldCheckServerRoutes: true,
serverRoutesExpected: 0, serverRoutesExpected: 0,
clientNetworkWatchersExpected: 0, clientNetworkWatchersExpected: 0,
}, },
@@ -384,7 +361,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
}, },
}, },
inputSerial: 1, inputSerial: 1,
shouldCheckServerRoutes: runtime.GOOS == "linux",
serverRoutesExpected: 2, serverRoutesExpected: 2,
clientNetworkWatchersExpected: 1, clientNetworkWatchersExpected: 1,
}, },
@@ -419,8 +395,9 @@ func TestManagerUpdateRoutes(t *testing.T) {
require.Len(t, routeManager.clientNetworks, testCase.clientNetworkWatchersExpected, "client networks size should match") require.Len(t, routeManager.clientNetworks, testCase.clientNetworkWatchersExpected, "client networks size should match")
if testCase.shouldCheckServerRoutes { if runtime.GOOS == "linux" {
require.Len(t, routeManager.serverRouter.routes, testCase.serverRoutesExpected, "server networks size should match") sr := routeManager.serverRouter.(*defaultServerRouter)
require.Len(t, sr.routes, testCase.serverRoutesExpected, "server networks size should match")
} }
}) })
} }

View File

@@ -0,0 +1,9 @@
package routemanager
import "github.com/netbirdio/netbird/route"
type serverRouter interface {
updateRoutes(map[string]*route.Route) error
removeFromServerNetwork(*route.Route) error
cleanUp()
}

View File

@@ -2,20 +2,11 @@ package routemanager
import ( import (
"context" "context"
"fmt"
"github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/route"
) )
type serverRouter struct { func newServerRouter(context.Context, *iface.WGIface) (serverRouter, error) {
return nil, fmt.Errorf("server route not supported on this os")
} }
func newServerRouter(ctx context.Context, wgInterface *iface.WGIface) *serverRouter {
return &serverRouter{}
}
func (r *serverRouter) updateRoutes(routesMap map[string]*route.Route) error {
return nil
}
func (r *serverRouter) cleanUp() {}

View File

@@ -13,7 +13,7 @@ import (
"github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/route"
) )
type serverRouter 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
@@ -21,16 +21,21 @@ type serverRouter struct {
wgInterface *iface.WGIface wgInterface *iface.WGIface
} }
func newServerRouter(ctx context.Context, wgInterface *iface.WGIface) *serverRouter { func newServerRouter(ctx context.Context, wgInterface *iface.WGIface) (serverRouter, error) {
return &serverRouter{ firewall, err := NewFirewall(ctx)
ctx: ctx, if err != nil {
routes: make(map[string]*route.Route), return nil, err
firewall: NewFirewall(ctx),
wgInterface: wgInterface,
}
} }
func (m *serverRouter) updateRoutes(routesMap map[string]*route.Route) error { return &defaultServerRouter{
ctx: ctx,
routes: make(map[string]*route.Route),
firewall: firewall,
wgInterface: wgInterface,
}, nil
}
func (m *defaultServerRouter) updateRoutes(routesMap map[string]*route.Route) error {
serverRoutesToRemove := make([]string, 0) serverRoutesToRemove := make([]string, 0)
if len(routesMap) > 0 { if len(routesMap) > 0 {
@@ -81,7 +86,7 @@ func (m *serverRouter) updateRoutes(routesMap map[string]*route.Route) error {
return nil return nil
} }
func (m *serverRouter) removeFromServerNetwork(route *route.Route) error { func (m *defaultServerRouter) removeFromServerNetwork(route *route.Route) error {
select { select {
case <-m.ctx.Done(): case <-m.ctx.Done():
log.Infof("not removing from server network because context is done") log.Infof("not removing from server network because context is done")
@@ -98,7 +103,7 @@ func (m *serverRouter) removeFromServerNetwork(route *route.Route) error {
} }
} }
func (m *serverRouter) addToServerNetwork(route *route.Route) error { func (m *defaultServerRouter) addToServerNetwork(route *route.Route) error {
select { select {
case <-m.ctx.Done(): case <-m.ctx.Done():
log.Infof("not adding to server network because context is done") log.Infof("not adding to server network because context is done")
@@ -115,6 +120,6 @@ func (m *serverRouter) addToServerNetwork(route *route.Route) error {
} }
} }
func (m *serverRouter) cleanUp() { func (m *defaultServerRouter) cleanUp() {
m.firewall.CleanRoutingRules() m.firewall.CleanRoutingRules()
} }

View File

@@ -20,7 +20,7 @@ func InterfaceFilter(disallowList []string) func(string) bool {
for _, s := range disallowList { for _, s := range disallowList {
if strings.HasPrefix(iFace, s) { if strings.HasPrefix(iFace, s) {
log.Debugf("ignoring interface %s - it is not allowed", iFace) log.Tracef("ignoring interface %s - it is not allowed", iFace)
return false return false
} }
} }

View File

@@ -54,14 +54,14 @@ type bpfSpecs struct {
// //
// It can be passed ebpf.CollectionSpec.Assign. // It can be passed ebpf.CollectionSpec.Assign.
type bpfProgramSpecs struct { type bpfProgramSpecs struct {
XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"` NbWgProxy *ebpf.ProgramSpec `ebpf:"nb_wg_proxy"`
} }
// bpfMapSpecs contains maps before they are loaded into the kernel. // bpfMapSpecs contains maps before they are loaded into the kernel.
// //
// It can be passed ebpf.CollectionSpec.Assign. // It can be passed ebpf.CollectionSpec.Assign.
type bpfMapSpecs struct { type bpfMapSpecs struct {
XdpPortMap *ebpf.MapSpec `ebpf:"xdp_port_map"` NbWgProxySettingsMap *ebpf.MapSpec `ebpf:"nb_wg_proxy_settings_map"`
} }
// bpfObjects contains all objects after they have been loaded into the kernel. // bpfObjects contains all objects after they have been loaded into the kernel.
@@ -83,12 +83,12 @@ func (o *bpfObjects) Close() error {
// //
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct { type bpfMaps struct {
XdpPortMap *ebpf.Map `ebpf:"xdp_port_map"` NbWgProxySettingsMap *ebpf.Map `ebpf:"nb_wg_proxy_settings_map"`
} }
func (m *bpfMaps) Close() error { func (m *bpfMaps) Close() error {
return _BpfClose( return _BpfClose(
m.XdpPortMap, m.NbWgProxySettingsMap,
) )
} }
@@ -96,12 +96,12 @@ func (m *bpfMaps) Close() error {
// //
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfPrograms struct { type bpfPrograms struct {
XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"` NbWgProxy *ebpf.Program `ebpf:"nb_wg_proxy"`
} }
func (p *bpfPrograms) Close() error { func (p *bpfPrograms) Close() error {
return _BpfClose( return _BpfClose(
p.XdpProgFunc, p.NbWgProxy,
) )
} }

View File

@@ -54,14 +54,14 @@ type bpfSpecs struct {
// //
// It can be passed ebpf.CollectionSpec.Assign. // It can be passed ebpf.CollectionSpec.Assign.
type bpfProgramSpecs struct { type bpfProgramSpecs struct {
XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"` NbWgProxy *ebpf.ProgramSpec `ebpf:"nb_wg_proxy"`
} }
// bpfMapSpecs contains maps before they are loaded into the kernel. // bpfMapSpecs contains maps before they are loaded into the kernel.
// //
// It can be passed ebpf.CollectionSpec.Assign. // It can be passed ebpf.CollectionSpec.Assign.
type bpfMapSpecs struct { type bpfMapSpecs struct {
XdpPortMap *ebpf.MapSpec `ebpf:"xdp_port_map"` NbWgProxySettingsMap *ebpf.MapSpec `ebpf:"nb_wg_proxy_settings_map"`
} }
// bpfObjects contains all objects after they have been loaded into the kernel. // bpfObjects contains all objects after they have been loaded into the kernel.
@@ -83,12 +83,12 @@ func (o *bpfObjects) Close() error {
// //
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct { type bpfMaps struct {
XdpPortMap *ebpf.Map `ebpf:"xdp_port_map"` NbWgProxySettingsMap *ebpf.Map `ebpf:"nb_wg_proxy_settings_map"`
} }
func (m *bpfMaps) Close() error { func (m *bpfMaps) Close() error {
return _BpfClose( return _BpfClose(
m.XdpPortMap, m.NbWgProxySettingsMap,
) )
} }
@@ -96,12 +96,12 @@ func (m *bpfMaps) Close() error {
// //
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfPrograms struct { type bpfPrograms struct {
XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"` NbWgProxy *ebpf.Program `ebpf:"nb_wg_proxy"`
} }
func (p *bpfPrograms) Close() error { func (p *bpfPrograms) Close() error {
return _BpfClose( return _BpfClose(
p.XdpProgFunc, p.NbWgProxy,
) )
} }

View File

@@ -50,22 +50,22 @@ func (l *EBPF) Load(proxyPort, wgPort int) error {
_ = objs.Close() _ = objs.Close()
}() }()
err = objs.XdpPortMap.Put(mapKeyProxyPort, uint16(proxyPort)) err = objs.NbWgProxySettingsMap.Put(mapKeyProxyPort, uint16(proxyPort))
if err != nil { if err != nil {
return err return err
} }
err = objs.XdpPortMap.Put(mapKeyWgPort, uint16(wgPort)) err = objs.NbWgProxySettingsMap.Put(mapKeyWgPort, uint16(wgPort))
if err != nil { if err != nil {
return err return err
} }
defer func() { defer func() {
_ = objs.XdpPortMap.Close() _ = objs.NbWgProxySettingsMap.Close()
}() }()
l.link, err = link.AttachXDP(link.XDPOptions{ l.link, err = link.AttachXDP(link.XDPOptions{
Program: objs.XdpProgFunc, Program: objs.NbWgProxy,
Interface: ifce.Index, Interface: ifce.Index,
}) })
if err != nil { if err != nil {
@@ -75,7 +75,7 @@ func (l *EBPF) Load(proxyPort, wgPort int) error {
return err return err
} }
// Free free ebpf program // Free ebpf program
func (l *EBPF) Free() error { func (l *EBPF) Free() error {
if l.link != nil { if l.link != nil {
return l.link.Close() return l.link.Close()

View File

@@ -15,7 +15,7 @@
const __u32 map_key_proxy_port = 0; const __u32 map_key_proxy_port = 0;
const __u32 map_key_wg_port = 1; const __u32 map_key_wg_port = 1;
struct bpf_map_def SEC("maps") xdp_port_map = { struct bpf_map_def SEC("maps") nb_wg_proxy_settings_map = {
.type = BPF_MAP_TYPE_ARRAY, .type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(__u32), .key_size = sizeof(__u32),
.value_size = sizeof(__u16), .value_size = sizeof(__u16),
@@ -27,14 +27,14 @@ __u16 wg_port = 0;
bool read_port_settings() { bool read_port_settings() {
__u16 *value; __u16 *value;
value = bpf_map_lookup_elem(&xdp_port_map, &map_key_proxy_port); value = bpf_map_lookup_elem(&nb_wg_proxy_settings_map, &map_key_proxy_port);
if(!value) { if(!value) {
return false; return false;
} }
proxy_port = *value; proxy_port = *value;
value = bpf_map_lookup_elem(&xdp_port_map, &map_key_wg_port); value = bpf_map_lookup_elem(&nb_wg_proxy_settings_map, &map_key_wg_port);
if(!value) { if(!value) {
return false; return false;
} }
@@ -44,7 +44,7 @@ bool read_port_settings() {
} }
SEC("xdp") SEC("xdp")
int xdp_prog_func(struct xdp_md *ctx) { int nb_wg_proxy(struct xdp_md *ctx) {
if(proxy_port == 0 || wg_port == 0) { if(proxy_port == 0 || wg_port == 0) {
if(!read_port_settings()){ if(!read_port_settings()){
return XDP_PASS; return XDP_PASS;

View File

@@ -14,7 +14,7 @@ func (w *Factory) GetProxy() Proxy {
func (w *Factory) Free() error { func (w *Factory) Free() error {
if w.ebpfProxy != nil { if w.ebpfProxy != nil {
return w.ebpfProxy.CloseConn() return w.ebpfProxy.Free()
} }
return nil return nil
} }

View File

@@ -12,7 +12,7 @@ func NewFactory(wgPort int) *Factory {
ebpfProxy := NewWGEBPFProxy(wgPort) ebpfProxy := NewWGEBPFProxy(wgPort)
err := ebpfProxy.Listen() err := ebpfProxy.Listen()
if err != nil { if err != nil {
log.Errorf("failed to initialize ebpf proxy: %s", err) log.Warnf("failed to initialize ebpf proxy, fallback to user space proxy: %s", err)
return f return f
} }

View File

@@ -69,7 +69,7 @@ func (p *WGEBPFProxy) Listen() error {
p.conn, err = net.ListenUDP("udp", &addr) p.conn, err = net.ListenUDP("udp", &addr)
if err != nil { if err != nil {
cErr := p.Free() cErr := p.Free()
if err != nil { if cErr != nil {
log.Errorf("failed to close the wgproxy: %s", cErr) log.Errorf("failed to close the wgproxy: %s", cErr)
} }
return err return err
@@ -104,6 +104,7 @@ func (p *WGEBPFProxy) CloseConn() error {
// Free resources // Free resources
func (p *WGEBPFProxy) Free() error { func (p *WGEBPFProxy) Free() error {
log.Debugf("free up ebpf wg proxy")
var err1, err2, err3 error var err1, err2, err3 error
if p.conn != nil { if p.conn != nil {
err1 = p.conn.Close() err1 = p.conn.Close()
@@ -153,7 +154,9 @@ func (p *WGEBPFProxy) proxyToRemote() {
return return
} }
p.turnConnMutex.Lock()
conn, ok := p.turnConnStore[uint16(addr.Port)] conn, ok := p.turnConnStore[uint16(addr.Port)]
p.turnConnMutex.Unlock()
if !ok { if !ok {
log.Errorf("turn conn not found by port: %d", addr.Port) log.Errorf("turn conn not found by port: %d", addr.Port)
continue continue

View File

@@ -20,6 +20,7 @@ type WGUserSpaceProxy struct {
// NewWGUserSpaceProxy instantiate a user space WireGuard proxy // NewWGUserSpaceProxy instantiate a user space WireGuard proxy
func NewWGUserSpaceProxy(wgPort int) *WGUserSpaceProxy { func NewWGUserSpaceProxy(wgPort int) *WGUserSpaceProxy {
log.Debugf("instantiate new userspace proxy")
p := &WGUserSpaceProxy{ p := &WGUserSpaceProxy{
localWGListenPort: wgPort, localWGListenPort: wgPort,
} }

View File

@@ -130,16 +130,22 @@ func ParseNameServerURL(nsURL string) (NameServer, error) {
// Copy copies a nameserver group object // Copy copies a nameserver group object
func (g *NameServerGroup) Copy() *NameServerGroup { func (g *NameServerGroup) Copy() *NameServerGroup {
return &NameServerGroup{ nsGroup := &NameServerGroup{
ID: g.ID, ID: g.ID,
Name: g.Name, Name: g.Name,
Description: g.Description, Description: g.Description,
NameServers: g.NameServers, NameServers: make([]NameServer, len(g.NameServers)),
Groups: g.Groups, Groups: make([]string, len(g.Groups)),
Enabled: g.Enabled, Enabled: g.Enabled,
Primary: g.Primary, Primary: g.Primary,
Domains: g.Domains, Domains: make([]string, len(g.Domains)),
} }
copy(nsGroup.NameServers, g.NameServers)
copy(nsGroup.Groups, g.Groups)
copy(nsGroup.Domains, g.Domains)
return nsGroup
} }
// IsEqual compares one nameserver group with the other // IsEqual compares one nameserver group with the other

2
go.mod
View File

@@ -31,7 +31,7 @@ require (
fyne.io/fyne/v2 v2.1.4 fyne.io/fyne/v2 v2.1.4
github.com/c-robinson/iplib v1.0.3 github.com/c-robinson/iplib v1.0.3
github.com/cilium/ebpf v0.10.0 github.com/cilium/ebpf v0.10.0
github.com/coreos/go-iptables v0.6.0 github.com/coreos/go-iptables v0.7.0
github.com/creack/pty v1.1.18 github.com/creack/pty v1.1.18
github.com/eko/gocache/v3 v3.1.1 github.com/eko/gocache/v3 v3.1.1
github.com/getlantern/systray v1.2.1 github.com/getlantern/systray v1.2.1

4
go.sum
View File

@@ -131,8 +131,8 @@ github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcD
github.com/coocood/freecache v1.2.1 h1:/v1CqMq45NFH9mp/Pt142reundeBM0dVUD3osQBeu/U= github.com/coocood/freecache v1.2.1 h1:/v1CqMq45NFH9mp/Pt142reundeBM0dVUD3osQBeu/U=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=

View File

@@ -0,0 +1,736 @@
#!/bin/bash
set -e
handle_request_command_status() {
PARSED_RESPONSE=$1
FUNCTION_NAME=$2
RESPONSE=$3
if [[ $PARSED_RESPONSE -ne 0 ]]; then
echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
exit 1
fi
}
handle_zitadel_request_response() {
PARSED_RESPONSE=$1
FUNCTION_NAME=$2
RESPONSE=$3
if [[ $PARSED_RESPONSE == "null" ]]; then
echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
exit 1
fi
sleep 1
}
check_docker_compose() {
if command -v docker-compose &> /dev/null
then
echo "docker-compose"
return
fi
if docker compose --help &> /dev/null
then
echo "docker compose"
return
fi
echo "docker-compose is not installed or not in PATH. Please follow the steps from the official guide: https://docs.docker.com/engine/install/" > /dev/stderr
exit 1
}
check_jq() {
if ! command -v jq &> /dev/null
then
echo "jq is not installed or not in PATH, please install with your package manager. e.g. sudo apt install jq" > /dev/stderr
exit 1
fi
}
wait_crdb() {
set +e
while true; do
if $DOCKER_COMPOSE_COMMAND exec -T crdb curl -sf -o /dev/null 'http://localhost:8080/health?ready=1'; then
break
fi
echo -n " ."
sleep 5
done
echo " done"
set -e
}
init_crdb() {
echo -e "\nInitializing Zitadel's CockroachDB\n\n"
$DOCKER_COMPOSE_COMMAND up -d crdb
echo ""
# shellcheck disable=SC2028
echo -n "Waiting cockroachDB to become ready "
wait_crdb
$DOCKER_COMPOSE_COMMAND exec -T crdb /bin/bash -c "cp /cockroach/certs/* /zitadel-certs/ && cockroach cert create-client --overwrite --certs-dir /zitadel-certs/ --ca-key /zitadel-certs/ca.key zitadel_user && chown -R 1000:1000 /zitadel-certs/"
handle_request_command_status $? "init_crdb failed" ""
}
get_main_ip_address() {
if [[ "$OSTYPE" == "darwin"* ]]; then
interface=$(route -n get default | grep 'interface:' | awk '{print $2}')
ip_address=$(ifconfig "$interface" | grep 'inet ' | awk '{print $2}')
else
interface=$(ip route | grep default | awk '{print $5}' | head -n 1)
ip_address=$(ip addr show "$interface" | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1)
fi
echo "$ip_address"
}
wait_pat() {
PAT_PATH=$1
set +e
while true; do
if [[ -f "$PAT_PATH" ]]; then
break
fi
echo -n " ."
sleep 1
done
echo " done"
set -e
}
wait_api() {
INSTANCE_URL=$1
PAT=$2
set +e
while true; do
curl -s --fail -o /dev/null "$INSTANCE_URL/auth/v1/users/me" -H "Authorization: Bearer $PAT"
if [[ $? -eq 0 ]]; then
break
fi
echo -n " ."
sleep 1
done
echo " done"
set -e
}
create_new_project() {
INSTANCE_URL=$1
PAT=$2
PROJECT_NAME="NETBIRD"
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/projects" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{"name": "'"$PROJECT_NAME"'"}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.id')
handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_project" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
create_new_application() {
INSTANCE_URL=$1
PAT=$2
APPLICATION_NAME=$3
BASE_REDIRECT_URL1=$4
BASE_REDIRECT_URL2=$5
LOGOUT_URL=$6
ZITADEL_DEV_MODE=$7
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/projects/$PROJECT_ID/apps/oidc" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"name": "'"$APPLICATION_NAME"'",
"redirectUris": [
"'"$BASE_REDIRECT_URL1"'",
"'"$BASE_REDIRECT_URL2"'"
],
"postLogoutRedirectUris": [
"'"$LOGOUT_URL"'"
],
"RESPONSETypes": [
"OIDC_RESPONSE_TYPE_CODE"
],
"grantTypes": [
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
"OIDC_GRANT_TYPE_REFRESH_TOKEN"
],
"appType": "OIDC_APP_TYPE_USER_AGENT",
"authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
"version": "OIDC_VERSION_1_0",
"devMode": '"$ZITADEL_DEV_MODE"',
"accessTokenType": "OIDC_TOKEN_TYPE_JWT",
"accessTokenRoleAssertion": true,
"skipNativeAppSuccessPage": true
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.clientId')
handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_application" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
create_service_user() {
INSTANCE_URL=$1
PAT=$2
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/users/machine" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"userName": "netbird-service-account",
"name": "Netbird Service Account",
"description": "Netbird Service Account for IDP management",
"accessTokenType": "ACCESS_TOKEN_TYPE_JWT"
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
handle_zitadel_request_response "$PARSED_RESPONSE" "create_service_user" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
create_service_user_secret() {
INSTANCE_URL=$1
PAT=$2
USER_ID=$3
RESPONSE=$(
curl -sS -X PUT "$INSTANCE_URL/management/v1/users/$USER_ID/secret" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{}'
)
SERVICE_USER_CLIENT_ID=$(echo "$RESPONSE" | jq -r '.clientId')
handle_zitadel_request_response "$SERVICE_USER_CLIENT_ID" "create_service_user_secret_id" "$RESPONSE"
SERVICE_USER_CLIENT_SECRET=$(echo "$RESPONSE" | jq -r '.clientSecret')
handle_zitadel_request_response "$SERVICE_USER_CLIENT_SECRET" "create_service_user_secret" "$RESPONSE"
}
add_organization_user_manager() {
INSTANCE_URL=$1
PAT=$2
USER_ID=$3
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/orgs/me/members" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"userId": "'"$USER_ID"'",
"roles": [
"ORG_USER_MANAGER"
]
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
handle_zitadel_request_response "$PARSED_RESPONSE" "add_organization_user_manager" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
create_admin_user() {
INSTANCE_URL=$1
PAT=$2
USERNAME=$3
PASSWORD=$4
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/users/human/_import" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"userName": "'"$USERNAME"'",
"profile": {
"firstName": "Zitadel",
"lastName": "Admin"
},
"email": {
"email": "'"$USERNAME"'",
"isEmailVerified": true
},
"password": "'"$PASSWORD"'",
"passwordChangeRequired": true
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
handle_zitadel_request_response "$PARSED_RESPONSE" "create_admin_user" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
add_instance_admin() {
INSTANCE_URL=$1
PAT=$2
USER_ID=$3
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/admin/v1/members" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"userId": "'"$USER_ID"'",
"roles": [
"IAM_OWNER"
]
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
handle_zitadel_request_response "$PARSED_RESPONSE" "add_instance_admin" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
delete_auto_service_user() {
INSTANCE_URL=$1
PAT=$2
RESPONSE=$(
curl -sS -X GET "$INSTANCE_URL/auth/v1/users/me" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
)
USER_ID=$(echo "$RESPONSE" | jq -r '.user.id')
handle_zitadel_request_response "$USER_ID" "delete_auto_service_user_get_user" "$RESPONSE"
RESPONSE=$(
curl -sS -X DELETE "$INSTANCE_URL/admin/v1/members/$USER_ID" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_instance_permissions" "$RESPONSE"
RESPONSE=$(
curl -sS -X DELETE "$INSTANCE_URL/management/v1/orgs/me/members/$USER_ID" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_org_permissions" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
init_zitadel() {
echo -e "\nInitializing Zitadel with NetBird's applications\n"
INSTANCE_URL="$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT"
TOKEN_PATH=./machinekey/zitadel-admin-sa.token
echo -n "Waiting for Zitadel's PAT to be created "
wait_pat "$TOKEN_PATH"
echo "Reading Zitadel PAT"
PAT=$(cat $TOKEN_PATH)
if [ "$PAT" = "null" ]; then
echo "Failed requesting getting Zitadel PAT"
exit 1
fi
echo -n "Waiting for Zitadel to become ready "
wait_api "$INSTANCE_URL" "$PAT"
# create the zitadel project
echo "Creating new zitadel project"
PROJECT_ID=$(create_new_project "$INSTANCE_URL" "$PAT")
ZITADEL_DEV_MODE=false
BASE_REDIRECT_URL=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN
if [[ $NETBIRD_HTTP_PROTOCOL == "http" ]]; then
ZITADEL_DEV_MODE=true
fi
# create zitadel spa applications
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")
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")
MACHINE_USER_ID=$(create_service_user "$INSTANCE_URL" "$PAT")
SERVICE_USER_CLIENT_ID="null"
SERVICE_USER_CLIENT_SECRET="null"
create_service_user_secret "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID"
DATE=$(add_organization_user_manager "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID")
ZITADEL_ADMIN_USERNAME="admin@$NETBIRD_DOMAIN"
ZITADEL_ADMIN_PASSWORD="$(openssl rand -base64 32 | sed 's/=//g')@"
HUMAN_USER_ID=$(create_admin_user "$INSTANCE_URL" "$PAT" "$ZITADEL_ADMIN_USERNAME" "$ZITADEL_ADMIN_PASSWORD")
DATE="null"
DATE=$(add_instance_admin "$INSTANCE_URL" "$PAT" "$HUMAN_USER_ID")
DATE="null"
DATE=$(delete_auto_service_user "$INSTANCE_URL" "$PAT")
if [ "$DATE" = "null" ]; then
echo "Failed deleting auto service user"
echo "Please remove it manually"
fi
export NETBIRD_AUTH_CLIENT_ID=$DASHBOARD_APPLICATION_CLIENT_ID
export NETBIRD_AUTH_CLIENT_ID_CLI=$CLI_APPLICATION_CLIENT_ID
export NETBIRD_IDP_MGMT_CLIENT_ID=$SERVICE_USER_CLIENT_ID
export NETBIRD_IDP_MGMT_CLIENT_SECRET=$SERVICE_USER_CLIENT_SECRET
export ZITADEL_ADMIN_USERNAME
export ZITADEL_ADMIN_PASSWORD
}
check_nb_domain() {
DOMAIN=$1
if [ "$DOMAIN-x" == "-x" ]; then
echo "The NETBIRD_DOMAIN variable cannot be empty." > /dev/stderr
return 1
fi
if [ "$DOMAIN" == "netbird.example.com" ]; then
echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr
retrun 1
fi
return 0
}
read_nb_domain() {
READ_NETBIRD_DOMAIN=""
echo -n "Enter the domain you want to use for NetBird (e.g. netbird.my-domain.com): " > /dev/stderr
read -r READ_NETBIRD_DOMAIN < /dev/tty
if ! check_nb_domain "$READ_NETBIRD_DOMAIN"; then
read_nb_domain
fi
echo "$READ_NETBIRD_DOMAIN"
}
initEnvironment() {
CADDY_SECURE_DOMAIN=""
ZITADEL_EXTERNALSECURE="false"
ZITADEL_TLS_MODE="disabled"
ZITADEL_MASTERKEY="$(openssl rand -base64 32 | head -c 32)"
NETBIRD_PORT=80
NETBIRD_HTTP_PROTOCOL="http"
TURN_USER="self"
TURN_PASSWORD=$(openssl rand -base64 32 | sed 's/=//g')
TURN_MIN_PORT=49152
TURN_MAX_PORT=65535
if ! check_nb_domain "$NETBIRD_DOMAIN"; then
NETBIRD_DOMAIN=$(read_nb_domain)
fi
if [ "$NETBIRD_DOMAIN" == "use-ip" ]; then
NETBIRD_DOMAIN=$(get_main_ip_address)
else
ZITADEL_EXTERNALSECURE="true"
ZITADEL_TLS_MODE="external"
NETBIRD_PORT=443
CADDY_SECURE_DOMAIN=", $NETBIRD_DOMAIN:$NETBIRD_PORT"
NETBIRD_HTTP_PROTOCOL="https"
fi
if [[ "$OSTYPE" == "darwin"* ]]; then
ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -v+30M "+%Y-%m-%dT%H:%M:%SZ")
else
ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -d "+30 minutes" "+%Y-%m-%dT%H:%M:%SZ")
fi
check_jq
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
if [ -f zitadel.env ]; then
echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
echo "You can use the following commands:"
echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
echo " rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json"
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
exit 1
fi
echo Rendering initial files...
renderDockerCompose > docker-compose.yml
renderCaddyfile > Caddyfile
renderZitadelEnv > zitadel.env
echo "" > dashboard.env
echo "" > turnserver.conf
echo "" > management.json
mkdir -p machinekey
chmod 777 machinekey
init_crdb
echo -e "\nStarting Zidatel IDP for user management\n\n"
$DOCKER_COMPOSE_COMMAND up -d caddy zitadel
init_zitadel
echo -e "\nRendering NetBird files...\n"
renderTurnServerConf > turnserver.conf
renderManagementJson > management.json
renderDashboardEnv > dashboard.env
echo -e "\nStarting NetBird services\n"
$DOCKER_COMPOSE_COMMAND up -d
echo -e "\nDone!\n"
echo "You can access the NetBird dashboard at $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT"
echo "Login with the following credentials:"
echo "Username: $ZITADEL_ADMIN_USERNAME" | tee .env
echo "Password: $ZITADEL_ADMIN_PASSWORD" | tee -a .env
}
renderCaddyfile() {
cat <<EOF
{
debug
servers :80,:443 {
protocols h1 h2c
}
}
:80${CADDY_SECURE_DOMAIN} {
# Signal
reverse_proxy /signalexchange.SignalExchange/* h2c://signal:10000
# Management
reverse_proxy /api/* management:80
reverse_proxy /management.ManagementService/* h2c://management:80
# Zitadel
reverse_proxy /zitadel.admin.v1.AdminService/* h2c://zitadel:8080
reverse_proxy /admin/v1/* h2c://zitadel:8080
reverse_proxy /zitadel.auth.v1.AuthService/* h2c://zitadel:8080
reverse_proxy /auth/v1/* h2c://zitadel:8080
reverse_proxy /zitadel.management.v1.ManagementService/* h2c://zitadel:8080
reverse_proxy /management/v1/* h2c://zitadel:8080
reverse_proxy /zitadel.system.v1.SystemService/* h2c://zitadel:8080
reverse_proxy /system/v1/* h2c://zitadel:8080
reverse_proxy /assets/v1/* h2c://zitadel:8080
reverse_proxy /ui/* h2c://zitadel:8080
reverse_proxy /oidc/v1/* h2c://zitadel:8080
reverse_proxy /saml/v2/* h2c://zitadel:8080
reverse_proxy /oauth/v2/* h2c://zitadel:8080
reverse_proxy /.well-known/openid-configuration h2c://zitadel:8080
reverse_proxy /openapi/* h2c://zitadel:8080
reverse_proxy /debug/* h2c://zitadel:8080
# Dashboard
reverse_proxy /* dashboard:80
}
EOF
}
renderTurnServerConf() {
cat <<EOF
listening-port=3478
tls-listening-port=5349
min-port=$TURN_MIN_PORT
max-port=$TURN_MAX_PORT
fingerprint
lt-cred-mech
user=$TURN_USER:$TURN_PASSWORD
realm=wiretrustee.com
cert=/etc/coturn/certs/cert.pem
pkey=/etc/coturn/private/privkey.pem
log-file=stdout
no-software-attribute
pidfile="/var/tmp/turnserver.pid"
no-cli
EOF
}
renderManagementJson() {
cat <<EOF
{
"Stuns": [
{
"Proto": "udp",
"URI": "stun:$NETBIRD_DOMAIN:3478"
}
],
"TURNConfig": {
"Turns": [
{
"Proto": "udp",
"URI": "turn:$NETBIRD_DOMAIN:3478",
"Username": "$TURN_USER",
"Password": "$TURN_PASSWORD"
}
],
"TimeBasedCredentials": false
},
"Signal": {
"Proto": "$NETBIRD_HTTP_PROTOCOL",
"URI": "$NETBIRD_DOMAIN:$NETBIRD_PORT"
},
"HttpConfig": {
"AuthIssuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN",
"AuthAudience": "$NETBIRD_AUTH_CLIENT_ID",
"OIDCConfigEndpoint":"$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/.well-known/openid-configuration"
},
"IdpManagerConfig": {
"ManagerType": "zitadel",
"ClientConfig": {
"Issuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
"TokenEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT/oauth/v2/token",
"ClientID": "$NETBIRD_IDP_MGMT_CLIENT_ID",
"ClientSecret": "$NETBIRD_IDP_MGMT_CLIENT_SECRET",
"GrantType": "client_credentials"
},
"ExtraConfig": {
"ManagementEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT/management/v1"
}
},
"PKCEAuthorizationFlow": {
"ProviderConfig": {
"Audience": "$NETBIRD_AUTH_CLIENT_ID_CLI",
"ClientID": "$NETBIRD_AUTH_CLIENT_ID_CLI",
"Scope": "openid profile email offline_access",
"RedirectURLs": ["http://localhost:53000/","http://localhost:54000/"]
}
}
}
EOF
}
renderDashboardEnv() {
cat <<EOF
# Endpoints
NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT
NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT
# OIDC
AUTH_AUDIENCE=$NETBIRD_AUTH_CLIENT_ID
AUTH_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
AUTH_AUTHORITY=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT
USE_AUTH0=false
AUTH_SUPPORTED_SCOPES="openid profile email offline_access"
AUTH_REDIRECT_URI=/nb-auth
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
# SSL
NGINX_SSL_PORT=443
# Letsencrypt
LETSENCRYPT_DOMAIN=none
EOF
}
renderZitadelEnv() {
cat <<EOF
ZITADEL_LOG_LEVEL=debug
ZITADEL_MASTERKEY=$ZITADEL_MASTERKEY
ZITADEL_DATABASE_COCKROACH_HOST=crdb
ZITADEL_DATABASE_COCKROACH_USER_USERNAME=zitadel_user
ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE=verify-full
ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT="/crdb-certs/ca.crt"
ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT="/crdb-certs/client.zitadel_user.crt"
ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY="/crdb-certs/client.zitadel_user.key"
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_MODE=verify-full
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_ROOTCERT="/crdb-certs/ca.crt"
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_CERT="/crdb-certs/client.root.crt"
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_KEY="/crdb-certs/client.root.key"
ZITADEL_EXTERNALSECURE=$ZITADEL_EXTERNALSECURE
ZITADEL_TLS_ENABLED="false"
ZITADEL_EXTERNALPORT=$NETBIRD_PORT
ZITADEL_EXTERNALDOMAIN=$NETBIRD_DOMAIN
ZITADEL_FIRSTINSTANCE_PATPATH=/machinekey/zitadel-admin-sa.token
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_SCOPES=openid
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE=$ZIDATE_TOKEN_EXPIRATION_DATE
EOF
}
renderDockerCompose() {
cat <<EOF
version: "3.4"
services:
# Caddy reverse proxy
caddy:
image: caddy
restart: unless-stopped
networks: [ netbird ]
ports:
- '443:443'
- '80:80'
- '8080:8080'
volumes:
- netbird_caddy_data:/data
- ./Caddyfile:/etc/caddy/Caddyfile
#UI dashboard
dashboard:
image: wiretrustee/dashboard:latest
restart: unless-stopped
networks: [netbird]
env_file:
- ./dashboard.env
# Signal
signal:
image: netbirdio/signal:latest
restart: unless-stopped
networks: [netbird]
# Management
management:
image: netbirdio/management:latest
restart: unless-stopped
networks: [netbird]
volumes:
- netbird_management:/var/lib/netbird
- ./management.json:/etc/netbird/management.json
command: [
"--port", "80",
"--log-file", "console",
"--log-level", "info",
"--disable-anonymous-metrics=false",
"--single-account-mode-domain=netbird.selfhosted",
"--dns-domain=netbird.selfhosted",
"--idp-sign-key-refresh-enabled",
]
# Coturn, AKA relay server
coturn:
image: coturn/coturn
restart: unless-stopped
domainname: netbird.relay.selfhosted
volumes:
- ./turnserver.conf:/etc/turnserver.conf:ro
network_mode: host
command:
- -c /etc/turnserver.conf
# Zitadel - identity provider
zitadel:
restart: 'always'
networks: [netbird]
image: 'ghcr.io/zitadel/zitadel:v2.31.3'
command: 'start-from-init --masterkeyFromEnv --tlsMode $ZITADEL_TLS_MODE'
env_file:
- ./zitadel.env
depends_on:
crdb:
condition: 'service_healthy'
volumes:
- ./machinekey:/machinekey
- netbird_zitadel_certs:/crdb-certs:ro
# CockroachDB for zitadel
crdb:
restart: 'always'
networks: [netbird]
image: 'cockroachdb/cockroach:v22.2.2'
command: 'start-single-node --advertise-addr crdb'
volumes:
- netbird_crdb_data:/cockroach/cockroach-data
- netbird_crdb_certs:/cockroach/certs
- netbird_zitadel_certs:/zitadel-certs
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:8080/health?ready=1" ]
interval: '10s'
timeout: '30s'
retries: 5
start_period: '20s'
volumes:
netbird_management:
netbird_caddy_data:
netbird_crdb_data:
netbird_crdb_certs:
netbird_zitadel_certs:
networks:
netbird:
EOF
}
initEnvironment

View File

@@ -1,122 +0,0 @@
#!/bin/bash
set -e
request_jwt_token() {
INSTANCE_URL=$1
BODY="grant_type=client_credentials&scope=urn:zitadel:iam:org:project:id:zitadel:aud&client_id=$ZITADEL_CLIENT_ID&client_secret=$ZITADEL_CLIENT_SECRET"
RESPONSE=$(
curl -X POST "$INSTANCE_URL/oauth/v2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "$BODY"
)
echo "$RESPONSE" | jq -r '.access_token'
}
create_new_project() {
INSTANCE_URL=$1
ACCESS_TOKEN=$2
PROJECT_NAME="NETBIRD"
RESPONSE=$(
curl -X POST "$INSTANCE_URL/management/v1/projects" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "'"$PROJECT_NAME"'"}'
)
echo "$RESPONSE" | jq -r '.id'
}
create_new_application() {
INSTANCE_URL=$1
ACCESS_TOKEN=$2
APPLICATION_NAME="netbird"
RESPONSE=$(
curl -X POST "$INSTANCE_URL/management/v1/projects/$PROJECT_ID/apps/oidc" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "'"$APPLICATION_NAME"'",
"redirectUris": [
"'"$BASE_REDIRECT_URL"'/auth"
],
"RESPONSETypes": [
"OIDC_RESPONSE_TYPE_CODE"
],
"grantTypes": [
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
"OIDC_GRANT_TYPE_REFRESH_TOKEN"
],
"appType": "OIDC_APP_TYPE_USER_AGENT",
"authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
"postLogoutRedirectUris": [
"'"$BASE_REDIRECT_URL"'/silent-auth"
],
"version": "OIDC_VERSION_1_0",
"devMode": '"$ZITADEL_DEV_MODE"',
"accessTokenType": "OIDC_TOKEN_TYPE_JWT",
"accessTokenRoleAssertion": true,
"skipNativeAppSuccessPage": true
}'
)
echo "$RESPONSE" | jq -r '.clientId'
}
configure_zitadel_instance() {
# extract zitadel instance url
INSTANCE_URL=$(echo "$NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT" | sed 's/\/\.well-known\/openid-configuration//')
DOC_URL="https://netbird.io/docs/integrations/identity-providers/self-hosted/using-netbird-with-zitadel#step-4-create-a-service-user"
echo ""
printf "configuring zitadel instance: $INSTANCE_URL \n \
before proceeding, please create a new service account for authorization by following the instructions (step 4 and 5
) in the documentation at %s\n" "$DOC_URL"
echo "Please ensure that the new service account has 'Org Owner' permission in order for this to work."
echo ""
read -n 1 -s -r -p "press any key to continue..."
echo ""
# prompt the user to enter service account clientID
echo ""
read -r -p "enter service account ClientId: " ZITADEL_CLIENT_ID
echo ""
# Prompt the user to enter service account clientSecret
read -r -p "enter service account ClientSecret: " ZITADEL_CLIENT_SECRET
echo ""
# get an access token from zitadel
echo "retrieving access token from zitadel"
ACCESS_TOKEN=$(request_jwt_token "$INSTANCE_URL")
if [ "$ACCESS_TOKEN" = "null" ]; then
echo "failed requesting access token"
exit 1
fi
# create the zitadel project
echo "creating new zitadel project"
PROJECT_ID=$(create_new_project "$INSTANCE_URL" "$ACCESS_TOKEN")
if [ "$PROJECT_ID" = "null" ]; then
echo "failed creating new zitadel project"
exit 1
fi
ZITADEL_DEV_MODE=false
if [[ $NETBIRD_DOMAIN == *"localhost"* ]]; then
BASE_REDIRECT_URL="http://$NETBIRD_DOMAIN"
ZITADEL_DEV_MODE=true
else
BASE_REDIRECT_URL="https://$NETBIRD_DOMAIN"
fi
# create zitadel spa application
echo "creating new zitadel spa application"
APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$ACCESS_TOKEN")
if [ "$APPLICATION_CLIENT_ID" = "null" ]; then
echo "failed creating new zitadel spa application"
exit 1
fi
}

View File

@@ -371,24 +371,24 @@ func handlerFunc(gRPCHandler *grpc.Server, httpHandler http.Handler) http.Handle
} }
func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) { func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
config := &server.Config{} loadedConfig := &server.Config{}
_, err := util.ReadJson(mgmtConfigPath, config) _, err := util.ReadJson(mgmtConfigPath, loadedConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if mgmtLetsencryptDomain != "" { if mgmtLetsencryptDomain != "" {
config.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain loadedConfig.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain
} }
if mgmtDataDir != "" { if mgmtDataDir != "" {
config.Datadir = mgmtDataDir loadedConfig.Datadir = mgmtDataDir
} }
if certKey != "" && certFile != "" { if certKey != "" && certFile != "" {
config.HttpConfig.CertFile = certFile loadedConfig.HttpConfig.CertFile = certFile
config.HttpConfig.CertKey = certKey loadedConfig.HttpConfig.CertKey = certKey
} }
oidcEndpoint := config.HttpConfig.OIDCConfigEndpoint oidcEndpoint := loadedConfig.HttpConfig.OIDCConfigEndpoint
if oidcEndpoint != "" { if oidcEndpoint != "" {
// if OIDCConfigEndpoint is specified, we can load DeviceAuthEndpoint and TokenEndpoint automatically // if OIDCConfigEndpoint is specified, we can load DeviceAuthEndpoint and TokenEndpoint automatically
log.Infof("loading OIDC configuration from the provided IDP configuration endpoint %s", oidcEndpoint) log.Infof("loading OIDC configuration from the provided IDP configuration endpoint %s", oidcEndpoint)
@@ -399,45 +399,45 @@ func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
log.Infof("loaded OIDC configuration from the provided IDP configuration endpoint: %s", oidcEndpoint) log.Infof("loaded OIDC configuration from the provided IDP configuration endpoint: %s", oidcEndpoint)
log.Infof("overriding HttpConfig.AuthIssuer with a new value %s, previously configured value: %s", log.Infof("overriding HttpConfig.AuthIssuer with a new value %s, previously configured value: %s",
oidcConfig.Issuer, config.HttpConfig.AuthIssuer) oidcConfig.Issuer, loadedConfig.HttpConfig.AuthIssuer)
config.HttpConfig.AuthIssuer = oidcConfig.Issuer loadedConfig.HttpConfig.AuthIssuer = oidcConfig.Issuer
log.Infof("overriding HttpConfig.AuthKeysLocation (JWT certs) with a new value %s, previously configured value: %s", log.Infof("overriding HttpConfig.AuthKeysLocation (JWT certs) with a new value %s, previously configured value: %s",
oidcConfig.JwksURI, config.HttpConfig.AuthKeysLocation) oidcConfig.JwksURI, loadedConfig.HttpConfig.AuthKeysLocation)
config.HttpConfig.AuthKeysLocation = oidcConfig.JwksURI loadedConfig.HttpConfig.AuthKeysLocation = oidcConfig.JwksURI
if !(config.DeviceAuthorizationFlow == nil || strings.ToLower(config.DeviceAuthorizationFlow.Provider) == string(server.NONE)) { if !(loadedConfig.DeviceAuthorizationFlow == nil || strings.ToLower(loadedConfig.DeviceAuthorizationFlow.Provider) == string(server.NONE)) {
log.Infof("overriding DeviceAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s", log.Infof("overriding DeviceAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
oidcConfig.TokenEndpoint, config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint) oidcConfig.TokenEndpoint, loadedConfig.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint)
config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint loadedConfig.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
log.Infof("overriding DeviceAuthorizationFlow.DeviceAuthEndpoint with a new value: %s, previously configured value: %s", log.Infof("overriding DeviceAuthorizationFlow.DeviceAuthEndpoint with a new value: %s, previously configured value: %s",
oidcConfig.DeviceAuthEndpoint, config.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint) oidcConfig.DeviceAuthEndpoint, loadedConfig.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint)
config.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint = oidcConfig.DeviceAuthEndpoint loadedConfig.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint = oidcConfig.DeviceAuthEndpoint
u, err := url.Parse(oidcEndpoint) u, err := url.Parse(oidcEndpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Infof("overriding DeviceAuthorizationFlow.ProviderConfig.Domain with a new value: %s, previously configured value: %s", log.Infof("overriding DeviceAuthorizationFlow.ProviderConfig.Domain with a new value: %s, previously configured value: %s",
u.Host, config.DeviceAuthorizationFlow.ProviderConfig.Domain) u.Host, loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Domain)
config.DeviceAuthorizationFlow.ProviderConfig.Domain = u.Host loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Domain = u.Host
if config.DeviceAuthorizationFlow.ProviderConfig.Scope == "" { if loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Scope == "" {
config.DeviceAuthorizationFlow.ProviderConfig.Scope = server.DefaultDeviceAuthFlowScope loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Scope = server.DefaultDeviceAuthFlowScope
} }
} }
if config.PKCEAuthorizationFlow != nil { if loadedConfig.PKCEAuthorizationFlow != nil {
log.Infof("overriding PKCEAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s", log.Infof("overriding PKCEAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
oidcConfig.TokenEndpoint, config.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint) oidcConfig.TokenEndpoint, loadedConfig.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint)
config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint loadedConfig.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
log.Infof("overriding PKCEAuthorizationFlow.AuthorizationEndpoint with a new value: %s, previously configured value: %s", log.Infof("overriding PKCEAuthorizationFlow.AuthorizationEndpoint with a new value: %s, previously configured value: %s",
oidcConfig.AuthorizationEndpoint, config.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint) oidcConfig.AuthorizationEndpoint, loadedConfig.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint)
config.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint = oidcConfig.AuthorizationEndpoint loadedConfig.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint = oidcConfig.AuthorizationEndpoint
} }
} }
return config, err return loadedConfig, err
} }
// OIDCConfigResponse used for parsing OIDC config response // OIDCConfigResponse used for parsing OIDC config response

View File

@@ -139,10 +139,14 @@ type DefaultAccountManager struct {
type Settings struct { type Settings struct {
// PeerLoginExpirationEnabled globally enables or disables peer login expiration // PeerLoginExpirationEnabled globally enables or disables peer login expiration
PeerLoginExpirationEnabled bool PeerLoginExpirationEnabled bool
// PeerLoginExpiration is a setting that indicates when peer login expires. // PeerLoginExpiration is a setting that indicates when peer login expires.
// Applies to all peers that have Peer.LoginExpirationEnabled set to true. // Applies to all peers that have Peer.LoginExpirationEnabled set to true.
PeerLoginExpiration time.Duration PeerLoginExpiration time.Duration
// GroupsPropagationEnabled allows to propagate auto groups from the user to the peer
GroupsPropagationEnabled bool
// JWTGroupsEnabled allows extract groups from JWT claim, which name defined in the JWTGroupsClaimName // JWTGroupsEnabled allows extract groups from JWT claim, which name defined in the JWTGroupsClaimName
// and add it to account groups. // and add it to account groups.
JWTGroupsEnabled bool JWTGroupsEnabled bool
@@ -158,6 +162,7 @@ func (s *Settings) Copy() *Settings {
PeerLoginExpiration: s.PeerLoginExpiration, PeerLoginExpiration: s.PeerLoginExpiration,
JWTGroupsEnabled: s.JWTGroupsEnabled, JWTGroupsEnabled: s.JWTGroupsEnabled,
JWTGroupsClaimName: s.JWTGroupsClaimName, JWTGroupsClaimName: s.JWTGroupsClaimName,
GroupsPropagationEnabled: s.GroupsPropagationEnabled,
} }
} }
@@ -192,6 +197,7 @@ type UserInfo struct {
Status string `json:"-"` Status string `json:"-"`
IsServiceUser bool `json:"is_service_user"` IsServiceUser bool `json:"is_service_user"`
IsBlocked bool `json:"is_blocked"` IsBlocked bool `json:"is_blocked"`
LastLogin time.Time `json:"last_login"`
} }
// getRoutesToSync returns the enabled routes for the peer ID and the routes // getRoutesToSync returns the enabled routes for the peer ID and the routes
@@ -624,26 +630,110 @@ func (a *Account) GetPeer(peerID string) *Peer {
return a.Peers[peerID] return a.Peers[peerID]
} }
// AddJWTGroups to existed groups if they does not exists // SetJWTGroups to account and to user autoassigned groups
func (a *Account) AddJWTGroups(groups []string) (int, error) { func (a *Account) SetJWTGroups(userID string, groupsNames []string) bool {
existedGroups := make(map[string]*Group) user, ok := a.Users[userID]
for _, g := range a.Groups { if !ok {
existedGroups[g.Name] = g return false
} }
var count int existedGroupsByName := make(map[string]*Group)
for _, name := range groups { for _, group := range a.Groups {
if _, ok := existedGroups[name]; !ok { existedGroupsByName[group.Name] = group
id := xid.New().String() }
a.Groups[id] = &Group{
ID: id, // remove JWT groups from the autogroups, to sync them again
removed := 0
jwtAutoGroups := make(map[string]struct{})
for i, id := range user.AutoGroups {
if group, ok := a.Groups[id]; ok && group.Issued == GroupIssuedJWT {
jwtAutoGroups[group.Name] = struct{}{}
user.AutoGroups = append(user.AutoGroups[:i-removed], user.AutoGroups[i-removed+1:]...)
removed++
}
}
// create JWT groups if they doesn't exist
// and all of them to the autogroups
var modified bool
for _, name := range groupsNames {
group, ok := existedGroupsByName[name]
if !ok {
group = &Group{
ID: xid.New().String(),
Name: name, Name: name,
Issued: GroupIssuedJWT, Issued: GroupIssuedJWT,
} }
count++ a.Groups[group.ID] = group
}
// only JWT groups will be synced
if group.Issued == GroupIssuedJWT {
user.AutoGroups = append(user.AutoGroups, group.ID)
if _, ok := jwtAutoGroups[name]; !ok {
modified = true
}
delete(jwtAutoGroups, name)
} }
} }
return count, nil
// if not empty it means we removed some groups
if len(jwtAutoGroups) > 0 {
modified = true
}
return modified
}
// UserGroupsAddToPeers adds groups to all peers of user
func (a *Account) UserGroupsAddToPeers(userID string, groups ...string) {
userPeers := make(map[string]struct{})
for pid, peer := range a.Peers {
if peer.UserID == userID {
userPeers[pid] = struct{}{}
}
}
for _, gid := range groups {
group, ok := a.Groups[gid]
if !ok {
continue
}
groupPeers := make(map[string]struct{})
for _, pid := range group.Peers {
groupPeers[pid] = struct{}{}
}
for pid := range userPeers {
groupPeers[pid] = struct{}{}
}
group.Peers = group.Peers[:0]
for pid := range groupPeers {
group.Peers = append(group.Peers, pid)
}
}
}
// UserGroupsRemoveFromPeers removes groups from all peers of user
func (a *Account) UserGroupsRemoveFromPeers(userID string, groups ...string) {
for _, gid := range groups {
group, ok := a.Groups[gid]
if !ok {
continue
}
update := make([]string, 0, len(group.Peers))
for _, pid := range group.Peers {
peer, ok := a.Peers[pid]
if !ok {
continue
}
if peer.UserID != userID {
update = append(update, pid)
}
}
group.Peers = update
}
} }
// BuildManager creates a new DefaultAccountManager with a provided Store // BuildManager creates a new DefaultAccountManager with a provided Store
@@ -797,6 +887,7 @@ func (am *DefaultAccountManager) peerLoginExpirationJob(accountID string) func()
log.Errorf("failed saving peer status while expiring peer %s", peer.ID) log.Errorf("failed saving peer status while expiring peer %s", peer.ID)
return account.GetNextPeerExpiration() return account.GetNextPeerExpiration()
} }
am.storeEvent(peer.UserID, peer.ID, account.Id, activity.PeerLoginExpired, peer.EventMeta(am.GetDNSDomain()))
} }
log.Debugf("discovered %d peers to expire for account %s", len(peerIDs), account.Id) log.Debugf("discovered %d peers to expire for account %s", len(peerIDs), account.Id)
@@ -1282,21 +1373,58 @@ func (am *DefaultAccountManager) GetAccountFromToken(claims jwtclaims.Authorizat
} }
if claim, ok := claims.Raw[account.Settings.JWTGroupsClaimName]; ok { if claim, ok := claims.Raw[account.Settings.JWTGroupsClaimName]; ok {
if slice, ok := claim.([]interface{}); ok { if slice, ok := claim.([]interface{}); ok {
var groups []string var groupsNames []string
for _, item := range slice { for _, item := range slice {
if g, ok := item.(string); ok { if g, ok := item.(string); ok {
groups = append(groups, g) groupsNames = append(groupsNames, g)
} else { } else {
log.Errorf("JWT claim %q is not a string: %v", account.Settings.JWTGroupsClaimName, item) log.Errorf("JWT claim %q is not a string: %v", account.Settings.JWTGroupsClaimName, item)
} }
} }
n, err := account.AddJWTGroups(groups)
if err != nil { oldGroups := make([]string, len(user.AutoGroups))
log.Errorf("failed to add JWT groups: %v", err) copy(oldGroups, user.AutoGroups)
} // if groups were added or modified, save the account
if n > 0 { if account.SetJWTGroups(claims.UserId, groupsNames) {
if account.Settings.GroupsPropagationEnabled {
if user, err := account.FindUser(claims.UserId); err == nil {
addNewGroups := difference(user.AutoGroups, oldGroups)
removeOldGroups := difference(oldGroups, user.AutoGroups)
account.UserGroupsAddToPeers(claims.UserId, addNewGroups...)
account.UserGroupsRemoveFromPeers(claims.UserId, removeOldGroups...)
account.Network.IncSerial()
if err := am.Store.SaveAccount(account); err != nil { if err := am.Store.SaveAccount(account); err != nil {
log.Errorf("failed to save account: %v", err) log.Errorf("failed to save account: %v", err)
} else {
if err := am.updateAccountPeers(account); err != nil {
log.Errorf("failed updating account peers while updating user %s", account.Id)
}
for _, g := range addNewGroups {
if group := account.GetGroup(g); group != nil {
am.storeEvent(user.Id, user.Id, account.Id, activity.GroupAddedToUser,
map[string]any{
"group": group.Name,
"group_id": group.ID,
"is_service_user": user.IsServiceUser,
"user_name": user.ServiceUserName})
}
}
for _, g := range removeOldGroups {
if group := account.GetGroup(g); group != nil {
am.storeEvent(user.Id, user.Id, account.Id, activity.GroupRemovedFromUser,
map[string]any{
"group": group.Name,
"group_id": group.ID,
"is_service_user": user.IsServiceUser,
"user_name": user.ServiceUserName})
}
}
}
}
} else {
if err := am.Store.SaveAccount(account); err != nil {
log.Errorf("failed to save account: %v", err)
}
} }
} }
} else { } else {
@@ -1343,7 +1471,7 @@ func (am *DefaultAccountManager) getAccountWithAuthorizationClaims(claims jwtcla
if _, ok := accountFromID.Users[claims.UserId]; !ok { if _, ok := accountFromID.Users[claims.UserId]; !ok {
return nil, fmt.Errorf("user %s is not part of the account id %s", claims.UserId, claims.AccountId) return nil, fmt.Errorf("user %s is not part of the account id %s", claims.UserId, claims.AccountId)
} }
if accountFromID.DomainCategory == PrivateCategory || claims.DomainCategory != PrivateCategory { if accountFromID.DomainCategory == PrivateCategory || claims.DomainCategory != PrivateCategory || accountFromID.Domain != claims.Domain {
return accountFromID, nil return accountFromID, nil
} }
} }

View File

@@ -3,6 +3,7 @@ package server
import ( import (
"crypto/sha256" "crypto/sha256"
b64 "encoding/base64" b64 "encoding/base64"
"encoding/json"
"fmt" "fmt"
"net" "net"
"reflect" "reflect"
@@ -216,7 +217,6 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
assert.Len(t, networkMap.Peers, len(testCase.expectedPeers)) assert.Len(t, networkMap.Peers, len(testCase.expectedPeers))
assert.Len(t, networkMap.OfflinePeers, len(testCase.expectedOfflinePeers)) assert.Len(t, networkMap.OfflinePeers, len(testCase.expectedOfflinePeers))
} }
} }
func TestNewAccount(t *testing.T) { func TestNewAccount(t *testing.T) {
@@ -1349,6 +1349,11 @@ func TestAccount_Copy(t *testing.T) {
Peers: map[string]*Peer{ Peers: map[string]*Peer{
"peer1": { "peer1": {
Key: "key1", Key: "key1",
Status: &PeerStatus{
LastSeen: time.Now(),
Connected: true,
LoginExpired: false,
},
}, },
}, },
Users: map[string]*User{ Users: map[string]*User{
@@ -1372,27 +1377,35 @@ func TestAccount_Copy(t *testing.T) {
Groups: map[string]*Group{ Groups: map[string]*Group{
"group1": { "group1": {
ID: "group1", ID: "group1",
Peers: []string{"peer1"},
}, },
}, },
Rules: map[string]*Rule{ Rules: map[string]*Rule{
"rule1": { "rule1": {
ID: "rule1", ID: "rule1",
Destination: []string{},
Source: []string{},
}, },
}, },
Policies: []*Policy{ Policies: []*Policy{
{ {
ID: "policy1", ID: "policy1",
Enabled: true, Enabled: true,
Rules: make([]*PolicyRule, 0),
}, },
}, },
Routes: map[string]*route.Route{ Routes: map[string]*route.Route{
"route1": { "route1": {
ID: "route1", ID: "route1",
Groups: []string{"group1"},
}, },
}, },
NameServerGroups: map[string]*nbdns.NameServerGroup{ NameServerGroups: map[string]*nbdns.NameServerGroup{
"nsGroup1": { "nsGroup1": {
ID: "nsGroup1", ID: "nsGroup1",
Domains: []string{},
Groups: []string{},
NameServers: []nbdns.NameServer{},
}, },
}, },
DNSSettings: &DNSSettings{DisabledManagementGroups: []string{}}, DNSSettings: &DNSSettings{DisabledManagementGroups: []string{}},
@@ -1403,10 +1416,20 @@ func TestAccount_Copy(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
accountCopy := account.Copy() accountCopy := account.Copy()
assert.Equal(t, account, accountCopy, "account copy returned a different value than expected") accBytes, err := json.Marshal(account)
if err != nil {
t.Fatal(err)
}
account.Peers["peer1"].Status.Connected = false // we change original object to confirm that copy wont change
accCopyBytes, err := json.Marshal(accountCopy)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, string(accBytes), string(accCopyBytes), "account copy returned a different value than expected")
} }
// hasNilField validates pointers, maps and slices if they are nil // hasNilField validates pointers, maps and slices if they are nil
// TODO: make it check nested fields too
func hasNilField(x interface{}) error { func hasNilField(x interface{}) error {
rv := reflect.ValueOf(x) rv := reflect.ValueOf(x)
rv = rv.Elem() rv = rv.Elem()
@@ -1931,6 +1954,120 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) {
} }
} }
func TestAccount_SetJWTGroups(t *testing.T) {
// create a new account
account := &Account{
Peers: map[string]*Peer{
"peer1": {ID: "peer1", Key: "key1", UserID: "user1"},
"peer2": {ID: "peer2", Key: "key2", UserID: "user1"},
"peer3": {ID: "peer3", Key: "key3", UserID: "user1"},
"peer4": {ID: "peer4", Key: "key4", UserID: "user2"},
"peer5": {ID: "peer5", Key: "key5", UserID: "user2"},
},
Groups: map[string]*Group{
"group1": {ID: "group1", Name: "group1", Issued: GroupIssuedAPI, Peers: []string{}},
},
Settings: &Settings{GroupsPropagationEnabled: true},
Users: map[string]*User{
"user1": {Id: "user1"},
"user2": {Id: "user2"},
},
}
t.Run("api group already exists", func(t *testing.T) {
updated := account.SetJWTGroups("user1", []string{"group1"})
assert.False(t, updated, "account should not be updated")
assert.Empty(t, account.Users["user1"].AutoGroups, "auto groups must be empty")
})
t.Run("add jwt group", func(t *testing.T) {
updated := account.SetJWTGroups("user1", []string{"group1", "group2"})
assert.True(t, updated, "account should be updated")
assert.Len(t, account.Groups, 2, "new group should be added")
assert.Len(t, account.Users["user1"].AutoGroups, 1, "new group should be added")
assert.Contains(t, account.Groups, account.Users["user1"].AutoGroups[0], "groups must contain group2 from user groups")
})
t.Run("existed group not update", func(t *testing.T) {
updated := account.SetJWTGroups("user1", []string{"group2"})
assert.False(t, updated, "account should not be updated")
assert.Len(t, account.Groups, 2, "groups count should not be changed")
})
t.Run("add new group", func(t *testing.T) {
updated := account.SetJWTGroups("user2", []string{"group1", "group3"})
assert.True(t, updated, "account should be updated")
assert.Len(t, account.Groups, 3, "new group should be added")
assert.Len(t, account.Users["user2"].AutoGroups, 1, "new group should be added")
assert.Contains(t, account.Groups, account.Users["user2"].AutoGroups[0], "groups must contain group3 from user groups")
})
}
func TestAccount_UserGroupsAddToPeers(t *testing.T) {
account := &Account{
Peers: map[string]*Peer{
"peer1": {ID: "peer1", Key: "key1", UserID: "user1"},
"peer2": {ID: "peer2", Key: "key2", UserID: "user1"},
"peer3": {ID: "peer3", Key: "key3", UserID: "user1"},
"peer4": {ID: "peer4", Key: "key4", UserID: "user2"},
"peer5": {ID: "peer5", Key: "key5", UserID: "user2"},
},
Groups: map[string]*Group{
"group1": {ID: "group1", Name: "group1", Issued: GroupIssuedAPI, Peers: []string{}},
"group2": {ID: "group2", Name: "group2", Issued: GroupIssuedAPI, Peers: []string{}},
"group3": {ID: "group3", Name: "group3", Issued: GroupIssuedAPI, Peers: []string{}},
},
Users: map[string]*User{"user1": {Id: "user1"}, "user2": {Id: "user2"}},
}
t.Run("add groups", func(t *testing.T) {
account.UserGroupsAddToPeers("user1", "group1", "group2")
assert.ElementsMatch(t, account.Groups["group1"].Peers, []string{"peer1", "peer2", "peer3"}, "group1 contains users peers")
assert.ElementsMatch(t, account.Groups["group2"].Peers, []string{"peer1", "peer2", "peer3"}, "group2 contains users peers")
})
t.Run("add same groups", func(t *testing.T) {
account.UserGroupsAddToPeers("user1", "group1", "group2")
assert.Len(t, account.Groups["group1"].Peers, 3, "peers amount in group1 didn't change")
assert.Len(t, account.Groups["group2"].Peers, 3, "peers amount in group2 didn't change")
})
t.Run("add second user peers", func(t *testing.T) {
account.UserGroupsAddToPeers("user2", "group2")
assert.ElementsMatch(t, account.Groups["group2"].Peers,
[]string{"peer1", "peer2", "peer3", "peer4", "peer5"}, "group2 contains first and second user peers")
})
}
func TestAccount_UserGroupsRemoveFromPeers(t *testing.T) {
account := &Account{
Peers: map[string]*Peer{
"peer1": {ID: "peer1", Key: "key1", UserID: "user1"},
"peer2": {ID: "peer2", Key: "key2", UserID: "user1"},
"peer3": {ID: "peer3", Key: "key3", UserID: "user1"},
"peer4": {ID: "peer4", Key: "key4", UserID: "user2"},
"peer5": {ID: "peer5", Key: "key5", UserID: "user2"},
},
Groups: map[string]*Group{
"group1": {ID: "group1", Name: "group1", Issued: GroupIssuedAPI, Peers: []string{"peer1", "peer2", "peer3"}},
"group2": {ID: "group2", Name: "group2", Issued: GroupIssuedAPI, Peers: []string{"peer1", "peer2", "peer3", "peer4", "peer5"}},
"group3": {ID: "group3", Name: "group3", Issued: GroupIssuedAPI, Peers: []string{"peer4", "peer5"}},
},
Users: map[string]*User{"user1": {Id: "user1"}, "user2": {Id: "user2"}},
}
t.Run("remove groups", func(t *testing.T) {
account.UserGroupsRemoveFromPeers("user1", "group1", "group2")
assert.Empty(t, account.Groups["group1"].Peers, "remove all peers from group1")
assert.ElementsMatch(t, account.Groups["group2"].Peers, []string{"peer4", "peer5"}, "group2 contains only second users peers")
})
t.Run("remove group with no peers", func(t *testing.T) {
account.UserGroupsRemoveFromPeers("user1", "group3")
assert.Len(t, account.Groups["group3"].Peers, 2, "peers amount should not change")
})
}
func createManager(t *testing.T) (*DefaultAccountManager, error) { func createManager(t *testing.T) (*DefaultAccountManager, error) {
store, err := createStore(t) store, err := createStore(t)
if err != nil { if err != nil {

View File

@@ -1,5 +1,14 @@
package activity package activity
// Activity that triggered an Event
type Activity int
// Code is an activity string representation
type Code struct {
message string
code string
}
const ( const (
// PeerAddedByUser indicates that a user added a new peer to the system // PeerAddedByUser indicates that a user added a new peer to the system
PeerAddedByUser Activity = iota PeerAddedByUser Activity = iota
@@ -97,314 +106,80 @@ const (
UserUnblocked UserUnblocked
// GroupDeleted indicates that a user deleted group // GroupDeleted indicates that a user deleted group
GroupDeleted GroupDeleted
// UserLoggedInPeer indicates that user logged in their peer with an interactive SSO login
UserLoggedInPeer
// PeerLoginExpired indicates that the user peer login has been expired and peer disconnected
PeerLoginExpired
// DashboardLogin indicates that the user logged in to the dashboard
DashboardLogin
) )
const ( var activityMap = map[Activity]Code{
// PeerAddedByUserMessage is a human-readable text message of the PeerAddedByUser activity PeerAddedByUser: {"Peer added", "user.peer.add"},
PeerAddedByUserMessage string = "Peer added" PeerAddedWithSetupKey: {"Peer added", "setupkey.peer.add"},
// PeerAddedWithSetupKeyMessage is a human-readable text message of the PeerAddedWithSetupKey activity UserJoined: {"User joined", "user.join"},
PeerAddedWithSetupKeyMessage = PeerAddedByUserMessage UserInvited: {"User invited", "user.invite"},
// UserJoinedMessage is a human-readable text message of the UserJoined activity AccountCreated: {"Account created", "account.create"},
UserJoinedMessage string = "User joined" PeerRemovedByUser: {"Peer deleted", "user.peer.delete"},
// UserInvitedMessage is a human-readable text message of the UserInvited activity RuleAdded: {"Rule added", "rule.add"},
UserInvitedMessage string = "User invited" RuleUpdated: {"Rule updated", "rule.update"},
// AccountCreatedMessage is a human-readable text message of the AccountCreated activity RuleRemoved: {"Rule deleted", "rule.delete"},
AccountCreatedMessage string = "Account created" PolicyAdded: {"Policy added", "policy.add"},
// PeerRemovedByUserMessage is a human-readable text message of the PeerRemovedByUser activity PolicyUpdated: {"Policy updated", "policy.update"},
PeerRemovedByUserMessage string = "Peer deleted" PolicyRemoved: {"Policy deleted", "policy.delete"},
// RuleAddedMessage is a human-readable text message of the RuleAdded activity SetupKeyCreated: {"Setup key created", "setupkey.add"},
RuleAddedMessage string = "Rule added" SetupKeyUpdated: {"Setup key updated", "setupkey.update"},
// RuleRemovedMessage is a human-readable text message of the RuleRemoved activity SetupKeyRevoked: {"Setup key revoked", "setupkey.revoke"},
RuleRemovedMessage string = "Rule deleted" SetupKeyOverused: {"Setup key overused", "setupkey.overuse"},
// RuleUpdatedMessage is a human-readable text message of the RuleRemoved activity GroupCreated: {"Group created", "group.add"},
RuleUpdatedMessage string = "Rule updated" GroupUpdated: {"Group updated", "group.update"},
// PolicyAddedMessage is a human-readable text message of the PolicyAdded activity GroupAddedToPeer: {"Group added to peer", "peer.group.add"},
PolicyAddedMessage string = "Policy added" GroupRemovedFromPeer: {"Group removed from peer", "peer.group.delete"},
// PolicyRemovedMessage is a human-readable text message of the PolicyRemoved activity GroupAddedToUser: {"Group added to user", "user.group.add"},
PolicyRemovedMessage string = "Policy deleted" GroupRemovedFromUser: {"Group removed from user", "user.group.delete"},
// PolicyUpdatedMessage is a human-readable text message of the PolicyRemoved activity UserRoleUpdated: {"User role updated", "user.role.update"},
PolicyUpdatedMessage string = "Policy updated" GroupAddedToSetupKey: {"Group added to setup key", "setupkey.group.add"},
// SetupKeyCreatedMessage is a human-readable text message of the SetupKeyCreated activity GroupRemovedFromSetupKey: {"Group removed from user setup key", "setupkey.group.delete"},
SetupKeyCreatedMessage string = "Setup key created" GroupAddedToDisabledManagementGroups: {"Group added to disabled management DNS setting", "dns.setting.disabled.management.group.add"},
// SetupKeyUpdatedMessage is a human-readable text message of the SetupKeyUpdated activity GroupRemovedFromDisabledManagementGroups: {"Group removed from disabled management DNS setting", "dns.setting.disabled.management.group.delete"},
SetupKeyUpdatedMessage string = "Setup key updated" RouteCreated: {"Route created", "route.add"},
// SetupKeyRevokedMessage is a human-readable text message of the SetupKeyRevoked activity RouteRemoved: {"Route deleted", "route.delete"},
SetupKeyRevokedMessage string = "Setup key revoked" RouteUpdated: {"Route updated", "route.update"},
// SetupKeyOverusedMessage is a human-readable text message of the SetupKeyOverused activity PeerSSHEnabled: {"Peer SSH server enabled", "peer.ssh.enable"},
SetupKeyOverusedMessage string = "Setup key overused" PeerSSHDisabled: {"Peer SSH server disabled", "peer.ssh.disable"},
// GroupCreatedMessage is a human-readable text message of the GroupCreated activity PeerRenamed: {"Peer renamed", "peer.rename"},
GroupCreatedMessage string = "Group created" PeerLoginExpirationEnabled: {"Peer login expiration enabled", "peer.login.expiration.enable"},
// GroupUpdatedMessage is a human-readable text message of the GroupUpdated activity PeerLoginExpirationDisabled: {"Peer login expiration disabled", "peer.login.expiration.disable"},
GroupUpdatedMessage string = "Group updated" NameserverGroupCreated: {"Nameserver group created", "nameserver.group.add"},
// GroupAddedToPeerMessage is a human-readable text message of the GroupAddedToPeer activity NameserverGroupDeleted: {"Nameserver group deleted", "nameserver.group.delete"},
GroupAddedToPeerMessage string = "Group added to peer" NameserverGroupUpdated: {"Nameserver group updated", "nameserver.group.update"},
// GroupRemovedFromPeerMessage is a human-readable text message of the GroupRemovedFromPeer activity AccountPeerLoginExpirationDurationUpdated: {"Account peer login expiration duration updated", "account.setting.peer.login.expiration.update"},
GroupRemovedFromPeerMessage string = "Group removed from peer" AccountPeerLoginExpirationEnabled: {"Account peer login expiration enabled", "account.setting.peer.login.expiration.enable"},
// GroupAddedToUserMessage is a human-readable text message of the GroupAddedToUser activity AccountPeerLoginExpirationDisabled: {"Account peer login expiration disabled", "account.setting.peer.login.expiration.disable"},
GroupAddedToUserMessage string = "Group added to user" PersonalAccessTokenCreated: {"Personal access token created", "personal.access.token.create"},
// GroupRemovedFromUserMessage is a human-readable text message of the GroupRemovedFromUser activity PersonalAccessTokenDeleted: {"Personal access token deleted", "personal.access.token.delete"},
GroupRemovedFromUserMessage string = "Group removed from user" ServiceUserCreated: {"Service user created", "service.user.create"},
// UserRoleUpdatedMessage is a human-readable text message of the UserRoleUpdatedMessage activity ServiceUserDeleted: {"Service user deleted", "service.user.delete"},
UserRoleUpdatedMessage string = "User role updated" UserBlocked: {"User blocked", "user.block"},
// GroupAddedToSetupKeyMessage is a human-readable text message of the GroupAddedToSetupKey activity UserUnblocked: {"User unblocked", "user.unblock"},
GroupAddedToSetupKeyMessage string = "Group added to setup key" GroupDeleted: {"Group deleted", "group.delete"},
// GroupRemovedFromSetupKeyMessage is a human-readable text message of the GroupRemovedFromSetupKey activity UserLoggedInPeer: {"User logged in peer", "user.peer.login"},
GroupRemovedFromSetupKeyMessage string = "Group removed from user setup key" PeerLoginExpired: {"Peer login expired", "peer.login.expire"},
// GroupAddedToDisabledManagementGroupsMessage is a human-readable text message of the GroupAddedToDisabledManagementGroups activity DashboardLogin: {"Dashboard login", "dashboard.login"},
GroupAddedToDisabledManagementGroupsMessage string = "Group added to disabled management DNS setting"
// GroupRemovedFromDisabledManagementGroupsMessage is a human-readable text message of the GroupRemovedFromDisabledManagementGroups activity
GroupRemovedFromDisabledManagementGroupsMessage string = "Group removed from disabled management DNS setting"
// RouteCreatedMessage is a human-readable text message of the RouteCreated activity
RouteCreatedMessage string = "Route created"
// RouteRemovedMessage is a human-readable text message of the RouteRemoved activity
RouteRemovedMessage string = "Route deleted"
// RouteUpdatedMessage is a human-readable text message of the RouteUpdated activity
RouteUpdatedMessage string = "Route updated"
// PeerSSHEnabledMessage is a human-readable text message of the PeerSSHEnabled activity
PeerSSHEnabledMessage string = "Peer SSH server enabled"
// PeerSSHDisabledMessage is a human-readable text message of the PeerSSHDisabled activity
PeerSSHDisabledMessage string = "Peer SSH server disabled"
// PeerRenamedMessage is a human-readable text message of the PeerRenamed activity
PeerRenamedMessage string = "Peer renamed"
// PeerLoginExpirationDisabledMessage is a human-readable text message of the PeerLoginExpirationDisabled activity
PeerLoginExpirationDisabledMessage string = "Peer login expiration disabled"
// PeerLoginExpirationEnabledMessage is a human-readable text message of the PeerLoginExpirationEnabled activity
PeerLoginExpirationEnabledMessage string = "Peer login expiration enabled"
// NameserverGroupCreatedMessage is a human-readable text message of the NameserverGroupCreated activity
NameserverGroupCreatedMessage string = "Nameserver group created"
// NameserverGroupDeletedMessage is a human-readable text message of the NameserverGroupDeleted activity
NameserverGroupDeletedMessage string = "Nameserver group deleted"
// NameserverGroupUpdatedMessage is a human-readable text message of the NameserverGroupUpdated activity
NameserverGroupUpdatedMessage string = "Nameserver group updated"
// AccountPeerLoginExpirationEnabledMessage is a human-readable text message of the AccountPeerLoginExpirationEnabled activity
AccountPeerLoginExpirationEnabledMessage string = "Peer login expiration enabled for the account"
// AccountPeerLoginExpirationDisabledMessage is a human-readable text message of the AccountPeerLoginExpirationDisabled activity
AccountPeerLoginExpirationDisabledMessage string = "Peer login expiration disabled for the account"
// AccountPeerLoginExpirationDurationUpdatedMessage is a human-readable text message of the AccountPeerLoginExpirationDurationUpdated activity
AccountPeerLoginExpirationDurationUpdatedMessage string = "Peer login expiration duration updated"
// PersonalAccessTokenCreatedMessage is a human-readable text message of the PersonalAccessTokenCreated activity
PersonalAccessTokenCreatedMessage string = "Personal access token created"
// PersonalAccessTokenDeletedMessage is a human-readable text message of the PersonalAccessTokenDeleted activity
PersonalAccessTokenDeletedMessage string = "Personal access token deleted"
// ServiceUserCreatedMessage is a human-readable text message of the ServiceUserCreated activity
ServiceUserCreatedMessage string = "Service user created"
// ServiceUserDeletedMessage is a human-readable text message of the ServiceUserDeleted activity
ServiceUserDeletedMessage string = "Service user deleted"
// UserBlockedMessage is a human-readable text message of the UserBlocked activity
UserBlockedMessage string = "User blocked"
// UserUnblockedMessage is a human-readable text message of the UserUnblocked activity
UserUnblockedMessage string = "User unblocked"
// GroupDeletedMessage is a human-readable text message of the GroupDeleted activity
GroupDeletedMessage string = "Group deleted"
)
// Activity that triggered an Event
type Activity int
// Message returns a string representation of an activity
func (a Activity) Message() string {
switch a {
case PeerAddedByUser:
return PeerAddedByUserMessage
case PeerRemovedByUser:
return PeerRemovedByUserMessage
case PeerAddedWithSetupKey:
return PeerAddedWithSetupKeyMessage
case UserJoined:
return UserJoinedMessage
case UserInvited:
return UserInvitedMessage
case AccountCreated:
return AccountCreatedMessage
case RuleAdded:
return RuleAddedMessage
case RuleRemoved:
return RuleRemovedMessage
case RuleUpdated:
return RuleUpdatedMessage
case PolicyAdded:
return PolicyAddedMessage
case PolicyRemoved:
return PolicyRemovedMessage
case PolicyUpdated:
return PolicyUpdatedMessage
case SetupKeyCreated:
return SetupKeyCreatedMessage
case SetupKeyUpdated:
return SetupKeyUpdatedMessage
case SetupKeyRevoked:
return SetupKeyRevokedMessage
case SetupKeyOverused:
return SetupKeyOverusedMessage
case GroupCreated:
return GroupCreatedMessage
case GroupUpdated:
return GroupUpdatedMessage
case GroupAddedToPeer:
return GroupAddedToPeerMessage
case GroupRemovedFromPeer:
return GroupRemovedFromPeerMessage
case GroupRemovedFromUser:
return GroupRemovedFromUserMessage
case GroupAddedToUser:
return GroupAddedToUserMessage
case UserRoleUpdated:
return UserRoleUpdatedMessage
case GroupAddedToSetupKey:
return GroupAddedToSetupKeyMessage
case GroupRemovedFromSetupKey:
return GroupRemovedFromSetupKeyMessage
case GroupAddedToDisabledManagementGroups:
return GroupAddedToDisabledManagementGroupsMessage
case GroupRemovedFromDisabledManagementGroups:
return GroupRemovedFromDisabledManagementGroupsMessage
case RouteCreated:
return RouteCreatedMessage
case RouteRemoved:
return RouteRemovedMessage
case RouteUpdated:
return RouteUpdatedMessage
case PeerSSHEnabled:
return PeerSSHEnabledMessage
case PeerSSHDisabled:
return PeerSSHDisabledMessage
case PeerLoginExpirationEnabled:
return PeerLoginExpirationEnabledMessage
case PeerLoginExpirationDisabled:
return PeerLoginExpirationDisabledMessage
case PeerRenamed:
return PeerRenamedMessage
case NameserverGroupCreated:
return NameserverGroupCreatedMessage
case NameserverGroupDeleted:
return NameserverGroupDeletedMessage
case NameserverGroupUpdated:
return NameserverGroupUpdatedMessage
case AccountPeerLoginExpirationEnabled:
return AccountPeerLoginExpirationEnabledMessage
case AccountPeerLoginExpirationDisabled:
return AccountPeerLoginExpirationDisabledMessage
case AccountPeerLoginExpirationDurationUpdated:
return AccountPeerLoginExpirationDurationUpdatedMessage
case PersonalAccessTokenCreated:
return PersonalAccessTokenCreatedMessage
case PersonalAccessTokenDeleted:
return PersonalAccessTokenDeletedMessage
case ServiceUserCreated:
return ServiceUserCreatedMessage
case ServiceUserDeleted:
return ServiceUserDeletedMessage
case UserBlocked:
return UserBlockedMessage
case UserUnblocked:
return UserUnblockedMessage
case GroupDeleted:
return GroupDeletedMessage
default:
return "UNKNOWN_ACTIVITY"
}
} }
// StringCode returns a string code of the activity // StringCode returns a string code of the activity
func (a Activity) StringCode() string { func (a Activity) StringCode() string {
switch a { if code, ok := activityMap[a]; ok {
case PeerAddedByUser: return code.code
return "user.peer.add" }
case PeerRemovedByUser:
return "user.peer.delete"
case PeerAddedWithSetupKey:
return "setupkey.peer.add"
case UserJoined:
return "user.join"
case UserInvited:
return "user.invite"
case UserBlocked:
return "user.block"
case UserUnblocked:
return "user.unblock"
case AccountCreated:
return "account.create"
case RuleAdded:
return "rule.add"
case RuleRemoved:
return "rule.delete"
case RuleUpdated:
return "rule.update"
case PolicyAdded:
return "policy.add"
case PolicyRemoved:
return "policy.delete"
case PolicyUpdated:
return "policy.update"
case SetupKeyCreated:
return "setupkey.add"
case SetupKeyRevoked:
return "setupkey.revoke"
case SetupKeyOverused:
return "setupkey.overuse"
case SetupKeyUpdated:
return "setupkey.update"
case GroupCreated:
return "group.add"
case GroupUpdated:
return "group.update"
case GroupDeleted:
return "group.delete"
case GroupRemovedFromPeer:
return "peer.group.delete"
case GroupAddedToPeer:
return "peer.group.add"
case GroupAddedToUser:
return "user.group.add"
case GroupRemovedFromUser:
return "user.group.delete"
case UserRoleUpdated:
return "user.role.update"
case GroupAddedToSetupKey:
return "setupkey.group.add"
case GroupRemovedFromSetupKey:
return "setupkey.group.delete"
case GroupAddedToDisabledManagementGroups:
return "dns.setting.disabled.management.group.add"
case GroupRemovedFromDisabledManagementGroups:
return "dns.setting.disabled.management.group.delete"
case RouteCreated:
return "route.add"
case RouteRemoved:
return "route.delete"
case RouteUpdated:
return "route.update"
case PeerRenamed:
return "peer.rename"
case PeerSSHEnabled:
return "peer.ssh.enable"
case PeerSSHDisabled:
return "peer.ssh.disable"
case PeerLoginExpirationDisabled:
return "peer.login.expiration.disable"
case PeerLoginExpirationEnabled:
return "peer.login.expiration.enable"
case NameserverGroupCreated:
return "nameserver.group.add"
case NameserverGroupDeleted:
return "nameserver.group.delete"
case NameserverGroupUpdated:
return "nameserver.group.update"
case AccountPeerLoginExpirationDurationUpdated:
return "account.setting.peer.login.expiration.update"
case AccountPeerLoginExpirationEnabled:
return "account.setting.peer.login.expiration.enable"
case AccountPeerLoginExpirationDisabled:
return "account.setting.peer.login.expiration.disable"
case PersonalAccessTokenCreated:
return "personal.access.token.create"
case PersonalAccessTokenDeleted:
return "personal.access.token.delete"
case ServiceUserCreated:
return "service.user.create"
case ServiceUserDeleted:
return "service.user.delete"
default:
return "UNKNOWN_ACTIVITY" return "UNKNOWN_ACTIVITY"
} }
// Message returns a string representation of an activity
func (a Activity) Message() string {
if code, ok := activityMap[a]; ok {
return code.message
}
return "UNKNOWN_ACTIVITY"
} }

View File

@@ -570,6 +570,26 @@ func (s *FileStore) SavePeerStatus(accountID, peerID string, peerStatus PeerStat
return nil return nil
} }
// SaveUserLastLogin stores the last login time for a user in memory. It doesn't attempt to persist data to speed up things.
func (s *FileStore) SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error {
s.mux.Lock()
defer s.mux.Unlock()
account, err := s.getAccount(accountID)
if err != nil {
return err
}
peer := account.Users[userID]
if peer == nil {
return status.Errorf(status.NotFound, "user %s not found", userID)
}
peer.LastLogin = lastLogin
return nil
}
// Close the FileStore persisting data to disk // Close the FileStore persisting data to disk
func (s *FileStore) Close() error { func (s *FileStore) Close() error {
s.mux.Lock() s.mux.Lock()

View File

@@ -59,12 +59,14 @@ func (g *Group) EventMeta() map[string]any {
} }
func (g *Group) Copy() *Group { func (g *Group) Copy() *Group {
return &Group{ group := &Group{
ID: g.ID, ID: g.ID,
Name: g.Name, Name: g.Name,
Issued: g.Issued, Issued: g.Issued,
Peers: g.Peers[:], Peers: make([]string, len(g.Peers)),
} }
copy(group.Peers, g.Peers)
return group
} }
// GetGroup object of the peers // GetGroup object of the peers

View File

@@ -80,12 +80,14 @@ func (h *AccountsHandler) UpdateAccount(w http.ResponseWriter, r *http.Request)
if req.Settings.JwtGroupsEnabled != nil { if req.Settings.JwtGroupsEnabled != nil {
settings.JWTGroupsEnabled = *req.Settings.JwtGroupsEnabled settings.JWTGroupsEnabled = *req.Settings.JwtGroupsEnabled
} }
if req.Settings.GroupsPropagationEnabled != nil {
settings.GroupsPropagationEnabled = *req.Settings.GroupsPropagationEnabled
}
if req.Settings.JwtGroupsClaimName != nil { if req.Settings.JwtGroupsClaimName != nil {
settings.JWTGroupsClaimName = *req.Settings.JwtGroupsClaimName settings.JWTGroupsClaimName = *req.Settings.JwtGroupsClaimName
} }
updatedAccount, err := h.accountManager.UpdateAccountSettings(accountID, user.Id, settings) updatedAccount, err := h.accountManager.UpdateAccountSettings(accountID, user.Id, settings)
if err != nil { if err != nil {
util.WriteError(err, w) util.WriteError(err, w)
return return
@@ -102,6 +104,7 @@ func toAccountResponse(account *server.Account) *api.Account {
Settings: api.AccountSettings{ Settings: api.AccountSettings{
PeerLoginExpiration: int(account.Settings.PeerLoginExpiration.Seconds()), PeerLoginExpiration: int(account.Settings.PeerLoginExpiration.Seconds()),
PeerLoginExpirationEnabled: account.Settings.PeerLoginExpirationEnabled, PeerLoginExpirationEnabled: account.Settings.PeerLoginExpirationEnabled,
GroupsPropagationEnabled: &account.Settings.GroupsPropagationEnabled,
JwtGroupsEnabled: &account.Settings.JWTGroupsEnabled, JwtGroupsEnabled: &account.Settings.JWTGroupsEnabled,
JwtGroupsClaimName: &account.Settings.JWTGroupsClaimName, JwtGroupsClaimName: &account.Settings.JWTGroupsClaimName,
}, },

View File

@@ -38,7 +38,6 @@ func initAccountsTestData(account *server.Account, admin *server.User) *Accounts
accCopy := account.Copy() accCopy := account.Copy()
accCopy.UpdateSettings(newSettings) accCopy.UpdateSettings(newSettings)
return accCopy, nil return accCopy, nil
}, },
}, },
claimsExtractor: jwtclaims.NewClaimsExtractor( claimsExtractor: jwtclaims.NewClaimsExtractor(
@@ -54,7 +53,6 @@ func initAccountsTestData(account *server.Account, admin *server.User) *Accounts
} }
func TestAccounts_AccountsHandler(t *testing.T) { func TestAccounts_AccountsHandler(t *testing.T) {
accountID := "test_account" accountID := "test_account"
adminUser := server.NewAdminUser("test_user") adminUser := server.NewAdminUser("test_user")
@@ -94,6 +92,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
expectedSettings: api.AccountSettings{ expectedSettings: api.AccountSettings{
PeerLoginExpiration: int(time.Hour.Seconds()), PeerLoginExpiration: int(time.Hour.Seconds()),
PeerLoginExpirationEnabled: false, PeerLoginExpirationEnabled: false,
GroupsPropagationEnabled: br(false),
JwtGroupsClaimName: sr(""), JwtGroupsClaimName: sr(""),
JwtGroupsEnabled: br(false), JwtGroupsEnabled: br(false),
}, },
@@ -110,6 +109,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
expectedSettings: api.AccountSettings{ expectedSettings: api.AccountSettings{
PeerLoginExpiration: 15552000, PeerLoginExpiration: 15552000,
PeerLoginExpirationEnabled: true, PeerLoginExpirationEnabled: true,
GroupsPropagationEnabled: br(false),
JwtGroupsClaimName: sr(""), JwtGroupsClaimName: sr(""),
JwtGroupsEnabled: br(false), JwtGroupsEnabled: br(false),
}, },
@@ -126,12 +126,30 @@ func TestAccounts_AccountsHandler(t *testing.T) {
expectedSettings: api.AccountSettings{ expectedSettings: api.AccountSettings{
PeerLoginExpiration: 15552000, PeerLoginExpiration: 15552000,
PeerLoginExpirationEnabled: false, PeerLoginExpirationEnabled: false,
GroupsPropagationEnabled: br(false),
JwtGroupsClaimName: sr("roles"), JwtGroupsClaimName: sr("roles"),
JwtGroupsEnabled: br(true), JwtGroupsEnabled: br(true),
}, },
expectedArray: false, expectedArray: false,
expectedID: accountID, expectedID: accountID,
}, },
{
name: "PutAccount OK wiht JWT Propagation",
expectedBody: true,
requestType: http.MethodPut,
requestPath: "/api/accounts/" + accountID,
requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 554400,\"peer_login_expiration_enabled\": true,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"groups\",\"groups_propagation_enabled\":true}}"),
expectedStatus: http.StatusOK,
expectedSettings: api.AccountSettings{
PeerLoginExpiration: 554400,
PeerLoginExpirationEnabled: true,
GroupsPropagationEnabled: br(true),
JwtGroupsClaimName: sr("groups"),
JwtGroupsEnabled: br(true),
},
expectedArray: false,
expectedID: accountID,
},
{ {
name: "Update account failure with high peer_login_expiration more than 180 days", name: "Update account failure with high peer_login_expiration more than 180 days",
expectedBody: true, expectedBody: true,

View File

@@ -54,6 +54,10 @@ components:
description: Period of time after which peer login expires (seconds). description: Period of time after which peer login expires (seconds).
type: integer type: integer
example: 43200 example: 43200
groups_propagation_enabled:
description: Allows propagate the new user auto groups to peers that belongs to the user
type: boolean
example: true
jwt_groups_enabled: jwt_groups_enabled:
description: Allows extract groups from JWT claim and add it to account groups. description: Allows extract groups from JWT claim and add it to account groups.
type: boolean type: boolean
@@ -96,6 +100,11 @@ components:
type: string type: string
enum: [ "active","invited","blocked" ] enum: [ "active","invited","blocked" ]
example: active example: active
last_login:
description: Last time this user performed a login to the dashboard
type: string
format: date-time
example: 2023-05-05T09:00:35.477782Z
auto_groups: auto_groups:
description: Groups to auto-assign to peers registered by this user description: Groups to auto-assign to peers registered by this user
type: array type: array
@@ -327,7 +336,7 @@ components:
type: string type: string
example: valid example: valid
auto_groups: auto_groups:
description: Setup key groups to auto-assign to peers registered with this key description: List of group IDs to auto-assign to peers registered with this key
type: array type: array
items: items:
type: string type: string
@@ -369,13 +378,15 @@ components:
expires_in: expires_in:
description: Expiration time in seconds description: Expiration time in seconds
type: integer type: integer
example: 43200 minimum: 86400
maximum: 31536000
example: 86400
revoked: revoked:
description: Setup key revocation status description: Setup key revocation status
type: boolean type: boolean
example: false example: false
auto_groups: auto_groups:
description: Setup key groups to auto-assign to peers registered with this key description: List of group IDs to auto-assign to peers registered with this key
type: array type: array
items: items:
type: string type: string
@@ -886,7 +897,7 @@ components:
description: The string code of the activity that occurred during the event description: The string code of the activity that occurred during the event
type: string type: string
enum: [ "user.peer.delete", "user.join", "user.invite", "user.peer.add", "user.group.add", "user.group.delete", enum: [ "user.peer.delete", "user.join", "user.invite", "user.peer.add", "user.group.add", "user.group.delete",
"user.role.update", "user.block", "user.unblock", "user.role.update", "user.block", "user.unblock", "user.peer.login",
"setupkey.peer.add", "setupkey.add", "setupkey.update", "setupkey.revoke", "setupkey.overuse", "setupkey.peer.add", "setupkey.add", "setupkey.update", "setupkey.revoke", "setupkey.overuse",
"setupkey.group.delete", "setupkey.group.add", "setupkey.group.delete", "setupkey.group.add",
"rule.add", "rule.delete", "rule.update", "rule.add", "rule.delete", "rule.update",
@@ -895,7 +906,7 @@ components:
"account.create", "account.setting.peer.login.expiration.update", "account.setting.peer.login.expiration.disable", "account.setting.peer.login.expiration.enable", "account.create", "account.setting.peer.login.expiration.update", "account.setting.peer.login.expiration.disable", "account.setting.peer.login.expiration.enable",
"route.add", "route.delete", "route.update", "route.add", "route.delete", "route.update",
"nameserver.group.add", "nameserver.group.delete", "nameserver.group.update", "nameserver.group.add", "nameserver.group.delete", "nameserver.group.update",
"peer.ssh.disable", "peer.ssh.enable", "peer.rename", "peer.login.expiration.disable", "peer.login.expiration.enable", "peer.ssh.disable", "peer.ssh.enable", "peer.rename", "peer.login.expiration.disable", "peer.login.expiration.enable", "peer.login.expire",
"service.user.create", "personal.access.token.create", "service.user.delete", "personal.access.token.delete" ] "service.user.create", "personal.access.token.create", "service.user.delete", "personal.access.token.delete" ]
example: route.add example: route.add
initiator_id: initiator_id:

View File

@@ -27,6 +27,7 @@ const (
EventActivityCodeNameserverGroupUpdate EventActivityCode = "nameserver.group.update" EventActivityCodeNameserverGroupUpdate EventActivityCode = "nameserver.group.update"
EventActivityCodePeerLoginExpirationDisable EventActivityCode = "peer.login.expiration.disable" EventActivityCodePeerLoginExpirationDisable EventActivityCode = "peer.login.expiration.disable"
EventActivityCodePeerLoginExpirationEnable EventActivityCode = "peer.login.expiration.enable" EventActivityCodePeerLoginExpirationEnable EventActivityCode = "peer.login.expiration.enable"
EventActivityCodePeerLoginExpire EventActivityCode = "peer.login.expire"
EventActivityCodePeerRename EventActivityCode = "peer.rename" EventActivityCodePeerRename EventActivityCode = "peer.rename"
EventActivityCodePeerSshDisable EventActivityCode = "peer.ssh.disable" EventActivityCodePeerSshDisable EventActivityCode = "peer.ssh.disable"
EventActivityCodePeerSshEnable EventActivityCode = "peer.ssh.enable" EventActivityCodePeerSshEnable EventActivityCode = "peer.ssh.enable"
@@ -57,6 +58,7 @@ const (
EventActivityCodeUserJoin EventActivityCode = "user.join" EventActivityCodeUserJoin EventActivityCode = "user.join"
EventActivityCodeUserPeerAdd EventActivityCode = "user.peer.add" EventActivityCodeUserPeerAdd EventActivityCode = "user.peer.add"
EventActivityCodeUserPeerDelete EventActivityCode = "user.peer.delete" EventActivityCodeUserPeerDelete EventActivityCode = "user.peer.delete"
EventActivityCodeUserPeerLogin EventActivityCode = "user.peer.login"
EventActivityCodeUserRoleUpdate EventActivityCode = "user.role.update" EventActivityCodeUserRoleUpdate EventActivityCode = "user.role.update"
EventActivityCodeUserUnblock EventActivityCode = "user.unblock" EventActivityCodeUserUnblock EventActivityCode = "user.unblock"
) )
@@ -129,6 +131,9 @@ type AccountRequest struct {
// AccountSettings defines model for AccountSettings. // AccountSettings defines model for AccountSettings.
type AccountSettings struct { type AccountSettings struct {
// GroupsPropagationEnabled Allows propagate the new user auto groups to peers that belongs to the user
GroupsPropagationEnabled *bool `json:"groups_propagation_enabled,omitempty"`
// JwtGroupsClaimName Name of the claim from which we extract groups names to add it to account groups. // JwtGroupsClaimName Name of the claim from which we extract groups names to add it to account groups.
JwtGroupsClaimName *string `json:"jwt_groups_claim_name,omitempty"` JwtGroupsClaimName *string `json:"jwt_groups_claim_name,omitempty"`
@@ -681,7 +686,7 @@ type RuleRequest struct {
// SetupKey defines model for SetupKey. // SetupKey defines model for SetupKey.
type SetupKey struct { type SetupKey struct {
// AutoGroups Setup key groups to auto-assign to peers registered with this key // AutoGroups List of group IDs to auto-assign to peers registered with this key
AutoGroups []string `json:"auto_groups"` AutoGroups []string `json:"auto_groups"`
// Expires Setup Key expiration date // Expires Setup Key expiration date
@@ -723,7 +728,7 @@ type SetupKey struct {
// SetupKeyRequest defines model for SetupKeyRequest. // SetupKeyRequest defines model for SetupKeyRequest.
type SetupKeyRequest struct { type SetupKeyRequest struct {
// AutoGroups Setup key groups to auto-assign to peers registered with this key // AutoGroups List of group IDs to auto-assign to peers registered with this key
AutoGroups []string `json:"auto_groups"` AutoGroups []string `json:"auto_groups"`
// ExpiresIn Expiration time in seconds // ExpiresIn Expiration time in seconds
@@ -762,6 +767,9 @@ type User struct {
// IsServiceUser Is true if this user is a service user // IsServiceUser Is true if this user is a service user
IsServiceUser *bool `json:"is_service_user,omitempty"` IsServiceUser *bool `json:"is_service_user,omitempty"`
// LastLogin Last time this user performed a login to the dashboard
LastLogin *time.Time `json:"last_login,omitempty"`
// Name User's name from idp provider // Name User's name from idp provider
Name string `json:"name"` Name string `json:"name"`

View File

@@ -54,6 +54,7 @@ var baseExistingNSGroup = &nbdns.NameServerGroup{
}, },
}, },
Groups: []string{"testing"}, Groups: []string{"testing"},
Domains: []string{"domain"},
Enabled: true, Enabled: true,
} }

View File

@@ -60,6 +60,13 @@ func (h *SetupKeysHandler) CreateSetupKey(w http.ResponseWriter, r *http.Request
expiresIn := time.Duration(req.ExpiresIn) * time.Second expiresIn := time.Duration(req.ExpiresIn) * time.Second
day := time.Hour * 24
year := day * 365
if expiresIn < day || expiresIn > year {
util.WriteError(status.Errorf(status.InvalidArgument, "expiresIn should be between 1 day and 365 days"), w)
return
}
if req.AutoGroups == nil { if req.AutoGroups == nil {
req.AutoGroups = []string{} req.AutoGroups = []string{}
} }

View File

@@ -143,7 +143,7 @@ func TestSetupKeysHandlers(t *testing.T) {
requestType: http.MethodPost, requestType: http.MethodPost,
requestPath: "/api/setup-keys", requestPath: "/api/setup-keys",
requestBody: bytes.NewBuffer( requestBody: bytes.NewBuffer(
[]byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"%s\"}", newSetupKey.Name, newSetupKey.Type))), []byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"%s\",\"expires_in\":86400}", newSetupKey.Name, newSetupKey.Type))),
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
expectedBody: true, expectedBody: true,
expectedSetupKey: toResponseBody(newSetupKey), expectedSetupKey: toResponseBody(newSetupKey),

View File

@@ -270,5 +270,6 @@ func toUserResponse(user *server.UserInfo, currenUserID string) *api.User {
IsCurrent: &isCurrent, IsCurrent: &isCurrent,
IsServiceUser: &user.IsServiceUser, IsServiceUser: &user.IsServiceUser,
IsBlocked: user.IsBlocked, IsBlocked: user.IsBlocked,
LastLogin: &user.LastLogin,
} }
} }

View File

@@ -461,7 +461,7 @@ func (am *Auth0Manager) UpdateUserAppMetadata(userID string, appMetadata AppMeta
return nil return nil
} }
func buildCreateUserRequestPayload(email string, name string, accountID string) (string, error) { func buildCreateUserRequestPayload(email, name, accountID, invitedByEmail string) (string, error) {
invite := true invite := true
req := &createUserRequest{ req := &createUserRequest{
Email: email, Email: email,
@@ -469,6 +469,7 @@ func buildCreateUserRequestPayload(email string, name string, accountID string)
AppMeta: AppMetadata{ AppMeta: AppMetadata{
WTAccountID: accountID, WTAccountID: accountID,
WTPendingInvite: &invite, WTPendingInvite: &invite,
WTInvitedBy: invitedByEmail,
}, },
Connection: "Username-Password-Authentication", Connection: "Username-Password-Authentication",
Password: GeneratePassword(8, 1, 1, 1), Password: GeneratePassword(8, 1, 1, 1),
@@ -634,9 +635,9 @@ func (am *Auth0Manager) GetUserByEmail(email string) ([]*UserData, error) {
} }
// CreateUser creates a new user in Auth0 Idp and sends an invite // CreateUser creates a new user in Auth0 Idp and sends an invite
func (am *Auth0Manager) CreateUser(email string, name string, accountID string) (*UserData, error) { func (am *Auth0Manager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
payloadString, err := buildCreateUserRequestPayload(email, name, accountID) payloadString, err := buildCreateUserRequestPayload(email, name, accountID, invitedByEmail)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -343,7 +343,7 @@ func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{ updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
name: "Bad Status Code", name: "Bad Status Code",
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp), inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":null}}", appMetadata.WTAccountID), expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":null,\"wt_invited_by_email\":\"\"}}", appMetadata.WTAccountID),
appMetadata: appMetadata, appMetadata: appMetadata,
statusCode: 400, statusCode: 400,
helper: JsonParser{}, helper: JsonParser{},
@@ -366,7 +366,7 @@ func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{ updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
name: "Good request", name: "Good request",
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp), inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":null}}", appMetadata.WTAccountID), expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":null,\"wt_invited_by_email\":\"\"}}", appMetadata.WTAccountID),
appMetadata: appMetadata, appMetadata: appMetadata,
statusCode: 200, statusCode: 200,
helper: JsonParser{}, helper: JsonParser{},
@@ -378,7 +378,7 @@ func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
updateUserAppMetadataTestCase5 := updateUserAppMetadataTest{ updateUserAppMetadataTestCase5 := updateUserAppMetadataTest{
name: "Update Pending Invite", name: "Update Pending Invite",
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp), inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":true}}", appMetadata.WTAccountID), expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":true,\"wt_invited_by_email\":\"\"}}", appMetadata.WTAccountID),
appMetadata: AppMetadata{ appMetadata: AppMetadata{
WTAccountID: "ok", WTAccountID: "ok",
WTPendingInvite: &invite, WTPendingInvite: &invite,

View File

@@ -362,7 +362,7 @@ func (am *AuthentikManager) GetAllAccounts() (map[string][]*UserData, error) {
} }
// CreateUser creates a new user in authentik Idp and sends an invitation. // CreateUser creates a new user in authentik Idp and sends an invitation.
func (am *AuthentikManager) CreateUser(email string, name string, accountID string) (*UserData, error) { func (am *AuthentikManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
ctx, err := am.authenticationContext() ctx, err := am.authenticationContext()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -236,7 +236,7 @@ func (ac *AzureCredentials) Authenticate() (JWTToken, error) {
} }
// CreateUser creates a new user in azure AD Idp. // CreateUser creates a new user in azure AD Idp.
func (am *AzureManager) CreateUser(email string, name string, accountID string) (*UserData, error) { func (am *AzureManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
payload, err := buildAzureCreateUserRequestPayload(email, name, accountID, am.ClientID) payload, err := buildAzureCreateUserRequestPayload(email, name, accountID, am.ClientID)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -185,7 +185,7 @@ func (gm *GoogleWorkspaceManager) GetAllAccounts() (map[string][]*UserData, erro
} }
// CreateUser creates a new user in Google Workspace and sends an invitation. // CreateUser creates a new user in Google Workspace and sends an invitation.
func (gm *GoogleWorkspaceManager) CreateUser(email string, name string, accountID string) (*UserData, error) { func (gm *GoogleWorkspaceManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
invite := true invite := true
metadata := AppMetadata{ metadata := AppMetadata{
WTAccountID: accountID, WTAccountID: accountID,

View File

@@ -15,7 +15,7 @@ type Manager interface {
GetUserDataByID(userId string, appMetadata AppMetadata) (*UserData, error) GetUserDataByID(userId string, appMetadata AppMetadata) (*UserData, error)
GetAccount(accountId string) ([]*UserData, error) GetAccount(accountId string) ([]*UserData, error)
GetAllAccounts() (map[string][]*UserData, error) GetAllAccounts() (map[string][]*UserData, error)
CreateUser(email string, name string, accountID string) (*UserData, error) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error)
GetUserByEmail(email string) ([]*UserData, error) GetUserByEmail(email string) ([]*UserData, error)
InviteUserByID(userID string) error InviteUserByID(userID string) error
} }
@@ -72,6 +72,7 @@ type AppMetadata struct {
// maps to wt_account_id when json.marshal // maps to wt_account_id when json.marshal
WTAccountID string `json:"wt_account_id,omitempty"` WTAccountID string `json:"wt_account_id,omitempty"`
WTPendingInvite *bool `json:"wt_pending_invite"` WTPendingInvite *bool `json:"wt_pending_invite"`
WTInvitedBy string `json:"wt_invited_by_email"`
} }
// JWTToken a JWT object that holds information of a token // JWTToken a JWT object that holds information of a token

View File

@@ -230,7 +230,7 @@ func (kc *KeycloakCredentials) Authenticate() (JWTToken, error) {
} }
// CreateUser creates a new user in keycloak Idp and sends an invite. // CreateUser creates a new user in keycloak Idp and sends an invite.
func (km *KeycloakManager) CreateUser(email string, name string, accountID string) (*UserData, error) { func (km *KeycloakManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
jwtToken, err := km.credentials.Authenticate() jwtToken, err := km.credentials.Authenticate()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -103,7 +103,7 @@ func (oc *OktaCredentials) Authenticate() (JWTToken, error) {
} }
// CreateUser creates a new user in okta Idp and sends an invitation. // CreateUser creates a new user in okta Idp and sends an invitation.
func (om *OktaManager) CreateUser(email string, name string, accountID string) (*UserData, error) { func (om *OktaManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
var ( var (
sendEmail = true sendEmail = true
activate = true activate = true

View File

@@ -234,7 +234,7 @@ func (zc *ZitadelCredentials) Authenticate() (JWTToken, error) {
} }
// CreateUser creates a new user in zitadel Idp and sends an invite. // CreateUser creates a new user in zitadel Idp and sends an invite.
func (zm *ZitadelManager) CreateUser(email string, name string, accountID string) (*UserData, error) { func (zm *ZitadelManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
payload, err := buildZitadelCreateUserRequestPayload(email, name) payload, err := buildZitadelCreateUserRequestPayload(email, name)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -1,6 +1,8 @@
package jwtclaims package jwtclaims
import ( import (
"time"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
) )
@@ -10,6 +12,7 @@ type AuthorizationClaims struct {
AccountId string AccountId string
Domain string Domain string
DomainCategory string DomainCategory string
LastLogin time.Time
Raw jwt.MapClaims Raw jwt.MapClaims
} }

View File

@@ -2,6 +2,7 @@ package jwtclaims
import ( import (
"net/http" "net/http"
"time"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
) )
@@ -17,6 +18,8 @@ const (
DomainCategorySuffix = "wt_account_domain_category" DomainCategorySuffix = "wt_account_domain_category"
// UserIDClaim claim for the user id // UserIDClaim claim for the user id
UserIDClaim = "sub" UserIDClaim = "sub"
// LastLoginSuffix claim for the last login
LastLoginSuffix = "nb_last_login"
) )
// ExtractClaims Extract function type // ExtractClaims Extract function type
@@ -93,9 +96,24 @@ func (c *ClaimsExtractor) FromToken(token *jwt.Token) AuthorizationClaims {
if ok { if ok {
jwtClaims.DomainCategory = domainCategoryClaim.(string) jwtClaims.DomainCategory = domainCategoryClaim.(string)
} }
LastLoginClaimString, ok := claims[c.authAudience+LastLoginSuffix]
if ok {
jwtClaims.LastLogin = parseTime(LastLoginClaimString.(string))
}
return jwtClaims return jwtClaims
} }
func parseTime(timeString string) time.Time {
if timeString == "" {
return time.Time{}
}
parsedTime, err := time.Parse(time.RFC3339, timeString)
if err != nil {
return time.Time{}
}
return parsedTime
}
// fromRequestContext extracts claims from the request context previously filled by the JWT token (after auth) // fromRequestContext extracts claims from the request context previously filled by the JWT token (after auth)
func (c *ClaimsExtractor) fromRequestContext(r *http.Request) AuthorizationClaims { func (c *ClaimsExtractor) fromRequestContext(r *http.Request) AuthorizationClaims {
if r.Context().Value(TokenUserProperty) == nil { if r.Context().Value(TokenUserProperty) == nil {

View File

@@ -4,12 +4,15 @@ import (
"context" "context"
"net/http" "net/http"
"testing" "testing"
"time"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audiance string) *http.Request { func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audiance string) *http.Request {
const layout = "2006-01-02T15:04:05.999Z"
claimMaps := jwt.MapClaims{} claimMaps := jwt.MapClaims{}
if claims.UserId != "" { if claims.UserId != "" {
claimMaps[UserIDClaim] = claims.UserId claimMaps[UserIDClaim] = claims.UserId
@@ -23,6 +26,9 @@ func newTestRequestWithJWT(t *testing.T, claims AuthorizationClaims, audiance st
if claims.DomainCategory != "" { if claims.DomainCategory != "" {
claimMaps[audiance+DomainCategorySuffix] = claims.DomainCategory claimMaps[audiance+DomainCategorySuffix] = claims.DomainCategory
} }
if claims.LastLogin != (time.Time{}) {
claimMaps[audiance+LastLoginSuffix] = claims.LastLogin.Format(layout)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
r, err := http.NewRequest(http.MethodGet, "http://localhost", nil) r, err := http.NewRequest(http.MethodGet, "http://localhost", nil)
require.NoError(t, err, "creating testing request failed") require.NoError(t, err, "creating testing request failed")
@@ -40,6 +46,9 @@ func TestExtractClaimsFromRequestContext(t *testing.T) {
expectedMSG string expectedMSG string
} }
const layout = "2006-01-02T15:04:05.999Z"
lastLogin, _ := time.Parse(layout, "2023-08-17T09:30:40.465Z")
testCase1 := test{ testCase1 := test{
name: "All Claim Fields", name: "All Claim Fields",
inputAudiance: "https://login/", inputAudiance: "https://login/",
@@ -47,11 +56,13 @@ func TestExtractClaimsFromRequestContext(t *testing.T) {
UserId: "test", UserId: "test",
Domain: "test.com", Domain: "test.com",
AccountId: "testAcc", AccountId: "testAcc",
LastLogin: lastLogin,
DomainCategory: "public", DomainCategory: "public",
Raw: jwt.MapClaims{ Raw: jwt.MapClaims{
"https://login/wt_account_domain": "test.com", "https://login/wt_account_domain": "test.com",
"https://login/wt_account_domain_category": "public", "https://login/wt_account_domain_category": "public",
"https://login/wt_account_id": "testAcc", "https://login/wt_account_id": "testAcc",
"https://login/nb_last_login": lastLogin.Format(layout),
"sub": "test", "sub": "test",
}, },
}, },

View File

@@ -1,14 +1,18 @@
package server package server
import ( import (
"errors"
"regexp"
"strconv"
"unicode/utf8"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
nbdns "github.com/netbirdio/netbird/dns" nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/status"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
"strconv"
"unicode/utf8"
) )
const ( const (
@@ -26,6 +30,8 @@ const (
UpdateNameServerGroupPrimary UpdateNameServerGroupPrimary
// UpdateNameServerGroupDomains indicates a nameserver group' domains update operation // UpdateNameServerGroupDomains indicates a nameserver group' domains update operation
UpdateNameServerGroupDomains UpdateNameServerGroupDomains
domainPattern = `^(?i)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$`
) )
// NameServerGroupUpdateOperationType operation type // NameServerGroupUpdateOperationType operation type
@@ -364,9 +370,8 @@ func validateDomainInput(primary bool, domains []string) error {
" you should set either primary or domain") " you should set either primary or domain")
} }
for _, domain := range domains { for _, domain := range domains {
_, valid := dns.IsDomainName(domain) if err := validateDomain(domain); err != nil {
if !valid { return status.Errorf(status.InvalidArgument, "nameserver group got an invalid domain: %s %q", domain, err)
return status.Errorf(status.InvalidArgument, "nameserver group got an invalid domain: %s", domain)
} }
} }
return nil return nil
@@ -417,3 +422,21 @@ func validateGroups(list []string, groups map[string]*Group) error {
return nil return nil
} }
func validateDomain(domain string) error {
domainMatcher := regexp.MustCompile(domainPattern)
if !domainMatcher.MatchString(domain) {
return errors.New("domain should consists of only letters, numbers, and hyphens with no leading, trailing hyphens, or spaces")
}
labels, valid := dns.IsDomainName(domain)
if !valid {
return errors.New("invalid domain name")
}
if labels < 2 {
return errors.New("domain should consists of a minimum of two labels")
}
return nil
}

View File

@@ -1160,3 +1160,69 @@ func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, error
return account, nil return account, nil
} }
func TestValidateDomain(t *testing.T) {
testCases := []struct {
name string
domain string
errFunc require.ErrorAssertionFunc
}{
{
name: "Valid domain name with multiple labels",
domain: "123.example.com",
errFunc: require.NoError,
},
{
name: "Valid domain name with hyphen",
domain: "test-example.com",
errFunc: require.NoError,
},
{
name: "Invalid domain name with double hyphen",
domain: "test--example.com",
errFunc: require.Error,
},
{
name: "Invalid domain name with only one label",
domain: "com",
errFunc: require.Error,
},
{
name: "Invalid domain name with a label exceeding 63 characters",
domain: "dnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdnsdns.com",
errFunc: require.Error,
},
{
name: "Invalid domain name starting with a hyphen",
domain: "-example.com",
errFunc: require.Error,
},
{
name: "Invalid domain name ending with a hyphen",
domain: "example.com-",
errFunc: require.Error,
},
{
name: "Invalid domain with unicode",
domain: "example?,.com",
errFunc: require.Error,
},
{
name: "Invalid domain with space before top-level domain",
domain: "space .example.com",
errFunc: require.Error,
},
{
name: "Invalid domain with trailing space",
domain: "example.com ",
errFunc: require.Error,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
testCase.errFunc(t, validateDomain(testCase.domain))
})
}
}

View File

@@ -108,6 +108,10 @@ func (p *Peer) AddedWithSSOLogin() bool {
// Copy copies Peer object // Copy copies Peer object
func (p *Peer) Copy() *Peer { func (p *Peer) Copy() *Peer {
peerStatus := p.Status
if peerStatus != nil {
peerStatus = p.Status.Copy()
}
return &Peer{ return &Peer{
ID: p.ID, ID: p.ID,
Key: p.Key, Key: p.Key,
@@ -115,11 +119,11 @@ func (p *Peer) Copy() *Peer {
IP: p.IP, IP: p.IP,
Meta: p.Meta, Meta: p.Meta,
Name: p.Name, Name: p.Name,
Status: p.Status, DNSLabel: p.DNSLabel,
Status: peerStatus,
UserID: p.UserID, UserID: p.UserID,
SSHKey: p.SSHKey, SSHKey: p.SSHKey,
SSHEnabled: p.SSHEnabled, SSHEnabled: p.SSHEnabled,
DNSLabel: p.DNSLabel,
LoginExpirationEnabled: p.LoginExpirationEnabled, LoginExpirationEnabled: p.LoginExpirationEnabled,
LastLogin: p.LastLogin, LastLogin: p.LastLogin,
} }
@@ -695,6 +699,8 @@ func (am *DefaultAccountManager) LoginPeer(login PeerLogin) (*Peer, *NetworkMap,
updatePeerLastLogin(peer, account) updatePeerLastLogin(peer, account)
updateRemotePeers = true updateRemotePeers = true
shouldStoreAccount = true shouldStoreAccount = true
am.storeEvent(login.UserID, peer.ID, account.Id, activity.UserLoggedInPeer, peer.EventMeta(am.GetDNSDomain()))
} }
peer, updated := updatePeerMeta(peer, login.Meta, account) peer, updated := updatePeerMeta(peer, login.Meta, account)

View File

@@ -36,6 +36,18 @@ type PersonalAccessToken struct {
LastUsed time.Time LastUsed time.Time
} }
func (t *PersonalAccessToken) Copy() *PersonalAccessToken {
return &PersonalAccessToken{
ID: t.ID,
Name: t.Name,
HashedToken: t.HashedToken,
ExpirationDate: t.ExpirationDate,
CreatedBy: t.CreatedBy,
CreatedAt: t.CreatedAt,
LastUsed: t.LastUsed,
}
}
// PersonalAccessTokenGenerated holds the new PersonalAccessToken and the plain text version of it // PersonalAccessTokenGenerated holds the new PersonalAccessToken and the plain text version of it
type PersonalAccessTokenGenerated struct { type PersonalAccessTokenGenerated struct {
PlainToken string PlainToken string

View File

@@ -95,18 +95,22 @@ type PolicyRule struct {
// Copy returns a copy of a policy rule // Copy returns a copy of a policy rule
func (pm *PolicyRule) Copy() *PolicyRule { func (pm *PolicyRule) Copy() *PolicyRule {
return &PolicyRule{ rule := &PolicyRule{
ID: pm.ID, ID: pm.ID,
Name: pm.Name, Name: pm.Name,
Description: pm.Description, Description: pm.Description,
Enabled: pm.Enabled, Enabled: pm.Enabled,
Action: pm.Action, Action: pm.Action,
Destinations: pm.Destinations[:], Destinations: make([]string, len(pm.Destinations)),
Sources: pm.Sources[:], Sources: make([]string, len(pm.Sources)),
Bidirectional: pm.Bidirectional, Bidirectional: pm.Bidirectional,
Protocol: pm.Protocol, Protocol: pm.Protocol,
Ports: pm.Ports[:], Ports: make([]string, len(pm.Ports)),
} }
copy(rule.Destinations, pm.Destinations)
copy(rule.Sources, pm.Sources)
copy(rule.Ports, pm.Ports)
return rule
} }
// ToRule converts the PolicyRule to a legacy representation of the Rule (for backwards compatibility) // ToRule converts the PolicyRule to a legacy representation of the Rule (for backwards compatibility)
@@ -147,9 +151,10 @@ func (p *Policy) Copy() *Policy {
Name: p.Name, Name: p.Name,
Description: p.Description, Description: p.Description,
Enabled: p.Enabled, Enabled: p.Enabled,
Rules: make([]*PolicyRule, len(p.Rules)),
} }
for _, r := range p.Rules { for i, r := range p.Rules {
c.Rules = append(c.Rules, r.Copy()) c.Rules[i] = r.Copy()
} }
return c return c
} }

View File

@@ -45,15 +45,18 @@ type Rule struct {
} }
func (r *Rule) Copy() *Rule { func (r *Rule) Copy() *Rule {
return &Rule{ rule := &Rule{
ID: r.ID, ID: r.ID,
Name: r.Name, Name: r.Name,
Description: r.Description, Description: r.Description,
Disabled: r.Disabled, Disabled: r.Disabled,
Source: r.Source[:], Source: make([]string, len(r.Source)),
Destination: r.Destination[:], Destination: make([]string, len(r.Destination)),
Flow: r.Flow, Flow: r.Flow,
} }
copy(rule.Source, r.Source)
copy(rule.Destination, r.Destination)
return rule
} }
// EventMeta returns activity event meta related to this rule // EventMeta returns activity event meta related to this rule

View File

@@ -90,8 +90,8 @@ type SetupKey struct {
// Copy copies SetupKey to a new object // Copy copies SetupKey to a new object
func (key *SetupKey) Copy() *SetupKey { func (key *SetupKey) Copy() *SetupKey {
autoGroups := make([]string, 0) autoGroups := make([]string, len(key.AutoGroups))
autoGroups = append(autoGroups, key.AutoGroups...) copy(autoGroups, key.AutoGroups)
if key.UpdatedAt.IsZero() { if key.UpdatedAt.IsZero() {
key.UpdatedAt = key.CreatedAt key.UpdatedAt = key.CreatedAt
} }

View File

@@ -1,5 +1,7 @@
package server package server
import "time"
type Store interface { type Store interface {
GetAllAccounts() []*Account GetAllAccounts() []*Account
GetAccount(accountID string) (*Account, error) GetAccount(accountID string) (*Account, error)
@@ -20,6 +22,7 @@ type Store interface {
// AcquireGlobalLock should attempt to acquire a global lock and return a function that releases the lock // AcquireGlobalLock should attempt to acquire a global lock and return a function that releases the lock
AcquireGlobalLock() func() AcquireGlobalLock() func()
SavePeerStatus(accountID, peerID string, status PeerStatus) error SavePeerStatus(accountID, peerID string, status PeerStatus) error
SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error
// Close should close the store persisting all unsaved data. // Close should close the store persisting all unsaved data.
Close() error Close() error
} }

View File

@@ -1,9 +1,11 @@
package server package server
import ( import (
"github.com/netbirdio/netbird/management/proto"
log "github.com/sirupsen/logrus"
"sync" "sync"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/management/proto"
) )
const channelBufferSize = 100 const channelBufferSize = 100
@@ -33,7 +35,7 @@ func (p *PeersUpdateManager) SendUpdate(peerID string, update *UpdateMessage) er
if channel, ok := p.peerChannels[peerID]; ok { if channel, ok := p.peerChannels[peerID]; ok {
select { select {
case channel <- update: case channel <- update:
log.Infof("update was sent to channel for peer %s", peerID) log.Debugf("update was sent to channel for peer %s", peerID)
default: default:
log.Warnf("channel for peer %s is %d full", peerID, len(channel)) log.Warnf("channel for peer %s is %d full", peerID, len(channel))
} }

View File

@@ -3,6 +3,7 @@ package server
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/google/uuid" "github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -53,6 +54,8 @@ type User struct {
PATs map[string]*PersonalAccessToken PATs map[string]*PersonalAccessToken
// Blocked indicates whether the user is blocked. Blocked users can't use the system. // Blocked indicates whether the user is blocked. Blocked users can't use the system.
Blocked bool Blocked bool
// LastLogin is the last time the user logged in to IdP
LastLogin time.Time
} }
// IsBlocked returns true if the user is blocked, false otherwise // IsBlocked returns true if the user is blocked, false otherwise
@@ -60,6 +63,10 @@ func (u *User) IsBlocked() bool {
return u.Blocked return u.Blocked
} }
func (u *User) LastDashboardLoginChanged(LastLogin time.Time) bool {
return LastLogin.After(u.LastLogin) && !u.LastLogin.IsZero()
}
// IsAdmin returns true if the user is an admin, false otherwise // IsAdmin returns true if the user is an admin, false otherwise
func (u *User) IsAdmin() bool { func (u *User) IsAdmin() bool {
return u.Role == UserRoleAdmin return u.Role == UserRoleAdmin
@@ -82,6 +89,7 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
Status: string(UserStatusActive), Status: string(UserStatusActive),
IsServiceUser: u.IsServiceUser, IsServiceUser: u.IsServiceUser,
IsBlocked: u.Blocked, IsBlocked: u.Blocked,
LastLogin: u.LastLogin,
}, nil }, nil
} }
if userData.ID != u.Id { if userData.ID != u.Id {
@@ -102,6 +110,7 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
Status: string(userStatus), Status: string(userStatus),
IsServiceUser: u.IsServiceUser, IsServiceUser: u.IsServiceUser,
IsBlocked: u.Blocked, IsBlocked: u.Blocked,
LastLogin: u.LastLogin,
}, nil }, nil
} }
@@ -111,9 +120,7 @@ func (u *User) Copy() *User {
copy(autoGroups, u.AutoGroups) copy(autoGroups, u.AutoGroups)
pats := make(map[string]*PersonalAccessToken, len(u.PATs)) pats := make(map[string]*PersonalAccessToken, len(u.PATs))
for k, v := range u.PATs { for k, v := range u.PATs {
patCopy := new(PersonalAccessToken) pats[k] = v.Copy()
*patCopy = *v
pats[k] = patCopy
} }
return &User{ return &User{
Id: u.Id, Id: u.Id,
@@ -123,6 +130,7 @@ func (u *User) Copy() *User {
ServiceUserName: u.ServiceUserName, ServiceUserName: u.ServiceUserName,
PATs: pats, PATs: pats,
Blocked: u.Blocked, Blocked: u.Blocked,
LastLogin: u.LastLogin,
} }
} }
@@ -186,6 +194,7 @@ func (am *DefaultAccountManager) createServiceUser(accountID string, initiatorUs
AutoGroups: newUser.AutoGroups, AutoGroups: newUser.AutoGroups,
Status: string(UserStatusActive), Status: string(UserStatusActive),
IsServiceUser: true, IsServiceUser: true,
LastLogin: time.Time{},
}, nil }, nil
} }
@@ -215,6 +224,12 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite
return nil, status.Errorf(status.NotFound, "account %s doesn't exist", accountID) return nil, status.Errorf(status.NotFound, "account %s doesn't exist", accountID)
} }
// initiator is the one who is inviting the new user
initiatorUser, err := am.lookupUserInCache(userID, account)
if err != nil {
return nil, status.Errorf(status.NotFound, "user %s doesn't exist in IdP", userID)
}
// check if the user is already registered with this email => reject // check if the user is already registered with this email => reject
user, err := am.lookupUserInCacheByEmail(invite.Email, accountID) user, err := am.lookupUserInCacheByEmail(invite.Email, accountID)
if err != nil { if err != nil {
@@ -234,7 +249,7 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite
return nil, status.Errorf(status.UserAlreadyExists, "can't invite a user with an existing NetBird account") return nil, status.Errorf(status.UserAlreadyExists, "can't invite a user with an existing NetBird account")
} }
idpUser, err := am.idpManager.CreateUser(invite.Email, invite.Name, accountID) idpUser, err := am.idpManager.CreateUser(invite.Email, invite.Name, accountID, initiatorUser.Email)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -260,7 +275,6 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite
am.storeEvent(userID, newUser.Id, accountID, activity.UserInvited, nil) am.storeEvent(userID, newUser.Id, accountID, activity.UserInvited, nil)
return newUser.ToUserInfo(idpUser) return newUser.ToUserInfo(idpUser)
} }
// GetUser looks up a user by provided authorization claims. // GetUser looks up a user by provided authorization claims.
@@ -275,6 +289,21 @@ func (am *DefaultAccountManager) GetUser(claims jwtclaims.AuthorizationClaims) (
if !ok { if !ok {
return nil, status.Errorf(status.NotFound, "user not found") return nil, status.Errorf(status.NotFound, "user not found")
} }
// this code should be outside of the am.GetAccountFromToken(claims) because this method is called also by the gRPC
// server when user authenticates a device. And we need to separate the Dashboard login event from the Device login event.
unlock := am.Store.AcquireAccountLock(account.Id)
newLogin := user.LastDashboardLoginChanged(claims.LastLogin)
err = am.Store.SaveUserLastLogin(account.Id, claims.UserId, claims.LastLogin)
unlock()
if newLogin {
meta := map[string]any{"timestamp": claims.LastLogin}
am.storeEvent(claims.UserId, claims.UserId, account.Id, activity.DashboardLogin, meta)
if err != nil {
log.Errorf("failed saving user last login: %v", err)
}
}
return user, nil return user, nil
} }
@@ -600,10 +629,26 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd
} }
} }
if update.AutoGroups != nil && account.Settings.GroupsPropagationEnabled {
removedGroups := difference(oldUser.AutoGroups, update.AutoGroups)
// need force update all auto groups in any case they will not be dublicated
account.UserGroupsAddToPeers(oldUser.Id, update.AutoGroups...)
account.UserGroupsRemoveFromPeers(oldUser.Id, removedGroups...)
account.Network.IncSerial()
if err = am.Store.SaveAccount(account); err != nil { if err = am.Store.SaveAccount(account); err != nil {
return nil, err return nil, err
} }
if err := am.updateAccountPeers(account); err != nil {
log.Errorf("failed updating account peers while updating user %s", accountID)
}
} else {
if err = am.Store.SaveAccount(account); err != nil {
return nil, err
}
}
defer func() { defer func() {
if oldUser.IsBlocked() != update.IsBlocked() { if oldUser.IsBlocked() != update.IsBlocked() {
if update.IsBlocked() { if update.IsBlocked() {
@@ -629,7 +674,6 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd
} else { } else {
log.Errorf("group %s not found while saving user activity event of account %s", g, account.Id) log.Errorf("group %s not found while saving user activity event of account %s", g, account.Id)
} }
} }
for _, g := range addedGroups { for _, g := range addedGroups {
@@ -640,7 +684,6 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd
} }
} }
} }
}() }()
if !isNil(am.idpManager) && !newUser.IsServiceUser { if !isNil(am.idpManager) && !newUser.IsServiceUser {

View File

@@ -267,6 +267,7 @@ func TestUser_Copy(t *testing.T) {
}, },
}, },
Blocked: false, Blocked: false,
LastLogin: time.Now(),
} }
err := validateStruct(user) err := validateStruct(user)

View File

@@ -30,7 +30,15 @@ download_release_binary() {
BINARY_BASE_NAME="${VERSION#v}_${OS_TYPE}_${ARCH}_signed.zip" BINARY_BASE_NAME="${VERSION#v}_${OS_TYPE}_${ARCH}_signed.zip"
fi fi
if [ "$1" = "$UI_APP" ]; then
BINARY_NAME="$1-${OS_TYPE}_${BINARY_BASE_NAME}"
if [ "$OS_TYPE" = "darwin" ]; then
BINARY_NAME="$1_${BINARY_BASE_NAME}" BINARY_NAME="$1_${BINARY_BASE_NAME}"
fi
else
BINARY_NAME="$1_${BINARY_BASE_NAME}"
fi
DOWNLOAD_URL="${BASE_URL}/${VERSION}/${BINARY_NAME}" DOWNLOAD_URL="${BASE_URL}/${VERSION}/${BINARY_NAME}"
echo "Installing $1 from $DOWNLOAD_URL" echo "Installing $1 from $DOWNLOAD_URL"
@@ -128,6 +136,14 @@ install_native_binaries() {
fi fi
} }
check_use_bin_variable() {
if [ "${USE_BIN_INSTALL}-x" = "true-x" ]; then
echo "The installation will be performed using binary files"
return 0
fi
return 1
}
install_netbird() { install_netbird() {
# Check if netbird CLI is installed # Check if netbird CLI is installed
if [ -x "$(command -v netbird)" ]; then if [ -x "$(command -v netbird)" ]; then
@@ -170,8 +186,10 @@ install_netbird() {
echo "Netbird UI installation will be omitted as Linux does not run desktop environment" echo "Netbird UI installation will be omitted as Linux does not run desktop environment"
fi fi
# Check the availability of a compactible package manager # Check the availability of a compatible package manager
if [ -x "$(command -v apt)" ]; then if check_use_bin_variable; then
PACKAGE_MANAGER="bin"
elif [ -x "$(command -v apt)" ]; then
PACKAGE_MANAGER="apt" PACKAGE_MANAGER="apt"
echo "The installation will be performed using apt package manager" echo "The installation will be performed using apt package manager"
elif [ -x "$(command -v dnf)" ]; then elif [ -x "$(command -v dnf)" ]; then
@@ -191,7 +209,9 @@ install_netbird() {
INSTALL_DIR="/usr/local/bin" INSTALL_DIR="/usr/local/bin"
# Check the availability of a compatible package manager # Check the availability of a compatible package manager
if [ -x "$(command -v brew)" ]; then if check_use_bin_variable; then
PACKAGE_MANAGER="bin"
elif [ -x "$(command -v brew)" ]; then
PACKAGE_MANAGER="brew" PACKAGE_MANAGER="brew"
echo "The installation will be performed using brew package manager" echo "The installation will be performed using brew package manager"
fi fi

View File

@@ -1,8 +1,9 @@
package route package route
import ( import (
"github.com/netbirdio/netbird/management/server/status"
"net/netip" "net/netip"
"github.com/netbirdio/netbird/management/server/status"
) )
// Windows has some limitation regarding metric size that differ from Unix-like systems. // Windows has some limitation regarding metric size that differ from Unix-like systems.
@@ -83,7 +84,7 @@ func (r *Route) EventMeta() map[string]any {
// Copy copies a route object // Copy copies a route object
func (r *Route) Copy() *Route { func (r *Route) Copy() *Route {
return &Route{ route := &Route{
ID: r.ID, ID: r.ID,
Description: r.Description, Description: r.Description,
NetID: r.NetID, NetID: r.NetID,
@@ -93,8 +94,10 @@ func (r *Route) Copy() *Route {
Metric: r.Metric, Metric: r.Metric,
Masquerade: r.Masquerade, Masquerade: r.Masquerade,
Enabled: r.Enabled, Enabled: r.Enabled,
Groups: r.Groups, Groups: make([]string, len(r.Groups)),
} }
copy(route.Groups, r.Groups)
return route
} }
// IsEqual compares one route with the other // IsEqual compares one route with the other