Thanks to visit codestin.com
Credit goes to github.com

Skip to content

tonymet/dualstack

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Go Test Go Reference

Dualstack -- utilities to ease migration to ipv6

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

How to Listen Properly to support ipv6 and ipv4 dual stack

  • 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

To protect a net.Listener, you can wrap with middleware.FirewallListener

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)
}

To listen to all localhost interfaces , use multilistener

ml, err := multilistener.NewLocalLoopback()
if err != nil{
    panic(err)
}
// http will serve on [::1] and 127.0.0.1
http.Serve(ml, nil)

How to Protect an Existing http.Server

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()

ip6check -- Install & Run

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

ip6check Docker Image

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

Why Now?

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.

Package Overview

  • 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

linter

import "github.com/tonymet/dualstack/linter"

Index

Variables

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)

middleware

import "github.com/tonymet/dualstack/middleware"

Index

Variables

var ErrFirewall = errors.New("blocked remote addr")

var ErrIPError = errors.New("error reading remote IP")

func LocalOnlyMiddleware

func LocalOnlyMiddleware(next http.Handler) http.Handler

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.

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: Forbidden

Output

Local Request Status: 200
Local Request Body: Allowed
Remote Request Status: 403
Remote Request Body: Forbidden

type FirewallListener

FirewallListener wraps a net.Listener to block non-localhost connections.

type FirewallListener struct {
    net.Listener
}

func NewFirewallListener

func NewFirewallListener(l net.Listener) *FirewallListener

NewFirewallListener creates and returns a new FirewallListener that wraps an existing listener.

func (*FirewallListener) Accept

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.

multilistener

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

Index

type Addresses

type Addresses = []string

type MultiListener

MultiListener implements net.Listener interface

multiplexes multiple net.Listeners concurrently looping over Accept()

type MultiListener struct {
    // contains filtered or unexported fields
}

func NewLocalLoopback

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]:8129

Output

Serving HTTP [::1]:8129,127.0.0.1:8129
Preferred Addr: [::1]:8129

func NewMultiListener

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

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 (*MultiListener) Accept

func (dl *MultiListener) Accept() (net.Conn, error)

func (*MultiListener) Addr

func (dl *MultiListener) Addr() net.Addr

Addr returns the preferred (first) interface Addr

func (*MultiListener) AllAddr

func (dl *MultiListener) AllAddr() net.Addr

AllAddr returns all the addresses, comma-separated

NOTE: NOT A VALID IP ADDRESS . Use Addr() for a valid address

func (*MultiListener) Close

func (dl *MultiListener) Close() (err error)

Close closes all internal channels

do not defer Close() if passing to http.Server

func (*MultiListener) Network

func (dl *MultiListener) Network() string

Network() implementation for net.Addr

func (*MultiListener) String

func (dl *MultiListener) String() string

String() joins all addresses, comma separated, for logs & debug

http-server-ipv6

import "github.com/tonymet/dualstack/cmd/http-server-ipv6"

Index

func ListenAll

func ListenAll()

func ListenAndBindLocal

func ListenAndBindLocal()

func ListenWithMultiListener

func ListenWithMultiListener()

type DSListener

type DSListener struct {
    // contains filtered or unexported fields
}

func ListenLocal

func ListenLocal() (dsl DSListener, err error)

ip6check

import "github.com/tonymet/dualstack/cmd/ip6check"

Index

simple-listener

import "github.com/tonymet/dualstack/cmd/simple-listener"

Index

bad-go-code

import "github.com/tonymet/dualstack/internal/bad-go-code"

Index

testing

import "github.com/tonymet/dualstack/middleware/testing"

middleware/testing package with mock interfaces for testing net.Listener

Index

type MockConn

MockConn is a fake net.Conn for testing purposes.

type MockConn struct {
    net.Conn
    // contains filtered or unexported fields
}

func (*MockConn) Close

func (m *MockConn) Close() error

func (*MockConn) RemoteAddr

func (m *MockConn) RemoteAddr() net.Addr

type MockListener

MockListener is a fake net.Listener for testing purposes.

type MockListener struct {
    net.Listener
    // contains filtered or unexported fields
}

func NewMockListener

func NewMockListener() *MockListener

func (*MockListener) Accept

func (m *MockListener) Accept() (net.Conn, error)

func (*MockListener) Addr

func (m *MockListener) Addr() net.Addr

func (*MockListener) Close

func (m *MockListener) Close() error

a

import "github.com/tonymet/dualstack/linter/testdata/ip4byte"

Index

parseip

import "github.com/tonymet/dualstack/linter/testdata/parseip"

Index

func GoodIpv4

func GoodIpv4()

Generated by gomarkdoc

About

Go module for network dualstack ipv4 & ipv6

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages