Building Your Own Proxy Farm on Raspberry Pi for Anti-Detect Browsers
Ready to protect your online identity?
Choose your plan and start running undetectable browser profiles today.
Residential and mobile proxies are the backbone of any serious anti-detect browser operation. Commercial proxy providers charge $8–15 per gigabyte for mobile traffic, and the quality varies wildly — shared pools get burned fast, dedicated mobile proxies cost $30–50 per IP per day, and you have zero control over rotation timing or geolocation accuracy. Building your own proxy farm on Raspberry Pi hardware with 4G USB modems gives you full control over IP rotation, predictable costs after the initial investment, and genuinely unique mobile IPs that have never been associated with proxy traffic.
This guide covers the complete process: hardware selection, modem configuration, IP rotation scripting, proxy server setup, and integration with anti-detect browser profiles.
Hardware Requirements and Cost Breakdown
A single proxy node consists of a Raspberry Pi, a USB 4G modem, and a SIM card. Here is the bill of materials for a 10-node farm:
| Component | Per Unit | Qty | Total |
|---|---|---|---|
| Raspberry Pi 4 Model B (2GB) | $45 | 10 | $450 |
| Huawei E3372h USB Modem | $25 | 10 | $250 |
| Prepaid Data SIM (varies) | $10/mo | 10 | $100/mo |
| USB Hub (powered, 7-port) | $20 | 2 | $40 |
| MicroSD Card 32GB | $8 | 10 | $80 |
| Gigabit Ethernet Switch (16-port) | $35 | 1 | $35 |
| Rack/shelf for mounting | $30 | 1 | $30 |
| Total initial investment | $885 | ||
| Monthly operating cost | $100 |
After the initial hardware purchase, you’re paying only for SIM card data plans. At $10 per SIM per month with a typical 30GB data allowance, the cost per gigabyte drops to $0.33 — roughly 25 times cheaper than commercial mobile proxy services. Even with 10 nodes, the setup pays for itself within two months compared to renting the equivalent mobile proxy capacity.
Modem Selection
Not all USB modems work equally well for proxy farming. The critical requirements are:
HiLink mode support: Modems like the Huawei E3372h operate in HiLink mode, where the modem presents itself as a USB Ethernet device with its own DHCP and NAT. This is ideal because the modem handles all the cellular connection management, and the Raspberry Pi simply sees a new network interface.
AT command access: For IP rotation, you need to send AT commands to the modem to force it to disconnect and reconnect to the cellular network, obtaining a new IP. The modem must expose a serial interface alongside the Ethernet interface.
Band compatibility: Ensure the modem supports the LTE bands used by your target carrier. In the US, this typically means Bands 2, 4, 12, and 66 for T-Mobile, or Bands 2, 5, 13, and 66 for Verizon.
Recommended modems in order of preference:
- Huawei E3372h-320 — Widely available, excellent Linux support, reliable AT command interface.
- ZTE MF833V — Good alternative with similar functionality, often cheaper in some regions.
- Quectel EC25 — Mini PCIe form factor (requires USB adapter) but offers the best AT command documentation and band flexibility.
Avoid Huawei E3372s models — the “s” variants operate in “stick” mode, which requires usb_modeswitch and PPP configuration instead of the cleaner HiLink approach.
Operating System and Base Configuration
Flash each Raspberry Pi with Raspberry Pi OS Lite (64-bit). The Lite version has no desktop environment, saving RAM and reducing the attack surface.
# On your workstation, flash the SD card
sudo rpi-imager
# Select: Raspberry Pi OS Lite (64-bit)
# Configure: Enable SSH, set hostname (proxy-01, proxy-02, etc.)
# Set a strong password or SSH key
After first boot, connect via SSH and run the initial configuration:
# Update system
sudo apt update && sudo apt upgrade -y
# Install required packages
sudo apt install -y \
usb-modeswitch \
usb-modeswitch-data \
minicom \
squid \
iptables-persistent \
python3-pip \
python3-serial \
curl \
jq
# Disable unnecessary services to save resources
sudo systemctl disable bluetooth
sudo systemctl disable avahi-daemon
sudo systemctl disable triggerhappy
# Set timezone
sudo timedatectl set-timezone UTC
Modem Detection and Configuration
Insert the USB modem and verify detection:
# Check USB device recognition
lsusb
# Expected: Bus 001 Device 003: ID 12d1:14dc Huawei Technologies Co., Ltd. E33372
# Check network interface
ip link show
# Expected: eth1 (or similar) — the modem's HiLink network interface
# Check serial interface for AT commands
ls /dev/ttyUSB*
# Expected: /dev/ttyUSB0, /dev/ttyUSB1
# AT commands typically go to /dev/ttyUSB0
If the modem is not in HiLink mode and shows only serial interfaces, use usb_modeswitch:
# Force switch to HiLink mode
sudo usb_modeswitch -v 12d1 -p 1f01 -M '55534243123456780000000000000a11062000000000000100000000000000'
Test AT command communication:
# Send AT command to verify modem responds
echo -e "AT\r" > /dev/ttyUSB0
cat /dev/ttyUSB0
# Expected: OK
# Get current IP address from modem
echo -e "AT^DHCP?\r" > /dev/ttyUSB0
# Get signal quality
echo -e "AT+CSQ\r" > /dev/ttyUSB0
Persistent Modem Naming with udev
When you have multiple modems connected (for multi-SIM setups on a single Pi), the /dev/ttyUSB* numbering can change on reboot. Create udev rules to assign persistent names:
# Identify modem serial numbers
udevadm info -a -n /dev/ttyUSB0 | grep serial
# Create udev rule
sudo tee /etc/udev/rules.d/99-modems.rules << 'EOF'
SUBSYSTEM=="tty", ATTRS{serial}=="MODEM_SERIAL_1", SYMLINK+="modem-sim1"
SUBSYSTEM=="tty", ATTRS{serial}=="MODEM_SERIAL_2", SYMLINK+="modem-sim2"
EOF
sudo udevadm control --reload-rules
sudo udevadm trigger
IP Rotation Script
The rotation mechanism works by sending AT commands that force the modem to disconnect from the cellular network and reconnect, which causes the carrier to assign a new IP address from their CGNAT pool. The rotation script handles this process and verifies that a new IP was actually obtained.
#!/usr/bin/env python3
"""ip_rotator.py — Force IP rotation on a Huawei HiLink modem via AT commands."""
import serial
import time
import subprocess
import logging
import requests
import sys
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
log = logging.getLogger('ip-rotator')
MODEM_DEVICE = '/dev/ttyUSB0'
MODEM_IFACE = 'eth1' # HiLink network interface
BAUD_RATE = 115200
MAX_RETRIES = 5
IP_CHECK_URL = 'https://api.ipify.org?format=json'
def send_at_command(ser, command, timeout=5):
"""Send an AT command and return the response."""
ser.write(f'{command}\r\n'.encode())
time.sleep(timeout)
response = ser.read(ser.in_waiting).decode('utf-8', errors='ignore')
return response.strip()
def get_current_ip():
"""Get the external IP address through the modem interface."""
try:
result = requests.get(
IP_CHECK_URL,
timeout=10,
# Force traffic through modem interface
)
return result.json().get('ip')
except Exception as e:
log.warning(f'Failed to get current IP: {e}')
return None
def rotate_ip():
"""Force IP rotation by toggling airplane mode via AT commands."""
old_ip = get_current_ip()
log.info(f'Current IP: {old_ip}')
try:
ser = serial.Serial(MODEM_DEVICE, BAUD_RATE, timeout=3)
# Method 1: Toggle CFUN (radio off/on)
log.info('Sending AT+CFUN=0 (radio off)')
response = send_at_command(ser, 'AT+CFUN=0', timeout=3)
log.info(f'Response: {response}')
time.sleep(2)
log.info('Sending AT+CFUN=1 (radio on)')
response = send_at_command(ser, 'AT+CFUN=1', timeout=5)
log.info(f'Response: {response}')
ser.close()
except serial.SerialException as e:
log.error(f'Serial communication failed: {e}')
# Fallback: toggle the USB interface
log.info('Falling back to interface toggle')
subprocess.run(['sudo', 'ip', 'link', 'set', MODEM_IFACE, 'down'])
time.sleep(3)
subprocess.run(['sudo', 'ip', 'link', 'set', MODEM_IFACE, 'up'])
# Wait for reconnection and verify new IP
for attempt in range(MAX_RETRIES):
time.sleep(5)
new_ip = get_current_ip()
if new_ip and new_ip != old_ip:
log.info(f'IP rotated successfully: {old_ip} -> {new_ip}')
return new_ip
log.info(f'Attempt {attempt + 1}/{MAX_RETRIES}: '
f'IP unchanged or unavailable ({new_ip})')
log.warning(f'IP rotation may have failed after {MAX_RETRIES} attempts')
return get_current_ip()
if __name__ == '__main__':
rotate_ip()
Automatic Rotation via Cron
Set up a cron job to rotate IPs at a configurable interval:
# Rotate every 10 minutes
echo "*/10 * * * * root /usr/local/bin/python3 /opt/proxy-farm/ip_rotator.py >> /var/log/ip-rotation.log 2>&1" | \
sudo tee /etc/cron.d/ip-rotation
For more granular control, you can trigger rotation from the anti-detect browser profile launch via a webhook:
#!/usr/bin/env python3
"""rotation_webhook.py — HTTP endpoint to trigger on-demand IP rotation."""
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
from ip_rotator import rotate_ip
class RotationHandler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path == '/rotate':
new_ip = rotate_ip()
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({
'status': 'ok',
'new_ip': new_ip
}).encode())
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
pass # Suppress request logging
HTTPServer(('0.0.0.0', 8888), RotationHandler).serve_forever()
Setting Up the Proxy Server
Each Raspberry Pi runs Squid as a forward proxy, routing traffic through the 4G modem’s network interface. Configure Squid to listen on the Pi’s Ethernet IP and forward traffic through the modem interface:
# /etc/squid/squid.conf
# Listen on the LAN-facing Ethernet interface
http_port 3128
# Route outbound traffic through the modem interface
tcp_outgoing_address <MODEM_GATEWAY_IP>
# Authentication
auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwords
auth_param basic realm Proxy Farm
acl authenticated proxy_auth REQUIRED
http_access allow authenticated
http_access deny all
# Performance tuning for Raspberry Pi
memory_pools off
cache deny all
dns_nameservers 1.1.1.1 8.8.8.8
# Access log for debugging
access_log /var/log/squid/access.log squid
# Hide proxy headers
via off
forwarded_for delete
request_header_access X-Forwarded-For deny all
Create the password file:
sudo apt install -y apache2-utils
sudo htpasswd -c /etc/squid/passwords proxyuser
sudo systemctl restart squid
For SOCKS5 proxy support (which many anti-detect browsers prefer), install dante-server alongside or instead of Squid:
sudo apt install -y dante-server
# /etc/danted.conf
logoutput: syslog
internal: eth0 port = 1080
external: eth1 # Modem interface
socksmethod: username
user.privileged: root
user.unprivileged: nobody
client pass {
from: 0.0.0.0/0 to: 0.0.0.0/0
log: connect disconnect error
socksmethod: username
}
socks pass {
from: 0.0.0.0/0 to: 0.0.0.0/0
log: connect disconnect error
protocol: tcp udp
}
Network Architecture and Routing
The Raspberry Pi has two network interfaces: eth0 (LAN, connected to your network switch) and eth1 (the 4G modem’s HiLink interface). The proxy server listens on eth0 and routes traffic out through eth1.
┌───────────┐ ┌────────────────────────────────────┐ ┌───────────┐
│ Anti-Det │ │ Raspberry Pi │ │ Carrier │
│ Browser │────▶│ eth0:3128 → squid → eth1 (4G) │────▶│ Network │
│ │ │ Proxy Server │ │ (mobile) │
└───────────┘ └────────────────────────────────────┘ └───────────┘
Configure routing so that proxy traffic exits via the modem while management traffic (SSH) stays on the LAN:
# /etc/network/interfaces.d/routing
# Ensure default route uses modem for proxy traffic
# but management SSH stays on LAN
post-up ip route add default via <MODEM_GATEWAY> dev eth1 table 100
post-up ip rule add fwmark 0x1 table 100
# iptables: mark traffic from squid to use modem route
iptables -t mangle -A OUTPUT -m owner --uid-owner proxy -j MARK --set-mark 0x1
Save iptables rules persistently:
sudo netfilter-persistent save
Central Management Dashboard
With 10 or more Raspberry Pi nodes, managing each one individually over SSH becomes impractical. Create a lightweight management script that runs on a central machine:
#!/usr/bin/env python3
"""farm_manager.py — Central management for the proxy farm."""
import subprocess
import json
import concurrent.futures
NODES = [
{'name': 'proxy-01', 'host': '192.168.1.101', 'proxy_port': 3128, 'rotation_port': 8888},
{'name': 'proxy-02', 'host': '192.168.1.102', 'proxy_port': 3128, 'rotation_port': 8888},
{'name': 'proxy-03', 'host': '192.168.1.103', 'proxy_port': 3128, 'rotation_port': 8888},
# ... up to proxy-10
]
def check_node(node):
"""Check the status of a single proxy node."""
try:
# Check if proxy is responsive
result = subprocess.run(
['curl', '-sf', '--proxy', f'http://{node["host"]}:{node["proxy_port"]}',
'--proxy-user', 'proxyuser:password',
'-m', '10', 'https://api.ipify.org?format=json'],
capture_output=True, text=True, timeout=15
)
if result.returncode == 0:
ip_data = json.loads(result.stdout)
return {
'name': node['name'],
'status': 'online',
'external_ip': ip_data.get('ip'),
'host': node['host'],
}
except Exception as e:
pass
return {
'name': node['name'],
'status': 'offline',
'external_ip': None,
'host': node['host'],
}
def rotate_node(node):
"""Trigger IP rotation on a specific node."""
try:
result = subprocess.run(
['curl', '-sf', '-X', 'POST',
f'http://{node["host"]}:{node["rotation_port"]}/rotate',
'-m', '30'],
capture_output=True, text=True, timeout=35
)
return json.loads(result.stdout)
except Exception as e:
return {'status': 'error', 'message': str(e)}
def status_all():
"""Check status of all nodes in parallel."""
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(check_node, NODES))
print(f'\n{"Node":<12} {"Status":<10} {"External IP":<18} {"Host":<15}')
print('-' * 60)
for r in results:
print(f'{r["name"]:<12} {r["status"]:<10} {r["external_ip"] or "N/A":<18} {r["host"]:<15}')
online = sum(1 for r in results if r['status'] == 'online')
print(f'\nOnline: {online}/{len(NODES)}')
if __name__ == '__main__':
import sys
if len(sys.argv) > 1 and sys.argv[1] == 'rotate':
target = sys.argv[2] if len(sys.argv) > 2 else 'all'
nodes = NODES if target == 'all' else [n for n in NODES if n['name'] == target]
for node in nodes:
print(f'Rotating {node["name"]}...')
result = rotate_node(node)
print(f' Result: {result}')
else:
status_all()
Integration with Anti-Detect Browser Profiles
The final piece is connecting your proxy farm to anti-detect browser profiles. Each proxy node provides a unique mobile IP, and you can assign nodes to profiles either statically or dynamically.
Static Assignment
For long-running profiles that need a consistent IP range (same carrier, same city), assign a dedicated proxy node:
// Profile configuration
{
"name": "Profile-BR-01",
"proxy": {
"type": "http",
"host": "192.168.1.101",
"port": 3128,
"username": "proxyuser",
"password": "secure-password"
},
"fingerprint": {
"os": "windows",
"geoipAuto": true
}
}
With geoipAuto enabled, the anti-detect browser automatically derives the geolocation, timezone, and language from the proxy’s exit IP. Since your 4G modem is physically located in a specific city, the IP will consistently geolocate to that area.
Dynamic Assignment with Rotation
For scenarios where you need fresh IPs on every profile launch, integrate the rotation webhook into your profile launch workflow:
async function launchProfileWithFreshIP(profileId, proxyNode) {
// Step 1: Rotate IP on the assigned proxy node
const rotationResult = await fetch(
`http://${proxyNode.host}:${proxyNode.rotationPort}/rotate`,
{ method: 'POST' }
);
const { new_ip } = await rotationResult.json();
console.log(`Fresh IP obtained: ${new_ip}`);
// Step 2: Launch the anti-detect profile
const launchResult = await fetch(
`http://localhost:7891/api/profiles/${profileId}/launch`,
{ method: 'POST' }
);
return launchResult.json();
}
Load Balancing Across Nodes
If you have more profiles than proxy nodes, implement a round-robin or least-recently-used assignment:
import itertools
class ProxyPool:
def __init__(self, nodes):
self.nodes = nodes
self._cycle = itertools.cycle(nodes)
self._assignments = {}
def assign(self, profile_id):
"""Assign a proxy node to a profile using round-robin."""
if profile_id not in self._assignments:
self._assignments[profile_id] = next(self._cycle)
return self._assignments[profile_id]
def release(self, profile_id):
"""Release a proxy node assignment."""
self._assignments.pop(profile_id, None)
def get_usage(self):
"""Return how many profiles each node is serving."""
usage = {n['name']: 0 for n in self.nodes}
for node in self._assignments.values():
usage[node['name']] += 1
return usage
Monitoring and Maintenance
A proxy farm requires ongoing monitoring. Key metrics to track:
IP uniqueness: Log every IP obtained after rotation and track how often you see repeats. Most carriers cycle through a pool of a few hundred IPs per tower, so you will see repeats — but if you’re getting the same IP three times in a row, the rotation mechanism may need a longer delay between disconnect and reconnect.
Connection stability: 4G connections drop, especially in areas with poor signal. Monitor modem signal strength and implement automatic reconnection:
#!/bin/bash
# /opt/proxy-farm/health_check.sh
# Run via cron every 5 minutes
MODEM_IFACE="eth1"
PING_TARGET="1.1.1.1"
if ! ping -I $MODEM_IFACE -c 3 -W 5 $PING_TARGET > /dev/null 2>&1; then
logger -t proxy-farm "Modem connection lost, restarting interface"
sudo ip link set $MODEM_IFACE down
sleep 5
sudo ip link set $MODEM_IFACE up
sleep 10
if ! ping -I $MODEM_IFACE -c 3 -W 5 $PING_TARGET > /dev/null 2>&1; then
logger -t proxy-farm "Interface restart failed, power-cycling USB"
sudo uhubctl -l 1-1 -a off
sleep 5
sudo uhubctl -l 1-1 -a on
fi
fi
Data usage: Track bandwidth per node to avoid exceeding SIM card data caps. Add Squid access log parsing or use vnstat per interface:
sudo apt install -y vnstat
sudo vnstat --add -i eth1
# View daily usage:
vnstat -i eth1 -d
Security Hardening
Your proxy farm is an attractive target if exposed to the internet. Implement these security measures:
- Firewall: Allow proxy traffic only from your anti-detect browser machine’s IP. Deny all other inbound connections except SSH.
- VPN overlay: Place all proxy nodes and the anti-detect machine on a WireGuard mesh network. Proxy traffic never touches the public internet between your machine and the Pi.
- SSH keys only: Disable password authentication on all nodes.
- Log rotation: Configure logrotate for Squid access logs and rotation logs to prevent SD card exhaustion.
- Read-only filesystem: Mount the root filesystem as read-only with a tmpfs overlay for
/var/logand/tmpto extend SD card lifespan.
Scaling Beyond 10 Nodes
If you need more than 10 IPs, consider multi-modem configurations. A single Raspberry Pi 4 can drive two or even three USB modems simultaneously using a powered USB hub. This doubles or triples your IP capacity without additional Pis. Each modem gets its own network interface (eth1, eth2, eth3) and its own Squid instance on a different port.
For truly large-scale operations (50+ IPs), consider replacing Raspberry Pis with mini-PCs that have multiple USB 3.0 ports and more RAM, and using a configuration management tool like Ansible to maintain consistency across all nodes:
# ansible/playbook.yml
- hosts: proxy_nodes
become: true
tasks:
- name: Deploy rotation script
copy:
src: scripts/ip_rotator.py
dest: /opt/proxy-farm/ip_rotator.py
mode: '0755'
- name: Deploy squid config
template:
src: templates/squid.conf.j2
dest: /etc/squid/squid.conf
notify: restart squid
Conclusion
A DIY proxy farm on Raspberry Pi hardware provides the highest quality mobile proxies at a fraction of commercial costs. The key advantages are exclusivity (your IPs are never shared with other users), control (rotation timing is in your hands), and cost predictability (fixed monthly SIM costs regardless of bandwidth usage within the plan). The initial setup requires several hours per node, but once automated with the scripts and configurations described here, the farm runs autonomously with minimal intervention. For anti-detect browser users who manage more than a handful of profiles, the investment pays for itself within weeks.
Ready to protect your online identity?
Choose your plan and start running undetectable browser profiles today.
Earn 15% lifetime commission on every referral.
Become a Partner →