Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b5a874c
Apply refreshed DNS and internet block policy
kasnder Sep 5, 2026
2f72493
Fix native TCP window wrap and empty UDP
kasnder Sep 5, 2026
aff5fe5
Avoid redundant screen-off DoH evictions
kasnder Sep 5, 2026
ce44f66
Document hotpath audit and fix status
kasnder Sep 5, 2026
24f169b
Keep native regression tests outside workflow
kasnder Sep 5, 2026
43985e0
Correct native test CI status
kasnder Sep 5, 2026
fd6563f
Preserve intermediate DNS CNAME trackers
kasnder Sep 5, 2026
4f2732b
Normalise overlapping TCP retransmissions
kasnder Sep 5, 2026
4bd8a16
Fix high-severity UDP and WireGuard hotpaths
kasnder Sep 5, 2026
2499081
Record completion of high-severity hotpath fixes
kasnder Sep 5, 2026
88d1489
Address high-hotpath review findings
kasnder Sep 5, 2026
39e4f14
Merge remote-tracking branch 'origin/master' into codex/reconcile-hot…
kasnder Sep 6, 2026
c29ee35
Merge branch 'codex/reconcile-hotpath-912' into codex/reconcile-hotpa…
kasnder Sep 6, 2026
cf4cbd2
Merge remote-tracking branch 'origin/master' into codex/reconcile-hot…
kasnder Sep 6, 2026
86117f3
Merge branch 'codex/reconcile-hotpath-912' into codex/reconcile-hotpa…
kasnder Sep 6, 2026
e92d3d5
Run validation for dependent pull request bases
kasnder Sep 6, 2026
18692e3
Merge branch 'codex/reconcile-hotpath-912' into codex/reconcile-hotpa…
kasnder Sep 6, 2026
f218a99
Preserve DNS question provenance and refresh blocking evidence
kasnder Sep 6, 2026
080fa3e
Integrate tested DNS provenance fix into CNAME layer
kasnder Sep 6, 2026
09e627e
Merge bounded hotpath batch into remaining high-severity fixes
kasnder Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
branches: [ master, main ]
tags: [ '[0-9]*' ]
pull_request:
branches: [ master, main ]
branches: [ master, main, 'codex/**' ]

jobs:
native_defensive:
Expand Down
558 changes: 558 additions & 0 deletions HOTPATH_BUG_AUDIT.md

Large diffs are not rendered by default.

235 changes: 222 additions & 13 deletions app/src/main/jni/netguard/ip.c

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions app/src/main/jni/netguard/netguard.h
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,25 @@ int route_flow_lookup(int version, int protocol,
const void *daddr, uint16_t dport,
int *tunnel, int *uid_known);

// The policy verdict shares the flow cache generation with the route verdict.
// UNKNOWN means that the flow must take the normal Java block decision.
#define ROUTE_FLOW_VERDICT_UNKNOWN (-1)
#define ROUTE_FLOW_VERDICT_BLOCKED 0
#define ROUTE_FLOW_VERDICT_ALLOWED 1

int route_flow_lookup_verdict(int version, int protocol,
const void *saddr, uint16_t sport,
const void *daddr, uint16_t dport,
int *verdict);

void route_flow_store_verdict(int version, int protocol,
const void *saddr, uint16_t sport,
const void *daddr, uint16_t dport,
int verdict);
void route_flow_clear_verdict(int version, int protocol,
const void *saddr, uint16_t sport,
const void *daddr, uint16_t dport);

void route_flow_store(int version, int protocol,
const void *saddr, uint16_t sport,
const void *daddr, uint16_t dport,
Expand Down
78 changes: 78 additions & 0 deletions app/src/main/jni/netguard/policy.c
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ struct route_flow_entry {
uint8_t protocol;
uint8_t tunnel;
uint8_t uid_known;
int8_t verdict; // -1 unknown, 0 blocked, 1 allowed
uint16_t sport;
uint16_t dport;
uint8_t saddr[16];
Expand Down Expand Up @@ -251,6 +252,20 @@ static int route_flow_matches(const struct route_flow_entry *e, uint32_t gen,
now - e->time <= ROUTE_FLOW_MAX_AGE;
}

static struct route_flow_entry *route_flow_find(int version, int protocol,
const void *saddr, uint16_t sport,
const void *daddr, uint16_t dport,
uint32_t gen, time_t now) {
struct route_flow_entry *set = route_flows[route_flow_set(
version, protocol, saddr, sport, daddr, dport)];
for (int way = 0; way < ROUTE_FLOW_WAYS; way++) {
struct route_flow_entry *e = &set[way];
if (route_flow_matches(e, gen, version, protocol, saddr, sport, daddr, dport, now))
return e;
}
return NULL;
}

/**
* The remembered verdict for this flow, if any.
*
Expand All @@ -277,6 +292,22 @@ int route_flow_lookup(int version, int protocol,
return 0;
}

int route_flow_lookup_verdict(int version, int protocol,
const void *saddr, uint16_t sport,
const void *daddr, uint16_t dport,
int *verdict) {
uint32_t gen = atomic_load_explicit(&route_flow_gen, memory_order_acquire);
time_t now = time(NULL);
struct route_flow_entry *e = route_flow_find(
version, protocol, saddr, sport, daddr, dport, gen, now);
if (e == NULL)
return 0;

e->time = now;
*verdict = e->verdict;
return 1;
}

/**
* Remember this flow's verdict, in the first empty/stale/current-gen-oldest
* way of its set. It is a cache, and a miss only costs the fallback path that
Expand All @@ -299,11 +330,13 @@ void route_flow_store(int version, int protocol,
// to a wrong answer).
time_t now = time(NULL);
struct route_flow_entry *victim = NULL;
int8_t verdict = ROUTE_FLOW_VERDICT_UNKNOWN;
struct route_flow_entry *oldest = &set[0];
for (int way = 0; way < ROUTE_FLOW_WAYS; way++) {
struct route_flow_entry *e = &set[way];
if (route_flow_matches(e, gen, version, protocol, saddr, sport, daddr, dport, now)) {
victim = e;
verdict = e->verdict;
break;
}
if (victim == NULL && (e->gen != gen || now - e->time > ROUTE_FLOW_MAX_AGE))
Expand All @@ -320,6 +353,7 @@ void route_flow_store(int version, int protocol,
e->protocol = (uint8_t) protocol;
e->tunnel = (uint8_t) (tunnel ? 1 : 0);
e->uid_known = (uint8_t) (uid_known ? 1 : 0);
e->verdict = verdict;
e->sport = sport;
e->dport = dport;
memset(e->saddr, 0, sizeof(e->saddr));
Expand All @@ -328,3 +362,47 @@ void route_flow_store(int version, int protocol,
memcpy(e->daddr, daddr, alen);
e->time = now;
}

void route_flow_store_verdict(int version, int protocol,
const void *saddr, uint16_t sport,
const void *daddr, uint16_t dport,
int verdict) {
if (verdict != ROUTE_FLOW_VERDICT_BLOCKED && verdict != ROUTE_FLOW_VERDICT_ALLOWED)
return;

uint32_t gen = atomic_load_explicit(&route_flow_gen, memory_order_acquire);
time_t now = time(NULL);
struct route_flow_entry *e = route_flow_find(
version, protocol, saddr, sport, daddr, dport, gen, now);

// A policy result can be produced before the route branch (for example a
// blocked first TCP packet), so make sure there is a generation-scoped
// entry to attach it to. Only create a missing entry here: an existing
// route may carry a resolved per-app tunnel and uid_known decision that a
// policy verdict must not replace with the global default.
if (e == NULL) {
route_flow_store(version, protocol, saddr, sport, daddr, dport,
route_default_is_tunnel(), 0);
gen = atomic_load_explicit(&route_flow_gen, memory_order_acquire);
now = time(NULL);
e = route_flow_find(version, protocol, saddr, sport, daddr, dport, gen, now);
}

if (e != NULL) {
e->verdict = (int8_t) verdict;
e->time = now;
}
}

void route_flow_clear_verdict(int version, int protocol,
const void *saddr, uint16_t sport,
const void *daddr, uint16_t dport) {
uint32_t gen = atomic_load_explicit(&route_flow_gen, memory_order_acquire);
time_t now = time(NULL);
struct route_flow_entry *e = route_flow_find(
version, protocol, saddr, sport, daddr, dport, gen, now);
if (e != NULL) {
e->verdict = ROUTE_FLOW_VERDICT_UNKNOWN;
e->time = now;
}
}
Loading