proxy: implement blocklist

This commit is contained in:
djkazic
2025-05-10 18:00:53 -04:00
parent b05d801d8c
commit d8e17e2da3
5 changed files with 101 additions and 4 deletions

View File

@@ -862,7 +862,9 @@ func createProxy(cfg *Config, challenger challenger.Challenger,
},
))
prxy, err := proxy.New(authenticator, cfg.Services, localServices...)
prxy, err := proxy.New(
authenticator, cfg.Services, cfg.Blocklist, localServices...,
)
return prxy, proxyCleanup, err
}

View File

@@ -226,6 +226,9 @@ type Config struct {
// Logging controls various aspects of aperture logging.
Logging *build.LogConfig `group:"logging" namespace:"logging"`
// Blocklist is a list of IPs to deny access to.
Blocklist []string `long:"blocklist" description:"List of IP addresses to block from accessing the proxy."`
}
func (c *Config) validate() error {
@@ -270,5 +273,6 @@ func NewConfig() *Config {
WriteTimeout: defaultWriteTimeout,
InvoiceBatchSize: defaultInvoiceBatchSize,
Logging: build.DefaultLogConfig(),
Blocklist: []string{},
}
}

View File

@@ -4,6 +4,7 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
"net/http/httputil"
"os"
@@ -73,18 +74,30 @@ type Proxy struct {
localServices []LocalService
authenticator auth.Authenticator
services []*Service
blocklist map[string]struct{}
}
// New returns a new Proxy instance that proxies between the services specified,
// using the auth to validate each request's headers and get new challenge
// headers if necessary.
func New(auth auth.Authenticator, services []*Service,
localServices ...LocalService) (*Proxy, error) {
blocklist []string, localServices ...LocalService) (*Proxy, error) {
blMap := make(map[string]struct{})
for _, ip := range blocklist {
parsed := net.ParseIP(ip)
if parsed == nil {
log.Warnf("Could not parse IP %q in blocklist; skipping", ip)
continue
}
blMap[parsed.String()] = struct{}{}
}
proxy := &Proxy{
localServices: localServices,
authenticator: auth,
services: services,
blocklist: blMap,
}
err := proxy.UpdateServices(services)
if err != nil {
@@ -106,6 +119,14 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
defer logRequest()
// Blocklist check
if _, blocked := p.blocklist[remoteIP.String()]; blocked {
log.Debugf("Blocked request from IP: %s", remoteIP)
addCorsHeaders(w.Header())
sendDirectResponse(w, r, http.StatusForbidden, "access denied")
return
}
// For OPTIONS requests we only need to set the CORS headers, not serve
// any content;
if r.Method == "OPTIONS" {

View File

@@ -111,6 +111,72 @@ func TestProxyHTTP(t *testing.T) {
}
}
// TestProxyHTTPBlocklist tests that the proxy can block HTTP requests from
// a blocked IP.
func TestProxyHTTPBlocklist(t *testing.T) {
services := []*proxy.Service{{
Address: testTargetServiceAddress,
HostRegexp: testHostRegexp,
PathRegexp: testPathRegexpHTTP,
Protocol: "http",
Auth: "off",
}}
mockAuth := auth.NewMockAuthenticator()
// Block the IP that will be used in the request.
blockedIP := "127.0.0.1"
p, err := proxy.New(mockAuth, services, []string{blockedIP})
require.NoError(t, err)
// Start the proxy server.
server := &http.Server{
Addr: testProxyAddr,
Handler: http.HandlerFunc(p.ServeHTTP),
}
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
t.Errorf("proxy serve error: %v", err)
}
}()
defer closeOrFail(t, server)
// Start the backend server.
backendService := &http.Server{Addr: testTargetServiceAddress}
go func() { _ = startBackendHTTP(backendService) }()
defer closeOrFail(t, backendService)
time.Sleep(100 * time.Millisecond)
// Make a request with a spoofed RemoteAddr that matches the blocklist.
req, err := http.NewRequest(
"GET",
fmt.Sprintf("http://%s/http/test", testProxyAddr),
nil,
)
require.NoError(t, err)
// Create a custom transport to override the local IP — simulate blocked IP.
customTransport := &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
lAddr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
d := net.Dialer{LocalAddr: lAddr}
return d.DialContext(context.Background(), "tcp", testProxyAddr)
},
}
client := &http.Client{Transport: customTransport}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusForbidden, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "access denied\n", string(body))
}
// runHTTPTest tests that the proxy can forward HTTP requests to a backend
// service and handle L402 authentication correctly.
func runHTTPTest(t *testing.T, tc *testCase, method string) {
@@ -125,7 +191,7 @@ func runHTTPTest(t *testing.T, tc *testCase, method string) {
}}
mockAuth := auth.NewMockAuthenticator()
p, err := proxy.New(mockAuth, services)
p, err := proxy.New(mockAuth, services, []string{})
require.NoError(t, err)
// Start server that gives requests to the proxy.
@@ -288,7 +354,7 @@ func runGRPCTest(t *testing.T, tc *testCase) {
// Create the proxy server and start serving on TLS.
mockAuth := auth.NewMockAuthenticator()
p, err := proxy.New(mockAuth, services)
p, err := proxy.New(mockAuth, services, []string{})
require.NoError(t, err)
server := &http.Server{
Addr: testProxyAddr,

View File

@@ -61,6 +61,10 @@ authenticator:
# Set to true to skip verification of the mailbox server's tls cert.
devserver: false
# List of IPs to block from accessing the proxy.
blocklist:
- "1.1.1.1"
- "1.0.0.1"
# The selected database backend. The current default backend is "sqlite".
# Aperture also has support for postgres and etcd.