Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,20 @@ BindAddress = 127.0.0.1:25344
# Avoid using spaces in the password field
#Password = ...

# Domain whitelist routing (optional). When TunnelDomains is set, only connections
# whose destination host matches one of the patterns are routed through wireguard;
# every other connection is dialed directly over your normal network. When
# TunnelDomains is unset, all traffic is routed through wireguard (default).
# Each TunnelDomains line is a single, full Go regular expression (RE2). Repeat
# the key for multiple patterns; do NOT comma-separate (so quantifiers like {2,4}
# keep working). Matching is case-insensitive and a trailing dot is ignored.
#TunnelDomains = ^(.*\.)?example\.com$
#TunnelDomains = ^ipinfo\.io$
# Set LogDomains = true to log every connection's destination host and whether it
# was routed to the TUNNEL or DIRECT. Useful for discovering which domains your
# apps reach before writing TunnelDomains. Off by default.
#LogDomains = true

# http creates a http proxy on your LAN, and all traffic would be routed via wireguard.
[http]
BindAddress = 127.0.0.1:25345
Expand All @@ -171,10 +185,18 @@ BindAddress = 127.0.0.1:25345
#CertFile = ...
#KeyFile = ...

# TunnelDomains / LogDomains work here too (same semantics as [Socks5] above).
#TunnelDomains = ^(.*\.)?example\.com$
#LogDomains = true

# SNI creates a transparent TLS proxy on your LAN, and all traffic would be routed via wireguard,
# using Server Name Indication as routing destination.
[SNI]
BindAddress = 0.0.0.0:443

# TunnelDomains / LogDomains work here too, matched against the TLS SNI hostname.
#TunnelDomains = ^(.*\.)?example\.com$
#LogDomains = true
```

Alternatively, if you already have a wireguard config, you can import it in the
Expand Down
96 changes: 87 additions & 9 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/go-ini/ini"
Expand Down Expand Up @@ -57,21 +58,27 @@ type TCPServerTunnelConfig struct {
}

type Socks5Config struct {
BindAddress string
Username string
Password string
BindAddress string
Username string
Password string
TunnelDomains []*regexp.Regexp
LogDomains bool
}

type SNIConfig struct {
BindAddress string
BindAddress string
TunnelDomains []*regexp.Regexp
LogDomains bool
}

type HTTPConfig struct {
BindAddress string
Username string
Password string
CertFile string
KeyFile string
BindAddress string
Username string
Password string
CertFile string
KeyFile string
TunnelDomains []*regexp.Regexp
LogDomains bool
}

type ResolveConfig struct {
Expand Down Expand Up @@ -421,6 +428,41 @@ func parseTCPServerTunnelConfig(section *ini.Section) (RoutineSpawner, error) {
return config, nil
}

// parseRegexList reads a whitelist of regular expressions from a section key.
// Each occurrence of the key (one per line; AllowShadows is enabled) is treated
// as a single, complete regex — the value is NOT split on commas, so quantifiers
// like `a{2,4}` work. An absent key yields no patterns. An invalid pattern is a
// configuration error, so --configtest rejects it before any traffic flows.
func parseRegexList(section *ini.Section, keyName string) ([]*regexp.Regexp, error) {
key, err := section.GetKey(keyName)
if err != nil {
return nil, nil
}

var patterns []*regexp.Regexp
for _, raw := range key.ValueWithShadows() {
raw = strings.TrimSpace(raw)
if raw == "" {
continue
}
re, cerr := regexp.Compile(raw)
if cerr != nil {
return nil, errors.New("invalid " + keyName + " regex `" + raw + "`: " + cerr.Error())
}
patterns = append(patterns, re)
}
return patterns, nil
}

// parseBoolKey reads an optional boolean key, defaulting to false when absent.
func parseBoolKey(section *ini.Section, keyName string) (bool, error) {
key, err := section.GetKey(keyName)
if err != nil {
return false, nil
}
return key.Bool()
}

func parseSocks5Config(section *ini.Section) (RoutineSpawner, error) {
config := &Socks5Config{}

Expand All @@ -436,6 +478,18 @@ func parseSocks5Config(section *ini.Section) (RoutineSpawner, error) {
password, _ := parseString(section, "Password")
config.Password = password

tunnelDomains, err := parseRegexList(section, "TunnelDomains")
if err != nil {
return nil, err
}
config.TunnelDomains = tunnelDomains

logDomains, err := parseBoolKey(section, "LogDomains")
if err != nil {
return nil, err
}
config.LogDomains = logDomains

return config, nil
}

Expand All @@ -448,6 +502,18 @@ func parseSNIConfig(section *ini.Section) (RoutineSpawner, error) {
}
config.BindAddress = bindAddress

tunnelDomains, err := parseRegexList(section, "TunnelDomains")
if err != nil {
return nil, err
}
config.TunnelDomains = tunnelDomains

logDomains, err := parseBoolKey(section, "LogDomains")
if err != nil {
return nil, err
}
config.LogDomains = logDomains

return config, nil
}

Expand All @@ -472,6 +538,18 @@ func parseHTTPConfig(section *ini.Section) (RoutineSpawner, error) {
keyFile, _ := parseString(section, "KeyFile")
config.KeyFile = keyFile

tunnelDomains, err := parseRegexList(section, "TunnelDomains")
if err != nil {
return nil, err
}
config.TunnelDomains = tunnelDomains

logDomains, err := parseBoolKey(section, "LogDomains")
if err != nil {
return nil, err
}
config.LogDomains = logDomains

return config, nil
}

Expand Down
63 changes: 63 additions & 0 deletions router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package wireproxy

import (
"log"
"net"
"regexp"
"strings"
)

// DomainRouter decides whether a connection to a given host is routed through
// the WireGuard tunnel (host matches the whitelist) or dialed directly.
//
// An empty pattern list means "tunnel everything", preserving the legacy
// behaviour where every proxied connection goes through the tunnel.
type DomainRouter struct {
patterns []*regexp.Regexp
logNames bool
}

// NewDomainRouter builds a DomainRouter from compiled regex patterns. When
// logNames is true, every routing decision is logged so users can discover
// which domains their applications reach (and thus what to whitelist).
func NewDomainRouter(patterns []*regexp.Regexp, logNames bool) *DomainRouter {
return &DomainRouter{patterns: patterns, logNames: logNames}
}

// shouldTunnel reports whether host should be routed through the tunnel.
func (r *DomainRouter) shouldTunnel(host string) bool {
if r == nil || len(r.patterns) == 0 {
return true
}
host = strings.ToLower(strings.TrimSuffix(host, "."))
for _, p := range r.patterns {
if p.MatchString(host) {
return true
}
}
return false
}

// route decides how to dial host and, when logging is enabled, records the
// decision. It returns true when the connection should use the tunnel.
func (r *DomainRouter) route(host string) bool {
tunnel := r.shouldTunnel(host)
if r != nil && r.logNames {
dest := "DIRECT"
if tunnel {
dest = "TUNNEL"
}
log.Printf("route: %s -> %s\n", host, dest)
}
return tunnel
}

// hostFromAddr extracts the host portion from a "host:port" address, falling
// back to the whole string when there is no port.
func hostFromAddr(address string) string {
host, _, err := net.SplitHostPort(address)
if err != nil {
return address
}
return host
}
Loading