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 “old-school” way to get that is to stand up a DHCP server and a DNS server and keep them in sync. Since my network has tens of BrightSign devices in addition to my hobby robotics devices, this becomes actual work.
So I built a better way. It’s a clean little utility wrapped around a new Go module called gofi, and it genuinely saves me time managing my network — I add, rename, and pin devices in seconds now instead of clicking through a web UI or babysitting a config file.
And 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 and the superpowers skill set driving the agentic loop, it became a thing I actually finished. And now it saves me time nearly evey day. Hold that thought — I’ll come back to it.
What gofi Is
If 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.
gofi is a Go module I wrote to do that dirty work for you. It lives under Emerging Robotics alongside Gorai (which I wrote about here), and it carries the same design sensibility I brought to that: idiomatic Go, single static binary, no runtime to ship, no nonsense.
I have to give credit where it’s due here: the inspiration came from the excellent UniPoller project (unpoller, formerly unifi-poller). If you’ve ever wanted UniFi metrics in Prometheus or InfluxDB, that’s the tool, and it proved years ago that you can talk to a UniFi controller cleanly from Go. But UniPoller is built to read — it’s telemetry, polling, dashboards. I wanted the other half: to configure and control the gear from code. Seeing how well UniPoller did its job is a big part of what convinced me the control side was worth building. So thank you to that crew — gofi stands on your shoulders.
How I Actually Built It
Here’s where I have to be honest about the machinery, because it’s the whole point of this post.
I did not sit down and hand-write a UniFi API client from scratch. What little time I have on weekends I use for Gorai. What I did was drive the entire thing through Claude Code and the superpowers skills, in a real agentic loop — the same disciplined approach I used (much more manually) to port gocat from Python to Go. It went in phases, and every phase was a genuine collaboration:
- 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,services,auth,transport,websocket,mock. 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,
gofipsbeing the star. The ISC-DHCP-syntax idea was mine; turning it into a validated, dry-run-capable, round-trippable CLI was the loop.
At 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.
And I didn’t do this in one sitting. I’d have a terminal window open and perhaps sitting there for a week doing nothing - claude patiently waiting for me to say the design was OK and implementation could proceed. When I got some more time I read the design and approved. Rinse and repeat. Literally in my spare time I built this tool and module.
But let me show you the thing that actually saves me time and effort nearly every day.
The Practical Payoff: gofips
gofi ships a command-line tool I called gofips — “fixed IPs.” 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.
That was the whole idea. Instead of inventing yet another config format, gofips represents each reservation as the exact host { ... } block you’d write in dhcpd.conf:
host garage-pi {
hardware ethernet aa:bb:cc:11:22:33;
fixed-address 10.20.30.10;
}
The host label doubles as the DNS name. One block, one device, one name, one address. Readable by a human, diffable by git.
Step 1: Dump What You’ve Got
Let’s talk through a fictional example. Say my home network lives on 10.20.30.0/24 and the UDM Pro answers at 10.20.30.1. I’ve got three devices I’ve already pinned down through the UI over the years. Let’s get them out:
gofips -H 10.20.30.1 -k --get > hosts.conf
(-H points at the controller; -k skips TLS verification, because of course it’s using a self-signed cert on your LAN.)
Out comes a clean, sorted file:
host garage-pi {
hardware ethernet aa:bb:cc:11:22:33;
fixed-address 10.20.30.10;
}
host shop-printer {
hardware ethernet aa:bb:cc:44:55:66;
fixed-address 10.20.30.11;
}
host attic-sensor {
hardware ethernet aa:bb:cc:77:88:99;
fixed-address 10.20.30.12;
}
If you have a home lab, that’s likely to be hundreds of devices - not three. So you get my point about how this tool can help! Everything the controller knows about my fixed leases is now a plain text file I can read, version, and check into a repo.
Step 2: Add a Device
Now I want to add a BrightSign player I’ve got on the bench. This is a real, everyday thing for me: I’m constantly dropping devices on my network to develop software on them — right now a player I’m using while building a BrightSign extension. When you’re developing on a device, you want it at a known name so you can ssh brightsign-dev or point a build script at it without hunting for whatever address DHCP handed out this morning. That’s exactly the kind of friction gofips erases. 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:
host garage-pi {
hardware ethernet aa:bb:cc:11:22:33;
fixed-address 10.20.30.10;
}
host shop-printer {
hardware ethernet aa:bb:cc:44:55:66;
fixed-address 10.20.30.11;
}
host attic-sensor {
hardware ethernet aa:bb:cc:77:88:99;
fixed-address 10.20.30.12;
}
host brightsign-dev {
hardware ethernet aa:bb:cc:aa:bb:cc;
fixed-address 10.20.30.13;
}
Step 3: Load It Back
Preview first — because you do check before you push to prod, right?
gofips -H 10.20.30.1 -k --set hosts.conf --dry-run
--dry-run validates 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:
gofips -H 10.20.30.1 -k --set hosts.conf
Done. The BrightSign player now has a fixed address and a stable name I can ssh to, and the other three were left exactly as they were. --set validates the whole file before it applies anything, then pushes it through the controller’s API — there’s no daemon to reload and nothing to restart. That last part matters more than it sounds, and here’s why.
Why This Beats Running Your Own DHCP + DNS
Here’s the part I really want you to sit with - and remember that I’ve been running Debian home servers since way before my kids were born… so giving that up was losing some nostalgia.
The 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 (unless you use dnsmasq, which I never liked) — 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 when something fails.
And 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. Oh, and add a network at a second home? Double the friction!
Want to feel that concretely? Here’s the manual version on a stock Ubuntu box. For the leases you’d run isc-dhcp-server, edit /etc/dhcp/dhcpd.conf (which, funny enough, is the exact same host syntax gofips borrowed), then bounce the daemon. For the names you’d run bind9, edit the zone file, add an A record, remember to bump the SOA serial — forget that and nothing propagates — then reload:
sudo nano /etc/dhcp/dhcpd.conf # add the host block
sudo systemctl restart isc-dhcp-server
sudo nano /etc/bind/db.10.20.30 # add the A record, bump the serial
sudo systemctl reload bind9
Those restarts are the steps everyone forgets — and they’re exactly where it bites. Fat-finger a semicolon in dhcpd.conf or botch the serial in the zone file and the service doesn’t “half-apply” anything. It flatly refuses to come back up:
$ sudo systemctl status isc-dhcp-server
Active: failed (Result: exit-code)
Now your DHCP — or worse, your DNS — is simply down, not answering requests at all, and every device on the network feels it until you notice, read the logs, and fix the typo. Two configs, two daemons, two reloads, two chances to take the network down, every single time you add one device. gofips validates first and pushes through an API that’s already running; there’s no service to knock over.
Now, I cut my teeth doing sysadmin on a Sun Sparkstation running SunOS 4.1.3 (if I recall). My fingers do all that stuff while my brain is still sipping coffee. But really? Yes, Virginia, there is a better way.
Your UniFi gateway is already running these services - you just have to enable them. It already has a DHCP and DNS server. Itt’s already running, already patched with your Unifi 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.
So the comparison isn’t just the interface, it’s not just “gofips vs. a DHCP server.” It’s:
- Two extra servers to run, sync, patch, and back up — versus —
- One text file and a static Go binary, driving hardware you already own.
That’s not a small quality-of-life win. That’s deleting an entire subsystem from your homelab. And as I’ve said before, the best infrastructure is often the infrastructure you got rid of. This is GitOps for your DHCP reservations: the file is the source of truth, --set is the deploy, and --get tells you the current state. Check the file into a repo and you’ve got history, review, and rollback for free.
Conclusion
gofi 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. If it can help others, great!
AND… it’s a Go module, so you can use it to write other tools. This is the part I get genuinely excited about, so bear with me.
I’ve argued before that automation is robotics — that the line between “managing a network” and “commanding a fleet of machines” is mostly in your head. Your UniFi controller is a robot: a thing with sensors (what’s connected, what’s up, what’s talking) and actuators (block this client, open that VLAN, pin this address). gofi is just the clean set of hands that lets code reach in and work it. Once you see it that way, “network management” stops being a chore and starts being one more mesh of resources and tools you can automate.
And here’s where it gets fun. Because gofi is a proper module with a real API surface, wrapping it into an MCP server is an obvious next step — the same move I made with gorai-mcp for my robotics framework. Expose the UniFi controller’s resources and tools to an LLM and you could just ask: “pin the new BrightSign player to a static address and give it a name,” and the agent does it. No UI, no config file, no memorized flags. I haven’t built that yet — but gofi is exactly the foundation that makes it a weekend morning’s work instead of a moonshot.
That’s the real point. gofi isn’t just a tool I use; it’s a building block — a Lego brick. I snapped gofips together out of it, someone else can snap together a Prometheus exporter or a Terraform provider or that MCP server, and none of us has to re-solve the “talk to UniFi from Go” problem ever again. That’s what open-sourcing a clean module does: it hands everybody a brick they didn’t have to mold themselves. Go build something with it.
But even if you never write a line of Go, gofips alone 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.
And the honest, day-to-day payoff for me: it saves me real time managing my network. I develop software on devices constantly, and being able to drop a new player or board on the network with a stable name — ssh brightsign-dev and go — instead of chasing DHCP addresses is the kind of small friction that adds up to hours. gofips took that friction to zero.
And 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.
This 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. It won’t take you nearly as long as you thought it would!
If this helps you, drop me a note on LinkedIn. And if you build something fun on top of gofi — or with the loop — I really want to hear about it.