| config | ||
| db | ||
| dhcp | ||
| gentoo/etc | ||
| mariadb_modules | ||
| .gitignore | ||
| dhcp-server.conf.example | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| README.md | ||
dhcp-server
Ein hochperformanter DHCP-Server in Go, der vollständig auf MariaDB als Backend setzt – ohne eigenen IP-Cache. Entwickelt für den Einsatz hinter DHCP-Relay-Agents (RFC 3046 / Option 82).
Features
- Kein interner IP-Cache – alle Lease-Daten werden direkt aus der MariaDB gelesen und geschrieben
- DHCP-Relay-Unterstützung – vollständige Verarbeitung von Option 82 (circuit-id, remote-id)
- Statische Leases – Einträge mit
lease_end = NULLwerden niemals durch den DHCP-Server verändert - NAK bei ungültigem RENEW – Mismatch von IP und MAC führt zu DHCPNAK, der Client muss neu bootstrappen
- Parallele Verarbeitung – jedes eingehende Paket wird in einer eigenen Goroutine verarbeitet
- Temporäre IP-Reservierung – 15-Sekunden-Cache zwischen DISCOVER/OFFER und REQUEST/ACK verhindert Doppelvergabe
- TR-069 ACS-Provisioning – optionale DHCP Option 43 mit ACS-URL (Suboption 1) und ProvisioningCode (Suboption 2) gemäß TR-069-Spezifikation
Voraussetzungen
- Go 1.21+
- MariaDB mit den Tabellen
network_mapunddhcp_leases(siehe Datenbankschema) - MariaDB-Funktionen
get_first_ipv6undget_last_ipv6müssen in der Datenbank vorhanden sein - Root-Rechte oder
CAP_NET_BIND_SERVICEfür Port 67
Installation
git clone https://github.com/youruser/dhcp-server.git
cd dhcp-server
go build -o dhcp-server .
Konfiguration
Die Konfigurationsdatei wird standardmäßig als dhcp-server.conf im aktuellen Verzeichnis erwartet. Der Pfad kann mit -config überschrieben werden.
# IP-Adresse auf der der Server lauscht (Standard: 0.0.0.0)
bind_addr = 0.0.0.0
# Eigene Server-IP (wird als Server Identifier in DHCP-Paketen gesetzt)
server_ip = 192.168.1.1
# MariaDB DSN: user:password@tcp(host:port)/dbname
# Hinweis: parseTime=true wird automatisch angehängt
db_dsn = dhcp_user:secret@tcp(127.0.0.1:3306)/dhcp_db
# Lease-Dauer in Sekunden (Standard: 3600 = 1 Stunde)
lease_duration = 3600
# DNS-Server (kommagetrennt)
dns_servers = 8.8.8.8, 8.8.4.4
# TR-069 ACS-Server URL (DHCP Option 43, Suboption 1)
# Wird nur gesendet wenn angegeben.
acs_url = http://acs.example.com:7547
# TR-069 ProvisioningCode (DHCP Option 43, Suboption 2)
# Wird nur gesendet wenn angegeben.
provisioning_code = example
| Parameter | Pflicht | Standard | Beschreibung |
|---|---|---|---|
bind_addr |
Nein | 0.0.0.0 |
IP-Adresse, auf der der Server lauscht |
server_ip |
Ja* | – | Server-IP für DHCP Option 54; *Pflicht wenn bind_addr = 0.0.0.0 |
db_dsn |
Ja | – | MariaDB-Verbindungsstring |
lease_duration |
Nein | 3600 |
Lease-Dauer in Sekunden |
dns_servers |
Nein | 8.8.8.8, 8.8.4.4 |
DNS-Server für DHCP Option 6 |
acs_url |
Nein | – | TR-069 ACS-URL; wird als DHCP Option 43, Suboption 1 gesendet |
provisioning_code |
Nein | – | TR-069 ProvisioningCode; wird als DHCP Option 43, Suboption 2 gesendet |
Starten
# Direkt
sudo ./dhcp-server
# Mit alternativer Konfigurationsdatei
sudo ./dhcp-server -config /etc/dhcp-server/production.conf
Gentoo / OpenRC
Fertige Vorlagen liegen unter gentoo/:
# Dateien kopieren
cp gentoo/etc/init.d/dhcp-server /etc/init.d/dhcp-server
cp gentoo/etc/conf.d/dhcp-server /etc/conf.d/dhcp-server
# Binary und Konfiguration installieren
cp dhcp-server /usr/local/sbin/dhcp-server
mkdir -p /etc/dhcp-server
cp dhcp-server.conf /etc/dhcp-server/dhcp-server.conf
# → server_ip, db_dsn usw. anpassen
# Dienst aktivieren und starten
rc-update add dhcp-server default
rc-service dhcp-server start
Alle Pfade, Benutzer und die Log-Datei werden in /etc/conf.d/dhcp-server konfiguriert.
Standardmäßig läuft der Dienst als root. Um einen dedizierten Systembenutzer zu verwenden,
muss dem Binary die Capability CAP_NET_BIND_SERVICE gesetzt werden (Port 67):
emerge sys-libs/libcap
setcap cap_net_bind_service=+ep /usr/local/sbin/dhcp-server
Anschließend in /etc/conf.d/dhcp-server DHCP_SERVER_USER und DHCP_SERVER_GROUP anpassen.
systemd-Unit
[Unit]
Description=DHCP Server
After=network.target mariadb.service
Requires=mariadb.service
[Service]
ExecStart=/usr/local/sbin/dhcp-server -config /etc/dhcp-server/dhcp-server.conf
Restart=on-failure
RestartSec=5s
AmbientCapabilities=CAP_NET_BIND_SERVICE
User=dhcpd
[Install]
WantedBy=multi-user.target
Datenbankschema
network_map
Enthält das Netzwerk-Layout. IP-Adressen werden als IPv6-Binary (16 Byte) gespeichert. IPv4-Adressen folgen dem Format ::ffff:x.x.x.x.
CREATE TABLE `network_map` (
`ipaddress` binary(16) NOT NULL DEFAULT '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0',
`cidr` tinyint(1) UNSIGNED NOT NULL DEFAULT 0,
`type` enum('router','access-point','client-bridge','interconnection',
'dhcp-server','dns-server','voip-server','static-client',
'network','dhcp-range','broadcast') NOT NULL DEFAULT 'router',
`description` text DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_german1_ci
COMMENT='network-map' ROW_FORMAT=FIXED;
Relevante type-Werte für den DHCP-Server:
| Typ | Bedeutung |
|---|---|
router |
Gateway des Netzes; wird per giaddr gesucht um CIDR und Subnetzmaske zu ermitteln |
dhcp-range |
Einzelne IP, die für DHCP-Vergabe freigegeben ist |
Hinweis zum CIDR-Feld: Das cidr-Feld ist relativ zur IPv6-Notation. Umrechnung auf IPv4-CIDR: IPv4-CIDR = cidr - 96
| IPv6-CIDR | IPv4-CIDR | Subnetzmaske |
|---|---|---|
| 128 | /32 | 255.255.255.255 |
| 120 | /24 | 255.255.255.0 |
| 122 | /26 | 255.255.255.192 |
| 112 | /16 | 255.255.0.0 |
dhcp_leases
Speichert alle Lease-Einträge. Das Feld ip_address wird als Integer (INET_ATON) gespeichert.
CREATE TABLE `dhcp_leases` (
`mac_address` char(25) NOT NULL,
`user_id` int(9) UNSIGNED DEFAULT NULL,
`contract_id` int(9) UNSIGNED DEFAULT NULL,
`ip_address` int(10) UNSIGNED NOT NULL,
`dhcp_relay` int(11) UNSIGNED DEFAULT NULL,
`dhcp_circuit_id` char(255) DEFAULT NULL,
`dhcp_remote_id` char(255) DEFAULT NULL,
`hostname` char(255) DEFAULT NULL,
`lease_start` datetime NOT NULL,
`lease_end` datetime DEFAULT NULL,
`last_change` datetime NOT NULL
) ENGINE=Aria DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
| Feld | Beschreibung |
|---|---|
lease_end |
NULL = statischer Lease (wird vom DHCP-Server nie verändert) |
last_change |
Wird bei jeder Kommunikation mit dem Client aktualisiert |
dhcp_relay |
IP des DHCP-Relay-Agents (giaddr), gespeichert als INET_ATON |
dhcp_circuit_id |
Option 82, Sub-Option 1 |
dhcp_remote_id |
Option 82, Sub-Option 2 |
Ablauf
Client Relay DHCP-Server MariaDB
| | | |
|-- DISCOVER --->| | |
| |-- DISCOVER +82 ->| |
| | |-- GetExistingLease(mac)
| | |-- GetFreeIPs(giaddr)
| | | reserve(ip, mac, 15s)
| |<---- OFFER ------| |
|<-- OFFER ------| | |
| | | |
|-- REQUEST ---->| | |
| |-- REQUEST +82 -->| |
| | |-- GetLeaseByIP(ip)
| | |-- ACKLease(mac, ip)
| |<----- ACK -------| |
|<-- ACK --------| | |
| | | |
|-- RENEW ------>| | |
| |-- RENEW +82 ---->| |
| | |-- GetLeaseByIP(ip)
| | | MAC passt? → ACK / NAK
| |<--- ACK/NAK -----| |
|<-- ACK/NAK ----| | |
Statische Leases
Ein Eintrag in dhcp_leases mit lease_end = NULL gilt als statischer Lease. Der DHCP-Server:
- erkennt den Eintrag und antwortet mit ACK
- aktualisiert nur
last_change, niemalslease_end - gibt die IP nicht an andere Clients weiter
MariaDB UDF: get_first_ipv6 / get_last_ipv6
Der Server benötigt die benutzerdefinierten Funktionen get_first_ipv6 und get_last_ipv6, um aus einer IPv6-Adresse und einem CIDR-Wert den Netzwerkbereich zu berechnen.
Warum C-UDF? MariaDB besitzt keinen nativen 128-Bit-Integer-Typ. Reine SQL-Implementierungen dieser Funktionen scheitern daran, dass bitweise Operationen in SQL auf 64 Bit beschränkt sind. Die UDF in mariadb_modules/ipv6udf.c arbeitet direkt auf dem 16-Byte-Binärpuffer und umgeht diese Einschränkung.
Funktionsweise
| Funktion | Berechnung |
|---|---|
get_first_ipv6(ip, cidr) |
ip & cidr_mask(cidr) → Netzwerkadresse |
get_last_ipv6(ip, cidr) |
first_ip | ~cidr_mask(cidr) → Broadcast-/letzte Adresse |
Kompilieren und installieren
# Abhängigkeiten (Debian/Ubuntu)
apt install gcc libmariadb-dev
# Kompilieren
gcc -shared -fPIC -o ipv6udf.so mariadb_modules/ipv6udf.c \
$(mariadb_config --cflags)
# In das MariaDB-Plugin-Verzeichnis kopieren
cp ipv6udf.so $(mariadb_config --plugindir)/
# Funktionen in MariaDB registrieren
mariadb -u root -p <<'SQL'
CREATE FUNCTION get_first_ipv6 RETURNS STRING SONAME 'ipv6udf.so';
CREATE FUNCTION get_last_ipv6 RETURNS STRING SONAME 'ipv6udf.so';
SQL
Deinstallieren
DROP FUNCTION IF EXISTS get_first_ipv6;
DROP FUNCTION IF EXISTS get_last_ipv6;
Anschließend ipv6udf.so manuell aus dem Plugin-Verzeichnis entfernen.
Projektstruktur
dhcp-server/
├── main.go # Einstiegspunkt, -config Flag
├── dhcp-server.conf # Konfigurationsvorlage
├── go.mod
├── config/
│ └── config.go # Konfiguration laden und validieren
├── db/
│ ├── db.go # Alle MariaDB-Abfragen (kein SQL außerhalb dieser Datei)
│ └── dsl.go # DSLStats-Struct + InsertDSLStats (TR-101 Leitungswerte)
├── dhcp/
│ ├── cache.go # 15-Sekunden IP-Reservierungs-Cache (in-memory)
│ ├── handler.go # DHCP-Nachrichtenverarbeitung (DISCOVER/REQUEST/RELEASE)
│ ├── option82.go # Option-82-Parser + TR-101 TLV-Decoder (Nokia DSLAM)
│ └── server.go # UDP-Listener auf Port 67
├── mariadb_modules/
│ └── ipv6udf.c # C-UDF: get_first_ipv6 / get_last_ipv6 für MariaDB
└── gentoo/
└── etc/
├── init.d/
│ └── dhcp-server # OpenRC init-Script
└── conf.d/
└── dhcp-server # OpenRC Konfiguration (Pfade, User, Log)
Lizenz
MIT