mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-05 08:54:09 -04:00
change(publicip/dns): use DNS over TLS only
- Fix critical issue #492 - Remove `google` dns provider since it does not support DNS over TLS
This commit is contained in:
@@ -1,16 +1,10 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
import "time"
|
||||
|
||||
type Fetcher struct {
|
||||
ring ring
|
||||
client Client
|
||||
client4 Client
|
||||
client6 Client
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
type ring struct {
|
||||
@@ -28,29 +22,11 @@ func New(options ...Option) (f *Fetcher, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: settings.timeout,
|
||||
}
|
||||
|
||||
return &Fetcher{
|
||||
ring: ring{
|
||||
counter: new(uint32),
|
||||
providers: settings.providers,
|
||||
},
|
||||
client: &dns.Client{
|
||||
Net: "udp",
|
||||
Dialer: dialer,
|
||||
Timeout: settings.timeout,
|
||||
},
|
||||
client4: &dns.Client{
|
||||
Net: "udp4",
|
||||
Dialer: dialer,
|
||||
Timeout: settings.timeout,
|
||||
},
|
||||
client6: &dns.Client{
|
||||
Net: "udp6",
|
||||
Dialer: dialer,
|
||||
Timeout: settings.timeout,
|
||||
},
|
||||
timeout: settings.timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -16,7 +16,4 @@ func Test_New(t *testing.T) {
|
||||
|
||||
assert.NotNil(t, impl.ring.counter)
|
||||
assert.NotEmpty(t, impl.ring.providers)
|
||||
assert.NotNil(t, impl.client)
|
||||
assert.NotNil(t, impl.client4)
|
||||
assert.NotNil(t, impl.client6)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNetworkNotSupported = errors.New("network not supported")
|
||||
ErrAnswerNotReceived = errors.New("response answer not received")
|
||||
ErrAnswerTypeMismatch = errors.New("answer type is not expected")
|
||||
ErrAnswerTypeNotSupported = errors.New("answer type not supported")
|
||||
@@ -17,8 +19,21 @@ var (
|
||||
ErrIPMalformed = errors.New("IP address malformed")
|
||||
)
|
||||
|
||||
func fetch(ctx context.Context, client Client, providerData providerData) (
|
||||
publicIPs []netip.Addr, err error) {
|
||||
func fetch(ctx context.Context, client Client, network string,
|
||||
providerData providerData) (publicIPs []netip.Addr, err error) {
|
||||
var serverHost string
|
||||
switch network {
|
||||
case "tcp":
|
||||
serverHost = providerData.TLSName
|
||||
case "tcp4":
|
||||
serverHost = providerData.IPv4.String()
|
||||
case "tcp6":
|
||||
serverHost = providerData.IPv6.String()
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", ErrNetworkNotSupported, network)
|
||||
}
|
||||
serverAddress := net.JoinHostPort(serverHost, "853")
|
||||
|
||||
message := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
@@ -32,7 +47,7 @@ func fetch(ctx context.Context, client Client, providerData providerData) (
|
||||
},
|
||||
}
|
||||
|
||||
r, _, err := client.ExchangeContext(ctx, message, providerData.nameserver)
|
||||
r, _, err := client.ExchangeContext(ctx, message, serverAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package dns
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -18,10 +19,10 @@ func Test_fetch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
providerData := providerData{
|
||||
nameserver: "nameserver",
|
||||
fqdn: "record",
|
||||
class: dns.ClassNONE,
|
||||
qType: dns.Type(dns.TypeTXT),
|
||||
TLSName: "nameserver",
|
||||
fqdn: "record",
|
||||
class: dns.ClassNONE,
|
||||
qType: dns.Type(dns.TypeTXT),
|
||||
}
|
||||
|
||||
expectedMessage := &dns.Msg{
|
||||
@@ -101,11 +102,13 @@ func Test_fetch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client := mock_dns.NewMockClient(ctrl)
|
||||
expectedAddress := net.JoinHostPort(providerData.TLSName, "853")
|
||||
client.EXPECT().
|
||||
ExchangeContext(ctx, expectedMessage, providerData.nameserver).
|
||||
ExchangeContext(ctx, expectedMessage, expectedAddress).
|
||||
Return(testCase.response, time.Millisecond, testCase.exchangeErr)
|
||||
|
||||
publicIPs, err := fetch(ctx, client, providerData)
|
||||
const network = "tcp" // so it picks the TLSName field as the address
|
||||
publicIPs, err := fetch(ctx, client, network, providerData)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
func Test_integration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fetcher, err := New(SetProviders(Google, Cloudflare, OpenDNS))
|
||||
fetcher, err := New(SetProviders(Cloudflare, OpenDNS))
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -27,12 +27,7 @@ func Test_integration(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, publicIP2)
|
||||
|
||||
publicIP3, err := fetcher.IP4(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, publicIP2)
|
||||
|
||||
assert.Equal(t, publicIP1, publicIP2)
|
||||
assert.Equal(t, publicIP1, publicIP3)
|
||||
assert.Equal(t, publicIP1.String(), publicIP2.String())
|
||||
|
||||
t.Logf("Public IP is %s", publicIP1)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -13,7 +16,7 @@ var (
|
||||
)
|
||||
|
||||
func (f *Fetcher) IP(ctx context.Context) (publicIP netip.Addr, err error) {
|
||||
publicIPs, err := f.ip(ctx, f.client)
|
||||
publicIPs, err := f.ip(ctx, "tcp")
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
@@ -21,7 +24,7 @@ func (f *Fetcher) IP(ctx context.Context) (publicIP netip.Addr, err error) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) IP4(ctx context.Context) (publicIP netip.Addr, err error) {
|
||||
publicIPs, err := f.ip(ctx, f.client4)
|
||||
publicIPs, err := f.ip(ctx, "tcp4")
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
@@ -35,7 +38,7 @@ func (f *Fetcher) IP4(ctx context.Context) (publicIP netip.Addr, err error) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) IP6(ctx context.Context) (publicIP netip.Addr, err error) {
|
||||
publicIPs, err := f.ip(ctx, f.client6)
|
||||
publicIPs, err := f.ip(ctx, "tcp6")
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
@@ -48,9 +51,19 @@ func (f *Fetcher) IP6(ctx context.Context) (publicIP netip.Addr, err error) {
|
||||
return netip.Addr{}, fmt.Errorf("%w: ipv6", ErrIPNotFoundForVersion)
|
||||
}
|
||||
|
||||
func (f *Fetcher) ip(ctx context.Context, client Client) (
|
||||
func (f *Fetcher) ip(ctx context.Context, network string) (
|
||||
publicIPs []netip.Addr, err error) {
|
||||
index := int(atomic.AddUint32(f.ring.counter, 1)) % len(f.ring.providers)
|
||||
provider := f.ring.providers[index]
|
||||
return fetch(ctx, client, provider.data())
|
||||
providerData := f.ring.providers[index].data()
|
||||
|
||||
client := &dns.Client{
|
||||
Net: network + "-tls",
|
||||
Timeout: f.timeout,
|
||||
TLSConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ServerName: providerData.TLSName,
|
||||
},
|
||||
}
|
||||
|
||||
return fetch(ctx, client, network, providerData)
|
||||
}
|
||||
|
||||
@@ -31,18 +31,18 @@ func Test_SetProviders(t *testing.T) {
|
||||
initialSettings: settings{
|
||||
providers: []Provider{Cloudflare},
|
||||
},
|
||||
providers: []Provider{Google},
|
||||
providers: []Provider{OpenDNS},
|
||||
expectedSettings: settings{
|
||||
providers: []Provider{Google},
|
||||
providers: []Provider{OpenDNS},
|
||||
},
|
||||
},
|
||||
"Google and Cloudflare": {
|
||||
"OpenDNS and Cloudflare": {
|
||||
initialSettings: settings{
|
||||
providers: []Provider{Cloudflare},
|
||||
},
|
||||
providers: []Provider{Google, Cloudflare},
|
||||
providers: []Provider{OpenDNS, Cloudflare},
|
||||
expectedSettings: settings{
|
||||
providers: []Provider{Cloudflare, Google},
|
||||
providers: []Provider{Cloudflare, OpenDNS},
|
||||
},
|
||||
},
|
||||
"invalid provider": {
|
||||
|
||||
@@ -3,6 +3,7 @@ package dns
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@@ -11,14 +12,12 @@ type Provider string
|
||||
|
||||
const (
|
||||
Cloudflare Provider = "cloudflare"
|
||||
Google Provider = "google"
|
||||
OpenDNS Provider = "opendns"
|
||||
)
|
||||
|
||||
func ListProviders() []Provider {
|
||||
return []Provider{
|
||||
Cloudflare,
|
||||
Google,
|
||||
OpenDNS,
|
||||
}
|
||||
}
|
||||
@@ -35,35 +34,45 @@ func ValidateProvider(provider Provider) error {
|
||||
}
|
||||
|
||||
type providerData struct {
|
||||
nameserver string
|
||||
fqdn string
|
||||
class dns.Class
|
||||
qType dns.Type
|
||||
// Address for IPv4 or IPv6.
|
||||
Address string
|
||||
IPv4 netip.Addr
|
||||
IPv6 netip.Addr
|
||||
TLSName string
|
||||
fqdn string
|
||||
class dns.Class
|
||||
qType dns.Type
|
||||
}
|
||||
|
||||
func (provider Provider) data() providerData {
|
||||
switch provider {
|
||||
case Google:
|
||||
return providerData{
|
||||
nameserver: "ns1.google.com:53",
|
||||
fqdn: "o-o.myaddr.l.google.com.",
|
||||
class: dns.ClassINET,
|
||||
qType: dns.Type(dns.TypeTXT),
|
||||
}
|
||||
func (p Provider) data() providerData {
|
||||
switch p {
|
||||
// Note on deprecating Google:
|
||||
// Only their nameserver ns1.google.com returns your public IP address.
|
||||
// All their other nameservers return the closest Google datacenter IP.
|
||||
// Unfortunately, ns1.google.com is not compatible with DNS over TLS,
|
||||
// and dns.google.com is but does not echo your IP address.
|
||||
// dig TXT @ns1.google.com o-o.myaddr.l.google.com +tls
|
||||
// dig TXT @dns.google.com o-o.myaddr.l.google.com +tls
|
||||
case Cloudflare:
|
||||
return providerData{
|
||||
nameserver: "one.one.one.one:53",
|
||||
fqdn: "whoami.cloudflare.",
|
||||
class: dns.ClassCHAOS,
|
||||
qType: dns.Type(dns.TypeTXT),
|
||||
Address: "1dot1dot1dot1.cloudflare-dns.com",
|
||||
IPv4: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
||||
IPv6: netip.AddrFrom16([16]byte{0x26, 0x6, 0x47, 0x0, 0x47, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x11, 0x11}), //nolint:lll
|
||||
TLSName: "cloudflare-dns.com",
|
||||
fqdn: "whoami.cloudflare.",
|
||||
class: dns.ClassCHAOS,
|
||||
qType: dns.Type(dns.TypeTXT),
|
||||
}
|
||||
case OpenDNS:
|
||||
return providerData{
|
||||
nameserver: "resolver1.opendns.com:53",
|
||||
fqdn: "myip.opendns.com.",
|
||||
class: dns.ClassINET,
|
||||
qType: dns.Type(dns.TypeANY),
|
||||
Address: "dns.opendns.com",
|
||||
IPv4: netip.AddrFrom4([4]byte{208, 67, 222, 222}),
|
||||
IPv6: netip.AddrFrom16([16]byte{0x26, 0x20, 0x1, 0x19, 0x0, 0x35, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x35}), //nolint:lll
|
||||
TLSName: "dns.opendns.com",
|
||||
fqdn: "myip.opendns.com.",
|
||||
class: dns.ClassINET,
|
||||
qType: dns.Type(dns.TypeANY),
|
||||
}
|
||||
}
|
||||
panic(`provider unknown: "` + string(provider) + `"`)
|
||||
panic(`provider unknown: "` + string(p) + `"`)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
@@ -17,7 +18,7 @@ func Test_ValidateProvider(t *testing.T) {
|
||||
err error
|
||||
}{
|
||||
"valid provider": {
|
||||
provider: Google,
|
||||
provider: Cloudflare,
|
||||
},
|
||||
"invalid provider": {
|
||||
provider: Provider("invalid"),
|
||||
@@ -49,31 +50,28 @@ func Test_data(t *testing.T) {
|
||||
data providerData
|
||||
panicMessage string
|
||||
}{
|
||||
"google": {
|
||||
provider: Google,
|
||||
data: providerData{
|
||||
nameserver: "ns1.google.com:53",
|
||||
fqdn: "o-o.myaddr.l.google.com.",
|
||||
class: dns.ClassINET,
|
||||
qType: dns.Type(dns.TypeTXT),
|
||||
},
|
||||
},
|
||||
"cloudflare": {
|
||||
provider: Cloudflare,
|
||||
data: providerData{
|
||||
nameserver: "one.one.one.one:53",
|
||||
fqdn: "whoami.cloudflare.",
|
||||
class: dns.ClassCHAOS,
|
||||
qType: dns.Type(dns.TypeTXT),
|
||||
Address: "1dot1dot1dot1.cloudflare-dns.com",
|
||||
IPv4: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
||||
IPv6: netip.AddrFrom16([16]byte{0x26, 0x6, 0x47, 0x0, 0x47, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x11, 0x11}), //nolint:lll
|
||||
TLSName: "cloudflare-dns.com",
|
||||
fqdn: "whoami.cloudflare.",
|
||||
class: dns.ClassCHAOS,
|
||||
qType: dns.Type(dns.TypeTXT),
|
||||
},
|
||||
},
|
||||
"opendns": {
|
||||
provider: OpenDNS,
|
||||
data: providerData{
|
||||
nameserver: "resolver1.opendns.com:53",
|
||||
fqdn: "myip.opendns.com.",
|
||||
class: dns.ClassINET,
|
||||
qType: dns.Type(dns.TypeANY),
|
||||
Address: "dns.opendns.com",
|
||||
IPv4: netip.AddrFrom4([4]byte{208, 67, 222, 222}),
|
||||
IPv6: netip.AddrFrom16([16]byte{0x26, 0x20, 0x1, 0x19, 0x0, 0x35, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x35}), //nolint:lll
|
||||
TLSName: "dns.opendns.com",
|
||||
fqdn: "myip.opendns.com.",
|
||||
class: dns.ClassINET,
|
||||
qType: dns.Type(dns.TypeANY),
|
||||
},
|
||||
},
|
||||
"invalid provider": {
|
||||
|
||||
Reference in New Issue
Block a user