Fast. Stable. Easy to run.
Three open source projects for DOCSIS cable plants. docsis-server boots cable modems from MySQL tables: DHCP, TFTP, time and syslog in six small Rust processes. docsis-admin is the web portal for those tables. Instead of managing hundreds of configuration files, you manage the fragments inside them and let the files build themselves. It also counts the routable IPv4 a plant had to buy, per interface, and takes back the addresses that are only being held. docsis-monitor watches the wire and says so when provisioning stops.
GNU General Public License, version 2 or later · Rust, edition 2024 · MySQL 8.x or MariaDB 10.11+
6
Independent services
0
Lines of unsafe Rust
20 yrs
The C server ran in production
1 row
To change a service tier
3
Clocks before an address is freed
0
Config files to manage
An integrated provisioning server for DOCSIS cable plants
Working alongside a CMTS, docsis-server boots cable modems and the customer equipment behind them. It answers DHCPv4 and DHCPv6, hands each modem its DOCSIS configuration file over TFTP, serves time-of-day, and collects syslog from the plant. It is written in Rust, forbids unsafe throughout, and is a port of a C server that ran a production cable plant for two decades.
Speed
Generated files are cached in memory until a fragment changes. Above a configurable request rate the DHCP and TFTP services switch to in-memory caches, so a plant-wide reboot storm is answered without waiting on the database.
Stability
Six independent processes; a crash in one does not take the others down. A database outage is survived, not latched: writes are queued and replayed, and a read-only backup server can take over reads.
Ease of use
Everything lives in MySQL tables your billing or provisioning system can write directly. Changing a service tier is an edit to one row; a misspelled setting is reported at start-up with its line and column.
01
Provisions a modem from the moment it powers on
The modem broadcasts a DHCPDISCOVER, the CMTS relays it, and the server decides from the relay's address and VLAN which pool the modem belongs to, allocates a management address, and tells it where to fetch its configuration. The modem downloads that file over TFTP and comes online. A device whose MAC is on the blacklist is refused before any of that, and refused in silence, because a NAK only invites it to ask again. The modem a device sits behind is checked as well, so blacklisting a modem stops what is behind it.
02
Tells devices apart
An embedded telephony adapter, a set-top box and a customer's laptop each belong on a different network with different options. The server classifies them from the DHCP vendor options the device sends and from per-modem flags, and draws each from its own pool.
03
Manage fragments, not files
One fragment per quality of service level. One fragment for a particular model of cable modem. Multiply the number of service levels by the number of modem models and you get an unmanageable number of files. Manage only the fragments and let the files be created dynamically, and the file management goes away. The server concatenates a modem's fragments, signs the result with both DOCSIS message integrity checks and serves it from memory; a static file from disk still works when you want one.
04
Keeps addressing stable
An IPv4 lease can be pinned so the server never reclaims or reissues it. On IPv6 a subscriber gets an address for their router and a delegated prefix for the network behind it, and the delegation is stable across renewals and even across the lease being archived and re-issued.
05
Answers the CMTS when it asks about a lease
A CMTS that never watched a device get its address asks the server whether that address really belongs to it, and forwards nothing for the device until it is told. Both dialects of that question are answered: the standard one from RFC 4388, and the earlier draft the older Cisco CMTS speak, which share a message number and differ in what comes back. Ask by address, by MAC, or by the client identifier the device sent, which is matched against the identifier recorded for it the last time it was served. Answering allocates nothing, so asking about an address cannot bring a lease for it into being.
06
Runs as six independent processes
One per service, each with its own PID file and log. A crash in one does not take the others down, and a supervisor restarts it. Survives a database outage, degrades to in-memory caches under load, and can be steered while running with tell-docserv.
| Service | Port | What it does |
|---|---|---|
| DHCPv4 | 67 | Provisions modems and customer equipment, and answers lease queries |
| DHCPv6 | 547 | Customer equipment only, with prefix delegation |
| TFTP | 69 | Serves static and generated configuration files |
| Time | 37 | RFC 868 time-of-day, which DOCSIS modems require |
| Syslog | 514 | Collects device syslog into daily tables |
| Background | — | Drains deferred writes, keeps the connection warm, takes remote commands |
None of those logs is kept in the provisioning database. What DHCP, DHCPv6 and TFTP record goes to a log database, device syslog to another, and each writes one table a day. A backup of the plant then carries the plant rather than the month's traffic, and a day's logs are disposed of by dropping a table. Nothing removes one on its own: how long to keep them is an operator's decision.
The web portal for the tables behind it
docsis-server keeps all of its state in MySQL, so anything that can write a row can run the plant. docsis-admin is that interface. One instance connects to the MySQL servers of several docsis-servers and switches between them from a selector at the top.
Admins edit the tables
Modem inventory and the catalogue of models behind it, IPv4 and IPv6 address pools, DHCP option groups, configuration fragments with a TLV form and a raw-text fallback, leases, and the blacklist. Edits save when you leave the cell.
Help Desk looks things up
Search a CM MAC, CM IP, CPE MAC or CPE IP across every server at once, then open the modem: what it was entitled to, where it is now, the equipment behind it, and its DHCP and TFTP logs. Each model in the catalogue carries a photograph, opened full size while somebody reads a serial number off the back of a box over the phone.
Each family keeps its own options
DHCPv4 and DHCPv6 share option numbers and share none of their meanings, so the two catalogues are separate and a sub-option belonging to the other family is refused rather than stored. DHCPv6 carries the RFC 7598 MAP-E and MAP-T containers an eRouter asks for before it can build a MAP domain, the RFC 5970 boot file URL a set-top box or an eMTA boots from, and DNS and NTP.
Signing in
Two-factor is RFC 6238, enrolled in two presses, and a code is spent once it is used. A wrong password starts a cooldown on the address it came from and on the account it was aimed at, so neither one client retrying nor a thousand guessing one account goes faster than a try per cooldown. Sessions end at four in the morning where the operator is, rather than after a fixed number of hours that expire somebody mid-sentence. Help desk runs to the coming Sunday, an admin to the next four o'clock, and an admin who presented a code for three days.
Trust, but verify
We trust our own work. We also expect it to fail one day, and we would rather hear it from a monitor than from a customer. docsis-monitor is a completely separate process that watches the wire the provisioning server sits on and counts what crosses it. It shares no code, no database and no configuration with the server it is checking on.
It sees what the server cannot
A request that never arrives leaves nothing in the server's own logs, because nothing reached it to be logged. A relay that stopped relaying, a VLAN that stopped trunking, a firewall rule somebody added on Friday: through every one of those the server's tables look quiet and healthy. The only place it is visible is the wire.
It watches both sides
Counting replies is not knowing requests were answered. Each request is matched to its reply by transaction id, so the monitor reports not only that answers went out but how long they took. A server that is up, listening and failing everything looks exactly like one that is down, and this tells the two apart.
Per CMTS, not per plant
One interface carries several thousand modems and should never be silent for five minutes. Another carries forty and is properly silent for an hour. Each gets its own idea of normal, measured from the plant rather than guessed at, and one interface gone quiet is an alert even while the other eleven are busy.
And it tells somebody
When DHCP requests stop arriving, or arrive and go unanswered, or config files stop being served, an SNMP trap goes to the network management platform and mail goes to whoever is on call. The subject carries the whole finding, because that is all a phone shows at two in the morning.
# ~/logs/docsis-monitor.log 300s window: 24014 requests, 24014 replies (100% answered) — working 10.100.0.1 11907 req 11907 rep 4ms mean, 31ms worst — working 10.100.0.17 8104 req 8104 rep 5ms mean, 44ms worst — working 10.100.0.33 4003 req 4003 rep 4ms mean, 27ms worst — working # and when one of them stops 300s window: 20011 requests, 20011 replies — quiet 10.100.0.33 0 req 0 rep idle 412s of 300s — quiet trap sent to 10.0.0.9:162 — quiet mailed noc@example.com
A monitor that shares its code with the thing it watches fails at the same moment for the same reason. This one is its own program, on its own schedule, reading packets rather than tables.
Counting the addresses you had to buy
A plant has as much private space as it will ever want. Routable IPv4 is bought, at a price that keeps going up, and nobody is making more of it. So the pool cards count routable addresses only. A range inside 10/8, 172.16/12 or 192.168/16 is left out of the total, because rolling it in buries the number that matters under tens of thousands of addresses that cost nothing. A bundle with nothing routable on it shows no count at all, which is the honest answer to how much scarce space it is using.
A block belongs to one interface
A block is routed to the interface that serves it, so an address free on the wrong bundle is not free at all. Capacity folds to one row per interface per range type: modems, then customer equipment. Eleven customer ranges under one bundle become one row, because the next customer is served from whichever of them has room. A range nobody can classify is counted as routable on purpose. The figure feeds a purchase, so an unreadable row belongs in front of somebody rather than quietly left out of the total.
Amber while there is still time to buy
The thermometer turns amber at three quarters full and red at ninety five per cent. Amber is far enough out to start an address request and have it arrive; red on a large range is already down to a few dozen. The page opens sorted by percentage, tightest first, so the thing about to run out is at the top of it rather than somewhere in the middle.
Most over-provisioning is stale leases
Before buying a block, look at what is being held by equipment that is gone. Every row carries the count of live leases nothing has refreshed in thirty, sixty and ninety days. Customer equipment holds a one day lease and a modem two days, so anything past a month is not coming back on its own. The block you were about to order is often already sitting in the table.
Take them back, and never one still in use
One press retires them. Each lease is copied to the archive and only then removed, both inside a single transaction, so an address can still be traced to whoever last held it. An address goes only if it has been quiet on all three of its clocks: when the lease began, when it expired, and when the server last heard about it.
Two of those are not enough, and this is the part that costs somebody their service. A subscriber who types the address into their own computer stops renewing it, so the lease’s own times go stale within a day — while the CMTS keeps asking the server about them, and that refreshes the third. Retire on the first two and you take a live address off a paying customer who is sitting there using it. Pinned addresses and dynamic ranges are never touched either.
Free, and free to hand out, are not the same
When a range runs dry the server latches it full, and the latch outlives the leases that caused it, so a row can read comfortably green and still serve nothing. A row is marked full only when every range under it is latched, and the server rechecks the latches before it refuses a modem, so one busy evening does not bar a pool for good. An exhausted range also runs the alert program, at most once a minute: a capacity problem should reach somebody, not turn into a fork storm while every modem in the neighbourhood retries.
Nobody claims the same block twice
Two pools claiming one range is a duplicate address that nobody traces back to a provisioning table. A new network is checked against every plant the console is configured for and not only the one being edited, because addresses are unique across the whole network. A plant that cannot be reached is reported as unchecked rather than as clean.
| Held on ubr-03, not refreshed in 90 days | 64 |
| Quiet on all three clocks, so retirable | 9 |
The other fifty five are pinned, dynamic, or subscribers whose CMTS is still asking after them. The dialog shows the second number, because the first one is not what pressing the button does.
249 of 254 routable IPv4 addresses in use (98%). The modem range on the same bundle is private and is not in that number: it is not the scarce thing, and counting it would have made this card read as 46% full.
Build, load the schema, start
The configuration file is JSON read with a relaxed parser, so it takes comments, trailing commas and unquoted keys, and it ships heavily annotated. Every key is typed: a misspelling or an out-of-range value is reported at start-up with the line and column rather than silently ignored.
- A Rust toolchain supporting edition 2024 (1.85 or newer)
- MySQL 8.x or MariaDB 10.11 or newer, InnoDB, utf8mb4
- A database for the logs, where each service makes its own table a day, so that account needs
CREATE - A CMTS configured to relay DHCP to this server
- The docsis encoder, which builds
gen_docsis, only if you use generated configuration files
# build git clone https://github.com/mmqr-developer/docsis_server.git cd docsis_server cargo build --release # the provisioning database, and the two the logs go in mysql -u root -p -e "CREATE DATABASE docsis_server" mysql -u root -p -e "CREATE DATABASE docsis_server_logs" mysql -u root -p -e "CREATE DATABASE syslog_server" # the tables, into the provisioning database only mysql -u root -p docsis_server < scripts_and_docs/schema-dhcp.sql mysql -u root -p docsis_server < scripts_and_docs/schema-dhcpv6.sql # only if you serve IPv6 # copy and edit the configuration cp scripts_and_docs/docsis-server.json /etc/docsis-server.json # starts the activated services docsis_server
Contributing
Questions about either project go to developer@mmqr.ca.