From 5dcaff26cfcf1195a493363b69dbe157fa47db13 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 17 Jun 2026 21:35:15 +0300 Subject: [PATCH 01/11] Add svgs and generator python scripts --- public/blog/how-iroh-works/connect-by-key.svg | 106 +++++ .../blog/how-iroh-works/embedding-phone.svg | 120 ++++++ .../blog/how-iroh-works/endpoint-startup.svg | 136 ++++++ public/blog/how-iroh-works/hole-punching.svg | 253 +++++++++++ .../blog/how-iroh-works/publish-relay-dht.svg | 144 +++++++ public/blog/how-iroh-works/publish-relay.svg | 119 +++++ public/blog/how-iroh-works/routing-moves.svg | 202 +++++++++ scripts/endpoint-startup.gen.py | 228 ++++++++++ scripts/endpoint-startup.land.txt | 1 + scripts/hole-punching.gen.py | 407 ++++++++++++++++++ scripts/publish-relay-dht.gen.py | 247 +++++++++++ scripts/publish-relay.gen.py | 222 ++++++++++ src/app/blog/how-iroh-works/page.mdx | 149 +++++++ 13 files changed, 2334 insertions(+) create mode 100644 public/blog/how-iroh-works/connect-by-key.svg create mode 100644 public/blog/how-iroh-works/embedding-phone.svg create mode 100644 public/blog/how-iroh-works/endpoint-startup.svg create mode 100644 public/blog/how-iroh-works/hole-punching.svg create mode 100644 public/blog/how-iroh-works/publish-relay-dht.svg create mode 100644 public/blog/how-iroh-works/publish-relay.svg create mode 100644 public/blog/how-iroh-works/routing-moves.svg create mode 100644 scripts/endpoint-startup.gen.py create mode 100644 scripts/endpoint-startup.land.txt create mode 100644 scripts/hole-punching.gen.py create mode 100644 scripts/publish-relay-dht.gen.py create mode 100644 scripts/publish-relay.gen.py create mode 100644 src/app/blog/how-iroh-works/page.mdx diff --git a/public/blog/how-iroh-works/connect-by-key.svg b/public/blog/how-iroh-works/connect-by-key.svg new file mode 100644 index 00000000..04f8ad14 --- /dev/null +++ b/public/blog/how-iroh-works/connect-by-key.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + 10.0.0.42 + + 192.168.1.7 + + + 203.0.113.15 + + + 172.20.4.88 + + + 192.168.10.55 + + + + + + iroh + πŸ”‘ a4f7c0… + + + + + + + iroh + πŸ”‘ 8e2b1d… + + + Alice + Bob + + + + + connect + + + + + diff --git a/public/blog/how-iroh-works/embedding-phone.svg b/public/blog/how-iroh-works/embedding-phone.svg new file mode 100644 index 00000000..dc950c24 --- /dev/null +++ b/public/blog/how-iroh-works/embedding-phone.svg @@ -0,0 +1,120 @@ + + + + iOS app (Swift) +iroh via iroh-ffi + + + + swift app + + iroh + + + + + Android app (Kotlin) +iroh via iroh-ffi + + + + + kotlin app + + iroh + + + + + Native desktop (C++) +iroh via iroh-c-ffi + + + + c++ app + + iroh + + + + + + + Web app (JavaScript) +iroh compiled to WASM + + + + + + + + + js app + + iroh + + + + + Server daemon (Rust) +iroh crate directly + + + + + + + + + + + + rust daemon + + iroh + + + + + Embedded firmware (Rust) +iroh crate + + + + + + + + + + + + + + + + + + firmware + + iroh + + diff --git a/public/blog/how-iroh-works/endpoint-startup.svg b/public/blog/how-iroh-works/endpoint-startup.svg new file mode 100644 index 00000000..f3fcc4da --- /dev/null +++ b/public/blog/how-iroh-works/endpoint-startup.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + us-west + + + + + + + + + + + + 52.10.18.7 + + + + + us-east + + + + + + + + + + + + 44.208.61.5 + + + + + eu-west + + + + + + + + + + + + 18.196.142.9 + + + + + + + + + + + + + + + iroh + πŸ”‘ 8e2b… + + + + 73.118.42.9 + + + + + + + + + + us-east: 19 ms + us-west: 71 ms + eu-west: 102 ms + + + + + diff --git a/public/blog/how-iroh-works/hole-punching.svg b/public/blog/how-iroh-works/hole-punching.svg new file mode 100644 index 00000000..2689e601 --- /dev/null +++ b/public/blog/how-iroh-works/hole-punching.svg @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + relay + + + + 8.3.1.9 + + + + + + + 10.0.0.1 + + + + 4.9.8.2 + + + + + + + 192.168.0.1 + + + + + + + + + iroh + πŸ”‘ 1a9c… + Alice + + + EndpointId: 1a9c… + Addr: 10.0.0.3:2104 + Addr: 8.3.1.9:2104 + relay: us-west + + + + + + + + + iroh + πŸ”‘ 8e2b… + Bob + + + EndpointId: 8e2b… + Addr: 192.168.0.3:4153 + Addr: 4.9.8.2:4153 + relay: us-east + + + + + + + + + + + + ADD_ADDRESS 192.168.0.3:4153 + ADD_ADDRESS 4.9.8.2:4153 + + + + + + + + + Bob + Candidate: 192.168.0.3:4153 + Candidate: 4.9.8.2:4153 + + + + + + + + + + + + REACH_OUT10.0.0.3:2104 + REACH_OUT8.3.1.9:2104 + + + + + + + + + Alice + Candidate: 10.0.0.3:2104 + Candidate: 8.3.1.9:2104 + I need to reach out + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + not routable + + + + + + not routable + + + + + + no mapping + + + + + + + + 4.9.8.2:4153 β†’ + 10.0.0.3:2104 + + + + + + + + + + + + + + + + + + + 8.3.1.9:2104 β†’ + 192.168.0.3:4153 + + + + + + + + + + + + + + + + PATH_CHALLENGE + 192.168.0.3:4153 + + + + + PATH_CHALLENGE + 10.0.0.3:2104 + + + + + PATH_CHALLENGE + 4.9.8.2:4153 + + + + + PATH_CHALLENGE + 8.3.1.9:2104 + + + + + PATH_RESPONSE + PATH_CHALLENGE + + + + + PATH_RESPONSE + + diff --git a/public/blog/how-iroh-works/publish-relay-dht.svg b/public/blog/how-iroh-works/publish-relay-dht.svg new file mode 100644 index 00000000..4177fb60 --- /dev/null +++ b/public/blog/how-iroh-works/publish-relay-dht.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + Mainline DHT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + iroh + πŸ”‘ 8e2b… + Bob + + + NodeId: 8e2b… + Home relay: us-east + + + + + + + + + + iroh + πŸ”‘ 1a9c… + Alice + + 8e2b… is at relay us-east + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DHT put + 8e2b… + Relay: us-east + + + + + + + DHT get + 8e2b… + + + + + + + ;; ANSWER SECTION: + TXT "relay=https://us-east" + + diff --git a/public/blog/how-iroh-works/publish-relay.svg b/public/blog/how-iroh-works/publish-relay.svg new file mode 100644 index 00000000..be17cad1 --- /dev/null +++ b/public/blog/how-iroh-works/publish-relay.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dns.iroh.link + + + + + 1f3a… β†’ eu-west + 7c0e… β†’ us-west + b48d… β†’ us-east + 2a91… β†’ eu-west + 8e2b… β†’ us-east + + + + + + + + + iroh + πŸ”‘ 8e2b… + Bob + + + NodeId: 8e2b… + Home relay: us-east + + + + + + + + + + iroh + πŸ”‘ 1a9c… + Alice + + + 8e2b… is at relay us-east + + + + + + + + + + + + + + + + + HTTPS PUT + Relay: us-east + Signed by: 8e2b… + + + + + + + + + DNS LOOKUP + TXT _iroh.8e2b….dns.iroh.link + + + + ;; ANSWER SECTION: + TXT "relay=https://us-east" + + + diff --git a/public/blog/how-iroh-works/routing-moves.svg b/public/blog/how-iroh-works/routing-moves.svg new file mode 100644 index 00000000..9efbfee7 --- /dev/null +++ b/public/blog/how-iroh-works/routing-moves.svg @@ -0,0 +1,202 @@ + + + + + + + + + + + + 10.0.0.42 + + + + iroh + πŸ”‘ a4f7c0… + Alice + + + + + 10.0.0.7 + 192.168.1.7 + 172.16.0.7 + + + + + iroh + πŸ”‘ 8e2b1d… + Bob + + + + + + + + + + + + + + + + + + + + + home router + + 203.0.113.1 + + + + + + + 10.0.0.1 + + + + + mobile network + + 198.51.100.1 + + + + + + + 192.168.1.1 + + + + + satellite internet + + 100.64.10.1 + + + + 172.16.0.1 + + diff --git a/scripts/endpoint-startup.gen.py b/scripts/endpoint-startup.gen.py new file mode 100644 index 00000000..d9b07eb9 --- /dev/null +++ b/scripts/endpoint-startup.gen.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""Generator for endpoint-startup.svg (the "Endpoint startup" figure in how-iroh-works). + +This SVG is generated, not hand-edited. To change it, edit this script and run: + + python3 endpoint-startup.gen.py + +It reads the projected land outline from endpoint-startup.land.txt (Natural Earth +110m coastline, plate carrΓ©e with standard parallel 45Β°, clipped to a US + Europe +window) and writes ../public/blog/how-iroh-works/endpoint-startup.svg. + +NOTE: this lives in scripts/, not in src/app/, because anything under the Next.js +app router directory gets bundled by webpack (which chokes on .py files). + +The animation: the three relay wires fade in at 2s, then an equal-speed probe +packet round-trips Bob -> relay -> Bob on each wire. Because speed is constant and +distance differs, us-east returns first, then us-west, then eu-west. A readout to +the right of the phone gains a line each time a packet returns. +""" + +import math +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +LAND_PATH = os.path.join(HERE, "endpoint-startup.land.txt") +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", + "public", "blog", "how-iroh-works", "endpoint-startup.svg")) + +land = open(LAND_PATH).read() + +# ---- projection: plate carrΓ©e, standard parallel 45Β°, US + Europe window ---- +lon0, lon1 = -130, 42 +lat0, lat1 = 16, 72 +cosp = math.cos(math.radians(45)) +scale = 820.0 / ((lon1 - lon0) * cosp) +def proj(lon, lat): + return ((lon - lon0) * cosp * scale, (lat1 - lat) * scale) + +# city coordinates (lon, lat) +seattle = (-122.33, 47.61) +delaware = (-75.55, 39.16) # Dover/Wilmington area +frankfurt = (8.68, 50.11) +florida = (-80.2, 25.8) # Miami area + + +def relay(cx, cy, label, ip): + # pizzabox 1U server icon centered horizontally on cx, sitting above the location dot + bx, by, bw, bh = cx-23, cy-30, 46, 15 + s = [] + s.append(f' ') + s.append(f' ') + s.append(f' {label}') + # connector from box bottom to dot + s.append(f' ') + # location dot + s.append(f' ') + # pizzabox body + s.append(f' ') + # front bezel split line + s.append(f' ') + # LEDs + s.append(f' ') + s.append(f' ') + # vent slits on the right + for i in range(5): + vx = bx+22+i*4 + s.append(f' ') + # public IPv4 below the location dot + s.append(f' {ip}') + s.append(f' ') + return "\n".join(s) + + +def arc(x1, y1, x2, y2, k=0.16): + # great-circle-style curved wire: quadratic bezier bowing "up" (toward the top). + # Returns (path_d, arc_length). Larger k => more northern bow. + mx, my = (x1+x2)/2, (y1+y2)/2 + dx, dy = x2-x1, y2-y1 + L = math.hypot(dx, dy) + px, py = -dy/L, dx/L + if py > 0: # always bow upward + px, py = -px, -py + off = k*L + cx, cy = mx+px*off, my+py*off + d = f"M {x1:.0f} {y1:.0f} Q {cx:.0f} {cy:.0f} {x2:.0f} {y2:.0f}" + def q(t, a, b, c): return (1-t)**2*a + 2*(1-t)*t*b + t**2*c + N, length, prev = 60, 0.0, (x1, y1) + for i in range(1, N+1): + t = i/N + cur = (q(t, x1, cx, x2), q(t, y1, cy, y2)) + length += math.hypot(cur[0]-prev[0], cur[1]-prev[1]) + prev = cur + return d, length + + +# ---- Bob: small Android phone in the Atlantic, leader line to his FL location ---- +bx0, by0 = 300, 244 # phone top-left +bw, bh = 48, 88 +bcx, bcy = bx0+bw/2, by0+bh/2 +flx, fly = proj(*florida) +bob = f''' + + + + + + + + + + + + iroh + πŸ”‘ 8e2b… + ''' + +uwx, uwy = round(proj(*seattle)[0]), round(proj(*seattle)[1]) +uex, uey = round(proj(*delaware)[0]), round(proj(*delaware)[1]) +ewx, ewy = round(proj(*frankfurt)[0]), round(proj(*frankfurt)[1]) +bx_, by_ = round(flx), round(fly) # Bob's location dot + +# made-up public IPv4 addresses for the relays +ip_uw, ip_ue, ip_ew = "52.10.18.7", "44.208.61.5", "18.196.142.9" +# Bob's public IP β€” reflected back by the first relay to respond +ip_bob = "73.118.42.9" + +# wire paths (us-east kept nearly straight); keep lengths for equal-speed packets +d_uw, L_uw = arc(bx_, by_, uwx, uwy, k=0.16) +d_ue, L_ue = arc(bx_, by_, uex, uey, k=0.05) +d_ew, L_ew = arc(bx_, by_, ewx, ewy, k=0.10) + +wires = f''' + + + + + ''' + +# Equal-speed probe packets: round-trip Bob -> relay -> Bob. +# Duration is proportional to path length so speed is constant => us-east returns first. +SPEED = 120.0 # px per second (slow enough that the short us-east hop is still watchable) +START = 3.0 # packets launch after the wires have faded in +def packet(wid, length): + dur = 2*length/SPEED # there and back + return f''' + + + + + ''' + +packets = f''' + +{packet("w-uw", L_uw)} +{packet("w-ue", L_ue)} +{packet("w-ew", L_ew)} + ''' + +# Latency readout, right of the phone: one line appears each time a packet returns. +def ret_time(length): + return START + 2*length/SPEED # packet is back at Bob at START + round-trip dur +def readline(y, label, ms, begin): + return (f' ' + f'{label}: {ms} ms' + f'') +readout = f''' + + +{readline(268, "us-east", 19, ret_time(L_ue))} +{readline(294, "us-west", 71, ret_time(L_uw))} +{readline(320, "eu-west", 102, ret_time(L_ew))} + + + + ''' + +# When the last response is in, us-east wins: overlay its wire in connection-blue, +# then keep it gently pulsing to show the persistent home-relay connection. +home = f''' + + + + ''' + +# The endpoint's public IP appears just below the phone on the first response. +phone_ip = f''' + {ip_bob}''' + +# Cropped viewport: trim the empty Arctic (top) and eastern Europe (right) so the +# relevant US <-> western-Europe band fills the frame. The land path is unchanged; +# this just clips it. Keep the MDX
aspectRatio in sync with VB_W / VB_H. +VB_X, VB_Y, VB_W, VB_H = 0, 92, 705, 280 +svg = f''' + + + + + + + +{wires} + +{packets} + +{relay(uwx, uwy, "us-west", ip_uw)} + +{relay(uex, uey, "us-east", ip_ue)} + +{relay(ewx, ewy, "eu-west", ip_ew)} + +{bob} + +{phone_ip} + +{home} + +{readout} + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/endpoint-startup.land.txt b/scripts/endpoint-startup.land.txt new file mode 100644 index 00000000..a746ad2e --- /dev/null +++ b/scripts/endpoint-startup.land.txt @@ -0,0 +1 @@ +M 329,417 L 324,417 L 326,416 L 326,413 L 329,412 L 329,417 Z M 307,363 L 306,364 L 299,364 L 299,362 L 300,361 L 304,361 L 307,363 Z M 253,365 L 252,366 L 249,365 L 246,363 L 247,361 L 253,361 L 256,365 L 253,365 Z M 274,351 L 278,353 L 278,351 L 282,351 L 286,353 L 287,355 L 290,355 L 290,357 L 292,357 L 294,360 L 292,363 L 290,361 L 286,361 L 284,363 L 283,361 L 281,362 L 279,367 L 278,364 L 275,363 L 270,363 L 267,364 L 265,362 L 265,360 L 273,361 L 275,360 L 273,357 L 273,354 L 270,353 L 271,351 L 274,351 Z M 240,332 L 242,334 L 246,334 L 255,342 L 259,344 L 259,346 L 263,346 L 266,349 L 266,350 L 249,352 L 252,348 L 250,346 L 247,346 L 244,340 L 242,340 L 236,337 L 230,336 L 228,334 L 230,333 L 225,332 L 222,336 L 220,336 L 219,338 L 215,338 L 220,332 L 228,329 L 235,330 L 240,332 Z M 250,325 L 249,326 L 246,320 L 247,315 L 248,316 L 250,325 Z M 249,306 L 244,307 L 243,305 L 249,304 L 249,306 Z M 253,306 L 252,311 L 249,303 L 253,306 Z M 785,245 L 781,248 L 782,250 L 777,252 L 775,251 L 774,249 L 785,245 Z M 733,245 L 735,247 L 743,247 L 743,248 L 745,247 L 745,249 L 738,250 L 738,249 L 732,248 L 733,245 Z M 694,228 L 692,239 L 679,232 L 680,228 L 685,229 L 694,228 Z M 664,208 L 667,212 L 666,221 L 664,221 L 662,223 L 660,221 L 659,209 L 661,210 L 664,208 Z M 665,201 L 664,206 L 662,205 L 661,201 L 661,198 L 665,195 L 665,201 Z M 316,172 L 324,172 L 320,176 L 314,173 L 313,170 L 315,168 L 316,172 Z M 325,154 L 317,152 L 312,149 L 320,150 L 325,153 L 325,154 Z M 31,158 L 29,159 L 21,156 L 14,150 L 9,148 L 7,145 L 8,143 L 20,146 L 24,152 L 29,155 L 31,158 Z M 352,144 L 349,150 L 352,147 L 355,149 L 354,151 L 358,153 L 360,151 L 365,153 L 363,158 L 367,157 L 369,165 L 367,171 L 361,170 L 363,164 L 361,163 L 356,169 L 353,169 L 356,166 L 352,164 L 337,164 L 336,162 L 339,160 L 337,158 L 341,154 L 346,143 L 349,140 L 353,137 L 356,138 L 352,144 Z M -13,121 L -8,121 L -10,128 L -6,134 L -8,134 L -15,125 L -15,120 L -13,121 Z M 587,133 L 579,137 L 572,136 L 576,129 L 574,122 L 584,114 L 588,113 L 593,118 L 590,122 L 591,127 L 587,133 Z M 680,111 L 677,116 L 672,112 L 672,109 L 679,107 L 680,111 Z M 605,90 L 600,97 L 610,97 L 609,102 L 605,108 L 610,108 L 614,117 L 618,118 L 622,129 L 628,130 L 627,134 L 625,136 L 627,140 L 622,143 L 616,143 L 608,145 L 606,144 L 603,147 L 598,146 L 595,149 L 592,147 L 599,140 L 603,139 L 596,138 L 595,135 L 600,133 L 597,129 L 598,125 L 605,125 L 606,121 L 602,117 L 597,116 L 596,114 L 597,111 L 596,109 L 593,113 L 593,106 L 590,103 L 592,96 L 596,90 L 605,90 Z M 242,66 L 240,70 L 238,69 L 237,67 L 239,65 L 242,66 Z M 229,63 L 224,66 L 220,66 L 219,64 L 223,61 L 229,61 L 229,63 Z M 214,43 L 215,46 L 217,45 L 231,51 L 231,54 L 234,54 L 238,56 L 234,58 L 226,56 L 224,53 L 212,60 L 210,56 L 204,57 L 208,54 L 210,42 L 214,43 Z M 551,37 L 549,42 L 555,46 L 549,51 L 531,57 L 511,54 L 516,51 L 506,48 L 514,47 L 514,45 L 504,43 L 507,39 L 514,38 L 522,42 L 529,39 L 535,40 L 543,37 L 551,37 Z M 258,33 L 253,33 L 252,30 L 254,26 L 258,25 L 262,27 L 261,31 L 258,33 Z M 164,20 L 161,22 L 154,20 L 150,21 L 144,18 L 152,13 L 159,16 L 164,20 Z M 188,17 L 188,24 L 194,18 L 200,23 L 199,28 L 203,32 L 208,27 L 212,22 L 212,14 L 226,16 L 232,19 L 233,22 L 229,26 L 232,30 L 232,33 L 222,38 L 216,39 L 211,37 L 203,49 L 198,53 L 191,54 L 187,57 L 187,61 L 181,62 L 176,67 L 170,75 L 169,80 L 168,88 L 175,89 L 180,101 L 186,99 L 195,102 L 203,108 L 214,113 L 228,114 L 227,119 L 228,126 L 232,134 L 239,140 L 242,138 L 245,131 L 243,120 L 239,117 L 247,114 L 252,109 L 255,104 L 254,100 L 251,94 L 245,89 L 251,82 L 247,65 L 251,64 L 264,66 L 268,64 L 278,71 L 279,73 L 288,74 L 289,88 L 294,89 L 297,93 L 304,89 L 312,79 L 327,101 L 325,106 L 336,113 L 343,115 L 346,117 L 348,123 L 352,124 L 354,126 L 354,134 L 347,139 L 340,141 L 334,147 L 326,148 L 315,146 L 303,147 L 299,152 L 293,155 L 281,170 L 285,169 L 292,160 L 302,154 L 310,153 L 314,157 L 309,161 L 312,174 L 319,177 L 326,176 L 331,169 L 332,173 L 335,176 L 329,180 L 313,187 L 308,192 L 305,191 L 304,186 L 313,180 L 300,181 L 301,183 L 285,191 L 282,196 L 282,200 L 284,204 L 286,204 L 285,201 L 287,203 L 286,205 L 272,208 L 268,209 L 275,208 L 277,209 L 270,212 L 267,212 L 267,211 L 266,213 L 267,213 L 266,218 L 263,223 L 260,219 L 262,227 L 258,235 L 259,230 L 256,227 L 256,221 L 255,224 L 256,229 L 253,228 L 256,230 L 256,236 L 258,237 L 259,246 L 256,251 L 251,253 L 248,257 L 245,257 L 243,260 L 242,262 L 237,266 L 232,273 L 232,283 L 238,304 L 237,315 L 235,316 L 233,316 L 232,313 L 230,311 L 225,297 L 226,293 L 224,289 L 219,283 L 214,286 L 208,280 L 195,281 L 193,282 L 194,288 L 190,289 L 187,289 L 183,285 L 179,286 L 175,285 L 172,285 L 168,287 L 164,292 L 159,295 L 156,301 L 157,311 L 154,322 L 153,334 L 156,346 L 161,355 L 163,358 L 168,360 L 170,363 L 181,359 L 187,355 L 189,344 L 198,341 L 205,340 L 206,342 L 206,345 L 202,353 L 203,354 L 201,362 L 199,361 L 200,362 L 199,374 L 196,378 L 200,380 L 201,378 L 205,379 L 210,378 L 215,378 L 222,382 L 223,384 L 223,389 L 222,394 L 222,402 L 220,411 L 228,425 L 230,425 L 232,426 L 239,423 L 240,421 L 246,422 L 251,427 L 253,427 L 257,422 L 259,422 L 260,414 L 263,411 L 266,411 L 266,409 L 270,410 L 278,402 L 281,404 L 280,408 L 277,408 L 278,411 L 278,415 L 276,419 L 278,424 L 280,424 L 281,419 L 280,417 L 279,411 L 285,409 L 285,406 L 286,403 L 288,408 L 291,408 L 294,412 L 295,414 L 304,414 L 307,417 L 310,417 L 313,415 L 313,414 L 325,413 L 321,415 L 322,418 L 326,419 L 330,422 L 331,428 L 333,427 L 338,432 L 341,436 L 341,439 L 343,440 L 347,445 L 353,447 L 354,445 L 357,445 L 363,447 L 368,449 L 373,455 L 373,457 L 375,457 L 379,473 L 382,474 L 382,478 L 378,484 L 380,486 L 388,487 L 388,494 L 392,489 L 406,496 L 408,500 L 407,504 L 413,502 L 422,505 L 429,505 L 436,510 L 442,518 L 446,520 L 450,520 L 452,522 L 454,535 L 452,546 L 443,560 L 437,573 L 435,573 L 434,578 L 434,591 L 433,606 L 431,609 L 430,618 L 425,626 L 425,633 L 421,636 L 420,640 L 414,640 L 407,643 L 404,646 L 398,648 L 393,653 L 389,660 L 389,669 L 387,679 L 383,682 L 378,694 L 371,703 L 368,709 L 363,717 L 358,721 L 354,720 L 352,720 L 347,718 L 344,718 L 341,714 L 341,718 L 347,723 L 346,728 L 349,731 L 349,734 L 344,743 L 337,746 L 328,748 L 323,747 L 323,760 L 321,762 L 316,763 L 311,761 L 309,762 L 310,769 L 313,771 L 316,769 L 317,772 L 313,774 L 309,779 L 307,789 L 303,789 L 299,793 L 298,798 L 302,803 L 307,804 L 305,810 L 300,814 L 296,822 L 292,824 L 290,827 L 292,834 L 295,838 L 289,838 L 282,842 L 281,848 L 274,846 L 262,838 L 261,834 L 262,830 L 260,825 L 259,814 L 261,807 L 266,802 L 259,800 L 264,794 L 265,783 L 271,785 L 273,771 L 270,769 L 268,778 L 265,777 L 269,755 L 271,750 L 269,736 L 271,736 L 279,704 L 278,694 L 280,688 L 279,680 L 282,672 L 286,630 L 285,619 L 284,609 L 279,605 L 279,603 L 270,596 L 257,584 L 255,579 L 256,577 L 240,534 L 232,527 L 234,524 L 232,517 L 233,513 L 239,503 L 238,500 L 237,504 L 234,501 L 235,499 L 234,493 L 236,492 L 238,483 L 238,480 L 244,476 L 243,474 L 245,474 L 246,468 L 248,467 L 252,459 L 250,458 L 251,454 L 250,440 L 247,435 L 246,431 L 247,429 L 243,425 L 240,425 L 236,431 L 238,435 L 234,437 L 233,433 L 233,434 L 231,433 L 230,431 L 225,430 L 225,431 L 222,428 L 221,424 L 216,421 L 215,417 L 214,421 L 211,418 L 211,413 L 210,412 L 211,411 L 202,398 L 202,397 L 203,398 L 203,396 L 201,395 L 201,397 L 198,397 L 188,392 L 185,392 L 180,387 L 175,380 L 168,376 L 159,380 L 139,370 L 134,365 L 126,362 L 124,359 L 119,355 L 117,351 L 116,348 L 117,347 L 118,341 L 114,332 L 103,316 L 99,313 L 98,311 L 99,307 L 93,302 L 92,298 L 90,297 L 85,290 L 80,275 L 73,271 L 72,274 L 73,282 L 88,306 L 92,322 L 95,322 L 98,328 L 96,332 L 91,324 L 85,319 L 84,310 L 79,305 L 78,306 L 74,302 L 71,299 L 74,298 L 75,296 L 76,293 L 69,286 L 61,263 L 57,259 L 55,258 L 55,256 L 45,252 L 44,248 L 40,242 L 36,231 L 30,223 L 29,217 L 27,214 L 28,202 L 26,197 L 28,191 L 29,179 L 28,169 L 25,161 L 26,159 L 33,162 L 35,168 L 37,166 L 34,155 L 21,146 L 12,143 L 10,137 L 10,133 L 4,130 L 3,124 L -2,119 L -3,116 L -9,111 L -11,105 L -17,100 L -19,94 L -32,93 L -37,91 L -47,84 L -60,80 L -67,81 L -82,75 L -87,76 L -86,81 L -104,87 L -104,83 L -102,76 L -97,74 L -98,72 L -104,76 L -108,80 L -115,85 L -111,89 L -116,93 L -125,98 L -127,101 L -134,105 L -136,108 L -141,111 L -144,110 L -158,117 L -166,119 L -167,118 L -152,109 L -146,108 L -132,97 L -131,92 L -129,88 L -134,90 L -136,89 L -139,92 L -142,88 L -143,91 L -145,87 L -149,90 L -152,90 L -152,83 L -155,81 L -161,82 L -169,77 L -169,74 L -172,71 L -165,60 L -161,59 L -158,60 L -154,57 L -150,58 L -147,56 L -148,52 L -150,51 L -147,49 L -155,50 L -156,52 L -160,50 L -167,51 L -174,49 L -176,47 L -182,43 L -164,37 L -160,37 L -161,40 L -151,40 L -155,35 L -161,33 L -169,27 L -175,25 L -173,21 L -164,21 L -158,18 L -157,14 L -152,11 L -138,7 L -134,8 L -127,4 L -120,6 L -116,9 L -114,7 L -106,8 L -106,9 L -99,11 L -94,10 L -71,14 L -65,12 L -31,21 L -21,16 L -14,17 L 1,12 L 4,15 L 8,13 L 9,10 L 12,11 L 20,17 L 27,12 L 27,18 L 33,16 L 35,14 L 41,15 L 59,20 L 70,21 L 77,24 L 70,28 L 79,29 L 96,27 L 101,31 L 106,28 L 101,25 L 104,23 L 114,22 L 122,27 L 128,26 L 136,29 L 150,28 L 150,24 L 154,23 L 162,25 L 161,32 L 165,26 L 168,27 L 171,20 L 160,13 L 160,5 L 166,1 L 172,2 L 177,5 L 183,12 L 179,16 L 188,17 Z M 75,-8 L 73,-4 L 84,-6 L 90,-3 L 96,-6 L 100,-4 L 104,2 L 106,-0 L 103,-7 L 107,-8 L 112,-7 L 117,-5 L 122,7 L 138,13 L 138,16 L 130,17 L 133,19 L 131,22 L 115,19 L 100,22 L 80,23 L 77,20 L 70,18 L 66,19 L 60,14 L 84,11 L 75,9 L 58,10 L 55,7 L 66,5 L 59,5 L 51,3 L 58,-5 L 71,-9 L 75,-8 Z M 122,-10 L 117,-5 L 110,-10 L 118,-11 L 122,-10 Z M 256,-7 L 256,-6 L 241,-5 L 234,-9 L 234,-11 L 248,-11 L 256,-7 Z M 207,-8 L 211,-4 L 215,-9 L 227,-12 L 236,-5 L 235,-0 L 244,-2 L 249,-5 L 266,2 L 267,5 L 275,3 L 280,7 L 292,10 L 296,13 L 300,19 L 292,22 L 303,27 L 311,28 L 317,34 L 325,35 L 323,39 L 315,47 L 309,44 L 302,38 L 296,39 L 295,43 L 308,51 L 311,58 L 310,63 L 292,56 L 304,66 L 304,68 L 291,65 L 281,61 L 275,58 L 277,56 L 263,49 L 263,51 L 249,52 L 245,50 L 248,45 L 267,44 L 266,42 L 273,32 L 272,29 L 270,27 L 263,23 L 253,21 L 256,19 L 251,15 L 247,15 L 243,12 L 241,14 L 232,15 L 197,11 L 193,8 L 198,5 L 191,5 L 190,-2 L 193,-8 L 198,-10 L 211,-12 L 207,-8 Z M 141,-12 L 147,-11 L 156,-12 L 157,-10 L 152,-7 L 160,-4 L 159,2 L 151,5 L 146,4 L 131,-3 L 131,-6 L 141,-5 L 136,-9 L 141,-12 Z M 175,-5 L 170,-0 L 165,-0 L 162,-6 L 162,-10 L 164,-13 L 169,-14 L 188,-13 L 181,-7 L 175,-5 Z M 45,4 L 33,7 L 30,4 L 19,1 L 29,-11 L 24,-15 L 40,-17 L 59,-15 L 69,-10 L 51,-4 L 45,1 L 45,4 Z M 173,-20 L 171,-17 L 158,-20 L 161,-23 L 168,-25 L 173,-20 Z M 150,-32 L 154,-29 L 154,-25 L 152,-20 L 144,-20 L 139,-21 L 139,-25 L 131,-24 L 131,-29 L 136,-29 L 143,-31 L 150,-31 L 150,-32 Z M 104,-28 L 106,-26 L 115,-27 L 116,-23 L 113,-20 L 97,-19 L 85,-16 L 78,-16 L 77,-18 L 87,-21 L 65,-21 L 59,-22 L 65,-28 L 70,-30 L 83,-28 L 91,-24 L 100,-23 L 93,-30 L 97,-32 L 102,-32 L 104,-28 Z M 168,-34 L 174,-32 L 183,-32 L 187,-30 L 186,-27 L 195,-24 L 208,-23 L 216,-25 L 233,-25 L 238,-22 L 239,-20 L 229,-16 L 223,-17 L 200,-16 L 179,-19 L 177,-26 L 172,-29 L 162,-30 L 157,-32 L 159,-35 L 168,-34 Z M 66,-38 L 65,-33 L 61,-31 L 41,-26 L 34,-28 L 52,-37 L 66,-38 Z M 1130,-34 L 1131,-30 L 1135,-32 L 1149,-32 L 1160,-28 L 1164,-26 L 1163,-22 L 1141,-15 L 1154,-12 L 1159,-13 L 1161,-9 L 1163,-11 L 1171,-12 L 1186,-11 L 1187,-8 L 1207,-7 L 1207,-12 L 1225,-11 L 1233,-7 L 1235,-3 L 1232,0 L 1238,5 L 1246,8 L 1250,1 L 1258,4 L 1266,2 L 1275,4 L 1279,3 L 1287,3 L 1283,-3 L 1289,-6 L 1333,-1 L 1337,3 L 1349,8 L 1368,7 L 1378,8 L 1382,10 L 1381,15 L 1387,17 L 1393,16 L 1411,17 L 1420,16 L 1428,22 L 1434,20 L 1430,16 L 1432,13 L 1448,15 L 1458,14 L 1471,18 L 1478,20 L 1478,47 L 1472,50 L 1466,50 L 1470,53 L 1475,61 L 1475,64 L 1474,65 L 1465,64 L 1448,70 L 1434,79 L 1432,82 L 1425,77 L 1413,82 L 1410,80 L 1406,83 L 1399,82 L 1398,86 L 1392,93 L 1392,95 L 1398,97 L 1397,107 L 1393,107 L 1391,113 L 1393,116 L 1384,119 L 1383,127 L 1376,128 L 1374,135 L 1367,142 L 1361,112 L 1363,103 L 1367,99 L 1367,96 L 1375,94 L 1391,79 L 1400,73 L 1404,64 L 1398,64 L 1395,70 L 1383,77 L 1379,69 L 1367,71 L 1355,83 L 1359,87 L 1341,89 L 1341,84 L 1334,83 L 1328,87 L 1313,85 L 1298,87 L 1264,116 L 1271,117 L 1274,122 L 1278,123 L 1282,120 L 1287,120 L 1294,127 L 1294,133 L 1290,140 L 1288,159 L 1280,169 L 1279,173 L 1263,193 L 1256,197 L 1253,197 L 1250,194 L 1244,199 L 1243,201 L 1241,200 L 1239,203 L 1238,205 L 1238,210 L 1228,217 L 1227,221 L 1232,225 L 1237,237 L 1237,245 L 1235,249 L 1227,253 L 1223,254 L 1223,245 L 1221,238 L 1225,237 L 1221,231 L 1219,230 L 1217,231 L 1214,229 L 1217,225 L 1217,219 L 1212,216 L 1206,218 L 1202,221 L 1197,223 L 1199,220 L 1198,217 L 1202,213 L 1200,209 L 1190,216 L 1187,221 L 1183,221 L 1180,224 L 1183,229 L 1187,230 L 1187,233 L 1190,235 L 1196,230 L 1200,233 L 1203,233 L 1204,236 L 1197,238 L 1195,242 L 1190,245 L 1188,250 L 1193,254 L 1195,260 L 1201,272 L 1201,277 L 1198,279 L 1199,282 L 1202,284 L 1200,295 L 1197,296 L 1185,320 L 1172,332 L 1167,333 L 1164,336 L 1162,333 L 1160,337 L 1153,340 L 1148,341 L 1146,348 L 1144,349 L 1142,344 L 1144,341 L 1137,339 L 1129,346 L 1125,352 L 1124,357 L 1132,373 L 1136,377 L 1139,382 L 1141,395 L 1140,407 L 1131,416 L 1121,427 L 1119,423 L 1121,419 L 1117,415 L 1113,414 L 1109,403 L 1105,400 L 1100,400 L 1101,395 L 1097,395 L 1097,402 L 1092,418 L 1093,423 L 1096,423 L 1099,435 L 1101,439 L 1104,440 L 1111,448 L 1113,453 L 1113,467 L 1115,468 L 1117,477 L 1113,477 L 1103,467 L 1097,450 L 1098,445 L 1097,442 L 1089,429 L 1089,433 L 1088,429 L 1091,408 L 1089,404 L 1089,397 L 1087,393 L 1085,377 L 1083,371 L 1074,379 L 1069,377 L 1070,369 L 1069,363 L 1066,355 L 1066,352 L 1064,352 L 1060,346 L 1056,332 L 1051,332 L 1052,334 L 1050,338 L 1048,337 L 1047,338 L 1044,337 L 1044,339 L 1034,341 L 1035,346 L 1032,350 L 1025,354 L 1020,362 L 1012,371 L 1012,374 L 1003,378 L 1001,383 L 1003,398 L 1001,404 L 1000,416 L 998,416 L 996,421 L 997,423 L 993,425 L 989,432 L 985,425 L 981,409 L 977,400 L 975,387 L 970,378 L 967,356 L 966,341 L 959,345 L 956,345 L 950,337 L 952,334 L 950,331 L 945,326 L 941,324 L 940,319 L 936,314 L 913,316 L 893,312 L 891,304 L 889,302 L 881,307 L 875,305 L 870,299 L 865,298 L 859,282 L 856,283 L 853,281 L 851,284 L 848,283 L 852,299 L 859,305 L 859,311 L 862,319 L 862,314 L 863,310 L 864,309 L 866,311 L 865,319 L 867,323 L 870,322 L 877,323 L 888,307 L 889,317 L 891,322 L 893,324 L 900,327 L 905,335 L 899,348 L 896,347 L 895,349 L 895,358 L 893,358 L 890,360 L 888,365 L 885,365 L 883,367 L 883,369 L 881,371 L 878,371 L 870,375 L 868,380 L 856,386 L 852,391 L 848,391 L 846,394 L 837,396 L 834,400 L 827,400 L 823,383 L 824,383 L 823,372 L 816,360 L 815,354 L 810,348 L 806,342 L 806,333 L 803,326 L 798,322 L 796,313 L 787,296 L 785,296 L 786,287 L 781,299 L 778,294 L 774,284 L 776,292 L 782,309 L 790,324 L 789,330 L 796,337 L 798,360 L 803,364 L 807,378 L 816,388 L 826,402 L 826,405 L 823,406 L 827,409 L 830,415 L 832,415 L 842,413 L 850,409 L 855,408 L 863,404 L 863,414 L 856,440 L 847,457 L 842,466 L 825,483 L 818,497 L 815,499 L 814,502 L 812,503 L 809,515 L 807,517 L 804,525 L 805,529 L 808,532 L 807,543 L 813,558 L 814,584 L 811,594 L 808,598 L 798,604 L 786,619 L 785,624 L 788,635 L 789,634 L 789,648 L 775,659 L 775,662 L 777,662 L 773,679 L 769,684 L 763,695 L 754,706 L 751,709 L 743,712 L 743,714 L 740,713 L 737,715 L 732,713 L 727,714 L 713,720 L 708,715 L 707,716 L 707,714 L 705,705 L 707,704 L 707,699 L 692,668 L 688,646 L 688,635 L 683,626 L 680,614 L 676,607 L 676,592 L 679,577 L 685,567 L 685,558 L 681,547 L 683,543 L 677,519 L 665,500 L 662,493 L 665,479 L 664,478 L 666,465 L 665,460 L 662,459 L 660,453 L 652,457 L 648,457 L 644,448 L 640,443 L 629,444 L 610,454 L 606,452 L 598,451 L 592,452 L 584,456 L 582,456 L 577,453 L 565,440 L 561,436 L 558,433 L 557,425 L 550,417 L 549,412 L 545,408 L 543,408 L 541,403 L 540,394 L 538,389 L 536,386 L 538,385 L 541,377 L 541,373 L 543,363 L 542,350 L 538,344 L 539,338 L 542,333 L 544,325 L 548,320 L 551,308 L 554,306 L 557,299 L 560,296 L 564,296 L 574,284 L 573,275 L 575,266 L 578,261 L 587,255 L 591,244 L 595,244 L 598,247 L 602,247 L 609,248 L 614,245 L 619,243 L 627,239 L 643,237 L 645,238 L 650,235 L 660,236 L 665,234 L 668,234 L 668,238 L 672,235 L 673,237 L 670,240 L 670,243 L 672,245 L 671,251 L 668,254 L 669,258 L 672,258 L 673,261 L 675,262 L 680,264 L 686,265 L 692,268 L 695,274 L 706,278 L 711,281 L 715,277 L 714,271 L 716,268 L 719,265 L 722,264 L 729,265 L 731,268 L 739,270 L 740,273 L 746,272 L 758,277 L 763,273 L 767,273 L 771,274 L 772,277 L 773,275 L 781,277 L 785,273 L 786,264 L 791,252 L 791,247 L 792,244 L 790,241 L 792,238 L 789,239 L 785,237 L 782,241 L 775,242 L 771,238 L 766,238 L 765,241 L 761,242 L 757,238 L 752,238 L 749,232 L 745,228 L 748,223 L 745,219 L 750,213 L 757,213 L 759,208 L 768,208 L 774,204 L 780,202 L 787,202 L 803,209 L 812,209 L 818,205 L 819,203 L 817,198 L 795,180 L 798,179 L 802,174 L 799,171 L 806,168 L 806,167 L 791,171 L 786,173 L 787,178 L 789,179 L 794,179 L 793,181 L 788,182 L 781,186 L 779,185 L 780,182 L 774,180 L 780,176 L 779,175 L 771,173 L 771,171 L 766,171 L 761,180 L 761,182 L 759,183 L 757,183 L 756,191 L 753,194 L 752,198 L 754,205 L 758,207 L 757,209 L 751,209 L 745,215 L 744,210 L 739,209 L 733,211 L 736,215 L 734,216 L 731,216 L 729,213 L 728,214 L 731,221 L 729,223 L 734,228 L 734,232 L 730,230 L 731,233 L 728,234 L 730,240 L 727,240 L 723,237 L 720,227 L 715,216 L 712,214 L 713,204 L 710,200 L 696,192 L 692,187 L 693,187 L 691,184 L 691,182 L 688,180 L 686,183 L 685,181 L 685,179 L 686,178 L 682,177 L 679,179 L 678,185 L 680,188 L 684,192 L 687,197 L 692,203 L 696,203 L 697,204 L 696,205 L 703,210 L 708,215 L 707,217 L 704,214 L 700,213 L 698,217 L 702,220 L 701,223 L 699,224 L 697,229 L 695,230 L 697,223 L 693,215 L 685,208 L 681,207 L 677,204 L 670,196 L 668,189 L 662,186 L 651,195 L 641,193 L 635,195 L 634,203 L 630,207 L 624,209 L 618,220 L 620,224 L 618,227 L 617,232 L 613,233 L 610,238 L 599,238 L 594,243 L 592,243 L 589,236 L 584,235 L 582,237 L 580,236 L 577,237 L 578,227 L 575,227 L 574,224 L 578,211 L 577,198 L 575,195 L 582,190 L 599,193 L 611,193 L 613,189 L 614,175 L 606,165 L 598,162 L 598,157 L 604,156 L 612,157 L 611,150 L 615,153 L 626,147 L 628,142 L 638,137 L 642,127 L 649,125 L 653,125 L 654,123 L 658,123 L 658,125 L 662,121 L 658,111 L 658,104 L 660,100 L 665,100 L 670,96 L 669,102 L 669,104 L 672,105 L 671,107 L 669,107 L 666,111 L 667,117 L 672,119 L 672,121 L 679,118 L 687,123 L 704,116 L 709,117 L 709,118 L 713,118 L 715,116 L 721,113 L 720,103 L 723,98 L 727,96 L 731,101 L 735,101 L 736,92 L 734,93 L 731,90 L 731,86 L 743,84 L 753,84 L 759,81 L 754,78 L 745,78 L 729,82 L 726,78 L 721,76 L 722,69 L 720,63 L 722,59 L 727,55 L 741,46 L 740,44 L 734,40 L 726,42 L 721,47 L 722,51 L 705,62 L 701,72 L 709,80 L 705,88 L 700,90 L 698,101 L 695,107 L 690,107 L 687,112 L 681,112 L 680,106 L 672,89 L 669,84 L 660,92 L 653,94 L 647,90 L 644,68 L 648,63 L 661,58 L 670,51 L 690,28 L 711,15 L 722,12 L 730,12 L 737,7 L 745,7 L 754,5 L 769,10 L 763,12 L 768,16 L 773,14 L 781,18 L 794,20 L 812,27 L 816,31 L 816,35 L 811,39 L 803,40 L 781,35 L 778,36 L 786,41 L 786,51 L 796,55 L 797,52 L 794,49 L 797,46 L 809,50 L 813,49 L 809,44 L 820,37 L 825,38 L 829,40 L 832,35 L 828,31 L 830,27 L 827,23 L 840,25 L 843,29 L 837,30 L 837,34 L 841,36 L 848,34 L 849,30 L 876,21 L 879,22 L 875,26 L 881,26 L 884,24 L 893,24 L 900,21 L 906,25 L 911,21 L 906,17 L 908,14 L 923,17 L 946,26 L 950,23 L 945,19 L 945,18 L 939,17 L 940,14 L 938,7 L 947,0 L 950,-6 L 953,-7 L 966,-5 L 967,-1 L 962,4 L 965,6 L 967,11 L 966,20 L 971,24 L 969,29 L 960,38 L 965,39 L 967,37 L 972,35 L 973,32 L 978,29 L 975,25 L 977,20 L 972,20 L 971,16 L 974,9 L 968,4 L 977,-1 L 976,-6 L 978,-6 L 981,-2 L 979,4 L 984,6 L 982,1 L 990,-2 L 1000,-2 L 1008,2 L 1004,-4 L 1004,-11 L 1012,-12 L 1034,-13 L 1030,-17 L 1035,-21 L 1041,-21 L 1050,-25 L 1063,-25 L 1064,-27 L 1077,-28 L 1081,-26 L 1091,-30 L 1100,-30 L 1101,-33 L 1106,-36 L 1117,-38 L 1125,-36 L 1119,-35 L 1130,-34 Z M 854,207 L 856,212 L 859,212 L 860,214 L 856,215 L 854,222 L 853,224 L 854,232 L 859,233 L 862,237 L 869,238 L 876,236 L 877,223 L 873,221 L 874,216 L 871,216 L 872,210 L 877,211 L 881,209 L 877,205 L 876,201 L 872,203 L 872,208 L 870,202 L 871,199 L 870,197 L 865,195 L 862,189 L 860,187 L 860,185 L 864,185 L 864,180 L 868,179 L 873,180 L 873,174 L 873,170 L 868,170 L 864,168 L 854,173 L 852,177 L 847,178 L 842,185 L 847,191 L 846,196 L 854,207 Z M 172,-37 L 161,-37 L 160,-39 L 170,-39 L 173,-38 L 172,-37 Z M 94,-38 L 86,-36 L 78,-39 L 82,-41 L 89,-41 L 96,-40 L 94,-38 Z M 738,-39 L 727,-37 L 719,-38 L 722,-40 L 719,-42 L 729,-44 L 731,-41 L 738,-39 Z M 163,-41 L 156,-39 L 152,-41 L 150,-44 L 150,-46 L 158,-46 L 164,-43 L 163,-41 Z M 143,-43 L 145,-40 L 129,-43 L 118,-43 L 123,-45 L 117,-47 L 117,-49 L 139,-46 L 143,-43 Z M 707,-52 L 722,-47 L 710,-44 L 708,-39 L 704,-38 L 701,-32 L 696,-32 L 685,-36 L 690,-39 L 683,-41 L 673,-46 L 670,-52 L 683,-54 L 685,-52 L 692,-52 L 694,-54 L 701,-54 L 707,-52 Z M 293,-75 L 316,-73 L 325,-72 L 325,-70 L 297,-64 L 308,-64 L 289,-58 L 280,-53 L 268,-50 L 253,-49 L 260,-49 L 256,-47 L 260,-44 L 240,-35 L 240,-34 L 248,-34 L 248,-32 L 236,-28 L 223,-30 L 209,-29 L 193,-30 L 193,-33 L 201,-35 L 199,-40 L 202,-40 L 215,-37 L 208,-42 L 200,-43 L 204,-46 L 213,-47 L 214,-50 L 207,-52 L 205,-56 L 222,-55 L 230,-57 L 202,-57 L 194,-60 L 184,-64 L 183,-67 L 205,-69 L 212,-72 L 218,-71 L 223,-70 L 227,-73 L 242,-75 L 293,-75 Z M 491,-78 L 520,-72 L 512,-70 L 468,-69 L 470,-68 L 487,-68 L 501,-66 L 511,-68 L 515,-66 L 509,-62 L 545,-67 L 559,-66 L 562,-63 L 539,-56 L 524,-55 L 535,-55 L 526,-46 L 526,-38 L 532,-34 L 524,-33 L 516,-31 L 525,-28 L 526,-22 L 521,-21 L 527,-15 L 517,-15 L 522,-12 L 521,-10 L 507,-9 L 513,-4 L 513,-1 L 504,-4 L 502,-2 L 508,-1 L 514,4 L 516,9 L 508,10 L 498,4 L 500,8 L 494,12 L 513,13 L 487,24 L 468,26 L 463,29 L 457,36 L 446,41 L 430,44 L 426,48 L 426,53 L 423,57 L 416,63 L 418,68 L 413,80 L 406,81 L 399,75 L 390,75 L 385,71 L 382,65 L 374,56 L 371,52 L 371,46 L 364,40 L 366,35 L 362,32 L 367,25 L 374,22 L 376,19 L 377,14 L 365,18 L 359,16 L 359,12 L 361,8 L 375,10 L 362,3 L 358,4 L 354,2 L 359,-4 L 346,-18 L 340,-21 L 340,-24 L 328,-28 L 293,-27 L 279,-34 L 301,-36 L 281,-38 L 270,-41 L 271,-43 L 306,-50 L 308,-52 L 295,-55 L 300,-57 L 316,-62 L 323,-63 L 321,-66 L 347,-69 L 362,-69 L 367,-67 L 380,-70 L 408,-65 L 396,-69 L 397,-72 L 413,-76 L 430,-75 L 436,-78 L 491,-78 Z \ No newline at end of file diff --git a/scripts/hole-punching.gen.py b/scripts/hole-punching.gen.py new file mode 100644 index 00000000..033f4705 --- /dev/null +++ b/scripts/hole-punching.gen.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python3 +"""Generator for hole-punching.svg ("Establishing direct connections"). + +Initial state of the hole-punching story: Alice and Bob, each behind a normal +home router, are connected *through the relay* (us-east) β€” all data flows up to +the relay and back down. Later steps (CallMeMaybe, address discovery, the ping, +and the direct path) build on this. + +Continues the discovery story: Bob = 8e2b…, home relay us-east; Alice = 1a9c…. +Same look as the other how-iroh-works figures; all connections blue. + +Edit this script and run: python3 hole-punching.gen.py +Writes ../public/blog/how-iroh-works/hole-punching.svg. +Keep the MDX
aspectRatio in sync with VB_W / VB_H. +""" + +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", "public", "blog", "how-iroh-works", "hole-punching.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#374151", "#2563eb", "#dc2626", "#15803d" +CYCLE = 39.0 # one loop; popups fade after both checks are green, then the direct path stands alone +T_POPUP_FADE = 29.5 # NAT bubbles fade out here (a few seconds after both mappings are validated) + + +def anim_opacity(points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + vals = ";".join(str(v) for _, v in points) + return (f'') + + +def anim_motion(wire_id, points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + kp = ";".join(str(k) for _, k in points) + return (f'') + + +def dot(color, opa_pts, mot_pts, wire="relay-path"): + return f''' + + {anim_opacity(opa_pts)} + {anim_motion(wire, mot_pts)} + ''' + + +def phone(px0, py0, keytext): + pw, ph = 50, 92 + cx = px0+pw/2 + return f''' + + + + iroh + πŸ”‘ {keytext}''' + + +def router(cx, cy, pub_ip, lan_ip): + # same design as the "Guaranteed connections" diagram: public IP above, + # L-shaped antennas, body, LAN IP below + bx, by, bw, bh = cx-30, cy-20, 60, 40 + return f''' + {pub_ip} + + + + + + + {lan_ip} + ''' + + +def relay(cx, cy, label): + bx, by, bw, bh = cx-32, cy-9, 64, 18 + s = [' ', + f' ', + f' ', + f' ', + f' '] + for i in range(6): + vx = bx+24+i*5 + s.append(f' ') + s.append(f' {label}') + s.append(' ') + return "\n".join(s) + + +def facts(x, lines, anchor="start"): + out = [f' '] + for y, label, val in lines: + out.append(f' {label} {val}') + out.append(' ') + return "\n".join(out) + + +# ---- positions ---- +RELAY = (440, 66) +R1 = (200, 250) # Alice's router +R2 = (680, 250) # Bob's router +A_PH = (175, 312) # Alice phone top-left +B_PH = (655, 312) # Bob phone top-left +acx, bcx = 200, 680 +PH = 92 + +# relay legs: Alice <-> relay (attach left of box) and relay <-> Bob (attach right of box), +# so the "relay" label below the box stays clear +# one continuous relay pipe: rises through each router, flattens and passes +# horizontally *through* the relay (in the left edge, out the right), then drops. +# The relay clearly sits on the pipe; the "relay" label stays clear below it. +rx, ry = RELAY +relay_d = (f"M {acx} {A_PH[1]} L {acx} {R1[1]-20} " + f"C {acx} 120 {rx-160} {ry} {rx-32} {ry} " + f"L {rx+32} {ry} " + f"C {rx+160} {ry} {bcx} 120 {bcx} {R2[1]-20} " + f"L {bcx} {B_PH[1]}") + +alice_facts = facts(A_PH[0]+62, [ + (A_PH[1]+18, "EndpointId:", "1a9c…"), + (A_PH[1]+36, "Addr:", "10.0.0.3:2104"), + (A_PH[1]+54, "Addr:", "8.3.1.9:2104"), + (A_PH[1]+72, "relay:", "us-west"), +]) +bob_facts = facts(B_PH[0]+62, [ + (B_PH[1]+18, "EndpointId:", "8e2b…"), + (B_PH[1]+36, "Addr:", "192.168.0.3:4153"), + (B_PH[1]+54, "Addr:", "4.9.8.2:4153"), + (B_PH[1]+72, "relay:", "us-east"), +]) + +# ---- beat 1: ADD_ADDRESS (Bob advertises his candidate addresses to Alice, over the relay) ---- +# one packet carrying both ADD_ADDRESS frames travels Bob -> relay -> Alice. Not time-critical, +# and it does nothing to the routers' forwarding tables. +addaddr_pkt = dot(BLUE, + [(0, 0), (1.0, 0), (1.3, 1), (3.9, 1), (4.2, 0), (CYCLE, 0)], + [(0, 1), (1.0, 1), (4.0, 0), (CYCLE, 0)]) +addaddr_co_pts = [(0, 0), (1.0, 0), (1.4, 1), (4.0, 1), (4.4, 0), (CYCLE, 0)] +addaddr_co = f''' + {anim_opacity(addaddr_co_pts)} + + ADD_ADDRESS 192.168.0.3:4153 + ADD_ADDRESS 4.9.8.2:4153 + ''' + +# when the ADD_ADDRESS packet lands, Alice now knows Bob's candidates β€” ~2s popup +# beside Alice (briefly covering her own info panel) +ak_pts = [(0, 0), (4.0, 0), (4.3, 1), (6.2, 1), (6.5, 0), (CYCLE, 0)] +alice_knows = f''' + {anim_opacity(ak_pts)} + + + + + Bob + Candidate: 192.168.0.3:4153 + Candidate: 4.9.8.2:4153 + ''' + +# ---- beat 2: REACH_OUT (Alice -> Bob over the relay) β€” carries Alice's candidate addresses +# and is the trigger that starts probing; shown here decoupled from the probes for clarity ---- +reachout_pkt = dot(BLUE, + [(0, 0), (6.8, 0), (7.1, 1), (9.2, 1), (9.4, 0), (CYCLE, 0)], + [(0, 0), (6.8, 0), (9.3, 1), (CYCLE, 1)]) +reachout_co_pts = [(0, 0), (6.8, 0), (7.1, 1), (9.3, 1), (9.6, 0), (CYCLE, 0)] +reachout_co = f''' + {anim_opacity(reachout_co_pts)} + + REACH_OUT10.0.0.3:2104 + REACH_OUT8.3.1.9:2104 + ''' + +# Bob now knows Alice's candidates β€” ~2s popup beside Bob (toward center) +bk_pts = [(0, 0), (9.3, 0), (9.6, 1), (11.6, 1), (11.9, 0), (CYCLE, 0)] +bob_knows = f''' + {anim_opacity(bk_pts)} + + + + + Alice + Candidate: 10.0.0.3:2104 + Candidate: 8.3.1.9:2104 + I need to reach out + ''' + +# ============================ hole punch (Alice is the client β†’ she probes first) ============================ +# Locals fail (not routable). Then the public chain on ONE shared direct route: Alice's sacrificial +# probe opens her hole but is rejected at Bob's NAT; Bob's probe opens his hole and gets through; +# Alice replies PATH_RESPONSE+PATH_CHALLENGE; Bob replies PATH_RESPONSE β†’ both paths validated. +# No timeouts. Each step gets a clear window before the next. +T_AL, T_BL = 12.4, 14.6 # local probes (fail) +T_AP = 16.9 # Alice public β€” sacrificial +T_BP = 19.9 # Bob public β€” succeeds +T_AR = 22.9 # Alice reply: PATH_RESPONSE + PATH_CHALLENGE +T_BR = 25.7 # Bob reply: PATH_RESPONSE β†’ validated +T_BLUE = T_BR + 2.4 # direct path turns blue once the response validates it + +DROP_A, DROP_B = (382, 200), (498, 200) # local drops, band above the routers +probe_local_a_d = f"M {acx} {A_PH[1]} L {acx} {R1[1]-20} C {acx} 205 300 200 {DROP_A[0]} {DROP_A[1]}" +probe_local_b_d = f"M {bcx} {B_PH[1]} L {bcx} {R2[1]-20} C {bcx} 205 580 200 {DROP_B[0]} {DROP_B[1]}" + +# the one direct (internet) route, Alice(0) -> Bob(1). Every public packet rides it. +ARC = 158 +direct_d = f"M {acx} {A_PH[1]} L {acx} {R1[1]-20} C {acx} {ARC} {bcx} {ARC} {bcx} {R2[1]-20} L {bcx} {B_PH[1]}" + + +def cubic_len(p0, p1, p2, p3, n=120): + def b(t, a, bb, c, d): return (1-t)**3*a + 3*(1-t)**2*t*bb + 3*(1-t)*t*t*c + t**3*d + prev, L = p0, 0.0 + for i in range(1, n+1): + t = i/n + cur = (b(t, p0[0], p1[0], p2[0], p3[0]), b(t, p0[1], p1[1], p2[1], p3[1])) + L += ((cur[0]-prev[0])**2 + (cur[1]-prev[1])**2)**0.5 + prev = cur + return L + + +_seg = A_PH[1] - (R1[1]-20) +_arc = cubic_len((acx, R1[1]-20), (acx, ARC), (bcx, ARC), (bcx, R2[1]-20)) +F_BR = (_seg + _arc) / (_seg + _arc + _seg) # fraction at Bob's router β€” where the sacrificial bounces +REJECT = (bcx, R2[1]-20) # Bob's router WAN β€” sacrificial probe rejected here + + +def hp_pkt(wire, t0, dur, kp0=0, kp1=1, color=BLUE): + arr = t0 + dur + return f''' + + {anim_opacity([(0, 0), (t0, 0), (t0+0.15, 1), (arr-0.12, 1), (arr+0.06, 0), (CYCLE, 0)])} + {anim_motion(wire, [(0, kp0), (t0, kp0), (arr, kp1), (CYCLE, kp1)])} + ''' + + +def drop_x(p, t_arr, label, hold=1.9, dx=0): + pts = [(0, 0), (t_arr-0.05, 0), (t_arr+0.15, 1), (t_arr+hold, 1), (t_arr+hold+0.3, 0), (CYCLE, 0)] + if dx: # label to the side instead of below + lbl = f'{label}' + else: + lbl = f'{label}' + return f''' + {anim_opacity(pts)} + + + {lbl} + ''' + + +def callout(side, lines, t0, t1, w=164): + x = 240 if side == "L" else (656 - w) + h = 18 + len(lines)*19 + pts = [(0, 0), (t0-0.3, 0), (t0, 1), (t1, 1), (t1+0.3, 0), (CYCLE, 0)] + rows = "\n".join( + f' {txt}' + for i, (txt, bold) in enumerate(lines)) + return f''' + {anim_opacity(pts)} + +{rows} + ''' + + +def pc_co(side, addr, t0, t1): + return callout(side, [("PATH_CHALLENGE", True), (addr, False)], t0, t1) + + +def nat_bubble(side, l1, l2, t_on, t_used): + # thinking bubble on a router: the NAT mapping (remote public -> local private). A green + # check appears next to the mapping the moment a packet actually uses it. + out0, out1 = CYCLE-0.7, CYCLE-0.3 + pts = [(0, 0), (t_on, 0), (t_on+0.4, 1), (out0, 1), (out1, 0), (CYCLE, 0)] + hg_pts = [(0, 0), (t_on+0.4, 0), (t_on+0.7, 1), (t_used, 1), (t_used+0.3, 0), (CYCLE, 0)] # hourglass: created, temporary + chk_pts = [(0, 0), (t_used, 0), (t_used+0.3, 1), (CYCLE, 1)] # check: used / validated + if side == "a": # to the right of Alice's router, trail back to it + bx, trail = 240, [(236, 251, 3), (231, 250, 2.5), (227, 249, 2)] + else: # to the right of Bob's router + bx, trail = 720, [(716, 251, 3), (711, 250, 2.5), (707, 249, 2)] + dots = "\n".join(f' ' for c in trail) + cx = bx+138 + return f''' + {anim_opacity(pts)} +{dots} + + {l1} + {l2} + + + + + + {anim_opacity(hg_pts)} + + + + {anim_opacity(chk_pts)} + + ''' + + +# beat 3 β€” sequential locals (Alice first), not routable +loc_a_pkt = hp_pkt("probe-local-a", T_AL, 1.4) +loc_b_pkt = hp_pkt("probe-local-b", T_BL, 1.4) +loc_a_drop = drop_x(DROP_A, T_AL+1.4, "not routable", hold=0.5) +loc_b_drop = drop_x(DROP_B, T_BL+1.4, "not routable", hold=0.5) +loc_a_co = pc_co("L", "192.168.0.3:4153", T_AL-0.3, T_AL+1.5) +loc_b_co = pc_co("R", "10.0.0.3:2104", T_BL-0.3, T_BL+1.5) + +# beat 4 β€” Alice's sacrificial public probe: opens her hole, rejected at Bob's NAT (bounces) +pub_a_pkt = hp_pkt("direct", T_AP, 2.0, kp0=0, kp1=F_BR) +pub_a_drop = drop_x(REJECT, T_AP+2.0, "no mapping", hold=0.6, dx=16) +pub_a_co = pc_co("L", "4.9.8.2:4153", T_AP-0.3, T_AP+2.1) +nat_a = nat_bubble("a", "4.9.8.2:4153 β†’", "10.0.0.3:2104", T_AP+0.6, t_used=21.9) + +# beat 5 β€” Bob's public probe: opens his hole, through Alice's hole, reaches Alice (success) +pub_b_pkt = hp_pkt("direct", T_BP, 2.2, kp0=1, kp1=0) +pub_b_co = pc_co("R", "8.3.1.9:2104", T_BP-0.3, T_BP+2.3) +nat_b = nat_bubble("b", "8.3.1.9:2104 β†’", "192.168.0.3:4153", T_BP+0.6, t_used=24.9) + +# beat 6 β€” Alice's bundled reply (PATH_RESPONSE + PATH_CHALLENGE) β†’ Bob +reply_pkt = hp_pkt("direct", T_AR, 2.2, kp0=0, kp1=1) +reply_co = callout("L", [("PATH_RESPONSE", True), ("PATH_CHALLENGE", True)], T_AR-0.3, T_AR+2.3, w=164) + +# beat 7 β€” Bob's PATH_RESPONSE β†’ Alice; both directions validated +resp_pkt = hp_pkt("direct", T_BR, 2.2, kp0=1, kp1=0) +resp_co = callout("R", [("PATH_RESPONSE", True)], T_BR-0.3, T_BR+2.3, w=150) + +# the direct path itself: appears gray as Bob's packet goes through, turns blue once validated +direct_opa = [(0, 0), (T_BP, 0), (T_BP+0.3, 1), (CYCLE-0.5, 1), (CYCLE-0.2, 0), (CYCLE, 0)] +direct_stroke = (f'') +direct_path = (f' ' + f'{anim_opacity(direct_opa)}{direct_stroke}') + +# relay pipe fades blue -> gray once the direct path takes over +relay_stroke = (f'') + +VB_X, VB_Y, VB_W, VB_H = 0, 30, 940, 430 +svg = f''' + + {relay_stroke} + +{direct_path} + + + + +{relay(*RELAY, "relay")} + +{router(*R1, "8.3.1.9", "10.0.0.1")} + +{router(*R2, "4.9.8.2", "192.168.0.1")} + + + +{phone(*A_PH, "1a9c…")} + Alice + +{alice_facts} + + + +{phone(*B_PH, "8e2b…")} + Bob + +{bob_facts} + +{addaddr_pkt} + +{addaddr_co} + +{alice_knows} + +{reachout_pkt} + +{reachout_co} + +{bob_knows} + + +{loc_a_pkt} +{loc_b_pkt} +{pub_a_pkt} +{pub_b_pkt} +{reply_pkt} +{resp_pkt} +{loc_a_drop} +{loc_b_drop} +{pub_a_drop} +{nat_a} +{nat_b} +{loc_a_co} +{loc_b_co} +{pub_a_co} +{pub_b_co} +{reply_co} +{resp_co} + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/publish-relay-dht.gen.py b/scripts/publish-relay-dht.gen.py new file mode 100644 index 00000000..206f41b9 --- /dev/null +++ b/scripts/publish-relay-dht.gen.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +"""Generator for publish-relay-dht.svg ("Mainline DHT based address lookup"). + +The fully peer-to-peer variant of the DNS discovery figure. Instead of one +server (dns.iroh.link) there is a cloud (the Mainline DHT) holding many nodes. +Bob publishes his signed record to several random DHT nodes (green); Alice +resolves it by querying several random nodes (blue). + +All dots travel at the same speed, so they reach their nodes (and return) at +different times depending on distance. No arrowheads β€” the moving dots show the +direction. The answer is revealed as soon as the FIRST query response returns. + +One master SMIL loop (CYCLE seconds): publish -> Alice resolves -> arrows fade +out -> final state holds ~10s -> loop. + +Edit this script and run: python3 publish-relay-dht.gen.py +Writes ../public/blog/how-iroh-works/publish-relay-dht.svg. +Keep the MDX
aspectRatio in sync with VB_W / VB_H. +""" + +import math +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", "public", "blog", "how-iroh-works", "publish-relay-dht.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#374151" +GREEN, BLUE, RED = "#15803d", "#2563eb", "#dc2626" +SPEED = 150.0 # px/second β€” same for every dot + + +def arc(x1, y1, x2, y2, k=0.16): + mx, my = (x1+x2)/2, (y1+y2)/2 + dx, dy = x2-x1, y2-y1 + L = math.hypot(dx, dy) + px, py = -dy/L, dx/L + if py > 0: + px, py = -px, -py + cx, cy = mx+px*k*L, my+py*k*L + d = f"M {x1:.0f} {y1:.0f} Q {cx:.0f} {cy:.0f} {x2:.0f} {y2:.0f}" + def q(t, a, b, c): return (1-t)**2*a + 2*(1-t)*t*b + t**2*c + N, length, prev = 50, 0.0, (x1, y1) + for i in range(1, N+1): + t = i/N + cur = (q(t, x1, cx, x2), q(t, y1, cy, y2)) + length += math.hypot(cur[0]-prev[0], cur[1]-prev[1]) + prev = cur + return d, length + + +def phone(px0, py0, keytext): + pw, ph = 48, 88 + cx = px0+pw/2 + return f''' + + + + iroh + πŸ”‘ {keytext}''' + + +# ============================ geometry ============================ +# DHT cloud (soft, fill-only overlapping circles) +cloud_circles = [(320, 150, 56), (392, 124, 62), (462, 130, 58), (524, 156, 50), + (360, 172, 56), (442, 178, 56), (292, 174, 40), (548, 182, 38)] +nodes = [(320, 162), (376, 138), (432, 172), (484, 142), (528, 168), (402, 196)] +pub_nodes = nodes[0:5] # Bob publishes to these +look_nodes = nodes[1:6] # Alice queries these + +bx0, by0 = 96, 296 +bcx = bx0+24 +PH = 88 +bob_top = (bcx, by0) + +ax0, ay0 = 664, 296 +acx = ax0+24 +alice_top = (acx, ay0) + +# ============================ timeline (constant speed) ============================ +PUB_LAUNCH = 1.5 +pub = [] # (path_d, arrival_time) +for n in pub_nodes: + d, L = arc(bob_top[0], bob_top[1], n[0], n[1], k=0.16) + pub.append((d, PUB_LAUNCH + L/SPEED)) +PUB_DONE = max(t for _, t in pub) + +ALICE_IN = PUB_DONE + 0.3 +LOOK_LAUNCH = ALICE_IN + 1.2 +look = [] # (path_d, reach_node_time, back_at_alice_time, has_data) +for n in look_nodes: + d, L = arc(n[0], n[1], alice_top[0], alice_top[1], k=0.16) + half = L/SPEED + look.append((d, LOOK_LAUNCH + half, LOOK_LAUNCH + 2*half, n in pub_nodes)) +FIRST_VALID = min(b for _, _, b, ok in look if ok) # first node that actually has the data +LAST_RETURN = max(b for _, _, b, _ in look) + +HOLD = 5.0 +ARROWS_GONE = LAST_RETURN + 0.6 +CYCLE = ARROWS_GONE + HOLD + 0.6 +OUT0, OUT1 = CYCLE - 0.6, CYCLE - 0.2 # persistent elements fade out before loop + + +def anim_opacity(points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + vals = ";".join(str(v) for _, v in points) + return (f'') + + +def anim_motion(wire_id, points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + kp = ";".join(str(k) for _, k in points) + return (f'') + + +def dot(wire_id, color, opa_pts, mot_pts): + return f''' + + {anim_opacity(opa_pts)} + {anim_motion(wire_id, mot_pts)} + ''' + + +def answer_dot(wire_id, color_down, launch, reach, back): + # query goes up blue, answer comes back coloured (green=valid, red=no data) + up = [(0, 0), (launch, 0), (launch+0.15, 1), (reach-0.05, 1), (reach, 0), (CYCLE, 0)] + down = [(0, 0), (reach, 0), (reach+0.05, 1), (back-0.05, 1), (back+0.1, 0), (CYCLE, 0)] + mot = [(0, 1), (launch, 1), (reach, 0), (back, 1), (CYCLE, 1)] + return f''' + {anim_motion(wire_id, mot)} + {anim_opacity(up)} + {anim_opacity(down)} + ''' + + +# ============================ static backdrop ============================ +cloud = [' ', ' '] +for cx, cy, r in cloud_circles: + cloud.append(f' ') +cloud.append(f' Mainline DHT') +cloud.append(' ') +cloud = "\n".join(cloud) + +# nodes are black; a published node turns green when its message arrives (one is never reached) +nd = [' ', ' '] +for x, y in nodes: + nd.append(f' ') +for i, (x, y) in enumerate(pub_nodes): + arr = pub[i][1] + g_pts = [(0, 0), (arr, 0), (arr+0.3, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] + nd.append(f' {anim_opacity(g_pts)}') +nd.append(' ') +node_dots = "\n".join(nd) + +bob = f''' + +{phone(bx0, by0, "8e2b…")} + Bob + + + NodeId: 8e2b… + Home relay: us-east + ''' + +alice_pts = [(0, 0), (ALICE_IN, 0), (ALICE_IN+0.6, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] +result_pts = [(0, 0), (FIRST_VALID, 0), (FIRST_VALID+0.5, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] +alice = f''' + + {anim_opacity(alice_pts)} +{phone(ax0, ay0, "1a9c…")} + Alice + + 8e2b… is at relay us-east{anim_opacity(result_pts)}''' + +# ============================ wires + dots ============================ +pub_wire_pts = [(0, 0), (0.6, 0), (1.0, 1), (PUB_DONE, 1), (PUB_DONE+0.5, 0), (CYCLE, 0)] +look_wire_pts = [(0, 0), (LOOK_LAUNCH-0.6, 0), (LOOK_LAUNCH, 1), (LAST_RETURN, 1), (LAST_RETURN+0.6, 0), (CYCLE, 0)] +wires, packets = [], [] +for i, (d, arr) in enumerate(pub): + wires.append(f' {anim_opacity(pub_wire_pts)}') + opa = [(0, 0), (PUB_LAUNCH, 0), (PUB_LAUNCH+0.15, 1), (arr-0.05, 1), (arr+0.1, 0), (CYCLE, 0)] + mot = [(0, 0), (PUB_LAUNCH, 0), (arr, 1), (CYCLE, 1)] + packets.append(dot(f"pw{i}", BLUE, opa, mot)) +for i, (d, reach, back, ok) in enumerate(look): + wires.append(f' {anim_opacity(look_wire_pts)}') + packets.append(answer_dot(f"lw{i}", GREEN if ok else RED, LOOK_LAUNCH, reach, back)) +wires = "\n".join(wires) +packets = "\n".join(packets) + +# ============================ callouts ============================ +pub_co_pts = [(0, 0), (1.6, 0), (2.0, 1), (PUB_DONE, 1), (PUB_DONE+0.5, 0), (CYCLE, 0)] +pub_co = f''' + + {anim_opacity(pub_co_pts)} + + DHT put + 8e2b… + Relay: us-east + ''' + +# DHT get callout (read request) β€” same box as the answer, shown until the answer arrives +read_pts = [(0, 0), (LOOK_LAUNCH-0.3, 0), (LOOK_LAUNCH+0.1, 1), (FIRST_VALID-0.1, 1), (FIRST_VALID+0.3, 0), (CYCLE, 0)] +read_co = f''' + + {anim_opacity(read_pts)} + + DHT get + 8e2b… + ''' + +# answer popup is visible while the dots are still travelling, then vanishes +ans_pts = [(0, 0), (FIRST_VALID, 0), (FIRST_VALID+0.4, 1), (LAST_RETURN, 1), (LAST_RETURN+0.6, 0), (CYCLE, 0)] +ans_co = f''' + + {anim_opacity(ans_pts)} + + ;; ANSWER SECTION: + TXT "relay=https://us-east" + ''' + +VB_X, VB_Y, VB_W, VB_H = 0, 44, 820, 376 +svg = f''' +{cloud} + +{wires} + +{node_dots} + +{bob} + +{alice} + +{packets} + +{pub_co} + +{read_co} + +{ans_co} + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH} (CYCLE={CYCLE:.1f}s, first valid answer at {FIRST_VALID:.1f}s)") diff --git a/scripts/publish-relay.gen.py b/scripts/publish-relay.gen.py new file mode 100644 index 00000000..1d60f4b2 --- /dev/null +++ b/scripts/publish-relay.gen.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Generator for publish-relay.svg (the "Publishing the home relay" figure). + +Adapted from rklaehn's "DNS discovery" slide, restyled to match the other +how-iroh-works figures. No map. Our device "Bob" (left) publishes its current +home relay to the vanilla DNS server dns.iroh.link with a signed DNS packet sent +as an HTTPS PUT (green). Alice (right) resolves it with a DNS lookup (blue). +Continues the endpoint-startup story (Bob's home relay = us-east, key 8e2b...). + +One master loop (CYCLE seconds), driven entirely by SMIL so every element stays +in sync: publish once -> record added -> Alice resolves once -> arrows fade out +-> final state holds ~10s -> loop. + +To change it, edit this script and run: python3 publish-relay.gen.py +It writes ../public/blog/how-iroh-works/publish-relay.svg. +Keep the MDX
aspectRatio in sync with VB_W / VB_H. +""" + +import math +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", "public", "blog", "how-iroh-works", "publish-relay.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#374151" +GREEN, BLUE = "#15803d", "#2563eb" + +CYCLE = 14.5 # seconds for one full loop (~5s static hold) + + +def anim_opacity(points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + vals = ";".join(str(v) for _, v in points) + return (f'') + + +def anim_motion(wire_id, points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + kp = ";".join(str(k) for _, k in points) + return (f'') + + +def arc(x1, y1, x2, y2, k=0.16): + mx, my = (x1+x2)/2, (y1+y2)/2 + dx, dy = x2-x1, y2-y1 + L = math.hypot(dx, dy) + px, py = -dy/L, dx/L + if py > 0: + px, py = -px, -py + cx, cy = mx+px*k*L, my+py*k*L + return f"M {x1:.0f} {y1:.0f} Q {cx:.0f} {cy:.0f} {x2:.0f} {y2:.0f}" + + +def phone(px0, py0, keytext): + pw, ph = 48, 88 + cx = px0+pw/2 + return f''' + + + + iroh + πŸ”‘ {keytext}''' + + +def doc_packet(wire_id, color, opa_pts, mot_pts): + # a simple dot riding a wire (the popup explains what the request is) + return f''' + + {anim_opacity(opa_pts)} + {anim_motion(wire_id, mot_pts)} + ''' + + +# ============================ static backdrop ============================ + +# dns.iroh.link server, top-center (vanilla 3U rack, gray LEDs) +sx, sy, sw, sh = 378, 56, 64, 66 +scx = sx+sw/2 +srv = [' ', ' ', + f' '] +for i in range(3): + ry = sy + 4 + i*21 + srv.append(f' ') + srv.append(f' ') + for j in range(6): + vx = sx+24+j*5 + srv.append(f' ') + if i < 2: + yy = sy+4+(i+1)*21-2 + srv.append(f' ') +srv.append(f' dns.iroh.link') +srv.append(' ') +server = "\n".join(srv) + +# Bob (left, publisher) β€” always present +bx0, by0 = 96, 296 +bcx = bx0+24 +PH = 88 +bob = f''' + +{phone(bx0, by0, "8e2b…")} + Bob + + + NodeId: 8e2b… + Home relay: us-east + ''' + +# ============================ records on the server ============================ +rec_x = sx+sw+30 +existing = [("1f3a…", "eu-west"), ("7c0e…", "us-west"), + ("b48d…", "us-east"), ("2a91…", "eu-west")] +rlh, ry0 = 16, 60 +rec_lines = [f' {k} β†’ {r}' + for i, (k, r) in enumerate(existing)] +ynew = ry0 + len(existing)*rlh +rec_pts = [(0, 0), (3.5, 0), (4.0, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] +record = f''' + +{chr(10).join(rec_lines)} + 8e2b… β†’ us-east{anim_opacity(rec_pts)} + ''' + +# ============================ Alice (right, resolver) ============================ +ax0, ay0 = 664, 296 +acx = ax0+24 +alice_pts = [(0, 0), (3.8, 0), (4.4, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] +result_pts = [(0, 0), (8.0, 0), (8.5, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] +alice = f''' + + {anim_opacity(alice_pts)} +{phone(ax0, ay0, "1a9c…")} + Alice + + + 8e2b… is at relay us-east{anim_opacity(result_pts)}''' + +# ============================ wires ============================ +put_d = arc(bcx, by0, sx+18, sy+sh, k=0.18) # Bob -> server +lookup_d = arc(sx+sw-18, sy+sh, acx, ay0, k=0.18) # server -> Alice (answer direction) +put_wire_pts = [(0, 0), (0.6, 0), (1.0, 1), (4.0, 1), (4.5, 0), (CYCLE, 0)] +lookup_wire_pts = [(0, 0), (4.4, 0), (5.0, 1), (8.3, 1), (8.8, 0), (CYCLE, 0)] +wires = f''' {anim_opacity(put_wire_pts)} + {anim_opacity(lookup_wire_pts)}''' + +# ============================ packets ============================ +# PUT: Bob(0) -> server(1), fades out as it lands (~3.5s) +put_pkt = doc_packet("put-wire", BLUE, + [(0, 0), (1.5, 0), (1.7, 1), (3.4, 1), (3.6, 0), (CYCLE, 0)], + [(0, 0), (1.5, 0), (3.5, 1), (CYCLE, 1)]) +# LOOKUP: on lookup-wire 0=server,1=Alice. Query Alice(1)->server(0), answer server(0)->Alice(1). +lookup_pkt = doc_packet("lookup-wire", BLUE, + [(0, 0), (5.0, 0), (5.2, 1), (7.9, 1), (8.1, 0), (CYCLE, 0)], + [(0, 1), (5.0, 1), (6.5, 0), (8.0, 1), (CYCLE, 1)]) + +# ============================ callouts ============================ +put_co_pts = [(0, 0), (1.6, 0), (2.0, 1), (4.0, 1), (4.5, 0), (CYCLE, 0)] +put_co = f''' + + {anim_opacity(put_co_pts)} + + HTTPS PUT + Relay: us-east + Signed by: 8e2b… + ''' + +lookup_co_pts = [(0, 0), (4.6, 0), (5.0, 1), (8.3, 1), (8.8, 0), (CYCLE, 0)] +# the query content is shown on the way up, then replaced by the answer as the dot heads back (~6.5s) +query_pts = [(0, 0), (4.6, 0), (5.0, 1), (6.3, 1), (6.6, 0), (CYCLE, 0)] +answer_pts = [(0, 0), (6.4, 0), (6.7, 1), (8.3, 1), (8.7, 0), (CYCLE, 0)] +lookup_co = f''' + + {anim_opacity(lookup_co_pts)} + + + {anim_opacity(query_pts)} + DNS LOOKUP + TXT _iroh.8e2b….dns.iroh.link + + + {anim_opacity(answer_pts)} + ;; ANSWER SECTION: + TXT "relay=https://us-east" + + ''' + +VB_X, VB_Y, VB_W, VB_H = 0, 44, 820, 376 +svg = f''' + + + + + + + + + +{wires} + +{server} + +{record} + +{bob} + +{alice} + +{put_pkt} +{lookup_pkt} + +{put_co} + +{lookup_co} + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/src/app/blog/how-iroh-works/page.mdx b/src/app/blog/how-iroh-works/page.mdx new file mode 100644 index 00000000..c571af9f --- /dev/null +++ b/src/app/blog/how-iroh-works/page.mdx @@ -0,0 +1,149 @@ +import { BlogPostLayout } from '@/components/BlogPostLayout' +import { MotionCanvas } from '@/components/MotionCanvas' + +export const post = { + draft: false, + author: 'RΓΌdiger Klaehn', + date: '2026-06-17', + title: 'How iroh works', + description: '', +} + +export const metadata = { + title: post.title, + description: post.description, + openGraph: { + title: post.title, + description: post.description, + images: [{ + url: `/api/og?title=Blog&subtitle=${post.title}`, + width: 1200, + height: 630, + alt: post.title, + type: 'image/png', + }], + type: 'article' + } +} + +export default (props) => + +We have had a lot of explanations about how iroh works in the past. But before 1.0 it was a bit of a moving target. Now that [1.0 is out][v1] it is time to provide a detailed explanation. + +# Iroh is a library + +Iroh is a lightweight native library that is meant to be embedded into applications. It can be embedded in rust applications [directly][iroh-crate], and into [C][iroh-c-ffi-repo], [C++][iroh-c-ffi-repo], [swift][iroh-swift], [python][iroh-python], [javascript][iroh-js] and [kotlin][iroh-kotlin] via [our bindings][iroh-ffi]. + +
+ + iroh embedded across six device contexts: iOS, Android, desktop, browser, server, embedded + +
+ +# The two key features of iroh + +## Connect by key + +Iroh endpoints are identified by a cryptographic key. This allows us to connect no matter where the remote endpoint is. + +
+ + Alice connects to Bob by key β€” IPs change, the key stays stable + +
+ +## Guaranteed connections + +We make sure that you do get a connection whenever possible. And we make sure that the connection is as fast as possible. + +When conditions change, we immediately react and choose the new best path. This is transparent to the user. + +
+ + Alice and Bob connected via a shared router, with two more routers available for when Bob moves + +
+ +# So how this it actually work? + +Now that we have shown *what* iroh does for you, let's go into details about *how*. + +# Iroh components + +## Iroh library + +The iroh library itself provides an API to create endpoints and connect to other endpoints, as well as a rich API for connections and data streams. + +## Iroh relay + +The iroh relays work in the background on a publicly reachable servers to faciliate direct connections and to relay data in case establishing a direct connection is not possible. + +# Endpoint startup + +An iroh endpoint is configured with a set of relays. Either the rate limited public relays operated by number0, relays bought via iroh services, or self-hosted relays. + +Iroh works just fine with a single relay, but works best if you have relays in all geographic regions where your users are. + +## Determining the home relay + +
+ + A map of the US and Europe with iroh relays in us-west (Seattle), us-east (Delaware), and eu-west (Frankfurt), and an endpoint in Florida + +
+ +On startup, an iroh endpoint sends [QAD] probes to all configured relays. This serves two purposes: learning our own public IP address as well as learning which relay is closest in terms of latency. The closest relay becomes our home relay. We keep a secure websocket connection open to our home relay. + +## Publishing the home relay + +If we had only one relay, we would be done now. But frequently we have multiple relays. Alice needs to know which relay to use to talk to Bob. For this we have two mechanisms + +### DNS based address lookup + +We offer a mechanism based on DNS out of the box. Bob publishes a signed DNS record to a DNS server operated by number0 via a https PUT request. Alice or whoever wants to talk to bob then looks up this record using a DNS query or a https query. + +
+ + Bob publishes a signed DNS record with his home relay to dns.iroh.link via an HTTPS PUT; Alice resolves it with a DNS lookup + +
+ +### Mainline DHT based address lookup + +The above mechanism works fine in most cases. But maybe you want something that is fully peer to peer. For that case we offer an optional DHT based address lookup mechanism. We publish the exact same record as before on the mainline DHT, using the [bep_0044] extension. + +
+ + Bob publishes his signed record to several random nodes of the Mainline DHT; Alice resolves it by querying several random nodes + +
+ + +## Establishing direct connections + +So far we have described a system that makes sure that endpoints can talk at all. But all data is flowing through the relays. This adds latency, limits performance, and causes costs for the relay operator. + +How do we make the connections fast and direct? This is aguably the most complex part of the system. + +## Hole punching + +Hole punching happens *inside* the QUIC connection via an extension `n0_nat_traversal`. It is inspired by the [QUIC NAT traversal draft][quic-nat-traversal], but uses its own transport parameter ID. + +
+ + Alice and Bob, each behind a home router, connected through the relay β€” all data flows up to the relay and back down + +
+ + +[iroh-c-ffi-repo]: https://github.com/n0-computer/iroh-c-ffi +[iroh-crate]: https://crates.io/crates/iroh +[iroh-ffi]: https://github.com/n0-computer/iroh-ffi +[iroh-js]: https://www.npmjs.com/package/@number0/iroh +[iroh-kotlin]: https://central.sonatype.com/artifact/computer.iroh/iroh +[iroh-python]: https://pypi.org/project/iroh/ +[iroh-swift]: https://swiftpackageindex.com/n0-computer/iroh-ffi +[QAD]: https://datatracker.ietf.org/doc/draft-ietf-quic-address-discovery/ +[bep_0044]: https://www.bittorrent.org/beps/bep_0044.html +[v1]: /blog/v1 +[quic-nat-traversal]: https://datatracker.ietf.org/doc/draft-seemann-quic-nat-traversal/ From a0592aa145b8b3e40fafd43e3b4aa2acd314f23d Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 17 Jun 2026 21:47:27 +0300 Subject: [PATCH 02/11] finish hp first version --- public/blog/how-iroh-works/hole-punching.svg | 77 ++++++++++---------- scripts/hole-punching.gen.py | 19 +++-- 2 files changed, 52 insertions(+), 44 deletions(-) diff --git a/public/blog/how-iroh-works/hole-punching.svg b/public/blog/how-iroh-works/hole-punching.svg index 2689e601..8b67071f 100644 --- a/public/blog/how-iroh-works/hole-punching.svg +++ b/public/blog/how-iroh-works/hole-punching.svg @@ -1,8 +1,11 @@ - + - + + + + @@ -79,19 +82,19 @@ - - + + - + ADD_ADDRESS 192.168.0.3:4153 ADD_ADDRESS 4.9.8.2:4153 - + @@ -103,19 +106,19 @@ - - + + - + REACH_OUT10.0.0.3:2104 REACH_OUT8.3.1.9:2104 - + @@ -129,54 +132,54 @@ - - + + - - + + - - + + - - + + - - + + - - + + - + not routable - + not routable - + no mapping - + @@ -188,15 +191,15 @@ - + - + - + @@ -208,45 +211,45 @@ - + - + - + PATH_CHALLENGE 192.168.0.3:4153 - + PATH_CHALLENGE 10.0.0.3:2104 - + PATH_CHALLENGE 4.9.8.2:4153 - + PATH_CHALLENGE 8.3.1.9:2104 - + PATH_RESPONSE PATH_CHALLENGE - + PATH_RESPONSE diff --git a/scripts/hole-punching.gen.py b/scripts/hole-punching.gen.py index 033f4705..51feafcb 100644 --- a/scripts/hole-punching.gen.py +++ b/scripts/hole-punching.gen.py @@ -22,6 +22,7 @@ MONO = "'Space Mono', monospace" INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#374151", "#2563eb", "#dc2626", "#15803d" +FADED = "#aab2bd" # inactive/secondary connection β€” clearly lighter than the structure gray CYCLE = 39.0 # one loop; popups fade after both checks are green, then the direct path stands alone T_POPUP_FADE = 29.5 # NAT bubbles fade out here (a few seconds after both mappings are validated) @@ -270,7 +271,7 @@ def pc_co(side, addr, t0, t1): def nat_bubble(side, l1, l2, t_on, t_used): # thinking bubble on a router: the NAT mapping (remote public -> local private). A green # check appears next to the mapping the moment a packet actually uses it. - out0, out1 = CYCLE-0.7, CYCLE-0.3 + out0, out1 = T_POPUP_FADE, T_POPUP_FADE+1.0 pts = [(0, 0), (t_on, 0), (t_on+0.4, 1), (out0, 1), (out1, 0), (CYCLE, 0)] hg_pts = [(0, 0), (t_on+0.4, 0), (t_on+0.7, 1), (t_used, 1), (t_used+0.3, 0), (CYCLE, 0)] # hourglass: created, temporary chk_pts = [(0, 0), (t_used, 0), (t_used+0.3, 1), (CYCLE, 1)] # check: used / validated @@ -327,18 +328,19 @@ def nat_bubble(side, l1, l2, t_on, t_used): resp_pkt = hp_pkt("direct", T_BR, 2.2, kp0=1, kp1=0) resp_co = callout("R", [("PATH_RESPONSE", True)], T_BR-0.3, T_BR+2.3, w=150) -# the direct path itself: appears gray as Bob's packet goes through, turns blue once validated +# the direct path itself: appears faded-gray as Bob's packet goes through, turns blue once validated direct_opa = [(0, 0), (T_BP, 0), (T_BP+0.3, 1), (CYCLE-0.5, 1), (CYCLE-0.2, 0), (CYCLE, 0)] direct_stroke = (f'') -direct_path = (f' ' +direct_path = (f' ' f'{anim_opacity(direct_opa)}{direct_stroke}') -# relay pipe fades blue -> gray once the direct path takes over +# the relay stays blue/active (user data flows over it during hole punching) and only fades to +# gray at the moment the direct path becomes blue β€” i.e. when the handover happens relay_stroke = (f'') + f'values="{BLUE};{BLUE};{FADED};{FADED}" ' + f'keyTimes="0;{T_BLUE/CYCLE:.4f};{(T_BLUE+1.0)/CYCLE:.4f};1"/>') VB_X, VB_Y, VB_W, VB_H = 0, 30, 940, 430 svg = f''' @@ -346,6 +348,9 @@ def nat_bubble(side, l1, l2, t_on, t_used): {relay_stroke} {direct_path} + + + From ce6ea317123219ae3fea970c38c1170d290f4367 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Thu, 18 Jun 2026 11:58:48 +0300 Subject: [PATCH 03/11] Add button to open the animations in a separate window. --- public/blog/how-iroh-works/connect-by-key.svg | 8 + .../blog/how-iroh-works/endpoint-startup.svg | 7 + .../blog/how-iroh-works/hole-punching-lan.svg | 155 +++++++++ public/blog/how-iroh-works/hole-punching.svg | 7 + .../blog/how-iroh-works/publish-relay-dht.svg | 27 +- public/blog/how-iroh-works/publish-relay.svg | 11 +- public/blog/how-iroh-works/routing-moves.svg | 121 +++---- scripts/hole-punching-lan.gen.py | 305 ++++++++++++++++++ src/app/blog/how-iroh-works/page.mdx | 83 +++-- 9 files changed, 608 insertions(+), 116 deletions(-) create mode 100644 public/blog/how-iroh-works/hole-punching-lan.svg create mode 100644 scripts/hole-punching-lan.gen.py diff --git a/public/blog/how-iroh-works/connect-by-key.svg b/public/blog/how-iroh-works/connect-by-key.svg index 04f8ad14..12083f02 100644 --- a/public/blog/how-iroh-works/connect-by-key.svg +++ b/public/blog/how-iroh-works/connect-by-key.svg @@ -38,6 +38,9 @@ @keyframes uni-arr { 0%, 35% { opacity: 0; } 37%, 42% { opacity: 1; } 44%, 100% { opacity: 0; } } /* Bidirectional arrow: 6s onwards */ @keyframes bi-arr { 0%, 42% { opacity: 0; } 44%, 100% { opacity: 1; } } + + .timer-fill { animation: timer-fill 14s infinite linear; transform-origin: left; transform: scaleX(0); } + @keyframes timer-fill { to { transform: scaleX(1); } } ]]> @@ -103,4 +106,9 @@ + + + + + diff --git a/public/blog/how-iroh-works/endpoint-startup.svg b/public/blog/how-iroh-works/endpoint-startup.svg index f3fcc4da..08f41b08 100644 --- a/public/blog/how-iroh-works/endpoint-startup.svg +++ b/public/blog/how-iroh-works/endpoint-startup.svg @@ -133,4 +133,11 @@ + + + + + + + diff --git a/public/blog/how-iroh-works/hole-punching-lan.svg b/public/blog/how-iroh-works/hole-punching-lan.svg new file mode 100644 index 00000000..25aef036 --- /dev/null +++ b/public/blog/how-iroh-works/hole-punching-lan.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + relay + + + + + + + + + + 192.168.0.1 + + + + + + + + + iroh + πŸ”‘ 1a9c… + Alice + + + EndpointId: 1a9c… + Addr: 192.168.0.3:2104 + Addr: 4.9.8.2:2104 + relay: us-east + + + + + + + + + iroh + πŸ”‘ 8e2b… + Bob + + + EndpointId: 8e2b… + Addr: 192.168.0.5:4153 + Addr: 4.9.8.2:4153 + relay: us-east + + + + + + + + + + + + ADD_ADDRESS 192.168.0.5:4153 + ADD_ADDRESS 4.9.8.2:4153 + + + + + + + + + Bob + Candidate: 192.168.0.5:4153 + Candidate: 4.9.8.2:4153 + + + + + + + + + + + + REACH_OUT192.168.0.3:2104 + REACH_OUT4.9.8.2:2104 + + + + + + + + + Alice + Candidate: 192.168.0.3:2104 + Candidate: 4.9.8.2:2104 + + + + + + + + + + + + + + + + + + + + + + direct (LAN) + + + + + PATH_CHALLENGE + 192.168.0.5:4153 + + + + + PATH_RESPONSE + PATH_CHALLENGE + 192.168.0.3:2104 + + + + + PATH_RESPONSE + + diff --git a/public/blog/how-iroh-works/hole-punching.svg b/public/blog/how-iroh-works/hole-punching.svg index 8b67071f..98decf45 100644 --- a/public/blog/how-iroh-works/hole-punching.svg +++ b/public/blog/how-iroh-works/hole-punching.svg @@ -253,4 +253,11 @@ PATH_RESPONSE + + + + + + + diff --git a/public/blog/how-iroh-works/publish-relay-dht.svg b/public/blog/how-iroh-works/publish-relay-dht.svg index 4177fb60..ee35e6a8 100644 --- a/public/blog/how-iroh-works/publish-relay-dht.svg +++ b/public/blog/how-iroh-works/publish-relay-dht.svg @@ -12,16 +12,16 @@ Mainline DHT - - - - - - - - - - + + + + + + + + + + @@ -141,4 +141,11 @@ ;; ANSWER SECTION: TXT "relay=https://us-east" + + + + + + + diff --git a/public/blog/how-iroh-works/publish-relay.svg b/public/blog/how-iroh-works/publish-relay.svg index be17cad1..6cf6556d 100644 --- a/public/blog/how-iroh-works/publish-relay.svg +++ b/public/blog/how-iroh-works/publish-relay.svg @@ -8,8 +8,8 @@ - - + + @@ -116,4 +116,11 @@ TXT "relay=https://us-east" + + + + + + + diff --git a/public/blog/how-iroh-works/routing-moves.svg b/public/blog/how-iroh-works/routing-moves.svg index 9efbfee7..fabe54ca 100644 --- a/public/blog/how-iroh-works/routing-moves.svg +++ b/public/blog/how-iroh-works/routing-moves.svg @@ -1,73 +1,15 @@ - - - + 11s Threshold 2: Bob is now closer to R3. R2 + multi R1β†’R2 fade out. R3 + multi R1β†’R3 fade in. + 11-15s Bob continues to final position. + 15-20s Static. --> + @@ -102,10 +44,13 @@ - - 10.0.0.7 - 192.168.1.7 - 172.16.0.7 + + + 10.0.0.7 + 192.168.1.7 + 172.16.0.7 @@ -116,8 +61,7 @@ - + - + marker-start="url(#arr)" marker-end="url(#arr)" opacity="0"> + - + marker-start="url(#arr)" marker-end="url(#arr)" opacity="0"> + @@ -165,7 +110,7 @@ home router - 203.0.113.1 + 203.0.113.1 @@ -176,11 +121,12 @@ - + + + mobile network - 198.51.100.1 - + 198.51.100.1 @@ -190,13 +136,22 @@ - + + + satellite internet - 100.64.10.1 - + 100.64.10.1 172.16.0.1 + + + + + + + + diff --git a/scripts/hole-punching-lan.gen.py b/scripts/hole-punching-lan.gen.py new file mode 100644 index 00000000..8e406b45 --- /dev/null +++ b/scripts/hole-punching-lan.gen.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +"""Generator for hole-punching-lan.svg ("Direct connections on the same network"). + +The easy, very common case: Alice and Bob are two devices on the *same* home +network, behind the *same* router. They still discover each other over the relay +(ADD_ADDRESS / REACH_OUT), but then the *local* candidate addresses are directly +reachable across the LAN β€” so the very first PATH_CHALLENGE to the local address +succeeds. No NAT hole to punch, no sacrificial probe, no public-address probes. + +Layout is a "Y": the relay sits on top of the stem (the router's uplink), the +router is the junction, Alice and Bob hang off the two legs. Data first flows up +the stem to the relay and back; once the LAN path validates, the stem fades to +gray and the two legs (the direct LAN path) carry everything. + +Same look as the other how-iroh-works figures. + +Edit this script and run: python3 hole-punching-lan.gen.py +Writes ../public/blog/how-iroh-works/hole-punching-lan.svg. +Keep the MDX
aspectRatio in sync with VB_W / VB_H. +""" + +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", "public", "blog", "how-iroh-works", "hole-punching-lan.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#374151", "#2563eb", "#dc2626", "#15803d" +FADED = "#aab2bd" # inactive/secondary connection β€” clearly lighter than the structure gray +CYCLE = 27.0 +T_POPUP_FADE = 23.5 # popups fade a little after the LAN path is validated + + +def anim_opacity(points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + vals = ";".join(str(v) for _, v in points) + return (f'') + + +def anim_motion(wire_id, points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + kp = ";".join(str(k) for _, k in points) + return (f'') + + +def dot(color, opa_pts, mot_pts, wire): + return f''' + + {anim_opacity(opa_pts)} + {anim_motion(wire, mot_pts)} + ''' + + +def phone(px0, py0, keytext): + pw, ph = 50, 92 + cx = px0+pw/2 + return f''' + + + + iroh + πŸ”‘ {keytext}''' + + +def router_lan(cx, cy, lan_ip): + # single home router; the stem (uplink to the relay) leaves the top center, + # so the LAN gateway IP goes below and there is no label above. + bx, by, bw, bh = cx-30, cy-20, 60, 40 + return f''' + + + + + + + {lan_ip} + ''' + + +def relay(cx, cy, label): + bx, by, bw, bh = cx-32, cy-9, 64, 18 + s = [' ', + f' ', + f' ', + f' ', + f' '] + for i in range(6): + vx = bx+24+i*5 + s.append(f' ') + s.append(f' {label}') + s.append(' ') + return "\n".join(s) + + +def facts(x, lines, anchor="start"): + out = [f' '] + for y, label, val in lines: + out.append(f' {label} {val}') + out.append(' ') + return "\n".join(out) + + +# ---- positions (a "Y": relay on top, router at the junction, phones on the legs) ---- +RELAY = (440, 66) +ROUTER = (440, 200) +A_PH = (175, 312) # Alice phone top-left +B_PH = (655, 312) # Bob phone top-left +acx, bcx = 200, 680 +PH = 92 +JX, JY = 440, 220 # junction (router bottom center) where the legs meet the stem +RELAY_B = 78 # relay box bottom + +# The legs curve: they leave each phone vertically and arrive at the router vertically +# (from below), so they flow smoothly into the vertical stem up to the relay. +DV, DV2 = 64, 88 # control-point offsets: vertical out of the phone / into the router +_legA = f"C {acx} {A_PH[1]-DV} {JX} {JY+DV2} {JX} {JY}" # phone A -> junction +_legB = f"C {bcx} {B_PH[1]-DV} {JX} {JY+DV2} {JX} {JY}" # phone B -> junction +_legA_r = f"C {JX} {JY+DV2} {acx} {A_PH[1]-DV} {acx} {A_PH[1]}" # junction -> phone A +_legB_r = f"C {JX} {JY+DV2} {bcx} {B_PH[1]-DV} {bcx} {B_PH[1]}" # junction -> phone B + +# stem (uplink to the relay); lan path (Alice -> router -> Bob) +stem_d = f"M {JX} {JY} L {JX} {RELAY_B}" +lan_d = f"M {acx} {A_PH[1]} {_legA} {_legB_r}" +# relay round-trips: up the stem to the relay and back down (the latency made visible) +relay_b2a = f"M {bcx} {B_PH[1]} {_legB} L {JX} {RELAY_B} L {JX} {JY} {_legA_r}" +relay_a2b = f"M {acx} {A_PH[1]} {_legA} L {JX} {RELAY_B} L {JX} {JY} {_legB_r}" + +alice_facts = facts(A_PH[0]+62, [ + (A_PH[1]+18, "EndpointId:", "1a9c…"), + (A_PH[1]+36, "Addr:", "192.168.0.3:2104"), + (A_PH[1]+54, "Addr:", "4.9.8.2:2104"), + (A_PH[1]+72, "relay:", "us-east"), +]) +bob_facts = facts(B_PH[0]+62, [ + (B_PH[1]+18, "EndpointId:", "8e2b…"), + (B_PH[1]+36, "Addr:", "192.168.0.5:4153"), + (B_PH[1]+54, "Addr:", "4.9.8.2:4153"), + (B_PH[1]+72, "relay:", "us-east"), +]) + +# ---- beat 1: ADD_ADDRESS (Bob advertises his candidates to Alice, over the relay) ---- +addaddr_pkt = dot(BLUE, + [(0, 0), (1.0, 0), (1.3, 1), (4.3, 1), (4.6, 0), (CYCLE, 0)], + [(0, 1), (1.0, 1), (4.5, 0), (CYCLE, 0)], "relay-b2a") +addaddr_co_pts = [(0, 0), (1.0, 0), (1.4, 1), (4.4, 1), (4.8, 0), (CYCLE, 0)] +addaddr_co = f''' + {anim_opacity(addaddr_co_pts)} + + ADD_ADDRESS 192.168.0.5:4153 + ADD_ADDRESS 4.9.8.2:4153 + ''' + +# Alice now knows Bob's candidates β€” ~2s popup beside Alice +ak_pts = [(0, 0), (4.4, 0), (4.7, 1), (6.6, 1), (6.9, 0), (CYCLE, 0)] +alice_knows = f''' + {anim_opacity(ak_pts)} + + + + + Bob + Candidate: 192.168.0.5:4153 + Candidate: 4.9.8.2:4153 + ''' + +# ---- beat 2: REACH_OUT (Alice -> Bob over the relay), carries Alice's candidates ---- +reachout_pkt = dot(BLUE, + [(0, 0), (7.5, 0), (7.8, 1), (10.8, 1), (11.1, 0), (CYCLE, 0)], + [(0, 0), (7.5, 0), (11.0, 1), (CYCLE, 1)], "relay-a2b") +reachout_co_pts = [(0, 0), (7.5, 0), (7.9, 1), (10.9, 1), (11.2, 0), (CYCLE, 0)] +reachout_co = f''' + {anim_opacity(reachout_co_pts)} + + REACH_OUT192.168.0.3:2104 + REACH_OUT4.9.8.2:2104 + ''' + +# Bob now knows Alice's candidates β€” ~2s popup beside Bob +bk_pts = [(0, 0), (11.0, 0), (11.3, 1), (13.2, 1), (13.5, 0), (CYCLE, 0)] +bob_knows = f''' + {anim_opacity(bk_pts)} + + + + + Alice + Candidate: 192.168.0.3:2104 + Candidate: 4.9.8.2:2104 + ''' + +# ============================ LAN hole punch (no NAT in the way) ============================ +# Same subnet, so the local candidate is directly reachable: the first PATH_CHALLENGE to the +# local address gets through. No sacrificial probe, no public probes, no drops. +# Alice is the client β†’ she probes first. Each step gets a clear window. +T_C1 = 14.2 # Alice PATH_CHALLENGE -> Bob (local) β€” gets through +T_C2 = 16.8 # Bob PATH_CHALLENGE + PATH_RESPONSE -> Alice +T_R = 19.4 # Alice PATH_RESPONSE -> Bob β€” both directions validated +T_BLUE = T_R + 2.2 + + +def hp_pkt(t0, dur, kp0, kp1, color=BLUE): + arr = t0 + dur + return f''' + + {anim_opacity([(0, 0), (t0, 0), (t0+0.15, 1), (arr-0.12, 1), (arr+0.06, 0), (CYCLE, 0)])} + {anim_motion("lan", [(0, kp0), (t0, kp0), (arr, kp1), (CYCLE, kp1)])} + ''' + + +def callout(side, lines, t0, t1, w=170): + x = 232 if side == "L" else (648 - w) + h = 18 + len(lines)*19 + pts = [(0, 0), (t0-0.3, 0), (t0, 1), (t1, 1), (t1+0.3, 0), (CYCLE, 0)] + rows = "\n".join( + f' {txt}' + for i, (txt, bold) in enumerate(lines)) + return f''' + {anim_opacity(pts)} + +{rows} + ''' + + +# beat 3 β€” Alice's PATH_CHALLENGE to Bob's local address: directly reachable, gets through +c1_pkt = hp_pkt(T_C1, 2.0, 0, 1) +c1_co = callout("L", [("PATH_CHALLENGE", True), ("192.168.0.5:4153", False)], T_C1-0.3, T_C1+2.1) + +# beat 4 β€” Bob answers: PATH_RESPONSE (to Alice's challenge) + his own PATH_CHALLENGE +c2_pkt = hp_pkt(T_C2, 2.0, 1, 0) +c2_co = callout("R", [("PATH_RESPONSE", True), ("PATH_CHALLENGE", True), ("192.168.0.3:2104", False)], T_C2-0.3, T_C2+2.1) + +# beat 5 β€” Alice's PATH_RESPONSE to Bob β€” both directions validated +r_pkt = hp_pkt(T_R, 2.0, 0, 1) +r_co = callout("L", [("PATH_RESPONSE", True)], T_R-0.3, T_R+2.1, w=150) + +# the stem (relay uplink) is blue/active until the LAN path validates, then fades to gray +stem_stroke = (f'') + +# "direct" confirmation appears when the path is validated +direct_pts = [(0, 0), (T_BLUE, 0), (T_BLUE+0.4, 1), (CYCLE, 1)] +direct_badge = f''' + {anim_opacity(direct_pts)} + + direct (LAN) + ''' + +VB_X, VB_Y, VB_W, VB_H = 0, 30, 940, 430 +svg = f''' + + {stem_stroke} + + + + + + +{relay(*RELAY, "relay")} + +{router_lan(*ROUTER, "192.168.0.1")} + + + +{phone(*A_PH, "1a9c…")} + Alice + +{alice_facts} + + + +{phone(*B_PH, "8e2b…")} + Bob + +{bob_facts} + +{addaddr_pkt} + +{addaddr_co} + +{alice_knows} + +{reachout_pkt} + +{reachout_co} + +{bob_knows} + + +{c1_pkt} +{c2_pkt} +{r_pkt} +{direct_badge} +{c1_co} +{c2_co} +{r_co} + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/src/app/blog/how-iroh-works/page.mdx b/src/app/blog/how-iroh-works/page.mdx index c571af9f..4defef49 100644 --- a/src/app/blog/how-iroh-works/page.mdx +++ b/src/app/blog/how-iroh-works/page.mdx @@ -58,10 +58,15 @@ We make sure that you do get a connection whenever possible. And we make sure th When conditions change, we immediately react and choose the new best path. This is transparent to the user. -
- - Alice and Bob connected via a shared router, with two more routers available for when Bob moves - +
+ +
+ + Alice and Bob connected via a shared router, with two more routers available for when Bob moves + +
# So how this it actually work? @@ -86,36 +91,51 @@ Iroh works just fine with a single relay, but works best if you have relays in a ## Determining the home relay -
- - A map of the US and Europe with iroh relays in us-west (Seattle), us-east (Delaware), and eu-west (Frankfurt), and an endpoint in Florida - +
+ +
+ + A map of the US and Europe with iroh relays in us-west (Seattle), us-east (Delaware), and eu-west (Frankfurt), and an endpoint in Florida + +
On startup, an iroh endpoint sends [QAD] probes to all configured relays. This serves two purposes: learning our own public IP address as well as learning which relay is closest in terms of latency. The closest relay becomes our home relay. We keep a secure websocket connection open to our home relay. ## Publishing the home relay -If we had only one relay, we would be done now. But frequently we have multiple relays. Alice needs to know which relay to use to talk to Bob. For this we have two mechanisms +Iroh works just fine with one relay. But frequently we have multiple relays. Alice needs to know which relay to use to talk to Bob. For this we have two mechanisms ### DNS based address lookup We offer a mechanism based on DNS out of the box. Bob publishes a signed DNS record to a DNS server operated by number0 via a https PUT request. Alice or whoever wants to talk to bob then looks up this record using a DNS query or a https query. -
- - Bob publishes a signed DNS record with his home relay to dns.iroh.link via an HTTPS PUT; Alice resolves it with a DNS lookup - +
+ +
+ + Bob publishes a signed DNS record with his home relay to dns.iroh.link via an HTTPS PUT; Alice resolves it with a DNS lookup + +
### Mainline DHT based address lookup The above mechanism works fine in most cases. But maybe you want something that is fully peer to peer. For that case we offer an optional DHT based address lookup mechanism. We publish the exact same record as before on the mainline DHT, using the [bep_0044] extension. -
- - Bob publishes his signed record to several random nodes of the Mainline DHT; Alice resolves it by querying several random nodes - +
+ +
+ + Bob publishes his signed record to several random nodes of the Mainline DHT; Alice resolves it by querying several random nodes + +
@@ -129,10 +149,31 @@ How do we make the connections fast and direct? This is aguably the most complex Hole punching happens *inside* the QUIC connection via an extension `n0_nat_traversal`. It is inspired by the [QUIC NAT traversal draft][quic-nat-traversal], but uses its own transport parameter ID. -
- - Alice and Bob, each behind a home router, connected through the relay β€” all data flows up to the relay and back down - +
+ +
+ + Alice and Bob, each behind a home router, connected through the relay β€” all data flows up to the relay and back down + +
+
+ + +## Local direct connections + +If two devices are in the same network, hole punching is not needed. Each side just needs to learn the local address of the remote. + +
+ +
+ + Alice and Bob on the same home network discover each other over the relay, then connect directly across the LAN β€” the relay uplink fades away once the local path is validated + +
From c63d68ed3f82e556ccde36d0c9d2ef12b58ab2ec Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Thu, 18 Jun 2026 12:01:03 +0300 Subject: [PATCH 04/11] typo --- src/app/blog/how-iroh-works/page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/blog/how-iroh-works/page.mdx b/src/app/blog/how-iroh-works/page.mdx index 4defef49..e51ab255 100644 --- a/src/app/blog/how-iroh-works/page.mdx +++ b/src/app/blog/how-iroh-works/page.mdx @@ -81,7 +81,7 @@ The iroh library itself provides an API to create endpoints and connect to other ## Iroh relay -The iroh relays work in the background on a publicly reachable servers to faciliate direct connections and to relay data in case establishing a direct connection is not possible. +The iroh relays work in the background on a publicly reachable servers to facilitate direct connections and to relay data in case establishing a direct connection is not possible. # Endpoint startup From 349e333ebd19395b3040e9edc32275befaafa6d2 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Thu, 18 Jun 2026 16:45:28 +0300 Subject: [PATCH 05/11] WIP --- .../blog/how-iroh-works/hole-punching-lan.svg | 8 +++---- public/blog/how-iroh-works/hole-punching.svg | 3 +-- .../blog/how-iroh-works/publish-relay-dht.svg | 3 +-- public/blog/how-iroh-works/publish-relay.svg | 3 +-- scripts/hole-punching-lan.gen.py | 22 +++++++++++-------- src/app/blog/how-iroh-works/page.mdx | 2 +- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/public/blog/how-iroh-works/hole-punching-lan.svg b/public/blog/how-iroh-works/hole-punching-lan.svg index 25aef036..72c9528d 100644 --- a/public/blog/how-iroh-works/hole-punching-lan.svg +++ b/public/blog/how-iroh-works/hole-punching-lan.svg @@ -1,11 +1,11 @@ - + - + - - + + diff --git a/public/blog/how-iroh-works/hole-punching.svg b/public/blog/how-iroh-works/hole-punching.svg index 98decf45..60159e2e 100644 --- a/public/blog/how-iroh-works/hole-punching.svg +++ b/public/blog/how-iroh-works/hole-punching.svg @@ -49,8 +49,7 @@ - - + iroh πŸ”‘ 1a9c… diff --git a/public/blog/how-iroh-works/publish-relay-dht.svg b/public/blog/how-iroh-works/publish-relay-dht.svg index ee35e6a8..ca52e1f8 100644 --- a/public/blog/how-iroh-works/publish-relay-dht.svg +++ b/public/blog/how-iroh-works/publish-relay-dht.svg @@ -57,8 +57,7 @@ - - + iroh πŸ”‘ 1a9c… diff --git a/public/blog/how-iroh-works/publish-relay.svg b/public/blog/how-iroh-works/publish-relay.svg index 6cf6556d..371d4cbc 100644 --- a/public/blog/how-iroh-works/publish-relay.svg +++ b/public/blog/how-iroh-works/publish-relay.svg @@ -71,8 +71,7 @@ - - + iroh πŸ”‘ 1a9c… diff --git a/scripts/hole-punching-lan.gen.py b/scripts/hole-punching-lan.gen.py index 8e406b45..4594eabd 100644 --- a/scripts/hole-punching-lan.gen.py +++ b/scripts/hole-punching-lan.gen.py @@ -114,19 +114,23 @@ def facts(x, lines, anchor="start"): RELAY_B = 78 # relay box bottom # The legs curve: they leave each phone vertically and arrive at the router vertically -# (from below), so they flow smoothly into the vertical stem up to the relay. +# (from below). They enter the router box at two *separate* points tucked inside it, so +# the legs never cross; the vertical stem leaves the box top center up to the relay. DV, DV2 = 64, 88 # control-point offsets: vertical out of the phone / into the router -_legA = f"C {acx} {A_PH[1]-DV} {JX} {JY+DV2} {JX} {JY}" # phone A -> junction -_legB = f"C {bcx} {B_PH[1]-DV} {JX} {JY+DV2} {JX} {JY}" # phone B -> junction -_legA_r = f"C {JX} {JY+DV2} {acx} {A_PH[1]-DV} {acx} {A_PH[1]}" # junction -> phone A -_legB_r = f"C {JX} {JY+DV2} {bcx} {B_PH[1]-DV} {bcx} {B_PH[1]}" # junction -> phone B +OJ = 16 # legs enter the box this far either side of center +JYH = 213 # internal junction, hidden inside the router box +JXA, JXB = JX-OJ, JX+OJ +_legA = f"C {acx} {A_PH[1]-DV} {JXA} {JYH+DV2} {JXA} {JYH}" +_legB = f"C {bcx} {B_PH[1]-DV} {JXB} {JYH+DV2} {JXB} {JYH}" +_legA_r = f"C {JXA} {JYH+DV2} {acx} {A_PH[1]-DV} {acx} {A_PH[1]}" +_legB_r = f"C {JXB} {JYH+DV2} {bcx} {B_PH[1]-DV} {bcx} {B_PH[1]}" # stem (uplink to the relay); lan path (Alice -> router -> Bob) -stem_d = f"M {JX} {JY} L {JX} {RELAY_B}" -lan_d = f"M {acx} {A_PH[1]} {_legA} {_legB_r}" +stem_d = f"M {JX} {JYH} L {JX} {RELAY_B}" +lan_d = f"M {acx} {A_PH[1]} {_legA} L {JXB} {JYH} {_legB_r}" # relay round-trips: up the stem to the relay and back down (the latency made visible) -relay_b2a = f"M {bcx} {B_PH[1]} {_legB} L {JX} {RELAY_B} L {JX} {JY} {_legA_r}" -relay_a2b = f"M {acx} {A_PH[1]} {_legA} L {JX} {RELAY_B} L {JX} {JY} {_legB_r}" +relay_b2a = f"M {bcx} {B_PH[1]} {_legB} L {JX} {JYH} L {JX} {RELAY_B} L {JX} {JYH} L {JXA} {JYH} {_legA_r}" +relay_a2b = f"M {acx} {A_PH[1]} {_legA} L {JX} {JYH} L {JX} {RELAY_B} L {JX} {JYH} L {JXB} {JYH} {_legB_r}" alice_facts = facts(A_PH[0]+62, [ (A_PH[1]+18, "EndpointId:", "1a9c…"), diff --git a/src/app/blog/how-iroh-works/page.mdx b/src/app/blog/how-iroh-works/page.mdx index e51ab255..0e9f64a5 100644 --- a/src/app/blog/how-iroh-works/page.mdx +++ b/src/app/blog/how-iroh-works/page.mdx @@ -52,7 +52,7 @@ Iroh endpoints are identified by a cryptographic key. This allows us to connect
-## Guaranteed connections +## Reliable connections We make sure that you do get a connection whenever possible. And we make sure that the connection is as fast as possible. From 0b989e35ac98f593761ab463aa13c18284804e6d Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Fri, 19 Jun 2026 15:00:00 +0300 Subject: [PATCH 06/11] Make sure SVGs also work reasoably well in dark mode. Also add a temporary dark mode toggle and correct some details in the hole punching animation --- public/blog/how-iroh-works/connect-by-key.svg | 40 ++- .../blog/how-iroh-works/embedding-phone.svg | 45 +-- .../blog/how-iroh-works/endpoint-startup.svg | 64 ++-- .../blog/how-iroh-works/hole-punching-lan.svg | 121 +++---- public/blog/how-iroh-works/hole-punching.svg | 179 +++++----- .../blog/how-iroh-works/publish-relay-dht.svg | 89 +++-- public/blog/how-iroh-works/publish-relay.svg | 51 ++- public/blog/how-iroh-works/routing-moves.svg | 47 ++- scripts/connect-by-key.gen.py | 226 +++++++++++++ scripts/endpoint-startup.gen.py | 91 +++-- scripts/hole-punching-lan.gen.py | 104 ++++-- scripts/hole-punching.gen.py | 118 +++++-- scripts/publish-relay-dht.gen.py | 88 ++++- scripts/publish-relay.gen.py | 86 ++++- scripts/routing-moves.gen.py | 315 ++++++++++++++++++ src/app/providers.jsx | 4 +- src/components/BlogPostLayout.jsx | 3 + src/components/TempDarkToggle.jsx | 37 ++ src/styles/tailwind.css | 1 + 19 files changed, 1277 insertions(+), 432 deletions(-) create mode 100644 scripts/connect-by-key.gen.py create mode 100644 scripts/routing-moves.gen.py create mode 100644 src/components/TempDarkToggle.jsx diff --git a/public/blog/how-iroh-works/connect-by-key.svg b/public/blog/how-iroh-works/connect-by-key.svg index 12083f02..f686ceed 100644 --- a/public/blog/how-iroh-works/connect-by-key.svg +++ b/public/blog/how-iroh-works/connect-by-key.svg @@ -1,4 +1,5 @@ + + + + + + + + + 10.0.0.42 + +{chr(10).join(ip_rows)} + + +{alice_iphone()} + + +{bob_android()} + + + Alice + Bob + + + + + connect + + + + + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/endpoint-startup.gen.py b/scripts/endpoint-startup.gen.py index d9b07eb9..5a865333 100644 --- a/scripts/endpoint-startup.gen.py +++ b/scripts/endpoint-startup.gen.py @@ -21,12 +21,42 @@ import math import os +CYCLE = 14.0 # one loop; everything resets afterwards + HERE = os.path.dirname(os.path.abspath(__file__)) LAND_PATH = os.path.join(HERE, "endpoint-startup.land.txt") OUT_PATH = os.path.normpath(os.path.join( HERE, "..", "public", "blog", "how-iroh-works", "endpoint-startup.svg")) +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the πŸ”‘ emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + land = open(LAND_PATH).read() # ---- projection: plate carrΓ©e, standard parallel 45Β°, US + Europe window ---- @@ -56,7 +86,7 @@ def relay(cx, cy, label, ip): # location dot s.append(f' ') # pizzabox body - s.append(f' ') + s.append(f' ') # front bezel split line s.append(f' ') # LEDs @@ -107,12 +137,12 @@ def q(t, a, b, c): return (1-t)**2*a + 2*(1-t)*t*b + t**2*c - - + + - + iroh - πŸ”‘ 8e2b… + {key_label(bcx, by0+58, "8e2b…", 8, "#d97706")} ''' uwx, uwy = round(proj(*seattle)[0]), round(proj(*seattle)[1]) @@ -143,11 +173,16 @@ def q(t, a, b, c): return (1-t)**2*a + 2*(1-t)*t*b + t**2*c START = 3.0 # packets launch after the wires have faded in def packet(wid, length): dur = 2*length/SPEED # there and back + t0 = START / CYCLE + t1 = (START + dur*0.04) / CYCLE + tmid = (START + dur*0.5) / CYCLE + t2 = (START + dur*0.96) / CYCLE + t3 = (START + dur) / CYCLE + opa = f'values="0;0;1;1;1;0;0" keyTimes="0;{t0:.4f};{t1:.4f};{tmid:.4f};{t2:.4f};{t3:.4f};1"' + mot = f'keyTimes="0;{t0:.4f};{tmid:.4f};{t3:.4f};1" keyPoints="0;0;1;0;0"' return f''' - - + + ''' @@ -163,46 +198,57 @@ def packet(wid, length): def ret_time(length): return START + 2*length/SPEED # packet is back at Bob at START + round-trip dur def readline(y, label, ms, begin): - return (f' ' + t0 = begin / CYCLE + t1 = (begin + 0.4) / CYCLE + return (f' ' f'{label}: {ms} ms' - f'') + f'') readout = f''' - + {readline(268, "us-east", 19, ret_time(L_ue))} {readline(294, "us-west", 71, ret_time(L_uw))} {readline(320, "eu-west", 102, ret_time(L_ew))} - + ''' # When the last response is in, us-east wins: overlay its wire in connection-blue, # then keep it gently pulsing to show the persistent home-relay connection. -home = f''' +_rt = ret_time(L_ew) +home = f''' - - + ''' # The endpoint's public IP appears just below the phone on the first response. +_pt0 = ret_time(L_ue) / CYCLE +_pt1 = (ret_time(L_ue) + 0.4) / CYCLE phone_ip = f''' - {ip_bob}''' + {ip_bob}''' # Cropped viewport: trim the empty Arctic (top) and eastern Europe (right) so the # relevant US <-> western-Europe band fills the frame. The land path is unchanged; # this just clips it. Keep the MDX
aspectRatio in sync with VB_W / VB_H. VB_X, VB_Y, VB_W, VB_H = 0, 92, 705, 280 svg = f''' + - + {wires} @@ -221,6 +267,11 @@ def readline(y, label, ms, begin): {home} {readout} + + + + + ''' diff --git a/scripts/hole-punching-lan.gen.py b/scripts/hole-punching-lan.gen.py index 4594eabd..f25611f0 100644 --- a/scripts/hole-punching-lan.gen.py +++ b/scripts/hole-punching-lan.gen.py @@ -26,7 +26,7 @@ HERE, "..", "public", "blog", "how-iroh-works", "hole-punching-lan.svg")) MONO = "'Space Mono', monospace" -INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#374151", "#2563eb", "#dc2626", "#15803d" +INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#111", "#2563eb", "#dc2626", "#15803d" FADED = "#aab2bd" # inactive/secondary connection β€” clearly lighter than the structure gray CYCLE = 27.0 T_POPUP_FADE = 23.5 # popups fade a little after the LAN path is validated @@ -54,15 +54,69 @@ def dot(color, opa_pts, mot_pts, wire): ''' -def phone(px0, py0, keytext): +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the πŸ”‘ emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_screen(px0, py0, pw, ph, indigo): + """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" + sx0, sx1 = px0+3, px0+pw-3 + sy0, sy1 = py0+3, py0+ph-3 + cx = px0 + pw/2 + no = 7 # outer notch half-width + ni = 4 # inner notch half-width + nd = 5 # notch depth + ny = sy0 + nd + r = 6 # screen corner radius + return ( + f'' + ) + +def phone(px0, py0, keytext, iphone=False): pw, ph = 50, 92 cx = px0+pw/2 - return f''' - - - + if iphone: + screen = iphone_screen(px0, py0, pw, ph, INDIGO) + else: + screen = f'\n ' + return f''' + {screen} + iroh - πŸ”‘ {keytext}''' + {key_label(cx, py0+60, keytext, 8, AMBER)}''' def router_lan(cx, cy, lan_ip): @@ -74,7 +128,7 @@ def router_lan(cx, cy, lan_ip): - + {lan_ip} ''' @@ -83,7 +137,7 @@ def router_lan(cx, cy, lan_ip): def relay(cx, cy, label): bx, by, bw, bh = cx-32, cy-9, 64, 18 s = [' ', - f' ', + f' ', f' ', f' ', f' '] @@ -152,7 +206,7 @@ def facts(x, lines, anchor="start"): addaddr_co_pts = [(0, 0), (1.0, 0), (1.4, 1), (4.4, 1), (4.8, 0), (CYCLE, 0)] addaddr_co = f''' {anim_opacity(addaddr_co_pts)} - + ADD_ADDRESS 192.168.0.5:4153 ADD_ADDRESS 4.9.8.2:4153 ''' @@ -177,7 +231,7 @@ def facts(x, lines, anchor="start"): reachout_co_pts = [(0, 0), (7.5, 0), (7.9, 1), (10.9, 1), (11.2, 0), (CYCLE, 0)] reachout_co = f''' {anim_opacity(reachout_co_pts)} - + REACH_OUT192.168.0.3:2104 REACH_OUT4.9.8.2:2104 ''' @@ -224,7 +278,7 @@ def callout(side, lines, t0, t1, w=170): for i, (txt, bold) in enumerate(lines)) return f''' {anim_opacity(pts)} - + {rows} ''' @@ -256,6 +310,7 @@ def callout(side, lines, t0, t1, w=170): VB_X, VB_Y, VB_W, VB_H = 0, 30, 940, 430 svg = f''' + {stem_stroke} @@ -270,7 +325,7 @@ def callout(side, lines, t0, t1, w=170): -{phone(*A_PH, "1a9c…")} +{phone(*A_PH, "1a9c…", iphone=True)} Alice {alice_facts} @@ -282,26 +337,27 @@ def callout(side, lines, t0, t1, w=170): {bob_facts} + {addaddr_pkt} - -{addaddr_co} - -{alice_knows} - {reachout_pkt} - -{reachout_co} - -{bob_knows} - - {c1_pkt} {c2_pkt} {r_pkt} {direct_badge} + + +{addaddr_co} +{alice_knows} +{reachout_co} +{bob_knows} {c1_co} {c2_co} {r_co} + + + + + ''' diff --git a/scripts/hole-punching.gen.py b/scripts/hole-punching.gen.py index 51feafcb..9ae8bc56 100644 --- a/scripts/hole-punching.gen.py +++ b/scripts/hole-punching.gen.py @@ -21,7 +21,7 @@ HERE, "..", "public", "blog", "how-iroh-works", "hole-punching.svg")) MONO = "'Space Mono', monospace" -INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#374151", "#2563eb", "#dc2626", "#15803d" +INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#111", "#2563eb", "#dc2626", "#15803d" FADED = "#aab2bd" # inactive/secondary connection β€” clearly lighter than the structure gray CYCLE = 39.0 # one loop; popups fade after both checks are green, then the direct path stands alone T_POPUP_FADE = 29.5 # NAT bubbles fade out here (a few seconds after both mappings are validated) @@ -49,15 +49,69 @@ def dot(color, opa_pts, mot_pts, wire="relay-path"): ''' -def phone(px0, py0, keytext): +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the πŸ”‘ emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_screen(px0, py0, pw, ph, indigo): + """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" + sx0, sx1 = px0+3, px0+pw-3 + sy0, sy1 = py0+3, py0+ph-3 + cx = px0 + pw/2 + no = 7 # outer notch half-width + ni = 4 # inner notch half-width + nd = 5 # notch depth + ny = sy0 + nd + r = 6 # screen corner radius + return ( + f'' + ) + +def phone(px0, py0, keytext, iphone=False): pw, ph = 50, 92 cx = px0+pw/2 - return f''' - - - + if iphone: + screen = iphone_screen(px0, py0, pw, ph, INDIGO) + else: + screen = f'\n ' + return f''' + {screen} + iroh - πŸ”‘ {keytext}''' + {key_label(cx, py0+60, keytext, 8, AMBER)}''' def router(cx, cy, pub_ip, lan_ip): @@ -70,7 +124,7 @@ def router(cx, cy, pub_ip, lan_ip): - + {lan_ip} ''' @@ -79,7 +133,7 @@ def router(cx, cy, pub_ip, lan_ip): def relay(cx, cy, label): bx, by, bw, bh = cx-32, cy-9, 64, 18 s = [' ', - f' ', + f' ', f' ', f' ', f' '] @@ -142,7 +196,7 @@ def facts(x, lines, anchor="start"): addaddr_co_pts = [(0, 0), (1.0, 0), (1.4, 1), (4.0, 1), (4.4, 0), (CYCLE, 0)] addaddr_co = f''' {anim_opacity(addaddr_co_pts)} - + ADD_ADDRESS 192.168.0.3:4153 ADD_ADDRESS 4.9.8.2:4153 ''' @@ -169,7 +223,7 @@ def facts(x, lines, anchor="start"): reachout_co_pts = [(0, 0), (6.8, 0), (7.1, 1), (9.3, 1), (9.6, 0), (CYCLE, 0)] reachout_co = f''' {anim_opacity(reachout_co_pts)} - + REACH_OUT10.0.0.3:2104 REACH_OUT8.3.1.9:2104 ''' @@ -200,9 +254,9 @@ def facts(x, lines, anchor="start"): T_BR = 25.7 # Bob reply: PATH_RESPONSE β†’ validated T_BLUE = T_BR + 2.4 # direct path turns blue once the response validates it -DROP_A, DROP_B = (382, 200), (498, 200) # local drops, band above the routers -probe_local_a_d = f"M {acx} {A_PH[1]} L {acx} {R1[1]-20} C {acx} 205 300 200 {DROP_A[0]} {DROP_A[1]}" -probe_local_b_d = f"M {bcx} {B_PH[1]} L {bcx} {R2[1]-20} C {bcx} 205 580 200 {DROP_B[0]} {DROP_B[1]}" +DROP_A, DROP_B = (acx, 200), (bcx, 200) # local drops β€” straight up above each router (no bend toward the other side, which read as if the packet was trying to reach the peer) +probe_local_a_d = f"M {acx} {A_PH[1]} L {acx} {DROP_A[1]}" +probe_local_b_d = f"M {bcx} {B_PH[1]} L {bcx} {DROP_B[1]}" # the one direct (internet) route, Alice(0) -> Bob(1). Every public packet rides it. ARC = 158 @@ -259,7 +313,7 @@ def callout(side, lines, t0, t1, w=164): for i, (txt, bold) in enumerate(lines)) return f''' {anim_opacity(pts)} - + {rows} ''' @@ -291,7 +345,7 @@ def nat_bubble(side, l1, l2, t_on, t_used): - + {anim_opacity(hg_pts)} @@ -344,13 +398,14 @@ def nat_bubble(side, l1, l2, t_on, t_used): VB_X, VB_Y, VB_W, VB_H = 0, 30, 940, 430 svg = f''' + {relay_stroke} {direct_path} - - - + + + @@ -363,7 +418,7 @@ def nat_bubble(side, l1, l2, t_on, t_used): -{phone(*A_PH, "1a9c…")} +{phone(*A_PH, "1a9c…", iphone=True)} Alice {alice_facts} @@ -375,19 +430,9 @@ def nat_bubble(side, l1, l2, t_on, t_used): {bob_facts} + {addaddr_pkt} - -{addaddr_co} - -{alice_knows} - {reachout_pkt} - -{reachout_co} - -{bob_knows} - - {loc_a_pkt} {loc_b_pkt} {pub_a_pkt} @@ -399,12 +444,23 @@ def nat_bubble(side, l1, l2, t_on, t_used): {pub_a_drop} {nat_a} {nat_b} + + +{addaddr_co} +{alice_knows} +{reachout_co} +{bob_knows} {loc_a_co} {loc_b_co} {pub_a_co} {pub_b_co} {reply_co} {resp_co} + + + + + ''' diff --git a/scripts/publish-relay-dht.gen.py b/scripts/publish-relay-dht.gen.py index 206f41b9..f8179fa8 100644 --- a/scripts/publish-relay-dht.gen.py +++ b/scripts/publish-relay-dht.gen.py @@ -26,7 +26,7 @@ HERE, "..", "public", "blog", "how-iroh-works", "publish-relay-dht.svg")) MONO = "'Space Mono', monospace" -INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#374151" +INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#111" GREEN, BLUE, RED = "#15803d", "#2563eb", "#dc2626" SPEED = 150.0 # px/second β€” same for every dot @@ -50,15 +50,69 @@ def q(t, a, b, c): return (1-t)**2*a + 2*(1-t)*t*b + t**2*c return d, length -def phone(px0, py0, keytext): +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the πŸ”‘ emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_screen(px0, py0, pw, ph, indigo): + """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" + sx0, sx1 = px0+3, px0+pw-3 + sy0, sy1 = py0+3, py0+ph-3 + cx = px0 + pw/2 + no = 7 # outer notch half-width + ni = 4 # inner notch half-width + nd = 5 # notch depth + ny = sy0 + nd + r = 6 # screen corner radius + return ( + f'' + ) + +def phone(px0, py0, keytext, iphone=False): pw, ph = 48, 88 cx = px0+pw/2 - return f''' - - - + if iphone: + screen = iphone_screen(px0, py0, pw, ph, INDIGO) + else: + screen = f'\n ' + return f''' + {screen} + iroh - πŸ”‘ {keytext}''' + {key_label(cx, py0+58, keytext, 8, AMBER)}''' # ============================ geometry ============================ @@ -139,7 +193,7 @@ def answer_dot(wire_id, color_down, launch, reach, back): # ============================ static backdrop ============================ cloud = [' ', ' '] for cx, cy, r in cloud_circles: - cloud.append(f' ') + cloud.append(f' ') cloud.append(f' Mainline DHT') cloud.append(' ') cloud = "\n".join(cloud) @@ -170,7 +224,7 @@ def answer_dot(wire_id, color_down, launch, reach, back): alice = f''' {anim_opacity(alice_pts)} -{phone(ax0, ay0, "1a9c…")} +{phone(ax0, ay0, "1a9c…", iphone=True)} Alice 8e2b… is at relay us-east{anim_opacity(result_pts)}''' @@ -180,12 +234,12 @@ def answer_dot(wire_id, color_down, launch, reach, back): look_wire_pts = [(0, 0), (LOOK_LAUNCH-0.6, 0), (LOOK_LAUNCH, 1), (LAST_RETURN, 1), (LAST_RETURN+0.6, 0), (CYCLE, 0)] wires, packets = [], [] for i, (d, arr) in enumerate(pub): - wires.append(f' {anim_opacity(pub_wire_pts)}') + wires.append(f' {anim_opacity(pub_wire_pts)}') opa = [(0, 0), (PUB_LAUNCH, 0), (PUB_LAUNCH+0.15, 1), (arr-0.05, 1), (arr+0.1, 0), (CYCLE, 0)] mot = [(0, 0), (PUB_LAUNCH, 0), (arr, 1), (CYCLE, 1)] packets.append(dot(f"pw{i}", BLUE, opa, mot)) for i, (d, reach, back, ok) in enumerate(look): - wires.append(f' {anim_opacity(look_wire_pts)}') + wires.append(f' {anim_opacity(look_wire_pts)}') packets.append(answer_dot(f"lw{i}", GREEN if ok else RED, LOOK_LAUNCH, reach, back)) wires = "\n".join(wires) packets = "\n".join(packets) @@ -195,7 +249,7 @@ def answer_dot(wire_id, color_down, launch, reach, back): pub_co = f''' {anim_opacity(pub_co_pts)} - + DHT put 8e2b… Relay: us-east @@ -206,7 +260,7 @@ def answer_dot(wire_id, color_down, launch, reach, back): read_co = f''' {anim_opacity(read_pts)} - + DHT get 8e2b… ''' @@ -216,13 +270,14 @@ def answer_dot(wire_id, color_down, launch, reach, back): ans_co = f''' {anim_opacity(ans_pts)} - + ;; ANSWER SECTION: TXT "relay=https://us-east" ''' VB_X, VB_Y, VB_W, VB_H = 0, 44, 820, 376 svg = f''' + {cloud} {wires} @@ -240,6 +295,11 @@ def answer_dot(wire_id, color_down, launch, reach, back): {read_co} {ans_co} + + + + + ''' diff --git a/scripts/publish-relay.gen.py b/scripts/publish-relay.gen.py index 1d60f4b2..c43162f2 100644 --- a/scripts/publish-relay.gen.py +++ b/scripts/publish-relay.gen.py @@ -24,7 +24,7 @@ HERE, "..", "public", "blog", "how-iroh-works", "publish-relay.svg")) MONO = "'Space Mono', monospace" -INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#374151" +INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#111" GREEN, BLUE = "#15803d", "#2563eb" CYCLE = 14.5 # seconds for one full loop (~5s static hold) @@ -55,15 +55,69 @@ def arc(x1, y1, x2, y2, k=0.16): return f"M {x1:.0f} {y1:.0f} Q {cx:.0f} {cy:.0f} {x2:.0f} {y2:.0f}" -def phone(px0, py0, keytext): +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the πŸ”‘ emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_screen(px0, py0, pw, ph, indigo): + """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" + sx0, sx1 = px0+3, px0+pw-3 + sy0, sy1 = py0+3, py0+ph-3 + cx = px0 + pw/2 + no = 7 # outer notch half-width + ni = 4 # inner notch half-width + nd = 5 # notch depth + ny = sy0 + nd + r = 6 # screen corner radius + return ( + f'' + ) + +def phone(px0, py0, keytext, iphone=False): pw, ph = 48, 88 cx = px0+pw/2 - return f''' - - - + if iphone: + screen = iphone_screen(px0, py0, pw, ph, INDIGO) + else: + screen = f'\n ' + return f''' + {screen} + iroh - πŸ”‘ {keytext}''' + {key_label(cx, py0+58, keytext, 8, AMBER)}''' def doc_packet(wire_id, color, opa_pts, mot_pts): @@ -81,7 +135,7 @@ def doc_packet(wire_id, color, opa_pts, mot_pts): sx, sy, sw, sh = 378, 56, 64, 66 scx = sx+sw/2 srv = [' ', ' ', - f' '] + f' '] for i in range(3): ry = sy + 4 + i*21 srv.append(f' ') @@ -133,7 +187,7 @@ def doc_packet(wire_id, color, opa_pts, mot_pts): alice = f''' {anim_opacity(alice_pts)} -{phone(ax0, ay0, "1a9c…")} +{phone(ax0, ay0, "1a9c…", iphone=True)} Alice @@ -144,8 +198,8 @@ def doc_packet(wire_id, color, opa_pts, mot_pts): lookup_d = arc(sx+sw-18, sy+sh, acx, ay0, k=0.18) # server -> Alice (answer direction) put_wire_pts = [(0, 0), (0.6, 0), (1.0, 1), (4.0, 1), (4.5, 0), (CYCLE, 0)] lookup_wire_pts = [(0, 0), (4.4, 0), (5.0, 1), (8.3, 1), (8.8, 0), (CYCLE, 0)] -wires = f''' {anim_opacity(put_wire_pts)} - {anim_opacity(lookup_wire_pts)}''' +wires = f''' {anim_opacity(put_wire_pts)} + {anim_opacity(lookup_wire_pts)}''' # ============================ packets ============================ # PUT: Bob(0) -> server(1), fades out as it lands (~3.5s) @@ -162,7 +216,7 @@ def doc_packet(wire_id, color, opa_pts, mot_pts): put_co = f''' {anim_opacity(put_co_pts)} - + HTTPS PUT Relay: us-east Signed by: 8e2b… @@ -175,7 +229,7 @@ def doc_packet(wire_id, color, opa_pts, mot_pts): lookup_co = f''' {anim_opacity(lookup_co_pts)} - + {anim_opacity(query_pts)} DNS LOOKUP @@ -190,6 +244,7 @@ def doc_packet(wire_id, color, opa_pts, mot_pts): VB_X, VB_Y, VB_W, VB_H = 0, 44, 820, 376 svg = f''' + @@ -215,6 +270,11 @@ def doc_packet(wire_id, color, opa_pts, mot_pts): {put_co} {lookup_co} + + + + + ''' diff --git a/scripts/routing-moves.gen.py b/scripts/routing-moves.gen.py new file mode 100644 index 00000000..36706d7a --- /dev/null +++ b/scripts/routing-moves.gen.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +"""Generator for routing-moves.svg. + +Bob's commute story: home wifi (R1) β†’ cellular (R2, cell tower) β†’ in-flight +(R3, Starlink ground station). Alice stays put; the connection follows Bob. +20s loop, all SMIL so every element shares one timeline (no CSS/SMIL drift). + +To change it, edit this script and run: python3 routing-moves.gen.py +It writes ../public/blog/how-iroh-works/routing-moves.svg. +Keep the MDX
aspectRatio in sync with VB_W / VB_H. +""" +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", "public", "blog", "how-iroh-works", "routing-moves.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY = "#6366f1", "#d97706", "#888" +CYCLE = 20 # seconds + +VB_W, VB_H = 800, 380 + +# Animation thresholds (fractions of the cycle): +# 0.25 β€” Bob starts moving (and R2 fades in) +# 0.30 β€” single-bend path swaps to multi R1β†’R2 +# 0.55 β€” R2 + multi R1β†’R2 fade out +# 0.57 β€” R3 + multi R1β†’R3 fade in +# 0.75 β€” Bob reaches final position + + +def key_label(cx, y, text, size, color): + """Tiny drawn key glyph + monospace text, replacing the πŸ”‘ emoji.""" + s = size + gold = "#eab308" + w = s * 1.3 + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 + bx = xl + rb + hs = s * 0.18 + xr = xl + w + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_outline(cx, top, bottom): + """iPhone outer body + screen path with notch in the top. Centered at cx, body height = bottom-top.""" + bx0 = cx - 45 # body 90 wide + sx0, sx1 = cx - 42, cx + 42 # screen 84 wide + sy0, sy1 = top + 3, bottom - 3 # screen y span + # Notch geometry (matches the original hand-tuned shape): + # outer corners at (cx-15, sy0) and (cx+15, sy0) β€” 30px wide + # inner notch bottom at (cx-9 .. cx+9) at y = sy0 + 9 + n_outer_l, n_outer_r = cx - 15, cx + 15 + n_inner_l, n_inner_r = cx - 9, cx + 9 + n_bottom = sy0 + 9 + body = f'' + path = ( + f'' + ) + return body + "\n " + path + + +def phone_iphone(cx, top, key, label, *, ip_text=None, animated_ips=None): + """Static iPhone β€” used for Alice.""" + lines = [f' '] + if ip_text is not None: + lines.append(f' {ip_text}') + if animated_ips: + for ip in animated_ips: + lines.append(ip) + lines += [ + f' {iphone_outline(cx, top, top + 150)}', + f' ', + f' iroh', + f' {key_label(cx, top + 95, key, 10, AMBER)}', + f' {label}', + f' ', + ] + return "\n".join(lines) + + +def phone_android(cx, top, key, label, *, animated_ips=None): + """Android phone β€” used for Bob (with movement and IP changes).""" + bx0 = cx - 45 + sx0 = cx - 42 + lines = [' '] + lines.append(f' ') + if animated_ips: + for ip in animated_ips: + lines.append(' ' + ip) + lines += [ + f' ', + f' ', + f' ', + f' ', + f' iroh', + f' {key_label(cx, top + 95, key, 10, AMBER)}', + f' {label}', + ' ', + ] + return "\n".join(lines) + + +def bob_ip(text, key_times, vals, *, initial_opacity=None): + extra = '' if initial_opacity is None else f' opacity="{initial_opacity}"' + return ( + f'' + f'{text}' + f'' + ) + + +def morph_path(d0, mid_d, k_mid_start, k_mid_end, stroke_attrs): + """Path with d-morph animation between two shapes.""" + return ( + f' \n' + f' \n' + ) + + +# ============================ Alice (static iPhone) ============================ +alice_block = phone_iphone(120, 190, "a4f7c0…", "Alice", ip_text="10.0.0.42") + +# ============================ Bob (Android, moves) ============================ +bob_ips = [ + bob_ip("10.0.0.7", "0;0.29;0.31;1", "1;1;0;0"), + bob_ip("192.168.1.7", "0;0.29;0.31;0.55;0.57;1", "0;0;1;1;0;0", initial_opacity="0"), + bob_ip("172.16.0.7", "0;0.55;0.57;1", "0;0;1;1", initial_opacity="0"), +] +bob_block = phone_android(240, 190, "8e2b1d…", "Bob", animated_ips=bob_ips) + +# ============================ Connection paths ============================ +single_d = "M 120 240 Q 120 161 180 161 Q 240 161 240 240" +single_d2 = "M 120 240 Q 120 161 180 161 Q 285 161 285 240" +single_path = ( + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' +) + +multi_r1r2_d = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 30 420 30 420 90 L 420 130 C 420 200 285 170 285 240" +multi_r1r2_d2 = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 30 420 30 420 90 L 420 130 C 420 200 528 170 528 240" +multi_r1r2_path = ( + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' +) + +multi_r1r3_d = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 10 660 10 660 90 L 660 130 C 660 200 552 170 552 240" +multi_r1r3_d2 = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 10 660 10 660 90 L 660 130 C 660 200 720 170 720 240" +multi_r1r3_path = ( + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' +) + +# ============================ Routers ============================ +# R1 β€” home wifi (always visible). Public IP fades in once multi-routing kicks in. +r1 = f''' + + home router + + 203.0.113.1 + + + + + + + 10.0.0.1 + ''' + +# R2 β€” cell tower (appears 5s, fades out at 11s). 3 panels on a mast with crossbar. +r2 = f''' + + + + mobile network + + 198.51.100.1 + + + + + + 192.168.1.1 + ''' + +# R3 β€” Starlink ground station (fades in at 11s). Flat tilted parallelogram + kickstand. +r3 = f''' + + + + satellite internet + + 100.64.10.1 + + + 172.16.0.1 + ''' + +# ============================ Timer bar ============================ +timer = f''' + + + + + ''' + +# ============================ Assemble ============================ +svg = f''' + + + + + + + + +{alice_block} + + +{bob_block} + +{single_path} +{multi_r1r2_path} +{multi_r1r3_path} + +{r1} + +{r2} + +{r3} + + +{timer} + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/src/app/providers.jsx b/src/app/providers.jsx index d659167d..87650a0d 100644 --- a/src/app/providers.jsx +++ b/src/app/providers.jsx @@ -6,8 +6,10 @@ import {ThemeProvider} from 'next-themes'; export const AppContext = createContext({}) export function Providers({ children }) { + // TEMP-DARK-TOGGLE: drop forcedTheme so the floating dev toggle can switch modes. + // To revert: restore forcedTheme="light" and remove disableTransitionOnChange's neighbours. return ( - + {children} ); diff --git a/src/components/BlogPostLayout.jsx b/src/components/BlogPostLayout.jsx index 121e4fa1..9751742f 100644 --- a/src/components/BlogPostLayout.jsx +++ b/src/components/BlogPostLayout.jsx @@ -5,10 +5,13 @@ import References from '@/components/References' import { HeaderSparse } from '@/components/HeaderSparse' import { FooterMarketing } from '@/components/FooterMarketing' import { formatDate } from '@/lib/formatDate' +// TEMP-DARK-TOGGLE: remove this import and the below to revert. +import { TempDarkToggle } from '@/components/TempDarkToggle' export function BlogPostLayout({ article, references = [], children }) { return (
+
diff --git a/src/components/TempDarkToggle.jsx b/src/components/TempDarkToggle.jsx new file mode 100644 index 00000000..11dc405e --- /dev/null +++ b/src/components/TempDarkToggle.jsx @@ -0,0 +1,37 @@ +'use client'; +// TEMP-DARK-TOGGLE: floating button to switch between light and dark while tweaking SVGs. +// To revert: delete this file and remove the usage in src/components/BlogPostLayout.jsx. + +import {useEffect, useState} from 'react'; +import {useTheme} from 'next-themes'; + +export function TempDarkToggle() { + const {resolvedTheme, setTheme} = useTheme(); + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + if (!mounted) return null; + const isDark = resolvedTheme === 'dark'; + return ( + + ); +} diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index 78842fca..819ed328 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -105,6 +105,7 @@ } .dark { + color-scheme: dark; --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); From b0e7ede1e64d4bc552890129f7c1941fb5fd29d6 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Fri, 19 Jun 2026 18:09:26 +0300 Subject: [PATCH 07/11] Small tweaks --- public/blog/how-iroh-works/hole-punching.svg | 16 ++++++++-------- public/blog/how-iroh-works/publish-relay.svg | 1 + scripts/hole-punching.gen.py | 2 +- scripts/publish-relay.gen.py | 1 + 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/public/blog/how-iroh-works/hole-punching.svg b/public/blog/how-iroh-works/hole-punching.svg index be25e8e0..d4757f12 100644 --- a/public/blog/how-iroh-works/hole-punching.svg +++ b/public/blog/how-iroh-works/hole-punching.svg @@ -8,8 +8,8 @@ - - + + @@ -123,15 +123,15 @@ - - - not routable + + + not routable - - - not routable + + + not routable diff --git a/public/blog/how-iroh-works/publish-relay.svg b/public/blog/how-iroh-works/publish-relay.svg index 0a122731..ca079a79 100644 --- a/public/blog/how-iroh-works/publish-relay.svg +++ b/public/blog/how-iroh-works/publish-relay.svg @@ -45,6 +45,7 @@ + 1f3a… β†’ eu-west 7c0e… β†’ us-west diff --git a/scripts/hole-punching.gen.py b/scripts/hole-punching.gen.py index 9ae8bc56..0f333b44 100644 --- a/scripts/hole-punching.gen.py +++ b/scripts/hole-punching.gen.py @@ -254,7 +254,7 @@ def facts(x, lines, anchor="start"): T_BR = 25.7 # Bob reply: PATH_RESPONSE β†’ validated T_BLUE = T_BR + 2.4 # direct path turns blue once the response validates it -DROP_A, DROP_B = (acx, 200), (bcx, 200) # local drops β€” straight up above each router (no bend toward the other side, which read as if the packet was trying to reach the peer) +DROP_A, DROP_B = (acx, 165), (bcx, 165) # local drops β€” straight up above each router (no bend toward the other side, which read as if the packet was trying to reach the peer). Kept high enough that the "not routable" label clears the router's public IP below. probe_local_a_d = f"M {acx} {A_PH[1]} L {acx} {DROP_A[1]}" probe_local_b_d = f"M {bcx} {B_PH[1]} L {bcx} {DROP_B[1]}" diff --git a/scripts/publish-relay.gen.py b/scripts/publish-relay.gen.py index c43162f2..f8367ba2 100644 --- a/scripts/publish-relay.gen.py +++ b/scripts/publish-relay.gen.py @@ -174,6 +174,7 @@ def doc_packet(wire_id, color, opa_pts, mot_pts): ynew = ry0 + len(existing)*rlh rec_pts = [(0, 0), (3.5, 0), (4.0, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] record = f''' + {chr(10).join(rec_lines)} 8e2b… β†’ us-east{anim_opacity(rec_pts)} From d128bea37c58886c7c623a82b5a3d7f39691a7c3 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 24 Jun 2026 11:04:08 +0300 Subject: [PATCH 08/11] Move svg output to global animations dir so we can reference it from the docs site. --- public/animations/connect-by-key.svg | 80 ++++++++ public/animations/embedding-phone.svg | 121 +++++++++++ public/animations/endpoint-startup.svg | 130 ++++++++++++ public/animations/hole-punching-lan.svg | 156 ++++++++++++++ public/animations/hole-punching.svg | 257 ++++++++++++++++++++++++ public/animations/publish-relay-dht.svg | 149 ++++++++++++++ public/animations/publish-relay.svg | 125 ++++++++++++ public/animations/routing-moves.svg | 148 ++++++++++++++ scripts/connect-by-key.gen.py | 147 +++++++------- scripts/endpoint-startup.gen.py | 24 +-- scripts/hole-punching-lan.gen.py | 7 +- scripts/hole-punching.gen.py | 7 +- scripts/publish-relay-dht.gen.py | 7 +- scripts/publish-relay.gen.py | 7 +- scripts/routing-moves.gen.py | 7 +- src/app/blog/how-iroh-works/page.mdx | 44 ++-- 16 files changed, 1294 insertions(+), 122 deletions(-) create mode 100644 public/animations/connect-by-key.svg create mode 100644 public/animations/embedding-phone.svg create mode 100644 public/animations/endpoint-startup.svg create mode 100644 public/animations/hole-punching-lan.svg create mode 100644 public/animations/hole-punching.svg create mode 100644 public/animations/publish-relay-dht.svg create mode 100644 public/animations/publish-relay.svg create mode 100644 public/animations/routing-moves.svg diff --git a/public/animations/connect-by-key.svg b/public/animations/connect-by-key.svg new file mode 100644 index 00000000..5a5f4740 --- /dev/null +++ b/public/animations/connect-by-key.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + 10.0.0.42 + + 192.168.1.7 + + 203.0.113.15 + + 172.20.4.88 + + 192.168.10.55 + + + + + + iroh + a4f7c0… + + + + + + + iroh + 8e2b1d… + + + Alice + Bob + + + + + + connect + + + + + + + + + + diff --git a/public/animations/embedding-phone.svg b/public/animations/embedding-phone.svg new file mode 100644 index 00000000..df4cb511 --- /dev/null +++ b/public/animations/embedding-phone.svg @@ -0,0 +1,121 @@ + + + + + iOS app (Swift) +iroh via iroh-ffi + + + + swift app + + iroh + + + + + Android app (Kotlin) +iroh via iroh-ffi + + + + + kotlin app + + iroh + + + + + Native desktop (C++) +iroh via iroh-c-ffi + + + + c++ app + + iroh + + + + + + + Web app (JavaScript) +iroh compiled to WASM + + + + + + + + + js app + + iroh + + + + + Server daemon (Rust) +iroh crate directly + + + + + + + + + + + + rust daemon + + iroh + + + + + Embedded firmware (Rust) +iroh crate + + + + + + + + + + + + + + + + + + firmware + + iroh + + diff --git a/public/animations/endpoint-startup.svg b/public/animations/endpoint-startup.svg new file mode 100644 index 00000000..480f0913 --- /dev/null +++ b/public/animations/endpoint-startup.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + us-west + + + + + + + + + + + + 52.10.18.7 + + + + + us-east + + + + + + + + + + + + 44.208.61.5 + + + + + eu-west + + + + + + + + + + + + 18.196.142.9 + + + + + + + + + + + + + + + iroh + 8e2b… + + + + 73.118.42.9 + + + + + + + + + + + + us-east: 19 ms + us-west: 71 ms + eu-west: 102 ms + + + + + + + + + + diff --git a/public/animations/hole-punching-lan.svg b/public/animations/hole-punching-lan.svg new file mode 100644 index 00000000..c62dfaac --- /dev/null +++ b/public/animations/hole-punching-lan.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + relay + + + + + + + + + + 192.168.0.1 + + + + + + + + iroh + 1a9c… + Alice + + + EndpointId: 1a9c… + Addr: 192.168.0.3:2104 + Addr: 4.9.8.2:2104 + relay: us-east + + + + + + + + + iroh + 8e2b… + Bob + + + EndpointId: 8e2b… + Addr: 192.168.0.5:4153 + Addr: 4.9.8.2:4153 + relay: us-east + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + direct (LAN) + + + + + + + ADD_ADDRESS 192.168.0.5:4153 + ADD_ADDRESS 4.9.8.2:4153 + + + + + + + + Bob + Candidate: 192.168.0.5:4153 + Candidate: 4.9.8.2:4153 + + + + + REACH_OUT192.168.0.3:2104 + REACH_OUT4.9.8.2:2104 + + + + + + + + Alice + Candidate: 192.168.0.3:2104 + Candidate: 4.9.8.2:2104 + + + + + PATH_CHALLENGE + 192.168.0.5:4153 + + + + + PATH_RESPONSE + PATH_CHALLENGE + 192.168.0.3:2104 + + + + + PATH_RESPONSE + + + + + + + diff --git a/public/animations/hole-punching.svg b/public/animations/hole-punching.svg new file mode 100644 index 00000000..d4757f12 --- /dev/null +++ b/public/animations/hole-punching.svg @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + relay + + + + 8.3.1.9 + + + + + + + 10.0.0.1 + + + + 4.9.8.2 + + + + + + + 192.168.0.1 + + + + + + + + iroh + 1a9c… + Alice + + + EndpointId: 1a9c… + Addr: 10.0.0.3:2104 + Addr: 8.3.1.9:2104 + relay: us-west + + + + + + + + + iroh + 8e2b… + Bob + + + EndpointId: 8e2b… + Addr: 192.168.0.3:4153 + Addr: 4.9.8.2:4153 + relay: us-east + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + not routable + + + + + + not routable + + + + + + no mapping + + + + + + + + 4.9.8.2:4153 β†’ + 10.0.0.3:2104 + + + + + + + + + + + + + + + + + + + 8.3.1.9:2104 β†’ + 192.168.0.3:4153 + + + + + + + + + + + + + + + + + + ADD_ADDRESS 192.168.0.3:4153 + ADD_ADDRESS 4.9.8.2:4153 + + + + + + + + Bob + Candidate: 192.168.0.3:4153 + Candidate: 4.9.8.2:4153 + + + + + REACH_OUT10.0.0.3:2104 + REACH_OUT8.3.1.9:2104 + + + + + + + + Alice + Candidate: 10.0.0.3:2104 + Candidate: 8.3.1.9:2104 + I need to reach out + + + + + PATH_CHALLENGE + 192.168.0.3:4153 + + + + + PATH_CHALLENGE + 10.0.0.3:2104 + + + + + PATH_CHALLENGE + 4.9.8.2:4153 + + + + + PATH_CHALLENGE + 8.3.1.9:2104 + + + + + PATH_RESPONSE + PATH_CHALLENGE + + + + + PATH_RESPONSE + + + + + + + diff --git a/public/animations/publish-relay-dht.svg b/public/animations/publish-relay-dht.svg new file mode 100644 index 00000000..cdf67db1 --- /dev/null +++ b/public/animations/publish-relay-dht.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + Mainline DHT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + iroh + 8e2b… + Bob + + + NodeId: 8e2b… + Home relay: us-east + + + + + + + + + iroh + 1a9c… + Alice + + 8e2b… is at relay us-east + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DHT put + 8e2b… + Relay: us-east + + + + + + + DHT get + 8e2b… + + + + + + + ;; ANSWER SECTION: + TXT "relay=https://us-east" + + + + + + + diff --git a/public/animations/publish-relay.svg b/public/animations/publish-relay.svg new file mode 100644 index 00000000..ca079a79 --- /dev/null +++ b/public/animations/publish-relay.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dns.iroh.link + + + + + + 1f3a… β†’ eu-west + 7c0e… β†’ us-west + b48d… β†’ us-east + 2a91… β†’ eu-west + 8e2b… β†’ us-east + + + + + + + + + iroh + 8e2b… + Bob + + + NodeId: 8e2b… + Home relay: us-east + + + + + + + + + iroh + 1a9c… + Alice + + + 8e2b… is at relay us-east + + + + + + + + + + + + + + + + + HTTPS PUT + Relay: us-east + Signed by: 8e2b… + + + + + + + + + DNS LOOKUP + TXT _iroh.8e2b….dns.iroh.link + + + + ;; ANSWER SECTION: + TXT "relay=https://us-east" + + + + + + + + diff --git a/public/animations/routing-moves.svg b/public/animations/routing-moves.svg new file mode 100644 index 00000000..357ba067 --- /dev/null +++ b/public/animations/routing-moves.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + 10.0.0.42 + + + + iroh + a4f7c0… + Alice + + + + + + 10.0.0.7 + 192.168.1.7 + 172.16.0.7 + + + + + iroh + 8e2b1d… + Bob + + + + + + + + + + + + + + + + + + + + + + + + home router + + 203.0.113.1 + + + + + + + 10.0.0.1 + + + + + + + mobile network + + 198.51.100.1 + + + + + + 192.168.1.1 + + + + + + + satellite internet + + 100.64.10.1 + + + 172.16.0.1 + + + + + + + + + + diff --git a/scripts/connect-by-key.gen.py b/scripts/connect-by-key.gen.py index 5777e7b2..c91561a9 100644 --- a/scripts/connect-by-key.gen.py +++ b/scripts/connect-by-key.gen.py @@ -7,17 +7,22 @@ between the keys (unidirectional β†’ bidirectional after the handshake) stays up. The point: iroh dials keys, not addresses. -14s loop. CSS-driven keyframes (single clock, no SMIL/CSS mixing). +14s loop. SMIL-driven (/), single clock β€” no CSS +keyframes. SMIL keeps everything (opacity here, motion-along-paths elsewhere) +on one clock, co-located with the geometry, so the file animates under any host +or embedding. These diagrams are also consumed cross-site by +docs.iroh.computer, where self-contained SMIL is what survives. To change it, edit this script and run: python3 connect-by-key.gen.py -It writes ../public/blog/how-iroh-works/connect-by-key.svg. -Keep the MDX
aspectRatio in sync with VB_W / VB_H. +It writes ../public/animations/connect-by-key.svg. +The page embeds this via /; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). """ import os HERE = os.path.dirname(os.path.abspath(__file__)) OUT_PATH = os.path.normpath(os.path.join( - HERE, "..", "public", "blog", "how-iroh-works", "connect-by-key.svg")) + HERE, "..", "public", "animations", "connect-by-key.svg")) MONO = "'Space Mono', monospace" INDIGO, AMBER, GRAY, RED = "#6366f1", "#d97706", "#888", "#dc2626" @@ -26,6 +31,26 @@ VB_W, VB_H = 800, 320 +def fmt_kt(pct): + """Percentage (0-100) of the loop -> SMIL keyTimes fraction string.""" + if pct <= 0: + return "0" + if pct >= 100: + return "1" + return f"{pct / 100:.4f}".rstrip("0").rstrip(".") + + +def opacity_anim(stops): + """SMIL on opacity. `stops` is a list of (percent, opacity) + matching what a CSS @keyframes block would express; the first stop's value + is also the element's resting opacity (set it as the opacity attribute).""" + keytimes = ";".join(fmt_kt(p) for p, _ in stops) + values = ";".join(str(v) for _, v in stops) + return (f'') + + def key_label(cx, y, text, size, color): """Tiny drawn gold key glyph + monospace text, replacing the πŸ”‘ emoji.""" s = size @@ -77,34 +102,28 @@ def key_label(cx, y, text, size, color): ] -def ip_animation_css(): - lines = [] - for i, (vis_from, vis_to) in enumerate(TEXT_KEYFRAMES, start=1): - cls = f'bob-ip{i}-t' - if i == 1: - lines.append(f' .{cls} {{ animation: {cls} {CYCLE}s infinite linear; }}') - else: - lines.append(f' .{cls} {{ animation: {cls} {CYCLE}s infinite linear; opacity: 0; }}') - for i, kf in enumerate(LINE_KEYFRAMES, start=1): - if kf is None: continue - cls = f'bob-ip{i}-l' - lines.append(f' .{cls} {{ animation: {cls} {CYCLE}s infinite linear; opacity: 0; }}') - lines.append('') - for i, (vis_from, vis_to) in enumerate(TEXT_KEYFRAMES, start=1): - if i == 1: - # Start visible, fade out at end of slot 1 - lines.append(f' @keyframes bob-ip{i}-t {{ 0%, {vis_to}% {{ opacity: 1; }} {vis_to + 2}%, 100% {{ opacity: 0; }} }}') - elif i < len(TEXT_KEYFRAMES): - lines.append(f' @keyframes bob-ip{i}-t {{ 0%, {vis_from - 2}% {{ opacity: 0; }} {vis_from}%, {vis_to}% {{ opacity: 1; }} {vis_to + 2}%, 100% {{ opacity: 0; }} }}') - else: - # Final IP β€” stay visible to end - lines.append(f' @keyframes bob-ip{i}-t {{ 0%, {vis_from - 2}% {{ opacity: 0; }} {vis_from}%, 100% {{ opacity: 1; }} }}') - for i, kf in enumerate(LINE_KEYFRAMES, start=1): - if kf is None: continue - sf, st = kf - vis_from, vis_to = TEXT_KEYFRAMES[i - 1] - lines.append(f' @keyframes bob-ip{i}-l {{ 0%, {sf - 2}% {{ opacity: 0; }} {sf}%, {st}% {{ opacity: 1; }} {st + 2}%, 100% {{ opacity: 0; }} }}') - return "\n".join(lines) +def text_stops(i): + """Opacity stops for IP-text slot i (1-indexed), matching the old CSS.""" + vis_from, vis_to = TEXT_KEYFRAMES[i - 1] + if i == 1: + # Start visible, fade out at end of slot 1. + return [(0, 1), (vis_to, 1), (vis_to + 2, 0), (100, 0)] + if i < len(TEXT_KEYFRAMES): + return [(0, 0), (vis_from - 2, 0), (vis_from, 1), (vis_to, 1), (vis_to + 2, 0), (100, 0)] + # Final IP β€” stay visible to end. + return [(0, 0), (vis_from - 2, 0), (vis_from, 1), (100, 1)] + + +def line_stops(i): + """Opacity stops for the strikethrough line of slot i (1-indexed).""" + sf, st = LINE_KEYFRAMES[i - 1] + return [(0, 0), (sf - 2, 0), (sf, 1), (st, 1), (st + 2, 0), (100, 0)] + + +# Keys appear at ~5s and stay; "connect" arrow shows 5–6s; bidirectional from 6s. +KEYS_ANIM = opacity_anim([(0, 0), (35, 0), (37, 1), (100, 1)]) +UNI_ANIM = opacity_anim([(0, 0), (35, 0), (37, 1), (42, 1), (44, 0), (100, 0)]) +BI_ANIM = opacity_anim([(0, 0), (42, 0), (44, 1), (100, 1)]) def alice_iphone(): @@ -131,7 +150,7 @@ def alice_iphone(): f' fill="none" stroke="{INDIGO}" stroke-width="1.5"/>\n' f' \n' f' iroh\n' - f' {key_label(200, 208, "a4f7c0…", 11, AMBER)}' + f' {KEYS_ANIM}{key_label(200, 208, "a4f7c0…", 11, AMBER)}' ) @@ -143,49 +162,36 @@ def bob_android(): f' \n' f' \n' f' iroh\n' - f' {key_label(600, 208, "8e2b1d…", 11, AMBER)}' + f' {KEYS_ANIM}{key_label(600, 208, "8e2b1d…", 11, AMBER)}' ) # ============================ assemble ============================ ip_rows = [] for i, (text, lx0, lx1) in enumerate(BOB_IPS, start=1): - ip_rows.append(f' {text}') + t_stops = text_stops(i) + init = t_stops[0][1] + ip_rows.append( + f' {text}{opacity_anim(t_stops)}') if lx0 is not None: - ip_rows.append(f' ') + ip_rows.append( + f' {opacity_anim(line_stops(i))}') svg = f''' + - - @@ -207,18 +213,19 @@ def bob_android(): Bob - + + {UNI_ANIM} connect - + {BI_ANIM} - - - + + + ''' diff --git a/scripts/endpoint-startup.gen.py b/scripts/endpoint-startup.gen.py index 5a865333..ed50c08f 100644 --- a/scripts/endpoint-startup.gen.py +++ b/scripts/endpoint-startup.gen.py @@ -7,7 +7,7 @@ It reads the projected land outline from endpoint-startup.land.txt (Natural Earth 110m coastline, plate carrΓ©e with standard parallel 45Β°, clipped to a US + Europe -window) and writes ../public/blog/how-iroh-works/endpoint-startup.svg. +window) and writes ../public/animations/endpoint-startup.svg. NOTE: this lives in scripts/, not in src/app/, because anything under the Next.js app router directory gets bundled by webpack (which chokes on .py files). @@ -27,7 +27,7 @@ LAND_PATH = os.path.join(HERE, "endpoint-startup.land.txt") OUT_PATH = os.path.normpath(os.path.join( HERE, "..", - "public", "blog", "how-iroh-works", "endpoint-startup.svg")) + "public", "animations", "endpoint-startup.svg")) def key_label(cx, y, text, size, color): """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx @@ -161,7 +161,8 @@ def q(t, a, b, c): return (1-t)**2*a + 2*(1-t)*t*b + t**2*c d_ew, L_ew = arc(bx_, by_, ewx, ewy, k=0.10) wires = f''' - + + @@ -206,7 +207,9 @@ def readline(y, label, ms, begin): f'values="0;0;1;1" keyTimes="0;{t0:.4f};{t1:.4f};1"/>') readout = f''' - + + + {readline(268, "us-east", 19, ret_time(L_ue))} {readline(294, "us-west", 71, ret_time(L_uw))} {readline(320, "eu-west", 102, ret_time(L_ew))} @@ -233,20 +236,11 @@ def readline(y, label, ms, begin): # Cropped viewport: trim the empty Arctic (top) and eastern Europe (right) so the # relevant US <-> western-Europe band fills the frame. The land path is unchanged; -# this just clips it. Keep the MDX
aspectRatio in sync with VB_W / VB_H. +# this just clips it. The page embeds this via /; +# the intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). VB_X, VB_Y, VB_W, VB_H = 0, 92, 705, 280 svg = f''' - - - - diff --git a/scripts/hole-punching-lan.gen.py b/scripts/hole-punching-lan.gen.py index f25611f0..1f27ddaa 100644 --- a/scripts/hole-punching-lan.gen.py +++ b/scripts/hole-punching-lan.gen.py @@ -15,15 +15,16 @@ Same look as the other how-iroh-works figures. Edit this script and run: python3 hole-punching-lan.gen.py -Writes ../public/blog/how-iroh-works/hole-punching-lan.svg. -Keep the MDX
aspectRatio in sync with VB_W / VB_H. +Writes ../public/animations/hole-punching-lan.svg. +The page embeds this via /; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). """ import os HERE = os.path.dirname(os.path.abspath(__file__)) OUT_PATH = os.path.normpath(os.path.join( - HERE, "..", "public", "blog", "how-iroh-works", "hole-punching-lan.svg")) + HERE, "..", "public", "animations", "hole-punching-lan.svg")) MONO = "'Space Mono', monospace" INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#111", "#2563eb", "#dc2626", "#15803d" diff --git a/scripts/hole-punching.gen.py b/scripts/hole-punching.gen.py index 0f333b44..9912fb86 100644 --- a/scripts/hole-punching.gen.py +++ b/scripts/hole-punching.gen.py @@ -10,15 +10,16 @@ Same look as the other how-iroh-works figures; all connections blue. Edit this script and run: python3 hole-punching.gen.py -Writes ../public/blog/how-iroh-works/hole-punching.svg. -Keep the MDX
aspectRatio in sync with VB_W / VB_H. +Writes ../public/animations/hole-punching.svg. +The page embeds this via /; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). """ import os HERE = os.path.dirname(os.path.abspath(__file__)) OUT_PATH = os.path.normpath(os.path.join( - HERE, "..", "public", "blog", "how-iroh-works", "hole-punching.svg")) + HERE, "..", "public", "animations", "hole-punching.svg")) MONO = "'Space Mono', monospace" INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#111", "#2563eb", "#dc2626", "#15803d" diff --git a/scripts/publish-relay-dht.gen.py b/scripts/publish-relay-dht.gen.py index f8179fa8..8ae2c082 100644 --- a/scripts/publish-relay-dht.gen.py +++ b/scripts/publish-relay-dht.gen.py @@ -14,8 +14,9 @@ out -> final state holds ~10s -> loop. Edit this script and run: python3 publish-relay-dht.gen.py -Writes ../public/blog/how-iroh-works/publish-relay-dht.svg. -Keep the MDX
aspectRatio in sync with VB_W / VB_H. +Writes ../public/animations/publish-relay-dht.svg. +The page embeds this via /; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). """ import math @@ -23,7 +24,7 @@ HERE = os.path.dirname(os.path.abspath(__file__)) OUT_PATH = os.path.normpath(os.path.join( - HERE, "..", "public", "blog", "how-iroh-works", "publish-relay-dht.svg")) + HERE, "..", "public", "animations", "publish-relay-dht.svg")) MONO = "'Space Mono', monospace" INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#111" diff --git a/scripts/publish-relay.gen.py b/scripts/publish-relay.gen.py index f8367ba2..af52b000 100644 --- a/scripts/publish-relay.gen.py +++ b/scripts/publish-relay.gen.py @@ -12,8 +12,9 @@ -> final state holds ~10s -> loop. To change it, edit this script and run: python3 publish-relay.gen.py -It writes ../public/blog/how-iroh-works/publish-relay.svg. -Keep the MDX
aspectRatio in sync with VB_W / VB_H. +It writes ../public/animations/publish-relay.svg. +The page embeds this via /; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). """ import math @@ -21,7 +22,7 @@ HERE = os.path.dirname(os.path.abspath(__file__)) OUT_PATH = os.path.normpath(os.path.join( - HERE, "..", "public", "blog", "how-iroh-works", "publish-relay.svg")) + HERE, "..", "public", "animations", "publish-relay.svg")) MONO = "'Space Mono', monospace" INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#111" diff --git a/scripts/routing-moves.gen.py b/scripts/routing-moves.gen.py index 36706d7a..fe36911d 100644 --- a/scripts/routing-moves.gen.py +++ b/scripts/routing-moves.gen.py @@ -6,14 +6,15 @@ 20s loop, all SMIL so every element shares one timeline (no CSS/SMIL drift). To change it, edit this script and run: python3 routing-moves.gen.py -It writes ../public/blog/how-iroh-works/routing-moves.svg. -Keep the MDX
aspectRatio in sync with VB_W / VB_H. +It writes ../public/animations/routing-moves.svg. +The page embeds this via /; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). """ import os HERE = os.path.dirname(os.path.abspath(__file__)) OUT_PATH = os.path.normpath(os.path.join( - HERE, "..", "public", "blog", "how-iroh-works", "routing-moves.svg")) + HERE, "..", "public", "animations", "routing-moves.svg")) MONO = "'Space Mono', monospace" INDIGO, AMBER, GRAY = "#6366f1", "#d97706", "#888" diff --git a/src/app/blog/how-iroh-works/page.mdx b/src/app/blog/how-iroh-works/page.mdx index 0e9f64a5..4753fcb8 100644 --- a/src/app/blog/how-iroh-works/page.mdx +++ b/src/app/blog/how-iroh-works/page.mdx @@ -35,8 +35,8 @@ We have had a lot of explanations about how iroh works in the past. But before 1 Iroh is a lightweight native library that is meant to be embedded into applications. It can be embedded in rust applications [directly][iroh-crate], and into [C][iroh-c-ffi-repo], [C++][iroh-c-ffi-repo], [swift][iroh-swift], [python][iroh-python], [javascript][iroh-js] and [kotlin][iroh-kotlin] via [our bindings][iroh-ffi].
- - iroh embedded across six device contexts: iOS, Android, desktop, browser, server, embedded + + iroh embedded across six device contexts: iOS, Android, desktop, browser, server, embedded @@ -47,8 +47,8 @@ Iroh is a lightweight native library that is meant to be embedded into applicati Iroh endpoints are identified by a cryptographic key. This allows us to connect no matter where the remote endpoint is.
- - Alice connects to Bob by key β€” IPs change, the key stays stable + + Alice connects to Bob by key β€” IPs change, the key stays stable @@ -60,11 +60,11 @@ When conditions change, we immediately react and choose the new best path. This
- - Alice and Bob connected via a shared router, with two more routers available for when Bob moves + + Alice and Bob connected via a shared router, with two more routers available for when Bob moves @@ -93,11 +93,11 @@ Iroh works just fine with a single relay, but works best if you have relays in a
- - A map of the US and Europe with iroh relays in us-west (Seattle), us-east (Delaware), and eu-west (Frankfurt), and an endpoint in Florida + + A map of the US and Europe with iroh relays in us-west (Seattle), us-east (Delaware), and eu-west (Frankfurt), and an endpoint in Florida @@ -114,11 +114,11 @@ We offer a mechanism based on DNS out of the box. Bob publishes a signed DNS rec
- - Bob publishes a signed DNS record with his home relay to dns.iroh.link via an HTTPS PUT; Alice resolves it with a DNS lookup + + Bob publishes a signed DNS record with his home relay to dns.iroh.link via an HTTPS PUT; Alice resolves it with a DNS lookup @@ -129,11 +129,11 @@ The above mechanism works fine in most cases. But maybe you want something that
- - Bob publishes his signed record to several random nodes of the Mainline DHT; Alice resolves it by querying several random nodes + + Bob publishes his signed record to several random nodes of the Mainline DHT; Alice resolves it by querying several random nodes @@ -151,11 +151,11 @@ Hole punching happens *inside* the QUIC connection via an extension `n0_nat_trav
- - Alice and Bob, each behind a home router, connected through the relay β€” all data flows up to the relay and back down + + Alice and Bob, each behind a home router, connected through the relay β€” all data flows up to the relay and back down @@ -167,11 +167,11 @@ If two devices are in the same network, hole punching is not needed. Each side j
- - Alice and Bob on the same home network discover each other over the relay, then connect directly across the LAN β€” the relay uplink fades away once the local path is validated + + Alice and Bob on the same home network discover each other over the relay, then connect directly across the LAN β€” the relay uplink fades away once the local path is validated From 84e53480aa49e9d7f48747c4e88f12f43b02d850 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 24 Jun 2026 11:06:42 +0300 Subject: [PATCH 09/11] Remove orphaned animaiton svgs --- public/blog/how-iroh-works/connect-by-key.svg | 112 -------- .../blog/how-iroh-works/embedding-phone.svg | 121 --------- .../blog/how-iroh-works/endpoint-startup.svg | 137 ---------- .../blog/how-iroh-works/hole-punching-lan.svg | 156 ----------- public/blog/how-iroh-works/hole-punching.svg | 257 ------------------ .../blog/how-iroh-works/publish-relay-dht.svg | 149 ---------- public/blog/how-iroh-works/publish-relay.svg | 125 --------- public/blog/how-iroh-works/routing-moves.svg | 148 ---------- 8 files changed, 1205 deletions(-) delete mode 100644 public/blog/how-iroh-works/connect-by-key.svg delete mode 100644 public/blog/how-iroh-works/embedding-phone.svg delete mode 100644 public/blog/how-iroh-works/endpoint-startup.svg delete mode 100644 public/blog/how-iroh-works/hole-punching-lan.svg delete mode 100644 public/blog/how-iroh-works/hole-punching.svg delete mode 100644 public/blog/how-iroh-works/publish-relay-dht.svg delete mode 100644 public/blog/how-iroh-works/publish-relay.svg delete mode 100644 public/blog/how-iroh-works/routing-moves.svg diff --git a/public/blog/how-iroh-works/connect-by-key.svg b/public/blog/how-iroh-works/connect-by-key.svg deleted file mode 100644 index f686ceed..00000000 --- a/public/blog/how-iroh-works/connect-by-key.svg +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - 10.0.0.42 - - 192.168.1.7 - - 203.0.113.15 - - 172.20.4.88 - - 192.168.10.55 - - - - - - iroh - a4f7c0… - - - - - - - iroh - 8e2b1d… - - - Alice - Bob - - - - - connect - - - - - - - - - - diff --git a/public/blog/how-iroh-works/embedding-phone.svg b/public/blog/how-iroh-works/embedding-phone.svg deleted file mode 100644 index df4cb511..00000000 --- a/public/blog/how-iroh-works/embedding-phone.svg +++ /dev/null @@ -1,121 +0,0 @@ - - - - - iOS app (Swift) -iroh via iroh-ffi - - - - swift app - - iroh - - - - - Android app (Kotlin) -iroh via iroh-ffi - - - - - kotlin app - - iroh - - - - - Native desktop (C++) -iroh via iroh-c-ffi - - - - c++ app - - iroh - - - - - - - Web app (JavaScript) -iroh compiled to WASM - - - - - - - - - js app - - iroh - - - - - Server daemon (Rust) -iroh crate directly - - - - - - - - - - - - rust daemon - - iroh - - - - - Embedded firmware (Rust) -iroh crate - - - - - - - - - - - - - - - - - - firmware - - iroh - - diff --git a/public/blog/how-iroh-works/endpoint-startup.svg b/public/blog/how-iroh-works/endpoint-startup.svg deleted file mode 100644 index 2e8c23aa..00000000 --- a/public/blog/how-iroh-works/endpoint-startup.svg +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - us-west - - - - - - - - - - - - 52.10.18.7 - - - - - us-east - - - - - - - - - - - - 44.208.61.5 - - - - - eu-west - - - - - - - - - - - - 18.196.142.9 - - - - - - - - - - - - - - - iroh - 8e2b… - - - - 73.118.42.9 - - - - - - - - - - us-east: 19 ms - us-west: 71 ms - eu-west: 102 ms - - - - - - - - - - diff --git a/public/blog/how-iroh-works/hole-punching-lan.svg b/public/blog/how-iroh-works/hole-punching-lan.svg deleted file mode 100644 index c62dfaac..00000000 --- a/public/blog/how-iroh-works/hole-punching-lan.svg +++ /dev/null @@ -1,156 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - relay - - - - - - - - - - 192.168.0.1 - - - - - - - - iroh - 1a9c… - Alice - - - EndpointId: 1a9c… - Addr: 192.168.0.3:2104 - Addr: 4.9.8.2:2104 - relay: us-east - - - - - - - - - iroh - 8e2b… - Bob - - - EndpointId: 8e2b… - Addr: 192.168.0.5:4153 - Addr: 4.9.8.2:4153 - relay: us-east - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - direct (LAN) - - - - - - - ADD_ADDRESS 192.168.0.5:4153 - ADD_ADDRESS 4.9.8.2:4153 - - - - - - - - Bob - Candidate: 192.168.0.5:4153 - Candidate: 4.9.8.2:4153 - - - - - REACH_OUT192.168.0.3:2104 - REACH_OUT4.9.8.2:2104 - - - - - - - - Alice - Candidate: 192.168.0.3:2104 - Candidate: 4.9.8.2:2104 - - - - - PATH_CHALLENGE - 192.168.0.5:4153 - - - - - PATH_RESPONSE - PATH_CHALLENGE - 192.168.0.3:2104 - - - - - PATH_RESPONSE - - - - - - - diff --git a/public/blog/how-iroh-works/hole-punching.svg b/public/blog/how-iroh-works/hole-punching.svg deleted file mode 100644 index d4757f12..00000000 --- a/public/blog/how-iroh-works/hole-punching.svg +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - relay - - - - 8.3.1.9 - - - - - - - 10.0.0.1 - - - - 4.9.8.2 - - - - - - - 192.168.0.1 - - - - - - - - iroh - 1a9c… - Alice - - - EndpointId: 1a9c… - Addr: 10.0.0.3:2104 - Addr: 8.3.1.9:2104 - relay: us-west - - - - - - - - - iroh - 8e2b… - Bob - - - EndpointId: 8e2b… - Addr: 192.168.0.3:4153 - Addr: 4.9.8.2:4153 - relay: us-east - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - not routable - - - - - - not routable - - - - - - no mapping - - - - - - - - 4.9.8.2:4153 β†’ - 10.0.0.3:2104 - - - - - - - - - - - - - - - - - - - 8.3.1.9:2104 β†’ - 192.168.0.3:4153 - - - - - - - - - - - - - - - - - - ADD_ADDRESS 192.168.0.3:4153 - ADD_ADDRESS 4.9.8.2:4153 - - - - - - - - Bob - Candidate: 192.168.0.3:4153 - Candidate: 4.9.8.2:4153 - - - - - REACH_OUT10.0.0.3:2104 - REACH_OUT8.3.1.9:2104 - - - - - - - - Alice - Candidate: 10.0.0.3:2104 - Candidate: 8.3.1.9:2104 - I need to reach out - - - - - PATH_CHALLENGE - 192.168.0.3:4153 - - - - - PATH_CHALLENGE - 10.0.0.3:2104 - - - - - PATH_CHALLENGE - 4.9.8.2:4153 - - - - - PATH_CHALLENGE - 8.3.1.9:2104 - - - - - PATH_RESPONSE - PATH_CHALLENGE - - - - - PATH_RESPONSE - - - - - - - diff --git a/public/blog/how-iroh-works/publish-relay-dht.svg b/public/blog/how-iroh-works/publish-relay-dht.svg deleted file mode 100644 index cdf67db1..00000000 --- a/public/blog/how-iroh-works/publish-relay-dht.svg +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - - Mainline DHT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - iroh - 8e2b… - Bob - - - NodeId: 8e2b… - Home relay: us-east - - - - - - - - - iroh - 1a9c… - Alice - - 8e2b… is at relay us-east - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DHT put - 8e2b… - Relay: us-east - - - - - - - DHT get - 8e2b… - - - - - - - ;; ANSWER SECTION: - TXT "relay=https://us-east" - - - - - - - diff --git a/public/blog/how-iroh-works/publish-relay.svg b/public/blog/how-iroh-works/publish-relay.svg deleted file mode 100644 index ca079a79..00000000 --- a/public/blog/how-iroh-works/publish-relay.svg +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dns.iroh.link - - - - - - 1f3a… β†’ eu-west - 7c0e… β†’ us-west - b48d… β†’ us-east - 2a91… β†’ eu-west - 8e2b… β†’ us-east - - - - - - - - - iroh - 8e2b… - Bob - - - NodeId: 8e2b… - Home relay: us-east - - - - - - - - - iroh - 1a9c… - Alice - - - 8e2b… is at relay us-east - - - - - - - - - - - - - - - - - HTTPS PUT - Relay: us-east - Signed by: 8e2b… - - - - - - - - - DNS LOOKUP - TXT _iroh.8e2b….dns.iroh.link - - - - ;; ANSWER SECTION: - TXT "relay=https://us-east" - - - - - - - - diff --git a/public/blog/how-iroh-works/routing-moves.svg b/public/blog/how-iroh-works/routing-moves.svg deleted file mode 100644 index 357ba067..00000000 --- a/public/blog/how-iroh-works/routing-moves.svg +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - 10.0.0.42 - - - - iroh - a4f7c0… - Alice - - - - - - 10.0.0.7 - 192.168.1.7 - 172.16.0.7 - - - - - iroh - 8e2b1d… - Bob - - - - - - - - - - - - - - - - - - - - - - - - home router - - 203.0.113.1 - - - - - - - 10.0.0.1 - - - - - - - mobile network - - 198.51.100.1 - - - - - - 192.168.1.1 - - - - - - - satellite internet - - 100.64.10.1 - - - 172.16.0.1 - - - - - - - - - - From 42a6b933b290ff713cacdac124ab3eb07caeb289 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 24 Jun 2026 11:12:41 +0300 Subject: [PATCH 10/11] Remove blog post, the text will live in docs --- src/app/blog/how-iroh-works/page.mdx | 190 --------------------------- 1 file changed, 190 deletions(-) delete mode 100644 src/app/blog/how-iroh-works/page.mdx diff --git a/src/app/blog/how-iroh-works/page.mdx b/src/app/blog/how-iroh-works/page.mdx deleted file mode 100644 index 4753fcb8..00000000 --- a/src/app/blog/how-iroh-works/page.mdx +++ /dev/null @@ -1,190 +0,0 @@ -import { BlogPostLayout } from '@/components/BlogPostLayout' -import { MotionCanvas } from '@/components/MotionCanvas' - -export const post = { - draft: false, - author: 'RΓΌdiger Klaehn', - date: '2026-06-17', - title: 'How iroh works', - description: '', -} - -export const metadata = { - title: post.title, - description: post.description, - openGraph: { - title: post.title, - description: post.description, - images: [{ - url: `/api/og?title=Blog&subtitle=${post.title}`, - width: 1200, - height: 630, - alt: post.title, - type: 'image/png', - }], - type: 'article' - } -} - -export default (props) => - -We have had a lot of explanations about how iroh works in the past. But before 1.0 it was a bit of a moving target. Now that [1.0 is out][v1] it is time to provide a detailed explanation. - -# Iroh is a library - -Iroh is a lightweight native library that is meant to be embedded into applications. It can be embedded in rust applications [directly][iroh-crate], and into [C][iroh-c-ffi-repo], [C++][iroh-c-ffi-repo], [swift][iroh-swift], [python][iroh-python], [javascript][iroh-js] and [kotlin][iroh-kotlin] via [our bindings][iroh-ffi]. - -
- - iroh embedded across six device contexts: iOS, Android, desktop, browser, server, embedded - -
- -# The two key features of iroh - -## Connect by key - -Iroh endpoints are identified by a cryptographic key. This allows us to connect no matter where the remote endpoint is. - -
- - Alice connects to Bob by key β€” IPs change, the key stays stable - -
- -## Reliable connections - -We make sure that you do get a connection whenever possible. And we make sure that the connection is as fast as possible. - -When conditions change, we immediately react and choose the new best path. This is transparent to the user. - -
- -
- - Alice and Bob connected via a shared router, with two more routers available for when Bob moves - -
-
- -# So how this it actually work? - -Now that we have shown *what* iroh does for you, let's go into details about *how*. - -# Iroh components - -## Iroh library - -The iroh library itself provides an API to create endpoints and connect to other endpoints, as well as a rich API for connections and data streams. - -## Iroh relay - -The iroh relays work in the background on a publicly reachable servers to facilitate direct connections and to relay data in case establishing a direct connection is not possible. - -# Endpoint startup - -An iroh endpoint is configured with a set of relays. Either the rate limited public relays operated by number0, relays bought via iroh services, or self-hosted relays. - -Iroh works just fine with a single relay, but works best if you have relays in all geographic regions where your users are. - -## Determining the home relay - -
- -
- - A map of the US and Europe with iroh relays in us-west (Seattle), us-east (Delaware), and eu-west (Frankfurt), and an endpoint in Florida - -
-
- -On startup, an iroh endpoint sends [QAD] probes to all configured relays. This serves two purposes: learning our own public IP address as well as learning which relay is closest in terms of latency. The closest relay becomes our home relay. We keep a secure websocket connection open to our home relay. - -## Publishing the home relay - -Iroh works just fine with one relay. But frequently we have multiple relays. Alice needs to know which relay to use to talk to Bob. For this we have two mechanisms - -### DNS based address lookup - -We offer a mechanism based on DNS out of the box. Bob publishes a signed DNS record to a DNS server operated by number0 via a https PUT request. Alice or whoever wants to talk to bob then looks up this record using a DNS query or a https query. - -
- -
- - Bob publishes a signed DNS record with his home relay to dns.iroh.link via an HTTPS PUT; Alice resolves it with a DNS lookup - -
-
- -### Mainline DHT based address lookup - -The above mechanism works fine in most cases. But maybe you want something that is fully peer to peer. For that case we offer an optional DHT based address lookup mechanism. We publish the exact same record as before on the mainline DHT, using the [bep_0044] extension. - -
- -
- - Bob publishes his signed record to several random nodes of the Mainline DHT; Alice resolves it by querying several random nodes - -
-
- - -## Establishing direct connections - -So far we have described a system that makes sure that endpoints can talk at all. But all data is flowing through the relays. This adds latency, limits performance, and causes costs for the relay operator. - -How do we make the connections fast and direct? This is aguably the most complex part of the system. - -## Hole punching - -Hole punching happens *inside* the QUIC connection via an extension `n0_nat_traversal`. It is inspired by the [QUIC NAT traversal draft][quic-nat-traversal], but uses its own transport parameter ID. - -
- -
- - Alice and Bob, each behind a home router, connected through the relay β€” all data flows up to the relay and back down - -
-
- - -## Local direct connections - -If two devices are in the same network, hole punching is not needed. Each side just needs to learn the local address of the remote. - -
- -
- - Alice and Bob on the same home network discover each other over the relay, then connect directly across the LAN β€” the relay uplink fades away once the local path is validated - -
-
- - -[iroh-c-ffi-repo]: https://github.com/n0-computer/iroh-c-ffi -[iroh-crate]: https://crates.io/crates/iroh -[iroh-ffi]: https://github.com/n0-computer/iroh-ffi -[iroh-js]: https://www.npmjs.com/package/@number0/iroh -[iroh-kotlin]: https://central.sonatype.com/artifact/computer.iroh/iroh -[iroh-python]: https://pypi.org/project/iroh/ -[iroh-swift]: https://swiftpackageindex.com/n0-computer/iroh-ffi -[QAD]: https://datatracker.ietf.org/doc/draft-ietf-quic-address-discovery/ -[bep_0044]: https://www.bittorrent.org/beps/bep_0044.html -[v1]: /blog/v1 -[quic-nat-traversal]: https://datatracker.ietf.org/doc/draft-seemann-quic-nat-traversal/ From e4caebdc3a7ec1ea88d2ae668a88019c7ac20d48 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 24 Jun 2026 11:16:24 +0300 Subject: [PATCH 11/11] Remove changes for dark mode testing --- src/components/BlogPostLayout.jsx | 3 --- src/components/TempDarkToggle.jsx | 37 ------------------------------- src/styles/tailwind.css | 1 - 3 files changed, 41 deletions(-) delete mode 100644 src/components/TempDarkToggle.jsx diff --git a/src/components/BlogPostLayout.jsx b/src/components/BlogPostLayout.jsx index 6340b125..d9865c36 100644 --- a/src/components/BlogPostLayout.jsx +++ b/src/components/BlogPostLayout.jsx @@ -5,13 +5,10 @@ import References from '@/components/References' import { HeaderSparse } from '@/components/HeaderSparse' import { FooterMarketing } from '@/components/FooterMarketing' import { formatDate } from '@/lib/formatDate' -// TEMP-DARK-TOGGLE: remove this import and the below to revert. -import { TempDarkToggle } from '@/components/TempDarkToggle' export function BlogPostLayout({ article, references = [], children }) { return (
-
diff --git a/src/components/TempDarkToggle.jsx b/src/components/TempDarkToggle.jsx deleted file mode 100644 index 11dc405e..00000000 --- a/src/components/TempDarkToggle.jsx +++ /dev/null @@ -1,37 +0,0 @@ -'use client'; -// TEMP-DARK-TOGGLE: floating button to switch between light and dark while tweaking SVGs. -// To revert: delete this file and remove the usage in src/components/BlogPostLayout.jsx. - -import {useEffect, useState} from 'react'; -import {useTheme} from 'next-themes'; - -export function TempDarkToggle() { - const {resolvedTheme, setTheme} = useTheme(); - const [mounted, setMounted] = useState(false); - useEffect(() => setMounted(true), []); - if (!mounted) return null; - const isDark = resolvedTheme === 'dark'; - return ( - - ); -} diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index 819ed328..78842fca 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -105,7 +105,6 @@ } .dark { - color-scheme: dark; --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885);