{"id":314,"date":"2026-10-02T04:59:24","date_gmt":"2026-10-02T04:59:24","guid":{"rendered":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/"},"modified":"2026-10-02T04:59:24","modified_gmt":"2026-10-02T04:59:24","slug":"building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes","status":"publish","type":"post","link":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/","title":{"rendered":"Building a Mobile Proxy Farm: Linux, Network Architecture, Docker and Kubernetes"},"content":{"rendered":"<p>2 ott 2026 \u00b7 @juan<\/p>\n<h2><strong>Introduction<\/strong><\/h2>\n<p>A mobile proxy farm is, stripped of marketing language, a rack of cellular modems attached to Linux hosts, each modem exposing its carrier-assigned connection through a proxy endpoint that remote clients can use. The engineering problem is less glamorous than the term suggests. It is mostly about USB stability, deterministic device naming, per-interface routing, process isolation, controlled IP rotation and observability across dozens or hundreds of flaky radio links.<\/p>\n<p>This guide walks through that stack layer by layer: hardware, Linux modem management, network architecture with policy routing and network namespaces, the proxy daemons themselves, an IP rotation service, containerization with Docker and orchestration with Kubernetes, and finally monitoring, security and compliance. I include working configuration and code snippets throughout. They are written to be readable and adaptable rather than copy-paste production code; test everything on your own hardware and carrier before relying on it.<\/p>\n<p>I also mention a few open-source projects hosted on GitHub that are widely used as building blocks. I name repositories by their owner and project name rather than linking to them, so you can search for them directly.<\/p>\n<h2><strong>What a Mobile Proxy Actually Is<\/strong><\/h2>\n<p>A conventional datacenter proxy forwards traffic from a server with a static IP address allocated to a hosting provider. A <a href=\"https:\/\/alumnit.ca\/how-to-choose-a-residential-proxy-server-a-buying-guide\/\">residential proxy<\/a> forwards traffic through a fixed-line consumer connection. A <a href=\"https:\/\/tlo.mit.edu\/industry-entrepreneurs\/available-technologies\/application-data-proxies-and-cognitive-firewalls\" data-id=\"6abf3530100114f25d2ec07c\" target=\"_blank\" rel=\"nofollow\">mobile proxy<\/a> forwards traffic through a cellular data connection: 4G LTE or 5G, via a SIM card on a mobile network operator (MNO) or mobile virtual network operator (MVNO).<\/p>\n<p>The defining property of mobile egress is carrier-grade NAT (CGNAT). Most mobile operators do not give each subscriber a unique public IPv4 address. Instead, the modem receives an address from a private or shared range, frequently from the block that RFC 6598 reserves for exactly this purpose, and the carrier translates many subscribers onto a pool of public IPv4 addresses at its NAT gateways. A single public IPv4 address at a given moment may therefore represent many real users.<\/p>\n<p>That has two consequences engineers need to understand:<\/p>\n<ol>\n<li><strong>The public IP is not yours.<\/strong> You cannot see it on the modem interface. The address shown by ip addr on the host is the carrier-side private address. To learn the public egress IP, you must ask an external service from that interface.<\/li>\n<li><strong>The public IP is not stable.<\/strong> It can change when the data session is re-established, when the device re-registers, or according to the carrier&#8217;s NAT pool behavior. Rotation in a mobile proxy farm is essentially the deliberate triggering of a new data session.<\/li>\n<\/ol>\n<p>IPv6 adds another dimension. Many carriers now assign native IPv6 prefixes to mobile devices, often a \/64 per session. Whether that matters depends on the target services your clients reach and whether your proxy layer supports IPv6 egress. I keep the examples below IPv4-centric because that is where most operational complexity lives, but I note IPv6 implications where relevant.<\/p>\n<h2><strong>Legitimate Use Cases and Acceptable Use<\/strong><\/h2>\n<p>Mobile proxies have real engineering and business uses, including:<\/p>\n<ul>\n<li><strong>Ad verification:<\/strong> checking that ads render correctly and are not being served fraudulently to mobile users in a given region.<\/li>\n<li><strong>Mobile app and website QA:<\/strong> testing how services behave on real carrier networks, including geolocation, CDN routing, captive behaviors and latency.<\/li>\n<li><strong>Localization testing:<\/strong> verifying pricing, content and compliance banners as seen by users on specific national carriers.<\/li>\n<li><strong>Network research and measurement:<\/strong> studying carrier NAT behavior, DNS resolution and routing paths.<\/li>\n<li><strong>Brand protection and price monitoring<\/strong>, within the limits of the target sites&#8217; terms and applicable law.<\/li>\n<\/ul>\n<p>The same infrastructure can be abused for credential stuffing, ad fraud, fake account creation, scalping and evasion of fraud controls. Those uses can be illegal, and they also harm the operator: carriers suspend SIMs linked to abuse, and providers that tolerate abuse get blocklisted and sued. I treat acceptable-use enforcement as part of the architecture, not an afterthought, and the final section covers it in detail. Before building anything, read your carrier&#8217;s terms of service. Many consumer and IoT data plans explicitly prohibit reselling connectivity or running servers, and operating a commercial proxy service on such plans can breach contract.<\/p>\n<h2><strong>The Hardware Layer<\/strong><\/h2>\n<p>Everything above the hardware inherits its instability. Most production incidents in modem farms trace back to power, USB enumeration, heat or radio conditions, not to software.<\/p>\n<h3><strong>Choosing the cellular device<\/strong><\/h3>\n<p>There are four common device classes, each with different control surfaces.<\/p>\n<p><strong>USB dongles in HiLink mode.<\/strong> Devices such as the Huawei E3372 in its HiLink firmware variant present themselves to the host as a USB Ethernet adapter (CDC-ECM or RNDIS) and run their own internal router with a web UI, usually on . The host sees a NAT&#8217;d LAN connection, which means double NAT on top of the carrier&#8217;s CGNAT. They are simple to bring up but are controlled through the device&#8217;s HTTP API rather than standard Linux modem tooling, and every dongle defaults to the same subnet, which you must work around.<\/p>\n<p><strong>USB dongles in stick (modem) mode.<\/strong> The same hardware families with &#8220;stick&#8221; firmware expose serial AT ports and a QMI or MBIM data interface. These are managed by ModemManager, give the host the carrier-assigned IP directly, and integrate much more cleanly with Linux routing.<\/p>\n<p><strong>M.2 or mini PCIe modules on USB carrier boards.<\/strong> Industrial LTE and 5G modules, for example Quectel&#8217;s EC25 family for LTE or RM5xx series for 5G, mounted on USB adapter boards with external antennas. They cost more, but offer better thermal behavior, proper antenna connectors, documented AT command sets and longer product lifetimes. For any farm intended to run continuously, I consider modules the most defensible choice.<\/p>\n<p><strong>Android phones.<\/strong> Phones tethered over USB provide good radios and batteries that smooth out power fluctuations, but they introduce an entire operating system to manage, battery swelling risks when kept permanently charged, and control via ADB rather than standard modem interfaces. They are best treated as a niche option.<\/p>\n<h3><strong>USB topology and controllers<\/strong><\/h3>\n<p>USB is the weakest link. Each modem enumerates as several interfaces and endpoints, and USB host controllers have finite endpoint and bandwidth resources. Large numbers of devices on one controller can produce enumeration failures that look like random modem crashes.<\/p>\n<p>Practical guidance:<\/p>\n<ul>\n<li>Spread modems across multiple physical host controllers rather than chaining many hubs onto one root port. On x86 hosts, a PCIe USB expansion card with several independent controllers is often more reliable than a single motherboard controller fanned out through hubs.<\/li>\n<li>Inspect the tree with lsusb -t to see which controller and hub each device sits on.<\/li>\n<li>Use industrial powered hubs with per-port power switching. Per-port power control lets software hard-reset a hung modem by cutting its power, which recovers states that no software command can.<\/li>\n<\/ul>\n<p>For that last point, the open-source tool uhubctl, from the GitHub repository <strong>mvp\/uhubctl<\/strong>, controls per-port power on hubs that implement it correctly. Hub compatibility varies, so check before buying hardware.<\/p>\n<p># List hubs that support per-port power switching<\/p>\n<p>sudo uhubctl<\/p>\n<p># Power-cycle port 3 on hub 1-1.2<\/p>\n<p>sudo uhubctl -l 1-1.2 -p 3 -a cycle -d 5<\/p>\n<h3><strong>Power and heat<\/strong><\/h3>\n<p>Cellular modems draw short current spikes during transmission, especially at the cell edge where they transmit at high power. An underpowered hub causes brown-outs that reset modems at random. Budget generously for hub power supplies, use short, good-quality cables, and avoid bus-powered hubs entirely.<\/p>\n<p>Heat is the second silent killer. Densely packed dongles throttle or drop connections as they warm up. Provide airflow, space devices apart, and monitor module temperature where the hardware reports it through AT commands or ModemManager.<\/p>\n<h3><strong>Radio and SIM considerations<\/strong><\/h3>\n<p>Signal quality affects throughput, latency and session stability. Use external antennas where the device supports them, place antennas away from the metal rack, and record signal metrics such as RSRP, RSRQ and SINR for every modem so you can correlate failures with radio conditions.<\/p>\n<p>SIM management is an operational discipline of its own. Track each SIM&#8217;s ICCID, plan, APN, data limits and the physical slot it occupies. Many farms lose hours to a single misplaced SIM. Treat SIM inventory as configuration data stored alongside your infrastructure code.<\/p>\n<h3><strong>The host<\/strong><\/h3>\n<p>For small farms, an x86 mini PC running a mainstream Linux distribution such as Debian or Ubuntu Server works well. Single-board computers can work for a handful of modems but often share limited USB bandwidth across all ports. Whatever you choose, use a recent kernel, because driver fixes for qmi_wwan, cdc_mbim and option arrive continuously.<\/p>\n<h2><strong>Linux Modem Management<\/strong><\/h2>\n<p>Once hardware is connected, the host must reliably detect each modem, put it into the correct mode, bring up a data session and give the resulting network interface a stable identity. This is where most homegrown farms accumulate fragile shell scripts. A cleaner approach leans on the standard Linux tooling.<\/p>\n<h3><strong>Mode switching<\/strong><\/h3>\n<p>Many USB dongles first enumerate as a virtual CD-ROM containing Windows drivers. The usb_modeswitch package, together with its udev rules and device database, switches them into their networking mode automatically. Check lsusb before and after plugging in a device: the product ID usually changes once the switch succeeds.<\/p>\n<p>sudo apt install usb-modeswitch usb-modeswitch-data modemmanager libqmi-utils libmbim-utils jq<\/p>\n<p>lsusb<\/p>\n<h3><strong>ModemManager and mmcli<\/strong><\/h3>\n<p>ModemManager is the standard Linux daemon for controlling mobile broadband devices. It speaks QMI, MBIM and AT protocols and exposes a uniform D-Bus API. Its command-line client, mmcli, is all you need for most automation, and it supports JSON output with -J, which makes scripting far less brittle than parsing human-readable text.<\/p>\n<p># List modems<\/p>\n<p>mmcli -L<\/p>\n<p># Inspect modem 0 as JSON<\/p>\n<p>mmcli -m 0 -J | jq &#8216;.modem.generic | {state, &#8220;equipment-identifier&#8221;, &#8220;signal-quality&#8221;}&#8217;<\/p>\n<p># Connect using the carrier&#8217;s APN<\/p>\n<p>mmcli -m 0 &#8211;simple-connect=&#8221;apn=internet,ip-type=ipv4v6&#8243;<\/p>\n<p># Inspect the resulting bearer (index shown in the modem output)<\/p>\n<p>mmcli -b 0 -J | jq &#8216;.bearer | {status, &#8220;ipv4-config&#8221;}&#8217;<\/p>\n<p>The bearer output tells you which kernel network interface carries the session and the IP configuration the carrier assigned: address, prefix, gateway and DNS. Note that ModemManager does not configure the interface itself unless NetworkManager is driving it. If you use ModemManager standalone, as I recommend for farms, your own tooling must apply that configuration.<\/p>\n<h3><strong>Modem indexes are not stable<\/strong><\/h3>\n<p>One trap catches nearly everyone. mmcli modem indexes (-m 0, -m 1) are assigned in detection order and increment whenever a modem re-enumerates. After a few resets, modem 3 may become modem 17. Never store indexes in configuration. Key everything on a stable hardware identifier, typically the IMEI reported as equipment-identifier, and resolve the current index at runtime.<\/p>\n<p>#!\/usr\/bin\/env bash<\/p>\n<p># : print the current mmcli index for a given IMEI<\/p>\n<p>set -euo pipefail<\/p>\n<p>IMEI=&#8221;$1&#8243;<\/p>\n<p>for path in $(mmcli -L -J | jq -r &#8216;.&#8221;modem-list&#8221;[]&#8217;); do<\/p>\n<p>idx=&#8221;${path##*\/}&#8221;<\/p>\n<p>if [[ &#8220;$(mmcli -m &#8220;$idx&#8221; -J | jq -r &#8216;.modem.generic.&#8221;equipment-identifier&#8221;&#8216;)&#8221; == &#8220;$IMEI&#8221; ]]; then<\/p>\n<p>echo &#8220;$idx&#8221;; exit 0<\/p>\n<p>fi<\/p>\n<p>done<\/p>\n<p>echo &#8220;modem $IMEI not found&#8221; &gt;&amp;2; exit 1<\/p>\n<h3><strong>Stable interface names<\/strong><\/h3>\n<p>Kernel interface names such as wwan0 or wwp0s20f0u3i4 either change with enumeration order or are cryptic. Assign deterministic names based on the physical USB path using systemd .link files. First, find the path:<\/p>\n<p>udevadm info \/sys\/class\/net\/wwan0 | grep ID_PATH=<\/p>\n<p># E: ID_PATH=pci-0000:00:14.0-usb-0:2.3:1.4<\/p>\n<p>Then create a link file that binds that path to a name you choose:<\/p>\n<p># \/etc\/systemd\/network\/<\/p>\n<p>[Match]<\/p>\n<p>Path=pci-0000:00:14.0-usb-0:2.3:1.4<\/p>\n<p>[Link]<\/p>\n<p>Name=wwm03<\/p>\n<p>Now the modem in hub port 2.3 is always wwm03, regardless of enumeration order. Because the name is tied to the physical port, swapping a failed modem for a replacement in the same port preserves all downstream configuration. Your inventory then maps three identifiers together: physical port, IMEI and SIM ICCID.<\/p>\n<\/p>\n<h3><strong>Bringing up an interface without NetworkManager<\/strong><\/h3>\n<p>With ModemManager standalone, a small script reads the bearer configuration and applies it to the interface. For QMI devices, the kernel driver often needs raw-IP mode, which ModemManager typically sets; verify with cat \/sys\/class\/net\/&lt;iface&gt;\/qmi\/raw_ip.<\/p>\n<p>#!\/usr\/bin\/env bash<\/p>\n<p># &lt;imei&gt; &lt;ifname&gt; &lt;apn&gt;<\/p>\n<p>set -euo pipefail<\/p>\n<p>IMEI=&#8221;$1&#8243;; IFACE=&#8221;$2&#8243;; APN=&#8221;$3&#8243;<\/p>\n<p>M=$(\/usr\/local\/bin\/ &#8220;$IMEI&#8221;)<\/p>\n<p>mmcli -m &#8220;$M&#8221; &#8211;enable<\/p>\n<p>mmcli -m &#8220;$M&#8221; &#8211;simple-connect=&#8221;apn=${APN},ip-type=ipv4&#8243;<\/p>\n<p>B=$(mmcli -m &#8220;$M&#8221; -J | jq -r &#8216;.modem.generic.bearers[0]&#8217; | awk -F\/ &#8216;{print $NF}&#8217;)<\/p>\n<p>CFG=$(mmcli -b &#8220;$B&#8221; -J)<\/p>\n<p>ADDR=$(jq -r &#8216;.bearer.&#8221;ipv4-config&#8221;.address&#8217; &lt;&lt;&lt;&#8220;$CFG&#8221;)<\/p>\n<p>PFX=$(jq -r &#8216;.bearer.&#8221;ipv4-config&#8221;.prefix&#8217; &lt;&lt;&lt;&#8220;$CFG&#8221;)<\/p>\n<p>GW=$(jq -r &#8216;.bearer.&#8221;ipv4-config&#8221;.gateway&#8217; &lt;&lt;&lt;&#8220;$CFG&#8221;)<\/p>\n<p>ip link set &#8220;$IFACE&#8221; up<\/p>\n<p>ip addr flush dev &#8220;$IFACE&#8221;<\/p>\n<p>ip addr add &#8220;${ADDR}\/${PFX}&#8221; dev &#8220;$IFACE&#8221;<\/p>\n<p>echo &#8220;$GW&#8221; &gt; &#8220;\/run\/modems\/${IFACE}.gw&#8221;<\/p>\n<p>Notice what the script deliberately does not do: it does not add a default route. With dozens of modems, adding default routes to the main routing table would make the host&#8217;s egress unpredictable. Routing is handled separately, per modem, which is the subject of the next section.<\/p>\n<p>Also tell NetworkManager, if installed, to leave these interfaces alone, for example with an unmanaged-devices=interface-name:wwm* entry in its configuration. Two daemons fighting over the same interface is a classic source of intermittent failures.<\/p>\n<h2><strong>Network Architecture: Isolating Each Modem<\/strong><\/h2>\n<p>The central networking requirement of a proxy farm is simple to state and easy to get wrong: <strong>traffic entering the proxy for modem N must leave through modem N, and only modem N.<\/strong> If a modem drops, its traffic must fail rather than silently leak out of another modem or the host&#8217;s wired uplink. That leak would expose the wrong IP, break client expectations and potentially route customer traffic through infrastructure it should never touch.<\/p>\n<p>There are two mainstream ways to <a href=\"https:\/\/stackoverflow.com\/questions\/5426279\/how-shall-i-build-up-my-own-proxy-server\" data-id=\"6abf3530100114f25d2ec07d\" target=\"_blank\">achieve this on Linux<\/a>: policy-based routing in a shared network stack, and network namespaces. I strongly prefer namespaces for anything beyond a few modems, but understanding policy routing helps explain why.<\/p>\n<h3><strong>Option A: policy routing with source-based rules<\/strong><\/h3>\n<p>Linux supports multiple routing tables and rules that choose a table based on packet attributes. You give each modem its own table, place a default route through that modem in it, and add a rule saying traffic sourced from that modem&#8217;s address uses that table.<\/p>\n<p># Table 103 for wwm03<\/p>\n<p>IFACE=wwm03<\/p>\n<p>TABLE=103<\/p>\n<p>ADDR=$(ip -4 -o addr show dev $IFACE | awk &#8216;{print $4}&#8217; | cut -d\/ -f1)<\/p>\n<p>ip route replace default dev $IFACE table $TABLE<\/p>\n<p>ip rule add from $ADDR lookup $TABLE priority 1000$((TABLE % 100))<\/p>\n<p>For raw-IP point-to-point interfaces, a device route without a gateway is sufficient; for Ethernet-style interfaces such as HiLink dongles, add via &lt;gateway&gt;. The proxy daemon must then bind its outgoing sockets to the modem&#8217;s source address.<\/p>\n<p>This works, but it is fragile at scale. Every rotation changes the source address, so rules must be rewritten atomically. A process that forgets to bind its source address falls through to the main table and leaks. DNS lookups made by the proxy may go out the wrong interface unless handled carefully. And all modems share one conntrack table and one set of firewall rules, which complicates debugging.<\/p>\n<p>An alternative within this model is marking packets with an fwmark per user or per listening port and routing by mark, but it inherits the same leak-on-misconfiguration property.<\/p>\n<h3><strong>Option B: one network namespace per modem<\/strong><\/h3>\n<p>A network namespace is an isolated copy of the Linux network stack: its own interfaces, routing tables, firewall rules and sockets. If you move a modem&#8217;s interface into a dedicated namespace and run that modem&#8217;s proxy process inside the same namespace, the only path to the internet that process can see is the modem. When the modem goes down, the namespace has no default route, and connections fail closed. Isolation becomes a property of the structure rather than of correct rule-writing.<\/p>\n<p>The architecture looks like this:<\/p>\n<p>+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- host (root netns) &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-+<\/p>\n<p>client &#8211;TCP&#8211;&gt; | eth0 :20003 &#8211;DNAT&#8211;&gt; veth-h03 |<\/p>\n<p>| | |<\/p>\n<p>| +&#8212;&#8212; netns m03 &#8212;&#8211;v&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;+ |<\/p>\n<p>| | veth-n03 &lt;- proxy listens | |<\/p>\n<p>| | wwm03 (modem) default route -&gt; internet | |<\/p>\n<p>| +&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;+ |<\/p>\n<p>+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;+<\/p>\n<p>The setup script:<\/p>\n<p>#!\/usr\/bin\/env bash<\/p>\n<p># &lt;id&gt; &lt;ifname&gt;<\/p>\n<p>set -euo pipefail<\/p>\n<p>ID=&#8221;$1&#8243;; IFACE=&#8221;$2&#8243;<\/p>\n<p>NS=&#8221;m${ID}&#8221;<\/p>\n<p>VH=&#8221;veth-h${ID}&#8221;; VN=&#8221;veth-n${ID}&#8221;<\/p>\n<p>HOST_IP=&#8221;10.200.${ID#0}.1&#8243;; NS_IP=&#8221;10.200.${ID#0}.2&#8243;<\/p>\n<p>ip netns add &#8220;$NS&#8221; 2&gt;\/dev\/null || true<\/p>\n<p>ip link set &#8220;$IFACE&#8221; netns &#8220;$NS&#8221;<\/p>\n<p>ip link add &#8220;$VH&#8221; type veth peer name &#8220;$VN&#8221;<\/p>\n<p>ip link set &#8220;$VN&#8221; netns &#8220;$NS&#8221;<\/p>\n<p>ip addr add &#8220;${HOST_IP}\/30&#8221; dev &#8220;$VH&#8221;; ip link set &#8220;$VH&#8221; up<\/p>\n<p>ip netns exec &#8220;$NS&#8221; ip link set lo up<\/p>\n<p>ip netns exec &#8220;$NS&#8221; ip addr add &#8220;${NS_IP}\/30&#8221; dev &#8220;$VN&#8221;<\/p>\n<p>ip netns exec &#8220;$NS&#8221; ip link set &#8220;$VN&#8221; up<\/p>\n<p>ip netns exec &#8220;$NS&#8221; ip link set &#8220;$IFACE&#8221; up<\/p>\n<p># The modem is the ONLY default route inside the namespace<\/p>\n<p>ip netns exec &#8220;$NS&#8221; ip route replace default dev &#8220;$IFACE&#8221;<\/p>\n<p>Two details are easy to miss.<\/p>\n<p><strong>Moving the interface resets its addressing.<\/strong> When an interface changes namespace, its addresses are removed. Run the bring-up logic from the previous section after the move, executing the ip addr commands inside the namespace with ip netns exec. ModemManager itself stays in the root namespace, because it talks to the modem over USB control ports, not through the network interface.<\/p>\n<p><strong>DNS must be namespace-local.<\/strong> ip netns exec bind-mounts files from \/etc\/netns\/&lt;name&gt;\/ over their \/etc counterparts, so you can give each namespace its own resolver configuration, ideally pointing at the carrier DNS servers reported in the bearer:<\/p>\n<p>mkdir -p \/etc\/netns\/m03<\/p>\n<p>printf &#8216;nameserver %s\\n&#8217; $DNS1 $DNS2 &gt; \/etc\/netns\/m03\/resolv.conf<\/p>\n<p>This prevents a subtle leak in which proxied connections exit through the modem but DNS lookups exit through the host&#8217;s uplink, revealing a different network to the resolver and producing geolocation mismatches.<\/p>\n<h3><strong>Exposing namespaces to clients<\/strong><\/h3>\n<p>The proxy inside each namespace listens on its veth address. On the host, a DNAT rule maps a public port to that address. Using nftables:<\/p>\n<p># \/etc\/nftables.d\/proxy-farm.nft<\/p>\n<p>table ip farm {<\/p>\n<p>chain prerouting {<\/p>\n<p>type nat hook prerouting priority dstnat; policy accept;<\/p>\n<p>tcp dport 20003 dnat to <\/p>\n<p>tcp dport 21003 dnat to <\/p>\n<p>}<\/p>\n<p>chain forward {<\/p>\n<p>type filter hook forward priority filter; policy drop;<\/p>\n<p>ct state established,related accept<\/p>\n<p>iifname &#8220;eth0&#8221; oifname &#8220;veth-h*&#8221; ip daddr tcp dport { 3128, 1080 } accept<\/p>\n<p>}<\/p>\n<p>}<\/p>\n<p>Enable forwarding with sysctl -w net.ipv4.ip_forward=1. Note the forward chain&#8217;s default drop policy: nothing from the namespaces can traverse the host toward its own uplink unless explicitly allowed, which closes the last leak path.<\/p>\n<p>In larger designs, clients do not connect to individual modem ports at all. A front-door gateway authenticates the client, chooses a modem according to sticky-session or rotation policy, and forwards to the right namespace. I return to that pattern in the Kubernetes section.<\/p>\n<h2><strong>The Proxy Layer<\/strong><\/h2>\n<p>With one isolated namespace per modem, the proxy daemon becomes almost trivial: it listens on the namespace&#8217;s veth address and opens outbound connections normally, because the only route available is the modem. Two mature open-source daemons dominate small and mid-sized deployments.<\/p>\n<h3><strong>3proxy<\/strong><\/h3>\n<p>3proxy, maintained by Vladimir Dubrovin and hosted on GitHub as <strong>z3APA3A\/3proxy<\/strong> (also mirrored under the <strong>3proxy<\/strong> organization), is a compact C proxy server that bundles HTTP\/HTTPS (CONNECT), SOCKS4\/5 and several other proxy services in a single binary. It is lightweight, which matters when you run one instance per modem, and its configuration language is terse.<\/p>\n<p>A per-modem configuration:<\/p>\n<p># \/etc\/3proxy\/m03.cfg<\/p>\n<p>nserver # replace with carrier DNS from the bearer<\/p>\n<p>nscache 65536<\/p>\n<p>timeouts 1 5 30 60 180 1800 15 60<\/p>\n<p>log \/var\/log\/3proxy\/m03.log D<\/p>\n<p>logformat &#8220;L%Y-%m-%dT%H:%M:%S %N %p %E %U %C:%c %R:%r %O %I %T&#8221;<\/p>\n<p>rotate 14<\/p>\n<p>users alice:CR:$1$Qn3b$exampleexampleexample0<\/p>\n<p>auth strong<\/p>\n<p>allow alice<\/p>\n<p>maxconn 500<\/p>\n<p>proxy -p3128 -i10.200.3.2<\/p>\n<p>socks -p1080 -i10.200.3.2<\/p>\n<p>A few notes. -i sets the internal address the service listens on. 3proxy also offers -e to set the external address used for outgoing connections, which is how you would pin egress in a policy-routing design; with namespaces it is unnecessary, because the namespace already constrains egress. The CR user type stores a crypt-style hash rather than a cleartext password; avoid cleartext credentials in configuration files. auth strong requires username and password authentication, and the allow line restricts which users may use the services that follow.<\/p>\n<h3><strong>GOST<\/strong><\/h3>\n<p>GOST (GO Simple Tunnel) is a Go-based tunnel and proxy toolkit. Version 3, a substantial refactor, lives in the GitHub repository <strong>go-gost\/gost<\/strong>; the legacy version 2 remains at <strong>ginuerzh\/gost<\/strong>, and the two differ in configuration syntax, so check which documentation you are reading. GOST supports HTTP, SOCKS5, chaining, rate limiting, admission control, dynamic configuration through a web API, and Prometheus metrics, which makes it attractive when you want the proxy itself to expose operational data.<\/p>\n<p>The simplest invocation, run inside the namespace:<\/p>\n<p>ip netns exec m03 gost \\<\/p>\n<p>-L &#8220;&#8221; \\<\/p>\n<p>-L &#8220;socks5:\/\/alice:s3cret@10.200.3.2:1080&#8221;<\/p>\n<p>For real deployments, prefer a YAML configuration so credentials are not visible in the process list:<\/p>\n<p># \/etc\/gost\/m03.yaml<\/p>\n<p>services:<\/p>\n<p>&#8211; name: m03-http<\/p>\n<p>addr: <\/p>\n<p>handler:<\/p>\n<p>type: http<\/p>\n<p>auther: users<\/p>\n<p>listener:<\/p>\n<p>type: tcp<\/p>\n<p>&#8211; name: m03-socks<\/p>\n<p>addr: <\/p>\n<p>handler:<\/p>\n<p>type: socks5<\/p>\n<p>auther: users<\/p>\n<p>listener:<\/p>\n<p>type: tcp<\/p>\n<p>authers:<\/p>\n<p>&#8211; name: users<\/p>\n<p>auths:<\/p>\n<p>&#8211; username: alice<\/p>\n<p>password: s3cret<\/p>\n<p>metrics:<\/p>\n<p>addr: <\/p>\n<p>path: \/metrics<\/p>\n<p>Verify field names against the version you deploy; GOST&#8217;s configuration schema has evolved across release candidates.<\/p>\n<h3><strong>Supervising per-modem proxies with systemd<\/strong><\/h3>\n<p>On a single host without containers, systemd can run each proxy inside its namespace through a template unit. The NetworkNamespacePath= directive places the service in an existing namespace:<\/p>\n<p># \/etc\/systemd\/system\/farm-proxy@.service<\/p>\n<p>[Unit]<\/p>\n<p>Description=Proxy for modem namespace %i<\/p>\n<p>After=network-online.target<\/p>\n<p>[Service]<\/p>\n<p>NetworkNamespacePath=\/run\/netns\/%i<\/p>\n<p>ExecStart=\/usr\/local\/bin\/3proxy \/etc\/3proxy\/%i.cfg<\/p>\n<p>Restart=always<\/p>\n<p>RestartSec=2<\/p>\n<p>DynamicUser=yes<\/p>\n<p>CapabilityBoundingSet=<\/p>\n<p>NoNewPrivileges=yes<\/p>\n<p>ProtectSystem=strict<\/p>\n<p>LogsDirectory=3proxy<\/p>\n<p>[Install]<\/p>\n<p>WantedBy=multi-user.target<\/p>\n<p>Then systemctl enable &#8211;now farm-proxy@m03. Because the listening ports are above 1024, the service needs no special capabilities, so the hardening directives cost nothing.<\/p>\n<h3><strong>HTTP versus SOCKS5 and protocol considerations<\/strong><\/h3>\n<p>HTTP proxies handle plain HTTP requests and tunnel HTTPS through CONNECT. SOCKS5 carries arbitrary TCP and, where supported, UDP via UDP ASSOCIATE. Offering both is standard. If clients use SOCKS5 with remote DNS resolution, the proxy resolves hostnames inside the namespace, which keeps DNS consistent with the egress network. With local resolution on the client side, the client&#8217;s DNS reveals its own network, a frequent source of geolocation inconsistencies that customers misattribute to the proxy.<\/p>\n<p>UDP deserves caution. Carrier NATs often have short UDP mapping timeouts, and UDP traffic is disproportionately associated with abuse such as amplification attacks. Many operators disable UDP relay entirely unless a customer has a specific, vetted need.<\/p>\n<h2><strong>IP Rotation<\/strong><\/h2>\n<p>Rotation means forcing the carrier to assign a new session, which usually, though not always, yields a different public egress IP behind the carrier&#8217;s NAT. It is the feature customers ask about first and the one that causes the most operational damage when implemented carelessly.<\/p>\n<h3><strong>Rotation mechanisms<\/strong><\/h3>\n<p>There are three levels of intervention, from gentle to brutal.<\/p>\n<p><strong>1. Bearer reconnect.<\/strong> Tear down and re-establish the data session. With ModemManager:<\/p>\n<p>M=$(\/usr\/local\/bin\/ &#8220;$IMEI&#8221;)<\/p>\n<p>mmcli -m &#8220;$M&#8221; &#8211;simple-disconnect<\/p>\n<p>sleep 2<\/p>\n<p>mmcli -m &#8220;$M&#8221; &#8211;simple-connect=&#8221;apn=${APN},ip-type=ipv4&#8243;<\/p>\n<p>This is fast and usually sufficient to obtain a new private address. Whether the public address changes depends on how the carrier&#8217;s NAT assigns pool addresses.<\/p>\n<p><strong>2. Radio power cycle (&#8220;airplane mode&#8221;).<\/strong> Put the modem&#8217;s radio into a low-power state, then back on, forcing it to detach from and re-attach to the network. ModemManager exposes this as power states:<\/p>\n<p>mmcli -m &#8220;$M&#8221; &#8211;set-power-state-low<\/p>\n<p>sleep 5<\/p>\n<p>mmcli -m &#8220;$M&#8221; &#8211;set-power-state-on<\/p>\n<p>mmcli -m &#8220;$M&#8221; &#8211;simple-connect=&#8221;apn=${APN},ip-type=ipv4&#8243;<\/p>\n<p>Re-attachment takes longer than a bearer reconnect, but more reliably produces a fresh network-side context.<\/p>\n<p><strong>3. Hard reset.<\/strong> Cut USB power with per-port switching. This is a recovery mechanism, not a rotation mechanism. Using it routinely causes re-enumeration, new mmcli indexes, interface churn and long downtime.<\/p>\n<p>For HiLink dongles, rotation goes through the device&#8217;s HTTP API instead. The Python library <strong>Salamek\/huawei-lte-api<\/strong> on GitHub wraps that API for many Huawei LTE routers and USB sticks, including the E3372, and exposes calls such as toggling the mobile data switch. Because every HiLink dongle sits at the same default address, you must either change each device&#8217;s LAN subnet in its settings or reach each one from inside its own namespace, which the namespace design already provides.<\/p>\n<h3><strong>Rotation is a shared-state problem<\/strong><\/h3>\n<p>Rotation interrupts every connection currently flowing through that modem. If two customers share a modem and one triggers a rotation, the other loses their sessions. A rotation service therefore needs:<\/p>\n<ul>\n<li><strong>Per-modem locking<\/strong>, so concurrent rotation requests do not stack.<\/li>\n<li><strong>Cooldowns<\/strong>, enforcing a minimum interval between rotations to protect the modem, the SIM and your standing with the carrier. Frequent re-attachments look abnormal to operators.<\/li>\n<li><strong>Ownership rules<\/strong>, so only the customer currently assigned to a modem may rotate it.<\/li>\n<li><strong>Verification<\/strong>, confirming that the modem came back and reporting the new public IP, or reporting honestly that it did not change.<\/li>\n<\/ul>\n<h3><strong>A minimal rotation service<\/strong><\/h3>\n<p>The following FastAPI sketch shows the shape of such a service. It runs on the host, in the root namespace, because it needs ModemManager over D-Bus, and it checks the public IP from inside the modem&#8217;s namespace.<\/p>\n<p># <\/p>\n<p>import asyncio, json, os, time<\/p>\n<p>from fastapi import FastAPI, HTTPException<\/p>\n<p>INVENTORY = json.load(open(&#8220;\/etc\/farm\/modems.json&#8221;)) # id -&gt; {imei, apn, netns}<\/p>\n<p>COOLDOWN_S = 120<\/p>\n<p>IP_ECHO_URL = os.environ[&#8220;IP_ECHO_URL&#8221;] # an IP-echo endpoint you operate<\/p>\n<p>locks = {mid: asyncio.Lock() for mid in INVENTORY}<\/p>\n<p>last_rotation: dict[str, float] = {}<\/p>\n<p>app = FastAPI()<\/p>\n<p>async def run(*cmd: str, timeout: float = 60) -&gt; str:<\/p>\n<p>p = await asyncio.create_subprocess_exec(<\/p>\n<p>*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)<\/p>\n<p>out, err = await asyncio.wait_for(p.communicate(), timeout)<\/p>\n<p>if p.returncode != 0:<\/p>\n<p>raise RuntimeError(f&#8221;{cmd[0]} failed: {err.decode().strip()}&#8221;)<\/p>\n<p>return out.decode().strip()<\/p>\n<p>async def public_ip(netns: str) -&gt; str:<\/p>\n<p>return await run(&#8220;ip&#8221;, &#8220;netns&#8221;, &#8220;exec&#8221;, netns,<\/p>\n<p>&#8220;curl&#8221;, &#8220;-s&#8221;, &#8220;&#8211;max-time&#8221;, &#8220;10&#8221;, IP_ECHO_URL)<\/p>\n<p>@app.post(&#8220;\/modems\/{mid}\/rotate&#8221;)<\/p>\n<p>async def rotate(mid: str):<\/p>\n<p>m = INVENTORY.get(mid)<\/p>\n<p>if m is None:<\/p>\n<p>raise HTTPException(404, &#8220;unknown modem&#8221;)<\/p>\n<p>if time.monotonic() &#8211; last_rotation.get(mid, 0) &lt; COOLDOWN_S:<\/p>\n<p>raise HTTPException(429, &#8220;cooldown active&#8221;)<\/p>\n<p>if locks[mid].locked():<\/p>\n<p>raise HTTPException(409, &#8220;rotation in progress&#8221;)<\/p>\n<p>async with locks[mid]:<\/p>\n<p>before = await public_ip(m[&#8220;netns&#8221;])<\/p>\n<p>idx = await run(&#8220;\/usr\/local\/bin\/&#8221;, m[&#8220;imei&#8221;])<\/p>\n<p>await run(&#8220;mmcli&#8221;, &#8220;-m&#8221;, idx, &#8220;&#8211;set-power-state-low&#8221;)<\/p>\n<p>await asyncio.sleep(5)<\/p>\n<p>await run(&#8220;mmcli&#8221;, &#8220;-m&#8221;, idx, &#8220;&#8211;set-power-state-on&#8221;)<\/p>\n<p>await run(&#8220;\/usr\/local\/bin\/&#8221;, m[&#8220;imei&#8221;], mid, m[&#8220;apn&#8221;], timeout=90)<\/p>\n<p>after = await public_ip(m[&#8220;netns&#8221;])<\/p>\n<p>last_rotation[mid] = time.monotonic()<\/p>\n<p>return {&#8220;modem&#8221;: mid, &#8220;before&#8221;: before, &#8220;after&#8221;: after,<\/p>\n<p>&#8220;changed&#8221;: before != after}<\/p>\n<p>In production you would add authentication, persist cooldown state, emit metrics, and replace the external IP-echo service with one you operate yourself, both for reliability and to avoid sending your fleet&#8217;s egress IPs to a third party. Note also that from earlier must be adapted to run its ip commands inside the namespace once the interface has been moved.<\/p>\n<h3><strong>Sticky sessions versus rotating pools<\/strong><\/h3>\n<p>Customers generally want one of two behaviors. <strong>Sticky<\/strong> sessions keep a client on the same modem, and therefore the same IP, for a period, which matters for workflows like logged-in QA sessions. <strong>Rotating<\/strong> pools assign a different modem per connection or per time window. The gateway layer implements both by mapping a session token embedded in the proxy username, for example alice-session-abc123, to a modem, with a time-to-live. This is a common convention in commercial proxy services and is easy to parse at the gateway.<\/p>\n<h2><strong>Containerizing with Docker<\/strong><\/h2>\n<p>Containers bring reproducible builds, versioned deployments and resource limits to the proxy layer. They do not change the fundamental constraint: a modem is a physical device attached to a specific host, and its network interface lives in that host&#8217;s kernel.<\/p>\n<h3><strong>Building a proxy image<\/strong><\/h3>\n<p>A small multi-stage build for 3proxy:<\/p>\n<p># Dockerfile<\/p>\n<p>FROM debian:bookworm-slim AS build<\/p>\n<p>RUN apt-get update &amp;&amp; apt-get install -y &#8211;no-install-recommends \\<\/p>\n<p>build-essential git ca-certificates &amp;&amp; rm -rf \/var\/lib\/apt\/lists\/*<\/p>\n<p># Pass the 3proxy repository&#8217;s git URL with &#8211;build-arg<\/p>\n<p>ARG THREEPROXY_REPO<\/p>\n<p>ARG THREEPROXY_REF=master<\/p>\n<p>RUN git clone &#8211;depth 1 &#8211;branch ${THREEPROXY_REF} ${THREEPROXY_REPO} \/src<\/p>\n<p>WORKDIR \/src<\/p>\n<p>RUN ln -s Makefile.Linux Makefile &amp;&amp; make<\/p>\n<p>FROM debian:bookworm-slim<\/p>\n<p>COPY &#8211;from=build \/src\/bin\/3proxy \/usr\/local\/bin\/3proxy<\/p>\n<p>RUN useradd -r -u 10001 proxy<\/p>\n<p>USER 10001<\/p>\n<p>ENTRYPOINT [&#8220;\/usr\/local\/bin\/3proxy&#8221;]<\/p>\n<p>CMD [&#8220;\/etc\/3proxy\/3proxy.cfg&#8221;]<\/p>\n<p>Pin THREEPROXY_REF to a release tag rather than a moving branch in production, and confirm the output binary path for the version you build. Official images also exist on Docker Hub for both 3proxy and GOST (gogost\/gost), which may be preferable to building your own.<\/p>\n<h3><strong>Getting the modem into the container<\/strong><\/h3>\n<p>Docker does not natively attach containers to arbitrary named network namespaces. There are three workable patterns.<\/p>\n<p><strong>Pattern 1: move the interface into the container&#8217;s namespace.<\/strong> Start the container with no networking, find its PID, and move both the modem interface and a veth end into its namespace:<\/p>\n<p>CID=$(docker run -d &#8211;name proxy-m03 &#8211;network none \\<\/p>\n<p>-v \/etc\/3proxy\/m03.cfg:\/etc\/3proxy\/3proxy.cfg:ro farm\/3proxy:1.0)<\/p>\n<p>PID=$(docker inspect -f &#8216;{{.State.Pid}}&#8217; &#8220;$CID&#8221;)<\/p>\n<p>mkdir -p \/run\/netns &amp;&amp; ln -sf \/proc\/$PID\/ns\/net \/run\/netns\/c-m03<\/p>\n<p>ip link set wwm03 netns c-m03<\/p>\n<p># &#8230;then create the veth pair, addresses and default route as in <\/p>\n<p>This yields the same isolation as the bare-metal namespace design, with the proxy packaged as an image. The cost is that a container restart destroys the namespace and returns the modem interface to the host, so the attach logic must run again on every restart.<\/p>\n<p><strong>Pattern 2: use Podman instead.<\/strong> Podman supports joining an existing namespace directly with &#8211;network ns:\/run\/netns\/m03, which keeps the namespace, and the modem inside it, stable across container restarts. For single-host farms, this is the cleanest option I know of.<\/p>\n<p><strong>Pattern 3: host networking with explicit egress binding.<\/strong> Run with &#8211;network host and pin egress with policy routing and the proxy&#8217;s external-address option. It is simple, but you lose namespace isolation and inherit all the leak risks of Option A.<\/p>\n<h2><strong>Orchestrating with <a href=\"https:\/\/alumnit.ca\/orchestrate-the-automation-of-your-container-in-kubernetes-using-ansible-modules\/\">Kubernetes<\/a><\/strong><\/h2>\n<p><a href=\"https:\/\/alumnit.ca\/orchestrate-the-automation-of-your-container-in-kubernetes-using-ansible-modules\/\">Kubernetes<\/a> is worthwhile once you manage several farm hosts, want declarative rollouts, and need a control plane for gateways, rotation APIs, billing and monitoring. Lightweight distributions such as k3s are common at the edge. The key design decision is separating <strong>node-local hardware management<\/strong> from <strong>per-modem proxy workloads<\/strong>.<\/p>\n<h3><strong>The node agent: a privileged DaemonSet<\/strong><\/h3>\n<p>Modem management touches USB devices, D-Bus and the root network namespace, so it runs as a DaemonSet with host access, scheduled only on nodes labeled as modem hosts:<\/p>\n<p>apiVersion: apps\/v1<\/p>\n<p>kind: DaemonSet<\/p>\n<p>metadata:<\/p>\n<p>name: modem-agent<\/p>\n<p>namespace: farm-system<\/p>\n<p>spec:<\/p>\n<p>selector:<\/p>\n<p>matchLabels: {app: modem-agent}<\/p>\n<p>template:<\/p>\n<p>metadata:<\/p>\n<p>labels: {app: modem-agent}<\/p>\n<p>spec:<\/p>\n<p>nodeSelector:<\/p>\n<p>: &#8220;true&#8221;<\/p>\n<p>hostNetwork: true<\/p>\n<p>hostPID: true<\/p>\n<p>containers:<\/p>\n<p>&#8211; name: agent<\/p>\n<p>image: <\/p>\n<p>securityContext:<\/p>\n<p>privileged: true<\/p>\n<p>volumeMounts:<\/p>\n<p>&#8211; {name: dbus, mountPath: \/run\/dbus}<\/p>\n<p>&#8211; {name: farm-run, mountPath: \/run\/farm}<\/p>\n<p>&#8211; {name: dev, mountPath: \/dev}<\/p>\n<p>volumes:<\/p>\n<p>&#8211; {name: dbus, hostPath: {path: \/run\/dbus}}<\/p>\n<p>&#8211; {name: farm-run, hostPath: {path: \/run\/farm, type: DirectoryOrCreate}}<\/p>\n<p>&#8211; {name: dev, hostPath: {path: \/dev}}<\/p>\n<p>The agent talks to the host&#8217;s ModemManager, keeps sessions connected, executes rotations, and writes each modem&#8217;s current bearer configuration to \/run\/farm\/&lt;modem-id&gt;.json. It also publishes inventory, for example as node labels or as a custom resource, so the scheduler and gateway know which modems exist on which node.<\/p>\n<h3><strong>Per-modem pods with Multus and host-device<\/strong><\/h3>\n<p>Each modem then gets its own proxy pod, pinned to the node that physically holds it. To hand the modem interface to the pod, use <strong>Multus<\/strong>, the meta-CNI plugin from the GitHub repository <strong>k8snetworkplumbingwg\/multus-cni<\/strong>, together with the host-device plugin from <strong>containernetworking\/plugins<\/strong>, which moves an existing host interface into a pod&#8217;s network namespace.<\/p>\n<p>apiVersion: <\/p>\n<p>kind: NetworkAttachmentDefinition<\/p>\n<p>metadata:<\/p>\n<p>name: modem-wwm03<\/p>\n<p>namespace: farm<\/p>\n<p>spec:<\/p>\n<p>config: |<\/p>\n<p>{<\/p>\n<p>&#8220;cniVersion&#8221;: &#8220;0.4.0&#8221;,<\/p>\n<p>&#8220;type&#8221;: &#8220;host-device&#8221;,<\/p>\n<p>&#8220;device&#8221;: &#8220;wwm03&#8221;<\/p>\n<p>}<\/p>\n<p>The pod keeps its normal cluster interface for inbound connections from the gateway, plus the modem as a secondary interface. A small sidecar with NET_ADMIN applies the address from the agent&#8217;s bearer file and replaces the pod&#8217;s default route with the modem, while keeping a specific route for the cluster pod and service CIDRs on eth0, so the gateway can still reach the proxy:<\/p>\n<p>apiVersion: apps\/v1<\/p>\n<p>kind: Deployment<\/p>\n<p>metadata:<\/p>\n<p>name: proxy-m03<\/p>\n<p>namespace: farm<\/p>\n<p>spec:<\/p>\n<p>replicas: 1<\/p>\n<p>strategy: {type: Recreate}<\/p>\n<p>selector:<\/p>\n<p>matchLabels: {app: farm-proxy, modem: m03}<\/p>\n<p>template:<\/p>\n<p>metadata:<\/p>\n<p>labels: {app: farm-proxy, modem: m03}<\/p>\n<p>annotations:<\/p>\n<p>: modem-wwm03<\/p>\n<p>spec:<\/p>\n<p>nodeSelector:<\/p>\n<p>: farm-node-01<\/p>\n<p>containers:<\/p>\n<p>&#8211; name: proxy<\/p>\n<p>image: <\/p>\n<p>ports: [{containerPort: 3128}, {containerPort: 1080}]<\/p>\n<p>livenessProbe:<\/p>\n<p>exec: {command: [&#8220;\/bin\/sh&#8221;, &#8220;-c&#8221;, &#8220;ip link show wwm03 | grep -q UP&#8221;]}<\/p>\n<p>periodSeconds: 15<\/p>\n<p>&#8211; name: netcfg<\/p>\n<p>image: <\/p>\n<p>env: [{name: MODEM_ID, value: m03}, {name: CLUSTER_CIDRS, value: &#8220;&#8221;}]<\/p>\n<p>securityContext:<\/p>\n<p>capabilities: {add: [&#8220;NET_ADMIN&#8221;]}<\/p>\n<p>volumeMounts: [{name: farm-run, mountPath: \/run\/farm, readOnly: true}]<\/p>\n<p>volumes:<\/p>\n<p>&#8211; {name: farm-run, hostPath: {path: \/run\/farm}}<\/p>\n<p>The Recreate strategy matters: a rolling update would try to start a second pod requesting the same physical interface while the first still holds it.<\/p>\n<p>One caveat deserves emphasis. If a modem re-enumerates on USB, the kernel destroys its interface, and the replacement appears in the host&#8217;s root namespace, not in the pod. The liveness probe above catches this and restarts the pod, at which point Multus attaches the new interface. Expect that cycle to cost tens of seconds per incident; it is one more reason to invest in stable power and USB.<\/p>\n<h3><strong>The gateway<\/strong><\/h3>\n<p>Clients should not connect to individual modem pods. A gateway Deployment, exposed through a LoadBalancer or NodePort Service, authenticates clients, applies sticky-session or rotation policy, enforces rate limits and acceptable-use rules, and forwards each connection to the chosen modem pod via a per-modem ClusterIP Service selecting modem: &lt;id&gt;. GOST&#8217;s chaining and dynamic configuration API can implement much of this; larger operators typically write a dedicated gateway so policy, billing and logging live in one place.<\/p>\n<p>Manifests for dozens of modems should be generated rather than written by hand. A Helm chart or Kustomize overlay that renders one NetworkAttachmentDefinition, Deployment and Service per inventory entry keeps the cluster state aligned with the physical inventory file.<\/p>\n<h2><strong>Capacity Planning<\/strong><\/h2>\n<p>Before buying hardware, it helps to reason about where the bottlenecks will be. They rarely sit where newcomers expect.<\/p>\n<p><strong>Radio throughput is variable and shared.<\/strong> A modem&#8217;s throughput depends on the cell&#8217;s load, signal quality, carrier aggregation support and the plan&#8217;s traffic shaping. Advertised category speeds describe the radio&#8217;s theoretical ceiling, not what a crowded cell will deliver at 8 p.m. Measure real throughput per site at different times of day before committing to customer bandwidth guarantees.<\/p>\n<p><strong>Uplink is usually the scarcer direction.<\/strong> Cellular networks allocate far less capacity to uplink than downlink. Workloads that upload heavily, such as posting media, will saturate a modem much faster than browsing workloads.<\/p>\n<p><strong>Data caps and fair-use policies dominate economics.<\/strong> Many &#8220;unlimited&#8221; plans throttle after a fair-use threshold. Track consumption per SIM in near real time and route traffic away from SIMs approaching their limits, otherwise a single heavy customer can degrade a modem for everyone sharing it.<\/p>\n<p><strong>Host resources are rarely the constraint.<\/strong> A proxy process per modem consumes little CPU and memory at typical traffic volumes. The host&#8217;s real constraints are USB topology and power, discussed earlier. Size the host for observability tooling, logging and headroom rather than for proxy throughput.<\/p>\n<p><strong>Concurrency per modem should be capped.<\/strong> Carrier NAT tables limit concurrent sessions per subscriber, and exceeding them causes connection failures that look like random proxy errors. Set maxconn in 3proxy, or the equivalent limiter in GOST, to a conservative value per modem and tune upward based on observed error rates.<\/p>\n<p>A useful planning exercise is to model each modem as a resource with four budgets: throughput, uplink, monthly data and concurrent connections. The gateway&#8217;s scheduling logic should be aware of all four, not only of which modems are online.<\/p>\n<h2><strong>Troubleshooting Common Failures<\/strong><\/h2>\n<p>Most incidents fall into a small number of recognizable patterns. Knowing them shortens recovery dramatically.<\/p>\n<h3><strong>Modem disappears from mmcli -L<\/strong><\/h3>\n<p>Check the kernel log first with journalctl -k or dmesg. Messages about USB disconnects, over-current or descriptor read errors point to power or cabling. If the device still appears in lsusb but not in ModemManager, the modem may be stuck in a state that ModemManager cannot probe; restart ModemManager or power-cycle the port with uhubctl.<\/p>\n<h3><strong>Modem registered but bearer will not connect<\/strong><\/h3>\n<p>Compare the registration state and operator shown by mmcli -m &lt;idx&gt; with expectations. A wrong APN, an exhausted data allowance, a SIM PIN or a suspended SIM all produce connection failures. Many carriers send an SMS when a plan is exhausted; ModemManager can list messages with mmcli -m &lt;idx&gt; &#8211;messaging-list-sms, which is worth automating.<\/p>\n<h3><strong>Bearer connected but no traffic<\/strong><\/h3>\n<p>The modem has an address but packets go nowhere. For QMI devices, check raw-IP mode. Inside the namespace, confirm the interface is up, the address is applied and the default route points at the modem:<\/p>\n<p>ip netns exec m03 ip -br addr<\/p>\n<p>ip netns exec m03 ip route<\/p>\n<p>ip netns exec m03 ping -c 3 -I wwm03 <\/p>\n<p>If ping by IP works but hostnames fail, the namespace&#8217;s resolver configuration is wrong or missing.<\/p>\n<h3><strong>Proxy reachable but clients see the wrong IP<\/strong><\/h3>\n<p>This is almost always a leak: the proxy process is not running in the namespace you think it is, or a policy-routing rule fell through to the main table. Check which namespace a process belongs to with ip netns identify &lt;pid&gt;, and test egress from inside the namespace rather than from the host.<\/p>\n<h3><strong>Rotation succeeds but the IP stays the same<\/strong><\/h3>\n<p>Some carriers reassign the same public NAT address to a subscriber for a while. Lengthening the low-power interval sometimes helps; on some networks only waiting does. Report it to customers honestly rather than retrying aggressively, which risks flagging the SIM.<\/p>\n<h3><strong>Random disconnects under load<\/strong><\/h3>\n<p>Correlate the timestamps with temperature and signal metrics. Disconnects that cluster during hot afternoons point to thermal problems; disconnects that follow poor SINR point to radio conditions; disconnects across many modems at once point to a shared hub or power supply.<\/p>\n<h2><strong>Inventory as Code<\/strong><\/h2>\n<p>Every script in this guide depends on one piece of data: a reliable mapping between logical modem IDs, physical USB ports, IMEIs, SIMs, APNs and network namespaces. Keep that mapping in a version-controlled file and generate everything else from it, including systemd .link files, 3proxy configs, nftables rules and Kubernetes manifests.<\/p>\n<p>A minimal inventory:<\/p>\n<p>{<\/p>\n<p>&#8220;m03&#8221;: {<\/p>\n<p>&#8220;node&#8221;: &#8220;farm-node-01&#8221;,<\/p>\n<p>&#8220;usb_path&#8221;: &#8220;pci-0000:00:14.0-usb-0:2.3:1.4&#8221;,<\/p>\n<p>&#8220;ifname&#8221;: &#8220;wwm03&#8221;,<\/p>\n<p>&#8220;imei&#8221;: &#8220;860000000000003&#8221;,<\/p>\n<p>&#8220;iccid&#8221;: &#8220;8900000000000000003&#8221;,<\/p>\n<p>&#8220;carrier&#8221;: &#8220;example-mno&#8221;,<\/p>\n<p>&#8220;apn&#8221;: &#8220;internet&#8221;,<\/p>\n<p>&#8220;netns&#8221;: &#8220;m03&#8221;,<\/p>\n<p>&#8220;ports&#8221;: {&#8220;http&#8221;: 20003, &#8220;socks5&#8221;: 21003},<\/p>\n<p>&#8220;limits&#8221;: {&#8220;maxconn&#8221;: 300, &#8220;monthly_gb&#8221;: 200}<\/p>\n<p>}<\/p>\n<p>}<\/p>\n<p>A small generator renders configuration from it. Using Python and Jinja2 templates:<\/p>\n<p># <\/p>\n<p>import json, pathlib<\/p>\n<p>from jinja2 import Environment, FileSystemLoader<\/p>\n<p>inv = json.load(open(&#8220;inventory.json&#8221;))<\/p>\n<p>env = Environment(loader=FileSystemLoader(&#8220;templates&#8221;), keep_trailing_newline=True)<\/p>\n<p>out = pathlib.Path(&#8220;build&#8221;)<\/p>\n<p>for mid, m in inv.items():<\/p>\n<p>for tpl, dest in [(&#8220;link.j2&#8243;, f&#8221;systemd\/10-{m[&#8216;ifname&#8217;]}.link&#8221;),<\/p>\n<p>(&#8220;3proxy.cfg.j2&#8243;, f&#8221;3proxy\/{mid}.cfg&#8221;),<\/p>\n<p>(&#8220;deployment.yaml.j2&#8243;, f&#8221;k8s\/{mid}.yaml&#8221;)]:<\/p>\n<p>path = out \/ dest<\/p>\n<p>path.parent.mkdir(parents=True, exist_ok=True)<\/p>\n<p>path.write_text(env.get_template(tpl).render(id=mid, **m))<\/p>\n<p>The benefits compound. Replacing a failed modem becomes a one-line change to the IMEI. Moving a SIM to another slot is a reviewed commit instead of a late-night edit on a production host. Auditing which customer used which SIM on which date becomes a matter of reading the history of one file alongside your logs.<\/p>\n<p>Treat SIM identifiers in the inventory as sensitive. ICCIDs and IMEIs are not secrets in the cryptographic sense, but they link infrastructure to carrier accounts and should not live in public repositories.<\/p>\n<h2><strong>Building the Gateway<\/strong><\/h2>\n<p>The gateway is the component that turns a collection of modems into a service. Its core logic is simple to describe: authenticate the client, parse any session or targeting parameters from the proxy username, select a modem, and relay the connection.<\/p>\n<p>The username convention mentioned earlier carries routing intent without any custom client software. A client configures a standard proxy URL whose username looks like alice-session-7f3a-ttl-600, and the gateway decodes it:<\/p>\n<p># (excerpt)<\/p>\n<p>import re, time, random<\/p>\n<p>PATTERN = re.compile(r&#8221;^(?P&lt;user&gt;[a-z0-9]+)(?:-session-(?P&lt;sid&gt;[a-z0-9]+))?(?:-ttl-(?P&lt;ttl&gt;\\d+))?$&#8221;)<\/p>\n<p>sticky: dict[tuple[str, str], tuple[str, float]] = {}<\/p>\n<p>def pick_modem(username: str, healthy: list[str]) -&gt; tuple[str, str]:<\/p>\n<p>m = PATTERN.match(username)<\/p>\n<p>if not m:<\/p>\n<p>raise ValueError(&#8220;malformed username&#8221;)<\/p>\n<p>user, sid = m[&#8220;user&#8221;], m[&#8220;sid&#8221;]<\/p>\n<p>ttl = min(int(m[&#8220;ttl&#8221;] or 600), 3600)<\/p>\n<p>if sid:<\/p>\n<p>key = (user, sid)<\/p>\n<p>modem, expires = sticky.get(key, (None, 0))<\/p>\n<p>if modem in healthy and expires &gt; time.time():<\/p>\n<p>return user, modem<\/p>\n<p>modem = random.choice(healthy)<\/p>\n<p>sticky[key] = (modem, time.time() + ttl)<\/p>\n<p>return user, modem<\/p>\n<p>return user, random.choice(healthy)<\/p>\n<p>A production gateway replaces the in-memory dictionary with a shared store such as Redis so that multiple gateway replicas agree on session assignments, weights selection by each modem&#8217;s remaining budgets from the capacity model, and caps the TTL so a single customer cannot pin a modem indefinitely. It also enforces per-customer concurrency and bandwidth quotas before a connection ever reaches a modem, which protects shared modems from one noisy tenant.<\/p>\n<p>For the relay itself, you can either implement HTTP CONNECT and SOCKS5 in the gateway or chain to the per-modem proxies and let them handle the protocol. Chaining is simpler and keeps the gateway thin; GOST&#8217;s forwarding chains are designed for this pattern. Implementing protocols directly gives finer control over logging and admission decisions. Many teams start with chaining and move protocol handling into the gateway only when they need it.<\/p>\n<p>Whichever approach you choose, log every admission decision with the customer, modem, destination host and outcome. Those records are what you will need when an abuse report arrives.<\/p>\n<h2><strong>Testing and Validation<\/strong><\/h2>\n<p>A farm should be tested like any other distributed system, with particular attention to failure paths.<\/p>\n<p><strong>Leak tests.<\/strong> For every modem, take its interface down and confirm that connections through its proxy fail rather than succeeding through another route. Then bring it up and confirm that the reported public IP belongs to the expected carrier. Automate this check and run it after every deployment.<\/p>\n<p><strong>DNS consistency tests.<\/strong> Resolve a test hostname through each proxy with remote resolution and confirm that the queries arrive from the carrier&#8217;s resolver, not from your uplink. If you operate your own authoritative DNS for a test domain, its query logs make this straightforward.<\/p>\n<p><strong>Rotation tests.<\/strong> Trigger rotations under load and measure how long the modem takes to return, how many in-flight connections fail, and how often the public IP actually changes. These numbers become the honest figures you quote to customers.<\/p>\n<p><strong>Chaos tests.<\/strong> Power-cycle random USB ports with uhubctl during business hours on a staging rack, and confirm that the node agent, liveness probes and gateway health checks recover without manual intervention.<\/p>\n<p><strong>Security tests.<\/strong> From inside a proxy session, attempt to reach private ranges, the metadata address and your management network. Every attempt should fail. Attempt unauthenticated connections to every exposed port; every one should be refused.<\/p>\n<p>Run these tests on a schedule, not only at launch. Carrier behavior, firmware and kernel drivers all change underneath you.<\/p>\n<h2><strong>Monitoring and Observability<\/strong><\/h2>\n<p>A modem farm without metrics is a farm you debug by guessing. Every modem should report radio, session and proxy health, and every alert should identify a modem by its stable ID, not by an mmcli index.<\/p>\n<p>The metrics worth collecting fall into four groups:<\/p>\n<ul>\n<li><strong>Radio:<\/strong> access technology (LTE, 5G NR), RSRP, RSRQ, SINR, band and cell ID, available from mmcli -m &lt;idx&gt; &#8211;signal-get after enabling extended signal reporting with &#8211;signal-setup=&lt;seconds&gt;.<\/li>\n<li><strong>Session:<\/strong> modem state, bearer connected or not, session uptime, current private and public IP, rotation count and rotation duration.<\/li>\n<li><strong>Proxy:<\/strong> active connections, bytes in and out, authentication failures and upstream connect errors per modem. GOST can expose these as Prometheus metrics directly; with 3proxy, parse its logs.<\/li>\n<li><strong>Hardware:<\/strong> USB re-enumeration events from the kernel log, modem temperature where reported, and hub port resets.<\/li>\n<\/ul>\n<p>A small exporter in the node agent can translate mmcli JSON into Prometheus metrics:<\/p>\n<p># signal_exporter.py (excerpt)<\/p>\n<p>from prometheus_client import Gauge, start_http_server<\/p>\n<p>import json, subprocess, time<\/p>\n<p>RSRP = Gauge(&#8220;modem_rsrp_dbm&#8221;, &#8220;LTE RSRP&#8221;, [&#8220;modem&#8221;])<\/p>\n<p>UP = Gauge(&#8220;modem_bearer_connected&#8221;, &#8220;Bearer connected&#8221;, [&#8220;modem&#8221;])<\/p>\n<p>def mm(*args):<\/p>\n<p>return json.loads(subprocess.check_output([&#8220;mmcli&#8221;, *args, &#8220;-J&#8221;]))<\/p>\n<p>def scrape(mid, idx):<\/p>\n<p>sig = mm(&#8220;-m&#8221;, idx, &#8220;&#8211;signal-get&#8221;)[&#8220;modem&#8221;][&#8220;signal&#8221;][&#8220;lte&#8221;]<\/p>\n<p>if sig.get(&#8220;rsrp&#8221;) not in (None, &#8220;&#8211;&#8220;):<\/p>\n<p>RSRP.labels(mid).set(float(sig[&#8220;rsrp&#8221;]))<\/p>\n<p>state = mm(&#8220;-m&#8221;, idx)[&#8220;modem&#8221;][&#8220;generic&#8221;][&#8220;state&#8221;]<\/p>\n<p>UP.labels(mid).set(1 if state == &#8220;connected&#8221; else 0)<\/p>\n<p>if __name__ == &#8220;__main__&#8221;:<\/p>\n<p>start_http_server(9105)<\/p>\n<p>while True:<\/p>\n<p>for mid, idx in current_inventory(): # IMEI-resolved indexes<\/p>\n<p>scrape(mid, idx)<\/p>\n<p>time.sleep(30)<\/p>\n<p>The JSON key layout of &#8211;signal-get varies between ModemManager versions, so test the parsing against your installed version. Plot RSRP against disconnect frequency per modem; the correlation usually tells you whether a problem is radio or USB.<\/p>\n<h2><strong>Security Hardening<\/strong><\/h2>\n<p>An open proxy is abused within hours of being discovered. The minimum baseline:<\/p>\n<ul>\n<li><strong>Never expose unauthenticated proxy ports.<\/strong> Require credentials or source-IP allowlists on every listener, and rate-limit authentication failures at the gateway.<\/li>\n<li><strong>Store credentials as hashes<\/strong> in proxy configs and as Kubernetes Secrets, never in plain manifests or command lines.<\/li>\n<li><strong>Restrict destinations.<\/strong> Block outbound connections to private ranges, cloud metadata endpoints such as , and your own management network from inside every modem namespace, so a customer cannot use the proxy to reach your infrastructure.<\/li>\n<li><strong>Block high-risk ports.<\/strong> Outbound SMTP on port 25 is the classic example; spam through your modems will get SIMs suspended quickly.<\/li>\n<li><strong>Separate management from data.<\/strong> The rotation API, node agents and monitoring should live on a management network that is unreachable from proxy namespaces.<\/li>\n<li><strong>Keep firmware and kernels current,<\/strong> and track modem firmware versions in your inventory.<\/li>\n<\/ul>\n<p>The destination blocklist is easy to apply per namespace with nftables:<\/p>\n<p>ip netns exec m03 nft -f &#8211; &lt;&lt;&#8216;EOF&#8217;<\/p>\n<p>table inet egress {<\/p>\n<p>chain out {<\/p>\n<p>type filter hook output priority filter; policy accept;<\/p>\n<p>ip daddr { , , , } oifname &#8220;wwm03&#8221; drop<\/p>\n<p>tcp dport 25 drop<\/p>\n<p>}<\/p>\n<p>}<\/p>\n<p>EOF<\/p>\n<h2><strong>Abuse Prevention, Compliance and the Law<\/strong><\/h2>\n<p>This is not legal advice, and rules vary widely by country, but several principles apply almost everywhere.<\/p>\n<p><strong>Carrier contracts.<\/strong> Consumer and many IoT data plans prohibit reselling access, running servers or tethering at commercial scale. Operating a proxy service on such plans can breach contract and lead to termination of every SIM on the account. Business or M2M plans with explicit permission are the defensible route.<\/p>\n<p><strong>Know your customer.<\/strong> Commercial operators commonly verify customer identity, require stated use cases, and refuse anonymous high-risk usage. Because traffic from your modems is attributed to your SIMs, abuse complaints and law-enforcement requests will reach you, not your customers.<\/p>\n<p><strong>Logging and data protection.<\/strong> Keep enough connection metadata, such as customer, modem, timestamps, destination host and byte counts, to investigate abuse, but treat it as personal data under regimes such as the GDPR where applicable. Define retention periods and access controls before you collect anything.<\/p>\n<p><strong>Target-site terms and computer misuse law.<\/strong> Accessing websites through proxies is generally lawful, but using proxies to evade blocks, exceed authorization or automate prohibited actions can violate site terms and, in some circumstances, computer misuse statutes. Your acceptable-use policy should prohibit credential stuffing, account creation fraud, ad fraud, scalping, spam and similar activity, and your gateway should enforce it with rate limits, destination policies and the ability to suspend customers immediately.<\/p>\n<h2><strong>Conclusion<\/strong><\/h2>\n<p>A <strong>mobile proxy farm like <\/strong><a href=\"https:\/\/4g-mobile-rotating-proxy-shop.mystrikingly.com\/blog\/build-mobile-proxy-farm-and-sell-4g-proxies\" data-id=\"6abf3530100114f25d2ec07e\" target=\"_blank\">this<\/a> to <strong>sell mobile proxy with a proxy SIM Card<\/strong> is an exercise in turning unreliable hardware into a dependable service. The decisions that matter most are structural: modules over consumer dongles where budget allows, stable identities keyed on physical port and IMEI, one network namespace per modem so isolation fails closed, a rotation service that treats each modem as shared state with locks and cooldowns, and an orchestration layer that respects the fact that each modem belongs to exactly one machine. Open-source building blocks such as 3proxy, GOST, ModemManager, uhubctl, Multus and the CNI host-device plugin cover most of the stack. What they cannot supply is operational discipline and an acceptable-use policy enforced in code, and those are what separate a service from a liability.<\/p>\n<h2><strong>Frequently Asked Questions<\/strong><\/h2>\n<h3><strong>Why not just use a single router with several SIMs?<\/strong><\/h3>\n<p>Multi-SIM routers usually fail over or bond connections rather than exposing each SIM as an independently addressable egress. A farm needs per-modem isolation, per-modem rotation and per-modem monitoring.<\/p>\n<h3><strong>Does rotation always produce a new public IP?<\/strong><\/h3>\n<p>No. It produces a new session. Whether the public IP changes depends on the carrier&#8217;s CGNAT pool behavior. Always verify and report the result honestly.<\/p>\n<h3><strong>Can I run this on a <a href=\"https:\/\/alumnit.ca\/why-you-must-run-the-kubernetes-program-on-a-raspberry-pi-device\/\">Raspberry Pi<\/a>?<\/strong><\/h3>\n<p>For a few modems, yes. USB bandwidth and power become limiting factors quickly, so larger farms generally use x86 hosts with multiple USB controllers.<\/p>\n<h3><strong>Is Kubernetes necessary?<\/strong><\/h3>\n<p>No. A single host with namespaces, systemd and Podman works well for small farms. Kubernetes pays off across multiple hosts and when you need a full control plane.<\/p>\n<h3><strong>What about IPv6?<\/strong><\/h3>\n<p>Many carriers assign IPv6 prefixes to mobile sessions. If your proxies and clients support it, request ip-type=ipv4v6 and configure IPv6 routes and firewall rules inside each namespace too; otherwise IPv6 can become an unmonitored leak path.<\/p>\n<h2><strong>Glossary<\/strong><\/h2>\n<p><strong>APN (Access Point Name):<\/strong> The carrier gateway identifier a modem uses to establish a data session.<\/p>\n<p><strong>Bearer:<\/strong> ModemManager&#8217;s object representing an active data session and its IP configuration.<\/p>\n<p><strong>CGNAT (carrier-grade NAT):<\/strong> Large-scale NAT at the carrier that maps many subscribers onto a shared pool of public IPv4 addresses.<\/p>\n<p><strong>CNI (Container Network Interface):<\/strong> The plugin standard Kubernetes uses to configure pod networking.<\/p>\n<p><strong>DaemonSet:<\/strong> A Kubernetes workload that runs one pod on each selected node.<\/p>\n<p><strong>HiLink:<\/strong> Huawei firmware mode in which a USB dongle acts as its own router and appears to the host as a network adapter.<\/p>\n<p><strong>ICCID \/ IMEI:<\/strong> The SIM card&#8217;s unique identifier and the modem hardware&#8217;s unique identifier, respectively.<\/p>\n<p><strong>MBIM \/ QMI:<\/strong> Control protocols used to manage cellular modems over USB.<\/p>\n<p><strong>Multus:<\/strong> A meta-CNI plugin that lets Kubernetes pods attach multiple network interfaces.<\/p>\n<p><strong>Network namespace:<\/strong> An isolated instance of the Linux network stack with its own interfaces, routes and firewall rules.<\/p>\n<p><strong>Policy routing:<\/strong> Linux routing that selects among multiple routing tables based on rules such as source address or packet mark.<\/p>\n<p><strong>RSRP \/ RSRQ \/ SINR:<\/strong> LTE radio quality measurements for received signal power, signal quality and signal-to-interference-plus-noise ratio.<\/p>\n<p><strong>Sticky session:<\/strong> A proxy session that keeps a client on the same egress modem for a defined period.<\/p>\n<h2>Other Articles from Our Blog \u2013 Proxy Server<\/h2>\n<p><a href=\"https:\/\/alumnit.ca\/how-to-choose-a-residential-proxy-server-a-buying-guide\" data-id=\"6abf3530100114f25d2ec07f\">Cheap Residential Proxies<\/a><\/p>\n<p><a href=\"https:\/\/alumnit.ca\/how-to-use-proxy-servers-for-web-scraping-without-getting-blocked\" data-id=\"6abf3530100114f25d2ec080\">Proxy for Scraping<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>2 ott 2026 \u00b7 @juan<br \>\nIntroduction<br \>\nA mobile proxy farm is, stripped of marketing language, a rack of cellular modems attached to Linux hosts, each modem exposing its carrier-assigned connection through a proxy endpoint that remote clients can use. The engineering problem is less glamorous than the term suggests. It is mostly about USB stability, deterministic device naming, per-interface routing, process isolation, controlled IP rotation and observability across dozens or hundreds of flaky radio links.<br \>\nThis guide walks through that stack layer by layer: hardware, Linux modem management, network &#8230;<\/p>\n","protected":false},"author":2,"featured_media":313,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5,2,4],"tags":[],"class_list":["post-314","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-kubernetes","category-linux","category-python"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.6 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Building a Mobile Proxy Farm: Linux, Network Architecture, Docker and Kubernetes - The Lumnit<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \>\n<link rel=\"canonical\" href=\"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/\" \>\n<meta property=\"og:locale\" content=\"en_US\" \>\n<meta property=\"og:type\" content=\"article\" \>\n<meta property=\"og:title\" content=\"Building a mobile proxy farm: linux, network architecture, docker and kubernetes - the lumnit\" \>\n<meta property=\"og:description\" content=\"2 ott 2026 \u00b7 @juan introduction a mobile proxy farm is, stripped of marketing language, rack cellular modems attached to linux hosts, each modem exposing its carrier-assigned connection through endpoint that remote clients can use. the engineering problem is less glamorous than term suggests. it mostly about usb stability, deterministic device naming, per-interface routing, process isolation, controlled ip rotation and observability across dozens or hundreds flaky radio links. this guide walks stack layer by layer: hardware, management, network ...\" \>\n<meta property=\"og:url\" content=\"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/\" \>\n<meta property=\"og:site_name\" content=\"The lumnit\" \>\n<meta property=\"article:published_time\" content=\"2026-10-02T04:59:24+00:00\" \>\n<meta name=\"author\" content=\"tanna\" \>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \>\n<meta name=\"twitter:label1\" content=\"Written by\" \>\n\t<meta name=\"twitter:data1\" content=\"tanna\" \>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \>\n\t<meta name=\"twitter:data2\" content=\"42 minutes\" \>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/\"},\"author\":{\"name\":\"tanna\",\"@id\":\"http:\\\/\\\/alumnit.ca\\\/#\\\/schema\\\/person\\\/9fd0a703a0e9591780fa55d5b8ed2a9e\"},\"headline\":\"Building a Mobile Proxy Farm: Linux, Network Architecture, Docker and Kubernetes\",\"datePublished\":\"2026-10-02T04:59:24+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/\"},\"wordCount\":8427,\"commentCount\":0,\"publisher\":{\"@id\":\"http:\\\/\\\/alumnit.ca\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/alumnit.ca\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/image-0-923ebfaa.png\",\"articleSection\":[\"Kubernetes\",\"Linux\",\"Python\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/\",\"url\":\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/\",\"name\":\"Building a Mobile Proxy Farm: Linux, Network Architecture, Docker and Kubernetes - The Lumnit\",\"isPartOf\":{\"@id\":\"http:\\\/\\\/alumnit.ca\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/alumnit.ca\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/image-0-923ebfaa.png\",\"datePublished\":\"2026-10-02T04:59:24+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/#primaryimage\",\"url\":\"https:\\\/\\\/alumnit.ca\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/image-0-923ebfaa.png\",\"contentUrl\":\"https:\\\/\\\/alumnit.ca\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/image-0-923ebfaa.png\",\"width\":2240,\"height\":1260,\"caption\":\"Image 1 from Mobile Proxy Farm how to Sell Proxies.png\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/alumnit.ca\\\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"http:\\\/\\\/alumnit.ca\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Building a Mobile Proxy Farm: Linux, Network Architecture, Docker and Kubernetes\"}]},{\"@type\":\"WebSite\",\"@id\":\"http:\\\/\\\/alumnit.ca\\\/#website\",\"url\":\"http:\\\/\\\/alumnit.ca\\\/\",\"name\":\"The Lumnit\",\"description\":\"Resource focused on open source\",\"publisher\":{\"@id\":\"http:\\\/\\\/alumnit.ca\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"http:\\\/\\\/alumnit.ca\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"http:\\\/\\\/alumnit.ca\\\/#organization\",\"name\":\"The Lumnit\",\"url\":\"http:\\\/\\\/alumnit.ca\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"http:\\\/\\\/alumnit.ca\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/alumnit.ca\\\/wp-content\\\/uploads\\\/2020\\\/12\\\/cropped-logo.png\",\"contentUrl\":\"https:\\\/\\\/alumnit.ca\\\/wp-content\\\/uploads\\\/2020\\\/12\\\/cropped-logo.png\",\"width\":700,\"height\":178,\"caption\":\"The Lumnit\"},\"image\":{\"@id\":\"http:\\\/\\\/alumnit.ca\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"http:\\\/\\\/alumnit.ca\\\/#\\\/schema\\\/person\\\/9fd0a703a0e9591780fa55d5b8ed2a9e\",\"name\":\"tanna\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/232d3d12fb7a569b52d57bde4852656d662e564594819cedca36d9a10c67c68a?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/232d3d12fb7a569b52d57bde4852656d662e564594819cedca36d9a10c67c68a?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/232d3d12fb7a569b52d57bde4852656d662e564594819cedca36d9a10c67c68a?s=96&d=mm&r=g\",\"caption\":\"tanna\"},\"url\":\"https:\\\/\\\/alumnit.ca\\\/author\\\/tanna\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Building a Mobile Proxy Farm: Linux, Network Architecture, Docker and Kubernetes - The Lumnit","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/","og_locale":"en_US","og_type":"article","og_title":"Building a Mobile Proxy Farm: Linux, Network Architecture, Docker and Kubernetes - The Lumnit","og_description":"2 ott 2026 \u00b7 @juan Introduction A mobile proxy farm is, stripped of marketing language, a rack of cellular modems attached to Linux hosts, each modem exposing its carrier-assigned connection through a proxy endpoint that remote clients can use. The engineering problem is less glamorous than the term suggests. It is mostly about USB stability, deterministic device naming, per-interface routing, process isolation, controlled IP rotation and observability across dozens or hundreds of flaky radio links. This guide walks through that stack layer by layer: hardware, Linux modem management, network ...","og_url":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/","og_site_name":"The Lumnit","article_published_time":"2026-10-02T04:59:24+00:00","author":"tanna","twitter_card":"summary_large_image","twitter_misc":{"Written by":"tanna","Est. reading time":"42 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/#article","isPartOf":{"@id":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/"},"author":{"name":"tanna","@id":"http:\/\/alumnit.ca\/#\/schema\/person\/9fd0a703a0e9591780fa55d5b8ed2a9e"},"headline":"Building a Mobile Proxy Farm: Linux, Network Architecture, Docker and Kubernetes","datePublished":"2026-10-02T04:59:24+00:00","mainEntityOfPage":{"@id":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/"},"wordCount":8427,"commentCount":0,"publisher":{"@id":"http:\/\/alumnit.ca\/#organization"},"image":{"@id":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/#primaryimage"},"thumbnailUrl":"https:\/\/alumnit.ca\/wp-content\/uploads\/2026\/10\/image-0-923ebfaa.png","articleSection":["Kubernetes","Linux","Python"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/","url":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/","name":"Building a Mobile Proxy Farm: Linux, Network Architecture, Docker and Kubernetes - The Lumnit","isPartOf":{"@id":"http:\/\/alumnit.ca\/#website"},"primaryImageOfPage":{"@id":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/#primaryimage"},"image":{"@id":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/#primaryimage"},"thumbnailUrl":"https:\/\/alumnit.ca\/wp-content\/uploads\/2026\/10\/image-0-923ebfaa.png","datePublished":"2026-10-02T04:59:24+00:00","breadcrumb":{"@id":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/#primaryimage","url":"https:\/\/alumnit.ca\/wp-content\/uploads\/2026\/10\/image-0-923ebfaa.png","contentUrl":"https:\/\/alumnit.ca\/wp-content\/uploads\/2026\/10\/image-0-923ebfaa.png","width":2240,"height":1260,"caption":"Image 1 from Mobile Proxy Farm how to Sell Proxies.png"},{"@type":"BreadcrumbList","@id":"https:\/\/alumnit.ca\/building-a-mobile-proxy-farm-linux-network-architecture-docker-and-kubernetes\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http:\/\/alumnit.ca\/"},{"@type":"ListItem","position":2,"name":"Building a Mobile Proxy Farm: Linux, Network Architecture, Docker and Kubernetes"}]},{"@type":"WebSite","@id":"http:\/\/alumnit.ca\/#website","url":"http:\/\/alumnit.ca\/","name":"The Lumnit","description":"Resource focused on open source","publisher":{"@id":"http:\/\/alumnit.ca\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"http:\/\/alumnit.ca\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"http:\/\/alumnit.ca\/#organization","name":"The Lumnit","url":"http:\/\/alumnit.ca\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"http:\/\/alumnit.ca\/#\/schema\/logo\/image\/","url":"https:\/\/alumnit.ca\/wp-content\/uploads\/2020\/12\/cropped-logo.png","contentUrl":"https:\/\/alumnit.ca\/wp-content\/uploads\/2020\/12\/cropped-logo.png","width":700,"height":178,"caption":"The Lumnit"},"image":{"@id":"http:\/\/alumnit.ca\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"http:\/\/alumnit.ca\/#\/schema\/person\/9fd0a703a0e9591780fa55d5b8ed2a9e","name":"tanna","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/232d3d12fb7a569b52d57bde4852656d662e564594819cedca36d9a10c67c68a?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/232d3d12fb7a569b52d57bde4852656d662e564594819cedca36d9a10c67c68a?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/232d3d12fb7a569b52d57bde4852656d662e564594819cedca36d9a10c67c68a?s=96&d=mm&r=g","caption":"tanna"},"url":"https:\/\/alumnit.ca\/author\/tanna\/"}]}},"_links":{"self":[{"href":"https:\/\/alumnit.ca\/morpheus\/wp\/v2\/posts\/314","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/alumnit.ca\/morpheus\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/alumnit.ca\/morpheus\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/alumnit.ca\/morpheus\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/alumnit.ca\/morpheus\/wp\/v2\/comments?post=314"}],"version-history":[{"count":0,"href":"https:\/\/alumnit.ca\/morpheus\/wp\/v2\/posts\/314\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/alumnit.ca\/morpheus\/wp\/v2\/media\/313"}],"wp:attachment":[{"href":"https:\/\/alumnit.ca\/morpheus\/wp\/v2\/media?parent=314"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/alumnit.ca\/morpheus\/wp\/v2\/categories?post=314"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/alumnit.ca\/morpheus\/wp\/v2\/tags?post=314"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}