Compare commits

...

733 Commits

Author SHA1 Message Date
Maycon Santos
3b19e8c57e Use Trace level for account lock messages 2023-04-04 20:04:18 +02:00
Maycon Santos
fe1ea4a2d0 Check multiple audience values (#781)
Some IDP use different audience for different clients. 
This update checks HTTP and Device authorization flow audience values.



---------

Co-authored-by: Givi Khojanashvili <gigovich@gmail.com>
2023-04-04 16:40:56 +02:00
Maycon Santos
f14f34cf2b Add token source and device flow audience variables (#780)
Supporting new dashboard option to configure a source token.

Adding configuration support for setting 
a different audience for device authorization flow.

fix custom id claim variable
2023-04-04 15:56:02 +02:00
Bethuel
109481e26d Use first available package manager (#782) 2023-04-04 14:26:17 +02:00
Bethuel
18098e7a7d Add single line installer (#775)
detect OS package manager
If a supported package manager is not available,
use binary installation

Check if desktop environment is available
Skip installing the UI client if SKIP_UI_APP is set to true

added tests for Ubuntu and macOS tests
2023-04-04 00:35:54 +02:00
Ruakij
5993982cca Add disable letsencrypt (#747)
Add NETBIRD_DISABLE_LETSENCRYPT support to explicit disable let's encrypt

Organize the setup.env.example variables into sections

Add traefik example
2023-04-04 00:21:40 +02:00
Zoltan Papp
86f9051a30 Fix/connection listener (#777)
Fix add/remove connection listener

In case we call the RemoveConnListener from Java then
we lose the reference from the original instance
2023-04-03 16:59:13 +02:00
pascal-fischer
769388cd21 Merge pull request #776 from netbirdio/feature/activity_events_for_pat
feature/activity_events_for_pat
2023-04-03 12:27:51 +02:00
pascal-fischer
c54fb9643c Merge pull request #774 from netbirdio/feature/add_pat_middleware
Feature/add pat middleware
2023-04-03 12:09:11 +02:00
Givi Khojanashvili
5dc0ff42a5 Fix broken auto-generated Rego rule (#769)
Default Rego policy generated from the rules in some cases is broken.
This change fixes the Rego template for rules to generate policies.

Also, file store load constantly regenerates policy objects from rules.
It allows updating/fixing of the default Rego template during releases.
2023-04-01 12:02:08 +02:00
Pascal Fischer
45badd2c39 add event store to user tests 2023-04-01 11:11:30 +02:00
Pascal Fischer
d3de035961 error responses always lower case + duplicate error response fix 2023-04-01 11:04:21 +02:00
Pascal Fischer
b2da0ae70f add activity events on PAT creation and deletion 2023-03-31 17:41:22 +02:00
Pascal Fischer
931c20c8fe fix test name 2023-03-31 12:45:10 +02:00
Pascal Fischer
2eaf4aa8d7 add test for auth middleware 2023-03-31 12:44:22 +02:00
Pascal Fischer
110067c00f change order for access control checks and aquire account lock after global lock 2023-03-31 12:03:53 +02:00
Pascal Fischer
32c96c15b8 disable linter errors by comment 2023-03-31 10:30:05 +02:00
Pascal Fischer
ca1dc5ac88 disable access control for token endpoint 2023-03-30 19:03:44 +02:00
Pascal Fischer
ce775d59ae revert codacy 2023-03-30 18:59:35 +02:00
Pascal Fischer
f273fe9f51 revert codacy 2023-03-30 18:54:55 +02:00
Pascal Fischer
e08af7fcdf codacy 2023-03-30 17:46:21 +02:00
Pascal Fischer
454240ca05 comments for codacy 2023-03-30 17:32:44 +02:00
Pascal Fischer
1343a3f00e add test + codacy 2023-03-30 16:43:39 +02:00
Pascal Fischer
2a79995706 fix linter 2023-03-30 16:22:15 +02:00
Pascal Fischer
e869882da1 fix merge 2023-03-30 16:14:51 +02:00
Pascal Fischer
6c8bb60632 fix merge 2023-03-30 16:06:46 +02:00
Pascal Fischer
4d7029d80c Merge branch 'main' into feature/add_pat_middleware
# Conflicts:
#	management/server/grpcserver.go
#	management/server/http/middleware/jwt.go
2023-03-30 16:06:21 +02:00
pascal-fischer
909f305728 Merge pull request #766 from netbirdio/feature/add_rest_endpoints_for_pat
Feature/add rest endpoints for pat
2023-03-30 15:55:48 +02:00
Pascal Fischer
5e2f66d591 fix codacy 2023-03-30 15:23:24 +02:00
Pascal Fischer
a7519859bc fix test 2023-03-30 14:15:44 +02:00
Pascal Fischer
9b000b89d5 Merge branch 'feature/add_rest_endpoints_for_pat' into feature/add_pat_middleware
# Conflicts:
#	management/server/mock_server/account_mock.go
2023-03-30 14:02:58 +02:00
Pascal Fischer
5c1acdbf2f move validation into account manager + func for get requests 2023-03-30 13:58:44 +02:00
Pascal Fischer
db3a9f0aa2 refactor jwt token validation and add PAT to middleware auth 2023-03-30 10:54:09 +02:00
Pascal Fischer
ecc4f8a10d fix Pat handler test 2023-03-29 19:13:01 +02:00
Pascal Fischer
03abdfa112 return empty object on all handlers instead of empty string 2023-03-29 18:46:40 +02:00
Pascal Fischer
9746a7f61a remove debug logs 2023-03-29 18:27:01 +02:00
Pascal Fischer
4ec6d5d20b remove debug logs 2023-03-29 18:23:10 +02:00
Pascal Fischer
3bab745142 last_used can be nil 2023-03-29 17:46:09 +02:00
Pascal Fischer
0ca3d27a80 update account mock 2023-03-29 15:25:44 +02:00
Pascal Fischer
c5942e6b33 store hashed token base64 encoded 2023-03-29 15:21:53 +02:00
Pascal Fischer
726ffb5740 add comments for exported functions 2023-03-29 15:06:54 +02:00
Maycon Santos
dfb7960cd4 Fix pre-shared key query name for android configuration (#773) 2023-03-29 10:41:14 +02:00
Zoltan Papp
ab0cf1b8aa Fix slice bounds out of range in msg decryption (#768) 2023-03-29 10:40:31 +02:00
Zoltan Papp
8ebd6ce963 Add OnDisconnecting service callback (#767)
Add OnDisconnecting service callback for mobile
2023-03-29 10:39:54 +02:00
Pascal Fischer
42ba0765c8 fix linter 2023-03-28 14:54:06 +02:00
Pascal Fischer
514403db37 use object instead of plain token for create response + handler test 2023-03-28 14:47:15 +02:00
Zoltan Papp
488d338ce8 Refactor the authentication part of mobile exports (#759)
Refactor the auth code into async calls for mobile framework

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-03-28 09:57:23 +02:00
Pascal Fischer
6a75ec4ab7 fix http error codes 2023-03-27 17:42:05 +02:00
Pascal Fischer
b66e984ddd set limits for expiration 2023-03-27 17:28:24 +02:00
Pascal Fischer
c65a934107 refactor to use name instead of description 2023-03-27 16:28:49 +02:00
Zoltan Papp
55ebf93815 Fix nil pointer exception when create config (#765)
The config stored in a wrong variable when has been generated a
new config
2023-03-27 15:37:58 +02:00
Pascal Fischer
9e74f30d2f fix delete token parameter lookup 2023-03-27 15:19:19 +02:00
Zoltan Papp
71d24e59e6 Add fqdn and address for notification listener (#757)
Extend the status notification listeners with FQDN and address
changes. It is required for mobile services.
2023-03-24 18:51:35 +01:00
Zoltan Papp
992cfe64e1 Add ipv6 test for stdnet pkg (#761) 2023-03-24 10:46:40 +01:00
Zoltan Papp
d1703479ff Add custom ice stdnet implementation (#754)
On Android, because of the hard SELinux policies can not list the
interfaces of the ICE package. Without it can not generate a host type
candidate. In this pull request, the list of interfaces comes via the Java
interface.
2023-03-24 08:40:39 +01:00
Maycon Santos
a27fe4326c Add JWT middleware validation failure log (#760)
We will log the middleware log now, but in the next
releases we should provide a generic error that can be
parsed by the dashboard.
2023-03-23 18:26:41 +01:00
Misha Bragin
e6292e3124 Disable peer expiration of peers added with setup keys (#758) 2023-03-23 17:47:53 +01:00
Maycon Santos
628b497e81 Adjustments for the change server flow (#756)
Check SSO support by calling the internal.GetDeviceAuthorizationFlowInfo

Rename LoginSaveConfigIfSSOSupported to SaveConfigIfSSOSupported

Receive device name as input for setup-key login

have a default android name when no context value is provided

log non parsed errors from management registration calls
2023-03-23 16:35:06 +01:00
Bethuel
8f66dea11c Add Keycloak Idp Manager (#746)
Added intergration with keycloak user API.
2023-03-23 14:54:31 +01:00
Pascal Fischer
de8608f99f add rest endpoints and update openapi doc 2023-03-21 16:02:19 +01:00
pascal-fischer
9c5adfea2b Merge pull request #745 from netbirdio/feature/pat_persistence
PAT persistence
2023-03-21 14:38:24 +01:00
Pascal Fischer
8e4710763e use single line return for SaveAccount 2023-03-21 14:02:34 +01:00
Pascal Fischer
82af60838e use "ok" convention for check variables throughout files_store 2023-03-21 14:00:59 +01:00
Pascal Fischer
311b67fe5a change error messages 2023-03-21 13:56:31 +01:00
Pascal Fischer
94d39ab48c improve style for tests 2023-03-21 13:34:48 +01:00
Pascal Fischer
41a47be379 add function comments, implement account mock functions and added error handling in tests 2023-03-20 16:38:17 +01:00
Pascal Fischer
e30def175b switch PATs to map and add deletion 2023-03-20 16:14:55 +01:00
Pascal Fischer
e1ef091d45 remove unnecessary string conversion 2023-03-20 12:08:01 +01:00
pascal-fischer
511ba6d51f Delete pat_handler.go 2023-03-20 11:47:54 +01:00
Pascal Fischer
b852198f67 codacy and lint hints 2023-03-20 11:44:12 +01:00
Zoltan Papp
891ba277b1 Mobile (#735)
Initial modification to support mobile client

Export necessary interfaces for Android framework
2023-03-17 10:37:27 +01:00
Zoltan Papp
747797271e Fix connstate indication (#732)
Fix the status indication in the client service. The status of the
management server and the signal server was incorrect if the network
connection was broken. Basically the status update was not used by
the management and signal library.
2023-03-16 17:22:36 +01:00
Pascal Fischer
628a201e31 fix PAT array split 2023-03-16 16:59:32 +01:00
Maycon Santos
731d3ae464 Exchange proxy mode via signal (#727)
Before defining if we will use direct or proxy connection we will exchange a 
message with the other peer if the modes match we keep the decision 
from the shouldUseProxy function otherwise we skip using direct connection.

Added a feature support message to the signal protocol
2023-03-16 16:46:17 +01:00
Pascal Fischer
453643683d add method to account mock 2023-03-16 16:44:05 +01:00
Pascal Fischer
b8cab2882b storing and retrieving PATs 2023-03-16 15:57:44 +01:00
pascal-fischer
6143b819c5 Merge pull request #725 from netbirdio/feature/add_PAT_generation
Adding Personal Access Token generation
2023-03-16 15:50:40 +01:00
Pascal Fischer
3b42d5e48a fix imports after merge 2023-03-16 11:59:12 +01:00
Pascal Fischer
1d4dfa41d2 clean dependencies 2023-03-16 11:46:53 +01:00
pascal-fischer
f8db5742b5 Merge branch 'main' into feature/add_PAT_generation 2023-03-16 11:36:43 +01:00
Pascal Fischer
bc3cec23ec use slice copy 2023-03-16 11:32:55 +01:00
Givi Khojanashvili
f03aadf064 Feat firewall controller interface (#740)
Add a standard interface for the client firewall to support ACL.
2023-03-16 13:00:08 +04:00
Zoltan Papp
292ee260ad Add version info command to signal server (#739)
Add version command to signal and management servers.

The version information will be filled during build time.
2023-03-15 07:54:51 +01:00
Givi Khojanashvili
2a1efbd0fd Don't drop Rules from file storage after migration to Policies (#741)
Rego policy migration clears the rules property of the file storage, but it does not allow rollback management upgrade, so this changes pre-saves rules in the file store and updates it from the policies.
2023-03-15 09:42:40 +04:00
Givi Khojanashvili
3bfa26b13b Feat rego default policy (#700)
Converts rules to Rego policies and allow users to write raw policies to set up connectivity and firewall on the clients.
2023-03-13 18:14:18 +04:00
Misha Bragin
221934447e Send remote agents updates when peer re-authenticates (#737)
When peer login expires, all remote peers are updated to exclude the peer from connecting.
Once a peer re-authenticates, the remote peers are not updated.
This peer fixes the behavior.
2023-03-10 17:39:29 +01:00
Misha Bragin
9ce8056b17 Use global login expiration setting when sending network map (#731)
Peers were considered expired and not sent to remote peers
when global expiration was disabled.
2023-03-09 11:24:42 +01:00
Misha Bragin
c65a5acab9 Update release banner 2023-03-09 08:24:25 +01:00
Pascal Fischer
62de082961 fix account test 2023-03-08 12:21:44 +01:00
Pascal Fischer
c4d9b76634 add comment for exported const 2023-03-08 12:09:22 +01:00
Pascal Fischer
b4bb5c6bb8 use const and do array copy 2023-03-08 11:54:10 +01:00
Pascal Fischer
2b1965c941 switch secret generation to use lib 2023-03-08 11:36:03 +01:00
Pascal Fischer
83e7e30218 store hashedToken as string 2023-03-08 11:30:09 +01:00
Zoltan Papp
24310c63e2 Remove mgm close steps, in defer doing it already (#729)
Simple code cleaning. Remove duplicated steps in login.
In the defer already close the management connection.
2023-03-07 15:01:47 +01:00
Misha Bragin
ed4f90b6aa Report offline peers to agents (#728)
The peer login expiration ACL check introduced in #714
filters out peers that are expired and agents receive a network map 
without that expired peers.
However, the agents should see those peers in status "Disconnected".

This PR extends the Agent <-> Management protocol 
by introducing a new field OfflinePeers
that contain expired peers. Agents keep track of those and display 
then just in the Status response.
2023-03-07 10:17:25 +01:00
Maycon Santos
0e9610c5b2 Refactor/clean shouldUseProxy (#722)
make code more readable by split code into smaller functions

add CandidateTypePeerReflexive check

Add shouldUseProxy tests
2023-03-06 17:33:54 +01:00
Pascal Fischer
ed470d7dbe add comments for exported functions 2023-03-06 14:46:04 +01:00
Pascal Fischer
cb8abacadd extend User Copy function 2023-03-06 14:01:18 +01:00
Pascal Fischer
bcac5f7b32 fixed some namings 2023-03-06 13:51:32 +01:00
Pascal Fischer
95d87384ab fixed some namings 2023-03-06 13:49:07 +01:00
Maycon Santos
ea3899e6d6 Update ICE to version 2.3.1 (#720)
It resolves a TLS relay issue with servername

fixes #719
2023-03-05 16:42:49 +01:00
Zoltan Papp
337d3edcc4 Use the conn state of peer on proper way (#717)
The ConnStatus is a custom type based on iota
like an enum. The problem was nowhere used to the
benefits of this implementation. All ConnStatus
instances has been compared with strings. I
suppose the reason to do it to avoid a circle
dependency. In this commit the separated status
package has been moved to peer package.

Remove unused, exported functions from engine
2023-03-03 19:49:18 +01:00
Misha Bragin
e914adb5cd Move Login business logic from gRPC API to Accountmanager (#713)
The Management gRPC API has too much business logic 
happening while it has to be in the Account manager.
This also needs to make more requests to the store 
through the account manager.
2023-03-03 18:35:38 +01:00
Pascal Fischer
2f2d45de9e updated PAT struct to only use user id instead of user 2023-03-03 16:37:39 +01:00
Pascal Fischer
b3f339c753 improved code for token checksum calc 2023-03-03 14:51:33 +01:00
Pascal Fischer
e0fc779f58 add id to the PAT 2023-03-02 16:19:31 +01:00
Zoltan Papp
f64e0754ee Config cleaning (#710)
Code cleaning in the config.go of the client. This change keep the
logic in original state. The name of the exported function was not
covered well the internal workflow. Without read the comment was not
understandable what is the difference between the GetConfig and
ReadConfig. By the way both of them doing write operation.
2023-03-02 13:28:14 +01:00
Misha Bragin
fe22eb3b98 Check peer expiration after ACL check (#714)
Bug 1: When calculating the network map, peers added by a setup key
were falling under expiration logic while they shouldn't.

Bug 2: Peers HTTP API didn't return expired peers for non-admin users
because of the expired peer check in the ACL logic.

The fix applies peer expiration checks outside of the ACL logic.
2023-03-02 12:45:10 +01:00
Pascal Fischer
69be2a8071 add generating token (only frame for now, actual token is only dummy) 2023-03-01 20:12:04 +01:00
Misha Bragin
1bda8fd563 Remove stale peer indices when getting peer by key after removing (#711)
When we delete a peer from an account, we save the account in the file store.
The file store maintains peerID -> accountID and peerKey -> accountID indices.
Those can't be updated when we delete a peer because the store saves the whole account
without a peer already and has no access to the removed peer.
In this PR, we dynamically check if there are stale indices when GetAccountByPeerPubKey
and GetAccountByPeerID.
2023-03-01 12:11:32 +01:00
pascal-fischer
1ab791e91b Merge pull request #707 from netbirdio/chore/NB-93-re-arrange-api-code
chore/re-arrange-api-code
2023-03-01 09:52:42 +01:00
Misha Bragin
41948f7919 Fix peer status update when expiring peers (#708) 2023-02-28 20:02:30 +01:00
Pascal Fischer
60f67076b0 change methods to not link 2023-02-28 18:17:55 +01:00
Pascal Fischer
c645171c40 split api code into smaller pieces 2023-02-28 18:08:02 +01:00
pascal-fischer
f832c83a18 Merge pull request #706 from netbirdio/chore/rename_handler_objects_and_methods_for_api
chore/rename_handler_objects_and_methods_for_api
2023-02-28 17:15:27 +01:00
Zoltan Papp
462a86cfcc Allow to create config file next to binary (#701)
Force to use the proper temp dir

If we do not define the configDir then the Go
create a random temp dir for copy routine.
It is not optimal from security purpose.
2023-02-28 17:01:38 +01:00
Pascal Fischer
8a130ec3f1 add comments to fix codacy 2023-02-28 16:51:30 +01:00
Pascal Fischer
c26cd3b9fe add comments for constructors and fix typo 2023-02-28 15:46:08 +01:00
Pascal Fischer
9d7b515b26 changed the naming convention for all handling objects and methods to have unified way 2023-02-28 15:27:43 +01:00
Pascal Fischer
f1f90807e4 changed the naming convention for all handling objects and methods to have unified way 2023-02-28 15:01:24 +01:00
pascal-fischer
5bb875a0fa Merge pull request #704 from netbirdio/feature/extend-client-status-cmd-to-print-json-or-yaml
Feature/extend client status cmd to print json or yaml
2023-02-28 11:17:20 +01:00
pascal-fischer
9a88ed3cda Use regex in formatter test because order of attributes can vary (#705)
Fix test for formatter where the attributes are changing order 
for some reason to not have random test failures.
Used regex to catch both cases.
2023-02-28 09:25:44 +01:00
Pascal Fischer
8026c84c95 remove flag test 2023-02-27 17:45:02 +01:00
Pascal Fischer
82059df324 remove daemon status from output 2023-02-27 17:12:34 +01:00
Pascal Fischer
23610db727 apply first set of review comments (mostly reorder and naming) 2023-02-27 17:06:20 +01:00
Misha Bragin
f984b8a091 Proactively expire peers' login per account (#698)
Goals:

Enable peer login expiration when adding new peer
Expire peer's login when the time comes
The account manager triggers peer expiration routine in future if the
following conditions are true:

peer expiration is enabled for the account
there is at least one peer that has expiration enabled and is connected
The time of the next expiration check is based on the nearest peer expiration.
Account manager finds a peer with the oldest last login (auth) timestamp and
calculates the time when it has to run the routine as a sum of the configured
peer login expiration duration and the peer's last login time.

When triggered, the expiration routine checks whether there are expired peers.
The management server closes the update channel of these peers and updates
network map of other peers to exclude expired peers so that the expired peers
are not able to connect anywhere.

The account manager can reschedule or cancel peer expiration in the following cases:

when admin changes account setting (peer expiration enable/disable)
when admin updates the expiration duration of the account
when admin updates peer expiration (enable/disable)
when peer connects (Sync)
P.S. The network map calculation was updated to exclude peers that have login expired.
2023-02-27 16:44:26 +01:00
pascal-fischer
4330bfd8ca Merge branch 'main' into feature/extend-client-status-cmd-to-print-json-or-yaml 2023-02-27 16:00:40 +01:00
Pascal Fischer
5782496287 fix codacy 2023-02-27 15:52:46 +01:00
Pascal Fischer
a0f2b5f591 fix codacy 2023-02-27 15:34:17 +01:00
Pascal Fischer
0350faf75d return empty strings for not applicable values 2023-02-27 15:14:41 +01:00
Zoltan Papp
9f951c8fb5 Add human-readbale log output (#681)
Add human-readable log output. It prints out the exact source code line information.
2023-02-27 12:20:07 +01:00
Pascal Fischer
8276e0908a clean go.mod 2023-02-27 11:33:12 +01:00
Pascal Fischer
6539b591b6 fix indention in test for detail output 2023-02-27 11:23:34 +01:00
Pascal Fischer
014f1b841f fix indention in test for yaml output 2023-02-27 11:04:53 +01:00
Maycon Santos
b52afe8d42 Update pion/ICE and its dependencies (#703)
Among other improvements, it fixes a memory leak with
srfx conn channels not being closed

it also make use of new pion/transport Net interface
https://github.com/pion/ice/pull/471
2023-02-24 19:30:23 +01:00
Pascal Fischer
f36869e97d use yaml v3 2023-02-24 19:14:22 +01:00
Pascal Fischer
78c6231c01 Added Output struct to properly name json and yaml attr's and add missing tests 2023-02-24 19:01:54 +01:00
Pascal Fischer
e75535d30b Refactor status functions and add first tests 2023-02-23 20:13:19 +01:00
Zoltan Papp
d8429c5c34 Fix nil pointer exception in config parser (#702)
In config reader if the input.PreSharedKey is nil then the GetConfig
throw nil pointer exception
2023-02-23 09:48:43 +01:00
Zoltan Papp
c3ed08c249 Fix nil pointer exception in error handling (#696)
In case if the wgctrl.New() return with err, should not close the
resource.
2023-02-21 10:50:34 +01:00
Zoltan Papp
2f0b652dad Fix error handling in Stop/Start functions (#699)
Properly close all resources in case of any error
during the start or stop procedure
2023-02-21 10:46:58 +01:00
Maycon Santos
d4214638a0 Update service pkg with log directory fix (#692)
This service pkg update includes directory check and creation
2023-02-16 18:04:14 +01:00
Misha Bragin
c962d29280 Fix login expiration enum in OpenAPI (#694)
Add missing OpenAPI enums for the peer login expiration events
2023-02-16 15:36:36 +01:00
Misha Bragin
44af5be30f Reject peer login expiration update when no SSO login (#693) 2023-02-16 13:03:53 +01:00
Misha Bragin
fe63a64b6e Add Account HTTP API (#691)
Extend HTTP API with Account endpoints to configure global peer login expiration.
GET /api/accounts
PUT /api/account/{id}/

The GET endpoint returns an array of accounts with
always one account in the list. No exceptions.

The PUT endpoint updates account settings:
PeerLoginExpiration and PeerLoginExpirationEnabled.

PeerLoginExpiration is a duration in seconds after which peers' logins will expire.
2023-02-16 12:00:41 +01:00
Misha Bragin
d31219ba89 Update peer status when login expires (#688)
Extend PeerStatus with an extra field LoginExpired, that can be stored in the database.
2023-02-15 11:27:22 +01:00
Misha Bragin
756ce96da9 Add login expiration fields to peer HTTP API (#687)
Return login expiration related fields in the Peer HTTP GET endpoint.
Support enable/disable peer's login expiration via HTTP PUT.
2023-02-14 10:14:00 +01:00
Zoltan Papp
b64f5ffcb4 Mobile prerefactor (#680)
Small code cleaning in the iface package. These changes necessary to 
get a clean code in case if we involve more platforms. The OS related 
functions has been distributed into separate files and it has been 
mixed with not OS related logic. The goal is to get a clear picture 
of the layer between WireGuard and business logic.
2023-02-13 18:34:56 +01:00
Givi Khojanashvili
eb45310c8f Fix nameserver peer conn check (#676)
* Disable upstream DNS resolver after several tries and fails

* Add tests for upstream fails

* Use an extra flag to disable domains in DNS upstreams

* Fix hashing IPs of nameservers for updates.
2023-02-13 18:25:11 +04:00
Misha Bragin
d5dfed498b Add account settings (#686)
Add account settings with a global peer expiration flag and duration
2023-02-13 15:07:15 +01:00
Misha Bragin
3fc89749c1 Add peer login expiration (#682)
This PR adds a peer login expiration logic that requires
peers created by a user to re-authenticate (re-login) after
a certain threshold of time (24h by default).

The Account object now has a PeerLoginExpiration
property that indicates the duration after which a peer's
login will expire and a login will be required. Defaults to 24h.

There are two new properties added to the Peer object:
LastLogin that indicates the last time peer successfully used
the Login gRPC endpoint and LoginExpirationEnabled that
enables/disables peer login expiration.

The login expiration logic applies only to peers that were created
by a user and not those that were added with a setup key.
2023-02-13 12:21:02 +01:00
Maycon Santos
aecee361d0 Use new sign pipeline v0.0.5 (#679) 2023-02-13 12:13:28 +01:00
Misha Bragin
f8273c3ce9 Add network activity monitoring as complete in Readme (#675) 2023-02-08 08:38:10 +01:00
Misha Bragin
00a8092482 Add GET peer HTTP API endpoint (#670) 2023-02-07 20:11:08 +01:00
Maycon Santos
64dbd5fbfc Refactor Management and Admin URL config (#674)
avoid sending admin or management URLs on service start
as it doesn't have an input

Parse management and admin URL when needed

Pass empty admin url on commands to prevent default overwrite
2023-02-07 11:40:05 +01:00
Maycon Santos
b5217350cf Revert preshared-key shorthand flag (#671)
This prevents conflict with the
 ssh command shorthand flag

add some init tests
2023-02-06 10:35:37 +01:00
Givi Khojanashvili
3ec8274b8e Feature: add custom id claim (#667)
This feature allows using the custom claim in the JWT token as a user ID.

Refactor claims extractor with options support

Add is_current to the user API response
2023-02-03 21:47:20 +01:00
tcskiran
494e56d1be Macos version error (#666)
use sw_vers to retrieve the proper macOS version.
2023-02-03 21:40:30 +01:00
Misha Bragin
9adadfade4 Use Peer.ID instead of Peer.Key as peer identifier (#664)
Replace Peer.Key as internal identifier with a randomly generated Peer.ID 
in the Management service.
Every group now references peers by ID instead of a public key.
Every route now references peers by ID instead of a public key.
FileStore does store.json file migration on startup by generating Peer.ID and replacing
all Peer.Key identifier references .
2023-02-03 10:33:28 +01:00
Misha Bragin
9e408b5bbc Add more activity events (#663) 2023-01-25 16:29:59 +01:00
Misha Bragin
a0de9aa345 Simplify event storing with one generic method (#662)
Use the generic storeEvent() funcion to store all activity events.
2023-01-24 10:17:24 +01:00
Maycon Santos
4406d50c18 Check if peer name change before update labels (#658) 2023-01-20 10:07:37 +01:00
Maycon Santos
5e3502bb83 Enable CGO in goreleaser for management (#657)
Update the debug docker image file

update goreleaser version
Install and reference CGO OS deps
2023-01-19 15:03:10 +01:00
Maycon Santos
793e4f1f29 Allow empty groups for DNS setting (#656)
We should allow passing empty group slice for DisabledManagementGroups to reset any setting.


Fix DNS settings activities message
2023-01-18 14:01:50 +01:00
Maycon Santos
dcf6533ed5 Adding --external-ip-map and --dns-resolver-address and shorthand flags (#652)
Adding --external-ip-map and --dns-resolver-address to up command and shorthand option to global flags.

Refactor get and read config functions with new ConfigInput type.

updated cobra package to latest release.
2023-01-17 19:16:50 +01:00
Maycon Santos
12ae2e93fc Adding DNS settings for accounts (#655)
Allow users to set groups in which the DNS management is disabled

Added API, activity store, and network map sync test
2023-01-17 17:34:40 +01:00
Zoltan Papp
2bc3d88af3 Involve foreground mode switch for up cmd (#654)
Add new --deamon-off command line parameter
for 'up' cmd instead of existing log-file workaround

Split the up function and organize the code
2023-01-16 18:12:51 +01:00
Misha Bragin
afaf0660be Install ca-certs in the Management docker image build (#650) 2023-01-11 08:19:22 +01:00
Misha Bragin
d4d8c5f037 Fix cgo dependencies in the management docker container (#649) 2023-01-10 17:24:51 +01:00
Maycon Santos
e5adc1eb23 Use macOS v11 to build UI release (#648)
This allows us to run clients on older macOS versions
2023-01-10 16:49:24 +01:00
Jonathan Hult
44f612f121 Fix typo - "netforwad" -> "netforward" (#647) 2023-01-08 23:51:08 +01:00
Roy
f9dfafa9d9 Add device flow scope. (#616)
add the openid as the base scope
2023-01-08 22:26:14 +01:00
Maycon Santos
ca62f6787a Use configuration input struct (#645)
As we will be passing more flags to configure
 local agents, we need a more flexible type
2023-01-08 12:57:28 +01:00
Maycon Santos
27f4993ce3 Add netbird installer project (#637) 2023-01-08 11:33:04 +01:00
Misha Bragin
5c0b8a46f0 Add system activity tracking and event store (#636)
This PR adds system activity tracking. 
The management service records events like 
add/remove peer,  group, rule, route, etc.

The activity events are stored in the SQLite event store
and can be queried by the HTTP API.
2023-01-02 15:11:32 +01:00
Oskar Manhart
50caacff69 Fix COTURN config when selfhosting netbird
Coturn doesn't read the turnserver.conf when selfhosting netbird.
This PR fixes that.
2022-12-22 12:02:48 +01:00
Maycon Santos
d18966276a Store the previous applied dns configuration hash (#628)
This prevents changing the system
DNS config when there is nothing to new

It also prevents issues with network change on google chrome
2022-12-16 17:00:20 +01:00
Maycon Santos
6b32e2dc07 Validate single account domain input (#624) 2022-12-13 13:43:29 +01:00
Maycon Santos
c0a62b6ddc Add DNS domain to getting started scripts (#625) 2022-12-13 13:42:43 +01:00
Maycon Santos
7dfef091bb Properly parse dns resolver address (#622)
Prevent panic when address is empty. Common with older managers, where
resolver is disabled by default as
we receive an empty dns config
2022-12-13 12:26:48 +01:00
Maycon Santos
93fcfeae91 Export single account domain variable 2022-12-08 19:45:33 +01:00
Maycon Santos
6f610dca89 Filter routes to sync from same HA group (#618)
An additional check and filter for routes that are part
 of the same HA group where the peer is a routing peer
2022-12-08 15:15:50 +01:00
Maycon Santos
eec24fc730 Use latest tag for dashboard (#617)
Using the latest tag will align with the
dashboard's new release cycle that relies on tags
2022-12-08 15:15:17 +01:00
Krzysztof Nazarewski
1204bbd54a HA Network Routes: prevent routing directly-accessible networks through VPN interface (#612)
Prevent routing peer to add routes from the same HA group as client routes
2022-12-08 13:19:55 +01:00
Maycon Santos
0be46c083d Generate validation certificate from mandatory JWK fields (#614)
When there is no X5c we will use N and E fields of 
a JWK to generate the public RSA and a Pem certificate
2022-12-07 22:06:43 +01:00
Maycon Santos
0fbfec4ce4 Remove wiretrustee conflict checks (#615) 2022-12-07 18:53:48 +01:00
Maycon Santos
d43f0200a6 Handle peer deletion and state update (#611)
If peer is deleted in the console,
we set its state as needs login

On Down command we clean any previous state errors
this prevents need for daemon restart

Removed state error wrapping when engine exits, log is enough
2022-12-06 15:37:30 +01:00
Maycon Santos
a387e3cfc2 Add network routes distribution groups (#606)
Updated tests, API, and account manager methods

Sync routes to peers in the distribution groups

Added store upgrade by adding the All group to routes that don't have them
2022-12-06 10:11:57 +01:00
Misha Bragin
d1b7c23b19 Add SetupKey usage limit (#605)
Add a usage_limit parameter to the API.
This limits the number of times a setup key
can be used. 
usage_limit == 0 indicates the the usage is inlimited.
2022-12-05 13:09:59 +01:00
Maycon Santos
d2d5d4b4b9 Update go version (#603)
Removed ioctl code and remove exception from lint action
2022-12-04 13:22:21 +01:00
Maycon Santos
d029136d3d Add security policy file (#600) 2022-12-02 13:54:22 +01:00
Maycon Santos
a6d2f673ad Add contribution guide (#595)
* Add contribution guide

* update code of conduct contact email

* add PR template
2022-12-02 13:31:31 +01:00
Maycon Santos
0cf0dc048b Update issue templates (#597) 2022-12-02 13:31:15 +01:00
Rui Lopes
5ade879e31 Remove the leading space from the Signal status value (#594) 2022-12-01 11:48:13 +01:00
Maycon Santos
a814715ef8 Add resolvconf configurator for linux (#592) 2022-11-29 14:51:18 +01:00
Maycon Santos
4a30b66503 Check if system is our manager when resolvconf (#590)
Sometimes resolvconf will manage the /etc/resolv.conf file
And systemd-resolved still the DNS manager
2022-11-29 13:37:50 +01:00
Maycon Santos
ae500b63a7 User custom loopback address (#589)
We will probe a set of addresses and port
to define the one available for our DNS service

if none is available, we return an error
2022-11-29 11:49:18 +01:00
Maycon Santos
20a73e3e14 Sync peers FQDN (#584)
Use stdout and stderr log path only if on Linux and attempt to create the path

Update status system with FQDN fields and 
status command to display the domain names of remote and local peers

Set some DNS logs to tracing

update readme file
2022-11-26 13:29:50 +01:00
Misha Bragin
fcf7786a85 Disable route when removing peer (#582) 2022-11-25 18:11:07 +01:00
Maycon Santos
a78fd69f80 Feature/dns client configuration (#563)
Added host configurators for Linux, Windows, and macOS.

The host configurator will update the peer system configuration
 directing DNS queries according to its capabilities.

Some Linux distributions don't support split (match) DNS or custom ports,
 and that will be reported to our management system in another PR
2022-11-23 13:39:42 +01:00
Genteure
4bd5029e7b Enable IPv6 address discovery (#578)
Agents will use IPv6 when available for ICE negotiation
2022-11-23 11:03:29 +01:00
Tom Kunicki
f604956246 External NAT IP mapping support (#487)
* External NAT IP mapping support

* Ignore blacklisted interfaces, even if in user specified in  mapping
2022-11-23 08:42:12 +01:00
Misha Bragin
53c532bbb4 Fix interactive SSO login when creating account from a device (#575) 2022-11-22 12:37:36 +01:00
Misha Bragin
8b0a1bbae0 Display peers of a user that it has access to (#571)
If a user has a non-admin role, display all peers
that user's peers have access to when calling
/peers endpoint of the HTTP API.
2022-11-21 17:45:14 +01:00
Misha Bragin
e965d6c022 Fix CISPA note 2022-11-21 17:36:07 +01:00
Misha Bragin
11f8249eed Add CISPA note (#572) 2022-11-21 16:38:41 +01:00
Maycon Santos
d63a9ce4a7 Return peer's FQDN via API (#567)
Added a temp method to retrieve the dns domain
2022-11-21 11:14:42 +01:00
Maycon Santos
9cb66bdb5d Update last run time and active count (#568)
* Update last run time and active count

We will collect the active peer min and max versions

* Get UI client usage
2022-11-18 16:35:13 +01:00
Genteure
c8ace8bbbe Fix docker network interface filter (#564)
docker network address are assigned on network interfaces that start with "br-"
2022-11-15 22:07:58 +01:00
Misha Bragin
509d23c7cf Replace gRPC errors in business logic with internal ones (#558) 2022-11-11 20:36:45 +01:00
Misha Bragin
1db4027bea Remove docs typo 2022-11-10 10:48:00 +01:00
Misha Bragin
d4dbc322be Add ref to ICE in Readme 2022-11-10 10:46:40 +01:00
Misha Bragin
e19d5dca7f Refactor AddPeer to ensure consistency (#557) 2022-11-08 16:14:36 +01:00
Maycon Santos
157137e4ad Use a single way to generate network map (#550) 2022-11-08 11:38:40 +01:00
Maycon Santos
7d7e576775 Set report caller when info or higher (#555) 2022-11-08 10:56:13 +01:00
Misha Bragin
f37b43a542 Save Peer Status separately in the FileStore (#554)
Due to peer reconnects when restarting the Management service,
there are lots of SaveStore operations to update peer status.

Store.SavePeerStatus stores peer status separately and the
FileStore implementation stores it in memory.
2022-11-08 10:46:12 +01:00
Maycon Santos
7e262572a4 Move dns label generation to store (#552) 2022-11-08 10:31:34 +01:00
Misha Bragin
a768a0aa8a Always lock the store when getting an account (#551) 2022-11-07 19:09:22 +01:00
Misha Bragin
ed7ac81027 Introduce locking on the account level (#548) 2022-11-07 17:52:23 +01:00
Maycon Santos
1f845f466c Add account copy test (#549) 2022-11-07 17:37:28 +01:00
Maycon Santos
270f0e4ce8 Feature/dns protocol (#543)
Added DNS update protocol message

Added sync to clients

Update nameserver API with new fields

Added default NS groups

Added new dns-name flag for the management service append to peer DNS label
2022-11-07 15:38:21 +01:00
Misha Bragin
d0c6d88971 Simplified Store Interface (#545)
This PR simplifies Store and FileStore
by keeping just the Get and Save account methods.

The AccountManager operates mostly around
a single account, so it makes sense to fetch
the whole account object from the store.
2022-11-07 12:10:56 +01:00
Misha Bragin
4321b71984 Hide content based on user role (#541) 2022-11-05 10:24:50 +01:00
Maycon Santos
e8d82c1bd3 Feature/dns-server (#537)
Adding DNS server for client

Updated the API with new fields

Added custom zone object for peer's DNS resolution
2022-11-03 18:39:37 +01:00
Misha Bragin
6aa7a2c5e1 Hide setup key from non-admin users (#539) 2022-11-03 17:02:31 +01:00
Rui Lopes
2e0bf61e9a correctly set the windows application icon on windows (#535)
the icon format is not really supported, so this uses a png instead.

this closes https://github.com/netbirdio/netbird/issues/534.
2022-11-01 00:34:30 +01:00
Maycon Santos
126af9dffc Return gateway address if not nil (#533)
If the gateway address would be nil which is
the case on macOS, we return the preferredSrc

added tests for getExistingRIBRouteGateway function

update log message
2022-10-31 11:54:34 +01:00
Maycon Santos
4cdf2df660 Update sign pipeline version to 0.0.4 (#531)
This version has a fix for the
macOS UI client architecture
2022-10-31 11:03:42 +01:00
Maycon Santos
9a4c9aa286 Add active peers count per OS (#526)
* Add active peers count per OS

* increase iface tests timeout
2022-10-26 14:48:40 +02:00
Rui Lopes
5ed61700ff Set the application icon, settings window title and systray tooltip (#523) 2022-10-26 14:34:30 +02:00
Misha Bragin
84117a9fb7 Update WireGuard trademark note 2022-10-23 11:47:42 +02:00
Misha Bragin
92b612eba4 Update demo video link 2022-10-22 16:55:49 +02:00
Misha Bragin
aeeaa21eed Update README.md (#524) 2022-10-22 16:19:16 +02:00
Misha Bragin
d228cd0cb1 Remove release note 2022-10-22 15:10:09 +02:00
Misha Bragin
b41f36fccd Add gRPC metrics (#522) 2022-10-22 15:06:54 +02:00
Misha Bragin
d2cde4a040 Add IdP metrics (#521) 2022-10-22 13:29:39 +02:00
Misha Bragin
84879a356b Extract app metrics to a separate struct (#520) 2022-10-22 11:50:21 +02:00
Misha Bragin
ed2214f9a9 Add HTTP request/response totals to metrics (#519) 2022-10-22 10:07:13 +02:00
braginini
4388dcc20b Listen metrics on all interfaces 2022-10-21 16:50:06 +02:00
Misha Bragin
4f1f0df7d2 Add Open-telemetry support (#517)
This PR brings open-telemetry metrics to the
Management service.
The Management service exposes new HTTP endpoint
/metrics on 8081 port by default.
The port can be changed by specifying
--metrics-port PORT flag when starting the service.
2022-10-21 16:24:13 +02:00
Misha Bragin
08ddf04c5f Fix IdP tests (#516) 2022-10-19 18:36:10 +02:00
Misha Bragin
b5ee2174a8 Do not set wt_pending_invite when unnecessary (#515)
wt_pending_invite property is set for every user on IdP.
Avoid setting it when unnecessary.
2022-10-19 17:51:41 +02:00
Misha Bragin
7218a3d563 Management single account mode (#511) 2022-10-19 17:43:28 +02:00
Maycon Santos
04e4407ea7 Add anonymous usage metrics collection (#508)
This will help us understand usage on self-hosted deployments

The collection may be disabled by using the flag --disable-anonymous-metrics or 
NETBIRD_DISABLE_ANONYMOUS_METRICS in setup.env
2022-10-16 13:33:46 +02:00
Misha Bragin
06055af361 Super user invites (#483)
This PR brings user invites logic to the Management service
via HTTP API. 
The POST /users/ API endpoint creates a new user in the Idp
and then in the local storage. 
Once the invited user signs ups, the account invitation is redeemed.
There are a few limitations.
This works only with an enabled IdP manager.
Users that already have a registered account can't be invited.
2022-10-13 18:26:31 +02:00
Maycon Santos
abd1230a69 Disable uninstall message when upgrade is silent (#505)
Fix a problem with $INSTDIR pointing to subfolder
2022-10-13 15:00:39 +02:00
Maycon Santos
f7de12daf8 Support custom redirect URIs (#499) 2022-10-12 12:25:46 +02:00
Maycon Santos
c49fb0c40c Get windows version from registry (#502)
Avoid problems with localization by retrieving
version information from registry

Return a default 0.0.0.0 if operation fails
2022-10-12 12:25:33 +02:00
Maycon Santos
6e9a162877 Seticon only when status changes (#504)
* Seticon only when status changes

This prevents a memory leak with the systray lib
when setting the icon every 2 seconds causes a large memory consumption

see https://github.com/getlantern/systray/issues/135

* Use fork with permanent fix
2022-10-12 12:25:06 +02:00
Maycon Santos
b4e03f4616 Feature/add nameservers API endpoint (#491)
Add nameservers endpoint and Open API definition

updated open api generator cli
2022-10-10 11:06:54 +02:00
Misha Bragin
369a7ef345 Add SSO MFA demo gif (#489) 2022-10-10 11:06:25 +02:00
Maycon Santos
c88e6a7342 Run tests only on branch main and on pull requests (#492)
* Use reusable workflow and control push and pr test exec

* use format

* use path ref

* Run tests on push to main and on PRs
2022-10-03 00:17:16 +05:00
Maycon Santos
2cd9b11e7d Add DNS nameserver support to management (#484)
Add DNS package and Nameserver group objects

Add CRUD operations for Nameserver Groups to account manager

Add Routes and Nameservers to Account Copy method

Run docker tests with timeout and serial flags
2022-09-30 16:47:11 +05:00
Maycon Santos
93d20e370b Add incoming routing rules (#486)
add an income firewall rule for each routing pair
the pair for the income rule has inverted
source and destination
2022-09-30 14:39:15 +05:00
Maycon Santos
878ca6db22 Check if domain from claim is valid (#485)
If domain is invalid we call GetAccountByUserOrAccountId
2022-09-29 13:51:18 +05:00
braginini
2033650908 Remove IdP client secret validation 2022-09-26 18:58:14 +02:00
Misha Bragin
34c1c7d901 Add hostname, userID, ui version to the HTTP API peer response (#479) 2022-09-26 18:02:45 +02:00
Misha Bragin
051fd3a4d7 Fix Management and Signal gRPC client stream leak (#482) 2022-09-26 18:02:20 +02:00
Misha Bragin
af69a48745 Support user role update (#478) 2022-09-23 14:18:42 +02:00
Maycon Santos
68ff97ba84 Parse and received provider proper error message (#476) 2022-09-23 14:18:29 +02:00
Misha Bragin
c5705803a5 Output plain NetBird IPv4 in status command (#474) 2022-09-22 09:25:52 +02:00
braginini
7e1ae448e0 Add extra logging to Sync and Login requests 2022-09-22 09:25:31 +02:00
Misha Bragin
518a2561a2 Add auto-assign groups to the User API (#467) 2022-09-22 09:06:32 +02:00
Maycon Santos
c75ffd0f4b Update ICE library (#471) 2022-09-20 11:40:18 +02:00
Maycon Santos
e4ad6174ca Improve module load (#470)
* Add additional check for needed kernel modules

* Check if wireguard and tun modules are loaded

If modules are loaded return true, otherwise attempt to load them

* fix state check

* Add module function tests

* Add test execution in container

* run client package tests on docker

* add package comment to new file

* force entrypoint

* add --privileged flag

* clean only if tables where created

* run from within the directories
2022-09-15 01:26:11 +05:00
Misha Bragin
6de313070a Always return empty auto_groups if previously were nil (#468) 2022-09-13 17:19:03 +02:00
Misha Bragin
cd7d1a80c9 Assign groups to peers when registering with the setup key (#466) 2022-09-13 13:39:46 +02:00
Misha Bragin
be7d829858 Add SetupKey auto-groups property (#460) 2022-09-11 23:16:40 +02:00
Maycon Santos
ed1872560f Use the client network for log errors (#455) 2022-09-07 18:26:59 +02:00
Maycon Santos
de898899a4 update slack invite tittle 2022-09-05 18:44:04 +02:00
Maycon Santos
b63ec71aed Check if login stream was canceled before printing warn (#451) 2022-09-05 17:44:26 +02:00
Maycon Santos
1012172f04 Add routing peer support (#441)
Handle routes updates from management

Manage routing firewall rules

Manage peer RIB table

Add get peer and get notification channel from the status recorder

Update interface peers allowed IPs
2022-09-05 09:06:35 +02:00
Maycon Santos
788bb00ef1 Fix service install when sysV service bin exists (#450) 2022-09-05 08:56:07 +02:00
Maycon Santos
4e5ee70b3d Load WgPort from config file and exchange via signal (#449)
Added additional common blacklisted interfaces

Updated the signal protocol to pass the peer port and netbird version

Co-authored-by: braginini <bangvalo@gmail.com>
2022-09-02 19:33:35 +02:00
Maycon Santos
f1c00ae543 Update service library with rcS init system support (#447) 2022-09-02 14:03:02 +02:00
Misha Bragin
553a13588b Free up gRPC client resources on errors (#448) 2022-09-01 18:28:45 +02:00
Maycon Santos
586c0f5c3d Log remote address when not registered (#445) 2022-08-27 17:55:05 +02:00
Maycon Santos
c13f0b9f07 Use select for turn credentials and peers update (#443)
Also, prevent peer update when SSH is the same
2022-08-27 12:57:03 +02:00
Misha Bragin
dd4ff61b51 Do not autoload authissuer for the IDPManager config (#442) 2022-08-25 09:24:24 +02:00
szakharchenko
e3657610bc Avoid pulling in management code in client (#437)
Avoid management code import for the legacy port value, hardcoding it
instead (it's literally spelled out in a comment below as well).
2022-08-24 16:30:40 +02:00
Misha Bragin
e8733a37af Update scripts for the self-hosted Oauth 2.0 Device Auth Grant support (#439)
Support Oauth 2.0 Device Auth Grant in the
self-hosted scripts.
2022-08-24 14:37:18 +02:00
Misha Bragin
3def84b111 Support Generic OAuth 2.0 Device Authorization Grant (#433)
Support Generic OAuth 2.0 Device Authorization Grant
as per RFC specification https://www.rfc-editor.org/rfc/rfc8628.
The previous version supported only Auth0 as an IDP backend.
This implementation enables the Interactive SSO Login feature 
for any IDP compatible with the specification, e.g., Keycloak.
2022-08-23 15:46:12 +02:00
Maycon Santos
47add9a9c3 Don't create index if peer is empty (#435)
When checking for existing prefix routes
Return nil if peer is empty
2022-08-23 11:09:56 +02:00
Maycon Santos
09312b3e6d Add Network ID and rename Prefix to Network (#432)
Adding network ID will allow us to group

Renaming Prefix with Network
will keep things more clear and Consistent
2022-08-22 14:10:24 +02:00
Misha Bragin
762a26dcea Fix Register/Deregister race on Signal (#431)
This PR fixes a race condition that happens
when agents connect to a Signal stream, multiple
times within a short amount of time. Common on
slow and unstable internet connections.
Every time an agent establishes a new connection
to Signal, Signal creates a Stream and writes an entry
to the registry of connected peers storing the stream.
Every time an agent disconnects, Signal removes the
stream from the registry.
Due to unstable connections, the agent could detect
a broken connection, and attempt to reconnect to Signal.
Signal will override the stream, but it might detect
the old broken connection later, causing peer deregistration.
It will deregister the peer leaving the client thinking
it is still connected, rejecting any messages.
2022-08-22 12:21:19 +02:00
Maycon Santos
000ea72aec Add routing Rest API support (#428)
Routing API will allow us to list, create, update, and delete routes.
2022-08-20 19:11:54 +02:00
Maycon Santos
4b34a6d6df Add routing support to management service (#424)
Management will receive and store routes that are associated with a peer ID.
The routes are distributed to peers according to their ACLs.
2022-08-18 18:22:15 +02:00
Misha Bragin
c39cd2f7b0 Support new properties for OIDC auth (#426)
This PR updates infrastructure_scripts to support
self-hosted setup with a generic OIDC provider.
2022-08-17 21:44:20 +02:00
Misha Bragin
6dc3e8ca90 Enable HTTP/2 when loading TLS config from file (#423)
When creating TLSConfig from provided certificate file, the HTTP/2 support is not enabled.
It works with Certmanager because it adds h2 support.
We enable it the same way when creating TLSConfig from files.
2022-08-15 19:36:00 +02:00
Misha Bragin
245863cd51 Update docker-compose to reflect new ports (#411) 2022-08-05 22:41:57 +02:00
Maycon Santos
14e322d3f7 Handle CORS requests before authentication (#413)
This helps our FE to get proper request responses
2022-08-05 22:41:04 +02:00
Misha Bragin
1be8c16e34 Decrease log level on peer status remove (#410) 2022-08-01 17:52:22 +02:00
Misha Bragin
851de3fd4e Output NetBird daemon and CLI versions on status command (#408) 2022-08-01 12:42:45 +02:00
Maycon Santos
c13288781f Fix checksum conflict and version injection (#409)
custom name_template for darwin ui release checksum file

fix darwin ui version injection to correct path
2022-08-01 12:20:30 +02:00
Misha Bragin
e34e0ccd12 Check and update Agent's Management URL if is legacy (#406)
All the existing agents by default connect to port 33073 of the
Management service. This value is also stored in the local config.
All the agents won't switch to the new port 443
unless explicitly specified in the config.
We want the transition to be smooth for our users, therefore
this PR adds logic to check whether the old port 33073 can be
changed to 443 and updates the config automatically.
2022-07-30 19:17:18 +02:00
Maycon Santos
95dc9cc16c Split goreleaser for UI and parallelized workflow (#405)
decouple goreleaser ui might help us
parallelize workflow and run local tests

dividing the release workflow for each goreleaser
and making trigger sign a different job them
when small issues with sign happen
2022-07-30 14:44:01 +02:00
Maycon Santos
d1c2b3d703 Use unix.Uname to get Darwin system info (#404)
This prevents the client from needing to use command line tools
2022-07-30 11:31:27 +02:00
Misha Bragin
966661fe91 Serve Management gRPC and HTTP on a single 80/443 port (#400)
This PR is a part of an effort to use standard ports (443 or 80) that are usually allowed by default in most of the environments.

Right now Management Service runs the Let'sEncrypt manager on port 443, HTTP API server on port 33071,
and a gRPC server on port 33073. There are three separate listeners.
This PR combines these listeners into one.
With this change, the HTTP and gRPC server runs on either 443 with TLS or 80 without TLS
by default (no --port specified).
Let's Encrypt manager always runs on port 443 if enabled.
The backward compatibility server runs on port 33073 (with TLS or without).
HTTP port 33071 is obsolete and not used anymore.

Newly installed agents will connect to port 443 by default instead of port 33073 if not specified otherwise.
2022-07-29 20:37:09 +02:00
Misha Bragin
67ddaade58 Go mod tidy (#401)
Check git status after go mod tidy
2022-07-27 20:19:55 +02:00
Maycon Santos
138cf35e00 Sync go mod (#399) 2022-07-27 18:57:18 +02:00
Maycon Santos
2555a6c3e8 Use proxy when any candidate is relay (#398)
We should use relayed port when remote or local
candidate is of the relay type
2022-07-27 18:12:39 +02:00
Misha Bragin
86a66c6202 Make Signal Service listen on a standard 443/80 port instead of 10000 (#396)
Right now Signal Service runs the Let'sEncrypt manager on port 80
and a gRPC server on port 10000. There are two separate listeners.
This PR combines these listeners into one with a cmux lib.
The gRPC server runs on either 443 with TLS or 80 without TLS.
Let's Encrypt manager always runs on port 80.
2022-07-25 19:55:38 +02:00
Misha Bragin
275d364df6 Fix TURN credentials renewal (#394)
Update conn config with new TURN credentials

Updated Signal connection timeout to 5s
2022-07-21 22:07:38 +02:00
Maycon Santos
a3c5fa1307 Add PATH to client Dockerfile (#389)
Useful when SSH to client containers
2022-07-12 15:35:51 +02:00
Maycon Santos
75a69ca26b Write the Admin URL when creating new config (#388) 2022-07-12 15:02:51 +02:00
Misha Bragin
ae8e3ad6fe Enable SSH Login for docker (#385) 2022-07-07 16:33:16 +02:00
Maycon Santos
ff729f6755 Use id command for user lookup on MacOS (#384)
When building client without CGO, user.Lookup
attempts to get user from /etc/passwd
Which doesn't have the user as MacOS uses
opendirectoryd as user directory
2022-07-07 16:13:46 +02:00
Maycon Santos
7e1b20da5d Always initialize status recorder (#383)
Always initialize the status recorder

Utilize proto methods to get pbFullStatus values.
2022-07-07 13:54:47 +02:00
Misha Bragin
d4a3ee9d87 Load user profile when SSH (#380)
This PR fixes issues with the terminal when
running netbird ssh to a remote agent.
Every session looks up a user and loads its
profile. If no user is found, the connection is rejected.
The default user is root.
2022-07-07 11:24:38 +02:00
Maycon Santos
49e9113e0f Enhance status command (#382)
Print peer status from the package

Added --detail flag for detailed status output
2022-07-05 19:47:50 +02:00
Misha Bragin
3bdfa3cc8e Introduce larger retries for the agent (#379)
The Management client will try reconnecting in case.
of network issues or non-permanent errors.
If the device was off-boarded, then the client will stop retrying.
2022-07-02 20:38:16 +02:00
Maycon Santos
8c953c5a2c Add client status collection (#368) 2022-07-02 12:02:17 +02:00
Maycon Santos
e95f0f7acb Support 32 bit (#374)
Add build for 32 bits linux

improved windows test time
2022-07-01 10:42:38 +02:00
Misha Bragin
fa7b413fe7 Fix SSH command on Docker (#377) 2022-06-29 14:03:30 +02:00
Misha Bragin
295f0c755a Add Router nodes feature to the coming soon list 2022-06-27 08:57:06 +03:00
Misha Bragin
a98f6f840a Add Easy SSH to the features list 2022-06-27 08:55:32 +03:00
Misha Bragin
faad5a1e98 Add Easy SSH banner 2022-06-27 08:50:34 +03:00
Maycon Santos
e8caa562b0 Send netmask from account network (#369)
* Send netmask from account network

Added the GetPeerNetwork method to account manager

Pass a copy of the network to the toPeerConfig function
to retrieve the netmask from the network instead of constant

updated methods and added test

* check if the network is the same for 2 peers

* Use expect with BeEquivalentTo
2022-06-24 21:30:51 +02:00
Maycon Santos
1aafc15607 Update self hosting scripts (#367)
split setup.env with example and base

add setup.env to .gitignore to avoid overwrite from new versions

Added test workflow for docker-compose 
and validated configure.sh generated variables
2022-06-24 14:50:14 +02:00
Misha Bragin
06860c4c10 NetBird SSH (#361)
This PR adds support for SSH access through the NetBird network
without managing SSH skeys.
NetBird client app has an embedded SSH server (Linux/Mac only) 
and a netbird ssh command.
2022-06-23 17:04:53 +02:00
Maycon Santos
f883a10535 Rollback dash board image location 2022-06-21 19:01:50 +02:00
Maycon Santos
8ec7f1cd96 Update dashboard docker image 2022-06-21 18:17:38 +02:00
mlsmaycon
aae84e40e2 Update slack invitations link 2022-06-21 11:01:10 +02:00
Misha Bragin
5623735234 Update docs to reflect released access control 2022-06-20 22:34:16 +02:00
Maycon Santos
f9f2d7c7ef Check if new account ID is already being used (#364) 2022-06-20 18:20:43 +02:00
Maycon Santos
35c7cae267 Add homebrew bin path on Apple Silicon (#365)
This was causing issues on new models
2022-06-20 11:34:24 +02:00
Maycon Santos
503a116f7c OpenAPI specification and API Adjusts (#356)
Introduced an OpenAPI specification.
Updated API handlers to use the specification types.

Added patch operation for rules and groups
and methods to the account manager.

HTTP PUT operations require id, fail if not provided.

Use snake_case for HTTP request and response body
2022-06-14 10:32:54 +02:00
Misha Bragin
a454a1aa28 Create account in once place (#358)
There are a few places where an account is created.
When we create a new account, there should be
some defaults set. E.g. created by and group ALL.
It makes sense to add it in one place to avoid inconsistencies.
2022-06-09 13:14:34 +02:00
Misha Bragin
a88ac40b05 Update README to comply with Codacy standards (#360) 2022-06-09 12:09:05 +02:00
Misha Bragin
bfff6110aa Add community projects section 2022-06-09 08:32:41 +02:00
Maycon Santos
f810feafdf Expire device flow info on success (#359)
We should expire the device flow
info soon as we get a token with success.
2022-06-09 02:14:31 +02:00
braginini
57536da245 Go mod tidy 2022-06-08 01:08:48 +02:00
braginini
c9b5328f19 Fix account ALL group creation 2022-06-08 00:30:19 +02:00
Misha Bragin
dab146ed87 Improve Management startup time (#355) 2022-06-06 13:45:59 +02:00
Misha Bragin
b96e616844 Update badges 2022-06-06 12:11:20 +02:00
Misha Bragin
0cba0f81e0 Warmup IDP cache on Management start (#354) 2022-06-06 12:05:44 +02:00
Misha Bragin
ebd70a569c Add caching when querying IDP Manager (#353) 2022-06-05 21:36:42 +02:00
Misha Bragin
e7b43253b0 Fix interface ignore list (#352) 2022-06-05 14:43:13 +02:00
Givi Khojanashvili
d005cd32b0 fix(acl): update each peer's network when rule,group or peer changed (#333)
* fix(acl): update each peer's network when rule,group or peer changed

* fix(ACL): update network test

* fix(acl): cleanup indexes before update them

* fix(acl): clean up rules indexes only for account
2022-06-04 22:02:22 +02:00
Misha Bragin
fa0399d975 Add more interfaces to ignore (#351) 2022-06-04 20:15:41 +02:00
Misha Bragin
e6e9f0322f Handle peer interface config change (#348)
Before this change, NetBird Agent wasn't handling
peer interface configuration changes dynamically.
Also, remote peer configuration changes have
not been applied (e.g. AllowedIPs changed).
Not a very common cause, but still it should be handled.
Now, Agent reacts to PeerConfig changes sent from the
management service and restarts remote connections
if AllowedIps have been changed.
2022-06-04 19:41:01 +02:00
Misha Bragin
60ac8c3268 Handle Network out of range (#347) 2022-06-02 12:56:02 +02:00
Misha Bragin
2e5d4ba6fa Update links in Start using NetBird (#346)
* Update links in Start using NetBird

* Update internals overview and co structure

* Netbird to NetBird
2022-05-31 16:06:34 +02:00
Misha Bragin
0fbe78375e Log whether kernel or userspace WireGuard is used (#345) 2022-05-30 15:52:43 +02:00
Misha Bragin
87631cbc8b Replace IP allocation logic (#342)
The peer IP allocation logic was allocating sequential peer IP from the 100.64.0.0/10 
address block.
Each account is created with a random subnet from 100.64.0.0/10.
The total amount of potential subnets is 64.
The new logic allocates random peer IP
from the account subnet.
This gives us flexibility to add support for
multi subnet accounts without overlapping IPs.
2022-05-29 22:43:39 +02:00
Misha Bragin
ec39202590 Referer README installation steps to docs website (#344) 2022-05-29 22:39:33 +02:00
Maycon Santos
b227a7c34e Add NETBIRD_MGMT_GRPC_API_ENDPOINT support to our scripts (#341) 2022-05-28 20:47:44 +02:00
Maycon Santos
c86bacb5c3 Unblock menu when login (#340)
* GetClientID method and increase interval on slow_down err

* Reuse existing authentication flow if is not expired

Created a new struct to hold additional info
 about the flow

 If there is a waiting sso running, we cancel its context

* Run the up command on a goroutine

* Use time.Until

* Use proper ctx and consistently use goroutine for up/down
2022-05-28 18:37:08 +02:00
Misha Bragin
59a964eed8 Change network mask to limit number of peers to 65k (#339) 2022-05-28 12:54:09 +02:00
Misha Bragin
feff6dc966 Update announcement bar in README 2022-05-28 09:48:51 +02:00
Maycon Santos
258cb3d43b Fix UP calls when state is idle (#338)
* Fix UP calls when state is idle

When we want to login we can call server.Login
It already checks the login status of the peer

* Remove unused status

* Defer close daemon client conn

Co-authored-by: braginini <bangvalo@gmail.com>
2022-05-27 19:16:58 +02:00
Misha Bragin
4088aaf6fe Pass engine context to management and signal clients (#337) 2022-05-27 15:54:51 +02:00
Misha Bragin
1bb504ea78 Fix peer status Connected when removed from the management (#336) 2022-05-27 15:26:36 +02:00
Maycon Santos
594da0a6b8 Display client's version on UI (#335) 2022-05-27 13:56:12 +02:00
Misha Bragin
889fa646fc Fix duplicate output of interactive login (#334) 2022-05-27 13:55:24 +02:00
Misha Bragin
59ae10a66d Replace README gifs (#332) 2022-05-26 15:53:38 +02:00
Maycon Santos
3e4b779d7b Added Netbird as dependency and renamed linux shortcut name (#330) 2022-05-26 15:29:55 +02:00
Misha Bragin
98c764c095 Output message and SSO login URL when netbird up (#331) 2022-05-26 15:26:14 +02:00
Maycon Santos
e5c429af1a Move flags declaration to root (#329)
This allows for mgmtDataDir and mgmtConfig to be initialized properly

use handleMigration function for copying files
2022-05-26 12:55:39 +02:00
Misha Bragin
4b5e6b93a6 Update README reflecting recent changes (#328) 2022-05-26 12:26:14 +02:00
Misha Bragin
2c087cd254 Rename Wiretrustee in logs and be log output friendly on startup (#327) 2022-05-26 10:09:11 +02:00
shatoboar
94fbfcdb85 Versioning of UI and grpc-agent for passing version (#324)
Send Desktop UI client version as user-agent to daemon

This is sent on every login request to the management

Parse the GRPC context on the system package and 
retrieves the user-agent

Management receives the new UIVersion field and 
store in the Peer's system meta
2022-05-25 23:25:02 +02:00
Maycon Santos
5e3eceb0d6 Update MacOS and Windows installers (#325)
Updated windows installer package generation with

launch UI after install
remove older version
remove wiretrustee
added install and uninstall scripts
Updated brew cask:

run installer script to start daemon
Daemon conflicts with wiretrustee on brew

Removed migrate check on non-root commands like status

CLI CMD is now going to stdout
2022-05-25 19:41:03 +02:00
Givi Khojanashvili
65069c1787 feat(ac): add access control middleware (#321) 2022-05-25 18:26:50 +02:00
Misha Bragin
abe78666d4 Store updated system info on Login to Management (#323) 2022-05-23 13:03:57 +02:00
Maycon Santos
5cbfa4bb9e Rebrand client cli (#320) 2022-05-22 18:53:47 +02:00
Misha Bragin
32611e1131 FIx external docs location in README 2022-05-22 14:03:43 +02:00
Maycon Santos
e334e8db53 Renaming project builds and including new Icons (#318)
Added MacOS icons, plist, and cask template file

Adjusted goreleaser with the new name for all builds

Added Icon and update windows-ui build to include it and avoid console

migrated Docker builds to new namespace netbirdio
2022-05-21 18:42:56 +02:00
Misha Bragin
3eb230e1a0 Fix Peer Deletion & HTTP endpoints (#319) 2022-05-21 17:27:04 +02:00
Givi Khojanashvili
3ce3ccc39a Add rules for ACL (#306)
Add rules HTTP endpoint for frontend - CRUD operations.
Add Default rule - allow all.
Send network map to peers based on rules.
2022-05-21 15:21:39 +02:00
Maycon Santos
11a3863c28 update docker hub namespace (#316) 2022-05-20 11:00:15 +02:00
Maycon Santos
3992fe4743 remove extra sign (#315) 2022-05-20 10:53:56 +02:00
Misha Bragin
6ce8a13ffa Update links to docs and blog 2022-05-18 10:33:37 +02:00
Maycon Santos
001cf98dce Update daemon server adminURL and managementURL fields (#314)
Removed the UP call in the login function

Attempt login on change to get status
2022-05-18 00:22:47 +02:00
shatoboar
77e58295e7 Rename wiretrustee-signal to netbird-signal (#313)
* rename wiretrustee-signal to netbird-signal

* Rename Signal repositories and source bin

* Adjust docker-compose with signal volume [skip ci]

Co-authored-by: mlsmaycon <mlsmaycon@gmail.com>
2022-05-13 21:51:41 +02:00
shatoboar
7d893c0238 Rename management from Wiretrustee to Netbird (#311)
Rename documentation and goreleaser build names

Added a migration function for when the old path exists and the new one doesn't

updated the configure.sh to generate the docker-compose with a new path only 
if no pre-existing volume with old name exists
2022-05-13 14:11:21 +02:00
Misha Bragin
b623c255b6 Improve output of a status command (#312) 2022-05-12 21:57:31 +02:00
Maycon Santos
e5c52efb4c Client Login via device authorization flow (#309)
UI and CLI Clients are now able to use SSO login by default

we will check if the management has configured or supports SSO providers

daemon will handle fetching and waiting for an access token

Oauth package was moved to internal to avoid one extra package at this stage

Secrets were removed from OAuth

CLI clients have less and better output

2 new status were introduced, NeedsLogin and FailedLogin for better messaging

With NeedsLogin we no longer have endless login attempts
2022-05-12 11:17:24 +02:00
Maycon Santos
49cca57565 Saving new user to existing account (#310)
Add check if user with
account id metadata belongs to account
2022-05-09 14:30:20 +02:00
Maycon Santos
7e5449fb55 Get Device Authorization Flow information from management (#308)
We will configure the device authorization
flow information and a client will
retrieve it and initiate a
device authorization gran flow
2022-05-08 11:04:57 +02:00
Maycon Santos
fec3132585 Adding peer registration support to JWT (#305)
The management will validate the JWT as it does in the API
 and will register the Peer to the user's account.

New fields were added to grpc messages in management
 and client daemon and its clients were updated

Peer has one new field, UserID, 
that will hold the id of the user that registered it

JWT middleware CheckJWT got a splitter 
and renamed to support validation for non HTTP requests

Added test for adding new Peer with UserID

Lots of tests update because of a new field
2022-05-05 20:02:15 +02:00
Givi Khojanashvili
fbf778a221 fix(client): add checking on empty config in the gRPC handler (#307) 2022-05-05 20:00:28 +02:00
shatoboar
c7e5e5c7c9 Add User HTTP Endpoint to the Management service (#303)
Exposes endpoint under "/users/" that returns information on users.
Calls IDP manager to get information not stored locally (email, name), 
which in the case of the managed version is auth0.
2022-05-05 08:58:34 +02:00
Givi Khojanashvili
219888254e Feat peer groups (#304)
* feat(management): add groups

* squash

* feat(management): add handlers for groups

* feat(management): add handlers for groups

* chore(management): add tests for the get group of the management

* chore(management): add tests for save group
2022-05-03 16:02:51 +02:00
Misha Bragin
70ffc9d625 Make systray connected/disconnected icon switch faster (#299) 2022-04-18 09:43:37 +02:00
Maycon Santos
17fbbbea2a Skip docker login and upload artifacts (#298)
skipping docker login when PR to catch issues earlier

Also, uploading artifacts and keeping then for 3 days
This will help some debug
2022-04-15 18:59:23 +02:00
Misha Bragin
f5933660ba Fix goreleaser linux & windows builds (#297) 2022-04-15 18:19:30 +02:00
Givi Khojanashvili
951e011a9c Add Settings window to Agent UI
Agent systray UI has been extended with
a setting window that allows configuring 
management URL, admin URL and 
supports pre-shared key.
While for the Netbird managed version 
the Settings are not necessary, it helps
to properly configure the self-hosted version.
2022-04-15 17:30:12 +02:00
shatoboar
196207402d Changing back link for Docker (#293)
Fixes issue #292
2022-04-04 21:53:31 +02:00
Maycon Santos
83e743d704 update audience documentation (#291) 2022-04-04 14:22:42 +02:00
Maycon Santos
c3bc85e22d Rename module to netbirdio/netbird (#288)
rename the go module to netbirdio/netbird 
as part of our rebranding.
2022-03-26 12:08:54 +01:00
Maycon Santos
ede2795529 Replace Wiretrustee links and naming (#287)
* Replace Wiretrustee links and naming

* Upper case for Netbrid in README

* Replace logo

* Dashboard URL to app.netbird.io

Co-authored-by: Misha Bragin <bangvalo@gmail.com>
2022-03-26 11:39:27 +01:00
braginini
a0d5a8fb9c Rename systray menu items and add new logo 2022-03-25 15:28:51 +01:00
Givi Khojanashvili
2aaeeac7f6 Fix stop not cleaning up WireGuard interface (#286) 2022-03-25 13:21:04 +01:00
Givi Khojanashvili
a15d52b263 Add UI binary to windows installer (#285)
This PR adds Desktop UI to Windows installer
2022-03-23 18:24:25 +01:00
Maycon Santos
97ab8f4c34 Updating Go 1.18 and Wireguard dependencies (#282) 2022-03-22 14:59:17 +01:00
shatoboar
cf336bd49d Update self-hosting.md (#281)
Describing setup ports mentioned in #278
2022-03-22 14:03:03 +01:00
Maycon Santos
a2fc4ec221 Rotate Access token with refresh token (#280)
Add method for rotating access token with refresh tokens
This will be useful for catching expired sessions and
offboarding users

Also added functions to handle secrets. They have to be revisited
as some tests didn't run on CI as they waited some user input, like password
2022-03-22 13:12:11 +01:00
Misha Bragin
76db9afa11 Push UI client on mac to brew tap (#279) 2022-03-21 09:36:57 +01:00
Givi Khojanashvili
4ef3c7a637 Add basic desktop UI - systray
This PR adds a basic UI for desktop
applications that support Linux, Max
and Windows.
2022-03-20 17:36:35 +01:00
Maycon Santos
bd61be24be Add OAuth Package and Auth0 Client (#273)
Adding package for retrieving an access 
token with a device login flow

For now, we got Auth0 as a client but the 
Interface Client is ready
2022-03-20 08:29:18 +01:00
Maycon Santos
1cd1e84290 Run tests in serial and update multi-peer test (#269)
Updates test workflows with serial execution to avoid collision 
of ports and resource names.

Also, used -exec sudo flag for UNIX tests and removed not-needed
 limits configuration on Linux and added a 5 minutes timeout.

Updated the multi-peer tests in the client/internal/engine_test.go
 to provide proper validation when creating or starting 
a peer engine instance fails.

As some operations of the tests running on windows
 are slow, we will experiment with disabling the Defender before 
restoring cache and checkout a repository, then we reenable 
it to run the tests.

disabled extra logs for windows interface
2022-03-16 11:02:06 +01:00
Mikhail Bragin
957474817f Remove up_test (#268)
up_test is redundant because it is tested in
up_daemon_test

Co-authored-by: mlsmaycon <mlsmaycon@gmail.com>
2022-03-14 17:33:15 +01:00
Maycon Santos
3a69f334e8 Set correct goreleaser version and run on push (#267)
Set the correct goreleaser version with v prefix
and enable run the pipeline on every push
2022-03-14 13:38:06 +01:00
Mikhail Bragin
1660a915e2 Fix goreleaser version to 1.6.3 (#266)
As status command relies on log,
we should always use console as output

Co-authored-by: mlsmaycon <mlsmaycon@gmail.com>
2022-03-14 13:16:16 +01:00
Mikhail Bragin
e3b809a1d4 Update ManagementURL in Config (#262)
If ManagementURL is present in the config file
and cmd (e.g. up or login) specifies a new one,
then update config file with a new ManagementURL
2022-03-13 15:17:18 +01:00
Mikhail Bragin
b2f4322a31 Set local unix socket permissions to rw (#263) 2022-03-13 15:16:35 +01:00
Mikhail Bragin
d7b69b91b9 Fix error when removing peer conn (#264)
When stopping engine, all peer conns have to be closed
and for each peer WireGuard iface is called
to remove WireGuard peer.
This operation happens in a goroutine causing
Engine to remove the whole WireGuard interface before.
Therefore consequent calls to RemovePeer are unsuccessful.
This fix just adds a small delay before removing interface.
2022-03-13 15:16:16 +01:00
Mikhail Bragin
a3a6283ac6 Update windows installer and docs
Update binary installation docs
Add service start in Windows installer
2022-03-11 14:05:44 +01:00
Mikhail Bragin
be0c5c887c Persist Network Serial to Store to avoid outdated netmap sent (#260)
Fix outdated update coming from management
even when it is actually not outdated.
2022-03-10 18:18:38 +01:00
Mikhail Bragin
8cc93e0dbe Init logger for every cmd (#259) 2022-03-10 18:14:07 +01:00
Maycon Santos
24d5f9efac Enable Report Caller when log level Debug (#258)
Enabling report caller when log level equals debug
Also added a Caller prettyfier
to avoid full file path
2022-03-10 14:14:00 +01:00
Maycon Santos
c1b162c974 Improve private domain's behavior tests and logic (#256)
Improved the behavior tests for private domains
and its logic as well because on existing accounts
there was no primary status update
2022-03-10 13:47:36 +01:00
Maycon Santos
612ef98f03 Call start services function for tests (#257)
* Call start services function for tests

when testing CMDs we were using some global
variables which got replaced by parallel test

Now we will call a single function independently
for each test
2022-03-10 11:53:09 +01:00
Maycon Santos
605ca03519 Fix IDP Manager config structs with correct tags (#253)
* Fix IDP Manager config structs with correct tags

When loading the configuration from file
we will use the Auth0ClientConfig and when
sending the post to retrieve a token
 we use the auth0JWTRequest with proper tags

 Also, removed the idle timeout as it was closing
 all idle connections
2022-03-09 17:22:47 +01:00
Maycon Santos
ff62fec956 Handle category change with provided Acc Id (#252)
When account id supplied via claim, we should
handle change of the domain classification.

If category of domain change to private, we
should re-evaluate the private account
2022-03-09 13:31:42 +01:00
braginini
347a668bd5 Fix UP cmd to pass managementURL to daemon 2022-03-08 16:10:44 +01:00
Givi Khojanashvili
ef47385e38 Split client app into cmd and daemon service (#239) 2022-03-08 14:47:55 +01:00
Mikhail Bragin
3e46f38166 Fix README 2022-03-06 21:40:09 +01:00
Mikhail Bragin
64e2e34dae Add Slack badge 2022-03-06 14:16:17 +01:00
Mikhail Bragin
8dd92f14bf Add more badges to README
Codacy and Go report badges
2022-03-06 09:57:07 +01:00
Maycon Santos
071b03e790 Updated self-hosted scripts and documentation (#249)
* Updated self-hosted scripts and documentation

Added more variables to setup.env and
Updated the documentation.

We are now configuring turn server
with template as well.

* Updated self-hosted scripts and documentation

Added more variables to setup.env and
Updated the documentation.

We are now configuring turn server
with template as well.

* Updated self-hosted scripts and documentation

Added more variables to setup.env and
Updated the documentation.

We are now configuring turn server
with template as well.

* Updated self-hosted scripts and documentation

Added more variables to setup.env and
Updated the documentation.

We are now configuring turn server
with template as well.
2022-03-05 11:20:04 +01:00
Maycon Santos
3385ea6379 Update windows sign pipeline version (#248)
With the latest versions of runner 
and updates of dependencies 
a new version was generated.
2022-03-03 10:21:58 +01:00
Mikhail Bragin
430e0415df Remove Wireguard peer in no-proxy mode on Close (#247)
When connection is closed the wireguard peer should be gone.
It wasn't happening until this fix.
2022-03-02 14:50:22 +01:00
Mikhail Bragin
b72ed91cb4 Update self-hosting docs
Running Dashboard, Management, Signal, and Coturn section.
Mentioned in #244
2022-03-02 09:52:09 +01:00
Maycon Santos
0b8387bd2c Group users of same private domain (#243)
* Added Domain Category field and fix store tests

* Add GetAccountByDomain method

* Add Domain Category to authorization claims

* Initial GetAccountWithAuthorizationClaims test cases

* Renamed Private Domain map and index it on saving account

* New Go build tags

* Added NewRegularUser function

* Updated restore to account for primary domain account

Also, added another test case

* Added grouping user of private domains

Also added auxiliary methods for update metadata and domain attributes

* Update http handles get account method and tests

* Fix lint and document another case

* Removed unnecessary log

* Move use cases to method and add flow comments

* Split the new user and existing logic from GetAccountWithAuthorizationClaims

* Review: minor corrections

Co-authored-by: braginini <bangvalo@gmail.com>
2022-03-01 15:22:18 +01:00
Mikhail Bragin
5d4c2643a3 Support no-proxy mode connection mode (#245)
When one of the peers has a static public host IP
or both peers are in the same local network
we establish a direct Wireguard connection
bypassing proxy usage.
This helps reduce FD usage and improves
performance.
2022-03-01 14:07:33 +01:00
Mikhail Bragin
69cda73bbb Update README badges 2022-02-28 16:51:12 +01:00
Maycon Santos
b29948b910 Jwtclaims package (#242)
* Move JWTClaims logic to its own package

* Add extractor tests
2022-02-23 20:02:02 +01:00
shatoboar
5f5cbf7e20 Test mgmt http handler (#240) 2022-02-22 18:18:05 +01:00
shatoboar
41c6af6b6f Extracted AccountManager to interface (#230) 2022-02-22 11:28:19 +01:00
Mikhail Bragin
23fad49756 Update docker pull badge 2022-02-22 11:24:24 +01:00
Maycon Santos
5546eba36a Write to temp file before saving data (#238)
* Create temp file before saving data

On the event of full disk, we may encounter the case where the
destination file get replaced by an empty file as the
ioutil.WriteFile truncates the destination before write.

* Close the tempFile instance before moving it

* Blacklist Wireguard interfaces for ICE checks
2022-02-20 19:03:16 +01:00
braginini
60a9da734f Bump wiretrustee-ice version 2022-02-18 15:06:44 +01:00
Maycon Santos
852c7c50c0 Fix IDPManagement initialization when no config (#234)
* initialize idpmanage only if config was set

* removed LetsEncryption information for local selfhosted deployments
2022-02-17 19:18:46 +01:00
braginini
1c2c1a876b chore: fix selected candidate pair logging 2022-02-17 08:36:37 +01:00
Mikhail Bragin
e5dcd4753e single socket ice (#232)
Enables single socket for HOST and SRFLX candidates by utilizing pion.ice UDPMux
2022-02-16 20:00:21 +01:00
braginini
765d3a0ad0 fix: account JWT claim 2022-02-15 13:38:22 +01:00
braginini
97e4f9a801 chore: add client distfiles to gitignore 2022-02-15 13:01:56 +01:00
shatoboar
d468718d00 fix: go mod tidy (#231) 2022-02-15 12:46:46 +01:00
shatoboar
15e371b592 sends wtversion to dashboard-UI (#229) 2022-02-14 17:51:07 +01:00
Maycon Santos
cd9a418df2 Store domain information (#217)
* extract claim information from JWT

* get account function

* Store domain

* tests missing domain

* update existing account with domain

* add store domain tests
2022-02-11 17:18:18 +01:00
Jim Tittsler
919f0aa3da fix typos (#226) 2022-02-10 19:18:25 +01:00
shatoboar
b59fd50226 Add client version to the client app and send it to the management service (#222)
* test: WIP mocking the grpc server for testing the sending of the client information

* WIP: Test_SystemMetaDataFromClient with mocks, todo:

* fix: failing meta data test

* test: add system meta expectation in management client test

* fix: removing deprecated register function, replacing with new one

* fix: removing deprecated register function from mockclient interface impl

* fix: fixing interface declaration

* chore: remove unused commented code

Co-authored-by: braginini <bangvalo@gmail.com>
2022-02-08 18:03:27 +01:00
Mikhail Bragin
3c959bb178 Login exits on a single attempt to connect to management (#220)
* fix: login exits on a single attempt to connect to management

* chore: add log verbosity for Login operation
2022-02-06 18:56:00 +01:00
shatoboar
efbb5acf63 Add client version to the client app and send it to the management service (#218)
* moved wiretrustee version from main to system.info

* added wiretrustee version for all supported platforms

* typo corrected

* refactor: use single WiretrusteeVersion() func to get version of the client

Co-authored-by: braginini <bangvalo@gmail.com>
2022-02-03 18:35:54 +01:00
shatoboar
b339a9321a fix: reducing github actions (#215) 2022-02-01 11:53:24 +01:00
Maycon Santos
b045865d6e remove demo word from Hosted version (#212) 2022-01-26 14:25:33 +01:00
Mikhail Bragin
8680f16abd Conduct (#205)
* docs: add code of conduct
2022-01-26 09:33:16 +01:00
Maycon Santos
98dc5824ce Rollback stopping management client within engine stop (#204)
* start close handler when using console

* don't close management client within engine stop
2022-01-25 11:18:01 +01:00
Maycon Santos
0739038d51 Fix unstable parallel tests (#202)
* update interface tests and configuration messages

* little debug

* little debug on both errors

* print all devs

* list of devices

* debug func

* handle interface close

* debug socks

* debug socks

* if ports match

* use random assigned ports

* remove unused const

* close management client connection when stopping engine

* GracefulStop when management clients are closed

* enable workflows on PRs too

* remove iface_test debug code
2022-01-25 09:40:28 +01:00
braginini
8ab6eb1cf4 chore: fix lint errors 2022-01-25 08:41:27 +01:00
Steffen Vogel
30625c68a9 Fix detection of wireguard kernel module on non-amd64 archs (#200) 2022-01-24 22:45:52 +01:00
Maycon Santos
fd7282d3cf Link account id with the external user store (#184)
* get account id from access token claim

* use GetOrCreateAccountByUser and add test

* correct account id claim

* remove unused account

* Idp manager interface

* auth0 idp manager

* use if instead of switch case

* remove unnecessary lock

* NewAuth0Manager

* move idpmanager to its own package

* update metadata when accountId is not supplied

* update tests with idpmanager field

* format

* new idp manager and config support

* validate if we fetch the interface before converting to string

* split getJWTToken

* improve tests

* proper json fields and handle defer body close

* fix ci lint notes

* documentation and proper defer position

* UpdateUserAppMetadata tests

* update documentation

* ManagerCredentials interface

* Marshal and Unmarshal functions

* fix tests

* ManagerHelper and ManagerHTTPClient

* further tests with mocking

* rename package and custom http client

* sync local packages

* remove idp suffix
2022-01-24 11:21:30 +01:00
Mikhail Bragin
2ad899b066 Test conn (#199)
* test: add conn tests

* test: add ConnStatus tests

* test: add error test

* test: add more conn tests
2022-01-21 13:52:19 +01:00
braginini
dfa67410b5 chore: update license and AUTHORS 2022-01-19 16:22:40 +01:00
Mikhail Bragin
23f028e65d test: improve engine test (#198) 2022-01-18 17:52:55 +01:00
Mikhail Bragin
5db130a12e Support new Management service protocol (NetworkMap) (#193)
* feature: support new management service protocol

* chore: add more logging to track networkmap serial

* refactor: organize peer update code in engine

* chore: fix lint issues

* refactor: extract Signal client interface

* test: add signal client mock

* refactor: introduce Management Service client interface

* chore: place management and signal clients mocks to respective packages

* test: add Serial test to the engine

* fix: lint issues

* test: unit tests for a networkMapUpdate

* test: unit tests Sync update
2022-01-18 16:44:58 +01:00
Mikhail Bragin
9a3fba3fa3 docs: fix typo 2022-01-17 20:21:52 +01:00
Maycon Santos
0f7ab4354b Fix cicd testing issue (#197)
* sync module

* cache per test os

* different port for tests

* wireguard packages versions
2022-01-17 15:10:18 +01:00
Maycon Santos
64f2d295a8 Refactor Interface package and update windows driver (#192)
* script to generate syso files

* test wireguard-windows driver package

* set int log

* add windows test

* add windows test

* verbose bash

* use cd

* move checkout

* exit 0

* removed tty flag

* artifact path

* fix tags and add cache

* fix cache

* fix cache

* test dir

* restore artifacts in the root

* try dll file

* try dll file

* copy dll

* typo in copy dll

* compile test

* checkout first

* updated cicd

* fix add address issue and gen GUID

* psexec typo

* accept eula

* mod tidy before tests

* regular test exec and verbose test with psexec

* test all

* return WGInterface Interface

* use WgIfaceName and timeout after 30 seconds

* different ports and validate connect 2 peers

* Use time.After for timeout and close interface

* Use time.After for testing connect peers

* WG Interface struct

* Update engine and parse address

* refactor Linux create and assignAddress

* NewWGIface and configuration methods

* Update proxy with interface methods

* update up command test

* resolve lint warnings

* remove psexec test

* close copied files

* add goos before build

* run tests on mac,windows and linux

* cache by testing os

* run on push

* fix indentation

* adjust test timeouts

* remove parallel flag

* mod tidy before test

* ignore syso files

* removed functions and renamed vars

* different IPs for connect peers test

* Generate syso with DLL

* Single Close method

* use port from test constant

* test: remove wireguard interfaces after finishing engine test

* use load_wgnt_from_rsrc

Co-authored-by: braginini <bangvalo@gmail.com>
2022-01-17 14:01:58 +01:00
Mikhail Bragin
afb302d5e7 Change Management Sync protocol to support incremental (serial) network changes (#191)
* feature: introduce NetworkMap to the management protocol with a Serial ID

* test: add Management Sync method protocol test

* test: add Management Sync method NetworkMap field check [FAILING]

* test: add Management Sync method NetworkMap field check [FAILING]

* feature: fill NetworkMap property to when Deleting peer

* feature: fill NetworkMap in the Sync protocol

* test: code review mentions - GeneratePrivateKey() in the test

* fix: wiretrustee client use wireguard GeneratePrivateKey() instead of GenerateKey()

* test: add NetworkMap test

* fix: management_proto test remove store.json on test finish
2022-01-16 17:10:36 +01:00
Mikhail Bragin
9d1ecbbfb2 Management - add serial to Network reflecting network updates (#179)
* chore: [management] - add account serial ID

* Fix concurrency on the client (#183)

* reworked peer connection establishment logic eliminating race conditions and deadlocks while running many peers

* chore: move serial to Network from Account

* feature: increment Network serial ID when adding/removing peers

* chore: extract network struct init to network.go

* chore: add serial test when adding peer to the account

* test: add ModificationID test on AddPeer and DeletePeer
2022-01-14 14:34:27 +01:00
Maycon Santos
bafa71fc2e rollback wireguard go and wgctrl (#185)
* rollback wireguard go and wgctrl

* rollback wireguard go and wgctrl to last working versions
2022-01-12 12:50:56 +01:00
Mikhail Bragin
319632ffe8 Fix concurrency on the client (#183)
* reworked peer connection establishment logic eliminating race conditions and deadlocks while running many peers
2022-01-10 18:43:13 +01:00
braginini
828410b34c chore: [client] - add some randomization to peer conn timeout 2022-01-01 14:03:03 +01:00
Mikhail Bragin
4d2b194570 [Signal] - when peer disconnects registry keeps broken gRPC stream (#178)
* fix: [signal] - when peer disconnects registry keeps broken gRPC stream. The peer is removed on stream closed.

* chore: [signal] - improve logging

* chore: [signal] - improve logging
2021-12-31 19:25:44 +01:00
Mikhail Bragin
a67b9a16af fix peer update concurrency on the client side (#177)
* fix: gRpc Signal and Management connections deadlock on IDLE state

* fix: client peer update concurrency issues
2021-12-31 18:11:33 +01:00
Mikhail Bragin
6ae27c9a9b Refactor: support multiple users under the same account (#170)
* feature: add User entity to Account

* test: new file store creation test

* test: add FileStore persist-restore tests

* test: add GetOrCreateAccountByUser Accountmanager test

* refactor: rename account manager users file

* refactor: use userId instead of accountId when handling Management HTTP API

* fix: new account creation for every request

* fix: golint

* chore: add account creator to Account Entity to identify who created the account.

* chore: use xid ID generator for account IDs

* fix: test failures

* test: check that CreatedBy is stored when account is stored

* chore: add account copy method

* test: remove test for non existent GetOrCreateAccount func

* chore: add accounts conversion function

* fix: golint

* refactor: simplify admin user creation

* refactor: move migration script to a separate package
2021-12-27 13:17:15 +01:00
braginini
ff6e369a21 chore: explain why keeping service lib at specific version 2021-12-21 12:10:18 +01:00
braginini
5c3b5e7f40 fix: rollback kardianos pkg 2021-12-21 12:07:14 +01:00
Mikhail Bragin
8c75ef8bef update to go 1.17 (#167)
* chore: update to go 1.17

* fix: update workflows go version

* fix: golint errors/update grpc
2021-12-21 10:02:25 +01:00
Mikhail Bragin
fdc11fff47 update docs (#164) 2021-12-06 13:54:46 +01:00
Mikhail Bragin
3dca2d6953 Update README.md 2021-11-22 23:11:26 +01:00
Mikhail Bragin
6b7d4cf644 feature: add Wireguard preshared-key support (#160) 2021-11-21 17:47:19 +01:00
Mikhail Bragin
edd4125742 docs: simplify intro 2021-11-20 14:53:57 +01:00
Maycon Santos
7bf9793f85 Support environment vars (#155)
* updage flag values from environment variables

* add log and removing unused constants

* removing unused code

* Docker build client

* fix indentation

* Documentation with docker command

* use docker volume
2021-11-15 09:11:50 +01:00
Maycon Santos
fcbf980588 Stop service before uninstall (#158) 2021-11-14 21:30:18 +01:00
Mikhail Bragin
d08e5efbce fix: too many open files caused by agent not being closed (#154)
* fix: too many open files caused by agent not being closed after unsuccessful attempts to start a peer connection (happens when no network available)

* fix: minor refactor to consider signal status
2021-11-14 19:41:17 +01:00
Maycon Santos
95ef8547f3 Signal management arm builds (#152)
* Add arm builds for Signal and Management services

* adding arm's binary version
2021-11-07 13:11:03 +01:00
Mikhail Bragin
ed1e4dfc51 refactor signal client sync func (#147)
* refactor: move goroutine that runs Signal Client Receive to the engine for better control

* chore: fix comments typo

* test: fix golint

* chore: comments update

* chore: consider connection state=READY in signal and management clients

* chore: fix typos

* test: fix signal ping-pong test

* chore: add wait condition to signal client

* refactor: add stream status to the Signal client

* refactor: defer mutex unlock
2021-11-06 15:00:13 +01:00
braginini
4d34fb4e64 chore: decrease backoff maxinterval to avoid long connection waiting times on the client app 2021-11-02 14:51:29 +01:00
Maycon Santos
1fb8b74cd2 set IF arm6 and empty attribute for package (#146)
There is a behavior or bug in goreleaser where it appends the file name in the target URL and that was causing issues and misconfigured properties
2021-11-01 20:33:26 +01:00
Mikhail Bragin
d040cfed7e fix: client app retry logic (#144)
* fix: retry logic
2021-11-01 09:34:06 +01:00
Maycon Santos
2c729fe5cc remove architecture info from deb (#145) 2021-11-01 09:33:22 +01:00
braginini
e9066b4651 chore: increase signal and management gRPC clients timeouts 2021-10-31 12:14:00 +01:00
Mikhail Bragin
673e807528 chore: set default key expiration if not provided by frontednd (#142) 2021-10-31 12:06:44 +01:00
Mikhail Bragin
892080bc38 docs: update key features 2021-10-27 13:56:55 +02:00
braginini
2d39f6ccae fix: remove ICE port limits 2021-10-27 10:49:03 +02:00
Mikhail Bragin
0b2c26847b fix: return ctx error when UP command exists (#140) 2021-10-26 21:49:05 +02:00
braginini
595ea0d4f8 chore: decrease log verbosity 2021-10-26 10:08:28 +02:00
Maycon Santos
f714868fdd remove arch if and replacement for debian packages (#138) 2021-10-23 10:29:49 +02:00
Mikhail Bragin
81821a1f39 docs: update diagram and Wireguard title (#137)
* docs: update diagram and Wireguard title
2021-10-21 10:06:29 +02:00
mlsmaycon
842b143a48 sync go.sum 2021-10-20 21:57:16 +02:00
Maycon Santos
1323a74db0 fix: avoid failing and extra error messages (#136)
* avoid failing and extra error messages

* avoid extra error messages when executed after pre_remove.sh

* remove extra output and avoid failure on minor errors

* ensure the steps will run only on remove
2021-10-20 11:51:32 +02:00
Mikhail Bragin
74485d3b13 fix: service hanging when error on startup has been encountered (#135) 2021-10-18 13:29:26 +02:00
Mikhail Bragin
bef3b3392b fix: graceful shutdown (#134)
* fix: graceful shutdown

* fix: windows graceful shutdown
2021-10-17 22:15:38 +02:00
Maycon Santos
fcea3c99d4 Enhance up command (#133)
* move setup-key to root command

* up will check login and start service

* update tests to reflect new UP capabilities

* display client IP

* removed unused argument

* install service if not installed

* update post-install and add pre remove script

* improve log messages

* handle service status failures and install service when needed

* removing unused files

* update documentation and description

* add version command

* update service lib version

* using lib constant for not installed services

* match version from goreleaser

* fix: graceful shutdown

* stop only if service is running

* add logs initialization to service controller commands

Co-authored-by: braginini <bangvalo@gmail.com>
2021-10-17 21:34:07 +02:00
Mikhail Bragin
96799a25b5 docs: fix gif size 2021-10-16 16:54:37 +02:00
Mikhail Bragin
07291cdb93 docs: update readme (#132)
* update readme
2021-10-16 16:53:39 +02:00
Mikhail Bragin
21139938c1 docs: highlight Slack channel 2021-10-12 14:37:49 +02:00
Maycon Santos
5cf2d0a6a9 add slack invitation link (#129) 2021-10-12 12:16:09 +02:00
Maycon Santos
8551afe04e enhancement: Support new architectures and auto upload packages to repo (#128)
* adding uploads

* adding uploads

* adding uploads

* adding uploads

* adding uploads

* adding uploads

* use https://pkgs.wiretrustee.com/

* use https://pkgs.wiretrustee.com/

* use https://pkgs.wiretrustee.com/

* set yum id

* secrets for goreleaser uploads

* ensure Github release is enabled
2021-10-12 12:15:45 +02:00
braginini
1685817171 docs: correct installation steps 2021-10-03 18:55:47 +02:00
Mikhail Bragin
e17f662683 docs: update intro (#125)
* docs: update intro
2021-10-03 18:21:41 +02:00
Mikhail Bragin
a764fb870c docs: move intro link up in readme 2021-09-27 09:23:19 +02:00
Mikhail Bragin
cabff941ac docs: add self-hosting video 2021-09-26 16:49:59 +02:00
Mikhail Bragin
b5f35dfb5e docs: replace beta with app.wiretrustee.com (#123)
* docs: replace beta with app.wiretrustee.com

* docs: add Signal port to the list of the open ports

* docs: minor corrections
2021-09-26 11:44:34 +02:00
braginini
1d426b7f81 docs: fix docker-compose management image 2021-09-25 20:17:01 +02:00
Maycon Santos
e4f9406d44 Removed installer and add workflow dispatch (#120) 2021-09-25 19:30:12 +02:00
braginini
7c79ff62ee fix: coturn port 2021-09-25 19:29:43 +02:00
Mikhail Bragin
32c369257b management/support cert from file (#122)
* feature: support cert file in management service

* docs: add new management commands
2021-09-25 19:22:49 +02:00
Mikhail Bragin
08dd719aa1 self-hosting guide (#121)
* docs: first steps of the self-hosting guide

* feature: add setup configurator for the self-hosted guide

* docs: add setup.env comments

* docs: simplify installation steps - support ./configure.sh

* docs: fix file references

* docs: fix minor docs issues

* docs: remove unused title
2021-09-25 19:12:05 +02:00
Mikhail Bragin
84c714dd93 Update quickstart.md 2021-09-23 14:39:55 +02:00
Mikhail Bragin
996c8d7c62 docs: referer to the new video 2021-09-23 14:38:51 +02:00
Mikhail Bragin
25e68ce493 docs: fix broken intro link 2021-09-22 14:18:48 +02:00
Mikhail Bragin
4881dcbd51 docs: add Getting Started hosted version guide (#119)
* docs: add Getting Started hosted version guide

* docs: fix screenshot sizes

* docs: self-hosting section

* docs: increase screenshots width

* docs: reference getting started from main readme

* docs: add refs to sections

* docs: move docs to a separate folder

* docs: add intro

* docs: correct intro docs

* docs: correct image location

* docs: correct language
2021-09-22 14:16:46 +02:00
Mikhail Bragin
d505f70972 Update README.md 2021-09-13 08:50:15 +02:00
Mikhail Bragin
6a80684378 docs: add slack 2021-09-13 08:18:18 +02:00
Mikhail Bragin
2624a7c4e6 docs: update Auth0 notes 2021-09-13 08:06:28 +02:00
Mikhail Bragin
9a412e7bf1 Update README.md 2021-09-13 07:58:52 +02:00
Mikhail Bragin
b5d1690129 Update README.md 2021-09-12 20:38:26 +02:00
Mikhail Bragin
d4bec15ca3 Update README.md 2021-09-12 20:37:55 +02:00
Mikhail Bragin
3212aca7c7 docs: add reference to auth0 react guide 2021-09-12 09:39:03 +03:00
Mikhail Bragin
b97a2251d3 fix docker compose signal volume 2021-09-12 09:08:55 +03:00
braginini
528a26ea3e chore: simplify direct connection logic 2021-09-09 16:43:52 +02:00
Mikhail Bragin
13288374f1 test: fix peer reflexive connectivity (#116) 2021-09-09 10:45:04 +02:00
Mikhail Bragin
ec759bc461 Delete peer (#114)
* feature: add peer deletion

* feature: add peer deletion [CLIENT]

* fix: lint error

* test: fix sync block

* test: fix management test

* feature: add client stop after was deleted

* chore: remove permission denied cancellation

* chore: add larger signal backoff

* feature: notify deleted peer of removal

* fix: lint issue

* chore: add 2nd default key - one off

* test: fix account key check
2021-09-07 18:36:46 +02:00
braginini
a859f6c511 docs: add info about Auth0 2021-09-07 10:38:07 +02:00
braginini
081162864d fix: management config 2021-09-07 10:35:33 +02:00
braginini
090f3ae5d0 chore: rename config to management json 2021-09-07 10:30:56 +02:00
braginini
fb1116e77b chore: release archive back to tar.gz 2021-09-07 10:28:43 +02:00
braginini
879750af7c fix: docker compose 2021-09-07 10:25:55 +02:00
Mikhail Bragin
13b4be31df feature: add logging to a file (#112)
* feature: add logging to a file

* refactor: move InitLog to util lib

* docs: update signal and management docs

* chore: update docker compose

* set --log-file to console

* chore: comment out log volume in docker compose

Co-authored-by: mlsmaycon <mlsmaycon@gmail.com>
2021-09-07 09:53:18 +02:00
Mikhail Bragin
15f7d856db Update README.md 2021-09-06 16:12:57 +02:00
Mikhail Bragin
72197d1970 chore: update docker compose 2021-09-06 16:11:48 +02:00
braginini
a56aba8b06 chore: init STUNs and TURNs as empty arrays 2021-09-06 14:23:03 +02:00
Maycon Santos
4acbdc47e5 Add windows installer documentation (#111) 2021-09-06 14:15:47 +02:00
Maycon Santos
ee3c292699 Add homebrew tap (#110)
* test homebrew task

* secret HOMEBREW_TAP_GITHUB_TOKEN

* prepare for pr

* use homebrew-client

* add brew install
2021-09-06 14:15:08 +02:00
Maycon Santos
6c233fcc3f Add windows installer (#109)
* windows installer

* unpack function in local dir

* working-directory client

* using env var plugin

* test tag and publishing

* getting version from tag

* using version number

* remove unnecessary commands and add description

* using long version outputs

* uncomment docker steps
2021-09-06 10:20:26 +02:00
Mikhail Bragin
a4db0b4e94 client update of TURNs and STUNs (#106)
* feature: update STUNs and TURNs in engine

* fix: setup TURN credentials request only when refresh enabled

* feature: update TURNs and STUNs in teh client app on Management update

* chore: disable peer reflexive candidates in ICE

* chore: relocate management.json

* chore: make TURN secret and pwd plain text in config
2021-09-03 17:47:40 +02:00
Mikhail Bragin
81c5aa1341 docs: add dashboard repo link 2021-09-03 15:33:49 +02:00
Mikhail Bragin
8c5f6186f1 Update README.md 2021-09-03 11:19:53 +02:00
Mikhail Bragin
88e9d2c20d Update README.md 2021-09-03 11:18:45 +02:00
Mikhail Bragin
9d76cf1ea7 docs: add demo location 2021-09-03 11:18:17 +02:00
Mikhail Bragin
737d4b5f2c Update config.json 2021-09-03 11:07:13 +02:00
Mikhail Bragin
4485124b67 fix: docker-compose management config 2021-09-03 11:03:30 +02:00
Mikhail Bragin
b17424d630 Turn credentials generation (#102)
* abstract peer channel

* remove wip code

* refactor NewServer with Peer updates channel

* feature: add TURN credentials manager

* hmac logic

* example test function

* test: add TimeBasedAuthSecretsManager_GenerateCredentials  test

* test: make tests for now with hardcoded secret

* test: add TimeBasedAuthSecretsManager_SetupRefresh test

* test: add TimeBasedAuthSecretsManager_SetupRefresh test

* test: add TimeBasedAuthSecretsManager_CancelRefresh test

* feature: extract TURNConfig to the management config

* feature: return hash based TURN credentials only on initial sync

* feature: make TURN time based secret credentials optional

Co-authored-by: mlsmaycon <mlsmaycon@gmail.com>
2021-09-02 14:41:54 +02:00
braginini
86f3b1e5c8 chore: goreleaser zip format instead of tar.gz 2021-08-30 13:02:39 +02:00
Maycon Santos
a31cbb1f5b abstract peer channel (#101)
* abstract peer channel

* remove wip code

* refactor NewServer with Peer updates channel

* add PeersUpdateManager tests

* adding documentation

* using older version of linter

* verbose lint

* skip cache

* setup go version

* extra output

* configure fetch-depth

* exit 0

* skip-build-cache: true

* disabling failure for lint for now

* fix: darwin issue

* enable lint failure

* remove sock file for macOS

* refactor: remove tests interdependence

* fixed linux native iface

Co-authored-by: braginini <bangvalo@gmail.com>
2021-08-29 17:48:31 +02:00
Maycon Santos
4f4edf8442 fix: management amd64 docker image generation (#104) 2021-08-29 17:43:21 +02:00
braginini
a78e518327 feature: detect peers in local network 2021-08-29 16:12:39 +02:00
braginini
2c1d7c0fd4 fix: rename info package 2021-08-29 13:34:17 +02:00
braginini
593c66fea6 fix: darwin issue 2021-08-29 13:30:34 +02:00
braginini
5f8211773d fix: lint error 2021-08-29 13:23:58 +02:00
braginini
64ca05c8e7 fix: lint error 2021-08-29 13:22:52 +02:00
braginini
307d41c08a Merge remote-tracking branch 'origin/main' 2021-08-27 11:45:55 +02:00
braginini
5c7260298f chore: adjust system info discovery methods 2021-08-27 11:45:46 +02:00
braginini
7f7858b0a6 chore: adjust system info discovery methods 2021-08-27 11:34:38 +02:00
braginini
3c4b0b3a4b fix: remove existing wiretrustee interface if existed 2021-08-26 18:04:19 +02:00
braginini
d4a24ac001 chore: cherrypick hotfix iface 2021-08-26 15:32:05 +02:00
Mikhail Bragin
737866c149 docs: update README (#96)
* docs: update README
2021-08-25 15:40:51 +02:00
braginini
49800a6d03 fix: minor HTTP bugs 2021-08-25 14:16:17 +02:00
Mikhail Bragin
0fa15e6920 Add peer meta data (#95)
* feature: add peer GET and DELETE API methods

* refactor: extract peer business logic to a separate file

* refactor: extract peer business logic to a separate file

* feature: add peer update HTTP endpoint

* chore: fill peer new fields

* merge with main

* refactor: HTTP methods according to standards

* feature: add peer system metadata

* feature: add peer status

* test: fix removal
2021-08-24 11:50:19 +02:00
Mikhail Bragin
95845c88fe Extend peer http endpoint (#94)
* feature: add peer GET and DELETE API methods

* refactor: extract peer business logic to a separate file

* refactor: extract peer business logic to a separate file

* feature: add peer update HTTP endpoint

* chore: fill peer new fields

* merge with main

* refactor: HTTP methods according to standards

* chore: setup keys POST endpoint without ID
2021-08-23 21:43:05 +02:00
Mikhail Bragin
6869b48905 feature: increase key usage after successful peer registration (#93) 2021-08-22 11:29:25 +02:00
braginini
90ef1e939b fix: test 2021-08-20 23:12:39 +02:00
braginini
bff137b109 fix: test 2021-08-20 22:37:58 +02:00
braginini
2e9fc20567 feature: add update setup key endpoint 2021-08-20 22:33:43 +02:00
braginini
617f79e2e0 Merge branch 'main' of github.com:wiretrustee/wiretrustee into extend-setupkey-http-api 2021-08-20 19:50:16 +02:00
Mikhail Bragin
d75353fbb8 extend setup key logic (#91)
* feature: extend setup key logic

* fix: login test

* test: change test data to support new setup key fields

* test: add setup key tests

* test: add setup key UUID format validation

* test: add AddAccount test

* test: add more AccountManager tests

* fix: golint errors

* test: increase client interface up timout
2021-08-20 19:48:42 +02:00
braginini
b5a20bf1ba chore: remove unused static dashboard files 2021-08-20 16:25:51 +02:00
braginini
695148410f test: increase client interface up timout 2021-08-20 15:55:28 +02:00
braginini
07ab9c196d fix: golint errors 2021-08-20 15:51:29 +02:00
braginini
4a5901ada1 test: add more AccountManager tests 2021-08-20 15:44:18 +02:00
braginini
4c427ae900 test: add AddAccount test 2021-08-20 15:18:29 +02:00
braginini
22fdb0a029 test: add setup key UUID format validation 2021-08-20 15:08:07 +02:00
braginini
1b056ab75a test: add setup key tests 2021-08-20 15:01:57 +02:00
braginini
3a41014adb Merge branch 'main' into setup-key-http-api 2021-08-20 14:18:25 +02:00
braginini
708835afa8 test: change test data to support new setup key fields 2021-08-20 13:40:07 +02:00
braginini
8364e03944 Merge remote-tracking branch 'origin/main' 2021-08-20 13:29:46 +02:00
braginini
0017360b8d fix: up test 2021-08-20 13:29:29 +02:00
Mikhail Bragin
9ace93d9fc Update README.md 2021-08-20 13:24:54 +02:00
Mikhail Bragin
b127e424f9 add release note to README 2021-08-20 13:23:57 +02:00
braginini
34cffb3bf0 fix: login test 2021-08-19 21:17:39 +02:00
braginini
02cc6a30f5 feature: extend setup key logic 2021-08-19 21:12:21 +02:00
braginini
c68d9dff4a chore: setup key upper case 2021-08-19 20:07:12 +02:00
Mikhail Bragin
1dfa99d07c add wiretrustee LOGIN command (#90)
* feature: add wiretrustee LOGIN command

* chore: add management initial connection timeout

* test: add login cmd test

* test: validate generated config in login cmd

* test: add up command test

* chore: add timeout to signal client creation method

* test: close wireguard interface once test finished
2021-08-18 13:35:42 +02:00
braginini
f7e51e7453 refactor: use logrus in service_controller 2021-08-17 09:14:21 +02:00
braginini
2c6748610c fix: service command not running up 2021-08-17 09:13:25 +02:00
Mikhail Bragin
e8ca289f4a test: add management client tests (#89)
* test: add management client tests

* test: add management client Sync test

* fix: engine test

* test: remove engine tests

* test: return engine tests [check]

* test: remove engine tests [check]
2021-08-16 23:30:51 +02:00
braginini
2a97053cae fix: windows build 2021-08-16 15:23:09 +02:00
Maycon Santos
38e3c9c062 Add cors headers (#85)
* disable EnableAuthOnOptions

* Setup basic cors headers

* feature: user cors lib
2021-08-16 11:29:57 +02:00
Mikhail Bragin
877ad97a96 Peer management login (#83)
* feature: replace RegisterPeer with Login method that does both - registration and login

* test: add management login test

* feature: add WiretrusteeConfig to the Login response to configure peer global config

* feature: add client peer login support

* fix: missing parts

* chore: update go deps

* feature: support Management Service gRPC endpoints [CLIENT]

* feature: finalize client sync with management

* fix: management store peer key lower case restore

* fix: management returns peer ip without a mask

* refactor: remove cmd pkg

* fix: invalid tun interface name on mac

* fix: timeout when calling management client

* fix: tests and lint errors

* fix: golang-test workflow

* fix: client service tests

* fix: iface build

* feature: detect management scheme on startup

* chore: better logs for management

* fix: goreleaser

* fix: lint errors

* fix: signal TLS

* fix: direct Wireguard connection

* chore: verbose logging on direct connection
2021-08-15 16:56:26 +02:00
Maycon Santos
80de6a75d5 Self contained signal cmd build (#82)
* Moved Signal CMD to Signal directory

* Removed config dir and fixed a parameter typo

* removed attempt to create ssl directory

* Update Signal build configuration

* move Signal documentation to its directory

* removed unused variables

* test build management and signal

* User run as subcommand to execute the signal daemon
2021-08-13 08:46:30 +02:00
braginini
dcc9dcacdc feature: add setup key HTTP api response fields 2021-08-12 13:24:06 +02:00
Mikhail Bragin
3c47a3c408 peer management HTTP API (#81)
* feature: create account for a newly registered user

* feature: finalize user auth flow

* feature: create protected API with JWT

* chore: cleanup http server

* feature: add UI assets

* chore: update react UI

* refactor: move account not exists -> create to AccountManager

* chore: update UI

* chore: return only peers on peers endpoint

* chore: add UI path to the config

* chore: remove ui from management

* chore: remove unused Docker comamnds

* docs: update management config sample

* fix: store creation

* feature: introduce peer response to the HTTP api

* fix: lint errors

* feature: add setup-keys HTTP endpoint

* fix: return empty json arrays in HTTP API

* feature: add new peer response fields
2021-08-12 12:49:10 +02:00
Maycon Santos
d5af5f1878 Refactor: Move Signal server and client (#80)
* Move Signal Server

* Move Signal Client

* Cleanup duplicates and unused files

* Moved Signal client tests
2021-08-09 19:21:48 +02:00
braginini
9f0c86c28e refactor: move grpc and http APIs to separate packages 2021-08-07 13:51:17 +02:00
braginini
08d44b1d5f refactor: move LetsEncryptDomain to HttpServer config 2021-08-07 13:35:52 +02:00
Mikhail Bragin
1f29975737 feature: basic auth0 support (#78)
* feature: basic auth0 support

* refactor: improve auth flow

* refactor: extract HttpServer config

* feature: merge HTTP API layer with Let's Encrypt
2021-08-07 12:26:07 +02:00
andpar83
11982d6dde Add client's interaction with management service (#71)
* Add client's interaction with management service

* Getting updates

* Fixed key and nil ptr

* Added setupKey param

* Added managment address parameter

* Fixed test

* feature: use RemotePeers from the management server instead of deprecated Peers

* merge: merge changes from main
2021-08-01 19:06:01 +02:00
Maycon Santos
6ce5b2c815 Support Signal server with TLS (#76)
* tlsEnabled flag and DialOption

* Update signal client invocations
2021-08-01 12:54:35 +02:00
Maycon Santos
ea99def502 Update mgmt binary name and config doc (#75)
* using wiretrustee-mgmt for binary name

* using wiretrustee-mgmt

* updated documentation and compose files to use config.json
2021-07-31 12:33:04 +02:00
Maycon Santos
f51a79d3b3 Mgmt docker and document (#72)
* debug image and use wiretrustee/management repository

* Update documentation and docker-compose to include management

* improve documentation and add debug image build

* update docker-compose section with management service notes.

* fix broken doc link
2021-07-31 10:29:49 +02:00
Mikhail Bragin
2c2c1e19df Peer configuration management (#69)
* feature: add config properties to the SyncResponse of the management gRpc service

* fix: lint errors

* chore: modify management protocol according to the review notes

* fix: management proto fields sequence

* feature: add proper peer configuration to be synced

* chore: minor changes

* feature: finalize peer config management

* fix: lint errors

* feature: add management server config file

* refactor: extract hosts-config to a separate file

* refactor: review notes applied to correct file_store usage

* refactor: extract management service configuration to a file

* refactor: simplify management config
2021-07-30 17:46:38 +02:00
Maycon Santos
c0c4c4a266 build wiretrustee management binaries (#68) 2021-07-25 18:06:18 +02:00
Mikhail Bragin
3b30beb567 add config properties to the SyncResponse of the management gRpc service (#66)
* feature: add config properties to the SyncResponse of the management gRpc service
2021-07-25 17:08:16 +02:00
andpar83
9e4aa4f1f1 Move management server to a separate directory (#67)
* Move management server to a separate directory
2021-07-24 16:14:29 +02:00
braginini
83ac774264 test: fix management multiple concurrent peers test 2021-07-22 15:53:15 +02:00
Mikhail Bragin
2172d6f1b9 Extract common server encryption logic (#65)
* refactor: extract common message encryption logic
* refactor: move letsencrypt logic to common
* refactor: rename common package to encryption
* test: add encryption tests
2021-07-22 15:23:24 +02:00
braginini
c98be683bf docs: add management service docs 2021-07-22 12:32:04 +02:00
Mikhail Bragin
079d35eada Extend Management to support peer changes distribution (#55)
* feature: add peer sync and a server public key endpoints
* test: add Management.Sync() gRpc endpoint test
* feat: implement peer sync
* docs: added some comments to the Management server
* chore: use for loop over channel when monitoring peer updates
* fix: exit infinite loop when sending updates to peers
* test: add multiple concurrent peers test for management service
* chore: remove unused test
* fix: reduce the amount peers for a concurrent peer update test

Co-authored-by: braginini <m.bragin@wiretrustee.com>
2021-07-22 10:28:00 +02:00
Mikhail Bragin
d27eb317aa update signal gRpc, enable TLS and add keepalive params (#62)
* chore: update signal gRpc
* chore: add Signal keep alive params and policy
* feature: add signal TLS support
* refactor: move signal Dockerfile to the corresponding folder
Co-authored-by: braginini <m.bragin@wiretrustee.com>
2021-07-21 20:23:11 +02:00
braginini
940578d600 chore: use latest golang-grpc libs 2021-07-20 18:09:26 +02:00
Maycon Santos
1a8c03bef0 feature: Support live peer list update (#51)
* created InitializePeer and ClosePeerConnection functions

* feature: simplify peer stopping

* chore: remove unused code

* feature: basic management service implementation (#44)

* feat: basic management service implementation [FAILING TESTS]

* test: fix healthcheck test

* test: #39 add peer registration endpoint test

* feat: #39 add setup key handling

* feat: #39 add peer management store persistence

* refactor: extract config read/write to the utility package

* refactor: move file contents copy to the utility package

* refactor: use Accounts instead of Users in the Store

* feature: add management server Docker file

* refactor: introduce datadir instead of config

* chore: use filepath.Join to concat filepaths instead of string concat

* refactor: move stop channel to the root

* refactor: move stop channel to the root

* review: fix PR review notes

Co-authored-by: braginini <hello@wiretrustee.com>

* Handle read config file errors

* feature: add letsencrypt support to the management service

* fix: lint warnings

* chore: change default datadir

* refactor: set default flags in code not Dockerfile

* chore: remove unused code

* Added RemovePeer and centralized configureDevice code

* remove peer from the wg interface when closing proxy

* remove config file

* add iface tests

* fix tests, validate if file exists before removing it

* removed unused functions UpdateListenPort and ConfigureWithKeyGen

* Ensure we don't wait for timeout when closing

* Rename ClosePeerConnection to RemovePeerConnection

* Avoid returning on uapi Accept failures

* Added engine tests

* Remove extra add address code

* Adding iface.Close

* Ensure Close the interface and disable parallel test execution

* check err var when listing interfaces

* chore: add synchronisation to peer management

* chore: add connection status to track peer connection

* refactor: remove unused code

Co-authored-by: braginini <hello@wiretrustee.com>
Co-authored-by: Mikhail Bragin <bangvalo@gmail.com>
2021-07-19 15:02:11 +02:00
braginini
4e17890597 docs: minor FilesStore corrections 2021-07-18 21:00:32 +02:00
andpar83
7b52049333 Improve addition of new peers in Management service. (#56)
* Store refactoring
* Improve addition of new peers in Management service.
2021-07-18 20:51:09 +02:00
Mikhail Bragin
f9c3ed784f Merge pull request #52 from wiretrustee/tls-peer-management
feature: add letsencrypt support to the management service
2021-07-18 10:17:13 +02:00
braginini
ea524e2a53 chore: remove unused code 2021-07-17 17:42:00 +02:00
Mikhail Bragin
bffea0e145 Merge pull request #53 from wiretrustee/handle-read-config-file-errors
Handle read config file errors
2021-07-17 17:30:07 +02:00
braginini
2d85fcfcc3 refactor: set default flags in code not Dockerfile 2021-07-17 17:26:51 +02:00
braginini
07118d972d chore: change default datadir 2021-07-17 15:47:16 +02:00
braginini
84f4d51c6c fix: lint warnings 2021-07-17 15:46:25 +02:00
mlsmaycon
1e250fc0df Handle read config file errors 2021-07-17 14:58:02 +02:00
braginini
d4a9f4d38a feature: add letsencrypt support to the management service 2021-07-17 14:51:16 +02:00
Mikhail Bragin
4587f7686e feature: basic management service implementation (#44)
* feat: basic management service implementation [FAILING TESTS]

* test: fix healthcheck test

* test: #39 add peer registration endpoint test

* feat: #39 add setup key handling

* feat: #39 add peer management store persistence

* refactor: extract config read/write to the utility package

* refactor: move file contents copy to the utility package

* refactor: use Accounts instead of Users in the Store

* feature: add management server Docker file

* refactor: introduce datadir instead of config

* chore: use filepath.Join to concat filepaths instead of string concat

* refactor: move stop channel to the root

* refactor: move stop channel to the root

* review: fix PR review notes

Co-authored-by: braginini <hello@wiretrustee.com>
2021-07-17 14:38:59 +02:00
Mikhail Bragin
dd50f495ab docs: add Wireguard trademark statement 2021-06-29 12:50:58 +03:00
Mikhail Bragin
bb2477491f Merge pull request #37 from wiretrustee/add-service-command
feature: Adding service command
2021-06-27 16:50:27 +02:00
mlsmaycon
f4d7faaf4e debug port value 2021-06-25 11:49:16 +02:00
mlsmaycon
cffb08ad23 Use go bin 2021-06-25 11:23:13 +02:00
mlsmaycon
8d05789749 preserve env GOROOT 2021-06-25 11:18:34 +02:00
mlsmaycon
ca5970140f set config path to avoid ci/cd limitations 2021-06-25 11:08:16 +02:00
mlsmaycon
ac628b6efa use sudo for testing service installation 2021-06-25 11:01:21 +02:00
mlsmaycon
80665049dc fixed Init Execution 2021-06-25 10:59:10 +02:00
mlsmaycon
881f078759 Removed engine.Stop 2021-06-25 10:58:42 +02:00
mlsmaycon
1cf9b143e0 update go.mod with service command dependecies 2021-06-25 10:40:47 +02:00
mlsmaycon
158547f3eb rebase 2021-06-25 10:39:56 +02:00
mlsmaycon
ab6452065d Updated documentation for Powershell as admin 2021-06-25 10:28:50 +02:00
mlsmaycon
e553c5e97e Avoid prompt admin at every execution 2021-06-25 10:28:27 +02:00
Mikhail Bragin
3041ff4ef7 Merge pull request #36 from wiretrustee/avoid-proxy-when-local-net
feature: initial implementation of avoiding local proxy if peers are …
2021-06-25 07:15:37 +02:00
mlsmaycon
61a7f3013b Rename Name function 2021-06-24 23:16:09 +02:00
braginini
dac865c61f chore: add log to detect a usage of the Wireguard kernel module 2021-06-24 12:49:14 +02:00
braginini
a40669270a refactor: rearrange iface package 2021-06-24 11:46:33 +02:00
braginini
f2ca2fc7c1 refactort: extract method to create Wireguard interface using kernel module 2021-06-24 11:02:40 +02:00
braginini
729b16e599 fix: windows iface build 2021-06-24 10:59:41 +02:00
braginini
561bd681d9 fix: golint errors 2021-06-24 10:55:05 +02:00
braginini
0e313eec24 fix: mod.go build only for linux 2021-06-23 16:16:48 +02:00
braginini
4216cd2986 feature: add feature to determine when to run wireguard userspace implementation or native one (linux) 2021-06-23 16:11:54 +02:00
mlsmaycon
c18899d135 Add windows documentation 2021-06-23 01:38:44 +02:00
mlsmaycon
20248dadb7 Merge remote-tracking branch 'origin/add-service-command' into add-service-command 2021-06-23 01:07:05 +02:00
mlsmaycon
1a06518f1b Update resource file with requireAdministrator, added resources.rc and manifests.xml 2021-06-23 01:06:47 +02:00
braginini
dd72a01ecf feature: add check of Wireguard kernel module existence (Linux only) 2021-06-22 14:38:28 +02:00
braginini
bbfbf797d5 chore: remove os.Exit - unnecessary call 2021-06-22 12:11:51 +02:00
mlsmaycon
52db303104 Add service command tests 2021-06-22 01:17:30 +02:00
mlsmaycon
5122294adf golint: properly handle defer engine stop 2021-06-22 01:17:01 +02:00
mlsmaycon
a87f828844 Adjust service command outputs to use cmd Print functions 2021-06-22 01:07:12 +02:00
braginini
8088c7a591 feature: add stop handling for engine 2021-06-21 11:18:03 +02:00
mlsmaycon
74355a2292 fix windows default config path 2021-06-20 23:33:49 +02:00
mlsmaycon
a66cdccda9 Add service controllers and installers commands 2021-06-20 23:01:44 +02:00
mlsmaycon
06c7af058b Create config dir if using default configPath 2021-06-20 23:01:12 +02:00
mlsmaycon
41b50a08d4 feature: Adding service run command 2021-06-19 15:09:32 +02:00
Mikhail Bragin
3c45da553a Merge pull request #28 from wiretrustee/test-signal-grpc
test: add basic signal IT tests
2021-06-19 14:51:35 +03:00
braginini
8dfccfc800 refactor: remove unused code 2021-06-18 13:22:56 +02:00
braginini
021092800b fix: extract constants from iface to iface_configuration 2021-06-18 13:10:32 +02:00
mlsmaycon
aa854c5899 add linux native wg interface 2021-06-18 13:01:43 +02:00
braginini
e41fdedd5b feature: enable ice mDNS 2021-06-17 21:31:53 +02:00
braginini
923cabda9a feature: initial implementation of avoiding local proxy if peers are in the same net 2021-06-17 14:27:33 +02:00
braginini
db673ed34f fix: #35 peer Registration Race when client connects to the signal server 2021-06-17 11:12:35 +02:00
Maycon Santos
6465e2556a Merge pull request #34 from stv0g/remove-dead-code
Remove dead code
2021-06-15 20:54:43 +02:00
Steffen Vogel
89dba7951a remove unused function 2021-06-15 20:50:59 +02:00
braginini
9308a51800 refactor: rename SignalExchangeServer to Server to comply with good practices 2021-06-15 19:02:46 +02:00
braginini
94c0091a7b test: add message exchange test timeout 2021-06-15 18:58:47 +02:00
braginini
f247f9a2f8 chore: fix golint error 2021-06-15 16:31:45 +02:00
braginini
c49bd23ac5 chore: fix golint error 2021-06-15 16:20:39 +02:00
braginini
11174a50cd 6Merge branch 'test-signal-grpc' of github.com:wiretrustee/wiretrustee into test-signal-grpc 2021-06-15 16:13:49 +02:00
braginini
dfcf9f9087 test: add messages exchange between peers [SIGNAL] 2021-06-15 16:13:27 +02:00
braginini
5f8a489f90 test: add basic signal IT tests 2021-06-15 16:13:27 +02:00
braginini
9b9c7ada7d test: add messages exchange between peers [SIGNAL] 2021-06-15 16:08:46 +02:00
Mikhail Bragin
8b31088968 Merge pull request #29 from wiretrustee/fix-mac-build
Fix mac build
2021-06-15 14:42:39 +03:00
Steffen Vogel
00f2ee34a0 remove dead code 2021-06-15 11:03:43 +02:00
Maycon Santos
51337fbf65 Merge pull request #31 from stv0g/fix-typo
fix typo in directory name
2021-06-15 09:56:14 +02:00
Steffen Vogel
ca83e8c4a0 fix typo in directory name 2021-06-15 09:31:25 +02:00
Maycon Santos
2784f6a098 Merge pull request #30 from andpar83/signal-doc
Fix Signal doc styling
2021-06-15 08:52:17 +02:00
Andrey Parfenov
6b5010f7d5 Fix Signal doc styling 2021-06-14 20:08:06 -07:00
mlsmaycon
714c4c3c44 use darwin 2021-06-15 00:13:52 +02:00
mlsmaycon
d5c4f6cb40 fix matrix var to use os 2021-06-15 00:08:54 +02:00
mlsmaycon
7df6cde968 fix a typo and rename the embedded dll 2021-06-15 00:02:42 +02:00
mlsmaycon
744984861b Add build to the test 2021-06-15 00:00:55 +02:00
braginini
83fe84d11a test: add basic signal IT tests 2021-06-14 16:57:18 +02:00
Mikhail Bragin
e059059e62 Merge pull request #27 from wiretrustee/synchronize-peer-registry
chore: [Signal] synchronize peer registry
2021-06-11 19:01:04 +03:00
braginini
06b0c46a5d chore: [Signal] synchronize peer registry 2021-06-10 17:08:40 +02:00
Maycon Santos
8acddfd510 Merge pull request #26 from wiretrustee/add-windows-support
Add initial Windows support
2021-06-07 10:00:34 +02:00
mlsmaycon
caf2229d3b renamed uapiConn and lint errors 2021-06-07 00:35:17 +02:00
mlsmaycon
698ebe2287 Removed elevate for now 2021-06-06 23:59:19 +02:00
mlsmaycon
54235f0a77 build windows 2021-06-06 23:57:16 +02:00
mlsmaycon
05168ae12f Add wintun.dll in form of system object file 2021-06-06 21:59:38 +02:00
mlsmaycon
255ad7faa9 Split create Interface based on OS with elevate 2021-06-06 21:51:56 +02:00
mlsmaycon
6e4c232ff2 Split create Interface based on OS 2021-06-06 15:48:57 +02:00
mlsmaycon
59360519d6 Add windows support and update wireguard-go deps 2021-06-06 00:40:44 +02:00
Mikhail Bragin
3520b6471b Merge pull request #23 from wiretrustee/test-signal-encryption
test: add signal encryption test
2021-06-04 10:12:00 +03:00
braginini
74061597a3 fix: test workflow trigger 2021-06-03 12:35:31 +02:00
braginini
33a98c7a2c test: add signal peer test 2021-06-03 12:23:18 +02:00
braginini
9b327ea6ba test: add signal encryption test 2021-06-03 11:39:19 +02:00
Mikhail Bragin
45697a0000 docs: fix roadmap links 2021-06-02 21:31:37 +02:00
Mikhail Bragin
884cd8dc55 docs: add Product Roadmap 2021-06-02 21:30:19 +02:00
Mikhail Bragin
f8eaf2f40e Merge pull request #11 from wiretrustee/infra-files
Adding example infrastructure files and MACOS guide
2021-05-30 12:37:31 +02:00
Mikhail Bragin
0609e1d75d chore: minor readme fixes 2021-05-30 12:35:18 +02:00
mlsmaycon
8c9bc96c85 multiple typos 2021-05-30 10:29:30 +03:00
mlsmaycon
68112870dc Updated doc for docker-compose examples and macos configuration 2021-05-30 10:26:49 +03:00
mlsmaycon
ae69f4cf1b remove server-name for the example 2021-05-30 10:25:25 +03:00
mlsmaycon
c8ad10d653 Adding example docker compose for signal and coturn 2021-05-25 11:32:25 +05:00
Mikhail Bragin
e622b2a529 Merge pull request #10 from wiretrustee/lint-warns
fix doc and lint warns
2021-05-19 11:18:33 +02:00
braginini
44d5e7f205 fix: golint errors (part 3) 2021-05-19 11:17:15 +02:00
braginini
790858c31b fix: golint errors (part 2) 2021-05-19 11:13:25 +02:00
braginini
5342f10e7f fix: golint errors 2021-05-19 10:58:21 +02:00
braginini
f0048d16fb Merge remote-tracking branch 'origin/main' into lint-warns
# Conflicts:
#	connection/engine.go
2021-05-19 10:45:44 +02:00
braginini
635cd2202d Merge branch 'main' of github.com:wiretrustee/wiretrustee into main 2021-05-16 18:06:13 +02:00
braginini
a773ec8150 feat: add interface black list to avoid undesired interfaces 2021-05-16 18:05:08 +02:00
mlsmaycon
84c6eb5e16 Add golangci-lint workflow 2021-05-15 15:44:35 +05:00
mlsmaycon
73720951d7 fix doc and lint warns for the cmd package 2021-05-15 15:33:07 +05:00
mlsmaycon
6d339295be fix doc 2021-05-15 15:24:30 +05:00
mlsmaycon
f1cff0e13a fix doc and lint warns for connection package 2021-05-15 15:23:56 +05:00
mlsmaycon
e6358e7bb2 fix doc and lint warns for signal package 2021-05-15 15:20:49 +05:00
mlsmaycon
2337c3d84d fix doc and lint warns for iface package 2021-05-15 15:05:15 +05:00
Mikhail Bragin
9554247d70 Merge pull request #9 from wiretrustee/using-docker-hub
Using docker hub
2021-05-15 09:06:55 +02:00
mlsmaycon
a9d3ce227a updated doc with docker hub registry 2021-05-15 11:58:59 +05:00
mlsmaycon
33e9ecfcb9 using docker hub 2021-05-15 11:58:31 +05:00
Mikhail Bragin
7446ef857b docs: fix TURN note 2021-05-12 20:02:07 +02:00
Maycon Santos
8adf92a957 Merge pull request #8 from wiretrustee/license
License
2021-05-12 12:36:38 +05:00
Maycon Santos
d47d14a7d3 add end of line 2021-05-11 22:40:09 +05:00
braginini
47933bcbfa license: correct license text 2021-05-11 14:38:41 +02:00
braginini
7080309842 license: add BSD license text and authors 2021-05-11 14:33:02 +02:00
381 changed files with 64446 additions and 2558 deletions

View File

@@ -0,0 +1,30 @@
---
name: Bug/Issue report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the problem**
A clear and concise description of what the problem is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**NetBird status -d output:**
If applicable, add the output of the `netbird status -d` command
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

11
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,11 @@
## Describe your changes
## Issue ticket number and link
### Checklist
- [ ] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] Extended the README / documentation, if necessary

View File

@@ -0,0 +1,32 @@
name: Test Code Darwin
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: macos-latest
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.19.x
- name: Checkout code
uses: actions/checkout@v2
- name: Cache Go modules
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: macos-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
macos-go-
- name: Install modules
run: go mod tidy
- name: Test
run: go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...

92
.github/workflows/golang-test-linux.yml vendored Normal file
View File

@@ -0,0 +1,92 @@
name: Test Code Linux
on:
push:
branches:
- main
pull_request:
jobs:
test:
strategy:
matrix:
arch: ['386','amd64']
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.19.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 libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib
- name: Install modules
run: go mod tidy
- name: Test
run: CGO_ENABLED=1 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.19.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 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
- 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

View File

@@ -0,0 +1,52 @@
name: Test Code Windows
on:
push:
branches:
- main
pull_request:
jobs:
pre:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- run: bash -x wireguard_nt.sh
working-directory: client
- uses: actions/upload-artifact@v2
with:
name: syso
path: client/*.syso
retention-days: 1
test:
needs: pre
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.19.x
- uses: actions/cache@v2
with:
path: |
%LocalAppData%\go-build
~\go\pkg\mod
~\AppData\Local\go-build
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- uses: actions/download-artifact@v2
with:
name: syso
path: iface\
- name: Test
run: go test -tags=load_wgnt_from_rsrc -timeout 5m -p 1 ./...

18
.github/workflows/golangci-lint.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: golangci-lint
on: [pull_request]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.19.x
- name: Install dependencies
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
args: --timeout=6m

View File

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

View File

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

View File

@@ -4,6 +4,13 @@ on:
push:
tags:
- 'v*'
branches:
- main
pull_request:
env:
SIGN_PIPE_VER: "v0.0.5"
GORELEASER_VER: "v1.14.1"
jobs:
release:
@@ -14,11 +21,15 @@ jobs:
uses: actions/checkout@v2
with:
fetch-depth: 0 # It is required for GoReleaser to work properly
- name: Generate syso with DLL
run: bash -x wireguard_nt.sh
working-directory: client
-
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
go-version: 1.19
-
name: Cache Go modules
uses: actions/cache@v1
@@ -30,6 +41,9 @@ jobs:
-
name: Install modules
run: go mod tidy
-
name: check git status
run: git --no-pager diff --exit-code
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
@@ -37,17 +51,147 @@ jobs:
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to GitHub Packages Docker Registry
name: Login to Docker hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ secrets.GITHUB_TOKEN }}
password: ${{ secrets.GITHUB_TOKEN }}
username: netbirdio
password: ${{ secrets.DOCKER_TOKEN }}
- name: Install OS build dependencies
run: sudo apt update && sudo apt install -y -q gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
version: latest
version: ${{ env.GORELEASER_VER }}
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
-
name: upload non tags for debug purposes
uses: actions/upload-artifact@v2
with:
name: release
path: dist/
retention-days: 3
release_ui:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0 # It is required for GoReleaser to work properly
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
- name: Cache Go modules
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-ui-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-ui-go-
- name: Install modules
run: go mod tidy
- name: check git status
run: git --no-pager diff --exit-code
- name: Install dependencies
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-mingw-w64-x86-64
- name: Install rsrc
run: go install github.com/akavel/rsrc@v0.10.2
- name: Generate windows rsrc
run: rsrc -arch amd64 -ico client/ui/netbird.ico -manifest client/ui/manifest.xml -o client/ui/resources_windows_amd64.syso
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
version: ${{ env.GORELEASER_VER }}
args: release --config .goreleaser_ui.yaml --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
- name: upload non tags for debug purposes
uses: actions/upload-artifact@v2
with:
name: release-ui
path: dist/
retention-days: 3
release_ui_darwin:
runs-on: macos-11
steps:
-
name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0 # It is required for GoReleaser to work properly
-
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
-
name: Cache Go modules
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-ui-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-ui-go-
-
name: Install modules
run: go mod tidy
-
name: Run GoReleaser
id: goreleaser
uses: goreleaser/goreleaser-action@v2
with:
version: ${{ env.GORELEASER_VER }}
args: release --config .goreleaser_ui_darwin.yaml --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
name: upload non tags for debug purposes
uses: actions/upload-artifact@v2
with:
name: release-ui-darwin
path: dist/
retention-days: 3
trigger_windows_signer:
runs-on: ubuntu-latest
needs: [release,release_ui]
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Trigger Windows binaries sign pipeline
uses: benc-uk/workflow-dispatch@v1
with:
workflow: Sign windows bin and installer
repo: netbirdio/sign-pipelines
ref: ${{ env.SIGN_PIPE_VER }}
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref }}" }'
trigger_darwin_signer:
runs-on: ubuntu-latest
needs: release_ui_darwin
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Trigger Darwin App binaries sign pipeline
uses: benc-uk/workflow-dispatch@v1
with:
workflow: Sign darwin ui app with dispatch
repo: netbirdio/sign-pipelines
ref: ${{ env.SIGN_PIPE_VER }}
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref }}" }'

View File

@@ -0,0 +1,92 @@
name: Test Docker Compose Linux
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Install jq
run: sudo apt-get install -y jq
- name: Install curl
run: sudo apt-get install -y curl
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.19.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: cp setup.env
run: cp infrastructure_files/tests/setup.env infrastructure_files/
- name: run configure
working-directory: infrastructure_files
run: bash -x configure.sh
env:
CI_NETBIRD_DOMAIN: localhost
CI_NETBIRD_AUTH_CLIENT_ID: testing.client.id
CI_NETBIRD_AUTH_AUDIENCE: testing.ci
CI_NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT: https://example.eu.auth0.com/.well-known/openid-configuration
CI_NETBIRD_USE_AUTH0: true
- name: check values
working-directory: infrastructure_files
env:
CI_NETBIRD_DOMAIN: localhost
CI_NETBIRD_AUTH_CLIENT_ID: testing.client.id
CI_NETBIRD_AUTH_AUDIENCE: testing.ci
CI_NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT: https://example.eu.auth0.com/.well-known/openid-configuration
CI_NETBIRD_USE_AUTH0: true
CI_NETBIRD_AUTH_SUPPORTED_SCOPES: "openid profile email offline_access api email_verified"
CI_NETBIRD_AUTH_AUTHORITY: https://example.eu.auth0.com/
CI_NETBIRD_AUTH_JWT_CERTS: https://example.eu.auth0.com/.well-known/jwks.json
CI_NETBIRD_AUTH_TOKEN_ENDPOINT: https://example.eu.auth0.com/oauth/token
CI_NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT: https://example.eu.auth0.com/oauth/device/code
CI_NETBIRD_AUTH_REDIRECT_URI: "/peers"
CI_NETBIRD_TOKEN_SOURCE: "idToken"
CI_NETBIRD_AUTH_USER_ID_CLAIM: "email"
CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE: "super"
run: |
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_AUDIENCE docker-compose.yml | grep $CI_NETBIRD_AUTH_AUDIENCE
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 NETBIRD_MGMT_API_ENDPOINT docker-compose.yml | grep "$CI_NETBIRD_DOMAIN: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=$'
grep LETSENCRYPT_DOMAIN docker-compose.yml | egrep 'LETSENCRYPT_DOMAIN=$'
grep NETBIRD_TOKEN_SOURCE docker-compose.yml | grep $CI_NETBIRD_TOKEN_SOURCE
grep AuthUserIDClaim management.json | grep $CI_NETBIRD_AUTH_USER_ID_CLAIM
grep -A 1 ProviderConfig management.json | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE
- name: run docker compose up
working-directory: infrastructure_files
run: |
docker-compose up -d
sleep 5
docker-compose ps
docker-compose logs --tail=20
- name: test running containers
run: |
count=$(docker compose ps --format json | jq '.[] | select(.Project | contains("infrastructure_files")) | .State' | grep -c running)
test $count -eq 4
working-directory: infrastructure_files

14
.gitignore vendored
View File

@@ -1,2 +1,14 @@
.idea
*.iml
.run
*.iml
dist/
bin/
.env
conf.json
http-cmds.sh
infrastructure_files/management.json
infrastructure_files/docker-compose.yml
*.syso
client/.distfiles/
infrastructure_files/setup.env
.vscode

View File

@@ -1,39 +1,110 @@
project_name: wiretrustee
project_name: netbird
builds:
- env: [CGO_ENABLED=0]
- id: netbird
dir: client
binary: netbird
env: [CGO_ENABLED=0]
goos:
- linux
- darwin
- windows
goarch:
- arm
- amd64
- arm64
- mips
- 386
gomips:
- hardfloat
- softfloat
ignore:
- goos: darwin
- goos: windows
goarch: arm64
- goos: windows
goarch: arm
- goos: windows
goarch: 386
ldflags:
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
mod_timestamp: '{{ .CommitTimestamp }}'
tags:
- load_wgnt_from_rsrc
- id: netbird-mgmt
dir: management
env:
- CGO_ENABLED=1
- >-
{{- if eq .Runtime.Goos "linux" }}
{{- if eq .Arch "arm64"}}CC=aarch64-linux-gnu-gcc{{- end }}
{{- if eq .Arch "arm"}}CC=arm-linux-gnueabihf-gcc{{- end }}
{{- end }}
binary: netbird-mgmt
goos:
- linux
goarch:
- amd64
- arm64
- arm
ldflags:
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
mod_timestamp: '{{ .CommitTimestamp }}'
- id: netbird-signal
dir: signal
env: [CGO_ENABLED=0]
binary: netbird-signal
goos:
- linux
goarch:
- amd64
- arm64
- arm
ldflags:
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
mod_timestamp: '{{ .CommitTimestamp }}'
archives:
- builds:
- netbird
nfpms:
- maintainer: Wiretrustee <wiretrustee@wiretrustee.com>
description: Wiretrustee project.
homepage: https://wiretrustee.com/
- maintainer: Netbird <dev@netbird.io>
description: Netbird client.
homepage: https://netbird.io/
id: netbird-deb
bindir: /usr/bin
builds:
- netbird
formats:
- deb
- rpm
contents:
- src: release_files/wiretrustee.service
dst: /lib/systemd/system/wiretrustee.service
- src: release_files/wiretrustee.json
dst: /etc/wiretrustee/wiretrustee.json
type: "config|noreplace"
scripts:
postinstall: "release_files/post_install.sh"
preremove: "release_files/pre_remove.sh"
- maintainer: Netbird <dev@netbird.io>
description: Netbird client.
homepage: https://netbird.io/
id: netbird-rpm
bindir: /usr/bin
builds:
- netbird
formats:
- rpm
scripts:
postinstall: "release_files/post_install.sh"
preremove: "release_files/pre_remove.sh"
dockers:
- image_templates:
- ghcr.io/wiretrustee/wiretrustee:signal-{{ .Version }}-amd64
- netbirdio/netbird:{{ .Version }}-amd64
ids:
- netbird
goarch: amd64
use_buildx: true
dockerfile: Dockerfile
use: buildx
dockerfile: client/Dockerfile
build_flag_templates:
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.created={{.Date}}"
@@ -41,12 +112,14 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- "--label=maintainer=dev@netbird.io"
- image_templates:
- ghcr.io/wiretrustee/wiretrustee:signal-{{ .Version }}-arm64v8
- netbirdio/netbird:{{ .Version }}-arm64v8
ids:
- netbird
goarch: arm64
use_buildx: true
dockerfile: Dockerfile
use: buildx
dockerfile: client/Dockerfile
build_flag_templates:
- "--platform=linux/arm64"
- "--label=org.opencontainers.image.created={{.Date}}"
@@ -54,15 +127,236 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=wiretrustee@wiretrustee.com"
- "--label=maintainer=dev@netbird.io"
- image_templates:
- netbirdio/netbird:{{ .Version }}-arm
ids:
- netbird
goarch: arm
goarm: 6
use: buildx
dockerfile: client/Dockerfile
build_flag_templates:
- "--platform=linux/arm"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- image_templates:
- netbirdio/signal:{{ .Version }}-amd64
ids:
- netbird-signal
goarch: amd64
use: buildx
dockerfile: signal/Dockerfile
build_flag_templates:
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- image_templates:
- netbirdio/signal:{{ .Version }}-arm64v8
ids:
- netbird-signal
goarch: arm64
use: buildx
dockerfile: signal/Dockerfile
build_flag_templates:
- "--platform=linux/arm64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- image_templates:
- netbirdio/signal:{{ .Version }}-arm
ids:
- netbird-signal
goarch: arm
goarm: 6
use: buildx
dockerfile: signal/Dockerfile
build_flag_templates:
- "--platform=linux/arm"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- image_templates:
- netbirdio/management:{{ .Version }}-amd64
ids:
- netbird-mgmt
goarch: amd64
use: buildx
dockerfile: management/Dockerfile
build_flag_templates:
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- image_templates:
- netbirdio/management:{{ .Version }}-arm64v8
ids:
- netbird-mgmt
goarch: arm64
use: buildx
dockerfile: management/Dockerfile
build_flag_templates:
- "--platform=linux/arm64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- image_templates:
- netbirdio/management:{{ .Version }}-arm
ids:
- netbird-mgmt
goarch: arm
goarm: 6
use: buildx
dockerfile: management/Dockerfile
build_flag_templates:
- "--platform=linux/arm"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- image_templates:
- netbirdio/management:{{ .Version }}-debug-amd64
ids:
- netbird-mgmt
goarch: amd64
use: buildx
dockerfile: management/Dockerfile.debug
build_flag_templates:
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- image_templates:
- netbirdio/management:{{ .Version }}-debug-arm64v8
ids:
- netbird-mgmt
goarch: arm64
use: buildx
dockerfile: management/Dockerfile.debug
build_flag_templates:
- "--platform=linux/arm64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- image_templates:
- netbirdio/management:{{ .Version }}-debug-arm
ids:
- netbird-mgmt
goarch: arm
goarm: 6
use: buildx
dockerfile: management/Dockerfile.debug
build_flag_templates:
- "--platform=linux/arm"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
docker_manifests:
- name_template: ghcr.io/wiretrustee/wiretrustee:signal-{{ .Version }}
- name_template: netbirdio/netbird:{{ .Version }}
image_templates:
- ghcr.io/wiretrustee/wiretrustee:signal-{{ .Version }}-arm64v8
- ghcr.io/wiretrustee/wiretrustee:signal-{{ .Version }}-amd64
- netbirdio/netbird:{{ .Version }}-arm64v8
- netbirdio/netbird:{{ .Version }}-arm
- netbirdio/netbird:{{ .Version }}-amd64
- name_template: ghcr.io/wiretrustee/wiretrustee:signal-latest
- name_template: netbirdio/netbird:latest
image_templates:
- ghcr.io/wiretrustee/wiretrustee:signal-{{ .Version }}-arm64v8
- ghcr.io/wiretrustee/wiretrustee:signal-{{ .Version }}-amd64
- netbirdio/netbird:{{ .Version }}-arm64v8
- netbirdio/netbird:{{ .Version }}-arm
- netbirdio/netbird:{{ .Version }}-amd64
- name_template: netbirdio/signal:{{ .Version }}
image_templates:
- netbirdio/signal:{{ .Version }}-arm64v8
- netbirdio/signal:{{ .Version }}-arm
- netbirdio/signal:{{ .Version }}-amd64
- name_template: netbirdio/signal:latest
image_templates:
- netbirdio/signal:{{ .Version }}-arm64v8
- netbirdio/signal:{{ .Version }}-arm
- netbirdio/signal:{{ .Version }}-amd64
- name_template: netbirdio/management:{{ .Version }}
image_templates:
- netbirdio/management:{{ .Version }}-arm64v8
- netbirdio/management:{{ .Version }}-arm
- netbirdio/management:{{ .Version }}-amd64
- name_template: netbirdio/management:latest
image_templates:
- netbirdio/management:{{ .Version }}-arm64v8
- netbirdio/management:{{ .Version }}-arm
- netbirdio/management:{{ .Version }}-amd64
- name_template: netbirdio/management:debug-latest
image_templates:
- netbirdio/management:{{ .Version }}-debug-arm64v8
- netbirdio/management:{{ .Version }}-debug-arm
- netbirdio/management:{{ .Version }}-debug-amd64
brews:
-
ids:
- default
tap:
owner: netbirdio
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
commit_author:
name: Netbird
email: dev@netbird.io
description: Netbird project.
download_strategy: CurlDownloadStrategy
homepage: https://netbird.io/
license: "BSD3"
test: |
system "#{bin}/{{ .ProjectName }} version"
uploads:
- name: debian
ids:
- netbird-deb
mode: archive
target: https://pkgs.wiretrustee.com/debian/pool/{{ .ArtifactName }};deb.distribution=stable;deb.component=main;deb.architecture={{ if .Arm }}armhf{{ else }}{{ .Arch }}{{ end }};deb.package=
username: dev@wiretrustee.com
method: PUT
- name: yum
ids:
- netbird-rpm
mode: archive
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
username: dev@wiretrustee.com
method: PUT

98
.goreleaser_ui.yaml Normal file
View File

@@ -0,0 +1,98 @@
project_name: netbird-ui
builds:
- id: netbird-ui
dir: client/ui
binary: netbird-ui
env:
- CGO_ENABLED=1
goos:
- linux
goarch:
- amd64
ldflags:
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
mod_timestamp: '{{ .CommitTimestamp }}'
- id: netbird-ui-windows
dir: client/ui
binary: netbird-ui
env:
- CGO_ENABLED=1
- CC=x86_64-w64-mingw32-gcc
goos:
- windows
goarch:
- amd64
ldflags:
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
- -H windowsgui
mod_timestamp: '{{ .CommitTimestamp }}'
archives:
- id: linux-arch
name_template: "{{ .ProjectName }}-linux_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
builds:
- netbird-ui
- id: windows-arch
name_template: "{{ .ProjectName }}-windows_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
builds:
- netbird-ui-windows
nfpms:
- maintainer: Netbird <dev@netbird.io>
description: Netbird client UI.
homepage: https://netbird.io/
id: netbird-ui-deb
package_name: netbird-ui
builds:
- netbird-ui
formats:
- deb
contents:
- src: client/ui/netbird.desktop
dst: /usr/share/applications/netbird.desktop
- src: client/ui/disconnected.png
dst: /usr/share/pixmaps/netbird.png
dependencies:
- libayatana-appindicator3-1
- libgtk-3-dev
- libappindicator3-dev
- netbird
- maintainer: Netbird <dev@netbird.io>
description: Netbird client UI.
homepage: https://netbird.io/
id: netbird-ui-rpm
package_name: netbird-ui
builds:
- netbird-ui
formats:
- rpm
contents:
- src: client/ui/netbird.desktop
dst: /usr/share/applications/netbird.desktop
- src: client/ui/disconnected.png
dst: /usr/share/pixmaps/netbird.png
dependencies:
- libayatana-appindicator3-1
- libgtk-3-dev
- libappindicator3-dev
- netbird
uploads:
- name: debian
ids:
- netbird-ui-deb
mode: archive
target: https://pkgs.wiretrustee.com/debian/pool/{{ .ArtifactName }};deb.distribution=stable;deb.component=main;deb.architecture={{ if .Arm }}armhf{{ else }}{{ .Arch }}{{ end }};deb.package=
username: dev@wiretrustee.com
method: PUT
- name: yum
ids:
- netbird-ui-rpm
mode: archive
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
username: dev@wiretrustee.com
method: PUT

View File

@@ -0,0 +1,29 @@
project_name: netbird-ui
builds:
- id: netbird-ui-darwin
dir: client/ui
binary: netbird-ui
env: [CGO_ENABLED=1]
goos:
- darwin
goarch:
- amd64
- arm64
gomips:
- hardfloat
- softfloat
ldflags:
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
mod_timestamp: '{{ .CommitTimestamp }}'
tags:
- load_wgnt_from_rsrc
archives:
- builds:
- netbird-ui-darwin
checksum:
name_template: "{{ .ProjectName }}_darwin_checksums.txt"
changelog:
skip: true

3
AUTHORS Normal file
View File

@@ -0,0 +1,3 @@
Mikhail Bragin (https://github.com/braginini)
Maycon Santos (https://github.com/mlsmaycon)
Wiretrustee UG (haftungsbeschränkt)

132
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,132 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
community@netbird.io.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

218
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,218 @@
# Contributing to NetBird
Thanks for your interest in contributing to NetBird.
There are many ways that you can contribute:
- Reporting issues
- Updating documentation
- Sharing use cases in slack or Reddit
- Bug fix or feature enhancement
If you haven't already, join our slack workspace [here](https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A), we would love to discuss topics that need community contribution and enhancements to existing features.
## Contents
- [Contributing to NetBird](#contributing-to-netbird)
- [Contents](#contents)
- [Code of conduct](#code-of-conduct)
- [Directory structure](#directory-structure)
- [Development setup](#development-setup)
- [Requirements](#requirements)
- [Local NetBird setup](#local-netbird-setup)
- [Build and start](#build-and-start)
- [Test suite](#test-suite)
- [Checklist before submitting a PR](#checklist-before-submitting-a-pr)
- [Other project repositories](#other-project-repositories)
- [Checklist before submitting a new node](#checklist-before-submitting-a-new-node)
- [Contributor License Agreement](#contributor-license-agreement)
## Code of conduct
This project and everyone participating in it are governed by the Code of
Conduct which can be found in the file [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report
unacceptable behavior to community@netbird.io.
## Directory structure
The NetBird project monorepo is organized to maintain most of its individual dependencies code within their directories, except for a few auxiliary or shared packages.
The most important directories are:
- [/.github](/.github) - Github actions workflow files and issue templates
- [/client](/client) - NetBird agent code
- [/client/cmd](/client/cmd) - NetBird agent cli code
- [/client/internal](/client/internal) - NetBird agent business logic code
- [/client/proto](/client/proto) - NetBird agent daemon GRPC proto files
- [/client/server](/client/server) - NetBird agent daemon code for background execution
- [/client/ui](/client/ui) - NetBird agent UI code
- [/encryption](/encryption) - Contain main encryption code for agent communication
- [/iface](/iface) - Wireguard® interface code
- [/infrastructure_files](/infrastructure_files) - Getting started files containing docker and template scripts
- [/management](/management) - Management service code
- [/management/client](/management/client) - Management service client code which is imported by the agent code
- [/management/proto](/management/proto) - Management service GRPC proto files
- [/management/server](/management/server) - Management service server code
- [/management/server/http](/management/server/http) - Management service REST API code
- [/management/server/idp](/management/server/idp) - Management service IDP management code
- [/release_files](/release_files) - Files that goes into release packages
- [/signal](/signal) - Signal service code
- [/signal/client](/signal/client) - Signal service client code which is imported by the agent code
- [/signal/peer](/signal/peer) - Signal service peer message logic
- [/signal/proto](/signal/proto) - Signal service GRPC proto files
- [/signal/server](/signal/server) - Signal service server code
## Development setup
If you want to contribute to bug fixes or improve existing features, you have to ensure that all needed
dependencies are installed. Here is a short guide on how that can be done.
### Requirements
#### Go 1.19
Follow the installation guide from https://go.dev/
#### UI client - Fyne toolkit
We use the fyne toolkit in our UI client. You can follow its requirement guide to have all its dependencies installed: https://developer.fyne.io/started/#prerequisites
#### gRPC
You can follow the instructions from the quickstarter guide https://grpc.io/docs/languages/go/quickstart/#prerequisites and then run the `generate.sh` files located in each `proto` directory to generate changes.
> **IMPORTANT**: We are very open to contributions that can improve the client daemon protocol. For Signal and Management protocols, please reach out on slack or via github issues with your proposals.
#### Docker
Follow the installation guide from https://docs.docker.com/get-docker/
#### Goreleaser and golangci-lint
We utilize two tools in our Github actions workflows:
- Goreleaser: Used for release packaging. You can follow the installation steps [here](https://goreleaser.com/install/); keep in mind to match the version defined in [release.yml](/.github/workflows/release.yml)
- golangci-lint: Used for linting checks. You can follow the installation steps [here](https://golangci-lint.run/usage/install/); keep in mind to match the version defined in [golangci-lint.yml](/.github/workflows/golangci-lint.yml)
They can be executed from the repository root before every push or PR:
**Goreleaser**
```shell
goreleaser --snapshot --rm-dist
```
**golangci-lint**
```shell
golangci-lint run
```
### Local NetBird setup
> **IMPORTANT**: All the steps below have to get executed at least once to get the development setup up and running!
Now that everything NetBird requires to run is installed, the actual NetBird code can be
checked out and set up:
1. [Fork](https://guides.github.com/activities/forking/#fork) the NetBird repository
2. Clone your forked repository
```
git clone https://github.com/<your_github_username>/netbird.git
```
3. Go into the repository folder
```
cd netbird
```
4. Add the original NetBird repository as `upstream` to your forked repository
```
git remote add upstream https://github.com/netbirdio/netbird.git
```
5. Install all Go dependencies:
```
go mod tidy
```
### Build and start
#### Client
> Windows clients have a Wireguard driver requirement. We provide a bash script that can be executed in WLS 2 with docker support [wireguard_nt.sh](/client/wireguard_nt.sh).
To start NetBird, execute:
```
cd client
# bash wireguard_nt.sh # if windows
go build .
```
To start NetBird the client in the foreground:
```
sudo ./client up --log-level debug --log-file console
```
> On Windows use a powershell with administrator privileges
#### Signal service
To start NetBird's signal, execute:
```
cd signal
go build .
```
To start NetBird the signal service:
```
./signal run --log-level debug --log-file console
```
#### Management service
> You may need to generate a configuration file for management. Follow steps 2 to 5 from our [self-hosting guide](https://netbird.io/docs/getting-started/self-hosting).
To start NetBird's management, execute:
```
cd management
go build .
```
To start NetBird the management service:
```
./management management --log-level debug --log-file console --config ./management.json
```
### Test suite
The tests can be started via:
```
cd netbird
go test -exec sudo ./...
```
> On Windows use a powershell with administrator privileges
## Checklist before submitting a PR
As a critical network service and open-source project, we must enforce a few things before submitting the pull-requests:
- Keep functions as simple as possible, with a single purpose
- Use private functions and constants where possible
- Comment on any new public functions
- Add unit tests for any new public function
> When pushing fixes to the PR comments, please push as separate commits; we will squash the PR before merging, so there is no need to squash it before pushing it, and we are more than okay with 10-100 commits in a single PR. This helps review the fixes to the requested changes.
## Other project repositories
NetBird project is composed of 3 main repositories:
- NetBird: This repository, which contains the code for the agents and control plane services.
- Dashboard: https://github.com/netbirdio/dashboard, contains the Administration UI for the management service
- Documentations: https://github.com/netbirdio/docs, contains the documentation from https://netbird.io/docs
## Contributor License Agreement
That we do not have any potential problems later it is sadly necessary to sign a [Contributor License Agreement](CONTRIBUTOR_LICENSE_AGREEMENT.md). That can be done literally with the push of a button.
A bot will automatically comment on the pull request once it got opened asking for the agreement to be signed. Before it did not get signed it is sadly not possible to merge it in.

View File

@@ -0,0 +1,148 @@
# Contributor License Agreement
We are incredibly thankful for the contributions we receive from the community.
We require our external contributors to sign a Contributor License Agreement ("CLA") in
order to ensure that our projects remain licensed under Free and Open Source licenses such
as BSD-3 while allowing Wiretrustee to build a sustainable business.
Wiretrustee is committed to having a true Open Source Software ("OSS") license for
our software. A CLA enables Wiretrustee to safely commercialize our products
while keeping a standard OSS license with all the rights that license grants to users: the
ability to use the project in their own projects or businesses, to republish modified
source, or to completely fork the project.
This page gives a human-friendly summary of our CLA, details on why we require a CLA, how
contributors can sign our CLA, and more. You may view the full legal CLA document (below).
# Human-friendly summary
This is a human-readable summary of (and not a substitute for) the full agreement (below).
This highlights only some of key terms of the CLA. It has no legal value and you should
carefully review all the terms of the actual CLA before agreeing.
<li>Grant of copyright license. You give Wiretrustee permission to use your copyrighted work
in commercial products.
</li>
<li>Grant of patent license. If your contributed work uses a patent, you give Wiretrustee a
license to use that patent including within commercial products. You also agree that you
have permission to grant this license.
</li>
<li>No Warranty or Support Obligations.
By making a contribution, you are not obligating yourself to provide support for the
contribution, and you are not taking on any warranty obligations or providing any
assurances about how it will perform.
</li>
The CLA does not change the terms of the standard open source license used by our software
such as BSD-3 or MIT.
You are still free to use our projects within your own projects or businesses, republish
modified source, and more.
Please reference the appropriate license for the project you're contributing to to learn
more.
# Why require a CLA?
Agreeing to a CLA explicitly states that you are entitled to provide a contribution, that you cannot withdraw permission
to use your contribution at a later date, and that Wiretrustee has permission to use your contribution in our commercial
products.
This removes any ambiguities or uncertainties caused by not having a CLA and allows users and customers to confidently
adopt our projects. At the same time, the CLA ensures that all contributions to our open source projects are licensed
under the project's respective open source license, such as BSD-3.
Requiring a CLA is a common and well-accepted practice in open source. Major open source projects require CLAs such as
Apache Software Foundation projects, Facebook projects (such as React), Google projects (including Go), Python, Django,
and more. Each of these projects remains licensed under permissive OSS licenses such as MIT, Apache, BSD, and more.
# Signing the CLA
Open a pull request ("PR") to any of our open source projects to sign the CLA. A bot will comment on the PR asking you
to sign the CLA if you haven't already.
Follow the steps given by the bot to sign the CLA. This will require you to log in with GitHub (we only request public
information from your account) and to fill in a few additional details such as your name and email address. We will only
use this information for CLA tracking; none of your submitted information will be used for marketing purposes.
You only have to sign the CLA once. Once you've signed the CLA, future contributions to any Wiretrustee project will not
require you to sign again.
# Legal Terms and Agreement
In order to clarify the intellectual property license granted with Contributions from any person or entity, Wiretrustee
UG (haftungsbeschränkt) ("Wiretrustee") must have a Contributor License Agreement ("CLA") on file that has been signed
by each Contributor, indicating agreement to the license terms below. This license does not change your rights to use
your own Contributions for any other purpose.
You accept and agree to the following terms and conditions for Your present and future Contributions submitted to
Wiretrustee. Except for the license granted herein to Wiretrustee and recipients of software distributed by Wiretrustee,
You reserve all right, title, and interest in and to Your Contributions.
1. Definitions.
```
"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner
that is making this Agreement with Wiretrustee. For legal entities, the entity making a Contribution and all other
entities that control, are controlled by, or are under common control with that entity are considered
to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect,
to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty
percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
```
```
"Contribution" shall mean any original work of authorship, including any modifications or additions to
an existing work, that is or previously has been intentionally submitted by You to Wiretrustee for inclusion in,
or documentation of, any of the products owned or managed by Wiretrustee (the "Work").
For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication
sent to Wiretrustee or its representatives, including but not limited to communication on electronic mailing lists,
source code control systems, and issue tracking systems that are managed by, or on behalf of,
Wiretrustee for the purpose of discussing and improving the Work, but excluding communication that is conspicuously
marked or otherwise designated in writing by You as "Not a Contribution."
```
2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Wiretrustee
and to recipients of software distributed by Wiretrustee a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly
perform, sublicense, and distribute Your Contributions and such derivative works.
3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Wiretrustee and
to recipients of software distributed by Wiretrustee a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import,
and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are
necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which
such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (
including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have
contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity
under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to
intellectual property that you create that includes your Contributions, you represent that you have received
permission to make Contributions on behalf of that employer, that you will have received permission from your current
and future employers for all future Contributions, that your applicable employer has waived such rights for all of
your current and future Contributions to Wiretrustee, or that your employer has executed a separate Corporate CLA
with Wiretrustee.
5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of
others). You represent that Your Contribution submissions include complete details of any third-party license or
other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware
and which are associated with any part of Your Contributions.
6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support.
You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in
writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT,
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
7. Should You wish to submit work that is not Your original creation, You may submit it to Wiretrustee separately from
any Contribution, identifying the complete details of its source and of any license or other restriction (including,
but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and
conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
8. You agree to notify Wiretrustee of any facts or circumstances of which you become aware that would make these
representations inaccurate in any respect.

View File

@@ -1,5 +0,0 @@
FROM gcr.io/distroless/base:debug
EXPOSE 10000
ENTRYPOINT [ "/go/bin/wiretrustee","signal" ]
CMD ["--log-level","DEBUG"]
COPY wiretrustee /go/bin/wiretrustee

13
LICENSE Normal file
View File

@@ -0,0 +1,13 @@
BSD 3-Clause License
Copyright (c) 2022 Wiretrustee UG (haftungsbeschränkt) & AUTHORS
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

171
README.md
View File

@@ -1,77 +1,114 @@
# Wiretrustee
<p align="center">
<strong>:hatching_chick: New Release! Peer expiration.</strong>
<a href="https://github.com/netbirdio/netbird/releases">
Learn more
</a>
</p>
<br/>
<div align="center">
<p align="center">
<img width="234" src="docs/media/logo-full.png"/>
</p>
<p>
<a href="https://github.com/netbirdio/netbird/blob/main/LICENSE">
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
</a>
<a href="https://www.codacy.com/gh/netbirdio/netbird/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=netbirdio/netbird&amp;utm_campaign=Badge_Grade"><img src="https://app.codacy.com/project/badge/Grade/e3013d046aec44cdb7462c8673b00976"/></a>
<br>
<a href="https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">
<img src="https://img.shields.io/badge/slack-@netbird-red.svg?logo=slack"/>
</a>
</p>
</div>
A WireGuard®-based mesh network that connects your devices into a single private network.
### Why using Wiretrustee?
<p align="center">
<strong>
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/>
Join our <a href="https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">Slack channel</a>
<br/>
</strong>
</p>
* Connect multiple devices to each other via a secure peer-to-peer Wireguard VPN tunnel. At home, the office, or anywhere else.
* No need to open ports and expose public IPs on the device.
* Automatically reconnects in case of network failures or switches.
* Automatic NAT traversal.
* Relay server fallback in case of an unsuccessful peer-to-peer connection.
* Private key never leaves your device.
* Works on ARM devices (e.g. Raspberry Pi).
<br>
### A bit on Wiretrustee internals
* Wiretrustee 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 devices.
* A connection session negotiation between peers is achieved with the Wiretrustee Signalling server [signal](signal/)
* Contents of the messages sent between peers through the signalling server are encrypted with Wireguard keys, making it impossible to inspect them.
The routing of the messages on a Signalling server is based on public Wireguard keys.
* Occasionally, the NAT-traversal is unsuccessful due to strict NATs (e.g. mobile carrier grade NAT).
For that matter, there is support for a relay server fallback (TURN). So in case, the (NAT-traversal is unsuccessful???), a secure Wireguard tunnel is established via TURN server.
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in Wiretrustee setups.
**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.**
### What Wiretrustee is not doing:
* Wireguard key management. In consequence, you need to generate peer keys and specify them on Wiretrustee initialization step.
* Peer address management. You have to specify a unique peer local address (e.g. 10.30.30.1/24) when configuring Wiretrustee
It requires zero configuration effort leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
### Client Installation
1. Checkout Wiretrustee [releases](https://github.com/wiretrustee/wiretrustee/releases)
2. Download the latest release:
```shell
wget https://github.com/wiretrustee/wiretrustee/releases/download/v0.0.4/wiretrustee_0.0.4_linux_amd64.rpm
```
3. Install the package
```shell
sudo dpkg -i wiretrustee_0.0.4_linux_amd64.deb
```
### Client Configuration
1. Initialize Wiretrustee:
```shell
sudo wiretrustee init \
--stunURLs stun:stun.wiretrustee.com:3468,stun:stun.l.google.com:19302 \
--turnURLs <TURN User>:<TURN password>@turn:stun.wiretrustee.com:3468 \
--signalAddr signal.wiretrustee.com:10000 \
--wgLocalAddr 10.30.30.1/24 \
--log-level info
```
It is important to mention that the ```wgLocalAddr``` parameter has to be unique across your network.
E.g. if you have Peer A with ```wgLocalAddr=10.30.30.1/24``` then another Peer B can have ```wgLocalAddr=10.30.30.2/24```
NetBird uses [NAT traversal techniques](https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment) to automatically create an overlay peer-to-peer network connecting machines regardless of location (home, office, data center, container, cloud, or edge environments), unifying virtual private network management experience.
If for some reason, you already have a generated Wireguard key, you can specify it with the ```--wgKey``` parameter.
If not specified, then a new one will be generated, and its corresponding public key will be output to the log.
A new config will be generated and stored under ```/etc/wiretrustee/config.json```
**Key features:**
- \[x] Automatic IP allocation and network management with a Web UI ([separate repo](https://github.com/netbirdio/dashboard))
- \[x] Automatic WireGuard peer (machine) discovery and configuration.
- \[x] Encrypted peer-to-peer connections without a central VPN gateway.
- \[x] Connection relay fallback in case a peer-to-peer connection is not possible.
- \[x] Desktop client applications for Linux, MacOS, and Windows (systray).
- \[x] Multiuser support - sharing network between multiple users.
- \[x] SSO and MFA support.
- \[x] Multicloud and hybrid-cloud support.
- \[x] Kernel WireGuard usage when possible.
- \[x] Access Controls - groups & rules.
- \[x] Remote SSH access without managing SSH keys.
- \[x] Network Routes.
- \[x] Private DNS.
- \[x] Network Activity Monitoring.
2. Add a peer to connect to.
```shell
sudo wiretrustee add-peer --allowedIPs 10.30.30.2/32 --key '<REMOTE PEER WIREUARD PUBLIC KEY>'
```
**Coming soon:**
- \[ ] Mobile clients.
### Secure peer-to-peer VPN with SSO and MFA in minutes
https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
### 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.
- Every agent connects to [Management Service](management/) that holds network state, manages peer IPs, and distributes network updates to agents (peers).
- 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) servers.
- Agents negotiate a connection through [Signal Service](signal/) passing p2p encrypted messages with candidates.
- 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.
<p float="left" align="middle">
<img src="https://netbird.io/docs/img/architecture/high-level-dia.png" width="700"/>
</p>
See a complete [architecture overview](https://netbird.io/docs/overview/architecture) for details.
3. Restart Wiretrustee to reload changes
```shell
sudo systemctl restart wiretrustee.service
sudo systemctl status wiretrustee.service
```
### Running the Signal service
We have packed the signal into docker images. You can pull the images from the Github registry and execute it with the following commands:
````shell
docker pull ghcr.io/wiretrustee/wiretrustee:signal-latest
docker run -d --name wiretrustee-signal -p 10000:10000 ghcr.io/wiretrustee/wiretrustee:signal-latest
````
The default log-level is set to INFO, if you need you can change it using by updating the docker cmd as followed:
````shell
docker run -d --name wiretrustee-signal -p 10000:10000 ghcr.io/wiretrustee/wiretrustee:signal-latest --log-level DEBUG
````
### Roadmap
* Android app
- [Public Roadmap](https://github.com/netbirdio/netbird/projects/2)
### Community projects
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird)
- [NetBird installer script](https://github.com/physk/netbird-installer)
### Support acknowledgement
In November 2022, NetBird joined the [StartUpSecure program](https://www.forschung-it-sicherheit-kommunikationssysteme.de/foerderung/bekanntmachungen/startup-secure) sponsored by The Federal Ministry of Education and Research of The Federal Republic of Germany. Together with [CISPA Helmholtz Center for Information Security](https://cispa.de/en) NetBird brings the security best practices and simplicity to private networking.
![CISPA_Logo_BLACK_EN_RZ_RGB (1)](https://user-images.githubusercontent.com/700848/203091324-c6d311a0-22b5-4b05-a288-91cbc6cdcc46.png)
### Testimonials
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), and [Coturn](https://github.com/coturn/coturn). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
### Legal
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.

12
SECURITY.md Normal file
View File

@@ -0,0 +1,12 @@
# Security Policy
NetBird's goal is to provide a secure network. If you find a vulnerability or bug, please report it by opening an issue [here](https://github.com/netbirdio/netbird/issues/new?assignees=&labels=&template=bug-issue-report.md&title=) or by contacting us by email.
There has yet to be an official bug bounty program for the NetBird project.
## Supported Versions
- We currently support only the latest version
## Reporting a Vulnerability
Please report security issues to `security@netbird.io`

7
client/Dockerfile Normal file
View File

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

129
client/android/client.go Normal file
View File

@@ -0,0 +1,129 @@
package android
import (
"context"
"sync"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/formatter"
"github.com/netbirdio/netbird/iface"
)
// ConnectionListener export internal Listener for mobile
type ConnectionListener interface {
peer.Listener
}
// TunAdapter export internal TunAdapter for mobile
type TunAdapter interface {
iface.TunAdapter
}
// IFaceDiscover export internal IFaceDiscover for mobile
type IFaceDiscover interface {
stdnet.IFaceDiscover
}
func init() {
formatter.SetLogcatFormatter(log.StandardLogger())
}
// Client struct manage the life circle of background service
type Client struct {
cfgFile string
tunAdapter iface.TunAdapter
iFaceDiscover IFaceDiscover
recorder *peer.Status
ctxCancel context.CancelFunc
ctxCancelLock *sync.Mutex
deviceName string
}
// NewClient instantiate a new Client
func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover) *Client {
lvl, _ := log.ParseLevel("trace")
log.SetLevel(lvl)
return &Client{
cfgFile: cfgFile,
deviceName: deviceName,
tunAdapter: tunAdapter,
iFaceDiscover: iFaceDiscover,
recorder: peer.NewRecorder(""),
ctxCancelLock: &sync.Mutex{},
}
}
// Run start the internal client. It is a blocker function
func (c *Client) Run(urlOpener URLOpener) error {
cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
ConfigPath: c.cfgFile,
})
if err != nil {
return err
}
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
var ctx context.Context
//nolint
ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
c.ctxCancelLock.Lock()
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
defer c.ctxCancel()
c.ctxCancelLock.Unlock()
auth := NewAuthWithConfig(ctx, cfg)
err = auth.login(urlOpener)
if err != nil {
return err
}
// todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx)
return internal.RunClient(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover)
}
// Stop the internal client and free the resources
func (c *Client) Stop() {
c.ctxCancelLock.Lock()
defer c.ctxCancelLock.Unlock()
if c.ctxCancel == nil {
return
}
c.ctxCancel()
}
// PeersList return with the list of the PeerInfos
func (c *Client) PeersList() *PeerInfoArray {
fullStatus := c.recorder.GetFullStatus()
peerInfos := make([]PeerInfo, len(fullStatus.Peers))
for n, p := range fullStatus.Peers {
pi := PeerInfo{
p.IP,
p.FQDN,
p.ConnStatus.String(),
p.Direct,
}
peerInfos[n] = pi
}
return &PeerInfoArray{items: peerInfos}
}
// SetConnectionListener set the network connection listener
func (c *Client) SetConnectionListener(listener ConnectionListener) {
c.recorder.SetConnectionListener(listener)
}
// RemoveConnectionListener remove connection listener
func (c *Client) RemoveConnectionListener() {
c.recorder.RemoveConnectionListener()
}

234
client/android/login.go Normal file
View File

@@ -0,0 +1,234 @@
package android
import (
"context"
"fmt"
"time"
"github.com/cenkalti/backoff/v4"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/cmd"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/client/internal"
)
// SSOListener is async listener for mobile framework
type SSOListener interface {
OnSuccess(bool)
OnError(error)
}
// ErrListener is async listener for mobile framework
type ErrListener interface {
OnSuccess()
OnError(error)
}
// URLOpener it is a callback interface. The Open function will be triggered if
// the backend want to show an url for the user
type URLOpener interface {
Open(string)
}
// Auth can register or login new client
type Auth struct {
ctx context.Context
config *internal.Config
cfgPath string
}
// NewAuth instantiate Auth struct and validate the management URL
func NewAuth(cfgPath string, mgmURL string) (*Auth, error) {
inputCfg := internal.ConfigInput{
ManagementURL: mgmURL,
}
cfg, err := internal.CreateInMemoryConfig(inputCfg)
if err != nil {
return nil, err
}
return &Auth{
ctx: context.Background(),
config: cfg,
cfgPath: cfgPath,
}, nil
}
// NewAuthWithConfig instantiate Auth based on existing config
func NewAuthWithConfig(ctx context.Context, config *internal.Config) *Auth {
return &Auth{
ctx: ctx,
config: config,
}
}
// SaveConfigIfSSOSupported test the connectivity with the management server by retrieving the server device flow info.
// If it returns a flow info than save the configuration and return true. If it gets a codes.NotFound, it means that SSO
// is not supported and returns false without saving the configuration. For other errors return false.
func (a *Auth) SaveConfigIfSSOSupported(listener SSOListener) {
go func() {
sso, err := a.saveConfigIfSSOSupported()
if err != nil {
listener.OnError(err)
} else {
listener.OnSuccess(sso)
}
}()
}
func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
supportsSSO := true
err := a.withBackOff(a.ctx, func() (err error) {
_, err = internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.NotFound {
supportsSSO = false
err = nil
}
return err
})
if !supportsSSO {
return false, nil
}
if err != nil {
return false, fmt.Errorf("backoff cycle failed: %v", err)
}
err = internal.WriteOutConfig(a.cfgPath, a.config)
return true, err
}
// LoginWithSetupKeyAndSaveConfig test the connectivity with the management server with the setup key.
func (a *Auth) LoginWithSetupKeyAndSaveConfig(resultListener ErrListener, setupKey string, deviceName string) {
go func() {
err := a.loginWithSetupKeyAndSaveConfig(setupKey, deviceName)
if err != nil {
resultListener.OnError(err)
} else {
resultListener.OnSuccess()
}
}()
}
func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string) error {
//nolint
ctxWithValues := context.WithValue(a.ctx, system.DeviceNameCtxKey, deviceName)
err := a.withBackOff(a.ctx, func() error {
backoffErr := internal.Login(ctxWithValues, a.config, setupKey, "")
if s, ok := gstatus.FromError(backoffErr); ok && (s.Code() == codes.PermissionDenied) {
// we got an answer from management, exit backoff earlier
return backoff.Permanent(backoffErr)
}
return backoffErr
})
if err != nil {
return fmt.Errorf("backoff cycle failed: %v", err)
}
return internal.WriteOutConfig(a.cfgPath, a.config)
}
// Login try register the client on the server
func (a *Auth) Login(resultListener ErrListener, urlOpener URLOpener) {
go func() {
err := a.login(urlOpener)
if err != nil {
resultListener.OnError(err)
} else {
resultListener.OnSuccess()
}
}()
}
func (a *Auth) login(urlOpener URLOpener) error {
var needsLogin bool
// check if we need to generate JWT token
err := a.withBackOff(a.ctx, func() (err error) {
needsLogin, err = internal.IsLoginRequired(a.ctx, a.config.PrivateKey, a.config.ManagementURL, a.config.SSHKey)
return
})
if err != nil {
return fmt.Errorf("backoff cycle failed: %v", err)
}
jwtToken := ""
if needsLogin {
tokenInfo, err := a.foregroundGetTokenInfo(urlOpener)
if err != nil {
return fmt.Errorf("interactive sso login failed: %v", err)
}
jwtToken = tokenInfo.AccessToken
}
err = a.withBackOff(a.ctx, func() error {
err := internal.Login(a.ctx, a.config, "", jwtToken)
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
return nil
}
return err
})
if err != nil {
return fmt.Errorf("backoff cycle failed: %v", err)
}
return nil
}
func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*internal.TokenInfo, error) {
providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
if err != nil {
s, ok := gstatus.FromError(err)
if ok && s.Code() == codes.NotFound {
return nil, fmt.Errorf("no SSO provider returned from management. " +
"If you are using hosting Netbird see documentation at " +
"https://github.com/netbirdio/netbird/tree/main/management for details")
} else if ok && s.Code() == codes.Unimplemented {
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
"please update your servver or use Setup Keys to login", a.config.ManagementURL)
} else {
return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err)
}
}
hostedClient := internal.NewHostedDeviceFlow(
providerConfig.ProviderConfig.Audience,
providerConfig.ProviderConfig.ClientID,
providerConfig.ProviderConfig.TokenEndpoint,
providerConfig.ProviderConfig.DeviceAuthEndpoint,
)
flowInfo, err := hostedClient.RequestDeviceCode(context.TODO())
if err != nil {
return nil, fmt.Errorf("getting a request device code failed: %v", err)
}
go urlOpener.Open(flowInfo.VerificationURIComplete)
waitTimeout := time.Duration(flowInfo.ExpiresIn)
waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout*time.Second)
defer cancel()
tokenInfo, err := hostedClient.WaitToken(waitCTX, flowInfo)
if err != nil {
return nil, fmt.Errorf("waiting for browser login failed: %v", err)
}
return &tokenInfo, nil
}
func (a *Auth) withBackOff(ctx context.Context, bf func() error) error {
return backoff.RetryNotify(
bf,
backoff.WithContext(cmd.CLIBackOffSettings, ctx),
func(err error, duration time.Duration) {
log.Warnf("retrying Login to the Management service in %v due to error %v", duration, err)
})
}

View File

@@ -0,0 +1,37 @@
package android
// PeerInfo describe information about the peers. It designed for the UI usage
type PeerInfo struct {
IP string
FQDN string
ConnStatus string // Todo replace to enum
Direct bool
}
// PeerInfoCollection made for Java layer to get non default types as collection
type PeerInfoCollection interface {
Add(s string) PeerInfoCollection
Get(i int) string
Size() int
}
// PeerInfoArray is the implementation of the PeerInfoCollection
type PeerInfoArray struct {
items []PeerInfo
}
// Add new PeerInfo to the collection
func (array PeerInfoArray) Add(s PeerInfo) PeerInfoArray {
array.items = append(array.items, s)
return array
}
// Get return an element of the collection
func (array PeerInfoArray) Get(i int) *PeerInfo {
return &array.items[i]
}
// Size return with the size of the collection
func (array PeerInfoArray) Size() int {
return len(array.items)
}

View File

@@ -0,0 +1,78 @@
package android
import (
"github.com/netbirdio/netbird/client/internal"
)
// Preferences export a subset of the internal config for gomobile
type Preferences struct {
configInput internal.ConfigInput
}
// NewPreferences create new Preferences instance
func NewPreferences(configPath string) *Preferences {
ci := internal.ConfigInput{
ConfigPath: configPath,
}
return &Preferences{ci}
}
// GetManagementURL read url from config file
func (p *Preferences) GetManagementURL() (string, error) {
if p.configInput.ManagementURL != "" {
return p.configInput.ManagementURL, nil
}
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
return cfg.ManagementURL.String(), err
}
// SetManagementURL store the given url and wait for commit
func (p *Preferences) SetManagementURL(url string) {
p.configInput.ManagementURL = url
}
// GetAdminURL read url from config file
func (p *Preferences) GetAdminURL() (string, error) {
if p.configInput.AdminURL != "" {
return p.configInput.AdminURL, nil
}
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
return cfg.AdminURL.String(), err
}
// SetAdminURL store the given url and wait for commit
func (p *Preferences) SetAdminURL(url string) {
p.configInput.AdminURL = url
}
// GetPreSharedKey read preshared key from config file
func (p *Preferences) GetPreSharedKey() (string, error) {
if p.configInput.PreSharedKey != nil {
return *p.configInput.PreSharedKey, nil
}
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
return cfg.PreSharedKey, err
}
// SetPreSharedKey store the given key and wait for commit
func (p *Preferences) SetPreSharedKey(key string) {
p.configInput.PreSharedKey = &key
}
// Commit write out the changes into config file
func (p *Preferences) Commit() error {
_, err := internal.UpdateOrCreateConfig(p.configInput)
return err
}

View File

@@ -0,0 +1,120 @@
package android
import (
"path/filepath"
"testing"
"github.com/netbirdio/netbird/client/internal"
)
func TestPreferences_DefaultValues(t *testing.T) {
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
p := NewPreferences(cfgFile)
defaultVar, err := p.GetAdminURL()
if err != nil {
t.Fatalf("failed to read default value: %s", err)
}
if defaultVar != internal.DefaultAdminURL {
t.Errorf("invalid default admin url: %s", defaultVar)
}
defaultVar, err = p.GetManagementURL()
if err != nil {
t.Fatalf("failed to read default management URL: %s", err)
}
if defaultVar != internal.DefaultManagementURL {
t.Errorf("invalid default management url: %s", defaultVar)
}
var preSharedKey string
preSharedKey, err = p.GetPreSharedKey()
if err != nil {
t.Fatalf("failed to read default preshared key: %s", err)
}
if preSharedKey != "" {
t.Errorf("invalid preshared key: %s", preSharedKey)
}
}
func TestPreferences_ReadUncommitedValues(t *testing.T) {
exampleString := "exampleString"
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
p := NewPreferences(cfgFile)
p.SetAdminURL(exampleString)
resp, err := p.GetAdminURL()
if err != nil {
t.Fatalf("failed to read admin url: %s", err)
}
if resp != exampleString {
t.Errorf("unexpected admin url: %s", resp)
}
p.SetManagementURL(exampleString)
resp, err = p.GetManagementURL()
if err != nil {
t.Fatalf("failed to read managmenet url: %s", err)
}
if resp != exampleString {
t.Errorf("unexpected managemenet url: %s", resp)
}
p.SetPreSharedKey(exampleString)
resp, err = p.GetPreSharedKey()
if err != nil {
t.Fatalf("failed to read preshared key: %s", err)
}
if resp != exampleString {
t.Errorf("unexpected preshared key: %s", resp)
}
}
func TestPreferences_Commit(t *testing.T) {
exampleURL := "https://myurl.com:443"
examplePresharedKey := "topsecret"
cfgFile := filepath.Join(t.TempDir(), "netbird.json")
p := NewPreferences(cfgFile)
p.SetAdminURL(exampleURL)
p.SetManagementURL(exampleURL)
p.SetPreSharedKey(examplePresharedKey)
err := p.Commit()
if err != nil {
t.Fatalf("failed to save changes: %s", err)
}
p = NewPreferences(cfgFile)
resp, err := p.GetAdminURL()
if err != nil {
t.Fatalf("failed to read admin url: %s", err)
}
if resp != exampleURL {
t.Errorf("unexpected admin url: %s", resp)
}
resp, err = p.GetManagementURL()
if err != nil {
t.Fatalf("failed to read managmenet url: %s", err)
}
if resp != exampleURL {
t.Errorf("unexpected managemenet url: %s", resp)
}
resp, err = p.GetPreSharedKey()
if err != nil {
t.Fatalf("failed to read preshared key: %s", err)
}
if resp != examplePresharedKey {
t.Errorf("unexpected preshared key: %s", resp)
}
}

46
client/cmd/down.go Normal file
View File

@@ -0,0 +1,46 @@
package cmd
import (
"context"
"github.com/netbirdio/netbird/util"
"time"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/proto"
)
var downCmd = &cobra.Command{
Use: "down",
Short: "down netbird connections",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
log.Errorf("failed initializing log %v", err)
return err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
log.Errorf("failed to connect to service CLI interface %v", err)
return err
}
defer conn.Close()
daemonClient := proto.NewDaemonServiceClient(conn)
if _, err := daemonClient.Down(ctx, &proto.DownRequest{}); err != nil {
log.Errorf("call service down method: %v", err)
return err
}
return nil
},
}

209
client/cmd/login.go Normal file
View File

@@ -0,0 +1,209 @@
package cmd
import (
"context"
"fmt"
"time"
"github.com/skratchdot/open-golang/open"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/util"
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/proto"
)
var loginCmd = &cobra.Command{
Use: "login",
Short: "login to the Netbird Management Service (first run)",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx := internal.CtxInitState(context.Background())
// workaround to run without service
if logFile == "console" {
err = handleRebrand(cmd)
if err != nil {
return err
}
config, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: configPath,
PreSharedKey: &preSharedKey,
})
if err != nil {
return fmt.Errorf("get config file: %v", err)
}
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
err = foregroundLogin(ctx, cmd, config, setupKey)
if err != nil {
return fmt.Errorf("foreground login failed: %v", err)
}
cmd.Println("Logging successfully")
return nil
}
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
return fmt.Errorf("failed to connect to daemon error: %v\n"+
"If the daemon is not running please run: "+
"\nnetbird service install \nnetbird service start\n", err)
}
defer conn.Close()
client := proto.NewDaemonServiceClient(conn)
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)
}
}
cmd.Println("Logging successfully")
return nil
},
}
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.Config, setupKey string) error {
needsLogin := false
err := WithBackOff(func() error {
err := internal.Login(ctx, config, "", "")
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
needsLogin = true
return nil
}
return err
})
if err != nil {
return fmt.Errorf("backoff cycle failed: %v", err)
}
jwtToken := ""
if setupKey == "" && needsLogin {
tokenInfo, err := foregroundGetTokenInfo(ctx, cmd, config)
if err != nil {
return fmt.Errorf("interactive sso login failed: %v", err)
}
jwtToken = tokenInfo.AccessToken
}
err = WithBackOff(func() error {
err := internal.Login(ctx, config, setupKey, jwtToken)
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
return nil
}
return err
})
if err != nil {
return fmt.Errorf("backoff cycle failed: %v", err)
}
return nil
}
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*internal.TokenInfo, error) {
providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
if err != nil {
s, ok := gstatus.FromError(err)
if ok && s.Code() == codes.NotFound {
return nil, fmt.Errorf("no SSO provider returned from management. " +
"If you are using hosting Netbird see documentation at " +
"https://github.com/netbirdio/netbird/tree/main/management for details")
} else if ok && s.Code() == codes.Unimplemented {
mgmtURL := managementURL
if mgmtURL == "" {
mgmtURL = internal.DefaultManagementURL
}
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
"please update your servver or use Setup Keys to login", mgmtURL)
} else {
return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err)
}
}
hostedClient := internal.NewHostedDeviceFlow(
providerConfig.ProviderConfig.Audience,
providerConfig.ProviderConfig.ClientID,
providerConfig.ProviderConfig.TokenEndpoint,
providerConfig.ProviderConfig.DeviceAuthEndpoint,
)
flowInfo, err := hostedClient.RequestDeviceCode(context.TODO())
if err != nil {
return nil, fmt.Errorf("getting a request device code failed: %v", err)
}
openURL(cmd, flowInfo.VerificationURIComplete)
waitTimeout := time.Duration(flowInfo.ExpiresIn)
waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout*time.Second)
defer c()
tokenInfo, err := hostedClient.WaitToken(waitCTX, flowInfo)
if err != nil {
return nil, fmt.Errorf("waiting for browser login failed: %v", err)
}
return &tokenInfo, nil
}
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.Printf("Alternatively, you may want to use a setup key, see:\n\n https://www.netbird.io/docs/overview/setup-keys\n")
}
}

53
client/cmd/login_test.go Normal file
View File

@@ -0,0 +1,53 @@
package cmd
import (
"fmt"
"strings"
"testing"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/util"
)
func TestLogin(t *testing.T) {
mgmAddr := startTestingServices(t)
tempDir := t.TempDir()
confPath := tempDir + "/config.json"
mgmtURL := fmt.Sprintf("http://%s", mgmAddr)
rootCmd.SetArgs([]string{
"login",
"--config",
confPath,
"--log-file",
"console",
"--setup-key",
strings.ToUpper("a2c8e62b-38f5-4553-b31e-dd66c696cebb"),
"--management-url",
mgmtURL,
})
err := rootCmd.Execute()
if err != nil {
t.Fatal(err)
}
// validate generated config
actualConf := &internal.Config{}
_, err = util.ReadJson(confPath, actualConf)
if err != nil {
t.Errorf("expected proper config file written, got broken %v", err)
}
if actualConf.ManagementURL.String() != mgmtURL {
t.Errorf("expected management URL %s got %s", mgmtURL, actualConf.ManagementURL.String())
}
if actualConf.WgIface != iface.WgInterfaceDefault {
t.Errorf("expected WgIfaceName %s got %s", iface.WgInterfaceDefault, actualConf.WgIface)
}
if len(actualConf.PrivateKey) == 0 {
t.Errorf("expected non empty Private key, got empty")
}
}

308
client/cmd/root.go Normal file
View File

@@ -0,0 +1,308 @@
package cmd
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/signal"
"path"
"runtime"
"strings"
"syscall"
"time"
"github.com/cenkalti/backoff/v4"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/netbirdio/netbird/client/internal"
)
const (
externalIPMapFlag = "external-ip-map"
dnsResolverAddress = "dns-resolver-address"
)
var (
configPath string
defaultConfigPathDir string
defaultConfigPath string
oldDefaultConfigPathDir string
oldDefaultConfigPath string
logLevel string
defaultLogFileDir string
defaultLogFile string
oldDefaultLogFileDir string
oldDefaultLogFile string
logFile string
daemonAddr string
managementURL string
adminURL string
setupKey string
preSharedKey string
natExternalIPs []string
customDNSAddress string
rootCmd = &cobra.Command{
Use: "netbird",
Short: "",
Long: "",
SilenceUsage: true,
}
)
// Execute executes the root command.
func Execute() error {
return rootCmd.Execute()
}
func init() {
defaultConfigPathDir = "/etc/netbird/"
defaultLogFileDir = "/var/log/netbird/"
oldDefaultConfigPathDir = "/etc/wiretrustee/"
oldDefaultLogFileDir = "/var/log/wiretrustee/"
if runtime.GOOS == "windows" {
defaultConfigPathDir = os.Getenv("PROGRAMDATA") + "\\Netbird\\"
defaultLogFileDir = os.Getenv("PROGRAMDATA") + "\\Netbird\\"
oldDefaultConfigPathDir = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\"
oldDefaultLogFileDir = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\"
}
defaultConfigPath = defaultConfigPathDir + "config.json"
defaultLogFile = defaultLogFileDir + "client.log"
oldDefaultConfigPath = oldDefaultConfigPathDir + "config.json"
oldDefaultLogFile = oldDefaultLogFileDir + "client.log"
defaultDaemonAddr := "unix:///var/run/netbird.sock"
if runtime.GOOS == "windows" {
defaultDaemonAddr = "tcp://127.0.0.1:41731"
}
rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL))
rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL))
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location")
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level")
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the the log will be output to stdout")
rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)")
rootCmd.PersistentFlags().StringVar(&preSharedKey, "preshared-key", "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.")
rootCmd.AddCommand(serviceCmd)
rootCmd.AddCommand(upCmd)
rootCmd.AddCommand(downCmd)
rootCmd.AddCommand(statusCmd)
rootCmd.AddCommand(loginCmd)
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(sshCmd)
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil,
`Sets external IPs maps between local addresses and interfaces.`+
`You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+
`An empty string "" clears the previous configuration. `+
`E.g. --external-ip-map 12.34.56.78/10.0.0.1 or --external-ip-map 12.34.56.200,12.34.56.78/10.0.0.1,12.34.56.80/eth1 `+
`or --external-ip-map ""`,
)
upCmd.PersistentFlags().StringVar(&customDNSAddress, dnsResolverAddress, "",
`Sets a custom address for NetBird's local DNS resolver. `+
`If set, the agent won't attempt to discover the best ip and port to listen on. `+
`An empty string "" clears the previous configuration. `+
`E.g. --dns-resolver-address 127.0.0.1:5053 or --dns-resolver-address ""`,
)
}
// SetupCloseHandler handles SIGTERM signal and exits with success
func SetupCloseHandler(ctx context.Context, cancel context.CancelFunc) {
termCh := make(chan os.Signal, 1)
signal.Notify(termCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
done := ctx.Done()
select {
case <-done:
case <-termCh:
}
log.Info("shutdown signal received")
cancel()
}()
}
// SetFlagsFromEnvVars reads and updates flag values from environment variables with prefix WT_
func SetFlagsFromEnvVars(cmd *cobra.Command) {
flags := cmd.PersistentFlags()
flags.VisitAll(func(f *pflag.Flag) {
oldEnvVar := FlagNameToEnvVar(f.Name, "WT_")
if value, present := os.LookupEnv(oldEnvVar); present {
err := flags.Set(f.Name, value)
if err != nil {
log.Infof("unable to configure flag %s using variable %s, err: %v", f.Name, oldEnvVar, err)
}
}
newEnvVar := FlagNameToEnvVar(f.Name, "NB_")
if value, present := os.LookupEnv(newEnvVar); present {
err := flags.Set(f.Name, value)
if err != nil {
log.Infof("unable to configure flag %s using variable %s, err: %v", f.Name, newEnvVar, err)
}
}
})
}
// FlagNameToEnvVar converts flag name to environment var name adding a prefix,
// replacing dashes and making all uppercase (e.g. setup-keys is converted to NB_SETUP_KEYS according to the input prefix)
func FlagNameToEnvVar(cmdFlag string, prefix string) string {
parsed := strings.ReplaceAll(cmdFlag, "-", "_")
upper := strings.ToUpper(parsed)
return prefix + upper
}
// DialClientGRPCServer returns client connection to the dameno server.
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
return grpc.DialContext(
ctx,
strings.TrimPrefix(addr, "tcp://"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
}
// WithBackOff execute function in backoff cycle.
func WithBackOff(bf func() error) error {
return backoff.RetryNotify(bf, CLIBackOffSettings, func(err error, duration time.Duration) {
log.Warnf("retrying Login to the Management service in %v due to error %v", duration, err)
})
}
// CLIBackOffSettings is default backoff settings for CLI commands.
var CLIBackOffSettings = &backoff.ExponentialBackOff{
InitialInterval: time.Second,
RandomizationFactor: backoff.DefaultRandomizationFactor,
Multiplier: backoff.DefaultMultiplier,
MaxInterval: 10 * time.Second,
MaxElapsedTime: 30 * time.Second,
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}
func handleRebrand(cmd *cobra.Command) error {
var err error
if logFile == defaultLogFile {
if migrateToNetbird(oldDefaultLogFile, defaultLogFile) {
cmd.Printf("will copy Log dir %s and its content to %s\n", oldDefaultLogFileDir, defaultLogFileDir)
err = cpDir(oldDefaultLogFileDir, defaultLogFileDir)
if err != nil {
return err
}
}
}
if configPath == defaultConfigPath {
if migrateToNetbird(oldDefaultConfigPath, defaultConfigPath) {
cmd.Printf("will copy Config dir %s and its content to %s\n", oldDefaultConfigPathDir, defaultConfigPathDir)
err = cpDir(oldDefaultConfigPathDir, defaultConfigPathDir)
if err != nil {
return err
}
}
}
return nil
}
func cpFile(src, dst string) error {
var err error
var srcfd *os.File
var dstfd *os.File
var srcinfo os.FileInfo
if srcfd, err = os.Open(src); err != nil {
return err
}
defer srcfd.Close()
if dstfd, err = os.Create(dst); err != nil {
return err
}
defer dstfd.Close()
if _, err = io.Copy(dstfd, srcfd); err != nil {
return err
}
if srcinfo, err = os.Stat(src); err != nil {
return err
}
return os.Chmod(dst, srcinfo.Mode())
}
func copySymLink(source, dest string) error {
link, err := os.Readlink(source)
if err != nil {
return err
}
return os.Symlink(link, dest)
}
func cpDir(src string, dst string) error {
var err error
var fds []os.DirEntry
var srcinfo os.FileInfo
if srcinfo, err = os.Stat(src); err != nil {
return err
}
if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
return err
}
if fds, err = os.ReadDir(src); err != nil {
return err
}
for _, fd := range fds {
srcfp := path.Join(src, fd.Name())
dstfp := path.Join(dst, fd.Name())
fileInfo, err := os.Stat(srcfp)
if err != nil {
return fmt.Errorf("fouldn't get fileInfo; %v", err)
}
switch fileInfo.Mode() & os.ModeType {
case os.ModeSymlink:
if err = copySymLink(srcfp, dstfp); err != nil {
return fmt.Errorf("failed to copy from %s to %s; %v", srcfp, dstfp, err)
}
case os.ModeDir:
if err = cpDir(srcfp, dstfp); err != nil {
return fmt.Errorf("failed to copy from %s to %s; %v", srcfp, dstfp, err)
}
default:
if err = cpFile(srcfp, dstfp); err != nil {
return fmt.Errorf("failed to copy from %s to %s; %v", srcfp, dstfp, err)
}
}
}
return nil
}
func migrateToNetbird(oldPath, newPath string) bool {
_, errOld := os.Stat(oldPath)
_, errNew := os.Stat(newPath)
if errors.Is(errOld, fs.ErrNotExist) || errNew == nil {
return false
}
return true
}

36
client/cmd/root_test.go Normal file
View File

@@ -0,0 +1,36 @@
package cmd
import (
"fmt"
"io"
"testing"
)
func TestInitCommands(t *testing.T) {
helpFlag := "-h"
commandArgs := [][]string{{"root", helpFlag}}
for _, command := range rootCmd.Commands() {
commandArgs = append(commandArgs, []string{command.Name(), command.Name(), helpFlag})
for _, subcommand := range command.Commands() {
commandArgs = append(commandArgs, []string{command.Name() + " " + subcommand.Name(), command.Name(), subcommand.Name(), helpFlag})
}
}
for _, args := range commandArgs {
t.Run(fmt.Sprintf("Testing Command %s", args[0]), func(t *testing.T) {
defer func() {
err := recover()
if err != nil {
t.Fatalf("got an panic error while running the command: %s -h. Error: %s", args[0], err)
}
}()
rootCmd.SetArgs(args[1:])
rootCmd.SetOut(io.Discard)
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running %s command, got %v", args[0], err)
return
}
})
}
}

51
client/cmd/service.go Normal file
View File

@@ -0,0 +1,51 @@
package cmd
import (
"context"
"runtime"
"github.com/kardianos/service"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"github.com/netbirdio/netbird/client/internal"
)
type program struct {
ctx context.Context
cancel context.CancelFunc
serv *grpc.Server
}
func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
ctx = internal.CtxInitState(ctx)
return &program{ctx: ctx, cancel: cancel}
}
func newSVCConfig() *service.Config {
name := "netbird"
if runtime.GOOS == "windows" {
name = "Netbird"
}
return &service.Config{
Name: name,
DisplayName: "Netbird",
Description: "A WireGuard-based mesh network that connects your devices into a single private network.",
Option: make(service.KeyValue),
}
}
func newSVC(prg *program, conf *service.Config) (service.Service, error) {
s, err := service.New(prg, conf)
if err != nil {
log.Fatal(err)
return nil, err
}
return s, nil
}
var serviceCmd = &cobra.Command{
Use: "service",
Short: "manages Netbird service",
}

View File

@@ -0,0 +1,216 @@
package cmd
import (
"context"
"fmt"
"net"
"os"
"strings"
"time"
"github.com/kardianos/service"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/server"
"github.com/netbirdio/netbird/util"
"github.com/spf13/cobra"
"google.golang.org/grpc"
)
func (p *program) Start(svc service.Service) error {
// Start should not block. Do the actual work async.
log.Info("starting Netbird service") //nolint
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
p.serv = grpc.NewServer()
split := strings.Split(daemonAddr, "://")
switch split[0] {
case "unix":
// cleanup failed close
stat, err := os.Stat(split[1])
if err == nil && !stat.IsDir() {
if err := os.Remove(split[1]); err != nil {
log.Debugf("remove socket file: %v", err)
}
}
case "tcp":
default:
return fmt.Errorf("unsupported daemon address protocol: %v", split[0])
}
listen, err := net.Listen(split[0], split[1])
if err != nil {
return fmt.Errorf("failed to listen daemon interface: %w", err)
}
go func() {
defer listen.Close()
if split[0] == "unix" {
err = os.Chmod(split[1], 0666)
if err != nil {
log.Errorf("failed setting daemon permissions: %v", split[1])
return
}
}
serverInstance := server.New(p.ctx, configPath, logFile)
if err := serverInstance.Start(); err != nil {
log.Fatalf("failed to start daemon: %v", err)
}
proto.RegisterDaemonServiceServer(p.serv, serverInstance)
log.Printf("started daemon server: %v", split[1])
if err := p.serv.Serve(listen); err != nil {
log.Errorf("failed to serve daemon requests: %v", err)
}
}()
return nil
}
func (p *program) Stop(srv service.Service) error {
p.cancel()
if p.serv != nil {
p.serv.Stop()
}
time.Sleep(time.Second * 2)
log.Info("stopped Netbird service") //nolint
return nil
}
var runCmd = &cobra.Command{
Use: "run",
Short: "runs Netbird as service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
err = util.InitLog(logLevel, logFile)
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx, cancel := context.WithCancel(cmd.Context())
SetupCloseHandler(ctx, cancel)
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
return err
}
err = s.Run()
if err != nil {
return err
}
cmd.Printf("Netbird service is running")
return nil
},
}
var startCmd = &cobra.Command{
Use: "start",
Short: "starts Netbird service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
err = util.InitLog(logLevel, logFile)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(cmd.Context())
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
cmd.PrintErrln(err)
return err
}
err = s.Start()
if err != nil {
cmd.PrintErrln(err)
return err
}
cmd.Println("Netbird service has been started")
return nil
},
}
var stopCmd = &cobra.Command{
Use: "stop",
Short: "stops Netbird service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
err = util.InitLog(logLevel, logFile)
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx, cancel := context.WithCancel(cmd.Context())
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
return err
}
err = s.Stop()
if err != nil {
return err
}
cmd.Println("Netbird service has been stopped")
return nil
},
}
var restartCmd = &cobra.Command{
Use: "restart",
Short: "restarts Netbird service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
err = util.InitLog(logLevel, logFile)
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx, cancel := context.WithCancel(cmd.Context())
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
return err
}
err = s.Restart()
if err != nil {
return err
}
cmd.Println("Netbird service has been restarted")
return nil
},
}

View File

@@ -0,0 +1,112 @@
package cmd
import (
"context"
"os"
"path/filepath"
"runtime"
"github.com/spf13/cobra"
)
var installCmd = &cobra.Command{
Use: "install",
Short: "installs Netbird service",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
svcConfig := newSVCConfig()
svcConfig.Arguments = []string{
"service",
"run",
"--config",
configPath,
"--log-level",
logLevel,
}
if managementURL != "" {
svcConfig.Arguments = append(svcConfig.Arguments, "--management-url", managementURL)
}
if logFile != "console" {
svcConfig.Arguments = append(svcConfig.Arguments, "--log-file", logFile)
}
if runtime.GOOS == "linux" {
// Respected only by systemd systems
svcConfig.Dependencies = []string{"After=network.target syslog.target"}
if logFile != "console" {
setStdLogPath := true
dir := filepath.Dir(logFile)
_, err := os.Stat(dir)
if err != nil {
err = os.MkdirAll(dir, 0750)
if err != nil {
setStdLogPath = false
}
}
if setStdLogPath {
svcConfig.Option["LogOutput"] = true
svcConfig.Option["LogDirectory"] = dir
}
}
}
ctx, cancel := context.WithCancel(cmd.Context())
s, err := newSVC(newProgram(ctx, cancel), svcConfig)
if err != nil {
cmd.PrintErrln(err)
return err
}
err = s.Install()
if err != nil {
cmd.PrintErrln(err)
return err
}
cmd.Println("Netbird service has been installed")
return nil
},
}
var uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "uninstalls Netbird service from system",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(cmd.Context())
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
return err
}
err = s.Uninstall()
if err != nil {
return err
}
cmd.Println("Netbird has been uninstalled")
return nil
},
}

120
client/cmd/ssh.go Normal file
View File

@@ -0,0 +1,120 @@
package cmd
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal"
nbssh "github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/util"
)
var (
port int
user = "root"
host string
)
var sshCmd = &cobra.Command{
Use: "ssh",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires a host argument")
}
split := strings.Split(args[0], "@")
if len(split) == 2 {
user = split[0]
host = split[1]
} else {
host = args[0]
}
return nil
},
Short: "connect to a remote SSH server",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
SetFlagsFromEnvVars(cmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
if !util.IsAdmin() {
cmd.Printf("error: you must have Administrator privileges to run this command\n")
return nil
}
ctx := internal.CtxInitState(cmd.Context())
config, err := internal.UpdateConfig(internal.ConfigInput{
ConfigPath: configPath,
})
if err != nil {
return err
}
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
sshctx, cancel := context.WithCancel(ctx)
go func() {
// blocking
if err := runSSH(sshctx, host, []byte(config.SSHKey), cmd); err != nil {
log.Print(err)
}
cancel()
}()
select {
case <-sig:
cancel()
case <-sshctx.Done():
}
return nil
},
}
func runSSH(ctx context.Context, addr string, pemKey []byte, cmd *cobra.Command) error {
c, err := nbssh.DialWithKey(fmt.Sprintf("%s:%d", addr, port), user, pemKey)
if err != nil {
cmd.Printf("Error: %v\n", err)
cmd.Printf("Couldn't connect. " +
"You might be disconnected from the NetBird network, or the NetBird agent isn't running.\n" +
"Run the status command: \n\n" +
" netbird status\n\n" +
"It might also be that the SSH server is disabled on the agent you are trying to connect to.\n")
return nil
}
go func() {
<-ctx.Done()
err = c.Close()
if err != nil {
return
}
}()
err = c.OpenTerminal()
if err != nil {
return err
}
return nil
}
func init() {
sshCmd.PersistentFlags().IntVarP(&port, "port", "p", nbssh.DefaultSSHPort, "Sets remote SSH port. Defaults to "+fmt.Sprint(nbssh.DefaultSSHPort))
}

435
client/cmd/status.go Normal file
View File

@@ -0,0 +1,435 @@
package cmd
import (
"context"
"encoding/json"
"fmt"
"net"
"net/netip"
"sort"
"strings"
"time"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
"gopkg.in/yaml.v3"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/util"
"github.com/netbirdio/netbird/version"
)
type peerStateDetailOutput struct {
FQDN string `json:"fqdn" yaml:"fqdn"`
IP string `json:"netbirdIp" yaml:"netbirdIp"`
PubKey string `json:"publicKey" yaml:"publicKey"`
Status string `json:"status" yaml:"status"`
LastStatusUpdate time.Time `json:"lastStatusUpdate" yaml:"lastStatusUpdate"`
ConnType string `json:"connectionType" yaml:"connectionType"`
Direct bool `json:"direct" yaml:"direct"`
IceCandidateType iceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"`
}
type peersStateOutput struct {
Total int `json:"total" yaml:"total"`
Connected int `json:"connected" yaml:"connected"`
Details []peerStateDetailOutput `json:"details" yaml:"details"`
}
type signalStateOutput struct {
URL string `json:"url" yaml:"url"`
Connected bool `json:"connected" yaml:"connected"`
}
type managementStateOutput struct {
URL string `json:"url" yaml:"url"`
Connected bool `json:"connected" yaml:"connected"`
}
type iceCandidateType struct {
Local string `json:"local" yaml:"local"`
Remote string `json:"remote" yaml:"remote"`
}
type statusOutputOverview struct {
Peers peersStateOutput `json:"peers" yaml:"peers"`
CliVersion string `json:"cliVersion" yaml:"cliVersion"`
DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"`
ManagementState managementStateOutput `json:"management" yaml:"management"`
SignalState signalStateOutput `json:"signal" yaml:"signal"`
IP string `json:"netbirdIp" yaml:"netbirdIp"`
PubKey string `json:"publicKey" yaml:"publicKey"`
KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"`
FQDN string `json:"fqdn" yaml:"fqdn"`
}
var (
detailFlag bool
ipv4Flag bool
jsonFlag bool
yamlFlag bool
ipsFilter []string
statusFilter string
ipsFilterMap map[string]struct{}
)
var statusCmd = &cobra.Command{
Use: "status",
Short: "status of the Netbird Service",
RunE: statusFunc,
}
func init() {
ipsFilterMap = make(map[string]struct{})
statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information in human-readable format")
statusCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "display detailed status information in json format")
statusCmd.PersistentFlags().BoolVar(&yamlFlag, "yaml", false, "display detailed status information in yaml format")
statusCmd.PersistentFlags().BoolVar(&ipv4Flag, "ipv4", false, "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33")
statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4")
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 statusFunc(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := parseFilters()
if err != nil {
return err
}
err = util.InitLog(logLevel, "console")
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx := internal.CtxInitState(context.Background())
resp, _ := getStatus(ctx, cmd)
if err != nil {
return nil
}
if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) {
cmd.Printf("Daemon status: %s\n\n"+
"Run UP command to log in with SSO (interactive login):\n\n"+
" netbird up \n\n"+
"If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n"+
"you can use a setup-key:\n\n netbird up --management-url <YOUR_MANAGEMENT_URL> --setup-key <YOUR_SETUP_KEY>\n\n"+
"More info: https://www.netbird.io/docs/overview/setup-keys\n\n",
resp.GetStatus(),
)
return nil
}
if ipv4Flag {
cmd.Print(parseInterfaceIP(resp.GetFullStatus().GetLocalPeerState().GetIP()))
return nil
}
outputInformationHolder := convertToStatusOutputOverview(resp)
statusOutputString := ""
switch {
case detailFlag:
statusOutputString = parseToFullDetailSummary(outputInformationHolder)
case jsonFlag:
statusOutputString, err = parseToJSON(outputInformationHolder)
case yamlFlag:
statusOutputString, err = parseToYAML(outputInformationHolder)
default:
statusOutputString = parseGeneralSummary(outputInformationHolder, false)
}
if err != nil {
return err
}
cmd.Print(statusOutputString)
return nil
}
func getStatus(ctx context.Context, cmd *cobra.Command) (*proto.StatusResponse, error) {
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
return nil, fmt.Errorf("failed to connect to daemon error: %v\n"+
"If the daemon is not running please run: "+
"\nnetbird service install \nnetbird service start\n", err)
}
defer conn.Close()
resp, err := proto.NewDaemonServiceClient(conn).Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true})
if err != nil {
return nil, fmt.Errorf("status failed: %v", status.Convert(err).Message())
}
return resp, nil
}
func parseFilters() error {
switch strings.ToLower(statusFilter) {
case "", "disconnected", "connected":
default:
return fmt.Errorf("wrong status filter, should be one of connected|disconnected, got: %s", statusFilter)
}
if len(ipsFilter) > 0 {
for _, addr := range ipsFilter {
_, err := netip.ParseAddr(addr)
if err != nil {
return fmt.Errorf("got an invalid IP address in the filter: address %s, error %s", addr, err)
}
ipsFilterMap[addr] = struct{}{}
}
}
return nil
}
func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverview {
pbFullStatus := resp.GetFullStatus()
managementState := pbFullStatus.GetManagementState()
managementOverview := managementStateOutput{
URL: managementState.GetURL(),
Connected: managementState.GetConnected(),
}
signalState := pbFullStatus.GetSignalState()
signalOverview := signalStateOutput{
URL: signalState.GetURL(),
Connected: signalState.GetConnected(),
}
peersOverview := mapPeers(resp.GetFullStatus().GetPeers())
overview := statusOutputOverview{
Peers: peersOverview,
CliVersion: version.NetbirdVersion(),
DaemonVersion: resp.GetDaemonVersion(),
ManagementState: managementOverview,
SignalState: signalOverview,
IP: pbFullStatus.GetLocalPeerState().GetIP(),
PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(),
KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(),
FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(),
}
return overview
}
func mapPeers(peers []*proto.PeerState) peersStateOutput {
var peersStateDetail []peerStateDetailOutput
localICE := ""
remoteICE := ""
connType := ""
peersConnected := 0
for _, pbPeerState := range peers {
isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String()
if skipDetailByFilters(pbPeerState, isPeerConnected) {
continue
}
if isPeerConnected {
peersConnected = peersConnected + 1
localICE = pbPeerState.GetLocalIceCandidateType()
remoteICE = pbPeerState.GetRemoteIceCandidateType()
connType = "P2P"
if pbPeerState.Relayed {
connType = "Relayed"
}
}
timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local()
peerState := peerStateDetailOutput{
IP: pbPeerState.GetIP(),
PubKey: pbPeerState.GetPubKey(),
Status: pbPeerState.GetConnStatus(),
LastStatusUpdate: timeLocal.UTC(),
ConnType: connType,
Direct: pbPeerState.GetDirect(),
IceCandidateType: iceCandidateType{
Local: localICE,
Remote: remoteICE,
},
FQDN: pbPeerState.GetFqdn(),
}
peersStateDetail = append(peersStateDetail, peerState)
}
sortPeersByIP(peersStateDetail)
peersOverview := peersStateOutput{
Total: len(peersStateDetail),
Connected: peersConnected,
Details: peersStateDetail,
}
return peersOverview
}
func sortPeersByIP(peersStateDetail []peerStateDetailOutput) {
if len(peersStateDetail) > 0 {
sort.SliceStable(peersStateDetail, func(i, j int) bool {
iAddr, _ := netip.ParseAddr(peersStateDetail[i].IP)
jAddr, _ := netip.ParseAddr(peersStateDetail[j].IP)
return iAddr.Compare(jAddr) == -1
})
}
}
func parseInterfaceIP(interfaceIP string) string {
ip, _, err := net.ParseCIDR(interfaceIP)
if err != nil {
return ""
}
return fmt.Sprintf("%s\n", ip)
}
func parseToJSON(overview statusOutputOverview) (string, error) {
jsonBytes, err := json.Marshal(overview)
if err != nil {
return "", fmt.Errorf("json marshal failed")
}
return string(jsonBytes), err
}
func parseToYAML(overview statusOutputOverview) (string, error) {
yamlBytes, err := yaml.Marshal(overview)
if err != nil {
return "", fmt.Errorf("yaml marshal failed")
}
return string(yamlBytes), nil
}
func parseGeneralSummary(overview statusOutputOverview, showURL bool) string {
managementConnString := "Disconnected"
if overview.ManagementState.Connected {
managementConnString = "Connected"
if showURL {
managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL)
}
}
signalConnString := "Disconnected"
if overview.SignalState.Connected {
signalConnString = "Connected"
if showURL {
signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL)
}
}
interfaceTypeString := "Userspace"
interfaceIP := overview.IP
if overview.KernelInterface {
interfaceTypeString = "Kernel"
} else if overview.IP == "" {
interfaceTypeString = "N/A"
interfaceIP = "N/A"
}
peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total)
summary := fmt.Sprintf(
"Daemon version: %s\n"+
"CLI version: %s\n"+
"Management: %s\n"+
"Signal: %s\n"+
"FQDN: %s\n"+
"NetBird IP: %s\n"+
"Interface type: %s\n"+
"Peers count: %s\n",
overview.DaemonVersion,
version.NetbirdVersion(),
managementConnString,
signalConnString,
overview.FQDN,
interfaceIP,
interfaceTypeString,
peersCountString,
)
return summary
}
func parseToFullDetailSummary(overview statusOutputOverview) string {
parsedPeersString := parsePeers(overview.Peers)
summary := parseGeneralSummary(overview, true)
return fmt.Sprintf(
"Peers detail:"+
"%s\n"+
"%s",
parsedPeersString,
summary,
)
}
func parsePeers(peers peersStateOutput) string {
var (
peersString = ""
)
for _, peerState := range peers.Details {
localICE := "-"
if peerState.IceCandidateType.Local != "" {
localICE = peerState.IceCandidateType.Local
}
remoteICE := "-"
if peerState.IceCandidateType.Remote != "" {
remoteICE = peerState.IceCandidateType.Remote
}
peerString := fmt.Sprintf(
"\n %s:\n"+
" NetBird IP: %s\n"+
" Public key: %s\n"+
" Status: %s\n"+
" -- detail --\n"+
" Connection type: %s\n"+
" Direct: %t\n"+
" ICE candidate (Local/Remote): %s/%s\n"+
" Last connection update: %s\n",
peerState.FQDN,
peerState.IP,
peerState.PubKey,
peerState.Status,
peerState.ConnType,
peerState.Direct,
localICE,
remoteICE,
peerState.LastStatusUpdate.Format("2006-01-02 15:04:05"),
)
peersString = peersString + peerString
}
return peersString
}
func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool {
statusEval := false
ipEval := false
if statusFilter != "" {
lowerStatusFilter := strings.ToLower(statusFilter)
if lowerStatusFilter == "disconnected" && isConnected {
statusEval = true
} else if lowerStatusFilter == "connected" && !isConnected {
statusEval = true
}
}
if len(ipsFilter) > 0 {
_, ok := ipsFilterMap[peerState.IP]
if !ok {
ipEval = true
}
}
return statusEval || ipEval
}

301
client/cmd/status_test.go Normal file
View File

@@ -0,0 +1,301 @@
package cmd
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/version"
)
var resp = &proto.StatusResponse{
Status: "Connected",
FullStatus: &proto.FullStatus{
Peers: []*proto.PeerState{
{
IP: "192.168.178.101",
PubKey: "Pubkey1",
Fqdn: "peer-1.awesome-domain.com",
ConnStatus: "Connected",
ConnStatusUpdate: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 1, 0, time.UTC)),
Relayed: false,
Direct: true,
LocalIceCandidateType: "",
RemoteIceCandidateType: "",
},
{
IP: "192.168.178.102",
PubKey: "Pubkey2",
Fqdn: "peer-2.awesome-domain.com",
ConnStatus: "Connected",
ConnStatusUpdate: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 2, 0, time.UTC)),
Relayed: true,
Direct: false,
LocalIceCandidateType: "relay",
RemoteIceCandidateType: "prflx",
},
},
ManagementState: &proto.ManagementState{
URL: "my-awesome-management.com:443",
Connected: true,
},
SignalState: &proto.SignalState{
URL: "my-awesome-signal.com:443",
Connected: true,
},
LocalPeerState: &proto.LocalPeerState{
IP: "192.168.178.100/16",
PubKey: "Some-Pub-Key",
KernelInterface: true,
Fqdn: "some-localhost.awesome-domain.com",
},
},
DaemonVersion: "0.14.1",
}
var overview = statusOutputOverview{
Peers: peersStateOutput{
Total: 2,
Connected: 2,
Details: []peerStateDetailOutput{
{
IP: "192.168.178.101",
PubKey: "Pubkey1",
FQDN: "peer-1.awesome-domain.com",
Status: "Connected",
LastStatusUpdate: time.Date(2001, 1, 1, 1, 1, 1, 0, time.UTC),
ConnType: "P2P",
Direct: true,
IceCandidateType: iceCandidateType{
Local: "",
Remote: "",
},
},
{
IP: "192.168.178.102",
PubKey: "Pubkey2",
FQDN: "peer-2.awesome-domain.com",
Status: "Connected",
LastStatusUpdate: time.Date(2002, 2, 2, 2, 2, 2, 0, time.UTC),
ConnType: "Relayed",
Direct: false,
IceCandidateType: iceCandidateType{
Local: "relay",
Remote: "prflx",
},
},
},
},
CliVersion: version.NetbirdVersion(),
DaemonVersion: "0.14.1",
ManagementState: managementStateOutput{
URL: "my-awesome-management.com:443",
Connected: true,
},
SignalState: signalStateOutput{
URL: "my-awesome-signal.com:443",
Connected: true,
},
IP: "192.168.178.100/16",
PubKey: "Some-Pub-Key",
KernelInterface: true,
FQDN: "some-localhost.awesome-domain.com",
}
func TestConversionFromFullStatusToOutputOverview(t *testing.T) {
convertedResult := convertToStatusOutputOverview(resp)
assert.Equal(t, overview, convertedResult)
}
func TestSortingOfPeers(t *testing.T) {
peers := []peerStateDetailOutput{
{
IP: "192.168.178.104",
},
{
IP: "192.168.178.102",
},
{
IP: "192.168.178.101",
},
{
IP: "192.168.178.105",
},
{
IP: "192.168.178.103",
},
}
sortPeersByIP(peers)
assert.Equal(t, peers[3].IP, "192.168.178.104")
}
func TestParsingToJSON(t *testing.T) {
json, _ := parseToJSON(overview)
//@formatter:off
expectedJSON := "{\"" +
"peers\":" +
"{" +
"\"total\":2," +
"\"connected\":2," +
"\"details\":" +
"[" +
"{" +
"\"fqdn\":\"peer-1.awesome-domain.com\"," +
"\"netbirdIp\":\"192.168.178.101\"," +
"\"publicKey\":\"Pubkey1\"," +
"\"status\":\"Connected\"," +
"\"lastStatusUpdate\":\"2001-01-01T01:01:01Z\"," +
"\"connectionType\":\"P2P\"," +
"\"direct\":true," +
"\"iceCandidateType\":" +
"{" +
"\"local\":\"\"," +
"\"remote\":\"\"" +
"}" +
"}," +
"{" +
"\"fqdn\":\"peer-2.awesome-domain.com\"," +
"\"netbirdIp\":\"192.168.178.102\"," +
"\"publicKey\":\"Pubkey2\"," +
"\"status\":\"Connected\"," +
"\"lastStatusUpdate\":\"2002-02-02T02:02:02Z\"," +
"\"connectionType\":\"Relayed\"," +
"\"direct\":false," +
"\"iceCandidateType\":" +
"{" +
"\"local\":\"relay\"," +
"\"remote\":\"prflx\"" +
"}" +
"}" +
"]" +
"}," +
"\"cliVersion\":\"development\"," +
"\"daemonVersion\":\"0.14.1\"," +
"\"management\":" +
"{" +
"\"url\":\"my-awesome-management.com:443\"," +
"\"connected\":true" +
"}," +
"\"signal\":" +
"{\"" +
"url\":\"my-awesome-signal.com:443\"," +
"\"connected\":true" +
"}," +
"\"netbirdIp\":\"192.168.178.100/16\"," +
"\"publicKey\":\"Some-Pub-Key\"," +
"\"usesKernelInterface\":true," +
"\"fqdn\":\"some-localhost.awesome-domain.com\"" +
"}"
// @formatter:on
assert.Equal(t, expectedJSON, json)
}
func TestParsingToYAML(t *testing.T) {
yaml, _ := parseToYAML(overview)
expectedYAML := "peers:\n" +
" total: 2\n" +
" connected: 2\n" +
" details:\n" +
" - fqdn: peer-1.awesome-domain.com\n" +
" netbirdIp: 192.168.178.101\n" +
" publicKey: Pubkey1\n" +
" status: Connected\n" +
" lastStatusUpdate: 2001-01-01T01:01:01Z\n" +
" connectionType: P2P\n" +
" direct: true\n" +
" iceCandidateType:\n" +
" local: \"\"\n" +
" remote: \"\"\n" +
" - fqdn: peer-2.awesome-domain.com\n" +
" netbirdIp: 192.168.178.102\n" +
" publicKey: Pubkey2\n" +
" status: Connected\n" +
" lastStatusUpdate: 2002-02-02T02:02:02Z\n" +
" connectionType: Relayed\n" +
" direct: false\n" +
" iceCandidateType:\n" +
" local: relay\n" +
" remote: prflx\n" +
"cliVersion: development\n" +
"daemonVersion: 0.14.1\n" +
"management:\n" +
" url: my-awesome-management.com:443\n" +
" connected: true\n" +
"signal:\n" +
" url: my-awesome-signal.com:443\n" +
" connected: true\n" +
"netbirdIp: 192.168.178.100/16\n" +
"publicKey: Some-Pub-Key\n" +
"usesKernelInterface: true\n" +
"fqdn: some-localhost.awesome-domain.com\n"
assert.Equal(t, expectedYAML, yaml)
}
func TestParsingToDetail(t *testing.T) {
detail := parseToFullDetailSummary(overview)
expectedDetail := "Peers detail:\n" +
" peer-1.awesome-domain.com:\n" +
" NetBird IP: 192.168.178.101\n" +
" Public key: Pubkey1\n" +
" Status: Connected\n" +
" -- detail --\n" +
" Connection type: P2P\n" +
" Direct: true\n" +
" ICE candidate (Local/Remote): -/-\n" +
" Last connection update: 2001-01-01 01:01:01\n" +
"\n" +
" peer-2.awesome-domain.com:\n" +
" NetBird IP: 192.168.178.102\n" +
" Public key: Pubkey2\n" +
" Status: Connected\n" +
" -- detail --\n" +
" Connection type: Relayed\n" +
" Direct: false\n" +
" ICE candidate (Local/Remote): relay/prflx\n" +
" Last connection update: 2002-02-02 02:02:02\n" +
"\n" +
"Daemon version: 0.14.1\n" +
"CLI version: development\n" +
"Management: Connected to my-awesome-management.com:443\n" +
"Signal: Connected to my-awesome-signal.com:443\n" +
"FQDN: some-localhost.awesome-domain.com\n" +
"NetBird IP: 192.168.178.100/16\n" +
"Interface type: Kernel\n" +
"Peers count: 2/2 Connected\n"
assert.Equal(t, expectedDetail, detail)
}
func TestParsingToShortVersion(t *testing.T) {
shortVersion := parseGeneralSummary(overview, false)
expectedString := "Daemon version: 0.14.1\n" +
"CLI version: development\n" +
"Management: Connected\n" +
"Signal: Connected\n" +
"FQDN: some-localhost.awesome-domain.com\n" +
"NetBird IP: 192.168.178.100/16\n" +
"Interface type: Kernel\n" +
"Peers count: 2/2 Connected\n"
assert.Equal(t, expectedString, shortVersion)
}
func TestParsingOfIP(t *testing.T) {
InterfaceIP := "192.168.178.123/16"
parsedIP := parseInterfaceIP(InterfaceIP)
assert.Equal(t, "192.168.178.123\n", parsedIP)
}

120
client/cmd/testutil.go Normal file
View File

@@ -0,0 +1,120 @@
package cmd
import (
"context"
"github.com/netbirdio/netbird/management/server/activity"
"net"
"path/filepath"
"testing"
"time"
"github.com/netbirdio/netbird/util"
clientProto "github.com/netbirdio/netbird/client/proto"
client "github.com/netbirdio/netbird/client/server"
mgmtProto "github.com/netbirdio/netbird/management/proto"
mgmt "github.com/netbirdio/netbird/management/server"
sigProto "github.com/netbirdio/netbird/signal/proto"
sig "github.com/netbirdio/netbird/signal/server"
"google.golang.org/grpc"
)
func startTestingServices(t *testing.T) string {
config := &mgmt.Config{}
_, err := util.ReadJson("../testdata/management.json", config)
if err != nil {
t.Fatal(err)
}
testDir := t.TempDir()
config.Datadir = testDir
err = util.CopyFileContents("../testdata/store.json", filepath.Join(testDir, "store.json"))
if err != nil {
t.Fatal(err)
}
_, signalLis := startSignal(t)
signalAddr := signalLis.Addr().String()
config.Signal.URI = signalAddr
_, mgmLis := startManagement(t, config)
mgmAddr := mgmLis.Addr().String()
return mgmAddr
}
func startSignal(t *testing.T) (*grpc.Server, net.Listener) {
lis, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
s := grpc.NewServer()
sigProto.RegisterSignalExchangeServer(s, sig.NewServer())
go func() {
if err := s.Serve(lis); err != nil {
panic(err)
}
}()
return s, lis
}
func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Listener) {
lis, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
s := grpc.NewServer()
store, err := mgmt.NewFileStore(config.Datadir)
if err != nil {
t.Fatal(err)
}
peersUpdateManager := mgmt.NewPeersUpdateManager()
eventStore := &activity.InMemoryEventStore{}
if err != nil {
return nil, nil
}
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "",
eventStore)
if err != nil {
t.Fatal(err)
}
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager, nil)
if err != nil {
t.Fatal(err)
}
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
go func() {
if err := s.Serve(lis); err != nil {
t.Error(err)
}
}()
return s, lis
}
func startClientDaemon(
t *testing.T, ctx context.Context, managementURL, configPath string,
) (*grpc.Server, net.Listener) {
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
s := grpc.NewServer()
server := client.New(ctx,
configPath, "")
if err := server.Start(); err != nil {
t.Fatal(err)
}
clientProto.RegisterDaemonServiceServer(s, server)
go func() {
if err := s.Serve(lis); err != nil {
t.Error(err)
}
}()
time.Sleep(time.Second)
return s, lis
}

268
client/cmd/up.go Normal file
View File

@@ -0,0 +1,268 @@
package cmd
import (
"context"
"fmt"
"net"
"net/netip"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/util"
)
const (
invalidInputType int = iota
ipInputType
interfaceInputType
)
var (
foregroundMode bool
upCmd = &cobra.Command{
Use: "up",
Short: "install, login and start Netbird client",
RunE: upFunc,
}
)
func init() {
upCmd.PersistentFlags().BoolVarP(&foregroundMode, "foreground-mode", "F", false, "start service in foreground")
}
func upFunc(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
SetFlagsFromEnvVars(cmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
err = validateNATExternalIPs(natExternalIPs)
if err != nil {
return err
}
ctx := internal.CtxInitState(cmd.Context())
if foregroundMode {
return runInForegroundMode(ctx, cmd)
}
return runInDaemonMode(ctx, cmd)
}
func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
err := handleRebrand(cmd)
if err != nil {
return err
}
customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
if err != nil {
return err
}
config, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: configPath,
PreSharedKey: &preSharedKey,
NATExternalIPs: natExternalIPs,
CustomDNSAddress: customDNSAddressConverted,
})
if err != nil {
return fmt.Errorf("get config file: %v", err)
}
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
err = foregroundLogin(ctx, cmd, config, setupKey)
if err != nil {
return fmt.Errorf("foreground login failed: %v", err)
}
var cancel context.CancelFunc
ctx, cancel = context.WithCancel(ctx)
SetupCloseHandler(ctx, cancel)
return internal.RunClient(ctx, config, peer.NewRecorder(config.ManagementURL.String()), nil, nil)
}
func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
if err != nil {
return err
}
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
return fmt.Errorf("failed to connect to daemon error: %v\n"+
"If the daemon is not running please run: "+
"\nnetbird service install \nnetbird service start\n", err)
}
defer func() {
err := conn.Close()
if err != nil {
log.Warnf("failed closing dameon gRPC client connection %v", err)
return
}
}()
client := proto.NewDaemonServiceClient(conn)
status, err := client.Status(ctx, &proto.StatusRequest{})
if err != nil {
return fmt.Errorf("unable to get daemon status: %v", err)
}
if status.Status == string(internal.StatusConnected) {
cmd.Println("Already connected")
return nil
}
loginRequest := proto.LoginRequest{
SetupKey: setupKey,
PreSharedKey: preSharedKey,
ManagementUrl: managementURL,
AdminURL: adminURL,
NatExternalIPs: natExternalIPs,
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
CustomDNSAddress: customDNSAddressConverted,
}
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)
}
cmd.Println("Connected")
return nil
}
func validateNATExternalIPs(list []string) error {
for _, element := range list {
if element == "" {
return fmt.Errorf("empty string is not a valid input for %s", externalIPMapFlag)
}
subElements := strings.Split(element, "/")
if len(subElements) > 2 {
return fmt.Errorf("%s is not a valid input for %s. it should be formated as \"String\" or \"String/String\"", element, externalIPMapFlag)
}
if len(subElements) == 1 && !isValidIP(subElements[0]) {
return fmt.Errorf("%s is not a valid input for %s. it should be formated as \"IP\" or \"IP/IP\", or \"IP/Interface Name\"", element, externalIPMapFlag)
}
last := 0
for _, singleElement := range subElements {
inputType, err := validateElement(singleElement)
if err != nil {
return fmt.Errorf("%s is not a valid input for %s. it should be an IP string or a network name", singleElement, externalIPMapFlag)
}
if last == interfaceInputType && inputType == interfaceInputType {
return fmt.Errorf("%s is not a valid input for %s. it should not contain two interface names", element, externalIPMapFlag)
}
last = inputType
}
}
return nil
}
func validateElement(element string) (int, error) {
if isValidIP(element) {
return ipInputType, nil
}
validIface, err := isValidInterface(element)
if err != nil {
return invalidInputType, fmt.Errorf("unable to validate the network interface name, error: %s", err)
}
if validIface {
return interfaceInputType, nil
}
return interfaceInputType, fmt.Errorf("invalid IP or network interface name not found")
}
func isValidIP(ip string) bool {
return net.ParseIP(ip) != nil
}
func isValidInterface(name string) (bool, error) {
netInterfaces, err := net.Interfaces()
if err != nil {
return false, err
}
for _, iface := range netInterfaces {
if iface.Name == name {
return true, nil
}
}
return false, nil
}
func parseCustomDNSAddress(modified bool) ([]byte, error) {
var parsed []byte
if modified {
if !isValidAddrPort(customDNSAddress) {
return nil, fmt.Errorf("%s is invalid, it should be formated as IP:Port string or as an empty string like \"\"", customDNSAddress)
}
if customDNSAddress == "" && logFile != "console" {
parsed = []byte("empty")
} else {
parsed = []byte(customDNSAddress)
}
}
return parsed, nil
}
func isValidAddrPort(input string) bool {
if input == "" {
return true
}
_, err := netip.ParseAddrPort(input)
return err == nil
}

View File

@@ -0,0 +1,75 @@
package cmd
import (
"context"
"testing"
"time"
"github.com/netbirdio/netbird/client/internal"
)
var cliAddr string
func TestUpDaemon(t *testing.T) {
mgmAddr := startTestingServices(t)
tempDir := t.TempDir()
confPath := tempDir + "/config.json"
ctx := internal.CtxInitState(context.Background())
state := internal.CtxGetState(ctx)
_, cliLis := startClientDaemon(t, ctx, "http://"+mgmAddr, confPath)
cliAddr = cliLis.Addr().String()
daemonAddr = "tcp://" + cliAddr
rootCmd.SetArgs([]string{
"login",
"--daemon-addr", "tcp://" + cliAddr,
"--setup-key", "A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
"--log-file", "",
})
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
return
}
time.Sleep(time.Second * 3)
if status, err := state.Status(); err != nil && status != internal.StatusIdle {
t.Errorf("wrong status after login: %s, %v", internal.StatusIdle, err)
return
}
rootCmd.SetArgs([]string{
"up",
"--daemon-addr", "tcp://" + cliAddr,
"--log-file", "",
})
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
return
}
time.Sleep(time.Second * 3)
if status, err := state.Status(); err != nil && status != internal.StatusConnected {
t.Errorf("wrong status after connect: %s, %v", status, err)
return
}
rootCmd.SetArgs([]string{
"status",
"--daemon-addr", "tcp://" + cliAddr,
})
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
return
}
time.Sleep(time.Second * 3)
rootCmd.SetErr(nil)
rootCmd.SetArgs([]string{"down", "--daemon-addr", "tcp://" + cliAddr})
if err := rootCmd.Execute(); err != nil {
t.Errorf("expected no error while running up command, got %v", err)
return
}
// we can't check status here, because context already canceled
}

18
client/cmd/version.go Normal file
View File

@@ -0,0 +1,18 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/version"
)
var (
versionCmd = &cobra.Command{
Use: "version",
Short: "prints Netbird version",
Run: func(cmd *cobra.Command, args []string) {
cmd.SetOut(cmd.OutOrStdout())
cmd.Println(version.NetbirdVersion())
},
}
)

View File

@@ -0,0 +1,57 @@
package firewall
import (
"net"
)
// Rule abstraction should be implemented by each firewall manager
//
// Each firewall type for different OS can use different type
// of the properties to hold data of the created rule
type Rule interface {
// GetRuleID returns the rule id
GetRuleID() string
}
// Direction is the direction of the traffic
type Direction int
const (
// DirectionSrc is the direction of the traffic from the source
DirectionSrc Direction = iota
// DirectionDst is the direction of the traffic from the destination
DirectionDst
)
// Action is the action to be taken on a rule
type Action int
const (
// ActionAccept is the action to accept a packet
ActionAccept Action = iota
// ActionDrop is the action to drop a packet
ActionDrop
)
// Manager is the high level abstraction of a firewall manager
//
// It declares methods which handle actions required by the
// Netbird client for ACL and routing functionality
type Manager interface {
// AddFiltering rule to the firewall
AddFiltering(
ip net.IP,
port *Port,
direction Direction,
action Action,
comment string,
) (Rule, error)
// DeleteRule from the firewall by rule definition
DeleteRule(rule Rule) error
// Reset firewall to the default state
Reset() error
// TODO: migrate routemanager firewal actions to this interface
}

View File

@@ -0,0 +1,160 @@
package iptables
import (
"fmt"
"net"
"strconv"
"sync"
"github.com/coreos/go-iptables/iptables"
"github.com/google/uuid"
fw "github.com/netbirdio/netbird/client/firewall"
)
const (
// ChainFilterName is the name of the chain that is used for filtering by the Netbird client
ChainFilterName = "NETBIRD-ACL"
)
// Manager of iptables firewall
type Manager struct {
mutex sync.Mutex
ipv4Client *iptables.IPTables
ipv6Client *iptables.IPTables
}
// Create iptables firewall manager
func Create() (*Manager, error) {
m := &Manager{}
// init clients for booth ipv4 and ipv6
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
if err != nil {
return nil, fmt.Errorf("iptables is not installed in the system or not supported")
}
m.ipv4Client = ipv4Client
ipv6Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
if err != nil {
return nil, fmt.Errorf("ip6tables is not installed in the system or not supported")
}
m.ipv6Client = ipv6Client
if err := m.Reset(); err != nil {
return nil, fmt.Errorf("failed to reset firewall: %s", err)
}
return m, nil
}
// AddFiltering rule to the firewall
func (m *Manager) AddFiltering(
ip net.IP,
port *fw.Port,
direction fw.Direction,
action fw.Action,
comment string,
) (fw.Rule, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
client := m.client(ip)
ok, err := client.ChainExists("filter", ChainFilterName)
if err != nil {
return nil, fmt.Errorf("failed to check if chain exists: %s", err)
}
if !ok {
if err := client.NewChain("filter", ChainFilterName); err != nil {
return nil, fmt.Errorf("failed to create chain: %s", err)
}
}
if port == nil || port.Values == nil || (port.IsRange && len(port.Values) != 2) {
return nil, fmt.Errorf("invalid port definition")
}
pv := strconv.Itoa(port.Values[0])
if port.IsRange {
pv += ":" + strconv.Itoa(port.Values[1])
}
specs := m.filterRuleSpecs("filter", ChainFilterName, ip, pv, direction, action, comment)
if err := client.AppendUnique("filter", ChainFilterName, specs...); err != nil {
return nil, err
}
rule := &Rule{
id: uuid.New().String(),
specs: specs,
v6: ip.To4() == nil,
}
return rule, nil
}
// DeleteRule from the firewall by rule definition
func (m *Manager) DeleteRule(rule fw.Rule) error {
m.mutex.Lock()
defer m.mutex.Unlock()
r, ok := rule.(*Rule)
if !ok {
return fmt.Errorf("invalid rule type")
}
client := m.ipv4Client
if r.v6 {
client = m.ipv6Client
}
return client.Delete("filter", ChainFilterName, r.specs...)
}
// Reset firewall to the default state
func (m *Manager) Reset() error {
m.mutex.Lock()
defer m.mutex.Unlock()
if err := m.reset(m.ipv4Client, "filter", ChainFilterName); err != nil {
return fmt.Errorf("clean ipv4 firewall ACL chain: %w", err)
}
if err := m.reset(m.ipv6Client, "filter", ChainFilterName); err != nil {
return fmt.Errorf("clean ipv6 firewall ACL chain: %w", err)
}
return nil
}
// reset firewall chain, clear it and drop it
func (m *Manager) reset(client *iptables.IPTables, table, chain string) error {
ok, err := client.ChainExists(table, chain)
if err != nil {
return fmt.Errorf("failed to check if chain exists: %w", err)
}
if !ok {
return nil
}
if err := client.ClearChain(table, ChainFilterName); err != nil {
return fmt.Errorf("failed to clear chain: %w", err)
}
return client.DeleteChain(table, ChainFilterName)
}
// filterRuleSpecs returns the specs of a filtering rule
func (m *Manager) filterRuleSpecs(
table string, chain string, ip net.IP, port string,
direction fw.Direction, action fw.Action, comment string,
) (specs []string) {
if direction == fw.DirectionSrc {
specs = append(specs, "-s", ip.String())
}
specs = append(specs, "-p", "tcp", "--dport", port)
specs = append(specs, "-j", m.actionToStr(action))
return append(specs, "-m", "comment", "--comment", comment)
}
// client returns corresponding iptables client for the given ip
func (m *Manager) client(ip net.IP) *iptables.IPTables {
if ip.To4() != nil {
return m.ipv4Client
}
return m.ipv6Client
}
func (m *Manager) actionToStr(action fw.Action) string {
if action == fw.ActionAccept {
return "ACCEPT"
}
return "DROP"
}

View File

@@ -0,0 +1,105 @@
package iptables
import (
"net"
"testing"
"github.com/coreos/go-iptables/iptables"
fw "github.com/netbirdio/netbird/client/firewall"
)
func TestNewManager(t *testing.T) {
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
if err != nil {
t.Fatal(err)
}
manager, err := Create()
if err != nil {
t.Fatal(err)
}
var rule1 fw.Rule
t.Run("add first rule", func(t *testing.T) {
ip := net.ParseIP("10.20.0.2")
port := &fw.Port{Proto: fw.PortProtocolTCP, Values: []int{8080}}
rule1, err = manager.AddFiltering(ip, port, fw.DirectionDst, fw.ActionAccept, "accept HTTP traffic")
if err != nil {
t.Errorf("failed to add rule: %v", err)
}
checkRuleSpecs(t, ipv4Client, true, rule1.(*Rule).specs...)
})
var rule2 fw.Rule
t.Run("add second rule", func(t *testing.T) {
ip := net.ParseIP("10.20.0.3")
port := &fw.Port{
Proto: fw.PortProtocolTCP,
Values: []int{8043: 8046},
}
rule2, err = manager.AddFiltering(
ip, port, fw.DirectionDst, fw.ActionAccept, "accept HTTPS traffic from ports range")
if err != nil {
t.Errorf("failed to add rule: %v", err)
}
checkRuleSpecs(t, ipv4Client, true, rule2.(*Rule).specs...)
})
t.Run("delete first rule", func(t *testing.T) {
if err := manager.DeleteRule(rule1); err != nil {
t.Errorf("failed to delete rule: %v", err)
}
checkRuleSpecs(t, ipv4Client, false, rule1.(*Rule).specs...)
})
t.Run("delete second rule", func(t *testing.T) {
if err := manager.DeleteRule(rule2); err != nil {
t.Errorf("failed to delete rule: %v", err)
}
checkRuleSpecs(t, ipv4Client, false, rule2.(*Rule).specs...)
})
t.Run("reset check", func(t *testing.T) {
// add second rule
ip := net.ParseIP("10.20.0.3")
port := &fw.Port{Proto: fw.PortProtocolUDP, Values: []int{5353}}
_, err = manager.AddFiltering(ip, port, fw.DirectionDst, fw.ActionAccept, "accept Fake DNS traffic")
if err != nil {
t.Errorf("failed to add rule: %v", err)
}
if err := manager.Reset(); err != nil {
t.Errorf("failed to reset: %v", err)
}
ok, err := ipv4Client.ChainExists("filter", ChainFilterName)
if err != nil {
t.Errorf("failed to drop chain: %v", err)
}
if ok {
t.Errorf("chain '%v' still exists after Reset", ChainFilterName)
}
})
}
func checkRuleSpecs(t *testing.T, ipv4Client *iptables.IPTables, mustExists bool, rulespec ...string) {
exists, err := ipv4Client.Exists("filter", ChainFilterName, rulespec...)
if err != nil {
t.Errorf("failed to check rule: %v", err)
return
}
if !exists && mustExists {
t.Errorf("rule '%v' does not exist", rulespec)
return
}
if exists && !mustExists {
t.Errorf("rule '%v' exist", rulespec)
return
}
}

View File

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

24
client/firewall/port.go Normal file
View File

@@ -0,0 +1,24 @@
package firewall
// PortProtocol is the protocol of the port
type PortProtocol string
const (
// PortProtocolTCP is the TCP protocol
PortProtocolTCP PortProtocol = "tcp"
// PortProtocolUDP is the UDP protocol
PortProtocolUDP PortProtocol = "udp"
)
// Port of the address for firewall rule
type Port struct {
// IsRange is true Values contains two values, the first is the start port, the second is the end port
IsRange bool
// Values contains one value for single port, multiple values for the list of ports, or two values for the range of ports
Values []int
// Proto is the protocol of the port
Proto PortProtocol
}

215
client/installer.nsis Normal file
View File

@@ -0,0 +1,215 @@
!define APP_NAME "Netbird"
!define COMP_NAME "Netbird"
!define WEB_SITE "Netbird.io"
!define VERSION $%APPVER%
!define COPYRIGHT "Netbird Authors, 2022"
!define DESCRIPTION "A WireGuard®-based mesh network that connects your devices into a single private network"
!define INSTALLER_NAME "netbird-installer.exe"
!define MAIN_APP_EXE "Netbird"
!define ICON "ui\\netbird.ico"
!define BANNER "ui\\banner.bmp"
!define LICENSE_DATA "..\\LICENSE"
!define INSTALL_DIR "$PROGRAMFILES64\${APP_NAME}"
!define INSTALL_TYPE "SetShellVarContext all"
!define REG_ROOT "HKLM"
!define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAIN_APP_EXE}"
!define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}"
!define UI_APP_NAME "Netbird UI"
!define UI_APP_EXE "Netbird-ui"
!define UI_REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${UI_APP_EXE}"
!define UI_UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UI_APP_NAME}"
Unicode True
######################################################################
VIProductVersion "${VERSION}"
VIAddVersionKey "ProductName" "${APP_NAME}"
VIAddVersionKey "CompanyName" "${COMP_NAME}"
VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
VIAddVersionKey "FileDescription" "${DESCRIPTION}"
VIAddVersionKey "FileVersion" "${VERSION}"
######################################################################
SetCompressor /SOLID Lzma
Name "${APP_NAME}"
Caption "${APP_NAME}"
OutFile "..\\${INSTALLER_NAME}"
BrandingText "${APP_NAME}"
InstallDirRegKey "${REG_ROOT}" "${REG_APP_PATH}" ""
InstallDir "${INSTALL_DIR}"
LicenseData "${LICENSE_DATA}"
ShowInstDetails Show
######################################################################
!define MUI_ICON "${ICON}"
!define MUI_UNICON "${ICON}"
!define MUI_WELCOMEFINISHPAGE_BITMAP "${BANNER}"
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "${BANNER}"
!define MUI_FINISHPAGE_RUN
!define MUI_FINISHPAGE_RUN_TEXT "Start ${UI_APP_NAME}"
!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink"
######################################################################
!include "MUI2.nsh"
!include LogicLib.nsh
!define MUI_ABORTWARNING
!define MUI_UNABORTWARNING
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "${LICENSE_DATA}"
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH
!insertmacro MUI_LANGUAGE "English"
######################################################################
Function GetAppFromCommand
Exch $1
Push $2
StrCpy $2 $1 1 0
StrCmp $2 '"' 0 done
Push $3
StrCpy $3 ""
loop:
IntOp $3 $3 + 1
StrCpy $2 $1 1 $3
StrCmp $2 '' +2
StrCmp $2 '"' 0 loop
StrCpy $1 $1 $3
StrCpy $1 $1 "" 1 ; Remove starting quote
Pop $3
done:
Pop $2
Exch $1
FunctionEnd
!macro GetAppFromCommand in out
Push "${in}"
Call GetAppFromCommand
Pop ${out}
!macroend
!macro UninstallPreviousNSIS UninstCommand CustomParameters
Push $0
Push $1
Push $2
Push '${CustomParameters}'
Push '${UninstCommand}'
Call GetAppFromCommand ; Remove quotes and parameters from UninstCommand
Pop $0
Pop $1
GetFullPathName $2 "$0\.."
ExecWait '"$0" /S $1 _?=$2'
Delete "$0" ; Extra cleanup because we used _?=
RMDir "$2"
Pop $2
Pop $1
Pop $0
!macroend
Function .onInit
StrCpy $INSTDIR "${INSTALL_DIR}"
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
${If} $R0 != ""
# if silent install jump to uninstall step
IfSilent uninstall
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}
FunctionEnd
######################################################################
Section -MainProgram
${INSTALL_TYPE}
# SetOverwrite ifnewer
SetOutPath "$INSTDIR"
File /r "..\\dist\\netbird_windows_amd64\\"
SectionEnd
######################################################################
Section -Icons_Reg
SetOutPath "$INSTDIR"
WriteUninstaller "$INSTDIR\netbird_uninstall.exe"
WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayName" "${APP_NAME}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "UninstallString" "$INSTDIR\netbird_uninstall.exe"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\${MAIN_APP_EXE}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayVersion" "${VERSION}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}"
WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
EnVar::SetHKLM
EnVar::AddValueEx "path" "$INSTDIR"
SetShellVarContext current
CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
SetShellVarContext all
SectionEnd
Section -Post
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service install'
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service start'
# sleep a bit for visibility
Sleep 1000
SectionEnd
######################################################################
Section Uninstall
${INSTALL_TYPE}
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service stop'
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
# kill ui client
ExecWait `taskkill /im ${UI_APP_EXE}.exe`
# wait the service uninstall take unblock the executable
Sleep 3000
Delete "$INSTDIR\${UI_APP_EXE}"
Delete "$INSTDIR\${MAIN_APP_EXE}"
RmDir /r "$INSTDIR"
SetShellVarContext current
Delete "$DESKTOP\${APP_NAME}.lnk"
Delete "$SMPROGRAMS\${APP_NAME}.lnk"
SetShellVarContext all
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
EnVar::SetHKLM
EnVar::DeleteValue "path" "$INSTDIR"
SectionEnd
Function LaunchLink
SetShellVarContext current
SetOutPath $INSTDIR
ShellExecAsUser::ShellExecAsUser "" "$DESKTOP\${APP_NAME}.lnk"
SetShellVarContext all
FunctionEnd

305
client/internal/config.go Normal file
View File

@@ -0,0 +1,305 @@
package internal
import (
"fmt"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/util"
)
const (
// ManagementLegacyPort is the port that was used before by the Management gRPC server.
// It is used for backward compatibility now.
// NB: hardcoded from github.com/netbirdio/netbird/management/cmd to avoid import
ManagementLegacyPort = 33073
// DefaultManagementURL points to the NetBird's cloud management endpoint
DefaultManagementURL = "https://api.wiretrustee.com:443"
// DefaultAdminURL points to NetBird's cloud management console
DefaultAdminURL = "https://app.netbird.io:443"
)
var defaultInterfaceBlacklist = []string{iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts",
"Tailscale", "tailscale", "docker", "veth", "br-"}
// ConfigInput carries configuration changes to the client
type ConfigInput struct {
ManagementURL string
AdminURL string
ConfigPath string
PreSharedKey *string
NATExternalIPs []string
CustomDNSAddress []byte
}
// Config Configuration type
type Config struct {
// Wireguard private key of local peer
PrivateKey string
PreSharedKey string
ManagementURL *url.URL
AdminURL *url.URL
WgIface string
WgPort int
IFaceBlackList []string
DisableIPv6Discovery bool
// SSHKey is a private SSH key in a PEM format
SSHKey string
// ExternalIP mappings, if different than the host interface IP
//
// External IP must not be behind a CGNAT and port-forwarding for incoming UDP packets from WgPort on ExternalIP
// to WgPort on host interface IP must be present. This can take form of single port-forwarding rule, 1:1 DNAT
// mapping ExternalIP to host interface IP, or a NAT DMZ to host interface IP.
//
// A single mapping will take the form of: external[/internal]
// external (required): either the external IP address or "stun" to use STUN to determine the external IP address
// internal (optional): either the internal/interface IP address or an interface name
//
// examples:
// "12.34.56.78" => all interfaces IPs will be mapped to external IP of 12.34.56.78
// "12.34.56.78/eth0" => IPv4 assigned to interface eth0 will be mapped to external IP of 12.34.56.78
// "12.34.56.78/10.1.2.3" => interface IP 10.1.2.3 will be mapped to external IP of 12.34.56.78
NATExternalIPs []string
// CustomDNSAddress sets the DNS resolver listening address in format ip:port
CustomDNSAddress string
}
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
func ReadConfig(configPath string) (*Config, error) {
if configFileIsExists(configPath) {
config := &Config{}
if _, err := util.ReadJson(configPath, config); err != nil {
return nil, err
}
return config, nil
}
cfg, err := createNewConfig(ConfigInput{ConfigPath: configPath})
if err != nil {
return nil, err
}
err = WriteOutConfig(configPath, cfg)
return cfg, err
}
// UpdateConfig update existing configuration according to input configuration and return with the configuration
func UpdateConfig(input ConfigInput) (*Config, error) {
if !configFileIsExists(input.ConfigPath) {
return nil, status.Errorf(codes.NotFound, "config file doesn't exist")
}
return update(input)
}
// UpdateOrCreateConfig reads existing config or generates a new one
func UpdateOrCreateConfig(input ConfigInput) (*Config, error) {
if !configFileIsExists(input.ConfigPath) {
log.Infof("generating new config %s", input.ConfigPath)
cfg, err := createNewConfig(input)
if err != nil {
return nil, err
}
err = WriteOutConfig(input.ConfigPath, cfg)
return cfg, err
}
if isPreSharedKeyHidden(input.PreSharedKey) {
input.PreSharedKey = nil
}
return update(input)
}
// CreateInMemoryConfig generate a new config but do not write out it to the store
func CreateInMemoryConfig(input ConfigInput) (*Config, error) {
return createNewConfig(input)
}
// WriteOutConfig write put the prepared config to the given path
func WriteOutConfig(path string, config *Config) error {
return util.WriteJson(path, config)
}
// createNewConfig creates a new config generating a new Wireguard key and saving to file
func createNewConfig(input ConfigInput) (*Config, error) {
wgKey := generateKey()
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
if err != nil {
return nil, err
}
config := &Config{
SSHKey: string(pem),
PrivateKey: wgKey,
WgIface: iface.WgInterfaceDefault,
WgPort: iface.DefaultWgPort,
IFaceBlackList: []string{},
DisableIPv6Discovery: false,
NATExternalIPs: input.NATExternalIPs,
CustomDNSAddress: string(input.CustomDNSAddress),
}
defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
if err != nil {
return nil, err
}
config.ManagementURL = defaultManagementURL
if input.ManagementURL != "" {
URL, err := parseURL("Management URL", input.ManagementURL)
if err != nil {
return nil, err
}
config.ManagementURL = URL
}
if input.PreSharedKey != nil {
config.PreSharedKey = *input.PreSharedKey
}
defaultAdminURL, err := parseURL("Admin URL", DefaultAdminURL)
if err != nil {
return nil, err
}
config.AdminURL = defaultAdminURL
if input.AdminURL != "" {
newURL, err := parseURL("Admin Panel URL", input.AdminURL)
if err != nil {
return nil, err
}
config.AdminURL = newURL
}
config.IFaceBlackList = defaultInterfaceBlacklist
return config, nil
}
func update(input ConfigInput) (*Config, error) {
config := &Config{}
if _, err := util.ReadJson(input.ConfigPath, config); err != nil {
return nil, err
}
refresh := false
if input.ManagementURL != "" && config.ManagementURL.String() != input.ManagementURL {
log.Infof("new Management URL provided, updated to %s (old value %s)",
input.ManagementURL, config.ManagementURL)
newURL, err := parseURL("Management URL", input.ManagementURL)
if err != nil {
return nil, err
}
config.ManagementURL = newURL
refresh = true
}
if input.AdminURL != "" && (config.AdminURL == nil || config.AdminURL.String() != input.AdminURL) {
log.Infof("new Admin Panel URL provided, updated to %s (old value %s)",
input.AdminURL, config.AdminURL)
newURL, err := parseURL("Admin Panel URL", input.AdminURL)
if err != nil {
return nil, err
}
config.AdminURL = newURL
refresh = true
}
if input.PreSharedKey != nil && config.PreSharedKey != *input.PreSharedKey {
log.Infof("new pre-shared key provided, updated to %s (old value %s)",
*input.PreSharedKey, config.PreSharedKey)
config.PreSharedKey = *input.PreSharedKey
refresh = true
}
if config.SSHKey == "" {
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
if err != nil {
return nil, err
}
config.SSHKey = string(pem)
refresh = true
}
if config.WgPort == 0 {
config.WgPort = iface.DefaultWgPort
refresh = true
}
if input.NATExternalIPs != nil && len(config.NATExternalIPs) != len(input.NATExternalIPs) {
config.NATExternalIPs = input.NATExternalIPs
refresh = true
}
if input.CustomDNSAddress != nil {
config.CustomDNSAddress = string(input.CustomDNSAddress)
refresh = true
}
if refresh {
// since we have new management URL, we need to update config file
if err := util.WriteJson(input.ConfigPath, config); err != nil {
return nil, err
}
}
return config, nil
}
// parseURL parses and validates a service URL
func parseURL(serviceName, serviceURL string) (*url.URL, error) {
parsedMgmtURL, err := url.ParseRequestURI(serviceURL)
if err != nil {
log.Errorf("failed parsing %s URL %s: [%s]", serviceName, serviceURL, err.Error())
return nil, err
}
if parsedMgmtURL.Scheme != "https" && parsedMgmtURL.Scheme != "http" {
return nil, fmt.Errorf(
"invalid %s URL provided %s. Supported format [http|https]://[host]:[port]",
serviceName, serviceURL)
}
if parsedMgmtURL.Port() == "" {
switch parsedMgmtURL.Scheme {
case "https":
parsedMgmtURL.Host = parsedMgmtURL.Host + ":443"
case "http":
parsedMgmtURL.Host = parsedMgmtURL.Host + ":80"
default:
log.Infof("unable to determine a default port for schema %s in URL %s", parsedMgmtURL.Scheme, serviceURL)
}
}
return parsedMgmtURL, err
}
// generateKey generates a new Wireguard private key
func generateKey() string {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
panic(err)
}
return key.String()
}
// don't overwrite pre-shared key if we receive asterisks from UI
func isPreSharedKeyHidden(preSharedKey *string) bool {
if preSharedKey != nil && *preSharedKey == "**********" {
return true
}
return false
}
func configFileIsExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}

View File

@@ -0,0 +1,124 @@
package internal
import (
"errors"
"os"
"path/filepath"
"testing"
"github.com/netbirdio/netbird/util"
"github.com/stretchr/testify/assert"
)
func TestGetConfig(t *testing.T) {
// case 1: new default config has to be generated
config, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
})
if err != nil {
return
}
assert.Equal(t, config.ManagementURL.String(), DefaultManagementURL)
assert.Equal(t, config.AdminURL.String(), DefaultAdminURL)
if err != nil {
return
}
managementURL := "https://test.management.url:33071"
adminURL := "https://app.admin.url:443"
path := filepath.Join(t.TempDir(), "config.json")
preSharedKey := "preSharedKey"
// case 2: new config has to be generated
config, err = UpdateOrCreateConfig(ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: path,
PreSharedKey: &preSharedKey,
})
if err != nil {
return
}
assert.Equal(t, config.ManagementURL.String(), managementURL)
assert.Equal(t, config.PreSharedKey, preSharedKey)
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
t.Errorf("config file was expected to be created under path %s", path)
}
// case 3: existing config -> fetch it
config, err = UpdateOrCreateConfig(ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: path,
PreSharedKey: &preSharedKey,
})
if err != nil {
return
}
assert.Equal(t, config.ManagementURL.String(), managementURL)
assert.Equal(t, config.PreSharedKey, preSharedKey)
// case 4: existing config, but new managementURL has been provided -> update config
newManagementURL := "https://test.newManagement.url:33071"
config, err = UpdateOrCreateConfig(ConfigInput{
ManagementURL: newManagementURL,
AdminURL: adminURL,
ConfigPath: path,
PreSharedKey: &preSharedKey,
})
if err != nil {
return
}
assert.Equal(t, config.ManagementURL.String(), newManagementURL)
assert.Equal(t, config.PreSharedKey, preSharedKey)
// read once more to make sure that config file has been updated with the new management URL
readConf, err := util.ReadJson(path, config)
if err != nil {
return
}
assert.Equal(t, readConf.(*Config).ManagementURL.String(), newManagementURL)
}
func TestHiddenPreSharedKey(t *testing.T) {
hidden := "**********"
samplePreSharedKey := "mysecretpresharedkey"
tests := []struct {
name string
preSharedKey *string
want string
}{
{"nil", nil, ""},
{"hidden", &hidden, ""},
{"filled", &samplePreSharedKey, samplePreSharedKey},
}
// generate default cfg
cfgFile := filepath.Join(t.TempDir(), "config.json")
_, _ = UpdateOrCreateConfig(ConfigInput{
ConfigPath: cfgFile,
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: cfgFile,
PreSharedKey: tt.preSharedKey,
})
if err != nil {
t.Fatalf("failed to get cfg: %s", err)
}
if cfg.PreSharedKey != tt.want {
t.Fatalf("invalid preshared key: '%s', expected: '%s' ", cfg.PreSharedKey, tt.want)
}
})
}
}

347
client/internal/connect.go Normal file
View File

@@ -0,0 +1,347 @@
package internal
import (
"context"
"fmt"
"strings"
"time"
"github.com/cenkalti/backoff/v4"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/iface"
mgm "github.com/netbirdio/netbird/management/client"
mgmProto "github.com/netbirdio/netbird/management/proto"
signal "github.com/netbirdio/netbird/signal/client"
)
// RunClient with main logic.
func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.IFaceDiscover) error {
backOff := &backoff.ExponentialBackOff{
InitialInterval: time.Second,
RandomizationFactor: 1,
Multiplier: 1.7,
MaxInterval: 15 * time.Second,
MaxElapsedTime: 3 * 30 * 24 * time.Hour, // 3 months
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}
state := CtxGetState(ctx)
defer func() {
s, err := state.Status()
if err != nil || s != StatusNeedsLogin {
state.Set(StatusIdle)
}
}()
wrapErr := state.Wrap
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
if err != nil {
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
return wrapErr(err)
}
var mgmTlsEnabled bool
if config.ManagementURL.Scheme == "https" {
mgmTlsEnabled = true
}
publicSSHKey, err := ssh.GeneratePublicKey([]byte(config.SSHKey))
if err != nil {
return err
}
statusRecorder.MarkManagementDisconnected()
statusRecorder.ClientStart()
defer statusRecorder.ClientStop()
operation := func() error {
// if context cancelled we not start new backoff cycle
select {
case <-ctx.Done():
return nil
default:
}
state.Set(StatusConnecting)
engineCtx, cancel := context.WithCancel(ctx)
defer func() {
statusRecorder.MarkManagementDisconnected()
statusRecorder.CleanLocalPeerState()
cancel()
}()
log.Debugf("conecting to the Management service %s", config.ManagementURL.Host)
mgmClient, err := mgm.NewClient(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
mgmNotifier := statusRecorderToMgmConnStateNotifier(statusRecorder)
mgmClient.SetConnStateListener(mgmNotifier)
if err != nil {
return wrapErr(gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err))
}
log.Debugf("connected to the Management service %s", config.ManagementURL.Host)
defer func() {
err = mgmClient.Close()
if err != nil {
log.Warnf("failed to close the Management service client %v", err)
}
}()
// connect (just a connection, no stream yet) and login to Management Service to get an initial global Wiretrustee config
loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey)
if err != nil {
log.Debug(err)
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
state.Set(StatusNeedsLogin)
return backoff.Permanent(wrapErr(err)) // unrecoverable error
}
return wrapErr(err)
}
statusRecorder.MarkManagementConnected()
localPeerState := peer.LocalPeerState{
IP: loginResp.GetPeerConfig().GetAddress(),
PubKey: myPrivateKey.PublicKey().String(),
KernelInterface: iface.WireguardModuleIsLoaded(),
FQDN: loginResp.GetPeerConfig().GetFqdn(),
}
statusRecorder.UpdateLocalPeerState(localPeerState)
signalURL := fmt.Sprintf("%s://%s",
strings.ToLower(loginResp.GetWiretrusteeConfig().GetSignal().GetProtocol().String()),
loginResp.GetWiretrusteeConfig().GetSignal().GetUri(),
)
statusRecorder.UpdateSignalAddress(signalURL)
statusRecorder.MarkSignalDisconnected()
defer statusRecorder.MarkSignalDisconnected()
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
if err != nil {
log.Error(err)
return wrapErr(err)
}
defer func() {
err = signalClient.Close()
if err != nil {
log.Warnf("failed closing Signal service client %v", err)
}
}()
signalNotifier := statusRecorderToSignalConnStateNotifier(statusRecorder)
signalClient.SetConnStateListener(signalNotifier)
statusRecorder.MarkSignalConnected()
peerConfig := loginResp.GetPeerConfig()
engineConfig, err := createEngineConfig(myPrivateKey, config, peerConfig, tunAdapter, iFaceDiscover)
if err != nil {
log.Error(err)
return wrapErr(err)
}
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig, statusRecorder)
err = engine.Start()
if err != nil {
log.Errorf("error while starting Netbird Connection Engine: %s", err)
return wrapErr(err)
}
log.Print("Netbird engine started, my IP is: ", peerConfig.Address)
state.Set(StatusConnected)
<-engineCtx.Done()
statusRecorder.ClientTeardown()
backOff.Reset()
err = engine.Stop()
if err != nil {
log.Errorf("failed stopping engine %v", err)
return wrapErr(err)
}
log.Info("stopped NetBird client")
if _, err := state.Status(); err == ErrResetConnection {
return err
}
return nil
}
err = backoff.Retry(operation, backOff)
if err != nil {
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
state.Set(StatusNeedsLogin)
}
return err
}
return nil
}
// createEngineConfig converts configuration received from Management Service to EngineConfig
func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.IFaceDiscover) (*EngineConfig, error) {
engineConf := &EngineConfig{
WgIfaceName: config.WgIface,
WgAddr: peerConfig.Address,
TunAdapter: tunAdapter,
IFaceDiscover: iFaceDiscover,
IFaceBlackList: config.IFaceBlackList,
DisableIPv6Discovery: config.DisableIPv6Discovery,
WgPrivateKey: key,
WgPort: config.WgPort,
SSHKey: []byte(config.SSHKey),
NATExternalIPs: config.NATExternalIPs,
CustomDNSAddress: config.CustomDNSAddress,
}
if config.PreSharedKey != "" {
preSharedKey, err := wgtypes.ParseKey(config.PreSharedKey)
if err != nil {
return nil, err
}
engineConf.PreSharedKey = &preSharedKey
}
return engineConf, nil
}
// connectToSignal creates Signal Service client and established a connection
func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig, ourPrivateKey wgtypes.Key) (*signal.GrpcClient, error) {
var sigTLSEnabled bool
if wtConfig.Signal.Protocol == mgmProto.HostConfig_HTTPS {
sigTLSEnabled = true
} else {
sigTLSEnabled = false
}
signalClient, err := signal.NewClient(ctx, wtConfig.Signal.Uri, ourPrivateKey, sigTLSEnabled)
if err != nil {
log.Errorf("error while connecting to the Signal Exchange Service %s: %s", wtConfig.Signal.Uri, err)
return nil, gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Signal Service : %s", err)
}
return signalClient, nil
}
// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Wiretrustee config (signal, turn, stun hosts, etc)
func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte) (*mgmProto.LoginResponse, error) {
serverPublicKey, err := client.GetServerPublicKey()
if err != nil {
return nil, gstatus.Errorf(codes.FailedPrecondition, "failed while getting Management Service public key: %s", err)
}
sysInfo := system.GetInfo(ctx)
loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey)
if err != nil {
return nil, err
}
return loginResp, nil
}
// UpdateOldManagementPort checks whether client can switch to the new Management port 443.
// If it can switch, then it updates the config and returns a new one. Otherwise, it returns the provided config.
// The check is performed only for the NetBird's managed version.
func UpdateOldManagementPort(ctx context.Context, config *Config, configPath string) (*Config, error) {
defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
if err != nil {
return nil, err
}
if config.ManagementURL.Hostname() != defaultManagementURL.Hostname() {
// only do the check for the NetBird's managed version
return config, nil
}
var mgmTlsEnabled bool
if config.ManagementURL.Scheme == "https" {
mgmTlsEnabled = true
}
if !mgmTlsEnabled {
// only do the check for HTTPs scheme (the hosted version of the Management service is always HTTPs)
return config, nil
}
if mgmTlsEnabled && config.ManagementURL.Port() == fmt.Sprintf("%d", ManagementLegacyPort) {
newURL, err := parseURL("Management URL", fmt.Sprintf("%s://%s:%d",
config.ManagementURL.Scheme, config.ManagementURL.Hostname(), 443))
if err != nil {
return nil, err
}
// here we check whether we could switch from the legacy 33073 port to the new 443
log.Infof("attempting to switch from the legacy Management URL %s to the new one %s",
config.ManagementURL.String(), newURL.String())
key, err := wgtypes.ParseKey(config.PrivateKey)
if err != nil {
log.Infof("couldn't switch to the new Management %s", newURL.String())
return config, err
}
client, err := mgm.NewClient(ctx, newURL.Host, key, mgmTlsEnabled)
if err != nil {
log.Infof("couldn't switch to the new Management %s", newURL.String())
return config, err
}
defer func() {
err = client.Close()
if err != nil {
log.Warnf("failed to close the Management service client %v", err)
}
}()
// gRPC check
_, err = client.GetServerPublicKey()
if err != nil {
log.Infof("couldn't switch to the new Management %s", newURL.String())
return nil, err
}
// everything is alright => update the config
newConfig, err := UpdateConfig(ConfigInput{
ManagementURL: newURL.String(),
ConfigPath: configPath,
})
if err != nil {
log.Infof("couldn't switch to the new Management %s", newURL.String())
return config, fmt.Errorf("failed updating config file: %v", err)
}
log.Infof("successfully switched to the new Management URL: %s", newURL.String())
return newConfig, nil
}
return config, nil
}
func statusRecorderToMgmConnStateNotifier(statusRecorder *peer.Status) mgm.ConnStateNotifier {
var sri interface{} = statusRecorder
mgmNotifier, _ := sri.(mgm.ConnStateNotifier)
return mgmNotifier
}
func statusRecorderToSignalConnStateNotifier(statusRecorder *peer.Status) signal.ConnStateNotifier {
var sri interface{} = statusRecorder
notifier, _ := sri.(signal.ConnStateNotifier)
return notifier
}

View File

@@ -0,0 +1,120 @@
package internal
import (
"context"
"fmt"
"net/url"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
mgm "github.com/netbirdio/netbird/management/client"
)
// DeviceAuthorizationFlow represents Device Authorization Flow information
type DeviceAuthorizationFlow struct {
Provider string
ProviderConfig ProviderConfig
}
// ProviderConfig has all attributes needed to initiate a device authorization flow
type ProviderConfig struct {
// ClientID An IDP application client id
ClientID string
// ClientSecret An IDP application client secret
ClientSecret string
// Domain An IDP API domain
// Deprecated. Use OIDCConfigEndpoint instead
Domain string
// Audience An Audience for to authorization validation
Audience string
// TokenEndpoint is the endpoint of an IDP manager where clients can obtain access token
TokenEndpoint string
// DeviceAuthEndpoint is the endpoint of an IDP manager where clients can obtain device authorization code
DeviceAuthEndpoint string
}
// GetDeviceAuthorizationFlowInfo initialize a DeviceAuthorizationFlow instance and return with it
func GetDeviceAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmURL *url.URL) (DeviceAuthorizationFlow, error) {
// validate our peer's Wireguard PRIVATE key
myPrivateKey, err := wgtypes.ParseKey(privateKey)
if err != nil {
log.Errorf("failed parsing Wireguard key %s: [%s]", privateKey, err.Error())
return DeviceAuthorizationFlow{}, err
}
var mgmTLSEnabled bool
if mgmURL.Scheme == "https" {
mgmTLSEnabled = true
}
log.Debugf("connecting to Management Service %s", mgmURL.String())
mgmClient, err := mgm.NewClient(ctx, mgmURL.Host, myPrivateKey, mgmTLSEnabled)
if err != nil {
log.Errorf("failed connecting to Management Service %s %v", mgmURL.String(), err)
return DeviceAuthorizationFlow{}, err
}
log.Debugf("connected to the Management service %s", mgmURL.String())
defer func() {
err = mgmClient.Close()
if err != nil {
log.Warnf("failed to close the Management service client %v", err)
}
}()
serverKey, err := mgmClient.GetServerPublicKey()
if err != nil {
log.Errorf("failed while getting Management Service public key: %v", err)
return DeviceAuthorizationFlow{}, err
}
protoDeviceAuthorizationFlow, err := mgmClient.GetDeviceAuthorizationFlow(*serverKey)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
log.Warnf("server couldn't find device flow, contact admin: %v", err)
return DeviceAuthorizationFlow{}, err
}
log.Errorf("failed to retrieve device flow: %v", err)
return DeviceAuthorizationFlow{}, err
}
deviceAuthorizationFlow := DeviceAuthorizationFlow{
Provider: protoDeviceAuthorizationFlow.Provider.String(),
ProviderConfig: ProviderConfig{
Audience: protoDeviceAuthorizationFlow.GetProviderConfig().GetAudience(),
ClientID: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientID(),
ClientSecret: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientSecret(),
Domain: protoDeviceAuthorizationFlow.GetProviderConfig().Domain,
TokenEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetTokenEndpoint(),
DeviceAuthEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetDeviceAuthEndpoint(),
},
}
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
}

View File

@@ -0,0 +1,41 @@
package dns
import (
"context"
"github.com/godbus/dbus/v5"
log "github.com/sirupsen/logrus"
"time"
)
const dbusDefaultFlag = 0
func isDbusListenerRunning(dest string, path dbus.ObjectPath) bool {
obj, closeConn, err := getDbusObject(dest, path)
if err != nil {
return false
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Store()
return err == nil
}
func getDbusObject(dest string, path dbus.ObjectPath) (dbus.BusObject, func(), error) {
conn, err := dbus.SystemBus()
if err != nil {
return nil, nil, err
}
obj := conn.Object(dest, path)
closeFunc := func() {
closeErr := conn.Close()
if closeErr != nil {
log.Warnf("got an error closing dbus connection, err: %s", closeErr)
}
}
return obj, closeFunc, nil
}

View File

@@ -0,0 +1,156 @@
package dns
import (
"bytes"
"fmt"
"os"
log "github.com/sirupsen/logrus"
)
const (
fileGeneratedResolvConfContentHeader = "# Generated by NetBird"
fileGeneratedResolvConfSearchBeginContent = "search "
fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader +
"\n# If needed you can restore the original file by copying back %s\n\nnameserver %s\n" +
fileGeneratedResolvConfSearchBeginContent + "%s\n"
)
const (
fileDefaultResolvConfBackupLocation = defaultResolvConfPath + ".original.netbird"
fileMaxLineCharsLimit = 256
fileMaxNumberOfSearchDomains = 6
)
var fileSearchLineBeginCharCount = len(fileGeneratedResolvConfSearchBeginContent)
type fileConfigurator struct {
originalPerms os.FileMode
}
func newFileConfigurator() (hostManager, error) {
return &fileConfigurator{}, nil
}
func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error {
backupFileExist := false
_, err := os.Stat(fileDefaultResolvConfBackupLocation)
if err == nil {
backupFileExist = true
}
if !config.routeAll {
if backupFileExist {
err = f.restore()
if err != nil {
return fmt.Errorf("unable to configure DNS for this peer using file manager without a Primary nameserver group. Restoring the original file return err: %s", err)
}
}
return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured")
}
managerType, err := getOSDNSManagerType()
if err != nil {
return err
}
switch managerType {
case fileManager, netbirdManager:
if !backupFileExist {
err = f.backup()
if err != nil {
return fmt.Errorf("unable to backup the resolv.conf file")
}
}
default:
// todo improve this and maybe restart DNS manager from scratch
return fmt.Errorf("something happened and file manager is not your prefered host dns configurator, restart the agent")
}
var searchDomains string
appendedDomains := 0
for _, dConf := range config.domains {
if dConf.matchOnly || dConf.disabled {
continue
}
if appendedDomains >= fileMaxNumberOfSearchDomains {
// lets log all skipped domains
log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, dConf.domain)
continue
}
if fileSearchLineBeginCharCount+len(searchDomains) > fileMaxLineCharsLimit {
// lets log all skipped domains
log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, dConf.domain)
continue
}
searchDomains += " " + dConf.domain
appendedDomains++
}
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains)
err = writeDNSConfig(content, defaultResolvConfPath, f.originalPerms)
if err != nil {
err = f.restore()
if err != nil {
log.Errorf("attempt to restore default file failed with error: %s", err)
}
return err
}
log.Infof("created a NetBird managed %s file with your DNS settings. Added %d search domains. Search list: %s", defaultResolvConfPath, appendedDomains, searchDomains)
return nil
}
func (f *fileConfigurator) restoreHostDNS() error {
return f.restore()
}
func (f *fileConfigurator) backup() error {
stats, err := os.Stat(defaultResolvConfPath)
if err != nil {
return fmt.Errorf("got an error while checking stats for %s file. Error: %s", defaultResolvConfPath, err)
}
f.originalPerms = stats.Mode()
err = copyFile(defaultResolvConfPath, fileDefaultResolvConfBackupLocation)
if err != nil {
return fmt.Errorf("got error while backing up the %s file. Error: %s", defaultResolvConfPath, err)
}
return nil
}
func (f *fileConfigurator) restore() error {
err := copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath)
if err != nil {
return fmt.Errorf("got error while restoring the %s file from %s. Error: %s", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err)
}
return os.RemoveAll(fileDefaultResolvConfBackupLocation)
}
func writeDNSConfig(content, fileName string, permissions os.FileMode) error {
log.Debugf("creating managed file %s", fileName)
var buf bytes.Buffer
buf.WriteString(content)
err := os.WriteFile(fileName, buf.Bytes(), permissions)
if err != nil {
return fmt.Errorf("got an creating resolver file %s. Error: %s", fileName, err)
}
return nil
}
func copyFile(src, dest string) error {
stats, err := os.Stat(src)
if err != nil {
return fmt.Errorf("got an error while checking stats for %s file when copying it. Error: %s", src, err)
}
bytesRead, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("got an error while reading the file %s file for copy. Error: %s", src, err)
}
err = os.WriteFile(dest, bytesRead, stats.Mode())
if err != nil {
return fmt.Errorf("got an writing the destination file %s for copy. Error: %s", dest, err)
}
return nil
}

View File

@@ -0,0 +1,84 @@
package dns
import (
"fmt"
"strings"
nbdns "github.com/netbirdio/netbird/dns"
)
type hostManager interface {
applyDNSConfig(config hostDNSConfig) error
restoreHostDNS() error
}
type hostDNSConfig struct {
domains []domainConfig
routeAll bool
serverIP string
serverPort int
}
type domainConfig struct {
disabled bool
domain string
matchOnly bool
}
type mockHostConfigurator struct {
applyDNSConfigFunc func(config hostDNSConfig) error
restoreHostDNSFunc func() error
}
func (m *mockHostConfigurator) applyDNSConfig(config hostDNSConfig) error {
if m.applyDNSConfigFunc != nil {
return m.applyDNSConfigFunc(config)
}
return fmt.Errorf("method applyDNSSettings is not implemented")
}
func (m *mockHostConfigurator) restoreHostDNS() error {
if m.restoreHostDNSFunc != nil {
return m.restoreHostDNSFunc()
}
return fmt.Errorf("method restoreHostDNS is not implemented")
}
func newNoopHostMocker() hostManager {
return &mockHostConfigurator{
applyDNSConfigFunc: func(config hostDNSConfig) error { return nil },
restoreHostDNSFunc: func() error { return nil },
}
}
func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) hostDNSConfig {
config := hostDNSConfig{
routeAll: false,
serverIP: ip,
serverPort: port,
}
for _, nsConfig := range dnsConfig.NameServerGroups {
if len(nsConfig.NameServers) == 0 {
continue
}
if nsConfig.Primary {
config.routeAll = true
}
for _, domain := range nsConfig.Domains {
config.domains = append(config.domains, domainConfig{
domain: strings.TrimSuffix(domain, "."),
matchOnly: true,
})
}
}
for _, customZone := range dnsConfig.CustomZones {
config.domains = append(config.domains, domainConfig{
domain: strings.TrimSuffix(customZone.Domain, "."),
matchOnly: false,
})
}
return config
}

View File

@@ -0,0 +1,263 @@
package dns
import (
"bufio"
"bytes"
"fmt"
"os/exec"
"strconv"
"strings"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
)
const (
netbirdDNSStateKeyFormat = "State:/Network/Service/NetBird-%s/DNS"
globalIPv4State = "State:/Network/Global/IPv4"
primaryServiceSetupKeyFormat = "Setup:/Network/Service/%s/DNS"
keySupplementalMatchDomains = "SupplementalMatchDomains"
keySupplementalMatchDomainsNoSearch = "SupplementalMatchDomainsNoSearch"
keyServerAddresses = "ServerAddresses"
keyServerPort = "ServerPort"
arraySymbol = "* "
digitSymbol = "# "
scutilPath = "/usr/sbin/scutil"
searchSuffix = "Search"
matchSuffix = "Match"
)
type systemConfigurator struct {
// primaryServiceID primary interface in the system. AKA the interface with the default route
primaryServiceID string
createdKeys map[string]struct{}
}
func newHostManager(_ *iface.WGIface) (hostManager, error) {
return &systemConfigurator{
createdKeys: make(map[string]struct{}),
}, nil
}
func (s *systemConfigurator) applyDNSConfig(config hostDNSConfig) error {
var err error
if config.routeAll {
err = s.addDNSSetupForAll(config.serverIP, config.serverPort)
if err != nil {
return err
}
} else if s.primaryServiceID != "" {
err = s.removeKeyFromSystemConfig(getKeyWithInput(primaryServiceSetupKeyFormat, s.primaryServiceID))
if err != nil {
return err
}
s.primaryServiceID = ""
log.Infof("removed %s:%d as main DNS resolver for this peer", config.serverIP, config.serverPort)
}
var (
searchDomains []string
matchDomains []string
)
for _, dConf := range config.domains {
if dConf.disabled {
continue
}
if dConf.matchOnly {
matchDomains = append(matchDomains, dConf.domain)
continue
}
searchDomains = append(searchDomains, dConf.domain)
}
matchKey := getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)
if len(matchDomains) != 0 {
err = s.addMatchDomains(matchKey, strings.Join(matchDomains, " "), config.serverIP, config.serverPort)
} else {
log.Infof("removing match domains from the system")
err = s.removeKeyFromSystemConfig(matchKey)
}
if err != nil {
return err
}
searchKey := getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix)
if len(searchDomains) != 0 {
err = s.addSearchDomains(searchKey, strings.Join(searchDomains, " "), config.serverIP, config.serverPort)
} else {
log.Infof("removing search domains from the system")
err = s.removeKeyFromSystemConfig(searchKey)
}
if err != nil {
return err
}
return nil
}
func (s *systemConfigurator) restoreHostDNS() error {
lines := ""
for key := range s.createdKeys {
lines += buildRemoveKeyOperation(key)
keyType := "search"
if strings.Contains(key, matchSuffix) {
keyType = "match"
}
log.Infof("removing %s domains from system", keyType)
}
if s.primaryServiceID != "" {
lines += buildRemoveKeyOperation(getKeyWithInput(primaryServiceSetupKeyFormat, s.primaryServiceID))
log.Infof("restoring DNS resolver configuration for system")
}
_, err := runSystemConfigCommand(wrapCommand(lines))
if err != nil {
log.Errorf("got an error while cleaning the system configuration: %s", err)
return err
}
return nil
}
func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error {
line := buildRemoveKeyOperation(key)
_, err := runSystemConfigCommand(wrapCommand(line))
if err != nil {
return err
}
delete(s.createdKeys, key)
return nil
}
func (s *systemConfigurator) addSearchDomains(key, domains string, ip string, port int) error {
err := s.addDNSState(key, domains, ip, port, true)
if err != nil {
return err
}
log.Infof("added %d search domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains)
s.createdKeys[key] = struct{}{}
return nil
}
func (s *systemConfigurator) addMatchDomains(key, domains, dnsServer string, port int) error {
err := s.addDNSState(key, domains, dnsServer, port, false)
if err != nil {
return err
}
log.Infof("added %d match domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains)
s.createdKeys[key] = struct{}{}
return nil
}
func (s *systemConfigurator) addDNSState(state, domains, dnsServer string, port int, enableSearch bool) error {
noSearch := "1"
if enableSearch {
noSearch = "0"
}
lines := buildAddCommandLine(keySupplementalMatchDomains, arraySymbol+domains)
lines += buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+noSearch)
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer)
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
addDomainCommand := buildCreateStateWithOperation(state, lines)
stdinCommands := wrapCommand(addDomainCommand)
_, err := runSystemConfigCommand(stdinCommands)
if err != nil {
return fmt.Errorf("got error while applying state for domains %s, error: %s", domains, err)
}
return nil
}
func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error {
primaryServiceKey := s.getPrimaryService()
if primaryServiceKey == "" {
return fmt.Errorf("couldn't find the primary service key")
}
err := s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port)
if err != nil {
return err
}
log.Infof("configured %s:%d as main DNS resolver for this peer", dnsServer, port)
s.primaryServiceID = primaryServiceKey
return nil
}
func (s *systemConfigurator) getPrimaryService() string {
line := buildCommandLine("show", globalIPv4State, "")
stdinCommands := wrapCommand(line)
b, err := runSystemConfigCommand(stdinCommands)
if err != nil {
log.Error("got error while sending the command: ", err)
return ""
}
scanner := bufio.NewScanner(bytes.NewReader(b))
for scanner.Scan() {
text := scanner.Text()
if strings.Contains(text, "PrimaryService") {
return strings.TrimSpace(strings.Split(text, ":")[1])
}
}
return ""
}
func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int) error {
lines := buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+strconv.Itoa(0))
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer)
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
addDomainCommand := buildCreateStateWithOperation(setupKey, lines)
stdinCommands := wrapCommand(addDomainCommand)
_, err := runSystemConfigCommand(stdinCommands)
if err != nil {
return fmt.Errorf("got error while applying dns setup, error: %s", err)
}
return nil
}
func getKeyWithInput(format, key string) string {
return fmt.Sprintf(format, key)
}
func buildAddCommandLine(key, value string) string {
return buildCommandLine("d.add", key, value)
}
func buildCommandLine(action, key, value string) string {
return fmt.Sprintf("%s %s %s\n", action, key, value)
}
func wrapCommand(commands string) string {
return fmt.Sprintf("open\n%s\nquit\n", commands)
}
func buildRemoveKeyOperation(key string) string {
return fmt.Sprintf("remove %s\n", key)
}
func buildCreateStateWithOperation(state, commands string) string {
return buildWriteStateOperation("set", state, commands)
}
func buildWriteStateOperation(operation, state, commands string) string {
return fmt.Sprintf("d.init\n%s %s\n%s\nset %s\n", operation, state, commands, state)
}
func runSystemConfigCommand(command string) ([]byte, error) {
cmd := exec.Command(scutilPath)
cmd.Stdin = strings.NewReader(command)
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("got error while running system configuration command: \"%s\", error: %s", command, err)
}
return out, nil
}

View File

@@ -0,0 +1,87 @@
package dns
import (
"bufio"
"fmt"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
"os"
"strings"
)
const (
defaultResolvConfPath = "/etc/resolv.conf"
)
const (
netbirdManager osManagerType = iota
fileManager
networkManager
systemdManager
resolvConfManager
)
type osManagerType int
func newHostManager(wgInterface *iface.WGIface) (hostManager, error) {
osManager, err := getOSDNSManagerType()
if err != nil {
return nil, err
}
log.Debugf("discovered mode is: %d", osManager)
switch osManager {
case networkManager:
return newNetworkManagerDbusConfigurator(wgInterface)
case systemdManager:
return newSystemdDbusConfigurator(wgInterface)
case resolvConfManager:
return newResolvConfConfigurator(wgInterface)
default:
return newFileConfigurator()
}
}
func getOSDNSManagerType() (osManagerType, error) {
file, err := os.Open(defaultResolvConfPath)
if err != nil {
return 0, fmt.Errorf("unable to open %s for checking owner, got error: %s", defaultResolvConfPath, err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
if len(text) == 0 {
continue
}
if text[0] != '#' {
return fileManager, nil
}
if strings.Contains(text, fileGeneratedResolvConfContentHeader) {
return netbirdManager, nil
}
if strings.Contains(text, "NetworkManager") && isDbusListenerRunning(networkManagerDest, networkManagerDbusObjectNode) && isNetworkManagerSupported() {
log.Debugf("is nm running on supported v? %t", isNetworkManagerSupportedVersion())
return networkManager, nil
}
if strings.Contains(text, "systemd-resolved") && isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) {
return systemdManager, nil
}
if strings.Contains(text, "resolvconf") {
if isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) {
var value string
err = getSystemdDbusProperty(systemdDbusResolvConfModeProperty, &value)
if err == nil {
if value == systemdDbusResolvConfModeForeign {
return systemdManager, nil
}
}
log.Errorf("got an error while checking systemd resolv conf mode, error: %s", err)
}
return resolvConfManager, nil
}
}
return fileManager, nil
}

View File

@@ -0,0 +1,264 @@
package dns
import (
"fmt"
"strings"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/windows/registry"
)
const (
dnsPolicyConfigMatchPath = "SYSTEM\\CurrentControlSet\\Services\\Dnscache\\Parameters\\DnsPolicyConfig\\NetBird-Match"
dnsPolicyConfigVersionKey = "Version"
dnsPolicyConfigVersionValue = 2
dnsPolicyConfigNameKey = "Name"
dnsPolicyConfigGenericDNSServersKey = "GenericDNSServers"
dnsPolicyConfigConfigOptionsKey = "ConfigOptions"
dnsPolicyConfigConfigOptionsValue = 0x8
)
const (
interfaceConfigPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces"
interfaceConfigNameServerKey = "NameServer"
interfaceConfigSearchListKey = "SearchList"
tcpipParametersPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters"
)
type registryConfigurator struct {
guid string
routingAll bool
existingSearchDomains []string
}
func newHostManager(wgInterface *iface.WGIface) (hostManager, error) {
guid, err := wgInterface.GetInterfaceGUIDString()
if err != nil {
return nil, err
}
return &registryConfigurator{
guid: guid,
}, nil
}
func (r *registryConfigurator) applyDNSConfig(config hostDNSConfig) error {
var err error
if config.routeAll {
err = r.addDNSSetupForAll(config.serverIP)
if err != nil {
return err
}
} else if r.routingAll {
err = r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey)
if err != nil {
return err
}
r.routingAll = false
log.Infof("removed %s as main DNS forwarder for this peer", config.serverIP)
}
var (
searchDomains []string
matchDomains []string
)
for _, dConf := range config.domains {
if dConf.disabled {
continue
}
if !dConf.matchOnly {
searchDomains = append(searchDomains, dConf.domain)
}
matchDomains = append(matchDomains, "."+dConf.domain)
}
if len(matchDomains) != 0 {
err = r.addDNSMatchPolicy(matchDomains, config.serverIP)
} else {
err = removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath)
}
if err != nil {
return err
}
err = r.updateSearchDomains(searchDomains)
if err != nil {
return err
}
return nil
}
func (r *registryConfigurator) addDNSSetupForAll(ip string) error {
err := r.setInterfaceRegistryKeyStringValue(interfaceConfigNameServerKey, ip)
if err != nil {
return fmt.Errorf("adding dns setup for all failed with error: %s", err)
}
r.routingAll = true
log.Infof("configured %s:53 as main DNS forwarder for this peer", ip)
return nil
}
func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) error {
_, err := registry.OpenKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath, registry.QUERY_VALUE)
if err == nil {
err = registry.DeleteKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath)
if err != nil {
return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %s", dnsPolicyConfigMatchPath, err)
}
}
regKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath, registry.SET_VALUE)
if err != nil {
return fmt.Errorf("unable to create registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %s", dnsPolicyConfigMatchPath, err)
}
err = regKey.SetDWordValue(dnsPolicyConfigVersionKey, dnsPolicyConfigVersionValue)
if err != nil {
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigVersionKey, err)
}
err = regKey.SetStringsValue(dnsPolicyConfigNameKey, domains)
if err != nil {
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigNameKey, err)
}
err = regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip)
if err != nil {
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigGenericDNSServersKey, err)
}
err = regKey.SetDWordValue(dnsPolicyConfigConfigOptionsKey, dnsPolicyConfigConfigOptionsValue)
if err != nil {
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigConfigOptionsKey, err)
}
log.Infof("added %d match domains to the state. Domain list: %s", len(domains), domains)
return nil
}
func (r *registryConfigurator) restoreHostDNS() error {
err := removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath)
if err != nil {
log.Error(err)
}
return r.updateSearchDomains([]string{})
}
func (r *registryConfigurator) updateSearchDomains(domains []string) error {
value, err := getLocalMachineRegistryKeyStringValue(tcpipParametersPath, interfaceConfigSearchListKey)
if err != nil {
return fmt.Errorf("unable to get current search domains failed with error: %s", err)
}
valueList := strings.Split(value, ",")
setExisting := false
if len(r.existingSearchDomains) == 0 {
r.existingSearchDomains = valueList
setExisting = true
}
if len(domains) == 0 && setExisting {
log.Infof("added %d search domains to the registry. Domain list: %s", len(domains), domains)
return nil
}
newList := append(r.existingSearchDomains, domains...)
err = setLocalMachineRegistryKeyStringValue(tcpipParametersPath, interfaceConfigSearchListKey, strings.Join(newList, ","))
if err != nil {
return fmt.Errorf("adding search domain failed with error: %s", err)
}
log.Infof("updated the search domains in the registry with %d domains. Domain list: %s", len(domains), domains)
return nil
}
func (r *registryConfigurator) setInterfaceRegistryKeyStringValue(key, value string) error {
regKey, err := r.getInterfaceRegistryKey()
if err != nil {
return err
}
defer regKey.Close()
err = regKey.SetStringValue(key, value)
if err != nil {
return fmt.Errorf("applying key %s with value \"%s\" for interface failed with error: %s", key, value, err)
}
return nil
}
func (r *registryConfigurator) deleteInterfaceRegistryKeyProperty(propertyKey string) error {
regKey, err := r.getInterfaceRegistryKey()
if err != nil {
return err
}
defer regKey.Close()
err = regKey.DeleteValue(propertyKey)
if err != nil {
return fmt.Errorf("deleting registry key %s for interface failed with error: %s", propertyKey, err)
}
return nil
}
func (r *registryConfigurator) getInterfaceRegistryKey() (registry.Key, error) {
var regKey registry.Key
regKeyPath := interfaceConfigPath + "\\" + r.guid
regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.SET_VALUE)
if err != nil {
return regKey, fmt.Errorf("unable to open the interface registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %s", regKeyPath, err)
}
return regKey, nil
}
func removeRegistryKeyFromDNSPolicyConfig(regKeyPath string) error {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.QUERY_VALUE)
if err == nil {
k.Close()
err = registry.DeleteKey(registry.LOCAL_MACHINE, regKeyPath)
if err != nil {
return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %s", regKeyPath, err)
}
}
return nil
}
func getLocalMachineRegistryKeyStringValue(keyPath, key string) (string, error) {
regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.QUERY_VALUE)
if err != nil {
return "", fmt.Errorf("unable to open existing key from registry, key path: HKEY_LOCAL_MACHINE\\%s, error: %s", keyPath, err)
}
defer regKey.Close()
val, _, err := regKey.GetStringValue(key)
if err != nil {
return "", fmt.Errorf("getting %s value for key path HKEY_LOCAL_MACHINE\\%s failed with error: %s", key, keyPath, err)
}
return val, nil
}
func setLocalMachineRegistryKeyStringValue(keyPath, key, value string) error {
regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.SET_VALUE)
if err != nil {
return fmt.Errorf("unable to open existing key from registry, key path: HKEY_LOCAL_MACHINE\\%s, error: %s", keyPath, err)
}
defer regKey.Close()
err = regKey.SetStringValue(key, value)
if err != nil {
return fmt.Errorf("setting %s value %s for key path HKEY_LOCAL_MACHINE\\%s failed with error: %s", key, value, keyPath, err)
}
return nil
}

View File

@@ -0,0 +1,68 @@
package dns
import (
"fmt"
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
log "github.com/sirupsen/logrus"
"sync"
)
type registrationMap map[string]struct{}
type localResolver struct {
registeredMap registrationMap
records sync.Map
}
// ServeDNS handles a DNS request
func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
log.Tracef("received question: %#v\n", r.Question[0])
replyMessage := &dns.Msg{}
replyMessage.SetReply(r)
replyMessage.RecursionAvailable = true
replyMessage.Rcode = dns.RcodeSuccess
response := d.lookupRecord(r)
if response != nil {
replyMessage.Answer = append(replyMessage.Answer, response)
}
err := w.WriteMsg(replyMessage)
if err != nil {
log.Debugf("got an error while writing the local resolver response, error: %v", err)
}
}
func (d *localResolver) lookupRecord(r *dns.Msg) dns.RR {
question := r.Question[0]
record, found := d.records.Load(buildRecordKey(question.Name, question.Qclass, question.Qtype))
if !found {
return nil
}
return record.(dns.RR)
}
func (d *localResolver) registerRecord(record nbdns.SimpleRecord) error {
fullRecord, err := dns.NewRR(record.String())
if err != nil {
return err
}
fullRecord.Header().Rdlength = record.Len()
header := fullRecord.Header()
d.records.Store(buildRecordKey(header.Name, header.Class, header.Rrtype), fullRecord)
return nil
}
func (d *localResolver) deleteRecord(recordKey string) {
d.records.Delete(dns.Fqdn(recordKey))
}
func buildRecordKey(name string, class, qType uint16) string {
key := fmt.Sprintf("%s_%d_%d", name, class, qType)
return key
}

View File

@@ -0,0 +1,86 @@
package dns
import (
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
"strings"
"testing"
)
func TestLocalResolver_ServeDNS(t *testing.T) {
recordA := nbdns.SimpleRecord{
Name: "peera.netbird.cloud.",
Type: 1,
Class: nbdns.DefaultClass,
TTL: 300,
RData: "1.2.3.4",
}
recordCNAME := nbdns.SimpleRecord{
Name: "peerb.netbird.cloud.",
Type: 5,
Class: nbdns.DefaultClass,
TTL: 300,
RData: "www.netbird.io",
}
testCases := []struct {
name string
inputRecord nbdns.SimpleRecord
inputMSG *dns.Msg
responseShouldBeNil bool
}{
{
name: "Should Resolve A Record",
inputRecord: recordA,
inputMSG: new(dns.Msg).SetQuestion(recordA.Name, dns.TypeA),
},
{
name: "Should Resolve CNAME Record",
inputRecord: recordCNAME,
inputMSG: new(dns.Msg).SetQuestion(recordCNAME.Name, dns.TypeCNAME),
},
{
name: "Should Not Write When Not Found A Record",
inputRecord: recordA,
inputMSG: new(dns.Msg).SetQuestion("not.found.com", dns.TypeA),
responseShouldBeNil: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
resolver := &localResolver{
registeredMap: make(registrationMap),
}
_ = resolver.registerRecord(testCase.inputRecord)
var responseMSG *dns.Msg
responseWriter := &mockResponseWriter{
WriteMsgFunc: func(m *dns.Msg) error {
responseMSG = m
return nil
},
}
resolver.ServeDNS(responseWriter, testCase.inputMSG)
if responseMSG == nil || len(responseMSG.Answer) == 0 {
if testCase.responseShouldBeNil {
return
}
t.Fatalf("should write a response message")
}
answerString := responseMSG.Answer[0].String()
if !strings.Contains(answerString, testCase.inputRecord.Name) {
t.Fatalf("answer doesn't contain the same domain name: \nWant: %s\nGot:%s", testCase.name, answerString)
}
if !strings.Contains(answerString, dns.Type(testCase.inputRecord.Type).String()) {
t.Fatalf("answer doesn't contain the correct type: \nWant: %s\nGot:%s", dns.Type(testCase.inputRecord.Type).String(), answerString)
}
if !strings.Contains(answerString, testCase.inputRecord.RData) {
t.Fatalf("answer doesn't contain the same address: \nWant: %s\nGot:%s", testCase.inputRecord.RData, answerString)
}
})
}
}

View File

@@ -0,0 +1,35 @@
package dns
import (
"fmt"
nbdns "github.com/netbirdio/netbird/dns"
)
// MockServer is the mock instance of a dns server
type MockServer struct {
StartFunc func()
StopFunc func()
UpdateDNSServerFunc func(serial uint64, update nbdns.Config) error
}
// Start mock implementation of Start from Server interface
func (m *MockServer) Start() {
if m.StartFunc != nil {
m.StartFunc()
}
}
// Stop mock implementation of Stop from Server interface
func (m *MockServer) Stop() {
if m.StopFunc != nil {
m.StopFunc()
}
}
// UpdateDNSServer mock implementation of UpdateDNSServer from Server interface
func (m *MockServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
if m.UpdateDNSServerFunc != nil {
return m.UpdateDNSServerFunc(serial, update)
}
return fmt.Errorf("method UpdateDNSServer is not implemented")
}

View File

@@ -0,0 +1,26 @@
package dns
import (
"net"
"github.com/miekg/dns"
)
type mockResponseWriter struct {
WriteMsgFunc func(m *dns.Msg) error
}
func (rw *mockResponseWriter) WriteMsg(m *dns.Msg) error {
if rw.WriteMsgFunc != nil {
return rw.WriteMsgFunc(m)
}
return nil
}
func (rw *mockResponseWriter) LocalAddr() net.Addr { return nil }
func (rw *mockResponseWriter) RemoteAddr() net.Addr { return nil }
func (rw *mockResponseWriter) Write([]byte) (int, error) { return 0, nil }
func (rw *mockResponseWriter) Close() error { return nil }
func (rw *mockResponseWriter) TsigStatus() error { return nil }
func (rw *mockResponseWriter) TsigTimersOnly(bool) {}
func (rw *mockResponseWriter) Hijack() {}

View File

@@ -0,0 +1,302 @@
package dns
import (
"context"
"encoding/binary"
"fmt"
"net/netip"
"regexp"
"time"
"github.com/godbus/dbus/v5"
"github.com/hashicorp/go-version"
"github.com/miekg/dns"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
)
const (
networkManagerDest = "org.freedesktop.NetworkManager"
networkManagerDbusObjectNode = "/org/freedesktop/NetworkManager"
networkManagerDbusDNSManagerInterface = "org.freedesktop.NetworkManager.DnsManager"
networkManagerDbusDNSManagerObjectNode = networkManagerDbusObjectNode + "/DnsManager"
networkManagerDbusDNSManagerModeProperty = networkManagerDbusDNSManagerInterface + ".Mode"
networkManagerDbusDNSManagerRcManagerProperty = networkManagerDbusDNSManagerInterface + ".RcManager"
networkManagerDbusVersionProperty = "org.freedesktop.NetworkManager.Version"
networkManagerDbusGetDeviceByIPIfaceMethod = networkManagerDest + ".GetDeviceByIpIface"
networkManagerDbusDeviceInterface = "org.freedesktop.NetworkManager.Device"
networkManagerDbusDeviceGetAppliedConnectionMethod = networkManagerDbusDeviceInterface + ".GetAppliedConnection"
networkManagerDbusDeviceReapplyMethod = networkManagerDbusDeviceInterface + ".Reapply"
networkManagerDbusDeviceDeleteMethod = networkManagerDbusDeviceInterface + ".Delete"
networkManagerDbusDefaultBehaviorFlag networkManagerConfigBehavior = 0
networkManagerDbusIPv4Key = "ipv4"
networkManagerDbusIPv6Key = "ipv6"
networkManagerDbusDNSKey = "dns"
networkManagerDbusDNSSearchKey = "dns-search"
networkManagerDbusDNSPriorityKey = "dns-priority"
// dns priority doc https://wiki.gnome.org/Projects/NetworkManager/DNS
networkManagerDbusPrimaryDNSPriority int32 = -500
networkManagerDbusWithMatchDomainPriority int32 = 0
networkManagerDbusSearchDomainOnlyPriority int32 = 50
supportedNetworkManagerVersionConstraint = ">= 1.16, < 1.28"
)
type networkManagerDbusConfigurator struct {
dbusLinkObject dbus.ObjectPath
routingAll bool
}
// the types below are based on dbus specification, each field is mapped to a dbus type
// see https://dbus.freedesktop.org/doc/dbus-specification.html#basic-types for more details on dbus types
// see https://networkmanager.dev/docs/api/latest/gdbus-org.freedesktop.NetworkManager.Device.html on Network Manager input types
// networkManagerConnSettings maps to a (a{sa{sv}}) dbus output from GetAppliedConnection and input for Reapply methods
type networkManagerConnSettings map[string]map[string]dbus.Variant
// networkManagerConfigVersion maps to a (t) dbus output from GetAppliedConnection and input for Reapply methods
type networkManagerConfigVersion uint64
// networkManagerConfigBehavior maps to a (u) dbus input for GetAppliedConnection and Reapply methods
type networkManagerConfigBehavior uint32
// cleanDeprecatedSettings cleans deprecated settings that still returned by
// the GetAppliedConnection methods but can't be reApplied
func (s networkManagerConnSettings) cleanDeprecatedSettings() {
for _, key := range []string{"addresses", "routes"} {
delete(s[networkManagerDbusIPv4Key], key)
delete(s[networkManagerDbusIPv6Key], key)
}
}
func newNetworkManagerDbusConfigurator(wgInterface *iface.WGIface) (hostManager, error) {
obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusObjectNode)
if err != nil {
return nil, err
}
defer closeConn()
var s string
err = obj.Call(networkManagerDbusGetDeviceByIPIfaceMethod, dbusDefaultFlag, wgInterface.Name()).Store(&s)
if err != nil {
return nil, err
}
log.Debugf("got network manager dbus Link Object: %s from net interface %s", s, wgInterface.Name())
return &networkManagerDbusConfigurator{
dbusLinkObject: dbus.ObjectPath(s),
}, nil
}
func (n *networkManagerDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
connSettings, configVersion, err := n.getAppliedConnectionSettings()
if err != nil {
return fmt.Errorf("got an error while retrieving the applied connection settings, error: %s", err)
}
connSettings.cleanDeprecatedSettings()
dnsIP, err := netip.ParseAddr(config.serverIP)
if err != nil {
return fmt.Errorf("unable to parse ip address, error: %s", err)
}
convDNSIP := binary.LittleEndian.Uint32(dnsIP.AsSlice())
connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSKey] = dbus.MakeVariant([]uint32{convDNSIP})
var (
searchDomains []string
matchDomains []string
)
for _, dConf := range config.domains {
if dConf.disabled {
continue
}
if dConf.matchOnly {
matchDomains = append(matchDomains, "~."+dns.Fqdn(dConf.domain))
continue
}
searchDomains = append(searchDomains, dns.Fqdn(dConf.domain))
}
newDomainList := append(searchDomains, matchDomains...)
priority := networkManagerDbusSearchDomainOnlyPriority
switch {
case config.routeAll:
priority = networkManagerDbusPrimaryDNSPriority
newDomainList = append(newDomainList, "~.")
if !n.routingAll {
log.Infof("configured %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
}
case len(matchDomains) > 0:
priority = networkManagerDbusWithMatchDomainPriority
}
if priority != networkManagerDbusPrimaryDNSPriority && n.routingAll {
log.Infof("removing %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
n.routingAll = false
}
connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSPriorityKey] = dbus.MakeVariant(priority)
connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSSearchKey] = dbus.MakeVariant(newDomainList)
log.Infof("adding %d search domains and %d match domains. Search list: %s , Match list: %s", len(searchDomains), len(matchDomains), searchDomains, matchDomains)
err = n.reApplyConnectionSettings(connSettings, configVersion)
if err != nil {
return fmt.Errorf("got an error while reapplying the connection with new settings, error: %s", err)
}
return nil
}
func (n *networkManagerDbusConfigurator) restoreHostDNS() error {
// once the interface is gone network manager cleans all config associated with it
return n.deleteConnectionSettings()
}
func (n *networkManagerDbusConfigurator) getAppliedConnectionSettings() (networkManagerConnSettings, networkManagerConfigVersion, error) {
obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject)
if err != nil {
return nil, 0, fmt.Errorf("got error while attempting to retrieve the applied connection settings, err: %s", err)
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
var (
connSettings networkManagerConnSettings
configVersion networkManagerConfigVersion
)
err = obj.CallWithContext(ctx, networkManagerDbusDeviceGetAppliedConnectionMethod, dbusDefaultFlag,
networkManagerDbusDefaultBehaviorFlag).Store(&connSettings, &configVersion)
if err != nil {
return nil, 0, fmt.Errorf("got error while calling GetAppliedConnection method with context, err: %s", err)
}
return connSettings, configVersion, nil
}
func (n *networkManagerDbusConfigurator) reApplyConnectionSettings(connSettings networkManagerConnSettings, configVersion networkManagerConfigVersion) error {
obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject)
if err != nil {
return fmt.Errorf("got error while attempting to retrieve the applied connection settings, err: %s", err)
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
err = obj.CallWithContext(ctx, networkManagerDbusDeviceReapplyMethod, dbusDefaultFlag,
connSettings, configVersion, networkManagerDbusDefaultBehaviorFlag).Store()
if err != nil {
return fmt.Errorf("got error while calling ReApply method with context, err: %s", err)
}
return nil
}
func (n *networkManagerDbusConfigurator) deleteConnectionSettings() error {
obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject)
if err != nil {
return fmt.Errorf("got error while attempting to retrieve the applied connection settings, err: %s", err)
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
err = obj.CallWithContext(ctx, networkManagerDbusDeviceDeleteMethod, dbusDefaultFlag).Store()
if err != nil {
return fmt.Errorf("got error while calling delete method with context, err: %s", err)
}
return nil
}
func isNetworkManagerSupported() bool {
return isNetworkManagerSupportedVersion() && isNetworkManagerSupportedMode()
}
func isNetworkManagerSupportedMode() bool {
var mode string
err := getNetworkManagerDNSProperty(networkManagerDbusDNSManagerModeProperty, &mode)
if err != nil {
log.Error(err)
return false
}
switch mode {
case "dnsmasq", "unbound", "systemd-resolved":
return true
default:
var rcManager string
err = getNetworkManagerDNSProperty(networkManagerDbusDNSManagerRcManagerProperty, &rcManager)
if err != nil {
log.Error(err)
return false
}
if rcManager == "unmanaged" {
return false
}
}
return true
}
func getNetworkManagerDNSProperty(property string, store any) error {
obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusDNSManagerObjectNode)
if err != nil {
return fmt.Errorf("got error while attempting to retrieve the network manager dns manager object, error: %s", err)
}
defer closeConn()
v, e := obj.GetProperty(property)
if e != nil {
return fmt.Errorf("got an error getting property %s: %v", property, e)
}
return v.Store(store)
}
func isNetworkManagerSupportedVersion() bool {
obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusObjectNode)
if err != nil {
log.Errorf("got error while attempting to get the network manager object, err: %s", err)
return false
}
defer closeConn()
value, err := obj.GetProperty(networkManagerDbusVersionProperty)
if err != nil {
log.Errorf("unable to retrieve network manager mode, got error: %s", err)
return false
}
versionValue, err := parseVersion(value.Value().(string))
if err != nil {
return false
}
constraints, err := version.NewConstraint(supportedNetworkManagerVersionConstraint)
if err != nil {
return false
}
return constraints.Check(versionValue)
}
func parseVersion(inputVersion string) (*version.Version, error) {
reg, err := regexp.Compile(version.SemverRegexpRaw)
if err != nil {
return nil, err
}
if inputVersion == "" || !reg.MatchString(inputVersion) {
return nil, fmt.Errorf("couldn't parse the provided version: Not SemVer")
}
verObj, err := version.NewVersion(inputVersion)
if err != nil {
return nil, err
}
return verObj, nil
}

View File

@@ -0,0 +1,85 @@
package dns
import (
"fmt"
"os/exec"
"strings"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
)
const resolvconfCommand = "resolvconf"
type resolvconf struct {
ifaceName string
}
func newResolvConfConfigurator(wgInterface *iface.WGIface) (hostManager, error) {
return &resolvconf{
ifaceName: wgInterface.Name(),
}, nil
}
func (r *resolvconf) applyDNSConfig(config hostDNSConfig) error {
var err error
if !config.routeAll {
err = r.restoreHostDNS()
if err != nil {
log.Error(err)
}
return fmt.Errorf("unable to configure DNS for this peer using resolvconf manager without a nameserver group with all domains configured")
}
var searchDomains string
appendedDomains := 0
for _, dConf := range config.domains {
if dConf.matchOnly || dConf.disabled {
continue
}
if appendedDomains >= fileMaxNumberOfSearchDomains {
// lets log all skipped domains
log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, dConf.domain)
continue
}
if fileSearchLineBeginCharCount+len(searchDomains) > fileMaxLineCharsLimit {
// lets log all skipped domains
log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, dConf.domain)
continue
}
searchDomains += " " + dConf.domain
appendedDomains++
}
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains)
err = r.applyConfig(content)
if err != nil {
return err
}
log.Infof("added %d search domains. Search list: %s", appendedDomains, searchDomains)
return nil
}
func (r *resolvconf) restoreHostDNS() error {
cmd := exec.Command(resolvconfCommand, "-f", "-d", r.ifaceName)
_, err := cmd.Output()
if err != nil {
return fmt.Errorf("got an error while removing resolvconf configuration for %s interface, error: %s", r.ifaceName, err)
}
return nil
}
func (r *resolvconf) applyConfig(content string) error {
cmd := exec.Command(resolvconfCommand, "-x", "-a", r.ifaceName)
cmd.Stdin = strings.NewReader(content)
_, err := cmd.Output()
if err != nil {
return fmt.Errorf("got an error while appying resolvconf configuration for %s interface, error: %s", r.ifaceName, err)
}
return nil
}

View File

@@ -0,0 +1,10 @@
package dns
import nbdns "github.com/netbirdio/netbird/dns"
// Server is a dns server interface
type Server interface {
Start()
Stop()
UpdateDNSServer(serial uint64, update nbdns.Config) error
}

View File

@@ -0,0 +1,32 @@
package dns
import (
"context"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
)
// DefaultServer dummy dns server
type DefaultServer struct {
}
// NewDefaultServer On Android the DNS feature is not supported yet
func NewDefaultServer(ctx context.Context, wgInterface *iface.WGIface, customAddress string) (*DefaultServer, error) {
return &DefaultServer{}, nil
}
// Start dummy implementation
func (s DefaultServer) Start() {
}
// Stop dummy implementation
func (s DefaultServer) Stop() {
}
// UpdateDNSServer dummy implementation
func (s DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
return nil
}

View File

@@ -0,0 +1,465 @@
//go:build !android
package dns
import (
"context"
"fmt"
"net"
"net/netip"
"runtime"
"sync"
"time"
"github.com/miekg/dns"
"github.com/mitchellh/hashstructure/v2"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
)
const (
defaultPort = 53
customPort = 5053
defaultIP = "127.0.0.1"
customIP = "127.0.0.153"
)
// DefaultServer dns server object
type DefaultServer struct {
ctx context.Context
ctxCancel context.CancelFunc
upstreamCtxCancel context.CancelFunc
mux sync.Mutex
server *dns.Server
dnsMux *dns.ServeMux
dnsMuxMap registrationMap
localResolver *localResolver
wgInterface *iface.WGIface
hostManager hostManager
updateSerial uint64
listenerIsRunning bool
runtimePort int
runtimeIP string
previousConfigHash uint64
currentConfig hostDNSConfig
customAddress *netip.AddrPort
}
type muxUpdate struct {
domain string
handler dns.Handler
}
// NewDefaultServer returns a new dns server
func NewDefaultServer(ctx context.Context, wgInterface *iface.WGIface, customAddress string) (*DefaultServer, error) {
mux := dns.NewServeMux()
dnsServer := &dns.Server{
Net: "udp",
Handler: mux,
UDPSize: 65535,
}
ctx, stop := context.WithCancel(ctx)
var addrPort *netip.AddrPort
if customAddress != "" {
parsedAddrPort, err := netip.ParseAddrPort(customAddress)
if err != nil {
stop()
return nil, fmt.Errorf("unable to parse the custom dns address, got error: %s", err)
}
addrPort = &parsedAddrPort
}
defaultServer := &DefaultServer{
ctx: ctx,
ctxCancel: stop,
server: dnsServer,
dnsMux: mux,
dnsMuxMap: make(registrationMap),
localResolver: &localResolver{
registeredMap: make(registrationMap),
},
wgInterface: wgInterface,
runtimePort: defaultPort,
customAddress: addrPort,
}
hostmanager, err := newHostManager(wgInterface)
if err != nil {
stop()
return nil, err
}
defaultServer.hostManager = hostmanager
return defaultServer, err
}
// Start runs the listener in a go routine
func (s *DefaultServer) Start() {
if s.customAddress != nil {
s.runtimeIP = s.customAddress.Addr().String()
s.runtimePort = int(s.customAddress.Port())
} else {
ip, port, err := s.getFirstListenerAvailable()
if err != nil {
log.Error(err)
return
}
s.runtimeIP = ip
s.runtimePort = port
}
s.server.Addr = fmt.Sprintf("%s:%d", s.runtimeIP, s.runtimePort)
log.Debugf("starting dns on %s", s.server.Addr)
go func() {
s.setListenerStatus(true)
defer s.setListenerStatus(false)
err := s.server.ListenAndServe()
if err != nil {
log.Errorf("dns server running with %d port returned an error: %v. Will not retry", s.runtimePort, err)
}
}()
}
func (s *DefaultServer) getFirstListenerAvailable() (string, int, error) {
ips := []string{defaultIP, customIP}
if runtime.GOOS != "darwin" && s.wgInterface != nil {
ips = append([]string{s.wgInterface.Address().IP.String()}, ips...)
}
ports := []int{defaultPort, customPort}
for _, port := range ports {
for _, ip := range ips {
addrString := fmt.Sprintf("%s:%d", ip, port)
udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(addrString))
probeListener, err := net.ListenUDP("udp", udpAddr)
if err == nil {
err = probeListener.Close()
if err != nil {
log.Errorf("got an error closing the probe listener, error: %s", err)
}
return ip, port, nil
}
log.Warnf("binding dns on %s is not available, error: %s", addrString, err)
}
}
return "", 0, fmt.Errorf("unable to find an unused ip and port combination. IPs tested: %v and ports %v", ips, ports)
}
func (s *DefaultServer) setListenerStatus(running bool) {
s.listenerIsRunning = running
}
// Stop stops the server
func (s *DefaultServer) Stop() {
s.mux.Lock()
defer s.mux.Unlock()
s.ctxCancel()
err := s.hostManager.restoreHostDNS()
if err != nil {
log.Error(err)
}
err = s.stopListener()
if err != nil {
log.Error(err)
}
}
func (s *DefaultServer) stopListener() error {
if !s.listenerIsRunning {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := s.server.ShutdownContext(ctx)
if err != nil {
return fmt.Errorf("stopping dns server listener returned an error: %v", err)
}
return nil
}
// UpdateDNSServer processes an update received from the management service
func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
select {
case <-s.ctx.Done():
log.Infof("not updating DNS server as context is closed")
return s.ctx.Err()
default:
if serial < s.updateSerial {
return fmt.Errorf("not applying dns update, error: "+
"network update is %d behind the last applied update", s.updateSerial-serial)
}
s.mux.Lock()
defer s.mux.Unlock()
hash, err := hashstructure.Hash(update, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
IgnoreZeroValue: true,
SlicesAsSets: true,
UseStringer: true,
})
if err != nil {
log.Errorf("unable to hash the dns configuration update, got error: %s", err)
}
if s.previousConfigHash == hash {
log.Debugf("not applying the dns configuration update as there is nothing new")
s.updateSerial = serial
return nil
}
if err := s.applyConfiguration(update); err != nil {
return err
}
s.updateSerial = serial
s.previousConfigHash = hash
return nil
}
}
func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
// is the service should be disabled, we stop the listener
// and proceed with a regular update to clean up the handlers and records
if !update.ServiceEnable {
err := s.stopListener()
if err != nil {
log.Error(err)
}
} else if !s.listenerIsRunning {
s.Start()
}
localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones)
if err != nil {
return fmt.Errorf("not applying dns update, error: %v", err)
}
upstreamMuxUpdates, err := s.buildUpstreamHandlerUpdate(update.NameServerGroups)
if err != nil {
return fmt.Errorf("not applying dns update, error: %v", err)
}
muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...)
s.updateMux(muxUpdates)
s.updateLocalResolver(localRecords)
s.currentConfig = dnsConfigToHostDNSConfig(update, s.runtimeIP, s.runtimePort)
if err = s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
log.Error(err)
}
return nil
}
func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]muxUpdate, map[string]nbdns.SimpleRecord, error) {
var muxUpdates []muxUpdate
localRecords := make(map[string]nbdns.SimpleRecord, 0)
for _, customZone := range customZones {
if len(customZone.Records) == 0 {
return nil, nil, fmt.Errorf("received an empty list of records")
}
muxUpdates = append(muxUpdates, muxUpdate{
domain: customZone.Domain,
handler: s.localResolver,
})
for _, record := range customZone.Records {
var class uint16 = dns.ClassINET
if record.Class != nbdns.DefaultClass {
return nil, nil, fmt.Errorf("received an invalid class type: %s", record.Class)
}
key := buildRecordKey(record.Name, class, uint16(record.Type))
localRecords[key] = record
}
}
return muxUpdates, localRecords, nil
}
func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.NameServerGroup) ([]muxUpdate, error) {
// clean up the previous upstream resolver
if s.upstreamCtxCancel != nil {
s.upstreamCtxCancel()
}
var muxUpdates []muxUpdate
for _, nsGroup := range nameServerGroups {
if len(nsGroup.NameServers) == 0 {
log.Warn("received a nameserver group with empty nameserver list")
continue
}
var ctx context.Context
ctx, s.upstreamCtxCancel = context.WithCancel(s.ctx)
handler := newUpstreamResolver(ctx)
for _, ns := range nsGroup.NameServers {
if ns.NSType != nbdns.UDPNameServerType {
log.Warnf("skiping nameserver %s with type %s, this peer supports only %s",
ns.IP.String(), ns.NSType.String(), nbdns.UDPNameServerType.String())
continue
}
handler.upstreamServers = append(handler.upstreamServers, getNSHostPort(ns))
}
if len(handler.upstreamServers) == 0 {
log.Errorf("received a nameserver group with an invalid nameserver list")
continue
}
// when upstream fails to resolve domain several times over all it servers
// it will calls this hook to exclude self from the configuration and
// reapply DNS settings, but it not touch the original configuration and serial number
// because it is temporal deactivation until next try
//
// after some period defined by upstream it trys to reactivate self by calling this hook
// everything we need here is just to re-apply current configuration because it already
// contains this upstream settings (temporal deactivation not removed it)
handler.deactivate, handler.reactivate = s.upstreamCallbacks(nsGroup, handler)
if nsGroup.Primary {
muxUpdates = append(muxUpdates, muxUpdate{
domain: nbdns.RootZone,
handler: handler,
})
continue
}
if len(nsGroup.Domains) == 0 {
return nil, fmt.Errorf("received a non primary nameserver group with an empty domain list")
}
for _, domain := range nsGroup.Domains {
if domain == "" {
return nil, fmt.Errorf("received a nameserver group with an empty domain element")
}
muxUpdates = append(muxUpdates, muxUpdate{
domain: domain,
handler: handler,
})
}
}
return muxUpdates, nil
}
func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) {
muxUpdateMap := make(registrationMap)
for _, update := range muxUpdates {
s.registerMux(update.domain, update.handler)
muxUpdateMap[update.domain] = struct{}{}
}
for key := range s.dnsMuxMap {
_, found := muxUpdateMap[key]
if !found {
s.deregisterMux(key)
}
}
s.dnsMuxMap = muxUpdateMap
}
func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) {
for key := range s.localResolver.registeredMap {
_, found := update[key]
if !found {
s.localResolver.deleteRecord(key)
}
}
updatedMap := make(registrationMap)
for key, record := range update {
err := s.localResolver.registerRecord(record)
if err != nil {
log.Warnf("got an error while registering the record (%s), error: %v", record.String(), err)
}
updatedMap[key] = struct{}{}
}
s.localResolver.registeredMap = updatedMap
}
func getNSHostPort(ns nbdns.NameServer) string {
return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port)
}
func (s *DefaultServer) registerMux(pattern string, handler dns.Handler) {
s.dnsMux.Handle(pattern, handler)
}
func (s *DefaultServer) deregisterMux(pattern string) {
s.dnsMux.HandleRemove(pattern)
}
// upstreamCallbacks returns two functions, the first one is used to deactivate
// the upstream resolver from the configuration, the second one is used to
// reactivate it. Not allowed to call reactivate before deactivate.
func (s *DefaultServer) upstreamCallbacks(
nsGroup *nbdns.NameServerGroup,
handler dns.Handler,
) (deactivate func(), reactivate func()) {
var removeIndex map[string]int
deactivate = func() {
s.mux.Lock()
defer s.mux.Unlock()
l := log.WithField("nameservers", nsGroup.NameServers)
l.Info("temporary deactivate nameservers group due timeout")
removeIndex = make(map[string]int)
for _, domain := range nsGroup.Domains {
removeIndex[domain] = -1
}
if nsGroup.Primary {
removeIndex[nbdns.RootZone] = -1
s.currentConfig.routeAll = false
}
for i, item := range s.currentConfig.domains {
if _, found := removeIndex[item.domain]; found {
s.currentConfig.domains[i].disabled = true
s.deregisterMux(item.domain)
removeIndex[item.domain] = i
}
}
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
l.WithError(err).Error("fail to apply nameserver deactivation on the host")
}
}
reactivate = func() {
s.mux.Lock()
defer s.mux.Unlock()
for domain, i := range removeIndex {
if i == -1 || i >= len(s.currentConfig.domains) || s.currentConfig.domains[i].domain != domain {
continue
}
s.currentConfig.domains[i].disabled = false
s.registerMux(domain, handler)
}
l := log.WithField("nameservers", nsGroup.NameServers)
l.Debug("reactivate temporary disabled nameserver group")
if nsGroup.Primary {
s.currentConfig.routeAll = true
}
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
l.WithError(err).Error("reactivate temporary disabled nameserver group, DNS update apply")
}
}
return
}

View File

@@ -0,0 +1,433 @@
package dns
import (
"context"
"fmt"
"net"
"net/netip"
"strings"
"testing"
"time"
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
)
var zoneRecords = []nbdns.SimpleRecord{
{
Name: "peera.netbird.cloud",
Type: 1,
Class: nbdns.DefaultClass,
TTL: 300,
RData: "1.2.3.4",
},
}
func TestUpdateDNSServer(t *testing.T) {
nameServers := []nbdns.NameServer{
{
IP: netip.MustParseAddr("8.8.8.8"),
NSType: nbdns.UDPNameServerType,
Port: 53,
},
{
IP: netip.MustParseAddr("8.8.4.4"),
NSType: nbdns.UDPNameServerType,
Port: 53,
},
}
testCases := []struct {
name string
initUpstreamMap registrationMap
initLocalMap registrationMap
initSerial uint64
inputSerial uint64
inputUpdate nbdns.Config
shouldFail bool
expectedUpstreamMap registrationMap
expectedLocalMap registrationMap
}{
{
name: "Initial Config Should Succeed",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []*nbdns.NameServerGroup{
{
Domains: []string{"netbird.io"},
NameServers: nameServers,
},
{
NameServers: nameServers,
Primary: true,
},
},
},
expectedUpstreamMap: registrationMap{"netbird.io": struct{}{}, "netbird.cloud": struct{}{}, nbdns.RootZone: struct{}{}},
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
},
{
name: "New Config Should Succeed",
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
initUpstreamMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []*nbdns.NameServerGroup{
{
Domains: []string{"netbird.io"},
NameServers: nameServers,
},
},
},
expectedUpstreamMap: registrationMap{"netbird.io": struct{}{}, "netbird.cloud": struct{}{}},
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
},
{
name: "Smaller Config Serial Should Be Skipped",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 2,
inputSerial: 1,
shouldFail: true,
},
{
name: "Empty NS Group Domain Or Not Primary Element Should Fail",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []*nbdns.NameServerGroup{
{
NameServers: nameServers,
},
},
},
shouldFail: true,
},
{
name: "Invalid NS Group Nameservers list Should Fail",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []*nbdns.NameServerGroup{
{
NameServers: nameServers,
},
},
},
shouldFail: true,
},
{
name: "Invalid Custom Zone Records list Should Fail",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
},
},
NameServerGroups: []*nbdns.NameServerGroup{
{
NameServers: nameServers,
Primary: true,
},
},
},
shouldFail: true,
},
{
name: "Empty Config Should Succeed and Clean Maps",
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
initUpstreamMap: registrationMap{zoneRecords[0].Name: struct{}{}},
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{ServiceEnable: true},
expectedUpstreamMap: make(registrationMap),
expectedLocalMap: make(registrationMap),
},
{
name: "Disabled Service Should clean map",
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
initUpstreamMap: registrationMap{zoneRecords[0].Name: struct{}{}},
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{ServiceEnable: false},
expectedUpstreamMap: make(registrationMap),
expectedLocalMap: make(registrationMap),
},
}
for n, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
wgIface, err := iface.NewWGIFace(fmt.Sprintf("utun230%d", n), fmt.Sprintf("100.66.100.%d/32", n+1), iface.DefaultMTU, nil)
if err != nil {
t.Fatal(err)
}
err = wgIface.Create()
if err != nil {
t.Fatal(err)
}
defer func() {
err = wgIface.Close()
if err != nil {
t.Log(err)
}
}()
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "")
if err != nil {
t.Fatal(err)
}
defer func() {
err = dnsServer.hostManager.restoreHostDNS()
if err != nil {
t.Log(err)
}
}()
dnsServer.dnsMuxMap = testCase.initUpstreamMap
dnsServer.localResolver.registeredMap = testCase.initLocalMap
dnsServer.updateSerial = testCase.initSerial
// pretend we are running
dnsServer.listenerIsRunning = true
err = dnsServer.UpdateDNSServer(testCase.inputSerial, testCase.inputUpdate)
if err != nil {
if testCase.shouldFail {
return
}
t.Fatalf("update dns server should not fail, got error: %v", err)
}
if len(dnsServer.dnsMuxMap) != len(testCase.expectedUpstreamMap) {
t.Fatalf("update upstream failed, map size is different than expected, want %d, got %d", len(testCase.expectedUpstreamMap), len(dnsServer.dnsMuxMap))
}
for key := range testCase.expectedUpstreamMap {
_, found := dnsServer.dnsMuxMap[key]
if !found {
t.Fatalf("update upstream failed, key %s was not found in the dnsMuxMap: %#v", key, dnsServer.dnsMuxMap)
}
}
if len(dnsServer.localResolver.registeredMap) != len(testCase.expectedLocalMap) {
t.Fatalf("update local failed, registered map size is different than expected, want %d, got %d", len(testCase.expectedLocalMap), len(dnsServer.localResolver.registeredMap))
}
for key := range testCase.expectedLocalMap {
_, found := dnsServer.localResolver.registeredMap[key]
if !found {
t.Fatalf("update local failed, key %s was not found in the localResolver.registeredMap: %#v", key, dnsServer.localResolver.registeredMap)
}
}
})
}
}
func TestDNSServerStartStop(t *testing.T) {
testCases := []struct {
name string
addrPort string
}{
{
name: "Should Pass With Port Discovery",
},
{
name: "Should Pass With Custom Port",
addrPort: "127.0.0.1:3535",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
dnsServer := getDefaultServerWithNoHostManager(t, testCase.addrPort)
dnsServer.hostManager = newNoopHostMocker()
dnsServer.Start()
time.Sleep(100 * time.Millisecond)
if !dnsServer.listenerIsRunning {
t.Fatal("dns server listener is not running")
}
defer dnsServer.Stop()
err := dnsServer.localResolver.registerRecord(zoneRecords[0])
if err != nil {
t.Error(err)
}
dnsServer.dnsMux.Handle("netbird.cloud", dnsServer.localResolver)
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: time.Second * 5,
}
addr := fmt.Sprintf("%s:%d", dnsServer.runtimeIP, dnsServer.runtimePort)
conn, err := d.DialContext(ctx, network, addr)
if err != nil {
t.Log(err)
// retry test before exit, for slower systems
return d.DialContext(ctx, network, addr)
}
return conn, nil
},
}
ips, err := resolver.LookupHost(context.Background(), zoneRecords[0].Name)
if err != nil {
t.Fatalf("failed to connect to the server, error: %v", err)
}
if ips[0] != zoneRecords[0].RData {
t.Fatalf("got a different IP from the server: want %s, got %s", zoneRecords[0].RData, ips[0])
}
dnsServer.Stop()
ctx, cancel := context.WithTimeout(context.TODO(), time.Second*1)
defer cancel()
_, err = resolver.LookupHost(ctx, zoneRecords[0].Name)
if err == nil {
t.Fatalf("we should encounter an error when querying a stopped server")
}
})
}
}
func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
hostManager := &mockHostConfigurator{}
server := DefaultServer{
dnsMux: dns.DefaultServeMux,
localResolver: &localResolver{
registeredMap: make(registrationMap),
},
hostManager: hostManager,
currentConfig: hostDNSConfig{
domains: []domainConfig{
{false, "domain0", false},
{false, "domain1", false},
{false, "domain2", false},
},
},
}
var domainsUpdate string
hostManager.applyDNSConfigFunc = func(config hostDNSConfig) error {
domains := []string{}
for _, item := range config.domains {
if item.disabled {
continue
}
domains = append(domains, item.domain)
}
domainsUpdate = strings.Join(domains, ",")
return nil
}
deactivate, reactivate := server.upstreamCallbacks(&nbdns.NameServerGroup{
Domains: []string{"domain1"},
NameServers: []nbdns.NameServer{
{IP: netip.MustParseAddr("8.8.0.0"), NSType: nbdns.UDPNameServerType, Port: 53},
},
}, nil)
deactivate()
expected := "domain0,domain2"
domains := []string{}
for _, item := range server.currentConfig.domains {
if item.disabled {
continue
}
domains = append(domains, item.domain)
}
got := strings.Join(domains, ",")
if expected != got {
t.Errorf("expected domains list: %q, got %q", expected, got)
}
reactivate()
expected = "domain0,domain1,domain2"
domains = []string{}
for _, item := range server.currentConfig.domains {
if item.disabled {
continue
}
domains = append(domains, item.domain)
}
got = strings.Join(domains, ",")
if expected != got {
t.Errorf("expected domains list: %q, got %q", expected, domainsUpdate)
}
}
func getDefaultServerWithNoHostManager(t *testing.T, addrPort string) *DefaultServer {
mux := dns.NewServeMux()
var parsedAddrPort *netip.AddrPort
if addrPort != "" {
parsed, err := netip.ParseAddrPort(addrPort)
if err != nil {
t.Fatal(err)
}
parsedAddrPort = &parsed
}
dnsServer := &dns.Server{
Net: "udp",
Handler: mux,
UDPSize: 65535,
}
ctx, cancel := context.WithCancel(context.TODO())
return &DefaultServer{
ctx: ctx,
ctxCancel: cancel,
server: dnsServer,
dnsMux: mux,
dnsMuxMap: make(registrationMap),
localResolver: &localResolver{
registeredMap: make(registrationMap),
},
customAddress: parsedAddrPort,
}
}

View File

@@ -0,0 +1,210 @@
package dns
import (
"context"
"fmt"
"net"
"net/netip"
"time"
"github.com/godbus/dbus/v5"
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
const (
systemdDbusManagerInterface = "org.freedesktop.resolve1.Manager"
systemdResolvedDest = "org.freedesktop.resolve1"
systemdDbusObjectNode = "/org/freedesktop/resolve1"
systemdDbusGetLinkMethod = systemdDbusManagerInterface + ".GetLink"
systemdDbusFlushCachesMethod = systemdDbusManagerInterface + ".FlushCaches"
systemdDbusResolvConfModeProperty = systemdDbusManagerInterface + ".ResolvConfMode"
systemdDbusLinkInterface = "org.freedesktop.resolve1.Link"
systemdDbusRevertMethodSuffix = systemdDbusLinkInterface + ".Revert"
systemdDbusSetDNSMethodSuffix = systemdDbusLinkInterface + ".SetDNS"
systemdDbusSetDefaultRouteMethodSuffix = systemdDbusLinkInterface + ".SetDefaultRoute"
systemdDbusSetDomainsMethodSuffix = systemdDbusLinkInterface + ".SetDomains"
systemdDbusResolvConfModeForeign = "foreign"
)
type systemdDbusConfigurator struct {
dbusLinkObject dbus.ObjectPath
routingAll bool
}
// the types below are based on dbus specification, each field is mapped to a dbus type
// see https://dbus.freedesktop.org/doc/dbus-specification.html#basic-types for more details on dbus types
// see https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html on resolve1 input types
// systemdDbusDNSInput maps to a (iay) dbus input for SetDNS method
type systemdDbusDNSInput struct {
Family int32
Address []byte
}
// systemdDbusLinkDomainsInput maps to a (sb) dbus input for SetDomains method
type systemdDbusLinkDomainsInput struct {
Domain string
MatchOnly bool
}
func newSystemdDbusConfigurator(wgInterface *iface.WGIface) (hostManager, error) {
iface, err := net.InterfaceByName(wgInterface.Name())
if err != nil {
return nil, err
}
obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode)
if err != nil {
return nil, err
}
defer closeConn()
var s string
err = obj.Call(systemdDbusGetLinkMethod, dbusDefaultFlag, iface.Index).Store(&s)
if err != nil {
return nil, err
}
log.Debugf("got dbus Link interface: %s from net interface %s and index %d", s, iface.Name, iface.Index)
return &systemdDbusConfigurator{
dbusLinkObject: dbus.ObjectPath(s),
}, nil
}
func (s *systemdDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
parsedIP, err := netip.ParseAddr(config.serverIP)
if err != nil {
return fmt.Errorf("unable to parse ip address, error: %s", err)
}
ipAs4 := parsedIP.As4()
defaultLinkInput := systemdDbusDNSInput{
Family: unix.AF_INET,
Address: ipAs4[:],
}
err = s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput})
if err != nil {
return fmt.Errorf("setting the interface DNS server %s:%d failed with error: %s", config.serverIP, config.serverPort, err)
}
var (
searchDomains []string
matchDomains []string
domainsInput []systemdDbusLinkDomainsInput
)
for _, dConf := range config.domains {
if dConf.disabled {
continue
}
domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{
Domain: dns.Fqdn(dConf.domain),
MatchOnly: dConf.matchOnly,
})
if dConf.matchOnly {
matchDomains = append(matchDomains, dConf.domain)
continue
}
searchDomains = append(searchDomains, dConf.domain)
}
if config.routeAll {
log.Infof("configured %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
err = s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, true)
if err != nil {
return fmt.Errorf("setting link as default dns router, failed with error: %s", err)
}
domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{
Domain: nbdns.RootZone,
MatchOnly: true,
})
s.routingAll = true
} else if s.routingAll {
log.Infof("removing %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
}
log.Infof("adding %d search domains and %d match domains. Search list: %s , Match list: %s", len(searchDomains), len(matchDomains), searchDomains, matchDomains)
err = s.setDomainsForInterface(domainsInput)
if err != nil {
log.Error(err)
}
return nil
}
func (s *systemdDbusConfigurator) setDomainsForInterface(domainsInput []systemdDbusLinkDomainsInput) error {
err := s.callLinkMethod(systemdDbusSetDomainsMethodSuffix, domainsInput)
if err != nil {
return fmt.Errorf("setting domains configuration failed with error: %s", err)
}
return s.flushCaches()
}
func (s *systemdDbusConfigurator) restoreHostDNS() error {
log.Infof("reverting link settings and flushing cache")
if !isDbusListenerRunning(systemdResolvedDest, s.dbusLinkObject) {
return nil
}
err := s.callLinkMethod(systemdDbusRevertMethodSuffix, nil)
if err != nil {
return fmt.Errorf("unable to revert link configuration, got error: %s", err)
}
return s.flushCaches()
}
func (s *systemdDbusConfigurator) flushCaches() error {
obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode)
if err != nil {
return fmt.Errorf("got error while attempting to retrieve the object %s, err: %s", systemdDbusObjectNode, err)
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
err = obj.CallWithContext(ctx, systemdDbusFlushCachesMethod, dbusDefaultFlag).Store()
if err != nil {
return fmt.Errorf("got error while calling the FlushCaches method with context, err: %s", err)
}
return nil
}
func (s *systemdDbusConfigurator) callLinkMethod(method string, value any) error {
obj, closeConn, err := getDbusObject(systemdResolvedDest, s.dbusLinkObject)
if err != nil {
return fmt.Errorf("got error while attempting to retrieve the object, err: %s", err)
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
if value != nil {
err = obj.CallWithContext(ctx, method, dbusDefaultFlag, value).Store()
} else {
err = obj.CallWithContext(ctx, method, dbusDefaultFlag).Store()
}
if err != nil {
return fmt.Errorf("got error while calling command with context, err: %s", err)
}
return nil
}
func getSystemdDbusProperty(property string, store any) error {
obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode)
if err != nil {
return fmt.Errorf("got error while attempting to retrieve the systemd dns manager object, error: %s", err)
}
defer closeConn()
v, e := obj.GetProperty(property)
if e != nil {
return fmt.Errorf("got an error getting property %s: %v", property, e)
}
return v.Store(store)
}

View File

@@ -0,0 +1,143 @@
package dns
import (
"context"
"errors"
"net"
"sync"
"sync/atomic"
"time"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
)
const (
failsTillDeact = int32(3)
reactivatePeriod = time.Minute
upstreamTimeout = 15 * time.Second
)
type upstreamResolver struct {
ctx context.Context
upstreamClient *dns.Client
upstreamServers []string
disabled bool
failsCount atomic.Int32
failsTillDeact int32
mutex sync.Mutex
reactivatePeriod time.Duration
upstreamTimeout time.Duration
deactivate func()
reactivate func()
}
func newUpstreamResolver(ctx context.Context) *upstreamResolver {
return &upstreamResolver{
ctx: ctx,
upstreamClient: &dns.Client{},
upstreamTimeout: upstreamTimeout,
reactivatePeriod: reactivatePeriod,
failsTillDeact: failsTillDeact,
}
}
// ServeDNS handles a DNS request
func (u *upstreamResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
defer u.checkUpstreamFails()
log.WithField("question", r.Question[0]).Trace("received an upstream question")
select {
case <-u.ctx.Done():
return
default:
}
for _, upstream := range u.upstreamServers {
ctx, cancel := context.WithTimeout(u.ctx, u.upstreamTimeout)
rm, t, err := u.upstreamClient.ExchangeContext(ctx, r, upstream)
cancel()
if err != nil {
if err == context.DeadlineExceeded || isTimeout(err) {
log.WithError(err).WithField("upstream", upstream).
Warn("got an error while connecting to upstream")
continue
}
u.failsCount.Add(1)
log.WithError(err).WithField("upstream", upstream).
Error("got an error while querying the upstream")
return
}
log.Tracef("took %s to query the upstream %s", t, upstream)
err = w.WriteMsg(rm)
if err != nil {
log.WithError(err).Error("got an error while writing the upstream resolver response")
}
// count the fails only if they happen sequentially
u.failsCount.Store(0)
return
}
u.failsCount.Add(1)
log.Error("all queries to the upstream nameservers failed with timeout")
}
// checkUpstreamFails counts fails and disables or enables upstream resolving
//
// If fails count is greater that failsTillDeact, upstream resolving
// will be disabled for reactivatePeriod, after that time period fails counter
// will be reset and upstream will be reactivated.
func (u *upstreamResolver) checkUpstreamFails() {
u.mutex.Lock()
defer u.mutex.Unlock()
if u.failsCount.Load() < u.failsTillDeact || u.disabled {
return
}
select {
case <-u.ctx.Done():
return
default:
log.Warnf("upstream resolving is disabled for %v", reactivatePeriod)
u.deactivate()
u.disabled = true
go u.waitUntilReactivation()
}
}
// waitUntilReactivation reset fails counter and activates upstream resolving
func (u *upstreamResolver) waitUntilReactivation() {
timer := time.NewTimer(u.reactivatePeriod)
defer func() {
if !timer.Stop() {
<-timer.C
}
}()
select {
case <-u.ctx.Done():
return
case <-timer.C:
log.Info("upstream resolving is reactivated")
u.failsCount.Store(0)
u.reactivate()
u.disabled = false
}
}
// isTimeout returns true if the given error is a network timeout error.
//
// Copied from k8s.io/apimachinery/pkg/util/net.IsTimeout
func isTimeout(err error) bool {
var neterr net.Error
if errors.As(err, &neterr) {
return neterr != nil && neterr.Timeout()
}
return false
}

View File

@@ -0,0 +1,156 @@
package dns
import (
"context"
"github.com/miekg/dns"
"strings"
"testing"
"time"
)
func TestUpstreamResolver_ServeDNS(t *testing.T) {
testCases := []struct {
name string
inputMSG *dns.Msg
responseShouldBeNil bool
InputServers []string
timeout time.Duration
cancelCTX bool
expectedAnswer string
}{
{
name: "Should Resolve A Record",
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
InputServers: []string{"8.8.8.8:53", "8.8.4.4:53"},
timeout: upstreamTimeout,
expectedAnswer: "1.1.1.1",
},
{
name: "Should Resolve If First Upstream Times Out",
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
InputServers: []string{"8.0.0.0:53", "8.8.4.4:53"},
timeout: 2 * time.Second,
expectedAnswer: "1.1.1.1",
},
{
name: "Should Not Resolve If Can't Connect To Both Servers",
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
InputServers: []string{"8.0.0.0:53", "8.0.0.1:53"},
timeout: 200 * time.Millisecond,
responseShouldBeNil: true,
},
{
name: "Should Not Resolve If Parent Context Is Canceled",
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
InputServers: []string{"8.0.0.0:53", "8.8.4.4:53"},
cancelCTX: true,
timeout: upstreamTimeout,
responseShouldBeNil: true,
},
//{
// name: "Should Resolve CNAME Record",
// inputMSG: new(dns.Msg).SetQuestion("one.one.one.one", dns.TypeCNAME),
//},
//{
// name: "Should Not Write When Not Found A Record",
// inputMSG: new(dns.Msg).SetQuestion("not.found.com", dns.TypeA),
// responseShouldBeNil: true,
//},
}
// should resolve if first upstream times out
// should not write when both fails
// should not resolve if parent context is canceled
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.TODO())
resolver := newUpstreamResolver(ctx)
resolver.upstreamServers = testCase.InputServers
resolver.upstreamTimeout = testCase.timeout
if testCase.cancelCTX {
cancel()
} else {
defer cancel()
}
var responseMSG *dns.Msg
responseWriter := &mockResponseWriter{
WriteMsgFunc: func(m *dns.Msg) error {
responseMSG = m
return nil
},
}
resolver.ServeDNS(responseWriter, testCase.inputMSG)
if responseMSG == nil {
if testCase.responseShouldBeNil {
return
}
t.Fatalf("should write a response message")
}
foundAnswer := false
for _, answer := range responseMSG.Answer {
if strings.Contains(answer.String(), testCase.expectedAnswer) {
foundAnswer = true
break
}
}
if !foundAnswer {
t.Errorf("couldn't find the required answer, %s, in the dns response", testCase.expectedAnswer)
}
})
}
}
func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
resolver := newUpstreamResolver(context.TODO())
resolver.upstreamServers = []string{"0.0.0.0:-1"}
resolver.failsTillDeact = 0
resolver.reactivatePeriod = time.Microsecond * 100
responseWriter := &mockResponseWriter{
WriteMsgFunc: func(m *dns.Msg) error { return nil },
}
failed := false
resolver.deactivate = func() {
failed = true
}
reactivated := false
resolver.reactivate = func() {
reactivated = true
}
resolver.ServeDNS(responseWriter, new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA))
if !failed {
t.Errorf("expected that resolving was deactivated")
return
}
if !resolver.disabled {
t.Errorf("resolver should be disabled")
return
}
time.Sleep(time.Millisecond * 200)
if !reactivated {
t.Errorf("expected that resolving was reactivated")
return
}
if resolver.failsCount.Load() != 0 {
t.Errorf("fails count after reactivation should be 0")
return
}
if resolver.disabled {
t.Errorf("should be enabled")
}
}

1063
client/internal/engine.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
//go:build !android
package internal
import (
"github.com/pion/transport/v2/stdnet"
)
func (e *Engine) newStdNet() (*stdnet.Net, error) {
return stdnet.NewNet()
}

View File

@@ -0,0 +1,7 @@
package internal
import "github.com/netbirdio/netbird/client/internal/stdnet"
func (e *Engine) newStdNet() (*stdnet.Net, error) {
return stdnet.NewNet(e.config.IFaceDiscover)
}

File diff suppressed because it is too large Load Diff

161
client/internal/login.go Normal file
View File

@@ -0,0 +1,161 @@
package internal
import (
"context"
"net/url"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/client/system"
mgm "github.com/netbirdio/netbird/management/client"
mgmProto "github.com/netbirdio/netbird/management/proto"
)
// IsLoginRequired check that the server is support SSO or not
func IsLoginRequired(ctx context.Context, privateKey string, mgmURL *url.URL, sshKey string) (bool, error) {
mgmClient, err := getMgmClient(ctx, privateKey, mgmURL)
if err != nil {
return false, err
}
defer func() {
err = mgmClient.Close()
if err != nil {
cStatus, ok := status.FromError(err)
if !ok || ok && cStatus.Code() != codes.Canceled {
log.Warnf("failed to close the Management service client, err: %v", err)
}
}
}()
log.Debugf("connected to the Management service %s", mgmURL.String())
pubSSHKey, err := ssh.GeneratePublicKey([]byte(sshKey))
if err != nil {
return false, err
}
_, err = doMgmLogin(ctx, mgmClient, pubSSHKey)
if isLoginNeeded(err) {
return true, nil
}
return false, err
}
// Login or register the client
func Login(ctx context.Context, config *Config, setupKey string, jwtToken string) error {
mgmClient, err := getMgmClient(ctx, config.PrivateKey, config.ManagementURL)
if err != nil {
return err
}
defer func() {
err = mgmClient.Close()
if err != nil {
cStatus, ok := status.FromError(err)
if !ok || ok && cStatus.Code() != codes.Canceled {
log.Warnf("failed to close the Management service client, err: %v", err)
}
}
}()
log.Debugf("connected to the Management service %s", config.ManagementURL.String())
pubSSHKey, err := ssh.GeneratePublicKey([]byte(config.SSHKey))
if err != nil {
return err
}
serverKey, err := doMgmLogin(ctx, mgmClient, pubSSHKey)
if isRegistrationNeeded(err) {
log.Debugf("peer registration required")
_, err = registerPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey)
return err
}
return err
}
func getMgmClient(ctx context.Context, privateKey string, mgmURL *url.URL) (*mgm.GrpcClient, error) {
// validate our peer's Wireguard PRIVATE key
myPrivateKey, err := wgtypes.ParseKey(privateKey)
if err != nil {
log.Errorf("failed parsing Wireguard key %s: [%s]", privateKey, err.Error())
return nil, err
}
var mgmTlsEnabled bool
if mgmURL.Scheme == "https" {
mgmTlsEnabled = true
}
log.Debugf("connecting to the Management service %s", mgmURL.String())
mgmClient, err := mgm.NewClient(ctx, mgmURL.Host, myPrivateKey, mgmTlsEnabled)
if err != nil {
log.Errorf("failed connecting to the Management service %s %v", mgmURL.String(), err)
return nil, err
}
return mgmClient, err
}
func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte) (*wgtypes.Key, error) {
serverKey, err := mgmClient.GetServerPublicKey()
if err != nil {
log.Errorf("failed while getting Management Service public key: %v", err)
return nil, err
}
sysInfo := system.GetInfo(ctx)
_, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey)
return serverKey, err
}
// registerPeer checks whether setupKey was provided via cmd line and if not then it prompts user to enter a key.
// Otherwise tries to register with the provided setupKey via command line.
func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte) (*mgmProto.LoginResponse, error) {
validSetupKey, err := uuid.Parse(setupKey)
if err != nil && jwtToken == "" {
return nil, status.Errorf(codes.InvalidArgument, "invalid setup-key or no sso information provided, err: %v", err)
}
log.Debugf("sending peer registration request to Management Service")
info := system.GetInfo(ctx)
loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info, pubSSHKey)
if err != nil {
log.Errorf("failed registering peer %v,%s", err, validSetupKey.String())
return nil, err
}
log.Infof("peer has been successfully registered on Management Service")
return loginResp, nil
}
func isLoginNeeded(err error) bool {
if err == nil {
return false
}
s, ok := status.FromError(err)
if !ok {
return false
}
if s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied {
return true
}
return false
}
func isRegistrationNeeded(err error) bool {
if err == nil {
return false
}
s, ok := status.FromError(err)
if !ok {
return false
}
if s.Code() == codes.PermissionDenied {
return true
}
return false
}

283
client/internal/oauth.go Normal file
View File

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

View File

@@ -0,0 +1,298 @@
package internal
import (
"context"
"fmt"
"github.com/golang-jwt/jwt"
"github.com/stretchr/testify/require"
"io"
"net/http"
"net/url"
"strings"
"testing"
"time"
)
type mockHTTPClient struct {
code int
resBody string
reqBody string
MaxReqs int
count int
countResBody string
err error
}
func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
body, err := io.ReadAll(req.Body)
if err == nil {
c.reqBody = string(body)
}
if c.MaxReqs > c.count {
c.count++
return &http.Response{
StatusCode: c.code,
Body: io.NopCloser(strings.NewReader(c.countResBody)),
}, c.err
}
return &http.Response{
StatusCode: c.code,
Body: io.NopCloser(strings.NewReader(c.resBody)),
}, c.err
}
func TestHosted_RequestDeviceCode(t *testing.T) {
type test struct {
name string
inputResBody string
inputReqCode int
inputReqError error
testingErrFunc require.ErrorAssertionFunc
expectedErrorMSG string
testingFunc require.ComparisonAssertionFunc
expectedOut DeviceAuthInfo
expectedMSG string
expectPayload string
}
expectedAudience := "ok"
expectedClientID := "bla"
expectedScope := "openid"
form := url.Values{}
form.Add("audience", expectedAudience)
form.Add("client_id", expectedClientID)
form.Add("scope", expectedScope)
expectPayload := form.Encode()
testCase1 := test{
name: "Payload Is Valid",
expectPayload: expectPayload,
inputReqCode: 200,
testingErrFunc: require.Error,
testingFunc: require.EqualValues,
}
testCase2 := test{
name: "Exit On Network Error",
inputReqError: fmt.Errorf("error"),
testingErrFunc: require.Error,
expectedErrorMSG: "should return error",
testingFunc: require.EqualValues,
expectPayload: expectPayload,
}
testCase3 := test{
name: "Exit On Exit Code",
inputReqCode: 400,
testingErrFunc: require.Error,
expectedErrorMSG: "should return error",
testingFunc: require.EqualValues,
expectPayload: expectPayload,
}
testCase4Out := DeviceAuthInfo{ExpiresIn: 10}
testCase4 := test{
name: "Got Device Code",
inputResBody: fmt.Sprintf("{\"expires_in\":%d}", testCase4Out.ExpiresIn),
expectPayload: expectPayload,
inputReqCode: 200,
testingErrFunc: require.NoError,
testingFunc: require.EqualValues,
expectedOut: testCase4Out,
expectedMSG: "out should match",
}
for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4} {
t.Run(testCase.name, func(t *testing.T) {
httpClient := mockHTTPClient{
resBody: testCase.inputResBody,
code: testCase.inputReqCode,
err: testCase.inputReqError,
}
hosted := Hosted{
Audience: expectedAudience,
ClientID: expectedClientID,
Scope: expectedScope,
TokenEndpoint: "test.hosted.com/token",
DeviceAuthEndpoint: "test.hosted.com/device/auth",
HTTPClient: &httpClient,
}
authInfo, err := hosted.RequestDeviceCode(context.TODO())
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
require.EqualValues(t, expectPayload, httpClient.reqBody, "payload should match")
testCase.testingFunc(t, testCase.expectedOut, authInfo, testCase.expectedMSG)
})
}
}
func TestHosted_WaitToken(t *testing.T) {
type test struct {
name string
inputResBody string
inputReqCode int
inputReqError error
inputMaxReqs int
inputCountResBody string
inputTimeout time.Duration
inputInfo DeviceAuthInfo
inputAudience string
testingErrFunc require.ErrorAssertionFunc
expectedErrorMSG string
testingFunc require.ComparisonAssertionFunc
expectedOut TokenInfo
expectedMSG string
expectPayload string
}
defaultInfo := DeviceAuthInfo{
DeviceCode: "test",
ExpiresIn: 10,
Interval: 1,
}
clientID := "test"
form := url.Values{}
form.Add("grant_type", HostedGrantType)
form.Add("device_code", defaultInfo.DeviceCode)
form.Add("client_id", clientID)
tokenReqPayload := form.Encode()
testCase1 := test{
name: "Payload Is Valid",
inputInfo: defaultInfo,
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
inputReqCode: 200,
testingErrFunc: require.Error,
testingFunc: require.EqualValues,
expectPayload: tokenReqPayload,
}
testCase2 := test{
name: "Exit On Network Error",
inputInfo: defaultInfo,
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
expectPayload: tokenReqPayload,
inputReqError: fmt.Errorf("error"),
testingErrFunc: require.Error,
expectedErrorMSG: "should return error",
testingFunc: require.EqualValues,
}
testCase3 := test{
name: "Exit On 4XX When Not Pending",
inputInfo: defaultInfo,
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
inputReqCode: 400,
expectPayload: tokenReqPayload,
testingErrFunc: require.Error,
expectedErrorMSG: "should return error",
testingFunc: require.EqualValues,
}
testCase4 := test{
name: "Exit On Exit Code 5XX",
inputInfo: defaultInfo,
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
inputReqCode: 500,
expectPayload: tokenReqPayload,
testingErrFunc: require.Error,
expectedErrorMSG: "should return error",
testingFunc: require.EqualValues,
}
testCase5 := test{
name: "Exit On Content Timeout",
inputInfo: defaultInfo,
inputTimeout: 0 * time.Second,
testingErrFunc: require.Error,
expectedErrorMSG: "should return error",
testingFunc: require.EqualValues,
}
audience := "test"
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"aud": audience})
var hmacSampleSecret []byte
tokenString, _ := token.SignedString(hmacSampleSecret)
testCase6 := test{
name: "Exit On Invalid Audience",
inputInfo: defaultInfo,
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\"}", tokenString),
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
inputReqCode: 200,
inputAudience: "super test",
testingErrFunc: require.Error,
testingFunc: require.EqualValues,
expectPayload: tokenReqPayload,
}
testCase7 := test{
name: "Received Token Info",
inputInfo: defaultInfo,
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\"}", tokenString),
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
inputReqCode: 200,
inputAudience: audience,
testingErrFunc: require.NoError,
testingFunc: require.EqualValues,
expectPayload: tokenReqPayload,
expectedOut: TokenInfo{AccessToken: tokenString},
}
testCase8 := test{
name: "Received Token Info after Multiple tries",
inputInfo: defaultInfo,
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\"}", tokenString),
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
inputMaxReqs: 2,
inputCountResBody: "{\"error\":\"authorization_pending\"}",
inputReqCode: 200,
inputAudience: audience,
testingErrFunc: require.NoError,
testingFunc: require.EqualValues,
expectPayload: tokenReqPayload,
expectedOut: TokenInfo{AccessToken: tokenString},
}
for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5, testCase6, testCase7, testCase8} {
t.Run(testCase.name, func(t *testing.T) {
httpClient := mockHTTPClient{
resBody: testCase.inputResBody,
code: testCase.inputReqCode,
err: testCase.inputReqError,
MaxReqs: testCase.inputMaxReqs,
countResBody: testCase.inputCountResBody,
}
hosted := Hosted{
Audience: testCase.inputAudience,
ClientID: clientID,
TokenEndpoint: "test.hosted.com/token",
DeviceAuthEndpoint: "test.hosted.com/device/auth",
HTTPClient: &httpClient,
}
ctx, cancel := context.WithTimeout(context.TODO(), testCase.inputTimeout)
defer cancel()
tokenInfo, err := hosted.WaitToken(ctx, testCase.inputInfo)
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
require.EqualValues(t, testCase.expectPayload, httpClient.reqBody, "payload should match")
testCase.testingFunc(t, testCase.expectedOut, tokenInfo, testCase.expectedMSG)
require.GreaterOrEqualf(t, testCase.inputMaxReqs, httpClient.count, "should run %d times", testCase.inputMaxReqs)
})
}
}

View File

@@ -0,0 +1,716 @@
package peer
import (
"context"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/pion/ice/v2"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl"
"github.com/netbirdio/netbird/client/internal/proxy"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/iface"
signal "github.com/netbirdio/netbird/signal/client"
sProto "github.com/netbirdio/netbird/signal/proto"
"github.com/netbirdio/netbird/version"
)
// ConnConfig is a peer Connection configuration
type ConnConfig struct {
// Key is a public key of a remote peer
Key string
// LocalKey is a public key of a local peer
LocalKey string
// StunTurn is a list of STUN and TURN URLs
StunTurn []*ice.URL
// InterfaceBlackList is a list of machine interfaces that should be filtered out by ICE Candidate gathering
// (e.g. if eth0 is in the list, host candidate of this interface won't be used)
InterfaceBlackList []string
DisableIPv6Discovery bool
Timeout time.Duration
ProxyConfig proxy.Config
UDPMux ice.UDPMux
UDPMuxSrflx ice.UniversalUDPMux
LocalWgPort int
NATExternalIPs []string
}
// OfferAnswer represents a session establishment offer or answer
type OfferAnswer struct {
IceCredentials IceCredentials
// WgListenPort is a remote WireGuard listen port.
// This field is used when establishing a direct WireGuard connection without any proxy.
// We can set the remote peer's endpoint with this port.
WgListenPort int
// Version of NetBird Agent
Version string
}
// IceCredentials ICE protocol credentials struct
type IceCredentials struct {
UFrag string
Pwd string
}
type Conn struct {
config ConnConfig
mu sync.Mutex
// signalCandidate is a handler function to signal remote peer about local connection candidate
signalCandidate func(candidate ice.Candidate) error
// signalOffer is a handler function to signal remote peer our connection offer (credentials)
signalOffer func(OfferAnswer) error
signalAnswer func(OfferAnswer) error
sendSignalMessage func(message *sProto.Message) error
// remoteOffersCh is a channel used to wait for remote credentials to proceed with the connection
remoteOffersCh chan OfferAnswer
// remoteAnswerCh is a channel used to wait for remote credentials answer (confirmation of our offer) to proceed with the connection
remoteAnswerCh chan OfferAnswer
closeCh chan struct{}
ctx context.Context
notifyDisconnected context.CancelFunc
agent *ice.Agent
status ConnStatus
statusRecorder *Status
proxy proxy.Proxy
remoteModeCh chan ModeMessage
meta meta
adapter iface.TunAdapter
iFaceDiscover stdnet.IFaceDiscover
}
// meta holds meta information about a connection
type meta struct {
protoSupport signal.FeaturesSupport
}
// ModeMessage represents a connection mode chosen by the peer
type ModeMessage struct {
// Direct indicates that it decided to use a direct connection
Direct bool
}
// GetConf returns the connection config
func (conn *Conn) GetConf() ConnConfig {
return conn.config
}
// UpdateConf updates the connection config
func (conn *Conn) UpdateConf(conf ConnConfig) {
conn.config = conf
}
// NewConn creates a new not opened Conn to the remote peer.
// To establish a connection run Conn.Open
func NewConn(config ConnConfig, statusRecorder *Status, adapter iface.TunAdapter, iFaceDiscover stdnet.IFaceDiscover) (*Conn, error) {
return &Conn{
config: config,
mu: sync.Mutex{},
status: StatusDisconnected,
closeCh: make(chan struct{}),
remoteOffersCh: make(chan OfferAnswer),
remoteAnswerCh: make(chan OfferAnswer),
statusRecorder: statusRecorder,
remoteModeCh: make(chan ModeMessage, 1),
adapter: adapter,
iFaceDiscover: iFaceDiscover,
}, nil
}
// interfaceFilter is a function passed to ICE Agent to filter out not allowed interfaces
// to avoid building tunnel over them
func interfaceFilter(blackList []string) func(string) bool {
return func(iFace string) bool {
for _, s := range blackList {
if strings.HasPrefix(iFace, s) {
log.Debugf("ignoring interface %s - it is not allowed", iFace)
return false
}
}
// look for unlisted WireGuard interfaces
wg, err := wgctrl.New()
if err != nil {
log.Debugf("trying to create a wgctrl client failed with: %v", err)
return true
}
defer func() {
_ = wg.Close()
}()
_, err = wg.Device(iFace)
return err != nil
}
}
func (conn *Conn) reCreateAgent() error {
conn.mu.Lock()
defer conn.mu.Unlock()
failedTimeout := 6 * time.Second
var err error
transportNet, err := conn.newStdNet()
if err != nil {
log.Warnf("failed to create pion's stdnet: %s", err)
}
agentConfig := &ice.AgentConfig{
MulticastDNSMode: ice.MulticastDNSModeDisabled,
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6},
Urls: conn.config.StunTurn,
CandidateTypes: []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive, ice.CandidateTypeRelay},
FailedTimeout: &failedTimeout,
InterfaceFilter: interfaceFilter(conn.config.InterfaceBlackList),
UDPMux: conn.config.UDPMux,
UDPMuxSrflx: conn.config.UDPMuxSrflx,
NAT1To1IPs: conn.config.NATExternalIPs,
Net: transportNet,
}
if conn.config.DisableIPv6Discovery {
agentConfig.NetworkTypes = []ice.NetworkType{ice.NetworkTypeUDP4}
}
conn.agent, err = ice.NewAgent(agentConfig)
if err != nil {
return err
}
err = conn.agent.OnCandidate(conn.onICECandidate)
if err != nil {
return err
}
err = conn.agent.OnConnectionStateChange(conn.onICEConnectionStateChange)
if err != nil {
return err
}
err = conn.agent.OnSelectedCandidatePairChange(conn.onICESelectedCandidatePair)
if err != nil {
return err
}
return nil
}
// Open opens connection to the remote peer starting ICE candidate gathering process.
// Blocks until connection has been closed or connection timeout.
// ConnStatus will be set accordingly
func (conn *Conn) Open() error {
log.Debugf("trying to connect to peer %s", conn.config.Key)
peerState := State{PubKey: conn.config.Key}
peerState.IP = strings.Split(conn.config.ProxyConfig.AllowedIps, "/")[0]
peerState.ConnStatusUpdate = time.Now()
peerState.ConnStatus = conn.status
err := conn.statusRecorder.UpdatePeerState(peerState)
if err != nil {
log.Warnf("erro while updating the state of peer %s,err: %v", conn.config.Key, err)
}
defer func() {
err := conn.cleanup()
if err != nil {
log.Warnf("error while cleaning up peer connection %s: %v", conn.config.Key, err)
return
}
}()
err = conn.reCreateAgent()
if err != nil {
return err
}
err = conn.sendOffer()
if err != nil {
return err
}
log.Debugf("connection offer sent to peer %s, waiting for the confirmation", conn.config.Key)
// Only continue once we got a connection confirmation from the remote peer.
// The connection timeout could have happened before a confirmation received from the remote.
// The connection could have also been closed externally (e.g. when we received an update from the management that peer shouldn't be connected)
var remoteOfferAnswer OfferAnswer
select {
case remoteOfferAnswer = <-conn.remoteOffersCh:
// received confirmation from the remote peer -> ready to proceed
err = conn.sendAnswer()
if err != nil {
return err
}
case remoteOfferAnswer = <-conn.remoteAnswerCh:
case <-time.After(conn.config.Timeout):
return NewConnectionTimeoutError(conn.config.Key, conn.config.Timeout)
case <-conn.closeCh:
// closed externally
return NewConnectionClosedError(conn.config.Key)
}
log.Debugf("received connection confirmation from peer %s running version %s and with remote WireGuard listen port %d",
conn.config.Key, remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort)
// at this point we received offer/answer and we are ready to gather candidates
conn.mu.Lock()
conn.status = StatusConnecting
conn.ctx, conn.notifyDisconnected = context.WithCancel(context.Background())
defer conn.notifyDisconnected()
conn.mu.Unlock()
peerState = State{PubKey: conn.config.Key}
peerState.ConnStatus = conn.status
peerState.ConnStatusUpdate = time.Now()
err = conn.statusRecorder.UpdatePeerState(peerState)
if err != nil {
log.Warnf("erro while updating the state of peer %s,err: %v", conn.config.Key, err)
}
err = conn.agent.GatherCandidates()
if err != nil {
return err
}
// will block until connection succeeded
// but it won't release if ICE Agent went into Disconnected or Failed state,
// so we have to cancel it with the provided context once agent detected a broken connection
isControlling := conn.config.LocalKey > conn.config.Key
var remoteConn *ice.Conn
if isControlling {
remoteConn, err = conn.agent.Dial(conn.ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
} else {
remoteConn, err = conn.agent.Accept(conn.ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
}
if err != nil {
return err
}
// dynamically set remote WireGuard port is other side specified a different one from the default one
remoteWgPort := iface.DefaultWgPort
if remoteOfferAnswer.WgListenPort != 0 {
remoteWgPort = remoteOfferAnswer.WgListenPort
}
// the ice connection has been established successfully so we are ready to start the proxy
err = conn.startProxy(remoteConn, remoteWgPort)
if err != nil {
return err
}
if conn.proxy.Type() == proxy.TypeNoProxy {
host, _, _ := net.SplitHostPort(remoteConn.LocalAddr().String())
rhost, _, _ := net.SplitHostPort(remoteConn.RemoteAddr().String())
// direct Wireguard connection
log.Infof("directly connected to peer %s [laddr <-> raddr] [%s:%d <-> %s:%d]", conn.config.Key, host, conn.config.LocalWgPort, rhost, remoteWgPort)
} else {
log.Infof("connected to peer %s [laddr <-> raddr] [%s <-> %s]", conn.config.Key, remoteConn.LocalAddr().String(), remoteConn.RemoteAddr().String())
}
// wait until connection disconnected or has been closed externally (upper layer, e.g. engine)
select {
case <-conn.closeCh:
// closed externally
return NewConnectionClosedError(conn.config.Key)
case <-conn.ctx.Done():
// disconnected from the remote peer
return NewConnectionDisconnectedError(conn.config.Key)
}
}
// useProxy determines whether a direct connection (without a go proxy) is possible
//
// There are 2 cases:
//
// * When neither candidate is from hard nat and one of the peers has a public IP
//
// * both peers are in the same private network
//
// Please note, that this check happens when peers were already able to ping each other using ICE layer.
func shouldUseProxy(pair *ice.CandidatePair) bool {
if !isHardNATCandidate(pair.Local) && isHostCandidateWithPublicIP(pair.Remote) {
return false
}
if !isHardNATCandidate(pair.Remote) && isHostCandidateWithPublicIP(pair.Local) {
return false
}
if isHostCandidateWithPrivateIP(pair.Local) && isHostCandidateWithPrivateIP(pair.Remote) {
return false
}
return true
}
func isHardNATCandidate(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeRelay || candidate.Type() == ice.CandidateTypePeerReflexive
}
func isHostCandidateWithPublicIP(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeHost && isPublicIP(candidate.Address())
}
func isHostCandidateWithPrivateIP(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeHost && !isPublicIP(candidate.Address())
}
func isPublicIP(address string) bool {
ip := net.ParseIP(address)
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsPrivate() {
return false
}
return true
}
// startProxy starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
func (conn *Conn) startProxy(remoteConn net.Conn, remoteWgPort int) error {
conn.mu.Lock()
defer conn.mu.Unlock()
var pair *ice.CandidatePair
pair, err := conn.agent.GetSelectedCandidatePair()
if err != nil {
return err
}
peerState := State{PubKey: conn.config.Key}
p := conn.getProxyWithMessageExchange(pair, remoteWgPort)
conn.proxy = p
err = p.Start(remoteConn)
if err != nil {
return err
}
conn.status = StatusConnected
peerState.ConnStatus = conn.status
peerState.ConnStatusUpdate = time.Now()
peerState.LocalIceCandidateType = pair.Local.Type().String()
peerState.RemoteIceCandidateType = pair.Remote.Type().String()
if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay {
peerState.Relayed = true
}
peerState.Direct = p.Type() == proxy.TypeNoProxy
err = conn.statusRecorder.UpdatePeerState(peerState)
if err != nil {
log.Warnf("unable to save peer's state, got error: %v", err)
}
return nil
}
func (conn *Conn) getProxyWithMessageExchange(pair *ice.CandidatePair, remoteWgPort int) proxy.Proxy {
useProxy := shouldUseProxy(pair)
localDirectMode := !useProxy
remoteDirectMode := localDirectMode
if conn.meta.protoSupport.DirectCheck {
go conn.sendLocalDirectMode(localDirectMode)
// will block until message received or timeout
remoteDirectMode = conn.receiveRemoteDirectMode()
}
if localDirectMode && remoteDirectMode {
log.Debugf("using WireGuard direct mode with peer %s", conn.config.Key)
return proxy.NewNoProxy(conn.config.ProxyConfig, remoteWgPort)
}
log.Debugf("falling back to local proxy mode with peer %s", conn.config.Key)
return proxy.NewWireguardProxy(conn.config.ProxyConfig)
}
func (conn *Conn) sendLocalDirectMode(localMode bool) {
// todo what happens when we couldn't deliver this message?
// we could retry, etc but there is no guarantee
err := conn.sendSignalMessage(&sProto.Message{
Key: conn.config.LocalKey,
RemoteKey: conn.config.Key,
Body: &sProto.Body{
Type: sProto.Body_MODE,
Mode: &sProto.Mode{
Direct: &localMode,
},
NetBirdVersion: version.NetbirdVersion(),
},
})
if err != nil {
log.Errorf("failed to send local proxy mode to remote peer %s, error: %s", conn.config.Key, err)
}
}
func (conn *Conn) receiveRemoteDirectMode() bool {
timeout := time.Second
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case receivedMSG := <-conn.remoteModeCh:
return receivedMSG.Direct
case <-timer.C:
// we didn't receive a message from remote so we assume that it supports the direct mode to keep the old behaviour
log.Debugf("timeout after %s while waiting for remote direct mode message from remote peer %s",
timeout, conn.config.Key)
return true
}
}
// cleanup closes all open resources and sets status to StatusDisconnected
func (conn *Conn) cleanup() error {
log.Debugf("trying to cleanup %s", conn.config.Key)
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.agent != nil {
err := conn.agent.Close()
if err != nil {
return err
}
conn.agent = nil
}
if conn.proxy != nil {
err := conn.proxy.Close()
if err != nil {
return err
}
conn.proxy = nil
}
if conn.notifyDisconnected != nil {
conn.notifyDisconnected()
conn.notifyDisconnected = nil
}
conn.status = StatusDisconnected
peerState := State{PubKey: conn.config.Key}
peerState.ConnStatus = conn.status
peerState.ConnStatusUpdate = time.Now()
err := conn.statusRecorder.UpdatePeerState(peerState)
if err != nil {
// pretty common error because by that time Engine can already remove the peer and status won't be available.
//todo rethink status updates
log.Debugf("error while updating peer's %s state, err: %v", conn.config.Key, err)
}
log.Debugf("cleaned up connection to peer %s", conn.config.Key)
return nil
}
// SetSignalOffer sets a handler function to be triggered by Conn when a new connection offer has to be signalled to the remote peer
func (conn *Conn) SetSignalOffer(handler func(offer OfferAnswer) error) {
conn.signalOffer = handler
}
// SetSignalAnswer sets a handler function to be triggered by Conn when a new connection answer has to be signalled to the remote peer
func (conn *Conn) SetSignalAnswer(handler func(answer OfferAnswer) error) {
conn.signalAnswer = handler
}
// SetSignalCandidate sets a handler function to be triggered by Conn when a new ICE local connection candidate has to be signalled to the remote peer
func (conn *Conn) SetSignalCandidate(handler func(candidate ice.Candidate) error) {
conn.signalCandidate = handler
}
// SetSendSignalMessage sets a handler function to be triggered by Conn when there is new message to send via signal
func (conn *Conn) SetSendSignalMessage(handler func(message *sProto.Message) error) {
conn.sendSignalMessage = handler
}
// onICECandidate is a callback attached to an ICE Agent to receive new local connection candidates
// and then signals them to the remote peer
func (conn *Conn) onICECandidate(candidate ice.Candidate) {
if candidate != nil {
// TODO: reported port is incorrect for CandidateTypeHost, makes understanding ICE use via logs confusing as port is ignored
log.Debugf("discovered local candidate %s", candidate.String())
go func() {
err := conn.signalCandidate(candidate)
if err != nil {
log.Errorf("failed signaling candidate to the remote peer %s %s", conn.config.Key, err)
}
}()
}
}
func (conn *Conn) onICESelectedCandidatePair(c1 ice.Candidate, c2 ice.Candidate) {
log.Debugf("selected candidate pair [local <-> remote] -> [%s <-> %s], peer %s", c1.String(), c2.String(),
conn.config.Key)
}
// onICEConnectionStateChange registers callback of an ICE Agent to track connection state
func (conn *Conn) onICEConnectionStateChange(state ice.ConnectionState) {
log.Debugf("peer %s ICE ConnectionState has changed to %s", conn.config.Key, state.String())
if state == ice.ConnectionStateFailed || state == ice.ConnectionStateDisconnected {
conn.notifyDisconnected()
}
}
func (conn *Conn) sendAnswer() error {
conn.mu.Lock()
defer conn.mu.Unlock()
localUFrag, localPwd, err := conn.agent.GetLocalUserCredentials()
if err != nil {
return err
}
log.Debugf("sending answer to %s", conn.config.Key)
err = conn.signalAnswer(OfferAnswer{
IceCredentials: IceCredentials{localUFrag, localPwd},
WgListenPort: conn.config.LocalWgPort,
Version: version.NetbirdVersion(),
})
if err != nil {
return err
}
return nil
}
// sendOffer prepares local user credentials and signals them to the remote peer
func (conn *Conn) sendOffer() error {
conn.mu.Lock()
defer conn.mu.Unlock()
localUFrag, localPwd, err := conn.agent.GetLocalUserCredentials()
if err != nil {
return err
}
err = conn.signalOffer(OfferAnswer{
IceCredentials: IceCredentials{localUFrag, localPwd},
WgListenPort: conn.config.LocalWgPort,
Version: version.NetbirdVersion(),
})
if err != nil {
return err
}
return nil
}
// Close closes this peer Conn issuing a close event to the Conn closeCh
func (conn *Conn) Close() error {
conn.mu.Lock()
defer conn.mu.Unlock()
select {
case conn.closeCh <- struct{}{}:
return nil
default:
// probably could happen when peer has been added and removed right after not even starting to connect
// todo further investigate
// this really happens due to unordered messages coming from management
// more importantly it causes inconsistency -> 2 Conn objects for the same peer
// e.g. this flow:
// update from management has peers: [1,2,3,4]
// engine creates a Conn for peers: [1,2,3,4] and schedules Open in ~1sec
// before conn.Open() another update from management arrives with peers: [1,2,3]
// engine removes peer 4 and calls conn.Close() which does nothing (this default clause)
// before conn.Open() another update from management arrives with peers: [1,2,3,4,5]
// engine adds a new Conn for 4 and 5
// therefore peer 4 has 2 Conn objects
log.Warnf("connection has been already closed or attempted closing not started coonection %s", conn.config.Key)
return NewConnectionAlreadyClosed(conn.config.Key)
}
}
// Status returns current status of the Conn
func (conn *Conn) Status() ConnStatus {
conn.mu.Lock()
defer conn.mu.Unlock()
return conn.status
}
// OnRemoteOffer handles an offer from the remote peer and returns true if the message was accepted, false otherwise
// doesn't block, discards the message if connection wasn't ready
func (conn *Conn) OnRemoteOffer(offer OfferAnswer) bool {
log.Debugf("OnRemoteOffer from peer %s on status %s", conn.config.Key, conn.status.String())
select {
case conn.remoteOffersCh <- offer:
return true
default:
log.Debugf("OnRemoteOffer skipping message from peer %s on status %s because is not ready", conn.config.Key, conn.status.String())
// connection might not be ready yet to receive so we ignore the message
return false
}
}
// OnRemoteAnswer handles an offer from the remote peer and returns true if the message was accepted, false otherwise
// doesn't block, discards the message if connection wasn't ready
func (conn *Conn) OnRemoteAnswer(answer OfferAnswer) bool {
log.Debugf("OnRemoteAnswer from peer %s on status %s", conn.config.Key, conn.status.String())
select {
case conn.remoteAnswerCh <- answer:
return true
default:
// connection might not be ready yet to receive so we ignore the message
log.Debugf("OnRemoteAnswer skipping message from peer %s on status %s because is not ready", conn.config.Key, conn.status.String())
return false
}
}
// OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer.
func (conn *Conn) OnRemoteCandidate(candidate ice.Candidate) {
log.Debugf("OnRemoteCandidate from peer %s -> %s", conn.config.Key, candidate.String())
go func() {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.agent == nil {
return
}
err := conn.agent.AddRemoteCandidate(candidate)
if err != nil {
log.Errorf("error while handling remote candidate from peer %s", conn.config.Key)
return
}
}()
}
func (conn *Conn) GetKey() string {
return conn.config.Key
}
// OnModeMessage unmarshall the payload message and send it to the mode message channel
func (conn *Conn) OnModeMessage(message ModeMessage) error {
select {
case conn.remoteModeCh <- message:
return nil
default:
return fmt.Errorf("unable to process mode message: channel busy")
}
}
// RegisterProtoSupportMeta register supported proto message in the connection metadata
func (conn *Conn) RegisterProtoSupportMeta(support []uint32) {
protoSupport := signal.ParseFeaturesSupported(support)
conn.meta.protoSupport = protoSupport
}

View File

@@ -0,0 +1,29 @@
package peer
import log "github.com/sirupsen/logrus"
const (
// StatusConnected indicate the peer is in connected state
StatusConnected ConnStatus = iota
// StatusConnecting indicate the peer is in connecting state
StatusConnecting
// StatusDisconnected indicate the peer is in disconnected state
StatusDisconnected
)
// ConnStatus describe the status of a peer's connection
type ConnStatus int
func (s ConnStatus) String() string {
switch s {
case StatusConnecting:
return "Connecting"
case StatusConnected:
return "Connected"
case StatusDisconnected:
return "Disconnected"
default:
log.Errorf("unknown status: %d", s)
return "INVALID_PEER_CONNECTION_STATUS"
}
}

View File

@@ -0,0 +1,27 @@
package peer
import (
"github.com/magiconair/properties/assert"
"testing"
)
func TestConnStatus_String(t *testing.T) {
tables := []struct {
name string
status ConnStatus
want string
}{
{"StatusConnected", StatusConnected, "Connected"},
{"StatusDisconnected", StatusDisconnected, "Disconnected"},
{"StatusConnecting", StatusConnecting, "Connecting"},
}
for _, table := range tables {
t.Run(table.name, func(t *testing.T) {
got := table.status.String()
assert.Equal(t, got, table.want, "they should be equal")
})
}
}

View File

@@ -0,0 +1,440 @@
package peer
import (
"sync"
"testing"
"time"
"github.com/magiconair/properties/assert"
"github.com/pion/ice/v2"
"golang.org/x/sync/errgroup"
"github.com/netbirdio/netbird/client/internal/proxy"
"github.com/netbirdio/netbird/iface"
sproto "github.com/netbirdio/netbird/signal/proto"
)
var connConf = ConnConfig{
Key: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
LocalKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
StunTurn: []*ice.URL{},
InterfaceBlackList: nil,
Timeout: time.Second,
ProxyConfig: proxy.Config{},
LocalWgPort: 51820,
}
func TestNewConn_interfaceFilter(t *testing.T) {
ignore := []string{iface.WgInterfaceDefault, "tun0", "zt", "ZeroTier", "utun", "wg", "ts",
"Tailscale", "tailscale"}
filter := interfaceFilter(ignore)
for _, s := range ignore {
assert.Equal(t, filter(s), false)
}
}
func TestConn_GetKey(t *testing.T) {
conn, err := NewConn(connConf, nil, nil, nil)
if err != nil {
return
}
got := conn.GetKey()
assert.Equal(t, got, connConf.Key, "they should be equal")
}
func TestConn_OnRemoteOffer(t *testing.T) {
conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil)
if err != nil {
return
}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
<-conn.remoteOffersCh
wg.Done()
}()
go func() {
for {
accepted := conn.OnRemoteOffer(OfferAnswer{
IceCredentials: IceCredentials{
UFrag: "test",
Pwd: "test",
},
WgListenPort: 0,
Version: "",
})
if accepted {
wg.Done()
return
}
}
}()
wg.Wait()
}
func TestConn_OnRemoteAnswer(t *testing.T) {
conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil)
if err != nil {
return
}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
<-conn.remoteAnswerCh
wg.Done()
}()
go func() {
for {
accepted := conn.OnRemoteAnswer(OfferAnswer{
IceCredentials: IceCredentials{
UFrag: "test",
Pwd: "test",
},
WgListenPort: 0,
Version: "",
})
if accepted {
wg.Done()
return
}
}
}()
wg.Wait()
}
func TestConn_Status(t *testing.T) {
conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil)
if err != nil {
return
}
tables := []struct {
name string
status ConnStatus
want ConnStatus
}{
{"StatusConnected", StatusConnected, StatusConnected},
{"StatusDisconnected", StatusDisconnected, StatusDisconnected},
{"StatusConnecting", StatusConnecting, StatusConnecting},
}
for _, table := range tables {
t.Run(table.name, func(t *testing.T) {
conn.status = table.status
got := conn.Status()
assert.Equal(t, got, table.want, "they should be equal")
})
}
}
func TestConn_Close(t *testing.T) {
conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil)
if err != nil {
return
}
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
<-conn.closeCh
wg.Done()
}()
go func() {
for {
err := conn.Close()
if err != nil {
continue
} else {
return
}
}
}()
wg.Wait()
}
type mockICECandidate struct {
ice.Candidate
AddressFunc func() string
TypeFunc func() ice.CandidateType
}
// Address mocks and overwrite ice.Candidate Address method
func (m *mockICECandidate) Address() string {
if m.AddressFunc != nil {
return m.AddressFunc()
}
return ""
}
// Type mocks and overwrite ice.Candidate Type method
func (m *mockICECandidate) Type() ice.CandidateType {
if m.TypeFunc != nil {
return m.TypeFunc()
}
return ice.CandidateTypeUnspecified
}
func TestConn_ShouldUseProxy(t *testing.T) {
publicHostCandidate := &mockICECandidate{
AddressFunc: func() string {
return "8.8.8.8"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeHost
},
}
privateHostCandidate := &mockICECandidate{
AddressFunc: func() string {
return "10.0.0.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeHost
},
}
srflxCandidate := &mockICECandidate{
AddressFunc: func() string {
return "1.1.1.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeServerReflexive
},
}
prflxCandidate := &mockICECandidate{
AddressFunc: func() string {
return "1.1.1.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypePeerReflexive
},
}
relayCandidate := &mockICECandidate{
AddressFunc: func() string {
return "1.1.1.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeRelay
},
}
testCases := []struct {
name string
candatePair *ice.CandidatePair
expected bool
}{
{
name: "Use Proxy When Local Candidate Is Relay",
candatePair: &ice.CandidatePair{
Local: relayCandidate,
Remote: privateHostCandidate,
},
expected: true,
},
{
name: "Use Proxy When Remote Candidate Is Relay",
candatePair: &ice.CandidatePair{
Local: privateHostCandidate,
Remote: relayCandidate,
},
expected: true,
},
{
name: "Use Proxy When Local Candidate Is Peer Reflexive",
candatePair: &ice.CandidatePair{
Local: prflxCandidate,
Remote: privateHostCandidate,
},
expected: true,
},
{
name: "Use Proxy When Remote Candidate Is Peer Reflexive",
candatePair: &ice.CandidatePair{
Local: privateHostCandidate,
Remote: prflxCandidate,
},
expected: true,
},
{
name: "Don't Use Proxy When Local Candidate Is Public And Remote Is Private",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: privateHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Remote Candidate Is Public And Local Is Private",
candatePair: &ice.CandidatePair{
Local: privateHostCandidate,
Remote: publicHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Local Candidate is Public And Remote Is Server Reflexive",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: srflxCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Remote Candidate is Public And Local Is Server Reflexive",
candatePair: &ice.CandidatePair{
Local: srflxCandidate,
Remote: publicHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Both Candidates Are Public",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: publicHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Both Candidates Are Private",
candatePair: &ice.CandidatePair{
Local: privateHostCandidate,
Remote: privateHostCandidate,
},
expected: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result := shouldUseProxy(testCase.candatePair)
if result != testCase.expected {
t.Errorf("got a different result. Expected %t Got %t", testCase.expected, result)
}
})
}
}
func TestGetProxyWithMessageExchange(t *testing.T) {
publicHostCandidate := &mockICECandidate{
AddressFunc: func() string {
return "8.8.8.8"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeHost
},
}
relayCandidate := &mockICECandidate{
AddressFunc: func() string {
return "1.1.1.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeRelay
},
}
testCases := []struct {
name string
candatePair *ice.CandidatePair
inputDirectModeSupport bool
inputRemoteModeMessage bool
expected proxy.Type
}{
{
name: "Should Result In Using Wireguard Proxy When Local Eval Is Use Proxy",
candatePair: &ice.CandidatePair{
Local: relayCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: true,
inputRemoteModeMessage: true,
expected: proxy.TypeWireguard,
},
{
name: "Should Result In Using Wireguard Proxy When Remote Eval Is Use Proxy",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: true,
inputRemoteModeMessage: false,
expected: proxy.TypeWireguard,
},
{
name: "Should Result In Using Wireguard Proxy When Remote Direct Mode Support Is False And Local Eval Is Use Proxy",
candatePair: &ice.CandidatePair{
Local: relayCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: false,
inputRemoteModeMessage: false,
expected: proxy.TypeWireguard,
},
{
name: "Should Result In Using Direct When Remote Direct Mode Support Is False And Local Eval Is No Use Proxy",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: false,
inputRemoteModeMessage: false,
expected: proxy.TypeNoProxy,
},
{
name: "Should Result In Using Direct When Local And Remote Eval Is No Proxy",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: true,
inputRemoteModeMessage: true,
expected: proxy.TypeNoProxy,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
g := errgroup.Group{}
conn, err := NewConn(connConf, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
conn.meta.protoSupport.DirectCheck = testCase.inputDirectModeSupport
conn.SetSendSignalMessage(func(message *sproto.Message) error {
return nil
})
g.Go(func() error {
return conn.OnModeMessage(ModeMessage{
Direct: testCase.inputRemoteModeMessage,
})
})
resultProxy := conn.getProxyWithMessageExchange(testCase.candatePair, 1000)
err = g.Wait()
if err != nil {
t.Error(err)
}
if resultProxy.Type() != testCase.expected {
t.Errorf("result didn't match expected value: Expected: %s, Got: %s", testCase.expected, resultProxy.Type())
}
})
}
}

View File

@@ -0,0 +1,72 @@
package peer
import (
"fmt"
"time"
)
// ConnectionTimeoutError is an error indicating that a peer Conn has been timed out
type ConnectionTimeoutError struct {
peer string
timeout time.Duration
}
func (e *ConnectionTimeoutError) Error() string {
return fmt.Sprintf("connection to peer %s timed out after %s", e.peer, e.timeout.String())
}
// NewConnectionTimeoutError creates a new ConnectionTimeoutError error
func NewConnectionTimeoutError(peer string, timeout time.Duration) error {
return &ConnectionTimeoutError{
peer: peer,
timeout: timeout,
}
}
// ConnectionClosedError is an error indicating that a peer Conn has been forcefully closed
type ConnectionClosedError struct {
peer string
}
func (e *ConnectionClosedError) Error() string {
return fmt.Sprintf("connection to peer %s has been closed", e.peer)
}
// NewConnectionClosedError creates a new ConnectionClosedError error
func NewConnectionClosedError(peer string) error {
return &ConnectionClosedError{
peer: peer,
}
}
// ConnectionDisconnectedError is an error indicating that a peer Conn has ctx from the remote
type ConnectionDisconnectedError struct {
peer string
}
func (e *ConnectionDisconnectedError) Error() string {
return fmt.Sprintf("disconnected from peer %s", e.peer)
}
// NewConnectionDisconnectedError creates a new ConnectionDisconnectedError error
func NewConnectionDisconnectedError(peer string) error {
return &ConnectionDisconnectedError{
peer: peer,
}
}
// ConnectionAlreadyClosedError is an error indicating that a peer Conn has been already closed and the invocation of the Close() method has been performed over a closed connection
type ConnectionAlreadyClosedError struct {
peer string
}
func (e *ConnectionAlreadyClosedError) Error() string {
return fmt.Sprintf("connection to peer %s has been already closed", e.peer)
}
// NewConnectionAlreadyClosed creates a new ConnectionAlreadyClosedError error
func NewConnectionAlreadyClosed(peer string) error {
return &ConnectionAlreadyClosedError{
peer: peer,
}
}

View File

@@ -0,0 +1,27 @@
package peer
import (
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestNewConnectionClosedError(t *testing.T) {
err := NewConnectionClosedError("X")
assert.Equal(t, &ConnectionClosedError{peer: "X"}, err)
}
func TestNewConnectionDisconnectedError(t *testing.T) {
err := NewConnectionDisconnectedError("X")
assert.Equal(t, &ConnectionDisconnectedError{peer: "X"}, err)
}
func TestNewConnectionTimeoutErrorC(t *testing.T) {
err := NewConnectionTimeoutError("X", time.Second)
assert.Equal(t, &ConnectionTimeoutError{peer: "X", timeout: time.Second}, err)
}
func TestNewConnectionAlreadyClosed(t *testing.T) {
err := NewConnectionAlreadyClosed("X")
assert.Equal(t, &ConnectionAlreadyClosedError{peer: "X"}, err)
}

View File

@@ -0,0 +1,11 @@
package peer
// Listener is a callback type about the NetBird network connection state
type Listener interface {
OnConnected()
OnDisconnected()
OnConnecting()
OnDisconnecting()
OnAddressChanged(string, string)
OnPeersListChanged(int)
}

View File

@@ -0,0 +1,149 @@
package peer
import (
"sync"
)
const (
stateDisconnected = iota
stateConnected
stateConnecting
stateDisconnecting
)
type notifier struct {
serverStateLock sync.Mutex
listenersLock sync.Mutex
listener Listener
currentServerState bool
currentClientState bool
lastNotification int
}
func newNotifier() *notifier {
return &notifier{}
}
func (n *notifier) setListener(listener Listener) {
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
n.serverStateLock.Lock()
n.notifyListener(listener, n.lastNotification)
n.serverStateLock.Unlock()
n.listener = listener
}
func (n *notifier) removeListener() {
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
n.listener = nil
}
func (n *notifier) updateServerStates(mgmState bool, signalState bool) {
n.serverStateLock.Lock()
defer n.serverStateLock.Unlock()
var newState bool
if mgmState && signalState {
newState = true
} else {
newState = false
}
if !n.isServerStateChanged(newState) {
return
}
n.currentServerState = newState
if n.lastNotification == stateDisconnecting {
return
}
n.lastNotification = n.calculateState(newState, n.currentClientState)
n.notify(n.lastNotification)
}
func (n *notifier) clientStart() {
n.serverStateLock.Lock()
defer n.serverStateLock.Unlock()
n.currentClientState = true
n.lastNotification = n.calculateState(n.currentServerState, true)
n.notify(n.lastNotification)
}
func (n *notifier) clientStop() {
n.serverStateLock.Lock()
defer n.serverStateLock.Unlock()
n.currentClientState = false
n.lastNotification = n.calculateState(n.currentServerState, false)
n.notify(n.lastNotification)
}
func (n *notifier) clientTearDown() {
n.serverStateLock.Lock()
defer n.serverStateLock.Unlock()
n.currentClientState = false
n.lastNotification = stateDisconnecting
n.notify(n.lastNotification)
}
func (n *notifier) isServerStateChanged(newState bool) bool {
return n.currentServerState != newState
}
func (n *notifier) notify(state int) {
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
if n.listener == nil {
return
}
n.notifyListener(n.listener, state)
}
func (n *notifier) notifyListener(l Listener, state int) {
go func() {
switch state {
case stateDisconnected:
l.OnDisconnected()
case stateConnected:
l.OnConnected()
case stateConnecting:
l.OnConnecting()
case stateDisconnecting:
l.OnDisconnecting()
}
}()
}
func (n *notifier) calculateState(serverState bool, clientState bool) int {
if serverState && clientState {
return stateConnected
}
if !clientState {
return stateDisconnected
}
return stateConnecting
}
func (n *notifier) peerListChanged(numOfPeers int) {
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
if n.listener == nil {
return
}
n.listener.OnPeersListChanged(numOfPeers)
}
func (n *notifier) localAddressChanged(fqdn, address string) {
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
if n.listener == nil {
return
}
n.listener.OnAddressChanged(fqdn, address)
}

View File

@@ -0,0 +1,98 @@
package peer
import (
"sync"
"testing"
)
type mocListener struct {
lastState int
wg sync.WaitGroup
peers int
}
func (l *mocListener) OnConnected() {
l.lastState = stateConnected
l.wg.Done()
}
func (l *mocListener) OnDisconnected() {
l.lastState = stateDisconnected
l.wg.Done()
}
func (l *mocListener) OnConnecting() {
l.lastState = stateConnecting
l.wg.Done()
}
func (l *mocListener) OnDisconnecting() {
l.lastState = stateDisconnecting
l.wg.Done()
}
func (l *mocListener) OnAddressChanged(host, addr string) {
}
func (l *mocListener) OnPeersListChanged(size int) {
l.peers = size
}
func (l *mocListener) setWaiter() {
l.wg.Add(1)
}
func (l *mocListener) wait() {
l.wg.Wait()
}
func Test_notifier_serverState(t *testing.T) {
type scenario struct {
name string
expected bool
mgmState bool
signalState bool
}
scenarios := []scenario{
{"connected", true, true, true},
{"mgm down", false, false, true},
{"signal down", false, true, false},
{"disconnected", false, false, false},
}
for _, tt := range scenarios {
t.Run(tt.name, func(t *testing.T) {
n := newNotifier()
n.updateServerStates(tt.mgmState, tt.signalState)
if n.currentServerState != tt.expected {
t.Errorf("invalid serverstate: %t, expected: %t", n.currentServerState, tt.expected)
}
})
}
}
func Test_notifier_SetListener(t *testing.T) {
listener := &mocListener{}
listener.setWaiter()
n := newNotifier()
n.lastNotification = stateConnecting
n.setListener(listener)
listener.wait()
if listener.lastState != n.lastNotification {
t.Errorf("invalid state: %d, expected: %d", listener.lastState, n.lastNotification)
}
}
func Test_notifier_RemoveListener(t *testing.T) {
listener := &mocListener{}
listener.setWaiter()
n := newNotifier()
n.lastNotification = stateConnecting
n.setListener(listener)
n.removeListener()
n.peerListChanged(1)
if listener.peers != 0 {
t.Errorf("invalid state: %d", listener.peers)
}
}

View File

@@ -0,0 +1,316 @@
package peer
import (
"errors"
"sync"
"time"
)
// State contains the latest state of a peer
type State struct {
IP string
PubKey string
FQDN string
ConnStatus ConnStatus
ConnStatusUpdate time.Time
Relayed bool
Direct bool
LocalIceCandidateType string
RemoteIceCandidateType string
}
// LocalPeerState contains the latest state of the local peer
type LocalPeerState struct {
IP string
PubKey string
KernelInterface bool
FQDN string
}
// SignalState contains the latest state of a signal connection
type SignalState struct {
URL string
Connected bool
}
// ManagementState contains the latest state of a management connection
type ManagementState struct {
URL string
Connected bool
}
// FullStatus contains the full state held by the Status instance
type FullStatus struct {
Peers []State
ManagementState ManagementState
SignalState SignalState
LocalPeerState LocalPeerState
}
// Status holds a state of peers, signal and management connections
type Status struct {
mux sync.Mutex
peers map[string]State
changeNotify map[string]chan struct{}
signalState bool
managementState bool
localPeer LocalPeerState
offlinePeers []State
mgmAddress string
signalAddress string
notifier *notifier
}
// NewRecorder returns a new Status instance
func NewRecorder(mgmAddress string) *Status {
return &Status{
peers: make(map[string]State),
changeNotify: make(map[string]chan struct{}),
offlinePeers: make([]State, 0),
notifier: newNotifier(),
mgmAddress: mgmAddress,
}
}
// ReplaceOfflinePeers replaces
func (d *Status) ReplaceOfflinePeers(replacement []State) {
d.mux.Lock()
defer d.mux.Unlock()
d.offlinePeers = make([]State, len(replacement))
copy(d.offlinePeers, replacement)
}
// AddPeer adds peer to Daemon status map
func (d *Status) AddPeer(peerPubKey string) error {
d.mux.Lock()
defer d.mux.Unlock()
_, ok := d.peers[peerPubKey]
if ok {
return errors.New("peer already exist")
}
d.peers[peerPubKey] = State{PubKey: peerPubKey, ConnStatus: StatusDisconnected}
return nil
}
// GetPeer adds peer to Daemon status map
func (d *Status) GetPeer(peerPubKey string) (State, error) {
d.mux.Lock()
defer d.mux.Unlock()
state, ok := d.peers[peerPubKey]
if !ok {
return State{}, errors.New("peer not found")
}
return state, nil
}
// RemovePeer removes peer from Daemon status map
func (d *Status) RemovePeer(peerPubKey string) error {
d.mux.Lock()
defer d.mux.Unlock()
_, ok := d.peers[peerPubKey]
if ok {
delete(d.peers, peerPubKey)
return nil
}
d.notifyPeerListChanged()
return errors.New("no peer with to remove")
}
// UpdatePeerState updates peer status
func (d *Status) UpdatePeerState(receivedState State) error {
d.mux.Lock()
defer d.mux.Unlock()
peerState, ok := d.peers[receivedState.PubKey]
if !ok {
return errors.New("peer doesn't exist")
}
if receivedState.IP != "" {
peerState.IP = receivedState.IP
}
if receivedState.ConnStatus != peerState.ConnStatus {
peerState.ConnStatus = receivedState.ConnStatus
peerState.ConnStatusUpdate = receivedState.ConnStatusUpdate
peerState.Direct = receivedState.Direct
peerState.Relayed = receivedState.Relayed
peerState.LocalIceCandidateType = receivedState.LocalIceCandidateType
peerState.RemoteIceCandidateType = receivedState.RemoteIceCandidateType
}
d.peers[receivedState.PubKey] = peerState
ch, found := d.changeNotify[receivedState.PubKey]
if found && ch != nil {
close(ch)
d.changeNotify[receivedState.PubKey] = nil
}
d.notifyPeerListChanged()
return nil
}
// UpdatePeerFQDN update peer's state fqdn only
func (d *Status) UpdatePeerFQDN(peerPubKey, fqdn string) error {
d.mux.Lock()
defer d.mux.Unlock()
peerState, ok := d.peers[peerPubKey]
if !ok {
return errors.New("peer doesn't exist")
}
peerState.FQDN = fqdn
d.peers[peerPubKey] = peerState
d.notifyPeerListChanged()
return nil
}
// GetPeerStateChangeNotifier returns a change notifier channel for a peer
func (d *Status) GetPeerStateChangeNotifier(peer string) <-chan struct{} {
d.mux.Lock()
defer d.mux.Unlock()
ch, found := d.changeNotify[peer]
if !found || ch == nil {
ch = make(chan struct{})
d.changeNotify[peer] = ch
}
return ch
}
// UpdateLocalPeerState updates local peer status
func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) {
d.mux.Lock()
defer d.mux.Unlock()
d.localPeer = localPeerState
d.notifyAddressChanged()
}
// CleanLocalPeerState cleans local peer status
func (d *Status) CleanLocalPeerState() {
d.mux.Lock()
defer d.mux.Unlock()
d.localPeer = LocalPeerState{}
d.notifyAddressChanged()
}
// MarkManagementDisconnected sets ManagementState to disconnected
func (d *Status) MarkManagementDisconnected() {
d.mux.Lock()
defer d.mux.Unlock()
defer d.onConnectionChanged()
d.managementState = false
}
// MarkManagementConnected sets ManagementState to connected
func (d *Status) MarkManagementConnected() {
d.mux.Lock()
defer d.mux.Unlock()
defer d.onConnectionChanged()
d.managementState = true
}
// UpdateSignalAddress update the address of the signal server
func (d *Status) UpdateSignalAddress(signalURL string) {
d.mux.Lock()
defer d.mux.Unlock()
d.signalAddress = signalURL
}
// UpdateManagementAddress update the address of the management server
func (d *Status) UpdateManagementAddress(mgmAddress string) {
d.mux.Lock()
defer d.mux.Unlock()
d.mgmAddress = mgmAddress
}
// MarkSignalDisconnected sets SignalState to disconnected
func (d *Status) MarkSignalDisconnected() {
d.mux.Lock()
defer d.mux.Unlock()
defer d.onConnectionChanged()
d.signalState = false
}
// MarkSignalConnected sets SignalState to connected
func (d *Status) MarkSignalConnected() {
d.mux.Lock()
defer d.mux.Unlock()
defer d.onConnectionChanged()
d.signalState = true
}
// GetFullStatus gets full status
func (d *Status) GetFullStatus() FullStatus {
d.mux.Lock()
defer d.mux.Unlock()
fullStatus := FullStatus{
ManagementState: ManagementState{
d.mgmAddress,
d.managementState,
},
SignalState: SignalState{
d.signalAddress,
d.signalState,
},
LocalPeerState: d.localPeer,
}
for _, status := range d.peers {
fullStatus.Peers = append(fullStatus.Peers, status)
}
fullStatus.Peers = append(fullStatus.Peers, d.offlinePeers...)
return fullStatus
}
// ClientStart will notify all listeners about the new service state
func (d *Status) ClientStart() {
d.notifier.clientStart()
}
// ClientStop will notify all listeners about the new service state
func (d *Status) ClientStop() {
d.notifier.clientStop()
}
// ClientTeardown will notify all listeners about the service is under teardown
func (d *Status) ClientTeardown() {
d.notifier.clientTearDown()
}
// SetConnectionListener set a listener to the notifier
func (d *Status) SetConnectionListener(listener Listener) {
d.notifier.setListener(listener)
}
// RemoveConnectionListener remove the listener from the notifier
func (d *Status) RemoveConnectionListener() {
d.notifier.removeListener()
}
func (d *Status) onConnectionChanged() {
d.notifier.updateServerStates(d.managementState, d.signalState)
}
func (d *Status) notifyPeerListChanged() {
d.notifier.peerListChanged(len(d.peers))
}
func (d *Status) notifyAddressChanged() {
d.notifier.localAddressChanged(d.localPeer.FQDN, d.localPeer.IP)
}

View File

@@ -0,0 +1,233 @@
package peer
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAddPeer(t *testing.T) {
key := "abc"
status := NewRecorder("https://mgm")
err := status.AddPeer(key)
assert.NoError(t, err, "shouldn't return error")
_, exists := status.peers[key]
assert.True(t, exists, "value was found")
err = status.AddPeer(key)
assert.Error(t, err, "should return error on duplicate")
}
func TestGetPeer(t *testing.T) {
key := "abc"
status := NewRecorder("https://mgm")
err := status.AddPeer(key)
assert.NoError(t, err, "shouldn't return error")
peerStatus, err := status.GetPeer(key)
assert.NoError(t, err, "shouldn't return error on getting peer")
assert.Equal(t, key, peerStatus.PubKey, "retrieved public key should match")
_, err = status.GetPeer("non_existing_key")
assert.Error(t, err, "should return error when peer doesn't exist")
}
func TestUpdatePeerState(t *testing.T) {
key := "abc"
ip := "10.10.10.10"
status := NewRecorder("https://mgm")
peerState := State{
PubKey: key,
}
status.peers[key] = peerState
peerState.IP = ip
err := status.UpdatePeerState(peerState)
assert.NoError(t, err, "shouldn't return error")
state, exists := status.peers[key]
assert.True(t, exists, "state should be found")
assert.Equal(t, ip, state.IP, "ip should be equal")
}
func TestStatus_UpdatePeerFQDN(t *testing.T) {
key := "abc"
fqdn := "peer-a.netbird.local"
status := NewRecorder("https://mgm")
peerState := State{
PubKey: key,
}
status.peers[key] = peerState
err := status.UpdatePeerFQDN(key, fqdn)
assert.NoError(t, err, "shouldn't return error")
state, exists := status.peers[key]
assert.True(t, exists, "state should be found")
assert.Equal(t, fqdn, state.FQDN, "fqdn should be equal")
}
func TestGetPeerStateChangeNotifierLogic(t *testing.T) {
key := "abc"
ip := "10.10.10.10"
status := NewRecorder("https://mgm")
peerState := State{
PubKey: key,
}
status.peers[key] = peerState
ch := status.GetPeerStateChangeNotifier(key)
assert.NotNil(t, ch, "channel shouldn't be nil")
peerState.IP = ip
err := status.UpdatePeerState(peerState)
assert.NoError(t, err, "shouldn't return error")
select {
case <-ch:
default:
t.Errorf("channel wasn't closed after update")
}
}
func TestRemovePeer(t *testing.T) {
key := "abc"
status := NewRecorder("https://mgm")
peerState := State{
PubKey: key,
}
status.peers[key] = peerState
err := status.RemovePeer(key)
assert.NoError(t, err, "shouldn't return error")
_, exists := status.peers[key]
assert.False(t, exists, "state value shouldn't be found")
err = status.RemovePeer("not existing")
assert.Error(t, err, "should return error when peer doesn't exist")
}
func TestUpdateLocalPeerState(t *testing.T) {
localPeerState := LocalPeerState{
IP: "10.10.10.10",
PubKey: "abc",
KernelInterface: false,
}
status := NewRecorder("https://mgm")
status.UpdateLocalPeerState(localPeerState)
assert.Equal(t, localPeerState, status.localPeer, "local peer status should be equal")
}
func TestCleanLocalPeerState(t *testing.T) {
emptyLocalPeerState := LocalPeerState{}
localPeerState := LocalPeerState{
IP: "10.10.10.10",
PubKey: "abc",
KernelInterface: false,
}
status := NewRecorder("https://mgm")
status.localPeer = localPeerState
status.CleanLocalPeerState()
assert.Equal(t, emptyLocalPeerState, status.localPeer, "local peer status should be empty")
}
func TestUpdateSignalState(t *testing.T) {
url := "https://signal"
var tests = []struct {
name string
connected bool
want bool
}{
{"should mark as connected", true, true},
{"should mark as disconnected", false, false},
}
status := NewRecorder("https://mgm")
status.UpdateSignalAddress(url)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.connected {
status.MarkSignalConnected()
} else {
status.MarkSignalDisconnected()
}
assert.Equal(t, test.want, status.signalState, "signal status should be equal")
})
}
}
func TestUpdateManagementState(t *testing.T) {
url := "https://management"
var tests = []struct {
name string
connected bool
want bool
}{
{"should mark as connected", true, true},
{"should mark as disconnected", false, false},
}
status := NewRecorder(url)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.connected {
status.MarkManagementConnected()
} else {
status.MarkManagementDisconnected()
}
assert.Equal(t, test.want, status.managementState, "signalState status should be equal")
})
}
}
func TestGetFullStatus(t *testing.T) {
key1 := "abc"
key2 := "def"
signalAddr := "https://signal"
managementState := ManagementState{
URL: "https://mgm",
Connected: true,
}
signalState := SignalState{
URL: signalAddr,
Connected: true,
}
peerState1 := State{
PubKey: key1,
}
peerState2 := State{
PubKey: key2,
}
status := NewRecorder("https://mgm")
status.UpdateSignalAddress(signalAddr)
status.managementState = managementState.Connected
status.signalState = signalState.Connected
status.peers[key1] = peerState1
status.peers[key2] = peerState2
fullStatus := status.GetFullStatus()
assert.Equal(t, managementState, fullStatus.ManagementState, "management status should be equal")
assert.Equal(t, signalState, fullStatus.SignalState, "signal status should be equal")
assert.ElementsMatch(t, []State{peerState1, peerState2}, fullStatus.Peers, "peers states should match")
}

View File

@@ -0,0 +1,11 @@
//go:build !android
package peer
import (
"github.com/pion/transport/v2/stdnet"
)
func (conn *Conn) newStdNet() (*stdnet.Net, error) {
return stdnet.NewNet()
}

View File

@@ -0,0 +1,7 @@
package peer
import "github.com/netbirdio/netbird/client/internal/stdnet"
func (conn *Conn) newStdNet() (*stdnet.Net, error) {
return stdnet.NewNet(conn.iFaceDiscover)
}

View File

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

View File

@@ -0,0 +1,55 @@
package proxy
import (
log "github.com/sirupsen/logrus"
"net"
)
// NoProxy is used when there is no need for a proxy between ICE and Wireguard.
// This is possible in either of these cases:
// - peers are in the same local network
// - one of the peers has a public static IP (host)
// NoProxy will just update remote peer with a remote host and fixed Wireguard port (r.g. 51820).
// In order NoProxy to work, Wireguard port has to be fixed for the time being.
type NoProxy struct {
config Config
// RemoteWgListenPort is a WireGuard port of a remote peer.
// It is used instead of the hardcoded 51820 port.
RemoteWgListenPort int
}
// NewNoProxy creates a new NoProxy with a provided config and remote peer's WireGuard listen port
func NewNoProxy(config Config, remoteWgPort int) *NoProxy {
return &NoProxy{config: config, RemoteWgListenPort: remoteWgPort}
}
func (p *NoProxy) Close() error {
err := p.config.WgInterface.RemovePeer(p.config.RemoteKey)
if err != nil {
return err
}
return nil
}
// Start just updates Wireguard peer with the remote IP and default Wireguard port
func (p *NoProxy) Start(remoteConn net.Conn) error {
log.Debugf("using NoProxy while connecting to peer %s", p.config.RemoteKey)
addr, err := net.ResolveUDPAddr("udp", remoteConn.RemoteAddr().String())
if err != nil {
return err
}
addr.Port = p.RemoteWgListenPort
err = p.config.WgInterface.UpdatePeer(p.config.RemoteKey, p.config.AllowedIps, DefaultWgKeepAlive,
addr, p.config.PreSharedKey)
if err != nil {
return err
}
return nil
}
func (p *NoProxy) Type() Type {
return TypeNoProxy
}

View File

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

View File

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

View File

@@ -0,0 +1,282 @@
package routemanager
import (
"context"
"fmt"
"net/netip"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/route"
)
type routerPeerStatus struct {
connected bool
relayed bool
direct bool
}
type routesUpdate struct {
updateSerial uint64
routes []*route.Route
}
type clientNetwork struct {
ctx context.Context
stop context.CancelFunc
statusRecorder *peer.Status
wgInterface *iface.WGIface
routes map[string]*route.Route
routeUpdate chan routesUpdate
peerStateUpdate chan struct{}
routePeersNotifiers map[string]chan struct{}
chosenRoute *route.Route
network netip.Prefix
updateSerial uint64
}
func newClientNetworkWatcher(ctx context.Context, wgInterface *iface.WGIface, statusRecorder *peer.Status, network netip.Prefix) *clientNetwork {
ctx, cancel := context.WithCancel(ctx)
client := &clientNetwork{
ctx: ctx,
stop: cancel,
statusRecorder: statusRecorder,
wgInterface: wgInterface,
routes: make(map[string]*route.Route),
routePeersNotifiers: make(map[string]chan struct{}),
routeUpdate: make(chan routesUpdate),
peerStateUpdate: make(chan struct{}),
network: network,
}
return client
}
func (c *clientNetwork) getRouterPeerStatuses() map[string]routerPeerStatus {
routePeerStatuses := make(map[string]routerPeerStatus)
for _, r := range c.routes {
peerStatus, err := c.statusRecorder.GetPeer(r.Peer)
if err != nil {
log.Debugf("couldn't fetch peer state: %v", err)
continue
}
routePeerStatuses[r.ID] = routerPeerStatus{
connected: peerStatus.ConnStatus == peer.StatusConnected,
relayed: peerStatus.Relayed,
direct: peerStatus.Direct,
}
}
return routePeerStatuses
}
func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[string]routerPeerStatus) string {
var chosen string
chosenScore := 0
currID := ""
if c.chosenRoute != nil {
currID = c.chosenRoute.ID
}
for _, r := range c.routes {
tempScore := 0
peerStatus, found := routePeerStatuses[r.ID]
if !found || !peerStatus.connected {
continue
}
if r.Metric < route.MaxMetric {
metricDiff := route.MaxMetric - r.Metric
tempScore = metricDiff * 10
}
if !peerStatus.relayed {
tempScore++
}
if !peerStatus.direct {
tempScore++
}
if tempScore > chosenScore || (tempScore == chosenScore && currID == r.ID) {
chosen = r.ID
chosenScore = tempScore
}
}
if chosen == "" {
var peers []string
for _, r := range c.routes {
peers = append(peers, r.Peer)
}
log.Warnf("no route was chosen for network %s because no peers from list %s were connected", c.network, peers)
} else if chosen != currID {
log.Infof("new chosen route is %s with peer %s with score %d", chosen, c.routes[chosen].Peer, chosenScore)
}
return chosen
}
func (c *clientNetwork) watchPeerStatusChanges(ctx context.Context, peerKey string, peerStateUpdate chan struct{}, closer chan struct{}) {
for {
select {
case <-ctx.Done():
return
case <-closer:
return
case <-c.statusRecorder.GetPeerStateChangeNotifier(peerKey):
state, err := c.statusRecorder.GetPeer(peerKey)
if err != nil || state.ConnStatus == peer.StatusConnecting {
continue
}
peerStateUpdate <- struct{}{}
log.Debugf("triggered route state update for Peer %s, state: %s", peerKey, state.ConnStatus)
}
}
}
func (c *clientNetwork) startPeersStatusChangeWatcher() {
for _, r := range c.routes {
_, found := c.routePeersNotifiers[r.Peer]
if !found {
c.routePeersNotifiers[r.Peer] = make(chan struct{})
go c.watchPeerStatusChanges(c.ctx, r.Peer, c.peerStateUpdate, c.routePeersNotifiers[r.Peer])
}
}
}
func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error {
state, err := c.statusRecorder.GetPeer(peerKey)
if err != nil || state.ConnStatus != peer.StatusConnected {
return nil
}
err = c.wgInterface.RemoveAllowedIP(peerKey, c.network.String())
if err != nil {
return fmt.Errorf("couldn't remove allowed IP %s removed for peer %s, err: %v",
c.network, c.chosenRoute.Peer, err)
}
return nil
}
func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
if c.chosenRoute != nil {
err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer)
if err != nil {
return err
}
err = removeFromRouteTableIfNonSystem(c.network, c.wgInterface.Address().IP.String())
if err != nil {
return fmt.Errorf("couldn't remove route %s from system, err: %v",
c.network, err)
}
}
return nil
}
func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
var err error
routerPeerStatuses := c.getRouterPeerStatuses()
chosen := c.getBestRouteFromStatuses(routerPeerStatuses)
if chosen == "" {
err = c.removeRouteFromPeerAndSystem()
if err != nil {
return err
}
c.chosenRoute = nil
return nil
}
if c.chosenRoute != nil && c.chosenRoute.ID == chosen {
if c.chosenRoute.IsEqual(c.routes[chosen]) {
return nil
}
}
if c.chosenRoute != nil {
err = c.removeRouteFromWireguardPeer(c.chosenRoute.Peer)
if err != nil {
return err
}
} else {
err = addToRouteTableIfNoExists(c.network, c.wgInterface.Address().IP.String())
if err != nil {
return fmt.Errorf("route %s couldn't be added for peer %s, err: %v",
c.network.String(), c.wgInterface.Address().IP.String(), err)
}
}
c.chosenRoute = c.routes[chosen]
err = c.wgInterface.AddAllowedIP(c.chosenRoute.Peer, c.network.String())
if err != nil {
log.Errorf("couldn't add allowed IP %s added for peer %s, err: %v",
c.network, c.chosenRoute.Peer, err)
}
return nil
}
func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) {
go func() {
c.routeUpdate <- update
}()
}
func (c *clientNetwork) handleUpdate(update routesUpdate) {
updateMap := make(map[string]*route.Route)
for _, r := range update.routes {
updateMap[r.ID] = r
}
for id, r := range c.routes {
_, found := updateMap[id]
if !found {
close(c.routePeersNotifiers[r.Peer])
delete(c.routePeersNotifiers, r.Peer)
}
}
c.routes = updateMap
}
// peersStateAndUpdateWatcher is the main point of reacting on client network routing events.
// All the processing related to the client network should be done here. Thread-safe.
func (c *clientNetwork) peersStateAndUpdateWatcher() {
for {
select {
case <-c.ctx.Done():
log.Debugf("stopping watcher for network %s", c.network)
err := c.removeRouteFromPeerAndSystem()
if err != nil {
log.Error(err)
}
return
case <-c.peerStateUpdate:
err := c.recalculateRouteAndUpdatePeerAndSystem()
if err != nil {
log.Error(err)
}
case update := <-c.routeUpdate:
if update.updateSerial < c.updateSerial {
log.Warnf("received a routes update with smaller serial number, ignoring it")
continue
}
log.Debugf("received a new client network route update for %s", c.network)
c.handleUpdate(update)
c.updateSerial = update.updateSerial
err := c.recalculateRouteAndUpdatePeerAndSystem()
if err != nil {
log.Error(err)
}
c.startPeersStatusChangeWatcher()
}
}
}

View File

@@ -0,0 +1,75 @@
package routemanager
var insertRuleTestCases = []struct {
name string
inputPair routerPair
ipVersion string
}{
{
name: "Insert Forwarding IPV4 Rule",
inputPair: routerPair{
ID: "zxa",
source: "100.100.100.1/32",
destination: "100.100.200.0/24",
masquerade: false,
},
ipVersion: ipv4,
},
{
name: "Insert Forwarding And Nat IPV4 Rules",
inputPair: routerPair{
ID: "zxa",
source: "100.100.100.1/32",
destination: "100.100.200.0/24",
masquerade: true,
},
ipVersion: ipv4,
},
{
name: "Insert Forwarding IPV6 Rule",
inputPair: routerPair{
ID: "zxa",
source: "fc00::1/128",
destination: "fc12::/64",
masquerade: false,
},
ipVersion: ipv6,
},
{
name: "Insert Forwarding And Nat IPV6 Rules",
inputPair: routerPair{
ID: "zxa",
source: "fc00::1/128",
destination: "fc12::/64",
masquerade: true,
},
ipVersion: ipv6,
},
}
var removeRuleTestCases = []struct {
name string
inputPair routerPair
ipVersion string
}{
{
name: "Remove Forwarding And Nat IPV4 Rules",
inputPair: routerPair{
ID: "zxa",
source: "100.100.100.1/32",
destination: "100.100.200.0/24",
masquerade: true,
},
ipVersion: ipv4,
},
{
name: "Remove Forwarding And Nat IPV6 Rules",
inputPair: routerPair{
ID: "zxa",
source: "fc00::1/128",
destination: "fc12::/64",
masquerade: true,
},
ipVersion: ipv6,
},
}

View File

@@ -0,0 +1,12 @@
package routemanager
type firewallManager interface {
// RestoreOrCreateContainers restores or creates a firewall container set of rules, tables and default rules
RestoreOrCreateContainers() error
// InsertRoutingRules inserts a routing firewall rule
InsertRoutingRules(pair routerPair) error
// RemoveRoutingRules removes a routing firewall rule
RemoveRoutingRules(pair routerPair) error
// CleanRoutingRules cleans a firewall set of containers
CleanRoutingRules()
}

Some files were not shown because too many files have changed in this diff Show More