work@hossein-assar.devReply within 24 hoursفارسی
Hossein AssarHossein AssarDesign & development
Back to projects
Engineering evidence · Full stack

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

Node.js · MariaDB · FreeRADIUS · Vercel
The Peyvand home page

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

01The system is split by what each component can reach. Iran holds the data — database, RADIUS, the VPN daemon and the provisioning logic — and exposes exactly one authenticated endpoint to the outside world. An edge layer abroad holds the integrations that need to talk to PayPal and Telegram, and relays signed requests inward.
02The Telegram bot moved from long polling to webhooks. Polling means holding a connection open to a blocked domain; webhooks invert the direction, which removed the need for an always-on machine entirely — no second server, no tunnel, no running cost.
03Provisioning is idempotent. One function is the single entry point for every payment method: it handles new accounts and renewals, extends from the current expiry rather than resetting it, and short-circuits on a transaction reference it has already seen. Adding a payment provider means writing one adapter and changing nothing else.
04Bilingual front end in Persian and English with full right-to-left and left-to-right switching, on one shared set of design tokens across both the marketing site and the customer dashboard.
05Customer dashboard on Node and Express with httpOnly cookie sessions, showing subscription status and order history.
06ocserv runs on port 443 so the traffic resembles ordinary HTTPS, which is what lets it through restrictive firewalls.

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.

The Persian landing page at peyvand.tech
01 — Persian marketing page
The English version, showing the right-to-left to left-to-right switch
02 — The same page in English
The dashboard showing subscription status and order history
03 — Customer dashboard
A receipt arriving in the bot with approve and reject buttons
04 — Telegram approval flow