# IP Address Parsing and Validation Edge Cases in the geoip Library

> Explore IP address parsing and validation edge cases like malformed inputs and invalid CIDR ranges handled by the geoip library. Learn how it ensures data integrity.

- Repository: [Loyalsoldier/geoip](https://github.com/loyalsoldier/geoip)
- Tags: deep-dive
- Published: 2026-03-06

---

**The geoip library handles malformed inputs, IPv4-in-IPv6 mappings, comment lines, and invalid CIDR ranges by centralizing all parsing in [`lib/entry.go`](https://github.com/loyalsoldier/geoip/blob/main/lib/entry.go)'s `processPrefix` method while using [`lookup.go`](https://github.com/loyalsoldier/geoip/blob/main/lookup.go)'s `isValidIPOrCIDR` for CLI pre-validation.**

The loyalsoldier/geoip library provides robust IP address parsing and validation for GeoIP data processing. Its architecture anticipates ambiguous inputs by normalizing all addresses into canonical `netip.Prefix` structures while detecting edge cases like IPv4-mapped IPv6 addresses, malformed CIDR notation, and empty or commented lines.

## Core Parsing Logic in lib/entry.go

The `processPrefix` method in [`lib/entry.go`](https://github.com/loyalsoldier/geoip/blob/main/lib/entry.go) serves as the central entry point for all IP and CIDR normalization. This function processes raw input strings and converts them into standardized `netip.Prefix` objects with explicit IPv4 or IPv6 family detection, handling multiple edge cases through dedicated branches.

### Empty Lines and Comment Handling

Before attempting numeric parsing, the library strips trailing comments and detects empty inputs. When a line contains only whitespace or comment markers, `processPrefix` returns `ErrCommentLine` (defined in [`lib/common.go`](https://github.com/loyalsoldier/geoip/blob/main/lib/common.go)), signaling the container builder to skip the entry rather than fail.

```go
// From lib/entry.go lines 95-98
return nil, "", ErrCommentLine

```

This handles inputs like `# comment` or `// comment` by stripping delimiters first, then checking if the remaining string is empty.

### Plain IPv4 and IPv6 Addresses

For inputs without CIDR notation, the library uses `netip.ParseAddr` to validate the string. **IPv4 addresses** automatically receive a `/32` prefix, while **IPv6 addresses** receive a `/128` prefix (lines 28-42). Both undergo unmapping to ensure canonical representation and prevent ambiguity in later processing stages.

### CIDR Notation and IPv4-in-IPv6 Mapping

The most complex edge case involves **IPv4-in-IPv6 prefixes** such as `::ffff:192.0.2.0/120`. When `processPrefix` detects `ip.Is4In6()` (lines 33-49), it unmaps the address and adjusts the prefix length by subtracting 96 bits, converting it to a pure IPv4 prefix (e.g., `192.0.2.0/24`).

Standard CIDR strings use `net.ParseCIDR` with conversion to `netip.Addr` via `go4.org/netipx`. The library explicitly checks for illegal IPv4-mapped-IPv6 strings in this branch (lines 99-115) to prevent mixing address families in the output database.

### Invalid Input Detection

The library returns specific errors for various failure modes:

- **ErrInvalidIPLength**: Triggered when an address length is neither 4 nor 6 bytes after parsing (lines 85-87)
- **ErrInvalidPrefix**: Returned when prefix bits exceed the maximum for the address family (e.g., `/33` for IPv4) during `ip.Prefix(bits)` creation (lines 33-55)
- **ErrInvalidPrefixType**: Falls through when the input type is unsupported, such as malformed `net.IPNet` structures or invalid pointer types (lines 144-147)

These error constants defined in [`lib/common.go`](https://github.com/loyalsoldier/geoip/blob/main/lib/common.go) allow upstream callers to distinguish between recoverable warnings and critical parsing failures.

## CLI Input Validation

The `lookup` command uses `isValidIPOrCIDR` in [`lookup.go`](https://github.com/loyalsoldier/geoip/blob/main/lookup.go) (lines 44-60) to pre-validate user queries before invoking heavy `Entry` processing. This lightweight validator distinguishes between CIDR and plain IP strings using `strings.Contains(search, "/")`, then applies `netip.ParsePrefix` or `netip.ParseAddr` accordingly.

```go
func isValidIPOrCIDR(search string) bool {
    if search == "" {
        return false
    }
    var err error
    switch strings.Contains(search, "/") {
    case true:
        _, err = netip.ParsePrefix(search)
    case false:
        _, err = netip.ParseAddr(search)
    }
    return err == nil
}

```

This prevents malformed inputs like empty strings or invalid octets from reaching the database lookup stage, improving responsiveness in both single-shot and REPL modes.

## Practical Usage Examples

### Normalizing Mixed Address Formats

The `AddPrefix` method accepts various input types and normalizes them through `processPrefix`:

```go
package main

import (
	"fmt"
	"github.com/Loyalsoldier/geoip/lib"
)

func main() {
	e := lib.NewEntry("MIXED")
	// Plain IPv4 becomes 1.2.3.4/32
	_ = e.AddPrefix("1.2.3.4")
	// IPv4-in-IPv6 becomes 192.0.2.0/24
	_ = e.AddPrefix("::ffff:192.0.2.0/120")
	// IPv6 CIDR preserved as fd00::/64
	_ = e.AddPrefix("fd00::/64")

	list, _ := e.MarshalText()
	fmt.Println(list)
}

```

The output contains canonical prefixes: `["1.2.3.4/32" "192.0.2.0/24" "fd00::/64"]`. Note how the IPv4-in-IPv6 input is automatically transformed into standard IPv4 CIDR notation.

### CLI Validation Behavior

```bash
$ geoip lookup -f text -u ./mylist.txt 1.2.3.4
true

$ geoip lookup -f text -u ./mylist.txt invalid_address
false

```

The second command returns `false` immediately because `isValidIPOrCIDR` rejects the malformed input before any database operations occur.

## Summary

- The `processPrefix` method in [`lib/entry.go`](https://github.com/loyalsoldier/geoip/blob/main/lib/entry.go) centralizes all IP address parsing and validation edge cases into a single canonical representation
- **IPv4-in-IPv6 prefixes** are automatically unmapped and converted to pure IPv4 CIDR notation by subtracting 96 bits from the prefix length
- Empty lines and comments return `ErrCommentLine` for silent skipping without interrupting batch processing
- Invalid IP lengths trigger `ErrInvalidIPLength`, while malformed CIDRs generate `ErrInvalidPrefix`, both defined in [`lib/common.go`](https://github.com/loyalsoldier/geoip/blob/main/lib/common.go)
- The CLI's `isValidIPOrCIDR` function in [`lookup.go`](https://github.com/loyalsoldier/geoip/blob/main/lookup.go) provides early validation to prevent processing invalid queries against the IP database

## Frequently Asked Questions

### How does the geoip library handle IPv4 addresses embedded in IPv6 notation?

When `processPrefix` encounters an address where `ip.Is4In6()` returns true, it unmaps the IPv4-mapped IPv6 address and subtracts 96 from the prefix length. This converts inputs like `::ffff:192.0.2.0/120` into standard IPv4 CIDR `192.0.2.0/24` as implemented in [`lib/entry.go`](https://github.com/loyalsoldier/geoip/blob/main/lib/entry.go) lines 33-49.

### What error does the library return for empty or comment-only lines?

The library returns `ErrCommentLine` (defined in [`lib/common.go`](https://github.com/loyalsoldier/geoip/blob/main/lib/common.go)) when processing lines that contain only whitespace or comment markers after stripping trailing comments. This allows bulk parsers in [`lib/entry.go`](https://github.com/loyalsoldier/geoip/blob/main/lib/entry.go) to skip these lines without terminating the entire operation.

### How does the CLI validate user input before processing?

The `lookup` command in [`lookup.go`](https://github.com/loyalsoldier/geoip/blob/main/lookup.go) uses the `isValidIPOrCIDR` helper to validate strings before invoking `Entry` methods. It checks for empty input and uses `netip.ParseAddr` or `netip.ParsePrefix` to ensure only valid IP addresses or CIDR ranges proceed to the lookup stage, preventing malformed queries from reaching the core engine.

### What happens when an IP address has an invalid byte length?

If an address does not satisfy `Is4()` or `Is6()` checks after parsing, `processPrefix` returns `ErrInvalidIPLength`. This catches malformed addresses that survive initial string parsing but fail byte-length validation at lines 85-87 of [`lib/entry.go`](https://github.com/loyalsoldier/geoip/blob/main/lib/entry.go).