Repository navigation
Remove the DHCP responder that hijacks tethered leases - #964
Merged
Merged
Conversation
check_dhcp() answered any packet reaching the tun with source port 68 or
destination port 67, replying with a hard-coded lease of 10.1.10.2/24,
gateway 10.1.10.1 and DNS 9.9.9.9 instead of forwarding it. Nothing on
the tun is a DHCP client, so that reply is never right; a tethered peer
that receives it is configured onto a subnet with no route off the
device.
The default route set keeps the responder out of reach: 224.0.0.0/3
covers the 255.255.255.255 broadcast and 192.168.0.0/16 is excluded, so
no DHCP packet enters the tun. Tethering compatibility mode installs a
plain 0.0.0.0/0 default route and deliberately puts both back inside the
tunnel, which arms the responder for exactly the users being told to
enable that mode to get tethering working again.
The function also wrote through its own const pointer,
inet_pton(AF_INET, "10.1.10.1", (void *) &u->saddr);
overwriting the live session's stored source address -- four bytes into
a union that holds 16 for IPv6, and into the tuple the session table
matches on.
Drop the responder rather than gate it. Forwarding is the correct
handling for a DHCP packet that does reach the tun: the broadcast has
nowhere to go through a UDP socket and is discarded, leaving the
on-link DHCP server (Android's own, for a tethering downstream) to
answer. dhcp_options.c existed only to serve it, and its test was never
wired into run_defensive_tests.sh.
The checked-in Doxygen output under docs/html/ still documents the
removed structs; it is generated and left to the next regeneration.
Claude-Session: https://claude.ai/code/session_01BEU2NQ6M7Zwgwpvn7Vsv5P
The removal missed this call site: test.yml compiles dhcp_options_test.c and dhcp_options.c in a step of its own, separately from run_defensive_tests.sh, so the `test` job failed on the two source files no longer being there. Claude-Session: https://claude.ai/code/session_01BEU2NQ6M7Zwgwpvn7Vsv5P
kasnder
marked this pull request as ready for review
September 19, 2026 19:52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found while investigating #962 (Bluetooth tethering). Not a fix for that issue — it removes a hazard in the workaround being recommended there.
The problem
udp.chanded any packet reaching the tun with source port 68 or destination port 67 tocheck_dhcp(), which answered it with a hard-coded lease instead of forwarding it:10.1.10.2, mask255.255.255.010.1.10.19.9.9.9Nothing on the tun is a DHCP client, so that reply is never correct. A tethered peer that receives it is configured onto a subnet with no route off the device. The function carried upstream NetGuard's own
// This is untestedcomment.The default route set keeps it out of reach.
224.0.0.0/3covers the255.255.255.255broadcast and192.168.0.0/16is excluded, so no DHCP packet enters the tun. Tethering compatibility mode arms it: it installs a plain0.0.0.0/0default route and, asVpnRoutes.getTetheringRoutes()documents, deliberately puts both the broadcast and the tethering downstream subnets back inside the tunnel. That is exactly the mode users with broken tethering are being told to enable.Second defect in the same function
uis aconst struct udp_session *. This casts the const away to overwrite the live session's stored source address — four bytes into a union that holds 16 for IPv6, and into the tuple the session table matches on.The change
The responder is dropped rather than gated. Forwarding is the right handling for a DHCP packet that does reach the tun: the broadcast has nowhere to go through a UDP socket and is discarded, leaving the on-link DHCP server — Android's own, for a tethering downstream — to answer.
dhcp_options.cand its test came in with c197cee ("Fix DHCP requests being answered with offers"), which corrected the reply type — DHCPOFFER vs DHCPACK, per option 53 — but left the lease contents, and the responder's premise, untouched. Both go with the responder they serve.Removed: the
udp.ccall site,dhcp.c,dhcp_options.{c,h}, the DHCP wire structs andcheck_dhcpprototype innetguard.h, the twoCMakeLists.txtentries,dhcp_options_test.cand theRun DHCP option host testsstep intest.ymlthat compiled it, and thecheck_dhcpstubs the two UDP tests needed to link.Testing
app/src/test/native/run_defensive_tests.sh— all 8 suites pass (tcp_defensive,udp_defensive,icmp_defensive,ip_header,tcp_window,udp_socket,icmp_socket,ip_flow_policy).ip6_ext_test,udp_state_test) re-run locally under the workflow's own-Wall -Wextra -Werrorinvocation: both pass.test.ymlparses as YAML after the step removal.-fsyntax-onlysweep over every remainingjni/netguard/*.c: no new errors.netguard.candutil.cfail on JNI types under the host shim, which is pre-existing and unrelated.assembleGithubDebugand the Gradle Java/lint tasks did not run. CI covers that.The first push missed the
test.ymlstep, which is why thetestjob went red on ce2cda9; 849f5db fixes it.Remaining risk
Low. No behaviour changes in the default route set, since the responder was unreachable there. In tethering compatibility mode, DHCP packets are now forwarded (and dropped by the socket layer) rather than answered with a wrong lease.
The checked-in Doxygen output under
docs/html/still documents the removed structs. It is generated output, left to the next regeneration rather than hand-edited here.HOTPATH_BUG_AUDIT.mdmentions the DHCP-option suite in its record of a past test run; that is a dated record and is left as written.https://claude.ai/code/session_01BEU2NQ6M7Zwgwpvn7Vsv5P