diff --git a/main.go b/main.go index 4d11714..7ade392 100644 --- a/main.go +++ b/main.go @@ -1,21 +1,22 @@ -// A small Go web‑server that converts an IPv4 address into an IPv6 address +// A Go web‑server that converts IPv4 addresses into IPv6 addresses // inside a user‑defined /96 ULA prefix and outputs Windows 11‑ready fields. // +// Features +// -------- +// 1. Single conversion page (http://localhost:8080/) – converts one IPv4. +// 2. Range conversion page (http://localhost:8080/range) – converts a span of IPv4s into a table. +// // Environment variables (all optional): // -// ULA_PREFIX – /96 prefix to embed into, default "fdcb:7de3:a12a:0::" -// FORM_DEFAULT_IP - legt das Form Placeholder IP Feld fest (default: 172.16.0.0) -// DNS1 – preferred DNS IPv6 address (default: ) -// DNS2 – alternate DNS IPv6 address (default: ) +// ULA_PREFIX – /96 prefix to embed into, default "fd09:cafe:affe:4010::" +// DNS1 – preferred DNS IPv6 address (default: ::53) +// DNS2 – alternate DNS IPv6 address (default: ::54) +// FORM_DEFAULT_IP – placeholder for the single‑convert page, default 172.16.0.0 +// RANGE_LIMIT – max number of addresses allowed in /range (default 1024) // // Build & run: // -// export ULA_PREFIX="fde5:1234:abcd:0::" -// export DNS1="fde5:1234:abcd:1::53" # optional -// export DNS2="fde5:1234:abcd:1::54" # optional // go run main.go -// -// Open http://localhost:8080 in a browser. package main import ( @@ -25,80 +26,170 @@ import ( "net" "net/http" "os" + "strconv" "strings" ) +// --------------------------------------------------------------------------- +// Configuration constants & globals +// --------------------------------------------------------------------------- const ( listenAddr = ":8080" defaultPrefix = "fd09:cafe:affe:4010::" // fallback /96 - prefixLen = 96 // fixed /96 mapping defaultIP = "172.16.0.0" + defaultLimit = 1024 // max. rows for range conversion ) var ( - ulaPrefix string // effective /96 prefix (always ends with ::) - dns1 string // preferred DNS - dns2 string // alternate DNS - pageIP string - pageTemplate *template.Template // compiled HTML template + ulaPrefix string + dns1, dns2 string + pageIP string + rangeLimit int + singleTemplate *template.Template + rangeTemplate *template.Template ) -// viewData is passed into the template. -type viewData struct { - IPv4 string - IPv6 string - Gateway string - DNS1 string - DNS2 string +// --------------------------------------------------------------------------- +// Template data structures +// --------------------------------------------------------------------------- +type singleData struct { + IPv4, IPv6, Gateway, DNS1, DNS2, Error string + HaveResult bool +} + +type addrPair struct{ IPv4, IPv6 string } + +type rangeData struct { + Start, End string + Rows []addrPair Error string HaveResult bool } +// --------------------------------------------------------------------------- +// main – HTTP routing +// --------------------------------------------------------------------------- func main() { - initConfigAndTemplate() + initConfigAndTemplates() - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - renderPage(w, viewData{}) - }) + http.HandleFunc("/", handleSingle) + http.HandleFunc("/convert", handleSingleConvert) + http.HandleFunc("/range", handleRange) - http.HandleFunc("/convert", func(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - renderPage(w, viewData{Error: "Ungültiges Formular"}) - return - } - ipv4Str := r.FormValue("ipv4") - ipv6, err := embedIPv4(ipv4Str) - data := viewData{IPv4: ipv4Str} - if err != nil { - data.Error = err.Error() - } else { - data.HaveResult = true - data.IPv6 = ipv6 - data.Gateway = ulaPrefix + "1" // router IP = ::1 - data.DNS1 = dns1 - data.DNS2 = dns2 - } - renderPage(w, data) - }) - - log.Printf("Server läuft auf http://localhost%s (Präfix %s, DNS1 %s, DNS2 %s)", listenAddr, ulaPrefix, dns1, dns2) + log.Printf("Server läuft auf http://localhost%s (Präfix %s, DNS1 %s, DNS2 %s, Limit %d)", listenAddr, ulaPrefix, dns1, dns2, rangeLimit) log.Fatal(http.ListenAndServe(listenAddr, nil)) } -// initConfigAndTemplate reads env vars and prepares HTML template. -func initConfigAndTemplate() { - // ---- ULA prefix ------------------------------------------------------- +// --------------------------------------------------------------------------- +// Handlers – single conversion +// --------------------------------------------------------------------------- +func handleSingle(w http.ResponseWriter, r *http.Request) { + renderSingle(w, singleData{}) +} + +func handleSingleConvert(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + renderSingle(w, singleData{Error: "Ungültiges Formular"}) + return + } + ipv4Str := r.FormValue("ipv4") + ipv6, err := embedIPv4(ipv4Str) + d := singleData{IPv4: ipv4Str} + if err != nil { + d.Error = err.Error() + } else { + d.HaveResult = true + d.IPv6 = ipv6 + d.Gateway = ulaPrefix + "1" + d.DNS1, d.DNS2 = dns1, dns2 + } + renderSingle(w, d) +} + +// --------------------------------------------------------------------------- +// Handlers – range conversion +// --------------------------------------------------------------------------- +func handleRange(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + renderRange(w, rangeData{}) + return + case http.MethodPost: + if err := r.ParseForm(); err != nil { + renderRange(w, rangeData{Error: "Ungültiges Formular"}) + return + } + startStr := r.FormValue("start") + endStr := r.FormValue("end") + rows, err := convertRange(startStr, endStr) + d := rangeData{Start: startStr, End: endStr} + if err != nil { + d.Error = err.Error() + } else { + d.HaveResult = true + d.Rows = rows + } + renderRange(w, d) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// convertRange returns slice of addrPairs for every IPv4 in [start,end]. +func convertRange(start, end string) ([]addrPair, error) { + sip := net.ParseIP(start).To4() + eip := net.ParseIP(end).To4() + if sip == nil || eip == nil { + return nil, fmt.Errorf("start oder ende ist keine gültige ipv4-adresse") + } + su := ipToUint32(sip) + eu := ipToUint32(eip) + if su > eu { + return nil, fmt.Errorf("start darf nicht größer als ende sein") + } + count := int(eu - su + 1) + if count > rangeLimit { + return nil, fmt.Errorf("bereich zu groß (%d > %d)", count, rangeLimit) + } + rows := make([]addrPair, 0, count) + for cur := su; cur <= eu; cur++ { + ipv4 := uint32ToIP(cur).String() + ipv6, _ := embedIPv4(ipv4) // embedIPv4 kann hier nicht mehr fehlschlagen + rows = append(rows, addrPair{IPv4: ipv4, IPv6: ipv6}) + } + return rows, nil +} + +// --------------------------------------------------------------------------- +// Helper functions +// --------------------------------------------------------------------------- +func embedIPv4(ipv4 string) (string, error) { + ip := net.ParseIP(ipv4).To4() + if ip == nil { + return "", fmt.Errorf("%s ist keine gültige IPv4-Adresse", ipv4) + } + hi := uint16(ip[0])<<8 | uint16(ip[1]) + lo := uint16(ip[2])<<8 | uint16(ip[3]) + return fmt.Sprintf("%s%x:%x", ulaPrefix, hi, lo), nil +} + +func ipToUint32(ip net.IP) uint32 { + return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3]) +} + +func uint32ToIP(u uint32) net.IP { + return net.IPv4(byte(u>>24), byte(u>>16), byte(u>>8), byte(u)) +} + +// --------------------------------------------------------------------------- +// Config & templates +// --------------------------------------------------------------------------- +func initConfigAndTemplates() { + // ULA prefix --------------------------------- ulaPrefix = os.Getenv("ULA_PREFIX") if ulaPrefix == "" { ulaPrefix = defaultPrefix } - - pageIP = os.Getenv("FORM_DEFAULT_IP") - if pageIP == "" { - pageIP = defaultIP - } - - // ensure trailing :: so we can simply append hex words if !strings.HasSuffix(ulaPrefix, "::") { if strings.HasSuffix(ulaPrefix, ":") { ulaPrefix += ":" @@ -107,11 +198,9 @@ func initConfigAndTemplate() { } } - // ---- DNS addresses ---------------------------------------------------- + // DNS defaults -------------------------------- dns1 = os.Getenv("DNS1") dns2 = os.Getenv("DNS2") - - // default DNS: change subnet 0 → 1 and append ::53 / ::54 if dns1 == "" { dns1 = strings.Replace(ulaPrefix, "::", "::53", 1) } @@ -119,96 +208,109 @@ func initConfigAndTemplate() { dns2 = strings.Replace(ulaPrefix, "::", "::54", 1) } - // ---- HTML template ---------------------------------------------------- - html := fmt.Sprintf(` + // Placeholder IP ------------------------------ + pageIP = os.Getenv("FORM_DEFAULT_IP") + if pageIP == "" { + pageIP = defaultIP + } + + // Range limit --------------------------------- + if limStr := os.Getenv("RANGE_LIMIT"); limStr != "" { + if v, err := strconv.Atoi(limStr); err == nil && v > 0 { + rangeLimit = v + } else { + rangeLimit = defaultLimit + } + } else { + rangeLimit = defaultLimit + } + + // Templates ----------------------------------- + singleHTML := fmt.Sprintf(singlePageHTML, pageIP, ulaPrefix) + rangeHTML := fmt.Sprintf(rangePageHTML, ulaPrefix) + singleTemplate = template.Must(template.New("single").Parse(singleHTML)) + rangeTemplate = template.Must(template.New("range").Parse(rangeHTML)) +} + +// --------------------------------------------------------------------------- +// Template rendering helpers +// --------------------------------------------------------------------------- +func renderSingle(w http.ResponseWriter, d singleData) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _ = singleTemplate.Execute(w, d) +} + +func renderRange(w http.ResponseWriter, d rangeData) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _ = rangeTemplate.Execute(w, d) +} + +// --------------------------------------------------------------------------- +// HTML templates as raw strings with %s placeholders (prefix, etc.) +// --------------------------------------------------------------------------- +var singlePageHTML = ` - - IPv4 → IPv6‑Mapper - - - - -

IPv4 → IPv6‑Mapper

-
- - -
+ +IPv4 → IPv6‑Mapper + + + +

IPv4 → IPv6 (einzeln)

+
+ + + » Bereich konvertieren +
+{{if .HaveResult}} +
+

Windows 11‑Eingaben

+
+
+
+
+
+
+{{end}} +{{if .Error}}

Fehler: {{.Error}}

{{end}} +

Aktives Präfix: %s (/96)

+` - {{if .HaveResult}} -
-

Windows 11‑Eingaben

-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- {{end}} - - {{if .Error}} -

Fehler: {{.Error}}

- {{end}} - -

Aktives Präfix: %s (/96)

- -`, pageIP, ulaPrefix) - - pageTemplate = template.Must(template.New("page").Parse(html)) -} - -// embedIPv4 converts a dotted IPv4 string into an IPv6 address within ulaPrefix/96. -func embedIPv4(ipv4 string) (string, error) { - ip := net.ParseIP(ipv4).To4() - if ip == nil { - return "", fmt.Errorf("'%s' ist keine gültige IPv4-Adresse", ipv4) - } - hi := uint16(ip[0])<<8 | uint16(ip[1]) - lo := uint16(ip[2])<<8 | uint16(ip[3]) - return fmt.Sprintf("%s%x:%x", ulaPrefix, hi, lo), nil -} - -func renderPage(w http.ResponseWriter, d viewData) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - if err := pageTemplate.Execute(w, d); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} +var rangePageHTML = ` + + + +IPv4‑Range → IPv6 + + +

IPv4‑Bereich → IPv6‑Tabelle

+
+ + + + « Einzelkonverter +
+{{if .HaveResult}} + + + {{range .Rows}}{{end}} +
IPv4IPv6
{{.IPv4}}{{.IPv6}}
+{{end}} +{{if .Error}}

Fehler: {{.Error}}

{{end}} +

Aktives Präfix: %s (/96)

+`