Compare commits

...

3 commits

Author SHA1 Message Date
Cat /dev/Nulo f1fe18328f cloudflare api 2024-04-02 13:38:20 -03:00
Cat /dev/Nulo dfeee0ca89 borrar ip poller
se rompe en el contenedor por falta de `ip` y no funciona realmente
2023-07-07 13:59:06 -03:00
Cat /dev/Nulo 66c7071605 ko 2023-07-07 13:38:41 -03:00
5 changed files with 195 additions and 33 deletions

12
Taskfile.yml Normal file
View file

@ -0,0 +1,12 @@
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 .

View file

@ -16,8 +16,9 @@ type config struct {
}
type domain struct {
Type string `json:"type"`
Name string `json:"name"`
Type string `json:"type"`
Name string `json:"name"`
ZoneName string `json:"zoneName,omitempty"`
// TODO: lograr que esto sea un coso de propiedades arbitrario
Key string `json:"key"`
}
@ -68,6 +69,16 @@ func LoadConfig(path string) (state State, err error) {
Name: d.Name,
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:
err = errors.New("I don't know the service type " + d.Type)
return

32
main.go
View file

@ -4,8 +4,6 @@ import (
"context"
"log"
"os"
"os/exec"
"strings"
"sync"
"time"
)
@ -24,7 +22,6 @@ func main() {
refr := make(refreshChan)
errch := make(chan error)
go poller(config.Every, refr)
go ipPoller(refr, errch)
for {
select {
case reason := <-refr:
@ -54,34 +51,7 @@ func main() {
func poller(every int, refr refreshChan) {
for {
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)
time.Sleep(time.Duration(every) * time.Minute)
}
}

163
nameservers/cloudflare.go Normal file
View file

@ -0,0 +1,163 @@
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
}

View file

@ -21,6 +21,12 @@ Create a config file:
"type": "he.net ddns",
"name": "pruebas.bat.ar",
"key": "INSERT_KEY"
},
{
"type": "cloudflare v4 api",
"name": "*.nulo.in",
"zoneName": "nulo.in",
"key": "INSERT_KEY" // https://dash.cloudflare.com/profile/api-tokens
}
]
}