// https-record: parse `dig +short -t TYPE65 ` (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) }