Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Added a possibility to use a private IP instead of a public IP #839

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
5 changes: 4 additions & 1 deletion cmd/ddns-updater/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,11 @@ func _main(ctx context.Context, reader *reader.Reader, args []string, logger log
Enabled: *config.PubIP.DNSEnabled,
Options: config.PubIP.ToDNSPOptions(),
}
privateIPSettings := publicip.PrivateIPSettings{
Enabled: *config.PubIP.PrivateIPEnabled,
}

ipGetter, err := publicip.NewFetcher(dnsSettings, httpSettings)
ipGetter, err := publicip.NewFetcher(dnsSettings, httpSettings, privateIPSettings)
if err != nil {
return err
}
Expand Down
29 changes: 23 additions & 6 deletions internal/config/pubip.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/qdm12/ddns-updater/pkg/publicip/dns"
"github.com/qdm12/ddns-updater/pkg/publicip/http"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
"github.com/qdm12/ddns-updater/pkg/publicip/privateip"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
Expand All @@ -24,6 +25,7 @@ type PubIP struct {
DNSEnabled *bool
DNSProviders []string
DNSTimeout time.Duration
PrivateIPEnabled *bool
}

func (p *PubIP) setDefaults() {
Expand All @@ -35,6 +37,7 @@ func (p *PubIP) setDefaults() {
p.DNSProviders = gosettings.DefaultSlice(p.DNSProviders, []string{all})
const defaultDNSTimeout = 3 * time.Second
p.DNSTimeout = gosettings.DefaultComparable(p.DNSTimeout, defaultDNSTimeout)
p.PrivateIPEnabled = gosettings.DefaultPointer(p.PrivateIPEnabled, true)
}

func (p PubIP) Validate() (err error) {
Expand Down Expand Up @@ -95,6 +98,8 @@ func (p *PubIP) toLinesNode() (node *gotree.Node) {
}
}

node.Appendf("Private IP enabled: %s", gosettings.BoolToYesNo(p.PrivateIPEnabled))

return node
}

Expand All @@ -110,6 +115,13 @@ func (p *PubIP) ToHTTPOptions() (options []http.Option) {
}
}

// ToPrivateIPSettings assumes the settings have been validated.
func (p *PubIP) ToPrivateIPSettings() privateip.Settings {
return privateip.Settings{
Enabled: *p.PrivateIPEnabled,
}
}

func stringsToHTTPProviders(providers []string, ipVersion ipversion.IPVersion) (
updatedProviders []http.Provider) {
updatedProvidersSet := make(map[string]struct{}, len(providers))
Expand Down Expand Up @@ -234,11 +246,13 @@ func validateHTTPIPProviders(providerStrings []string,
}

func (p *PubIP) read(r *reader.Reader, warner Warner) (err error) {
p.HTTPEnabled, p.DNSEnabled, err = getFetchers(r)
p.HTTPEnabled, p.DNSEnabled, p.PrivateIPEnabled, err = getFetchers(r)
if err != nil {
return err
}

p.PrivateIPEnabled = gosettings.DefaultPointer(p.PrivateIPEnabled, false)

p.HTTPIPProviders = r.CSV("PUBLICIP_HTTP_PROVIDERS",
reader.RetroKeys("IP_METHOD"))
p.HTTPIPv4Providers = r.CSV("PUBLICIPV4_HTTP_PROVIDERS",
Expand Down Expand Up @@ -297,32 +311,35 @@ func (p *PubIP) read(r *reader.Reader, warner Warner) (err error) {

var ErrFetcherNotValid = errors.New("fetcher is not valid")

func getFetchers(reader *reader.Reader) (http, dns *bool, err error) {
func getFetchers(reader *reader.Reader) (http, dns, privateIP *bool, err error) {
// TODO change to use reader.BoolPtr with retro-compatibility
s := reader.String("PUBLICIP_FETCHERS")
if s == "" {
return nil, nil, nil
return nil, nil, nil, nil
}

http, dns = new(bool), new(bool)
http, dns, privateIP = new(bool), new(bool), new(bool)
fields := strings.Split(s, ",")
for i, field := range fields {
switch strings.ToLower(field) {
case "all":
*http = true
*dns = true
*privateIP = true
case "http":
*http = true
case "dns":
*dns = true
case "privateip":
*privateIP = true
default:
return nil, nil, fmt.Errorf(
return nil, nil, nil, fmt.Errorf(
"%w: %q at position %d of %d",
ErrFetcherNotValid, field, i+1, len(fields))
}
}

return http, dns, nil
return http, dns, privateIP, nil
}

func handleRetroProvider(provider string) (updatedProvider string) {
Expand Down
5 changes: 3 additions & 2 deletions internal/config/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ func Test_Settings_String(t *testing.T) {
| | └── all
| ├── DNS enabled: yes
| ├── DNS timeout: 3s
| └── DNS over TLS providers
| └── all
| ├── DNS over TLS providers
| | └── all
| └── Private IP enabled: yes
├── Resolver: use Go default resolver
├── Server
| ├── Listening address: :8000
Expand Down
22 changes: 22 additions & 0 deletions pkg/publicip/privateip/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package privateip

import (
"errors"
)

type Options struct{}

var (
ErrInvalidOption = errors.New("invalid option for private IP retrieval")
)

// NewOptions returns default options for the private IP provider
func NewOptions() *Options {
return &Options{}
}

// Validate checks the options (for private IP, there might be no specific options to validate)
func (o *Options) Validate() error {
// No specific options to validate for private IP retrieval
return nil
}
33 changes: 33 additions & 0 deletions pkg/publicip/privateip/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package privateip

import (
"testing"
)

func TestNewOptions(t *testing.T) {
t.Parallel()

opts := NewOptions()
if opts == nil {
t.Fatalf("NewOptions() returned nil, expected non-nil *Options")
}
}

func TestOptions_Validate(t *testing.T) {
t.Parallel()

opts := NewOptions()
err := opts.Validate()
if err != nil {
t.Errorf("Options.Validate() returned error: %v, expected nil", err)
}
}

func TestErrInvalidOption(t *testing.T) {
t.Parallel()

expectedMessage := "invalid option for private IP retrieval"
if ErrInvalidOption.Error() != expectedMessage {
t.Errorf("ErrInvalidOption message = %q, want %q", ErrInvalidOption.Error(), expectedMessage)
}
}
129 changes: 129 additions & 0 deletions pkg/publicip/privateip/privateip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package privateip

import (
"context"
"fmt"
"net"
"net/netip"
)

// Settings structure for configuring the Private Fetcher.
type Settings struct {
Enabled bool
}

// InterfaceRetriever defines methods to retrieve network interfaces and their addresses.
type InterfaceRetriever interface {
Interfaces() ([]net.Interface, error)
Addrs(iface net.Interface) ([]net.Addr, error)
}

// RealInterfaceRetriever is the production implementation of InterfaceRetriever.
type RealInterfaceRetriever struct{}

// Interfaces retrieves the list of network interfaces.
func (rir RealInterfaceRetriever) Interfaces() ([]net.Interface, error) {
return net.Interfaces()
}

// Addrs retrieves the addresses for a given network interface.
func (rir RealInterfaceRetriever) Addrs(iface net.Interface) ([]net.Addr, error) {
return iface.Addrs()
}

// Fetcher struct to represent the private IP fetcher.
type Fetcher struct {
settings Settings
interfaceRetriever InterfaceRetriever
}

// New creates a new instance of the Private Fetcher with dependency injection.
func New(settings Settings, retriever InterfaceRetriever) (*Fetcher, error) {
if !settings.Enabled {
return nil, fmt.Errorf("private IP fetcher is disabled")
}

if retriever == nil {
return nil, fmt.Errorf("interface retriever cannot be nil")
}

return &Fetcher{
settings: settings,
interfaceRetriever: retriever,
}, nil
}

// IP fetches the private IP address of the machine.
func (f *Fetcher) IP(ctx context.Context) (netip.Addr, error) {
interfaces, err := f.interfaceRetriever.Interfaces()
if err != nil {
return netip.Addr{}, err
}

for _, iface := range interfaces {
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}

addrs, err := f.interfaceRetriever.Addrs(iface)
if err != nil {
return netip.Addr{}, err
}

for _, addr := range addrs {
switch v := addr.(type) {
case *net.IPNet:
if isPrivateIP(v.IP) {
privateAddr, err := netip.ParseAddr(v.IP.String())
if err != nil {
return netip.Addr{}, err
}
return privateAddr, nil
}
}
}
}

return netip.Addr{}, fmt.Errorf("no private IP address found")
}

// IP4 fetches the IPv4 address of the machine.
func (f *Fetcher) IP4(ctx context.Context) (netip.Addr, error) {
ip, err := f.IP(ctx)
if err != nil {
return netip.Addr{}, err
}
if ip.Is4() {
return ip, nil
}
return netip.Addr{}, fmt.Errorf("no IPv4 address found")
}

// IP6 fetches the IPv6 address of the machine.
func (f *Fetcher) IP6(ctx context.Context) (netip.Addr, error) {
ip, err := f.IP(ctx)
if err != nil {
return netip.Addr{}, err
}
if ip.Is6() {
return ip, nil
}
return netip.Addr{}, fmt.Errorf("no IPv6 address found")
}

// isPrivateIP checks if an IP is from a private range.
func isPrivateIP(ip net.IP) bool {
privateBlocks := []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
}

for _, block := range privateBlocks {
_, cidr, _ := net.ParseCIDR(block)
if cidr.Contains(ip) {
return true
}
}
return false
}
Loading