Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #1719: resolving hostnames for bind #1723

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
9 changes: 6 additions & 3 deletions gunicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,10 @@ def worker_class(self):
@property
def address(self):
s = self.settings['bind'].get()
return [util.parse_address(_compat.bytes_to_str(bind)) for bind in s]
addresses = []
for bind in s:
addresses.extend(util.parse_address(_compat.bytes_to_str(bind)))
return addresses

@property
def uid(self):
Expand Down Expand Up @@ -534,9 +537,9 @@ class Bind(Setting):
validator = validate_list_string

if 'PORT' in os.environ:
default = ['0.0.0.0:{0}'.format(os.environ.get('PORT'))]
default = ['[::]:{0}'.format(os.environ.get('PORT'))]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least on my machine, this returns only IPv6 addresses from socket.getaddrinfo(). Since this is a list, maybe we want to give it two entries so Gunicorn binds to IPv4 and IPv6 by default?

Copy link
Author

@wanneut wanneut Mar 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you bind to :: you will listen to any address (IPv4 and IPv6) as long the IPV6_V6ONLY flag is not set.
In fact :: could be written as ::0.0.0.0 which is already the IPv4-compatible address for 0.0.0.0. (Older version of ::FFFF:0.0.0.0)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about the machine that doesn't support or has not been installed with ipv6 support?

Copy link
Author

@wanneut wanneut Apr 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The older version dosn't work on IPv6 installations at all.
This version, works everywhere (IPv4 like IPv6) exempt of some obscure Linux-configurations (I think there are not many other OSes where something can be done.), where someone explicitly removed IPv6 support. I think such users should know how to specify another bind address.
If someone shoots himself into his foot he should not wonder if it hurts.
Gunicorn requires Python 2 (which is newer than IPv6.) multithreading and several other things which are much more uncommon than IPv6.
At the end you have to decide what your defaults are.
I mean what about installations with disabled IPv4? It is deprecated for 20 years now. Time to turn it off? IPv6 is compatible to it. So it shouldn't be needed any more. The new code could handle that.

else:
default = ['127.0.0.1:8000']
default = ['localhost:8000']

desc = """\
The socket to bind.
Expand Down
50 changes: 22 additions & 28 deletions gunicorn/sock.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import time

from gunicorn import util
from gunicorn.six import string_types


class BaseSocket(object):
Expand All @@ -20,12 +19,12 @@ def __init__(self, address, conf, log, fd=None):
self.log = log
self.conf = conf

self.cfg_addr = address
self.cfg_addr = address[4]
if fd is None:
sock = socket.socket(self.FAMILY, socket.SOCK_STREAM)
sock = socket.socket(address[0], socket.SOCK_STREAM)
bound = False
else:
sock = socket.fromfd(fd, self.FAMILY, socket.SOCK_STREAM)
sock = socket.fromfd(fd, address[0], socket.SOCK_STREAM)
os.close(fd)
bound = True

Expand Down Expand Up @@ -74,16 +73,15 @@ def close(self):

class TCPSocket(BaseSocket):

FAMILY = socket.AF_INET

def __str__(self):
def scheme(self):
if self.conf.is_ssl:
scheme = "https"
return "https"
else:
scheme = "http"
return "http"

def __str__(self):
addr = self.sock.getsockname()
return "%s://%s:%d" % (scheme, addr[0], addr[1])
return "%s://%s:%d" % (self.scheme(), addr[0], addr[1])

def set_options(self, sock, bound=False):
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
Expand All @@ -92,27 +90,23 @@ def set_options(self, sock, bound=False):

class TCP6Socket(TCPSocket):

FAMILY = socket.AF_INET6

def __str__(self):
(host, port, _, _) = self.sock.getsockname()
return "http://[%s]:%d" % (host, port)
return "%s://[%s]:%d" % (self.scheme(), host, port)


class UnixSocket(BaseSocket):

FAMILY = socket.AF_UNIX

def __init__(self, addr, conf, log, fd=None):
if fd is None:
try:
st = os.stat(addr)
st = os.stat(addr[4])
except OSError as e:
if e.args[0] != errno.ENOENT:
raise
else:
if stat.S_ISSOCK(st.st_mode):
os.remove(addr)
os.remove(addr[4])
else:
raise ValueError("%r is not a socket" % addr)
super(UnixSocket, self).__init__(addr, conf, log, fd=fd)
Expand All @@ -127,16 +121,15 @@ def bind(self, sock):
os.umask(old_umask)


def _sock_type(addr):
if isinstance(addr, tuple):
if util.is_ipv6(addr[0]):
sock_type = TCP6Socket
else:
sock_type = TCPSocket
elif isinstance(addr, string_types):
def _sock_type(af_type):
if af_type == socket.AF_INET6:
sock_type = TCP6Socket
elif af_type == socket.AF_INET:
sock_type = TCPSocket
elif af_type == socket.AF_UNIX:
sock_type = UnixSocket
else:
raise TypeError("Unable to create socket from: %r" % addr)
raise TypeError("Unable to create socket family: %r" % af_type)
return sock_type


Expand Down Expand Up @@ -166,15 +159,15 @@ def create_sockets(conf, log, fds=None):
for fd in fds:
sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
sock_name = sock.getsockname()
sock_type = _sock_type(sock_name)
sock_type = _sock_type(sock.family)
listener = sock_type(sock_name, conf, log, fd=fd)
listeners.append(listener)

return listeners

# no sockets is bound, first initialization of gunicorn in this env.
for addr in laddr:
sock_type = _sock_type(addr)
sock_type = _sock_type(addr[0])
sock = None
for i in range(5):
try:
Expand Down Expand Up @@ -204,6 +197,7 @@ def create_sockets(conf, log, fds=None):
def close_sockets(listeners, unlink=True):
for sock in listeners:
sock_name = sock.getsockname()
socket_family = sock.family
sock.close()
if unlink and _sock_type(sock_name) is UnixSocket:
if unlink and socket_family == socket.AF_UNIX:
os.unlink(sock_name)
15 changes: 2 additions & 13 deletions gunicorn/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,20 +221,9 @@ def unlink(filename):
if error.errno not in (errno.ENOENT, errno.ENOTDIR):
raise


def is_ipv6(addr):
try:
socket.inet_pton(socket.AF_INET6, addr)
except socket.error: # not a valid address
return False
except ValueError: # ipv6 not supported on this platform
return False
return True


def parse_address(netloc, default_port=8000):
if re.match(r'unix:(//)?', netloc):
return re.split(r'unix:(//)?', netloc)[-1]
return [(socket.AF_UNIX, socket.SOCK_STREAM, 0, '', re.split(r'unix:(//)?', netloc)[-1])]

if netloc.startswith("tcp://"):
netloc = netloc.split("tcp://")[1]
Expand All @@ -258,7 +247,7 @@ def parse_address(netloc, default_port=8000):
port = int(port)
else:
port = default_port
return (host, port)
return socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)


def close_on_exec(fd):
Expand Down
2 changes: 1 addition & 1 deletion tests/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import platform
from wsgiref.validate import validator

HOST = "127.0.0.1"
HOST = '127.0.0.1'


@validator
Expand Down
9 changes: 7 additions & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
from gunicorn.workers.sync import SyncWorker
from gunicorn import glogging
from gunicorn.instrument import statsd
from socket import AF_INET
from socket import AF_INET6
from socket import SOCK_STREAM

dirname = os.path.dirname(__file__)
def cfg_module():
Expand Down Expand Up @@ -74,7 +77,9 @@ def test_property_access():
assert c.workers == 3

# Address is parsed
assert c.address == [("127.0.0.1", 8000)]
assert c.address == [(AF_INET, SOCK_STREAM, 6, '', ('127.0.0.1', 8000))] \
or c.address == [(AF_INET6, SOCK_STREAM, 6, '', ('::1', 8000, 0, 0)), \
(AF_INET, SOCK_STREAM, 6, '', ('127.0.0.1', 8000))]

# User and group defaults
assert os.geteuid() == c.uid
Expand All @@ -84,7 +89,7 @@ def test_property_access():
assert "gunicorn" == c.proc_name

# Not a config property
pytest.raises(AttributeError, getattr, c, "foo")
pytest.raises(AttributeError, getattr, c, 'foo')
# Force to be not an error
class Baz(object):
def get(self):
Expand Down
21 changes: 21 additions & 0 deletions tests/test_sock.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
except ImportError:
import mock

import pytest

from socket import SOCK_STREAM
from socket import AF_UNIX
from socket import AF_INET
from socket import AF_INET6

from gunicorn import sock


Expand All @@ -25,6 +32,7 @@ def test_socket_close():
def test_unix_socket_close_unlink(unlink):
listener = mock.Mock()
listener.getsockname.return_value = '/var/run/test.sock'
listener.family = SOCK_STREAM
sock.close_sockets([listener])
listener.close.assert_called_with()
unlink.assert_called_once_with('/var/run/test.sock')
Expand All @@ -34,6 +42,19 @@ def test_unix_socket_close_unlink(unlink):
def test_unix_socket_close_without_unlink(unlink):
listener = mock.Mock()
listener.getsockname.return_value = '/var/run/test.sock'
listener.family = SOCK_STREAM
sock.close_sockets([listener], False)
listener.close.assert_called_with()
assert not unlink.called, 'unlink should not have been called'

@pytest.mark.parametrize('test_input, expected', [
(AF_UNIX, sock.UnixSocket),
(AF_INET, sock.TCPSocket),
(AF_INET6, sock.TCP6Socket)])
def test__sock_type(test_input, expected):
assert sock._sock_type(test_input) is expected

def test__sock_type2():
with pytest.raises(TypeError) as err:
sock._sock_type(17)
assert 'Unable to create socket family:' in str(err)
31 changes: 14 additions & 17 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,23 @@
from gunicorn import util
from gunicorn.errors import AppImportError
from gunicorn.six.moves.urllib.parse import SplitResult # pylint: disable=no-name-in-module
from socket import AF_INET
from socket import AF_INET6
from socket import AF_UNIX
from socket import SOCK_STREAM


@pytest.mark.parametrize('test_input, expected', [
('unix://var/run/test.sock', 'var/run/test.sock'),
('unix:/var/run/test.sock', '/var/run/test.sock'),
('', ('0.0.0.0', 8000)),
('[::1]:8000', ('::1', 8000)),
('localhost:8000', ('localhost', 8000)),
('127.0.0.1:8000', ('127.0.0.1', 8000)),
('localhost', ('localhost', 8000))
('unix://var/run/test.sock', [(AF_UNIX, SOCK_STREAM, 0, '', 'var/run/test.sock')]),
('unix:/var/run/test.sock', [(AF_UNIX, SOCK_STREAM, 0, '', '/var/run/test.sock')]),
#('', [(AF_INET6, SOCK_STREAM, 6, '', ('::', 8000, 0, 0))]),
('[::1]:8007', [(AF_INET6, SOCK_STREAM, 6, '', ('::1', 8007, 0, 0))]),
#('localhost:8007', [(AF_INET6, SOCK_STREAM, 6, '', ('::1', 8007, 0, 0)),\
#(AF_INET, SOCK_STREAM, 6, '', ('127.0.0.1', 8007))]),
('127.0.0.1:8007', [(AF_INET, SOCK_STREAM, 6, '', ('127.0.0.1', 8007))]),
('tcp://127.0.0.1:8007', [(AF_INET, SOCK_STREAM, 6, '', ('127.0.0.1', 8007))]),
#('localhost', [(AF_INET6, SOCK_STREAM, 6, '', ('::1', 8000, 0, 0)),
#(AF_INET, SOCK_STREAM, 6, '', ('127.0.0.1', 8000))])
])
def test_parse_address(test_input, expected):
assert util.parse_address(test_input) == expected
Expand All @@ -33,16 +40,6 @@ def test_http_date():
assert util.http_date(1508607753.740316) == 'Sat, 21 Oct 2017 17:42:33 GMT'


@pytest.mark.parametrize('test_input, expected', [
('1200:0000:AB00:1234:0000:2552:7777:1313', True),
('1200::AB00:1234::2552:7777:1313', False),
('21DA:D3:0:2F3B:2AA:FF:FE28:9C5A', True),
('1200:0000:AB00:1234:O000:2552:7777:1313', False),
])
def test_is_ipv6(test_input, expected):
assert util.is_ipv6(test_input) == expected


def test_warn(capsys):
util.warn('test warn')
_, err = capsys.readouterr()
Expand Down