Skip to content

Commit d8e17e2

Browse files
committed
proxy: implement blocklist
1 parent b05d801 commit d8e17e2

File tree

5 files changed

+101
-4
lines changed

5 files changed

+101
-4
lines changed

aperture.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,9 @@ func createProxy(cfg *Config, challenger challenger.Challenger,
862862
},
863863
))
864864

865-
prxy, err := proxy.New(authenticator, cfg.Services, localServices...)
865+
prxy, err := proxy.New(
866+
authenticator, cfg.Services, cfg.Blocklist, localServices...,
867+
)
866868
return prxy, proxyCleanup, err
867869
}
868870

config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ type Config struct {
226226

227227
// Logging controls various aspects of aperture logging.
228228
Logging *build.LogConfig `group:"logging" namespace:"logging"`
229+
230+
// Blocklist is a list of IPs to deny access to.
231+
Blocklist []string `long:"blocklist" description:"List of IP addresses to block from accessing the proxy."`
229232
}
230233

231234
func (c *Config) validate() error {
@@ -270,5 +273,6 @@ func NewConfig() *Config {
270273
WriteTimeout: defaultWriteTimeout,
271274
InvoiceBatchSize: defaultInvoiceBatchSize,
272275
Logging: build.DefaultLogConfig(),
276+
Blocklist: []string{},
273277
}
274278
}

proxy/proxy.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/tls"
55
"crypto/x509"
66
"fmt"
7+
"net"
78
"net/http"
89
"net/http/httputil"
910
"os"
@@ -73,18 +74,30 @@ type Proxy struct {
7374
localServices []LocalService
7475
authenticator auth.Authenticator
7576
services []*Service
77+
blocklist map[string]struct{}
7678
}
7779

7880
// New returns a new Proxy instance that proxies between the services specified,
7981
// using the auth to validate each request's headers and get new challenge
8082
// headers if necessary.
8183
func New(auth auth.Authenticator, services []*Service,
82-
localServices ...LocalService) (*Proxy, error) {
84+
blocklist []string, localServices ...LocalService) (*Proxy, error) {
85+
86+
blMap := make(map[string]struct{})
87+
for _, ip := range blocklist {
88+
parsed := net.ParseIP(ip)
89+
if parsed == nil {
90+
log.Warnf("Could not parse IP %q in blocklist; skipping", ip)
91+
continue
92+
}
93+
blMap[parsed.String()] = struct{}{}
94+
}
8395

8496
proxy := &Proxy{
8597
localServices: localServices,
8698
authenticator: auth,
8799
services: services,
100+
blocklist: blMap,
88101
}
89102
err := proxy.UpdateServices(services)
90103
if err != nil {
@@ -106,6 +119,14 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
106119
}
107120
defer logRequest()
108121

122+
// Blocklist check
123+
if _, blocked := p.blocklist[remoteIP.String()]; blocked {
124+
log.Debugf("Blocked request from IP: %s", remoteIP)
125+
addCorsHeaders(w.Header())
126+
sendDirectResponse(w, r, http.StatusForbidden, "access denied")
127+
return
128+
}
129+
109130
// For OPTIONS requests we only need to set the CORS headers, not serve
110131
// any content;
111132
if r.Method == "OPTIONS" {

proxy/proxy_test.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,72 @@ func TestProxyHTTP(t *testing.T) {
111111
}
112112
}
113113

114+
// TestProxyHTTPBlocklist tests that the proxy can block HTTP requests from
115+
// a blocked IP.
116+
func TestProxyHTTPBlocklist(t *testing.T) {
117+
services := []*proxy.Service{{
118+
Address: testTargetServiceAddress,
119+
HostRegexp: testHostRegexp,
120+
PathRegexp: testPathRegexpHTTP,
121+
Protocol: "http",
122+
Auth: "off",
123+
}}
124+
125+
mockAuth := auth.NewMockAuthenticator()
126+
127+
// Block the IP that will be used in the request.
128+
blockedIP := "127.0.0.1"
129+
p, err := proxy.New(mockAuth, services, []string{blockedIP})
130+
require.NoError(t, err)
131+
132+
// Start the proxy server.
133+
server := &http.Server{
134+
Addr: testProxyAddr,
135+
Handler: http.HandlerFunc(p.ServeHTTP),
136+
}
137+
go func() {
138+
if err := server.ListenAndServe(); err != http.ErrServerClosed {
139+
t.Errorf("proxy serve error: %v", err)
140+
}
141+
}()
142+
defer closeOrFail(t, server)
143+
144+
// Start the backend server.
145+
backendService := &http.Server{Addr: testTargetServiceAddress}
146+
go func() { _ = startBackendHTTP(backendService) }()
147+
defer closeOrFail(t, backendService)
148+
149+
time.Sleep(100 * time.Millisecond)
150+
151+
// Make a request with a spoofed RemoteAddr that matches the blocklist.
152+
req, err := http.NewRequest(
153+
"GET",
154+
fmt.Sprintf("http://%s/http/test", testProxyAddr),
155+
nil,
156+
)
157+
require.NoError(t, err)
158+
159+
// Create a custom transport to override the local IP — simulate blocked IP.
160+
customTransport := &http.Transport{
161+
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
162+
lAddr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
163+
d := net.Dialer{LocalAddr: lAddr}
164+
return d.DialContext(context.Background(), "tcp", testProxyAddr)
165+
},
166+
}
167+
client := &http.Client{Transport: customTransport}
168+
169+
resp, err := client.Do(req)
170+
require.NoError(t, err)
171+
defer resp.Body.Close()
172+
173+
require.Equal(t, http.StatusForbidden, resp.StatusCode)
174+
175+
body, err := io.ReadAll(resp.Body)
176+
require.NoError(t, err)
177+
require.Equal(t, "access denied\n", string(body))
178+
}
179+
114180
// runHTTPTest tests that the proxy can forward HTTP requests to a backend
115181
// service and handle L402 authentication correctly.
116182
func runHTTPTest(t *testing.T, tc *testCase, method string) {
@@ -125,7 +191,7 @@ func runHTTPTest(t *testing.T, tc *testCase, method string) {
125191
}}
126192

127193
mockAuth := auth.NewMockAuthenticator()
128-
p, err := proxy.New(mockAuth, services)
194+
p, err := proxy.New(mockAuth, services, []string{})
129195
require.NoError(t, err)
130196

131197
// Start server that gives requests to the proxy.
@@ -288,7 +354,7 @@ func runGRPCTest(t *testing.T, tc *testCase) {
288354

289355
// Create the proxy server and start serving on TLS.
290356
mockAuth := auth.NewMockAuthenticator()
291-
p, err := proxy.New(mockAuth, services)
357+
p, err := proxy.New(mockAuth, services, []string{})
292358
require.NoError(t, err)
293359
server := &http.Server{
294360
Addr: testProxyAddr,

sample-conf.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ authenticator:
6161
# Set to true to skip verification of the mailbox server's tls cert.
6262
devserver: false
6363

64+
# List of IPs to block from accessing the proxy.
65+
blocklist:
66+
- "1.1.1.1"
67+
- "1.0.0.1"
6468

6569
# The selected database backend. The current default backend is "sqlite".
6670
# Aperture also has support for postgres and etcd.

0 commit comments

Comments
 (0)