eping 4.2__tar.gz → 4.4__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.4
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.04
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.04
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.04"
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.04'
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):
@@ -1847,6 +2048,7 @@ def apply_prefer_hostname(hosts_list, dns_ttl):
1847
2048
  hostname's resolved addresses are cached for this check - see _cached_all_ips()."""
1848
2049
  hostname_ips = set()
1849
2050
  names = [h for h in hosts_list if not is_ip_host(h)]
2051
+ jr_prefetch(names)
1850
2052
  if len(names) > 1: # resolve in parallel - serial lookups of unresolvable names are slow
1851
2053
  with concurrent.futures.ThreadPoolExecutor(max_workers=32) as ex:
1852
2054
  for ips in ex.map(lambda n: _cached_all_ips(n, dns_ttl), names):
@@ -1866,6 +2068,7 @@ def apply_prefer_ip(hosts_list, dns_ttl):
1866
2068
  raw_ips = set(h for h in hosts_list if is_ip_host(h))
1867
2069
  if not raw_ips:
1868
2070
  return list(hosts_list)
2071
+ jr_prefetch(hosts_list)
1869
2072
  return [h for h in hosts_list
1870
2073
  if is_ip_host(h) or not raw_ips.intersection(_cached_all_ips(h, dns_ttl))]
1871
2074
 
@@ -1885,6 +2088,7 @@ def apply_ip_only_on(original_hosts_list, active_hosts_list, host_state, up_seen
1885
2088
  entries."""
1886
2089
  existing = set(h.lower() for h in original_hosts_list)
1887
2090
  ip_map, unresolved, dropped = {}, 0, 0
2091
+ jr_prefetch(original_hosts_list)
1888
2092
  for h in list(original_hosts_list):
1889
2093
  if is_ip_host(h):
1890
2094
  continue
@@ -1939,6 +2143,7 @@ def apply_ip_only_on_web(original_hosts_list, active_hosts_list, host_state,
1939
2143
  uses apply_ip_only_on above and keeps the old drop-on-collision behaviour."""
1940
2144
  existing = set(h.lower() for h in original_hosts_list)
1941
2145
  ip_map, unresolved, skipped = {}, 0, 0
2146
+ jr_prefetch(original_hosts_list)
1942
2147
  for h in list(original_hosts_list):
1943
2148
  if is_ip_host(h):
1944
2149
  continue
@@ -2739,12 +2944,14 @@ def run_ping_round(active_hosts_list, threads_arg, rate_pps=DEFAULT_RATE_PPS,
2739
2944
  # group above is split again by address family. In the common single-family
2740
2945
  # case this yields exactly one subgroup per group, same as before IPv6 support.
2741
2946
  subgroups = [] # (targets, retries, label, family)
2947
+ if ssh_target and jr_active():
2948
+ jr_prefetch(targets) # one ssh call for all uncached names
2742
2949
  for grp_targets, grp_retries, grp_name in groups:
2743
2950
  by_fam = {'4': [], '6': []}
2744
2951
  for t in grp_targets:
2745
2952
  fam = ip_version_str(t)
2746
2953
  if not fam and ssh_target: # name resolved on the jump host
2747
- fam = '6' if dns_family == '6' else '4'
2954
+ fam = ssh_name_family(t)
2748
2955
  if fam:
2749
2956
  by_fam[fam].append(t)
2750
2957
  for fam in ('4', '6'):
@@ -2842,6 +3049,13 @@ def run_ping_round(active_hosts_list, threads_arg, rate_pps=DEFAULT_RATE_PPS,
2842
3049
  new_row[0] = display_name
2843
3050
  rows.append(new_row)
2844
3051
 
3052
+ if ssh_target and not jr_active() and dns_family == 'auto':
3053
+ for r in rows: # alternate -4 / -6 for names fping cannot resolve
3054
+ if not is_ip_host(r[0]) and 'NO-DNS' in r[1]:
3055
+ if r[0] in _ssh_fam6:
3056
+ _ssh_fam6.discard(r[0])
3057
+ else:
3058
+ _ssh_fam6.add(r[0])
2845
3059
  # names that do not resolve never reach fping - report them like fping would
2846
3060
  if unresolved:
2847
3061
  now_str = get_date_time()
@@ -3306,6 +3520,7 @@ WEB_INDEX_HTML = r"""<!DOCTYPE html>
3306
3520
  <option value="accept-new">accept new hosts automatically (a changed key is still refused)</option>
3307
3521
  <option value="ask">ask - confirm an unknown host key (TRUST KEY)</option>
3308
3522
  <option value="off">off - no host key check at all (insecure)</option></select></td></tr>
3523
+ <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
3524
  <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
3525
  </table>
3311
3526
  <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 +4825,7 @@ function jumpFields(action){
4610
4825
  return {action: action, host: document.getElementById('jumpHost').value, port: document.getElementById('jumpPort').value,
4611
4826
  user: document.getElementById('jumpUser').value, key: document.getElementById('jumpKey').value,
4612
4827
  hostkey: document.getElementById('jumpHostKey').value,
4828
+ resolve: document.getElementById('jumpResolve').checked,
4613
4829
  secret: document.getElementById('jumpSecret').value};
4614
4830
  }
4615
4831
  function jumpButtons(){
@@ -4647,13 +4863,15 @@ function openJump(){
4647
4863
  if(j.ssh && j.ssh.ok){
4648
4864
  pre.style.color = 'var(--dim)';
4649
4865
  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 + ')' : '');
4866
+ ' | source: ' + (j.active ? j.active : 'local') + (j.pending ? ' (switching to ' + j.pending + ')' : '') +
4867
+ (j.active ? ' | names: ' + (j.resolver ? 'jump host (' + j.resolver + ')' : 'local') : '');
4651
4868
  } else {
4652
4869
  pre.style.color = 'var(--down,#f85149)';
4653
4870
  pre.textContent = (j.ssh && j.ssh.hint) || 'OpenSSH client not found on this computer.';
4654
4871
  }
4655
4872
  if(j.last){
4656
4873
  if(j.last.hostkey) document.getElementById('jumpHostKey').value = j.last.hostkey;
4874
+ if(typeof j.last.resolve === 'boolean') document.getElementById('jumpResolve').checked = j.last.resolve;
4657
4875
  ['host','port','user','key'].forEach(function(k){
4658
4876
  var el = document.getElementById('jump' + k.charAt(0).toUpperCase() + k.slice(1));
4659
4877
  if(el && !el.value) el.value = j.last[k] || '';
@@ -7189,6 +7407,8 @@ def build_cli_snapshot(args, backoff, timeout, retries, down_retries, flap_windo
7189
7407
  parts.append('-sshkey ' + args.ssh_key)
7190
7408
  if getattr(args, 'ssh_hostkey', 'accept-new') != 'accept-new':
7191
7409
  parts.append('-sshhk ' + args.ssh_hostkey)
7410
+ if getattr(args, 'ssh_no_resolve', False):
7411
+ parts.append('-sshnr')
7192
7412
  return ' '.join(parts)
7193
7413
 
7194
7414
 
@@ -7379,7 +7599,8 @@ def run_web_mode(original_hosts_list, host_state, args, logfile_file_name,
7379
7599
  gn_thread, gn_result, gn_candidates = get_names_start(
7380
7600
  original_hosts_list, int(args.dns_ttl))
7381
7601
  if gn_thread is None:
7382
- message = 'get names: no eligible IP host(s)'
7602
+ message = ('get names: not available - the jump host has no working resolver (or it is switched off)'
7603
+ if (ssh_target and not jr_active()) else 'get names: no eligible IP host(s)')
7383
7604
  else:
7384
7605
  message = ('get names: running in background (%d host(s))'
7385
7606
  % len(gn_candidates))
@@ -7961,6 +8182,7 @@ if __name__=='__main__':
7961
8182
  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
8183
  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
8184
  parser.add_argument('-sshkey', '--ssh_key', dest='ssh_key', metavar='FILE', default=None, help="private key file for -ssh (default: ssh agent / default keys)")
8185
+ 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
8186
  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
8187
  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
8188
  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 +8549,7 @@ if __name__=='__main__':
8327
8549
  # (CLI: [A]=ADD HOST, web gui: ADD HOST / UPLOAD FILE)
8328
8550
 
8329
8551
  if args.ssh:
8330
- ssh_init(args.ssh, args.ssh_key, args.ssh_hostkey)
8552
+ ssh_init(args.ssh, args.ssh_key, args.ssh_hostkey, not args.ssh_no_resolve)
8331
8553
  elif args.ssh_key:
8332
8554
  error_handler("ERROR: -sshkey needs -ssh")
8333
8555
 
@@ -8575,13 +8797,15 @@ if __name__=='__main__':
8575
8797
  Same actions as the web gui window (jump_handle). Pinging keeps running meanwhile."""
8576
8798
  rows, cols = screen.getmaxyx()
8577
8799
  dw = min(76, max(40, cols - 4))
8578
- dh = 18
8800
+ dh = 19
8579
8801
  dy = max(0, rows // 2 - dh // 2)
8580
8802
  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']
8803
+ labels = ['HOST', 'PORT', 'USER', 'PASSWORD', 'HOST KEY', 'NAMES', 'KEY FILE']
8804
+ keys = ['host', 'port', 'user', 'secret', 'hostkey', 'resolve', 'key']
8583
8805
  vals = [_jump_last.get('host', ''), _jump_last.get('port', ''), _jump_last.get('user', ''),
8584
- '', _jump_last.get('hostkey', 'accept-new'), _jump_last.get('key', '')]
8806
+ '', _jump_last.get('hostkey', 'accept-new'),
8807
+ 'on' if _jump_last.get('resolve', True) else 'off', _jump_last.get('key', '')]
8808
+ nm_text = {'on': 'resolve on the jump host', 'off': 'resolve locally'}
8585
8809
  hk_text = {'ask': 'ask (confirm unknown key)', 'accept-new': 'accept new hosts', 'off': 'OFF - no check (insecure)'}
8586
8810
  pre = ssh_prereq()
8587
8811
  status = ['']
@@ -8627,11 +8851,13 @@ if __name__=='__main__':
8627
8851
  screen_output(dy + 3, dx, '│' + (' ' + info)[:dw - 2].ljust(dw - 2) + '│', cls, 0)
8628
8852
  fw = dw - 16
8629
8853
  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])
8854
+ shown = ('*' * len(vals[i])) if keys[i] == 'secret' else (
8855
+ hk_text[vals[i]] + ' [ENTER/SPACE=change]' if keys[i] == 'hostkey' else
8856
+ nm_text[vals[i]] + ' [ENTER/SPACE=change]' if keys[i] == 'resolve' else vals[i])
8631
8857
  shown = shown[-fw:] if len(shown) > fw else shown
8632
8858
  line = ' ' + lab.ljust(10) + (shown + '_' * 0).ljust(fw) + ' '
8633
8859
  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)
8860
+ screen_output(dy + 11, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
8635
8861
  lines = []
8636
8862
  for t in status:
8637
8863
  lines.extend(wrap(t, dw - 4))
@@ -8639,18 +8865,18 @@ if __name__=='__main__':
8639
8865
  lines.extend(wrap(f, dw - 4))
8640
8866
  for i in range(4):
8641
8867
  txt = lines[i] if i < len(lines) else ''
8642
- screen_output(dy + 11 + i, dx, '│' + (' ' + txt)[:dw - 2].ljust(dw - 2) + '│', status_cls, 0)
8868
+ screen_output(dy + 12 + i, dx, '│' + (' ' + txt)[:dw - 2].ljust(dw - 2) + '│', status_cls, 0)
8643
8869
  bl = buttons()
8644
8870
  col = dx + 2
8645
- screen_output(dy + 15, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
8871
+ screen_output(dy + 16, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
8646
8872
  for i, (name, _a) in enumerate(bl):
8647
8873
  lab = ' [' + name + '] '
8648
8874
  hi = (focus == len(labels) + i)
8649
- screen_output(dy + 15, col, lab, 2 if hi else 1, 1 if hi else 0)
8875
+ screen_output(dy + 16, col, lab, 2 if hi else 1, 1 if hi else 0)
8650
8876
  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']:
8877
+ screen_output(dy + 17, dx, '│' + ' TAB/arrows=move ENTER=select ESC=close'.ljust(dw - 2) + '│', 1, 0)
8878
+ screen_output(dy + 18, dx, '└' + '─' * (dw - 2) + '┘', 1, 1)
8879
+ if focus < len(labels) and keys[focus] not in ('hostkey', 'resolve') and not busy['t']:
8654
8880
  curses.curs_set(1)
8655
8881
  shown_len = min(len(vals[focus]), fw)
8656
8882
  screen.move(dy + 4 + focus, dx + 12 + shown_len)
@@ -8722,6 +8948,8 @@ if __name__=='__main__':
8722
8948
  focus = (focus - 1) % nfocus
8723
8949
  elif ch in (10, 13, 32) and focus < len(labels) and keys[focus] == 'hostkey':
8724
8950
  vals[focus] = SSH_HOSTKEY_MODES[(SSH_HOSTKEY_MODES.index(vals[focus]) + 1) % 3]
8951
+ elif ch in (10, 13, 32) and focus < len(labels) and keys[focus] == 'resolve':
8952
+ vals[focus] = 'off' if vals[focus] == 'on' else 'on'
8725
8953
  elif ch in (10, 13):
8726
8954
  if focus < len(labels):
8727
8955
  focus = (focus + 1) % nfocus
@@ -8733,7 +8961,7 @@ if __name__=='__main__':
8733
8961
  status, status_cls = [pre['hint']], 3
8734
8962
  else:
8735
8963
  start(act)
8736
- elif focus < len(labels):
8964
+ elif focus < len(labels) and keys[focus] not in ('hostkey', 'resolve'):
8737
8965
  if ch in (curses.KEY_BACKSPACE, 127, 8):
8738
8966
  vals[focus] = vals[focus][:-1]
8739
8967
  elif 32 <= ch <= 126 and len(vals[focus]) < 255:
@@ -9529,7 +9757,8 @@ if __name__=='__main__':
9529
9757
  gn_thread, gn_result, gn_candidates = get_names_start(
9530
9758
  original_hosts_list, int(args.dns_ttl))
9531
9759
  if gn_thread is None:
9532
- notice('GET NAMES: NO ELIGIBLE IP HOST(S)', 2, 3)
9760
+ notice('GET NAMES: NOT AVAILABLE VIA JUMP HOST' if (ssh_target and not jr_active())
9761
+ else 'GET NAMES: NO ELIGIBLE IP HOST(S)', 2, 3)
9533
9762
  message = 'get names: no eligible ip host(s)'
9534
9763
  else:
9535
9764
  notice('GET NAMES: RUNNING IN BACKGROUND (%d HOST(S))'
@@ -10307,7 +10536,8 @@ if __name__=='__main__':
10307
10536
  gn_thread, gn_result, gn_candidates = get_names_start(
10308
10537
  original_hosts_list, int(args.dns_ttl))
10309
10538
  if gn_thread is None:
10310
- notice('GET NAMES: NO ELIGIBLE IP HOST(S)', 2, 3)
10539
+ notice('GET NAMES: NOT AVAILABLE VIA JUMP HOST' if (ssh_target and not jr_active())
10540
+ else 'GET NAMES: NO ELIGIBLE IP HOST(S)', 2, 3)
10311
10541
  else:
10312
10542
  notice('GET NAMES: RUNNING IN BACKGROUND (%d HOST(S))'
10313
10543
  % 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.4
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.04
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