Some checks failed
build / build (push) Has been cancelled
바닐라 "LAN 에 공개" 의 상수 8 을 믹스인 1개로 30 으로 바꾼다. 대상과 메서드를 문자열로만 적고 핸들러를 int -> int 로 만들어 마인크래프트 타입 참조 0 을 유지한다. 8 이 있는 자리 (바닐라 jar 바이트코드 실측, 버전마다 정확히 한 곳): - 1.17 ~ 1.21.8 class_1130.<init> (super 인자 → PlayerList.maxPlayers) - 1.21.9 ~ 1.21.11 class_1132.method_3802 (return 8) - 26.1 ~ 26.3 IntegratedServer.getMaxPlayers (공식 이름) @Pseudo + require=0 이라 자리를 못 찾는 버전은 크래시 없이 바닐라 8 로 돈다. 적용 여부는 웹 콘솔 "최대 인원" 과 로그로 보인다. tools/lan-limit-probe: 바닐라 jar + Fabric Loader 로 모드를 실제 로드해 확인하는 도구. 1.18.2 ~ 26.3 (11개 버전, 로더 0.19.5 / 0.14.25) 전부 30, 믹스인을 뺀 jar 는 8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
233 lines
10 KiB
Markdown
233 lines
10 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 이상) |
|
|
| 최대 인원 | 30명 (확장할 수 없는 버전은 바닐라 8명) |
|
|
| 믹스인 | 1개 — LAN 최대 인원 전용, 안 맞으면 스스로 빠진다 |
|
|
| 마인크래프트 클래스 참조 | 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 는 필요하지 않다. 게임 코드에서 건드리는 곳은 "LAN 에 공개" 의 최대 인원 상수
|
|
하나뿐이다(아래 "최대 인원").
|
|
|
|
---
|
|
|
|
## 처음 한 번: 시그널링 서버 준비
|
|
|
|
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
|
|
```
|
|
|
|
---
|
|
|
|
## 최대 인원
|
|
|
|
바닐라 "LAN 에 공개" 는 8명으로 막혀 있다. 이 모드는 그걸 **30명**으로 올린다. 호스트 쪽
|
|
모드만 일하면 되고 설정할 것은 없다.
|
|
|
|
버전마다 8 이 박힌 자리가 달라서, jar 하나에 알려진 자리를 전부 넣어 두고 실행 중인 버전에
|
|
있는 자리만 바꾼다.
|
|
|
|
| 버전 | 8 이 있는 곳 | 결과 |
|
|
|---|---|---|
|
|
| 1.17 ~ 1.21.8 | `IntegratedPlayerList` 생성자 → `PlayerList.maxPlayers` | 30명 |
|
|
| 1.21.9 ~ 1.21.11 | `IntegratedServer.getMaxPlayers()` | 30명 |
|
|
| 26.1 ~ 26.3 | `IntegratedServer.getMaxPlayers()` (공식 이름) | 30명 |
|
|
| 그 밖의 미래 버전 | 자리가 바뀌었을 수 있음 | 못 찾으면 **8명** 으로 그대로 동작 |
|
|
|
|
자리를 못 찾아도 게임은 크래시하지 않는다. 확장만 빠지고 바닐라 8명으로 돈다. 실제로 적용됐는지는
|
|
웹 콘솔의 **최대 인원** 칸과 로그로 확인한다.
|
|
|
|
```
|
|
[mc_p2p] LAN 최대 인원을 8 → 30 명으로 올렸다
|
|
```
|
|
|
|
게임 로그 맨 앞에 아래 같은 `WARN` 이 한두 줄 찍히는 것은 정상이다. 다른 버전용 대상 이름을
|
|
찾다가 없다고 알리는 것이다(1.21.x 에서는 `IntegratedServer`, 26.x 에서는 `class_1130`/`class_1132`).
|
|
|
|
```
|
|
[main/WARN]: Error loading class: net/minecraft/client/server/IntegratedServer (java.lang.ClassNotFoundException: ...)
|
|
```
|
|
|
|
---
|
|
|
|
## 연결 방식
|
|
|
|
경로를 동시에 시도해서 되는 것을 쓴다.
|
|
|
|
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`, 59개):
|
|
|
|
- 터널 전 구간 — 펀칭 프로브, 경로 선택, QUIC 핸드셰이크, 인증서 지문 고정, 방 비밀 대조,
|
|
TCP↔QUIC 양방향 운반. 직결과 중계 양쪽 모두 실제 소켓으로 왕복한다.
|
|
- 512KB 연속 전송이 손실 없이 도착한다(청크 전송 상황).
|
|
- 틀린 초대코드는 호스트가 거절하고 통합 서버에 닿지 않는다.
|
|
- 지문이 다르면 참가자가 연결을 끊는다.
|
|
- 직결과 중계가 동시에 유효할 때 직결이 이긴다.
|
|
- LAN 비콘을 실제 멀티캐스트로 쏘고 받아 포트를 읽는다.
|
|
- 시그널링 프로토콜 — 등록, 참가, 방 중복, 호스트 이탈 시 방 정리, 잘못된 입력.
|
|
- 웹 콘솔 — 실제 HTTP 요청/응답, 루프백 전용 바인딩.
|
|
- **마인크래프트 클래스 참조 0개** — 컴파일된 클래스의 상수 풀을 뒤져서 강제한다. 믹스인도
|
|
대상을 문자열로만 적고 게임 타입 서술자를 갖지 않는다는 것까지 검사한다.
|
|
|
|
**실제 Fabric 로더 위에서 확인된 것** (`tools/lan-limit-probe`):
|
|
|
|
- 바닐라 클라이언트 jar + Fabric Loader 로 모드를 실제로 로드해 최대 인원 믹스인이 적용되는지
|
|
본다. 1.18.2 · 1.20.1 · 1.21.1 · 1.21.8 · 1.21.9 · 1.21.11 · 26.1 · 26.3 전부 30.
|
|
1.21.9 이상은 `getMaxPlayers()` 를 직접 호출해 30 을 받았고, 1.21.8 이하는 생성된
|
|
`PlayerList` 의 `maxPlayers` 필드가 30 이다. 로더 0.19.5, 그리고 하한인 0.14.25(1.18.2·1.20.1).
|
|
|
|
**아직 확인되지 않은 것:**
|
|
|
|
- 실제 마인크래프트 클라이언트로 접속해 본 검증. 루프백 테스트는 프로토콜 계층까지만 본다.
|
|
- 실제 NAT 를 넘는 홀펀칭 성공률. 테스트는 같은 기기 안이라 NAT 가 없다.
|
|
- 화면을 띄운 실제 게임에서의 전체 흐름. 위 검증은 게임 메인 직전(preLaunch)에서 멈춘다.
|
|
최대 인원도 "판정에 쓰이는 값이 30" 까지 봤고, 실제로 9번째 이후 플레이어를 접속시켜 보지는 않았다.
|
|
|
|
---
|
|
|
|
## 라이선스
|
|
|
|
MIT. [LICENSE](LICENSE) 참고.
|