https://github.com/bavix/outway
Outway is a small service that steers egress traffic by domain. Policy is decided at the application layer (L7) using domain names from DNS, while enforcement is done at the network layer (L3/L4) by marking IPs and letting the OS route them (multi‑WAN by domains).
https://github.com/bavix/outway
dnsproxy
Last synced: about 1 month ago
JSON representation
Outway is a small service that steers egress traffic by domain. Policy is decided at the application layer (L7) using domain names from DNS, while enforcement is done at the network layer (L3/L4) by marking IPs and letting the OS route them (multi‑WAN by domains).
- Host: GitHub
- URL: https://github.com/bavix/outway
- Owner: bavix
- License: mit
- Created: 2025-09-23T17:17:32.000Z (9 months ago)
- Default Branch: master
- Last Pushed: 2025-12-17T10:24:46.000Z (6 months ago)
- Last Synced: 2026-03-29T14:51:33.900Z (3 months ago)
- Topics: dnsproxy
- Language: JavaScript
- Homepage:
- Size: 2.9 MB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 21
-
Metadata Files:
- Readme: README.md
- Changelog: changelog.md
- License: LICENSE
Awesome Lists containing this project
README
# Outway
Outway is a small service that steers egress traffic by domain. Policy is decided at the application layer (L7) using domain names from DNS, while enforcement is done at the network layer (L3/L4) by marking IPs and letting the OS route them (multi‑WAN by domains).
Example: you have two uplinks (wan1, wan2). You want `foo.example.com` via `wan1`, while `bar.example.com` always goes via `wan2`. With Outway you configure domain rules and the system enforces them.
## How it works
- Outway handles DNS queries for your apps
- On each DNS answer, it extracts IPs, assigns a mark with TTL, and programs the OS firewall
- Marked IPs follow the route/interface mapped to the matching domain rule group
- When TTL expires, the mark is removed automatically
## Features
- Domain‑based egress routing (interface per rule group)
- Request coalescing: deduplicates concurrent cache misses for the same host/QTYPE
- Clean URL format for upstream resolvers (single, strict format):
- `udp://host[:port]` (default 53)
- `tcp://host[:port]` (default 53)
- `tls://host[:port]` or `dot://host[:port]` (default 853)
- `quic://host[:port]` or `doq://host[:port]` (default 853)
- `https://host/path` (DoH, RFC 8484)
- Built‑in Admin UI with WebSocket realtime updates and polling fallback
- Prometheus metrics at `/metrics`
- Health endpoint at `/health`
## Caching
- LRU cache keyed by `fqdn:qtype` with per‑record TTL respected
- Expired entries are evicted on read; fresh responses are cached
- Singleflight coalescing prevents upstream stampedes for identical in‑flight queries
## Quick start
1) Install
```bash
go install github.com/bavix/outway@latest
```
2) Configure
Use `config.test.yaml` as a reference. Minimal realistic example (aligned with the test config):
```yaml
app_name: outway
listen:
udp: ":53"
tcp: ":53"
upstreams:
- name: cloudflare-doh
address: https://cloudflare-dns.com/dns-query
weight: 1
- name: cf-ipv4
address: udp://1.1.1.1:53
weight: 1
rule_groups:
- name: Default
description: Default egress group
via: utun4 # interface name
pin_ttl: true # keep TTL from DNS, don't shrink aggressively
patterns:
- "*.example.com"
```
Multi‑WAN example:
```yaml
rule_groups:
- name: wan1-sites
via: wan1
pin_ttl: true
patterns:
- "foo.example.com"
- name: wan2-sites
via: wan2
pin_ttl: true
patterns:
- "bar.example.com"
```
Fuller example (same structure as `config.test.yaml`):
```yaml
app_name: outway
listen:
udp: ":53"
tcp: ":53"
upstreams:
- name: cloudflare-doh
address: https://cloudflare-dns.com/dns-query
weight: 1
- name: cf-ipv4
address: udp://1.1.1.1:53
weight: 1
- name: cf-ipv6
address: udp://[2606:4700:4700::1111]:53
weight: 1
- name: google
address: udp://8.8.8.8:53
weight: 1
- name: opendns
address: udp://208.67.222.222:53
weight: 1
rule_groups:
- name: "YouTube & Google Services"
description: Route YouTube and Google services through specific interface
via: utun4
patterns:
- "*.youtube.com"
- "*.googlevideo.com"
- "*.ytimg.com"
- "*.googleapis.com"
- "*.googleusercontent.com"
pin_ttl: true
- name: Social Media
description: Social media platforms routing
via: utun4
patterns:
- "*.instagram.com"
- "*.facebook.com"
- "*.twitter.com"
- "*.x.com"
- "*.tiktok.com"
- "*.snapchat.com"
pin_ttl: true
- name: Streaming Services
description: Video streaming platforms
via: utun4
patterns:
- "*.netflix.com"
- "*.hulu.com"
- "*.disney.com"
- "*.amazon.com"
- "*.twitch.tv"
pin_ttl: true
- name: Development Tools
description: Development and version control services
via: utun3
patterns:
- "*.github.com"
- "*.gitlab.com"
- "*.bitbucket.org"
- "*.docker.com"
- "*.npmjs.org"
pin_ttl: true
- name: Blocked Domains
description: Blocked malicious domains
via: lo0
patterns:
- "*.malware.com"
- "*.phishing-site.com"
- "*.ads-tracker.com"
pin_ttl: true
history:
enabled: true
max_entries: 10000
log:
level: info
cache:
enabled: true
max_entries: 20000
http:
enabled: true
listen: 127.0.0.1:47823
read_timeout: 30s
write_timeout: 30s
idle_timeout: 2m0s
max_header_bytes: 1048576
hosts:
- pattern: localhost
a:
- 127.0.0.1
ttl: 60
- pattern: "*.example.com"
a:
- 127.0.0.1
ttl: 60
```
3) Run
```bash
outway run --config ./config.yaml
```
- Admin UI: `http://127.0.0.1:47823/`
- Metrics: `http://127.0.0.1:47823/metrics`
- Health: `http://127.0.0.1:47823/health`
## Commands
- `outway run` - Start the DNS proxy service
- `outway cleanup` - Cleanup all firewall rules created by Outway
- `outway self-update` - Update to the latest version from GitHub
- `outway --version` - Show version information
### Self-update
Update Outway to the latest version:
```bash
# Update to latest stable version
outway self-update
# Include prerelease versions
outway self-update --prerelease
```
The self-update command will download the appropriate binary for your platform, replace the current binary, and exit with code 42 to trigger automatic restart by your init system (systemd, OpenWrt /etc/init.d, etc.).
## Admin UI & configuration
- UI: `http://127.0.0.1:47823/` (by default)
- WebSocket realtime is used automatically; if WS is unavailable, UI switches to polling
To run the admin UI on a different address/port, configure the `http` section in the config:
```yaml
http:
enabled: true
listen: 0.0.0.0:8080 # address:port for Admin UI and API
read_timeout: 30s
write_timeout: 30s
idle_timeout: 2m0s
```
Upstreams in YAML are specified only in URL format — the type is derived from the scheme (`udp://`, `tcp://`, `dot://`, `doq://`, `https://`).
## Observability
- `/metrics` exposes Prometheus metrics (query rate, latency, marks, etc.)
- UI Dashboard shows:
- Uptime
- Queries in the last minute and error count (realtime)
- Cache hit rate (when available)
## System backends
- Linux: nftables/iptables
- macOS: pf
## Build
```bash
make build
make lint
```