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
)
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:
default_alt_names.append(
f"DNS:{urlparse(self.subdomain_host).hostname}"

View File

@@ -12,6 +12,7 @@ from . import orm
from .traitlets import URLPrefix
from .utils import (
can_connect,
fmt_ip_url,
make_ssl_context,
random_port,
url_path_join,
@@ -50,7 +51,7 @@ class Server(HasTraits):
since it can be non-connectable value, such as '', meaning all interfaces.
"""
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
@observe('bind_url')
@@ -216,4 +217,4 @@ class Hub(Server):
return url_path_join(self.url, 'api')
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 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
utcnow = partial(utcnow, with_tz=False)
@@ -157,7 +157,7 @@ class Server(Base):
spawner = relationship("Spawner", back_populates="server", uselist=False)
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

View File

@@ -436,7 +436,10 @@ class Service(LoggingConfigurable):
# since they are always local subprocesses
hub = copy.deepcopy(self.hub)
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(
cmd=self.command,

View File

@@ -48,6 +48,7 @@ from .traitlets import ByteSpecification, Callable, Command
from .utils import (
AnyTimeoutError,
exponential_backoff,
fmt_ip_url,
maybe_future,
random_port,
recursive_update,
@@ -469,20 +470,40 @@ class Spawner(LoggingConfigurable):
The IP address (or hostname) the single-user server should listen on.
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.
Subclasses which launch remotely or in containers
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
Default changed to '127.0.0.1', from ''.
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'.
Default changed to '127.0.0.1', from unspecified.
""",
).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(
0,
help="""
@@ -1093,7 +1114,7 @@ class Spawner(LoggingConfigurable):
base_url = '/'
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
# 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.
"""
if ip in {'', '0.0.0.0', '::'}:
if ip in {'', '0.0.0.0'}:
ip = '127.0.0.1'
elif ip == "::":
ip = "::1"
try:
socket.create_connection((ip, port)).close()
except OSError as e:
@@ -267,17 +269,20 @@ async def exponential_backoff(
async def wait_for_server(ip, port, timeout=10):
"""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'
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()
await exponential_backoff(
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,
)
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):
@@ -962,3 +967,13 @@ def recursive_update(target, new):
else:
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