
피어 투 피어 암호화 메신저 (Rust 기반, Noise IK 프로토콜 사용, UDP NAT 홀 펀칭)
UDP를 통한 종단간 암호화된 P2P 채팅. 계정 없음, 메시지를 중계/저장하는 중앙 서버 없음, 중개자 없음. 두 피어, 직접 연결, Noise 프로토콜 암호화만 있을 뿐입니다.
https://github.com/user-attachments/assets/939e96d3-45e3-4484-9a27-28c3a0457b05
두 사람이 각자 punchline connect <peer>를 실행합니다. Punchline이 NAT를 뚫고, 암호화된 핸드셰이크를 수행하며, 몇 밀리초 만에 개인 채팅으로 연결해 줍니다. 포함된 STUN 및 시그널 서버가 검색을 처리한 후 물러납니다.

cargo build --release
서버 시작 (두 피어 모두 접근 가능한 머신에서) 또는 64.225.107.28에 호스팅된 제 공용 서버를 사용하세요 (STUN: 포트 3478, 시그널: 포트 8743):
punchline-stund # STUN 서버 - 피어에게 공인 IP를 알려줌
punchline-signald # 시그널 서버 - 대화하려는 피어를 매칭
각 피어의 머신에서:
# 신원 생성 (X25519 키쌍)
punchline keygen
# 공개 키를 피어와 공유
punchline pubkey
# 상대방의 키 저장
punchline peers add alice a1b2c3d4...64_hex_chars
# 연결 (두 피어 모두 실행, 서로를 대상으로)
punchline connect alice --stun <server>:3478 --signal <server>:8743
TUI가 라이브 연결 진행 뷰와 함께 시작됩니다:
STUN 발견 - punchline-stund를 통해 외부 주소 확인
시그널 서버 - punchline-signald에 연결
피어 대기 - 시그널 서버가 두 피어를 매칭
홀 펀치 - 직접 UDP 경로 설정
Noise 핸드셰이크 - 암호화된 키 교환
완료되면 채팅에 입장합니다. 입력하고 Enter를 누르세요. Esc를 누르면 종료됩니다.
전체 시스템은 이 저장소에 포함된 세 개의 바이너리로 구성됩니다:
| 바이너리 | 역할 | 사용 시점 |
|---|---|---|
punchline-stund | STUN 서버 (UDP) - 클라이언트의 외부 IP:포트를 응답 |
초기 설정 후에는 STUN 및 시그널 서버에 더 이상 접촉하지 않습니다. 모든 데이터는 직접 P2P로 흐릅니다.
punchline전역 플래그:
| 플래그 | 설명 |
|---|---|
-v | 로그 상세도 증가 (-v = 디버그, -vv = 추적). |
-q, --quiet | 모든 로그 출력 억제. |
punchline-stundpunchline-signald매번 --stun과 --signal을 전달하는 대신 ~/.config/punchline/config.toml을 생성하세요:
stun_server = "203.0.113.10:3478"
signal_server = "203.0.113.10:8743"
punchline peers # 모두 나열
punchline peers add alice a1b2c3d4... # 추가
punchline peers remove alice # 제거
별칭은 ~/.punchline/known_peers.toml에 저장됩니다. 원시 64자리 16진수 키로 직접 연결할 수도 있습니다.
punchline status
신원, 설정, 서버 접근 가능성 (실제 STUN 프로브와 TCP 연결 전송), 피어 수를 표시합니다.
두 서버 모두 -v (디버그), -vv (추적), -q (조용함), --address, --port를 지원합니다:
punchline-stund -v --port 3478
punchline-signald -v --port 8743
~/.config/punchline/style.toml을 통해 TUI를 사용자 정의하세요.
비디오에서 사용된 스타일:
[colors]
my_text = "#ebdbb2"
peer_text = "#bdae93"
input_text = "#ebdbb2"
border = "#ebdbb2"
sidebar_key = "#ebdbb2"
sidebar_value = "#bdae93"
[padding]
chat_horizontal = 2
chat_vertical = 1
모든 색상은 16진수 RGB입니다. 파일이 없으면 터미널의 기본 색상이 사용됩니다.
punchline completions bash > ~/.local/share/bash-completion/completions/punchline
punchline completions zsh > ~/.zfunc/_punchline
punchline completions fish > ~/.config/fish/completions/punchline.fish
전체 프로토콜 이름: Noise_IK_25519_ChaChaPoly_SHA256
| 구성 요소 | 역할 |
|---|---|
| Noise IK | 핸드셰이크 패턴 - 개시자가 응답자의 공개 키를 알고 있음. 2개의 메시지로 완료. |
| X25519 |
IK 패턴은 개시자가 핸드셰이크 시작 전에 응답자의 정적 공개 키를 알고 있음을 의미합니다. 두 피어는 이미 상대방의 키를 가지고 있습니다 (대역 외 또는 피어 레지스트리를 통해 교환됨). 따라서 최초 신뢰(trust-on-first-use)가 필요하지 않습니다.
Punchline은 각 피어의 공개 키의 첫 8바이트를 빅엔디언 u64로 비교하여 결정적으로 개시자를 선택합니다. 값이 작은 피어가 개시자가 됩니다. 양측이 독립적으로 계산합니다.
신원은 ~/.punchline/id_x25519에 저장된 32바이트 X25519 비밀 키이며, Unix 권한은 0600입니다. 공개 키는 로드 시 파생됩니다. 키 생성은 x25519-dalek과 OsRng을 사용합니다.
각 UDP 패킷의 첫 번째 바이트가 유형을 식별합니다:
두 피어가 동시에 동일한 알고리즘을 실행합니다:
PROBE (0x00) 전송.PROBE를 수신하면 ACK (0x01) 전송으로 전환.ACK를 수신하면 마지막 ACK 하나를 보내고 성공 선언.메시지 (0x02)는 Noise 암호화된 UTF-8 페이로드를 운반합니다. Keepalive (0x03)는 암호화된 빈 페이로드로, 10초마다 전송되어 암호 논스 동기화를 유지합니다. 30초 동안 패킷이 없으면 연결이 끊깁니다.
WebSocket을 통한 JSON:
// PairRequest (클라이언트 -> 서버)
{ "external_addr": "203.0.113.5:48291", "public_key": "a1b2...", "target_public_key": "d4e5..." }
// PairResponse (서버 -> 클라이언트)
{ "target_external_addr": "198.51.100.7:51003", "target_public_key": "d4e5..." }
RFC 5389를 따름 (간소화): 바인딩 요청/응답과 XOR-MAPPED-ADDRESS. IPv4 전용.
네 개의 크레이트로 구성된 Cargo 워크스페이스:
crates/
├── proto/ # 공유 라이브러리: 암호화, STUN, 시그널 타입, 전송 트레잇
├── client/ # P2P 클라이언트: CLI, TUI, 연결 로직, 피어 관리
├── signald/ # 시그널 서버: WebSocket 피어 매칭
└── stund/ # STUN 서버: 외부 주소 발견
cargo install punchline # TUI 클라이언트
cargo install punchline-signald # 시그널 서버
cargo install punchline-stund # STUN 서버
전제 조건: Rust 2024 에디션 (rustc 1.85+)
git clone https://github.com/michal-pielka/punchline.git
cd punchline
cargo build --release
바이너리는 target/release/에 배치됩니다:
punchlinepunchline-signaldpunchline-stundcargo test
테스트는 암호화 연산, STUN 인코딩/디코딩, 시그널 프로토콜 직렬화, 설정 파싱, 피어 관리, 스타일 테마, Noise IK 핸드셰이크를 포함합니다.
MIT - LICENSE 참조.
| 설정 중에만 |
punchline-signald | 시그널 서버 (WebSocket) - 피어 매칭 및 주소 교환 | 설정 중에만 |
punchline | 메신저 자체 - CLI, TUI, 암호화, 홀 펀칭 | 항상 |
| 명령어 | 설명 |
|---|
keygen [--force] [-i path] | 새 X25519 신원 키쌍 생성. --force로 확인 없이 덮어쓰기. -i로 출력 경로 지정. |
pubkey [-i path] | 공개 키(64자리 16진수) 출력. -i로 특정 키 파일에서 파생. |
connect <peer> [-i path] [--stun addr] [--signal addr] | 별칭 또는 원시 16진수 키로 피어에 연결. -i로 신원 키 지정. TUI 실행. |
peers | 모든 알려진 피어 나열. |
peers add <name> <key> | 피어의 공개 키를 별명으로 저장. |
peers remove <name> | 별명으로 피어 제거. |
config path | 설정 파일 경로 출력. |
config show | 현재 설정 값 표시. |
status | 신원, 설정, 서버 접근 가능성, 피어 수 표시. |
completions <shell> | 셸 완성 생성 (bash, zsh, fish). |
| 플래그 |
|---|
| 설명 |
|---|
--address <addr> | 바인드 주소 (기본값: 0.0.0.0). |
--port <port> | 바인드 포트 (기본값: 3478). |
-v / -vv | 디버그 / 추적 로깅. |
-q | 조용한 모드. |
| 플래그 |
|---|
| 설명 |
|---|
--address <addr> | 바인드 주소 (기본값: 0.0.0.0). |
--port <port> | 바인드 포트 (기본값: 8743). |
-v / -vv | 디버그 / 추적 로깅. |
-q | 조용한 모드. |
| 타원 곡선 Diffie-Hellman 키 교환 (RFC 7748). 128비트 보안, 상수 시간. |
| ChaCha20-Poly1305 | 메시지 암호화용 AEAD 암호 (RFC 8439). TLS 1.3 및 WireGuard에서도 사용되는 동일 암호. |
| SHA-256 | Noise 내부에서 키 파생 및 핸드셰이크 해싱에 사용. |
| 접두사 | 유형 | 단계 | 설명 |
|---|
0x00 | PROBE | 홀 펀치 | 200ms마다 전송되어 NAT 구멍을 열음 |
0x01 | ACK | 홀 펀치 | PROBE 수신 확인 |
| (없음) | Handshake | 핸드셰이크 | 원시 Noise 암호화 핸드셰이크 페이로드 |
0x02 | Message | 전송 | 암호화된 채팅 메시지 |
0x03 | Keepalive | 전송 | 암호화된 빈 페이로드 (하트비트) |
| 크레이트 | 목적 |
|---|
snow | Noise 프로토콜 프레임워크 (핸드셰이크 + 전송 암호화) |
x25519-dalek | X25519 키 생성 및 파생 |
ratatui | 터미널 UI 프레임워크 |
crossterm | 터미널 이벤트 처리 |
clap | CLI 인자 파싱 + 셸 완성 |
tungstenite | WebSocket 클라이언트/서버 |
tracing | 구조화된 로깅 |