
P2P, 종단 간 암호화 채팅. 받은편지함 없음. 복구할 계정 없음. 아무도 듣지 않습니다 — 우리조차도요.
<div align="center">
<a href="https://github.com/Emn4tor/Seal">
<img src="https://assets.kitploit.com/production/public/readmes/49626/0d175b4ba9dd08a1b35a94415f701b49b89aac52b6f5c25731822653ea386e6f.png" width="112" height="112" alt="Seal 로고" />
</a>
<h1>Seal</h1>
<p><em>피어 투 피어, 종단 간 암호화 채팅.<br />받은 편지함 없음. 복구할 계정 없음. 아무도 듣지 않습니다 — 우리조차도.</em></p>
<p><a href="https://github.com/Emn4tor/Seal">github.com/Emn4tor/Seal</a></p>
<p>
<img src="https://img.shields.io/badge/rust-1.97+-c9a15c?style=flat-square&labelColor=0e1116&logo=rust&logoColor=e8e3d8" alt="Rust: 1.97+" />
<img src="https://img.shields.io/badge/tauri-2.11.5-4f8f86?style=flat-square&labelColor=0e1116&logo=tauri&logoColor=e8e3d8" alt="Tauri: 2.11.5" />
<img src="https://img.shields.io/badge/react-19.2-c9a15c?style=flat-square&labelColor=0e1116&logo=react&logoColor=e8e3d8" alt="React: 19.2" />
<img src="https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20Windows-4f8f86?style=flat-square&labelColor=0e1116" alt="Platforms: macOS, Linux, Windows" />
<img src="https://img.shields.io/badge/encryption-Olm%20/%20Megolm-c9a15c?style=flat-square&labelColor=0e1116" alt="Encryption: Olm / Megolm" />
<img src="https://img.shields.io/badge/server%20storage-none-4f8f86?style=flat-square&labelColor=0e1116" alt="Server-side message storage: none" />
</p>
<p>
<a href="https://github.com/Emn4tor/Seal/stargazers"><img src="https://img.shields.io/github/stars/Emn4tor/Seal?style=flat-square&color=c9a15c&labelColor=0e1116" alt="GitHub stars" /></a>
<a href="https://github.com/Emn4tor/Seal/network/members"><img src="https://img.shields.io/github/forks/Emn4tor/Seal?style=flat-square&color=4f8f86&labelColor=0e1116" alt="GitHub forks" /></a>
<a href="https://github.com/Emn4tor/Seal/issues"><img src="https://img.shields.io/github/issues/Emn4tor/Seal?style=flat-square&color=c9a15c&labelColor=0e1116" alt="GitHub issues" /></a>
<a href="https://github.com/Emn4tor/Seal/commits"><img src="https://img.shields.io/github/last-commit/Emn4tor/Seal?style=flat-square&color=4f8f86&labelColor=0e1116" alt="GitHub last commit" /></a>
<a href="https://github.com/Emn4tor/Seal"><img src="https://img.shields.io/github/repo-size/Emn4tor/Seal?style=flat-square&color=c9a15c&labelColor=0e1116" alt="GitHub repo size" /></a>
</p>
<p>
<a href="https://github.com/Emn4tor/Seal/graphs/contributors"><img src="https://img.shields.io/github/contributors/Emn4tor/Seal?style=for-the-badge&color=c9a15c&labelColor=0e1116" alt="Contributors" /></a>
<a href="https://github.com/Emn4tor/Seal/commits"><img src="https://img.shields.io/github/commit-activity/t/Emn4tor/Seal?style=for-the-badge&color=4f8f86&labelColor=0e1116" alt="Total commits" /></a>
</p>
</div>
메시지는 libp2p를 통해 피어 간에 직접 전송되며, 기기를 떠나기 전에 Signal 스타일의 Olm/Megolm 프로토콜([vodozemac](https://github.com/matrix-org/vodozemac)을 통해)로 암호화됩니다. 관여하는 유일한 서버는 피어가 서로의 현재 주소를 찾도록 도와주는 작은 디렉터리입니다. 이 서버는 메시지 내용을 절대 볼 수 없으며, 한 번의 명령으로 제거할 수 있습니다.
실제로 무엇으로부터 보호되는지와 그 방법은 [`docs/THREAT_MODEL.md`](https://github.com/emn4tor/seal/blob/main/docs/THREAT_MODEL.md) 및 [`docs/SECURITY.md`](https://github.com/emn4tor/seal/blob/main/docs/SECURITY.md)를 참조하세요.
## Contents
- [스크린샷](#screenshots)
- [기능](#features)
- [작동 방식](#how-it-works)
- [프로젝트 구조](#project-layout)
- [1. 사전 요구 사항](#1-prerequisites)
- [2. 빌드](#2-building)
- [3. 개발 모드로 실행](#3-running-it-in-dev-mode)
- [4. 테스트](#4-testing)
- [5. 백엔드(디렉터리 서버) 설정](#5-backend-directory-server-setup)
- [6. 앱 사용](#6-using-the-app)
---
## 스크린샷
<div align="center">
**첫 실행** — 이름을 정하세요. 설정할 것은 그것뿐입니다.
<img src="https://assets.kitploit.com/production/public/readmes/49626/c45dd83972e6760574f3a07186a511aa3c9b5d544d22aa7f4dd370d38038eb4d.png" width="820" alt="Seal의 첫 실행 화면: 암호화 모델 설명 옆에 표시 이름 입력 폼과 로그인 시 실행 토글이 있습니다." />
<br /><br />
**대화** — 그룹 레일, 연락처 목록, 종단 간 암호화된 채팅 창.
<img src="https://assets.kitploit.com/production/public/readmes/49626/fd29b87e22a02fc9a44097d9dc62b9ef74b73fd53d4762bc54ebb165b4ef84e8.png" width="820" alt="Seal의 1:1 대화 화면으로, 그룹 레일, 연락처 목록, 종단 간 암호화된 채팅 창을 보여줍니다." />
<br /><br />
**설정** — 마이크 감도, 푸시 투 토크, 로그인 시 실행, 네트워크 연결 가능 여부.
<img src="https://assets.kitploit.com/production/public/readmes/49626/425c46d395741ed088ac2c50c178e1e41eeb457487cfbc2c0fa6ab61515f648f.png" width="820" alt="Seal의 설정 패널: 마이크 감도, 푸시 투 토크, 로그인 시 실행, 네트워크 연결 가능 여부." />
</div>
## 기능
- **항상 종단 간 암호화** — 모든 메시지는 기기를 떠나기 전에 Olm(1:1) 또는 Megolm(그룹)으로 봉인되며, [vodozemac](https://github.com/matrix-org/vodozemac)의 Double-Ratchet 방식 체계를 사용합니다. 모든 메시지는 고유한 키를 갖습니다.
- **받은 편지함 없음** — 메시지는 직접적인 피어 투 피어 연결([libp2p](https://libp2p.io): QUIC/TCP + Noise, NAT용 릴레이 및 홀 펀칭 포함)을 통해 전송됩니다. 수신자가 오프라인이면 메시지는 로컬에서 대기하고 재시도됩니다. 다른 사람의 인프라에 큐에 쌓이지 않습니다.
- **데이터베이스가 아닌 디렉터리** — 관여하는 유일한 서버(`crates/directory-server`)는 사용자 ID를 현재 네트워크 주소에 매핑할 뿐 그 이상은 하지 않습니다. 구조적으로 메시지 내용을 읽을 수 없습니다. `Cargo.toml`은 그 방법을 아는 크레이트조차 의존하지 않습니다.
- **하나의 기기, 여러 계정** — 재시작 없이 전환할 수 있는 완전히 분리된 신원(키, 연락처, 메시지)을 지원합니다.
- **실제 멤버십 변경이 있는 그룹** — 그룹별 텍스트 및 음성 채널. 누군가를 제거하면 그룹의 키가 순환되어 이후 전송되는 어떤 내용도 읽을 수 없습니다.
- **내장 음성 기능** — 시스템 전체 단축키로 푸시 투 토크(Seal뿐 아니라 모든 앱에서 작동), 조절 가능한 마이크 감도, 선택적 음성 변조기.
- **메타데이터 없는 첨부 파일** — EXIF 데이터(GPS 위치, 카메라/기기 정보)는 이미지가 전송되기 전에 제거되며 기본적으로 활성화되어 있습니다.
- **진짜 패닉 버튼** — 설정 → 데이터 및 개인정보 보호에서 이 기기의 모든 키, 연락처, 메시지를 즉시 그리고 되돌릴 수 없게 삭제하며, 대화 상대에게는 전혀 영향을 주지 않습니다.
- **원한다면 로그인 시 실행** — 기본적으로 켜져 있으며 설정에서 한 번의 토글로 끌 수 있습니다.
- **하나의 코드베이스, 세 플랫폼** — [Tauri](https://tauri.app)를 통해 macOS, Windows, Linux에서 네이티브 창을 제공합니다.
## 작동 방식
이 앱에는 두 종류의 신원이 있으며, 의도적으로 분리되어 있습니다:
- **채팅 신원**은 앱을 처음 열 때 [vodozemac](https://github.com/matrix-org/vodozemac)이 로컬에서 생성하는 Ed25519/Curve25519 키 쌍입니다(`identity::Identity`). 공개 "사용자 ID"는 그 키의 지문일 뿐입니다(`wire_proto::user_id_from_ed25519`). 생성에 서버가 관여하지 않기 때문에 어떤 서버도 이를 발급하거나 취소할 수 없습니다.
- **네트워크 신원**은 전송 계층에서만 사용되는 별도의 libp2p 키 쌍(`PeerId`)입니다. 재시작 후에도 변경될 수 있으며 채팅 신원에는 전혀 영향을 주지 않습니다. 둘은 사용자가 직접 서명한 프레즌스 레코드로만 묶여 있습니다.
누군가를 찾는 것과 실제로 대화하는 것은 서로 다른 두 단계입니다:```
┌────────────────────────┐
│ directory server │
│ (axum + one SQLite │
│ file: users, │
│ presence, group │
│ rosters. Never │
│ message content.) │
└─────────┬───────────────┘
1. "where is bob │ 2. "here's my current
right now?" │ address" (signed,
│ expires in minutes)
┌─────────┴───────────────┐
▼ ▼
┌───────┐ 3. direct libp2p ┌───────┐
│ alice │◄──── connection ────►│ bob │
└───────┘ (Noise + Olm/ └───────┘
Megolm encrypted)
```
1. Alice는 디렉터리에서 Bob의 사용자 ID로 그를 찾는다. 그러면 Bob의
공개 키와 마지막으로 알린 네트워크 주소가 반환된다. 디렉터리가
보관하는 것은 그것이 전부다: 공개 키, 표시 이름, 그룹
멤버십 목록, 그리고 수명이 짧은 주소 알림
(`crates/directory-server`).
2. Alice는 libp2p(QUIC 또는 TCP+Noise, NAT 뒤에 있는 피어를 위한 릴레이 +
홀 펀칭 지원; `crates/net` 참조)를 통해 Bob에게 직접 연결한다.
디렉터리는 이 시점부터 완전히 관여하지 않는다.
3. 실제 메시지는 libp2p 연결에 올려지기 전에 1:1 채팅용 **Olm** 또는
그룹용 **Megolm**(`crates/crypto-session`)으로 암호화된다. 이는
모든 메시지가 고유한 키를 갖는 Double-Ratchet 방식의 기법이다.
서버 측 받은편지함은 없다: Bob이 오프라인이면 메시지는 로컬에서
대기하고 재시도되며, 다른 누군가의 인프라에 저장되지 않는다.
위의 모든 것은 Tauri 앱의 Rust 백엔드(`apps/desktop/src-tauri`)가 실제로
호출하는 `crates/core`의 `AppService`에 의해 조율된다. UI는
네트워크와 직접 통신하지 않는다.
## 프로젝트 구조```
crates/
wire-proto shared signed-request types for the directory API
identity vodozemac identity, OS-keychain key management
storage local encrypted store (contacts, messages, groups)
net libp2p transport + directory HTTP client
crypto-session Olm (1:1) / Megolm (group) session management
core orchestrates the above into `AppService` / `ChatNode`
directory-server the one server component (axum + SQLite)
apps/desktop the Tauri + React app
scripts/ build + backend-deployment scripts (§2, §5)
```
## 1. 사전 요구 사항
모든 플랫폼에서 **Rust**와 **Node.js**가 필요하며, Tauri가 네이티브 창을 빌드하는 데 필요한
플랫폼별 툴체인도 필요합니다. `storage`와 `directory-server`는 또한 SQLite를 소스에서
번들 컴파일하므로 일반 C 컴파일러가 필요합니다(이 프로젝트의 어디에서도
OpenSSL이나 다른 네이티브 암호화 라이브러리는 필요하지 않습니다).
모든 플랫폼 공통:
- [Rust](https://rustup.rs) (stable 채널; OS 패키지 관리자가 아닌 `rustup`으로 설치)
- [Node.js](https://nodejs.org) 20+ 및 npm
<details>
<summary><strong>macOS</strong></summary>```sh
xcode-select --install
```
That's it. Xcode Command Line Tools는 C 컴파일러와 Tauri의 macOS 백엔드(WKWebView 기반)에 필요한 프레임워크를 모두 제공합니다.
</details>
<details>
<summary><strong>Linux</strong></summary>
C 컴파일러, `pkg-config`, 그리고 Tauri의 Linux 백엔드가 링크하는 WebKitGTK/AppIndicator 개발 패키지를 설치합니다.
Debian/Ubuntu:```sh
sudo apt update
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev pkg-config
```
Fedora:```sh
sudo dnf install webkit2gtk4.1-devel openssl-devel curl wget file \
libappindicator-gtk3-devel librsvg2-devel pkgconf-pkg-config
sudo dnf group install "C Development Tools and Libraries"
```
아키텍처:```sh
sudo pacman -S --needed webkit2gtk-4.1 base-devel curl wget file openssl \
appmenu-gtk-module libappindicator-gtk3 librsvg pkgconf
```
(Tauri 릴리스마다 패키지 이름이 달라질 수 있습니다. 빌드 중 누락된 `.pc` 파일을 찾지 못해 실패한다면, 사용 중인 배포판의 [현재 Tauri Linux 사전 요구 사항](https://v2.tauri.app/start/prerequisites/)을 확인하세요.)
</details>
<details>
<summary><strong>Windows</strong></summary>
1. **Microsoft C++ Build Tools**를 설치합니다(Visual Studio 설치 관리자 →
"Desktop development with C++" 워크로드). Tauri의 네이티브 셸과 번들된 SQLite를 컴파일하는 데 모두 필요합니다.
2. **MSVC** Rust 툴체인을 설치합니다: `rustup default stable-msvc`.
3. **WebView2**: Windows 11 및 대부분의 최신 Windows 10 설치에는 이미 포함되어 있습니다. 없는 경우, Tauri 빌드에서 Evergreen 런타임 설치를 안내합니다.
</details>
---
## 2. 빌드
저장소 루트에서:```sh
# Rust workspace (backend crates + the directory server)
cargo build --workspace --release
# Frontend + the actual desktop app bundle (installer/.app/.exe)
cd apps/desktop
npm install
npm run tauri build
```
`npm run tauri build` 는 저장소 루트의 `target/release/bundle/` 아래에 플랫폼 네이티브 설치 프로그램을 생성합니다 (이것은 Cargo 워크스페이스이므로 Tauri 앱을 포함한 모든 크레이트가 하나의 최상위 `target/` 디렉터리를 공유합니다).
크로스 컴파일(예: macOS에서 Windows 설치 프로그램 빌드)은 설정되어 있지 않습니다: 각 대상 플랫폼에서 빌드하거나, CI 기반 릴리스를 원하면 Tauri의 GitHub Actions 워크플로우를 사용하세요.
### 또는 스크립트 사용
`scripts/` 에는 플랫폼/출력별 빌드 스크립트가 하나씩 있으며, 각각 독립적으로 실행할 수 있고 실제로 작동하는 아티팩트를 생성하는 것이 검증되었습니다:
| Script | 생성물 |
|---|---|
| `scripts/build-mac-dmg.sh` | macOS `.dmg` 설치 프로그램 |
| `scripts/build-mac-app.sh` | 원시 macOS `.app` 번들, 설치 프로그램 없음 |
| `scripts/build-linux.sh` | Linux `.AppImage` + `.deb` |
| `scripts/build-windows.ps1` | Windows `.msi` + `.exe` (NSIS) |
각 스크립트는 적절한 플래그와 플랫폼 검사를 사용하여 `npm run tauri build --bundles <...>` 를 감쌀 뿐입니다. 다른 번들 조합을 원하면 직접 원시 명령을 실행하세요 (`apps/desktop` 에서 `npx tauri build --help`).
`scripts/release.sh vX.Y.Z` 는 필요한 모든 위치의 버전을 올리고 커밋에 태그를 답니다 — [`docs/RELEASING.md`](https://github.com/emn4tor/seal/blob/main/docs/RELEASING.md) 참조. macOS와 Linux에서 실행되며, 커밋하거나 푸시하지 않습니다.
### 나만의 "Seal" 네트워크 구워 넣기
서버 선택 화면(§3)에는 항상 세 가지 옵션이 표시됩니다: **Seal** (자체 공식 네트워크), **Custom server**, 그리고 하단의 작은 **Local test server** 링크. "Seal"은 *빌드* 시점에 URL을 구워 넣기 전까지 비활성화되어 있습니다 (회색으로 표시되며 "이 빌드에는 아직 설정되지 않음" 표시):```sh
SEAL_DEFAULT_DIRECTORY_URL=https://directory.example.com npm run tauri build
```
자체 서버를 구축하고(§5) 실제 도메인을 가리키도록 설정했다면, 이 값을 설정하고 다시 빌드하세요. 그러면 이후 배포하는 모든 복사본에는 다른 코드를 건드리지 않아도 "Seal"이 해당 URL을 사용하는 실제 선택 가능한 옵션으로 표시됩니다. 일반/개발 빌드에서는 설정하지 않은 채로 두세요. 이 저장소에서는 공식 서버를 호스팅하지 않으므로 "Seal"은 비활성화 상태로 유지되며, 앱이 실제로 아무것도 실행하지 않는 플레이스홀더 도메인을 조용히 가리키기보다는 사용자가 커스텀 서버나 로컬 서버를 사용하게 됩니다.
---
## 3. 개발 모드에서 실행하기```sh
cd apps/desktop
npm install
npm run tauri dev
```
이 명령은 Vite 개발 서버를 시작하고, Rust 백엔드를 디버그 모드로 컴파일하며,
프런트엔드에 핫 리로드가 적용된 네이티브 창을 엽니다. 첫 빌드는 전체
의존성 트리를 컴파일하므로 몇 분이 걸리지만, 이후 실행은 빠릅니다.
### 서버 선택 (첫 실행)
처음 실행할 때 Seal은 어떤 디렉터리 서버를 사용할지 다음 순서로
묻습니다:
- **Seal**: 이 빌드에 내장되어 있다면 공식 네트워크입니다(§2
참조). 내장되기 전까지는 비활성화되어 있으며, 이 저장소는
플레이스홀더 도메인을 가리키도록 배포되지 않습니다.
- **Custom server**: 자신의 서버를 포함한 모든 서버(§5).
- **Local test server**: 맨 아래에 있는, 의도적으로
눈에 띄지 않게 한 작은 링크입니다. 앱 자체의 **내장 서버**를 띄웁니다.
이 서버는 `127.0.0.1:47100`/`47101`에 바인딩되고, 데이터는 OS의 앱 데이터 디렉터리에 저장됩니다.
Seal을 시험해 보거나 한 대의 머신에서 인스턴스를 테스트하기에는 좋지만,
실제 배포는 아닙니다. 두 번째 인스턴스가 해당 포트가 이미 사용 중임을 발견하면
새 서버를 시작하는 대신 첫 번째 인스턴스의 서버를 재사용합니다.
그 덕분에 한 대의 머신에서 두 인스턴스가 서로를 찾을 수 있습니다. 이 항목은
"Seal"이 구성되어 있지 않고 다른 것을 선택하지 않으면
자동으로 선택됩니다.
선택 사항은 저장되고(`server.json`이 앱의 다른 로컬 데이터 옆에 위치),
이후 모든 실행에서 조용히 재사용됩니다. 설정 → 디렉터리
서버에서 변경할 수 있으며, 변경 사항은 실행 중인 연결을 핫스왑하려 하지
않고 다음에 앱을 시작할 때 적용됩니다. 스크립트/개발 용도로는
환경 변수가 프롬프트를 완전히 건너뜁니다:```sh
P2P_CHAT_DIRECTORY_URL=https://directory.example.com npm run tauri dev
```
### 로컬에서 두 인스턴스 실행하기 (실제 메시징 테스트용)
각 인스턴스는 고유한 정체성이 필요합니다. Seal은 여러 계정을 기본적으로 지원하지만(이 기기의 설정 → 계정), 한 머신에서 두 개의 *별도 프로세스*를 실행하려면 `P2P_CHAT_PROFILE`이 더 빠른 방법입니다. 이 변수는 해당 이름의 계정을 비대화형으로 (첫 실행 시) 자동 생성하거나 (이후 실행마다) 자동 재개하며, 계정 선택 화면을 완전히 건너뜁니다:```sh
# terminal 1
P2P_CHAT_PROFILE=alice npm run tauri dev
# terminal 2
P2P_CHAT_PROFILE=bob npm run tauri dev
```
서버 선택(`server.json`)과 계정 목록(`accounts.json`)은
모두 한 머신의 프로세스 간에 공유되며, 프로필별로 공유되지 않습니다. 맨
처음 실행하는 인스턴스가 서버를 선택하고, 이후의 모든 프로필은
(여기서는 `bob` 포함) 자동으로 그 서버를 재사용합니다. 두 창은 모두
동일한 임베디드 디렉터리 서버에 연결되므로, 서로를 ID로 연락처에 추가하고
두 창 사이에서 메시지를 주고받을 수 있습니다.
Vite의 dev 서버는 Tauri의 webview가 가리킬 실제 고정 포트가 필요하며,
일반적으로는 `npm run tauri dev`를 한 번에 하나만 실행할 수 있습니다 —
두 번째 실행은 포트 1420이 이미 사용 중임을 발견하고 곧바로 실패합니다.
`npm run tauri`는 실제로는 작은 래퍼(`apps/desktop/scripts/tauri.mjs`)로,
첫 번째 이후의 모든 인스턴스에 대해 다음 사용 가능한 포트(1421, 1422, …)을 선택하고
이를 자동으로 연결해 주므로, 위의 두 명령을
두 개의 터미널에서 실행하면 그냥 작동합니다. 다르게 할 필요가 없습니다. 이는
`dev`에 대해서만 동작을 변경합니다 — `npm run tauri build` 및 그 외의 모든 것은
실제 CLI로 그대로 전달됩니다.
### 실제 빌드를 대상으로 한 테스트(dev 모드 아님)```sh
./scripts/run-two-mac-instances.sh # profiles: alice, bob
./scripts/run-two-mac-instances.sh carol dave
```
위와 같은 아이디어지만, 실제 빌드된 앱(`build-mac-app.sh` /
`build-mac-dmg.sh`의 출력물, 또는 `/Applications`에 설치된 복사본)을
`npm run tauri dev` 대신 서로 다른 `P2P_CHAT_PROFILE`로 두 번 실행하여
실제 사용자가 실행하는 환경에 더 가깝습니다. 두 프로세스의 PID와 종료 방법을 출력합니다.
### 디버깅
- **Rust 로그**: 실행 전에 `RUST_LOG`를 설정하세요. 예:
`RUST_LOG=debug npm run tauri dev` (또는 `RUST_LOG=p2p_core=debug,net=debug`로
범위를 좁힐 수 있습니다). 기록되는 필드는 메타데이터(피어/그룹/사용자
ID, 오류 유형)로 제한됩니다. 자세한 로그를 남겨도 안전한 이유는
[`docs/SECURITY.md`](https://github.com/emn4tor/seal/blob/main/docs/SECURITY.md)를 참조하세요.
- **프론트엔드**: 개발 창은 실제 웹뷰입니다. 마우스 오른쪽 버튼 클릭 → 요소 검사
(또는 개발자 도구 열기)는 일반 브라우저처럼 동작합니다.
- **백엔드 크레이트를 개별적으로**: 각 크레이트에는 자체 테스트 스위트가 있어 UI를 전혀 건드리지 않고
실행하고 반복할 수 있습니다. §4를 참조하세요.
- **독립형 디렉터리 서버**(내장 서버 대신): §5를 참조하세요.
---
## 4. 테스트```sh
# everything
cargo test --workspace
# one crate, e.g. the full backend-to-backend flow a Tauri command would trigger
cargo test -p p2p-core --test app_service
# lint + format check (what CI runs)
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
# dependency vulnerability scan
cargo install cargo-audit --locked # once
cargo audit
# frontend type-check + build
cd apps/desktop && npm run build
```
---
## 5. 백엔드(디렉터리 서버) 설정
이것이 실제로 무엇인지 정리하자면, 과장해서 상상하기 쉬우니: 하나의 axum
프로세스, 하나의 SQLite 파일, 세 종류의 레코드(공개 키, 수명이 짧은
프레즌스 알림, 그룹 명단), 모든 쓰기는 호출자 자신의
신원 키로 서명됩니다. 이 서버는 메시지 경로에 절대 포함되지 않습니다. 그 이유는
[`docs/THREAT_MODEL.md`](https://github.com/emn4tor/seal/blob/main/docs/THREAT_MODEL.md)에서 확인하세요.
구조적으로도, 단순한 정책만이 아닙니다: `directory-server`의 `Cargo.toml`은
메시지 내용을 읽는 방법을 아는 크레이트에조차 의존하지 않습니다.
### 가장 빠른 방법: 설정 스크립트```sh
sudo ./scripts/setup-backend.sh
```
대화형이며 Linux + systemd 전용입니다 (이유는 스크립트 헤더 참조). 스크립트는
사용 중인 배포판 계열을 묻습니다 (Debian/Ubuntu, Fedora/RHEL/Rocky/Alma,
Arch/Manjaro 또는 openSUSE. `/etc/os-release`에서 추측한 값으로 미리 채워져
있어서 보통 한 번의 키 입력으로 확인할 수 있습니다). 그리고 각 계열별 전용 함수로
해당 배포판의 빌드 전제 조건을 설치하고, Rust가 없으면 `rustup`을 통해
설치할 것을 제안하며, 릴리스 바이너리를 빌드하고, 전용 시스템 사용자를
만듭니다. 관리자 토큰을 생성하고, 자동 HTTPS와 함께 도메인을
[Caddy](https://caddyserver.com)로 구성할지 묻습니다 (배포판별로 Caddy 자체를
설치하며, 배포판 패키지가 없으면 Caddy 공식 정적 바이너리로 대체합니다).
아니면 직접 앞단을 구성하고 싶다면 루프백/일반 HTTP에만
바인딩할지 묻습니다. 그런 다음 systemd 서비스를 작성하고
활성화합니다. 다시 실행해도 안전합니다.
아래 내용은 실제로 수행하는 작업입니다. 직접 실행하거나 실행 전에 이해하고 싶다면
참고하세요.
### macOS: 빠른 LAN 테스트 서버```sh
./scripts/run-mac-test-server.sh
```
실제 호스팅용이 아닙니다. 도메인, TLS 또는 systemd(macOS에는 어차피 존재하지 않음)를
설정하지 않고도 동일한 네트워크의 두 기기(예: Mac + 다른 기기, 또는 같은 Wi-Fi를
사용하는 두 사람)에서 앱을 테스트하기 위한 것입니다. 릴리스 바이너리를 빌드하고,
관리자 토큰을 생성하며(이후 실행에서 재사용됨), 공개 API를 모든 인터페이스에
바인딩하고, 사용할 URL을 출력합니다.
Mac의 실제 LAN IP(`ipconfig getifaddr`)를 사용하므로, `127.0.0.1`만이 아니라
다른 기기에서도 접근할 수 있습니다. 관리자 포트는 루프백에서만 유지됩니다.
포그라운드에서 실행되며, `Ctrl-C`로 중지합니다. 데이터는
`~/.seal-test-server` 아래에 저장됩니다.
### 빠른 로컬 실행```sh
DIRECTORY_DB_PATH=/var/lib/seal-directory/directory.sqlite3 \
DIRECTORY_PUBLIC_ADDR=0.0.0.0:8080 \
DIRECTORY_ADMIN_ADDR=127.0.0.1:8090 \
DIRECTORY_ADMIN_TOKEN=$(openssl rand -hex 32) \
cargo run --release -p directory-server --bin directory-server
```
| 변수 | 필수 | 의미 |
|---|---|---|
| `DIRECTORY_DB_PATH` | 아니요 (기본값 `directory.sqlite3`, 현재 작업 디렉터리) | 단일 SQLite 파일이 위치하는 곳입니다. 상위 디렉터리가 존재해야 합니다. |
| `DIRECTORY_PUBLIC_ADDR` | 아니요 (기본값 `0.0.0.0:8080`) | 앱이 통신하는 rendezvous API입니다. 공개적으로 노출해도 됩니다. |
| `DIRECTORY_ADMIN_ADDR` | 아니요 (기본값 `127.0.0.1:8090`) | purge 엔드포인트입니다. 공개 인터넷에 노출하지 마세요. 아래를 참조하세요. |
| `DIRECTORY_ADMIN_TOKEN` | **예** | 관리자 API용 Bearer 토큰입니다. 이 값이 없으면 프로세스가 시작되지 않습니다. `openssl rand -hex 32` 또는 유사한 방법으로 생성하고, 다른 곳에서 재사용하지 마세요. |
프로세스는 시작 시 바인딩한 주소를 로그로 남기고, 만약
`DIRECTORY_ADMIN_ADDR`이 루프백이 아니라면 크게 경고합니다.
### 앱에서 이 서버를 가리키기
일반적으로 사용하게 되는 순서대로 세 가지 방법이 있습니다:
1. **최초 실행 화면**: "Custom server"를 선택하고 URL을 입력합니다. §3을 참조하세요.
2. **설정 → 디렉터리 서버**: 나중에 변경할 수 있으며, 다음
재시작 시 적용됩니다.
3. **`P2P_CHAT_DIRECTORY_URL`**, 실행 전에 설정: 묻는 과정을 완전히 건너뛰고
저장된 값을 모두 덮어쓰며, 개발/스크립트 실행에 유용합니다: ```sh
P2P_CHAT_DIRECTORY_URL=https://directory.example.com npm run tauri dev
```
Everyone who wants to find each other needs to point at the *same*
directory instance; it's how they look each other up in the first place.
### 실제 서비스로 실행하기 (systemd)
<details>
<summary>systemd 유닛 + 참고 사항 표시</summary>```ini
# /etc/systemd/system/seal-directory.service
[Unit]
Description=Seal directory server
After=network.target
[Service]
Type=simple
User=seal-directory
Group=seal-directory
Environment=DIRECTORY_DB_PATH=/var/lib/seal-directory/directory.sqlite3
Environment=DIRECTORY_PUBLIC_ADDR=127.0.0.1:8080
Environment=DIRECTORY_ADMIN_ADDR=127.0.0.1:8090
EnvironmentFile=/etc/seal-directory/admin-token.env ; DIRECTORY_ADMIN_TOKEN=...
ExecStart=/usr/local/bin/directory-server
Restart=on-failure
# Sandboxing: this process needs almost nothing
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ReadWritePaths=/var/lib/seal-directory
[Install]
WantedBy=multi-user.target
```
참고:
- `DIRECTORY_PUBLIC_ADDR`는 여기서는 의도적으로 **루프백**에 바인딩됩니다. TLS를 위해
axum을 인터넷에 직접 노출하는 대신 아래와 같이 리버스 프록시를 앞에
두세요.
- `seal-directory` 시스템 사용자/그룹을 생성하고,
`/var/lib/seal-directory`를 먼저 생성하세요 (`useradd --system --no-create-home
seal-directory && install -d -o seal-directory -g seal-directory
/var/lib/seal-directory`), 그리고 빌드된 `directory-server` 바이너리를
(`target/release/`에서) `/usr/local/bin/`으로 복사하세요.
- 관리자 토큰은 root만 읽을 수 있는 `EnvironmentFile`에 넣고, 직접
유닛 파일에 넣지 마세요 (유닛 파일은 종종 모든 사용자가 읽을 수 있습니다).
</details>
### 리버스 프록시를 통한 TLS
<details>
<summary>Caddy / nginx 구성 보기</summary>
[Caddy](https://caddyserver.com)는 가장 적은 설정으로 자동 HTTPS를 제공합니다.
설정:```
# /etc/caddy/Caddyfile
directory.example.com {
reverse_proxy 127.0.0.1:8080
}
```
`caddy run` (또는 `systemctl enable --now caddy`)은 인증서 발급/갱신을
자체적으로 처리합니다. nginx를 사용하고 싶다면 TLS를 nginx에서 종료하고
`proxy_pass http://127.0.0.1:8080;`를 사용하세요. 앱은 프록시 관점에서
일반 HTTP만 필요하기 때문입니다.
방화벽 측면에서: 외부에서 접근 가능해야 하는 것은 공용 포트뿐입니다
(위 예시의 `8080`, 프록시를 통해 443이 앞에 있음). 관리자 포트는
절대 외부에서 접근 가능하면 안 됩니다. SSH 포트 포워딩
(`ssh -L 8090:127.0.0.1:8090 your-server`)을 사용해 원격으로 purge를 실행해야 할 때
접근하세요.
</details>
### Purge하기```sh
cargo run --release -p directory-server --bin directory-admin -- \
--admin-url http://127.0.0.1:8090 --token "$DIRECTORY_ADMIN_TOKEN" purge
```
이것은 SQLite 파일을 삭제하고 빈 스키마를 다시 생성합니다. `DELETE` 문도 없고, 부분적인 상태도 없습니다. 먼저 누구에게 알리지 않고 실행해도 안전합니다. 그 안의 모든 레코드는 각 클라이언트가 이미 로컬에 보유한 데이터(자체 등록 정보, 현재 상태, 자신이 속한 그룹 명단)의 캐시일 뿐이므로, 클라이언트는 다음 작업을 수행하는 순간 몇 분 안에 다시 채웁니다. 이 데이터베이스에는 의도적으로 백업 정책이 없습니다. 백업을 유지하는 것이 전체 목적을 훼손하는 이유는 [`docs/SECURITY.md`](https://github.com/emn4tor/seal/blob/main/docs/SECURITY.md)를 참조하세요.
---
## 6. 앱 사용하기
1. **첫 실행, 첫 질문**: 사용할 디렉터리 서버를 선택합니다(§3). 기본값은 실행 중인 빌드에 내장된 값입니다(그것을 빌드한 사람이 공식 서버를 구성하지 않았다면 로컬 테스트 서버). 직접 또는 신뢰하는 누군가가 호스팅하는 서버를 지정하려면 "사용자 지정 서버"를 선택하세요.
---
[Read more](https://github.com/emn4tor/seal)