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:
Patrick Britton 2026-02-01 12:34:26 -06:00
parent 89901ab3f2
commit 5782563236
5 changed files with 151 additions and 0 deletions

View file

@ -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})

View file

@ -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

View file

@ -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=

View 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]
}

View 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."