Skip to content

Commit

Permalink
Support multiple IPs and IPv6, redundant IP sources; more IP sources
Browse files Browse the repository at this point in the history
  • Loading branch information
mholt committed Feb 27, 2021
1 parent da9cea3 commit aa9c816
Show file tree
Hide file tree
Showing 6 changed files with 441 additions and 127 deletions.
69 changes: 61 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,86 @@ Dynamic DNS app for Caddy

This is a simple Caddy app that keeps your DNS pointed to your machine; especially useful if your IP address is not static.

It simply queries a service (an "IP source") for your public IP address every so often and if it changes, it updates the DNS records with your configured provider.
It simply queries a service (an "IP source") for your public IP address every so often and if it changes, it updates the DNS records with your configured provider. It supports multiple IPs, including IPv4 and IPv6, as well as redundant IP sources.

The IP source and DNS providers are modular. In addition to this app module, you'll need to plug in [a DNS provider module from caddy-dns](https://github.com/caddy-dns).
IP sources and DNS providers are modular. This app comes with IP source modules. However, you'll need to plug in [a DNS provider module from caddy-dns](https://github.com/caddy-dns) so that your DNS records can be updated.

Example Caddy config:
Example minimal Caddy config:

```json
{
"apps": {
"dynamic_dns": {
"domain": "example.com",
"domains": {
"example.com": ["@"]
},
"dns_provider": {
"name": "cloudflare",
"api_token": "topsecret",
"api_token": "topsecret"
}
}
}
}
```

Example Caddyfile config, via [global options](https://caddyserver.com/docs/caddyfile/options):
This updates DNS records for `example.com` via Cloudflare's API. (Notice how the DNS zone is separate from record names/subdomains.)

Equivalent Caddyfile config ([global options](https://caddyserver.com/docs/caddyfile/options)):

```
{
dynamic_dns {
provider cloudflare {env.CLOUDFLARE_API_TOKEN}
domains {
example.com
}
}
}
```

Here's a more filled-out JSON config:

```json
{
"apps": {
"dynamic_dns": {
"ip_sources": [
{
"source": "upnp"
},
{
"source": "simple_http",
"endpoints": ["https://icanhazip.com"]
}
],
"domains": {
"example.com": ["@", "www"],
"example.net": ["subdomain"]
},
"dns_provider": {
"name": "cloudflare",
"api_token": "topsecret"
},
"check_interval": "5m"
}
}
}
```


This config prefers to get the IP address locally via UPnP (if edge router has UPnP enabled, of course), but if that fails, will fall back to querying `icanhazip.com` for the IP address. It then updates records for `example.com`, `www.example.com`, and `subdomain.example.net`. Notice how the zones and subdomains are separate; this eliminates ambiguity since we don't have to try to be clever and figure out the zone via recursive, authoritative DNS lookups. We also check every 5 minutes instead of 30 minutes (default).

Equivalent Caddyfile (there is not currently a way to customize IP sources via Caddyfile; PRs welcomed!):

```
{
dynamic_dns {
domain example.com
provider cloudflare {env.CLOUDFLARE_API_TOKEN}
domains {
example.com @ www
example.net subdomain
}
check_interval 5m
}
}
```
```
23 changes: 16 additions & 7 deletions caddyfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ func init() {
// Syntax:
//
// dynamic_dns {
// domain <domain>
// domains {
// <zone> <names...>
// }
// check_interval <duration>
// provider <name> ...
// }
//
// If <names...> are omitted after <zone>, then "@" will be assumed.
func parseApp(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
app := new(App)

Expand All @@ -30,9 +34,17 @@ func parseApp(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
// handle the block
for d.NextBlock(0) {
switch d.Val() {
case "domain":
if !d.Args(&app.Domain) {
return nil, d.ArgErr()
case "domains":
for d.NextBlock(0) {
zone := d.Val()
if zone == "" {
return nil, d.ArgErr()
}
names := d.RemainingArgs()
if len(names) == 0 {
names = []string{"@"}
}
app.Domains[zone] = names
}

case "check_interval":
Expand All @@ -57,9 +69,6 @@ func parseApp(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
}
app.DNSProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil)

// TODO: Implement this once there's actually more than one source
// case "ip_source":

default:
return nil, d.ArgErr()
}
Expand Down
Loading

0 comments on commit aa9c816

Please sign in to comment.