Repository: certbot/certbot

Issue:
Standalone mode does not bind to ipv6
As far as I can tell, the letsencrypt client does not bind to ipv6 in standalone mode nor do the commands given in manual mode. While this may not seem too significant, but for people with ipv6 only machines (like me) this is quite a big problem. My suggestion would be to somehow bind to both ipv4 and ipv6 at the same time.

Either way, what you're doing is a great service to the global internet community, keep on rocking!



Discussion:
At the moment the server won't connect over IPv6 to perform validation, either.  So this feature request is blocked (or at least not useful) until [this boulder ticket](https://github.com/letsencrypt/boulder/issues/593) is resolved.

Subticket of #180 

While I do see where you are coming from, I have to disagree with you. Having the client listen on ipv6 is relevant _right now_ because of NAT64.

What is NAT64, you may ask?
Basically, the network admin can set aside a block of ipv6 (the RFC range is 64:ff9b::/96) which will be 1:1 mapped to the ipv4 internet (basically, if you try to connect to the nat64 encoded version of an ipv4 it will automatically route it through to the public ipv4 (and a reverse path exists in my case)).
I am in fact in posession of an ipv4 address, it is however not directly attached to the server in question (it's behind the NAT64). Any connections to my ipv4 arrive as ipv6 on my machine. I've been able to work around the bug by using the manual option and doing some shenanigans to the given python code to make it listen on ipv6. (I can't use my web server directly due to the machine in question functioning primarily as a load balancer, as such it does not have the capability to serve a directory using an ipv6 enabled server like nginx or apache.)

At the very least, I do appreciate that you guys may look at this eventually.

Have you tried running the client under your setup? I don't think there is anything in the code that stops us from "binding" to IPv6. If there is, then please paste the traceback.

There is no error, it's just not binding/listening on ipv6. Basically, it seems to be listening on 0.0.0.0 but not :: (aka, it's listening on all ipv4 ips, but not all ipv6 ips) (I gathered this information with netstat). Now, from taking a cursory glance at the code I can spot why it does this.
Your acme_standalone HTTP01Server class uses six.moves.BaseHTTPServer, which is an alias for the normal python3 class of the same name. (And thus, albeit through some layers of abstraction) is effectively doing the same thing as the commands the manual mode tells you to run. Python is strange among the higher level languages in that there are several places where it doesn't transparently handle ipv6. You have to explicitly tell it to bind to/listen on ipv6.

As an example, here is my modified version of the python SimpleHTTPServer command manual mode has you run (I added some spacing for clarity):

```
import BaseHTTPServer, SimpleHTTPServer, socket

SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {'': 'text/plain'}

class HttpServerV6(BaseHTTPServer.HTTPServer):
    address_family = socket.AF_INET6

s = HttpServerV6(('', 80), SimpleHTTPServer.SimpleHTTPRequestHandler)
s.serve_forever()
```

Interestingly enough, it does seem that python implements "accepting ipv4 over ipv6". What does this mean? Well, if you run this and then check netstat you'll find it's listening on :::80 (port 80 on ::), which means that technically it's only listening on ipv6. BUT ipv6 has this neat socket option called IPV6_V6ONLY (yes, I'm diving a little into the python internals here). Basically, turning this option _off_ (which python does) allows an ipv6 socket that binds to :: (all ips/the ipv6 equivalent to 0.0.0.0) to accept both ipv6 _and_ ipv4 connections. The only real difference here is that the ips in use are ipv6 and that ipv4 ips are "mapped" to ipv6 ips in a way similar to what NAT64 does. Although it uses ::ffff: as the prefix rather than 64:ff9b:: So both `curl http://127.0.0.1/` and `curl "http://\[::1\]:8080/"` work (the \ is because curl is weird like that, you have to escape the [ and ] when entering ipv6 addresses).
Just for reference as to what these ips look like, here's the output I get from the script I posted above when running aformentioned curl commands:

```
::ffff:127.0.0.1 - - [13/Nov/2015 20:44:33] "GET / HTTP/1.1" 200 -
::1 - - [13/Nov/2015 20:45:38] "GET / HTTP/1.1" 200 -
```

Both calls returned the expected data. (For reference to people who've never seen ipv6 before, ::1 is the ipv6 equivalent of 127.0.0.1. Also, I need a shirt that says "No place like ::1".)

I hope this clarifies things a bit/helps.

I don't get why this is an "enhancement" since it can lead to all sorts of problems and weird failures on IPv6 enabled machines, like for my setup (which has perfectly working IPv4 connectivity, by the way):
1. let's encrypt is run behind a proxy which can be required for various reasons (e.g. for docker setups because tools like dockergen for automatic http routing don't like the let's encrypt simple daemon to start/quit so quickly)
2. the proxy does forward proxying to "localhost"
3. localhost resolves to ::1

Boom, broken. Would easily work if it had bound to ::0 instead of 0.0.0.0 which should be a one line change..

Edit: .. and while I could replace "localhost" with "127.0.0.1" in my proxy config, this is also a bad workaround in the long term because once let's encrypt actually supports IPv6 and uses that to reach out from the ACME server to me, I would assume it can trigger new obscure problems - so I'm basically setting myself up for future problems. Therefore, this really should be fixed in the standalone mode as soon as feasible before everyone is forced to bork their configs to work around this.

Edit 2: **treat example above with care,** since I just found out silly me also broke something else in my setup which is possibly the culprit. However, "localhost" does indeed resolve to "::1" on my machine. And listening to IPv6/IPv4 is pretty easy: just choosing ::0 will usually get you a dualstack socket that listens to both

With [IPv6 support in Let's Encrypt](https://letsencrypt.org/2016/07/26/full-ipv6-support.html), we should try to get this issue resolved quickly.

I confirmed that

```
from six.moves import socketserver
socketserver.TCPServer(("", 12345), socketserver.StreamRequestHandler)
```

does not end up listening on IPv6 interfaces. Following the comment above at https://github.com/certbot/certbot/issues/1466#issuecomment-156554125, apparently it might be sufficient to add

```
    address_family = socket.AF_INET6
```

at line 28 of `certbot/acme/acme/standalone.py` and then the servers will succeed in binding IPv6/IPv4 capable sockets. (I don't know if this is a problem on operating systems whose kernels lack any IPv6 support; maybe there's a simple way to do a try/except block that tries to create and delete an AF_INET6 socket, and then falls back to the default behavior on systems where no such socket can be instantiated?)

Perhaps we could do something like

```
    try:
        socket.socket(socket.AF_INET6)
        address_family = socket.AF_INET6
    except socket.error:
        pass
```

The theory is that if the system is unable to create `AF_INET6` sockets at all, then we just shouldn't change the default socket type for the server class to use them. But if it can, then we should. I haven't found a non-IPv6-capable system (i.e., without IPv6 in the kernel) to try this out on!

To see the plausibility that this solution will work, note the error from something like `socket.socket(socket.AF_IPX)` :-)

Confirms the solution works! Without the patch my ipv6-only site cannot be reached by letscrypt server and it reports a TLS-SNI-01 error; with it everything works OK.

Is there already a way to create or renew a certificate for the IPv6 address of a dualstack machine only even if there are A and AAAA records?
Real life example is a dyn.com dyn-DNS host which is behind a NAT-Gateway. The A-record is always the IPv4 of the gateway, the AAAA-record the real IPv6 address. certbot-auto fails due to the A-Record:

```
Processing /etc/letsencrypt/renewal/foo.selfip.org.conf
-------------------------------------------------------------------------------
2016-08-03 10:13:04,353:WARNING:certbot.renewal:Attempting to renew cert
from /etc/letsencrypt/renewal/foo.selfip.org.conf produced an unexpected error:
Failed authorization procedure. foo.selfip.org (tls-sni-01): urn:acme:error:unauthorized :: 
The client lacks sufficient authorization :: Incorrect validation certificate for TLS-SNI-01 challenge.
Requested 05e04a9591435fb04ff69a0a3d871b4b.134a86956fbc13ccb5467e4a29653e29.acme.invalid from 93.218.168.93:443.
Received certificate containing 'bar.homeunix.org'. Skipping.
```

Does someone want to open a pull request containing the reported solution here?

Made one PR in https://github.com/certbot/certbot/pull/3369

I tried the commit with "git checkout 1921e2e0ae" of @jayofdoom but I get this error altough an AAAA record is present and the webserver is reachable on ipv6. This is an ipv6 only hostname.

Failed authorization procedure. ipv6.j4m.nl (tls-sni-01): urn:acme:error:unknownHost :: The server could not resolve a domain name :: No valid IP addresses found for ipv6.j4m.nl

IMPORTANT NOTES:
- The following errors were reported by the server:
  
  Domain: ipv6.j4m.nl
  Type:   unknownHost
  Detail: No valid IP addresses found for ipv6.j4m.nl
  
  To fix these errors, please make sure that your domain name was
  entered correctly and the DNS A record(s) for that domain
  contain(s) the right IP address.

Is there any know workaround for this issue? Not being able to get certificates is somewhat of a blocker.
@alexanderkjall, if none of our other [plugins](https://certbot.eff.org/docs/using.html#plugins) work for your use case, then the patch proposed in #3369 should work for you.
I tried certbot-git today but I had no luck so far with this. I'm in the unlucky situation where my German cable provider assigns only a IPv6 address that is routable. Renewing my previous working certificate (at that time IPv4 worked) is now not possible anymore with IPv6 only :-( 

Error Message:
"Failed authorization procedure. www.domainname.de (tls-sni-01): urn:acme:error:connection :: The server could not connect to the client to verify the domain :: Failed to connect to [IPv6Address]:443 for TLS-SNI-01 challenge"
Is anyone working on this? There were some outside contributors who were interested, but if they're uninterested/unavailable one of the core developers can take over.
I tried following [this post](https://stefanchrist.eu/blog/2015_01_21/Using%20ipv6only%20in%20Nginx.xhtml) to make nginx open one connection for both ipv4 and v6, and then I couldn't access any of my websites through https. Could this be linked?
Assuming no outside contributor picks this up, this issue probably won't make our 0.11.0 release, but I'm planning on working on this myself for 0.12.0. As of right now, those two releases are scheduled for February 1st and March 1st respectively.
There have been two PRs attempting this: #3369 and #3687.

#3369 was a very small fix, which set, in the server classes:

`address_family = socket.AF_INET6`

It had two problems: It was failing unittests, and it wouldn't work on (a) OS's that don't support IPv6 (note: different from instances that lack an IPv6 interface), and (b) Python versions compiled without IPv6.

I am still hopeful that such an approach could work. For instance, could we wrap the assignment to address_family in a `try/except` that just `pass`es if there's an exception? The main blocker is that I don't know how to set up an OS without IPv6 support. I think doing that would be an important first step if someone wants to pick up this work.
Currently the best approach we're aware of is attempting to get both an IPv4 and an IPv6 socket, configuring the IPv6 socket to only get IPv6 traffic. There is some discussion of this in #3687.
Aha, I had missed this specific comment: https://github.com/certbot/certbot/pull/3687#issuecomment-261113255,which seems to indicate an additional reason #3369's small fix wouldn't work: It would break on FreeBSD and OpenBSD based on [this stackexchange answer](https://security.stackexchange.com/questions/92081/is-receiving-ipv4-connections-on-af-inet6-sockets-insecure).
FYI, we recently had an issue with Let's Encrypt where IPv6 validation was failing some of the time. Since we use Certbot for an end-to-end testing / monitoring tool, we might have caught this earlier if Certbot was able to handle IPv6 in standalone. So consider this a friendly request to bump priority a bit. :-)
Maybe a intermittent solution could be to have a parameter that tells the program to bind to IPv6, for example -6?
I think in general we should require a pretty high bar for adding new flags. In most cases, just binding to both is enough, and we have a plan for how to do that. So I think that's a better approach than adding a new flag.

BTW, I talked to @ohemorange offline and she's committed to implementing this soon.
Changing this to standalone because there's a separate PR out there for manual

Resolving patch:
diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py
--- a/acme/acme/crypto_util.py
+++ b/acme/acme/crypto_util.py
@@ -107,7 +107,7 @@ def accept(self):  # pylint: disable=missing-docstring
 
 
 def probe_sni(name, host, port=443, timeout=300,
-              method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('0', 0)):
+              method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('', 0)):
     """Probe SNI server for SSL certificate.
 
     :param bytes name: Byte string to send as the server name in the
@@ -132,9 +132,14 @@ def probe_sni(name, host, port=443, timeout=300,
     socket_kwargs = {} if sys.version_info < (2, 7) else {
         'source_address': source_address}
 
+    host_protocol_agnostic = None if host == '::' or host == '0' else host
+
     try:
         # pylint: disable=star-args
-        sock = socket.create_connection((host, port), **socket_kwargs)
+        logger.debug("Attempting to connect to %s:%d%s.", host_protocol_agnostic, port,
+            " from {0}:{1}".format(source_address[0], source_address[1]) if \
+            socket_kwargs else "")
+        sock = socket.create_connection((host_protocol_agnostic, port), **socket_kwargs)
     except socket.error as error:
         raise errors.Error(error)
 
diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py
--- a/acme/acme/standalone.py
+++ b/acme/acme/standalone.py
@@ -4,7 +4,9 @@
 import functools
 import logging
 import os
+import socket
 import sys
+import threading
 
 from six.moves import BaseHTTPServer  # type: ignore  # pylint: disable=import-error
 from six.moves import http_client  # pylint: disable=import-error
@@ -26,6 +28,11 @@ class TLSServer(socketserver.TCPServer):
     """Generic TLS Server."""
 
     def __init__(self, *args, **kwargs):
+        self.ipv6 = kwargs.pop("ipv6", False)
+        if self.ipv6:
+            self.address_family = socket.AF_INET6
+        else:
+            self.address_family = socket.AF_INET
         self.certs = kwargs.pop("certs", {})
         self.method = kwargs.pop(
             # pylint: disable=protected-access
@@ -49,12 +56,81 @@ class ACMEServerMixin:  # pylint: disable=old-style-class
     allow_reuse_address = True
 
 
+class BaseDualNetworkedServers(object):
+    """Base class for a pair of IPv6 and IPv4 servers that tries to do everything
+       it's asked for both servers, but where failures in one server don't
+       affect the other.
+
+       If two servers are instantiated, they will serve on the same port.
+       """
+
+    def __init__(self, ServerClass, server_address, *remaining_args, **kwargs):
+        port = server_address[1]
+        self.threads = []
+        self.servers = []
+
+        # Must try True first.
+        # Ubuntu, for example, will fail to bind to IPv4 if we've already bound
+        # to IPv6. But that's ok, since it will accept IPv4 connections on the IPv6
+        # socket. On the other hand, FreeBSD will successfully bind to IPv4 on the
+        # same port, which means that server will accept the IPv4 connections.
+        # If Python is compiled without IPv6, we'll error out but (probably) successfully
+        # create the IPv4 server.
+        for ip_version in [True, False]:
+            try:
+                kwargs["ipv6"] = ip_version
+                new_address = (server_address[0],) + (port,) + server_address[2:]
+                new_args = (new_address,) + remaining_args
+                server = ServerClass(*new_args, **kwargs) # pylint: disable=star-args
+            except socket.error:
+                logger.debug("Failed to bind to %s:%s using %s", new_address[0],
+                    new_address[1], "IPv6" if ip_version else "IPv4")
+            else:
+                self.servers.append(server)
+                # If two servers are set up and port 0 was passed in, ensure we always
+                # bind to the same port for both servers.
+                port = server.socket.getsockname()[1]
+        if len(self.servers) == 0:
+            raise socket.error("Could not bind to IPv4 or IPv6.")
+
+    def serve_forever(self):
+        """Wraps socketserver.TCPServer.serve_forever"""
+        for server in self.servers:
+            thread = threading.Thread(
+                # pylint: disable=no-member
+                target=server.serve_forever)
+            thread.start()
+            self.threads.append(thread)
+
+    def getsocknames(self):
+        """Wraps socketserver.TCPServer.socket.getsockname"""
+        return [server.socket.getsockname() for server in self.servers]
+
+    def shutdown_and_server_close(self):
+        """Wraps socketserver.TCPServer.shutdown, socketserver.TCPServer.server_close, and
+           threading.Thread.join"""
+        for server in self.servers:
+            server.shutdown()
+            server.server_close()
+        for thread in self.threads:
+            thread.join()
+        self.threads = []
+
+
 class TLSSNI01Server(TLSServer, ACMEServerMixin):
     """TLSSNI01 Server."""
 
-    def __init__(self, server_address, certs):
+    def __init__(self, server_address, certs, ipv6=False):
         TLSServer.__init__(
-            self, server_address, BaseRequestHandlerWithLogging, certs=certs)
+            self, server_address, BaseRequestHandlerWithLogging, certs=certs, ipv6=ipv6)
+
+
+class TLSSNI01DualNetworkedServers(BaseDualNetworkedServers):
+    """TLSSNI01Server Wrapper. Tries everything for both. Failures for one don't
+       affect the other."""
+
+    def __init__(self, *args, **kwargs):
+        BaseDualNetworkedServers.__init__(self, TLSSNI01Server, *args, **kwargs)
 
 
 class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler):
@@ -70,13 +146,33 @@ def handle(self):
         socketserver.BaseRequestHandler.handle(self)
 
 
-class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin):
+class HTTPServer(BaseHTTPServer.HTTPServer):
+    """Generic HTTP Server."""
+
+    def __init__(self, *args, **kwargs):
+        self.ipv6 = kwargs.pop("ipv6", False)
+        if self.ipv6:
+            self.address_family = socket.AF_INET6
+        else:
+            self.address_family = socket.AF_INET
+        BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
+
+
+class HTTP01Server(HTTPServer, ACMEServerMixin):
     """HTTP01 Server."""
 
-    def __init__(self, server_address, resources):
-        BaseHTTPServer.HTTPServer.__init__(
+    def __init__(self, server_address, resources, ipv6=False):
+        HTTPServer.__init__(
             self, server_address, HTTP01RequestHandler.partial_init(
-                simple_http_resources=resources))
+                simple_http_resources=resources), ipv6=ipv6)
+
+
+class HTTP01DualNetworkedServers(BaseDualNetworkedServers):
+    """HTTP01Server Wrapper. Tries everything for both. Failures for one don't
+       affect the other."""
+
+    def __init__(self, *args, **kwargs):
+        BaseDualNetworkedServers.__init__(self, HTTP01Server, *args, **kwargs)
 
 
 class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py
--- a/certbot/plugins/standalone.py
+++ b/certbot/plugins/standalone.py
@@ -3,7 +3,6 @@
 import collections
 import logging
 import socket
-import threading
 
 import OpenSSL
 import six
@@ -33,8 +32,6 @@ class ServerManager(object):
     will serve the same URLs!
 
     """
-    _Instance = collections.namedtuple("_Instance", "server thread")
-
     def __init__(self, certs, http_01_resources):
         self._instances = {}
         self.certs = certs
@@ -51,34 +48,32 @@ def run(self, port, challenge_type, listenaddr=""):
             either `acme.challenge.HTTP01` or `acme.challenges.TLSSNI01`.
         :param str listenaddr: (optional) The address to listen on. Defaults to all addrs.
 
-        :returns: Server instance.
+        :returns: DualNetworkedServers instance.
         :rtype: ACMEServerMixin
 
         """
         assert challenge_type in (challenges.TLSSNI01, challenges.HTTP01)
         if port in self._instances:
-            return self._instances[port].server
+            return self._instances[port]
 
         address = (listenaddr, port)
         try:
             if challenge_type is challenges.TLSSNI01:
-                server = acme_standalone.TLSSNI01Server(address, self.certs)
+                servers = acme_standalone.TLSSNI01DualNetworkedServers(address, self.certs)
             else:  # challenges.HTTP01
-                server = acme_standalone.HTTP01Server(
+                servers = acme_standalone.HTTP01DualNetworkedServers(
                     address, self.http_01_resources)
         except socket.error as error:
             raise errors.StandaloneBindError(error, port)
 
-        thread = threading.Thread(
-            # pylint: disable=no-member
-            target=server.serve_forever)
-        thread.start()
+        servers.serve_forever()
 
         # if port == 0, then random free port on OS is taken
         # pylint: disable=no-member
-        real_port = server.socket.getsockname()[1]
-        self._instances[real_port] = self._Instance(server, thread)
-        return server
+        # both servers, if they exist, have the same port
+        real_port = servers.getsocknames()[0][1]
+        self._instances[real_port] = servers
+        return servers
 
     def stop(self, port):
         """Stop ACME server running on the specified ``port``.
@@ -87,13 +82,12 @@ def stop(self, port):
 
         """
         instance = self._instances[port]
-        logger.debug("Stopping server at %s:%d...",
-                     *instance.server.socket.getsockname()[:2])
-        instance.server.shutdown()
+        for sockname in instance.getsocknames():
+            logger.debug("Stopping server at %s:%d...",
+                         *sockname[:2])
         # Not calling server_close causes problems when renewing multiple
         # certs with `certbot renew` using TLSSNI01 and PyOpenSSL 0.13
-        instance.server.server_close()
-        instance.thread.join()
+        instance.shutdown_and_server_close()
         del self._instances[port]
 
     def running(self):
@@ -102,12 +96,11 @@ def running(self):
         Once the server is stopped using `stop`, it will not be
         returned.
 
-        :returns: Mapping from ``port`` to ``server``.
+        :returns: Mapping from ``port`` to ``servers``.
         :rtype: tuple
 
         """
-        return dict((port, instance.server) for port, instance
-                    in six.iteritems(self._instances))
+        return self._instances.copy()
 
 
 SUPPORTED_CHALLENGES = [challenges.TLSSNI01, challenges.HTTP01]
@@ -236,38 +229,38 @@ def _try_perform_single(self, achall):
 
     def _perform_single(self, achall):
         if isinstance(achall.chall, challenges.HTTP01):
-            server, response = self._perform_http_01(achall)
+            servers, response = self._perform_http_01(achall)
         else:  # tls-sni-01
-            server, response = self._perform_tls_sni_01(achall)
-        self.served[server].add(achall)
+            servers, response = self._perform_tls_sni_01(achall)
+        self.served[servers].add(achall)
         return response
 
     def _perform_http_01(self, achall):
         port = self.config.http01_port
         addr = self.config.http01_address
-        server = self.servers.run(port, challenges.HTTP01, listenaddr=addr)
+        servers = self.servers.run(port, challenges.HTTP01, listenaddr=addr)
         response, validation = achall.response_and_validation()
         resource = acme_standalone.HTTP01RequestHandler.HTTP01Resource(
             chall=achall.chall, response=response, validation=validation)
         self.http_01_resources.add(resource)
-        return server, response
+        return servers, response
 
     def _perform_tls_sni_01(self, achall):
         port = self.config.tls_sni_01_port
         addr = self.config.tls_sni_01_address
-        server = self.servers.run(port, challenges.TLSSNI01, listenaddr=addr)
+        servers = self.servers.run(port, challenges.TLSSNI01, listenaddr=addr)
         response, (cert, _) = achall.response_and_validation(cert_key=self.key)
         self.certs[response.z_domain] = (self.key, cert)
-        return server, response
+        return servers, response
 
     def cleanup(self, achalls):  # pylint: disable=missing-docstring
-        # reduce self.served and close servers if none challenges are served
-        for server, server_achalls in self.served.items():
+        # reduce self.served and close servers if no challenges are served
+        for unused_servers, server_achalls in self.served.items():
             for achall in achalls:
                 if achall in server_achalls:
                     server_achalls.remove(achall)
-        for port, server in six.iteritems(self.servers.running()):
-            if not self.served[server]:
+        for port, servers in six.iteritems(self.servers.running()):
+            if not self.served[servers]:
                 self.servers.stop(port)
 
 


Test patch:
diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py
--- a/acme/acme/standalone_test.py
+++ b/acme/acme/standalone_test.py
@@ -1,6 +1,7 @@
 """Tests for acme.standalone."""
 import os
 import shutil
+import socket
 import threading
 import tempfile
 import time
@@ -9,6 +10,7 @@
 from six.moves import http_client  # pylint: disable=import-error
 from six.moves import socketserver  # type: ignore  # pylint: disable=import-error
 
+import mock
 import requests
 
 from acme import challenges
@@ -29,6 +31,13 @@ def test_bind(self):  # pylint: disable=no-self-use
             ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True)
         server.server_close()  # pylint: disable=no-member
 
+    def test_ipv6(self):
+        if socket.has_ipv6:
+            from acme.standalone import TLSServer
+            server = TLSServer(
+                ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True, ipv6=True)
+            server.server_close()  # pylint: disable=no-member
+
 
 class TLSSNI01ServerTest(unittest.TestCase):
     """Test for acme.standalone.TLSSNI01Server."""
@@ -112,6 +121,136 @@ def test_http01_not_found(self):
         self.assertFalse(self._test_http01(add=False))
 
 
+class BaseDualNetworkedServersTest(unittest.TestCase):
+    """Test for acme.standalone.BaseDualNetworkedServers."""
+
+    _multiprocess_can_split_ = True
+
+    class SingleProtocolServer(socketserver.TCPServer):
+        """Server that only serves on a single protocol. FreeBSD has this behavior for AF_INET6."""
+        def __init__(self, *args, **kwargs):
+            ipv6 = kwargs.pop("ipv6", False)
+            if ipv6:
+                self.address_family = socket.AF_INET6
+                kwargs["bind_and_activate"] = False
+            else:
+                self.address_family = socket.AF_INET
+            socketserver.TCPServer.__init__(self, *args, **kwargs)
+            if ipv6:
+                # pylint: disable=no-member
+                self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
+                try:
+                    self.server_bind()
+                    self.server_activate()
+                except:
+                    self.server_close()
+                    raise
+
+    @mock.patch("socket.socket.bind")
+    def test_fail_to_bind(self, mock_bind):
+        mock_bind.side_effect = socket.error
+        from acme.standalone import BaseDualNetworkedServers
+        self.assertRaises(socket.error, BaseDualNetworkedServers,
+            BaseDualNetworkedServersTest.SingleProtocolServer,
+            ("", 0),
+            socketserver.BaseRequestHandler)
+
+    def test_ports_equal(self):
+        from acme.standalone import BaseDualNetworkedServers
+        servers = BaseDualNetworkedServers(
+            BaseDualNetworkedServersTest.SingleProtocolServer,
+            ("", 0),
+            socketserver.BaseRequestHandler)
+        socknames = servers.getsocknames()
+        prev_port = None
+        # assert ports are equal
+        for sockname in socknames:
+            port = sockname[1]
+            if prev_port:
+                self.assertEqual(prev_port, port)
+            prev_port = port
+
+
+class TLSSNI01DualNetworkedServersTest(unittest.TestCase):
+    """Test for acme.standalone.TLSSNI01DualNetworkedServers."""
+
+    _multiprocess_can_split_ = True
+
+    def setUp(self):
+        self.certs = {b'localhost': (
+            test_util.load_pyopenssl_private_key('rsa2048_key.pem'),
+            test_util.load_cert('rsa2048_cert.pem'),
+        )}
+        from acme.standalone import TLSSNI01DualNetworkedServers
+        self.servers = TLSSNI01DualNetworkedServers(("", 0), certs=self.certs)
+        self.servers.serve_forever()
+
+    def tearDown(self):
+        self.servers.shutdown_and_server_close()
+
+    def test_connect(self):
+        socknames = self.servers.getsocknames()
+        # connect to all addresses
+        for sockname in socknames:
+            host, port = sockname[:2]
+            cert = crypto_util.probe_sni(
+                b'localhost', host=host, port=port, timeout=1)
+            self.assertEqual(jose.ComparableX509(cert),
+                             jose.ComparableX509(self.certs[b'localhost'][1]))
+
+
+class HTTP01DualNetworkedServersTest(unittest.TestCase):
+    """Tests for acme.standalone.HTTP01DualNetworkedServers."""
+
+    _multiprocess_can_split_ = True
+
+    def setUp(self):
+        self.account_key = jose.JWK.load(
+            test_util.load_vector('rsa1024_key.pem'))
+        self.resources = set()
+
+        from acme.standalone import HTTP01DualNetworkedServers
+        self.servers = HTTP01DualNetworkedServers(('', 0), resources=self.resources)
+
+        # pylint: disable=no-member
+        self.port = self.servers.getsocknames()[0][1]
+        self.servers.serve_forever()
+
+    def tearDown(self):
+        self.servers.shutdown_and_server_close()
+
+    def test_index(self):
+        response = requests.get(
+            'http://localhost:{0}'.format(self.port), verify=False)
+        self.assertEqual(
+            response.text, 'ACME client standalone challenge solver')
+        self.assertTrue(response.ok)
+
+    def test_404(self):
+        response = requests.get(
+            'http://localhost:{0}/foo'.format(self.port), verify=False)
+        self.assertEqual(response.status_code, http_client.NOT_FOUND)
+
+    def _test_http01(self, add):
+        chall = challenges.HTTP01(token=(b'x' * 16))
+        response, validation = chall.response_and_validation(self.account_key)
+
+        from acme.standalone import HTTP01RequestHandler
+        resource = HTTP01RequestHandler.HTTP01Resource(
+            chall=chall, response=response, validation=validation)
+        if add:
+            self.resources.add(resource)
+        return resource.response.simple_verify(
+            resource.chall, 'localhost', self.account_key.public_key(),
+            port=self.port)
+
+    def test_http01_found(self):
+        self.assertTrue(self._test_http01(add=True))
+
+    def test_http01_not_found(self):
+        self.assertFalse(self._test_http01(add=False))
+
+
 class TestSimpleTLSSNI01Server(unittest.TestCase):
     """Tests for acme.standalone.simple_tls_sni_01_server."""
 
diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py
--- a/certbot/plugins/standalone_test.py
+++ b/certbot/plugins/standalone_test.py
@@ -32,7 +32,7 @@ def test_init(self):
 
     def _test_run_stop(self, challenge_type):
         server = self.mgr.run(port=0, challenge_type=challenge_type)
-        port = server.socket.getsockname()[1]  # pylint: disable=no-member
+        port = server.getsocknames()[0][1]  # pylint: disable=no-member
         self.assertEqual(self.mgr.running(), {port: server})
         self.mgr.stop(port=port)
         self.assertEqual(self.mgr.running(), {})
@@ -45,7 +45,7 @@ def test_run_stop_http_01(self):
 
     def test_run_idempotent(self):
         server = self.mgr.run(port=0, challenge_type=challenges.HTTP01)
-        port = server.socket.getsockname()[1]  # pylint: disable=no-member
+        port = server.getsocknames()[0][1]  # pylint: disable=no-member
         server2 = self.mgr.run(port=port, challenge_type=challenges.HTTP01)
         self.assertEqual(self.mgr.running(), {port: server})
         self.assertTrue(server is server2)
@@ -53,9 +53,14 @@ def test_run_idempotent(self):
         self.assertEqual(self.mgr.running(), {})
 
     def test_run_bind_error(self):
-        some_server = socket.socket()
+        some_server = socket.socket(socket.AF_INET6)
         some_server.bind(("", 0))
         port = some_server.getsockname()[1]
+        maybe_another_server = socket.socket()
+        try:
+            maybe_another_server.bind(("", port))
+        except socket.error:
+            pass
         self.assertRaises(
             errors.StandaloneBindError, self.mgr.run, port,
             challenge_type=challenges.HTTP01)