#!/bin/sh # Moku · turn on SSH so your phone can reach this computer. # https://portholelab.com/ssh · v2 (this file never changes; fixes ship as v3) # # What this does: # 1. Installs the OpenSSH server with your package manager, if missing. # 2. Starts it now and at every boot. # 3. If ufw or firewalld is active and this computer is on a private # network, allows SSH from that local network only. # 4. Prints your username and this computer's address, and opens a page # with a QR code for the Moku app to scan. # # What it does not do: edit sshd_config, create users or keys, change any # password, or send anything to anyone. Only the commands that need root # run through sudo, which asks for your password. # # Everything runs from main(), called on the very last line, so a download # cut short runs nothing. MOKU_PAGE='https://portholelab.com/ssh/' zh() { case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in zh*) return 0 ;; esac return 1 } # say "中文" "English" 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'; } warn() { printf '\033[33m'; say "$1" "$2"; printf '\033[0m'; } fail() { printf '\033[31m'; say "$1" "$2"; printf '\033[0m'; exit 1; } # The address other devices on this network use to reach this one. primary_ip() { ip='' if command -v ip >/dev/null 2>&1; then # Asks the routing table only; nothing is sent. ip=$(ip -4 route get 1.1.1.1 2>/dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p' | head -n 1) fi if [ -z "$ip" ] && command -v hostname >/dev/null 2>&1; then ip=$(hostname -I 2>/dev/null | awk '{print $1}') fi printf '%s' "$ip" } # The local network the primary address sits on, e.g. 192.168.1.0/24. local_subnet() { command -v ip >/dev/null 2>&1 || return 0 dev=$(ip -4 route get 1.1.1.1 2>/dev/null | sed -n 's/.* dev \([^ ]*\).*/\1/p' | head -n 1) [ -n "$dev" ] || return 0 ip -4 route show dev "$dev" proto kernel scope link 2>/dev/null | awk -v src="$1" '$0 ~ ("src " src "( |$)") {print $1; exit}' } is_private() { case "$1" in 10.*|192.168.*) return 0 ;; 172.1[6-9].*|172.2[0-9].*|172.3[01].*) return 0 ;; esac return 1 } install_sshd() { if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update -qq && $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openssh-server elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y openssh-server elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y openssh-server elif command -v zypper >/dev/null 2>&1; then $SUDO zypper --non-interactive install openssh-server || $SUDO zypper --non-interactive install openssh elif command -v pacman >/dev/null 2>&1; then $SUDO pacman -S --needed --noconfirm openssh elif command -v apk >/dev/null 2>&1; then $SUDO apk add openssh-server else fail "没有识别出你的包管理器。请用系统自带的软件中心安装 OpenSSH Server,再运行一次。" \ "Couldn't find your package manager. Install OpenSSH Server from your software center, then run this again." fi } start_sshd() { if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then # Debian and Ubuntu call the unit ssh; everyone else calls it sshd. if systemctl list-unit-files ssh.service 2>/dev/null | grep -q '^ssh\.service'; then $SUDO systemctl enable --now ssh.service else $SUDO systemctl enable --now sshd.service fi elif command -v rc-service >/dev/null 2>&1; then $SUDO rc-update add sshd default >/dev/null 2>&1 $SUDO rc-service sshd start else fail "没法自动启动 SSH 服务(没有 systemd 或 OpenRC)。请手动启动 sshd。" \ "Couldn't start the SSH service automatically (no systemd or OpenRC). Please start sshd by hand." fi } # The port sshd really listens on; someone may have changed it. sshd_port() { port=$($SUDO sshd -T 2>/dev/null | awk '$1 == "port" {print $2; exit}') printf '%s' "${port:-22}" } open_firewall() { port=$1 subnet=$2 if command -v ufw >/dev/null 2>&1 && $SUDO ufw status 2>/dev/null | grep -q '^Status: active'; then say "ufw 已开启:只允许 $subnet(你的局域网)访问端口 $port。" \ "ufw is on: allowing port $port from $subnet (your local network) only." $SUDO ufw allow from "$subnet" to any port "$port" proto tcp comment 'Moku SSH (local network)' >/dev/null elif command -v firewall-cmd >/dev/null 2>&1 && $SUDO firewall-cmd --state >/dev/null 2>&1; then if [ "$port" = 22 ] && $SUDO firewall-cmd --query-service=ssh >/dev/null 2>&1; then return 0 fi say "firewalld 已开启:只允许 $subnet(你的局域网)访问端口 $port。" \ "firewalld is on: allowing port $port from $subnet (your local network) only." rule="rule family=\"ipv4\" source address=\"$subnet\" port port=\"$port\" protocol=\"tcp\" accept" $SUDO firewall-cmd --permanent --add-rich-rule="$rule" >/dev/null && $SUDO firewall-cmd --reload >/dev/null fi } main() { if grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null; then fail "这是 Windows 里的 WSL,手机连不到这里。请回到 portholelab.com/ssh 选 Windows,在 Windows 终端里运行那条命令。" \ "This is WSL inside Windows; your phone can't reach it. Go back to portholelab.com/ssh, pick Windows, and run that command in Windows Terminal." fi if [ "$(id -u)" = 0 ]; then SUDO='' elif command -v sudo >/dev/null 2>&1; then SUDO='sudo' say "有几步需要管理员权限,sudo 会问你这台电脑的登录密码(输入时不显示)。" \ "A few steps need admin rights; sudo will ask for this computer's login password (it won't show as you type)." else fail "需要 sudo 或 root 权限。请用 root 运行,或先安装 sudo。" \ "This needs sudo or root. Run it as root, or install sudo first." fi step "检查 OpenSSH 服务" "Checking the OpenSSH server" if command -v sshd >/dev/null 2>&1 || [ -x /usr/sbin/sshd ]; then say "已安装。" "Already installed." else install_sshd || fail "安装失败,请看上面的错误信息。" "Install failed; see the error above." fi step "启动 SSH(并设为开机自动启动)" "Starting SSH (and at every boot)" start_sshd || fail "启动失败,请看上面的错误信息。" "Couldn't start it; see the error above." port=$(sshd_port) addr=$(primary_ip) user=$(id -un) # Run through sudo, the person logging in is the one who typed sudo. if [ "$user" = root ] && [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != root ]; then user=$SUDO_USER fi if [ -n "$addr" ] && is_private "$addr"; then subnet=$(local_subnet "$addr") [ -n "$subnet" ] && open_firewall "$port" "$subnet" elif [ -n "$addr" ]; then warn "这台电脑的地址 $addr 不是家庭/办公室局域网地址,防火墙设置没有改动。" \ "This computer's address $addr isn't a home/office network address, so firewall settings were left alone." fi printf '\n\033[1;32m' say "完成。在 Moku 里填这些:" "Done. In Moku, enter:" printf '\033[0m' say " 用户名:$user" " Username: $user" say " 地址: ${addr:-?}" " Address: ${addr:-?}" [ "$port" = 22 ] || say " 端口: $port" " Port: $port" say " 密码: 你登录这台电脑用的密码" " Password: the one you log in to this computer with" if [ "$user" = root ]; then warn "多数系统默认不允许 root 用密码远程登录。如果连不上,请换成你平时登录用的普通账户。" \ "Most systems don't let root log in remotely with a password. If it won't connect, use your everyday account instead." fi if [ -z "$addr" ]; then warn "没找到这台电脑的网络地址。请确认它连着 Wi-Fi 或网线。" \ "Couldn't find this computer's network address. Check it's on Wi-Fi or Ethernet." return 0 fi # Only names that need no escaping go into the link; any other name is # typed in the app instead. case "$user" in *[!A-Za-z0-9._-]*) target="$addr" ;; *) target="$user@$addr" ;; esac # The page reads this after "#", which browsers never send to a server. url="${MOKU_PAGE}#show=ssh%3A%2F%2F$(printf '%s' "$target" | sed 's/@/%40/'):$port" printf '\n' say "或者在 Moku 里点「扫码导入」,扫这个页面上的二维码:" \ "Or tap \"Scan QR code\" in Moku and scan the code on this page:" printf ' %s\n' "$url" # Never as root: the browser would run with root's rights. if [ "$(id -u)" != 0 ] && [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] && command -v xdg-open >/dev/null 2>&1; then xdg-open "$url" >/dev/null 2>&1 & fi printf '\n' say "别让陌生人连你的 Wi-Fi:同一网络里的人都能尝试登录这台电脑。" \ "Keep strangers off your Wi-Fi: anyone on the same network can try to log in to this computer." } main "$@"