Skip to content
ppdragon16Public
forked from daeuniverse/dae

About

eBPF-based Linux high-performance transparent proxy solution.

Resources

Stars

5 stars

Watchers

0 watching

Forks

 
 

Latest commit

 

History

1,438 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dae-ppdn

中文 | English

dae-ppdn is a personal fork of dae-next, continuously enhanced with features and performance optimizations based on the author's real-world usage and needs.

Like upstream dae, it is a high-performance transparent proxy solution that leverages eBPF in the Linux kernel for traffic splitting — direct traffic bypasses the proxy entirely, achieving near-zero performance overhead.

Project Lineage

dae (daeuniverse/dae)          — Original project
 └─ dae-next (LostAttractor/dae) — Refactored outbound interface, new config options
      └─ dae-ppdn (this repo)    — HTTP APIs, DNS enhancements, extended protocol support

dae-next vs dae

Category Change
dial_mode replacement Replaced with three independent config options: dial_target_override, reroute_mode, sniff_verify_mode
Group priority Nodes in a group can now define a priority with weight, order, and latency parameters (e.g. priority: '0,2(,300ms)')
min_moving_avg EMA params The min_moving_avg policy now supports configurable EMA algorithm parameters: min_moving_avg(window, alpha) (e.g. min_moving_avg(10s, 0.2))
Metrics Restructured internal metrics
Outbound interface Refactored — all protocols require re-adaptation (progress tracked at LostAttractor/dae)

Config compatibility: dae-ppdn is compatible with dae-next configs. dae-next is not compatible with dae configs.

dae-ppdn vs dae-next

Category Additions
HTTP APIs RESTful command server via command_port (redirect control, priority management, dynamic DNS static entries)
udp_sniff_ports Configurable UDP port list for traffic sniffing (default: 443)
DNS dns_cache_tag Dialer-level DNS cache domain annotation (-l), allowing dialers on the same VPS/region to share DNS cache
DNS ecs EDNS0 Client Subnet control: global dns.ecs default (strip) plus per-dialer [ecs: ...] annotation (strip/<cidr>/pass) — stops client-subnet leaks and CDN region mismatch through proxies
DNS static User-defined static DNS entries with A, AAAA, TXT records — hot-reloadable via HTTP API
DNS via Route DNS queries through a specific outbound group (e.g. proxy_dns(via: ai))
DNS race Query multiple upstreams concurrently, first response wins — define race groups in the upstream section
DNS response routing Match DNS responses by answering upstream / response code / answer IPs / qtype / client and accept, reject, or re-resolve via another upstream
DNS mac + sip Per-client DNS filtering by MAC address or source IP
DNS pool tuning Configurable udp_pool_size, udp_pool_ttl, tcp_pool_size, tcp_pool_ttl
Protocols Extended support: Trojan, SSR, SS, SS2022, VLESS, VMess, AnyTLS, Tuic (v5), Juicity, Hysteria2
VLESS mux Supports both v2ray-native mux and sing-box smux

Note: Some protocol parameters may have incomplete support. Refer to the source for specifics.

Some performance optimizations are inspired by kdae. Notably, eBPF maps use BPF_MAP_TYPE_HASH instead of BPF_MAP_TYPE_LRU_HASH — the kernel never silently evicts entries, avoiding state inconsistency between kernel and userspace. Instead, a userspace Janitor (control/bpf_map_janitor.go) periodically scans and cleans up stale entries with explicit timeouts per map type.

Node Priority

Each node in a group can carry a priority annotation in the filter line, controlling its relative weight in node selection:

group {
  hk {
    filter: subtag(sub1) && name(regex:'香港') [priority: '0,2(,300ms)']
    filter: subtag(sub2) && name(regex:'香港') [priority: '0,1(,200ms)']
    filter: subtag(sub3) && name(regex:'香港')
    policy: min_moving_avg
  }
}

The priority value consists of a default priority and optional conditional priorities based on observed latency:

priority: '<default>[,<pri>(<latency_low>,<latency_high>)[; ...]]'
Part Meaning
<default> Base priority used when no latency condition matches
<pri>(<low>,<high>) Override priority when the node's observed latency falls within [low, high]

In the example above:

  • sub1 nodes default to priority 0, but jump to priority 2 when latency is below 300ms — with a min_moving_avg policy, higher-priority nodes are preferred, so sub1 dominates when it's fast.
  • sub2 nodes default to priority 0, jump to priority 1 when below 200ms — a tighter latency requirement with lower reward.
  • sub3 nodes have no annotation, meaning priority defaults to 0 with no conditional boost.

This mechanism allows fine-grained traffic steering: give higher-quality nodes more traffic during good periods, without permanently de-prioritizing other nodes.

Outbound Redirect

Outbound redirect allows one group to transparently forward all traffic to another group. A redirect group has no nodes, filters, or policies of its own — it acts as a lightweight alias.

Redirect is resolved at two levels:

  • Config level — declare redirect: <target> in a group block. All traffic routed to the source group is forwarded to the target group via a direct dialer.
  • Runtime level — change redirect targets on the fly via the HTTP API, no config reload required.
group {
  # These groups forward traffic to region-specific groups
  macbook {
    redirect: tw
  }
  wg {
    redirect: hk
  }
  ai {
    redirect: sg
  }

  # Real groups with nodes and policies
  hk {
    filter: subtag(sub1) && name(regex:'香港')
    policy: min_moving_avg(10s, 0.2)
  }
  tw {
    filter: subtag(sub1) && name(regex:'台湾')
    policy: min_moving_avg(10s, 0.2)
  }
  sg {
    filter: subtag(sub1) && name(regex:'新加坡')
    policy: min_moving_avg(10s, 0.2)
  }
}

routing {
  # Route specific devices/traffic to the redirect groups
  mac('11:22:33:44:55:66') -> macbook
  sip(10.0.0.1/24) -> wg
  domain(keyword:gemini, keyword:openai) -> ai
  fallback: hk
}

Redirect groups are zero-cost: they carry no nodes, skip connectivity checks, and when redirected to direct or block, traffic is short-circuited entirely in eBPF kernel space without touching userspace.

Hot Config Update

dae update allows hot-updating specific parts of the running configuration without a full reload — existing connections are not interrupted. It works by re-reading the config file and applying only the changed sections.

dae update sub     # Re-fetch subscriptions, refresh node list
dae update dns     # Apply DNS config changes (upstreams, routing rules)
dae update routing # Apply routing rule changes

dae update sub

Subscription update re-resolves all subscriptions and hot-swaps dialers, but the overall group structure must be preserved — structural changes require a full reload (SIGUSR1):

Allowed Rejected (use SIGUSR1 for full reload)
Add/remove nodes via subscription Add or delete a group block
Modify filter rules and annotations Rename a group
Change policy parameters Change group count
Change node priority / latency params Switch a group between redirect and normal
Modify redirect target (redirect: x → redirect: y)
Add/remove subscription blocks

Note: Only one dae update command can run at a time — concurrent updates are silently skipped.

dae update dns

Hot-swaps the entire DNS subsystem (upstreams, routing, cache settings). The command will fail if a via target references a group that does not exist.

dae update routing

Hot-swaps the routing rule table. The command will fail if a routing rule references a group (outbound) that does not exist.

HTTP APIs

When command_port is set in the global config, dae-ppdn exposes RESTful HTTP endpoints:

Redirect Management

# List all outbounds with redirect targets
curl http://localhost:1111/redirect

# Redirect an outbound to a different group
curl -X PUT http://localhost:1111/redirect/fb \
  -H "Content-Type: text/plain" -d 'tw'

# Remove a redirect (point back to itself)
curl -X PUT http://localhost:1111/redirect/fb \
  -H "Content-Type: text/plain" -d 'fb'

Priority Management

# List all priorities
curl http://localhost:1111/priority

# Change a specific dialer's priority (via query params)
curl -X PUT "http://localhost:1111/priority?outbound=hk&subtag=sub3&dialer=香港" \
  -H "Content-Type: text/plain" -d '0,3(,300ms)'

Combining redirect and priority APIs enables manual node switching: redirect a group to a single-node group, or boost a specific node's priority to maximum to force traffic through it.

Reverse IP Lookup

# Find which domains resolved to a given IP
curl http://localhost:1111/lookup/1.2.3.4

Static DNS Management

Static DNS entries defined in dns.static can be updated at runtime without restart:

# List all static entries
curl http://localhost:1111/static

# Get a specific entry
curl http://localhost:1111/static/local_nas

# Update an entry (multi-field)
curl -X PUT http://localhost:1111/static/local_nas \
  -H "Content-Type: text/plain" \
  -d $'a: 192.168.16.42\naaaa: fdfe::be24:11ff:fe64:1234\nttl: 3600'

Static DNS responses are not cached, so updates take effect immediately.

DNS Enhancements

dns_cache_tag (-l annotation)

Dialers sharing the same -l tag use a shared DNS cache domain. This is useful when multiple dialers reside on the same VPS in the same region — they can safely share cached DNS results, reducing upstream queries:

group {
  mix {
    filter: subtag(sub1) && name(keyword:'新加坡') [dns_cache_tag: 'sg']
    filter: subtag(sub2) && name(keyword:'东京')   [dns_cache_tag: 'jp']
    policy: min_moving_avg
  }
}

ecs (EDNS0 Client Subnet)

Queries forwarded through a proxy while carrying the client's ECS option make CDNs schedule by the client's region, yet the connection actually leaves from the exit IP: answers and connection path disagree, and the client subnet leaks through the tunnel. dae strips the client subnet by default (dns { ecs: 'strip' }), and allows a per-dialer override via the filter annotation:

group {
  mix {
    filter: subtag(sub1) [ecs: 'strip']           # remove the client subnet before forwarding (default)
    filter: subtag(sub2) [ecs: '203.0.113.0/24']  # announce this subnet so CDN answers match that region
    filter: subtag(sub3) [ecs: 'pass']            # forward the client's ECS option as-is
    policy: min_moving_avg
  }
}

Dialers applying different ECS policies never share cached DNS answers — the effective policy is mixed into the cache key, and a CIDR value has its host bits masked automatically.

dns/static

Define static A, AAAA, and TXT records directly in the config. Then reference them in DNS routing via static(name):

dns {
  static {
    local_nas {
      a: 192.168.16.42
      aaaa: 'fdfe::be24:11ff:fe64:3598'
      ttl: 600
    }
  }
  routing {
    request {
      qname(suffix:mydomain.xyz) -> static(local_nas)
    }
  }
}

dns/via

Force a DNS upstream query to use a specific outbound group:

qname(keyword:gemini, keyword:openai) -> proxy_dns(via: ai)

dns/race

Define a race group in the upstream section — it queries its upstreams concurrently and uses the first response:

upstream {
    race_dns: 'race(udp://1.1.1.1:53, udp://8.8.8.8:53)'
}

Reference it from routing by tag:

qname(geosite:gfw) -> race_dns

Members may be raw links or the tags of other upstreams. The group can also be bound to an outbound group at the reference site — every member is then dialed through that group while still picking the best node inside it:

qname(geosite:gfw) -> race_dns(via: ai)

Cached answers are picked in config order: the first member with a fresh entry answers without dialing. Otherwise the first expired entry is served as a stale answer while every member holding one is refreshed in the background — one flight per member, so that refresh races the group as well.

AAAA queries skip members whose dialer cannot proxy IPv6 and are answered empty only when no member can. The race covers the forwarding step: response rules run on the winning answer, and a rule that sends the query back to the group starts another round against its members. Tags must be unique, and a group with a single member is not a group at all: at load time its tag binds straight to that upstream, exactly as if it had been declared directly.

dns/response

The response block matches a DNS response — by the upstream that answered it, the response code, the IPs in the answer, the qtype, or the client — and then accepts it, rejects it, or re-resolves the query through another upstream. The canonical use is anti-poisoning: if a domestic upstream answers a non-CN IP for a foreign name, re-query through another upstream — a race group is allowed as the target, in which case its members are queried concurrently again:

response {
  rcode(nxdomain) -> accept   # names that don't exist: accept, don't re-resolve
  !upstream(race_dns) && !ip(geoip:private) && !ip(geoip:cn) -> race_dns
  fallback: accept
}

Notes:

  • rcode(...) matches the response code: noerror, formerr, servfail, nxdomain, notimp, refused, ... (case-insensitive) or the number (rcode(3)). An NXDOMAIN carries no answer IPs, so ip(...) never matches it — without an rcode rule, every NXDOMAIN from that upstream triggers a re-resolution round-trip. Trade-off: a name its upstream answers with NXDOMAIN is no longer recovered by the re-resolution.
  • upstream(<race tag>) matches a response answered by any member of that race group (the group itself never answers; responses are attributed to the member that did). A race group works as the re-resolution target too; the lookup-depth bound spans the expansion, so a rule that does not exclude the group's members converges to SERVFAIL instead of looping forever.
  • upstream(...) matches the upstream the response came from. A bare name also covers the via:-bound shadow members: a response answered through race_dns(via: ai) matches upstream(cf_dns), upstream('cf_dns(ai)'), or the group as a whole (upstream(race_dns)).
  • Excluding the re-resolution target with !upstream(...) is required: it stops the re-resolved answer from matching the same rule again and exhausting the lookup-depth bound (3), which would turn the query into SERVFAIL.
  • !ip(geoip:private) keeps LAN/static answers out of the re-resolution: they are intentional answers, and public upstreams would only replace them with NXDOMAIN.
  • Re-resolved answers are cached like ordinary ones, so subsequent queries hit the cache and only re-run the (cheap) response matching.
  • ! negation, multiple values (upstream(a, b) = a OR b), and && chaining with the other matchers (qtype, rcode, ip, mac, sip, qname) all work.
  • Everything else falls back to fallback: (accept or reject).

dns/mac + dns/sip

Per-client DNS filtering based on MAC address or source IP:

qtype(aaaa) && mac('11:22:33:44:55:66') -> reject
qtype(aaaa) && sip(192.168.1.100) -> reject

DNS Pool Tuning

Control DNS connection pooling behavior per forwarder (keyed by upstream + dialer + target):

dns {
  udp_pool_size: 10     # UDP connections per forwarder
  udp_pool_ttl: 10m     # TTL for idle UDP connections
  tcp_pool_size: 2      # TCP connections per forwarder
  tcp_pool_ttl: 30s     # TTL for idle TCP connections
}

Supported Protocols

Protocol Schemes Notes
HTTP(S) http, https Generic HTTP CONNECT proxy
Socks5 socks, socks5
Shadowsocks ss, shadowsocks AEAD, stream ciphers, SS2022 (2022-blake3-aes-*), simple-obfs, v2ray-plugin (WS+TLS)
ShadowsocksR ssr, shadowsocksr
VMess vmess AEAD, alterID=0; TCP, WS, TLS, gRPC, Meek, HTTPUpgrade
VLESS vless Reality, gRPC, Meek, HTTPUpgrade, mux, smux
Trojan trojan, trojan-go trojan-gfw and trojan-go
Tuic tuic v5
Juicity juicity
Hysteria2 hysteria2, hy2
AnyTLS anytls
Proxy chain -> Chain multiple protocols with -> separator

See docs/en/proxy-protocols.md for URI schemas and details.

VLESS Mux & SMux

VLESS supports both v2ray-native mux and sing-box smux for connection multiplexing. Add these query parameters to the VLESS node link:

Parameter Type Default Description
mux / multiplex bool false Enable v2ray-native mux
smux bool false Enable sing-box smux
mux_concurrency int 8 Max concurrent streams per connection
mux_idle_timeout int 300 Idle timeout in seconds before closing a sub-connection

Note: mux and smux are incompatible with flow (e.g. xtls-rprx-vision).

Example:

vless://uuid@host:port?type=tcp&security=reality&smux=1&mux_concurrency=4&mux_idle_timeout=120#NodeName

AnyTLS Idle Session

AnyTLS supports maintaining a pool of idle sessions to avoid the overhead of establishing new connections. Configure via URL query parameters:

Parameter Type Default Description
minIdleSession int 0 Minimum idle sessions kept alive for immediate use
idleCheckInterval duration — How often to scan for expired idle sessions (e.g. 30s)
idleTimeout duration — Max lifetime of an idle session before recycling (e.g. 5m)

Example:

anytls://user@host:port?minIdleSession=2&idleCheckInterval=30s&idleTimeout=5m#NodeName

Getting Started

Please refer to the Quick Start Guide for kernel requirements, installation, and minimal configuration.

A full example config is available at example_next.dae.

License

AGPL-3.0 (C) daeuniverse

Acknowledgements

About

eBPF-based Linux high-performance transparent proxy solution.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages