Files
mc_p2p_mod/README.md
EJClaw bacc2e91d3
Some checks failed
build / build (push) Has been cancelled
feat: 버전 독립 P2P 터널 모드 초기 구현
jar 하나로 모든 마인크래프트 버전을 지원해야 한다는 요구를 만족시키기 위해
마인크래프트 클래스를 전혀 참조하지 않는 구조로 설계했다. 26.1부터 게임이
비난독화되면서 Yarn/intermediary 가 폐지돼, 게임 클래스를 참조하는 jar 는
1.21.x 와 26.x 경계를 넘을 수 없다 — 버전별 빌드를 피할 유일한 길이다.

게임에서 필요한 두 가지를 자바 API 대신 네트워크 프로토콜로 얻는다.
호스트의 통합 서버 포트는 바닐라 "LAN 에 공개" 가 쏘는 멀티캐스트 비콘에서
읽고, 참가자에게는 같은 비콘을 우리가 쏘아 LAN 목록에 방을 띄운다. 덕분에
믹스인 0개, 버전 분기 0개, Loom·매핑 없는 평범한 자바 빌드가 됐다.

전송은 순수 자바 QUIC(Kwik)을 골랐다. 네이티브 WebRTC 대비 번들이 30MB →
0.71MB 로 줄고 아키텍처 제약이 사라진다. NAT 우회는 후보를 병렬로 시도하되
직결에 600ms 유예를 줘서, 항상 먼저 성공하는 중계가 직결을 밀어내지 않게 했다.
중계는 UDP 데이터그램을 그대로 넘기므로 직결과 터널 코드가 동일하고 QUIC
TLS 1.3 이 종단간이라 중계 서버는 트래픽을 읽을 수 없다.

초대코드는 120비트 난수 자체이고 서버에 등록되는 방 ID 는 그 해시에서
파생된다. 시그널링 서버 운영자도 방에 참가하거나 호스트를 사칭할 수 없다.

검증: 57개 테스트 전부 통과. 직결·중계 양쪽에서 터널이 실제 소켓으로 왕복하고,
512KB 전송, 초대코드 불일치 거절, 지문 불일치 차단, 실제 멀티캐스트 비콘
송수신, 시그널링 프로토콜, 웹 콘솔 HTTP 를 포함한다. 마인크래프트 클래스
참조 0개는 컴파일된 클래스의 상수 풀을 뒤지는 테스트로 강제한다.

실제 게임 클라이언트 접속과 실 NAT 홀펀칭 성공률은 아직 미검증이며
docs/DESIGN.md 에 남은 과제로 정리했다.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-28 11:29:25 +09:00

180 lines
7.1 KiB
Markdown

# mc_p2p_mod
초대코드 하나로 싱글플레이 월드를 친구와 함께 플레이하는 마인크래프트 P2P 모드.
**jar 파일 하나가 모든 마인크래프트 버전에서 동작한다.** 1.21 용, 1.21.8 용, 26.3 용을 따로
내려받지 않는다. 버전별 빌드도, 매핑도, 믹스인도 없다. 어떻게 가능한지는
[docs/DESIGN.md](docs/DESIGN.md) 에 근거와 함께 적어 두었다.
| | |
|---|---|
| 전송 | QUIC (TLS 1.3 종단간 암호화) |
| 네이티브 라이브러리 | 0개 |
| jar 크기 | 약 0.71 MB |
| 지원 버전 | Fabric 이 도는 모든 버전 (JVM 17 이상) |
| 믹스인 | 없음 |
| 마인크래프트 클래스 참조 | 0개 (테스트로 강제) |
---
## 설치
1. [Fabric Loader](https://fabricmc.net/use/installer/) 0.14 이상을 설치한다.
2. `mc_p2p_mod-<버전>.jar` 을 `mods` 폴더에 넣는다.
3. 게임을 켠다. 로그에 조작 주소가 찍힌다.
```
[mc_p2p] 준비 완료 — 조작은 http://127.0.0.1:25585 에서 한다
```
Fabric API 는 필요하지 않다. 다른 모드와 충돌할 여지도 사실상 없다 — 이 모드는 게임 코드를
한 줄도 건드리지 않는다.
---
## 처음 한 번: 시그널링 서버 준비
P2P 는 두 사람이 서로의 주소를 알아야 시작된다. 그 주소를 교환해 주는 작은 서버가 필요하다.
남의 서버에 의존하지 않도록, 이 저장소에 서버가 같이 들어 있다.
아무 VPS 한 대에서:
```bash
java -jar mc_p2p_mod-<버전>-rendezvous.jar 25580 40000-40100
```
- `25580` — 시그널링용 TCP 포트
- `40000-40100` — 중계용 UDP 포트 범위. 방화벽에서 이 범위만 열면 된다.
생략하면 임의 포트를 쓰므로 방화벽 설정이 번거로워진다.
메모리는 수십 MB 면 충분하다. 홀펀칭이 성공하는 한 트래픽은 이 서버를 지나지 않는다.
서버를 띄웠으면 게임에서 `http://127.0.0.1:25585` 를 열고 주소를 넣고 저장한다.
예: `p2p.example.com:25580`
---
## 방 만들기 (호스트)
1. 웹 콘솔에서 **호스트 준비** 를 누른다.
2. 게임에서 월드에 들어가 **일시정지 → LAN 에 공개** 를 누른다.
3. 웹 콘솔에 초대코드가 나타난다. 친구에게 준다.
```
K7Q2M9-F4M1TX-9VB0RS-T3WXZ8
```
바닐라의 "LAN 에 공개" 를 그대로 쓴다. 모드가 그 순간을 감지해서 방을 연다. 월드를 닫으면
방도 알아서 정리된다.
## 방 들어가기 (참가자)
1. 웹 콘솔에 초대코드를 붙여넣고 **참가** 를 누른다.
2. 게임의 **멀티플레이** 화면을 연다.
3. LAN 목록에 방이 나타난다. 누르면 접속된다.
주소를 입력하거나 서버를 추가할 필요가 없다. 목록에 직결/중계와 지연 시간이 같이 표시된다.
```
P2P 방 · 직결 23ms
```
---
## 연결 방식
경로를 동시에 시도해서 되는 것을 쓴다.
1. **직결 (host)** — 같은 공유기 안이면 가장 빠르다.
2. **직결 (srflx)** — STUN 으로 공인 주소를 알아내 양쪽이 동시에 패킷을 보내 NAT 에 구멍을
뚫는다. 가정용 공유기 대부분에서 성공한다.
3. **중계 (relay)** — 대칭 NAT·CGNAT 처럼 구멍이 안 뚫리는 환경의 보험. 시그널링 서버가
UDP 데이터그램을 그대로 넘긴다.
중계는 거의 항상 즉시 성공하므로, 아무 장치 없이 "먼저 된 것" 을 쓰면 직결이 가능한데도
중계로 굳어버린다. 그래서 직결에 기본 600ms 의 유예를 준다. 그 안에 홀펀칭이 되면 직결을
쓰고, 안 되면 중계로 넘어간다.
중계를 지나더라도 QUIC 의 TLS 1.3 이 종단간이라 **중계 서버는 게임 트래픽을 읽을 수 없다.**
---
## 설정
`config/mc_p2p.properties`. 웹 콘솔에서 바꾸는 값도 여기 저장된다.
| 키 | 기본값 | 설명 |
|---|---|---|
| `rendezvous` | (비어 있음) | 시그널링 서버 `호스트:포트` |
| `stun` | `stun.l.google.com:19302` | 비우면 같은 LAN 전용이 된다 |
| `webPort` | `25585` | 웹 콘솔 포트. 루프백에만 열린다 |
| `relayOnly` | `false` | 직결을 건너뛰고 중계만 쓴다(디버깅용) |
| `directPreferenceMs` | `600` | 직결에 주는 유예 시간 |
| `pathTimeoutMs` | `8000` | 경로 탐색 포기 시각 |
| `debug` | `false` | 상세 로그 |
---
## 보안
- **방 비밀** — 초대코드는 120비트 난수다. 호스트는 QUIC 스트림 첫머리에서 이 값을 대조한
뒤에야 통합 서버로 연결을 넘긴다. 코드가 없으면 방 ID 를 알아도 들어올 수 없다.
- **시그널링 서버는 초대코드를 모른다** — 서버에 등록되는 방 ID 는 코드의 SHA-256 에서
파생된 값이다. 서버 운영자도 방에 몰래 들어오거나 호스트를 사칭할 수 없다.
- **중간자 차단** — 호스트는 매번 새 인증서를 만들고, 참가자는 시그널링으로 받은 SHA-256
지문과 실제 핸드셰이크의 인증서를 대조한다. 다르면 즉시 끊는다.
- **웹 콘솔은 127.0.0.1 에만 바인딩된다** — 방을 열고 닫을 수 있는 창구이므로 LAN 에도
노출하지 않는다.
- **참가자 리스너도 127.0.0.1 전용이다** — 같은 공유기의 다른 기기가 초대코드 없이
올라타지 못한다.
시그널링 연결 자체는 아직 평문 TCP 다. 중간자가 이 경로를 장악하면 지문을 바꿔칠 수 있다.
TLS 적용은 남은 과제로 [docs/DESIGN.md](docs/DESIGN.md) 에 적어 두었다.
---
## 빌드
```bash
./gradlew build
```
Fabric Loom 을 쓰지 않으므로 마인크래프트를 내려받지 않고 몇 초 안에 끝난다.
산출물은 `build/libs/` 에 두 개가 나온다.
- `mc_p2p_mod-<버전>.jar` — 모드
- `mc_p2p_mod-<버전>-rendezvous.jar` — 시그널링/중계 서버
테스트만 돌리려면 `./gradlew test`.
---
## 검증 상태
솔직하게 적는다.
**기계로 확인된 것** (`./gradlew test`, 57개):
- 터널 전 구간 — 펀칭 프로브, 경로 선택, QUIC 핸드셰이크, 인증서 지문 고정, 방 비밀 대조,
TCP↔QUIC 양방향 운반. 직결과 중계 양쪽 모두 실제 소켓으로 왕복한다.
- 512KB 연속 전송이 손실 없이 도착한다(청크 전송 상황).
- 틀린 초대코드는 호스트가 거절하고 통합 서버에 닿지 않는다.
- 지문이 다르면 참가자가 연결을 끊는다.
- 직결과 중계가 동시에 유효할 때 직결이 이긴다.
- LAN 비콘을 실제 멀티캐스트로 쏘고 받아 포트를 읽는다.
- 시그널링 프로토콜 — 등록, 참가, 방 중복, 호스트 이탈 시 방 정리, 잘못된 입력.
- 웹 콘솔 — 실제 HTTP 요청/응답, 루프백 전용 바인딩.
- **마인크래프트 클래스 참조 0개** — 컴파일된 클래스의 상수 풀을 뒤져서 강제한다.
**아직 확인되지 않은 것:**
- 실제 마인크래프트 클라이언트로 접속해 본 검증. 루프백 테스트는 프로토콜 계층까지만 본다.
- 실제 NAT 를 넘는 홀펀칭 성공률. 테스트는 같은 기기 안이라 NAT 가 없다.
- 1.21.x 와 26.x 양쪽에서의 실제 로드 확인.
---
## 라이선스
MIT. [LICENSE](LICENSE) 참고.