Compare commits
2 Commits
dbc4671ddc
...
7a4a585257
Author | SHA1 | Date | |
---|---|---|---|
7a4a585257 | |||
a0bc0df32a |
29
.gitea/workflows/release.yaml
Normal file
29
.gitea/workflows/release.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
trategy:
|
||||||
|
matrix:
|
||||||
|
go-os: [ 'linux' ]
|
||||||
|
go-arch: [ 'amd64', 'arm64' ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Go ${{ matrix.go-os }} ${{ matrix.go-arch }}
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.21.x'
|
||||||
|
- name: Display Go version
|
||||||
|
run: GOOS=${{ matrix.go-os }} GOARCH=${{ matrix.go-arch }} go version
|
||||||
|
- name: Install dependencies
|
||||||
|
run: GOOS=${{ matrix.go-os }} GOARCH=${{ matrix.go-arch }} go get -v
|
||||||
|
- name: Test
|
||||||
|
run: GOOS=${{ matrix.go-os }} GOARCH=${{ matrix.go-arch }} go test -v ./...
|
||||||
|
- name: Build
|
||||||
|
run: GOOS=${{ matrix.go-os }} GOARCH=${{ matrix.go-arch }} go build -v ./cmd/update-ip
|
||||||
|
- name: Upload Go test results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: update-ip_${{ matrix.go-os }}_${{ matrix.go-arch }}
|
||||||
|
path: update-ip
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Don't check in private key files
|
||||||
|
*.key
|
51
cmd/update-ip/main.go
Normal file
51
cmd/update-ip/main.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
updateip "git.maze.io/wijnand/update-ip"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configFile := flag.String("config", "", "configuration file")
|
||||||
|
debugFlag := flag.Bool("debug", false, "enable debug messages")
|
||||||
|
traceFlag := flag.Bool("trace", false, "enable trace messages")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *traceFlag {
|
||||||
|
log.SetLevel(log.TraceLevel)
|
||||||
|
} else if *debugFlag {
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := updateip.Load(*configFile)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Fatal("failed to configure")
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, err := c.Resolver.CurrentIP()
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Fatal("failed to resolve current IP")
|
||||||
|
}
|
||||||
|
log.WithField("ip", ip).Info("resolved current IP")
|
||||||
|
|
||||||
|
if !c.Resolver.IsAllowed(ip) {
|
||||||
|
log.WithField("ip", ip).Fatal("not allowed by ACL")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, domain := range c.Domain {
|
||||||
|
log := log.WithField("domain", domain.Name)
|
||||||
|
log.Info("checking domain")
|
||||||
|
if len(domain.Update) == 0 {
|
||||||
|
log.Warn("no records to update!")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range domain.Update {
|
||||||
|
if err := domain.Provider.UpdateIP(domain.Name, record, ip); err != nil {
|
||||||
|
log.WithError(err).Error("update failed!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
config.go
Normal file
93
config.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package updateip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2/hclsimple"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Load(name string) (*Config, error) {
|
||||||
|
var (
|
||||||
|
config Config
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
log.WithField("config", name).Debug("loading configuration file")
|
||||||
|
if err = hclsimple.DecodeFile(name, nil, &config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
providers := make(map[string]Provider)
|
||||||
|
for _, v := range config.Provider {
|
||||||
|
if providers[v.Name], err = NewProvider(v); err != nil {
|
||||||
|
return nil, fmt.Errorf("error configuring provider %q: %w", v.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range config.Domain {
|
||||||
|
if p, ok := providers[v.ProviderName]; ok {
|
||||||
|
v.Provider = p
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("provider %q in domain %q is not configured", v.ProviderName, v.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v.Update) == 0 {
|
||||||
|
v.Update = append(v.Update, "@")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Resolver ResolverConfig `hcl:"resolver,block"`
|
||||||
|
Domain []*DomainConfig `hcl:"domain,block"`
|
||||||
|
Provider []*ProviderConfig `hcl:"provider,block"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResolverConfig struct {
|
||||||
|
Permit []string `hcl:"permit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ResolverConfig) CurrentIP() (net.IP, error) {
|
||||||
|
return NewResolver().CurrentIP()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ResolverConfig) IsAllowed(ip net.IP) bool {
|
||||||
|
if len(r.Permit) == 0 {
|
||||||
|
// Fail open if no ACL is configured.
|
||||||
|
log.WithField("ip", ip.String()).Debug("IP allowed by resolver; no ACL")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range r.Permit {
|
||||||
|
if _, ipnet, err := net.ParseCIDR(v); err == nil {
|
||||||
|
if ipnet.Contains(ip) {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"ip": ip.String(),
|
||||||
|
"range": ipnet.String(),
|
||||||
|
}).Debug("IP allowed by resolver ACL")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithField("ip", ip.String()).Warn("IP not allowed by resolver ACL")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type DomainConfig struct {
|
||||||
|
Name string `hcl:"name,label"`
|
||||||
|
ProviderName string `hcl:"provider"`
|
||||||
|
Provider Provider ``
|
||||||
|
Update []string `hcl:"update,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderConfig struct {
|
||||||
|
Name string `hcl:"name,label"`
|
||||||
|
Type string
|
||||||
|
Username string `hcl:"username"`
|
||||||
|
PrivateKey string `hcl:"private_key"`
|
||||||
|
}
|
39
fqdn.go
Normal file
39
fqdn.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package updateip
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// Canonicalize a DNS record.
|
||||||
|
func Canonicalize(domainName, recordName string) string {
|
||||||
|
if recordName == "@" {
|
||||||
|
return ToFqdn(domainName)
|
||||||
|
} else if strings.HasSuffix(recordName, ".@") {
|
||||||
|
return ToFqdn(strings.TrimSuffix(recordName, "@") + domainName)
|
||||||
|
}
|
||||||
|
return ToFqdn(recordName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFqdn checks if the label is a full-qualified name.
|
||||||
|
func IsFqdn(name string) bool {
|
||||||
|
return strings.HasSuffix(name, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFqdn converts the name into a fqdn appending a trailing dot.
|
||||||
|
func ToFqdn(name string) string {
|
||||||
|
if name == "@" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
n := len(name)
|
||||||
|
if n == 0 || name[n-1] == '.' {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return name + "."
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnFqdn converts the fqdn into a name removing the trailing dot.
|
||||||
|
func UnFqdn(name string) string {
|
||||||
|
n := len(name)
|
||||||
|
if n != 0 && name[n-1] == '.' {
|
||||||
|
return name[:n-1]
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
64
fqdn_test.go
Normal file
64
fqdn_test.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package updateip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToFqdn(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
domain string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "simple",
|
||||||
|
domain: "foo.example.com",
|
||||||
|
expected: "foo.example.com.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "already FQDN",
|
||||||
|
domain: "foo.example.com.",
|
||||||
|
expected: "foo.example.com.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
fqdn := ToFqdn(test.domain)
|
||||||
|
assert.Equal(t, test.expected, fqdn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnFqdn(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
fqdn string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "simple",
|
||||||
|
fqdn: "foo.example.",
|
||||||
|
expected: "foo.example",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "already domain",
|
||||||
|
fqdn: "foo.example",
|
||||||
|
expected: "foo.example",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
domain := UnFqdn(test.fqdn)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, domain)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
27
go.mod
Normal file
27
go.mod
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
module git.maze.io/wijnand/update-ip
|
||||||
|
|
||||||
|
go 1.22.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/hashicorp/hcl/v2 v2.21.0
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/transip/gotransip/v6 v6.25.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/agext/levenshtein v1.2.1 // indirect
|
||||||
|
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||||
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.9.0 // indirect
|
||||||
|
github.com/transip/gotransip v5.8.2+incompatible // indirect
|
||||||
|
github.com/zclconf/go-cty v1.13.0 // indirect
|
||||||
|
golang.org/x/mod v0.8.0 // indirect
|
||||||
|
golang.org/x/sys v0.5.0 // indirect
|
||||||
|
golang.org/x/text v0.11.0 // indirect
|
||||||
|
golang.org/x/tools v0.6.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
51
go.sum
Normal file
51
go.sum
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
||||||
|
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||||
|
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
||||||
|
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||||
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||||
|
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||||
|
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||||
|
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/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14=
|
||||||
|
github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
|
||||||
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||||
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||||
|
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/transip/gotransip v5.8.2+incompatible h1:aNJhw/w/3QBqFcHAIPz1ytoK5FexeMzbUCGrrhWr3H0=
|
||||||
|
github.com/transip/gotransip v5.8.2+incompatible/go.mod h1:uacMoJVmrfOcscM4Bi5NVg708b7c6rz2oDTWqa7i2Ic=
|
||||||
|
github.com/transip/gotransip/v6 v6.25.0 h1:/H+SjMq/9HNZ0/maE1OLhJpxLaCGHsxq0PWaMPJHxK4=
|
||||||
|
github.com/transip/gotransip/v6 v6.25.0/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=
|
||||||
|
github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0=
|
||||||
|
github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
|
||||||
|
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
|
||||||
|
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
|
||||||
|
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||||
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
66
label.go
Normal file
66
label.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package updateip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompressLabel returns the smallest possible subdomain label for the provided record against the domain.
|
||||||
|
func CompressLabel(domainName, recordName string) string {
|
||||||
|
domainName = UnFqdn(domainName)
|
||||||
|
recordName = UnFqdn(recordName)
|
||||||
|
if strings.EqualFold(domainName, recordName) {
|
||||||
|
return "@"
|
||||||
|
} else if i := indexFold(recordName, "."+domainName); i > -1 {
|
||||||
|
return recordName[:i]
|
||||||
|
}
|
||||||
|
return recordName
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExapandLabel gives the full DNS label for the (abbreviated) record.
|
||||||
|
func ExapandLabel(domainName, recordName string) string {
|
||||||
|
if strings.EqualFold(domainName, recordName) {
|
||||||
|
return recordName
|
||||||
|
} else if IsFqdn(recordName) {
|
||||||
|
return recordName
|
||||||
|
} else if recordName == "@" {
|
||||||
|
return domainName
|
||||||
|
} else if i := strings.LastIndex(recordName, ".@"); i > -1 {
|
||||||
|
return recordName[:i] + "." + domainName
|
||||||
|
}
|
||||||
|
return recordName + "." + domainName
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexFold returns the index of the first instance of substr in s (case insensitive), or -1 if substr is not present in s.
|
||||||
|
func indexFold(s, substr string) int {
|
||||||
|
ns := len(s)
|
||||||
|
nb := len(substr)
|
||||||
|
if ns < nb {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if nb == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if ns == nb {
|
||||||
|
if strings.EqualFold(s, substr) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
l := ns - nb
|
||||||
|
for i := 0; i <= l; {
|
||||||
|
src := s[i : i+nb]
|
||||||
|
if strings.EqualFold(src, substr) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
_, z := utf8.DecodeRuneInString(src)
|
||||||
|
i += z
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasSuffixFold Tests if the string s ends with the specified suffix (case insensitive).
|
||||||
|
func hasSuffixFold(s, suffix string) bool {
|
||||||
|
return len(s) >= len(suffix) && strings.EqualFold(s[len(s)-len(suffix):], suffix)
|
||||||
|
}
|
91
label_test.go
Normal file
91
label_test.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package updateip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCompressLabel(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
domain, record string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"same",
|
||||||
|
"example.org",
|
||||||
|
"example.org",
|
||||||
|
"@",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"same_fqdn",
|
||||||
|
"example.org.",
|
||||||
|
"example.org",
|
||||||
|
"@",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"simple",
|
||||||
|
"example.org",
|
||||||
|
"www.example.org",
|
||||||
|
"www",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unrelated",
|
||||||
|
"example.org",
|
||||||
|
"example.net",
|
||||||
|
"example.net",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(it *testing.T) {
|
||||||
|
it.Parallel()
|
||||||
|
|
||||||
|
result := CompressLabel(test.domain, test.record)
|
||||||
|
assert.Equal(it, test.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandLabel(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
domain, record string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"same",
|
||||||
|
"example.org",
|
||||||
|
"@",
|
||||||
|
"example.org",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"simple",
|
||||||
|
"example.org",
|
||||||
|
"www",
|
||||||
|
"www.example.org",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unrelated",
|
||||||
|
"example.org",
|
||||||
|
"example.net",
|
||||||
|
"example.net.example.org",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"using_at",
|
||||||
|
"example.org",
|
||||||
|
"www.@",
|
||||||
|
"www.example.org",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(it *testing.T) {
|
||||||
|
it.Parallel()
|
||||||
|
|
||||||
|
result := ExapandLabel(test.domain, test.record)
|
||||||
|
assert.Equal(it, test.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
39
provider.go
Normal file
39
provider.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package updateip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultTTL = 5 * time.Minute
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
UpdateIP(domain, name string, ip net.IP) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvider(config *ProviderConfig) (Provider, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("provider: config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Type == "" {
|
||||||
|
config.Type = config.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"name": config.Name,
|
||||||
|
"type": config.Type,
|
||||||
|
}).Debug("configuring provider")
|
||||||
|
|
||||||
|
switch strings.ToLower(config.Type) {
|
||||||
|
case "transip":
|
||||||
|
return NewTransIP(config)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("provider %s: unsupported type %q", config.Name, config.Type)
|
||||||
|
}
|
||||||
|
}
|
91
provider_transip.go
Normal file
91
provider_transip.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package updateip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/transip/gotransip/v6"
|
||||||
|
transipdomain "github.com/transip/gotransip/v6/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type transipProvider struct {
|
||||||
|
repo transipdomain.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransIP(config *ProviderConfig) (Provider, error) {
|
||||||
|
client, err := gotransip.NewClient(gotransip.ClientConfiguration{
|
||||||
|
AccountName: config.Username,
|
||||||
|
PrivateKeyPath: config.PrivateKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &transipProvider{
|
||||||
|
repo: transipdomain.Repository{Client: client},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *transipProvider) UpdateIP(domain, name string, ip net.IP) error {
|
||||||
|
domainName := UnFqdn(domain)
|
||||||
|
entries, err := p.repo.GetDNSEntries(domainName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entryName := CompressLabel(domainName, UnFqdn(name))
|
||||||
|
for _, entry := range entries {
|
||||||
|
log := log.WithFields(log.Fields{
|
||||||
|
"domain": domainName,
|
||||||
|
"name": entry.Name,
|
||||||
|
"type": entry.Type,
|
||||||
|
"ttl": entry.Expire,
|
||||||
|
"content": entry.Content,
|
||||||
|
})
|
||||||
|
log.Trace("checking DNS record")
|
||||||
|
|
||||||
|
if strings.EqualFold(ExapandLabel(domainName, entryName), ExapandLabel(domainName, entry.Name)) {
|
||||||
|
switch {
|
||||||
|
case IsLabel(entry.Type):
|
||||||
|
if entry.Content != ip.String() {
|
||||||
|
log.WithField("content", ip.String()).Info("updating DNS record content")
|
||||||
|
return p.repo.UpdateDNSEntry(domainName, entry)
|
||||||
|
}
|
||||||
|
log.Debug("not updating DNS record; already up-to-date!")
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case IsCanonicalName(entry.Type):
|
||||||
|
log.Info("removing DNS record")
|
||||||
|
if err = p.repo.RemoveDNSEntry(domainName, entry); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No entry found, create one
|
||||||
|
entry := transipdomain.DNSEntry{
|
||||||
|
Name: entryName,
|
||||||
|
Type: "A",
|
||||||
|
Content: ip.String(),
|
||||||
|
Expire: int(DefaultTTL.Seconds()),
|
||||||
|
}
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"name": entry.Name,
|
||||||
|
"type": "A",
|
||||||
|
"new": ip.String(),
|
||||||
|
"expire": entry.Expire,
|
||||||
|
}).Info("creating DNS record")
|
||||||
|
return p.repo.AddDNSEntry(domainName, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLabel check if the DNS record type is a DNS label (A-record).
|
||||||
|
func IsLabel(recordType string) bool {
|
||||||
|
return strings.EqualFold(recordType, "A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCanonicalName checks if the DNS record type is a DNS canonical name (CNAME record).
|
||||||
|
func IsCanonicalName(recordType string) bool {
|
||||||
|
return strings.EqualFold(recordType, "CNAME")
|
||||||
|
}
|
33
resolve.go
Normal file
33
resolve.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package updateip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Resolver interface {
|
||||||
|
CurrentIP() (net.IP, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResolver() Resolver {
|
||||||
|
return new(Ipify)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ipify struct{}
|
||||||
|
|
||||||
|
func (Ipify) CurrentIP() (net.IP, error) {
|
||||||
|
r, err := http.Get("https://api.ipify.org")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = r.Body.Close() }()
|
||||||
|
|
||||||
|
b, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return net.ParseIP(string(b)), nil
|
||||||
|
}
|
13
resolve_test.go
Normal file
13
resolve_test.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package updateip
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestResolveCurrentIP(t *testing.T) {
|
||||||
|
r := new(Ipify)
|
||||||
|
|
||||||
|
v, err := r.CurrentIP()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
t.Log("current IP:", v)
|
||||||
|
}
|
76
update-my-ip.hcl
Normal file
76
update-my-ip.hcl
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
resolver {
|
||||||
|
permit = [
|
||||||
|
# AS50266 - Odido Netherlands B.V.
|
||||||
|
"143.177.0.0/16", # Pool for fixed WBA users 65,536
|
||||||
|
"143.178.0.0/17", # Pool for fixed WBA users 32,768
|
||||||
|
"143.178.128.0/17", # Pool for fixed WBA users 32,768
|
||||||
|
"143.178.232.0/24", # Pool for fixed WBA users 256
|
||||||
|
"143.179.0.0/16", # Tele2 Nederland B.V. 65,536
|
||||||
|
"185.180.148.0/22", # Odido Netherlands B.V. 1,024
|
||||||
|
"185.35.112.0/22", # Odido Netherlands B.V. 1,024
|
||||||
|
"188.88.0.0/14", # Odido Netherlands B.V. 262,144
|
||||||
|
"188.88.0.0/16", # Pool for mobile internet users 65,536
|
||||||
|
"188.89.0.0/16", # Pool for mobile internet users 65,536
|
||||||
|
"188.90.0.0/16", # Pool for mobile internet users 65,536
|
||||||
|
"188.91.0.0/16", # Pool for mobile internet users 65,536
|
||||||
|
"195.191.16.0/23", # Odido Netherlands B.V. 512
|
||||||
|
"31.184.64.0/18", # Odido Netherlands 16,384
|
||||||
|
"31.187.128.0/17", # Pool for fixed WBA, ODF and VULA users 32,768
|
||||||
|
"31.20.0.0/16", # Pool for fixed WBA users 65,536
|
||||||
|
"31.201.0.0/16", # Pool for fixed WBA, ODF and VULA users 65,536
|
||||||
|
"31.21.0.0/16", # Pool for fixed WBA users 65,536
|
||||||
|
"37.143.80.0/21", # Odido Netherlands B.V. 2,048
|
||||||
|
"5.132.0.0/17", # Odido Netherlands B.V. 32,768
|
||||||
|
"62.166.128.0/17", # Pool for fixed WBA users 32,768
|
||||||
|
"62.250.0.0/17", # Odido Netherlands 32,768
|
||||||
|
"62.250.128.0/17", # Pool for fixed WBA users 32,768
|
||||||
|
"81.59.0.0/17", # Zon internet is one of the largest free ISP in the Netherlands 32,768
|
||||||
|
"82.172.0.0/17", # Tele2 Consumer is one of the largest ISP\'s in the Netherlands 32,768
|
||||||
|
"82.174.0.0/16", # Pool for fixed WBA users 65,536
|
||||||
|
"85.144.0.0/15", # Odido Netherlands B.V. 131,072
|
||||||
|
"85.146.0.0/17", # Odido Netherlands B.V. 32,768
|
||||||
|
"85.146.128.0/18", # Odido Netherlands B.V. 16,384
|
||||||
|
"85.223.0.0/17", # Odido Netherlands 32,768
|
||||||
|
"87.208.0.0/16", # Pool for fixed WBA users 65,536
|
||||||
|
"87.209.0.0/16", # Pool for fixed WBA users 65,536
|
||||||
|
"87.209.180.0/24", # Pool for fixed WBA users 256
|
||||||
|
"87.210.0.0/16", # Pool for fixed WBA users 65,536
|
||||||
|
"87.212.0.0/16", # Pool for fixed WBA users 65,536
|
||||||
|
"92.254.0.0/17", # Odido Netherlands 32,768
|
||||||
|
"94.157.0.0/16", # Pool for fixed WBA, ODF and VULA users 65,536
|
||||||
|
"95.98.0.0/15", # Odido Netherlands B.V. 131,072
|
||||||
|
"95.98.0.0/16", # Pool for mobile internet users 65,536
|
||||||
|
"95.99.0.0/16", # Pool for mobile internet users 65,536
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#domain "maze.io" {
|
||||||
|
# provider = "transip"
|
||||||
|
# update = ["maze.io", "www.maze.io"]
|
||||||
|
#}
|
||||||
|
|
||||||
|
domain "duzzdus.nl" {
|
||||||
|
provider = "transip"
|
||||||
|
}
|
||||||
|
|
||||||
|
domain "modderman-lenstra.nl" {
|
||||||
|
provider = "transip"
|
||||||
|
}
|
||||||
|
|
||||||
|
domain "maze.casa" {
|
||||||
|
provider = "transip"
|
||||||
|
}
|
||||||
|
|
||||||
|
domain "maze.network" {
|
||||||
|
provider = "transip"
|
||||||
|
}
|
||||||
|
|
||||||
|
domain "maze.io" {
|
||||||
|
provider = "transip"
|
||||||
|
update = ["@", "lab.maze.io"]
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "transip" {
|
||||||
|
username = "tehmaze"
|
||||||
|
private_key = "testdata/transip.key"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user