Compare commits
No commits in common. "pog" and "0.0.1" have entirely different histories.
5 changed files with 33 additions and 195 deletions
12
Taskfile.yml
12
Taskfile.yml
|
@ -1,12 +0,0 @@
|
||||||
version: "3"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
ko:
|
|
||||||
cmds:
|
|
||||||
- podman run --rm -it
|
|
||||||
-v $PWD:/work:Z
|
|
||||||
--workdir=/work
|
|
||||||
-e KO_DOCKER_REPO=gitea.nulo.in/nulo/ddnser
|
|
||||||
-v ~/.docker/:/docker-config:Z,ro
|
|
||||||
-e DOCKER_CONFIG=/docker-config
|
|
||||||
cgr.dev/chainguard/ko build --bare .
|
|
15
config.go
15
config.go
|
@ -16,9 +16,8 @@ type config struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type domain struct {
|
type domain struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ZoneName string `json:"zoneName,omitempty"`
|
|
||||||
// TODO: lograr que esto sea un coso de propiedades arbitrario
|
// TODO: lograr que esto sea un coso de propiedades arbitrario
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
}
|
}
|
||||||
|
@ -69,16 +68,6 @@ func LoadConfig(path string) (state State, err error) {
|
||||||
Name: d.Name,
|
Name: d.Name,
|
||||||
NameServer: &nameservers.HeNet{HTTPClient: &state.HTTPClient, Password: d.Key},
|
NameServer: &nameservers.HeNet{HTTPClient: &state.HTTPClient, Password: d.Key},
|
||||||
})
|
})
|
||||||
case "cloudflare v4 api":
|
|
||||||
if len(d.ZoneName) == 0 {
|
|
||||||
err = errors.New("cloudflare v4 api: missing zoneName property")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
state.Domains = append(state.Domains, Domain{
|
|
||||||
Name: d.Name,
|
|
||||||
NameServer: &nameservers.CloudflareV4{HTTPClient: &state.HTTPClient, Key: d.Key,
|
|
||||||
ZoneName: d.ZoneName},
|
|
||||||
})
|
|
||||||
default:
|
default:
|
||||||
err = errors.New("I don't know the service type " + d.Type)
|
err = errors.New("I don't know the service type " + d.Type)
|
||||||
return
|
return
|
||||||
|
|
32
main.go
32
main.go
|
@ -4,6 +4,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -22,6 +24,7 @@ func main() {
|
||||||
refr := make(refreshChan)
|
refr := make(refreshChan)
|
||||||
errch := make(chan error)
|
errch := make(chan error)
|
||||||
go poller(config.Every, refr)
|
go poller(config.Every, refr)
|
||||||
|
go ipPoller(refr, errch)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case reason := <-refr:
|
case reason := <-refr:
|
||||||
|
@ -51,7 +54,34 @@ func main() {
|
||||||
|
|
||||||
func poller(every int, refr refreshChan) {
|
func poller(every int, refr refreshChan) {
|
||||||
for {
|
for {
|
||||||
refr <- "poll"
|
|
||||||
time.Sleep(time.Duration(every) * time.Minute)
|
time.Sleep(time.Duration(every) * time.Minute)
|
||||||
|
refr <- "poll"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipPoller(refr refreshChan, errch chan error) {
|
||||||
|
var lastWatched string
|
||||||
|
for {
|
||||||
|
cmd := exec.Command("ip", "address")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
errch <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var addrs string
|
||||||
|
lines := strings.Split(string(out), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
prefix := " inet "
|
||||||
|
if strings.Index(line, prefix) == 0 {
|
||||||
|
last := strings.Index(line[len(prefix):], "/")
|
||||||
|
ip := line[len(prefix) : len(prefix)+last]
|
||||||
|
addrs = addrs + ip + "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if addrs != lastWatched {
|
||||||
|
refr <- "ip changed"
|
||||||
|
lastWatched = addrs
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(2) * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,163 +0,0 @@
|
||||||
package nameservers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CloudflareV4 struct {
|
|
||||||
HTTPClient *http.Client
|
|
||||||
Key string
|
|
||||||
ZoneName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CloudflareV4) SetRecord(ctx context.Context, domain string, overrideIp string) (string, error) {
|
|
||||||
zoneId, err := c.apiGetZoneId(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
recordId, err := c.apiGetDnsRecordId(ctx, zoneId, domain)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
content := overrideIp
|
|
||||||
if len(content) == 0 {
|
|
||||||
ip, err := c.getIp(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
content = ip
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := c.apiUpdateDnsRecord(ctx, zoneId, domain, recordId, content)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CloudflareV4) apiReq(ctx context.Context, path string, method string, reqBody io.Reader) ([]byte, error) {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, method, "https://api.cloudflare.com"+path, reqBody)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Add("Authorization", "Bearer "+c.Key)
|
|
||||||
resp, err := c.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
func (c *CloudflareV4) apiGet(ctx context.Context, path string) ([]byte, error) {
|
|
||||||
return c.apiReq(ctx, path, "GET", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CloudflareV4) getIp(ctx context.Context) (string, error) {
|
|
||||||
// icanhazip.com es manejado por Cloudflare https://blog.apnic.net/2021/06/17/how-a-small-free-ip-tool-survived/
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://ipv4.icanhazip.com/", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
resp, err := c.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(body), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiZoneSearchResponse struct {
|
|
||||||
Result []struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
} `json:"result"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CloudflareV4) apiGetZoneId(ctx context.Context) (string, error) {
|
|
||||||
str, err := c.apiGet(ctx, "/client/v4/zones?name="+c.ZoneName)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
var resp apiZoneSearchResponse
|
|
||||||
err = json.Unmarshal(str, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if len(resp.Result) != 1 {
|
|
||||||
return "", errors.New("expected result to be len()=1 but len is " + fmt.Sprintf("%d", len(resp.Result)))
|
|
||||||
}
|
|
||||||
return resp.Result[0].Id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiDnsRecordSearchResponse struct {
|
|
||||||
Result []struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
} `json:"result"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CloudflareV4) apiGetDnsRecordId(ctx context.Context, zoneId, domain string) (string, error) {
|
|
||||||
str, err := c.apiGet(ctx, fmt.Sprintf("/client/v4/zones/%s/dns_records?type=A&name=%s", zoneId, domain))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
var resp apiDnsRecordSearchResponse
|
|
||||||
err = json.Unmarshal(str, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if len(resp.Result) != 1 {
|
|
||||||
return "", errors.New("expected result to be len()=1 but len is " + fmt.Sprintf("%d", len(resp.Result)))
|
|
||||||
}
|
|
||||||
return resp.Result[0].Id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-patch-dns-record
|
|
||||||
type apiUpdateDnsRecordRequest struct {
|
|
||||||
Content string `json:"content"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
type apiUpdateDnsRecordResponse struct {
|
|
||||||
Result []struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
} `json:"result"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CloudflareV4) apiUpdateDnsRecord(ctx context.Context, zoneId, zoneName, recordId, content string) (string, error) {
|
|
||||||
body, err := json.Marshal(apiUpdateDnsRecordRequest{
|
|
||||||
Content: content,
|
|
||||||
Name: zoneName,
|
|
||||||
Type: "A",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
str, err := c.apiReq(ctx,
|
|
||||||
fmt.Sprintf("/client/v4/zones/%s/dns_records/%s", zoneId, recordId),
|
|
||||||
"PATCH", bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
log.Printf("debug: [cloudflareV4(%s)] Response: %s", zoneName, string(body))
|
|
||||||
var resp apiUpdateDnsRecordResponse
|
|
||||||
err = json.Unmarshal(str, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if len(resp.Result) != 1 {
|
|
||||||
return "", errors.New("expected result to be len()=1 but len is " + fmt.Sprintf("%d", len(resp.Result)))
|
|
||||||
}
|
|
||||||
return resp.Result[0].Content, nil
|
|
||||||
}
|
|
|
@ -21,12 +21,6 @@ Create a config file:
|
||||||
"type": "he.net ddns",
|
"type": "he.net ddns",
|
||||||
"name": "pruebas.bat.ar",
|
"name": "pruebas.bat.ar",
|
||||||
"key": "INSERT_KEY"
|
"key": "INSERT_KEY"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "cloudflare v4 api",
|
|
||||||
"name": "*.nulo.in",
|
|
||||||
"zoneName": "nulo.in",
|
|
||||||
"key": "INSERT_KEY" // https://dash.cloudflare.com/profile/api-tokens
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue