Skip to content

Commit daa8380

Browse files
authored
[client] Feature/lazy connection (#3379)
With the lazy connection feature, the peer will connect to target peers on-demand. The trigger can be any IP traffic. This feature can be enabled with the NB_ENABLE_EXPERIMENTAL_LAZY_CONN environment variable. When the engine receives a network map, it binds a free UDP port for every remote peer, and the system configures WireGuard endpoints for these ports. When traffic appears on a UDP socket, the system removes this listener and starts the peer connection procedure immediately. Key changes Fix slow netbird status -d command Move from engine.go file to conn_mgr.go the peer connection related code Refactor the iface interface usage and moved interface file next to the engine code Add new command line flag and UI option to enable feature The peer.Conn struct is reusable after it has been closed. Change connection states Connection states Idle: The peer is not attempting to establish a connection. This typically means it's in a lazy state or the remote peer is expired. Connecting: The peer is actively trying to establish a connection. This occurs when the peer has entered an active state and is continuously attempting to reach the remote peer. Connected: A successful peer-to-peer connection has been established and communication is active.
1 parent 4785f23 commit daa8380

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+3264
-1476
lines changed

client/cmd/root.go

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,23 @@ import (
2626
)
2727

2828
const (
29-
externalIPMapFlag = "external-ip-map"
30-
dnsResolverAddress = "dns-resolver-address"
31-
enableRosenpassFlag = "enable-rosenpass"
32-
rosenpassPermissiveFlag = "rosenpass-permissive"
33-
preSharedKeyFlag = "preshared-key"
34-
interfaceNameFlag = "interface-name"
35-
wireguardPortFlag = "wireguard-port"
36-
networkMonitorFlag = "network-monitor"
37-
disableAutoConnectFlag = "disable-auto-connect"
38-
serverSSHAllowedFlag = "allow-server-ssh"
39-
extraIFaceBlackListFlag = "extra-iface-blacklist"
40-
dnsRouteIntervalFlag = "dns-router-interval"
41-
systemInfoFlag = "system-info"
42-
blockLANAccessFlag = "block-lan-access"
43-
uploadBundle = "upload-bundle"
44-
uploadBundleURL = "upload-bundle-url"
29+
externalIPMapFlag = "external-ip-map"
30+
dnsResolverAddress = "dns-resolver-address"
31+
enableRosenpassFlag = "enable-rosenpass"
32+
rosenpassPermissiveFlag = "rosenpass-permissive"
33+
preSharedKeyFlag = "preshared-key"
34+
interfaceNameFlag = "interface-name"
35+
wireguardPortFlag = "wireguard-port"
36+
networkMonitorFlag = "network-monitor"
37+
disableAutoConnectFlag = "disable-auto-connect"
38+
serverSSHAllowedFlag = "allow-server-ssh"
39+
extraIFaceBlackListFlag = "extra-iface-blacklist"
40+
dnsRouteIntervalFlag = "dns-router-interval"
41+
systemInfoFlag = "system-info"
42+
blockLANAccessFlag = "block-lan-access"
43+
enableLazyConnectionFlag = "enable-lazy-connection"
44+
uploadBundle = "upload-bundle"
45+
uploadBundleURL = "upload-bundle-url"
4546
)
4647

4748
var (
@@ -80,6 +81,7 @@ var (
8081
blockLANAccess bool
8182
debugUploadBundle bool
8283
debugUploadBundleURL string
84+
lazyConnEnabled bool
8385

8486
rootCmd = &cobra.Command{
8587
Use: "netbird",
@@ -184,6 +186,7 @@ func init() {
184186
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.")
185187
upCmd.PersistentFlags().BoolVar(&serverSSHAllowed, serverSSHAllowedFlag, false, "Allow SSH server on peer. If enabled, the SSH server will be permitted")
186188
upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.")
189+
upCmd.PersistentFlags().BoolVar(&lazyConnEnabled, enableLazyConnectionFlag, false, "[Experimental] Enable the lazy connection feature. If enabled, the client will establish connections on-demand.")
187190

188191
debugCmd.PersistentFlags().BoolVarP(&debugSystemInfoFlag, systemInfoFlag, "S", true, "Adds system information to the debug bundle")
189192
debugCmd.PersistentFlags().BoolVarP(&debugUploadBundle, uploadBundle, "U", false, fmt.Sprintf("Uploads the debug bundle to a server from URL defined by %s", uploadBundleURL))

client/cmd/status.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func init() {
4444
statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4")
4545
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")
4646
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")
47-
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected")
47+
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(idle|connecting|connected), e.g., --filter-by-status connected")
4848
}
4949

5050
func statusFunc(cmd *cobra.Command, args []string) error {
@@ -127,12 +127,12 @@ func getStatus(ctx context.Context) (*proto.StatusResponse, error) {
127127

128128
func parseFilters() error {
129129
switch strings.ToLower(statusFilter) {
130-
case "", "disconnected", "connected":
130+
case "", "idle", "connecting", "connected":
131131
if strings.ToLower(statusFilter) != "" {
132132
enableDetailFlagWhenFilterFlag()
133133
}
134134
default:
135-
return fmt.Errorf("wrong status filter, should be one of connected|disconnected, got: %s", statusFilter)
135+
return fmt.Errorf("wrong status filter, should be one of connected|connecting|idle, got: %s", statusFilter)
136136
}
137137

138138
if len(ipsFilter) > 0 {

client/cmd/up.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
194194
ic.BlockLANAccess = &blockLANAccess
195195
}
196196

197+
if cmd.Flag(enableLazyConnectionFlag).Changed {
198+
ic.LazyConnectionEnabled = &lazyConnEnabled
199+
}
200+
197201
providedSetupKey, err := getSetupKey()
198202
if err != nil {
199203
return err
@@ -332,6 +336,10 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
332336
loginRequest.BlockLanAccess = &blockLANAccess
333337
}
334338

339+
if cmd.Flag(enableLazyConnectionFlag).Changed {
340+
loginRequest.LazyConnectionEnabled = &lazyConnEnabled
341+
}
342+
335343
var loginErr error
336344

337345
var loginResp *proto.LoginResponse

client/iface/configurer/kernel_unix.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -201,14 +201,30 @@ func (c *KernelConfigurer) configure(config wgtypes.Config) error {
201201
func (c *KernelConfigurer) Close() {
202202
}
203203

204-
func (c *KernelConfigurer) GetStats(peerKey string) (WGStats, error) {
205-
peer, err := c.getPeer(c.deviceName, peerKey)
204+
func (c *KernelConfigurer) GetStats() (map[string]WGStats, error) {
205+
stats := make(map[string]WGStats)
206+
wg, err := wgctrl.New()
207+
if err != nil {
208+
return nil, fmt.Errorf("wgctl: %w", err)
209+
}
210+
defer func() {
211+
err = wg.Close()
212+
if err != nil {
213+
log.Errorf("Got error while closing wgctl: %v", err)
214+
}
215+
}()
216+
217+
wgDevice, err := wg.Device(c.deviceName)
206218
if err != nil {
207-
return WGStats{}, fmt.Errorf("get wireguard stats: %w", err)
219+
return nil, fmt.Errorf("get device %s: %w", c.deviceName, err)
220+
}
221+
222+
for _, peer := range wgDevice.Peers {
223+
stats[peer.PublicKey.String()] = WGStats{
224+
LastHandshake: peer.LastHandshakeTime,
225+
TxBytes: peer.TransmitBytes,
226+
RxBytes: peer.ReceiveBytes,
227+
}
208228
}
209-
return WGStats{
210-
LastHandshake: peer.LastHandshakeTime,
211-
TxBytes: peer.TransmitBytes,
212-
RxBytes: peer.ReceiveBytes,
213-
}, nil
229+
return stats, nil
214230
}

client/iface/configurer/usp.go

Lines changed: 70 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package configurer
22

33
import (
4+
"encoding/base64"
45
"encoding/hex"
56
"fmt"
67
"net"
@@ -17,6 +18,13 @@ import (
1718
nbnet "github.com/netbirdio/netbird/util/net"
1819
)
1920

21+
const (
22+
ipcKeyLastHandshakeTimeSec = "last_handshake_time_sec"
23+
ipcKeyLastHandshakeTimeNsec = "last_handshake_time_nsec"
24+
ipcKeyTxBytes = "tx_bytes"
25+
ipcKeyRxBytes = "rx_bytes"
26+
)
27+
2028
var ErrAllowedIPNotFound = fmt.Errorf("allowed IP not found")
2129

2230
type WGUSPConfigurer struct {
@@ -217,91 +225,75 @@ func (t *WGUSPConfigurer) Close() {
217225
}
218226
}
219227

220-
func (t *WGUSPConfigurer) GetStats(peerKey string) (WGStats, error) {
228+
func (t *WGUSPConfigurer) GetStats() (map[string]WGStats, error) {
221229
ipc, err := t.device.IpcGet()
222230
if err != nil {
223-
return WGStats{}, fmt.Errorf("ipc get: %w", err)
224-
}
225-
226-
stats, err := findPeerInfo(ipc, peerKey, []string{
227-
"last_handshake_time_sec",
228-
"last_handshake_time_nsec",
229-
"tx_bytes",
230-
"rx_bytes",
231-
})
232-
if err != nil {
233-
return WGStats{}, fmt.Errorf("find peer info: %w", err)
234-
}
235-
236-
sec, err := strconv.ParseInt(stats["last_handshake_time_sec"], 10, 64)
237-
if err != nil {
238-
return WGStats{}, fmt.Errorf("parse handshake sec: %w", err)
239-
}
240-
nsec, err := strconv.ParseInt(stats["last_handshake_time_nsec"], 10, 64)
241-
if err != nil {
242-
return WGStats{}, fmt.Errorf("parse handshake nsec: %w", err)
243-
}
244-
txBytes, err := strconv.ParseInt(stats["tx_bytes"], 10, 64)
245-
if err != nil {
246-
return WGStats{}, fmt.Errorf("parse tx_bytes: %w", err)
247-
}
248-
rxBytes, err := strconv.ParseInt(stats["rx_bytes"], 10, 64)
249-
if err != nil {
250-
return WGStats{}, fmt.Errorf("parse rx_bytes: %w", err)
231+
return nil, fmt.Errorf("ipc get: %w", err)
251232
}
252233

253-
return WGStats{
254-
LastHandshake: time.Unix(sec, nsec),
255-
TxBytes: txBytes,
256-
RxBytes: rxBytes,
257-
}, nil
234+
return parseTransfers(ipc)
258235
}
259236

260-
func findPeerInfo(ipcInput string, peerKey string, searchConfigKeys []string) (map[string]string, error) {
261-
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
262-
if err != nil {
263-
return nil, fmt.Errorf("parse key: %w", err)
264-
}
265-
266-
hexKey := hex.EncodeToString(peerKeyParsed[:])
267-
268-
lines := strings.Split(ipcInput, "\n")
269-
270-
configFound := map[string]string{}
271-
foundPeer := false
237+
func parseTransfers(ipc string) (map[string]WGStats, error) {
238+
stats := make(map[string]WGStats)
239+
var (
240+
currentKey string
241+
currentStats WGStats
242+
hasPeer bool
243+
)
244+
lines := strings.Split(ipc, "\n")
272245
for _, line := range lines {
273246
line = strings.TrimSpace(line)
274247

275248
// If we're within the details of the found peer and encounter another public key,
276249
// this means we're starting another peer's details. So, stop.
277-
if strings.HasPrefix(line, "public_key=") && foundPeer {
278-
break
250+
if strings.HasPrefix(line, "public_key=") {
251+
peerID := strings.TrimPrefix(line, "public_key=")
252+
h, err := hex.DecodeString(peerID)
253+
if err != nil {
254+
return nil, fmt.Errorf("decode peerID: %w", err)
255+
}
256+
currentKey = base64.StdEncoding.EncodeToString(h)
257+
currentStats = WGStats{} // Reset stats for the new peer
258+
hasPeer = true
259+
stats[currentKey] = currentStats
260+
continue
279261
}
280262

281-
// Identify the peer with the specific public key
282-
if line == fmt.Sprintf("public_key=%s", hexKey) {
283-
foundPeer = true
263+
if !hasPeer {
264+
continue
284265
}
285266

286-
for _, key := range searchConfigKeys {
287-
if foundPeer && strings.HasPrefix(line, key+"=") {
288-
v := strings.SplitN(line, "=", 2)
289-
configFound[v[0]] = v[1]
290-
}
267+
key := strings.SplitN(line, "=", 2)
268+
if len(key) != 2 {
269+
continue
291270
}
292-
}
293-
294-
// todo: use multierr
295-
for _, key := range searchConfigKeys {
296-
if _, ok := configFound[key]; !ok {
297-
return configFound, fmt.Errorf("config key not found: %s", key)
271+
switch key[0] {
272+
case ipcKeyLastHandshakeTimeSec:
273+
hs, err := toLastHandshake(key[1])
274+
if err != nil {
275+
return nil, err
276+
}
277+
currentStats.LastHandshake = hs
278+
stats[currentKey] = currentStats
279+
case ipcKeyRxBytes:
280+
rxBytes, err := toBytes(key[1])
281+
if err != nil {
282+
return nil, fmt.Errorf("parse rx_bytes: %w", err)
283+
}
284+
currentStats.RxBytes = rxBytes
285+
stats[currentKey] = currentStats
286+
case ipcKeyTxBytes:
287+
TxBytes, err := toBytes(key[1])
288+
if err != nil {
289+
return nil, fmt.Errorf("parse tx_bytes: %w", err)
290+
}
291+
currentStats.TxBytes = TxBytes
292+
stats[currentKey] = currentStats
298293
}
299294
}
300-
if !foundPeer {
301-
return nil, fmt.Errorf("%w: %s", ErrPeerNotFound, peerKey)
302-
}
303295

304-
return configFound, nil
296+
return stats, nil
305297
}
306298

307299
func toWgUserspaceString(wgCfg wgtypes.Config) string {
@@ -355,6 +347,18 @@ func toWgUserspaceString(wgCfg wgtypes.Config) string {
355347
return sb.String()
356348
}
357349

350+
func toLastHandshake(stringVar string) (time.Time, error) {
351+
sec, err := strconv.ParseInt(stringVar, 10, 64)
352+
if err != nil {
353+
return time.Time{}, fmt.Errorf("parse handshake sec: %w", err)
354+
}
355+
return time.Unix(sec, 0), nil
356+
}
357+
358+
func toBytes(s string) (int64, error) {
359+
return strconv.ParseInt(s, 10, 64)
360+
}
361+
358362
func getFwmark() int {
359363
if nbnet.AdvancedRouting() {
360364
return nbnet.ControlPlaneMark

0 commit comments

Comments
 (0)