31f1ef35b4
The buffer-backed xdb.Searcher is not safe for concurrent use, so parallel audit appends could race on Search and corrupt internal state. Serialize v4 and v6 lookups behind separate mutexes so concurrent callers fall through one at a time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
215 lines
4.8 KiB
Go
215 lines
4.8 KiB
Go
package app
|
|
|
|
import (
|
|
"embed"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
|
)
|
|
|
|
//go:embed ipregiondata/ip2region_v4.xdb ipregiondata/ip2region_v6.xdb
|
|
var embeddedIPRegionFS embed.FS
|
|
|
|
const (
|
|
embeddedV4XDBPath = "ipregiondata/ip2region_v4.xdb"
|
|
embeddedV6XDBPath = "ipregiondata/ip2region_v6.xdb"
|
|
)
|
|
|
|
type IPRegionResolver struct {
|
|
searcher ipRegionSearcher
|
|
logger *zap.Logger
|
|
}
|
|
|
|
type ipRegionSearcher interface {
|
|
Search(ip any) (string, error)
|
|
Close()
|
|
}
|
|
|
|
func NewIPRegionResolver(v4XDBPath, v6XDBPath string, logger *zap.Logger) (*IPRegionResolver, error) {
|
|
searcher, err := newIPRegionSearcher(v4XDBPath, v6XDBPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &IPRegionResolver{searcher: searcher, logger: logger}, nil
|
|
}
|
|
|
|
func newIPRegionSearcher(v4XDBPath, v6XDBPath string) (*bufferIPRegionSearcher, error) {
|
|
v4Content, err := loadIPRegionContent(strings.TrimSpace(v4XDBPath), embeddedV4XDBPath, xdb.IPv4)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load ipv4 xdb: %w", err)
|
|
}
|
|
v6Content, err := loadIPRegionContent(strings.TrimSpace(v6XDBPath), embeddedV6XDBPath, xdb.IPv6)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load ipv6 xdb: %w", err)
|
|
}
|
|
|
|
searcher := &bufferIPRegionSearcher{}
|
|
if len(v4Content) > 0 {
|
|
searcher.v4, err = xdb.NewWithBuffer(xdb.IPv4, v4Content)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create ipv4 searcher: %w", err)
|
|
}
|
|
}
|
|
if len(v6Content) > 0 {
|
|
searcher.v6, err = xdb.NewWithBuffer(xdb.IPv6, v6Content)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create ipv6 searcher: %w", err)
|
|
}
|
|
}
|
|
return searcher, nil
|
|
}
|
|
|
|
func loadIPRegionContent(externalPath, embeddedPath string, version *xdb.Version) ([]byte, error) {
|
|
if externalPath != "" {
|
|
content, err := loadXDBContentFromFile(externalPath, version)
|
|
if err == nil {
|
|
return content, nil
|
|
}
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
content, err := xdb.LoadContentFromFS(embeddedIPRegionFS, embeddedPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := verifyXDBVersion(content, version); err != nil {
|
|
return nil, fmt.Errorf("verify embedded xdb %s: %w", embeddedPath, err)
|
|
}
|
|
return content, nil
|
|
}
|
|
|
|
func loadXDBContentFromFile(path string, version *xdb.Version) ([]byte, error) {
|
|
content, err := xdb.LoadContentFromFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := verifyXDBVersion(content, version); err != nil {
|
|
return nil, fmt.Errorf("verify external xdb %s: %w", path, err)
|
|
}
|
|
return content, nil
|
|
}
|
|
|
|
func verifyXDBVersion(content []byte, expected *xdb.Version) error {
|
|
if len(content) < xdb.HeaderInfoLength {
|
|
return fmt.Errorf("xdb content too short: got %d bytes", len(content))
|
|
}
|
|
header, err := xdb.LoadHeaderFromBuff(content)
|
|
if err != nil {
|
|
return fmt.Errorf("load header: %w", err)
|
|
}
|
|
version, err := xdb.VersionFromHeader(header)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if version.Id != expected.Id {
|
|
return fmt.Errorf("ip version mismatch: got %s, want %s", version.Name, expected.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *IPRegionResolver) Lookup(rawIP string) string {
|
|
ipText := strings.TrimSpace(rawIP)
|
|
if ipText == "" {
|
|
return ""
|
|
}
|
|
if host, _, err := net.SplitHostPort(ipText); err == nil {
|
|
ipText = host
|
|
}
|
|
|
|
ip := net.ParseIP(ipText)
|
|
if ip == nil {
|
|
return ""
|
|
}
|
|
if ip.IsLoopback() {
|
|
return "本机"
|
|
}
|
|
if ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
|
return "内网IP"
|
|
}
|
|
if r == nil || r.searcher == nil {
|
|
return ""
|
|
}
|
|
|
|
region, err := r.searcher.Search(ip.String())
|
|
if err != nil {
|
|
if r.logger != nil {
|
|
r.logger.Warn("ops audit ip2region lookup failed",
|
|
zap.String("ip", ip.String()),
|
|
zap.Error(err),
|
|
)
|
|
}
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(region)
|
|
}
|
|
|
|
type bufferIPRegionSearcher struct {
|
|
v4 *xdb.Searcher
|
|
v4Mu sync.Mutex
|
|
v6 *xdb.Searcher
|
|
v6Mu sync.Mutex
|
|
}
|
|
|
|
func (s *bufferIPRegionSearcher) Search(ip any) (string, error) {
|
|
ipBytes, err := parseIPBytes(ip)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
switch len(ipBytes) {
|
|
case xdb.IPv4.Bytes:
|
|
if s == nil || s.v4 == nil {
|
|
return "", nil
|
|
}
|
|
s.v4Mu.Lock()
|
|
defer s.v4Mu.Unlock()
|
|
return s.v4.Search(ipBytes)
|
|
case xdb.IPv6.Bytes:
|
|
if s == nil || s.v6 == nil {
|
|
return "", nil
|
|
}
|
|
s.v6Mu.Lock()
|
|
defer s.v6Mu.Unlock()
|
|
return s.v6.Search(ipBytes)
|
|
default:
|
|
return "", fmt.Errorf("invalid byte ip address with len=%d", len(ipBytes))
|
|
}
|
|
}
|
|
|
|
func (s *bufferIPRegionSearcher) Close() {
|
|
if s == nil {
|
|
return
|
|
}
|
|
if s.v4 != nil {
|
|
s.v4.Close()
|
|
}
|
|
if s.v6 != nil {
|
|
s.v6.Close()
|
|
}
|
|
}
|
|
|
|
func parseIPBytes(ip any) ([]byte, error) {
|
|
switch v := ip.(type) {
|
|
case string:
|
|
return xdb.ParseIP(v)
|
|
case []byte:
|
|
return v, nil
|
|
default:
|
|
return nil, fmt.Errorf("invalid ip value type %T", v)
|
|
}
|
|
}
|
|
|
|
func (r *IPRegionResolver) Close() {
|
|
if r != nil && r.searcher != nil {
|
|
r.searcher.Close()
|
|
}
|
|
}
|