This commit introduces the --me flag, which allows users to quickly display their current local IPv4 and public IPv4/IPv6 addresses. The public IPs are retrieved using ifconfig.me via curl, ensuring accurate detection even in CGNAT or split-DNS scenarios. Local IPv4 is determined via the system's active route through the configured resolver. The output also includes a CGNAT detection based on private IP ranges and the reserved 100.64.0.0/10 block, providing a helpful warning if detected. IPv6 privacy extensions are common, so local IPv6 detection has been removed to avoid redundant or unstable information.
295 lines
9.3 KiB
Python
295 lines
9.3 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 args.me:
|
||
local_v4, public_v4, public_v6 = get_my_ip()
|
||
print(f"Your local IPv4: {local_v4}")
|
||
print(f"Your public IPv4: {public_v4}")
|
||
print(f"Your public IPv6: {public_v6}")
|
||
if is_cgnat(public_v4):
|
||
print("⚠️ Hinweis: Du befindest dich möglicherweise hinter CGNAT (Carrier-Grade NAT)")
|
||
return
|
||
|
||
if not args.domain:
|
||
parser.print_help()
|
||
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()
|