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.
So I built a better way. It’s a clean little Go module called gofi.
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. 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.
What 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:
- 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
Why It’s Good
I’ve written a fair bit lately about why I reach for Go, and gofi is a textbook example of the payoff.
I split the packages the way you’d hope. types/ for the data structures, services/ for the twelve endpoint groups, auth/ for session and credential handling, transport/ for the HTTP client (with retry logic built in), websocket/ for the event stream, and a mock/ package 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.
The error handling is grown-up. Sentinel errors like ErrAuthenticationFailed, ErrSessionExpired, and ErrRateLimited mean 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.
It compiles to a single binary. No Python virtualenv to babysit, no runtime to install on the box. You go build and you copy one file. If you’ve read my rant on agentic Python, you already know how I feel about this.
And 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.
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. I don’t have the weekends for that anymore. 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 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.
But let me show you the part that actually made me smile.
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
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;
}
Think 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.
Step 2: Add a Device
Now 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:
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 pool-controller {
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 pool controller now has a fixed address and a DNS name, and the other three were left exactly as they were. --set provisions 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.
Why This Beats Running Your Own DHCP + DNS
Here’s the part I really want you to sit with.
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 — 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.
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.
Your 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.
So the comparison isn’t “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.
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.
Try it. Dump your leases and just look at the file. I think you’ll have the same reaction I did.
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.
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.