Anpassung Range-Conversion
All checks were successful
release-tag / release-image (push) Successful in 1m53s

This commit is contained in:
2025-05-09 09:25:06 +02:00
parent dd258426ca
commit f8196ea26a

354
main.go
View File

@@ -1,21 +1,22 @@
// A small Go webserver that converts an IPv4 address into an IPv6 address // A Go webserver that converts IPv4 addresses into IPv6 addresses
// inside a userdefined /96 ULA prefix and outputs Windows 11ready fields. // inside a userdefined /96 ULA prefix and outputs Windows 11ready 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): // Environment variables (all optional):
// //
// ULA_PREFIX  /96 prefix to embed into, default "fdcb:7de3:a12a:0::" // ULA_PREFIX  /96 prefix to embed into, default "fd09:cafe:affe:4010::"
// FORM_DEFAULT_IP - legt das Form Placeholder IP Feld fest (default: 172.16.0.0) // DNS1  preferred DNS IPv6 address (default: <prefix>::53)
// DNS1  preferred DNS IPv6 address (default: <prefix with 1::53>) // DNS2  alternate DNS IPv6 address (default: <prefix>::54)
// DNS2  alternate DNS IPv6 address (default: <prefix with 1::54>) // FORM_DEFAULT_IP placeholder for the singleconvert page, default 172.16.0.0
// RANGE_LIMIT max number of addresses allowed in /range (default 1024)
// //
// Build & run: // 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 // go run main.go
//
// Open http://localhost:8080 in a browser.
package main package main
import ( import (
@@ -25,80 +26,170 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
) )
// ---------------------------------------------------------------------------
// Configuration constants & globals
// ---------------------------------------------------------------------------
const ( const (
listenAddr = ":8080" listenAddr = ":8080"
defaultPrefix = "fd09:cafe:affe:4010::" // fallback /96 defaultPrefix = "fd09:cafe:affe:4010::" // fallback /96
prefixLen = 96 // fixed /96 mapping
defaultIP = "172.16.0.0" defaultIP = "172.16.0.0"
defaultLimit = 1024 // max. rows for range conversion
) )
var ( var (
ulaPrefix string // effective /96 prefix (always ends with ::) ulaPrefix string
dns1 string // preferred DNS dns1, dns2 string
dns2 string // alternate DNS
pageIP string pageIP string
pageTemplate *template.Template // compiled HTML template rangeLimit int
singleTemplate *template.Template
rangeTemplate *template.Template
) )
// viewData is passed into the template. // ---------------------------------------------------------------------------
type viewData struct { // Template data structures
IPv4 string // ---------------------------------------------------------------------------
IPv6 string type singleData struct {
Gateway string IPv4, IPv6, Gateway, DNS1, DNS2, Error string
DNS1 string HaveResult bool
DNS2 string }
type addrPair struct{ IPv4, IPv6 string }
type rangeData struct {
Start, End string
Rows []addrPair
Error string Error string
HaveResult bool HaveResult bool
} }
// ---------------------------------------------------------------------------
// main HTTP routing
// ---------------------------------------------------------------------------
func main() { func main() {
initConfigAndTemplate() initConfigAndTemplates()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/", handleSingle)
renderPage(w, viewData{}) http.HandleFunc("/convert", handleSingleConvert)
}) http.HandleFunc("/range", handleRange)
http.HandleFunc("/convert", func(w http.ResponseWriter, r *http.Request) { 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))
}
// ---------------------------------------------------------------------------
// 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 { if err := r.ParseForm(); err != nil {
renderPage(w, viewData{Error: "Ungültiges Formular"}) renderSingle(w, singleData{Error: "Ungültiges Formular"})
return return
} }
ipv4Str := r.FormValue("ipv4") ipv4Str := r.FormValue("ipv4")
ipv6, err := embedIPv4(ipv4Str) ipv6, err := embedIPv4(ipv4Str)
data := viewData{IPv4: ipv4Str} d := singleData{IPv4: ipv4Str}
if err != nil { if err != nil {
data.Error = err.Error() d.Error = err.Error()
} else { } else {
data.HaveResult = true d.HaveResult = true
data.IPv6 = ipv6 d.IPv6 = ipv6
data.Gateway = ulaPrefix + "1" // router IP = <prefix>::1 d.Gateway = ulaPrefix + "1"
data.DNS1 = dns1 d.DNS1, d.DNS2 = dns1, dns2
data.DNS2 = dns2
} }
renderPage(w, data) renderSingle(w, d)
})
log.Printf("Server läuft auf http://localhost%s (Präfix %s, DNS1 %s, DNS2 %s)", listenAddr, ulaPrefix, dns1, dns2)
log.Fatal(http.ListenAndServe(listenAddr, nil))
} }
// initConfigAndTemplate reads env vars and prepares HTML template. // ---------------------------------------------------------------------------
func initConfigAndTemplate() { // Handlers range conversion
// ---- ULA prefix ------------------------------------------------------- // ---------------------------------------------------------------------------
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") ulaPrefix = os.Getenv("ULA_PREFIX")
if ulaPrefix == "" { if ulaPrefix == "" {
ulaPrefix = defaultPrefix 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, "::") {
if strings.HasSuffix(ulaPrefix, ":") { if strings.HasSuffix(ulaPrefix, ":") {
ulaPrefix += ":" ulaPrefix += ":"
@@ -107,11 +198,9 @@ func initConfigAndTemplate() {
} }
} }
// ---- DNS addresses ---------------------------------------------------- // DNS defaults --------------------------------
dns1 = os.Getenv("DNS1") dns1 = os.Getenv("DNS1")
dns2 = os.Getenv("DNS2") dns2 = os.Getenv("DNS2")
// default DNS: change subnet 0 → 1 and append ::53 / ::54
if dns1 == "" { if dns1 == "" {
dns1 = strings.Replace(ulaPrefix, "::", "::53", 1) dns1 = strings.Replace(ulaPrefix, "::", "::53", 1)
} }
@@ -119,96 +208,109 @@ func initConfigAndTemplate() {
dns2 = strings.Replace(ulaPrefix, "::", "::54", 1) dns2 = strings.Replace(ulaPrefix, "::", "::54", 1)
} }
// ---- HTML template ---------------------------------------------------- // Placeholder IP ------------------------------
html := fmt.Sprintf(`<!DOCTYPE html> 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 = `<!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>IPv4  IPv6Mapper</title> <title>IPv4  IPv6Mapper</title>
<style> <style>
body{font-family:system-ui,sans-serif;margin:2rem;max-width:46rem} body{font-family:system-ui,sans-serif;margin:2rem;max-width:46rem}
form{display:flex;gap:.5rem;flex-wrap:wrap} form{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}
input[type=text],input[readonly]{padding:.4rem;font-size:1rem;border:1px solid #ccc;border-radius:4px;flex:1} input[type=text],input[readonly]{padding:.4rem;font-size:1rem;border:1px solid #ccc;border-radius:4px;flex:1}
button{padding:.5rem 1rem;font-size:1rem;cursor:pointer;border-radius:4px;border:1px solid #666;background:#eee} button{padding:.5rem 1rem;font-size:1rem;cursor:pointer;border-radius:4px;border:1px solid #666;background:#eee}
button.copy{padding:.3rem .6rem;font-size:.9rem;margin-left:.3rem;background:#def} button.copy{padding:.3rem .6rem;font-size:.9rem;margin-left:.3rem;background:#def}
#result{margin-top:1.5rem}
.row{display:flex;align-items:center;margin-bottom:.4rem} .row{display:flex;align-items:center;margin-bottom:.4rem}
.row label{width:14rem} .row label{width:14rem}
</style> </style>
<script> <script>async function copy(id){const v=document.getElementById(id).value;await navigator.clipboard.writeText(v);const b=document.getElementById(id+'Btn');const o=b.textContent;b.textContent='✔';setTimeout(()=>b.textContent=o,1200);}</script>
async function copy(id){ </head><body>
const val=document.getElementById(id).value; <h1>IPv4  IPv6 (einzeln)</h1>
await navigator.clipboard.writeText(val);
const btn=document.getElementById(id+"Btn");
const old=btn.textContent;
btn.textContent="✔ kopiert";
setTimeout(()=>btn.textContent=old,1200);
}
</script>
</head>
<body>
<h1>IPv4  IPv6Mapper</h1>
<form action="/convert" method="post"> <form action="/convert" method="post">
<input type="text" name="ipv4" placeholder="%s" value="{{.IPv4}}" required /> <input name="ipv4" type="text" placeholder="%s" value="{{.IPv4}}" required />
<button type="submit">Umrechnen</button> <button>Umrechnen</button>
<a href="/range" style="margin-left:1rem">&raquo; Bereich&nbsp;konvertieren</a>
</form> </form>
{{if .HaveResult}} {{if .HaveResult}}
<div id="result"> <div>
<h2>Windows 11Eingaben</h2> <h2>Windows 11Eingaben</h2>
<div class="row"> <div class="row"><label>IPAdresse</label><input readonly id="ip" value="{{.IPv6}}"><button type="button" class="copy" id="ipBtn" onclick="copy('ip')">Copy</button></div>
<label for="ip">IPAdresse</label> <div class="row"><label>Subnetzpräfix</label><input readonly id="pl" value="96"><button type="button" class="copy" id="plBtn" onclick="copy('pl')">Copy</button></div>
<input readonly id="ip" value="{{.IPv6}}" /> <div class="row"><label>Gateway</label><input readonly id="gw" value="{{.Gateway}}"><button type="button" class="copy" id="gwBtn" onclick="copy('gw')">Copy</button></div>
<button type="button" class="copy" id="ipBtn" onclick="copy('ip')">Kopieren</button> <div class="row"><label>DNS bevorzugt</label><input readonly id="dns1" value="{{.DNS1}}"><button type="button" class="copy" id="dns1Btn" onclick="copy('dns1')">Copy</button></div>
</div> <div class="row"><label>DNS alternativ</label><input readonly id="dns2" value="{{.DNS2}}"><button type="button" class="copy" id="dns2Btn" onclick="copy('dns2')">Copy</button></div>
<div class="row">
<label for="pl">Subnetzpräfixlänge</label>
<input readonly id="pl" value="96" />
<button type="button" class="copy" id="plBtn" onclick="copy('pl')">Kopieren</button>
</div>
<div class="row">
<label for="gw">Gateway (Bleibt vorerst leer!)</label>
<input readonly id="gw" value="{{.Gateway}}" />
<button type="button" class="copy" id="gwBtn" onclick="copy('gw')">Kopieren</button>
</div>
<div class="row">
<label for="dns1">Bevorzugter DNS</label>
<input readonly id="dns1" value="{{.DNS1}}" />
<button type="button" class="copy" id="dns1Btn" onclick="copy('dns1')">Kopieren</button>
</div>
<div class="row">
<label for="dns2">Alternativer DNS</label>
<input readonly id="dns2" value="{{.DNS2}}" />
<button type="button" class="copy" id="dns2Btn" onclick="copy('dns2')">Kopieren</button>
</div>
</div> </div>
{{end}} {{end}}
{{if .Error}}<p style="color:#b00">Fehler: {{.Error}}</p>{{end}}
<p style="margin-top:1rem">Aktives Präfix: <code>%s</code> (/96)</p>
</body></html>`
{{if .Error}} var rangePageHTML = `<!DOCTYPE html>
<p style="color:#b00">Fehler: {{.Error}}</p> <html lang="de">
<head>
<meta charset="utf-8" />
<title>IPv4Range  IPv6</title>
<style>
body{font-family:system-ui,sans-serif;margin:2rem;max-width:60rem}
table{border-collapse:collapse;width:100%}
th,td{border:1px solid #ccc;padding:.4rem;text-align:left;font-family:monospace}
form{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}
input[type=text]{padding:.4rem;font-size:1rem;border:1px solid #ccc;border-radius:4px;flex:1}
button{padding:.5rem 1rem;font-size:1rem;cursor:pointer;border-radius:4px;border:1px solid #666;background:#eee}
</style>
</head><body>
<h1>IPv4Bereich  IPv6Tabelle</h1>
<form action="/range" method="post">
<input name="start" type="text" placeholder="StartIPv4" value="{{.Start}}" required />
<input name="end" type="text" placeholder="EndIPv4" value="{{.End}}" required />
<button>Konvertieren</button>
<a href="/" style="margin-left:1rem">&laquo; Einzelkonverter</a>
</form>
{{if .HaveResult}}
<table>
<tr><th>IPv4</th><th>IPv6</th></tr>
{{range .Rows}}<tr><td>{{.IPv4}}</td><td>{{.IPv6}}</td></tr>{{end}}
</table>
{{end}} {{end}}
{{if .Error}}<p style="color:#b00">Fehler: {{.Error}}</p>{{end}}
<p style="margin-top:1.5rem">Aktives Präfix: <code>%s</code> (/96)</p> <p style="margin-top:1rem">Aktives Präfix: <code>%s</code> (/96)</p>
</body> </body></html>`
</html>`, 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)
}
}