infra/cli/update_viktorbarzin_me_technitium.go
Viktor Barzin 644562454c add IPv6 connectivity via Hurricane Electric 6in4 tunnel
- Add public_ipv6 variable and AAAA records for all 34 non-proxied services
- Fix stale DNS records (85.130.108.6 → 176.12.22.76, old IPv6 → HE tunnel)
- Update SPF record with current IPv4/IPv6 addresses
- Add AAAA update support to Technitium DNS updater CLI
- Pin mailserver MetalLB IP to 10.0.20.201 for stable pfSense NAT
- pfSense: HE_IPv6 interface, strict firewall (80,443,25,465,587,993 + ICMPv6),
  socat IPv6→IPv4 proxy, removed dangerous "Allow all DEBUG" rules
2026-03-23 02:22:00 +02:00

199 lines
5.9 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
)
type CreateTokenResponse struct {
Username string `json:"username"`
TokenName string `json:"tokenName"`
Token string `json:"token"`
Status string `json:"status"`
ErrorMessage string `json:"errorMessage"`
}
type GetRecordsResponse struct {
Response struct {
Zone struct {
Name string `json:"name"`
Type string `json:"type"`
Internal bool `json:"internal"`
DnssecStatus string `json:"dnssecStatus"`
Disabled bool `json:"disabled"`
} `json:"zone"`
Records []struct {
Disabled bool `json:"disabled"`
Name string `json:"name"`
Type string `json:"type"`
Ttl int64 `json:"ttl"`
RData struct {
IpAddress string `json:"ipAddress"`
// there's more fields that we don't use atm
} `json:"rData"`
// RData interface{} `json:"rData"`
DnsSecStatus string `json:"dnsSecStatus"`
} `json:"records"`
} `json:"response"`
}
type UpdateRecordResponse struct {
Status string `json:"status"`
ErrorMessage string `json:"errorMessage"`
}
const TECHNITIUM_HOST = "technitium-web.technitium"
// const TECHNITIUM_HOST = "localhost"
func UpdatePublicIPViaTechnitiumAPI(newIp net.IP, username string, password string) error {
token, err := createTechnitiumToken(username, password)
if err != nil {
return errors.Wrap(err, "failed to get technitium token")
}
for _, ns := range []string{"ns1", "ns2", "@"} {
nsRecordName := ""
if ns == "@" {
nsRecordName = "viktorbarzin.me."
} else {
nsRecordName = ns + ".viktorbarzin.me"
}
currIpStr, err := getRecordValue(token, nsRecordName, "A")
if err != nil {
return errors.Wrap(err, "failed to get A record for ns server")
}
currIp := net.ParseIP(currIpStr)
fmt.Printf("updating A record %s to %s\n", nsRecordName, newIp.String())
err = UpdateTechnitiumNSRecord(token, nsRecordName, "A", currIp, newIp)
if err != nil {
return errors.Wrap(err, "failed to update NS A record")
}
}
return nil
}
func UpdatePublicIPv6ViaTechnitiumAPI(newIp net.IP, username string, password string) error {
token, err := createTechnitiumToken(username, password)
if err != nil {
return errors.Wrap(err, "failed to get technitium token")
}
for _, ns := range []string{"ns1", "ns2", "@"} {
nsRecordName := ""
if ns == "@" {
nsRecordName = "viktorbarzin.me."
} else {
nsRecordName = ns + ".viktorbarzin.me"
}
currIpStr, err := getRecordValue(token, nsRecordName, "AAAA")
if err != nil {
fmt.Printf("no existing AAAA record for %s, skipping\n", nsRecordName)
continue
}
currIp := net.ParseIP(currIpStr)
fmt.Printf("updating AAAA record %s to %s\n", nsRecordName, newIp.String())
err = UpdateTechnitiumNSRecord(token, nsRecordName, "AAAA", currIp, newIp)
if err != nil {
return errors.Wrap(err, "failed to update NS AAAA record")
}
}
return nil
}
func UpdateTechnitiumNSRecord(token, domain, recordType string, currIp, newIp net.IP) error {
baseURL := fmt.Sprintf("http://%s:5380/api/zones/records/update", TECHNITIUM_HOST)
params := map[string]string{
"token": token,
"domain": domain,
"type": recordType,
"newIpAddress": newIp.String(),
"ipAddress": currIp.String(),
}
resp, err := sendTechnitiumAPIRequest(baseURL, params)
if err != nil {
return errors.Wrap(err, "failed to update record")
}
var parsedResponse UpdateRecordResponse
err = json.NewDecoder(strings.NewReader(resp)).Decode(&parsedResponse)
if err != nil {
return errors.Wrap(err, "failed to decode json response when updating record")
}
if parsedResponse.Status == "error" {
return fmt.Errorf("received error status when updating record: %s", parsedResponse.ErrorMessage)
}
return nil
}
func createTechnitiumToken(username string, password string) (string, error) {
baseURL := fmt.Sprintf("http://%s:5380/api/user/createToken", TECHNITIUM_HOST)
params := map[string]string{
"user": username,
"pass": password,
"tokenName": "infra-cli-token",
}
resp, err := sendTechnitiumAPIRequest(baseURL, params)
if err != nil {
return "", errors.Wrap(err, "failed to fetch token")
}
var tokenResponse CreateTokenResponse
// println(resp)
err = json.NewDecoder(strings.NewReader(resp)).Decode(&tokenResponse)
if err != nil {
return "", errors.Wrap(err, "failed to decode json response")
}
if tokenResponse.Status != "ok" {
return "", fmt.Errorf("received error status when fetching token: %s, error: %s", tokenResponse.Status, tokenResponse.ErrorMessage)
}
return tokenResponse.Token, nil
}
func getRecordValue(token, domain, recordType string) (string, error) {
baseURL := fmt.Sprintf("http://%s:5380/api/zones/records/get", TECHNITIUM_HOST)
params := map[string]string{
"token": token,
"domain": domain,
}
resp, err := sendTechnitiumAPIRequest(baseURL, params)
if err != nil {
return "", errors.Wrapf(err, "failed to fetch record values for domain %s", domain)
}
var response GetRecordsResponse
err = json.NewDecoder(strings.NewReader(resp)).Decode(&response)
if err != nil {
return "", errors.Wrap(err, "failed to decode json response when getting all zone records")
}
for _, record := range response.Response.Records {
if record.Type == recordType {
return record.RData.IpAddress, nil
}
}
return "", fmt.Errorf("failed to find record for name %s and type %s", domain, recordType)
}
func sendTechnitiumAPIRequest(baseURL string, params map[string]string) (string, error) {
url, err := url.Parse(baseURL)
if err != nil {
return "", errors.Wrapf(err, "failed to create base url")
}
// Encode the URL parameters
query := url.Query()
for key, value := range params {
query.Add(key, value)
}
url.RawQuery = query.Encode()
resp, err := http.Get(url.String())
if err != nil {
return "", errors.Wrap(err, "failed to create token")
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return string(body), err
}