Skip to content
Draft
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
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
- [Miscellaneous topics](#miscellaneous-topics)
- [IPv6 support](#ipv6-support)
- [Network interface IP address](#network-interface-ip-address)
- [Query IP through specific network interface](#query-ip-through-specific-network-interface)
- [SOCKS5 proxy support](#socks5-proxy-support)
- [Display debug info](#display-debug-info)
- [Obtain IP from RouterOS](#obtain-ip-from-router-os)
Expand Down Expand Up @@ -1190,6 +1191,32 @@ With `interface-name` replaced by the name of the network interface, e.g. `eth0`

Note: If `ip_urls` is also specified, it will be used to perform an online lookup first and the network interface IP will be used as a fallback in case of failure.

#### Query IP through specific network interface

If you have multiple network interfaces and want to query your public IP address through a specific interface (useful when you have multiple uplinks, some behind CGNAT and others with public IPs), you can use the `query_interface` configuration option:

```json
"query_interface": "wan0",
```

With `wan0` replaced by the name of the network interface you want to use for querying the IP address. When this option is set, GoDNS will bind the HTTP request to the specified network interface, ensuring the IP query goes through that interface rather than the default route.

This is particularly useful in scenarios where:
- You have multiple WAN connections with different routing priorities
- One interface is behind CGNAT while another has a public IP
- You want to ensure the IP query reflects the actual public IP of a specific interface

Example configuration for dual-WAN setup:
```json
{
"ip_urls": ["https://api.ipify.org/"],
"query_interface": "wan0",
"ip_type": "IPv4"
}
```

Note: The `query_interface` option is different from `ip_interface`. The `query_interface` specifies which interface to use when making the HTTP request to query your public IP, while `ip_interface` reads the IP address directly from the local interface without making any external requests.

#### SOCKS5 proxy support

You can make all remote calls go through a [SOCKS5 proxy](https://en.wikipedia.org/wiki/SOCKS#SOCKS5) by specifying it in the configuration file this way:
Expand Down
27 changes: 27 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
- [杂项主题](#杂项主题)
- [IPv6 支持](#ipv6-支持)
- [网络接口 IP 地址](#网络接口-ip-地址)
- [通过特定网络接口查询 IP](#通过特定网络接口查询-ip)
- [SOCKS5 代理支持](#socks5-代理支持)
- [显示调试信息](#显示调试信息)
- [从 RouterOS 获取 IP](#从-routeros-获取-ip)
Expand Down Expand Up @@ -1188,6 +1189,32 @@ http://localhost:5000/api/v1/send?domain=ddns.example.com&ip=192.168.1.1&ip_type

注意:如果也指定了 `ip_urls`,它将首先用于执行在线查找,网络接口 IP 将在失败情况下用作后备。

#### 通过特定网络接口查询 IP

如果您有多个网络接口,并希望通过特定接口查询公共 IP 地址(在有多个上行链路时很有用,其中一些在 CGNAT 后面,而其他的有公共 IP),您可以使用 `query_interface` 配置选项:

```json
"query_interface": "wan0",
```

将 `wan0` 替换为要用于查询 IP 地址的网络接口名称。设置此选项后,GoDNS 将 HTTP 请求绑定到指定的网络接口,确保 IP 查询通过该接口而不是默认路由。

这在以下场景中特别有用:
- 您有多个具有不同路由优先级的 WAN 连接
- 一个接口在 CGNAT 后面,而另一个有公共 IP
- 您想确保 IP 查询反映特定接口的实际公共 IP

双 WAN 设置的示例配置:
```json
{
"ip_urls": ["https://api.ipify.org/"],
"query_interface": "wan0",
"ip_type": "IPv4"
}
```

注意:`query_interface` 选项与 `ip_interface` 不同。`query_interface` 指定进行 HTTP 请求查询公共 IP 时使用哪个接口,而 `ip_interface` 直接从本地接口读取 IP 地址,不进行任何外部请求。

#### SOCKS5 代理支持

您可以通过在配置文件中指定 [SOCKS5 代理](https://en.wikipedia.org/wiki/SOCKS#SOCKS5) 来使所有远程调用通过该代理:
Expand Down
1 change: 1 addition & 0 deletions configs/config_multi_sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ interval: 300
resolver: 8.8.8.8
user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36"
ip_interface: eth0
query_interface:

mikrotik:
enabled: false
Expand Down
1 change: 1 addition & 0 deletions configs/config_sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"resolver": "8.8.8.8",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36",
"ip_interface": "eth0",
"query_interface": "",
"mikrotik": {
"enabled": false,
"addr": "http://192.168.88.1:81",
Expand Down
1 change: 1 addition & 0 deletions configs/config_sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interval: 300
resolver: 8.8.8.8
user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36"
ip_interface: eth0
query_interface:
mikrotik:
enabled: false
addr: "http://192.168.20.1:81"
Expand Down
15 changes: 8 additions & 7 deletions internal/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,14 @@ type Settings struct {
Domains []Domain `json:"domains" yaml:"domains"`

// Network and IP configuration
IPUrl string `json:"ip_url" yaml:"ip_url"`
IPUrls []string `json:"ip_urls" yaml:"ip_urls"`
IPV6Url string `json:"ipv6_url" yaml:"ipv6_url"`
IPV6Urls []string `json:"ipv6_urls" yaml:"ipv6_urls"`
IPInterface string `json:"ip_interface" yaml:"ip_interface"`
IPType string `json:"ip_type" yaml:"ip_type"`
Resolver string `json:"resolver" yaml:"resolver"`
IPUrl string `json:"ip_url" yaml:"ip_url"`
IPUrls []string `json:"ip_urls" yaml:"ip_urls"`
IPV6Url string `json:"ipv6_url" yaml:"ipv6_url"`
IPV6Urls []string `json:"ipv6_urls" yaml:"ipv6_urls"`
IPInterface string `json:"ip_interface" yaml:"ip_interface"`
QueryInterface string `json:"query_interface" yaml:"query_interface"`
IPType string `json:"ip_type" yaml:"ip_type"`
Resolver string `json:"resolver" yaml:"resolver"`

// Application configuration
Interval int `json:"interval" yaml:"interval"`
Expand Down
24 changes: 24 additions & 0 deletions internal/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,27 @@ func TestLoadWithEnvPath(t *testing.T) {

t.Log(settings)
}

func TestQueryInterfaceField(t *testing.T) {
// Test JSON config
var jsonSettings Settings
err := LoadSettings("../../configs/config_sample.json", &jsonSettings)
if err != nil {
t.Fatal(err.Error())
}

// Query interface field should exist (even if empty)
_ = jsonSettings.QueryInterface

// Test YAML config
var yamlSettings Settings
err = LoadSettings("../../configs/config_sample.yaml", &yamlSettings)
if err != nil {
t.Fatal(err.Error())
}

// Query interface field should exist (even if empty)
_ = yamlSettings.QueryInterface

t.Log("query_interface field loaded successfully")
}
63 changes: 61 additions & 2 deletions pkg/lib/ip_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,69 @@ func (helper *IPHelper) getIPOnline() string {
proto = "tcp4"
}

return (&net.Dialer{
dialer := &net.Dialer{
Timeout: time.Second * utils.DefaultTimeout,
KeepAlive: 30 * time.Second,
}).DialContext(ctx, proto, addr)
}

// Bind to specific network interface if configured
if helper.configuration.QueryInterface != "" {
iface, err := net.InterfaceByName(helper.configuration.QueryInterface)
if err != nil {
log.Errorf("Failed to find query interface %s: %v", helper.configuration.QueryInterface, err)
return nil, err
}

addrs, err := iface.Addrs()
if err != nil {
log.Errorf("Failed to get addresses for query interface %s: %v", helper.configuration.QueryInterface, err)
return nil, err
}

// Find a suitable local address from the interface
var localAddr net.IP
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}

if ip == nil {
continue
}

// Match IP version with protocol
if proto == "tcp4" && ip.To4() != nil {
localAddr = ip
break
} else if proto == "tcp6" && ip.To4() == nil {
localAddr = ip
break
} else if proto == "tcp" {
// For generic tcp, prefer IPv4
if ip.To4() != nil {
localAddr = ip
break
}
if localAddr == nil {
localAddr = ip
}
}
}

if localAddr == nil {
log.Errorf("No suitable address found on query interface %s for protocol %s", helper.configuration.QueryInterface, proto)
return nil, errors.New("no suitable address on interface")
}

dialer.LocalAddr = &net.TCPAddr{IP: localAddr}
log.Debugf("Binding query to interface %s with local address %s", helper.configuration.QueryInterface, localAddr)
}

return dialer.DialContext(ctx, proto, addr)
},
}

Expand Down