A self-hosted network looking glass with a Next.js frontend and a Bun/Hono backend. Supports ping, traceroute, MTR, and BGP lookups from one or more nodes.
looking-glass/
├── backend/ # Bun + Hono API — runs on each network node
└── frontend/ # Next.js UI — runs once, points at one or more backends
The backend exposes a small HTTP API that runs system network tools (ping, traceroute, mtr, birdc). Deploy one instance per node/location you want to expose.
The frontend is a single Next.js app configured with the URLs of all your backend nodes. It can be hosted anywhere — Vercel, a VPS, etc.
- Bun v1.0+
- Linux system tools:
ping,traceroute,mtr - BIRD2 (
birdc) — only required if you want BGP lookups
- Node.js 18+ and npm (or Bun)
Repeat these steps on each node you want to add.
Update the package list and install the required network tools:
sudo apt update && sudo apt upgrade -y
sudo apt install -y iputils-ping traceroute mtr-tinyIf you want BGP lookups via BIRD2:
sudo apt install -y bird2Verify the tools are available:
ping -c 1 1.1.1.1
traceroute -w 1 -q 1 1.1.1.1
mtr -rwbc 1 1.1.1.1
birdc show status # only if bird2 is installedcurl -fsSL https://bun.sh/install | bash
source ~/.bashrcConfirm the install and note the path — you'll need it for the systemd unit:
which bun
# e.g. /root/.bun/bin/bunIf Bun was installed via nvm or another version manager the path may differ (e.g.
/root/.nvm/versions/node/v26.4.0/bin/bun). Create a stable symlink so the service path never breaks when versions change:sudo ln -s $(which bun) /usr/local/bin/bunThen use
ExecStart=/usr/local/bin/bun run src/index.tsin the service file.
Lock down the node so only SSH and your frontend server can reach the backend port.
sudo apt install -y ufw
# Default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# SSH — keep this open or you will lose access
sudo ufw allow 22/tcp
# Backend API — allow only your frontend server's IP
sudo ufw allow from <frontend-server-ip> to any port 8080 proto tcp
# Enable
sudo ufw enable
sudo ufw status verboseReplace <frontend-server-ip> with the public IP of the server running your frontend. To add more frontend servers later:
sudo ufw allow from <another-ip> to any port 8080 proto tcpExpected output after ufw status verbose:
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
8080/tcp ALLOW IN <frontend-server-ip>
22/tcp (v6) ALLOW IN Anywhere (v6)
scp -r looking-glass/backend user@your-node:/opt/looking-glassOr clone the repo directly on the node.
cd /opt/looking-glass
bun installEdit config.json:
{
"port": 8080
}| Field | Description |
|---|---|
port |
Port the API listens on. Default 8080. |
Development (hot reload):
bun run devProduction:
bun run startCreate /etc/systemd/system/looking-glass.service:
[Unit]
Description=NodeByte Looking Glass Backend
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/looking-glass
ExecStart=/root/.bun/bin/bun run src/index.ts
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload
systemctl enable --now looking-glass| Endpoint | Description |
|---|---|
GET /lg/ping?ip=<target> |
5-packet ICMP ping |
GET /lg/traceroute?ip=<target> |
Traceroute with 1s timeout, 1 probe per hop |
GET /lg/mtr?ip=<target> |
MTR report (5 cycles) |
GET /lg/bgp?ip=<target> |
BGP route lookup via birdc (IP or CIDR subnet) |
All endpoints accept an IPv4/IPv6 address or a domain name, except BGP which requires an IP or CIDR prefix.
cd looking-glass/frontend
npm installEdit config.json. This file controls branding and the list of backend nodes shown in the UI.
{
"brand": {
"name": "NodeByte",
"logo": "https://example.com/logo.svg",
"invertLogo": true
},
"locations": [
{
"name": "Europe",
"backends": [
{
"name": "Helsinki, Finland",
"url": "http://65.21.161.115:8080",
"info": {
"ipv4": "65.21.161.115",
"ipv6": "2a01:4f9::1",
"datacenter": "HEL-1",
"location": "Europe"
}
}
]
}
]
}| Field | Description |
|---|---|
name |
Display name shown in the header and footer. |
logo |
URL to your logo image. |
invertLogo |
Set true if your logo is dark and needs inverting in light mode. |
An array of location groups. Each group has a name (used as a label in the node selector) and a backends array.
| Field | Description |
|---|---|
name |
Node display name shown in the selector and node grid. |
url |
Base URL of the backend API for this node. |
info.ipv4 |
(Optional) IPv4 address shown in the node info card. |
info.ipv6 |
(Optional) IPv6 address shown in the node info card. |
info.datacenter |
(Optional) Datacenter identifier. |
info.location |
(Optional) Human-readable location string. |
All info fields are optional — omit or leave empty to hide them from the UI.
Development:
npm run devProduction build:
npm run build
npm run startThe frontend runs on port 3000 by default. Use a reverse proxy (nginx, Caddy) to expose it publicly with TLS.
Keep the Next.js process running as a systemd service on your frontend host.
Create /etc/systemd/system/looking-glass-frontend.service:
[Unit]
Description=NodeByte Looking Glass Frontend
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/looking-glass/frontend
ExecStart=/usr/bin/npm run start
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3000
[Install]
WantedBy=multi-user.targetBuild and enable:
cd /opt/looking-glass/frontend
npm run build
systemctl daemon-reload
systemctl enable --now looking-glass-frontendNote: The
User=field should match whichever user owns/opt/looking-glass/frontend. If you're running as root, replacewww-datawithroot.
Each backend node needs its own service. The unit file is the same on every node.
Create /etc/systemd/system/looking-glass-backend.service:
[Unit]
Description=NodeByte Looking Glass Backend
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/looking-glass/backend
ExecStart=/root/.bun/bin/bun run src/index.ts
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.targetEnable:
systemctl daemon-reload
systemctl enable --now looking-glass-backendCheck the Bun binary path on your system with which bun and update ExecStart if it differs from /root/.bun/bin/bun.
Place this in /etc/nginx/sites-available/looking-glass and symlink it to sites-enabled.
server {
listen 80;
listen [::]:80;
server_name lg.yourdomain.com;
# Redirect all HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name lg.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/lg.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/lg.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}ln -s /etc/nginx/sites-available/looking-glass /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginxObtain a certificate with Certbot if you don't have one yet:
certbot --nginx -d lg.yourdomain.comIf you want to front the backend with nginx on each node (e.g. for TLS or access logging), add a server block on the node:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name node1.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/node1.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/node1.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Restrict access to the frontend server's IP only
allow <frontend-server-ip>;
deny all;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Then update the url field in the frontend config.json to use https://node1.yourdomain.com instead of the bare IP and port.
- The backend runs commands directly on the host system. Do not expose the backend API publicly — it should only be reachable by your frontend server.
- BGP lookups require BIRD2 to be installed and running with
birdcaccessible in$PATH. - The frontend fetches directly from backend URLs in the browser, so backend nodes must be reachable from end-users if you do not proxy through the frontend.