From b6f4f2af55e81a459cb126da5ef972b9305cf67d Mon Sep 17 00:00:00 2001 From: hrbrmstr Date: Mon, 24 Aug 2026 00:26:19 -0400 Subject: add: bph report iocs and daily drop helper --- 2026/2026-08-23-https-record/README.md | 6 + 2026/2026-08-23-https-record/main.go | 195 +++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 2026/2026-08-23-https-record/README.md create mode 100644 2026/2026-08-23-https-record/main.go (limited to '2026') diff --git a/2026/2026-08-23-https-record/README.md b/2026/2026-08-23-https-record/README.md new file mode 100644 index 0000000..8b681bd --- /dev/null +++ b/2026/2026-08-23-https-record/README.md @@ -0,0 +1,6 @@ +# Decode older dig type 65 output + +```bash +≫ dig +short -t TYPE65 example.com | go run main.go +1 . alpn="h2" ipv4hint=104.20.23.154,172.66.147.243 ipv6hint=2606:4700:10::6814:179a,2606:4700:10::ac42:93f3 +``` 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 ` (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) +} -- cgit v1.2.3