Help us shape the future of Go! Take the 2026 Go Developer Survey

ipfilter

package
v0.3.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2026 License: Apache-2.0 Imports: 6 Imported by: 1

Documentation

Overview

Package ipfilter refuses requests by client address with 403 ip_not_allowed (ADR-0085):

allow, err := ipfilter.ParsePrefixes(os.Getenv("OFFICE_RANGES"))
onlyOffice, err := ipfilter.New(allow, nil)
handler := httpx.Chain(mux, httpx.TrustedProxies(proxies), onlyOffice)

gorbital.dev/gorbital/opshttp puts every /ops/ route behind it when OPS_ALLOWED_IPS is set.

It is a package of its own, not part of httpx, because apps generated by orb v0.1 record every problem code of the gorbital.dev packages they link in api/surface.json: a new code in httpx would fail their tests (docs/guides/stability.md).

Stability: experimental until v0.2.0 (ADR-0015, ADR-0081).

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrDenyAll = errors.New("ipfilter: a deny range can't cover every address; list the allowed ranges instead")

ErrDenyAll reports a deny range covering every IPv4 or IPv6 address, which would refuse every request of that family.

Functions

func New

func New(allow, deny []netip.Prefix) (httpx.Middleware, error)

New returns middleware that refuses requests by client address with a 403 problem, code "ip_not_allowed". The address is the request's RemoteAddr, so behind a proxy install httpx.TrustedProxies first. A request is refused when its address is in a deny range, or when allow isn't empty and its address is in no allow range: deny wins, and an empty allow list allows every address not denied. A RemoteAddr that isn't an IP address is refused. IPv4-mapped IPv6 addresses are compared as IPv4, and IPv6 zones are ignored.

It returns an error for an invalid range, a deny range covering every address of a family (ErrDenyAll; list allowed ranges instead), or an allow range entirely inside a deny range, which could never match. With both lists empty the middleware changes nothing.

Example
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"

	"gorbital.dev/httpx"
	"gorbital.dev/httpx/ipfilter"
)

func main() {
	allow, err := ipfilter.ParsePrefixes("10.0.0.0/8, 2001:db8::/32")
	if err != nil {
		panic(err)
	}
	onlyOffice, err := ipfilter.New(allow, nil)
	if err != nil {
		panic(err)
	}
	ops := httpx.Chain(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte("ok"))
	}), onlyOffice)

	for _, addr := range []string{"10.1.2.3:52000", "203.0.113.9:52000"} {
		req := httptest.NewRequest(http.MethodGet, "/ops/system", nil)
		req.RemoteAddr = addr
		rec := httptest.NewRecorder()
		ops.ServeHTTP(rec, req)
		fmt.Println(addr, rec.Code, rec.Body.String() == "ok" || problemCode(rec.Body.Bytes()) == "ip_not_allowed")
	}
}

// problemCode returns the code of a problem response body.
func problemCode(body []byte) string {
	var p httpx.Problem
	_ = json.Unmarshal(body, &p)
	return p.Code
}
Output:
10.1.2.3:52000 200 true
203.0.113.9:52000 403 true
Example (BehindAProxy)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"gorbital.dev/httpx"
	"gorbital.dev/httpx/ipfilter"
)

func main() {
	// Behind a load balancer, resolve the client address first.
	proxies, _ := httpx.ParseTrustedProxies("10.0.0.0/8")
	deny, _ := ipfilter.ParsePrefixes("198.51.100.0/24")
	filter, err := ipfilter.New(nil, deny)
	if err != nil {
		panic(err)
	}
	h := httpx.Chain(http.NotFoundHandler(), httpx.TrustedProxies(proxies), filter)

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.RemoteAddr = "10.0.0.2:443"
	req.Header.Set("X-Forwarded-For", "198.51.100.7")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	fmt.Println(rec.Code)
}
Output:
403

func ParsePrefixes

func ParsePrefixes(list string) ([]netip.Prefix, error)

ParsePrefixes reads a comma-separated list of CIDR ranges or single addresses, such as "10.0.0.0/8, 2001:db8::/32, 192.0.2.10", for New. Ranges are masked ("10.0.0.5/8" is 10.0.0.0/8), and IPv4 addresses written in IPv6 form ("::ffff:10.0.0.5") become IPv4, as client addresses are compared. An empty list returns nil.

Example
package main

import (
	"fmt"

	"gorbital.dev/httpx/ipfilter"
)

func main() {
	p, err := ipfilter.ParsePrefixes("10.0.0.5/8, 192.0.2.10, ::ffff:198.51.100.1")
	fmt.Println(p, err)
	_, err = ipfilter.ParsePrefixes("office-vpn")
	fmt.Println(err)
}
Output:
[10.0.0.0/8 192.0.2.10/32 198.51.100.1/32] <nil>
ipfilter: "office-vpn" is not a CIDR range or IP address

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL