惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
T
ThreatConnect
SecWiki News
SecWiki News
F
Future of Privacy Forum
AWS News Blog
AWS News Blog
C
Cisco Blogs
A
Arctic Wolf
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
V2EX
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
G
Google Developers Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Privacy International News Feed
C
Cyber Attacks, Cyber Crime and Cyber Security
N
News | PayPal Newsroom
Schneier on Security
Schneier on Security
NISL@THU
NISL@THU
Microsoft Azure Blog
Microsoft Azure Blog
量子位
The Hacker News
The Hacker News
Stack Overflow Blog
Stack Overflow Blog
Security Latest
Security Latest
M
Microsoft Research Blog - Microsoft Research
Google Online Security Blog
Google Online Security Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
InfoQ
Google DeepMind News
Google DeepMind News
Y
Y Combinator Blog
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
Martin Fowler
Martin Fowler
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Troy Hunt's Blog
F
Fox-IT International blog
S
Security @ Cisco Blogs
博客园 - 司徒正美
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
Comments on: Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LINUX DO - 最新话题
GbyAI
GbyAI
Project Zero
Project Zero
腾讯CDC
T
Tailwind CSS Blog

DEV Community

My CKA Cheat Sheet: Commands, Aliases, and Documentation Tricks I Used During the Exam Frontend Engineering Beyond Pixels: The Architecture of Digital Accessibility VLA or IL? A Controlled Dataset for Testing Whether Finetuning Turns Your VLA into a Fancy Imitation Learner Fabric AI Functions Turn GenAI Into a Data Pipeline Step The Treasure Hunt Engine That Broke Before the Traffic Did Reset Windows Update: The Definitive MSP Guide to RWU Your Resume Was Never Built for This I built a token-level debugger for comparing two LLMs VCP-Virtual Private Cloud Microsoft Copilot just exfiltrated a company's files. The attack was one email. Here's the mechanism. RAG 시스템 실전 구축 (v42) copilot cloud agent is becoming an automation api Cx Dev Log — 2026-04-23 Why Tesla Is Becoming the AI Enterprise Case Study Every Leader Should Understand ORA-00214 오류 원인과 해결 방법 완벽 가이드 SpecAgnt v2.0: The Agent Lifecycle Framework for AI-Native Engineering Optimizing Signal Latency and Weight Allocations in Algorithmic Pipelines SSH Under the Hood: Protocols, Mechanisms, and the Full Technical Story دليل بوابات الدفع للتاجر العربي في 2026 (وكيف تختار المناسبة لمتجرك) Cómo Mi Configuración de Docker Me Salvó de un Ataque de Supply Chain (Y Por Qué la Tuya Debería Hacerlo También) How My Docker Setup Saved Me From a Supply Chain Attack (And Why Yours Should Too) Astro: The epitome of SEO Technical Update I Gave My AI Agent the Ability to Research Before It Writes — Here’s What Changed Kubernetes sem Cloud Provider (Parte 2): Criando Operators em Go para automação e self-service de plataforma AI Memory Needs an Authority Policy, Not Just More Context You've done tutorial after tutorial. Your GitHub is still empty. (Free 1‑page PDF, no signup) TypeScript 7.0: The Go Compiler That Makes TS 10x Faster Connecting Wallets the Right Way: wagmi v2 and EIP-6963 The 5-Layer Architecture Every Production Multi-Agent System Needs (And Why Most Skip Layers 4 and 5) CSS Scroll-Driven Animations: No JavaScript Required Vite 8 + Rolldown: Rust-Powered Builds That Are 10–30x Faster Core Architectural Components of Azure My Skills How I Use AI as a Senior Engineer Construí um motor ATS determinístico porque estava cansado de adivinhar por que meu currículo era rejeitado SCS-Lab1 — CloudTrail: Trail + S3 + KMS + Log Validation LuisCore MCP server — daily syndication · 2026-05-25 Cursor vs JetBrains Rider for C#/.NET in 2026: which to pay for I built a local-first movie recommender with Corrective-RAG (cited explanations, hybrid retrieval, runs entirely on Ollama) Scaling to 1 Million Users : Load Balancing & Caching Strategies How the Events Table That Looked Right Killed Our Queue Three Failures My AI Memory System Caught — And the Flaw It Revealed in Itself dotnet Framework life cycle tool LangGraph 워크플로우 템플릿 (v41) I built a free image compression API — no signup, just curl Designing TikTok from Scratch — A System Design Deep Dive PREDICTION-20260525-0007: boredom-with-asymmetric-leverage [2026-Q3 through 2027-Q3] [Boost] How to integrate the QuickBooks Invoice API in 2026 How I Cut My Anthropic API Bill by 50% With a Local Python Tool Vibe Coding Problems: 7 Visual Bugs AI Code Generators Always Ship Chinese AI Models 2026: The Agentic Revolution, Hardware Independence, and What It Means for Global Developers The Quiet AI War Inside Your Browser The 12-Line Anti-Bot Trick That Saved Our Airdrop Snapshot From Sybil Farms Building a production-ready SaaS dashboard in Next.js 16 — Recharts, TanStack Table, dark mode, and collapsible sidebar Why 2026 Belongs to Agentic AI (And How to Build Your First Local Agent) It Was 2024 When We Tried to Outsmart the Treasure Hunt Engine RAG 시스템 실전 구축 (v40) I Found a Tool That Generates a Complete .NET 8 or Java Spring Boot API From SQL Schema in 30 Seconds I Added a 4th Agent That Audits My Other Agents. It Caught My Strategist Procrastinating for 3 Weeks. Streaming LLM responses to the browser in Go (Server-Sent Events) How We Publish and Manage Educational Admission Updates at Scale on DailyAxom A prompt is not a conversation. It's a component contract. How to Pass the EAA 2025 Accessibility Audit — A Step-by-Step WCAG Checklist Building an Autonomous MCP Lead Generation System with Hermes Agent LangGraph 워크플로우 템플릿 (v40) How I Built 100 Browser-Based Image Tools With No Server (FFmpeg WASM, PDF-lib, AI Background Removal) Nginx CVE-2026-9256, AI Prompt Injection Defenses, and Claude AI Data Leak Demo Scaling RAG for 10M+ Docs, .md Agent Memory, & Claude Code for Motion Graphics Diagram as Code with draw.io DuckDB Delta, PostgreSQL 17 Migration, & SQLite Optimization Deep Dives Windows 11 Microsoft Account Login Recovery During Internet Restrictions The Linux Commands You Forgot Exist (And Why AI Workflows Make Them Relevant Again) Spec-Driven Development Without an IDE: I Generated NestJS, Go, Spring Boot, Laravel, and Rust Apps From a Single PRD File Components are states Edge SEO y Middleware: Cómo Interceptar a Googlebot y LLMs antes de llegar a tu Servidor Context window exceeded at turn 23. Here's how I track token usage without a tokenizer. My Hermes agent spent $3 before I noticed. Now it can't. My Hermes agent's stop condition was a 40-line if/elif chain. I replaced it with 3 lines. My agent kept hitting context limits. This one function fixed it. Create and configure Azure Firewall Your Hermes agent's audit log is leaking customer emails. Here's a 100-line lib that fixes that. My agent kept forgetting what it was doing. A scratchpad fixed it. I replaced 200 lines of ad-hoc state management in my Hermes agent with one object. Per-Key Rate Limiting for Agent Tool Calls: Stop One User From Breaking Everything Composable Output Guardrails: Filter Agent Responses Before They Reach Users Sanitize Your LLM Message Lists Before Every API Call Thread a Run ID Through Every Agent Call So You Can Debug Anything Normalize Provider Error JSON So Your Agent Can Actually Handle Failures Priority Queue for Agent Sub-Tasks: Stop Processing Low-Priority Work First Static Lint Rules for Your LLM Prompts (Before They Hit Production) tool-call-budgets: Stop Runaway Agent Loops Before They Hit Your Invoice Step Through Your Agent's Failures Like a Debugger The Simplest Stop Condition: A Hard Cap on Agent Loop Iterations Score Your Agent's Responses With a 0.0-1.0 Rubric (No LLM Judge Required) Fix Bad Structured Output by Feeding the Error Back to the Model Building an effective Storyblok Tool Plugin with SvelteKit How to Get Your Renault / Dacia Radio Code for Free RAG 시스템 실전 구축 (v39)
Embedding sing-box in an iOS messenger to bypass Russian DPI (no VPN)
I. Tager · 2026-05-26 · via DEV Community

Embedding sing-box in an iOS messenger to bypass Russian DPI (no VPN)
Our HTTPS API was timing out and our WebSocket refused to upgrade for a growing slice of Russian users. A carrier-level DPI was selectively killing our traffic.

We rejected "tell users to install a VPN" right away. It destroys onboarding conversion, depends on a third-party app staying installed and updated, and shifts the censorship problem from us to whoever runs that VPN. We needed the bypass to live inside our app: no VPN profile, no extra step, no system permission prompt.

This is what we built.

What we're hiding from

Three things can block us:

Blanket IP block. Cheap to deploy, cheap to maintain.
SNI inspection. The censor reads the unencrypted SNI in the TLS ClientHello and drops the connection if the host is on a blocklist.
DPI. Pattern-matching on the wire bytes themselves, looking for protocol fingerprints regardless of destination.
A modern Russian blocklist runs all three. So whatever we send needs to live on an IP that isn't in any "VPN provider" range, carry an SNI that points at a popular unrelated site, and look on the wire like a normal browser hitting that site.

VLESS over TLS with the Reality extension hits all three.

Why VLESS+Reality

A standard TLS-based proxy serves its own certificate for whatever fake domain the operator picked. Active probing finds the mismatch (the IP claims to be microsoft.com but serves a cert with CN=whatever.proxy) and the IP gets flagged.

Reality skips this. The proxy doesn't present its own certificate at all. During the TLS handshake it proxies the handshake to a real upstream site. The client sees:

SNI www.microsoft.com
A real Microsoft TLS certificate
A valid TLS session
If the censor actively probes the IP with arbitrary TLS connections, those connections also get proxied to Microsoft. They see a working microsoft.com from our IP. A client with the pre-shared Reality public key plus short ID and the xtls-rprx-vision flow extension gets routed to the real VLESS endpoint behind the proxy.

VLESS itself is intentionally minimal (stateless, no encryption of its own, no protocol fingerprint to match). With Reality wrapping it, the wire bytes look like a Microsoft TLS session because for the handshake portion they actually are.

Embedding sing-box on iOS

sing-box is a Go-based proxy platform that speaks VLESS+Reality, Hysteria2, and a dozen other transports as both client and server.

We use gomobile bind to compile the parts we need into a native .xcframework. The framework exposes a tiny Go API: pass it a JSON config, get back a service that owns its own listener. We start the service at app launch, point it at a local SOCKS5+HTTP inbound on 127.0.0.1, route the rest of the app through it.

Build command, with the tags that matter:

cd ~/sing-box-src
gomobile bind \
-target ios,iossimulator \
-tags "with_quic,with_utls" \
-o Rcqbox.xcframework \
./mobile

with_quic is required for Hysteria2 (more on that below). with_utls is required for Reality's uTLS-based ClientHello fingerprinting. Without these tags your build silently lacks the protocols and start() throws "QUIC is not included in this build" with no other hint.

Do not include with_naive_outbound. It pulls Cronet, and Cronet's C++ personality routine collides with libsignal_ffi's at link time on iOS. That took an evening to track down.

After building, patch a nullability conflict in the generated Rcqbox.objc.h header (the auto-generated init is marked nullable but its inherited counterpart is nonnull). Without the patch Xcode complains on every clean build:

for h in $(find Rcqbox.xcframework -name "Rcqbox.objc.h"); do
sed -i '' 's/- (nullable instancetype)init;/- (nonnull instancetype)init;/g' "$h"
done

App-level proxy, not NEPacketTunnelProvider

iOS has two ways to route app traffic through a tunnel.

NEPacketTunnelProvider is Apple's official VPN extension API. Captures all device traffic. Creates a VPN profile in Settings.

App-level SOCKS proxy configures URLSession to route through a local SOCKS endpoint. Only the configured URLSession's traffic gets tunneled. No VPN profile, no extra entitlement, no separate process with its own memory limits.

We picked the second. We only need to tunnel our messenger's traffic, not the user's entire device. WebRTC voice calls work better with native NAT traversal, photo uploads to other services shouldn't suddenly route through Frankfurt, and the user shouldn't see a VPN icon in their status bar because of us.

The URLSession side is as boring as it should be:

let config = URLSessionConfiguration.default
config.connectionProxyDictionary = [
"SOCKSEnable": 1,
"SOCKSProxy": "127.0.0.1",
"SOCKSPort": localPort,
]
let session = URLSession(configuration: config)

The WebSocket uses the same config. There's no UI: no toggle, no "connect" button, nothing in Settings → VPN. The app just works.

sing-box config
The VLESS+Reality outbound, stripped to essentials:

{
"type": "vless",
"tag": "relay-yandex",
"server": "165.22.90.214",
"server_port": 443,
"uuid": "<your-uuid>",
"flow": "xtls-rprx-vision",
"tls": {
"enabled": true,
"server_name": "www.yandex.ru",
"utls": { "enabled": true, "fingerprint": "chrome" },
"reality": {
"enabled": true,
"public_key": "<server-pubkey>",
"short_id": "<short-id>"
}
}
}

Local inbound:

{
"type": "mixed",
"tag": "in",
"listen": "127.0.0.1",
"listen_port": 0
}

listen_port: 0 lets the OS pick a free port. We read it back after start() and feed it into URLSession's SOCKSPort.

What bit us: the IP picks who you can reach

We picked a Vultr instance for the first relay because it was cheap and globally distributed. It didn't work from day one. Not in the "running fine, then blocked after a week" sense. In the "TCP RST on every connection from every Russian carrier we tested" sense.

The protocol was fine. The Vultr IP range was already in the blocklist. Small cloud providers are catalogued and their ranges blanket-blocked at the country edge. There's no collateral damage to legitimate users when an entire small-ISP /24 disappears, so the censor pays nothing to drop it.

We skipped the "burn an IP, rotate, burn another" iteration entirely and went straight to a multi-cloud pool on major providers: DigitalOcean, Oracle Cloud Free Tier, Google Cloud, AWS Lightsail. When a major cloud's IP range gets blocked wholesale, a long tail of legitimate sites hosted on those ranges goes with it. The political cost of that collateral damage is enough to make wholesale blocking expensive for the censor, and individual IPs survive.

With Reality, the protocol holds. The IP picks who you can reach.

Don't hardcode the relay

Once you accept that IPs can be blocked from day one, hardcoding the relay address/keys/SNI inside the iOS binary stops being acceptable. Every rotation means an App Store release: 24 hours minimum, 5 days worst case.

We moved the relay list to a signed JSON blob on Cloudflare Workers + KV. On boot, the iOS client fetches the blob, verifies an Ed25519 signature against a public key embedded in the binary, caches the result. A bundled fallback ships in the binary so a fresh install with no network still has something to try.

Rotation is: SSH into a fresh VPS, run the bootstrap script (sing-box install + Reality keypair + UUID + systemd unit, ~2 minutes), bump the source-of-truth YAML, re-sign, push to KV. Plus mirror to a GitHub raw URL because some Russian carriers blanket-block Cloudflare entirely (this surprised us — raw.githubusercontent.com sees much less Russian DPI than CF does).

Clients pick up the new list on next boot or transport re-engage. No App Store release.

The iOS client's urltest outbound pings every relay through our /health endpoint and picks the fastest, re-evaluating every 5 minutes.

What this isn't
The tunnel is not magic. The operator of the relay sees the user's traffic exactly the way the user's ISP otherwise would. Message content is end-to-end encrypted at the application layer (we use libsignal, the Signal Protocol library, for that), so the relay sees only ciphertext bound for our backend, plus metadata: which IP is talking to which IP, when, how much, with what timing. That's enough for a determined adversary to do real harm.

Tunnel and privacy are two different problems solved by two different layers. Reality is about getting your bytes to the destination without the censor killing them. End-to-end encryption is about whether the destination, or anyone in between, can read those bytes. Don't conflate them, and don't tell users the tunnel is hiding their conversation. It's hiding the fact that the conversation is happening with you.

By default the tunnel is off. The app tries direct first. Only if direct fails (or the user explicitly toggles it on) does sing-box engage. This keeps latency optimal for users on unrestricted networks and keeps the relay bandwidth bill manageable.

When Reality wasn't enough

Days after going live with the multi-cloud relay pool, we got reports from testers on one specific carrier class in the Moscow region: whitelist DPI, where the operator allows traffic to a small allowlist of approved sites and selectively kills everything else.

Sing-box logs on every relay showed the same line: REALITY: processed invalid connection, immediate TCP RST. We tried changing the upstream SNI through every Western brand we had (microsoft.com, apple.com, amazon.com), all killed identically. Rebuilt the Reality keypair. Freshly provisioned an IP. Killed.

What was being detected wasn't the destination or the SNI. It was the Reality TLS handshake itself. Reality's uTLS mimics Chrome closely, but there are still subtle distinguishers: extension order, GREASE pattern timing, post-handshake byte distribution. Russian DPI on whitelist carriers had started matching on those.

We needed a transport that didn't fingerprint as TLS at all.

Hysteria2 alongside Reality

Hysteria2 is a UDP-based transport built on QUIC with optional Salamander obfuscation: an XOR layer keyed by a shared password that masks every QUIC packet so the censor can't read the QUIC ClientHello to fingerprint it.

Why this works where Reality didn't:

UDP gets cheaper DPI treatment than TCP. Most censors invest pattern-matching cycles into long-lived TCP streams.
Salamander makes the first packet look like random bytes. There's no QUIC handshake to fingerprint because every byte gets XOR'd before transmission.
sing-box natively carries Hysteria2 once you've built with with_quic.
We added Hysteria2 inbounds alongside the existing VLESS+Reality on the same relay servers. UDP/443 next to the existing TCP/443. Sing-box happily speaks both transports on the same port number across different IP protocols. The iOS-side config got Hysteria2 outbounds in the same urltest block as the VLESS ones. Whichever path responds fastest wins per connection. The loser stays warm for failover.

Hysteria2 outbound, stripped:

{
"type": "hysteria2",
"tag": "relay-yandex-hy2",
"server": "165.22.90.214",
"server_port": 443,
"password": "<user-password>",
"obfs": {
"type": "salamander",
"password": "<obfs-password>"
},
"tls": {
"enabled": true,
"server_name": "www.yandex.ru",
"insecure": true
}
}

Self-signed certificate with CN=www.yandex.ru on the relay, insecure: true on the client. Auth happens out-of-band via the user-password and obfs-password. PKI here would give the censor another fingerprint to match.

Rebuilt Rcqbox.xcframework from sing-box source with with_quic,with_utls, deployed the new signed relay config carrying the Hysteria2 entries (priority 0, tried first). Twenty-four hours later, one message from the affected tester: "It's works!". The same DPI that had killed every Reality variant let Hysteria2-over-Salamander through.

What the live setup looks like

Every relay now carries both transports:

VLESS+Reality on TCP/443, SNI per relay (microsoft.com, apple.com, amazon.com, yandex.ru)
Hysteria2+Salamander on UDP/443, same SNI
Seven entries across DigitalOcean Frankfurt, Oracle Jerusalem, GCP us-central1, AWS Singapore. The iOS client's urltest picks the fastest live path per session. On a clean network the user gets a TCP path (lower latency). On a Reality-killing network they get Hysteria2, on the same relay, no switch noticeable to the user. On a network where one relay's IP is wholesale-blocked they get a different relay's TCP path. On a network where everything is blocked, they get nothing, but they're no worse off than with any other approach.

Adding Hysteria2 didn't replace Reality. TCP paths are still the default on the majority of networks (lower latency, better for the WebSocket). Hy2 is the safety net for when Reality gets fingerprinted, plus a faster fallback on UDP-friendly networks.

The iOS client is RCQ, an anonymous messenger (no phone number, no email, just a 9-digit UIN) currently in open beta. AGPL-3.0, source at github.com/rcq-messenger/rcq-ios.