Skip to content

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 →