Skip to content

Commit 31dbe9e

Browse files
committed
feat(dns): resolve network-local names
- remove no longer needed DNS_KEEP_NAMESERVER - add DNS_LOCAL_RESOLVERS which defaults to private nameservers found in etc/resolv.conf at start - ⚠️ turning DOT=off will cause the local resolution to no longer work
1 parent 2bd1964 commit 31dbe9e

File tree

9 files changed

+83
-56
lines changed

9 files changed

+83
-56
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
178178
UNBLOCK= \
179179
DNS_UPDATE_PERIOD=24h \
180180
DNS_ADDRESS=127.0.0.1 \
181-
DNS_KEEP_NAMESERVER=off \
181+
DNS_LOCAL_RESOLVERS= \
182182
# HTTP proxy
183183
HTTPPROXY= \
184184
HTTPPROXY_LOG=off \

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/klauspost/compress v1.18.1
1010
github.com/klauspost/pgzip v1.2.6
1111
github.com/pelletier/go-toml/v2 v2.2.4
12-
github.com/qdm12/dns/v2 v2.0.0-rc8
12+
github.com/qdm12/dns/v2 v2.0.0-rc8.0.20251105160351-52207aa09672
1313
github.com/qdm12/gosettings v0.4.4
1414
github.com/qdm12/goshutdown v0.3.0
1515
github.com/qdm12/gosplash v0.2.0
@@ -47,7 +47,7 @@ require (
4747
github.com/prometheus/client_model v0.6.1 // indirect
4848
github.com/prometheus/common v0.60.1 // indirect
4949
github.com/prometheus/procfs v0.15.1 // indirect
50-
github.com/qdm12/goservices v0.1.0 // indirect
50+
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
5151
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
5252
github.com/vishvananda/netns v0.0.5 // indirect
5353
golang.org/x/crypto v0.43.0 // indirect

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPA
5353
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
5454
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
5555
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
56-
github.com/qdm12/dns/v2 v2.0.0-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ18=
57-
github.com/qdm12/dns/v2 v2.0.0-rc8/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
58-
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
59-
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
56+
github.com/qdm12/dns/v2 v2.0.0-rc8.0.20251105160351-52207aa09672 h1:SL4ofJRf8ftAY3VvXnUi+XQaUFLpsWFWVaty8PJMJK8=
57+
github.com/qdm12/dns/v2 v2.0.0-rc8.0.20251105160351-52207aa09672/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
58+
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
59+
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
6060
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
6161
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
6262
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=

internal/configuration/settings/dns.go

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package settings
22

33
import (
4+
"errors"
45
"fmt"
56
"net/netip"
67

8+
"github.com/qdm12/dns/v2/pkg/nameserver"
79
"github.com/qdm12/gosettings"
810
"github.com/qdm12/gosettings/reader"
911
"github.com/qdm12/gotree"
@@ -17,36 +19,38 @@ type DNS struct {
1719
// DoT server. It cannot be the zero value in the internal
1820
// state.
1921
ServerAddress netip.Addr
20-
// KeepNameserver is true if the existing DNS server
21-
// found in /etc/resolv.conf should be used
22-
// Note setting this to true will likely DNS traffic
23-
// outside the VPN tunnel since it would go through
24-
// the local DNS server of your Docker/Kubernetes
25-
// configuration, which is likely not going through the tunnel.
26-
// This will also disable the DNS over TLS server and the
27-
// `ServerAddress` field will be ignored.
28-
// It defaults to false and cannot be nil in the
29-
// internal state.
30-
KeepNameserver *bool
3122
// DOT contains settings to configure the DoT
3223
// server.
3324
DoT DoT
25+
// LocalResolvers are IP:port addresses of local DNS resolvers
26+
// to which queries for local domains should be sent.
27+
// By default, it is the private nameservers found in /etc/resolv.conf
28+
// at container start, before the DNS is setup.
29+
LocalResolvers []netip.AddrPort
3430
}
3531

32+
var ErrLocalResolverNotPrivate = errors.New("local resolver is not a private IP address")
33+
3634
func (d DNS) validate() (err error) {
3735
err = d.DoT.validate()
3836
if err != nil {
3937
return fmt.Errorf("validating DoT settings: %w", err)
4038
}
4139

40+
for _, resolver := range d.LocalResolvers {
41+
if !resolver.Addr().IsPrivate() && !resolver.Addr().IsLoopback() {
42+
return fmt.Errorf("%w: %s", ErrLocalResolverNotPrivate, resolver)
43+
}
44+
}
45+
4246
return nil
4347
}
4448

4549
func (d *DNS) Copy() (copied DNS) {
4650
return DNS{
4751
ServerAddress: d.ServerAddress,
48-
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
4952
DoT: d.DoT.copy(),
53+
LocalResolvers: gosettings.CopySlice(d.LocalResolvers),
5054
}
5155
}
5256

@@ -55,15 +59,15 @@ func (d *DNS) Copy() (copied DNS) {
5559
// settings.
5660
func (d *DNS) overrideWith(other DNS) {
5761
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
58-
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
5962
d.DoT.overrideWith(other.DoT)
63+
d.LocalResolvers = gosettings.OverrideWithSlice(d.LocalResolvers, other.LocalResolvers)
6064
}
6165

6266
func (d *DNS) setDefaults() {
6367
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
6468
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
65-
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
6669
d.DoT.setDefaults()
70+
d.LocalResolvers = gosettings.DefaultSlice(d.LocalResolvers, nameserver.GetPrivateDNSServers())
6771
}
6872

6973
func (d DNS) String() string {
@@ -72,12 +76,13 @@ func (d DNS) String() string {
7276

7377
func (d DNS) toLinesNode() (node *gotree.Node) {
7478
node = gotree.New("DNS settings:")
75-
node.Appendf("Keep existing nameserver(s): %s", gosettings.BoolToYesNo(d.KeepNameserver))
76-
if *d.KeepNameserver {
77-
return node
78-
}
7979
node.Appendf("DNS server address to use: %s", d.ServerAddress)
8080
node.AppendNode(d.DoT.toLinesNode())
81+
localResolversNode := gotree.New("Local resolvers:")
82+
for _, resolver := range d.LocalResolvers {
83+
localResolversNode.Appendf("%s", resolver.String())
84+
}
85+
node.AppendNode(localResolversNode)
8186
return node
8287
}
8388

@@ -87,14 +92,14 @@ func (d *DNS) read(r *reader.Reader) (err error) {
8792
return err
8893
}
8994

90-
d.KeepNameserver, err = r.BoolPtr("DNS_KEEP_NAMESERVER")
95+
err = d.DoT.read(r)
9196
if err != nil {
92-
return err
97+
return fmt.Errorf("DNS over TLS settings: %w", err)
9398
}
9499

95-
err = d.DoT.read(r)
100+
d.LocalResolvers, err = r.CSVNetipAddrPorts("DNS_LOCAL_RESOLVERS")
96101
if err != nil {
97-
return fmt.Errorf("DNS over TLS settings: %w", err)
102+
return err
98103
}
99104

100105
return nil

internal/configuration/settings/settings_test.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package settings
22

33
import (
4+
"strings"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
@@ -38,19 +39,19 @@ func Test_Settings_String(t *testing.T) {
3839
| ├── Run OpenVPN as: root
3940
| └── Verbosity level: 1
4041
├── DNS settings:
41-
| ├── Keep existing nameserver(s): no
4242
| ├── DNS server address to use: 127.0.0.1
43-
| └── DNS over TLS settings:
44-
| ├── Enabled: yes
45-
| ├── Update period: every 24h0m0s
46-
| ├── Upstream resolvers:
47-
| | └── Cloudflare
48-
| ├── Caching: yes
49-
| ├── IPv6: no
50-
| └── DNS filtering settings:
51-
| ├── Block malicious: yes
52-
| ├── Block ads: no
53-
| └── Block surveillance: yes
43+
| ├── DNS over TLS settings:
44+
| | ├── Enabled: yes
45+
| | ├── Update period: every 24h0m0s
46+
| | ├── Upstream resolvers:
47+
| | | └── Cloudflare
48+
| | ├── Caching: yes
49+
| | ├── IPv6: no
50+
| | └── DNS filtering settings:
51+
| | ├── Block malicious: yes
52+
| | ├── Block ads: no
53+
| | └── Block surveillance: yes
54+
| └── Local resolvers: [private nameservers found in /etc/resolv.conf]
5455
├── Firewall settings:
5556
| └── Enabled: yes
5657
├── Log settings:
@@ -91,6 +92,17 @@ func Test_Settings_String(t *testing.T) {
9192

9293
s := testCase.settings.String()
9394

95+
lines := strings.Split(s, "\n")
96+
for i := range lines {
97+
const subPart = "Local resolvers:"
98+
j := strings.Index(lines[i], subPart)
99+
if j == -1 {
100+
continue
101+
}
102+
lines[i] = lines[i][:j+len(subPart)] + " [private nameservers found in /etc/resolv.conf]"
103+
}
104+
s = strings.Join(lines, "\n")
105+
94106
assert.Equal(t, testCase.s, s)
95107
})
96108
}

internal/dns/plaintext.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ func (l *Loop) useUnencryptedDNS(fallback bool) {
2727
}
2828

2929
const dialTimeout = 3 * time.Second
30+
const defaultDNSPort = 53
3031
settingsInternalDNS := nameserver.SettingsInternalDNS{
31-
IP: targetIP,
32-
Timeout: dialTimeout,
32+
AddrPort: netip.AddrPortFrom(targetIP, defaultDNSPort),
33+
Timeout: dialTimeout,
3334
}
3435
nameserver.UseDNSInternally(settingsInternalDNS)
3536

3637
settingsSystemWide := nameserver.SettingsSystemDNS{
37-
IP: targetIP,
38+
IPs: []netip.Addr{targetIP},
3839
ResolvPath: l.resolvConf,
3940
}
4041
err := nameserver.UseDNSSystemWide(settingsSystemWide)

internal/dns/run.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,8 @@ import (
1010
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
1111
defer close(done)
1212

13-
if *l.GetSettings().KeepNameserver {
14-
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " +
15-
"this will likely leak DNS traffic outside the VPN " +
16-
"and go through your container network DNS outside the VPN tunnel!")
17-
} else {
18-
const fallback = false
19-
l.useUnencryptedDNS(fallback)
20-
}
13+
const fallback = false
14+
l.useUnencryptedDNS(fallback)
2115

2216
select {
2317
case <-l.start:
@@ -31,7 +25,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
3125
var runError <-chan error
3226

3327
settings := l.GetSettings()
34-
for !*settings.KeepNameserver && *settings.DoT.Enabled {
28+
for *settings.DoT.Enabled {
3529
var err error
3630
runError, err = l.setupServer(ctx)
3731
if err == nil {
@@ -56,7 +50,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
5650
}
5751

5852
settings = l.GetSettings()
59-
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
53+
if !*settings.DoT.Enabled {
6054
const fallback = false
6155
l.useUnencryptedDNS(fallback)
6256
}

internal/dns/settings.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/qdm12/dns/v2/pkg/middlewares/cache/lru"
1010
filtermiddleware "github.com/qdm12/dns/v2/pkg/middlewares/filter"
1111
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
12+
"github.com/qdm12/dns/v2/pkg/middlewares/localdns"
1213
"github.com/qdm12/dns/v2/pkg/provider"
1314
"github.com/qdm12/dns/v2/pkg/server"
1415
"github.com/qdm12/gluetun/internal/configuration/settings"
@@ -70,5 +71,17 @@ func buildDoTSettings(settings settings.DNS,
7071
}
7172
serverSettings.Middlewares = append(serverSettings.Middlewares, filterMiddleware)
7273

74+
localDNSMiddleware, err := localdns.New(localdns.Settings{
75+
Resolvers: settings.LocalResolvers, // auto-detected at container start only
76+
Logger: logger,
77+
})
78+
if err != nil {
79+
return server.Settings{}, fmt.Errorf("creating local DNS middleware: %w", err)
80+
}
81+
// Place after cache middleware, since we want to avoid caching for local
82+
// hostnames that may change regularly.
83+
// Place after filter middleware to avoid conflicts with the rebinding protection.
84+
serverSettings.Middlewares = append(serverSettings.Middlewares, localDNSMiddleware)
85+
7386
return serverSettings, nil
7487
}

internal/dns/setup.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"net/netip"
78

89
"github.com/qdm12/dns/v2/pkg/check"
910
"github.com/qdm12/dns/v2/pkg/nameserver"
@@ -37,11 +38,12 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
3738
l.server = server
3839

3940
// use internal DNS server
41+
const defaultDNSPort = 53
4042
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
41-
IP: settings.ServerAddress,
43+
AddrPort: netip.AddrPortFrom(settings.ServerAddress, defaultDNSPort),
4244
})
4345
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
44-
IP: settings.ServerAddress,
46+
IPs: []netip.Addr{settings.ServerAddress},
4547
ResolvPath: l.resolvConf,
4648
})
4749
if err != nil {

0 commit comments

Comments
 (0)