Compare commits

...

28 Commits

Author SHA1 Message Date
Maycon Santos
a9452e7b32 avoid running rule operation if client is nil 2023-08-09 21:37:44 +02:00
Zoltan Papp
950d2efed7 Update iptables lib to v0.7.0
In Docker can not determine well the path of
executable file
2023-08-09 12:10:18 +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
Bethuel Mmbaga
64f6343fcc Add html screen for pkce flow (#1034)
* add html screen for pkce flow

* remove unused CSS classes in pkce-auth-msg.html

* remove links to external sources
2023-07-28 18:10:12 +02:00
Maycon Santos
24713fbe59 Move ebpf code to its own package to avoid crash issues in Android (#1033)
* Move ebpf code to its own package to avoid crash issues in Android

Older versions of android crashes because of the bytecode files
Even when they aren't loaded as it was our case

* move c file to own folder

* fix lint
2023-07-27 15:34:27 +02:00
Bethuel Mmbaga
7794b744f8 Add PKCE authorization flow (#1012)
Enhance the user experience by enabling authentication to Netbird using Single Sign-On (SSO) with any Identity Provider (IDP) provider. Current client offers this capability through the Device Authorization Flow, however, is not widely supported by many IDPs, and even some that do support it do not provide a complete verification URL.

To address these challenges, this pull request enable Authorization Code Flow with Proof Key for Code Exchange (PKCE) for client logins, which is a more widely adopted and secure approach to facilitate SSO with various IDP providers.
2023-07-27 11:31:07 +02:00
Maycon Santos
0d0c30c16d Avoid compiling linux NewFactory for Android (#1032) 2023-07-26 16:21:04 +02:00
Zoltan Papp
b0364da67c Wg ebpf proxy (#911)
EBPF proxy between TURN (relay) and WireGuard to reduce number of used ports used by the NetBird agent.
- Separate the wg configuration from the proxy logic
- In case if eBPF type proxy has only one single proxy instance
- In case if the eBPF is not supported fallback to the original proxy Implementation

Between the signature of eBPF type proxy and original proxy has 
differences so this is why the factory structure exists
2023-07-26 14:00:47 +02:00
Givi Khojanashvili
6dee89379b Feat optimize acl performance iptables (#1025)
* use ipset for iptables

* Update unit-tests for iptables

* Remove debug code

* Update dependencies

* Create separate sets for dPort and sPort rules

* Fix iptables tests

* Fix 0.0.0.0 processing in iptables with ipset
2023-07-24 13:00:23 +02:00
Maycon Santos
76db4f801a Record idp manager type (#1027)
This allows to define priority on support different managers
2023-07-22 19:30:59 +02:00
Zoltan Papp
6c2ed4b4f2 Add default forward rule (#1021)
* Add default forward rule

* Fix

* Add multiple forward rules

* Fix delete rule error handling
2023-07-22 18:39:23 +02:00
Maycon Santos
2541c78dd0 Use error level for JWT parsing error logs (#1026) 2023-07-22 17:56:27 +02:00
Yury Gargay
97b6e79809 Fix DefaultAccountManager GetGroupsFromTheToken false positive tests (#1019)
This fixes the test logic creates copy of account with empty id and
re-pointing the indices to it.

Also, adds additional check for empty ID in SaveAccount method of FileStore.
2023-07-22 15:54:08 +04:00
Givi Khojanashvili
6ad3847615 Fix nfset not binds to the rule (#1024) 2023-07-21 17:45:58 +02:00
86 changed files with 4219 additions and 1241 deletions

View File

@@ -7,6 +7,16 @@ on:
branches:
- main
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:
SIGN_PIPE_VER: "v0.0.8"

View File

@@ -1,10 +1,13 @@
name: Test Docker Compose Linux
name: Test Infrastructure files
on:
push:
branches:
- main
pull_request:
paths:
- 'infrastructure_files/**'
- '.github/workflows/test-infrastructure-files.yml'
concurrency:
@@ -12,7 +15,7 @@ concurrency:
cancel-in-progress: true
jobs:
test:
test-docker-compose:
runs-on: ubuntu-latest
steps:
- name: Install jq
@@ -35,7 +38,7 @@ jobs:
${{ runner.os }}-go-
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: cp setup.env
run: cp infrastructure_files/tests/setup.env infrastructure_files/
@@ -69,6 +72,7 @@ jobs:
CI_NETBIRD_AUTH_JWT_CERTS: https://example.eu.auth0.com/.well-known/jwks.json
CI_NETBIRD_AUTH_TOKEN_ENDPOINT: https://example.eu.auth0.com/oauth/token
CI_NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT: https://example.eu.auth0.com/oauth/device/code
CI_NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT: https://example.eu.auth0.com/authorize
CI_NETBIRD_AUTH_REDIRECT_URI: "/peers"
CI_NETBIRD_TOKEN_SOURCE: "idToken"
CI_NETBIRD_AUTH_USER_ID_CLAIM: "email"
@@ -91,8 +95,8 @@ jobs:
grep LETSENCRYPT_DOMAIN docker-compose.yml | egrep 'LETSENCRYPT_DOMAIN=$'
grep NETBIRD_TOKEN_SOURCE docker-compose.yml | grep $CI_NETBIRD_TOKEN_SOURCE
grep AuthUserIDClaim management.json | grep $CI_NETBIRD_AUTH_USER_ID_CLAIM
grep -A 1 ProviderConfig management.json | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE
grep Scope management.json | grep "$CI_NETBIRD_AUTH_DEVICE_AUTH_SCOPE"
grep -A 3 DeviceAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE
grep -A 8 DeviceAuthorizationFlow management.json | grep -A 6 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_DEVICE_AUTH_SCOPE"
grep UseIDToken management.json | grep false
grep -A 1 IdpManagerConfig management.json | grep ManagerType | grep $CI_NETBIRD_MGMT_IDP
grep -A 3 IdpManagerConfig management.json | grep -A 1 ClientConfig | grep Issuer | grep $CI_NETBIRD_AUTH_AUTHORITY
@@ -100,6 +104,12 @@ jobs:
grep -A 5 IdpManagerConfig management.json | grep -A 3 ClientConfig | grep ClientID | grep $CI_NETBIRD_IDP_MGMT_CLIENT_ID
grep -A 6 IdpManagerConfig management.json | grep -A 4 ClientConfig | grep ClientSecret | grep $CI_NETBIRD_IDP_MGMT_CLIENT_SECRET
grep -A 7 IdpManagerConfig management.json | grep -A 5 ClientConfig | grep GrantType | grep client_credentials
grep -A 2 PKCEAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_AUDIENCE
grep -A 3 PKCEAuthorizationFlow management.json | grep -A 2 ProviderConfig | grep ClientID | grep $CI_NETBIRD_AUTH_CLIENT_ID
grep -A 4 PKCEAuthorizationFlow management.json | grep -A 3 ProviderConfig | grep ClientSecret | grep $CI_NETBIRD_AUTH_CLIENT_SECRET
grep -A 5 PKCEAuthorizationFlow management.json | grep -A 4 ProviderConfig | grep AuthorizationEndpoint | grep $CI_NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT
grep -A 6 PKCEAuthorizationFlow management.json | grep -A 5 ProviderConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT
grep -A 7 PKCEAuthorizationFlow management.json | grep -A 6 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
- name: run docker compose up
working-directory: infrastructure_files
@@ -114,3 +124,28 @@ jobs:
count=$(docker compose ps --format json | jq '.[] | select(.Project | contains("infrastructure_files")) | .State' | grep -c running)
test $count -eq 4
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 }}
username: dev@wiretrustee.com
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

@@ -36,46 +36,61 @@
<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 isolates every machine and device by applying granular access policies, while allowing you to manage them intuitively from a single place.
**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.
| Connectivity | Management | Automation | Platforms |
|-------------------------------------------------------------------|--------------------------------------------------------------------------|----------------------------------------------------------------------------|---------------------------------------|
| <ul><li> - \[x] Kernel WireGuard </ul></li> | <ul><li> - \[x] [Admin Web UI](https://github.com/netbirdio/dashboard) </ul></li> | <ul><li> - \[x] [Public API](https://docs.netbird.io/api) </ul></li> | <ul><li> - \[x] Linux </ul></li> |
| <ul><li> - \[x] Peer-to-peer connections </ul></li> | <ul><li> - \[x] Auto peer discovery and configuration </ul></li> | <ul><li> - \[x] [Setup keys for bulk network provisioning](https://docs.netbird.io/how-to/register-machines-using-setup-keys) </ul></li> | <ul><li> - \[x] Mac </ul></li> |
| <ul><li> - \[x] Peer-to-peer encryption </ul></li> | <ul><li> - \[x] [IdP integrations](https://docs.netbird.io/selfhosted/identity-providers) </ul></li> | <ul><li> - \[x] [Self-hosting quickstart script](https://docs.netbird.io/selfhosted/selfhosted-quickstart) </ul></li> | <ul><li> - \[x] Windows </ul></li> |
| <ul><li> - \[x] Connection relay fallback </ul></li> | <ul><li> - \[x] [SSO & MFA support](https://docs.netbird.io/how-to/installation#running-net-bird-with-sso-login) </ul></li> | <ul><li> - \[x] IdP groups sync with JWT </ul></li> | <ul><li> - \[x] Android </ul></li> |
| <ul><li> - \[x] [Routes to external networks](https://docs.netbird.io/how-to/routing-traffic-to-private-networks) </ul></li> | <ul><li> - \[x] [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> | | |
### Secure peer-to-peer VPN with SSO and MFA in minutes
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.
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
### Quickstart with NetBird Cloud
### Start using NetBird
- Hosted version: [https://app.netbird.io/](https://app.netbird.io/).
- See our documentation for [Quickstart Guide](https://docs.netbird.io/how-to/getting-started).
- If you are looking to self-host NetBird, check our [Self-Hosting Guide](https://docs.netbird.io/selfhosted/selfhosted-guide).
- Step-by-step [Installation Guide](https://docs.netbird.io/how-to/getting-started#installation) for different platforms.
- Web UI [repository](https://github.com/netbirdio/dashboard).
- 5 min [demo video](https://youtu.be/Tu9tPsUWaY0) on YouTube.
- 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
- Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
@@ -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.
<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>
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
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird)
- [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
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)
### 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
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.

View File

@@ -6,15 +6,14 @@ import (
"time"
"github.com/cenkalti/backoff/v4"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/cmd"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/auth"
"github.com/netbirdio/netbird/client/system"
)
// SSOListener is async listener for mobile framework
@@ -87,9 +86,15 @@ func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
err := a.withBackOff(a.ctx, func() (err error) {
_, err = internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.NotFound {
supportsSSO = false
err = nil
_, err = internal.GetPKCEAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.NotFound {
supportsSSO = false
err = nil
}
return err
}
return err
})
@@ -183,27 +188,15 @@ func (a *Auth) login(urlOpener URLOpener) error {
return nil
}
func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*internal.TokenInfo, error) {
providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*auth.TokenInfo, error) {
oAuthFlow, err := auth.NewOAuthFlow(a.ctx, a.config)
if err != nil {
s, ok := gstatus.FromError(err)
if ok && s.Code() == codes.NotFound {
return nil, fmt.Errorf("no SSO provider returned from management. " +
"If you are using hosting Netbird see documentation at " +
"https://github.com/netbirdio/netbird/tree/main/management for details")
} else if ok && s.Code() == codes.Unimplemented {
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
"please update your servver or use Setup Keys to login", a.config.ManagementURL)
} else {
return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err)
}
return nil, err
}
hostedClient := internal.NewHostedDeviceFlow(providerConfig.ProviderConfig)
flowInfo, err := hostedClient.RequestDeviceCode(context.TODO())
flowInfo, err := oAuthFlow.RequestAuthInfo(context.TODO())
if err != nil {
return nil, fmt.Errorf("getting a request device code failed: %v", err)
return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err)
}
go urlOpener.Open(flowInfo.VerificationURIComplete)
@@ -211,7 +204,7 @@ func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*internal.TokenInfo,
waitTimeout := time.Duration(flowInfo.ExpiresIn)
waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout*time.Second)
defer cancel()
tokenInfo, err := hostedClient.WaitToken(waitCTX, flowInfo)
tokenInfo, err := oAuthFlow.WaitToken(waitCTX, flowInfo)
if err != nil {
return nil, fmt.Errorf("waiting for browser login failed: %v", err)
}

View File

@@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"github.com/netbirdio/netbird/client/internal/auth"
"strings"
"time"
@@ -163,31 +164,15 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.C
return nil
}
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*internal.TokenInfo, error) {
providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) {
oAuthFlow, err := auth.NewOAuthFlow(ctx, config)
if err != nil {
s, ok := gstatus.FromError(err)
if ok && s.Code() == codes.NotFound {
return nil, fmt.Errorf("no SSO provider returned from management. " +
"If you are using hosting Netbird see documentation at " +
"https://github.com/netbirdio/netbird/tree/main/management for details")
} else if ok && s.Code() == codes.Unimplemented {
mgmtURL := managementURL
if mgmtURL == "" {
mgmtURL = internal.DefaultManagementURL
}
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
"please update your servver or use Setup Keys to login", mgmtURL)
} else {
return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err)
}
return nil, err
}
hostedClient := internal.NewHostedDeviceFlow(providerConfig.ProviderConfig)
flowInfo, err := hostedClient.RequestDeviceCode(context.TODO())
flowInfo, err := oAuthFlow.RequestAuthInfo(context.TODO())
if err != nil {
return nil, fmt.Errorf("getting a request device code failed: %v", err)
return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err)
}
openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode)
@@ -196,7 +181,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout*time.Second)
defer c()
tokenInfo, err := hostedClient.WaitToken(waitCTX, flowInfo)
tokenInfo, err := oAuthFlow.WaitToken(waitCTX, flowInfo)
if err != nil {
return nil, fmt.Errorf("waiting for browser login failed: %v", err)
}
@@ -206,8 +191,10 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string) {
var codeMsg string
if !strings.Contains(verificationURIComplete, userCode) {
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
if userCode != "" {
if !strings.Contains(verificationURIComplete, userCode) {
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
}
}
err := open.Run(verificationURIComplete)

View File

@@ -8,6 +8,7 @@ import (
"github.com/coreos/go-iptables/iptables"
"github.com/google/uuid"
"github.com/nadoo/ipset"
log "github.com/sirupsen/logrus"
fw "github.com/netbirdio/netbird/client/firewall"
@@ -35,6 +36,8 @@ type Manager struct {
inputDefaultRuleSpecs []string
outputDefaultRuleSpecs []string
wgIface iFaceMapper
rulesets map[string]ruleset
}
// iFaceMapper defines subset methods of interface required for manager
@@ -43,6 +46,11 @@ type iFaceMapper interface {
Address() iface.WGAddress
}
type ruleset struct {
rule *Rule
ips map[string]string
}
// Create iptables firewall manager
func Create(wgIface iFaceMapper) (*Manager, error) {
m := &Manager{
@@ -51,6 +59,11 @@ func Create(wgIface iFaceMapper) (*Manager, error) {
"-i", wgIface.Name(), "-j", ChainInputFilterName, "-s", wgIface.Address().String()},
outputDefaultRuleSpecs: []string{
"-o", wgIface.Name(), "-j", ChainOutputFilterName, "-d", wgIface.Address().String()},
rulesets: make(map[string]ruleset),
}
if err := ipset.Init(); err != nil {
return nil, fmt.Errorf("init ipset: %w", err)
}
// init clients for booth ipv4 and ipv6
@@ -111,22 +124,45 @@ func (m *Manager) AddFiltering(
if sPort != nil && sPort.Values != nil {
sPortVal = strconv.Itoa(sPort.Values[0])
}
ipsetName = m.transformIPsetName(ipsetName, sPortVal, dPortVal)
ruleID := uuid.New().String()
if comment == "" {
comment = ruleID
}
specs := m.filterRuleSpecs(
"filter",
ip,
string(protocol),
sPortVal,
dPortVal,
direction,
action,
comment,
)
if ipsetName != "" {
rs, rsExists := m.rulesets[ipsetName]
if !rsExists {
if err := ipset.Flush(ipsetName); err != nil {
log.Errorf("flush ipset %q before use it: %v", ipsetName, err)
}
if err := ipset.Create(ipsetName); err != nil {
return nil, fmt.Errorf("failed to create ipset: %w", err)
}
}
if err := ipset.Add(ipsetName, ip.String()); err != nil {
return nil, fmt.Errorf("failed to add IP to ipset: %w", err)
}
if rsExists {
// if ruleset already exists it means we already have the firewall rule
// so we need to update IPs in the ruleset and return new fw.Rule object for ACL manager.
rs.ips[ip.String()] = ruleID
return &Rule{
ruleID: ruleID,
ipsetName: ipsetName,
ip: ip.String(),
dst: direction == fw.RuleDirectionOUT,
v6: ip.To4() == nil,
}, nil
}
// this is new ipset so we need to create firewall rule for it
}
specs := m.filterRuleSpecs("filter", ip, string(protocol), sPortVal, dPortVal,
direction, action, comment, ipsetName)
if direction == fw.RuleDirectionOUT {
ok, err := client.Exists("filter", ChainOutputFilterName, specs...)
@@ -154,12 +190,24 @@ func (m *Manager) AddFiltering(
}
}
return &Rule{
id: ruleID,
specs: specs,
dst: direction == fw.RuleDirectionOUT,
v6: ip.To4() == nil,
}, nil
rule := &Rule{
ruleID: ruleID,
specs: specs,
ipsetName: ipsetName,
ip: ip.String(),
dst: direction == fw.RuleDirectionOUT,
v6: ip.To4() == nil,
}
if ipsetName != "" {
// ipset name is defined and it means that this rule was created
// for it, need to assosiate it with ruleset
m.rulesets[ipsetName] = ruleset{
rule: rule,
ips: map[string]string{rule.ip: ruleID},
}
}
return rule, nil
}
// DeleteRule from the firewall by rule definition
@@ -180,6 +228,31 @@ func (m *Manager) DeleteRule(rule fw.Rule) error {
client = m.ipv6Client
}
if rs, ok := m.rulesets[r.ipsetName]; ok {
// delete IP from ruleset IPs list and ipset
if _, ok := rs.ips[r.ip]; ok {
if err := ipset.Del(r.ipsetName, r.ip); err != nil {
return fmt.Errorf("failed to delete ip from ipset: %w", err)
}
delete(rs.ips, r.ip)
}
// if after delete, set still contains other IPs,
// no need to delete firewall rule and we should exit here
if len(rs.ips) != 0 {
return nil
}
// we delete last IP from the set, that means we need to delete
// set itself and assosiated firewall rule too
delete(m.rulesets, r.ipsetName)
if err := ipset.Destroy(r.ipsetName); err != nil {
log.Errorf("delete empty ipset: %v", err)
}
r = rs.rule
}
if r.dst {
return client.Delete("filter", ChainOutputFilterName, r.specs...)
}
@@ -246,6 +319,16 @@ func (m *Manager) reset(client *iptables.IPTables, table string) error {
return nil
}
for ipsetName := range m.rulesets {
if err := ipset.Flush(ipsetName); err != nil {
log.Errorf("flush ipset %q during reset: %v", ipsetName, err)
}
if err := ipset.Destroy(ipsetName); err != nil {
log.Errorf("delete ipset %q during reset: %v", ipsetName, err)
}
delete(m.rulesets, ipsetName)
}
return nil
}
@@ -253,6 +336,7 @@ func (m *Manager) reset(client *iptables.IPTables, table string) error {
func (m *Manager) filterRuleSpecs(
table string, ip net.IP, protocol string, sPort, dPort string,
direction fw.RuleDirection, action fw.Action, comment string,
ipsetName string,
) (specs []string) {
matchByIP := true
// don't use IP matching if IP is ip 0.0.0.0
@@ -262,11 +346,19 @@ func (m *Manager) filterRuleSpecs(
switch direction {
case fw.RuleDirectionIN:
if matchByIP {
specs = append(specs, "-s", ip.String())
if ipsetName != "" {
specs = append(specs, "-m", "set", "--set", ipsetName, "src")
} else {
specs = append(specs, "-s", ip.String())
}
}
case fw.RuleDirectionOUT:
if matchByIP {
specs = append(specs, "-d", ip.String())
if ipsetName != "" {
specs = append(specs, "-m", "set", "--set", ipsetName, "dst")
} else {
specs = append(specs, "-d", ip.String())
}
}
}
if protocol != "all" {
@@ -348,3 +440,16 @@ func (m *Manager) actionToStr(action fw.Action) string {
}
return "DROP"
}
func (m *Manager) transformIPsetName(ipsetName string, sPort, dPort string) string {
if ipsetName == "" {
return ""
} else if sPort != "" && dPort != "" {
return ipsetName + "-sport-dport"
} else if sPort != "" {
return ipsetName + "-sport"
} else if dPort != "" {
return ipsetName + "-dport"
}
return ipsetName
}

View File

@@ -55,12 +55,13 @@ func TestIptablesManager(t *testing.T) {
// just check on the local interface
manager, err := Create(mock)
require.NoError(t, err)
time.Sleep(time.Second)
defer func() {
if err := manager.Reset(); err != nil {
t.Errorf("clear the manager state: %v", err)
}
err := manager.Reset()
require.NoError(t, err, "clear the manager state")
time.Sleep(time.Second)
}()
@@ -88,19 +89,17 @@ func TestIptablesManager(t *testing.T) {
})
t.Run("delete first rule", func(t *testing.T) {
if err := manager.DeleteRule(rule1); err != nil {
require.NoError(t, err, "failed to delete rule")
}
err := manager.DeleteRule(rule1)
require.NoError(t, err, "failed to delete rule")
checkRuleSpecs(t, ipv4Client, ChainOutputFilterName, false, rule1.(*Rule).specs...)
})
t.Run("delete second rule", func(t *testing.T) {
if err := manager.DeleteRule(rule2); err != nil {
require.NoError(t, err, "failed to delete rule")
}
err := manager.DeleteRule(rule2)
require.NoError(t, err, "failed to delete rule")
checkRuleSpecs(t, ipv4Client, ChainInputFilterName, false, rule2.(*Rule).specs...)
require.Empty(t, manager.rulesets, "rulesets index after removed second rule must be empty")
})
t.Run("reset check", func(t *testing.T) {
@@ -122,6 +121,88 @@ func TestIptablesManager(t *testing.T) {
})
}
func TestIptablesManagerIPSet(t *testing.T) {
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
require.NoError(t, err)
mock := &iFaceMock{
NameFunc: func() string {
return "lo"
},
AddressFunc: func() iface.WGAddress {
return iface.WGAddress{
IP: net.ParseIP("10.20.0.1"),
Network: &net.IPNet{
IP: net.ParseIP("10.20.0.0"),
Mask: net.IPv4Mask(255, 255, 255, 0),
},
}
},
}
// just check on the local interface
manager, err := Create(mock)
require.NoError(t, err)
time.Sleep(time.Second)
defer func() {
err := manager.Reset()
require.NoError(t, err, "clear the manager state")
time.Sleep(time.Second)
}()
var rule1 fw.Rule
t.Run("add first rule with set", func(t *testing.T) {
ip := net.ParseIP("10.20.0.2")
port := &fw.Port{Values: []int{8080}}
rule1, err = manager.AddFiltering(
ip, "tcp", nil, port, fw.RuleDirectionOUT,
fw.ActionAccept, "default", "accept HTTP traffic",
)
require.NoError(t, err, "failed to add rule")
checkRuleSpecs(t, ipv4Client, ChainOutputFilterName, true, rule1.(*Rule).specs...)
require.Equal(t, rule1.(*Rule).ipsetName, "default-dport", "ipset name must be set")
require.Equal(t, rule1.(*Rule).ip, "10.20.0.2", "ipset IP must be set")
})
var rule2 fw.Rule
t.Run("add second rule", func(t *testing.T) {
ip := net.ParseIP("10.20.0.3")
port := &fw.Port{
Values: []int{443},
}
rule2, err = manager.AddFiltering(
ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept,
"default", "accept HTTPS traffic from ports range",
)
require.NoError(t, err, "failed to add rule")
require.Equal(t, rule2.(*Rule).ipsetName, "default-sport", "ipset name must be set")
require.Equal(t, rule2.(*Rule).ip, "10.20.0.3", "ipset IP must be set")
})
t.Run("delete first rule", func(t *testing.T) {
err := manager.DeleteRule(rule1)
require.NoError(t, err, "failed to delete rule")
require.NotContains(t, manager.rulesets, rule1.(*Rule).ruleID, "rule must be removed form the ruleset index")
})
t.Run("delete second rule", func(t *testing.T) {
err := manager.DeleteRule(rule2)
require.NoError(t, err, "failed to delete rule")
require.Empty(t, manager.rulesets, "rulesets index after removed second rule must be empty")
})
t.Run("reset check", func(t *testing.T) {
err = manager.Reset()
require.NoError(t, err, "failed to reset")
})
}
func checkRuleSpecs(t *testing.T, ipv4Client *iptables.IPTables, chainName string, mustExists bool, rulespec ...string) {
exists, err := ipv4Client.Exists("filter", chainName, rulespec...)
require.NoError(t, err, "failed to check rule")
@@ -153,9 +234,9 @@ func TestIptablesCreatePerformance(t *testing.T) {
time.Sleep(time.Second)
defer func() {
if err := manager.Reset(); err != nil {
t.Errorf("clear the manager state: %v", err)
}
err := manager.Reset()
require.NoError(t, err, "clear the manager state")
time.Sleep(time.Second)
}()

View File

@@ -2,13 +2,16 @@ package iptables
// Rule to handle management of rules
type Rule struct {
id string
ruleID string
ipsetName string
specs []string
ip string
dst bool
v6 bool
}
// GetRuleID returns the rule id
func (r *Rule) GetRuleID() string {
return r.id
return r.ruleID
}

View File

@@ -146,7 +146,7 @@ func (m *Manager) AddFiltering(
// with fresh created set and set element
var isSetNew bool
ipset, err := m.rConn.GetSetByName(table, ipsetName)
ipset, err = m.rConn.GetSetByName(table, ipsetName)
if err != nil {
if ipset, err = m.createSet(table, rawIP, ipsetName); err != nil {
return nil, fmt.Errorf("get set name: %v", err)

View File

@@ -0,0 +1,202 @@
package auth
import (
"context"
"encoding/json"
"fmt"
"github.com/netbirdio/netbird/client/internal"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// HostedGrantType grant type for device flow on Hosted
const (
HostedGrantType = "urn:ietf:params:oauth:grant-type:device_code"
)
var _ OAuthFlow = &DeviceAuthorizationFlow{}
// DeviceAuthorizationFlow implements the OAuthFlow interface,
// for the Device Authorization Flow.
type DeviceAuthorizationFlow struct {
providerConfig internal.DeviceAuthProviderConfig
HTTPClient HTTPClient
}
// RequestDeviceCodePayload used for request device code payload for auth0
type RequestDeviceCodePayload struct {
Audience string `json:"audience"`
ClientID string `json:"client_id"`
Scope string `json:"scope"`
}
// TokenRequestPayload used for requesting the auth0 token
type TokenRequestPayload struct {
GrantType string `json:"grant_type"`
DeviceCode string `json:"device_code,omitempty"`
ClientID string `json:"client_id"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// TokenRequestResponse used for parsing Hosted token's response
type TokenRequestResponse struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
TokenInfo
}
// NewDeviceAuthorizationFlow returns device authorization flow client
func NewDeviceAuthorizationFlow(config internal.DeviceAuthProviderConfig) (*DeviceAuthorizationFlow, error) {
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
httpTransport.MaxIdleConns = 5
httpClient := &http.Client{
Timeout: 10 * time.Second,
Transport: httpTransport,
}
return &DeviceAuthorizationFlow{
providerConfig: config,
HTTPClient: httpClient,
}, nil
}
// GetClientID returns the provider client id
func (d *DeviceAuthorizationFlow) GetClientID(ctx context.Context) string {
return d.providerConfig.ClientID
}
// RequestAuthInfo requests a device code login flow information from Hosted
func (d *DeviceAuthorizationFlow) RequestAuthInfo(ctx context.Context) (AuthFlowInfo, error) {
form := url.Values{}
form.Add("client_id", d.providerConfig.ClientID)
form.Add("audience", d.providerConfig.Audience)
form.Add("scope", d.providerConfig.Scope)
req, err := http.NewRequest("POST", d.providerConfig.DeviceAuthEndpoint,
strings.NewReader(form.Encode()))
if err != nil {
return AuthFlowInfo{}, fmt.Errorf("creating request failed with error: %v", err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := d.HTTPClient.Do(req)
if err != nil {
return AuthFlowInfo{}, fmt.Errorf("doing request failed with error: %v", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return AuthFlowInfo{}, fmt.Errorf("reading body failed with error: %v", err)
}
if res.StatusCode != 200 {
return AuthFlowInfo{}, fmt.Errorf("request device code returned status %d error: %s", res.StatusCode, string(body))
}
deviceCode := AuthFlowInfo{}
err = json.Unmarshal(body, &deviceCode)
if err != nil {
return AuthFlowInfo{}, fmt.Errorf("unmarshaling response failed with error: %v", err)
}
// Fallback to the verification_uri if the IdP doesn't support verification_uri_complete
if deviceCode.VerificationURIComplete == "" {
deviceCode.VerificationURIComplete = deviceCode.VerificationURI
}
return deviceCode, err
}
func (d *DeviceAuthorizationFlow) requestToken(info AuthFlowInfo) (TokenRequestResponse, error) {
form := url.Values{}
form.Add("client_id", d.providerConfig.ClientID)
form.Add("grant_type", HostedGrantType)
form.Add("device_code", info.DeviceCode)
req, err := http.NewRequest("POST", d.providerConfig.TokenEndpoint, strings.NewReader(form.Encode()))
if err != nil {
return TokenRequestResponse{}, fmt.Errorf("failed to create request access token: %v", err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := d.HTTPClient.Do(req)
if err != nil {
return TokenRequestResponse{}, fmt.Errorf("failed to request access token with error: %v", err)
}
defer func() {
err := res.Body.Close()
if err != nil {
return
}
}()
body, err := io.ReadAll(res.Body)
if err != nil {
return TokenRequestResponse{}, fmt.Errorf("failed reading access token response body with error: %v", err)
}
if res.StatusCode > 499 {
return TokenRequestResponse{}, fmt.Errorf("access token response returned code: %s", string(body))
}
tokenResponse := TokenRequestResponse{}
err = json.Unmarshal(body, &tokenResponse)
if err != nil {
return TokenRequestResponse{}, fmt.Errorf("parsing token response failed with error: %v", err)
}
return tokenResponse, nil
}
// WaitToken waits user's login and authorize the app. Once the user's authorize
// it retrieves the access token from Hosted's endpoint and validates it before returning
func (d *DeviceAuthorizationFlow) WaitToken(ctx context.Context, info AuthFlowInfo) (TokenInfo, error) {
interval := time.Duration(info.Interval) * time.Second
ticker := time.NewTicker(interval)
for {
select {
case <-ctx.Done():
return TokenInfo{}, ctx.Err()
case <-ticker.C:
tokenResponse, err := d.requestToken(info)
if err != nil {
return TokenInfo{}, fmt.Errorf("parsing token response failed with error: %v", err)
}
if tokenResponse.Error != "" {
if tokenResponse.Error == "authorization_pending" {
continue
} else if tokenResponse.Error == "slow_down" {
interval = interval + (3 * time.Second)
ticker.Reset(interval)
continue
}
return TokenInfo{}, fmt.Errorf(tokenResponse.ErrorDescription)
}
tokenInfo := TokenInfo{
AccessToken: tokenResponse.AccessToken,
TokenType: tokenResponse.TokenType,
RefreshToken: tokenResponse.RefreshToken,
IDToken: tokenResponse.IDToken,
ExpiresIn: tokenResponse.ExpiresIn,
UseIDToken: d.providerConfig.UseIDToken,
}
err = isValidAccessToken(tokenInfo.GetTokenToUse(), d.providerConfig.Audience)
if err != nil {
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
}
return tokenInfo, err
}
}
}

View File

@@ -1,17 +1,17 @@
package internal
package auth
import (
"context"
"fmt"
"github.com/golang-jwt/jwt"
"github.com/netbirdio/netbird/client/internal"
"github.com/stretchr/testify/require"
"io"
"net/http"
"net/url"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt"
"github.com/stretchr/testify/require"
)
type mockHTTPClient struct {
@@ -53,7 +53,7 @@ func TestHosted_RequestDeviceCode(t *testing.T) {
testingErrFunc require.ErrorAssertionFunc
expectedErrorMSG string
testingFunc require.ComparisonAssertionFunc
expectedOut DeviceAuthInfo
expectedOut AuthFlowInfo
expectedMSG string
expectPayload string
}
@@ -92,7 +92,7 @@ func TestHosted_RequestDeviceCode(t *testing.T) {
testingFunc: require.EqualValues,
expectPayload: expectPayload,
}
testCase4Out := DeviceAuthInfo{ExpiresIn: 10}
testCase4Out := AuthFlowInfo{ExpiresIn: 10}
testCase4 := test{
name: "Got Device Code",
inputResBody: fmt.Sprintf("{\"expires_in\":%d}", testCase4Out.ExpiresIn),
@@ -113,8 +113,8 @@ func TestHosted_RequestDeviceCode(t *testing.T) {
err: testCase.inputReqError,
}
hosted := Hosted{
providerConfig: ProviderConfig{
deviceFlow := &DeviceAuthorizationFlow{
providerConfig: internal.DeviceAuthProviderConfig{
Audience: expectedAudience,
ClientID: expectedClientID,
Scope: expectedScope,
@@ -125,7 +125,7 @@ func TestHosted_RequestDeviceCode(t *testing.T) {
HTTPClient: &httpClient,
}
authInfo, err := hosted.RequestDeviceCode(context.TODO())
authInfo, err := deviceFlow.RequestAuthInfo(context.TODO())
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
require.EqualValues(t, expectPayload, httpClient.reqBody, "payload should match")
@@ -145,7 +145,7 @@ func TestHosted_WaitToken(t *testing.T) {
inputMaxReqs int
inputCountResBody string
inputTimeout time.Duration
inputInfo DeviceAuthInfo
inputInfo AuthFlowInfo
inputAudience string
testingErrFunc require.ErrorAssertionFunc
expectedErrorMSG string
@@ -155,7 +155,7 @@ func TestHosted_WaitToken(t *testing.T) {
expectPayload string
}
defaultInfo := DeviceAuthInfo{
defaultInfo := AuthFlowInfo{
DeviceCode: "test",
ExpiresIn: 10,
Interval: 1,
@@ -278,8 +278,8 @@ func TestHosted_WaitToken(t *testing.T) {
countResBody: testCase.inputCountResBody,
}
hosted := Hosted{
providerConfig: ProviderConfig{
deviceFlow := DeviceAuthorizationFlow{
providerConfig: internal.DeviceAuthProviderConfig{
Audience: testCase.inputAudience,
ClientID: clientID,
TokenEndpoint: "test.hosted.com/token",
@@ -287,11 +287,12 @@ func TestHosted_WaitToken(t *testing.T) {
Scope: "openid",
UseIDToken: false,
},
HTTPClient: &httpClient}
HTTPClient: &httpClient,
}
ctx, cancel := context.WithTimeout(context.TODO(), testCase.inputTimeout)
defer cancel()
tokenInfo, err := hosted.WaitToken(ctx, testCase.inputInfo)
tokenInfo, err := deviceFlow.WaitToken(ctx, testCase.inputInfo)
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
require.EqualValues(t, testCase.expectPayload, httpClient.reqBody, "payload should match")

View File

@@ -0,0 +1,90 @@
package auth
import (
"context"
"fmt"
"net/http"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal"
)
// OAuthFlow represents an interface for authorization using different OAuth 2.0 flows
type OAuthFlow interface {
RequestAuthInfo(ctx context.Context) (AuthFlowInfo, error)
WaitToken(ctx context.Context, info AuthFlowInfo) (TokenInfo, error)
GetClientID(ctx context.Context) string
}
// HTTPClient http client interface for API calls
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// AuthFlowInfo holds information for the OAuth 2.0 authorization flow
type AuthFlowInfo struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
// Claims used when validating the access token
type Claims struct {
Audience interface{} `json:"aud"`
}
// TokenInfo holds information of issued access token
type TokenInfo struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
UseIDToken bool `json:"-"`
}
// GetTokenToUse returns either the access or id token based on UseIDToken field
func (t TokenInfo) GetTokenToUse() string {
if t.UseIDToken {
return t.IDToken
}
return t.AccessToken
}
// NewOAuthFlow initializes and returns the appropriate OAuth flow based on the management configuration.
func NewOAuthFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) {
log.Debug("getting device authorization flow info")
// Try to initialize the Device Authorization Flow
deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
if err == nil {
return NewDeviceAuthorizationFlow(deviceFlowInfo.ProviderConfig)
}
log.Debugf("getting device authorization flow info failed with error: %v", err)
log.Debugf("falling back to pkce authorization flow info")
// If Device Authorization Flow failed, try the PKCE Authorization Flow
pkceFlowInfo, err := internal.GetPKCEAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
if err != nil {
s, ok := gstatus.FromError(err)
if ok && s.Code() == codes.NotFound {
return nil, fmt.Errorf("no SSO provider returned from management. " +
"If you are using hosting Netbird see documentation at " +
"https://github.com/netbirdio/netbird/tree/main/management for details")
} else if ok && s.Code() == codes.Unimplemented {
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)
} else {
return nil, fmt.Errorf("getting pkce authorization flow info failed with error: %v", err)
}
}
return NewPKCEAuthorizationFlow(pkceFlowInfo.ProviderConfig)
}

View File

@@ -0,0 +1,245 @@
package auth
import (
"context"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"fmt"
"html/template"
"net"
"net/http"
"net/url"
"strings"
"time"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/templates"
)
var _ OAuthFlow = &PKCEAuthorizationFlow{}
const (
queryState = "state"
queryCode = "code"
queryError = "error"
queryErrorDesc = "error_description"
defaultPKCETimeoutSeconds = 300
)
// PKCEAuthorizationFlow implements the OAuthFlow interface for
// the Authorization Code Flow with PKCE.
type PKCEAuthorizationFlow struct {
providerConfig internal.PKCEAuthProviderConfig
state string
codeVerifier string
oAuthConfig *oauth2.Config
}
// NewPKCEAuthorizationFlow returns new PKCE authorization code flow.
func NewPKCEAuthorizationFlow(config internal.PKCEAuthProviderConfig) (*PKCEAuthorizationFlow, error) {
var availableRedirectURL string
// find the first available redirect URL
for _, redirectURL := range config.RedirectURLs {
if !isRedirectURLPortUsed(redirectURL) {
availableRedirectURL = redirectURL
break
}
}
if availableRedirectURL == "" {
return nil, fmt.Errorf("no available port found from configured redirect URLs: %q", config.RedirectURLs)
}
cfg := &oauth2.Config{
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: config.AuthorizationEndpoint,
TokenURL: config.TokenEndpoint,
},
RedirectURL: availableRedirectURL,
Scopes: strings.Split(config.Scope, " "),
}
return &PKCEAuthorizationFlow{
providerConfig: config,
oAuthConfig: cfg,
}, nil
}
// GetClientID returns the provider client id
func (p *PKCEAuthorizationFlow) GetClientID(_ context.Context) string {
return p.providerConfig.ClientID
}
// RequestAuthInfo requests a authorization code login flow information.
func (p *PKCEAuthorizationFlow) RequestAuthInfo(_ context.Context) (AuthFlowInfo, error) {
state, err := randomBytesInHex(24)
if err != nil {
return AuthFlowInfo{}, fmt.Errorf("could not generate random state: %v", err)
}
p.state = state
codeVerifier, err := randomBytesInHex(64)
if err != nil {
return AuthFlowInfo{}, fmt.Errorf("could not create a code verifier: %v", err)
}
p.codeVerifier = codeVerifier
codeChallenge := createCodeChallenge(codeVerifier)
authURL := p.oAuthConfig.AuthCodeURL(
state,
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
oauth2.SetAuthURLParam("code_challenge", codeChallenge),
oauth2.SetAuthURLParam("audience", p.providerConfig.Audience),
)
return AuthFlowInfo{
VerificationURIComplete: authURL,
ExpiresIn: defaultPKCETimeoutSeconds,
}, nil
}
// WaitToken waits for the OAuth token in the PKCE Authorization Flow.
// It starts an HTTP server to receive the OAuth token callback and waits for the token or an error.
// Once the token is received, it is converted to TokenInfo and validated before returning.
func (p *PKCEAuthorizationFlow) WaitToken(ctx context.Context, _ AuthFlowInfo) (TokenInfo, error) {
tokenChan := make(chan *oauth2.Token, 1)
errChan := make(chan error, 1)
go p.startServer(tokenChan, errChan)
select {
case <-ctx.Done():
return TokenInfo{}, ctx.Err()
case token := <-tokenChan:
return p.handleOAuthToken(token)
case err := <-errChan:
return TokenInfo{}, err
}
}
func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errChan chan<- error) {
parsedURL, err := url.Parse(p.oAuthConfig.RedirectURL)
if err != nil {
errChan <- fmt.Errorf("failed to parse redirect URL: %v", err)
return
}
port := parsedURL.Port()
server := http.Server{Addr: fmt.Sprintf(":%s", port)}
defer func() {
if err := server.Shutdown(context.Background()); err != nil {
log.Errorf("error while shutting down pkce flow server: %v", err)
}
}()
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
tokenValidatorFunc := func() (*oauth2.Token, error) {
query := req.URL.Query()
if authError := query.Get(queryError); authError != "" {
authErrorDesc := query.Get(queryErrorDesc)
return nil, fmt.Errorf("%s.%s", authError, authErrorDesc)
}
// Prevent timing attacks on state
if state := query.Get(queryState); subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 {
return nil, fmt.Errorf("invalid state")
}
code := query.Get(queryCode)
if code == "" {
return nil, fmt.Errorf("missing code")
}
return p.oAuthConfig.Exchange(
req.Context(),
code,
oauth2.SetAuthURLParam("code_verifier", p.codeVerifier),
)
}
token, err := tokenValidatorFunc()
if err != nil {
renderPKCEFlowTmpl(w, err)
errChan <- fmt.Errorf("PKCE authorization flow failed: %v", err)
return
}
renderPKCEFlowTmpl(w, nil)
tokenChan <- token
})
if err := server.ListenAndServe(); err != nil {
errChan <- err
}
}
func (p *PKCEAuthorizationFlow) handleOAuthToken(token *oauth2.Token) (TokenInfo, error) {
tokenInfo := TokenInfo{
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
TokenType: token.TokenType,
ExpiresIn: token.Expiry.Second(),
UseIDToken: p.providerConfig.UseIDToken,
}
if idToken, ok := token.Extra("id_token").(string); ok {
tokenInfo.IDToken = idToken
}
if err := isValidAccessToken(tokenInfo.GetTokenToUse(), p.providerConfig.Audience); err != nil {
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
}
return tokenInfo, nil
}
func createCodeChallenge(codeVerifier string) string {
sha2 := sha256.Sum256([]byte(codeVerifier))
return base64.RawURLEncoding.EncodeToString(sha2[:])
}
// isRedirectURLPortUsed checks if the port used in the redirect URL is in use.
func isRedirectURLPortUsed(redirectURL string) bool {
parsedURL, err := url.Parse(redirectURL)
if err != nil {
log.Errorf("failed to parse redirect URL: %v", err)
return true
}
addr := fmt.Sprintf(":%s", parsedURL.Port())
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
if err != nil {
return false
}
defer func() {
if err := conn.Close(); err != nil {
log.Errorf("error while closing the connection: %v", err)
}
}()
return true
}
func renderPKCEFlowTmpl(w http.ResponseWriter, authError error) {
tmpl, err := template.New("pkce-auth-flow").Parse(templates.PKCEAuthMsgTmpl)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := make(map[string]string)
if authError != nil {
data["Error"] = authError.Error()
}
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

View File

@@ -0,0 +1,62 @@
package auth
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"reflect"
"strings"
)
func randomBytesInHex(count int) (string, error) {
buf := make([]byte, count)
_, err := io.ReadFull(rand.Reader, buf)
if err != nil {
return "", fmt.Errorf("could not generate %d random bytes: %v", count, err)
}
return hex.EncodeToString(buf), nil
}
// isValidAccessToken is a simple validation of the access token
func isValidAccessToken(token string, audience string) error {
if token == "" {
return fmt.Errorf("token received is empty")
}
encodedClaims := strings.Split(token, ".")[1]
claimsString, err := base64.RawURLEncoding.DecodeString(encodedClaims)
if err != nil {
return err
}
claims := Claims{}
err = json.Unmarshal(claimsString, &claims)
if err != nil {
return err
}
if claims.Audience == nil {
return fmt.Errorf("required token field audience is absent")
}
// Audience claim of JWT can be a string or an array of strings
typ := reflect.TypeOf(claims.Audience)
switch typ.Kind() {
case reflect.String:
if claims.Audience == audience {
return nil
}
case reflect.Slice:
for _, aud := range claims.Audience.([]interface{}) {
if audience == aud {
return nil
}
}
}
return fmt.Errorf("invalid JWT token audience field")
}

View File

@@ -16,11 +16,11 @@ import (
// DeviceAuthorizationFlow represents Device Authorization Flow information
type DeviceAuthorizationFlow struct {
Provider string
ProviderConfig ProviderConfig
ProviderConfig DeviceAuthProviderConfig
}
// ProviderConfig has all attributes needed to initiate a device authorization flow
type ProviderConfig struct {
// DeviceAuthProviderConfig has all attributes needed to initiate a device authorization flow
type DeviceAuthProviderConfig struct {
// ClientID An IDP application client id
ClientID string
// ClientSecret An IDP application client secret
@@ -88,7 +88,7 @@ func GetDeviceAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmU
deviceAuthorizationFlow := DeviceAuthorizationFlow{
Provider: protoDeviceAuthorizationFlow.Provider.String(),
ProviderConfig: ProviderConfig{
ProviderConfig: DeviceAuthProviderConfig{
Audience: protoDeviceAuthorizationFlow.GetProviderConfig().GetAudience(),
ClientID: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientID(),
ClientSecret: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientSecret(),
@@ -105,7 +105,7 @@ func GetDeviceAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmU
deviceAuthorizationFlow.ProviderConfig.Scope = "openid"
}
err = isProviderConfigValid(deviceAuthorizationFlow.ProviderConfig)
err = isDeviceAuthProviderConfigValid(deviceAuthorizationFlow.ProviderConfig)
if err != nil {
return DeviceAuthorizationFlow{}, err
}
@@ -113,7 +113,7 @@ func GetDeviceAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmU
return deviceAuthorizationFlow, nil
}
func isProviderConfigValid(config ProviderConfig) error {
func isDeviceAuthProviderConfigValid(config DeviceAuthProviderConfig) error {
errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator"
if config.Audience == "" {
return fmt.Errorf(errorMSGFormat, "Audience")

View File

@@ -15,7 +15,8 @@ const (
fileGeneratedResolvConfSearchBeginContent = "search "
fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader +
"\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 (
@@ -91,7 +92,12 @@ func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error {
searchDomains += " " + dConf.domain
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)
if err != nil {
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 {
primaryServiceKey := s.getPrimaryService()
primaryServiceKey, existingNameserver := s.getPrimaryService()
if primaryServiceKey == "" {
return fmt.Errorf("couldn't find the primary service key")
}
err := s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port)
err := s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port, existingNameserver)
if err != nil {
return err
}
@@ -196,27 +195,32 @@ func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error
return nil
}
func (s *systemConfigurator) getPrimaryService() string {
func (s *systemConfigurator) getPrimaryService() (string, string) {
line := buildCommandLine("show", globalIPv4State, "")
stdinCommands := wrapCommand(line)
b, err := runSystemConfigCommand(stdinCommands)
if err != nil {
log.Error("got error while sending the command: ", err)
return ""
return "", ""
}
scanner := bufio.NewScanner(bytes.NewReader(b))
primaryService := ""
router := ""
for scanner.Scan() {
text := scanner.Text()
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(keyServerAddresses, arraySymbol+dnsServer)
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer+" "+existingDNSServer)
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
addDomainCommand := buildCreateStateWithOperation(setupKey, lines)
stdinCommands := wrapCommand(addDomainCommand)

View File

@@ -4,6 +4,7 @@ package dns
import (
"fmt"
"os"
"os/exec"
"strings"
@@ -59,7 +60,11 @@ func (r *resolvconf) applyDNSConfig(config hostDNSConfig) error {
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)
if err != nil {

View File

@@ -20,8 +20,8 @@ import (
"github.com/netbirdio/netbird/client/internal/acl"
"github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/proxy"
"github.com/netbirdio/netbird/client/internal/routemanager"
"github.com/netbirdio/netbird/client/internal/wgproxy"
nbssh "github.com/netbirdio/netbird/client/ssh"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
@@ -101,7 +101,8 @@ type Engine struct {
ctx context.Context
wgInterface *iface.WGIface
wgInterface *iface.WGIface
wgProxyFactory *wgproxy.Factory
udpMux *bind.UniversalUDPMuxDefault
udpMuxConn io.Closer
@@ -132,6 +133,7 @@ func NewEngine(
signalClient signal.Client, mgmClient mgm.Client,
config *EngineConfig, mobileDep MobileDependency, statusRecorder *peer.Status,
) *Engine {
return &Engine{
ctx: ctx,
cancel: cancel,
@@ -146,6 +148,7 @@ func NewEngine(
networkSerial: 0,
sshServerFunc: nbssh.DefaultSSHServer,
statusRecorder: statusRecorder,
wgProxyFactory: wgproxy.NewFactory(config.WgPort),
}
}
@@ -282,7 +285,7 @@ func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
for _, p := range peersUpdate {
peerPubKey := p.GetWgPubKey()
if peerConn, ok := e.peerConns[peerPubKey]; ok {
if peerConn.GetConf().ProxyConfig.AllowedIps != strings.Join(p.AllowedIps, ",") {
if peerConn.WgConfig().AllowedIps != strings.Join(p.AllowedIps, ",") {
modified = append(modified, p)
continue
}
@@ -795,9 +798,7 @@ func (e *Engine) connWorker(conn *peer.Conn, peerKey string) {
// we might have received new STUN and TURN servers meanwhile, so update them
e.syncMsgMux.Lock()
conf := conn.GetConf()
conf.StunTurn = append(e.STUNs, e.TURNs...)
conn.UpdateConf(conf)
conn.UpdateStunTurn(append(e.STUNs, e.TURNs...))
e.syncMsgMux.Unlock()
err := conn.Open()
@@ -826,9 +827,9 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, e
stunTurn = append(stunTurn, e.STUNs...)
stunTurn = append(stunTurn, e.TURNs...)
proxyConfig := proxy.Config{
wgConfig := peer.WgConfig{
RemoteKey: pubKey,
WgListenAddr: fmt.Sprintf("127.0.0.1:%d", e.config.WgPort),
WgListenPort: e.config.WgPort,
WgInterface: e.wgInterface,
AllowedIps: allowedIPs,
PreSharedKey: e.config.PreSharedKey,
@@ -845,13 +846,13 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, e
Timeout: timeout,
UDPMux: e.udpMux.UDPMuxDefault,
UDPMuxSrflx: e.udpMux,
ProxyConfig: proxyConfig,
WgConfig: wgConfig,
LocalWgPort: e.config.WgPort,
NATExternalIPs: e.parseNATExternalIPMappings(),
UserspaceBind: e.wgInterface.IsUserspaceBind(),
}
peerConn, err := peer.NewConn(config, e.statusRecorder, e.mobileDep.TunAdapter, e.mobileDep.IFaceDiscover)
peerConn, err := peer.NewConn(config, e.statusRecorder, e.wgProxyFactory, e.mobileDep.TunAdapter, e.mobileDep.IFaceDiscover)
if err != nil {
return nil, err
}
@@ -1008,6 +1009,10 @@ func (e *Engine) parseNATExternalIPMappings() []string {
}
func (e *Engine) close() {
if err := e.wgProxyFactory.Free(); err != nil {
log.Errorf("failed closing ebpf proxy: %s", err)
}
log.Debugf("removing Netbird interface %s", e.config.WgIfaceName)
if e.wgInterface != nil {
if err := e.wgInterface.Close(); err != nil {

View File

@@ -367,9 +367,9 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
t.Errorf("expecting Engine.peerConns to contain peer %s", p)
}
expectedAllowedIPs := strings.Join(p.AllowedIps, ",")
if conn.GetConf().ProxyConfig.AllowedIps != expectedAllowedIPs {
if conn.WgConfig().AllowedIps != expectedAllowedIPs {
t.Errorf("expecting peer %s to have AllowedIPs= %s, got %s", p.GetWgPubKey(),
expectedAllowedIPs, conn.GetConf().ProxyConfig.AllowedIps)
expectedAllowedIPs, conn.WgConfig().AllowedIps)
}
}
})

View File

@@ -1,286 +0,0 @@
package internal
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"reflect"
"strings"
"time"
)
// OAuthClient is a OAuth client interface for various idp providers
type OAuthClient interface {
RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error)
GetClientID(ctx context.Context) string
}
// HTTPClient http client interface for API calls
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// DeviceAuthInfo holds information for the OAuth device login flow
type DeviceAuthInfo struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
// HostedGrantType grant type for device flow on Hosted
const (
HostedGrantType = "urn:ietf:params:oauth:grant-type:device_code"
HostedRefreshGrant = "refresh_token"
)
// Hosted client
type Hosted struct {
providerConfig ProviderConfig
HTTPClient HTTPClient
}
// RequestDeviceCodePayload used for request device code payload for auth0
type RequestDeviceCodePayload struct {
Audience string `json:"audience"`
ClientID string `json:"client_id"`
Scope string `json:"scope"`
}
// TokenRequestPayload used for requesting the auth0 token
type TokenRequestPayload struct {
GrantType string `json:"grant_type"`
DeviceCode string `json:"device_code,omitempty"`
ClientID string `json:"client_id"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// TokenRequestResponse used for parsing Hosted token's response
type TokenRequestResponse struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
TokenInfo
}
// Claims used when validating the access token
type Claims struct {
Audience interface{} `json:"aud"`
}
// TokenInfo holds information of issued access token
type TokenInfo struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
UseIDToken bool `json:"-"`
}
// GetTokenToUse returns either the access or id token based on UseIDToken field
func (t TokenInfo) GetTokenToUse() string {
if t.UseIDToken {
return t.IDToken
}
return t.AccessToken
}
// NewHostedDeviceFlow returns an Hosted OAuth client
func NewHostedDeviceFlow(config ProviderConfig) *Hosted {
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
httpTransport.MaxIdleConns = 5
httpClient := &http.Client{
Timeout: 10 * time.Second,
Transport: httpTransport,
}
return &Hosted{
providerConfig: config,
HTTPClient: httpClient,
}
}
// GetClientID returns the provider client id
func (h *Hosted) GetClientID(ctx context.Context) string {
return h.providerConfig.ClientID
}
// RequestDeviceCode requests a device code login flow information from Hosted
func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error) {
form := url.Values{}
form.Add("client_id", h.providerConfig.ClientID)
form.Add("audience", h.providerConfig.Audience)
form.Add("scope", h.providerConfig.Scope)
req, err := http.NewRequest("POST", h.providerConfig.DeviceAuthEndpoint,
strings.NewReader(form.Encode()))
if err != nil {
return DeviceAuthInfo{}, fmt.Errorf("creating request failed with error: %v", err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := h.HTTPClient.Do(req)
if err != nil {
return DeviceAuthInfo{}, fmt.Errorf("doing request failed with error: %v", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return DeviceAuthInfo{}, fmt.Errorf("reading body failed with error: %v", err)
}
if res.StatusCode != 200 {
return DeviceAuthInfo{}, fmt.Errorf("request device code returned status %d error: %s", res.StatusCode, string(body))
}
deviceCode := DeviceAuthInfo{}
err = json.Unmarshal(body, &deviceCode)
if err != nil {
return DeviceAuthInfo{}, fmt.Errorf("unmarshaling response failed with error: %v", err)
}
// Fallback to the verification_uri if the IdP doesn't support verification_uri_complete
if deviceCode.VerificationURIComplete == "" {
deviceCode.VerificationURIComplete = deviceCode.VerificationURI
}
return deviceCode, err
}
func (h *Hosted) requestToken(info DeviceAuthInfo) (TokenRequestResponse, error) {
form := url.Values{}
form.Add("client_id", h.providerConfig.ClientID)
form.Add("grant_type", HostedGrantType)
form.Add("device_code", info.DeviceCode)
req, err := http.NewRequest("POST", h.providerConfig.TokenEndpoint, strings.NewReader(form.Encode()))
if err != nil {
return TokenRequestResponse{}, fmt.Errorf("failed to create request access token: %v", err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := h.HTTPClient.Do(req)
if err != nil {
return TokenRequestResponse{}, fmt.Errorf("failed to request access token with error: %v", err)
}
defer func() {
err := res.Body.Close()
if err != nil {
return
}
}()
body, err := io.ReadAll(res.Body)
if err != nil {
return TokenRequestResponse{}, fmt.Errorf("failed reading access token response body with error: %v", err)
}
if res.StatusCode > 499 {
return TokenRequestResponse{}, fmt.Errorf("access token response returned code: %s", string(body))
}
tokenResponse := TokenRequestResponse{}
err = json.Unmarshal(body, &tokenResponse)
if err != nil {
return TokenRequestResponse{}, fmt.Errorf("parsing token response failed with error: %v", err)
}
return tokenResponse, nil
}
// WaitToken waits user's login and authorize the app. Once the user's authorize
// it retrieves the access token from Hosted's endpoint and validates it before returning
func (h *Hosted) WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error) {
interval := time.Duration(info.Interval) * time.Second
ticker := time.NewTicker(interval)
for {
select {
case <-ctx.Done():
return TokenInfo{}, ctx.Err()
case <-ticker.C:
tokenResponse, err := h.requestToken(info)
if err != nil {
return TokenInfo{}, fmt.Errorf("parsing token response failed with error: %v", err)
}
if tokenResponse.Error != "" {
if tokenResponse.Error == "authorization_pending" {
continue
} else if tokenResponse.Error == "slow_down" {
interval = interval + (3 * time.Second)
ticker.Reset(interval)
continue
}
return TokenInfo{}, fmt.Errorf(tokenResponse.ErrorDescription)
}
tokenInfo := TokenInfo{
AccessToken: tokenResponse.AccessToken,
TokenType: tokenResponse.TokenType,
RefreshToken: tokenResponse.RefreshToken,
IDToken: tokenResponse.IDToken,
ExpiresIn: tokenResponse.ExpiresIn,
UseIDToken: h.providerConfig.UseIDToken,
}
err = isValidAccessToken(tokenInfo.GetTokenToUse(), h.providerConfig.Audience)
if err != nil {
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
}
return tokenInfo, err
}
}
}
// isValidAccessToken is a simple validation of the access token
func isValidAccessToken(token string, audience string) error {
if token == "" {
return fmt.Errorf("token received is empty")
}
encodedClaims := strings.Split(token, ".")[1]
claimsString, err := base64.RawURLEncoding.DecodeString(encodedClaims)
if err != nil {
return err
}
claims := Claims{}
err = json.Unmarshal(claimsString, &claims)
if err != nil {
return err
}
if claims.Audience == nil {
return fmt.Errorf("required token field audience is absent")
}
// Audience claim of JWT can be a string or an array of strings
typ := reflect.TypeOf(claims.Audience)
switch typ.Kind() {
case reflect.String:
if claims.Audience == audience {
return nil
}
case reflect.Slice:
for _, aud := range claims.Audience.([]interface{}) {
if audience == aud {
return nil
}
}
}
return fmt.Errorf("invalid JWT token audience field")
}

View File

@@ -10,9 +10,10 @@ import (
"github.com/pion/ice/v2"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/internal/proxy"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/internal/wgproxy"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/iface/bind"
signal "github.com/netbirdio/netbird/signal/client"
@@ -23,8 +24,18 @@ import (
const (
iceKeepAliveDefault = 4 * time.Second
iceDisconnectedTimeoutDefault = 6 * time.Second
defaultWgKeepAlive = 25 * time.Second
)
type WgConfig struct {
WgListenPort int
RemoteKey string
WgInterface *iface.WGIface
AllowedIps string
PreSharedKey *wgtypes.Key
}
// ConnConfig is a peer Connection configuration
type ConnConfig struct {
@@ -43,7 +54,7 @@ type ConnConfig struct {
Timeout time.Duration
ProxyConfig proxy.Config
WgConfig WgConfig
UDPMux ice.UDPMux
UDPMuxSrflx ice.UniversalUDPMux
@@ -98,7 +109,9 @@ type Conn struct {
statusRecorder *Status
proxy proxy.Proxy
wgProxyFactory *wgproxy.Factory
wgProxy wgproxy.Proxy
remoteModeCh chan ModeMessage
meta meta
@@ -122,14 +135,19 @@ func (conn *Conn) GetConf() ConnConfig {
return conn.config
}
// UpdateConf updates the connection config
func (conn *Conn) UpdateConf(conf ConnConfig) {
conn.config = conf
// WgConfig returns the WireGuard config
func (conn *Conn) WgConfig() WgConfig {
return conn.config.WgConfig
}
// UpdateStunTurn update the turn and stun addresses
func (conn *Conn) UpdateStunTurn(turnStun []*ice.URL) {
conn.config.StunTurn = turnStun
}
// NewConn creates a new not opened Conn to the remote peer.
// To establish a connection run Conn.Open
func NewConn(config ConnConfig, statusRecorder *Status, adapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover) (*Conn, error) {
func NewConn(config ConnConfig, statusRecorder *Status, wgProxyFactory *wgproxy.Factory, adapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover) (*Conn, error) {
return &Conn{
config: config,
mu: sync.Mutex{},
@@ -139,6 +157,7 @@ func NewConn(config ConnConfig, statusRecorder *Status, adapter iface.TunAdapter
remoteAnswerCh: make(chan OfferAnswer),
statusRecorder: statusRecorder,
remoteModeCh: make(chan ModeMessage, 1),
wgProxyFactory: wgProxyFactory,
adapter: adapter,
iFaceDiscover: iFaceDiscover,
}, nil
@@ -215,12 +234,12 @@ func (conn *Conn) candidateTypes() []ice.CandidateType {
func (conn *Conn) Open() error {
log.Debugf("trying to connect to peer %s", conn.config.Key)
peerState := State{PubKey: conn.config.Key}
peerState.IP = strings.Split(conn.config.ProxyConfig.AllowedIps, "/")[0]
peerState.ConnStatusUpdate = time.Now()
peerState.ConnStatus = conn.status
peerState := State{
PubKey: conn.config.Key,
IP: strings.Split(conn.config.WgConfig.AllowedIps, "/")[0],
ConnStatusUpdate: time.Now(),
ConnStatus: conn.status,
}
err := conn.statusRecorder.UpdatePeerState(peerState)
if err != nil {
log.Warnf("erro while updating the state of peer %s,err: %v", conn.config.Key, err)
@@ -275,10 +294,11 @@ func (conn *Conn) Open() error {
defer conn.notifyDisconnected()
conn.mu.Unlock()
peerState = State{PubKey: conn.config.Key}
peerState.ConnStatus = conn.status
peerState.ConnStatusUpdate = time.Now()
peerState = State{
PubKey: conn.config.Key,
ConnStatus: conn.status,
ConnStatusUpdate: time.Now(),
}
err = conn.statusRecorder.UpdatePeerState(peerState)
if err != nil {
log.Warnf("erro while updating the state of peer %s,err: %v", conn.config.Key, err)
@@ -309,19 +329,12 @@ func (conn *Conn) Open() error {
remoteWgPort = remoteOfferAnswer.WgListenPort
}
// the ice connection has been established successfully so we are ready to start the proxy
err = conn.startProxy(remoteConn, remoteWgPort)
remoteAddr, err := conn.configureConnection(remoteConn, remoteWgPort)
if err != nil {
return err
}
if conn.proxy.Type() == proxy.TypeDirectNoProxy {
host, _, _ := net.SplitHostPort(remoteConn.LocalAddr().String())
rhost, _, _ := net.SplitHostPort(remoteConn.RemoteAddr().String())
// direct Wireguard connection
log.Infof("directly connected to peer %s [laddr <-> raddr] [%s:%d <-> %s:%d]", conn.config.Key, host, conn.config.LocalWgPort, rhost, remoteWgPort)
} else {
log.Infof("connected to peer %s [laddr <-> raddr] [%s <-> %s]", conn.config.Key, remoteConn.LocalAddr().String(), remoteConn.RemoteAddr().String())
}
log.Infof("connected to peer %s, endpoint address: %s", conn.config.Key, remoteAddr.String())
// wait until connection disconnected or has been closed externally (upper layer, e.g. engine)
select {
@@ -338,54 +351,60 @@ func isRelayCandidate(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeRelay
}
// startProxy starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
func (conn *Conn) startProxy(remoteConn net.Conn, remoteWgPort int) error {
// configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int) (net.Addr, error) {
conn.mu.Lock()
defer conn.mu.Unlock()
var pair *ice.CandidatePair
pair, err := conn.agent.GetSelectedCandidatePair()
if err != nil {
return err
return nil, err
}
peerState := State{PubKey: conn.config.Key}
p := conn.getProxy(pair, remoteWgPort)
conn.proxy = p
err = p.Start(remoteConn)
var endpoint net.Addr
if isRelayCandidate(pair.Local) {
log.Debugf("setup relay connection")
conn.wgProxy = conn.wgProxyFactory.GetProxy()
endpoint, err = conn.wgProxy.AddTurnConn(remoteConn)
if err != nil {
return nil, err
}
} else {
// To support old version's with direct mode we attempt to punch an additional role with the remote wireguard port
go conn.punchRemoteWGPort(pair, remoteWgPort)
endpoint = remoteConn.RemoteAddr()
}
endpointUdpAddr, _ := net.ResolveUDPAddr(endpoint.Network(), endpoint.String())
err = conn.config.WgConfig.WgInterface.UpdatePeer(conn.config.WgConfig.RemoteKey, conn.config.WgConfig.AllowedIps, defaultWgKeepAlive, endpointUdpAddr, conn.config.WgConfig.PreSharedKey)
if err != nil {
return err
if conn.wgProxy != nil {
_ = conn.wgProxy.CloseConn()
}
return nil, err
}
conn.status = StatusConnected
peerState.ConnStatus = conn.status
peerState.ConnStatusUpdate = time.Now()
peerState.LocalIceCandidateType = pair.Local.Type().String()
peerState.RemoteIceCandidateType = pair.Remote.Type().String()
peerState := State{
PubKey: conn.config.Key,
ConnStatus: conn.status,
ConnStatusUpdate: time.Now(),
LocalIceCandidateType: pair.Local.Type().String(),
RemoteIceCandidateType: pair.Remote.Type().String(),
Direct: !isRelayCandidate(pair.Local),
}
if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay {
peerState.Relayed = true
}
peerState.Direct = p.Type() == proxy.TypeDirectNoProxy || p.Type() == proxy.TypeNoProxy
err = conn.statusRecorder.UpdatePeerState(peerState)
if err != nil {
log.Warnf("unable to save peer's state, got error: %v", err)
}
return nil
}
// todo rename this method and the proxy package to something more appropriate
func (conn *Conn) getProxy(pair *ice.CandidatePair, remoteWgPort int) proxy.Proxy {
if isRelayCandidate(pair.Local) {
return proxy.NewWireGuardProxy(conn.config.ProxyConfig)
}
// To support old version's with direct mode we attempt to punch an additional role with the remote wireguard port
go conn.punchRemoteWGPort(pair, remoteWgPort)
return proxy.NewNoProxy(conn.config.ProxyConfig)
return endpoint, nil
}
func (conn *Conn) punchRemoteWGPort(pair *ice.CandidatePair, remoteWgPort int) {
@@ -414,22 +433,22 @@ func (conn *Conn) cleanup() error {
conn.mu.Lock()
defer conn.mu.Unlock()
var err1, err2, err3 error
if conn.agent != nil {
err := conn.agent.Close()
if err != nil {
return err
err1 = conn.agent.Close()
if err1 == nil {
conn.agent = nil
}
conn.agent = nil
}
if conn.proxy != nil {
err := conn.proxy.Close()
if err != nil {
return err
}
conn.proxy = nil
if conn.wgProxy != nil {
err2 = conn.wgProxy.CloseConn()
conn.wgProxy = nil
}
// todo: is it problem if we try to remove a peer what is never existed?
err3 = conn.config.WgConfig.WgInterface.RemovePeer(conn.config.WgConfig.RemoteKey)
if conn.notifyDisconnected != nil {
conn.notifyDisconnected()
conn.notifyDisconnected = nil
@@ -437,10 +456,11 @@ func (conn *Conn) cleanup() error {
conn.status = StatusDisconnected
peerState := State{PubKey: conn.config.Key}
peerState.ConnStatus = conn.status
peerState.ConnStatusUpdate = time.Now()
peerState := State{
PubKey: conn.config.Key,
ConnStatus: conn.status,
ConnStatusUpdate: time.Now(),
}
err := conn.statusRecorder.UpdatePeerState(peerState)
if err != nil {
// pretty common error because by that time Engine can already remove the peer and status won't be available.
@@ -449,8 +469,13 @@ func (conn *Conn) cleanup() error {
}
log.Debugf("cleaned up connection to peer %s", conn.config.Key)
return nil
if err1 != nil {
return err1
}
if err2 != nil {
return err2
}
return err3
}
// SetSignalOffer sets a handler function to be triggered by Conn when a new connection offer has to be signalled to the remote peer

View File

@@ -5,12 +5,11 @@ import (
"testing"
"time"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/magiconair/properties/assert"
"github.com/pion/ice/v2"
"github.com/netbirdio/netbird/client/internal/proxy"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/internal/wgproxy"
"github.com/netbirdio/netbird/iface"
)
@@ -20,7 +19,6 @@ var connConf = ConnConfig{
StunTurn: []*ice.URL{},
InterfaceBlackList: nil,
Timeout: time.Second,
ProxyConfig: proxy.Config{},
LocalWgPort: 51820,
}
@@ -37,7 +35,11 @@ func TestNewConn_interfaceFilter(t *testing.T) {
}
func TestConn_GetKey(t *testing.T) {
conn, err := NewConn(connConf, nil, nil, nil)
wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort)
defer func() {
_ = wgProxyFactory.Free()
}()
conn, err := NewConn(connConf, nil, wgProxyFactory, nil, nil)
if err != nil {
return
}
@@ -48,8 +50,11 @@ func TestConn_GetKey(t *testing.T) {
}
func TestConn_OnRemoteOffer(t *testing.T) {
conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil)
wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort)
defer func() {
_ = wgProxyFactory.Free()
}()
conn, err := NewConn(connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil)
if err != nil {
return
}
@@ -82,8 +87,11 @@ func TestConn_OnRemoteOffer(t *testing.T) {
}
func TestConn_OnRemoteAnswer(t *testing.T) {
conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil)
wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort)
defer func() {
_ = wgProxyFactory.Free()
}()
conn, err := NewConn(connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil)
if err != nil {
return
}
@@ -115,8 +123,11 @@ func TestConn_OnRemoteAnswer(t *testing.T) {
wg.Wait()
}
func TestConn_Status(t *testing.T) {
conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil)
wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort)
defer func() {
_ = wgProxyFactory.Free()
}()
conn, err := NewConn(connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil)
if err != nil {
return
}
@@ -142,8 +153,11 @@ func TestConn_Status(t *testing.T) {
}
func TestConn_Close(t *testing.T) {
conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil)
wgProxyFactory := wgproxy.NewFactory(connConf.LocalWgPort)
defer func() {
_ = wgProxyFactory.Free()
}()
conn, err := NewConn(connConf, NewRecorder("https://mgm"), wgProxyFactory, nil, nil)
if err != nil {
return
}

View File

@@ -17,6 +17,7 @@ type notifier struct {
listener Listener
currentClientState bool
lastNotification int
lastNumberOfPeers int
}
func newNotifier() *notifier {
@@ -29,6 +30,7 @@ func (n *notifier) setListener(listener Listener) {
n.serverStateLock.Lock()
n.notifyListener(listener, n.lastNotification)
listener.OnPeersListChanged(n.lastNumberOfPeers)
n.serverStateLock.Unlock()
n.listener = listener
@@ -124,6 +126,7 @@ func (n *notifier) calculateState(managementConn, signalConn bool) int {
}
func (n *notifier) peerListChanged(numOfPeers int) {
n.lastNumberOfPeers = numOfPeers
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
if n.listener == nil {

View File

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

View File

@@ -0,0 +1,128 @@
package internal
import (
"context"
"fmt"
"net/url"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
mgm "github.com/netbirdio/netbird/management/client"
)
// PKCEAuthorizationFlow represents PKCE Authorization Flow information
type PKCEAuthorizationFlow struct {
ProviderConfig PKCEAuthProviderConfig
}
// PKCEAuthProviderConfig has all attributes needed to initiate pkce authorization flow
type PKCEAuthProviderConfig struct {
// ClientID An IDP application client id
ClientID string
// ClientSecret An IDP application client secret
ClientSecret string
// Audience An Audience for to authorization validation
Audience string
// TokenEndpoint is the endpoint of an IDP manager where clients can obtain access token
TokenEndpoint string
// AuthorizationEndpoint is the endpoint of an IDP manager where clients can obtain authorization code
AuthorizationEndpoint string
// Scopes provides the scopes to be included in the token request
Scope string
// RedirectURL handles authorization code from IDP manager
RedirectURLs []string
// UseIDToken indicates if the id token should be used for authentication
UseIDToken bool
}
// GetPKCEAuthorizationFlowInfo initialize a PKCEAuthorizationFlow instance and return with it
func GetPKCEAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmURL *url.URL) (PKCEAuthorizationFlow, error) {
// validate our peer's Wireguard PRIVATE key
myPrivateKey, err := wgtypes.ParseKey(privateKey)
if err != nil {
log.Errorf("failed parsing Wireguard key %s: [%s]", privateKey, err.Error())
return PKCEAuthorizationFlow{}, err
}
var mgmTLSEnabled bool
if mgmURL.Scheme == "https" {
mgmTLSEnabled = true
}
log.Debugf("connecting to Management Service %s", mgmURL.String())
mgmClient, err := mgm.NewClient(ctx, mgmURL.Host, myPrivateKey, mgmTLSEnabled)
if err != nil {
log.Errorf("failed connecting to Management Service %s %v", mgmURL.String(), err)
return PKCEAuthorizationFlow{}, err
}
log.Debugf("connected to the Management service %s", mgmURL.String())
defer func() {
err = mgmClient.Close()
if err != nil {
log.Warnf("failed to close the Management service client %v", err)
}
}()
serverKey, err := mgmClient.GetServerPublicKey()
if err != nil {
log.Errorf("failed while getting Management Service public key: %v", err)
return PKCEAuthorizationFlow{}, err
}
protoPKCEAuthorizationFlow, err := mgmClient.GetPKCEAuthorizationFlow(*serverKey)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
log.Warnf("server couldn't find pkce flow, contact admin: %v", err)
return PKCEAuthorizationFlow{}, err
}
log.Errorf("failed to retrieve pkce flow: %v", err)
return PKCEAuthorizationFlow{}, err
}
authFlow := PKCEAuthorizationFlow{
ProviderConfig: PKCEAuthProviderConfig{
Audience: protoPKCEAuthorizationFlow.GetProviderConfig().GetAudience(),
ClientID: protoPKCEAuthorizationFlow.GetProviderConfig().GetClientID(),
ClientSecret: protoPKCEAuthorizationFlow.GetProviderConfig().GetClientSecret(),
TokenEndpoint: protoPKCEAuthorizationFlow.GetProviderConfig().GetTokenEndpoint(),
AuthorizationEndpoint: protoPKCEAuthorizationFlow.GetProviderConfig().GetAuthorizationEndpoint(),
Scope: protoPKCEAuthorizationFlow.GetProviderConfig().GetScope(),
RedirectURLs: protoPKCEAuthorizationFlow.GetProviderConfig().GetRedirectURLs(),
UseIDToken: protoPKCEAuthorizationFlow.GetProviderConfig().GetUseIDToken(),
},
}
err = isPKCEProviderConfigValid(authFlow.ProviderConfig)
if err != nil {
return PKCEAuthorizationFlow{}, err
}
return authFlow, nil
}
func isPKCEProviderConfigValid(config PKCEAuthProviderConfig) error {
errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator"
if config.Audience == "" {
return fmt.Errorf(errorMSGFormat, "Audience")
}
if config.ClientID == "" {
return fmt.Errorf(errorMSGFormat, "Client ID")
}
if config.TokenEndpoint == "" {
return fmt.Errorf(errorMSGFormat, "Token Endpoint")
}
if config.AuthorizationEndpoint == "" {
return fmt.Errorf(errorMSGFormat, "Authorization Auth Endpoint")
}
if config.Scope == "" {
return fmt.Errorf(errorMSGFormat, "PKCE Auth Scopes")
}
if config.RedirectURLs == nil {
return fmt.Errorf(errorMSGFormat, "PKCE Redirect URLs")
}
return nil
}

View File

@@ -1,72 +0,0 @@
package proxy
import (
"context"
log "github.com/sirupsen/logrus"
"net"
"time"
)
// DummyProxy just sends pings to the RemoteKey peer and reads responses
type DummyProxy struct {
conn net.Conn
remote string
ctx context.Context
cancel context.CancelFunc
}
func NewDummyProxy(remote string) *DummyProxy {
p := &DummyProxy{remote: remote}
p.ctx, p.cancel = context.WithCancel(context.Background())
return p
}
func (p *DummyProxy) Close() error {
p.cancel()
return nil
}
func (p *DummyProxy) Start(remoteConn net.Conn) error {
p.conn = remoteConn
go func() {
buf := make([]byte, 1500)
for {
select {
case <-p.ctx.Done():
return
default:
_, err := p.conn.Read(buf)
if err != nil {
log.Errorf("error while reading RemoteKey %s proxy %v", p.remote, err)
return
}
//log.Debugf("received %s from %s", string(buf[:n]), p.remote)
}
}
}()
go func() {
for {
select {
case <-p.ctx.Done():
return
default:
_, err := p.conn.Write([]byte("hello"))
//log.Debugf("sent ping to %s", p.remote)
if err != nil {
log.Errorf("error while writing to RemoteKey %s proxy %v", p.remote, err)
return
}
time.Sleep(5 * time.Second)
}
}
}()
return nil
}
func (p *DummyProxy) Type() Type {
return TypeDummy
}

View File

@@ -1,42 +0,0 @@
package proxy
import (
log "github.com/sirupsen/logrus"
"net"
)
// NoProxy is used just to configure WireGuard without any local proxy in between.
// Used when the WireGuard interface is userspace and uses bind.ICEBind
type NoProxy struct {
config Config
}
// NewNoProxy creates a new NoProxy with a provided config
func NewNoProxy(config Config) *NoProxy {
return &NoProxy{config: config}
}
// Close removes peer from the WireGuard interface
func (p *NoProxy) Close() error {
err := p.config.WgInterface.RemovePeer(p.config.RemoteKey)
if err != nil {
return err
}
return nil
}
// Start just updates WireGuard peer with the remote address
func (p *NoProxy) Start(remoteConn net.Conn) error {
log.Debugf("using NoProxy to connect to peer %s at %s", p.config.RemoteKey, remoteConn.RemoteAddr().String())
addr, err := net.ResolveUDPAddr("udp", remoteConn.RemoteAddr().String())
if err != nil {
return err
}
return p.config.WgInterface.UpdatePeer(p.config.RemoteKey, p.config.AllowedIps, DefaultWgKeepAlive,
addr, p.config.PreSharedKey)
}
func (p *NoProxy) Type() Type {
return TypeNoProxy
}

View File

@@ -1,35 +0,0 @@
package proxy
import (
"github.com/netbirdio/netbird/iface"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"io"
"net"
"time"
)
const DefaultWgKeepAlive = 25 * time.Second
type Type string
const (
TypeDirectNoProxy Type = "DirectNoProxy"
TypeWireGuard Type = "WireGuard"
TypeDummy Type = "Dummy"
TypeNoProxy Type = "NoProxy"
)
type Config struct {
WgListenAddr string
RemoteKey string
WgInterface *iface.WGIface
AllowedIps string
PreSharedKey *wgtypes.Key
}
type Proxy interface {
io.Closer
// Start creates a local remoteConn and starts proxying data from/to remoteConn
Start(remoteConn net.Conn) error
Type() Type
}

View File

@@ -1,128 +0,0 @@
package proxy
import (
"context"
log "github.com/sirupsen/logrus"
"net"
)
// WireGuardProxy proxies
type WireGuardProxy struct {
ctx context.Context
cancel context.CancelFunc
config Config
remoteConn net.Conn
localConn net.Conn
}
func NewWireGuardProxy(config Config) *WireGuardProxy {
p := &WireGuardProxy{config: config}
p.ctx, p.cancel = context.WithCancel(context.Background())
return p
}
func (p *WireGuardProxy) updateEndpoint() error {
udpAddr, err := net.ResolveUDPAddr(p.localConn.LocalAddr().Network(), p.localConn.LocalAddr().String())
if err != nil {
return err
}
// add local proxy connection as a Wireguard peer
err = p.config.WgInterface.UpdatePeer(p.config.RemoteKey, p.config.AllowedIps, DefaultWgKeepAlive,
udpAddr, p.config.PreSharedKey)
if err != nil {
return err
}
return nil
}
func (p *WireGuardProxy) Start(remoteConn net.Conn) error {
p.remoteConn = remoteConn
var err error
p.localConn, err = net.Dial("udp", p.config.WgListenAddr)
if err != nil {
log.Errorf("failed dialing to local Wireguard port %s", err)
return err
}
err = p.updateEndpoint()
if err != nil {
log.Errorf("error while updating Wireguard peer endpoint [%s] %v", p.config.RemoteKey, err)
return err
}
go p.proxyToRemote()
go p.proxyToLocal()
return nil
}
func (p *WireGuardProxy) Close() error {
p.cancel()
if c := p.localConn; c != nil {
err := p.localConn.Close()
if err != nil {
return err
}
}
err := p.config.WgInterface.RemovePeer(p.config.RemoteKey)
if err != nil {
return err
}
return nil
}
// proxyToRemote proxies everything from Wireguard to the RemoteKey peer
// blocks
func (p *WireGuardProxy) proxyToRemote() {
buf := make([]byte, 1500)
for {
select {
case <-p.ctx.Done():
log.Debugf("stopped proxying to remote peer %s due to closed connection", p.config.RemoteKey)
return
default:
n, err := p.localConn.Read(buf)
if err != nil {
continue
}
_, err = p.remoteConn.Write(buf[:n])
if err != nil {
continue
}
}
}
}
// proxyToLocal proxies everything from the RemoteKey peer to local Wireguard
// blocks
func (p *WireGuardProxy) proxyToLocal() {
buf := make([]byte, 1500)
for {
select {
case <-p.ctx.Done():
log.Debugf("stopped proxying from remote peer %s due to closed connection", p.config.RemoteKey)
return
default:
n, err := p.remoteConn.Read(buf)
if err != nil {
continue
}
_, err = p.localConn.Write(buf[:n])
if err != nil {
continue
}
}
}
}
func (p *WireGuardProxy) Type() Type {
return TypeWireGuard
}

View File

@@ -51,13 +51,17 @@ type iptablesManager struct {
func newIptablesManager(parentCtx context.Context) *iptablesManager {
ctx, cancel := context.WithCancel(parentCtx)
ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
if !isIptablesClientAvailable(ipv4Client) {
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
if err != nil {
log.Debugf("failed to initialize iptables for ipv4: %s", err)
} else if !isIptablesClientAvailable(ipv4Client) {
log.Infof("iptables is missing for ipv4")
ipv4Client = nil
}
ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6)
if !isIptablesClientAvailable(ipv6Client) {
ipv6Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
if err != nil {
log.Debugf("failed to initialize iptables for ipv6: %s", err)
} else if !isIptablesClientAvailable(ipv6Client) {
log.Infof("iptables is missing for ipv6")
ipv6Client = nil
}
@@ -391,6 +395,10 @@ func (i *iptablesManager) insertRoutingRule(keyFormat, table, chain, jump string
ipVersion = ipv6
}
if iptablesClient == nil {
return fmt.Errorf("unable to insert iptables routing rules. Iptables client is not initialized")
}
ruleKey := genKey(keyFormat, pair.ID)
rule := genRuleSpec(jump, ruleKey, pair.source, pair.destination)
existingRule, found := i.rules[ipVersion][ruleKey]
@@ -455,6 +463,10 @@ func (i *iptablesManager) removeRoutingRule(keyFormat, table, chain string, pair
ipVersion = ipv6
}
if iptablesClient == nil {
return fmt.Errorf("unable to remove iptables routing rules. Iptables client is not initialized")
}
ruleKey := genKey(keyFormat, pair.ID)
existingRule, found := i.rules[ipVersion][ruleKey]
if found {

View File

@@ -19,6 +19,9 @@ const (
nftablesTable = "netbird-rt"
nftablesRoutingForwardingChain = "netbird-rt-fwd"
nftablesRoutingNatChain = "netbird-rt-nat"
userDataAcceptForwardRuleSrc = "frwacceptsrc"
userDataAcceptForwardRuleDst = "frwacceptdst"
)
// constants needed to create nftable rules
@@ -71,25 +74,28 @@ var (
)
type nftablesManager struct {
ctx context.Context
stop context.CancelFunc
conn *nftables.Conn
tableIPv4 *nftables.Table
tableIPv6 *nftables.Table
chains map[string]map[string]*nftables.Chain
rules map[string]*nftables.Rule
mux sync.Mutex
ctx context.Context
stop context.CancelFunc
conn *nftables.Conn
tableIPv4 *nftables.Table
tableIPv6 *nftables.Table
chains map[string]map[string]*nftables.Chain
rules map[string]*nftables.Rule
filterTable *nftables.Table
defaultForwardRules []*nftables.Rule
mux sync.Mutex
}
func newNFTablesManager(parentCtx context.Context) (*nftablesManager, error) {
ctx, cancel := context.WithCancel(parentCtx)
mgr := &nftablesManager{
ctx: ctx,
stop: cancel,
conn: &nftables.Conn{},
chains: make(map[string]map[string]*nftables.Chain),
rules: make(map[string]*nftables.Rule),
ctx: ctx,
stop: cancel,
conn: &nftables.Conn{},
chains: make(map[string]map[string]*nftables.Chain),
rules: make(map[string]*nftables.Rule),
defaultForwardRules: make([]*nftables.Rule, 2),
}
err := mgr.isSupported()
@@ -97,6 +103,11 @@ func newNFTablesManager(parentCtx context.Context) (*nftablesManager, error) {
return nil, err
}
err = mgr.readFilterTable()
if err != nil {
return nil, err
}
return mgr, nil
}
@@ -109,6 +120,13 @@ func (n *nftablesManager) CleanRoutingRules() {
n.conn.FlushTable(n.tableIPv6)
n.conn.FlushTable(n.tableIPv4)
}
if n.defaultForwardRules[0] != nil {
err := n.eraseDefaultForwardRule()
if err != nil {
log.Errorf("failed to delete forward rule: %s", err)
}
}
log.Debugf("flushing tables result in: %v error", n.conn.Flush())
}
@@ -241,6 +259,112 @@ func (n *nftablesManager) refreshRulesMap() error {
return nil
}
func (n *nftablesManager) readFilterTable() error {
tables, err := n.conn.ListTables()
if err != nil {
return err
}
for _, t := range tables {
if t.Name == "filter" {
n.filterTable = t
return nil
}
}
return nil
}
func (n *nftablesManager) eraseDefaultForwardRule() error {
if n.defaultForwardRules[0] == nil {
return nil
}
err := n.refreshDefaultForwardRule()
if err != nil {
return err
}
for i, r := range n.defaultForwardRules {
err = n.conn.DelRule(r)
if err != nil {
log.Errorf("failed to delete forward rule (%d): %s", i, err)
}
n.defaultForwardRules[i] = nil
}
return nil
}
func (n *nftablesManager) refreshDefaultForwardRule() error {
rules, err := n.conn.GetRules(n.defaultForwardRules[0].Table, n.defaultForwardRules[0].Chain)
if err != nil {
return fmt.Errorf("unable to list rules in forward chain: %s", err)
}
found := false
for i, r := range n.defaultForwardRules {
for _, rule := range rules {
if string(rule.UserData) == string(r.UserData) {
n.defaultForwardRules[i] = rule
found = true
break
}
}
}
if !found {
return fmt.Errorf("unable to find forward accept rule")
}
return nil
}
func (n *nftablesManager) acceptForwardRule(sourceNetwork string) error {
src := generateCIDRMatcherExpressions("source", sourceNetwork)
dst := generateCIDRMatcherExpressions("destination", "0.0.0.0/0")
var exprs []expr.Any
exprs = append(src, append(dst, &expr.Verdict{
Kind: expr.VerdictAccept,
})...)
r := &nftables.Rule{
Table: n.filterTable,
Chain: &nftables.Chain{
Name: "FORWARD",
Table: n.filterTable,
Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookForward,
Priority: nftables.ChainPriorityFilter,
},
Exprs: exprs,
UserData: []byte(userDataAcceptForwardRuleSrc),
}
n.defaultForwardRules[0] = n.conn.AddRule(r)
src = generateCIDRMatcherExpressions("source", "0.0.0.0/0")
dst = generateCIDRMatcherExpressions("destination", sourceNetwork)
exprs = append(src, append(dst, &expr.Verdict{
Kind: expr.VerdictAccept,
})...)
r = &nftables.Rule{
Table: n.filterTable,
Chain: &nftables.Chain{
Name: "FORWARD",
Table: n.filterTable,
Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookForward,
Priority: nftables.ChainPriorityFilter,
},
Exprs: exprs,
UserData: []byte(userDataAcceptForwardRuleDst),
}
n.defaultForwardRules[1] = n.conn.AddRule(r)
return nil
}
// checkOrCreateDefaultForwardingRules checks if the default forwarding rules are enabled
func (n *nftablesManager) checkOrCreateDefaultForwardingRules() {
_, foundIPv4 := n.rules[ipv4Forwarding]
@@ -294,6 +418,14 @@ func (n *nftablesManager) InsertRoutingRules(pair routerPair) error {
}
}
if n.defaultForwardRules[0] == nil && n.filterTable != nil {
err = n.acceptForwardRule(pair.source)
if err != nil {
log.Errorf("unable to create default forward rule: %s", err)
}
log.Debugf("default accept forward rule added")
}
err = n.conn.Flush()
if err != nil {
return fmt.Errorf("nftables: unable to insert rules for %s: %v", pair.destination, err)
@@ -374,6 +506,13 @@ func (n *nftablesManager) RemoveRoutingRules(pair routerPair) error {
return err
}
if len(n.rules) == 2 && n.defaultForwardRules[0] != nil {
err := n.eraseDefaultForwardRule()
if err != nil {
log.Errorf("failed to delte default fwd rule: %s", err)
}
}
err = n.conn.Flush()
if err != nil {
return fmt.Errorf("nftables: received error while applying rule removal for %s: %v", pair.destination, err)

View File

@@ -0,0 +1,8 @@
package templates
import (
_ "embed"
)
//go:embed pkce-auth-msg.html
var PKCEAuthMsgTmpl string

View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: #f7f8f9;
font-family: sans-serif, Arial, Tahoma;
}
.container {
width: 100%;
background: white;
border: 1px solid #e8e9ea;
text-align: center;
padding: 20px;
padding-bottom: 50px;
max-width: 550px;
margin: 0 10px;
}
.logo {
height: 80px;
border-bottom: 1px solid #e8e9ea;
display: flex;
justify-content: center;
align-items: center;
}
.logo img {
width: 130px;
}
.content {
font-size: 13px;
color: #525252;
line-height: 18px;
padding: 10px 0;
}
.content div {
font-size: 18px;
line-height: normal;
margin-bottom: 5px;
color: black;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">
<img src="https://img.mailinblue.com/6211297/images/content_library/original/64bd4ce82e1ea753e439b6a2.png">
</div>
<br>
{{ if .Error }}
<svg xmlns="http://www.w3.org/2000/svg" height="50" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="none" stroke="red" stroke-width="3"/>
<path d="M30 30 L70 70 M30 70 L70 30" fill="none" stroke="red" stroke-width="3"/>
</svg>
<div class="content">
<div>
Login failed
</div>
{{ .Error }}.
</div>
{{ else }}
<svg xmlns="http://www.w3.org/2000/svg" height="50" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="none" stroke="#5cb85c" stroke-width="3"/>
<path d="M30 50 L45 65 L70 35" fill="none" stroke="#5cb85c" stroke-width="5"/>
</svg>
<div class="content">
<div>
Login successful
</div>
Your device is now registered and logged in to NetBird.
<br>
You can now close this window.
</div>
{{ end }}
</div>
</body>
</html>

View File

@@ -0,0 +1,120 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
package ebpf
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
// loadBpf returns the embedded CollectionSpec for bpf.
func loadBpf() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_BpfBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load bpf: %w", err)
}
return spec, err
}
// loadBpfObjects loads bpf and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *bpfObjects
// *bpfPrograms
// *bpfMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadBpf()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// bpfSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfSpecs struct {
bpfProgramSpecs
bpfMapSpecs
}
// bpfSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfProgramSpecs struct {
NbWgProxy *ebpf.ProgramSpec `ebpf:"nb_wg_proxy"`
}
// bpfMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfMapSpecs struct {
NbWgProxySettingsMap *ebpf.MapSpec `ebpf:"nb_wg_proxy_settings_map"`
}
// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
bpfPrograms
bpfMaps
}
func (o *bpfObjects) Close() error {
return _BpfClose(
&o.bpfPrograms,
&o.bpfMaps,
)
}
// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
NbWgProxySettingsMap *ebpf.Map `ebpf:"nb_wg_proxy_settings_map"`
}
func (m *bpfMaps) Close() error {
return _BpfClose(
m.NbWgProxySettingsMap,
)
}
// bpfPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfPrograms struct {
NbWgProxy *ebpf.Program `ebpf:"nb_wg_proxy"`
}
func (p *bpfPrograms) Close() error {
return _BpfClose(
p.NbWgProxy,
)
}
func _BpfClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed bpf_bpfeb.o
var _BpfBytes []byte

Binary file not shown.

View File

@@ -0,0 +1,120 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
package ebpf
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
// loadBpf returns the embedded CollectionSpec for bpf.
func loadBpf() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_BpfBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load bpf: %w", err)
}
return spec, err
}
// loadBpfObjects loads bpf and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *bpfObjects
// *bpfPrograms
// *bpfMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadBpf()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// bpfSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfSpecs struct {
bpfProgramSpecs
bpfMapSpecs
}
// bpfSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfProgramSpecs struct {
NbWgProxy *ebpf.ProgramSpec `ebpf:"nb_wg_proxy"`
}
// bpfMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfMapSpecs struct {
NbWgProxySettingsMap *ebpf.MapSpec `ebpf:"nb_wg_proxy_settings_map"`
}
// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
bpfPrograms
bpfMaps
}
func (o *bpfObjects) Close() error {
return _BpfClose(
&o.bpfPrograms,
&o.bpfMaps,
)
}
// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
NbWgProxySettingsMap *ebpf.Map `ebpf:"nb_wg_proxy_settings_map"`
}
func (m *bpfMaps) Close() error {
return _BpfClose(
m.NbWgProxySettingsMap,
)
}
// bpfPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfPrograms struct {
NbWgProxy *ebpf.Program `ebpf:"nb_wg_proxy"`
}
func (p *bpfPrograms) Close() error {
return _BpfClose(
p.NbWgProxy,
)
}
func _BpfClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed bpf_bpfel.o
var _BpfBytes []byte

Binary file not shown.

View File

@@ -0,0 +1,84 @@
//go:build linux && !android
package ebpf
import (
_ "embed"
"net"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
const (
mapKeyProxyPort uint32 = 0
mapKeyWgPort uint32 = 1
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang-14 bpf src/portreplace.c --
// EBPF is a wrapper for eBPF program
type EBPF struct {
link link.Link
}
// NewEBPF create new EBPF instance
func NewEBPF() *EBPF {
return &EBPF{}
}
// Load load ebpf program
func (l *EBPF) Load(proxyPort, wgPort int) error {
// it required for Docker
err := rlimit.RemoveMemlock()
if err != nil {
return err
}
ifce, err := net.InterfaceByName("lo")
if err != nil {
return err
}
// Load pre-compiled programs into the kernel.
objs := bpfObjects{}
err = loadBpfObjects(&objs, nil)
if err != nil {
return err
}
defer func() {
_ = objs.Close()
}()
err = objs.NbWgProxySettingsMap.Put(mapKeyProxyPort, uint16(proxyPort))
if err != nil {
return err
}
err = objs.NbWgProxySettingsMap.Put(mapKeyWgPort, uint16(wgPort))
if err != nil {
return err
}
defer func() {
_ = objs.NbWgProxySettingsMap.Close()
}()
l.link, err = link.AttachXDP(link.XDPOptions{
Program: objs.NbWgProxy,
Interface: ifce.Index,
})
if err != nil {
return err
}
return err
}
// Free ebpf program
func (l *EBPF) Free() error {
if l.link != nil {
return l.link.Close()
}
return nil
}

View File

@@ -0,0 +1,18 @@
//go:build linux
package ebpf
import (
"testing"
)
func Test_newEBPF(t *testing.T) {
ebpf := NewEBPF()
err := ebpf.Load(1234, 51892)
defer func() {
_ = ebpf.Free()
}()
if err != nil {
t.Errorf("%s", err)
}
}

View File

@@ -0,0 +1,90 @@
#include <stdbool.h>
#include <linux/if_ether.h> // ETH_P_IP
#include <linux/udp.h>
#include <linux/ip.h>
#include <netinet/in.h>
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#define bpf_printk(fmt, ...) \
({ \
char ____fmt[] = fmt; \
bpf_trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \
})
const __u32 map_key_proxy_port = 0;
const __u32 map_key_wg_port = 1;
struct bpf_map_def SEC("maps") nb_wg_proxy_settings_map = {
.type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(__u32),
.value_size = sizeof(__u16),
.max_entries = 10,
};
__u16 proxy_port = 0;
__u16 wg_port = 0;
bool read_port_settings() {
__u16 *value;
value = bpf_map_lookup_elem(&nb_wg_proxy_settings_map, &map_key_proxy_port);
if(!value) {
return false;
}
proxy_port = *value;
value = bpf_map_lookup_elem(&nb_wg_proxy_settings_map, &map_key_wg_port);
if(!value) {
return false;
}
wg_port = *value;
return true;
}
SEC("xdp")
int nb_wg_proxy(struct xdp_md *ctx) {
if(proxy_port == 0 || wg_port == 0) {
if(!read_port_settings()){
return XDP_PASS;
}
bpf_printk("proxy port: %d, wg port: %d", proxy_port, wg_port);
}
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
struct iphdr *ip = (data + sizeof(struct ethhdr));
struct udphdr *udp = (data + sizeof(struct ethhdr) + sizeof(struct iphdr));
// return early if not enough data
if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr) > data_end){
return XDP_PASS;
}
// skip non IPv4 packages
if (eth->h_proto != htons(ETH_P_IP)) {
return XDP_PASS;
}
if (ip->protocol != IPPROTO_UDP) {
return XDP_PASS;
}
// 2130706433 = 127.0.0.1
if (ip->daddr != htonl(2130706433)) {
return XDP_PASS;
}
if (udp->source != htons(wg_port)){
return XDP_PASS;
}
__be16 new_src_port = udp->dest;
__be16 new_dst_port = htons(proxy_port);
udp->dest = new_dst_port;
udp->source = new_src_port;
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";

View File

@@ -0,0 +1,20 @@
package wgproxy
type Factory struct {
wgPort int
ebpfProxy Proxy
}
func (w *Factory) GetProxy() Proxy {
if w.ebpfProxy != nil {
return w.ebpfProxy
}
return NewWGUserSpaceProxy(w.wgPort)
}
func (w *Factory) Free() error {
if w.ebpfProxy != nil {
return w.ebpfProxy.Free()
}
return nil
}

View File

@@ -0,0 +1,21 @@
//go:build !android
package wgproxy
import (
log "github.com/sirupsen/logrus"
)
func NewFactory(wgPort int) *Factory {
f := &Factory{wgPort: wgPort}
ebpfProxy := NewWGEBPFProxy(wgPort)
err := ebpfProxy.Listen()
if err != nil {
log.Warnf("failed to initialize ebpf proxy, fallback to user space proxy: %s", err)
return f
}
f.ebpfProxy = ebpfProxy
return f
}

View File

@@ -0,0 +1,7 @@
//go:build !linux || android
package wgproxy
func NewFactory(wgPort int) *Factory {
return &Factory{wgPort: wgPort}
}

View File

@@ -0,0 +1,32 @@
package wgproxy
import (
"fmt"
"net"
)
const (
portRangeStart = 3128
portRangeEnd = 3228
)
type portLookup struct {
}
func (pl portLookup) searchFreePort() (int, error) {
for i := portRangeStart; i <= portRangeEnd; i++ {
if pl.tryToBind(i) == nil {
return i, nil
}
}
return 0, fmt.Errorf("failed to bind free port for eBPF proxy")
}
func (pl portLookup) tryToBind(port int) error {
l, err := net.ListenPacket("udp", fmt.Sprintf(":%d", port))
if err != nil {
return err
}
_ = l.Close()
return nil
}

View File

@@ -0,0 +1,42 @@
package wgproxy
import (
"fmt"
"net"
"testing"
)
func Test_portLookup_searchFreePort(t *testing.T) {
pl := portLookup{}
_, err := pl.searchFreePort()
if err != nil {
t.Fatal(err)
}
}
func Test_portLookup_on_allocated(t *testing.T) {
pl := portLookup{}
allocatedPort, err := allocatePort(portRangeStart)
if err != nil {
t.Fatal(err)
}
defer allocatedPort.Close()
fp, err := pl.searchFreePort()
if err != nil {
t.Fatal(err)
}
if fp != (portRangeStart + 1) {
t.Errorf("invalid free port, expected: %d, got: %d", portRangeStart+1, fp)
}
}
func allocatePort(port int) (net.PacketConn, error) {
c, err := net.ListenPacket("udp", fmt.Sprintf(":%d", port))
if err != nil {
return nil, err
}
return c, err
}

View File

@@ -0,0 +1,12 @@
package wgproxy
import (
"net"
)
// Proxy is a transfer layer between the Turn connection and the WireGuard
type Proxy interface {
AddTurnConn(urnConn net.Conn) (net.Addr, error)
CloseConn() error
Free() error
}

View File

@@ -0,0 +1,255 @@
//go:build linux && !android
package wgproxy
import (
"fmt"
"io"
"net"
"os"
"sync"
"syscall"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
log "github.com/sirupsen/logrus"
ebpf2 "github.com/netbirdio/netbird/client/internal/wgproxy/ebpf"
)
// WGEBPFProxy definition for proxy with EBPF support
type WGEBPFProxy struct {
ebpf *ebpf2.EBPF
lastUsedPort uint16
localWGListenPort int
turnConnStore map[uint16]net.Conn
turnConnMutex sync.Mutex
rawConn net.PacketConn
conn *net.UDPConn
}
// NewWGEBPFProxy create new WGEBPFProxy instance
func NewWGEBPFProxy(wgPort int) *WGEBPFProxy {
log.Debugf("instantiate ebpf proxy")
wgProxy := &WGEBPFProxy{
localWGListenPort: wgPort,
ebpf: ebpf2.NewEBPF(),
lastUsedPort: 0,
turnConnStore: make(map[uint16]net.Conn),
}
return wgProxy
}
// Listen load ebpf program and listen the proxy
func (p *WGEBPFProxy) Listen() error {
pl := portLookup{}
wgPorxyPort, err := pl.searchFreePort()
if err != nil {
return err
}
p.rawConn, err = p.prepareSenderRawSocket()
if err != nil {
return err
}
err = p.ebpf.Load(wgPorxyPort, p.localWGListenPort)
if err != nil {
return err
}
addr := net.UDPAddr{
Port: wgPorxyPort,
IP: net.ParseIP("127.0.0.1"),
}
p.conn, err = net.ListenUDP("udp", &addr)
if err != nil {
cErr := p.Free()
if cErr != nil {
log.Errorf("failed to close the wgproxy: %s", cErr)
}
return err
}
go p.proxyToRemote()
log.Infof("local wg proxy listening on: %d", wgPorxyPort)
return nil
}
// AddTurnConn add new turn connection for the proxy
func (p *WGEBPFProxy) AddTurnConn(turnConn net.Conn) (net.Addr, error) {
wgEndpointPort, err := p.storeTurnConn(turnConn)
if err != nil {
return nil, err
}
go p.proxyToLocal(wgEndpointPort, turnConn)
log.Infof("turn conn added to wg proxy store: %s, endpoint port: :%d", turnConn.RemoteAddr(), wgEndpointPort)
wgEndpoint := &net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: int(wgEndpointPort),
}
return wgEndpoint, nil
}
// CloseConn doing nothing because this type of proxy implementation does not store the connection
func (p *WGEBPFProxy) CloseConn() error {
return nil
}
// Free resources
func (p *WGEBPFProxy) Free() error {
log.Debugf("free up ebpf wg proxy")
var err1, err2, err3 error
if p.conn != nil {
err1 = p.conn.Close()
}
err2 = p.ebpf.Free()
if p.rawConn != nil {
err3 = p.rawConn.Close()
}
if err1 != nil {
return err1
}
if err2 != nil {
return err2
}
return err3
}
func (p *WGEBPFProxy) proxyToLocal(endpointPort uint16, remoteConn net.Conn) {
buf := make([]byte, 1500)
for {
n, err := remoteConn.Read(buf)
if err != nil {
if err != io.EOF {
log.Errorf("failed to read from turn conn (endpoint: :%d): %s", endpointPort, err)
}
p.removeTurnConn(endpointPort)
return
}
err = p.sendPkg(buf[:n], endpointPort)
if err != nil {
log.Errorf("failed to write out turn pkg to local conn: %v", err)
}
}
}
// proxyToRemote read messages from local WireGuard interface and forward it to remote conn
func (p *WGEBPFProxy) proxyToRemote() {
buf := make([]byte, 1500)
for {
n, addr, err := p.conn.ReadFromUDP(buf)
if err != nil {
log.Errorf("failed to read UDP pkg from WG: %s", err)
return
}
p.turnConnMutex.Lock()
conn, ok := p.turnConnStore[uint16(addr.Port)]
p.turnConnMutex.Unlock()
if !ok {
log.Errorf("turn conn not found by port: %d", addr.Port)
continue
}
_, err = conn.Write(buf[:n])
if err != nil {
log.Debugf("failed to forward local wg pkg (%d) to remote turn conn: %s", addr.Port, err)
}
}
}
func (p *WGEBPFProxy) storeTurnConn(turnConn net.Conn) (uint16, error) {
p.turnConnMutex.Lock()
defer p.turnConnMutex.Unlock()
np, err := p.nextFreePort()
if err != nil {
return np, err
}
p.turnConnStore[np] = turnConn
return np, nil
}
func (p *WGEBPFProxy) removeTurnConn(turnConnID uint16) {
log.Tracef("remove turn conn from store by port: %d", turnConnID)
p.turnConnMutex.Lock()
defer p.turnConnMutex.Unlock()
delete(p.turnConnStore, turnConnID)
}
func (p *WGEBPFProxy) nextFreePort() (uint16, error) {
if len(p.turnConnStore) == 65535 {
return 0, fmt.Errorf("reached maximum turn connection numbers")
}
generatePort:
if p.lastUsedPort == 65535 {
p.lastUsedPort = 1
} else {
p.lastUsedPort++
}
if _, ok := p.turnConnStore[p.lastUsedPort]; ok {
goto generatePort
}
return p.lastUsedPort, nil
}
func (p *WGEBPFProxy) prepareSenderRawSocket() (net.PacketConn, error) {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil {
return nil, err
}
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)
if err != nil {
return nil, err
}
err = syscall.SetsockoptString(fd, syscall.SOL_SOCKET, syscall.SO_BINDTODEVICE, "lo")
if err != nil {
return nil, err
}
return net.FilePacketConn(os.NewFile(uintptr(fd), fmt.Sprintf("fd %d", fd)))
}
func (p *WGEBPFProxy) sendPkg(data []byte, port uint16) error {
localhost := net.ParseIP("127.0.0.1")
payload := gopacket.Payload(data)
ipH := &layers.IPv4{
DstIP: localhost,
SrcIP: localhost,
Version: 4,
TTL: 64,
Protocol: layers.IPProtocolUDP,
}
udpH := &layers.UDP{
SrcPort: layers.UDPPort(port),
DstPort: layers.UDPPort(p.localWGListenPort),
}
err := udpH.SetNetworkLayerForChecksum(ipH)
if err != nil {
return err
}
layerBuffer := gopacket.NewSerializeBuffer()
err = gopacket.SerializeLayers(layerBuffer, gopacket.SerializeOptions{ComputeChecksums: true, FixLengths: true}, ipH, udpH, payload)
if err != nil {
return err
}
_, err = p.rawConn.WriteTo(layerBuffer.Bytes(), &net.IPAddr{IP: localhost})
return err
}

View File

@@ -0,0 +1,56 @@
//go:build linux && !android
package wgproxy
import (
"testing"
)
func TestWGEBPFProxy_connStore(t *testing.T) {
wgProxy := NewWGEBPFProxy(1)
p, _ := wgProxy.storeTurnConn(nil)
if p != 1 {
t.Errorf("invalid initial port: %d", wgProxy.lastUsedPort)
}
numOfConns := 10
for i := 0; i < numOfConns; i++ {
p, _ = wgProxy.storeTurnConn(nil)
}
if p != uint16(numOfConns)+1 {
t.Errorf("invalid last used port: %d, expected: %d", p, numOfConns+1)
}
if len(wgProxy.turnConnStore) != numOfConns+1 {
t.Errorf("invalid store size: %d, expected: %d", len(wgProxy.turnConnStore), numOfConns+1)
}
}
func TestWGEBPFProxy_portCalculation_overflow(t *testing.T) {
wgProxy := NewWGEBPFProxy(1)
_, _ = wgProxy.storeTurnConn(nil)
wgProxy.lastUsedPort = 65535
p, _ := wgProxy.storeTurnConn(nil)
if len(wgProxy.turnConnStore) != 2 {
t.Errorf("invalid store size: %d, expected: %d", len(wgProxy.turnConnStore), 2)
}
if p != 2 {
t.Errorf("invalid last used port: %d, expected: %d", p, 2)
}
}
func TestWGEBPFProxy_portCalculation_maxConn(t *testing.T) {
wgProxy := NewWGEBPFProxy(1)
for i := 0; i < 65535; i++ {
_, _ = wgProxy.storeTurnConn(nil)
}
_, err := wgProxy.storeTurnConn(nil)
if err == nil {
t.Errorf("invalid turn conn store calculation")
}
}

View File

@@ -0,0 +1,106 @@
package wgproxy
import (
"context"
"fmt"
"net"
log "github.com/sirupsen/logrus"
)
// WGUserSpaceProxy proxies
type WGUserSpaceProxy struct {
localWGListenPort int
ctx context.Context
cancel context.CancelFunc
remoteConn net.Conn
localConn net.Conn
}
// NewWGUserSpaceProxy instantiate a user space WireGuard proxy
func NewWGUserSpaceProxy(wgPort int) *WGUserSpaceProxy {
log.Debugf("instantiate new userspace proxy")
p := &WGUserSpaceProxy{
localWGListenPort: wgPort,
}
p.ctx, p.cancel = context.WithCancel(context.Background())
return p
}
// AddTurnConn start the proxy with the given remote conn
func (p *WGUserSpaceProxy) AddTurnConn(remoteConn net.Conn) (net.Addr, error) {
p.remoteConn = remoteConn
var err error
p.localConn, err = net.Dial("udp", fmt.Sprintf(":%d", p.localWGListenPort))
if err != nil {
log.Errorf("failed dialing to local Wireguard port %s", err)
return nil, err
}
go p.proxyToRemote()
go p.proxyToLocal()
return p.localConn.LocalAddr(), err
}
// CloseConn close the localConn
func (p *WGUserSpaceProxy) CloseConn() error {
p.cancel()
if p.localConn == nil {
return nil
}
return p.localConn.Close()
}
// Free doing nothing because this implementation of proxy does not have global state
func (p *WGUserSpaceProxy) Free() error {
return nil
}
// proxyToRemote proxies everything from Wireguard to the RemoteKey peer
// blocks
func (p *WGUserSpaceProxy) proxyToRemote() {
buf := make([]byte, 1500)
for {
select {
case <-p.ctx.Done():
return
default:
n, err := p.localConn.Read(buf)
if err != nil {
continue
}
_, err = p.remoteConn.Write(buf[:n])
if err != nil {
continue
}
}
}
}
// proxyToLocal proxies everything from the RemoteKey peer to local Wireguard
// blocks
func (p *WGUserSpaceProxy) proxyToLocal() {
buf := make([]byte, 1500)
for {
select {
case <-p.ctx.Done():
return
default:
n, err := p.remoteConn.Read(buf)
if err != nil {
continue
}
_, err = p.localConn.Write(buf[:n])
if err != nil {
continue
}
}
}
}

View File

@@ -3,6 +3,7 @@ package server
import (
"context"
"fmt"
"github.com/netbirdio/netbird/client/internal/auth"
"sync"
"time"
@@ -38,8 +39,8 @@ type Server struct {
type oauthAuthFlow struct {
expiresAt time.Time
client internal.OAuthClient
info internal.DeviceAuthInfo
flow auth.OAuthFlow
info auth.AuthFlowInfo
waitCancel context.CancelFunc
}
@@ -206,28 +207,15 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
state.Set(internal.StatusConnecting)
if msg.SetupKey == "" {
providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
oAuthFlow, err := auth.NewOAuthFlow(ctx, config)
if err != nil {
state.Set(internal.StatusLoginFailed)
s, ok := gstatus.FromError(err)
if ok && s.Code() == codes.NotFound {
return nil, gstatus.Errorf(codes.NotFound, "no SSO provider returned from management. "+
"If you are using hosting Netbird see documentation at "+
"https://github.com/netbirdio/netbird/tree/main/management for details")
} else if ok && s.Code() == codes.Unimplemented {
return nil, gstatus.Errorf(codes.Unimplemented, "the management server, %s, does not support SSO providers, "+
"please update your server or use Setup Keys to login", config.ManagementURL)
} else {
log.Errorf("getting device authorization flow info failed with error: %v", err)
return nil, err
}
return nil, err
}
hostedClient := internal.NewHostedDeviceFlow(providerConfig.ProviderConfig)
if s.oauthAuthFlow.client != nil && s.oauthAuthFlow.client.GetClientID(ctx) == hostedClient.GetClientID(context.TODO()) {
if s.oauthAuthFlow.flow != nil && s.oauthAuthFlow.flow.GetClientID(ctx) == oAuthFlow.GetClientID(context.TODO()) {
if s.oauthAuthFlow.expiresAt.After(time.Now().Add(90 * time.Second)) {
log.Debugf("using previous device flow info")
log.Debugf("using previous oauth flow info")
return &proto.LoginResponse{
NeedsSSOLogin: true,
VerificationURI: s.oauthAuthFlow.info.VerificationURI,
@@ -242,25 +230,25 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
}
}
deviceAuthInfo, err := hostedClient.RequestDeviceCode(context.TODO())
authInfo, err := oAuthFlow.RequestAuthInfo(context.TODO())
if err != nil {
log.Errorf("getting a request device code failed: %v", err)
log.Errorf("getting a request OAuth flow failed: %v", err)
return nil, err
}
s.mutex.Lock()
s.oauthAuthFlow.client = hostedClient
s.oauthAuthFlow.info = deviceAuthInfo
s.oauthAuthFlow.expiresAt = time.Now().Add(time.Duration(deviceAuthInfo.ExpiresIn) * time.Second)
s.oauthAuthFlow.flow = oAuthFlow
s.oauthAuthFlow.info = authInfo
s.oauthAuthFlow.expiresAt = time.Now().Add(time.Duration(authInfo.ExpiresIn) * time.Second)
s.mutex.Unlock()
state.Set(internal.StatusNeedsLogin)
return &proto.LoginResponse{
NeedsSSOLogin: true,
VerificationURI: deviceAuthInfo.VerificationURI,
VerificationURIComplete: deviceAuthInfo.VerificationURIComplete,
UserCode: deviceAuthInfo.UserCode,
VerificationURI: authInfo.VerificationURI,
VerificationURIComplete: authInfo.VerificationURIComplete,
UserCode: authInfo.UserCode,
}, nil
}
@@ -289,8 +277,8 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
s.actCancel = cancel
s.mutex.Unlock()
if s.oauthAuthFlow.client == nil {
return nil, gstatus.Errorf(codes.Internal, "oauth client is not initialized")
if s.oauthAuthFlow.flow == nil {
return nil, gstatus.Errorf(codes.Internal, "oauth flow is not initialized")
}
state := internal.CtxGetState(ctx)
@@ -304,10 +292,10 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
state.Set(internal.StatusConnecting)
s.mutex.Lock()
deviceAuthInfo := s.oauthAuthFlow.info
flowInfo := s.oauthAuthFlow.info
s.mutex.Unlock()
if deviceAuthInfo.UserCode != msg.UserCode {
if flowInfo.UserCode != msg.UserCode {
state.Set(internal.StatusLoginFailed)
return nil, gstatus.Errorf(codes.InvalidArgument, "sso user code is invalid")
}
@@ -324,7 +312,7 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
s.oauthAuthFlow.waitCancel = cancel
s.mutex.Unlock()
tokenInfo, err := s.oauthAuthFlow.client.WaitToken(waitCTX, deviceAuthInfo)
tokenInfo, err := s.oauthAuthFlow.flow.WaitToken(waitCTX, flowInfo)
if err != nil {
if err == context.Canceled {
return nil, nil

5
go.mod
View File

@@ -30,7 +30,8 @@ require (
require (
fyne.io/fyne/v2 v2.1.4
github.com/c-robinson/iplib v1.0.3
github.com/coreos/go-iptables v0.6.0
github.com/cilium/ebpf v0.10.0
github.com/coreos/go-iptables v0.7.0
github.com/creack/pty v1.1.18
github.com/eko/gocache/v3 v3.1.1
github.com/getlantern/systray v1.2.1
@@ -48,6 +49,7 @@ require (
github.com/mdlayher/socket v0.4.0
github.com/miekg/dns v1.1.43
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/nadoo/ipset v0.5.0
github.com/okta/okta-sdk-golang/v2 v2.18.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pion/logging v0.2.2
@@ -124,7 +126,6 @@ require (
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect

16
go.sum
View File

@@ -101,6 +101,8 @@ github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmE
github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
github.com/cilium/ebpf v0.10.0 h1:nk5HPMeoBXtOzbkZBWym+ZWq1GIiHUsBFXxwewXAHLQ=
github.com/cilium/ebpf v0.10.0/go.mod h1:DPiVdY/kT534dgc9ERmvP8mWA+9gvwgKfRvk4nNWnoE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -129,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/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/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk=
github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
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-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=
@@ -177,7 +179,7 @@ github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQL
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA=
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -419,7 +421,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -485,6 +487,8 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nadoo/ipset v0.5.0 h1:5GJUAuZ7ITQQQGne5J96AmFjRtI8Avlbk6CabzYWVUc=
github.com/nadoo/ipset v0.5.0/go.mod h1:rYF5DQLRGGoQ8ZSWeK+6eX5amAuPqwFkWjhQlEITGJQ=
github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0 h1:hirFRfx3grVA/9eEyjME5/z3nxdJlN9kfQpvWWPk32g=
github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
github.com/netbirdio/systray v0.0.0-20221012095658-dc8eda872c0c h1:wK/s4nyZj/GF/kFJQjX6nqNfE0G3gcqd6hhnPCyp4sw=
@@ -550,7 +554,6 @@ github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI=
github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -594,8 +597,7 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so=
github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM=
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=

View File

@@ -82,7 +82,7 @@ func (m *UniversalUDPMuxDefault) ReadFromConn(ctx context.Context) {
default:
_, a, err := m.params.UDPConn.ReadFrom(buf)
if err != nil {
log.Errorf("error while reading packet %s", err)
log.Errorf("error while reading packet: %s", err)
continue
}
msg := &stun.Message{

View File

@@ -74,7 +74,7 @@ func (w *WGIface) UpdatePeer(peerKey string, allowedIps string, keepAlive time.D
w.mu.Lock()
defer w.mu.Unlock()
log.Debugf("updating interface %s peer %s: endpoint %s ", w.tun.DeviceName(), peerKey, endpoint)
log.Debugf("updating interface %s peer %s, endpoint %s ", w.tun.DeviceName(), peerKey, endpoint)
return w.configurer.updatePeer(peerKey, allowedIps, keepAlive, endpoint, preSharedKey)
}

View File

@@ -43,6 +43,10 @@ NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN=${NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN:-f
NETBIRD_DISABLE_ANONYMOUS_METRICS=${NETBIRD_DISABLE_ANONYMOUS_METRICS:-false}
NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
# PKCE authorization flow
NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS=${NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS:-"53000"}
NETBIRD_AUTH_PKCE_USE_ID_TOKEN=${NETBIRD_AUTH_PKCE_USE_ID_TOKEN:-false}
# exports
export NETBIRD_DOMAIN
export NETBIRD_AUTH_CLIENT_ID
@@ -80,4 +84,6 @@ export NETBIRD_AUTH_USER_ID_CLAIM
export NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE
export NETBIRD_TOKEN_SOURCE
export NETBIRD_AUTH_DEVICE_AUTH_SCOPE
export NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN
export NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN
export NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT
export NETBIRD_AUTH_PKCE_USE_ID_TOKEN

View File

@@ -99,12 +99,17 @@ export NETBIRD_AUTH_AUTHORITY=$(jq -r '.issuer' openid-configuration.json)
export NETBIRD_AUTH_JWT_CERTS=$(jq -r '.jwks_uri' openid-configuration.json)
export NETBIRD_AUTH_TOKEN_ENDPOINT=$(jq -r '.token_endpoint' openid-configuration.json)
export NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT=$(jq -r '.device_authorization_endpoint' openid-configuration.json)
export NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT=$(jq -r '.authorization_endpoint' openid-configuration.json)
if [[ ! -z "${NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID}" ]]; then
# user enabled Device Authorization Grant feature
export NETBIRD_AUTH_DEVICE_AUTH_PROVIDER="hosted"
fi
if [ "$NETBIRD_TOKEN_SOURCE" = "idToken" ]; then
export NETBIRD_AUTH_PKCE_USE_ID_TOKEN=true
fi
# Check if letsencrypt was disabled
if [[ "$NETBIRD_DISABLE_LETSENCRYPT" == "true" ]]; then
export NETBIRD_DASHBOARD_ENDPOINT="https://$NETBIRD_DOMAIN:443"
@@ -151,6 +156,14 @@ if [ -n "$NETBIRD_MGMT_IDP" ]; then
export NETBIRD_IDP_MGMT_EXTRA_CONFIG=$EXTRA_CONFIG
fi
IFS=',' read -r -a REDIRECT_URL_PORTS <<< "$NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS"
REDIRECT_URLS=""
for port in "${REDIRECT_URL_PORTS[@]}"; do
REDIRECT_URLS+="\"http://localhost:${port}\","
done
export NETBIRD_AUTH_PKCE_REDIRECT_URLS=${REDIRECT_URLS%,}
env | grep NETBIRD
envsubst <docker-compose.yml.tmpl >docker-compose.yml

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

@@ -59,5 +59,17 @@
"Scope": "$NETBIRD_AUTH_DEVICE_AUTH_SCOPE",
"UseIDToken": $NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN
}
},
"PKCEAuthorizationFlow": {
"ProviderConfig": {
"Audience": "$NETBIRD_AUTH_AUDIENCE",
"ClientID": "$NETBIRD_AUTH_CLIENT_ID",
"ClientSecret": "$NETBIRD_AUTH_CLIENT_SECRET",
"AuthorizationEndpoint": "$NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT",
"TokenEndpoint": "$NETBIRD_AUTH_TOKEN_ENDPOINT",
"Scope": "$NETBIRD_AUTH_SUPPORTED_SCOPES",
"RedirectURLs": [$NETBIRD_AUTH_PKCE_REDIRECT_URLS],
"UseIDToken": $NETBIRD_AUTH_PKCE_USE_ID_TOKEN
}
}
}

View File

@@ -37,6 +37,12 @@ NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
NETBIRD_AUTH_DEVICE_AUTH_SCOPE="openid"
NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN=false
# -------------------------------------------
# OIDC PKCE Authorization Flow
# -------------------------------------------
# Comma separated port numbers. if already in use, PKCE flow will choose an available port from the list as an alternative
# eg. 53000,54000
NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS="53000"
# -------------------------------------------
# IDP Management
# -------------------------------------------
# eg. zitadel, auth0, azure, keycloak

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

@@ -15,5 +15,6 @@ type Client interface {
Register(serverKey wgtypes.Key, setupKey string, jwtToken string, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error)
Login(serverKey wgtypes.Key, sysInfo *system.Info, sshKey []byte) (*proto.LoginResponse, error)
GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error)
GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error)
GetNetworkMap() (*proto.NetworkMap, error)
}

View File

@@ -400,3 +400,49 @@ func Test_GetDeviceAuthorizationFlow(t *testing.T) {
assert.Equal(t, expectedFlowInfo.Provider, flowInfo.Provider, "provider should match")
assert.Equal(t, expectedFlowInfo.ProviderConfig.ClientID, flowInfo.ProviderConfig.ClientID, "provider configured client ID should match")
}
func Test_GetPKCEAuthorizationFlow(t *testing.T) {
s, lis, mgmtMockServer, serverKey := startMockManagement(t)
defer s.GracefulStop()
testKey, err := wgtypes.GenerateKey()
if err != nil {
log.Fatal(err)
}
serverAddr := lis.Addr().String()
ctx := context.Background()
client, err := NewClient(ctx, serverAddr, testKey, false)
if err != nil {
log.Fatalf("error while creating testClient: %v", err)
}
expectedFlowInfo := &proto.PKCEAuthorizationFlow{
ProviderConfig: &proto.ProviderConfig{
ClientID: "client",
ClientSecret: "secret",
},
}
mgmtMockServer.GetPKCEAuthorizationFlowFunc = func(ctx context.Context, req *mgmtProto.EncryptedMessage) (*proto.EncryptedMessage, error) {
encryptedResp, err := encryption.EncryptMessage(serverKey, client.key, expectedFlowInfo)
if err != nil {
return nil, err
}
return &mgmtProto.EncryptedMessage{
WgPubKey: serverKey.PublicKey().String(),
Body: encryptedResp,
Version: 0,
}, nil
}
flowInfo, err := client.GetPKCEAuthorizationFlow(serverKey)
if err != nil {
t.Error("error while retrieving pkce auth flow information")
}
assert.Equal(t, expectedFlowInfo.ProviderConfig.ClientID, flowInfo.ProviderConfig.ClientID, "provider configured client ID should match")
assert.Equal(t, expectedFlowInfo.ProviderConfig.ClientSecret, flowInfo.ProviderConfig.ClientSecret, "provider configured client secret should match")
}

View File

@@ -366,6 +366,40 @@ func (c *GrpcClient) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.D
return flowInfoResp, nil
}
// GetPKCEAuthorizationFlow returns a pkce authorization flow information.
// It also takes care of encrypting and decrypting messages.
func (c *GrpcClient) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) {
if !c.ready() {
return nil, fmt.Errorf("no connection to management in order to get pkce authorization flow")
}
mgmCtx, cancel := context.WithTimeout(c.ctx, time.Second*2)
defer cancel()
message := &proto.PKCEAuthorizationFlowRequest{}
encryptedMSG, err := encryption.EncryptMessage(serverKey, c.key, message)
if err != nil {
return nil, err
}
resp, err := c.realClient.GetPKCEAuthorizationFlow(mgmCtx, &proto.EncryptedMessage{
WgPubKey: c.key.PublicKey().String(),
Body: encryptedMSG,
})
if err != nil {
return nil, err
}
flowInfoResp := &proto.PKCEAuthorizationFlow{}
err = encryption.DecryptMessage(serverKey, c.key, resp.Body, flowInfoResp)
if err != nil {
errWithMSG := fmt.Errorf("failed to decrypt pkce authorization flow message: %s", err)
log.Error(errWithMSG)
return nil, errWithMSG
}
return flowInfoResp, nil
}
func (c *GrpcClient) notifyDisconnected() {
c.connStateCallbackLock.RLock()
defer c.connStateCallbackLock.RUnlock()

View File

@@ -13,6 +13,7 @@ type MockClient struct {
RegisterFunc func(serverKey wgtypes.Key, setupKey string, jwtToken string, info *system.Info, sshKey []byte) (*proto.LoginResponse, error)
LoginFunc func(serverKey wgtypes.Key, info *system.Info, sshKey []byte) (*proto.LoginResponse, error)
GetDeviceAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.DeviceAuthorizationFlow, error)
GetPKCEAuthorizationFlowFunc func(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error)
}
func (m *MockClient) Close() error {
@@ -57,6 +58,13 @@ func (m *MockClient) GetDeviceAuthorizationFlow(serverKey wgtypes.Key) (*proto.D
return m.GetDeviceAuthorizationFlowFunc(serverKey)
}
func (m *MockClient) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKCEAuthorizationFlow, error) {
if m.GetPKCEAuthorizationFlowFunc == nil {
return nil, nil
}
return m.GetPKCEAuthorizationFlow(serverKey)
}
// GetNetworkMap mock implementation of GetNetworkMap from mgm.Client interface
func (m *MockClient) GetNetworkMap() (*proto.NetworkMap, error) {
return nil, nil

View File

@@ -218,7 +218,11 @@ var (
if !disableMetrics {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
metricsWorker := metrics.NewWorker(ctx, installationID, store, peersUpdateManager)
idpManager := "disabled"
if config.IdpManagerConfig != nil && config.IdpManagerConfig.ManagerType != "" {
idpManager = config.IdpManagerConfig.ManagerType
}
metricsWorker := metrics.NewWorker(ctx, installationID, store, peersUpdateManager, idpManager)
go metricsWorker.Run()
}
@@ -367,24 +371,24 @@ func handlerFunc(gRPCHandler *grpc.Server, httpHandler http.Handler) http.Handle
}
func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
config := &server.Config{}
_, err := util.ReadJson(mgmtConfigPath, config)
loadedConfig := &server.Config{}
_, err := util.ReadJson(mgmtConfigPath, loadedConfig)
if err != nil {
return nil, err
}
if mgmtLetsencryptDomain != "" {
config.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain
loadedConfig.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain
}
if mgmtDataDir != "" {
config.Datadir = mgmtDataDir
loadedConfig.Datadir = mgmtDataDir
}
if certKey != "" && certFile != "" {
config.HttpConfig.CertFile = certFile
config.HttpConfig.CertKey = certKey
loadedConfig.HttpConfig.CertFile = certFile
loadedConfig.HttpConfig.CertKey = certKey
}
oidcEndpoint := config.HttpConfig.OIDCConfigEndpoint
oidcEndpoint := loadedConfig.HttpConfig.OIDCConfigEndpoint
if oidcEndpoint != "" {
// if OIDCConfigEndpoint is specified, we can load DeviceAuthEndpoint and TokenEndpoint automatically
log.Infof("loading OIDC configuration from the provided IDP configuration endpoint %s", oidcEndpoint)
@@ -395,44 +399,54 @@ func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
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",
oidcConfig.Issuer, config.HttpConfig.AuthIssuer)
config.HttpConfig.AuthIssuer = oidcConfig.Issuer
oidcConfig.Issuer, loadedConfig.HttpConfig.AuthIssuer)
loadedConfig.HttpConfig.AuthIssuer = oidcConfig.Issuer
log.Infof("overriding HttpConfig.AuthKeysLocation (JWT certs) with a new value %s, previously configured value: %s",
oidcConfig.JwksURI, config.HttpConfig.AuthKeysLocation)
config.HttpConfig.AuthKeysLocation = oidcConfig.JwksURI
oidcConfig.JwksURI, loadedConfig.HttpConfig.AuthKeysLocation)
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",
oidcConfig.TokenEndpoint, config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint)
config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
oidcConfig.TokenEndpoint, loadedConfig.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint)
loadedConfig.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
log.Infof("overriding DeviceAuthorizationFlow.DeviceAuthEndpoint with a new value: %s, previously configured value: %s",
oidcConfig.DeviceAuthEndpoint, config.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint)
config.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint = oidcConfig.DeviceAuthEndpoint
oidcConfig.DeviceAuthEndpoint, loadedConfig.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint)
loadedConfig.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint = oidcConfig.DeviceAuthEndpoint
u, err := url.Parse(oidcEndpoint)
if err != nil {
return nil, err
}
log.Infof("overriding DeviceAuthorizationFlow.ProviderConfig.Domain with a new value: %s, previously configured value: %s",
u.Host, config.DeviceAuthorizationFlow.ProviderConfig.Domain)
config.DeviceAuthorizationFlow.ProviderConfig.Domain = u.Host
u.Host, loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Domain)
loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Domain = u.Host
if config.DeviceAuthorizationFlow.ProviderConfig.Scope == "" {
config.DeviceAuthorizationFlow.ProviderConfig.Scope = server.DefaultDeviceAuthFlowScope
if loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Scope == "" {
loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Scope = server.DefaultDeviceAuthFlowScope
}
}
if loadedConfig.PKCEAuthorizationFlow != nil {
log.Infof("overriding PKCEAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
oidcConfig.TokenEndpoint, loadedConfig.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint)
loadedConfig.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
log.Infof("overriding PKCEAuthorizationFlow.AuthorizationEndpoint with a new value: %s, previously configured value: %s",
oidcConfig.AuthorizationEndpoint, loadedConfig.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint)
loadedConfig.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint = oidcConfig.AuthorizationEndpoint
}
}
return config, err
return loadedConfig, err
}
// OIDCConfigResponse used for parsing OIDC config response
type OIDCConfigResponse struct {
Issuer string `json:"issuer"`
TokenEndpoint string `json:"token_endpoint"`
DeviceAuthEndpoint string `json:"device_authorization_endpoint"`
JwksURI string `json:"jwks_uri"`
Issuer string `json:"issuer"`
TokenEndpoint string `json:"token_endpoint"`
DeviceAuthEndpoint string `json:"device_authorization_endpoint"`
JwksURI string `json:"jwks_uri"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
}
// fetchOIDCConfig fetches OIDC configuration from the IDP

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.21.9
// protoc v3.21.12
// source: management.proto
package proto
@@ -162,7 +162,7 @@ func (x FirewallRuleDirection) Number() protoreflect.EnumNumber {
// Deprecated: Use FirewallRuleDirection.Descriptor instead.
func (FirewallRuleDirection) EnumDescriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{25, 0}
return file_management_proto_rawDescGZIP(), []int{27, 0}
}
type FirewallRuleAction int32
@@ -208,7 +208,7 @@ func (x FirewallRuleAction) Number() protoreflect.EnumNumber {
// Deprecated: Use FirewallRuleAction.Descriptor instead.
func (FirewallRuleAction) EnumDescriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{25, 1}
return file_management_proto_rawDescGZIP(), []int{27, 1}
}
type FirewallRuleProtocol int32
@@ -263,7 +263,7 @@ func (x FirewallRuleProtocol) Number() protoreflect.EnumNumber {
// Deprecated: Use FirewallRuleProtocol.Descriptor instead.
func (FirewallRuleProtocol) EnumDescriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{25, 2}
return file_management_proto_rawDescGZIP(), []int{27, 2}
}
type EncryptedMessage struct {
@@ -1477,7 +1477,96 @@ func (x *DeviceAuthorizationFlow) GetProviderConfig() *ProviderConfig {
return nil
}
// ProviderConfig has all attributes needed to initiate a device authorization flow
// PKCEAuthorizationFlowRequest empty struct for future expansion
type PKCEAuthorizationFlowRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *PKCEAuthorizationFlowRequest) Reset() {
*x = PKCEAuthorizationFlowRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_management_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *PKCEAuthorizationFlowRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PKCEAuthorizationFlowRequest) ProtoMessage() {}
func (x *PKCEAuthorizationFlowRequest) ProtoReflect() protoreflect.Message {
mi := &file_management_proto_msgTypes[18]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PKCEAuthorizationFlowRequest.ProtoReflect.Descriptor instead.
func (*PKCEAuthorizationFlowRequest) Descriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{18}
}
// PKCEAuthorizationFlow represents Authorization Code Flow information
// that can be used by the client to login initiate a Oauth 2.0 authorization code grant flow
// with Proof Key for Code Exchange (PKCE). See https://datatracker.ietf.org/doc/html/rfc7636
type PKCEAuthorizationFlow struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ProviderConfig *ProviderConfig `protobuf:"bytes,1,opt,name=ProviderConfig,proto3" json:"ProviderConfig,omitempty"`
}
func (x *PKCEAuthorizationFlow) Reset() {
*x = PKCEAuthorizationFlow{}
if protoimpl.UnsafeEnabled {
mi := &file_management_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *PKCEAuthorizationFlow) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PKCEAuthorizationFlow) ProtoMessage() {}
func (x *PKCEAuthorizationFlow) ProtoReflect() protoreflect.Message {
mi := &file_management_proto_msgTypes[19]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PKCEAuthorizationFlow.ProtoReflect.Descriptor instead.
func (*PKCEAuthorizationFlow) Descriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{19}
}
func (x *PKCEAuthorizationFlow) GetProviderConfig() *ProviderConfig {
if x != nil {
return x.ProviderConfig
}
return nil
}
// ProviderConfig has all attributes needed to initiate a device/pkce authorization flow
type ProviderConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1500,12 +1589,16 @@ type ProviderConfig struct {
Scope string `protobuf:"bytes,7,opt,name=Scope,proto3" json:"Scope,omitempty"`
// UseIDToken indicates if the id token should be used for authentication
UseIDToken bool `protobuf:"varint,8,opt,name=UseIDToken,proto3" json:"UseIDToken,omitempty"`
// AuthorizationEndpoint is the endpoint of an IDP manager where clients can obtain authorization code.
AuthorizationEndpoint string `protobuf:"bytes,9,opt,name=AuthorizationEndpoint,proto3" json:"AuthorizationEndpoint,omitempty"`
// RedirectURLs handles authorization code from IDP manager
RedirectURLs []string `protobuf:"bytes,10,rep,name=RedirectURLs,proto3" json:"RedirectURLs,omitempty"`
}
func (x *ProviderConfig) Reset() {
*x = ProviderConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_management_proto_msgTypes[18]
mi := &file_management_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1518,7 +1611,7 @@ func (x *ProviderConfig) String() string {
func (*ProviderConfig) ProtoMessage() {}
func (x *ProviderConfig) ProtoReflect() protoreflect.Message {
mi := &file_management_proto_msgTypes[18]
mi := &file_management_proto_msgTypes[20]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1531,7 +1624,7 @@ func (x *ProviderConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProviderConfig.ProtoReflect.Descriptor instead.
func (*ProviderConfig) Descriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{18}
return file_management_proto_rawDescGZIP(), []int{20}
}
func (x *ProviderConfig) GetClientID() string {
@@ -1590,6 +1683,20 @@ func (x *ProviderConfig) GetUseIDToken() bool {
return false
}
func (x *ProviderConfig) GetAuthorizationEndpoint() string {
if x != nil {
return x.AuthorizationEndpoint
}
return ""
}
func (x *ProviderConfig) GetRedirectURLs() []string {
if x != nil {
return x.RedirectURLs
}
return nil
}
// Route represents a route.Route object
type Route struct {
state protoimpl.MessageState
@@ -1608,7 +1715,7 @@ type Route struct {
func (x *Route) Reset() {
*x = Route{}
if protoimpl.UnsafeEnabled {
mi := &file_management_proto_msgTypes[19]
mi := &file_management_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1621,7 +1728,7 @@ func (x *Route) String() string {
func (*Route) ProtoMessage() {}
func (x *Route) ProtoReflect() protoreflect.Message {
mi := &file_management_proto_msgTypes[19]
mi := &file_management_proto_msgTypes[21]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1634,7 +1741,7 @@ func (x *Route) ProtoReflect() protoreflect.Message {
// Deprecated: Use Route.ProtoReflect.Descriptor instead.
func (*Route) Descriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{19}
return file_management_proto_rawDescGZIP(), []int{21}
}
func (x *Route) GetID() string {
@@ -1700,7 +1807,7 @@ type DNSConfig struct {
func (x *DNSConfig) Reset() {
*x = DNSConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_management_proto_msgTypes[20]
mi := &file_management_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1713,7 +1820,7 @@ func (x *DNSConfig) String() string {
func (*DNSConfig) ProtoMessage() {}
func (x *DNSConfig) ProtoReflect() protoreflect.Message {
mi := &file_management_proto_msgTypes[20]
mi := &file_management_proto_msgTypes[22]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1726,7 +1833,7 @@ func (x *DNSConfig) ProtoReflect() protoreflect.Message {
// Deprecated: Use DNSConfig.ProtoReflect.Descriptor instead.
func (*DNSConfig) Descriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{20}
return file_management_proto_rawDescGZIP(), []int{22}
}
func (x *DNSConfig) GetServiceEnable() bool {
@@ -1763,7 +1870,7 @@ type CustomZone struct {
func (x *CustomZone) Reset() {
*x = CustomZone{}
if protoimpl.UnsafeEnabled {
mi := &file_management_proto_msgTypes[21]
mi := &file_management_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1776,7 +1883,7 @@ func (x *CustomZone) String() string {
func (*CustomZone) ProtoMessage() {}
func (x *CustomZone) ProtoReflect() protoreflect.Message {
mi := &file_management_proto_msgTypes[21]
mi := &file_management_proto_msgTypes[23]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1789,7 +1896,7 @@ func (x *CustomZone) ProtoReflect() protoreflect.Message {
// Deprecated: Use CustomZone.ProtoReflect.Descriptor instead.
func (*CustomZone) Descriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{21}
return file_management_proto_rawDescGZIP(), []int{23}
}
func (x *CustomZone) GetDomain() string {
@@ -1822,7 +1929,7 @@ type SimpleRecord struct {
func (x *SimpleRecord) Reset() {
*x = SimpleRecord{}
if protoimpl.UnsafeEnabled {
mi := &file_management_proto_msgTypes[22]
mi := &file_management_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1835,7 +1942,7 @@ func (x *SimpleRecord) String() string {
func (*SimpleRecord) ProtoMessage() {}
func (x *SimpleRecord) ProtoReflect() protoreflect.Message {
mi := &file_management_proto_msgTypes[22]
mi := &file_management_proto_msgTypes[24]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1848,7 +1955,7 @@ func (x *SimpleRecord) ProtoReflect() protoreflect.Message {
// Deprecated: Use SimpleRecord.ProtoReflect.Descriptor instead.
func (*SimpleRecord) Descriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{22}
return file_management_proto_rawDescGZIP(), []int{24}
}
func (x *SimpleRecord) GetName() string {
@@ -1900,7 +2007,7 @@ type NameServerGroup struct {
func (x *NameServerGroup) Reset() {
*x = NameServerGroup{}
if protoimpl.UnsafeEnabled {
mi := &file_management_proto_msgTypes[23]
mi := &file_management_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1913,7 +2020,7 @@ func (x *NameServerGroup) String() string {
func (*NameServerGroup) ProtoMessage() {}
func (x *NameServerGroup) ProtoReflect() protoreflect.Message {
mi := &file_management_proto_msgTypes[23]
mi := &file_management_proto_msgTypes[25]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1926,7 +2033,7 @@ func (x *NameServerGroup) ProtoReflect() protoreflect.Message {
// Deprecated: Use NameServerGroup.ProtoReflect.Descriptor instead.
func (*NameServerGroup) Descriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{23}
return file_management_proto_rawDescGZIP(), []int{25}
}
func (x *NameServerGroup) GetNameServers() []*NameServer {
@@ -1964,7 +2071,7 @@ type NameServer struct {
func (x *NameServer) Reset() {
*x = NameServer{}
if protoimpl.UnsafeEnabled {
mi := &file_management_proto_msgTypes[24]
mi := &file_management_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1977,7 +2084,7 @@ func (x *NameServer) String() string {
func (*NameServer) ProtoMessage() {}
func (x *NameServer) ProtoReflect() protoreflect.Message {
mi := &file_management_proto_msgTypes[24]
mi := &file_management_proto_msgTypes[26]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1990,7 +2097,7 @@ func (x *NameServer) ProtoReflect() protoreflect.Message {
// Deprecated: Use NameServer.ProtoReflect.Descriptor instead.
func (*NameServer) Descriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{24}
return file_management_proto_rawDescGZIP(), []int{26}
}
func (x *NameServer) GetIP() string {
@@ -2030,7 +2137,7 @@ type FirewallRule struct {
func (x *FirewallRule) Reset() {
*x = FirewallRule{}
if protoimpl.UnsafeEnabled {
mi := &file_management_proto_msgTypes[25]
mi := &file_management_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2043,7 +2150,7 @@ func (x *FirewallRule) String() string {
func (*FirewallRule) ProtoMessage() {}
func (x *FirewallRule) ProtoReflect() protoreflect.Message {
mi := &file_management_proto_msgTypes[25]
mi := &file_management_proto_msgTypes[27]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2056,7 +2163,7 @@ func (x *FirewallRule) ProtoReflect() protoreflect.Message {
// Deprecated: Use FirewallRule.ProtoReflect.Descriptor instead.
func (*FirewallRule) Descriptor() ([]byte, []int) {
return file_management_proto_rawDescGZIP(), []int{25}
return file_management_proto_rawDescGZIP(), []int{27}
}
func (x *FirewallRule) GetPeerIP() string {
@@ -2270,121 +2377,140 @@ var file_management_proto_rawDesc = []byte{
0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e,
0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16,
0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f,
0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x90, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69,
0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69,
0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69,
0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53,
0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69,
0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69,
0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a,
0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f,
0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63,
0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a,
0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f,
0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65,
0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55,
0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xb5, 0x01, 0x0a, 0x05, 0x52, 0x6f,
0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a,
0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12,
0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50,
0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20,
0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d,
0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52,
0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e,
0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49,
0x44, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45,
0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72,
0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d,
0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61,
0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38,
0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73,
0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74,
0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32,
0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d,
0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72,
0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f,
0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c,
0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73,
0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54,
0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0x7f, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65,
0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e,
0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61,
0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79,
0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12,
0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09,
0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d,
0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12,
0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50,
0x6f, 0x72, 0x74, 0x22, 0xf0, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c,
0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x40, 0x0a, 0x09,
0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72,
0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37,
0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f,
0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65,
0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52,
0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f,
0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52,
0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72,
0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x1c, 0x0a, 0x09, 0x64, 0x69,
0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12,
0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x22, 0x1e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08,
0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x22, 0x3c, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10,
0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43,
0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04,
0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x32, 0xf7, 0x02, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05,
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47,
0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d,
0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76,
0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a,
0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63,
0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75,
0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75,
0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12,
0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72,
0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a,
0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65,
0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75,
0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f,
0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54,
0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49,
0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72,
0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18,
0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c,
0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03,
0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73,
0x22, 0xb5, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65,
0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74,
0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54,
0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f,
0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65,
0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72,
0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65,
0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61,
0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53,
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10,
0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73,
0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72,
0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47,
0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a,
0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e,
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f,
0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22,
0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a,
0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44,
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d,
0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a,
0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70,
0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04,
0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61,
0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22,
0x7f, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f,
0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52,
0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07,
0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50,
0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73,
0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e,
0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16,
0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06,
0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03,
0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xf0, 0x02, 0x0a, 0x0c, 0x46,
0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50,
0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65,
0x72, 0x49, 0x50, 0x12, 0x40, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65,
0x2e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65,
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e,
0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d,
0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e,
0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69,
0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a,
0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72,
0x74, 0x22, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06,
0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x22,
0x1e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43,
0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x22,
0x3c, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55,
0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10,
0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44,
0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x32, 0xd1, 0x03,
0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70,
0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e,
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79,
0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e,
0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00,
0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b,
0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c,
0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47,
0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b,
0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46,
0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22,
0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
}
var (
@@ -2400,7 +2526,7 @@ func file_management_proto_rawDescGZIP() []byte {
}
var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
var file_management_proto_goTypes = []interface{}{
(HostConfig_Protocol)(0), // 0: management.HostConfig.Protocol
(DeviceAuthorizationFlowProvider)(0), // 1: management.DeviceAuthorizationFlow.provider
@@ -2425,15 +2551,17 @@ var file_management_proto_goTypes = []interface{}{
(*SSHConfig)(nil), // 20: management.SSHConfig
(*DeviceAuthorizationFlowRequest)(nil), // 21: management.DeviceAuthorizationFlowRequest
(*DeviceAuthorizationFlow)(nil), // 22: management.DeviceAuthorizationFlow
(*ProviderConfig)(nil), // 23: management.ProviderConfig
(*Route)(nil), // 24: management.Route
(*DNSConfig)(nil), // 25: management.DNSConfig
(*CustomZone)(nil), // 26: management.CustomZone
(*SimpleRecord)(nil), // 27: management.SimpleRecord
(*NameServerGroup)(nil), // 28: management.NameServerGroup
(*NameServer)(nil), // 29: management.NameServer
(*FirewallRule)(nil), // 30: management.FirewallRule
(*timestamppb.Timestamp)(nil), // 31: google.protobuf.Timestamp
(*PKCEAuthorizationFlowRequest)(nil), // 23: management.PKCEAuthorizationFlowRequest
(*PKCEAuthorizationFlow)(nil), // 24: management.PKCEAuthorizationFlow
(*ProviderConfig)(nil), // 25: management.ProviderConfig
(*Route)(nil), // 26: management.Route
(*DNSConfig)(nil), // 27: management.DNSConfig
(*CustomZone)(nil), // 28: management.CustomZone
(*SimpleRecord)(nil), // 29: management.SimpleRecord
(*NameServerGroup)(nil), // 30: management.NameServerGroup
(*NameServer)(nil), // 31: management.NameServer
(*FirewallRule)(nil), // 32: management.FirewallRule
(*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp
}
var file_management_proto_depIdxs = []int32{
14, // 0: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig
@@ -2444,7 +2572,7 @@ var file_management_proto_depIdxs = []int32{
9, // 5: management.LoginRequest.peerKeys:type_name -> management.PeerKeys
14, // 6: management.LoginResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig
17, // 7: management.LoginResponse.peerConfig:type_name -> management.PeerConfig
31, // 8: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp
33, // 8: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp
15, // 9: management.WiretrusteeConfig.stuns:type_name -> management.HostConfig
16, // 10: management.WiretrusteeConfig.turns:type_name -> management.ProtectedHostConfig
15, // 11: management.WiretrusteeConfig.signal:type_name -> management.HostConfig
@@ -2453,35 +2581,38 @@ var file_management_proto_depIdxs = []int32{
20, // 14: management.PeerConfig.sshConfig:type_name -> management.SSHConfig
17, // 15: management.NetworkMap.peerConfig:type_name -> management.PeerConfig
19, // 16: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig
24, // 17: management.NetworkMap.Routes:type_name -> management.Route
25, // 18: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig
26, // 17: management.NetworkMap.Routes:type_name -> management.Route
27, // 18: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig
19, // 19: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig
30, // 20: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule
32, // 20: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule
20, // 21: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig
1, // 22: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider
23, // 23: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig
28, // 24: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup
26, // 25: management.DNSConfig.CustomZones:type_name -> management.CustomZone
27, // 26: management.CustomZone.Records:type_name -> management.SimpleRecord
29, // 27: management.NameServerGroup.NameServers:type_name -> management.NameServer
2, // 28: management.FirewallRule.Direction:type_name -> management.FirewallRule.direction
3, // 29: management.FirewallRule.Action:type_name -> management.FirewallRule.action
4, // 30: management.FirewallRule.Protocol:type_name -> management.FirewallRule.protocol
5, // 31: management.ManagementService.Login:input_type -> management.EncryptedMessage
5, // 32: management.ManagementService.Sync:input_type -> management.EncryptedMessage
13, // 33: management.ManagementService.GetServerKey:input_type -> management.Empty
13, // 34: management.ManagementService.isHealthy:input_type -> management.Empty
5, // 35: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage
5, // 36: management.ManagementService.Login:output_type -> management.EncryptedMessage
5, // 37: management.ManagementService.Sync:output_type -> management.EncryptedMessage
12, // 38: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse
13, // 39: management.ManagementService.isHealthy:output_type -> management.Empty
5, // 40: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage
36, // [36:41] is the sub-list for method output_type
31, // [31:36] is the sub-list for method input_type
31, // [31:31] is the sub-list for extension type_name
31, // [31:31] is the sub-list for extension extendee
0, // [0:31] is the sub-list for field type_name
25, // 23: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig
25, // 24: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig
30, // 25: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup
28, // 26: management.DNSConfig.CustomZones:type_name -> management.CustomZone
29, // 27: management.CustomZone.Records:type_name -> management.SimpleRecord
31, // 28: management.NameServerGroup.NameServers:type_name -> management.NameServer
2, // 29: management.FirewallRule.Direction:type_name -> management.FirewallRule.direction
3, // 30: management.FirewallRule.Action:type_name -> management.FirewallRule.action
4, // 31: management.FirewallRule.Protocol:type_name -> management.FirewallRule.protocol
5, // 32: management.ManagementService.Login:input_type -> management.EncryptedMessage
5, // 33: management.ManagementService.Sync:input_type -> management.EncryptedMessage
13, // 34: management.ManagementService.GetServerKey:input_type -> management.Empty
13, // 35: management.ManagementService.isHealthy:input_type -> management.Empty
5, // 36: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage
5, // 37: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage
5, // 38: management.ManagementService.Login:output_type -> management.EncryptedMessage
5, // 39: management.ManagementService.Sync:output_type -> management.EncryptedMessage
12, // 40: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse
13, // 41: management.ManagementService.isHealthy:output_type -> management.Empty
5, // 42: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage
5, // 43: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage
38, // [38:44] is the sub-list for method output_type
32, // [32:38] is the sub-list for method input_type
32, // [32:32] is the sub-list for extension type_name
32, // [32:32] is the sub-list for extension extendee
0, // [0:32] is the sub-list for field type_name
}
func init() { file_management_proto_init() }
@@ -2707,7 +2838,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ProviderConfig); i {
switch v := v.(*PKCEAuthorizationFlowRequest); i {
case 0:
return &v.state
case 1:
@@ -2719,7 +2850,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Route); i {
switch v := v.(*PKCEAuthorizationFlow); i {
case 0:
return &v.state
case 1:
@@ -2731,7 +2862,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DNSConfig); i {
switch v := v.(*ProviderConfig); i {
case 0:
return &v.state
case 1:
@@ -2743,7 +2874,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CustomZone); i {
switch v := v.(*Route); i {
case 0:
return &v.state
case 1:
@@ -2755,7 +2886,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SimpleRecord); i {
switch v := v.(*DNSConfig); i {
case 0:
return &v.state
case 1:
@@ -2767,7 +2898,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NameServerGroup); i {
switch v := v.(*CustomZone); i {
case 0:
return &v.state
case 1:
@@ -2779,7 +2910,7 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NameServer); i {
switch v := v.(*SimpleRecord); i {
case 0:
return &v.state
case 1:
@@ -2791,6 +2922,30 @@ func file_management_proto_init() {
}
}
file_management_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NameServerGroup); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_management_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NameServer); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_management_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*FirewallRule); i {
case 0:
return &v.state
@@ -2809,7 +2964,7 @@ func file_management_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_management_proto_rawDesc,
NumEnums: 5,
NumMessages: 26,
NumMessages: 28,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -31,6 +31,13 @@ service ManagementService {
// EncryptedMessage of the request has a body of DeviceAuthorizationFlowRequest.
// EncryptedMessage of the response has a body of DeviceAuthorizationFlow.
rpc GetDeviceAuthorizationFlow(EncryptedMessage) returns (EncryptedMessage) {}
// Exposes a PKCE authorization code flow information
// This is used for initiating a Oauth 2 authorization grant flow
// with Proof Key for Code Exchange (PKCE) which will be used by our clients to Login.
// EncryptedMessage of the request has a body of PKCEAuthorizationFlowRequest.
// EncryptedMessage of the response has a body of PKCEAuthorizationFlow.
rpc GetPKCEAuthorizationFlow(EncryptedMessage) returns (EncryptedMessage) {}
}
message EncryptedMessage {
@@ -237,7 +244,17 @@ message DeviceAuthorizationFlow {
}
}
// ProviderConfig has all attributes needed to initiate a device authorization flow
// PKCEAuthorizationFlowRequest empty struct for future expansion
message PKCEAuthorizationFlowRequest {}
// PKCEAuthorizationFlow represents Authorization Code Flow information
// that can be used by the client to login initiate a Oauth 2.0 authorization code grant flow
// with Proof Key for Code Exchange (PKCE). See https://datatracker.ietf.org/doc/html/rfc7636
message PKCEAuthorizationFlow {
ProviderConfig ProviderConfig = 1;
}
// ProviderConfig has all attributes needed to initiate a device/pkce authorization flow
message ProviderConfig {
// An IDP application client id
string ClientID = 1;
@@ -256,6 +273,10 @@ message ProviderConfig {
string Scope = 7;
// UseIDToken indicates if the id token should be used for authentication
bool UseIDToken = 8;
// AuthorizationEndpoint is the endpoint of an IDP manager where clients can obtain authorization code.
string AuthorizationEndpoint = 9;
// RedirectURLs handles authorization code from IDP manager
repeated string RedirectURLs = 10;
}
// Route represents a route.Route object

View File

@@ -37,6 +37,12 @@ type ManagementServiceClient interface {
// EncryptedMessage of the request has a body of DeviceAuthorizationFlowRequest.
// EncryptedMessage of the response has a body of DeviceAuthorizationFlow.
GetDeviceAuthorizationFlow(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error)
// Exposes a PKCE authorization code flow information
// This is used for initiating a Oauth 2 authorization grant flow
// with Proof Key for Code Exchange (PKCE) which will be used by our clients to Login.
// EncryptedMessage of the request has a body of PKCEAuthorizationFlowRequest.
// EncryptedMessage of the response has a body of PKCEAuthorizationFlow.
GetPKCEAuthorizationFlow(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error)
}
type managementServiceClient struct {
@@ -115,6 +121,15 @@ func (c *managementServiceClient) GetDeviceAuthorizationFlow(ctx context.Context
return out, nil
}
func (c *managementServiceClient) GetPKCEAuthorizationFlow(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) {
out := new(EncryptedMessage)
err := c.cc.Invoke(ctx, "/management.ManagementService/GetPKCEAuthorizationFlow", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ManagementServiceServer is the server API for ManagementService service.
// All implementations must embed UnimplementedManagementServiceServer
// for forward compatibility
@@ -138,6 +153,12 @@ type ManagementServiceServer interface {
// EncryptedMessage of the request has a body of DeviceAuthorizationFlowRequest.
// EncryptedMessage of the response has a body of DeviceAuthorizationFlow.
GetDeviceAuthorizationFlow(context.Context, *EncryptedMessage) (*EncryptedMessage, error)
// Exposes a PKCE authorization code flow information
// This is used for initiating a Oauth 2 authorization grant flow
// with Proof Key for Code Exchange (PKCE) which will be used by our clients to Login.
// EncryptedMessage of the request has a body of PKCEAuthorizationFlowRequest.
// EncryptedMessage of the response has a body of PKCEAuthorizationFlow.
GetPKCEAuthorizationFlow(context.Context, *EncryptedMessage) (*EncryptedMessage, error)
mustEmbedUnimplementedManagementServiceServer()
}
@@ -160,6 +181,9 @@ func (UnimplementedManagementServiceServer) IsHealthy(context.Context, *Empty) (
func (UnimplementedManagementServiceServer) GetDeviceAuthorizationFlow(context.Context, *EncryptedMessage) (*EncryptedMessage, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDeviceAuthorizationFlow not implemented")
}
func (UnimplementedManagementServiceServer) GetPKCEAuthorizationFlow(context.Context, *EncryptedMessage) (*EncryptedMessage, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetPKCEAuthorizationFlow not implemented")
}
func (UnimplementedManagementServiceServer) mustEmbedUnimplementedManagementServiceServer() {}
// UnsafeManagementServiceServer may be embedded to opt out of forward compatibility for this service.
@@ -266,6 +290,24 @@ func _ManagementService_GetDeviceAuthorizationFlow_Handler(srv interface{}, ctx
return interceptor(ctx, in, info, handler)
}
func _ManagementService_GetPKCEAuthorizationFlow_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(EncryptedMessage)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagementServiceServer).GetPKCEAuthorizationFlow(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/management.ManagementService/GetPKCEAuthorizationFlow",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagementServiceServer).GetPKCEAuthorizationFlow(ctx, req.(*EncryptedMessage))
}
return interceptor(ctx, in, info, handler)
}
// ManagementService_ServiceDesc is the grpc.ServiceDesc for ManagementService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -289,6 +331,10 @@ var ManagementService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetDeviceAuthorizationFlow",
Handler: _ManagementService_GetDeviceAuthorizationFlow_Handler,
},
{
MethodName: "GetPKCEAuthorizationFlow",
Handler: _ManagementService_GetPKCEAuthorizationFlow_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View File

@@ -139,10 +139,14 @@ type DefaultAccountManager struct {
type Settings struct {
// PeerLoginExpirationEnabled globally enables or disables peer login expiration
PeerLoginExpirationEnabled bool
// PeerLoginExpiration is a setting that indicates when peer login expires.
// Applies to all peers that have Peer.LoginExpirationEnabled set to true.
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
// and add it to account groups.
JWTGroupsEnabled bool
@@ -158,6 +162,7 @@ func (s *Settings) Copy() *Settings {
PeerLoginExpiration: s.PeerLoginExpiration,
JWTGroupsEnabled: s.JWTGroupsEnabled,
JWTGroupsClaimName: s.JWTGroupsClaimName,
GroupsPropagationEnabled: s.GroupsPropagationEnabled,
}
}
@@ -624,26 +629,96 @@ func (a *Account) GetPeer(peerID string) *Peer {
return a.Peers[peerID]
}
// AddJWTGroups to existed groups if they does not exists
func (a *Account) AddJWTGroups(groups []string) (int, error) {
existedGroups := make(map[string]*Group)
for _, g := range a.Groups {
existedGroups[g.Name] = g
// AddJWTGroups to account and to user autoassigned groups
func (a *Account) AddJWTGroups(userID string, groups []string) bool {
user, ok := a.Users[userID]
if !ok {
return false
}
var count int
existedGroupsByName := make(map[string]*Group)
for _, group := range a.Groups {
existedGroupsByName[group.Name] = group
}
autoGroups := make(map[string]struct{})
for _, groupID := range user.AutoGroups {
autoGroups[groupID] = struct{}{}
}
var modified bool
for _, name := range groups {
if _, ok := existedGroups[name]; !ok {
id := xid.New().String()
a.Groups[id] = &Group{
ID: id,
group, ok := existedGroupsByName[name]
if !ok {
group = &Group{
ID: xid.New().String(),
Name: name,
Issued: GroupIssuedJWT,
}
count++
a.Groups[group.ID] = group
modified = true
}
if _, ok := autoGroups[group.ID]; !ok {
if group.Issued == GroupIssuedJWT {
user.AutoGroups = append(user.AutoGroups, group.ID)
modified = true
}
}
}
return count, nil
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
@@ -1290,11 +1365,13 @@ func (am *DefaultAccountManager) GetAccountFromToken(claims jwtclaims.Authorizat
log.Errorf("JWT claim %q is not a string: %v", account.Settings.JWTGroupsClaimName, item)
}
}
n, err := account.AddJWTGroups(groups)
if err != nil {
log.Errorf("failed to add JWT groups: %v", err)
}
if n > 0 {
// if groups were added or modified, save the account
if account.AddJWTGroups(claims.UserId, groups) {
if account.Settings.GroupsPropagationEnabled {
if user, err := account.FindUser(claims.UserId); err == nil {
account.UserGroupsAddToPeers(claims.UserId, append(user.AutoGroups, groups...)...)
}
}
if err := am.Store.SaveAccount(account); err != nil {
log.Errorf("failed to save account: %v", err)
}

View File

@@ -216,7 +216,6 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
assert.Len(t, networkMap.Peers, len(testCase.expectedPeers))
assert.Len(t, networkMap.OfflinePeers, len(testCase.expectedOfflinePeers))
}
}
func TestNewAccount(t *testing.T) {
@@ -470,11 +469,15 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) {
require.NoError(t, err, "unable to create account manager")
accountID := initAccount.Id
_, err = manager.GetAccountByUserOrAccountID(userId, accountID, domain)
acc, err := manager.GetAccountByUserOrAccountID(userId, accountID, domain)
require.NoError(t, err, "create init user failed")
// as initAccount was created without account id we have to take the id after account initialization
// that happens inside the GetAccountByUserOrAccountID where the id is getting generated
// it is important to set the id as it help to avoid creating additional account with empty Id and re-pointing indices to it
initAccount.Id = acc.Id
claims := jwtclaims.AuthorizationClaims{
AccountId: accountID,
AccountId: accountID, // is empty as it is based on accountID right after initialization of initAccount
Domain: domain,
UserId: userId,
DomainCategory: "test-category",
@@ -491,6 +494,7 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) {
initAccount.Settings.JWTGroupsEnabled = true
err := manager.Store.SaveAccount(initAccount)
require.NoError(t, err, "save account failed")
require.Len(t, manager.Store.GetAllAccounts(), 1, "only one account should exist")
account, _, err := manager.GetAccountFromToken(claims)
require.NoError(t, err, "get account by token failed")
@@ -502,6 +506,7 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) {
initAccount.Settings.JWTGroupsClaimName = "idp-groups"
err := manager.Store.SaveAccount(initAccount)
require.NoError(t, err, "save account failed")
require.Len(t, manager.Store.GetAllAccounts(), 1, "only one account should exist")
account, _, err := manager.GetAccountFromToken(claims)
require.NoError(t, err, "get account by token failed")
@@ -1925,6 +1930,120 @@ func TestAccount_GetNextPeerExpiration(t *testing.T) {
}
}
func TestAccount_AddJWTGroups(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.AddJWTGroups("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.AddJWTGroups("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.AddJWTGroups("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.AddJWTGroups("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) {
store, err := createStore(t)
if err != nil {

View File

@@ -42,6 +42,8 @@ type Config struct {
IdpManagerConfig *idp.Config
DeviceAuthorizationFlow *DeviceAuthorizationFlow
PKCEAuthorizationFlow *PKCEAuthorizationFlow
}
// GetAuthAudiences returns the audience from the http config and device authorization flow config
@@ -101,7 +103,14 @@ type DeviceAuthorizationFlow struct {
ProviderConfig ProviderConfig
}
// ProviderConfig has all attributes needed to initiate a device authorization flow
// PKCEAuthorizationFlow represents Authorization Code Flow information
// that can be used by the client to login initiate a Oauth 2.0 authorization code grant flow
// with Proof Key for Code Exchange (PKCE). See https://datatracker.ietf.org/doc/html/rfc7636
type PKCEAuthorizationFlow struct {
ProviderConfig ProviderConfig
}
// ProviderConfig has all attributes needed to initiate a device/pkce authorization flow
type ProviderConfig struct {
// ClientID An IDP application client id
ClientID string
@@ -116,10 +125,14 @@ type ProviderConfig struct {
TokenEndpoint string
// DeviceAuthEndpoint is the endpoint of an IDP manager where clients can obtain device authorization code
DeviceAuthEndpoint string
// AuthorizationEndpoint is the endpoint of an IDP manager where clients can obtain authorization code
AuthorizationEndpoint string
// Scopes provides the scopes to be included in the token request
Scope string
// UseIDToken indicates if the id token should be used for authentication
UseIDToken bool
// RedirectURL handles authorization code from IDP manager
RedirectURLs []string
}
// validateURL validates input http url

View File

@@ -289,6 +289,10 @@ func (s *FileStore) SaveAccount(account *Account) error {
s.mux.Lock()
defer s.mux.Unlock()
if account.Id == "" {
return status.Errorf(status.InvalidArgument, "account id should not be empty")
}
accountCopy := account.Copy()
s.Accounts[accountCopy.Id] = accountCopy

View File

@@ -543,3 +543,49 @@ func (s *GRPCServer) GetDeviceAuthorizationFlow(ctx context.Context, req *proto.
Body: encryptedResp,
}, nil
}
// GetPKCEAuthorizationFlow returns a pkce authorization flow information
// This is used for initiating an Oauth 2 pkce authorization grant flow
// which will be used by our clients to Login
func (s *GRPCServer) GetPKCEAuthorizationFlow(_ context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
if err != nil {
errMSG := fmt.Sprintf("error while parsing peer's Wireguard public key %s on GetPKCEAuthorizationFlow request.", req.WgPubKey)
log.Warn(errMSG)
return nil, status.Error(codes.InvalidArgument, errMSG)
}
err = encryption.DecryptMessage(peerKey, s.wgKey, req.Body, &proto.PKCEAuthorizationFlowRequest{})
if err != nil {
errMSG := fmt.Sprintf("error while decrypting peer's message with Wireguard public key %s.", req.WgPubKey)
log.Warn(errMSG)
return nil, status.Error(codes.InvalidArgument, errMSG)
}
if s.config.PKCEAuthorizationFlow == nil {
return nil, status.Error(codes.NotFound, "no pkce authorization flow information available")
}
flowInfoResp := &proto.PKCEAuthorizationFlow{
ProviderConfig: &proto.ProviderConfig{
Audience: s.config.PKCEAuthorizationFlow.ProviderConfig.Audience,
ClientID: s.config.PKCEAuthorizationFlow.ProviderConfig.ClientID,
ClientSecret: s.config.PKCEAuthorizationFlow.ProviderConfig.ClientSecret,
TokenEndpoint: s.config.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint,
AuthorizationEndpoint: s.config.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint,
Scope: s.config.PKCEAuthorizationFlow.ProviderConfig.Scope,
RedirectURLs: s.config.PKCEAuthorizationFlow.ProviderConfig.RedirectURLs,
UseIDToken: s.config.PKCEAuthorizationFlow.ProviderConfig.UseIDToken,
},
}
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, flowInfoResp)
if err != nil {
return nil, status.Error(codes.Internal, "failed to encrypt no pkce authorization flow information")
}
return &proto.EncryptedMessage{
WgPubKey: s.wgKey.PublicKey().String(),
Body: encryptedResp,
}, nil
}

View File

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

View File

@@ -38,7 +38,6 @@ func initAccountsTestData(account *server.Account, admin *server.User) *Accounts
accCopy := account.Copy()
accCopy.UpdateSettings(newSettings)
return accCopy, nil
},
},
claimsExtractor: jwtclaims.NewClaimsExtractor(
@@ -54,7 +53,6 @@ func initAccountsTestData(account *server.Account, admin *server.User) *Accounts
}
func TestAccounts_AccountsHandler(t *testing.T) {
accountID := "test_account"
adminUser := server.NewAdminUser("test_user")
@@ -94,6 +92,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
expectedSettings: api.AccountSettings{
PeerLoginExpiration: int(time.Hour.Seconds()),
PeerLoginExpirationEnabled: false,
GroupsPropagationEnabled: br(false),
JwtGroupsClaimName: sr(""),
JwtGroupsEnabled: br(false),
},
@@ -110,6 +109,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
expectedSettings: api.AccountSettings{
PeerLoginExpiration: 15552000,
PeerLoginExpirationEnabled: true,
GroupsPropagationEnabled: br(false),
JwtGroupsClaimName: sr(""),
JwtGroupsEnabled: br(false),
},
@@ -126,12 +126,30 @@ func TestAccounts_AccountsHandler(t *testing.T) {
expectedSettings: api.AccountSettings{
PeerLoginExpiration: 15552000,
PeerLoginExpirationEnabled: false,
GroupsPropagationEnabled: br(false),
JwtGroupsClaimName: sr("roles"),
JwtGroupsEnabled: br(true),
},
expectedArray: false,
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",
expectedBody: true,

View File

@@ -54,6 +54,10 @@ components:
description: Period of time after which peer login expires (seconds).
type: integer
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:
description: Allows extract groups from JWT claim and add it to account groups.
type: boolean
@@ -327,7 +331,7 @@ components:
type: string
example: valid
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
items:
type: string
@@ -369,13 +373,15 @@ components:
expires_in:
description: Expiration time in seconds
type: integer
example: 43200
minimum: 86400
maximum: 31536000
example: 86400
revoked:
description: Setup key revocation status
type: boolean
example: false
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
items:
type: string

View File

@@ -129,6 +129,9 @@ type AccountRequest struct {
// AccountSettings defines model for AccountSettings.
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 *string `json:"jwt_groups_claim_name,omitempty"`
@@ -681,7 +684,7 @@ type RuleRequest struct {
// SetupKey defines model for SetupKey.
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"`
// Expires Setup Key expiration date
@@ -723,7 +726,7 @@ type SetupKey struct {
// SetupKeyRequest defines model for SetupKeyRequest.
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"`
// ExpiresIn Expiration time in seconds

View File

@@ -57,7 +57,7 @@ func (m *AuthMiddleware) Handler(h http.Handler) http.Handler {
case "bearer":
err := m.CheckJWTFromRequest(w, r)
if err != nil {
log.Debugf("Error when validating JWT claims: %s", err.Error())
log.Errorf("Error when validating JWT claims: %s", err.Error())
util.WriteError(status.Errorf(status.Unauthorized, "token invalid"), w)
return
}

View File

@@ -60,6 +60,13 @@ func (h *SetupKeysHandler) CreateSetupKey(w http.ResponseWriter, r *http.Request
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 {
req.AutoGroups = []string{}
}

View File

@@ -143,7 +143,7 @@ func TestSetupKeysHandlers(t *testing.T) {
requestType: http.MethodPost,
requestPath: "/api/setup-keys",
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,
expectedBody: true,
expectedSetupKey: toResponseBody(newSetupKey),

View File

@@ -155,7 +155,7 @@ func (m *JWTValidator) ValidateAndParse(token string) (*jwt.Token, error) {
// Check if there was an error in parsing...
if err != nil {
log.Debugf("error parsing token: %v", err)
log.Errorf("error parsing token: %v", err)
return nil, fmt.Errorf("Error parsing token: %w", err)
}

View File

@@ -59,6 +59,7 @@ type ConnManager interface {
type Worker struct {
ctx context.Context
id string
idpManager string
dataSource DataSource
connManager ConnManager
startupTime time.Time
@@ -66,11 +67,12 @@ type Worker struct {
}
// NewWorker returns a metrics worker
func NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager) *Worker {
func NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {
currentTime := time.Now()
return &Worker{
ctx: ctx,
id: id,
idpManager: idpManager,
dataSource: dataSource,
connManager: connManager,
startupTime: currentTime,
@@ -277,6 +279,7 @@ func (w *Worker) generateProperties() properties {
metricsProperties["min_active_peer_version"] = minActivePeerVersion
metricsProperties["max_active_peer_version"] = maxActivePeerVersion
metricsProperties["ui_clients"] = uiClient
metricsProperties["idp_manager"] = w.idpManager
for protocol, count := range rulesProtocol {
metricsProperties["rules_protocol_"+protocol] = count

View File

@@ -16,6 +16,7 @@ type ManagementServiceServerMock struct {
GetServerKeyFunc func(context.Context, *proto.Empty) (*proto.ServerKeyResponse, error)
IsHealthyFunc func(context.Context, *proto.Empty) (*proto.Empty, error)
GetDeviceAuthorizationFlowFunc func(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error)
GetPKCEAuthorizationFlowFunc func(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error)
}
func (m ManagementServiceServerMock) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
@@ -52,3 +53,10 @@ func (m ManagementServiceServerMock) GetDeviceAuthorizationFlow(ctx context.Cont
}
return nil, status.Errorf(codes.Unimplemented, "method GetDeviceAuthorizationFlow not implemented")
}
func (m ManagementServiceServerMock) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
if m.GetPKCEAuthorizationFlowFunc != nil {
return m.GetPKCEAuthorizationFlowFunc(ctx, req)
}
return nil, status.Errorf(codes.Unimplemented, "method GetPKCEAuthorizationFlow not implemented")
}

View File

@@ -260,7 +260,6 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite
am.storeEvent(userID, newUser.Id, accountID, activity.UserInvited, nil)
return newUser.ToUserInfo(idpUser)
}
// GetUser looks up a user by provided authorization claims.
@@ -600,6 +599,13 @@ 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...)
}
if err = am.Store.SaveAccount(account); err != nil {
return nil, err
}
@@ -640,7 +646,6 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd
}
}
}
}()
if !isNil(am.idpManager) && !newUser.IsServiceUser {