Skip to content

Commit beb6620

Browse files
authored
[management, client] Add API to change the network range (#4177)
1 parent 58eb3c8 commit beb6620

File tree

20 files changed

+606
-27
lines changed

20 files changed

+606
-27
lines changed

client/iface/wgproxy/bind/proxy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ func fakeAddress(peerAddress *net.UDPAddr) (*netip.AddrPort, error) {
171171

172172
fakeIP, err := netip.ParseAddr(fmt.Sprintf("127.1.%s.%s", octets[2], octets[3]))
173173
if err != nil {
174-
return nil, fmt.Errorf("failed to parse new IP: %w", err)
174+
return nil, fmt.Errorf("parse new IP: %w", err)
175175
}
176176

177177
netipAddr := netip.AddrPortFrom(fakeIP, uint16(peerAddress.Port))

client/iface/wgproxy/ebpf/wrapper.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func (e *ProxyWrapper) CloseConn() error {
9595
e.closeListener.SetCloseListener(nil)
9696

9797
if err := e.remoteConn.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
98-
return fmt.Errorf("failed to close remote conn: %w", err)
98+
return fmt.Errorf("close remote conn: %w", err)
9999
}
100100
return nil
101101
}

client/internal/engine.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -861,15 +861,10 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
861861
return errors.New("wireguard interface is not initialized")
862862
}
863863

864+
// Cannot update the IP address without restarting the engine because
865+
// the firewall, route manager, and other components cache the old address
864866
if e.wgInterface.Address().String() != conf.Address {
865-
oldAddr := e.wgInterface.Address().String()
866-
log.Debugf("updating peer address from %s to %s", oldAddr, conf.Address)
867-
err := e.wgInterface.UpdateAddr(conf.Address)
868-
if err != nil {
869-
return err
870-
}
871-
e.config.WgAddr = conf.Address
872-
log.Infof("updated peer address from %s to %s", oldAddr, conf.Address)
867+
log.Infof("peer IP address has changed from %s to %s", e.wgInterface.Address().String(), conf.Address)
873868
}
874869

875870
if conf.GetSshConfig() != nil {
@@ -880,7 +875,7 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
880875
}
881876

882877
state := e.statusRecorder.GetLocalPeerState()
883-
state.IP = e.config.WgAddr
878+
state.IP = e.wgInterface.Address().String()
884879
state.PubKey = e.config.WgPrivateKey.PublicKey().String()
885880
state.KernelInterface = device.WireGuardModuleIsLoaded()
886881
state.FQDN = conf.GetFqdn()

management/cmd/management.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ var (
142142

143143
err := handleRebrand(cmd)
144144
if err != nil {
145-
return fmt.Errorf("failed to migrate files %v", err)
145+
return fmt.Errorf("migrate files %v", err)
146146
}
147147

148148
if _, err = os.Stat(config.Datadir); os.IsNotExist(err) {
@@ -184,15 +184,15 @@ var (
184184
}
185185
eventStore, key, err := integrations.InitEventStore(ctx, config.Datadir, config.DataStoreEncryptionKey, integrationMetrics)
186186
if err != nil {
187-
return fmt.Errorf("failed to initialize database: %s", err)
187+
return fmt.Errorf("initialize database: %s", err)
188188
}
189189

190190
if config.DataStoreEncryptionKey != key {
191191
log.WithContext(ctx).Infof("update config with activity store key")
192192
config.DataStoreEncryptionKey = key
193193
err := updateMgmtConfig(ctx, types.MgmtConfigPath, config)
194194
if err != nil {
195-
return fmt.Errorf("failed to write out store encryption key: %s", err)
195+
return fmt.Errorf("write out store encryption key: %s", err)
196196
}
197197
}
198198

@@ -205,7 +205,7 @@ var (
205205

206206
integratedPeerValidator, err := integrations.NewIntegratedValidator(ctx, eventStore)
207207
if err != nil {
208-
return fmt.Errorf("failed to initialize integrated peer validator: %v", err)
208+
return fmt.Errorf("initialize integrated peer validator: %v", err)
209209
}
210210

211211
permissionsManager := integrations.InitPermissionsManager(store)
@@ -217,7 +217,7 @@ var (
217217
accountManager, err := server.BuildManager(ctx, store, peersUpdateManager, idpManager, mgmtSingleAccModeDomain,
218218
dnsDomain, eventStore, geo, userDeleteFromIDPEnabled, integratedPeerValidator, appMetrics, proxyController, settingsManager, permissionsManager, config.DisableDefaultPolicy)
219219
if err != nil {
220-
return fmt.Errorf("failed to build default manager: %v", err)
220+
return fmt.Errorf("build default manager: %v", err)
221221
}
222222

223223
secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsManager)

management/server/account.go

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"math/rand"
88
"net"
9+
"net/netip"
910
"os"
1011
"reflect"
1112
"regexp"
@@ -324,6 +325,13 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
324325
return err
325326
}
326327

328+
if oldSettings.NetworkRange != newSettings.NetworkRange {
329+
if err = am.reallocateAccountPeerIPs(ctx, transaction, accountID, newSettings.NetworkRange); err != nil {
330+
return err
331+
}
332+
updateAccountPeers = true
333+
}
334+
327335
if oldSettings.RoutingPeerDNSResolutionEnabled != newSettings.RoutingPeerDNSResolutionEnabled ||
328336
oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled ||
329337
oldSettings.DNSDomain != newSettings.DNSDomain {
@@ -362,7 +370,18 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
362370
return nil, err
363371
}
364372
if oldSettings.DNSDomain != newSettings.DNSDomain {
365-
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountDNSDomainUpdated, nil)
373+
eventMeta := map[string]any{
374+
"old_dns_domain": oldSettings.DNSDomain,
375+
"new_dns_domain": newSettings.DNSDomain,
376+
}
377+
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountDNSDomainUpdated, eventMeta)
378+
}
379+
if oldSettings.NetworkRange != newSettings.NetworkRange {
380+
eventMeta := map[string]any{
381+
"old_network_range": oldSettings.NetworkRange.String(),
382+
"new_network_range": newSettings.NetworkRange.String(),
383+
}
384+
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountNetworkRangeUpdated, eventMeta)
366385
}
367386

368387
if updateAccountPeers || extraSettingsChanged || groupChangesAffectPeers {
@@ -2018,3 +2037,154 @@ func propagateUserGroupMemberships(ctx context.Context, transaction store.Store,
20182037

20192038
return len(updatedGroups) > 0, peersAffected, nil
20202039
}
2040+
2041+
// reallocateAccountPeerIPs re-allocates all peer IPs when the network range changes
2042+
func (am *DefaultAccountManager) reallocateAccountPeerIPs(ctx context.Context, transaction store.Store, accountID string, newNetworkRange netip.Prefix) error {
2043+
if !newNetworkRange.IsValid() {
2044+
return nil
2045+
}
2046+
2047+
newIPNet := net.IPNet{
2048+
IP: newNetworkRange.Masked().Addr().AsSlice(),
2049+
Mask: net.CIDRMask(newNetworkRange.Bits(), newNetworkRange.Addr().BitLen()),
2050+
}
2051+
2052+
account, err := transaction.GetAccount(ctx, accountID)
2053+
if err != nil {
2054+
return err
2055+
}
2056+
2057+
account.Network.Net = newIPNet
2058+
2059+
peers, err := transaction.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "")
2060+
if err != nil {
2061+
return err
2062+
}
2063+
2064+
var takenIPs []net.IP
2065+
2066+
for _, peer := range peers {
2067+
newIP, err := types.AllocatePeerIP(newIPNet, takenIPs)
2068+
if err != nil {
2069+
return status.Errorf(status.Internal, "allocate IP for peer %s: %v", peer.ID, err)
2070+
}
2071+
2072+
log.WithContext(ctx).Infof("reallocating peer %s IP from %s to %s due to network range change",
2073+
peer.ID, peer.IP.String(), newIP.String())
2074+
2075+
peer.IP = newIP
2076+
takenIPs = append(takenIPs, newIP)
2077+
}
2078+
2079+
if err = transaction.SaveAccount(ctx, account); err != nil {
2080+
return err
2081+
}
2082+
2083+
for _, peer := range peers {
2084+
if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil {
2085+
return status.Errorf(status.Internal, "save updated peer %s: %v", peer.ID, err)
2086+
}
2087+
}
2088+
2089+
log.WithContext(ctx).Infof("successfully re-allocated IPs for %d peers in account %s to network range %s",
2090+
len(peers), accountID, newNetworkRange.String())
2091+
2092+
return nil
2093+
}
2094+
2095+
func (am *DefaultAccountManager) validateIPForUpdate(account *types.Account, peers []*nbpeer.Peer, peerID string, newIP netip.Addr) error {
2096+
if !account.Network.Net.Contains(newIP.AsSlice()) {
2097+
return status.Errorf(status.InvalidArgument, "IP %s is not within the account network range %s", newIP.String(), account.Network.Net.String())
2098+
}
2099+
2100+
for _, peer := range peers {
2101+
if peer.ID != peerID && peer.IP.Equal(newIP.AsSlice()) {
2102+
return status.Errorf(status.InvalidArgument, "IP %s is already assigned to peer %s", newIP.String(), peer.ID)
2103+
}
2104+
}
2105+
return nil
2106+
}
2107+
2108+
func (am *DefaultAccountManager) UpdatePeerIP(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) error {
2109+
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
2110+
defer unlock()
2111+
2112+
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Update)
2113+
if err != nil {
2114+
return fmt.Errorf("validate user permissions: %w", err)
2115+
}
2116+
if !allowed {
2117+
return status.NewPermissionDeniedError()
2118+
}
2119+
2120+
updateNetworkMap, err := am.updatePeerIPInTransaction(ctx, accountID, userID, peerID, newIP)
2121+
if err != nil {
2122+
return fmt.Errorf("update peer IP transaction: %w", err)
2123+
}
2124+
2125+
if updateNetworkMap {
2126+
am.BufferUpdateAccountPeers(ctx, accountID)
2127+
}
2128+
return nil
2129+
}
2130+
2131+
func (am *DefaultAccountManager) updatePeerIPInTransaction(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) (bool, error) {
2132+
var updateNetworkMap bool
2133+
err := am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
2134+
account, err := transaction.GetAccount(ctx, accountID)
2135+
if err != nil {
2136+
return fmt.Errorf("get account: %w", err)
2137+
}
2138+
2139+
existingPeer, err := transaction.GetPeerByID(ctx, store.LockingStrengthShare, accountID, peerID)
2140+
if err != nil {
2141+
return fmt.Errorf("get peer: %w", err)
2142+
}
2143+
2144+
if existingPeer.IP.Equal(newIP.AsSlice()) {
2145+
return nil
2146+
}
2147+
2148+
peers, err := transaction.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "")
2149+
if err != nil {
2150+
return fmt.Errorf("get account peers: %w", err)
2151+
}
2152+
2153+
if err := am.validateIPForUpdate(account, peers, peerID, newIP); err != nil {
2154+
return err
2155+
}
2156+
2157+
if err := am.savePeerIPUpdate(ctx, transaction, accountID, userID, existingPeer, newIP); err != nil {
2158+
return err
2159+
}
2160+
2161+
updateNetworkMap = true
2162+
return nil
2163+
})
2164+
return updateNetworkMap, err
2165+
}
2166+
2167+
func (am *DefaultAccountManager) savePeerIPUpdate(ctx context.Context, transaction store.Store, accountID, userID string, peer *nbpeer.Peer, newIP netip.Addr) error {
2168+
log.WithContext(ctx).Infof("updating peer %s IP from %s to %s", peer.ID, peer.IP, newIP)
2169+
2170+
settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
2171+
if err != nil {
2172+
return fmt.Errorf("get account settings: %w", err)
2173+
}
2174+
dnsDomain := am.GetDNSDomain(settings)
2175+
2176+
eventMeta := peer.EventMeta(dnsDomain)
2177+
oldIP := peer.IP.String()
2178+
2179+
peer.IP = newIP.AsSlice()
2180+
err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer)
2181+
if err != nil {
2182+
return fmt.Errorf("save peer: %w", err)
2183+
}
2184+
2185+
eventMeta["old_ip"] = oldIP
2186+
eventMeta["ip"] = newIP.String()
2187+
am.StoreEvent(ctx, userID, peer.ID, accountID, activity.PeerIPUpdated, eventMeta)
2188+
2189+
return nil
2190+
}

management/server/account/manager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type Manager interface {
5151
MarkPeerConnected(ctx context.Context, peerKey string, connected bool, realIP net.IP, accountID string) error
5252
DeletePeer(ctx context.Context, accountID, peerID, userID string) error
5353
UpdatePeer(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error)
54+
UpdatePeerIP(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) error
5455
GetNetworkMap(ctx context.Context, peerID string) (*types.NetworkMap, error)
5556
GetPeerNetwork(ctx context.Context, peerID string) (*types.Network, error)
5657
AddPeer(ctx context.Context, setupKey, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error)

management/server/account_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"net"
9+
"net/netip"
910
"os"
1011
"reflect"
1112
"strconv"
@@ -3522,3 +3523,70 @@ func TestDefaultAccountManager_UpdateAccountOnboarding(t *testing.T) {
35223523
require.NoError(t, err)
35233524
})
35243525
}
3526+
3527+
func TestDefaultAccountManager_UpdatePeerIP(t *testing.T) {
3528+
manager, err := createManager(t)
3529+
require.NoError(t, err, "unable to create account manager")
3530+
3531+
accountID, err := manager.GetAccountIDByUserID(context.Background(), userID, "")
3532+
require.NoError(t, err, "unable to create an account")
3533+
3534+
key1, err := wgtypes.GenerateKey()
3535+
require.NoError(t, err, "unable to generate WireGuard key")
3536+
key2, err := wgtypes.GenerateKey()
3537+
require.NoError(t, err, "unable to generate WireGuard key")
3538+
3539+
peer1, _, _, err := manager.AddPeer(context.Background(), "", userID, &nbpeer.Peer{
3540+
Key: key1.PublicKey().String(),
3541+
Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-1"},
3542+
})
3543+
require.NoError(t, err, "unable to add peer1")
3544+
3545+
peer2, _, _, err := manager.AddPeer(context.Background(), "", userID, &nbpeer.Peer{
3546+
Key: key2.PublicKey().String(),
3547+
Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer-2"},
3548+
})
3549+
require.NoError(t, err, "unable to add peer2")
3550+
3551+
t.Run("update peer IP successfully", func(t *testing.T) {
3552+
account, err := manager.Store.GetAccount(context.Background(), accountID)
3553+
require.NoError(t, err, "unable to get account")
3554+
3555+
newIP, err := types.AllocatePeerIP(account.Network.Net, []net.IP{peer1.IP, peer2.IP})
3556+
require.NoError(t, err, "unable to allocate new IP")
3557+
3558+
newAddr := netip.MustParseAddr(newIP.String())
3559+
err = manager.UpdatePeerIP(context.Background(), accountID, userID, peer1.ID, newAddr)
3560+
require.NoError(t, err, "unable to update peer IP")
3561+
3562+
updatedPeer, err := manager.GetPeer(context.Background(), accountID, peer1.ID, userID)
3563+
require.NoError(t, err, "unable to get updated peer")
3564+
assert.Equal(t, newIP.String(), updatedPeer.IP.String(), "peer IP should be updated")
3565+
})
3566+
3567+
t.Run("update peer IP with same IP should be no-op", func(t *testing.T) {
3568+
currentAddr := netip.MustParseAddr(peer1.IP.String())
3569+
err := manager.UpdatePeerIP(context.Background(), accountID, userID, peer1.ID, currentAddr)
3570+
require.NoError(t, err, "updating with same IP should not error")
3571+
})
3572+
3573+
t.Run("update peer IP with collision should fail", func(t *testing.T) {
3574+
peer2Addr := netip.MustParseAddr(peer2.IP.String())
3575+
err := manager.UpdatePeerIP(context.Background(), accountID, userID, peer1.ID, peer2Addr)
3576+
require.Error(t, err, "should fail when IP is already assigned")
3577+
assert.Contains(t, err.Error(), "already assigned", "error should mention IP collision")
3578+
})
3579+
3580+
t.Run("update peer IP outside network range should fail", func(t *testing.T) {
3581+
invalidAddr := netip.MustParseAddr("192.168.1.100")
3582+
err := manager.UpdatePeerIP(context.Background(), accountID, userID, peer1.ID, invalidAddr)
3583+
require.Error(t, err, "should fail when IP is outside network range")
3584+
assert.Contains(t, err.Error(), "not within the account network range", "error should mention network range")
3585+
})
3586+
3587+
t.Run("update peer IP with invalid peer ID should fail", func(t *testing.T) {
3588+
newAddr := netip.MustParseAddr("100.64.0.101")
3589+
err := manager.UpdatePeerIP(context.Background(), accountID, userID, "invalid-peer-id", newAddr)
3590+
require.Error(t, err, "should fail with invalid peer ID")
3591+
})
3592+
}

management/server/activity/codes.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ const (
175175
AccountLazyConnectionEnabled Activity = 85
176176
AccountLazyConnectionDisabled Activity = 86
177177

178+
AccountNetworkRangeUpdated Activity = 87
179+
PeerIPUpdated Activity = 88
180+
178181
AccountDeleted Activity = 99999
179182
)
180183

@@ -277,6 +280,10 @@ var activityMap = map[Activity]Code{
277280

278281
AccountLazyConnectionEnabled: {"Account lazy connection enabled", "account.setting.lazy.connection.enable"},
279282
AccountLazyConnectionDisabled: {"Account lazy connection disabled", "account.setting.lazy.connection.disable"},
283+
284+
AccountNetworkRangeUpdated: {"Account network range updated", "account.network.range.update"},
285+
286+
PeerIPUpdated: {"Peer IP updated", "peer.ip.update"},
280287
}
281288

282289
// StringCode returns a string code of the activity

0 commit comments

Comments
 (0)