diff --git a/README.md b/README.md index 511d9cb..12eeba4 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ Create a `config.json` for the client. Enter the domain name you want to update, { "domains": { "mydomain.example.com": { + "ip4": true, + "ip6": false, "provider": "gcp", "provider_config": { "project_id": "example-project", diff --git a/cmd/cloud-dyndns-client/main.go b/cmd/cloud-dyndns-client/main.go index 3479298..1ef748d 100644 --- a/cmd/cloud-dyndns-client/main.go +++ b/cmd/cloud-dyndns-client/main.go @@ -30,9 +30,9 @@ import ( "golang.org/x/sync/errgroup" - "github.com/ianlewis/cloud-dyndns-client/pkg/backend" - "github.com/ianlewis/cloud-dyndns-client/pkg/backend/gcp" - "github.com/ianlewis/cloud-dyndns-client/pkg/sync" + "github.com/lordwelch/cloud-dyndns-client/pkg/backend" + "github.com/lordwelch/cloud-dyndns-client/pkg/backend/gcp" + "github.com/lordwelch/cloud-dyndns-client/pkg/sync" ) // VERSION is the current version of the application. @@ -40,6 +40,8 @@ var VERSION = "0.1.5" // Domain is a single domain listed in the configuration file. type Domain struct { + IP4 bool `json:"ip4"` + IP6 bool `json:"ip6"` Provider string `json:"provider"` ProviderConfig map[string]interface{} `json:"provider_config"` Backend backend.DNSBackend @@ -112,6 +114,44 @@ func getConfig(pathToJSON string) (Config, error) { return cfg, nil } +func constructRecord(name string, d *Domain) []sync.Record { + var records []sync.Record + if d.IP4 { + records = append(records, sync.Record{ + Record: backend.NewDNSRecord( + name, + "A", + 600, + []string{}, + ), + Backend: d.Backend, + }) + } + if d.IP6 { + records = append(records, sync.Record{ + Record: backend.NewDNSRecord( + name, + "AAAA", + 600, + []string{}, + ), + Backend: d.Backend, + }) + } + if len(records) < 1 { + records = append(records, sync.Record{ + Record: backend.NewDNSRecord( + name, + "A", + 600, + []string{}, + ), + Backend: d.Backend, + }) + } + return records +} + // Main is the main function for the cloud-dyndns-client command. It returns the OS exit code. func main() { addr := flag.String("addr", "", "Address to listen on for health checks.") @@ -136,15 +176,7 @@ func main() { if !strings.HasSuffix(name, ".") { name = name + "." } - records = append(records, sync.Record{ - Record: backend.NewDNSRecord( - name, - "A", - 600, - []string{}, - ), - Backend: d.Backend, - }) + records = append(records, constructRecord(name, d)...) } // Create a new syncer. This will sync DNS records to backends @@ -153,7 +185,8 @@ func main() { // The IP Address poller will poll for the Internet IP address. // When a new address is polled the data will be forwarded to the syncer. - poller := sync.NewIPAddressPoller(5 * time.Minute) + IP4Poller := sync.NewIPAddressPoller(sync.IP4, 5*time.Minute, nil) + IP6Poller := sync.NewIPAddressPoller(sync.IP6, 5*time.Minute, nil) // Create a waitgroup to manage the goroutines for the main loops. // The waitgroup can be used to wait for goroutines to finish. @@ -162,21 +195,35 @@ func main() { // TODO: Refactor and move this code to it's own package wg.Go(func() error { return syncer.Run(ctx.Done()) }) - wg.Go(func() error { return poller.Run(ctx.Done()) }) + wg.Go(func() error { return IP4Poller.Run(ctx.Done()) }) wg.Go(func() error { // This goroutine receives IP address polling results // and updates the desired records in the Syncer. - c := poller.Channel() + ip4c := IP4Poller.Channel() + ip6c := IP6Poller.Channel() for { select { - case ip := <-c: + case ip := <-ip4c: for _, r := range records { - syncer.UpdateRecord( - r.Record.Name(), - r.Record.Type(), - r.Record.Ttl(), - []string{ip}, - ) + if r.Record.Type() == "A" { + syncer.UpdateRecord( + r.Record.Name(), + r.Record.Type(), + r.Record.Ttl(), + []string{ip}, + ) + } + } + case ip := <-ip6c: + for _, r := range records { + if r.Record.Type() == "AAAA" { + syncer.UpdateRecord( + r.Record.Name(), + r.Record.Type(), + r.Record.Ttl(), + []string{ip}, + ) + } } case <-ctx.Done(): return nil diff --git a/pkg/backend/gcp/backend.go b/pkg/backend/gcp/backend.go index c5df0b3..deaf741 100644 --- a/pkg/backend/gcp/backend.go +++ b/pkg/backend/gcp/backend.go @@ -22,7 +22,7 @@ import ( "golang.org/x/oauth2/google" dns "google.golang.org/api/dns/v1" - "github.com/ianlewis/cloud-dyndns-client/pkg/backend" + "github.com/lordwelch/cloud-dyndns-client/pkg/backend" ) var cloudDnsScopes = []string{ diff --git a/pkg/sync/ipaddress.go b/pkg/sync/ipaddress.go index 347199f..6d4d49e 100644 --- a/pkg/sync/ipaddress.go +++ b/pkg/sync/ipaddress.go @@ -11,19 +11,21 @@ // 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 sync import ( - "context" "fmt" "log" - "math/rand" - "net/http" - "strings" "time" - "golang.org/x/net/html" + externalip "gitlab.com/vocdoni/go-external-ip" +) + +type IPType uint + +const ( + IP4 IPType = 4 + IP6 IPType = 6 ) var webCheck = []string{ @@ -37,94 +39,72 @@ var webCheck = []string{ type IPAddressPoller struct { channels []chan string pollInterval time.Duration + consensus *externalip.Consensus + iptype IPType } -func NewIPAddressPoller(pollInterval time.Duration) *IPAddressPoller { +func NewIPAddressPoller(iptype IPType, pollInterval time.Duration, consensus *externalip.Consensus) *IPAddressPoller { + if consensus == nil { + return &IPAddressPoller{ + pollInterval: pollInterval, + consensus: externalip.DefaultConsensus(nil, nil), + iptype: iptype, + } + } return &IPAddressPoller{ pollInterval: pollInterval, + consensus: consensus, + iptype: iptype, } } -// Channel() returns a channel that receives data whenever an +// Channel returns a channel that receives data whenever an // IP address value is received. -func (p *IPAddressPoller) Channel() <-chan string { +func (i *IPAddressPoller) Channel() <-chan string { c := make(chan string, 1) - p.channels = append(p.channels, c) + i.channels = append(i.channels, c) return c } // poll() runs a single polling event and retrieving the internet IP. -func (p *IPAddressPoller) poll() error { - // Shuffle the list of URLs randomly so that they aren't - // always used in the same order. - urls := make([]string, len(webCheck)) - copy(urls, webCheck) - for i := range urls { - j := rand.Intn(i + 1) - urls[i], urls[j] = urls[j], urls[i] - } - +func (i *IPAddressPoller) poll() error { // Make a request to each url and send to the - // channels if an IP is retrieved - var lastErr error - for i := range urls { - ip, err := request(urls[i]) - if err != nil { - lastErr = err - continue - } - - for _, c := range p.channels { - select { - case c <- ip: - default: - } - } - return nil + // channels if a consensus is achieved + ip, err := i.consensus.ExternalIP(uint(i.iptype)) + if err != nil { + return fmt.Errorf("could not obtain IP address: %w", err) } - - return fmt.Errorf("Could not obtain IP address: %v", lastErr) + for _, c := range i.channels { + select { + case c <- ip.String(): + default: + } + } + return nil } // request() makes a request to a URL to get the internet IP address. -func request(url string) (string, error) { - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - return "", err - } +// func html_parser(url string) (string, error) { +// z := html.NewTokenizer(resp.Body) +// for { +// tt := z.Next() +// switch tt { +// case html.ErrorToken: +// return "", z.Err() +// case html.TextToken: +// text := strings.Trim(string(z.Text()), " \n\t") +// if text != "" { +// ip := "" +// fmt.Sscanf(text, "Current IP Address: %s", &ip) +// if ip != "" { +// return strings.Trim(ip, " \n\t"), nil +// } +// } +// } +// } +// } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - req = req.WithContext(ctx) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("Got status code from %q: %d", url, resp.StatusCode) - } - - z := html.NewTokenizer(resp.Body) - for { - tt := z.Next() - switch tt { - case html.ErrorToken: - return "", z.Err() - case html.TextToken: - text := strings.Trim(string(z.Text()), " \n\t") - if text != "" { - ip := "" - fmt.Sscanf(text, "Current IP Address: %s", &ip) - if ip != "" { - return strings.Trim(ip, " \n\t"), nil - } - } - } - } -} - -// Run() starts the main loop for the poller. +// Run starts the main loop for the poller. func (i *IPAddressPoller) Run(stopCh <-chan struct{}) error { if err := i.poll(); err != nil { log.Printf("Error polling for IP: %v", err) diff --git a/pkg/sync/sync.go b/pkg/sync/sync.go index c145f24..72dffeb 100644 --- a/pkg/sync/sync.go +++ b/pkg/sync/sync.go @@ -23,7 +23,7 @@ import ( "sync" "time" - "github.com/ianlewis/cloud-dyndns-client/pkg/backend" + "github.com/lordwelch/cloud-dyndns-client/pkg/backend" ) type Record struct {