Skip to content

Commit f2ab25a

Browse files
committed
added middleware and firewall
1 parent cbee02b commit f2ab25a

File tree

9 files changed

+731
-13
lines changed

9 files changed

+731
-13
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
README.md:multilistener/*
2-
gomarkdoc --header-file header.in --output README.md ./multilistener
1+
README.md:multilistener/* linter/* middleware/* header.in
2+
gomarkdoc --header-file header.in --output README.md ./...
33

44
lint:
55
golangci-lint run

README.md

Lines changed: 282 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,183 @@
33
[![Go Test](https://github.com/tonymet/dualstack/actions/workflows/go.yml/badge.svg)](https://github.com/tonymet/dualstack/actions/workflows/go.yml)
44
[![Go Reference](https://pkg.go.dev/badge/github.com/tonymet/dualstack.svg)](https://pkg.go.dev/github.com/tonymet/dualstack)
55

6+
## Dualstack -- utilities to ease migration to ipv6
7+
8+
### Overview
9+
10+
* multilistener -- listen on multiple local loopback interfaces with multilistener.NewLocalLoopback()
11+
* middleware -- block remote connections on net.Listener and http.Server. see middleware.FirewallListener and middleware.LocalOnlyMiddleware
12+
* linter -- identify ipv6 anti-patterns
13+
14+
# linter
15+
16+
```go
17+
import "github.com/tonymet/dualstack/linter"
18+
```
19+
20+
## Index
21+
22+
- [Variables](<#variables>)
23+
24+
25+
## Variables
26+
27+
<a name="AnalyzerIP4"></a>Analyzer is the core component of our static analysis checker. It defines the name, documentation, and the function that performs the analysis.
28+
29+
```go
30+
var AnalyzerIP4 = &analysis.Analyzer{
31+
Name: "ipv4checker",
32+
Doc: "Reports calls to net.Listen using a hardcoded IPv4 loopback address.",
33+
Run: runIP4,
34+
}
35+
```
36+
37+
<a name="AnalyzerParseIP"></a>The Analyzer's name and description.
38+
39+
```go
40+
var AnalyzerParseIP = &analysis.Analyzer{
41+
Name: "checkip",
42+
Doc: "checks for net.ParseIP calls without a net.IP.To4() check",
43+
Run: runParseIP,
44+
Requires: []*analysis.Analyzer{
45+
inspect.Analyzer,
46+
},
47+
}
48+
```
49+
50+
<a name="Analyzers"></a>
51+
52+
```go
53+
var Analyzers []*analysis.Analyzer = make([]*analysis.Analyzer, 0)
54+
```
55+
56+
# middleware
57+
58+
```go
59+
import "github.com/tonymet/dualstack/middleware"
60+
```
61+
62+
## Index
63+
64+
- [Variables](<#variables>)
65+
- [func LocalOnlyMiddleware\(next http.Handler\) http.Handler](<#LocalOnlyMiddleware>)
66+
- [type FirewallListener](<#FirewallListener>)
67+
- [func NewFirewallListener\(l net.Listener\) \*FirewallListener](<#NewFirewallListener>)
68+
- [func \(fl \*FirewallListener\) Accept\(\) \(net.Conn, error\)](<#FirewallListener.Accept>)
69+
70+
71+
## Variables
72+
73+
<a name="ErrFirewall"></a>
74+
75+
```go
76+
var ErrFirewall = errors.New("blocked remote addr")
77+
```
78+
79+
<a name="ErrIPError"></a>
80+
81+
```go
82+
var ErrIPError = errors.New("error reading remote IP")
83+
```
84+
85+
<a name="LocalOnlyMiddleware"></a>
86+
## func LocalOnlyMiddleware
87+
88+
```go
89+
func LocalOnlyMiddleware(next http.Handler) http.Handler
90+
```
91+
92+
LocalOnlyMiddleware checks if a request is coming from a local interface by accessing the actual connection's remote address. This version uses a type assertion to get the binary IP address directly, avoiding string parsing.
93+
94+
<details><summary>Example</summary>
95+
<p>
96+
97+
ExampleLocalOnlyMiddleware example of wrapping a common handler
98+
99+
```go
100+
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
101+
w.WriteHeader(http.StatusOK)
102+
w.Write([]byte("Allowed")) // nolint:errcheck
103+
})
104+
protectedHandler := LocalOnlyMiddleware(nextHandler)
105+
// for common apps use http.Handle("/", protectedHandler)
106+
ts := httptest.NewServer(protectedHandler)
107+
defer ts.Close()
108+
109+
// Create a request with a local remote address
110+
reqLocal := httptest.NewRequest("GET", ts.URL, nil)
111+
reqLocal.RemoteAddr = "127.0.0.1:12345" // Simulate a local client
112+
113+
// Create a response recorder
114+
rrLocal := httptest.NewRecorder()
115+
116+
// Serve the request through the middleware
117+
protectedHandler.ServeHTTP(rrLocal, reqLocal)
118+
119+
fmt.Printf("Local Request Status: %d\n", rrLocal.Result().StatusCode)
120+
fmt.Printf("Local Request Body: %s\n", rrLocal.Body.String())
121+
122+
// --- Test Case 2: Request from a non-local IP (should be forbidden) ---
123+
// Create a request with a non-local remote address
124+
reqRemote := httptest.NewRequest("GET", ts.URL, nil)
125+
reqRemote.RemoteAddr = "192.168.1.100:54321" // Simulate a remote client
126+
127+
// Create a response recorder
128+
rrRemote := httptest.NewRecorder()
129+
130+
// Serve the request through the middleware
131+
protectedHandler.ServeHTTP(rrRemote, reqRemote)
132+
133+
fmt.Printf("Remote Request Status: %d\n", rrRemote.Result().StatusCode)
134+
fmt.Printf("Remote Request Body: %s\n", rrRemote.Body.String())
135+
// Output:
136+
// Local Request Status: 200
137+
// Local Request Body: Allowed
138+
// Remote Request Status: 403
139+
// Remote Request Body: Forbidden
140+
```
141+
142+
#### Output
143+
144+
```
145+
Local Request Status: 200
146+
Local Request Body: Allowed
147+
Remote Request Status: 403
148+
Remote Request Body: Forbidden
149+
```
150+
151+
</p>
152+
</details>
153+
154+
<a name="FirewallListener"></a>
155+
## type FirewallListener
156+
157+
FirewallListener wraps a net.Listener to block non\-localhost connections.
158+
159+
```go
160+
type FirewallListener struct {
161+
net.Listener
162+
}
163+
```
164+
165+
<a name="NewFirewallListener"></a>
166+
### func NewFirewallListener
167+
168+
```go
169+
func NewFirewallListener(l net.Listener) *FirewallListener
170+
```
171+
172+
NewFirewallListener creates and returns a new FirewallListener that wraps an existing listener.
173+
174+
<a name="FirewallListener.Accept"></a>
175+
### func \(\*FirewallListener\) Accept
176+
177+
```go
178+
func (fl *FirewallListener) Accept() (net.Conn, error)
179+
```
180+
181+
Accept is the middleware for our firewall. It wraps the underlying Accept call, inspects the connection's IP address, and blocks it if it's not a localhost address.
182+
6183
# multilistener
7184

8185
```go
@@ -27,7 +204,7 @@ use multilistener.ListenLocalLoopback to return a single Listener for all ipv4 &
27204
- [func \(dl \*MultiListener\) Accept\(\) \(net.Conn, error\)](<#MultiListener.Accept>)
28205
- [func \(dl \*MultiListener\) Addr\(\) net.Addr](<#MultiListener.Addr>)
29206
- [func \(dl \*MultiListener\) AllAddr\(\) net.Addr](<#MultiListener.AllAddr>)
30-
- [func \(dl \*MultiListener\) Close\(\) error](<#MultiListener.Close>)
207+
- [func \(dl \*MultiListener\) Close\(\) \(err error\)](<#MultiListener.Close>)
31208
- [func \(dl \*MultiListener\) Network\(\) string](<#MultiListener.Network>)
32209
- [func \(dl \*MultiListener\) String\(\) string](<#MultiListener.String>)
33210

@@ -71,23 +248,27 @@ ipv6 is the preferred address when Addr\(\) is called
71248
NewLocalLoopback when you want to listen to ipv6 & ipv4 loopback with one listener
72249

73250
```go
74-
dual, err := NewLocalLoopback("8080")
251+
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
252+
fmt.Fprint(w, "Hello World!\n")
253+
})
254+
255+
dual, err := NewLocalLoopback("8129")
75256
if err != nil {
76257
panic(err)
77258
}
78259
fmt.Printf("Serving HTTP %+v\n", dual.AllAddr())
79260
fmt.Printf("Preferred Addr: %+v\n", dual.Addr())
80-
go http.Serve(dual, nil)
261+
go http.Serve(dual, nil) //nolint:errcheck
81262
// Output:
82-
// Serving HTTP [::1]:8080,127.0.0.1:8080
83-
// Preferred Addr: [::1]:8080
263+
// Serving HTTP [::1]:8129,127.0.0.1:8129
264+
// Preferred Addr: [::1]:8129
84265
```
85266

86267
#### Output
87268

88269
```
89-
Serving HTTP [::1]:8080,127.0.0.1:8080
90-
Preferred Addr: [::1]:8080
270+
Serving HTTP [::1]:8129,127.0.0.1:8129
271+
Preferred Addr: [::1]:8129
91272
```
92273

93274
</p>
@@ -148,7 +329,7 @@ NOTE: NOT A VALID IP ADDRESS . Use Addr\(\) for a valid address
148329
### func \(\*MultiListener\) Close
149330

150331
```go
151-
func (dl *MultiListener) Close() error
332+
func (dl *MultiListener) Close() (err error)
152333
```
153334

154335
Close closes all internal channels
@@ -162,7 +343,7 @@ do not defer Close\(\) if passing to http.Server
162343
func (dl *MultiListener) Network() string
163344
```
164345

165-
net.Addr.Network\(\) implementation
346+
Network\(\) implementation for net.Addr
166347

167348
<a name="MultiListener.String"></a>
168349
### func \(\*MultiListener\) String
@@ -171,6 +352,98 @@ net.Addr.Network\(\) implementation
171352
func (dl *MultiListener) String() string
172353
```
173354

355+
String\(\) joins all addresses, comma separated, for logs & debug
356+
357+
# http\-server\-ipv6
358+
359+
```go
360+
import "github.com/tonymet/dualstack/cmd/http-server-ipv6"
361+
```
362+
363+
## Index
364+
365+
- [func ListenAll\(\)](<#ListenAll>)
366+
- [func ListenAndBindLocal\(\)](<#ListenAndBindLocal>)
367+
- [func ListenWithMultiListener\(\)](<#ListenWithMultiListener>)
368+
- [type DSListener](<#DSListener>)
369+
- [func ListenLocal\(\) \(dsl DSListener, err error\)](<#ListenLocal>)
370+
371+
372+
<a name="ListenAll"></a>
373+
## func ListenAll
374+
375+
```go
376+
func ListenAll()
377+
```
378+
379+
380+
381+
<a name="ListenAndBindLocal"></a>
382+
## func ListenAndBindLocal
383+
384+
```go
385+
func ListenAndBindLocal()
386+
```
387+
388+
389+
390+
<a name="ListenWithMultiListener"></a>
391+
## func ListenWithMultiListener
392+
393+
```go
394+
func ListenWithMultiListener()
395+
```
396+
397+
398+
399+
<a name="DSListener"></a>
400+
## type DSListener
401+
402+
403+
404+
```go
405+
type DSListener struct {
406+
// contains filtered or unexported fields
407+
}
408+
```
409+
410+
<a name="ListenLocal"></a>
411+
### func ListenLocal
412+
413+
```go
414+
func ListenLocal() (dsl DSListener, err error)
415+
```
416+
417+
418+
419+
# linter
420+
421+
```go
422+
import "github.com/tonymet/dualstack/cmd/linter"
423+
```
424+
425+
## Index
426+
427+
428+
429+
# simple\-listener
430+
431+
```go
432+
import "github.com/tonymet/dualstack/cmd/simple-listener"
433+
```
434+
435+
## Index
436+
437+
438+
439+
# bad\-go\-code
440+
441+
```go
442+
import "github.com/tonymet/dualstack/internal/bad-go-code"
443+
```
444+
445+
## Index
446+
174447

175448

176449
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)

header.in

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
[![Go Test](https://github.com/tonymet/dualstack/actions/workflows/go.yml/badge.svg)](https://github.com/tonymet/dualstack/actions/workflows/go.yml)
2-
[![Go Reference](https://pkg.go.dev/badge/github.com/tonymet/dualstack.svg)](https://pkg.go.dev/github.com/tonymet/dualstack)
2+
[![Go Reference](https://pkg.go.dev/badge/github.com/tonymet/dualstack.svg)](https://pkg.go.dev/github.com/tonymet/dualstack)
3+
4+
## Dualstack -- utilities to ease migration to ipv6
5+
6+
### Overview
7+
8+
* multilistener -- listen on multiple local loopback interfaces with multilistener.NewLocalLoopback()
9+
* middleware -- block remote connections on net.Listener and http.Server. see middleware.FirewallListener and middleware.LocalOnlyMiddleware
10+
* linter -- identify ipv6 anti-patterns

internal/bad-go-code/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ func main() {
1212

1313
// net.ParseIP handles both IPv4 and IPv6. To check that it's *not* IPv4,
1414
// you can see if the To4() method returns nil.
15-
fmt.Printf(ip.String())
15+
fmt.Println(ip.String())
1616
}

0 commit comments

Comments
 (0)