Compare commits

...

136 Commits

Author SHA1 Message Date
Misha Bragin
9c2dc05df1 Eval/higher timeouts (#1776) 2024-03-31 19:39:52 +02:00
Misha Bragin
40d56e5d29 Update network security image (#1765) 2024-03-28 18:43:32 +01:00
Viktor Liu
fd23d0c28f Don't block on failed routing setup (#1768) 2024-03-28 18:12:25 +01:00
Viktor Liu
4fff93a1f2 Ignore unsupported address families (#1766) 2024-03-28 13:06:54 +01:00
Misha Bragin
22beac1b1b Fix invalid token due to the cache race (#1763) 2024-03-28 12:33:56 +01:00
Jeremy Wu
bd7a65d798 support to configure extra blacklist of iface in "up" command (#1734)
Support to configure extra blacklist of iface in "up" command
2024-03-28 09:56:41 +01:00
Zoltan Papp
2d76b058fc Feature/peer validator (#1553)
Follow up management-integrations changes

move groups to separated packages to avoid circle dependencies
save location information in Login action
2024-03-27 18:48:48 +01:00
pascal-fischer
ea2d060f93 Add limited dashboard view (#1738) 2024-03-27 16:11:45 +01:00
Zoltan Papp
68b377a28c Collect chassis.serial (#1748) 2024-03-26 15:33:01 +01:00
pascal-fischer
af50eb350f Change log level for JWT override message of single account mode (#1747) 2024-03-25 14:25:26 +01:00
Viktor Liu
2475473227 Support client default routes for Linux (#1667)
All routes are now installed in a custom netbird routing table.
Management and wireguard traffic is now marked with a custom fwmark.
When the mark is present the traffic is routed via the main routing table, bypassing the VPN.
When the mark is absent the traffic is routed via the netbird routing table, if:
- there's no match in the main routing table
- it would match the default route in the routing table

IPv6 traffic is blocked when a default route IPv4 route is configured to avoid leakage.
2024-03-21 16:49:28 +01:00
pascal-fischer
846871913d Add latency checks to peer connection and status output (#1725)
* adding peer healthcheck

* generate proto file

* fix return in udp mux and replace with continue

* use ice agent for latency checks

* fix status output

* remove some logs

* fix status test

* revert bind and ebpf code

* fix error handling on binding response callback

* extend error handling on binding response callback

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2024-03-20 11:18:34 +01:00
Viktor Liu
6cba9c0818 Remove context niling (#1729) 2024-03-19 12:32:07 +01:00
Maycon Santos
f0672b87bc Add missing dns domain to tests to avoid verbose test logs (#1724) 2024-03-18 12:25:01 +01:00
Maycon Santos
9b0fe2c8e5 Validate authentik issuer url (#1723)
* Validate authentik issuer url

* test issuer

* adjust test times on windows
2024-03-18 10:12:46 +01:00
Misha Bragin
abd57d1191 Avoid creating duplicate groups with the same name (#1579)
Avoid creating groups with the same name via API calls. 

JWT and integrations still allowed to register groups with duplicated names
2024-03-17 11:13:39 +01:00
Maycon Santos
416f04c27a Unblock ACL apply filtering because of dns probes (#1711)
moved the e.dnsServer.ProbeAvailability() to run after ACL apply filtering

run the probes in parallel
2024-03-15 18:57:18 +01:00
Maycon Santos
fc7c1e397f Disable force jsonfile variable (#1611)
This enables windows management tests

Added another DNS server to the dns server tests
2024-03-15 10:50:02 +01:00
Aaron Turner
52a3ac6b06 Add support for inviting/deleting users via Zitadel (#1572)
This fixes the "Invite User" button in Dashboard v2.0.0
and enables the usage of the --user-delete-from-idp flag for Zitadel.

Unlike the NetBird SaaS solution, we rely on Zitadel to send
the emails on our behalf.
2024-03-15 10:32:51 +01:00
Yury Gargay
0b3b50c705 Remove deprecated Rules API endpoints (#1523) 2024-03-14 21:31:21 +01:00
Maycon Santos
042141db06 Update account attributes only when there is a domain (#1701)
add log for when a domain is not present
2024-03-14 14:17:22 +01:00
Viktor Liu
4a1aee1ae0 Add routes and dns servers to status command (#1680)
* Add routes (client and server) to status command
* Add DNS servers to status output
2024-03-12 19:06:16 +01:00
Krzysztof Nazarewski
ba33572ec9 add --service/-s flag for specifying system service name (#1691) 2024-03-12 18:29:19 +01:00
Maycon Santos
9d213e0b54 Add fallback retry to daemon (#1690)
This change adds a fallback retry to the daemon service.

this retry has a larger interval with a shorter max retry run time
then others retries
2024-03-12 18:05:41 +01:00
Maycon Santos
5dde044fa5 Check for record not found when searching the store (#1686)
This change returns status.NotFound only on gorm.ErrRecordNotFound and status.Internal on every other DB error
2024-03-10 19:09:45 +01:00
Bethuel Mmbaga
5a3d9e401f Send terminal notification on peer session expiry (#1660)
Send notification through terminal on user session expiration in Linux and macOS, 
unless UI application is installed to handle it instead.
2024-03-08 18:28:13 +01:00
Maycon Santos
fde1a2196c add ansible collections contributions (#1675) 2024-03-06 23:30:16 +01:00
Maycon Santos
0aeb87742a Return 1s when next expiration is too low (#1672)
using the login expired issue could cause problems with ticker used in the scheduler

This change makes 1s the minimum number returned when rescheduling the peer expiration task
2024-03-06 15:18:53 +01:00
Deniz Adrian
6d747b2f83 Do not fail on virtualized windows systems (#1669)
this handles virtualized systems without Win32_ComputerSystemProduct entries by returning 'unknown' for system product name

Co-authored-by: Bjoern Brauer <zaubernerd@zaubernerd.de>
2024-03-06 14:32:34 +01:00
Viktor Liu
199bf73103 Remove usage stats (#1665) 2024-03-05 09:45:32 +01:00
Maycon Santos
17f5abc653 Handle canceling schedule and avoid recursive call (#1636)
Using time.Ticker allows us to avoid recursive calls that may end up in schedule running and possible deadlock if no routine is listening for cancel calls
2024-03-03 10:35:01 +01:00
Maycon Santos
aa935bdae3 Register creation time for peer, user and account (#1654)
This change register creation time for new peers, users and accounts
2024-03-02 13:49:40 +01:00
Bethuel Mmbaga
452419c4c3 Send UI notification on peer connection session expiry (#1646)
notify the user when the peer connection session expires after it switches from connected 
to disconnected and the status is login required
2024-03-01 17:57:17 +01:00
Viktor Liu
17b1099032 Enhance DNS failover reliability (#1637)
* Fix using wrong array index in log to avoid potential panic

* Increase gRPC connection timeout and add the timeout resolv.conf option

This makes sure the dns client is able to failover to a second
configured nameserver, if present. That is the case then when using the
dns `file` manager and a resolv.conf file generated for netbird.

* On file backup restore, remove the first NS if it's the netbird NS

* Bump dns mangager discovery message from debug to info to ease debugging
2024-03-01 15:17:35 +01:00
pascal-fischer
a4b9e93217 add environment meta from grpc to store (#1651) 2024-03-01 15:15:56 +01:00
Maycon Santos
63d7957140 Log version when starting management and signal (#1649)
Output log version at service startup
2024-02-29 16:05:47 +01:00
Maycon Santos
9a6814deff Enable review linter with exported docs check (#1647)
Exported function and methods should have comments. This warns on undocumented exported functions and methods.
2024-02-29 16:05:31 +01:00
Misha Bragin
190698bcf2 Add open-source network security image (#1643) 2024-02-29 14:04:32 +00:00
pascal-fischer
468fa2940b add quotes to all timestamps in openapi spec (#1642) 2024-02-29 13:09:43 +01:00
Maycon Santos
79a0647a26 Update bug-issue-report.md label (#1640) 2024-02-29 10:21:00 +01:00
Maycon Santos
17ceb3bde8 Add reverse proxy settings to management.json template (#1639)
Add grpc_set_header to the nginx example template
2024-02-28 20:24:28 +01:00
Yury Gargay
5a8f1763a6 Implement Auth0 IdP get all connections method (#1613) 2024-02-28 16:57:35 +01:00
Bethuel Mmbaga
f64e73ca70 Fix invalid cross-device link when moving geolocation databases (#1638)
* Fix invalid cross-device link when move geonames db

* Add test for geolocation databases in workflow

This step checks the existence and proper functioning of geolocation databases, including GeoLite2-City.mmdb and Geonames.db. It will help us ensure that geolocation databases are loaded correctly in the management.

* Enable debug mode

* Increase sleep duration in geolocation tests
2024-02-28 16:42:33 +03:00
pascal-fischer
b085419ab8 FIx order when validating account settings (#1632)
* moved extraSettings validation to the end

* moved extraSettings validation directly after permission check
2024-02-27 14:17:22 +01:00
Bethuel Mmbaga
d78b652ff7 Rename PrivateNetworkCheck to PeerNetworkRangeCheck (#1629)
* Rename PrivateNetworkCheck to PeerNetworkRangeCheck

* update description and example

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2024-02-27 11:59:48 +01:00
Viktor Liu
7251150c1c Combine update-available and connected/disconnected tray icon states (#1615)
This PR updates the system tray icons to reflect both connection status and availability of updates. Now, the tray will show distinct icons for the following states: connected, disconnected, update available while connected, and update available while disconnected. This change improves user experience by providing a clear visual status indicator.

- Add new icons for connected and disconnected states with update available.
- Implement logic to switch icons based on connection status and update availability.
- Remove old icon references for default and update states.
2024-02-26 23:28:33 +01:00
Bethuel Mmbaga
b65c2f69b0 Add support for downloading Geo databases to the management service (#1626)
Adds support for downloading Geo databases to the management service. If the Geo databases are not found, the service will automatically attempt to download them during startup.
2024-02-26 22:49:28 +01:00
Yury Gargay
d8ce08d898 Extend bypass middleware with support of wildcard paths (#1628)
---------

Co-authored-by: Viktor Liu <viktor@netbird.io>
2024-02-26 17:54:58 +01:00
Maycon Santos
e1c50248d9 Add support for device flow on getting started with zitadel (#1616) 2024-02-26 12:33:16 +01:00
Maycon Santos
ce2d14c08e Update download-geolite2.sh to use packages URLs (#1624)
makes use of our hosted download URLs for geolocation DBs
2024-02-26 10:29:54 +01:00
Misha Bragin
52fd9a575a Add quantum resistance status output (#1608) 2024-02-24 11:41:13 +00:00
Yury Gargay
9028c3c1f7 Check git status after go mod tidy on CI (#1614) 2024-02-23 12:00:43 +01:00
ph1ll
9357a587e9 Make SQLite default in configuration generation script (#1610) 2024-02-23 11:43:11 +01:00
Bethuel Mmbaga
a47c69c472 Add private network posture check (#1606)
* wip: Add PrivateNetworkCheck checks interface implementation

* use generic CheckAction constant

* Add private network check to posture checks

* Fix copy function target in posture checks

* Add network check functionality to posture package

* regenerate the openapi specs

* Update Posture Check actions in test file

* Remove unused function

* Refactor network address handling in PrivateNetworkCheck

* Refactor Prefixes to Ranges in private network checks

* Implement private network checks in posture checks handler tests

* Add test for check copy

* Add gorm serializer for network range
2024-02-22 19:22:43 +03:00
Yury Gargay
bbea4c3cc3 Use SQLite store as default when running tests when env is not set (#1612) 2024-02-22 16:51:56 +01:00
Viktor Liu
b7a6cbfaa5 Add account usage logic (#1567)
---------

Co-authored-by: Yury Gargay <yury.gargay@gmail.com>
2024-02-22 12:27:08 +01:00
pascal-fischer
e18bf565a2 Add permissive mode to rosenpass (#1599)
* add rosenpass-permissive flag

* Clarify rosenpass-permissive flag message

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>

---------

Co-authored-by: Misha Bragin <bangvalo@gmail.com>
Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2024-02-21 17:23:17 +01:00
Bethuel Mmbaga
51fa3c92c5 Fix copying function in posture checks process (#1605) 2024-02-21 19:19:13 +03:00
Maycon Santos
d65602f904 Add posture checks metrics report (#1603) 2024-02-21 15:16:43 +01:00
Yury Gargay
8d9e1fed5f Mark new peer meta fields required in OpenAPI spec (#1604) 2024-02-21 15:06:42 +01:00
Bethuel Mmbaga
e1eddd1cab Fix incorrect assignment of SystemSerialNumber and SystemManufacturer (#1600) 2024-02-20 22:50:14 +03:00
Yury Gargay
0fbf72434e Make SQLite default for new installations (#1529)
* Make SQLite default for new installations

* if var is not set, return empty string

this allows getStoreEngineFromDatadir to detect json store files

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2024-02-20 15:06:32 +01:00
pascal-fischer
51f133fdc6 Extend system meta (#1598)
* wip: add posture checks structs

* add netbird version check

* Refactor posture checks and add version checks

* Add posture check activities (#1445)

* Integrate Endpoints for Posture Checks (#1432)

* wip: add posture checks structs

* add netbird version check

* Refactor posture checks and add version checks

* Implement posture and version checks in API models

* Refactor API models and enhance posture check functionality

* wip: add posture checks endpoints

* go mod tidy

* Reference the posture checks by id's in policy

* Add posture checks management to server

* Add posture checks management mocks

* implement posture checks handlers

* Add posture checks to account copy and fix tests

* Refactor posture checks validation

* wip: Add posture checks handler tests

* Add JSON encoding support to posture checks

* Encode posture checks to correct api response object

* Refactored posture checks implementation to align with the new API schema

* Refactor structure of `Checks` from slice to map

* Cleanup

* Add posture check activities (#1445)

* Revert map to use list of checks

* Add posture check activity events

* Refactor posture check initialization in account test

* Improve the handling of version range in posture check

* Fix tests and linter

* Remove max_version from NBVersionCheck

* Added unit tests for NBVersionCheck

* go mod tidy

* Extend policy endpoint with posture checks (#1450)

* Implement posture and version checks in API models

* go mod tidy

* Allow attaching posture checks to policy

* Update error message for linked posture check on deleting

* Refactor PostureCheck and Checks structures

* go mod tidy

* Add validation for non-existing posture checks

* fix unit tests

* use Wt version

* Remove the enabled field, as posture check will now automatically be activated by default when attaching to a policy

* wip: add posture checks structs

* add netbird version check

* Refactor posture checks and add version checks

* Add posture check activities (#1445)

* Integrate Endpoints for Posture Checks (#1432)

* wip: add posture checks structs

* add netbird version check

* Refactor posture checks and add version checks

* Implement posture and version checks in API models

* Refactor API models and enhance posture check functionality

* wip: add posture checks endpoints

* go mod tidy

* Reference the posture checks by id's in policy

* Add posture checks management to server

* Add posture checks management mocks

* implement posture checks handlers

* Add posture checks to account copy and fix tests

* Refactor posture checks validation

* wip: Add posture checks handler tests

* Add JSON encoding support to posture checks

* Encode posture checks to correct api response object

* Refactored posture checks implementation to align with the new API schema

* Refactor structure of `Checks` from slice to map

* Cleanup

* Add posture check activities (#1445)

* Revert map to use list of checks

* Add posture check activity events

* Refactor posture check initialization in account test

* Improve the handling of version range in posture check

* Fix tests and linter

* Remove max_version from NBVersionCheck

* Added unit tests for NBVersionCheck

* go mod tidy

* Extend policy endpoint with posture checks (#1450)

* Implement posture and version checks in API models

* go mod tidy

* Allow attaching posture checks to policy

* Update error message for linked posture check on deleting

* Refactor PostureCheck and Checks structures

* go mod tidy

* Add validation for non-existing posture checks

* fix unit tests

* use Wt version

* Remove the enabled field, as posture check will now automatically be activated by default when attaching to a policy

* Extend network map generation with posture checks (#1466)

* Apply posture checks to network map generation

* run policy posture checks on peers to connect

* Refactor and streamline policy posture check process for peers to connect.

* Add posture checks testing in a network map

* Remove redundant nil check in policy.go

* Refactor peer validation check in policy.go

* Update 'Check' function signature and use logger for version check

* Refactor posture checks run on sources and updated the validation func

* Update peer validation

* fix tests

* improved test coverage for policy posture check

* Refactoring

* Extend NetBird agent to collect kernel version (#1495)

* Add KernelVersion field to LoginRequest

* Add KernelVersion to system info retrieval

* Fix tests

* Remove Core field from system info

* Replace Core field with new OSVersion field in system info

* Added WMI dependency to info_windows.go

* Add OS Version posture checks  (#1479)

* Initial support of Geolocation service (#1491)

* Add Geo Location posture check (#1500)

* wip: implement geolocation check

* add geo location posture checks to posture api

* Merge branch 'feature/posture-checks' into geo-posture-check

* Remove CityGeoNameID and update required fields in API

* Add geoLocation checks to posture checks handler tests

* Implement geo location-based checks for peers

* Update test values and embed location struct in peer system

* add support for country wide checks

* initialize country code regex once

* Fix peer meta core compability with older clients (#1515)

* Refactor extraction of OSVersion in grpcserver

* Ignore lint check

* Fix peer meta core compability with older management (#1532)

* Revert core field deprecation

* fix tests

* Extend peer meta with location information (#1517)

This PR uses the geolocation service to resolve IP to location. 
The lookup happens once on the first connection - when a client calls the Sync func.
The location is stored as part of the peer:

* Add Locations endpoints (#1516)

* add locations endpoints

* Add sqlite3 check and database generation in geolite script

* Add SQLite storage for geolocation data

* Refactor file existence check into a separate function

* Integrate geolocation services into management application

* Refactoring

* Refactor city retrieval to include Geonames ID

* Add signature verification for GeoLite2 database download

* Change to in-memory database for geolocation store

* Merge manager to geolocation

* Update GetAllCountries to return Country name and iso code

* fix tests

* Add reload to SqliteStore

* Add geoname indexes

* move db file check to connectDB

* Add concurrency safety to SQL queries and database reloading

The commit adds mutex locks to the GetAllCountries and GetCitiesByCountry functions to ensure thread-safety during database queries. Additionally, it introduces a mechanism to safely close the old database connection before a new connection is established upon reloading, which improves the reliability of database operations. Lastly, it moves the checking of database file existence to the connectDB function.

* Add sha256 sum check to geolocation store before reload

* Use read lock

* Check SHA256 twice when reload geonames db

---------

Co-authored-by: Yury Gargay <yury.gargay@gmail.com>

* Add tests and validation for empty peer location in GeoLocationCheck (#1546)

* Disallow Geo check creation/update without configured Geo DB (#1548)

* Fix shared access to in memory copy of geonames.db (#1550)

* Trim suffix in when evaluate Min Kernel Version in OS check

* Add Valid Peer Windows Kernel version test

* Add Geolocation handler tests (#1556)

* Implement user admin checks in posture checks

* Add geolocation handler tests

* Mark initGeolocationTestData as helper func

* Add error handling to geolocation database closure

* Add cleanup function to close geolocation resources

* Simplify checks definition serialisation (#1555)

* Regenerate network map on posture check update (#1563)

* change network state and generate map on posture check update

* Refactoring

* Make city name optional (#1575)

* Do not return empty city name

* Validate action param of geo location checks (#1577)

We only support allow and deny

* Switch realip middleware to upstream (#1578)

* Be more silent in download-geolite2.sh script

* Fix geonames db reload (#1580)

* Ensure posture check name uniqueness when create (#1594)

* Enhance the management of posture checks (#1595)

* add a correct min version and kernel for os posture check example

* handle error when geo or location db is nil

* expose all peer location details in api response

* Check for nil geolocation manager only

* Validate posture check before save

* bump open api version

* add peer location fields to toPeerListItemResponse

* Feautre/extend sys meta (#1536)

* Collect network addresses

* Add Linux sys product info

* Fix peer meta comparison

* Collect sys info on mac

* Add windows sys info

* Fix test

* Fix test

* Fix grpc client

* Ignore test

* Fix test

* Collect IPv6 addresses

* Change the IP to IP + net

* fix tests

* Use netip on server side

* Serialize netip to json

* Extend Peer metadata with cloud detection (#1552)

* add cloud detection + test binary

* test windows exe

* Collect IPv6 addresses

* Change the IP to IP + net

* switch to forked cloud detect lib

* new test builds

* new GCE build

* discontinue using library but local copy instead

* fix imports

* remove openstack check

* add hierarchy to cloud check

* merge IBM and SoftLayer

* close resp bodies and use os lib for file reading

* close more resp bodies

* fix error check logic

* parallelize IBM checks

* fix response value

* go mod tidy

* include context + change kubernetes detection

* add context in info functions

* extract platform into separate field

* fix imports

* add missing wmi import

---------

Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com>

---------

Co-authored-by: pascal-fischer <32096965+pascal-fischer@users.noreply.github.com>

* generate proto

* remove test binaries

---------

Co-authored-by: bcmmbaga <bethuelmbaga12@gmail.com>
Co-authored-by: Yury Gargay <yury.gargay@gmail.com>
Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com>
2024-02-20 11:53:11 +01:00
charnesp
d5338c09dc Disable SSH server by default on client side and add the flag --allow-server-ssh to enable it (#1508)
This changes the default behavior for new peers, by requiring the agent to be executed with allow-server-ssh set to true in order for the management configuration to take effect.
2024-02-20 11:13:27 +01:00
Oskar Manhart
8fd4166c53 feat: add --disable-auto-connectflag to prevent auto connection after daemon service start (fixes #444, fixes #1382) (#1161)
With these changes, the command up supports the flag --disable-auto-connect that allows users to disable auto connection on the client after a computer restart or when the daemon restarts.
2024-02-20 10:10:05 +01:00
Yury Gargay
9bc7b9e897 Add initial support of device posture checks (#1540)
This PR implements the following posture checks:

* Agent minimum version allowed
* OS minimum version allowed
* Geo-location based on connection IP

For the geo-based location, we rely on GeoLite2 databases which are free IP geolocation databases. MaxMind was tested and we provide a script that easily allows to download of all necessary files, see infrastructure_files/download-geolite2.sh.

The OpenAPI spec should extensively cover the life cycle of current version posture checks.
2024-02-20 09:59:56 +01:00
Yury Gargay
db3cba5e0f Remove Account.Rules from Store engines (#1528) 2024-02-19 17:17:36 +01:00
Maycon Santos
cb3408a10b Allow adding 3 nameserver addresses (#1588) 2024-02-19 14:29:20 +01:00
Viktor Liu
0afd738509 Make sure the iOS dialer does not get overwritten (#1585)
* Make sure our iOS dialer does not get overwritten

* set dial timeout for both clients on ios

---------

Co-authored-by: Pascal Fischer <pascal@netbird.io>
2024-02-16 14:37:47 +01:00
Maycon Santos
cf87f1e702 Fix/prevent returning error from external cache (#1576)
* Prevent returning error from external cache query

* link comment

* fix spell and remove unnecessary return
2024-02-13 13:10:17 +01:00
Maycon Santos
e890fdae54 Return error when peer is not valid (#1573)
Fix count with invalid peers
2024-02-13 10:59:31 +01:00
Maycon Santos
dd14db6478 Properly handle cache error and return userdata (#1571) 2024-02-12 21:54:16 +01:00
Maycon Santos
88747e3e01 Add an extra server reflexive candidate with WG port (#1549)
sends an extra server reflexive candidate to the remote peer with our related port (usually the Wireguard port)
this is useful when a network has an existing port forwarding rule for the Wireguard port and the local peer and avoids creating a 1:1 NAT on the local network.
2024-02-08 16:50:37 +01:00
Yury Gargay
fb30931365 Expose trusted proxy list and counter configuration for realip middleware (#1535) 2024-02-08 14:40:40 +01:00
Maycon Santos
a7547b9990 Get cache from external cache when refresh fails (#1537)
In some cases, when the refresh cache fails, we should try to get the cache from the external cache obj.

This may happen if the IDP is not responsive between storing metadata and refreshing the cache
2024-02-07 16:14:30 +01:00
Maycon Santos
62bacee8dc Use dashboard v2 for getting started scripts (#1530) 2024-02-05 17:10:08 +01:00
Yury Gargay
71cd2e3e03 Update grpc-middleware to bring changes related to realip (#1526) 2024-02-05 14:18:15 +01:00
Yury Gargay
bdf71ab7ff Remove query parameter from policy endpoints (#1527) 2024-02-05 14:07:11 +01:00
Zoltan Papp
a2f2a6e21a Fix/resolv parser (#1520)
fix an issue In case if the original resolv.conf file is empty, then it can cause a nil pointer
2024-02-02 17:54:33 +01:00
Zoltan Papp
f89332fcd2 Update port, ip choice logic in DNS service (#1514)
Ensure we use WG address instead of loopback addresses for eBPF.
- First try to use 53 port
- Try to use 5053 port on WG interface for eBPF
- Try to use 5053 on WG interface or loopback interface
2024-02-02 17:53:55 +01:00
Zoltan Papp
8604add997 Export info log level setter for Android (#1518) 2024-02-01 16:30:38 +01:00
Yury Gargay
93cab49696 Extract peer real IP from Load Balancer when possible (#1510) 2024-01-31 16:02:24 +01:00
Krzysztof Nazarewski
b6835d9467 getFirstListenerAvailable(): adjust logging levels and add success message (#1513)
it was worrying to see multiple warnings and no success message when lacking CAP_NET_BIND_SERVICE
2024-01-31 11:20:18 +01:00
Viktor Liu
846d486366 Restore dns on unclean shutdown (#1494) 2024-01-30 09:58:56 +01:00
Viktor Liu
9c56f74235 Fix iOS DNS timeout (#1504) 2024-01-29 17:10:47 +01:00
Viktor Liu
25b3641be8 Fix data dir creation permissions (#1503) 2024-01-29 14:21:45 +01:00
Maycon Santos
c41504b571 Update bug-issue-report and feature request templates (#1499)
* Update bug-issue-report.md

* Update feature_request.md
2024-01-26 18:22:02 +01:00
pascal-fischer
399493a954 Allow service users with user role read-only access to all resources (#1484)
We allow service users with user role read-only access 
to all resources so users can create service user and propagate 
PATs without having to give full admin permissions.
2024-01-25 09:50:27 +01:00
Zoltan Papp
4771fed64f Support disabled resolved stub server mode (#1493)
In the case of disabled stub listeren the list of name servers is unordered. The solution is to configure the resolv.conf file directly instead of dbus API.
Because third-party services also can manipulate the DNS settings the agent watch the resolv.conf file and keep it up to date.

- apply file type DNS manager if in the name server list does not exist the 127.0.0.53 address
- watching the resolv.conf file with inotify service and overwrite all the time if the configuration has changed and it invalid
- fix resolv.conf generation algorithm
2024-01-24 16:47:26 +01:00
Viktor Liu
88117f7d16 Validate upstream reachability on first DNS configuration (#1489)
* Test upstream DNS for availability as soon as they are received from management

* Use root zone for DNS tests
2024-01-23 17:23:12 +01:00
Viktor Liu
5ac9f9fe2f Tidy up mod (#1492) 2024-01-22 23:05:02 +01:00
Viktor Liu
a7d6632298 Extend netbird status command to include health information (#1471)
* Adds management, signal, and relay (STUN/TURN) health probes to the status command.

* Adds a reason when the management or signal connections are disconnected.

* Adds last wireguard handshake and received/sent bytes per peer
2024-01-22 12:20:24 +01:00
Maycon Santos
d4194cba6a Fix race condition with JWT group sync (#1486)
This PR fixes the issue that caused JWT group membership not being store
Therefore causing many event logs and inconsistency
2024-01-20 23:50:57 +01:00
pascal-fischer
131d9f1bc7 Add getGroupByName method (#1481)
* add get group by name method to account manager

* remove contains function and add proper description for GetGroupByName method

* add to mock server
2024-01-19 15:41:27 +01:00
pascal-fischer
f099e02b34 Fix preshared key not persisted in config (#1474)
* replace the preshared key attribute in LoginRequest protobuff with an optional replacement

* mark old field as deprecated

* fix ui client to also keep preshared key
2024-01-19 10:30:41 +01:00
Maycon Santos
93646e6a13 Upgrade integrations versions to 8a7c87accb22 (#1482)
* Upgrade integrations versions to 8a7c87accb22

* sync go sum
2024-01-18 18:20:59 +01:00
Zoltan Papp
67a2127fd7 Add iOS build test (#1412)
* merge validate workflows into mobile



---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2024-01-18 18:20:23 +01:00
dependabot[bot]
dd7fcbd083 Bump golang.org/x/crypto from 0.14.0 to 0.17.0 (#1401)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-18 16:02:08 +01:00
xcf13363175
d5f330b9c0 Fix musl build (#1480)
* Update go.mod

* Update go.sum
2024-01-18 15:44:33 +01:00
Maycon Santos
9fa0fbda0d Enable group propagation by default (#1469)
Group updates to user auto groups will propagate by default for new accounts
2024-01-15 19:26:27 +01:00
Zoltan Papp
5a7aa461de Remove debug lines (#1468)
Remove print lines from EBPF code
2024-01-15 18:04:19 +01:00
Maycon Santos
e9c967b27c Add support for setting interface name and wireguard port (#1467)
This PR adds support for setting the
wireguard interface name and port
with the netbird up command
2024-01-15 15:53:23 +01:00
Zoltan Papp
ace588758c Update Java and specify version of cmdline tool (#1456) 2024-01-12 12:31:14 +01:00
Yury Gargay
8bb16e016c Fix typo in iface/tun_usp_linux.go (#1457) 2024-01-12 09:36:06 +01:00
Misha Bragin
6a2a97f088 Fix client SSH server error log (#1455) 2024-01-11 14:36:27 +01:00
Zoltan Papp
3591795a58 Fix allow netbird traffic for nftables and userspace (#1446)
Add default allow rules for input and output chains as part of the allownetbird call for userspace mode
2024-01-11 12:21:58 +01:00
Yury Gargay
5311ce4e4a Soft deprecate Rules API (#1454) 2024-01-10 13:55:11 +01:00
Maycon Santos
c61cb00f40 Add external-ip support for coturn (#1439)
Handles the case when users are running Coturn with peers in the same network, and these peers connect to the relay server via private IP addresses (e.g., Oracle cloud), which causes relay candidates to be allocated using private IP addresses. This causes issues with external peers who can't reach these private addresses.

Use the provided IP address with NETBIRD_TURN_EXTERNAL_IP or discover the address via https://jsonip.com API.

For quick-start guide with Zitadel, we only use the discover method with the external API
2024-01-10 13:03:46 +01:00
pascal-fischer
72a1e97304 add unimplemented as a valid error in SSO check (#1440) 2024-01-10 08:54:05 +01:00
Zoltan Papp
5242851ecc Use cached wintun zip package in github workflows (#1448) 2024-01-09 10:21:53 +01:00
pascal-fischer
cb69348a30 Update contribution and readme file (#1447)
Include the release of rosenpass and the update to go 1.21 in the dev containers and readme
2024-01-08 15:41:22 +01:00
Zoltan Papp
69dbcbd362 Remove duplicated chain add (#1444)
Remove duplicated chain add operation
2024-01-08 13:29:53 +01:00
pascal-fischer
5de4acf2fe Integrate Rosenpass (#1153)
This PR aims to integrate Rosenpass with NetBird. It adds a manager for Rosenpass that starts a Rosenpass server and handles the managed peers. It uses the cunicu/go-rosenpass implementation. Rosenpass will then negotiate a pre-shared key every 2 minutes and apply it to the wireguard connection.

The Feature can be enabled by setting a flag during the netbird up --enable-rosenpass command.

If two peers are both support and have the Rosenpass feature enabled they will create a post-quantum secure connection. If one of the peers or both don't have this feature enabled or are running an older version that does not have this feature yet, the NetBird client will fall back to a plain Wireguard connection without pre-shared keys for those connections (keeping Rosenpass negotiation for the rest).

Additionally, this PR includes an update of all Github Actions workflows to use go version 1.21.0 as this is a requirement for the integration.

---------

Co-authored-by: braginini <bangvalo@gmail.com>
Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2024-01-08 12:25:35 +01:00
Prasad Manigaradi
aa3b79d311 Update setup.env.example (#1433)
This parameter is required as per the documentation, but was missing in the default example.
2024-01-08 10:09:41 +01:00
Maycon Santos
8b4ec96516 Update user's last login when authenticating a peer (#1437)
* Update user's last login when authenticating a peer

Prior to this update the user's last login only updated on dashboard authentication

* use account and user methods
2024-01-06 12:57:05 +01:00
Maycon Santos
1f3a12d941 Cancel peer expiration scheduled job when deleting account (#1434) 2024-01-04 17:10:55 +01:00
Zoltan Papp
1de3bb5420 Netstack (#1403)
Add netstack support for the agent to run it without privileges.

- use interface for tun device
- use common IPC for userspace WireGuard integration
- move udpmux creation and sharedsock to tun layer
2024-01-03 16:06:20 +01:00
Zoltan Papp
163933d429 Fix route change notifier (#1431)
Compare the differences between the new routes
and initial routes
2024-01-03 11:54:19 +01:00
Misha Bragin
875a2e2b63 Add iOS support to README (#1430) 2024-01-02 20:21:06 +01:00
Maycon Santos
fd8bba6aa3 Fix Windows settings popup with mesa 3d openGL emulator (#1428)
By copying the emulator driver next to our binary, our GUI setting popup works on remote desktop connections

the dll is added as part of our sign pipelines workflow
2024-01-02 16:16:20 +01:00
Maycon Santos
86908eee58 Fix Windows name on WMI error (#1426)
Before, netbird would exit and prevent the agent from starting if getting the system name using WMI was an issue.

This change returns a default value in this case
2024-01-01 21:28:42 +01:00
Maycon Santos
c1caec3fcb Update management-integrations/additions (#1425) 2024-01-01 20:17:29 +01:00
Maycon Santos
b28b8fce50 Remove the user from the cache without refreshing it (#1422)
Some IdPs might have eventual consistency for their API calls, and refreshing the cache with its data may return the deleted user as part of the account

Introduce a new account manager method, removeUserFromCache, to remove the user from the local cache without refresh
2024-01-01 19:17:44 +01:00
Maycon Santos
f780f17f85 Use integrated activity store (#1421)
---------

Co-authored-by: braginini <bangvalo@gmail.com>
2024-01-01 19:11:11 +01:00
Maycon Santos
5903715a61 Update cloud management URL to https://api.netbird.io:443 (#1402)
With this change we are updating client configuration files to use the new domain
2023-12-27 20:56:04 +01:00
Bethuel Mmbaga
5469de53c5 Fix quickstart script incompatibility with latest Zitadel version (#1400) 2023-12-27 16:15:06 +01:00
Zoltan Papp
bc3d647d6b Update pion v3 (#1398)
Update Pion related versions to the latest
---------

Co-authored-by: Yury Gargay <yury.gargay@gmail.com>
2023-12-20 23:02:42 +01:00
pascal-fischer
7060b63838 use specific apline image version so the iptables will be installed with version 1.8.9 instead of 1.8.10 (#1405) 2023-12-20 19:41:57 +01:00
Maycon Santos
3168b80ad0 Improve release workflows speed (#1397)
Removing extra cache store with setup-go action 
and adding ~/.cache/go-build to the cached directory list
2023-12-18 12:09:44 +01:00
pascal-fischer
818c6b885f Feature/add iOS support (#1244)
* starting engine by passing file descriptor on engine start

* inject logger that does not compile

* logger and first client

* first working connection

* support for routes and working connection

* small refactor for better code quality in swift

* trying to add DNS

* fix

* updated

* fix route deletion

* trying to bind the DNS resolver dialer to an interface

* use dns.Client.Exchange

* fix metadata send on startup

* switching between client to query upstream

* fix panic on no dns response

* fix after merge changes

* add engine ready listener

* replace engine listener with connection listener

* disable relay connection for iOS until proxy is refactored into bind

* Extract private upstream for iOS and fix function headers for other OS

* Update mock Server

* Fix dns server and upstream tests

* Fix engine null pointer with mobile dependencies for other OS

* Revert back to disabling upstream on no response

* Fix some of the remarks from the linter

* Fix linter

* re-arrange duration calculation

* revert exported HostDNSConfig

* remove unused engine listener

* remove development logs

* refactor dns code and interface name propagation

* clean dns server test

* disable upstream deactivation for iOS

* remove files after merge

* fix dns server darwin

* fix server mock

* fix build flags

* move service listen back to initialize

* add wgInterface to hostManager initialization on android

* fix typo and remove unused function

* extract upstream exchange for ios and rest

* remove todo

* separate upstream logic to ios file

* Fix upstream test

* use interface and embedded struct for upstream

* set properly upstream client

* remove placeholder

* remove ios specific attributes

* fix upstream test

* merge ipc parser and wg configurer for mobile

* fix build annotation

* use json for DNS settings handover through gomobile

* add logs for DNS json string

* bring back check on ios for private upstream

* remove wrong (and unused) line

* fix wrongly updated comments on DNSSetting export

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-12-18 11:46:58 +01:00
Maycon Santos
01f28baec7 Update .gitignore to exclude all generated files (#1394)
updated a typo in the configure.sh file for turnserver.conf backup
2023-12-17 18:49:47 +01:00
Diego Noguês
56896794b3 feat: organizing infrastructure_files folder and adds new envs (#1235)
This PR aims to organize a little the files within `infrastructure_files` folder and adds some new ENV vars to the process.

1. It creates the `artifacts` folder within the `infrastructure_files` folder, the idea behind it is to split templates from artifacts created after running `./configure.sh`. It makes it easier to cp/rsync only `artifacts` content to the final server/destination.

2. Creates `NETBIRD_TURN_DOMAIN` and `TURN_DOMAIN` ENV vars. The idea behind it is to make it possible to split the management/signal server from TURN server. If `NETBIRD_TURN_DOMAIN` is not set, then, `TURN_DOMAIN` will be set as `NETBIRD_DOMAIN`.

3. Creates `*_TAG` ENVs for each component. The idea behind it is to give the users the choice to use `latest` tag as default or tie it to specific versions of each component in the stack.
2023-12-17 17:43:06 +01:00
pascal-fischer
f73a2e2848 Allow removal of preshared keys (#1385)
* update cli commands to respect an empty string and handle different from undefined

* remove test for unintended behaviour

* remove test for unintended behaviour
2023-12-14 11:48:12 +01:00
Maycon Santos
19fa071a93 Support status filter by names (#1387)
Users can filter status based on peers fully qualified names.

e.g., netbird status -d --filter-by-names peer-a,peer-b.netbird.cloud

enable detailed info when using only filter flags
2023-12-14 11:18:43 +01:00
Bethuel Mmbaga
cba3c549e9 Add JWT group-based access control for adding new peers (#1383)
* Added function to check user access by JWT groups in the account management mock server and account manager

* Refactor auth middleware for group-based JWT access control

* Add group-based JWT access control on adding new peer with JWT

* Remove mapping error as the token validation error is already present in grpc error codes

* use GetAccountFromToken to prevent single mode issues

* handle foreground login message

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-12-13 13:18:35 +03:00
pascal-fischer
65247de48d Fix nil pointer handling in get peers from group (#1381)
Fix nil handling in getAllPeersFromGroups to not include nil pointer in the output.
2023-12-12 18:17:00 +01:00
348 changed files with 21500 additions and 5422 deletions

View File

@@ -1,4 +1,4 @@
FROM golang:1.20-bullseye
FROM golang:1.21-bullseye
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends\

View File

@@ -7,7 +7,7 @@
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/go:1": {
"version": "1.20"
"version": "1.21"
}
},
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",

View File

@@ -2,15 +2,17 @@
name: Bug/Issue report
about: Create a report to help us improve
title: ''
labels: ''
labels: ['triage-needed']
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 '....'
@@ -18,13 +20,25 @@ Steps to reproduce the behavior:
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Are you using NetBird Cloud?**
Please specify whether you use NetBird Cloud or self-host NetBird's control plane.
**NetBird version**
`netbird version`
**NetBird status -d output:**
If applicable, add the output of the `netbird status -d` command
If applicable, add the `netbird status -d' command output.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@@ -2,7 +2,7 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
labels: ['feature-request']
assignees: ''
---

View File

@@ -20,7 +20,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: "1.20.x"
go-version: "1.21.x"
- name: Checkout code
uses: actions/checkout@v3
@@ -35,5 +35,8 @@ jobs:
- name: Install modules
run: go mod tidy
- name: check git status
run: git --no-pager diff --exit-code
- name: Test
run: NETBIRD_STORE_ENGINE=${{ matrix.store }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
run: NETBIRD_STORE_ENGINE=${{ matrix.store }} go test -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 ./...

View File

@@ -14,14 +14,14 @@ jobs:
test:
strategy:
matrix:
arch: ['386','amd64']
store: ['jsonfile', 'sqlite']
arch: [ '386','amd64' ]
store: [ 'jsonfile', 'sqlite' ]
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: "1.20.x"
go-version: "1.21.x"
- name: Cache Go modules
@@ -36,13 +36,20 @@ jobs:
uses: actions/checkout@v3
- 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
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
- name: Install 32-bit libpcap
if: matrix.arch == '386'
run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
- name: Install modules
run: go mod tidy
- name: check git status
run: git --no-pager diff --exit-code
- name: Test
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} go test -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 ./...
test_client_on_docker:
runs-on: ubuntu-20.04
@@ -50,7 +57,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: "1.20.x"
go-version: "1.21.x"
- name: Cache Go modules
uses: actions/cache@v3
@@ -64,11 +71,14 @@ jobs:
uses: actions/checkout@v3
- 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
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
- name: Install modules
run: go mod tidy
- name: check git status
run: git --no-pager diff --exit-code
- name: Generate Iface Test bin
run: CGO_ENABLED=0 go test -c -o iface-testing.bin ./iface/
@@ -76,7 +86,7 @@ jobs:
run: CGO_ENABLED=0 go test -c -o sharedsock-testing.bin ./sharedsock
- name: Generate RouteManager Test bin
run: CGO_ENABLED=0 go test -c -o routemanager-testing.bin ./client/internal/routemanager/...
run: CGO_ENABLED=1 go test -c -o routemanager-testing.bin -tags netgo -ldflags '-w -extldflags "-static -ldbus-1 -lpcap"' ./client/internal/routemanager/...
- name: Generate nftables Manager Test bin
run: CGO_ENABLED=0 go test -c -o nftablesmanager-testing.bin ./client/firewall/nftables/...
@@ -103,7 +113,7 @@ jobs:
- name: Run Engine tests in docker with file store
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_ENGINE="jsonfile" --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1
- name: Run Engine tests in docker with sqlite store
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_ENGINE="sqlite" --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1

View File

@@ -23,13 +23,13 @@ jobs:
uses: actions/setup-go@v4
id: go
with:
go-version: "1.20.x"
go-version: "1.21.x"
- name: Download wintun
uses: carlosperate/download-file-action@v2
id: download-wintun
with:
file-url: https://www.wintun.net/builds/wintun-0.14.1.zip
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
file-name: wintun.zip
location: ${{ env.downloadPath }}
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
@@ -49,4 +49,4 @@ jobs:
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -timeout 5m -p 1 ./... > test-out.txt 2>&1"
- name: test output
if: ${{ always() }}
run: Get-Content test-out.txt
run: Get-Content test-out.txt

View File

@@ -1,7 +1,7 @@
name: golangci-lint
on: [pull_request]
permissions:
permissions:
contents: read
pull-requests: read
@@ -36,11 +36,11 @@ jobs:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: "1.20.x"
go-version: "1.21.x"
cache: false
- name: Install dependencies
if: matrix.os == 'ubuntu-latest'
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:

View File

@@ -1,4 +1,4 @@
name: Android build validation
name: Mobile build validation
on:
push:
@@ -11,7 +11,7 @@ concurrency:
cancel-in-progress: true
jobs:
build:
android_build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -19,9 +19,16 @@ jobs:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: "1.20.x"
go-version: "1.21.x"
- name: Setup Android SDK
uses: android-actions/setup-android@v2
uses: android-actions/setup-android@v3
with:
cmdline-tools-version: 8512546
- name: Setup Java
uses: actions/setup-java@v3
with:
java-version: "11"
distribution: "adopt"
- name: NDK Cache
id: ndk-cache
uses: actions/cache@v3
@@ -29,13 +36,30 @@ jobs:
path: /usr/local/lib/android/sdk/ndk
key: ndk-cache-23.1.7779620
- name: Setup NDK
run: /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;23.1.7779620"
run: /usr/local/lib/android/sdk/cmdline-tools/7.0/bin/sdkmanager --install "ndk;23.1.7779620"
- name: install gomobile
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20230531173138-3c911d8e3eda
- name: gomobile init
run: gomobile init
- name: build android nebtird lib
- name: build android netbird lib
run: PATH=$PATH:$(go env GOPATH) gomobile bind -o $GITHUB_WORKSPACE/netbird.aar -javapkg=io.netbird.gomobile -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/io.netbird.client/cache/wireguard -X github.com/netbirdio/netbird/version.version=buildtest" $GITHUB_WORKSPACE/client/android
env:
CGO_ENABLED: 0
ANDROID_NDK_HOME: /usr/local/lib/android/sdk/ndk/23.1.7779620
ANDROID_NDK_HOME: /usr/local/lib/android/sdk/ndk/23.1.7779620
ios_build:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: "1.21.x"
- name: install gomobile
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20230531173138-3c911d8e3eda
- name: gomobile init
run: gomobile init
- name: build iOS netbird lib
run: PATH=$PATH:$(go env GOPATH) gomobile bind -target=ios -bundleid=io.netbird.framework -ldflags="-X github.com/netbirdio/netbird/version.version=buildtest" -o $GITHUB_WORKSPACE/NetBirdSDK.xcframework $GITHUB_WORKSPACE/client/ios/NetBirdSDK
env:
CGO_ENABLED: 0

View File

@@ -20,7 +20,7 @@ on:
- 'client/ui/**'
env:
SIGN_PIPE_VER: "v0.0.10"
SIGN_PIPE_VER: "v0.0.11"
GORELEASER_VER: "v1.14.1"
concurrency:
@@ -44,15 +44,18 @@ jobs:
name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.20"
go-version: "1.21"
cache: false
-
name: Cache Go modules
uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-releaser-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
${{ runner.os }}-go-releaser-
-
name: Install modules
run: go mod tidy
@@ -117,14 +120,17 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.20"
go-version: "1.21"
cache: false
- name: Cache Go modules
uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-ui-go-${{ hashFiles('**/go.sum') }}
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-ui-go-releaser-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-ui-go-
${{ runner.os }}-ui-go-releaser-
- name: Install modules
run: go mod tidy
@@ -169,18 +175,24 @@ jobs:
name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.20"
go-version: "1.21"
cache: false
-
name: Cache Go modules
uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-ui-go-${{ hashFiles('**/go.sum') }}
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-ui-go-releaser-darwin-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-ui-go-
${{ runner.os }}-ui-go-releaser-darwin-
-
name: Install modules
run: go mod tidy
-
name: check git status
run: git --no-pager diff --exit-code
-
name: Run GoReleaser
id: goreleaser

View File

@@ -28,7 +28,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: "1.20.x"
go-version: "1.21.x"
- name: Cache Go modules
uses: actions/cache@v3
@@ -62,7 +62,7 @@ jobs:
CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false
- name: check values
working-directory: infrastructure_files
working-directory: infrastructure_files/artifacts
env:
CI_NETBIRD_DOMAIN: localhost
CI_NETBIRD_AUTH_CLIENT_ID: testing.client.id
@@ -87,8 +87,10 @@ jobs:
CI_NETBIRD_SIGNAL_PORT: 12345
CI_NETBIRD_STORE_CONFIG_ENGINE: "sqlite"
CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false
CI_NETBIRD_TURN_EXTERNAL_IP: "1.2.3.4"
run: |
set -x
grep AUTH_CLIENT_ID docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_ID
grep AUTH_CLIENT_SECRET docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_SECRET
grep AUTH_AUTHORITY docker-compose.yml | grep $CI_NETBIRD_AUTH_AUTHORITY
@@ -107,7 +109,7 @@ jobs:
grep Engine management.json | grep "$CI_NETBIRD_STORE_CONFIG_ENGINE"
grep IdpSignKeyRefreshEnabled management.json | grep "$CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH"
grep UseIDToken management.json | grep false
grep -A 1 IdpManagerConfig management.json | grep ManagerType | grep $CI_NETBIRD_MGMT_IDP
grep -A 1 IdpManagerConfig management.json | grep ManagerType | grep $CI_NETBIRD_MGMT_IDP
grep -A 3 IdpManagerConfig management.json | grep -A 1 ClientConfig | grep Issuer | grep $CI_NETBIRD_AUTH_AUTHORITY
grep -A 4 IdpManagerConfig management.json | grep -A 2 ClientConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT
grep -A 5 IdpManagerConfig management.json | grep -A 3 ClientConfig | grep ClientID | grep $CI_NETBIRD_IDP_MGMT_CLIENT_ID
@@ -120,10 +122,14 @@ jobs:
grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT
grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep -A 3 RedirectURLs | grep "http://localhost:53000"
grep "external-ip" turnserver.conf | grep $CI_NETBIRD_TURN_EXTERNAL_IP
- name: Install modules
run: go mod tidy
- name: check git status
run: git --no-pager diff --exit-code
- name: Build management binary
working-directory: management
run: CGO_ENABLED=1 go build -o netbird-mgmt main.go
@@ -143,7 +149,7 @@ jobs:
docker build -t netbirdio/signal:latest .
- name: run docker compose up
working-directory: infrastructure_files
working-directory: infrastructure_files/artifacts
run: |
docker-compose up -d
sleep 5
@@ -152,9 +158,16 @@ jobs:
- name: test running containers
run: |
count=$(docker compose ps --format json | jq '. | select(.Name | contains("infrastructure_files")) | .State' | grep -c running)
count=$(docker compose ps --format json | jq '. | select(.Name | contains("artifacts")) | .State' | grep -c running)
test $count -eq 4
working-directory: infrastructure_files
working-directory: infrastructure_files/artifacts
- name: test geolocation databases
working-directory: infrastructure_files/artifacts
run: |
sleep 30
docker compose exec management ls -l /var/lib/netbird/ | grep -i GeoLite2-City.mmdb
docker compose exec management ls -l /var/lib/netbird/ | grep -i geonames.db
test-getting-started-script:
runs-on: ubuntu-latest
@@ -175,8 +188,24 @@ jobs:
- name: test management.json file gen
run: test -f management.json
- name: test turnserver.conf file gen
run: test -f turnserver.conf
run: |
set -x
test -f turnserver.conf
grep external-ip turnserver.conf
- name: test zitadel.env file gen
run: test -f zitadel.env
- name: test dashboard.env file gen
run: test -f dashboard.env
run: test -f dashboard.env
test-download-geolite2-script:
runs-on: ubuntu-latest
steps:
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y unzip sqlite3
- name: Checkout code
uses: actions/checkout@v3
- name: test script
run: bash -x infrastructure_files/download-geolite2.sh
- name: test mmdb file exists
run: test -f GeoLite2-City.mmdb
- name: test geonames file exists
run: test -f geonames.db

21
.gitignore vendored
View File

@@ -6,11 +6,20 @@ bin/
.env
conf.json
http-cmds.sh
infrastructure_files/management.json
infrastructure_files/management-*.json
infrastructure_files/docker-compose.yml
infrastructure_files/openid-configuration.json
infrastructure_files/turnserver.conf
setup.env
infrastructure_files/**/Caddyfile
infrastructure_files/**/dashboard.env
infrastructure_files/**/zitadel.env
infrastructure_files/**/management.json
infrastructure_files/**/management-*.json
infrastructure_files/**/docker-compose.yml
infrastructure_files/**/openid-configuration.json
infrastructure_files/**/turnserver.conf
infrastructure_files/**/management.json.bkp.**
infrastructure_files/**/management-*.json.bkp.**
infrastructure_files/**/docker-compose.yml.bkp.**
infrastructure_files/**/openid-configuration.json.bkp.**
infrastructure_files/**/turnserver.conf.bkp.**
management/management
client/client
client/client.exe
@@ -20,4 +29,4 @@ infrastructure_files/setup.env
infrastructure_files/setup-*.env
.vscode
.DS_Store
*.db
GeoLite2-City*

View File

@@ -63,6 +63,14 @@ linters-settings:
enable:
- nilness
revive:
rules:
- name: exported
severity: warning
disabled: false
arguments:
- "checkPrivateReceivers"
- "sayRepetitiveInsteadOfStutters"
tenv:
# The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
# Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
@@ -93,6 +101,7 @@ linters:
- nilerr # finds the code that returns nil even if it checks that the error is not nil
- nilnil # checks that there is no simultaneous return of nil error and an invalid value
- predeclared # predeclared finds code that shadows one of Go's predeclared identifiers
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.
- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
- thelper # thelper detects Go test helpers without t.Helper() call and checks the consistency of test helpers.
- wastedassign # wastedassign finds wasted assignment statements

View File

@@ -54,7 +54,7 @@ nfpms:
contents:
- src: client/ui/netbird.desktop
dst: /usr/share/applications/netbird.desktop
- src: client/ui/netbird-systemtray-default.png
- src: client/ui/netbird-systemtray-connected.png
dst: /usr/share/pixmaps/netbird.png
dependencies:
- netbird
@@ -71,7 +71,7 @@ nfpms:
contents:
- src: client/ui/netbird.desktop
dst: /usr/share/applications/netbird.desktop
- src: client/ui/netbird-systemtray-default.png
- src: client/ui/netbird-systemtray-connected.png
dst: /usr/share/pixmaps/netbird.png
dependencies:
- netbird

View File

@@ -189,6 +189,8 @@ CGO_ENABLED=0 go build .
> Windows clients have a Wireguard driver requirement. You can download the wintun driver from https://www.wintun.net/builds/wintun-0.14.1.zip, after decompressing, you can copy the file `windtun\bin\ARCH\wintun.dll` to the same path as your binary file or to `C:\Windows\System32\wintun.dll`.
> To test the client GUI application on Windows machines with RDP or vituralized environments (e.g. virtualbox or cloud), you need to download and extract the opengl32.dll from https://fdossena.com/?p=mesa/index.frag next to the built application.
To start NetBird the client in the foreground:
```
@@ -272,6 +274,8 @@ go test -exec sudo ./...
```
> On Windows use a powershell with administrator privileges
> Non-GTK environments will need the `libayatana-appindicator3-dev` (debian/ubuntu) package installed
## 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

View File

@@ -1,6 +1,6 @@
<p align="center">
<strong>:hatching_chick: New Release! Self-hosting in under 5 min.</strong>
<a href="https://github.com/netbirdio/netbird#quickstart-with-self-hosted-netbird">
<strong>:hatching_chick: New Release! Device Posture Checks.</strong>
<a href="https://docs.netbird.io/how-to/manage-posture-checks">
Learn more
</a>
</p>
@@ -42,25 +42,23 @@
**Secure.** NetBird enables secure remote access by applying granular access policies, while allowing you to manage them intuitively from a single place. Works universally on any infrastructure.
### Secure peer-to-peer VPN with SSO and MFA in minutes
### Open-Source Network Security in a Single Platform
![image](https://github.com/netbirdio/netbird/assets/700848/c0d7bae4-3301-499a-bb4e-5e4a225bf35f)
https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov
### Key features
| Connectivity | Management | Automation | Platforms |
|-------------------------------------------------------------------|--------------------------------------------------------------------------|----------------------------------------------------------------------------|---------------------------------------|
| <ul><li> - \[x] Kernel WireGuard </ul></li> | <ul><li> - \[x] [Admin Web UI](https://github.com/netbirdio/dashboard) </ul></li> | <ul><li> - \[x] [Public API](https://docs.netbird.io/api) </ul></li> | <ul><li> - \[x] Linux </ul></li> |
| <ul><li> - \[x] Peer-to-peer connections </ul></li> | <ul><li> - \[x] Auto peer discovery and configuration </ul></li> | <ul><li> - \[x] [Setup keys for bulk network provisioning](https://docs.netbird.io/how-to/register-machines-using-setup-keys) </ul></li> | <ul><li> - \[x] Mac </ul></li> |
| <ul><li> - \[x] Peer-to-peer encryption </ul></li> | <ul><li> - \[x] [IdP integrations](https://docs.netbird.io/selfhosted/identity-providers) </ul></li> | <ul><li> - \[x] [Self-hosting quickstart script](https://docs.netbird.io/selfhosted/selfhosted-quickstart) </ul></li> | <ul><li> - \[x] Windows </ul></li> |
| <ul><li> - \[x] Connection relay fallback </ul></li> | <ul><li> - \[x] [SSO & MFA support](https://docs.netbird.io/how-to/installation#running-net-bird-with-sso-login) </ul></li> | <ul><li> - \[x] IdP groups sync with JWT </ul></li> | <ul><li> - \[x] Android </ul></li> |
| <ul><li> - \[x] [Routes to external networks](https://docs.netbird.io/how-to/routing-traffic-to-private-networks) </ul></li> | <ul><li> - \[x] [Access control - groups & rules](https://docs.netbird.io/how-to/manage-network-access) </ul></li> | | <ul><li> - \[ ] iOS </ul></li> |
| <ul><li> - \[x] NAT traversal with BPF </ul></li> | <ul><li> - \[x] [Private DNS](https://docs.netbird.io/how-to/manage-dns-in-your-network) </ul></li> | | <ul><li> - \[x] Docker </ul></li> |
| | <ul><li> - \[x] [Multiuser support](https://docs.netbird.io/how-to/add-users-to-your-network) </ul></li> | | <ul><li> - \[x] OpenWRT </ul></li> |
| | <ul><li> - \[x] [Activity logging](https://docs.netbird.io/how-to/monitor-system-and-network-activity) </ul></li> | | |
| | <ul><li> - \[x] SSH access management </ul></li> | | |
| Connectivity | Management | Security | Automation | Platforms |
|------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
| <ul><li> - \[x] Kernel WireGuard </ul></li> | <ul><li> - \[x] [Admin Web UI](https://github.com/netbirdio/dashboard) </ul></li> | <ul><li> - \[x] [SSO & MFA support](https://docs.netbird.io/how-to/installation#running-net-bird-with-sso-login) </ul></li> | <ul><li> - \[x] [Public API](https://docs.netbird.io/api) </ul></li> | <ul><li> - \[x] Linux </ul></li> |
| <ul><li> - \[x] Peer-to-peer connections </ul></li> | <ul><li> - \[x] Auto peer discovery and configuration </ul></li> | <ul><li> - \[x] [Access control - groups & rules](https://docs.netbird.io/how-to/manage-network-access) </ul></li> | <ul><li> - \[x] [Setup keys for bulk network provisioning](https://docs.netbird.io/how-to/register-machines-using-setup-keys) </ul></li> | <ul><li> - \[x] Mac </ul></li> |
| <ul><li> - \[x] Connection relay fallback </ul></li> | <ul><li> - \[x] [IdP integrations](https://docs.netbird.io/selfhosted/identity-providers) </ul></li> | <ul><li> - \[x] [Activity logging](https://docs.netbird.io/how-to/monitor-system-and-network-activity) </ul></li> | <ul><li> - \[x] [Self-hosting quickstart script](https://docs.netbird.io/selfhosted/selfhosted-quickstart) </ul></li> | <ul><li> - \[x] Windows </ul></li> |
| <ul><li> - \[x] [Routes to external networks](https://docs.netbird.io/how-to/routing-traffic-to-private-networks) </ul></li> | <ul><li> - \[x] [Private DNS](https://docs.netbird.io/how-to/manage-dns-in-your-network) </ul></li> | <ul><li> - \[x] [Device posture checks](https://docs.netbird.io/how-to/manage-posture-checks) </ul></li> | <ul><li> - \[x] IdP groups sync with JWT </ul></li> | <ul><li> - \[x] Android </ul></li> |
| <ul><li> - \[x] NAT traversal with BPF </ul></li> | <ul><li> - \[x] [Multiuser support](https://docs.netbird.io/how-to/add-users-to-your-network) </ul></li> | <ul><li> - \[x] Peer-to-peer encryption </ul></li> | | <ul><li> - \[x] iOS </ul></li> |
| | | <ul><li> - \[x] [Quantum-resistance with Rosenpass](https://netbird.io/knowledge-hub/the-first-quantum-resistant-mesh-vpn) </ul></li> | | <ul><li> - \[x] OpenWRT </ul></li> |
| | | <ui><li> - \[x] [Periodic re-authentication](https://docs.netbird.io/how-to/enforce-periodic-user-authentication)</ul></li> | | <ul><li> - \[x] [Serverless](https://docs.netbird.io/how-to/netbird-on-faas) </ul></li> |
| | | | | <ul><li> - \[x] Docker </ul></li> |
### Quickstart with NetBird Cloud
- Download and install NetBird at [https://app.netbird.io/install](https://app.netbird.io/install)
@@ -109,8 +107,8 @@ export NETBIRD_DOMAIN=netbird.example.com; curl -fsSL https://github.com/netbird
See a complete [architecture overview](https://docs.netbird.io/about-netbird/how-netbird-works#architecture) for details.
### Community projects
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird)
- [NetBird installer script](https://github.com/physk/netbird-installer)
- [NetBird ansible collection by Dominion Solutions](https://galaxy.ansible.com/ui/repo/published/dominion_solutions/netbird/)
**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).

View File

@@ -1,4 +1,4 @@
FROM alpine:3
FROM alpine:3.18.5
RUN apk add --no-cache ca-certificates iptables ip6tables
ENV NB_FOREGROUND_MODE=true
ENTRYPOINT [ "/go/bin/netbird","up"]

View File

@@ -79,6 +79,7 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead
return err
}
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive)
var ctx context.Context
//nolint
@@ -109,6 +110,7 @@ func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener
return err
}
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive)
var ctx context.Context
//nolint
@@ -139,6 +141,11 @@ func (c *Client) SetTraceLogLevel() {
log.SetLevel(log.TraceLevel)
}
// SetInfoLogLevel configure the logger to info level
func (c *Client) SetInfoLogLevel() {
log.SetLevel(log.InfoLevel)
}
// PeersList return with the list of the PeerInfos
func (c *Client) PeersList() *PeerInfoArray {

View File

@@ -51,7 +51,7 @@ var loginCmd = &cobra.Command{
AdminURL: adminURL,
ConfigPath: configPath,
}
if preSharedKey != "" {
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
ic.PreSharedKey = &preSharedKey
}
@@ -60,7 +60,7 @@ var loginCmd = &cobra.Command{
return fmt.Errorf("get config file: %v", err)
}
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
config, _ = internal.UpdateOldManagementURL(ctx, config, configPath)
err = foregroundLogin(ctx, cmd, config, setupKey)
if err != nil {
@@ -82,12 +82,15 @@ var loginCmd = &cobra.Command{
loginRequest := proto.LoginRequest{
SetupKey: setupKey,
PreSharedKey: preSharedKey,
ManagementUrl: managementURL,
IsLinuxDesktopClient: isLinuxRunningDesktop(),
Hostname: hostName,
}
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
loginRequest.OptionalPreSharedKey = &preSharedKey
}
var loginErr error
var loginResp *proto.LoginResponse
@@ -151,13 +154,21 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.C
jwtToken = tokenInfo.GetTokenToUse()
}
var lastError error
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) {
lastError = err
return nil
}
return err
})
if lastError != nil {
return fmt.Errorf("login failed: %v", lastError)
}
if err != nil {
return fmt.Errorf("backoff cycle failed: %v", err)
}

View File

@@ -25,8 +25,16 @@ import (
)
const (
externalIPMapFlag = "external-ip-map"
dnsResolverAddress = "dns-resolver-address"
externalIPMapFlag = "external-ip-map"
dnsResolverAddress = "dns-resolver-address"
enableRosenpassFlag = "enable-rosenpass"
rosenpassPermissiveFlag = "rosenpass-permissive"
preSharedKeyFlag = "preshared-key"
interfaceNameFlag = "interface-name"
wireguardPortFlag = "wireguard-port"
disableAutoConnectFlag = "disable-auto-connect"
serverSSHAllowedFlag = "allow-server-ssh"
extraIFaceBlackListFlag = "extra-iface-blacklist"
)
var (
@@ -49,6 +57,14 @@ var (
preSharedKey string
natExternalIPs []string
customDNSAddress string
rosenpassEnabled bool
rosenpassPermissive bool
serverSSHAllowed bool
interfaceName string
wireguardPort uint16
serviceName string
autoConnectDisabled bool
extraIFaceBlackList []string
rootCmd = &cobra.Command{
Use: "netbird",
Short: "",
@@ -87,14 +103,21 @@ func init() {
if runtime.GOOS == "windows" {
defaultDaemonAddr = "tcp://127.0.0.1:41731"
}
defaultServiceName := "netbird"
if runtime.GOOS == "windows" {
defaultServiceName = "Netbird"
}
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(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
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 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.PersistentFlags().StringVar(&preSharedKey, preSharedKeyFlag, "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.")
rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device")
rootCmd.AddCommand(serviceCmd)
rootCmd.AddCommand(upCmd)
@@ -118,6 +141,10 @@ func init() {
`An empty string "" clears the previous configuration. `+
`E.g. --dns-resolver-address 127.0.0.1:5053 or --dns-resolver-address ""`,
)
upCmd.PersistentFlags().BoolVar(&rosenpassEnabled, enableRosenpassFlag, false, "[Experimental] Enable Rosenpass feature. If enabled, the connection will be post-quantum secured via Rosenpass.")
upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.")
upCmd.PersistentFlags().BoolVar(&serverSSHAllowed, serverSSHAllowedFlag, false, "Allow SSH server on peer. If enabled, the SSH server will be permitted")
upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.")
}
// SetupCloseHandler handles SIGTERM signal and exits with success
@@ -168,7 +195,7 @@ func FlagNameToEnvVar(cmdFlag string, prefix string) string {
return prefix + upper
}
// DialClientGRPCServer returns client connection to the dameno server.
// DialClientGRPCServer returns client connection to the daemon server.
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()

View File

@@ -2,8 +2,6 @@ package cmd
import (
"context"
"runtime"
"github.com/kardianos/service"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -24,12 +22,8 @@ func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
}
func newSVCConfig() *service.Config {
name := "netbird"
if runtime.GOOS == "windows" {
name = "Netbird"
}
return &service.Config{
Name: name,
Name: serviceName,
DisplayName: "Netbird",
Description: "A WireGuard-based mesh network that connects your devices into a single private network.",
Option: make(service.KeyValue),

View File

@@ -11,11 +11,12 @@ import (
"github.com/kardianos/service"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"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 {
@@ -109,7 +110,6 @@ var runCmd = &cobra.Command{
if err != nil {
return err
}
cmd.Printf("Netbird service is running")
return nil
},
}

View File

@@ -77,6 +77,7 @@ var installCmd = &cobra.Command{
cmd.PrintErrln(err)
return err
}
cmd.Println("Netbird service has been installed")
return nil
},
@@ -106,7 +107,7 @@ var uninstallCmd = &cobra.Command{
if err != nil {
return err
}
cmd.Println("Netbird has been uninstalled")
cmd.Println("Netbird service has been uninstalled")
return nil
},
}

View File

@@ -22,14 +22,21 @@ import (
)
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"`
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"`
IceCandidateEndpoint iceCandidateType `json:"iceCandidateEndpoint" yaml:"iceCandidateEndpoint"`
LastWireguardHandshake time.Time `json:"lastWireguardHandshake" yaml:"lastWireguardHandshake"`
TransferReceived int64 `json:"transferReceived" yaml:"transferReceived"`
TransferSent int64 `json:"transferSent" yaml:"transferSent"`
Latency time.Duration `json:"latency" yaml:"latency"`
RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"`
Routes []string `json:"routes" yaml:"routes"`
}
type peersStateOutput struct {
@@ -41,11 +48,25 @@ type peersStateOutput struct {
type signalStateOutput struct {
URL string `json:"url" yaml:"url"`
Connected bool `json:"connected" yaml:"connected"`
Error string `json:"error" yaml:"error"`
}
type managementStateOutput struct {
URL string `json:"url" yaml:"url"`
Connected bool `json:"connected" yaml:"connected"`
Error string `json:"error" yaml:"error"`
}
type relayStateOutputDetail struct {
URI string `json:"uri" yaml:"uri"`
Available bool `json:"available" yaml:"available"`
Error string `json:"error" yaml:"error"`
}
type relayStateOutput struct {
Total int `json:"total" yaml:"total"`
Available int `json:"available" yaml:"available"`
Details []relayStateOutputDetail `json:"details" yaml:"details"`
}
type iceCandidateType struct {
@@ -53,26 +74,40 @@ type iceCandidateType struct {
Remote string `json:"remote" yaml:"remote"`
}
type nsServerGroupStateOutput struct {
Servers []string `json:"servers" yaml:"servers"`
Domains []string `json:"domains" yaml:"domains"`
Enabled bool `json:"enabled" yaml:"enabled"`
Error string `json:"error" yaml:"error"`
}
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"`
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"`
Relays relayStateOutput `json:"relays" yaml:"relays"`
IP string `json:"netbirdIp" yaml:"netbirdIp"`
PubKey string `json:"publicKey" yaml:"publicKey"`
KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"`
FQDN string `json:"fqdn" yaml:"fqdn"`
RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"`
RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"`
Routes []string `json:"routes" yaml:"routes"`
NSServerGroups []nsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"`
}
var (
detailFlag bool
ipv4Flag bool
jsonFlag bool
yamlFlag bool
ipsFilter []string
statusFilter string
ipsFilterMap map[string]struct{}
detailFlag bool
ipv4Flag bool
jsonFlag bool
yamlFlag bool
ipsFilter []string
prefixNamesFilter []string
statusFilter string
ipsFilterMap map[string]struct{}
prefixNamesFilterMap map[string]struct{}
)
var statusCmd = &cobra.Command{
@@ -83,12 +118,14 @@ var statusCmd = &cobra.Command{
func init() {
ipsFilterMap = make(map[string]struct{})
prefixNamesFilterMap = 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().StringSliceVar(&prefixNamesFilter, "filter-by-names", []string{}, "filters the detailed output by a list of one or more peer FQDN or hostnames, e.g., --filter-by-names peer-a,peer-b.netbird.cloud")
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected")
}
@@ -142,7 +179,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
case yamlFlag:
statusOutputString, err = parseToYAML(outputInformationHolder)
default:
statusOutputString = parseGeneralSummary(outputInformationHolder, false)
statusOutputString = parseGeneralSummary(outputInformationHolder, false, false, false)
}
if err != nil {
@@ -172,8 +209,12 @@ func getStatus(ctx context.Context, cmd *cobra.Command) (*proto.StatusResponse,
}
func parseFilters() error {
switch strings.ToLower(statusFilter) {
case "", "disconnected", "connected":
if strings.ToLower(statusFilter) != "" {
enableDetailFlagWhenFilterFlag()
}
default:
return fmt.Errorf("wrong status filter, should be one of connected|disconnected, got: %s", statusFilter)
}
@@ -185,11 +226,26 @@ func parseFilters() error {
return fmt.Errorf("got an invalid IP address in the filter: address %s, error %s", addr, err)
}
ipsFilterMap[addr] = struct{}{}
enableDetailFlagWhenFilterFlag()
}
}
if len(prefixNamesFilter) > 0 {
for _, name := range prefixNamesFilter {
prefixNamesFilterMap[strings.ToLower(name)] = struct{}{}
}
enableDetailFlagWhenFilterFlag()
}
return nil
}
func enableDetailFlagWhenFilterFlag() {
if !detailFlag && !jsonFlag && !yamlFlag {
detailFlag = true
}
}
func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverview {
pbFullStatus := resp.GetFullStatus()
@@ -197,37 +253,89 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv
managementOverview := managementStateOutput{
URL: managementState.GetURL(),
Connected: managementState.GetConnected(),
Error: managementState.Error,
}
signalState := pbFullStatus.GetSignalState()
signalOverview := signalStateOutput{
URL: signalState.GetURL(),
Connected: signalState.GetConnected(),
Error: signalState.Error,
}
relayOverview := mapRelays(pbFullStatus.GetRelays())
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(),
Peers: peersOverview,
CliVersion: version.NetbirdVersion(),
DaemonVersion: resp.GetDaemonVersion(),
ManagementState: managementOverview,
SignalState: signalOverview,
Relays: relayOverview,
IP: pbFullStatus.GetLocalPeerState().GetIP(),
PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(),
KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(),
FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(),
RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(),
RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(),
Routes: pbFullStatus.GetLocalPeerState().GetRoutes(),
NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()),
}
return overview
}
func mapRelays(relays []*proto.RelayState) relayStateOutput {
var relayStateDetail []relayStateOutputDetail
var relaysAvailable int
for _, relay := range relays {
available := relay.GetAvailable()
relayStateDetail = append(relayStateDetail,
relayStateOutputDetail{
URI: relay.URI,
Available: available,
Error: relay.GetError(),
},
)
if available {
relaysAvailable++
}
}
return relayStateOutput{
Total: len(relays),
Available: relaysAvailable,
Details: relayStateDetail,
}
}
func mapNSGroups(servers []*proto.NSGroupState) []nsServerGroupStateOutput {
mappedNSGroups := make([]nsServerGroupStateOutput, 0, len(servers))
for _, pbNsGroupServer := range servers {
mappedNSGroups = append(mappedNSGroups, nsServerGroupStateOutput{
Servers: pbNsGroupServer.GetServers(),
Domains: pbNsGroupServer.GetDomains(),
Enabled: pbNsGroupServer.GetEnabled(),
Error: pbNsGroupServer.GetError(),
})
}
return mappedNSGroups
}
func mapPeers(peers []*proto.PeerState) peersStateOutput {
var peersStateDetail []peerStateDetailOutput
localICE := ""
remoteICE := ""
localICEEndpoint := ""
remoteICEEndpoint := ""
connType := ""
peersConnected := 0
lastHandshake := time.Time{}
transferReceived := int64(0)
transferSent := int64(0)
for _, pbPeerState := range peers {
isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String()
if skipDetailByFilters(pbPeerState, isPeerConnected) {
@@ -238,10 +346,15 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput {
localICE = pbPeerState.GetLocalIceCandidateType()
remoteICE = pbPeerState.GetRemoteIceCandidateType()
localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint()
remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint()
connType = "P2P"
if pbPeerState.Relayed {
connType = "Relayed"
}
lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local()
transferReceived = pbPeerState.GetBytesRx()
transferSent = pbPeerState.GetBytesTx()
}
timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local()
@@ -256,7 +369,17 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput {
Local: localICE,
Remote: remoteICE,
},
FQDN: pbPeerState.GetFqdn(),
IceCandidateEndpoint: iceCandidateType{
Local: localICEEndpoint,
Remote: remoteICEEndpoint,
},
FQDN: pbPeerState.GetFqdn(),
LastWireguardHandshake: lastHandshake,
TransferReceived: transferReceived,
TransferSent: transferSent,
Latency: pbPeerState.GetLatency().AsDuration(),
RosenpassEnabled: pbPeerState.GetRosenpassEnabled(),
Routes: pbPeerState.GetRoutes(),
}
peersStateDetail = append(peersStateDetail, peerState)
@@ -306,22 +429,31 @@ func parseToYAML(overview statusOutputOverview) (string, error) {
return string(yamlBytes), nil
}
func parseGeneralSummary(overview statusOutputOverview, showURL bool) string {
managementConnString := "Disconnected"
func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays bool, showNameServers bool) string {
var managementConnString string
if overview.ManagementState.Connected {
managementConnString = "Connected"
if showURL {
managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL)
}
} else {
managementConnString = "Disconnected"
if overview.ManagementState.Error != "" {
managementConnString = fmt.Sprintf("%s, reason: %s", managementConnString, overview.ManagementState.Error)
}
}
signalConnString := "Disconnected"
var signalConnString string
if overview.SignalState.Connected {
signalConnString = "Connected"
if showURL {
signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL)
}
} else {
signalConnString = "Disconnected"
if overview.SignalState.Error != "" {
signalConnString = fmt.Sprintf("%s, reason: %s", signalConnString, overview.SignalState.Error)
}
}
interfaceTypeString := "Userspace"
@@ -333,6 +465,64 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool) string {
interfaceIP = "N/A"
}
var relaysString string
if showRelays {
for _, relay := range overview.Relays.Details {
available := "Available"
reason := ""
if !relay.Available {
available = "Unavailable"
reason = fmt.Sprintf(", reason: %s", relay.Error)
}
relaysString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason)
}
} else {
relaysString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total)
}
routes := "-"
if len(overview.Routes) > 0 {
sort.Strings(overview.Routes)
routes = strings.Join(overview.Routes, ", ")
}
var dnsServersString string
if showNameServers {
for _, nsServerGroup := range overview.NSServerGroups {
enabled := "Available"
if !nsServerGroup.Enabled {
enabled = "Unavailable"
}
errorString := ""
if nsServerGroup.Error != "" {
errorString = fmt.Sprintf(", reason: %s", nsServerGroup.Error)
errorString = strings.TrimSpace(errorString)
}
domainsString := strings.Join(nsServerGroup.Domains, ", ")
if domainsString == "" {
domainsString = "." // Show "." for the default zone
}
dnsServersString += fmt.Sprintf(
"\n [%s] for [%s] is %s%s",
strings.Join(nsServerGroup.Servers, ", "),
domainsString,
enabled,
errorString,
)
}
} else {
dnsServersString = fmt.Sprintf("%d/%d Available", countEnabled(overview.NSServerGroups), len(overview.NSServerGroups))
}
rosenpassEnabledStatus := "false"
if overview.RosenpassEnabled {
rosenpassEnabledStatus = "true"
if overview.RosenpassPermissive {
rosenpassEnabledStatus = "true (permissive)" //nolint:gosec
}
}
peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total)
summary := fmt.Sprintf(
@@ -340,25 +530,33 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool) string {
"CLI version: %s\n"+
"Management: %s\n"+
"Signal: %s\n"+
"Relays: %s\n"+
"Nameservers: %s\n"+
"FQDN: %s\n"+
"NetBird IP: %s\n"+
"Interface type: %s\n"+
"Quantum resistance: %s\n"+
"Routes: %s\n"+
"Peers count: %s\n",
overview.DaemonVersion,
version.NetbirdVersion(),
managementConnString,
signalConnString,
relaysString,
dnsServersString,
overview.FQDN,
interfaceIP,
interfaceTypeString,
rosenpassEnabledStatus,
routes,
peersCountString,
)
return summary
}
func parseToFullDetailSummary(overview statusOutputOverview) string {
parsedPeersString := parsePeers(overview.Peers)
summary := parseGeneralSummary(overview, true)
parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive)
summary := parseGeneralSummary(overview, true, true, true)
return fmt.Sprintf(
"Peers detail:"+
@@ -369,7 +567,7 @@ func parseToFullDetailSummary(overview statusOutputOverview) string {
)
}
func parsePeers(peers peersStateOutput) string {
func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bool) string {
var (
peersString = ""
)
@@ -386,6 +584,48 @@ func parsePeers(peers peersStateOutput) string {
remoteICE = peerState.IceCandidateType.Remote
}
localICEEndpoint := "-"
if peerState.IceCandidateEndpoint.Local != "" {
localICEEndpoint = peerState.IceCandidateEndpoint.Local
}
remoteICEEndpoint := "-"
if peerState.IceCandidateEndpoint.Remote != "" {
remoteICEEndpoint = peerState.IceCandidateEndpoint.Remote
}
lastStatusUpdate := "-"
if !peerState.LastStatusUpdate.IsZero() {
lastStatusUpdate = peerState.LastStatusUpdate.Format("2006-01-02 15:04:05")
}
lastWireGuardHandshake := "-"
if !peerState.LastWireguardHandshake.IsZero() && peerState.LastWireguardHandshake != time.Unix(0, 0) {
lastWireGuardHandshake = peerState.LastWireguardHandshake.Format("2006-01-02 15:04:05")
}
rosenpassEnabledStatus := "false"
if rosenpassEnabled {
if peerState.RosenpassEnabled {
rosenpassEnabledStatus = "true"
} else {
if rosenpassPermissive {
rosenpassEnabledStatus = "false (remote didn't enable quantum resistance)"
} else {
rosenpassEnabledStatus = "false (connection won't work without a permissive mode)"
}
}
} else {
if peerState.RosenpassEnabled {
rosenpassEnabledStatus = "false (connection might not work without a remote permissive mode)"
}
}
routes := "-"
if len(peerState.Routes) > 0 {
sort.Strings(peerState.Routes)
routes = strings.Join(peerState.Routes, ", ")
}
peerString := fmt.Sprintf(
"\n %s:\n"+
" NetBird IP: %s\n"+
@@ -395,7 +635,13 @@ func parsePeers(peers peersStateOutput) string {
" Connection type: %s\n"+
" Direct: %t\n"+
" ICE candidate (Local/Remote): %s/%s\n"+
" Last connection update: %s\n",
" ICE candidate endpoints (Local/Remote): %s/%s\n"+
" Last connection update: %s\n"+
" Last WireGuard handshake: %s\n"+
" Transfer status (received/sent) %s/%s\n"+
" Quantum resistance: %s\n"+
" Routes: %s\n"+
" Latency: %s\n",
peerState.FQDN,
peerState.IP,
peerState.PubKey,
@@ -404,7 +650,15 @@ func parsePeers(peers peersStateOutput) string {
peerState.Direct,
localICE,
remoteICE,
peerState.LastStatusUpdate.Format("2006-01-02 15:04:05"),
localICEEndpoint,
remoteICEEndpoint,
lastStatusUpdate,
lastWireGuardHandshake,
toIEC(peerState.TransferReceived),
toIEC(peerState.TransferSent),
rosenpassEnabledStatus,
routes,
peerState.Latency.String(),
)
peersString += peerString
@@ -415,6 +669,7 @@ func parsePeers(peers peersStateOutput) string {
func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool {
statusEval := false
ipEval := false
nameEval := false
if statusFilter != "" {
lowerStatusFilter := strings.ToLower(statusFilter)
@@ -431,5 +686,39 @@ func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool {
ipEval = true
}
}
return statusEval || ipEval
if len(prefixNamesFilter) > 0 {
for prefixNameFilter := range prefixNamesFilterMap {
if !strings.HasPrefix(peerState.Fqdn, prefixNameFilter) {
nameEval = true
break
}
}
}
return statusEval || ipEval || nameEval
}
func toIEC(b int64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB",
float64(b)/float64(div), "KMGTPE"[exp])
}
func countEnabled(dnsServers []nsServerGroupStateOutput) int {
count := 0
for _, server := range dnsServers {
if server.Enabled {
count++
}
}
return count
}

View File

@@ -1,10 +1,14 @@
package cmd
import (
"bytes"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/netbirdio/netbird/client/proto"
@@ -25,41 +29,95 @@ var resp = &proto.StatusResponse{
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.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: "",
LocalIceCandidateEndpoint: "",
RemoteIceCandidateEndpoint: "",
LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)),
BytesRx: 200,
BytesTx: 100,
Routes: []string{
"10.1.0.0/24",
},
Latency: durationpb.New(time.Duration(10000000)),
},
{
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",
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",
LocalIceCandidateEndpoint: "10.0.0.1:10001",
RemoteIceCandidateEndpoint: "10.0.10.1:10002",
LastWireguardHandshake: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 3, 0, time.UTC)),
BytesRx: 2000,
BytesTx: 1000,
Latency: durationpb.New(time.Duration(10000000)),
},
},
ManagementState: &proto.ManagementState{
URL: "my-awesome-management.com:443",
Connected: true,
Error: "",
},
SignalState: &proto.SignalState{
URL: "my-awesome-signal.com:443",
Connected: true,
Error: "",
},
Relays: []*proto.RelayState{
{
URI: "stun:my-awesome-stun.com:3478",
Available: true,
Error: "",
},
{
URI: "turns:my-awesome-turn.com:443?transport=tcp",
Available: false,
Error: "context: deadline exceeded",
},
},
LocalPeerState: &proto.LocalPeerState{
IP: "192.168.178.100/16",
PubKey: "Some-Pub-Key",
KernelInterface: true,
Fqdn: "some-localhost.awesome-domain.com",
Routes: []string{
"10.10.0.0/24",
},
},
DnsServers: []*proto.NSGroupState{
{
Servers: []string{
"8.8.8.8:53",
},
Domains: nil,
Enabled: true,
Error: "",
},
{
Servers: []string{
"1.1.1.1:53",
"2.2.2.2:53",
},
Domains: []string{
"example.com",
"example.net",
},
Enabled: false,
Error: "timeout",
},
},
},
DaemonVersion: "0.14.1",
@@ -82,6 +140,17 @@ var overview = statusOutputOverview{
Local: "",
Remote: "",
},
IceCandidateEndpoint: iceCandidateType{
Local: "",
Remote: "",
},
LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC),
TransferReceived: 200,
TransferSent: 100,
Routes: []string{
"10.1.0.0/24",
},
Latency: time.Duration(10000000),
},
{
IP: "192.168.178.102",
@@ -95,6 +164,14 @@ var overview = statusOutputOverview{
Local: "relay",
Remote: "prflx",
},
IceCandidateEndpoint: iceCandidateType{
Local: "10.0.0.1:10001",
Remote: "10.0.10.1:10002",
},
LastWireguardHandshake: time.Date(2002, 2, 2, 2, 2, 3, 0, time.UTC),
TransferReceived: 2000,
TransferSent: 1000,
Latency: time.Duration(10000000),
},
},
},
@@ -103,15 +180,58 @@ var overview = statusOutputOverview{
ManagementState: managementStateOutput{
URL: "my-awesome-management.com:443",
Connected: true,
Error: "",
},
SignalState: signalStateOutput{
URL: "my-awesome-signal.com:443",
Connected: true,
Error: "",
},
Relays: relayStateOutput{
Total: 2,
Available: 1,
Details: []relayStateOutputDetail{
{
URI: "stun:my-awesome-stun.com:3478",
Available: true,
Error: "",
},
{
URI: "turns:my-awesome-turn.com:443?transport=tcp",
Available: false,
Error: "context: deadline exceeded",
},
},
},
IP: "192.168.178.100/16",
PubKey: "Some-Pub-Key",
KernelInterface: true,
FQDN: "some-localhost.awesome-domain.com",
NSServerGroups: []nsServerGroupStateOutput{
{
Servers: []string{
"8.8.8.8:53",
},
Domains: nil,
Enabled: true,
Error: "",
},
{
Servers: []string{
"1.1.1.1:53",
"2.2.2.2:53",
},
Domains: []string{
"example.com",
"example.net",
},
Enabled: false,
Error: "timeout",
},
},
Routes: []string{
"10.10.0.0/24",
},
}
func TestConversionFromFullStatusToOutputOverview(t *testing.T) {
@@ -145,107 +265,223 @@ func TestSortingOfPeers(t *testing.T) {
}
func TestParsingToJSON(t *testing.T) {
json, _ := parseToJSON(overview)
jsonString, _ := 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\"" +
"}"
expectedJSONString := `
{
"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": ""
},
"iceCandidateEndpoint": {
"local": "",
"remote": ""
},
"lastWireguardHandshake": "2001-01-01T01:01:02Z",
"transferReceived": 200,
"transferSent": 100,
"latency": 10000000,
"quantumResistance": false,
"routes": [
"10.1.0.0/24"
]
},
{
"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"
},
"iceCandidateEndpoint": {
"local": "10.0.0.1:10001",
"remote": "10.0.10.1:10002"
},
"lastWireguardHandshake": "2002-02-02T02:02:03Z",
"transferReceived": 2000,
"transferSent": 1000,
"latency": 10000000,
"quantumResistance": false,
"routes": null
}
]
},
"cliVersion": "development",
"daemonVersion": "0.14.1",
"management": {
"url": "my-awesome-management.com:443",
"connected": true,
"error": ""
},
"signal": {
"url": "my-awesome-signal.com:443",
"connected": true,
"error": ""
},
"relays": {
"total": 2,
"available": 1,
"details": [
{
"uri": "stun:my-awesome-stun.com:3478",
"available": true,
"error": ""
},
{
"uri": "turns:my-awesome-turn.com:443?transport=tcp",
"available": false,
"error": "context: deadline exceeded"
}
]
},
"netbirdIp": "192.168.178.100/16",
"publicKey": "Some-Pub-Key",
"usesKernelInterface": true,
"fqdn": "some-localhost.awesome-domain.com",
"quantumResistance": false,
"quantumResistancePermissive": false,
"routes": [
"10.10.0.0/24"
],
"dnsServers": [
{
"servers": [
"8.8.8.8:53"
],
"domains": null,
"enabled": true,
"error": ""
},
{
"servers": [
"1.1.1.1:53",
"2.2.2.2:53"
],
"domains": [
"example.com",
"example.net"
],
"enabled": false,
"error": "timeout"
}
]
}`
// @formatter:on
assert.Equal(t, expectedJSON, json)
var expectedJSON bytes.Buffer
require.NoError(t, json.Compact(&expectedJSON, []byte(expectedJSONString)))
assert.Equal(t, expectedJSON.String(), jsonString)
}
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"
expectedYAML :=
`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: ""
iceCandidateEndpoint:
local: ""
remote: ""
lastWireguardHandshake: 2001-01-01T01:01:02Z
transferReceived: 200
transferSent: 100
latency: 10ms
quantumResistance: false
routes:
- 10.1.0.0/24
- 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
iceCandidateEndpoint:
local: 10.0.0.1:10001
remote: 10.0.10.1:10002
lastWireguardHandshake: 2002-02-02T02:02:03Z
transferReceived: 2000
transferSent: 1000
latency: 10ms
quantumResistance: false
routes: []
cliVersion: development
daemonVersion: 0.14.1
management:
url: my-awesome-management.com:443
connected: true
error: ""
signal:
url: my-awesome-signal.com:443
connected: true
error: ""
relays:
total: 2
available: 1
details:
- uri: stun:my-awesome-stun.com:3478
available: true
error: ""
- uri: turns:my-awesome-turn.com:443?transport=tcp
available: false
error: 'context: deadline exceeded'
netbirdIp: 192.168.178.100/16
publicKey: Some-Pub-Key
usesKernelInterface: true
fqdn: some-localhost.awesome-domain.com
quantumResistance: false
quantumResistancePermissive: false
routes:
- 10.10.0.0/24
dnsServers:
- servers:
- 8.8.8.8:53
domains: []
enabled: true
error: ""
- servers:
- 1.1.1.1:53
- 2.2.2.2:53
domains:
- example.com
- example.net
enabled: false
error: timeout
`
assert.Equal(t, expectedYAML, yaml)
}
@@ -253,50 +489,78 @@ func TestParsingToYAML(t *testing.T) {
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"
expectedDetail :=
`Peers detail:
peer-1.awesome-domain.com:
NetBird IP: 192.168.178.101
Public key: Pubkey1
Status: Connected
-- detail --
Connection type: P2P
Direct: true
ICE candidate (Local/Remote): -/-
ICE candidate endpoints (Local/Remote): -/-
Last connection update: 2001-01-01 01:01:01
Last WireGuard handshake: 2001-01-01 01:01:02
Transfer status (received/sent) 200 B/100 B
Quantum resistance: false
Routes: 10.1.0.0/24
Latency: 10ms
peer-2.awesome-domain.com:
NetBird IP: 192.168.178.102
Public key: Pubkey2
Status: Connected
-- detail --
Connection type: Relayed
Direct: false
ICE candidate (Local/Remote): relay/prflx
ICE candidate endpoints (Local/Remote): 10.0.0.1:10001/10.0.10.1:10002
Last connection update: 2002-02-02 02:02:02
Last WireGuard handshake: 2002-02-02 02:02:03
Transfer status (received/sent) 2.0 KiB/1000 B
Quantum resistance: false
Routes: -
Latency: 10ms
Daemon version: 0.14.1
CLI version: development
Management: Connected to my-awesome-management.com:443
Signal: Connected to my-awesome-signal.com:443
Relays:
[stun:my-awesome-stun.com:3478] is Available
[turns:my-awesome-turn.com:443?transport=tcp] is Unavailable, reason: context: deadline exceeded
Nameservers:
[8.8.8.8:53] for [.] is Available
[1.1.1.1:53, 2.2.2.2:53] for [example.com, example.net] is Unavailable, reason: timeout
FQDN: some-localhost.awesome-domain.com
NetBird IP: 192.168.178.100/16
Interface type: Kernel
Quantum resistance: false
Routes: 10.10.0.0/24
Peers count: 2/2 Connected
`
assert.Equal(t, expectedDetail, detail)
}
func TestParsingToShortVersion(t *testing.T) {
shortVersion := parseGeneralSummary(overview, false)
shortVersion := parseGeneralSummary(overview, false, false, 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"
expectedString :=
`Daemon version: 0.14.1
CLI version: development
Management: Connected
Signal: Connected
Relays: 1/2 Available
Nameservers: 1/2 Available
FQDN: some-localhost.awesome-domain.com
NetBird IP: 192.168.178.100/16
Interface type: Kernel
Quantum resistance: false
Routes: 10.10.0.0/24
Peers count: 2/2 Connected
`
assert.Equal(t, expectedString, shortVersion)
}

View File

@@ -13,6 +13,7 @@ import (
"google.golang.org/grpc"
"github.com/netbirdio/management-integrations/integrations"
clientProto "github.com/netbirdio/netbird/client/proto"
client "github.com/netbirdio/netbird/client/server"
mgmtProto "github.com/netbirdio/netbird/management/proto"
@@ -78,8 +79,8 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste
if err != nil {
return nil, nil
}
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "",
eventStore, false)
iv, _ := integrations.NewIntegratedValidator(eventStore)
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, iv)
if err != nil {
t.Fatal(err)
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net"
"net/netip"
"runtime"
"strings"
log "github.com/sirupsen/logrus"
@@ -16,6 +17,7 @@ import (
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/util"
)
@@ -36,6 +38,9 @@ var (
func init() {
upCmd.PersistentFlags().BoolVarP(&foregroundMode, "foreground-mode", "F", false, "start service in foreground")
upCmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "Wireguard interface name")
upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "Wireguard interface listening port")
upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening")
}
func upFunc(cmd *cobra.Command, args []string) error {
@@ -79,22 +84,60 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
}
ic := internal.ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: configPath,
NATExternalIPs: natExternalIPs,
CustomDNSAddress: customDNSAddressConverted,
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: configPath,
NATExternalIPs: natExternalIPs,
CustomDNSAddress: customDNSAddressConverted,
ExtraIFaceBlackList: extraIFaceBlackList,
}
if preSharedKey != "" {
if cmd.Flag(enableRosenpassFlag).Changed {
ic.RosenpassEnabled = &rosenpassEnabled
}
if cmd.Flag(rosenpassPermissiveFlag).Changed {
ic.RosenpassPermissive = &rosenpassPermissive
}
if cmd.Flag(serverSSHAllowedFlag).Changed {
ic.ServerSSHAllowed = &serverSSHAllowed
}
if cmd.Flag(interfaceNameFlag).Changed {
if err := parseInterfaceName(interfaceName); err != nil {
return err
}
ic.InterfaceName = &interfaceName
}
if cmd.Flag(wireguardPortFlag).Changed {
p := int(wireguardPort)
ic.WireguardPort = &p
}
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
ic.PreSharedKey = &preSharedKey
}
if cmd.Flag(disableAutoConnectFlag).Changed {
ic.DisableAutoConnect = &autoConnectDisabled
if autoConnectDisabled {
cmd.Println("Autoconnect has been disabled. The client won't connect automatically when the service starts.")
}
if !autoConnectDisabled {
cmd.Println("Autoconnect has been enabled. The client will connect automatically when the service starts.")
}
}
config, err := internal.UpdateOrCreateConfig(ic)
if err != nil {
return fmt.Errorf("get config file: %v", err)
}
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
config, _ = internal.UpdateOldManagementURL(ctx, config, configPath)
err = foregroundLogin(ctx, cmd, config, setupKey)
if err != nil {
@@ -108,7 +151,6 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
}
func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
if err != nil {
return err
@@ -142,7 +184,6 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
loginRequest := proto.LoginRequest{
SetupKey: setupKey,
PreSharedKey: preSharedKey,
ManagementUrl: managementURL,
AdminURL: adminURL,
NatExternalIPs: natExternalIPs,
@@ -150,6 +191,39 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
CustomDNSAddress: customDNSAddressConverted,
IsLinuxDesktopClient: isLinuxRunningDesktop(),
Hostname: hostName,
ExtraIFaceBlacklist: extraIFaceBlackList,
}
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
loginRequest.OptionalPreSharedKey = &preSharedKey
}
if cmd.Flag(enableRosenpassFlag).Changed {
loginRequest.RosenpassEnabled = &rosenpassEnabled
}
if cmd.Flag(rosenpassPermissiveFlag).Changed {
loginRequest.RosenpassPermissive = &rosenpassPermissive
}
if cmd.Flag(serverSSHAllowedFlag).Changed {
loginRequest.ServerSSHAllowed = &serverSSHAllowed
}
if cmd.Flag(disableAutoConnectFlag).Changed {
loginRequest.DisableAutoConnect = &autoConnectDisabled
}
if cmd.Flag(interfaceNameFlag).Changed {
if err := parseInterfaceName(interfaceName); err != nil {
return err
}
loginRequest.InterfaceName = &interfaceName
}
if cmd.Flag(wireguardPortFlag).Changed {
wp := int64(wireguardPort)
loginRequest.WireguardPort = &wp
}
var loginErr error
@@ -223,6 +297,18 @@ func validateNATExternalIPs(list []string) error {
return nil
}
func parseInterfaceName(name string) error {
if runtime.GOOS != "darwin" {
return nil
}
if strings.HasPrefix(name, "utun") {
return nil
}
return fmt.Errorf("invalid interface name %s. Please use the prefix utun followed by a number on MacOS. e.g., utun1 or utun199", name)
}
func validateElement(element string) (int, error) {
if isValidIP(element) {
return ipInputType, nil

View File

@@ -58,6 +58,7 @@ type AclManager struct {
type iFaceMapper interface {
Name() string
Address() iface.WGAddress
IsUserspaceBind() bool
}
func newAclManager(table *nftables.Table, wgIface iFaceMapper, routeingFwChainName string) (*AclManager, error) {
@@ -198,6 +199,81 @@ func (m *AclManager) DeleteRule(rule firewall.Rule) error {
return nil
}
// createDefaultAllowRules In case if the USP firewall manager can use the native firewall manager we must to create allow rules for
// input and output chains
func (m *AclManager) createDefaultAllowRules() error {
expIn := []expr.Any{
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseNetworkHeader,
Offset: 12,
Len: 4,
},
// mask
&expr.Bitwise{
SourceRegister: 1,
DestRegister: 1,
Len: 4,
Mask: []byte{0x00, 0x00, 0x00, 0x00},
Xor: zeroXor,
},
// net address
&expr.Cmp{
Register: 1,
Data: []byte{0x00, 0x00, 0x00, 0x00},
},
&expr.Verdict{
Kind: expr.VerdictAccept,
},
}
_ = m.rConn.InsertRule(&nftables.Rule{
Table: m.workTable,
Chain: m.chainInputRules,
Position: 0,
Exprs: expIn,
})
expOut := []expr.Any{
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseNetworkHeader,
Offset: 16,
Len: 4,
},
// mask
&expr.Bitwise{
SourceRegister: 1,
DestRegister: 1,
Len: 4,
Mask: []byte{0x00, 0x00, 0x00, 0x00},
Xor: zeroXor,
},
// net address
&expr.Cmp{
Register: 1,
Data: []byte{0x00, 0x00, 0x00, 0x00},
},
&expr.Verdict{
Kind: expr.VerdictAccept,
},
}
_ = m.rConn.InsertRule(&nftables.Rule{
Table: m.workTable,
Chain: m.chainOutputRules,
Position: 0,
Exprs: expOut,
})
err := m.rConn.Flush()
if err != nil {
log.Debugf("failed to create default allow rules: %s", err)
return err
}
return nil
}
// Flush rule/chain/set operations from the buffer
//
// Method also get all rules after flush and refreshes handle values in the rulesets
@@ -735,7 +811,6 @@ func (m *AclManager) createPreroutingMangle() *nftables.Chain {
Chain: chain,
Exprs: expressions,
})
chain = m.rConn.AddChain(chain)
return chain
}

View File

@@ -106,11 +106,19 @@ func (m *Manager) RemoveRoutingRules(pair firewall.RouterPair) error {
}
// AllowNetbird allows netbird interface traffic
// todo review this method usage
func (m *Manager) AllowNetbird() error {
if !m.wgIface.IsUserspaceBind() {
return nil
}
m.mutex.Lock()
defer m.mutex.Unlock()
err := m.aclManager.createDefaultAllowRules()
if err != nil {
return fmt.Errorf("failed to create default allow rules: %v", err)
}
chains, err := m.rConn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
if err != nil {
return fmt.Errorf("list of chains: %w", err)
@@ -145,6 +153,7 @@ func (m *Manager) AllowNetbird() error {
if err != nil {
return fmt.Errorf("failed to flush allow input netbird rules: %v", err)
}
return nil
}

View File

@@ -37,6 +37,8 @@ func (i *iFaceMock) Address() iface.WGAddress {
panic("AddressFunc is not set")
}
func (i *iFaceMock) IsUserspaceBind() bool { return false }
func TestNftablesManager(t *testing.T) {
mock := &iFaceMock{
NameFunc: func() string {

View File

@@ -193,6 +193,7 @@ Sleep 3000
Delete "$INSTDIR\${UI_APP_EXE}"
Delete "$INSTDIR\${MAIN_APP_EXE}"
Delete "$INSTDIR\wintun.dll"
Delete "$INSTDIR\opengl32.dll"
RmDir /r "$INSTDIR"
SetShellVarContext all

View File

@@ -38,7 +38,7 @@ func TestDefaultManager(t *testing.T) {
defer ctrl.Finish()
ifaceMock := mocks.NewMockIFaceMapper(ctrl)
ifaceMock.EXPECT().IsUserspaceBind().Return(true)
ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
ifaceMock.EXPECT().SetFilter(gomock.Any())
ip, network, err := net.ParseCIDR("172.0.0.1/32")
if err != nil {
@@ -331,7 +331,7 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) {
defer ctrl.Finish()
ifaceMock := mocks.NewMockIFaceMapper(ctrl)
ifaceMock.EXPECT().IsUserspaceBind().Return(true)
ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
ifaceMock.EXPECT().SetFilter(gomock.Any())
ip, network, err := net.ParseCIDR("172.0.0.1/32")
if err != nil {

View File

@@ -26,7 +26,7 @@ type HTTPClient interface {
}
// AuthFlowInfo holds information for the OAuth 2.0 authorization flow
type AuthFlowInfo struct {
type AuthFlowInfo struct { //nolint:revive
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`

View File

@@ -1,6 +1,7 @@
package internal
import (
"context"
"fmt"
"net/url"
"os"
@@ -12,31 +13,43 @@ import (
"github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/iface"
mgm "github.com/netbirdio/netbird/management/client"
"github.com/netbirdio/netbird/util"
)
const (
// ManagementLegacyPort is the port that was used before by the Management gRPC server.
// managementLegacyPortString 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
managementLegacyPortString = "33073"
// DefaultManagementURL points to the NetBird's cloud management endpoint
DefaultManagementURL = "https://api.wiretrustee.com:443"
DefaultManagementURL = "https://api.netbird.io:443"
// oldDefaultManagementURL points to the NetBird's old cloud management endpoint
oldDefaultManagementURL = "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-", "lo"}
var defaultInterfaceBlacklist = []string{
iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts",
"Tailscale", "tailscale", "docker", "veth", "br-", "lo",
}
// ConfigInput carries configuration changes to the client
type ConfigInput struct {
ManagementURL string
AdminURL string
ConfigPath string
PreSharedKey *string
NATExternalIPs []string
CustomDNSAddress []byte
ManagementURL string
AdminURL string
ConfigPath string
PreSharedKey *string
ServerSSHAllowed *bool
NATExternalIPs []string
CustomDNSAddress []byte
RosenpassEnabled *bool
RosenpassPermissive *bool
InterfaceName *string
WireguardPort *int
DisableAutoConnect *bool
ExtraIFaceBlackList []string
}
// Config Configuration type
@@ -50,10 +63,13 @@ type Config struct {
WgPort int
IFaceBlackList []string
DisableIPv6Discovery bool
RosenpassEnabled bool
RosenpassPermissive bool
ServerSSHAllowed *bool
// SSHKey is a private SSH key in a PEM format
SSHKey string
// ExternalIP mappings, if different than the host interface IP
// ExternalIP mappings, if different from 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
@@ -71,6 +87,10 @@ type Config struct {
NATExternalIPs []string
// CustomDNSAddress sets the DNS resolver listening address in format ip:port
CustomDNSAddress string
// DisableAutoConnect determines whether the client should not start with the service
// it's set to false by default due to backwards compatibility
DisableAutoConnect bool
}
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
@@ -80,6 +100,7 @@ func ReadConfig(configPath string) (*Config, error) {
if _, err := util.ReadJson(configPath, config); err != nil {
return nil, err
}
return config, nil
}
@@ -136,15 +157,16 @@ func createNewConfig(input ConfigInput) (*Config, error) {
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),
ServerSSHAllowed: util.False(),
DisableAutoConnect: false,
}
defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
@@ -161,10 +183,32 @@ func createNewConfig(input ConfigInput) (*Config, error) {
config.ManagementURL = URL
}
config.WgPort = iface.DefaultWgPort
if input.WireguardPort != nil {
config.WgPort = *input.WireguardPort
}
config.WgIface = iface.WgInterfaceDefault
if input.InterfaceName != nil {
config.WgIface = *input.InterfaceName
}
if input.PreSharedKey != nil {
config.PreSharedKey = *input.PreSharedKey
}
if input.RosenpassEnabled != nil {
config.RosenpassEnabled = *input.RosenpassEnabled
}
if input.RosenpassPermissive != nil {
config.RosenpassPermissive = *input.RosenpassPermissive
}
if input.ServerSSHAllowed != nil {
config.ServerSSHAllowed = input.ServerSSHAllowed
}
defaultAdminURL, err := parseURL("Admin URL", DefaultAdminURL)
if err != nil {
return nil, err
@@ -179,7 +223,8 @@ func createNewConfig(input ConfigInput) (*Config, error) {
config.AdminURL = newURL
}
config.IFaceBlackList = defaultInterfaceBlacklist
// nolint:gocritic
config.IFaceBlackList = append(defaultInterfaceBlacklist, input.ExtraIFaceBlackList...)
return config, nil
}
@@ -215,12 +260,9 @@ func update(input ConfigInput) (*Config, error) {
}
if input.PreSharedKey != nil && config.PreSharedKey != *input.PreSharedKey {
if *input.PreSharedKey != "" {
log.Infof("new pre-shared key provides, updated to %s (old value %s)",
*input.PreSharedKey, config.PreSharedKey)
config.PreSharedKey = *input.PreSharedKey
refresh = true
}
log.Infof("new pre-shared key provided, replacing old key")
config.PreSharedKey = *input.PreSharedKey
refresh = true
}
if config.SSHKey == "" {
@@ -236,6 +278,17 @@ func update(input ConfigInput) (*Config, error) {
config.WgPort = iface.DefaultWgPort
refresh = true
}
if input.WireguardPort != nil {
config.WgPort = *input.WireguardPort
refresh = true
}
if input.InterfaceName != nil {
config.WgIface = *input.InterfaceName
refresh = true
}
if input.NATExternalIPs != nil && len(config.NATExternalIPs) != len(input.NATExternalIPs) {
config.NATExternalIPs = input.NATExternalIPs
refresh = true
@@ -246,6 +299,38 @@ func update(input ConfigInput) (*Config, error) {
refresh = true
}
if input.RosenpassEnabled != nil {
config.RosenpassEnabled = *input.RosenpassEnabled
refresh = true
}
if input.RosenpassPermissive != nil {
config.RosenpassPermissive = *input.RosenpassPermissive
refresh = true
}
if input.DisableAutoConnect != nil {
config.DisableAutoConnect = *input.DisableAutoConnect
refresh = true
}
if input.ServerSSHAllowed != nil {
config.ServerSSHAllowed = input.ServerSSHAllowed
refresh = true
}
if config.ServerSSHAllowed == nil {
config.ServerSSHAllowed = util.True()
refresh = true
}
if len(input.ExtraIFaceBlackList) > 0 {
for _, iFace := range util.SliceDiff(input.ExtraIFaceBlackList, config.IFaceBlackList) {
config.IFaceBlackList = append(config.IFaceBlackList, iFace)
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 {
@@ -305,3 +390,85 @@ func configFileIsExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
// UpdateOldManagementURL checks whether client can switch to the new Management URL with port 443 and the management domain.
// 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 UpdateOldManagementURL(ctx context.Context, config *Config, configPath string) (*Config, error) {
defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
if err != nil {
return nil, err
}
parsedOldDefaultManagementURL, err := parseURL("Management URL", oldDefaultManagementURL)
if err != nil {
return nil, err
}
if config.ManagementURL.Hostname() != defaultManagementURL.Hostname() &&
config.ManagementURL.Hostname() != parsedOldDefaultManagementURL.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 config.ManagementURL.Port() != managementLegacyPortString &&
config.ManagementURL.Hostname() == defaultManagementURL.Hostname() {
return config, nil
}
newURL, err := parseURL("Management URL", fmt.Sprintf("%s://%s:%d",
config.ManagementURL.Scheme, defaultManagementURL.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
}

View File

@@ -1,13 +1,16 @@
package internal
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
"github.com/netbirdio/netbird/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/util"
)
func TestGetConfig(t *testing.T) {
@@ -15,7 +18,6 @@ func TestGetConfig(t *testing.T) {
config, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
})
if err != nil {
return
}
@@ -60,22 +62,7 @@ func TestGetConfig(t *testing.T) {
assert.Equal(t, config.ManagementURL.String(), managementURL)
assert.Equal(t, config.PreSharedKey, preSharedKey)
// case 4: new empty pre-shared key config -> fetch it
newPreSharedKey := ""
config, err = UpdateOrCreateConfig(ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: path,
PreSharedKey: &newPreSharedKey,
})
if err != nil {
return
}
assert.Equal(t, config.ManagementURL.String(), managementURL)
assert.Equal(t, config.PreSharedKey, preSharedKey)
// case 5: existing config, but new managementURL has been provided -> update config
// case 4: existing config, but new managementURL has been provided -> update config
newManagementURL := "https://test.newManagement.url:33071"
config, err = UpdateOrCreateConfig(ConfigInput{
ManagementURL: newManagementURL,
@@ -98,6 +85,26 @@ func TestGetConfig(t *testing.T) {
assert.Equal(t, readConf.(*Config).ManagementURL.String(), newManagementURL)
}
func TestExtraIFaceBlackList(t *testing.T) {
extraIFaceBlackList := []string{"eth1"}
path := filepath.Join(t.TempDir(), "config.json")
config, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: path,
ExtraIFaceBlackList: extraIFaceBlackList,
})
if err != nil {
return
}
assert.Contains(t, config.IFaceBlackList, "eth1")
readConf, err := util.ReadJson(path, config)
if err != nil {
return
}
assert.Contains(t, readConf.(*Config).IFaceBlackList, "eth1")
}
func TestHiddenPreSharedKey(t *testing.T) {
hidden := "**********"
samplePreSharedKey := "mysecretpresharedkey"
@@ -123,7 +130,6 @@ func TestHiddenPreSharedKey(t *testing.T) {
ConfigPath: cfgFile,
PreSharedKey: tt.preSharedKey,
})
if err != nil {
t.Fatalf("failed to get cfg: %s", err)
}
@@ -134,3 +140,60 @@ func TestHiddenPreSharedKey(t *testing.T) {
})
}
}
func TestUpdateOldManagementURL(t *testing.T) {
tests := []struct {
name string
previousManagementURL string
expectedManagementURL string
fileShouldNotChange bool
}{
{
name: "Update old management URL with legacy port",
previousManagementURL: "https://api.wiretrustee.com:33073",
expectedManagementURL: DefaultManagementURL,
},
{
name: "Update old management URL",
previousManagementURL: oldDefaultManagementURL,
expectedManagementURL: DefaultManagementURL,
},
{
name: "No update needed when management URL is up to date",
previousManagementURL: DefaultManagementURL,
expectedManagementURL: DefaultManagementURL,
fileShouldNotChange: true,
},
{
name: "No update needed when not using cloud management",
previousManagementURL: "https://netbird.example.com:33073",
expectedManagementURL: "https://netbird.example.com:33073",
fileShouldNotChange: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
config, err := UpdateOrCreateConfig(ConfigInput{
ManagementURL: tt.previousManagementURL,
ConfigPath: configPath,
})
require.NoError(t, err, "failed to create testing config")
previousStats, err := os.Stat(configPath)
require.NoError(t, err, "failed to create testing config stats")
resultConfig, err := UpdateOldManagementURL(context.TODO(), config, configPath)
require.NoError(t, err, "got error when updating old management url")
require.Equal(t, tt.expectedManagementURL, resultConfig.ManagementURL.String())
newStats, err := os.Stat(configPath)
require.NoError(t, err, "failed to create testing config stats")
switch tt.fileShouldNotChange {
case true:
require.Equal(t, previousStats.ModTime(), newStats.ModTime(), "file should not change")
case false:
require.NotEqual(t, previousStats.ModTime(), newStats.ModTime(), "file should have changed")
}
})
}
}

View File

@@ -2,6 +2,7 @@ package internal
import (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -22,16 +23,39 @@ import (
mgm "github.com/netbirdio/netbird/management/client"
mgmProto "github.com/netbirdio/netbird/management/proto"
signal "github.com/netbirdio/netbird/signal/client"
"github.com/netbirdio/netbird/util"
"github.com/netbirdio/netbird/version"
)
// RunClient with main logic.
func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status) error {
return runClient(ctx, config, statusRecorder, MobileDependency{})
return runClient(ctx, config, statusRecorder, MobileDependency{}, nil, nil, nil, nil)
}
// RunClientWithProbes runs the client's main logic with probes attached
func RunClientWithProbes(
ctx context.Context,
config *Config,
statusRecorder *peer.Status,
mgmProbe *Probe,
signalProbe *Probe,
relayProbe *Probe,
wgProbe *Probe,
) error {
return runClient(ctx, config, statusRecorder, MobileDependency{}, mgmProbe, signalProbe, relayProbe, wgProbe)
}
// RunClientMobile with main logic on mobile system
func RunClientMobile(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover, networkChangeListener listener.NetworkChangeListener, dnsAddresses []string, dnsReadyListener dns.ReadyListener) error {
func RunClientMobile(
ctx context.Context,
config *Config,
statusRecorder *peer.Status,
tunAdapter iface.TunAdapter,
iFaceDiscover stdnet.ExternalIFaceDiscover,
networkChangeListener listener.NetworkChangeListener,
dnsAddresses []string,
dnsReadyListener dns.ReadyListener,
) error {
// in case of non Android os these variables will be nil
mobileDependency := MobileDependency{
TunAdapter: tunAdapter,
@@ -40,12 +64,43 @@ func RunClientMobile(ctx context.Context, config *Config, statusRecorder *peer.S
HostDNSAddresses: dnsAddresses,
DnsReadyListener: dnsReadyListener,
}
return runClient(ctx, config, statusRecorder, mobileDependency)
return runClient(ctx, config, statusRecorder, mobileDependency, nil, nil, nil, nil)
}
func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status, mobileDependency MobileDependency) error {
func RunClientiOS(
ctx context.Context,
config *Config,
statusRecorder *peer.Status,
fileDescriptor int32,
networkChangeListener listener.NetworkChangeListener,
dnsManager dns.IosDnsManager,
) error {
mobileDependency := MobileDependency{
FileDescriptor: fileDescriptor,
NetworkChangeListener: networkChangeListener,
DnsManager: dnsManager,
}
return runClient(ctx, config, statusRecorder, mobileDependency, nil, nil, nil, nil)
}
func runClient(
ctx context.Context,
config *Config,
statusRecorder *peer.Status,
mobileDependency MobileDependency,
mgmProbe *Probe,
signalProbe *Probe,
relayProbe *Probe,
wgProbe *Probe,
) error {
log.Infof("starting NetBird client version %s", version.NetbirdVersion())
// Check if client was not shut down in a clean way and restore DNS config if required.
// Otherwise, we might not be able to connect to the management server to retrieve new config.
if err := dns.CheckUncleanShutdown(config.WgIface); err != nil {
log.Errorf("checking unclean shutdown error: %s", err)
}
backOff := &backoff.ExponentialBackOff{
InitialInterval: time.Second,
RandomizationFactor: 1,
@@ -94,7 +149,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
engineCtx, cancel := context.WithCancel(ctx)
defer func() {
statusRecorder.MarkManagementDisconnected()
statusRecorder.MarkManagementDisconnected(state.err)
statusRecorder.CleanLocalPeerState()
cancel()
}()
@@ -143,8 +198,10 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
statusRecorder.UpdateSignalAddress(signalURL)
statusRecorder.MarkSignalDisconnected()
defer statusRecorder.MarkSignalDisconnected()
statusRecorder.MarkSignalDisconnected(nil)
defer func() {
statusRecorder.MarkSignalDisconnected(state.err)
}()
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
@@ -172,7 +229,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
return wrapErr(err)
}
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig, mobileDependency, statusRecorder)
engine := NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, engineConfig, mobileDependency, statusRecorder, mgmProbe, signalProbe, relayProbe, wgProbe)
err = engine.Start()
if err != nil {
log.Errorf("error while starting Netbird Connection Engine: %s", err)
@@ -195,7 +252,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
log.Info("stopped NetBird client")
if _, err := state.Status(); err == ErrResetConnection {
if _, err := state.Status(); errors.Is(err, ErrResetConnection) {
return err
}
@@ -226,6 +283,9 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe
SSHKey: []byte(config.SSHKey),
NATExternalIPs: config.NATExternalIPs,
CustomDNSAddress: config.CustomDNSAddress,
RosenpassEnabled: config.RosenpassEnabled,
RosenpassPermissive: config.RosenpassPermissive,
ServerSSHAllowed: util.ReturnBoolWithDefaultTrue(config.ServerSSHAllowed),
}
if config.PreSharedKey != "" {
@@ -274,83 +334,6 @@ func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte)
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)

View File

@@ -4,9 +4,11 @@ package dns
import (
"context"
"fmt"
"time"
"github.com/godbus/dbus/v5"
log "github.com/sirupsen/logrus"
"time"
)
const dbusDefaultFlag = 0
@@ -14,6 +16,7 @@ const dbusDefaultFlag = 0
func isDbusListenerRunning(dest string, path dbus.ObjectPath) bool {
obj, closeConn, err := getDbusObject(dest, path)
if err != nil {
log.Tracef("error getting dbus object: %s", err)
return false
}
defer closeConn()
@@ -21,14 +24,18 @@ func isDbusListenerRunning(dest string, path dbus.ObjectPath) bool {
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
if err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Store(); err != nil {
log.Tracef("error calling dbus: %s", err)
return false
}
return true
}
func getDbusObject(dest string, path dbus.ObjectPath) (dbus.BusObject, func(), error) {
conn, err := dbus.SystemBus()
if err != nil {
return nil, nil, err
return nil, nil, fmt.Errorf("get dbus: %w", err)
}
obj := conn.Object(dest, path)

View File

@@ -3,11 +3,12 @@
package dns
import (
"bufio"
"bytes"
"fmt"
"net/netip"
"os"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
@@ -23,30 +24,40 @@ const (
fileMaxNumberOfSearchDomains = 6
)
const (
dnsFailoverTimeout = 4 * time.Second
dnsFailoverAttempts = 1
)
type fileConfigurator struct {
originalPerms os.FileMode
repair *repair
originalPerms os.FileMode
nbNameserverIP string
}
func newFileConfigurator() (hostManager, error) {
return &fileConfigurator{}, nil
fc := &fileConfigurator{}
fc.repair = newRepair(defaultResolvConfPath, fc.updateConfig)
return fc, nil
}
func (f *fileConfigurator) supportCustomPort() bool {
return false
}
func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error {
func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig) error {
backupFileExist := false
_, err := os.Stat(fileDefaultResolvConfBackupLocation)
if err == nil {
backupFileExist = true
}
if !config.routeAll {
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 Primary nameserver group. Restoring the original file return err: %w", err)
}
}
return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured")
@@ -55,66 +66,150 @@ func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error {
if !backupFileExist {
err = f.backup()
if err != nil {
return fmt.Errorf("unable to backup the resolv.conf file")
return fmt.Errorf("unable to backup the resolv.conf file: %w", err)
}
}
searchDomainList := searchDomains(config)
nbSearchDomains := searchDomains(config)
f.nbNameserverIP = config.ServerIP
originalSearchDomains, nameServers, others, err := originalDNSConfigs(fileDefaultResolvConfBackupLocation)
resolvConf, err := parseBackupResolvConf()
if err != nil {
log.Error(err)
log.Errorf("could not read original search domains from %s: %s", fileDefaultResolvConfBackupLocation, err)
}
searchDomainList = mergeSearchDomains(searchDomainList, originalSearchDomains)
f.repair.stopWatchFileChanges()
err = f.updateConfig(nbSearchDomains, f.nbNameserverIP, resolvConf)
if err != nil {
return err
}
f.repair.watchFileChanges(nbSearchDomains, f.nbNameserverIP)
return nil
}
func (f *fileConfigurator) updateConfig(nbSearchDomains []string, nbNameserverIP string, cfg *resolvConf) error {
searchDomainList := mergeSearchDomains(nbSearchDomains, cfg.searchDomains)
nameServers := generateNsList(nbNameserverIP, cfg)
options := prepareOptionsWithTimeout(cfg.others, int(dnsFailoverTimeout.Seconds()), dnsFailoverAttempts)
buf := prepareResolvConfContent(
searchDomainList,
append([]string{config.serverIP}, nameServers...),
others)
nameServers,
options)
log.Debugf("creating managed file %s", defaultResolvConfPath)
err = os.WriteFile(defaultResolvConfPath, buf.Bytes(), f.originalPerms)
err := os.WriteFile(defaultResolvConfPath, buf.Bytes(), f.originalPerms)
if err != nil {
restoreErr := f.restore()
if restoreErr != nil {
log.Errorf("attempt to restore default file failed with error: %s", err)
}
return fmt.Errorf("got an creating resolver file %s. Error: %s", defaultResolvConfPath, err)
return fmt.Errorf("creating resolver file %s. Error: %w", defaultResolvConfPath, err)
}
log.Infof("created a NetBird managed %s file with the DNS settings. Added %d search domains. Search list: %s", defaultResolvConfPath, len(searchDomainList), searchDomainList)
// create another backup for unclean shutdown detection right after overwriting the original resolv.conf
if err := createUncleanShutdownIndicator(fileDefaultResolvConfBackupLocation, fileManager, nbNameserverIP); err != nil {
log.Errorf("failed to create unclean shutdown resolv.conf backup: %s", err)
}
log.Infof("created a NetBird managed %s file with your DNS settings. Added %d search domains. Search list: %s", defaultResolvConfPath, len(searchDomainList), searchDomainList)
return nil
}
func (f *fileConfigurator) restoreHostDNS() error {
f.repair.stopWatchFileChanges()
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)
return fmt.Errorf("checking stats for %s file. Error: %w", 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 fmt.Errorf("backing up %s: %w", defaultResolvConfPath, err)
}
return nil
}
func (f *fileConfigurator) restore() error {
err := copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath)
err := removeFirstNbNameserver(fileDefaultResolvConfBackupLocation, f.nbNameserverIP)
if err != nil {
return fmt.Errorf("got error while restoring the %s file from %s. Error: %s", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err)
log.Errorf("Failed to remove netbird nameserver from %s on backup restore: %s", fileDefaultResolvConfBackupLocation, err)
}
err = copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath)
if err != nil {
return fmt.Errorf("restoring %s from %s: %w", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err)
}
if err := removeUncleanShutdownIndicator(); err != nil {
log.Errorf("failed to remove unclean shutdown resolv.conf backup: %s", err)
}
return os.RemoveAll(fileDefaultResolvConfBackupLocation)
}
func (f *fileConfigurator) restoreUncleanShutdownDNS(storedDNSAddress *netip.Addr) error {
resolvConf, err := parseDefaultResolvConf()
if err != nil {
return fmt.Errorf("parse current resolv.conf: %w", err)
}
// no current nameservers set -> restore
if len(resolvConf.nameServers) == 0 {
return restoreResolvConfFile()
}
currentDNSAddress, err := netip.ParseAddr(resolvConf.nameServers[0])
// not a valid first nameserver -> restore
if err != nil {
log.Errorf("restoring unclean shutdown: parse dns address %s failed: %s", resolvConf.nameServers[0], err)
return restoreResolvConfFile()
}
// current address is still netbird's non-available dns address -> restore
// comparing parsed addresses only, to remove ambiguity
if currentDNSAddress.String() == storedDNSAddress.String() {
return restoreResolvConfFile()
}
log.Info("restoring unclean shutdown: first current nameserver differs from saved nameserver pre-netbird: not restoring")
return nil
}
func restoreResolvConfFile() error {
log.Debugf("restoring unclean shutdown: restoring %s from %s", defaultResolvConfPath, fileUncleanShutdownResolvConfLocation)
if err := copyFile(fileUncleanShutdownResolvConfLocation, defaultResolvConfPath); err != nil {
return fmt.Errorf("restoring %s from %s: %w", defaultResolvConfPath, fileUncleanShutdownResolvConfLocation, err)
}
if err := removeUncleanShutdownIndicator(); err != nil {
log.Errorf("failed to remove unclean shutdown resolv.conf file: %s", err)
}
return nil
}
// generateNsList generates a list of nameservers from the config and adds the primary nameserver to the beginning of the list
func generateNsList(nbNameserverIP string, cfg *resolvConf) []string {
ns := make([]string, 1, len(cfg.nameServers)+1)
ns[0] = nbNameserverIP
for _, cfgNs := range cfg.nameServers {
if nbNameserverIP != cfgNs {
ns = append(ns, cfgNs)
}
}
return ns
}
func prepareResolvConfContent(searchDomains, nameServers, others []string) bytes.Buffer {
var buf bytes.Buffer
buf.WriteString(fileGeneratedResolvConfContentHeaderNextLine)
@@ -138,83 +233,19 @@ func prepareResolvConfContent(searchDomains, nameServers, others []string) bytes
return buf
}
func searchDomains(config hostDNSConfig) []string {
func searchDomains(config HostDNSConfig) []string {
listOfDomains := make([]string, 0)
for _, dConf := range config.domains {
if dConf.matchOnly || dConf.disabled {
for _, dConf := range config.Domains {
if dConf.MatchOnly || dConf.Disabled {
continue
}
listOfDomains = append(listOfDomains, dConf.domain)
listOfDomains = append(listOfDomains, dConf.Domain)
}
return listOfDomains
}
func originalDNSConfigs(resolvconfFile string) (searchDomains, nameServers, others []string, err error) {
file, err := os.Open(resolvconfFile)
if err != nil {
err = fmt.Errorf(`could not read existing resolv.conf`)
return
}
defer file.Close()
reader := bufio.NewReader(file)
for {
lineBytes, isPrefix, readErr := reader.ReadLine()
if readErr != nil {
break
}
if isPrefix {
err = fmt.Errorf(`resolv.conf line too long`)
return
}
line := strings.TrimSpace(string(lineBytes))
if strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "domain") {
continue
}
if strings.HasPrefix(line, "options") && strings.Contains(line, "rotate") {
line = strings.ReplaceAll(line, "rotate", "")
splitLines := strings.Fields(line)
if len(splitLines) == 1 {
continue
}
line = strings.Join(splitLines, " ")
}
if strings.HasPrefix(line, "search") {
splitLines := strings.Fields(line)
if len(splitLines) < 2 {
continue
}
searchDomains = splitLines[1:]
continue
}
if strings.HasPrefix(line, "nameserver") {
splitLines := strings.Fields(line)
if len(splitLines) != 2 {
continue
}
nameServers = append(nameServers, splitLines[1])
continue
}
others = append(others, line)
}
return
}
// merge search domains lists and cut off the list if it is too long
// merge search Domains lists and cut off the list if it is too long
func mergeSearchDomains(searchDomains []string, originalSearchDomains []string) []string {
lineSize := len("search")
searchDomainsList := make([]string, 0, len(searchDomains)+len(originalSearchDomains))
@@ -225,14 +256,27 @@ func mergeSearchDomains(searchDomains []string, originalSearchDomains []string)
return searchDomainsList
}
// validateAndFillSearchDomains checks if the search domains list is not too long and if the line is not too long
// validateAndFillSearchDomains checks if the search Domains list is not too long and if the line is not too long
// extend s slice with vs elements
// return with the number of characters in the searchDomains line
func validateAndFillSearchDomains(initialLineChars int, s *[]string, vs []string) int {
for _, sd := range vs {
duplicated := false
for _, fs := range *s {
if fs == sd {
duplicated = true
break
}
}
if duplicated {
continue
}
tmpCharsNumber := initialLineChars + 1 + len(sd)
if tmpCharsNumber > fileMaxLineCharsLimit {
// lets log all skipped domains
// lets log all skipped Domains
log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, sd)
continue
}
@@ -240,29 +284,45 @@ func validateAndFillSearchDomains(initialLineChars int, s *[]string, vs []string
initialLineChars = tmpCharsNumber
if len(*s) >= fileMaxNumberOfSearchDomains {
// lets log all skipped domains
// lets log all skipped Domains
log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, sd)
continue
}
*s = append(*s, sd)
}
return initialLineChars
}
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)
return fmt.Errorf("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)
return fmt.Errorf("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 fmt.Errorf("writing the destination file %s for copy. Error: %s", dest, err)
}
return nil
}
func isContains(subList []string, list []string) bool {
for _, sl := range subList {
var found bool
for _, l := range list {
if sl == l {
found = true
}
}
if !found {
return false
}
}
return true
}

View File

@@ -1,3 +1,5 @@
//go:build !android
package dns
import (
@@ -7,7 +9,7 @@ import (
func Test_mergeSearchDomains(t *testing.T) {
searchDomains := []string{"a", "b"}
originDomains := []string{"a", "b"}
originDomains := []string{"c", "d"}
mergedDomains := mergeSearchDomains(searchDomains, originDomains)
if len(mergedDomains) != 4 {
t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 4)
@@ -49,6 +51,67 @@ func Test_mergeSearchTooLongDomain(t *testing.T) {
}
}
func Test_isContains(t *testing.T) {
type args struct {
subList []string
list []string
}
tests := []struct {
args args
want bool
}{
{
args: args{
subList: []string{"a", "b", "c"},
list: []string{"a", "b", "c"},
},
want: true,
},
{
args: args{
subList: []string{"a"},
list: []string{"a", "b", "c"},
},
want: true,
},
{
args: args{
subList: []string{"d"},
list: []string{"a", "b", "c"},
},
want: false,
},
{
args: args{
subList: []string{"a"},
list: []string{},
},
want: false,
},
{
args: args{
subList: []string{},
list: []string{"b"},
},
want: true,
},
{
args: args{
subList: []string{},
list: []string{},
},
want: true,
},
}
for _, tt := range tests {
t.Run("list check test", func(t *testing.T) {
if got := isContains(tt.args.subList, tt.args.list); got != tt.want {
t.Errorf("isContains() = %v, want %v", got, tt.want)
}
})
}
}
func getLongLine() string {
x := "search "
for {

View File

@@ -0,0 +1,168 @@
//go:build !android
package dns
import (
"fmt"
"os"
"regexp"
"strings"
log "github.com/sirupsen/logrus"
)
const (
defaultResolvConfPath = "/etc/resolv.conf"
)
var timeoutRegex = regexp.MustCompile(`timeout:\d+`)
var attemptsRegex = regexp.MustCompile(`attempts:\d+`)
type resolvConf struct {
nameServers []string
searchDomains []string
others []string
}
func (r *resolvConf) String() string {
return fmt.Sprintf("search domains: %v, name servers: %v, others: %s", r.searchDomains, r.nameServers, r.others)
}
func parseDefaultResolvConf() (*resolvConf, error) {
return parseResolvConfFile(defaultResolvConfPath)
}
func parseBackupResolvConf() (*resolvConf, error) {
return parseResolvConfFile(fileDefaultResolvConfBackupLocation)
}
func parseResolvConfFile(resolvConfFile string) (*resolvConf, error) {
rconf := &resolvConf{
searchDomains: make([]string, 0),
nameServers: make([]string, 0),
others: make([]string, 0),
}
file, err := os.Open(resolvConfFile)
if err != nil {
return rconf, fmt.Errorf("failed to open %s file: %w", resolvConfFile, err)
}
defer func() {
if err := file.Close(); err != nil {
log.Errorf("failed closing %s: %s", resolvConfFile, err)
}
}()
cur, err := os.ReadFile(resolvConfFile)
if err != nil {
return rconf, fmt.Errorf("failed to read %s file: %w", resolvConfFile, err)
}
if len(cur) == 0 {
return rconf, fmt.Errorf("file is empty")
}
for _, line := range strings.Split(string(cur), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "domain") {
continue
}
if strings.HasPrefix(line, "options") && strings.Contains(line, "rotate") {
line = strings.ReplaceAll(line, "rotate", "")
splitLines := strings.Fields(line)
if len(splitLines) == 1 {
continue
}
line = strings.Join(splitLines, " ")
}
if strings.HasPrefix(line, "search") {
splitLines := strings.Fields(line)
if len(splitLines) < 2 {
continue
}
rconf.searchDomains = splitLines[1:]
continue
}
if strings.HasPrefix(line, "nameserver") {
splitLines := strings.Fields(line)
if len(splitLines) != 2 {
continue
}
rconf.nameServers = append(rconf.nameServers, splitLines[1])
continue
}
if line != "" {
rconf.others = append(rconf.others, line)
}
}
return rconf, nil
}
// prepareOptionsWithTimeout appends timeout to existing options if it doesn't exist,
// otherwise it adds a new option with timeout and attempts.
func prepareOptionsWithTimeout(input []string, timeout int, attempts int) []string {
configs := make([]string, len(input))
copy(configs, input)
for i, config := range configs {
if strings.HasPrefix(config, "options") {
config = strings.ReplaceAll(config, "rotate", "")
config = strings.Join(strings.Fields(config), " ")
if strings.Contains(config, "timeout:") {
config = timeoutRegex.ReplaceAllString(config, fmt.Sprintf("timeout:%d", timeout))
} else {
config = strings.Replace(config, "options ", fmt.Sprintf("options timeout:%d ", timeout), 1)
}
if strings.Contains(config, "attempts:") {
config = attemptsRegex.ReplaceAllString(config, fmt.Sprintf("attempts:%d", attempts))
} else {
config = strings.Replace(config, "options ", fmt.Sprintf("options attempts:%d ", attempts), 1)
}
configs[i] = config
return configs
}
}
return append(configs, fmt.Sprintf("options timeout:%d attempts:%d", timeout, attempts))
}
// removeFirstNbNameserver removes the given nameserver from the given file if it is in the first position
// and writes the file back to the original location
func removeFirstNbNameserver(filename, nameserverIP string) error {
resolvConf, err := parseResolvConfFile(filename)
if err != nil {
return fmt.Errorf("parse backup resolv.conf: %w", err)
}
content, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("read %s: %w", filename, err)
}
if len(resolvConf.nameServers) > 1 && resolvConf.nameServers[0] == nameserverIP {
newContent := strings.Replace(string(content), fmt.Sprintf("nameserver %s\n", nameserverIP), "", 1)
stat, err := os.Stat(filename)
if err != nil {
return fmt.Errorf("stat %s: %w", filename, err)
}
if err := os.WriteFile(filename, []byte(newContent), stat.Mode()); err != nil {
return fmt.Errorf("write %s: %w", filename, err)
}
}
return nil
}

View File

@@ -0,0 +1,304 @@
//go:build !android
package dns
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_parseResolvConf(t *testing.T) {
testCases := []struct {
input string
expectedSearch []string
expectedNS []string
expectedOther []string
}{
{
input: `domain example.org
search example.org
nameserver 192.168.0.1
`,
expectedSearch: []string{"example.org"},
expectedNS: []string{"192.168.0.1"},
expectedOther: []string{},
},
{
input: `# This is /run/systemd/resolve/resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
#
# This file might be symlinked as /etc/resolv.conf. If you're looking at
# /etc/resolv.conf and seeing this text, you have followed the symlink.
#
# This is a dynamic resolv.conf file for connecting local clients directly to
# all known uplink DNS servers. This file lists all configured search domains.
#
# Third party programs should typically not access this file directly, but only
# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a
# different way, replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.
nameserver 192.168.2.1
nameserver 100.81.99.197
search netbird.cloud
`,
expectedSearch: []string{"netbird.cloud"},
expectedNS: []string{"192.168.2.1", "100.81.99.197"},
expectedOther: []string{},
},
{
input: `# This is /run/systemd/resolve/resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
#
# This file might be symlinked as /etc/resolv.conf. If you're looking at
# /etc/resolv.conf and seeing this text, you have followed the symlink.
#
# This is a dynamic resolv.conf file for connecting local clients directly to
# all known uplink DNS servers. This file lists all configured search domains.
#
# Third party programs should typically not access this file directly, but only
# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a
# different way, replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.
nameserver 192.168.2.1
nameserver 100.81.99.197
search netbird.cloud
options debug
`,
expectedSearch: []string{"netbird.cloud"},
expectedNS: []string{"192.168.2.1", "100.81.99.197"},
expectedOther: []string{"options debug"},
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run("test", func(t *testing.T) {
t.Parallel()
tmpResolvConf := filepath.Join(t.TempDir(), "resolv.conf")
err := os.WriteFile(tmpResolvConf, []byte(testCase.input), 0644)
if err != nil {
t.Fatal(err)
}
cfg, err := parseResolvConfFile(tmpResolvConf)
if err != nil {
t.Fatal(err)
}
ok := compareLists(cfg.searchDomains, testCase.expectedSearch)
if !ok {
t.Errorf("invalid parse result for search domains, expected: %v, got: %v", testCase.expectedSearch, cfg.searchDomains)
}
ok = compareLists(cfg.nameServers, testCase.expectedNS)
if !ok {
t.Errorf("invalid parse result for ns domains, expected: %v, got: %v", testCase.expectedNS, cfg.nameServers)
}
ok = compareLists(cfg.others, testCase.expectedOther)
if !ok {
t.Errorf("invalid parse result for others, expected: %v, got: %v", testCase.expectedOther, cfg.others)
}
})
}
}
func compareLists(search []string, search2 []string) bool {
if len(search) != len(search2) {
return false
}
for i, v := range search {
if v != search2[i] {
return false
}
}
return true
}
func Test_emptyFile(t *testing.T) {
cfg, err := parseResolvConfFile("/tmp/nothing")
if err == nil {
t.Errorf("expected error, got nil")
}
if len(cfg.others) != 0 || len(cfg.searchDomains) != 0 || len(cfg.nameServers) != 0 {
t.Errorf("expected empty config, got %v", cfg)
}
}
func Test_symlink(t *testing.T) {
input := `# This is /run/systemd/resolve/resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
#
# This file might be symlinked as /etc/resolv.conf. If you're looking at
# /etc/resolv.conf and seeing this text, you have followed the symlink.
#
# This is a dynamic resolv.conf file for connecting local clients directly to
# all known uplink DNS servers. This file lists all configured search domains.
#
# Third party programs should typically not access this file directly, but only
# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a
# different way, replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.
nameserver 192.168.0.1
`
tmpResolvConf := filepath.Join(t.TempDir(), "resolv.conf")
err := os.WriteFile(tmpResolvConf, []byte(input), 0644)
if err != nil {
t.Fatal(err)
}
tmpLink := filepath.Join(t.TempDir(), "symlink")
err = os.Symlink(tmpResolvConf, tmpLink)
if err != nil {
t.Fatal(err)
}
cfg, err := parseResolvConfFile(tmpLink)
if err != nil {
t.Fatal(err)
}
if len(cfg.nameServers) != 1 {
t.Errorf("unexpected resolv.conf content: %v", cfg)
}
}
func TestPrepareOptionsWithTimeout(t *testing.T) {
tests := []struct {
name string
others []string
timeout int
attempts int
expected []string
}{
{
name: "Append new options with timeout and attempts",
others: []string{"some config"},
timeout: 2,
attempts: 2,
expected: []string{"some config", "options timeout:2 attempts:2"},
},
{
name: "Modify existing options to exclude rotate and include timeout and attempts",
others: []string{"some config", "options rotate someother"},
timeout: 3,
attempts: 2,
expected: []string{"some config", "options attempts:2 timeout:3 someother"},
},
{
name: "Existing options with timeout and attempts are updated",
others: []string{"some config", "options timeout:4 attempts:3"},
timeout: 5,
attempts: 4,
expected: []string{"some config", "options timeout:5 attempts:4"},
},
{
name: "Modify existing options, add missing attempts before timeout",
others: []string{"some config", "options timeout:4"},
timeout: 4,
attempts: 3,
expected: []string{"some config", "options attempts:3 timeout:4"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := prepareOptionsWithTimeout(tc.others, tc.timeout, tc.attempts)
assert.Equal(t, tc.expected, result)
})
}
}
func TestRemoveFirstNbNameserver(t *testing.T) {
testCases := []struct {
name string
content string
ipToRemove string
expected string
}{
{
name: "Unrelated nameservers with comments and options",
content: `# This is a comment
options rotate
nameserver 1.1.1.1
# Another comment
nameserver 8.8.4.4
search example.com`,
ipToRemove: "9.9.9.9",
expected: `# This is a comment
options rotate
nameserver 1.1.1.1
# Another comment
nameserver 8.8.4.4
search example.com`,
},
{
name: "First nameserver matches",
content: `search example.com
nameserver 9.9.9.9
# oof, a comment
nameserver 8.8.4.4
options attempts:5`,
ipToRemove: "9.9.9.9",
expected: `search example.com
# oof, a comment
nameserver 8.8.4.4
options attempts:5`,
},
{
name: "Target IP not the first nameserver",
// nolint:dupword
content: `# Comment about the first nameserver
nameserver 8.8.4.4
# Comment before our target
nameserver 9.9.9.9
options timeout:2`,
ipToRemove: "9.9.9.9",
// nolint:dupword
expected: `# Comment about the first nameserver
nameserver 8.8.4.4
# Comment before our target
nameserver 9.9.9.9
options timeout:2`,
},
{
name: "Only nameserver matches",
content: `options debug
nameserver 9.9.9.9
search localdomain`,
ipToRemove: "9.9.9.9",
expected: `options debug
nameserver 9.9.9.9
search localdomain`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tempDir := t.TempDir()
tempFile := filepath.Join(tempDir, "resolv.conf")
err := os.WriteFile(tempFile, []byte(tc.content), 0644)
assert.NoError(t, err)
err = removeFirstNbNameserver(tempFile, tc.ipToRemove)
assert.NoError(t, err)
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, tc.expected, string(content), "The resulting content should match the expected output.")
})
}
}

View File

@@ -0,0 +1,159 @@
//go:build !android
package dns
import (
"path"
"path/filepath"
"sync"
"github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus"
)
var (
eventTypes = []fsnotify.Op{
fsnotify.Create,
fsnotify.Write,
fsnotify.Remove,
fsnotify.Rename,
}
)
type repairConfFn func([]string, string, *resolvConf) error
type repair struct {
operationFile string
updateFn repairConfFn
watchDir string
inotify *fsnotify.Watcher
inotifyWg sync.WaitGroup
}
func newRepair(operationFile string, updateFn repairConfFn) *repair {
targetFile := targetFile(operationFile)
return &repair{
operationFile: targetFile,
watchDir: path.Dir(targetFile),
updateFn: updateFn,
}
}
func (f *repair) watchFileChanges(nbSearchDomains []string, nbNameserverIP string) {
if f.inotify != nil {
return
}
log.Infof("start to watch resolv.conf: %s", f.operationFile)
inotify, err := fsnotify.NewWatcher()
if err != nil {
log.Errorf("failed to start inotify watcher for resolv.conf: %s", err)
return
}
f.inotify = inotify
f.inotifyWg.Add(1)
go func() {
defer f.inotifyWg.Done()
for event := range f.inotify.Events {
if !f.isEventRelevant(event) {
continue
}
log.Tracef("%s changed, check if it is broken", f.operationFile)
rConf, err := parseResolvConfFile(f.operationFile)
if err != nil {
log.Warnf("failed to parse resolv conf: %s", err)
continue
}
log.Debugf("check resolv.conf parameters: %s", rConf)
if !isNbParamsMissing(nbSearchDomains, nbNameserverIP, rConf) {
log.Tracef("resolv.conf still correct, skip the update")
continue
}
log.Info("broken params in resolv.conf, repairing it...")
err = f.inotify.Remove(f.watchDir)
if err != nil {
log.Errorf("failed to rm inotify watch for resolv.conf: %s", err)
}
err = f.updateFn(nbSearchDomains, nbNameserverIP, rConf)
if err != nil {
log.Errorf("failed to repair resolv.conf: %v", err)
}
err = f.inotify.Add(f.watchDir)
if err != nil {
log.Errorf("failed to re-add inotify watch for resolv.conf: %s", err)
return
}
}
}()
err = f.inotify.Add(f.watchDir)
if err != nil {
log.Errorf("failed to add inotify watch for resolv.conf: %s", err)
return
}
}
func (f *repair) stopWatchFileChanges() {
if f.inotify == nil {
return
}
err := f.inotify.Close()
if err != nil {
log.Warnf("failed to close resolv.conf inotify: %v", err)
}
f.inotifyWg.Wait()
f.inotify = nil
}
func (f *repair) isEventRelevant(event fsnotify.Event) bool {
var ok bool
for _, et := range eventTypes {
if event.Has(et) {
ok = true
break
}
}
if !ok {
return false
}
if event.Name == f.operationFile {
return true
}
return false
}
// nbParamsAreMissing checks if the resolv.conf file contains all the parameters that NetBird needs
// check the NetBird related nameserver IP at the first place
// check the NetBird related search domains in the search domains list
func isNbParamsMissing(nbSearchDomains []string, nbNameserverIP string, rConf *resolvConf) bool {
if !isContains(nbSearchDomains, rConf.searchDomains) {
return true
}
if len(rConf.nameServers) == 0 {
return true
}
if rConf.nameServers[0] != nbNameserverIP {
return true
}
return false
}
func targetFile(filename string) string {
target, err := filepath.EvalSymlinks(filename)
if err != nil {
log.Errorf("evarl err: %s", err)
}
return target
}

View File

@@ -0,0 +1,175 @@
//go:build !android
package dns
import (
"context"
"os"
"path/filepath"
"testing"
"time"
"github.com/netbirdio/netbird/util"
)
func TestMain(m *testing.M) {
_ = util.InitLog("debug", "console")
code := m.Run()
os.Exit(code)
}
func Test_newRepairtmp(t *testing.T) {
type args struct {
resolvConfContent string
touchedConfContent string
wantChange bool
}
tests := []args{
{
resolvConfContent: `
nameserver 10.0.0.1
nameserver 8.8.8.8
searchdomain netbird.cloud something`,
touchedConfContent: `
nameserver 8.8.8.8
searchdomain netbird.cloud something`,
wantChange: true,
},
{
resolvConfContent: `
nameserver 10.0.0.1
nameserver 8.8.8.8
searchdomain netbird.cloud something`,
touchedConfContent: `
nameserver 10.0.0.1
nameserver 8.8.8.8
searchdomain netbird.cloud something somethingelse`,
wantChange: false,
},
{
resolvConfContent: `
nameserver 10.0.0.1
nameserver 8.8.8.8
searchdomain netbird.cloud something`,
touchedConfContent: `
nameserver 10.0.0.1
searchdomain netbird.cloud something`,
wantChange: false,
},
{
resolvConfContent: `
nameserver 10.0.0.1
nameserver 8.8.8.8
searchdomain netbird.cloud something`,
touchedConfContent: `
searchdomain something`,
wantChange: true,
},
{
resolvConfContent: `
nameserver 10.0.0.1
nameserver 8.8.8.8
searchdomain netbird.cloud something`,
touchedConfContent: `
nameserver 10.0.0.1`,
wantChange: true,
},
{
resolvConfContent: `
nameserver 10.0.0.1
nameserver 8.8.8.8
searchdomain netbird.cloud something`,
touchedConfContent: `
nameserver 8.8.8.8`,
wantChange: true,
},
}
for _, tt := range tests {
tt := tt
t.Run("test", func(t *testing.T) {
t.Parallel()
workDir := t.TempDir()
operationFile := workDir + "/resolv.conf"
err := os.WriteFile(operationFile, []byte(tt.resolvConfContent), 0755)
if err != nil {
t.Fatalf("failed to write out resolv.conf: %s", err)
}
var changed bool
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
updateFn := func([]string, string, *resolvConf) error {
changed = true
cancel()
return nil
}
r := newRepair(operationFile, updateFn)
r.watchFileChanges([]string{"netbird.cloud"}, "10.0.0.1")
err = os.WriteFile(operationFile, []byte(tt.touchedConfContent), 0755)
if err != nil {
t.Fatalf("failed to write out resolv.conf: %s", err)
}
<-ctx.Done()
r.stopWatchFileChanges()
if changed != tt.wantChange {
t.Errorf("unexpected result: want: %v, got: %v", tt.wantChange, changed)
}
})
}
}
func Test_newRepairSymlink(t *testing.T) {
resolvConfContent := `
nameserver 10.0.0.1
nameserver 8.8.8.8
searchdomain netbird.cloud something`
modifyContent := `nameserver 8.8.8.8`
tmpResolvConf := filepath.Join(t.TempDir(), "resolv.conf")
err := os.WriteFile(tmpResolvConf, []byte(resolvConfContent), 0644)
if err != nil {
t.Fatal(err)
}
tmpLink := filepath.Join(t.TempDir(), "symlink")
err = os.Symlink(tmpResolvConf, tmpLink)
if err != nil {
t.Fatal(err)
}
var changed bool
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
updateFn := func([]string, string, *resolvConf) error {
changed = true
cancel()
return nil
}
r := newRepair(tmpLink, updateFn)
r.watchFileChanges([]string{"netbird.cloud"}, "10.0.0.1")
err = os.WriteFile(tmpLink, []byte(modifyContent), 0755)
if err != nil {
t.Fatalf("failed to write out resolv.conf: %s", err)
}
<-ctx.Done()
r.stopWatchFileChanges()
if changed != true {
t.Errorf("unexpected result: want: %v, got: %v", true, false)
}
}

View File

@@ -2,37 +2,40 @@ package dns
import (
"fmt"
"net/netip"
"strings"
nbdns "github.com/netbirdio/netbird/dns"
)
type hostManager interface {
applyDNSConfig(config hostDNSConfig) error
applyDNSConfig(config HostDNSConfig) error
restoreHostDNS() error
supportCustomPort() bool
restoreUncleanShutdownDNS(storedDNSAddress *netip.Addr) error
}
type hostDNSConfig struct {
domains []domainConfig
routeAll bool
serverIP string
serverPort int
type HostDNSConfig struct {
Domains []DomainConfig `json:"domains"`
RouteAll bool `json:"routeAll"`
ServerIP string `json:"serverIP"`
ServerPort int `json:"serverPort"`
}
type domainConfig struct {
disabled bool
domain string
matchOnly bool
type DomainConfig struct {
Disabled bool `json:"disabled"`
Domain string `json:"domain"`
MatchOnly bool `json:"matchOnly"`
}
type mockHostConfigurator struct {
applyDNSConfigFunc func(config hostDNSConfig) error
restoreHostDNSFunc func() error
supportCustomPortFunc func() bool
applyDNSConfigFunc func(config HostDNSConfig) error
restoreHostDNSFunc func() error
supportCustomPortFunc func() bool
restoreUncleanShutdownDNSFunc func(*netip.Addr) error
}
func (m *mockHostConfigurator) applyDNSConfig(config hostDNSConfig) error {
func (m *mockHostConfigurator) applyDNSConfig(config HostDNSConfig) error {
if m.applyDNSConfigFunc != nil {
return m.applyDNSConfigFunc(config)
}
@@ -53,40 +56,48 @@ func (m *mockHostConfigurator) supportCustomPort() bool {
return false
}
func (m *mockHostConfigurator) restoreUncleanShutdownDNS(storedDNSAddress *netip.Addr) error {
if m.restoreUncleanShutdownDNSFunc != nil {
return m.restoreUncleanShutdownDNSFunc(storedDNSAddress)
}
return fmt.Errorf("method restoreUncleanShutdownDNS is not implemented")
}
func newNoopHostMocker() hostManager {
return &mockHostConfigurator{
applyDNSConfigFunc: func(config hostDNSConfig) error { return nil },
restoreHostDNSFunc: func() error { return nil },
supportCustomPortFunc: func() bool { return true },
applyDNSConfigFunc: func(config HostDNSConfig) error { return nil },
restoreHostDNSFunc: func() error { return nil },
supportCustomPortFunc: func() bool { return true },
restoreUncleanShutdownDNSFunc: func(*netip.Addr) error { return nil },
}
}
func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) hostDNSConfig {
config := hostDNSConfig{
routeAll: false,
serverIP: ip,
serverPort: port,
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
config.RouteAll = true
}
for _, domain := range nsConfig.Domains {
config.domains = append(config.domains, domainConfig{
domain: strings.TrimSuffix(domain, "."),
matchOnly: !nsConfig.SearchDomainsEnabled,
config.Domains = append(config.Domains, DomainConfig{
Domain: strings.TrimSuffix(domain, "."),
MatchOnly: !nsConfig.SearchDomainsEnabled,
})
}
}
for _, customZone := range dnsConfig.CustomZones {
config.domains = append(config.domains, domainConfig{
domain: strings.TrimSuffix(customZone.Domain, "."),
matchOnly: false,
config.Domains = append(config.Domains, DomainConfig{
Domain: strings.TrimSuffix(customZone.Domain, "."),
MatchOnly: false,
})
}

View File

@@ -1,13 +1,15 @@
package dns
import "net/netip"
type androidHostManager struct {
}
func newHostManager(wgInterface WGIface) (hostManager, error) {
func newHostManager() (hostManager, error) {
return &androidHostManager{}, nil
}
func (a androidHostManager) applyDNSConfig(config hostDNSConfig) error {
func (a androidHostManager) applyDNSConfig(config HostDNSConfig) error {
return nil
}
@@ -18,3 +20,7 @@ func (a androidHostManager) restoreHostDNS() error {
func (a androidHostManager) supportCustomPort() bool {
return false
}
func (a androidHostManager) restoreUncleanShutdownDNS(*netip.Addr) error {
return nil
}

View File

@@ -1,9 +1,13 @@
//go:build !ios
package dns
import (
"bufio"
"bytes"
"fmt"
"io"
"net/netip"
"os/exec"
"strconv"
"strings"
@@ -32,7 +36,7 @@ type systemConfigurator struct {
createdKeys map[string]struct{}
}
func newHostManager(_ WGIface) (hostManager, error) {
func newHostManager() (hostManager, error) {
return &systemConfigurator{
createdKeys: make(map[string]struct{}),
}, nil
@@ -42,21 +46,26 @@ func (s *systemConfigurator) supportCustomPort() bool {
return true
}
func (s *systemConfigurator) applyDNSConfig(config hostDNSConfig) error {
func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig) error {
var err error
if config.routeAll {
err = s.addDNSSetupForAll(config.serverIP, config.serverPort)
if config.RouteAll {
err = s.addDNSSetupForAll(config.ServerIP, config.ServerPort)
if err != nil {
return err
return fmt.Errorf("add dns setup for all: %w", err)
}
} else if s.primaryServiceID != "" {
err = s.removeKeyFromSystemConfig(getKeyWithInput(primaryServiceSetupKeyFormat, s.primaryServiceID))
if err != nil {
return err
return fmt.Errorf("remote key from system config: %w", err)
}
s.primaryServiceID = ""
log.Infof("removed %s:%d as main DNS resolver for this peer", config.serverIP, config.serverPort)
log.Infof("removed %s:%d as main DNS resolver for this peer", config.ServerIP, config.ServerPort)
}
// create a file for unclean shutdown detection
if err := createUncleanShutdownIndicator(); err != nil {
log.Errorf("failed to create unclean shutdown file: %s", err)
}
var (
@@ -64,37 +73,37 @@ func (s *systemConfigurator) applyDNSConfig(config hostDNSConfig) error {
matchDomains []string
)
for _, dConf := range config.domains {
if dConf.disabled {
for _, dConf := range config.Domains {
if dConf.Disabled {
continue
}
if dConf.matchOnly {
matchDomains = append(matchDomains, dConf.domain)
if dConf.MatchOnly {
matchDomains = append(matchDomains, dConf.Domain)
continue
}
searchDomains = append(searchDomains, dConf.domain)
searchDomains = append(searchDomains, dConf.Domain)
}
matchKey := getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)
if len(matchDomains) != 0 {
err = s.addMatchDomains(matchKey, strings.Join(matchDomains, " "), config.serverIP, config.serverPort)
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
return fmt.Errorf("add match domains: %w", err)
}
searchKey := getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix)
if len(searchDomains) != 0 {
err = s.addSearchDomains(searchKey, strings.Join(searchDomains, " "), config.serverIP, config.serverPort)
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 fmt.Errorf("add search domains: %w", err)
}
return nil
@@ -117,7 +126,11 @@ func (s *systemConfigurator) restoreHostDNS() error {
_, err := runSystemConfigCommand(wrapCommand(lines))
if err != nil {
log.Errorf("got an error while cleaning the system configuration: %s", err)
return err
return fmt.Errorf("clean system: %w", err)
}
if err := removeUncleanShutdownIndicator(); err != nil {
log.Errorf("failed to remove unclean shutdown file: %s", err)
}
return nil
@@ -127,7 +140,7 @@ func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error {
line := buildRemoveKeyOperation(key)
_, err := runSystemConfigCommand(wrapCommand(line))
if err != nil {
return err
return fmt.Errorf("remove key: %w", err)
}
delete(s.createdKeys, key)
@@ -138,7 +151,7 @@ func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error {
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
return fmt.Errorf("add dns state: %w", err)
}
log.Infof("added %d search domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains)
@@ -151,7 +164,7 @@ func (s *systemConfigurator) addSearchDomains(key, domains string, ip string, po
func (s *systemConfigurator) addMatchDomains(key, domains, dnsServer string, port int) error {
err := s.addDNSState(key, domains, dnsServer, port, false)
if err != nil {
return err
return fmt.Errorf("add dns state: %w", err)
}
log.Infof("added %d match domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains)
@@ -176,33 +189,37 @@ func (s *systemConfigurator) addDNSState(state, domains, dnsServer string, port
_, err := runSystemConfigCommand(stdinCommands)
if err != nil {
return fmt.Errorf("got error while applying state for domains %s, error: %s", domains, err)
return fmt.Errorf("applying state for domains %s, error: %w", domains, err)
}
return nil
}
func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error {
primaryServiceKey, existingNameserver := s.getPrimaryService()
if primaryServiceKey == "" {
return fmt.Errorf("couldn't find the primary service key")
primaryServiceKey, existingNameserver, err := s.getPrimaryService()
if err != nil || primaryServiceKey == "" {
return fmt.Errorf("couldn't find the primary service key: %w", err)
}
err := s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port, existingNameserver)
err = s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port, existingNameserver)
if err != nil {
return err
return fmt.Errorf("add dns setup: %w", 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, string) {
func (s *systemConfigurator) getPrimaryService() (string, string, error) {
line := buildCommandLine("show", globalIPv4State, "")
stdinCommands := wrapCommand(line)
b, err := runSystemConfigCommand(stdinCommands)
if err != nil {
log.Error("got error while sending the command: ", err)
return "", ""
return "", "", fmt.Errorf("sending the command: %w", err)
}
scanner := bufio.NewScanner(bytes.NewReader(b))
primaryService := ""
router := ""
@@ -215,7 +232,11 @@ func (s *systemConfigurator) getPrimaryService() (string, string) {
router = strings.TrimSpace(strings.Split(text, ":")[1])
}
}
return primaryService, router
if err := scanner.Err(); err != nil && err != io.EOF {
return primaryService, router, fmt.Errorf("scan: %w", err)
}
return primaryService, router, nil
}
func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int, existingDNSServer string) error {
@@ -226,7 +247,14 @@ func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int, e
stdinCommands := wrapCommand(addDomainCommand)
_, err := runSystemConfigCommand(stdinCommands)
if err != nil {
return fmt.Errorf("got error while applying dns setup, error: %s", err)
return fmt.Errorf("applying dns setup, error: %w", err)
}
return nil
}
func (s *systemConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error {
if err := s.restoreHostDNS(); err != nil {
return fmt.Errorf("restoring dns via scutil: %w", err)
}
return nil
}
@@ -264,7 +292,7 @@ func runSystemConfigCommand(command string) ([]byte, error) {
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 nil, fmt.Errorf("running system configuration command: \"%s\", error: %w", command, err)
}
return out, nil
}

View File

@@ -0,0 +1,43 @@
package dns
import (
"encoding/json"
"fmt"
"net/netip"
log "github.com/sirupsen/logrus"
)
type iosHostManager struct {
dnsManager IosDnsManager
config HostDNSConfig
}
func newHostManager(dnsManager IosDnsManager) (hostManager, error) {
return &iosHostManager{
dnsManager: dnsManager,
}, nil
}
func (a iosHostManager) applyDNSConfig(config HostDNSConfig) error {
jsonData, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
jsonString := string(jsonData)
log.Debugf("Applying DNS settings: %s", jsonString)
a.dnsManager.ApplyDns(jsonString)
return nil
}
func (a iosHostManager) restoreHostDNS() error {
return nil
}
func (a iosHostManager) supportCustomPort() bool {
return false
}
func (a iosHostManager) restoreUncleanShutdownDNS(*netip.Addr) error {
return nil
}

View File

@@ -4,17 +4,15 @@ package dns
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"strings"
log "github.com/sirupsen/logrus"
)
const (
defaultResolvConfPath = "/etc/resolv.conf"
)
const (
netbirdManager osManagerType = iota
fileManager
@@ -23,8 +21,27 @@ const (
resolvConfManager
)
var ErrUnknownOsManagerType = errors.New("unknown os manager type")
type osManagerType int
func newOsManagerType(osManager string) (osManagerType, error) {
switch osManager {
case "netbird":
return fileManager, nil
case "file":
return netbirdManager, nil
case "networkManager":
return networkManager, nil
case "systemd":
return systemdManager, nil
case "resolvconf":
return resolvConfManager, nil
default:
return 0, ErrUnknownOsManagerType
}
}
func (t osManagerType) String() string {
switch t {
case netbirdManager:
@@ -42,13 +59,17 @@ func (t osManagerType) String() string {
}
}
func newHostManager(wgInterface WGIface) (hostManager, error) {
func newHostManager(wgInterface string) (hostManager, error) {
osManager, err := getOSDNSManagerType()
if err != nil {
return nil, err
}
log.Debugf("discovered mode is: %s", osManager)
log.Infof("System DNS manager discovered: %s", osManager)
return newHostManagerFromType(wgInterface, osManager)
}
func newHostManagerFromType(wgInterface string, osManager osManagerType) (hostManager, error) {
switch osManager {
case networkManager:
return newNetworkManagerDbusConfigurator(wgInterface)
@@ -62,12 +83,15 @@ func newHostManager(wgInterface WGIface) (hostManager, error) {
}
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)
return 0, fmt.Errorf("unable to open %s for checking owner, got error: %w", defaultResolvConfPath, err)
}
defer file.Close()
defer func() {
if err := file.Close(); err != nil {
log.Errorf("close file %s: %s", defaultResolvConfPath, err)
}
}()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
@@ -85,7 +109,11 @@ func getOSDNSManagerType() (osManagerType, error) {
return networkManager, nil
}
if strings.Contains(text, "systemd-resolved") && isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) {
return systemdManager, nil
if checkStub() {
return systemdManager, nil
} else {
return fileManager, nil
}
}
if strings.Contains(text, "resolvconf") {
if isDbusListenerRunning(systemdResolvedDest, systemdDbusObjectNode) {
@@ -101,5 +129,26 @@ func getOSDNSManagerType() (osManagerType, error) {
return resolvConfManager, nil
}
}
if err := scanner.Err(); err != nil && err != io.EOF {
return 0, fmt.Errorf("scan: %w", err)
}
return fileManager, nil
}
// checkStub checks if the stub resolver is disabled in systemd-resolved. If it is disabled, we fall back to file manager.
func checkStub() bool {
rConf, err := parseDefaultResolvConf()
if err != nil {
log.Warnf("failed to parse resolv conf: %s", err)
return true
}
for _, ns := range rConf.nameServers {
if ns == "127.0.0.53" {
return true
}
}
return false
}

View File

@@ -2,6 +2,8 @@ package dns
import (
"fmt"
"io"
"net/netip"
"strings"
log "github.com/sirupsen/logrus"
@@ -9,7 +11,7 @@ import (
)
const (
dnsPolicyConfigMatchPath = "SYSTEM\\CurrentControlSet\\Services\\Dnscache\\Parameters\\DnsPolicyConfig\\NetBird-Match"
dnsPolicyConfigMatchPath = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig\NetBird-Match`
dnsPolicyConfigVersionKey = "Version"
dnsPolicyConfigVersionValue = 2
dnsPolicyConfigNameKey = "Name"
@@ -19,7 +21,7 @@ const (
)
const (
interfaceConfigPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces"
interfaceConfigPath = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces`
interfaceConfigNameServerKey = "NameServer"
interfaceConfigSearchListKey = "SearchList"
)
@@ -34,29 +36,38 @@ func newHostManager(wgInterface WGIface) (hostManager, error) {
if err != nil {
return nil, err
}
return newHostManagerWithGuid(guid)
}
func newHostManagerWithGuid(guid string) (hostManager, error) {
return &registryConfigurator{
guid: guid,
}, nil
}
func (s *registryConfigurator) supportCustomPort() bool {
func (r *registryConfigurator) supportCustomPort() bool {
return false
}
func (r *registryConfigurator) applyDNSConfig(config hostDNSConfig) error {
func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig) error {
var err error
if config.routeAll {
err = r.addDNSSetupForAll(config.serverIP)
if config.RouteAll {
err = r.addDNSSetupForAll(config.ServerIP)
if err != nil {
return err
return fmt.Errorf("add dns setup: %w", err)
}
} else if r.routingAll {
err = r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey)
if err != nil {
return err
return fmt.Errorf("delete interface registry key property: %w", err)
}
r.routingAll = false
log.Infof("removed %s as main DNS forwarder for this peer", config.serverIP)
log.Infof("removed %s as main DNS forwarder for this peer", config.ServerIP)
}
// create a file for unclean shutdown detection
if err := createUncleanShutdownIndicator(r.guid); err != nil {
log.Errorf("failed to create unclean shutdown file: %s", err)
}
var (
@@ -64,28 +75,28 @@ func (r *registryConfigurator) applyDNSConfig(config hostDNSConfig) error {
matchDomains []string
)
for _, dConf := range config.domains {
if dConf.disabled {
for _, dConf := range config.Domains {
if dConf.Disabled {
continue
}
if !dConf.matchOnly {
searchDomains = append(searchDomains, dConf.domain)
if !dConf.MatchOnly {
searchDomains = append(searchDomains, dConf.Domain)
}
matchDomains = append(matchDomains, "."+dConf.domain)
matchDomains = append(matchDomains, "."+dConf.Domain)
}
if len(matchDomains) != 0 {
err = r.addDNSMatchPolicy(matchDomains, config.serverIP)
err = r.addDNSMatchPolicy(matchDomains, config.ServerIP)
} else {
err = removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath)
}
if err != nil {
return err
return fmt.Errorf("add dns match policy: %w", err)
}
err = r.updateSearchDomains(searchDomains)
if err != nil {
return err
return fmt.Errorf("update search domains: %w", err)
}
return nil
@@ -94,7 +105,7 @@ func (r *registryConfigurator) applyDNSConfig(config hostDNSConfig) error {
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)
return fmt.Errorf("adding dns setup for all failed with error: %w", err)
}
r.routingAll = true
log.Infof("configured %s:53 as main DNS forwarder for this peer", ip)
@@ -106,33 +117,33 @@ func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) er
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)
return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %w", 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)
return fmt.Errorf("unable to create registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %w", dnsPolicyConfigMatchPath, err)
}
err = regKey.SetDWordValue(dnsPolicyConfigVersionKey, dnsPolicyConfigVersionValue)
if err != nil {
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigVersionKey, err)
return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigVersionKey, err)
}
err = regKey.SetStringsValue(dnsPolicyConfigNameKey, domains)
if err != nil {
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigNameKey, err)
return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigNameKey, err)
}
err = regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip)
if err != nil {
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigGenericDNSServersKey, err)
return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigGenericDNSServersKey, err)
}
err = regKey.SetDWordValue(dnsPolicyConfigConfigOptionsKey, dnsPolicyConfigConfigOptionsValue)
if err != nil {
return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigConfigOptionsKey, err)
return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigConfigOptionsKey, err)
}
log.Infof("added %d match domains to the state. Domain list: %s", len(domains), domains)
@@ -141,18 +152,25 @@ func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) er
}
func (r *registryConfigurator) restoreHostDNS() error {
err := removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath)
if err != nil {
log.Error(err)
if err := removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath); err != nil {
log.Errorf("remove registry key from dns policy config: %s", err)
}
return r.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey)
if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey); err != nil {
return fmt.Errorf("remove interface registry key: %w", err)
}
if err := removeUncleanShutdownIndicator(); err != nil {
log.Errorf("failed to remove unclean shutdown file: %s", err)
}
return nil
}
func (r *registryConfigurator) updateSearchDomains(domains []string) error {
err := r.setInterfaceRegistryKeyStringValue(interfaceConfigSearchListKey, strings.Join(domains, ","))
if err != nil {
return fmt.Errorf("adding search domain failed with error: %s", err)
return fmt.Errorf("adding search domain failed with error: %w", err)
}
log.Infof("updated the search domains in the registry with %d domains. Domain list: %s", len(domains), domains)
@@ -163,13 +181,13 @@ func (r *registryConfigurator) updateSearchDomains(domains []string) error {
func (r *registryConfigurator) setInterfaceRegistryKeyStringValue(key, value string) error {
regKey, err := r.getInterfaceRegistryKey()
if err != nil {
return err
return fmt.Errorf("get interface registry key: %w", err)
}
defer regKey.Close()
defer closer(regKey)
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 fmt.Errorf("applying key %s with value \"%s\" for interface failed with error: %w", key, value, err)
}
return nil
@@ -178,13 +196,13 @@ func (r *registryConfigurator) setInterfaceRegistryKeyStringValue(key, value str
func (r *registryConfigurator) deleteInterfaceRegistryKeyProperty(propertyKey string) error {
regKey, err := r.getInterfaceRegistryKey()
if err != nil {
return err
return fmt.Errorf("get interface registry key: %w", err)
}
defer regKey.Close()
defer closer(regKey)
err = regKey.DeleteValue(propertyKey)
if err != nil {
return fmt.Errorf("deleting registry key %s for interface failed with error: %s", propertyKey, err)
return fmt.Errorf("deleting registry key %s for interface failed with error: %w", propertyKey, err)
}
return nil
@@ -197,20 +215,33 @@ func (r *registryConfigurator) getInterfaceRegistryKey() (registry.Key, error) {
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, fmt.Errorf("unable to open the interface registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %w", regKeyPath, err)
}
return regKey, nil
}
func (r *registryConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error {
if err := r.restoreHostDNS(); err != nil {
return fmt.Errorf("restoring dns via registry: %w", err)
}
return nil
}
func removeRegistryKeyFromDNSPolicyConfig(regKeyPath string) error {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.QUERY_VALUE)
if err == nil {
k.Close()
defer closer(k)
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 fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %w", regKeyPath, err)
}
}
return nil
}
func closer(closer io.Closer) {
if err := closer.Close(); err != nil {
log.Errorf("failed to close: %s", err)
}
}

View File

@@ -52,7 +52,7 @@ func (d *localResolver) lookupRecord(r *dns.Msg) dns.RR {
func (d *localResolver) registerRecord(record nbdns.SimpleRecord) error {
fullRecord, err := dns.NewRR(record.String())
if err != nil {
return err
return fmt.Errorf("register record: %w", err)
}
fullRecord.Header().Rdlength = record.Len()
@@ -71,3 +71,5 @@ func buildRecordKey(name string, class, qType uint16) string {
key := fmt.Sprintf("%s_%d_%d", name, class, qType)
return key
}
func (d *localResolver) probeAvailability() {}

View File

@@ -1,10 +1,12 @@
package dns
import (
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
"strings"
"testing"
"github.com/miekg/dns"
nbdns "github.com/netbirdio/netbird/dns"
)
func TestLocalResolver_ServeDNS(t *testing.T) {

View File

@@ -33,7 +33,7 @@ func (m *MockServer) DnsIP() string {
}
func (m *MockServer) OnUpdatedHostDNSServer(strings []string) {
//TODO implement me
// TODO implement me
panic("implement me")
}
@@ -48,3 +48,7 @@ func (m *MockServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
func (m *MockServer) SearchDomains() []string {
return make([]string, 0)
}
// ProbeAvailability mocks implementation of ProbeAvailability from the Server interface
func (m *MockServer) ProbeAvailability() {
}

View File

@@ -5,15 +5,18 @@ package dns
import (
"context"
"encoding/binary"
"errors"
"fmt"
"net/netip"
"strings"
"time"
"github.com/godbus/dbus/v5"
"github.com/hashicorp/go-version"
"github.com/miekg/dns"
nbversion "github.com/netbirdio/netbird/version"
log "github.com/sirupsen/logrus"
nbversion "github.com/netbirdio/netbird/version"
)
const (
@@ -40,9 +43,13 @@ const (
networkManagerDbusPrimaryDNSPriority int32 = -500
networkManagerDbusWithMatchDomainPriority int32 = 0
networkManagerDbusSearchDomainOnlyPriority int32 = 50
supportedNetworkManagerVersionConstraint = ">= 1.16, < 1.28"
)
var supportedNetworkManagerVersionConstraints = []string{
">= 1.16, < 1.27",
">= 1.44, < 1.45",
}
type networkManagerDbusConfigurator struct {
dbusLinkObject dbus.ObjectPath
routingAll bool
@@ -70,19 +77,19 @@ func (s networkManagerConnSettings) cleanDeprecatedSettings() {
}
}
func newNetworkManagerDbusConfigurator(wgInterface WGIface) (hostManager, error) {
func newNetworkManagerDbusConfigurator(wgInterface string) (hostManager, error) {
obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusObjectNode)
if err != nil {
return nil, err
return nil, fmt.Errorf("get nm dbus: %w", err)
}
defer closeConn()
var s string
err = obj.Call(networkManagerDbusGetDeviceByIPIfaceMethod, dbusDefaultFlag, wgInterface.Name()).Store(&s)
err = obj.Call(networkManagerDbusGetDeviceByIPIfaceMethod, dbusDefaultFlag, wgInterface).Store(&s)
if err != nil {
return nil, err
return nil, fmt.Errorf("call: %w", err)
}
log.Debugf("got network manager dbus Link Object: %s from net interface %s", s, wgInterface.Name())
log.Debugf("got network manager dbus Link Object: %s from net interface %s", s, wgInterface)
return &networkManagerDbusConfigurator{
dbusLinkObject: dbus.ObjectPath(s),
@@ -93,17 +100,17 @@ func (n *networkManagerDbusConfigurator) supportCustomPort() bool {
return false
}
func (n *networkManagerDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
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)
return fmt.Errorf("retrieving the applied connection settings, error: %w", err)
}
connSettings.cleanDeprecatedSettings()
dnsIP, err := netip.ParseAddr(config.serverIP)
dnsIP, err := netip.ParseAddr(config.ServerIP)
if err != nil {
return fmt.Errorf("unable to parse ip address, error: %s", err)
return fmt.Errorf("unable to parse ip address, error: %w", err)
}
convDNSIP := binary.LittleEndian.Uint32(dnsIP.AsSlice())
connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSKey] = dbus.MakeVariant([]uint32{convDNSIP})
@@ -111,56 +118,70 @@ func (n *networkManagerDbusConfigurator) applyDNSConfig(config hostDNSConfig) er
searchDomains []string
matchDomains []string
)
for _, dConf := range config.domains {
if dConf.disabled {
for _, dConf := range config.Domains {
if dConf.Disabled {
continue
}
if dConf.matchOnly {
matchDomains = append(matchDomains, "~."+dns.Fqdn(dConf.domain))
if dConf.MatchOnly {
matchDomains = append(matchDomains, "~."+dns.Fqdn(dConf.Domain))
continue
}
searchDomains = append(searchDomains, dns.Fqdn(dConf.domain))
searchDomains = append(searchDomains, dns.Fqdn(dConf.Domain))
}
newDomainList := append(searchDomains, matchDomains...) //nolint:gocritic
priority := networkManagerDbusSearchDomainOnlyPriority
switch {
case config.routeAll:
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)
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)
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)
// create a backup for unclean shutdown detection before adding domains, as these might end up in the resolv.conf file.
// The file content itself is not important for network-manager restoration
if err := createUncleanShutdownIndicator(defaultResolvConfPath, networkManager, dnsIP.String()); err != nil {
log.Errorf("failed to create unclean shutdown resolv.conf backup: %s", err)
}
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 fmt.Errorf("reapplying the connection with new settings, error: %w", err)
}
return nil
}
func (n *networkManagerDbusConfigurator) restoreHostDNS() error {
// once the interface is gone network manager cleans all config associated with it
return n.deleteConnectionSettings()
if err := n.deleteConnectionSettings(); err != nil {
return fmt.Errorf("delete connection settings: %w", err)
}
if err := removeUncleanShutdownIndicator(); err != nil {
log.Errorf("failed to remove unclean shutdown resolv.conf backup: %s", err)
}
return nil
}
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)
return nil, 0, fmt.Errorf("attempting to retrieve the applied connection settings, err: %w", err)
}
defer closeConn()
@@ -175,7 +196,7 @@ func (n *networkManagerDbusConfigurator) getAppliedConnectionSettings() (network
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 nil, 0, fmt.Errorf("calling GetAppliedConnection method with context, err: %w", err)
}
return connSettings, configVersion, nil
@@ -184,7 +205,7 @@ func (n *networkManagerDbusConfigurator) getAppliedConnectionSettings() (network
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)
return fmt.Errorf("attempting to retrieve the applied connection settings, err: %w", err)
}
defer closeConn()
@@ -194,7 +215,7 @@ func (n *networkManagerDbusConfigurator) reApplyConnectionSettings(connSettings
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 fmt.Errorf("calling ReApply method with context, err: %w", err)
}
return nil
@@ -203,21 +224,34 @@ func (n *networkManagerDbusConfigurator) reApplyConnectionSettings(connSettings
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)
return fmt.Errorf("attempting to retrieve the applied connection settings, err: %w", err)
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
// this call is required to remove the device for DNS cleanup, even if it fails
err = obj.CallWithContext(ctx, networkManagerDbusDeviceDeleteMethod, dbusDefaultFlag).Store()
if err != nil {
return fmt.Errorf("got error while calling delete method with context, err: %s", err)
var dbusErr dbus.Error
if errors.As(err, &dbusErr) && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name {
// interface is gone already
return nil
}
return fmt.Errorf("calling delete method with context, err: %s", err)
}
return nil
}
func (n *networkManagerDbusConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error {
if err := n.restoreHostDNS(); err != nil {
return fmt.Errorf("restoring dns via network-manager: %w", err)
}
return nil
}
func isNetworkManagerSupported() bool {
return isNetworkManagerSupportedVersion() && isNetworkManagerSupportedMode()
}
@@ -249,13 +283,13 @@ func isNetworkManagerSupportedMode() bool {
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)
return fmt.Errorf("attempting to retrieve the network manager dns manager object, error: %w", err)
}
defer closeConn()
v, e := obj.GetProperty(property)
if e != nil {
return fmt.Errorf("got an error getting property %s: %v", property, e)
return fmt.Errorf("getting property %s: %w", property, e)
}
return v.Store(store)
@@ -277,15 +311,26 @@ func isNetworkManagerSupportedVersion() bool {
}
versionValue, err := parseVersion(value.Value().(string))
if err != nil {
log.Errorf("nm: parse version: %s", err)
return false
}
constraints, err := version.NewConstraint(supportedNetworkManagerVersionConstraint)
if err != nil {
return false
var supported bool
for _, constraint := range supportedNetworkManagerVersionConstraints {
constr, err := version.NewConstraint(constraint)
if err != nil {
log.Errorf("nm: create constraint: %s", err)
return false
}
if met := constr.Check(versionValue); met {
supported = true
break
}
}
return constraints.Check(versionValue)
log.Debugf("network manager constraints [%s] met: %t", strings.Join(supportedNetworkManagerVersionConstraints, " | "), supported)
return supported
}
func parseVersion(inputVersion string) (*version.Version, error) {

View File

@@ -52,6 +52,6 @@ func (n *notifier) notify() {
}
go func(l listener.NetworkChangeListener) {
l.OnNetworkChanged()
l.OnNetworkChanged("")
}(n.listener)
}

View File

@@ -5,6 +5,7 @@ package dns
import (
"bytes"
"fmt"
"net/netip"
"os/exec"
log "github.com/sirupsen/logrus"
@@ -21,17 +22,17 @@ type resolvconf struct {
}
// supported "openresolv" only
func newResolvConfConfigurator(wgInterface WGIface) (hostManager, error) {
originalSearchDomains, nameServers, others, err := originalDNSConfigs("/etc/resolv.conf")
func newResolvConfConfigurator(wgInterface string) (hostManager, error) {
resolvConfEntries, err := parseDefaultResolvConf()
if err != nil {
log.Error(err)
log.Errorf("could not read original search domains from %s: %s", defaultResolvConfPath, err)
}
return &resolvconf{
ifaceName: wgInterface.Name(),
originalSearchDomains: originalSearchDomains,
originalNameServers: nameServers,
othersConfigs: others,
ifaceName: wgInterface,
originalSearchDomains: resolvConfEntries.searchDomains,
originalNameServers: resolvConfEntries.nameServers,
othersConfigs: resolvConfEntries.others,
}, nil
}
@@ -39,12 +40,12 @@ func (r *resolvconf) supportCustomPort() bool {
return false
}
func (r *resolvconf) applyDNSConfig(config hostDNSConfig) error {
func (r *resolvconf) applyDNSConfig(config HostDNSConfig) error {
var err error
if !config.routeAll {
if !config.RouteAll {
err = r.restoreHostDNS()
if err != nil {
log.Error(err)
log.Errorf("restore host dns: %s", err)
}
return fmt.Errorf("unable to configure DNS for this peer using resolvconf manager without a nameserver group with all domains configured")
}
@@ -52,14 +53,21 @@ func (r *resolvconf) applyDNSConfig(config hostDNSConfig) error {
searchDomainList := searchDomains(config)
searchDomainList = mergeSearchDomains(searchDomainList, r.originalSearchDomains)
options := prepareOptionsWithTimeout(r.othersConfigs, int(dnsFailoverTimeout.Seconds()), dnsFailoverAttempts)
buf := prepareResolvConfContent(
searchDomainList,
append([]string{config.serverIP}, r.originalNameServers...),
r.othersConfigs)
append([]string{config.ServerIP}, r.originalNameServers...),
options)
// create a backup for unclean shutdown detection before the resolv.conf is changed
if err := createUncleanShutdownIndicator(defaultResolvConfPath, resolvConfManager, config.ServerIP); err != nil {
log.Errorf("failed to create unclean shutdown resolv.conf backup: %s", err)
}
err = r.applyConfig(buf)
if err != nil {
return err
return fmt.Errorf("apply config: %w", err)
}
log.Infof("added %d search domains. Search list: %s", len(searchDomainList), searchDomainList)
@@ -67,20 +75,34 @@ func (r *resolvconf) applyDNSConfig(config hostDNSConfig) error {
}
func (r *resolvconf) restoreHostDNS() error {
// openresolv only, debian resolvconf doesn't support "-f"
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 fmt.Errorf("removing resolvconf configuration for %s interface, error: %w", r.ifaceName, err)
}
if err := removeUncleanShutdownIndicator(); err != nil {
log.Errorf("failed to remove unclean shutdown resolv.conf backup: %s", err)
}
return nil
}
func (r *resolvconf) applyConfig(content bytes.Buffer) error {
// openresolv only, debian resolvconf doesn't support "-x"
cmd := exec.Command(resolvconfCommand, "-x", "-a", r.ifaceName)
cmd.Stdin = &content
_, err := cmd.Output()
if err != nil {
return fmt.Errorf("got an error while applying resolvconf configuration for %s interface, error: %s", r.ifaceName, err)
return fmt.Errorf("applying resolvconf configuration for %s interface, error: %w", r.ifaceName, err)
}
return nil
}
func (r *resolvconf) restoreUncleanShutdownDNS(*netip.Addr) error {
if err := r.restoreHostDNS(); err != nil {
return fmt.Errorf("restoring dns for interface %s: %w", r.ifaceName, err)
}
return nil
}

View File

@@ -31,10 +31,13 @@ func (r *responseWriter) RemoteAddr() net.Addr {
func (r *responseWriter) WriteMsg(msg *dns.Msg) error {
buff, err := msg.Pack()
if err != nil {
return err
return fmt.Errorf("pack: %w", err)
}
_, err = r.Write(buff)
return err
if _, err := r.Write(buff); err != nil {
return fmt.Errorf("write: %w", err)
}
return nil
}
// Write writes a raw buffer back to the client.

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/netip"
"strings"
"sync"
"github.com/miekg/dns"
@@ -11,6 +12,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer"
nbdns "github.com/netbirdio/netbird/dns"
)
@@ -19,6 +21,11 @@ type ReadyListener interface {
OnReady()
}
// IosDnsManager is a dns manager interface for iOS
type IosDnsManager interface {
ApplyDns(string)
}
// Server is a dns server interface
type Server interface {
Initialize() error
@@ -27,6 +34,7 @@ type Server interface {
UpdateDNSServer(serial uint64, update nbdns.Config) error
OnUpdatedHostDNSServer(strings []string)
SearchDomains() []string
ProbeAvailability()
}
type registeredHandlerMap map[string]handlerWithStop
@@ -43,7 +51,7 @@ type DefaultServer struct {
hostManager hostManager
updateSerial uint64
previousConfigHash uint64
currentConfig hostDNSConfig
currentConfig HostDNSConfig
// permanent related properties
permanent bool
@@ -52,11 +60,15 @@ type DefaultServer struct {
// make sense on mobile only
searchDomainNotifier *notifier
iosDnsManager IosDnsManager
statusRecorder *peer.Status
}
type handlerWithStop interface {
dns.Handler
stop()
probeAvailability()
}
type muxUpdate struct {
@@ -65,7 +77,12 @@ type muxUpdate struct {
}
// NewDefaultServer returns a new dns server
func NewDefaultServer(ctx context.Context, wgInterface WGIface, customAddress string) (*DefaultServer, error) {
func NewDefaultServer(
ctx context.Context,
wgInterface WGIface,
customAddress string,
statusRecorder *peer.Status,
) (*DefaultServer, error) {
var addrPort *netip.AddrPort
if customAddress != "" {
parsedAddrPort, err := netip.ParseAddrPort(customAddress)
@@ -82,13 +99,20 @@ func NewDefaultServer(ctx context.Context, wgInterface WGIface, customAddress st
dnsService = newServiceViaListener(wgInterface, addrPort)
}
return newDefaultServer(ctx, wgInterface, dnsService), nil
return newDefaultServer(ctx, wgInterface, dnsService, statusRecorder), nil
}
// NewDefaultServerPermanentUpstream returns a new dns server. It optimized for mobile systems
func NewDefaultServerPermanentUpstream(ctx context.Context, wgInterface WGIface, hostsDnsList []string, config nbdns.Config, listener listener.NetworkChangeListener) *DefaultServer {
func NewDefaultServerPermanentUpstream(
ctx context.Context,
wgInterface WGIface,
hostsDnsList []string,
config nbdns.Config,
listener listener.NetworkChangeListener,
statusRecorder *peer.Status,
) *DefaultServer {
log.Debugf("host dns address list is: %v", hostsDnsList)
ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface))
ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface), statusRecorder)
ds.permanent = true
ds.hostsDnsList = hostsDnsList
ds.addHostRootZone()
@@ -99,7 +123,19 @@ func NewDefaultServerPermanentUpstream(ctx context.Context, wgInterface WGIface,
return ds
}
func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService service) *DefaultServer {
// NewDefaultServerIos returns a new dns server. It optimized for ios
func NewDefaultServerIos(
ctx context.Context,
wgInterface WGIface,
iosDnsManager IosDnsManager,
statusRecorder *peer.Status,
) *DefaultServer {
ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface), statusRecorder)
ds.iosDnsManager = iosDnsManager
return ds
}
func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService service, statusRecorder *peer.Status) *DefaultServer {
ctx, stop := context.WithCancel(ctx)
defaultServer := &DefaultServer{
ctx: ctx,
@@ -109,7 +145,8 @@ func newDefaultServer(ctx context.Context, wgInterface WGIface, dnsService servi
localResolver: &localResolver{
registeredMap: make(registrationMap),
},
wgInterface: wgInterface,
wgInterface: wgInterface,
statusRecorder: statusRecorder,
}
return defaultServer
@@ -127,12 +164,15 @@ func (s *DefaultServer) Initialize() (err error) {
if s.permanent {
err = s.service.Listen()
if err != nil {
return err
return fmt.Errorf("service listen: %w", err)
}
}
s.hostManager, err = newHostManager(s.wgInterface)
return
s.hostManager, err = s.initialize()
if err != nil {
return fmt.Errorf("initialize: %w", err)
}
return nil
}
// DnsIP returns the DNS resolver server IP address
@@ -210,7 +250,7 @@ func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) erro
}
if err := s.applyConfiguration(update); err != nil {
return err
return fmt.Errorf("apply configuration: %w", err)
}
s.updateSerial = serial
@@ -223,20 +263,34 @@ func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) erro
func (s *DefaultServer) SearchDomains() []string {
var searchDomains []string
for _, dConf := range s.currentConfig.domains {
if dConf.disabled {
for _, dConf := range s.currentConfig.Domains {
if dConf.Disabled {
continue
}
if dConf.matchOnly {
if dConf.MatchOnly {
continue
}
searchDomains = append(searchDomains, dConf.domain)
searchDomains = append(searchDomains, dConf.Domain)
}
return searchDomains
}
// ProbeAvailability tests each upstream group's servers for availability
// and deactivates the group if no server responds
func (s *DefaultServer) ProbeAvailability() {
var wg sync.WaitGroup
for _, mux := range s.dnsMuxMap {
wg.Add(1)
go func(mux handlerWithStop) {
defer wg.Done()
mux.probeAvailability()
}(mux)
}
wg.Wait()
}
func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
// is the service should be disabled, we stop the listener or fake resolver
// is the service should be Disabled, we stop the listener or fake resolver
// and proceed with a regular update to clean up the handlers and records
if update.ServiceEnable {
_ = s.service.Listen()
@@ -262,7 +316,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
if s.service.RuntimePort() != defaultPort && !s.hostManager.supportCustomPort() {
log.Warnf("the DNS manager of this peer doesn't support custom port. Disabling primary DNS setup. " +
"Learn more at: https://docs.netbird.io/how-to/manage-dns-in-your-network#local-resolver")
hostUpdate.routeAll = false
hostUpdate.RouteAll = false
}
if err = s.hostManager.applyDNSConfig(hostUpdate); err != nil {
@@ -273,6 +327,8 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
s.searchDomainNotifier.onNewSearchDomains(s.SearchDomains())
}
s.updateNSGroupStates(update.NameServerGroups)
return nil
}
@@ -312,7 +368,16 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam
continue
}
handler := newUpstreamResolver(s.ctx)
handler, err := newUpstreamResolver(
s.ctx,
s.wgInterface.Name(),
s.wgInterface.Address().IP,
s.wgInterface.Address().Network,
s.statusRecorder,
)
if err != nil {
return nil, fmt.Errorf("unable to create a new upstream resolver, error: %v", err)
}
for _, ns := range nsGroup.NameServers {
if ns.NSType != nbdns.UDPNameServerType {
log.Warnf("skipping nameserver %s with type %s, this peer supports only %s",
@@ -362,6 +427,7 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam
})
}
}
return muxUpdates, nil
}
@@ -430,14 +496,14 @@ func getNSHostPort(ns nbdns.NameServer) string {
func (s *DefaultServer) upstreamCallbacks(
nsGroup *nbdns.NameServerGroup,
handler dns.Handler,
) (deactivate func(), reactivate func()) {
) (deactivate func(error), reactivate func()) {
var removeIndex map[string]int
deactivate = func() {
deactivate = func(err error) {
s.mux.Lock()
defer s.mux.Unlock()
l := log.WithField("nameservers", nsGroup.NameServers)
l.Info("temporary deactivate nameservers group due timeout")
l.Info("Temporarily deactivating nameservers group due to timeout")
removeIndex = make(map[string]int)
for _, domain := range nsGroup.Domains {
@@ -445,29 +511,32 @@ func (s *DefaultServer) upstreamCallbacks(
}
if nsGroup.Primary {
removeIndex[nbdns.RootZone] = -1
s.currentConfig.routeAll = false
s.currentConfig.RouteAll = false
}
for i, item := range s.currentConfig.domains {
if _, found := removeIndex[item.domain]; found {
s.currentConfig.domains[i].disabled = true
s.service.DeregisterMux(item.domain)
removeIndex[item.domain] = i
for i, item := range s.currentConfig.Domains {
if _, found := removeIndex[item.Domain]; found {
s.currentConfig.Domains[i].Disabled = true
s.service.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")
l.Errorf("Failed to apply nameserver deactivation on the host: %v", err)
}
s.updateNSState(nsGroup, err, false)
}
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 {
if i == -1 || i >= len(s.currentConfig.Domains) || s.currentConfig.Domains[i].Domain != domain {
continue
}
s.currentConfig.domains[i].disabled = false
s.currentConfig.Domains[i].Disabled = false
s.service.RegisterMux(domain, handler)
}
@@ -475,17 +544,29 @@ func (s *DefaultServer) upstreamCallbacks(
l.Debug("reactivate temporary disabled nameserver group")
if nsGroup.Primary {
s.currentConfig.routeAll = true
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")
}
s.updateNSState(nsGroup, nil, true)
}
return
}
func (s *DefaultServer) addHostRootZone() {
handler := newUpstreamResolver(s.ctx)
handler, err := newUpstreamResolver(
s.ctx,
s.wgInterface.Name(),
s.wgInterface.Address().IP,
s.wgInterface.Address().Network,
s.statusRecorder,
)
if err != nil {
log.Errorf("unable to create a new upstream resolver, error: %v", err)
return
}
handler.upstreamServers = make([]string, len(s.hostsDnsList))
for n, ua := range s.hostsDnsList {
a, err := netip.ParseAddr(ua)
@@ -501,7 +582,50 @@ func (s *DefaultServer) addHostRootZone() {
handler.upstreamServers[n] = fmt.Sprintf("%s:53", ipString)
}
handler.deactivate = func() {}
handler.deactivate = func(error) {}
handler.reactivate = func() {}
s.service.RegisterMux(nbdns.RootZone, handler)
}
func (s *DefaultServer) updateNSGroupStates(groups []*nbdns.NameServerGroup) {
var states []peer.NSGroupState
for _, group := range groups {
var servers []string
for _, ns := range group.NameServers {
servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port))
}
state := peer.NSGroupState{
ID: generateGroupKey(group),
Servers: servers,
Domains: group.Domains,
// The probe will determine the state, default enabled
Enabled: true,
Error: nil,
}
states = append(states, state)
}
s.statusRecorder.UpdateDNSStates(states)
}
func (s *DefaultServer) updateNSState(nsGroup *nbdns.NameServerGroup, err error, enabled bool) {
states := s.statusRecorder.GetDNSStates()
id := generateGroupKey(nsGroup)
for i, state := range states {
if state.ID == id {
states[i].Enabled = enabled
states[i].Error = err
break
}
}
s.statusRecorder.UpdateDNSStates(states)
}
func generateGroupKey(nsGroup *nbdns.NameServerGroup) string {
var servers []string
for _, ns := range nsGroup.NameServers {
servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port))
}
return fmt.Sprintf("%s_%s_%s", nsGroup.ID, nsGroup.Name, strings.Join(servers, ","))
}

View File

@@ -0,0 +1,5 @@
package dns
func (s *DefaultServer) initialize() (manager hostManager, err error) {
return newHostManager()
}

View File

@@ -0,0 +1,7 @@
//go:build !ios
package dns
func (s *DefaultServer) initialize() (manager hostManager, err error) {
return newHostManager()
}

View File

@@ -0,0 +1,5 @@
package dns
func (s *DefaultServer) initialize() (manager hostManager, err error) {
return newHostManager(s.iosDnsManager)
}

View File

@@ -0,0 +1,7 @@
//go:build !android
package dns
func (s *DefaultServer) initialize() (manager hostManager, err error) {
return newHostManager(s.wgInterface.Name())
}

View File

@@ -12,8 +12,10 @@ import (
"github.com/golang/mock/gomock"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/firewall/uspfilter"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/stdnet"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/formatter"
@@ -58,6 +60,10 @@ func (w *mocWGIface) SetFilter(filter iface.PacketFilter) error {
return nil
}
func (w *mocWGIface) GetStats(_ string) (iface.WGStats, error) {
return iface.WGStats{}, nil
}
var zoneRecords = []nbdns.SimpleRecord{
{
Name: "peera.netbird.cloud",
@@ -250,11 +256,12 @@ func TestUpdateDNSServer(t *testing.T) {
for n, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
privKey, _ := wgtypes.GenerateKey()
newNet, err := stdnet.NewNet(nil)
if err != nil {
t.Fatal(err)
}
wgIface, err := iface.NewWGIFace(fmt.Sprintf("utun230%d", n), fmt.Sprintf("100.66.100.%d/32", n+1), iface.DefaultMTU, nil, newNet)
wgIface, err := iface.NewWGIFace(fmt.Sprintf("utun230%d", n), fmt.Sprintf("100.66.100.%d/32", n+1), 33100, privKey.String(), iface.DefaultMTU, newNet, nil)
if err != nil {
t.Fatal(err)
}
@@ -268,7 +275,7 @@ func TestUpdateDNSServer(t *testing.T) {
t.Log(err)
}
}()
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "")
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", &peer.Status{})
if err != nil {
t.Fatal(err)
}
@@ -331,7 +338,8 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
return
}
wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.1/32", iface.DefaultMTU, nil, newNet)
privKey, _ := wgtypes.GeneratePrivateKey()
wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.1/32", 33100, privKey.String(), iface.DefaultMTU, newNet, nil)
if err != nil {
t.Errorf("build interface wireguard: %v", err)
return
@@ -368,7 +376,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
return
}
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "")
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", &peer.Status{})
if err != nil {
t.Errorf("create DNS server: %v", err)
return
@@ -463,7 +471,7 @@ func TestDNSServerStartStop(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort)
dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort, &peer.Status{})
if err != nil {
t.Fatalf("%v", err)
}
@@ -527,23 +535,24 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
registeredMap: make(registrationMap),
},
hostManager: hostManager,
currentConfig: hostDNSConfig{
domains: []domainConfig{
currentConfig: HostDNSConfig{
Domains: []DomainConfig{
{false, "domain0", false},
{false, "domain1", false},
{false, "domain2", false},
},
},
statusRecorder: &peer.Status{},
}
var domainsUpdate string
hostManager.applyDNSConfigFunc = func(config hostDNSConfig) error {
hostManager.applyDNSConfigFunc = func(config HostDNSConfig) error {
domains := []string{}
for _, item := range config.domains {
if item.disabled {
for _, item := range config.Domains {
if item.Disabled {
continue
}
domains = append(domains, item.domain)
domains = append(domains, item.Domain)
}
domainsUpdate = strings.Join(domains, ",")
return nil
@@ -556,14 +565,14 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
},
}, nil)
deactivate()
deactivate(nil)
expected := "domain0,domain2"
domains := []string{}
for _, item := range server.currentConfig.domains {
if item.disabled {
for _, item := range server.currentConfig.Domains {
if item.Disabled {
continue
}
domains = append(domains, item.domain)
domains = append(domains, item.Domain)
}
got := strings.Join(domains, ",")
if expected != got {
@@ -573,11 +582,11 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
reactivate()
expected = "domain0,domain1,domain2"
domains = []string{}
for _, item := range server.currentConfig.domains {
if item.disabled {
for _, item := range server.currentConfig.Domains {
if item.Disabled {
continue
}
domains = append(domains, item.domain)
domains = append(domains, item.Domain)
}
got = strings.Join(domains, ",")
if expected != got {
@@ -594,7 +603,7 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) {
var dnsList []string
dnsConfig := nbdns.Config{}
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil)
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil, &peer.Status{})
err = dnsServer.Initialize()
if err != nil {
t.Errorf("failed to initialize DNS server: %v", err)
@@ -618,7 +627,7 @@ func TestDNSPermanent_updateUpstream(t *testing.T) {
}
defer wgIFace.Close()
dnsConfig := nbdns.Config{}
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil)
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, &peer.Status{})
err = dnsServer.Initialize()
if err != nil {
t.Errorf("failed to initialize DNS server: %v", err)
@@ -710,7 +719,7 @@ func TestDNSPermanent_matchOnly(t *testing.T) {
}
defer wgIFace.Close()
dnsConfig := nbdns.Config{}
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil)
dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil, &peer.Status{})
err = dnsServer.Initialize()
if err != nil {
t.Errorf("failed to initialize DNS server: %v", err)
@@ -741,6 +750,11 @@ func TestDNSPermanent_matchOnly(t *testing.T) {
NSType: nbdns.UDPNameServerType,
Port: 53,
},
{
IP: netip.MustParseAddr("9.9.9.9"),
NSType: nbdns.UDPNameServerType,
Port: 53,
},
},
Domains: []string{"customdomain.com"},
Primary: false,
@@ -782,7 +796,8 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) {
return nil, err
}
wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.2/24", iface.DefaultMTU, nil, newNet)
privKey, _ := wgtypes.GeneratePrivateKey()
wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.2/24", 33100, privKey.String(), iface.DefaultMTU, newNet, nil)
if err != nil {
t.Fatalf("build interface wireguard: %v", err)
return nil, err

View File

@@ -0,0 +1,5 @@
package dns
func (s *DefaultServer) initialize() (manager hostManager, err error) {
return newHostManager(s.wgInterface)
}

View File

@@ -28,7 +28,7 @@ type serviceViaListener struct {
customAddr *netip.AddrPort
server *dns.Server
listenIP string
listenPort int
listenPort uint16
listenerIsRunning bool
listenerFlagLock sync.Mutex
ebpfService ebpfMgr.Manager
@@ -63,18 +63,9 @@ func (s *serviceViaListener) Listen() error {
s.listenIP, s.listenPort, err = s.evalListenAddress()
if err != nil {
log.Errorf("failed to eval runtime address: %s", err)
return err
return fmt.Errorf("eval listen address: %w", err)
}
s.server.Addr = fmt.Sprintf("%s:%d", s.listenIP, s.listenPort)
if s.shouldApplyPortFwd() {
s.ebpfService = ebpf.GetEbpfManagerInstance()
err = s.ebpfService.LoadDNSFwd(s.listenIP, s.listenPort)
if err != nil {
log.Warnf("failed to load DNS port forwarder, custom port may not work well on some Linux operating systems: %s", err)
s.ebpfService = nil
}
}
log.Debugf("starting dns on %s", s.server.Addr)
go func() {
s.setListenerStatus(true)
@@ -128,7 +119,7 @@ func (s *serviceViaListener) RuntimePort() int {
if s.ebpfService != nil {
return defaultPort
} else {
return s.listenPort
return int(s.listenPort)
}
}
@@ -140,54 +131,112 @@ func (s *serviceViaListener) setListenerStatus(running bool) {
s.listenerIsRunning = running
}
func (s *serviceViaListener) getFirstListenerAvailable() (string, int, error) {
ips := []string{defaultIP, customIP}
// evalListenAddress figure out the listen address for the DNS server
// first check the 53 port availability on WG interface or lo, if not success
// pick a random port on WG interface for eBPF, if not success
// check the 5053 port availability on WG interface or lo without eBPF usage,
func (s *serviceViaListener) evalListenAddress() (string, uint16, error) {
if s.customAddr != nil {
return s.customAddr.Addr().String(), s.customAddr.Port(), nil
}
ip, ok := s.testFreePort(defaultPort)
if ok {
return ip, defaultPort, nil
}
ebpfSrv, port, ok := s.tryToUseeBPF()
if ok {
s.ebpfService = ebpfSrv
return s.wgInterface.Address().IP.String(), port, nil
}
ip, ok = s.testFreePort(customPort)
if ok {
return ip, customPort, nil
}
return "", 0, fmt.Errorf("failed to find a free port for DNS server")
}
func (s *serviceViaListener) testFreePort(port int) (string, bool) {
var ips []string
if runtime.GOOS != "darwin" {
ips = append([]string{s.wgInterface.Address().IP.String()}, ips...)
ips = []string{s.wgInterface.Address().IP.String(), defaultIP, customIP}
} else {
ips = []string{defaultIP, customIP}
}
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)
for _, ip := range ips {
if !s.tryToBind(ip, port) {
continue
}
return ip, true
}
return "", 0, fmt.Errorf("unable to find an unused ip and port combination. IPs tested: %v and ports %v", ips, ports)
return "", false
}
func (s *serviceViaListener) evalListenAddress() (string, int, error) {
if s.customAddr != nil {
return s.customAddr.Addr().String(), int(s.customAddr.Port()), nil
}
return s.getFirstListenerAvailable()
}
// shouldApplyPortFwd decides whether to apply eBPF program to capture DNS traffic on port 53.
// This is needed because on some operating systems if we start a DNS server not on a default port 53, the domain name
// resolution won't work.
// So, in case we are running on Linux and picked a non-default port (53) we should fall back to the eBPF solution that will capture
// traffic on port 53 and forward it to a local DNS server running on 5053.
func (s *serviceViaListener) shouldApplyPortFwd() bool {
if runtime.GOOS != "linux" {
func (s *serviceViaListener) tryToBind(ip string, port int) bool {
addrString := fmt.Sprintf("%s:%d", ip, port)
udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(addrString))
probeListener, err := net.ListenUDP("udp", udpAddr)
if err != nil {
log.Warnf("binding dns on %s is not available, error: %s", addrString, err)
return false
}
if s.customAddr != nil {
return false
}
if s.listenPort == defaultPort {
return false
err = probeListener.Close()
if err != nil {
log.Errorf("got an error closing the probe listener, error: %s", err)
}
return true
}
// tryToUseeBPF decides whether to apply eBPF program to capture DNS traffic on port 53.
// This is needed because on some operating systems if we start a DNS server not on a default port 53,
// the domain name resolution won't work. So, in case we are running on Linux and picked a free
// port we should fall back to the eBPF solution that will capture traffic on port 53 and forward
// it to a local DNS server running on the chosen port.
func (s *serviceViaListener) tryToUseeBPF() (ebpfMgr.Manager, uint16, bool) {
if runtime.GOOS != "linux" {
return nil, 0, false
}
port, err := s.generateFreePort() //nolint:staticcheck,unused
if err != nil {
log.Warnf("failed to generate a free port for eBPF DNS forwarder server: %s", err)
return nil, 0, false
}
ebpfSrv := ebpf.GetEbpfManagerInstance()
err = ebpfSrv.LoadDNSFwd(s.wgInterface.Address().IP.String(), int(port))
if err != nil {
log.Warnf("failed to load DNS forwarder eBPF program, error: %s", err)
return nil, 0, false
}
return ebpfSrv, port, true
}
func (s *serviceViaListener) generateFreePort() (uint16, error) {
ok := s.tryToBind(s.wgInterface.Address().IP.String(), customPort)
if ok {
return customPort, nil
}
udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort("0.0.0.0:0"))
probeListener, err := net.ListenUDP("udp", udpAddr)
if err != nil {
log.Debugf("failed to bind random port for DNS: %s", err)
return 0, err
}
addrPort := netip.MustParseAddrPort(probeListener.LocalAddr().String()) // might panic if address is incorrect
err = probeListener.Close()
if err != nil {
log.Debugf("failed to free up DNS port: %s", err)
return 0, err
}
return addrPort.Port(), nil
}

View File

@@ -44,7 +44,7 @@ func (s *serviceViaMemory) Listen() error {
var err error
s.udpFilterHookID, err = s.filterDNSTraffic()
if err != nil {
return err
return fmt.Errorf("filter dns traffice: %w", err)
}
s.listenerIsRunning = true

View File

@@ -4,6 +4,7 @@ package dns
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
@@ -30,6 +31,8 @@ const (
systemdDbusSetDefaultRouteMethodSuffix = systemdDbusLinkInterface + ".SetDefaultRoute"
systemdDbusSetDomainsMethodSuffix = systemdDbusLinkInterface + ".SetDomains"
systemdDbusResolvConfModeForeign = "foreign"
dbusErrorUnknownObject = "org.freedesktop.DBus.Error.UnknownObject"
)
type systemdDbusConfigurator struct {
@@ -52,22 +55,22 @@ type systemdDbusLinkDomainsInput struct {
MatchOnly bool
}
func newSystemdDbusConfigurator(wgInterface WGIface) (hostManager, error) {
iface, err := net.InterfaceByName(wgInterface.Name())
func newSystemdDbusConfigurator(wgInterface string) (hostManager, error) {
iface, err := net.InterfaceByName(wgInterface)
if err != nil {
return nil, err
return nil, fmt.Errorf("get interface: %w", err)
}
obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode)
if err != nil {
return nil, err
return nil, fmt.Errorf("get dbus resolved dest: %w", err)
}
defer closeConn()
var s string
err = obj.Call(systemdDbusGetLinkMethod, dbusDefaultFlag, iface.Index).Store(&s)
if err != nil {
return nil, err
return nil, fmt.Errorf("get dbus link method: %w", err)
}
log.Debugf("got dbus Link interface: %s from net interface %s and index %d", s, iface.Name, iface.Index)
@@ -81,10 +84,10 @@ func (s *systemdDbusConfigurator) supportCustomPort() bool {
return true
}
func (s *systemdDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
parsedIP, err := netip.ParseAddr(config.serverIP)
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)
return fmt.Errorf("unable to parse ip address, error: %w", err)
}
ipAs4 := parsedIP.As4()
defaultLinkInput := systemdDbusDNSInput{
@@ -93,7 +96,7 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
}
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)
return fmt.Errorf("setting the interface DNS server %s:%d failed with error: %w", config.ServerIP, config.ServerPort, err)
}
var (
@@ -101,27 +104,27 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
matchDomains []string
domainsInput []systemdDbusLinkDomainsInput
)
for _, dConf := range config.domains {
if dConf.disabled {
for _, dConf := range config.Domains {
if dConf.Disabled {
continue
}
domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{
Domain: dns.Fqdn(dConf.domain),
MatchOnly: dConf.matchOnly,
Domain: dns.Fqdn(dConf.Domain),
MatchOnly: dConf.MatchOnly,
})
if dConf.matchOnly {
matchDomains = append(matchDomains, dConf.domain)
if dConf.MatchOnly {
matchDomains = append(matchDomains, dConf.Domain)
continue
}
searchDomains = append(searchDomains, dConf.domain)
searchDomains = append(searchDomains, dConf.Domain)
}
if config.routeAll {
log.Infof("configured %s:%d as main DNS forwarder for this peer", config.serverIP, config.serverPort)
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)
return fmt.Errorf("setting link as default dns router, failed with error: %w", err)
}
domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{
Domain: nbdns.RootZone,
@@ -129,7 +132,13 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
})
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("removing %s:%d as main DNS forwarder for this peer", config.ServerIP, config.ServerPort)
}
// create a backup for unclean shutdown detection before adding domains, as these might end up in the resolv.conf file.
// The file content itself is not important for systemd restoration
if err := createUncleanShutdownIndicator(defaultResolvConfPath, systemdManager, parsedIP.String()); err != nil {
log.Errorf("failed to create unclean shutdown resolv.conf backup: %s", err)
}
log.Infof("adding %d search domains and %d match domains. Search list: %s , Match list: %s", len(searchDomains), len(matchDomains), searchDomains, matchDomains)
@@ -143,7 +152,7 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
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 fmt.Errorf("setting domains configuration failed with error: %w", err)
}
return s.flushCaches()
}
@@ -153,17 +162,29 @@ func (s *systemdDbusConfigurator) restoreHostDNS() error {
if !isDbusListenerRunning(systemdResolvedDest, s.dbusLinkObject) {
return nil
}
// this call is required for DNS cleanup, even if it fails
err := s.callLinkMethod(systemdDbusRevertMethodSuffix, nil)
if err != nil {
return fmt.Errorf("unable to revert link configuration, got error: %s", err)
var dbusErr dbus.Error
if errors.As(err, &dbusErr) && dbusErr.Name == dbusErrorUnknownObject {
// interface is gone already
return nil
}
return fmt.Errorf("unable to revert link configuration, got error: %w", err)
}
if err := removeUncleanShutdownIndicator(); err != nil {
log.Errorf("failed to remove unclean shutdown resolv.conf backup: %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)
return fmt.Errorf("attempting to retrieve the object %s, err: %w", systemdDbusObjectNode, err)
}
defer closeConn()
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
@@ -171,7 +192,7 @@ func (s *systemdDbusConfigurator) flushCaches() error {
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 fmt.Errorf("calling the FlushCaches method with context, err: %w", err)
}
return nil
@@ -180,7 +201,7 @@ func (s *systemdDbusConfigurator) flushCaches() error {
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)
return fmt.Errorf("attempting to retrieve the object, err: %w", err)
}
defer closeConn()
@@ -194,22 +215,29 @@ func (s *systemdDbusConfigurator) callLinkMethod(method string, value any) error
}
if err != nil {
return fmt.Errorf("got error while calling command with context, err: %s", err)
return fmt.Errorf("calling command with context, err: %w", err)
}
return nil
}
func (s *systemdDbusConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error {
if err := s.restoreHostDNS(); err != nil {
return fmt.Errorf("restoring dns via systemd: %w", 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)
return fmt.Errorf("attempting to retrieve the systemd dns manager object, error: %w", err)
}
defer closeConn()
v, e := obj.GetProperty(property)
if e != nil {
return fmt.Errorf("got an error getting property %s: %v", property, e)
return fmt.Errorf("getting property %s: %w", property, e)
}
return v.Store(store)

View File

@@ -0,0 +1,5 @@
package dns
func CheckUncleanShutdown(string) error {
return nil
}

View File

@@ -0,0 +1,59 @@
//go:build !ios
package dns
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
log "github.com/sirupsen/logrus"
)
const fileUncleanShutdownFileLocation = "/var/lib/netbird/unclean_shutdown_dns"
func CheckUncleanShutdown(string) error {
if _, err := os.Stat(fileUncleanShutdownFileLocation); err != nil {
if errors.Is(err, fs.ErrNotExist) {
// no file -> clean shutdown
return nil
} else {
return fmt.Errorf("state: %w", err)
}
}
log.Warnf("detected unclean shutdown, file %s exists. Restoring unclean shutdown dns settings.", fileUncleanShutdownFileLocation)
manager, err := newHostManager()
if err != nil {
return fmt.Errorf("create host manager: %w", err)
}
if err := manager.restoreUncleanShutdownDNS(nil); err != nil {
return fmt.Errorf("restore unclean shutdown backup: %w", err)
}
return nil
}
func createUncleanShutdownIndicator() error {
dir := filepath.Dir(fileUncleanShutdownFileLocation)
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
return fmt.Errorf("create dir %s: %w", dir, err)
}
if err := os.WriteFile(fileUncleanShutdownFileLocation, nil, 0644); err != nil { //nolint:gosec
return fmt.Errorf("create %s: %w", fileUncleanShutdownFileLocation, err)
}
return nil
}
func removeUncleanShutdownIndicator() error {
if err := os.Remove(fileUncleanShutdownFileLocation); err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("remove %s: %w", fileUncleanShutdownFileLocation, err)
}
return nil
}

View File

@@ -0,0 +1,5 @@
package dns
func CheckUncleanShutdown(string) error {
return nil
}

View File

@@ -0,0 +1,96 @@
//go:build !android
package dns
import (
"errors"
"fmt"
"io/fs"
"net/netip"
"os"
"path/filepath"
"strings"
log "github.com/sirupsen/logrus"
)
const (
fileUncleanShutdownResolvConfLocation = "/var/lib/netbird/resolv.conf"
fileUncleanShutdownManagerTypeLocation = "/var/lib/netbird/manager"
)
func CheckUncleanShutdown(wgIface string) error {
if _, err := os.Stat(fileUncleanShutdownResolvConfLocation); err != nil {
if errors.Is(err, fs.ErrNotExist) {
// no file -> clean shutdown
return nil
} else {
return fmt.Errorf("state: %w", err)
}
}
log.Warnf("detected unclean shutdown, file %s exists", fileUncleanShutdownResolvConfLocation)
managerData, err := os.ReadFile(fileUncleanShutdownManagerTypeLocation)
if err != nil {
return fmt.Errorf("read %s: %w", fileUncleanShutdownManagerTypeLocation, err)
}
managerFields := strings.Split(string(managerData), ",")
if len(managerFields) < 2 {
return errors.New("split manager data: insufficient number of fields")
}
osManagerTypeStr, dnsAddressStr := managerFields[0], managerFields[1]
dnsAddress, err := netip.ParseAddr(dnsAddressStr)
if err != nil {
return fmt.Errorf("parse dns address %s failed: %w", dnsAddressStr, err)
}
log.Warnf("restoring unclean shutdown dns settings via previously detected manager: %s", osManagerTypeStr)
// determine os manager type, so we can invoke the respective restore action
osManagerType, err := newOsManagerType(osManagerTypeStr)
if err != nil {
return fmt.Errorf("detect previous host manager: %w", err)
}
manager, err := newHostManagerFromType(wgIface, osManagerType)
if err != nil {
return fmt.Errorf("create previous host manager: %w", err)
}
if err := manager.restoreUncleanShutdownDNS(&dnsAddress); err != nil {
return fmt.Errorf("restore unclean shutdown backup: %w", err)
}
return nil
}
func createUncleanShutdownIndicator(sourcePath string, managerType osManagerType, dnsAddress string) error {
dir := filepath.Dir(fileUncleanShutdownResolvConfLocation)
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
return fmt.Errorf("create dir %s: %w", dir, err)
}
if err := copyFile(sourcePath, fileUncleanShutdownResolvConfLocation); err != nil {
return fmt.Errorf("create %s: %w", sourcePath, err)
}
managerData := fmt.Sprintf("%s,%s", managerType, dnsAddress)
if err := os.WriteFile(fileUncleanShutdownManagerTypeLocation, []byte(managerData), 0644); err != nil { //nolint:gosec
return fmt.Errorf("create %s: %w", fileUncleanShutdownManagerTypeLocation, err)
}
return nil
}
func removeUncleanShutdownIndicator() error {
if err := os.Remove(fileUncleanShutdownResolvConfLocation); err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("remove %s: %w", fileUncleanShutdownResolvConfLocation, err)
}
if err := os.Remove(fileUncleanShutdownManagerTypeLocation); err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("remove %s: %w", fileUncleanShutdownManagerTypeLocation, err)
}
return nil
}

View File

@@ -0,0 +1,75 @@
package dns
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/sirupsen/logrus"
)
const (
netbirdProgramDataLocation = "Netbird"
fileUncleanShutdownFile = "unclean_shutdown_dns.txt"
)
func CheckUncleanShutdown(string) error {
file := getUncleanShutdownFile()
if _, err := os.Stat(file); err != nil {
if errors.Is(err, fs.ErrNotExist) {
// no file -> clean shutdown
return nil
} else {
return fmt.Errorf("state: %w", err)
}
}
logrus.Warnf("detected unclean shutdown, file %s exists. Restoring unclean shutdown dns settings.", file)
guid, err := os.ReadFile(file)
if err != nil {
return fmt.Errorf("read %s: %w", file, err)
}
manager, err := newHostManagerWithGuid(string(guid))
if err != nil {
return fmt.Errorf("create host manager: %w", err)
}
if err := manager.restoreUncleanShutdownDNS(nil); err != nil {
return fmt.Errorf("restore unclean shutdown backup: %w", err)
}
return nil
}
func createUncleanShutdownIndicator(guid string) error {
file := getUncleanShutdownFile()
dir := filepath.Dir(file)
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
return fmt.Errorf("create dir %s: %w", dir, err)
}
if err := os.WriteFile(file, []byte(guid), 0600); err != nil {
return fmt.Errorf("create %s: %w", file, err)
}
return nil
}
func removeUncleanShutdownIndicator() error {
file := getUncleanShutdownFile()
if err := os.Remove(file); err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("remove %s: %w", file, err)
}
return nil
}
func getUncleanShutdownFile() string {
return filepath.Join(os.Getenv("PROGRAMDATA"), netbirdProgramDataLocation, fileUncleanShutdownFile)
}

View File

@@ -5,26 +5,38 @@ import (
"errors"
"fmt"
"net"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/hashicorp/go-multierror"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/peer"
)
const (
failsTillDeact = int32(5)
reactivatePeriod = 30 * time.Second
upstreamTimeout = 15 * time.Second
probeTimeout = 2 * time.Second
)
const testRecord = "."
type upstreamClient interface {
ExchangeContext(ctx context.Context, m *dns.Msg, a string) (r *dns.Msg, rtt time.Duration, err error)
exchange(ctx context.Context, upstream string, r *dns.Msg) (*dns.Msg, time.Duration, error)
}
type upstreamResolver struct {
type UpstreamResolver interface {
serveDNS(r *dns.Msg) (*dns.Msg, time.Duration, error)
upstreamExchange(upstream string, r *dns.Msg) (*dns.Msg, time.Duration, error)
}
type upstreamResolverBase struct {
ctx context.Context
cancel context.CancelFunc
upstreamClient upstreamClient
@@ -36,30 +48,35 @@ type upstreamResolver struct {
reactivatePeriod time.Duration
upstreamTimeout time.Duration
deactivate func()
reactivate func()
deactivate func(error)
reactivate func()
statusRecorder *peer.Status
}
func newUpstreamResolver(parentCTX context.Context) *upstreamResolver {
ctx, cancel := context.WithCancel(parentCTX)
return &upstreamResolver{
func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status) *upstreamResolverBase {
ctx, cancel := context.WithCancel(ctx)
return &upstreamResolverBase{
ctx: ctx,
cancel: cancel,
upstreamClient: &dns.Client{},
upstreamTimeout: upstreamTimeout,
reactivatePeriod: reactivatePeriod,
failsTillDeact: failsTillDeact,
statusRecorder: statusRecorder,
}
}
func (u *upstreamResolver) stop() {
func (u *upstreamResolverBase) stop() {
log.Debugf("stopping serving DNS for upstreams %s", u.upstreamServers)
u.cancel()
}
// ServeDNS handles a DNS request
func (u *upstreamResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
defer u.checkUpstreamFails()
func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
var err error
defer func() {
u.checkUpstreamFails(err)
}()
log.WithField("question", r.Question[0]).Trace("received an upstream question")
@@ -70,20 +87,36 @@ func (u *upstreamResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
}
for _, upstream := range u.upstreamServers {
ctx, cancel := context.WithTimeout(u.ctx, u.upstreamTimeout)
rm, t, err := u.upstreamClient.ExchangeContext(ctx, r, upstream)
var rm *dns.Msg
var t time.Duration
cancel()
func() {
ctx, cancel := context.WithTimeout(u.ctx, u.upstreamTimeout)
defer cancel()
rm, t, err = u.upstreamClient.exchange(ctx, upstream, r)
}()
if err != nil {
if err == context.DeadlineExceeded || isTimeout(err) {
if errors.Is(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")
Error("got other error while querying the upstream")
return
}
if rm == nil {
log.WithError(err).WithField("upstream", upstream).
Warn("no response from upstream")
return
}
// those checks need to be independent of each other due to memory address issues
if !rm.Response {
log.WithError(err).WithField("upstream", upstream).
Warn("no response from upstream")
return
}
@@ -106,7 +139,7 @@ func (u *upstreamResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
// 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() {
func (u *upstreamResolverBase) checkUpstreamFails(err error) {
u.mutex.Lock()
defer u.mutex.Unlock()
@@ -118,15 +151,57 @@ func (u *upstreamResolver) checkUpstreamFails() {
case <-u.ctx.Done():
return
default:
log.Warnf("upstream resolving is disabled for %v", reactivatePeriod)
u.deactivate()
u.disabled = true
go u.waitUntilResponse()
}
u.disable(err)
}
// probeAvailability tests all upstream servers simultaneously and
// disables the resolver if none work
func (u *upstreamResolverBase) probeAvailability() {
u.mutex.Lock()
defer u.mutex.Unlock()
select {
case <-u.ctx.Done():
return
default:
}
var success bool
var mu sync.Mutex
var wg sync.WaitGroup
var errors *multierror.Error
for _, upstream := range u.upstreamServers {
upstream := upstream
wg.Add(1)
go func() {
defer wg.Done()
err := u.testNameserver(upstream)
if err != nil {
errors = multierror.Append(errors, err)
log.Warnf("probing upstream nameserver %s: %s", upstream, err)
return
}
mu.Lock()
defer mu.Unlock()
success = true
}()
}
wg.Wait()
// didn't find a working upstream server, let's disable and try later
if !success {
u.disable(errors.ErrorOrNil())
}
}
// waitUntilResponse retries, in an exponential interval, querying the upstream servers until it gets a positive response
func (u *upstreamResolver) waitUntilResponse() {
func (u *upstreamResolverBase) waitUntilResponse() {
exponentialBackOff := &backoff.ExponentialBackOff{
InitialInterval: 500 * time.Millisecond,
RandomizationFactor: 0.5,
@@ -137,8 +212,6 @@ func (u *upstreamResolver) waitUntilResponse() {
Clock: backoff.SystemClock,
}
r := new(dns.Msg).SetQuestion("netbird.io.", dns.TypeA)
operation := func() error {
select {
case <-u.ctx.Done():
@@ -146,20 +219,17 @@ func (u *upstreamResolver) waitUntilResponse() {
default:
}
var err error
for _, upstream := range u.upstreamServers {
ctx, cancel := context.WithTimeout(u.ctx, u.upstreamTimeout)
_, _, err = u.upstreamClient.ExchangeContext(ctx, r, upstream)
cancel()
if err == nil {
if err := u.testNameserver(upstream); err != nil {
log.Tracef("upstream check for %s: %s", upstream, err)
} else {
// at least one upstream server is available, stop probing
return nil
}
}
log.Tracef("checking connectivity with upstreams %s failed with error: %s. Retrying in %s", err, u.upstreamServers, exponentialBackOff.NextBackOff())
return fmt.Errorf("got an error from upstream check call")
log.Tracef("checking connectivity with upstreams %s failed. Retrying in %s", u.upstreamServers, exponentialBackOff.NextBackOff())
return fmt.Errorf("upstream check call error")
}
err := backoff.Retry(operation, exponentialBackOff)
@@ -184,3 +254,27 @@ func isTimeout(err error) bool {
}
return false
}
func (u *upstreamResolverBase) disable(err error) {
if u.disabled {
return
}
// todo test the deactivation logic, it seems to affect the client
if runtime.GOOS != "ios" {
log.Warnf("Upstream resolving is Disabled for %v", reactivatePeriod)
u.deactivate(err)
u.disabled = true
go u.waitUntilResponse()
}
}
func (u *upstreamResolverBase) testNameserver(server string) error {
ctx, cancel := context.WithTimeout(u.ctx, probeTimeout)
defer cancel()
r := new(dns.Msg).SetQuestion(testRecord, dns.TypeSOA)
_, _, err := u.upstreamClient.exchange(ctx, server, r)
return err
}

View File

@@ -0,0 +1,109 @@
//go:build ios
package dns
import (
"context"
"net"
"syscall"
"time"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
"github.com/netbirdio/netbird/client/internal/peer"
)
type upstreamResolverIOS struct {
*upstreamResolverBase
lIP net.IP
lNet *net.IPNet
iIndex int
}
func newUpstreamResolver(
ctx context.Context,
interfaceName string,
ip net.IP,
net *net.IPNet,
statusRecorder *peer.Status,
) (*upstreamResolverIOS, error) {
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder)
index, err := getInterfaceIndex(interfaceName)
if err != nil {
log.Debugf("unable to get interface index for %s: %s", interfaceName, err)
return nil, err
}
ios := &upstreamResolverIOS{
upstreamResolverBase: upstreamResolverBase,
lIP: ip,
lNet: net,
iIndex: index,
}
ios.upstreamClient = ios
return ios, nil
}
func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
client := &dns.Client{}
upstreamHost, _, err := net.SplitHostPort(upstream)
if err != nil {
log.Errorf("error while parsing upstream host: %s", err)
}
timeout := upstreamTimeout
if deadline, ok := ctx.Deadline(); ok {
timeout = time.Until(deadline)
}
client.DialTimeout = timeout
upstreamIP := net.ParseIP(upstreamHost)
if u.lNet.Contains(upstreamIP) || net.IP.IsPrivate(upstreamIP) {
log.Debugf("using private client to query upstream: %s", upstream)
client = u.getClientPrivate(timeout)
}
// Cannot use client.ExchangeContext because it overwrites our Dialer
return client.Exchange(r, upstream)
}
// getClientPrivate returns a new DNS client bound to the local IP address of the Netbird interface
// This method is needed for iOS
func (u *upstreamResolverIOS) getClientPrivate(dialTimeout time.Duration) *dns.Client {
dialer := &net.Dialer{
LocalAddr: &net.UDPAddr{
IP: u.lIP,
Port: 0, // Let the OS pick a free port
},
Timeout: dialTimeout,
Control: func(network, address string, c syscall.RawConn) error {
var operr error
fn := func(s uintptr) {
operr = unix.SetsockoptInt(int(s), unix.IPPROTO_IP, unix.IP_BOUND_IF, u.iIndex)
}
if err := c.Control(fn); err != nil {
return err
}
if operr != nil {
log.Errorf("error while setting socket option: %s", operr)
}
return operr
},
}
client := &dns.Client{
Dialer: dialer,
}
return client
}
func getInterfaceIndex(interfaceName string) (int, error) {
iface, err := net.InterfaceByName(interfaceName)
return iface.Index, err
}

View File

@@ -0,0 +1,37 @@
//go:build !ios
package dns
import (
"context"
"net"
"time"
"github.com/miekg/dns"
"github.com/netbirdio/netbird/client/internal/peer"
)
type upstreamResolverNonIOS struct {
*upstreamResolverBase
}
func newUpstreamResolver(
ctx context.Context,
_ string,
_ net.IP,
_ *net.IPNet,
statusRecorder *peer.Status,
) (*upstreamResolverNonIOS, error) {
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder)
nonIOS := &upstreamResolverNonIOS{
upstreamResolverBase: upstreamResolverBase,
}
upstreamResolverBase.upstreamClient = nonIOS
return nonIOS, nil
}
func (u *upstreamResolverNonIOS) exchange(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
upstreamExchangeClient := &dns.Client{}
return upstreamExchangeClient.ExchangeContext(ctx, r, upstream)
}

View File

@@ -2,6 +2,7 @@ package dns
import (
"context"
"net"
"strings"
"testing"
"time"
@@ -49,15 +50,6 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
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
@@ -66,7 +58,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.TODO())
resolver := newUpstreamResolver(ctx)
resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil)
resolver.upstreamServers = testCase.InputServers
resolver.upstreamTimeout = testCase.timeout
if testCase.cancelCTX {
@@ -113,13 +105,13 @@ type mockUpstreamResolver struct {
err error
}
// ExchangeContext mock implementation of ExchangeContext from upstreamResolver
func (c mockUpstreamResolver) ExchangeContext(_ context.Context, _ *dns.Msg, _ string) (r *dns.Msg, rtt time.Duration, err error) {
// exchange mock implementation of exchange from upstreamResolver
func (c mockUpstreamResolver) exchange(_ context.Context, _ string, _ *dns.Msg) (*dns.Msg, time.Duration, error) {
return c.r, c.rtt, c.err
}
func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
resolver := &upstreamResolver{
resolver := &upstreamResolverBase{
ctx: context.TODO(),
upstreamClient: &mockUpstreamResolver{
err: nil,
@@ -139,7 +131,7 @@ func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
}
failed := false
resolver.deactivate = func() {
resolver.deactivate = func(error) {
failed = true
}
@@ -156,7 +148,7 @@ func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
}
if !resolver.disabled {
t.Errorf("resolver should be disabled")
t.Errorf("resolver should be Disabled")
return
}

View File

@@ -11,4 +11,5 @@ type WGIface interface {
IsUserspaceBind() bool
GetFilter() iface.PacketFilter
GetDevice() *iface.DeviceWrapper
GetStats(peerKey string) (iface.WGStats, error)
}

View File

@@ -9,5 +9,6 @@ type WGIface interface {
IsUserspaceBind() bool
GetFilter() iface.PacketFilter
GetDevice() *iface.DeviceWrapper
GetStats(peerKey string) (iface.WGStats, error)
GetInterfaceGUIDString() (string, error)
}

View File

@@ -1,6 +1,5 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
package ebpf

View File

@@ -1,6 +1,5 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
//go:build 386 || amd64 || amd64p32 || arm || arm64 || loong64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
package ebpf

View File

@@ -13,7 +13,7 @@ const (
)
func (tf *GeneralManager) LoadDNSFwd(ip string, dnsPort int) error {
log.Debugf("load ebpf DNS forwarder: address: %s:%d", ip, dnsPort)
log.Debugf("load eBPF DNS forwarder, watching addr: %s:53, redirect to port: %d", ip, dnsPort)
tf.lock.Lock()
defer tf.lock.Unlock()

View File

@@ -46,8 +46,8 @@ int xdp_dns_fwd(struct iphdr *ip, struct udphdr *udp) {
if(!read_settings()){
return XDP_PASS;
}
bpf_printk("dns port: %d", ntohs(dns_port));
bpf_printk("dns ip: %d", ntohl(dns_ip));
// bpf_printk("dns port: %d", ntohs(dns_port));
// bpf_printk("dns ip: %d", ntohl(dns_ip));
}
if (udp->dest == GENERAL_DNS_PORT && ip->daddr == dns_ip) {
@@ -61,4 +61,4 @@ int xdp_dns_fwd(struct iphdr *ip, struct udphdr *udp) {
}
return XDP_PASS;
}
}

View File

@@ -8,12 +8,6 @@
#include "dns_fwd.c"
#include "wg_proxy.c"
#define bpf_printk(fmt, ...) \
({ \
char ____fmt[] = fmt; \
bpf_trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \
})
const __u16 flag_feature_wg_proxy = 0b01;
const __u16 flag_feature_dns_fwd = 0b10;
@@ -63,4 +57,4 @@ int nb_xdp_prog(struct xdp_md *ctx) {
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
char _license[] SEC("license") = "GPL";

View File

@@ -0,0 +1,17 @@
# DNS forwarder
The agent attach the XDP program to the lo device. We can not use fake address in eBPF because the
traffic does not appear in the eBPF program. The program capture the traffic on wg_ip:53 and
overwrite in it the destination port to 5053.
# Debug
The CONFIG_BPF_EVENTS kernel module is required for bpf_printk.
Apply this code to use bpf_printk
```
#define bpf_printk(fmt, ...) \
({ \
char ____fmt[] = fmt; \
bpf_trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \
})
```

View File

@@ -34,7 +34,7 @@ int xdp_wg_proxy(struct iphdr *ip, struct udphdr *udp) {
if (!read_port_settings()){
return XDP_PASS;
}
bpf_printk("proxy port: %d, wg port: %d", proxy_port, wg_port);
// bpf_printk("proxy port: %d, wg port: %d", proxy_port, wg_port);
}
// 2130706433 = 127.0.0.1
@@ -51,4 +51,4 @@ int xdp_wg_proxy(struct iphdr *ip, struct udphdr *udp) {
udp->dest = new_dst_port;
udp->source = new_src_port;
return XDP_PASS;
}
}

View File

@@ -3,7 +3,6 @@ package internal
import (
"context"
"fmt"
"io"
"math/rand"
"net"
"net/netip"
@@ -13,7 +12,8 @@ import (
"sync"
"time"
"github.com/pion/ice/v2"
"github.com/pion/ice/v3"
"github.com/pion/stun/v2"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@@ -22,6 +22,8 @@ import (
"github.com/netbirdio/netbird/client/internal/acl"
"github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/relay"
"github.com/netbirdio/netbird/client/internal/rosenpass"
"github.com/netbirdio/netbird/client/internal/routemanager"
"github.com/netbirdio/netbird/client/internal/wgproxy"
nbssh "github.com/netbirdio/netbird/client/ssh"
@@ -31,7 +33,6 @@ import (
mgm "github.com/netbirdio/netbird/management/client"
mgmProto "github.com/netbirdio/netbird/management/proto"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/sharedsock"
signal "github.com/netbirdio/netbird/signal/client"
sProto "github.com/netbirdio/netbird/signal/proto"
"github.com/netbirdio/netbird/util"
@@ -77,6 +78,11 @@ type EngineConfig struct {
NATExternalIPs []string
CustomDNSAddress string
RosenpassEnabled bool
RosenpassPermissive bool
ServerSSHAllowed bool
}
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
@@ -87,6 +93,8 @@ type Engine struct {
mgmClient mgm.Client
// peerConns is a map that holds all the peers that are known to this peer
peerConns map[string]*peer.Conn
// rpManager is a Rosenpass manager
rpManager *rosenpass.Manager
// syncMsgMux is used to guarantee sequential Management Service message processing
syncMsgMux *sync.Mutex
@@ -95,9 +103,9 @@ type Engine struct {
mobileDep MobileDependency
// STUNs is a list of STUN servers used by ICE
STUNs []*ice.URL
STUNs []*stun.URI
// TURNs is a list of STUN servers used by ICE
TURNs []*ice.URL
TURNs []*stun.URI
cancel context.CancelFunc
@@ -106,8 +114,7 @@ type Engine struct {
wgInterface *iface.WGIface
wgProxyFactory *wgproxy.Factory
udpMux *bind.UniversalUDPMuxDefault
udpMuxConn io.Closer
udpMux *bind.UniversalUDPMuxDefault
// networkSerial is the latest CurrentSerial (state ID) of the network sent by the Management service
networkSerial uint64
@@ -122,6 +129,11 @@ type Engine struct {
acl acl.Manager
dnsServer dns.Server
mgmProbe *Probe
signalProbe *Probe
relayProbe *Probe
wgProbe *Probe
}
// Peer is an instance of the Connection Peer
@@ -132,11 +144,43 @@ type Peer struct {
// NewEngine creates a new Connection Engine
func NewEngine(
ctx context.Context, cancel context.CancelFunc,
signalClient signal.Client, mgmClient mgm.Client,
config *EngineConfig, mobileDep MobileDependency, statusRecorder *peer.Status,
ctx context.Context,
cancel context.CancelFunc,
signalClient signal.Client,
mgmClient mgm.Client,
config *EngineConfig,
mobileDep MobileDependency,
statusRecorder *peer.Status,
) *Engine {
return NewEngineWithProbes(
ctx,
cancel,
signalClient,
mgmClient,
config,
mobileDep,
statusRecorder,
nil,
nil,
nil,
nil,
)
}
// NewEngineWithProbes creates a new Connection Engine with probes attached
func NewEngineWithProbes(
ctx context.Context,
cancel context.CancelFunc,
signalClient signal.Client,
mgmClient mgm.Client,
config *EngineConfig,
mobileDep MobileDependency,
statusRecorder *peer.Status,
mgmProbe *Probe,
signalProbe *Probe,
relayProbe *Probe,
wgProbe *Probe,
) *Engine {
return &Engine{
ctx: ctx,
cancel: cancel,
@@ -146,12 +190,16 @@ func NewEngine(
syncMsgMux: &sync.Mutex{},
config: config,
mobileDep: mobileDep,
STUNs: []*ice.URL{},
TURNs: []*ice.URL{},
STUNs: []*stun.URI{},
TURNs: []*stun.URI{},
networkSerial: 0,
sshServerFunc: nbssh.DefaultSSHServer,
statusRecorder: statusRecorder,
wgProxyFactory: wgproxy.NewFactory(config.WgPort),
mgmProbe: mgmProbe,
signalProbe: signalProbe,
relayProbe: relayProbe,
wgProbe: wgProbe,
}
}
@@ -180,58 +228,48 @@ func (e *Engine) Start() error {
e.syncMsgMux.Lock()
defer e.syncMsgMux.Unlock()
wgIFaceName := e.config.WgIfaceName
wgAddr := e.config.WgAddr
myPrivateKey := e.config.WgPrivateKey
var err error
transportNet, err := e.newStdNet()
wgIface, err := e.newWgIface()
if err != nil {
log.Errorf("failed to create pion's stdnet: %s", err)
log.Errorf("failed creating wireguard interface instance %s: [%s]", e.config.WgIfaceName, err)
return fmt.Errorf("new wg interface: %w", err)
}
e.wgInterface = wgIface
if e.config.RosenpassEnabled {
log.Infof("rosenpass is enabled")
if e.config.RosenpassPermissive {
log.Infof("running rosenpass in permissive mode")
} else {
log.Infof("running rosenpass in strict mode")
}
e.rpManager, err = rosenpass.NewManager(e.config.PreSharedKey, e.config.WgIfaceName)
if err != nil {
return fmt.Errorf("create rosenpass manager: %w", err)
}
err := e.rpManager.Run()
if err != nil {
return fmt.Errorf("run rosenpass manager: %w", err)
}
}
e.wgInterface, err = iface.NewWGIFace(wgIFaceName, wgAddr, iface.DefaultMTU, e.mobileDep.TunAdapter, transportNet)
initialRoutes, dnsServer, err := e.newDnsServer()
if err != nil {
log.Errorf("failed creating wireguard interface instance %s: [%s]", wgIFaceName, err.Error())
return err
e.close()
return fmt.Errorf("create dns server: %w", err)
}
e.dnsServer = dnsServer
var routes []*route.Route
if runtime.GOOS == "android" {
var dnsConfig *nbdns.Config
routes, dnsConfig, err = e.readInitialSettings()
if err != nil {
return err
}
if e.dnsServer == nil {
e.dnsServer = dns.NewDefaultServerPermanentUpstream(e.ctx, e.wgInterface, e.mobileDep.HostDNSAddresses, *dnsConfig, e.mobileDep.NetworkChangeListener)
go e.mobileDep.DnsReadyListener.OnReady()
}
} else if e.dnsServer == nil {
// todo fix custom address
e.dnsServer, err = dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress)
if err != nil {
e.close()
return err
}
e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder, initialRoutes)
if err := e.routeManager.Init(); err != nil {
log.Errorf("Failed to initialize route manager: %s", err)
}
e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder, routes)
e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener)
if runtime.GOOS == "android" {
err = e.wgInterface.CreateOnMobile(iface.MobileIFaceArguments{
Routes: e.routeManager.InitialRouteRange(),
Dns: e.dnsServer.DnsIP(),
SearchDomains: e.dnsServer.SearchDomains(),
})
} else {
err = e.wgInterface.Create()
}
err = e.wgInterfaceCreate()
if err != nil {
log.Errorf("failed creating tunnel interface %s: [%s]", wgIFaceName, err.Error())
log.Errorf("failed creating tunnel interface %s: [%s]", e.config.WgIfaceName, err.Error())
e.close()
return err
return fmt.Errorf("create wg interface: %w", err)
}
e.firewall, err = firewall.NewFirewall(e.ctx, e.wgInterface)
@@ -243,35 +281,15 @@ func (e *Engine) Start() error {
err = e.routeManager.EnableServerRouter(e.firewall)
if err != nil {
e.close()
return err
return fmt.Errorf("enable server router: %w", err)
}
}
err = e.wgInterface.Configure(myPrivateKey.String(), e.config.WgPort)
e.udpMux, err = e.wgInterface.Up()
if err != nil {
log.Errorf("failed configuring Wireguard interface [%s]: %s", wgIFaceName, err.Error())
log.Errorf("failed to pull up wgInterface [%s]: %s", e.wgInterface.Name(), err.Error())
e.close()
return err
}
if e.wgInterface.IsUserspaceBind() {
iceBind := e.wgInterface.GetBind()
udpMux, err := iceBind.GetICEMux()
if err != nil {
e.close()
return err
}
e.udpMux = udpMux
log.Infof("using userspace bind mode %s", udpMux.LocalAddr().String())
} else {
rawSock, err := sharedsock.Listen(e.config.WgPort, sharedsock.NewIncomingSTUNFilter())
if err != nil {
return err
}
mux := bind.NewUniversalUDPMuxDefault(bind.UniversalUDPMuxParams{UDPConn: rawSock, Net: transportNet})
go mux.ReadFromConn(e.ctx)
e.udpMuxConn = rawSock
e.udpMux = mux
return fmt.Errorf("up wg interface: %w", err)
}
if e.firewall != nil {
@@ -281,11 +299,12 @@ func (e *Engine) Start() error {
err = e.dnsServer.Initialize()
if err != nil {
e.close()
return err
return fmt.Errorf("initialize dns server: %w", err)
}
e.receiveSignalEvents()
e.receiveManagementEvents()
e.receiveProbeEvents()
return nil
}
@@ -415,7 +434,8 @@ func sendSignal(message *sProto.Message, s signal.Client) error {
}
// SignalOfferAnswer signals either an offer or an answer to remote peer
func SignalOfferAnswer(offerAnswer peer.OfferAnswer, myKey wgtypes.Key, remoteKey wgtypes.Key, s signal.Client, isAnswer bool) error {
func SignalOfferAnswer(offerAnswer peer.OfferAnswer, myKey wgtypes.Key, remoteKey wgtypes.Key, s signal.Client,
isAnswer bool) error {
var t sProto.Body_Type
if isAnswer {
t = sProto.Body_ANSWER
@@ -426,7 +446,7 @@ func SignalOfferAnswer(offerAnswer peer.OfferAnswer, myKey wgtypes.Key, remoteKe
msg, err := signal.MarshalCredential(myKey, offerAnswer.WgListenPort, remoteKey, &signal.Credential{
UFrag: offerAnswer.IceCredentials.UFrag,
Pwd: offerAnswer.IceCredentials.Pwd,
}, t)
}, t, offerAnswer.RosenpassPubKey, offerAnswer.RosenpassAddr)
if err != nil {
return err
}
@@ -473,44 +493,52 @@ func isNil(server nbssh.Server) bool {
}
func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error {
if sshConf.GetSshEnabled() {
if runtime.GOOS == "windows" {
log.Warnf("running SSH server on Windows is not supported")
return nil
}
// start SSH server if it wasn't running
if isNil(e.sshServer) {
//nil sshServer means it has not yet been started
var err error
e.sshServer, err = e.sshServerFunc(e.config.SSHKey,
fmt.Sprintf("%s:%d", e.wgInterface.Address().IP.String(), nbssh.DefaultSSHPort))
if err != nil {
return err
if !e.config.ServerSSHAllowed {
log.Warnf("running SSH server is not permitted")
return nil
} else {
if sshConf.GetSshEnabled() {
if runtime.GOOS == "windows" {
log.Warnf("running SSH server on Windows is not supported")
return nil
}
go func() {
// blocking
err = e.sshServer.Start()
// start SSH server if it wasn't running
if isNil(e.sshServer) {
// nil sshServer means it has not yet been started
var err error
e.sshServer, err = e.sshServerFunc(e.config.SSHKey,
fmt.Sprintf("%s:%d", e.wgInterface.Address().IP.String(), nbssh.DefaultSSHPort))
if err != nil {
// will throw error when we stop it even if it is a graceful stop
log.Debugf("stopped SSH server with error %v", err)
return err
}
e.syncMsgMux.Lock()
defer e.syncMsgMux.Unlock()
e.sshServer = nil
log.Infof("stopped SSH server")
}()
} else {
log.Debugf("SSH server is already running")
go func() {
// blocking
err = e.sshServer.Start()
if err != nil {
// will throw error when we stop it even if it is a graceful stop
log.Debugf("stopped SSH server with error %v", err)
}
e.syncMsgMux.Lock()
defer e.syncMsgMux.Unlock()
e.sshServer = nil
log.Infof("stopped SSH server")
}()
} else {
log.Debugf("SSH server is already running")
}
} else if !isNil(e.sshServer) {
// Disable SSH server request, so stop it if it was running
err := e.sshServer.Stop()
if err != nil {
log.Warnf("failed to stop SSH server %v", err)
}
e.sshServer = nil
}
} else if !isNil(e.sshServer) {
// Disable SSH server request, so stop it if it was running
err := e.sshServer.Stop()
if err != nil {
log.Warnf("failed to stop SSH server %v", err)
}
e.sshServer = nil
return nil
}
return nil
}
func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
@@ -528,7 +556,7 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
if conf.GetSshConfig() != nil {
err := e.updateSSH(conf.GetSshConfig())
if err != nil {
log.Warnf("failed handling SSH server setup %v", e)
log.Warnf("failed handling SSH server setup %v", err)
}
}
@@ -546,9 +574,7 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
// E.g. when a new peer has been registered and we are allowed to connect to it.
func (e *Engine) receiveManagementEvents() {
go func() {
err := e.mgmClient.Sync(func(update *mgmProto.SyncResponse) error {
return e.handleSync(update)
})
err := e.mgmClient.Sync(e.handleSync)
if err != nil {
// happens if management is unavailable for a long time.
// We want to cancel the operation of the whole client
@@ -565,10 +591,10 @@ func (e *Engine) updateSTUNs(stuns []*mgmProto.HostConfig) error {
if len(stuns) == 0 {
return nil
}
var newSTUNs []*ice.URL
var newSTUNs []*stun.URI
log.Debugf("got STUNs update from Management Service, updating")
for _, stun := range stuns {
url, err := ice.ParseURL(stun.Uri)
for _, s := range stuns {
url, err := stun.ParseURI(s.Uri)
if err != nil {
return err
}
@@ -583,10 +609,10 @@ func (e *Engine) updateTURNs(turns []*mgmProto.ProtectedHostConfig) error {
if len(turns) == 0 {
return nil
}
var newTURNs []*ice.URL
var newTURNs []*stun.URI
log.Debugf("got TURNs update from Management Service, updating")
for _, turn := range turns {
url, err := ice.ParseURL(turn.HostConfig.Uri)
url, err := stun.ParseURI(turn.HostConfig.Uri)
if err != nil {
return err
}
@@ -678,7 +704,13 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
if e.acl != nil {
e.acl.ApplyFiltering(networkMap)
}
e.networkSerial = serial
// Test received (upstream) servers for availability right away instead of upon usage.
// If no server of a server group responds this will disable the respective handler and retry later.
e.dnsServer.ProbeAvailability()
return nil
}
@@ -836,7 +868,7 @@ func (e *Engine) peerExists(peerKey string) bool {
func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, error) {
log.Debugf("creating peer connection %s", pubKey)
var stunTurn []*ice.URL
var stunTurn []*stun.URI
stunTurn = append(stunTurn, e.STUNs...)
stunTurn = append(stunTurn, e.TURNs...)
@@ -848,6 +880,26 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, e
PreSharedKey: e.config.PreSharedKey,
}
if e.config.RosenpassEnabled && !e.config.RosenpassPermissive {
lk := []byte(e.config.WgPrivateKey.PublicKey().String())
rk := []byte(wgConfig.RemoteKey)
var keyInput []byte
if string(lk) > string(rk) {
//nolint:gocritic
keyInput = append(lk[:16], rk[:16]...)
} else {
//nolint:gocritic
keyInput = append(rk[:16], lk[:16]...)
}
key, err := wgtypes.NewKey(keyInput)
if err != nil {
return nil, err
}
wgConfig.PreSharedKey = &key
}
// randomize connection timeout
timeout := time.Duration(rand.Intn(PeerConnectionTimeoutMax-PeerConnectionTimeoutMin)+PeerConnectionTimeoutMin) * time.Millisecond
config := peer.ConnConfig{
@@ -863,6 +915,8 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, e
LocalWgPort: e.config.WgPort,
NATExternalIPs: e.parseNATExternalIPMappings(),
UserspaceBind: e.wgInterface.IsUserspaceBind(),
RosenpassPubKey: e.getRosenpassPubKey(),
RosenpassAddr: e.getRosenpassAddr(),
}
peerConn, err := peer.NewConn(config, e.statusRecorder, e.wgProxyFactory, e.mobileDep.TunAdapter, e.mobileDep.IFaceDiscover)
@@ -894,6 +948,12 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, e
return sendSignal(message, e.signal)
})
if e.rpManager != nil {
peerConn.SetOnConnected(e.rpManager.OnConnected)
peerConn.SetOnDisconnected(e.rpManager.OnDisconnected)
}
return peerConn, nil
}
@@ -919,13 +979,21 @@ func (e *Engine) receiveSignalEvents() {
conn.RegisterProtoSupportMeta(msg.Body.GetFeaturesSupported())
var rosenpassPubKey []byte
rosenpassAddr := ""
if msg.GetBody().GetRosenpassConfig() != nil {
rosenpassPubKey = msg.GetBody().GetRosenpassConfig().GetRosenpassPubKey()
rosenpassAddr = msg.GetBody().GetRosenpassConfig().GetRosenpassServerAddr()
}
conn.OnRemoteOffer(peer.OfferAnswer{
IceCredentials: peer.IceCredentials{
UFrag: remoteCred.UFrag,
Pwd: remoteCred.Pwd,
},
WgListenPort: int(msg.GetBody().GetWgListenPort()),
Version: msg.GetBody().GetNetBirdVersion(),
WgListenPort: int(msg.GetBody().GetWgListenPort()),
Version: msg.GetBody().GetNetBirdVersion(),
RosenpassPubKey: rosenpassPubKey,
RosenpassAddr: rosenpassAddr,
})
case sProto.Body_ANSWER:
remoteCred, err := signal.UnMarshalCredential(msg)
@@ -933,15 +1001,23 @@ func (e *Engine) receiveSignalEvents() {
return err
}
conn.RegisterProtoSupportMeta(msg.Body.GetFeaturesSupported())
conn.RegisterProtoSupportMeta(msg.GetBody().GetFeaturesSupported())
var rosenpassPubKey []byte
rosenpassAddr := ""
if msg.GetBody().GetRosenpassConfig() != nil {
rosenpassPubKey = msg.GetBody().GetRosenpassConfig().GetRosenpassPubKey()
rosenpassAddr = msg.GetBody().GetRosenpassConfig().GetRosenpassServerAddr()
}
conn.OnRemoteAnswer(peer.OfferAnswer{
IceCredentials: peer.IceCredentials{
UFrag: remoteCred.UFrag,
Pwd: remoteCred.Pwd,
},
WgListenPort: int(msg.GetBody().GetWgListenPort()),
Version: msg.GetBody().GetNetBirdVersion(),
WgListenPort: int(msg.GetBody().GetWgListenPort()),
Version: msg.GetBody().GetNetBirdVersion(),
RosenpassPubKey: rosenpassPubKey,
RosenpassAddr: rosenpassAddr,
})
case sProto.Body_CANDIDATE:
candidate, err := ice.UnmarshalCandidate(msg.GetBody().Payload)
@@ -1024,6 +1100,11 @@ func (e *Engine) close() {
log.Errorf("failed closing ebpf proxy: %s", err)
}
// stop/restore DNS first so dbus and friends don't complain because of a missing interface
if e.dnsServer != nil {
e.dnsServer.Stop()
}
log.Debugf("removing Netbird interface %s", e.config.WgIfaceName)
if e.wgInterface != nil {
if err := e.wgInterface.Close(); err != nil {
@@ -1031,18 +1112,6 @@ func (e *Engine) close() {
}
}
if e.udpMux != nil {
if err := e.udpMux.Close(); err != nil {
log.Debugf("close udp mux: %v", err)
}
}
if e.udpMuxConn != nil {
if err := e.udpMuxConn.Close(); err != nil {
log.Debugf("close udp mux connection: %v", err)
}
}
if !isNil(e.sshServer) {
err := e.sshServer.Stop()
if err != nil {
@@ -1054,16 +1123,16 @@ func (e *Engine) close() {
e.routeManager.Stop()
}
if e.dnsServer != nil {
e.dnsServer.Stop()
}
if e.firewall != nil {
err := e.firewall.Reset()
if err != nil {
log.Warnf("failed to reset firewall: %s", err)
}
}
if e.rpManager != nil {
_ = e.rpManager.Close()
}
}
func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) {
@@ -1076,6 +1145,75 @@ func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) {
return routes, &dnsCfg, nil
}
func (e *Engine) newWgIface() (*iface.WGIface, error) {
transportNet, err := e.newStdNet()
if err != nil {
log.Errorf("failed to create pion's stdnet: %s", err)
}
var mArgs *iface.MobileIFaceArguments
switch runtime.GOOS {
case "android":
mArgs = &iface.MobileIFaceArguments{
TunAdapter: e.mobileDep.TunAdapter,
TunFd: int(e.mobileDep.FileDescriptor),
}
case "ios":
mArgs = &iface.MobileIFaceArguments{
TunFd: int(e.mobileDep.FileDescriptor),
}
default:
}
return iface.NewWGIFace(e.config.WgIfaceName, e.config.WgAddr, e.config.WgPort, e.config.WgPrivateKey.String(), iface.DefaultMTU, transportNet, mArgs)
}
func (e *Engine) wgInterfaceCreate() (err error) {
switch runtime.GOOS {
case "android":
err = e.wgInterface.CreateOnAndroid(e.routeManager.InitialRouteRange(), e.dnsServer.DnsIP(), e.dnsServer.SearchDomains())
case "ios":
e.mobileDep.NetworkChangeListener.SetInterfaceIP(e.config.WgAddr)
err = e.wgInterface.Create()
default:
err = e.wgInterface.Create()
}
return err
}
func (e *Engine) newDnsServer() ([]*route.Route, dns.Server, error) {
// due to tests where we are using a mocked version of the DNS server
if e.dnsServer != nil {
return nil, e.dnsServer, nil
}
switch runtime.GOOS {
case "android":
routes, dnsConfig, err := e.readInitialSettings()
if err != nil {
return nil, nil, err
}
dnsServer := dns.NewDefaultServerPermanentUpstream(
e.ctx,
e.wgInterface,
e.mobileDep.HostDNSAddresses,
*dnsConfig,
e.mobileDep.NetworkChangeListener,
e.statusRecorder,
)
go e.mobileDep.DnsReadyListener.OnReady()
return routes, dnsServer, nil
case "ios":
dnsServer := dns.NewDefaultServerIos(e.ctx, e.wgInterface, e.mobileDep.DnsManager, e.statusRecorder)
return nil, dnsServer, nil
default:
dnsServer, err := dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress, e.statusRecorder)
if err != nil {
return nil, nil, err
}
return nil, dnsServer, nil
}
}
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
@@ -1096,3 +1234,83 @@ func findIPFromInterface(iface *net.Interface) (net.IP, error) {
}
return nil, fmt.Errorf("interface %s don't have an ipv4 address", iface.Name)
}
func (e *Engine) getRosenpassPubKey() []byte {
if e.rpManager != nil {
return e.rpManager.GetPubKey()
}
return nil
}
func (e *Engine) getRosenpassAddr() string {
if e.rpManager != nil {
return e.rpManager.GetAddress().String()
}
return ""
}
func (e *Engine) receiveProbeEvents() {
if e.signalProbe != nil {
go e.signalProbe.Receive(e.ctx, func() bool {
healthy := e.signal.IsHealthy()
log.Debugf("received signal probe request, healthy: %t", healthy)
return healthy
})
}
if e.mgmProbe != nil {
go e.mgmProbe.Receive(e.ctx, func() bool {
healthy := e.mgmClient.IsHealthy()
log.Debugf("received management probe request, healthy: %t", healthy)
return healthy
})
}
if e.relayProbe != nil {
go e.relayProbe.Receive(e.ctx, func() bool {
healthy := true
results := append(e.probeSTUNs(), e.probeTURNs()...)
e.statusRecorder.UpdateRelayStates(results)
// A single failed server will result in a "failed" probe
for _, res := range results {
if res.Err != nil {
healthy = false
break
}
}
log.Debugf("received relay probe request, healthy: %t", healthy)
return healthy
})
}
if e.wgProbe != nil {
go e.wgProbe.Receive(e.ctx, func() bool {
log.Debug("received wg probe request")
for _, peer := range e.peerConns {
key := peer.GetKey()
wgStats, err := peer.GetConf().WgConfig.WgInterface.GetStats(key)
if err != nil {
log.Debugf("failed to get wg stats for peer %s: %s", key, err)
}
// wgStats could be zero value, in which case we just reset the stats
if err := e.statusRecorder.UpdateWireGuardPeerState(key, wgStats); err != nil {
log.Debugf("failed to update wg stats for peer %s: %s", key, err)
}
}
return true
})
}
}
func (e *Engine) probeSTUNs() []relay.ProbeResult {
return relay.ProbeAll(e.ctx, relay.ProbeSTUN, e.STUNs)
}
func (e *Engine) probeTURNs() []relay.ProbeResult {
return relay.ProbeAll(e.ctx, relay.ProbeTURN, e.TURNs)
}

View File

@@ -13,7 +13,7 @@ import (
"testing"
"time"
"github.com/pion/transport/v2/stdnet"
"github.com/pion/transport/v3/stdnet"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -21,6 +21,7 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
"github.com/netbirdio/management-integrations/integrations"
"github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/routemanager"
@@ -70,10 +71,11 @@ func TestEngine_SSH(t *testing.T) {
defer cancel()
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
WgIfaceName: "utun101",
WgAddr: "100.64.0.1/24",
WgPrivateKey: key,
WgPort: 33100,
WgIfaceName: "utun101",
WgAddr: "100.64.0.1/24",
WgPrivateKey: key,
WgPort: 33100,
ServerSSHAllowed: true,
}, MobileDependency{}, peer.NewRecorder("https://mgm"))
engine.dnsServer = &dns.MockServer{
@@ -213,7 +215,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
if err != nil {
t.Fatal(err)
}
engine.wgInterface, err = iface.NewWGIFace("utun102", "100.64.0.1/24", iface.DefaultMTU, nil, newNet)
engine.wgInterface, err = iface.NewWGIFace("utun102", "100.64.0.1/24", engine.config.WgPort, key.String(), iface.DefaultMTU, newNet, nil)
if err != nil {
t.Fatal(err)
}
@@ -567,7 +569,7 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
if err != nil {
t.Fatal(err)
}
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, iface.DefaultMTU, nil, newNet)
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, engine.config.WgPort, key.String(), iface.DefaultMTU, newNet, nil)
assert.NoError(t, err, "shouldn't return error")
input := struct {
inputSerial uint64
@@ -736,7 +738,7 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
if err != nil {
t.Fatal(err)
}
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, iface.DefaultMTU, nil, newNet)
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, 33100, key.String(), iface.DefaultMTU, newNet, nil)
assert.NoError(t, err, "shouldn't return error")
mockRouteManager := &routemanager.MockManager{
@@ -1049,8 +1051,8 @@ func startManagement(dataDir string) (*grpc.Server, string, error) {
if err != nil {
return nil, "", err
}
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "",
eventStore, false)
ia, _ := integrations.NewIntegratedValidator(eventStore)
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia)
if err != nil {
return nil, "", err
}

View File

@@ -3,5 +3,6 @@ package listener
// NetworkChangeListener is a callback interface for mobile system
type NetworkChangeListener interface {
// OnNetworkChanged invoke when network settings has been changed
OnNetworkChanged()
OnNetworkChanged(string)
SetInterfaceIP(string)
}

View File

@@ -9,9 +9,14 @@ import (
// MobileDependency collect all dependencies for mobile platform
type MobileDependency struct {
// Android only
TunAdapter iface.TunAdapter
IFaceDiscover stdnet.ExternalIFaceDiscover
NetworkChangeListener listener.NetworkChangeListener
HostDNSAddresses []string
DnsReadyListener dns.ReadyListener
// iOS only
DnsManager dns.IosDnsManager
FileDescriptor int32
}

View File

@@ -4,11 +4,13 @@ import (
"context"
"fmt"
"net"
"runtime"
"strings"
"sync"
"time"
"github.com/pion/ice/v2"
"github.com/pion/ice/v3"
"github.com/pion/stun/v2"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@@ -24,6 +26,8 @@ import (
const (
iceKeepAliveDefault = 4 * time.Second
iceDisconnectedTimeoutDefault = 6 * time.Second
// iceRelayAcceptanceMinWaitDefault is the same as in the Pion ICE package
iceRelayAcceptanceMinWaitDefault = 2 * time.Second
defaultWgKeepAlive = 25 * time.Second
)
@@ -45,7 +49,7 @@ type ConnConfig struct {
LocalKey string
// StunTurn is a list of STUN and TURN URLs
StunTurn []*ice.URL
StunTurn []*stun.URI
// 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)
@@ -65,6 +69,11 @@ type ConnConfig struct {
// UsesBind indicates whether the WireGuard interface is userspace and uses bind.ICEBind
UserspaceBind bool
// RosenpassPubKey is this peer's Rosenpass public key
RosenpassPubKey []byte
// RosenpassPubKey is this peer's RosenpassAddr server address (IP:port)
RosenpassAddr string
}
// OfferAnswer represents a session establishment offer or answer
@@ -77,6 +86,12 @@ type OfferAnswer struct {
// Version of NetBird Agent
Version string
// RosenpassPubKey is the Rosenpass public key of the remote peer when receiving this message
// This value is the local Rosenpass server public key when sending the message
RosenpassPubKey []byte
// RosenpassAddr is the Rosenpass server address (IP:port) of the remote peer when receiving this message
// This value is the local Rosenpass server address when sending the message
RosenpassAddr string
}
// IceCredentials ICE protocol credentials struct
@@ -95,6 +110,8 @@ type Conn struct {
signalOffer func(OfferAnswer) error
signalAnswer func(OfferAnswer) error
sendSignalMessage func(message *sProto.Message) error
onConnected func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string)
onDisconnected func(remotePeer string, wgIP string)
// remoteOffersCh is a channel used to wait for remote credentials to proceed with the connection
remoteOffersCh chan OfferAnswer
@@ -115,8 +132,12 @@ type Conn struct {
remoteModeCh chan ModeMessage
meta meta
adapter iface.TunAdapter
iFaceDiscover stdnet.ExternalIFaceDiscover
adapter iface.TunAdapter
iFaceDiscover stdnet.ExternalIFaceDiscover
sentExtraSrflx bool
remoteEndpoint *net.UDPAddr
remoteConn *ice.Conn
}
// meta holds meta information about a connection
@@ -141,7 +162,7 @@ func (conn *Conn) WgConfig() WgConfig {
}
// UpdateStunTurn update the turn and stun addresses
func (conn *Conn) UpdateStunTurn(turnStun []*ice.URL) {
func (conn *Conn) UpdateStunTurn(turnStun []*stun.URI) {
conn.config.StunTurn = turnStun
}
@@ -177,20 +198,22 @@ func (conn *Conn) reCreateAgent() error {
iceKeepAlive := iceKeepAlive()
iceDisconnectedTimeout := iceDisconnectedTimeout()
iceRelayAcceptanceMinWait := iceRelayAcceptanceMinWait()
agentConfig := &ice.AgentConfig{
MulticastDNSMode: ice.MulticastDNSModeDisabled,
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6},
Urls: conn.config.StunTurn,
CandidateTypes: conn.candidateTypes(),
FailedTimeout: &failedTimeout,
InterfaceFilter: stdnet.InterfaceFilter(conn.config.InterfaceBlackList),
UDPMux: conn.config.UDPMux,
UDPMuxSrflx: conn.config.UDPMuxSrflx,
NAT1To1IPs: conn.config.NATExternalIPs,
Net: transportNet,
DisconnectedTimeout: &iceDisconnectedTimeout,
KeepaliveInterval: &iceKeepAlive,
MulticastDNSMode: ice.MulticastDNSModeDisabled,
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6},
Urls: conn.config.StunTurn,
CandidateTypes: conn.candidateTypes(),
FailedTimeout: &failedTimeout,
InterfaceFilter: stdnet.InterfaceFilter(conn.config.InterfaceBlackList),
UDPMux: conn.config.UDPMux,
UDPMuxSrflx: conn.config.UDPMuxSrflx,
NAT1To1IPs: conn.config.NATExternalIPs,
Net: transportNet,
DisconnectedTimeout: &iceDisconnectedTimeout,
KeepaliveInterval: &iceKeepAlive,
RelayAcceptanceMinWait: &iceRelayAcceptanceMinWait,
}
if conn.config.DisableIPv6Discovery {
@@ -218,6 +241,17 @@ func (conn *Conn) reCreateAgent() error {
return err
}
err = conn.agent.OnSuccessfulSelectedPairBindingResponse(func(p *ice.CandidatePair) {
err := conn.statusRecorder.UpdateLatency(conn.config.Key, p.Latency())
if err != nil {
log.Debugf("failed to update latency for peer %s: %s", conn.config.Key, err)
return
}
})
if err != nil {
return fmt.Errorf("failed setting binding response callback: %w", err)
}
return nil
}
@@ -225,6 +259,10 @@ func (conn *Conn) candidateTypes() []ice.CandidateType {
if hasICEForceRelayConn() {
return []ice.CandidateType{ice.CandidateTypeRelay}
}
// TODO: remove this once we have refactored userspace proxy into the bind package
if runtime.GOOS == "ios" {
return []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive}
}
return []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive, ice.CandidateTypeRelay}
}
@@ -328,8 +366,12 @@ func (conn *Conn) Open() error {
if remoteOfferAnswer.WgListenPort != 0 {
remoteWgPort = remoteOfferAnswer.WgListenPort
}
conn.remoteConn = remoteConn
// the ice connection has been established successfully so we are ready to start the proxy
remoteAddr, err := conn.configureConnection(remoteConn, remoteWgPort)
remoteAddr, err := conn.configureConnection(remoteConn, remoteWgPort, remoteOfferAnswer.RosenpassPubKey,
remoteOfferAnswer.RosenpassAddr)
if err != nil {
return err
}
@@ -352,7 +394,7 @@ func isRelayCandidate(candidate ice.Candidate) bool {
}
// configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int) (net.Addr, error) {
func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int, remoteRosenpassPubKey []byte, remoteRosenpassAddr string) (net.Addr, error) {
conn.mu.Lock()
defer conn.mu.Unlock()
@@ -370,12 +412,13 @@ func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int) (ne
return nil, err
}
} else {
// To support old version's with direct mode we attempt to punch an additional role with the remote wireguard port
// To support old version's with direct mode we attempt to punch an additional role with the remote WireGuard port
go conn.punchRemoteWGPort(pair, remoteWgPort)
endpoint = remoteConn.RemoteAddr()
}
endpointUdpAddr, _ := net.ResolveUDPAddr(endpoint.Network(), endpoint.String())
conn.remoteEndpoint = endpointUdpAddr
err = conn.config.WgConfig.WgInterface.UpdatePeer(conn.config.WgConfig.RemoteKey, conn.config.WgConfig.AllowedIps, defaultWgKeepAlive, endpointUdpAddr, conn.config.WgConfig.PreSharedKey)
if err != nil {
@@ -386,14 +429,21 @@ func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int) (ne
}
conn.status = StatusConnected
rosenpassEnabled := false
if remoteRosenpassPubKey != nil {
rosenpassEnabled = true
}
peerState := State{
PubKey: conn.config.Key,
ConnStatus: conn.status,
ConnStatusUpdate: time.Now(),
LocalIceCandidateType: pair.Local.Type().String(),
RemoteIceCandidateType: pair.Remote.Type().String(),
Direct: !isRelayCandidate(pair.Local),
PubKey: conn.config.Key,
ConnStatus: conn.status,
ConnStatusUpdate: time.Now(),
LocalIceCandidateType: pair.Local.Type().String(),
RemoteIceCandidateType: pair.Remote.Type().String(),
LocalIceCandidateEndpoint: fmt.Sprintf("%s:%d", pair.Local.Address(), pair.Local.Port()),
RemoteIceCandidateEndpoint: fmt.Sprintf("%s:%d", pair.Remote.Address(), pair.Local.Port()),
Direct: !isRelayCandidate(pair.Local),
RosenpassEnabled: rosenpassEnabled,
}
if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay {
peerState.Relayed = true
@@ -404,6 +454,15 @@ func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int) (ne
log.Warnf("unable to save peer's state, got error: %v", err)
}
_, ipNet, err := net.ParseCIDR(conn.config.WgConfig.AllowedIps)
if err != nil {
return nil, err
}
if conn.onConnected != nil {
conn.onConnected(conn.config.Key, remoteRosenpassPubKey, ipNet.IP.String(), remoteRosenpassAddr)
}
return endpoint, nil
}
@@ -433,6 +492,8 @@ func (conn *Conn) cleanup() error {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.sentExtraSrflx = false
var err1, err2, err3 error
if conn.agent != nil {
err1 = conn.agent.Close()
@@ -454,6 +515,10 @@ func (conn *Conn) cleanup() error {
conn.notifyDisconnected = nil
}
if conn.status == StatusConnected && conn.onDisconnected != nil {
conn.onDisconnected(conn.config.WgConfig.RemoteKey, conn.config.WgConfig.AllowedIps)
}
conn.status = StatusDisconnected
peerState := State{
@@ -464,9 +529,12 @@ func (conn *Conn) cleanup() error {
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
// todo rethink status updates
log.Debugf("error while updating peer's %s state, err: %v", conn.config.Key, err)
}
if err := conn.statusRecorder.UpdateWireGuardPeerState(conn.config.Key, iface.WGStats{}); err != nil {
log.Debugf("failed to reset wireguard stats for peer %s: %s", conn.config.Key, err)
}
log.Debugf("cleaned up connection to peer %s", conn.config.Key)
if err1 != nil {
@@ -483,6 +551,16 @@ func (conn *Conn) SetSignalOffer(handler func(offer OfferAnswer) error) {
conn.signalOffer = handler
}
// SetOnConnected sets a handler function to be triggered by Conn when a new connection to a remote peer established
func (conn *Conn) SetOnConnected(handler func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string)) {
conn.onConnected = handler
}
// SetOnDisconnected sets a handler function to be triggered by Conn when a connection to a remote disconnected
func (conn *Conn) SetOnDisconnected(handler func(remotePeer string, wgIP string)) {
conn.onDisconnected = 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
@@ -509,6 +587,30 @@ func (conn *Conn) onICECandidate(candidate ice.Candidate) {
if err != nil {
log.Errorf("failed signaling candidate to the remote peer %s %s", conn.config.Key, err)
}
// sends an extra server reflexive candidate to the remote peer with our related port (usually the wireguard port)
// this is useful when network has an existing port forwarding rule for the wireguard port and this peer
if !conn.sentExtraSrflx && candidate.Type() == ice.CandidateTypeServerReflexive && candidate.Port() != candidate.RelatedAddress().Port {
relatedAdd := candidate.RelatedAddress()
extraSrflx, err := ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
Network: candidate.NetworkType().String(),
Address: candidate.Address(),
Port: relatedAdd.Port,
Component: candidate.Component(),
RelAddr: relatedAdd.Address,
RelPort: relatedAdd.Port,
})
if err != nil {
log.Errorf("failed creating extra server reflexive candidate %s", err)
return
}
err = conn.signalCandidate(extraSrflx)
if err != nil {
log.Errorf("failed signaling the extra server reflexive candidate to the remote peer %s: %s", conn.config.Key, err)
return
}
conn.sentExtraSrflx = true
}
}()
}
}
@@ -537,9 +639,11 @@ func (conn *Conn) sendAnswer() error {
log.Debugf("sending answer to %s", conn.config.Key)
err = conn.signalAnswer(OfferAnswer{
IceCredentials: IceCredentials{localUFrag, localPwd},
WgListenPort: conn.config.LocalWgPort,
Version: version.NetbirdVersion(),
IceCredentials: IceCredentials{localUFrag, localPwd},
WgListenPort: conn.config.LocalWgPort,
Version: version.NetbirdVersion(),
RosenpassPubKey: conn.config.RosenpassPubKey,
RosenpassAddr: conn.config.RosenpassAddr,
})
if err != nil {
return err
@@ -558,9 +662,11 @@ func (conn *Conn) sendOffer() error {
return err
}
err = conn.signalOffer(OfferAnswer{
IceCredentials: IceCredentials{localUFrag, localPwd},
WgListenPort: conn.config.LocalWgPort,
Version: version.NetbirdVersion(),
IceCredentials: IceCredentials{localUFrag, localPwd},
WgListenPort: conn.config.LocalWgPort,
Version: version.NetbirdVersion(),
RosenpassPubKey: conn.config.RosenpassPubKey,
RosenpassAddr: conn.config.RosenpassAddr,
})
if err != nil {
return err

View File

@@ -6,7 +6,7 @@ import (
"time"
"github.com/magiconair/properties/assert"
"github.com/pion/ice/v2"
"github.com/pion/stun/v2"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/internal/wgproxy"
@@ -16,7 +16,7 @@ import (
var connConf = ConnConfig{
Key: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
LocalKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
StunTurn: []*ice.URL{},
StunTurn: []*stun.URI{},
InterfaceBlackList: nil,
Timeout: time.Second,
LocalWgPort: 51820,

View File

@@ -10,9 +10,10 @@ import (
)
const (
envICEKeepAliveIntervalSec = "NB_ICE_KEEP_ALIVE_INTERVAL_SEC"
envICEDisconnectedTimeoutSec = "NB_ICE_DISCONNECTED_TIMEOUT_SEC"
envICEForceRelayConn = "NB_ICE_FORCE_RELAY_CONN"
envICEKeepAliveIntervalSec = "NB_ICE_KEEP_ALIVE_INTERVAL_SEC"
envICEDisconnectedTimeoutSec = "NB_ICE_DISCONNECTED_TIMEOUT_SEC"
envICERelayAcceptanceMinWaitSec = "NB_ICE_RELAY_ACCEPTANCE_MIN_WAIT_SEC"
envICEForceRelayConn = "NB_ICE_FORCE_RELAY_CONN"
)
func iceKeepAlive() time.Duration {
@@ -21,7 +22,7 @@ func iceKeepAlive() time.Duration {
return iceKeepAliveDefault
}
log.Debugf("setting ICE keep alive interval to %s seconds", keepAliveEnv)
log.Infof("setting ICE keep alive interval to %s seconds", keepAliveEnv)
keepAliveEnvSec, err := strconv.Atoi(keepAliveEnv)
if err != nil {
log.Warnf("invalid value %s set for %s, using default %v", keepAliveEnv, envICEKeepAliveIntervalSec, iceKeepAliveDefault)
@@ -37,7 +38,7 @@ func iceDisconnectedTimeout() time.Duration {
return iceDisconnectedTimeoutDefault
}
log.Debugf("setting ICE disconnected timeout to %s seconds", disconnectedTimeoutEnv)
log.Infof("setting ICE disconnected timeout to %s seconds", disconnectedTimeoutEnv)
disconnectedTimeoutSec, err := strconv.Atoi(disconnectedTimeoutEnv)
if err != nil {
log.Warnf("invalid value %s set for %s, using default %v", disconnectedTimeoutEnv, envICEDisconnectedTimeoutSec, iceDisconnectedTimeoutDefault)
@@ -47,6 +48,22 @@ func iceDisconnectedTimeout() time.Duration {
return time.Duration(disconnectedTimeoutSec) * time.Second
}
func iceRelayAcceptanceMinWait() time.Duration {
iceRelayAcceptanceMinWaitEnv := os.Getenv(envICERelayAcceptanceMinWaitSec)
if iceRelayAcceptanceMinWaitEnv == "" {
return iceRelayAcceptanceMinWaitDefault
}
log.Infof("setting ICE relay acceptance min wait to %s seconds", iceRelayAcceptanceMinWaitEnv)
disconnectedTimeoutSec, err := strconv.Atoi(iceRelayAcceptanceMinWaitEnv)
if err != nil {
log.Warnf("invalid value %s set for %s, using default %v", iceRelayAcceptanceMinWaitEnv, envICERelayAcceptanceMinWaitSec, iceRelayAcceptanceMinWaitDefault)
return iceRelayAcceptanceMinWaitDefault
}
return time.Duration(disconnectedTimeoutSec) * time.Second
}
func hasICEForceRelayConn() bool {
disconnectedTimeoutEnv := os.Getenv(envICEForceRelayConn)
return strings.ToLower(disconnectedTimeoutEnv) == "true"

View File

@@ -4,19 +4,33 @@ import (
"errors"
"sync"
"time"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal/relay"
"github.com/netbirdio/netbird/iface"
)
// 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
IP string
PubKey string
FQDN string
ConnStatus ConnStatus
ConnStatusUpdate time.Time
Relayed bool
Direct bool
LocalIceCandidateType string
RemoteIceCandidateType string
LocalIceCandidateEndpoint string
RemoteIceCandidateEndpoint string
LastWireguardHandshake time.Time
BytesTx int64
BytesRx int64
Latency time.Duration
RosenpassEnabled bool
Routes map[string]struct{}
}
// LocalPeerState contains the latest state of the local peer
@@ -25,18 +39,37 @@ type LocalPeerState struct {
PubKey string
KernelInterface bool
FQDN string
Routes map[string]struct{}
}
// SignalState contains the latest state of a signal connection
type SignalState struct {
URL string
Connected bool
Error error
}
// ManagementState contains the latest state of a management connection
type ManagementState struct {
URL string
Connected bool
Error error
}
// RosenpassState contains the latest state of the Rosenpass configuration
type RosenpassState struct {
Enabled bool
Permissive bool
}
// NSGroupState represents the status of a DNS server group, including associated domains,
// whether it's enabled, and the last error message encountered during probing.
type NSGroupState struct {
ID string
Servers []string
Domains []string
Enabled bool
Error error
}
// FullStatus contains the full state held by the Status instance
@@ -45,20 +78,29 @@ type FullStatus struct {
ManagementState ManagementState
SignalState SignalState
LocalPeerState LocalPeerState
RosenpassState RosenpassState
Relays []relay.ProbeResult
NSGroupStates []NSGroupState
}
// Status holds a state of peers, signal and management connections
// Status holds a state of peers, signal, management connections and relays
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
mux sync.Mutex
peers map[string]State
changeNotify map[string]chan struct{}
signalState bool
signalError error
managementState bool
managementError error
relayStates []relay.ProbeResult
localPeer LocalPeerState
offlinePeers []State
mgmAddress string
signalAddress string
notifier *notifier
rosenpassEnabled bool
rosenpassPermissive bool
nsGroupStates []NSGroupState
// To reduce the number of notification invocation this bool will be true when need to call the notification
// Some Peer actions mostly used by in a batch when the network map has been synchronized. In these type of events
@@ -147,6 +189,10 @@ func (d *Status) UpdatePeerState(receivedState State) error {
peerState.IP = receivedState.IP
}
if receivedState.Routes != nil {
peerState.Routes = receivedState.Routes
}
skipNotification := shouldSkipNotify(receivedState, peerState)
if receivedState.ConnStatus != peerState.ConnStatus {
@@ -156,6 +202,9 @@ func (d *Status) UpdatePeerState(receivedState State) error {
peerState.Relayed = receivedState.Relayed
peerState.LocalIceCandidateType = receivedState.LocalIceCandidateType
peerState.RemoteIceCandidateType = receivedState.RemoteIceCandidateType
peerState.LocalIceCandidateEndpoint = receivedState.LocalIceCandidateEndpoint
peerState.RemoteIceCandidateEndpoint = receivedState.RemoteIceCandidateEndpoint
peerState.RosenpassEnabled = receivedState.RosenpassEnabled
}
d.peers[receivedState.PubKey] = peerState
@@ -174,6 +223,25 @@ func (d *Status) UpdatePeerState(receivedState State) error {
return nil
}
// UpdateWireGuardPeerState updates the WireGuard bits of the peer state
func (d *Status) UpdateWireGuardPeerState(pubKey string, wgStats iface.WGStats) error {
d.mux.Lock()
defer d.mux.Unlock()
peerState, ok := d.peers[pubKey]
if !ok {
return errors.New("peer doesn't exist")
}
peerState.LastWireguardHandshake = wgStats.LastHandshake
peerState.BytesRx = wgStats.RxBytes
peerState.BytesTx = wgStats.TxBytes
d.peers[pubKey] = peerState
return nil
}
func shouldSkipNotify(received, curr State) bool {
switch {
case received.ConnStatus == StatusConnecting:
@@ -229,6 +297,13 @@ func (d *Status) GetPeerStateChangeNotifier(peer string) <-chan struct{} {
return ch
}
// GetLocalPeerState returns the local peer state
func (d *Status) GetLocalPeerState() LocalPeerState {
d.mux.Lock()
defer d.mux.Unlock()
return d.localPeer
}
// UpdateLocalPeerState updates local peer status
func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) {
d.mux.Lock()
@@ -248,12 +323,13 @@ func (d *Status) CleanLocalPeerState() {
}
// MarkManagementDisconnected sets ManagementState to disconnected
func (d *Status) MarkManagementDisconnected() {
func (d *Status) MarkManagementDisconnected(err error) {
d.mux.Lock()
defer d.mux.Unlock()
defer d.onConnectionChanged()
d.managementState = false
d.managementError = err
}
// MarkManagementConnected sets ManagementState to connected
@@ -263,6 +339,7 @@ func (d *Status) MarkManagementConnected() {
defer d.onConnectionChanged()
d.managementState = true
d.managementError = nil
}
// UpdateSignalAddress update the address of the signal server
@@ -279,13 +356,22 @@ func (d *Status) UpdateManagementAddress(mgmAddress string) {
d.mgmAddress = mgmAddress
}
// UpdateRosenpass update the Rosenpass configuration
func (d *Status) UpdateRosenpass(rosenpassEnabled, rosenpassPermissive bool) {
d.mux.Lock()
defer d.mux.Unlock()
d.rosenpassPermissive = rosenpassPermissive
d.rosenpassEnabled = rosenpassEnabled
}
// MarkSignalDisconnected sets SignalState to disconnected
func (d *Status) MarkSignalDisconnected() {
func (d *Status) MarkSignalDisconnected(err error) {
d.mux.Lock()
defer d.mux.Unlock()
defer d.onConnectionChanged()
d.signalState = false
d.signalError = err
}
// MarkSignalConnected sets SignalState to connected
@@ -295,6 +381,84 @@ func (d *Status) MarkSignalConnected() {
defer d.onConnectionChanged()
d.signalState = true
d.signalError = nil
}
func (d *Status) UpdateRelayStates(relayResults []relay.ProbeResult) {
d.mux.Lock()
defer d.mux.Unlock()
d.relayStates = relayResults
}
func (d *Status) UpdateDNSStates(dnsStates []NSGroupState) {
d.mux.Lock()
defer d.mux.Unlock()
d.nsGroupStates = dnsStates
}
func (d *Status) GetRosenpassState() RosenpassState {
return RosenpassState{
d.rosenpassEnabled,
d.rosenpassPermissive,
}
}
func (d *Status) GetManagementState() ManagementState {
return ManagementState{
d.mgmAddress,
d.managementState,
d.managementError,
}
}
func (d *Status) UpdateLatency(pubKey string, latency time.Duration) error {
if latency <= 0 {
return nil
}
d.mux.Lock()
defer d.mux.Unlock()
peerState, ok := d.peers[pubKey]
if !ok {
return errors.New("peer doesn't exist")
}
peerState.Latency = latency
d.peers[pubKey] = peerState
return nil
}
// IsLoginRequired determines if a peer's login has expired.
func (d *Status) IsLoginRequired() bool {
d.mux.Lock()
defer d.mux.Unlock()
// if peer is connected to the management then login is not expired
if d.managementState {
return false
}
s, ok := gstatus.FromError(d.managementError)
if ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
return true
}
return false
}
func (d *Status) GetSignalState() SignalState {
return SignalState{
d.signalAddress,
d.signalState,
d.signalError,
}
}
func (d *Status) GetRelayStates() []relay.ProbeResult {
return d.relayStates
}
func (d *Status) GetDNSStates() []NSGroupState {
return d.nsGroupStates
}
// GetFullStatus gets full status
@@ -303,15 +467,12 @@ func (d *Status) GetFullStatus() FullStatus {
defer d.mux.Unlock()
fullStatus := FullStatus{
ManagementState: ManagementState{
d.mgmAddress,
d.managementState,
},
SignalState: SignalState{
d.signalAddress,
d.signalState,
},
LocalPeerState: d.localPeer,
ManagementState: d.GetManagementState(),
SignalState: d.GetSignalState(),
LocalPeerState: d.localPeer,
Relays: d.GetRelayStates(),
RosenpassState: d.GetRosenpassState(),
NSGroupStates: d.GetDNSStates(),
}
for _, status := range d.peers {

View File

@@ -1,6 +1,7 @@
package peer
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
@@ -152,9 +153,10 @@ func TestUpdateSignalState(t *testing.T) {
name string
connected bool
want bool
err error
}{
{"should mark as connected", true, true},
{"should mark as disconnected", false, false},
{"should mark as connected", true, true, nil},
{"should mark as disconnected", false, false, errors.New("test")},
}
status := NewRecorder("https://mgm")
@@ -165,9 +167,10 @@ func TestUpdateSignalState(t *testing.T) {
if test.connected {
status.MarkSignalConnected()
} else {
status.MarkSignalDisconnected()
status.MarkSignalDisconnected(test.err)
}
assert.Equal(t, test.want, status.signalState, "signal status should be equal")
assert.Equal(t, test.err, status.signalError)
})
}
}
@@ -178,9 +181,10 @@ func TestUpdateManagementState(t *testing.T) {
name string
connected bool
want bool
err error
}{
{"should mark as connected", true, true},
{"should mark as disconnected", false, false},
{"should mark as connected", true, true, nil},
{"should mark as disconnected", false, false, errors.New("test")},
}
status := NewRecorder(url)
@@ -190,9 +194,10 @@ func TestUpdateManagementState(t *testing.T) {
if test.connected {
status.MarkManagementConnected()
} else {
status.MarkManagementDisconnected()
status.MarkManagementDisconnected(test.err)
}
assert.Equal(t, test.want, status.managementState, "signalState status should be equal")
assert.Equal(t, test.err, status.managementError)
})
}
}

51
client/internal/probe.go Normal file
View File

@@ -0,0 +1,51 @@
package internal
import "context"
// Probe allows to run on-demand callbacks from different code locations.
// Pass the probe to a receiving and a sending end. The receiving end starts listening
// to requests with Receive and executes a callback when the sending end requests it
// by calling Probe.
type Probe struct {
request chan struct{}
result chan bool
ready bool
}
// NewProbe returns a new initialized probe.
func NewProbe() *Probe {
return &Probe{
request: make(chan struct{}),
result: make(chan bool),
}
}
// Probe requests the callback to be run and returns a bool indicating success.
// It always returns true as long as the receiver is not ready.
func (p *Probe) Probe() bool {
if !p.ready {
return true
}
p.request <- struct{}{}
return <-p.result
}
// Receive starts listening for probe requests. On such a request it runs the supplied
// callback func which must return a bool indicating success.
// Blocks until the passed context is cancelled.
func (p *Probe) Receive(ctx context.Context, callback func() bool) {
p.ready = true
defer func() {
p.ready = false
}()
for {
select {
case <-ctx.Done():
return
case <-p.request:
p.result <- callback()
}
}
}

View File

@@ -0,0 +1,187 @@
package relay
import (
"context"
"fmt"
"net"
"sync"
"time"
"github.com/pion/stun/v2"
"github.com/pion/turn/v3"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/stdnet"
nbnet "github.com/netbirdio/netbird/util/net"
)
// ProbeResult holds the info about the result of a relay probe request
type ProbeResult struct {
URI *stun.URI
Err error
Addr string
}
// ProbeSTUN tries binding to the given STUN uri and acquiring an address
func ProbeSTUN(ctx context.Context, uri *stun.URI) (addr string, probeErr error) {
defer func() {
if probeErr != nil {
log.Debugf("stun probe error from %s: %s", uri, probeErr)
}
}()
net, err := stdnet.NewNet(nil)
if err != nil {
probeErr = fmt.Errorf("new net: %w", err)
return
}
client, err := stun.DialURI(uri, &stun.DialConfig{
Net: net,
})
if err != nil {
probeErr = fmt.Errorf("dial: %w", err)
return
}
defer func() {
if err := client.Close(); err != nil && probeErr == nil {
probeErr = fmt.Errorf("close: %w", err)
}
}()
done := make(chan struct{})
if err = client.Start(stun.MustBuild(stun.TransactionID, stun.BindingRequest), func(res stun.Event) {
if res.Error != nil {
probeErr = fmt.Errorf("request: %w", err)
return
}
var xorAddr stun.XORMappedAddress
if getErr := xorAddr.GetFrom(res.Message); getErr != nil {
probeErr = fmt.Errorf("get xor addr: %w", err)
return
}
log.Debugf("stun probe received address from %s: %s", uri, xorAddr)
addr = xorAddr.String()
done <- struct{}{}
}); err != nil {
probeErr = fmt.Errorf("client: %w", err)
return
}
select {
case <-ctx.Done():
probeErr = fmt.Errorf("stun request: %w", ctx.Err())
return
case <-done:
}
return addr, nil
}
// ProbeTURN tries allocating a session from the given TURN URI
func ProbeTURN(ctx context.Context, uri *stun.URI) (addr string, probeErr error) {
defer func() {
if probeErr != nil {
log.Debugf("turn probe error from %s: %s", uri, probeErr)
}
}()
turnServerAddr := fmt.Sprintf("%s:%d", uri.Host, uri.Port)
var conn net.PacketConn
switch uri.Proto {
case stun.ProtoTypeUDP:
var err error
conn, err = nbnet.NewListener().ListenPacket(ctx, "udp", "")
if err != nil {
probeErr = fmt.Errorf("listen: %w", err)
return
}
case stun.ProtoTypeTCP:
tcpConn, err := nbnet.NewDialer().DialContext(ctx, "tcp", turnServerAddr)
if err != nil {
probeErr = fmt.Errorf("dial: %w", err)
return
}
conn = turn.NewSTUNConn(tcpConn)
default:
probeErr = fmt.Errorf("conn: unknown proto: %s", uri.Proto)
return
}
defer func() {
if err := conn.Close(); err != nil && probeErr == nil {
probeErr = fmt.Errorf("conn close: %w", err)
}
}()
net, err := stdnet.NewNet(nil)
if err != nil {
probeErr = fmt.Errorf("new net: %w", err)
return
}
cfg := &turn.ClientConfig{
STUNServerAddr: turnServerAddr,
TURNServerAddr: turnServerAddr,
Conn: conn,
Username: uri.Username,
Password: uri.Password,
Net: net,
}
client, err := turn.NewClient(cfg)
if err != nil {
probeErr = fmt.Errorf("create client: %w", err)
return
}
defer client.Close()
if err := client.Listen(); err != nil {
probeErr = fmt.Errorf("client listen: %w", err)
return
}
relayConn, err := client.Allocate()
if err != nil {
probeErr = fmt.Errorf("allocate: %w", err)
return
}
defer func() {
if err := relayConn.Close(); err != nil && probeErr == nil {
probeErr = fmt.Errorf("close relay conn: %w", err)
}
}()
log.Debugf("turn probe relay address from %s: %s", uri, relayConn.LocalAddr())
return relayConn.LocalAddr().String(), nil
}
// ProbeAll probes all given servers asynchronously and returns the results
func ProbeAll(
ctx context.Context,
fn func(ctx context.Context, uri *stun.URI) (addr string, probeErr error),
relays []*stun.URI,
) []ProbeResult {
results := make([]ProbeResult, len(relays))
var wg sync.WaitGroup
for i, uri := range relays {
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
wg.Add(1)
go func(res *ProbeResult, stunURI *stun.URI) {
defer wg.Done()
res.URI = stunURI
res.Addr, res.Err = fn(ctx, stunURI)
}(&results[i], uri)
}
wg.Wait()
return results
}

View File

@@ -0,0 +1,204 @@
package rosenpass
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"log/slog"
"net"
"os"
"strconv"
"strings"
"sync"
rp "cunicu.li/go-rosenpass"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
func hashRosenpassKey(key []byte) string {
hasher := sha256.New()
hasher.Write(key)
return hex.EncodeToString(hasher.Sum(nil))
}
type Manager struct {
ifaceName string
spk []byte
ssk []byte
rpKeyHash string
preSharedKey *[32]byte
rpPeerIDs map[string]*rp.PeerID
rpWgHandler *NetbirdHandler
server *rp.Server
lock sync.Mutex
port int
}
// NewManager creates a new Rosenpass manager
func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string) (*Manager, error) {
public, secret, err := rp.GenerateKeyPair()
if err != nil {
return nil, err
}
rpKeyHash := hashRosenpassKey(public)
log.Debugf("generated new rosenpass key pair with public key %s", rpKeyHash)
return &Manager{ifaceName: wgIfaceName, rpKeyHash: rpKeyHash, spk: public, ssk: secret, preSharedKey: (*[32]byte)(preSharedKey), rpPeerIDs: make(map[string]*rp.PeerID), lock: sync.Mutex{}}, nil
}
func (m *Manager) GetPubKey() []byte {
return m.spk
}
// GetAddress returns the address of the Rosenpass server
func (m *Manager) GetAddress() *net.UDPAddr {
return &net.UDPAddr{Port: m.port}
}
// addPeer adds a new peer to the Rosenpass server
func (m *Manager) addPeer(rosenpassPubKey []byte, rosenpassAddr string, wireGuardIP string, wireGuardPubKey string) error {
var err error
pcfg := rp.PeerConfig{PublicKey: rosenpassPubKey}
if m.preSharedKey != nil {
pcfg.PresharedKey = *m.preSharedKey
}
if bytes.Compare(m.spk, rosenpassPubKey) == 1 {
_, strPort, err := net.SplitHostPort(rosenpassAddr)
if err != nil {
return fmt.Errorf("failed to parse rosenpass address: %w", err)
}
peerAddr := fmt.Sprintf("%s:%s", wireGuardIP, strPort)
if pcfg.Endpoint, err = net.ResolveUDPAddr("udp", peerAddr); err != nil {
return fmt.Errorf("failed to resolve peer endpoint address: %w", err)
}
}
peerID, err := m.server.AddPeer(pcfg)
if err != nil {
return err
}
key, err := wgtypes.ParseKey(wireGuardPubKey)
if err != nil {
return err
}
m.rpWgHandler.AddPeer(peerID, m.ifaceName, rp.Key(key))
m.rpPeerIDs[wireGuardPubKey] = &peerID
return nil
}
// removePeer removes a peer from the Rosenpass server
func (m *Manager) removePeer(wireGuardPubKey string) error {
err := m.server.RemovePeer(*m.rpPeerIDs[wireGuardPubKey])
if err != nil {
return err
}
m.rpWgHandler.RemovePeer(*m.rpPeerIDs[wireGuardPubKey])
return nil
}
func (m *Manager) generateConfig() (rp.Config, error) {
opts := &slog.HandlerOptions{
Level: slog.LevelDebug,
}
logger := slog.New(slog.NewTextHandler(os.Stdout, opts))
cfg := rp.Config{Logger: logger}
cfg.PublicKey = m.spk
cfg.SecretKey = m.ssk
cfg.Peers = []rp.PeerConfig{}
m.rpWgHandler, _ = NewNetbirdHandler(m.preSharedKey, m.ifaceName)
cfg.Handlers = []rp.Handler{m.rpWgHandler}
port, err := findRandomAvailableUDPPort()
if err != nil {
log.Errorf("could not determine a random port for rosenpass server. Error: %s", err)
return rp.Config{}, err
}
m.port = port
cfg.ListenAddrs = []*net.UDPAddr{m.GetAddress()}
return cfg, nil
}
func (m *Manager) OnDisconnected(peerKey string, wgIP string) {
m.lock.Lock()
defer m.lock.Unlock()
if _, ok := m.rpPeerIDs[peerKey]; !ok {
// if we didn't have this peer yet, just skip
return
}
err := m.removePeer(peerKey)
if err != nil {
log.Error("failed to remove rosenpass peer", err)
}
delete(m.rpPeerIDs, peerKey)
}
// Run starts the Rosenpass server
func (m *Manager) Run() error {
conf, err := m.generateConfig()
if err != nil {
return err
}
m.server, err = rp.NewUDPServer(conf)
if err != nil {
return err
}
log.Infof("starting rosenpass server on port %d", m.port)
return m.server.Run()
}
// Close closes the Rosenpass server
func (m *Manager) Close() error {
if m.server != nil {
err := m.server.Close()
if err != nil {
log.Errorf("failed closing local rosenpass server")
}
m.server = nil
}
return nil
}
// OnConnected is a handler function that is triggered when a connection to a remote peer establishes
func (m *Manager) OnConnected(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string) {
m.lock.Lock()
defer m.lock.Unlock()
if remoteRosenpassPubKey == nil {
log.Warnf("remote peer with public key %s does not support rosenpass", remoteWireGuardKey)
return
}
rpKeyHash := hashRosenpassKey(remoteRosenpassPubKey)
log.Debugf("received remote rosenpass key %s, my key %s", rpKeyHash, m.rpKeyHash)
err := m.addPeer(remoteRosenpassPubKey, remoteRosenpassAddr, wireGuardIP, remoteWireGuardKey)
if err != nil {
log.Errorf("failed to add rosenpass peer: %s", err)
return
}
}
func findRandomAvailableUDPPort() (int, error) {
conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err != nil {
return 0, fmt.Errorf("could not find an available UDP port: %w", err)
}
defer conn.Close()
splitAddress := strings.Split(conn.LocalAddr().String(), ":")
return strconv.Atoi(splitAddress[len(splitAddress)-1])
}

View File

@@ -0,0 +1,126 @@
package rosenpass
import (
"fmt"
"log/slog"
rp "cunicu.li/go-rosenpass"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type wireGuardPeer struct {
Interface string
PublicKey rp.Key
}
type NetbirdHandler struct {
ifaceName string
client *wgctrl.Client
peers map[rp.PeerID]wireGuardPeer
presharedKey [32]byte
}
func NewNetbirdHandler(preSharedKey *[32]byte, wgIfaceName string) (hdlr *NetbirdHandler, err error) {
hdlr = &NetbirdHandler{
ifaceName: wgIfaceName,
peers: map[rp.PeerID]wireGuardPeer{},
}
if preSharedKey != nil {
hdlr.presharedKey = *preSharedKey
}
if hdlr.client, err = wgctrl.New(); err != nil {
return nil, fmt.Errorf("failed to creat WireGuard client: %w", err)
}
return hdlr, nil
}
func (h *NetbirdHandler) AddPeer(pid rp.PeerID, intf string, pk rp.Key) {
h.peers[pid] = wireGuardPeer{
Interface: intf,
PublicKey: pk,
}
}
func (h *NetbirdHandler) RemovePeer(pid rp.PeerID) {
delete(h.peers, pid)
}
func (h *NetbirdHandler) HandshakeCompleted(pid rp.PeerID, key rp.Key) {
log.Debug("Handshake complete")
h.outputKey(rp.KeyOutputReasonStale, pid, key)
}
func (h *NetbirdHandler) HandshakeExpired(pid rp.PeerID) {
key, _ := rp.GeneratePresharedKey()
log.Debug("Handshake expired")
h.outputKey(rp.KeyOutputReasonStale, pid, key)
}
func (h *NetbirdHandler) outputKey(_ rp.KeyOutputReason, pid rp.PeerID, psk rp.Key) {
wg, ok := h.peers[pid]
if !ok {
return
}
device, err := h.client.Device(h.ifaceName)
if err != nil {
log.Errorf("Failed to get WireGuard device: %v", err)
return
}
config := []wgtypes.PeerConfig{
{
UpdateOnly: true,
PublicKey: wgtypes.Key(wg.PublicKey),
PresharedKey: (*wgtypes.Key)(&psk),
},
}
for _, peer := range device.Peers {
if peer.PublicKey == wgtypes.Key(wg.PublicKey) {
if publicKeyEmpty(peer.PresharedKey) || peer.PresharedKey == h.presharedKey {
log.Debugf("Restart wireguard connection to peer %s", peer.PublicKey)
config = []wgtypes.PeerConfig{
{
PublicKey: wgtypes.Key(wg.PublicKey),
PresharedKey: (*wgtypes.Key)(&psk),
Endpoint: peer.Endpoint,
AllowedIPs: peer.AllowedIPs,
},
}
err = h.client.ConfigureDevice(wg.Interface, wgtypes.Config{
Peers: []wgtypes.PeerConfig{
{
Remove: true,
PublicKey: wgtypes.Key(wg.PublicKey),
},
},
})
if err != nil {
slog.Debug("Failed to remove peer")
return
}
}
}
}
if err = h.client.ConfigureDevice(wg.Interface, wgtypes.Config{
Peers: config,
}); err != nil {
log.Errorf("Failed to apply rosenpass key: %v", err)
}
}
func publicKeyEmpty(key wgtypes.Key) bool {
for _, b := range key {
if b != 0 {
return false
}
}
return true
}

View File

@@ -41,6 +41,7 @@ type clientNetwork struct {
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,
@@ -72,6 +73,18 @@ func (c *clientNetwork) getRouterPeerStatuses() map[string]routerPeerStatus {
return routePeerStatuses
}
// getBestRouteFromStatuses determines the most optimal route from the available routes
// within a clientNetwork, taking into account peer connection status, route metrics, and
// preference for non-relayed and direct connections.
//
// It follows these prioritization rules:
// * Connected peers: Only routes with connected peers are considered.
// * Metric: Routes with lower metrics (better) are prioritized.
// * Non-relayed: Routes without relays are preferred.
// * Direct connections: Routes with direct peer connections are favored.
// * Stability: In case of equal scores, the currently active route (if any) is maintained.
//
// It returns the ID of the selected optimal route.
func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[string]routerPeerStatus) string {
chosen := ""
chosenScore := 0
@@ -158,15 +171,21 @@ func (c *clientNetwork) startPeersStatusChangeWatcher() {
func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error {
state, err := c.statusRecorder.GetPeer(peerKey)
if err != nil {
return err
return fmt.Errorf("get peer state: %v", err)
}
delete(state.Routes, c.network.String())
if err := c.statusRecorder.UpdatePeerState(state); err != nil {
log.Warnf("Failed to update peer state: %v", err)
}
if 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",
return fmt.Errorf("remove allowed IP %s removed for peer %s, err: %v",
c.network, c.chosenRoute.Peer, err)
}
return nil
@@ -174,30 +193,26 @@ func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error {
func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
if c.chosenRoute != nil {
err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer)
if err != nil {
return err
if err := removeFromRouteTableIfNonSystem(c.network, c.wgInterface.Address().IP.String(), c.wgInterface.Name()); err != nil {
return fmt.Errorf("remove route %s from system, err: %v", c.network, 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)
if err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer); err != nil {
return fmt.Errorf("remove route: %v", err)
}
}
return nil
}
func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
var err error
routerPeerStatuses := c.getRouterPeerStatuses()
chosen := c.getBestRouteFromStatuses(routerPeerStatuses)
// If no route is chosen, remove the route from the peer and system
if chosen == "" {
err = c.removeRouteFromPeerAndSystem()
if err != nil {
return err
if err := c.removeRouteFromPeerAndSystem(); err != nil {
return fmt.Errorf("remove route from peer and system: %v", err)
}
c.chosenRoute = nil
@@ -205,6 +220,7 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
return nil
}
// If the chosen route is the same as the current route, do nothing
if c.chosenRoute != nil && c.chosenRoute.ID == chosen {
if c.chosenRoute.IsEqual(c.routes[chosen]) {
return nil
@@ -212,21 +228,34 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
}
if c.chosenRoute != nil {
err = c.removeRouteFromWireguardPeer(c.chosenRoute.Peer)
if err != nil {
return err
// If a previous route exists, remove it from the peer
if err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer); err != nil {
return fmt.Errorf("remove route from peer: %v", err)
}
} else {
err = addToRouteTableIfNoExists(c.network, c.wgInterface.Address().IP.String())
if err != nil {
// otherwise add the route to the system
if err := addToRouteTableIfNoExists(c.network, c.wgInterface.Address().IP.String(), c.wgInterface.Name()); 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())
state, err := c.statusRecorder.GetPeer(c.chosenRoute.Peer)
if err != nil {
log.Errorf("Failed to get peer state: %v", err)
} else {
if state.Routes == nil {
state.Routes = map[string]struct{}{}
}
state.Routes[c.network.String()] = struct{}{}
if err := c.statusRecorder.UpdatePeerState(state); err != nil {
log.Warnf("Failed to update peer state: %v", err)
}
}
if err := c.wgInterface.AddAllowedIP(c.chosenRoute.Peer, c.network.String()); err != nil {
log.Errorf("couldn't add allowed IP %s added for peer %s, err: %v",
c.network, c.chosenRoute.Peer, err)
}
@@ -267,21 +296,21 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() {
log.Debugf("stopping watcher for network %s", c.network)
err := c.removeRouteFromPeerAndSystem()
if err != nil {
log.Error(err)
log.Errorf("Couldn't remove route from peer and system for network %s: %v", c.network, err)
}
return
case <-c.peerStateUpdate:
err := c.recalculateRouteAndUpdatePeerAndSystem()
if err != nil {
log.Error(err)
log.Errorf("Couldn't recalculate route and update peer and system: %v", err)
}
case update := <-c.routeUpdate:
if update.updateSerial < c.updateSerial {
log.Warnf("received a routes update with smaller serial number, ignoring it")
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)
log.Debugf("Received a new client network route update for %s", c.network)
c.handleUpdate(update)
@@ -289,7 +318,7 @@ func (c *clientNetwork) peersStateAndUpdateWatcher() {
err := c.recalculateRouteAndUpdatePeerAndSystem()
if err != nil {
log.Error(err)
log.Errorf("Couldn't recalculate route and update peer and system for network %s: %v", c.network, err)
}
c.startPeersStatusChangeWatcher()

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