Previously, running chkip without any arguments attempted to resolve None as a domain, resulting in misleading output like "No A record". This commit adds a check to print the usage line if neither a domain nor the --me flag is specified. This improves usability and makes the tool behave more predictably when called without arguments.
306 lines
9.6 KiB
Python
306 lines
9.6 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
chkip.py – DNS- und Mailserver-Check-Tool
|
||
|
||
Erstellt von Pascal Bouquet am 17.04.2025
|
||
Aktualisiert am 19.04.2025
|
||
|
||
Dieses Programm ist freie Software: Sie können es unter den Bedingungen der
|
||
GNU General Public License, wie von der Free Software Foundation veröffentlicht,
|
||
weitergeben und/oder modifizieren, entweder gemäß Version 3 der Lizenz oder
|
||
(nach Ihrer Wahl) jeder späteren Version.
|
||
|
||
Dieses Programm wird in der Hoffnung verbreitet, dass es nützlich sein wird,
|
||
aber OHNE JEDE GEWÄHRLEISTUNG – sogar ohne die implizite Gewährleistung der
|
||
MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. Siehe die GNU General
|
||
Public License für weitere Details.
|
||
|
||
Sie sollten eine Kopie der GNU General Public License zusammen mit diesem
|
||
Programm erhalten haben. Falls nicht, siehe <https://www.gnu.org/licenses/>.
|
||
"""
|
||
|
||
|
||
import sys
|
||
import re
|
||
import ipaddress
|
||
import dns.resolver
|
||
import dns.reversename
|
||
import requests
|
||
import argparse
|
||
import json
|
||
import socket
|
||
import subprocess
|
||
|
||
# Resolver festlegen
|
||
resolver = dns.resolver.Resolver()
|
||
resolver.nameservers = ['159.69.110.93', '1.1.1.1', '9.9.9.9']
|
||
resolver.timeout = 2
|
||
resolver.lifetime = 3
|
||
|
||
def is_ip_address(value):
|
||
try:
|
||
ipaddress.ip_address(value)
|
||
return True
|
||
except ValueError:
|
||
return False
|
||
|
||
def resolve_a(domain):
|
||
try:
|
||
return str(resolver.resolve(domain, 'A')[0])
|
||
except:
|
||
return None
|
||
|
||
def resolve_aaaa(domain):
|
||
try:
|
||
return str(resolver.resolve(domain, 'AAAA')[0])
|
||
except:
|
||
return None
|
||
|
||
def resolve_mx(domain):
|
||
try:
|
||
answers = resolver.resolve(domain, 'MX')
|
||
return sorted([(r.preference, str(r.exchange).rstrip('.')) for r in answers])
|
||
except:
|
||
return []
|
||
|
||
def get_ptr(ip):
|
||
try:
|
||
rev_name = dns.reversename.from_address(ip)
|
||
return str(resolver.resolve(rev_name, "PTR")[0]).rstrip('.')
|
||
except:
|
||
return None
|
||
|
||
def fcrdns_check(ip, ptr):
|
||
try:
|
||
resolved_ips = [str(r) for r in resolver.resolve(ptr, 'A')]
|
||
return 'ok' if ip in resolved_ips else f'failed ({", ".join(resolved_ips)})'
|
||
except:
|
||
return 'failed (no A record)'
|
||
|
||
def ipinfo_hostname(ip):
|
||
try:
|
||
r = requests.get(f"https://ipinfo.io/{ip}", timeout=2)
|
||
return r.json().get("hostname", "N/A")
|
||
except:
|
||
return "N/A"
|
||
|
||
def resolve_spf(domain):
|
||
try:
|
||
txt = resolver.resolve(domain, 'TXT')
|
||
for r in txt:
|
||
s = b''.join(r.strings).decode()
|
||
if s.startswith('v=spf1'):
|
||
return s
|
||
return "No SPF record found"
|
||
except:
|
||
return "SPF lookup failed"
|
||
|
||
def resolve_dmarc(domain):
|
||
try:
|
||
txt = resolver.resolve(f"_dmarc.{domain}", 'TXT')
|
||
return b''.join(txt[0].strings).decode()
|
||
except:
|
||
return "No DMARC record found"
|
||
|
||
def resolve_mta_sts(domain):
|
||
try:
|
||
txt = resolver.resolve(f"_mta-sts.{domain}", 'TXT')
|
||
return b''.join(txt[0].strings).decode()
|
||
except:
|
||
return "No MTA-STS record found"
|
||
|
||
def resolve_dkim(domain, selector):
|
||
try:
|
||
txt = resolver.resolve(f"{selector}._domainkey.{domain}", 'TXT')
|
||
return b''.join(txt[0].strings).decode()
|
||
except:
|
||
return f"No DKIM record found for selector '{selector}'"
|
||
|
||
def resolve_tlsa(domain, mx_host):
|
||
tlsa_name = f"_25._tcp.{mx_host}"
|
||
try:
|
||
results = resolver.resolve(tlsa_name, 'TLSA')
|
||
return [f"{r.usage} {r.selector} {r.mtype} {r.cert.hex()}" for r in results]
|
||
except:
|
||
return "No TLSA record found"
|
||
|
||
def get_local_ipv4():
|
||
try:
|
||
ip = next((ip for ip in resolver.nameservers if '.' in ip), '1.1.1.1')
|
||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
s.connect((ip, 80))
|
||
local_ip = s.getsockname()[0]
|
||
s.close()
|
||
return local_ip
|
||
except:
|
||
return "unavailable"
|
||
|
||
def get_my_ip():
|
||
def run_curl(protocol_flag):
|
||
try:
|
||
result = subprocess.run(
|
||
["curl", "-s", protocol_flag, "ifconfig.me/ip"],
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.DEVNULL,
|
||
timeout=2,
|
||
text=True
|
||
)
|
||
return result.stdout.strip()
|
||
except:
|
||
return "unavailable"
|
||
|
||
local_v4 = get_local_ipv4()
|
||
public_v4 = run_curl("-4")
|
||
public_v6 = run_curl("-6")
|
||
return local_v4, public_v4, public_v6
|
||
|
||
def is_cgnat(ip):
|
||
try:
|
||
ip_obj = ipaddress.ip_address(ip)
|
||
return ip_obj.is_private or ip_obj in ipaddress.ip_network("100.64.0.0/10")
|
||
except:
|
||
return False
|
||
|
||
def print_json(output):
|
||
print(json.dumps(output, indent=2))
|
||
|
||
def print_text(output, is_ip_mode=False):
|
||
if is_ip_mode:
|
||
print(f"PTR: {output.get('PTR')}")
|
||
print(f"rDNS: {output.get('rDNS')}")
|
||
print(f"FCrDNS: {output.get('FCrDNS')}")
|
||
else:
|
||
print(f"A: {output.get('A')}")
|
||
print(f"AAAA: {output.get('AAAA')}")
|
||
mx = output.get("MX")
|
||
if isinstance(mx, list):
|
||
print("MX:")
|
||
for entry in mx:
|
||
print(f" Host: {entry.get('host')}")
|
||
print(f" IP: {entry.get('ip')}")
|
||
print(f" PTR: {entry.get('ptr')}")
|
||
print(f" FCrDNS: {entry.get('fcrdns')}")
|
||
print()
|
||
elif isinstance(mx, str):
|
||
print(f"MX: {mx}")
|
||
print(f"rDNS: {output.get('rDNS')}")
|
||
if "SPF" in output:
|
||
print(f"SPF: {output['SPF']}")
|
||
if "DMARC" in output:
|
||
print(f"DMARC: {output['DMARC']}")
|
||
if "MTA-STS" in output:
|
||
print(f"MTA-STS: {output['MTA-STS']}")
|
||
if "TLSA" in output:
|
||
for item in output["TLSA"]:
|
||
for host, tlsa in item.items():
|
||
print(f"TLSA: _25._tcp.{host}")
|
||
if isinstance(tlsa, list):
|
||
for r in tlsa:
|
||
print(f" {r}")
|
||
else:
|
||
print(f" {tlsa}")
|
||
if "DKIM" in output:
|
||
print(f"DKIM: {output.get('DKIM')}")
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="chkip.py – DNS- und Mailserver-Check-Tool")
|
||
parser.add_argument("domain", nargs="?", help="Domain oder IP-Adresse")
|
||
parser.add_argument("-sS", "--spf", action="store_true", help="Prüft den SPF-Eintrag")
|
||
parser.add_argument("-sD", "--dmarc", action="store_true", help="Prüft den DMARC-Eintrag")
|
||
parser.add_argument("-sM", "--mta_sts", action="store_true", help="Prüft den MTA-STS-Eintrag")
|
||
parser.add_argument("-sT", "--tlsa", action="store_true", help="Prüft den TLSA-Eintrag")
|
||
parser.add_argument("-sDK", "--dkim", type=str, help="Prüft den DKIM-Eintrag für den angegebenen Selector")
|
||
parser.add_argument("--json", action="store_true", help="Ausgabe als JSON")
|
||
parser.add_argument("--me", action="store_true", help="Zeigt lokale & öffentliche IPs und prüft auf CGNAT")
|
||
args = parser.parse_args()
|
||
|
||
if not args.me and not args.domain:
|
||
print(parser.format_usage().strip())
|
||
return
|
||
|
||
if args.me:
|
||
local_v4, public_v4, public_v6 = get_my_ip()
|
||
result = {
|
||
"local_ipv4": local_v4,
|
||
"public_ipv4": public_v4,
|
||
"public_ipv6": public_v6
|
||
}
|
||
if is_cgnat(public_v4):
|
||
result["cgnat"] = True
|
||
|
||
if args.json:
|
||
print(json.dumps(result, indent=2))
|
||
else:
|
||
print(f"Your local IPv4: {result['local_ipv4']}")
|
||
print(f"Your public IPv4: {result['public_ipv4']}")
|
||
print(f"Your public IPv6: {result['public_ipv6']}")
|
||
if result.get("cgnat"):
|
||
print("⚠️ Hinweis: Du befindest dich möglicherweise hinter CGNAT (Carrier-Grade NAT)")
|
||
return
|
||
|
||
domain = args.domain
|
||
is_ip = is_ip_address(domain)
|
||
output = {}
|
||
|
||
if is_ip:
|
||
output["PTR"] = get_ptr(domain) or "No PTR record"
|
||
output["rDNS"] = ipinfo_hostname(domain)
|
||
ptr = output["PTR"]
|
||
if ptr and not ptr.startswith("No"):
|
||
resolved = resolve_a(ptr)
|
||
output["FCrDNS"] = "ok" if resolved == domain else f"failed ({resolved})"
|
||
else:
|
||
output["FCrDNS"] = "failed (no PTR)"
|
||
|
||
if args.json:
|
||
print_json(output)
|
||
else:
|
||
print_text(output, is_ip_mode=True)
|
||
return
|
||
|
||
# Domain-Zweig
|
||
output["A"] = resolve_a(domain) or "No A record"
|
||
output["AAAA"] = resolve_aaaa(domain) or "No AAAA record"
|
||
mx_records = resolve_mx(domain)
|
||
if not mx_records:
|
||
output["MX"] = "No MX record"
|
||
else:
|
||
mx_list = []
|
||
for pref, mx_host in mx_records:
|
||
host = mx_host.rstrip('.')
|
||
ip = resolve_a(host)
|
||
ptr = get_ptr(ip) if ip else None
|
||
fcr = fcrdns_check(ip, ptr) if ip and ptr else "N/A"
|
||
mx_list.append({
|
||
"host": host,
|
||
"ip": ip,
|
||
"ptr": ptr,
|
||
"fcrdns": fcr
|
||
})
|
||
output["MX"] = mx_list
|
||
|
||
output["rDNS"] = ipinfo_hostname(output["A"]) if output["A"] else None
|
||
if args.spf:
|
||
output["SPF"] = resolve_spf(domain)
|
||
if args.dmarc:
|
||
output["DMARC"] = resolve_dmarc(domain)
|
||
if args.mta_sts:
|
||
output["MTA-STS"] = resolve_mta_sts(domain)
|
||
if args.dkim:
|
||
output["DKIM"] = resolve_dkim(domain, args.dkim)
|
||
if args.tlsa and mx_records:
|
||
output["TLSA"] = []
|
||
for _, mx in mx_records:
|
||
host = mx.rstrip('.')
|
||
output["TLSA"].append({host: resolve_tlsa(domain, host)})
|
||
|
||
if args.json:
|
||
print_json(output)
|
||
else:
|
||
print_text(output, is_ip_mode=False)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|