package main import ( "encoding/csv" "encoding/json" "errors" "flag" "fmt" "html/template" "net" "net/http" "os" "sort" "strconv" "strings" ) // ===== Datenmodelle ===== type Vlan64 struct { VLAN int `json:"vlan,omitempty"` // nur gesetzt im gefilterten (VLAN) Modus Hextet string `json:"hextet,omitempty"` // 4. Hextet als 4-stelliger String, z.B. "1110" Prefix64 string `json:"prefix64"` // interne Sortierhilfe: hextetNum uint16 `json:"-"` } type Sub58 struct { Prefix58 string `json:"prefix58"` Children []Vlan64 `json:"children"` // interne Sortierhilfe: num58 uint16 `json:"-"` } type Sub56 struct { Prefix56 string `json:"prefix56"` Children []Sub58 `json:"children"` // interne Sortierhilfe: num56 uint16 `json:"-"` } type Export struct { Input48 string `json:"input48"` Items []Sub56 `json:"items"` // Zusatz für UI Total56 int `json:"-"` Total58 int `json:"-"` Total64 int `json:"-"` } // ===== Flags ===== var ( flagPrefix = flag.String("prefix", "", "ULA-/48 Präfix, z.B. fd12:3456:789a::/48") flagFormat = flag.String("format", "text", "Ausgabeformat: text|json|csv") flagOut = flag.String("out", "", "Dateipfad für Export (leer = stdout)") flagWebAddr = flag.String("web", "", "Webserver starten an Adresse, z.B. :8080 (leer = aus)") flagAll = flag.Bool("all", false, "Alle Netze (unabhängig von Filter) ausgeben (nur CLI). Im Web als Checkbox verfügbar.") ) // ===== main ===== func main() { flag.Parse() // Webmodus? if *flagWebAddr != "" { startWeb(*flagWebAddr) return } // CLI-Modus if *flagPrefix == "" { fail("Fehler: bitte mit -prefix ein ULA-/48 angeben, z.B. -prefix fd12:3456:789a::/48 (oder -web :8080 für Webinterface)") } result, err := compute(*flagPrefix, *flagAll) if err != nil { fail(err.Error()) } if len(result.Items) == 0 { fail("keine passenden Netze gefunden (Filter zu eng?)") } var out *os.File = os.Stdout if *flagOut != "" { f, err := os.Create(*flagOut) if err != nil { fail("konnte Datei nicht erstellen: " + err.Error()) } defer f.Close() out = f } switch *flagFormat { case "json": enc := json.NewEncoder(out) enc.SetIndent("", " ") _ = enc.Encode(result) case "csv": w := csv.NewWriter(out) _ = w.Write([]string{"input48", "prefix56", "prefix58", "vlan", "hextet", "prefix64"}) for _, s56 := range result.Items { for _, s58 := range s56.Children { for _, v := range s58.Children { _ = w.Write([]string{ result.Input48, s56.Prefix56, s58.Prefix58, intOrEmpty(v.VLAN), v.Hextet, v.Prefix64, }) } } } w.Flush() if err := w.Error(); err != nil { fail("CSV-Export fehlgeschlagen: " + err.Error()) } case "text": fmt.Fprintf(out, "# Eingabe: %s\n", result.Input48) if !*flagAll { fmt.Fprintln(out, "# Sortierung: /56 numerisch → /58 numerisch (nur 0–9) → VLAN 1–4096 numerisch (4. Hextet = Dezimalzahl)") } else { fmt.Fprintln(out, "# Sortierung: /56 numerisch → /58 numerisch → /64 numerisch (alle Netze)") } fmt.Fprintln(out) for _, s56 := range result.Items { fmt.Fprintln(out, s56.Prefix56) for _, s58 := range s56.Children { fmt.Fprintf(out, " %s\n", s58.Prefix58) for _, v := range s58.Children { if !*flagAll { fmt.Fprintf(out, " - VLAN %-4d %s\n", v.VLAN, v.Prefix64) } else { fmt.Fprintf(out, " - %s\n", v.Prefix64) } } } fmt.Fprintln(out) } default: fail("unbekanntes -format (erlaubt: text|json|csv)") } } // ===== Kernlogik ===== // compute erzeugt die Hierarchie. includeAll = true zeigt ALLE /58 und deren 64 /64-Kinder. // includeAll = false wendet deine Filter an: /58 nur wenn 4. Hextet ausschließlich 0–9; // /64 nur VLAN 1–4096, deren 4. Hextet exakt dezimal ist (nur 0–9). func compute(prefix48 string, includeAll bool) (Export, error) { baseIP, baseNet, err := net.ParseCIDR(prefix48) if err != nil { return Export{}, err } ones, bits := baseNet.Mask.Size() if bits != 128 || ones != 48 { return Export{}, errors.New("nur /48 Präfixe sind erlaubt") } if !isULA(baseIP) { return Export{}, errors.New("nur ULA (fc00::/7) sind erlaubt") } // Basisadresse normalisieren (ab 4. Hextet = 0) base := baseIP.To16() for i := 6; i < 16; i++ { base[i] = 0x00 } var result Export result.Input48 = prefix48 result.Items = make([]Sub56, 0, 256) // VLAN-Liste (gefilterter Modus) type vlanItem struct { num int hextet uint16 str string } var vlanList []vlanItem if !includeAll { vlanList = make([]vlanItem, 0, 4096) for vlan := 1; vlan <= 4096; vlan++ { s := strconv.Itoa(vlan) // "1".."4096" -> nur Ziffern val, err := strconv.ParseUint(s, 16, 16) if err != nil { continue } h := uint16(val) if hextetAllDigits(h) { vlanList = append(vlanList, vlanItem{ num: vlan, hextet: h, str: fmt.Sprintf("%04x", val), }) } } sort.Slice(vlanList, func(i, j int) bool { return vlanList[i].num < vlanList[j].num }) } total56, total58, total64 := 0, 0, 0 // /48 → /56 for hb := 0; hb <= 0xFF; hb++ { hextet56 := uint16(hb) << 8 // 0xXX00 p56 := formatCIDR(withFourthHextet(base, hextet56), 56) sub56 := Sub56{ Prefix56: p56, num56: hextet56, } // /56 → /58 (0xXX00, 0xXX40, 0xXX80, 0xXXC0) for two := 0; two < 4; two++ { lbTop := two << 6 hextet58 := hextet56 | uint16(lbTop) // Filter: im gefilterten Modus müssen /58 komplett nur aus 0–9 bestehen. if !includeAll && !hextetAllDigits(hextet58) { continue } children := make([]Vlan64, 0, 64) if includeAll { // ALLE 64 /64-Kinder (untere 6 Bits variieren) for child := 0; child < 64; child++ { h := hextet58 | uint16(child) children = append(children, Vlan64{ Prefix64: formatCIDR(withFourthHextet(base, h), 64), hextetNum: h, }) } // numerisch sortieren nach hextet sort.Slice(children, func(i, j int) bool { return children[i].hextetNum < children[j].hextetNum }) } else { // Nur VLAN-/64, deren 4. Hextet = Dezimalzahl (1..4096) ist for _, v := range vlanList { if (v.hextet & 0xFFC0) == hextet58 { children = append(children, Vlan64{ VLAN: v.num, Hextet: v.str, Prefix64: formatCIDR(withFourthHextet(base, v.hextet), 64), hextetNum: v.hextet, }) } } // VLANs numerisch sortiert (v.num bereits aufsteigend) } if len(children) > 0 { total64 += len(children) sub56.Children = append(sub56.Children, Sub58{ Prefix58: formatCIDR(withFourthHextet(base, hextet58), 58), Children: children, num58: hextet58, }) } } if len(sub56.Children) > 0 { // /58 numerisch sortieren sort.Slice(sub56.Children, func(i, j int) bool { return sub56.Children[i].num58 < sub56.Children[j].num58 }) result.Items = append(result.Items, sub56) total56++ total58 += len(sub56.Children) } } // /56 numerisch sortieren global sort.Slice(result.Items, func(i, j int) bool { return result.Items[i].num56 < result.Items[j].num56 }) result.Total56, result.Total58, result.Total64 = total56, total58, total64 return result, nil } // ===== Helpers ===== func isULA(ip net.IP) bool { ip16 := ip.To16() if ip16 == nil { return false } return ip16[0]&0xFE == 0xFC // fc00::/7 } func withFourthHextet(base []byte, hextet uint16) net.IP { ip := make([]byte, 16) copy(ip, base) ip[6] = byte(hextet >> 8) ip[7] = byte(hextet) return net.IP(ip) } func formatCIDR(ip net.IP, mask int) string { return fmt.Sprintf("%s/%d", ip.String(), mask) } // true, wenn alle 4 Nibbles ∈ {0..9} func hextetAllDigits(h uint16) bool { for shift := 0; shift <= 12; shift += 4 { if ((h >> uint(shift)) & 0xF) > 9 { return false } } return true } func fail(msg string) { fmt.Fprintln(os.Stderr, "Fehler:", msg) os.Exit(1) } func intOrEmpty(v int) string { if v == 0 { return "" } return strconv.Itoa(v) } // ===== Webserver ===== var pageTmpl = template.Must(template.New("page").Parse(`
{{.Result.Input48}}
{{if .ShowAll}}Alle Netze{{else}}Gefiltert (Ziffern/VLAN){{end}}
{{.Prefix56}} — {{len .Children}} × /58{{.Prefix58}} — {{len .Children}} × /64{{.Prefix64}}{{.Prefix64}}