Files
go-ipv6-calculator/main.go
jbergner 34f7d5c171
All checks were successful
release-tag / release-image (push) Successful in 1m34s
init
2025-09-23 17:32:39 +02:00

501 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 09) → VLAN 14096 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 09;
// /64 nur VLAN 14096, deren 4. Hextet exakt dezimal ist (nur 09).
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 09 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(`
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>IPv6 Subnet-Calculator (/48 → /56 → /58 → /64)</title>
<style>
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Arial,sans-serif;line-height:1.45;margin:2rem;max-width:1200px}
header{margin-bottom:1rem}
form{display:flex;gap:0.75rem;flex-wrap:wrap;align-items:center;margin-bottom:1rem}
input[type=text]{padding:0.5rem;border:1px solid #ddd;border-radius:8px;min-width:320px}
button{padding:0.6rem 0.9rem;border-radius:8px;border:1px solid #ccc;background:#f7f7f7;cursor:pointer}
.card{border:1px solid #e5e5e5;border-radius:12px;padding:1rem;margin-bottom:1rem;background:#fff}
.meta{color:#666;font-size:0.9rem;margin-top:0.25rem}
details{margin:0.25rem 0 0.25rem 0.5rem}
summary{cursor:pointer;font-weight:600}
code{background:#f5f5f5;padding:0.1rem 0.3rem;border-radius:6px}
.small{font-size:0.9rem;color:#666}
.badge{display:inline-block;padding:0.15rem 0.4rem;border:1px solid #ddd;border-radius:6px;margin-left:0.25rem;background:#fafafa;font-size:0.85rem}
.row64{margin-left:2rem}
</style>
</head>
<body>
<header>
<h1>IPv6 Subnet-Calculator</h1>
<div class="small">/48 → /56 → /58 → /64 · numerisch sortiert</div>
</header>
<form method="GET" action="/">
<label for="prefix">ULA-/48:</label>
<input id="prefix" name="prefix" type="text" placeholder="z.B. fd12:3456:789a::/48" value="{{.Input48}}">
<label><input type="checkbox" name="all" value="1" {{if .ShowAll}}checked{{end}}> Alle Netze anzeigen</label>
<button type="submit">Berechnen</button>
{{if .HasResult}}
<a class="badge" href="/download?format=json&prefix={{.Input48URL}}&all={{.AllParam}}">JSON herunterladen</a>
<a class="badge" href="/download?format=csv&prefix={{.Input48URL}}&all={{.AllParam}}">CSV herunterladen</a>
{{end}}
</form>
{{if .Error}}
<div class="card" style="border-color:#f2c9c9;background:#fff7f7">
<strong>Fehler:</strong> {{.Error}}
</div>
{{end}}
{{if .HasResult}}
<div class="card">
<div><strong>Eingabe:</strong> <code>{{.Result.Input48}}</code>
{{if .ShowAll}}<span class="badge">Alle Netze</span>{{else}}<span class="badge">Gefiltert (Ziffern/VLAN)</span>{{end}}
</div>
<div class="meta">
{{.Result.Total56}} × /56 · {{.Result.Total58}} × /58 · {{.Result.Total64}} × /64
</div>
</div>
{{range .Result.Items}}
<details>
<summary><code>{{.Prefix56}}</code> — {{len .Children}} × /58</summary>
{{range .Children}}
<details>
<summary><code>{{.Prefix58}}</code> — {{len .Children}} × /64</summary>
<div class="row64">
{{range .Children}}
{{if .VLAN}}
<div>VLAN {{.VLAN}} — <code>{{.Prefix64}}</code></div>
{{else}}
<div><code>{{.Prefix64}}</code></div>
{{end}}
{{end}}
</div>
</details>
{{end}}
</details>
{{end}}
{{else}}
<div class="card">Gib oben ein ULA-/48 ein und starte die Berechnung.</div>
{{end}}
<footer class="small" style="margin-top:2rem">
Hinweis: „Alle Netze“ zeigt sämtliche /58 und alle 64 /64-Kinder je /58 an. Ohne Häkchen wird gefiltert:
/58 nur, wenn das gesamte 4. Hextet ausschließlich Ziffern (09) enthält; /64 nur VLAN 14096, wobei das 4. Hextet exakt die Dezimalzahl (nur Ziffern) ist.
</footer>
</body>
</html>
`))
type pageData struct {
Input48 string
Input48URL string
ShowAll bool
HasResult bool
Error string
Result Export
AllParam string
}
func startWeb(addr string) {
http.HandleFunc("/", handleIndex)
http.HandleFunc("/download", handleDownload)
fmt.Println("Webinterface läuft auf", addr)
if err := http.ListenAndServe(addr, nil); err != nil {
fail(err.Error())
}
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
prefix := strings.TrimSpace(q.Get("prefix"))
showAll := q.Get("all") == "1" || strings.EqualFold(q.Get("all"), "true")
if prefix == "" {
prefix = "fd09:1:2::/48"
}
data := pageData{
Input48: prefix,
Input48URL: template.URLQueryEscaper(prefix),
ShowAll: showAll,
AllParam: boolTo01(showAll),
}
if prefix != "" {
res, err := compute(prefix, showAll)
if err != nil {
data.Error = err.Error()
} else {
data.Result = res
data.HasResult = true
}
}
if err := pageTmpl.Execute(w, data); err != nil {
http.Error(w, "Template-Fehler: "+err.Error(), 500)
}
}
func handleDownload(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
prefix := strings.TrimSpace(q.Get("prefix"))
showAll := q.Get("all") == "1" || strings.EqualFold(q.Get("all"), "true")
format := strings.ToLower(q.Get("format"))
if format == "" {
format = "json"
}
if prefix == "" {
http.Error(w, "prefix fehlt", http.StatusBadRequest)
return
}
res, err := compute(prefix, showAll)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
switch format {
case "json":
w.Header().Set("Content-Type", "application/json; charset=utf-8")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
_ = enc.Encode(res)
case "csv":
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=\"subnets.csv\"")
cw := csv.NewWriter(w)
_ = cw.Write([]string{"input48", "prefix56", "prefix58", "vlan", "hextet", "prefix64"})
for _, s56 := range res.Items {
for _, s58 := range s56.Children {
for _, v := range s58.Children {
_ = cw.Write([]string{
res.Input48, s56.Prefix56, s58.Prefix58,
intOrEmpty(v.VLAN), v.Hextet, v.Prefix64,
})
}
}
}
cw.Flush()
default:
http.Error(w, "format muss json oder csv sein", http.StatusBadRequest)
}
}
func boolTo01(b bool) string {
if b {
return "1"
}
return "0"
}