We recognize there are still barriers to ipv6 adoption. This project aims to identify anti-patterns blocking ipv6 / dual stack
compatibility like net.Listen("tcp", "127.0.0.1") . First , ip6check is a linter/checker
/analyzer that identifies faulty code. dualstack offers a few ipv6-compatible
approaches to secure those interfaces: multilistener to listen to multiple interfaces with a single Accept(), middleware
to block remote http traffic, and firewall to block remote tcp connections.
Now that the utilties are mature, the next step is to expand the lint suite and enter PRs on open source projects to help improve ipv6 compatibility
net.Listen("tcp", ":" + port)is the preferred dual stack listener on all interfaces. The kernel will handle ipv4 & ipv6 connections. But for loopback services like oauth, this risks exposure to the internet
e.g.
l, _ := net.Listen("tcp", ":" + port)
protectedListener := middleware.FirewallListener{l}
//
for{
// use as usual
conn, err := protectedListener.Accept()
if err == net.errClosed{
return
} else if err != nil{
// connection was blocked
continue
}
go handleConnection(conn)
}
ml, err := multilistener.NewLocalLoopback()
if err != nil{
panic(err)
}
// http will serve on [::1] and 127.0.0.1
http.Serve(ml, nil)
LocalOnlyMiddleware adds a remote-address filter before your handler. Any remote address will receive 403 / unauthorized.
func ExampleLocalOnlyMiddleware() {
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Allowed"))
})
protectedHandler := LocalOnlyMiddleware(nextHandler)
// for common apps use http.Handle("/", protectedHandler)
ts := httptest.NewServer(protectedHandler)
defer ts.Close()
go install github.com/tonymet/dualstack/cmd/ip6check
ip6check ./internal/bad-go-code
bad-go-code/main.go:11:2: call to `net.ParseIP` should be followed by a check for IPv4 or handle IPv6 compatibility
You can pull and run the Docker image for your CI workflow
docker run -v$(pwd):/workspace us-west1-docker.pkg.dev/tonym-us/dualstack/ip6check /workspace/main.go
Developers are familiar with ipv4 as a stable and secure approach. Local oauth services are mature. But With IPv4 address exhaustion accelerating, ipv6 compatibility is more urgent now than it has been in the past.
- multilistener -- listen on multiple local loopback interfaces with multilistener.NewLocalLoopback()
- middleware -- block remote connections on net.Listener and http.Server. see middleware.FirewallListener and middleware.LocalOnlyMiddleware
- linter -- identify ipv6 anti-patterns
import "github.com/tonymet/dualstack/linter"Analyzer is the core component of our static analysis checker. It defines the name, documentation, and the function that performs the analysis.
var AnalyzerIP4 = &analysis.Analyzer{
Name: "ipv4checker",
Doc: "Reports calls to net.Listen using a hardcoded IPv4 loopback address.",
Run: runIP4,
}Analyzer is the entry point for our linter.
var AnalyzerIP4Byte = &analysis.Analyzer{
Name: "ipv4linter",
Doc: "Checks for incorrect IPv4 size assumptions on net.IP variables.",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: runIP4Byte,
}The Analyzer's name and description.
var AnalyzerParseIP = &analysis.Analyzer{
Name: "checkip",
Doc: "checks for net.ParseIP calls without a net.IP.To4() check",
Run: runParseIP,
Requires: []*analysis.Analyzer{
inspect.Analyzer,
},
}var Analyzers []*analysis.Analyzer = make([]*analysis.Analyzer, 0)import "github.com/tonymet/dualstack/middleware"var ErrFirewall = errors.New("blocked remote addr")var ErrIPError = errors.New("error reading remote IP")func LocalOnlyMiddleware(next http.Handler) http.HandlerLocalOnlyMiddleware 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.
Example
ExampleLocalOnlyMiddleware example of wrapping a common handler
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Allowed")) // nolint:errcheck
})
protectedHandler := LocalOnlyMiddleware(nextHandler)
// for common apps use http.Handle("/", protectedHandler)
ts := httptest.NewServer(protectedHandler)
defer ts.Close()
// Create a request with a local remote address
reqLocal := httptest.NewRequest("GET", ts.URL, nil)
reqLocal.RemoteAddr = "127.0.0.1:12345" // Simulate a local client
// Create a response recorder
rrLocal := httptest.NewRecorder()
// Serve the request through the middleware
protectedHandler.ServeHTTP(rrLocal, reqLocal)
fmt.Printf("Local Request Status: %d\n", rrLocal.Result().StatusCode)
fmt.Printf("Local Request Body: %s\n", rrLocal.Body.String())
// --- Test Case 2: Request from a non-local IP (should be forbidden) ---
// Create a request with a non-local remote address
reqRemote := httptest.NewRequest("GET", ts.URL, nil)
reqRemote.RemoteAddr = "192.168.1.100:54321" // Simulate a remote client
// Create a response recorder
rrRemote := httptest.NewRecorder()
// Serve the request through the middleware
protectedHandler.ServeHTTP(rrRemote, reqRemote)
fmt.Printf("Remote Request Status: %d\n", rrRemote.Result().StatusCode)
fmt.Printf("Remote Request Body: %s\n", rrRemote.Body.String())
// Output:
// Local Request Status: 200
// Local Request Body: Allowed
// Remote Request Status: 403
// Remote Request Body: ForbiddenLocal Request Status: 200
Local Request Body: Allowed
Remote Request Status: 403
Remote Request Body: Forbidden
FirewallListener wraps a net.Listener to block non-localhost connections.
type FirewallListener struct {
net.Listener
}func NewFirewallListener(l net.Listener) *FirewallListenerNewFirewallListener creates and returns a new FirewallListener that wraps an existing listener.
func (fl *FirewallListener) Accept() (net.Conn, error)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.
import "github.com/tonymet/dualstack/multilistener"Package multilistener -- listen to all loopback interfaces, or a mixed slice of ipv4 & ipv6 interfaces
go std library (net.Listen("tcp", ":8080")) can listen to ALL interfaces but cannot listen to all local interfaces by default.
use multilistener.ListenLocalLoopback to return a single Listener for all ipv4 & ipv6 loopback interfaces
©️ 2025 Anthony Metzidis
- type Addresses
- type MultiListener
- func NewLocalLoopback(port string) (*MultiListener, error)
- func NewMultiListener(addrs Addresses) (*MultiListener, error)
- func NewMultiListenerRaw(listeners []net.Listener) (*MultiListener, error)
- func (dl *MultiListener) Accept() (net.Conn, error)
- func (dl *MultiListener) Addr() net.Addr
- func (dl *MultiListener) AllAddr() net.Addr
- func (dl *MultiListener) Close() (err error)
- func (dl *MultiListener) Network() string
- func (dl *MultiListener) String() string
type Addresses = []stringMultiListener implements net.Listener interface
multiplexes multiple net.Listeners concurrently looping over Accept()
type MultiListener struct {
// contains filtered or unexported fields
}func NewLocalLoopback(port string) (*MultiListener, error)NewLocalLoopback returns Multilistener on ipv6 & ipv4 loopback addresses
ipv6 is the preferred address when Addr() is called
Example
NewLocalLoopback when you want to listen to ipv6 & ipv4 loopback with one listener
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World!\n")
})
dual, err := NewLocalLoopback("8129")
if err != nil {
panic(err)
}
fmt.Printf("Serving HTTP %+v\n", dual.AllAddr())
fmt.Printf("Preferred Addr: %+v\n", dual.Addr())
go http.Serve(dual, nil) //nolint:errcheck
// Output:
// Serving HTTP [::1]:8129,127.0.0.1:8129
// Preferred Addr: [::1]:8129Serving HTTP [::1]:8129,127.0.0.1:8129
Preferred Addr: [::1]:8129
func NewMultiListener(addrs Addresses) (*MultiListener, error)NewMultiListener returns multilistener over slice of []string
see net.Dial and net.Listen for the string format of the address e.g. "[::1]:8080" for ipv6 and "127.0.0.1:8080" for ipv4
func NewMultiListenerRaw(listeners []net.Listener) (*MultiListener, error)NewMultiListenerRaw returns a MultiListener wrapper of multiple listeners
useful when raw net.Addr or net.Listener is needed
func (dl *MultiListener) Accept() (net.Conn, error)func (dl *MultiListener) Addr() net.AddrAddr returns the preferred (first) interface Addr
func (dl *MultiListener) AllAddr() net.AddrAllAddr returns all the addresses, comma-separated
NOTE: NOT A VALID IP ADDRESS . Use Addr() for a valid address
func (dl *MultiListener) Close() (err error)Close closes all internal channels
do not defer Close() if passing to http.Server
func (dl *MultiListener) Network() stringNetwork() implementation for net.Addr
func (dl *MultiListener) String() stringString() joins all addresses, comma separated, for logs & debug
import "github.com/tonymet/dualstack/cmd/http-server-ipv6"func ListenAll()func ListenAndBindLocal()func ListenWithMultiListener()type DSListener struct {
// contains filtered or unexported fields
}func ListenLocal() (dsl DSListener, err error)import "github.com/tonymet/dualstack/cmd/ip6check"import "github.com/tonymet/dualstack/cmd/simple-listener"import "github.com/tonymet/dualstack/internal/bad-go-code"import "github.com/tonymet/dualstack/middleware/testing"middleware/testing package with mock interfaces for testing net.Listener
MockConn is a fake net.Conn for testing purposes.
type MockConn struct {
net.Conn
// contains filtered or unexported fields
}func (m *MockConn) Close() errorfunc (m *MockConn) RemoteAddr() net.AddrMockListener is a fake net.Listener for testing purposes.
type MockListener struct {
net.Listener
// contains filtered or unexported fields
}func NewMockListener() *MockListenerfunc (m *MockListener) Accept() (net.Conn, error)func (m *MockListener) Addr() net.Addrfunc (m *MockListener) Close() errorimport "github.com/tonymet/dualstack/linter/testdata/ip4byte"import "github.com/tonymet/dualstack/linter/testdata/parseip"func GoodIpv4()Generated by gomarkdoc