picnic

package module
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

picnic

Build Status codecov Go Report Card Apache V2 License GitHub Release GoDoc

pick NIC — bind sockets to a specific local network interface, cross-platform, with zero dependencies.

picnic works through the two callbacks the standard library funnels all socket creation through — net.Dialer.Control and net.ListenConfig.Control — so one mechanism covers the whole stack:

You want… Routes through picnic entry
raw TCP/UDP/unix dials, net/http, crypto/tls, WebSockets (over http.Client) net.Dialer.Control BindDialer
UDP → QUIC → HTTP/3 → WebTransport (e.g. quic-go over a net.PacketConn), TCP servers net.ListenConfig.Control BindListenConfig
anything else either callback Control
// Stream path — net/http, WebSocket, TLS, raw TCP:
var d net.Dialer
if err := picnic.Name("eth0").BindDialer(&d); err != nil {
    log.Fatal(err)
}
conn, err := d.DialContext(ctx, "tcp4", "example.com:443")

// Packet path — QUIC / HTTP-3 / WebTransport:
var lc net.ListenConfig
if err := picnic.Name("eth0").BindListenConfig(&lc); err != nil {
    log.Fatal(err)
}
pc, err := lc.ListenPacket(ctx, "udp4", ":0")   // hand pc to quic-go

How it works

picnic binds the device itself with the platform's interface-bind socket option. This steers egress without binding the socket's address, so it composes with a later connect (dialer) or bind (listener) — which is why one mechanism serves both the stream and packet paths.

Platform Mechanism Privilege
Linux SO_BINDTODEVICE needs CAP_NET_RAW
macOS IP_BOUND_IF / IPV6_BOUND_IF none
Windows IP_UNICAST_IF / IPV6_UNICAST_IF none

When the device option is unavailable (Linux without CAP_NET_RAW, or a BSD that has no such option), BindDialer falls back to binding a source address from the interface. A dialer can do this because connect supplies the destination; a listener cannot, because ListenPacket binds the socket itself — so on those platforms BindListenConfig leaves the socket un-bound to the interface.

You never pass an IP version: picnic derives the family (which selects the v4 vs v6 option) from the dial's network string (tcp4/tcp6) or the destination address.

Caveats

  • The source-address fallback is destination-blind. On an interface holding both a global unicast (GUA) and a unique local (ULA) IPv6 address, picnic prefers the GUA, since a ULA source cannot reach a global destination — but no in-process source selection is perfect without the route. For guaranteed egress, use the device-bind path (on Linux, grant CAP_NET_RAW) or pair source binding with OS policy routing.
  • BindListenConfig needs the device option. On Linux without CAP_NET_RAW, or a BSD with no per-socket interface bind, a ListenPacket socket cannot be interface-bound (its own bind precludes the source-address fallback). macOS and Windows need no privilege, so the common QUIC targets are covered.

Why a separate package

Binding to an interface is per-OS socket-option code (SO_BINDTODEVICE, IP_BOUND_IF, IP_UNICAST_IF). That can only be meaningfully tested on real Linux, macOS, and Windows runners — which is exactly what this repository's CI matrix does. Keeping it standalone also gives the wider Go ecosystem the small, permissively licensed, dependency-free helper it currently lacks.

License

Apache-2.0. See LICENSE.

Documentation

Overview

Package picnic ("pick NIC") binds sockets to a specific local network interface, cross-platform, with zero dependencies.

It works through the two callbacks the standard library funnels all socket creation through — net.Dialer.Control and net.ListenConfig.Control (same signature) — so a single mechanism covers raw TCP/UDP, net/http, crypto/tls (tls.Dialer), WebSocket libraries that dial over net/http, and the UDP/QUIC stacks behind HTTP-3 and WebTransport (e.g. quic-go over a net.PacketConn). Use BindDialer for the stream path, BindListenConfig for the packet path, or Control to attach binding to anything else.

picnic binds the device itself with the platform's interface-bind socket option — SO_BINDTODEVICE on Linux, IP_BOUND_IF/IPV6_BOUND_IF on macOS, IP_UNICAST_IF/IPV6_UNICAST_IF on Windows. These steer egress without binding the socket's address, so they compose with a later connect or bind. Only Linux's option requires elevated privilege (CAP_NET_RAW); the others do not.

When the device option is unavailable (Linux without CAP_NET_RAW, or a platform with no such option), BindDialer falls back to binding a source address belonging to the interface — a dialer can do this because connect supplies the destination, but a ListenConfig cannot, since ListenPacket binds the socket itself. So that source fallback applies to the dialer path only.

The destination's family (which selects the v4 vs v6 option) is taken from the dial's network string ("tcp4"/"tcp6") or, failing that, the destination IP, so callers never pass an IP version explicitly.

Caveat: the source-address fallback is destination-blind. On an interface holding both a global unicast address (GUA) and a unique local address (ULA), picnic prefers the GUA, since a ULA source cannot reach a global destination; but no in-process source selection can be perfect without the route. Where deterministic egress matters, use the device-bind path (on Linux, grant CAP_NET_RAW) or pair the source bind with OS policy routing.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Name

type Name string

Name is the name of a local network interface, e.g. "eth0", "en0", "Wi-Fi".

func (Name) BindDialer

func (n Name) BindDialer(d *net.Dialer) error

BindDialer configures d so that sockets it creates egress this interface, covering everything built on net.Dialer: raw TCP/UDP/unix dials, net/http (http.Transport.DialContext), crypto/tls (tls.Dialer.NetDialer), and WebSocket libraries that dial over an *http.Client. It validates that the interface exists and sets d.ControlContext (so it is honored even if a plain d.Control is also present).

Example

Send an HTTPS request out a specific interface. picnic governs only the TCP socket; net/http layers TLS for the https URL on top of it.

(No Output: line — this reaches the network and depends on the host's interfaces, so it is compiled as documentation but not run by `go test`.)

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"net/http"

	"github.com/xmidt-org/picnic"
)

func main() {
	// Binding the dialer is the entire integration.
	var dialer net.Dialer
	if err := picnic.Name("eth0").BindDialer(&dialer); err != nil {
		log.Fatal(err)
	}

	client := &http.Client{
		Transport: &http.Transport{DialContext: dialer.DialContext},
	}

	req, err := http.NewRequestWithContext(
		context.Background(), http.MethodGet, "https://github.com", nil)
	if err != nil {
		log.Fatal(err)
	}

	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	fmt.Println(resp.Status)
}

func (Name) BindListenConfig

func (n Name) BindListenConfig(lc *net.ListenConfig) error

BindListenConfig configures lc so that sockets it creates egress this interface, covering the packet path: net.ListenConfig.ListenPacket (UDP) — and thus QUIC, HTTP-3, and WebTransport stacks (e.g. quic-go) that run over a net.PacketConn you supply — as well as net.ListenConfig.Listen for servers.

It binds via the device socket option only; it does not use the dialer's source-address fallback, which would collide with ListenPacket's own bind. On a platform with no device option (and, for Linux, without CAP_NET_RAW) the socket is therefore not interface-bound.

Example

Open a UDP socket bound to a specific interface for a QUIC / HTTP-3 / WebTransport stack. The resulting net.PacketConn is what you hand to, e.g., quic-go's quic.Transport{Conn: pc}.

package main

import (
	"context"
	"log"
	"net"

	"github.com/xmidt-org/picnic"
)

func main() {
	var lc net.ListenConfig
	if err := picnic.Name("eth0").BindListenConfig(&lc); err != nil {
		log.Fatal(err)
	}

	pc, err := lc.ListenPacket(context.Background(), "udp4", ":0")
	if err != nil {
		log.Fatal(err)
	}
	defer pc.Close()

	// Hand pc to a QUIC stack to run HTTP-3 / WebTransport out this interface.
	_ = pc
}

func (Name) Control

func (n Name) Control() (func(network, address string, c syscall.RawConn) error, error)

Control returns the interface-binding callback. Its signature matches both net.Dialer.Control and net.ListenConfig.Control, so it can be attached to any standard-library socket constructor. Like BindListenConfig it binds via the device socket option only, without the dialer's source-address fallback (which would collide with a ListenConfig's own bind) — use BindDialer for that. It errors immediately if the interface name is empty or does not currently exist.

Directories

Path Synopsis
cmd
example command
Command example is a runnable demo of picnic: it sends an HTTPS request out a chosen local network interface and prints the source address actually used, proving the bind took effect.
Command example is a runnable demo of picnic: it sends an HTTPS request out a chosen local network interface and prints the source address actually used, proving the bind took effect.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL