mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 14:03:02 +00:00
Merge pull request #4988 from manics/ipv6
More IPv6: Use bare IPv6 for configuration, use `[ipv6]` when displaying IPv6 outputs
This commit is contained in:
@@ -1922,7 +1922,11 @@ class JupyterHub(Application):
|
|||||||
self.internal_ssl_components_trust
|
self.internal_ssl_components_trust
|
||||||
)
|
)
|
||||||
|
|
||||||
default_alt_names = ["IP:127.0.0.1", "DNS:localhost"]
|
default_alt_names = [
|
||||||
|
"IP:127.0.0.1",
|
||||||
|
"IP:0:0:0:0:0:0:0:1",
|
||||||
|
"DNS:localhost",
|
||||||
|
]
|
||||||
if self.subdomain_host:
|
if self.subdomain_host:
|
||||||
default_alt_names.append(
|
default_alt_names.append(
|
||||||
f"DNS:{urlparse(self.subdomain_host).hostname}"
|
f"DNS:{urlparse(self.subdomain_host).hostname}"
|
||||||
|
@@ -12,6 +12,7 @@ from . import orm
|
|||||||
from .traitlets import URLPrefix
|
from .traitlets import URLPrefix
|
||||||
from .utils import (
|
from .utils import (
|
||||||
can_connect,
|
can_connect,
|
||||||
|
fmt_ip_url,
|
||||||
make_ssl_context,
|
make_ssl_context,
|
||||||
random_port,
|
random_port,
|
||||||
url_path_join,
|
url_path_join,
|
||||||
@@ -50,7 +51,7 @@ class Server(HasTraits):
|
|||||||
since it can be non-connectable value, such as '', meaning all interfaces.
|
since it can be non-connectable value, such as '', meaning all interfaces.
|
||||||
"""
|
"""
|
||||||
if self.ip in {'', '0.0.0.0', '::'}:
|
if self.ip in {'', '0.0.0.0', '::'}:
|
||||||
return self.url.replace(self._connect_ip, self.ip or '*', 1)
|
return self.url.replace(self._connect_ip, fmt_ip_url(self.ip) or '*', 1)
|
||||||
return self.url
|
return self.url
|
||||||
|
|
||||||
@observe('bind_url')
|
@observe('bind_url')
|
||||||
@@ -216,4 +217,4 @@ class Hub(Server):
|
|||||||
return url_path_join(self.url, 'api')
|
return url_path_join(self.url, 'api')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.ip}:{self.port}>"
|
return f"<{self.__class__.__name__} {fmt_ip_url(self.ip)}:{self.port}>"
|
||||||
|
@@ -46,7 +46,7 @@ from sqlalchemy.pool import StaticPool
|
|||||||
from sqlalchemy.types import LargeBinary, Text, TypeDecorator
|
from sqlalchemy.types import LargeBinary, Text, TypeDecorator
|
||||||
from tornado.log import app_log
|
from tornado.log import app_log
|
||||||
|
|
||||||
from .utils import compare_token, hash_token, new_token, random_port, utcnow
|
from .utils import compare_token, fmt_ip_url, hash_token, new_token, random_port, utcnow
|
||||||
|
|
||||||
# top-level variable for easier mocking in tests
|
# top-level variable for easier mocking in tests
|
||||||
utcnow = partial(utcnow, with_tz=False)
|
utcnow = partial(utcnow, with_tz=False)
|
||||||
@@ -157,7 +157,7 @@ class Server(Base):
|
|||||||
spawner = relationship("Spawner", back_populates="server", uselist=False)
|
spawner = relationship("Spawner", back_populates="server", uselist=False)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Server({self.ip}:{self.port})>"
|
return f"<Server({fmt_ip_url(self.ip)}:{self.port})>"
|
||||||
|
|
||||||
|
|
||||||
# lots of things have roles
|
# lots of things have roles
|
||||||
|
@@ -436,7 +436,10 @@ class Service(LoggingConfigurable):
|
|||||||
# since they are always local subprocesses
|
# since they are always local subprocesses
|
||||||
hub = copy.deepcopy(self.hub)
|
hub = copy.deepcopy(self.hub)
|
||||||
hub.connect_url = ''
|
hub.connect_url = ''
|
||||||
hub.connect_ip = '127.0.0.1'
|
if self.hub.ip and ":" in self.hub.ip:
|
||||||
|
hub.connect_ip = "::1"
|
||||||
|
else:
|
||||||
|
hub.connect_ip = "127.0.0.1"
|
||||||
|
|
||||||
self.spawner = _ServiceSpawner(
|
self.spawner = _ServiceSpawner(
|
||||||
cmd=self.command,
|
cmd=self.command,
|
||||||
|
@@ -48,6 +48,7 @@ from .traitlets import ByteSpecification, Callable, Command
|
|||||||
from .utils import (
|
from .utils import (
|
||||||
AnyTimeoutError,
|
AnyTimeoutError,
|
||||||
exponential_backoff,
|
exponential_backoff,
|
||||||
|
fmt_ip_url,
|
||||||
maybe_future,
|
maybe_future,
|
||||||
random_port,
|
random_port,
|
||||||
recursive_update,
|
recursive_update,
|
||||||
@@ -469,20 +470,40 @@ class Spawner(LoggingConfigurable):
|
|||||||
The IP address (or hostname) the single-user server should listen on.
|
The IP address (or hostname) the single-user server should listen on.
|
||||||
|
|
||||||
Usually either '127.0.0.1' (default) or '0.0.0.0'.
|
Usually either '127.0.0.1' (default) or '0.0.0.0'.
|
||||||
|
On IPv6 only networks use '::1' or '::'.
|
||||||
|
|
||||||
|
If the spawned singleuser server is running JupyterHub 5.3.0 later
|
||||||
|
You can set this to the empty string '' to indicate both IPv4 and IPv6.
|
||||||
|
|
||||||
The JupyterHub proxy implementation should be able to send packets to this interface.
|
The JupyterHub proxy implementation should be able to send packets to this interface.
|
||||||
|
|
||||||
Subclasses which launch remotely or in containers
|
Subclasses which launch remotely or in containers
|
||||||
should override the default to '0.0.0.0'.
|
should override the default to '0.0.0.0'.
|
||||||
|
|
||||||
|
.. versionchanged:: 5.3
|
||||||
|
An empty string '' means all interfaces (IPv4 and IPv6). Prior to this
|
||||||
|
the behaviour of '' was not defined.
|
||||||
|
|
||||||
.. versionchanged:: 2.0
|
.. versionchanged:: 2.0
|
||||||
Default changed to '127.0.0.1', from ''.
|
Default changed to '127.0.0.1', from unspecified.
|
||||||
In most cases, this does not result in a change in behavior,
|
|
||||||
as '' was interpreted as 'unspecified',
|
|
||||||
which used the subprocesses' own default, itself usually '127.0.0.1'.
|
|
||||||
""",
|
""",
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
|
@validate("ip")
|
||||||
|
def _strip_ipv6(self, proposal):
|
||||||
|
"""
|
||||||
|
Prior to 5.3.0 it was necessary to use [] when specifying an
|
||||||
|
[ipv6] due to the IP being concatenated with the port when forming URLs
|
||||||
|
without [].
|
||||||
|
|
||||||
|
To avoid breaking existing workarounds strip [].
|
||||||
|
"""
|
||||||
|
v = proposal["value"]
|
||||||
|
if v.startswith("[") and v.endswith("]"):
|
||||||
|
self.log.warning("Removing '[' ']' from Spawner.ip %s", self.ip)
|
||||||
|
v = v[1:-1]
|
||||||
|
return v
|
||||||
|
|
||||||
port = Integer(
|
port = Integer(
|
||||||
0,
|
0,
|
||||||
help="""
|
help="""
|
||||||
@@ -1093,7 +1114,7 @@ class Spawner(LoggingConfigurable):
|
|||||||
base_url = '/'
|
base_url = '/'
|
||||||
|
|
||||||
proto = 'https' if self.internal_ssl else 'http'
|
proto = 'https' if self.internal_ssl else 'http'
|
||||||
bind_url = f"{proto}://{self.ip}:{self.port}{base_url}"
|
bind_url = f"{proto}://{fmt_ip_url(self.ip)}:{self.port}{base_url}"
|
||||||
env["JUPYTERHUB_SERVICE_URL"] = bind_url
|
env["JUPYTERHUB_SERVICE_URL"] = bind_url
|
||||||
|
|
||||||
# the public URLs of this server and the Hub
|
# the public URLs of this server and the Hub
|
||||||
|
@@ -108,8 +108,10 @@ def can_connect(ip, port):
|
|||||||
|
|
||||||
Return True if we can connect, False otherwise.
|
Return True if we can connect, False otherwise.
|
||||||
"""
|
"""
|
||||||
if ip in {'', '0.0.0.0', '::'}:
|
if ip in {'', '0.0.0.0'}:
|
||||||
ip = '127.0.0.1'
|
ip = '127.0.0.1'
|
||||||
|
elif ip == "::":
|
||||||
|
ip = "::1"
|
||||||
try:
|
try:
|
||||||
socket.create_connection((ip, port)).close()
|
socket.create_connection((ip, port)).close()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
@@ -267,17 +269,20 @@ async def exponential_backoff(
|
|||||||
|
|
||||||
async def wait_for_server(ip, port, timeout=10):
|
async def wait_for_server(ip, port, timeout=10):
|
||||||
"""Wait for any server to show up at ip:port."""
|
"""Wait for any server to show up at ip:port."""
|
||||||
if ip in {'', '0.0.0.0', '::'}:
|
if ip in {'', '0.0.0.0'}:
|
||||||
ip = '127.0.0.1'
|
ip = '127.0.0.1'
|
||||||
app_log.debug("Waiting %ss for server at %s:%s", timeout, ip, port)
|
elif ip == "::":
|
||||||
|
ip = "::1"
|
||||||
|
display_ip = fmt_ip_url(ip)
|
||||||
|
app_log.debug("Waiting %ss for server at %s:%s", timeout, display_ip, port)
|
||||||
tic = time.perf_counter()
|
tic = time.perf_counter()
|
||||||
await exponential_backoff(
|
await exponential_backoff(
|
||||||
lambda: can_connect(ip, port),
|
lambda: can_connect(ip, port),
|
||||||
f"Server at {ip}:{port} didn't respond in {timeout} seconds",
|
f"Server at {display_ip}:{port} didn't respond in {timeout} seconds",
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
toc = time.perf_counter()
|
toc = time.perf_counter()
|
||||||
app_log.debug("Server at %s:%s responded in %.2fs", ip, port, toc - tic)
|
app_log.debug("Server at %s:%s responded in %.2fs", display_ip, port, toc - tic)
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_http_server(url, timeout=10, ssl_context=None):
|
async def wait_for_http_server(url, timeout=10, ssl_context=None):
|
||||||
@@ -962,3 +967,13 @@ def recursive_update(target, new):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
target[k] = v
|
target[k] = v
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_ip_url(ip):
|
||||||
|
"""
|
||||||
|
Format an IP for use in URLs. IPv6 is wrapped with [], everything else is
|
||||||
|
unchanged
|
||||||
|
"""
|
||||||
|
if ":" in ip:
|
||||||
|
return f"[{ip}]"
|
||||||
|
return ip
|
||||||
|
Reference in New Issue
Block a user