Kieran's opinionated (and probably slightly dumb) nix config
at main 18 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.atelier.bore; 9 10 boreScript = pkgs.writeShellScript "bore" '' 11 CONFIG_FILE="bore.toml" 12 13 # Check for flags 14 if [ "$1" = "--list" ] || [ "$1" = "-l" ]; then 15 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Active tunnels" 16 echo 17 18 tunnels=$(${pkgs.curl}/bin/curl -s https://${cfg.domain}/api/proxy/http) 19 20 if ! echo "$tunnels" | ${pkgs.jq}/bin/jq -e '.proxies | length > 0' >/dev/null 2>&1; then 21 ${pkgs.gum}/bin/gum style --foreground 117 "No active tunnels" 22 exit 0 23 fi 24 25 # Filter only online tunnels with valid conf 26 echo "$tunnels" | ${pkgs.jq}/bin/jq -r '.proxies[] | select(.status == "online" and .conf != null) | if .type == "http" then "\(.name) https://\(.conf.subdomain).${cfg.domain} [http]" elif .type == "tcp" then "\(.name) tcp://\(.conf.remotePort) localhost:\(.conf.localPort) [tcp]" elif .type == "udp" then "\(.name) udp://\(.conf.remotePort) localhost:\(.conf.localPort) [udp]" else "\(.name) [\(.type)]" end' | while read -r line; do 27 ${pkgs.gum}/bin/gum style --foreground 35 " $line" 28 done 29 exit 0 30 fi 31 32 if [ "$1" = "--saved" ] || [ "$1" = "-s" ]; then 33 if [ ! -f "$CONFIG_FILE" ]; then 34 ${pkgs.gum}/bin/gum style --foreground 117 "No bore.toml found in current directory" 35 exit 0 36 fi 37 38 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Saved tunnels in bore.toml" 39 echo 40 41 # Parse TOML and show tunnels 42 while IFS= read -r line; do 43 if [[ "$line" =~ ^\[([^]]+)\] ]]; then 44 current_tunnel="''${BASH_REMATCH[1]}" 45 elif [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then 46 port="''${BASH_REMATCH[1]}" 47 elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then 48 protocol="''${BASH_REMATCH[1]}" 49 elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then 50 label="''${BASH_REMATCH[1]}" 51 proto_display="''${protocol:-http}" 52 ${pkgs.gum}/bin/gum style --foreground 35 " $current_tunnel localhost:$port [$proto_display] [$label]" 53 label="" 54 protocol="" 55 elif [[ -z "$line" ]] && [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then 56 proto_display="''${protocol:-http}" 57 ${pkgs.gum}/bin/gum style --foreground 35 " $current_tunnel localhost:$port [$proto_display]" 58 current_tunnel="" 59 port="" 60 protocol="" 61 fi 62 done < "$CONFIG_FILE" 63 64 # Handle last entry if file doesn't end with blank line 65 if [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then 66 proto_display="''${protocol:-http}" 67 if [[ -n "$label" ]]; then 68 ${pkgs.gum}/bin/gum style --foreground 35 " $current_tunnel localhost:$port [$proto_display] [$label]" 69 else 70 ${pkgs.gum}/bin/gum style --foreground 35 " $current_tunnel localhost:$port [$proto_display]" 71 fi 72 fi 73 exit 0 74 fi 75 76 # Get tunnel name/subdomain 77 if [ -n "$1" ]; then 78 tunnel_name="$1" 79 else 80 # Check if we have a bore.toml in current directory 81 if [ -f "$CONFIG_FILE" ]; then 82 # Count tunnels in TOML 83 tunnel_count=$(${pkgs.gnugrep}/bin/grep -c '^\[' "$CONFIG_FILE" 2>/dev/null || echo "0") 84 85 if [ "$tunnel_count" -gt 0 ]; then 86 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel" 87 echo 88 89 # Show choice between new or saved 90 choice=$(${pkgs.gum}/bin/gum choose "New tunnel" "Use saved tunnel") 91 92 if [ "$choice" = "Use saved tunnel" ]; then 93 # Extract tunnel names from TOML 94 saved_names=$(${pkgs.gnugrep}/bin/grep '^\[' "$CONFIG_FILE" | ${pkgs.gnused}/bin/sed 's/^\[\(.*\)\]$/\1/') 95 tunnel_name=$(echo "$saved_names" | ${pkgs.gum}/bin/gum choose) 96 97 if [ -z "$tunnel_name" ]; then 98 ${pkgs.gum}/bin/gum style --foreground 196 "No tunnel selected" 99 exit 1 100 fi 101 102 # Parse TOML for this tunnel's config 103 in_section=false 104 while IFS= read -r line; do 105 if [[ "$line" =~ ^\[([^]]+)\] ]]; then 106 if [[ "''${BASH_REMATCH[1]}" = "$tunnel_name" ]]; then 107 in_section=true 108 else 109 in_section=false 110 fi 111 elif [[ "$in_section" = true ]]; then 112 if [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then 113 port="''${BASH_REMATCH[1]}" 114 elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then 115 protocol="''${BASH_REMATCH[1]}" 116 elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then 117 label="''${BASH_REMATCH[1]}" 118 fi 119 fi 120 done < "$CONFIG_FILE" 121 122 proto_display="''${protocol:-http}" 123 ${pkgs.gum}/bin/gum style --foreground 35 " Loaded from bore.toml: $tunnel_name localhost:$port [$proto_display]''${label:+ [$label]}" 124 else 125 # New tunnel - prompt for protocol first to determine what to ask for 126 protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp") 127 if [ -z "$protocol" ]; then 128 protocol="http" 129 fi 130 131 if [ "$protocol" = "http" ]; then 132 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ") 133 else 134 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ") 135 fi 136 137 if [ -z "$tunnel_name" ]; then 138 ${pkgs.gum}/bin/gum style --foreground 196 "No name provided" 139 exit 1 140 fi 141 fi 142 else 143 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel" 144 echo 145 # Prompt for protocol first 146 protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp") 147 if [ -z "$protocol" ]; then 148 protocol="http" 149 fi 150 151 if [ "$protocol" = "http" ]; then 152 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ") 153 else 154 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ") 155 fi 156 157 if [ -z "$tunnel_name" ]; then 158 ${pkgs.gum}/bin/gum style --foreground 196 "No name provided" 159 exit 1 160 fi 161 fi 162 else 163 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel" 164 echo 165 # Prompt for protocol first 166 protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp") 167 if [ -z "$protocol" ]; then 168 protocol="http" 169 fi 170 171 if [ "$protocol" = "http" ]; then 172 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ") 173 else 174 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ") 175 fi 176 177 if [ -z "$tunnel_name" ]; then 178 ${pkgs.gum}/bin/gum style --foreground 196 "No name provided" 179 exit 1 180 fi 181 fi 182 fi 183 184 # Validate tunnel name (only for http subdomains) 185 if [ "$protocol" = "http" ]; then 186 if ! echo "$tunnel_name" | ${pkgs.gnugrep}/bin/grep -qE '^[a-z0-9-]+$'; then 187 ${pkgs.gum}/bin/gum style --foreground 196 "Invalid subdomain (use only lowercase letters, numbers, and hyphens)" 188 exit 1 189 fi 190 fi 191 192 # Get port (skip if loaded from saved config) 193 if [ -z "$port" ]; then 194 if [ -n "$2" ]; then 195 port="$2" 196 else 197 port=$(${pkgs.gum}/bin/gum input --placeholder "8000" --prompt "Local port: ") 198 if [ -z "$port" ]; then 199 ${pkgs.gum}/bin/gum style --foreground 196 "No port provided" 200 exit 1 201 fi 202 fi 203 fi 204 205 # Validate port 206 if ! echo "$port" | ${pkgs.gnugrep}/bin/grep -qE '^[0-9]+$'; then 207 ${pkgs.gum}/bin/gum style --foreground 196 "Invalid port (must be a number)" 208 exit 1 209 fi 210 211 # Get optional protocol, label and save flag (skip if loaded from saved config) 212 save_config=false 213 if [ -z "$label" ]; then 214 shift 2 2>/dev/null || true 215 while [[ $# -gt 0 ]]; do 216 case "$1" in 217 --protocol|-p) 218 protocol="$2" 219 shift 2 220 ;; 221 --label|-l) 222 label="$2" 223 shift 2 224 ;; 225 --save) 226 save_config=true 227 shift 228 ;; 229 *) 230 shift 231 ;; 232 esac 233 done 234 235 # Prompt for protocol if not provided via flag and not loaded from saved config and not already set 236 if [ -z "$protocol" ]; then 237 protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp") 238 if [ -z "$protocol" ]; then 239 protocol="http" 240 fi 241 fi 242 243 # Prompt for label if not provided via flag and not loaded from saved config 244 if [ -z "$label" ]; then 245 # Allow multiple labels selection 246 labels=$(${pkgs.gum}/bin/gum choose --no-limit --header "Labels (select multiple):" "dev" "prod" "custom") 247 248 if [ -n "$labels" ]; then 249 # Check if custom was selected 250 if echo "$labels" | ${pkgs.gnugrep}/bin/grep -q "custom"; then 251 custom_label=$(${pkgs.gum}/bin/gum input --placeholder "my-label" --prompt "Custom label: ") 252 if [ -z "$custom_label" ]; then 253 ${pkgs.gum}/bin/gum style --foreground 196 "No custom label provided" 254 exit 1 255 fi 256 # Replace 'custom' with the actual custom label 257 labels=$(echo "$labels" | ${pkgs.gnused}/bin/sed "s/custom/$custom_label/") 258 fi 259 # Join labels with comma 260 label=$(echo "$labels" | ${pkgs.coreutils}/bin/tr '\n' ',' | ${pkgs.gnused}/bin/sed 's/,$//') 261 fi 262 fi 263 fi 264 265 # Default protocol to http if still not set 266 if [ -z "$protocol" ]; then 267 protocol="http" 268 fi 269 270 # Check if local port is accessible 271 if ! ${pkgs.netcat}/bin/nc -z 127.0.0.1 "$port" 2>/dev/null; then 272 ${pkgs.gum}/bin/gum style --foreground 214 "! Warning: Nothing listening on localhost:$port" 273 fi 274 275 # Save configuration if requested 276 if [ "$save_config" = true ]; then 277 # Check if tunnel already exists in TOML 278 if [ -f "$CONFIG_FILE" ] && ${pkgs.gnugrep}/bin/grep -q "^\[$tunnel_name\]" "$CONFIG_FILE"; then 279 # Update existing entry 280 ${pkgs.gnused}/bin/sed -i "/^\[$tunnel_name\]/,/^\[/{ 281 s/^port[[:space:]]*=.*/port = $port/ 282 s/^protocol[[:space:]]*=.*/protocol = \"$protocol\"/ 283 ''${label:+s/^label[[:space:]]*=.*/label = \"$label\"/} 284 }" "$CONFIG_FILE" 285 else 286 # Append new entry 287 { 288 echo "" 289 echo "[$tunnel_name]" 290 echo "port = $port" 291 if [ "$protocol" != "http" ]; then 292 echo "protocol = \"$protocol\"" 293 fi 294 if [ -n "$label" ]; then 295 echo "label = \"$label\"" 296 fi 297 } >> "$CONFIG_FILE" 298 fi 299 300 ${pkgs.gum}/bin/gum style --foreground 35 " Configuration saved to bore.toml" 301 echo 302 fi 303 304 # Create config file 305 config_file=$(${pkgs.coreutils}/bin/mktemp) 306 trap "${pkgs.coreutils}/bin/rm -f $config_file" EXIT 307 308 # Encode label into proxy name if provided (format: tunnel_name[label1,label2]) 309 proxy_name="$tunnel_name" 310 if [ -n "$label" ]; then 311 proxy_name="''${tunnel_name}[''${label}]" 312 fi 313 314 # Build proxy configuration based on protocol 315 if [ "$protocol" = "http" ]; then 316 ${pkgs.coreutils}/bin/cat > $config_file <<EOF 317 serverAddr = "${cfg.serverAddr}" 318 serverPort = ${toString cfg.serverPort} 319 320 auth.method = "token" 321 auth.tokenSource.type = "file" 322 auth.tokenSource.file.path = "${cfg.authTokenFile}" 323 324 [[proxies]] 325 name = "$proxy_name" 326 type = "http" 327 localIP = "127.0.0.1" 328 localPort = $port 329 subdomain = "$tunnel_name" 330 EOF 331 elif [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then 332 # For TCP/UDP, enable admin API to query allocated port 333 # Use Python to find a free port (cross-platform and guaranteed to work) 334 admin_port=$(${pkgs.python3}/bin/python3 -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()') 335 336 ${pkgs.coreutils}/bin/cat > $config_file <<EOF 337 serverAddr = "${cfg.serverAddr}" 338 serverPort = ${toString cfg.serverPort} 339 340 auth.method = "token" 341 auth.tokenSource.type = "file" 342 auth.tokenSource.file.path = "${cfg.authTokenFile}" 343 344 webServer.addr = "127.0.0.1" 345 webServer.port = $admin_port 346 347 [[proxies]] 348 name = "$proxy_name" 349 type = "$protocol" 350 localIP = "127.0.0.1" 351 localPort = $port 352 remotePort = 0 353 EOF 354 else 355 ${pkgs.gum}/bin/gum style --foreground 196 "Invalid protocol: $protocol (must be http, tcp, or udp)" 356 exit 1 357 fi 358 359 # Start tunnel 360 echo 361 ${pkgs.gum}/bin/gum style --foreground 35 " Tunnel configured" 362 ${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port" 363 if [ "$protocol" = "http" ]; then 364 public_url="https://$tunnel_name.${cfg.domain}" 365 ${pkgs.gum}/bin/gum style --foreground 117 " Public: $public_url" 366 else 367 ${pkgs.gum}/bin/gum style --foreground 117 " Protocol: $protocol" 368 ${pkgs.gum}/bin/gum style --foreground 214 " Waiting for server to allocate port..." 369 fi 370 echo 371 ${pkgs.gum}/bin/gum style --foreground 214 "Connecting to ${cfg.serverAddr}:${toString cfg.serverPort}..." 372 echo 373 374 # For TCP/UDP, capture output to parse allocated port 375 if [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then 376 # Start frpc in background and capture its PID 377 ${pkgs.frp}/bin/frpc -c $config_file 2>&1 | while IFS= read -r line; do 378 echo "$line" 379 380 # Look for successful proxy start 381 if echo "$line" | ${pkgs.gnugrep}/bin/grep -q "start proxy success"; then 382 # Wait a moment for the proxy to fully initialize 383 sleep 1 384 385 # Query the frpc admin API for proxy status 386 proxy_status=$(${pkgs.curl}/bin/curl -s http://127.0.0.1:$admin_port/api/status 2>/dev/null || echo "{}") 387 388 # Try to extract remote port from JSON response 389 # Format: "remote_addr":"bore.dunkirk.sh:20097" 390 remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".tcp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null) 391 if [ -z "$remote_addr" ] || [ "$remote_addr" = "null" ]; then 392 remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".udp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null) 393 fi 394 395 # Extract just the port number 396 remote_port=$(echo "$remote_addr" | ${pkgs.gnugrep}/bin/grep -oP ':\K[0-9]+$') 397 398 if [ -n "$remote_port" ] && [ "$remote_port" != "null" ]; then 399 echo 400 ${pkgs.gum}/bin/gum style --foreground 35 " Tunnel established" 401 ${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port" 402 ${pkgs.gum}/bin/gum style --foreground 117 " Remote: ${cfg.serverAddr}:$remote_port" 403 ${pkgs.gum}/bin/gum style --foreground 117 " Type: $protocol" 404 echo 405 fi 406 fi 407 done 408 else 409 exec ${pkgs.frp}/bin/frpc -c $config_file 410 fi 411 ''; 412 413 bore = pkgs.stdenv.mkDerivation { 414 pname = "bore"; 415 version = "1.0"; 416 417 dontUnpack = true; 418 419 nativeBuildInputs = with pkgs; [ pandoc installShellFiles ]; 420 421 manPageSrc = ./bore.1.md; 422 bashCompletionSrc = ./completions/bore.bash; 423 zshCompletionSrc = ./completions/bore.zsh; 424 fishCompletionSrc = ./completions/bore.fish; 425 426 buildPhase = '' 427 # Convert markdown man page to man format 428 ${pkgs.pandoc}/bin/pandoc -s -t man $manPageSrc -o bore.1 429 ''; 430 431 installPhase = '' 432 mkdir -p $out/bin 433 434 # Install binary 435 cp ${boreScript} $out/bin/bore 436 chmod +x $out/bin/bore 437 438 # Install man page 439 installManPage bore.1 440 441 # Install completions 442 installShellCompletion --bash --name bore $bashCompletionSrc 443 installShellCompletion --zsh --name _bore $zshCompletionSrc 444 installShellCompletion --fish --name bore.fish $fishCompletionSrc 445 ''; 446 447 meta = with lib; { 448 description = "Secure tunneling service CLI"; 449 homepage = "https://bore.dunkirk.sh"; 450 license = licenses.mit; 451 maintainers = [ ]; 452 }; 453 }; 454in 455{ 456 options.atelier.bore = { 457 enable = lib.mkEnableOption "bore tunneling service"; 458 459 serverAddr = lib.mkOption { 460 type = lib.types.str; 461 default = "bore.dunkirk.sh"; 462 description = "bore server address"; 463 }; 464 465 serverPort = lib.mkOption { 466 type = lib.types.port; 467 default = 7000; 468 description = "bore server port"; 469 }; 470 471 domain = lib.mkOption { 472 type = lib.types.str; 473 default = "bore.dunkirk.sh"; 474 description = "Domain for public tunnel URLs"; 475 }; 476 477 authTokenFile = lib.mkOption { 478 type = lib.types.nullOr lib.types.path; 479 default = null; 480 description = "Path to file containing authentication token"; 481 }; 482 }; 483 484 config = lib.mkIf cfg.enable { 485 home.packages = [ 486 pkgs.frp 487 bore 488 ]; 489 }; 490}