Skip to content

Commit

Permalink
feat(provider): specify interface name (#941)
Browse files Browse the repository at this point in the history
  • Loading branch information
favonia authored Sep 24, 2024
1 parent 640d30b commit 69f8cf2
Show file tree
Hide file tree
Showing 17 changed files with 604 additions and 163 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
65 changes: 32 additions & 33 deletions README.markdown

Large diffs are not rendered by default.

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
}
*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
26 changes: 19 additions & 7 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,7 +155,18 @@ 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},
"custom": {true, " url:https://url.io ", false, "", trace, custom, true, nil},
"local:lo": {true, " local : lo ", false, "", trace, localLoopback, true, nil},
"local:": {
true, " local: ", false, "", trace, trace, false,
func(m *mocks.MockPP) {
m.EXPECT().Noticef(
pp.EmojiUserError,
`%s=local: must be followed by a network interface name`,
key,
)
},
},
"custom": {true, " url:https://url.io ", false, "", trace, custom, true, nil},
"ipify": {
true, " ipify ", false, "", trace, ipify, true,
func(m *mocks.MockPP) {
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
15 changes: 4 additions & 11 deletions internal/provider/local_cloudflare.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
package provider

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

// 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{
ProviderName: "local",
RemoteUDPAddr: map[ipnet.Type]string{
// 1.0.0.1 is used in case 1.1.1.1 is hijacked by the router
ipnet.IP4: "1.0.0.1:443",
ipnet.IP6: "[2606:4700:4700::1111]:443",
},
return protocol.LocalAuto{
ProviderName: "local",
RemoteUDPAddr: "api.cloudflare.com:443",
}
}
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,
}
}
51 changes: 0 additions & 51 deletions internal/provider/protocol/local.go

This file was deleted.

63 changes: 63 additions & 0 deletions internal/provider/protocol/local_auto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package protocol

import (
"context"
"net"
"net/netip"

"github.com/favonia/cloudflare-ddns/internal/ipnet"
"github.com/favonia/cloudflare-ddns/internal/pp"
)

// LocalAuto detects the IP address by pretending to send out an UDP packet
// and using the source IP address assigned by the system. In most cases
// it will detect the IP address of the network interface toward the internet.
// (No actual UDP packets will be sent out.)
type LocalAuto struct {
// Name of the detection protocol.
ProviderName string

// The target of the hypothetical UDP packet to be sent.
RemoteUDPAddr string
}

// Name of the detection protocol.
func (p LocalAuto) Name() string {
return p.ProviderName
}

// ExtractUDPAddr converts an address from [net.Interface.Addrs] to [netip.Addr].
// The address will be unmapped.
func ExtractUDPAddr(ppfmt pp.PP, addr net.Addr) (netip.Addr, bool) {
switch v := addr.(type) {
case *net.UDPAddr:
ip := v.AddrPort().Addr().Unmap()
if !ip.IsValid() {
ppfmt.Noticef(pp.EmojiImpossible, "Failed to parse UDP source address %q", v.IP.String())
return netip.Addr{}, false
}
return ip, ip.IsValid()
default:
ppfmt.Noticef(pp.EmojiImpossible, "Unexpected UDP source address data %q of type %T", addr.String(), addr)
return netip.Addr{}, false
}
}

// GetIP detects the IP address by pretending to send an UDP packet.
// (No actual UDP packets will be sent out.)
func (p LocalAuto) GetIP(_ context.Context, ppfmt pp.PP, ipNet ipnet.Type) (netip.Addr, Method, bool) {
conn, err := net.Dial(ipNet.UDPNetwork(), p.RemoteUDPAddr)
if err != nil {
ppfmt.Noticef(pp.EmojiError, "Failed to detect a local %s address: %v", ipNet.Describe(), err)
return netip.Addr{}, MethodUnspecified, false
}
defer conn.Close()

ip, ok := ExtractUDPAddr(ppfmt, conn.LocalAddr())
if !ok {
return netip.Addr{}, MethodUnspecified, false
}

normalizedIP, ok := ipNet.NormalizeDetectedIP(ppfmt, ip)
return normalizedIP, MethodPrimary, ok
}
Loading

0 comments on commit 69f8cf2

Please sign in to comment.