Outbounds
Outbounds tell keen-pbr where matching traffic should go.
Every outbound tag must match ^[a-z][a-z0-9_]*$ and be at most 24 characters.
Most users only need:
- one
interfaceoutbound for the VPN connection - one
interfaceoutbound for the normal connection, oftenwan - optionally one
urltestoutbound if they want automatic failover between several connections
If you are not sure, start with interface outbounds and come back to the others later.
Common Example
{
"outbounds": [
{
"type": "interface",
"tag": "vpn",
"interface": "tun0",
"gateway": "10.8.0.1",
"gateway6": "2001:db8::1"
},
{
"type": "interface",
"tag": "wan",
"interface": "eth0",
"gateway": "192.168.1.1"
}
]
}Types
interface
Use this when you want to send traffic through a specific connection such as a VPN tunnel or your normal internet uplink.
| Field | Type | Required | Description |
|---|---|---|---|
tag |
string | yes | Unique identifier |
type |
string | yes | "interface" |
interface |
string | yes | Egress network interface name (e.g. tun0) |
gateway |
string | no | Optional IPv4 gateway address |
gateway6 |
string | no | Optional IPv6 gateway address |
If neither gateway nor gateway6 is set, keen-pbr creates both IPv4 and IPv6 default routes for the interface outbound.
If only one gateway is set, keen-pbr creates a routed default for that address family and an unreachable default for the other family so marked traffic cannot leak outside the outbound table.
If both gateway and gateway6 are set, keen-pbr creates distinct IPv4 and IPv6 default routes.
{
"outbounds": [
{
"type": "interface",
"tag": "vpn",
"interface": "tun0",
"gateway": "10.8.0.1",
"gateway6": "2001:db8::1"
}
]
}table
Use this only if another service on the system already created a separate routing table for you and you want keen-pbr to reuse it.
To find existing table IDs before choosing one, inspect the current policy routing state and routes. To see table name mappings, check /etc/iproute2/rt_tables:
ip rule show
cat /etc/iproute2/rt_tables| Field | Type | Required | Description |
|---|---|---|---|
tag |
string | yes | Unique identifier |
type |
string | yes | "table" |
table |
integer | yes | Existing routing table number |
{
"outbounds": [
{
"type": "table",
"tag": "custom_table",
"table": 200
}
]
}blackhole
Drops all matching traffic. Useful for blocking access to specific resources.
| Field | Type | Required | Description |
|---|---|---|---|
tag |
string | yes | Unique identifier |
type |
string | yes | "blackhole" |
{
"outbounds": [
{
"type": "blackhole",
"tag": "block"
}
]
}ignore
Passes traffic through without any routing modification. Use this to explicitly exclude traffic from other rules.
When a route rule resolves to an ignore outbound, keen-pbr installs a matching firewall pass-through verdict for that traffic. This stops further keen-pbr rule processing without setting a mark or dropping the packet. No routing table or ip rule is created for that match, so the packet follows the system’s normal routing. Since route rules are evaluated top to bottom and the first match wins, ignore is most useful for exception rules placed before broader catch-all rules.
| Field | Type | Required | Description |
|---|---|---|---|
tag |
string | yes | Unique identifier |
type |
string | yes | "ignore" |
{
"outbounds": [
{
"type": "ignore",
"tag": "direct"
}
]
}urltest
Use this when you have several candidate outbounds and want keen-pbr to automatically pick the best available one.
keen-pbr always appends terminal IPv4 and IPv6 unreachable default routes to the generated urltest table. This acts as a kill-switch: if no selected or fallback child route is usable, marked traffic is blocked instead of leaking via normal routing.
| Field | Type | Required | Description |
|---|---|---|---|
tag |
string | yes | Unique identifier |
type |
string | yes | "urltest" |
url |
string | yes | URL used for availability and latency checks |
interval_ms |
integer | no (default: 180000) |
Interval between probes in milliseconds |
probe_timeout_ms |
integer | no (default: 5000) |
Timeout for each individual probe attempt in milliseconds |
tolerance_ms |
integer | no (default: 100) |
Latency tolerance in ms; prevent outbound switching if the latency difference between the current and new best outbound is less than this tolerance |
outbound_groups |
array | yes | Ordered list of outbound groups (see below) |
retry |
object | no | Retry configuration (see below) |
circuit_breaker |
object | no | Circuit breaker configuration (see below) |
Outbound Groups
Groups are checked in order. Within the first healthy group, outbounds are selected by the lowest latency. If all outbounds in a group are unhealthy, the next group is evaluated. This lets you define more complex priority rules. For example, you can prefer one of two slower outbounds first, and if both are unavailable, fall back to another outbound that is faster but more expensive.
| Field | Type | Required | Description |
|---|---|---|---|
outbounds |
array of string | yes | Ordered list of outbound tags to try |
Retry Configuration
| Field | Type | Required | Description |
|---|---|---|---|
attempts |
integer | no (default: 3) |
Number of probe attempts before marking outbound as failed |
interval_ms |
integer | no (default: 1000) |
Delay between retry attempts in milliseconds |
Circuit Breaker Configuration
| Field | Type | Required | Description |
|---|---|---|---|
failure_threshold |
integer | no (default: 5) |
Consecutive failures before opening the circuit |
success_threshold |
integer | no (default: 2) |
Consecutive successes in half-open state to close the circuit |
timeout_ms |
integer | no (default: 30000) |
Time before transitioning from open to half-open state |
half_open_max_requests |
integer | no (default: 1) |
Max probe requests allowed in half-open state |
Circuit breaker states:
closed— healthy, traffic passes through normallyopen— failed, traffic blocked during cooldown periodhalf_open— testing recovery with limited probe requests
{
"outbounds": [
{
"type": "urltest",
"tag": "auto_select",
"url": "https://www.gstatic.com/generate_204",
"interval_ms": 180000,
"probe_timeout_ms": 5000,
"tolerance_ms": 100,
"outbound_groups": [
{ "outbounds": ["vpn1", "vpn2"] },
{ "outbounds": ["wan"] }
],
"retry": {
"attempts": 3,
"interval_ms": 1000
},
"circuit_breaker": {
"failure_threshold": 5,
"success_threshold": 2,
"timeout_ms": 30000,
"half_open_max_requests": 1
}
}
]
}