fix: recognise the tunnel device OpenVPN opened #15

Closed
mysticalsoap wants to merge 1 commit from fix/dco-device-detection into trunk
Owner

Problem

aqomui learns the tunnel's interface by matching TUN/TAP device in OpenVPN's
output. DCO -- kernel offload, the default data path since 2.6 -- never prints
that line, so on any current OpenVPN self.tun stayed None and everything
keyed to it was quietly skipped: DNS never reached the tunnel, and the bypass
has no interface to give dnsmasq.

The parse was already unsound where the line does appear. It stripped the
timestamp with line.replace(time.asctime(), '') -- which matches only while
OpenVPN's format agrees with Python's asctime and the second has not
ticked over between logging and parsing -- then took a fixed field index. With
2.7's timestamp format the strip does nothing and index [3] lands on the
literal word device, so self.tun became "device" rather than "tun0".
That affects the userspace path too, not only DCO.

Fix

Match the device name by pattern, removing the dependency on both the timestamp
and the field position. Covers DCO device tun0 opened and
ovpn-dco device [tun0] opened alongside the original form.

Applying DNS needed a second change to go with it. OpenVPN configures the
tunnel's servers itself (2.7 ships /usr/lib/openvpn/dns-updown), and the
pushed-DNS parse keys off PUSH: Received control message, which 2.7 does not
print either. So with the device name known, aqomui would have overwritten
the servers actually in use with its configured fallbacks. It now only routes
queries into the tunnel when it never saw a push and no alternative servers were
requested -- which is exactly what was missing: the servers were already right,
the routing domain was absent.

Verification

Added tests/test_tun_device.py -- no root, network or account needed. Both DCO
forms are taken verbatim from a 2.7.6 connection log, alongside the userspace
forms and lines that mention a device but do not announce one
(net_iface_up: set tun0 up, net_addr_v4_add: ... dev tun0).

Observed directly on a live 2.7.6 + DCO connection: TUN/TAP device appears
zero times in the whole session log, as do PUSH: Received control message
and dhcp-option DNS. tun0 carried Proton's 10.96.0.1 -- set by OpenVPN,
not aqomui -- and no routing domain at all.

Known trade-off, please read before merging

~. sends every lookup into the tunnel. That is what closes the leak, and
it also breaks split-horizon DNS: on this machine an internal host that resolves
to a LAN address via the local resolver started resolving to its public address
instead, and became unreachable. Confirmed by setting ~. by hand.

resolved picks the most specific matching domain, so the remedy is a narrower
routing domain on the physical link (~internal.example on enp5s0) which then
outranks ~. for that suffix. That is per-network configuration aqomui has no
business guessing, but it does mean this fix can break LAN name resolution for
anyone with a split-horizon setup and no such override.

Worth deciding whether aqomui should expose a "keep these domains off the
tunnel" setting rather than leaving users to discover this. Raised as an audit
item, not addressed here.

Underlying weakness

Both halves of this are the same defect: reading state out of human-readable log
output that upstream is free to change, and has. OpenVPN's management
interface
reports device, state and pushed options as structured events
instead. That is the durable fix and a substantially larger change -- flagged
for the audit as a concrete, bounded modernisation target rather than a vague
refactor.

## Problem aqomui learns the tunnel's interface by matching `TUN/TAP device` in OpenVPN's output. DCO -- kernel offload, the default data path since 2.6 -- never prints that line, so on any current OpenVPN `self.tun` stayed `None` and everything keyed to it was quietly skipped: DNS never reached the tunnel, and the bypass has no interface to give dnsmasq. The parse was already unsound where the line *does* appear. It stripped the timestamp with `line.replace(time.asctime(), '')` -- which matches only while OpenVPN's format agrees with Python's `asctime` **and** the second has not ticked over between logging and parsing -- then took a fixed field index. With 2.7's timestamp format the strip does nothing and index `[3]` lands on the literal word `device`, so `self.tun` became `"device"` rather than `"tun0"`. That affects the userspace path too, not only DCO. ## Fix Match the device name by pattern, removing the dependency on both the timestamp and the field position. Covers `DCO device tun0 opened` and `ovpn-dco device [tun0] opened` alongside the original form. Applying DNS needed a second change to go with it. OpenVPN configures the tunnel's servers itself (2.7 ships `/usr/lib/openvpn/dns-updown`), and the pushed-DNS parse keys off `PUSH: Received control message`, which 2.7 does not print either. So with the device name known, aqomui would have *overwritten* the servers actually in use with its configured fallbacks. It now only routes queries into the tunnel when it never saw a push and no alternative servers were requested -- which is exactly what was missing: the servers were already right, the routing domain was absent. ## Verification Added `tests/test_tun_device.py` -- no root, network or account needed. Both DCO forms are taken verbatim from a 2.7.6 connection log, alongside the userspace forms and lines that mention a device but do not announce one (`net_iface_up: set tun0 up`, `net_addr_v4_add: ... dev tun0`). Observed directly on a live 2.7.6 + DCO connection: `TUN/TAP device` appears **zero** times in the whole session log, as do `PUSH: Received control message` and `dhcp-option DNS`. `tun0` carried Proton's `10.96.0.1` -- set by OpenVPN, not aqomui -- and no routing domain at all. ## Known trade-off, please read before merging `~.` sends **every** lookup into the tunnel. That is what closes the leak, and it also breaks split-horizon DNS: on this machine an internal host that resolves to a LAN address via the local resolver started resolving to its public address instead, and became unreachable. Confirmed by setting `~.` by hand. resolved picks the most specific matching domain, so the remedy is a narrower routing domain on the physical link (`~internal.example` on `enp5s0`) which then outranks `~.` for that suffix. That is per-network configuration aqomui has no business guessing, but it does mean this fix can break LAN name resolution for anyone with a split-horizon setup and no such override. Worth deciding whether aqomui should expose a "keep these domains off the tunnel" setting rather than leaving users to discover this. Raised as an audit item, not addressed here. ## Underlying weakness Both halves of this are the same defect: reading state out of human-readable log output that upstream is free to change, and has. OpenVPN's **management interface** reports device, state and pushed options as structured events instead. That is the durable fix and a substantially larger change -- flagged for the audit as a concrete, bounded modernisation target rather than a vague refactor.
aqomui learns the tunnel's interface by matching "TUN/TAP device" in
OpenVPN's output. DCO -- kernel offload, the default data path since 2.6 --
never prints that line, so on any current OpenVPN self.tun stayed None and
everything keyed to it was quietly skipped: DNS was never applied to the
tunnel, and the bypass has nothing to give dnsmasq.

The parse was already unsound where the line does appear. It stripped the
timestamp with line.replace(time.asctime(), ''), which matches only while
OpenVPN's format agrees with Python's asctime and the second has not ticked
over between logging and parsing, then took a fixed field index. With 2.7's
timestamp format the strip does nothing and index 3 lands on the literal
word "device", so self.tun became "device" rather than "tun0" -- for the
userspace path too, not just DCO.

Matching the device name by pattern removes the dependency on both the
timestamp and the field position, and covers "DCO device tun0 opened" and
"ovpn-dco device [tun0] opened" alongside the original form.

Applying DNS needed a second change to go with it. OpenVPN configures the
tunnel's servers itself, and the pushed-DNS parse keys off "PUSH: Received
control message", which 2.7 does not print either -- so with the device name
now known, aqomui would have overwritten the servers actually in use with
its configured fallbacks. It now only routes queries into the tunnel when it
never saw a push and no alternative servers were requested, which is what
was missing: the servers were right, the routing domain was absent, and
every unqualified lookup went on leaving by the physical link.

Both of these are the same underlying weakness -- reading state out of
human-readable log output. OpenVPN's management interface reports it as
structured events instead, which is the durable fix and a larger change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Owner

changing to actual interface instead of log reading, don't need this change anymore

changing to actual interface instead of log reading, don't need this change anymore
mysticalsoap closed this pull request 2026-08-17 18:06:55 -04:00

Pull request closed

Sign in to join this conversation.
No description provided.