# Moku · reach this PC from anywhere with Tailcat. # https://portholelab.com/ssh · v2 (this file never changes; fixes ship as v3) # # Run it in a normal Terminal / PowerShell window (not as administrator): # irm https://portholelab.com/ssh/v2/away.ps1 | iex # To let only your phone in, set its key (Moku › Settings › SSH) first: # $env:MOKU_ALLOW='nodekey:...'; irm https://portholelab.com/ssh/v2/away.ps1 | iex # # What this does: # 1. Checks SSH is already on (turn it on first with the command on # portholelab.com/ssh). # 2. Downloads the Tailcat 0.7.0 release file # (https://github.com/tailscale/tailcat), from portholelab.com's copy # or else from GitHub, refusing any file whose SHA-256 differs from the # one written below. It lives in your own AppData folder; no # administrator rights needed. # 3. Makes this PC a lasting Tailcat address, once. # 4. Runs Tailcat in this window, passing connections to this PC'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 PC through # Tailcat any more. Nothing keeps running in the background, and SSH still # asks for your password on every login. # # Everything runs from Invoke-MokuTailcat, called on the very last line, so # a download cut short runs nothing. function Invoke-MokuTailcat { $zh = (Get-UICulture).Name -like 'zh*' function Say([string]$Cn, [string]$En, [string]$Color = 'Gray') { Write-Host $(if ($zh) { $Cn } else { $En }) -ForegroundColor $Color } function Step([string]$Cn, [string]$En) { Write-Host '' Say "==> $Cn" "==> $En" 'White' } $version = '0.7.0' $assets = @{ 'AMD64' = @{ Name = 'windows_amd64'; Sha256 = 'f04cac07e3bf6c700b0b543df0d3315a3b312cd5a05ee76714e1cc848e9b2dff' } 'ARM64' = @{ Name = 'windows_arm64'; Sha256 = '1c2d081375ade2365402f9fcf611e89dbb2c5c5ee06936aaff2b205dbe427578' } } $identity = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() if ($identity.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Say '这一步不需要管理员权限,也不应该用管理员运行。请关掉这个窗口,打开普通的「终端」,再粘贴运行。' ` "This step doesn't need, and shouldn't have, admin rights. Close this window, open a normal Terminal, and paste it there." 'Yellow' return } $allow = "$env:MOKU_ALLOW".Trim() if ($allow -and $allow -notmatch '^nodekey:[0-9a-fA-F]+(,nodekey:[0-9a-fA-F]+)*$') { Say 'MOKU_ALLOW 应该是 nodekey: 开头的一串字符。' 'MOKU_ALLOW should start with nodekey:' 'Red' return } Step '检查 SSH' 'Checking SSH' $sshd = Get-Service -Name sshd -ErrorAction SilentlyContinue if ($null -eq $sshd -or $sshd.Status -ne 'Running') { Say '这台电脑的 SSH 还没打开。请先按 portholelab.com/ssh 上的第一步打开,再运行这条命令。' ` "SSH isn't on yet. Turn it on with the first step on portholelab.com/ssh, then run this again." 'Yellow' return } Say '已打开。' "It's on." # Two servers on one key take turns dropping each other's connections. # Any Tailcat in this sign-in counts, not only this script's copy: one # installed some other way reads the same default key. $session = (Get-Process -Id $PID).SessionId $running = Get-Process -Name tailcat -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $session } if ($running) { Say 'Tailcat 已经在运行了(另一个窗口,或你自己启动的)。用那个就行,或者先把它关掉再运行这条命令。' ` 'Tailcat is already running (another window, or one you started yourself). Use that one, or stop it and run this again.' 'Yellow' return } Step "准备 Tailcat $version" "Getting Tailcat $version" $arch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } $asset = $assets[$arch] if ($null -eq $asset) { Say "Tailcat 没有给这种处理器($arch)的版本。" "Tailcat has no build for this processor ($arch)." 'Red' return } $dir = Join-Path $env:LOCALAPPDATA "Moku\tailcat\$version" $exe = Join-Path $dir 'tailcat.exe' if (-not (Test-Path $exe)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null $file = "tailcat_${version}_$($asset.Name).zip" $zip = Join-Path $dir $file [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $ProgressPreference = 'SilentlyContinue' $ok = $false # The copy on portholelab.com is for networks that can't reach # GitHub. Either way the file must match the SHA-256 above. foreach ($url in @("https://portholelab.com/ssh/tailcat/$version/$file", "https://github.com/tailscale/tailcat/releases/download/v$version/$file")) { Say "正在下载 $url" "Downloading $url" Remove-Item -Force $zip -ErrorAction SilentlyContinue try { Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $zip -TimeoutSec 120 -ErrorAction Stop } catch { continue } $got = (Get-FileHash -Algorithm SHA256 -Path $zip).Hash.ToLowerInvariant() if ($got -eq $asset.Sha256) { $ok = $true break } Say "校验不符(SHA-256 $got),已删除,没有运行。" "Checksum mismatch (SHA-256 $got). Deleted, nothing was run." 'Yellow' } if (-not $ok) { Remove-Item -Force $zip -ErrorAction SilentlyContinue Say '没能下载到校验一致的 Tailcat。请换个网络再试。' "Couldn't download a Tailcat that matches its checksum. Try another network." 'Red' return } Say "校验通过(SHA-256 $($asset.Sha256))。" "Checksum OK (SHA-256 $($asset.Sha256))." $unpack = Join-Path $dir 'unpack' Expand-Archive -Path $zip -DestinationPath $unpack -Force Move-Item -Force (Join-Path $unpack 'tailcat.exe') $exe Remove-Item -Recurse -Force $unpack, $zip } $keys = & $exe genkey --list 2>$null if (-not ($keys -contains 'default')) { Step '为这台电脑生成固定地址(只需一次)' 'Making a lasting address for this PC (once)' & $exe genkey --key=default --fixed-region if ($LASTEXITCODE -ne 0) { Say '生成失败。' "Couldn't make one." 'Red' return } } Step '启动 Tailcat' 'Starting Tailcat' Say '如果 Windows 防火墙弹窗询问 tailcat,点「取消」也能用(会经中继连接,稍慢一点)。' ` 'If Windows Firewall asks about tailcat, Cancel is fine too (it connects through a relay, a little slower).' $work = Join-Path ([IO.Path]::GetTempPath()) ('moku-tailcat-' + [Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $work | Out-Null $addrFile = Join-Path $work 'address' $serveArgs = @('serve') if ($allow) { $serveArgs += "--allow=$allow" } $serveArgs += '22' $proc = $null try { $env:TAILCAT_ADDR_FILE = $addrFile $proc = Start-Process -FilePath $exe -ArgumentList $serveArgs -NoNewWindow -PassThru $deadline = (Get-Date).AddSeconds(60) while (-not ((Test-Path $addrFile) -and (Get-Item $addrFile).Length -gt 0)) { if ($proc.HasExited) { Say 'Tailcat 没能启动,请看上面的错误信息。' "Tailcat didn't start; see the error above." 'Red' return } if ((Get-Date) -gt $deadline) { Say 'Tailcat 60 秒内没有就绪,请检查网络。' "Tailcat wasn't ready within 60 seconds. Check the network." 'Red' return } Start-Sleep -Milliseconds 500 } $address = (Get-Content -Raw $addrFile).Trim() Remove-Item -Force $addrFile # The login name rides along in the QR code so Moku can fill it in. $user = $env:USERNAME if ($env:USERDOMAIN -and $env:USERDOMAIN -ne $env:COMPUTERNAME) { $user = "$env:USERDOMAIN\$env:USERNAME" } Write-Host '' 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.' 'Green' Say " 用户名:$user" " Username: $user" 'White' Say ' 密码: 你登录 Windows 的密码。用微软账户登录的,就是微软账户密码(不是开机 PIN 码)。' ` ' Password: your Windows password. With a Microsoft account, that account''s password (not your PIN).' 'White' if (-not $allow) { Say "这个地址相当于钥匙:别发到群里或公开的地方。泄露了就运行:& '$exe' genkey --key=default --fixed-region --force 换一个。" ` "Treat that address like a key: don't post it anywhere public. If it leaks, run: & '$exe' genkey --key=default --fixed-region --force" 'Yellow' } Say '保持这个窗口打开;关掉窗口或按 Ctrl+C 就停止远程访问。' 'Keep this window open; closing it or pressing Ctrl+C stops remote access.' # The page reads this after "#", which browsers never send to a server. if ($address -match '^tc[A-Za-z0-9_-]+$') { $target = $address $escaped = [uri]::EscapeDataString($user) if ($escaped -match '^[A-Za-z0-9._~%-]+$') { $target = 'ssh://' + $escaped + '@' + $address } Start-Process -FilePath 'explorer.exe' -ArgumentList ('"https://portholelab.com/ssh/#show=' + [uri]::EscapeDataString($target) + '"') } $proc.WaitForExit() } finally { Remove-Item Env:\TAILCAT_ADDR_FILE -ErrorAction SilentlyContinue if ($null -ne $proc -and -not $proc.HasExited) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue } } Invoke-MokuTailcat