From 70ee5660e133cedb64cd3942fdb04582c9fbf00e Mon Sep 17 00:00:00 2001
From: cyBerta <cyberta@riseup.net>
Date: Tue, 15 Apr 2025 03:22:24 +0200
Subject: [PATCH] fix index out of range panic in case doh resolution returns
 empty A record slice. While being at it add IPv6 resolution to DoH
 implementation

---
 pkg/bootstrap/doh.go      | 35 +++++++++++++++++++++++++++++++++--
 pkg/bootstrap/doh_test.go | 16 ++++++++++++++++
 pkg/bootstrap/utils.go    | 13 ++++++++++---
 3 files changed, 59 insertions(+), 5 deletions(-)

diff --git a/pkg/bootstrap/doh.go b/pkg/bootstrap/doh.go
index d56bcf0..b96ec9a 100644
--- a/pkg/bootstrap/doh.go
+++ b/pkg/bootstrap/doh.go
@@ -36,16 +36,47 @@ func dohQuery(domain string) (string, error) {
 			HTTPClient: &http.Client{Timeout: 10 * time.Second},
 		}
 
+		// lookup A records for IPv4
 		ips, _, err := resolver.LookupA(domain)
 		if err != nil {
 			log.Warn().
 				Str("resolver", dnsServer).
 				Str("domain", domain).
 				Err(err).
-				Msg("Could not resolve host with DNS over HTTPs")
+				Msg("Could not resolve host's IPv4 address with DNS over HTTPS")
 			continue
 		}
-		return ips[0].IP4, nil
+
+		if len(ips) > 0 {
+			return ips[0].IP4, nil
+		}
+
+		// fallback: lookup AAAA records for IPv6
+		log.Warn().
+			Str("resolver", dnsServer).
+			Str("domain", domain).
+			Err(err).
+			Msg("No A records found for domain")
+
+		v6Ips, _, err := resolver.LookupAAAA(domain)
+		if err != nil {
+			log.Warn().
+				Str("resolver", dnsServer).
+				Str("domain", domain).
+				Err(err).
+				Msg("Could not resolve host's IPv6 address with DNS over HTTPS")
+			continue
+		}
+
+		if len(v6Ips) > 0 {
+			return v6Ips[0].IP6, nil
+		}
+
+		log.Warn().
+			Str("resolver", dnsServer).
+			Str("domain", domain).
+			Err(err).
+			Msg("No AAAA records found for domain")
 	}
 	return "", errors.New("Could not resolve ip with DNS over HTTPS. Tried all resolvers")
 
diff --git a/pkg/bootstrap/doh_test.go b/pkg/bootstrap/doh_test.go
index 4f01b58..1a7d83e 100644
--- a/pkg/bootstrap/doh_test.go
+++ b/pkg/bootstrap/doh_test.go
@@ -2,6 +2,7 @@ package bootstrap
 
 import (
 	"os"
+	"strings"
 	"testing"
 
 	"github.com/rs/zerolog"
@@ -18,4 +19,19 @@ func TestDoh(t *testing.T) {
 	ip, err := dohQuery("leap.se")
 	assert.NoError(t, err, "dohQuery failed")
 	assert.NotNil(t, ip, "ip should not be nil")
+	assert.NotEmpty(t, ip, "ip should not be empty")
+}
+
+func TestDohHandleEmptyRecords(t *testing.T) {
+	ip, err := dohQuery("notexising-kjhfdfghfhjgiuzuzfgfcdxfsa.org")
+	assert.Error(t, err, "dohQuery failed")
+	assert.Empty(t, ip, "ip should be empty")
+}
+
+func TestDohHandleAAAARecords(t *testing.T) {
+	ip, err := dohQuery("ipv6.google.com")
+	assert.NoError(t, err, "dohQuery failed")
+	assert.NotNil(t, ip, "ip should not be nil")
+	assert.NotEmpty(t, ip, "ip should not be empty")
+	assert.Equal(t, true, strings.Contains(ip, ":") && !strings.Contains(ip, "."))
 }
diff --git a/pkg/bootstrap/utils.go b/pkg/bootstrap/utils.go
index 4347bca..871231d 100644
--- a/pkg/bootstrap/utils.go
+++ b/pkg/bootstrap/utils.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"net/url"
 	"strconv"
+	"strings"
 	"time"
 
 	bitmask_storage "0xacab.org/leap/bitmask-core/pkg/storage"
@@ -57,16 +58,22 @@ func (c *Config) getAPIClient() *http.Client {
 							Str("domain", addr).
 							Msg("Resolving host with DNS over HTTPs")
 
-						ip4, err := dohQuery(c.Host)
+						ip, err := dohQuery(c.Host)
 						if err != nil {
 							return nil, err
 						}
 
 						log.Debug().
 							Str("domain", addr).
-							Str("ip4", ip4).
+							Str("ip", ip).
 							Msg("Sucessfully resolved host via DNS over HTTPs")
-						addr = fmt.Sprintf("%s:%d", ip4, c.Port)
+						if strings.Contains(ip, ":") {
+							// IPv6 address requires extra brackets in order to
+							// distinguish address from port
+							addr = fmt.Sprintf("[%s]:%d", ip, c.Port)
+						} else {
+							addr = fmt.Sprintf("%s:%d", ip, c.Port)
+						}
 					}
 
 					roller, err := utls.NewRoller()
-- 
GitLab