#!/bin/sh # Moku · reach this computer from anywhere with Tailcat (macOS and Linux). # https://portholelab.com/ssh · v2 (this file never changes; fixes ship as v3) # # Run it as yourself, not as root: # curl -fsSL https://portholelab.com/ssh/v2/away.sh | sh # To let only your phone in, put its key (Moku › Settings › SSH) in front: # curl -fsSL https://portholelab.com/ssh/v2/away.sh | MOKU_ALLOW=nodekey:... sh # # What this does: # 1. Checks SSH is already on (turn it on first with the command on # portholelab.com/ssh). # 2. Gets Tailcat 0.7.0 (https://github.com/tailscale/tailcat): on Linux # the release file, from portholelab.com's copy or else from GitHub, # refusing any file whose SHA-256 differs from the one written below; # on macOS through Homebrew. It lives in your home folder, no root # needed. # 3. Makes this computer a lasting Tailcat address, once. # 4. Runs Tailcat in this window, passing connections to this computer's # own SSH, and opens a page with the address and your # username as a QR code. # # Close this window (or press Ctrl+C) and nobody can reach this computer # through Tailcat any more. Nothing keeps running in the background, and # SSH still asks for your password on every login. # # Everything runs from main(), called on the very last line, so a download # cut short runs nothing. MOKU_PAGE='https://portholelab.com/ssh/' TC_VERSION='0.7.0' TC_BASE="https://github.com/tailscale/tailcat/releases/download/v$TC_VERSION" TC_MIRROR="https://portholelab.com/ssh/tailcat/$TC_VERSION" zh() { case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in zh*) return 0 ;; esac [ "$(uname -s)" = Darwin ] && defaults read -g AppleLanguages 2>/dev/null | sed -n '2p' | grep -q 'zh' } say() { if zh; then printf '%s\n' "$1"; else printf '%s\n' "$2"; fi; } step() { printf '\n\033[1m==> '; say "$1" "$2"; printf '\033[0m'; } # To stderr: it also runs inside $(...), whose output is a path. fail() { { printf '\033[31m'; say "$1" "$2"; printf '\033[0m'; } >&2; exit 1; } ssh_on() { if [ "$(uname -s)" = Darwin ]; then nc -z -G 1 127.0.0.1 22 >/dev/null 2>&1 elif command -v ss >/dev/null 2>&1; then ss -Hltn 'sport = :22' 2>/dev/null | grep -q . else nc -z 127.0.0.1 22 >/dev/null 2>&1 fi } # fetch URL FILE: gives up on a stalled connection instead of hanging. fetch() { if command -v curl >/dev/null 2>&1; then curl -fL --proto '=https' --tlsv1.2 --connect-timeout 15 \ --speed-limit 1024 --speed-time 20 --progress-bar -o "$2" "$1" >&2 else wget --timeout=20 -O "$2" "$1" >&2 fi } sha256_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' else shasum -a 256 "$1" | awk '{print $1}' fi } # Prints the path of a Tailcat binary this script can trust. linux_tailcat() { case "$(uname -m)" in x86_64|amd64) asset=linux_amd64 sum=23c0b1887a5ec422f0d18a9c52b4f5357815febdaae738a1eb54036d10bd9ee6 ;; aarch64|arm64) asset=linux_arm64 sum=bbb1ab50f24f00effe1e1fd86d0501803fb80793a90785a2a16ff3428f03d8ef ;; armv7l|armv7*) asset=linux_armv7 sum=cad3994b1f336b67e8a3a5273a9cecb44331d14d57eb32bfe7d0919adeab22b7 ;; *) fail "Tailcat 没有给这种处理器($(uname -m))的版本。" \ "Tailcat has no build for this processor ($(uname -m))." ;; esac dir="${XDG_DATA_HOME:-$HOME/.local/share}/moku/tailcat/$TC_VERSION" if [ -x "$dir/tailcat" ]; then printf '%s' "$dir/tailcat" return 0 fi mkdir -p "$dir" || fail "无法创建 $dir" "Couldn't create $dir" file="tailcat_${TC_VERSION}_$asset.tar.gz" archive="$dir/$file" ok='' # The copy on portholelab.com is for networks that can't reach GitHub. # Either way the file must match the SHA-256 above before it runs. for url in "$TC_MIRROR/$file" "$TC_BASE/$file"; do say "正在下载 $url" "Downloading $url" >&2 rm -f "$archive" if fetch "$url" "$archive"; then got=$(sha256_of "$archive") if [ "$got" = "$sum" ]; then ok=1 break fi say "校验不符(SHA-256 $got),已删除,没有运行。" \ "Checksum mismatch (SHA-256 $got). Deleted, nothing was run." >&2 fi done if [ -z "$ok" ]; then rm -f "$archive" fail "没能下载到校验一致的 Tailcat。请换个网络再试。" \ "Couldn't download a Tailcat that matches its checksum. Try another network." fi say "校验通过(SHA-256 $sum)。" "Checksum OK (SHA-256 $sum)." >&2 if ! tar -xzf "$archive" -C "$dir" tailcat; then fail "解压失败。" "Couldn't unpack it." fi rm -f "$archive" chmod 755 "$dir/tailcat" printf '%s' "$dir/tailcat" } mac_tailcat() { if command -v brew >/dev/null 2>&1; then if ! brew list --formula tailcat >/dev/null 2>&1; then say "正在用 Homebrew 安装 Tailcat……" "Installing Tailcat with Homebrew..." >&2 brew install tailcat >&2 || fail "Homebrew 安装失败。" "Homebrew couldn't install it." fi printf '%s' "$(brew --prefix)/bin/tailcat" else fail "Mac 上需要先装 Homebrew(https://brew.sh),再运行一次。" \ "On a Mac this needs Homebrew (https://brew.sh) first. Then run this again." fi } main() { if [ "$(id -u)" = 0 ]; then fail "请不要用 root 或 sudo 运行:Tailcat 用你自己的身份运行就够了。" \ "Don't run this as root or with sudo; Tailcat only needs to run as you." fi allow="${MOKU_ALLOW:-}" case "$allow" in ''|nodekey:*) ;; *) fail "MOKU_ALLOW 应该是 nodekey: 开头的一串字符。" "MOKU_ALLOW should start with nodekey:" ;; esac case "$allow" in *[!A-Za-z0-9:,]*) fail "MOKU_ALLOW 里有无效字符。" "MOKU_ALLOW has invalid characters." ;; esac step "检查 SSH" "Checking SSH" if ! ssh_on; then fail "这台电脑的 SSH 还没打开。请先按 portholelab.com/ssh 上第一步打开,再运行这条命令。" \ "SSH isn't on yet. Turn it on with the first step on portholelab.com/ssh, then run this again." fi say "已打开。" "It's on." # Two servers on one key take turns dropping each other's connections. # Any Tailcat of this user counts, not only this script's copy: one # installed some other way reads the same default key. if command -v pgrep >/dev/null 2>&1 && pgrep -u "$(id -u)" -x tailcat >/dev/null 2>&1; then fail "Tailcat 已经在运行了(另一个窗口,或你自己启动的)。用那个就行,或者先把它关掉再运行这条命令。" \ "Tailcat is already running (another window, or one you started yourself). Use that one, or stop it and run this again." fi step "准备 Tailcat $TC_VERSION" "Getting Tailcat $TC_VERSION" if [ "$(uname -s)" = Darwin ]; then tc=$(mac_tailcat); else tc=$(linux_tailcat); fi [ -x "$tc" ] || exit 1 if ! "$tc" genkey --list 2>/dev/null | grep -qx default; then step "为这台电脑生成固定地址(只需一次)" "Making a lasting address for this computer (once)" "$tc" genkey --key=default --fixed-region || fail "生成失败。" "Couldn't make one." fi step "启动 Tailcat" "Starting Tailcat" work=$(mktemp -d) || exit 1 addr_file="$work/address" if [ -n "$allow" ]; then TAILCAT_ADDR_FILE="$addr_file" "$tc" serve --allow="$allow" 22 & else TAILCAT_ADDR_FILE="$addr_file" "$tc" serve 22 & fi pid=$! trap 'kill "$pid" 2>/dev/null; rm -rf "$work"' EXIT # HUP: the window was closed. trap 'exit 129' HUP trap 'exit 130' INT TERM tries=0 while [ ! -s "$addr_file" ]; do kill -0 "$pid" 2>/dev/null || fail "Tailcat 没能启动,请看上面的错误信息。" "Tailcat didn't start; see the error above." tries=$((tries + 1)) [ "$tries" -lt 120 ] || fail "Tailcat 60 秒内没有就绪,请检查网络。" "Tailcat wasn't ready within 60 seconds. Check the network." sleep 0.5 done address=$(cat "$addr_file") rm -f "$addr_file" # The login name rides along in the QR code so Moku can fill it in; it # is only left out when it has characters a URL would need to escape. user=$(id -un) printf '\n\033[1;32m' say "Tailcat 已就绪。在 Moku 里点「扫码导入」扫描打开的页面,或把上面 tc 开头的地址发到手机粘贴。" \ "Tailcat is ready. In Moku, tap \"Scan QR code\" and scan the page that opened, or paste the tc… address above." printf '\033[0m' say " 用户名:$user" " Username: $user" say " 密码: 这台电脑的登录密码" " Password: the one you log in to this computer with" if [ -z "$allow" ]; then say "这个地址相当于钥匙:别发到群里或公开的地方。泄露了就运行:$tc genkey --key=default --fixed-region --force 换一个。" \ "Treat that address like a key: don't post it anywhere public. If it leaks, run: $tc genkey --key=default --fixed-region --force" fi say "保持这个窗口打开;关掉窗口或按 Ctrl+C 就停止远程访问。" \ "Keep this window open; closing it or pressing Ctrl+C stops remote access." target=$address case "$user" in ''|*[!A-Za-z0-9._-]*) ;; *) target="ssh://$user@$address" ;; esac url="${MOKU_PAGE}#show=$target" case "$address" in tc*[!A-Za-z0-9_-]*|'') url='' ;; tc*) ;; *) url='' ;; esac if [ -z "$url" ]; then : elif [ "$(uname -s)" = Darwin ]; then open "$url" 2>/dev/null elif [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] && command -v xdg-open >/dev/null 2>&1; then xdg-open "$url" >/dev/null 2>&1 & fi wait "$pid" } main "$@"