eping 4.2__tar.gz → 4.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eping
3
- Version: 4.2
3
+ Version: 4.5
4
4
  Summary: Continuous ICMP reachability monitor built on fping, with a CSV logfile analyser (epinga)
5
5
  Author-email: Ewald Jeitler <ewald@jeitler.cc>
6
6
  License: GPL-3.0-or-later
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Dynamic: license-file
19
19
 
20
- # eping.py 4.02
20
+ # eping.py 4.05
21
21
 
22
22
  Continuous ICMP reachability monitor built on `fping`. Scans a host list in a loop,
23
23
  reports each host UP/DOWN/NO-DNS, counts state changes. CLI (curses) or web GUI.
@@ -449,6 +449,7 @@ separated) · `-r` IP range (comma separated, shortened end)
449
449
  | `-ssh` | `[USER@]HOST[:PORT]` | off | run fping on a jump host via ssh (see *SSH source (JUMPHOST)*) |
450
450
  | `-sshkey` | file | agent | private key file for `-ssh` |
451
451
  | `-sshhk` | accept-new/ask/off | accept-new | host key check for `-ssh` |
452
+ | `-sshnr` | — | off | with `-ssh`: do not resolve names on the jump host (use the local resolver) |
452
453
  | `-ncf` | — | off | CSV: do not ping the CSV IP of a name without DNS record |
453
454
  | `-up` | — | 0 | learning phase: after N rounds show only hosts seen UP (view CURRENTLY-UP; all hosts are still pinged until SET REFERENCE) |
454
455
  | `-setref` | — | off | with `-up`: hosts seen UP become new reference list |
@@ -509,24 +510,31 @@ these rows automatically.
509
510
  ## SSH source (JUMPHOST)
510
511
 
511
512
  Run fping on a jump host instead of locally (system `ssh` only). Any Linux host with a working `fping`
512
- (e.g. a caretaker) will do; it resolves the names itself. Start: `-ssh [USER@]HOST[:PORT]` (+ `-sshkey FILE`, `-sshhk MODE`);
513
+ (e.g. a caretaker) will do; it resolves the names itself. Start: `-ssh [USER@]HOST[:PORT]` (+ `-sshkey FILE`, `-sshhk MODE`, `-sshnr`);
513
514
  the password or key passphrase is asked on the terminal at startup. At runtime: `J` in the CLI,
514
515
  JUMPHOST button in the web GUI (after ADV OPTIONS).
515
516
 
516
517
  ![JUMPHOST window](https://raw.githubusercontent.com/ewaldj/eping/main/docs/img/jumphost.png)
517
518
 
518
- - Window: HOST, PORT, USER, PASSWORD (key passphrase), HOST KEY, KEY FILE. TEST checks ssh, login and
519
+ - Window: HOST, PORT, USER, PASSWORD (key passphrase), HOST KEY, NAMES, KEY FILE. TEST checks ssh, login and
519
520
  fping on the jump host, CONNECT switches the scan source, USE LOCAL switches back. A missing local
520
521
  `ssh` is shown in the window.
521
522
  - HOST KEY: `accept-new` (default) - hosts seen for the first time are trusted automatically, a changed
522
523
  key is still refused; `ask` - an unknown key is shown with its fingerprint and must be accepted with
523
524
  TRUST KEY; `off` - no check at all (insecure).
525
+ - NAMES (default on): the DNS lookups eping does itself - GET NAMES, PREFER HOSTNAME / IP ADDRESS, IP ONLY -
526
+ run on the jump host over the same ssh connection (no second login), using `getent`, else `dig`,
527
+ `nslookup` or `host` there; TEST/CONNECT report which tool works. One batch per call, names are validated
528
+ and sent via stdin. Without a working tool, or with the option off (`-sshnr`), the local resolver is used
529
+ and GET NAMES is not available. The DNS caches are cleared on every source switch.
530
+ A name with only an AAAA record is pinged with `fping -6` there (A is preferred, as locally). Without a resolver on
531
+ the jump host, a name that fping reports as NO-DNS is tried with `-6` in the next round, alternating with `-4`; `-4` / `-6` force one family.
524
532
  - The switch is applied at the start of the next round; hosts, states and counters are kept.
525
533
  - Connection loss: results of the round are discarded, hosts keep their state, a banner is shown and
526
534
  eping reconnects. Logged as `#COMMENT#` rows (`JUMPHOST ... OFFLINE: <reason>`, `JUMPHOST ... ONLINE
527
535
  again`, `SOURCE SWITCH <old> -> <new>`), shown in the report's comment timeline.
528
536
  - Log column `SOURCE`: `local` or the jump host of each row.
529
- - Limits: GET NAMES and the CSV-IP fallback are not available over ssh; at most 4 ssh processes
537
+ - Limits: the CSV-IP fallback is not available over ssh; at most 4 ssh processes
530
538
  (2 on Windows); the web password transfer is plain HTTP - use `-bind 127.0.0.1` or a trusted network.
531
539
 
532
540
  ## Web GUI header
@@ -1,4 +1,4 @@
1
- # eping.py 4.02
1
+ # eping.py 4.05
2
2
 
3
3
  Continuous ICMP reachability monitor built on `fping`. Scans a host list in a loop,
4
4
  reports each host UP/DOWN/NO-DNS, counts state changes. CLI (curses) or web GUI.
@@ -430,6 +430,7 @@ separated) · `-r` IP range (comma separated, shortened end)
430
430
  | `-ssh` | `[USER@]HOST[:PORT]` | off | run fping on a jump host via ssh (see *SSH source (JUMPHOST)*) |
431
431
  | `-sshkey` | file | agent | private key file for `-ssh` |
432
432
  | `-sshhk` | accept-new/ask/off | accept-new | host key check for `-ssh` |
433
+ | `-sshnr` | — | off | with `-ssh`: do not resolve names on the jump host (use the local resolver) |
433
434
  | `-ncf` | — | off | CSV: do not ping the CSV IP of a name without DNS record |
434
435
  | `-up` | — | 0 | learning phase: after N rounds show only hosts seen UP (view CURRENTLY-UP; all hosts are still pinged until SET REFERENCE) |
435
436
  | `-setref` | — | off | with `-up`: hosts seen UP become new reference list |
@@ -490,24 +491,31 @@ these rows automatically.
490
491
  ## SSH source (JUMPHOST)
491
492
 
492
493
  Run fping on a jump host instead of locally (system `ssh` only). Any Linux host with a working `fping`
493
- (e.g. a caretaker) will do; it resolves the names itself. Start: `-ssh [USER@]HOST[:PORT]` (+ `-sshkey FILE`, `-sshhk MODE`);
494
+ (e.g. a caretaker) will do; it resolves the names itself. Start: `-ssh [USER@]HOST[:PORT]` (+ `-sshkey FILE`, `-sshhk MODE`, `-sshnr`);
494
495
  the password or key passphrase is asked on the terminal at startup. At runtime: `J` in the CLI,
495
496
  JUMPHOST button in the web GUI (after ADV OPTIONS).
496
497
 
497
498
  ![JUMPHOST window](https://raw.githubusercontent.com/ewaldj/eping/main/docs/img/jumphost.png)
498
499
 
499
- - Window: HOST, PORT, USER, PASSWORD (key passphrase), HOST KEY, KEY FILE. TEST checks ssh, login and
500
+ - Window: HOST, PORT, USER, PASSWORD (key passphrase), HOST KEY, NAMES, KEY FILE. TEST checks ssh, login and
500
501
  fping on the jump host, CONNECT switches the scan source, USE LOCAL switches back. A missing local
501
502
  `ssh` is shown in the window.
502
503
  - HOST KEY: `accept-new` (default) - hosts seen for the first time are trusted automatically, a changed
503
504
  key is still refused; `ask` - an unknown key is shown with its fingerprint and must be accepted with
504
505
  TRUST KEY; `off` - no check at all (insecure).
506
+ - NAMES (default on): the DNS lookups eping does itself - GET NAMES, PREFER HOSTNAME / IP ADDRESS, IP ONLY -
507
+ run on the jump host over the same ssh connection (no second login), using `getent`, else `dig`,
508
+ `nslookup` or `host` there; TEST/CONNECT report which tool works. One batch per call, names are validated
509
+ and sent via stdin. Without a working tool, or with the option off (`-sshnr`), the local resolver is used
510
+ and GET NAMES is not available. The DNS caches are cleared on every source switch.
511
+ A name with only an AAAA record is pinged with `fping -6` there (A is preferred, as locally). Without a resolver on
512
+ the jump host, a name that fping reports as NO-DNS is tried with `-6` in the next round, alternating with `-4`; `-4` / `-6` force one family.
505
513
  - The switch is applied at the start of the next round; hosts, states and counters are kept.
506
514
  - Connection loss: results of the round are discarded, hosts keep their state, a banner is shown and
507
515
  eping reconnects. Logged as `#COMMENT#` rows (`JUMPHOST ... OFFLINE: <reason>`, `JUMPHOST ... ONLINE
508
516
  again`, `SOURCE SWITCH <old> -> <new>`), shown in the report's comment timeline.
509
517
  - Log column `SOURCE`: `local` or the jump host of each row.
510
- - Limits: GET NAMES and the CSV-IP fallback are not available over ssh; at most 4 ssh processes
518
+ - Limits: the CSV-IP fallback is not available over ssh; at most 4 ssh processes
511
519
  (2 on Windows); the web password transfer is plain HTTP - use `-bind 127.0.0.1` or a trusted network.
512
520
 
513
521
  ## Web GUI header
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "eping"
7
- version = "4.02"
7
+ version = "4.05"
8
8
  description = "Continuous ICMP reachability monitor built on fping, with a CSV logfile analyser (epinga)"
9
9
  readme = "README.md"
10
10
  license = { text = "GPL-3.0-or-later" }
@@ -7,7 +7,7 @@
7
7
  # I knew how it worked.
8
8
  # Now, only god knows it!
9
9
  # - - - - - - - - - - - - - - - - - - - - - - - -
10
- VERSION = '4.02'
10
+ VERSION = '4.05'
11
11
  version = VERSION # legacy alias (kept for existing references)
12
12
 
13
13
  # --- scaling limits ---
@@ -545,13 +545,26 @@ _source_events = [] # pending comment texts (JUMPHOST OFFLINE / ONLINE, SO
545
545
  _ssh_pending = None # None = no change, False = switch to local, SshCfg = switch to it
546
546
  _ssh_lock = threading.Lock()
547
547
  _ssh_cfgs = [] # every SshCfg that owns a temp dir (cleaned at exit)
548
- _jump_last = {'host': '', 'port': '', 'user': '', 'key': '', 'hostkey': 'accept-new'} # form values of the last try (never the secret)
548
+ _jump_last = {'host': '', 'port': '', 'user': '', 'key': '', 'hostkey': 'accept-new', 'resolve': True} # form values of the last try (never the secret)
549
549
  _jump_hostkeys = {} # (host, port) -> hashed known_hosts lines offered by TRUST KEY
550
550
 
551
+ # Name resolution on the jump host: with a working resolver tool there (getent, dig,
552
+ # nslookup or host - found and probed by ssh_check) the DNS lookups that eping does
553
+ # itself (GET NAMES, PREFER HOSTNAME / IP ADDRESS, IP ONLY) run there, over the same ssh
554
+ # connection, instead of on this computer. Results are cached per source.
555
+ JR_TOOLS = ('getent', 'dig', 'nslookup', 'host') # in order of preference
556
+ JR_CHUNK = 400 # names per ssh call
557
+ _jr_fwd = {} # name -> (ips, expires_at)
558
+ _jr_rev = {} # ip -> (name or None, expires_at)
559
+ _jr_cache_lock = threading.Lock()
560
+ _jr_run_lock = threading.Lock() # one remote lookup batch at a time
561
+
551
562
  class SshCfg(object):
552
563
  """One jump host: where, how to log in, and its private temp dir (ControlPath + askpass)."""
553
- def __init__(self, host, user=None, port=None, key=None, secret='', hostkey='accept-new'):
564
+ def __init__(self, host, user=None, port=None, key=None, secret='', hostkey='accept-new', resolve=True):
554
565
  self.host, self.user, self.port = host, (user or None), (port or None)
566
+ self.resolve = bool(resolve) # wish: resolve names on the jump host
567
+ self.resolver = '' # tool that works there ('' = none / not checked)
555
568
  self.hostkey = hostkey if hostkey in SSH_HOSTKEY_MODES else 'accept-new'
556
569
  self.key = os.path.expanduser(key) if key else None
557
570
  self.secret = secret or ''
@@ -668,24 +681,161 @@ def ssh_wrap(argv, c=None):
668
681
  SSH_REMOTE_PATH, ' '.join(shlex.quote(a) for a in argv))
669
682
  return ssh_base(c) + [c.target, 'sh -c ' + shlex.quote(inner)]
670
683
 
671
- def ssh_exec(c, script, timeout=SSH_TEST_TIMEOUT):
672
- """Run a /bin/sh script on jump host c. Returns (returncode, output)."""
684
+ def ssh_exec(c, script, timeout=SSH_TEST_TIMEOUT, stdin_text=None):
685
+ """Run a /bin/sh script on jump host c (optionally feeding stdin_text). Returns (returncode, output)."""
673
686
  inner = 'PATH="$PATH:%s"; export PATH\n%s' % (SSH_REMOTE_PATH, script)
687
+ kw = {'input': stdin_text} if stdin_text is not None else {'stdin': subprocess.DEVNULL}
674
688
  try:
675
689
  p = subprocess.run(ssh_base(c) + [c.target, 'sh -c ' + shlex.quote(inner)],
676
- stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
677
- encoding='utf-8', errors='replace', timeout=timeout, env=ssh_env(c))
690
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
691
+ encoding='utf-8', errors='replace', timeout=timeout, env=ssh_env(c), **kw)
678
692
  return p.returncode, p.stdout or ''
679
693
  except FileNotFoundError:
680
694
  return 255, 'ssh command not found'
681
695
  except subprocess.TimeoutExpired:
682
696
  return 255, 'ssh timeout'
683
697
 
684
- _SSH_CHECK = r'''command -v fping >/dev/null 2>&1 || { echo EPING_NOFPING; exit 0; }
698
+ _SSH_CHECK = r'''RES=""; for t in getent dig nslookup host; do command -v $t >/dev/null 2>&1 && { RES=$t; break; }; done
699
+ echo "EPING_RESOLVER=$RES"
700
+ command -v fping >/dev/null 2>&1 || { echo EPING_NOFPING; exit 0; }
685
701
  echo "EPING_VERSION=$(fping -v 2>&1 | head -1)"
686
702
  out=$(fping -4 -c1 -t500 127.0.0.1 2>&1); echo "EPING_RC=$?"; echo "EPING_OUT=$out"
687
703
  echo EPING_HELP_BEGIN; fping -h 2>&1; echo EPING_HELP_END'''
688
704
 
705
+ # One lookup per input line, up to 16 in parallel (xargs -P). The body must not contain
706
+ # single quotes: it is wrapped into sh -c '...'. Result line: "<input> <answers...>".
707
+ _JR_FWD = {
708
+ 'getent': r'r=$(getent ahosts "$0" 2>/dev/null | awk "{print \$1}" | sort -u | tr "\n" " ")',
709
+ 'dig': r'r=$( (dig +short +time=3 +tries=1 A "$0"; dig +short +time=3 +tries=1 AAAA "$0") 2>/dev/null | grep -E "^[0-9a-fA-F:.]+\$" | sort -u | tr "\n" " ")',
710
+ 'nslookup': r'r=$(nslookup -timeout=3 "$0" 2>/dev/null | awk "/^Name:/{f=1} f&&/^Address/{print \$NF}" | sort -u | tr "\n" " ")',
711
+ 'host': r'r=$(host -W 3 "$0" 2>/dev/null | awk "/has address|has IPv6 address/{print \$NF}" | sort -u | tr "\n" " ")',
712
+ }
713
+ _JR_REV = {
714
+ 'getent': r'r=$(getent hosts "$0" 2>/dev/null | awk "NR==1{print \$2}")',
715
+ 'dig': r'r=$(dig +short +time=3 +tries=1 -x "$0" 2>/dev/null | head -1)',
716
+ 'nslookup': r'r=$(nslookup -timeout=3 "$0" 2>/dev/null | awk "/name = /{print \$NF; exit}")',
717
+ 'host': r'r=$(host -W 3 "$0" 2>/dev/null | awk "/domain name pointer/{print \$NF; exit}")',
718
+ }
719
+
720
+ def _jr_run(mode, items, c):
721
+ """Look up items ('fwd': names -> addresses, 'rev': addresses -> name) on jump host c
722
+ with its resolver tool. Returns {item: [answer tokens]}; items of a failed batch are
723
+ missing. Input is validated (no shell metacharacters) and goes in via stdin."""
724
+ body = (_JR_FWD if mode == 'fwd' else _JR_REV)[c.resolver]
725
+ script = ('XP=-P16; echo x | xargs -P16 -n1 true >/dev/null 2>&1 || XP=""\n'
726
+ "xargs -n1 $XP sh -c '" + body + '; echo "$0 $r"' + "'")
727
+ out = {}
728
+ items = [i for i in items if re.match(r'^[A-Za-z0-9._:-]{1,253}$', i)]
729
+ for k in range(0, len(items), JR_CHUNK):
730
+ chunk = items[k:k + JR_CHUNK]
731
+ rc, text = ssh_exec(c, script, timeout=30 + len(chunk) // 5, stdin_text='\n'.join(chunk) + '\n')
732
+ if rc != 0:
733
+ continue
734
+ for line in text.splitlines():
735
+ parts = line.split()
736
+ if parts and parts[0] in chunk:
737
+ out[parts[0]] = parts[1:]
738
+ return out
739
+
740
+ def jr_active():
741
+ """True if lookups go to the jump host (ssh source, enabled, working resolver)."""
742
+ c = ssh_cfg
743
+ return bool(c is not None and c.resolve and c.resolver)
744
+
745
+ def _jr_ttl():
746
+ try:
747
+ return max(30.0, float(getattr(globals().get('args'), 'dns_ttl', 300)))
748
+ except Exception:
749
+ return 300.0
750
+
751
+ def jr_forward(names):
752
+ """{name: [addresses]} resolved on the jump host (cached, one ssh call for all
753
+ uncached names). A name whose batch failed is missing in the result."""
754
+ c = ssh_cfg
755
+ if not jr_active():
756
+ return {}
757
+ now = time.time()
758
+ res, todo = {}, []
759
+ with _jr_cache_lock:
760
+ for n in set(names):
761
+ e = _jr_fwd.get(n)
762
+ if e and e[1] > now:
763
+ res[n] = e[0]
764
+ else:
765
+ todo.append(n)
766
+ if todo:
767
+ with _jr_run_lock:
768
+ got = _jr_run('fwd', todo, c)
769
+ now, ttl = time.time(), _jr_ttl()
770
+ with _jr_cache_lock:
771
+ for n in todo:
772
+ if n not in got:
773
+ res[n] = []
774
+ _jr_fwd[n] = ([], now + DNS_FAIL_TTL) # failed batch / bad name: retry later
775
+ continue
776
+ ips = []
777
+ for a in got[n]:
778
+ try:
779
+ ipaddress.ip_address(a)
780
+ ips.append(a)
781
+ except ValueError:
782
+ pass
783
+ _jr_fwd[n] = (ips, now + (ttl if ips else min(ttl, DNS_FAIL_TTL)))
784
+ res[n] = ips
785
+ return res
786
+
787
+ def jr_reverse(ips):
788
+ """{ip: hostname or None} (PTR) looked up on the jump host, cached."""
789
+ c = ssh_cfg
790
+ if not jr_active():
791
+ return {}
792
+ now = time.time()
793
+ res, todo = {}, []
794
+ with _jr_cache_lock:
795
+ for i in set(ips):
796
+ e = _jr_rev.get(i)
797
+ if e and e[1] > now:
798
+ res[i] = e[0]
799
+ else:
800
+ todo.append(i)
801
+ if todo:
802
+ with _jr_run_lock:
803
+ got = _jr_run('rev', todo, c)
804
+ now, ttl = time.time(), _jr_ttl()
805
+ with _jr_cache_lock:
806
+ for i in todo:
807
+ if i in got:
808
+ nm = got[i][0].rstrip('.') if got[i] else None
809
+ _jr_rev[i] = (nm or None, now + (ttl if nm else min(ttl, DNS_FAIL_TTL)))
810
+ res[i] = nm or None
811
+ else:
812
+ res[i] = None
813
+ _jr_rev[i] = (None, now + DNS_FAIL_TTL)
814
+ return res
815
+
816
+ _ssh_fam6 = set() # hostnames tried with fping -6 on the jump host (no resolver there, see below)
817
+
818
+ def ssh_name_family(name):
819
+ """Address family ('4'/'6') used for the fping run of a hostname on the jump host.
820
+ With a resolver there the names are looked up first (A preferred, AAAA if there is
821
+ no A record - same rule as locally), so an IPv6-only name is pinged with -6.
822
+ Without one, -4 is used and a name that fping reports as NO-DNS is tried with -6
823
+ in the next round (and back to -4 if that fails too), see run_ping_round()."""
824
+ if dns_family in ('4', '6'):
825
+ return dns_family
826
+ if jr_active():
827
+ ips = resolve_all_ips(name)
828
+ if ips:
829
+ return ip_version_str(ips[0]) or '4'
830
+ return '4'
831
+ return '6' if name in _ssh_fam6 else '4'
832
+
833
+ def jr_prefetch(hosts):
834
+ """Resolve all hostnames of 'hosts' on the jump host in one go (fills the cache, so
835
+ the per-name lookups of the callers below do not each need an ssh call)."""
836
+ if jr_active():
837
+ jr_forward([h for h in hosts if not is_ip_host(h)])
838
+
689
839
  def ssh_scan_hostkeys(c):
690
840
  """Fetch the jump host's keys (hashed known_hosts lines) and their fingerprints."""
691
841
  lines, fps = [], []
@@ -758,6 +908,15 @@ def ssh_check(c):
758
908
  m = re.search(r'EPING_HELP_BEGIN\n(.*)EPING_HELP_END', out, re.S)
759
909
  r.update(ok=True, code='ok', help=(m.group(1) if m else ''))
760
910
  c.help, c.version = r['help'], r['version']
911
+ # resolver on the jump host: tool found by the check script, then a real lookup
912
+ c.resolver = ''
913
+ tool = (re.search(r'EPING_RESOLVER=(\w*)', out) or [None, ''])[1]
914
+ if tool in JR_TOOLS:
915
+ c.resolver = tool
916
+ got = _jr_run('fwd', ['localhost'], c)
917
+ if not any(a in ('127.0.0.1', '::1') for a in got.get('localhost', [])):
918
+ c.resolver = ''
919
+ r['resolver'] = c.resolver
761
920
  return r
762
921
 
763
922
  def ssh_activate(c):
@@ -770,6 +929,13 @@ def ssh_activate(c):
770
929
  _fping_caps = None
771
930
  _source_down = False
772
931
  _source_alert = ''
932
+ with _jr_cache_lock:
933
+ _jr_fwd.clear()
934
+ _jr_rev.clear()
935
+ _ssh_fam6.clear()
936
+ with _dns_lock: # lookups made for the old source do not apply
937
+ _dns_cache.clear()
938
+ _addr_redundancy_cache.clear()
773
939
  if old is not None and old is not c:
774
940
  old.close()
775
941
 
@@ -792,7 +958,7 @@ def ssh_apply_pending():
792
958
  ssh_activate(p if p is not False else None)
793
959
  _source_events.append('SOURCE SWITCH %s -> %s' % (old, SOURCE_LABEL))
794
960
 
795
- def ssh_init(target, key, hostkey='accept-new'):
961
+ def ssh_init(target, key, hostkey='accept-new', resolve=True):
796
962
  """-ssh at startup: ask for the secret, trust the host key (ssh's own prompt), verify
797
963
  that fping works on the jump host. Exits with a clear message on any problem."""
798
964
  parsed = ssh_parse_target(target)
@@ -811,7 +977,7 @@ def ssh_init(target, key, hostkey='accept-new'):
811
977
  % ((user + '@' if user else '') + host))
812
978
  except (EOFError, KeyboardInterrupt):
813
979
  error_handler("ERROR: -ssh: aborted")
814
- c = SshCfg(host, user, port, key, secret, hostkey)
980
+ c = SshCfg(host, user, port, key, secret, hostkey, resolve)
815
981
  print('SSH source %s: connecting ...' % c.label)
816
982
  sys.stdout.flush()
817
983
  r = ssh_check(c)
@@ -831,6 +997,9 @@ def ssh_init(target, key, hostkey='accept-new'):
831
997
  error_handler("ERROR: -ssh: " + r['error'])
832
998
  ssh_activate(c)
833
999
  print('SSH source %s: OK (%s)' % (c.label, r['version']))
1000
+ print('Name resolution: ' + (('on the jump host (%s)' % c.resolver) if (c.resolve and c.resolver)
1001
+ else ('local (disabled by -sshnr)' if not c.resolve
1002
+ else 'local (no working getent/dig/nslookup/host on the jump host)')))
834
1003
  sys.stdout.flush()
835
1004
 
836
1005
  def jump_cfg_from(req):
@@ -851,11 +1020,13 @@ def jump_cfg_from(req):
851
1020
  hk = str(req.get('hostkey', 'accept-new'))
852
1021
  if hk not in SSH_HOSTKEY_MODES:
853
1022
  hk = 'accept-new'
854
- _jump_last.update(host=host, user=user, port=port, key=key, hostkey=hk)
855
- return SshCfg(host, user, int(port) if port else None, key, secret, hk), ''
1023
+ resolve = str(req.get('resolve', True)).strip().lower() not in ('0', 'false', 'off', 'no')
1024
+ _jump_last.update(host=host, user=user, port=port, key=key, hostkey=hk, resolve=resolve)
1025
+ return SshCfg(host, user, int(port) if port else None, key, secret, hk, resolve), ''
856
1026
 
857
1027
  def jump_info():
858
1028
  return {'ssh': ssh_prereq(), 'active': SOURCE_LABEL if ssh_cfg else '', 'last': dict(_jump_last),
1029
+ 'resolver': (ssh_cfg.resolver if (ssh_cfg and ssh_cfg.resolve) else ''),
859
1030
  'pending': ('local' if _ssh_pending is False else (_ssh_pending.label if _ssh_pending else ''))}
860
1031
 
861
1032
  def jump_handle(req):
@@ -873,11 +1044,18 @@ def jump_handle(req):
873
1044
  e = ssh_trust_hostkeys(c.host, c.port)
874
1045
  return dict(ok=not e, code='trust', error=e, message='' if e else 'host key trusted - press TEST')
875
1046
  r = ssh_check(c)
1047
+ note = ''
1048
+ if r['ok']:
1049
+ note = ('; names are resolved on the jump host (%s)' % c.resolver if (c.resolve and c.resolver)
1050
+ else ('; names are resolved locally (option off)' if not c.resolve
1051
+ else '; names are resolved locally (no working getent/dig/nslookup/host there)'))
876
1052
  if act == 'test' or not r['ok']:
877
1053
  c.close()
1054
+ if r['ok']:
1055
+ r['message'] = 'OK - ' + (r['version'] or 'fping works') + note
878
1056
  return r
879
1057
  ssh_request(c) # connect
880
- r['message'] = 'connected to %s - switching at the next round' % c.label
1058
+ r['message'] = 'connected to %s - switching at the next round%s' % (c.label, note)
881
1059
  return r
882
1060
 
883
1061
  def source_set(down, reason=''):
@@ -987,6 +1165,20 @@ def resolve_all_ips(name, family=None):
987
1165
  Uncached and used only where all addresses matter (prefer-hostname redundancy
988
1166
  check) or once per name resolution - not hot enough to need caching here."""
989
1167
  fam = family if family is not None else dns_family
1168
+ if jr_active(): # the jump host's view of DNS
1169
+ ips = jr_forward([name]).get(name) or []
1170
+ v4, v6 = [], []
1171
+ for a in ips:
1172
+ ip = ipaddress.ip_address(a)
1173
+ if ip.version == 4:
1174
+ v4.append(a)
1175
+ elif not ip.ipv4_mapped:
1176
+ v6.append(a)
1177
+ key = lambda a: int(ipaddress.ip_address(a))
1178
+ for grp in ((v6, v4) if fam == '6' else (v4, v6)):
1179
+ if grp:
1180
+ return sorted(grp, key=key)
1181
+ return []
990
1182
  af_list = {'4': [socket.AF_INET, socket.AF_INET6],
991
1183
  '6': [socket.AF_INET6, socket.AF_INET]}.get(
992
1184
  fam, [socket.AF_INET, socket.AF_INET6])
@@ -1031,6 +1223,9 @@ def get_names_for_ips(ip_list, timeout=DNS_PTR_TIMEOUT, workers=DNS_RESOLVERS):
1031
1223
  this runs."""
1032
1224
  if not ip_list:
1033
1225
  return {}
1226
+ if jr_active(): # PTR lookups on the jump host
1227
+ got = jr_reverse(list(ip_list))
1228
+ return dict((ip, got.get(ip)) for ip in ip_list)
1034
1229
  old_timeout = socket.getdefaulttimeout()
1035
1230
  socket.setdefaulttimeout(timeout)
1036
1231
  results = {}
@@ -1108,6 +1303,8 @@ def _forward_confirms(name, ip):
1108
1303
  that resolves back to exactly this ip. Guards GET NAMES: a PTR record with no
1109
1304
  matching forward record must not be used to rename a host, or pinging it
1110
1305
  afterwards by name would fail (NO-DNS) or silently hit a different address."""
1306
+ if jr_active():
1307
+ return ip in (jr_forward([name]).get(name) or [])
1111
1308
  try:
1112
1309
  af = socket.AF_INET6 if ip_version_str(ip) == '6' else socket.AF_INET
1113
1310
  infos = socket.getaddrinfo(name, None, af, socket.SOCK_DGRAM)
@@ -1127,6 +1324,8 @@ def _get_names_lookup(candidates):
1127
1324
  ptr = get_names_for_ips(candidates)
1128
1325
  checkable = [ip for ip in candidates if ptr.get(ip) and match_re(ptr[ip], fqdn_re)]
1129
1326
  fwd_ok = {}
1327
+ if checkable and jr_active():
1328
+ jr_forward([ptr[ip] for ip in checkable]) # one ssh call for all forward checks
1130
1329
  if checkable:
1131
1330
  with concurrent.futures.ThreadPoolExecutor(
1132
1331
  max_workers=max(1, min(DNS_RESOLVERS, len(checkable)))) as pool:
@@ -1200,6 +1399,7 @@ def apply_get_names(original_hosts_list, active_hosts_list, host_state, up_seen,
1200
1399
  only used at startup (-gn), before the CLI/web loop exists to stay responsive
1201
1400
  for. The interactive [G] key/cmd runs this in the background instead, see
1202
1401
  get_names_start()/get_names_finish()."""
1402
+ jr_prefetch(original_hosts_list)
1203
1403
  covered_ips = set()
1204
1404
  for h in original_hosts_list:
1205
1405
  if not is_ip_host(h):
@@ -1223,8 +1423,9 @@ def get_names_start(original_hosts_list, dns_ttl):
1223
1423
  Returns (thread, result_holder, candidates); thread is None (nothing started)
1224
1424
  when there is nothing to look up. Poll thread.is_alive() and, once False, call
1225
1425
  get_names_finish() with result_holder."""
1226
- if ssh_target: # reverse DNS here would not match the jump host's view
1426
+ if ssh_target and not jr_active(): # reverse DNS here would not match the jump host's view
1227
1427
  return None, None, []
1428
+ jr_prefetch(original_hosts_list)
1228
1429
  covered_ips = set()
1229
1430
  for h in original_hosts_list:
1230
1431
  if not is_ip_host(h):
@@ -1481,8 +1682,10 @@ def fping_cmd(summary_hosts_list, lock, cmd_base=None):
1481
1682
  elif ssh_target and line.strip():
1482
1683
  stray.append(line.strip())
1483
1684
  ping.wait()
1484
- if ssh_target and (ping.returncode == 255 or (not fping_result_data and ping.returncode not in (0, 1, 2)
1485
- and not _abort_scan)):
1685
+ # a scan cut short by a command (scan_abort) ends the ssh client with exit 255 ("Killed
1686
+ # by signal 15"): that is not a lost jump host
1687
+ if ssh_target and not _abort_scan and (ping.returncode == 255 or (
1688
+ not fping_result_data and ping.returncode not in (0, 1, 2))):
1486
1689
  _source_fail_reason = (' / '.join(stray) or ('ssh/fping exit ' + str(ping.returncode)))[:200]
1487
1690
  with _procs_lock:
1488
1691
  try:
@@ -1847,6 +2050,7 @@ def apply_prefer_hostname(hosts_list, dns_ttl):
1847
2050
  hostname's resolved addresses are cached for this check - see _cached_all_ips()."""
1848
2051
  hostname_ips = set()
1849
2052
  names = [h for h in hosts_list if not is_ip_host(h)]
2053
+ jr_prefetch(names)
1850
2054
  if len(names) > 1: # resolve in parallel - serial lookups of unresolvable names are slow
1851
2055
  with concurrent.futures.ThreadPoolExecutor(max_workers=32) as ex:
1852
2056
  for ips in ex.map(lambda n: _cached_all_ips(n, dns_ttl), names):
@@ -1866,6 +2070,7 @@ def apply_prefer_ip(hosts_list, dns_ttl):
1866
2070
  raw_ips = set(h for h in hosts_list if is_ip_host(h))
1867
2071
  if not raw_ips:
1868
2072
  return list(hosts_list)
2073
+ jr_prefetch(hosts_list)
1869
2074
  return [h for h in hosts_list
1870
2075
  if is_ip_host(h) or not raw_ips.intersection(_cached_all_ips(h, dns_ttl))]
1871
2076
 
@@ -1885,6 +2090,7 @@ def apply_ip_only_on(original_hosts_list, active_hosts_list, host_state, up_seen
1885
2090
  entries."""
1886
2091
  existing = set(h.lower() for h in original_hosts_list)
1887
2092
  ip_map, unresolved, dropped = {}, 0, 0
2093
+ jr_prefetch(original_hosts_list)
1888
2094
  for h in list(original_hosts_list):
1889
2095
  if is_ip_host(h):
1890
2096
  continue
@@ -1939,6 +2145,7 @@ def apply_ip_only_on_web(original_hosts_list, active_hosts_list, host_state,
1939
2145
  uses apply_ip_only_on above and keeps the old drop-on-collision behaviour."""
1940
2146
  existing = set(h.lower() for h in original_hosts_list)
1941
2147
  ip_map, unresolved, skipped = {}, 0, 0
2148
+ jr_prefetch(original_hosts_list)
1942
2149
  for h in list(original_hosts_list):
1943
2150
  if is_ip_host(h):
1944
2151
  continue
@@ -2739,12 +2946,14 @@ def run_ping_round(active_hosts_list, threads_arg, rate_pps=DEFAULT_RATE_PPS,
2739
2946
  # group above is split again by address family. In the common single-family
2740
2947
  # case this yields exactly one subgroup per group, same as before IPv6 support.
2741
2948
  subgroups = [] # (targets, retries, label, family)
2949
+ if ssh_target and jr_active():
2950
+ jr_prefetch(targets) # one ssh call for all uncached names
2742
2951
  for grp_targets, grp_retries, grp_name in groups:
2743
2952
  by_fam = {'4': [], '6': []}
2744
2953
  for t in grp_targets:
2745
2954
  fam = ip_version_str(t)
2746
2955
  if not fam and ssh_target: # name resolved on the jump host
2747
- fam = '6' if dns_family == '6' else '4'
2956
+ fam = ssh_name_family(t)
2748
2957
  if fam:
2749
2958
  by_fam[fam].append(t)
2750
2959
  for fam in ('4', '6'):
@@ -2842,6 +3051,13 @@ def run_ping_round(active_hosts_list, threads_arg, rate_pps=DEFAULT_RATE_PPS,
2842
3051
  new_row[0] = display_name
2843
3052
  rows.append(new_row)
2844
3053
 
3054
+ if ssh_target and not jr_active() and dns_family == 'auto':
3055
+ for r in rows: # alternate -4 / -6 for names fping cannot resolve
3056
+ if not is_ip_host(r[0]) and 'NO-DNS' in r[1]:
3057
+ if r[0] in _ssh_fam6:
3058
+ _ssh_fam6.discard(r[0])
3059
+ else:
3060
+ _ssh_fam6.add(r[0])
2845
3061
  # names that do not resolve never reach fping - report them like fping would
2846
3062
  if unresolved:
2847
3063
  now_str = get_date_time()
@@ -3306,6 +3522,7 @@ WEB_INDEX_HTML = r"""<!DOCTYPE html>
3306
3522
  <option value="accept-new">accept new hosts automatically (a changed key is still refused)</option>
3307
3523
  <option value="ask">ask - confirm an unknown host key (TRUST KEY)</option>
3308
3524
  <option value="off">off - no host key check at all (insecure)</option></select></td></tr>
3525
+ <tr><td style="padding:3px 8px 3px 0;white-space:nowrap">NAMES</td><td colspan="3"><label style="font-size:12px;cursor:pointer"><input type="checkbox" id="jumpResolve" checked> resolve names on the jump host (getent / dig / nslookup / host; for GET NAMES, PREFER HOSTNAME / IP ADDRESS, IP ONLY)</label></td></tr>
3309
3526
  <tr><td style="padding:3px 8px 3px 0;white-space:nowrap">KEY FILE</td><td colspan="3"><input type="text" id="jumpKey" style="width:100%;box-sizing:border-box" placeholder="optional, path on the computer running eping.py" autocomplete="off" spellcheck="false"></td></tr>
3310
3527
  </table>
3311
3528
  <div id="jumpWarn" style="display:none;font-size:11px;color:var(--flap,#d9a400);margin-top:6px">This page is not served from this computer (HTTP, no encryption): the password would cross the network in clear text. Prefer a key.</div>
@@ -4610,6 +4827,7 @@ function jumpFields(action){
4610
4827
  return {action: action, host: document.getElementById('jumpHost').value, port: document.getElementById('jumpPort').value,
4611
4828
  user: document.getElementById('jumpUser').value, key: document.getElementById('jumpKey').value,
4612
4829
  hostkey: document.getElementById('jumpHostKey').value,
4830
+ resolve: document.getElementById('jumpResolve').checked,
4613
4831
  secret: document.getElementById('jumpSecret').value};
4614
4832
  }
4615
4833
  function jumpButtons(){
@@ -4647,13 +4865,15 @@ function openJump(){
4647
4865
  if(j.ssh && j.ssh.ok){
4648
4866
  pre.style.color = 'var(--dim)';
4649
4867
  pre.textContent = 'ssh client: ' + (j.ssh.version || 'found') + (j.ssh.keyscan ? '' : ' (ssh-keyscan missing: unknown host keys cannot be trusted here)') +
4650
- ' | source: ' + (j.active ? j.active : 'local') + (j.pending ? ' (switching to ' + j.pending + ')' : '');
4868
+ ' | source: ' + (j.active ? j.active : 'local') + (j.pending ? ' (switching to ' + j.pending + ')' : '') +
4869
+ (j.active ? ' | names: ' + (j.resolver ? 'jump host (' + j.resolver + ')' : 'local') : '');
4651
4870
  } else {
4652
4871
  pre.style.color = 'var(--down,#f85149)';
4653
4872
  pre.textContent = (j.ssh && j.ssh.hint) || 'OpenSSH client not found on this computer.';
4654
4873
  }
4655
4874
  if(j.last){
4656
4875
  if(j.last.hostkey) document.getElementById('jumpHostKey').value = j.last.hostkey;
4876
+ if(typeof j.last.resolve === 'boolean') document.getElementById('jumpResolve').checked = j.last.resolve;
4657
4877
  ['host','port','user','key'].forEach(function(k){
4658
4878
  var el = document.getElementById('jump' + k.charAt(0).toUpperCase() + k.slice(1));
4659
4879
  if(el && !el.value) el.value = j.last[k] || '';
@@ -7189,6 +7409,8 @@ def build_cli_snapshot(args, backoff, timeout, retries, down_retries, flap_windo
7189
7409
  parts.append('-sshkey ' + args.ssh_key)
7190
7410
  if getattr(args, 'ssh_hostkey', 'accept-new') != 'accept-new':
7191
7411
  parts.append('-sshhk ' + args.ssh_hostkey)
7412
+ if getattr(args, 'ssh_no_resolve', False):
7413
+ parts.append('-sshnr')
7192
7414
  return ' '.join(parts)
7193
7415
 
7194
7416
 
@@ -7379,7 +7601,8 @@ def run_web_mode(original_hosts_list, host_state, args, logfile_file_name,
7379
7601
  gn_thread, gn_result, gn_candidates = get_names_start(
7380
7602
  original_hosts_list, int(args.dns_ttl))
7381
7603
  if gn_thread is None:
7382
- message = 'get names: no eligible IP host(s)'
7604
+ message = ('get names: not available - the jump host has no working resolver (or it is switched off)'
7605
+ if (ssh_target and not jr_active()) else 'get names: no eligible IP host(s)')
7383
7606
  else:
7384
7607
  message = ('get names: running in background (%d host(s))'
7385
7608
  % len(gn_candidates))
@@ -7961,6 +8184,7 @@ if __name__=='__main__':
7961
8184
  parser.add_argument('-fs', '--full_sweep', default=str(FULL_SWEEP_DEF), dest='full_sweep', help="every Nth run probes every host with full retries, range 0-" + str(FULL_SWEEP_MAX) + " (default: " + str(FULL_SWEEP_DEF) + ", 0 = disabled/never)")
7962
8185
  parser.add_argument('-ssh', '--ssh', dest='ssh', metavar='[USER@]HOST[:PORT]', default=None, help="run fping on this jump host via ssh instead of locally (system ssh; password or key passphrase is asked at startup; the jump host resolves the names and needs a working fping)")
7963
8186
  parser.add_argument('-sshkey', '--ssh_key', dest='ssh_key', metavar='FILE', default=None, help="private key file for -ssh (default: ssh agent / default keys)")
8187
+ parser.add_argument('-sshnr', '--ssh_no_resolve', action="store_true", dest='ssh_no_resolve', help="with -ssh: do not resolve names on the jump host (default: GET NAMES, PREFER HOSTNAME / IP ADDRESS and IP ONLY use getent / dig / nslookup / host there when available; otherwise the local resolver is used)")
7964
8188
  parser.add_argument('-sshhk', '--ssh_hostkey', dest='ssh_hostkey', choices=SSH_HOSTKEY_MODES, default='accept-new', help="host key check for -ssh: accept-new = trust hosts seen for the first time, a changed key is refused (default); ask = confirm an unknown key; off = no check at all (insecure)")
7965
8189
  parser.add_argument('-ncf', '--no_csv_fallback', action="store_true", dest='no_csv_fallback', help="do not ping the CSV address of a host whose name does not resolve (default: it is pinged by the CSV IP and marked in the host popup)")
7966
8190
  parser.add_argument('-ncs', '--no_check_source', action="store_true", dest='no_check_source', help="do not pass --check-source to fping (only needed for hosts replying from a different address)")
@@ -8327,7 +8551,7 @@ if __name__=='__main__':
8327
8551
  # (CLI: [A]=ADD HOST, web gui: ADD HOST / UPLOAD FILE)
8328
8552
 
8329
8553
  if args.ssh:
8330
- ssh_init(args.ssh, args.ssh_key, args.ssh_hostkey)
8554
+ ssh_init(args.ssh, args.ssh_key, args.ssh_hostkey, not args.ssh_no_resolve)
8331
8555
  elif args.ssh_key:
8332
8556
  error_handler("ERROR: -sshkey needs -ssh")
8333
8557
 
@@ -8575,13 +8799,15 @@ if __name__=='__main__':
8575
8799
  Same actions as the web gui window (jump_handle). Pinging keeps running meanwhile."""
8576
8800
  rows, cols = screen.getmaxyx()
8577
8801
  dw = min(76, max(40, cols - 4))
8578
- dh = 18
8802
+ dh = 19
8579
8803
  dy = max(0, rows // 2 - dh // 2)
8580
8804
  dx = max(0, cols // 2 - dw // 2)
8581
- labels = ['HOST', 'PORT', 'USER', 'PASSWORD', 'HOST KEY', 'KEY FILE']
8582
- keys = ['host', 'port', 'user', 'secret', 'hostkey', 'key']
8805
+ labels = ['HOST', 'PORT', 'USER', 'PASSWORD', 'HOST KEY', 'NAMES', 'KEY FILE']
8806
+ keys = ['host', 'port', 'user', 'secret', 'hostkey', 'resolve', 'key']
8583
8807
  vals = [_jump_last.get('host', ''), _jump_last.get('port', ''), _jump_last.get('user', ''),
8584
- '', _jump_last.get('hostkey', 'accept-new'), _jump_last.get('key', '')]
8808
+ '', _jump_last.get('hostkey', 'accept-new'),
8809
+ 'on' if _jump_last.get('resolve', True) else 'off', _jump_last.get('key', '')]
8810
+ nm_text = {'on': 'resolve on the jump host', 'off': 'resolve locally'}
8585
8811
  hk_text = {'ask': 'ask (confirm unknown key)', 'accept-new': 'accept new hosts', 'off': 'OFF - no check (insecure)'}
8586
8812
  pre = ssh_prereq()
8587
8813
  status = ['']
@@ -8627,11 +8853,13 @@ if __name__=='__main__':
8627
8853
  screen_output(dy + 3, dx, '│' + (' ' + info)[:dw - 2].ljust(dw - 2) + '│', cls, 0)
8628
8854
  fw = dw - 16
8629
8855
  for i, lab in enumerate(labels):
8630
- shown = ('*' * len(vals[i])) if keys[i] == 'secret' else (hk_text[vals[i]] + ' [ENTER/SPACE=change]' if keys[i] == 'hostkey' else vals[i])
8856
+ shown = ('*' * len(vals[i])) if keys[i] == 'secret' else (
8857
+ hk_text[vals[i]] + ' [ENTER/SPACE=change]' if keys[i] == 'hostkey' else
8858
+ nm_text[vals[i]] + ' [ENTER/SPACE=change]' if keys[i] == 'resolve' else vals[i])
8631
8859
  shown = shown[-fw:] if len(shown) > fw else shown
8632
8860
  line = ' ' + lab.ljust(10) + (shown + '_' * 0).ljust(fw) + ' '
8633
8861
  screen_output(dy + 4 + i, dx, '│' + line[:dw - 2].ljust(dw - 2) + '│', 2 if focus == i else 1, 1 if focus == i else 0)
8634
- screen_output(dy + 10, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
8862
+ screen_output(dy + 11, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
8635
8863
  lines = []
8636
8864
  for t in status:
8637
8865
  lines.extend(wrap(t, dw - 4))
@@ -8639,18 +8867,18 @@ if __name__=='__main__':
8639
8867
  lines.extend(wrap(f, dw - 4))
8640
8868
  for i in range(4):
8641
8869
  txt = lines[i] if i < len(lines) else ''
8642
- screen_output(dy + 11 + i, dx, '│' + (' ' + txt)[:dw - 2].ljust(dw - 2) + '│', status_cls, 0)
8870
+ screen_output(dy + 12 + i, dx, '│' + (' ' + txt)[:dw - 2].ljust(dw - 2) + '│', status_cls, 0)
8643
8871
  bl = buttons()
8644
8872
  col = dx + 2
8645
- screen_output(dy + 15, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
8873
+ screen_output(dy + 16, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
8646
8874
  for i, (name, _a) in enumerate(bl):
8647
8875
  lab = ' [' + name + '] '
8648
8876
  hi = (focus == len(labels) + i)
8649
- screen_output(dy + 15, col, lab, 2 if hi else 1, 1 if hi else 0)
8877
+ screen_output(dy + 16, col, lab, 2 if hi else 1, 1 if hi else 0)
8650
8878
  col += len(lab) + 1
8651
- screen_output(dy + 16, dx, '│' + ' TAB/arrows=move ENTER=select ESC=close'.ljust(dw - 2) + '│', 1, 0)
8652
- screen_output(dy + 17, dx, '└' + '─' * (dw - 2) + '┘', 1, 1)
8653
- if focus < len(labels) and keys[focus] != 'hostkey' and not busy['t']:
8879
+ screen_output(dy + 17, dx, '│' + ' TAB/arrows=move ENTER=select ESC=close'.ljust(dw - 2) + '│', 1, 0)
8880
+ screen_output(dy + 18, dx, '└' + '─' * (dw - 2) + '┘', 1, 1)
8881
+ if focus < len(labels) and keys[focus] not in ('hostkey', 'resolve') and not busy['t']:
8654
8882
  curses.curs_set(1)
8655
8883
  shown_len = min(len(vals[focus]), fw)
8656
8884
  screen.move(dy + 4 + focus, dx + 12 + shown_len)
@@ -8722,6 +8950,8 @@ if __name__=='__main__':
8722
8950
  focus = (focus - 1) % nfocus
8723
8951
  elif ch in (10, 13, 32) and focus < len(labels) and keys[focus] == 'hostkey':
8724
8952
  vals[focus] = SSH_HOSTKEY_MODES[(SSH_HOSTKEY_MODES.index(vals[focus]) + 1) % 3]
8953
+ elif ch in (10, 13, 32) and focus < len(labels) and keys[focus] == 'resolve':
8954
+ vals[focus] = 'off' if vals[focus] == 'on' else 'on'
8725
8955
  elif ch in (10, 13):
8726
8956
  if focus < len(labels):
8727
8957
  focus = (focus + 1) % nfocus
@@ -8733,7 +8963,7 @@ if __name__=='__main__':
8733
8963
  status, status_cls = [pre['hint']], 3
8734
8964
  else:
8735
8965
  start(act)
8736
- elif focus < len(labels):
8966
+ elif focus < len(labels) and keys[focus] not in ('hostkey', 'resolve'):
8737
8967
  if ch in (curses.KEY_BACKSPACE, 127, 8):
8738
8968
  vals[focus] = vals[focus][:-1]
8739
8969
  elif 32 <= ch <= 126 and len(vals[focus]) < 255:
@@ -9529,7 +9759,8 @@ if __name__=='__main__':
9529
9759
  gn_thread, gn_result, gn_candidates = get_names_start(
9530
9760
  original_hosts_list, int(args.dns_ttl))
9531
9761
  if gn_thread is None:
9532
- notice('GET NAMES: NO ELIGIBLE IP HOST(S)', 2, 3)
9762
+ notice('GET NAMES: NOT AVAILABLE VIA JUMP HOST' if (ssh_target and not jr_active())
9763
+ else 'GET NAMES: NO ELIGIBLE IP HOST(S)', 2, 3)
9533
9764
  message = 'get names: no eligible ip host(s)'
9534
9765
  else:
9535
9766
  notice('GET NAMES: RUNNING IN BACKGROUND (%d HOST(S))'
@@ -10307,7 +10538,8 @@ if __name__=='__main__':
10307
10538
  gn_thread, gn_result, gn_candidates = get_names_start(
10308
10539
  original_hosts_list, int(args.dns_ttl))
10309
10540
  if gn_thread is None:
10310
- notice('GET NAMES: NO ELIGIBLE IP HOST(S)', 2, 3)
10541
+ notice('GET NAMES: NOT AVAILABLE VIA JUMP HOST' if (ssh_target and not jr_active())
10542
+ else 'GET NAMES: NO ELIGIBLE IP HOST(S)', 2, 3)
10311
10543
  else:
10312
10544
  notice('GET NAMES: RUNNING IN BACKGROUND (%d HOST(S))'
10313
10545
  % len(gn_candidates), 2)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eping
3
- Version: 4.2
3
+ Version: 4.5
4
4
  Summary: Continuous ICMP reachability monitor built on fping, with a CSV logfile analyser (epinga)
5
5
  Author-email: Ewald Jeitler <ewald@jeitler.cc>
6
6
  License: GPL-3.0-or-later
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Dynamic: license-file
19
19
 
20
- # eping.py 4.02
20
+ # eping.py 4.05
21
21
 
22
22
  Continuous ICMP reachability monitor built on `fping`. Scans a host list in a loop,
23
23
  reports each host UP/DOWN/NO-DNS, counts state changes. CLI (curses) or web GUI.
@@ -449,6 +449,7 @@ separated) · `-r` IP range (comma separated, shortened end)
449
449
  | `-ssh` | `[USER@]HOST[:PORT]` | off | run fping on a jump host via ssh (see *SSH source (JUMPHOST)*) |
450
450
  | `-sshkey` | file | agent | private key file for `-ssh` |
451
451
  | `-sshhk` | accept-new/ask/off | accept-new | host key check for `-ssh` |
452
+ | `-sshnr` | — | off | with `-ssh`: do not resolve names on the jump host (use the local resolver) |
452
453
  | `-ncf` | — | off | CSV: do not ping the CSV IP of a name without DNS record |
453
454
  | `-up` | — | 0 | learning phase: after N rounds show only hosts seen UP (view CURRENTLY-UP; all hosts are still pinged until SET REFERENCE) |
454
455
  | `-setref` | — | off | with `-up`: hosts seen UP become new reference list |
@@ -509,24 +510,31 @@ these rows automatically.
509
510
  ## SSH source (JUMPHOST)
510
511
 
511
512
  Run fping on a jump host instead of locally (system `ssh` only). Any Linux host with a working `fping`
512
- (e.g. a caretaker) will do; it resolves the names itself. Start: `-ssh [USER@]HOST[:PORT]` (+ `-sshkey FILE`, `-sshhk MODE`);
513
+ (e.g. a caretaker) will do; it resolves the names itself. Start: `-ssh [USER@]HOST[:PORT]` (+ `-sshkey FILE`, `-sshhk MODE`, `-sshnr`);
513
514
  the password or key passphrase is asked on the terminal at startup. At runtime: `J` in the CLI,
514
515
  JUMPHOST button in the web GUI (after ADV OPTIONS).
515
516
 
516
517
  ![JUMPHOST window](https://raw.githubusercontent.com/ewaldj/eping/main/docs/img/jumphost.png)
517
518
 
518
- - Window: HOST, PORT, USER, PASSWORD (key passphrase), HOST KEY, KEY FILE. TEST checks ssh, login and
519
+ - Window: HOST, PORT, USER, PASSWORD (key passphrase), HOST KEY, NAMES, KEY FILE. TEST checks ssh, login and
519
520
  fping on the jump host, CONNECT switches the scan source, USE LOCAL switches back. A missing local
520
521
  `ssh` is shown in the window.
521
522
  - HOST KEY: `accept-new` (default) - hosts seen for the first time are trusted automatically, a changed
522
523
  key is still refused; `ask` - an unknown key is shown with its fingerprint and must be accepted with
523
524
  TRUST KEY; `off` - no check at all (insecure).
525
+ - NAMES (default on): the DNS lookups eping does itself - GET NAMES, PREFER HOSTNAME / IP ADDRESS, IP ONLY -
526
+ run on the jump host over the same ssh connection (no second login), using `getent`, else `dig`,
527
+ `nslookup` or `host` there; TEST/CONNECT report which tool works. One batch per call, names are validated
528
+ and sent via stdin. Without a working tool, or with the option off (`-sshnr`), the local resolver is used
529
+ and GET NAMES is not available. The DNS caches are cleared on every source switch.
530
+ A name with only an AAAA record is pinged with `fping -6` there (A is preferred, as locally). Without a resolver on
531
+ the jump host, a name that fping reports as NO-DNS is tried with `-6` in the next round, alternating with `-4`; `-4` / `-6` force one family.
524
532
  - The switch is applied at the start of the next round; hosts, states and counters are kept.
525
533
  - Connection loss: results of the round are discarded, hosts keep their state, a banner is shown and
526
534
  eping reconnects. Logged as `#COMMENT#` rows (`JUMPHOST ... OFFLINE: <reason>`, `JUMPHOST ... ONLINE
527
535
  again`, `SOURCE SWITCH <old> -> <new>`), shown in the report's comment timeline.
528
536
  - Log column `SOURCE`: `local` or the jump host of each row.
529
- - Limits: GET NAMES and the CSV-IP fallback are not available over ssh; at most 4 ssh processes
537
+ - Limits: the CSV-IP fallback is not available over ssh; at most 4 ssh processes
530
538
  (2 on Windows); the web password transfer is plain HTTP - use `-bind 127.0.0.1` or a trusted network.
531
539
 
532
540
  ## Web GUI header
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes