Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2cd02da
add srvAllowedHostsSuffix option to srv uri
sleepyStick Jun 11, 2026
499a3ec
Merge branch 'master' into PYTHON-5814
sleepyStick Jun 11, 2026
3423d39
sync unified tests
sleepyStick Jun 16, 2026
3e30434
Merge branch 'master' into PYTHON-5814
sleepyStick Jun 16, 2026
1ac4967
add unified test (forgot to commit this previously oops)
sleepyStick Jun 17, 2026
a8a3b4e
Merge branch 'master' into PYTHON-5814
sleepyStick Jun 17, 2026
9663485
Merge branch 'master' into PYTHON-5814
sleepyStick Jun 23, 2026
a71dd7a
Merge branch 'master' into PYTHON-5814
sleepyStick Jun 24, 2026
4a6ba01
add more tests and edit docstring
sleepyStick Jun 29, 2026
466a47e
edit changelog
sleepyStick Jun 29, 2026
7902127
cache public suffix list after first load
sleepyStick Jun 30, 2026
c8f8c9f
add example to docstring
sleepyStick Jun 30, 2026
2c8ad29
Merge branch 'main' into PYTHON-5814
sleepyStick Aug 11, 2026
14ae604
add psl tests
sleepyStick Aug 12, 2026
1fc7fe2
remove two label minimum
sleepyStick Aug 25, 2026
fa34923
add test
sleepyStick Aug 25, 2026
aaad72f
Merge branch 'main' into PYTHON-5814
sleepyStick Aug 25, 2026
a2c5cbd
lower the srv response
sleepyStick Aug 27, 2026
071a285
replaced this test with a different one in the spec repo -- this one …
sleepyStick Aug 27, 2026
63d57ca
add psl to resync spec script so it can be synced regularly and sync …
sleepyStick Aug 27, 2026
7c4e57a
Merge branch 'main' into PYTHON-5814
sleepyStick Aug 27, 2026
ae72df1
update changelog
sleepyStick Aug 27, 2026
c950d35
Merge branch 'PYTHON-5814' of github.com:sleepyStick/mongo-python-dri…
sleepyStick Aug 27, 2026
8f58480
NS feedback
sleepyStick Aug 27, 2026
820c648
Merge branch 'main' into PYTHON-5814
sleepyStick Aug 27, 2026
d99b4e9
sort the list alphabetically
sleepyStick Aug 27, 2026
684f31b
add test and convert all strings to punycode
sleepyStick Aug 29, 2026
44283f2
pre-convert to punycode
sleepyStick Aug 31, 2026
7a6c2e8
Merge branch 'main' into PYTHON-5814
sleepyStick Aug 31, 2026
46efa90
fix srv_allow_hosts_suffix in settings shared
sleepyStick Aug 31, 2026
c949f54
resync specs
sleepyStick Sep 2, 2026
8fb7ded
PYTHON-5814 add srvHostValidator
sleepyStick Sep 11, 2026
46f8c83
Merge branch 'main' into PYTHON-5814
sleepyStick Sep 11, 2026
1fba850
add tests
sleepyStick Sep 15, 2026
b745c3b
failed hosts shouldn't cause valid hosts to not be added during SRV p…
sleepyStick Sep 24, 2026
461ef6f
Merge branch 'main' into PYTHON-5814
sleepyStick Sep 24, 2026
23d664a
Merge branch 'main' into PYTHON-5814
sleepyStick Sep 24, 2026
886172f
i think this should fix the failing tests...
sleepyStick Sep 24, 2026
c7f1de3
Merge branch 'PYTHON-5814' of github.com:sleepyStick/mongo-python-dri…
sleepyStick Sep 24, 2026
9c94fbd
Merge branch 'main' into PYTHON-5814
sleepyStick Sep 24, 2026
26512c7
run pre-commit locally after merge
sleepyStick Sep 24, 2026
abb1550
clean up code comments
sleepyStick Sep 25, 2026
46de8d4
Merge branch 'main' into PYTHON-5814
sleepyStick Sep 25, 2026
07df266
AC feedback
sleepyStick Sep 25, 2026
aaceee2
AC feedback
sleepyStick Sep 28, 2026
386ff9e
Merge branch 'main' into PYTHON-5814
aclark4life Sep 28, 2026
59b7360
add bug to changelog
sleepyStick Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ PyMongo 4.19 brings a number of changes including:
interpreter remain daemon threads and shutdown behavior is unchanged.
Note that because these threads are non-daemon, a subinterpreter may block
on teardown until any in-flight monitor work completes.
- Added the ``srv_host_validator`` keyword argument to
:class:`~pymongo.synchronous.mongo_client.MongoClient` and
:class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient`, an alternative to
``srvAllowedHostsSuffix`` for deployments whose acceptable SRV hosts cannot be
expressed as a single suffix. The callback is invoked once per SRV-returned
host and returns ``True`` to accept it. It is mutually exclusive with
``srvAllowedHostsSuffix`` and, because it takes a callable, cannot be set in a
connection string. See the
:class:`~pymongo.synchronous.mongo_client.MongoClient` and
:class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient` documentation for
security considerations.
- ``srvAllowedHostsSuffix`` may now be set to a single label reserved for

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we add a bug fix entry for the bug in SRV polling?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I considered that but then initially decided against it because I felt that users don't have a huge control over polling behavior and no one had reported the bug so i'm not sure how realistic it was for folks to run into the case where their behavior would change. But also I'm not sure how to word the bug fix description in a way they'd understand -- I don't expect the average user to know what SRV polling refers to? But what do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I considered that but then initially decided against it because I felt that users don't have a huge control over polling behavior and no one had reported the bug so i'm not sure how realistic it was for folks to run into the case where their behavior would change. But also I'm not sure how to word the bug fix description in a way they'd understand -- I don't expect the average user to know what SRV polling refers to? But what do you think?

I would include it just because that's what happened kind of interesting and I would not worry as much about how it may be perceived… I agree it's minor though so either way up to you! E.g.

"Fixed a bug in SRV polling where the topology wouldn't change (new hosts wouldn't be added and hosts no longer in DNS wouldn't be removed) if one returned host was invalid"

private or special use, such as ``localhost``, ``test``, or ``internal``.

Bug fixes
.........
Expand All @@ -37,6 +50,8 @@ Bug fixes
delimiters (`PYTHON-6040`_).
- :class:`~pymongo.driver_info.DriverInfo` now raises :class:`ValueError` when
any field contains the reserved ``|`` delimiter (`PYTHON-6040`_).
- Fixed a bug in SRV polling where invalid hosts where topology would not be
updated if one returned host was invalid.

.. _PYTHON-6074: https://jira.mongodb.org/browse/PYTHON-6074
.. _PYTHON-6040: https://jira.mongodb.org/browse/PYTHON-6040
Expand Down
22 changes: 21 additions & 1 deletion pymongo/_psl.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,34 @@

_PUBLIC_SUFFIXES: Optional[tuple[set[str], set[str], set[str]]] = None

# Single labels that srvAllowedHostsSuffix may be set to. See the
# srvAllowedHostsSuffix section of the Initial DNS Seedlist Discovery spec.
SPECIAL_USE_LABELS = frozenset(
[
# RFC 6761 special use names.
"test",
"localhost",
"invalid",
"example",
# RFC 6762 multicast DNS.
"local",
# Reserved by ICANN for private use.
"internal",
# Not officially reserved by ICANN but commonly used privately.
"corp",
"home",
"mail",
]
)


def _to_punycode(string: str) -> str:
"""Convert a string to Punycode."""
try:
return string.encode("idna").decode("ascii")
except UnicodeError:
# Not every input is a valid IDNA label (e.g. empty or over-long
# labels). Leave it as-is and let the caller reject it.
# labels). Return it unchanged -- SRV host will fail verification.
return string


Expand Down
11 changes: 11 additions & 0 deletions pymongo/_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,17 @@ def log_srv_monitor_failure(failure: Exception) -> None:
_debug_log(_SDAM_LOGGER, message="SRV monitor check failed", failure=repr(failure))


def log_srv_monitor_invalid_host(host: str, failure: Exception) -> None:
"""Emit a log entry when the SRV monitor skips a host that failed verification."""
if _is_debug_enabled(_SDAM_LOGGER):
_debug_log(
_SDAM_LOGGER,
message="SRV monitor skipped invalid host",
serverHost=host,
failure=repr(failure),
)


def log_command_retry(
topology_id: Optional[ObjectId],
command_name: str,
Expand Down
57 changes: 55 additions & 2 deletions pymongo/asynchronous/mongo_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,31 @@ def __init__(
srvAllowedHostsSuffix=".internal.example.com",
)

- `srv_host_validator`: (callable or None) A callback used in place of the
default parent-domain check for hosts returned by SRV DNS records. It is
called once per returned host with the lowercased hostname as its only
argument, and must return ``True`` to accept the host or ``False`` to
reject it. During initial DNS discovery, rejecting a host raises
:exc:`~pymongo.errors.ConfigurationError`, as does an exception raised by
the callback itself. Use this when the set of acceptable hosts cannot be
expressed as a single suffix::

def validator(host: str) -> bool:
return host.endswith((".a.example.com", ".b.example.com"))

AsyncMongoClient(
"mongodb+srv://cluster.example.com/",
srv_host_validator=validator,
)

The callback must not block. It is mutually exclusive with
``srvAllowedHostsSuffix``. Because this option is a callable, it can
only be passed in as a keyword argument, not through the connection string.

.. warning::

This option replaces the built-in DNS spoofing safeguards.
Please use with caution.

| **Write Concern options:**
| (Only set if passed. No default values.)
Expand Down Expand Up @@ -637,6 +662,12 @@ def __init__(

.. seealso:: The MongoDB documentation on `connections <https://dochub.mongodb.org/core/connections>`_.

.. versionchanged:: 4.19
Added the ``srv_host_validator`` keyword argument.

.. versionchanged:: 4.18
Added the ``srvAllowedHostsSuffix`` URI and keyword argument.

.. versionchanged:: 4.17
Added the ``max_adaptive_retries`` and ``enable_overload_retargeting`` URI and keyword arguments.

Expand Down Expand Up @@ -823,6 +854,7 @@ def __init__(
srv_service_name = keyword_opts.get("srvservicename")
srv_max_hosts = keyword_opts.get("srvmaxhosts")
srv_allowed_hosts_suffix = keyword_opts.get("srvallowedhostssuffix")
srv_host_validator = keyword_opts.get("srv_host_validator")
if len([h for h in self._host if "/" in h]) > 1:
raise ConfigurationError("host must not contain multiple MongoDB URIs")
for entity in self._host:
Expand Down Expand Up @@ -915,7 +947,11 @@ def __init__(
)

self._init_based_on_options(
self._seeds, srv_max_hosts, srv_service_name, srv_allowed_hosts_suffix
self._seeds,
srv_max_hosts,
srv_service_name,
srv_allowed_hosts_suffix,
srv_host_validator,
)

self._opened = False
Expand All @@ -935,6 +971,7 @@ async def _resolve_srv(self) -> None:
srv_service_name = keyword_opts.get("srvservicename")
srv_max_hosts = keyword_opts.get("srvmaxhosts")
srv_allowed_hosts_suffix = keyword_opts.get("srvallowedhostssuffix")
srv_host_validator = keyword_opts.get("srv_host_validator")
for entity in self._host:
# A hostname can only include a-z, 0-9, '-' and '.'. If we find a '/'
# it must be a URI,
Expand All @@ -956,6 +993,7 @@ async def _resolve_srv(self) -> None:
srv_service_name=srv_service_name,
srv_max_hosts=srv_max_hosts,
srv_allowed_hosts_suffix=srv_allowed_hosts_suffix,
srv_host_validator=srv_host_validator,
)
seeds.update(res["nodelist"])
opts = res["options"]
Expand Down Expand Up @@ -1000,7 +1038,11 @@ async def _resolve_srv(self) -> None:
)

self._init_based_on_options(
seeds, srv_max_hosts, srv_service_name, srv_allowed_hosts_suffix
seeds,
srv_max_hosts,
srv_service_name,
srv_allowed_hosts_suffix,
srv_host_validator,
)

def _init_based_on_options(
Expand All @@ -1009,7 +1051,17 @@ def _init_based_on_options(
srv_max_hosts: Any,
srv_service_name: Any,
srv_allowed_hosts_suffix: Any,
srv_host_validator: Any = None,
) -> None:
if srv_host_validator is not None:
if srv_allowed_hosts_suffix is not None:
raise ConfigurationError(
"Cannot specify both srv_host_validator and srvAllowedHostsSuffix"
)
if not self._resolve_srv_info["is_srv"]:
raise ConfigurationError(
"The srv_host_validator option is only allowed with 'mongodb+srv://' URIs"
)
self._event_listeners = self._options.pool_options._event_listeners
self._topology_settings = TopologySettings(
seeds=seeds,
Expand All @@ -1028,6 +1080,7 @@ def _init_based_on_options(
srv_service_name=srv_service_name,
srv_max_hosts=srv_max_hosts,
srv_allowed_hosts_suffix=srv_allowed_hosts_suffix,
srv_host_validator=srv_host_validator,
server_monitoring_mode=self._options.server_monitoring_mode,
topology_id=self._topology_settings._topology_id if self._topology_settings else None,
)
Expand Down
1 change: 1 addition & 0 deletions pymongo/asynchronous/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ async def _get_seedlist(self) -> Optional[list[tuple[str, Any]]]:
self._settings.pool_options.connect_timeout,
self._settings.srv_service_name,
srv_allowed_hosts_suffix=self._settings.srv_allowed_hosts_suffix,
srv_host_validator=self._settings.srv_host_validator,
)
seedlist, ttl = await resolver.get_hosts_and_min_ttl()
if len(seedlist) == 0:
Expand Down
4 changes: 3 additions & 1 deletion pymongo/asynchronous/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import threading
from collections.abc import Collection
from typing import Optional
from typing import Callable, Optional

from bson.objectid import ObjectId
from pymongo import common
Expand Down Expand Up @@ -51,6 +51,7 @@ def __init__(
srv_service_name: str = common.SRV_SERVICE_NAME,
srv_max_hosts: int = 0,
srv_allowed_hosts_suffix: Optional[str] = None,
srv_host_validator: Optional[Callable[[str], bool]] = None,
server_monitoring_mode: str = common.SERVER_MONITORING_MODE,
topology_id: Optional[ObjectId] = None,
):
Expand All @@ -77,6 +78,7 @@ def __init__(
srv_service_name,
srv_max_hosts,
srv_allowed_hosts_suffix,
srv_host_validator,
server_monitoring_mode,
topology_id,
)
Loading
Loading