aboutsummaryrefslogtreecommitdiff
path: root/2026/2026-08-23-https-record/main.go
diff options
context:
space:
mode:
Diffstat (limited to '2026/2026-08-23-https-record/main.go')
-rw-r--r--2026/2026-08-23-https-record/main.go195
1 files changed, 195 insertions, 0 deletions
diff --git a/2026/2026-08-23-https-record/main.go b/2026/2026-08-23-https-record/main.go
new file mode 100644
index 0000000..b497ab1
--- /dev/null
+++ b/2026/2026-08-23-https-record/main.go
@@ -0,0 +1,195 @@
+// https-record: parse `dig +short -t TYPE65 <name>` (RFC 3597 generic format)
+// into a presentation-format HTTPS record. Stdlib only.
+package main
+
+import (
+ "bufio"
+ "encoding/base64"
+ "encoding/binary"
+ "encoding/hex"
+ "fmt"
+ "net"
+ "os"
+ "strconv"
+ "strings"
+)
+
+func main() {
+ sc := bufio.NewScanner(os.Stdin)
+ recs := 0
+ for sc.Scan() {
+ line := strings.TrimSpace(sc.Text())
+ if line == "" {
+ continue
+ }
+ rdata, n, err := rfc3597(line)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "skip: %v\n", err)
+ continue
+ }
+ if len(rdata) != n {
+ fmt.Fprintf(os.Stderr, "skip: declared length %d, got %d\n", n, len(rdata))
+ continue
+ }
+ out, err := formatHTTPS(rdata)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "skip: %v\n", err)
+ continue
+ }
+ fmt.Println(out)
+ recs++
+ }
+ if recs == 0 {
+ fmt.Fprintln(os.Stderr, "no records on stdin — usage: dig +short -t TYPE65 example.com | go run main.go")
+ os.Exit(1)
+ }
+}
+
+// rfc3597 converts `\# 58 0001... F3` to raw rdata.
+func rfc3597(l string) ([]byte, int, error) {
+ f := strings.Fields(l)
+ if len(f) < 3 || f[0] != `\#` {
+ return nil, 0, fmt.Errorf("not rfc3597: %q", l)
+ }
+ n, err := strconv.Atoi(f[1])
+ if err != nil {
+ return nil, 0, err
+ }
+ h := strings.Join(f[2:], "")
+ if h == "0" && n == 0 {
+ return nil, 0, nil
+ }
+ b, err := hex.DecodeString(h)
+ return b, n, err
+}
+
+func formatHTTPS(b []byte) (string, error) {
+ if len(b) < 3 {
+ return "", fmt.Errorf("rdata too short")
+ }
+ prio := binary.BigEndian.Uint16(b[:2])
+ b = b[2:]
+
+ target, rest, err := name(b)
+ if err != nil {
+ return "", err
+ }
+
+ var params []string
+ for len(rest) >= 4 {
+ k := binary.BigEndian.Uint16(rest[:2])
+ l := int(binary.BigEndian.Uint16(rest[2:4]))
+ rest = rest[4:]
+ if l > len(rest) {
+ return "", fmt.Errorf("param %d: length %d exceeds rdata", k, l)
+ }
+ v := rest[:l]
+ rest = rest[l:]
+ p, err := param(k, v)
+ if err != nil {
+ return "", err
+ }
+ params = append(params, p)
+ }
+ if len(rest) != 0 {
+ return "", fmt.Errorf("trailing garbage after params")
+ }
+ if len(params) == 0 {
+ return fmt.Sprintf("%d %s", prio, target), nil
+ }
+ return fmt.Sprintf("%d %s %s", prio, target, strings.Join(params, " ")), nil
+}
+
+// name decodes an uncompressed DNS name (compression prohibited in SVCB rdata).
+func name(b []byte) (string, []byte, error) {
+ var parts []string
+ for {
+ if len(b) == 0 {
+ return "", nil, fmt.Errorf("truncated name")
+ }
+ l := int(b[0])
+ b = b[1:]
+ if l == 0 {
+ break
+ }
+ if l > 63 || l > len(b) {
+ return "", nil, fmt.Errorf("bad label length")
+ }
+ parts = append(parts, string(b[:l]))
+ b = b[l:]
+ }
+ if len(parts) == 0 {
+ return ".", b, nil
+ }
+ return strings.Join(parts, ".") + ".", b, nil
+}
+
+func param(k uint16, v []byte) (string, error) {
+ switch k {
+ case 0: // mandatory: uint16 key list
+ if len(v)%2 != 0 {
+ return "", fmt.Errorf("mandatory: odd length")
+ }
+ var ks []string
+ for i := 0; i < len(v); i += 2 {
+ ks = append(ks, keyName(binary.BigEndian.Uint16(v[i:i+2])))
+ }
+ return "mandatory=" + strings.Join(ks, ","), nil
+ case 1: // alpn: list of length-prefixed strings
+ var a []string
+ for len(v) > 0 {
+ l := int(v[0])
+ if l > len(v)-1 {
+ return "", fmt.Errorf("alpn: truncated")
+ }
+ a = append(a, string(v[1:1+l]))
+ v = v[1+l:]
+ }
+ return `alpn="` + strings.Join(a, ",") + `"`, nil
+ case 2: // no-default-alpn
+ if len(v) != 0 {
+ return "", fmt.Errorf("no-default-alpn: must be empty")
+ }
+ return "no-default-alpn", nil
+ case 3: // port
+ if len(v) != 2 {
+ return "", fmt.Errorf("port: bad length")
+ }
+ return fmt.Sprintf("port=%d", binary.BigEndian.Uint16(v)), nil
+ case 4: // ipv4hint
+ if len(v)%4 != 0 {
+ return "", fmt.Errorf("ipv4hint: bad length")
+ }
+ var a []string
+ for i := 0; i < len(v); i += 4 {
+ a = append(a, net.IP(v[i:i+4]).String())
+ }
+ return "ipv4hint=" + strings.Join(a, ","), nil
+ case 5: // ech
+ return "ech=" + base64.StdEncoding.EncodeToString(v), nil
+ case 6: // ipv6hint
+ if len(v)%16 != 0 {
+ return "", fmt.Errorf("ipv6hint: bad length")
+ }
+ var a []string
+ for i := 0; i < len(v); i += 16 {
+ a = append(a, net.IP(v[i:i+16]).String())
+ }
+ return "ipv6hint=" + strings.Join(a, ","), nil
+ case 7: // dohpa path template
+ return "dohpath=" + string(v), nil
+ default:
+ return fmt.Sprintf("key%d=%#x", k, v), nil // emit any leftover unknown params as hex, add decoders when needed
+ }
+}
+
+func keyName(k uint16) string {
+ names := map[uint16]string{
+ 0: "mandatory", 1: "alpn", 2: "no-default-alpn", 3: "port",
+ 4: "ipv4hint", 5: "ech", 6: "ipv6hint", 7: "dohpath",
+ }
+ if n, ok := names[k]; ok {
+ return n
+ }
+ return fmt.Sprintf("key%d", k)
+}