commit 1ffcd90796c237dbacf5030ba0b78474343a2007 Author: Faerbit Date: Sun Feb 4 12:58:01 2024 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc0f50b --- /dev/null +++ b/.gitignore @@ -0,0 +1,93 @@ +# Created by https://www.toptal.com/developers/gitignore/api/goland+all +# Edit at https://www.toptal.com/developers/gitignore?templates=goland+all + +### GoLand+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +# End of https://www.toptal.com/developers/gitignore/api/goland+all + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a478135 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module dyndns-inwx-go + +go 1.21 + +require ( + github.com/libdns/inwx v0.2.0 + github.com/libdns/libdns v0.2.1 + github.com/sethvargo/go-envconfig v1.0.0 +) + +require ( + github.com/boombuler/barcode v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pquerna/otp v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0595ec8 --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/libdns/inwx v0.2.0 h1:yCP5EiVqmmp71dFhCD34PkUxLQixJTEZNOlO6itdNeA= +github.com/libdns/inwx v0.2.0/go.mod h1:QNKQqPp5wVvbuMMmGFZz0UL9tZ6FaWlKTMPeUqPsJ+g= +github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= +github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/sethvargo/go-envconfig v1.0.0 h1:1C66wzy4QrROf5ew4KdVw942CQDa55qmlYmw9FZxZdU= +github.com/sethvargo/go-envconfig v1.0.0/go.mod h1:Lzc75ghUn5ucmcRGIdGQ33DKJrcjk4kihFYgSTBmjIc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..e47d869 --- /dev/null +++ b/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "context" + "fmt" + "github.com/libdns/libdns" + "log" + "net" + "os" + "strings" + "time" + + "github.com/libdns/inwx" + "github.com/sethvargo/go-envconfig" +) + +type Config struct { + Username string `env:"DYNDNS_USERNAME, required"` + Password string `env:"DYNDNS_PASSWORD, required"` + Zone string `env:"DYNDNS_ZONE, required"` + Record string `env:"DYNDNS_RECORD, required"` + Nameservers string `env:"DYNDNS_NAMESERVERS, required"` +} + +func main() { + ctx := context.Background() + var cfg Config + if err := envconfig.Process(ctx, &cfg); err != nil { + log.Fatal("Error reading config: ", err) + } + + ifaces, err := net.Interfaces() + if err != nil { + log.Fatal("Error getting interfaces: ", err) + } + var ownIp string + for _, iface := range ifaces { + addrs, err := iface.Addrs() + if err != nil { + log.Fatalf("Error getting addresses for %v: %s", iface, err) + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && + ipnet.IP.To4() == nil && + !ipnet.IP.IsPrivate() && + !ipnet.IP.IsLinkLocalUnicast() && + !ipnet.IP.IsLoopback() && + ipnet.IP.IsGlobalUnicast() { + ownIp = ipnet.IP.String() + } + } + } + log.Print("Detected own IP: ", ownIp) + + resolv := &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{ + Timeout: time.Duration(10) * time.Second, + } + var conn net.Conn + var err error + for _, server := range strings.Split(cfg.Nameservers, ",") { + conn, err = d.DialContext(ctx, network, fmt.Sprintf("%s:53", server)) + if err == nil { + return conn, err + } else { + log.Printf("Error resolving with NS \"%s\"", server) + } + } + return conn, err + }, + } + + target := fmt.Sprintf("%s.%s", cfg.Record, cfg.Zone) + resolvIps, err := resolv.LookupHost(ctx, target) + if err != nil { + log.Fatalf("Error resolving %s: %v", target, err) + } + if len(resolvIps) != 1 { + log.Fatalf("Detected %d IPs for \"%s\". Don't know what to do", len(resolvIps), target) + } + resolvIp := resolvIps[0] + log.Printf("Resolved IP: %s", resolvIp) + + forceUpdate := len(os.Args) == 2 && os.Args[1] == "--force" + if forceUpdate { + log.Println("Detected force update") + } + + if ownIp == resolvIp && !forceUpdate { + log.Println("Resolved IP is equal to own IP. Nothing to do.") + os.Exit(0) + } + + provider := &inwx.Provider{ + Username: cfg.Username, + Password: cfg.Password, + } + + records, err := provider.SetRecords(context.Background(), cfg.Zone, []libdns.Record{ + { + Type: "AAAA", + Name: cfg.Record, + Value: ownIp, + TTL: time.Duration(15) * time.Minute, + }, + }) + + if err != nil { + log.Fatalf("Error updating DNS record \"%s\": %s", target, err) + } + + for _, record := range records { + log.Printf("Set record: %v", record) + } +}