Repository: certbot/certbot

Issue:
Handle IPv6 adresses in Apache vhost entries
When running `./letsencrypt-auto --agree-dev-preview --server https://acme-v01.api.letsencrypt.org/directory auth` it crashes out with this:

```
Error while restarting Apache:
Restarting apache2 (via systemctl): apache2.service failed!

Job for apache2.service failed. See 'systemctl status apache2.service' and 'journalctl -xn' for details.
```

Upon further investigation via `journalctl -xn` I get this:

```
-- Logs begin at Tue 2015-09-08 20:40:09 CDT, end at Mon 2015-10-26 17:16:13 CDT. --
Oct 26 17:16:12 dag apache2[581]: AH00526: Syntax error on line 2 of /etc/apache2/le_dvsni_cert_challenge.conf:
Oct 26 17:16:12 dag apache2[581]: The address or port is invalid
Oct 26 17:16:12 dag apache2[581]: Action 'configtest' failed.
Oct 26 17:16:12 dag apache2[581]: The Apache error log may have more information.
Oct 26 17:16:12 dag systemd[1]: apache2.service: control process exited, code=exited status=1
Oct 26 17:16:12 dag systemd[1]: Failed to start LSB: Apache2 web server.
```

Unfortunately the file in question /etc/apache2/le_dvsni_cert_challenge.conf is not available after letsencrypt-auto crashes out.

Running on Debian jessie x86_64



Discussion:
Yes, the configuration changes and challenge files are cleaned up/reverted in the result of a failure.  (Note for future work: We should have an available debug flag to avoid this and make debugging easier)

Looking at the template of the dvsni challenge file found here... https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt-apache/letsencrypt_apache/dvsni.py#L32

It looks like the syntax error probably occurred when trying to include the vhost address.  Given the `address or port is invalid` error.

Could you provide the VirtualHost block of the domain you selected to upgrade?

The challenge configuration file attempts to copy over the addresses of the selected VirtualHost while changing the port to 443.

This is the vhost I was trying to add let's encrypt to:

```
<VirtualHost 173.192.30.7:80 [2607:f0d0:1005:99::3:1337]:80>
DocumentRoot /xxxx/
ServerName noodles.net.nz
ServerAlias www.noodles.net.nz
CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined
<Directory "/xxxx/">
  AllowOverride All
</Directory>
</VirtualHost>

<VirtualHost 173.192.30.7:443 [2607:f0d0:1005:99::3:1337]:443>
DocumentRoot /xxxx/
ServerName noodles.net.nz
ServerAlias www.noodles.net.nz
CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined
<Directory "xxxx">
  AllowOverride All
</Directory>

  SSLEngine on

  SSLHonorCipherOrder On
  SSLProtocol all -SSLv2 -SSLv3
  SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS"

   SSLCertificateFile /xxxx/noodles.net.nz.crt
   SSLCertificateKeyFile /xxxx/noodles.net.nz.key

   Header set Strict-Transport-Security "max-age=31536000; preload"
</VirtualHost>
```

I guess the apache letsencrypt-auto only works with simple configs?

No, it is just that the Apache plugin is very much in alpha/beta right now.  It shouldn't be difficult to handle this.  I am guessing that IPv6 addresses aren't parsed appropriately.

I did my development and testing on EC2, (and Boulder isn't currently connecting to IPv6) and didn't get a chance to test IPv6 configurations.

Thanks for the help!  We will make sure that the plugin can handle this soon and also add in appropriate test cases to make sure we don't have regressions.

In the meantime, you can run the Standalone plugin, Web-root plugin, or Manual plugin to attain a certificate.

I can confirm that after removing the IPv6 address from the VirtualHost line that it works.

Just to make sure it does not fall off the table once IPv6 support gets added:

The reverse order of IPv4 and IPv6 address in the vhost line should be supported as well, e.g.

`<VirtualHost [2a01:238:429e:6b00:6188:46:8080:e1b4]:80 85.214.41.172:80>`

The clean up isn't quite perfect either - the le_dvsni_cert_challenge.conf file certainly seems to be removed but 000-default-le-ssl.conf is left laying around.  Please make sure ipv6 works on the public launch, we don't need people to have another reason to drop v6 support. :-)

The cleanup problem is almost certainly due to #808 which there is a work in progress PR for (#1582).

@andydavidson IPv6 support won't be in the Public Beta release of Let's Encrypt, though there's a chance it'll be on the list for 1.0.

What do you mean with no IPv6 support?  It's really annoying that
each time you run it you have to go and edit the config files
first to remove the IPv6 addresses in it.  All that we want is
that it doesn't break parsing a file with IPv6 addresses in it.

I totally agree with @kroeckx.
Full IPv6 support would mean to me that LE supports also IPv6 only hosts. I'm pretty sure almost everyone agrees that that is something which can wait after the 1.0.
But there are a lot of dual hosted machines out there. And as long as LE ignores the v6 part of the vhost and runs its verification successfully over v4 only I would assume the owners of these hosts would be happy. That is something which should be in scope for 1.0.

@kroeckx ok that makes sense.  We'll put this on the list of things needed for 1.0 Apache support.

Any update on this?


Resolving patch:
diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py
--- a/letsencrypt/plugins/common.py
+++ b/letsencrypt/plugins/common.py
@@ -110,8 +110,17 @@ def __init__(self, tup):
     @classmethod
     def fromstring(cls, str_addr):
         """Initialize Addr from string."""
-        tup = str_addr.partition(':')
-        return cls((tup[0], tup[2]))
+        if str_addr.startswith('['):
+            # ipv6 addresses starts with [
+            endIndex = str_addr.rfind(']')
+            host = str_addr[:endIndex + 1]
+            port = ''
+            if len(str_addr) > endIndex + 2 and str_addr[endIndex + 1] == ':':
+                port = str_addr[endIndex + 2:]
+            return cls((host, port))
+        else:
+            tup = str_addr.partition(':')
+            return cls((tup[0], tup[2]))
 
     def __str__(self):
         if self.tup[1]:


Test patch:
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf
similarity index 100%
rename from letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143.conf
rename to letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143b.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf
similarity index 100%
rename from letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143b.conf
rename to letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf
new file mode 100644
--- /dev/null
+++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf
@@ -0,0 +1,9 @@
+<VirtualHost [2607:f0d0:1005:99::3:1337]:80 173.192.30.7:80>
+DocumentRoot /xxxx/
+ServerName noodles.net.nz
+ServerAlias www.noodles.net.nz
+CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined
+<Directory "/xxxx/">
+  AllowOverride All
+</Directory>
+</VirtualHost>
diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf
new file mode 100644
--- /dev/null
+++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf
@@ -0,0 +1,9 @@
+<VirtualHost [2607:f0d0:1005:99::3:1337] 173.192.30.7:80>
+DocumentRoot /xxxx/
+ServerName noodles.net.nz
+ServerAlias www.noodles.net.nz
+CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined
+<Directory "/xxxx/">
+  AllowOverride All
+</Directory>
+</VirtualHost>
diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py
--- a/letsencrypt/plugins/common_test.py
+++ b/letsencrypt/plugins/common_test.py
@@ -81,6 +81,9 @@ def setUp(self):
         self.addr1 = Addr.fromstring("192.168.1.1")
         self.addr2 = Addr.fromstring("192.168.1.1:*")
         self.addr3 = Addr.fromstring("192.168.1.1:80")
+        self.addr4 = Addr.fromstring("[fe00::1]")
+        self.addr5 = Addr.fromstring("[fe00::1]:*")
+        self.addr6 = Addr.fromstring("[fe00::1]:80")
 
     def test_fromstring(self):
         self.assertEqual(self.addr1.get_addr(), "192.168.1.1")
@@ -89,22 +92,38 @@ def test_fromstring(self):
         self.assertEqual(self.addr2.get_port(), "*")
         self.assertEqual(self.addr3.get_addr(), "192.168.1.1")
         self.assertEqual(self.addr3.get_port(), "80")
+        self.assertEqual(self.addr4.get_addr(), "[fe00::1]")
+        self.assertEqual(self.addr4.get_port(), "")
+        self.assertEqual(self.addr5.get_addr(), "[fe00::1]")
+        self.assertEqual(self.addr5.get_port(), "*")
+        self.assertEqual(self.addr6.get_addr(), "[fe00::1]")
+        self.assertEqual(self.addr6.get_port(), "80")
 
     def test_str(self):
         self.assertEqual(str(self.addr1), "192.168.1.1")
         self.assertEqual(str(self.addr2), "192.168.1.1:*")
         self.assertEqual(str(self.addr3), "192.168.1.1:80")
+        self.assertEqual(str(self.addr4), "[fe00::1]")
+        self.assertEqual(str(self.addr5), "[fe00::1]:*")
+        self.assertEqual(str(self.addr6), "[fe00::1]:80")
 
     def test_get_addr_obj(self):
         self.assertEqual(str(self.addr1.get_addr_obj("443")), "192.168.1.1:443")
         self.assertEqual(str(self.addr2.get_addr_obj("")), "192.168.1.1")
         self.assertEqual(str(self.addr1.get_addr_obj("*")), "192.168.1.1:*")
+        self.assertEqual(str(self.addr4.get_addr_obj("443")), "[fe00::1]:443")
+        self.assertEqual(str(self.addr5.get_addr_obj("")), "[fe00::1]")
+        self.assertEqual(str(self.addr4.get_addr_obj("*")), "[fe00::1]:*")
 
     def test_eq(self):
         self.assertEqual(self.addr1, self.addr2.get_addr_obj(""))
         self.assertNotEqual(self.addr1, self.addr2)
         self.assertFalse(self.addr1 == 3333)
 
+        self.assertEqual(self.addr4, self.addr4.get_addr_obj(""))
+        self.assertNotEqual(self.addr4, self.addr5)
+        self.assertFalse(self.addr4 == 3333)
+
     def test_set_inclusion(self):
         from letsencrypt.plugins.common import Addr
         set_a = set([self.addr1, self.addr2])
@@ -114,6 +133,13 @@ def test_set_inclusion(self):
 
         self.assertEqual(set_a, set_b)
 
+        set_c = set([self.addr4, self.addr5])
+        addr4b = Addr.fromstring("[fe00::1]")
+        addr5b = Addr.fromstring("[fe00::1]:*")
+        set_d = set([addr4b, addr5b])
+
+        self.assertEqual(set_c, set_d)
+
 
 class TLSSNI01Test(unittest.TestCase):
     """Tests for letsencrypt.plugins.common.TLSSNI01."""