A fully offline, self-hosted neighborhood file sharing and chat network running on a repurposed router and a mini PC. No internet required. No ISP. No tracking. Just vibes.
This project turns a TP-Link Archer C7 router and a Lenovo mini PC into a neighborhood-scale local network. When neighbors connect to the WiFi, they're greeted by a spooky retro captive portal, then dropped into a fully functional file sharing and real-time chat site — all without touching the internet.
Features:
- Retro CRT-style web interface
- Real-time WebSocket chat
- File sharing with upload, download, preview, move, delete, and folder creation
- Media previews (images, video, audio) inline in the browser
- Spooky captive portal disclaimer page
- Password-protected admin panel
- MAC address activity logging
- Ban/unban system for bad actors
- Works on Android, iPhone, Windows, and anything with a browser
| Component | Hardware Used |
|---|---|
| Router | TP-Link Archer C7 v1 |
| Single Board/Mini PC | Lenovo Mini PC |
| Storage | 5TB Seagate USB HDD |
| Router Extra Storage | 16GB USB thumbdrive (extroot) |
The router handles WiFi broadcasting and the captive portal. The PC does all the heavy lifting — web server, file storage, chat, and admin.
Phone/Laptop connects to "KIM ATE MY SHORTS" WiFi
|
v
Router (192.168.1.1) — TP-Link Archer C7 running OpenWrt
|
| nodogsplash intercepts unauthenticated traffic
|
v
Captive Portal — Spooky disclaimer page served from router
|
| User clicks ENTER THE NETWORK
|
v
kimatemyshorts.net resolves to 192.168.1.50 (via dnsmasq)
|
v
Ubuntu PC (192.168.1.50) — nginx + Flask
|
v
Retro site — chat, file sharing, admin panel
|
v
5TB HDD mounted at /mnt/hdd — actual file storage
Flash OpenWrt 25.12.3 to your Archer C7. The ath79 target is correct for the C7 v1.
The Archer C7 has very limited internal flash. A USB thumbdrive is used as overlay storage to allow installing packages freely.
# Format your USB drive as ext4 first, then:
apk add block-mount kmod-fs-ext4 e2fsprogs kmod-usb-storage kmod-usb2 kmod-usb3
DEVICE=/dev/sda
eval $(block info ${DEVICE} | grep -o -e 'UUID="\S*"')
uci -q delete fstab.overlay
uci set fstab.overlay="mount"
uci set fstab.overlay.uuid="${UUID}"
uci set fstab.overlay.target="/overlay"
uci commit fstab
mount ${DEVICE} /mnt
tar -C /overlay -cvf - . | tar -C /mnt -xf -
umount /mnt
rebootAfter reboot you should have 27GB+ of overlay space.
apk update
apk add nodogsplash dnsmasq-full luci block-mount kmod-fs-ext4 e2fsprogs kmod-usb-storage kmod-usb2 kmod-usb3 openssh-sftp-serveruci set wireless.radio0=wifi-device
uci set wireless.radio0.type='mac80211'
uci set wireless.radio0.path='platform/ahb/18100000.wmac'
uci set wireless.radio0.channel='6'
uci set wireless.radio0.band='2g'
uci set wireless.radio0.htmode='HT20'
uci set wireless.radio0.disabled='0'
uci set wireless.default_radio0=wifi-iface
uci set wireless.default_radio0.device='radio0'
uci set wireless.default_radio0.mode='ap'
uci set wireless.default_radio0.ssid='KIM ATE MY SHORTS'
uci set wireless.default_radio0.encryption='none'
uci set wireless.default_radio0.network='lan'
uci set wireless.default_radio0.disabled='0'
uci commit wireless
wifi reloaddnsmasq is used to resolve kimatemyshorts.net to the PC's IP so users never see a raw IP address.
uci add_list dhcp.@dnsmasq[0].address='/kimatemyshorts.net/192.168.1.50'
uci add_list dhcp.@dnsmasq[0].address='/status.client/192.168.1.1'
uci add_list dhcp.@dnsmasq[0].address='/captive.apple.com/192.168.1.1'
uci add_list dhcp.@dnsmasq[0].address='/www.apple.com/192.168.1.1'
uci commit dhcp
/etc/init.d/dnsmasq restartThe Apple addresses point to the router so iPhones trigger the captive portal notification.
The PC should never be sent to the captive portal. Add its MAC to the trusted list:
uci add_list nodogsplash.@nodogsplash[0].trustedmac='XX:XX:XX:XX:XX:XX'
uci commit nodogsplashReplace with your PC's actual MAC address (ip link show).
nodogsplash was chosen over openNDS because it is significantly faster on the MIPS-based Archer C7. openNDS runs a massive shell script on every portal request which bogs down the old hardware. nodogsplash is a lightweight C binary.
Create /etc/nodogsplash/nodogsplash.conf:
GatewayInterface br-lan
MaxClients 20
SessionTimeout 1440
CheckInterval 30
RedirectURL http://kimatemyshorts.net
PreAuthIdleTimeout 30
AuthIdleTimeout 480
FirewallRuleSet users-to-router {
FirewallRule allow tcp port 2050
FirewallRule allow tcp port 80
FirewallRule allow udp port 53
FirewallRule allow udp port 67
FirewallRule allow tcp port 22
}
Key decisions:
SessionTimeout 1440— 24 hour sessions so users don't get kicked every minuteCheckInterval 30— check every 30 seconds not every 1 second (reduces CPU load)FirewallRuleSet— allows unauthenticated clients to reach the portal on port 2050
The splash page lives at /etc/nodogsplash/htdocs/splash.html.
Critical design decision: Do NOT actually authenticate users through nodogsplash's $authaction flow. Instead, clicking ENTER THE NETWORK simply redirects to http://kimatemyshorts.net via JavaScript. This means:
- The user is never authenticated in nodogsplash's eyes
- Android/iPhone never detects "internet access"
- The captive portal browser stays open showing our site
- Users stay in the portal experience instead of being dropped to their home screen
<script>
function startAccess(){
// Show loading animation
document.getElementById('main-content').style.display='none';
document.getElementById('loading-box').style.display='block';
// After animation completes, redirect to our site
setTimeout(function(){
window.location.href='http://kimatemyshorts.net';
},2000);
}
</script>See splash.html in this repo for the full file.
Start nodogsplash:
/etc/init.d/nodogsplash enable
/etc/init.d/nodogsplash startInstall Ubuntu Server 26.04 LTS. During installation:
- Set hostname to
kimshare - Enable OpenSSH server
- Set a static IP of
192.168.1.50(or configure via netplan after install)
# /etc/netplan/00-installer-config.yaml
network:
ethernets:
eno1:
addresses:
- 192.168.1.50/24
routes:
- to: default
via: 192.168.1.1
nameservers:
addresses: [192.168.1.1]
version: 2sudo netplan applyThe 5TB HDD stores all files and logs. Mount it permanently:
sudo mkdir -p /mnt/hdd
sudo mount /dev/sdc2 /mnt/hdd
# Get UUID
sudo blkid /dev/sdc2
# Add to fstab for auto-mount on boot
echo 'UUID=YOUR-UUID-HERE /mnt/hdd ext4 defaults 0 2' | sudo tee -a /etc/fstabGive your user ownership:
sudo chown -R rac:rac /mnt/hdd
sudo chmod -R 755 /mnt/hddsudo apt update
sudo apt install -y nginx python3 python3-flask python3-pip
sudo pip3 install flask-sock --break-system-packagesnginx serves the static HTML files and proxies API and WebSocket requests to Flask.
# /etc/nginx/sites-available/kimnode
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/kimnode;
index index.html;
server_name _;
client_max_body_size 10G;
location / {
try_files $uri $uri/ =404;
}
location /api/ {
proxy_pass http://127.0.0.1:5000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /admin/ {
auth_basic "RESTRICTED";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://127.0.0.1:5000/admin/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /ws/ {
proxy_pass http://127.0.0.1:5000/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400;
}
}sudo ln -s /etc/nginx/sites-available/kimnode /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxsudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd raccoonThe Flask app handles all dynamic functionality — file operations, logging, chat, and admin.
See app.py in this repo for the full file. Key endpoints:
| Endpoint | Method | Description |
|---|---|---|
/api/files |
GET | List files in a directory |
/api/upload |
POST | Upload files |
/api/download |
GET | Download a file |
/api/preview/<path> |
GET | Serve file for inline preview |
/api/delete |
POST | Delete file or folder |
/api/mkdir |
POST | Create a folder |
/api/move |
POST | Move a file |
/api/log |
POST | Write to activity log |
/api/checkban |
GET | Check if a MAC is banned |
/api/ban |
POST | Ban a MAC address |
/api/unban |
POST | Unban a MAC address |
/api/chat/history |
GET | Get last 100 chat messages |
/api/chat/clear |
POST | Clear chat (admin only) |
/ws/chat |
WebSocket | Real-time chat |
/admin/kimadmin_raccoon |
GET | Admin panel (nginx auth required) |
Run Flask as a persistent service:
# /etc/systemd/system/kimnode.service
[Unit]
Description=KimNode Flask API
After=network.target
[Service]
User=rac
WorkingDirectory=/var/www/kimnode/api
ExecStart=/usr/bin/python3 app.py
Restart=always
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable kimnode
sudo systemctl start kimnodeFiles go in /var/www/kimnode/:
index.html— main retro interface (handle login, home/chat/files/info tabs)disclaimer.html— NOT used by the portal (kept as backup)
- Green/cyan/orange/purple tab color scheme
- Handle-based login (no accounts, no passwords)
- Real-time WebSocket chat with persistent message history
- File browser with breadcrumb navigation and back button
- Inline media previews — images, video, audio
- Upload, download, move, delete, mkdir
- Activity logging on all actions
Access at: http://kimatemyshorts.net/admin/kimadmin_raccoon
You will be prompted for the nginx basic auth password you set earlier.
Features:
- Ban a user — enter their MAC address and a reason
- Banned users list — see all bans with dates and reasons, unban with one click
- Clear chat — wipe all chat messages for all users
- Activity log — full log of every join, upload, download, delete, move, and chat message with IP address, MAC address, user agent, and handle
When users perform actions on the site, the JavaScript sends a POST to /api/log. nginx forwards the request to Flask and includes the X-Real-IP header with the client's real IP. Flask then looks up that IP in /proc/net/arp to find the associated MAC address and writes it to the log.
This means visible in the log is exactly which physical device did what, even if they use a different handle each time.
- Go to the admin panel
- Find the MAC address of the offending device in the activity log
- Enter the MAC and a reason in the BAN section
- Click BAN
The ban is stored in /mnt/hdd/banned_macs.json. The site checks the ban list on page load — banned users see an ACCESS DENIED page instead of the normal site.
To unban, click UNBAN next to their entry in the admin panel.
Router (OpenWrt)
├── /etc/nodogsplash/
│ ├── nodogsplash.conf # Portal configuration
│ └── htdocs/
│ └── splash.html # Captive portal page
└── /etc/config/
├── wireless # WiFi config
└── dhcp # DNS config
Ubuntu PC
├── /var/www/kimnode/
│ ├── index.html # Main retro site
│ └── api/
│ └── app.py # Flask backend
├── /etc/nginx/sites-available/
│ └── kimnode # nginx config
├── /etc/systemd/system/
│ └── kimnode.service # Systemd service
└── /mnt/hdd/
├── files/ # User uploaded files
├── admin.log # Activity log
└── banned_macs.json # Ban list
ls /sys/class/ieee80211/
iw phy phy0 interface add wlan0 type managed
wifi reloadCheck nodogsplash is running and the conf file exists:
ps | grep nodo
nodogsplash -f 2>&1 | head -10Make sure session timeouts are set correctly in nodogsplash.conf:
SessionTimeout 1440
CheckInterval 30
If timeouts are set to 1 minute, users get kicked every minute causing constant reconnects.
sudo systemctl status kimnode
sudo journalctl -u kimnode -n 20Common issues:
- Port 5000 already in use:
sudo fuser -k 5000/tcp - Permission denied on HDD:
sudo chown -R rac:rac /mnt/hdd
The X-Real-IP header must be passed from nginx to Flask. Verify:
grep "X-Real-IP" /etc/nginx/sites-available/kimnodeAdd the PC's MAC to the trusted list so nodogsplash doesn't block it:
uci add_list nodogsplash.@nodogsplash[0].trustedmac='XX:XX:XX:XX:XX:XX'
uci commit nodogsplash
/etc/init.d/nodogsplash restartWhy nodogsplash instead of openNDS? Started with openNDS but found it extremely slow on the MIPS-based Archer C7. Every portal request runs a large shell script through libopennds.sh which the old hardware struggles with. nodogsplash is a lightweight C binary that handles the same job much faster.
Why not authenticate users at the portal? When nodogsplash authenticates a user, Android and iOS detect "internet access" and automatically close the captive portal browser. The goal is for users to stay inside the portal experience. By never authenticating — just redirecting to our site — the portal browser stays open and users get the full retro experience without needing to open a separate browser.
Why a separate PC instead of serving from the router? The Archer C7 has 128MB RAM and a slow MIPS processor. Running nginx, Flask, WebSockets, and serving large files from it would be painful. The mini PC handles everything with ease and the 5TB HDD provides massive storage that would be impossible on the router.
Why Ubuntu over other options? Ubuntu Server LTS is stable, well-documented, and has great package support for everything needed — nginx, Python, Flask, flask-sock. No compilation required, everything installs cleanly.
Why VT323 and Share Tech Mono?
The whole aesthetic is a neighborhood BBS from the early 90s. VT323 is a proper terminal font, Share Tech Mono handles smaller body text. The green-on-black CRT scanline effect is achieved with a CSS repeating-linear-gradient overlay fixed to the viewport.
- The admin panel URL contains the admin secret as a path component (
/admin/kimadmin_raccoon). Changekimadmin_raccooninapp.pyto something unique. - nginx basic auth adds a second layer of password protection to the admin panel.
- The ban system uses MAC addresses which can be spoofed. It is not a hard security barrier, just a deterrent for casual bad actors on a neighborhood network.
- This system is designed for a trusted local community. Do not expose it to the internet.
Built through extensive trial and error. The hardest part is getting the captive portal to behave on modern Android and iOS — the trick is to never authenticate at all.
Stack:
- OpenWrt 25.12.3 (ath79)
- nodogsplash 5.0.2
- Ubuntu 26.04 LTS
- nginx
- Python 3 / Flask / flask-sock
- VT323 + Share Tech Mono (Google Fonts)
KIM ATE MY SHORTS — a free network for the people, by the autists.
