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
71248NewLocalLoopback 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" )
75256if err != nil {
76257 panic (err)
77258}
78259fmt.Printf (" Serving HTTP %+v \n " , dual.AllAddr ())
79260fmt.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
154335Close closes all internal channels
@@ -162,7 +343,7 @@ do not defer Close\(\) if passing to http.Server
162343func (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
171352func (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
176449Generated by [gomarkdoc](<https:// github.com/princjef/gomarkdoc>)
0 commit comments