Files
mc_p2p_mod/README.md
EJClaw 40b8894a98
Some checks failed
build / build (push) Has been cancelled
docs: 참고 대상의 제작자·저장소·서버 주소를 문서에서 제거
코드와 산출물 jar 에는 애초에 외부 서버 주소가 없었으나(rendezvous 기본값이
빈 문자열), 설계 문서에 참고 대상의 제작자명과 저장소 경로가 남아 있었다.
기술적 관찰 사실은 설계 판단의 근거로 필요하므로 유지하되 "기존 구현" 으로
중립화했다.

더불어 이 프로젝트가 남의 인프라에 얹혀 가지 않는다는 점을 README 에 명시했다.
rendezvous 는 빈 값으로 시작해 사용자가 직접 띄운 서버만 쓰며, 유일하게 남는
외부 호출인 STUN 기본값도 제거하는 방법을 함께 적었다.

검증: 작업 트리와 양쪽 산출물 jar 전수 검색 0건, 테스트 57개 전부 통과.

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

192 lines
7.9 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`
### 외부 서버를 쓰지 않는다
이 모드에는 **다른 사람의 시그널링·중계 서버 주소가 기본값으로 들어 있지 않다.** `rendezvous`
는 비어 있는 상태로 시작하고, 사용자가 직접 띄운 서버를 넣어야 비로소 동작한다. 남의 인프라에
얹혀 가는 구성을 아예 만들 수 없게 한 것이다.
남는 외부 호출은 `stun` 기본값 하나뿐이다(`stun.l.google.com:19302`). 공인 주소를 알아내는 데만
쓰이고 게임 트래픽은 지나가지 않는다. 그래도 완전히 없애고 싶으면 두 가지 방법이 있다.
- 같은 공유기 안에서만 쓸 거라면 `stun` 을 빈 값으로 둔다. 외부 호출이 0 이 된다.
- 인터넷을 넘어야 하면 직접 띄운 STUN 서버(coturn 등)를 적는다.
---
## 방 만들기 (호스트)
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) 참고.