Files
ipv6calculator/main.go
jbergner 1ffea42f26
All checks were successful
release-tag / release-image (push) Successful in 1m58s
CopyPasteError-Fix
2025-05-09 11:17:49 +02:00

348 lines
12 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

// 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 "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:
//
// go run main.go
package main
import (
"fmt"
"html/template"
"log"
"net"
"net/http"
"os"
"strconv"
"strings"
)
// ---------------------------------------------------------------------------
// Configuration constants & globals
// ---------------------------------------------------------------------------
const (
listenAddr = ":8080"
defaultPrefix = "fd09:cafe:affe:4010::" // fallback /96
defaultIP = "172.16.0.0"
defaultLimit = 1024 // max. rows for range conversion
)
var (
ulaPrefix string
dns1, dns2 string
pageIP string
rangeLimit int
singleTemplate *template.Template
rangeTemplate *template.Template
dhcpServer string
dhcpScope string
)
// ---------------------------------------------------------------------------
// Template data structures
// ---------------------------------------------------------------------------
type singleData struct {
IPv4, IPv6, Gateway, DNS1, DNS2, Error string
HaveResult bool
}
type addrPair struct{ Name, IPv4, IPv6 string }
type rangeData struct {
Start, End string
DhcpServer, DhcpScope string
Rows []addrPair
Error string
HaveResult bool
}
// ---------------------------------------------------------------------------
// main HTTP routing
// ---------------------------------------------------------------------------
func main() {
initConfigAndTemplates()
http.HandleFunc("/", handleSingle)
http.HandleFunc("/convert", handleSingleConvert)
http.HandleFunc("/range", handleRange)
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 {
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, DhcpServer: dhcpServer, DhcpScope: dhcpScope}
if err != nil {
d.Error = err.Error()
} else {
d.HaveResult = true
for a, b := range rows {
fmt.Println(a, b)
octets := strings.Split(b.IPv4, ".")
if len(octets) != 4 {
fmt.Println("Ungültige IP-Adresse!")
return
}
O3 := ""
O4 := ""
if len(octets[2]) < 2 {
O3 = "0" + octets[2]
} else {
O3 = octets[2]
}
if len(octets[3]) < 2 {
O4 = "0" + octets[3]
} else {
O4 = octets[3]
}
N := "PC" + O3 + O4
d.Rows = append(d.Rows, addrPair{IPv4: b.IPv4, IPv6: b.IPv6, Name: N})
}
}
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
}
if !strings.HasSuffix(ulaPrefix, "::") {
if strings.HasSuffix(ulaPrefix, ":") {
ulaPrefix += ":"
} else {
ulaPrefix += "::"
}
}
// DNS defaults --------------------------------
dns1 = os.Getenv("DNS1")
dns2 = os.Getenv("DNS2")
if dns1 == "" {
dns1 = strings.Replace(ulaPrefix, "::", "::53", 1)
}
if dns2 == "" {
dns2 = strings.Replace(ulaPrefix, "::", "::54", 1)
}
// Placeholder IP ------------------------------
pageIP = os.Getenv("FORM_DEFAULT_IP")
if pageIP == "" {
pageIP = defaultIP
}
dhcpScope = os.Getenv("DHCP_SCOPE")
dhcpServer = os.Getenv("DHCP_SERVER")
// 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 := rangePageHTML
singleTemplate = template.Must(template.New("single").Parse(singleHTML))
rangeTemplate = template.Must(template.New("range").Parse(rangeHTML))
fmt.Println(ulaPrefix, dns1, dns2, pageIP, dhcpServer, dhcpScope, rangeLimit)
fmt.Println(singleHTML)
fmt.Println(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 → IPv6-Mapper</title>
<style>
body{font-family:system-ui,sans-serif;margin:2rem}
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}
.row{display:flex;align-items:center;margin-bottom:.4rem}
.row label{width:14rem}
</style>
<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 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>
<h2>Windows 11-Eingaben</h2>
<div class="row"><label>IP-Adresse</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>`
var rangePageHTML = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>IPv4-Range → IPv6</title>
<style>
body{font-family:system-ui,sans-serif;margin:2rem}
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>IPv4-Bereich → IPv6-Tabelle</h1>
<form action="/range" method="post">
<input name="start" type="text" placeholder="Start-IPv4" value="{{.Start}}" required />
<input name="end" type="text" placeholder="End-IPv4" 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><th>DHCP-IPv4</th><th>DHCP-IPv6</th></tr>
{{range .Rows}}<tr><td>{{.IPv4}}</td><td>{{.IPv6}}</td><td>netsh DHCP Server {{$.DhcpServer}} Scope {{$.DhcpScope}} Add reservedip {{.IPv4}} "{{.Name}}.stadt-hilden.de" "" "DHCP"</td><td>---</td></tr>{{end}}
</table>
{{end}}
{{if .Error}}<p style="color:#b00">Fehler: {{.Error}}</p>{{end}}
</body></html>`