Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-asyncssh for openSUSE:Factory 
checked in at 2026-06-16 18:49:11
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-asyncssh (Old)
 and      /work/SRC/openSUSE:Factory/.python-asyncssh.new.1981 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-asyncssh"

Tue Jun 16 18:49:11 2026 rev:34 rq:1359766 version:2.23.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-asyncssh/python-asyncssh.changes  
2026-04-10 21:28:48.066472845 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-asyncssh.new.1981/python-asyncssh.changes    
    2026-06-16 18:49:15.123450681 +0200
@@ -1,0 +2,22 @@
+Tue Jun 16 10:54:48 UTC 2026 - Nico Krapp <[email protected]>
+
+- Update to 2.23.1
+  * Fixed an SCP path traversal issue.
+  * Expanded previous fix to block unsafe user substitutions in server config.
+  * Fixed default value for reuse_address and reuse_port, matching the
+    behavior of asyncio.create_server().
+- Update to 2.23.0 (fixes CVE-2026-45309, bsc#1268333)
+  * Added support for "Match localnetwork".
+  * Enabled support for RSA with SHA-2 signatures in ssh-agent and Pageant.
+  * Changed MAC algorithm negotation to be skipped when using AEAD ciphers.
+  * Improved graceful termination when using ProxyCommand, waiting for the
+    ProxyCommand tunnel to close when cleaning up a connection.
+  * Blocked unsafe user substitutions from being used in server config.
+  * Fixed an issue with config evaluation when "Match final" was combined
+    with Hostname directives.
+  * Fixed a resource leak in xauth support.
+  * Fixed issue with multi-hop ProxyJump directives in a config file not
+    working correctly.
+  * Fixed string encoding in SFTPName objects returned by realpath().
+
+-------------------------------------------------------------------

Old:
----
  asyncssh-2.22.0.tar.gz

New:
----
  asyncssh-2.23.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-asyncssh.spec ++++++
--- /var/tmp/diff_new_pack.lJvMCt/_old  2026-06-16 18:49:16.107491852 +0200
+++ /var/tmp/diff_new_pack.lJvMCt/_new  2026-06-16 18:49:16.119492354 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-asyncssh
-Version:        2.22.0
+Version:        2.23.1
 Release:        0
 Summary:        Asynchronous SSHv2 client and server library
 License:        EPL-2.0 OR GPL-2.0-or-later
@@ -31,6 +31,7 @@
 BuildRequires:  %{python_module cryptography >= 39.0}
 BuildRequires:  %{python_module fido2 >= 2}
 BuildRequires:  %{python_module gssapi >= 1.2.0}
+BuildRequires:  %{python_module ifaddr >= 0.2.0}
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module pyOpenSSL >= 17.0.0}
 BuildRequires:  %{python_module pytest}
@@ -47,6 +48,7 @@
 Recommends:     python-bcrypt >= 3.1.3
 Recommends:     python-fido2 >= 2
 Recommends:     python-gssapi >= 1.2.0
+Recommends:     python-ifaddr >= 0.2.0
 Recommends:     python-libnacl >= 1.4.2
 Recommends:     python-pyOpenSSL >= 23.0.0
 BuildArch:      noarch

++++++ asyncssh-2.22.0.tar.gz -> asyncssh-2.23.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/PKG-INFO new/asyncssh-2.23.1/PKG-INFO
--- old/asyncssh-2.22.0/PKG-INFO        2025-12-22 00:34:03.760306100 +0100
+++ new/asyncssh-2.23.1/PKG-INFO        2026-06-07 16:10:42.531565200 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: asyncssh
-Version: 2.22.0
+Version: 2.23.1
 Summary: AsyncSSH: Asynchronous SSHv2 client and server library
 Author-email: Ron Frederick <[email protected]>
 License: EPL-2.0 OR GPL-2.0-or-later
@@ -32,10 +32,10 @@
 Requires-Dist: bcrypt>=3.1.3; extra == "bcrypt"
 Provides-Extra: fido2
 Requires-Dist: fido2>=2; extra == "fido2"
+Provides-Extra: ifaddr
+Requires-Dist: ifaddr>=0.2.0; extra == "ifaddr"
 Provides-Extra: gssapi
 Requires-Dist: gssapi>=1.2.0; extra == "gssapi"
-Provides-Extra: libnacl
-Requires-Dist: libnacl>=1.4.2; extra == "libnacl"
 Provides-Extra: pkcs11
 Requires-Dist: python-pkcs11>=0.7.0; extra == "pkcs11"
 Provides-Extra: pyopenssl
@@ -196,6 +196,9 @@
 * Install fido2 from https://pypi.org/project/fido2 if you want support
   for key exchange and authentication with U2F/FIDO2 security keys.
 
+* Install ifaddr from https://pypi.org/project/ifaddr/ if you want
+  support for matching on local network IP addresses.
+
 * Install python-pkcs11 from https://pypi.org/project/python-pkcs11 if
   you want support for accessing PIV keys on PKCS#11 security tokens.
 
@@ -221,24 +224,25 @@
 
   | bcrypt
   | fido2
+  | ifaddr
   | gssapi
   | pkcs11
   | pyOpenSSL
   | pywin32
 
-For example, to install bcrypt, fido2, gssapi, pkcs11, and pyOpenSSL
+For example, to install bcrypt, fido2, gssapi, ifaddr, pkcs11, and pyOpenSSL
 on UNIX, you can run:
 
   ::
 
-    pip install 'asyncssh[bcrypt,fido2,gssapi,pkcs11,pyOpenSSL]'
+    pip install 'asyncssh[bcrypt,fido2,gssapi,ifaddr,pkcs11,pyOpenSSL]'
 
-To install bcrypt, fido2, pkcs11, pyOpenSSL, and pywin32 on Windows,
+To install bcrypt, fido2, ifaddr, pkcs11, pyOpenSSL, and pywin32 on Windows,
 you can run:
 
   ::
 
-    pip install 'asyncssh[bcrypt,fido2,pkcs11,pyOpenSSL,pywin32]'
+    pip install 'asyncssh[bcrypt,fido2,ifaddr,pkcs11,pyOpenSSL,pywin32]'
 
 Note that you will still need to manually install the libnettle library
 for UMAC support. Unfortunately, since liboqs and libnettle are not
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/README.rst 
new/asyncssh-2.23.1/README.rst
--- old/asyncssh-2.22.0/README.rst      2025-12-21 23:41:04.000000000 +0100
+++ new/asyncssh-2.23.1/README.rst      2026-05-09 04:30:40.000000000 +0200
@@ -150,6 +150,9 @@
 * Install fido2 from https://pypi.org/project/fido2 if you want support
   for key exchange and authentication with U2F/FIDO2 security keys.
 
+* Install ifaddr from https://pypi.org/project/ifaddr/ if you want
+  support for matching on local network IP addresses.
+
 * Install python-pkcs11 from https://pypi.org/project/python-pkcs11 if
   you want support for accessing PIV keys on PKCS#11 security tokens.
 
@@ -175,24 +178,25 @@
 
   | bcrypt
   | fido2
+  | ifaddr
   | gssapi
   | pkcs11
   | pyOpenSSL
   | pywin32
 
-For example, to install bcrypt, fido2, gssapi, pkcs11, and pyOpenSSL
+For example, to install bcrypt, fido2, gssapi, ifaddr, pkcs11, and pyOpenSSL
 on UNIX, you can run:
 
   ::
 
-    pip install 'asyncssh[bcrypt,fido2,gssapi,pkcs11,pyOpenSSL]'
+    pip install 'asyncssh[bcrypt,fido2,gssapi,ifaddr,pkcs11,pyOpenSSL]'
 
-To install bcrypt, fido2, pkcs11, pyOpenSSL, and pywin32 on Windows,
+To install bcrypt, fido2, ifaddr, pkcs11, pyOpenSSL, and pywin32 on Windows,
 you can run:
 
   ::
 
-    pip install 'asyncssh[bcrypt,fido2,pkcs11,pyOpenSSL,pywin32]'
+    pip install 'asyncssh[bcrypt,fido2,ifaddr,pkcs11,pyOpenSSL,pywin32]'
 
 Note that you will still need to manually install the libnettle library
 for UMAC support. Unfortunately, since liboqs and libnettle are not
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/agent.py 
new/asyncssh-2.23.1/asyncssh/agent.py
--- old/asyncssh-2.22.0/asyncssh/agent.py       2024-12-25 21:47:00.000000000 
+0100
+++ new/asyncssh-2.23.1/asyncssh/agent.py       2026-05-09 04:30:40.000000000 
+0200
@@ -121,9 +121,7 @@
         else:
             sig_algorithm = algorithm
 
-        # Neither Pageant nor the Win10 OpenSSH agent seems to support the
-        # ssh-agent protocol flags used to request RSA SHA2 signatures yet
-        if sig_algorithm == b'ssh-rsa' and sys.platform != 'win32':
+        if sig_algorithm == b'ssh-rsa':
             sig_algorithms: Sequence[bytes] = \
                 (b'rsa-sha2-256', b'rsa-sha2-512', b'ssh-rsa')
         else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/config.py 
new/asyncssh-2.23.1/asyncssh/config.py
--- old/asyncssh-2.22.0/asyncssh/config.py      2025-12-21 23:41:04.000000000 
+0100
+++ new/asyncssh-2.23.1/asyncssh/config.py      2026-06-07 16:00:27.000000000 
+0200
@@ -1,4 +1,4 @@
-# Copyright (c) 2020-2024 by Ron Frederick <[email protected]> and others.
+# Copyright (c) 2020-2026 by Ron Frederick <[email protected]> and others.
 #
 # This program and the accompanying materials are made available under
 # the terms of the Eclipse Public License v2.0 which accompanies this
@@ -29,20 +29,27 @@
 from hashlib import sha1
 from pathlib import Path, PurePath
 from subprocess import DEVNULL
-from typing import Callable, Dict, List, NoReturn, Optional, Sequence
-from typing import Set, Tuple, Union, cast
+from typing import Callable, Dict, Iterator, List, NoReturn, Optional
+from typing import Sequence, Set, Tuple, Union, cast
 
 from .constants import DEFAULT_PORT
 from .logging import logger
-from .misc import DefTuple, FilePath, ip_address
+from .misc import DefTuple, FilePath, IllegalUserName, ip_address
 from .pattern import HostPatternList, WildcardPatternList
 
+try:
+    import ifaddr
+    _ifaddr_available = True
+except ImportError: # pragma: no cover
+    _ifaddr_available = False
+
 
 ConfigPaths = Union[None, FilePath, Sequence[FilePath]]
 
 
 _token_pattern = re.compile(r'%(.)')
-_env_pattern = re.compile(r'\${(.*)}')
+_env_pattern = re.compile(r'\${(.*?)}')
+_unsafe_user_pattern = re.compile(r'^\.\.$|^~|^[A-Za-z]:|[/\\]|\$\{.*?\}')
 
 
 def _exec(cmd: str) -> bool:
@@ -52,6 +59,18 @@
                           stdout=DEVNULL, stderr=DEVNULL).returncode == 0
 
 
+def _get_local_ips() -> Iterator[str]:
+    """Return local IP addresses of the system"""
+
+    for adapter in ifaddr.get_adapters():
+        for ip in adapter.ips:
+            if isinstance(ip.ip, tuple):
+                addr, _, scope_id = ip.ip
+                yield f'{addr}%{scope_id}' if scope_id else addr
+            else:
+                yield ip.ip
+
+
 class ConfigParseError(ValueError):
     """Configuration parsing exception"""
 
@@ -183,6 +202,10 @@
             elif match == 'final':
                 result = cast(bool, self._final)
             else:
+                if (match == 'localnetwork' and
+                        not _ifaddr_available): # pragma: no cover
+                    self._error('Local network match requires ifaddr module')
+
                 match_val = self._match_val(match)
 
                 if match != 'exec' and match_val is None:
@@ -201,6 +224,15 @@
                         ip = ip_address(cast(str, match_val)) \
                             if match_val else None
                         result = host_pat.matches(None, match_val, ip)
+                    elif match == 'localnetwork':
+                        host_pat = HostPatternList(arg)
+
+                        for addr in cast(Iterator[str], match_val):
+                            if host_pat.matches(None, addr, ip_address(addr)):
+                                result = True
+                                break
+                        else:
+                            result = False
                     else:
                         wild_pat = WildcardPatternList(arg)
                         result = wild_pat.matches(match_val)
@@ -514,6 +546,8 @@
             return self._options.get('Hostname', self._orig_host)
         elif match == 'originalhost':
             return self._orig_host
+        elif match == 'localnetwork':
+            return _get_local_ips()
         elif match == 'localuser':
             return self._local_user
         elif match == 'user':
@@ -677,7 +711,23 @@
             return None
 
     def _set_tokens(self) -> None:
-        """Set the tokens available for percent expansion"""
+        """Set the tokens available for percent expansion
+
+           Only allow "safe" username substitutions. Unsafe usernames are:
+
+               - a username of exactly ".."
+               - a username beginning with a "~"
+               - a username beginning with a Windows drive letter and a ":"
+               - a username containing forward or backward slashes
+               - a username containing an env substitution like "${...}"
+
+           Note: this code assumes that saslprep has already been performed
+           on the username before it is accessed here.
+
+        """
+
+        if _unsafe_user_pattern.search(self._user):
+            raise IllegalUserName('Unsafe username substitution')
 
         self._tokens.update({'u': self._user})
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/connection.py 
new/asyncssh-2.23.1/asyncssh/connection.py
--- old/asyncssh-2.22.0/asyncssh/connection.py  2025-12-21 23:41:04.000000000 
+0100
+++ new/asyncssh-2.23.1/asyncssh/connection.py  2026-06-07 16:00:27.000000000 
+0200
@@ -82,6 +82,7 @@
 
 from .encryption import Encryption, get_encryption_algs
 from .encryption import get_default_encryption_algs
+from .encryption import encryption_needs_mac
 from .encryption import get_encryption_params, get_encryption
 
 from .forward import SSHForwarder
@@ -196,6 +197,9 @@
     def close(self) -> None:
         """Close this tunnel"""
 
+    async def wait_closed(self):
+        """Wait for this tunnel to close"""
+
 class _TunnelConnectorProtocol(_TunnelProtocol, Protocol):
     """Protocol to open a connection to tunnel an SSH connection over"""
 
@@ -279,7 +283,7 @@
                              options: 'SSHConnectionOptions') -> Optional[str]:
     """Canonicalize a host name"""
 
-    host = options.host
+    host = options.orig_host
 
     if not options.canonicalize_hostname or not options.canonical_domains:
         logger.info('Host canonicalization disabled')
@@ -387,6 +391,11 @@
 
             self._conn.connection_lost(exc)
 
+        def process_exited(self):
+            """Called when the child process has exited"""
+
+            self._close_event.set()
+
         def write(self, data: bytes) -> None:
             """Write data to this tunnel"""
 
@@ -403,13 +412,20 @@
 
             if self._transport: # pragma: no cover
                 self._transport.close()
+                self._transport = None
 
-            self._close_event.set()
+        async def wait_closed(self):
+            """Wait for this subprocess to exit"""
+
+            await self._close_event.wait()
 
+    _, tunnel = await loop.subprocess_exec(_ProxyCommandTunnel, *command,
+                                           start_new_session=True)
 
-    _, tunnel = await loop.subprocess_exec(_ProxyCommandTunnel, *command)
+    conn = cast(_Conn, cast(_ProxyCommandTunnel, tunnel).get_conn())
+    conn.set_tunnel(tunnel)
 
-    return cast(_Conn, cast(_ProxyCommandTunnel, tunnel).get_conn())
+    return conn
 
 
 async def _open_tunnel(tunnels: object, options: _Options,
@@ -437,8 +453,8 @@
 
             last_conn = conn
             conn = await connect(host, port, username=username,
-                                 passphrase=options.passphrase, tunnel=conn,
-                                 config=config)
+                                 passphrase=options.passphrase,
+                                 tunnel=conn or (), config=config)
             conn.set_tunnel(last_conn)
 
             if options.canonicalize_hostname != 'always':
@@ -459,7 +475,7 @@
 
     canon_host = await _canonicalize_host(loop, options)
 
-    host = canon_host if canon_host else options.host
+    host = canon_host if canon_host else options.orig_host
     canonical = bool(canon_host)
     final = options.config.has_match_final()
 
@@ -537,7 +553,7 @@
 async def _listen(options: _Options, config: DefTuple[ConfigPaths],
                   loop: asyncio.AbstractEventLoop, flags: int,
                   backlog: int, sock: Optional[socket.socket],
-                  reuse_address: bool, reuse_port: bool,
+                  reuse_address: Optional[bool], reuse_port: Optional[bool],
                   conn_factory: Callable[[], _Conn],
                   msg: str) -> 'SSHAcceptor':
     """Make inbound TCP or SSH tunneled listener"""
@@ -651,7 +667,8 @@
 
 def _select_algs(alg_type: str, algs: _AlgsArg, config_algs: _AlgsArg,
                  possible_algs: List[bytes], default_algs: List[bytes],
-                 none_value: Optional[bytes] = None) -> Sequence[bytes]:
+                 none_value: Optional[bytes] = None,
+                 allow_empty: bool = False) -> Sequence[bytes]:
     """Select a set of allowed algorithms"""
 
     if algs == ():
@@ -682,6 +699,8 @@
         return result
     elif none_value:
         return [none_value]
+    elif allow_empty:
+        return []
     else:
         raise ValueError(f'No {alg_type} algorithms selected')
 
@@ -714,7 +733,7 @@
                             get_default_encryption_algs())
     mac_algs = _select_algs('MAC', mac_algs_arg,
                             cast(_AlgsArg, config.get('MACs', ())),
-                            get_mac_algs(), get_default_mac_algs())
+                            get_mac_algs(), get_default_mac_algs(), None, True)
     cmp_algs = _select_algs('compression', cmp_algs_arg,
                             cast(_AlgsArg, config.get_compression_algs()),
                             get_compression_algs(),
@@ -1090,15 +1109,15 @@
 
             self._owner = None
 
+        if self._tunnel:
+            self._tunnel.close()
+            self._tunnel = None
+
         self._cancel_login_timer()
         self._close_event.set()
 
         self._inpbuf = b''
 
-        if self._tunnel:
-            self._tunnel.close()
-            self._tunnel = None
-
     def _cancel_login_timer(self) -> None:
         """Cancel the login timer"""
 
@@ -1489,8 +1508,8 @@
 
         raise KeyExchangeFailed(
             f'No matching {alg_type} algorithm found, sent '
-            f'{b",".join(local_algs).decode("ascii")} and received '
-            f'{b",".join(remote_algs).decode("ascii")}')
+            f'{b",".join(local_algs).decode("ascii") or "<None>"} and received 
'
+            f'{b",".join(remote_algs).decode("ascii") or "<None>"}')
 
     def _get_extra_kex_algs(self) -> List[bytes]:
         """Return the extra kex algs to add"""
@@ -1852,7 +1871,7 @@
         self.logger.debug2('  Key exchange algs: %s', kex_algs)
         self.logger.debug2('  Host key algs: %s', host_key_algs)
         self.logger.debug2('  Encryption algs: %s', self._enc_algs)
-        self.logger.debug2('  MAC algs: %s', self._mac_algs)
+        self.logger.debug2('  MAC algs: %s', self._mac_algs or '<None>')
         self.logger.debug2('  Compression algs: %s', self._cmp_algs)
 
         cookie = os.urandom(16)
@@ -1905,12 +1924,6 @@
         mac_keysize_sc, mac_hashsize_sc, etm_sc = \
             get_encryption_params(self._enc_alg_sc, self._mac_alg_sc)
 
-        if mac_keysize_cs == 0:
-            self._mac_alg_cs = self._enc_alg_cs
-
-        if mac_keysize_sc == 0:
-            self._mac_alg_sc = self._enc_alg_sc
-
         cmp_after_auth_cs = get_compression_params(self._cmp_alg_cs)
         cmp_after_auth_sc = get_compression_params(self._cmp_alg_sc)
 
@@ -2406,11 +2419,11 @@
         self.logger.debug2('  Host key algs: %s', peer_host_key_algs)
         self.logger.debug2('  Client to server:')
         self.logger.debug2('    Encryption algs: %s', enc_algs_cs)
-        self.logger.debug2('    MAC algs: %s', mac_algs_cs)
+        self.logger.debug2('    MAC algs: %s', mac_algs_cs or '<None>')
         self.logger.debug2('    Compression algs: %s', cmp_algs_cs)
         self.logger.debug2('  Server to client:')
         self.logger.debug2('    Encryption algs: %s', enc_algs_sc)
-        self.logger.debug2('    MAC algs: %s', mac_algs_sc)
+        self.logger.debug2('    MAC algs: %s', mac_algs_sc or '<None>')
         self.logger.debug2('    Compression algs: %s', cmp_algs_sc)
 
         kex_alg = self._choose_alg('key exchange', kex_algs, peer_kex_algs)
@@ -2431,8 +2444,17 @@
         self._enc_alg_sc = self._choose_alg('encryption', self._enc_algs,
                                             enc_algs_sc)
 
-        self._mac_alg_cs = self._choose_alg('MAC', self._mac_algs, mac_algs_cs)
-        self._mac_alg_sc = self._choose_alg('MAC', self._mac_algs, mac_algs_sc)
+        if encryption_needs_mac(self._enc_alg_cs):
+            self._mac_alg_cs = self._choose_alg('MAC', self._mac_algs,
+                                                mac_algs_cs)
+        else:
+            self._mac_alg_cs = self._enc_alg_cs
+
+        if encryption_needs_mac(self._enc_alg_sc):
+            self._mac_alg_sc = self._choose_alg('MAC', self._mac_algs,
+                                                mac_algs_sc)
+        else:
+            self._mac_alg_sc = self._enc_alg_sc
 
         self._cmp_alg_cs = self._choose_alg('compression', self._cmp_algs,
                                             cmp_algs_cs)
@@ -2851,6 +2873,9 @@
         if self._agent:
             await self._agent.wait_closed()
 
+        if self._tunnel:
+            await self._tunnel.wait_closed()
+
         await self._close_event.wait()
 
     def disconnect(self, code: int, reason: str,
@@ -7277,6 +7302,7 @@
     waiter: Optional[asyncio.Future]
     protocol_factory: _ProtocolFactory
     version: bytes
+    orig_host: str
     host: str
     port: int
     tunnel: object
@@ -7369,6 +7395,7 @@
         self.protocol_factory = protocol_factory
         self.version = _validate_version(version)
 
+        self.orig_host = host
         self.host = cast(str, config.get('Hostname', host))
         self.port = cast(int, port if port != () else
             config.get('Port', DEFAULT_PORT))
@@ -9326,7 +9353,8 @@
                  tunnel: DefTuple[_TunnelListener] = (),
                  family: DefTuple[int] = (), flags:int = socket.AI_PASSIVE,
                  backlog: int = 100, sock: Optional[socket.socket] = None,
-                 reuse_address: bool = False, reuse_port: bool = False,
+                 reuse_address: Optional[bool] = None,
+                 reuse_port: Optional[bool] = None,
                  acceptor: _AcceptHandler = None,
                  error_handler: _ErrorHandler = None,
                  config: DefTuple[ConfigPaths] = (),
@@ -9384,7 +9412,7 @@
            port other existing sockets are bound to, so long as they all
            set this flag when being created. If not specified, the
            default is to not allow this. This option is not supported
-           on Windows or Python versions prior to 3.4.4.
+           on Windows.
        :param acceptor: (optional)
            A `callable` or coroutine which will be called when the
            SSH handshake completes on an accepted connection, taking
@@ -9414,8 +9442,8 @@
        :type flags: flags to pass to :meth:`getaddrinfo() <socket.getaddrinfo>`
        :type backlog: `int`
        :type sock: :class:`socket.socket` or `None`
-       :type reuse_address: `bool`
-       :type reuse_port: `bool`
+       :type reuse_address: `bool` or `None`
+       :type reuse_port: `bool` or `None`
        :type acceptor: `callable` or coroutine
        :type error_handler: `callable`
        :type config: `list` of `str`
@@ -9451,7 +9479,8 @@
                          family: DefTuple[int] = (),
                          flags: int = socket.AI_PASSIVE, backlog: int = 100,
                          sock: Optional[socket.socket] = None,
-                         reuse_address: bool = False, reuse_port: bool = False,
+                         reuse_address: Optional[bool] = None,
+                         reuse_port: Optional[bool] = None,
                          acceptor: _AcceptHandler = None,
                          error_handler: _ErrorHandler = None,
                          config: DefTuple[ConfigPaths] = (),
@@ -9518,7 +9547,7 @@
            port other existing sockets are bound to, so long as they all
            set this flag when being created. If not specified, the
            default is to not allow this. This option is not supported
-           on Windows or Python versions prior to 3.4.4.
+           on Windows.
        :param acceptor: (optional)
            A `callable` or coroutine which will be called when the
            SSH handshake completes on an accepted connection, taking
@@ -9553,8 +9582,8 @@
        :type flags: flags to pass to :meth:`getaddrinfo() <socket.getaddrinfo>`
        :type backlog: `int`
        :type sock: :class:`socket.socket` or `None`
-       :type reuse_address: `bool`
-       :type reuse_port: `bool`
+       :type reuse_address: `bool` or `None`
+       :type reuse_port: `bool` or `None`
        :type acceptor: `callable` or coroutine
        :type error_handler: `callable`
        :type config: `list` of `str`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/crypto/pq.py 
new/asyncssh-2.23.1/asyncssh/crypto/pq.py
--- old/asyncssh-2.22.0/asyncssh/crypto/pq.py   2024-10-26 18:20:18.000000000 
+0200
+++ new/asyncssh-2.23.1/asyncssh/crypto/pq.py   2026-05-09 04:30:40.000000000 
+0200
@@ -58,10 +58,12 @@
             self.pubkey_bytes, self.privkey_bytes, \
             self.ciphertext_bytes, self.secret_bytes, \
             oqs_name = _pq_algs[alg_name]
-        except KeyError: # pragma: no cover, other algs not registered
-            raise ValueError(f'Unknown PQ algorithm {oqs_name}') from None
+        except KeyError:
+            raise ValueError('Unknown PQ algorithm ' +
+                             alg_name.decode()) from None
 
-        if not hasattr(_oqs, 'OQS_' + oqs_name + '_keypair'): # pragma: no 
cover
+        if not hasattr(_oqs, 'OQS_' + oqs_name + # pragma: no cover
+                       '_keypair'):
             oqs_name += '_ipd'
 
         self._keypair = getattr(_oqs, 'OQS_' + oqs_name + '_keypair')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/encryption.py 
new/asyncssh-2.23.1/asyncssh/encryption.py
--- old/asyncssh-2.22.0/asyncssh/encryption.py  2024-07-03 18:02:26.000000000 
+0200
+++ new/asyncssh-2.23.1/asyncssh/encryption.py  2026-05-09 04:30:40.000000000 
+0200
@@ -47,6 +47,12 @@
         raise NotImplementedError
 
     @classmethod
+    def needs_mac(cls) -> bool:
+        """Return whether a MAC algorithm is needed for this encryption"""
+
+        return True
+
+    @classmethod
     def get_mac_params(cls, mac_alg: bytes) -> Tuple[int, int, bool]:
         """Get parameters of the MAC algorithm used with this encryption"""
 
@@ -162,6 +168,12 @@
         return cls(GCMCipher(cipher_name, key, iv))
 
     @classmethod
+    def needs_mac(cls) -> bool:
+        """GCM encryption doesn't need an external MAC algorithm"""
+
+        return False
+
+    @classmethod
     def get_mac_params(cls, mac_alg: bytes) -> Tuple[int, int, bool]:
         """Get parameters of the MAC algorithm used with this encryption"""
 
@@ -201,6 +213,12 @@
         return cls(ChachaCipher(key))
 
     @classmethod
+    def needs_mac(cls) -> bool:
+        """Chacha20 encryption doesn't need an external MAC algorithm"""
+
+        return False
+
+    @classmethod
     def get_mac_params(cls, mac_alg: bytes) -> Tuple[int, int, bool]:
         """Get parameters of the MAC algorithm used with this encryption"""
 
@@ -258,8 +276,14 @@
     return _default_enc_algs
 
 
-def get_encryption_params(enc_alg: bytes,
-                          mac_alg: bytes = b'') -> _EncParams:
+def encryption_needs_mac(enc_alg: bytes) -> bool:
+    """Return whether an encryption algorithm needs a MAC algorithm"""
+
+    encryption, _ = _enc_params[enc_alg]
+    return encryption.needs_mac()
+
+
+def get_encryption_params(enc_alg: bytes, mac_alg: bytes = b'') -> _EncParams:
     """Get parameters of an encryption and MAC algorithm"""
 
     encryption, cipher_name = _enc_params[enc_alg]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/kex.py 
new/asyncssh-2.23.1/asyncssh/kex.py
--- old/asyncssh-2.22.0/asyncssh/kex.py 2024-10-26 18:20:18.000000000 +0200
+++ new/asyncssh-2.23.1/asyncssh/kex.py 2026-05-09 04:30:40.000000000 +0200
@@ -35,7 +35,7 @@
 
 
 _KexAlgList = List[bytes]
-_KexAlgMap = Dict[bytes, Tuple[Type['Kex'], HashType, object]]
+_KexAlgMap = Dict[bytes, Tuple[Type['Kex'], HashType, Tuple]]
 
 
 _kex_algs: _KexAlgList = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/misc.py 
new/asyncssh-2.23.1/asyncssh/misc.py
--- old/asyncssh-2.22.0/asyncssh/misc.py        2025-09-28 15:31:10.000000000 
+0200
+++ new/asyncssh-2.23.1/asyncssh/misc.py        2026-06-06 18:01:56.000000000 
+0200
@@ -251,7 +251,7 @@
 
     if addrinfo[0] == socket.AF_INET6:
         sa = addrinfo[4]
-        addr = sa[0]
+        addr = cast(str, sa[0])
 
         idx = addr.find('%')
         if idx >= 0: # pragma: no cover
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/scp.py 
new/asyncssh-2.23.1/asyncssh/scp.py
--- old/asyncssh-2.22.0/asyncssh/scp.py 2025-09-28 15:31:10.000000000 +0200
+++ new/asyncssh-2.23.1/asyncssh/scp.py 2026-06-07 16:00:27.000000000 +0200
@@ -136,6 +136,10 @@
 
     try:
         permissions, size, name = args.split(None, 2)
+
+        if b'/' in name or b'\\' in name or name == b'..':
+            raise _scp_error(SFTPBadMessage, 'Invalid filename')
+
         return int(permissions, 8), int(size), name
     except ValueError:
         raise _scp_error(SFTPBadMessage,
@@ -346,7 +350,8 @@
 
         if isinstance(exc, SFTPError):
             reason = exc.reason.encode('utf-8')
-        elif isinstance(exc, OSError): # pragma: no branch (win32)
+        elif isinstance(exc, OSError) and \
+                exc.strerror: # pragma: no branch (win32)
             reason = exc.strerror.encode('utf-8')
 
             filename = cast(BytesOrStr, exc.filename)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/server.py 
new/asyncssh-2.23.1/asyncssh/server.py
--- old/asyncssh-2.22.0/asyncssh/server.py      2025-05-03 04:57:00.000000000 
+0200
+++ new/asyncssh-2.23.1/asyncssh/server.py      2026-05-09 04:30:40.000000000 
+0200
@@ -854,6 +854,10 @@
                      * An :class:`SSHListener` object
                      * `True` to set up standard port forwarding
                      * `False` to reject the request
+                     * A callable to use as an accept handler, taking
+                       arguments of the original host and port of the
+                       client and returning a boolean to indicate
+                       whether or not to allow connection forwarding.
                      * A coroutine object which returns one of the above
 
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/sftp.py 
new/asyncssh-2.23.1/asyncssh/sftp.py
--- old/asyncssh-2.22.0/asyncssh/sftp.py        2025-12-21 23:41:04.000000000 
+0100
+++ new/asyncssh-2.23.1/asyncssh/sftp.py        2026-06-07 16:00:27.000000000 
+0200
@@ -145,6 +145,7 @@
 else:
     _LocalPath = bytes
 
+_SFTPExtensions = Sequence[Tuple[bytes, bytes]]
 _SFTPFileObj = IO[bytes]
 _SFTPPath = Union[bytes, FilePath]
 _SFTPPaths = Union[_SFTPPath, Sequence[_SFTPPath]]
@@ -1718,7 +1719,7 @@
     untrans_name: Optional[bytes]
     extended: Sequence[Tuple[bytes, bytes]] = ()
 
-    def _format_ns(self, k: str):
+    def _format_ns(self, k: str) -> str:
         """Convert epoch seconds & nanoseconds to a string date & time"""
 
         result = time.ctime(getattr(self, k))
@@ -2241,7 +2242,7 @@
 class SFTPGlob:
     """SFTP glob matcher"""
 
-    def __init__(self, fs: _SFTPGlobProtocol, multiple=False):
+    def __init__(self, fs: _SFTPGlobProtocol, multiple: bool = False):
         self._fs = fs
         self._multiple = multiple
         self._prev_matches: Set[bytes] = set()
@@ -2280,7 +2281,7 @@
 
         return path, patlist
 
-    def _report_match(self, path, attrs):
+    def _report_match(self, path: bytes, attrs: SFTPAttrs) -> None:
         """Report a matching name"""
 
         self._matched = True
@@ -2293,7 +2294,7 @@
 
         self._new_matches.append(SFTPName(path, attrs=attrs))
 
-    async def _stat(self, path) -> Optional[SFTPAttrs]:
+    async def _stat(self, path: bytes) -> Optional[SFTPAttrs]:
         """Cache results of calls to stat"""
 
         try:
@@ -2309,7 +2310,7 @@
         self._stat_cache[path] = attrs
         return attrs
 
-    async def _scandir(self, path) -> AsyncIterator[SFTPName]:
+    async def _scandir(self, path: bytes) -> AsyncIterator[SFTPName]:
         """Cache results of calls to scandir"""
 
         try:
@@ -2392,7 +2393,7 @@
 
     async def match(self, pattern: bytes,
                     error_handler: SFTPErrorHandler = None,
-                    sftp_version = MIN_SFTP_VERSION) -> Sequence[SFTPName]:
+                    sftp_version: int = MIN_SFTP_VERSION) -> 
Sequence[SFTPName]:
         """Match against a glob pattern"""
 
         self._new_matches = []
@@ -2476,7 +2477,7 @@
             self._reader = None
             self._writer = None
 
-    def _log_extensions(self, extensions: Sequence[Tuple[bytes, bytes]]):
+    def _log_extensions(self, extensions: _SFTPExtensions) -> None:
         """Dump a formatted list of extensions to the debug log"""
 
         for name, data in extensions:
@@ -3628,7 +3629,7 @@
 
         return self._offset
 
-    async def stat(self, flags = FILEXFER_ATTR_DEFINED_V4) -> SFTPAttrs:
+    async def stat(self, flags: int = FILEXFER_ATTR_DEFINED_V4) -> SFTPAttrs:
         """Return file attributes of the remote file
 
            This method queries file attributes of the currently open file.
@@ -5737,6 +5738,14 @@
                     else:
                         raise
 
+            names[0].filename =  self.decode(cast(bytes, names[0].filename),
+                                             isinstance(path, (str, PurePath)))
+
+            if names[0].longname is not None:
+                names[0].longname =  \
+                    self.decode(cast(bytes, names[0].longname),
+                                isinstance(path, (str, PurePath)))
+
             return names[0]
         else:
             return self.decode(cast(bytes, names[0].filename),
@@ -7000,6 +7009,14 @@
        tree. This will also affect path names returned by the
        :meth:`realpath` and :meth:`readlink` methods.
 
+
+           .. note:: AsyncSSH prevents creation of links to files outside
+                     of the selected chroot directory. However, pre-existing
+                     links inside the chroot which point outside of it will
+                     be followed. To completely isolate access to only the
+                     specified chroot, pre-existing links like this should
+                     be avoided.
+
     """
 
     # The default implementation of a number of these methods don't need self
@@ -7289,10 +7306,8 @@
         if pflags & FXF_EXCL:
             flags |= os.O_EXCL
 
-        try:
-            flags |= os.O_BINARY
-        except AttributeError: # pragma: no cover
-            pass
+        if sys.platform == 'win32': # pragma: no cover
+            flags |= os.O_BINARY # pylint: disable=no-member
 
         perms = 0o666 if attrs.permissions is None else attrs.permissions
 
@@ -7392,10 +7407,8 @@
                 desired_access & ACE4_WRITE_DATA:
             mode += '+'
 
-        try:
-            open_flags |= os.O_BINARY
-        except AttributeError: # pragma: no cover
-            pass
+        if sys.platform == 'win32': # pragma: no cover
+            open_flags |= os.O_BINARY # pylint: disable=no-member
 
         perms = 0o666 if attrs.permissions is None else attrs.permissions
 
@@ -7604,7 +7617,7 @@
             # information.
 
             # pylint: disable=no-member
-            listdir_result = self.listdir(path) # type: ignore
+            listdir_result = self.listdir(path)
 
             if inspect.isawaitable(listdir_result):
                 listdir_result = await cast(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/sk.py 
new/asyncssh-2.23.1/asyncssh/sk.py
--- old/asyncssh-2.22.0/asyncssh/sk.py  2025-12-21 23:41:04.000000000 +0100
+++ new/asyncssh-2.23.1/asyncssh/sk.py  2026-05-09 04:30:40.000000000 +0200
@@ -359,6 +359,6 @@
     sk_use_webauthn = WindowsClient.is_available() and \
                       hasattr(ctypes, 'windll') and \
                       not ctypes.windll.shell32.IsUserAnAdmin()
-except ImportError:
+except (AttributeError, ImportError):
     WindowsClient = None # type: ignore
     sk_use_webauthn = False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/version.py 
new/asyncssh-2.23.1/asyncssh/version.py
--- old/asyncssh-2.22.0/asyncssh/version.py     2025-12-21 23:47:20.000000000 
+0100
+++ new/asyncssh-2.23.1/asyncssh/version.py     2026-06-07 16:05:19.000000000 
+0200
@@ -26,4 +26,4 @@
 
 __url__ = 'http://asyncssh.timeheart.net'
 
-__version__ = '2.22.0'
+__version__ = '2.23.1'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh/x11.py 
new/asyncssh-2.23.1/asyncssh/x11.py
--- old/asyncssh-2.22.0/asyncssh/x11.py 2024-10-26 18:20:18.000000000 +0200
+++ new/asyncssh-2.23.1/asyncssh/x11.py 2026-05-09 04:30:40.000000000 +0200
@@ -491,19 +491,21 @@
     if not new_file:
         raise ValueError('Unable to acquire Xauthority lock')
 
-    new_entry = SSHXAuthorityEntry(XAUTH_FAMILY_HOSTNAME, host,
-                                   dpynum, auth_proto, auth_data)
+    with new_file:
+        new_entry = SSHXAuthorityEntry(XAUTH_FAMILY_HOSTNAME, host,
+                                       dpynum, auth_proto, auth_data)
 
-    new_file.write(bytes(new_entry))
+        new_file.write(bytes(new_entry))
 
-    for entry in walk_xauth(auth_path):
-        if (entry.family != new_entry.family or entry.addr != new_entry.addr or
-                entry.dpynum != new_entry.dpynum):
-            new_file.write(bytes(entry))
+        for entry in walk_xauth(auth_path):
+            if (entry.family != new_entry.family or
+                    entry.addr != new_entry.addr or
+                    entry.dpynum != new_entry.dpynum):
+                new_file.write(bytes(entry))
 
-    new_file.close()
+        new_file.close()
 
-    os.replace(new_auth_path, auth_path)
+        os.replace(new_auth_path, auth_path)
 
 
 async def create_x11_client_listener(loop: asyncio.AbstractEventLoop,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh.egg-info/PKG-INFO 
new/asyncssh-2.23.1/asyncssh.egg-info/PKG-INFO
--- old/asyncssh-2.22.0/asyncssh.egg-info/PKG-INFO      2025-12-22 
00:34:03.000000000 +0100
+++ new/asyncssh-2.23.1/asyncssh.egg-info/PKG-INFO      2026-06-07 
16:10:42.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: asyncssh
-Version: 2.22.0
+Version: 2.23.1
 Summary: AsyncSSH: Asynchronous SSHv2 client and server library
 Author-email: Ron Frederick <[email protected]>
 License: EPL-2.0 OR GPL-2.0-or-later
@@ -32,10 +32,10 @@
 Requires-Dist: bcrypt>=3.1.3; extra == "bcrypt"
 Provides-Extra: fido2
 Requires-Dist: fido2>=2; extra == "fido2"
+Provides-Extra: ifaddr
+Requires-Dist: ifaddr>=0.2.0; extra == "ifaddr"
 Provides-Extra: gssapi
 Requires-Dist: gssapi>=1.2.0; extra == "gssapi"
-Provides-Extra: libnacl
-Requires-Dist: libnacl>=1.4.2; extra == "libnacl"
 Provides-Extra: pkcs11
 Requires-Dist: python-pkcs11>=0.7.0; extra == "pkcs11"
 Provides-Extra: pyopenssl
@@ -196,6 +196,9 @@
 * Install fido2 from https://pypi.org/project/fido2 if you want support
   for key exchange and authentication with U2F/FIDO2 security keys.
 
+* Install ifaddr from https://pypi.org/project/ifaddr/ if you want
+  support for matching on local network IP addresses.
+
 * Install python-pkcs11 from https://pypi.org/project/python-pkcs11 if
   you want support for accessing PIV keys on PKCS#11 security tokens.
 
@@ -221,24 +224,25 @@
 
   | bcrypt
   | fido2
+  | ifaddr
   | gssapi
   | pkcs11
   | pyOpenSSL
   | pywin32
 
-For example, to install bcrypt, fido2, gssapi, pkcs11, and pyOpenSSL
+For example, to install bcrypt, fido2, gssapi, ifaddr, pkcs11, and pyOpenSSL
 on UNIX, you can run:
 
   ::
 
-    pip install 'asyncssh[bcrypt,fido2,gssapi,pkcs11,pyOpenSSL]'
+    pip install 'asyncssh[bcrypt,fido2,gssapi,ifaddr,pkcs11,pyOpenSSL]'
 
-To install bcrypt, fido2, pkcs11, pyOpenSSL, and pywin32 on Windows,
+To install bcrypt, fido2, ifaddr, pkcs11, pyOpenSSL, and pywin32 on Windows,
 you can run:
 
   ::
 
-    pip install 'asyncssh[bcrypt,fido2,pkcs11,pyOpenSSL,pywin32]'
+    pip install 'asyncssh[bcrypt,fido2,ifaddr,pkcs11,pyOpenSSL,pywin32]'
 
 Note that you will still need to manually install the libnettle library
 for UMAC support. Unfortunately, since liboqs and libnettle are not
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/asyncssh.egg-info/requires.txt 
new/asyncssh-2.23.1/asyncssh.egg-info/requires.txt
--- old/asyncssh-2.22.0/asyncssh.egg-info/requires.txt  2025-12-22 
00:34:03.000000000 +0100
+++ new/asyncssh-2.23.1/asyncssh.egg-info/requires.txt  2026-06-07 
16:10:42.000000000 +0200
@@ -10,8 +10,8 @@
 [gssapi]
 gssapi>=1.2.0
 
-[libnacl]
-libnacl>=1.4.2
+[ifaddr]
+ifaddr>=0.2.0
 
 [pkcs11]
 python-pkcs11>=0.7.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/docs/api.rst 
new/asyncssh-2.23.1/docs/api.rst
--- old/asyncssh-2.22.0/docs/api.rst    2025-05-03 05:11:23.000000000 +0200
+++ new/asyncssh-2.23.1/docs/api.rst    2026-05-09 04:30:40.000000000 +0200
@@ -1256,10 +1256,7 @@
 
 AsyncSSH has extensive public key and certificate support.
 
-Supported public key types include DSA, RSA, and ECDSA. In addition, Ed25519
-and Ed448 keys are supported if OpenSSL 1.1.1b or later is installed.
-Alternately, Ed25519 support is available when the libnacl package and
-libsodium library are installed.
+Supported public key types include DSA, RSA, ECDSA, Ed25519, and Ed448.
 
 Supported certificate types include OpenSSH version 01 certificates for
 DSA, RSA, ECDSA, Ed25519, and Ed448 keys and X.509 certificates for DSA,
@@ -2078,10 +2075,6 @@
 GSS authentication support is only available when the gssapi package is
 installed on UNIX or the pywin32 package is installed on Windows.
 
-Curve25519 and Curve448 support is available when OpenSSL 1.1.1 or
-later is installed. Alternately, Curve25519 is available when the
-libnacl package and libsodium library are installed.
-
 SNTRUP support is available when the Open Quantum Safe (liboqs)
 dynamic library is installed.
 
@@ -2115,9 +2108,6 @@
   | arcfour128
   | arcfour
 
-Chacha20-Poly1305 support is available when either OpenSSL 1.1.1b or later
-or the libnacl package and libsodium library are installed.
-
 .. index:: MAC algorithms
 .. _MACAlgs:
 
@@ -2266,10 +2256,6 @@
   | ssh-dss-cert-v01\@openssh.com
   | ssh-dss
 
-Ed25519 and Ed448 support is available when OpenSSL 1.1.1b or later is
-installed. Alternately, Ed25519 is available when the libnacl package
-and libsodium library are installed.
-
 .. index:: Constants
 .. _Constants:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/docs/changes.rst 
new/asyncssh-2.23.1/docs/changes.rst
--- old/asyncssh-2.22.0/docs/changes.rst        2025-12-22 00:21:19.000000000 
+0100
+++ new/asyncssh-2.23.1/docs/changes.rst        2026-06-07 16:05:19.000000000 
+0200
@@ -3,6 +3,60 @@
 Change Log
 ==========
 
+Release 2.23.1 (6 Jun 2026)
+---------------------------
+
+* Fixed an SCP path traversal issue. Thanks go to Jaden Furtado for
+  reporting this issue.
+
+* Expanded previous fix to block unsafe user substitutions in server
+  config. Thanks go to GitHub user cesabici-bit for reporting this
+  issue.
+
+* Fixed default value for reuse_address and reuse_port, matching
+  the behaavior of asyncio.create_server(). Thanks go to Alexander
+  Shlemin for reporting the inconsistency.
+
+Release 2.23.0 (8 May 2026)
+---------------------------
+
+* Added support for "Match localnetwork". Thanks go to Théophile Bastian
+  for reporting this new match type, added in OpenSSH 9.4.
+
+* Enabled support for RSA with SHA-2 signatures in ssh-agent and Pageant.
+  Thanks go to GitHub user Netzvamp for reporting this.
+
+* Changed MAC algorithm negotation to be skipped when using AEAD ciphers.
+  Thanks go to GitHub user LilleCarl for reporting this issue and
+  suggesting a potential fix.
+
+* Improved graceful termination when using ProxyCommand, waiting for
+  the ProxyCommand tunnel to close when cleaning up a connection. Thanks
+  go to Simon Liétar for reporting this issue and helping to investigate
+  possible solutions.
+
+* Blocked unsafe user substitutions from being used in server config.
+  Thanks go to GitHub user 0xHunSec for reporting this problem and
+  providing reproduction code.
+
+* Fixed an issue with config evaluation when "Match final" was combined
+  with Hostname directives. Thanks go to GitHub user commonism for
+  reporting this issue and coming up with a reproducible test case and a
+  potential fix.
+
+* Fixed a resource leak in xauth support. Thanks go to GitHub user
+  taovinci0 for reporting this problem and providing an initial version
+  of a fix.
+
+* Fixed issue with multi-hop ProxyJump directives in a config file
+  not working correctly. Thanks go to Rémi Benoit for reporting this
+  problem and providing a detailed root cause analysis.
+
+* Fixed string encoding in SFTPName objects returned by realpath().
+  Thanks go to GitHub user vivodi for reporting this and providing
+  reproduction code.
+
+
 Release 2.22.0 (21 Dec 2025)
 ----------------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/pyproject.toml 
new/asyncssh-2.23.1/pyproject.toml
--- old/asyncssh-2.22.0/pyproject.toml  2025-12-21 23:41:04.000000000 +0100
+++ new/asyncssh-2.23.1/pyproject.toml  2026-05-09 04:30:40.000000000 +0200
@@ -35,8 +35,8 @@
 [project.optional-dependencies]
 bcrypt    = ['bcrypt >= 3.1.3']
 fido2     = ['fido2 >= 2']
+ifaddr    = ['ifaddr >= 0.2.0']
 gssapi    = ['gssapi >= 1.2.0']
-libnacl   = ['libnacl >= 1.4.2']
 pkcs11    = ['python-pkcs11 >= 0.7.0']
 pyOpenSSL = ['pyOpenSSL >= 23.0.0']
 pywin32   = ['pywin32 >= 227']
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/tests/test_config.py 
new/asyncssh-2.23.1/tests/test_config.py
--- old/asyncssh-2.22.0/tests/test_config.py    2025-12-21 23:41:04.000000000 
+0100
+++ new/asyncssh-2.23.1/tests/test_config.py    2026-06-07 16:00:27.000000000 
+0200
@@ -1,4 +1,4 @@
-# Copyright (c) 2020-2024 by Ron Frederick <[email protected]> and others.
+# Copyright (c) 2020-2026 by Ron Frederick <[email protected]> and others.
 #
 # This program and the accompanying materials are made available under
 # the terms of the Eclipse Public License v2.0 which accompanies this
@@ -388,6 +388,19 @@
         self.assertEqual(config.get('BindAddress'), 'addr')
         self.assertEqual(config.get('Port'), 2222)
 
+    def test_match_local_network(self):
+        """Test matching against a local network"""
+
+        config = self._parse_config('Match localnetwork 127.0.0.1\n'
+                                    '  Hostname newhost\n'
+                                    'Match localnetwork 0.0.0.0\n'
+                                    '  Port 1111\n'
+                                    'Match localnetwork ::1\n'
+                                    '  Port 2222')
+
+        self.assertEqual(config.get('Hostname'), 'newhost')
+        self.assertEqual(config.get('Port'), 2222)
+
     def test_host_key_alias(self):
         """Test setting HostKeyAlias"""
 
@@ -534,9 +547,10 @@
     def test_env_expansion(self):
         """Test environment variable expansion"""
 
-        config = self._parse_config('RemoteCommand ${HOME}/.ssh')
+        config = self._parse_config(
+            'RemoteCommand ${HOME}/${USERPROFILE}/.ssh')
 
-        self.assertEqual(config.get('RemoteCommand'), './.ssh')
+        self.assertEqual(config.get('RemoteCommand'), '././.ssh')
 
     def test_invalid_env_expansion(self):
         """Test invalid environment variable expansion"""
@@ -585,6 +599,16 @@
         config = self._parse_config('Match address 127.0.0.0/8\nPermitTTY no')
         self.assertEqual(config.get('PermitTTY'), False)
 
+    def test_unsafe_user(self):
+        """Test unsafe characters in username"""
+
+        for user in ('xxx..yyy', 'xxx~yyy', 'Cxxx:'):
+            self._parse_config('AuthorizedKeysFile %u', user=user)
+
+        for user in ('..', '~xxx', 'C:xxx', '/xxx', '\\xxx', '${xxx}'):
+            with self.assertRaises(asyncssh.IllegalUserName):
+                self._parse_config('AuthorizedKeysFile %u', user=user)
+
     def test_reload(self):
         """Test update of match options"""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/tests/test_connection.py 
new/asyncssh-2.23.1/tests/test_connection.py
--- old/asyncssh-2.22.0/tests/test_connection.py        2025-12-21 
23:41:04.000000000 +0100
+++ new/asyncssh-2.23.1/tests/test_connection.py        2026-05-09 
04:30:40.000000000 +0200
@@ -1133,8 +1133,17 @@
     async def test_empty_mac_algs(self):
         """Test connecting with an empty list of MAC algorithms"""
 
-        with self.assertRaises(ValueError):
-            await self.connect(mac_algs=[])
+        with self.assertRaises(asyncssh.KeyExchangeFailed):
+            await self.connect(encryption_algs=['aes128-ctr'], mac_algs=[])
+
+    @asynctest
+    async def test_empty_mac_algs_with_aead_cipher(self):
+        """Test connecting with an empty list of MAC algs with AEAD cipher"""
+
+        async with self.connect(
+                encryption_algs=['[email protected]'],
+                mac_algs=[]):
+            pass
 
     @asynctest
     async def test_invalid_mac_alg(self):
@@ -1935,6 +1944,25 @@
                 pass
 
 
+class _TestConnectionEmptyServerMACAlgs(ServerTestCase):
+    """Unit test server with empty list of MAC algs and AEAD cipher"""
+
+    @classmethod
+    async def start_server(cls):
+        """Start an SSH server to connect to"""
+
+        return await cls.create_server(
+            _TunnelServer, encryption_algs=['[email protected]'],
+            mac_algs=[])
+
+    @asynctest
+    async def test_connect(self):
+        """Test connecting to server advertizing empty MAC algs"""
+
+        async with self.connect():
+            pass
+
+
 class _TestConnectionAsyncAcceptor(ServerTestCase):
     """Unit test for async acceptor"""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/tests/test_forward.py 
new/asyncssh-2.23.1/tests/test_forward.py
--- old/asyncssh-2.22.0/tests/test_forward.py   2025-12-21 23:41:04.000000000 
+0100
+++ new/asyncssh-2.23.1/tests/test_forward.py   2026-05-09 04:30:40.000000000 
+0200
@@ -436,6 +436,30 @@
             os.remove('.ssh/config')
 
     @asynctest
+    async def test_proxy_jump_config(self):
+        """Test ProxyJump to a host with config options"""
+
+        jump_host = await self.create_server(
+            _TCPConnectionServer, authorized_client_keys='authorized_keys')
+        jump_port = jump_host.get_port()
+
+        try:
+            write_file('.ssh/config',
+                       'Host jumphost\n'
+                       '  Hostname localhost\n'
+                       f'  Port {jump_port}\n'
+                       'Match final host  target\n'
+                       '  Hostname localhost\n'
+                       f'  Port {self._server_port}\n'
+                       '  ProxyJump jumphost\n', 'w')
+
+            async with self.connect(host='target', username='ckey'):
+                pass
+        finally:
+            jump_host.close()
+            os.remove('.ssh/config')
+
+    @asynctest
     async def test_ssh_connect_reverse_tunnel(self):
         """Test creating a tunneled reverse direction SSH connection"""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/tests/test_kex.py 
new/asyncssh-2.23.1/tests/test_kex.py
--- old/asyncssh-2.22.0/tests/test_kex.py       2025-12-21 23:41:04.000000000 
+0100
+++ new/asyncssh-2.23.1/tests/test_kex.py       2026-05-09 04:30:40.000000000 
+0200
@@ -650,3 +650,9 @@
 
         client_conn.close()
         server_conn.close()
+
+    def test_invalid_pq_alg(self):
+        """Test providing an invalid PQ algorithm"""
+
+        with self.assertRaisesRegex(ValueError, 'Unknown PQ algorithm 
invalid'):
+            PQDH(b'invalid')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/tests/test_process.py 
new/asyncssh-2.23.1/tests/test_process.py
--- old/asyncssh-2.22.0/tests/test_process.py   2024-10-26 18:20:18.000000000 
+0200
+++ new/asyncssh-2.23.1/tests/test_process.py   2026-05-09 04:30:40.000000000 
+0200
@@ -1210,10 +1210,9 @@
         with open('stdin', 'w') as file:
             file.write(data)
 
-        file = await aiofiles.open('stdin', 'r')
-
-        async with self.connect() as conn:
-            result = await conn.run('echo', stdin=file)
+        async with aiofiles.open('stdin', 'r') as file:
+            async with self.connect() as conn:
+                result = await conn.run('echo', stdin=file)
 
         self.assertEqual(result.stdout, data)
         self.assertEqual(result.stderr, data)
@@ -1227,10 +1226,9 @@
         with open('stdin', 'wb') as file:
             file.write(data)
 
-        file = await aiofiles.open('stdin', 'rb')
-
-        async with self.connect() as conn:
-            result = await conn.run('echo', stdin=file, encoding=None)
+        async with aiofiles.open('stdin', 'rb') as file:
+            async with self.connect() as conn:
+                result = await conn.run('echo', stdin=file, encoding=None)
 
         self.assertEqual(result.stdout, data)
         self.assertEqual(result.stderr, data)
@@ -1241,10 +1239,9 @@
 
         data = str(id(self))
 
-        file = await aiofiles.open('stdout', 'w')
-
-        async with self.connect() as conn:
-            result = await conn.run('echo', input=data, stdout=file)
+        async with aiofiles.open('stdout', 'w') as file:
+            async with self.connect() as conn:
+                result = await conn.run('echo', input=data, stdout=file)
 
         with open('stdout') as file:
             stdout_data = file.read()
@@ -1275,11 +1272,10 @@
 
         data = str(id(self)).encode() + b'\xff'
 
-        file = await aiofiles.open('stdout', 'wb')
-
-        async with self.connect() as conn:
-            result = await conn.run('echo', input=data, stdout=file,
-                                    encoding=None)
+        async with aiofiles.open('stdout', 'wb') as file:
+            async with self.connect() as conn:
+                result = await conn.run('echo', input=data, stdout=file,
+                                        encoding=None)
 
         with open('stdout', 'rb') as file:
             stdout_data = file.read()
@@ -1297,11 +1293,10 @@
         with open('stdin', 'w') as file:
             file.write(data)
 
-        file = await aiofiles.open('stdin', 'r')
-
-        async with self.connect() as conn:
-            result = await conn.run('delay', stdin=file,
-                                    stderr=asyncssh.DEVNULL)
+        async with aiofiles.open('stdin', 'r') as file:
+            async with self.connect() as conn:
+                result = await conn.run('delay', stdin=file,
+                                        stderr=asyncssh.DEVNULL)
 
         self.assertEqual(result.stdout, data)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/tests/test_sftp.py 
new/asyncssh-2.23.1/tests/test_sftp.py
--- old/asyncssh-2.22.0/tests/test_sftp.py      2025-12-21 23:41:04.000000000 
+0100
+++ new/asyncssh-2.23.1/tests/test_sftp.py      2026-06-07 16:00:27.000000000 
+0200
@@ -5737,6 +5737,9 @@
 
                 if command.endswith('get_connection_lost'):
                     pass
+                elif command.endswith('get_invalid_filename_response'):
+                    await process.stdin.read(1)
+                    process.stdout.write('C0644 0 ../src\n')
                 elif command.endswith('get_dir_no_recurse'):
                     await process.stdin.read(1)
                     process.stdout.write('D0755 0 src\n')
@@ -5773,6 +5776,17 @@
         return await cls.create_server(process_factory=_handle_client)
 
     @asynctest
+    async def test_get_invalid_filename_response(self):
+        """Test receiving directory when recurse wasn't requested"""
+
+        try:
+            with self.assertRaises((SFTPBadMessage, SFTPConnectionLost)):
+                await scp((self._scp_server, 'get_invalid_filename_response'),
+                          'dst')
+        finally:
+            remove('dst')
+
+    @asynctest
     async def test_get_directory_without_recurse(self):
         """Test receiving directory when recurse wasn't requested"""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.22.0/tox.ini new/asyncssh-2.23.1/tox.ini
--- old/asyncssh-2.22.0/tox.ini 2025-12-21 23:41:04.000000000 +0100
+++ new/asyncssh-2.23.1/tox.ini 2026-05-09 04:30:40.000000000 +0200
@@ -11,7 +11,7 @@
     aiofiles>=0.6.0
     bcrypt>=3.1.3
     fido2>=2
-    libnacl>=1.4.2
+    ifaddr>=0.2.0
     pyOpenSSL>=23.0.0
     pytest>=7.0.1
     pytest-cov>=3.0.0

Reply via email to