Skip to content

Commit 49b5ed5

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 ef9086a commit 49b5ed5

11 files changed

+337
-13
lines changed

.golangci.yml

+2
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ issues:
171171
text: 'dnsTimeout is a global variable'
172172
- path: challenge/dns01/nameserver_test.go
173173
text: 'findXByFqdnTestCases is a global variable'
174+
- path: challenge/dns01/network.go
175+
text: 'currentNetworkStack is a global variable'
174176
- path: challenge/http01/domain_matcher.go
175177
text: 'string `Host` has \d occurrences, make it a constant'
176178
- path: challenge/http01/domain_matcher.go

challenge/dns01/nameserver.go

+10-4
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,8 @@ func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {
265265

266266
func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {
267267
if ok, _ := strconv.ParseBool(os.Getenv("LEGO_EXPERIMENTAL_DNS_TCP_ONLY")); ok {
268-
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
268+
network := currentNetworkStack.Network("tcp")
269+
tcp := &dns.Client{Net: network, Timeout: dnsTimeout}
269270
r, _, err := tcp.Exchange(m, ns)
270271
if err != nil {
271272
return r, &DNSError{Message: "DNS call error", MsgIn: m, NS: ns, Err: err}
@@ -274,11 +275,16 @@ func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {
274275
return r, nil
275276
}
276277

277-
udp := &dns.Client{Net: "udp", Timeout: dnsTimeout}
278+
udpNetwork := currentNetworkStack.Network("udp")
279+
udp := &dns.Client{Net: udpNetwork, Timeout: dnsTimeout}
278280
r, _, err := udp.Exchange(m, ns)
279281

280-
if r != nil && r.Truncated {
281-
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
282+
// We can encounter a net.OpError if the nameserver is not listening
283+
// on UDP at all, i.e. net.Dial could not make a connection.
284+
var opErr *net.OpError
285+
if (r != nil && r.Truncated) || errors.As(err, &opErr) {
286+
tcpNetwork := currentNetworkStack.Network("tcp")
287+
tcp := &dns.Client{Net: tcpNetwork, Timeout: dnsTimeout}
282288
// If the TCP request succeeds, the "err" will reset to nil
283289
r, _, err = tcp.Exchange(m, ns)
284290
}

challenge/dns01/nameserver_test.go

+123-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,133 @@ package dns01
22

33
import (
44
"errors"
5+
"net"
56
"sort"
7+
"sync"
68
"testing"
79

810
"github.com/miekg/dns"
911
"github.com/stretchr/testify/assert"
1012
"github.com/stretchr/testify/require"
1113
)
1214

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

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),
@@ -60,6 +73,10 @@ func TestProviderServer_GetAddress(t *testing.T) {
6073
t.Run(test.desc, func(t *testing.T) {
6174
t.Parallel()
6275

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

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)