# 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) 참고.