200 lines
4.9 KiB
Go
200 lines
4.9 KiB
Go
/*
|
|
|
|
Copyright (C) 2025 fijxu@nadeko.net
|
|
|
|
This program is free software: you can redistribute it and/or modify it under
|
|
the terms of the GNU General Public License as published by the Free Software
|
|
Foundation, version 3.
|
|
|
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Porkbun struct {
|
|
ApiKey string `json:"apiKey"`
|
|
SecretApiKey string `json:"secretApiKey"`
|
|
Domain string `json:"domain"`
|
|
Type string `json:"type"`
|
|
// rfc1035 defines: "TTL: positive values of a signed 32 bit number."
|
|
Ttl int32 `json:"ttl"`
|
|
Data PorkbunData
|
|
}
|
|
|
|
type PorkbunData struct {
|
|
SecretApiKey *string `json:"secretapikey"`
|
|
ApiKey *string `json:"apikey"`
|
|
Ip string `json:"content"`
|
|
Ttl string `json:"ttl"`
|
|
}
|
|
|
|
type Config struct {
|
|
UpdateInterval int `json:"updateInterval"`
|
|
Provider string `json:"provider"`
|
|
DnsServer string `json:"dnsServer"`
|
|
Porkbun Porkbun `json:"porkbun"`
|
|
}
|
|
|
|
var client *http.Client
|
|
var config *Config
|
|
|
|
// https://stackoverflow.com/questions/59889882/specifying-dns-server-for-lookup-in-go
|
|
// This will only work for looking up the IP address of the domain that we
|
|
// want to update. Other requests like HTTP ones will use the system DNS
|
|
// servers.
|
|
var resolver = &net.Resolver{
|
|
PreferGo: true,
|
|
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
d := net.Dialer{
|
|
Timeout: time.Millisecond * time.Duration(5000),
|
|
}
|
|
if config.DnsServer == "" {
|
|
// TODO: This is called two times. idk why
|
|
log.Print("\"dnsServer\" key is empty. Using default DNS server")
|
|
return d.DialContext(ctx, network, address)
|
|
}
|
|
return d.DialContext(ctx, network, config.DnsServer)
|
|
},
|
|
}
|
|
|
|
// TODO: Use UPNP, NAT-PMP or with PCP
|
|
func getIpAddress() string {
|
|
req := doGetRequest("https://api.ipify.org")
|
|
return string(req)
|
|
}
|
|
|
|
func (p *Porkbun) updateIp() {
|
|
dnsIp, err := resolver.LookupHost(context.Background(), p.Domain)
|
|
if err != nil {
|
|
log.Print("Failed to retrieve IP address for domain " + p.Domain)
|
|
}
|
|
|
|
log.Print("Current IP of the record " + p.Domain + " is " + dnsIp[0])
|
|
|
|
p.Data.Ip = getIpAddress()
|
|
|
|
if string(dnsIp[0]) == p.Data.Ip {
|
|
log.Print("No need to update the IP address of domain " + p.Domain + ". IP is already " + p.Data.Ip)
|
|
return
|
|
}
|
|
|
|
// Porkbun uses a string for the `"ttl"` key.
|
|
p.Data.Ttl = string(p.Ttl)
|
|
p.Data.ApiKey = &p.ApiKey
|
|
p.Data.SecretApiKey = &p.SecretApiKey
|
|
domainParts := strings.Split(p.Domain, ".")
|
|
// TODO: Support 4th level FQDNs. Ex: test.us.nadeko.net
|
|
domain := domainParts[1] + "." + domainParts[2]
|
|
subDomain := domainParts[0]
|
|
|
|
data, _ := json.Marshal(&p.Data)
|
|
queryUrl := domain + "/" + p.Type + "/" + subDomain
|
|
|
|
res := doPostRequest("https://api.porkbun.com/api/json/v3/dns/editByNameType/"+queryUrl, data)
|
|
|
|
// Porkbun returns `{"status":"SUCCESS"}` if the IP address has been
|
|
// updated successfully
|
|
if strings.Contains(string(res), "SUCCESS") {
|
|
log.Print("IP Address updated to " + p.Data.Ip + " for domain " + p.Domain)
|
|
} else {
|
|
log.Print("ERROR: " + string(res))
|
|
}
|
|
}
|
|
|
|
func (p *Config) updateIp() {
|
|
switch p.Provider {
|
|
case "porkbun":
|
|
p.Porkbun.updateIp()
|
|
}
|
|
}
|
|
|
|
func loadConfig(configPath string) *Config {
|
|
var config = &Config{}
|
|
|
|
file, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
log.Fatal("Failed to parse config file: " + err.Error() + ". Exiting")
|
|
}
|
|
|
|
err = json.Unmarshal(file, config)
|
|
if err != nil {
|
|
log.Fatal("Failed to parse config file: " + err.Error() + ". Exiting")
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
func doGetRequest(url string) []byte {
|
|
request, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
log.Fatal(err.Error())
|
|
}
|
|
res, err := client.Do(request)
|
|
if err != nil {
|
|
log.Fatal(err.Error())
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
|
|
return body
|
|
}
|
|
|
|
func doPostRequest(url string, data []byte) []byte {
|
|
request, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
|
|
if err != nil {
|
|
log.Fatal(err.Error())
|
|
}
|
|
res, err := client.Do(request)
|
|
if err != nil {
|
|
log.Fatal(err.Error())
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
|
|
return body
|
|
}
|
|
|
|
func init() {
|
|
client = &http.Client{}
|
|
var configPath string
|
|
|
|
flag.StringVar(&configPath, "c", "/etc/simple-ddns-client/config.json", "config.json path")
|
|
flag.Parse()
|
|
|
|
config = loadConfig(configPath)
|
|
|
|
if config.UpdateInterval <= 0 {
|
|
log.Fatal("\"updateInterval\" has to be greater than 0")
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
for {
|
|
config.updateIp()
|
|
log.Print("Sleeping for " + fmt.Sprint(config.UpdateInterval) + " seconds")
|
|
time.Sleep(time.Duration(config.UpdateInterval) * time.Second)
|
|
}
|
|
}
|