IPv4 ↔ IPv6 Converter — Go source
Convert between IPv4 and IPv6 addresses both ways. Parse and validate addresses, expand and compress IPv6 to its canonical RFC 5952 form, map an IPv4 into IPv4-mapped and IPv4-compatible IPv6 (or any custom /96 prefix), and extract an embedded IPv4 back out.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package ipconverter is the Go twin of CosmoDev's src/lib/ip-converter.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in
// lock-step). Pure + deterministic, never panics. Every parse function
// returns a zero value with ok=false (mirroring the TS `null`) on invalid
// input; the string renderers return "" (mirroring the TS ''). The
// table-driven tests in ip-converter_test.go share vectors with
// src/lib/ip-converter.test.ts so the two implementations are held to the
// same contract.
//
// IPv6 text follows RFC 5952: lowercase hex, no leading zeros, the single
// longest run of zero groups collapsed to "::", and a dotted-decimal tail
// only for IPv4-mapped ("::ffff:") addresses.
package ipconverter
import (
"regexp"
"strconv"
"strings"
)
// Mode is the IPv4 embedding family used by IPv4ToIPv6.
type Mode int
const (
// ModeMapped produces the IPv4-mapped form "::ffff:a.b.c.d". It is the
// zero value, matching the TS default (mode: 'mapped').
ModeMapped Mode = iota
// ModeCompatible produces the IPv4-compatible form "::a.b.c.d".
ModeCompatible
)
// IPv4ToIPv6Options mirrors Ipv4ToIpv6Options in the TS lib. The zero value
// (IPv4ToIPv6Options{}) matches the TS default (no options → mapped).
//
// A non-empty Prefix overrides Mode and places the IPv4 after any custom
// /96 prefix (e.g. "64:ff9b::" yields "64:ff9b::a.b.c.d").
type IPv4ToIpv6Options struct {
Mode Mode
Prefix string // empty → use Mode; non-empty → overrides Mode
}
var (
// hex matches one 16-bit IPv6 group: 1–4 lowercase/uppercase hex digits.
hex = regexp.MustCompile(`^[0-9a-fA-F]{1,4}$`)
// dec3 matches one IPv4 octet candidate: 1–3 ASCII digits (range checked separately).
dec3 = regexp.MustCompile(`^\d{1,3}$`)
)
// ParseIPv4 parses a dotted-decimal IPv4 string into four octets, validating
// each is 0–255. It is the Go twin of parseIpv4() and returns ok=false for
// anything that is not exactly four numeric octets in range.
func ParseIPv4(s string) ([]int, bool) {
parts := strings.Split(strings.TrimSpace(s), ".")
if len(parts) != 4 {
return nil, false
}
octets := make([]int, 0, 4)
for _, p := range parts {
if !dec3.MatchString(p) {
return nil, false
}
n, err := strconv.Atoi(p)
if err != nil || n < 0 || n > 255 {
return nil, false
}
octets = append(octets, n)
}
return octets, true
}
// validOctets reports whether v is exactly four integers in 0–255.
func validOctets(v []int) bool {
if len(v) != 4 {
return false
}
for _, o := range v {
if o < 0 || o > 255 {
return false
}
}
return true
}
// dotted renders four octets as "a.b.c.d". The caller guarantees len(octets)==4.
func dotted(octets []int) string {
return strconv.Itoa(octets[0]) + "." + strconv.Itoa(octets[1]) + "." +
strconv.Itoa(octets[2]) + "." + strconv.Itoa(octets[3])
}
// IPv4ToString renders four octets as "a.b.c.d", or "" if the octets are out
// of range or the wrong count. It is the Go twin of ipv4ToString().
func IPv4ToString(octets []int) string {
if !validOctets(octets) {
return ""
}
return dotted(octets)
}
// parseHex parses an already-validated 1–4 digit hex group into an int.
func parseHex(g string) int {
n, _ := strconv.ParseInt(g, 16, 0)
return int(n)
}
// hexGroup renders one 16-bit group as lowercase hex with no leading zeros.
func hexGroup(v int) string { return strconv.FormatInt(int64(v), 16) }
// joinHex joins 16-bit groups with ":" using hexGroup.
func joinHex(groups []int) string {
parts := make([]string, len(groups))
for i, v := range groups {
parts[i] = hexGroup(v)
}
return strings.Join(parts, ":")
}
// ParseIPv6 parses an IPv6 string (with "::" compression, hex groups, and an
// optional dotted-decimal IPv4 tail) into eight 16-bit groups. It is the Go
// twin of parseIpv6() and returns ok=false on any malformed input.
func ParseIPv6(s string) ([]int, bool) {
input := strings.TrimSpace(s)
if input == "" {
return nil, false
}
// At most one "::" compression marker.
if strings.Count(input, "::") > 1 {
return nil, false
}
if dc := strings.Index(input, "::"); dc >= 0 {
before := input[:dc]
after := input[dc+2:]
head := make([]int, 0, 8)
if before != "" {
for _, g := range strings.Split(before, ":") {
if !hex.MatchString(g) {
return nil, false
}
head = append(head, parseHex(g))
}
}
tail := make([]int, 0, 8)
if after != "" {
tailTokens := strings.Split(after, ":")
for i, g := range tailTokens {
if i == len(tailTokens)-1 && strings.Contains(g, ".") {
oct, ok := ParseIPv4(g)
if !ok {
return nil, false
}
tail = append(tail, (oct[0]<<8)|oct[1], (oct[2]<<8)|oct[3])
} else {
if !hex.MatchString(g) {
return nil, false
}
tail = append(tail, parseHex(g))
}
}
}
total := len(head) + len(tail)
if total >= 8 { // "::" must elide at least one group
return nil, false
}
result := make([]int, 0, 8)
result = append(result, head...)
for i := 0; i < 8-total; i++ {
result = append(result, 0)
}
result = append(result, tail...)
return result, true
}
// No compression: split and parse, allowing a dotted-quad only in the last slot.
tokens := strings.Split(input, ":")
groups := make([]int, 0, 8)
for i, g := range tokens {
if i == len(tokens)-1 && strings.Contains(g, ".") {
oct, ok := ParseIPv4(g)
if !ok {
return nil, false
}
groups = append(groups, (oct[0]<<8)|oct[1], (oct[2]<<8)|oct[3])
} else {
if !hex.MatchString(g) {
return nil, false
}
groups = append(groups, parseHex(g))
}
}
if len(groups) != 8 {
return nil, false
}
return groups, true
}
// isMapped reports whether eight groups form an IPv4-mapped ("::ffff:") address.
func isMapped(g []int) bool {
return g[0] == 0 && g[1] == 0 && g[2] == 0 && g[3] == 0 && g[4] == 0 && g[5] == 0xffff
}
// isCompatible reports whether eight groups form an IPv4-compatible ("::") address.
func isCompatible(g []int) bool {
return g[0] == 0 && g[1] == 0 && g[2] == 0 && g[3] == 0 && g[4] == 0 && g[5] == 0
}
// isValidIPv6Groups reports whether groups is exactly eight integers in 16-bit range.
func isValidIPv6Groups(groups []int) bool {
if len(groups) != 8 {
return false
}
for _, v := range groups {
if v < 0 || v > 0xffff {
return false
}
}
return true
}
// compressGroups collapses the longest run (length ≥ 2) of zero groups into
// "::" (first run wins on ties) and strips leading zeros — RFC 5952 canonical
// text for pure-hex IPv6. It does not emit dotted-decimal; call renderCanonical
// for that.
func compressGroups(groups []int) string {
bestStart, bestLen := -1, 0
curStart, curLen := -1, 0
for i := 0; i < len(groups); i++ {
if groups[i] == 0 {
if curStart < 0 {
curStart = i
}
curLen++
if curLen > bestLen {
bestLen = curLen
bestStart = curStart
}
} else {
curStart, curLen = -1, 0
}
}
if bestLen < 2 {
return joinHex(groups)
}
before := joinHex(groups[:bestStart])
after := joinHex(groups[bestStart+bestLen:])
return before + "::" + after
}
// renderWithEmbeddedTail renders a compressed high part followed by a
// dotted-decimal IPv4 tail. When the high part already ends in "::" (its zero
// run reaches the boundary) the IPv4 attaches directly; otherwise a single
// ":" separates them — so "::ffff:" → "::ffff:a.b.c.d" and "::" → "::a.b.c.d".
func renderWithEmbeddedTail(high, octets []int) string {
highStr := compressGroups(high)
if strings.HasSuffix(highStr, "::") {
return highStr + dotted(octets)
}
return highStr + ":" + dotted(octets)
}
// renderCanonical produces RFC 5952 canonical text for eight groups: a
// dotted-decimal tail for IPv4-mapped ("::ffff:") addresses, otherwise pure
// compressed hex. The deprecated IPv4-compatible range ("::/96") is NOT
// rendered dotted — that would mis-render the unspecified ("::") and loopback
// ("::1") addresses as "::0.0.0.0" / "::0.0.0.1". Compatible extraction is
// still available via IPv6ToIPv4.
func renderCanonical(groups []int) string {
if isMapped(groups) {
octets := []int{(groups[6] >> 8) & 0xff, groups[6] & 0xff, (groups[7] >> 8) & 0xff, groups[7] & 0xff}
return renderWithEmbeddedTail(groups[:6], octets)
}
return compressGroups(groups)
}
// expandGroup renders one 16-bit group as four-digit lowercase hex with leading
// zeros (mirrors v.toString(16).padStart(4, '0')).
func expandGroup(v int) string {
s := strconv.FormatInt(int64(v), 16)
for len(s) < 4 {
s = "0" + s
}
return s
}
// IPv6ToString renders eight groups as canonical compressed IPv6, or "" if
// invalid. It is the Go twin of ipv6ToString().
func IPv6ToString(groups []int) string {
if !isValidIPv6Groups(groups) {
return ""
}
return renderCanonical(groups)
}
// ExpandIPv6 expands an IPv6 string to its full eight-group, four-hex-digit
// form; "" if invalid. It is the Go twin of expandIpv6().
func ExpandIPv6(s string) string {
g, ok := ParseIPv6(s)
if !ok {
return ""
}
parts := make([]string, len(g))
for i, v := range g {
parts[i] = expandGroup(v)
}
return strings.Join(parts, ":")
}
// CompressIPv6 compresses an IPv6 string to its RFC 5952 canonical form; "" if
// invalid. It is the Go twin of compressIpv6().
func CompressIPv6(s string) string {
g, ok := ParseIPv6(s)
if !ok {
return ""
}
return renderCanonical(g)
}
// IPv4ToIPv6 embeds an IPv4 octet quad into an IPv6 address. By default it
// produces the IPv4-mapped form "::ffff:a.b.c.d"; ModeCompatible yields
// "::a.b.c.d"; a non-empty Prefix overrides both. It is the Go twin of
// ipv4ToIpv6() and returns "" for invalid octets or prefix.
func IPv4ToIpv6(octets []int, opts IPv4ToIpv6Options) string {
if !validOctets(octets) {
return ""
}
if opts.Prefix != "" {
p, ok := ParseIPv6(opts.Prefix)
if !ok {
return ""
}
return renderWithEmbeddedTail(p[:6], octets)
}
if opts.Mode == ModeCompatible {
return renderWithEmbeddedTail([]int{0, 0, 0, 0, 0, 0}, octets)
}
return renderWithEmbeddedTail([]int{0, 0, 0, 0, 0, 0xffff}, octets)
}
// IPv6ToIpv4 extracts the embedded IPv4 from an IPv4-mapped ("::ffff:a.b.c.d")
// or IPv4-compatible ("::a.b.c.d") address. ok is false when the address
// carries no embedded IPv4 (or is unparseable). It is the Go twin of ipv6ToIpv4().
func IPv6ToIpv4(s string) (string, bool) {
g, ok := ParseIPv6(s)
if !ok || (!isMapped(g) && !isCompatible(g)) {
return "", false
}
octets := []int{(g[6] >> 8) & 0xff, g[6] & 0xff, (g[7] >> 8) & 0xff, g[7] & 0xff}
return dotted(octets), true
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →