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:
Quentin McGaw
2023-06-17 13:31:28 +00:00
parent 828373da7f
commit 320d91d8e3
16 changed files with 124 additions and 107 deletions

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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": {

View File

@@ -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) + `"`)
}

View File

@@ -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": {