Nginx IPv6 support

This commit is contained in:
Joona Hoikkala 2017-10-08 23:54:59 +03:00
parent cacc40817b
commit 994a6ce114
4 changed files with 84 additions and 17 deletions

View file

@ -247,6 +247,24 @@ class NginxConfigurator(common.Installer):
return vhost
def ipv6_info(self):
"""Returns tuple of booleans (ipv6_active, ipv6only_present)
ipv6_active is true if any server block has an active ipv6 address.
ipv6only_present is true if ipv6only=on option exists in configuration.
:rtype: tuple of type (bool, bool)
"""
vhosts = self.parser.get_vhosts()
ipv6_active = False
ipv6only_present = False
for vh in vhosts:
for addr in vh.addrs:
if addr.ipv6:
ipv6_active = True
if addr.ipv6only:
ipv6only_present = True
return (ipv6_active, ipv6only_present)
def _get_ranked_matches(self, target_name):
"""Returns a ranked list of vhosts that match target_name.
The ranking gives preference to SSL vhosts.
@ -405,9 +423,12 @@ class NginxConfigurator(common.Installer):
all_names.add(host)
elif not common.private_ips_regex.match(host):
# If it isn't a private IP, do a reverse DNS lookup
# TODO: IPv6 support
try:
socket.inet_aton(host)
if addr.ipv6:
host = addr.get_ipv6_exploded()
socket.inet_pton(socket.AF_INET6, host)
else:
socket.inet_pton(socket.AF_INET, host)
all_names.add(socket.gethostbyaddr(host)[0])
except (socket.error, socket.herror, socket.timeout):
continue
@ -443,15 +464,29 @@ class NginxConfigurator(common.Installer):
:type vhost: :class:`~certbot_nginx.obj.VirtualHost`
"""
ipv6info = self.ipv6_info()
# If the vhost was implicitly listening on the default Nginx port,
# have it continue to do so.
if len(vhost.addrs) == 0:
listen_block = [['\n ', 'listen', ' ', self.DEFAULT_LISTEN_PORT]]
self.parser.add_server_directives(vhost, listen_block, replace=False)
ipv6_block = ['']
if vhost.ipv6_enabled():
ipv6_block = ['\n ',
'listen',
' ',
'[::]:{} ssl'.format(self.config.tls_sni_01_port)]
if not ipv6info[1]:
# ipv6only=on is absent in global config
ipv6_block.append(' ')
ipv6_block.append('ipv6only=on')
snakeoil_cert, snakeoil_key = self._get_snakeoil_paths()
ssl_block = ([
ipv6_block,
['\n ', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)],
['\n ', 'ssl_certificate', ' ', snakeoil_cert],
['\n ', 'ssl_certificate_key', ' ', snakeoil_key],

View file

@ -34,10 +34,12 @@ class Addr(common.Addr):
UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0')
CANONICAL_UNSPECIFIED_ADDRESS = UNSPECIFIED_IPV4_ADDRESSES[0]
def __init__(self, host, port, ssl, default):
def __init__(self, host, port, ssl, default, ipv6, ipv6only):
super(Addr, self).__init__((host, port))
self.ssl = ssl
self.default = default
self.ipv6 = ipv6
self.ipv6only = ipv6only
self.unspecified_address = host in self.UNSPECIFIED_IPV4_ADDRESSES
@classmethod
@ -46,6 +48,8 @@ class Addr(common.Addr):
parts = str_addr.split(' ')
ssl = False
default = False
ipv6 = False
ipv6only = False
host = ''
port = ''
@ -56,15 +60,25 @@ class Addr(common.Addr):
if addr.startswith('unix:'):
return None
tup = addr.partition(':')
if re.match(r'^\d+$', tup[0]):
# This is a bare port, not a hostname. E.g. listen 80
host = ''
port = tup[0]
# IPv6 check
ipv6_match = re.match(r'\[.*\]', addr)
if ipv6_match:
ipv6 = True
# IPv6 handling
host = ipv6_match.group()
# The rest of the addr string will be the port, if any
port = addr[ipv6_match.end()+1:]
else:
# This is a host-port tuple. E.g. listen 127.0.0.1:*
host = tup[0]
port = tup[2]
# IPv4 handling
tup = addr.partition(':')
if re.match(r'^\d+$', tup[0]):
# This is a bare port, not a hostname. E.g. listen 80
host = ''
port = tup[0]
else:
# This is a host-port tuple. E.g. listen 127.0.0.1:*
host = tup[0]
port = tup[2]
# The rest of the parts are options; we only care about ssl and default
while len(parts) > 0:
@ -73,8 +87,10 @@ class Addr(common.Addr):
ssl = True
elif nextpart == 'default_server':
default = True
elif nextpart == "ipv6only=on":
ipv6only = True
return cls(host, port, ssl, default)
return cls(host, port, ssl, default, ipv6, ipv6only)
def to_string(self, include_default=True):
"""Return string representation of Addr"""
@ -114,8 +130,6 @@ class Addr(common.Addr):
self.tup[1]), self.ipv6) == \
common.Addr((other.CANONICAL_UNSPECIFIED_ADDRESS,
other.tup[1]), other.ipv6)
# Nginx plugin currently doesn't support IPv6 but this will
# future-proof it
return super(Addr, self).__eq__(other)
def __eq__(self, other):
@ -195,6 +209,13 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
return True
return False
def ipv6_enabled(self):
"""Return true if one or more of the listen directives in vhost supports
IPv6"""
for a in self.addrs:
if a.ipv6:
return True
def _find_directive(directives, directive_name):
"""Find a directive of type directive_name in directives
"""

View file

@ -51,6 +51,8 @@ class NginxTlsSni01(common.TLSSNI01):
default_addr = "{0} ssl".format(
self.configurator.config.tls_sni_01_port)
ipv6info = self.configurator.ipv6_info()
for achall in self.achalls:
vhost = self.configurator.choose_vhost(achall.domain)
if vhost is None:
@ -63,7 +65,17 @@ class NginxTlsSni01(common.TLSSNI01):
if vhost.addrs:
addresses.append(list(vhost.addrs))
else:
addresses.append([obj.Addr.fromstring(default_addr)])
if ipv6info[0]:
# If IPv6 is active in Nginx configuration
ipv6_addr = "[::]:{} ssl".format(
self.configurator.config.tls_sni_01_port)
if not ipv6info[1]:
# If ipv6only=on is not already present in the config
ipv6_addr = ipv6_addr + " ipv6only=on"
addresses.append([obj.Addr.fromstring(default_addr),
obj.Addr.fromstring(ipv6_addr)])
else:
addresses.append([obj.Addr.fromstring(default_addr)])
# Create challenge certs
responses = [self._setup_challenge_cert(x) for x in self.achalls]
@ -117,7 +129,6 @@ class NginxTlsSni01(common.TLSSNI01):
raise errors.MisconfigurationError(
'LetsEncrypt could not find an HTTP block to include '
'TLS-SNI-01 challenges in %s.' % root)
config = [self._make_server_block(pair[0], pair[1])
for pair in six.moves.zip(self.achalls, ll_addrs)]
config = nginxparser.UnspacedList(config)

View file

@ -251,7 +251,7 @@ class Addr(object):
"""Normalized representation of addr/port tuple
"""
if self.ipv6:
return (self._normalize_ipv6(self.tup[0]), self.tup[1])
return (self.get_ipv6_exploded(), self.tup[1])
return self.tup
def __eq__(self, other):