Initial commit

This commit is contained in:
Marco Realacci 2023-12-07 00:31:47 +01:00
parent 7cacb3532b
commit 1d7e5e7cc8
11 changed files with 246 additions and 1 deletions

8
.idea/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/SLAACsense.iml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/SLAACsense.iml" filepath="$PROJECT_DIR$/.idea/SLAACsense.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

7
Dockerfile Normal file
View file

@ -0,0 +1,7 @@
FROM python:latest
LABEL authors="notherealmarco"
WORKDIR /app/
COPY . .
RUN pip install -r requirements.txt
ENTRYPOINT ["python3", "/app/main.py"]

View file

@ -1 +1,36 @@
# SLAACsense
# SLAACsense
SLAACsense streamlines the process of configuring DNS records on OPNsense routers using Technitium DNS Server.
Designed to enhance network management, the tool automatically defines DNS A, AAAA, and PTR records for each device connected to the network based on its DHCPv4 hostname.
By leveraging the DHCPv4 lease information and mapping it to the MAC address, the tool navigates the NDP table to retrieve IPv6 addresses associated with each host. Subsequently, it configures the DNS records accordingly, providing a seamless solution for maintaining an up-to-date and accurate DNS configuration.
## Usage:
Define the environment variables in the docker-compose file, then run: `docker compose up -d`
### Environment variables:
| Variable Name | Description | Example Value |
|-----------------------|------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| `OPNSENSE_URL` | The base URL of your OPNsense instance | http://192.168.1.1 (required)
| `OPNSENSE_API_KEY` | OPNsense API key | `your_opnsense_api_key` (required) |
| `OPNSENSE_API_SECRET` | OPNsense API secret | `a_very_secret_token` (required) |
| `TECHNITIUM_URL` | The base URL of your Technitium DNS instance | `dns.myawesomehome.home.arpa` (required) |
| `TECHNITIUM_TOKEN` | Technitium DNS token | `another_very_secret_token` (required) |
| `DNS_ZONE_SUBNETS` | Comma separated DNS zones and IPv4 subnet | `192.168.1.0/24=lan.home.arpa,192.168.2.0/24=dmz.home.arpa` (required) |
| `DO_V4` | If set to true, A records will be configured, otherwise only AAAA records are configured | `false` (defaults to false) |
| `VERIFY_HTTPS` | Verify OPNsense and Technitium's SSL certificates | `true` (defaults to true) |
| `CLOCK` | Interval between updates (in seconds) | `30` (defaults to 30) |
### Note
You have to create the corresponding DNS zones in the Technitium dashboard, you can configure them as primary or conditional forwarder zones.
### Contributing:
I welcome contributions! Feel free to submit issues, feature requests, or pull requests.
For example, you may add the support for other DNS servers, like Bind, and other routing platforms, like pfSense and OpenWrt.
### License:
This tool is released under the MIT license. See the LICENSE file for details.

16
docker-compose.yml Normal file
View file

@ -0,0 +1,16 @@
version: '3.6'
services:
slaacsense:
build: .
container_name: slaacsense
restart: unless-stopped
environment:
- OPNSENSE_URL=${OPNSENSE_URL}
- OPNSENSE_API_KEY=${OPNSENSE_API_KEY}
- OPNSENSE_API_SECRET=${OPNSENSE_API_SECRET}
- TECHNITIUM_URL=${TECHNITIUM_URL}
- TECHNITIUM_TOKEN=${TECHNITIUM_TOKEN}
- DNS_ZONE_SUBNETS=${DNS_ZONE_SUBNETS}
- DO_V4=${DO_V4}
- VERIFY_HTTPS=${VERIFY_HTTPS}
- CLOCK=${CLOCK}

142
main.py Normal file
View file

@ -0,0 +1,142 @@
import logging
import os
import time
import requests
import ipaddress
import urllib3
OPNSENSE_URL = ""
OPNSENSE_API_KEY = ""
OPNSENSE_API_SECRET = ""
TECHNITIUM_URL = ""
TECHNITIUM_TOKEN = ""
IPV6_PREFIX = ""
DNS_ZONE_SUBNETS = ""
DO_V4 = False
VERIFY_HTTPS = False
CLOCK = 30
def get_opnsense_data(path):
r = requests.get(url=OPNSENSE_URL + path, verify=VERIFY_HTTPS, auth=(OPNSENSE_API_KEY, OPNSENSE_API_SECRET))
if r.status_code != 200:
logging.error("Error occurred" + str(r.status_code) + ": " + r.text)
return None
return r.json()
def get_ndp():
return get_opnsense_data("/api/diagnostics/interface/search_ndp")
def get_dhcp4_leases():
return get_opnsense_data("/api/dhcpv4/leases/searchLease")
def build_matches(ndp, leases):
matches = set()
for e in leases["rows"]:
ip6s = tuple(x["ip"].split("%")[0] for x in ndp["rows"] if x["mac"] == e["mac"])
if len(ip6s) == 0:
continue
matches.add((e["address"], ip6s, e["hostname"]))
return matches
def find_zone(zones, ip4):
for zone in zones:
if ip4 in zone[0]: return zone[1]
return None
def make_record(zones, match):
zone = find_zone(zones, ipaddress.ip_address(match[0]))
if zone is None:
logging.warning("Could not find a DNS zone for " + match[0])
return
ip4 = match[0]
ip6s = [ipaddress.ip_address(x) for x in match[1]]
hostname = match[2]
if hostname == "":
logging.warning("no hostname found for " + match[0])
return
for ip6 in ip6s:
v6path = "/api/zones/records/add?token=" + TECHNITIUM_TOKEN + "&domain=" + hostname + "." + zone + "&type=AAAA&ttl=1&overwrite=true&ptr=true&ipAddress=" + ip6.exploded
logging.info("Inserting AAAA: " + hostname + "." + zone + " " + ip6.compressed)
r = requests.get(url=TECHNITIUM_URL + v6path, verify=VERIFY_HTTPS)
if r.status_code != 200:
logging.error("Error occurred" + str(r.status_code) + ": " + r.text)
continue
if DO_V4:
v4path = "/api/zones/records/add?token=" + TECHNITIUM_TOKEN + "&domain=" + hostname + "." + zone + "&type=A&ttl=1&overwrite=true&ptr=true&ipAddress=" + ip4
logging.info("Inserting A: " + hostname + "." + zone + " " + ip4)
r = requests.get(url=TECHNITIUM_URL + v4path, verify=VERIFY_HTTPS)
if r.status_code != 200:
logging.error("Error occurred" + str(r.status_code) + ": " + r.text)
def run():
if not VERIFY_HTTPS:
urllib3.disable_warnings()
previous_matches = set()
zones = []
for z in DNS_ZONE_SUBNETS.split(","):
zone = z.split("=")
zones.append((ipaddress.ip_network(zone[0]), zone[1]))
while True:
ndp = get_ndp()
if ndp is None:
logging.error("Error retrieving NDP table")
continue
leases = get_dhcp4_leases()
if leases is None:
logging.error("Error retrieving DHCPv4 leases")
continue
matches = build_matches(ndp, leases)
new_matches = matches - previous_matches
previous_matches = matches
for match in new_matches:
make_record(zones, match)
time.sleep(CLOCK)
def verify_env() -> bool:
if not OPNSENSE_URL: return False
if not OPNSENSE_API_KEY: return False
if not OPNSENSE_API_SECRET: return False
if not TECHNITIUM_URL: return False
if not TECHNITIUM_TOKEN: return False
if not DNS_ZONE_SUBNETS: return False
return True
if __name__ == "__main__":
logging.getLogger().setLevel(os.getenv("LOG_LEVEL", "INFO"))
logging.info("loading environment...")
OPNSENSE_URL = os.getenv("OPNSENSE_URL", None)
OPNSENSE_API_KEY = os.getenv("OPNSENSE_API_KEY", None)
OPNSENSE_API_SECRET = os.getenv("OPNSENSE_API_SECRET", None)
TECHNITIUM_URL = os.getenv("TECHNITIUM_URL", None)
TECHNITIUM_TOKEN = os.getenv("TECHNITIUM_TOKEN", None)
DNS_ZONE_SUBNETS = os.getenv("DNS_ZONE_SUBNETS", None)
DO_V4 = (os.getenv("DO_V4", "false").lower() == "true")
VERIFY_HTTPS = (os.getenv("VERIFY_HTTPS", "true").lower() == "true")
CLOCK = int(os.getenv("CLOCK", "30"))
if not verify_env():
logging.error("Missing mandatory environment variables")
exit(0)
logging.info("Starting SLAACsense...")
logging.info("OPNSENSE_URL: {}".format(OPNSENSE_URL))
logging.info("TECHNITIUM_URL: {}".format(TECHNITIUM_URL))
logging.info("VERIFY_HTTPS: {}".format(VERIFY_HTTPS))
run()

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
requests
urllib3