feat!: Migration auf Go-Binary
BREAKING CHANGE: Die alte Shell-Version muss vor der Installation der Go-Version deinstalliert werden.
This commit is contained in:
245
internal/geoip/geoip.go
Normal file
245
internal/geoip/geoip.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
LoadGeoIPCache(ttl, dbMtime int64) (map[string]string, error)
|
||||
UpsertGeoIP(ip, country string, dbMtime int64) error
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
DBPath string
|
||||
effectivePath string
|
||||
LicenseKey string
|
||||
Dir string
|
||||
TTL int64
|
||||
Store Store
|
||||
reader *maxminddb.Reader
|
||||
cache map[string]string
|
||||
mtime int64
|
||||
}
|
||||
|
||||
func New(dbPath, licenseKey, dir string, ttl int64, store Store) *Resolver {
|
||||
return &Resolver{DBPath: dbPath, LicenseKey: licenseKey, Dir: dir, TTL: ttl, Store: store, cache: map[string]string{}}
|
||||
}
|
||||
|
||||
func (r *Resolver) Open(ctx context.Context) error {
|
||||
path := r.DBPath
|
||||
if path == "" && r.LicenseKey != "" {
|
||||
var err error
|
||||
path, err = r.ensureAutoDB(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
r.effectivePath = path
|
||||
st, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader, err := maxminddb.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.reader = reader
|
||||
r.mtime = st.ModTime().Unix()
|
||||
if r.Store != nil {
|
||||
if c, err := r.Store.LoadGeoIPCache(r.TTL, r.mtime); err == nil {
|
||||
r.cache = c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resolver) Close() error {
|
||||
if r.reader != nil {
|
||||
return r.reader.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Resolver) Lookup(ip string) (string, error) {
|
||||
if v, ok := r.cache[ip]; ok {
|
||||
return v, nil
|
||||
}
|
||||
if r.reader == nil {
|
||||
return r.lookupLegacy(ip)
|
||||
}
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed == nil {
|
||||
return "", fmt.Errorf("invalid IP %q", ip)
|
||||
}
|
||||
var rec struct {
|
||||
Country struct {
|
||||
ISOCode string `maxminddb:"iso_code"`
|
||||
} `maxminddb:"country"`
|
||||
RegisteredCountry struct {
|
||||
ISOCode string `maxminddb:"iso_code"`
|
||||
} `maxminddb:"registered_country"`
|
||||
}
|
||||
if err := r.reader.Lookup(parsed, &rec); err != nil {
|
||||
return "", err
|
||||
}
|
||||
cc := strings.ToUpper(rec.Country.ISOCode)
|
||||
if cc == "" {
|
||||
cc = strings.ToUpper(rec.RegisteredCountry.ISOCode)
|
||||
}
|
||||
if cc != "" {
|
||||
r.cache[ip] = cc
|
||||
if r.Store != nil {
|
||||
_ = r.Store.UpsertGeoIP(ip, cc, r.mtime)
|
||||
}
|
||||
}
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupLegacy(ip string) (string, error) {
|
||||
if strings.Contains(ip, ":") {
|
||||
if cc, err := runGeoIPCommand("geoiplookup6", ip); err == nil && cc != "" {
|
||||
return cc, nil
|
||||
}
|
||||
} else {
|
||||
if cc, err := runGeoIPCommand("geoiplookup", ip); err == nil && cc != "" {
|
||||
return cc, nil
|
||||
}
|
||||
}
|
||||
if r.effectivePath != "" {
|
||||
if cc, err := runGeoIPCommand("mmdblookup", "--file", r.effectivePath, "--ip", ip, "country", "iso_code"); err == nil && cc != "" {
|
||||
return cc, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no GeoIP result for %s", ip)
|
||||
}
|
||||
|
||||
func runGeoIPCommand(name string, args ...string) (string, error) {
|
||||
if _, err := exec.LookPath(name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
out, err := exec.Command(name, args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
re := regexp.MustCompile(`\b[A-Z]{2}\b`)
|
||||
matches := re.FindAllString(string(out), -1)
|
||||
for _, m := range matches {
|
||||
if m != "IP" {
|
||||
return strings.ToUpper(m), nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func ShouldBlock(country, mode string, countries []string) bool {
|
||||
if country == "" || len(countries) == 0 {
|
||||
return false
|
||||
}
|
||||
found := false
|
||||
country = strings.ToUpper(country)
|
||||
for _, c := range countries {
|
||||
if strings.ToUpper(strings.TrimSpace(c)) == country {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if strings.ToLower(mode) == "allowlist" {
|
||||
return !found
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
func IsPrivateIP(s string) bool {
|
||||
if p, err := netip.ParsePrefix(s); err == nil {
|
||||
return isPrivateAddr(p.Addr())
|
||||
}
|
||||
a, err := netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return isPrivateAddr(a)
|
||||
}
|
||||
|
||||
func isPrivateAddr(a netip.Addr) bool {
|
||||
return a.IsPrivate() || a.IsLoopback() || a.IsLinkLocalUnicast() || a.IsUnspecified() ||
|
||||
(a.Is4() && strings.HasPrefix(a.String(), "100.") && isCGNAT(a))
|
||||
}
|
||||
|
||||
func isCGNAT(a netip.Addr) bool {
|
||||
p := a.As4()
|
||||
return p[0] == 100 && p[1] >= 64 && p[1] <= 127
|
||||
}
|
||||
|
||||
func (r *Resolver) ensureAutoDB(ctx context.Context) (string, error) {
|
||||
if err := os.MkdirAll(r.Dir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
dst := filepath.Join(r.Dir, "GeoLite2-Country.mmdb")
|
||||
if st, err := os.Stat(dst); err == nil && time.Since(st.ModTime()) < 24*time.Hour {
|
||||
return dst, nil
|
||||
}
|
||||
url := "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=" + r.LicenseKey + "&suffix=tar.gz"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("MaxMind download failed: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
gzr, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer gzr.Close()
|
||||
tr := tar.NewReader(gzr)
|
||||
tmp := dst + ".tmp"
|
||||
for {
|
||||
h, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if h.FileInfo().IsDir() || filepath.Base(h.Name) != "GeoLite2-Country.mmdb" {
|
||||
continue
|
||||
}
|
||||
f, err := os.Create(tmp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, copyErr := io.Copy(f, tr)
|
||||
closeErr := f.Close()
|
||||
if copyErr != nil {
|
||||
return "", copyErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return "", closeErr
|
||||
}
|
||||
return dst, os.Rename(tmp, dst)
|
||||
}
|
||||
return "", fmt.Errorf("GeoLite2-Country.mmdb not found in archive")
|
||||
}
|
||||
30
internal/geoip/geoip_test.go
Normal file
30
internal/geoip/geoip_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package geoip
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestShouldBlockModes(t *testing.T) {
|
||||
countries := []string{"CN", "RU"}
|
||||
if !ShouldBlock("cn", "blocklist", countries) {
|
||||
t.Fatal("blocklist should block listed country")
|
||||
}
|
||||
if ShouldBlock("DE", "blocklist", countries) {
|
||||
t.Fatal("blocklist should allow unlisted country")
|
||||
}
|
||||
if ShouldBlock("CN", "allowlist", countries) {
|
||||
t.Fatal("allowlist should allow listed country")
|
||||
}
|
||||
if !ShouldBlock("DE", "allowlist", countries) {
|
||||
t.Fatal("allowlist should block unlisted country")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPrivateIP(t *testing.T) {
|
||||
for _, ip := range []string{"127.0.0.1", "192.168.1.10", "10.1.2.3", "100.64.0.1", "::1", "fd00::1"} {
|
||||
if !IsPrivateIP(ip) {
|
||||
t.Fatalf("%s should be private", ip)
|
||||
}
|
||||
}
|
||||
if IsPrivateIP("8.8.8.8") {
|
||||
t.Fatal("8.8.8.8 should be public")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user