Kieran's opinionated (and probably slightly dumb) nix config

feat: add a fancy bore landing page

dunkirk.sh 4b53e7b0 4898ecfd

verified
Changed files
+351 -1
modules
home
apps
nixos
+1 -1
modules/home/apps/frpc.nix
···
auth.tokenSource.file.path = "${cfg.authTokenFile}"
[[proxies]]
-
name = "$subdomain-tunnel"
+
name = "$subdomain"
type = "http"
localIP = "127.0.0.1"
localPort = $port
+315
modules/nixos/services/bore-dashboard.html
···
+
<!DOCTYPE html>
+
<html lang="en">
+
+
<head>
+
<meta charset="UTF-8">
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
<title>🚇 bore</title>
+
<style>
+
* {
+
margin: 0;
+
padding: 0;
+
box-sizing: border-box;
+
}
+
+
body {
+
font-family: 'SF Mono', 'Monaco', monospace;
+
background: #0d1117;
+
color: #e6edf3;
+
padding: 2rem;
+
line-height: 1.5;
+
}
+
+
.container {
+
max-width: 1200px;
+
margin: 0 auto;
+
}
+
+
header {
+
margin-bottom: 3rem;
+
}
+
+
h1 {
+
font-size: 2.5rem;
+
background: linear-gradient(135deg, #14b8a6, #fb923c);
+
-webkit-background-clip: text;
+
-webkit-text-fill-color: transparent;
+
margin-bottom: 0.5rem;
+
}
+
+
.subtitle {
+
color: #8b949e;
+
font-size: 0.95rem;
+
}
+
+
.stats {
+
display: grid;
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+
gap: 1rem;
+
margin-bottom: 2rem;
+
}
+
+
.stat-card {
+
background: #161b22;
+
border: 1px solid #30363d;
+
border-radius: 0;
+
padding: 1.5rem;
+
}
+
+
.stat-label {
+
color: #8b949e;
+
font-size: 0.85rem;
+
margin-bottom: 0.5rem;
+
}
+
+
.stat-value {
+
font-size: 2rem;
+
font-weight: 600;
+
color: #14b8a6;
+
}
+
+
.stat-value.orange {
+
color: #fb923c;
+
}
+
+
.section {
+
background: #161b22;
+
border: 1px solid #30363d;
+
border-radius: 0;
+
padding: 1.5rem;
+
margin-bottom: 2rem;
+
}
+
+
h2 {
+
color: #14b8a6;
+
font-size: 1.2rem;
+
margin-bottom: 1.5rem;
+
display: flex;
+
align-items: center;
+
gap: 0.5rem;
+
}
+
+
.tunnel-list {
+
display: flex;
+
flex-direction: column;
+
gap: 1rem;
+
}
+
+
.tunnel {
+
background: #0d1117;
+
border: 1px solid #30363d;
+
border-radius: 0;
+
padding: 1rem;
+
display: grid;
+
grid-template-columns: 1fr auto;
+
gap: 1rem;
+
align-items: center;
+
}
+
+
.tunnel-icon {
+
display: none;
+
}
+
+
.tunnel-info {
+
flex: 1;
+
}
+
+
.tunnel-name {
+
color: #e6edf3;
+
font-weight: 600;
+
margin-bottom: 0.25rem;
+
}
+
+
.tunnel-url {
+
color: #8b949e;
+
font-size: 0.85rem;
+
}
+
+
.tunnel-url a {
+
color: #14b8a6;
+
text-decoration: none;
+
}
+
+
.tunnel-url a:hover {
+
text-decoration: underline;
+
}
+
+
.tunnel-status {
+
padding: 0.25rem 0.75rem;
+
border-radius: 0;
+
font-size: 0.8rem;
+
font-weight: 500;
+
}
+
+
.status-online {
+
background: rgba(20, 184, 166, 0.2);
+
color: #14b8a6;
+
border: 1px solid #14b8a6;
+
}
+
+
.empty-state {
+
text-align: center;
+
padding: 3rem 1rem;
+
color: #8b949e;
+
}
+
+
.empty-icon {
+
font-size: 3rem;
+
margin-bottom: 1rem;
+
opacity: 0.5;
+
}
+
+
code {
+
background: #0d1117;
+
padding: 0.2rem 0.5rem;
+
border-radius: 0;
+
color: #fb923c;
+
font-size: 0.9rem;
+
}
+
+
.usage {
+
background: #0d1117;
+
padding: 1rem;
+
border-radius: 0;
+
margin-top: 1rem;
+
}
+
+
.usage pre {
+
color: #8b949e;
+
font-size: 0.9rem;
+
overflow-x: auto;
+
}
+
+
.last-updated {
+
text-align: center;
+
color: #8b949e;
+
font-size: 0.8rem;
+
margin-top: 2rem;
+
}
+
</style>
+
</head>
+
+
<body>
+
<div class="container">
+
<header>
+
<h1>🚇 bore</h1>
+
<p class="subtitle">tunnel dashboard • bore.dunkirk.sh</p>
+
</header>
+
+
<div class="stats">
+
<div class="stat-card">
+
<div class="stat-label">active tunnels</div>
+
<div class="stat-value" id="activeTunnels">—</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-label">server status</div>
+
<div class="stat-value orange" id="serverStatus">—</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-label">active connections</div>
+
<div class="stat-value" id="totalConnections">—</div>
+
</div>
+
</div>
+
+
<div class="section">
+
<h2>~ active tunnels</h2>
+
<div class="tunnel-list" id="tunnelList">
+
<div class="empty-state">
+
<div class="empty-icon">🚇</div>
+
<p>no active tunnels</p>
+
</div>
+
</div>
+
</div>
+
+
<div class="last-updated">
+
last updated: <span id="lastUpdated">never</span>
+
</div>
+
</div>
+
+
<script>
+
let fetchFailCount = 0;
+
const MAX_FAIL_COUNT = 3;
+
+
async function fetchStats() {
+
try {
+
// Fetch server info
+
const serverResponse = await fetch('/api/serverinfo');
+
if (!serverResponse.ok) throw new Error('API unavailable');
+
const serverData = await serverResponse.json();
+
+
// Fetch HTTP proxies (tunnels)
+
const proxiesResponse = await fetch('/api/proxy/http');
+
const proxiesData = await proxiesResponse.json();
+
+
// Reset fail count on success
+
fetchFailCount = 0;
+
+
// Update stats
+
document.getElementById('activeTunnels').textContent = serverData.clientCounts || 0;
+
document.getElementById('serverStatus').textContent = 'online';
+
document.getElementById('totalConnections').textContent = serverData.curConns || 0;
+
+
// Update tunnels list
+
const tunnelList = document.getElementById('tunnelList');
+
const proxies = proxiesData.proxies || [];
+
+
if (proxies.length === 0) {
+
tunnelList.innerHTML = `
+
<div class="empty-state">
+
<div class="empty-icon">🚇</div>
+
<p>no active tunnels</p>
+
</div>
+
`;
+
} else {
+
tunnelList.innerHTML = proxies.map(proxy => {
+
const subdomain = proxy.conf.subdomain;
+
const url = `https://${subdomain}.bore.dunkirk.sh`;
+
const statusClass = proxy.status === 'online' ? 'status-online' : '';
+
+
return `
+
<div class="tunnel">
+
<div class="tunnel-icon">→</div>
+
<div class="tunnel-info">
+
<div class="tunnel-name">${proxy.name}</div>
+
<div class="tunnel-url">
+
<a href="${url}" target="_blank">${url}</a>
+
</div>
+
<div style="color: #8b949e; font-size: 0.75rem; margin-top: 0.25rem;">
+
started: ${proxy.lastStartTime} • traffic in: ${formatBytes(proxy.todayTrafficIn)} • out: ${formatBytes(proxy.todayTrafficOut)}
+
</div>
+
</div>
+
<div class="tunnel-status ${statusClass}">${proxy.status}</div>
+
</div>
+
`;
+
}).join('');
+
}
+
+
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
+
} catch (error) {
+
fetchFailCount++;
+
document.getElementById('serverStatus').textContent = 'offline';
+
console.error('Failed to fetch stats:', error);
+
+
// Reload page if failed multiple times (server might have updated)
+
if (fetchFailCount >= MAX_FAIL_COUNT) {
+
console.log('Multiple fetch failures detected, reloading page...');
+
window.location.reload();
+
}
+
}
+
}
+
+
function formatBytes(bytes) {
+
if (bytes === 0) return '0 B';
+
const k = 1024;
+
const sizes = ['B', 'KB', 'MB', 'GB'];
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
+
}
+
+
// Fetch immediately and then every 5 seconds
+
fetchStats();
+
setInterval(fetchStats, 5000);
+
</script>
+
</body>
+
+
</html>
+35
modules/nixos/services/frps.nix
···
bindPort = ${toString cfg.bindPort}
vhostHTTPPort = ${toString cfg.vhostHTTPPort}
+
# Dashboard and Prometheus metrics
+
webServer.addr = "127.0.0.1"
+
webServer.port = 7400
+
enablePrometheus = true
+
# Authentication token - clients need this to connect
auth.method = "token"
${tokenConfig}
···
# Automatically configure Caddy for wildcard domain
services.caddy = lib.mkIf cfg.enableCaddy {
+
# Dashboard for base domain
+
virtualHosts."${cfg.domain}" = {
+
extraConfig = ''
+
tls {
+
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
+
}
+
header {
+
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+
}
+
+
# Proxy /metrics to frps dashboard
+
handle /metrics {
+
reverse_proxy localhost:7400
+
}
+
+
# Proxy /api/* to frps dashboard
+
handle /api/* {
+
reverse_proxy localhost:7400
+
}
+
+
# Serve dashboard HTML
+
handle {
+
root * ${./.}
+
try_files bore-dashboard.html
+
file_server
+
}
+
'';
+
};
+
+
# Wildcard subdomain proxy to frps
virtualHosts."*.${cfg.domain}" = {
extraConfig = ''
tls {