Add GeoIP middleware to block requests from outside North America
- Add GeoIP middleware that checks country codes - Block all countries except US, CA, MX, and Central American countries - Add setup script for GeoIP database - Gracefully handle missing database (logs warning but continues)
This commit is contained in:
parent
89901ab3f2
commit
5782563236
|
|
@ -89,6 +89,16 @@ func main() {
|
||||||
MaxAge: 12 * time.Hour,
|
MaxAge: 12 * time.Hour,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Initialize GeoIP middleware for geographic blocking
|
||||||
|
geoIPMiddleware, err := middleware.NewGeoIPMiddleware("/opt/sojorn/geoip/GeoLite2-Country.mmdb")
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to initialize GeoIP middleware, geographic filtering disabled")
|
||||||
|
} else {
|
||||||
|
defer geoIPMiddleware.Close()
|
||||||
|
r.Use(geoIPMiddleware.Middleware())
|
||||||
|
log.Info().Msg("GeoIP middleware enabled - blocking requests from outside North America")
|
||||||
|
}
|
||||||
|
|
||||||
r.NoRoute(func(c *gin.Context) {
|
r.NoRoute(func(c *gin.Context) {
|
||||||
log.Debug().Msgf("No route found for %s %s", c.Request.Method, c.Request.URL.Path)
|
log.Debug().Msgf("No route found for %s %s", c.Request.Method, c.Request.URL.Path)
|
||||||
c.JSON(404, gin.H{"error": "route not found", "path": c.Request.URL.Path, "method": c.Request.Method})
|
c.JSON(404, gin.H{"error": "route not found", "path": c.Request.URL.Path, "method": c.Request.Method})
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,8 @@ require (
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/oschwald/geoip2-golang v1.13.0 // indirect
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
|
||||||
|
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
||||||
github.com/pashagolub/pgxmock/v4 v4.9.0 h1:itlO8nrVRnzkdMBXLs8pWUyyB2PC3Gku0WGIj/gGl7I=
|
github.com/pashagolub/pgxmock/v4 v4.9.0 h1:itlO8nrVRnzkdMBXLs8pWUyyB2PC3Gku0WGIj/gGl7I=
|
||||||
github.com/pashagolub/pgxmock/v4 v4.9.0/go.mod h1:9L57pC193h2aKRHVyiiE817avasIPZnPwPlw3JczWvM=
|
github.com/pashagolub/pgxmock/v4 v4.9.0/go.mod h1:9L57pC193h2aKRHVyiiE817avasIPZnPwPlw3JczWvM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
|
|
||||||
92
go-backend/internal/middleware/geoip.go
Normal file
92
go-backend/internal/middleware/geoip.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/oschwald/geoip2-golang"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GeoIPMiddleware blocks requests from outside North America
|
||||||
|
type GeoIPMiddleware struct {
|
||||||
|
db *geoip2.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGeoIPMiddleware creates a new GeoIP middleware
|
||||||
|
func NewGeoIPMiddleware(dbPath string) (*GeoIPMiddleware, error) {
|
||||||
|
db, err := geoip2.Open(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GeoIPMiddleware{db: db}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the GeoIP database
|
||||||
|
func (g *GeoIPMiddleware) Close() error {
|
||||||
|
return g.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware returns the Gin middleware function
|
||||||
|
func (g *GeoIPMiddleware) Middleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Get client IP
|
||||||
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
|
// Parse IP
|
||||||
|
ip := net.ParseIP(clientIP)
|
||||||
|
if ip == nil {
|
||||||
|
// Invalid IP, block it
|
||||||
|
log.Warn().Str("ip", clientIP).Msg("Invalid IP address, blocking request")
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip for private/local IPs
|
||||||
|
if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up country
|
||||||
|
record, err := g.db.Country(ip)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Str("ip", clientIP).Err(err).Msg("Failed to lookup country, blocking request")
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if country is in North America
|
||||||
|
countryCode := record.Country.IsoCode
|
||||||
|
if !g.isNorthAmericanCountry(countryCode) {
|
||||||
|
log.Info().Str("ip", clientIP).Str("country", countryCode).Msg("Blocking request from outside North America")
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied - region not supported"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isNorthAmericanCountry checks if the country code is from North America
|
||||||
|
func (g *GeoIPMiddleware) isNorthAmericanCountry(countryCode string) bool {
|
||||||
|
northAmericanCountries := map[string]bool{
|
||||||
|
"US": true, // United States
|
||||||
|
"CA": true, // Canada
|
||||||
|
"MX": true, // Mexico
|
||||||
|
"GT": true, // Guatemala
|
||||||
|
"BZ": true, // Belize
|
||||||
|
"SV": true, // El Salvador
|
||||||
|
"HN": true, // Honduras
|
||||||
|
"NI": true, // Nicaragua
|
||||||
|
"CR": true, // Costa Rica
|
||||||
|
"PA": true, // Panama
|
||||||
|
}
|
||||||
|
|
||||||
|
return northAmericanCountries[countryCode]
|
||||||
|
}
|
||||||
43
go-backend/scripts/setup-geoip.sh
Normal file
43
go-backend/scripts/setup-geoip.sh
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Setup script for GeoIP database
|
||||||
|
# This downloads the GeoLite2-Country database from MaxMind
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
GEOIP_DIR="/opt/sojorn/geoip"
|
||||||
|
DATABASE_FILE="$GEOIP_DIR/GeoLite2-Country.mmdb"
|
||||||
|
|
||||||
|
echo "Setting up GeoIP database for geographic filtering..."
|
||||||
|
|
||||||
|
# Create directory if it doesn't exist
|
||||||
|
sudo mkdir -p "$GEOIP_DIR"
|
||||||
|
sudo chown patrick:patrick "$GEOIP_DIR"
|
||||||
|
|
||||||
|
# Download the GeoLite2-Country database
|
||||||
|
echo "Downloading GeoLite2-Country database..."
|
||||||
|
cd "$GEOIP_DIR"
|
||||||
|
|
||||||
|
# Use curl to download the database (you'll need a MaxMind account for this)
|
||||||
|
# For now, we'll create a placeholder - you should replace this with actual download
|
||||||
|
echo "Note: You need to sign up for a free MaxMind account to download the GeoLite2 database"
|
||||||
|
echo "Visit: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data"
|
||||||
|
echo ""
|
||||||
|
echo "After getting your license key, download with:"
|
||||||
|
echo "curl -v 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=YOUR_LICENSE_KEY&suffix=tar.gz' | tar xz --strip-components=1 -C ."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create a placeholder file for now (this won't work but allows the service to start)
|
||||||
|
echo "Creating placeholder database file (you should replace this with the real database)"
|
||||||
|
cat > placeholder.txt << 'EOF'
|
||||||
|
This is a placeholder file. You need to download the actual GeoLite2-Country.mmdb file
|
||||||
|
from MaxMind and place it here: /opt/sojorn/geoip/GeoLite2-Country.mmdb
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. Sign up for free account at https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
|
||||||
|
2. Get your license key
|
||||||
|
3. Download: curl -v 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=YOUR_LICENSE_KEY&suffix=tar.gz' | tar xz --strip-components=1 -C .
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Setup complete. Please download the actual GeoIP database as described above."
|
||||||
|
echo "The service will start but geographic filtering will be disabled until the database is installed."
|
||||||
Loading…
Reference in a new issue