{"slug": "gofi-static-dhcp-leases-without-the-server", "title": "gofi: Static DHCP Leases Without the Server", "summary": "A developer built gofi, a Go module for managing Ubiquiti UniFi network gear programmatically, and credits agentic AI tools like Claude Code for making the project possible as a side project. The module provides type-safe, concurrent-safe access to UniFi Network Application endpoints including devices, networks, WLANs, clients, firewall rules, and real-time events, compiling to a single static binary with no runtime dependencies. The project is hosted under Emerging Robotics alongside the earlier Gorai module.", "body_md": "Here’s a thing I’ve quietly hated for years: managing static IP assignments and DNS names for the little fleet of gear on my network. Every homelab has it — the Pi in the garage, the shop printer, the sensor node in the attic, the pool controller. You want them at known addresses with known names. And the “normal” way to get that is to stand up a DHCP server and a DNS server and keep them in sync. Ugh, no.\n\nSo I built a better way. It’s a clean little Go module called [gofi](https://github.com/emergingrobotics/gofi).\n\nAnd I want to say the quiet part out loud right up front: **I could not have built this without agentic AI help.** Not “it would have taken longer.” Not “it would have been less polished.” *Impossible* — at least not as a side project squeezed around a day job. Reverse-engineering an undocumented vendor API, designing a proper module around it, writing the whole thing plus a real test suite plus the command-line utilities? That’s weeks of grinding I was never going to spend. With [Claude Code](https://blog.herlein.com/post/claude-code/) and the [superpowers](https://github.com/obra/superpowers) skill set driving the agentic loop, it became a thing I actually finished. Hold that thought — I’ll come back to it.\n\n## What gofi Is\n\nIf you run Ubiquiti UniFi gear — a UDM Pro, UDM SE, a UDR, whatever — you already know the drill. The Network application is genuinely good. The web UI is polished. And the moment you want to do anything *programmatically*, you’re stuck reverse-engineering an undocumented internal API, juggling login cookies, and praying the endpoint didn’t change in the last firmware bump.\n\n[gofi](https://github.com/emergingrobotics/gofi) is a Go module I wrote to do that dirty work for you. It lives under [Emerging Robotics](https://emergingrobotics.ai/) alongside [Gorai](https://github.com/emergingrobotics/gorai) (which I [wrote about here](https://blog.herlein.com/post/announcing-gorai/)), and it carries the same design sensibility I brought to that: idiomatic Go, single static binary, no runtime to ship, no nonsense.\n\nWhat I was after was type-safe, concurrent-safe access to all the major UniFi Network Application endpoints. In practice that means the thing you’d normally reach for the UI to do, you can now do from code:\n\n**Devices**— list, adopt, restart, upgrade, locate your APs and switches** Networks**— VLANs and subnets with full CRUD** WLANs**— create and modify wireless networks, MAC filtering** Clients**— see what’s connected, block a device, authorize a guest** Firewall**— both the v1 and v2 rule APIs** Events**— real-time WebSocket streaming of what’s happening on the network\n\n## Why It’s Good\n\nI’ve written a fair bit lately about [why I reach for Go](https://blog.herlein.com/post/why-go-for-robotics/), and gofi is a textbook example of the payoff.\n\n**I split the packages the way you’d hope.** `types/`\n\nfor the data structures, `services/`\n\nfor the twelve endpoint groups, `auth/`\n\nfor session and credential handling, `transport/`\n\nfor the HTTP client (with retry logic built in), `websocket/`\n\nfor the event stream, and a `mock/`\n\npackage so you can test against a fake controller instead of poking your actual network. I’ve shipped enough real software to know that last one isn’t optional.\n\n**The error handling is grown-up.** Sentinel errors like `ErrAuthenticationFailed`\n\n, `ErrSessionExpired`\n\n, and `ErrRateLimited`\n\nmean you can branch on *what actually went wrong* instead of grepping strings out of an error message. That’s the difference between a library you can build on and a script someone posted once.\n\n**It compiles to a single binary.** No Python virtualenv to babysit, no runtime to install on the box. You `go build`\n\nand you copy one file. If you’ve read my [rant on agentic Python](https://blog.herlein.com/post/agentic-python-considered-harmful/), you already know how I feel about this.\n\nAnd there’s a real test suite behind it — hundreds of tests, run with race detection, backed by that mock server. This isn’t a weekend proof-of-concept. It’s the thing you were going to have to write yourself, already written, already tested.\n\n## How I Actually Built It\n\nHere’s where I have to be honest about the machinery, because it’s the whole point of this post.\n\nI did not sit down and hand-write a UniFi API client from scratch. I don’t have the weekends for that anymore. What I did was drive the entire thing through [Claude Code](https://blog.herlein.com/post/claude-code/) and the [superpowers](https://github.com/obra/superpowers) skills, in a real agentic loop — the same disciplined approach I used to [port gocat from Python to Go](https://blog.herlein.com/post/claude-code-gocat/). It went in phases, and every phase was a genuine collaboration:\n\n**Analyze the APIs.** UniFi’s Network Application API is famously undocumented — a moving target of endpoints, auth quirks, and cookie juggling. I pointed the agent at the traffic, at community notes, at the actual controller responses, and we mapped out what each endpoint really does. This is the part that would have taken me*weeks*of tedious spelunking alone. The loop chewed through it in an afternoon.**Design the module.** Before a line of the real implementation, we worked out the package structure —`types`\n\n,`services`\n\n,`auth`\n\n,`transport`\n\n,`websocket`\n\n,`mock`\n\n. I made the architectural calls; the agent pressure-tested them, caught the places my first instinct would have painted me into a corner, and kept the design idiomatic.**Write the module.** Then the actual code — service by service, endpoint by endpoint, with the mock server and the test suite growing right alongside. Hundreds of tests with race detection didn’t happen because I’m a saint about testing. They happened because the loop makes writing them cheap enough that there’s no excuse not to.**Write the utilities.** Finally the tools on top,`gofips`\n\nbeing the star. The ISC-DHCP-syntax idea was mine; turning it into a validated, dry-run-capable, round-trippable CLI was the loop.\n\nAt every step *I* was the engineer making the decisions — the architecture, the tradeoffs, the “no, do it this way” calls. The agent was the tireless pair that did the grinding, remembered every detail, and never got bored on endpoint number forty. That division of labor is exactly what makes this possible.\n\nBut let me show you the part that actually made me smile.\n\n## The Practical Payoff: gofips\n\ngofi ships a command-line tool I called `gofips`\n\n— “fi**xed** IP**s**.” Its whole job is managing the static DHCP reservations (and their DNS names) on your UniFi controller. And I built it to feel deeply familiar to anyone who’s ever run a real DHCP server: it speaks **ISC DHCP host-declaration syntax**.\n\nThat was the whole idea. Instead of inventing yet another config format, gofips represents each reservation as the exact `host { ... }`\n\nblock you’d write in `dhcpd.conf`\n\n:\n\n```\nhost garage-pi {\n    hardware ethernet aa:bb:cc:11:22:33;\n    fixed-address 10.20.30.10;\n}\n```\n\nThe `host`\n\nlabel doubles as the DNS name. One block, one device, one name, one address. Readable by a human, diffable by git.\n\n### Step 1: Dump What You’ve Got\n\nSay my home network lives on `10.20.30.0/24`\n\nand the UDM Pro answers at `10.20.30.1`\n\n. I’ve got three devices I’ve already pinned down through the UI over the years. Let’s get them out:\n\n```\ngofips -H 10.20.30.1 -k --get > hosts.conf\n```\n\n(`-H`\n\npoints at the controller; `-k`\n\nskips TLS verification, because of course it’s using a self-signed cert on your LAN.)\n\nOut comes a clean, sorted file:\n\n```\nhost garage-pi {\n    hardware ethernet aa:bb:cc:11:22:33;\n    fixed-address 10.20.30.10;\n}\n\nhost shop-printer {\n    hardware ethernet aa:bb:cc:44:55:66;\n    fixed-address 10.20.30.11;\n}\n\nhost attic-sensor {\n    hardware ethernet aa:bb:cc:77:88:99;\n    fixed-address 10.20.30.12;\n}\n```\n\nThink about that for a minute. Everything the controller knows about my fixed leases is now a plain text file I can read, version, and check into a repo. That alone is worth the price of admission.\n\n### Step 2: Add a Device\n\nNow I want to add my pool controller. (Yes, I automate my pool. It’s a flaw of mine.) I don’t open a browser. I don’t click through three panels of the UniFi app. I just edit the file and add one block:\n\n```\nhost garage-pi {\n    hardware ethernet aa:bb:cc:11:22:33;\n    fixed-address 10.20.30.10;\n}\n\nhost shop-printer {\n    hardware ethernet aa:bb:cc:44:55:66;\n    fixed-address 10.20.30.11;\n}\n\nhost attic-sensor {\n    hardware ethernet aa:bb:cc:77:88:99;\n    fixed-address 10.20.30.12;\n}\n\nhost pool-controller {\n    hardware ethernet aa:bb:cc:aa:bb:cc;\n    fixed-address 10.20.30.13;\n}\n```\n\n### Step 3: Load It Back\n\nPreview first — because you *do* check before you push to prod, right?\n\n```\ngofips -H 10.20.30.1 -k --set hosts.conf --dry-run\n```\n\n`--dry-run`\n\nvalidates the whole file and tells you exactly what it *would* change, without touching a thing. When it looks right, drop the flag and apply it:\n\n```\ngofips -H 10.20.30.1 -k --set hosts.conf\n```\n\nDone. The pool controller now has a fixed address and a DNS name, and the other three were left exactly as they were. `--set`\n\nprovisions the whole file in one shot, with full validation *before* it applies anything — so you don’t get a half-applied mess if entry four has a typo.\n\n## Why This Beats Running Your Own DHCP + DNS\n\nHere’s the part I really want you to sit with.\n\nThe traditional way to get “known devices at known addresses with known names” is to run **two** servers. You stand up ISC DHCP (or dnsmasq) for the leases. You stand up BIND (or, again, dnsmasq wearing a different hat) for the names. Then you spend the rest of your natural life keeping the two of them in agreement — because a fixed IP with no matching DNS record, or a DNS record pointing at the wrong lease, is exactly the kind of gremlin that eats an evening.\n\nAnd that’s *before* you get to the operational tax: two daemons to patch, two configs to back up, two things that can fall over at 2am and take your whole network’s name resolution with them.\n\nYour UniFi gateway is *already doing all of this*. It’s already the DHCP server. It already does the DNS. It’s already running, already patched with the rest of your firmware, already backed up with your controller config. gofips just gives you a sane, scriptable, text-based front door to the piece you actually wanted to control.\n\nSo the comparison isn’t “gofips vs. a DHCP server.” It’s:\n\n**Two extra servers** to run, sync, patch, and back up —*versus*—**One text file** and a static Go binary, driving hardware you already own.\n\nThat’s not a small quality-of-life win. That’s deleting an entire subsystem from your homelab. And as I’ve [said before](https://blog.herlein.com/post/disposability/), the best infrastructure is often the infrastructure you got rid of. This is [GitOps](https://blog.herlein.com/post/cicd-thoughts/) for your DHCP reservations: the file is the source of truth, `--set`\n\nis the deploy, and `--get`\n\ntells you the current state. Check the file into a repo and you’ve got history, review, and rollback for free.\n\n## Conclusion\n\ngofi is the Go module I wish I’d had years ago for talking to UniFi gear — clean package layout, real error taxonomy, a mock server, a proper test suite. If you’ve got any UniFi automation ambitions, grab it instead of hand-rolling yet another cookie-juggling API client. That’s exactly why I open-sourced it.\n\nBut even if you never write a line of Go, `gofips`\n\nalone earns it a spot in your toolbox. Dump your fixed leases to a file, edit the file, load it back. No second DHCP server. No third DNS server. No 2am sync gremlins. Just your gateway, doing the job it was already doing… just now with an easy way to manage leases and names.\n\nTry it. Dump your leases and just *look* at the file. I think you’ll have the same reaction I did.\n\nAnd now the part I promised to come back to. **I could not have built gofi on my own.** Not the API archaeology, not the module design, not the test suite, and definitely not all of it together as a nights-and-weekends project. It exists because the agentic loop — Claude Code plus superpowers — turned “weeks of grinding I’ll never get to” into “a real tool I actually shipped.” I made the engineering decisions; the loop did the tireless work of carrying them out. That’s the collaboration, and it’s genuinely new.\n\nThis is the shining example I keep pointing people at when they ask what these AI tools are *actually* good for. Not toy demos. Not autocomplete. A useful, tested, open-source tool that solves a real problem I have — one that simply would not exist otherwise. If you’re a builder sitting on a “someday” project you’ve decided is too much work for one person, I’d gently suggest that the math has changed. Go build the thing.\n\nIf this helps you, drop me a note on [LinkedIn](https://www.linkedin.com/in/gherlein/). And if you build something fun on top of gofi — or with the loop — I really want to hear about it.", "url": "https://wpnews.pro/news/gofi-static-dhcp-leases-without-the-server", "canonical_source": "https://blog.herlein.com/post/gofi-static-dhcp-leases/", "published_at": "2026-07-16 08:00:01+00:00", "updated_at": "2026-07-16 23:18:52.403879+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["gofi", "Ubiquiti UniFi", "Emerging Robotics", "Claude Code", "Gorai"], "alternates": {"html": "https://wpnews.pro/news/gofi-static-dhcp-leases-without-the-server", "markdown": "https://wpnews.pro/news/gofi-static-dhcp-leases-without-the-server.md", "text": "https://wpnews.pro/news/gofi-static-dhcp-leases-without-the-server.txt", "jsonld": "https://wpnews.pro/news/gofi-static-dhcp-leases-without-the-server.jsonld"}}