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