Skip to content

Commit 9a3d13f

Browse files
committed
Update hostname canonicalization to better match OpenSSH
This commit changes the initial hostname check in host canonicalization to only look for literal IP addresses, rather than attempting to resolve the host as if it were already fully qualified. This is more consistent with OpenSSH and also should avoid a potentially slow additional name lookup on every connection.
1 parent f3d4176 commit 9a3d13f

File tree

3 files changed

+36
-32
lines changed

3 files changed

+36
-32
lines changed

asyncssh/connection.py

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import getpass
2626
import inspect
2727
import io
28+
import ipaddress
2829
import os
2930
import shlex
3031
import socket
@@ -274,44 +275,42 @@ async def create_server(self, session_factory: TCPListenerFactory,
274275
_DEFAULT_MAX_LINE_LENGTH = 1024 # 1024 characters
275276

276277

277-
async def _resolve_host(host, loop: asyncio.AbstractEventLoop) -> Optional[str]:
278-
"""Attempt to resolve a hostname, returning a canonical name"""
279-
280-
try:
281-
addrinfo = await loop.getaddrinfo(host + '.', 0,
282-
flags=socket.AI_CANONNAME)
283-
except socket.gaierror:
284-
return None
285-
else:
286-
return addrinfo[0][3]
287-
288-
289278
async def _canonicalize_host(loop: asyncio.AbstractEventLoop,
290279
options: 'SSHConnectionOptions') -> Optional[str]:
291280
"""Canonicalize a host name"""
292281

293282
host = options.host
294283

295284
if not options.canonicalize_hostname or not options.canonical_domains or \
296-
host.count('.') > options.canonicalize_max_dots or \
297-
(await _resolve_host(host, loop)):
285+
host.count('.') > options.canonicalize_max_dots:
286+
return None
287+
288+
try:
289+
ipaddress.ip_address(host)
290+
except ValueError:
291+
pass
292+
else:
298293
return None
299294

300295
for domain in options.canonical_domains:
301296
canon_host = f'{host}.{domain}'
302-
cname = await _resolve_host(canon_host, loop)
303297

304-
if cname is not None:
305-
if cname:
306-
for patterns in options.canonicalize_permitted_cnames:
307-
host_pat, cname_pat = map(WildcardPatternList, patterns)
298+
try:
299+
addrinfo = await loop.getaddrinfo(
300+
canon_host, 0, flags=socket.AI_CANONNAME)
301+
except socket.gaierror:
302+
continue
303+
304+
cname = addrinfo[0][3]
305+
306+
if cname:
307+
for patterns in options.canonicalize_permitted_cnames:
308+
host_pat, cname_pat = map(WildcardPatternList, patterns)
308309

309-
if host_pat.matches(canon_host) and \
310-
cname_pat.matches(cname):
311-
canon_host = cname
312-
break
310+
if host_pat.matches(canon_host) and cname_pat.matches(cname):
311+
return cname
313312

314-
return canon_host
313+
return canon_host
315314

316315
if not options.canonicalize_fallback_local:
317316
raise OSError(f'Unable to canonicalize hostname "{host}"')

tests/test_connection.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2688,18 +2688,27 @@ async def start_server(cls):
26882688
async def test_canonicalize(self):
26892689
"""Test hostname canonicalization"""
26902690

2691-
async with self.connect('testhost', known_hosts=(['skey.pub'], [], []),
2691+
async with self.connect('testhost', known_hosts=None,
26922692
canonicalize_hostname=True,
26932693
canonical_domains=['test']) as conn:
26942694
self.assertEqual(conn.get_extra_info('host'), 'testhost.test')
26952695

2696+
@asynctest
2697+
async def test_canonicalize_ip_address(self):
2698+
"""Test hostname canonicalization with IP address"""
2699+
2700+
async with self.connect('127.0.0.1', known_hosts=None,
2701+
canonicalize_hostname=True,
2702+
canonicalize_max_dots=3,
2703+
canonical_domains=['test']) as conn:
2704+
self.assertEqual(conn.get_extra_info('host'), '127.0.0.1')
2705+
26962706
@asynctest
26972707
async def test_canonicalize_proxy(self):
26982708
"""Test hostname canonicalization with proxy"""
26992709

27002710
with open('config', 'w') as f:
2701-
f.write('UserKnownHostsFile none\n'
2702-
'Match host localhost\nPubkeyAuthentication no')
2711+
f.write('UserKnownHostsFile none\n')
27032712

27042713
async with self.connect('testhost', config='config',
27052714
tunnel=f'localhost:{self._server_port}',
@@ -2712,8 +2721,7 @@ async def test_canonicalize_always(self):
27122721
"""Test hostname canonicalization for all connections"""
27132722

27142723
with open('config', 'w') as f:
2715-
f.write('UserKnownHostsFile none\n'
2716-
'Match host localhost\nPubkeyAuthentication no')
2724+
f.write('UserKnownHostsFile none\n')
27172725

27182726
async with self.connect('testhost', config='config',
27192727
tunnel=f'localhost:{self._server_port}',

tests/util.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,6 @@ def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
110110

111111
# pylint: disable=unused-argument
112112

113-
if host.endswith('.'):
114-
host = host[:-1]
115-
116113
try:
117114
return [(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP,
118115
hosts[host], ('127.0.0.1', port))]

0 commit comments

Comments
 (0)