-
Notifications
You must be signed in to change notification settings - Fork 109
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cmd/dyndns: prototype for dynamic DNS daemon (#50)
Updates #46. Signed-off-by: Matt Layher <[email protected]>
- Loading branch information
Showing
5 changed files
with
328 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
// Copyright 2018 Google Inc. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
// Binary dyndns updates configured DNS records with the | ||
// current public IPv4 address (of network interface uplink0). | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"flag" | ||
"fmt" | ||
"io/ioutil" | ||
"log" | ||
"net" | ||
"os" | ||
"time" | ||
|
||
"github.com/libdns/cloudflare" | ||
"github.com/libdns/libdns" | ||
"github.com/rtr7/router7/internal/dyndns" | ||
) | ||
|
||
var update = dyndns.Update | ||
|
||
type DynDNSRecord struct { | ||
// TODO: multiple providers support | ||
Cloudflare struct { | ||
APIToken string `json:"api_token"` | ||
} `json:"cloudflare"` | ||
Zone string `json:"zone"` | ||
RecordName string `json:"record_name"` | ||
// TODO: make RecordType customizable if non-A is ever desired | ||
RecordTTLSeconds int `json:"record_ttl_seconds"` | ||
} | ||
|
||
func getIPv4Address(ifname string) (string, error) { | ||
iface, err := net.InterfaceByName(ifname) | ||
if err != nil { | ||
return "", err | ||
} | ||
addrs, err := iface.Addrs() | ||
if err != nil { | ||
return "", err | ||
} | ||
for _, a := range addrs { | ||
ipnet, ok := a.(*net.IPNet) | ||
if !ok { | ||
continue | ||
} | ||
if ipnet.IP.To4() == nil { | ||
continue // not IPv4 | ||
} | ||
return ipnet.IP.String(), nil | ||
} | ||
return "", fmt.Errorf("no IPv4 address found on interface %s", ifname) | ||
} | ||
|
||
func logic(ifname string, records []DynDNSRecord) error { | ||
if len(records) == 0 { | ||
return nil // exit early | ||
} | ||
|
||
addr, err := getIPv4Address(ifname) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
for _, r := range records { | ||
apiToken := r.Cloudflare.APIToken | ||
if t, ok := os.LookupEnv("ROUTER7_CLOUDFLARE_API_KEY"); ok { | ||
apiToken = t | ||
} | ||
provider := &cloudflare.Provider{ | ||
APIToken: apiToken, | ||
} | ||
|
||
ctx := context.Background() | ||
record := libdns.Record{ | ||
Name: r.RecordName, | ||
Type: "A", | ||
Value: addr, | ||
TTL: time.Duration(r.RecordTTLSeconds) * time.Second, | ||
} | ||
if err := update(ctx, r.Zone, record, provider); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func main() { | ||
var ( | ||
configFile = flag.String( | ||
"config_file", | ||
"/perm/dyndns.json", | ||
"Path to the JSON configuration", | ||
) | ||
|
||
ifname = flag.String( | ||
"interface_name", | ||
"uplink0", | ||
"Network interface name to take the first IPv4 address from", | ||
) | ||
|
||
oneoff = flag.Bool( | ||
"oneoff", | ||
false, | ||
"run once (as opposed to continuously, in a loop)", | ||
) | ||
) | ||
flag.Parse() | ||
var config struct { | ||
Records []DynDNSRecord `json:"records"` | ||
} | ||
b, err := ioutil.ReadFile(*configFile) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
if err := json.Unmarshal(b, &config); err != nil { | ||
log.Fatal(err) | ||
} | ||
for { | ||
if err := logic(*ifname, config.Records); err != nil { | ||
log.Fatal(err) | ||
} | ||
if *oneoff { | ||
break | ||
} | ||
time.Sleep(1 * time.Minute) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/libdns/libdns" | ||
"github.com/rtr7/router7/internal/dyndns" | ||
) | ||
|
||
func TestLogic(t *testing.T) { | ||
cfg := DynDNSRecord{ | ||
Zone: "zekjur.net", | ||
RecordName: "dyndns.zekjur.net", | ||
RecordTTLSeconds: 300, // 5 minutes | ||
} | ||
update = func(ctx context.Context, zone string, record libdns.Record, _ dyndns.RecordGetterSetter) error { | ||
if got, want := zone, cfg.Zone; got != want { | ||
return fmt.Errorf("update(): unexpected zone: got %q, want %q", got, want) | ||
} | ||
if got, want := record.Name, cfg.RecordName; got != want { | ||
return fmt.Errorf("update(): unexpected record name: got %q, want %q", got, want) | ||
} | ||
return nil | ||
} | ||
if err := logic("lo", []DynDNSRecord{cfg}); err != nil { | ||
t.Fatalf("logic: %v", err) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// Copyright 2018 Google Inc. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package dyndns | ||
|
||
import ( | ||
"context" | ||
"log" | ||
|
||
"github.com/libdns/libdns" | ||
) | ||
|
||
type RecordGetterSetter interface { | ||
libdns.RecordGetter | ||
libdns.RecordSetter | ||
} | ||
|
||
// Update takes a record which should be updated | ||
// within the specified zone. | ||
func Update(ctx context.Context, zone string, record libdns.Record, provider RecordGetterSetter) error { | ||
existing, err := provider.GetRecords(ctx, zone) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
var updated []libdns.Record | ||
for _, rec := range existing { | ||
if rec.Name != record.Name || rec.Type != record.Type { | ||
continue | ||
} | ||
|
||
if rec.Value == record.Value { | ||
log.Printf("exit early: record up to date (%s)", record.Value) | ||
return nil | ||
} | ||
|
||
// TODO: it appears the cloudflare provider expects a non-empty ID to | ||
// mean that a record should be created rather than updated. This behavior | ||
// means that we clear the ID to force an update by the zone name. | ||
// | ||
// See: https://github.com/libdns/cloudflare/issues/1. | ||
rec.ID = "" | ||
rec.Value = record.Value | ||
updated = append(updated, rec) | ||
break | ||
} | ||
if len(updated) == 0 { | ||
updated = []libdns.Record{record} | ||
} | ||
|
||
if _, err := provider.SetRecords(ctx, zone, updated); err != nil { | ||
return err | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// Copyright 2018 Google Inc. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package dyndns | ||
|
||
import ( | ||
"context" | ||
"log" | ||
"testing" | ||
"time" | ||
|
||
"github.com/libdns/libdns" | ||
) | ||
|
||
func TestUpdate(t *testing.T) { | ||
ctx, canc := context.WithCancel(context.Background()) | ||
defer canc() | ||
|
||
const zone = "zekjur.net" | ||
provider := &testProvider{ | ||
getRecords: func(ctx context.Context, zone string) ([]libdns.Record, error) { | ||
return []libdns.Record{ | ||
{ | ||
ID: "rec1", | ||
Name: "dyndns.zekjur.net", | ||
Type: "A", | ||
TTL: 5 * time.Minute, | ||
Value: "127.0.0.3", | ||
}, | ||
|
||
{ | ||
ID: "rec1", | ||
Name: "unrelated.zekjur.net", | ||
Type: "A", | ||
TTL: 5 * time.Minute, | ||
Value: "127.0.0.42", | ||
}, | ||
}, nil | ||
}, | ||
setRecords: func(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) { | ||
log.Printf("setRecords(zone=%q): %+v", zone, recs) | ||
// Don't care about return records? | ||
return nil, nil | ||
}, | ||
} | ||
update := libdns.Record{ | ||
Name: "dyndns.zekjur.net", | ||
Type: "A", | ||
Value: "127.0.0.4", | ||
TTL: 5 * time.Minute, | ||
} | ||
if err := Update(ctx, zone, update, provider); err != nil { | ||
t.Fatalf("Update = %v", err) | ||
} | ||
|
||
// TODO: add a test to verify setRecords is not called | ||
// when no updates are necessary. | ||
} | ||
|
||
var ( | ||
_ RecordGetterSetter = &testProvider{} | ||
) | ||
|
||
type testProvider struct { | ||
getRecords func(ctx context.Context, zone string) ([]libdns.Record, error) | ||
setRecords func(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) | ||
} | ||
|
||
func (p *testProvider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) { | ||
return p.getRecords(ctx, zone) | ||
} | ||
|
||
func (p *testProvider) SetRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) { | ||
return p.setRecords(ctx, zone, recs) | ||
} |