Skip to content
dhtechPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

4 Commits

Folders and files

Repository files navigation

lease2db

lease2db follows the lease file of an ISC DHCP server (dhcpd.leases) and keeps the history of who held which address, and when in a PostgreSQL database.

ISC dhcpd only knows the present: its lease file says who has an address now, and forgets everybody who had it before. lease2db turns that file into a history you can ask questions of:

  • Who had 192.0.2.57 last Tuesday at 21:40?
  • Which addresses has this MAC address had, and on which switch port?
  • How full is every network right now?

It is one static binary. It does not talk to dhcpd, does not change its configuration and does not need to run as root: it only reads two files.

How it works

dhcpd appends a lease <ip> { ... } block to its lease file every time something happens to a lease, and now and then rewrites the whole file with one block per lease. lease2db reads the blocks as they appear and keeps one row for every time a client held an address - from the moment it got it until it gave it up, lost it, or somebody else got it:

  • a renewal extends the row; it does not make a new one
  • a release, an expiry or another client taking the address ends the row
  • the same client coming back later starts a new row

Because the lease file repeats itself (after every rewrite, and whenever lease2db starts), every block is compared with what is already stored. Reading the same file any number of times gives the same rows, so the program can be stopped, restarted or moved to another machine at any time. If the database is away, nothing is lost: the file is still there, and lease2db catches up when the database is back.

Install

Download the archive for your machine from the releases page and check it against SHA256SUMS:

tar xzf lease2db_<version>_linux_amd64.tar.gz
install -m 755 lease2db_<version>_linux_amd64/lease2db /usr/local/bin/
lease2db -version

The released binaries are built with the latest stable Go. Or build it yourself (Go 1.25 or newer):

go build ./cmd/lease2db

The database

lease2db creates and updates its own tables. All it needs is a PostgreSQL database and a role that may create tables in it:

CREATE ROLE lease2db LOGIN PASSWORD '...';
CREATE DATABASE dhcp OWNER lease2db;

The tests run against PostgreSQL 17.

Run

lease2db -dsn 'host=db.example.net dbname=dhcp user=lease2db password=... sslmode=verify-full' \
         -leases /var/lib/dhcp/dhcpd.leases \
         -scopes /etc/dhcp/dhcpd.conf
Flag Default
-dsn $LEASE2DB_DSN PostgreSQL connection string or postgres:// URL
-leases /var/lib/dhcp/dhcpd.leases dhcpd's lease file
-scopes (none) dhcpd configuration file with the subnet declarations
-interval 1s how often to look for new leases
-metrics (none) write Prometheus metrics to this file
-version print the version and exit

Run it on the machine where dhcpd runs, as any user that can read the two files. An example systemd unit is in contrib/. Prefer LEASE2DB_DSN in an environment file over -dsn when the connection string holds a password: command lines are visible to every user of the machine.

Connecting without a password

The connection string takes the usual sslmode, sslrootcert, sslcert and sslkey settings, so lease2db can log in with a client certificate instead of a password:

host=db.example.net dbname=dhcp user=lease2db sslmode=verify-full
sslrootcert=/etc/ssl/ca.pem sslcert=/etc/ssl/dhcp1.pem sslkey=/etc/ssl/private/dhcp1.key

with a line like this in the server's pg_hba.conf (and ssl_ca_file set):

hostssl dhcp lease2db 192.0.2.10/32 cert map=lease2db

Scopes

With -scopes, the networks dhcpd hands addresses out in - every subnet declaration with its ranges and option routers - are kept in the table scopes, and reread whenever the file changes. That is what makes "how full is this network" answerable. include statements are not followed: point -scopes at the file that holds the subnets.

A network gets a name and a VLAN number when the line before its declaration says so, in a comment of this form:

#@ NET OFFICE 192.0.2.0/24 - VLAN 10
subnet 192.0.2.0 netmask 255.255.255.0 {
	range 192.0.2.20 192.0.2.250;
	option routers 192.0.2.1;
}

Without -scopes the lease history works just the same; only the per-network views stay empty.

What is in the database

leases - one row for every time a client held an address.

Column
ip, mac the address and the client (mac is NULL for an abandoned address)
first_seen when this client got the address
starts its latest renewal
ends when the lease runs out, or ended; NULL = never
state dhcpd's binding state: active, free, expired, released, abandoned, ...
current true for the latest row of its address
hostname the name the client sent
circuit_id, remote_id relay agent information (option 82): where the client is plugged in. bytea - the bytes the relay sent
vendor_class the client's vendor class, when dhcpd is set up to record it
updated when the row last changed

scopes - the networks: network, name, vlan, gateway, addresses (in the dynamic ranges), active (in the configuration now).

Applications should read the views rather than the tables:

View
leases_history every row of leases with its network (network, scope, vlan), a boolean active (leased right now), and the relay information made readable: circuit_id_text, remote_id_text
leases_active the rows of leases_history that are leased right now
scopes_usage for every active network: name, vlan, gateway, addresses, leases (leased right now) and percent_used
leases_totals one row: active_leases, active_clients (distinct MAC addresses), clients_ever

Relay agent information (option 82)

circuit_id and remote_id tell which switch and port a client sits behind, in whatever format the relay's vendor chose - text on some (ge-0/0/7.0:guests), packed bytes on others. dhcpd writes them into its lease file as quoted text when every byte happens to be printable and as hex otherwise; lease2db stores the bytes themselves, so text that merely looks like hex is never mistaken for it.

circuit_id_text and remote_id_text (the function lease2db_show(bytea)) give them for reading: as text when all bytes are printable ASCII, else as hex (00:04:00:64:01:0c). Decoding a vendor's packed format - VLAN, module and port on Cisco, for example - is left to the application:

SELECT get_byte(circuit_id, 2) * 256 + get_byte(circuit_id, 3) AS vlan,
       get_byte(circuit_id, 4) AS module, get_byte(circuit_id, 5) AS port
FROM leases_active WHERE ip = '192.0.2.57' AND length(circuit_id) = 6;

Questions to ask it

Where is this address plugged in, right now?

SELECT mac, hostname, remote_id_text AS switch, circuit_id_text AS port, scope, vlan
FROM leases_active WHERE ip = '192.0.2.57';

Who had this address at that time?

SELECT mac, hostname, circuit_id_text, first_seen, ends
FROM leases_history
WHERE ip = '192.0.2.57'
  AND first_seen <= '2026-01-13 21:40+01'
  AND (ends IS NULL OR ends >= '2026-01-13 21:40+01');

Where has this client been? (Search by hostname works the same way.)

SELECT ip, scope, first_seen, ends, state, circuit_id_text
FROM leases_history WHERE mac = '00:00:5e:00:53:01' ORDER BY first_seen;

How full is every network?

SELECT name, vlan, network, gateway, leases, addresses, percent_used
FROM scopes_usage ORDER BY percent_used DESC NULLS LAST;

Who is in this network right now?

SELECT ip, mac, hostname, ends FROM leases_active WHERE vlan = 10 ORDER BY ip;

How many leases and clients are there?

SELECT * FROM leases_totals;

The views work directly as data sources for Grafana's PostgreSQL plugin. Give an application that only reads a role of its own:

CREATE ROLE dhcp_read LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE dhcp TO dhcp_read;
GRANT USAGE ON SCHEMA public TO dhcp_read;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO dhcp_read;
ALTER DEFAULT PRIVILEGES FOR ROLE lease2db IN SCHEMA public GRANT SELECT ON TABLES TO dhcp_read;

Good to know

  • Times in the lease file are UTC; the columns are timestamptz.
  • A lease handed out and gone again while lease2db was not running is only in the history if dhcpd had not yet rewritten its file - dhcpd keeps the last word about every address, not everything in between.
  • Addresses given by fixed-address are not in the lease file at all, so they are not in the history either - and have no relay information.
  • Old rows are never deleted. Delete what you no longer want yourself, for example DELETE FROM leases WHERE NOT current AND ends < now() - interval '1 year'.
  • DHCPv6 (dhcpd6.leases) is not read.

Metrics

With -metrics /var/lib/prometheus/node-exporter/lease2db.prom the program writes its own state for the node exporter's textfile collector every 15 seconds:

Metric
lease2db_connected 1 = connected to the database
lease2db_last_success_timestamp_seconds when the file was last read and the database written
lease2db_blocks_total lease blocks read
lease2db_changes_total blocks that changed a row
lease2db_blocks_refused_total blocks that could not be understood (they are logged and skipped)
lease2db_errors_total times the database connection was lost
lease2db_file_restarts_total times the file was read from the top
lease2db_addresses addresses with a row in the history
lease2db_leases_active addresses leased right now
lease2db_clients_active clients (MAC addresses) holding a lease right now
lease2db_scope_addresses{network,name,vlan} addresses in the network's dynamic ranges (with -scopes)
lease2db_scope_leases_active{network,name,vlan} addresses of the network leased right now (with -scopes)

A sensible alert: time() - lease2db_last_success_timestamp_seconds > 300. And a graph of how full the networks are, over time, without asking the database: lease2db_scope_leases_active / lease2db_scope_addresses.

Development

go test ./...

The tests of the database layer need a real PostgreSQL and are skipped without one. To run them, point LEASE2DB_TEST_DSN at a database the tests may fill and empty:

LEASE2DB_TEST_DSN='postgres://postgres:test@localhost/lease2db_test?sslmode=disable' go test -p 1 ./...

A release is a tag: pushing vX.Y.Z builds the archives and publishes them.

License

GPL-3.0-or-later. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages