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:
Min RK
2025-03-28 10:31:07 +01:00
committed by GitHub
6 changed files with 60 additions and 16 deletions

View File

@@ -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}"

View File

@@ -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}>"

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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