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

推荐订阅源

S
Schneier on Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threat Research - Cisco Blogs
C
Cyber Attacks, Cyber Crime and Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
A
Arctic Wolf
Security Latest
Security Latest
Simon Willison's Weblog
Simon Willison's Weblog
I
Intezer
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Troy Hunt's Blog
Latest news
Latest news
Help Net Security
Help Net Security
S
Security Affairs
Webroot Blog
Webroot Blog
The Hacker News
The Hacker News
AI
AI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Tor Project blog
Forbes - Security
Forbes - Security
Google DeepMind News
Google DeepMind News
AWS News Blog
AWS News Blog
Attack and Defense Labs
Attack and Defense Labs
P
Proofpoint News Feed
www.infosecurity-magazine.com
www.infosecurity-magazine.com
H
Help Net Security
L
Lohrmann on Cybersecurity
S
SegmentFault 最新的问题
Google Online Security Blog
Google Online Security Blog
MongoDB | Blog
MongoDB | Blog
Cyberwarzone
Cyberwarzone
The Last Watchdog
The Last Watchdog
S
Securelist
N
News and Events Feed by Topic
S
Secure Thoughts
F
Fortinet All Blogs
博客园_首页
C
Cybersecurity and Infrastructure Security Agency CISA
量子位
M
MIT News - Artificial intelligence
F
Full Disclosure
T
The Blog of Author Tim Ferriss
T
Tailwind CSS Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Microsoft Security Blog
Microsoft Security Blog
I
InfoQ
P
Privacy International News Feed
L
LangChain Blog
Know Your Adversary
Know Your Adversary
C
CERT Recently Published Vulnerability Notes

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
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.