ddns

package module
v0.0.0-...-dacb5da Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2023 License: MIT Imports: 14 Imported by: 0

README

This project was written specifically to run on the Raspberry Pi Zero W and update records with the local IP addresses assigned to the Pi. It likely works on any platform supported by Go, however.

ddns

Go Reference Go

ddns is a small Go library for dynamically updating DNS records.

Currently the only DNS provider included is Cloudflare, but the ddns.Provider interface is a single method if you would like to wrap your own provider's API.

package main

import (
	"context"
	"log"
	"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")),
	)
	if err != nil {
		log.Fatalf("error creating ddns client: %s", err)
	}
	ctx := context.Background()
	err = c.RunDDNS(ctx)
	if err != nil {
		log.Fatalf("dns update failed: %s", err)
	}
}

More examples: https://pkg.go.dev/github.com/Travis-Britz/ddns#pkg-examples

ddnscf

ddnscf is a small command line tool for dynamically updating Cloudflare DNS records.

Installation

Using go install:

go install github.com/Travis-Britz/ddns/cmd/ddnscf@latest

Other:

build ddns/cmd/ddnscf and then move the build binary to your preferred location (with execute permissions).

Once the program is in place, run it. It will prompt for a Cloudflare API token and then store it to a file. The token must have Zone.DNS:Edit permissions.

To skip the prompt you may create the key file in advance with the proper file permissions:

echo "MyVerySecretDNSToken" > ~/.cloudflare && chmod 600 ~/.cloudflare
Usage

ddnscf -h:

Usage of ddnscf:
-d string
        The domain name to update
-k string
        Path to cloudflare API credentials file (default "~/.cloudflare")
-ip string
        Set a specific IP address
-url string
        Use a public IP lookup URL
-if string
        Use a specific network interface
-i string
        Interval duration between runs (default 5m0s)
-once
        Run once and exit
-v
        Enable verbose logging
Examples

Update a domain with all of the local IPs assigned to the Pi:

ddnscf -v -d pi1.example.com

Update a domain with the IPs for a specific network interface:

ddnscf -v -d pi1.example.com -if wlan0

Update a domain with our public IP (using a lookup service):

ddnscf -v -d pi1.example.com -url https://ipv4.icanhazip.com

url must speak HTTP and respond "200 OK" with an IP address as the first line of the response body.

Update a domain once with a specific IP:

ddnscf -v -d pi1.example.com -ip 192.168.0.2 -once

Update a domain every minute:

ddnscf -v -d pi1.example.com -i 1m

Systemd Service

Create the service file:

cd /lib/systemd/system
sudo touch ddnscf.service

Add the contents:

[Unit]
Description=Keep DNS records updated
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/bin/ddnscf -d pi1.example.com -k /home/pi/.cloudflare
User=pi

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable ddnscf.service

Tips

Configuring devices or the router on the network where this ddns client resides to use Cloudflare's 1.1.1.1 DNS resolver service will further reduce DNS propagation time in the event of IP changes.

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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewCloudflare

func NewCloudflare(token string) func() (Provider, error)

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

func UsingHTTPClient(httpclient *http.Client) clientOption

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

func WithLogger(logger *log.Logger) clientOption

WithLogger configures the client with a logger for verbose logging.

The default logger discards verbose log messages.

Types

type DDNSClient

type DDNSClient interface {
	RunDDNS(ctx context.Context) error
}

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

type Resolver interface {
	Resolve(context.Context) ([]netip.Addr, error)
}

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

func FromString(addr string) Resolver

FromString constructs a resolver that parses an IP from the string addr.

func InterfaceResolver

func InterfaceResolver(iface ...string) Resolver

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

func Join(resolver ...Resolver) Resolver

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

func WebResolver(serviceURL ...string) Resolver

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

type ResolverFunc func(context.Context) ([]netip.Addr, error)

The ResolverFunc type is an adapter that allows the use of oridnary 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:

func (ResolverFunc) Resolve

func (f ResolverFunc) Resolve(ctx context.Context) ([]netip.Addr, error)

Resolve calls f(ctx)

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL