Skip to content

Commit 2fe2af3

Browse files
authored
[client] Clean up match domain reg entries between config changes (#4676)
1 parent cd9a867 commit 2fe2af3

File tree

3 files changed

+168
-5
lines changed

3 files changed

+168
-5
lines changed

client/internal/dns/host_windows.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
nberrors "github.com/netbirdio/netbird/client/errors"
1919
"github.com/netbirdio/netbird/client/internal/statemanager"
20+
"github.com/netbirdio/netbird/client/internal/winregistry"
2021
)
2122

2223
var (
@@ -197,16 +198,17 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager
197198
matchDomains = append(matchDomains, "."+strings.TrimSuffix(dConf.Domain, "."))
198199
}
199200

201+
if err := r.removeDNSMatchPolicies(); err != nil {
202+
log.Errorf("cleanup old dns match policies: %s", err)
203+
}
204+
200205
if len(matchDomains) != 0 {
201206
count, err := r.addDNSMatchPolicy(matchDomains, config.ServerIP)
202207
if err != nil {
203208
return fmt.Errorf("add dns match policy: %w", err)
204209
}
205210
r.nrptEntryCount = count
206211
} else {
207-
if err := r.removeDNSMatchPolicies(); err != nil {
208-
return fmt.Errorf("remove dns match policies: %w", err)
209-
}
210212
r.nrptEntryCount = 0
211213
}
212214

@@ -273,9 +275,9 @@ func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []s
273275
return fmt.Errorf("remove existing dns policy: %w", err)
274276
}
275277

276-
regKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, policyPath, registry.SET_VALUE)
278+
regKey, _, err := winregistry.CreateVolatileKey(registry.LOCAL_MACHINE, policyPath, registry.SET_VALUE)
277279
if err != nil {
278-
return fmt.Errorf("create registry key HKEY_LOCAL_MACHINE\\%s: %w", policyPath, err)
280+
return fmt.Errorf("create volatile registry key HKEY_LOCAL_MACHINE\\%s: %w", policyPath, err)
279281
}
280282
defer closer(regKey)
281283

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package dns
2+
3+
import (
4+
"fmt"
5+
"net/netip"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
"golang.org/x/sys/windows/registry"
11+
)
12+
13+
// TestNRPTEntriesCleanupOnConfigChange tests that old NRPT entries are properly cleaned up
14+
// when the number of match domains decreases between configuration changes.
15+
func TestNRPTEntriesCleanupOnConfigChange(t *testing.T) {
16+
if testing.Short() {
17+
t.Skip("skipping registry integration test in short mode")
18+
}
19+
20+
defer cleanupRegistryKeys(t)
21+
cleanupRegistryKeys(t)
22+
23+
testIP := netip.MustParseAddr("100.64.0.1")
24+
25+
// Create a test interface registry key so updateSearchDomains doesn't fail
26+
testGUID := "{12345678-1234-1234-1234-123456789ABC}"
27+
interfacePath := `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\` + testGUID
28+
testKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, interfacePath, registry.SET_VALUE)
29+
require.NoError(t, err, "Should create test interface registry key")
30+
testKey.Close()
31+
defer func() {
32+
_ = registry.DeleteKey(registry.LOCAL_MACHINE, interfacePath)
33+
}()
34+
35+
cfg := &registryConfigurator{
36+
guid: testGUID,
37+
gpo: false,
38+
}
39+
40+
config5 := HostDNSConfig{
41+
ServerIP: testIP,
42+
Domains: []DomainConfig{
43+
{Domain: "domain1.com", MatchOnly: true},
44+
{Domain: "domain2.com", MatchOnly: true},
45+
{Domain: "domain3.com", MatchOnly: true},
46+
{Domain: "domain4.com", MatchOnly: true},
47+
{Domain: "domain5.com", MatchOnly: true},
48+
},
49+
}
50+
51+
err = cfg.applyDNSConfig(config5, nil)
52+
require.NoError(t, err)
53+
54+
// Verify all 5 entries exist
55+
for i := 0; i < 5; i++ {
56+
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i))
57+
require.NoError(t, err)
58+
assert.True(t, exists, "Entry %d should exist after first config", i)
59+
}
60+
61+
config2 := HostDNSConfig{
62+
ServerIP: testIP,
63+
Domains: []DomainConfig{
64+
{Domain: "domain1.com", MatchOnly: true},
65+
{Domain: "domain2.com", MatchOnly: true},
66+
},
67+
}
68+
69+
err = cfg.applyDNSConfig(config2, nil)
70+
require.NoError(t, err)
71+
72+
// Verify first 2 entries exist
73+
for i := 0; i < 2; i++ {
74+
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i))
75+
require.NoError(t, err)
76+
assert.True(t, exists, "Entry %d should exist after second config", i)
77+
}
78+
79+
// Verify entries 2-4 are cleaned up
80+
for i := 2; i < 5; i++ {
81+
exists, err := registryKeyExists(fmt.Sprintf("%s-%d", dnsPolicyConfigMatchPath, i))
82+
require.NoError(t, err)
83+
assert.False(t, exists, "Entry %d should NOT exist after reducing to 2 domains", i)
84+
}
85+
}
86+
87+
func registryKeyExists(path string) (bool, error) {
88+
k, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.QUERY_VALUE)
89+
if err != nil {
90+
if err == registry.ErrNotExist {
91+
return false, nil
92+
}
93+
return false, err
94+
}
95+
k.Close()
96+
return true, nil
97+
}
98+
99+
func cleanupRegistryKeys(*testing.T) {
100+
cfg := &registryConfigurator{nrptEntryCount: 10}
101+
_ = cfg.removeDNSMatchPolicies()
102+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package winregistry
2+
3+
import (
4+
"syscall"
5+
"unsafe"
6+
7+
"golang.org/x/sys/windows/registry"
8+
)
9+
10+
var (
11+
advapi = syscall.NewLazyDLL("advapi32.dll")
12+
regCreateKeyExW = advapi.NewProc("RegCreateKeyExW")
13+
)
14+
15+
const (
16+
// Registry key options
17+
regOptionNonVolatile = 0x0 // Key is preserved when system is rebooted
18+
regOptionVolatile = 0x1 // Key is not preserved when system is rebooted
19+
20+
// Registry disposition values
21+
regCreatedNewKey = 0x1
22+
regOpenedExistingKey = 0x2
23+
)
24+
25+
// CreateVolatileKey creates a volatile registry key named path under open key root.
26+
// CreateVolatileKey returns the new key and a boolean flag that reports whether the key already existed.
27+
// The access parameter specifies the access rights for the key to be created.
28+
//
29+
// Volatile keys are stored in memory and are automatically deleted when the system is shut down.
30+
// This provides automatic cleanup without requiring manual registry maintenance.
31+
func CreateVolatileKey(root registry.Key, path string, access uint32) (registry.Key, bool, error) {
32+
pathPtr, err := syscall.UTF16PtrFromString(path)
33+
if err != nil {
34+
return 0, false, err
35+
}
36+
37+
var (
38+
handle syscall.Handle
39+
disposition uint32
40+
)
41+
42+
ret, _, _ := regCreateKeyExW.Call(
43+
uintptr(root),
44+
uintptr(unsafe.Pointer(pathPtr)),
45+
0, // reserved
46+
0, // class
47+
uintptr(regOptionVolatile), // options - volatile key
48+
uintptr(access), // desired access
49+
0, // security attributes
50+
uintptr(unsafe.Pointer(&handle)),
51+
uintptr(unsafe.Pointer(&disposition)),
52+
)
53+
54+
if ret != 0 {
55+
return 0, false, syscall.Errno(ret)
56+
}
57+
58+
return registry.Key(handle), disposition == regOpenedExistingKey, nil
59+
}

0 commit comments

Comments
 (0)