Libux banner

Libux

5 devlogs
17h 27m 42s

Libux is a TUI tool for Debian that saves you from endlessly hammering away at the keyboard. It handles firewalls, web servers, and other services through a simple menu. I’m building this for people who are learning networking, understand the logi…

Libux is a TUI tool for Debian that saves you from endlessly hammering away at the keyboard. It handles firewalls, web servers, and other services through a simple menu. I’m building this for people who are learning networking, understand the logic behind things like firewalls, but aren’t big fans of Linux and don’t want to memorize commands. The point is to have everything under control while keeping it simple.

Translated with DeepL.com (free version)

This project uses AI

I use AI as a technical mentor to sanity-check my logic, especially with security-heavy stuff like iptables. It helps me brainstorm features, refine the TUI, and make sure the commands are safe and won’t lock anyone out. It’s basically my second pair of eyes for code reviews and architecture.

Demo Repository

Loading README...

mforbelak

Libux: The great cleanup and a few things that should have existed already

Spent the last few sessions finishing features that were on the list for quite long and did a proper refactor before the codebase became unmanageable.

Things that should have existed already

Save and restore across reboots is done. OUTPUT chain works. ip_forward toggle is there with an option to make it persistent. You can now edit rules directly in nano with automatic backup and rollback if the restore fails. Discard changes if you made a mess. Confirmation before deleting a rule - turns out accidentally removing a rule without making sure you want to do so is a bad idea.

Logging

When you toggle a chain policy to DROP, Libux now asks if you want to log dropped traffic. Adds a LOG rule at the end of the chain with optional prefix and rate limit. Nothing fancy, just dmesg | grep YOUR_PREFIX and you can see what’s being blocked.

The refactor which was needed

fw_shared.py was turning into a junk pile. allow_port() and allow_icmp() are now proper shared functions instead of copy-pasted blocks in every chain file. ask_ip(), ask(), ask_required() all moved to utils.py. utils.pause() replaced 20 identical try/except input blocks. utils.run_cmd() handles returncode checking in one place instead of ignoring it everywhere.

Quality of life

Chain policy now shows directly in the main firewall menu - DROP is red, ACCEPT is green. Settings menu with verbose mode that prints every iptables command before it runs. Auto backup before flush and toggle policy. install.sh and start.sh so you don’t have to remember the venv setup every time.

Up next
Network Configuration
Interfaces
- Show interfaces
- Set IP address (static/DHCP)
- Enable/Disable interface
DNS
- Show current DNS
- Set DNS servers
Gateway
- Show gateway
- Set default gateway
Hostname
- Show hostname
- Set hostname

Attachment
0
mforbelak

Libux: Chains, policies, and actually finishing things

Spent most of today closing out features that were half-done and making the firewall feel like a real tool instead of a proof of concept.

Flush and policy toggle

Every chain menu now has two new options — flush the chain and toggle the policy between ACCEPT and DROP. Simple stuff but it was annoying to not have it. The toggle reads the current policy first so you always know what you’re switching to. Show also now displays the policy line even when the chain is empty, which was kind of essential information to just not show.

Source IP filtering everywhere

HTTP, HTTPS, ICMP, MASQUERADE, DNAT, established/related on FORWARD — all of them now optionally take a source IP or subnet. Enter to skip, validation loop if you type something wrong. The DNAT one was the most interesting because the source IP needs to propagate to the auto-generated FORWARD rule too, otherwise you’d have a DNAT rule restricted to one subnet but the FORWARD rule letting everything through anyway.

Protocol choice in PREROUTING

DNAT was TCP only before. Now it asks. Relevant for things like WireGuard which runs on UDP.

The spaghetti problem

firewall.py was becoming unreadable. Split into fw_input.py, fw_forward.py, fw_nat.py and fw_shared.py. shared has the stuff that gets used everywhere — remove_rule, show_chain, rule_exists, ask, flush_chain, toggle_policy.

Up next

Save and restore across reboots, OUTPUT chain, ip_forward toggle.

Attachment
0
mforbelak

Validation, validation, validation

DNAT is working. Root check is in. Ctrl+C no longer bricks subprocess calls. This time was mostly about making sure the firewall actually rejects bad input instead of just passing garbage straight to iptables and hoping for the best.

No more typing interface names

Added pick_interface() — instead of typing eth0 and praying you didn’t make a typo, the app now reads /sys/class/net/ and shows you a menu. Obvious in hindsight. Applied it everywhere interfaces were previously typed manually: FORWARD, MASQUERADE, DNAT.

check_port and check_ip

Two new validation functions in utils. check_port() checks it’s actually a number between 1 and 65535. check_ip() uses Python’s ipaddress module and handles both plain IPs and CIDR notation since that’s what iptables expects anyway. Both are now used consistently across all modules instead of raw isdigit() scattered around.

The baseline wizard got smarter

The secure setup wizard now asks if you even want SSH before asking which port. Port validation loops until you give it something valid instead of silently failing. Also fixed a rule that was supposed to handle DNS but was actually just punching a hole for anything coming from port 53. That was embarrassing.

Ctrl+C inside subprocess

show_firewall() was calling subprocess.run() with capture_output=True which blocks — hitting Ctrl+C mid-execution threw a traceback instead of just cancelling. Wrapped it in try/except. Done.

Up next

Prevent duplicate rules. iptables -C exists for exactly this reason. One shared function in fw_shared.py, applied everywhere rules are added.

Attachment
0
mforbelak

The firewall starts to work

Yesterday I got the menus talking to each other. Today I made them actually useful.

Purple is the new orange
Spent way too long picking a color. Settled on purple. It’s everywhere now and it looks clean.

Ctrl+C no longer kills everything
This was embarrassing — hitting Ctrl+C anywhere would crash the whole script with a giant traceback. Fixed. Now it just goes back to the previous menu like a normal person would expect.

The firewall does stuff now
You can actually add and remove rules for INPUT and FORWARD chains. Allow HTTP, HTTPS, custom ports, ping, traffic between specific interfaces — it all works. Also added MASQUERADE for NAT which took like 10 minutes once I stopped second-guessing myself.

500 lines in one file is not it
firewall.py got out of hand fast. Split it into 4 separate files. Instantly felt better.

The colored output is genuinely cool
The firewall rules viewer now color-codes everything. ACCEPT is green, DROP is red, chain names are pink. Small thing but it makes it actually readable.

Tomorrow
Port forwarding (DNAT), per-chain views, and making sure you can’t accidentally add the same rule twice.

Attachment
0
mforbelak

I’ve finally finished the base for my project! It might look like just a few menus, but getting the logic of switching between them without the script crashing or looping infinitely was a real challenge.

The “Textual” Incident
I’m going to be honest here: I almost fell into the trap of “scope creep.” I spent some time looking into Textual because it looks incredibly fancy and pro. But after a few hours, I realized I have no idea how to actually use it properly yet. It’s a whole different world of asynchronous programming and CSS-like styling

What’s working now:

  • The Base: I have a solid foundation where the main menu, installation checks, and firewall sub-menus all talk to each other correctly.

  • Navigation: Switching between different chains (INPUT, FORWARD, etc.) works smoothly now.

  • The “Aha!” moment: I fixed the bug where special characters and ANSI colors were breaking the terminal rendering.

Attachment
0