mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-02 07:33:52 -04:00
Compare commits
21 Commits
v0.6.0
...
users_cach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c86c620016 | ||
|
|
1e444f58c1 | ||
|
|
f53990d6c1 | ||
|
|
02a6ac44be | ||
|
|
43e472c958 | ||
|
|
60ac8c3268 | ||
|
|
cea5693512 | ||
|
|
49ec33504a | ||
|
|
2e5d4ba6fa | ||
|
|
0fbe78375e | ||
|
|
87631cbc8b | ||
|
|
ec39202590 | ||
|
|
b227a7c34e | ||
|
|
c86bacb5c3 | ||
|
|
59a964eed8 | ||
|
|
feff6dc966 | ||
|
|
258cb3d43b | ||
|
|
4088aaf6fe | ||
|
|
1bb504ea78 | ||
|
|
594da0a6b8 | ||
|
|
889fa646fc |
219
README.md
219
README.md
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<strong>Big News! Wiretrustee becomes Netbird</strong>.
|
||||
<a href="https://netbird.io/blog/wiretrustee-becomes-netbird">
|
||||
<strong>:hatching_chick: New release! Beta Update May 2022</strong>.
|
||||
<a href="https://github.com/netbirdio/netbird/releases/tag/v0.6.0">
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
@@ -18,8 +18,7 @@
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/wiretrustee/wiretrustee/tags">
|
||||
<img src="https://img.shields.io/docker/pulls/wiretrustee/wiretrustee" />
|
||||
</a>
|
||||
<img src="https://badgen.net/badge/Open%20Source%3F/Yes%21/blue?icon=github" />
|
||||
</a>
|
||||
<br>
|
||||
<a href="https://www.codacy.com/gh/wiretrustee/wiretrustee/dashboard?utm_source=github.com&utm_medium=referral&utm_content=wiretrustee/wiretrustee&utm_campaign=Badge_Grade"><img src="https://app.codacy.com/project/badge/Grade/d366de2c9d8b4cf982da27f8f5831809"/></a>
|
||||
<a href="https://goreportcard.com/report/wiretrustee/wiretrustee">
|
||||
@@ -35,7 +34,7 @@
|
||||
|
||||
<p align="center">
|
||||
<strong>
|
||||
Start using Netbird at <a href="https://app.netbird.io/">app.netbird.io</a>
|
||||
Start using NetBird at <a href="https://app.netbird.io/">app.netbird.io</a>
|
||||
<br/>
|
||||
See <a href="https://netbird.io/docs/">Documentation</a>
|
||||
<br/>
|
||||
@@ -47,15 +46,15 @@
|
||||
|
||||
<br>
|
||||
|
||||
**Netbird is an open-source VPN management platform built on top of WireGuard® making it easy to create secure private networks for your organization or home.**
|
||||
**NetBird is an open-source VPN management platform built on top of WireGuard® making it easy to create secure private networks for your organization or home.**
|
||||
|
||||
It requires zero configuration effort leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
||||
|
||||
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:**
|
||||
* Automatic IP allocation and management.
|
||||
* Automatic peer (machine) discovery and configuration.
|
||||
* Automatic WireGuard peer (machine) discovery and configuration.
|
||||
* Encrypted peer-to-peer connections without a central VPN gateway.
|
||||
* Connection relay fallback in case a peer-to-peer connection is not possible.
|
||||
* Network management layer with a neat Web UI panel ([separate repo](https://github.com/netbirdio/dashboard))
|
||||
@@ -78,23 +77,26 @@ Netbird creates an overlay peer-to-peer network connecting machines automaticall
|
||||
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
||||
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
|
||||
|
||||
Hosted version: [https://app.netbird.io/](https://app.netbird.io/)
|
||||
|
||||
[Web UI repository](https://github.com/netbirdio/dashboard)
|
||||
### Start using NetBird
|
||||
* Hosted version: [https://app.netbird.io/](https://app.netbird.io/).
|
||||
* See our documentation for [Quickstart Guide](https://netbird.io/docs/getting-started/quickstart).
|
||||
* If you are looking to self-host NetBird, check our [Self-Hosting Guide](https://netbird.io/docs/getting-started/self-hosting).
|
||||
* Step-by-step [Installation Guide](https://netbird.io/docs/getting-started/installation) for different platforms.
|
||||
* Web UI [repository](https://github.com/netbirdio/dashboard).
|
||||
* 5 min [demo video](https://youtu.be/Tu9tPsUWaY0) on YouTube.
|
||||
|
||||
|
||||
### A bit on Netbird internals
|
||||
* Every machine in the network runs [Netbird Agent (or Client)](client/) that manages WireGuard.
|
||||
* Netbird features a [Management Service](management/) that offers peer IP management and network updates distribution (e.g. when a new machine joins the network others are getting notified if allowed by access controls). Simply put, this service holds the state of the network.
|
||||
### A bit on NetBird internals
|
||||
* Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
|
||||
* NetBird features [Management Service](management/) that holds network state, manages peer IPs, and distributes network updates to peers.
|
||||
* Every agent is connected to Management Service.
|
||||
* Netbird agent uses WebRTC ICE implemented in [pion/ice library](https://github.com/pion/ice) to discover connection candidates when establishing a peer-to-peer connection between machines.
|
||||
* NetBird agent uses WebRTC ICE implemented in [pion/ice library](https://github.com/pion/ice) to discover connection candidates when establishing a peer-to-peer connection between machines.
|
||||
* Connection candidates are discovered with a help of [STUN](https://en.wikipedia.org/wiki/STUN) server.
|
||||
* Agents negotiate a connection through [Signal Service](signal/).
|
||||
* Signal Service uses public Wireguard keys to route messages between peers.
|
||||
Contents of the messages sent between peers through the signaling server are encrypted with Wireguard keys, making it impossible to inspect them.
|
||||
* Sometimes the NAT traversal is unsuccessful due to strict NATs (e.g. mobile carrier-grade NAT) and p2p connection isn't possible. When this occurs the system falls back to a relay server called [TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT), and a secure Wireguard tunnel is established via the TURN server.
|
||||
* Agents negotiate a connection through [Signal Service](signal/) passing p2p encrypted messages.
|
||||
* Signal Service uses public WireGuard keys to route messages between peers.
|
||||
* Sometimes the NAT traversal is unsuccessful due to strict NATs (e.g. mobile carrier-grade NAT) and p2p connection isn't possible. When this occurs the system falls back to a relay server called [TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT), and a secure WireGuard tunnel is established via the TURN server.
|
||||
|
||||
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in Netbird setups.
|
||||
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in NetBird setups.
|
||||
|
||||
<p float="left" align="middle">
|
||||
<img src="https://netbird.io/docs/img/architecture/high-level-dia.png" width="700"/>
|
||||
@@ -102,182 +104,11 @@ Hosted version: [https://app.netbird.io/](https://app.netbird.io/)
|
||||
|
||||
See a complete [architecture overview](https://netbird.io/docs/overview/architecture) for details.
|
||||
|
||||
**Testimonials:** We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), and [Coturn](https://github.com/coturn/coturn). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
|
||||
|
||||
### Product Roadmap
|
||||
### Roadmap
|
||||
- [Public Roadmap](https://github.com/netbirdio/netbird/projects/2)
|
||||
|
||||
### Client Installation
|
||||
#### Linux
|
||||
|
||||
**APT/Debian**
|
||||
1. Add the repository:
|
||||
```shell
|
||||
sudo apt-get update
|
||||
sudo apt-get install ca-certificates curl gnupg -y
|
||||
curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo apt-key add -
|
||||
echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list
|
||||
```
|
||||
2. Update APT's cache
|
||||
```shell
|
||||
sudo apt-get update
|
||||
```
|
||||
3. Install the package
|
||||
```shell
|
||||
# for CLI only
|
||||
sudo apt-get install netbird
|
||||
# for GUI package
|
||||
sudo apt-get install netbird-ui
|
||||
```
|
||||
**RPM/Red hat**
|
||||
1. Add the repository:
|
||||
```shell
|
||||
cat <<EOF | sudo tee /etc/yum.repos.d/wiretrustee.repo
|
||||
[Wiretrustee]
|
||||
name=Wiretrustee
|
||||
baseurl=https://pkgs.wiretrustee.com/yum/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
gpgkey=https://pkgs.wiretrustee.com/yum/repodata/repomd.xml.key
|
||||
repo_gpgcheck=1
|
||||
EOF
|
||||
```
|
||||
2. Install the package
|
||||
```shell
|
||||
# for CLI only
|
||||
sudo yum install netbird
|
||||
# for GUI package
|
||||
sudo yum install netbird-ui
|
||||
```
|
||||
#### MACOS
|
||||
**Homebrew install**
|
||||
1. Download and install homebrew at https://brew.sh/
|
||||
2. If wiretrustee was previously installed with homebrew, you will need to run:
|
||||
```shell
|
||||
# Stop and uninstall daemon service:
|
||||
sudo wiretrustee service stop
|
||||
sudo wiretrustee service uninstall
|
||||
# unlik the app
|
||||
brew unlink wiretrustee
|
||||
```
|
||||
> netbird will copy any existing configuration from the Wiretrustee's default configuration paths to the new Netbird's default location
|
||||
3. Install the client
|
||||
```shell
|
||||
# for CLI only
|
||||
brew install netbirdio/tap/netbird
|
||||
# for GUI package
|
||||
brew install --cask netbirdio/tap/netbird-ui
|
||||
```
|
||||
4. If you are install CLI only, you need to install and start the client daemon service:
|
||||
```shell
|
||||
sudo netbird service install
|
||||
sudo netbird service start
|
||||
```
|
||||
**Installation from binary (CLI only)**
|
||||
1. Checkout Netbird [releases](https://github.com/netbirdio/netbird/releases/latest)
|
||||
2. Download the latest release (**Switch VERSION to the latest**):
|
||||
```shell
|
||||
curl -o ./netbird_<VERSION>_darwin_amd64.tar.gz https://github.com/netbirdio/netbird/releases/download/v<VERSION>/wiretrustee_<VERSION>_darwin_amd64.tar.gz
|
||||
```
|
||||
3. Decompress
|
||||
```shell
|
||||
tar xcf ./netbird_<VERSION>_darwin_amd64.tar.gz
|
||||
sudo mv netbird /usr/bin/netbird
|
||||
chmod +x /usr/bin/netbird
|
||||
```
|
||||
After that you may need to add /usr/bin in your PATH environment variable:
|
||||
````shell
|
||||
export PATH=$PATH:/usr/bin
|
||||
````
|
||||
4. Install and run the service
|
||||
```shell
|
||||
sudo netbird service install
|
||||
sudo netbird service start
|
||||
```
|
||||
|
||||
#### Windows
|
||||
1. Checkout Netbird [releases](https://github.com/netbirdio/netbird/releases/latest)
|
||||
2. Download the latest Windows release installer ```netbird_installer_<VERSION>_windows_amd64.exe``` (**Switch VERSION to the latest**):
|
||||
3. Proceed with installation steps
|
||||
4. This will install the client in the C:\\Program Files\\Netbird and add the client service
|
||||
5. After installing, you can follow the [Client Configuration](#Client-Configuration) steps.
|
||||
> To uninstall the client and service, you can use Add/Remove programs
|
||||
|
||||
### Client Configuration
|
||||
If you installed the UI client, you can launch it and click on Connect
|
||||
> It will open your browser, and you will be prompt for email and password
|
||||
|
||||
Simply run:
|
||||
```shell
|
||||
netbird up
|
||||
```
|
||||
> It will open your browser, and you will be prompt for email and password
|
||||
|
||||
Check connection status:
|
||||
```shell
|
||||
netbird status
|
||||
```
|
||||
In case you are activating a server peer, you can use a setup-key as described in the steps below:
|
||||
|
||||
|
||||
1. Login to the Management Service. You need to have a `setup key` in hand (see ).
|
||||
|
||||
For all systems:
|
||||
```shell
|
||||
netbird up --setup-key <SETUP KEY>
|
||||
```
|
||||
|
||||
For **Docker**, you can run with the following command:
|
||||
```shell
|
||||
docker run --network host --privileged --rm -d -e NB_SETUP_KEY=<SETUP KEY> -v netbird-client:/etc/netbird netbirdio/netbird:<TAG>
|
||||
```
|
||||
> TAG > 0.6.0 version
|
||||
|
||||
Alternatively, if you are hosting your own Management Service provide `--management-url` property pointing to your Management Service:
|
||||
```shell
|
||||
sudo netbird up --setup-key <SETUP KEY> --management-url http://localhost:33073
|
||||
```
|
||||
|
||||
> You could also omit the `--setup-key` property. In this case, the tool will prompt for the key.
|
||||
|
||||
2. Check connection status:
|
||||
```shell
|
||||
netbird status
|
||||
```
|
||||
|
||||
3. Check your IP:
|
||||
For **MACOS** you will just start the service:
|
||||
````shell
|
||||
sudo ifconfig utun100
|
||||
````
|
||||
For **Linux** systems:
|
||||
```shell
|
||||
ip addr show wt0
|
||||
```
|
||||
For **Windows** systems:
|
||||
```shell
|
||||
netsh interface ip show config name="wt0"
|
||||
```
|
||||
|
||||
4. Repeat on other machines.
|
||||
|
||||
### Troubleshooting
|
||||
1. If you are using self-hosted version and haven't specified `--management-url`, the client app will use the default URL
|
||||
which is ```https://api.wiretrustee.com:33073```.
|
||||
|
||||
2. If you have specified a wrong `--management-url` (e.g., just by mistake when self-hosting)
|
||||
to override it you can do the following:
|
||||
|
||||
```shell
|
||||
netbird down
|
||||
netbird up --management-url https://<CORRECT HOST:PORT>/
|
||||
```
|
||||
|
||||
To override it see solution #1 above.
|
||||
|
||||
### Running Dashboard, Management, Signal and Coturn
|
||||
See [Self-Hosting Guide](https://netbird.io/docs/getting-started/self-hosting)
|
||||
|
||||
### Testimonials
|
||||
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), and [Coturn](https://github.com/coturn/coturn). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
|
||||
|
||||
### Legal
|
||||
[WireGuard](https://wireguard.com/) is a registered trademark of Jason A. Donenfeld.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM gcr.io/distroless/base:debug
|
||||
ENV WT_LOG_FILE=console
|
||||
ENTRYPOINT [ "/go/bin/netbird","up"]
|
||||
COPY netbird /go/bin/netbird
|
||||
COPY netbird /go/bin/netbird
|
||||
|
||||
@@ -92,7 +92,7 @@ var loginCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
if loginResp.NeedsSSOLogin {
|
||||
openURL(cmd, loginResp.VerificationURI, loginResp.VerificationURIComplete, loginResp.UserCode)
|
||||
openURL(cmd, loginResp.VerificationURIComplete)
|
||||
|
||||
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
|
||||
if err != nil {
|
||||
@@ -175,7 +175,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
|
||||
return nil, fmt.Errorf("getting a request device code failed: %v", err)
|
||||
}
|
||||
|
||||
openURL(cmd, flowInfo.VerificationURI, flowInfo.VerificationURIComplete, flowInfo.UserCode)
|
||||
openURL(cmd, flowInfo.VerificationURIComplete)
|
||||
|
||||
waitTimeout := time.Duration(flowInfo.ExpiresIn)
|
||||
waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout*time.Second)
|
||||
@@ -189,13 +189,12 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
|
||||
return &tokenInfo, nil
|
||||
}
|
||||
|
||||
func openURL(cmd *cobra.Command, verificationURI, verificationURIComplete, userCode string) {
|
||||
func openURL(cmd *cobra.Command, verificationURIComplete string) {
|
||||
err := open.Run(verificationURIComplete)
|
||||
cmd.Printf("Please do the SSO login in your browser. \n" +
|
||||
"If your browser didn't open automatically, use this URL to log in:\n\n" +
|
||||
" " + verificationURIComplete + " \n\n")
|
||||
if err != nil {
|
||||
cmd.Println("Unable to open the default browser.")
|
||||
cmd.Println("If this is not an interactive shell, you may want to use the setup key, see https://www.netbird.io/docs/overview/setup-keys")
|
||||
cmd.Printf("Otherwise, you can continue the login flow by accessing the url below:\n\t%s\n", verificationURI)
|
||||
cmd.Printf("Use the access code: %s\n", userCode)
|
||||
cmd.Println("Or press CTRL + C or COMMAND + C")
|
||||
cmd.Printf("Alternatively, you may want to use a setup key, see:\n\n https://www.netbird.io/docs/overview/setup-keys\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/codes"
|
||||
gstatus "google.golang.org/grpc/status"
|
||||
@@ -55,7 +56,13 @@ var upCmd = &cobra.Command{
|
||||
"If the daemon is not running please run: "+
|
||||
"\nnetbird service install \nnetbird service start\n", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
defer func() {
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
log.Warnf("failed closing dameon gRPC client connection %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
client := proto.NewDaemonServiceClient(conn)
|
||||
|
||||
@@ -64,55 +71,51 @@ var upCmd = &cobra.Command{
|
||||
return fmt.Errorf("unable to get daemon status: %v", err)
|
||||
}
|
||||
|
||||
if status.Status == string(internal.StatusNeedsLogin) || status.Status == string(internal.StatusLoginFailed) {
|
||||
loginRequest := proto.LoginRequest{
|
||||
SetupKey: setupKey,
|
||||
PreSharedKey: preSharedKey,
|
||||
ManagementUrl: managementURL,
|
||||
}
|
||||
|
||||
var loginErr error
|
||||
|
||||
var loginResp *proto.LoginResponse
|
||||
|
||||
err = WithBackOff(func() error {
|
||||
var backOffErr error
|
||||
loginResp, backOffErr = client.Login(ctx, &loginRequest)
|
||||
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
|
||||
s.Code() == codes.PermissionDenied ||
|
||||
s.Code() == codes.NotFound ||
|
||||
s.Code() == codes.Unimplemented) {
|
||||
loginErr = backOffErr
|
||||
return nil
|
||||
}
|
||||
return backOffErr
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("login backoff cycle failed: %v", err)
|
||||
}
|
||||
|
||||
if loginErr != nil {
|
||||
return fmt.Errorf("login failed: %v", loginErr)
|
||||
}
|
||||
|
||||
if loginResp.NeedsSSOLogin {
|
||||
|
||||
openURL(cmd, loginResp.VerificationURI, loginResp.VerificationURIComplete, loginResp.UserCode)
|
||||
|
||||
cmd.Printf("Please do the SSO login in your browser. \n" +
|
||||
"If your browser didn't open automatically, use this URL to log in:\n\n" +
|
||||
" " + loginResp.VerificationURIComplete + " \n\n")
|
||||
|
||||
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
|
||||
if err != nil {
|
||||
return fmt.Errorf("waiting sso login failed with: %v", err)
|
||||
}
|
||||
}
|
||||
} else if status.Status != string(internal.StatusIdle) {
|
||||
if status.Status == string(internal.StatusConnected) {
|
||||
cmd.Println("Already connected")
|
||||
return nil
|
||||
}
|
||||
|
||||
loginRequest := proto.LoginRequest{
|
||||
SetupKey: setupKey,
|
||||
PreSharedKey: preSharedKey,
|
||||
ManagementUrl: managementURL,
|
||||
}
|
||||
|
||||
var loginErr error
|
||||
|
||||
var loginResp *proto.LoginResponse
|
||||
|
||||
err = WithBackOff(func() error {
|
||||
var backOffErr error
|
||||
loginResp, backOffErr = client.Login(ctx, &loginRequest)
|
||||
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
|
||||
s.Code() == codes.PermissionDenied ||
|
||||
s.Code() == codes.NotFound ||
|
||||
s.Code() == codes.Unimplemented) {
|
||||
loginErr = backOffErr
|
||||
return nil
|
||||
}
|
||||
return backOffErr
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("login backoff cycle failed: %v", err)
|
||||
}
|
||||
|
||||
if loginErr != nil {
|
||||
return fmt.Errorf("login failed: %v", loginErr)
|
||||
}
|
||||
|
||||
if loginResp.NeedsSSOLogin {
|
||||
|
||||
openURL(cmd, loginResp.VerificationURIComplete)
|
||||
|
||||
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
|
||||
if err != nil {
|
||||
return fmt.Errorf("waiting sso login failed with: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := client.Up(ctx, &proto.UpRequest{}); err != nil {
|
||||
return fmt.Errorf("call service up method: %v", err)
|
||||
}
|
||||
|
||||
@@ -60,8 +60,11 @@ func RunClient(ctx context.Context, config *Config) error {
|
||||
mgmTlsEnabled = true
|
||||
}
|
||||
|
||||
engineCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// connect (just a connection, no stream yet) and login to Management Service to get an initial global Wiretrustee config
|
||||
mgmClient, loginResp, err := connectToManagement(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
||||
mgmClient, loginResp, err := connectToManagement(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
||||
@@ -73,7 +76,7 @@ func RunClient(ctx context.Context, config *Config) error {
|
||||
}
|
||||
|
||||
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
|
||||
signalClient, err := connectToSignal(ctx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
|
||||
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return wrapErr(err)
|
||||
@@ -87,9 +90,6 @@ func RunClient(ctx context.Context, config *Config) error {
|
||||
return wrapErr(err)
|
||||
}
|
||||
|
||||
engineCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig)
|
||||
err = engine.Start()
|
||||
if err != nil {
|
||||
|
||||
@@ -16,6 +16,7 @@ type OAuthClient interface {
|
||||
RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
|
||||
RotateAccessToken(ctx context.Context, refreshToken string) (TokenInfo, error)
|
||||
WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error)
|
||||
GetClientID(ctx context.Context) string
|
||||
}
|
||||
|
||||
// HTTPClient http client interface for API calls
|
||||
@@ -104,6 +105,11 @@ func NewHostedDeviceFlow(audience string, clientID string, domain string) *Hoste
|
||||
}
|
||||
}
|
||||
|
||||
// GetClientID returns the provider client id
|
||||
func (h *Hosted) GetClientID(ctx context.Context) string {
|
||||
return h.ClientID
|
||||
}
|
||||
|
||||
// RequestDeviceCode requests a device code login flow information from Hosted
|
||||
func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error) {
|
||||
url := "https://" + h.Domain + "/oauth/device/code"
|
||||
@@ -150,7 +156,8 @@ func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
|
||||
// WaitToken waits user's login and authorize the app. Once the user's authorize
|
||||
// it retrieves the access token from Hosted's endpoint and validates it before returning
|
||||
func (h *Hosted) WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error) {
|
||||
ticker := time.NewTicker(time.Duration(info.Interval) * time.Second)
|
||||
interval := time.Duration(info.Interval) * time.Second
|
||||
ticker := time.NewTicker(interval)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -181,7 +188,12 @@ func (h *Hosted) WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo,
|
||||
if tokenResponse.Error != "" {
|
||||
if tokenResponse.Error == "authorization_pending" {
|
||||
continue
|
||||
} else if tokenResponse.Error == "slow_down" {
|
||||
interval = interval + (3 * time.Second)
|
||||
ticker.Reset(interval)
|
||||
continue
|
||||
}
|
||||
|
||||
return TokenInfo{}, fmt.Errorf(tokenResponse.ErrorDescription)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,14 +26,20 @@ type Server struct {
|
||||
configPath string
|
||||
logFile string
|
||||
|
||||
oauthClient internal.OAuthClient
|
||||
deviceAuthInfo internal.DeviceAuthInfo
|
||||
oauthAuthFlow oauthAuthFlow
|
||||
|
||||
mutex sync.Mutex
|
||||
config *internal.Config
|
||||
proto.UnimplementedDaemonServiceServer
|
||||
}
|
||||
|
||||
type oauthAuthFlow struct {
|
||||
expiresAt time.Time
|
||||
client internal.OAuthClient
|
||||
info internal.DeviceAuthInfo
|
||||
waitCancel context.CancelFunc
|
||||
}
|
||||
|
||||
// New server instance constructor.
|
||||
func New(ctx context.Context, managementURL, adminURL, configPath, logFile string) *Server {
|
||||
return &Server{
|
||||
@@ -187,6 +193,21 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
|
||||
providerConfig.ProviderConfig.Domain,
|
||||
)
|
||||
|
||||
if s.oauthAuthFlow.client != nil && s.oauthAuthFlow.client.GetClientID(ctx) == hostedClient.GetClientID(context.TODO()) {
|
||||
if s.oauthAuthFlow.expiresAt.After(time.Now().Add(90 * time.Second)) {
|
||||
log.Debugf("using previous device flow info")
|
||||
return &proto.LoginResponse{
|
||||
NeedsSSOLogin: true,
|
||||
VerificationURI: s.oauthAuthFlow.info.VerificationURI,
|
||||
VerificationURIComplete: s.oauthAuthFlow.info.VerificationURIComplete,
|
||||
UserCode: s.oauthAuthFlow.info.UserCode,
|
||||
}, nil
|
||||
} else {
|
||||
log.Warnf("canceling previous waiting execution")
|
||||
s.oauthAuthFlow.waitCancel()
|
||||
}
|
||||
}
|
||||
|
||||
deviceAuthInfo, err := hostedClient.RequestDeviceCode(context.TODO())
|
||||
if err != nil {
|
||||
log.Errorf("getting a request device code failed: %v", err)
|
||||
@@ -194,8 +215,9 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
s.oauthClient = hostedClient
|
||||
s.deviceAuthInfo = deviceAuthInfo
|
||||
s.oauthAuthFlow.client = hostedClient
|
||||
s.oauthAuthFlow.info = deviceAuthInfo
|
||||
s.oauthAuthFlow.expiresAt = time.Now().Add(time.Duration(deviceAuthInfo.ExpiresIn) * time.Second)
|
||||
s.mutex.Unlock()
|
||||
|
||||
state.Set(internal.StatusNeedsLogin)
|
||||
@@ -233,7 +255,7 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
|
||||
s.actCancel = cancel
|
||||
s.mutex.Unlock()
|
||||
|
||||
if s.oauthClient == nil {
|
||||
if s.oauthAuthFlow.client == nil {
|
||||
return nil, gstatus.Errorf(codes.Internal, "oauth client is not initialized")
|
||||
}
|
||||
|
||||
@@ -248,7 +270,7 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
|
||||
state.Set(internal.StatusConnecting)
|
||||
|
||||
s.mutex.Lock()
|
||||
deviceAuthInfo := s.deviceAuthInfo
|
||||
deviceAuthInfo := s.oauthAuthFlow.info
|
||||
s.mutex.Unlock()
|
||||
|
||||
if deviceAuthInfo.UserCode != msg.UserCode {
|
||||
@@ -256,12 +278,26 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
|
||||
return nil, gstatus.Errorf(codes.InvalidArgument, "sso user code is invalid")
|
||||
}
|
||||
|
||||
waitTimeout := time.Duration(deviceAuthInfo.ExpiresIn)
|
||||
waitCTX, cancel := context.WithTimeout(ctx, waitTimeout*time.Second)
|
||||
if s.oauthAuthFlow.waitCancel != nil {
|
||||
s.oauthAuthFlow.waitCancel()
|
||||
}
|
||||
|
||||
waitTimeout := time.Until(s.oauthAuthFlow.expiresAt)
|
||||
waitCTX, cancel := context.WithTimeout(ctx, waitTimeout)
|
||||
defer cancel()
|
||||
|
||||
tokenInfo, err := s.oauthClient.WaitToken(waitCTX, deviceAuthInfo)
|
||||
s.mutex.Lock()
|
||||
s.oauthAuthFlow.waitCancel = cancel
|
||||
s.mutex.Unlock()
|
||||
|
||||
tokenInfo, err := s.oauthAuthFlow.client.WaitToken(waitCTX, deviceAuthInfo)
|
||||
if err != nil {
|
||||
if err == context.Canceled {
|
||||
return nil, nil
|
||||
}
|
||||
s.mutex.Lock()
|
||||
s.oauthAuthFlow.expiresAt = time.Now()
|
||||
s.mutex.Unlock()
|
||||
state.Set(internal.StatusLoginFailed)
|
||||
log.Errorf("waiting for browser login failed: %v", err)
|
||||
return nil, err
|
||||
|
||||
2
client/testdata/store.json
vendored
2
client/testdata/store.json
vendored
@@ -18,7 +18,7 @@
|
||||
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
|
||||
"Net": {
|
||||
"IP": "100.64.0.0",
|
||||
"Mask": "/8AAAA=="
|
||||
"Mask": "//8AAA=="
|
||||
},
|
||||
"Dns": null
|
||||
},
|
||||
|
||||
@@ -90,7 +90,7 @@ type serviceClient struct {
|
||||
icConnected []byte
|
||||
icDisconnected []byte
|
||||
|
||||
// systray menu itmes
|
||||
// systray menu items
|
||||
mStatus *systray.MenuItem
|
||||
mUp *systray.MenuItem
|
||||
mDown *systray.MenuItem
|
||||
@@ -259,36 +259,27 @@ func (s *serviceClient) menuUpClick() error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.login()
|
||||
if err != nil {
|
||||
log.Errorf("login failed with: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
status, err := conn.Status(s.ctx, &proto.StatusRequest{})
|
||||
if err != nil {
|
||||
log.Errorf("get service status: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if status.Status == string(internal.StatusNeedsLogin) || status.Status == string(internal.StatusLoginFailed) {
|
||||
err = s.login()
|
||||
if err != nil {
|
||||
log.Errorf("get service status: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
status, err = conn.Status(s.ctx, &proto.StatusRequest{})
|
||||
if err != nil {
|
||||
log.Errorf("get service status: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if status.Status != string(internal.StatusIdle) {
|
||||
if status.Status == string(internal.StatusConnected) {
|
||||
log.Warnf("already connected")
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := s.conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
||||
log.Errorf("up service: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -373,6 +364,9 @@ func (s *serviceClient) onTrayReady() {
|
||||
systray.AddSeparator()
|
||||
s.mSettings = systray.AddMenuItem("Settings", "Settings of the application")
|
||||
systray.AddSeparator()
|
||||
v := systray.AddMenuItem("v"+system.NetbirdVersion(), "Client Version: "+system.NetbirdVersion())
|
||||
v.Disable()
|
||||
systray.AddSeparator()
|
||||
s.mQuit = systray.AddMenuItem("Quit", "Quit the client app")
|
||||
|
||||
go func() {
|
||||
@@ -393,15 +387,19 @@ func (s *serviceClient) onTrayReady() {
|
||||
case <-s.mAdminPanel.ClickedCh:
|
||||
err = open.Run(s.adminURL)
|
||||
case <-s.mUp.ClickedCh:
|
||||
s.mUp.Disable()
|
||||
if err = s.menuUpClick(); err != nil {
|
||||
s.mUp.Enable()
|
||||
}
|
||||
go func() {
|
||||
err := s.menuUpClick()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
case <-s.mDown.ClickedCh:
|
||||
s.mDown.Disable()
|
||||
if err = s.menuDownClick(); err != nil {
|
||||
s.mDown.Enable()
|
||||
}
|
||||
go func() {
|
||||
err := s.menuDownClick()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
case <-s.mSettings.ClickedCh:
|
||||
s.mSettings.Disable()
|
||||
go func() {
|
||||
|
||||
1
go.mod
1
go.mod
@@ -29,6 +29,7 @@ require (
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.1.4
|
||||
github.com/c-robinson/iplib v1.0.3
|
||||
github.com/getlantern/systray v1.2.1
|
||||
github.com/magiconair/properties v1.8.5
|
||||
github.com/rs/xid v1.3.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -70,6 +70,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/c-robinson/iplib v1.0.3 h1:NG0UF0GoEsrC1/vyfX1Lx2Ss7CySWl3KqqXh3q4DdPU=
|
||||
github.com/c-robinson/iplib v1.0.3/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=
|
||||
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
|
||||
@@ -38,9 +38,10 @@ func WireguardModExists() bool {
|
||||
func (w *WGIface) Create() error {
|
||||
|
||||
if WireguardModExists() {
|
||||
log.Debug("using kernel Wireguard module")
|
||||
log.Info("using kernel WireGuard")
|
||||
return w.CreateWithKernel()
|
||||
} else {
|
||||
log.Info("using userspace WireGuard")
|
||||
return w.CreateWithUserspace()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ fi
|
||||
if [[ $NETBIRD_DOMAIN == "localhost" || $NETBIRD_DOMAIN == "127.0.0.1" ]]
|
||||
then
|
||||
export NETBIRD_MGMT_API_ENDPOINT=http://$NETBIRD_DOMAIN:$NETBIRD_MGMT_API_PORT
|
||||
export NETBIRD_MGMT_GRPC_API_ENDPOINT=http://$NETBIRD_DOMAIN:$NETBIRD_MGMT_GRPC_API_PORT
|
||||
unset NETBIRD_MGMT_API_CERT_FILE
|
||||
unset NETBIRD_MGMT_API_CERT_KEY_FILE
|
||||
fi
|
||||
|
||||
@@ -12,6 +12,7 @@ services:
|
||||
- AUTH0_CLIENT_ID=$NETBIRD_AUTH0_CLIENT_ID
|
||||
- AUTH0_AUDIENCE=$NETBIRD_AUTH0_AUDIENCE
|
||||
- NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
||||
- NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_MGMT_GRPC_API_ENDPOINT
|
||||
- NGINX_SSL_PORT=443
|
||||
- LETSENCRYPT_DOMAIN=$NETBIRD_DOMAIN
|
||||
- LETSENCRYPT_EMAIL=$NETBIRD_LETSENCRYPT_EMAIL
|
||||
@@ -39,7 +40,7 @@ services:
|
||||
- $LETSENCRYPT_VOLUMENAME:/etc/letsencrypt:ro
|
||||
- ./management.json:/etc/netbird/management.json
|
||||
ports:
|
||||
- 33073:33073 #gRPC port
|
||||
- $NETBIRD_MGMT_GRPC_API_PORT:33073 #gRPC port
|
||||
- $NETBIRD_MGMT_API_PORT:33071 #API port
|
||||
# # port and command for Let's Encrypt validation
|
||||
# - 443:443
|
||||
|
||||
@@ -19,8 +19,12 @@ NETBIRD_LETSENCRYPT_EMAIL=""
|
||||
|
||||
# Management API port
|
||||
NETBIRD_MGMT_API_PORT=33071
|
||||
# Management GRPC API port
|
||||
NETBIRD_MGMT_GRPC_API_PORT=33073
|
||||
# Management API endpoint address, used by the Dashboard
|
||||
NETBIRD_MGMT_API_ENDPOINT=https://$NETBIRD_DOMAIN:$NETBIRD_MGMT_API_PORT
|
||||
# Management GRPC API endpoint address, used by the hosts to register
|
||||
NETBIRD_MGMT_GRPC_API_ENDPOINT=https://$NETBIRD_DOMAIN:NETBIRD_MGMT_GRPC_API_PORT
|
||||
# Management Certficate file path. These are generated by the Dashboard container
|
||||
NETBIRD_MGMT_API_CERT_FILE="/etc/letsencrypt/live/$NETBIRD_DOMAIN/fullchain.pem"
|
||||
# Management Certficate key file path.
|
||||
@@ -50,6 +54,8 @@ export NETBIRD_AUTH0_AUDIENCE
|
||||
export NETBIRD_LETSENCRYPT_EMAIL
|
||||
export NETBIRD_MGMT_API_PORT
|
||||
export NETBIRD_MGMT_API_ENDPOINT
|
||||
export NETBIRD_MGMT_GRPC_API_PORT
|
||||
export NETBIRD_MGMT_GRPC_API_ENDPOINT
|
||||
export NETBIRD_MGMT_API_CERT_FILE
|
||||
export NETBIRD_MGMT_API_CERT_KEY_FILE
|
||||
export TURN_USER
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"google.golang.org/grpc/codes"
|
||||
gstatus "google.golang.org/grpc/status"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
@@ -115,6 +117,9 @@ func (c *GrpcClient) Sync(msgHandler func(msg *proto.SyncResponse) error) error
|
||||
// blocking until error
|
||||
err = c.receiveEvents(stream, *serverPubKey, msgHandler)
|
||||
if err != nil {
|
||||
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
|
||||
return backoff.Permanent(err)
|
||||
}
|
||||
backOff.Reset()
|
||||
return err
|
||||
}
|
||||
@@ -124,7 +129,7 @@ func (c *GrpcClient) Sync(msgHandler func(msg *proto.SyncResponse) error) error
|
||||
|
||||
err := backoff.Retry(operation, backOff)
|
||||
if err != nil {
|
||||
log.Warnf("exiting Management Service connection retry loop due to unrecoverable error: %s", err)
|
||||
log.Warnf("exiting Management Service connection retry loop due to Permanent error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -328,11 +328,12 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID string) ([]*UserI
|
||||
|
||||
queriedUsers := make([]*idp.UserData, 0)
|
||||
if !isNil(am.idpManager) {
|
||||
queriedUsers, err = am.idpManager.GetBatchedUserData(accountID)
|
||||
queriedUsers, err = am.idpManager.GetAllUsers(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// TODO: we need to check whether we need to refresh our cache or not
|
||||
|
||||
userInfo := make([]*UserInfo, 0)
|
||||
|
||||
@@ -352,7 +353,6 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID string) ([]*UserI
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -265,10 +265,6 @@ func TestAccountManager_AddAccount(t *testing.T) {
|
||||
userId := "account_creator"
|
||||
expectedPeersSize := 0
|
||||
expectedSetupKeysSize := 2
|
||||
expectedNetwork := net.IPNet{
|
||||
IP: net.IP{100, 64, 0, 0},
|
||||
Mask: net.IPMask{255, 192, 0, 0},
|
||||
}
|
||||
|
||||
account, err := manager.AddAccount(expectedId, userId, "")
|
||||
if err != nil {
|
||||
@@ -287,8 +283,9 @@ func TestAccountManager_AddAccount(t *testing.T) {
|
||||
t.Errorf("expected account to have len(SetupKeys) = %v, got %v", expectedSetupKeysSize, len(account.SetupKeys))
|
||||
}
|
||||
|
||||
if account.Network.Net.String() != expectedNetwork.String() {
|
||||
t.Errorf("expected account to have Network = %v, got %v", expectedNetwork.String(), account.Network.Net.String())
|
||||
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
|
||||
if !ipNet.Contains(account.Network.Net.IP) {
|
||||
t.Errorf("expected account's Network to be a subnet of %v, got %v", ipNet.String(), account.Network.Net.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +416,6 @@ func TestAccountManager_AddPeer(t *testing.T) {
|
||||
return
|
||||
}
|
||||
expectedPeerKey := key.PublicKey().String()
|
||||
expectedPeerIP := "100.64.0.1"
|
||||
expectedSetupKey := setupKey.Key
|
||||
|
||||
peer, err := manager.AddPeer(setupKey.Key, "", &Peer{
|
||||
@@ -442,8 +438,8 @@ func TestAccountManager_AddPeer(t *testing.T) {
|
||||
t.Errorf("expecting just added peer to have key = %s, got %s", expectedPeerKey, peer.Key)
|
||||
}
|
||||
|
||||
if peer.IP.String() != expectedPeerIP {
|
||||
t.Errorf("expecting just added peer to have IP = %s, got %s", expectedPeerIP, peer.IP.String())
|
||||
if !account.Network.Net.Contains(peer.IP) {
|
||||
t.Errorf("expecting just added peer's IP %s to be in a network range %s", peer.IP.String(), account.Network.Net.String())
|
||||
}
|
||||
|
||||
if peer.SetupKey != expectedSetupKey {
|
||||
@@ -482,7 +478,6 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) {
|
||||
return
|
||||
}
|
||||
expectedPeerKey := key.PublicKey().String()
|
||||
expectedPeerIP := "100.64.0.1"
|
||||
expectedUserId := userId
|
||||
|
||||
peer, err := manager.AddPeer("", userId, &Peer{
|
||||
@@ -505,8 +500,8 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) {
|
||||
t.Errorf("expecting just added peer to have key = %s, got %s", expectedPeerKey, peer.Key)
|
||||
}
|
||||
|
||||
if peer.IP.String() != expectedPeerIP {
|
||||
t.Errorf("expecting just added peer to have IP = %s, got %s", expectedPeerIP, peer.IP.String())
|
||||
if !account.Network.Net.Contains(peer.IP) {
|
||||
t.Errorf("expecting just added peer's IP %s to be in a network range %s", peer.IP.String(), account.Network.Net.String())
|
||||
}
|
||||
|
||||
if peer.UserID != expectedUserId {
|
||||
@@ -596,7 +591,7 @@ func TestGetUsersFromAccount(t *testing.T) {
|
||||
for _, userInfo := range userInfos {
|
||||
id := userInfo.ID
|
||||
assert.Equal(t, userInfo.ID, users[id].Id)
|
||||
assert.Equal(t, string(userInfo.Role), string(users[id].Role))
|
||||
assert.Equal(t, userInfo.Role, string(users[id].Role))
|
||||
assert.Equal(t, userInfo.Name, "")
|
||||
assert.Equal(t, userInfo.Email, "")
|
||||
}
|
||||
|
||||
@@ -203,8 +203,11 @@ func (s *Server) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest) (*Pe
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.FailedPrecondition {
|
||||
return nil, err
|
||||
s, ok := status.FromError(err)
|
||||
if ok {
|
||||
if s.Code() == codes.FailedPrecondition || s.Code() == codes.OutOfRange {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, status.Errorf(codes.NotFound, "provided setup key doesn't exists")
|
||||
}
|
||||
@@ -365,7 +368,7 @@ func toWiretrusteeConfig(config *Config, turnCredentials *TURNCredentials) *prot
|
||||
|
||||
func toPeerConfig(peer *Peer) *proto.PeerConfig {
|
||||
return &proto.PeerConfig{
|
||||
Address: peer.IP.String() + "/24", // todo make it explicit
|
||||
Address: peer.IP.String() + "/16", // todo make it explicit
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -18,10 +21,11 @@ import (
|
||||
|
||||
// Auth0Manager auth0 manager client instance
|
||||
type Auth0Manager struct {
|
||||
authIssuer string
|
||||
httpClient ManagerHTTPClient
|
||||
credentials ManagerCredentials
|
||||
helper ManagerHelper
|
||||
authIssuer string
|
||||
httpClient ManagerHTTPClient
|
||||
credentials ManagerCredentials
|
||||
helper ManagerHelper
|
||||
cachedUsersByAccountId map[string][]Auth0Profile
|
||||
}
|
||||
|
||||
// Auth0ClientConfig auth0 manager client configurations
|
||||
@@ -51,6 +55,38 @@ type Auth0Credentials struct {
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
type Auth0Profile struct {
|
||||
AccountId string `json:"wt_account_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastLogin string `json:"last_login"`
|
||||
}
|
||||
|
||||
type UserExportJobResponse struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
ConnectionId string `json:"connection_id"`
|
||||
Format string `json:"format"`
|
||||
Limit int `json:"limit"`
|
||||
Connection string `json:"connection"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
type ExportJobStatusResponse struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
ConnectionId string `json:"connection_id"`
|
||||
Format string `json:"format"`
|
||||
Limit int `json:"limit"`
|
||||
Location string `json:"location"`
|
||||
Connection string `json:"connection"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
// NewAuth0Manager creates a new instance of the Auth0Manager
|
||||
func NewAuth0Manager(config Auth0ClientConfig) (*Auth0Manager, error) {
|
||||
|
||||
@@ -81,11 +117,13 @@ func NewAuth0Manager(config Auth0ClientConfig) (*Auth0Manager, error) {
|
||||
httpClient: httpClient,
|
||||
helper: helper,
|
||||
}
|
||||
|
||||
return &Auth0Manager{
|
||||
authIssuer: config.AuthIssuer,
|
||||
credentials: credentials,
|
||||
httpClient: httpClient,
|
||||
helper: helper,
|
||||
authIssuer: config.AuthIssuer,
|
||||
credentials: credentials,
|
||||
httpClient: httpClient,
|
||||
helper: helper,
|
||||
cachedUsersByAccountId: make(map[string][]Auth0Profile),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -186,44 +224,198 @@ func (c *Auth0Credentials) Authenticate() (JWTToken, error) {
|
||||
return c.jwtToken, nil
|
||||
}
|
||||
|
||||
func batchRequestUsersUrl(authIssuer, accountId string, page int) (string, url.Values, error) {
|
||||
u, err := url.Parse(authIssuer + "/api/v2/users")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("page", strconv.Itoa(page))
|
||||
q.Set("search_engine", "v3")
|
||||
q.Set("q", "app_metadata.wt_account_id:"+accountId)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String(), q, nil
|
||||
}
|
||||
|
||||
func requestByUserIdUrl(authIssuer, userId string) string {
|
||||
return authIssuer + "/api/v2/users/" + userId
|
||||
}
|
||||
|
||||
// GetBatchedUserData requests users in batches from Auth0
|
||||
func (am *Auth0Manager) GetBatchedUserData(accountId string) ([]*UserData, error) {
|
||||
jwtToken, err := am.credentials.Authenticate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Gets all users from cache, if the cache exists
|
||||
// Otherwise we will initialize the cache with creating the export job on auth0
|
||||
func (am *Auth0Manager) GetAllUsers(accountId string) ([]*UserData, error) {
|
||||
if len(am.cachedUsersByAccountId[accountId]) == 0 {
|
||||
err := am.createExportUsersJob(accountId)
|
||||
if err != nil {
|
||||
log.Debugf("Couldn't cache users; %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var list []*UserData
|
||||
|
||||
cachedUsers := am.cachedUsersByAccountId[accountId]
|
||||
for _, val := range cachedUsers {
|
||||
list = append(list, &UserData{
|
||||
Name: val.Name,
|
||||
Email: val.Email,
|
||||
ID: val.UserID,
|
||||
})
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// This creates an export job on auth0 for all users.
|
||||
func (am *Auth0Manager) createExportUsersJob(accountId string) error {
|
||||
jwtToken, err := am.credentials.Authenticate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reqURL := am.authIssuer + "/api/v2/jobs/users-exports"
|
||||
|
||||
payloadString := fmt.Sprintf("{\"format\": \"json\"," +
|
||||
"\"fields\": [{\"name\": \"created_at\"}, {\"name\": \"last_login\"},{\"name\": \"user_id\"}, {\"name\": \"email\"}, {\"name\": \"name\"}, {\"name\": \"app_metadata.wt_account_id\", \"export_as\": \"wt_account_id\"}]}")
|
||||
|
||||
payload := strings.NewReader(payloadString)
|
||||
|
||||
exportJobReq, err := http.NewRequest("POST", reqURL, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exportJobReq.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||
exportJobReq.Header.Add("content-type", "application/json")
|
||||
|
||||
jobResp, err := am.httpClient.Do(exportJobReq)
|
||||
if err != nil {
|
||||
log.Debugf("Couldn't get job response %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = jobResp.Body.Close()
|
||||
if err != nil {
|
||||
log.Errorf("error while closing update user app metadata response body: %v", err)
|
||||
}
|
||||
}()
|
||||
if jobResp.StatusCode != 200 {
|
||||
return fmt.Errorf("unable to update the appMetadata, statusCode %d", jobResp.StatusCode)
|
||||
}
|
||||
|
||||
var exportJobResp UserExportJobResponse
|
||||
|
||||
body, err := ioutil.ReadAll(jobResp.Body)
|
||||
if err != nil {
|
||||
log.Debugf("Coudln't read export job response; %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = am.helper.Unmarshal(body, &exportJobResp)
|
||||
if err != nil {
|
||||
log.Debugf("Coudln't unmarshal export job response; %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if exportJobResp.Id == "" {
|
||||
return fmt.Errorf("couldn't get an batch id status %d, %s, response body: %v", jobResp.StatusCode, jobResp.Status, exportJobResp)
|
||||
}
|
||||
|
||||
log.Debugf("batch id status %d, %s, response body: %v", jobResp.StatusCode, jobResp.Status, exportJobResp)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
done, downloadLink, err := am.checkExportJobStatus(ctx, exportJobResp.Id)
|
||||
if err != nil {
|
||||
log.Debugf("Failed at getting status checks from exportJob; %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if done {
|
||||
err = am.cacheUsers(downloadLink)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to cache users via download link; %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Downloads the users from auth0 and caches it in memory
|
||||
// Users are only cached if they have an wt_account_id stored in auth0
|
||||
func (am *Auth0Manager) cacheUsers(location string) error {
|
||||
body, err := doGetReq(am.httpClient, location, "")
|
||||
if err != nil {
|
||||
log.Debugf("Can't download cached users; %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
bodyReader := bytes.NewReader(body)
|
||||
|
||||
gzipReader, err := gzip.NewReader(bodyReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(gzipReader)
|
||||
|
||||
for decoder.More() {
|
||||
profile := Auth0Profile{}
|
||||
err = decoder.Decode(&profile)
|
||||
if err != nil {
|
||||
log.Errorf("Couldn't decode profile; %v", err)
|
||||
return err
|
||||
}
|
||||
if profile.AccountId != "" {
|
||||
am.cachedUsersByAccountId[profile.AccountId] = append(am.cachedUsersByAccountId[profile.AccountId], profile)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This checks the status of the job created at CreateExportUsersJob.
|
||||
// If the status is "completed", then return the downloadLink
|
||||
func (am *Auth0Manager) checkExportJobStatus(ctx context.Context, jobId string) (bool, string, error) {
|
||||
retry := time.NewTicker(time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debugf("Export job status stopped...\n")
|
||||
return false, "", ctx.Err()
|
||||
case <-retry.C:
|
||||
jwtToken, err := am.credentials.Authenticate()
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
statusUrl := am.authIssuer + "/api/v2/jobs/" + jobId
|
||||
body, err := doGetReq(am.httpClient, statusUrl, jwtToken.AccessToken)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
var status ExportJobStatusResponse
|
||||
err = am.helper.Unmarshal(body, &status)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
log.Debugf("Current export job status is %v", status.Status)
|
||||
|
||||
if status.Status != "completed" {
|
||||
continue
|
||||
}
|
||||
|
||||
return true, status.Location, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidates old cache for Account and re-queries it from auth0
|
||||
func (am *Auth0Manager) forceUpdateUserCache(accountId string) error {
|
||||
jwtToken, err := am.credentials.Authenticate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var list []Auth0Profile
|
||||
|
||||
// https://auth0.com/docs/manage-users/user-search/retrieve-users-with-get-users-endpoint#limitations
|
||||
// auth0 limitation of 1000 users via this endpoint
|
||||
for page := 0; page < 20; page++ {
|
||||
reqURL, query, err := batchRequestUsersUrl(am.authIssuer, accountId, page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, reqURL, strings.NewReader(query.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||
@@ -231,41 +423,42 @@ func (am *Auth0Manager) GetBatchedUserData(accountId string) ([]*UserData, error
|
||||
|
||||
res, err := am.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
var batch []UserData
|
||||
var batch []Auth0Profile
|
||||
err = json.Unmarshal(body, &batch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("requested batch; %v", batch)
|
||||
|
||||
err = res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("unable to request UserData from auth0, statusCode %d", res.StatusCode)
|
||||
return fmt.Errorf("unable to request UserData from auth0, statusCode %d", res.StatusCode)
|
||||
}
|
||||
|
||||
if len(batch) == 0 {
|
||||
return list, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
for user := range batch {
|
||||
list = append(list, &batch[user])
|
||||
list = append(list, batch[user])
|
||||
}
|
||||
}
|
||||
am.cachedUsersByAccountId[accountId] = list
|
||||
|
||||
return list, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserDataByID requests user data from auth0 via ID
|
||||
@@ -359,3 +552,54 @@ func (am *Auth0Manager) UpdateUserAppMetadata(userId string, appMetadata AppMeta
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func batchRequestUsersUrl(authIssuer, accountId string, page int) (string, url.Values, error) {
|
||||
u, err := url.Parse(authIssuer + "/api/v2/users")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("page", strconv.Itoa(page))
|
||||
q.Set("search_engine", "v3")
|
||||
q.Set("q", "app_metadata.wt_account_id:"+accountId)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String(), q, nil
|
||||
}
|
||||
|
||||
func requestByUserIdUrl(authIssuer, userId string) string {
|
||||
return authIssuer + "/api/v2/users/" + userId
|
||||
}
|
||||
|
||||
// Boilerplate implementation for Get Requests.
|
||||
func doGetReq(client ManagerHTTPClient, url, accessToken string) ([]byte, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if accessToken != "" {
|
||||
req.Header.Add("authorization", "Bearer "+accessToken)
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = res.Body.Close()
|
||||
if err != nil {
|
||||
log.Errorf("error while closing body for url %s: %v", url, err)
|
||||
}
|
||||
}()
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("unable to get %s, statusCode %d", url, res.StatusCode)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
type Manager interface {
|
||||
UpdateUserAppMetadata(userId string, appMetadata AppMetadata) error
|
||||
GetUserDataByID(userId string, appMetadata AppMetadata) (*UserData, error)
|
||||
GetBatchedUserData(accountId string) ([]*UserData, error)
|
||||
GetAllUsers(accountId string) ([]*UserData, error)
|
||||
}
|
||||
|
||||
// Config an idp configuration struct to be loaded from management server's config file
|
||||
|
||||
@@ -251,8 +251,10 @@ func Test_SyncProtocol(t *testing.T) {
|
||||
t.Fatal("expecting SyncResponse to have NetworkMap with a non-nil PeerConfig")
|
||||
}
|
||||
|
||||
if networkMap.GetPeerConfig().GetAddress() != "100.64.0.1/24" {
|
||||
t.Fatal("expecting SyncResponse to have NetworkMap with a PeerConfig having valid Address")
|
||||
expectedIPNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
|
||||
ip, _, _ := net.ParseCIDR(networkMap.GetPeerConfig().GetAddress())
|
||||
if !expectedIPNet.Contains(ip) {
|
||||
t.Fatalf("expecting SyncResponse to have NetworkMap with a PeerConfig having valid IP address %s", networkMap.GetPeerConfig().GetAddress())
|
||||
}
|
||||
|
||||
if networkMap.GetSerial() <= 0 {
|
||||
|
||||
@@ -107,8 +107,6 @@ var _ = Describe("Management service", func() {
|
||||
err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(resp.PeerConfig.Address).To(BeEquivalentTo("100.64.0.1/24"))
|
||||
|
||||
expectedSignalConfig := &mgmtProto.HostConfig{
|
||||
Uri: "signal.wiretrustee.com:10000",
|
||||
Protocol: mgmtProto.HostConfig_HTTP,
|
||||
@@ -308,10 +306,10 @@ var _ = Describe("Management service", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Context("when there are 50 peers registered under one account", func() {
|
||||
Context("when there are 10 peers registered under one account", func() {
|
||||
Context("when there are 10 more peers registered under the same account", func() {
|
||||
Specify("all of the 50 peers will get updates of 10 newly registered peers", func() {
|
||||
initialPeers := 20
|
||||
Specify("all of the 10 peers will get updates of 10 newly registered peers", func() {
|
||||
initialPeers := 10
|
||||
additionalPeers := 10
|
||||
|
||||
var peers []wgtypes.Key
|
||||
@@ -367,7 +365,7 @@ var _ = Describe("Management service", func() {
|
||||
key, _ := wgtypes.GenerateKey()
|
||||
loginPeerWithValidSetupKey(serverPubKey, key, client)
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
n := rand.Intn(500)
|
||||
n := rand.Intn(200)
|
||||
time.Sleep(time.Duration(n) * time.Millisecond)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/c-robinson/iplib"
|
||||
"github.com/rs/xid"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
upperIPv4 = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 255, 255, 255, 255}
|
||||
upperIPv6 = []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||
"time"
|
||||
)
|
||||
|
||||
type NetworkMap struct {
|
||||
@@ -30,10 +28,19 @@ type Network struct {
|
||||
}
|
||||
|
||||
// NewNetwork creates a new Network initializing it with a Serial=0
|
||||
// It takes a random /16 subnet from 100.64.0.0/10 (64 different subnets)
|
||||
func NewNetwork() *Network {
|
||||
|
||||
n := iplib.NewNet4(net.ParseIP("100.64.0.0"), 10)
|
||||
sub, _ := n.Subnet(16)
|
||||
|
||||
s := rand.NewSource(time.Now().Unix())
|
||||
r := rand.New(s)
|
||||
intn := r.Intn(len(sub))
|
||||
|
||||
return &Network{
|
||||
Id: xid.New().String(),
|
||||
Net: net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}},
|
||||
Net: sub[intn].IPNet,
|
||||
Dns: "",
|
||||
Serial: 0}
|
||||
}
|
||||
@@ -63,51 +70,60 @@ func (n *Network) Copy() *Network {
|
||||
|
||||
// AllocatePeerIP pics an available IP from an net.IPNet.
|
||||
// This method considers already taken IPs and reuses IPs if there are gaps in takenIps
|
||||
// E.g. if ipNet=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.5] then the result would be 100.30.0.2
|
||||
// E.g. if ipNet=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.4] then the result would be 100.30.0.2 or 100.30.0.3
|
||||
func AllocatePeerIP(ipNet net.IPNet, takenIps []net.IP) (net.IP, error) {
|
||||
takenIpMap := make(map[string]net.IP)
|
||||
takenIpMap[ipNet.IP.String()] = ipNet.IP
|
||||
takenIPMap := make(map[string]struct{})
|
||||
takenIPMap[ipNet.IP.String()] = struct{}{}
|
||||
for _, ip := range takenIps {
|
||||
takenIpMap[ip.String()] = ip
|
||||
}
|
||||
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); ip = GetNextIP(ip) {
|
||||
if _, ok := takenIpMap[ip.String()]; !ok {
|
||||
return ip, nil
|
||||
}
|
||||
takenIPMap[ip.String()] = struct{}{}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed allocating new IP for the ipNet %s and takenIps %s", ipNet.String(), takenIps)
|
||||
ips, _ := generateIPs(&ipNet, takenIPMap)
|
||||
|
||||
if len(ips) == 0 {
|
||||
return nil, status.Errorf(codes.OutOfRange, "failed allocating new IP for the ipNet %s - network is out of IPs", ipNet.String())
|
||||
}
|
||||
|
||||
// pick a random IP
|
||||
s := rand.NewSource(time.Now().Unix())
|
||||
r := rand.New(s)
|
||||
intn := r.Intn(len(ips))
|
||||
|
||||
return ips[intn], nil
|
||||
}
|
||||
|
||||
// GetNextIP returns the next IP from the given IP address. If the given IP is
|
||||
// the last IP of a v4 or v6 range, the same IP is returned.
|
||||
// Credits to Cilium team.
|
||||
// Copyright 2017-2020 Authors of Cilium
|
||||
func GetNextIP(ip net.IP) net.IP {
|
||||
if ip.Equal(upperIPv4) || ip.Equal(upperIPv6) {
|
||||
return ip
|
||||
// generateIPs generates a list of all possible IPs of the given network excluding IPs specified in the exclusion list
|
||||
func generateIPs(ipNet *net.IPNet, exclusions map[string]struct{}) ([]net.IP, int) {
|
||||
|
||||
var ips []net.IP
|
||||
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); incIP(ip) {
|
||||
if _, ok := exclusions[ip.String()]; !ok && ip[3] != 0 {
|
||||
ips = append(ips, copyIP(ip))
|
||||
}
|
||||
}
|
||||
|
||||
nextIP := make(net.IP, len(ip))
|
||||
switch len(ip) {
|
||||
case net.IPv4len:
|
||||
ipU32 := binary.BigEndian.Uint32(ip)
|
||||
ipU32++
|
||||
binary.BigEndian.PutUint32(nextIP, ipU32)
|
||||
return nextIP
|
||||
case net.IPv6len:
|
||||
ipU64 := binary.BigEndian.Uint64(ip[net.IPv6len/2:])
|
||||
ipU64++
|
||||
binary.BigEndian.PutUint64(nextIP[net.IPv6len/2:], ipU64)
|
||||
if ipU64 == 0 {
|
||||
ipU64 = binary.BigEndian.Uint64(ip[:net.IPv6len/2])
|
||||
ipU64++
|
||||
binary.BigEndian.PutUint64(nextIP[:net.IPv6len/2], ipU64)
|
||||
} else {
|
||||
copy(nextIP[:net.IPv6len/2], ip[:net.IPv6len/2])
|
||||
}
|
||||
return nextIP
|
||||
// remove network address and broadcast address
|
||||
lenIPs := len(ips)
|
||||
switch {
|
||||
case lenIPs < 2:
|
||||
return ips, lenIPs
|
||||
|
||||
default:
|
||||
return ip
|
||||
return ips[1 : len(ips)-1], lenIPs - 2
|
||||
}
|
||||
}
|
||||
|
||||
func copyIP(ip net.IP) net.IP {
|
||||
dup := make(net.IP, len(ip))
|
||||
copy(dup, ip)
|
||||
return dup
|
||||
}
|
||||
|
||||
func incIP(ip net.IP) {
|
||||
for j := len(ip) - 1; j >= 0; j-- {
|
||||
ip[j]++
|
||||
if ip[j] > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
39
management/server/network_test.go
Normal file
39
management/server/network_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewNetwork(t *testing.T) {
|
||||
network := NewNetwork()
|
||||
|
||||
// generated net should be a subnet of a larger 100.64.0.0/10 net
|
||||
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
|
||||
assert.Equal(t, ipNet.Contains(network.Net.IP), true)
|
||||
}
|
||||
|
||||
func TestAllocatePeerIP(t *testing.T) {
|
||||
|
||||
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 255, 255, 0}}
|
||||
var ips []net.IP
|
||||
for i := 0; i < 253; i++ {
|
||||
ip, err := AllocatePeerIP(ipNet, ips)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
|
||||
assert.Len(t, ips, 253)
|
||||
|
||||
uniq := make(map[string]struct{})
|
||||
for _, ip := range ips {
|
||||
if _, ok := uniq[ip.String()]; !ok {
|
||||
uniq[ip.String()] = struct{}{}
|
||||
} else {
|
||||
t.Errorf("found duplicate IP %s", ip.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,7 +346,10 @@ func (am *DefaultAccountManager) AddPeer(
|
||||
}
|
||||
|
||||
network := account.Network
|
||||
nextIp, _ := AllocatePeerIP(network.Net, takenIps)
|
||||
nextIp, err := AllocatePeerIP(network.Net, takenIps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newPeer := &Peer{
|
||||
Key: peer.Key,
|
||||
|
||||
2
management/server/testdata/store.json
vendored
2
management/server/testdata/store.json
vendored
@@ -21,7 +21,7 @@
|
||||
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
|
||||
"Net": {
|
||||
"IP": "100.64.0.0",
|
||||
"Mask": "/8AAAA=="
|
||||
"Mask": "//8AAA=="
|
||||
},
|
||||
"Dns": null
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user