Skip to content

Commit

Permalink
feat(provider): support local:interface
Browse files Browse the repository at this point in the history
  • Loading branch information
favonia committed Sep 22, 2024
1 parent 2d95d69 commit f47fcf5
Show file tree
Hide file tree
Showing 16 changed files with 286 additions and 62 deletions.
2 changes: 2 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ linters:
- exportloopref # deprecated

- dupl # somewhat unpredictable, and never leads to actual code changes
- goconst # never leads to actual code changes
- mnd # never leads to actual code changes

- cyclop # can detect complicated code, but never leads to actual code changes
- funlen # can detect complicated code, but never leads to actual code changes
Expand Down
23 changes: 12 additions & 11 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -297,22 +297,23 @@ _(Click to expand the following items.)_
<details>
<summary>🔍 IP address providers</summary>

| Name | Meaning | Default Value |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ |
| `IP4_PROVIDER` | This specifies how to detect the current IPv4 address. Available providers include `cloudflare.doh`, `cloudflare.trace`, `local`, `url:<URL>`, and `none`. The special `none` provider disables IPv4 completely. See below for a detailed explanation. | `cloudflare.trace` |
| `IP6_PROVIDER` | This specifies how to detect the current IPv6 address. Available providers include `cloudflare.doh`, `cloudflare.trace`, `local`, `url:<URL>`, and `none`. The special `none` provider disables IPv6 completely. See below for a detailed explanation. | `cloudflare.trace` |
| Name | Meaning | Default Value |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `IP4_PROVIDER` | This specifies how to detect the current IPv4 address. Available providers include `cloudflare.doh`, `cloudflare.trace`, `local`, `local:<iface>`, `url:<URL>`, and `none`. The special `none` provider disables IPv4 completely. See below for a detailed explanation. | `cloudflare.trace` |
| `IP6_PROVIDER` | This specifies how to detect the current IPv6 address. Available providers include `cloudflare.doh`, `cloudflare.trace`, `local`, `local:<iface>`, `url:<URL>`, and `none`. The special `none` provider disables IPv6 completely. See below for a detailed explanation. | `cloudflare.trace` |

> 👉 The option `IP4_PROVIDER` governs `A`-type DNS records and IPv4 addresses in WAF lists, while the option `IP6_PROVIDER` governs `AAAA`-type DNS records and IPv6 addresses in WAF lists. The two options act independently of each other. You can specify different address providers for IPv4 and IPv6.

> 📡 Available IP address providers:
>
> | Provider Name | Explanation |
> | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
> | `cloudflare.doh` | Get the IP address by querying `whoami.cloudflare.` against [Cloudflare via DNS-over-HTTPS](https://developers.cloudflare.com/1.1.1.1/dns-over-https). 🤖 The updater will connect `1.1.1.1` for IPv4 and `2606:4700:4700::1111` for IPv6. Since version 1.9.3, the updater will switch to `1.0.0.1` for IPv4 if `1.1.1.1` appears to be blocked or intercepted by your ISP or your router (which is still not uncommon). Since version 1.14.0, the blockage detection uses a variant of [the Happy Eyeballs algorithm](https://en.wikipedia.org/wiki/Happy_Eyeballs) to reduce delay. |
> | `cloudflare.trace` | Get the IP address by parsing the [Cloudflare debugging page](https://one.one.one.one/cdn-cgi/trace). **This is the default provider.** 🤖 The updater will connect `1.1.1.1` for IPv4 and `2606:4700:4700::1111` for IPv6. Since version 1.9.3, the updater will switch to `1.0.0.1` for IPv4 if `1.1.1.1` appears to be blocked or intercepted by your ISP or your router (which is still not uncommon). Since version 1.14.0, the blockage detection uses a variant of [the Happy Eyeballs algorithm](https://en.wikipedia.org/wiki/Happy_Eyeballs) to reduce delay. |
> | `local` | Get the IP address via local network interfaces. When multiple local network interfaces or in general multiple IP addresses are present, the updater will use the address that _would have_ been used for outbound UDP connections to Cloudflare servers. (No data will be transmitted.) ⚠️ You need access to the host network (such as `network_mode: host` in Docker Compose) for this policy, for otherwise the updater will detect the addresses inside [the default bridge network in Docker](https://docs.docker.com/network/bridge/) instead of those in the host network. |
> | `url:<URL>` | Fetch the content at `URL` and treat it as the IP address. The provider format is `url:` followed by the URL. For example, `IP4_PROVIDER=url:https://api4.ipify.org` will fetch the IPv4 address from <https://api4.ipify.org>, a server maintained by [ipify](https://www.ipify.org). Note that the updater will only use IPv4 to connect to `URL` for fetching IPv4 addresses, and similarly only IPv6 for IPv6 addresses. Currently, only the HTTP(S) schema is supported. |
> | `none` | Stop the DNS updating for the specified IP version completely. For example `IP4_PROVIDER=none` will disable IPv4 completely. Existing DNS records will not be removed. ⚠️ The IP addresses of the disabled IP version will be removed from WAF lists; so `IP4_PROVIDER=none` will remove all IPv4 addresses from all managed WAF lists. 🧪 As the support of WAF lists is experimental, this behavior is subject to changes and please [provide feedback](https://github.com/favonia/cloudflare-ddns/issues/new). |
> | Provider Name | Explanation |
> | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
> | `cloudflare.doh` | Get the IP address by querying `whoami.cloudflare.` against [Cloudflare via DNS-over-HTTPS](https://developers.cloudflare.com/1.1.1.1/dns-over-https). 🤖 The updater will connect `1.1.1.1` for IPv4 and `2606:4700:4700::1111` for IPv6. Since version 1.9.3, the updater will switch to `1.0.0.1` for IPv4 if `1.1.1.1` appears to be blocked or intercepted by your ISP or your router (which is still not uncommon). Since version 1.14.0, the blockage detection uses a variant of [the Happy Eyeballs algorithm](https://en.wikipedia.org/wiki/Happy_Eyeballs) to reduce delay. |
> | `cloudflare.trace` | Get the IP address by parsing the [Cloudflare debugging page](https://one.one.one.one/cdn-cgi/trace). **This is the default provider.** 🤖 The updater will connect `1.1.1.1` for IPv4 and `2606:4700:4700::1111` for IPv6. Since version 1.9.3, the updater will switch to `1.0.0.1` for IPv4 if `1.1.1.1` appears to be blocked or intercepted by your ISP or your router (which is still not uncommon). Since version 1.14.0, the blockage detection uses a variant of [the Happy Eyeballs algorithm](https://en.wikipedia.org/wiki/Happy_Eyeballs) to reduce delay. |
> | `local` | Get the IP address via local network interfaces. When multiple local network interfaces or in general multiple IP addresses are present, the updater will use the address that _would have_ been used for outbound UDP connections to Cloudflare servers. (No data will be transmitted.) ⚠️ The updater needs access to the host network (such as `network_mode: host` in Docker Compose) for this provider, for otherwise the updater will detect the addresses inside [the default bridge network in Docker](https://docs.docker.com/network/bridge/) instead of those in the host network. |
> | `local:<iface>` | Get the IP address via the specific local network interface `iface`. When multiple IP address are assigned to the interface `iface`, the updater will choose the first global unicast IP address of the matching IP family (IPv4 or IPv6), or the first link-local unicast IP address of the matching family if it cannot find any global one. ⚠️ The updater needs access to the host network (such as `network_mode: host` in Docker Compose) for this provider, for otherwise the updater will detect the addresses inside [the default bridge network in Docker](https://docs.docker.com/network/bridge/) instead of those in the host network. |
> | `url:<URL>` | Fetch the content at `URL` and treat it as the IP address. The provider format is `url:` followed by the URL. For example, `IP4_PROVIDER=url:https://api4.ipify.org` will fetch the IPv4 address from <https://api4.ipify.org>, a server maintained by [ipify](https://www.ipify.org). Note that the updater will only use IPv4 to connect to `URL` for fetching IPv4 addresses, and similarly only IPv6 for IPv6 addresses. Currently, only the HTTP(S) schema is supported. |
> | `none` | Stop the DNS updating for the specified IP version completely. For example `IP4_PROVIDER=none` will disable IPv4 completely. Existing DNS records will not be removed. ⚠️ The IP addresses of the disabled IP version will be removed from WAF lists; so `IP4_PROVIDER=none` will remove all IPv4 addresses from all managed WAF lists. 🧪 As the support of WAF lists is experimental, this behavior is subject to changes and please [provide feedback](https://github.com/favonia/cloudflare-ddns/issues/new). |

</details>

Expand Down
4 changes: 2 additions & 2 deletions internal/api/cloudflare_waf.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import (
// - An IPv6 CIDR ranges with a prefix from /4 to /64
// For this updater, only the maximum values matter.
var WAFListMaxBitLen = map[ipnet.Type]int{ //nolint:gochecknoglobals
ipnet.IP4: 32, //nolint:mnd
ipnet.IP6: 64, //nolint:mnd
ipnet.IP4: 32,
ipnet.IP6: 64,
}

func hintWAFListPermission(ppfmt pp.PP, err error) {
Expand Down
6 changes: 3 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ func Default() *Config {
UpdateCron: cron.MustNew("@every 5m"),
UpdateOnStart: true,
DeleteOnStop: false,
CacheExpiration: time.Hour * 6, //nolint:mnd
CacheExpiration: time.Hour * 6,
TTL: api.TTLAuto,
ProxiedTemplate: "false",
Proxied: map[domain.Domain]bool{},
RecordComment: "",
WAFListDescription: "",
DetectionTimeout: time.Second * 5, //nolint:mnd
UpdateTimeout: time.Second * 30, //nolint:mnd
DetectionTimeout: time.Second * 5,
UpdateTimeout: time.Second * 30,
Monitor: monitor.NewComposed(),
Notifier: notifier.NewComposed(),
}
Expand Down
45 changes: 29 additions & 16 deletions internal/config/env_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,47 +83,60 @@ func ReadProvider(ppfmt pp.PP, key, keyDeprecated string, field *provider.Provid
return false
}

switch val {
case "cloudflare":
parts := strings.SplitN(val, ":", 2) // len(parts) >= 1 because val is not empty
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}

switch {
case len(parts) == 1 && parts[0] == "cloudflare":
ppfmt.Noticef(
pp.EmojiUserError,
`%s=cloudflare is invalid; use %s=cloudflare.trace or %s=cloudflare.doh`,
key, key, key,
)
return false
case "cloudflare.trace":
case len(parts) == 1 && parts[0] == "cloudflare.trace":
*field = provider.NewCloudflareTrace()
return true
case "cloudflare.doh":
case len(parts) == 1 && parts[0] == "cloudflare.doh":
*field = provider.NewCloudflareDOH()
return true
case "ipify":
case len(parts) == 1 && parts[0] == "ipify":
ppfmt.Noticef(
pp.EmojiUserWarning,
`%s=ipify is deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`,
key, key, key,
)
*field = provider.NewIpify()
return true
case "local":
case len(parts) == 1 && parts[0] == "local":
*field = provider.NewLocal()
return true
case "none":
*field = nil
case len(parts) == 2 && parts[0] == "local":
if parts[1] == "" {
ppfmt.Noticef(
pp.EmojiUserError,
`%s=local: must be followed by a network interface name`,
key,
)
return false

Check warning on line 123 in internal/config/env_provider.go

View check run for this annotation

Codecov / codecov/patch

internal/config/env_provider.go#L118-L123

Added lines #L118 - L123 were not covered by tests
}
*field = provider.NewLocalWithInterface(parts[1])
return true
}

if strings.HasPrefix(val, "url:") {
url := strings.TrimSpace(strings.TrimPrefix(val, "url:"))
p, ok := provider.NewCustomURL(ppfmt, url)
case len(parts) == 2 && parts[0] == "url":
p, ok := provider.NewCustomURL(ppfmt, parts[1])
if ok {
*field = p
}
return ok
case len(parts) == 1 && parts[0] == "none":
*field = nil
return true
default:
ppfmt.Noticef(pp.EmojiUserError, "%s (%q) is not a valid provider", key, val)
return false
}

ppfmt.Noticef(pp.EmojiUserError, "%s (%q) is not a valid provider", key, val)
return false
}

// ReadProviderMap reads the environment variables IP4_PROVIDER and IP6_PROVIDER,
Expand Down
14 changes: 8 additions & 6 deletions internal/config/env_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ func TestReadProvider(t *testing.T) {
keyDeprecated := keyPrefix + "DEPRECATED"

var (
none provider.Provider
doh = provider.NewCloudflareDOH()
trace = provider.NewCloudflareTrace()
local = provider.NewLocal()
ipify = provider.NewIpify()
custom = provider.MustNewCustomURL("https://url.io")
none provider.Provider
doh = provider.NewCloudflareDOH()
trace = provider.NewCloudflareTrace()
local = provider.NewLocal()
localLoopback = provider.NewLocalWithInterface("lo")
ipify = provider.NewIpify()
custom = provider.MustNewCustomURL("https://url.io")
)

for name, tc := range map[string]struct {
Expand Down Expand Up @@ -154,6 +155,7 @@ func TestReadProvider(t *testing.T) {
"cloudflare.doh": {true, " \tcloudflare.doh ", false, "", none, doh, true, nil},
"none": {true, " none ", false, "", trace, none, true, nil},
"local": {true, " local ", false, "", trace, local, true, nil},
"local:lo": {true, " local : lo ", false, "", trace, localLoopback, true, nil},
"custom": {true, " url:https://url.io ", false, "", trace, custom, true, nil},
"ipify": {
true, " ipify ", false, "", trace, ipify, true,
Expand Down
4 changes: 2 additions & 2 deletions internal/config/env_waf.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ func ReadAndAppendWAFListNames(ppfmt pp.PP, key string, field *[]api.WAFList) bo
for _, val := range vals {
var list api.WAFList

parts := strings.SplitN(val, "/", 2) //nolint:mnd
if len(parts) != 2 { //nolint:mnd
parts := strings.SplitN(val, "/", 2)
if len(parts) != 2 {
ppfmt.Noticef(pp.EmojiUserError, `List %q should be in format "account-id/list-name"`, val)
return false
}
Expand Down
2 changes: 1 addition & 1 deletion internal/pp/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func EnglishJoin(items []string) string {
return "(none)"
case 1:
return items[0]
case 2: //nolint:mnd
case 2:
return fmt.Sprintf("%s and %s", items[0], items[1])
default:
return fmt.Sprintf("%s, and %s", strings.Join(items[:l-1], ", "), items[l-1])
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/local_cloudflare.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
// NewLocal creates a specialized Local provider that uses Cloudflare as the remote server.
// (No actual UDP packets will be sent to Cloudflare.)
func NewLocal() Provider {
return protocol.Local{
return protocol.LocalAuto{
ProviderName: "local",
RemoteUDPAddr: map[ipnet.Type]string{
// 1.0.0.1 is used in case 1.1.1.1 is hijacked by the router
Expand Down
11 changes: 11 additions & 0 deletions internal/provider/local_iface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package provider

import "github.com/favonia/cloudflare-ddns/internal/provider/protocol"

// NewLocalWithInterface creates a protocol.LocalWithInterface provider.
func NewLocalWithInterface(iface string) Provider {
return protocol.LocalWithInterface{
ProviderName: "local:" + iface,
InterfaceName: iface,
}
}
Loading

0 comments on commit f47fcf5

Please sign in to comment.