Skip to content
Merged
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
36 changes: 34 additions & 2 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 Down Expand Up @@ -185,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 @@ -201,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
58 changes: 55 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 @@ -186,6 +189,12 @@ 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 @@ -196,6 +205,10 @@ macos_keychain_password =
| `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
Expand All @@ -222,12 +235,39 @@ macos_keychain_password = "my # password"
> 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 @@ -635,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 @@ -663,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 @@ -684,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 @@ -755,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
52 changes: 49 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 @@ -169,6 +171,12 @@ 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 @@ -179,6 +187,10 @@ macos_keychain_password =
| `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 初始化完成,
然后执行
Expand All @@ -203,12 +215,35 @@ macos_keychain_password = "my # password"
> 敏感程度高于一把专用 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 @@ -579,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 @@ -607,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 @@ -627,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 @@ -696,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
10 changes: 10 additions & 0 deletions sd_template/3ds/3dssh/config.ini.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,13 @@ passphrase =
# SECURITY WARNING: this stores your macOS login password in plaintext on the
# removable SD card. Prefer a dedicated macOS account and keep the SD card safe.
macos_keychain_password =

# Optional native Tailscale transport. Setting an auth key enables Tailscale.
# Later launches also start automatically when tailscale.state exists, so the
# key may remain configured but removing it reduces exposure on the SD card.
tailscale_auth_key =

# Optional values below use these defaults when omitted or left empty.
tailscale_hostname = dssh-3ds
tailscale_state = sdmc:/3ds/3dssh/tailscale.state
tailscale_control_url = https://controlplane.tailscale.com
34 changes: 34 additions & 0 deletions source/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ int config_load(ssh_config_t *cfg, const char *path) {
set_str(cfg->key_path, "sdmc:/3ds/3dssh/id_rsa");
cfg->passphrase[0] = 0;
cfg->macos_keychain_password[0] = 0;
cfg->tailscale_auth_key[0] = 0;
set_str(cfg->tailscale_hostname, "dssh-3ds");
set_str(cfg->tailscale_state, "sdmc:/3ds/3dssh/tailscale.state");
set_str(cfg->tailscale_control_url,
"https://controlplane.tailscale.com");

FILE *fp = fopen(path, "r");
if (!fp) return 0;
Expand All @@ -92,7 +97,36 @@ int config_load(ssh_config_t *cfg, const char *path) {
else if (!strcmp(key, "passphrase")) set_str(cfg->passphrase, val);
else if (!strcmp(key, "macos_keychain_password"))
set_str(cfg->macos_keychain_password, val);
else if (!strcmp(key, "tailscale_auth_key"))
set_str(cfg->tailscale_auth_key, val);
else if (!strcmp(key, "tailscale_hostname"))
set_str(cfg->tailscale_hostname, val);
else if (!strcmp(key, "tailscale_state"))
set_str(cfg->tailscale_state, val);
else if (!strcmp(key, "tailscale_control_url"))
set_str(cfg->tailscale_control_url, val);
}
fclose(fp);

/* An explicitly empty optional value has the same meaning as omitting it. */
if (!cfg->tailscale_hostname[0])
set_str(cfg->tailscale_hostname, "dssh-3ds");
if (!cfg->tailscale_state[0])
set_str(cfg->tailscale_state,
"sdmc:/3ds/3dssh/tailscale.state");
if (!cfg->tailscale_control_url[0])
set_str(cfg->tailscale_control_url,
"https://controlplane.tailscale.com");
return 1;
}

int config_tailscale_should_start(const ssh_config_t *cfg) {
if (!cfg) return 0;
if (cfg->tailscale_auth_key[0]) return 1;
if (!cfg->tailscale_state[0]) return 0;

FILE *state = fopen(cfg->tailscale_state, "rb");
if (!state) return 0;
fclose(state);
return 1;
}
Loading