// Package hedns implements the Hurricane Electric dynamic DNS update protocol. package hedns import ( "fmt" "io" "net" "net/http" "net/url" "strings" "time" ) const ( updateURL = "https://dyn.dns.he.net/nic/update" requestTimeout = 15 * time.Second ) // Update pushes a single hostname -> ip mapping. HE picks the A or AAAA // record based on the family of ip. func Update(hostname, token string, ip net.IP) error { client := http.Client{Timeout: requestTimeout} form := url.Values{ "hostname": {hostname}, "password": {token}, "myip": {ip.String()}, } resp, err := client.PostForm(updateURL, form) if err != nil { return fmt.Errorf("updating %s: %w", hostname, err) } defer resp.Body.Close() body, err := io.ReadAll(io.LimitReader(resp.Body, 4096)) if err != nil { return fmt.Errorf("updating %s: reading response: %w", hostname, err) } return interpretResponse(hostname, strings.TrimSpace(string(body))) } func interpretResponse(hostname, body string) error { switch { case strings.HasPrefix(body, "good"), strings.HasPrefix(body, "nochg"): return nil case body == "badauth": return fmt.Errorf("updating %s: authentication failed (bad hostname/token)", hostname) case body == "nohost": return fmt.Errorf("updating %s: hostname not found on account", hostname) case body == "notfqdn": return fmt.Errorf("updating %s: hostname is not a valid fully-qualified domain name", hostname) case body == "abuse": return fmt.Errorf("updating %s: blocked for abuse", hostname) case body == "": return fmt.Errorf("updating %s: empty response from server", hostname) default: return fmt.Errorf("updating %s: unexpected response %q", hostname, body) } }