mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-21 17:56:39 +00:00
Compare commits
29 Commits
feature/in
...
feature/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45de3c1b06 | ||
|
|
04e4407ea7 | ||
|
|
06055af361 | ||
|
|
abd1230a69 | ||
|
|
f7de12daf8 | ||
|
|
c49fb0c40c | ||
|
|
6e9a162877 | ||
|
|
b4e03f4616 | ||
|
|
369a7ef345 | ||
|
|
c88e6a7342 | ||
|
|
2cd9b11e7d | ||
|
|
93d20e370b | ||
|
|
878ca6db22 | ||
|
|
2033650908 | ||
|
|
34c1c7d901 | ||
|
|
051fd3a4d7 | ||
|
|
af69a48745 | ||
|
|
68ff97ba84 | ||
|
|
c5705803a5 | ||
|
|
7e1ae448e0 | ||
|
|
518a2561a2 | ||
|
|
c75ffd0f4b | ||
|
|
e4ad6174ca | ||
|
|
6de313070a | ||
|
|
cd7d1a80c9 | ||
|
|
be7d829858 | ||
|
|
ed1872560f | ||
|
|
de898899a4 | ||
|
|
b63ec71aed |
7
.github/workflows/golang-test-darwin.yml
vendored
7
.github/workflows/golang-test-darwin.yml
vendored
@@ -1,5 +1,10 @@
|
|||||||
name: Test Code Darwin
|
name: Test Code Darwin
|
||||||
on: [push,pull_request]
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
59
.github/workflows/golang-test-linux.yml
vendored
59
.github/workflows/golang-test-linux.yml
vendored
@@ -1,5 +1,10 @@
|
|||||||
name: Test Code Linux
|
name: Test Code Linux
|
||||||
on: [push,pull_request]
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -33,3 +38,55 @@ jobs:
|
|||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: GOARCH=${{ matrix.arch }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
|
run: GOARCH=${{ matrix.arch }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
|
||||||
|
|
||||||
|
test_client_on_docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.x
|
||||||
|
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
|
||||||
|
|
||||||
|
- name: Install modules
|
||||||
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: Generate Iface Test bin
|
||||||
|
run: go test -c -o iface-testing.bin ./iface/...
|
||||||
|
|
||||||
|
- name: Generate RouteManager Test bin
|
||||||
|
run: go test -c -o routemanager-testing.bin ./client/internal/routemanager/...
|
||||||
|
|
||||||
|
- name: Generate Engine Test bin
|
||||||
|
run: go test -c -o engine-testing.bin ./client/internal/*.go
|
||||||
|
|
||||||
|
- name: Generate Peer Test bin
|
||||||
|
run: go test -c -o peer-testing.bin ./client/internal/peer/...
|
||||||
|
|
||||||
|
- run: chmod +x *testing.bin
|
||||||
|
|
||||||
|
- name: Run Iface tests in docker
|
||||||
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/iface --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/iface-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
|
|
||||||
|
- name: Run RouteManager tests in docker
|
||||||
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/routemanager-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
|
|
||||||
|
- name: Run Engine tests in docker
|
||||||
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
|
|
||||||
|
- name: Run Peer tests in docker
|
||||||
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/peer --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/peer-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
7
.github/workflows/golang-test-windows.yml
vendored
7
.github/workflows/golang-test-windows.yml
vendored
@@ -1,5 +1,10 @@
|
|||||||
name: Test Code Windows
|
name: Test Code Windows
|
||||||
on: [push,pull_request]
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre:
|
pre:
|
||||||
|
|||||||
12
.github/workflows/test-docker-compose-linux.yml
vendored
12
.github/workflows/test-docker-compose-linux.yml
vendored
@@ -1,5 +1,10 @@
|
|||||||
name: Test Docker Compose Linux
|
name: Test Docker Compose Linux
|
||||||
on: [push,pull_request]
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -51,13 +56,16 @@ jobs:
|
|||||||
CI_NETBIRD_AUTH_JWT_CERTS: https://example.eu.auth0.com/.well-known/jwks.json
|
CI_NETBIRD_AUTH_JWT_CERTS: https://example.eu.auth0.com/.well-known/jwks.json
|
||||||
CI_NETBIRD_AUTH_TOKEN_ENDPOINT: https://example.eu.auth0.com/oauth/token
|
CI_NETBIRD_AUTH_TOKEN_ENDPOINT: https://example.eu.auth0.com/oauth/token
|
||||||
CI_NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT: https://example.eu.auth0.com/oauth/device/code
|
CI_NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT: https://example.eu.auth0.com/oauth/device/code
|
||||||
|
CI_NETBIRD_AUTH_REDIRECT_URI: "/peers"
|
||||||
run: |
|
run: |
|
||||||
grep AUTH_CLIENT_ID docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_ID
|
grep AUTH_CLIENT_ID docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_ID
|
||||||
grep AUTH_AUTHORITY docker-compose.yml | grep $CI_NETBIRD_AUTH_AUTHORITY
|
grep AUTH_AUTHORITY docker-compose.yml | grep $CI_NETBIRD_AUTH_AUTHORITY
|
||||||
grep AUTH_AUDIENCE docker-compose.yml | grep $CI_NETBIRD_AUTH_AUDIENCE
|
grep AUTH_AUDIENCE docker-compose.yml | grep $CI_NETBIRD_AUTH_AUDIENCE
|
||||||
grep AUTH_SUPPORTED_SCOPES docker-compose.yml | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
|
grep AUTH_SUPPORTED_SCOPES docker-compose.yml | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
|
||||||
grep USE_AUTH0 docker-compose.yml | grep $CI_NETBIRD_USE_AUTH0
|
grep USE_AUTH0 docker-compose.yml | grep $CI_NETBIRD_USE_AUTH0
|
||||||
grep NETBIRD_MGMT_API_ENDPOINT docker-compose.yml | grep "http://localhost:33073"
|
grep NETBIRD_MGMT_API_ENDPOINT docker-compose.yml | grep "http://localhost:33073"
|
||||||
|
grep AUTH_REDIRECT_URI docker-compose.yml | grep $CI_NETBIRD_AUTH_REDIRECT_URI
|
||||||
|
grep AUTH_SILENT_REDIRECT_URI docker-compose.yml | egrep 'AUTH_SILENT_REDIRECT_URI=$'
|
||||||
|
|
||||||
- name: run docker compose up
|
- name: run docker compose up
|
||||||
working-directory: infrastructure_files
|
working-directory: infrastructure_files
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ builds:
|
|||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
|
||||||
- id: netbird-signal
|
- id: netbird-signal
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -16,7 +16,7 @@
|
|||||||
<a href="https://www.codacy.com/gh/netbirdio/netbird/dashboard?utm_source=github.com&utm_medium=referral&utm_content=netbirdio/netbird&utm_campaign=Badge_Grade"><img src="https://app.codacy.com/project/badge/Grade/e3013d046aec44cdb7462c8673b00976"/></a>
|
<a href="https://www.codacy.com/gh/netbirdio/netbird/dashboard?utm_source=github.com&utm_medium=referral&utm_content=netbirdio/netbird&utm_campaign=Badge_Grade"><img src="https://app.codacy.com/project/badge/Grade/e3013d046aec44cdb7462c8673b00976"/></a>
|
||||||
<br>
|
<br>
|
||||||
<a href="https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">
|
<a href="https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">
|
||||||
<img src="https://img.shields.io/badge/slack-@wiretrustee-red.svg?logo=slack"/>
|
<img src="https://img.shields.io/badge/slack-@netbird-red.svg?logo=slack"/>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,28 +43,27 @@ It requires zero configuration effort leaving behind the hassle of opening ports
|
|||||||
NetBird creates an overlay peer-to-peer network connecting machines automatically regardless of their location (home, office, datacenter, container, cloud or edge environments) unifying virtual private network management experience.
|
NetBird creates an overlay peer-to-peer network connecting machines automatically regardless of their location (home, office, datacenter, container, cloud or edge environments) unifying virtual private network management experience.
|
||||||
|
|
||||||
**Key features:**
|
**Key features:**
|
||||||
- \[x] Automatic IP allocation and network management with a Web UI ([separate repo](https://github.com/netbirdio/dashboard))
|
- \[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] Automatic WireGuard peer (machine) discovery and configuration.
|
||||||
- \[x] Encrypted peer-to-peer connections without a central VPN gateway.
|
- \[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] Connection relay fallback in case a peer-to-peer connection is not possible.
|
||||||
- \[x] Desktop client applications for Linux, MacOS, and Windows (systray).
|
- \[x] Desktop client applications for Linux, MacOS, and Windows (systray).
|
||||||
- \[x] Multiuser support - sharing network between multiple users.
|
- \[x] Multiuser support - sharing network between multiple users.
|
||||||
- \[x] SSO and MFA support.
|
- \[x] SSO and MFA support.
|
||||||
- \[x] Multicloud and hybrid-cloud support.
|
- \[x] Multicloud and hybrid-cloud support.
|
||||||
- \[x] Kernel WireGuard usage when possible.
|
- \[x] Kernel WireGuard usage when possible.
|
||||||
- \[x] Access Controls - groups & rules.
|
- \[x] Access Controls - groups & rules.
|
||||||
- \[x] Remote SSH access without managing SSH keys.
|
- \[x] Remote SSH access without managing SSH keys.
|
||||||
|
- \[x] Network Routes.
|
||||||
|
|
||||||
**Coming soon:**
|
**Coming soon:**
|
||||||
- \[ ] Network Routes.
|
|
||||||
- \[ ] Private DNS.
|
- \[ ] Private DNS.
|
||||||
- \[ ] Mobile clients.
|
- \[ ] Mobile clients.
|
||||||
- \[ ] Network Activity Monitoring.
|
- \[ ] Network Activity Monitoring.
|
||||||
|
|
||||||
### Secure peer-to-peer VPN with SSO and MFA in minutes
|
### Secure peer-to-peer VPN with SSO and MFA in minutes
|
||||||
<p float="left" align="middle">
|
<p float="left" align="middle">
|
||||||
<img src="docs/media/peerA.gif" width="400"/>
|
<img src="docs/media/netbird-sso-mfa-demo.gif" width="800"/>
|
||||||
<img src="docs/media/peerB.gif" width="400"/>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
detailFlag bool
|
detailFlag bool
|
||||||
|
ipv4Flag bool
|
||||||
ipsFilter []string
|
ipsFilter []string
|
||||||
statusFilter string
|
statusFilter string
|
||||||
ipsFilterMap map[string]struct{}
|
ipsFilterMap map[string]struct{}
|
||||||
@@ -73,7 +75,7 @@ var statusCmd = &cobra.Command{
|
|||||||
pbFullStatus := resp.GetFullStatus()
|
pbFullStatus := resp.GetFullStatus()
|
||||||
fullStatus := fromProtoFullStatus(pbFullStatus)
|
fullStatus := fromProtoFullStatus(pbFullStatus)
|
||||||
|
|
||||||
cmd.Print(parseFullStatus(fullStatus, detailFlag, daemonStatus, resp.GetDaemonVersion()))
|
cmd.Print(parseFullStatus(fullStatus, detailFlag, daemonStatus, resp.GetDaemonVersion(), ipv4Flag))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -82,8 +84,9 @@ var statusCmd = &cobra.Command{
|
|||||||
func init() {
|
func init() {
|
||||||
ipsFilterMap = make(map[string]struct{})
|
ipsFilterMap = make(map[string]struct{})
|
||||||
statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information")
|
statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information")
|
||||||
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g. --filter-by-ips 100.64.0.100,100.64.0.200")
|
statusCmd.PersistentFlags().BoolVar(&ipv4Flag, "ipv4", false, "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33")
|
||||||
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g. --filter-by-status connected")
|
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200")
|
||||||
|
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFilters() error {
|
func parseFilters() error {
|
||||||
@@ -142,7 +145,19 @@ func fromProtoFullStatus(pbFullStatus *proto.FullStatus) nbStatus.FullStatus {
|
|||||||
return fullStatus
|
return fullStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFullStatus(fullStatus nbStatus.FullStatus, printDetail bool, daemonStatus string, daemonVersion string) string {
|
func parseFullStatus(fullStatus nbStatus.FullStatus, printDetail bool, daemonStatus string, daemonVersion string, flag bool) string {
|
||||||
|
|
||||||
|
interfaceIP := fullStatus.LocalPeerState.IP
|
||||||
|
|
||||||
|
ip, _, err := net.ParseCIDR(interfaceIP)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipv4Flag {
|
||||||
|
return fmt.Sprintf("%s\n", ip)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
managementStatusURL = ""
|
managementStatusURL = ""
|
||||||
signalStatusURL = ""
|
signalStatusURL = ""
|
||||||
@@ -164,8 +179,6 @@ func parseFullStatus(fullStatus nbStatus.FullStatus, printDetail bool, daemonSta
|
|||||||
signalConnString = "Connected"
|
signalConnString = "Connected"
|
||||||
}
|
}
|
||||||
|
|
||||||
interfaceIP := fullStatus.LocalPeerState.IP
|
|
||||||
|
|
||||||
if fullStatus.LocalPeerState.KernelInterface {
|
if fullStatus.LocalPeerState.KernelInterface {
|
||||||
interfaceTypeString = "Kernel"
|
interfaceTypeString = "Kernel"
|
||||||
} else if fullStatus.LocalPeerState.IP == "" {
|
} else if fullStatus.LocalPeerState.IP == "" {
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ done:
|
|||||||
Pop $2
|
Pop $2
|
||||||
Exch $1
|
Exch $1
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
!macro GetAppFromCommand in out
|
!macro GetAppFromCommand in out
|
||||||
Push "${in}"
|
Push "${in}"
|
||||||
Call GetAppFromCommand
|
Call GetAppFromCommand
|
||||||
@@ -117,7 +118,7 @@ Call GetAppFromCommand ; Remove quotes and parameters from UninstCommand
|
|||||||
Pop $0
|
Pop $0
|
||||||
Pop $1
|
Pop $1
|
||||||
GetFullPathName $2 "$0\.."
|
GetFullPathName $2 "$0\.."
|
||||||
ExecWait '"$0" $1 _?=$2'
|
ExecWait '"$0" /S $1 _?=$2'
|
||||||
Delete "$0" ; Extra cleanup because we used _?=
|
Delete "$0" ; Extra cleanup because we used _?=
|
||||||
RMDir "$2"
|
RMDir "$2"
|
||||||
Pop $2
|
Pop $2
|
||||||
@@ -126,30 +127,27 @@ Pop $0
|
|||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
Function .onInit
|
Function .onInit
|
||||||
|
StrCpy $INSTDIR "${INSTALL_DIR}"
|
||||||
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Wiretrustee" "UninstallString"
|
|
||||||
${If} $R0 != ""
|
|
||||||
MessageBox MB_YESNO|MB_ICONQUESTION "Wiretrustee is installed. We must remove it before installing Netbird. Procced?" IDNO noWTUninstOld
|
|
||||||
!insertmacro UninstallPreviousNSIS $R0 "/NoMsgBox"
|
|
||||||
noWTUninstOld:
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
||||||
${If} $R0 != ""
|
${If} $R0 != ""
|
||||||
MessageBox MB_YESNO|MB_ICONQUESTION "$(^NAME) is already installed. Do you want to remove the previous version?" IDNO noUninstOld
|
# if silent install jump to uninstall step
|
||||||
!insertmacro UninstallPreviousNSIS $R0 "/NoMsgBox"
|
IfSilent uninstall
|
||||||
noUninstOld:
|
|
||||||
|
MessageBox MB_YESNO|MB_ICONQUESTION "NetBird is already installed. We must remove it before installing upgrading NetBird. Proceed?" IDNO done IDYES uninstall
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
!insertmacro UninstallPreviousNSIS $R0 "/NoMsgBox"
|
||||||
|
done:
|
||||||
|
|
||||||
${EndIf}
|
${EndIf}
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
######################################################################
|
######################################################################
|
||||||
Section -MainProgram
|
Section -MainProgram
|
||||||
${INSTALL_TYPE}
|
${INSTALL_TYPE}
|
||||||
SetOverwrite ifnewer
|
# SetOverwrite ifnewer
|
||||||
SetOutPath "$INSTDIR"
|
SetOutPath "$INSTDIR"
|
||||||
File /r "..\\dist\\netbird_windows_amd64\\"
|
File /r "..\\dist\\netbird_windows_amd64\\"
|
||||||
|
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
Section -Icons_Reg
|
Section -Icons_Reg
|
||||||
@@ -172,24 +170,29 @@ SetShellVarContext current
|
|||||||
CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
||||||
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section -Post
|
||||||
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service install'
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service install'
|
||||||
Exec '"$INSTDIR\${MAIN_APP_EXE}" service start'
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service start'
|
||||||
# sleep a bit for visibility
|
# sleep a bit for visibility
|
||||||
Sleep 1000
|
Sleep 1000
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
Section Uninstall
|
Section Uninstall
|
||||||
${INSTALL_TYPE}
|
${INSTALL_TYPE}
|
||||||
|
|
||||||
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service stop'
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service stop'
|
||||||
Exec '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
|
||||||
|
|
||||||
# kill ui client
|
# kill ui client
|
||||||
ExecWait `taskkill /im ${UI_APP_EXE}.exe`
|
ExecWait `taskkill /im ${UI_APP_EXE}.exe`
|
||||||
|
|
||||||
# wait the service uninstall take unblock the executable
|
# wait the service uninstall take unblock the executable
|
||||||
Sleep 3000
|
Sleep 3000
|
||||||
|
Delete "$INSTDIR\${UI_APP_EXE}"
|
||||||
|
Delete "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
RmDir /r "$INSTDIR"
|
RmDir /r "$INSTDIR"
|
||||||
|
|
||||||
SetShellVarContext current
|
SetShellVarContext current
|
||||||
@@ -209,4 +212,4 @@ SetShellVarContext current
|
|||||||
SetOutPath $INSTDIR
|
SetOutPath $INSTDIR
|
||||||
ShellExecAsUser::ShellExecAsUser "" "$DESKTOP\${APP_NAME}.lnk"
|
ShellExecAsUser::ShellExecAsUser "" "$DESKTOP\${APP_NAME}.lnk"
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ func GetDeviceAuthorizationFlowInfo(ctx context.Context, config *Config) (Device
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return DeviceAuthorizationFlow{
|
deviceAuthorizationFlow := DeviceAuthorizationFlow{
|
||||||
Provider: protoDeviceAuthorizationFlow.Provider.String(),
|
Provider: protoDeviceAuthorizationFlow.Provider.String(),
|
||||||
|
|
||||||
ProviderConfig: ProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
@@ -274,5 +274,29 @@ func GetDeviceAuthorizationFlowInfo(ctx context.Context, config *Config) (Device
|
|||||||
TokenEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetTokenEndpoint(),
|
TokenEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetTokenEndpoint(),
|
||||||
DeviceAuthEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetDeviceAuthEndpoint(),
|
DeviceAuthEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetDeviceAuthEndpoint(),
|
||||||
},
|
},
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
err = isProviderConfigValid(deviceAuthorizationFlow.ProviderConfig)
|
||||||
|
if err != nil {
|
||||||
|
return DeviceAuthorizationFlow{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceAuthorizationFlow, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isProviderConfigValid(config ProviderConfig) error {
|
||||||
|
errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator"
|
||||||
|
if config.Audience == "" {
|
||||||
|
return fmt.Errorf(errorMSGFormat, "Audience")
|
||||||
|
}
|
||||||
|
if config.ClientID == "" {
|
||||||
|
return fmt.Errorf(errorMSGFormat, "Client ID")
|
||||||
|
}
|
||||||
|
if config.TokenEndpoint == "" {
|
||||||
|
return fmt.Errorf(errorMSGFormat, "Token Endpoint")
|
||||||
|
}
|
||||||
|
if config.DeviceAuthEndpoint == "" {
|
||||||
|
return fmt.Errorf(errorMSGFormat, "Device Auth Endpoint")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func RunClient(ctx context.Context, config *Config, statusRecorder *nbStatus.Sta
|
|||||||
localPeerState := nbStatus.LocalPeerState{
|
localPeerState := nbStatus.LocalPeerState{
|
||||||
IP: loginResp.GetPeerConfig().GetAddress(),
|
IP: loginResp.GetPeerConfig().GetAddress(),
|
||||||
PubKey: myPrivateKey.PublicKey().String(),
|
PubKey: myPrivateKey.PublicKey().String(),
|
||||||
KernelInterface: iface.WireguardModExists(),
|
KernelInterface: iface.WireguardModuleIsLoaded(),
|
||||||
}
|
}
|
||||||
|
|
||||||
statusRecorder.UpdateLocalPeerState(localPeerState)
|
statusRecorder.UpdateLocalPeerState(localPeerState)
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ func Login(ctx context.Context, config *Config, setupKey string, jwtToken string
|
|||||||
defer func() {
|
defer func() {
|
||||||
err = mgmClient.Close()
|
err = mgmClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to close the Management service client %v", err)
|
cStatus, ok := status.FromError(err)
|
||||||
|
if !ok || ok && cStatus.Code() != codes.Canceled {
|
||||||
|
log.Warnf("failed to close the Management service client, err: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
|
|||||||
err = addToRouteTableIfNoExists(c.network, c.wgInterface.GetAddress().IP.String())
|
err = addToRouteTableIfNoExists(c.network, c.wgInterface.GetAddress().IP.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("route %s couldn't be added for peer %s, err: %v",
|
return fmt.Errorf("route %s couldn't be added for peer %s, err: %v",
|
||||||
c.chosenRoute.Network.String(), c.wgInterface.GetAddress().IP.String(), err)
|
c.network.String(), c.wgInterface.GetAddress().IP.String(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,16 @@ import (
|
|||||||
import "github.com/google/nftables"
|
import "github.com/google/nftables"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ipv6Forwarding = "netbird-rt-ipv6-forwarding"
|
ipv6Forwarding = "netbird-rt-ipv6-forwarding"
|
||||||
ipv4Forwarding = "netbird-rt-ipv4-forwarding"
|
ipv4Forwarding = "netbird-rt-ipv4-forwarding"
|
||||||
ipv6Nat = "netbird-rt-ipv6-nat"
|
ipv6Nat = "netbird-rt-ipv6-nat"
|
||||||
ipv4Nat = "netbird-rt-ipv4-nat"
|
ipv4Nat = "netbird-rt-ipv4-nat"
|
||||||
natFormat = "netbird-nat-%s"
|
natFormat = "netbird-nat-%s"
|
||||||
forwardingFormat = "netbird-fwd-%s"
|
forwardingFormat = "netbird-fwd-%s"
|
||||||
ipv6 = "ipv6"
|
inNatFormat = "netbird-nat-in-%s"
|
||||||
ipv4 = "ipv4"
|
inForwardingFormat = "netbird-fwd-in-%s"
|
||||||
|
ipv6 = "ipv6"
|
||||||
|
ipv4 = "ipv4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func genKey(format string, input string) string {
|
func genKey(format string, input string) string {
|
||||||
@@ -53,3 +55,13 @@ func NewFirewall(parentCTX context.Context) firewallManager {
|
|||||||
|
|
||||||
return manager
|
return manager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getInPair(pair routerPair) routerPair {
|
||||||
|
return routerPair{
|
||||||
|
ID: pair.ID,
|
||||||
|
// invert source/destination
|
||||||
|
source: pair.destination,
|
||||||
|
destination: pair.source,
|
||||||
|
masquerade: pair.masquerade,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -311,7 +311,37 @@ func (i *iptablesManager) InsertRoutingRules(pair routerPair) error {
|
|||||||
i.mux.Lock()
|
i.mux.Lock()
|
||||||
defer i.mux.Unlock()
|
defer i.mux.Unlock()
|
||||||
|
|
||||||
|
err := i.insertRoutingRule(forwardingFormat, iptablesFilterTable, iptablesRoutingForwardingChain, routingFinalForwardJump, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.insertRoutingRule(inForwardingFormat, iptablesFilterTable, iptablesRoutingForwardingChain, routingFinalForwardJump, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pair.masquerade {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.insertRoutingRule(natFormat, iptablesNatTable, iptablesRoutingNatChain, routingFinalNatJump, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.insertRoutingRule(inNatFormat, iptablesNatTable, iptablesRoutingNatChain, routingFinalNatJump, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertRoutingRule inserts an iptable rule
|
||||||
|
func (i *iptablesManager) insertRoutingRule(keyFormat, table, chain, jump string, pair routerPair) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
prefix := netip.MustParsePrefix(pair.source)
|
prefix := netip.MustParsePrefix(pair.source)
|
||||||
ipVersion := ipv4
|
ipVersion := ipv4
|
||||||
iptablesClient := i.ipv4Client
|
iptablesClient := i.ipv4Client
|
||||||
@@ -320,43 +350,22 @@ func (i *iptablesManager) InsertRoutingRules(pair routerPair) error {
|
|||||||
ipVersion = ipv6
|
ipVersion = ipv6
|
||||||
}
|
}
|
||||||
|
|
||||||
forwardRuleKey := genKey(forwardingFormat, pair.ID)
|
ruleKey := genKey(keyFormat, pair.ID)
|
||||||
forwardRule := genRuleSpec(routingFinalForwardJump, forwardRuleKey, pair.source, pair.destination)
|
rule := genRuleSpec(jump, ruleKey, pair.source, pair.destination)
|
||||||
existingRule, found := i.rules[ipVersion][forwardRuleKey]
|
existingRule, found := i.rules[ipVersion][ruleKey]
|
||||||
if found {
|
if found {
|
||||||
err = iptablesClient.DeleteIfExists(iptablesFilterTable, iptablesRoutingForwardingChain, existingRule...)
|
err = iptablesClient.DeleteIfExists(table, chain, existingRule...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("iptables: error while removing existing forwarding rule for %s: %v", pair.destination, err)
|
return fmt.Errorf("iptables: error while removing existing %s rule for %s: %v", getIptablesRuleType(table), pair.destination, err)
|
||||||
}
|
}
|
||||||
delete(i.rules[ipVersion], forwardRuleKey)
|
delete(i.rules[ipVersion], ruleKey)
|
||||||
}
|
}
|
||||||
err = iptablesClient.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forwardRule...)
|
err = iptablesClient.Insert(table, chain, 1, rule...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("iptables: error while adding new forwarding rule for %s: %v", pair.destination, err)
|
return fmt.Errorf("iptables: error while adding new %s rule for %s: %v", getIptablesRuleType(table), pair.destination, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
i.rules[ipVersion][forwardRuleKey] = forwardRule
|
i.rules[ipVersion][ruleKey] = rule
|
||||||
|
|
||||||
if !pair.masquerade {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
natRuleKey := genKey(natFormat, pair.ID)
|
|
||||||
natRule := genRuleSpec(routingFinalNatJump, natRuleKey, pair.source, pair.destination)
|
|
||||||
existingRule, found = i.rules[ipVersion][natRuleKey]
|
|
||||||
if found {
|
|
||||||
err = iptablesClient.DeleteIfExists(iptablesNatTable, iptablesRoutingNatChain, existingRule...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("iptables: error while removing existing nat rulefor %s: %v", pair.destination, err)
|
|
||||||
}
|
|
||||||
delete(i.rules[ipVersion], natRuleKey)
|
|
||||||
}
|
|
||||||
err = iptablesClient.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, natRule...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("iptables: error while adding new nat rulefor %s: %v", pair.destination, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
i.rules[ipVersion][natRuleKey] = natRule
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -366,7 +375,37 @@ func (i *iptablesManager) RemoveRoutingRules(pair routerPair) error {
|
|||||||
i.mux.Lock()
|
i.mux.Lock()
|
||||||
defer i.mux.Unlock()
|
defer i.mux.Unlock()
|
||||||
|
|
||||||
|
err := i.removeRoutingRule(forwardingFormat, iptablesFilterTable, iptablesRoutingForwardingChain, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.removeRoutingRule(inForwardingFormat, iptablesFilterTable, iptablesRoutingForwardingChain, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pair.masquerade {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.removeRoutingRule(natFormat, iptablesNatTable, iptablesRoutingNatChain, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.removeRoutingRule(inNatFormat, iptablesNatTable, iptablesRoutingNatChain, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeRoutingRule removes an iptables rule
|
||||||
|
func (i *iptablesManager) removeRoutingRule(keyFormat, table, chain string, pair routerPair) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
prefix := netip.MustParsePrefix(pair.source)
|
prefix := netip.MustParsePrefix(pair.source)
|
||||||
ipVersion := ipv4
|
ipVersion := ipv4
|
||||||
iptablesClient := i.ipv4Client
|
iptablesClient := i.ipv4Client
|
||||||
@@ -375,29 +414,23 @@ func (i *iptablesManager) RemoveRoutingRules(pair routerPair) error {
|
|||||||
ipVersion = ipv6
|
ipVersion = ipv6
|
||||||
}
|
}
|
||||||
|
|
||||||
forwardRuleKey := genKey(forwardingFormat, pair.ID)
|
ruleKey := genKey(keyFormat, pair.ID)
|
||||||
existingRule, found := i.rules[ipVersion][forwardRuleKey]
|
existingRule, found := i.rules[ipVersion][ruleKey]
|
||||||
if found {
|
if found {
|
||||||
err = iptablesClient.DeleteIfExists(iptablesFilterTable, iptablesRoutingForwardingChain, existingRule...)
|
err = iptablesClient.DeleteIfExists(table, chain, existingRule...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("iptables: error while removing existing forwarding rule for %s: %v", pair.destination, err)
|
return fmt.Errorf("iptables: error while removing existing %s rule for %s: %v", getIptablesRuleType(table), pair.destination, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete(i.rules[ipVersion], forwardRuleKey)
|
delete(i.rules[ipVersion], ruleKey)
|
||||||
|
|
||||||
if !pair.masquerade {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
natRuleKey := genKey(natFormat, pair.ID)
|
|
||||||
existingRule, found = i.rules[ipVersion][natRuleKey]
|
|
||||||
if found {
|
|
||||||
err = iptablesClient.DeleteIfExists(iptablesNatTable, iptablesRoutingNatChain, existingRule...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("iptables: error while removing existing nat rule for %s: %v", pair.destination, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete(i.rules[ipVersion], natRuleKey)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getIptablesRuleType(table string) string {
|
||||||
|
ruleType := "forwarding"
|
||||||
|
if table == iptablesNatTable {
|
||||||
|
ruleType = "nat"
|
||||||
|
}
|
||||||
|
return ruleType
|
||||||
|
}
|
||||||
|
|||||||
@@ -159,6 +159,17 @@ func TestIptablesManager_InsertRoutingRules(t *testing.T) {
|
|||||||
require.True(t, found, "forwarding rule should exist in the manager map")
|
require.True(t, found, "forwarding rule should exist in the manager map")
|
||||||
require.Equal(t, forwardRule[:4], foundRule[:4], "stored forwarding rule should match")
|
require.Equal(t, forwardRule[:4], foundRule[:4], "stored forwarding rule should match")
|
||||||
|
|
||||||
|
inForwardRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID)
|
||||||
|
inForwardRule := genRuleSpec(routingFinalForwardJump, inForwardRuleKey, getInPair(testCase.inputPair).source, getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesFilterTable, iptablesRoutingForwardingChain, inForwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
require.True(t, exists, "income forwarding rule should exist")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[testCase.ipVersion][inForwardRuleKey]
|
||||||
|
require.True(t, found, "income forwarding rule should exist in the manager map")
|
||||||
|
require.Equal(t, inForwardRule[:4], foundRule[:4], "stored income forwarding rule should match")
|
||||||
|
|
||||||
natRuleKey := genKey(natFormat, testCase.inputPair.ID)
|
natRuleKey := genKey(natFormat, testCase.inputPair.ID)
|
||||||
natRule := genRuleSpec(routingFinalNatJump, natRuleKey, testCase.inputPair.source, testCase.inputPair.destination)
|
natRule := genRuleSpec(routingFinalNatJump, natRuleKey, testCase.inputPair.source, testCase.inputPair.destination)
|
||||||
|
|
||||||
@@ -172,7 +183,23 @@ func TestIptablesManager_InsertRoutingRules(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
require.False(t, exists, "nat rule should not be created")
|
require.False(t, exists, "nat rule should not be created")
|
||||||
_, foundNat := manager.rules[testCase.ipVersion][natRuleKey]
|
_, foundNat := manager.rules[testCase.ipVersion][natRuleKey]
|
||||||
require.False(t, foundNat, "nat rule should exist in the map")
|
require.False(t, foundNat, "nat rule should not exist in the map")
|
||||||
|
}
|
||||||
|
|
||||||
|
inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID)
|
||||||
|
inNatRule := genRuleSpec(routingFinalNatJump, inNatRuleKey, getInPair(testCase.inputPair).source, getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesNatTable, iptablesRoutingNatChain, inNatRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if testCase.inputPair.masquerade {
|
||||||
|
require.True(t, exists, "income nat rule should be created")
|
||||||
|
foundNatRule, foundNat := manager.rules[testCase.ipVersion][inNatRuleKey]
|
||||||
|
require.True(t, foundNat, "income nat rule should exist in the map")
|
||||||
|
require.Equal(t, inNatRule[:4], foundNatRule[:4], "stored income nat rule should match")
|
||||||
|
} else {
|
||||||
|
require.False(t, exists, "nat rule should not be created")
|
||||||
|
_, foundNat := manager.rules[testCase.ipVersion][inNatRuleKey]
|
||||||
|
require.False(t, foundNat, "income nat rule should not exist in the map")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -213,12 +240,24 @@ func TestIptablesManager_RemoveRoutingRules(t *testing.T) {
|
|||||||
err = iptablesClient.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forwardRule...)
|
err = iptablesClient.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forwardRule...)
|
||||||
require.NoError(t, err, "inserting rule should not return error")
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
inForwardRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID)
|
||||||
|
inForwardRule := genRuleSpec(routingFinalForwardJump, inForwardRuleKey, getInPair(testCase.inputPair).source, getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, inForwardRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
natRuleKey := genKey(natFormat, testCase.inputPair.ID)
|
natRuleKey := genKey(natFormat, testCase.inputPair.ID)
|
||||||
natRule := genRuleSpec(routingFinalNatJump, natRuleKey, testCase.inputPair.source, testCase.inputPair.destination)
|
natRule := genRuleSpec(routingFinalNatJump, natRuleKey, testCase.inputPair.source, testCase.inputPair.destination)
|
||||||
|
|
||||||
err = iptablesClient.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, natRule...)
|
err = iptablesClient.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, natRule...)
|
||||||
require.NoError(t, err, "inserting rule should not return error")
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID)
|
||||||
|
inNatRule := genRuleSpec(routingFinalNatJump, inNatRuleKey, getInPair(testCase.inputPair).source, getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, inNatRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
delete(manager.rules, ipv4)
|
delete(manager.rules, ipv4)
|
||||||
delete(manager.rules, ipv6)
|
delete(manager.rules, ipv6)
|
||||||
|
|
||||||
@@ -235,12 +274,26 @@ func TestIptablesManager_RemoveRoutingRules(t *testing.T) {
|
|||||||
_, found := manager.rules[testCase.ipVersion][forwardRuleKey]
|
_, found := manager.rules[testCase.ipVersion][forwardRuleKey]
|
||||||
require.False(t, found, "forwarding rule should exist in the manager map")
|
require.False(t, found, "forwarding rule should exist in the manager map")
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesFilterTable, iptablesRoutingForwardingChain, inForwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
require.False(t, exists, "income forwarding rule should not exist")
|
||||||
|
|
||||||
|
_, found = manager.rules[testCase.ipVersion][inForwardRuleKey]
|
||||||
|
require.False(t, found, "income forwarding rule should exist in the manager map")
|
||||||
|
|
||||||
exists, err = iptablesClient.Exists(iptablesNatTable, iptablesRoutingNatChain, natRule...)
|
exists, err = iptablesClient.Exists(iptablesNatTable, iptablesRoutingNatChain, natRule...)
|
||||||
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesNatTable, iptablesRoutingNatChain)
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
require.False(t, exists, "nat rule should not exist")
|
require.False(t, exists, "nat rule should not exist")
|
||||||
|
|
||||||
_, found = manager.rules[testCase.ipVersion][natRuleKey]
|
_, found = manager.rules[testCase.ipVersion][natRuleKey]
|
||||||
require.False(t, found, "forwarding rule should exist in the manager map")
|
require.False(t, found, "nat rule should exist in the manager map")
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesNatTable, iptablesRoutingNatChain, inNatRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
require.False(t, exists, "income nat rule should not exist")
|
||||||
|
|
||||||
|
_, found = manager.rules[testCase.ipVersion][inNatRuleKey]
|
||||||
|
require.False(t, found, "income nat rule should exist in the manager map")
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
)
|
)
|
||||||
import "github.com/google/nftables"
|
import "github.com/google/nftables"
|
||||||
|
|
||||||
//
|
|
||||||
const (
|
const (
|
||||||
nftablesTable = "netbird-rt"
|
nftablesTable = "netbird-rt"
|
||||||
nftablesRoutingForwardingChain = "netbird-rt-fwd"
|
nftablesRoutingForwardingChain = "netbird-rt-fwd"
|
||||||
@@ -84,8 +83,10 @@ func (n *nftablesManager) CleanRoutingRules() {
|
|||||||
n.mux.Lock()
|
n.mux.Lock()
|
||||||
defer n.mux.Unlock()
|
defer n.mux.Unlock()
|
||||||
log.Debug("flushing tables")
|
log.Debug("flushing tables")
|
||||||
n.conn.FlushTable(n.tableIPv6)
|
if n.tableIPv4 != nil && n.tableIPv6 != nil {
|
||||||
n.conn.FlushTable(n.tableIPv4)
|
n.conn.FlushTable(n.tableIPv6)
|
||||||
|
n.conn.FlushTable(n.tableIPv4)
|
||||||
|
}
|
||||||
log.Debugf("flushing tables result in: %v error", n.conn.Flush())
|
log.Debugf("flushing tables result in: %v error", n.conn.Flush())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,53 +247,77 @@ func (n *nftablesManager) InsertRoutingRules(pair routerPair) error {
|
|||||||
n.mux.Lock()
|
n.mux.Lock()
|
||||||
defer n.mux.Unlock()
|
defer n.mux.Unlock()
|
||||||
|
|
||||||
|
err := n.refreshRulesMap()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.insertRoutingRule(forwardingFormat, nftablesRoutingForwardingChain, pair, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = n.insertRoutingRule(inForwardingFormat, nftablesRoutingForwardingChain, getInPair(pair), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pair.masquerade {
|
||||||
|
err = n.insertRoutingRule(natFormat, nftablesRoutingNatChain, pair, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = n.insertRoutingRule(inNatFormat, nftablesRoutingNatChain, getInPair(pair), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.conn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to insert rules for %s: %v", pair.destination, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertRoutingRule inserts a nftable rule to the conn client flush queue
|
||||||
|
func (n *nftablesManager) insertRoutingRule(format, chain string, pair routerPair, isNat bool) error {
|
||||||
|
|
||||||
prefix := netip.MustParsePrefix(pair.source)
|
prefix := netip.MustParsePrefix(pair.source)
|
||||||
|
|
||||||
sourceExp := generateCIDRMatcherExpressions("source", pair.source)
|
sourceExp := generateCIDRMatcherExpressions("source", pair.source)
|
||||||
destExp := generateCIDRMatcherExpressions("destination", pair.destination)
|
destExp := generateCIDRMatcherExpressions("destination", pair.destination)
|
||||||
|
|
||||||
forwardExp := append(sourceExp, append(destExp, exprCounterAccept...)...)
|
var expression []expr.Any
|
||||||
fwdKey := genKey(forwardingFormat, pair.ID)
|
if isNat {
|
||||||
if prefix.Addr().Unmap().Is4() {
|
expression = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
||||||
n.rules[fwdKey] = n.conn.InsertRule(&nftables.Rule{
|
|
||||||
Table: n.tableIPv4,
|
|
||||||
Chain: n.chains[ipv4][nftablesRoutingForwardingChain],
|
|
||||||
Exprs: forwardExp,
|
|
||||||
UserData: []byte(fwdKey),
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
n.rules[fwdKey] = n.conn.InsertRule(&nftables.Rule{
|
expression = append(sourceExp, append(destExp, exprCounterAccept...)...)
|
||||||
Table: n.tableIPv6,
|
|
||||||
Chain: n.chains[ipv6][nftablesRoutingForwardingChain],
|
|
||||||
Exprs: forwardExp,
|
|
||||||
UserData: []byte(fwdKey),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if pair.masquerade {
|
ruleKey := genKey(format, pair.ID)
|
||||||
natExp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
|
||||||
natKey := genKey(natFormat, pair.ID)
|
|
||||||
|
|
||||||
if prefix.Addr().Unmap().Is4() {
|
_, exists := n.rules[ruleKey]
|
||||||
n.rules[natKey] = n.conn.InsertRule(&nftables.Rule{
|
if exists {
|
||||||
Table: n.tableIPv4,
|
err := n.removeRoutingRule(format, pair)
|
||||||
Chain: n.chains[ipv4][nftablesRoutingNatChain],
|
if err != nil {
|
||||||
Exprs: natExp,
|
return err
|
||||||
UserData: []byte(natKey),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
n.rules[natKey] = n.conn.InsertRule(&nftables.Rule{
|
|
||||||
Table: n.tableIPv6,
|
|
||||||
Chain: n.chains[ipv6][nftablesRoutingNatChain],
|
|
||||||
Exprs: natExp,
|
|
||||||
UserData: []byte(natKey),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := n.conn.Flush()
|
if prefix.Addr().Unmap().Is4() {
|
||||||
if err != nil {
|
n.rules[ruleKey] = n.conn.InsertRule(&nftables.Rule{
|
||||||
return fmt.Errorf("nftables: unable to insert rules for %s: %v", pair.destination, err)
|
Table: n.tableIPv4,
|
||||||
|
Chain: n.chains[ipv4][chain],
|
||||||
|
Exprs: expression,
|
||||||
|
UserData: []byte(ruleKey),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
n.rules[ruleKey] = n.conn.InsertRule(&nftables.Rule{
|
||||||
|
Table: n.tableIPv6,
|
||||||
|
Chain: n.chains[ipv6][chain],
|
||||||
|
Exprs: expression,
|
||||||
|
UserData: []byte(ruleKey),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -307,26 +332,26 @@ func (n *nftablesManager) RemoveRoutingRules(pair routerPair) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fwdKey := genKey(forwardingFormat, pair.ID)
|
err = n.removeRoutingRule(forwardingFormat, pair)
|
||||||
natKey := genKey(natFormat, pair.ID)
|
if err != nil {
|
||||||
fwdRule, found := n.rules[fwdKey]
|
return err
|
||||||
if found {
|
|
||||||
err = n.conn.DelRule(fwdRule)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("nftables: unable to remove forwarding rule for %s: %v", pair.destination, err)
|
|
||||||
}
|
|
||||||
log.Debugf("nftables: removing forwarding rule for %s", pair.destination)
|
|
||||||
delete(n.rules, fwdKey)
|
|
||||||
}
|
}
|
||||||
natRule, found := n.rules[natKey]
|
|
||||||
if found {
|
err = n.removeRoutingRule(inForwardingFormat, getInPair(pair))
|
||||||
err = n.conn.DelRule(natRule)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return fmt.Errorf("nftables: unable to remove nat rule for %s: %v", pair.destination, err)
|
|
||||||
}
|
|
||||||
log.Debugf("nftables: removing nat rule for %s", pair.destination)
|
|
||||||
delete(n.rules, natKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = n.removeRoutingRule(natFormat, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.removeRoutingRule(inNatFormat, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = n.conn.Flush()
|
err = n.conn.Flush()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("nftables: received error while applying rule removal for %s: %v", pair.destination, err)
|
return fmt.Errorf("nftables: received error while applying rule removal for %s: %v", pair.destination, err)
|
||||||
@@ -335,6 +360,29 @@ func (n *nftablesManager) RemoveRoutingRules(pair routerPair) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// removeRoutingRule add a nftable rule to the removal queue and delete from rules map
|
||||||
|
func (n *nftablesManager) removeRoutingRule(format string, pair routerPair) error {
|
||||||
|
ruleKey := genKey(format, pair.ID)
|
||||||
|
|
||||||
|
rule, found := n.rules[ruleKey]
|
||||||
|
if found {
|
||||||
|
ruleType := "forwarding"
|
||||||
|
if rule.Chain.Type == nftables.ChainTypeNAT {
|
||||||
|
ruleType = "nat"
|
||||||
|
}
|
||||||
|
|
||||||
|
err := n.conn.DelRule(rule)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to remove %s rule for %s: %v", ruleType, pair.destination, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("nftables: removing %s rule for %s", ruleType, pair.destination)
|
||||||
|
|
||||||
|
delete(n.rules, ruleKey)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// getPayloadDirectives get expression directives based on ip version and direction
|
// getPayloadDirectives get expression directives based on ip version and direction
|
||||||
func getPayloadDirectives(direction string, isIPv4 bool, isIPv6 bool) (uint32, uint32, []byte) {
|
func getPayloadDirectives(direction string, isIPv4 bool, isIPv6 bool) (uint32, uint32, []byte) {
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -189,6 +189,45 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceExp = generateCIDRMatcherExpressions("source", getInPair(testCase.inputPair).source)
|
||||||
|
destExp = generateCIDRMatcherExpressions("destination", getInPair(testCase.inputPair).destination)
|
||||||
|
testingExpression = append(sourceExp, destExp...)
|
||||||
|
inFwdRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID)
|
||||||
|
|
||||||
|
found = 0
|
||||||
|
for _, registeredChains := range manager.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 && string(rule.UserData) == inFwdRuleKey {
|
||||||
|
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "income forwarding rule elements should match")
|
||||||
|
found = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
||||||
|
|
||||||
|
if testCase.inputPair.masquerade {
|
||||||
|
inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID)
|
||||||
|
found := 0
|
||||||
|
for _, registeredChains := range manager.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 && string(rule.UserData) == inNatRuleKey {
|
||||||
|
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "income nat rule elements should match")
|
||||||
|
found = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,6 +280,28 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
|
|||||||
UserData: []byte(natRuleKey),
|
UserData: []byte(natRuleKey),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
sourceExp = generateCIDRMatcherExpressions("source", getInPair(testCase.inputPair).source)
|
||||||
|
destExp = generateCIDRMatcherExpressions("destination", getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
forwardExp = append(sourceExp, append(destExp, exprCounterAccept...)...)
|
||||||
|
inForwardRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID)
|
||||||
|
insertedInForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: table,
|
||||||
|
Chain: manager.chains[testCase.ipVersion][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: forwardExp,
|
||||||
|
UserData: []byte(inForwardRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
natExp = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
||||||
|
inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID)
|
||||||
|
|
||||||
|
insertedInNat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: table,
|
||||||
|
Chain: manager.chains[testCase.ipVersion][nftablesRoutingNatChain],
|
||||||
|
Exprs: natExp,
|
||||||
|
UserData: []byte(inNatRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
err = nftablesTestingClient.Flush()
|
err = nftablesTestingClient.Flush()
|
||||||
require.NoError(t, err, "shouldn't return error")
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
@@ -259,8 +320,10 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
|
|||||||
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if len(rule.UserData) > 0 {
|
if len(rule.UserData) > 0 {
|
||||||
require.NotEqual(t, insertedForwarding.UserData, rule.UserData, "forwarding rule should exist")
|
require.NotEqual(t, insertedForwarding.UserData, rule.UserData, "forwarding rule should not exist")
|
||||||
require.NotEqual(t, insertedNat.UserData, rule.UserData, "nat rule should exist")
|
require.NotEqual(t, insertedNat.UserData, rule.UserData, "nat rule should not exist")
|
||||||
|
require.NotEqual(t, insertedInForwarding.UserData, rule.UserData, "income forwarding rule should not exist")
|
||||||
|
require.NotEqual(t, insertedInNat.UserData, rule.UserData, "income nat rule should not exist")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,17 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetInfo retrieves and parses the system information
|
// GetInfo retrieves and parses the system information
|
||||||
func GetInfo(ctx context.Context) *Info {
|
func GetInfo(ctx context.Context) *Info {
|
||||||
cmd := exec.Command("cmd", "ver")
|
ver := getOSVersion()
|
||||||
cmd.Stdin = strings.NewReader("some")
|
|
||||||
var out bytes.Buffer
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
osStr := strings.Replace(out.String(), "\n", "", -1)
|
|
||||||
osStr = strings.Replace(osStr, "\r\n", "", -1)
|
|
||||||
tmp1 := strings.Index(osStr, "[Version")
|
|
||||||
tmp2 := strings.Index(osStr, "]")
|
|
||||||
var ver string
|
|
||||||
if tmp1 == -1 || tmp2 == -1 {
|
|
||||||
ver = "unknown"
|
|
||||||
} else {
|
|
||||||
ver = osStr[tmp1+9 : tmp2]
|
|
||||||
}
|
|
||||||
gio := &Info{Kernel: "windows", OSVersion: ver, Core: ver, Platform: "unknown", OS: "windows", GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
|
gio := &Info{Kernel: "windows", OSVersion: ver, Core: ver, Platform: "unknown", OS: "windows", GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
|
||||||
gio.Hostname, _ = os.Hostname()
|
gio.Hostname, _ = os.Hostname()
|
||||||
gio.WiretrusteeVersion = NetbirdVersion()
|
gio.WiretrusteeVersion = NetbirdVersion()
|
||||||
@@ -38,3 +19,37 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
|
|
||||||
return gio
|
return gio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getOSVersion() string {
|
||||||
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return "0.0.0.0"
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
deferErr := k.Close()
|
||||||
|
if deferErr != nil {
|
||||||
|
log.Error(deferErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
major, _, err := k.GetIntegerValue("CurrentMajorVersionNumber")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
minor, _, err := k.GetIntegerValue("CurrentMinorVersionNumber")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
build, _, err := k.GetStringValue("CurrentBuildNumber")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
// Update Build Revision
|
||||||
|
ubr, _, err := k.GetIntegerValue("UBR")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
ver := fmt.Sprintf("%d.%d.%s.%d", major, minor, build, ubr)
|
||||||
|
return ver
|
||||||
|
}
|
||||||
|
|||||||
@@ -324,12 +324,12 @@ func (s *serviceClient) updateStatus() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.Status == string(internal.StatusConnected) {
|
if status.Status == string(internal.StatusConnected) && !s.mUp.Disabled() {
|
||||||
systray.SetIcon(s.icConnected)
|
systray.SetIcon(s.icConnected)
|
||||||
s.mStatus.SetTitle("Connected")
|
s.mStatus.SetTitle("Connected")
|
||||||
s.mUp.Disable()
|
s.mUp.Disable()
|
||||||
s.mDown.Enable()
|
s.mDown.Enable()
|
||||||
} else {
|
} else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() {
|
||||||
systray.SetIcon(s.icDisconnected)
|
systray.SetIcon(s.icDisconnected)
|
||||||
s.mStatus.SetTitle("Disconnected")
|
s.mStatus.SetTitle("Disconnected")
|
||||||
s.mDown.Disable()
|
s.mDown.Disable()
|
||||||
|
|||||||
6
dns/dns.go
Normal file
6
dns/dns.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Package dns implement dns types and standard methods and functions
|
||||||
|
// to parse and normalize dns records and configuration
|
||||||
|
package dns
|
||||||
|
|
||||||
|
// DefaultDNSPort well-known port number
|
||||||
|
const DefaultDNSPort = 53
|
||||||
184
dns/nameserver.go
Normal file
184
dns/nameserver.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MaxGroupNameChar maximum group name size
|
||||||
|
MaxGroupNameChar = 40
|
||||||
|
// InvalidNameServerType invalid nameserver type
|
||||||
|
InvalidNameServerType NameServerType = iota
|
||||||
|
// UDPNameServerType udp nameserver type
|
||||||
|
UDPNameServerType
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// InvalidNameServerTypeString invalid nameserver type as string
|
||||||
|
InvalidNameServerTypeString = "invalid"
|
||||||
|
// UDPNameServerTypeString udp nameserver type as string
|
||||||
|
UDPNameServerTypeString = "udp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NameServerType nameserver type
|
||||||
|
type NameServerType int
|
||||||
|
|
||||||
|
// String returns nameserver type string
|
||||||
|
func (n NameServerType) String() string {
|
||||||
|
switch n {
|
||||||
|
case UDPNameServerType:
|
||||||
|
return UDPNameServerTypeString
|
||||||
|
default:
|
||||||
|
return InvalidNameServerTypeString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToNameServerType returns a nameserver type
|
||||||
|
func ToNameServerType(typeString string) NameServerType {
|
||||||
|
switch typeString {
|
||||||
|
case UDPNameServerTypeString:
|
||||||
|
return UDPNameServerType
|
||||||
|
default:
|
||||||
|
return InvalidNameServerType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameServerGroup group of nameservers and with group ids
|
||||||
|
type NameServerGroup struct {
|
||||||
|
// ID identifier of group
|
||||||
|
ID string
|
||||||
|
// Name group name
|
||||||
|
Name string
|
||||||
|
// Description group description
|
||||||
|
Description string
|
||||||
|
// NameServers list of nameservers
|
||||||
|
NameServers []NameServer
|
||||||
|
// Groups list of peer group IDs to distribute the nameservers information
|
||||||
|
Groups []string
|
||||||
|
// Enabled group status
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameServer represents a DNS nameserver
|
||||||
|
type NameServer struct {
|
||||||
|
// IP address of nameserver
|
||||||
|
IP netip.Addr
|
||||||
|
// NSType nameserver type
|
||||||
|
NSType NameServerType
|
||||||
|
// Port nameserver listening port
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies a nameserver object
|
||||||
|
func (n *NameServer) Copy() *NameServer {
|
||||||
|
return &NameServer{
|
||||||
|
IP: n.IP,
|
||||||
|
NSType: n.NSType,
|
||||||
|
Port: n.Port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEqual compares one nameserver with the other
|
||||||
|
func (n *NameServer) IsEqual(other *NameServer) bool {
|
||||||
|
return other.IP == n.IP &&
|
||||||
|
other.NSType == n.NSType &&
|
||||||
|
other.Port == n.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseNameServerURL parses a nameserver url in the format <type>://<ip>:<port>, e.g., udp://1.1.1.1:53
|
||||||
|
func ParseNameServerURL(nsURL string) (NameServer, error) {
|
||||||
|
parsedURL, err := url.Parse(nsURL)
|
||||||
|
if err != nil {
|
||||||
|
return NameServer{}, err
|
||||||
|
}
|
||||||
|
var ns NameServer
|
||||||
|
parsedScheme := strings.ToLower(parsedURL.Scheme)
|
||||||
|
nsType := ToNameServerType(parsedScheme)
|
||||||
|
if nsType == InvalidNameServerType {
|
||||||
|
return NameServer{}, fmt.Errorf("invalid nameserver url schema type, got %s", parsedScheme)
|
||||||
|
}
|
||||||
|
ns.NSType = nsType
|
||||||
|
|
||||||
|
parsedPort, err := strconv.Atoi(parsedURL.Port())
|
||||||
|
if err != nil {
|
||||||
|
return NameServer{}, fmt.Errorf("invalid nameserver url port, got %s", parsedURL.Port())
|
||||||
|
}
|
||||||
|
ns.Port = parsedPort
|
||||||
|
|
||||||
|
parsedAddr, err := netip.ParseAddr(parsedURL.Hostname())
|
||||||
|
if err != nil {
|
||||||
|
return NameServer{}, fmt.Errorf("invalid nameserver url IP, got %s", parsedURL.Hostname())
|
||||||
|
}
|
||||||
|
|
||||||
|
ns.IP = parsedAddr
|
||||||
|
|
||||||
|
return ns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies a nameserver group object
|
||||||
|
func (g *NameServerGroup) Copy() *NameServerGroup {
|
||||||
|
return &NameServerGroup{
|
||||||
|
ID: g.ID,
|
||||||
|
Name: g.Name,
|
||||||
|
Description: g.Description,
|
||||||
|
NameServers: g.NameServers,
|
||||||
|
Groups: g.Groups,
|
||||||
|
Enabled: g.Enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEqual compares one nameserver group with the other
|
||||||
|
func (g *NameServerGroup) IsEqual(other *NameServerGroup) bool {
|
||||||
|
return other.ID == g.ID &&
|
||||||
|
other.Name == g.Name &&
|
||||||
|
other.Description == g.Description &&
|
||||||
|
compareNameServerList(g.NameServers, other.NameServers) &&
|
||||||
|
compareGroupsList(g.Groups, other.Groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareNameServerList(list, other []NameServer) bool {
|
||||||
|
if len(list) != len(other) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ns := range list {
|
||||||
|
if !containsNameServer(ns, other) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsNameServer(element NameServer, list []NameServer) bool {
|
||||||
|
for _, ns := range list {
|
||||||
|
if ns.IsEqual(&element) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareGroupsList(list, other []string) bool {
|
||||||
|
if len(list) != len(other) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, id := range list {
|
||||||
|
match := false
|
||||||
|
for _, otherID := range other {
|
||||||
|
if id == otherID {
|
||||||
|
match = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
BIN
docs/media/netbird-sso-mfa-demo.gif
Normal file
BIN
docs/media/netbird-sso-mfa-demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 887 KiB |
21
go.mod
21
go.mod
@@ -11,7 +11,7 @@ require (
|
|||||||
github.com/kardianos/service v1.2.1-0.20210728001519-a323c3813bc7 //keep this version otherwise wiretrustee up command breaks
|
github.com/kardianos/service v1.2.1-0.20210728001519-a323c3813bc7 //keep this version otherwise wiretrustee up command breaks
|
||||||
github.com/onsi/ginkgo v1.16.5
|
github.com/onsi/ginkgo v1.16.5
|
||||||
github.com/onsi/gomega v1.18.1
|
github.com/onsi/gomega v1.18.1
|
||||||
github.com/pion/ice/v2 v2.1.17
|
github.com/pion/ice/v2 v2.2.7
|
||||||
github.com/rs/cors v1.8.0
|
github.com/rs/cors v1.8.0
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
github.com/spf13/cobra v1.3.0
|
github.com/spf13/cobra v1.3.0
|
||||||
@@ -32,7 +32,7 @@ require (
|
|||||||
github.com/c-robinson/iplib v1.0.3
|
github.com/c-robinson/iplib v1.0.3
|
||||||
github.com/coreos/go-iptables v0.6.0
|
github.com/coreos/go-iptables v0.6.0
|
||||||
github.com/creack/pty v1.1.18
|
github.com/creack/pty v1.1.18
|
||||||
github.com/eko/gocache/v2 v2.3.1
|
github.com/eko/gocache/v3 v3.1.1
|
||||||
github.com/getlantern/systray v1.2.1
|
github.com/getlantern/systray v1.2.1
|
||||||
github.com/gliderlabs/ssh v0.3.4
|
github.com/gliderlabs/ssh v0.3.4
|
||||||
github.com/google/nftables v0.0.0-20220808154552-2eca00135732
|
github.com/google/nftables v0.0.0-20220808154552-2eca00135732
|
||||||
@@ -41,8 +41,8 @@ require (
|
|||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/rs/xid v1.3.0
|
github.com/rs/xid v1.3.0
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||||
github.com/stretchr/testify v1.7.1
|
github.com/stretchr/testify v1.8.0
|
||||||
golang.org/x/net v0.0.0-20220513224357-95641704303c
|
golang.org/x/net v0.0.0-20220630215102-69896b714898
|
||||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
|
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,13 +80,13 @@ require (
|
|||||||
github.com/nxadm/tail v1.4.8 // indirect
|
github.com/nxadm/tail v1.4.8 // indirect
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||||
github.com/pegasus-kv/thrift v0.13.0 // indirect
|
github.com/pegasus-kv/thrift v0.13.0 // indirect
|
||||||
github.com/pion/dtls/v2 v2.1.2 // indirect
|
github.com/pion/dtls/v2 v2.1.5 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
github.com/pion/mdns v0.0.5 // indirect
|
github.com/pion/mdns v0.0.5 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/stun v0.3.5 // indirect
|
github.com/pion/stun v0.3.5 // indirect
|
||||||
github.com/pion/transport v0.13.0 // indirect
|
github.com/pion/transport v0.13.1 // indirect
|
||||||
github.com/pion/turn/v2 v2.0.7 // indirect
|
github.com/pion/turn/v2 v2.0.8 // indirect
|
||||||
github.com/pion/udp v0.1.1 // indirect
|
github.com/pion/udp v0.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.12.2 // indirect
|
github.com/prometheus/client_golang v1.12.2 // indirect
|
||||||
@@ -99,6 +99,7 @@ require (
|
|||||||
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect
|
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect
|
||||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
|
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
|
||||||
github.com/yuin/goldmark v1.4.1 // indirect
|
github.com/yuin/goldmark v1.4.1 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf // indirect
|
||||||
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect
|
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||||
@@ -112,11 +113,11 @@ require (
|
|||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
honnef.co/go/tools v0.2.2 // indirect
|
honnef.co/go/tools v0.2.2 // indirect
|
||||||
k8s.io/apimachinery v0.23.5 // indirect
|
k8s.io/apimachinery v0.23.5 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/pion/ice/v2 => github.com/wiretrustee/ice/v2 v2.1.21-0.20220218121004-dc81faead4bb
|
|
||||||
|
|
||||||
replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-20220905002524-6ac14ad5ea84
|
replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-20220905002524-6ac14ad5ea84
|
||||||
|
|
||||||
|
replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-20221012095658-dc8eda872c0c
|
||||||
|
|||||||
42
go.sum
42
go.sum
@@ -134,8 +134,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
|
|||||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
github.com/eko/gocache/v2 v2.3.1 h1:8MMkfqGJ0KIA9OXT0rXevcEIrU16oghrGDiIDJDFCa0=
|
github.com/eko/gocache/v3 v3.1.1 h1:r3CBwLnqPkcK56h9Do2CWw1kZ4TeKK0wDE1Oo/YZnhs=
|
||||||
github.com/eko/gocache/v2 v2.3.1/go.mod h1:l2z8OmpZHL0CpuzDJtxm267eF3mZW1NqUsMj+sKrbUs=
|
github.com/eko/gocache/v3 v3.1.1/go.mod h1:UpP/LyHAioP/a/dizgl0MpgZ3A3CkS4NbG/mWkGTQ9M=
|
||||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||||
@@ -178,8 +178,6 @@ github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0F
|
|||||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||||
github.com/getlantern/systray v1.2.1 h1:udsC2k98v2hN359VTFShuQW6GGprRprw6kD6539JikI=
|
|
||||||
github.com/getlantern/systray v1.2.1/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
|
|
||||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
@@ -474,6 +472,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
|
|||||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||||
github.com/netbirdio/service v0.0.0-20220905002524-6ac14ad5ea84 h1:u8kpzR9ld1uAeH/BAXsS0SfcnhooLWeO7UgHSBVPD9I=
|
github.com/netbirdio/service v0.0.0-20220905002524-6ac14ad5ea84 h1:u8kpzR9ld1uAeH/BAXsS0SfcnhooLWeO7UgHSBVPD9I=
|
||||||
github.com/netbirdio/service v0.0.0-20220905002524-6ac14ad5ea84/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
github.com/netbirdio/service v0.0.0-20220905002524-6ac14ad5ea84/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||||
|
github.com/netbirdio/systray v0.0.0-20221012095658-dc8eda872c0c h1:wK/s4nyZj/GF/kFJQjX6nqNfE0G3gcqd6hhnPCyp4sw=
|
||||||
|
github.com/netbirdio/systray v0.0.0-20221012095658-dc8eda872c0c/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
@@ -505,8 +505,10 @@ github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTK
|
|||||||
github.com/pegasus-kv/thrift v0.13.0 h1:4ESwaNoHImfbHa9RUGJiJZ4hrxorihZHk5aarYwY8d4=
|
github.com/pegasus-kv/thrift v0.13.0 h1:4ESwaNoHImfbHa9RUGJiJZ4hrxorihZHk5aarYwY8d4=
|
||||||
github.com/pegasus-kv/thrift v0.13.0/go.mod h1:Gl9NT/WHG6ABm6NsrbfE8LiJN0sAyneCrvB4qN4NPqQ=
|
github.com/pegasus-kv/thrift v0.13.0/go.mod h1:Gl9NT/WHG6ABm6NsrbfE8LiJN0sAyneCrvB4qN4NPqQ=
|
||||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
github.com/pion/dtls/v2 v2.1.2 h1:22Q1Jk9L++Yo7BIf9130MonNPfPVb+YgdYLeyQotuAA=
|
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
|
||||||
github.com/pion/dtls/v2 v2.1.2/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
|
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
|
||||||
|
github.com/pion/ice/v2 v2.2.7 h1:kG9tux3WdYUSqqqnf+O5zKlpy41PdlvLUBlYJeV2emQ=
|
||||||
|
github.com/pion/ice/v2 v2.2.7/go.mod h1:Ckj7cWZ717rtU01YoDQA9ntGWCk95D42uVZ8sI0EL+8=
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
||||||
@@ -516,10 +518,11 @@ github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TB
|
|||||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
||||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
||||||
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
||||||
github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY=
|
|
||||||
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
||||||
github.com/pion/turn/v2 v2.0.7 h1:SZhc00WDovK6czaN1RSiHqbwANtIO6wfZQsU0m0KNE8=
|
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
|
||||||
github.com/pion/turn/v2 v2.0.7/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
|
||||||
|
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
|
||||||
|
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
||||||
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
||||||
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
@@ -606,6 +609,7 @@ github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15
|
|||||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
@@ -613,8 +617,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
|||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
@@ -624,8 +629,6 @@ github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJ
|
|||||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
|
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
|
||||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||||
github.com/wiretrustee/ice/v2 v2.1.21-0.20220218121004-dc81faead4bb h1:CU1/+CEeCPvYXgfAyqTJXSQSf6hW3wsWM6Dfz6HkHEQ=
|
|
||||||
github.com/wiretrustee/ice/v2 v2.1.21-0.20220218121004-dc81faead4bb/go.mod h1:XT1Nrb4OxbVFPffbQMbq4PaeEkpRLVzdphh3fjrw7DY=
|
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
@@ -662,7 +665,7 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c=
|
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c=
|
||||||
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@@ -675,6 +678,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
|||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
|
golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf h1:oXVg4h2qJDd9htKxb5SCpFBHLipW6hXmL3qpUixS2jw=
|
||||||
|
golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
|
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
|
||||||
@@ -772,8 +777,10 @@ golang.org/x/net v0.0.0-20211208012354-db4efeb81f4b/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220513224357-95641704303c h1:nF9mHSvoKBLkQNQhJZNsc66z2UzAMUbLGjC95CF3pU0=
|
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw=
|
||||||
|
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -906,6 +913,8 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20211214234402-4825e8c3871d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211214234402-4825e8c3871d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
|
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
|
||||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
@@ -1185,8 +1194,9 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (w *WGIface) assignAddr() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WireguardModExists check if we can load wireguard mod (linux only)
|
// WireguardModuleIsLoaded check if we can load wireguard mod (linux only)
|
||||||
func WireguardModExists() bool {
|
func WireguardModuleIsLoaded() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,29 @@
|
|||||||
package iface
|
package iface
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NativeLink struct {
|
type NativeLink struct {
|
||||||
Link *netlink.Link
|
Link *netlink.Link
|
||||||
}
|
}
|
||||||
|
|
||||||
// WireguardModExists check if we can load wireguard mod (linux only)
|
|
||||||
func WireguardModExists() bool {
|
|
||||||
link := newWGLink("mustnotexist")
|
|
||||||
|
|
||||||
// We willingly try to create a device with an invalid
|
|
||||||
// MTU here as the validation of the MTU will be performed after
|
|
||||||
// the validation of the link kind and hence allows us to check
|
|
||||||
// for the existance of the wireguard module without actually
|
|
||||||
// creating a link.
|
|
||||||
//
|
|
||||||
// As a side-effect, this will also let the kernel lazy-load
|
|
||||||
// the wireguard module.
|
|
||||||
link.attrs.MTU = math.MaxInt
|
|
||||||
|
|
||||||
err := netlink.LinkAdd(link)
|
|
||||||
|
|
||||||
return errors.Is(err, syscall.EINVAL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create creates a new Wireguard interface, sets a given IP and brings it up.
|
// Create creates a new Wireguard interface, sets a given IP and brings it up.
|
||||||
// Will reuse an existing one.
|
// Will reuse an existing one.
|
||||||
func (w *WGIface) Create() error {
|
func (w *WGIface) Create() error {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
defer w.mu.Unlock()
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
if WireguardModExists() {
|
if WireguardModuleIsLoaded() {
|
||||||
log.Info("using kernel WireGuard")
|
log.Info("using kernel WireGuard")
|
||||||
return w.createWithKernel()
|
return w.createWithKernel()
|
||||||
} else {
|
} else {
|
||||||
|
if !tunModuleIsLoaded() {
|
||||||
|
return fmt.Errorf("couldn't check or load tun module")
|
||||||
|
}
|
||||||
log.Info("using userspace WireGuard")
|
log.Info("using userspace WireGuard")
|
||||||
return w.createWithUserspace()
|
return w.createWithUserspace()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func (w *WGIface) UpdateAddr(newAddr string) error {
|
|||||||
return w.assignAddr(luid)
|
return w.assignAddr(luid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WireguardModExists check if we can load wireguard mod (linux only)
|
// WireguardModuleIsLoaded check if we can load wireguard mod (linux only)
|
||||||
func WireguardModExists() bool {
|
func WireguardModuleIsLoaded() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
349
iface/module_linux.go
Normal file
349
iface/module_linux.go
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
// Package iface provides wireguard network interface creation and management
|
||||||
|
package iface
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Holds logic to check existence of kernel modules used by wireguard interfaces
|
||||||
|
// Copied from https://github.com/paultag/go-modprobe and
|
||||||
|
// https://github.com/pmorjan/kmod
|
||||||
|
|
||||||
|
type status int
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultModuleDir = "/lib/modules"
|
||||||
|
unknown status = iota
|
||||||
|
unloaded
|
||||||
|
unloading
|
||||||
|
loading
|
||||||
|
live
|
||||||
|
inuse
|
||||||
|
)
|
||||||
|
|
||||||
|
type module struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrModuleNotFound is the error resulting if a module can't be found.
|
||||||
|
ErrModuleNotFound = errors.New("module not found")
|
||||||
|
moduleLibDir = defaultModuleDir
|
||||||
|
// get the root directory for the kernel modules. If this line panics,
|
||||||
|
// it's because getModuleRoot has failed to get the uname of the running
|
||||||
|
// kernel (likely a non-POSIX system, but maybe a broken kernel?)
|
||||||
|
moduleRoot = getModuleRoot()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the module root (/lib/modules/$(uname -r)/)
|
||||||
|
func getModuleRoot() string {
|
||||||
|
uname := unix.Utsname{}
|
||||||
|
if err := unix.Uname(&uname); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for ; uname.Release[i] != 0; i++ {
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(moduleLibDir, string(uname.Release[:i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// tunModuleIsLoaded check if tun module exist, if is not attempt to load it
|
||||||
|
func tunModuleIsLoaded() bool {
|
||||||
|
_, err := os.Stat("/dev/net/tun")
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("couldn't access device /dev/net/tun, go error %v, "+
|
||||||
|
"will attempt to load tun module, if running on container add flag --cap-add=NET_ADMIN", err)
|
||||||
|
|
||||||
|
tunLoaded, err := tryToLoadModule("tun")
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to find or load tun module, got error: %v", err)
|
||||||
|
}
|
||||||
|
return tunLoaded
|
||||||
|
}
|
||||||
|
|
||||||
|
// WireguardModuleIsLoaded check if we can load wireguard mod (linux only)
|
||||||
|
func WireguardModuleIsLoaded() bool {
|
||||||
|
if canCreateFakeWireguardInterface() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, err := tryToLoadModule("wireguard")
|
||||||
|
if err != nil {
|
||||||
|
log.Info(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
func canCreateFakeWireguardInterface() bool {
|
||||||
|
link := newWGLink("mustnotexist")
|
||||||
|
|
||||||
|
// We willingly try to create a device with an invalid
|
||||||
|
// MTU here as the validation of the MTU will be performed after
|
||||||
|
// the validation of the link kind and hence allows us to check
|
||||||
|
// for the existance of the wireguard module without actually
|
||||||
|
// creating a link.
|
||||||
|
//
|
||||||
|
// As a side-effect, this will also let the kernel lazy-load
|
||||||
|
// the wireguard module.
|
||||||
|
link.attrs.MTU = math.MaxInt
|
||||||
|
|
||||||
|
err := netlink.LinkAdd(link)
|
||||||
|
|
||||||
|
return errors.Is(err, syscall.EINVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryToLoadModule(moduleName string) (bool, error) {
|
||||||
|
if isModuleEnabled(moduleName) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
modulePath, err := getModulePath(moduleName)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("couldn't find module path for %s, error: %v", moduleName, err)
|
||||||
|
}
|
||||||
|
if modulePath == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("trying to load %s module", moduleName)
|
||||||
|
|
||||||
|
err = loadModuleWithDependencies(moduleName, modulePath)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("couldn't load %s module, error: %v", moduleName, err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isModuleEnabled(name string) bool {
|
||||||
|
builtin, builtinErr := isBuiltinModule(name)
|
||||||
|
state, statusErr := moduleStatus(name)
|
||||||
|
return (builtinErr == nil && builtin) || (statusErr == nil && state >= loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getModulePath(name string) (string, error) {
|
||||||
|
var foundPath string
|
||||||
|
skipRemainingDirs := false
|
||||||
|
|
||||||
|
err := filepath.WalkDir(
|
||||||
|
moduleRoot,
|
||||||
|
func(path string, info fs.DirEntry, err error) error {
|
||||||
|
if skipRemainingDirs {
|
||||||
|
return fs.SkipDir
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// skip broken files
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.Type().IsRegular() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nameFromPath := pathToName(path)
|
||||||
|
if nameFromPath == name {
|
||||||
|
foundPath = path
|
||||||
|
skipRemainingDirs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathToName(s string) string {
|
||||||
|
s = filepath.Base(s)
|
||||||
|
for ext := filepath.Ext(s); ext != ""; ext = filepath.Ext(s) {
|
||||||
|
s = strings.TrimSuffix(s, ext)
|
||||||
|
}
|
||||||
|
return cleanName(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanName(s string) string {
|
||||||
|
return strings.ReplaceAll(strings.TrimSpace(s), "-", "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBuiltinModule(name string) (bool, error) {
|
||||||
|
f, err := os.Open(filepath.Join(moduleRoot, "/modules.builtin"))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := f.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed closing modules.builtin file, %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if pathToName(line) == name {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// /proc/modules
|
||||||
|
// name | memory size | reference count | references | state: <Live|Loading|Unloading>
|
||||||
|
// macvlan 28672 1 macvtap, Live 0x0000000000000000
|
||||||
|
func moduleStatus(name string) (status, error) {
|
||||||
|
state := unknown
|
||||||
|
f, err := os.Open("/proc/modules")
|
||||||
|
if err != nil {
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := f.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed closing /proc/modules file, %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
state = unloaded
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.Fields(scanner.Text())
|
||||||
|
if fields[0] == name {
|
||||||
|
if fields[2] != "0" {
|
||||||
|
state = inuse
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch fields[4] {
|
||||||
|
case "Live":
|
||||||
|
state = live
|
||||||
|
case "Loading":
|
||||||
|
state = loading
|
||||||
|
case "Unloading":
|
||||||
|
state = unloading
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadModuleWithDependencies(name, path string) error {
|
||||||
|
deps, err := getModuleDependencies(name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't load list of module %s dependecies", name)
|
||||||
|
}
|
||||||
|
for _, dep := range deps {
|
||||||
|
err = loadModule(dep.name, dep.path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't load dependecy module %s for %s", dep.name, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return loadModule(name, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadModule(name, path string) error {
|
||||||
|
state, err := moduleStatus(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if state >= loading {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := f.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed closing %s file, %v", path, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// first try finit_module(2), then init_module(2)
|
||||||
|
err = unix.FinitModule(int(f.Fd()), "", 0)
|
||||||
|
if errors.Is(err, unix.ENOSYS) {
|
||||||
|
buf, err := ioutil.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return unix.InitModule(buf, "")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getModuleDependencies returns a module dependencies
|
||||||
|
func getModuleDependencies(name string) ([]module, error) {
|
||||||
|
f, err := os.Open(filepath.Join(moduleRoot, "/modules.dep"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := f.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed closing modules.dep file, %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var deps []string
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if pathToName(strings.TrimSuffix(fields[0], ":")) == name {
|
||||||
|
deps = fields
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(deps) == 0 {
|
||||||
|
return nil, ErrModuleNotFound
|
||||||
|
}
|
||||||
|
deps[0] = strings.TrimSuffix(deps[0], ":")
|
||||||
|
|
||||||
|
var modules []module
|
||||||
|
for _, v := range deps {
|
||||||
|
if pathToName(v) != name {
|
||||||
|
modules = append(modules, module{
|
||||||
|
name: pathToName(v),
|
||||||
|
path: filepath.Join(moduleRoot, v),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modules, nil
|
||||||
|
}
|
||||||
221
iface/module_linux_test.go
Normal file
221
iface/module_linux_test.go
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
package iface
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetModuleDependencies(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
module string
|
||||||
|
expected []module
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Get Single Dependency",
|
||||||
|
module: "bar",
|
||||||
|
expected: []module{
|
||||||
|
{name: "foo", path: "kernel/a/foo.ko"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Get Multiple Dependencies",
|
||||||
|
module: "baz",
|
||||||
|
expected: []module{
|
||||||
|
{name: "foo", path: "kernel/a/foo.ko"},
|
||||||
|
{name: "bar", path: "kernel/a/bar.ko"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Get No Dependencies",
|
||||||
|
module: "foo",
|
||||||
|
expected: []module{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
defer resetGlobals()
|
||||||
|
_, _ = createFiles(t)
|
||||||
|
modules, err := getModuleDependencies(testCase.module)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := testCase.expected
|
||||||
|
for i := range expected {
|
||||||
|
expected[i].path = moduleRoot + "/" + expected[i].path
|
||||||
|
}
|
||||||
|
|
||||||
|
require.ElementsMatchf(t, modules, expected, "returned modules should match")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsBuiltinModule(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
module string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Built In Should Return True",
|
||||||
|
module: "foo_bi",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Not Built In Should Return False",
|
||||||
|
module: "not_built_in",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
defer resetGlobals()
|
||||||
|
_, _ = createFiles(t)
|
||||||
|
|
||||||
|
isBuiltIn, err := isBuiltinModule(testCase.module)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, testCase.expected, isBuiltIn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModuleStatus(t *testing.T) {
|
||||||
|
random, err := getRandomLoadedModule(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("should be able to get random module")
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
module string
|
||||||
|
shouldBeLoaded bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Should Return Module Loading Or Greater Status",
|
||||||
|
module: random,
|
||||||
|
shouldBeLoaded: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Return Module Unloaded Or Lower Status",
|
||||||
|
module: "not_loaded_module",
|
||||||
|
shouldBeLoaded: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
defer resetGlobals()
|
||||||
|
_, _ = createFiles(t)
|
||||||
|
|
||||||
|
state, err := moduleStatus(testCase.module)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if testCase.shouldBeLoaded {
|
||||||
|
require.GreaterOrEqual(t, loading, state, "moduleStatus for %s should return state loading", testCase.module)
|
||||||
|
} else {
|
||||||
|
require.Less(t, state, loading, "module should return state unloading or lower")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetGlobals() {
|
||||||
|
moduleLibDir = defaultModuleDir
|
||||||
|
moduleRoot = getModuleRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFiles(t *testing.T) (string, []module) {
|
||||||
|
writeFile := func(path, text string) {
|
||||||
|
if err := ioutil.WriteFile(path, []byte(text), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var u unix.Utsname
|
||||||
|
if err := unix.Uname(&u); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
moduleLibDir = t.TempDir()
|
||||||
|
|
||||||
|
moduleRoot = getModuleRoot()
|
||||||
|
if err := os.Mkdir(moduleRoot, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
text := "kernel/a/foo.ko:\n"
|
||||||
|
text += "kernel/a/bar.ko: kernel/a/foo.ko\n"
|
||||||
|
text += "kernel/a/baz.ko: kernel/a/bar.ko kernel/a/foo.ko\n"
|
||||||
|
writeFile(filepath.Join(moduleRoot, "/modules.dep"), text)
|
||||||
|
|
||||||
|
text = "kernel/a/foo_bi.ko\n"
|
||||||
|
text += "kernel/a/bar-bi.ko.gz\n"
|
||||||
|
writeFile(filepath.Join(moduleRoot, "/modules.builtin"), text)
|
||||||
|
|
||||||
|
modules := []module{
|
||||||
|
{name: "foo", path: "kernel/a/foo.ko"},
|
||||||
|
{name: "bar", path: "kernel/a/bar.ko"},
|
||||||
|
{name: "baz", path: "kernel/a/baz.ko"},
|
||||||
|
}
|
||||||
|
return moduleLibDir, modules
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRandomLoadedModule(t *testing.T) (string, error) {
|
||||||
|
f, err := os.Open("/proc/modules")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := f.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("failed closing /proc/modules file, %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
lines, err := lineCounter(f)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
counter := 1
|
||||||
|
midLine := lines / 2
|
||||||
|
modName := ""
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.Fields(scanner.Text())
|
||||||
|
if counter == midLine {
|
||||||
|
if fields[4] == "Unloading" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
modName = fields[0]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
if scanner.Err() != nil {
|
||||||
|
return "", scanner.Err()
|
||||||
|
}
|
||||||
|
return modName, nil
|
||||||
|
}
|
||||||
|
func lineCounter(r io.Reader) (int, error) {
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
|
count := 0
|
||||||
|
lineSep := []byte{'\n'}
|
||||||
|
|
||||||
|
for {
|
||||||
|
c, err := r.Read(buf)
|
||||||
|
count += bytes.Count(buf[:c], lineSep)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == io.EOF:
|
||||||
|
return count, nil
|
||||||
|
|
||||||
|
case err != nil:
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ LETSENCRYPT_VOLUMESUFFIX="letsencrypt"
|
|||||||
|
|
||||||
NETBIRD_AUTH_DEVICE_AUTH_PROVIDER="none"
|
NETBIRD_AUTH_DEVICE_AUTH_PROVIDER="none"
|
||||||
|
|
||||||
|
NETBIRD_DISABLE_ANONYMOUS_METRICS=${NETBIRD_DISABLE_ANONYMOUS_METRICS:-false}
|
||||||
|
|
||||||
# exports
|
# exports
|
||||||
export NETBIRD_DOMAIN
|
export NETBIRD_DOMAIN
|
||||||
export NETBIRD_AUTH_CLIENT_ID
|
export NETBIRD_AUTH_CLIENT_ID
|
||||||
@@ -45,6 +47,8 @@ export NETBIRD_MGMT_API_CERT_KEY_FILE
|
|||||||
export NETBIRD_AUTH_DEVICE_AUTH_PROVIDER
|
export NETBIRD_AUTH_DEVICE_AUTH_PROVIDER
|
||||||
export NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID
|
export NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID
|
||||||
export NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT
|
export NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT
|
||||||
|
export NETBIRD_AUTH_REDIRECT_URI
|
||||||
|
export NETBIRD_AUTH_SILENT_REDIRECT_URI
|
||||||
export TURN_USER
|
export TURN_USER
|
||||||
export TURN_PASSWORD
|
export TURN_PASSWORD
|
||||||
export TURN_MIN_PORT
|
export TURN_MIN_PORT
|
||||||
@@ -53,3 +57,4 @@ export VOLUME_PREFIX
|
|||||||
export MGMT_VOLUMESUFFIX
|
export MGMT_VOLUMESUFFIX
|
||||||
export SIGNAL_VOLUMESUFFIX
|
export SIGNAL_VOLUMESUFFIX
|
||||||
export LETSENCRYPT_VOLUMESUFFIX
|
export LETSENCRYPT_VOLUMESUFFIX
|
||||||
|
export NETBIRD_DISABLE_ANONYMOUS_METRICS
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ services:
|
|||||||
- NGINX_SSL_PORT=443
|
- NGINX_SSL_PORT=443
|
||||||
- LETSENCRYPT_DOMAIN=$NETBIRD_DOMAIN
|
- LETSENCRYPT_DOMAIN=$NETBIRD_DOMAIN
|
||||||
- LETSENCRYPT_EMAIL=$NETBIRD_LETSENCRYPT_EMAIL
|
- LETSENCRYPT_EMAIL=$NETBIRD_LETSENCRYPT_EMAIL
|
||||||
|
- AUTH_REDIRECT_URI=$NETBIRD_AUTH_REDIRECT_URI
|
||||||
|
- AUTH_SILENT_REDIRECT_URI=$NETBIRD_AUTH_SILENT_REDIRECT_URI
|
||||||
volumes:
|
volumes:
|
||||||
- $LETSENCRYPT_VOLUMENAME:/etc/letsencrypt/
|
- $LETSENCRYPT_VOLUMENAME:/etc/letsencrypt/
|
||||||
# Signal
|
# Signal
|
||||||
@@ -46,7 +48,7 @@ services:
|
|||||||
# # port and command for Let's Encrypt validation without dashboard container
|
# # port and command for Let's Encrypt validation without dashboard container
|
||||||
# - 443:443
|
# - 443:443
|
||||||
# command: ["--letsencrypt-domain", "$NETBIRD_DOMAIN", "--log-file", "console"]
|
# command: ["--letsencrypt-domain", "$NETBIRD_DOMAIN", "--log-file", "console"]
|
||||||
command: ["--port", "443", "--log-file", "console"]
|
command: ["--port", "443", "--log-file", "console", "--disable-anonymous-metrics=$NETBIRD_DISABLE_ANONYMOUS_METRICS"]
|
||||||
# Coturn
|
# Coturn
|
||||||
coturn:
|
coturn:
|
||||||
image: coturn/coturn
|
image: coturn/coturn
|
||||||
|
|||||||
@@ -12,4 +12,11 @@ NETBIRD_USE_AUTH0="false"
|
|||||||
NETBIRD_AUTH_DEVICE_AUTH_PROVIDER="none"
|
NETBIRD_AUTH_DEVICE_AUTH_PROVIDER="none"
|
||||||
NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID=""
|
NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID=""
|
||||||
# e.g. hello@mydomain.com
|
# e.g. hello@mydomain.com
|
||||||
NETBIRD_LETSENCRYPT_EMAIL=""
|
NETBIRD_LETSENCRYPT_EMAIL=""
|
||||||
|
# if your IDP provider doesn't support fragmented URIs, configure custom
|
||||||
|
# redirect and silent redirect URIs, these will be concatenated into your NETBIRD_DOMAIN domain.
|
||||||
|
# NETBIRD_AUTH_REDIRECT_URI="/peers"
|
||||||
|
# NETBIRD_AUTH_SILENT_REDIRECT_URI="/add-peers"
|
||||||
|
|
||||||
|
# Disable anonymous metrics collection, see more information at https://netbird.io/docs/FAQ/metrics-collection
|
||||||
|
NETBIRD_DISABLE_ANONYMOUS_METRICS=false
|
||||||
@@ -10,4 +10,5 @@ NETBIRD_AUTH_CLIENT_ID=$CI_NETBIRD_AUTH_CLIENT_ID
|
|||||||
NETBIRD_USE_AUTH0=$CI_NETBIRD_USE_AUTH0
|
NETBIRD_USE_AUTH0=$CI_NETBIRD_USE_AUTH0
|
||||||
NETBIRD_AUTH_AUDIENCE=$CI_NETBIRD_AUTH_AUDIENCE
|
NETBIRD_AUTH_AUDIENCE=$CI_NETBIRD_AUTH_AUDIENCE
|
||||||
# e.g. hello@mydomain.com
|
# e.g. hello@mydomain.com
|
||||||
NETBIRD_LETSENCRYPT_EMAIL=""
|
NETBIRD_LETSENCRYPT_EMAIL=""
|
||||||
|
NETBIRD_AUTH_REDIRECT_URI="/peers"
|
||||||
@@ -109,7 +109,9 @@ func (c *GrpcClient) Sync(msgHandler func(msg *proto.SyncResponse) error) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
stream, err := c.connectToStream(*serverPubKey)
|
ctx, cancelStream := context.WithCancel(c.ctx)
|
||||||
|
defer cancelStream()
|
||||||
|
stream, err := c.connectToStream(ctx, *serverPubKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("failed to open Management Service stream: %s", err)
|
log.Debugf("failed to open Management Service stream: %s", err)
|
||||||
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
||||||
@@ -145,7 +147,7 @@ func (c *GrpcClient) Sync(msgHandler func(msg *proto.SyncResponse) error) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GrpcClient) connectToStream(serverPubKey wgtypes.Key) (proto.ManagementService_SyncClient, error) {
|
func (c *GrpcClient) connectToStream(ctx context.Context, serverPubKey wgtypes.Key) (proto.ManagementService_SyncClient, error) {
|
||||||
req := &proto.SyncRequest{}
|
req := &proto.SyncRequest{}
|
||||||
|
|
||||||
myPrivateKey := c.key
|
myPrivateKey := c.key
|
||||||
@@ -156,9 +158,12 @@ func (c *GrpcClient) connectToStream(serverPubKey wgtypes.Key) (proto.Management
|
|||||||
log.Errorf("failed encrypting message: %s", err)
|
log.Errorf("failed encrypting message: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
syncReq := &proto.EncryptedMessage{WgPubKey: myPublicKey.String(), Body: encryptedReq}
|
syncReq := &proto.EncryptedMessage{WgPubKey: myPublicKey.String(), Body: encryptedReq}
|
||||||
return c.realClient.Sync(c.ctx, syncReq)
|
sync, err := c.realClient.Sync(ctx, syncReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sync, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GrpcClient) receiveEvents(stream proto.ManagementService_SyncClient, serverPubKey wgtypes.Key, msgHandler func(msg *proto.SyncResponse) error) error {
|
func (c *GrpcClient) receiveEvents(stream proto.ManagementService_SyncClient, serverPubKey wgtypes.Key, msgHandler func(msg *proto.SyncResponse) error) error {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
httpapi "github.com/netbirdio/netbird/management/server/http"
|
httpapi "github.com/netbirdio/netbird/management/server/http"
|
||||||
|
"github.com/netbirdio/netbird/management/server/metrics"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"golang.org/x/crypto/acme/autocert"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c"
|
||||||
@@ -161,6 +164,21 @@ var (
|
|||||||
}
|
}
|
||||||
mgmtProto.RegisterManagementServiceServer(gRPCAPIHandler, srv)
|
mgmtProto.RegisterManagementServiceServer(gRPCAPIHandler, srv)
|
||||||
|
|
||||||
|
installationID, err := getInstallationID(store)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("cannot load TLS credentials: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("metrics ", disableMetrics)
|
||||||
|
|
||||||
|
if !disableMetrics {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
metricsWorker := metrics.NewWorker(ctx, installationID, store, peersUpdateManager)
|
||||||
|
go metricsWorker.Run()
|
||||||
|
}
|
||||||
|
|
||||||
var compatListener net.Listener
|
var compatListener net.Listener
|
||||||
if mgmtPort != ManagementLegacyPort {
|
if mgmtPort != ManagementLegacyPort {
|
||||||
// The Management gRPC server was running on port 33073 previously. Old agents that are already connected to it
|
// The Management gRPC server was running on port 33073 previously. Old agents that are already connected to it
|
||||||
@@ -228,6 +246,20 @@ func notifyStop(msg string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getInstallationID(store server.Store) (string, error) {
|
||||||
|
installationID := store.GetInstallationID()
|
||||||
|
if installationID != "" {
|
||||||
|
return installationID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
installationID = strings.ToUpper(uuid.New().String())
|
||||||
|
err := store.SaveInstallationID(installationID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return installationID, nil
|
||||||
|
}
|
||||||
|
|
||||||
func serveGRPC(grpcServer *grpc.Server, port int) (net.Listener, error) {
|
func serveGRPC(grpcServer *grpc.Server, port int) (net.Listener, error) {
|
||||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ var (
|
|||||||
mgmtConfig string
|
mgmtConfig string
|
||||||
logLevel string
|
logLevel string
|
||||||
logFile string
|
logFile string
|
||||||
|
disableMetrics bool
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "netbird-mgmt",
|
Use: "netbird-mgmt",
|
||||||
@@ -66,6 +67,7 @@ func init() {
|
|||||||
mgmtCmd.Flags().StringVar(&mgmtLetsencryptDomain, "letsencrypt-domain", "", "a domain to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS")
|
mgmtCmd.Flags().StringVar(&mgmtLetsencryptDomain, "letsencrypt-domain", "", "a domain to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS")
|
||||||
mgmtCmd.Flags().StringVar(&certFile, "cert-file", "", "Location of your SSL certificate. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
|
mgmtCmd.Flags().StringVar(&certFile, "cert-file", "", "Location of your SSL certificate. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
|
||||||
mgmtCmd.Flags().StringVar(&certKey, "cert-key", "", "Location of your SSL certificate private key. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
|
mgmtCmd.Flags().StringVar(&certKey, "cert-key", "", "Location of your SSL certificate private key. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
|
||||||
|
mgmtCmd.Flags().BoolVar(&disableMetrics, "disable-anonymous-metrics", false, "disables push of anonymous usage metrics to NetBird")
|
||||||
rootCmd.MarkFlagRequired("config") //nolint
|
rootCmd.MarkFlagRequired("config") //nolint
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "")
|
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "")
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if ! which realpath > /dev/null 2>&1
|
||||||
|
then
|
||||||
|
echo realpath is not installed
|
||||||
|
echo run: brew install coreutils
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
old_pwd=$(pwd)
|
||||||
|
script_path=$(dirname $(realpath "$0"))
|
||||||
|
cd "$script_path"
|
||||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
|
||||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
|
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
|
||||||
protoc -I proto/ proto/management.proto --go_out=. --go-grpc_out=.
|
protoc -I ./ ./management.proto --go_out=../ --go-grpc_out=../
|
||||||
|
cd "$old_pwd"
|
||||||
@@ -3,8 +3,9 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/eko/gocache/v2/cache"
|
"github.com/eko/gocache/v3/cache"
|
||||||
cacheStore "github.com/eko/gocache/v2/store"
|
cacheStore "github.com/eko/gocache/v3/store"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"github.com/netbirdio/netbird/management/server/idp"
|
"github.com/netbirdio/netbird/management/server/idp"
|
||||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -28,20 +30,29 @@ const (
|
|||||||
CacheExpirationMin = 3 * 24 * 3600 * time.Second // 3 days
|
CacheExpirationMin = 3 * 24 * 3600 * time.Second // 3 days
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func cacheEntryExpiration() time.Duration {
|
||||||
|
r := rand.Intn(int(CacheExpirationMax.Milliseconds()-CacheExpirationMin.Milliseconds())) + int(CacheExpirationMin.Milliseconds())
|
||||||
|
return time.Duration(r) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
type AccountManager interface {
|
type AccountManager interface {
|
||||||
GetOrCreateAccountByUser(userId, domain string) (*Account, error)
|
GetOrCreateAccountByUser(userId, domain string) (*Account, error)
|
||||||
GetAccountByUser(userId string) (*Account, error)
|
GetAccountByUser(userId string) (*Account, error)
|
||||||
AddSetupKey(
|
CreateSetupKey(
|
||||||
accountId string,
|
accountId string,
|
||||||
keyName string,
|
keyName string,
|
||||||
keyType SetupKeyType,
|
keyType SetupKeyType,
|
||||||
expiresIn time.Duration,
|
expiresIn time.Duration,
|
||||||
|
autoGroups []string,
|
||||||
) (*SetupKey, error)
|
) (*SetupKey, error)
|
||||||
RevokeSetupKey(accountId string, keyId string) (*SetupKey, error)
|
SaveSetupKey(accountID string, key *SetupKey) (*SetupKey, error)
|
||||||
RenameSetupKey(accountId string, keyId string, newName string) (*SetupKey, error)
|
CreateUser(accountID string, key *UserInfo) (*UserInfo, error)
|
||||||
|
ListSetupKeys(accountID string) ([]*SetupKey, error)
|
||||||
|
SaveUser(accountID string, key *User) (*UserInfo, error)
|
||||||
|
GetSetupKey(accountID, keyID string) (*SetupKey, error)
|
||||||
GetAccountById(accountId string) (*Account, error)
|
GetAccountById(accountId string) (*Account, error)
|
||||||
GetAccountByUserOrAccountId(userId, accountId, domain string) (*Account, error)
|
GetAccountByUserOrAccountId(userId, accountId, domain string) (*Account, error)
|
||||||
GetAccountWithAuthorizationClaims(claims jwtclaims.AuthorizationClaims) (*Account, error)
|
GetAccountFromToken(claims jwtclaims.AuthorizationClaims) (*Account, error)
|
||||||
IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error)
|
IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error)
|
||||||
AccountExists(accountId string) (*bool, error)
|
AccountExists(accountId string) (*bool, error)
|
||||||
GetPeer(peerKey string) (*Peer, error)
|
GetPeer(peerKey string) (*Peer, error)
|
||||||
@@ -75,15 +86,25 @@ type AccountManager interface {
|
|||||||
UpdateRoute(accountID string, routeID string, operations []RouteUpdateOperation) (*route.Route, error)
|
UpdateRoute(accountID string, routeID string, operations []RouteUpdateOperation) (*route.Route, error)
|
||||||
DeleteRoute(accountID, routeID string) error
|
DeleteRoute(accountID, routeID string) error
|
||||||
ListRoutes(accountID string) ([]*route.Route, error)
|
ListRoutes(accountID string) ([]*route.Route, error)
|
||||||
|
GetNameServerGroup(accountID, nsGroupID string) (*nbdns.NameServerGroup, error)
|
||||||
|
CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error)
|
||||||
|
SaveNameServerGroup(accountID string, nsGroupToSave *nbdns.NameServerGroup) error
|
||||||
|
UpdateNameServerGroup(accountID, nsGroupID string, operations []NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error)
|
||||||
|
DeleteNameServerGroup(accountID, nsGroupID string) error
|
||||||
|
ListNameServerGroups(accountID string) ([]*nbdns.NameServerGroup, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultAccountManager struct {
|
type DefaultAccountManager struct {
|
||||||
Store Store
|
Store Store
|
||||||
// mutex to synchronise account operations (e.g. generating Peer IP address inside the Network)
|
// mux to synchronise account operations (e.g. generating Peer IP address inside the Network)
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
|
// cacheMux and cacheLoading helps to make sure that only a single cache reload runs at a time per accountID
|
||||||
|
cacheMux sync.Mutex
|
||||||
|
// cacheLoading keeps the accountIDs that are currently reloading. The accountID has to be removed once cache has been reloaded
|
||||||
|
cacheLoading map[string]chan struct{}
|
||||||
peersUpdateManager *PeersUpdateManager
|
peersUpdateManager *PeersUpdateManager
|
||||||
idpManager idp.Manager
|
idpManager idp.Manager
|
||||||
cacheManager cache.CacheInterface
|
cacheManager cache.CacheInterface[[]*idp.UserData]
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,13 +123,16 @@ type Account struct {
|
|||||||
Groups map[string]*Group
|
Groups map[string]*Group
|
||||||
Rules map[string]*Rule
|
Rules map[string]*Rule
|
||||||
Routes map[string]*route.Route
|
Routes map[string]*route.Route
|
||||||
|
NameServerGroups map[string]*nbdns.NameServerGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
|
AutoGroups []string `json:"auto_groups"`
|
||||||
|
Status string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Account) Copy() *Account {
|
func (a *Account) Copy() *Account {
|
||||||
@@ -137,15 +161,27 @@ func (a *Account) Copy() *Account {
|
|||||||
rules[id] = rule.Copy()
|
rules[id] = rule.Copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routes := map[string]*route.Route{}
|
||||||
|
for id, route := range a.Routes {
|
||||||
|
routes[id] = route.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
nsGroups := map[string]*nbdns.NameServerGroup{}
|
||||||
|
for id, nsGroup := range a.NameServerGroups {
|
||||||
|
nsGroups[id] = nsGroup.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
return &Account{
|
return &Account{
|
||||||
Id: a.Id,
|
Id: a.Id,
|
||||||
CreatedBy: a.CreatedBy,
|
CreatedBy: a.CreatedBy,
|
||||||
SetupKeys: setupKeys,
|
SetupKeys: setupKeys,
|
||||||
Network: a.Network.Copy(),
|
Network: a.Network.Copy(),
|
||||||
Peers: peers,
|
Peers: peers,
|
||||||
Users: users,
|
Users: users,
|
||||||
Groups: groups,
|
Groups: groups,
|
||||||
Rules: rules,
|
Rules: rules,
|
||||||
|
Routes: routes,
|
||||||
|
NameServerGroups: nsGroups,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +204,8 @@ func BuildManager(
|
|||||||
peersUpdateManager: peersUpdateManager,
|
peersUpdateManager: peersUpdateManager,
|
||||||
idpManager: idpManager,
|
idpManager: idpManager,
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
|
cacheMux: sync.Mutex{},
|
||||||
|
cacheLoading: map[string]chan struct{}{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// if account has not default group
|
// if account has not default group
|
||||||
@@ -184,9 +222,9 @@ func BuildManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
gocacheClient := gocache.New(CacheExpirationMax, 30*time.Minute)
|
gocacheClient := gocache.New(CacheExpirationMax, 30*time.Minute)
|
||||||
gocacheStore := cacheStore.NewGoCache(gocacheClient, nil)
|
gocacheStore := cacheStore.NewGoCache(gocacheClient)
|
||||||
|
|
||||||
am.cacheManager = cache.NewLoadable(am.loadFromCache, cache.New(gocacheStore))
|
am.cacheManager = cache.NewLoadable[[]*idp.UserData](am.loadAccount, cache.New[[]*idp.UserData](gocacheStore))
|
||||||
|
|
||||||
if !isNil(am.idpManager) {
|
if !isNil(am.idpManager) {
|
||||||
go func() {
|
go func() {
|
||||||
@@ -231,11 +269,7 @@ func (am *DefaultAccountManager) warmupIDPCache() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for accountID, users := range userData {
|
for accountID, users := range userData {
|
||||||
rand.Seed(time.Now().UnixNano())
|
err = am.cacheManager.Set(am.ctx, accountID, users, cacheStore.WithExpiration(cacheEntryExpiration()))
|
||||||
|
|
||||||
r := rand.Intn(int(CacheExpirationMax.Milliseconds()-CacheExpirationMin.Milliseconds())) + int(CacheExpirationMin.Milliseconds())
|
|
||||||
expiration := time.Duration(r) * time.Millisecond
|
|
||||||
err = am.cacheManager.Set(am.ctx, accountID, users, &cacheStore.Options{Expiration: expiration})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -244,93 +278,6 @@ func (am *DefaultAccountManager) warmupIDPCache() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSetupKey generates a new setup key with a given name and type, and adds it to the specified account
|
|
||||||
func (am *DefaultAccountManager) AddSetupKey(
|
|
||||||
accountId string,
|
|
||||||
keyName string,
|
|
||||||
keyType SetupKeyType,
|
|
||||||
expiresIn time.Duration,
|
|
||||||
) (*SetupKey, error) {
|
|
||||||
am.mux.Lock()
|
|
||||||
defer am.mux.Unlock()
|
|
||||||
|
|
||||||
keyDuration := DefaultSetupKeyDuration
|
|
||||||
if expiresIn != 0 {
|
|
||||||
keyDuration = expiresIn
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := am.Store.GetAccount(accountId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.NotFound, "account not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
setupKey := GenerateSetupKey(keyName, keyType, keyDuration)
|
|
||||||
account.SetupKeys[setupKey.Key] = setupKey
|
|
||||||
|
|
||||||
err = am.Store.SaveAccount(account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "failed adding account key")
|
|
||||||
}
|
|
||||||
|
|
||||||
return setupKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RevokeSetupKey marks SetupKey as revoked - becomes not valid anymore
|
|
||||||
func (am *DefaultAccountManager) RevokeSetupKey(accountId string, keyId string) (*SetupKey, error) {
|
|
||||||
am.mux.Lock()
|
|
||||||
defer am.mux.Unlock()
|
|
||||||
|
|
||||||
account, err := am.Store.GetAccount(accountId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.NotFound, "account not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
setupKey := getAccountSetupKeyById(account, keyId)
|
|
||||||
if setupKey == nil {
|
|
||||||
return nil, status.Errorf(codes.NotFound, "unknown setupKey %s", keyId)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyCopy := setupKey.Copy()
|
|
||||||
keyCopy.Revoked = true
|
|
||||||
account.SetupKeys[keyCopy.Key] = keyCopy
|
|
||||||
err = am.Store.SaveAccount(account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "failed adding account key")
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyCopy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenameSetupKey renames existing setup key of the specified account.
|
|
||||||
func (am *DefaultAccountManager) RenameSetupKey(
|
|
||||||
accountId string,
|
|
||||||
keyId string,
|
|
||||||
newName string,
|
|
||||||
) (*SetupKey, error) {
|
|
||||||
am.mux.Lock()
|
|
||||||
defer am.mux.Unlock()
|
|
||||||
|
|
||||||
account, err := am.Store.GetAccount(accountId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.NotFound, "account not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
setupKey := getAccountSetupKeyById(account, keyId)
|
|
||||||
if setupKey == nil {
|
|
||||||
return nil, status.Errorf(codes.NotFound, "unknown setupKey %s", keyId)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyCopy := setupKey.Copy()
|
|
||||||
keyCopy.Name = newName
|
|
||||||
account.SetupKeys[keyCopy.Key] = keyCopy
|
|
||||||
err = am.Store.SaveAccount(account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "failed adding account key")
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyCopy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAccountById returns an existing account using its ID or error (NotFound) if doesn't exist
|
// GetAccountById returns an existing account using its ID or error (NotFound) if doesn't exist
|
||||||
func (am *DefaultAccountManager) GetAccountById(accountId string) (*Account, error) {
|
func (am *DefaultAccountManager) GetAccountById(accountId string) (*Account, error) {
|
||||||
am.mux.Lock()
|
am.mux.Lock()
|
||||||
@@ -356,7 +303,7 @@ func (am *DefaultAccountManager) GetAccountByUserOrAccountId(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.NotFound, "account not found using user id: %s", userId)
|
return nil, status.Errorf(codes.NotFound, "account not found using user id: %s", userId)
|
||||||
}
|
}
|
||||||
err = am.updateIDPMetadata(userId, account.Id)
|
err = am.addAccountIDToIDPAppMeta(userId, account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -370,10 +317,28 @@ func isNil(i idp.Manager) bool {
|
|||||||
return i == nil || reflect.ValueOf(i).IsNil()
|
return i == nil || reflect.ValueOf(i).IsNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateIDPMetadata update user's app metadata in idp manager
|
// addAccountIDToIDPAppMeta update user's app metadata in idp manager
|
||||||
func (am *DefaultAccountManager) updateIDPMetadata(userId, accountID string) error {
|
func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(userID string, account *Account) error {
|
||||||
if !isNil(am.idpManager) {
|
if !isNil(am.idpManager) {
|
||||||
err := am.idpManager.UpdateUserAppMetadata(userId, idp.AppMetadata{WTAccountId: accountID})
|
|
||||||
|
// user can be nil if it wasn't found (e.g., just created)
|
||||||
|
user, err := am.lookupUserInCache(userID, account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user != nil && user.AppMetadata.WTAccountID == account.Id {
|
||||||
|
// it was already set, so we skip the unnecessary update
|
||||||
|
log.Debugf("skipping IDP App Meta update because accountID %s has been already set for user %s",
|
||||||
|
account.Id, userID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.idpManager.UpdateUserAppMetadata(userID, idp.AppMetadata{WTAccountID: account.Id})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.Errorf(
|
return status.Errorf(
|
||||||
codes.Internal,
|
codes.Internal,
|
||||||
@@ -381,39 +346,113 @@ func (am *DefaultAccountManager) updateIDPMetadata(userId, accountID string) err
|
|||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// refresh cache to reflect the update
|
||||||
|
_, err = am.refreshCache(account.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeLocalAndQueryUser(queried idp.UserData, local User) *UserInfo {
|
func (am *DefaultAccountManager) loadAccount(_ context.Context, accountID interface{}) ([]*idp.UserData, error) {
|
||||||
return &UserInfo{
|
log.Debugf("account %s not found in cache, reloading", accountID)
|
||||||
ID: local.Id,
|
|
||||||
Email: queried.Email,
|
|
||||||
Name: queried.Name,
|
|
||||||
Role: string(local.Role),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *DefaultAccountManager) loadFromCache(_ context.Context, accountID interface{}) (interface{}, error) {
|
|
||||||
return am.idpManager.GetAccount(fmt.Sprintf("%v", accountID))
|
return am.idpManager.GetAccount(fmt.Sprintf("%v", accountID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *DefaultAccountManager) lookupCache(accountUsers map[string]*User, accountID string) ([]*idp.UserData, error) {
|
func (am *DefaultAccountManager) lookupUserInCacheByEmail(email string, accountID string) (*idp.UserData, error) {
|
||||||
data, err := am.cacheManager.Get(am.ctx, accountID)
|
data, err := am.getAccountFromCache(accountID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userData := data.([]*idp.UserData)
|
for _, datum := range data {
|
||||||
|
if datum.Email == email {
|
||||||
|
return datum, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupUserInCache looks up user in the IdP cache and returns it. If the user wasn't found, the function returns nil
|
||||||
|
func (am *DefaultAccountManager) lookupUserInCache(userID string, account *Account) (*idp.UserData, error) {
|
||||||
|
users := make(map[string]struct{}, len(account.Users))
|
||||||
|
for _, user := range account.Users {
|
||||||
|
users[user.Id] = struct{}{}
|
||||||
|
}
|
||||||
|
log.Debugf("looking up user %s of account %s in cache", userID, account.Id)
|
||||||
|
userData, err := am.lookupCache(users, account.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, datum := range userData {
|
||||||
|
if datum.ID == userID {
|
||||||
|
return datum, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *DefaultAccountManager) refreshCache(accountID string) ([]*idp.UserData, error) {
|
||||||
|
return am.getAccountFromCache(accountID, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAccountFromCache returns user data for a given account ensuring that cache load happens only once
|
||||||
|
func (am *DefaultAccountManager) getAccountFromCache(accountID string, forceReload bool) ([]*idp.UserData, error) {
|
||||||
|
am.cacheMux.Lock()
|
||||||
|
loadingChan := am.cacheLoading[accountID]
|
||||||
|
if loadingChan == nil {
|
||||||
|
loadingChan = make(chan struct{})
|
||||||
|
am.cacheLoading[accountID] = loadingChan
|
||||||
|
am.cacheMux.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
am.cacheMux.Lock()
|
||||||
|
delete(am.cacheLoading, accountID)
|
||||||
|
close(loadingChan)
|
||||||
|
am.cacheMux.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if forceReload {
|
||||||
|
err := am.cacheManager.Delete(am.ctx, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return am.cacheManager.Get(am.ctx, accountID)
|
||||||
|
}
|
||||||
|
am.cacheMux.Unlock()
|
||||||
|
|
||||||
|
log.Debugf("one request to get account %s is already running", accountID)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-loadingChan:
|
||||||
|
// channel has been closed meaning cache was loaded => simply return from cache
|
||||||
|
return am.cacheManager.Get(am.ctx, accountID)
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
return nil, fmt.Errorf("timeout while waiting for account %s cache to reload", accountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *DefaultAccountManager) lookupCache(accountUsers map[string]struct{}, accountID string) ([]*idp.UserData, error) {
|
||||||
|
data, err := am.getAccountFromCache(accountID, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
userDataMap := make(map[string]struct{})
|
userDataMap := make(map[string]struct{})
|
||||||
for _, datum := range userData {
|
for _, datum := range data {
|
||||||
userDataMap[datum.ID] = struct{}{}
|
userDataMap[datum.ID] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check whether we need to reload the cache
|
// check whether we need to reload the cache
|
||||||
// the accountUsers ID list is the source of truth and all the users should be in the cache
|
// the accountUsers ID list is the source of truth and all the users should be in the cache
|
||||||
reload := len(accountUsers) != len(userData)
|
reload := len(accountUsers) != len(data)
|
||||||
for user := range accountUsers {
|
for user := range accountUsers {
|
||||||
if _, ok := userDataMap[user]; !ok {
|
if _, ok := userDataMap[user]; !ok {
|
||||||
reload = true
|
reload = true
|
||||||
@@ -422,59 +461,13 @@ func (am *DefaultAccountManager) lookupCache(accountUsers map[string]*User, acco
|
|||||||
|
|
||||||
if reload {
|
if reload {
|
||||||
// reload cache once avoiding loops
|
// reload cache once avoiding loops
|
||||||
err := am.cacheManager.Delete(am.ctx, accountID)
|
data, err = am.refreshCache(accountID)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
data, err = am.cacheManager.Get(am.ctx, accountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
userData = data.([]*idp.UserData)
|
|
||||||
}
|
|
||||||
|
|
||||||
return userData, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUsersFromAccount performs a batched request for users from IDP by account id
|
|
||||||
func (am *DefaultAccountManager) GetUsersFromAccount(accountID string) ([]*UserInfo, error) {
|
|
||||||
account, err := am.GetAccountById(accountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
queriedUsers := make([]*idp.UserData, 0)
|
|
||||||
if !isNil(am.idpManager) {
|
|
||||||
queriedUsers, err = am.lookupCache(account.Users, accountID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userInfo := make([]*UserInfo, 0)
|
return data, err
|
||||||
|
|
||||||
// in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo
|
|
||||||
if len(queriedUsers) == 0 {
|
|
||||||
for _, user := range account.Users {
|
|
||||||
userInfo = append(userInfo, &UserInfo{
|
|
||||||
ID: user.Id,
|
|
||||||
Email: "",
|
|
||||||
Name: "",
|
|
||||||
Role: string(user.Role),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return userInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, queriedUser := range queriedUsers {
|
|
||||||
if localUser, contains := account.Users[queriedUser.ID]; contains {
|
|
||||||
userInfo = append(userInfo, mergeLocalAndQueryUser(*queriedUser, *localUser))
|
|
||||||
log.Debugf("Merged userinfo to send back; %v", userInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return userInfo, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateAccountDomainAttributes updates the account domain attributes and then, saves the account
|
// updateAccountDomainAttributes updates the account domain attributes and then, saves the account
|
||||||
@@ -504,7 +497,6 @@ func (am *DefaultAccountManager) updateAccountDomainAttributes(
|
|||||||
|
|
||||||
// handleExistingUserAccount handles existing User accounts and update its domain attributes.
|
// handleExistingUserAccount handles existing User accounts and update its domain attributes.
|
||||||
//
|
//
|
||||||
//
|
|
||||||
// If there is no primary domain account yet, we set the account as primary for the domain. Otherwise,
|
// If there is no primary domain account yet, we set the account as primary for the domain. Otherwise,
|
||||||
// we compare the account's ID with the domain account ID, and if they don't match, we set the account as
|
// we compare the account's ID with the domain account ID, and if they don't match, we set the account as
|
||||||
// non-primary account for the domain. We don't merge accounts at this stage, because of cases when a domain
|
// non-primary account for the domain. We don't merge accounts at this stage, because of cases when a domain
|
||||||
@@ -530,7 +522,7 @@ func (am *DefaultAccountManager) handleExistingUserAccount(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we should register the account ID to this user's metadata in our IDP manager
|
// we should register the account ID to this user's metadata in our IDP manager
|
||||||
err = am.updateIDPMetadata(claims.UserId, existingAcc.Id)
|
err = am.addAccountIDToIDPAppMeta(claims.UserId, existingAcc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -568,7 +560,7 @@ func (am *DefaultAccountManager) handleNewUserAccount(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = am.updateIDPMetadata(claims.UserId, account.Id)
|
err = am.addAccountIDToIDPAppMeta(claims.UserId, account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -576,7 +568,61 @@ func (am *DefaultAccountManager) handleNewUserAccount(
|
|||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccountWithAuthorizationClaims retrievs an account using JWT Claims.
|
// redeemInvite checks whether user has been invited and redeems the invite
|
||||||
|
func (am *DefaultAccountManager) redeemInvite(account *Account, userID string) error {
|
||||||
|
// only possible with the enabled IdP manager
|
||||||
|
if am.idpManager == nil {
|
||||||
|
log.Warnf("invites only work with enabled IdP manager")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := am.lookupUserInCache(userID, account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
return status.Errorf(codes.NotFound, "user %s not found in the IdP", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.AppMetadata.WTPendingInvite {
|
||||||
|
log.Infof("redeeming invite for user %s account %s", userID, account.Id)
|
||||||
|
// User has already logged in, meaning that IdP should have set wt_pending_invite to false.
|
||||||
|
// Our job is to just reload cache.
|
||||||
|
go func() {
|
||||||
|
// set AppMetadata setting WTPendingInvite = false to indicate that it was redeemed
|
||||||
|
// it shouldn't be necessary since the IdP should redeem on login,
|
||||||
|
// but we make sure we do it if IdP is not capable of resetting teh flag.
|
||||||
|
_ = am.idpManager.UpdateUserAppMetadata(userID, idp.AppMetadata{WTAccountID: account.Id, WTPendingInvite: false}) //nolint
|
||||||
|
|
||||||
|
_, err = am.refreshCache(account.Id)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed reloading cache when redeeming user %s under account %s", userID, account.Id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debugf("user %s of account %s redeemed invite", user.ID, account.Id)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccountFromToken returns an account associated with this token
|
||||||
|
func (am *DefaultAccountManager) GetAccountFromToken(claims jwtclaims.AuthorizationClaims) (*Account, error) {
|
||||||
|
account, err := am.getAccountWithAuthorizationClaims(claims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.redeemInvite(account, claims.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAccountWithAuthorizationClaims retrievs an account using JWT Claims.
|
||||||
// if domain is of the PrivateCategory category, it will evaluate
|
// if domain is of the PrivateCategory category, it will evaluate
|
||||||
// if account is new, existing or if there is another account with the same domain
|
// if account is new, existing or if there is another account with the same domain
|
||||||
//
|
//
|
||||||
@@ -593,12 +639,12 @@ func (am *DefaultAccountManager) handleNewUserAccount(
|
|||||||
// Existing user + Existing account + Existing Indexed Domain -> Nothing changes
|
// Existing user + Existing account + Existing Indexed Domain -> Nothing changes
|
||||||
//
|
//
|
||||||
// Existing user + Existing account + Existing domain reclassified Domain as private -> Nothing changes (index domain)
|
// Existing user + Existing account + Existing domain reclassified Domain as private -> Nothing changes (index domain)
|
||||||
func (am *DefaultAccountManager) GetAccountWithAuthorizationClaims(
|
func (am *DefaultAccountManager) getAccountWithAuthorizationClaims(
|
||||||
claims jwtclaims.AuthorizationClaims,
|
claims jwtclaims.AuthorizationClaims,
|
||||||
) (*Account, error) {
|
) (*Account, error) {
|
||||||
// if Account ID is part of the claims
|
// if Account ID is part of the claims
|
||||||
// it means that we've already classified the domain and user has an account
|
// it means that we've already classified the domain and user has an account
|
||||||
if claims.DomainCategory != PrivateCategory {
|
if claims.DomainCategory != PrivateCategory || !isDomainValid(claims.Domain) {
|
||||||
return am.GetAccountByUserOrAccountId(claims.UserId, claims.AccountId, claims.Domain)
|
return am.GetAccountByUserOrAccountId(claims.UserId, claims.AccountId, claims.Domain)
|
||||||
} else if claims.AccountId != "" {
|
} else if claims.AccountId != "" {
|
||||||
accountFromID, err := am.GetAccountById(claims.AccountId)
|
accountFromID, err := am.GetAccountById(claims.AccountId)
|
||||||
@@ -638,6 +684,11 @@ func (am *DefaultAccountManager) GetAccountWithAuthorizationClaims(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isDomainValid(domain string) bool {
|
||||||
|
re := regexp.MustCompile(`^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`)
|
||||||
|
return re.Match([]byte(domain))
|
||||||
|
}
|
||||||
|
|
||||||
// AccountExists checks whether account exists (returns true) or not (returns false)
|
// AccountExists checks whether account exists (returns true) or not (returns false)
|
||||||
func (am *DefaultAccountManager) AccountExists(accountId string) (*bool, error) {
|
func (am *DefaultAccountManager) AccountExists(accountId string) (*bool, error) {
|
||||||
am.mux.Lock()
|
am.mux.Lock()
|
||||||
@@ -688,40 +739,33 @@ func newAccountWithId(accountId, userId, domain string) *Account {
|
|||||||
|
|
||||||
setupKeys := make(map[string]*SetupKey)
|
setupKeys := make(map[string]*SetupKey)
|
||||||
defaultKey := GenerateDefaultSetupKey()
|
defaultKey := GenerateDefaultSetupKey()
|
||||||
oneOffKey := GenerateSetupKey("One-off key", SetupKeyOneOff, DefaultSetupKeyDuration)
|
oneOffKey := GenerateSetupKey("One-off key", SetupKeyOneOff, DefaultSetupKeyDuration, []string{})
|
||||||
setupKeys[defaultKey.Key] = defaultKey
|
setupKeys[defaultKey.Key] = defaultKey
|
||||||
setupKeys[oneOffKey.Key] = oneOffKey
|
setupKeys[oneOffKey.Key] = oneOffKey
|
||||||
network := NewNetwork()
|
network := NewNetwork()
|
||||||
peers := make(map[string]*Peer)
|
peers := make(map[string]*Peer)
|
||||||
users := make(map[string]*User)
|
users := make(map[string]*User)
|
||||||
routes := make(map[string]*route.Route)
|
routes := make(map[string]*route.Route)
|
||||||
|
nameServersGroups := make(map[string]*nbdns.NameServerGroup)
|
||||||
users[userId] = NewAdminUser(userId)
|
users[userId] = NewAdminUser(userId)
|
||||||
log.Debugf("created new account %s with setup key %s", accountId, defaultKey.Key)
|
log.Debugf("created new account %s with setup key %s", accountId, defaultKey.Key)
|
||||||
|
|
||||||
acc := &Account{
|
acc := &Account{
|
||||||
Id: accountId,
|
Id: accountId,
|
||||||
SetupKeys: setupKeys,
|
SetupKeys: setupKeys,
|
||||||
Network: network,
|
Network: network,
|
||||||
Peers: peers,
|
Peers: peers,
|
||||||
Users: users,
|
Users: users,
|
||||||
CreatedBy: userId,
|
CreatedBy: userId,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Routes: routes,
|
Routes: routes,
|
||||||
|
NameServerGroups: nameServersGroups,
|
||||||
}
|
}
|
||||||
|
|
||||||
addAllGroup(acc)
|
addAllGroup(acc)
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAccountSetupKeyById(acc *Account, keyId string) *SetupKey {
|
|
||||||
for _, k := range acc.SetupKeys {
|
|
||||||
if keyId == k.Id {
|
|
||||||
return k
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAccountSetupKeyByKey(acc *Account, key string) *SetupKey {
|
func getAccountSetupKeyByKey(acc *Account, key string) *SetupKey {
|
||||||
for _, k := range acc.SetupKeys {
|
for _, k := range acc.SetupKeys {
|
||||||
if key == k.Key {
|
if key == k.Key {
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ func TestAccountManager_GetOrCreateAccountByUser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
func TestDefaultAccountManager_GetAccountFromToken(t *testing.T) {
|
||||||
type initUserParams jwtclaims.AuthorizationClaims
|
type initUserParams jwtclaims.AuthorizationClaims
|
||||||
|
|
||||||
type test struct {
|
type test struct {
|
||||||
@@ -140,6 +140,7 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
|||||||
expectedMSG string
|
expectedMSG string
|
||||||
expectedUserRole UserRole
|
expectedUserRole UserRole
|
||||||
expectedDomainCategory string
|
expectedDomainCategory string
|
||||||
|
expectedDomain string
|
||||||
expectedPrimaryDomainStatus bool
|
expectedPrimaryDomainStatus bool
|
||||||
expectedCreatedBy string
|
expectedCreatedBy string
|
||||||
expectedUsers []string
|
expectedUsers []string
|
||||||
@@ -168,6 +169,7 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
|||||||
expectedMSG: "account IDs shouldn't match",
|
expectedMSG: "account IDs shouldn't match",
|
||||||
expectedUserRole: UserRoleAdmin,
|
expectedUserRole: UserRoleAdmin,
|
||||||
expectedDomainCategory: "",
|
expectedDomainCategory: "",
|
||||||
|
expectedDomain: publicDomain,
|
||||||
expectedPrimaryDomainStatus: false,
|
expectedPrimaryDomainStatus: false,
|
||||||
expectedCreatedBy: "pub-domain-user",
|
expectedCreatedBy: "pub-domain-user",
|
||||||
expectedUsers: []string{"pub-domain-user"},
|
expectedUsers: []string{"pub-domain-user"},
|
||||||
@@ -188,6 +190,7 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
|||||||
testingFunc: require.NotEqual,
|
testingFunc: require.NotEqual,
|
||||||
expectedMSG: "account IDs shouldn't match",
|
expectedMSG: "account IDs shouldn't match",
|
||||||
expectedUserRole: UserRoleAdmin,
|
expectedUserRole: UserRoleAdmin,
|
||||||
|
expectedDomain: unknownDomain,
|
||||||
expectedDomainCategory: "",
|
expectedDomainCategory: "",
|
||||||
expectedPrimaryDomainStatus: false,
|
expectedPrimaryDomainStatus: false,
|
||||||
expectedCreatedBy: "unknown-domain-user",
|
expectedCreatedBy: "unknown-domain-user",
|
||||||
@@ -205,6 +208,7 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
|||||||
testingFunc: require.NotEqual,
|
testingFunc: require.NotEqual,
|
||||||
expectedMSG: "account IDs shouldn't match",
|
expectedMSG: "account IDs shouldn't match",
|
||||||
expectedUserRole: UserRoleAdmin,
|
expectedUserRole: UserRoleAdmin,
|
||||||
|
expectedDomain: privateDomain,
|
||||||
expectedDomainCategory: PrivateCategory,
|
expectedDomainCategory: PrivateCategory,
|
||||||
expectedPrimaryDomainStatus: true,
|
expectedPrimaryDomainStatus: true,
|
||||||
expectedCreatedBy: "pvt-domain-user",
|
expectedCreatedBy: "pvt-domain-user",
|
||||||
@@ -227,6 +231,7 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
|||||||
testingFunc: require.Equal,
|
testingFunc: require.Equal,
|
||||||
expectedMSG: "account IDs should match",
|
expectedMSG: "account IDs should match",
|
||||||
expectedUserRole: UserRoleUser,
|
expectedUserRole: UserRoleUser,
|
||||||
|
expectedDomain: privateDomain,
|
||||||
expectedDomainCategory: PrivateCategory,
|
expectedDomainCategory: PrivateCategory,
|
||||||
expectedPrimaryDomainStatus: true,
|
expectedPrimaryDomainStatus: true,
|
||||||
expectedCreatedBy: defaultInitAccount.UserId,
|
expectedCreatedBy: defaultInitAccount.UserId,
|
||||||
@@ -244,6 +249,7 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
|||||||
testingFunc: require.Equal,
|
testingFunc: require.Equal,
|
||||||
expectedMSG: "account IDs should match",
|
expectedMSG: "account IDs should match",
|
||||||
expectedUserRole: UserRoleAdmin,
|
expectedUserRole: UserRoleAdmin,
|
||||||
|
expectedDomain: defaultInitAccount.Domain,
|
||||||
expectedDomainCategory: PrivateCategory,
|
expectedDomainCategory: PrivateCategory,
|
||||||
expectedPrimaryDomainStatus: true,
|
expectedPrimaryDomainStatus: true,
|
||||||
expectedCreatedBy: defaultInitAccount.UserId,
|
expectedCreatedBy: defaultInitAccount.UserId,
|
||||||
@@ -262,12 +268,32 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
|||||||
testingFunc: require.Equal,
|
testingFunc: require.Equal,
|
||||||
expectedMSG: "account IDs should match",
|
expectedMSG: "account IDs should match",
|
||||||
expectedUserRole: UserRoleAdmin,
|
expectedUserRole: UserRoleAdmin,
|
||||||
|
expectedDomain: defaultInitAccount.Domain,
|
||||||
expectedDomainCategory: PrivateCategory,
|
expectedDomainCategory: PrivateCategory,
|
||||||
expectedPrimaryDomainStatus: true,
|
expectedPrimaryDomainStatus: true,
|
||||||
expectedCreatedBy: defaultInitAccount.UserId,
|
expectedCreatedBy: defaultInitAccount.UserId,
|
||||||
expectedUsers: []string{defaultInitAccount.UserId},
|
expectedUsers: []string{defaultInitAccount.UserId},
|
||||||
}
|
}
|
||||||
for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5, testCase6} {
|
|
||||||
|
testCase7 := test{
|
||||||
|
name: "User With Private Category And Empty Domain",
|
||||||
|
inputClaims: jwtclaims.AuthorizationClaims{
|
||||||
|
Domain: "",
|
||||||
|
UserId: "pvt-domain-user",
|
||||||
|
DomainCategory: PrivateCategory,
|
||||||
|
},
|
||||||
|
inputInitUserParams: defaultInitAccount,
|
||||||
|
testingFunc: require.NotEqual,
|
||||||
|
expectedMSG: "account IDs shouldn't match",
|
||||||
|
expectedUserRole: UserRoleAdmin,
|
||||||
|
expectedDomain: "",
|
||||||
|
expectedDomainCategory: "",
|
||||||
|
expectedPrimaryDomainStatus: false,
|
||||||
|
expectedCreatedBy: "pvt-domain-user",
|
||||||
|
expectedUsers: []string{"pvt-domain-user"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5, testCase6, testCase7} {
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
manager, err := createManager(t)
|
manager, err := createManager(t)
|
||||||
require.NoError(t, err, "unable to create account manager")
|
require.NoError(t, err, "unable to create account manager")
|
||||||
@@ -284,7 +310,7 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
|||||||
testCase.inputClaims.AccountId = initAccount.Id
|
testCase.inputClaims.AccountId = initAccount.Id
|
||||||
}
|
}
|
||||||
|
|
||||||
account, err := manager.GetAccountWithAuthorizationClaims(testCase.inputClaims)
|
account, err := manager.GetAccountFromToken(testCase.inputClaims)
|
||||||
require.NoError(t, err, "support function failed")
|
require.NoError(t, err, "support function failed")
|
||||||
verifyNewAccountHasDefaultFields(t, account, testCase.expectedCreatedBy, testCase.inputClaims.Domain, testCase.expectedUsers)
|
verifyNewAccountHasDefaultFields(t, account, testCase.expectedCreatedBy, testCase.inputClaims.Domain, testCase.expectedUsers)
|
||||||
verifyCanAddPeerToAccount(t, manager, account, testCase.expectedCreatedBy)
|
verifyCanAddPeerToAccount(t, manager, account, testCase.expectedCreatedBy)
|
||||||
@@ -294,6 +320,7 @@ func TestDefaultAccountManager_GetAccountWithAuthorizationClaims(t *testing.T) {
|
|||||||
require.EqualValues(t, testCase.expectedUserRole, account.Users[testCase.inputClaims.UserId].Role, "expected user role should match")
|
require.EqualValues(t, testCase.expectedUserRole, account.Users[testCase.inputClaims.UserId].Role, "expected user role should match")
|
||||||
require.EqualValues(t, testCase.expectedDomainCategory, account.DomainCategory, "expected account domain category should match")
|
require.EqualValues(t, testCase.expectedDomainCategory, account.DomainCategory, "expected account domain category should match")
|
||||||
require.EqualValues(t, testCase.expectedPrimaryDomainStatus, account.IsDomainPrimaryAccount, "expected account primary status should match")
|
require.EqualValues(t, testCase.expectedPrimaryDomainStatus, account.IsDomainPrimaryAccount, "expected account primary status should match")
|
||||||
|
require.EqualValues(t, testCase.expectedDomain, account.Domain, "expected account domain should match")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
management/server/error.go
Normal file
52
management/server/error.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UserAlreadyExists indicates that user already exists
|
||||||
|
UserAlreadyExists ErrorType = 1
|
||||||
|
// AccountNotFound indicates that specified account hasn't been found
|
||||||
|
AccountNotFound ErrorType = iota
|
||||||
|
// PreconditionFailed indicates that some pre-condition for the operation hasn't been fulfilled
|
||||||
|
PreconditionFailed ErrorType = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorType is a type of the Error
|
||||||
|
type ErrorType int32
|
||||||
|
|
||||||
|
// Error is an internal error
|
||||||
|
type Error struct {
|
||||||
|
errorType ErrorType
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the Type of the error
|
||||||
|
func (e *Error) Type() ErrorType {
|
||||||
|
return e.errorType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error is an error string
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf returns Error(errorType, fmt.Sprintf(format, a...)).
|
||||||
|
func Errorf(errorType ErrorType, format string, a ...interface{}) error {
|
||||||
|
return &Error{
|
||||||
|
errorType: errorType,
|
||||||
|
message: fmt.Sprintf(format, a...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromError returns Error, true if the provided error is of type of Error. nil, false otherwise
|
||||||
|
func FromError(err error) (s *Error, ok bool) {
|
||||||
|
if err == nil {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
if e, ok := err.(*Error); ok {
|
||||||
|
return e, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ type FileStore struct {
|
|||||||
PeerKeyId2DstRulesId map[string]map[string]struct{} `json:"-"`
|
PeerKeyId2DstRulesId map[string]map[string]struct{} `json:"-"`
|
||||||
PeerKeyID2RouteIDs map[string]map[string]struct{} `json:"-"`
|
PeerKeyID2RouteIDs map[string]map[string]struct{} `json:"-"`
|
||||||
AccountPrefix2RouteIDs map[string]map[string][]string `json:"-"`
|
AccountPrefix2RouteIDs map[string]map[string][]string `json:"-"`
|
||||||
|
InstallationID string
|
||||||
|
|
||||||
// mutex to synchronise Store read/write operations
|
// mutex to synchronise Store read/write operations
|
||||||
mux sync.Mutex `json:"-"`
|
mux sync.Mutex `json:"-"`
|
||||||
@@ -415,8 +416,10 @@ func (s *FileStore) GetAccountPeers(accountId string) ([]*Peer, error) {
|
|||||||
|
|
||||||
// GetAllAccounts returns all accounts
|
// GetAllAccounts returns all accounts
|
||||||
func (s *FileStore) GetAllAccounts() (all []*Account) {
|
func (s *FileStore) GetAllAccounts() (all []*Account) {
|
||||||
|
s.mux.Lock()
|
||||||
|
defer s.mux.Unlock()
|
||||||
for _, a := range s.Accounts {
|
for _, a := range s.Accounts {
|
||||||
all = append(all, a)
|
all = append(all, a.Copy())
|
||||||
}
|
}
|
||||||
|
|
||||||
return all
|
return all
|
||||||
@@ -566,3 +569,18 @@ func (s *FileStore) GetRoutesByPrefix(accountID string, prefix netip.Prefix) ([]
|
|||||||
|
|
||||||
return routes, nil
|
return routes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInstallationID returns the installation ID from the store
|
||||||
|
func (s *FileStore) GetInstallationID() string {
|
||||||
|
return s.InstallationID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveInstallationID saves the installation ID
|
||||||
|
func (s *FileStore) SaveInstallationID(id string) error {
|
||||||
|
s.mux.Lock()
|
||||||
|
defer s.mux.Unlock()
|
||||||
|
|
||||||
|
s.InstallationID = id
|
||||||
|
|
||||||
|
return s.persist(s.storeFile)
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
|
gRPCPeer "google.golang.org/grpc/peer"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,7 +80,10 @@ func (s *GRPCServer) GetServerKey(ctx context.Context, req *proto.Empty) (*proto
|
|||||||
// Sync validates the existence of a connecting peer, sends an initial state (all available for the connecting peers) and
|
// Sync validates the existence of a connecting peer, sends an initial state (all available for the connecting peers) and
|
||||||
// notifies the connected peer of any updates (e.g. new peers under the same account)
|
// notifies the connected peer of any updates (e.g. new peers under the same account)
|
||||||
func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_SyncServer) error {
|
func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_SyncServer) error {
|
||||||
log.Debugf("Sync request from peer %s", req.WgPubKey)
|
p, ok := gRPCPeer.FromContext(srv.Context())
|
||||||
|
if ok {
|
||||||
|
log.Debugf("Sync request from peer [%s] [%s]", req.WgPubKey, p.Addr.String())
|
||||||
|
}
|
||||||
|
|
||||||
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
|
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -177,7 +181,7 @@ func (s *GRPCServer) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest)
|
|||||||
return nil, status.Errorf(codes.Internal, "invalid jwt token, err: %v", err)
|
return nil, status.Errorf(codes.Internal, "invalid jwt token, err: %v", err)
|
||||||
}
|
}
|
||||||
claims := jwtclaims.ExtractClaimsWithToken(token, s.config.HttpConfig.AuthAudience)
|
claims := jwtclaims.ExtractClaimsWithToken(token, s.config.HttpConfig.AuthAudience)
|
||||||
_, err = s.accountManager.GetAccountWithAuthorizationClaims(claims)
|
_, err = s.accountManager.GetAccountFromToken(claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "unable to fetch account with claims, err: %v", err)
|
return nil, status.Errorf(codes.Internal, "unable to fetch account with claims, err: %v", err)
|
||||||
}
|
}
|
||||||
@@ -255,7 +259,10 @@ func (s *GRPCServer) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest)
|
|||||||
// In case it isn't, the endpoint checks whether setup key is provided within the request and tries to register a peer.
|
// In case it isn't, the endpoint checks whether setup key is provided within the request and tries to register a peer.
|
||||||
// In case of the successful registration login is also successful
|
// In case of the successful registration login is also successful
|
||||||
func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
|
func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
|
||||||
log.Debugf("Login request from peer %s", req.WgPubKey)
|
p, ok := gRPCPeer.FromContext(ctx)
|
||||||
|
if ok {
|
||||||
|
log.Debugf("Login request from peer [%s] [%s]", req.WgPubKey, p.Addr.String())
|
||||||
|
}
|
||||||
|
|
||||||
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
|
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ generate:
|
|||||||
models: true
|
models: true
|
||||||
embedded-spec: false
|
embedded-spec: false
|
||||||
output: types.gen.go
|
output: types.gen.go
|
||||||
|
compatibility:
|
||||||
|
always-prefix-enum-values: true
|
||||||
@@ -11,6 +11,6 @@ fi
|
|||||||
old_pwd=$(pwd)
|
old_pwd=$(pwd)
|
||||||
script_path=$(dirname $(realpath "$0"))
|
script_path=$(dirname $(realpath "$0"))
|
||||||
cd "$script_path"
|
cd "$script_path"
|
||||||
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.11.0
|
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@4a1477f6a8ba6ca8115cc23bb2fb67f0b9fca18e
|
||||||
oapi-codegen --config cfg.yaml openapi.yml
|
oapi-codegen --config cfg.yaml openapi.yml
|
||||||
cd "$old_pwd"
|
cd "$old_pwd"
|
||||||
@@ -16,6 +16,8 @@ tags:
|
|||||||
description: Interact with and view information about rules.
|
description: Interact with and view information about rules.
|
||||||
- name: Routes
|
- name: Routes
|
||||||
description: Interact with and view information about routes.
|
description: Interact with and view information about routes.
|
||||||
|
- name: DNS
|
||||||
|
description: Interact with and view information about DNS configuration.
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
User:
|
User:
|
||||||
@@ -31,13 +33,59 @@ components:
|
|||||||
description: User's name from idp provider
|
description: User's name from idp provider
|
||||||
type: string
|
type: string
|
||||||
role:
|
role:
|
||||||
description: User's Netbird account role
|
description: User's NetBird account role
|
||||||
type: string
|
type: string
|
||||||
|
status:
|
||||||
|
description: User's status
|
||||||
|
type: string
|
||||||
|
enum: [ "active","invited","disabled" ]
|
||||||
|
auto_groups:
|
||||||
|
description: Groups to auto-assign to peers registered by this user
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- email
|
- email
|
||||||
- name
|
- name
|
||||||
- role
|
- role
|
||||||
|
- auto_groups
|
||||||
|
- status
|
||||||
|
UserRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
role:
|
||||||
|
description: User's NetBird account role
|
||||||
|
type: string
|
||||||
|
auto_groups:
|
||||||
|
description: Groups to auto-assign to peers registered by this user
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- role
|
||||||
|
- auto_groups
|
||||||
|
UserCreateRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
role:
|
||||||
|
description: User's NetBird account role
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
description: User's Email to send invite to
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: User's full name
|
||||||
|
type: string
|
||||||
|
auto_groups:
|
||||||
|
description: Groups to auto-assign to peers registered by this user
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- role
|
||||||
|
- auto_groups
|
||||||
|
- email
|
||||||
PeerMinimum:
|
PeerMinimum:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -76,20 +124,18 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/GroupMinimum'
|
$ref: '#/components/schemas/GroupMinimum'
|
||||||
activated_by:
|
|
||||||
description: Provides information of who activated the Peer. User or Setup Key
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
type:
|
|
||||||
type: string
|
|
||||||
value:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- type
|
|
||||||
- value
|
|
||||||
ssh_enabled:
|
ssh_enabled:
|
||||||
description: Indicates whether SSH server is enabled on this peer
|
description: Indicates whether SSH server is enabled on this peer
|
||||||
type: boolean
|
type: boolean
|
||||||
|
user_id:
|
||||||
|
description: User ID of the user that enrolled this peer
|
||||||
|
type: string
|
||||||
|
hostname:
|
||||||
|
description: Hostname of the machine
|
||||||
|
type: string
|
||||||
|
ui_version:
|
||||||
|
description: Peer's desktop UI version
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- ip
|
- ip
|
||||||
- connected
|
- connected
|
||||||
@@ -97,8 +143,8 @@ components:
|
|||||||
- os
|
- os
|
||||||
- version
|
- version
|
||||||
- groups
|
- groups
|
||||||
- activated_by
|
|
||||||
- ssh_enabled
|
- ssh_enabled
|
||||||
|
- hostname
|
||||||
SetupKey:
|
SetupKey:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -134,6 +180,15 @@ components:
|
|||||||
state:
|
state:
|
||||||
description: Setup key status, "valid", "overused","expired" or "revoked"
|
description: Setup key status, "valid", "overused","expired" or "revoked"
|
||||||
type: string
|
type: string
|
||||||
|
auto_groups:
|
||||||
|
description: Setup key groups to auto-assign to peers registered with this key
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
description: Setup key last update date
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- key
|
- key
|
||||||
@@ -145,6 +200,8 @@ components:
|
|||||||
- used_times
|
- used_times
|
||||||
- last_used
|
- last_used
|
||||||
- state
|
- state
|
||||||
|
- auto_groups
|
||||||
|
- updated_at
|
||||||
SetupKeyRequest:
|
SetupKeyRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -160,11 +217,17 @@ components:
|
|||||||
revoked:
|
revoked:
|
||||||
description: Setup key revocation status
|
description: Setup key revocation status
|
||||||
type: boolean
|
type: boolean
|
||||||
|
auto_groups:
|
||||||
|
description: Setup key groups to auto-assign to peers registered with this key
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- type
|
- type
|
||||||
- expires_in
|
- expires_in
|
||||||
- revoked
|
- revoked
|
||||||
|
- auto_groups
|
||||||
GroupMinimum:
|
GroupMinimum:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -338,6 +401,76 @@ components:
|
|||||||
enum: [ "network","network_id","description","enabled","peer","metric","masquerade" ]
|
enum: [ "network","network_id","description","enabled","peer","metric","masquerade" ]
|
||||||
required:
|
required:
|
||||||
- path
|
- path
|
||||||
|
Nameserver:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ip:
|
||||||
|
description: Nameserver IP
|
||||||
|
type: string
|
||||||
|
ns_type:
|
||||||
|
description: Nameserver Type
|
||||||
|
type: string
|
||||||
|
enum: ["udp"]
|
||||||
|
port:
|
||||||
|
description: Nameserver Port
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- ip
|
||||||
|
- ns_type
|
||||||
|
- port
|
||||||
|
NameserverGroupRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: Nameserver group name
|
||||||
|
type: string
|
||||||
|
maxLength: 40
|
||||||
|
minLength: 1
|
||||||
|
description:
|
||||||
|
description: Nameserver group description
|
||||||
|
type: string
|
||||||
|
nameservers:
|
||||||
|
description: Nameserver group
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 2
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Nameserver'
|
||||||
|
enabled:
|
||||||
|
description: Nameserver group status
|
||||||
|
type: boolean
|
||||||
|
groups:
|
||||||
|
description: Nameserver group tag groups
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- description
|
||||||
|
- nameservers
|
||||||
|
- enabled
|
||||||
|
- groups
|
||||||
|
NameserverGroup:
|
||||||
|
allOf:
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
description: Nameserver group ID
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- $ref: '#/components/schemas/NameserverGroupRequest'
|
||||||
|
NameserverGroupPatchOperation:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/PatchMinimum'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
path:
|
||||||
|
description: Nameserver group field to update in form /<field>
|
||||||
|
type: string
|
||||||
|
enum: [ "name","description","enabled","groups","nameservers" ]
|
||||||
|
required:
|
||||||
|
- path
|
||||||
|
|
||||||
responses:
|
responses:
|
||||||
not_found:
|
not_found:
|
||||||
@@ -392,6 +525,67 @@ paths:
|
|||||||
"$ref": "#/components/responses/forbidden"
|
"$ref": "#/components/responses/forbidden"
|
||||||
'500':
|
'500':
|
||||||
"$ref": "#/components/responses/internal_error"
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
/api/users/:
|
||||||
|
post:
|
||||||
|
summary: Create a User (invite)
|
||||||
|
tags: [ Users]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
requestBody:
|
||||||
|
description: User invite information
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserCreateRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A User object
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
/api/users/{id}:
|
||||||
|
put:
|
||||||
|
summary: Update information about a User
|
||||||
|
tags: [ Users]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The User ID
|
||||||
|
requestBody:
|
||||||
|
description: User update
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A User object
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
/api/peers:
|
/api/peers:
|
||||||
get:
|
get:
|
||||||
summary: Returns a list of all peers
|
summary: Returns a list of all peers
|
||||||
@@ -1169,6 +1363,176 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
description: The Route ID
|
description: The Route ID
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Delete status code
|
||||||
|
content: { }
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
/api/dns/nameservers:
|
||||||
|
get:
|
||||||
|
summary: Returns a list of all Nameserver Groups
|
||||||
|
tags: [ DNS ]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A JSON Array of Nameserver Groups
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/NameserverGroup'
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
post:
|
||||||
|
summary: Creates a Nameserver Group
|
||||||
|
tags: [ DNS ]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
requestBody:
|
||||||
|
description: New Nameserver Groups request
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/NameserverGroupRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A Nameserver Groups Object
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/NameserverGroup'
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
|
||||||
|
/api/dns/nameservers/{id}:
|
||||||
|
get:
|
||||||
|
summary: Get information about a Nameserver Groups
|
||||||
|
tags: [ DNS ]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The Nameserver Group ID
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A Nameserver Group object
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/NameserverGroup'
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
put:
|
||||||
|
summary: Update/Replace a Nameserver Group
|
||||||
|
tags: [ DNS ]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The Nameserver Group ID
|
||||||
|
requestBody:
|
||||||
|
description: Update Nameserver Group request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/NameserverGroupRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A Nameserver Group object
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/NameserverGroup'
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
patch:
|
||||||
|
summary: Update information about a Nameserver Group
|
||||||
|
tags: [ DNS ]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The Nameserver Group ID
|
||||||
|
requestBody:
|
||||||
|
description: Update Nameserver Group request using a list of json patch objects
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/NameserverGroupPatchOperation'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A Nameserver Group object
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/NameserverGroup'
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
delete:
|
||||||
|
summary: Delete a Nameserver Group
|
||||||
|
tags: [ DNS ]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The Nameserver Group ID
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Delete status code
|
description: Delete status code
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Package api provides primitives to interact with the openapi HTTP API.
|
// Package api provides primitives to interact with the openapi HTTP API.
|
||||||
//
|
//
|
||||||
// Code generated by github.com/deepmap/oapi-codegen version v1.11.0 DO NOT EDIT.
|
// Code generated by github.com/deepmap/oapi-codegen version v1.11.1-0.20220912230023-4a1477f6a8ba DO NOT EDIT.
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -24,6 +24,27 @@ const (
|
|||||||
GroupPatchOperationPathPeers GroupPatchOperationPath = "peers"
|
GroupPatchOperationPathPeers GroupPatchOperationPath = "peers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Defines values for NameserverNsType.
|
||||||
|
const (
|
||||||
|
NameserverNsTypeUdp NameserverNsType = "udp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defines values for NameserverGroupPatchOperationOp.
|
||||||
|
const (
|
||||||
|
NameserverGroupPatchOperationOpAdd NameserverGroupPatchOperationOp = "add"
|
||||||
|
NameserverGroupPatchOperationOpRemove NameserverGroupPatchOperationOp = "remove"
|
||||||
|
NameserverGroupPatchOperationOpReplace NameserverGroupPatchOperationOp = "replace"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defines values for NameserverGroupPatchOperationPath.
|
||||||
|
const (
|
||||||
|
NameserverGroupPatchOperationPathDescription NameserverGroupPatchOperationPath = "description"
|
||||||
|
NameserverGroupPatchOperationPathEnabled NameserverGroupPatchOperationPath = "enabled"
|
||||||
|
NameserverGroupPatchOperationPathGroups NameserverGroupPatchOperationPath = "groups"
|
||||||
|
NameserverGroupPatchOperationPathName NameserverGroupPatchOperationPath = "name"
|
||||||
|
NameserverGroupPatchOperationPathNameservers NameserverGroupPatchOperationPath = "nameservers"
|
||||||
|
)
|
||||||
|
|
||||||
// Defines values for PatchMinimumOp.
|
// Defines values for PatchMinimumOp.
|
||||||
const (
|
const (
|
||||||
PatchMinimumOpAdd PatchMinimumOp = "add"
|
PatchMinimumOpAdd PatchMinimumOp = "add"
|
||||||
@@ -66,300 +87,427 @@ const (
|
|||||||
RulePatchOperationPathSources RulePatchOperationPath = "sources"
|
RulePatchOperationPathSources RulePatchOperationPath = "sources"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Defines values for UserStatus.
|
||||||
|
const (
|
||||||
|
UserStatusActive UserStatus = "active"
|
||||||
|
UserStatusDisabled UserStatus = "disabled"
|
||||||
|
UserStatusInvited UserStatus = "invited"
|
||||||
|
)
|
||||||
|
|
||||||
// Group defines model for Group.
|
// Group defines model for Group.
|
||||||
type Group struct {
|
type Group struct {
|
||||||
// Group ID
|
// Id Group ID
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
|
||||||
// Group Name identifier
|
// Name Group Name identifier
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
// List of peers object
|
// Peers List of peers object
|
||||||
Peers []PeerMinimum `json:"peers"`
|
Peers []PeerMinimum `json:"peers"`
|
||||||
|
|
||||||
// Count of peers associated to the group
|
// PeersCount Count of peers associated to the group
|
||||||
PeersCount int `json:"peers_count"`
|
PeersCount int `json:"peers_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupMinimum defines model for GroupMinimum.
|
// GroupMinimum defines model for GroupMinimum.
|
||||||
type GroupMinimum struct {
|
type GroupMinimum struct {
|
||||||
// Group ID
|
// Id Group ID
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
|
||||||
// Group Name identifier
|
// Name Group Name identifier
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
// Count of peers associated to the group
|
// PeersCount Count of peers associated to the group
|
||||||
PeersCount int `json:"peers_count"`
|
PeersCount int `json:"peers_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupPatchOperation defines model for GroupPatchOperation.
|
// GroupPatchOperation defines model for GroupPatchOperation.
|
||||||
type GroupPatchOperation struct {
|
type GroupPatchOperation struct {
|
||||||
// Patch operation type
|
// Op Patch operation type
|
||||||
Op GroupPatchOperationOp `json:"op"`
|
Op GroupPatchOperationOp `json:"op"`
|
||||||
|
|
||||||
// Group field to update in form /<field>
|
// Path Group field to update in form /<field>
|
||||||
Path GroupPatchOperationPath `json:"path"`
|
Path GroupPatchOperationPath `json:"path"`
|
||||||
|
|
||||||
// Values to be applied
|
// Value Values to be applied
|
||||||
Value []string `json:"value"`
|
Value []string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch operation type
|
// GroupPatchOperationOp Patch operation type
|
||||||
type GroupPatchOperationOp string
|
type GroupPatchOperationOp string
|
||||||
|
|
||||||
// Group field to update in form /<field>
|
// GroupPatchOperationPath Group field to update in form /<field>
|
||||||
type GroupPatchOperationPath string
|
type GroupPatchOperationPath string
|
||||||
|
|
||||||
|
// Nameserver defines model for Nameserver.
|
||||||
|
type Nameserver struct {
|
||||||
|
// Ip Nameserver IP
|
||||||
|
Ip string `json:"ip"`
|
||||||
|
|
||||||
|
// NsType Nameserver Type
|
||||||
|
NsType NameserverNsType `json:"ns_type"`
|
||||||
|
|
||||||
|
// Port Nameserver Port
|
||||||
|
Port int `json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameserverNsType Nameserver Type
|
||||||
|
type NameserverNsType string
|
||||||
|
|
||||||
|
// NameserverGroup defines model for NameserverGroup.
|
||||||
|
type NameserverGroup struct {
|
||||||
|
// Description Nameserver group description
|
||||||
|
Description string `json:"description"`
|
||||||
|
|
||||||
|
// Enabled Nameserver group status
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
|
||||||
|
// Groups Nameserver group tag groups
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
|
||||||
|
// Id Nameserver group ID
|
||||||
|
Id string `json:"id"`
|
||||||
|
|
||||||
|
// Name Nameserver group name
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// Nameservers Nameserver group
|
||||||
|
Nameservers []Nameserver `json:"nameservers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameserverGroupPatchOperation defines model for NameserverGroupPatchOperation.
|
||||||
|
type NameserverGroupPatchOperation struct {
|
||||||
|
// Op Patch operation type
|
||||||
|
Op NameserverGroupPatchOperationOp `json:"op"`
|
||||||
|
|
||||||
|
// Path Nameserver group field to update in form /<field>
|
||||||
|
Path NameserverGroupPatchOperationPath `json:"path"`
|
||||||
|
|
||||||
|
// Value Values to be applied
|
||||||
|
Value []string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameserverGroupPatchOperationOp Patch operation type
|
||||||
|
type NameserverGroupPatchOperationOp string
|
||||||
|
|
||||||
|
// NameserverGroupPatchOperationPath Nameserver group field to update in form /<field>
|
||||||
|
type NameserverGroupPatchOperationPath string
|
||||||
|
|
||||||
|
// NameserverGroupRequest defines model for NameserverGroupRequest.
|
||||||
|
type NameserverGroupRequest struct {
|
||||||
|
// Description Nameserver group description
|
||||||
|
Description string `json:"description"`
|
||||||
|
|
||||||
|
// Enabled Nameserver group status
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
|
||||||
|
// Groups Nameserver group tag groups
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
|
||||||
|
// Name Nameserver group name
|
||||||
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// Nameservers Nameserver group
|
||||||
|
Nameservers []Nameserver `json:"nameservers"`
|
||||||
|
}
|
||||||
|
|
||||||
// PatchMinimum defines model for PatchMinimum.
|
// PatchMinimum defines model for PatchMinimum.
|
||||||
type PatchMinimum struct {
|
type PatchMinimum struct {
|
||||||
// Patch operation type
|
// Op Patch operation type
|
||||||
Op PatchMinimumOp `json:"op"`
|
Op PatchMinimumOp `json:"op"`
|
||||||
|
|
||||||
// Values to be applied
|
// Value Values to be applied
|
||||||
Value []string `json:"value"`
|
Value []string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch operation type
|
// PatchMinimumOp Patch operation type
|
||||||
type PatchMinimumOp string
|
type PatchMinimumOp string
|
||||||
|
|
||||||
// Peer defines model for Peer.
|
// Peer defines model for Peer.
|
||||||
type Peer struct {
|
type Peer struct {
|
||||||
// Provides information of who activated the Peer. User or Setup Key
|
// Connected Peer to Management connection status
|
||||||
ActivatedBy struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
} `json:"activated_by"`
|
|
||||||
|
|
||||||
// Peer to Management connection status
|
|
||||||
Connected bool `json:"connected"`
|
Connected bool `json:"connected"`
|
||||||
|
|
||||||
// Groups that the peer belongs to
|
// Groups Groups that the peer belongs to
|
||||||
Groups []GroupMinimum `json:"groups"`
|
Groups []GroupMinimum `json:"groups"`
|
||||||
|
|
||||||
// Peer ID
|
// Hostname Hostname of the machine
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
|
||||||
|
// Id Peer ID
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
|
||||||
// Peer's IP address
|
// Ip Peer's IP address
|
||||||
Ip string `json:"ip"`
|
Ip string `json:"ip"`
|
||||||
|
|
||||||
// Last time peer connected to Netbird's management service
|
// LastSeen Last time peer connected to Netbird's management service
|
||||||
LastSeen time.Time `json:"last_seen"`
|
LastSeen time.Time `json:"last_seen"`
|
||||||
|
|
||||||
// Peer's hostname
|
// Name Peer's hostname
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
// Peer's operating system and version
|
// Os Peer's operating system and version
|
||||||
Os string `json:"os"`
|
Os string `json:"os"`
|
||||||
|
|
||||||
// Indicates whether SSH server is enabled on this peer
|
// SshEnabled Indicates whether SSH server is enabled on this peer
|
||||||
SshEnabled bool `json:"ssh_enabled"`
|
SshEnabled bool `json:"ssh_enabled"`
|
||||||
|
|
||||||
// Peer's daemon or cli version
|
// UiVersion Peer's desktop UI version
|
||||||
|
UiVersion *string `json:"ui_version,omitempty"`
|
||||||
|
|
||||||
|
// UserId User ID of the user that enrolled this peer
|
||||||
|
UserId *string `json:"user_id,omitempty"`
|
||||||
|
|
||||||
|
// Version Peer's daemon or cli version
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PeerMinimum defines model for PeerMinimum.
|
// PeerMinimum defines model for PeerMinimum.
|
||||||
type PeerMinimum struct {
|
type PeerMinimum struct {
|
||||||
// Peer ID
|
// Id Peer ID
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
|
||||||
// Peer's hostname
|
// Name Peer's hostname
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route defines model for Route.
|
// Route defines model for Route.
|
||||||
type Route struct {
|
type Route struct {
|
||||||
// Route description
|
// Description Route description
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
|
||||||
// Route status
|
// Enabled Route status
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
|
||||||
// Route Id
|
// Id Route Id
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
|
||||||
// Indicate if peer should masquerade traffic to this route's prefix
|
// Masquerade Indicate if peer should masquerade traffic to this route's prefix
|
||||||
Masquerade bool `json:"masquerade"`
|
Masquerade bool `json:"masquerade"`
|
||||||
|
|
||||||
// Route metric number. Lowest number has higher priority
|
// Metric Route metric number. Lowest number has higher priority
|
||||||
Metric int `json:"metric"`
|
Metric int `json:"metric"`
|
||||||
|
|
||||||
// Network range in CIDR format
|
// Network Network range in CIDR format
|
||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
|
|
||||||
// Route network identifier, to group HA routes
|
// NetworkId Route network identifier, to group HA routes
|
||||||
NetworkId string `json:"network_id"`
|
NetworkId string `json:"network_id"`
|
||||||
|
|
||||||
// Network type indicating if it is IPv4 or IPv6
|
// NetworkType Network type indicating if it is IPv4 or IPv6
|
||||||
NetworkType string `json:"network_type"`
|
NetworkType string `json:"network_type"`
|
||||||
|
|
||||||
// Peer Identifier associated with route
|
// Peer Peer Identifier associated with route
|
||||||
Peer string `json:"peer"`
|
Peer string `json:"peer"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoutePatchOperation defines model for RoutePatchOperation.
|
// RoutePatchOperation defines model for RoutePatchOperation.
|
||||||
type RoutePatchOperation struct {
|
type RoutePatchOperation struct {
|
||||||
// Patch operation type
|
// Op Patch operation type
|
||||||
Op RoutePatchOperationOp `json:"op"`
|
Op RoutePatchOperationOp `json:"op"`
|
||||||
|
|
||||||
// Route field to update in form /<field>
|
// Path Route field to update in form /<field>
|
||||||
Path RoutePatchOperationPath `json:"path"`
|
Path RoutePatchOperationPath `json:"path"`
|
||||||
|
|
||||||
// Values to be applied
|
// Value Values to be applied
|
||||||
Value []string `json:"value"`
|
Value []string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch operation type
|
// RoutePatchOperationOp Patch operation type
|
||||||
type RoutePatchOperationOp string
|
type RoutePatchOperationOp string
|
||||||
|
|
||||||
// Route field to update in form /<field>
|
// RoutePatchOperationPath Route field to update in form /<field>
|
||||||
type RoutePatchOperationPath string
|
type RoutePatchOperationPath string
|
||||||
|
|
||||||
// RouteRequest defines model for RouteRequest.
|
// RouteRequest defines model for RouteRequest.
|
||||||
type RouteRequest struct {
|
type RouteRequest struct {
|
||||||
// Route description
|
// Description Route description
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
|
||||||
// Route status
|
// Enabled Route status
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
|
||||||
// Indicate if peer should masquerade traffic to this route's prefix
|
// Masquerade Indicate if peer should masquerade traffic to this route's prefix
|
||||||
Masquerade bool `json:"masquerade"`
|
Masquerade bool `json:"masquerade"`
|
||||||
|
|
||||||
// Route metric number. Lowest number has higher priority
|
// Metric Route metric number. Lowest number has higher priority
|
||||||
Metric int `json:"metric"`
|
Metric int `json:"metric"`
|
||||||
|
|
||||||
// Network range in CIDR format
|
// Network Network range in CIDR format
|
||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
|
|
||||||
// Route network identifier, to group HA routes
|
// NetworkId Route network identifier, to group HA routes
|
||||||
NetworkId string `json:"network_id"`
|
NetworkId string `json:"network_id"`
|
||||||
|
|
||||||
// Peer Identifier associated with route
|
// Peer Peer Identifier associated with route
|
||||||
Peer string `json:"peer"`
|
Peer string `json:"peer"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rule defines model for Rule.
|
// Rule defines model for Rule.
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
// Rule friendly description
|
// Description Rule friendly description
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
|
||||||
// Rule destination groups
|
// Destinations Rule destination groups
|
||||||
Destinations []GroupMinimum `json:"destinations"`
|
Destinations []GroupMinimum `json:"destinations"`
|
||||||
|
|
||||||
// Rules status
|
// Disabled Rules status
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
|
|
||||||
// Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
|
// Flow Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
|
||||||
Flow string `json:"flow"`
|
Flow string `json:"flow"`
|
||||||
|
|
||||||
// Rule ID
|
// Id Rule ID
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
|
||||||
// Rule name identifier
|
// Name Rule name identifier
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
// Rule source groups
|
// Sources Rule source groups
|
||||||
Sources []GroupMinimum `json:"sources"`
|
Sources []GroupMinimum `json:"sources"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RuleMinimum defines model for RuleMinimum.
|
// RuleMinimum defines model for RuleMinimum.
|
||||||
type RuleMinimum struct {
|
type RuleMinimum struct {
|
||||||
// Rule friendly description
|
// Description Rule friendly description
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
|
||||||
// Rules status
|
// Disabled Rules status
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
|
|
||||||
// Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
|
// Flow Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
|
||||||
Flow string `json:"flow"`
|
Flow string `json:"flow"`
|
||||||
|
|
||||||
// Rule name identifier
|
// Name Rule name identifier
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RulePatchOperation defines model for RulePatchOperation.
|
// RulePatchOperation defines model for RulePatchOperation.
|
||||||
type RulePatchOperation struct {
|
type RulePatchOperation struct {
|
||||||
// Patch operation type
|
// Op Patch operation type
|
||||||
Op RulePatchOperationOp `json:"op"`
|
Op RulePatchOperationOp `json:"op"`
|
||||||
|
|
||||||
// Rule field to update in form /<field>
|
// Path Rule field to update in form /<field>
|
||||||
Path RulePatchOperationPath `json:"path"`
|
Path RulePatchOperationPath `json:"path"`
|
||||||
|
|
||||||
// Values to be applied
|
// Value Values to be applied
|
||||||
Value []string `json:"value"`
|
Value []string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch operation type
|
// RulePatchOperationOp Patch operation type
|
||||||
type RulePatchOperationOp string
|
type RulePatchOperationOp string
|
||||||
|
|
||||||
// Rule field to update in form /<field>
|
// RulePatchOperationPath Rule field to update in form /<field>
|
||||||
type RulePatchOperationPath string
|
type RulePatchOperationPath string
|
||||||
|
|
||||||
// SetupKey defines model for SetupKey.
|
// SetupKey defines model for SetupKey.
|
||||||
type SetupKey struct {
|
type SetupKey struct {
|
||||||
// Setup Key expiration date
|
// AutoGroups Setup key groups to auto-assign to peers registered with this key
|
||||||
|
AutoGroups []string `json:"auto_groups"`
|
||||||
|
|
||||||
|
// Expires Setup Key expiration date
|
||||||
Expires time.Time `json:"expires"`
|
Expires time.Time `json:"expires"`
|
||||||
|
|
||||||
// Setup Key ID
|
// Id Setup Key ID
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
|
||||||
// Setup Key value
|
// Key Setup Key value
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
|
|
||||||
// Setup key last usage date
|
// LastUsed Setup key last usage date
|
||||||
LastUsed time.Time `json:"last_used"`
|
LastUsed time.Time `json:"last_used"`
|
||||||
|
|
||||||
// Setup key name identifier
|
// Name Setup key name identifier
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
// Setup key revocation status
|
// Revoked Setup key revocation status
|
||||||
Revoked bool `json:"revoked"`
|
Revoked bool `json:"revoked"`
|
||||||
|
|
||||||
// Setup key status, "valid", "overused","expired" or "revoked"
|
// State Setup key status, "valid", "overused","expired" or "revoked"
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
|
|
||||||
// Setup key type, one-off for single time usage and reusable
|
// Type Setup key type, one-off for single time usage and reusable
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
||||||
// Usage count of setup key
|
// UpdatedAt Setup key last update date
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// UsedTimes Usage count of setup key
|
||||||
UsedTimes int `json:"used_times"`
|
UsedTimes int `json:"used_times"`
|
||||||
|
|
||||||
// Setup key validity status
|
// Valid Setup key validity status
|
||||||
Valid bool `json:"valid"`
|
Valid bool `json:"valid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupKeyRequest defines model for SetupKeyRequest.
|
// SetupKeyRequest defines model for SetupKeyRequest.
|
||||||
type SetupKeyRequest struct {
|
type SetupKeyRequest struct {
|
||||||
// Expiration time in seconds
|
// AutoGroups Setup key groups to auto-assign to peers registered with this key
|
||||||
|
AutoGroups []string `json:"auto_groups"`
|
||||||
|
|
||||||
|
// ExpiresIn Expiration time in seconds
|
||||||
ExpiresIn int `json:"expires_in"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
|
||||||
// Setup Key name
|
// Name Setup Key name
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
// Setup key revocation status
|
// Revoked Setup key revocation status
|
||||||
Revoked bool `json:"revoked"`
|
Revoked bool `json:"revoked"`
|
||||||
|
|
||||||
// Setup key type, one-off for single time usage and reusable
|
// Type Setup key type, one-off for single time usage and reusable
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// User defines model for User.
|
// User defines model for User.
|
||||||
type User struct {
|
type User struct {
|
||||||
// User's email address
|
// AutoGroups Groups to auto-assign to peers registered by this user
|
||||||
|
AutoGroups []string `json:"auto_groups"`
|
||||||
|
|
||||||
|
// Email User's email address
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
|
||||||
// User ID
|
// Id User ID
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
|
||||||
// User's name from idp provider
|
// Name User's name from idp provider
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
// User's Netbird account role
|
// Role User's NetBird account role
|
||||||
|
Role string `json:"role"`
|
||||||
|
|
||||||
|
// Status User's status
|
||||||
|
Status UserStatus `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserStatus User's status
|
||||||
|
type UserStatus string
|
||||||
|
|
||||||
|
// UserCreateRequest defines model for UserCreateRequest.
|
||||||
|
type UserCreateRequest struct {
|
||||||
|
// AutoGroups Groups to auto-assign to peers registered by this user
|
||||||
|
AutoGroups []string `json:"auto_groups"`
|
||||||
|
|
||||||
|
// Email User's Email to send invite to
|
||||||
|
Email string `json:"email"`
|
||||||
|
|
||||||
|
// Name User's full name
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Role User's NetBird account role
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserRequest defines model for UserRequest.
|
||||||
|
type UserRequest struct {
|
||||||
|
// AutoGroups Groups to auto-assign to peers registered by this user
|
||||||
|
AutoGroups []string `json:"auto_groups"`
|
||||||
|
|
||||||
|
// Role User's NetBird account role
|
||||||
|
Role string `json:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatchApiDnsNameserversIdJSONBody defines parameters for PatchApiDnsNameserversId.
|
||||||
|
type PatchApiDnsNameserversIdJSONBody = []NameserverGroupPatchOperation
|
||||||
|
|
||||||
// PostApiGroupsJSONBody defines parameters for PostApiGroups.
|
// PostApiGroupsJSONBody defines parameters for PostApiGroups.
|
||||||
type PostApiGroupsJSONBody struct {
|
type PostApiGroupsJSONBody struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -381,28 +529,22 @@ type PutApiPeersIdJSONBody struct {
|
|||||||
SshEnabled bool `json:"ssh_enabled"`
|
SshEnabled bool `json:"ssh_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostApiRoutesJSONBody defines parameters for PostApiRoutes.
|
|
||||||
type PostApiRoutesJSONBody = RouteRequest
|
|
||||||
|
|
||||||
// PatchApiRoutesIdJSONBody defines parameters for PatchApiRoutesId.
|
// PatchApiRoutesIdJSONBody defines parameters for PatchApiRoutesId.
|
||||||
type PatchApiRoutesIdJSONBody = []RoutePatchOperation
|
type PatchApiRoutesIdJSONBody = []RoutePatchOperation
|
||||||
|
|
||||||
// PutApiRoutesIdJSONBody defines parameters for PutApiRoutesId.
|
|
||||||
type PutApiRoutesIdJSONBody = RouteRequest
|
|
||||||
|
|
||||||
// PostApiRulesJSONBody defines parameters for PostApiRules.
|
// PostApiRulesJSONBody defines parameters for PostApiRules.
|
||||||
type PostApiRulesJSONBody struct {
|
type PostApiRulesJSONBody struct {
|
||||||
// Rule friendly description
|
// Description Rule friendly description
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Destinations *[]string `json:"destinations,omitempty"`
|
Destinations *[]string `json:"destinations,omitempty"`
|
||||||
|
|
||||||
// Rules status
|
// Disabled Rules status
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
|
|
||||||
// Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
|
// Flow Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
|
||||||
Flow string `json:"flow"`
|
Flow string `json:"flow"`
|
||||||
|
|
||||||
// Rule name identifier
|
// Name Rule name identifier
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Sources *[]string `json:"sources,omitempty"`
|
Sources *[]string `json:"sources,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -412,26 +554,29 @@ type PatchApiRulesIdJSONBody = []RulePatchOperation
|
|||||||
|
|
||||||
// PutApiRulesIdJSONBody defines parameters for PutApiRulesId.
|
// PutApiRulesIdJSONBody defines parameters for PutApiRulesId.
|
||||||
type PutApiRulesIdJSONBody struct {
|
type PutApiRulesIdJSONBody struct {
|
||||||
// Rule friendly description
|
// Description Rule friendly description
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Destinations *[]string `json:"destinations,omitempty"`
|
Destinations *[]string `json:"destinations,omitempty"`
|
||||||
|
|
||||||
// Rules status
|
// Disabled Rules status
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
|
|
||||||
// Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
|
// Flow Rule flow, currently, only "bidirect" for bi-directional traffic is accepted
|
||||||
Flow string `json:"flow"`
|
Flow string `json:"flow"`
|
||||||
|
|
||||||
// Rule name identifier
|
// Name Rule name identifier
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Sources *[]string `json:"sources,omitempty"`
|
Sources *[]string `json:"sources,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostApiSetupKeysJSONBody defines parameters for PostApiSetupKeys.
|
// PostApiDnsNameserversJSONRequestBody defines body for PostApiDnsNameservers for application/json ContentType.
|
||||||
type PostApiSetupKeysJSONBody = SetupKeyRequest
|
type PostApiDnsNameserversJSONRequestBody = NameserverGroupRequest
|
||||||
|
|
||||||
// PutApiSetupKeysIdJSONBody defines parameters for PutApiSetupKeysId.
|
// PatchApiDnsNameserversIdJSONRequestBody defines body for PatchApiDnsNameserversId for application/json ContentType.
|
||||||
type PutApiSetupKeysIdJSONBody = SetupKeyRequest
|
type PatchApiDnsNameserversIdJSONRequestBody = PatchApiDnsNameserversIdJSONBody
|
||||||
|
|
||||||
|
// PutApiDnsNameserversIdJSONRequestBody defines body for PutApiDnsNameserversId for application/json ContentType.
|
||||||
|
type PutApiDnsNameserversIdJSONRequestBody = NameserverGroupRequest
|
||||||
|
|
||||||
// PostApiGroupsJSONRequestBody defines body for PostApiGroups for application/json ContentType.
|
// PostApiGroupsJSONRequestBody defines body for PostApiGroups for application/json ContentType.
|
||||||
type PostApiGroupsJSONRequestBody PostApiGroupsJSONBody
|
type PostApiGroupsJSONRequestBody PostApiGroupsJSONBody
|
||||||
@@ -446,13 +591,13 @@ type PutApiGroupsIdJSONRequestBody PutApiGroupsIdJSONBody
|
|||||||
type PutApiPeersIdJSONRequestBody PutApiPeersIdJSONBody
|
type PutApiPeersIdJSONRequestBody PutApiPeersIdJSONBody
|
||||||
|
|
||||||
// PostApiRoutesJSONRequestBody defines body for PostApiRoutes for application/json ContentType.
|
// PostApiRoutesJSONRequestBody defines body for PostApiRoutes for application/json ContentType.
|
||||||
type PostApiRoutesJSONRequestBody = PostApiRoutesJSONBody
|
type PostApiRoutesJSONRequestBody = RouteRequest
|
||||||
|
|
||||||
// PatchApiRoutesIdJSONRequestBody defines body for PatchApiRoutesId for application/json ContentType.
|
// PatchApiRoutesIdJSONRequestBody defines body for PatchApiRoutesId for application/json ContentType.
|
||||||
type PatchApiRoutesIdJSONRequestBody = PatchApiRoutesIdJSONBody
|
type PatchApiRoutesIdJSONRequestBody = PatchApiRoutesIdJSONBody
|
||||||
|
|
||||||
// PutApiRoutesIdJSONRequestBody defines body for PutApiRoutesId for application/json ContentType.
|
// PutApiRoutesIdJSONRequestBody defines body for PutApiRoutesId for application/json ContentType.
|
||||||
type PutApiRoutesIdJSONRequestBody = PutApiRoutesIdJSONBody
|
type PutApiRoutesIdJSONRequestBody = RouteRequest
|
||||||
|
|
||||||
// PostApiRulesJSONRequestBody defines body for PostApiRules for application/json ContentType.
|
// PostApiRulesJSONRequestBody defines body for PostApiRules for application/json ContentType.
|
||||||
type PostApiRulesJSONRequestBody PostApiRulesJSONBody
|
type PostApiRulesJSONRequestBody PostApiRulesJSONBody
|
||||||
@@ -464,7 +609,13 @@ type PatchApiRulesIdJSONRequestBody = PatchApiRulesIdJSONBody
|
|||||||
type PutApiRulesIdJSONRequestBody PutApiRulesIdJSONBody
|
type PutApiRulesIdJSONRequestBody PutApiRulesIdJSONBody
|
||||||
|
|
||||||
// PostApiSetupKeysJSONRequestBody defines body for PostApiSetupKeys for application/json ContentType.
|
// PostApiSetupKeysJSONRequestBody defines body for PostApiSetupKeys for application/json ContentType.
|
||||||
type PostApiSetupKeysJSONRequestBody = PostApiSetupKeysJSONBody
|
type PostApiSetupKeysJSONRequestBody = SetupKeyRequest
|
||||||
|
|
||||||
// PutApiSetupKeysIdJSONRequestBody defines body for PutApiSetupKeysId for application/json ContentType.
|
// PutApiSetupKeysIdJSONRequestBody defines body for PutApiSetupKeysId for application/json ContentType.
|
||||||
type PutApiSetupKeysIdJSONRequestBody = PutApiSetupKeysIdJSONBody
|
type PutApiSetupKeysIdJSONRequestBody = SetupKeyRequest
|
||||||
|
|
||||||
|
// PostApiUsersJSONRequestBody defines body for PostApiUsers for application/json ContentType.
|
||||||
|
type PostApiUsersJSONRequestBody = UserCreateRequest
|
||||||
|
|
||||||
|
// PutApiUsersIdJSONRequestBody defines body for PutApiUsersId for application/json ContentType.
|
||||||
|
type PutApiUsersIdJSONRequestBody = UserRequest
|
||||||
|
|||||||
@@ -67,14 +67,14 @@ func initGroupTestData(groups ...*server.Group) *Groups {
|
|||||||
}
|
}
|
||||||
return nil, fmt.Errorf("peer not found")
|
return nil, fmt.Errorf("peer not found")
|
||||||
},
|
},
|
||||||
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||||
return &server.Account{
|
return &server.Account{
|
||||||
Id: claims.AccountId,
|
Id: claims.AccountId,
|
||||||
Domain: "hotmail.com",
|
Domain: "hotmail.com",
|
||||||
Peers: TestPeers,
|
Peers: TestPeers,
|
||||||
Groups: map[string]*server.Group{
|
Groups: map[string]*server.Group{
|
||||||
"id-existed": &server.Group{ID: "id-existed", Peers: []string{"A", "B"}},
|
"id-existed": {ID: "id-existed", Peers: []string{"A", "B"}},
|
||||||
"id-all": &server.Group{ID: "id-all", Name: "All"}},
|
"id-all": {ID: "id-all", Name: "All"}},
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,17 +34,19 @@ func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience
|
|||||||
keysHandler := NewSetupKeysHandler(accountManager, authAudience)
|
keysHandler := NewSetupKeysHandler(accountManager, authAudience)
|
||||||
userHandler := NewUserHandler(accountManager, authAudience)
|
userHandler := NewUserHandler(accountManager, authAudience)
|
||||||
routesHandler := NewRoutes(accountManager, authAudience)
|
routesHandler := NewRoutes(accountManager, authAudience)
|
||||||
|
nameserversHandler := NewNameservers(accountManager, authAudience)
|
||||||
|
|
||||||
apiHandler.HandleFunc("/api/peers", peersHandler.GetPeers).Methods("GET", "OPTIONS")
|
apiHandler.HandleFunc("/api/peers", peersHandler.GetPeers).Methods("GET", "OPTIONS")
|
||||||
apiHandler.HandleFunc("/api/peers/{id}", peersHandler.HandlePeer).
|
apiHandler.HandleFunc("/api/peers/{id}", peersHandler.HandlePeer).
|
||||||
Methods("GET", "PUT", "DELETE", "OPTIONS")
|
Methods("GET", "PUT", "DELETE", "OPTIONS")
|
||||||
apiHandler.HandleFunc("/api/users", userHandler.GetUsers).Methods("GET", "OPTIONS")
|
apiHandler.HandleFunc("/api/users", userHandler.GetUsers).Methods("GET", "OPTIONS")
|
||||||
apiHandler.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("GET", "POST", "OPTIONS")
|
apiHandler.HandleFunc("/api/users/{id}", userHandler.UpdateUser).Methods("PUT", "OPTIONS")
|
||||||
apiHandler.HandleFunc("/api/setup-keys/{id}", keysHandler.HandleKey).Methods("GET", "PUT", "OPTIONS")
|
apiHandler.HandleFunc("/api/users", userHandler.CreateUserHandler).Methods("POST", "OPTIONS")
|
||||||
|
|
||||||
apiHandler.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("POST", "OPTIONS")
|
apiHandler.HandleFunc("/api/setup-keys", keysHandler.GetAllSetupKeysHandler).Methods("GET", "OPTIONS")
|
||||||
apiHandler.HandleFunc("/api/setup-keys/{id}", keysHandler.HandleKey).
|
apiHandler.HandleFunc("/api/setup-keys", keysHandler.CreateSetupKeyHandler).Methods("POST", "OPTIONS")
|
||||||
Methods("GET", "PUT", "DELETE", "OPTIONS")
|
apiHandler.HandleFunc("/api/setup-keys/{id}", keysHandler.GetSetupKeyHandler).Methods("GET", "OPTIONS")
|
||||||
|
apiHandler.HandleFunc("/api/setup-keys/{id}", keysHandler.UpdateSetupKeyHandler).Methods("PUT", "OPTIONS")
|
||||||
|
|
||||||
apiHandler.HandleFunc("/api/rules", rulesHandler.GetAllRulesHandler).Methods("GET", "OPTIONS")
|
apiHandler.HandleFunc("/api/rules", rulesHandler.GetAllRulesHandler).Methods("GET", "OPTIONS")
|
||||||
apiHandler.HandleFunc("/api/rules", rulesHandler.CreateRuleHandler).Methods("POST", "OPTIONS")
|
apiHandler.HandleFunc("/api/rules", rulesHandler.CreateRuleHandler).Methods("POST", "OPTIONS")
|
||||||
@@ -67,6 +69,13 @@ func APIHandler(accountManager s.AccountManager, authIssuer string, authAudience
|
|||||||
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.GetRouteHandler).Methods("GET", "OPTIONS")
|
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.GetRouteHandler).Methods("GET", "OPTIONS")
|
||||||
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.DeleteRouteHandler).Methods("DELETE", "OPTIONS")
|
apiHandler.HandleFunc("/api/routes/{id}", routesHandler.DeleteRouteHandler).Methods("DELETE", "OPTIONS")
|
||||||
|
|
||||||
|
apiHandler.HandleFunc("/api/dns/nameservers", nameserversHandler.GetAllNameserversHandler).Methods("GET", "OPTIONS")
|
||||||
|
apiHandler.HandleFunc("/api/dns/nameservers", nameserversHandler.CreateNameserverGroupHandler).Methods("POST", "OPTIONS")
|
||||||
|
apiHandler.HandleFunc("/api/dns/nameservers/{id}", nameserversHandler.UpdateNameserverGroupHandler).Methods("PUT", "OPTIONS")
|
||||||
|
apiHandler.HandleFunc("/api/dns/nameservers/{id}", nameserversHandler.PatchNameserverGroupHandler).Methods("PATCH", "OPTIONS")
|
||||||
|
apiHandler.HandleFunc("/api/dns/nameservers/{id}", nameserversHandler.GetNameserverGroupHandler).Methods("GET", "OPTIONS")
|
||||||
|
apiHandler.HandleFunc("/api/dns/nameservers/{id}", nameserversHandler.DeleteNameserverGroupHandler).Methods("DELETE", "OPTIONS")
|
||||||
|
|
||||||
return apiHandler, nil
|
return apiHandler, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
286
management/server/http/nameservers.go
Normal file
286
management/server/http/nameservers.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/management/server"
|
||||||
|
"github.com/netbirdio/netbird/management/server/http/api"
|
||||||
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Nameservers is the nameserver group handler of the account
|
||||||
|
type Nameservers struct {
|
||||||
|
jwtExtractor jwtclaims.ClaimsExtractor
|
||||||
|
accountManager server.AccountManager
|
||||||
|
authAudience string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNameservers returns a new instance of Nameservers handler
|
||||||
|
func NewNameservers(accountManager server.AccountManager, authAudience string) *Nameservers {
|
||||||
|
return &Nameservers{
|
||||||
|
accountManager: accountManager,
|
||||||
|
authAudience: authAudience,
|
||||||
|
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllNameserversHandler returns the list of nameserver groups for the account
|
||||||
|
func (h *Nameservers) GetAllNameserversHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsGroups, err := h.accountManager.ListNameServerGroups(account.Id)
|
||||||
|
if err != nil {
|
||||||
|
toHTTPError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiNameservers := make([]*api.NameserverGroup, 0)
|
||||||
|
for _, r := range nsGroups {
|
||||||
|
apiNameservers = append(apiNameservers, toNameserverGroupResponse(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSONObject(w, apiNameservers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNameserverGroupHandler handles nameserver group creation request
|
||||||
|
func (h *Nameservers) CreateNameserverGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req api.PostApiDnsNameserversJSONRequestBody
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsList, err := toServerNSList(req.Nameservers)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsGroup, err := h.accountManager.CreateNameServerGroup(account.Id, req.Name, req.Description, nsList, req.Groups, req.Enabled)
|
||||||
|
if err != nil {
|
||||||
|
toHTTPError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := toNameserverGroupResponse(nsGroup)
|
||||||
|
|
||||||
|
writeJSONObject(w, &resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateNameserverGroupHandler handles update to a nameserver group identified by a given ID
|
||||||
|
func (h *Nameservers) UpdateNameserverGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsGroupID := mux.Vars(r)["id"]
|
||||||
|
if len(nsGroupID) == 0 {
|
||||||
|
http.Error(w, "invalid nameserver group ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req api.PutApiDnsNameserversIdJSONRequestBody
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsList, err := toServerNSList(req.Nameservers)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedNSGroup := &nbdns.NameServerGroup{
|
||||||
|
ID: nsGroupID,
|
||||||
|
Name: req.Name,
|
||||||
|
Description: req.Description,
|
||||||
|
NameServers: nsList,
|
||||||
|
Groups: req.Groups,
|
||||||
|
Enabled: req.Enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.accountManager.SaveNameServerGroup(account.Id, updatedNSGroup)
|
||||||
|
if err != nil {
|
||||||
|
toHTTPError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := toNameserverGroupResponse(updatedNSGroup)
|
||||||
|
|
||||||
|
writeJSONObject(w, &resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatchNameserverGroupHandler handles patch updates to a nameserver group identified by a given ID
|
||||||
|
func (h *Nameservers) PatchNameserverGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsGroupID := mux.Vars(r)["id"]
|
||||||
|
if len(nsGroupID) == 0 {
|
||||||
|
http.Error(w, "invalid nameserver group ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req api.PatchApiDnsNameserversIdJSONRequestBody
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var operations []server.NameServerGroupUpdateOperation
|
||||||
|
for _, patch := range req {
|
||||||
|
if patch.Op != api.NameserverGroupPatchOperationOpReplace {
|
||||||
|
http.Error(w, fmt.Sprintf("nameserver groups only accepts replace operations, got %s", patch.Op),
|
||||||
|
http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch patch.Path {
|
||||||
|
case api.NameserverGroupPatchOperationPathName:
|
||||||
|
operations = append(operations, server.NameServerGroupUpdateOperation{
|
||||||
|
Type: server.UpdateNameServerGroupName,
|
||||||
|
Values: patch.Value,
|
||||||
|
})
|
||||||
|
case api.NameserverGroupPatchOperationPathDescription:
|
||||||
|
operations = append(operations, server.NameServerGroupUpdateOperation{
|
||||||
|
Type: server.UpdateNameServerGroupDescription,
|
||||||
|
Values: patch.Value,
|
||||||
|
})
|
||||||
|
case api.NameserverGroupPatchOperationPathNameservers:
|
||||||
|
operations = append(operations, server.NameServerGroupUpdateOperation{
|
||||||
|
Type: server.UpdateNameServerGroupNameServers,
|
||||||
|
Values: patch.Value,
|
||||||
|
})
|
||||||
|
case api.NameserverGroupPatchOperationPathGroups:
|
||||||
|
operations = append(operations, server.NameServerGroupUpdateOperation{
|
||||||
|
Type: server.UpdateNameServerGroupGroups,
|
||||||
|
Values: patch.Value,
|
||||||
|
})
|
||||||
|
case api.NameserverGroupPatchOperationPathEnabled:
|
||||||
|
operations = append(operations, server.NameServerGroupUpdateOperation{
|
||||||
|
Type: server.UpdateNameServerGroupEnabled,
|
||||||
|
Values: patch.Value,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
http.Error(w, "invalid patch path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedNSGroup, err := h.accountManager.UpdateNameServerGroup(account.Id, nsGroupID, operations)
|
||||||
|
if err != nil {
|
||||||
|
toHTTPError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := toNameserverGroupResponse(updatedNSGroup)
|
||||||
|
|
||||||
|
writeJSONObject(w, &resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNameserverGroupHandler handles nameserver group deletion request
|
||||||
|
func (h *Nameservers) DeleteNameserverGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsGroupID := mux.Vars(r)["id"]
|
||||||
|
if len(nsGroupID) == 0 {
|
||||||
|
http.Error(w, "invalid nameserver group ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.accountManager.DeleteNameServerGroup(account.Id, nsGroupID)
|
||||||
|
if err != nil {
|
||||||
|
toHTTPError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSONObject(w, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNameserverGroupHandler handles a nameserver group Get request identified by ID
|
||||||
|
func (h *Nameservers) GetNameserverGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsGroupID := mux.Vars(r)["id"]
|
||||||
|
if len(nsGroupID) == 0 {
|
||||||
|
http.Error(w, "invalid nameserver group ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsGroup, err := h.accountManager.GetNameServerGroup(account.Id, nsGroupID)
|
||||||
|
if err != nil {
|
||||||
|
toHTTPError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := toNameserverGroupResponse(nsGroup)
|
||||||
|
|
||||||
|
writeJSONObject(w, &resp)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func toServerNSList(apiNSList []api.Nameserver) ([]nbdns.NameServer, error) {
|
||||||
|
var nsList []nbdns.NameServer
|
||||||
|
for _, apiNS := range apiNSList {
|
||||||
|
parsed, err := nbdns.ParseNameServerURL(fmt.Sprintf("%s://%s:%d", apiNS.NsType, apiNS.Ip, apiNS.Port))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nsList = append(nsList, parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nsList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toNameserverGroupResponse(serverNSGroup *nbdns.NameServerGroup) *api.NameserverGroup {
|
||||||
|
var nsList []api.Nameserver
|
||||||
|
for _, ns := range serverNSGroup.NameServers {
|
||||||
|
apiNS := api.Nameserver{
|
||||||
|
Ip: ns.IP.String(),
|
||||||
|
NsType: api.NameserverNsType(ns.NSType.String()),
|
||||||
|
Port: ns.Port,
|
||||||
|
}
|
||||||
|
nsList = append(nsList, apiNS)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &api.NameserverGroup{
|
||||||
|
Id: serverNSGroup.ID,
|
||||||
|
Name: serverNSGroup.Name,
|
||||||
|
Description: serverNSGroup.Description,
|
||||||
|
Groups: serverNSGroup.Groups,
|
||||||
|
Nameservers: nsList,
|
||||||
|
Enabled: serverNSGroup.Enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
287
management/server/http/nameservers_test.go
Normal file
287
management/server/http/nameservers_test.go
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/management/server/http/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/netbirdio/netbird/management/server"
|
||||||
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||||
|
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
existingNSGroupID = "existingNSGroupID"
|
||||||
|
notFoundNSGroupID = "notFoundNSGroupID"
|
||||||
|
testNSGroupAccountID = "test_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testingNSAccount = &server.Account{
|
||||||
|
Id: testNSGroupAccountID,
|
||||||
|
Domain: "hotmail.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseExistingNSGroup = &nbdns.NameServerGroup{
|
||||||
|
ID: existingNSGroupID,
|
||||||
|
Name: "super",
|
||||||
|
Description: "super",
|
||||||
|
NameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: []string{"testing"},
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func initNameserversTestData() *Nameservers {
|
||||||
|
return &Nameservers{
|
||||||
|
accountManager: &mock_server.MockAccountManager{
|
||||||
|
GetNameServerGroupFunc: func(accountID, nsGroupID string) (*nbdns.NameServerGroup, error) {
|
||||||
|
if nsGroupID == existingNSGroupID {
|
||||||
|
return baseExistingNSGroup.Copy(), nil
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.NotFound, "nameserver group with ID %s not found", nsGroupID)
|
||||||
|
},
|
||||||
|
CreateNameServerGroupFunc: func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error) {
|
||||||
|
return &nbdns.NameServerGroup{
|
||||||
|
ID: existingNSGroupID,
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
NameServers: nameServerList,
|
||||||
|
Groups: groups,
|
||||||
|
Enabled: enabled,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
DeleteNameServerGroupFunc: func(accountID, nsGroupID string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
SaveNameServerGroupFunc: func(accountID string, nsGroupToSave *nbdns.NameServerGroup) error {
|
||||||
|
if nsGroupToSave.ID == existingNSGroupID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return status.Errorf(codes.NotFound, "nameserver group with ID %s was not found", nsGroupToSave.ID)
|
||||||
|
},
|
||||||
|
UpdateNameServerGroupFunc: func(accountID, nsGroupID string, operations []server.NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error) {
|
||||||
|
nsGroupToUpdate := baseExistingNSGroup.Copy()
|
||||||
|
if nsGroupID != nsGroupToUpdate.ID {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "nameserver group ID %s no longer exists", nsGroupID)
|
||||||
|
}
|
||||||
|
for _, operation := range operations {
|
||||||
|
switch operation.Type {
|
||||||
|
case server.UpdateNameServerGroupName:
|
||||||
|
nsGroupToUpdate.Name = operation.Values[0]
|
||||||
|
case server.UpdateNameServerGroupDescription:
|
||||||
|
nsGroupToUpdate.Description = operation.Values[0]
|
||||||
|
case server.UpdateNameServerGroupNameServers:
|
||||||
|
var parsedNSList []nbdns.NameServer
|
||||||
|
for _, nsURL := range operation.Values {
|
||||||
|
parsed, err := nbdns.ParseNameServerURL(nsURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parsedNSList = append(parsedNSList, parsed)
|
||||||
|
}
|
||||||
|
nsGroupToUpdate.NameServers = parsedNSList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nsGroupToUpdate, nil
|
||||||
|
},
|
||||||
|
GetAccountFromTokenFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||||
|
return testingNSAccount, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authAudience: "",
|
||||||
|
jwtExtractor: jwtclaims.ClaimsExtractor{
|
||||||
|
ExtractClaimsFromRequestContext: func(r *http.Request, authAudiance string) jwtclaims.AuthorizationClaims {
|
||||||
|
return jwtclaims.AuthorizationClaims{
|
||||||
|
UserId: "test_user",
|
||||||
|
Domain: "hotmail.com",
|
||||||
|
AccountId: testNSGroupAccountID,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNameserversHandlers(t *testing.T) {
|
||||||
|
tt := []struct {
|
||||||
|
name string
|
||||||
|
expectedStatus int
|
||||||
|
expectedBody bool
|
||||||
|
expectedNSGroup *api.NameserverGroup
|
||||||
|
requestType string
|
||||||
|
requestPath string
|
||||||
|
requestBody io.Reader
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Get Existing Nameserver Group",
|
||||||
|
requestType: http.MethodGet,
|
||||||
|
requestPath: "/api/dns/nameservers/" + existingNSGroupID,
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedBody: true,
|
||||||
|
expectedNSGroup: toNameserverGroupResponse(baseExistingNSGroup),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Get Not Existing Nameserver Group",
|
||||||
|
requestType: http.MethodGet,
|
||||||
|
requestPath: "/api/dns/nameservers/" + notFoundNSGroupID,
|
||||||
|
expectedStatus: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST OK",
|
||||||
|
requestType: http.MethodPost,
|
||||||
|
requestPath: "/api/dns/nameservers",
|
||||||
|
requestBody: bytes.NewBuffer(
|
||||||
|
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"1.1.1.1\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true}")),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedBody: true,
|
||||||
|
expectedNSGroup: &api.NameserverGroup{
|
||||||
|
Id: existingNSGroupID,
|
||||||
|
Name: "name",
|
||||||
|
Description: "Post",
|
||||||
|
Nameservers: []api.Nameserver{
|
||||||
|
{
|
||||||
|
Ip: "1.1.1.1",
|
||||||
|
NsType: "udp",
|
||||||
|
Port: 53,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: []string{"group"},
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST Invalid Nameserver",
|
||||||
|
requestType: http.MethodPost,
|
||||||
|
requestPath: "/api/dns/nameservers",
|
||||||
|
requestBody: bytes.NewBuffer(
|
||||||
|
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"1000\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true}")),
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
expectedBody: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PUT OK",
|
||||||
|
requestType: http.MethodPut,
|
||||||
|
requestPath: "/api/dns/nameservers/" + existingNSGroupID,
|
||||||
|
requestBody: bytes.NewBuffer(
|
||||||
|
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"1.1.1.1\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true}")),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedBody: true,
|
||||||
|
expectedNSGroup: &api.NameserverGroup{
|
||||||
|
Id: existingNSGroupID,
|
||||||
|
Name: "name",
|
||||||
|
Description: "Post",
|
||||||
|
Nameservers: []api.Nameserver{
|
||||||
|
{
|
||||||
|
Ip: "1.1.1.1",
|
||||||
|
NsType: "udp",
|
||||||
|
Port: 53,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: []string{"group"},
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PUT Not Existing Nameserver Group",
|
||||||
|
requestType: http.MethodPut,
|
||||||
|
requestPath: "/api/dns/nameservers/" + notFoundNSGroupID,
|
||||||
|
requestBody: bytes.NewBuffer(
|
||||||
|
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"1.1.1.1\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true}")),
|
||||||
|
expectedStatus: http.StatusNotFound,
|
||||||
|
expectedBody: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PUT Invalid Nameserver",
|
||||||
|
requestType: http.MethodPut,
|
||||||
|
requestPath: "/api/dns/nameservers/" + notFoundNSGroupID,
|
||||||
|
requestBody: bytes.NewBuffer(
|
||||||
|
[]byte("{\"name\":\"name\",\"Description\":\"Post\",\"nameservers\":[{\"ip\":\"100\",\"ns_type\":\"udp\",\"port\":53}],\"groups\":[\"group\"],\"enabled\":true}")),
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
expectedBody: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PATCH OK",
|
||||||
|
requestType: http.MethodPatch,
|
||||||
|
requestPath: "/api/dns/nameservers/" + existingNSGroupID,
|
||||||
|
requestBody: bytes.NewBufferString("[{\"op\":\"replace\",\"path\":\"description\",\"value\":[\"NewDesc\"]}]"),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedBody: true,
|
||||||
|
expectedNSGroup: &api.NameserverGroup{
|
||||||
|
Id: existingNSGroupID,
|
||||||
|
Name: baseExistingNSGroup.Name,
|
||||||
|
Description: "NewDesc",
|
||||||
|
Nameservers: toNameserverGroupResponse(baseExistingNSGroup).Nameservers,
|
||||||
|
Groups: baseExistingNSGroup.Groups,
|
||||||
|
Enabled: baseExistingNSGroup.Enabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PATCH Invalid Nameserver Group OK",
|
||||||
|
requestType: http.MethodPatch,
|
||||||
|
requestPath: "/api/dns/nameservers/" + notFoundRouteID,
|
||||||
|
requestBody: bytes.NewBufferString("[{\"op\":\"replace\",\"path\":\"description\",\"value\":[\"NewDesc\"]}]"),
|
||||||
|
expectedStatus: http.StatusNotFound,
|
||||||
|
expectedBody: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
p := initNameserversTestData()
|
||||||
|
|
||||||
|
for _, tc := range tt {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
||||||
|
|
||||||
|
router := mux.NewRouter()
|
||||||
|
router.HandleFunc("/api/dns/nameservers/{id}", p.GetNameserverGroupHandler).Methods("GET")
|
||||||
|
router.HandleFunc("/api/dns/nameservers", p.CreateNameserverGroupHandler).Methods("POST")
|
||||||
|
router.HandleFunc("/api/dns/nameservers/{id}", p.DeleteNameserverGroupHandler).Methods("DELETE")
|
||||||
|
router.HandleFunc("/api/dns/nameservers/{id}", p.UpdateNameserverGroupHandler).Methods("PUT")
|
||||||
|
router.HandleFunc("/api/dns/nameservers/{id}", p.PatchNameserverGroupHandler).Methods("PATCH")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
res := recorder.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
content, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("I don't know what I expected; %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status := recorder.Code; status != tc.expectedStatus {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v, content: %s",
|
||||||
|
status, tc.expectedStatus, string(content))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectedBody {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
got := &api.NameserverGroup{}
|
||||||
|
if err = json.Unmarshal(content, &got); err != nil {
|
||||||
|
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.expectedNSGroup, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
//Peers is a handler that returns peers of the account
|
// Peers is a handler that returns peers of the account
|
||||||
type Peers struct {
|
type Peers struct {
|
||||||
accountManager server.AccountManager
|
accountManager server.AccountManager
|
||||||
authAudience string
|
authAudience string
|
||||||
@@ -144,5 +144,8 @@ func toPeerResponse(peer *server.Peer, account *server.Account) *api.Peer {
|
|||||||
Version: peer.Meta.WtVersion,
|
Version: peer.Meta.WtVersion,
|
||||||
Groups: groupsInfo,
|
Groups: groupsInfo,
|
||||||
SshEnabled: peer.SSHEnabled,
|
SshEnabled: peer.SSHEnabled,
|
||||||
|
Hostname: peer.Meta.Hostname,
|
||||||
|
UserId: &peer.UserID,
|
||||||
|
UiVersion: &peer.Meta.UIVersion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
func initTestMetaData(peer ...*server.Peer) *Peers {
|
func initTestMetaData(peer ...*server.Peer) *Peers {
|
||||||
return &Peers{
|
return &Peers{
|
||||||
accountManager: &mock_server.MockAccountManager{
|
accountManager: &mock_server.MockAccountManager{
|
||||||
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||||
return &server.Account{
|
return &server.Account{
|
||||||
Id: claims.AccountId,
|
Id: claims.AccountId,
|
||||||
Domain: "hotmail.com",
|
Domain: "hotmail.com",
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ func (h *Routes) UpdateRouteHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req api.PutApiRoutesIdJSONBody
|
var req api.PutApiRoutesIdJSONRequestBody
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -348,6 +348,11 @@ func (h *Routes) DeleteRouteHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = h.accountManager.DeleteRoute(account.Id, routeID)
|
err = h.accountManager.DeleteRoute(account.Id, routeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
errStatus, ok := status.FromError(err)
|
||||||
|
if ok && errStatus.Code() == codes.NotFound {
|
||||||
|
http.Error(w, fmt.Sprintf("route %s not found under account %s", routeID, account.Id), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Errorf("failed delete route %s under account %s %v", routeID, account.Id, err)
|
log.Errorf("failed delete route %s under account %s %v", routeID, account.Id, err)
|
||||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ func initRoutesTestData() *Routes {
|
|||||||
SaveRouteFunc: func(_ string, _ *route.Route) error {
|
SaveRouteFunc: func(_ string, _ *route.Route) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
DeleteRouteFunc: func(_ string, _ string) error {
|
DeleteRouteFunc: func(_ string, peerIP string) error {
|
||||||
|
if peerIP != existingRouteID {
|
||||||
|
return status.Errorf(codes.NotFound, "Peer with ID %s not found", peerIP)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
GetPeerByIPFunc: func(_ string, peerIP string) (*server.Peer, error) {
|
GetPeerByIPFunc: func(_ string, peerIP string) (*server.Peer, error) {
|
||||||
@@ -117,7 +120,7 @@ func initRoutesTestData() *Routes {
|
|||||||
}
|
}
|
||||||
return routeToUpdate, nil
|
return routeToUpdate, nil
|
||||||
},
|
},
|
||||||
GetAccountWithAuthorizationClaimsFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
GetAccountFromTokenFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||||
return testingAccount, nil
|
return testingAccount, nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -155,7 +158,7 @@ func TestRoutesHandlers(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Get Not Existing Route",
|
name: "Get Not Existing Route",
|
||||||
requestType: http.MethodGet,
|
requestType: http.MethodGet,
|
||||||
requestPath: "/api/rules/" + notFoundRouteID,
|
requestPath: "/api/routes/" + notFoundRouteID,
|
||||||
expectedStatus: http.StatusNotFound,
|
expectedStatus: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -168,7 +171,7 @@ func TestRoutesHandlers(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Delete Not Existing Route",
|
name: "Delete Not Existing Route",
|
||||||
requestType: http.MethodDelete,
|
requestType: http.MethodDelete,
|
||||||
requestPath: "/api/rules/" + notFoundRouteID,
|
requestPath: "/api/routes/" + notFoundRouteID,
|
||||||
expectedStatus: http.StatusNotFound,
|
expectedStatus: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,14 +66,14 @@ func initRulesTestData(rules ...*server.Rule) *Rules {
|
|||||||
}
|
}
|
||||||
return &rule, nil
|
return &rule, nil
|
||||||
},
|
},
|
||||||
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||||
return &server.Account{
|
return &server.Account{
|
||||||
Id: claims.AccountId,
|
Id: claims.AccountId,
|
||||||
Domain: "hotmail.com",
|
Domain: "hotmail.com",
|
||||||
Rules: map[string]*server.Rule{"id-existed": &server.Rule{ID: "id-existed"}},
|
Rules: map[string]*server.Rule{"id-existed": &server.Rule{ID: "id-existed"}},
|
||||||
Groups: map[string]*server.Group{
|
Groups: map[string]*server.Group{
|
||||||
"F": &server.Group{ID: "F"},
|
"F": {ID: "F"},
|
||||||
"G": &server.Group{ID: "G"},
|
"G": {ID: "G"},
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/netbirdio/netbird/management/server"
|
"github.com/netbirdio/netbird/management/server"
|
||||||
"github.com/netbirdio/netbird/management/server/http/api"
|
"github.com/netbirdio/netbird/management/server/http/api"
|
||||||
@@ -28,54 +29,17 @@ func NewSetupKeysHandler(accountManager server.AccountManager, authAudience stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SetupKeys) updateKey(accountId string, keyId string, w http.ResponseWriter, r *http.Request) {
|
// CreateSetupKeyHandler is a POST requests that creates a new SetupKey
|
||||||
req := &api.PutApiSetupKeysIdJSONRequestBody{}
|
func (h *SetupKeys) CreateSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
log.Error(err)
|
||||||
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var key *server.SetupKey
|
|
||||||
if req.Revoked {
|
|
||||||
//handle only if being revoked, don't allow to enable key again for now
|
|
||||||
key, err = h.accountManager.RevokeSetupKey(accountId, keyId)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "failed revoking key", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(req.Name) != 0 {
|
|
||||||
key, err = h.accountManager.RenameSetupKey(accountId, keyId, req.Name)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "failed renaming key", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if key != nil {
|
|
||||||
writeSuccess(w, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SetupKeys) getKey(accountId string, keyId string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
account, err := h.accountManager.GetAccountById(accountId)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "account doesn't exist", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, key := range account.SetupKeys {
|
|
||||||
if key.Id == keyId {
|
|
||||||
writeSuccess(w, key)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
http.Error(w, "setup key not found", http.StatusNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
req := &api.PostApiSetupKeysJSONRequestBody{}
|
req := &api.PostApiSetupKeysJSONRequestBody{}
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
err = json.NewDecoder(r.Body).Decode(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -95,7 +59,13 @@ func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.R
|
|||||||
|
|
||||||
expiresIn := time.Duration(req.ExpiresIn) * time.Second
|
expiresIn := time.Duration(req.ExpiresIn) * time.Second
|
||||||
|
|
||||||
setupKey, err := h.accountManager.AddSetupKey(accountId, req.Name, server.SetupKeyType(req.Type), expiresIn)
|
if req.AutoGroups == nil {
|
||||||
|
req.AutoGroups = []string{}
|
||||||
|
}
|
||||||
|
// newExpiresIn := time.Duration(req.ExpiresIn) * time.Second
|
||||||
|
// newKey.ExpiresAt = time.Now().Add(newExpiresIn)
|
||||||
|
setupKey, err := h.accountManager.CreateSetupKey(account.Id, req.Name, server.SetupKeyType(req.Type), expiresIn,
|
||||||
|
req.AutoGroups)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStatus, ok := status.FromError(err)
|
errStatus, ok := status.FromError(err)
|
||||||
if ok && errStatus.Code() == codes.NotFound {
|
if ok && errStatus.Code() == codes.NotFound {
|
||||||
@@ -109,7 +79,8 @@ func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.R
|
|||||||
writeSuccess(w, setupKey)
|
writeSuccess(w, setupKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) {
|
// GetSetupKeyHandler is a GET request to get a SetupKey by ID
|
||||||
|
func (h *SetupKeys) GetSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
@@ -118,25 +89,84 @@ func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
keyId := vars["id"]
|
keyID := vars["id"]
|
||||||
if len(keyId) == 0 {
|
if len(keyID) == 0 {
|
||||||
http.Error(w, "invalid key Id", http.StatusBadRequest)
|
http.Error(w, "invalid key Id", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Method {
|
key, err := h.accountManager.GetSetupKey(account.Id, keyID)
|
||||||
case http.MethodPut:
|
if err != nil {
|
||||||
h.updateKey(account.Id, keyId, w, r)
|
errStatus, ok := status.FromError(err)
|
||||||
|
if ok && errStatus.Code() == codes.NotFound {
|
||||||
|
http.Error(w, fmt.Sprintf("setup key %s not found under account %s", keyID, account.Id), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Errorf("failed getting setup key %s under account %s %v", keyID, account.Id, err)
|
||||||
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
case http.MethodGet:
|
|
||||||
h.getKey(account.Id, keyId, w, r)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
http.Error(w, "", http.StatusNotFound)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeSuccess(w, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) {
|
// UpdateSetupKeyHandler is a PUT request to update server.SetupKey
|
||||||
|
func (h *SetupKeys) UpdateSetupKeyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
keyID := vars["id"]
|
||||||
|
if len(keyID) == 0 {
|
||||||
|
http.Error(w, "invalid key Id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &api.PutApiSetupKeysIdJSONRequestBody{}
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
http.Error(w, fmt.Sprintf("setup key name field is invalid: %s", req.Name), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.AutoGroups == nil {
|
||||||
|
http.Error(w, fmt.Sprintf("setup key AutoGroups field is invalid: %s", req.AutoGroups), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newKey := &server.SetupKey{}
|
||||||
|
newKey.AutoGroups = req.AutoGroups
|
||||||
|
newKey.Revoked = req.Revoked
|
||||||
|
newKey.Name = req.Name
|
||||||
|
newKey.Id = keyID
|
||||||
|
|
||||||
|
newKey, err = h.accountManager.SaveSetupKey(account.Id, newKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if e, ok := status.FromError(err); ok {
|
||||||
|
switch e.Code() {
|
||||||
|
case codes.NotFound:
|
||||||
|
http.Error(w, fmt.Sprintf("couldn't find setup key for ID %s", keyID), http.StatusNotFound)
|
||||||
|
default:
|
||||||
|
http.Error(w, "failed updating setup key", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeSuccess(w, newKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllSetupKeysHandler is a GET request that returns a list of SetupKey
|
||||||
|
func (h *SetupKeys) GetAllSetupKeysHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -145,28 +175,18 @@ func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Method {
|
setupKeys, err := h.accountManager.ListSetupKeys(account.Id)
|
||||||
case http.MethodPost:
|
if err != nil {
|
||||||
h.createKey(account.Id, w, r)
|
log.Error(err)
|
||||||
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
case http.MethodGet:
|
|
||||||
w.WriteHeader(200)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
respBody := []*api.SetupKey{}
|
|
||||||
for _, key := range account.SetupKeys {
|
|
||||||
respBody = append(respBody, toResponseBody(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(respBody)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed encoding account peers %s: %v", account.Id, err)
|
|
||||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
http.Error(w, "", http.StatusNotFound)
|
|
||||||
}
|
}
|
||||||
|
apiSetupKeys := make([]*api.SetupKey, 0)
|
||||||
|
for _, key := range setupKeys {
|
||||||
|
apiSetupKeys = append(apiSetupKeys, toResponseBody(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSONObject(w, apiSetupKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeSuccess(w http.ResponseWriter, key *server.SetupKey) {
|
func writeSuccess(w http.ResponseWriter, key *server.SetupKey) {
|
||||||
@@ -190,16 +210,19 @@ func toResponseBody(key *server.SetupKey) *api.SetupKey {
|
|||||||
} else {
|
} else {
|
||||||
state = "valid"
|
state = "valid"
|
||||||
}
|
}
|
||||||
|
|
||||||
return &api.SetupKey{
|
return &api.SetupKey{
|
||||||
Id: key.Id,
|
Id: key.Id,
|
||||||
Key: key.Key,
|
Key: key.Key,
|
||||||
Name: key.Name,
|
Name: key.Name,
|
||||||
Expires: key.ExpiresAt,
|
Expires: key.ExpiresAt,
|
||||||
Type: string(key.Type),
|
Type: string(key.Type),
|
||||||
Valid: key.IsValid(),
|
Valid: key.IsValid(),
|
||||||
Revoked: key.Revoked,
|
Revoked: key.Revoked,
|
||||||
UsedTimes: key.UsedTimes,
|
UsedTimes: key.UsedTimes,
|
||||||
LastUsed: key.LastUsed,
|
LastUsed: key.LastUsed,
|
||||||
State: state,
|
State: state,
|
||||||
|
AutoGroups: key.AutoGroups,
|
||||||
|
UpdatedAt: key.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
222
management/server/http/setupkeys_test.go
Normal file
222
management/server/http/setupkeys_test.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/netbirdio/netbird/management/server/http/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server"
|
||||||
|
"github.com/netbirdio/netbird/management/server/mock_server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
existingSetupKeyID = "existingSetupKeyID"
|
||||||
|
newSetupKeyName = "New Setup Key"
|
||||||
|
updatedSetupKeyName = "KKKey"
|
||||||
|
notFoundSetupKeyID = "notFoundSetupKeyID"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initSetupKeysTestMetaData(defaultKey *server.SetupKey, newKey *server.SetupKey, updatedSetupKey *server.SetupKey) *SetupKeys {
|
||||||
|
return &SetupKeys{
|
||||||
|
accountManager: &mock_server.MockAccountManager{
|
||||||
|
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||||
|
return &server.Account{
|
||||||
|
Id: testAccountID,
|
||||||
|
Domain: "hotmail.com",
|
||||||
|
SetupKeys: map[string]*server.SetupKey{
|
||||||
|
defaultKey.Key: defaultKey,
|
||||||
|
},
|
||||||
|
Groups: map[string]*server.Group{
|
||||||
|
"group-1": {ID: "group-1", Peers: []string{"A", "B"}},
|
||||||
|
"id-all": {ID: "id-all", Name: "All"}},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
CreateSetupKeyFunc: func(_ string, keyName string, typ server.SetupKeyType, _ time.Duration, _ []string) (*server.SetupKey, error) {
|
||||||
|
if keyName == newKey.Name || typ != newKey.Type {
|
||||||
|
return newKey, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed creating setup key")
|
||||||
|
},
|
||||||
|
GetSetupKeyFunc: func(accountID string, keyID string) (*server.SetupKey, error) {
|
||||||
|
switch keyID {
|
||||||
|
case defaultKey.Id:
|
||||||
|
return defaultKey, nil
|
||||||
|
case newKey.Id:
|
||||||
|
return newKey, nil
|
||||||
|
default:
|
||||||
|
return nil, status.Errorf(codes.NotFound, "key %s not found", keyID)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
SaveSetupKeyFunc: func(accountID string, key *server.SetupKey) (*server.SetupKey, error) {
|
||||||
|
if key.Id == updatedSetupKey.Id {
|
||||||
|
return updatedSetupKey, nil
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.NotFound, "key %s not found", key.Id)
|
||||||
|
},
|
||||||
|
|
||||||
|
ListSetupKeysFunc: func(accountID string) ([]*server.SetupKey, error) {
|
||||||
|
return []*server.SetupKey{defaultKey}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authAudience: "",
|
||||||
|
jwtExtractor: jwtclaims.ClaimsExtractor{
|
||||||
|
ExtractClaimsFromRequestContext: func(r *http.Request, authAudience string) jwtclaims.AuthorizationClaims {
|
||||||
|
return jwtclaims.AuthorizationClaims{
|
||||||
|
UserId: "test_user",
|
||||||
|
Domain: "hotmail.com",
|
||||||
|
AccountId: testAccountID,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupKeysHandlers(t *testing.T) {
|
||||||
|
defaultSetupKey := server.GenerateDefaultSetupKey()
|
||||||
|
defaultSetupKey.Id = existingSetupKeyID
|
||||||
|
|
||||||
|
newSetupKey := server.GenerateSetupKey(newSetupKeyName, server.SetupKeyReusable, 0, []string{"group-1"})
|
||||||
|
updatedDefaultSetupKey := defaultSetupKey.Copy()
|
||||||
|
updatedDefaultSetupKey.AutoGroups = []string{"group-1"}
|
||||||
|
updatedDefaultSetupKey.Name = updatedSetupKeyName
|
||||||
|
updatedDefaultSetupKey.Revoked = true
|
||||||
|
|
||||||
|
tt := []struct {
|
||||||
|
name string
|
||||||
|
requestType string
|
||||||
|
requestPath string
|
||||||
|
requestBody io.Reader
|
||||||
|
expectedStatus int
|
||||||
|
expectedBody bool
|
||||||
|
expectedSetupKey *api.SetupKey
|
||||||
|
expectedSetupKeys []*api.SetupKey
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Get Setup Keys",
|
||||||
|
requestType: http.MethodGet,
|
||||||
|
requestPath: "/api/setup-keys",
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedBody: true,
|
||||||
|
expectedSetupKeys: []*api.SetupKey{toResponseBody(defaultSetupKey)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Get Existing Setup Key",
|
||||||
|
requestType: http.MethodGet,
|
||||||
|
requestPath: "/api/setup-keys/" + existingSetupKeyID,
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedBody: true,
|
||||||
|
expectedSetupKey: toResponseBody(defaultSetupKey),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Get Not Existing Setup Key",
|
||||||
|
requestType: http.MethodGet,
|
||||||
|
requestPath: "/api/setup-keys/" + notFoundSetupKeyID,
|
||||||
|
expectedStatus: http.StatusNotFound,
|
||||||
|
expectedBody: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Create Setup Key",
|
||||||
|
requestType: http.MethodPost,
|
||||||
|
requestPath: "/api/setup-keys",
|
||||||
|
requestBody: bytes.NewBuffer(
|
||||||
|
[]byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"%s\"}", newSetupKey.Name, newSetupKey.Type))),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedBody: true,
|
||||||
|
expectedSetupKey: toResponseBody(newSetupKey),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update Setup Key",
|
||||||
|
requestType: http.MethodPut,
|
||||||
|
requestPath: "/api/setup-keys/" + defaultSetupKey.Id,
|
||||||
|
requestBody: bytes.NewBuffer(
|
||||||
|
[]byte(fmt.Sprintf("{\"name\":\"%s\",\"auto_groups\":[\"%s\"], \"revoked\":%v}",
|
||||||
|
updatedDefaultSetupKey.Type,
|
||||||
|
updatedDefaultSetupKey.AutoGroups[0],
|
||||||
|
updatedDefaultSetupKey.Revoked,
|
||||||
|
))),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedBody: true,
|
||||||
|
expectedSetupKey: toResponseBody(updatedDefaultSetupKey),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := initSetupKeysTestMetaData(defaultSetupKey, newSetupKey, updatedDefaultSetupKey)
|
||||||
|
|
||||||
|
for _, tc := range tt {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
||||||
|
|
||||||
|
router := mux.NewRouter()
|
||||||
|
router.HandleFunc("/api/setup-keys", handler.GetAllSetupKeysHandler).Methods("GET", "OPTIONS")
|
||||||
|
router.HandleFunc("/api/setup-keys", handler.CreateSetupKeyHandler).Methods("POST", "OPTIONS")
|
||||||
|
router.HandleFunc("/api/setup-keys/{id}", handler.GetSetupKeyHandler).Methods("GET", "OPTIONS")
|
||||||
|
router.HandleFunc("/api/setup-keys/{id}", handler.UpdateSetupKeyHandler).Methods("PUT", "OPTIONS")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
res := recorder.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
content, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("I don't know what I expected; %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status := recorder.Code; status != tc.expectedStatus {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v, content: %s",
|
||||||
|
status, tc.expectedStatus, string(content))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectedBody {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedSetupKey != nil {
|
||||||
|
got := &api.SetupKey{}
|
||||||
|
if err = json.Unmarshal(content, &got); err != nil {
|
||||||
|
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||||
|
}
|
||||||
|
assertKeys(t, got, tc.expectedSetupKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tc.expectedSetupKeys) > 0 {
|
||||||
|
var got []*api.SetupKey
|
||||||
|
if err = json.Unmarshal(content, &got); err != nil {
|
||||||
|
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||||
|
}
|
||||||
|
assertKeys(t, got[0], tc.expectedSetupKeys[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertKeys(t *testing.T, got *api.SetupKey, expected *api.SetupKey) {
|
||||||
|
// this comparison is done manually because when converting to JSON dates formatted differently
|
||||||
|
// assert.Equal(t, got.UpdatedAt, tc.expectedSetupKey.UpdatedAt) //doesn't work
|
||||||
|
assert.WithinDurationf(t, got.UpdatedAt, expected.UpdatedAt, 0, "")
|
||||||
|
assert.WithinDurationf(t, got.Expires, expected.Expires, 0, "")
|
||||||
|
assert.Equal(t, got.Name, expected.Name)
|
||||||
|
assert.Equal(t, got.Id, expected.Id)
|
||||||
|
assert.Equal(t, got.Key, expected.Key)
|
||||||
|
assert.Equal(t, got.Type, expected.Type)
|
||||||
|
assert.Equal(t, got.UsedTimes, expected.UsedTimes)
|
||||||
|
assert.Equal(t, got.Revoked, expected.Revoked)
|
||||||
|
assert.ElementsMatch(t, got.AutoGroups, expected.AutoGroups)
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
"github.com/netbirdio/netbird/management/server/http/api"
|
"github.com/netbirdio/netbird/management/server/http/api"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server"
|
"github.com/netbirdio/netbird/management/server"
|
||||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||||
@@ -24,6 +28,103 @@ func NewUserHandler(accountManager server.AccountManager, authAudience string) *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateUser is a PUT requests to update User data
|
||||||
|
func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPut {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
userID := vars["id"]
|
||||||
|
if len(userID) == 0 {
|
||||||
|
http.Error(w, "invalid user ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &api.PutApiUsersIdJSONRequestBody{}
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userRole := server.StrRoleToUserRole(req.Role)
|
||||||
|
if userRole == server.UserRoleUnknown {
|
||||||
|
http.Error(w, "invalid user role", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newUser, err := h.accountManager.SaveUser(account.Id, &server.User{
|
||||||
|
Id: userID,
|
||||||
|
Role: userRole,
|
||||||
|
AutoGroups: req.AutoGroups,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if e, ok := status.FromError(err); ok {
|
||||||
|
switch e.Code() {
|
||||||
|
case codes.NotFound:
|
||||||
|
http.Error(w, fmt.Sprintf("couldn't find a user for ID %s", userID), http.StatusNotFound)
|
||||||
|
default:
|
||||||
|
http.Error(w, "failed to update user", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSONObject(w, toUserResponse(newUser))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUserHandler creates a User in the system with a status "invited" (effectively this is a user invite).
|
||||||
|
func (h *UserHandler) CreateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &api.PostApiUsersJSONRequestBody{}
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if server.StrRoleToUserRole(req.Role) == server.UserRoleUnknown {
|
||||||
|
http.Error(w, "unknown user role "+req.Role, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newUser, err := h.accountManager.CreateUser(account.Id, &server.UserInfo{
|
||||||
|
Email: req.Email,
|
||||||
|
Name: *req.Name,
|
||||||
|
Role: req.Role,
|
||||||
|
AutoGroups: req.AutoGroups,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if e, ok := server.FromError(err); ok {
|
||||||
|
switch e.Type() {
|
||||||
|
case server.UserAlreadyExists:
|
||||||
|
http.Error(w, "You can't invite users with an existing NetBird account.", http.StatusPreconditionFailed)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http.Error(w, "failed to invite", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSONObject(w, toUserResponse(newUser))
|
||||||
|
}
|
||||||
|
|
||||||
// GetUsers returns a list of users of the account this user belongs to.
|
// GetUsers returns a list of users of the account this user belongs to.
|
||||||
// It also gathers additional user data (like email and name) from the IDP manager.
|
// It also gathers additional user data (like email and name) from the IDP manager.
|
||||||
func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
|
func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -43,7 +144,7 @@ func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
users := []*api.User{}
|
users := make([]*api.User, 0)
|
||||||
for _, r := range data {
|
for _, r := range data {
|
||||||
users = append(users, toUserResponse(r))
|
users = append(users, toUserResponse(r))
|
||||||
}
|
}
|
||||||
@@ -52,10 +153,28 @@ func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func toUserResponse(user *server.UserInfo) *api.User {
|
func toUserResponse(user *server.UserInfo) *api.User {
|
||||||
|
|
||||||
|
autoGroups := user.AutoGroups
|
||||||
|
if autoGroups == nil {
|
||||||
|
autoGroups = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var userStatus api.UserStatus
|
||||||
|
switch user.Status {
|
||||||
|
case "active":
|
||||||
|
userStatus = api.UserStatusActive
|
||||||
|
case "invited":
|
||||||
|
userStatus = api.UserStatusInvited
|
||||||
|
default:
|
||||||
|
userStatus = api.UserStatusDisabled
|
||||||
|
}
|
||||||
|
|
||||||
return &api.User{
|
return &api.User{
|
||||||
Id: user.ID,
|
Id: user.ID,
|
||||||
Name: user.Name,
|
Name: user.Name,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
|
AutoGroups: autoGroups,
|
||||||
|
Status: userStatus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
func initUsers(user ...*server.User) *UserHandler {
|
func initUsers(user ...*server.User) *UserHandler {
|
||||||
return &UserHandler{
|
return &UserHandler{
|
||||||
accountManager: &mock_server.MockAccountManager{
|
accountManager: &mock_server.MockAccountManager{
|
||||||
GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||||
users := make(map[string]*server.User, 0)
|
users := make(map[string]*server.User, 0)
|
||||||
for _, u := range user {
|
for _, u := range user {
|
||||||
users[u.Id] = u
|
users[u.Id] = u
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/netbirdio/netbird/management/server"
|
"github.com/netbirdio/netbird/management/server"
|
||||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
//writeJSONObject simply writes object to the HTTP reponse in JSON format
|
// writeJSONObject simply writes object to the HTTP reponse in JSON format
|
||||||
func writeJSONObject(w http.ResponseWriter, obj interface{}) {
|
func writeJSONObject(w http.ResponseWriter, obj interface{}) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
||||||
@@ -21,7 +24,7 @@ func writeJSONObject(w http.ResponseWriter, obj interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Duration is used strictly for JSON requests/responses due to duration marshalling issues
|
// Duration is used strictly for JSON requests/responses due to duration marshalling issues
|
||||||
type Duration struct {
|
type Duration struct {
|
||||||
time.Duration
|
time.Duration
|
||||||
}
|
}
|
||||||
@@ -57,10 +60,32 @@ func getJWTAccount(accountManager server.AccountManager,
|
|||||||
|
|
||||||
jwtClaims := jwtExtractor.ExtractClaimsFromRequestContext(r, authAudience)
|
jwtClaims := jwtExtractor.ExtractClaimsFromRequestContext(r, authAudience)
|
||||||
|
|
||||||
account, err := accountManager.GetAccountWithAuthorizationClaims(jwtClaims)
|
account, err := accountManager.GetAccountFromToken(jwtClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
|
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toHTTPError(err error, w http.ResponseWriter) {
|
||||||
|
errStatus, ok := status.FromError(err)
|
||||||
|
if ok && errStatus.Code() == codes.Internal {
|
||||||
|
http.Error(w, errStatus.String(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok && errStatus.Code() == codes.NotFound {
|
||||||
|
http.Error(w, errStatus.String(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok && errStatus.Code() == codes.InvalidArgument {
|
||||||
|
http.Error(w, errStatus.String(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unhandledMSG := fmt.Sprintf("got unhandled error code, error: %s", errStatus.String())
|
||||||
|
log.Error(unhandledMSG)
|
||||||
|
http.Error(w, unhandledMSG, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -54,6 +53,16 @@ type Auth0Credentials struct {
|
|||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createUserRequest is a user create request
|
||||||
|
type createUserRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
AppMeta AppMetadata `json:"app_metadata"`
|
||||||
|
Connection string `json:"connection"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
VerifyEmail bool `json:"verify_email"`
|
||||||
|
}
|
||||||
|
|
||||||
// userExportJobRequest is a user export request struct
|
// userExportJobRequest is a user export request struct
|
||||||
type userExportJobRequest struct {
|
type userExportJobRequest struct {
|
||||||
Format string `json:"format"`
|
Format string `json:"format"`
|
||||||
@@ -87,12 +96,13 @@ type userExportJobStatusResponse struct {
|
|||||||
|
|
||||||
// auth0Profile represents an Auth0 user profile response
|
// auth0Profile represents an Auth0 user profile response
|
||||||
type auth0Profile struct {
|
type auth0Profile struct {
|
||||||
AccountID string `json:"wt_account_id"`
|
AccountID string `json:"wt_account_id"`
|
||||||
UserID string `json:"user_id"`
|
PendingInvite bool `json:"wt_pending_invite"`
|
||||||
Name string `json:"name"`
|
UserID string `json:"user_id"`
|
||||||
Email string `json:"email"`
|
Name string `json:"name"`
|
||||||
CreatedAt string `json:"created_at"`
|
Email string `json:"email"`
|
||||||
LastLogin string `json:"last_login"`
|
CreatedAt string `json:"created_at"`
|
||||||
|
LastLogin string `json:"last_login"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuth0Manager creates a new instance of the Auth0Manager
|
// NewAuth0Manager creates a new instance of the Auth0Manager
|
||||||
@@ -172,7 +182,7 @@ func (c *Auth0Credentials) requestJWTToken() (*http.Response, error) {
|
|||||||
// parseRequestJWTResponse parses jwt raw response body and extracts token and expires in seconds
|
// parseRequestJWTResponse parses jwt raw response body and extracts token and expires in seconds
|
||||||
func (c *Auth0Credentials) parseRequestJWTResponse(rawBody io.ReadCloser) (JWTToken, error) {
|
func (c *Auth0Credentials) parseRequestJWTResponse(rawBody io.ReadCloser) (JWTToken, error) {
|
||||||
jwtToken := JWTToken{}
|
jwtToken := JWTToken{}
|
||||||
body, err := ioutil.ReadAll(rawBody)
|
body, err := io.ReadAll(rawBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return jwtToken, err
|
return jwtToken, err
|
||||||
}
|
}
|
||||||
@@ -230,7 +240,7 @@ func (c *Auth0Credentials) Authenticate() (JWTToken, error) {
|
|||||||
return c.jwtToken, nil
|
return c.jwtToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func batchRequestUsersURL(authIssuer, accountID string, page int) (string, url.Values, error) {
|
func batchRequestUsersURL(authIssuer, accountID string, page int, perPage int) (string, url.Values, error) {
|
||||||
u, err := url.Parse(authIssuer + "/api/v2/users")
|
u, err := url.Parse(authIssuer + "/api/v2/users")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
@@ -238,6 +248,7 @@ func batchRequestUsersURL(authIssuer, accountID string, page int) (string, url.V
|
|||||||
q := u.Query()
|
q := u.Query()
|
||||||
q.Set("page", strconv.Itoa(page))
|
q.Set("page", strconv.Itoa(page))
|
||||||
q.Set("search_engine", "v3")
|
q.Set("search_engine", "v3")
|
||||||
|
q.Set("per_page", strconv.Itoa(perPage))
|
||||||
q.Set("q", "app_metadata.wt_account_id:"+accountID)
|
q.Set("q", "app_metadata.wt_account_id:"+accountID)
|
||||||
u.RawQuery = q.Encode()
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
@@ -259,8 +270,9 @@ func (am *Auth0Manager) GetAccount(accountID string) ([]*UserData, error) {
|
|||||||
|
|
||||||
// https://auth0.com/docs/manage-users/user-search/retrieve-users-with-get-users-endpoint#limitations
|
// https://auth0.com/docs/manage-users/user-search/retrieve-users-with-get-users-endpoint#limitations
|
||||||
// auth0 limitation of 1000 users via this endpoint
|
// auth0 limitation of 1000 users via this endpoint
|
||||||
|
resultsPerPage := 50
|
||||||
for page := 0; page < 20; page++ {
|
for page := 0; page < 20; page++ {
|
||||||
reqURL, query, err := batchRequestUsersURL(am.authIssuer, accountID, page)
|
reqURL, query, err := batchRequestUsersURL(am.authIssuer, accountID, page, resultsPerPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -283,30 +295,31 @@ func (am *Auth0Manager) GetAccount(accountID string) ([]*UserData, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("failed requesting user data from IdP %s", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
var batch []UserData
|
var batch []UserData
|
||||||
err = json.Unmarshal(body, &batch)
|
err = json.Unmarshal(body, &batch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("requested batch; %v", batch)
|
log.Debugf("returned user batch for accountID %s on page %d, %v", accountID, page, batch)
|
||||||
|
|
||||||
err = res.Body.Close()
|
err = res.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
return nil, fmt.Errorf("unable to request UserData from auth0, statusCode %d", res.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(batch) == 0 {
|
|
||||||
return list, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for user := range batch {
|
for user := range batch {
|
||||||
list = append(list, &batch[user])
|
list = append(list, &batch[user])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(batch) == 0 || len(batch) < resultsPerPage {
|
||||||
|
log.Debugf("finished loading users for accountID %s", accountID)
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return list, nil
|
return list, nil
|
||||||
@@ -367,14 +380,12 @@ func (am *Auth0Manager) UpdateUserAppMetadata(userID string, appMetadata AppMeta
|
|||||||
|
|
||||||
reqURL := am.authIssuer + "/api/v2/users/" + userID
|
reqURL := am.authIssuer + "/api/v2/users/" + userID
|
||||||
|
|
||||||
data, err := am.helper.Marshal(appMetadata)
|
data, err := am.helper.Marshal(map[string]any{"app_metadata": appMetadata})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
payloadString := fmt.Sprintf("{\"app_metadata\": %s}", string(data))
|
payload := strings.NewReader(string(data))
|
||||||
|
|
||||||
payload := strings.NewReader(payloadString)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("PATCH", reqURL, payload)
|
req, err := http.NewRequest("PATCH", reqURL, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -383,7 +394,7 @@ func (am *Auth0Manager) UpdateUserAppMetadata(userID string, appMetadata AppMeta
|
|||||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||||
req.Header.Add("content-type", "application/json")
|
req.Header.Add("content-type", "application/json")
|
||||||
|
|
||||||
log.Debugf("updating metadata for user %s", userID)
|
log.Debugf("updating IdP metadata for user %s", userID)
|
||||||
|
|
||||||
res, err := am.httpClient.Do(req)
|
res, err := am.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -404,6 +415,27 @@ func (am *Auth0Manager) UpdateUserAppMetadata(userID string, appMetadata AppMeta
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildCreateUserRequestPayload(email string, name string, accountID string) (string, error) {
|
||||||
|
req := &createUserRequest{
|
||||||
|
Email: email,
|
||||||
|
Name: name,
|
||||||
|
AppMeta: AppMetadata{
|
||||||
|
WTAccountID: accountID,
|
||||||
|
WTPendingInvite: true,
|
||||||
|
},
|
||||||
|
Connection: "Username-Password-Authentication",
|
||||||
|
Password: GeneratePassword(8, 1, 1, 1),
|
||||||
|
VerifyEmail: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
str, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(str), nil
|
||||||
|
}
|
||||||
|
|
||||||
func buildUserExportRequest() (string, error) {
|
func buildUserExportRequest() (string, error) {
|
||||||
req := &userExportJobRequest{}
|
req := &userExportJobRequest{}
|
||||||
fields := make([]map[string]string, 0)
|
fields := make([]map[string]string, 0)
|
||||||
@@ -417,6 +449,11 @@ func buildUserExportRequest() (string, error) {
|
|||||||
"export_as": "wt_account_id",
|
"export_as": "wt_account_id",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fields = append(fields, map[string]string{
|
||||||
|
"name": "app_metadata.wt_pending_invite",
|
||||||
|
"export_as": "wt_pending_invite",
|
||||||
|
})
|
||||||
|
|
||||||
req.Format = "json"
|
req.Format = "json"
|
||||||
req.Fields = fields
|
req.Fields = fields
|
||||||
|
|
||||||
@@ -428,28 +465,39 @@ func buildUserExportRequest() (string, error) {
|
|||||||
return string(str), nil
|
return string(str), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
func (am *Auth0Manager) createPostRequest(endpoint string, payloadStr string) (*http.Request, error) {
|
||||||
// It returns a list of users indexed by accountID.
|
|
||||||
func (am *Auth0Manager) GetAllAccounts() (map[string][]*UserData, error) {
|
|
||||||
jwtToken, err := am.credentials.Authenticate()
|
jwtToken, err := am.credentials.Authenticate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reqURL := am.authIssuer + "/api/v2/jobs/users-exports"
|
reqURL := am.authIssuer + endpoint
|
||||||
|
|
||||||
|
payload := strings.NewReader(payloadStr)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", reqURL, payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||||
|
req.Header.Add("content-type", "application/json")
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||||
|
// It returns a list of users indexed by accountID.
|
||||||
|
func (am *Auth0Manager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||||
payloadString, err := buildUserExportRequest()
|
payloadString, err := buildUserExportRequest()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
payload := strings.NewReader(payloadString)
|
|
||||||
|
|
||||||
exportJobReq, err := http.NewRequest("POST", reqURL, payload)
|
exportJobReq, err := am.createPostRequest("/api/v2/jobs/users-exports", payloadString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
exportJobReq.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
|
||||||
exportJobReq.Header.Add("content-type", "application/json")
|
|
||||||
|
|
||||||
jobResp, err := am.httpClient.Do(exportJobReq)
|
jobResp, err := am.httpClient.Do(exportJobReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -469,7 +517,7 @@ func (am *Auth0Manager) GetAllAccounts() (map[string][]*UserData, error) {
|
|||||||
|
|
||||||
var exportJobResp userExportJobResponse
|
var exportJobResp userExportJobResponse
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(jobResp.Body)
|
body, err := io.ReadAll(jobResp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("Coudln't read export job response; %v", err)
|
log.Debugf("Coudln't read export job response; %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -500,6 +548,82 @@ func (am *Auth0Manager) GetAllAccounts() (map[string][]*UserData, error) {
|
|||||||
return nil, fmt.Errorf("failed extracting user profiles from auth0")
|
return nil, fmt.Errorf("failed extracting user profiles from auth0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserByEmail searches users with a given email. If no users have been found, this function returns an empty list.
|
||||||
|
// This function can return multiple users. This is due to the Auth0 internals - there could be multiple users with
|
||||||
|
// the same email but different connections that are considered as separate accounts (e.g., Google and username/password).
|
||||||
|
func (am *Auth0Manager) GetUserByEmail(email string) ([]*UserData, error) {
|
||||||
|
jwtToken, err := am.credentials.Authenticate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reqURL := am.authIssuer + "/api/v2/users-by-email?email=" + email
|
||||||
|
body, err := doGetReq(am.httpClient, reqURL, jwtToken.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userResp := []*UserData{}
|
||||||
|
|
||||||
|
err = am.helper.Unmarshal(body, &userResp)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Coudln't unmarshal export job response; %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser creates a new user in Auth0 Idp and sends an invite
|
||||||
|
func (am *Auth0Manager) CreateUser(email string, name string, accountID string) (*UserData, error) {
|
||||||
|
|
||||||
|
payloadString, err := buildCreateUserRequestPayload(email, name, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err := am.createPostRequest("/api/v2/users", payloadString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := am.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Couldn't get job response %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while closing create user response body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if !(resp.StatusCode == 200 || resp.StatusCode == 201) {
|
||||||
|
return nil, fmt.Errorf("unable to create user, statusCode %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var createResp UserData
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Coudln't read export job response; %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.helper.Unmarshal(body, &createResp)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Coudln't unmarshal export job response; %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if createResp.ID == "" {
|
||||||
|
return nil, fmt.Errorf("couldn't create user: response %v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("created user %s in account %s", createResp.ID, accountID)
|
||||||
|
|
||||||
|
return &createResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// checkExportJobStatus checks the status of the job created at CreateExportUsersJob.
|
// checkExportJobStatus checks the status of the job created at CreateExportUsersJob.
|
||||||
// If the status is "completed", then return the downloadLink
|
// If the status is "completed", then return the downloadLink
|
||||||
func (am *Auth0Manager) checkExportJobStatus(jobID string) (bool, string, error) {
|
func (am *Auth0Manager) checkExportJobStatus(jobID string) (bool, string, error) {
|
||||||
@@ -572,6 +696,10 @@ func (am *Auth0Manager) downloadProfileExport(location string) (map[string][]*Us
|
|||||||
ID: profile.UserID,
|
ID: profile.UserID,
|
||||||
Name: profile.Name,
|
Name: profile.Name,
|
||||||
Email: profile.Email,
|
Email: profile.Email,
|
||||||
|
AppMetadata: AppMetadata{
|
||||||
|
WTAccountID: profile.AccountID,
|
||||||
|
WTPendingInvite: profile.PendingInvite,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -605,7 +733,7 @@ func doGetReq(client ManagerHTTPClient, url, accessToken string) ([]byte, error)
|
|||||||
return nil, fmt.Errorf("unable to get %s, statusCode %d", url, res.StatusCode)
|
return nil, fmt.Errorf("unable to get %s, statusCode %d", url, res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -22,13 +22,13 @@ type mockHTTPClient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||||
body, err := ioutil.ReadAll(req.Body)
|
body, err := io.ReadAll(req.Body)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.reqBody = string(body)
|
c.reqBody = string(body)
|
||||||
}
|
}
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
StatusCode: c.code,
|
StatusCode: c.code,
|
||||||
Body: ioutil.NopCloser(strings.NewReader(c.resBody)),
|
Body: io.NopCloser(strings.NewReader(c.resBody)),
|
||||||
}, c.err
|
}, c.err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ func TestAuth0_RequestJWTToken(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
assert.NoError(t, err, "unable to read the response body")
|
assert.NoError(t, err, "unable to read the response body")
|
||||||
|
|
||||||
jwtToken := JWTToken{}
|
jwtToken := JWTToken{}
|
||||||
@@ -178,7 +178,7 @@ func TestAuth0_ParseRequestJWTResponse(t *testing.T) {
|
|||||||
for _, testCase := range []parseRequestJWTResponseTest{parseRequestJWTResponseTestCase1, parseRequestJWTResponseTestCase2} {
|
for _, testCase := range []parseRequestJWTResponseTest{parseRequestJWTResponseTestCase1, parseRequestJWTResponseTestCase2} {
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
|
||||||
rawBody := ioutil.NopCloser(strings.NewReader(testCase.inputResBody))
|
rawBody := io.NopCloser(strings.NewReader(testCase.inputResBody))
|
||||||
|
|
||||||
config := Auth0ClientConfig{}
|
config := Auth0ClientConfig{}
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
|
|||||||
|
|
||||||
exp := 15
|
exp := 15
|
||||||
token := newTestJWT(t, exp)
|
token := newTestJWT(t, exp)
|
||||||
appMetadata := AppMetadata{WTAccountId: "ok"}
|
appMetadata := AppMetadata{WTAccountID: "ok"}
|
||||||
|
|
||||||
updateUserAppMetadataTestCase1 := updateUserAppMetadataTest{
|
updateUserAppMetadataTestCase1 := updateUserAppMetadataTest{
|
||||||
name: "Bad Authentication",
|
name: "Bad Authentication",
|
||||||
@@ -340,7 +340,7 @@ func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
|
|||||||
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
|
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
|
||||||
name: "Bad Status Code",
|
name: "Bad Status Code",
|
||||||
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
|
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
|
||||||
expectedReqBody: fmt.Sprintf("{\"app_metadata\": {\"wt_account_id\":\"%s\"}}", appMetadata.WTAccountId),
|
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":false}}", appMetadata.WTAccountID),
|
||||||
appMetadata: appMetadata,
|
appMetadata: appMetadata,
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
helper: JsonParser{},
|
helper: JsonParser{},
|
||||||
@@ -363,7 +363,7 @@ func TestAuth0_UpdateUserAppMetadata(t *testing.T) {
|
|||||||
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
|
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
|
||||||
name: "Good request",
|
name: "Good request",
|
||||||
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
|
inputReqBody: fmt.Sprintf("{\"access_token\":\"%s\",\"scope\":\"read:users\",\"expires_in\":%d,\"token_type\":\"Bearer\"}", token, exp),
|
||||||
expectedReqBody: fmt.Sprintf("{\"app_metadata\": {\"wt_account_id\":\"%s\"}}", appMetadata.WTAccountId),
|
expectedReqBody: fmt.Sprintf("{\"app_metadata\":{\"wt_account_id\":\"%s\",\"wt_pending_invite\":false}}", appMetadata.WTAccountID),
|
||||||
appMetadata: appMetadata,
|
appMetadata: appMetadata,
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
helper: JsonParser{},
|
helper: JsonParser{},
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ type Manager interface {
|
|||||||
GetUserDataByID(userId string, appMetadata AppMetadata) (*UserData, error)
|
GetUserDataByID(userId string, appMetadata AppMetadata) (*UserData, error)
|
||||||
GetAccount(accountId string) ([]*UserData, error)
|
GetAccount(accountId string) ([]*UserData, error)
|
||||||
GetAllAccounts() (map[string][]*UserData, error)
|
GetAllAccounts() (map[string][]*UserData, error)
|
||||||
|
CreateUser(email string, name string, accountID string) (*UserData, error)
|
||||||
|
GetUserByEmail(email string) ([]*UserData, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config an idp configuration struct to be loaded from management server's config file
|
// Config an idp configuration struct to be loaded from management server's config file
|
||||||
@@ -38,16 +40,18 @@ type ManagerHelper interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserData struct {
|
type UserData struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ID string `json:"user_id"`
|
ID string `json:"user_id"`
|
||||||
|
AppMetadata AppMetadata `json:"app_metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppMetadata user app metadata to associate with a profile
|
// AppMetadata user app metadata to associate with a profile
|
||||||
type AppMetadata struct {
|
type AppMetadata struct {
|
||||||
// Wiretrustee account id to update in the IDP
|
// WTAccountID is a NetBird (previously Wiretrustee) account id to update in the IDP
|
||||||
// maps to wt_account_id when json.marshal
|
// maps to wt_account_id when json.marshal
|
||||||
WTAccountId string `json:"wt_account_id"`
|
WTAccountID string `json:"wt_account_id,omitempty"`
|
||||||
|
WTPendingInvite bool `json:"wt_pending_invite"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTToken a JWT object that holds information of a token
|
// JWTToken a JWT object that holds information of a token
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
package idp
|
package idp
|
||||||
|
|
||||||
import "encoding/json"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lowerCharSet = "abcdedfghijklmnopqrst"
|
||||||
|
upperCharSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
specialCharSet = "!@#$%&*"
|
||||||
|
numberSet = "0123456789"
|
||||||
|
allCharSet = lowerCharSet + upperCharSet + specialCharSet + numberSet
|
||||||
|
)
|
||||||
|
|
||||||
type JsonParser struct{}
|
type JsonParser struct{}
|
||||||
|
|
||||||
@@ -11,3 +23,37 @@ func (JsonParser) Marshal(v interface{}) ([]byte, error) {
|
|||||||
func (JsonParser) Unmarshal(data []byte, v interface{}) error {
|
func (JsonParser) Unmarshal(data []byte, v interface{}) error {
|
||||||
return json.Unmarshal(data, v)
|
return json.Unmarshal(data, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GeneratePassword generates user password
|
||||||
|
func GeneratePassword(passwordLength, minSpecialChar, minNum, minUpperCase int) string {
|
||||||
|
var password strings.Builder
|
||||||
|
|
||||||
|
//Set special character
|
||||||
|
for i := 0; i < minSpecialChar; i++ {
|
||||||
|
random := rand.Intn(len(specialCharSet))
|
||||||
|
password.WriteString(string(specialCharSet[random]))
|
||||||
|
}
|
||||||
|
|
||||||
|
//Set numeric
|
||||||
|
for i := 0; i < minNum; i++ {
|
||||||
|
random := rand.Intn(len(numberSet))
|
||||||
|
password.WriteString(string(numberSet[random]))
|
||||||
|
}
|
||||||
|
|
||||||
|
//Set uppercase
|
||||||
|
for i := 0; i < minUpperCase; i++ {
|
||||||
|
random := rand.Intn(len(upperCharSet))
|
||||||
|
password.WriteString(string(upperCharSet[random]))
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingLength := passwordLength - minSpecialChar - minNum - minUpperCase
|
||||||
|
for i := 0; i < remainingLength; i++ {
|
||||||
|
random := rand.Intn(len(allCharSet))
|
||||||
|
password.WriteString(string(allCharSet[random]))
|
||||||
|
}
|
||||||
|
inRune := []rune(password.String())
|
||||||
|
rand.Shuffle(len(inRune), func(i, j int) {
|
||||||
|
inRune[i], inRune[j] = inRune[j], inRune[i]
|
||||||
|
})
|
||||||
|
return string(inRune)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package server_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@@ -45,7 +44,7 @@ var _ = Describe("Management service", func() {
|
|||||||
level, _ := log.ParseLevel("Debug")
|
level, _ := log.ParseLevel("Debug")
|
||||||
log.SetLevel(level)
|
log.SetLevel(level)
|
||||||
var err error
|
var err error
|
||||||
dataDir, err = ioutil.TempDir("", "wiretrustee_mgmt_test_tmp_*")
|
dataDir, err = os.MkdirTemp("", "wiretrustee_mgmt_test_tmp_*")
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
err = util.CopyFileContents("testdata/store.json", filepath.Join(dataDir, "store.json"))
|
err = util.CopyFileContents("testdata/store.json", filepath.Join(dataDir, "store.json"))
|
||||||
|
|||||||
283
management/server/metrics/metrics.go
Normal file
283
management/server/metrics/metrics.go
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
// Package metrics gather anonymous information about the usage of NetBird management
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"github.com/netbirdio/netbird/management/server"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PayloadEvent identifies an event type
|
||||||
|
PayloadEvent = "self-hosted stats"
|
||||||
|
// payloadEndpoint metrics endpoint to send anonymous data
|
||||||
|
payloadEndpoint = "https://metrics.netbird.io"
|
||||||
|
// defaultPushInterval default interval to push metrics
|
||||||
|
defaultPushInterval = 24 * time.Hour
|
||||||
|
// requestTimeout http request timeout
|
||||||
|
requestTimeout = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type getTokenResponse struct {
|
||||||
|
PublicAPIToken string `json:"public_api_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pushPayload struct {
|
||||||
|
APIKey string `json:"api_key"`
|
||||||
|
DistinctID string `json:"distinct_id"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
Properties properties `json:"properties"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// properties metrics to push
|
||||||
|
type properties map[string]interface{}
|
||||||
|
|
||||||
|
// DataSource metric data source
|
||||||
|
type DataSource interface {
|
||||||
|
GetAllAccounts() []*server.Account
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnManager peer connection manager that holds state for current active connections
|
||||||
|
type ConnManager interface {
|
||||||
|
GetAllConnectedPeers() map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker metrics collector and pusher
|
||||||
|
type Worker struct {
|
||||||
|
ctx context.Context
|
||||||
|
id string
|
||||||
|
dataSource DataSource
|
||||||
|
connManager ConnManager
|
||||||
|
startupTime time.Time
|
||||||
|
lastRun time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWorker returns a metrics worker
|
||||||
|
func NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager) *Worker {
|
||||||
|
currentTime := time.Now()
|
||||||
|
return &Worker{
|
||||||
|
ctx: ctx,
|
||||||
|
id: id,
|
||||||
|
dataSource: dataSource,
|
||||||
|
connManager: connManager,
|
||||||
|
startupTime: currentTime,
|
||||||
|
lastRun: currentTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run runs the metrics worker
|
||||||
|
func (w *Worker) Run() {
|
||||||
|
pushTicker := time.NewTicker(defaultPushInterval)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-w.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-pushTicker.C:
|
||||||
|
err := w.sendMetrics()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worker) sendMetrics() error {
|
||||||
|
ctx, cancel := context.WithTimeout(w.ctx, requestTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
apiKey, err := getAPIKey(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := w.generatePayload(apiKey)
|
||||||
|
|
||||||
|
payloadString, err := buildMetricsPayload(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := http.Client{}
|
||||||
|
|
||||||
|
exportJobReq, err := createPostRequest(ctx, payloadEndpoint+"/capture/", payloadString)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create metrics post request %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jobResp, err := httpClient.Do(exportJobReq)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to push metrics %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = jobResp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while closing update metrics response body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if jobResp.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("unable to push anonymous metrics, got statusCode %d", jobResp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("sent anonymous metrics, next push will happen in %s. "+
|
||||||
|
"You can disable these metrics by running with flag --disable-anonymous-metrics,"+
|
||||||
|
" see more information at https://netbird.io/docs/FAQ/metrics-collection", defaultPushInterval)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worker) generatePayload(apiKey string) pushPayload {
|
||||||
|
properties := w.generateProperties()
|
||||||
|
|
||||||
|
return pushPayload{
|
||||||
|
APIKey: apiKey,
|
||||||
|
DistinctID: w.id,
|
||||||
|
Event: PayloadEvent,
|
||||||
|
Properties: properties,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worker) generateProperties() properties {
|
||||||
|
var (
|
||||||
|
uptime float64
|
||||||
|
accounts int
|
||||||
|
users int
|
||||||
|
peers int
|
||||||
|
setupKeysUsage int
|
||||||
|
activePeersLastDay int
|
||||||
|
osPeers map[string]int
|
||||||
|
userPeers int
|
||||||
|
rules int
|
||||||
|
groups int
|
||||||
|
routes int
|
||||||
|
nameservers int
|
||||||
|
version string
|
||||||
|
)
|
||||||
|
start := time.Now()
|
||||||
|
metricsProperties := make(properties)
|
||||||
|
osPeers = make(map[string]int)
|
||||||
|
uptime = time.Since(w.startupTime).Seconds()
|
||||||
|
connections := w.connManager.GetAllConnectedPeers()
|
||||||
|
version = system.NetbirdVersion()
|
||||||
|
|
||||||
|
for _, account := range w.dataSource.GetAllAccounts() {
|
||||||
|
accounts++
|
||||||
|
users = users + len(account.Users)
|
||||||
|
rules = rules + len(account.Rules)
|
||||||
|
groups = groups + len(account.Groups)
|
||||||
|
routes = routes + len(account.Routes)
|
||||||
|
nameservers = nameservers + len(account.NameServerGroups)
|
||||||
|
|
||||||
|
for _, key := range account.SetupKeys {
|
||||||
|
setupKeysUsage = setupKeysUsage + key.UsedTimes
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range account.Peers {
|
||||||
|
peers++
|
||||||
|
if peer.SetupKey != "" {
|
||||||
|
userPeers++
|
||||||
|
}
|
||||||
|
|
||||||
|
_, connected := connections[peer.Key]
|
||||||
|
if connected || peer.Status.LastSeen.After(w.lastRun) {
|
||||||
|
activePeersLastDay++
|
||||||
|
}
|
||||||
|
osKey := strings.ToLower(fmt.Sprintf("peer_os_%s", peer.Meta.GoOS))
|
||||||
|
osCount := osPeers[osKey]
|
||||||
|
osPeers[osKey] = osCount + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsProperties["uptime"] = uptime
|
||||||
|
metricsProperties["accounts"] = accounts
|
||||||
|
metricsProperties["users"] = users
|
||||||
|
metricsProperties["peers"] = peers
|
||||||
|
metricsProperties["setup_keys_usage"] = setupKeysUsage
|
||||||
|
metricsProperties["active_peers_last_day"] = activePeersLastDay
|
||||||
|
metricsProperties["user_peers"] = userPeers
|
||||||
|
metricsProperties["rules"] = rules
|
||||||
|
metricsProperties["groups"] = groups
|
||||||
|
metricsProperties["routes"] = routes
|
||||||
|
metricsProperties["nameservers"] = nameservers
|
||||||
|
metricsProperties["version"] = version
|
||||||
|
|
||||||
|
for os, count := range osPeers {
|
||||||
|
metricsProperties[os] = count
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsProperties["metric_generation_time"] = time.Since(start).Milliseconds()
|
||||||
|
|
||||||
|
return metricsProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAPIKey(ctx context.Context) (string, error) {
|
||||||
|
|
||||||
|
httpClient := http.Client{}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+"/GetToken", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to create request for metrics public api token %v", err)
|
||||||
|
}
|
||||||
|
response, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to request metrics public api token %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = response.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while closing metrics token response body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return "", fmt.Errorf("unable to retrieve metrics token, statusCode %d", response.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("coudln't get metrics token response; %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResponse getTokenResponse
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &tokenResponse)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("coudln't parse metrics public api token; %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse.PublicAPIToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMetricsPayload(payload pushPayload) (string, error) {
|
||||||
|
str, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to marshal metrics payload, got err: %v", err)
|
||||||
|
}
|
||||||
|
return string(str), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, error) {
|
||||||
|
reqURL := endpoint
|
||||||
|
|
||||||
|
payload := strings.NewReader(payloadStr)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", reqURL, payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("content-type", "application/json")
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package mock_server
|
package mock_server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"github.com/netbirdio/netbird/management/server"
|
"github.com/netbirdio/netbird/management/server"
|
||||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
@@ -10,47 +11,56 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MockAccountManager struct {
|
type MockAccountManager struct {
|
||||||
GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error)
|
GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error)
|
||||||
GetAccountByUserFunc func(userId string) (*server.Account, error)
|
GetAccountByUserFunc func(userId string) (*server.Account, error)
|
||||||
AddSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType, expiresIn time.Duration) (*server.SetupKey, error)
|
CreateSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType, expiresIn time.Duration, autoGroups []string) (*server.SetupKey, error)
|
||||||
RevokeSetupKeyFunc func(accountId string, keyId string) (*server.SetupKey, error)
|
GetSetupKeyFunc func(accountID string, keyID string) (*server.SetupKey, error)
|
||||||
RenameSetupKeyFunc func(accountId string, keyId string, newName string) (*server.SetupKey, error)
|
GetAccountByIdFunc func(accountId string) (*server.Account, error)
|
||||||
GetAccountByIdFunc func(accountId string) (*server.Account, error)
|
GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error)
|
||||||
GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error)
|
IsUserAdminFunc func(claims jwtclaims.AuthorizationClaims) (bool, error)
|
||||||
GetAccountWithAuthorizationClaimsFunc func(claims jwtclaims.AuthorizationClaims) (*server.Account, error)
|
AccountExistsFunc func(accountId string) (*bool, error)
|
||||||
IsUserAdminFunc func(claims jwtclaims.AuthorizationClaims) (bool, error)
|
GetPeerFunc func(peerKey string) (*server.Peer, error)
|
||||||
AccountExistsFunc func(accountId string) (*bool, error)
|
MarkPeerConnectedFunc func(peerKey string, connected bool) error
|
||||||
GetPeerFunc func(peerKey string) (*server.Peer, error)
|
RenamePeerFunc func(accountId string, peerKey string, newName string) (*server.Peer, error)
|
||||||
MarkPeerConnectedFunc func(peerKey string, connected bool) error
|
DeletePeerFunc func(accountId string, peerKey string) (*server.Peer, error)
|
||||||
RenamePeerFunc func(accountId string, peerKey string, newName string) (*server.Peer, error)
|
GetPeerByIPFunc func(accountId string, peerIP string) (*server.Peer, error)
|
||||||
DeletePeerFunc func(accountId string, peerKey string) (*server.Peer, error)
|
GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error)
|
||||||
GetPeerByIPFunc func(accountId string, peerIP string) (*server.Peer, error)
|
GetPeerNetworkFunc func(peerKey string) (*server.Network, error)
|
||||||
GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error)
|
AddPeerFunc func(setupKey string, userId string, peer *server.Peer) (*server.Peer, error)
|
||||||
GetPeerNetworkFunc func(peerKey string) (*server.Network, error)
|
GetGroupFunc func(accountID, groupID string) (*server.Group, error)
|
||||||
AddPeerFunc func(setupKey string, userId string, peer *server.Peer) (*server.Peer, error)
|
SaveGroupFunc func(accountID string, group *server.Group) error
|
||||||
GetGroupFunc func(accountID, groupID string) (*server.Group, error)
|
UpdateGroupFunc func(accountID string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error)
|
||||||
SaveGroupFunc func(accountID string, group *server.Group) error
|
DeleteGroupFunc func(accountID, groupID string) error
|
||||||
UpdateGroupFunc func(accountID string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error)
|
ListGroupsFunc func(accountID string) ([]*server.Group, error)
|
||||||
DeleteGroupFunc func(accountID, groupID string) error
|
GroupAddPeerFunc func(accountID, groupID, peerKey string) error
|
||||||
ListGroupsFunc func(accountID string) ([]*server.Group, error)
|
GroupDeletePeerFunc func(accountID, groupID, peerKey string) error
|
||||||
GroupAddPeerFunc func(accountID, groupID, peerKey string) error
|
GroupListPeersFunc func(accountID, groupID string) ([]*server.Peer, error)
|
||||||
GroupDeletePeerFunc func(accountID, groupID, peerKey string) error
|
GetRuleFunc func(accountID, ruleID string) (*server.Rule, error)
|
||||||
GroupListPeersFunc func(accountID, groupID string) ([]*server.Peer, error)
|
SaveRuleFunc func(accountID string, rule *server.Rule) error
|
||||||
GetRuleFunc func(accountID, ruleID string) (*server.Rule, error)
|
UpdateRuleFunc func(accountID string, ruleID string, operations []server.RuleUpdateOperation) (*server.Rule, error)
|
||||||
SaveRuleFunc func(accountID string, rule *server.Rule) error
|
DeleteRuleFunc func(accountID, ruleID string) error
|
||||||
UpdateRuleFunc func(accountID string, ruleID string, operations []server.RuleUpdateOperation) (*server.Rule, error)
|
ListRulesFunc func(accountID string) ([]*server.Rule, error)
|
||||||
DeleteRuleFunc func(accountID, ruleID string) error
|
GetUsersFromAccountFunc func(accountID string) ([]*server.UserInfo, error)
|
||||||
ListRulesFunc func(accountID string) ([]*server.Rule, error)
|
UpdatePeerMetaFunc func(peerKey string, meta server.PeerSystemMeta) error
|
||||||
GetUsersFromAccountFunc func(accountID string) ([]*server.UserInfo, error)
|
UpdatePeerSSHKeyFunc func(peerKey string, sshKey string) error
|
||||||
UpdatePeerMetaFunc func(peerKey string, meta server.PeerSystemMeta) error
|
UpdatePeerFunc func(accountID string, peer *server.Peer) (*server.Peer, error)
|
||||||
UpdatePeerSSHKeyFunc func(peerKey string, sshKey string) error
|
CreateRouteFunc func(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error)
|
||||||
UpdatePeerFunc func(accountID string, peer *server.Peer) (*server.Peer, error)
|
GetRouteFunc func(accountID, routeID string) (*route.Route, error)
|
||||||
CreateRouteFunc func(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error)
|
SaveRouteFunc func(accountID string, route *route.Route) error
|
||||||
GetRouteFunc func(accountID, routeID string) (*route.Route, error)
|
UpdateRouteFunc func(accountID string, routeID string, operations []server.RouteUpdateOperation) (*route.Route, error)
|
||||||
SaveRouteFunc func(accountID string, route *route.Route) error
|
DeleteRouteFunc func(accountID, routeID string) error
|
||||||
UpdateRouteFunc func(accountID string, routeID string, operations []server.RouteUpdateOperation) (*route.Route, error)
|
ListRoutesFunc func(accountID string) ([]*route.Route, error)
|
||||||
DeleteRouteFunc func(accountID, routeID string) error
|
SaveSetupKeyFunc func(accountID string, key *server.SetupKey) (*server.SetupKey, error)
|
||||||
ListRoutesFunc func(accountID string) ([]*route.Route, error)
|
ListSetupKeysFunc func(accountID string) ([]*server.SetupKey, error)
|
||||||
|
SaveUserFunc func(accountID string, user *server.User) (*server.UserInfo, error)
|
||||||
|
GetNameServerGroupFunc func(accountID, nsGroupID string) (*nbdns.NameServerGroup, error)
|
||||||
|
CreateNameServerGroupFunc func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error)
|
||||||
|
SaveNameServerGroupFunc func(accountID string, nsGroupToSave *nbdns.NameServerGroup) error
|
||||||
|
UpdateNameServerGroupFunc func(accountID, nsGroupID string, operations []server.NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error)
|
||||||
|
DeleteNameServerGroupFunc func(accountID, nsGroupID string) error
|
||||||
|
ListNameServerGroupsFunc func(accountID string) ([]*nbdns.NameServerGroup, error)
|
||||||
|
CreateUserFunc func(accountID string, key *server.UserInfo) (*server.UserInfo, error)
|
||||||
|
GetAccountFromTokenFunc func(claims jwtclaims.AuthorizationClaims) (*server.Account, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface
|
// GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface
|
||||||
@@ -82,40 +92,18 @@ func (am *MockAccountManager) GetAccountByUser(userId string) (*server.Account,
|
|||||||
return nil, status.Errorf(codes.Unimplemented, "method GetAccountByUser is not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method GetAccountByUser is not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSetupKey mock implementation of AddSetupKey from server.AccountManager interface
|
// CreateSetupKey mock implementation of CreateSetupKey from server.AccountManager interface
|
||||||
func (am *MockAccountManager) AddSetupKey(
|
func (am *MockAccountManager) CreateSetupKey(
|
||||||
accountId string,
|
accountId string,
|
||||||
keyName string,
|
keyName string,
|
||||||
keyType server.SetupKeyType,
|
keyType server.SetupKeyType,
|
||||||
expiresIn time.Duration,
|
expiresIn time.Duration,
|
||||||
|
autoGroups []string,
|
||||||
) (*server.SetupKey, error) {
|
) (*server.SetupKey, error) {
|
||||||
if am.AddSetupKeyFunc != nil {
|
if am.CreateSetupKeyFunc != nil {
|
||||||
return am.AddSetupKeyFunc(accountId, keyName, keyType, expiresIn)
|
return am.CreateSetupKeyFunc(accountId, keyName, keyType, expiresIn, autoGroups)
|
||||||
}
|
}
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method AddSetupKey is not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method CreateSetupKey is not implemented")
|
||||||
}
|
|
||||||
|
|
||||||
// RevokeSetupKey mock implementation of RevokeSetupKey from server.AccountManager interface
|
|
||||||
func (am *MockAccountManager) RevokeSetupKey(
|
|
||||||
accountId string,
|
|
||||||
keyId string,
|
|
||||||
) (*server.SetupKey, error) {
|
|
||||||
if am.RevokeSetupKeyFunc != nil {
|
|
||||||
return am.RevokeSetupKeyFunc(accountId, keyId)
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method RevokeSetupKey is not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenameSetupKey mock implementation of RenameSetupKey from server.AccountManager interface
|
|
||||||
func (am *MockAccountManager) RenameSetupKey(
|
|
||||||
accountId string,
|
|
||||||
keyId string,
|
|
||||||
newName string,
|
|
||||||
) (*server.SetupKey, error) {
|
|
||||||
if am.RenameSetupKeyFunc != nil {
|
|
||||||
return am.RenameSetupKeyFunc(accountId, keyId, newName)
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method RenameSetupKey is not implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccountById mock implementation of GetAccountById from server.AccountManager interface
|
// GetAccountById mock implementation of GetAccountById from server.AccountManager interface
|
||||||
@@ -139,19 +127,6 @@ func (am *MockAccountManager) GetAccountByUserOrAccountId(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccountWithAuthorizationClaims mock implementation of GetAccountWithAuthorizationClaims from server.AccountManager interface
|
|
||||||
func (am *MockAccountManager) GetAccountWithAuthorizationClaims(
|
|
||||||
claims jwtclaims.AuthorizationClaims,
|
|
||||||
) (*server.Account, error) {
|
|
||||||
if am.GetAccountWithAuthorizationClaimsFunc != nil {
|
|
||||||
return am.GetAccountWithAuthorizationClaimsFunc(claims)
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(
|
|
||||||
codes.Unimplemented,
|
|
||||||
"method GetAccountWithAuthorizationClaims is not implemented",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountExists mock implementation of AccountExists from server.AccountManager interface
|
// AccountExists mock implementation of AccountExists from server.AccountManager interface
|
||||||
func (am *MockAccountManager) AccountExists(accountId string) (*bool, error) {
|
func (am *MockAccountManager) AccountExists(accountId string) (*bool, error) {
|
||||||
if am.AccountExistsFunc != nil {
|
if am.AccountExistsFunc != nil {
|
||||||
@@ -415,3 +390,102 @@ func (am *MockAccountManager) ListRoutes(accountID string) ([]*route.Route, erro
|
|||||||
}
|
}
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoutes is not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ListRoutes is not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveSetupKey mocks SaveSetupKey of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) SaveSetupKey(accountID string, key *server.SetupKey) (*server.SetupKey, error) {
|
||||||
|
if am.SaveSetupKeyFunc != nil {
|
||||||
|
return am.SaveSetupKeyFunc(accountID, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method SaveSetupKey is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSetupKey mocks GetSetupKey of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) GetSetupKey(accountID, keyID string) (*server.SetupKey, error) {
|
||||||
|
if am.GetSetupKeyFunc != nil {
|
||||||
|
return am.GetSetupKeyFunc(accountID, keyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetSetupKey is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSetupKeys mocks ListSetupKeys of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) ListSetupKeys(accountID string) ([]*server.SetupKey, error) {
|
||||||
|
if am.ListSetupKeysFunc != nil {
|
||||||
|
return am.ListSetupKeysFunc(accountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListSetupKeys is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveUser mocks SaveUser of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) SaveUser(accountID string, user *server.User) (*server.UserInfo, error) {
|
||||||
|
if am.SaveUserFunc != nil {
|
||||||
|
return am.SaveUserFunc(accountID, user)
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method SaveUser is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNameServerGroup mocks GetNameServerGroup of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) GetNameServerGroup(accountID, nsGroupID string) (*nbdns.NameServerGroup, error) {
|
||||||
|
if am.GetNameServerGroupFunc != nil {
|
||||||
|
return am.GetNameServerGroupFunc(accountID, nsGroupID)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNameServerGroup mocks CreateNameServerGroup of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error) {
|
||||||
|
if am.CreateNameServerGroupFunc != nil {
|
||||||
|
return am.CreateNameServerGroupFunc(accountID, name, description, nameServerList, groups, enabled)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveNameServerGroup mocks SaveNameServerGroup of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) SaveNameServerGroup(accountID string, nsGroupToSave *nbdns.NameServerGroup) error {
|
||||||
|
if am.SaveNameServerGroupFunc != nil {
|
||||||
|
return am.SaveNameServerGroupFunc(accountID, nsGroupToSave)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateNameServerGroup mocks UpdateNameServerGroup of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) UpdateNameServerGroup(accountID, nsGroupID string, operations []server.NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error) {
|
||||||
|
if am.UpdateNameServerGroupFunc != nil {
|
||||||
|
return am.UpdateNameServerGroupFunc(accountID, nsGroupID, operations)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNameServerGroup mocks DeleteNameServerGroup of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) DeleteNameServerGroup(accountID, nsGroupID string) error {
|
||||||
|
if am.DeleteNameServerGroupFunc != nil {
|
||||||
|
return am.DeleteNameServerGroupFunc(accountID, nsGroupID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListNameServerGroups mocks ListNameServerGroups of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) ListNameServerGroups(accountID string) ([]*nbdns.NameServerGroup, error) {
|
||||||
|
if am.ListNameServerGroupsFunc != nil {
|
||||||
|
return am.ListNameServerGroupsFunc(accountID)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser mocks CreateUser of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) CreateUser(accountID string, invite *server.UserInfo) (*server.UserInfo, error) {
|
||||||
|
if am.CreateUserFunc != nil {
|
||||||
|
return am.CreateUserFunc(accountID, invite)
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CreateUser is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccountFromToken mocks GetAccountFromToken of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) GetAccountFromToken(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
|
||||||
|
if am.GetAccountFromTokenFunc != nil {
|
||||||
|
return am.GetAccountFromTokenFunc(claims)
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetAccountFromToken is not implemented")
|
||||||
|
}
|
||||||
|
|||||||
333
management/server/nameserver.go
Normal file
333
management/server/nameserver.go
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/rs/xid"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"strconv"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UpdateNameServerGroupName indicates a nameserver group name update operation
|
||||||
|
UpdateNameServerGroupName NameServerGroupUpdateOperationType = iota
|
||||||
|
// UpdateNameServerGroupDescription indicates a nameserver group description update operation
|
||||||
|
UpdateNameServerGroupDescription
|
||||||
|
// UpdateNameServerGroupNameServers indicates a nameserver group nameservers list update operation
|
||||||
|
UpdateNameServerGroupNameServers
|
||||||
|
// UpdateNameServerGroupGroups indicates a nameserver group' groups update operation
|
||||||
|
UpdateNameServerGroupGroups
|
||||||
|
// UpdateNameServerGroupEnabled indicates a nameserver group status update operation
|
||||||
|
UpdateNameServerGroupEnabled
|
||||||
|
)
|
||||||
|
|
||||||
|
// NameServerGroupUpdateOperationType operation type
|
||||||
|
type NameServerGroupUpdateOperationType int
|
||||||
|
|
||||||
|
func (t NameServerGroupUpdateOperationType) String() string {
|
||||||
|
switch t {
|
||||||
|
case UpdateNameServerGroupDescription:
|
||||||
|
return "UpdateNameServerGroupDescription"
|
||||||
|
case UpdateNameServerGroupName:
|
||||||
|
return "UpdateNameServerGroupName"
|
||||||
|
case UpdateNameServerGroupNameServers:
|
||||||
|
return "UpdateNameServerGroupNameServers"
|
||||||
|
case UpdateNameServerGroupGroups:
|
||||||
|
return "UpdateNameServerGroupGroups"
|
||||||
|
case UpdateNameServerGroupEnabled:
|
||||||
|
return "UpdateNameServerGroupEnabled"
|
||||||
|
default:
|
||||||
|
return "InvalidOperation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameServerGroupUpdateOperation operation object with type and values to be applied
|
||||||
|
type NameServerGroupUpdateOperation struct {
|
||||||
|
Type NameServerGroupUpdateOperationType
|
||||||
|
Values []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNameServerGroup gets a nameserver group object from account and nameserver group IDs
|
||||||
|
func (am *DefaultAccountManager) GetNameServerGroup(accountID, nsGroupID string) (*nbdns.NameServerGroup, error) {
|
||||||
|
am.mux.Lock()
|
||||||
|
defer am.mux.Unlock()
|
||||||
|
|
||||||
|
account, err := am.Store.GetAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
nsGroup, found := account.NameServerGroups[nsGroupID]
|
||||||
|
if found {
|
||||||
|
return nsGroup.Copy(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, status.Errorf(codes.NotFound, "nameserver group with ID %s not found", nsGroupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNameServerGroup creates and saves a new nameserver group
|
||||||
|
func (am *DefaultAccountManager) CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error) {
|
||||||
|
am.mux.Lock()
|
||||||
|
defer am.mux.Unlock()
|
||||||
|
|
||||||
|
account, err := am.Store.GetAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
newNSGroup := &nbdns.NameServerGroup{
|
||||||
|
ID: xid.New().String(),
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
NameServers: nameServerList,
|
||||||
|
Groups: groups,
|
||||||
|
Enabled: enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateNameServerGroup(false, newNSGroup, account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.NameServerGroups == nil {
|
||||||
|
account.NameServerGroups = make(map[string]*nbdns.NameServerGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
account.NameServerGroups[newNSGroup.ID] = newNSGroup
|
||||||
|
|
||||||
|
account.Network.IncSerial()
|
||||||
|
err = am.Store.SaveAccount(account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNSGroup.Copy(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveNameServerGroup saves nameserver group
|
||||||
|
func (am *DefaultAccountManager) SaveNameServerGroup(accountID string, nsGroupToSave *nbdns.NameServerGroup) error {
|
||||||
|
am.mux.Lock()
|
||||||
|
defer am.mux.Unlock()
|
||||||
|
|
||||||
|
if nsGroupToSave == nil {
|
||||||
|
return status.Errorf(codes.InvalidArgument, "nameserver group provided is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := am.Store.GetAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return status.Errorf(codes.NotFound, "account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateNameServerGroup(true, nsGroupToSave, account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
account.NameServerGroups[nsGroupToSave.ID] = nsGroupToSave
|
||||||
|
|
||||||
|
account.Network.IncSerial()
|
||||||
|
err = am.Store.SaveAccount(account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateNameServerGroup updates existing nameserver group with set of operations
|
||||||
|
func (am *DefaultAccountManager) UpdateNameServerGroup(accountID, nsGroupID string, operations []NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error) {
|
||||||
|
am.mux.Lock()
|
||||||
|
defer am.mux.Unlock()
|
||||||
|
|
||||||
|
account, err := am.Store.GetAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(operations) == 0 {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "operations shouldn't be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
nsGroupToUpdate, ok := account.NameServerGroups[nsGroupID]
|
||||||
|
if !ok {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "nameserver group ID %s no longer exists", nsGroupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
newNSGroup := nsGroupToUpdate.Copy()
|
||||||
|
|
||||||
|
for _, operation := range operations {
|
||||||
|
valuesCount := len(operation.Values)
|
||||||
|
if valuesCount < 1 {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "operation %s contains invalid number of values, it should be at least 1", operation.Type.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, value := range operation.Values {
|
||||||
|
if value == "" {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "operation %s contains invalid empty string value", operation.Type.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch operation.Type {
|
||||||
|
case UpdateNameServerGroupDescription:
|
||||||
|
newNSGroup.Description = operation.Values[0]
|
||||||
|
case UpdateNameServerGroupName:
|
||||||
|
if valuesCount > 1 {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "failed to parse name values, expected 1 value got %d", valuesCount)
|
||||||
|
}
|
||||||
|
err = validateNSGroupName(operation.Values[0], nsGroupID, account.NameServerGroups)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newNSGroup.Name = operation.Values[0]
|
||||||
|
case UpdateNameServerGroupNameServers:
|
||||||
|
var nsList []nbdns.NameServer
|
||||||
|
for _, url := range operation.Values {
|
||||||
|
ns, err := nbdns.ParseNameServerURL(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nsList = append(nsList, ns)
|
||||||
|
}
|
||||||
|
err = validateNSList(nsList)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newNSGroup.NameServers = nsList
|
||||||
|
case UpdateNameServerGroupGroups:
|
||||||
|
err = validateGroups(operation.Values, account.Groups)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newNSGroup.Groups = operation.Values
|
||||||
|
case UpdateNameServerGroupEnabled:
|
||||||
|
enabled, err := strconv.ParseBool(operation.Values[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "failed to parse enabled %s, not boolean", operation.Values[0])
|
||||||
|
}
|
||||||
|
newNSGroup.Enabled = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
account.NameServerGroups[nsGroupID] = newNSGroup
|
||||||
|
|
||||||
|
account.Network.IncSerial()
|
||||||
|
err = am.Store.SaveAccount(account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNSGroup.Copy(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNameServerGroup deletes nameserver group with nsGroupID
|
||||||
|
func (am *DefaultAccountManager) DeleteNameServerGroup(accountID, nsGroupID string) error {
|
||||||
|
am.mux.Lock()
|
||||||
|
defer am.mux.Unlock()
|
||||||
|
|
||||||
|
account, err := am.Store.GetAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return status.Errorf(codes.NotFound, "account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(account.NameServerGroups, nsGroupID)
|
||||||
|
|
||||||
|
account.Network.IncSerial()
|
||||||
|
err = am.Store.SaveAccount(account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListNameServerGroups returns a list of nameserver groups from account
|
||||||
|
func (am *DefaultAccountManager) ListNameServerGroups(accountID string) ([]*nbdns.NameServerGroup, error) {
|
||||||
|
am.mux.Lock()
|
||||||
|
defer am.mux.Unlock()
|
||||||
|
|
||||||
|
account, err := am.Store.GetAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
nsGroups := make([]*nbdns.NameServerGroup, 0, len(account.NameServerGroups))
|
||||||
|
for _, item := range account.NameServerGroups {
|
||||||
|
nsGroups = append(nsGroups, item.Copy())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nsGroups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNameServerGroup(existingGroup bool, nameserverGroup *nbdns.NameServerGroup, account *Account) error {
|
||||||
|
nsGroupID := ""
|
||||||
|
if existingGroup {
|
||||||
|
nsGroupID = nameserverGroup.ID
|
||||||
|
_, found := account.NameServerGroups[nsGroupID]
|
||||||
|
if !found {
|
||||||
|
return status.Errorf(codes.NotFound, "nameserver group with ID %s was not found", nsGroupID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validateNSGroupName(nameserverGroup.Name, nsGroupID, account.NameServerGroups)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateNSList(nameserverGroup.NameServers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateGroups(nameserverGroup.Groups, account.Groups)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNSGroupName(name, nsGroupID string, nsGroupMap map[string]*nbdns.NameServerGroup) error {
|
||||||
|
if utf8.RuneCountInString(name) > nbdns.MaxGroupNameChar || name == "" {
|
||||||
|
return status.Errorf(codes.InvalidArgument, "nameserver group name should be between 1 and %d", nbdns.MaxGroupNameChar)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, nsGroup := range nsGroupMap {
|
||||||
|
if name == nsGroup.Name && nsGroup.ID != nsGroupID {
|
||||||
|
return status.Errorf(codes.InvalidArgument, "a nameserver group with name %s already exist", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNSList(list []nbdns.NameServer) error {
|
||||||
|
nsListLenght := len(list)
|
||||||
|
if nsListLenght == 0 || nsListLenght > 2 {
|
||||||
|
return status.Errorf(codes.InvalidArgument, "the list of nameservers should be 1 or 2, got %d", len(list))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateGroups(list []string, groups map[string]*Group) error {
|
||||||
|
if len(list) == 0 {
|
||||||
|
return status.Errorf(codes.InvalidArgument, "the list of group IDs should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range list {
|
||||||
|
if id == "" {
|
||||||
|
return status.Errorf(codes.InvalidArgument, "group ID should not be empty string")
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for groupID := range groups {
|
||||||
|
if id == groupID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return status.Errorf(codes.InvalidArgument, "group id %s not found", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
965
management/server/nameserver_test.go
Normal file
965
management/server/nameserver_test.go
Normal file
@@ -0,0 +1,965 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
group1ID = "group1"
|
||||||
|
group2ID = "group2"
|
||||||
|
existingNSGroupName = "existing"
|
||||||
|
existingNSGroupID = "existingNSGroup"
|
||||||
|
nsGroupPeer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8="
|
||||||
|
nsGroupPeer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI="
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateNameServerGroup(t *testing.T) {
|
||||||
|
type input struct {
|
||||||
|
name string
|
||||||
|
description string
|
||||||
|
enabled bool
|
||||||
|
groups []string
|
||||||
|
nameServers []nbdns.NameServer
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
inputArgs input
|
||||||
|
shouldCreate bool
|
||||||
|
errFunc require.ErrorAssertionFunc
|
||||||
|
expectedNSGroup *nbdns.NameServerGroup
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Create A NS Group",
|
||||||
|
inputArgs: input{
|
||||||
|
name: "super",
|
||||||
|
description: "super",
|
||||||
|
groups: []string{group1ID},
|
||||||
|
nameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
errFunc: require.NoError,
|
||||||
|
shouldCreate: true,
|
||||||
|
expectedNSGroup: &nbdns.NameServerGroup{
|
||||||
|
Name: "super",
|
||||||
|
Description: "super",
|
||||||
|
Groups: []string{group1ID},
|
||||||
|
NameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Create If Name Exist",
|
||||||
|
inputArgs: input{
|
||||||
|
name: existingNSGroupName,
|
||||||
|
description: "super",
|
||||||
|
groups: []string{group1ID},
|
||||||
|
nameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Create If Name Is Small",
|
||||||
|
inputArgs: input{
|
||||||
|
name: "",
|
||||||
|
description: "super",
|
||||||
|
groups: []string{group1ID},
|
||||||
|
nameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Create If Name Is Large",
|
||||||
|
inputArgs: input{
|
||||||
|
name: "1234567890123456789012345678901234567890extra",
|
||||||
|
description: "super",
|
||||||
|
groups: []string{group1ID},
|
||||||
|
nameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Create A NS Group With No Nameservers Should Fail",
|
||||||
|
inputArgs: input{
|
||||||
|
name: "super",
|
||||||
|
description: "super",
|
||||||
|
groups: []string{group1ID},
|
||||||
|
nameServers: []nbdns.NameServer{},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Create A NS Group With More Than 2 Nameservers Should Fail",
|
||||||
|
inputArgs: input{
|
||||||
|
name: "super",
|
||||||
|
description: "super",
|
||||||
|
groups: []string{group1ID},
|
||||||
|
nameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.3.3"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Create If Groups Is Empty",
|
||||||
|
inputArgs: input{
|
||||||
|
name: "super",
|
||||||
|
description: "super",
|
||||||
|
groups: []string{},
|
||||||
|
nameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Create If Group Doesn't Exist",
|
||||||
|
inputArgs: input{
|
||||||
|
name: "super",
|
||||||
|
description: "super",
|
||||||
|
groups: []string{"missingGroup"},
|
||||||
|
nameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Create If Group ID Is Invalid",
|
||||||
|
inputArgs: input{
|
||||||
|
name: "super",
|
||||||
|
description: "super",
|
||||||
|
groups: []string{""},
|
||||||
|
nameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
am, err := createNSManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to create account manager")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := initTestNSAccount(t, am)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to init testing account")
|
||||||
|
}
|
||||||
|
|
||||||
|
outNSGroup, err := am.CreateNameServerGroup(
|
||||||
|
account.Id,
|
||||||
|
testCase.inputArgs.name,
|
||||||
|
testCase.inputArgs.description,
|
||||||
|
testCase.inputArgs.nameServers,
|
||||||
|
testCase.inputArgs.groups,
|
||||||
|
testCase.inputArgs.enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
testCase.errFunc(t, err)
|
||||||
|
|
||||||
|
if !testCase.shouldCreate {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign generated ID
|
||||||
|
testCase.expectedNSGroup.ID = outNSGroup.ID
|
||||||
|
|
||||||
|
if !testCase.expectedNSGroup.IsEqual(outNSGroup) {
|
||||||
|
t.Errorf("new nameserver group didn't match expected ns group:\nGot %#v\nExpected:%#v\n", outNSGroup, testCase.expectedNSGroup)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveNameServerGroup(t *testing.T) {
|
||||||
|
|
||||||
|
existingNSGroup := &nbdns.NameServerGroup{
|
||||||
|
ID: "testingNSGroup",
|
||||||
|
Name: "super",
|
||||||
|
Description: "super",
|
||||||
|
NameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: []string{group1ID},
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
validGroups := []string{group2ID}
|
||||||
|
invalidGroups := []string{"nonExisting"}
|
||||||
|
validNameServerList := []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
invalidNameServerListLarge := []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.3.3"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
invalidID := "doesntExist"
|
||||||
|
validName := "12345678901234567890qw"
|
||||||
|
invalidNameLarge := "12345678901234567890qwertyuiopqwertyuiop1"
|
||||||
|
invalidNameSmall := ""
|
||||||
|
invalidNameExisting := existingNSGroupName
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
existingNSGroup *nbdns.NameServerGroup
|
||||||
|
newID *string
|
||||||
|
newName *string
|
||||||
|
newNSList []nbdns.NameServer
|
||||||
|
newGroups []string
|
||||||
|
skipCopying bool
|
||||||
|
shouldCreate bool
|
||||||
|
errFunc require.ErrorAssertionFunc
|
||||||
|
expectedNSGroup *nbdns.NameServerGroup
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Should Update Name Server Group",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
newName: &validName,
|
||||||
|
newGroups: validGroups,
|
||||||
|
newNSList: validNameServerList,
|
||||||
|
errFunc: require.NoError,
|
||||||
|
shouldCreate: true,
|
||||||
|
expectedNSGroup: &nbdns.NameServerGroup{
|
||||||
|
ID: "testingNSGroup",
|
||||||
|
Name: validName,
|
||||||
|
Description: "super",
|
||||||
|
NameServers: validNameServerList,
|
||||||
|
Groups: validGroups,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update If Name Is Small",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
newName: &invalidNameSmall,
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update If Name Is Large",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
newName: &invalidNameLarge,
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update If Name Exists",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
newName: &invalidNameExisting,
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update If ID Don't Exist",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
newID: &invalidID,
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update If Nameserver List Is Small",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
newNSList: []nbdns.NameServer{},
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update If Nameserver List Is Large",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
newNSList: invalidNameServerListLarge,
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update If Groups List Is Empty",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
newGroups: []string{},
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update If Groups List Has Empty ID",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
newGroups: []string{""},
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update If Groups List Has Non Existing Group ID",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
newGroups: invalidGroups,
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
am, err := createNSManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to create account manager")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := initTestNSAccount(t, am)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to init testing account")
|
||||||
|
}
|
||||||
|
|
||||||
|
account.NameServerGroups[testCase.existingNSGroup.ID] = testCase.existingNSGroup
|
||||||
|
|
||||||
|
err = am.Store.SaveAccount(account)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("account should be saved")
|
||||||
|
}
|
||||||
|
|
||||||
|
var nsGroupToSave *nbdns.NameServerGroup
|
||||||
|
|
||||||
|
if !testCase.skipCopying {
|
||||||
|
nsGroupToSave = testCase.existingNSGroup.Copy()
|
||||||
|
|
||||||
|
if testCase.newID != nil {
|
||||||
|
nsGroupToSave.ID = *testCase.newID
|
||||||
|
}
|
||||||
|
|
||||||
|
if testCase.newName != nil {
|
||||||
|
nsGroupToSave.Name = *testCase.newName
|
||||||
|
}
|
||||||
|
|
||||||
|
if testCase.newGroups != nil {
|
||||||
|
nsGroupToSave.Groups = testCase.newGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
if testCase.newNSList != nil {
|
||||||
|
nsGroupToSave.NameServers = testCase.newNSList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.SaveNameServerGroup(account.Id, nsGroupToSave)
|
||||||
|
|
||||||
|
testCase.errFunc(t, err)
|
||||||
|
|
||||||
|
if !testCase.shouldCreate {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
savedNSGroup, saved := account.NameServerGroups[testCase.expectedNSGroup.ID]
|
||||||
|
require.True(t, saved)
|
||||||
|
|
||||||
|
if !testCase.expectedNSGroup.IsEqual(savedNSGroup) {
|
||||||
|
t.Errorf("new nameserver group didn't match expected group:\nGot %#v\nExpected:%#v\n", savedNSGroup, testCase.expectedNSGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateNameServerGroup(t *testing.T) {
|
||||||
|
nsGroupID := "testingNSGroup"
|
||||||
|
|
||||||
|
existingNSGroup := &nbdns.NameServerGroup{
|
||||||
|
ID: nsGroupID,
|
||||||
|
Name: "super",
|
||||||
|
Description: "super",
|
||||||
|
NameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: []string{group1ID},
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
existingNSGroup *nbdns.NameServerGroup
|
||||||
|
nsGroupID string
|
||||||
|
operations []NameServerGroupUpdateOperation
|
||||||
|
shouldCreate bool
|
||||||
|
errFunc require.ErrorAssertionFunc
|
||||||
|
expectedNSGroup *nbdns.NameServerGroup
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Should Update Single Property",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupName,
|
||||||
|
Values: []string{"superNew"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errFunc: require.NoError,
|
||||||
|
shouldCreate: true,
|
||||||
|
expectedNSGroup: &nbdns.NameServerGroup{
|
||||||
|
ID: nsGroupID,
|
||||||
|
Name: "superNew",
|
||||||
|
Description: "super",
|
||||||
|
NameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: []string{group1ID},
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Update Multiple Properties",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupName,
|
||||||
|
Values: []string{"superNew"},
|
||||||
|
},
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupDescription,
|
||||||
|
Values: []string{"superDescription"},
|
||||||
|
},
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupNameServers,
|
||||||
|
Values: []string{"udp://127.0.0.1:53", "udp://8.8.8.8:53"},
|
||||||
|
},
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupGroups,
|
||||||
|
Values: []string{group1ID, group2ID},
|
||||||
|
},
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupEnabled,
|
||||||
|
Values: []string{"false"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errFunc: require.NoError,
|
||||||
|
shouldCreate: true,
|
||||||
|
expectedNSGroup: &nbdns.NameServerGroup{
|
||||||
|
ID: nsGroupID,
|
||||||
|
Name: "superNew",
|
||||||
|
Description: "superDescription",
|
||||||
|
NameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("127.0.0.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("8.8.8.8"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: []string{group1ID, group2ID},
|
||||||
|
Enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update On Invalid ID",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: "nonExistingNSGroup",
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update On Empty Operations",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update On Empty Values",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update On Empty String",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupName,
|
||||||
|
Values: []string{""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update On Invalid Name Large String",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupName,
|
||||||
|
Values: []string{"12345678901234567890qwertyuiopqwertyuiop1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update On Invalid On Existing Name",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupName,
|
||||||
|
Values: []string{existingNSGroupName},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update On Invalid On Multiple Name Values",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupName,
|
||||||
|
Values: []string{"nameOne", "nameTwo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update On Invalid Boolean",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupEnabled,
|
||||||
|
Values: []string{"yes"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update On Invalid Nameservers Wrong Schema",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupNameServers,
|
||||||
|
Values: []string{"https://127.0.0.1:53"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update On Invalid Nameservers Wrong IP",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupNameServers,
|
||||||
|
Values: []string{"udp://8.8.8.300:53"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update On Large Number Of Nameservers",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupNameServers,
|
||||||
|
Values: []string{"udp://127.0.0.1:53", "udp://8.8.8.8:53", "udp://8.8.4.4:53"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Update On Invalid GroupID",
|
||||||
|
existingNSGroup: existingNSGroup,
|
||||||
|
nsGroupID: existingNSGroup.ID,
|
||||||
|
operations: []NameServerGroupUpdateOperation{
|
||||||
|
NameServerGroupUpdateOperation{
|
||||||
|
Type: UpdateNameServerGroupGroups,
|
||||||
|
Values: []string{"nonExistingGroupID"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
am, err := createNSManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to create account manager")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := initTestNSAccount(t, am)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to init testing account")
|
||||||
|
}
|
||||||
|
|
||||||
|
account.NameServerGroups[testCase.existingNSGroup.ID] = testCase.existingNSGroup
|
||||||
|
|
||||||
|
err = am.Store.SaveAccount(account)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("account should be saved")
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedRoute, err := am.UpdateNameServerGroup(account.Id, testCase.nsGroupID, testCase.operations)
|
||||||
|
testCase.errFunc(t, err)
|
||||||
|
|
||||||
|
if !testCase.shouldCreate {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase.expectedNSGroup.ID = updatedRoute.ID
|
||||||
|
|
||||||
|
if !testCase.expectedNSGroup.IsEqual(updatedRoute) {
|
||||||
|
t.Errorf("new nameserver group didn't match expected group:\nGot %#v\nExpected:%#v\n", updatedRoute, testCase.expectedNSGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteNameServerGroup(t *testing.T) {
|
||||||
|
nsGroupID := "testingNSGroup"
|
||||||
|
|
||||||
|
testingNSGroup := &nbdns.NameServerGroup{
|
||||||
|
ID: nsGroupID,
|
||||||
|
Name: "super",
|
||||||
|
Description: "super",
|
||||||
|
NameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.1.1"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("1.1.2.2"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: []string{group1ID},
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
am, err := createNSManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to create account manager")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := initTestNSAccount(t, am)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to init testing account")
|
||||||
|
}
|
||||||
|
|
||||||
|
account.NameServerGroups[testingNSGroup.ID] = testingNSGroup
|
||||||
|
|
||||||
|
err = am.Store.SaveAccount(account)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to save account")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.DeleteNameServerGroup(account.Id, testingNSGroup.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("deleting nameserver group failed with error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
savedAccount, err := am.Store.GetAccount(account.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to retrieve saved account with error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, found := savedAccount.NameServerGroups[testingNSGroup.ID]
|
||||||
|
if found {
|
||||||
|
t.Error("nameserver group shouldn't be found after delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNameServerGroup(t *testing.T) {
|
||||||
|
|
||||||
|
am, err := createNSManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to create account manager")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := initTestNSAccount(t, am)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to init testing account")
|
||||||
|
}
|
||||||
|
|
||||||
|
foundGroup, err := am.GetNameServerGroup(account.Id, existingNSGroupID)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("getting existing nameserver group failed with error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundGroup == nil {
|
||||||
|
t.Error("got a nil group while getting nameserver group with ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = am.GetNameServerGroup(account.Id, "not existing")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("getting not existing nameserver group should return error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNSManager(t *testing.T) (*DefaultAccountManager, error) {
|
||||||
|
store, err := createNSStore(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return BuildManager(store, NewPeersUpdateManager(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNSStore(t *testing.T) (Store, error) {
|
||||||
|
dataDir := t.TempDir()
|
||||||
|
store, err := NewStore(dataDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return store, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, error) {
|
||||||
|
peer1 := &Peer{
|
||||||
|
Key: nsGroupPeer1Key,
|
||||||
|
Name: "test-host1@netbird.io",
|
||||||
|
Meta: PeerSystemMeta{
|
||||||
|
Hostname: "test-host1@netbird.io",
|
||||||
|
GoOS: "linux",
|
||||||
|
Kernel: "Linux",
|
||||||
|
Core: "21.04",
|
||||||
|
Platform: "x86_64",
|
||||||
|
OS: "Ubuntu",
|
||||||
|
WtVersion: "development",
|
||||||
|
UIVersion: "development",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
peer2 := &Peer{
|
||||||
|
Key: nsGroupPeer2Key,
|
||||||
|
Name: "test-host2@netbird.io",
|
||||||
|
Meta: PeerSystemMeta{
|
||||||
|
Hostname: "test-host2@netbird.io",
|
||||||
|
GoOS: "linux",
|
||||||
|
Kernel: "Linux",
|
||||||
|
Core: "21.04",
|
||||||
|
Platform: "x86_64",
|
||||||
|
OS: "Ubuntu",
|
||||||
|
WtVersion: "development",
|
||||||
|
UIVersion: "development",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
existingNSGroup := nbdns.NameServerGroup{
|
||||||
|
ID: existingNSGroupID,
|
||||||
|
Name: existingNSGroupName,
|
||||||
|
Description: "",
|
||||||
|
NameServers: []nbdns.NameServer{
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("8.8.8.8"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: netip.MustParseAddr("8.8.4.4"),
|
||||||
|
NSType: nbdns.UDPNameServerType,
|
||||||
|
Port: nbdns.DefaultDNSPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: []string{group1ID},
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
accountID := "testingAcc"
|
||||||
|
userID := "testingUser"
|
||||||
|
domain := "example.com"
|
||||||
|
|
||||||
|
account := newAccountWithId(accountID, userID, domain)
|
||||||
|
|
||||||
|
account.NameServerGroups[existingNSGroup.ID] = &existingNSGroup
|
||||||
|
|
||||||
|
defaultGroup, err := account.GetGroupAll()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newGroup1 := defaultGroup.Copy()
|
||||||
|
newGroup1.ID = group1ID
|
||||||
|
newGroup2 := defaultGroup.Copy()
|
||||||
|
newGroup2.ID = group2ID
|
||||||
|
|
||||||
|
account.Groups[newGroup1.ID] = newGroup1
|
||||||
|
account.Groups[newGroup2.ID] = newGroup2
|
||||||
|
|
||||||
|
err = am.Store.SaveAccount(account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = am.AddPeer("", userID, peer1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = am.AddPeer("", userID, peer2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
@@ -294,6 +294,8 @@ func (am *DefaultAccountManager) AddPeer(
|
|||||||
var account *Account
|
var account *Account
|
||||||
var err error
|
var err error
|
||||||
var sk *SetupKey
|
var sk *SetupKey
|
||||||
|
// auto-assign groups that are coming with a SetupKey or a User
|
||||||
|
var groupsToAdd []string
|
||||||
if len(upperKey) != 0 {
|
if len(upperKey) != 0 {
|
||||||
account, err = am.Store.GetAccountBySetupKey(upperKey)
|
account, err = am.Store.GetAccountBySetupKey(upperKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -321,11 +323,20 @@ func (am *DefaultAccountManager) AddPeer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupsToAdd = sk.AutoGroups
|
||||||
|
|
||||||
} else if len(userID) != 0 {
|
} else if len(userID) != 0 {
|
||||||
account, err = am.Store.GetUserAccount(userID)
|
account, err = am.Store.GetUserAccount(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.NotFound, "unable to register peer, unknown user with ID: %s", userID)
|
return nil, status.Errorf(codes.NotFound, "unable to register peer, unknown user with ID: %s", userID)
|
||||||
}
|
}
|
||||||
|
user, ok := account.Users[userID]
|
||||||
|
if !ok {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "unable to register peer, unknown user with ID: %s", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
groupsToAdd = user.AutoGroups
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Empty setup key and jwt fail
|
// Empty setup key and jwt fail
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "no setup key or user id provided")
|
return nil, status.Errorf(codes.InvalidArgument, "no setup key or user id provided")
|
||||||
@@ -361,6 +372,14 @@ func (am *DefaultAccountManager) AddPeer(
|
|||||||
}
|
}
|
||||||
group.Peers = append(group.Peers, newPeer.Key)
|
group.Peers = append(group.Peers, newPeer.Key)
|
||||||
|
|
||||||
|
if len(groupsToAdd) > 0 {
|
||||||
|
for _, s := range groupsToAdd {
|
||||||
|
if g, ok := account.Groups[s]; ok && g.Name != "All" {
|
||||||
|
g.Peers = append(g.Peers, newPeer.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
account.Peers[newPeer.Key] = newPeer
|
account.Peers[newPeer.Key] = newPeer
|
||||||
if len(upperKey) != 0 {
|
if len(upperKey) != 0 {
|
||||||
account.SetupKeys[sk.Key] = sk.IncrementUsage()
|
account.SetupKeys[sk.Key] = sk.IncrementUsage()
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,8 +21,41 @@ const (
|
|||||||
DefaultSetupKeyDuration = 24 * 30 * time.Hour
|
DefaultSetupKeyDuration = 24 * 30 * time.Hour
|
||||||
// DefaultSetupKeyName is a default name of the default setup key
|
// DefaultSetupKeyName is a default name of the default setup key
|
||||||
DefaultSetupKeyName = "Default key"
|
DefaultSetupKeyName = "Default key"
|
||||||
|
|
||||||
|
// UpdateSetupKeyName indicates a setup key name update operation
|
||||||
|
UpdateSetupKeyName SetupKeyUpdateOperationType = iota
|
||||||
|
// UpdateSetupKeyRevoked indicates a setup key revoked filed update operation
|
||||||
|
UpdateSetupKeyRevoked
|
||||||
|
// UpdateSetupKeyAutoGroups indicates a setup key auto-assign groups update operation
|
||||||
|
UpdateSetupKeyAutoGroups
|
||||||
|
// UpdateSetupKeyExpiresAt indicates a setup key expiration time update operation
|
||||||
|
UpdateSetupKeyExpiresAt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SetupKeyUpdateOperationType operation type
|
||||||
|
type SetupKeyUpdateOperationType int
|
||||||
|
|
||||||
|
func (t SetupKeyUpdateOperationType) String() string {
|
||||||
|
switch t {
|
||||||
|
case UpdateSetupKeyName:
|
||||||
|
return "UpdateSetupKeyName"
|
||||||
|
case UpdateSetupKeyRevoked:
|
||||||
|
return "UpdateSetupKeyRevoked"
|
||||||
|
case UpdateSetupKeyAutoGroups:
|
||||||
|
return "UpdateSetupKeyAutoGroups"
|
||||||
|
case UpdateSetupKeyExpiresAt:
|
||||||
|
return "UpdateSetupKeyExpiresAt"
|
||||||
|
default:
|
||||||
|
return "InvalidOperation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupKeyUpdateOperation operation object with type and values to be applied
|
||||||
|
type SetupKeyUpdateOperation struct {
|
||||||
|
Type SetupKeyUpdateOperationType
|
||||||
|
Values []string
|
||||||
|
}
|
||||||
|
|
||||||
// SetupKeyType is the type of setup key
|
// SetupKeyType is the type of setup key
|
||||||
type SetupKeyType string
|
type SetupKeyType string
|
||||||
|
|
||||||
@@ -31,30 +67,40 @@ type SetupKey struct {
|
|||||||
Type SetupKeyType
|
Type SetupKeyType
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
// Revoked indicates whether the key was revoked or not (we don't remove them for tracking purposes)
|
// Revoked indicates whether the key was revoked or not (we don't remove them for tracking purposes)
|
||||||
Revoked bool
|
Revoked bool
|
||||||
// UsedTimes indicates how many times the key was used
|
// UsedTimes indicates how many times the key was used
|
||||||
UsedTimes int
|
UsedTimes int
|
||||||
// LastUsed last time the key was used for peer registration
|
// LastUsed last time the key was used for peer registration
|
||||||
LastUsed time.Time
|
LastUsed time.Time
|
||||||
|
// AutoGroups is a list of Group IDs that are auto assigned to a Peer when it uses this key to register
|
||||||
|
AutoGroups []string
|
||||||
}
|
}
|
||||||
|
|
||||||
//Copy copies SetupKey to a new object
|
// Copy copies SetupKey to a new object
|
||||||
func (key *SetupKey) Copy() *SetupKey {
|
func (key *SetupKey) Copy() *SetupKey {
|
||||||
|
autoGroups := make([]string, 0)
|
||||||
|
autoGroups = append(autoGroups, key.AutoGroups...)
|
||||||
|
if key.UpdatedAt.IsZero() {
|
||||||
|
key.UpdatedAt = key.CreatedAt
|
||||||
|
}
|
||||||
return &SetupKey{
|
return &SetupKey{
|
||||||
Id: key.Id,
|
Id: key.Id,
|
||||||
Key: key.Key,
|
Key: key.Key,
|
||||||
Name: key.Name,
|
Name: key.Name,
|
||||||
Type: key.Type,
|
Type: key.Type,
|
||||||
CreatedAt: key.CreatedAt,
|
CreatedAt: key.CreatedAt,
|
||||||
ExpiresAt: key.ExpiresAt,
|
ExpiresAt: key.ExpiresAt,
|
||||||
Revoked: key.Revoked,
|
UpdatedAt: key.UpdatedAt,
|
||||||
UsedTimes: key.UsedTimes,
|
Revoked: key.Revoked,
|
||||||
LastUsed: key.LastUsed,
|
UsedTimes: key.UsedTimes,
|
||||||
|
LastUsed: key.LastUsed,
|
||||||
|
AutoGroups: autoGroups,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//IncrementUsage makes a copy of a key, increments the UsedTimes by 1 and sets LastUsed to now
|
// IncrementUsage makes a copy of a key, increments the UsedTimes by 1 and sets LastUsed to now
|
||||||
func (key *SetupKey) IncrementUsage() *SetupKey {
|
func (key *SetupKey) IncrementUsage() *SetupKey {
|
||||||
c := key.Copy()
|
c := key.Copy()
|
||||||
c.UsedTimes = c.UsedTimes + 1
|
c.UsedTimes = c.UsedTimes + 1
|
||||||
@@ -83,24 +129,25 @@ func (key *SetupKey) IsOverUsed() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GenerateSetupKey generates a new setup key
|
// GenerateSetupKey generates a new setup key
|
||||||
func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration) *SetupKey {
|
func GenerateSetupKey(name string, t SetupKeyType, validFor time.Duration, autoGroups []string) *SetupKey {
|
||||||
key := strings.ToUpper(uuid.New().String())
|
key := strings.ToUpper(uuid.New().String())
|
||||||
createdAt := time.Now()
|
|
||||||
return &SetupKey{
|
return &SetupKey{
|
||||||
Id: strconv.Itoa(int(Hash(key))),
|
Id: strconv.Itoa(int(Hash(key))),
|
||||||
Key: key,
|
Key: key,
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: t,
|
Type: t,
|
||||||
CreatedAt: createdAt,
|
CreatedAt: time.Now(),
|
||||||
ExpiresAt: createdAt.Add(validFor),
|
ExpiresAt: time.Now().Add(validFor),
|
||||||
Revoked: false,
|
UpdatedAt: time.Now(),
|
||||||
UsedTimes: 0,
|
Revoked: false,
|
||||||
|
UsedTimes: 0,
|
||||||
|
AutoGroups: autoGroups,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateDefaultSetupKey generates a default setup key
|
// GenerateDefaultSetupKey generates a default setup key
|
||||||
func GenerateDefaultSetupKey() *SetupKey {
|
func GenerateDefaultSetupKey() *SetupKey {
|
||||||
return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration)
|
return GenerateSetupKey(DefaultSetupKeyName, SetupKeyReusable, DefaultSetupKeyDuration, []string{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Hash(s string) uint32 {
|
func Hash(s string) uint32 {
|
||||||
@@ -111,3 +158,127 @@ func Hash(s string) uint32 {
|
|||||||
}
|
}
|
||||||
return h.Sum32()
|
return h.Sum32()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateSetupKey generates a new setup key with a given name, type, list of groups IDs to auto-assign to peers registered with this key,
|
||||||
|
// and adds it to the specified account. A list of autoGroups IDs can be empty.
|
||||||
|
func (am *DefaultAccountManager) CreateSetupKey(accountID string, keyName string, keyType SetupKeyType,
|
||||||
|
expiresIn time.Duration, autoGroups []string) (*SetupKey, error) {
|
||||||
|
am.mux.Lock()
|
||||||
|
defer am.mux.Unlock()
|
||||||
|
|
||||||
|
keyDuration := DefaultSetupKeyDuration
|
||||||
|
if expiresIn != 0 {
|
||||||
|
keyDuration = expiresIn
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := am.Store.GetAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range autoGroups {
|
||||||
|
if _, ok := account.Groups[group]; !ok {
|
||||||
|
return nil, fmt.Errorf("group %s doesn't exist", group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupKey := GenerateSetupKey(keyName, keyType, keyDuration, autoGroups)
|
||||||
|
account.SetupKeys[setupKey.Key] = setupKey
|
||||||
|
|
||||||
|
err = am.Store.SaveAccount(account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed adding account key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return setupKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveSetupKey saves the provided SetupKey to the database overriding the existing one.
|
||||||
|
// Due to the unique nature of a SetupKey certain properties must not be overwritten
|
||||||
|
// (e.g. the key itself, creation date, ID, etc).
|
||||||
|
// These properties are overwritten: Name, AutoGroups, Revoked. The rest is copied from the existing key.
|
||||||
|
func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *SetupKey) (*SetupKey, error) {
|
||||||
|
am.mux.Lock()
|
||||||
|
defer am.mux.Unlock()
|
||||||
|
|
||||||
|
if keyToSave == nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "provided setup key to update is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := am.Store.GetAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldKey *SetupKey
|
||||||
|
for _, key := range account.SetupKeys {
|
||||||
|
if key.Id == keyToSave.Id {
|
||||||
|
oldKey = key.Copy()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if oldKey == nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "setup key not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// only auto groups, revoked status, and name can be updated for now
|
||||||
|
newKey := oldKey.Copy()
|
||||||
|
newKey.Name = keyToSave.Name
|
||||||
|
newKey.AutoGroups = keyToSave.AutoGroups
|
||||||
|
newKey.Revoked = keyToSave.Revoked
|
||||||
|
newKey.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
account.SetupKeys[newKey.Key] = newKey
|
||||||
|
|
||||||
|
if err = am.Store.SaveAccount(account); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newKey, am.updateAccountPeers(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSetupKeys returns a list of all setup keys of the account
|
||||||
|
func (am *DefaultAccountManager) ListSetupKeys(accountID string) ([]*SetupKey, error) {
|
||||||
|
am.mux.Lock()
|
||||||
|
defer am.mux.Unlock()
|
||||||
|
account, err := am.Store.GetAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]*SetupKey, 0, len(account.SetupKeys))
|
||||||
|
for _, key := range account.SetupKeys {
|
||||||
|
keys = append(keys, key.Copy())
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSetupKey looks up a SetupKey by KeyID, returns NotFound error if not found.
|
||||||
|
func (am *DefaultAccountManager) GetSetupKey(accountID, keyID string) (*SetupKey, error) {
|
||||||
|
am.mux.Lock()
|
||||||
|
defer am.mux.Unlock()
|
||||||
|
|
||||||
|
account, err := am.Store.GetAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundKey *SetupKey
|
||||||
|
for _, key := range account.SetupKeys {
|
||||||
|
if key.Id == keyID {
|
||||||
|
foundKey = key.Copy()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if foundKey == nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "setup key not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// the UpdatedAt field was introduced later, so there might be that some keys have a Zero value (e.g, null in the store file)
|
||||||
|
if foundKey.UpdatedAt.IsZero() {
|
||||||
|
foundKey.UpdatedAt = foundKey.CreatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundKey, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,23 +2,159 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestDefaultAccountManager_SaveSetupKey(t *testing.T) {
|
||||||
|
manager, err := createManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := "test_user"
|
||||||
|
account, err := manager.GetOrCreateAccountByUser(userID, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = manager.SaveGroup(account.Id, &Group{
|
||||||
|
ID: "group_1",
|
||||||
|
Name: "group_name_1",
|
||||||
|
Peers: []string{},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expiresIn := time.Hour
|
||||||
|
keyName := "my-test-key"
|
||||||
|
|
||||||
|
key, err := manager.CreateSetupKey(account.Id, keyName, SetupKeyReusable, expiresIn, []string{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
autoGroups := []string{"group_1", "group_2"}
|
||||||
|
newKeyName := "my-new-test-key"
|
||||||
|
revoked := true
|
||||||
|
newKey, err := manager.SaveSetupKey(account.Id, &SetupKey{
|
||||||
|
Id: key.Id,
|
||||||
|
Name: newKeyName,
|
||||||
|
Revoked: revoked,
|
||||||
|
AutoGroups: autoGroups,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertKey(t, newKey, newKeyName, revoked, "reusable", 0, key.CreatedAt, key.ExpiresAt,
|
||||||
|
key.Id, time.Now(), autoGroups)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultAccountManager_CreateSetupKey(t *testing.T) {
|
||||||
|
manager, err := createManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := "test_user"
|
||||||
|
account, err := manager.GetOrCreateAccountByUser(userID, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = manager.SaveGroup(account.Id, &Group{
|
||||||
|
ID: "group_1",
|
||||||
|
Name: "group_name_1",
|
||||||
|
Peers: []string{},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = manager.SaveGroup(account.Id, &Group{
|
||||||
|
ID: "group_2",
|
||||||
|
Name: "group_name_2",
|
||||||
|
Peers: []string{},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
|
||||||
|
expectedKeyName string
|
||||||
|
expectedUsedTimes int
|
||||||
|
expectedType string
|
||||||
|
expectedGroups []string
|
||||||
|
expectedCreatedAt time.Time
|
||||||
|
expectedUpdatedAt time.Time
|
||||||
|
expectedExpiresAt time.Time
|
||||||
|
expectedFailure bool //indicates whether key creation should fail
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
expiresIn := time.Hour
|
||||||
|
testCase1 := testCase{
|
||||||
|
name: "Should Create Setup Key successfully",
|
||||||
|
expectedKeyName: "my-test-key",
|
||||||
|
expectedUsedTimes: 0,
|
||||||
|
expectedType: "reusable",
|
||||||
|
expectedGroups: []string{"group_1", "group_2"},
|
||||||
|
expectedCreatedAt: now,
|
||||||
|
expectedUpdatedAt: now,
|
||||||
|
expectedExpiresAt: now.Add(expiresIn),
|
||||||
|
expectedFailure: false,
|
||||||
|
}
|
||||||
|
testCase2 := testCase{
|
||||||
|
name: "Create Setup Key should fail because of unexistent group",
|
||||||
|
expectedKeyName: "my-test-key",
|
||||||
|
expectedGroups: []string{"FAKE"},
|
||||||
|
expectedFailure: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tCase := range []testCase{testCase1, testCase2} {
|
||||||
|
t.Run(tCase.name, func(t *testing.T) {
|
||||||
|
key, err := manager.CreateSetupKey(account.Id, tCase.expectedKeyName, SetupKeyReusable, expiresIn,
|
||||||
|
tCase.expectedGroups)
|
||||||
|
|
||||||
|
if tCase.expectedFailure {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected to fail")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertKey(t, key, tCase.expectedKeyName, false, tCase.expectedType, tCase.expectedUsedTimes,
|
||||||
|
tCase.expectedCreatedAt, tCase.expectedExpiresAt, strconv.Itoa(int(Hash(key.Key))),
|
||||||
|
tCase.expectedUpdatedAt, tCase.expectedGroups)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestGenerateDefaultSetupKey(t *testing.T) {
|
func TestGenerateDefaultSetupKey(t *testing.T) {
|
||||||
expectedName := "Default key"
|
expectedName := "Default key"
|
||||||
expectedRevoke := false
|
expectedRevoke := false
|
||||||
expectedType := "reusable"
|
expectedType := "reusable"
|
||||||
expectedUsedTimes := 0
|
expectedUsedTimes := 0
|
||||||
expectedCreatedAt := time.Now()
|
expectedCreatedAt := time.Now()
|
||||||
|
expectedUpdatedAt := time.Now()
|
||||||
expectedExpiresAt := time.Now().Add(24 * 30 * time.Hour)
|
expectedExpiresAt := time.Now().Add(24 * 30 * time.Hour)
|
||||||
|
var expectedAutoGroups []string
|
||||||
|
|
||||||
key := GenerateDefaultSetupKey()
|
key := GenerateDefaultSetupKey()
|
||||||
|
|
||||||
assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt,
|
assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt,
|
||||||
expectedExpiresAt, strconv.Itoa(int(Hash(key.Key))))
|
expectedExpiresAt, strconv.Itoa(int(Hash(key.Key))), expectedUpdatedAt, expectedAutoGroups)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,41 +165,44 @@ func TestGenerateSetupKey(t *testing.T) {
|
|||||||
expectedUsedTimes := 0
|
expectedUsedTimes := 0
|
||||||
expectedCreatedAt := time.Now()
|
expectedCreatedAt := time.Now()
|
||||||
expectedExpiresAt := time.Now().Add(time.Hour)
|
expectedExpiresAt := time.Now().Add(time.Hour)
|
||||||
|
expectedUpdatedAt := time.Now()
|
||||||
|
var expectedAutoGroups []string
|
||||||
|
|
||||||
key := GenerateSetupKey(expectedName, SetupKeyOneOff, time.Hour)
|
key := GenerateSetupKey(expectedName, SetupKeyOneOff, time.Hour, []string{})
|
||||||
|
|
||||||
assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt, expectedExpiresAt, strconv.Itoa(int(Hash(key.Key))))
|
assertKey(t, key, expectedName, expectedRevoke, expectedType, expectedUsedTimes, expectedCreatedAt,
|
||||||
|
expectedExpiresAt, strconv.Itoa(int(Hash(key.Key))), expectedUpdatedAt, expectedAutoGroups)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetupKey_IsValid(t *testing.T) {
|
func TestSetupKey_IsValid(t *testing.T) {
|
||||||
validKey := GenerateSetupKey("valid key", SetupKeyOneOff, time.Hour)
|
validKey := GenerateSetupKey("valid key", SetupKeyOneOff, time.Hour, []string{})
|
||||||
if !validKey.IsValid() {
|
if !validKey.IsValid() {
|
||||||
t.Errorf("expected key to be valid, got invalid %v", validKey)
|
t.Errorf("expected key to be valid, got invalid %v", validKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// expired
|
// expired
|
||||||
expiredKey := GenerateSetupKey("invalid key", SetupKeyOneOff, -time.Hour)
|
expiredKey := GenerateSetupKey("invalid key", SetupKeyOneOff, -time.Hour, []string{})
|
||||||
if expiredKey.IsValid() {
|
if expiredKey.IsValid() {
|
||||||
t.Errorf("expected key to be invalid due to expiration, got valid %v", expiredKey)
|
t.Errorf("expected key to be invalid due to expiration, got valid %v", expiredKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// revoked
|
// revoked
|
||||||
revokedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour)
|
revokedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour, []string{})
|
||||||
revokedKey.Revoked = true
|
revokedKey.Revoked = true
|
||||||
if revokedKey.IsValid() {
|
if revokedKey.IsValid() {
|
||||||
t.Errorf("expected revoked key to be invalid, got valid %v", revokedKey)
|
t.Errorf("expected revoked key to be invalid, got valid %v", revokedKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// overused
|
// overused
|
||||||
overUsedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour)
|
overUsedKey := GenerateSetupKey("invalid key", SetupKeyOneOff, time.Hour, []string{})
|
||||||
overUsedKey.UsedTimes = 1
|
overUsedKey.UsedTimes = 1
|
||||||
if overUsedKey.IsValid() {
|
if overUsedKey.IsValid() {
|
||||||
t.Errorf("expected overused key to be invalid, got valid %v", overUsedKey)
|
t.Errorf("expected overused key to be invalid, got valid %v", overUsedKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// overused
|
// overused
|
||||||
reusableKey := GenerateSetupKey("valid key", SetupKeyReusable, time.Hour)
|
reusableKey := GenerateSetupKey("valid key", SetupKeyReusable, time.Hour, []string{})
|
||||||
reusableKey.UsedTimes = 99
|
reusableKey.UsedTimes = 99
|
||||||
if !reusableKey.IsValid() {
|
if !reusableKey.IsValid() {
|
||||||
t.Errorf("expected reusable key to be valid when used many times, got valid %v", reusableKey)
|
t.Errorf("expected reusable key to be valid when used many times, got valid %v", reusableKey)
|
||||||
@@ -71,7 +210,8 @@ func TestSetupKey_IsValid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func assertKey(t *testing.T, key *SetupKey, expectedName string, expectedRevoke bool, expectedType string,
|
func assertKey(t *testing.T, key *SetupKey, expectedName string, expectedRevoke bool, expectedType string,
|
||||||
expectedUsedTimes int, expectedCreatedAt time.Time, expectedExpiresAt time.Time, expectedID string) {
|
expectedUsedTimes int, expectedCreatedAt time.Time, expectedExpiresAt time.Time, expectedID string,
|
||||||
|
expectedUpdatedAt time.Time, expectedAutoGroups []string) {
|
||||||
if key.Name != expectedName {
|
if key.Name != expectedName {
|
||||||
t.Errorf("expected setup key to have Name %v, got %v", expectedName, key.Name)
|
t.Errorf("expected setup key to have Name %v, got %v", expectedName, key.Name)
|
||||||
}
|
}
|
||||||
@@ -92,6 +232,10 @@ func assertKey(t *testing.T, key *SetupKey, expectedName string, expectedRevoke
|
|||||||
t.Errorf("expected setup key to have ExpiresAt ~ %v, got %v", expectedExpiresAt, key.ExpiresAt)
|
t.Errorf("expected setup key to have ExpiresAt ~ %v, got %v", expectedExpiresAt, key.ExpiresAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if key.UpdatedAt.Sub(expectedUpdatedAt).Round(time.Hour) != 0 {
|
||||||
|
t.Errorf("expected setup key to have UpdatedAt ~ %v, got %v", expectedUpdatedAt, key.UpdatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
if key.CreatedAt.Sub(expectedCreatedAt).Round(time.Hour) != 0 {
|
if key.CreatedAt.Sub(expectedCreatedAt).Round(time.Hour) != 0 {
|
||||||
t.Errorf("expected setup key to have CreatedAt ~ %v, got %v", expectedCreatedAt, key.CreatedAt)
|
t.Errorf("expected setup key to have CreatedAt ~ %v, got %v", expectedCreatedAt, key.CreatedAt)
|
||||||
}
|
}
|
||||||
@@ -104,13 +248,19 @@ func assertKey(t *testing.T, key *SetupKey, expectedName string, expectedRevoke
|
|||||||
if key.Id != strconv.Itoa(int(Hash(key.Key))) {
|
if key.Id != strconv.Itoa(int(Hash(key.Key))) {
|
||||||
t.Errorf("expected key Id t= %v, got %v", expectedID, key.Id)
|
t.Errorf("expected key Id t= %v, got %v", expectedID, key.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(key.AutoGroups) != len(expectedAutoGroups) {
|
||||||
|
t.Errorf("expected key AutoGroups size=%d, got %d", len(expectedAutoGroups), len(key.AutoGroups))
|
||||||
|
}
|
||||||
|
assert.ElementsMatch(t, key.AutoGroups, expectedAutoGroups, "expected key AutoGroups to be equal")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetupKey_Copy(t *testing.T) {
|
func TestSetupKey_Copy(t *testing.T) {
|
||||||
|
|
||||||
key := GenerateSetupKey("key name", SetupKeyOneOff, time.Hour)
|
key := GenerateSetupKey("key name", SetupKeyOneOff, time.Hour, []string{})
|
||||||
keyCopy := key.Copy()
|
keyCopy := key.Copy()
|
||||||
|
|
||||||
assertKey(t, keyCopy, key.Name, key.Revoked, string(key.Type), key.UsedTimes, key.CreatedAt, key.ExpiresAt, key.Id)
|
assertKey(t, keyCopy, key.Name, key.Revoked, string(key.Type), key.UsedTimes, key.CreatedAt, key.ExpiresAt, key.Id,
|
||||||
|
key.UpdatedAt, key.AutoGroups)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,4 +21,6 @@ type Store interface {
|
|||||||
SaveAccount(account *Account) error
|
SaveAccount(account *Account) error
|
||||||
GetPeerRoutes(peerKey string) ([]*route.Route, error)
|
GetPeerRoutes(peerKey string) ([]*route.Route, error)
|
||||||
GetRoutesByPrefix(accountID string, prefix netip.Prefix) ([]*route.Route, error)
|
GetRoutesByPrefix(accountID string, prefix netip.Prefix) ([]*route.Route, error)
|
||||||
|
GetInstallationID() string
|
||||||
|
SaveInstallationID(id string) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,3 +70,14 @@ func (p *PeersUpdateManager) CloseChannel(peerKey string) {
|
|||||||
|
|
||||||
log.Debugf("closed updates channel of a peer %s", peerKey)
|
log.Debugf("closed updates channel of a peer %s", peerKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllConnectedPeers returns a copy of the connected peers map
|
||||||
|
func (p *PeersUpdateManager) GetAllConnectedPeers() map[string]struct{} {
|
||||||
|
p.channelsMux.Lock()
|
||||||
|
defer p.channelsMux.Unlock()
|
||||||
|
m := make(map[string]struct{})
|
||||||
|
for key := range p.peerChannels {
|
||||||
|
m[key] = struct{}{}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,40 +2,103 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"github.com/netbirdio/netbird/management/server/idp"
|
||||||
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UserRoleAdmin UserRole = "admin"
|
UserRoleAdmin UserRole = "admin"
|
||||||
UserRoleUser UserRole = "user"
|
UserRoleUser UserRole = "user"
|
||||||
|
UserRoleUnknown UserRole = "unknown"
|
||||||
|
|
||||||
|
UserStatusActive UserStatus = "active"
|
||||||
|
UserStatusDisabled UserStatus = "disabled"
|
||||||
|
UserStatusInvited UserStatus = "invited"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserRole is the role of the User
|
// StrRoleToUserRole returns UserRole for a given strRole or UserRoleUnknown if the specified role is unknown
|
||||||
|
func StrRoleToUserRole(strRole string) UserRole {
|
||||||
|
switch strings.ToLower(strRole) {
|
||||||
|
case "admin":
|
||||||
|
return UserRoleAdmin
|
||||||
|
case "user":
|
||||||
|
return UserRoleUser
|
||||||
|
default:
|
||||||
|
return UserRoleUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserStatus is the status of a User
|
||||||
|
type UserStatus string
|
||||||
|
|
||||||
|
// UserRole is the role of a User
|
||||||
type UserRole string
|
type UserRole string
|
||||||
|
|
||||||
// User represents a user of the system
|
// User represents a user of the system
|
||||||
type User struct {
|
type User struct {
|
||||||
Id string
|
Id string
|
||||||
Role UserRole
|
Role UserRole
|
||||||
|
// AutoGroups is a list of Group IDs to auto-assign to peers registered by this user
|
||||||
|
AutoGroups []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toUserInfo converts a User object to a UserInfo object.
|
||||||
|
func (u *User) toUserInfo(userData *idp.UserData) (*UserInfo, error) {
|
||||||
|
autoGroups := u.AutoGroups
|
||||||
|
if autoGroups == nil {
|
||||||
|
autoGroups = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if userData == nil {
|
||||||
|
return &UserInfo{
|
||||||
|
ID: u.Id,
|
||||||
|
Email: "",
|
||||||
|
Name: "",
|
||||||
|
Role: string(u.Role),
|
||||||
|
AutoGroups: u.AutoGroups,
|
||||||
|
Status: string(UserStatusActive),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if userData.ID != u.Id {
|
||||||
|
return nil, fmt.Errorf("wrong UserData provided for user %s", u.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
userStatus := UserStatusActive
|
||||||
|
if userData.AppMetadata.WTPendingInvite {
|
||||||
|
userStatus = UserStatusInvited
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserInfo{
|
||||||
|
ID: u.Id,
|
||||||
|
Email: userData.Email,
|
||||||
|
Name: userData.Name,
|
||||||
|
Role: string(u.Role),
|
||||||
|
AutoGroups: autoGroups,
|
||||||
|
Status: string(userStatus),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the user
|
||||||
func (u *User) Copy() *User {
|
func (u *User) Copy() *User {
|
||||||
|
autoGroups := make([]string, 0)
|
||||||
|
autoGroups = append(autoGroups, u.AutoGroups...)
|
||||||
return &User{
|
return &User{
|
||||||
Id: u.Id,
|
Id: u.Id,
|
||||||
Role: u.Role,
|
Role: u.Role,
|
||||||
|
AutoGroups: autoGroups,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUser creates a new user
|
// NewUser creates a new user
|
||||||
func NewUser(id string, role UserRole) *User {
|
func NewUser(id string, role UserRole) *User {
|
||||||
return &User{
|
return &User{
|
||||||
Id: id,
|
Id: id,
|
||||||
Role: role,
|
Role: role,
|
||||||
|
AutoGroups: []string{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +112,122 @@ func NewAdminUser(id string) *User {
|
|||||||
return NewUser(id, UserRoleAdmin)
|
return NewUser(id, UserRoleAdmin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateUser creates a new user under the given account. Effectively this is a user invite.
|
||||||
|
func (am *DefaultAccountManager) CreateUser(accountID string, invite *UserInfo) (*UserInfo, error) {
|
||||||
|
am.mux.Lock()
|
||||||
|
defer am.mux.Unlock()
|
||||||
|
|
||||||
|
if am.idpManager == nil {
|
||||||
|
return nil, Errorf(PreconditionFailed, "IdP manager must be enabled to send user invites")
|
||||||
|
}
|
||||||
|
|
||||||
|
if invite == nil {
|
||||||
|
return nil, fmt.Errorf("provided user update is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := am.Store.GetAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Errorf(AccountNotFound, "account %s doesn't exist", accountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the user is already registered with this email => reject
|
||||||
|
user, err := am.lookupUserInCacheByEmail(invite.Email, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user != nil {
|
||||||
|
return nil, Errorf(UserAlreadyExists, "user has an existing account")
|
||||||
|
}
|
||||||
|
|
||||||
|
users, err := am.idpManager.GetUserByEmail(invite.Email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(users) > 0 {
|
||||||
|
return nil, Errorf(UserAlreadyExists, "user has an existing account")
|
||||||
|
}
|
||||||
|
|
||||||
|
idpUser, err := am.idpManager.CreateUser(invite.Email, invite.Name, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
role := StrRoleToUserRole(invite.Role)
|
||||||
|
newUser := &User{
|
||||||
|
Id: idpUser.ID,
|
||||||
|
Role: role,
|
||||||
|
AutoGroups: invite.AutoGroups,
|
||||||
|
}
|
||||||
|
account.Users[idpUser.ID] = newUser
|
||||||
|
|
||||||
|
err = am.Store.SaveAccount(account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = am.refreshCache(account.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newUser.toUserInfo(idpUser)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveUser saves updates a given user. If the user doesn't exit it will throw status.NotFound error.
|
||||||
|
// Only User.AutoGroups field is allowed to be updated for now.
|
||||||
|
func (am *DefaultAccountManager) SaveUser(accountID string, update *User) (*UserInfo, error) {
|
||||||
|
am.mux.Lock()
|
||||||
|
defer am.mux.Unlock()
|
||||||
|
|
||||||
|
if update == nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "provided user update is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := am.Store.GetAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, newGroupID := range update.AutoGroups {
|
||||||
|
if _, ok := account.Groups[newGroupID]; !ok {
|
||||||
|
return nil,
|
||||||
|
status.Errorf(codes.InvalidArgument, "provided group ID %s in the user %s update doesn't exist",
|
||||||
|
newGroupID, update.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldUser := account.Users[update.Id]
|
||||||
|
if oldUser == nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "update not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// only auto groups, revoked status, and name can be updated for now
|
||||||
|
newUser := oldUser.Copy()
|
||||||
|
newUser.AutoGroups = update.AutoGroups
|
||||||
|
newUser.Role = update.Role
|
||||||
|
|
||||||
|
account.Users[newUser.Id] = newUser
|
||||||
|
|
||||||
|
if err = am.Store.SaveAccount(account); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isNil(am.idpManager) {
|
||||||
|
userData, err := am.lookupUserInCache(newUser.Id, account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if userData == nil {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "user %s not found in the IdP", newUser.Id)
|
||||||
|
}
|
||||||
|
return newUser.toUserInfo(userData)
|
||||||
|
}
|
||||||
|
return newUser.toUserInfo(nil)
|
||||||
|
}
|
||||||
|
|
||||||
// GetOrCreateAccountByUser returns an existing account for a given user id or creates a new one if doesn't exist
|
// GetOrCreateAccountByUser returns an existing account for a given user id or creates a new one if doesn't exist
|
||||||
func (am *DefaultAccountManager) GetOrCreateAccountByUser(userId, domain string) (*Account, error) {
|
func (am *DefaultAccountManager) GetOrCreateAccountByUser(userId, domain string) (*Account, error) {
|
||||||
am.mux.Lock()
|
am.mux.Lock()
|
||||||
@@ -96,7 +275,7 @@ func (am *DefaultAccountManager) GetAccountByUser(userId string) (*Account, erro
|
|||||||
|
|
||||||
// IsUserAdmin flag for current user authenticated by JWT token
|
// IsUserAdmin flag for current user authenticated by JWT token
|
||||||
func (am *DefaultAccountManager) IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error) {
|
func (am *DefaultAccountManager) IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error) {
|
||||||
account, err := am.GetAccountWithAuthorizationClaims(claims)
|
account, err := am.GetAccountFromToken(claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("get account: %v", err)
|
return false, fmt.Errorf("get account: %v", err)
|
||||||
}
|
}
|
||||||
@@ -108,3 +287,50 @@ func (am *DefaultAccountManager) IsUserAdmin(claims jwtclaims.AuthorizationClaim
|
|||||||
|
|
||||||
return user.Role == UserRoleAdmin, nil
|
return user.Role == UserRoleAdmin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsersFromAccount performs a batched request for users from IDP by account ID
|
||||||
|
func (am *DefaultAccountManager) GetUsersFromAccount(accountID string) ([]*UserInfo, error) {
|
||||||
|
account, err := am.GetAccountById(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
queriedUsers := make([]*idp.UserData, 0)
|
||||||
|
if !isNil(am.idpManager) {
|
||||||
|
users := make(map[string]struct{}, len(account.Users))
|
||||||
|
for _, user := range account.Users {
|
||||||
|
users[user.Id] = struct{}{}
|
||||||
|
}
|
||||||
|
queriedUsers, err = am.lookupCache(users, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfos := make([]*UserInfo, 0)
|
||||||
|
|
||||||
|
// in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo
|
||||||
|
if len(queriedUsers) == 0 {
|
||||||
|
for _, user := range account.Users {
|
||||||
|
info, err := user.toUserInfo(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userInfos = append(userInfos, info)
|
||||||
|
}
|
||||||
|
return userInfos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, queriedUser := range queriedUsers {
|
||||||
|
if localUser, contains := account.Users[queriedUser.ID]; contains {
|
||||||
|
|
||||||
|
info, err := localUser.toUserInfo(queriedUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userInfos = append(userInfos, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfos, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func NewClient(ctx context.Context, addr string, key wgtypes.Key, tlsEnabled boo
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//defaultBackoff is a basic backoff mechanism for general issues
|
// defaultBackoff is a basic backoff mechanism for general issues
|
||||||
func defaultBackoff(ctx context.Context) backoff.BackOff {
|
func defaultBackoff(ctx context.Context) backoff.BackOff {
|
||||||
return backoff.WithContext(&backoff.ExponentialBackOff{
|
return backoff.WithContext(&backoff.ExponentialBackOff{
|
||||||
InitialInterval: 800 * time.Millisecond,
|
InitialInterval: 800 * time.Millisecond,
|
||||||
@@ -121,9 +121,11 @@ func (c *GrpcClient) Receive(msgHandler func(msg *proto.Message) error) error {
|
|||||||
return fmt.Errorf("connection to signal is not ready and in %s state", connState)
|
return fmt.Errorf("connection to signal is not ready and in %s state", connState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect to Signal stream identifying ourselves with a public Wireguard key
|
// connect to Signal stream identifying ourselves with a public WireGuard key
|
||||||
// todo once the key rotation logic has been implemented, consider changing to some other identifier (received from management)
|
// todo once the key rotation logic has been implemented, consider changing to some other identifier (received from management)
|
||||||
stream, err := c.connect(c.key.PublicKey().String())
|
ctx, cancelStream := context.WithCancel(c.ctx)
|
||||||
|
defer cancelStream()
|
||||||
|
stream, err := c.connect(ctx, c.key.PublicKey().String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("disconnected from the Signal Exchange due to an error: %v", err)
|
log.Warnf("disconnected from the Signal Exchange due to an error: %v", err)
|
||||||
return err
|
return err
|
||||||
@@ -180,15 +182,13 @@ func (c *GrpcClient) getStreamStatusChan() <-chan struct{} {
|
|||||||
return c.connectedCh
|
return c.connectedCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GrpcClient) connect(key string) (proto.SignalExchange_ConnectStreamClient, error) {
|
func (c *GrpcClient) connect(ctx context.Context, key string) (proto.SignalExchange_ConnectStreamClient, error) {
|
||||||
c.stream = nil
|
c.stream = nil
|
||||||
|
|
||||||
// add key fingerprint to the request header to be identified on the server side
|
// add key fingerprint to the request header to be identified on the server side
|
||||||
md := metadata.New(map[string]string{proto.HeaderId: key})
|
md := metadata.New(map[string]string{proto.HeaderId: key})
|
||||||
ctx := metadata.NewOutgoingContext(c.ctx, md)
|
metaCtx := metadata.NewOutgoingContext(ctx, md)
|
||||||
|
stream, err := c.realClient.ConnectStream(metaCtx, grpc.WaitForReady(true))
|
||||||
stream, err := c.realClient.ConnectStream(ctx, grpc.WaitForReady(true))
|
|
||||||
|
|
||||||
c.stream = stream
|
c.stream = stream
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
Reference in New Issue
Block a user