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 ¶
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)
}
Output:
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
}
Output:
func (Name) Control ¶
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. |