@nvwa-os/wormhole
v0.1.0
Published
CLI: Aliyun ECS spot wormhole (Shadowsocks) helper
Readme
wormhole
CLI to manage a single Aliyun ECS spot instance tagged for this tool: create, status, renew auto-release, delete.
Setup
bun installPublish / npm install
Published tarball includes dist/cli.js (esbuild bundle) and README.md only — src/ is not in the package (package.json files).
npm install -g @nvwa-os/wormhole
wormhole --helpNeeds Node 18+. Maintainers: npm run build (or let prepublishOnly run it on npm publish).
First run: interactive setup asks for AccessKey (or uses env) and region + availability zone, then creates or reuses a VPC (172.16.0.0/16), vSwitch, and security group tagged WormholeNetworkProfile=<your profile>. Instance image/size/spot settings use built-in defaults (edit config.json later if needed).
You can also run wormhole init / wormhole init --force, or copy config.example.json manually and fill IDs yourself.
Credentials via environment variables:
ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET, orACCESS_KEY_ID/ACCESS_KEY_SECRET
RAM custom policy (script-style)
Create a RAM policy (e.g. attach to a RAM user used only for wormhole) with the actions below. They match what this CLI calls today: init / network bootstrap (VPC + vSwitch + security group; default egress; optional AuthorizeSecurityGroup for the configured listener port range TCP+UDP) and instance lifecycle (RunInstances with inline tags, DescribeInstances, ModifyInstanceAutoReleaseTime periodically while watch is alive and the instance is Running).
Resource: "*" is the simplest shape; you can later tighten with resource-level authorization if your org requires it.
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecs:AuthorizeSecurityGroup",
"ecs:AuthorizeSecurityGroupEgress",
"ecs:CreateSecurityGroup",
"ecs:DeleteInstance",
"ecs:DescribeInstances",
"ecs:DescribeRegions",
"ecs:DescribeSecurityGroups",
"ecs:DescribeZones",
"ecs:ModifyInstanceAutoReleaseTime",
"ecs:RunInstances",
"vpc:CreateVSwitch",
"vpc:CreateVpc",
"vpc:DescribeVSwitches",
"vpc:DescribeVpcs"
],
"Resource": "*"
}
]
}Not included (unused by wormhole today): ecs:RevokeSecurityGroup / ecs:DescribeSecurityGroupAttribute — wormhole does not revoke ingress or list rule details. ecs:RevokeSecurityGroupEgress — wormhole only adds egress rules, it does not revoke them. resourcemanager:ListResourceGroups — only if you extend init to pick a non-default resource group for CreateVpc. If Aliyun returns errors about custom images, keys, spot quotas, or tagging, add the corresponding actions for your account (for example ecs:DescribeImages, ecs:TagResources, or related APIs) per the error text.
Usage
bun run src/cli.ts # default = up (+ foreground watch; Ctrl+C deletes VM)
bun run src/cli.ts --keep-instance # Ctrl+C only exits CLI; VM stays up
bun run src/cli.ts -d # up then detach watch daemon (pid under ~/.config/wormhole/)
bun run src/cli.ts init
bun run src/cli.ts status
bun run src/cli.ts status --probe # TCP check public_ip:wormhole_port (stderr + exit 1 if fail)
bun run src/cli.ts status --sip002 # print ss://… (password from config or last wormhole up)
bun run src/cli.ts up
bun run src/cli.ts renew
bun run src/cli.ts down --yesOptional: bun run src/cli.ts --config /path/to/config.json or … up -d
Install globally: bun link in this repo (or run via bun /path/to/src/cli.ts).
Behavior
- One managed instance per
aliyun.profile, identified by tagsWormholeManaged=trueandWormholeProfile=<profile>. - Default command is
up(runningwormholewith no subcommand is the same aswormhole up). If config is missing and stdin/stdout are a TTY, interactiveinitruns first (same as before). - After
upsucceeds (including “already running”), the CLI keeps running: it pollsDescribeInstancesonwatchPollSeconds(default 60) and prints a status line only when something changed (status, IP, port,auto_release, etc.) so the scrollback is not filled with duplicates andss://stays easy to copy. While Running, it callsModifyInstanceAutoReleaseTimeat most everyautoReleaseRefreshSeconds(default 300). Aliyun still requires that horizon to be at least ~30 minutes, soleaseMinutesbelow 30 is raised (effectiveLeaseMinutes). Ctrl+C / SIGTERM delete the instance unless--keep-instance.-dkeeps the same behavior in a child process. renewis still available for a one-off push when you are not running the watch.uppicks a new random port on each run inside the configured pool (default 42000–42999), stores it in tagWormholePort, and configures Shadowsocks + client output for that port. Override the pool withaliyun.wormholePortMin/aliyun.wormholePortMax. UnlessauthorizeSecurityGroupis false, the security group gets TCP+UDP for the whole pool once (idempotent on eachup/ init).- Shadowsocks server: first boot runs UserData (no custom image):
aptinstalls curl/xz, then downloads the shadowsocks-rust Linux.tar.xzand enableswormhole-ss.service.DescribeInstances→Runningonly means the VM is up — apt + download + systemd still run afterward (often ~1–3 minutes to a listening port; GitHub can be slower from some regions).- Faster cold start: put the exact tarball your CPU needs (
x86_64-unknown-linux-gnuoraarch64-unknown-linux-gnu) on same-region OSS / CDN and setaliyun.shadowsocksRustTarballUrlto thathttps://…URL (still paired withshadowsocksRustVersiononly when using the default GitHub URL). - Fastest: build a custom image with
wormhole-ssserver+ config already on disk and pointimageIdat it; then trim or replace UserData (not automated here — you’d fork or extend the provider). UserData is visible to anyone with ECS/API access on the account—same as any cloud bootstrap secret.
- Faster cold start: put the exact tarball your CPU needs (
- Security group ports: Inbound: one TCP and one UDP rule for
wormholePortMin/wormholePortMax(descriptionswormhole: ss listener range tcp|udp). Outbound for a proxy must allow connections to arbitrary remote ports (web, DNS, etc.). On init and on eachup, wormhole idempotently ensures an IPv4 egress rule ALL → 0.0.0.0/0 (and tries IPv6 ::/0 if applicable).down --yesdeletes the instance only; pool ingress rules stay on the security group until you remove them in the console (or change the pool and re-authorize).
How to tell if Shadowsocks is ready / debugging
wormhole status --probe— TCP only (checks something listens). It cannot speak Shadowsocks/AEAD by design — that needs a real SS client and yourss://line.wormhole status --sip002— Prints oness://…line to stdout. Password comes fromaliyun.shadowsocksPasswordif set, otherwise from~/.config/wormhole/last-session.json(written after a successfulupon this machine, keyed byprofile). Import into Shadowrocket / Clash / Surge / Outline / shadowsocks-android, then browse — end‑to‑end check.wormhole up— Also printssip002=once and saves the same credentials (permissions 0600) forstatus --sip002later.SSH on the instance (when you have a key/login path): UserData log is typically
/var/log/cloud-init-output.log. Service checks:sudo systemctl status wormhole-ss.servicesudo journalctl -u wormhole-ss.service -b --no-pagersudo ss -tlnp | grep <wormhole_port>(see listener)- Config path from bootstrap:
/opt/wormhole/ss-config.json, binary:/usr/local/bin/wormhole-ssserver.
APIs used for network bootstrap
Aligned with your list: DescribeRegions, DescribeZones, DescribeSecurityGroups, CreateSecurityGroup, CreateVpc, DescribeVpcs.
Also required (ECS needs a subnet): CreateVSwitch + DescribeVSwitches (VPC API).
Not wired yet: ListResourceGroups — only needed if you want new VPCs under a non-default resource group (ResourceGroupId on CreateVpc). Optional: DescribeVpcAttribute for richer VPC status polling.
See RAM custom policy above for the exact Action list.
