Skip to content

Client: bring a persistent TUN interface (camex.ko) down and remove its address on exit (3.5.1) (#99) - #100

Merged
compot-dev merged 4 commits into
masterfrom
kmod-iface-cleanup
Oct 23, 2026
Merged

compot-dev merged 4 commits into
masterfrom
kmod-iface-cleanup

Conversation

@compot-dev

Copy link
Copy Markdown
Collaborator

Fixes #99.

With camex.ko (--tun-dev /dev/camex, or any device but /dev/net/tun) the interface belongs to the module and outlives the client. Closing the fd left camex UP with its address and the tunnel route until rmmod camex.

  • src/tun.c: the Linux backend remembers whether the interface outlives its fd (tun_persistent: opened by us, not /dev/net/tun). On close it clears IFF_UP and sets the address to 0.0.0.0, which removes it; the kernel drops the connected route and any route through the interface. The routes from CONFIG were already removed before. Logs Interface camex down, address removed. /dev/net/tun interfaces go away by themselves and are not touched; a device passed in (TUN provider, CAMEX_TUN_FD) is not ours to change.
  • docker/test-e2e-persistent-tun.sh (in CI): a persistent tun interface named camex (ip tuntap add, /dev/camex = the tun clone node) stands in for camex.ko:
    1. the client brings it up with its address and the tunnel route;
    2. after SIGTERM it is still there, but down, without address and route, and the client logs it;
    3. the next client on it gets its address again, and leaves it clean again;
    4. /dev/net/tun unchanged: tun0 goes away with the client.
  • Version 3.5.1, CHANGELOG.md.

Checked locally in containers (compot/cross):

  • native build without warnings, make test OK; QEMU=1 ci/build-musl.sh for x86_64, arm, aarch64, mipsel: unit tests OK;
  • test-e2e-persistent-tun.sh: 3.5.1 — PASS, 11/11; 3.5.0 from latest — the three cleanup checks fail, as expected.

On the real router (OpenWrt 15.05.1, kernel 3.18.92, camex.ko) the bug was seen with 3.5.0; this fix will be checked there with the CI build of this PR.

… address removed on exit (#99)

With /dev/camex (or any --tun-dev but /dev/net/tun) the interface outlives
the client: closing the fd left it UP with its address and the tunnel
route until rmmod camex. On close the client now clears IFF_UP and sets
the address to 0.0.0.0 (which removes it); the kernel drops the connected
route and any route through the interface. Interfaces from /dev/net/tun
go away by themselves; a device passed in (provider, CAMEX_TUN_FD) is
not touched.

test-e2e-persistent-tun.sh: a persistent tun interface named camex stands
in for camex.ko; clean after SIGTERM, reused by the next client;
/dev/net/tun unchanged. Version 3.5.1.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Clean up persistent Linux TUN interfaces on client exit

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Bring client-managed persistent TUN interfaces down and remove their addresses on exit.
• Preserve externally supplied TUN devices and the existing /dev/net/tun lifecycle.
• Add Docker regression coverage and document the fix in version 3.5.1.
Diagram

graph TD
  A["Client shutdown"] --> B["Linux TUN backend"] --> C{"Owned persistent?"}
  C -->|Yes| D["Down and unaddress"] --> E["Kernel route cleanup"] --> F["Close TUN fd"]
  C -->|No| F
Loading
High-Level Assessment

Keep cleanup in the existing TUN backend, where device ownership and the descriptor lifecycle are known. A separate shutdown path or networking dependency would add complexity without improving this targeted fix.

Files changed (5) +215 / -1

Bug fix (1) +46 / -0
tun.cClean up client-opened persistent Linux TUN interfaces +46/-0

Clean up client-opened persistent Linux TUN interfaces

• Tracks whether the opened device is persistent and, before closing its descriptor, brings its interface down and removes its IPv4 address. Leaves '/dev/net/tun' and externally supplied descriptors on their existing paths.

src/tun.c

Documentation (1) +11 / -0
CHANGELOG.mdDocument the 3.5.1 persistent-TUN fix +11/-0

Document the 3.5.1 persistent-TUN fix

• Describes interface cleanup, the exclusion for externally supplied devices, and the new regression test.

CHANGELOG.md

Other (3) +158 / -1
build.ymlRun persistent-TUN regression in x86_64 CI +4/-0

Run persistent-TUN regression in x86_64 CI

• Adds the Docker end-to-end test to the x86_64 workflow.

.github/workflows/build.yml

test-e2e-persistent-tun.shTest persistent interface cleanup and reuse +153/-0

Test persistent interface cleanup and reuse

• Uses a persistent Docker TUN interface to check setup, cleanup after SIGTERM, and reuse by a second client. Also confirms that a standard '/dev/net/tun' interface disappears when its client exits.

docker/test-e2e-persistent-tun.sh

version.hSet release version to 3.5.1 +1/-1

Set release version to 3.5.1

• Changes the compiled-in version from 3.5.0 to 3.5.1.

src/version.h

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. The tunnel test passes without a tunnel 🐞 Bug ≡ Correctness
Description
The test creates /dev/camex as a TUN clone device, but the --tun-dev path opens it without the
TUNSETIFF call that would attach the client's descriptor to the camex interface. The client can
still configure that interface and satisfy every address, route, shutdown, and reuse check while
packet I/O through its descriptor does not work.
Code

docker/test-e2e-persistent-tun.sh[R74-75]

+        mknod /dev/camex c 10 200
+        ip tuntap add dev camex mode tun
Evidence
The new script makes /dev/camex a clone node and separately creates camex. In the override
branch, the client opens that path; only the /dev/net/tun branch performs TUNSETIFF. The new
test checks interface configuration and cleanup but never packet traffic through the persistent
interface.

docker/test-e2e-persistent-tun.sh[71-76]
src/tun.c[532-578]
docker/test-e2e-persistent-tun.sh[107-146]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The persistent-interface stand-in uses a TUN clone node, but the client never attaches its opened descriptor to the interface. Administrative state checks can therefore pass without a working tunnel.
## Fix Focus Areas
- docker/test-e2e-persistent-tun.sh[71-76]
- docker/test-e2e-persistent-tun.sh[114-146]
## Recommended Fix
Use a test device whose opened descriptor is actually bound to the persistent interface, or add a module-backed integration test. Verify packet traffic through that interface as well as its state after shutdown and reuse.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. The shutdown test can pass without SIGTERM ✓ Resolved
Description
stop_client() ignores the result of pkill -TERM and returns success as soon as pgrep finds no
matching process. If a client exits between acquiring its address and the shutdown step, the test
can report a successful SIGTERM shutdown even though no signal reached it.
Code

docker/test-e2e-persistent-tun.sh[R99-101]

+    cli "pkill -TERM -f '^camex .*--name $1 '"
+    for _ in $(seq 1 10); do
+        cli "pgrep -f '^camex .*--name $1 ' >/dev/null" || return 0
Evidence
The client starts asynchronously, wait_addr checks only for an interface address, and
stop_client does not check whether pkill matched a process before treating an absent process as
success.

docker/test-e2e-persistent-tun.sh[81-95]
docker/test-e2e-persistent-tun.sh[97-105]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`stop_client()` can report successful SIGTERM shutdown when the client had already exited.
## Fix Focus Areas
- docker/test-e2e-persistent-tun.sh[97-104]
## Recommended Fix
Have `stop_client()` return failure if `pkill -TERM` did not signal a matching client, then retain the existing wait for that process to disappear.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Failed cleanup reports a clean interface ✓ Resolved
Description
tun_release_persistent() does not log a failed SIOCGIFFLAGS call and emits “Interface … down,
address removed” even when an ioctl failed. If reading the flags fails, the interface can remain up;
if clearing the address fails, it can retain its address, while the client closes the device and
reports success.
Code

src/tun.c[692]

+    log_message(LOG_INFO, "Interface %s down, address removed", tun_name);
Evidence
A failed flags read skips the down operation without logging it, and failures in either
state-changing ioctl do not prevent the unconditional success message. The caller then closes the fd
and resets its backend state regardless of the result.

src/tun.c[676-692]
src/tun.c[726-735]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Persistent-interface cleanup logs success even when reading flags or changing interface state fails.
## Fix Focus Areas
- src/tun.c[672-692]
- src/tun.c[726-735]
## Recommended Fix
Log failures from every cleanup ioctl, including SIOCGIFFLAGS. Track whether both the interface-down and address-removal steps succeeded, and emit the success message only when they did; otherwise emit a warning identifying the incomplete cleanup.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/tun.c Outdated
#99)

Review fix (Qodo): the success line was logged even when an ioctl
failed, and a failed SIOCGIFFLAGS was silent. Now each failure is logged
and the last line says whether the interface is down and without its
address, or what is left.
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

Comment thread docker/test-e2e-persistent-tun.sh Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3d8285c

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

Comment thread docker/test-e2e-persistent-tun.sh
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c1a6969

…ays what it does not cover (#99)

Review fixes (Qodo): stop_client fails when pkill finds no client (one
already gone got no signal); the header says the stand-in tests the
interface's life cycle only, packets over the real camex.ko are the QEMU
and device tests.
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit cc4697d

@compot-dev
compot-dev merged commit ce311af into master Oct 23, 2026
17 checks passed
@compot-dev
compot-dev deleted the kmod-iface-cleanup branch October 23, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client: with camex.ko the camex interface keeps its address and routes after the client stops

1 participant