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.
//
// 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: <prefix with 1::53>)
// DNS2  alternate DNS IPv6 address (default: <prefix with 1::54>)
// ULA_PREFIX  /96 prefix to embed into, default "fd09:cafe:affe:4010::"
// DNS1  preferred DNS IPv6 address (default: <prefix>::53)
// DNS2  alternate DNS IPv6 address (default: <prefix>::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:
//
// 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
ulaPrefix string
dns1, dns2 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 {
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) {
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 {
renderPage(w, viewData{Error: "Ungültiges Formular"})
renderSingle(w, singleData{Error: "Ungültiges Formular"})
return
}
ipv4Str := r.FormValue("ipv4")
ipv6, err := embedIPv4(ipv4Str)
data := viewData{IPv4: ipv4Str}
d := singleData{IPv4: ipv4Str}
if err != nil {
data.Error = err.Error()
d.Error = err.Error()
} else {
data.HaveResult = true
data.IPv6 = ipv6
data.Gateway = ulaPrefix + "1" // router IP = <prefix>::1
data.DNS1 = dns1
data.DNS2 = dns2
d.HaveResult = true
d.IPv6 = ipv6
d.Gateway = ulaPrefix + "1"
d.DNS1, d.DNS2 = dns1, 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.Fatal(http.ListenAndServe(listenAddr, nil))
renderSingle(w, d)
}
// initConfigAndTemplate reads env vars and prepares HTML template.
func initConfigAndTemplate() {
// ---- ULA prefix -------------------------------------------------------
// ---------------------------------------------------------------------------
// 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(`<!DOCTYPE html>
// 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 = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>IPv4  IPv6Mapper</title>
<style>
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}
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}
#result{margin-top:1.5rem}
.row{display:flex;align-items:center;margin-bottom:.4rem}
.row label{width:14rem}
</style>
<script>
async function copy(id){
const val=document.getElementById(id).value;
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>
<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>
</head><body>
<h1>IPv4  IPv6 (einzeln)</h1>
<form action="/convert" method="post">
<input type="text" name="ipv4" placeholder="%s" value="{{.IPv4}}" required />
<button type="submit">Umrechnen</button>
<input name="ipv4" type="text" placeholder="%s" value="{{.IPv4}}" required />
<button>Umrechnen</button>
<a href="/range" style="margin-left:1rem">&raquo; Bereich&nbsp;konvertieren</a>
</form>
{{if .HaveResult}}
<div id="result">
<div>
<h2>Windows 11Eingaben</h2>
<div class="row">
<label for="ip">IPAdresse</label>
<input readonly id="ip" value="{{.IPv6}}" />
<button type="button" class="copy" id="ipBtn" onclick="copy('ip')">Kopieren</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 class="row"><label>IPAdresse</label><input readonly id="ip" value="{{.IPv6}}"><button type="button" class="copy" id="ipBtn" onclick="copy('ip')">Copy</button></div>
<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>
<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>
<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 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>
{{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}}
<p style="color:#b00">Fehler: {{.Error}}</p>
var rangePageHTML = `<!DOCTYPE html>
<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}}
<p style="margin-top:1.5rem">Aktives Präfix: <code>%s</code> (/96)</p>
</body>
</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)
}
}
{{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>`