Skip to content

Add interface-based routing matcher with kernel and DNS integration#3

Open
olicesx wants to merge 149 commits intooptimize/code-quality-fixesfrom
codex/add-interface-matcher-for-dns-and-routing-rules
Open

Add interface-based routing matcher with kernel and DNS integration#3
olicesx wants to merge 149 commits intooptimize/code-quality-fixesfrom
codex/add-interface-matcher-for-dns-and-routing-rules

Conversation

@olicesx
Copy link
Copy Markdown
Owner

@olicesx olicesx commented Mar 3, 2026

Motivation

  • Add support for routing and DNS rules that match ingress/egress interfaces so rules like interface(wan:0eth) and interface(lan:3eth,4eth) can be used to distinguish WAN (out) and LAN (in) traffic.
  • Propagate interface metadata from eBPF/kernel routing into userspace routing decisions to allow consistent behavior across kernel and application-level matchers.
  • Provide a usable configuration/docs surface and examples for the new interface function in routing and DNS rules.

Description

  • Introduces a new match type MatchType_Interface and corresponding enum entries in generated constants and eBPF sync spec (common/consts/*, control/kern/ebpf_sync_defs.h).
  • Implements interface parsing and matching in userspace via component/routing/interface_matcher.go and wires the interface function into rule builders for routing and DNS (RoutingMatcherBuilder, RequestMatcherBuilder, ResponseMatcherBuilder).
  • Extends kernel eBPF routing and tproxy logic to capture ifindex and direction, store them in routing results, and match interface rules in kernel route evaluation (control/kern/tproxy.c, control/bpf_stub.go), and encodes interface data into BPF match sets.
  • Adds userspace support to resolve interface indices for kernel rules and to match by interface in the userspace routing matcher (control/routing_matcher_builder.go, control/routing_matcher_userspace.go), and exposes MatchWithInterface variants for routing and DNS that accept direction and ifname context.
  • Integrates interface-aware DNS routing by extracting interface context from UDP requests (control/dns_control.go) and adding RequestSelectWithInterface / ResponseSelectWithInterface and related plumbing in DNS component (component/dns/*).
  • Updates documentation and example files to document the interface matcher syntax and semantics (README.md, config/desc.go, example.dae, hack/templates/example-config.md).
  • Adds unit tests for interface parsing/matching (component/routing/interface_matcher_test.go, component/dns/interface_routing_test.go).

Testing

  • Ran go test ./component/routing -run TestParseInterfaceMatchers,TestMatchInterfaceDirectionAndName and the tests passed.
  • Ran go test ./component/dns -run TestRequestInterfaceMatcher and the test passed.
  • Verified build of modified packages containing new match types and function signatures by running go test ./... on the changed modules, which completed successfully.
  • No automated kernel/eBPF runtime tests were executed in CI as part of this change, only unit tests for parsing and userspace matching.

Codex Task

kix and others added 30 commits February 3, 2026 22:28
- Implemented a concurrency limit in DnsController to manage simultaneous DNS queries.
- Added a pipelined connection mechanism to optimize DNS request handling.
- Introduced tests for concurrency limits and race conditions in DNS processing.
- Enhanced error handling and logging in DNS listener and TCP relay functions.
- Refactored DNS handling methods to support singleflight for duplicate requests.
- Added benchmarks for pipelined connections and singleflight performance.
- Improved resource management with context cancellation in TCP relay operations.
… packet detection

- Implemented IsLikelyQuicInitialPacket to perform a fast header check on incoming UDP packets to filter out non-QUIC datagrams.
- Updated Sniffer to utilize this function for early rejection of irrelevant packets.
- Enhanced tests for IsLikelyQuicInitialPacket to ensure correct identification of QUIC initial packets.

refactor(control): optimize DNS connection handling and routing cache

- Improved connection pooling logic to prevent blocking on slow dials.
- Replaced sync.Map with atomic operations for pending request slots in pipelined connections.
- Added caching mechanism for UDP routing results with TTL to reduce redundant lookups.
- Updated DNS controller to use sync.Map for forwarder cache, enhancing concurrency.

test(control): add comprehensive tests for connection pool and routing cache

- Introduced tests for connection pool to ensure non-blocking behavior during slow dials.
- Added tests for response slot lifecycle to verify proper reuse and error handling.
- Implemented tests for UDP endpoint routing cache to validate hit and expiration behavior.
- guard DNS resolve against nil dialer to avoid panic paths in tests

- initialize direct dialers in netutils tests and skip when network is unavailable

- skip domain matcher geosite-dependent test when geosite.dat is absent

- gate eBPF kernel tests behind explicit dae_bpf_tests build tag

- remove fragile bitlist capacity assertions and validate tighten semantics

- enhance config marshaller for repeatable function filters and int/uint values

- make marshal test use secure temp files and assert round-trip idempotent output
- discard stale/mismatched UDP DNS responses and keep reading

- close connection only after stale/malformed response threshold

- add DoUDP regression tests for stale-discard and threshold-close
Revert DNS(53) goroutine fast-path introduced after run daeuniverse#697.

This aligns packet handling semantics with the last known-good run and avoids kernel-test WAN IPv6 UDP instability.
Drop pre-singleflight cache short-circuit introduced at run daeuniverse#698 boundary.

Restore the previous DNS handling flow to avoid WAN IPv6 UDP kernel-test regression.
- remove redundant EmitTask retry loop while preserving ordering semantics

- simplify queue recycle path after idle GC

- keep API and behavior unchanged
- add IPv4 fast path in hashAddrPort for sharded pools

- reuse single timestamp in LookupDnsRespCache to reduce hot-path overhead

- no API/behavior changes
Avoid waiting for secondary A/AAAA lookup when current query type is already preferred.

Keep response semantics unchanged; secondary lookup still runs for cache warming.
- allocate/wait secondary-lookup done channel only when needed

- early-return on canceled context in pipelined RoundTrip before write wait

- no API or protocol semantics changes
Problem:
- When DNS check option parsing fails or IP version is unavailable,
  CheckFunc returns (false, nil) to indicate 'skip check'
- But Check() treated this as failure, marking Alive=false and
  adding Timeout latency
- This caused all dialers to be marked unavailable when DNS check
  prerequisites weren't met, resulting in 'no alive dialer' errors

Root Cause:
Check() didn't distinguish between:
1. (true, nil) - success
2. (false, nil) - skip (should preserve state)
3. (false, err) - failure (should mark unavailable)

Solution:
Only update alive state on success (ok=true) or actual failure (err!=nil).
When (ok=false, err=nil), preserve existing alive state instead of
incorrectly marking as unavailable.

This allows dialers to remain alive when certain check types are
skipped due to configuration or network conditions.
Add regression tests for Dialer.Check state machine:
- repeated (ok=false, err=nil) skip checks must not mark dialer unavailable
- real failures (ok=false, err!=nil) must still mark dialer unavailable

This guards against cascading no-alive-dialer collapse when a check path
is temporarily skipped (e.g. DNS IP-version not available), while
preserving existing failure semantics.
kix and others added 14 commits March 2, 2026 16:31
- Introduced batch reading for UDP packets using ipv4.NewPacketConn and ReadBatch, improving performance for high-throughput scenarios.
- Refactored the Serve method in control_plane.go to handle both batch and single packet reads, with a fallback mechanism for single packet processing.
- Enhanced tests to verify the correctness of GSO handling in Anyfrom methods, ensuring GSO is not applied to single UDP datagrams.
- Added benchmarks for comparing single packet reads against batch reads, demonstrating performance improvements.
- Updated BPF tests to reflect changes in routing logic and added runtime metadata for active routing rules.
- Cleared LPM cache during rule reloads to prevent stale cache hits across different rule generations.
- Optimized ingress buffer strategies to reduce unnecessary memory copies, improving overall efficiency.
This commit resolves CI failures by restoring do_tproxy_wan_ingress
to match main branch behavior exactly.

Changes:
- Remove TCP early return (if l4proto != IPPROTO_UDP)
- Remove DNS fast path optimization in wan_ingress
- All UDP traffic now goes through conntrack (like main branch)

Rationale:
- The previous optimizations broke CI tests for WAN UDP scenarios
- main branch behavior is correct and doesn't interfere with forwarding
- Cloudflare origin pull continues to work because TCP is not processed
  in wan_ingress (only UDP conntrack is managed)

This aligns with the main branch design where wan_ingress only manages
UDP conntrack and doesn't make routing decisions or interfere with
forwarded traffic.
- Refactored LPM cache clearing in RoutingMatcherBuilder to use a generic delete function.
- Added GitHub Actions workflow for Go unit testing.
- Generated eBPF constants and types in ebpf_generated.go from JSON specification.
- Created ebpf_sync_spec.json to define match types, L4 protocols, IP versions, and outbound types.
- Introduced bpf_stub.go to provide a stub implementation for eBPF objects when not building with real eBPF.
- Added unit tests for validating required eBPF maps in control_plane_bpf_validation_test.go.
- Defined eBPF synchronization constants in ebpf_sync_defs.h.
- Created script gen_ebpf_sync.go to automate generation of eBPF synchronization files.
…lify error handling in _update_map_elem_by_cookie function
…t-practices

perf(domain-matcher): precompile bruteforce matcher patterns
Copilot AI review requested due to automatic review settings March 3, 2026 03:36
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 089817f44a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread control/dns_control.go Outdated
Comment thread control/kern/tproxy.c
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an interface(...) matcher that can be used in both traffic routing and DNS routing rules, and plumbs kernel-derived interface metadata (ifindex + direction) through to userspace match evaluation so routing decisions can be interface-aware end-to-end.

Changes:

  • Introduces a new match type/function (MatchType_Interface / interface(...)) and wires it into routing + DNS rule builders and matchers.
  • Extends eBPF routing/tproxy logic to record ifindex + direction and evaluate interface match rules in-kernel.
  • Updates docs/examples and adds unit tests for interface parsing/matching and DNS interface routing.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
hack/templates/example-config.md Adds interface matcher examples to generated example config docs.
example.dae Adds commented interface matcher examples for routing and DNS config.
control/utils.go Passes direction + interface name context into userspace routing matcher; derives ifname from ifindex.
control/routing_matcher_userspace.go Adds MatchWithInterface and evaluates MatchType_Interface in userspace matcher loop.
control/routing_matcher_builder.go Registers interface function; builds interface match sets; resolves ifindex for kernel rules.
control/kern/tproxy.c Adds ifindex + direction_in to routing result; adds kernel-side interface match evaluation; propagates skb ifindex.
control/kern/ebpf_sync_defs.h Adds MatchType_Interface to the shared enum used for eBPF/userspace sync.
control/dns_control.go Extracts interface context from BPF routing result and routes DNS requests/responses with interface-aware selectors.
control/bpf_stub.go Extends Go stub bpfRoutingResult struct with Ifindex and DirectionIn fields.
config/desc.go Documents the new interface function in routing and DNS sections.
component/routing/interface_matcher_test.go Adds tests for interface matcher parsing and direction/name matching.
component/routing/interface_matcher.go Implements parsing + matching logic for interface(wan:...) / interface(lan:...).
component/dns/response_routing.go Adds interface-aware matching to DNS response routing builder + matcher (MatchWithInterface).
component/dns/request_routing.go Adds interface-aware matching to DNS request routing builder + matcher (MatchWithInterface).
component/dns/interface_routing_test.go Adds a DNS request routing test covering interface matcher behavior.
component/dns/dns.go Adds RequestSelectWithInterface / ResponseSelectWithInterface and calls interface-aware matchers.
common/consts/routing.go Adds Function_Interface = "interface".
common/consts/ebpf_sync_spec.json Extends eBPF sync spec to include Interface match type.
common/consts/ebpf_generated.go Adds generated MatchType_Interface constant.
README.md Documents interface matcher availability and basic syntax.
Comments suppressed due to low confidence (2)

control/utils.go:46

  • net.InterfaceByIndex is called on every Route() invocation to derive ifname. This is on a hot path and can be relatively expensive (netlink/syscall per call depending on platform), and it is now in the critical routing decision loop. Consider caching ifindex -> ifname (e.g., sync.Map with occasional refresh, or wiring into the existing interface/link monitoring) and only resolving on cache miss.
	if routingResult.Ifindex > 0 {
		if iface, e := net.InterfaceByIndex(int(routingResult.Ifindex)); e == nil {
			ifname = iface.Name
		}

control/dns_control.go:889

  • dnsInterfaceContext resolves ifindex -> ifname via net.InterfaceByIndex for each UDP DNS request/response routing decision. Given DNS QPS can be high, this repeated lookup can add significant overhead. Consider sharing a cached resolver (e.g., sync.Map keyed by ifindex) with periodic invalidation or using the existing link monitor to keep names up to date.
	iface, err := net.InterfaceByIndex(int(req.routingResult.Ifindex))
	if err != nil {
		return direction, ""
	}
	return direction, iface.Name

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread control/kern/tproxy.c
Comment on lines +981 to +983
if (match_set->iface.zone == 1 && _is_wan)
direction_ok = true;
if (match_set->iface.zone == 2 && !_is_wan)
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In kernel route evaluation, MatchType_Interface uses _is_wan to decide zone semantics (wan vs lan). But _is_wan is defined as ctx->params->flag[2] (the first byte of the process name buffer), and in WAN egress paths pid_pname can be NULL (cookie missing) so flag[2] stays 0. That will incorrectly treat WAN egress traffic as non-WAN and prevent interface(wan:...) rules from ever matching in kernspace for those flows. Consider adding an explicit boolean/enum field in route_params (or a dedicated flag byte) to represent direction/zone (WAN egress vs LAN ingress) and set it unconditionally before calling route(), then use that field here instead of _is_wan.

Suggested change
if (match_set->iface.zone == 1 && _is_wan)
direction_ok = true;
if (match_set->iface.zone == 2 && !_is_wan)
/* zone: 1 => WAN egress, 2 => LAN ingress */
if (match_set->iface.zone == 1 && match_outbound)
direction_ok = true;
if (match_set->iface.zone == 2 && !match_outbound)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e01de00390

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread control/utils.go
}
ifname := ""
if routingResult.Ifindex > 0 {
if iface, e := net.InterfaceByIndex(int(routingResult.Ifindex)); e == nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve route interface names in host namespace

Route now converts routingResult.Ifindex with net.InterfaceByIndex directly, but that ifindex is written by tc eBPF from skb->ifindex on host-side interfaces, while packet serving is started under GetDaeNetns().With(...) (cmd/run.go), so this lookup can run in daens and return empty/wrong names. In that case MatchWithInterface receives ifname="", and interface-based userspace rerouting decisions (including control-plane reroute paths) silently miss.

Useful? React with 👍 / 👎.

Comment thread control/kern/tproxy.c
Comment on lines +978 to +979
if (match_set->iface.ifindex == 0)
break;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail unresolved interface rules instead of matching negations

The interface matcher bails out immediately when match_set->iface.ifindex == 0, and builder can intentionally emit ifindex=0 when resolution fails (it logs a warning and keeps the rule). With current rule reduction, that makes good_subrule stay false, so negated rules like !interface(...) are treated as matched for all packets while unresolved, which can broadly misroute traffic until the next reload.

Useful? React with 👍 / 👎.

@olicesx olicesx force-pushed the optimize/code-quality-fixes branch 2 times, most recently from 834dfbb to e1e101e Compare March 16, 2026 03:08
@olicesx
Copy link
Copy Markdown
Owner Author

olicesx commented Mar 18, 2026


Code review

Found 2 issues:

  1. Potential nil pointer dereference in addInterface functions for DNS routing (functions directly dereference upstream.Name without nil check, while similar functions like addQName and addQType explicitly check for nil)

func (b *RequestMatcherBuilder) addInterface(f *config_parser.Function, values []routing.InterfaceMatcher, upstream *routing.Outbound) (err error) {
upstreamId, err := b.upstreamToId(upstream.Name)

func (b *ResponseMatcherBuilder) addInterface(f *config_parser.Function, values []routing.InterfaceMatcher, upstream *routing.Outbound) (err error) {
upstreamId, err := b.upstreamToId(upstream.Name)

  1. Binary structure change to bpfRoutingResult requires synchronization between kernel and userspace (adding Ifindex uint32 and DirectionIn uint8 changes struct layout; verify that kernel C struct and Go struct remain ABI-compatible, especially regarding padding between Dscp and Ifindex)

dae/control/bpf_stub.go

Lines 87 to 100 in 59020de

}
type bpfRoutingResult struct {
_ structs.HostLayout
Mark uint32
Must uint8
Mac [6]uint8
Outbound uint8
Pname [16]uint8
Pid uint32
Dscp uint8
Ifindex uint32
DirectionIn uint8
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@olicesx
Copy link
Copy Markdown
Owner Author

olicesx commented Mar 18, 2026


Update on code review

After further analysis, both issues I reported appear to be false positives:

  1. Nil pointer dereference: The parameter is always allocated by (see ) and is never nil. Existing functions like and use the same pattern without nil checks.

  2. Binary structure mismatch: The PR correctly updates both the C kernel struct () and Go stub (), and ensures proper padding alignment. The layouts are compatible.

Please disregard my previous review - the code follows existing patterns correctly.

@olicesx olicesx force-pushed the optimize/code-quality-fixes branch from f83923b to 91f8500 Compare March 25, 2026 18:11
@olicesx olicesx force-pushed the optimize/code-quality-fixes branch from 08ebe0a to abdd21a Compare April 8, 2026 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants