UA2F: rewriting the User-Agent header on an OpenWRT router
Change User-Agent to F-string on OpenWRT router to prevent being detected.
At a glance
- What is it?
- UA2F is a C daemon for OpenWRT that intercepts outbound TCP traffic and replaces the HTTP User-Agent header with a fixed string, either through a netfilter queue or through a transparent proxy. It can be built into the firmware or compiled standalone, and it reports its own throughput against UA3F in the repository.
- Who is it for?
- Adopt UA2F if you run an OpenWRT router and need outbound HTTP requests to carry a fixed User-Agent without touching the clients on your network, since it operates at the router rather than on each device. Do not adopt it if you need to change encrypted traffic, since the README states HTTPS is already encrypted and needs no handling.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 12 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Where UA2F sits in the traffic path
The repository description is precise about the goal: change the User-Agent to a fixed string on an OpenWRT router to prevent being detected. The homepage links to a blog post that the project says it was built to follow.
So the design decision that defines UA2F is where the rewrite happens. It is not a browser extension and not a per-application setting. It is a daemon on the router, and it rewrites the traffic of every device behind it. A phone, a smart TV and a laptop on the same LAN all get the same User-Agent, because the router is the choke point where their plaintext HTTP traffic can be inspected and rewritten in one place.
That placement explains the engineering. The daemon needs to see TCP payloads, so it needs either netfilter's NFQUEUE to pull packets out of the network stack, or a transparent proxy that accepts redirected connections and re-originates them itself. Both are implemented, and the choice between them is one of the main configuration decisions a user makes.
The rewrite itself is constrained by HTTP semantics. UA2F does not change the packet size, so the User-Agent it produces has exactly the same length as the original one. When the custom string is shorter, the remainder is padded with spaces at the end; when it is longer, it is truncated. The README is explicit that the value at runtime will be a substring of the custom User-Agent whose length matches the original. The reason is straightforward once stated: rewriting a header in place, without recomputing checksums or reassembling packets, means the byte count has to stay fixed.
The primary language is C, the licence is GPL-3.0, and the default branch is master. The repository carries a CMakeLists.txt, an openwrt/ directory for the OpenWRT package, src/, scripts/, test/ and a uacheck/ directory.
The three run modes and the trap in TPROXY
The mode setting is the piece of the configuration that most needs reading twice.
NFQUEUE is the default and keeps the original behaviour, rewriting TCP packets through a netfilter queue. REDIRECT is a transparent proxy mode where the firewall REDIRECTs traffic to the local listen port and UA2F then connects to the original target. TPROXY is also a transparent proxy mode and is described as suitable for taking over forwarded traffic.
The catch is stated in the README for TPROXY. It needs policy routing to point fwmark 0x1c9 at lo. And a fixed-listen-port TPROXY does not handle the machine's own OUTPUT traffic, so local traffic has to use REDIRECT or NFQUEUE instead.
The reasoning behind the mark is worth understanding, since the firewall rules have to be written to match. UA2F's outbound connections set SO_MARK 0xc9, and the firewall rules should bypass that mark to avoid a loop where the rewritten connection gets captured by the same rules that captured the original. The fwmark 0x1c9 on the inbound side is what makes the transparent proxy's socket receive the original destination address, which is the only way to know where to connect on the far end. If you get that policy routing wrong, the symptom is that traffic through the router stops working rather than an error message, which is why it is called out explicitly.
The mode is set through UCI, and the command-line --mode flag overrides the UCI configuration. The listen_port option, default 10010, is the local transparent proxy port for REDIRECT and TPROXY; NFQUEUE does not use it.
Worker counts are per mode. nfqueue_workers defaults to 1 and ranges from 1 to 16, and it is both the worker thread count and the queue count, with a matching queue-balance rule generated when automatic firewall rules are enabled. proxy_workers defaults to 0, meaning automatic, and currently uses at most 4 CPUs.
Configuring it through UCI on OpenWRT
UA2F's OpenWRT configuration lives at /etc/config/ua2f, and changes to it require a commit and a service restart.
The quick start sequence below is the README's own, with the enabled flag, the optional firewall options and the main mode settings.
uci set ua2f.enabled.enabled=1
uci set ua2f.firewall.handle_fw=1
uci set ua2f.firewall.handle_tls=1
uci set ua2f.firewall.handle_mmtls=1
uci set ua2f.firewall.handle_intranet=1
uci set ua2f.main.custom_ua="Test UA/1.0"
uci set ua2f.main.mode="TPROXY"
uci set ua2f.main.listen_port="10010"
uci set ua2f.main.nfqueue_workers="1"
uci set ua2f.main.proxy_workers="0"
uci set ua2f.main.disable_connmark=1
uci commit ua2f
service ua2f enable
service ua2f startThe firewall options each have a sensible default and a reason. handle_fw defaults to 1 and controls whether the init script installs the netfilter rules automatically; turn it off and you configure netfilter yourself. handle_tls defaults to 0 because HTTPS is encrypted and there is nothing to rewrite. handle_intranet defaults to 1, and setting it to 0 bypasses internal and reserved addresses. handle_mmtls defaults to 0 and covers WeChat's mmtls traffic, and the rule only takes effect in the iptables NFQUEUE branch and is ineffective under nftables.
Two main options are worth calling out. disable_connmark defaults to 0 and disables both the conntrack marking and the associated cache. The README states plainly that this lowers performance but helps coexist with other software that also modifies Connmark, which is the kind of conflict that only shows up when two packages are installed at once. And the HTTP parser has its own limits: max_http_sessions defaults to 0, meaning no limit, and session_ttl defaults to 300 seconds, the idle expiry for an HTTP session.
The conntrack marking exists because of a kernel-level detail. The README notes that due to the newly added CONNMARK, compiling the kernel requires the NETFILTER_NETLINK_GLUE_CT flag, so a stock kernel without it will not work with this version.
Logs are read with:
logread | grep UA2FThere is also a public test page at ua-check.stagoh.com that the README points to for checking whether UA2F is working.
Building it into firmware, and the standalone path
There are two ways to get UA2F, and they suit different users.
The OpenWRT package route is the mainstream one. The repository has a Build OpenWRT Package CI workflow and an openwrt/ directory, and the release tags follow the usual OpenWRT convention of v5.2.0 for 2026-06-13, v5.1.1 for 2026-05-25 and v5.1.0 for 2026-05-24. If you run OpenWRT, installing the package from the package feed is the ordinary path.
The standalone route exists since v4.5.0. On non-OpenWRT systems you can run the daemon yourself, but you have to configure the firewall rules by hand, forwarding the traffic you want processed into the netfilter-queue's 10010 queue. Building for that case requires turning off the UCI support with a CMake flag.
-DUA2F_ENABLE_UCI=OFFThe default mode is still NFQUEUE in that case, and the transparent proxy modes are available too.
sudo ./build/ua2f --mode REDIRECT --listen-port 10010
sudo ./build/ua2f --mode TPROXY --listen-port 10010The same warnings apply outside OpenWRT: REDIRECT and TPROXY need their own netfilter rules, TPROXY needs the policy routing for fwmark 0x1c9 pointing at lo, and the firewall rules should bypass the mark UA2F sets on outbound connections so it does not loop. A fixed-listen-port TPROXY is for PREROUTING and forwarded traffic, and local OUTPUT traffic should use REDIRECT or NFQUEUE.
There is also a compile-time route for the User-Agent string itself. After make menuconfig, you can set the value with an option, and the constraint is stated precisely: UA2F_CUSTOM_UA must be a string no longer than (65535 + (MNL_SOCKET_BUFFER_SIZE / 2)) bytes, and MNL_SOCKET_BUFFER_SIZE is usually 8192. That formula is a consequence of the in-place, same-length rewrite: the string has to fit inside the largest packet the kernel will hand the daemon, since a packet cannot be made longer in place.
The benchmark against UA3F, and what it actually measures
The repository ships a benchmark, and it is worth reading carefully because the comparison it draws is narrow.
The test subject is UA2F against UA3F, another User-Agent rewriting tool. The environment is WSL2 on x86_64 with a Linux 6.18.33.1-microsoft-standard-WSL2 kernel, 16 cores, and go1.26.3 linux/amd64. The client runs in a separate network namespace with concurrency 128 and HTTP keep-alive, through a PREROUTING transparent proxy to an origin server at 10.250.0.1:18080. UA2F is built with RelWithDebInfo and UA3F with GLOBAL rewrite mode and the FFF User-Agent, and both complete the rewrite.
For 1 KiB responses over 50000 requests, in REDIRECT mode UA2F records 73515 requests per second against UA3F's 60474, with average latency of 1.69 ms against 2.02 ms, and the ratio rows put UA2F at 1.22 times the throughput and 0.84 times the latency. In TPROXY the throughput ratio is 1.15 times and the latency ratio 0.87 times.
The memory numbers are where the gap widens. UA2F's RSS is 2.5 MB against UA3F's 45.6 MB, a ratio of 0.054, and peak memory is 3.0 MB against 48.0 MB. CPU is 224 percent for UA2F against 489 percent for UA3F in REDIRECT, a ratio of 0.46.
For 64 KiB responses over 100000 requests the picture narrows. REDIRECT throughput ratio is 1.02 times, effectively parity, and TPROXY is 1.42 times. The memory ratio stays around 0.05 to 0.06.
The reason to read those numbers carefully is that the client is keep-alive HTTP, so the response body size changes how much work is being measured. At 1 KiB the per-request overhead dominates and UA2F's cheaper session handling shows clearly. At 64 KiB the proxy is moving a lot of bytes and the fixed per-request cost matters less, which is why the REDIRECT ratio drops to parity while TPROXY, where the socket path differs, stays well ahead.
The reproduction script is in the repository, and it builds the Go client and server itself and generates Markdown and JSON reports.
sudo python3 scripts/benchmark.py \
--ua2f ./build/ua2f \
--ua3f ./ref/UA3F/ua3f \
--ua2f-modes REDIRECT,TPROXY \
--ua3f-modes REDIRECT,TPROXY \
--requests 100000 \
--warmup 10000 \
--concurrency 128 \
--body-bytes 65536Changing --body-bytes to 1024 reproduces the small response case.
Where UA2F does not apply
There are three limits, and the first one is stated plainly in the configuration notes.
The first is that encrypted traffic is out of scope. The handle_tls option defaults to 0 and the reason given is that HTTPS is already encrypted and needs no handling. Any rewrite tool operating on plaintext headers has this boundary, and UA2F does not attempt to break it. If your goal is to change the User-Agent that a TLS-protected client sends, the header is inside the encrypted stream and this daemon cannot reach it. The WeChat mmtls option is the same idea for a different protocol, and it only works in the iptables NFQUEUE branch.
The second is that TPROXY on a fixed listen port does not cover the router's own traffic. Local OUTPUT traffic has to go through REDIRECT or NFQUEUE, and the policy routing for fwmark 0x1c9 has to be in place before TPROXY works at all. That is a real operational constraint rather than a documented caveat, because a router that appears to be dropping its own connections is a confusing symptom.
The third is the Connmark interaction. If other software on the router also modifies Connmark, the two can conflict, and the escape hatch is disable_connmark, which the README says lowers performance. That is a genuine trade rather than a free setting, and on a busy router the throughput cost is the price of coexistence.
There is also an open TODO list in the README, and it is short and specific: pthread support, with different threads handling enqueue and dequeue, and a rewrite of the regular expression matching. Both items point at the same internal design, a single-threaded path through the packet handling, and the regex work is what a change to the User-Agent matching would touch.
The honest alternative for the same goal depends on where you need it. If you only need to change the User-Agent for one application on one machine, setting it in that application's settings is simpler and touches nothing else on the network. If you need it for every device behind the router, UA2F is doing the thing a per-device approach cannot. And for a device that supports it natively, many Linux applications and most browsers expose a User-Agent override, so the router-level rewrite is only worth its complexity when the client set is large or the devices are not under your control.
On the tool comparison, UA3F is the alternative the benchmark names, and it is a different design: the memory figures in the table show it running at roughly twenty times UA2F's resident size, which points to a heavier implementation with more state per session. UA2F's smaller footprint is what makes it comfortable on a router, where memory is the scarce resource and a large daemon competes with the system you are trying to route.
Editorial conclusion
Adopt UA2F if you run an OpenWRT router and need outbound HTTP requests to carry a fixed User-Agent without touching the clients on your network, since it operates at the router rather than on each device. Do not adopt it if you need to change encrypted traffic, since the README states HTTPS is already encrypted and needs no handling. Before you flash it, read the mode notes for TPROXY, because it requires policy routing for fwmark 0x1c9 and does not handle local OUTPUT traffic on a fixed listen port, and note the last push was on 2026-10-11 with release v5.2.0 from 2026-06-13.
Frequently asked questions
What is UA2F and what does it do on OpenWrt?
UA2F is a C daemon that changes the User-Agent header of outbound HTTP traffic to a fixed string on an OpenWRT router, so the devices behind it are not distinguished by their default User-Agent. It rewrites the traffic in place without changing the packet size, padding with spaces or truncating to match the original length.
What are the NFQUEUE, REDIRECT and TPROXY modes in UA2F?
NFQUEUE is the default and rewrites TCP packets through a netfilter queue. REDIRECT is a transparent proxy mode where the firewall redirects traffic to the local listen port and UA2F connects to the original target. TPROXY is also a transparent proxy mode, suited to taking over forwarded traffic, and it needs policy routing for fwmark 0x1c9 pointed at lo.
How do I configure and start UA2F through UCI?
The configuration lives at /etc/config/ua2f. Set the options with uci set, for example uci set ua2f.enabled.enabled=1 and uci set ua2f.main.custom_ua="Test UA/1.0", then run uci commit ua2f, service ua2f enable and service ua2f start. You can read the logs with logread | grep UA2F.
Can I run UA2F outside OpenWRT?
Yes, since v4.5.0, but you must build with -DUA2F_ENABLE_UCI=OFF and configure the firewall rules yourself, forwarding the traffic you want processed into the netfilter-queue's 10010 queue. The default mode is still NFQUEUE, and the transparent proxy modes are run with flags such as --mode REDIRECT --listen-port 10010.
How does UA2F compare with UA3F in throughput and memory?
In the repository benchmark over 1 KiB responses with concurrency 128, UA2F in REDIRECT mode records 73515 requests per second against UA3F's 60474, with resident memory of 2.5 MB against 45.6 MB. Over 64 KiB responses the REDIRECT throughput ratio narrows to 1.02 times while TPROXY stays at 1.42 times, and the memory ratio stays near 0.05.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/zxilly-ua2f)Was this useful?