Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 84 additions & 4 deletions client/internal/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -903,10 +903,13 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
return errors.New("wireguard interface is not initialized")
}

// Cannot update the IP address without restarting the engine because
// the firewall, route manager, and other components cache the old address
if e.wgInterface.Address().String() != conf.Address {
log.Infof("peer IP address has changed from %s to %s", e.wgInterface.Address().String(), conf.Address)
// Check if IP address has changed and update the interface
currentAddr := e.wgInterface.Address().String()
if currentAddr != conf.Address {
if err := e.updateSelfPeerIP(currentAddr, conf.Address); err != nil {
log.Errorf("failed to update self peer IP: %v", err)
// Continue with the rest of the config update even if IP update fails
}
}

if conf.GetSshConfig() != nil {
Expand All @@ -926,6 +929,83 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
return nil
}

// updateSelfPeerIP updates the local WireGuard interface IP address when the management
// server changes the self peer's IP. This enables hot IP updates without requiring
// a full engine restart (netbird down && netbird up).
func (e *Engine) updateSelfPeerIP(oldAddr, newAddr string) error {
// Idempotency check: if addresses are the same, do nothing
if oldAddr == newAddr {
return nil
}

log.Infof("peer IP address has changed from %s to %s, updating local WireGuard interface", oldAddr, newAddr)

// Parse old IP for firewall rule updates (before UpdateAddr changes the interface)
oldIP := e.wgInterface.Address().IP

// Update the WireGuard interface with the new address
// The UpdateAddr method handles removing the old address and adding the new one
if err := e.wgInterface.UpdateAddr(newAddr); err != nil {
return fmt.Errorf("failed to update WireGuard interface address: %w", err)
}

// Update the engine config to reflect the new address
e.config.WgAddr = newAddr

// Update firewall rules that reference the local IP
if err := e.updateFirewallForAddressChange(oldIP); err != nil {
log.Warnf("failed to update firewall rules for address change: %v", err)
}

log.Infof("successfully updated local WireGuard IP from %s to %s", oldAddr, newAddr)

return nil
}

// updateFirewallForAddressChange updates firewall rules when the local IP address changes.
// This includes SSH port redirection and local IP bitmap updates.
func (e *Engine) updateFirewallForAddressChange(oldIP netip.Addr) error {
if e.firewall == nil {
return nil
}

newIP := e.wgInterface.Address().IP

// Restart SSH server if running - it needs to rebind to the new IP
if e.sshServer != nil && oldIP.IsValid() && newIP.IsValid() {
// Remove old SSH DNAT rule with old IP
if err := e.firewall.RemoveInboundDNAT(oldIP, firewallManager.ProtocolTCP, 22, 22022); err != nil {
log.Warnf("failed to remove old SSH port redirection for %s: %v", oldIP, err)
} else {
log.Debugf("removed old SSH port redirection: %s:22 -> %s:22022", oldIP, oldIP)
}

// Restart SSH server on new address
newListenAddr := netip.AddrPortFrom(newIP, 22022)
if err := e.sshServer.Restart(e.ctx, newListenAddr); err != nil {
log.Errorf("failed to restart SSH server on new address %s: %v", newIP, err)
} else {
log.Infof("SSH server restarted on new address %s:22022", newIP)
}

// Add new SSH DNAT rule with new IP
if err := e.firewall.AddInboundDNAT(newIP, firewallManager.ProtocolTCP, 22, 22022); err != nil {
log.Errorf("failed to add new SSH port redirection for %s: %v", newIP, err)
} else {
log.Infof("updated SSH port redirection: %s:22 -> %s:22022", newIP, newIP)
}
}

// Update local IP bitmap in userspace firewall
if localipfw, ok := e.firewall.(localIpUpdater); ok {
if err := localipfw.UpdateLocalIPs(); err != nil {
log.Warnf("failed to update local IPs after address change: %v", err)
}
}

return nil
}

// receiveManagementEvents connects to the Management Service event stream to receive updates from the management service
// E.g. when a new peer has been registered and we are allowed to connect to it.
func (e *Engine) receiveManagementEvents() {
Expand Down
1 change: 1 addition & 0 deletions client/internal/engine_ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
type sshServer interface {
Start(ctx context.Context, addr netip.AddrPort) error
Stop() error
Restart(ctx context.Context, newAddr netip.AddrPort) error
GetStatus() (bool, []sshserver.SessionInfo)
}

Expand Down
Loading