From d8e17e2da3c99cf312388d8f9ac1f54732a5c18f Mon Sep 17 00:00:00 2001 From: djkazic Date: Sat, 10 May 2025 18:00:53 -0400 Subject: [PATCH] proxy: implement blocklist --- aperture.go | 4 ++- config.go | 4 +++ proxy/proxy.go | 23 ++++++++++++++- proxy/proxy_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++-- sample-conf.yaml | 4 +++ 5 files changed, 101 insertions(+), 4 deletions(-) diff --git a/aperture.go b/aperture.go index 35cec7d..b19599f 100644 --- a/aperture.go +++ b/aperture.go @@ -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 } diff --git a/config.go b/config.go index 685427d..cba9d38 100644 --- a/config.go +++ b/config.go @@ -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{}, } } diff --git a/proxy/proxy.go b/proxy/proxy.go index c134123..242b9ff 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -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" { diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 3e37e75..64a2372 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -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, diff --git a/sample-conf.yaml b/sample-conf.yaml index bfcb1d5..5e19b80 100644 --- a/sample-conf.yaml +++ b/sample-conf.yaml @@ -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.