Skip to content

Commit 707cf0d

Browse files
committed
Add -4 and -6 flags (redo)
This PR is a redo of go-acme#1802. Since that PR has been idle so long, the branches have diverged quite a bit and it was easier to start anew. The work in this PR includes the work originally done by @dmke in go-acme#1802. This PR is to resolve go-acme#1801.
1 parent 0435924 commit 707cf0d

11 files changed

+347
-21
lines changed

.golangci.yml

+2
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ issues:
167167
text: 'dnsTimeout is a global variable'
168168
- path: challenge/dns01/nameserver_test.go
169169
text: 'findXByFqdnTestCases is a global variable'
170+
- path: challenge/dns01/network.go
171+
text: 'currentNetworkStack is a global variable'
170172
- path: challenge/http01/domain_matcher.go
171173
text: 'string `Host` has \d occurrences, make it a constant'
172174
- path: challenge/http01/domain_matcher.go

challenge/dns01/nameserver.go

+10-4
Original file line numberDiff line numberDiff line change
@@ -253,17 +253,23 @@ func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {
253253

254254
func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {
255255
if ok, _ := strconv.ParseBool(os.Getenv("LEGO_EXPERIMENTAL_DNS_TCP_ONLY")); ok {
256-
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
256+
network := currentNetworkStack.Network("tcp")
257+
tcp := &dns.Client{Net: network, Timeout: dnsTimeout}
257258
in, _, err := tcp.Exchange(m, ns)
258259

259260
return in, err
260261
}
261262

262-
udp := &dns.Client{Net: "udp", Timeout: dnsTimeout}
263+
udpNetwork := currentNetworkStack.Network("udp")
264+
udp := &dns.Client{Net: udpNetwork, Timeout: dnsTimeout}
263265
in, _, err := udp.Exchange(m, ns)
264266

265-
if in != nil && in.Truncated {
266-
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
267+
// We can encounter a net.OpError if the nameserver is not listening
268+
// on UDP at all, i.e. net.Dial could not make a connection.
269+
var opErr *net.OpError
270+
if (in != nil && in.Truncated) || errors.As(err, &opErr) {
271+
tcpNetwork := currentNetworkStack.Network("tcp")
272+
tcp := &dns.Client{Net: tcpNetwork, Timeout: dnsTimeout}
267273
// If the TCP request succeeds, the "err" will reset to nil
268274
in, _, err = tcp.Exchange(m, ns)
269275
}

challenge/dns01/nameserver_test.go

+133-10
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,133 @@
11
package dns01
22

33
import (
4+
"net"
45
"sort"
6+
"sync"
57
"testing"
68

9+
"github.com/miekg/dns"
710
"github.com/stretchr/testify/assert"
811
"github.com/stretchr/testify/require"
912
)
1013

14+
func testDNSHandler(writer dns.ResponseWriter, reply *dns.Msg) {
15+
msg := dns.Msg{}
16+
msg.SetReply(reply)
17+
18+
if reply.Question[0].Qtype == dns.TypeA {
19+
msg.Authoritative = true
20+
domain := msg.Question[0].Name
21+
msg.Answer = append(
22+
msg.Answer,
23+
&dns.A{
24+
Hdr: dns.RR_Header{
25+
Name: domain,
26+
Rrtype: dns.TypeA,
27+
Class: dns.ClassINET,
28+
Ttl: 60,
29+
},
30+
A: net.IPv4(127, 0, 0, 1),
31+
},
32+
)
33+
}
34+
35+
_ = writer.WriteMsg(&msg)
36+
}
37+
38+
// getTestNameserver constructs a new DNS server on a local address, or set
39+
// of addresses, that responds to an `A` query for `example.com`.
40+
func getTestNameserver(t *testing.T, network string) *dns.Server {
41+
t.Helper()
42+
server := &dns.Server{
43+
Handler: dns.HandlerFunc(testDNSHandler),
44+
Net: network,
45+
}
46+
switch network {
47+
case "tcp", "udp":
48+
server.Addr = "0.0.0.0:0"
49+
case "tcp4", "udp4":
50+
server.Addr = "127.0.0.1:0"
51+
case "tcp6", "udp6":
52+
server.Addr = "[::1]:0"
53+
}
54+
55+
waitLock := sync.Mutex{}
56+
waitLock.Lock()
57+
server.NotifyStartedFunc = waitLock.Unlock
58+
59+
go func() { _ = server.ListenAndServe() }()
60+
61+
waitLock.Lock()
62+
return server
63+
}
64+
65+
func startTestNameserver(t *testing.T, stack networkStack, proto string) (shutdown func(), addr string) {
66+
t.Helper()
67+
currentNetworkStack = stack
68+
srv := getTestNameserver(t, currentNetworkStack.Network(proto))
69+
70+
shutdown = func() { _ = srv.Shutdown() }
71+
if proto == "tcp" {
72+
addr = srv.Listener.Addr().String()
73+
} else {
74+
addr = srv.PacketConn.LocalAddr().String()
75+
}
76+
return
77+
}
78+
79+
func TestSendDNSQuery(t *testing.T) {
80+
currentNameservers := recursiveNameservers
81+
82+
t.Cleanup(func() {
83+
recursiveNameservers = currentNameservers
84+
currentNetworkStack = dualStack
85+
})
86+
87+
t.Run("does udp4 only", func(t *testing.T) {
88+
stop, addr := startTestNameserver(t, ipv4only, "udp")
89+
defer stop()
90+
91+
recursiveNameservers = ParseNameservers([]string{addr})
92+
msg := createDNSMsg("example.com.", dns.TypeA, true)
93+
result, queryError := sendDNSQuery(msg, addr)
94+
require.NoError(t, queryError)
95+
assert.Equal(t, result.Answer[0].(*dns.A).A.String(), "127.0.0.1")
96+
})
97+
98+
t.Run("does udp6 only", func(t *testing.T) {
99+
stop, addr := startTestNameserver(t, ipv6only, "udp")
100+
defer stop()
101+
102+
recursiveNameservers = ParseNameservers([]string{addr})
103+
msg := createDNSMsg("example.com.", dns.TypeA, true)
104+
result, queryError := sendDNSQuery(msg, addr)
105+
require.NoError(t, queryError)
106+
assert.Equal(t, result.Answer[0].(*dns.A).A.String(), "127.0.0.1")
107+
})
108+
109+
t.Run("does tcp4 and tcp6", func(t *testing.T) {
110+
stop, addr := startTestNameserver(t, dualStack, "tcp")
111+
host, port, _ := net.SplitHostPort(addr)
112+
defer stop()
113+
t.Logf("### port: %s", port)
114+
115+
addr6 := net.JoinHostPort(host, port)
116+
recursiveNameservers = ParseNameservers([]string{addr6})
117+
msg := createDNSMsg("example.com.", dns.TypeA, true)
118+
result, queryError := sendDNSQuery(msg, addr6)
119+
require.NoError(t, queryError)
120+
assert.Equal(t, result.Answer[0].(*dns.A).A.String(), "127.0.0.1")
121+
122+
addr4 := net.JoinHostPort("127.0.0.1", port)
123+
recursiveNameservers = ParseNameservers([]string{addr4})
124+
msg = createDNSMsg("example.com.", dns.TypeA, true)
125+
result, queryError = sendDNSQuery(msg, addr4)
126+
require.NoError(t, queryError)
127+
assert.Equal(t, result.Answer[0].(*dns.A).A.String(), "127.0.0.1")
128+
})
129+
}
130+
11131
func TestLookupNameserversOK(t *testing.T) {
12132
testCases := []struct {
13133
fqdn string
@@ -74,7 +194,7 @@ var findXByFqdnTestCases = []struct {
74194
zone string
75195
primaryNs string
76196
nameservers []string
77-
expectedError string
197+
expectedError string // regular expression
78198
}{
79199
{
80200
desc: "domain is a CNAME",
@@ -109,7 +229,7 @@ var findXByFqdnTestCases = []struct {
109229
fqdn: "test.lego.zz.",
110230
zone: "lego.zz.",
111231
nameservers: []string{"8.8.8.8:53"},
112-
expectedError: "could not find the start of authority for test.lego.zz.: NXDOMAIN",
232+
expectedError: `^could not find the start of authority for test\.lego\.zz.: NXDOMAIN`,
113233
},
114234
{
115235
desc: "several non existent nameservers",
@@ -119,18 +239,21 @@ var findXByFqdnTestCases = []struct {
119239
nameservers: []string{":7053", ":8053", "8.8.8.8:53"},
120240
},
121241
{
122-
desc: "only non-existent nameservers",
123-
fqdn: "mail.google.com.",
124-
zone: "google.com.",
125-
nameservers: []string{":7053", ":8053", ":9053"},
126-
expectedError: "could not find the start of authority for mail.google.com.: read udp",
242+
desc: "only non-existent nameservers",
243+
fqdn: "mail.google.com.",
244+
zone: "google.com.",
245+
nameservers: []string{":7053", ":8053", ":9053"},
246+
// NOTE: On Windows, net.DialContext finds a way down to the ContectEx syscall.
247+
// There a fault is marked as "connectex", not "connect", see
248+
// https://cs.opensource.google/go/go/+/refs/tags/go1.19.5:src/net/fd_windows.go;l=112
249+
expectedError: `^could not find the start of authority for mail\.google\.com.: dial tcp :9053: connect(ex)?:`,
127250
},
128251
{
129252
desc: "no nameservers",
130253
fqdn: "test.ldez.com.",
131254
zone: "ldez.com.",
132255
nameservers: []string{},
133-
expectedError: "could not find the start of authority for test.ldez.com.",
256+
expectedError: `^could not find the start of authority for test\.ldez\.com\.`,
134257
},
135258
}
136259

@@ -142,7 +265,7 @@ func TestFindZoneByFqdnCustom(t *testing.T) {
142265
zone, err := FindZoneByFqdnCustom(test.fqdn, test.nameservers)
143266
if test.expectedError != "" {
144267
require.Error(t, err)
145-
assert.Contains(t, err.Error(), test.expectedError)
268+
assert.Regexp(t, test.expectedError, err.Error())
146269
} else {
147270
require.NoError(t, err)
148271
assert.Equal(t, test.zone, zone)
@@ -159,7 +282,7 @@ func TestFindPrimaryNsByFqdnCustom(t *testing.T) {
159282
ns, err := FindPrimaryNsByFqdnCustom(test.fqdn, test.nameservers)
160283
if test.expectedError != "" {
161284
require.Error(t, err)
162-
assert.Contains(t, err.Error(), test.expectedError)
285+
assert.Regexp(t, test.expectedError, err.Error())
163286
} else {
164287
require.NoError(t, err)
165288
assert.Equal(t, test.primaryNs, ns)

challenge/dns01/network.go

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package dns01
2+
3+
// networkStack is used to indicate which IP stack should be used for DNS queries.
4+
type networkStack int
5+
6+
const (
7+
dualStack networkStack = iota
8+
ipv4only
9+
ipv6only
10+
)
11+
12+
// currentNetworkStack is used to define which IP stack will be used. The default is
13+
// both IPv4 and IPv6. Set to IPv4Only or IPv6Only to select either version.
14+
var currentNetworkStack = dualStack
15+
16+
// Network interprets the NetworkStack setting in relation to the desired
17+
// protocol. The proto value should be either "udp" or "tcp".
18+
func (s networkStack) Network(proto string) string {
19+
// The DNS client passes whatever value is set in (*dns.Client).Net to
20+
// the [net.Dialer](https://github.com/miekg/dns/blob/fe20d5d/client.go#L119-L141).
21+
// And the net.Dialer accepts strings such as "udp4" or "tcp6"
22+
// (https://cs.opensource.google/go/go/+/refs/tags/go1.18.9:src/net/dial.go;l=167-182).
23+
switch s {
24+
case ipv4only:
25+
return proto + "4"
26+
case ipv6only:
27+
return proto + "6"
28+
default:
29+
return proto
30+
}
31+
}
32+
33+
// SetIPv4Only forces DNS queries to only happen over the IPv4 stack.
34+
func SetIPv4Only() { currentNetworkStack = ipv4only }
35+
36+
// SetIPv6Only forces DNS queries to only happen over the IPv6 stack.
37+
func SetIPv6Only() { currentNetworkStack = ipv6only }
38+
39+
// SetDualStack indicates that both IPv4 and IPv6 should be allowed.
40+
// This setting lets the OS determine which IP stack to use.
41+
func SetDualStack() { currentNetworkStack = dualStack }

challenge/http01/http_challenge_server.go

+22
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,28 @@ func NewUnixProviderServer(socketPath string, mode fs.FileMode) *ProviderServer
4141
return &ProviderServer{network: "unix", address: socketPath, socketMode: mode, matcher: &hostMatcher{}}
4242
}
4343

44+
// SetIPv4Only starts the challenge server on an IPv4 address.
45+
//
46+
// Calling this method has no effect if s was created with NewUnixProviderServer.
47+
func (s *ProviderServer) SetIPv4Only() { s.setTCPStack("tcp4") }
48+
49+
// SetIPv6Only starts the challenge server on an IPv6 address.
50+
//
51+
// Calling this method has no effect if s was created with NewUnixProviderServer.
52+
func (s *ProviderServer) SetIPv6Only() { s.setTCPStack("tcp6") }
53+
54+
// SetDualStack indicates that both IPv4 and IPv6 should be allowed.
55+
// This setting lets the OS determine which IP stack to use for the challenge server.
56+
//
57+
// Calling this method has no effect if s was created with NewUnixProviderServer.
58+
func (s *ProviderServer) SetDualStack() { s.setTCPStack("tcp") }
59+
60+
func (s *ProviderServer) setTCPStack(network string) {
61+
if s.network != "unix" {
62+
s.network = network
63+
}
64+
}
65+
4466
// Present starts a web server and makes the token available at `ChallengePath(token)` for web requests.
4567
func (s *ProviderServer) Present(domain, token, keyAuth string) error {
4668
var err error

challenge/http01/http_challenge_test.go

+17
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func TestProviderServer_GetAddress(t *testing.T) {
3232
testCases := []struct {
3333
desc string
3434
server *ProviderServer
35+
network func(server *ProviderServer)
3536
expected string
3637
}{
3738
{
@@ -49,6 +50,18 @@ func TestProviderServer_GetAddress(t *testing.T) {
4950
server: NewProviderServer("localhost", "8080"),
5051
expected: "localhost:8080",
5152
},
53+
{
54+
desc: "TCP4 with host and port",
55+
server: NewProviderServer("localhost", "8080"),
56+
network: func(s *ProviderServer) { s.SetIPv4Only() },
57+
expected: "localhost:8080",
58+
},
59+
{
60+
desc: "TCP6 with host and port",
61+
server: NewProviderServer("localhost", "8080"),
62+
network: func(s *ProviderServer) { s.SetIPv6Only() },
63+
expected: "localhost:8080",
64+
},
5265
{
5366
desc: "UDS socket",
5467
server: NewUnixProviderServer(sock, fs.ModeSocket|0o666),
@@ -61,6 +74,10 @@ func TestProviderServer_GetAddress(t *testing.T) {
6174
t.Run(test.desc, func(t *testing.T) {
6275
t.Parallel()
6376

77+
if test.network != nil {
78+
test.network(test.server)
79+
}
80+
6481
address := test.server.GetAddress()
6582
assert.Equal(t, test.expected, address)
6683
})

challenge/tlsalpn01/tls_alpn_challenge_server.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,30 @@ const (
2626
type ProviderServer struct {
2727
iface string
2828
port string
29+
network string
2930
listener net.Listener
3031
}
3132

3233
// NewProviderServer creates a new ProviderServer on the selected interface and port.
3334
// Setting iface and / or port to an empty string will make the server fall back to
3435
// the "any" interface and port 443 respectively.
3536
func NewProviderServer(iface, port string) *ProviderServer {
36-
return &ProviderServer{iface: iface, port: port}
37+
if port == "" {
38+
port = defaultTLSPort
39+
}
40+
return &ProviderServer{iface: iface, port: port, network: "tcp"}
3741
}
3842

43+
// SetIPv4Only starts the challenge server on an IPv4 address.
44+
func (s *ProviderServer) SetIPv4Only() { s.network = "tcp4" }
45+
46+
// SetIPv6Only starts the challenge server on an IPv6 address.
47+
func (s *ProviderServer) SetIPv6Only() { s.network = "tcp6" }
48+
49+
// SetDualStack indicates that both IPv4 and IPv6 should be allowed.
50+
// This setting lets the OS determine which IP stack to use for the challenge server.
51+
func (s *ProviderServer) SetDualStack() { s.network = "tcp" }
52+
3953
func (s *ProviderServer) GetAddress() string {
4054
return net.JoinHostPort(s.iface, s.port)
4155
}
@@ -65,7 +79,7 @@ func (s *ProviderServer) Present(domain, token, keyAuth string) error {
6579
tlsConf.NextProtos = []string{ACMETLS1Protocol}
6680

6781
// Create the listener with the created tls.Config.
68-
s.listener, err = tls.Listen("tcp", s.GetAddress(), tlsConf)
82+
s.listener, err = tls.Listen(s.network, s.GetAddress(), tlsConf)
6983
if err != nil {
7084
return fmt.Errorf("could not start HTTPS server for challenge: %w", err)
7185
}

0 commit comments

Comments
 (0)