Repository navigation
APFPV link mode: join the OpenIPC apfpv camera's open AP with devourer - #129
Conversation
PR Summary by QodoAdd APFPV station link mode for OpenIPC cameras
AI Description
Diagram
High-Level Assessment
Files changed (11)
|
Code Review by Qodo
1.
|
josephnef
left a comment
There was a problem hiding this comment.
Thanks for this, it is a substantial and well-structured piece of work. The station state machine, the ARP/ICMP/TCP mini-stack and the devourer integration are more careful than most contributed network code: every header parse I traced is length-checked, lock ordering is consistent (mu_ → q_mu_, sta_mutex → mu_ only from JNI), the six new JNI signatures match, and the existing R8 keep rules already cover the new natives. No merge conflicts with master.
I am requesting changes for two blocking items and a handful of robustness fixes. Qodo's review above is largely accurate; where I agree I reference its numbering rather than repeat it.
CI preview
CI (pr.yml, merged after this PR was opened) has not run here yet. It will on your next push. Expect:
- clang-format job: fail. With the pinned clang-format 18.1.8 the diff is 195 lines in
StaLink.cpp, 22 inStaLink.h, 57 inWfbngLink.cpp, 4 inWfbngLink.hpp(mostly bin-packed arguments and one-line loop bodies, plus one 124-column line atStaLink.cpp:695). Fix withpip install clang-format==18.1.8 && scripts/format_sources.sh. - Host GoogleTest and debug APK build: expected to pass. The emulator smoke job is non-blocking.
Blocking
- Device teardown race, and it now affects WFB-NG mode too (Qodo #9, #1).
releaseDevice(fd)is called at the end ofrun()/runStation()(WfbngLink.cpp:278, 296, 375) and erases fromrtl_devices(WfbngLink.hpp:146-149). Master only erased on the early-abort path.rtl_devicesis an unsynchronisedstd::mapthatstop()(findthenat(fd).get()thenStopRxLoop()),nativeSetTxPower(:817-818) andstart_link_quality_thread(:679, 786) read from other threads. When the RX loop exits on its own (USB detach, libusb error) andWfbLinkManagerthen callsstop, the run thread can destroy the device whilestop()is insideStopRxLoop()through a raw pointer, and.at()on an erased fd throws through JNI. Keep the erase if devourer's per-adapter lock needs it, but guardrtl_devices/current_fdwith a mutex and havestop()and the setters take ashared_ptrcopy under it. Also setcurrent_fdonly after the adapter is accepted as the station (Qodo #1). - Formatting, see above.
Should fix
- ICMP echo of arbitrary size is injected as one MPDU (
StaLink.cpp:1161-1175). The reply copies the whole datagram, and reassembly allows up to 64 KB, so anyone on the open AP can make the worker hand a ~60 KB frame tosend_packet. Drop echo requests above ~1500 bytes. - Fragment reassembly completes with gaps (Qodo #2,
StaLink.cpp:1036).received += plencounts overlapping or duplicate fragments, so a zero-filled datagram can be forwarded to :5600. Track coverage or drop overlaps. - Unbounded buffers:
tcp_.rxgrows until the deadline with no cap (StaLink.cpp:1348, cap at e.g. 64 KB);seen_grows per distinct BSSID (:836, cap it). SetTxPowerfrom the JNI thread races the worker'sSetMonitorChannel/FastRetune(StaLink.cpp:280-289). devourer'sIRtlDevice.h:111-116requires a single control thread. Queue it through the worker likerequestTune.- Qodo #10 (failed channel switch recorded as success,
StaLink.cpp:304), #8 (TCP single-flight: a recovery key-frame request makes a user'sset_ap_channelreport "camera did not answer",:1208), #7 (partial HTTP reply accepted after 1.5 s idle,:1277), #5 and #6 (gap-recovery edge cases). All hold on my read. tools/apfpv-open-static-cam.sh:--allow-macas the last argument silently installs without the ACL (Qodo #3,:48); the adapter is replaced before the ACL file is written, so a write failure leaves a half-installed config (Qodo #4,:119); and$masteris assumed defined at theip neigh delsite (:107-108), add it to the pre-flight checks.
Document
- Open network without PMF: a spoofed deauth or a spoofed beacon carrying the SSID can steer the station (
StaLink.cpp:754, 836). Inherent to the design, but the README should say so and recommend--allow-mac. - Camera HTTP credentials go as Basic auth in cleartext over the open AP (
VideoActivity.java:2140,CameraControlDialog.java:519). Worth one sentence.
What would make this mergeable
- Run
scripts/format_sources.shwith clang-format 18.1.8. - Mutex around
rtl_devices/current_fd,shared_ptrcopies instop()and the setters. - Bound network input (ICMP echo size,
tcp_.rx,seen_) and fix fragment coverage. - Serialise
SetTxPowerwith the worker's retunes. - README notes on the security model.
If you want something merged sooner, the camera-side script plus its README section would stand on their own as a separate PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/apfpv-open-static-cam.sh patches the apfpv firmware's /usr/bin/adapter for an open access point without DHCP and with disassoc_low_ack=0, optionally with a MAC filter; --revert restores the backup. Referenced from the README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addresses the Qodo findings and the blocking items from the maintainer review. WfbngLink - Guard rtl_devices with dev_mutex; stop() and the TX power setter work on a shared_ptr copy taken under the lock, so run() releasing the device can no longer free it under them (use-after-free / std::map race). run() keeps its own reference and drops it before libusb_exit(). - current_fd is atomic and only set in runStation() once the adapter is accepted as the station, so a rejected second adapter cannot clobber it. - TX power in AP mode is queued to the StaLink worker (single control thread, no race with retunes). - The recovery counter reported to Java is monotonic across StaLink instances (ap_recovery_base), so a fast restart still triggers a key-frame request. StaLink - IPv4 reassembly tracks received 8-byte units; duplicate or overlapping fragments can no longer complete a datagram with holes. - A failed retune no longer updates channel/width/offset. - A dropout that ends between two worker ticks is detected in onRx() as well. - tcpRequest waits for the single session slot instead of failing at once. - A cut-off HTTP reply (missing headers or body per Content-Length) is not accepted after the idle timeout and fails at the deadline so callers retry. - Bound untrusted input: ICMP echo <= 1500 bytes, TCP reply <= 64 KiB, at most 64 remembered networks while searching. CameraControlDialog - cgi() retries when the JSON body is unreadable. Not included: tools/apfpv-open-static-cam.sh, README notes, clang-format run and the rebase onto master.
…stall - --allow-mac without a value stops instead of installing without the filter. - The accept file is written (and read back) before the patched adapter replaces the original; if replacing fails, the original is restored. - Pre-flight check that the adapter defines $master, which the ARP reset that replaces reset_single_ip_dhcp relies on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No functional change; output of scripts/format_sources.sh for the files this branch adds or touches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… replies) - AP mode never calls SetTxPower on the device from a JNI thread: without a running StaLink the run thread is bringing the adapter up or tearing it down, and runStation() applies adaptive_tx_power itself during bring-up. - tcpRequest(): one deadline covers waiting for the session slot and the exchange, so a caller no longer waits up to twice its timeout. - A reply over 64 KiB ends the request at once instead of re-ACKing until the deadline and then returning a truncated reply as success. - An HTTP reply that is cut off is a failure on every path: fewer bytes than "HTTP/", and a FIN or RST before Content-Length is reached. - CameraControlDialog.cgi() also retries a reply without a JSON body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Open access point without PMF (readable and forgeable traffic, spoofed deauth/beacons), the --allow-mac install and its limits, and the Drone login going out as cleartext Basic auth, also with every automatic key-frame request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reboot, the hostapd start channel and the SSID change reboot the camera, which often cuts its own answer off. cgi() retried on that and could reboot the camera a second time. These actions now go out once; a missing answer is reported as "may already be rebooting", and a new SSID is not adopted unless the camera confirmed it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r the device lock - A channel switch the driver refuses is requested again (up to 3 times) unless a newer one is pending. After a refused switch to a channel the camera announced, the station would otherwise stay behind: the search after a dropout only looks at the camera's boot channels. - HTTP requests (HTTP/1.0, Connection: close) count as answered only when the camera closes the connection, not after the idle gap used for air_man_ap. Replaces the Content-Length parsing. - stop() and the TX power setter call into the device under dev_mutex (StopRxLoop() only sets a flag) instead of holding shared_ptr copies, so the device is always destroyed on the run thread before libusb_exit(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the thorough reviews, @josephnef and Qodo. I've rebased on current master and addressed the points below. The branch was force-pushed, so the head is now Rebase
Blocking
Should fix
Documented
Open: Qodo #11 (cleartext credentials)
Which would you prefer? Verification
The camera script was exercised against a mock |
josephnef
left a comment
There was a problem hiding this comment.
Thanks for the fast and complete turnaround. I re-verified every item against e5d34f7 rather than taking the summary on trust, and all of it checks out:
- Teardown race:
dev_mutex+withDevice()now cover everyrtl_devicesaccess (insert,stop(),applyTxPower(),releaseDevice()), no rawIRtlDevice*escapes the lock, the device is only ever destroyed on the run thread, anddev_mutexis never nested insidemu_,sta_mutexorthread_mutexor held across a join.current_fdis atomic and only published for the accepted adapter. This also removes a latent race in WFB-NG mode. - ICMP cap at 1500, fragment coverage bitmap with the 8-byte rule,
tcp_.rxandseen_caps, TX power routed through the worker in AP mode and applied at bring-up otherwise, retune failure no longer recorded as success with bounded retries, single deadline on the TCP slot wait, HTTP completion on FIN withcgiOnce()for rebooting actions, the two gap-recovery edge cases, the script fixes, and the README security section: all confirmed in code. - Rebase is clean:
onPauseis identical to master and the APFPV switch usesWfbNgVpnService.stopRunning().
CI on this head: clang-format, host GoogleTest, debug APK and the emulator smoke test are all green.
Key-frame request over cleartext Basic auth. My recommendation is option 1 with a fallback, as a follow-up PR rather than a blocker here: send GET /request/idr without Authorization first; on a 401, resend once with the credentials and remember that for the session. The TCP client already returns the status line, so this is a handful of lines in the key-frame path. That stops the recurring root-credential leak on every camera that does not gate the endpoint, costs one extra round-trip once on the ones that do, and keeps recovery automatic. An "Automatic key-frame request" switch in the APFPV settings (default on) alongside it would give people who care a way out. Option 2 alone would silently degrade recovery for most users, and option 3 is not enough given the credential is the camera's root login.
Two non-blocking observations for later: withDevice holds dev_mutex across a USB register write in WFB-NG mode, so releaseDevice on a detached adapter can stall for a libusb timeout (bounded, not a deadlock); and retune failure is detected only via exceptions, so a devourer retune that fails silently would still be recorded as success.
Approving. Please resolve the one remaining Qodo thread (the ruleset requires it) and this can merge.
#129 added a second link mode (StaLink: 802.11 station over devourer with ARP/ICMP/TCP, UDP forwarding to the unchanged loopback consumers, camera control over air_man_ap and the aalinkFPV API) and a dev_mutex/withDevice() rule around WfbngLink's device map. Describe both, the preference keys, the cleartext key-frame follow-up, and the pixelpilot-sta log tag. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Adds a second link mode next to WFB-NG: the USB adapter (devourer userspace driver) joins the open Wi-Fi access point of an OpenIPC apfpv camera as a regular station, and the camera pushes its RTP stream to the phone's static IP.
Camera side (apfpv firmware)
The stock firmware runs a WPA2 access point with a DHCP server. For this mode, /usr/bin/adapter (which writes hostapd.conf on every start) was patched:
The camera changes are scripted in tools/apfpv-open-static-cam.sh (keeps a backup, --revert restores it).
Limitations
Open networks only (no WEP/WPA), static IP (no DHCP), no 802.11 fragment reassembly, UDP only plus one short TCP request at a time.
Tested on a RTL8812EU adapter with OpenIPC apfpv firmware.
🤖 Generated with Claude Code