Initial commit

This commit is contained in:
Jason Thistlethwaite
2026-06-07 13:55:01 -04:00
commit 3dde79cfe2
5 changed files with 249 additions and 0 deletions
Executable
+27
View File
@@ -0,0 +1,27 @@
---
created: 2026-06-07T13:36:16-04:00
modified: 2026-06-07T13:49:20-04:00
---
# Copyright (c)2026 Jason Thistlethwaite
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
4. Individuals known as Brice Gottesman, as well as any organization employing or contracting with him in any capacity, are not permitted to use this source or binaries in any capacity whatsoever.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Provenance & Authorship
This software, including its architectural design, failover logic, and multi-host configuration schemas, was designed, engineered, and authored exclusively by Jason Thistlethwaite.
No other individuals, former contractors, or external helpers hold authorship, intellectual property rights, or foundational design claims over this codebase. Any representations to the contrary made to third parties, online portfolios, or professional networking platforms are entirely fraudulent.
### License Restrictions
Please note that this software is distributed under a restricted, source-available license. Under **Condition 4** of the included LICENSE file, specific individuals—namely Brice Gottesman, alongside any parent organizations, subsidiaries, or clients engaging his services—are strictly prohibited from downloading, running, modifying, or distributing this software in any capacity.
+96
View File
@@ -0,0 +1,96 @@
---
title: ssh-pipeweasel - manage complicated SSH network paths with ease
author: Jason Thistlethwaite
tags:
- ssh
- vpn
- wireguard
- wip
modified: 2026-06-07T13:49:45-04:00
---
# ssh-pipeweasel
ssh-pipeweasel is a tool for making complicated SSH connections easier to manage.
I created it to handle situations where I need to connect to various hosts that aren't always accessible for the internet, particularly from my laptop, where multiple routes could be possible like LAN, a WireGuard tunnel, or something else.
## Standard Usage
- Modify ~/.ssh/config and add `ProxyCommand ~/.ssh/ssh-pipeweasel.sh %h` to `Host` declarations you want to use it with.
- **Recommended**:
- Add `ServerAliveInterval 60` and `ServerAliveCountMax 3` to any connections using ssh-pipeweasel
- Copy `proxyconfig.example.sh` to `~/.ssh/hosts.d/`, rename it to match your host, and then edit it to list the options for that host
- **Testing it:**
- `DEBUG=true ~/.ssh/ssh-pipeweasel.sh <yourhost>`
- The above should output the availability and latency of all configured routes to that host
- **Using it:**
- `ssh <yourhost>` and ssh-pipeweasel automatically works in the background, selecting the best path to reach your configured ssh server.
## Installation and Setup
```bash
# Create directory for host config files
mkdir -p ~/.ssh/hosts.d/
# Set secure, default permissions on .ssh directories
chmod 0700 ~/.ssh/ && chmod 0700 ~/.ssh/hosts.d/
# Put the pipeweasel in place and mark it executable
cp ./ssh-pipeweasel.sh ~/.ssh/
chmod +x ~/.ssh/ssh-pipeweasel.sh
```
### Example Config File
```sh
# Order does not matter; it automatically selects the lowest latency response
ENDPOINTS=(
"LAN|192.168.1.10|22"
"WireGuard|10.11.0.10|22"
"Public|mybastion.uplinklounge.com|9002"
)
```
The file should be named after the Host alias you used inside of `~/.ssh/config`
### Example ssh_config (`~/.ssh/config`) file entry
```
# Connection muxing is a must-have for me, but this section is not needed for
# ssh-pipeweasel to function
ControlMaster auto
ControlPersist 4800
ControlPath ~/.ssh/control/%r.%n.%p.sock
# List of hosts that use the pipeweasel
Host bastion, mgmt01, mgmt02, pve
ProxyCommand ~/.ssh/ssh-pipeweasel.sh %h
ServerAliveInterval 60
ServerAliveCountMax 3
# Host-specific configs like port forwards, username, which key to use, etc.
Host bastion
IdentityFile ~/.ssh/id_ecdsa
User jason
LocalForward 13307 localhost:3306
LocalForward 18081 localhost:8081
DynamicForward localhost:19011
LocalForward 2222 localhost:2222
TCPKeepAlive yes
ExitOnForwardFailure no
```
# Dependencies
ssh-pipeweasel has been tested and is known to work on Ubuntu Linux 26.04 LTS.
It's only dependencies are:
- netcat
- Test with netcat-openbsd 1.206, which comes with Ubuntu by default
- bash
- GNU Awk
- openssh, I would think obviously
# Disclaimer
This tool is released without any particular warranty or claim it's actually useful or safe to use for anything. I'm posting this just in case other people have the same headache I have and this helps them.
## License & Authorship
This project is authored exclusively by Jason Thistlethwaite. It is distributed under a custom, restricted license. Specific individuals and affiliated entities are strictly prohibited from using this software.
Please see the full [LICENSE.md](./LICENSE.md) file for complete details, provenance documentation, and restriction clauses.
+6
View File
@@ -0,0 +1,6 @@
# Order does not matter; it automatically selects the lowest latency response
ENDPOINTS=(
"LAN|192.168.1.10|22"
"WireGuard|10.11.0.10|22"
"Public|mybastion.uplinklounge.com|9002"
)
+5
View File
@@ -0,0 +1,5 @@
---
tags: MOCs
---
```folder-index-content
```
+115
View File
@@ -0,0 +1,115 @@
#!/bin/bash
# Save to: ~/.ssh/ssh-failover.sh
TARGET_HOST="$1"
CONFIG_DIR="$HOME/.ssh/hosts.d"
# Force lower-case comparison to prevent case mismatches from SSH %h token
TARGET_LOWER=$(echo "$TARGET_HOST" | tr '[:upper:]' '[:lower:]')
CONFIG_FILE="$CONFIG_DIR/proxyrule.${TARGET_LOWER}.conf"
TIMEOUT=0.5
DEBUG=${DEBUG:-false}
if [ ! -f "$CONFIG_FILE" ]; then
echo "FAILOVER ERROR: Config file missing at $CONFIG_FILE" >&2
exit 1
fi
source "$CONFIG_FILE"
if [ ${#ENDPOINTS[@]} -eq 0 ]; then
echo "FAILOVER ERROR: No ENDPOINTS array found inside $CONFIG_FILE" >&2
exit 1
fi
# Helper function to resolve hostnames to IP addresses before the timer starts
resolve_to_ip() {
local target=$1
# Check if it's already an IPv4 or IPv6 address to save process execution
if [[ "$target" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ || "$target" =~ : ]]; then
echo "$target"
else
# Resolve using standard system hosts databases (handles DNS, /etc/hosts, etc.)
local ip
ip=$(getent hosts "$target" | awk '{print $1; exit}')
# Fall back to dig if getent isn't providing a quick single target ip
[ -z "$ip" ] && ip=$(dig +short "$target" | tail -n1)
# If both fail, return original target so nc can try anyway
echo "${ip:-$target}"
fi
}
get_tcp_ms() {
local host=$1
local port=$2
local start=$EPOCHREALTIME
if timeout "$TIMEOUT" nc -z "$host" "$port" 2>/dev/null; then
local end=$EPOCHREALTIME
awk "BEGIN {print int(($end - $start) * 1000)}"
else
echo "9999"
fi
}
declare -a results
declare -a resolved_hosts
fastest_ms=9999
chosen_index=""
for ((i=0; i<${#ENDPOINTS[@]}; i++)); do
IFS="|" read -r label host port <<< "${ENDPOINTS[$i]}"
# CRITICAL BUGFIX: Strip any hidden trailing newlines (\n) or carriage returns (\r)
port=$(echo "$port" | tr -d '\r\n[:space:]')
# Pre-resolve hostnames to eliminate DNS lookup latency skew
resolved_host=$(resolve_to_ip "$host")
resolved_hosts[$i]=$resolved_host
ms=$(get_tcp_ms "$resolved_host" "$port")
results[$i]=$ms
# if [ "$DEBUG" = true ]; then
# echo "Comparing $ms to $fastest_ms"
# fi
if [ "$ms" -lt "$fastest_ms" ]; then
fastest_ms=$ms
chosen_index=$i
fi
done
# --- DYNAMIC DEBUG DISPLAY LOOP ---
if [ "$DEBUG" = true ]; then
echo "--------------------------------------------------------" >&2
printf "Testing routes for: \e[1;34m%s\e[0m\n" "$TARGET_HOST" >&2
echo "--------------------------------------------------------" >&2
printf "%-25s | %-12s | %-10s\n" "Endpoint / Host" "Status" "Latency" >&2
echo "--------------------------------------------------------" >&2
for ((i=0; i<${#ENDPOINTS[@]}; i++)); do
IFS="|" read -r label host port <<< "${ENDPOINTS[$i]}"
ms=${results[$i]}
if [ "$ms" -eq 9999 ]; then
printf "%-25s | \e[31m%-12s\e[0m | %-10s\n" "$label" "OFFLINE" "---" >&2
else
printf "%-25s | \e[32m%-12s\e[0m | %-10s\n" "$label" "ONLINE" "${ms}ms" >&2
fi
done
echo "--------------------------------------------------------" >&2
fi
# 2. Route traffic to the fastest endpoint found
if [ -n "$chosen_index" ] && [ "$fastest_ms" -lt 9999 ]; then
IFS="|" read -r label host port <<< "${ENDPOINTS[$chosen_index]}"
port=$(echo "$port" | tr -d '\r\n[:space:]')
[ "$DEBUG" = true ] && echo "-> Routing via $label ($host:$port)..." >&2
exec nc "$host" "$port"
else
echo "FAILOVER ERROR: All paths to $TARGET_HOST are dead." >&2
exit 1
fi