Files
mudfish-decky/docs/superpowers/specs/2026-10-24-mudfish-decky-design.md
T

158 lines
15 KiB
Markdown

# 미꾸라지 Full VPN 데키 플러그인 설계
작성일: 2026-10-24
## 1. 왜 만드는가
사용자의 **지포스나우(GeForce NOW) 계정은 일본 계정**이다. 스팀덱에서 지포스나우를 쓰려면 앱이 켜질 때와 로그인할 때, 게임을 실행할 때 **일본 IP**로 보여야 한다. 게임이 실행된 뒤에는 VPN을 꺼도 스트리밍이 문제없이 이어진다는 것을 사용자가 직접 확인했다.
미꾸라지(Mudfish) VPN은 스팀덱(SteamOS)을 공식 지원한다. 하지만 Konsole에서 띄운 미꾸라지는 데스크탑 모드에서 게임모드로 넘어가면 종료된다. 그래서 게임모드에서 켜고 끄려면 매번 데스크탑 모드를 오가야 한다.
이 플러그인은 **게임모드의 데키 패널에서 미꾸라지 Full VPN을 켜고 끄는 스위치**다.
### 사용 흐름
1. 게임모드에서 `...` 버튼을 누르고 데키 패널의 미꾸라지 플러그인에서 **켜기**를 누른다.
2. 패널에 일본 IP가 표시되는 것을 확인한다.
3. 지포스나우 앱을 실행해 로그인하고 게임을 시작한다.
4. 패널에서 **끄기**를 누른다. 이후 스트리밍은 직접 연결로 이어진다.
## 2. 목표 / 비목표 / 반목표
**목표**
- 게임모드 데키 패널에서 미꾸라지 Full VPN을 켜고 끈다.
- 연결 상태와 **현재 공인 IP·국가**를 보여 준다.
- 일본 노드는 기본이 **자동(그 시점에 핑이 가장 낮은 일본 노드)**이다. 수동 선택은 "고급" 접기 메뉴 안에 둔다.
- 연결된 뒤 지난 시간을 보여 줘서, 켜 둔 채로 잊는 일을 줄인다.
- 미리 데스크탑 모드에서 한 번 로그인해 두면(로그인 기억하기) 게임모드에서는 다시 로그인할 필요가 없다.
**비목표**
- 게임모드에서 아이디와 비밀번호를 입력해 로그인하는 기능. 토큰이 만료되면 안내 문구만 보여 준다.
- 미꾸라지 설치와 업데이트. 사용자가 공식 가이드대로 직접 한다.
- 자동 끄기, 시간 경과 알림 같은 자동화. 사용자가 수동으로 켜고 끄기로 했다.
- 아이템 모드 지원. 일본 이외 국가의 노드 선택.
- 데키 스토어 배포. 사용자 본인의 스팀덱 전용이다.
**반목표** (기술적으로는 돌아가도 실패로 보는 것)
- 사용자가 모르는 사이에 VPN이 켜진 상태로 남아 스트리밍 트래픽이 미꾸라지로 흘러가는 것. 종량제는 1GB당 135원이고 노드 경유 트래픽은 두 번 집계된다. 스트리밍 중에 켜져 있으면 시간당 수천 원이 나간다.
- 플러그인이 자동으로 **연결을 시작**하는 동작은 없어야 한다. 부팅할 때, 플러그인을 다시 불러올 때, 절전에서 깨어날 때 모두 마찬가지다.
- 끄기를 눌렀는데 실제로는 터널이나 경로 설정이 남아 있는 것.
- 사용자의 미꾸라지 계정 설정을 묻지 않고 바꾸는 것. 예외는 Full VPN 노드 번호 하나로, 사용자가 노드를 선택하거나 자동 선택이 될 때만 바꾼다.
## 3. 환경 (2026-10-24 SSH로 확인)
- 스팀덱: SteamOS (holo), 커널 7.2.7, Python 3.14.6. SSH 주소 `deck@192.168.1.50`이고 키 인증이 등록되어 있다.
- 데키로더 **v3.3.0**과 플러그인 13개가 설치되어 있다. 참고할 만한 기존 플러그인으로 `tailscale-control`(VPN 스위치)이 있다.
- 지포스나우: Flatpak `com.nvidia.geforcenow` (user).
- 미꾸라지 **6.6.0**이 `/opt/mudfish/6.6.0/`에 설치되어 있다. `/opt`는 `/home` 쪽 파티션(`/.steamos/offload/opt`)이라 SteamOS 업데이트 후에도 남는다.
- 미꾸라지 계정은 종량제(PPT)이고 `isOkayToUseNodes: true`이다. "로그인 기억하기"로 로그인해 둔 상태다(`autologin on`, `token_v3` 저장).
- 개발 중에는 `deck` 계정에 sudo 비밀번호 없음 설정(`/etc/sudoers.d/zz-dev-nopasswd`)이 켜져 있다. **개발이 끝나면 지워야 한다.** 플러그인 자체는 이 설정에 의존하면 안 된다. 데키 백엔드는 root로 실행된다.
## 4. 확인된 미꾸라지 동작 (실측)
### 실행 바이너리
- `/opt/mudfish/<버전>/bin/mudrun-headless -B`: 로컬 웹 UI와 API를 `127.0.0.1:8282`에서 연다. `-B`는 브라우저를 띄우지 않는 옵션이다. 8282가 사용 중이면 임의의 포트로 열리고, 실제 포트는 `/opt/mudfish/<버전>/var/.ipport`에 기록된다.
- root가 필요하다. 라우팅 테이블을 바꾸고 `tap0` 인터페이스를 만들기 때문이다.
- Konsole이나 SSH 세션에서 띄우면 세션이 끝날 때 같이 죽는다. `sudo systemd-run --unit=<이름> --collect <바이너리> -B`로 띄우면 세션과 상관없이 유지되는 것을 확인했다.
- 설정과 상태 파일은 `/opt/mudfish/<버전>/var/.conf`(텍스트), `.conf.db`, `mudrun.pid` 등이다.
### 로컬 API (`http://127.0.0.1:8282`)
| 요청 | 용도 | 응답/비고 |
|---|---|---|
| `GET /do/mudrun/status` | 전체 상태 | JSON. `isSignedIn`, `autologin`, `username`, `token_v3`, `jwt`, `api_server`(`api.mudfish.net`), `item_conf_mode`, `version`, `core_running`, `core_ready`, `client_ready`, `fullvpn_enabled`, `mudfish_fullvpn_sid` |
| `POST /do/auth` (JSON) | 로컬 데몬에 로그인 결과 전달 | `{username, password, remember_me, token_v3, jwt, expert_mode, lang, alias_enable}` → `{"status":200}` |
| `POST /do/mudfish/start` | 코어(터널) 시작 | `{"status":200}`. 이미 실행 중이면 201 |
| `POST /do/mudfish/stop` | 코어 종료 | `{"status":200}` |
| `POST /do/fullvpn/refresh` (form) | 실행 중에 Full VPN 노드 반영 | `sid`, `exceptions` |
| `GET /do/rtt/staticnodes` | 노드별 핑 측정값 | `{"rtts":[{sid, rttavg, success, failure, ...}]}` |
### 자동 로그인 (데몬을 다시 띄우면 `isSignedIn: false`가 됨)
미꾸라지 웹 UI(`signin.html`)가 하는 절차를 그대로 따르며, 명령어로 성공을 확인했다.
1. `GET /do/mudrun/status`로 `username`, `token_v3`, `item_conf_mode`, `version`, `api_server`를 읽는다.
2. `POST https://api.mudfish.net/signin_ng_v3`를 **multipart form**으로 보낸다. 필드는 `autologin=ON`, `username`, `password=<token_v3>`, `item_conf_mode`, `version`이다. 응답은 `{status, token_v3, jwt, expert_mode, lang, alias_enable}`이다. `status`가 401이면 캡차가 필요하다는 뜻이다(웹 UI 기준).
3. `POST /do/auth`에 JSON을 보낸다. 필드는 `username`, `password=<저장된 token_v3>`, `remember_me=true`, `token_v3=<응답값>`, `jwt=<응답값>`, `expert_mode`, `lang`, `alias_enable`이다.
4. 이후 `status.isSignedIn == true`가 된다.
### Full VPN 노드 설정 (계정에 저장되는 값)
- 읽기: `POST https://api.mudfish.net/graphql`, 헤더 `Authorization: <jwt>`
- `{ user { dataPlan isOkayToUseNodes progConf { mudfishFullvpnSid mudfishFullvpnVid mudfishFullvpnExceptions ... } staticNodes { sid location } } }`
- 웹 UI(`prog.html`의 `prog_graphql`)는 progConf의 다음 필드를 모두 읽는다: `mudfishCreditSavingMode`, `mudfishFullvpnSid`, `mudfishFullvpnVid`, `mudfishFullvpnXp`, `mudfishFullvpnExceptions`, `mudfishMtu`, `mudfishPreferWfp`, `mudfishProtomethod`, `mudfishReBrokerMaxconns`, `mudflowCompatv1`, `mudflowRttmethod`.
- 쓰기: `POST https://api.mudfish.net/prog`에 **progConf 객체 전체**(위 필드)를 JSON으로 보내고, 헤더는 `Authorization: <jwt>`이다. 응답은 `{status, msg, need_restart}`이다. 웹 UI는 이어서 로컬 기록용으로 `POST /do/db/prog_conf`에 같은 객체를 보낸다.
- 다른 필드는 읽은 값을 그대로 보내고, `mudfishFullvpnSid`만 바꾼다. `mudfishFullvpnVid`는 0이어야 한다. 웹 UI에는 sid와 vid가 둘 다 0보다 크면 막는 로직이 있다.
- 코어가 실행 중일 때 sid를 바꾸면 웹 UI는 `need_restart`이면 재시작하고, 아니면 `/do/fullvpn/refresh`를 호출한다.
- 현재 계정 값은 `mudfishFullvpnSid: 0`(Full VPN 꺼짐)이다. **sid > 0이면 Full VPN이 켜진 것으로 동작한다**(`status.fullvpn_enabled`). 아직 sid를 넣고 실제로 연결해 보지는 않았다. 아래 "검증이 필요한 부분"을 참고한다.
### 일본 노드
- `staticNodes` 228개 중 `location`이 `JP Asia (...)`로 시작하는 노드가 135개다. 예: `JP Asia (Tokyo - Linode3 IDC)`(sid 9), `JP Asia (Osaka - Vultr 1)`. 지역은 도쿄, 오사카, 홋카이도다.
- 핑은 `GET /do/rtt/staticnodes`의 `rttavg`(ms)를 sid로 맞춰 쓴다. 데몬이 켜져 있으면 미꾸라지가 주기적으로 측정한다. 측정 실패 노드는 `failure > 0`이거나 목록에 없다.
### 연결/해제 실측 (아이템 모드로 시험)
- start 후 약 6초 만에 `core_running → core_ready → client_ready`가 모두 true가 됐다. `tap0`가 생기고 아이템 IP 대역이 `tap0`로 잡혔다.
- stop 후 몇 초 안에 코어 프로세스가 종료되고 `tap0`도 사라졌다. 다만 **노드 IP로 가는 /32 경로 349개가 기존 게이트웨이(`via 192.168.1.1 dev wlan0`)로 남았다.** 같은 네트워크에서는 무해하지만, 다른 와이파이로 옮기면 그 노드들로 가는 경로가 틀어질 수 있다.
- 리눅스판에는 Windows의 프로세스 단위 캡처(WFP)가 없다. 아이템 모드에서는 지포스나우 로그인과 API 호스트(`login.nvidia.com`, `play/pcs/uas/als/mes.geforcenow.com`)가 미꾸라지를 거치지 않는다. 그래서 **Full VPN이 필요하다**(이 문서의 전제).
### 지포스나우 쪽 관찰
- 스팀덱 지포스나우 로그(2026-07-27)에 지역이 `KR GFN 1`(`prod.gkr.geforcenow.nvidiagrid.net`)로 잡혀 있었다. 앱이 켜질 때 IP로 지역을 정하므로, **앱을 켜기 전에 VPN이 연결되어 있어야 한다.** 그래서 패널에 "연결됨 + 일본 IP"를 분명하게 보여 주는 것이 중요하다.
## 5. 정해진 설계 (사용자 결정)
- **Full VPN 방식**만 지원한다.
- 켜기와 끄기는 **사용자가 수동**으로 한다. 자동 끄기나 알림은 넣지 않는다.
- 로그인은 데스크탑 모드에서 미리 한다(완료). 게임모드 로그인은 범위 밖이다.
- **끄기를 눌러도 계정의 Full VPN 노드 설정(sid)은 지우지 않는다.**
- 노드 선택은 **기본이 "자동"**이고, 켤 때마다 그 시점에 핑이 가장 낮은 일본 노드를 고른다. **수동 선택은 "고급" 접기 메뉴 안에 숨긴다.**
- 개발 중에는 Claude가 SSH로 스팀덱에 직접 배포하고 테스트한다.
## 6. 패널 구성
위에서 아래 순서다.
1. **상태 줄**: `꺼짐` / `연결 중…` / `연결됨` / `오류`.
- 연결됨이면 현재 공인 IP와 국가(예: `🇯🇵 JP · 1.2.3.4`)와 **연결 후 지난 시간**(예: `12분째`)을 보여 준다.
- 국가가 JP가 아니면 경고색으로 표시한다.
2. **켜기/끄기 토글**.
3. **노드 줄**: `자동 · Tokyo - Linode3 IDC (38ms)`처럼 실제로 쓰는(또는 쓸) 노드를 보여 준다.
4. **고급 (접힘)**: 펼치면 "자동" 항목과 일본 노드 목록이 나온다. 목록은 핑 오름차순이고 측정 실패 노드는 맨 아래에 둔다. 하나를 고르면 그 노드로 고정되고, "자동"을 고르면 자동으로 돌아간다. 이 선택은 플러그인 설정에 저장한다.
5. **오류/안내 문구**(필요할 때만):
- 로그인 토큰 만료: "데스크탑 모드에서 미꾸라지(127.0.0.1:8282)에 '로그인 기억하기'로 다시 로그인하세요".
- 미꾸라지가 설치되지 않음: 설치 가이드 안내.
- 연결 실패: 원인 한 줄.
## 7. 동작 규칙
- **데몬 상주**: 플러그인이 로드되면(부팅 시 포함) 미꾸라지 데몬이 떠 있지 않을 때만 transient systemd unit으로 띄운다. 이 상태는 **코어 미실행 대기 상태**라 핑 측정만 한다. **코어는 절대 자동으로 시작하지 않는다.**
- **켜기**:
1. 데몬을 확인한다.
2. 필요하면 자동 로그인한다.
3. 노드를 정한다. 자동이면 핑 최저 일본 노드, 수동이면 저장된 sid다.
4. 계정 sid가 다르면 `/prog`로 갱신한다.
5. `/do/mudfish/start`를 호출한다.
6. `client_ready`가 될 때까지 기다린다.
7. 공인 IP와 국가를 확인한다.
- **연결 중 노드 변경**(고급 메뉴): sid를 갱신한 뒤 `/do/fullvpn/refresh`를 호출하거나, `need_restart`이면 stop 후 start한다.
- **끄기**: `/do/mudfish/stop`을 호출하고, 코어 종료와 `tap0` 소멸을 확인한다. 미꾸라지가 남긴 노드 /32 경로를 정리한다. 데몬은 계속 떠 있다.
- **상태 판단의 기준은 미꾸라지 실제 상태**(`/do/mudrun/status`와 `tap0` 존재)다. 플러그인 내부 플래그에 의존하지 않는다. 패널을 열 때마다 새로 조회하고, 열려 있는 동안 주기적으로 갱신한다.
- 미꾸라지 경로는 `/opt/mudfish/*/bin/mudrun-headless` 중 **가장 높은 버전**을 쓴다.
- 미꾸라지 실행 파일은 root로 실행된다. 플러그인은 경로를 고정된 위치에서만 찾고, 사용자 입력을 명령어에 넣지 않는다.
## 8. 구현 기본값 (빌더가 바꿔도 되는 것)
- 데키 플러그인 표준 구성(공식 decky-plugin-template 계열)을 쓴다. 프론트엔드는 React/TypeScript(`@decky/ui`, `@decky/api`), 백엔드는 `main.py`(Python, `plugin.json`의 `flags: ["root"]`)다.
- HTTP 호출은 백엔드에서 Python 표준 라이브러리로 한다. 스팀덱에 별도 패키지를 설치하지 않는다.
- 공인 IP와 국가 확인 서비스는 빌더가 정한다. 예: `https://ipinfo.io/json`, `https://myip.mudfish.net`.
- systemd unit 이름, 폴링 주기, 타임아웃, 남은 경로 정리 방법(어떤 경로를 미꾸라지가 추가한 것으로 볼지)은 빌더가 정한다.
- 빌드는 Windows PC에서 하고, SSH로 `~/homebrew/plugins/<플러그인>/`에 배포한 뒤 `plugin_loader`를 재시작한다. 구체적인 도구는 빌더가 정한다.
## 9. 검증이 필요한 부분 (구현 초기에 실측)
1. `/prog`로 `mudfishFullvpnSid`를 바꾼 뒤 start했을 때 **실제로 모든 트래픽이 tap0로 가고 공인 IP가 일본으로 바뀌는지**. 기본 경로가 어떻게 바뀌는지, DNS는 어떻게 되는지도 확인한다.
2. 연결 중 sid를 바꿀 때 `need_restart` 값과 `/do/fullvpn/refresh`의 실제 효과.
3. Full VPN을 끈 뒤 기본 경로와 DNS가 원래대로 돌아오는지, 남는 것이 무엇인지.
4. 절전 후 깨어났을 때 데몬과 터널 상태.
5. 데스크탑 모드와 게임모드를 오갈 때 systemd로 띄운 데몬이 유지되는지.
6. 스팀덱 기본 Tailscale(`tailscale0`)과 Full VPN이 충돌하지 않는지.
## 10. 끝나면 할 일
- 스팀덱의 `/etc/sudoers.d/zz-dev-nopasswd`를 지운다(사용자가 직접 실행: `sudo rm /etc/sudoers.d/zz-dev-nopasswd`).
- 개발 중에 띄운 `mudfish-dev` transient unit을 정리한다.