Documentation
¶
Overview ¶
Package ddns provides functions useful for updating Dynamic DNS records.
Usage will always start with ddns.New, which returns the DDNSClient implementation. New requires a domain name which will be updated and a Provider implementation for a DNS provider. Additional client configuration options are listed in the docs for New.
Index ¶
- func NewCloudflare(token string) func() (Provider, error)
- func RunDaemon(ddnsClient DDNSClient, ctx context.Context, interval time.Duration, ...)
- func UsingHTTPClient(httpclient *http.Client) clientOption
- func UsingResolver(resolver Resolver) clientOption
- func WithLogger(logger *log.Logger) clientOption
- type DDNSClient
- type Provider
- type Resolver
- type ResolverFunc
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewCloudflare ¶
NewCloudflare is used by ddns.New to create a new Provider for Cloudflare.
func RunDaemon ¶
func RunDaemon(ddnsClient DDNSClient, ctx context.Context, interval time.Duration, logger logf)
RunDaemon runs ddnsClient every interval.
Run errors are reported to logger. A nil logger indicates messages should be sent to the log package's default log.
To stop the daemon, cancel the given context.
The daemon will also exit early if it detects authentication or authorization errors, rather than continue running with an expired or invalid token.
Example ¶
package main import ( "context" "log" "os" "time" "github.com/Travis-Britz/ddns" ) func main() { ddnsClient, err := ddns.New("dynamic-local-ip.example.com", ddns.NewCloudflare(os.Getenv("CLOUDFLARE_ZONE_TOKEN")), ) if err != nil { log.Fatalf("error creating ddns client: %s", err) } // run every 5 minutes and stop after an hour: ctx, cancel := context.WithTimeout(context.Background(), 1*time.Hour) defer cancel() ddns.RunDaemon(ddnsClient, ctx, 5*time.Minute, nil) }
Output:
func UsingHTTPClient ¶
UsingHTTPClient configures the DDNSClient to use the given httpclient for requests made by the Provider and Resolver implementations supplied by this package, or for other types if they implement a SetHTTPClient method.
func UsingResolver ¶
func UsingResolver(resolver Resolver) clientOption
UsingResolver configures the client with a different resolver. The default resolver gets the IP addresses of the local network interfaces.
Available resolvers in this package: InterfaceResolver, WebResolver, FromString.
func WithLogger ¶
WithLogger configures the client with a logger for verbose logging.
The default logger discards verbose log messages.
Types ¶
type DDNSClient ¶
DDNSClient is the interface for updating Dynamic DNS records.
It is implemented by the client returned by ddns.New.
func New ¶
func New(domain string, providerFn providerFn, options ...clientOption) (DDNSClient, error)
New creates a new DDNSClient for domain using the given DNS provider. Additional options may be specified: UsingResolver, UsingHTTPClient, WithLogger.
Example ¶
package main import ( "context" "io" "log" "net/http" "os" "github.com/Travis-Britz/ddns" ) func main() { c, err := ddns.New( "dynamic-local-ip.example.com", ddns.NewCloudflare(os.Getenv("CLOUDFLARE_ZONE_TOKEN")), ddns.UsingResolver(ddns.InterfaceResolver("eth0")), ddns.WithLogger(log.New(io.Discard, "", 0)), ddns.UsingHTTPClient(http.DefaultClient), ) if err != nil { log.Fatalf("error creating ddns client: %s", err) } // run once: err = c.RunDDNS(context.Background()) if err != nil { log.Fatalf("ddns update failed: %s", err) } }
Output:
type Provider ¶
type Provider interface {
SetDNSRecords(ctx context.Context, domain string, records []netip.Addr) error
}
Provider is the interface for setting DNS records with a DNS provider.
Records may be IPv4 and IPv6 combined, and implementations should expect both even if they only use one.
The given records are the desired set for domain. It is up to implementations to track changes between calls.
type Resolver ¶
Resolver is the interface for looking up our IP addresses.
Results may be either IPv4 or IPv6, but should not include loopback interface addresses such as ::1.
A non-nil error may be returned with partial results.
func FromString ¶
FromString constructs a resolver that parses an IP from the string addr.
func InterfaceResolver ¶
InterfaceResolver constructs a resolver that returns the IP addresses reported by the given network interfaces. If no interfaces are provided then all interfaces will be used.
Example ¶
package main import ( "context" "log" "os" "github.com/Travis-Britz/ddns" ) func main() { resolver := ddns.InterfaceResolver("eth0", "wlan0") ddnsClient, err := ddns.New("dynamic-local-ip.example.com", ddns.NewCloudflare(os.Getenv("CLOUDFLARE_ZONE_TOKEN")), ddns.UsingResolver(resolver), ) if err != nil { log.Fatalf("error creating ddns client: %s", err) } // run once: err = ddnsClient.RunDDNS(context.Background()) if err != nil { log.Fatalf("ddns update failed: %s", err) } }
Output:
func Join ¶
Join constructs a resolver that combines the output of multiple resolvers into one.
This is useful in some instances such as when you want records for both IPv4 and IPv6, but can only get one or the other from a single web service request.
Example ¶
package main import ( "context" "log" "os" "github.com/Travis-Britz/ddns" ) func main() { r := ddns.Join( ddns.WebResolver("https://ipv4.icanhazip.com/"), ddns.WebResolver("https://ipv6.icanhazip.com/"), ) ddnsClient, err := ddns.New("dynamic-ip.example.com", ddns.NewCloudflare(os.Getenv("CLOUDFLARE_ZONE_TOKEN")), ddns.UsingResolver(r), ) if err != nil { log.Fatalf("error creating ddns client: %s", err) } // run once: err = ddnsClient.RunDDNS(context.Background()) if err != nil { log.Fatalf("ddns update failed: %s", err) } }
Output:
func WebResolver ¶
WebResolver constructs a resolver which uses external web services to look up a "public" IP address.
Each serviceURL must speak HTTP and return status "200 OK", with a valid IPv4 or IPv6 address as the first line of the response body. All other responses are considered an error.
If only one serviceURL is given, then the resolver will simply return the response. If multiple are given, then the resolver will request from up to three of them and only return successfully if the first two non-error responses agreed on the IP. No addresses will be returned if the web services did not agree on the IP address. This approach is taken due to the sensitive nature of public services having control over DNS records. It is recommended to run your own service over https instead when possible.
For clients which have both IPv4 and IPv6 capability, it is possible for one service to return IPv4 and another to return IPv6, causing matching to fail. There are at least two ways to ensure both responses use the same protocol version: supply a custom *http.Client (using ddns.WithHTTPClient) with a custom http.Transport which is configured to use IPv4/6, or simply use a public IP service endpoint that prefers one or the other, e.g. https://ipv4.icanhazip.com.
If you want both IPv4 and IPv6 DNS records set, then use one of the above approaches to ensure IPv4 and IPv6 respectively for each of two web resolvers and then use ddns.Join to combine their results.
The http.Client used to make requests can be configured in ddns.New's clientOptions with ddns.UsingHTTPClient.
Example ¶
package main import ( "context" "log" "os" "github.com/Travis-Britz/ddns" ) func main() { // I'm not vouching for these services, but they do return the IP of the client connection. // If possible, run your own and provide the URL here instead. r := ddns.WebResolver( "https://checkip.amazonaws.com/", "https://icanhazip.com/", // operated by Cloudflare since ~2021 "https://ipinfo.io/ip", ) ddnsClient, err := ddns.New( "dynamic-ip.example.com", ddns.NewCloudflare(os.Getenv("CLOUDFLARE_ZONE_TOKEN")), ddns.UsingResolver(r), ) if err != nil { log.Fatalf("error creating ddns client: %s", err) } // run once: err = ddnsClient.RunDDNS(context.Background()) if err != nil { log.Fatalf("ddns update failed: %s", err) } }
Output:
type ResolverFunc ¶
The ResolverFunc type is an adapter that allows the use of ordinary functions as resolvers.
Example ¶
package main import ( "context" "log" "net/netip" "os" "time" "github.com/Travis-Britz/ddns" ) func main() { fn := func(ctx context.Context) ([]netip.Addr, error) { select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(100 * time.Millisecond): // simulating some lookup method ip, err := netip.ParseAddr("10.0.0.10") return []netip.Addr{ip}, err } } ddnsClient, err := ddns.New("dynamic-ip.example.com", ddns.NewCloudflare(os.Getenv("CLOUDFLARE_ZONE_TOKEN")), ddns.UsingResolver(ddns.ResolverFunc(fn)), ) if err != nil { log.Fatalf("error creating ddns client: %s", err) } // run once: err = ddnsClient.RunDDNS(context.Background()) if err != nil { log.Fatalf("ddns update failed: %s", err) } }
Output: