diff --git a/internal/provider/local_cloudflare.go b/internal/provider/local_cloudflare.go index a033a989..78e7f552 100644 --- a/internal/provider/local_cloudflare.go +++ b/internal/provider/local_cloudflare.go @@ -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 diff --git a/internal/provider/protocol/local.go b/internal/provider/protocol/local_auto.go similarity index 78% rename from internal/provider/protocol/local.go rename to internal/provider/protocol/local_auto.go index 5191eec3..b1430268 100644 --- a/internal/provider/protocol/local.go +++ b/internal/provider/protocol/local_auto.go @@ -9,11 +9,11 @@ import ( "github.com/favonia/cloudflare-ddns/internal/pp" ) -// Local detects the IP address by pretending to send out an UDP packet +// 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 Local struct { +type LocalAuto struct { // Name of the detection protocol. ProviderName string @@ -22,25 +22,23 @@ type Local struct { } // Name of the detection protocol. -func (p Local) Name() string { +func (p LocalAuto) Name() string { return p.ProviderName } // GetIP detects the IP address by pretending to send an UDP packet. // (No actual UDP packets will be sent out.) -func (p Local) GetIP(_ context.Context, ppfmt pp.PP, ipNet ipnet.Type) (netip.Addr, Method, bool) { - var invalidIP netip.Addr - +func (p LocalAuto) GetIP(_ context.Context, ppfmt pp.PP, ipNet ipnet.Type) (netip.Addr, Method, bool) { remoteUDPAddr, found := p.RemoteUDPAddr[ipNet] if !found { ppfmt.Noticef(pp.EmojiImpossible, "Unhandled IP network: %s", ipNet.Describe()) - return invalidIP, MethodUnspecified, false + return netip.Addr{}, MethodUnspecified, false } conn, err := net.Dial(ipNet.UDPNetwork(), remoteUDPAddr) if err != nil { ppfmt.Noticef(pp.EmojiError, "Failed to detect a local %s address: %v", ipNet.Describe(), err) - return invalidIP, MethodUnspecified, false + return netip.Addr{}, MethodUnspecified, false } defer conn.Close() diff --git a/internal/provider/protocol/local_test.go b/internal/provider/protocol/local_auto_test.go similarity index 98% rename from internal/provider/protocol/local_test.go rename to internal/provider/protocol/local_auto_test.go index e3a04135..993a5b40 100644 --- a/internal/provider/protocol/local_test.go +++ b/internal/provider/protocol/local_auto_test.go @@ -17,7 +17,7 @@ import ( func TestLocalName(t *testing.T) { t.Parallel() - p := &protocol.Local{ + p := &protocol.LocalAuto{ ProviderName: "very secret name", RemoteUDPAddr: nil, } @@ -102,7 +102,7 @@ func TestLocalGetIP(t *testing.T) { t.Parallel() mockCtrl := gomock.NewController(t) - provider := &protocol.Local{ + provider := &protocol.LocalAuto{ ProviderName: "", RemoteUDPAddr: map[ipnet.Type]string{ tc.addrKey: tc.addr, diff --git a/internal/provider/protocol/local_iface.go b/internal/provider/protocol/local_iface.go new file mode 100644 index 00000000..d9e441c4 --- /dev/null +++ b/internal/provider/protocol/local_iface.go @@ -0,0 +1,55 @@ +package protocol + +import ( + "context" + "net" + "net/netip" + + "github.com/favonia/cloudflare-ddns/internal/ipnet" + "github.com/favonia/cloudflare-ddns/internal/pp" +) + +// LocalWithInterface detects the IP address by choosing the first "good" IP +// address assigned to a network interface. +type LocalWithInterface struct { + // Name of the detection protocol. + ProviderName string + + // The name of the network interface + InterfaceName string +} + +// Name of the detection protocol. +func (p LocalWithInterface) Name() string { + return p.ProviderName +} + +// GetIP detects the IP address by pretending to send an UDP packet. +// (No actual UDP packets will be sent out.) +func (p LocalWithInterface) GetIP(_ context.Context, ppfmt pp.PP, ipNet ipnet.Type) (netip.Addr, Method, bool) { + iface, err := net.InterfaceByName(p.InterfaceName) + if err != nil { + ppfmt.Noticef(pp.EmojiUserError, "Failed to find an interface named %q: %v", p.InterfaceName, err) + return netip.Addr{}, MethodUnspecified, false + } + + addrs, err := iface.Addrs() + if err != nil { + ppfmt.Noticef(pp.EmojiUserError, "Failed to list addresses of %q: %v", p.InterfaceName, err) + return netip.Addr{}, MethodUnspecified, false + } + + for _, addr := range addrs { + ip, err := netip.ParseAddr(addr.String()) + if err != nil { + ppfmt.Noticef(pp.EmojiUserError, "Failed to parse address %q of %q: %v", addr.String(), p.InterfaceName, err) + return netip.Addr{}, MethodUnspecified, false + } + + if ipNet.Matches(ip) && ip.IsGlobalUnicast() { + return ip, MethodUnspecified, true + } + } + + return netip.Addr{}, MethodUnspecified, false +}