Files
ipv6calculator/main.go
jbergner 9c3ccd4770
All checks were successful
release-tag / release-image (push) Successful in 2m2s
Link hinzugefügt
2025-05-11 21:41:46 +02:00

480 lines
16 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 (
"encoding/json"
"fmt"
"html/template"
"log"
"net"
"net/http"
"os"
"sort"
"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
dhcpTemplate *template.Template
dhcpServer string
dhcpScope string
dhcpNamePrefix string
dhcpDomain 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, DhcpDomain string
Rows []addrPair
Error string
HaveResult bool
}
var DuidHostnameList []payload
type payload struct {
Hostname string `json:"hostname"`
DUIDs []string `json:"duids"`
IAIDs []uint32 `json:"iaids"`
}
type payloadHelper struct {
Hostname string
DomainName string
DhcpServer string
DUID string
IAID string
CalculatedIPv4 string
Dhcp4Scope string
CalculatedIPv6 string
Dhcp6Scope string
}
func octetsRaw(ip string) ([]string, error) {
parts := strings.Split(ip, ".")
if len(parts) != 4 {
return nil, fmt.Errorf("ungültige IPv4-Adresse: %q", ip)
}
return parts, nil
}
func DhcpHelperFunc(xHostname string, xDUIDs []string, xIAIDs []uint32) []payloadHelper {
/*IPv4*/
Ipv4Octets, _ := octetsRaw(defaultIP)
rHostname := []rune(xHostname)
qDUID := xDUIDs[0]
qSegment1 := string(rHostname[2:4])
qSegment2 := string(rHostname[4:])
qCalculatedIPv4 := Ipv4Octets[0] + "." + Ipv4Octets[1] + "." + qSegment1 + "." + qSegment2
qCalculatedIPv6, _ := embedIPv4(qCalculatedIPv4)
var res []payloadHelper
for _, t := range xIAIDs {
r := payloadHelper{
Hostname: xHostname,
DUID: qDUID,
CalculatedIPv4: qCalculatedIPv4,
CalculatedIPv6: qCalculatedIPv6,
Dhcp4Scope: dhcpScope,
Dhcp6Scope: ulaPrefix,
DomainName: dhcpDomain,
DhcpServer: dhcpServer,
IAID: fmt.Sprintf("%d", t),
}
res = append(res, r)
}
return res
}
func getDhcp() []payloadHelper {
var result []payloadHelper
sortByHostname(DuidHostnameList)
for _, b := range DuidHostnameList {
result = append(result, DhcpHelperFunc(b.Hostname, b.DUIDs, b.IAIDs)...)
}
return result
}
func register(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "nur POST erlaubt", http.StatusMethodNotAllowed)
return
}
var p payload
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, "ungültiges JSON", http.StatusBadRequest)
return
}
// --- hier kannst du speichern, weiterverarbeiten, loggen … ---
log.Printf("neuer Client: %s → DUIDs=%v", p.Hostname, p.DUIDs)
DuidHostnameList = append(DuidHostnameList, p)
w.WriteHeader(http.StatusNoContent) // 204
}
func getdhcp6(w http.ResponseWriter, r *http.Request) {
temp := getDhcp()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = dhcpTemplate.Execute(w, temp)
}
func sortByHostname(p []payload) {
sort.Slice(p, func(i, j int) bool {
// Optional: case-insensitiv vergleichen
return strings.ToLower(p[i].Hostname) < strings.ToLower(p[j].Hostname)
})
}
// ---------------------------------------------------------------------------
// main HTTP routing
// ---------------------------------------------------------------------------
func main() {
initConfigAndTemplates()
http.HandleFunc("/", handleSingle)
http.HandleFunc("/convert", handleSingleConvert)
http.HandleFunc("/range", handleRange)
http.HandleFunc("/register", register)
http.HandleFunc("/dhcp6", getdhcp6)
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, DhcpDomain: dhcpDomain}
if err != nil {
d.Error = err.Error()
} else {
d.HaveResult = true
for _, 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 := dhcpNamePrefix + 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")
dhcpNamePrefix = os.Getenv("DHCP_NAME_PREFIX")
dhcpDomain = os.Getenv("DHCP_DOMAIN")
// 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))
dhcpTemplate = template.Must(template.New("range").Parse(rangeDHCP6HTML))
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><a href="/dhcp6" style="margin-left:1rem">DHCPv6</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}}.{{$.DhcpDomain}}" "" "DHCP"</td><td>---</td></tr>{{end}}
</table>
{{end}}
{{if .Error}}<p style="color:#b00">Fehler: {{.Error}}</p>{{end}}
</body></html>`
var rangeDHCP6HTML = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>DHCP-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>DHCPv6 - DUID-Registrierung</h1>
<table>
<tr><th>Hostname</th><th>Duid</th><th>IPv4</th><th>IPv6</th><th>DHCP6</th></tr>
{{range .}}<tr><td>{{.Hostname}}</td><td>{{.DUID}}</td><td>{{.CalculatedIPv4}}</td><td>{{.CalculatedIPv6}}</td><td>Add-DhcpServerv6Reservation -ComputerName {{.DhcpServer}} -Prefix {{.Dhcp6Scope}} -IPAddress {{.CalculatedIPv6}} -ClientDuid {{.DUID}} -Iaid {{.IAID}} -Name {{.Hostname}}.{{.DomainName}} -Description "Auto-reserved after rollout"</td></tr>{{end}}
</table>
</body></html>`
//Add-DhcpServerv6Reservation -ComputerName $Srv -Prefix $Prefix -IPAddress IPv6Address -ClientDuid $l.ClientDuid -Iaid $l.Iaid -Name $l.HostName -Description "Auto-reserved after rollout"