mirror of
https://github.com/m13253/dns-over-https.git
synced 2026-03-31 16:15:40 +00:00
feat: add option to bind outgoing connections to a specific interface
This adds a new `interface` configuration option to `doh-client` that allows users to specify a network interface for all outgoing DNS queries (including bootstrap and passthrough traffic).
This commit is contained in:
@@ -90,6 +90,29 @@ func NewClient(conf *config.Config) (c *Client, err error) {
|
||||
Net: "tcp",
|
||||
Timeout: time.Duration(conf.Other.Timeout) * time.Second,
|
||||
}
|
||||
|
||||
if c.conf.Other.Interface != "" {
|
||||
// Setup UDP Dialer
|
||||
udpLocalAddr, err := c.bindToInterface("udp")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to bind passthrough UDP to interface %s: %v", c.conf.Other.Interface, err)
|
||||
}
|
||||
c.udpClient.Dialer = &net.Dialer{
|
||||
Timeout: time.Duration(conf.Other.Timeout) * time.Second,
|
||||
LocalAddr: udpLocalAddr,
|
||||
}
|
||||
|
||||
// Setup TCP Dialer
|
||||
tcpLocalAddr, err := c.bindToInterface("tcp")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to bind passthrough TCP to interface %s: %v", c.conf.Other.Interface, err)
|
||||
}
|
||||
c.tcpClient.Dialer = &net.Dialer{
|
||||
Timeout: time.Duration(conf.Other.Timeout) * time.Second,
|
||||
LocalAddr: tcpLocalAddr,
|
||||
}
|
||||
}
|
||||
|
||||
for _, addr := range conf.Listen {
|
||||
c.udpServers = append(c.udpServers, &dns.Server{
|
||||
Addr: addr,
|
||||
@@ -120,6 +143,14 @@ func NewClient(conf *config.Config) (c *Client, err error) {
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
if c.conf.Other.Interface != "" {
|
||||
localAddr, err := c.bindToInterface(network)
|
||||
if err != nil {
|
||||
log.Printf("Bootstrap dial warning: %v", err)
|
||||
} else {
|
||||
d.LocalAddr = localAddr
|
||||
}
|
||||
}
|
||||
numServers := len(c.bootstrap)
|
||||
bootstrap := c.bootstrap[rand.Intn(numServers)]
|
||||
conn, err := d.DialContext(ctx, network, bootstrap)
|
||||
@@ -241,6 +272,14 @@ func (c *Client) newHTTPClient() error {
|
||||
// DualStack: true,
|
||||
Resolver: c.bootstrapResolver,
|
||||
}
|
||||
if c.conf.Other.Interface != "" {
|
||||
localAddr, err := c.bindToInterface("tcp")
|
||||
if err != nil {
|
||||
log.Printf("Failed to resolve interface %s: %v", c.conf.Other.Interface, err)
|
||||
return err
|
||||
}
|
||||
dialer.LocalAddr = localAddr
|
||||
}
|
||||
c.httpTransport = &http.Transport{
|
||||
DialContext: dialer.DialContext,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
@@ -485,3 +524,50 @@ func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddre
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) bindToInterface(network string) (net.Addr, error) {
|
||||
if c.conf.Other.Interface == "" {
|
||||
return nil, nil
|
||||
}
|
||||
ifi, err := net.InterfaceByName(c.conf.Other.Interface)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs, err := ifi.Addrs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine if we need IPv4 or IPv6 based on the network string (e.g., "tcp4", "udp6")
|
||||
wantIPv6 := strings.Contains(network, "6")
|
||||
wantIPv4 := strings.Contains(network, "4") || !wantIPv6 // Default to 4 if not specified, or if generic "tcp"/"udp"
|
||||
|
||||
for _, addr := range addrs {
|
||||
ip, _, err := net.ParseCIDR(addr.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if we want IPv4 but got IPv6
|
||||
if ip.To4() == nil && wantIPv4 && !wantIPv6 {
|
||||
continue
|
||||
}
|
||||
// Skip if we want IPv6 but got IPv4
|
||||
if ip.To4() != nil && wantIPv6 {
|
||||
continue
|
||||
}
|
||||
// Skip IPv6 if disabled in config
|
||||
if ip.To4() == nil && c.conf.Other.NoIPv6 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Return the appropriate address type
|
||||
if strings.HasPrefix(network, "tcp") {
|
||||
return &net.TCPAddr{IP: ip}, nil
|
||||
}
|
||||
if strings.HasPrefix(network, "udp") {
|
||||
return &net.UDPAddr{IP: ip}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no suitable address found on interface %s for network %s", c.conf.Other.Interface, network)
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ type others struct {
|
||||
Bootstrap []string `toml:"bootstrap"`
|
||||
Passthrough []string `toml:"passthrough"`
|
||||
Timeout uint `toml:"timeout"`
|
||||
Interface string `toml:"interface"`
|
||||
NoCookies bool `toml:"no_cookies"`
|
||||
NoECS bool `toml:"no_ecs"`
|
||||
NoIPv6 bool `toml:"no_ipv6"`
|
||||
|
||||
@@ -97,6 +97,11 @@ passthrough = [
|
||||
# Timeout for upstream request in seconds
|
||||
timeout = 30
|
||||
|
||||
# Interface to bind to for outgoing connections.
|
||||
# If empty, the system default route is used (usually eth0 or wlan0).
|
||||
# Example: "eth1", "wlan0"
|
||||
interface = ""
|
||||
|
||||
# Disable HTTP Cookies
|
||||
#
|
||||
# Cookies may be useful if your upstream resolver is protected by some
|
||||
|
||||
Reference in New Issue
Block a user