chkip/chkip.py
Pascal Bouquet fb00a11bef Add --me flag to display local and public IPs with CGNAT detection
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.
2025-04-19 16:39:54 +02:00

295 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()