glypha 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glypha-2.0.0/PKG-INFO +217 -0
- glypha-2.0.0/README.md +202 -0
- glypha-2.0.0/glypha.egg-info/PKG-INFO +217 -0
- glypha-2.0.0/glypha.egg-info/SOURCES.txt +17 -0
- glypha-2.0.0/glypha.egg-info/dependency_links.txt +1 -0
- glypha-2.0.0/glypha.egg-info/entry_points.txt +4 -0
- glypha-2.0.0/glypha.egg-info/requires.txt +1 -0
- glypha-2.0.0/glypha.egg-info/top_level.txt +6 -0
- glypha-2.0.0/identity.py +31 -0
- glypha-2.0.0/peer.py +652 -0
- glypha-2.0.0/protocol.py +42 -0
- glypha-2.0.0/pyproject.toml +30 -0
- glypha-2.0.0/relay_server.py +106 -0
- glypha-2.0.0/rendezvous_server.py +99 -0
- glypha-2.0.0/setup.cfg +4 -0
- glypha-2.0.0/storage.py +80 -0
- glypha-2.0.0/tests/test_identity.py +51 -0
- glypha-2.0.0/tests/test_protocol.py +106 -0
- glypha-2.0.0/tests/test_storage.py +82 -0
glypha-2.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: glypha
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: P2P encrypted chat from raw Python sockets: E2E encryption, persistent identities, fingerprint verification, peer discovery, NAT traversal with relay fallback
|
|
5
|
+
Author: Adarsh Mishra
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/<your-github-username>/<repo-name>
|
|
8
|
+
Keywords: p2p,encryption,chat,sockets,nacl,networking,e2e
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Communications :: Chat
|
|
11
|
+
Classifier: Topic :: Education
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: pynacl>=1.5
|
|
15
|
+
|
|
16
|
+
# P2P Encrypted Chat
|
|
17
|
+
|
|
18
|
+
An educational peer-to-peer encrypted messaging system built from raw Python TCP sockets — custom message framing, persistent cryptographic identities, fingerprint-based peer authentication, encrypted local history, peer discovery via a cloud rendezvous server, and a measured NAT traversal experiment.
|
|
19
|
+
|
|
20
|
+
This is **not** a WebRTC/PeerJS wrapper. Every layer — framing, encryption, identity, storage, discovery — is built and understood from the socket up.
|
|
21
|
+
|
|
22
|
+
> ⚠️ **Educational project. Not Signal-grade security. Do not use for sensitive communications.** See [Security Model](#security-model) below.
|
|
23
|
+
|
|
24
|
+
## Table of Contents
|
|
25
|
+
|
|
26
|
+
- [What It Does](#what-it-does)
|
|
27
|
+
- [Architecture](#architecture)
|
|
28
|
+
- [Usage](#usage)
|
|
29
|
+
- [The NAT Traversal Experiment](#the-nat-traversal-experiment)
|
|
30
|
+
- [Security Model](#security-model)
|
|
31
|
+
- [Repository Layout](#repository-layout)
|
|
32
|
+
- [Testing](#testing)
|
|
33
|
+
- [Bugs Found (and Fixed) Along the Way](#bugs-found-and-fixed-along-the-way)
|
|
34
|
+
- [Infrastructure Notes](#infrastructure-notes)
|
|
35
|
+
- [Roadmap (V2)](#roadmap-v2)
|
|
36
|
+
|
|
37
|
+
## What It Does
|
|
38
|
+
|
|
39
|
+
| Capability | Proof |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Two peers chat E2E-encrypted | Verified on two physical laptops over LAN, recorded on video |
|
|
42
|
+
| Identity survives restarts | Same fingerprint (`c849:a2b7...`) across every session of the build |
|
|
43
|
+
| History persists, encrypted at rest | Restart → history reloads and decrypts; raw SQL shows only ciphertext |
|
|
44
|
+
| Peer discovery by name | `find aadarsh` → endpoint + fingerprint, worked across two Indian carrier networks + Azure |
|
|
45
|
+
| Cross-network discovery over the real Internet | Rendezvous hosted on an Azure VM; peers on home Wi-Fi (Delhi) + mobile hotspot registered and looked each other up |
|
|
46
|
+
| Fingerprint verification | Both sides display fingerprints; out-of-band comparison; pre-connect verification via the registry |
|
|
47
|
+
|
|
48
|
+
## Demo
|
|
49
|
+
|
|
50
|
+
🎥 Two physical laptops chatting over LAN, disconnect → reconnect → history reloads.
|
|
51
|
+
|
|
52
|
+
| Fingerprint verification across two screens | Rendezvous log: two peers, two public NAT IPs | Punch verdict on CGNAT |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| The out-of-band fingerprint check, side by side | The registry holding two different public NAT mappings (home Wi-Fi + mobile hotspot) — discovery across the real Internet | Control pass on loopback vs. failure on real CGNAT |
|
|
55
|
+
|
|
56
|
+
*(Screenshots live in `screenshots/`.)*
|
|
57
|
+
|
|
58
|
+
## Architecture
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
┌──────────────────────┐
|
|
62
|
+
│ Rendezvous Server │
|
|
63
|
+
│ Discovery/Signaling │ (Azure VM, port 7000)
|
|
64
|
+
└──────────┬───────────┘
|
|
65
|
+
│ endpoint + fingerprint
|
|
66
|
+
↙ ↘
|
|
67
|
+
┌────────┐ ┌────────┐
|
|
68
|
+
│ Peer A │◄─►│ Peer B │ direct P2P chat (port 9999)
|
|
69
|
+
└────────┘ └────────┘
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Every layer of the transport stack is implemented, not imported:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
Application (chat, history)
|
|
76
|
+
↓
|
|
77
|
+
Peer management (listen / connect / find / punch modes)
|
|
78
|
+
↓
|
|
79
|
+
Cryptographic identity (persistent keypairs, fingerprints)
|
|
80
|
+
↓
|
|
81
|
+
PyNaCl Box (X25519 key exchange + XSalsa20-Poly1305 AEAD)
|
|
82
|
+
↓
|
|
83
|
+
Custom framing ([4-byte big-endian length][payload])
|
|
84
|
+
↓
|
|
85
|
+
TCP sockets (blocking, threaded receive)
|
|
86
|
+
↓
|
|
87
|
+
IP / NAT / firewall
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Local storage is a separate concern from transport: every message is passed through `SecretBox.encrypt` before being written to a SQLite `BLOB`, using a storage key that is distinct from the transport identity key.
|
|
91
|
+
|
|
92
|
+
## Usage
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git clone <repo> && cd p2p-chat
|
|
96
|
+
pip install pynacl
|
|
97
|
+
|
|
98
|
+
# terminal 1 — rendezvous server (or point at the deployed one)
|
|
99
|
+
python rendezvous_server.py # port 7000
|
|
100
|
+
|
|
101
|
+
# terminal 2 — peer A, discoverable by name
|
|
102
|
+
python peer.py listen 9999 aadarsh <rv_ip>
|
|
103
|
+
|
|
104
|
+
# terminal 3 — peer B finds and connects
|
|
105
|
+
python peer.py find aadarsh <rv_ip>
|
|
106
|
+
python peer.py connect <aadarsh-ip> 9999 ishu
|
|
107
|
+
|
|
108
|
+
# NAT traversal attempt (coordinated simultaneous open)
|
|
109
|
+
python peer.py punch <my_id> <peer_id> <my_port> <rv_ip>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Once connected, both sides display their fingerprints. Compare them out-of-band (e.g. a phone call) before trusting the session. Type `quit` to exit — history reloads automatically the next time you connect to the same peer.
|
|
113
|
+
|
|
114
|
+
## The NAT Traversal Experiment
|
|
115
|
+
|
|
116
|
+
This is the centerpiece of the V1 build: a real, measured attempt at direct P2P connectivity across the open Internet — not a simulated or hand-waved result.
|
|
117
|
+
|
|
118
|
+
**Setup:** Rendezvous server on an Azure VM (Central India). Peer A on home Wi-Fi (`192.168.1.8` local, NAT public `122.162.151.183`). Peer B on a phone hotspot (`10.197.183.135` local behind carrier-grade NAT, NAT public `157.49.119.123`).
|
|
119
|
+
|
|
120
|
+
| Test | Result |
|
|
121
|
+
|---|---|
|
|
122
|
+
| Baseline: uncoordinated direct dial across the Internet | ❌ `WinError 10060` timeout (~20s) — NAT drops the unsolicited inbound SYN |
|
|
123
|
+
| Control: simultaneous open on loopback | ✅ Punched through — hole-punching mechanics proven correct |
|
|
124
|
+
| Simultaneous open, home NAT ↔ Airtel CGNAT, 60s | ❌ Neither NAT delivered the peer's SYNs |
|
|
125
|
+
|
|
126
|
+
**Conclusion:** the code is correct (the loopback control passes); the network refuses. Carrier-grade NAT shares one public IP across thousands of subscribers with endpoint-dependent mapping, which direct TCP hole punching cannot reliably defeat. This is a measured failure, explained at the NAT level, not a bug.
|
|
127
|
+
|
|
128
|
+
**Fallback strategy (documented, V2):** a relay forwards already-encrypted traffic. The rendezvous/relay server never holds keys — by construction, it doesn't even have PyNaCl installed, so the "phone book" literally cannot read messages even if it wanted to.
|
|
129
|
+
|
|
130
|
+
## Security Model
|
|
131
|
+
|
|
132
|
+
**Protects against:**
|
|
133
|
+
- Passive network observers (end-to-end AEAD encryption)
|
|
134
|
+
- Peer impersonation across reconnects (persistent identities + fingerprint verification)
|
|
135
|
+
- Plaintext history on disk (SQLite holds only ciphertext)
|
|
136
|
+
- IP changes redefining identity (history is keyed by fingerprint, not IP)
|
|
137
|
+
|
|
138
|
+
**Does NOT protect against:**
|
|
139
|
+
- **No forward secrecy** — static-static ECDH means a later key compromise exposes previously recorded traffic; there is no ratchet
|
|
140
|
+
- Unencrypted key files at rest
|
|
141
|
+
- Replay attacks (no counters or nonces yet)
|
|
142
|
+
- A malicious rendezvous server serving the wrong endpoint (fingerprint comparison mitigates this, but Trust-On-First-Use doesn't eliminate it)
|
|
143
|
+
- Traffic analysis / metadata leakage
|
|
144
|
+
- Machine compromise
|
|
145
|
+
- `peer_id` squatting — names aren't yet bound to keys (V2 pins names to the first-registered key)
|
|
146
|
+
- Implementation bugs not yet found
|
|
147
|
+
|
|
148
|
+
This is an educational project. **Do not use it for sensitive real-world communications.**
|
|
149
|
+
|
|
150
|
+
## Repository Layout
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
p2p-chat/
|
|
154
|
+
├── peer.py # unified peer: listen | connect | find | punch
|
|
155
|
+
├── protocol.py # length-prefixed framing (OSError-as-EOF contract)
|
|
156
|
+
├── identity.py # persistent transport + storage keys
|
|
157
|
+
├── storage.py # encrypted SQLite history
|
|
158
|
+
├── rendezvous_server.py # discovery/signaling (port 7000, TTL 90s)
|
|
159
|
+
├── crypto_test.py # manual crypto sanity check
|
|
160
|
+
├── tests/ # 19 pytest tests
|
|
161
|
+
├── project.md # living engineering doc (milestones, threat model)
|
|
162
|
+
├── README.md # this file
|
|
163
|
+
└── .gitignore # *.bin *.db *.pem __pycache__/ .pytest_cache/
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Testing
|
|
167
|
+
|
|
168
|
+
19 automated tests across 3 files, running in ~0.53s:
|
|
169
|
+
|
|
170
|
+
- **`test_protocol.py` (8 tests):** roundtrips (small/empty/binary-256), back-to-back frames in one burst, fragmented delivery (header split mid-stream), 1 MiB through a threaded reader, clean-FIN → `None`, RST → `None` (regression for the M13 shutdown bug, reproduced via `SO_LINGER(0)`)
|
|
171
|
+
- **`test_storage.py` (6 tests):** roundtrip, insertion order (locks `ORDER BY id`), per-fingerprint isolation, message direction, ciphertext-at-rest (reads the raw SQL like an attacker would), wrong key → `CryptoError` (regression for the M12 key-orphaning incident)
|
|
172
|
+
- **`test_identity.py` (5 tests):** key persistence, distinct identities, fingerprint stability across reloads, fingerprint format contract (16×4 hex groups; stripping colons = raw SHA-256), `SecretBox` key size
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
python -m pytest tests/ -v
|
|
176
|
+
# 19 passed in ~0.5s
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Bugs Found (and Fixed) Along the Way
|
|
180
|
+
|
|
181
|
+
Each of these was caught by the test suite or a real run, and each is now guarded against:
|
|
182
|
+
|
|
183
|
+
- **`Peer.py` vs `peer.py`** — Windows case-insensitivity masked this; caught on the first test run and would have broken on Linux/macOS
|
|
184
|
+
- **Windows `Ctrl+C` sends RST, not FIN** — the receive thread crashed with `ConnectionResetError`; fixed by treating `OSError` as EOF everywhere in `recv_message`, giving one consistent failure convention
|
|
185
|
+
- **Daemon thread killed mid-print at shutdown** (`Fatal Python error: _enter_buffered_busy`) — fixed by joining the receiver thread with a 2s timeout before exit
|
|
186
|
+
- **Phantom "Peer disconnected" on your own `quit`** — fixed with an `if connected:` guard
|
|
187
|
+
- **Storage-key rename orphaned encrypted history** — surfaces as a loud `CryptoError` by design; now locked by a regression test
|
|
188
|
+
- **`ORDER BY timestamp` scrambled messages at second-granularity** — switched to `ORDER BY id`
|
|
189
|
+
- **Fingerprint gate was exact-match and rejected `"y"`** — fixed with an explicit allowlist (`yes`/`y`); anything else still fails closed
|
|
190
|
+
- **Punch-mode split-brain connections** — fixed with identity-proof at accept: a connection must present the public key matching the registry fingerprint, or it's dropped
|
|
191
|
+
- **Windows failed-socket reuse** — fixed by using a fresh socket per punch attempt
|
|
192
|
+
- **`get_lan_ip()` originally used a TCP socket** — it would open a real connection to `8.8.8.8:80` (hanging ~20s when unreachable) instead of doing a route lookup — replaced with a UDP socket, which never sends a packet
|
|
193
|
+
- **Rendezvous lookup race** — fixed with a 20s retry loop so start order no longer matters
|
|
194
|
+
- **Python block-buffers stdout when redirected**, causing empty logs on an otherwise healthy server — fixed by running with `python3 -u`
|
|
195
|
+
|
|
196
|
+
## Infrastructure Notes
|
|
197
|
+
|
|
198
|
+
Real cloud/ops lessons from deploying the rendezvous server:
|
|
199
|
+
|
|
200
|
+
- **Azure for Students** has region allowlist restrictions (`RequestDisallowedByAzure`) and a B-series vCPU quota of 0 in some regions; worked around with a `B2ats_v2` instance (~$0.0062/hr). No auto-shutdown was available in-region, so a manual portal-stop discipline plus a $15 budget alert was used instead.
|
|
201
|
+
- Two independent firewall layers must both be opened for the server to be reachable: the cloud Network Security Group **and** the OS-level `ufw`.
|
|
202
|
+
- A VM restart kills running processes but not files, so the server is started with `nohup python3 -u rendezvous_server.py &` every time; `pgrep` is checked first to avoid stale double instances (which caused a confusing false failure once).
|
|
203
|
+
- MinTTY/Git Bash swallows `Ctrl+C` for console-less programs — use `winpty`.
|
|
204
|
+
- The VM's public IP can change on deallocate, so it's always re-read from the Azure portal rather than assumed.
|
|
205
|
+
- The rendezvous server records the **observed** source IP of a connecting peer (which can't be lied to) alongside the **peer-advertised** listen port (which it has no way to independently verify). Registering via `127.0.0.1` poisons the registry — the same observed-vs-advertised mechanism is what later reveals real NAT public mappings.
|
|
206
|
+
|
|
207
|
+
## Roadmap (V2)
|
|
208
|
+
|
|
209
|
+
- **Relay implementation** — the fallback for when punching fails; already-encrypted traffic is relayed, and the rendezvous server is upgraded to hand out relay info. This is what makes cross-Internet chat work reliably, not just when NAT cooperates.
|
|
210
|
+
- **Packaging** — a `pyproject.toml` and a PyPI release, so usage becomes `pip install <name>` followed by `p2pchat listen --name aadarsh` / `p2pchat connect aadarsh`, with no files, ports, or internals exposed to the end user.
|
|
211
|
+
- **Auto-fallback chain** — direct → punch → relay, orchestrated by a connection state machine.
|
|
212
|
+
- **Name pinning** — first registration binds a `peer_id` to a public key, closing the squatting gap.
|
|
213
|
+
- **Hardening** — `MAX_MESSAGE_SIZE`, replay counters, heartbeats/timeouts, and key-file permissions.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
*Stack: Python 3.13, stdlib `socket`/`threading`/`struct`/`sqlite3`/`json`, PyNaCl (libsodium bindings), pytest. No frameworks. ~5 core modules + rendezvous server, 19 automated tests, 15 milestones, ~4 weeks of build time.*
|
glypha-2.0.0/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# P2P Encrypted Chat
|
|
2
|
+
|
|
3
|
+
An educational peer-to-peer encrypted messaging system built from raw Python TCP sockets — custom message framing, persistent cryptographic identities, fingerprint-based peer authentication, encrypted local history, peer discovery via a cloud rendezvous server, and a measured NAT traversal experiment.
|
|
4
|
+
|
|
5
|
+
This is **not** a WebRTC/PeerJS wrapper. Every layer — framing, encryption, identity, storage, discovery — is built and understood from the socket up.
|
|
6
|
+
|
|
7
|
+
> ⚠️ **Educational project. Not Signal-grade security. Do not use for sensitive communications.** See [Security Model](#security-model) below.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [What It Does](#what-it-does)
|
|
12
|
+
- [Architecture](#architecture)
|
|
13
|
+
- [Usage](#usage)
|
|
14
|
+
- [The NAT Traversal Experiment](#the-nat-traversal-experiment)
|
|
15
|
+
- [Security Model](#security-model)
|
|
16
|
+
- [Repository Layout](#repository-layout)
|
|
17
|
+
- [Testing](#testing)
|
|
18
|
+
- [Bugs Found (and Fixed) Along the Way](#bugs-found-and-fixed-along-the-way)
|
|
19
|
+
- [Infrastructure Notes](#infrastructure-notes)
|
|
20
|
+
- [Roadmap (V2)](#roadmap-v2)
|
|
21
|
+
|
|
22
|
+
## What It Does
|
|
23
|
+
|
|
24
|
+
| Capability | Proof |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Two peers chat E2E-encrypted | Verified on two physical laptops over LAN, recorded on video |
|
|
27
|
+
| Identity survives restarts | Same fingerprint (`c849:a2b7...`) across every session of the build |
|
|
28
|
+
| History persists, encrypted at rest | Restart → history reloads and decrypts; raw SQL shows only ciphertext |
|
|
29
|
+
| Peer discovery by name | `find aadarsh` → endpoint + fingerprint, worked across two Indian carrier networks + Azure |
|
|
30
|
+
| Cross-network discovery over the real Internet | Rendezvous hosted on an Azure VM; peers on home Wi-Fi (Delhi) + mobile hotspot registered and looked each other up |
|
|
31
|
+
| Fingerprint verification | Both sides display fingerprints; out-of-band comparison; pre-connect verification via the registry |
|
|
32
|
+
|
|
33
|
+
## Demo
|
|
34
|
+
|
|
35
|
+
🎥 Two physical laptops chatting over LAN, disconnect → reconnect → history reloads.
|
|
36
|
+
|
|
37
|
+
| Fingerprint verification across two screens | Rendezvous log: two peers, two public NAT IPs | Punch verdict on CGNAT |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| The out-of-band fingerprint check, side by side | The registry holding two different public NAT mappings (home Wi-Fi + mobile hotspot) — discovery across the real Internet | Control pass on loopback vs. failure on real CGNAT |
|
|
40
|
+
|
|
41
|
+
*(Screenshots live in `screenshots/`.)*
|
|
42
|
+
|
|
43
|
+
## Architecture
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
┌──────────────────────┐
|
|
47
|
+
│ Rendezvous Server │
|
|
48
|
+
│ Discovery/Signaling │ (Azure VM, port 7000)
|
|
49
|
+
└──────────┬───────────┘
|
|
50
|
+
│ endpoint + fingerprint
|
|
51
|
+
↙ ↘
|
|
52
|
+
┌────────┐ ┌────────┐
|
|
53
|
+
│ Peer A │◄─►│ Peer B │ direct P2P chat (port 9999)
|
|
54
|
+
└────────┘ └────────┘
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Every layer of the transport stack is implemented, not imported:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
Application (chat, history)
|
|
61
|
+
↓
|
|
62
|
+
Peer management (listen / connect / find / punch modes)
|
|
63
|
+
↓
|
|
64
|
+
Cryptographic identity (persistent keypairs, fingerprints)
|
|
65
|
+
↓
|
|
66
|
+
PyNaCl Box (X25519 key exchange + XSalsa20-Poly1305 AEAD)
|
|
67
|
+
↓
|
|
68
|
+
Custom framing ([4-byte big-endian length][payload])
|
|
69
|
+
↓
|
|
70
|
+
TCP sockets (blocking, threaded receive)
|
|
71
|
+
↓
|
|
72
|
+
IP / NAT / firewall
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Local storage is a separate concern from transport: every message is passed through `SecretBox.encrypt` before being written to a SQLite `BLOB`, using a storage key that is distinct from the transport identity key.
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone <repo> && cd p2p-chat
|
|
81
|
+
pip install pynacl
|
|
82
|
+
|
|
83
|
+
# terminal 1 — rendezvous server (or point at the deployed one)
|
|
84
|
+
python rendezvous_server.py # port 7000
|
|
85
|
+
|
|
86
|
+
# terminal 2 — peer A, discoverable by name
|
|
87
|
+
python peer.py listen 9999 aadarsh <rv_ip>
|
|
88
|
+
|
|
89
|
+
# terminal 3 — peer B finds and connects
|
|
90
|
+
python peer.py find aadarsh <rv_ip>
|
|
91
|
+
python peer.py connect <aadarsh-ip> 9999 ishu
|
|
92
|
+
|
|
93
|
+
# NAT traversal attempt (coordinated simultaneous open)
|
|
94
|
+
python peer.py punch <my_id> <peer_id> <my_port> <rv_ip>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Once connected, both sides display their fingerprints. Compare them out-of-band (e.g. a phone call) before trusting the session. Type `quit` to exit — history reloads automatically the next time you connect to the same peer.
|
|
98
|
+
|
|
99
|
+
## The NAT Traversal Experiment
|
|
100
|
+
|
|
101
|
+
This is the centerpiece of the V1 build: a real, measured attempt at direct P2P connectivity across the open Internet — not a simulated or hand-waved result.
|
|
102
|
+
|
|
103
|
+
**Setup:** Rendezvous server on an Azure VM (Central India). Peer A on home Wi-Fi (`192.168.1.8` local, NAT public `122.162.151.183`). Peer B on a phone hotspot (`10.197.183.135` local behind carrier-grade NAT, NAT public `157.49.119.123`).
|
|
104
|
+
|
|
105
|
+
| Test | Result |
|
|
106
|
+
|---|---|
|
|
107
|
+
| Baseline: uncoordinated direct dial across the Internet | ❌ `WinError 10060` timeout (~20s) — NAT drops the unsolicited inbound SYN |
|
|
108
|
+
| Control: simultaneous open on loopback | ✅ Punched through — hole-punching mechanics proven correct |
|
|
109
|
+
| Simultaneous open, home NAT ↔ Airtel CGNAT, 60s | ❌ Neither NAT delivered the peer's SYNs |
|
|
110
|
+
|
|
111
|
+
**Conclusion:** the code is correct (the loopback control passes); the network refuses. Carrier-grade NAT shares one public IP across thousands of subscribers with endpoint-dependent mapping, which direct TCP hole punching cannot reliably defeat. This is a measured failure, explained at the NAT level, not a bug.
|
|
112
|
+
|
|
113
|
+
**Fallback strategy (documented, V2):** a relay forwards already-encrypted traffic. The rendezvous/relay server never holds keys — by construction, it doesn't even have PyNaCl installed, so the "phone book" literally cannot read messages even if it wanted to.
|
|
114
|
+
|
|
115
|
+
## Security Model
|
|
116
|
+
|
|
117
|
+
**Protects against:**
|
|
118
|
+
- Passive network observers (end-to-end AEAD encryption)
|
|
119
|
+
- Peer impersonation across reconnects (persistent identities + fingerprint verification)
|
|
120
|
+
- Plaintext history on disk (SQLite holds only ciphertext)
|
|
121
|
+
- IP changes redefining identity (history is keyed by fingerprint, not IP)
|
|
122
|
+
|
|
123
|
+
**Does NOT protect against:**
|
|
124
|
+
- **No forward secrecy** — static-static ECDH means a later key compromise exposes previously recorded traffic; there is no ratchet
|
|
125
|
+
- Unencrypted key files at rest
|
|
126
|
+
- Replay attacks (no counters or nonces yet)
|
|
127
|
+
- A malicious rendezvous server serving the wrong endpoint (fingerprint comparison mitigates this, but Trust-On-First-Use doesn't eliminate it)
|
|
128
|
+
- Traffic analysis / metadata leakage
|
|
129
|
+
- Machine compromise
|
|
130
|
+
- `peer_id` squatting — names aren't yet bound to keys (V2 pins names to the first-registered key)
|
|
131
|
+
- Implementation bugs not yet found
|
|
132
|
+
|
|
133
|
+
This is an educational project. **Do not use it for sensitive real-world communications.**
|
|
134
|
+
|
|
135
|
+
## Repository Layout
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
p2p-chat/
|
|
139
|
+
├── peer.py # unified peer: listen | connect | find | punch
|
|
140
|
+
├── protocol.py # length-prefixed framing (OSError-as-EOF contract)
|
|
141
|
+
├── identity.py # persistent transport + storage keys
|
|
142
|
+
├── storage.py # encrypted SQLite history
|
|
143
|
+
├── rendezvous_server.py # discovery/signaling (port 7000, TTL 90s)
|
|
144
|
+
├── crypto_test.py # manual crypto sanity check
|
|
145
|
+
├── tests/ # 19 pytest tests
|
|
146
|
+
├── project.md # living engineering doc (milestones, threat model)
|
|
147
|
+
├── README.md # this file
|
|
148
|
+
└── .gitignore # *.bin *.db *.pem __pycache__/ .pytest_cache/
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Testing
|
|
152
|
+
|
|
153
|
+
19 automated tests across 3 files, running in ~0.53s:
|
|
154
|
+
|
|
155
|
+
- **`test_protocol.py` (8 tests):** roundtrips (small/empty/binary-256), back-to-back frames in one burst, fragmented delivery (header split mid-stream), 1 MiB through a threaded reader, clean-FIN → `None`, RST → `None` (regression for the M13 shutdown bug, reproduced via `SO_LINGER(0)`)
|
|
156
|
+
- **`test_storage.py` (6 tests):** roundtrip, insertion order (locks `ORDER BY id`), per-fingerprint isolation, message direction, ciphertext-at-rest (reads the raw SQL like an attacker would), wrong key → `CryptoError` (regression for the M12 key-orphaning incident)
|
|
157
|
+
- **`test_identity.py` (5 tests):** key persistence, distinct identities, fingerprint stability across reloads, fingerprint format contract (16×4 hex groups; stripping colons = raw SHA-256), `SecretBox` key size
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
python -m pytest tests/ -v
|
|
161
|
+
# 19 passed in ~0.5s
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Bugs Found (and Fixed) Along the Way
|
|
165
|
+
|
|
166
|
+
Each of these was caught by the test suite or a real run, and each is now guarded against:
|
|
167
|
+
|
|
168
|
+
- **`Peer.py` vs `peer.py`** — Windows case-insensitivity masked this; caught on the first test run and would have broken on Linux/macOS
|
|
169
|
+
- **Windows `Ctrl+C` sends RST, not FIN** — the receive thread crashed with `ConnectionResetError`; fixed by treating `OSError` as EOF everywhere in `recv_message`, giving one consistent failure convention
|
|
170
|
+
- **Daemon thread killed mid-print at shutdown** (`Fatal Python error: _enter_buffered_busy`) — fixed by joining the receiver thread with a 2s timeout before exit
|
|
171
|
+
- **Phantom "Peer disconnected" on your own `quit`** — fixed with an `if connected:` guard
|
|
172
|
+
- **Storage-key rename orphaned encrypted history** — surfaces as a loud `CryptoError` by design; now locked by a regression test
|
|
173
|
+
- **`ORDER BY timestamp` scrambled messages at second-granularity** — switched to `ORDER BY id`
|
|
174
|
+
- **Fingerprint gate was exact-match and rejected `"y"`** — fixed with an explicit allowlist (`yes`/`y`); anything else still fails closed
|
|
175
|
+
- **Punch-mode split-brain connections** — fixed with identity-proof at accept: a connection must present the public key matching the registry fingerprint, or it's dropped
|
|
176
|
+
- **Windows failed-socket reuse** — fixed by using a fresh socket per punch attempt
|
|
177
|
+
- **`get_lan_ip()` originally used a TCP socket** — it would open a real connection to `8.8.8.8:80` (hanging ~20s when unreachable) instead of doing a route lookup — replaced with a UDP socket, which never sends a packet
|
|
178
|
+
- **Rendezvous lookup race** — fixed with a 20s retry loop so start order no longer matters
|
|
179
|
+
- **Python block-buffers stdout when redirected**, causing empty logs on an otherwise healthy server — fixed by running with `python3 -u`
|
|
180
|
+
|
|
181
|
+
## Infrastructure Notes
|
|
182
|
+
|
|
183
|
+
Real cloud/ops lessons from deploying the rendezvous server:
|
|
184
|
+
|
|
185
|
+
- **Azure for Students** has region allowlist restrictions (`RequestDisallowedByAzure`) and a B-series vCPU quota of 0 in some regions; worked around with a `B2ats_v2` instance (~$0.0062/hr). No auto-shutdown was available in-region, so a manual portal-stop discipline plus a $15 budget alert was used instead.
|
|
186
|
+
- Two independent firewall layers must both be opened for the server to be reachable: the cloud Network Security Group **and** the OS-level `ufw`.
|
|
187
|
+
- A VM restart kills running processes but not files, so the server is started with `nohup python3 -u rendezvous_server.py &` every time; `pgrep` is checked first to avoid stale double instances (which caused a confusing false failure once).
|
|
188
|
+
- MinTTY/Git Bash swallows `Ctrl+C` for console-less programs — use `winpty`.
|
|
189
|
+
- The VM's public IP can change on deallocate, so it's always re-read from the Azure portal rather than assumed.
|
|
190
|
+
- The rendezvous server records the **observed** source IP of a connecting peer (which can't be lied to) alongside the **peer-advertised** listen port (which it has no way to independently verify). Registering via `127.0.0.1` poisons the registry — the same observed-vs-advertised mechanism is what later reveals real NAT public mappings.
|
|
191
|
+
|
|
192
|
+
## Roadmap (V2)
|
|
193
|
+
|
|
194
|
+
- **Relay implementation** — the fallback for when punching fails; already-encrypted traffic is relayed, and the rendezvous server is upgraded to hand out relay info. This is what makes cross-Internet chat work reliably, not just when NAT cooperates.
|
|
195
|
+
- **Packaging** — a `pyproject.toml` and a PyPI release, so usage becomes `pip install <name>` followed by `p2pchat listen --name aadarsh` / `p2pchat connect aadarsh`, with no files, ports, or internals exposed to the end user.
|
|
196
|
+
- **Auto-fallback chain** — direct → punch → relay, orchestrated by a connection state machine.
|
|
197
|
+
- **Name pinning** — first registration binds a `peer_id` to a public key, closing the squatting gap.
|
|
198
|
+
- **Hardening** — `MAX_MESSAGE_SIZE`, replay counters, heartbeats/timeouts, and key-file permissions.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
*Stack: Python 3.13, stdlib `socket`/`threading`/`struct`/`sqlite3`/`json`, PyNaCl (libsodium bindings), pytest. No frameworks. ~5 core modules + rendezvous server, 19 automated tests, 15 milestones, ~4 weeks of build time.*
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: glypha
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: P2P encrypted chat from raw Python sockets: E2E encryption, persistent identities, fingerprint verification, peer discovery, NAT traversal with relay fallback
|
|
5
|
+
Author: Adarsh Mishra
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/<your-github-username>/<repo-name>
|
|
8
|
+
Keywords: p2p,encryption,chat,sockets,nacl,networking,e2e
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Communications :: Chat
|
|
11
|
+
Classifier: Topic :: Education
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: pynacl>=1.5
|
|
15
|
+
|
|
16
|
+
# P2P Encrypted Chat
|
|
17
|
+
|
|
18
|
+
An educational peer-to-peer encrypted messaging system built from raw Python TCP sockets — custom message framing, persistent cryptographic identities, fingerprint-based peer authentication, encrypted local history, peer discovery via a cloud rendezvous server, and a measured NAT traversal experiment.
|
|
19
|
+
|
|
20
|
+
This is **not** a WebRTC/PeerJS wrapper. Every layer — framing, encryption, identity, storage, discovery — is built and understood from the socket up.
|
|
21
|
+
|
|
22
|
+
> ⚠️ **Educational project. Not Signal-grade security. Do not use for sensitive communications.** See [Security Model](#security-model) below.
|
|
23
|
+
|
|
24
|
+
## Table of Contents
|
|
25
|
+
|
|
26
|
+
- [What It Does](#what-it-does)
|
|
27
|
+
- [Architecture](#architecture)
|
|
28
|
+
- [Usage](#usage)
|
|
29
|
+
- [The NAT Traversal Experiment](#the-nat-traversal-experiment)
|
|
30
|
+
- [Security Model](#security-model)
|
|
31
|
+
- [Repository Layout](#repository-layout)
|
|
32
|
+
- [Testing](#testing)
|
|
33
|
+
- [Bugs Found (and Fixed) Along the Way](#bugs-found-and-fixed-along-the-way)
|
|
34
|
+
- [Infrastructure Notes](#infrastructure-notes)
|
|
35
|
+
- [Roadmap (V2)](#roadmap-v2)
|
|
36
|
+
|
|
37
|
+
## What It Does
|
|
38
|
+
|
|
39
|
+
| Capability | Proof |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Two peers chat E2E-encrypted | Verified on two physical laptops over LAN, recorded on video |
|
|
42
|
+
| Identity survives restarts | Same fingerprint (`c849:a2b7...`) across every session of the build |
|
|
43
|
+
| History persists, encrypted at rest | Restart → history reloads and decrypts; raw SQL shows only ciphertext |
|
|
44
|
+
| Peer discovery by name | `find aadarsh` → endpoint + fingerprint, worked across two Indian carrier networks + Azure |
|
|
45
|
+
| Cross-network discovery over the real Internet | Rendezvous hosted on an Azure VM; peers on home Wi-Fi (Delhi) + mobile hotspot registered and looked each other up |
|
|
46
|
+
| Fingerprint verification | Both sides display fingerprints; out-of-band comparison; pre-connect verification via the registry |
|
|
47
|
+
|
|
48
|
+
## Demo
|
|
49
|
+
|
|
50
|
+
🎥 Two physical laptops chatting over LAN, disconnect → reconnect → history reloads.
|
|
51
|
+
|
|
52
|
+
| Fingerprint verification across two screens | Rendezvous log: two peers, two public NAT IPs | Punch verdict on CGNAT |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| The out-of-band fingerprint check, side by side | The registry holding two different public NAT mappings (home Wi-Fi + mobile hotspot) — discovery across the real Internet | Control pass on loopback vs. failure on real CGNAT |
|
|
55
|
+
|
|
56
|
+
*(Screenshots live in `screenshots/`.)*
|
|
57
|
+
|
|
58
|
+
## Architecture
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
┌──────────────────────┐
|
|
62
|
+
│ Rendezvous Server │
|
|
63
|
+
│ Discovery/Signaling │ (Azure VM, port 7000)
|
|
64
|
+
└──────────┬───────────┘
|
|
65
|
+
│ endpoint + fingerprint
|
|
66
|
+
↙ ↘
|
|
67
|
+
┌────────┐ ┌────────┐
|
|
68
|
+
│ Peer A │◄─►│ Peer B │ direct P2P chat (port 9999)
|
|
69
|
+
└────────┘ └────────┘
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Every layer of the transport stack is implemented, not imported:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
Application (chat, history)
|
|
76
|
+
↓
|
|
77
|
+
Peer management (listen / connect / find / punch modes)
|
|
78
|
+
↓
|
|
79
|
+
Cryptographic identity (persistent keypairs, fingerprints)
|
|
80
|
+
↓
|
|
81
|
+
PyNaCl Box (X25519 key exchange + XSalsa20-Poly1305 AEAD)
|
|
82
|
+
↓
|
|
83
|
+
Custom framing ([4-byte big-endian length][payload])
|
|
84
|
+
↓
|
|
85
|
+
TCP sockets (blocking, threaded receive)
|
|
86
|
+
↓
|
|
87
|
+
IP / NAT / firewall
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Local storage is a separate concern from transport: every message is passed through `SecretBox.encrypt` before being written to a SQLite `BLOB`, using a storage key that is distinct from the transport identity key.
|
|
91
|
+
|
|
92
|
+
## Usage
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git clone <repo> && cd p2p-chat
|
|
96
|
+
pip install pynacl
|
|
97
|
+
|
|
98
|
+
# terminal 1 — rendezvous server (or point at the deployed one)
|
|
99
|
+
python rendezvous_server.py # port 7000
|
|
100
|
+
|
|
101
|
+
# terminal 2 — peer A, discoverable by name
|
|
102
|
+
python peer.py listen 9999 aadarsh <rv_ip>
|
|
103
|
+
|
|
104
|
+
# terminal 3 — peer B finds and connects
|
|
105
|
+
python peer.py find aadarsh <rv_ip>
|
|
106
|
+
python peer.py connect <aadarsh-ip> 9999 ishu
|
|
107
|
+
|
|
108
|
+
# NAT traversal attempt (coordinated simultaneous open)
|
|
109
|
+
python peer.py punch <my_id> <peer_id> <my_port> <rv_ip>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Once connected, both sides display their fingerprints. Compare them out-of-band (e.g. a phone call) before trusting the session. Type `quit` to exit — history reloads automatically the next time you connect to the same peer.
|
|
113
|
+
|
|
114
|
+
## The NAT Traversal Experiment
|
|
115
|
+
|
|
116
|
+
This is the centerpiece of the V1 build: a real, measured attempt at direct P2P connectivity across the open Internet — not a simulated or hand-waved result.
|
|
117
|
+
|
|
118
|
+
**Setup:** Rendezvous server on an Azure VM (Central India). Peer A on home Wi-Fi (`192.168.1.8` local, NAT public `122.162.151.183`). Peer B on a phone hotspot (`10.197.183.135` local behind carrier-grade NAT, NAT public `157.49.119.123`).
|
|
119
|
+
|
|
120
|
+
| Test | Result |
|
|
121
|
+
|---|---|
|
|
122
|
+
| Baseline: uncoordinated direct dial across the Internet | ❌ `WinError 10060` timeout (~20s) — NAT drops the unsolicited inbound SYN |
|
|
123
|
+
| Control: simultaneous open on loopback | ✅ Punched through — hole-punching mechanics proven correct |
|
|
124
|
+
| Simultaneous open, home NAT ↔ Airtel CGNAT, 60s | ❌ Neither NAT delivered the peer's SYNs |
|
|
125
|
+
|
|
126
|
+
**Conclusion:** the code is correct (the loopback control passes); the network refuses. Carrier-grade NAT shares one public IP across thousands of subscribers with endpoint-dependent mapping, which direct TCP hole punching cannot reliably defeat. This is a measured failure, explained at the NAT level, not a bug.
|
|
127
|
+
|
|
128
|
+
**Fallback strategy (documented, V2):** a relay forwards already-encrypted traffic. The rendezvous/relay server never holds keys — by construction, it doesn't even have PyNaCl installed, so the "phone book" literally cannot read messages even if it wanted to.
|
|
129
|
+
|
|
130
|
+
## Security Model
|
|
131
|
+
|
|
132
|
+
**Protects against:**
|
|
133
|
+
- Passive network observers (end-to-end AEAD encryption)
|
|
134
|
+
- Peer impersonation across reconnects (persistent identities + fingerprint verification)
|
|
135
|
+
- Plaintext history on disk (SQLite holds only ciphertext)
|
|
136
|
+
- IP changes redefining identity (history is keyed by fingerprint, not IP)
|
|
137
|
+
|
|
138
|
+
**Does NOT protect against:**
|
|
139
|
+
- **No forward secrecy** — static-static ECDH means a later key compromise exposes previously recorded traffic; there is no ratchet
|
|
140
|
+
- Unencrypted key files at rest
|
|
141
|
+
- Replay attacks (no counters or nonces yet)
|
|
142
|
+
- A malicious rendezvous server serving the wrong endpoint (fingerprint comparison mitigates this, but Trust-On-First-Use doesn't eliminate it)
|
|
143
|
+
- Traffic analysis / metadata leakage
|
|
144
|
+
- Machine compromise
|
|
145
|
+
- `peer_id` squatting — names aren't yet bound to keys (V2 pins names to the first-registered key)
|
|
146
|
+
- Implementation bugs not yet found
|
|
147
|
+
|
|
148
|
+
This is an educational project. **Do not use it for sensitive real-world communications.**
|
|
149
|
+
|
|
150
|
+
## Repository Layout
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
p2p-chat/
|
|
154
|
+
├── peer.py # unified peer: listen | connect | find | punch
|
|
155
|
+
├── protocol.py # length-prefixed framing (OSError-as-EOF contract)
|
|
156
|
+
├── identity.py # persistent transport + storage keys
|
|
157
|
+
├── storage.py # encrypted SQLite history
|
|
158
|
+
├── rendezvous_server.py # discovery/signaling (port 7000, TTL 90s)
|
|
159
|
+
├── crypto_test.py # manual crypto sanity check
|
|
160
|
+
├── tests/ # 19 pytest tests
|
|
161
|
+
├── project.md # living engineering doc (milestones, threat model)
|
|
162
|
+
├── README.md # this file
|
|
163
|
+
└── .gitignore # *.bin *.db *.pem __pycache__/ .pytest_cache/
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Testing
|
|
167
|
+
|
|
168
|
+
19 automated tests across 3 files, running in ~0.53s:
|
|
169
|
+
|
|
170
|
+
- **`test_protocol.py` (8 tests):** roundtrips (small/empty/binary-256), back-to-back frames in one burst, fragmented delivery (header split mid-stream), 1 MiB through a threaded reader, clean-FIN → `None`, RST → `None` (regression for the M13 shutdown bug, reproduced via `SO_LINGER(0)`)
|
|
171
|
+
- **`test_storage.py` (6 tests):** roundtrip, insertion order (locks `ORDER BY id`), per-fingerprint isolation, message direction, ciphertext-at-rest (reads the raw SQL like an attacker would), wrong key → `CryptoError` (regression for the M12 key-orphaning incident)
|
|
172
|
+
- **`test_identity.py` (5 tests):** key persistence, distinct identities, fingerprint stability across reloads, fingerprint format contract (16×4 hex groups; stripping colons = raw SHA-256), `SecretBox` key size
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
python -m pytest tests/ -v
|
|
176
|
+
# 19 passed in ~0.5s
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Bugs Found (and Fixed) Along the Way
|
|
180
|
+
|
|
181
|
+
Each of these was caught by the test suite or a real run, and each is now guarded against:
|
|
182
|
+
|
|
183
|
+
- **`Peer.py` vs `peer.py`** — Windows case-insensitivity masked this; caught on the first test run and would have broken on Linux/macOS
|
|
184
|
+
- **Windows `Ctrl+C` sends RST, not FIN** — the receive thread crashed with `ConnectionResetError`; fixed by treating `OSError` as EOF everywhere in `recv_message`, giving one consistent failure convention
|
|
185
|
+
- **Daemon thread killed mid-print at shutdown** (`Fatal Python error: _enter_buffered_busy`) — fixed by joining the receiver thread with a 2s timeout before exit
|
|
186
|
+
- **Phantom "Peer disconnected" on your own `quit`** — fixed with an `if connected:` guard
|
|
187
|
+
- **Storage-key rename orphaned encrypted history** — surfaces as a loud `CryptoError` by design; now locked by a regression test
|
|
188
|
+
- **`ORDER BY timestamp` scrambled messages at second-granularity** — switched to `ORDER BY id`
|
|
189
|
+
- **Fingerprint gate was exact-match and rejected `"y"`** — fixed with an explicit allowlist (`yes`/`y`); anything else still fails closed
|
|
190
|
+
- **Punch-mode split-brain connections** — fixed with identity-proof at accept: a connection must present the public key matching the registry fingerprint, or it's dropped
|
|
191
|
+
- **Windows failed-socket reuse** — fixed by using a fresh socket per punch attempt
|
|
192
|
+
- **`get_lan_ip()` originally used a TCP socket** — it would open a real connection to `8.8.8.8:80` (hanging ~20s when unreachable) instead of doing a route lookup — replaced with a UDP socket, which never sends a packet
|
|
193
|
+
- **Rendezvous lookup race** — fixed with a 20s retry loop so start order no longer matters
|
|
194
|
+
- **Python block-buffers stdout when redirected**, causing empty logs on an otherwise healthy server — fixed by running with `python3 -u`
|
|
195
|
+
|
|
196
|
+
## Infrastructure Notes
|
|
197
|
+
|
|
198
|
+
Real cloud/ops lessons from deploying the rendezvous server:
|
|
199
|
+
|
|
200
|
+
- **Azure for Students** has region allowlist restrictions (`RequestDisallowedByAzure`) and a B-series vCPU quota of 0 in some regions; worked around with a `B2ats_v2` instance (~$0.0062/hr). No auto-shutdown was available in-region, so a manual portal-stop discipline plus a $15 budget alert was used instead.
|
|
201
|
+
- Two independent firewall layers must both be opened for the server to be reachable: the cloud Network Security Group **and** the OS-level `ufw`.
|
|
202
|
+
- A VM restart kills running processes but not files, so the server is started with `nohup python3 -u rendezvous_server.py &` every time; `pgrep` is checked first to avoid stale double instances (which caused a confusing false failure once).
|
|
203
|
+
- MinTTY/Git Bash swallows `Ctrl+C` for console-less programs — use `winpty`.
|
|
204
|
+
- The VM's public IP can change on deallocate, so it's always re-read from the Azure portal rather than assumed.
|
|
205
|
+
- The rendezvous server records the **observed** source IP of a connecting peer (which can't be lied to) alongside the **peer-advertised** listen port (which it has no way to independently verify). Registering via `127.0.0.1` poisons the registry — the same observed-vs-advertised mechanism is what later reveals real NAT public mappings.
|
|
206
|
+
|
|
207
|
+
## Roadmap (V2)
|
|
208
|
+
|
|
209
|
+
- **Relay implementation** — the fallback for when punching fails; already-encrypted traffic is relayed, and the rendezvous server is upgraded to hand out relay info. This is what makes cross-Internet chat work reliably, not just when NAT cooperates.
|
|
210
|
+
- **Packaging** — a `pyproject.toml` and a PyPI release, so usage becomes `pip install <name>` followed by `p2pchat listen --name aadarsh` / `p2pchat connect aadarsh`, with no files, ports, or internals exposed to the end user.
|
|
211
|
+
- **Auto-fallback chain** — direct → punch → relay, orchestrated by a connection state machine.
|
|
212
|
+
- **Name pinning** — first registration binds a `peer_id` to a public key, closing the squatting gap.
|
|
213
|
+
- **Hardening** — `MAX_MESSAGE_SIZE`, replay counters, heartbeats/timeouts, and key-file permissions.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
*Stack: Python 3.13, stdlib `socket`/`threading`/`struct`/`sqlite3`/`json`, PyNaCl (libsodium bindings), pytest. No frameworks. ~5 core modules + rendezvous server, 19 automated tests, 15 milestones, ~4 weeks of build time.*
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
identity.py
|
|
3
|
+
peer.py
|
|
4
|
+
protocol.py
|
|
5
|
+
pyproject.toml
|
|
6
|
+
relay_server.py
|
|
7
|
+
rendezvous_server.py
|
|
8
|
+
storage.py
|
|
9
|
+
glypha.egg-info/PKG-INFO
|
|
10
|
+
glypha.egg-info/SOURCES.txt
|
|
11
|
+
glypha.egg-info/dependency_links.txt
|
|
12
|
+
glypha.egg-info/entry_points.txt
|
|
13
|
+
glypha.egg-info/requires.txt
|
|
14
|
+
glypha.egg-info/top_level.txt
|
|
15
|
+
tests/test_identity.py
|
|
16
|
+
tests/test_protocol.py
|
|
17
|
+
tests/test_storage.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|