From 1472938a301d6216407be9b1d7a1a83ab61339f4 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 30 Mar 2019 16:23:24 +0000 Subject: [PATCH] init repo --- README.md | 33 ++++ scanner.py | 93 ++++++++++ server.py | 420 +++++++++++++++++++++++++++++++++++++++++++ settings.json.sample | 69 +++++++ 4 files changed, 615 insertions(+) create mode 100644 README.md create mode 100644 scanner.py create mode 100644 server.py create mode 100644 settings.json.sample diff --git a/README.md b/README.md new file mode 100644 index 0000000..157c5a5 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +t] +Description=Address Python Server +After=syslog.target + +[Service] +Type=simple +User=addressserver +Group=addressserver +WorkingDirectory=/home/addressserver/address_scanner +ExecStart=/usr/bin/python3 /home/addressserver/address_scanner/server.py +SyslogIdentifier=address_server +StandardOutput=syslog +StandardError=syslog +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +``` + +``` +apt-get install python3-pip +pip3 install netifaces paho-mqtt + +*/2 * * * * /usr/bin/python3 /root/scanner.py -i eth0 -4 192 -6 2a02 --host 192.168.25.221 --username mc8051 --password XXXXXXX -q + +``` + + +``` +https://www.thomaschristlieb.de/ein-python-script-mit-systemd-als-daemon-systemd-tut-garnicht-weh/ +https://stackoverflow.com/a/47370156 +``` diff --git a/scanner.py b/scanner.py new file mode 100644 index 0000000..545d3a9 --- /dev/null +++ b/scanner.py @@ -0,0 +1,93 @@ +import netifaces +import optparse +import sys +import socket +import time +import paho.mqtt.client as mqtt + +is_connected = False + +def on_connect(client, userdata, flags, rc): + global is_connected + if(rc == 0): + is_connected = True + +hostname = socket.gethostname() + +parser = optparse.OptionParser() + +parser.add_option('-i', '--interface', action="store", dest="interface", help="Network Interface", default="") +parser.add_option('-q', '--quite', action="store_true", dest="quite", help="No output") +parser.add_option('-4', '--match4', action="store", dest="match4", help="IPv4 MUST start with this argument", default="") +parser.add_option('-6', '--match6', action="store", dest="match6", help="IPv6 MUST start with this argument", default="") +parser.add_option('--host', action="store", dest="mqqt_host", help="MQTT Host", default="") +parser.add_option('--port', action="store", dest="mqqt_port", help="MQTT Port", default="1883") +parser.add_option('--username', action="store", dest="mqqt_user", help="MQTT Username (optional)", default="") +parser.add_option('--password', action="store", dest="mqqt_pass", help="MQTT Password (optional)", default="") + +options, args = parser.parse_args() + +client_enabled = not(not options.mqqt_host) + +if client_enabled: + client = mqtt.Client() + client.connect(options.mqqt_host, int(options.mqqt_port), 60) + + if options.mqqt_user and options.mqqt_pass: + client.username_pw_set(username=options.mqqt_user, password=options.mqqt_pass) + + client.on_connect = on_connect + client.loop_start() +else: + print("Warning: no MQTT server defined") + +if client_enabled: + timeout = 10 + while (not is_connected) and (timeout > 0): + timeout -= 1 + time.sleep(0.5) + + if timeout <= 0: + print("MQTT timeout") + sys.exit(-1) + +try: + addrs = netifaces.ifaddresses(options.interface) +except ValueError as ex: + print("Invalid interface " + options.interface) + sys.exit(-1) + +if not options.quite: + print("Checking network interface " + options.interface + " on " + hostname) + +if not options.match4 and not options.quite: + print("Warning: no IPv4-Match set") + +if not options.match6 and not options.quite: + print("Warning: no IPv6-Match set") + + +ipv4 = "" +for addr in addrs[netifaces.AF_INET]: + if addr["addr"].startswith(options.match4): + ipv4 = addr["addr"] + break + + +ipv6 = "" +for addr in addrs[netifaces.AF_INET6]: + if addr["addr"].startswith(options.match6): + ipv6 = addr["addr"] + break + + +if not options.quite: + print("") + print("Found IPv4 " + ipv4 + " and IPv6 " + ipv6) + +if client_enabled: + client.publish("network/" + hostname + "/hostname", hostname) + client.publish("network/" + hostname + "/ipv4", ipv4) + client.publish("network/" + hostname + "/ipv6", ipv6) + + client.loop_stop() \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..d98909c --- /dev/null +++ b/server.py @@ -0,0 +1,420 @@ +import schedule +import time +import paho.mqtt.client as mqtt +from urllib.parse import urlparse +import requests +import json +import socket +import CloudFlare +import logging +import smtplib +import ssl +import io + +logger = logging.getLogger('dns_updater') +logger.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%m/%d/%Y:%H:%M:%S') + + +ch = logging.StreamHandler() +ch.setFormatter(formatter) +logger.addHandler(ch) + +log_capture_string = io.StringIO() +ch2 = logging.StreamHandler(log_capture_string) +ch2.setFormatter(formatter) +logger.addHandler(ch2) + +OPENWRT_PATH = "/cgi-bin/luci/" +INTERVAL = 60 +userdata = dict() +mqtt_data = dict() +mail_data = dict() +openwrt = dict() +hosts = dict() +dns = dict() +firewall = dict() + +def readFile(): + with open("settings.json") as json_file: + global userdata + global hosts + global dns + global firewall + global mqtt_data + global mail_data + global openwrt + global OPENWRT_PATH + global INTERVAL + data = json.load(json_file) + + userdata = {"apikey": data["apikey"], "username": data["user"]} + INTERVAL = data["interval"] + + mqtt_data = data["mqtt"] + mail_data = data["mail"] + openwrt = data["openwrt"] + OPENWRT_PATH = "http://" + openwrt["host"] + OPENWRT_PATH + + if "hosts" in data: + z = hosts.copy() + z.update(data["hosts"]) + hosts = z + + if "dns" in data: + z = dns.copy() + z.update(data["dns"]) + dns = z + + if "firewall" in data: + z = firewall.copy() + z.update(data["firewall"]) + firewall = z + +readFile() + +def saveFile(): + logger.info("Saving settings.json file...") + with open('settings.json', 'wt') as out: + global userdata + global hosts + global dns + global INTERVAL + obj = { + "apikey": userdata["apikey"], + "user": userdata["username"], + "interval": INTERVAL, + "dns": dns, + "hosts": hosts, + "firewall": firewall, + "mqtt": mqtt_data, + "openwrt": openwrt, + "mail": mail_data, + } + res = json.dump(obj, out, sort_keys=True, indent=4, separators=(',', ': ')) + logger.info("File saved") + +schedule.every(2).minutes.do(saveFile) + + +def getPublicIP(): + r = requests.get("https://ipinfo.io/") + data = json.loads(r.text) + + return data.get("ip") + +def do_dns_update(cf, zone_name, zone_id, dns_name, ip_address, ip_address_type): + """Cloudflare API code - example""" + logger.info("Update %s to %s" % (dns_name+"."+zone_name, ip_address)) + + try: + prefix = "" if dns_name == "@" else dns_name+"." + params = {'name':prefix+zone_name, 'match':'all', 'type':ip_address_type} + dns_records = cf.zones.dns_records.get(zone_id, params=params) + except CloudFlare.exceptions.CloudFlareAPIError as e: + logger.error('/zones/dns_records %s - %d %s - api call failed' % (dns_name, e, e)) + + updated = False + changed = False + + # update the record - unless it's already correct + for dns_record in dns_records: + old_ip_address = dns_record['content'] + old_ip_address_type = dns_record['type'] + + if ip_address_type not in ['A', 'AAAA']: + # we only deal with A / AAAA records + continue + + if ip_address_type != old_ip_address_type: + # only update the correct address type (A or AAAA) + # we don't see this becuase of the search params above + logger.debug('IGNORED: %s %s ; wrong address family' % (dns_name, old_ip_address)) + continue + + if ip_address == old_ip_address: + logger.debug('UNCHANGED: %s %s' % (dns_name, ip_address)) + updated = True + continue + + # Yes, we need to update this record - we know it's the same address type + + dns_record_id = dns_record['id'] + dns_record = { + 'name':dns_name, + 'type':ip_address_type, + 'content':ip_address + } + try: + dns_record = cf.zones.dns_records.put(zone_id, dns_record_id, data=dns_record) + changed = True + except CloudFlare.exceptions.CloudFlareAPIError as e: + logger.error('/zones.dns_records.put %s - %d %s - api call failed' % (dns_name, e, e)) + logger.debug('UPDATED: %s %s -> %s' % (dns_name, old_ip_address, ip_address)) + updated = True + + if updated: + return changed + + # no exsiting dns record to update - so create dns record + dns_record = { + 'name':dns_name, + 'type':ip_address_type, + 'content':ip_address + } + try: + dns_record = cf.zones.dns_records.post(zone_id, data=dns_record) + changed = True + except CloudFlare.exceptions.CloudFlareAPIError as e: + logger.error('/zones.dns_records.post %s - %d %s - api call failed' % (dns_name, e, e)) + logger.debug('CREATED: %s %s' % (dns_name, ip_address)) + return changed + + +if openwrt["enabled"] == True: + jar = requests.cookies.RequestsCookieJar() + fw_login = requests.get(OPENWRT_PATH + "/rpc/auth", cookies=jar, json={ + "id": 1, + "method": "login", + "params": [ + openwrt["username"], + openwrt["password"] + ] + }) + + if "result" not in fw_login.json() or fw_login.json()["result"] is None: + exit("Incorrect OpenWrt Login") + + o = urlparse(OPENWRT_PATH) + jar.set('sysauth', fw_login.json()["result"], domain=o.netloc, path=o.path) + +def setFirewall(section, key, new_ipv6): + global jar + + logger.info("Updating firewall " + section + "." + key + "=" + new_ipv6) + + if not new_ipv6: + logger.debug("Empty IPv6... Skipping...") + return + + r = requests.get(OPENWRT_PATH + "/rpc/uci", cookies=jar, json={ + "id": 1, + "method": "get_all", + "params": [ + "firewall", + section + ] + }) + data = r.json() + + if not "result" in data or data["result"] is None: + logger.warning("Unknown firewall section %s... Skipping..." % (section)) + return + + result = data["result"] + + if not "family" in result or result["family"] is None: + logger.debug("No family set in firewall section %s... Skipping..." % (section)) + return + + if not key in result or result[key] is None: + logger.debug("No %s set in firewall section %s... Skipping..." % (key, section)) + return + + if result["family"] != "ipv6": + logger.debuginfo("Section %s is no ipv6... Skipping..." % (section)) + return + + if result[key] == new_ipv6: + logger.debug("Section %s has same ipv6... Skipping..." % (section)) + return + + r = requests.get(OPENWRT_PATH + "/rpc/uci", cookies=jar, json={ + "id": 1, + "method": "set", + "params": [ + "firewall", + section, + key, + new_ipv6 + ] + }) + logger.debug("updated = %s" % r.json()["result"]) + return True + +def commitFirewall(): + global jar + r = requests.get(OPENWRT_PATH + "/rpc/uci", cookies=jar, json={ + "id": 1, + "method": "commit", + "params": [ + "firewall", + ] + }) + +def updateFirewall(): + global hosts + global dns + global firewall + global commitFirewall + global setFirewall + logger.info("Updating firewall") + changed = False + for section in firewall: + for key in firewall[section]: + value = firewall[section][key] + ipv6 = hosts.get(value, dict()).get("ipv6", "") + changed = setFirewall(section, key, ipv6) + if changed == True: + logger.info("Firewall set... Commiting...") + commitFirewall() + logger.info("Firewall commited") + else: + logger.info("Nothing changed. Skipping firewall commit...") + return changed + +def updateDNS(): + global hosts + global userdata + global dns + global getPublicIP + global do_dns_update + + logger.info("Updating DNS") + + + cf = CloudFlare.CloudFlare(email=userdata["username"], token=userdata["apikey"]) + + PUBLIC = getPublicIP() + if not PUBLIC or PUBLIC is None: + logger.error("EMPTY PUBLIC IP?!") + return False + + changed = False + + for domain in dns: + params = {'name':domain} + zones = cf.zones.get(params=params) + + zone = zones[0] + zone_name = zone['name'] + zone_id = zone['id'] + for subdomain in dns[domain]: + ipv4_host = dns[domain][subdomain].get("ipv4", "") + ipv6_host = dns[domain][subdomain].get("ipv6", "") + + if ipv4_host.lower() == "public": + ipv4 = PUBLIC + else: + ipv4 = hosts.get(ipv4_host, dict()).get("ipv4", "") + + ipv6 = hosts.get(ipv6_host, dict()).get("ipv6", "") + + dns_records = [] + + if ipv4: + if do_dns_update(cf, zone_name, zone_id, subdomain, ipv4, "A"): + changed = True + + if ipv6: + if do_dns_update(cf, zone_name, zone_id, subdomain, ipv6, "AAAA"): + changed = True + return changed + +def sendMail(mail_data, title, content): + logger.info("Sending mail to %s" % mail_data["receipent"]) + # Create a secure SSL context + context = ssl.create_default_context() + + # Try to log in to server and send email + try: + server = smtplib.SMTP(mail_data["host"], mail_data["port"]) + server.ehlo() # Can be omitted + server.starttls(context=context) # Secure the connection + server.ehlo() # Can be omitted + server.login(mail_data["username"], mail_data["password"]) + + message = "From: " + mail_data["username"] + "\n" + message += "To: " + mail_data["receipent"] + "\n" + message += "Subject: "+title+"\n" + message += "\n" + message += content + + server.sendmail(mail_data["username"], mail_data["receipent"], message) + logger.debug("Mail send") + except Exception as e: + logger.error(e) + finally: + server.quit() + +def updateAll(): + global updateDNS + global updateFirewall + global openwrt + global log_capture_string + global mail_data + + log_capture_string.truncate(0) + log_capture_string.seek(0) + + changed = False + + if openwrt["enabled"] == True: + if updateFirewall(): + changed = True + else: + logger.info("Firewall update is disabled") + + if updateDNS(): + changed = True + + if mail_data and "enabled" in mail_data and mail_data["enabled"] == True: + if changed == True: + sendMail(mail_data, "Info: DNS/Firewall Address Server", log_capture_string.getvalue()) + else: + logger.debug("No email. nothing changed") + else: + logger.info("Email is disabled") + +schedule.every(INTERVAL).seconds.do(updateAll) + + + +def on_connect(client, userdata, flags, rc): + logger.debug("Connected with result code "+str(rc)) + if rc != 0: + logger.error("ERROR: Please check MQTT Login") + client.subscribe("network/+/hostname") + client.subscribe("network/+/ipv4") + client.subscribe("network/+/ipv6") + +def on_message(client, userdata, msg): + global hosts + pl = str((msg.payload).decode("utf-8")).lower() + logger.debug(msg.topic + ": " + pl) + + if msg.topic.startswith("network/") and (msg.topic.endswith("/ipv4") or msg.topic.endswith("/ipv6")): + host = msg.topic.split("/")[1] + + if not host in hosts: + hosts[host] = dict() + + hosts[host]["ipv4" if msg.topic.endswith("/ipv4") else "ipv6"] = pl + +client = mqtt.Client() +client.username_pw_set(username=mqtt_data["username"], password=mqtt_data["password"]) +client.connect(mqtt_data["host"], mqtt_data["port"], 60) +client.on_connect = on_connect +client.on_message = on_message + +client.loop_start() + +try: + while True: + schedule.run_pending() + time.sleep(1) +except KeyboardInterrupt as ex: + saveFile() + +# https://htmlpreview.github.io/?https://raw.githubusercontent.com/openwrt/luci/master/documentation/api/modules/luci.model.uci.html#Cursor.get +# https://wiki.teltonika.lt/view/UCI_command_usage \ No newline at end of file diff --git a/settings.json.sample b/settings.json.sample new file mode 100644 index 0000000..a764246 --- /dev/null +++ b/settings.json.sample @@ -0,0 +1,69 @@ +{ + "apikey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "dns": { + "domain2.com": { + "@": { + "ipv4": "public", + "ipv6": "proxy" + }, + "playing": { + "ipv6": "playground" + } + }, + "domain2.com": { + "test": { + "ipv4": "public", + "ipv6": "proxy" + } + } + }, + "firewall": { + "cf2bd": { + "dest_ip": "proxy" + }, + "cfgbd": { + "dest_ip": "proxy" + }, + "cfgd": { + "dest_ip": "plex" + } + }, + "hosts": { + "playground": { + "ipv4": "192.168.1.12", + "ipv6": "2a02:908::8b0b" + }, + "plex": { + "ipv4": "192.168.1.11", + "ipv6": "2a02:908::d237" + }, + "proxy": { + "ipv4": "192.168.1.10", + "ipv6": "2a02:908::9867" + } + }, + "interval": 600, + "mail": { + "enabled": true, + "host": "mail.example.com", + "password": "", + "port": 587, + "receipent": "user@example.com", + "username": "dns-updater@example.com" + }, + "mqtt": { + "_comment": "Please secure your MQTT server", + "host": "192.168.1.2", + "password": "", + "port": 1883, + "username": "mc8051" + }, + "openwrt": { + "_comment": "Please install luci JSON rpc package", + "enabled": true, + "host": "192.168.1.1", + "password": "", + "username": "root" + }, + "user": "user@example.com" +} \ No newline at end of file