Eigener DHCP-Server um die eigene Business-Logik abbilden zu können
Find a file
2026-06-22 19:17:11 +02:00
config Added TR-069 ACS URL and ProvisioningCode 2026-06-22 19:17:11 +02:00
db Protokollierung von Port-Changes durch beenden des alten Leases und erstellen eines neuen Leases 2026-06-15 09:09:10 +02:00
dhcp Added TR-069 ACS URL and ProvisioningCode 2026-06-22 19:17:11 +02:00
gentoo/etc Gentoo OpenRC hinzugefügt 2026-06-14 13:33:41 +02:00
mariadb_modules Initial commit 2026-06-14 10:09:55 +02:00
.gitignore Initial commit 2026-06-14 10:09:55 +02:00
dhcp-server.conf.example Added TR-069 ACS URL and ProvisioningCode 2026-06-22 19:17:11 +02:00
go.mod Initial commit 2026-06-14 10:09:55 +02:00
go.sum Initial commit 2026-06-14 10:09:55 +02:00
main.go Initial commit 2026-06-14 10:09:55 +02:00
README.md Added TR-069 ACS URL and ProvisioningCode 2026-06-22 19:17:11 +02:00

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 = NULL werden 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_map und dhcp_leases (siehe Datenbankschema)
  • MariaDB-Funktionen get_first_ipv6 und get_last_ipv6 müssen in der Datenbank vorhanden sein
  • Root-Rechte oder CAP_NET_BIND_SERVICE fü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, niemals lease_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