Vissa webbplatser släpper inte in besökare från EU. Ett vanligt VPN-abonnemang löser det, men kostar varje månad även när du bara behöver det några timmar. Jag byggde i stället en egen VPN-server i USA som inte finns förrän jag ber om den. Den startar med ett tryck på mobilen, stänger av sig själv när jag är klar och kostar i praktiken bara några cent i månaden.

I det här inlägget går jag igenom hur upplägget fungerar, vad som krävs, vad det kostar och varför det inte är rätt verktyg för streaming.
Faktaruta
| Vad det är | En VPN-utgång i USA som startas på begäran |
| Teknik | WireGuard, AWS Lambda, en liten EC2-instans (t4g.nano, Ubuntu) och DNS via Cloudflare |
| Region | AWS us-east-1 (N. Virginia) |
| Uppstartstid | Cirka 30–45 sekunder från tryck till färdig tunnel |
| Avstängning | Automatisk, cirka 30–35 minuter efter att tunneln stängts av |
| Fast kostnad | Några cent per månad (lagring av serverns avbild) |
| Rörlig kostnad | Cirka 0,01 USD per timme som servern körs |
| Datatrafik | 100 GB per månad ingår hos AWS, därefter cirka 0,09 USD/GB |
| Typisk månadskostnad | Cirka 0,3–0,5 USD vid ungefär 20 timmars användning |
| Passar för | Webbsurf, inloggningar, att nå geospärrade sidor |
| Passar inte för | Streaming, stora nedladdningar och sidor som blockerar datacenter-adresser |
Förutsättningar
- Ett AWS-konto och vana vid att klicka runt i konsolen. Du behöver kunna skapa en EC2-instans, en Lambda-funktion och en IAM-policy.
- En domän med DNS hos Cloudflare, och en API-token som bara får redigera DNS i den zonen (så skapar du en token).
- WireGuard-appen på telefonen och en genväg som anropar din funktion. På iPhone gör du det med Genvägar.
- Grundläggande Linux-kunskap för att installera WireGuard och ställa in IP-vidarebefordran på servern.
- Ett budgetlarm i AWS. Gör det innan du bygger, inte efteråt (mer om det nedan).
Räkna med ett par timmar första gången. Därefter är det bara att använda.
Så fungerar det
- Du trycker på en genväg i telefonen. Den anropar en Lambda-funktion via en funktions-URL och skickar med en hemlig token i en header. Utan token svarar funktionen med ett fel.
- Funktionen startar en server från en färdig avbild (AMI) med WireGuard redan installerat och konfigurerat. Finns det redan en körande server återanvänds den i stället för att en ny skapas.
- Funktionen väntar på serverns IP-adress och uppdaterar en DNS-post hos Cloudflare, så att din telefon alltid hittar rätt adress via ett vanligt hostnamn.
- Du slår på tunneln i WireGuard-appen. All trafik går nu ut via USA.
- Servern stänger av sig själv. Ett litet script kontrollerar var femte minut när tunneln senast hade kontakt. Efter 30 minuter utan kontakt, eller efter åtta timmar som längst, stängs servern ner och raderas helt, inklusive disken.
Eftersom servern raderas varje gång finns det ingenting som kostar mellan användningstillfällena, förutom avbilden den startas från.
Kostnadsmedvetenhet: därför blir det så billigt (och hur du håller det så)
AWS tar betalt för tid, lagring och utgående datatrafik. Så här ser det ut i det här upplägget:
| Post | Ungefärlig kostnad |
|---|---|
Server t4g.nano, EC2-priser | cirka 0,004 USD/timme |
| Publik IPv4-adress, VPC-priser | cirka 0,005 USD/timme, bara medan servern körs |
| Lagring av serverns avbild (en ögonblicksbild på några GB) | cirka 0,1 USD/månad |
| Lambda, funktions-URL och loggar | i praktiken 0 vid några få anrop |
| Utgående datatrafik | 100 GB/månad ingår, därefter cirka 0,09 USD/GB |
Sätt dessa skyddsnät på plats så att ett misstag inte blir dyrt:
- Se till att servern verkligen försvinner. Ställ in att den ska termineras (inte bara stoppas) när den stängs av, annars blir disken kvar och kostar.
- Begränsa antalet samtidiga körningar av funktionen till 1. Då kan en läckt URL inte starta en hel flotta av servrar.
- Sätt ett tak på körtiden. Mitt avstängningsscript stänger alltid servern efter åtta timmar, även om tunneln fortfarande är på.
- Ge funktionen minimala rättigheter. Den behöver kunna starta och lista servrar, men inte radera eller ändra något annat.
- Använd AWS Budgets för att få ett mejl när kostnaderna närmar sig en gräns. Budgetar uppdateras några gånger per dygn, så de är ett skyddsnät och inget larm i realtid.
- Kontrollera efter första testerna att inga instanser, lösa diskar eller reserverade IP-adresser ligger kvar.
Varför det här inte är lämpligt för streaming
Det här är en lösning för att surfa, logga in och nå sidor, inte för att titta på film. Det finns två skäl, och det första är kostnaden.
1. Trafikkostnaden äter upp allt. När du laddar ner något via VPN:en räknas det som utgående trafik från AWS. De första 100 GB per månad ingår, men streaming förbrukar dem snabbt. Enligt Netflix går det åt ungefär 3 GB per timme i HD och upp till cirka 7 GB per timme i 4K. Det ger:
- 100 GB räcker till ungefär 33 timmar HD eller 14 timmar 4K, och då har du inte gjort något annat.
- Därefter kostar trafiken ungefär 0,09 USD/GB, alltså runt 0,27 USD per timme HD-streaming.
- Själva servern kostar cirka 0,01 USD per timme. Trafiken blir alltså ungefär 30 gånger dyrare än servern, och det är den som avgör räkningen.
I ett upplägg som är byggt för att kosta några cent är det inte vettigt att bränna trafikbudgeten på film. Ett vanligt VPN-abonnemang med fast pris är då både enklare och billigare.
2. Streamingtjänster blockerar ofta datacenter-adresser. Adresser från molnleverantörer som AWS känns igen och stängs ofta ute, oavsett vilket land de ligger i. Du kan alltså betala för trafiken och ändå inte komma in. Samma sak gäller en del banker och nyhetstjänster.
Gott att veta
- Det hjälper mot enkla geospärrar, inte mot sidor som blockerar hela datacenter-intervall. Då hjälper ingen molnleverantör, bara en bostadsadress.
- Läs villkoren för de tjänster du använder. Att kringgå en geospärr kan strida mot en tjänsts användarvillkor.
- Ingen SSH-åtkomst är ett medvetet val. Servern har ingen inloggning alls, vilket minskar angreppsytan. Vill du uppdatera den bygger du en ny avbild.
- Det är ditt ansvar att hålla avbilden uppdaterad. Planera in en ny avbild då och då, till exempel några gånger per år.
Steg för steg: så sätter du upp det själv
Här är hela bygget, från tom AWS-konsol till färdig genväg. Domännamn, adresser och ID:n nedan är exempel: byt ut dem mot dina egna. Klistra aldrig in riktiga nycklar eller tokens någonstans offentligt.
Steg 0: Förberedelser
- Skapa ett budgetlarm i AWS Budgets innan du bygger något.
- Byt region uppe till höger i konsolen till US East (N. Virginia) us-east-1.
- Skapa en Cloudflare API-token med rättigheten Zone → DNS → Edit och en A-post
vpn(valfri IP, till exempel192.0.2.1, DNS only, TTL 60). Anteckna zonens Zone ID.
Steg 1: Skapa basinstansen
EC2 → Launch instance: namn wg-base, Ubuntu Server LTS arm64, instanstyp t4g.nano, nytt nyckelpar (ED25519, .pem, behövs bara under installationen), ny security group med SSH (TCP 22) endast från My IP, 8 GiB gp3. Logga in när instansen är Running:
chmod 400 ~/Downloads/wg-base-key.pemssh -i ~/Downloads/wg-base-key.pem ubuntu@<INSTANSENS-PUBLIKA-IP>
Steg 2: Installera WireGuard och skapa nycklar
sudo apt update && sudo apt install -y wireguard qrencodecommand -v iptables || sudo apt install -y iptablessudo -i
Kör nästa block efter att du är root (prompten börjar med root@). Klistra inte in det i samma svep som sudo -i, då körs raderna aldrig i rätt skal och inga nycklar skapas.
umask 077mkdir -p /etc/wireguard && cd /etc/wireguardwg genkey | tee server.key | wg pubkey > server.pubwg genkey | tee client.key | wg pubkey > client.pubwg genpsk > psk.keyls -lwc -c server.key server.pub client.key client.pub psk.key
Du ska se fem filer med rättigheterna -rw-------, 45 byte vardera.
Steg 3: IP-vidarebefordran och serverkonfiguration
Kontrollera nätverkskortets namn med ip route show default. På en t4g.nano heter det normalt ens5. Heter det något annat byter du namnet i reglerna.
echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/99-wireguard.confsysctl --system | grep ip_forwardcat > /etc/wireguard/wg0.conf <<EOF[Interface]Address = 10.8.0.1/24ListenPort = 51820PrivateKey = $(cat /etc/wireguard/server.key)PostUp = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o ens5 -j MASQUERADEPostUp = iptables -A FORWARD -i wg0 -j ACCEPTPostUp = iptables -A FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPTPostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o ens5 -j MASQUERADEPostDown = iptables -D FORWARD -i wg0 -j ACCEPTPostDown = iptables -D FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT[Peer]PublicKey = $(cat /etc/wireguard/client.pub)PresharedKey = $(cat /etc/wireguard/psk.key)AllowedIPs = 10.8.0.2/32EOFchmod 600 /etc/wireguard/wg0.confsystemctl enable --now wg-quick@wg0systemctl is-active wg-quick@wg0wg show
wg show ska visa gränssnittet wg0 på port 51820 och din peer, med dolda nycklar.
Steg 4: Öppna UDP 51820 och testa tunneln
I EC2 → instansen → Security → security group → Edit inbound rules: lägg till Custom UDP, port 51820, Anywhere-IPv4. Det är säkert nog eftersom WireGuard inte svarar på paket utan giltig nyckel. Skapa sedan klientkonfigurationen och en QR-kod (instansens publika IP används som Endpoint under testet):
cat > /etc/wireguard/client.conf <<EOF[Interface]PrivateKey = $(cat /etc/wireguard/client.key)Address = 10.8.0.2/32DNS = 1.1.1.1[Peer]PublicKey = $(cat /etc/wireguard/server.pub)PresharedKey = $(cat /etc/wireguard/psk.key)Endpoint = <INSTANSENS-PUBLIKA-IP>:51820AllowedIPs = 0.0.0.0/0, ::/0PersistentKeepalive = 25EOFchmod 600 /etc/wireguard/client.confqrencode -t ansiutf8 < /etc/wireguard/client.conf
Skanna koden i WireGuard-appen, slå på tunneln på mobildata och öppna en sida som visar din IP-adress. Den ska visa serverns adress, och wg show ska visa en latest handshake. ::/0 finns med så att IPv6-trafik inte läcker förbi tunneln. När det fungerar tar du bort klientens privata nyckel från servern, så att den inte hamnar i avbilden:
shred -u /etc/wireguard/client.key /etc/wireguard/client.confls -l /etc/wireguard/
Steg 5: Avstängningsscriptet
Scriptet stänger av servern efter 30 minuter utan handshake, eller efter åtta timmar som längst. Första kvarten efter uppstart gör det ingenting. Jag använder en systemd-timer i stället för cron, eftersom cron inte alltid är installerat.
cat > /usr/local/bin/wg-idle.sh <<'EOF'#!/bin/bashIDLE_MAX=1800 # 30 min utan handshakeUP_MAX=28800 # hårt tak: 8 timmarGRACE=900 # 15 min respit efter bootNOW=$(date +%s)UP=$(cut -d. -f1 /proc/uptime)if [ "$UP" -gt "$UP_MAX" ]; then logger -t wg-idle "max uptime reached, shutting down" /sbin/shutdown -h now exit 0fi[ "$UP" -lt "$GRACE" ] && exit 0LAST=$(wg show wg0 latest-handshakes | awk '{print $2}' | sort -n | tail -1)if [ -z "$LAST" ] || [ "$LAST" -eq 0 ] || [ $((NOW-LAST)) -gt "$IDLE_MAX" ]; then logger -t wg-idle "idle, shutting down" /sbin/shutdown -h nowfiEOFchmod 700 /usr/local/bin/wg-idle.shcat > /etc/systemd/system/wg-idle.service <<'EOF'[Unit]Description=Shut down when WireGuard is idle[Service]Type=oneshotExecStart=/usr/local/bin/wg-idle.shEOFcat > /etc/systemd/system/wg-idle.timer <<'EOF'[Unit]Description=Check WireGuard idle every 5 minutes[Timer]OnBootSec=5minOnUnitActiveSec=5min[Install]WantedBy=timers.targetEOFsystemctl daemon-reloadsystemctl enable --now wg-idle.timersystemctl list-timers wg-idle.timer
En påslagen tunnel räknas som i bruk, eftersom PersistentKeepalive håller handshaken färsk. Servern stängs alltså av ungefär 30 minuter efter att du slagit av tunneln. Gå inte under 300 sekunder för IDLE_MAX.
Steg 6: Städa och skapa AMI:n
Rensa bort sådant som inte ska följa med. Efter det här har du ingen SSH-åtkomst till nya instanser, och det är avsikten: bara UDP 51820 ska vara öppet.
rm -f /root/.bash_history /home/ubuntu/.bash_historyrm -f /home/ubuntu/.ssh/authorized_keys /root/.ssh/authorized_keyscloud-init clean --logsapt cleanhistory -c
Gå sedan till EC2 → markera instansen → Actions → Image and templates → Create image. Namnge den wg-vpn-v1 och bocka i Reboot instance så att kopian blir konsekvent. Din SSH-session bryts, vilket är väntat. Vänta tills AMI:n står som available under Images → AMIs och terminera sedan wg-base.
Steg 7: Launch template
EC2 → Launch Templates → Create launch template:
- Namn
wg-vpn, AMIwg-vpn-v1(under My AMIs), instanstypt4g.nano - Nyckelpar: Don’t include in launch template
- Security group: den befintliga, med UDP 51820 öppet
- Advanced details → Shutdown behavior: Terminate
- Advanced details → Metadata version: V2 only
Terminate gör att servern och disken försvinner när scriptet kör shutdown. Annars blir disken kvar och kostar. Saknar ditt konto default-VPC (mitt gjorde det) måste du ange subnet vid start. Hitta subnet-ID under VPC → Subnets och kontrollera att det ger publik IP. Lambda skickar in det.
Steg 8: Lambda-funktionen
Skapa funktionen. Lambda → Create function → Author from scratch: namn wg-vpn-start, en aktuell Python 3-runtime, arkitektur arm64. Under Configuration → General configuration sätter du Timeout till 1 minut, eftersom standardvärdet 3 sekunder är för kort.
Minimala rättigheter. Under Configuration → Permissions klickar du på rollen och lägger till en inline policy (JSON). Den får bara lista och starta servrar, bara i us-east-1, och kan inte terminera eller ändra något:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:DescribeInstances", "ec2:DescribeLaunchTemplateVersions" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "ec2:RunInstances", "ec2:CreateTags" ], "Resource": "*", "Condition": { "StringEquals": { "aws:RequestedRegion": "us-east-1" } } } ]}
Miljövariabler. Generera en hemlig token med openssl rand -hex 24, spara den i lösenordshanteraren och lägg in följande under Configuration → Environment variables:
| Nyckel | Värde |
|---|---|
SECRET | strängen du genererade |
CF_TOKEN | din Cloudflare API-token |
CF_ZONE_ID | zonens Zone ID |
DNS_NAME | vpn.exempel.se |
SUBNET_ID | till exempel subnet-0123456789abcdef0 |
LT_ID | launch template-ID, lt-... |
Koden. Ersätt innehållet i lambda_function.py och klicka Deploy. Funktionen kontrollerar token, startar en server (eller återanvänder en som redan körs), väntar på IP-adressen och uppdaterar DNS-posten hos Cloudflare. InvalidInstanceID.NotFound fångas och försöks om, eftersom EC2 ibland inte hittar en nystartad instans de första sekunderna. Koden visas i fyra kodblock nedan: klistra in dem efter varandra, i den ordningen, i samma fil.
import jsonimport osimport timeimport urllib.requestimport boto3from botocore.exceptions import ClientErrorec2 = boto3.client("ec2", region_name="us-east-1")SECRET = os.environ["SECRET"]CF_TOKEN = os.environ["CF_TOKEN"]CF_ZONE_ID = os.environ["CF_ZONE_ID"]DNS_NAME = os.environ["DNS_NAME"]SUBNET_ID = os.environ["SUBNET_ID"]LT_ID = os.environ["LT_ID"]def resp(code, body): return { "statusCode": code, "headers": {"Content-Type": "application/json"}, "body": json.dumps(body), }
def find_instance(): r = ec2.describe_instances( Filters=[ {"Name": "tag:Name", "Values": ["wg-vpn"]}, {"Name": "instance-state-name", "Values": ["pending", "running"]}, ] ) for res in r["Reservations"]: for i in res["Instances"]: return i["InstanceId"] return Nonedef start_instance(): r = ec2.run_instances( LaunchTemplate={"LaunchTemplateId": LT_ID, "Version": "$Default"}, SubnetId=SUBNET_ID, MinCount=1, MaxCount=1, TagSpecifications=[ {"ResourceType": "instance", "Tags": [{"Key": "Name", "Value": "wg-vpn"}]} ], ) return r["Instances"][0]["InstanceId"]
import threadingdef pause(seconds): threading.Event().wait(seconds)def wait_for_ip(instance_id, timeout=45): deadline = time.time() + timeout while True: if time.time() >= deadline: return None try: r = ec2.describe_instances(InstanceIds=[instance_id]) ip = r["Reservations"][0]["Instances"][0].get("PublicIpAddress") if ip: return ip except ClientError as err: code = err.response["Error"]["Code"] if code != "InvalidInstanceID.NotFound": raise pause(2)
def cf(method, path, payload=None): req = urllib.request.Request( "https://api.cloudflare.com/client/v4" + path, data=json.dumps(payload).encode() if payload is not None else None, method=method, headers={ "Authorization": "Bearer " + CF_TOKEN, "Content-Type": "application/json", "User-Agent": "wg-vpn-lambda/1.0", }, ) with urllib.request.urlopen(req, timeout=10) as f: return json.loads(f.read())def update_dns(ip): r = cf("GET", f"/zones/{CF_ZONE_ID}/dns_records?type=A&name={DNS_NAME}") records = r.get("result", []) if not records: raise RuntimeError("DNS record " + DNS_NAME + " not found") rec_id = records[0]["id"] cf( "PUT", f"/zones/{CF_ZONE_ID}/dns_records/{rec_id}", {"type": "A", "name": DNS_NAME, "content": ip, "ttl": 60, "proxied": False}, )def lambda_handler(event, context): headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()} if headers.get("x-token") != SECRET: return resp(403, {"error": "forbidden"}) instance_id = find_instance() started = False if not instance_id: instance_id = start_instance() started = True ip = wait_for_ip(instance_id) if not ip: return resp(504, {"error": "no public ip yet", "instance": instance_id}) update_dns(ip) return resp( 200, { "status": "started" if started else "already running", "instance": instance_id, "ip": ip, "dns": DNS_NAME, "note": "wait ~45s, then turn on the tunnel", }, )
Funktions-URL och skydd. Under Configuration → Function URL skapar du en URL med auth type NONE. Skyddet är din hemliga token i headern x-token. Sätt sedan Reserved concurrency till 1 under Configuration → Concurrency, så att en läckt URL inte kan starta fler än en server åt gången.
Steg 9: Testa kedjan
# utan token: ska ge {"error": "forbidden"}curl -s "https://<DIN-FUNCTION-URL>/"# med token: startar en server och uppdaterar DNS (tar 10-20 sekunder)curl -s -H "x-token: <SECRET>" "https://<DIN-FUNCTION-URL>/"# kontrollera att DNS-posten fick serverns IPdig +short vpn.exempel.se @1.1.1.1
Det andra anropet ska ge "status": "started". Kör du det igen direkt efter ska det ge "status": "already running" med samma instans-ID, vilket visar att ingen dubblett skapas.
Steg 10: Klient och genväg
- Ändra
Endpointi WireGuard-tunneln på telefonen tillvpn.exempel.se:51820. - Skapa en genväg i appen Genvägar med tre åtgärder: Hämta innehållet i URL (din funktions-URL, metod GET, rubrik
x-tokenmed din token), Vänta 30 sekunder och Visa avisering med texten ”VPN redo, slå på tunneln”. - Kör genvägen med tunneln avstängd, eftersom anropet går direkt till AWS. Slå på tunneln när aviseringen kommer.
Steg 11: Verifiera avstängningen och städa
Slå av tunneln och vänta 30–35 minuter. Servern ska då gå till Terminated under EC2 → Instances. Gör det här testet innan du litar på upplägget. Kontrollera sedan att inget ligger kvar som kostar: inga instanser som är Running eller Stopped, inga lösa diskar under Volumes, inga Elastic IPs, och bara den ögonblicksbild som hör till din AMI under Snapshots. Behåll AMI:n och dess snapshot, annars försvinner VPN:en.
Felsökning
| Symptom | Orsak och åtgärd |
|---|---|
| Inga nyckelfiler skapades | sudo -i låg i samma inklistring som kommandona. Kör sudo -i först och kommandona sedan. |
| Det går inte att starta från mallen (klagar på subnet) | Kontot saknar default-VPC. Ange subnet vid start, och skicka SubnetId från Lambda. |
Lambda svarar Internal Server Error med InvalidInstanceID.NotFound i loggen | EC2 hittar inte en nystartad instans direkt. Koden ovan försöker om. Loggen finns i CloudWatch under /aws/lambda/wg-vpn-start. |
Funktionen svarar forbidden trots token | Rubriken heter x-token och värdet måste vara exakt lika som SECRET, utan mellanslag. |
| Ingen handshake | Kontrollera att UDP 51820 är öppen och att dig visar serverns IP. Växla flygplansläge på telefonen för att rensa cachad DNS. |
| Handshake men inget internet | Kontrollera sysctl net.ipv4.ip_forward, NAT-regeln (iptables -t nat -L POSTROUTING -n -v) och att nätverkskortets namn stämmer. |
| Servern terminerar inte | Vänta 30–35 minuter. Kontrollera att Shutdown behavior är Terminate. Utan SSH läser du systemloggen under EC2 → Monitor and troubleshoot → Get system log. |
Vill du uppdatera servern bygger du en ny AMI: starta en testinstans med ett tillfälligt nyckelpar, ändra det som behövs, städa enligt steg 6, skapa wg-vpn-v2 och sätt den nya versionen av launch template som standard. Lambda använder $Default, så koden behöver inte ändras.
Vanliga frågor
Hur mycket kostar en egen VPN på AWS?
Vid måttlig användning, runt 20 timmar i månaden, handlar det om cirka 0,3–0,5 USD i månaden. Den fasta delen är bara lagringen av serverns avbild, ungefär 0,1 USD.
Hur snabbt är den klar att använda?
Cirka 30–45 sekunder efter att du tryckt på genvägen, eftersom servern startar medan funktionen väntar på IP-adressen.
Vad händer om jag glömmer att stänga av tunneln?
Servern stängs ner efter högst åtta timmar, så en glömd tunnel kostar några cent och inte mer.
Går det att streama via den?
Tekniskt går det, men det rekommenderas inte. Trafikkostnaden blir ungefär 30 gånger högre än serverkostnaden, och många streamingtjänster blockerar ändå AWS-adresser.
Kan flera enheter dela samma VPN?
Ja, men varje enhet behöver en egen WireGuard-nyckel. Det byggs in i serverns avbild.
Sammanfattning
En VPN som bara finns när du behöver den är billig, enkel att kontrollera och inte beroende av ett abonnemang. Den passar bra för att surfa och logga in från USA. Håll koll på trafiken, sätt skyddsnäten på plats innan du börjar, och använd något annat om målet är att titta på film.
Priser och funktioner hos AWS och Cloudflare ändras. Kontrollera aktuella uppgifter hos leverantörerna innan du bygger.
Lämna ett svar