mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-19 16:56:39 +00:00
Compare commits
17 Commits
update-get
...
fix/iptabl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9452e7b32 | ||
|
|
950d2efed7 | ||
|
|
a9b9b3fa0a | ||
|
|
cdf57275b7 | ||
|
|
e5e69b1f75 | ||
|
|
8eca83f3cb | ||
|
|
973316d194 | ||
|
|
a0a6ced148 | ||
|
|
0fc6c477a9 | ||
|
|
401a462398 | ||
|
|
a3839a6ef7 | ||
|
|
8aa4f240c7 | ||
|
|
d9686bae92 | ||
|
|
24e19ae287 | ||
|
|
74fde0ea2c | ||
|
|
890e09b787 | ||
|
|
48098c994d |
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -7,6 +7,16 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'go.mod'
|
||||||
|
- 'go.sum'
|
||||||
|
- '.goreleaser.yml'
|
||||||
|
- '.goreleaser_ui.yaml'
|
||||||
|
- '.goreleaser_ui_darwin.yaml'
|
||||||
|
- '.github/workflows/release.yml'
|
||||||
|
- 'release_files/**'
|
||||||
|
- '**/Dockerfile'
|
||||||
|
- '**/Dockerfile.*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.0.8"
|
SIGN_PIPE_VER: "v0.0.8"
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
name: Test Docker Compose Linux
|
name: Test Infrastructure files
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'infrastructure_files/**'
|
||||||
|
- '.github/workflows/test-infrastructure-files.yml'
|
||||||
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -12,7 +15,7 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test-docker-compose:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install jq
|
- name: Install jq
|
||||||
@@ -35,7 +38,7 @@ jobs:
|
|||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: cp setup.env
|
- name: cp setup.env
|
||||||
run: cp infrastructure_files/tests/setup.env infrastructure_files/
|
run: cp infrastructure_files/tests/setup.env infrastructure_files/
|
||||||
@@ -121,3 +124,28 @@ jobs:
|
|||||||
count=$(docker compose ps --format json | jq '.[] | select(.Project | contains("infrastructure_files")) | .State' | grep -c running)
|
count=$(docker compose ps --format json | jq '.[] | select(.Project | contains("infrastructure_files")) | .State' | grep -c running)
|
||||||
test $count -eq 4
|
test $count -eq 4
|
||||||
working-directory: infrastructure_files
|
working-directory: infrastructure_files
|
||||||
|
|
||||||
|
test-getting-started-script:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install jq
|
||||||
|
run: sudo apt-get install -y jq
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: run script
|
||||||
|
run: NETBIRD_DOMAIN=use-ip bash -x infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
|
||||||
|
- name: test Caddy file gen
|
||||||
|
run: test -f Caddyfile
|
||||||
|
- name: test docker-compose file gen
|
||||||
|
run: test -f docker-compose.yml
|
||||||
|
- name: test management.json file gen
|
||||||
|
run: test -f management.json
|
||||||
|
- name: test turnserver.conf file gen
|
||||||
|
run: test -f turnserver.conf
|
||||||
|
- name: test zitadel.env file gen
|
||||||
|
run: test -f zitadel.env
|
||||||
|
- name: test dashboard.env file gen
|
||||||
|
run: test -f dashboard.env
|
||||||
@@ -377,3 +377,13 @@ uploads:
|
|||||||
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
||||||
username: dev@wiretrustee.com
|
username: dev@wiretrustee.com
|
||||||
method: PUT
|
method: PUT
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
extra_files:
|
||||||
|
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
- glob: ./release_files/install.sh
|
||||||
|
|
||||||
|
release:
|
||||||
|
extra_files:
|
||||||
|
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
- glob: ./release_files/install.sh
|
||||||
81
README.md
81
README.md
@@ -36,46 +36,61 @@
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
**NetBird is an open-source VPN management platform built on top of WireGuard® making it easy to create secure private networks for your organization or home.**
|
**NetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, making it easy to create secure private networks for your organization or home.**
|
||||||
|
|
||||||
It requires zero configuration effort leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
**Connect.** NetBird creates a WireGuard-based overlay network that automatically connects your machines over an encrypted tunnel, leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
||||||
|
|
||||||
NetBird uses [NAT traversal techniques](https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment) to automatically create an overlay peer-to-peer network connecting machines regardless of location (home, office, data center, container, cloud, or edge environments), unifying virtual private network management experience.
|
**Secure.** NetBird isolates every machine and device by applying granular access policies, while allowing you to manage them intuitively from a single place.
|
||||||
|
|
||||||
**Key features:**
|
**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:**
|
| Connectivity | Management | Automation | Platforms |
|
||||||
- \[ ] Mobile clients.
|
|-------------------------------------------------------------------|--------------------------------------------------------------------------|----------------------------------------------------------------------------|---------------------------------------|
|
||||||
|
| <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
|
### Secure peer-to-peer VPN with SSO and MFA in minutes
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov
|
https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov
|
||||||
|
|
||||||
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
### Quickstart with NetBird Cloud
|
||||||
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
|
|
||||||
|
|
||||||
### Start using NetBird
|
- Download and install NetBird at [https://app.netbird.io/install](https://app.netbird.io/install)
|
||||||
- Hosted version: [https://app.netbird.io/](https://app.netbird.io/).
|
- Follow the steps to sign-up with Google, Microsoft, GitHub or your email address.
|
||||||
- See our documentation for [Quickstart Guide](https://docs.netbird.io/how-to/getting-started).
|
- Check NetBird [admin UI](https://app.netbird.io/).
|
||||||
- If you are looking to self-host NetBird, check our [Self-Hosting Guide](https://docs.netbird.io/selfhosted/selfhosted-guide).
|
- Add more machines.
|
||||||
- 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.
|
|
||||||
|
|
||||||
|
### Quickstart with self-hosted NetBird
|
||||||
|
|
||||||
|
> This is the quickest way to try self-hosted NetBird. It should take around 5 minutes to get started if you already have a public domain and a VM.
|
||||||
|
Follow the [Advanced guide with a custom identity provider](https://docs.netbird.io/selfhosted/selfhosted-guide#advanced-guide-with-a-custom-identity-provider) for installations with different IDPs.
|
||||||
|
|
||||||
|
**Infrastructure requirements:**
|
||||||
|
- A Linux VM with at least **1CPU** and **2GB** of memory.
|
||||||
|
- The VM should be publicly accessible on TCP ports **80** and **443** and UDP ports: **3478**, **49152-65535**.
|
||||||
|
- **Public domain** name pointing to the VM.
|
||||||
|
|
||||||
|
**Software requirements:**
|
||||||
|
- Docker installed on the VM with the docker compose plugin ([Docker installation guide](https://docs.docker.com/engine/install/)) or docker with docker-compose in version 2 or higher.
|
||||||
|
- [jq](https://jqlang.github.io/jq/) installed. In most distributions
|
||||||
|
Usually available in the official repositories and can be installed with `sudo apt install jq` or `sudo yum install jq`
|
||||||
|
- [curl](https://curl.se/) installed.
|
||||||
|
Usually available in the official repositories and can be installed with `sudo apt install curl` or `sudo yum install curl`
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
- Download and run the installation script:
|
||||||
|
```bash
|
||||||
|
export NETBIRD_DOMAIN=netbird.example.com; curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started-with-zitadel.sh | bash
|
||||||
|
```
|
||||||
|
- Once finished, you can manage the resources via `docker-compose`
|
||||||
|
|
||||||
### A bit on NetBird internals
|
### A bit on NetBird internals
|
||||||
- Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
|
- Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
|
||||||
@@ -88,18 +103,18 @@ For stable versions, see [releases](https://github.com/netbirdio/netbird/release
|
|||||||
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in NetBird setups.
|
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in NetBird setups.
|
||||||
|
|
||||||
<p float="left" align="middle">
|
<p float="left" align="middle">
|
||||||
<img src="https://netbird.io/docs/img/architecture/high-level-dia.png" width="700"/>
|
<img src="https://docs.netbird.io/docs-static/img/architecture/high-level-dia.png" width="700"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
See a complete [architecture overview](https://docs.netbird.io/about-netbird/how-netbird-works#architecture) for details.
|
See a complete [architecture overview](https://docs.netbird.io/about-netbird/how-netbird-works#architecture) for details.
|
||||||
|
|
||||||
### Roadmap
|
|
||||||
- [Public Roadmap](https://github.com/netbirdio/netbird/projects/2)
|
|
||||||
|
|
||||||
### Community projects
|
### Community projects
|
||||||
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird)
|
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird)
|
||||||
- [NetBird installer script](https://github.com/physk/netbird-installer)
|
- [NetBird installer script](https://github.com/physk/netbird-installer)
|
||||||
|
|
||||||
|
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
||||||
|
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
|
||||||
|
|
||||||
### Support acknowledgement
|
### Support acknowledgement
|
||||||
|
|
||||||
In November 2022, NetBird joined the [StartUpSecure program](https://www.forschung-it-sicherheit-kommunikationssysteme.de/foerderung/bekanntmachungen/startup-secure) sponsored by The Federal Ministry of Education and Research of The Federal Republic of Germany. Together with [CISPA Helmholtz Center for Information Security](https://cispa.de/en) NetBird brings the security best practices and simplicity to private networking.
|
In November 2022, NetBird joined the [StartUpSecure program](https://www.forschung-it-sicherheit-kommunikationssysteme.de/foerderung/bekanntmachungen/startup-secure) sponsored by The Federal Ministry of Education and Research of The Federal Republic of Germany. Together with [CISPA Helmholtz Center for Information Security](https://cispa.de/en) NetBird brings the security best practices and simplicity to private networking.
|
||||||
@@ -107,7 +122,7 @@ In November 2022, NetBird joined the [StartUpSecure program](https://www.forschu
|
|||||||

|

|
||||||
|
|
||||||
### Testimonials
|
### Testimonials
|
||||||
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), and [Coturn](https://github.com/coturn/coturn). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
|
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), [Coturn](https://github.com/coturn/coturn), and [Rosenpass](https://rosenpass.eu). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
|
||||||
|
|
||||||
### Legal
|
### Legal
|
||||||
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.
|
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ var _ OAuthFlow = &PKCEAuthorizationFlow{}
|
|||||||
const (
|
const (
|
||||||
queryState = "state"
|
queryState = "state"
|
||||||
queryCode = "code"
|
queryCode = "code"
|
||||||
|
queryError = "error"
|
||||||
|
queryErrorDesc = "error_description"
|
||||||
defaultPKCETimeoutSeconds = 300
|
defaultPKCETimeoutSeconds = 300
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -141,9 +143,13 @@ func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errC
|
|||||||
tokenValidatorFunc := func() (*oauth2.Token, error) {
|
tokenValidatorFunc := func() (*oauth2.Token, error) {
|
||||||
query := req.URL.Query()
|
query := req.URL.Query()
|
||||||
|
|
||||||
state := query.Get(queryState)
|
if authError := query.Get(queryError); authError != "" {
|
||||||
|
authErrorDesc := query.Get(queryErrorDesc)
|
||||||
|
return nil, fmt.Errorf("%s.%s", authError, authErrorDesc)
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent timing attacks on state
|
// Prevent timing attacks on state
|
||||||
if subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 {
|
if state := query.Get(queryState); subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 {
|
||||||
return nil, fmt.Errorf("invalid state")
|
return nil, fmt.Errorf("invalid state")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,12 +167,13 @@ func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errC
|
|||||||
|
|
||||||
token, err := tokenValidatorFunc()
|
token, err := tokenValidatorFunc()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errChan <- fmt.Errorf("PKCE authorization flow failed: %v", err)
|
|
||||||
renderPKCEFlowTmpl(w, err)
|
renderPKCEFlowTmpl(w, err)
|
||||||
|
errChan <- fmt.Errorf("PKCE authorization flow failed: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenChan <- token
|
|
||||||
renderPKCEFlowTmpl(w, nil)
|
renderPKCEFlowTmpl(w, nil)
|
||||||
|
tokenChan <- token
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := server.ListenAndServe(); err != nil {
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ const (
|
|||||||
fileGeneratedResolvConfSearchBeginContent = "search "
|
fileGeneratedResolvConfSearchBeginContent = "search "
|
||||||
fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader +
|
fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader +
|
||||||
"\n# If needed you can restore the original file by copying back %s\n\nnameserver %s\n" +
|
"\n# If needed you can restore the original file by copying back %s\n\nnameserver %s\n" +
|
||||||
fileGeneratedResolvConfSearchBeginContent + "%s\n"
|
fileGeneratedResolvConfSearchBeginContent + "%s\n\n" +
|
||||||
|
"%s\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -91,7 +92,12 @@ func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error {
|
|||||||
searchDomains += " " + dConf.domain
|
searchDomains += " " + dConf.domain
|
||||||
appendedDomains++
|
appendedDomains++
|
||||||
}
|
}
|
||||||
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains)
|
|
||||||
|
originalContent, err := os.ReadFile(fileDefaultResolvConfBackupLocation)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Could not read existing resolv.conf")
|
||||||
|
}
|
||||||
|
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains, string(originalContent))
|
||||||
err = writeDNSConfig(content, defaultResolvConfPath, f.originalPerms)
|
err = writeDNSConfig(content, defaultResolvConfPath, f.originalPerms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = f.restore()
|
err = f.restore()
|
||||||
|
|||||||
@@ -182,12 +182,11 @@ func (s *systemConfigurator) addDNSState(state, domains, dnsServer string, port
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error {
|
func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error {
|
||||||
primaryServiceKey := s.getPrimaryService()
|
primaryServiceKey, existingNameserver := s.getPrimaryService()
|
||||||
if primaryServiceKey == "" {
|
if primaryServiceKey == "" {
|
||||||
return fmt.Errorf("couldn't find the primary service key")
|
return fmt.Errorf("couldn't find the primary service key")
|
||||||
}
|
}
|
||||||
|
err := s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port, existingNameserver)
|
||||||
err := s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -196,27 +195,32 @@ func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemConfigurator) getPrimaryService() string {
|
func (s *systemConfigurator) getPrimaryService() (string, string) {
|
||||||
line := buildCommandLine("show", globalIPv4State, "")
|
line := buildCommandLine("show", globalIPv4State, "")
|
||||||
stdinCommands := wrapCommand(line)
|
stdinCommands := wrapCommand(line)
|
||||||
b, err := runSystemConfigCommand(stdinCommands)
|
b, err := runSystemConfigCommand(stdinCommands)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("got error while sending the command: ", err)
|
log.Error("got error while sending the command: ", err)
|
||||||
return ""
|
return "", ""
|
||||||
}
|
}
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
scanner := bufio.NewScanner(bytes.NewReader(b))
|
||||||
|
primaryService := ""
|
||||||
|
router := ""
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
text := scanner.Text()
|
text := scanner.Text()
|
||||||
if strings.Contains(text, "PrimaryService") {
|
if strings.Contains(text, "PrimaryService") {
|
||||||
return strings.TrimSpace(strings.Split(text, ":")[1])
|
primaryService = strings.TrimSpace(strings.Split(text, ":")[1])
|
||||||
|
}
|
||||||
|
if strings.Contains(text, "Router") {
|
||||||
|
router = strings.TrimSpace(strings.Split(text, ":")[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return primaryService, router
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int) error {
|
func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int, existingDNSServer string) error {
|
||||||
lines := buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+strconv.Itoa(0))
|
lines := buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+strconv.Itoa(0))
|
||||||
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer)
|
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer+" "+existingDNSServer)
|
||||||
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
|
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
|
||||||
addDomainCommand := buildCreateStateWithOperation(setupKey, lines)
|
addDomainCommand := buildCreateStateWithOperation(setupKey, lines)
|
||||||
stdinCommands := wrapCommand(addDomainCommand)
|
stdinCommands := wrapCommand(addDomainCommand)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -59,7 +60,11 @@ func (r *resolvconf) applyDNSConfig(config hostDNSConfig) error {
|
|||||||
appendedDomains++
|
appendedDomains++
|
||||||
}
|
}
|
||||||
|
|
||||||
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains)
|
originalContent, err := os.ReadFile(fileDefaultResolvConfBackupLocation)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Could not read existing resolv.conf")
|
||||||
|
}
|
||||||
|
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains, string(originalContent))
|
||||||
|
|
||||||
err = r.applyConfig(content)
|
err = r.applyConfig(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type notifier struct {
|
|||||||
listener Listener
|
listener Listener
|
||||||
currentClientState bool
|
currentClientState bool
|
||||||
lastNotification int
|
lastNotification int
|
||||||
|
lastNumberOfPeers int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNotifier() *notifier {
|
func newNotifier() *notifier {
|
||||||
@@ -29,6 +30,7 @@ func (n *notifier) setListener(listener Listener) {
|
|||||||
|
|
||||||
n.serverStateLock.Lock()
|
n.serverStateLock.Lock()
|
||||||
n.notifyListener(listener, n.lastNotification)
|
n.notifyListener(listener, n.lastNotification)
|
||||||
|
listener.OnPeersListChanged(n.lastNumberOfPeers)
|
||||||
n.serverStateLock.Unlock()
|
n.serverStateLock.Unlock()
|
||||||
|
|
||||||
n.listener = listener
|
n.listener = listener
|
||||||
@@ -124,6 +126,7 @@ func (n *notifier) calculateState(managementConn, signalConn bool) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) peerListChanged(numOfPeers int) {
|
func (n *notifier) peerListChanged(numOfPeers int) {
|
||||||
|
n.lastNumberOfPeers = numOfPeers
|
||||||
n.listenersLock.Lock()
|
n.listenersLock.Lock()
|
||||||
defer n.listenersLock.Unlock()
|
defer n.listenersLock.Unlock()
|
||||||
if n.listener == nil {
|
if n.listener == nil {
|
||||||
|
|||||||
@@ -353,9 +353,13 @@ func (d *Status) onConnectionChanged() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) notifyPeerListChanged() {
|
func (d *Status) notifyPeerListChanged() {
|
||||||
d.notifier.peerListChanged(len(d.peers) + len(d.offlinePeers))
|
d.notifier.peerListChanged(d.numOfPeers())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) notifyAddressChanged() {
|
func (d *Status) notifyAddressChanged() {
|
||||||
d.notifier.localAddressChanged(d.localPeer.FQDN, d.localPeer.IP)
|
d.notifier.localAddressChanged(d.localPeer.FQDN, d.localPeer.IP)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Status) numOfPeers() int {
|
||||||
|
return len(d.peers) + len(d.offlinePeers)
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,13 +51,17 @@ type iptablesManager struct {
|
|||||||
|
|
||||||
func newIptablesManager(parentCtx context.Context) *iptablesManager {
|
func newIptablesManager(parentCtx context.Context) *iptablesManager {
|
||||||
ctx, cancel := context.WithCancel(parentCtx)
|
ctx, cancel := context.WithCancel(parentCtx)
|
||||||
ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
if !isIptablesClientAvailable(ipv4Client) {
|
if err != nil {
|
||||||
|
log.Debugf("failed to initialize iptables for ipv4: %s", err)
|
||||||
|
} else if !isIptablesClientAvailable(ipv4Client) {
|
||||||
log.Infof("iptables is missing for ipv4")
|
log.Infof("iptables is missing for ipv4")
|
||||||
ipv4Client = nil
|
ipv4Client = nil
|
||||||
}
|
}
|
||||||
ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
ipv6Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||||
if !isIptablesClientAvailable(ipv6Client) {
|
if err != nil {
|
||||||
|
log.Debugf("failed to initialize iptables for ipv6: %s", err)
|
||||||
|
} else if !isIptablesClientAvailable(ipv6Client) {
|
||||||
log.Infof("iptables is missing for ipv6")
|
log.Infof("iptables is missing for ipv6")
|
||||||
ipv6Client = nil
|
ipv6Client = nil
|
||||||
}
|
}
|
||||||
@@ -391,6 +395,10 @@ func (i *iptablesManager) insertRoutingRule(keyFormat, table, chain, jump string
|
|||||||
ipVersion = ipv6
|
ipVersion = ipv6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if iptablesClient == nil {
|
||||||
|
return fmt.Errorf("unable to insert iptables routing rules. Iptables client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
ruleKey := genKey(keyFormat, pair.ID)
|
ruleKey := genKey(keyFormat, pair.ID)
|
||||||
rule := genRuleSpec(jump, ruleKey, pair.source, pair.destination)
|
rule := genRuleSpec(jump, ruleKey, pair.source, pair.destination)
|
||||||
existingRule, found := i.rules[ipVersion][ruleKey]
|
existingRule, found := i.rules[ipVersion][ruleKey]
|
||||||
@@ -455,6 +463,10 @@ func (i *iptablesManager) removeRoutingRule(keyFormat, table, chain string, pair
|
|||||||
ipVersion = ipv6
|
ipVersion = ipv6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if iptablesClient == nil {
|
||||||
|
return fmt.Errorf("unable to remove iptables routing rules. Iptables client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
ruleKey := genKey(keyFormat, pair.ID)
|
ruleKey := genKey(keyFormat, pair.ID)
|
||||||
existingRule, found := i.rules[ipVersion][ruleKey]
|
existingRule, found := i.rules[ipVersion][ruleKey]
|
||||||
if found {
|
if found {
|
||||||
|
|||||||
@@ -54,14 +54,14 @@ type bpfSpecs struct {
|
|||||||
//
|
//
|
||||||
// It can be passed ebpf.CollectionSpec.Assign.
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
type bpfProgramSpecs struct {
|
type bpfProgramSpecs struct {
|
||||||
XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"`
|
NbWgProxy *ebpf.ProgramSpec `ebpf:"nb_wg_proxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfMapSpecs contains maps before they are loaded into the kernel.
|
// bpfMapSpecs contains maps before they are loaded into the kernel.
|
||||||
//
|
//
|
||||||
// It can be passed ebpf.CollectionSpec.Assign.
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
type bpfMapSpecs struct {
|
type bpfMapSpecs struct {
|
||||||
XdpPortMap *ebpf.MapSpec `ebpf:"xdp_port_map"`
|
NbWgProxySettingsMap *ebpf.MapSpec `ebpf:"nb_wg_proxy_settings_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfObjects contains all objects after they have been loaded into the kernel.
|
// bpfObjects contains all objects after they have been loaded into the kernel.
|
||||||
@@ -83,12 +83,12 @@ func (o *bpfObjects) Close() error {
|
|||||||
//
|
//
|
||||||
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
type bpfMaps struct {
|
type bpfMaps struct {
|
||||||
XdpPortMap *ebpf.Map `ebpf:"xdp_port_map"`
|
NbWgProxySettingsMap *ebpf.Map `ebpf:"nb_wg_proxy_settings_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *bpfMaps) Close() error {
|
func (m *bpfMaps) Close() error {
|
||||||
return _BpfClose(
|
return _BpfClose(
|
||||||
m.XdpPortMap,
|
m.NbWgProxySettingsMap,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +96,12 @@ func (m *bpfMaps) Close() error {
|
|||||||
//
|
//
|
||||||
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
type bpfPrograms struct {
|
type bpfPrograms struct {
|
||||||
XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"`
|
NbWgProxy *ebpf.Program `ebpf:"nb_wg_proxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *bpfPrograms) Close() error {
|
func (p *bpfPrograms) Close() error {
|
||||||
return _BpfClose(
|
return _BpfClose(
|
||||||
p.XdpProgFunc,
|
p.NbWgProxy,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -54,14 +54,14 @@ type bpfSpecs struct {
|
|||||||
//
|
//
|
||||||
// It can be passed ebpf.CollectionSpec.Assign.
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
type bpfProgramSpecs struct {
|
type bpfProgramSpecs struct {
|
||||||
XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"`
|
NbWgProxy *ebpf.ProgramSpec `ebpf:"nb_wg_proxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfMapSpecs contains maps before they are loaded into the kernel.
|
// bpfMapSpecs contains maps before they are loaded into the kernel.
|
||||||
//
|
//
|
||||||
// It can be passed ebpf.CollectionSpec.Assign.
|
// It can be passed ebpf.CollectionSpec.Assign.
|
||||||
type bpfMapSpecs struct {
|
type bpfMapSpecs struct {
|
||||||
XdpPortMap *ebpf.MapSpec `ebpf:"xdp_port_map"`
|
NbWgProxySettingsMap *ebpf.MapSpec `ebpf:"nb_wg_proxy_settings_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bpfObjects contains all objects after they have been loaded into the kernel.
|
// bpfObjects contains all objects after they have been loaded into the kernel.
|
||||||
@@ -83,12 +83,12 @@ func (o *bpfObjects) Close() error {
|
|||||||
//
|
//
|
||||||
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
type bpfMaps struct {
|
type bpfMaps struct {
|
||||||
XdpPortMap *ebpf.Map `ebpf:"xdp_port_map"`
|
NbWgProxySettingsMap *ebpf.Map `ebpf:"nb_wg_proxy_settings_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *bpfMaps) Close() error {
|
func (m *bpfMaps) Close() error {
|
||||||
return _BpfClose(
|
return _BpfClose(
|
||||||
m.XdpPortMap,
|
m.NbWgProxySettingsMap,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +96,12 @@ func (m *bpfMaps) Close() error {
|
|||||||
//
|
//
|
||||||
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||||
type bpfPrograms struct {
|
type bpfPrograms struct {
|
||||||
XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"`
|
NbWgProxy *ebpf.Program `ebpf:"nb_wg_proxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *bpfPrograms) Close() error {
|
func (p *bpfPrograms) Close() error {
|
||||||
return _BpfClose(
|
return _BpfClose(
|
||||||
p.XdpProgFunc,
|
p.NbWgProxy,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -50,22 +50,22 @@ func (l *EBPF) Load(proxyPort, wgPort int) error {
|
|||||||
_ = objs.Close()
|
_ = objs.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = objs.XdpPortMap.Put(mapKeyProxyPort, uint16(proxyPort))
|
err = objs.NbWgProxySettingsMap.Put(mapKeyProxyPort, uint16(proxyPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = objs.XdpPortMap.Put(mapKeyWgPort, uint16(wgPort))
|
err = objs.NbWgProxySettingsMap.Put(mapKeyWgPort, uint16(wgPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = objs.XdpPortMap.Close()
|
_ = objs.NbWgProxySettingsMap.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
l.link, err = link.AttachXDP(link.XDPOptions{
|
l.link, err = link.AttachXDP(link.XDPOptions{
|
||||||
Program: objs.XdpProgFunc,
|
Program: objs.NbWgProxy,
|
||||||
Interface: ifce.Index,
|
Interface: ifce.Index,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -75,7 +75,7 @@ func (l *EBPF) Load(proxyPort, wgPort int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free free ebpf program
|
// Free ebpf program
|
||||||
func (l *EBPF) Free() error {
|
func (l *EBPF) Free() error {
|
||||||
if l.link != nil {
|
if l.link != nil {
|
||||||
return l.link.Close()
|
return l.link.Close()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
const __u32 map_key_proxy_port = 0;
|
const __u32 map_key_proxy_port = 0;
|
||||||
const __u32 map_key_wg_port = 1;
|
const __u32 map_key_wg_port = 1;
|
||||||
|
|
||||||
struct bpf_map_def SEC("maps") xdp_port_map = {
|
struct bpf_map_def SEC("maps") nb_wg_proxy_settings_map = {
|
||||||
.type = BPF_MAP_TYPE_ARRAY,
|
.type = BPF_MAP_TYPE_ARRAY,
|
||||||
.key_size = sizeof(__u32),
|
.key_size = sizeof(__u32),
|
||||||
.value_size = sizeof(__u16),
|
.value_size = sizeof(__u16),
|
||||||
@@ -27,14 +27,14 @@ __u16 wg_port = 0;
|
|||||||
|
|
||||||
bool read_port_settings() {
|
bool read_port_settings() {
|
||||||
__u16 *value;
|
__u16 *value;
|
||||||
value = bpf_map_lookup_elem(&xdp_port_map, &map_key_proxy_port);
|
value = bpf_map_lookup_elem(&nb_wg_proxy_settings_map, &map_key_proxy_port);
|
||||||
if(!value) {
|
if(!value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy_port = *value;
|
proxy_port = *value;
|
||||||
|
|
||||||
value = bpf_map_lookup_elem(&xdp_port_map, &map_key_wg_port);
|
value = bpf_map_lookup_elem(&nb_wg_proxy_settings_map, &map_key_wg_port);
|
||||||
if(!value) {
|
if(!value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ bool read_port_settings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SEC("xdp")
|
SEC("xdp")
|
||||||
int xdp_prog_func(struct xdp_md *ctx) {
|
int nb_wg_proxy(struct xdp_md *ctx) {
|
||||||
if(proxy_port == 0 || wg_port == 0) {
|
if(proxy_port == 0 || wg_port == 0) {
|
||||||
if(!read_port_settings()){
|
if(!read_port_settings()){
|
||||||
return XDP_PASS;
|
return XDP_PASS;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func (w *Factory) GetProxy() Proxy {
|
|||||||
|
|
||||||
func (w *Factory) Free() error {
|
func (w *Factory) Free() error {
|
||||||
if w.ebpfProxy != nil {
|
if w.ebpfProxy != nil {
|
||||||
return w.ebpfProxy.CloseConn()
|
return w.ebpfProxy.Free()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func NewFactory(wgPort int) *Factory {
|
|||||||
ebpfProxy := NewWGEBPFProxy(wgPort)
|
ebpfProxy := NewWGEBPFProxy(wgPort)
|
||||||
err := ebpfProxy.Listen()
|
err := ebpfProxy.Listen()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to initialize ebpf proxy: %s", err)
|
log.Warnf("failed to initialize ebpf proxy, fallback to user space proxy: %s", err)
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func (p *WGEBPFProxy) Listen() error {
|
|||||||
p.conn, err = net.ListenUDP("udp", &addr)
|
p.conn, err = net.ListenUDP("udp", &addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cErr := p.Free()
|
cErr := p.Free()
|
||||||
if err != nil {
|
if cErr != nil {
|
||||||
log.Errorf("failed to close the wgproxy: %s", cErr)
|
log.Errorf("failed to close the wgproxy: %s", cErr)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -104,6 +104,7 @@ func (p *WGEBPFProxy) CloseConn() error {
|
|||||||
|
|
||||||
// Free resources
|
// Free resources
|
||||||
func (p *WGEBPFProxy) Free() error {
|
func (p *WGEBPFProxy) Free() error {
|
||||||
|
log.Debugf("free up ebpf wg proxy")
|
||||||
var err1, err2, err3 error
|
var err1, err2, err3 error
|
||||||
if p.conn != nil {
|
if p.conn != nil {
|
||||||
err1 = p.conn.Close()
|
err1 = p.conn.Close()
|
||||||
@@ -153,7 +154,9 @@ func (p *WGEBPFProxy) proxyToRemote() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.turnConnMutex.Lock()
|
||||||
conn, ok := p.turnConnStore[uint16(addr.Port)]
|
conn, ok := p.turnConnStore[uint16(addr.Port)]
|
||||||
|
p.turnConnMutex.Unlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Errorf("turn conn not found by port: %d", addr.Port)
|
log.Errorf("turn conn not found by port: %d", addr.Port)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type WGUserSpaceProxy struct {
|
|||||||
|
|
||||||
// NewWGUserSpaceProxy instantiate a user space WireGuard proxy
|
// NewWGUserSpaceProxy instantiate a user space WireGuard proxy
|
||||||
func NewWGUserSpaceProxy(wgPort int) *WGUserSpaceProxy {
|
func NewWGUserSpaceProxy(wgPort int) *WGUserSpaceProxy {
|
||||||
|
log.Debugf("instantiate new userspace proxy")
|
||||||
p := &WGUserSpaceProxy{
|
p := &WGUserSpaceProxy{
|
||||||
localWGListenPort: wgPort,
|
localWGListenPort: wgPort,
|
||||||
}
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -31,7 +31,7 @@ require (
|
|||||||
fyne.io/fyne/v2 v2.1.4
|
fyne.io/fyne/v2 v2.1.4
|
||||||
github.com/c-robinson/iplib v1.0.3
|
github.com/c-robinson/iplib v1.0.3
|
||||||
github.com/cilium/ebpf v0.10.0
|
github.com/cilium/ebpf v0.10.0
|
||||||
github.com/coreos/go-iptables v0.6.0
|
github.com/coreos/go-iptables v0.7.0
|
||||||
github.com/creack/pty v1.1.18
|
github.com/creack/pty v1.1.18
|
||||||
github.com/eko/gocache/v3 v3.1.1
|
github.com/eko/gocache/v3 v3.1.1
|
||||||
github.com/getlantern/systray v1.2.1
|
github.com/getlantern/systray v1.2.1
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -131,8 +131,8 @@ github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcD
|
|||||||
github.com/coocood/freecache v1.2.1 h1:/v1CqMq45NFH9mp/Pt142reundeBM0dVUD3osQBeu/U=
|
github.com/coocood/freecache v1.2.1 h1:/v1CqMq45NFH9mp/Pt142reundeBM0dVUD3osQBeu/U=
|
||||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk=
|
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
|
||||||
github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||||
|
|||||||
736
infrastructure_files/getting-started-with-zitadel.sh
Normal file
736
infrastructure_files/getting-started-with-zitadel.sh
Normal 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
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -371,24 +371,24 @@ func handlerFunc(gRPCHandler *grpc.Server, httpHandler http.Handler) http.Handle
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
|
func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
|
||||||
config := &server.Config{}
|
loadedConfig := &server.Config{}
|
||||||
_, err := util.ReadJson(mgmtConfigPath, config)
|
_, err := util.ReadJson(mgmtConfigPath, loadedConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if mgmtLetsencryptDomain != "" {
|
if mgmtLetsencryptDomain != "" {
|
||||||
config.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain
|
loadedConfig.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain
|
||||||
}
|
}
|
||||||
if mgmtDataDir != "" {
|
if mgmtDataDir != "" {
|
||||||
config.Datadir = mgmtDataDir
|
loadedConfig.Datadir = mgmtDataDir
|
||||||
}
|
}
|
||||||
|
|
||||||
if certKey != "" && certFile != "" {
|
if certKey != "" && certFile != "" {
|
||||||
config.HttpConfig.CertFile = certFile
|
loadedConfig.HttpConfig.CertFile = certFile
|
||||||
config.HttpConfig.CertKey = certKey
|
loadedConfig.HttpConfig.CertKey = certKey
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcEndpoint := config.HttpConfig.OIDCConfigEndpoint
|
oidcEndpoint := loadedConfig.HttpConfig.OIDCConfigEndpoint
|
||||||
if oidcEndpoint != "" {
|
if oidcEndpoint != "" {
|
||||||
// if OIDCConfigEndpoint is specified, we can load DeviceAuthEndpoint and TokenEndpoint automatically
|
// if OIDCConfigEndpoint is specified, we can load DeviceAuthEndpoint and TokenEndpoint automatically
|
||||||
log.Infof("loading OIDC configuration from the provided IDP configuration endpoint %s", oidcEndpoint)
|
log.Infof("loading OIDC configuration from the provided IDP configuration endpoint %s", oidcEndpoint)
|
||||||
@@ -399,45 +399,45 @@ func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
|
|||||||
log.Infof("loaded OIDC configuration from the provided IDP configuration endpoint: %s", oidcEndpoint)
|
log.Infof("loaded OIDC configuration from the provided IDP configuration endpoint: %s", oidcEndpoint)
|
||||||
|
|
||||||
log.Infof("overriding HttpConfig.AuthIssuer with a new value %s, previously configured value: %s",
|
log.Infof("overriding HttpConfig.AuthIssuer with a new value %s, previously configured value: %s",
|
||||||
oidcConfig.Issuer, config.HttpConfig.AuthIssuer)
|
oidcConfig.Issuer, loadedConfig.HttpConfig.AuthIssuer)
|
||||||
config.HttpConfig.AuthIssuer = oidcConfig.Issuer
|
loadedConfig.HttpConfig.AuthIssuer = oidcConfig.Issuer
|
||||||
|
|
||||||
log.Infof("overriding HttpConfig.AuthKeysLocation (JWT certs) with a new value %s, previously configured value: %s",
|
log.Infof("overriding HttpConfig.AuthKeysLocation (JWT certs) with a new value %s, previously configured value: %s",
|
||||||
oidcConfig.JwksURI, config.HttpConfig.AuthKeysLocation)
|
oidcConfig.JwksURI, loadedConfig.HttpConfig.AuthKeysLocation)
|
||||||
config.HttpConfig.AuthKeysLocation = oidcConfig.JwksURI
|
loadedConfig.HttpConfig.AuthKeysLocation = oidcConfig.JwksURI
|
||||||
|
|
||||||
if !(config.DeviceAuthorizationFlow == nil || strings.ToLower(config.DeviceAuthorizationFlow.Provider) == string(server.NONE)) {
|
if !(loadedConfig.DeviceAuthorizationFlow == nil || strings.ToLower(loadedConfig.DeviceAuthorizationFlow.Provider) == string(server.NONE)) {
|
||||||
log.Infof("overriding DeviceAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
|
log.Infof("overriding DeviceAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
|
||||||
oidcConfig.TokenEndpoint, config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint)
|
oidcConfig.TokenEndpoint, loadedConfig.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint)
|
||||||
config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
|
loadedConfig.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
|
||||||
log.Infof("overriding DeviceAuthorizationFlow.DeviceAuthEndpoint with a new value: %s, previously configured value: %s",
|
log.Infof("overriding DeviceAuthorizationFlow.DeviceAuthEndpoint with a new value: %s, previously configured value: %s",
|
||||||
oidcConfig.DeviceAuthEndpoint, config.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint)
|
oidcConfig.DeviceAuthEndpoint, loadedConfig.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint)
|
||||||
config.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint = oidcConfig.DeviceAuthEndpoint
|
loadedConfig.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint = oidcConfig.DeviceAuthEndpoint
|
||||||
|
|
||||||
u, err := url.Parse(oidcEndpoint)
|
u, err := url.Parse(oidcEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Infof("overriding DeviceAuthorizationFlow.ProviderConfig.Domain with a new value: %s, previously configured value: %s",
|
log.Infof("overriding DeviceAuthorizationFlow.ProviderConfig.Domain with a new value: %s, previously configured value: %s",
|
||||||
u.Host, config.DeviceAuthorizationFlow.ProviderConfig.Domain)
|
u.Host, loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Domain)
|
||||||
config.DeviceAuthorizationFlow.ProviderConfig.Domain = u.Host
|
loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Domain = u.Host
|
||||||
|
|
||||||
if config.DeviceAuthorizationFlow.ProviderConfig.Scope == "" {
|
if loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Scope == "" {
|
||||||
config.DeviceAuthorizationFlow.ProviderConfig.Scope = server.DefaultDeviceAuthFlowScope
|
loadedConfig.DeviceAuthorizationFlow.ProviderConfig.Scope = server.DefaultDeviceAuthFlowScope
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.PKCEAuthorizationFlow != nil {
|
if loadedConfig.PKCEAuthorizationFlow != nil {
|
||||||
log.Infof("overriding PKCEAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
|
log.Infof("overriding PKCEAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
|
||||||
oidcConfig.TokenEndpoint, config.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint)
|
oidcConfig.TokenEndpoint, loadedConfig.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint)
|
||||||
config.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
|
loadedConfig.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
|
||||||
log.Infof("overriding PKCEAuthorizationFlow.AuthorizationEndpoint with a new value: %s, previously configured value: %s",
|
log.Infof("overriding PKCEAuthorizationFlow.AuthorizationEndpoint with a new value: %s, previously configured value: %s",
|
||||||
oidcConfig.AuthorizationEndpoint, config.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint)
|
oidcConfig.AuthorizationEndpoint, loadedConfig.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint)
|
||||||
config.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint = oidcConfig.AuthorizationEndpoint
|
loadedConfig.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint = oidcConfig.AuthorizationEndpoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, err
|
return loadedConfig, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIDCConfigResponse used for parsing OIDC config response
|
// OIDCConfigResponse used for parsing OIDC config response
|
||||||
|
|||||||
@@ -139,10 +139,14 @@ type DefaultAccountManager struct {
|
|||||||
type Settings struct {
|
type Settings struct {
|
||||||
// PeerLoginExpirationEnabled globally enables or disables peer login expiration
|
// PeerLoginExpirationEnabled globally enables or disables peer login expiration
|
||||||
PeerLoginExpirationEnabled bool
|
PeerLoginExpirationEnabled bool
|
||||||
|
|
||||||
// PeerLoginExpiration is a setting that indicates when peer login expires.
|
// PeerLoginExpiration is a setting that indicates when peer login expires.
|
||||||
// Applies to all peers that have Peer.LoginExpirationEnabled set to true.
|
// Applies to all peers that have Peer.LoginExpirationEnabled set to true.
|
||||||
PeerLoginExpiration time.Duration
|
PeerLoginExpiration time.Duration
|
||||||
|
|
||||||
|
// GroupsPropagationEnabled allows to propagate auto groups from the user to the peer
|
||||||
|
GroupsPropagationEnabled bool
|
||||||
|
|
||||||
// JWTGroupsEnabled allows extract groups from JWT claim, which name defined in the JWTGroupsClaimName
|
// JWTGroupsEnabled allows extract groups from JWT claim, which name defined in the JWTGroupsClaimName
|
||||||
// and add it to account groups.
|
// and add it to account groups.
|
||||||
JWTGroupsEnabled bool
|
JWTGroupsEnabled bool
|
||||||
@@ -158,6 +162,7 @@ func (s *Settings) Copy() *Settings {
|
|||||||
PeerLoginExpiration: s.PeerLoginExpiration,
|
PeerLoginExpiration: s.PeerLoginExpiration,
|
||||||
JWTGroupsEnabled: s.JWTGroupsEnabled,
|
JWTGroupsEnabled: s.JWTGroupsEnabled,
|
||||||
JWTGroupsClaimName: s.JWTGroupsClaimName,
|
JWTGroupsClaimName: s.JWTGroupsClaimName,
|
||||||
|
GroupsPropagationEnabled: s.GroupsPropagationEnabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,26 +629,96 @@ func (a *Account) GetPeer(peerID string) *Peer {
|
|||||||
return a.Peers[peerID]
|
return a.Peers[peerID]
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddJWTGroups to existed groups if they does not exists
|
// AddJWTGroups to account and to user autoassigned groups
|
||||||
func (a *Account) AddJWTGroups(groups []string) (int, error) {
|
func (a *Account) AddJWTGroups(userID string, groups []string) bool {
|
||||||
existedGroups := make(map[string]*Group)
|
user, ok := a.Users[userID]
|
||||||
for _, g := range a.Groups {
|
if !ok {
|
||||||
existedGroups[g.Name] = g
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var count int
|
existedGroupsByName := make(map[string]*Group)
|
||||||
|
for _, 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 {
|
for _, name := range groups {
|
||||||
if _, ok := existedGroups[name]; !ok {
|
group, ok := existedGroupsByName[name]
|
||||||
id := xid.New().String()
|
if !ok {
|
||||||
a.Groups[id] = &Group{
|
group = &Group{
|
||||||
ID: id,
|
ID: xid.New().String(),
|
||||||
Name: name,
|
Name: name,
|
||||||
Issued: GroupIssuedJWT,
|
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
|
// 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)
|
log.Errorf("JWT claim %q is not a string: %v", account.Settings.JWTGroupsClaimName, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
n, err := account.AddJWTGroups(groups)
|
// if groups were added or modified, save the account
|
||||||
if err != nil {
|
if account.AddJWTGroups(claims.UserId, groups) {
|
||||||
log.Errorf("failed to add JWT groups: %v", err)
|
if account.Settings.GroupsPropagationEnabled {
|
||||||
|
if user, err := account.FindUser(claims.UserId); err == nil {
|
||||||
|
account.UserGroupsAddToPeers(claims.UserId, append(user.AutoGroups, groups...)...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if n > 0 {
|
|
||||||
if err := am.Store.SaveAccount(account); err != nil {
|
if err := am.Store.SaveAccount(account); err != nil {
|
||||||
log.Errorf("failed to save account: %v", err)
|
log.Errorf("failed to save account: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,6 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
|
|||||||
assert.Len(t, networkMap.Peers, len(testCase.expectedPeers))
|
assert.Len(t, networkMap.Peers, len(testCase.expectedPeers))
|
||||||
assert.Len(t, networkMap.OfflinePeers, len(testCase.expectedOfflinePeers))
|
assert.Len(t, networkMap.OfflinePeers, len(testCase.expectedOfflinePeers))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewAccount(t *testing.T) {
|
func TestNewAccount(t *testing.T) {
|
||||||
@@ -1931,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) {
|
func createManager(t *testing.T) (*DefaultAccountManager, error) {
|
||||||
store, err := createStore(t)
|
store, err := createStore(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -80,12 +80,14 @@ func (h *AccountsHandler) UpdateAccount(w http.ResponseWriter, r *http.Request)
|
|||||||
if req.Settings.JwtGroupsEnabled != nil {
|
if req.Settings.JwtGroupsEnabled != nil {
|
||||||
settings.JWTGroupsEnabled = *req.Settings.JwtGroupsEnabled
|
settings.JWTGroupsEnabled = *req.Settings.JwtGroupsEnabled
|
||||||
}
|
}
|
||||||
|
if req.Settings.GroupsPropagationEnabled != nil {
|
||||||
|
settings.GroupsPropagationEnabled = *req.Settings.GroupsPropagationEnabled
|
||||||
|
}
|
||||||
if req.Settings.JwtGroupsClaimName != nil {
|
if req.Settings.JwtGroupsClaimName != nil {
|
||||||
settings.JWTGroupsClaimName = *req.Settings.JwtGroupsClaimName
|
settings.JWTGroupsClaimName = *req.Settings.JwtGroupsClaimName
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedAccount, err := h.accountManager.UpdateAccountSettings(accountID, user.Id, settings)
|
updatedAccount, err := h.accountManager.UpdateAccountSettings(accountID, user.Id, settings)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteError(err, w)
|
util.WriteError(err, w)
|
||||||
return
|
return
|
||||||
@@ -102,6 +104,7 @@ func toAccountResponse(account *server.Account) *api.Account {
|
|||||||
Settings: api.AccountSettings{
|
Settings: api.AccountSettings{
|
||||||
PeerLoginExpiration: int(account.Settings.PeerLoginExpiration.Seconds()),
|
PeerLoginExpiration: int(account.Settings.PeerLoginExpiration.Seconds()),
|
||||||
PeerLoginExpirationEnabled: account.Settings.PeerLoginExpirationEnabled,
|
PeerLoginExpirationEnabled: account.Settings.PeerLoginExpirationEnabled,
|
||||||
|
GroupsPropagationEnabled: &account.Settings.GroupsPropagationEnabled,
|
||||||
JwtGroupsEnabled: &account.Settings.JWTGroupsEnabled,
|
JwtGroupsEnabled: &account.Settings.JWTGroupsEnabled,
|
||||||
JwtGroupsClaimName: &account.Settings.JWTGroupsClaimName,
|
JwtGroupsClaimName: &account.Settings.JWTGroupsClaimName,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ func initAccountsTestData(account *server.Account, admin *server.User) *Accounts
|
|||||||
accCopy := account.Copy()
|
accCopy := account.Copy()
|
||||||
accCopy.UpdateSettings(newSettings)
|
accCopy.UpdateSettings(newSettings)
|
||||||
return accCopy, nil
|
return accCopy, nil
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
claimsExtractor: jwtclaims.NewClaimsExtractor(
|
claimsExtractor: jwtclaims.NewClaimsExtractor(
|
||||||
@@ -54,7 +53,6 @@ func initAccountsTestData(account *server.Account, admin *server.User) *Accounts
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAccounts_AccountsHandler(t *testing.T) {
|
func TestAccounts_AccountsHandler(t *testing.T) {
|
||||||
|
|
||||||
accountID := "test_account"
|
accountID := "test_account"
|
||||||
adminUser := server.NewAdminUser("test_user")
|
adminUser := server.NewAdminUser("test_user")
|
||||||
|
|
||||||
@@ -94,6 +92,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
expectedSettings: api.AccountSettings{
|
expectedSettings: api.AccountSettings{
|
||||||
PeerLoginExpiration: int(time.Hour.Seconds()),
|
PeerLoginExpiration: int(time.Hour.Seconds()),
|
||||||
PeerLoginExpirationEnabled: false,
|
PeerLoginExpirationEnabled: false,
|
||||||
|
GroupsPropagationEnabled: br(false),
|
||||||
JwtGroupsClaimName: sr(""),
|
JwtGroupsClaimName: sr(""),
|
||||||
JwtGroupsEnabled: br(false),
|
JwtGroupsEnabled: br(false),
|
||||||
},
|
},
|
||||||
@@ -110,6 +109,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
expectedSettings: api.AccountSettings{
|
expectedSettings: api.AccountSettings{
|
||||||
PeerLoginExpiration: 15552000,
|
PeerLoginExpiration: 15552000,
|
||||||
PeerLoginExpirationEnabled: true,
|
PeerLoginExpirationEnabled: true,
|
||||||
|
GroupsPropagationEnabled: br(false),
|
||||||
JwtGroupsClaimName: sr(""),
|
JwtGroupsClaimName: sr(""),
|
||||||
JwtGroupsEnabled: br(false),
|
JwtGroupsEnabled: br(false),
|
||||||
},
|
},
|
||||||
@@ -126,12 +126,30 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
|||||||
expectedSettings: api.AccountSettings{
|
expectedSettings: api.AccountSettings{
|
||||||
PeerLoginExpiration: 15552000,
|
PeerLoginExpiration: 15552000,
|
||||||
PeerLoginExpirationEnabled: false,
|
PeerLoginExpirationEnabled: false,
|
||||||
|
GroupsPropagationEnabled: br(false),
|
||||||
JwtGroupsClaimName: sr("roles"),
|
JwtGroupsClaimName: sr("roles"),
|
||||||
JwtGroupsEnabled: br(true),
|
JwtGroupsEnabled: br(true),
|
||||||
},
|
},
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedID: accountID,
|
expectedID: accountID,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "PutAccount OK wiht JWT Propagation",
|
||||||
|
expectedBody: true,
|
||||||
|
requestType: http.MethodPut,
|
||||||
|
requestPath: "/api/accounts/" + accountID,
|
||||||
|
requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 554400,\"peer_login_expiration_enabled\": true,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"groups\",\"groups_propagation_enabled\":true}}"),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedSettings: api.AccountSettings{
|
||||||
|
PeerLoginExpiration: 554400,
|
||||||
|
PeerLoginExpirationEnabled: true,
|
||||||
|
GroupsPropagationEnabled: br(true),
|
||||||
|
JwtGroupsClaimName: sr("groups"),
|
||||||
|
JwtGroupsEnabled: br(true),
|
||||||
|
},
|
||||||
|
expectedArray: false,
|
||||||
|
expectedID: accountID,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Update account failure with high peer_login_expiration more than 180 days",
|
name: "Update account failure with high peer_login_expiration more than 180 days",
|
||||||
expectedBody: true,
|
expectedBody: true,
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ components:
|
|||||||
description: Period of time after which peer login expires (seconds).
|
description: Period of time after which peer login expires (seconds).
|
||||||
type: integer
|
type: integer
|
||||||
example: 43200
|
example: 43200
|
||||||
|
groups_propagation_enabled:
|
||||||
|
description: Allows propagate the new user auto groups to peers that belongs to the user
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
jwt_groups_enabled:
|
jwt_groups_enabled:
|
||||||
description: Allows extract groups from JWT claim and add it to account groups.
|
description: Allows extract groups from JWT claim and add it to account groups.
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -327,7 +331,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
example: valid
|
example: valid
|
||||||
auto_groups:
|
auto_groups:
|
||||||
description: Setup key groups to auto-assign to peers registered with this key
|
description: List of group IDs to auto-assign to peers registered with this key
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
@@ -369,13 +373,15 @@ components:
|
|||||||
expires_in:
|
expires_in:
|
||||||
description: Expiration time in seconds
|
description: Expiration time in seconds
|
||||||
type: integer
|
type: integer
|
||||||
example: 43200
|
minimum: 86400
|
||||||
|
maximum: 31536000
|
||||||
|
example: 86400
|
||||||
revoked:
|
revoked:
|
||||||
description: Setup key revocation status
|
description: Setup key revocation status
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
auto_groups:
|
auto_groups:
|
||||||
description: Setup key groups to auto-assign to peers registered with this key
|
description: List of group IDs to auto-assign to peers registered with this key
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@@ -129,6 +129,9 @@ type AccountRequest struct {
|
|||||||
|
|
||||||
// AccountSettings defines model for AccountSettings.
|
// AccountSettings defines model for AccountSettings.
|
||||||
type AccountSettings struct {
|
type AccountSettings struct {
|
||||||
|
// GroupsPropagationEnabled Allows propagate the new user auto groups to peers that belongs to the user
|
||||||
|
GroupsPropagationEnabled *bool `json:"groups_propagation_enabled,omitempty"`
|
||||||
|
|
||||||
// JwtGroupsClaimName Name of the claim from which we extract groups names to add it to account groups.
|
// JwtGroupsClaimName Name of the claim from which we extract groups names to add it to account groups.
|
||||||
JwtGroupsClaimName *string `json:"jwt_groups_claim_name,omitempty"`
|
JwtGroupsClaimName *string `json:"jwt_groups_claim_name,omitempty"`
|
||||||
|
|
||||||
@@ -681,7 +684,7 @@ type RuleRequest struct {
|
|||||||
|
|
||||||
// SetupKey defines model for SetupKey.
|
// SetupKey defines model for SetupKey.
|
||||||
type SetupKey struct {
|
type SetupKey struct {
|
||||||
// AutoGroups Setup key groups to auto-assign to peers registered with this key
|
// AutoGroups List of group IDs to auto-assign to peers registered with this key
|
||||||
AutoGroups []string `json:"auto_groups"`
|
AutoGroups []string `json:"auto_groups"`
|
||||||
|
|
||||||
// Expires Setup Key expiration date
|
// Expires Setup Key expiration date
|
||||||
@@ -723,7 +726,7 @@ type SetupKey struct {
|
|||||||
|
|
||||||
// SetupKeyRequest defines model for SetupKeyRequest.
|
// SetupKeyRequest defines model for SetupKeyRequest.
|
||||||
type SetupKeyRequest struct {
|
type SetupKeyRequest struct {
|
||||||
// AutoGroups Setup key groups to auto-assign to peers registered with this key
|
// AutoGroups List of group IDs to auto-assign to peers registered with this key
|
||||||
AutoGroups []string `json:"auto_groups"`
|
AutoGroups []string `json:"auto_groups"`
|
||||||
|
|
||||||
// ExpiresIn Expiration time in seconds
|
// ExpiresIn Expiration time in seconds
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ func (h *SetupKeysHandler) CreateSetupKey(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
expiresIn := time.Duration(req.ExpiresIn) * time.Second
|
expiresIn := time.Duration(req.ExpiresIn) * time.Second
|
||||||
|
|
||||||
|
day := time.Hour * 24
|
||||||
|
year := day * 365
|
||||||
|
if expiresIn < day || expiresIn > year {
|
||||||
|
util.WriteError(status.Errorf(status.InvalidArgument, "expiresIn should be between 1 day and 365 days"), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if req.AutoGroups == nil {
|
if req.AutoGroups == nil {
|
||||||
req.AutoGroups = []string{}
|
req.AutoGroups = []string{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ func TestSetupKeysHandlers(t *testing.T) {
|
|||||||
requestType: http.MethodPost,
|
requestType: http.MethodPost,
|
||||||
requestPath: "/api/setup-keys",
|
requestPath: "/api/setup-keys",
|
||||||
requestBody: bytes.NewBuffer(
|
requestBody: bytes.NewBuffer(
|
||||||
[]byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"%s\"}", newSetupKey.Name, newSetupKey.Type))),
|
[]byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"%s\",\"expires_in\":86400}", newSetupKey.Name, newSetupKey.Type))),
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
expectedBody: true,
|
expectedBody: true,
|
||||||
expectedSetupKey: toResponseBody(newSetupKey),
|
expectedSetupKey: toResponseBody(newSetupKey),
|
||||||
|
|||||||
@@ -260,7 +260,6 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite
|
|||||||
am.storeEvent(userID, newUser.Id, accountID, activity.UserInvited, nil)
|
am.storeEvent(userID, newUser.Id, accountID, activity.UserInvited, nil)
|
||||||
|
|
||||||
return newUser.ToUserInfo(idpUser)
|
return newUser.ToUserInfo(idpUser)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUser looks up a user by provided authorization claims.
|
// GetUser looks up a user by provided authorization claims.
|
||||||
@@ -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 {
|
if err = am.Store.SaveAccount(account); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -640,7 +646,6 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if !isNil(am.idpManager) && !newUser.IsServiceUser {
|
if !isNil(am.idpManager) && !newUser.IsServiceUser {
|
||||||
|
|||||||
Reference in New Issue
Block a user