package main import ( "encoding/json" "fmt" "math" "net" "os" "github.com/maxmind/mmdbwriter" "github.com/maxmind/mmdbwriter/inserter" "github.com/maxmind/mmdbwriter/mmdbtype" ) func main() { if err := run(); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } func run() error { // Read the JSON object from stdin. // // Example: // // { // "1.1.1.1": { // "is_proxy": true, // "score": 42 // }, // "1.0.0.0/8": { // "is_hosting": true // } // } var input map[string]any decoder := json.NewDecoder(os.Stdin) decoder.UseNumber() if err := decoder.Decode(&input); err != nil { return fmt.Errorf("invalid JSON: %w", err) } // Create an IPv6 MMDB. // // IPv6 trees can contain both IPv6 and IPv4 networks. tree, err := mmdbwriter.New(mmdbwriter.Options{ DatabaseType: "IP Classification", Description: map[string]string{ "en": "Cloudfish", }, IPVersion: 6, // Valid values are 24, 28, and 32. RecordSize: 28, IncludeReservedNetworks: true, DisableIPv4Aliasing: true, }) if err != nil { return fmt.Errorf("creating MMDB: %w", err) } // Insert every IP/CIDR. // // Use TopLevelMergeWith instead of the normal Insert method. // // This is important when ranges overlap. For example: // // 1.2.0.0/16 -> {"is_proxy": true} // 1.2.3.0/24 -> {"is_hosting": true} // // An address inside 1.2.3.0/24 will retain both fields: // // { // "is_proxy": true, // "is_hosting": true // } for address, rawRecord := range input { network, err := parseNetwork(address) if err != nil { return fmt.Errorf("invalid IP/CIDR %q: %w", address, err) } record, err := toMMDBValue(rawRecord) if err != nil { return fmt.Errorf("invalid record for %q: %w", address, err) } if err := tree.InsertFunc( network, inserter.TopLevelMergeWith(record), ); err != nil { return fmt.Errorf("inserting %q: %w", address, err) } } // Write the binary MMDB to stdout. if _, err := tree.WriteTo(os.Stdout); err != nil { return fmt.Errorf("writing MMDB: %w", err) } return nil } // parseNetwork accepts: // // 1.1.1.1 // 1.0.0.0/16 // 2001:db8::1 // 2001:db8::/32 // // A plain IPv4 address becomes /32. // A plain IPv6 address becomes /128. func parseNetwork(s string) (*net.IPNet, error) { // First try CIDR notation. if _, network, err := net.ParseCIDR(s); err == nil { return network, nil } // Then try a plain IP address. ip := net.ParseIP(s) if ip == nil { return nil, fmt.Errorf("not a valid IP address or CIDR") } // IPv4. if ip4 := ip.To4(); ip4 != nil { return &net.IPNet{ IP: ip4, Mask: net.CIDRMask(32, 32), }, nil } // IPv6. return &net.IPNet{ IP: ip, Mask: net.CIDRMask(128, 128), }, nil } // toMMDBValue recursively converts normal JSON values into // MMDB values. func toMMDBValue(v any) (mmdbtype.DataType, error) { switch x := v.(type) { case nil: return nil, nil case bool: return mmdbtype.Bool(x), nil case string: return mmdbtype.String(x), nil case json.Number: return jsonNumberToMMDB(x) case []any: array := make(mmdbtype.Slice, len(x)) for i, item := range x { value, err := toMMDBValue(item) if err != nil { return nil, err } array[i] = value } return array, nil case map[string]any: m := make(mmdbtype.Map, len(x)) for key, value := range x { converted, err := toMMDBValue(value) if err != nil { return nil, fmt.Errorf("%s: %w", key, err) } m[mmdbtype.String(key)] = converted } return m, nil default: return nil, fmt.Errorf("unsupported JSON type %T", v) } } // jsonNumberToMMDB converts a JSON number into the most appropriate // MMDB numeric type. // // Integer values: // // 0 - 65535 -> Uint16 // 65536 - 4294967295 -> Uint32 // 4294967296 - MaxUint64 -> Uint64 // negative values -> Int32 // // Non-integer values are stored as Float64. func jsonNumberToMMDB(n json.Number) (mmdbtype.DataType, error) { s := n.String() // First try it as an integer. if i, err := n.Int64(); err == nil { // Positive integers. if i >= 0 { u := uint64(i) switch { case u <= math.MaxUint16: return mmdbtype.Uint16(u), nil case u <= math.MaxUint32: return mmdbtype.Uint32(u), nil default: return mmdbtype.Uint64(u), nil } } // Negative integers must fit in signed 32-bit MMDB type. if i >= math.MinInt32 { return mmdbtype.Int32(i), nil } return nil, fmt.Errorf( "integer %q is below the supported signed 32-bit range", s, ) } // Not an integer, so try float64. f, err := n.Float64() if err != nil { return nil, fmt.Errorf("invalid number %q", s) } if math.IsNaN(f) || math.IsInf(f, 0) { return nil, fmt.Errorf("invalid floating-point number %q", s) } return mmdbtype.Float64(f), nil }