Compare commits

...

3 Commits

Author SHA1 Message Date
Maycon Santos
355bab9bb4 set command with cursor 2025-07-02 12:20:43 +02:00
Carlos Hernandez
6922826919 [client] Support fullstatus without probes (#4052) 2025-07-02 10:42:47 +02:00
Maycon Santos
56a1a75e3f [client] Support random wireguard port on client (#4085)
Adds support for using a random available WireGuard port when the user specifies port `0`.

- Updates `freePort` logic to bind to the requested port (including `0`) without falling back to the default.
- Removes default port assignment in the configuration path, allowing `0` to propagate.
- Adjusts tests to handle dynamically assigned ports when using `0`.
2025-07-02 09:01:02 +02:00
12 changed files with 1936 additions and 538 deletions

View File

@@ -134,3 +134,37 @@ We use open-source technologies like [WireGuard®](https://www.wireguard.com/),
### Legal
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.
## Configuration Management
Netbird now supports direct configuration management via CLI commands:
- You can use `netbird set` as a regular user if the daemon is running; it will securely update the config via the daemon.
- If the daemon is not running, you need write access to the config file (typically requires root).
### Set a configuration value
```
netbird set <setting> <value>
# or using environment variables
NB_INTERFACE_NAME=utun5 netbird set interface-name
```
### Get a configuration value
```
netbird get <setting>
# or using environment variables
NB_INTERFACE_NAME=utun5 netbird get interface-name
```
### Show all configuration values
```
netbird show
```
- All settings support environment variable overrides: `NB_<SETTING>` or `WT_<SETTING>` (e.g. `NB_ENABLE_ROSENPASS=true`).
- Supported settings: management-url, admin-url, interface-name, external-ip-map, extra-iface-blacklist, dns-resolver-address, extra-dns-labels, preshared-key, enable-rosenpass, rosenpass-permissive, allow-server-ssh, network-monitor, disable-auto-connect, disable-client-routes, disable-server-routes, disable-dns, disable-firewall, block-lan-access, block-inbound, enable-lazy-connection, wireguard-port, dns-router-interval.
See `netbird set --help`, `netbird get --help`, and `netbird show --help` for more details.

View File

@@ -22,6 +22,7 @@ import (
"google.golang.org/grpc/credentials/insecure"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/upload-server/types"
)
@@ -87,6 +88,30 @@ var (
Long: "",
SilenceUsage: true,
}
getCmd = &cobra.Command{
Use: "get <setting>",
Short: "Get a configuration value from the config file",
Long: `Get a configuration value from the Netbird config file. You can also use NB_<SETTING> or WT_<SETTING> environment variables to override the value (same as 'set').`,
Args: cobra.ExactArgs(1),
RunE: getFunc,
}
showCmd = &cobra.Command{
Use: "show",
Short: "Show all configuration values",
Long: `Show all configuration values from the Netbird config file, with environment variable overrides if present.`,
Args: cobra.NoArgs,
RunE: showFunc,
}
reloadCmd = &cobra.Command{
Use: "reload",
Short: "Reload the configuration in the daemon (daemon mode)",
Long: `Reload the configuration from disk in the running daemon. Use after 'set' to apply changes without restarting the service.`,
Args: cobra.NoArgs,
RunE: reloadFunc,
}
)
// Execute executes the root command.
@@ -152,6 +177,9 @@ func init() {
rootCmd.AddCommand(networksCMD)
rootCmd.AddCommand(forwardingRulesCmd)
rootCmd.AddCommand(debugCmd)
rootCmd.AddCommand(getCmd)
rootCmd.AddCommand(showCmd)
rootCmd.AddCommand(reloadCmd)
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
@@ -408,3 +436,167 @@ func getClient(cmd *cobra.Command) (*grpc.ClientConn, error) {
return conn, nil
}
func getFunc(cmd *cobra.Command, args []string) error {
setting := args[0]
upper := strings.ToUpper(strings.ReplaceAll(setting, "-", "_"))
if v, ok := os.LookupEnv("NB_" + upper); ok {
cmd.Println(v)
return nil
} else if v, ok := os.LookupEnv("WT_" + upper); ok {
cmd.Println(v)
return nil
}
config, err := internal.ReadConfig(configPath)
if err != nil {
return fmt.Errorf("failed to read config: %v", err)
}
switch setting {
case "management-url":
cmd.Println(config.ManagementURL.String())
case "admin-url":
cmd.Println(config.AdminURL.String())
case "interface-name":
cmd.Println(config.WgIface)
case "external-ip-map":
cmd.Println(strings.Join(config.NATExternalIPs, ","))
case "extra-iface-blacklist":
cmd.Println(strings.Join(config.IFaceBlackList, ","))
case "dns-resolver-address":
cmd.Println(config.CustomDNSAddress)
case "extra-dns-labels":
cmd.Println(config.DNSLabels.SafeString())
case "preshared-key":
cmd.Println(config.PreSharedKey)
case "enable-rosenpass":
cmd.Println(config.RosenpassEnabled)
case "rosenpass-permissive":
cmd.Println(config.RosenpassPermissive)
case "allow-server-ssh":
if config.ServerSSHAllowed != nil {
cmd.Println(*config.ServerSSHAllowed)
} else {
cmd.Println(false)
}
case "network-monitor":
if config.NetworkMonitor != nil {
cmd.Println(*config.NetworkMonitor)
} else {
cmd.Println(false)
}
case "disable-auto-connect":
cmd.Println(config.DisableAutoConnect)
case "disable-client-routes":
cmd.Println(config.DisableClientRoutes)
case "disable-server-routes":
cmd.Println(config.DisableServerRoutes)
case "disable-dns":
cmd.Println(config.DisableDNS)
case "disable-firewall":
cmd.Println(config.DisableFirewall)
case "block-lan-access":
cmd.Println(config.BlockLANAccess)
case "block-inbound":
cmd.Println(config.BlockInbound)
case "enable-lazy-connection":
cmd.Println(config.LazyConnectionEnabled)
case "wireguard-port":
cmd.Println(config.WgPort)
case "dns-router-interval":
cmd.Println(config.DNSRouteInterval)
default:
return fmt.Errorf("unknown setting: %s", setting)
}
return nil
}
func showFunc(cmd *cobra.Command, args []string) error {
config, err := internal.ReadConfig(configPath)
if err != nil {
return fmt.Errorf("failed to read config: %v", err)
}
settings := []string{
"management-url", "admin-url", "interface-name", "external-ip-map", "extra-iface-blacklist", "dns-resolver-address", "extra-dns-labels", "preshared-key", "enable-rosenpass", "rosenpass-permissive", "allow-server-ssh", "network-monitor", "disable-auto-connect", "disable-client-routes", "disable-server-routes", "disable-dns", "disable-firewall", "block-lan-access", "block-inbound", "enable-lazy-connection", "wireguard-port", "dns-router-interval",
}
for _, setting := range settings {
upper := strings.ToUpper(strings.ReplaceAll(setting, "-", "_"))
var val string
if v, ok := os.LookupEnv("NB_" + upper); ok {
val = v + " (from NB_ env)"
} else if v, ok := os.LookupEnv("WT_" + upper); ok {
val = v + " (from WT_ env)"
} else {
switch setting {
case "management-url":
val = config.ManagementURL.String()
case "admin-url":
val = config.AdminURL.String()
case "interface-name":
val = config.WgIface
case "external-ip-map":
val = strings.Join(config.NATExternalIPs, ",")
case "extra-iface-blacklist":
val = strings.Join(config.IFaceBlackList, ",")
case "dns-resolver-address":
val = config.CustomDNSAddress
case "extra-dns-labels":
val = config.DNSLabels.SafeString()
case "preshared-key":
val = config.PreSharedKey
case "enable-rosenpass":
val = fmt.Sprintf("%v", config.RosenpassEnabled)
case "rosenpass-permissive":
val = fmt.Sprintf("%v", config.RosenpassPermissive)
case "allow-server-ssh":
if config.ServerSSHAllowed != nil {
val = fmt.Sprintf("%v", *config.ServerSSHAllowed)
} else {
val = "false"
}
case "network-monitor":
if config.NetworkMonitor != nil {
val = fmt.Sprintf("%v", *config.NetworkMonitor)
} else {
val = "false"
}
case "disable-auto-connect":
val = fmt.Sprintf("%v", config.DisableAutoConnect)
case "disable-client-routes":
val = fmt.Sprintf("%v", config.DisableClientRoutes)
case "disable-server-routes":
val = fmt.Sprintf("%v", config.DisableServerRoutes)
case "disable-dns":
val = fmt.Sprintf("%v", config.DisableDNS)
case "disable-firewall":
val = fmt.Sprintf("%v", config.DisableFirewall)
case "block-lan-access":
val = fmt.Sprintf("%v", config.BlockLANAccess)
case "block-inbound":
val = fmt.Sprintf("%v", config.BlockInbound)
case "enable-lazy-connection":
val = fmt.Sprintf("%v", config.LazyConnectionEnabled)
case "wireguard-port":
val = fmt.Sprintf("%d", config.WgPort)
case "dns-router-interval":
val = config.DNSRouteInterval.String()
}
}
cmd.Printf("%-22s: %s\n", setting, val)
}
return nil
}
func reloadFunc(cmd *cobra.Command, args []string) error {
conn, err := getClient(cmd)
if err != nil {
return err
}
defer conn.Close()
client := proto.NewDaemonServiceClient(conn)
_, err = client.ReloadConfig(cmd.Context(), &proto.ReloadConfigRequest{})
if err != nil {
return fmt.Errorf("failed to reload config in daemon: %v", err)
}
cmd.Println("Configuration reloaded in daemon.")
return nil
}

475
client/cmd/set.go Normal file
View File

@@ -0,0 +1,475 @@
package cmd
import (
"fmt"
"os"
osuser "os/user"
"strings"
"time"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/management/domain"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
)
var setCmd = &cobra.Command{
Use: "set <setting> <value>",
Short: "Set a configuration value without running up",
Long: `Set a configuration value in the Netbird config file without running 'up'.
You can also set values via environment variables NB_<SETTING> or WT_<SETTING> (e.g. NB_INTERFACE_NAME=utun5 netbird set interface-name).
Supported settings:
management-url (string) e.g. https://api.netbird.io:443
admin-url (string) e.g. https://app.netbird.io:443
interface-name (string) e.g. utun5
external-ip-map (list) comma-separated, e.g. 12.34.56.78,12.34.56.79/eth0
extra-iface-blacklist (list) comma-separated, e.g. eth1,eth2
dns-resolver-address (string) e.g. 127.0.0.1:5053
extra-dns-labels (list) comma-separated, e.g. vpc1,mgmt1
preshared-key (string)
enable-rosenpass (bool) true/false
rosenpass-permissive (bool) true/false
allow-server-ssh (bool) true/false
network-monitor (bool) true/false
disable-auto-connect (bool) true/false
disable-client-routes (bool) true/false
disable-server-routes (bool) true/false
disable-dns (bool) true/false
disable-firewall (bool) true/false
block-lan-access (bool) true/false
block-inbound (bool) true/false
enable-lazy-connection (bool) true/false
wireguard-port (int) e.g. 51820
dns-router-interval (duration) e.g. 1m, 30s
Examples:
NB_INTERFACE_NAME=utun5 netbird set interface-name
netbird set wireguard-port 51820
netbird set external-ip-map 12.34.56.78,12.34.56.79/eth0
netbird set enable-rosenpass true
netbird set dns-router-interval 2m
netbird set extra-dns-labels vpc1,mgmt1
netbird set disable-firewall true
`,
Args: cobra.ExactArgs(2),
RunE: setFunc,
}
func init() {
rootCmd.AddCommand(setCmd)
}
func setFunc(cmd *cobra.Command, args []string) error {
setting := args[0]
var value string
// Check environment variables first
upper := strings.ToUpper(strings.ReplaceAll(setting, "-", "_"))
if v, ok := os.LookupEnv("NB_" + upper); ok {
value = v
} else if v, ok := os.LookupEnv("WT_" + upper); ok {
value = v
} else {
if len(args) < 2 {
return fmt.Errorf("missing value for setting %s", setting)
}
value = args[1]
}
// If not root, try to use the daemon (only if cmd is not nil)
if cmd != nil {
if u, err := osuser.Current(); err == nil && u.Uid != "0" {
conn, err := getClient(cmd)
if err == nil {
defer conn.Close()
client := proto.NewDaemonServiceClient(conn)
_, err = client.SetConfigValue(cmd.Context(), &proto.SetConfigValueRequest{Setting: setting, Value: value})
if err == nil {
if cmd != nil {
cmd.Println("Configuration updated via daemon.")
} else {
fmt.Println("Configuration updated via daemon.")
}
return nil
}
if s, ok := status.FromError(err); ok {
return fmt.Errorf("daemon error: %v", s.Message())
}
return fmt.Errorf("failed to update config via daemon: %v", err)
}
// else: fall back to direct file write
}
}
switch setting {
case "management-url":
input := internal.ConfigInput{ConfigPath: configPath, ManagementURL: value}
_, err := internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set management-url: %v", err)
}
if cmd != nil {
cmd.Printf("Set management-url to: %s\n", value)
} else {
fmt.Printf("Set management-url to: %s\n", value)
}
case "admin-url":
input := internal.ConfigInput{ConfigPath: configPath, AdminURL: value}
_, err := internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set admin-url: %v", err)
}
if cmd != nil {
cmd.Printf("Set admin-url to: %s\n", value)
} else {
fmt.Printf("Set admin-url to: %s\n", value)
}
case "interface-name":
if err := parseInterfaceName(value); err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, InterfaceName: &value}
_, err := internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set interface-name: %v", err)
}
if cmd != nil {
cmd.Printf("Set interface-name to: %s\n", value)
} else {
fmt.Printf("Set interface-name to: %s\n", value)
}
case "external-ip-map":
var ips []string
if value == "" {
ips = []string{}
} else {
ips = strings.Split(value, ",")
}
if err := validateNATExternalIPs(ips); err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, NATExternalIPs: ips}
_, err := internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set external-ip-map: %v", err)
}
if cmd != nil {
cmd.Printf("Set external-ip-map to: %v\n", ips)
} else {
fmt.Printf("Set external-ip-map to: %v\n", ips)
}
case "extra-iface-blacklist":
var ifaces []string
if value == "" {
ifaces = []string{}
} else {
ifaces = strings.Split(value, ",")
}
input := internal.ConfigInput{ConfigPath: configPath, ExtraIFaceBlackList: ifaces}
_, err := internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set extra-iface-blacklist: %v", err)
}
if cmd != nil {
cmd.Printf("Set extra-iface-blacklist to: %v\n", ifaces)
} else {
fmt.Printf("Set extra-iface-blacklist to: %v\n", ifaces)
}
case "dns-resolver-address":
if value != "" && !isValidAddrPort(value) {
return fmt.Errorf("%s is invalid, it should be formatted as IP:Port string or as an empty string like \"\"", value)
}
input := internal.ConfigInput{ConfigPath: configPath, CustomDNSAddress: []byte(value)}
_, err := internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set dns-resolver-address: %v", err)
}
if cmd != nil {
cmd.Printf("Set dns-resolver-address to: %s\n", value)
} else {
fmt.Printf("Set dns-resolver-address to: %s\n", value)
}
case "extra-dns-labels":
var labels []string
if value == "" {
labels = []string{}
} else {
labels = strings.Split(value, ",")
}
domains, err := domain.ValidateDomains(labels)
if err != nil {
return fmt.Errorf("invalid DNS labels: %v", err)
}
input := internal.ConfigInput{ConfigPath: configPath, DNSLabels: domains}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set extra-dns-labels: %v", err)
}
if cmd != nil {
cmd.Printf("Set extra-dns-labels to: %v\n", labels)
} else {
fmt.Printf("Set extra-dns-labels to: %v\n", labels)
}
case "preshared-key":
input := internal.ConfigInput{ConfigPath: configPath, PreSharedKey: &value}
_, err := internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set preshared-key: %v", err)
}
if cmd != nil {
cmd.Printf("Set preshared-key to: %s\n", value)
} else {
fmt.Printf("Set preshared-key to: %s\n", value)
}
case "hostname":
// Hostname is not persisted in config, so just print a warning
if cmd != nil {
cmd.Printf("Warning: hostname is not persisted in config. Use --hostname with up command.\n")
} else {
fmt.Printf("Warning: hostname is not persisted in config. Use --hostname with up command.\n")
}
case "enable-rosenpass":
b, err := parseBool(value)
if err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, RosenpassEnabled: &b}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set enable-rosenpass: %v", err)
}
if cmd != nil {
cmd.Printf("Set enable-rosenpass to: %v\n", b)
} else {
fmt.Printf("Set enable-rosenpass to: %v\n", b)
}
case "rosenpass-permissive":
b, err := parseBool(value)
if err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, RosenpassPermissive: &b}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set rosenpass-permissive: %v", err)
}
if cmd != nil {
cmd.Printf("Set rosenpass-permissive to: %v\n", b)
} else {
fmt.Printf("Set rosenpass-permissive to: %v\n", b)
}
case "allow-server-ssh":
b, err := parseBool(value)
if err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, ServerSSHAllowed: &b}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set allow-server-ssh: %v", err)
}
if cmd != nil {
cmd.Printf("Set allow-server-ssh to: %v\n", b)
} else {
fmt.Printf("Set allow-server-ssh to: %v\n", b)
}
case "network-monitor":
b, err := parseBool(value)
if err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, NetworkMonitor: &b}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set network-monitor: %v", err)
}
if cmd != nil {
cmd.Printf("Set network-monitor to: %v\n", b)
} else {
fmt.Printf("Set network-monitor to: %v\n", b)
}
case "disable-auto-connect":
b, err := parseBool(value)
if err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, DisableAutoConnect: &b}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set disable-auto-connect: %v", err)
}
if cmd != nil {
cmd.Printf("Set disable-auto-connect to: %v\n", b)
} else {
fmt.Printf("Set disable-auto-connect to: %v\n", b)
}
case "disable-client-routes":
b, err := parseBool(value)
if err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, DisableClientRoutes: &b}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set disable-client-routes: %v", err)
}
if cmd != nil {
cmd.Printf("Set disable-client-routes to: %v\n", b)
} else {
fmt.Printf("Set disable-client-routes to: %v\n", b)
}
case "disable-server-routes":
b, err := parseBool(value)
if err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, DisableServerRoutes: &b}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set disable-server-routes: %v", err)
}
if cmd != nil {
cmd.Printf("Set disable-server-routes to: %v\n", b)
} else {
fmt.Printf("Set disable-server-routes to: %v\n", b)
}
case "disable-dns":
b, err := parseBool(value)
if err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, DisableDNS: &b}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set disable-dns: %v", err)
}
if cmd != nil {
cmd.Printf("Set disable-dns to: %v\n", b)
} else {
fmt.Printf("Set disable-dns to: %v\n", b)
}
case "disable-firewall":
b, err := parseBool(value)
if err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, DisableFirewall: &b}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set disable-firewall: %v", err)
}
if cmd != nil {
cmd.Printf("Set disable-firewall to: %v\n", b)
} else {
fmt.Printf("Set disable-firewall to: %v\n", b)
}
case "block-lan-access":
b, err := parseBool(value)
if err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, BlockLANAccess: &b}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set block-lan-access: %v", err)
}
if cmd != nil {
cmd.Printf("Set block-lan-access to: %v\n", b)
} else {
fmt.Printf("Set block-lan-access to: %v\n", b)
}
case "block-inbound":
b, err := parseBool(value)
if err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, BlockInbound: &b}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set block-inbound: %v", err)
}
if cmd != nil {
cmd.Printf("Set block-inbound to: %v\n", b)
} else {
fmt.Printf("Set block-inbound to: %v\n", b)
}
case "enable-lazy-connection":
b, err := parseBool(value)
if err != nil {
return err
}
input := internal.ConfigInput{ConfigPath: configPath, LazyConnectionEnabled: &b}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set enable-lazy-connection: %v", err)
}
if cmd != nil {
cmd.Printf("Set enable-lazy-connection to: %v\n", b)
} else {
fmt.Printf("Set enable-lazy-connection to: %v\n", b)
}
case "wireguard-port":
p, err := parseUint16(value)
if err != nil {
return err
}
pi := int(p)
input := internal.ConfigInput{ConfigPath: configPath, WireguardPort: &pi}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set wireguard-port: %v", err)
}
if cmd != nil {
cmd.Printf("Set wireguard-port to: %d\n", p)
} else {
fmt.Printf("Set wireguard-port to: %d\n", p)
}
case "dns-router-interval":
d, err := time.ParseDuration(value)
if err != nil {
return fmt.Errorf("invalid duration: %v", err)
}
input := internal.ConfigInput{ConfigPath: configPath, DNSRouteInterval: &d}
_, err = internal.UpdateOrCreateConfig(input)
if err != nil {
return fmt.Errorf("failed to set dns-router-interval: %v", err)
}
if cmd != nil {
cmd.Printf("Set dns-router-interval to: %s\n", d)
} else {
fmt.Printf("Set dns-router-interval to: %s\n", d)
}
default:
return fmt.Errorf("unknown setting: %s", setting)
}
if cmd != nil {
cmd.Println("Configuration updated successfully.")
} else {
fmt.Println("Configuration updated successfully.")
}
return nil
}
func parseBool(val string) (bool, error) {
v := strings.ToLower(val)
if v == "true" || v == "1" {
return true, nil
}
if v == "false" || v == "0" {
return false, nil
}
return false, fmt.Errorf("invalid boolean value: %s", val)
}
func parseUint16(val string) (uint16, error) {
var p uint16
_, err := fmt.Sscanf(val, "%d", &p)
if err != nil {
return 0, fmt.Errorf("invalid uint16 value: %s", val)
}
return p, nil
}

162
client/cmd/set_test.go Normal file
View File

@@ -0,0 +1,162 @@
package cmd
import (
"os"
"strings"
"testing"
"time"
"github.com/netbirdio/netbird/client/internal"
"github.com/stretchr/testify/require"
)
func TestSetCommand_AllSettings(t *testing.T) {
tempFile, err := os.CreateTemp("", "config.json")
require.NoError(t, err)
defer os.Remove(tempFile.Name())
// Write empty JSON object to the config file to avoid JSON parse errors
_, err = tempFile.WriteString("{}")
require.NoError(t, err)
tempFile.Close()
configPath = tempFile.Name()
tests := []struct {
setting string
value string
verify func(*testing.T, *internal.Config)
wantErr bool
}{
{"management-url", "https://test.mgmt:443", func(t *testing.T, c *internal.Config) {
require.Equal(t, "https://test.mgmt:443", c.ManagementURL.String())
}, false},
{"admin-url", "https://test.admin:443", func(t *testing.T, c *internal.Config) {
require.Equal(t, "https://test.admin:443", c.AdminURL.String())
}, false},
{"interface-name", "utun99", func(t *testing.T, c *internal.Config) {
require.Equal(t, "utun99", c.WgIface)
}, false},
{"external-ip-map", "12.34.56.78,12.34.56.79", func(t *testing.T, c *internal.Config) {
require.Equal(t, []string{"12.34.56.78", "12.34.56.79"}, c.NATExternalIPs)
}, false},
{"extra-iface-blacklist", "eth1,eth2", func(t *testing.T, c *internal.Config) {
require.Contains(t, c.IFaceBlackList, "eth1")
require.Contains(t, c.IFaceBlackList, "eth2")
}, false},
{"dns-resolver-address", "127.0.0.1:5053", func(t *testing.T, c *internal.Config) {
require.Equal(t, "127.0.0.1:5053", c.CustomDNSAddress)
}, false},
{"extra-dns-labels", "vpc1,mgmt1", func(t *testing.T, c *internal.Config) {
require.True(t, strings.Contains(c.DNSLabels.SafeString(), "vpc1"))
require.True(t, strings.Contains(c.DNSLabels.SafeString(), "mgmt1"))
}, false},
{"preshared-key", "testkey", func(t *testing.T, c *internal.Config) {
require.Equal(t, "testkey", c.PreSharedKey)
}, false},
{"enable-rosenpass", "true", func(t *testing.T, c *internal.Config) {
require.True(t, c.RosenpassEnabled)
}, false},
{"rosenpass-permissive", "false", func(t *testing.T, c *internal.Config) {
require.False(t, c.RosenpassPermissive)
}, false},
{"allow-server-ssh", "true", func(t *testing.T, c *internal.Config) {
require.NotNil(t, c.ServerSSHAllowed)
require.True(t, *c.ServerSSHAllowed)
}, false},
{"network-monitor", "false", func(t *testing.T, c *internal.Config) {
require.NotNil(t, c.NetworkMonitor)
require.False(t, *c.NetworkMonitor)
}, false},
{"disable-auto-connect", "true", func(t *testing.T, c *internal.Config) {
require.True(t, c.DisableAutoConnect)
}, false},
{"disable-client-routes", "false", func(t *testing.T, c *internal.Config) {
require.False(t, c.DisableClientRoutes)
}, false},
{"disable-server-routes", "true", func(t *testing.T, c *internal.Config) {
require.True(t, c.DisableServerRoutes)
}, false},
{"disable-dns", "false", func(t *testing.T, c *internal.Config) {
require.False(t, c.DisableDNS)
}, false},
{"disable-firewall", "true", func(t *testing.T, c *internal.Config) {
require.True(t, c.DisableFirewall)
}, false},
{"block-lan-access", "true", func(t *testing.T, c *internal.Config) {
require.True(t, c.BlockLANAccess)
}, false},
{"block-inbound", "false", func(t *testing.T, c *internal.Config) {
require.False(t, c.BlockInbound)
}, false},
{"enable-lazy-connection", "true", func(t *testing.T, c *internal.Config) {
require.True(t, c.LazyConnectionEnabled)
}, false},
{"wireguard-port", "51820", func(t *testing.T, c *internal.Config) {
require.Equal(t, 51820, c.WgPort)
}, false},
{"dns-router-interval", "2m", func(t *testing.T, c *internal.Config) {
require.Equal(t, 2*time.Minute, c.DNSRouteInterval)
}, false},
// Invalid cases
{"enable-rosenpass", "notabool", nil, true},
{"wireguard-port", "notanint", nil, true},
{"dns-router-interval", "notaduration", nil, true},
}
for _, tt := range tests {
t.Run(tt.setting+"="+tt.value, func(t *testing.T) {
args := []string{tt.setting, tt.value}
err := setFunc(nil, args)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
config, err := internal.ReadConfig(configPath)
require.NoError(t, err)
if tt.verify != nil {
tt.verify(t, config)
}
})
}
}
func TestSetCommand_EnvVars(t *testing.T) {
tempFile, err := os.CreateTemp("", "config.json")
require.NoError(t, err)
defer os.Remove(tempFile.Name())
// Write empty JSON object to the config file to avoid JSON parse errors
_, err = tempFile.WriteString("{}")
require.NoError(t, err)
tempFile.Close()
configPath = tempFile.Name()
os.Setenv("NB_INTERFACE_NAME", "utun77")
defer os.Unsetenv("NB_INTERFACE_NAME")
args := []string{"interface-name", "utun99"}
err = setFunc(nil, args)
require.NoError(t, err)
config, err := internal.ReadConfig(configPath)
require.NoError(t, err)
require.Equal(t, "utun77", config.WgIface)
os.Unsetenv("NB_INTERFACE_NAME")
os.Setenv("WT_INTERFACE_NAME", "utun88")
defer os.Unsetenv("WT_INTERFACE_NAME")
err = setFunc(nil, args)
require.NoError(t, err)
config, err = internal.ReadConfig(configPath)
require.NoError(t, err)
require.Equal(t, "utun88", config.WgIface)
os.Unsetenv("WT_INTERFACE_NAME")
// No env var, should use CLI value
err = setFunc(nil, args)
require.NoError(t, err)
config, err = internal.ReadConfig(configPath)
require.NoError(t, err)
require.Equal(t, "utun99", config.WgIface)
}

View File

@@ -120,7 +120,7 @@ func getStatus(ctx context.Context) (*proto.StatusResponse, error) {
}
defer conn.Close()
resp, err := proto.NewDaemonServiceClient(conn).Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true})
resp, err := proto.NewDaemonServiceClient(conn).Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true, ShouldRunProbes: true})
if err != nil {
return nil, fmt.Errorf("status failed: %v", status.Convert(err).Message())
}

View File

@@ -319,10 +319,6 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
*input.WireguardPort, config.WgPort)
config.WgPort = *input.WireguardPort
updated = true
} else if config.WgPort == 0 {
config.WgPort = iface.DefaultWgPort
log.Infof("using default Wireguard port %d", config.WgPort)
updated = true
}
if input.InterfaceName != nil && *input.InterfaceName != config.WgIface {

View File

@@ -17,7 +17,6 @@ import (
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/iface/device"
"github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/listener"
@@ -526,17 +525,13 @@ func statusRecorderToSignalConnStateNotifier(statusRecorder *peer.Status) signal
// freePort attempts to determine if the provided port is available, if not it will ask the system for a free port.
func freePort(initPort int) (int, error) {
addr := net.UDPAddr{}
if initPort == 0 {
initPort = iface.DefaultWgPort
}
addr.Port = initPort
addr := net.UDPAddr{Port: initPort}
conn, err := net.ListenUDP("udp", &addr)
if err == nil {
returnPort := conn.LocalAddr().(*net.UDPAddr).Port
closeConnWithLog(conn)
return initPort, nil
return returnPort, nil
}
// if the port is already in use, ask the system for a free port

View File

@@ -13,10 +13,10 @@ func Test_freePort(t *testing.T) {
shouldMatch bool
}{
{
name: "not provided, fallback to default",
name: "when port is 0 use random port",
port: 0,
want: 51820,
shouldMatch: true,
want: 0,
shouldMatch: false,
},
{
name: "provided and available",
@@ -31,7 +31,7 @@ func Test_freePort(t *testing.T) {
shouldMatch: false,
},
}
c1, err := net.ListenUDP("udp", &net.UDPAddr{Port: 51830})
c1, err := net.ListenUDP("udp", &net.UDPAddr{Port: 0})
if err != nil {
t.Errorf("freePort error = %v", err)
}
@@ -39,6 +39,14 @@ func Test_freePort(t *testing.T) {
_ = c1.Close()
}(c1)
if tests[1].port == c1.LocalAddr().(*net.UDPAddr).Port {
tests[1].port++
tests[1].want++
}
tests[2].port = c1.LocalAddr().(*net.UDPAddr).Port
tests[2].want = c1.LocalAddr().(*net.UDPAddr).Port
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

File diff suppressed because it is too large Load Diff

View File

@@ -67,6 +67,12 @@ service DaemonService {
rpc SubscribeEvents(SubscribeRequest) returns (stream SystemEvent) {}
rpc GetEvents(GetEventsRequest) returns (GetEventsResponse) {}
// Reloads the configuration from disk
rpc ReloadConfig(ReloadConfigRequest) returns (ReloadConfigResponse) {}
// Sets a configuration value (for use by regular users via the daemon)
rpc SetConfigValue(SetConfigValueRequest) returns (SetConfigValueResponse) {}
}
@@ -158,6 +164,7 @@ message UpResponse {}
message StatusRequest{
bool getFullPeerStatus = 1;
bool shouldRunProbes = 2;
}
message StatusResponse{
@@ -495,3 +502,12 @@ message GetEventsRequest {}
message GetEventsResponse {
repeated SystemEvent events = 1;
}
message ReloadConfigRequest {}
message ReloadConfigResponse {}
message SetConfigValueRequest {
string setting = 1;
string value = 2;
}
message SetConfigValueResponse {}

View File

@@ -55,6 +55,10 @@ type DaemonServiceClient interface {
TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error)
SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error)
GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error)
// Reloads the configuration from disk
ReloadConfig(ctx context.Context, in *ReloadConfigRequest, opts ...grpc.CallOption) (*ReloadConfigResponse, error)
// Sets a configuration value (for use by regular users via the daemon)
SetConfigValue(ctx context.Context, in *SetConfigValueRequest, opts ...grpc.CallOption) (*SetConfigValueResponse, error)
}
type daemonServiceClient struct {
@@ -268,6 +272,24 @@ func (c *daemonServiceClient) GetEvents(ctx context.Context, in *GetEventsReques
return out, nil
}
func (c *daemonServiceClient) ReloadConfig(ctx context.Context, in *ReloadConfigRequest, opts ...grpc.CallOption) (*ReloadConfigResponse, error) {
out := new(ReloadConfigResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/ReloadConfig", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *daemonServiceClient) SetConfigValue(ctx context.Context, in *SetConfigValueRequest, opts ...grpc.CallOption) (*SetConfigValueResponse, error) {
out := new(SetConfigValueResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/SetConfigValue", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// DaemonServiceServer is the server API for DaemonService service.
// All implementations must embed UnimplementedDaemonServiceServer
// for forward compatibility
@@ -309,6 +331,10 @@ type DaemonServiceServer interface {
TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error)
SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error
GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error)
// Reloads the configuration from disk
ReloadConfig(context.Context, *ReloadConfigRequest) (*ReloadConfigResponse, error)
// Sets a configuration value (for use by regular users via the daemon)
SetConfigValue(context.Context, *SetConfigValueRequest) (*SetConfigValueResponse, error)
mustEmbedUnimplementedDaemonServiceServer()
}
@@ -376,6 +402,12 @@ func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, Daemo
func (UnimplementedDaemonServiceServer) GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetEvents not implemented")
}
func (UnimplementedDaemonServiceServer) ReloadConfig(context.Context, *ReloadConfigRequest) (*ReloadConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReloadConfig not implemented")
}
func (UnimplementedDaemonServiceServer) SetConfigValue(context.Context, *SetConfigValueRequest) (*SetConfigValueResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetConfigValue not implemented")
}
func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {}
// UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service.
@@ -752,6 +784,42 @@ func _DaemonService_GetEvents_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
func _DaemonService_ReloadConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReloadConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).ReloadConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/ReloadConfig",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).ReloadConfig(ctx, req.(*ReloadConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DaemonService_SetConfigValue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetConfigValueRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).SetConfigValue(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/SetConfigValue",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).SetConfigValue(ctx, req.(*SetConfigValueRequest))
}
return interceptor(ctx, in, info, handler)
}
// DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -835,6 +903,14 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetEvents",
Handler: _DaemonService_GetEvents_Handler,
},
{
MethodName: "ReloadConfig",
Handler: _DaemonService_ReloadConfig_Handler,
},
{
MethodName: "SetConfigValue",
Handler: _DaemonService_SetConfigValue_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View File

@@ -5,10 +5,13 @@ import (
"fmt"
"os"
"os/exec"
"os/signal"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/cenkalti/backoff/v4"
@@ -95,6 +98,8 @@ func (s *Server) Start() error {
defer s.mutex.Unlock()
state := internal.CtxGetState(s.rootCtx)
s.setupReloadSignal()
if err := handlePanicLog(); err != nil {
log.Warnf("failed to redirect stderr: %v", err)
}
@@ -707,7 +712,9 @@ func (s *Server) Status(
s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive)
if msg.GetFullPeerStatus {
s.runProbes()
if msg.ShouldRunProbes {
s.runProbes()
}
fullStatus := s.statusRecorder.GetFullStatus()
pbFullStatus := toProtoFullStatus(fullStatus)
@@ -916,3 +923,186 @@ func sendTerminalNotification() error {
return wallCmd.Wait()
}
// Add a gRPC method to reload config from disk
func (s *Server) ReloadConfig(_ context.Context, _ *proto.ReloadConfigRequest) (*proto.ReloadConfigResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
config, err := internal.ReadConfig(s.latestConfigInput.ConfigPath)
if err != nil {
return nil, fmt.Errorf("failed to reload config: %v", err)
}
s.config = config
s.statusRecorder.UpdateManagementAddress(config.ManagementURL.String())
s.statusRecorder.UpdateRosenpass(config.RosenpassEnabled, config.RosenpassPermissive)
s.statusRecorder.UpdateLazyConnection(config.LazyConnectionEnabled)
log.Infof("Reloaded config from disk")
return &proto.ReloadConfigResponse{}, nil
}
// Optionally, handle SIGHUP to reload config
func (s *Server) setupReloadSignal() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP)
go func() {
for range c {
_, err := s.ReloadConfig(context.Background(), &proto.ReloadConfigRequest{})
if err != nil {
log.Warnf("failed to reload config on SIGHUP: %v", err)
}
}
}()
}
func (s *Server) SetConfigValue(_ context.Context, req *proto.SetConfigValueRequest) (*proto.SetConfigValueResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
setting := req.Setting
value := req.Value
input := internal.ConfigInput{ConfigPath: s.latestConfigInput.ConfigPath}
switch setting {
case "management-url":
input.ManagementURL = value
case "admin-url":
input.AdminURL = value
case "interface-name":
input.InterfaceName = &value
case "external-ip-map":
if value == "" {
input.NATExternalIPs = []string{}
} else {
input.NATExternalIPs = strings.Split(value, ",")
}
case "extra-iface-blacklist":
if value == "" {
input.ExtraIFaceBlackList = []string{}
} else {
input.ExtraIFaceBlackList = strings.Split(value, ",")
}
case "dns-resolver-address":
input.CustomDNSAddress = []byte(value)
case "extra-dns-labels":
if value == "" {
input.DNSLabels = nil
} else {
labels := strings.Split(value, ",")
domains, err := domain.ValidateDomains(labels)
if err != nil {
return nil, fmt.Errorf("invalid DNS labels: %v", err)
}
input.DNSLabels = domains
}
case "preshared-key":
input.PreSharedKey = &value
case "enable-rosenpass":
b, err := parseBool(value)
if err != nil {
return nil, err
}
input.RosenpassEnabled = &b
case "rosenpass-permissive":
b, err := parseBool(value)
if err != nil {
return nil, err
}
input.RosenpassPermissive = &b
case "allow-server-ssh":
b, err := parseBool(value)
if err != nil {
return nil, err
}
input.ServerSSHAllowed = &b
case "network-monitor":
b, err := parseBool(value)
if err != nil {
return nil, err
}
input.NetworkMonitor = &b
case "disable-auto-connect":
b, err := parseBool(value)
if err != nil {
return nil, err
}
input.DisableAutoConnect = &b
case "disable-client-routes":
b, err := parseBool(value)
if err != nil {
return nil, err
}
input.DisableClientRoutes = &b
case "disable-server-routes":
b, err := parseBool(value)
if err != nil {
return nil, err
}
input.DisableServerRoutes = &b
case "disable-dns":
b, err := parseBool(value)
if err != nil {
return nil, err
}
input.DisableDNS = &b
case "disable-firewall":
b, err := parseBool(value)
if err != nil {
return nil, err
}
input.DisableFirewall = &b
case "block-lan-access":
b, err := parseBool(value)
if err != nil {
return nil, err
}
input.BlockLANAccess = &b
case "block-inbound":
b, err := parseBool(value)
if err != nil {
return nil, err
}
input.BlockInbound = &b
case "enable-lazy-connection":
b, err := parseBool(value)
if err != nil {
return nil, err
}
input.LazyConnectionEnabled = &b
case "wireguard-port":
var p int
_, err := fmt.Sscanf(value, "%d", &p)
if err != nil {
return nil, fmt.Errorf("invalid wireguard-port: %s", value)
}
input.WireguardPort = &p
case "dns-router-interval":
d, err := time.ParseDuration(value)
if err != nil {
return nil, fmt.Errorf("invalid duration: %v", err)
}
input.DNSRouteInterval = &d
default:
return nil, fmt.Errorf("unknown setting: %s", setting)
}
_, err := internal.UpdateOrCreateConfig(input)
if err != nil {
return nil, fmt.Errorf("failed to update config: %v", err)
}
// Reload config in memory
config, err := internal.ReadConfig(s.latestConfigInput.ConfigPath)
if err == nil {
s.config = config
}
return &proto.SetConfigValueResponse{}, nil
}
func parseBool(val string) (bool, error) {
v := strings.ToLower(val)
if v == "true" || v == "1" {
return true, nil
}
if v == "false" || v == "0" {
return false, nil
}
return false, fmt.Errorf("invalid boolean value: %s", val)
}