Skip to main content

Echo IP Filtering

Introduction

IP filtering is an essential security technique that allows you to control which IP addresses can access your web application. By implementing IP filtering in your Echo applications, you can:

  • Restrict access to sensitive endpoints to only trusted IP addresses
  • Block malicious or suspicious IP addresses
  • Create private APIs accessible only from specific locations
  • Implement geolocation-based access controls

In this guide, you'll learn how to implement IP filtering in the Echo framework to enhance the security of your web applications.

Understanding IP Filtering

IP filtering works by checking the client's IP address against a predefined list of allowed or blocked addresses before processing the request. This check happens early in the request lifecycle, usually as middleware, preventing unauthorized requests from reaching your application logic.

Types of IP Filtering

There are several approaches to IP filtering:

  1. Allowlist (Whitelist): Only specified IP addresses can access the application; all others are blocked
  2. Blocklist (Blacklist): Specified IP addresses are blocked; all others can access
  3. Mixed approach: Different rules for different endpoints
  4. CIDR notation support: Filtering entire IP ranges rather than individual addresses

Basic IP Filtering in Echo

Let's start by implementing a simple IP filtering middleware in Echo:

go
package main

import (
"net"
"net/http"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

func main() {
e := echo.New()

// Add basic logging and recovery middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Create an IP filter middleware
e.Use(IPFilterMiddleware([]string{"127.0.0.1", "192.168.1.1"}))

e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, you've passed the IP check!")
})

e.Logger.Fatal(e.Start(":8080"))
}

// IPFilterMiddleware creates a middleware that only allows requests from specified IPs
func IPFilterMiddleware(allowedIPs []string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get IP from request
ip := c.RealIP()

// Check if IP is allowed
allowed := false
for _, allowedIP := range allowedIPs {
if ip == allowedIP {
allowed = true
break
}
}

if !allowed {
return c.String(http.StatusForbidden, "Access denied")
}

return next(c)
}
}
}

In this example:

  1. We create a custom middleware function that accepts a list of allowed IP addresses
  2. For each request, we extract the client's real IP address using Echo's c.RealIP() method
  3. We check if the IP is in our allowed list
  4. If not allowed, we return a 403 Forbidden response
  5. If allowed, we call the next handler in the chain

Testing the IP Filter

You can test this implementation by:

  1. Running the application
  2. Accessing it from your localhost (which will be allowed if you kept 127.0.0.1 in the list)
  3. Trying to access it from another machine or modifying the allowed IPs to exclude your IP

Advanced IP Filtering Techniques

Using CIDR Notation for IP Ranges

Often, you'll want to allow entire IP ranges rather than individual addresses. CIDR notation is perfect for this:

go
func IPFilterWithCIDRMiddleware(allowedCIDRs []string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ip := net.ParseIP(c.RealIP())
if ip == nil {
return c.String(http.StatusBadRequest, "Invalid IP address")
}

allowed := false
for _, cidr := range allowedCIDRs {
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
continue // Skip invalid CIDR notation
}

if ipNet.Contains(ip) {
allowed = true
break
}
}

if !allowed {
return c.String(http.StatusForbidden, "Access denied")
}

return next(c)
}
}
}

Usage example:

go
// Allow all IPs in the local network plus localhost
e.Use(IPFilterWithCIDRMiddleware([]string{"127.0.0.1/32", "192.168.1.0/24"}))

Combined Allow/Block List Approach

A more sophisticated approach is to maintain both allow and block lists:

go
type IPFilterConfig struct {
AllowedIPs []string
BlockedIPs []string
AllowedCIDRs []string
BlockedCIDRs []string
DefaultPolicy string // "allow" or "block"
}

func AdvancedIPFilterMiddleware(config IPFilterConfig) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
clientIP := net.ParseIP(c.RealIP())
if clientIP == nil {
return c.String(http.StatusBadRequest, "Invalid IP address")
}

// Check exact IP matches first
for _, blockedIP := range config.BlockedIPs {
if c.RealIP() == blockedIP {
return c.String(http.StatusForbidden, "Your IP is blocked")
}
}

for _, allowedIP := range config.AllowedIPs {
if c.RealIP() == allowedIP {
return next(c) // Explicitly allowed
}
}

// Then check CIDR ranges
for _, cidr := range config.BlockedCIDRs {
_, ipNet, err := net.ParseCIDR(cidr)
if err == nil && ipNet.Contains(clientIP) {
return c.String(http.StatusForbidden, "Your IP range is blocked")
}
}

for _, cidr := range config.AllowedCIDRs {
_, ipNet, err := net.ParseCIDR(cidr)
if err == nil && ipNet.Contains(clientIP) {
return next(c) // Explicitly allowed by range
}
}

// Apply default policy
if config.DefaultPolicy == "allow" {
return next(c)
}

return c.String(http.StatusForbidden, "Access denied by default policy")
}
}
}

Usage example:

go
e.Use(AdvancedIPFilterMiddleware(IPFilterConfig{
AllowedIPs: []string{"127.0.0.1"},
BlockedIPs: []string{"192.168.1.5"},
AllowedCIDRs: []string{"10.0.0.0/8"},
BlockedCIDRs: []string{"192.168.1.0/24"},
DefaultPolicy: "block", // Block all IPs by default
}))

Route-Specific IP Filtering

Sometimes you only want to apply IP filtering to specific routes, especially for admin endpoints:

go
// Public endpoint - no IP filtering
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Public area - accessible to everyone")
})

// Create an admin group with IP filtering
admin := e.Group("/admin")
admin.Use(IPFilterMiddleware([]string{"127.0.0.1", "10.0.0.5"}))

// All routes in the admin group require IP filtering
admin.GET("/dashboard", func(c echo.Context) error {
return c.String(http.StatusOK, "Admin dashboard - restricted access")
})

admin.GET("/settings", func(c echo.Context) error {
return c.String(http.StatusOK, "Admin settings - restricted access")
})

Real-World Implementation Example

Here's a complete example showing how to implement IP filtering in a real-world Echo application:

go
package main

import (
"net"
"net/http"
"strings"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

type IPFilter struct {
AllowedIPs []string
AllowedCIDRs []string
DefaultPolicy string
}

func main() {
e := echo.New()

// Add basic middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Public routes
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to our API!")
})

e.GET("/docs", func(c echo.Context) error {
return c.String(http.StatusOK, "API Documentation")
})

// Private API with IP filtering
apiFilter := IPFilter{
AllowedIPs: []string{"127.0.0.1"},
AllowedCIDRs: []string{
"10.0.0.0/8", // Internal network
"192.168.0.0/16", // Another internal range
},
DefaultPolicy: "block",
}

api := e.Group("/api/v1")
api.Use(createIPFilterMiddleware(apiFilter))

api.GET("/data", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "You have access to the API",
"client_ip": c.RealIP(),
})
})

// Admin panel with stricter IP filtering
adminFilter := IPFilter{
AllowedIPs: []string{"127.0.0.1"},
AllowedCIDRs: []string{
"10.0.0.5/32", // Only specific admin IP
},
DefaultPolicy: "block",
}

admin := e.Group("/admin")
admin.Use(createIPFilterMiddleware(adminFilter))

admin.GET("/dashboard", func(c echo.Context) error {
return c.String(http.StatusOK, "Admin Dashboard")
})

e.Logger.Fatal(e.Start(":8080"))
}

func createIPFilterMiddleware(filter IPFilter) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
clientIP := c.RealIP()

// Check direct IP matches
for _, allowedIP := range filter.AllowedIPs {
if clientIP == allowedIP {
return next(c)
}
}

// Check CIDR ranges
ip := net.ParseIP(clientIP)
if ip != nil {
for _, cidr := range filter.AllowedCIDRs {
_, ipNet, err := net.ParseCIDR(cidr)
if err == nil && ipNet.Contains(ip) {
return next(c)
}
}
}

// Apply default policy
if filter.DefaultPolicy == "allow" {
return next(c)
}

// Log the blocked request
c.Logger().Warnf("Blocked request from unauthorized IP: %s", clientIP)

return c.JSON(http.StatusForbidden, map[string]string{
"error": "Access denied",
"message": "Your IP address is not authorized to access this resource",
})
}
}
}

Best Practices for IP Filtering

  1. Always use c.RealIP() instead of parsing headers manually, as Echo handles the logic for extracting the real client IP even behind proxies

  2. Consider X-Forwarded-For headers carefully - if your application is behind a proxy, configure Echo appropriately:

go
e.IPExtractor = echo.ExtractIPFromXFFHeader()
  1. Combine with other security measures - IP filtering is just one layer of defense. Combine with:

    • Authentication
    • Rate limiting
    • TLS/HTTPS
    • Input validation
  2. Implement proper logging - Always log denied access attempts to detect potential attacks

  3. Review and update your IP lists regularly - IP addresses can change, and static rules may become outdated

  4. Consider dynamic IP filtering - For more sophisticated protection, implement dynamic IP blocking based on suspicious behavior

Handling Edge Cases

X-Forwarded-For and Proxy Considerations

When your application is behind a load balancer or proxy, the client's IP address is typically passed in the X-Forwarded-For header. Echo's c.RealIP() handles this correctly in most cases, but you may need custom configuration:

go
// Custom IP extraction logic
e.IPExtractor = func(r *http.Request) string {
// Trust specific proxies and get real client IP
forwardedFor := r.Header.Get("X-Forwarded-For")
if forwardedFor != "" {
// Get the first IP in the list (client IP)
ips := strings.Split(forwardedFor, ",")
if len(ips) > 0 {
clientIP := strings.TrimSpace(ips[0])
return clientIP
}
}

// Fallback to RemoteAddr
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
return ip
}

IPv6 Support

Don't forget to handle IPv6 addresses in your filtering logic:

go
func IPFilterMiddlewareWithIPv6(allowedIPs []string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
clientIP := c.RealIP()
parsedIP := net.ParseIP(clientIP)

if parsedIP == nil {
return c.String(http.StatusBadRequest, "Invalid IP address")
}

// Convert to IPv4 if it's an IPv4-mapped IPv6 address
if parsedIP.To4() != nil {
parsedIP = parsedIP.To4()
}

// Check if IP is allowed
allowed := false
for _, allowedIP := range allowedIPs {
allowedParsedIP := net.ParseIP(allowedIP)
if allowedParsedIP == nil {
continue
}

if parsedIP.Equal(allowedParsedIP) {
allowed = true
break
}
}

if !allowed {
return c.String(http.StatusForbidden, "Access denied")
}

return next(c)
}
}
}

Summary

IP filtering is a powerful security technique that helps you control access to your Echo web applications based on client IP addresses. In this guide, you've learned:

  1. How to implement basic IP filtering in Echo using custom middleware
  2. Advanced techniques like CIDR notation for IP range filtering
  3. How to combine allowlists and blocklists for more sophisticated control
  4. Route-specific IP filtering for protecting sensitive endpoints
  5. Handling edge cases like proxy configurations and IPv6 addresses
  6. Best practices to ensure your implementation is secure and maintainable

Remember that IP filtering is just one layer in a comprehensive security strategy. While it provides valuable protection, it should be combined with other security measures like authentication, rate limiting, and proper input validation.

Exercises

  1. Implement an IP filtering middleware that blocks access to your application's admin panel from outside your company's IP range.

  2. Create a dynamic IP filtering system that temporarily blocks IPs after multiple failed login attempts.

  3. Implement geolocation-based filtering using a GeoIP database to restrict access to certain countries.

  4. Build a logging system that records all blocked IP access attempts with timestamps and attempted URLs.

  5. Create an IP filtering middleware that uses environment variables to configure allowed IP ranges.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)