') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - alebeck/boring: The `boring` SSH tunnel manager · GitHub
Skip to content

Repository files navigation

The boring tunnel manager

A simple command line SSH tunnel manager that just works.

GitHub Actions Workflow StatusGitHub ReleaseGo Report CardCoverage StatusStatic Badge

Get it: brew install boring

Demo

Screenshot

Features

  • Ultra lightweight and fast
  • Local, remote and dynamic (SOCKS5) port forwarding
  • Works with SSH config and ssh-agent
  • Supports Unix sockets
  • Automatic re-connection and keep-alives
  • Human-friendly TOML configuration
  • Cross platform support
  • Smart shell completions

Usage

Usage:
boring list, l [-g <group>] List all tunnels
boring open, o (-a | -g <group> | <patterns>...)
<patterns>... Open tunnels matching any glob pattern
-a, --all Open all tunnels
-g, --group <group> Open all tunnels in a group
boring close, c Close tunnels (same options as 'open')
boring edit, e Edit the configuration file
boring version, v Show the version number
boring help, h Show this help message

Configuration

By default, boring reads its configuration from ~/.boring.toml on macOS and Windows, and from $XDG_CONFIG_HOME/boring/.boring.toml on Linux. If $XDG_CONFIG_HOME is not set, it defaults to ~/.config. The location of the config file can be overriden by setting $BORING_CONFIG. The config is a simple TOML file describing your tunnels:

# simple tunnel
[[tunnels]]
name = "dev"local = "9000"remote = "localhost:9000"host = "dev-server"# automatically matches host against SSH config# example of an explicit host (no SSH config)
[[tunnels]]
name = "prod"local = "5001"remote = "localhost:5001"host = "prod.example.com"user = "root"identity = "~/.ssh/id_prod"# will try default ones if not set# ... more tunnels

Currently, supported options at tunnel level are:

OptionDescription
nameAlias for the tunnel. Required.
localLocal address. Can be a "$host:$port" network address or a Unix socket. Can be abbreviated as "$port" in local and socks modes. Required in local, remote and socks modes.
remoteRemote address. As above, but can be abbreviated in remote and socks-remote modes. Required in local, remote and socks-remote modes.
hostEither a host alias that matches SSH configs or the actual hostname. Required.
modeMode of the tunnel. Can be either "local", "remote", "socks" or "socks-remote". Default is "local".
userSSH user. If not set, tries to read it from SSH config, defaulting to $USER.
identitySSH identity file. If not set, tries to read it from SSH config and ssh-agent, defaulting to standard identity files.
portSSH port. If not set, tries to read it from SSH config, defaulting to 22.
groupGroup that the tunnel is assigned to. Groups are only shown in list view if at least one tunnel has a group assigned. Can be used for grouped open, close, and list.

Options that can be provided at global and tunnel level (tunnel level takes precedence):

OptionDescription
keep_aliveKeep-alive interval in seconds. Default: 120 (2 minutes).

You can influence the behavior of boring via a couple of environment variables:

Show
VariableDescriptionDefault
$BORING_CONFIGConfig file location~/.boring.toml (Mac & Windows) and $XDG_CONFIG_HOME/boring/.boring.toml(Linux)
$BORING_LOG_FILELog file location/tmp/boringd.log
$BORING_SOCKSocket location/tmp/boringd.sock
$DEBUGEnable verbose logging

Installation

Homebrew

brew install boring

Pre-built

Get one of the pre-built binaries from the releases page. Then move the binary to a location in your $PATH.

Build yourself

git clone https://github.com/alebeck/boring &&cd boring
make

Then move the binary in dist to a location in your $PATH.

Note for Windows users Windows is fully supported since release 0.6.0. Users currently have to build from source, which is very easy. Make sure Go >= 1.25 is installed and then compile via
git clone https://github.com/alebeck/boring &&cd boring
.\build_win.bat

Then, move the executable to a location in your %PATH%.

Shell completion

Shell completion scripts are available for bash, zsh, and fish.

If boring was installed via Homebrew, and you have Homebrew completions enabled, nothing needs to be done.

Otherwise, install completions by adding the following to your shell's config file:

Bash

eval"$(boring --shell bash)"

Zsh

source<(boring --shell zsh)

Fish

boring --shell fish |source

Further Links

Credits

Go gopher logo by Renee French.

About

The `boring` SSH tunnel manager

Resources

Contributing

Stars

1.7k stars

Watchers

7 watching

Forks

Releases

Used by

Contributors

Languages