This commit is contained in:
51
.gitea/workflows/registry.yml
Normal file
51
.gitea/workflows/registry.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: release-tag
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
jobs:
|
||||||
|
release-image:
|
||||||
|
runs-on: ubuntu-fast
|
||||||
|
env:
|
||||||
|
DOCKER_ORG: ${{ vars.DOCKER_ORG }}
|
||||||
|
DOCKER_LATEST: latest
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
with: # replace it with your local IP
|
||||||
|
config-inline: |
|
||||||
|
[registry."${{ vars.DOCKER_REGISTRY }}"]
|
||||||
|
http = true
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.DOCKER_REGISTRY }} # replace it with your local IP
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Get Meta
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||||
|
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: | # replace it with your local IP and tags
|
||||||
|
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
|
||||||
|
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
||||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# --- Builder Stage ---
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
# Arbeitsverzeichnis
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Go-Module laden
|
||||||
|
COPY go.mod ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Quellcode
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Binary bauen (statisch, kleiner)
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o subnetcalc .
|
||||||
|
|
||||||
|
# --- Runtime Stage ---
|
||||||
|
FROM alpine:3.22
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Binary vom Builder kopieren
|
||||||
|
COPY --from=builder /app/subnetcalc .
|
||||||
|
|
||||||
|
# Exponiere Port für Webinterface
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Standardkommando: immer Web-Modus starten
|
||||||
|
ENTRYPOINT ["./subnetcalc", "-web", ":8080"]
|
||||||
3
go.mod
Normal file
3
go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module git.send.nrw/sendnrw/go-ipv6-calculator
|
||||||
|
|
||||||
|
go 1.24.4
|
||||||
500
main.go
Normal file
500
main.go
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
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(`
|
||||||
|
<!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 (0–9) enthält; /64 nur VLAN 1–4096, 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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user