Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "libts3ds"]
path = libts3ds
url = https://github.com/cadl/libts3ds.git
46 changes: 43 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ $(error "Please set DEVKITARM. Run via: tools/dkp.sh make")
endif

TOPDIR ?= $(CURDIR)
PROJECT_ROOT := $(patsubst %/,%,$(dir $(abspath $(firstword $(MAKEFILE_LIST)))))
include $(DEVKITARM)/3ds_rules

#---------------------------------------------------------------------------------
Expand All @@ -20,6 +21,8 @@ SOURCES := source
DATA := data
INCLUDES := include
ROMFS := romfs
LIBTS3DS_DIR := $(PROJECT_ROOT)/libts3ds
LIBTS3DS_A := $(LIBTS3DS_DIR)/build/3ds/libts3ds.a

APP_TITLE := 3DS SSH Client
APP_DESCRIPTION := SSH terminal with Chinese IME
Expand All @@ -34,13 +37,24 @@ CFLAGS := -g -Wall -O2 -mword-relocations \

CFLAGS += $(INCLUDE) -D__3DS__

# Diagnostic transport builds set this to auto, direct, peer-relay, or derp.
# It is compiled into the .3dsx and intentionally cannot be overridden by the
# SD-card configuration file.
DSSH_TAILSCALE_PATH ?= auto
CFLAGS += -DDSSH_TAILSCALE_PATH=\"$(DSSH_TAILSCALE_PATH)\"

# Keep libts3ds/microlink diagnostics out of the user's terminal by default.
# Set to 1 for a troubleshooting build that shows startup transport logs.
DSSH_TAILSCALE_VERBOSE ?= 0
CFLAGS += -DDSSH_TAILSCALE_VERBOSE=$(DSSH_TAILSCALE_VERBOSE)

CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11

ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)

# M3: citro2d/citro3d for top-screen ANSI terminal rendering.
LIBS := -lssh2 \
LIBS := -lts3ds -lssh2 \
-lmbedtls -lmbedx509 -lmbedcrypto \
-lcitro2d -lcitro3d \
-lctru -lm
Expand Down Expand Up @@ -77,9 +91,11 @@ export HFILES := $(addsuffix .h,$(BINFILES))

export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(LIBTS3DS_DIR)/include \
-I$(CURDIR)/$(BUILD)

export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
export LIBPATHS := -L$(LIBTS3DS_DIR)/build/3ds \
$(foreach dir,$(LIBDIRS),-L$(dir)/lib)

export _3DSXDEPS := $(if $(NO_SMDH),,$(OUTPUT).smdh)

Expand All @@ -104,7 +120,7 @@ ifneq ($(ROMFS),)
export _3DSXFLAGS += --romfs=$(CURDIR)/$(ROMFS)
endif

.PHONY: $(BUILD) clean all ime-dict test-ime cia cia-tools cia-clean
.PHONY: $(BUILD) clean all ime-dict test-ime test-config test-terminal cia cia-tools cia-clean

all: $(BUILD)

Expand All @@ -118,6 +134,14 @@ ime-dict:
test-ime:
@bash tools/test_ime.sh

# Host-side config parser tests (including quoted keychain passwords).
test-config:
@bash tools/test_config.sh

# Host-side terminal protocol tests (scrolling + fish cursor queries).
test-terminal:
@bash tools/test_terminal.sh

# ── M9: CIA packaging ───────────────────────────────────────────────
#
# `make cia` produces DSSH.cia from the already-built ELF + romfs.
Expand Down Expand Up @@ -177,6 +201,12 @@ cia-clean:
@rm -rf $(CIA_ASSETS) icon.png $(BUILD)/dssh.smdh $(BUILD)/dssh.bnr $(CIA_TARGET)

$(BUILD):
@test -f "$(LIBTS3DS_DIR)/Makefile.3ds" || { \
echo "error: libts3ds submodule is missing" >&2; \
echo "run: git submodule update --init --recursive" >&2; \
exit 1; \
}
@$(MAKE) --no-print-directory -C $(LIBTS3DS_DIR) -f Makefile.3ds all
@mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile

Expand All @@ -193,6 +223,16 @@ $(OUTPUT).3dsx : $(OUTPUT).elf $(_3DSXDEPS)
$(OFILES_SOURCES) : $(HFILES)
$(OUTPUT).elf : $(OFILES)

$(OUTPUT).elf: $(LIBTS3DS_A)

$(LIBTS3DS_A):
@test -f "$(LIBTS3DS_DIR)/Makefile.3ds" || { \
echo "error: libts3ds submodule is missing" >&2; \
echo "run: git submodule update --init --recursive" >&2; \
exit 1; \
}
@$(MAKE) --no-print-directory -C $(LIBTS3DS_DIR) -f Makefile.3ds all

%.bin.o %_bin.h : %.bin
@echo $(notdir $<)
@$(bin2o)
Expand Down
87 changes: 84 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
up questions, **B** to clear and start a new conversation.
- **RSA-4096 public-key auth** — libssh2 + mbedTLS, private key read
from the SD card.
- **Native Tailscale transport** — optionally join the 3DS to a tailnet and
carry SSH over direct UDP, Tailscale Peer Relay, or DERP without running Go
or `tailscaled`.
- **Full physical-key mapping** — D-pad arrow keys, hold-style modifiers
(L = Shift, Y = Ctrl, X = Alt), Circle Pad scrollback / mouse-wheel.
- **Anthropic-red crab mascot** — scampers along the bottom row, dodges
Expand Down Expand Up @@ -183,6 +186,15 @@ port = 22
user = ubuntu
key_path = sdmc:/3ds/3dssh/id_rsa
passphrase =

# Optional: unlock the current SSH user's macOS login keychain
macos_keychain_password =

# Optional: connect SSH through the tailnet
tailscale_auth_key =
tailscale_hostname = dssh-3ds
tailscale_state = sdmc:/3ds/3dssh/tailscale.state
tailscale_control_url = https://controlplane.tailscale.com
```

| Field | Meaning |
Expand All @@ -192,13 +204,70 @@ passphrase =
| `user` | SSH login user |
| `key_path` | Private key path; `sdmc:/...` is the 3DS standard SD prefix |
| `passphrase` | Optional key passphrase; leave empty (typing one on the soft keyboard is awkward) |
| `macos_keychain_password` | Optional macOS login password; setting it enables automatic unlock for the current SSH user |
| `tailscale_auth_key` | Auth key used for registration; setting it enables Tailscale automatically |
| `tailscale_hostname` | Device name shown in the tailnet |
| `tailscale_state` | Persistent machine/WireGuard/DISCO identity file |
| `tailscale_control_url` | Coordination server; defaults to Tailscale SaaS |

When `macos_keychain_password` is set, DSSH waits for the current interactive PTY
shell to finish initialization, then runs
`/usr/bin/security unlock-keychain "$HOME/Library/Keychains/login.keychain-db"`.
It waits until macOS prints `password to unlock` before sending the password
through the PTY—not `-p`, the remote process arguments, or shell history. This
matches macOS Keychain's interactive-session requirement and works with fish,
zsh, and bash. DSSH then clears its in-memory copy. If unlock fails, the normal
SSH shell remains available and a warning is shown. A later `claude` launch can
then read the credential already stored in that login keychain.

Successful unlock diagnostics are hidden and the bootstrap output is cleared
before fish redraws its prompt. On failure, DSSH keeps one concise warning with
the unlock and verification status codes. The password itself is never shown.

Quote a password containing `#` or leading/trailing spaces:

```ini
macos_keychain_password = "my # password"
```

> ⚠️ This option stores your **macOS login password in plaintext on the
> removable SD card**. That is more sensitive than a dedicated SSH key. Use a
> dedicated macOS account where practical, keep the SD card physically secure,
> and leave this field empty unless you accept this risk.

### Native Tailscale transport

Tailscale support is provided by
[`cadl/libts3ds`](https://github.com/cadl/libts3ds), an experimental native C
client for Nintendo 3DS derived from MicroLink. It is not the official Go
`tailscale/libtailscale` library and does not run `tailscaled`. DSSH pins the
audited source as the `libts3ds` git submodule at the
[`libts3ds-v0.1.0`](https://github.com/cadl/libts3ds/releases/tag/libts3ds-v0.1.0)
release. The top-level `make` builds the static library automatically.

When enabled, SSH uses the private libts3ds/lwIP TCP stack and can select
direct UDP, Tailscale Peer Relay, or DERP. The default build uses automatic
path selection. Diagnostic builds can fix the data path at compile time with
`DSSH_TAILSCALE_PATH=direct`, `peer-relay`, or `derp`.

Tailscale also starts automatically without an auth key when the state file
exists. `tailscale_hostname` defaults to `dssh-3ds`, and `tailscale_state`
defaults to `sdmc:/3ds/3dssh/tailscale.state`. The auth key may remain in the
configuration after registration, although removing it reduces exposure if the
SD card is lost.

The state file contains private machine, WireGuard, and DISCO identity keys.
Treat both it and the auth key as secrets. This is an unofficial community
integration and is not affiliated with or endorsed by Tailscale Inc.


Final SD layout:

```
sdmc:/3ds/3dssh/
├── config.ini
└── id_rsa
├── id_rsa
└── tailscale.state # created after Tailscale registration
```

---
Expand Down Expand Up @@ -606,10 +675,13 @@ wget https://apt.devkitpro.org/install-devkitpro-pacman
bash install-devkitpro-pacman
sudo dkp-pacman -S 3ds-dev 3ds-mbedtls 3ds-libpng 3ds-zlib

# 2. Clone + cd
git clone https://github.com/Fishason/DSSH.git
# 2. Clone recursively so libts3ds and its private lwIP are checked out
git clone --recurse-submodules https://github.com/Fishason/DSSH.git
cd DSSH

# Existing non-recursive clone only:
git submodule update --init --recursive

# 3. Cross-compile libssh2 (one-time, drops into $DEVKITPRO/portlibs/3ds/lib/)
bash build-libssh2.sh

Expand All @@ -634,6 +706,11 @@ bash tools/install_cia_tools.sh # installs bannertool + makerom into ~/bin
make cia # → DSSH.cia
```

The build command itself is unchanged: `make` first builds the pinned
`libts3ds/build/3ds/libts3ds.a`, then links DSSH. If the submodule is missing,
the Makefile prints the initialization command instead of failing later with a
missing directory or header.

### Test the IME engine on the host (no 3DS needed)

```bash
Expand All @@ -655,6 +732,7 @@ DSSH/
├── app.rsf # makerom CIA spec
├── Makefile # Top-level build (make / make cia / make test-ime)
├── build-libssh2.sh # libssh2 + mbedTLS ARM cross-compile
├── libts3ds/ # Pinned native Tailscale client submodule
├── source/
│ ├── main.c # Main loop, SSH receive, UTF-8 reassembly
│ ├── ssh_client.{c,h} # libssh2 wrapper
Expand Down Expand Up @@ -726,6 +804,9 @@ the full progression.
and box-drawing pixel font.
- **[libssh2](https://www.libssh2.org/)** + **[mbedTLS](https://www.trustedfirmware.org/projects/mbed-tls/)** —
SSH / TLS protocol stack.
- **[cadl/libts3ds](https://github.com/cadl/libts3ds)** — native Nintendo 3DS
Tailscale-compatible transport, derived from
**[CamM2325/microlink](https://github.com/CamM2325/microlink)**.
- **[devkitPro](https://devkitpro.org/) libctru / citro2d / citro3d** —
3DS user-mode runtime and rendering.
- **[carstene1ns/3ds-bannertool](https://github.com/carstene1ns/3ds-bannertool)**
Expand Down
79 changes: 76 additions & 3 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
SSH 会话完全不受打扰。modal 里按 **A** 关窗保留 history(可追问),
按 **B** 关窗清空 history(新对话)。
- **RSA-4096 公钥认证**:libssh2 + mbedTLS,私钥放 SD 卡读
- **原生 Tailscale 传输**:可选地让 3DS 加入 tailnet,通过直连 UDP、
Tailscale Peer Relay 或 DERP 承载 SSH,不运行 Go 或 `tailscaled`
- **物理键全映射**:D-pad 方向键、修饰键 hold-style(L=Shift / Y=Ctrl /
X=Alt)、Circle Pad scrollback / mouse-wheel
- **Anthropic 红螃蟹吉祥物**:底行左右奔跑,点击会躲开 🦀
Expand Down Expand Up @@ -166,6 +168,15 @@ port = 22
user = ubuntu
key_path = sdmc:/3ds/3dssh/id_rsa
passphrase =

# 可选:自动解锁当前 SSH 用户的 macOS 登录 keychain
macos_keychain_password =

# 可选:通过 tailnet 连接 SSH
tailscale_auth_key =
tailscale_hostname = dssh-3ds
tailscale_state = sdmc:/3ds/3dssh/tailscale.state
tailscale_control_url = https://controlplane.tailscale.com
```

| 字段 | 说明 |
Expand All @@ -175,13 +186,64 @@ passphrase =
| `user` | 服务器登录用户名 |
| `key_path` | 私钥路径,`sdmc:/...` 是 3DS 标准 SD 路径前缀 |
| `passphrase` | 私钥口令;建议留空(SD 卡上输 passphrase 体验差) |
| `macos_keychain_password` | 可选的 macOS 登录密码;填写后为当前 SSH 用户启用自动解锁 |
| `tailscale_auth_key` | 注册使用的 auth key;填写后自动开启 Tailscale |
| `tailscale_hostname` | tailnet 中显示的 3DS 设备名 |
| `tailscale_state` | 持久化机器、WireGuard 与 DISCO 身份的文件 |
| `tailscale_control_url` | 控制服务器,默认使用 Tailscale SaaS |

填写 `macos_keychain_password` 后,DSSH 会等待当前交互式 PTY shell 初始化完成,
然后执行
`/usr/bin/security unlock-keychain "$HOME/Library/Keychains/login.keychain-db"`。
只有检测到 macOS 输出 `password to unlock` 后才会通过 PTY 发送密码;不使用
`-p`,不会进入远端进程参数或 shell history。这满足 macOS Keychain 对交互式
会话的要求,并兼容 fish、zsh 和 bash。之后 DSSH 会清除内存中的密码副本。
即使解锁失败,普通 SSH shell 仍可正常使用,并会显示一条警告。之后再启动
`claude`,它就可以读取登录 keychain 中已有的 credential。

解锁成功时不会再显示诊断日志,DSSH 会清理 bootstrap 输出后再由 fish
重绘提示符。失败时只保留一条包含 unlock/verify 状态码的简短警告;密码
本身永远不会显示。

如果密码包含 `#` 或首尾空格,请加引号:

```ini
macos_keychain_password = "my # password"
```

> ⚠️ 此功能会把你的 **macOS 登录密码以明文保存在可移除的 SD 卡上**,其
> 敏感程度高于一把专用 SSH 密钥。建议尽量使用专门的 macOS 账户,妥善保管
> SD 卡;只有接受这一风险时才填写该字段。

### 原生 Tailscale 传输

Tailscale 支持由 [`cadl/libts3ds`](https://github.com/cadl/libts3ds)
提供。它是从 MicroLink 派生、面向 Nintendo 3DS 的实验性原生 C 客户端,
不是官方 Go `tailscale/libtailscale`,也不会运行 `tailscaled`。DSSH 通过
`libts3ds` git submodule 固定到经过真机验证的
[`libts3ds-v0.1.0`](https://github.com/cadl/libts3ds/releases/tag/libts3ds-v0.1.0)
源码;顶层 `make` 会自动先构建静态库。

开启后,SSH 使用 libts3ds 内部的私有 lwIP TCP 栈,可走直连 UDP、
Tailscale Peer Relay 或 DERP。默认构建自动选路;诊断构建可在编译时通过
`DSSH_TAILSCALE_PATH=direct`、`peer-relay` 或 `derp` 固定数据路径。

没有 auth key 时,只要 state 文件存在,Tailscale 也会自动启动。
`tailscale_hostname` 默认是 `dssh-3ds`,`tailscale_state` 默认是
`sdmc:/3ds/3dssh/tailscale.state`。注册成功后 auth key 可以继续保留在配置中;
如果担心 SD 卡遗失导致 key 暴露,也可以手动删除。

state 文件包含机器、WireGuard 和 DISCO 私钥身份,必须和 auth key 一样按
敏感信息保护。本集成为非官方社区项目,与 Tailscale Inc. 无隶属或背书关系。


最终 SD 卡布局:

```
sdmc:/3ds/3dssh/
├── config.ini
└── id_rsa
├── id_rsa
└── tailscale.state # Tailscale 注册后生成
```

---
Expand Down Expand Up @@ -552,10 +614,13 @@ wget https://apt.devkitpro.org/install-devkitpro-pacman
bash install-devkitpro-pacman
sudo dkp-pacman -S 3ds-dev 3ds-mbedtls 3ds-libpng 3ds-zlib

# 2. clone + 进项目
git clone https://github.com/Fishason/DSSH.git
# 2. 递归 clone,同时拉取 libts3ds 及其私有 lwIP
git clone --recurse-submodules https://github.com/Fishason/DSSH.git
cd DSSH

# 仅已有的非递归 clone 需要执行:
git submodule update --init --recursive

# 3. 交叉编译 libssh2(一次性,输出到 $DEVKITPRO/portlibs/3ds/lib/)
bash build-libssh2.sh

Expand All @@ -580,6 +645,10 @@ bash tools/install_cia_tools.sh # 装 bannertool + makerom 到 ~/bin
make cia # 输出 DSSH.cia
```

构建命令本身没有变化:`make` 会先生成固定版本的
`libts3ds/build/3ds/libts3ds.a`,再链接 DSSH。若 submodule 未初始化,
Makefile 会直接提示正确命令,而不是稍后报目录或头文件缺失。

### 测试 IME 引擎(host 端,不需 3DS)

```bash
Expand All @@ -600,6 +669,7 @@ DSSH/
├── app.rsf # makerom CIA spec
├── Makefile # 主构建(make / make cia / make test-ime)
├── build-libssh2.sh # libssh2 + mbedTLS ARM 交叉编译
├── libts3ds/ # 固定版本的原生 Tailscale 客户端 submodule
├── source/
│ ├── main.c # 主循环、SSH receive、UTF-8 边界
│ ├── ssh_client.{c,h} # libssh2 封装
Expand Down Expand Up @@ -669,6 +739,9 @@ SSH server (somewhere on the internet)
box-drawing 像素字体
- **[libssh2](https://www.libssh2.org/)** + **[mbedTLS](https://www.trustedfirmware.org/projects/mbed-tls/)** —
SSH/TLS 协议栈
- **[cadl/libts3ds](https://github.com/cadl/libts3ds)** — Nintendo 3DS 原生
Tailscale 兼容传输,派生自
**[CamM2325/microlink](https://github.com/CamM2325/microlink)**
- **[devkitPro](https://devkitpro.org/) libctru / citro2d / citro3d** —
3DS 用户态运行时 + 渲染
- **[carstene1ns/3ds-bannertool](https://github.com/carstene1ns/3ds-bannertool)**
Expand Down
1 change: 1 addition & 0 deletions libts3ds
Submodule libts3ds added at dd700e
Loading