Peyvand — a VPN that runs in reverse
A live commercial service giving Iranians abroad an Iranian IP address, so they can reach banks and government systems that reject foreign connections

Peyvand is not a website for academics, a shop or a manufacturer, and it is here on purpose. The three things I sell are built on infrastructure a client never sees — authentication, payments, data that must not be wrong. This is the project where that work is visible. If you want to know whether the person building your site can be trusted with a payment webhook or a database migration, read this one.
About the client
Most VPNs help people get out of a restricted network. Peyvand does the opposite. Iranian banks, the Sana judicial system, university portals, insurance and tax platforms all reject connections from foreign IP addresses — so an Iranian living abroad cannot log into their own bank account. Peyvand gives them an Iranian IP so they can. It is a real commercial service with paying customers, not a demo.
The problem
Halfway through building the commercial layer, I found that the third-party services I was integrating were unreachable from the server. Iran blocks api.telegram.org at the national level — I confirmed it rather than assuming: DNS on the server resolved Telegram to 10.10.34.36, the address the country’s filtering system returns in place of a real one, while the rest of the internet resolved and responded normally. PayPal blocks Iranian addresses from its own side. And Zarinpal, the Iranian payment gateway, does not load from outside Iran — which creates a loop where a customer would need the VPN in order to buy the VPN. Three separate constraints, all discovered after the code was written.
Project scope & my role
I built all of it: ocserv and FreeRADIUS on an Iranian server, a Node and Express provisioning API, a bilingual front end and customer dashboard, a Telegram sales bot with an admin approval flow, and a payments layer redesigned from scratch once I understood what each machine could and could not reach.
Key features
Key challenges
The request signature is the entire security boundary
The Iranian server cannot independently verify anything — it cannot ask PayPal whether a payment cleared, because PayPal is blocked. It has to trust the edge layer’s word. So that one endpoint has four layers rather than one: HMAC-SHA256 over the timestamp, a nonce and the exact raw request bytes; timestamp freshness, so a captured request expires in five minutes; single-use nonces enforced by a unique database key rather than a check-then-act in application code, so two concurrent replays cannot both succeed; and constant-time comparison, with an explicit length check first because the comparison function throws on a length mismatch and the throw itself leaks the length.
Payments that cannot be double-spent
Webhook signatures are verified, because the URL is public and without verification anyone can post “payment completed”. Amounts are checked server-side against the server’s own price table rather than trusted from the request body. And provisioning is idempotent on the payment processor’s transaction ID, so retries and duplicate webhooks cannot deliver the same subscription twice.
I audited my own code and found seven issues, two of them serious
One webhook would have run completely unauthenticated if an environment variable were missing — the library skips the check silently rather than throwing. The other let anyone attach their Telegram account to a stranger’s VPN account without proving ownership. Both are fixed; linking now requires a password, compared in constant time, with deliberately identical failure messages so the endpoint cannot be used to work out which usernames exist.
Serverless functions keep nothing in memory
Every piece of conversation state had to move into the database. A module-level variable holding a user’s place in a conversation works perfectly in local testing and fails silently in production, because the next message lands on a different instance.
A fail2ban filter validated against real log output
Not against what I assumed the logs would say. That caught a case where a username containing the text “auth failed” would otherwise have caused a successful login to be counted as a failed one.
Outcome
A live commercial service with paying customers, operated across two countries. Around 100 automated tests, weighted toward attacks rather than happy paths: replayed requests, tampered payloads, expired and future timestamps, forged signatures, plaintext transport with a valid signature, account theft, and brute force against the login path. Zarinpal onboarding, USDT payments, split-tunnel routing so only Iranian banking traffic is routed through the service, and a second server for failover are on the roadmap.
Project screenshots
Screenshots for this project are being prepared.