mirror of
https://github.com/notherealmarco/SLAACsense.git
synced 2025-03-13 05:19:10 +01:00
Initial commit
This commit is contained in:
parent
7cacb3532b
commit
1d7e5e7cc8
11 changed files with 246 additions and 1 deletions
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal 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
8
.idea/SLAACsense.iml
Normal 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>
|
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal 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
7
.idea/misc.xml
Normal 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
8
.idea/modules.xml
Normal 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
6
.idea/vcs.xml
Normal 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
7
Dockerfile
Normal 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"]
|
37
README.md
37
README.md
|
@ -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
16
docker-compose.yml
Normal 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
142
main.py
Normal 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
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
requests
|
||||
urllib3
|
Loading…
Reference in a new issue