The mihomo IP stack (MIPS) is the small, pure-Go userspace IP stack developed for mihomo. It converts complete IPv4 and IPv6 packets from an arbitrary L3 link into standard Go TCP, UDP, and IP protocol socket interfaces. It requires Go 1.20 or later, uses only the Go standard library, and does not require cgo.
The package is independent of any particular link, routing policy, or L2 neighbor implementation. An embedding application owns route admission and packet delivery.
package main
import (
"context"
"net/netip"
"github.com/metacubex/mipstack"
)
func open(ctx context.Context, local netip.Prefix, destination netip.AddrPort) error {
stack, err := mipstack.New(mipstack.Config{
LocalAddresses: []netip.Prefix{local},
MTU: 1500,
TCP: mipstack.TCPSocketDefaults{
CongestionControl: mipstack.CongestionControlCUBIC,
},
})
if err != nil {
return err
}
defer stack.Close()
if err = stack.Start(); err != nil {
return err
}
connection, err := stack.DialTCP(ctx, "tcp", netip.AddrPort{}, destination)
if err != nil {
return err
}
return connection.Close()
}Stack.Write delivers complete inbound IP packets to the stack. Stack.Read
returns complete outbound IP packets for the embedding link:
_, err := stack.Write([][]byte{inboundPacket}, 0)
buffer := make([]byte, 65535)
sizes := []int{0}
_, err = stack.Read([][]byte{buffer}, sizes, 0)
outboundPacket := buffer[:sizes[0]]The packet methods intentionally match the common batched userspace-TUN shape.
Read blocks for one packet and then drains up to 64 currently queued packets
into the supplied buffers; BatchSize reports that upper bound. Write
accepts every packet supplied in an inbound batch. Start is idempotent, while
Close is terminal and unblocks pending packet and socket operations.
Read requires sizes to be at least as long as the buffer slice and honors
the same leading offset in every buffer. Both methods report the successfully
completed packet prefix before any later-buffer error, as expected by wireguard-go's
packet-device loops. They also accept a buffer slice larger than BatchSize
because a composite WireGuard device may use a larger Bind batch; Read
still returns no more than 64 packets.
For integration with userspace packet-device consumers, Stack also provides
MTU, Name, and BatchSize. LocalAddresses returns an independent
snapshot of every configured address in configuration order. Operating-system
file descriptors and event channels whose element type belongs to another
package are left to embedding adapters so MIPS remains standard-library-only.
MIPS provides:
DialTCPfor active IPv4 and IPv6 TCP connections;ListenTCPfor specific or wildcard passive TCP endpoints;ListenTCPReusePortfor flow-distributed shared TCP bindings;DialUDPfor connected UDP sockets;ListenUDPfor unconnected UDP packet sockets;ListenUDPReusePortfor flow-distributed shared UDP bindings;DialIPandListenIPfor connected and unconnected IPv4 or IPv6 protocol payload sockets, using standard network names such asip4:icmp,ip6:ipv6-icmp, andip:99;- exported
TCPConn,TCPListener,UDPConn, andIPConnimplementations of the corresponding standardnetinterfaces.
The three DialTCP, DialUDP, and DialIP entry points mirror the netip-based
methods available on newer net.Dialer versions: they accept a context,
network name, local address, and remote address, while returning net.Conn for
straightforward adapter use. The package remains buildable with Go 1.20.
Listen methods accept the standard tcp, tcp4, tcp6, udp, udp4, and
udp6 network names. An empty netip.Addr has the same wildcard meaning as a
nil IP in net.TCPAddr or net.UDPAddr; its port is retained. For the generic
network, a wildcard becomes one dual-stack [::] endpoint when both address
families are configured. The 4 and 6 forms select one family and reject an
explicit address from the other family.
ListenIP applies the same empty-address rules to ip, ip4, and ip6
protocol sockets. A generic ip:* wildcard receives both families when both
are configured. On unconnected UDP and IP sockets, Read reads a payload and
discards its source address, matching the corresponding standard connection
types; ReadFrom and message reads retain it.
Ordinary listeners have exclusive bindings. The explicit ReusePort methods
may share an address and port only with other ReusePort listeners. Exact
bindings take precedence over wildcard bindings, and a per-registry keyed hash
keeps each TCP or UDP flow on one group member. Closing a TCP listener permits
an immediate rebind while its already accepted connections remain active,
matching the SO_REUSEADDR behavior used by standard Go listeners.
Active sockets allocate from the IANA dynamic range (49152..65535) first.
Only when that range is unavailable for the requested binding or TCP tuple do
they fall back to non-privileged ports 1024..49151. Ports below 1024 are
never selected automatically but remain available for explicit bindings.
TCP combines an RFC 6056-style SipHash offset of the local and remote
endpoints with a keyed full-period scan step. Different destinations therefore
observe separated sequences, and a collision-free sequence does not revisit a
recently closed tuple until it has traversed the complete range. The keys and
initial cursors are read from system randomness once when the Stack is created;
socket creation does not perform another system-random read.
Config.MaxTCPConnections may impose an application-selected resource bound;
its zero value does not impose an artificial connection limit. Listener count
is controlled only by available memory and explicit application creation.
Config.Routes == nil installs one default route for each configured address
family. A non-nil empty route slice deliberately admits only destinations that
are themselves local. IPv4-only stacks accept MTUs down to 68; configurations
containing IPv6 require the IPv6 minimum MTU of 1280.
Config.TCP defines policies inherited by newly created connections and
listeners: initial and maximum automatic receive/send buffers, completed and
half-open listener queues, congestion control, keepalive, receive-idle timeout,
Nagle behavior, TCP user timeout, DSCP bits, and IPv6 Flow Label policy. A zero
TCP Flow Label selects a stable keyed label for the connection tuple; a
nonzero value fixes the label for new connections. Congestion control accepts
CongestionControlCUBIC, CongestionControlReno, or CongestionControlBBR;
its zero value selects CUBIC. UpdateConfig applies a changed congestion
controller to established connections without an explicit per-connection
override. Existing sockets retain the other inherited policies. Receive window
scale is selected per connection from the configured receive ceiling,
so deliberate small-buffer policies retain window precision while large-BDP
connections can use their full automatic maximum. Calling SetReadBuffer or
SetWriteBuffer locks that side to the application value and disables its
automatic growth, matching the user-locked behavior of operating-system TCP
stacks. Automatic growth follows application-consumed and acknowledged bytes
per RTT rather than queue size or cwnd alone, so short-RTT scheduler batches do
not inflate buffers. SetCongestionControl and SetTrafficClass provide
per-connection overrides.
Config.UDP and Config.IP define the receive-buffer capacity, default
TTL/Hop Limit, default TOS/Traffic Class, and IPv6 Flow Label policy inherited
by new datagram sockets. SetReadBuffer, SetHopLimit, SetTrafficClass, and
SetFlowLabel provide per-socket overrides. A zero configured Flow Label uses
a stable keyed label for each flow; explicitly setting a socket label to zero
disables automatic labeling. Nonzero message control fields override the
socket defaults; an explicit zero TOS/Traffic Class or Flow Label encoded in
raw OOB data remains distinguishable from an omitted field.
Socket operation failures use *net.OpError. errors.Is continues to identify
os.ErrDeadlineExceeded, net.ErrClosed, and syscall errors. Orderly TCP EOF
is returned directly as io.EOF, and destination-specific writes on connected
UDP or IP sockets retain net.ErrWriteToConnected. Validated asynchronous ICMP
details are available through errors.As to mipstack.ICMPError.
TCPConn.SetLinger provides background graceful close, abortive close, and a
bounded wait for acknowledgement. UDPConn.SetReadBuffer changes the receive
queue's approximate retained-memory capacity; payload and per-datagram
metadata both count toward the bound. UDP writes are synchronous with packet
delivery to the embedding device, so SetWriteBuffer is a validated no-op.
TCPConn.Info returns a consistent live diagnostic snapshot from the
connection actor and retains the final snapshot after close. It includes RFC
9293 state, endpoints, negotiated extensions, RTT/RTO, congestion controller,
cwnd and ssthresh, peer/receive windows, bytes in flight, path MTU and active
probe state, buffer occupancy and automatic limits, byte counters, recovery
state, inherited keepalive/Nagle/DSCP/Flow Label policies, window-scale values,
and connection-local retransmission, PMTU-probe, and spurious-recovery counters.
It also reports the current and peak byte-bounded actor queue occupancy and
queue drops, making scheduler or embedding-link backpressure distinguishable
from network loss.
TCPListener.Info reports current, capacity, and lifetime peak occupancy for
the accept and SYN backlogs, along with handshake, SYN-cookie, accept, timeout,
and queue-drop counters. UDPConn.Info and IPConn.Info expose endpoint
identity, queue occupancy, socket defaults, path MTU for connected sockets,
and cumulative accepted, dropped, and transmitted datagram counters. Both
retain the latest correlated ICMP error. An automatic IPConn Flow Label is
reported as zero because raw payload fields may select a different flow on
each write; fixed socket labels are reported directly.
UDP message methods use the Linux 64-bit little-endian control-message layout
on every host. ReadMsgUDP emits IP_PKTINFO or IPV6_PKTINFO for the local
destination plus TTL/Hop Limit and TOS/Traffic Class, with Linux
MSG_TRUNC/MSG_CTRUNC flags. Passing that data to WriteMsgUDP selects the
corresponding managed source and output header fields. IPv6 messages also
carry Linux IPV6_FLOWINFO. IPConn uses the same ancillary representation.
IPv4ControlMessage and IPv6ControlMessage make that ancillary data
structured rather than opaque. Their Parse methods decode OOB returned by a
message read; their Marshal methods encode Src, TTL/Hop Limit, and
TOS/Traffic Class for a message write. IPv6ControlMessage also exposes the
20-bit Flow Label. Dst is populated while parsing. IfIndex is always zero
because MIPS has one embedding link.
TCP implements active and passive open, bounded accept and SYN queues,
concurrent four-tuple demultiplexing, safe local-port reuse for distinct remote
tuples, and bounded active and TIME_WAIT state. Validated inbound segments wait
in a dynamically allocated, byte-bounded FIFO, so idle connections do not pay
for a large channel while high-throughput connections are not constrained by
an arbitrary segment count. Initial sequence numbers follow RFC 6528: a
four-microsecond monotonic counter is added to a SipHash-derived per-four-tuple
offset under a 128-bit per-stack secret. Its data path includes bounded
send and receive buffers, adaptive RTO with exponential backoff, selectable
CUBIC, Reno, and paced model-based BBR congestion control, window scaling,
delayed ACKs, SACK multi-hole recovery with Proportional Rate Reduction, RACK
time-based loss detection, tail-loss probes, timestamp negotiation with PAWS,
and classic ECN feedback.
On a SACK-negotiated connection, only newly reported scoreboard information
counts toward RFC 6675 DupAcks; repeated cumulative ACKs and window-probe
responses without new SACK data cannot manufacture a loss episode.
Initial Reno and CUBIC slow start uses RFC 9406 HyStart++ and Conservative Slow
Start; BBR retains its own Startup model. Eifel timestamps and conservative
DSACK accounting detect spurious fast retransmits and timeouts, while the RFC
4015 response bounds the restored congestion window and makes the RTO more
conservative after a spurious timeout. TCP also handles overlap-aware receive
reassembly, data-bearing zero-window probes, reset validation, deadlines,
half-close, FIN states, and TIME_WAIT.
RTT sampling uses packet arrival time rather than actor scheduling time. Its minimum is the Linux-style three-sample running minimum over a 300-second window, so a route change can replace stale path history without retaining an unbounded sample set. When receive work and a protocol timer become ready together, the actor drains the finite receive snapshot that was already queued before servicing the timer. Packets arriving during that turn are excluded, so host scheduling delay cannot manufacture loss and a continuous packet stream cannot starve retransmission, liveness, pacing, or PMTU timers. Transmission timestamps start when packets enter the embedding device queue, and loss timers defer while the original packet still occupies that FIFO, so link backpressure is not misclassified as network loss.
TCP user timeout follows Linux TCP_USER_TIMEOUT: it applies only in
synchronized states, returns ETIMEDOUT, does not change retransmission or
keepalive probe timing, and bounds data that remains unacknowledged or unsent
behind a zero window. Retransmission does not restart the absolute deadline.
When keepalive is enabled, user timeout replaces the probe-count close policy.
MIPS treats it as a local socket policy and does not advertise the optional
RFC 5482 UTO option.
When the SYN backlog or configured connection capacity is exhausted, passive open uses stateless SYN cookies instead of retaining another half-open connection. A per-stack random key authenticates the complete tuple, client sequence, recent time period, and negotiated options. A valid final ACK reconstructs conservative MSS, window scaling, SACK, timestamp, and ECN state; forged or expired cookies do not allocate a connection.
Validated ICMP Packet Too Big errors maintain a bounded, expiring destination
PMTU cache. Error type/code combinations and quoted TCP sequence spans are
checked before they can affect transport state. Stack.PathMTU exposes the
currently confirmed value, while Stack.ConfirmPathMTU records an
application-proven packetization-layer acknowledgement for protocols that
manage probing directly. TCP immediately resegments outstanding data and
implements RFC 4821 binary-search PLPMTUD when a cached reduction expires.
Upward probes carry real data; cumulative ACK confirms success, while only
isolated loss proven by SACK suppresses congestion response. Concurrent loss
and timeouts remain ordinary congestion and use TCP-friendly probe backoff.
MSS changes preserve the required byte/packet congestion units, and successful
probes update sibling flows sharing the destination path. A failed path is
also reduced by validated ICMP or by IPv4 and IPv6 PMTU black-hole inference
after repeated RTOs.
UDP uses the learned MTU for ordinary fragmentation. WritePathMTUProbe and
WritePathMTUProbeTo send an explicitly unfragmented packet above the current
confirmed PMTU but no larger than the first-hop MTU. Because UDP has no
generic acknowledgement, sending alone never raises the PMTU; an application
must use its own protected acknowledgement and then call ConfirmPathMTU or
ConfirmPathMTUFor, following RFC 8899's packetization-layer contract.
Connected UDP sockets select a stable local address and filter inbound remote
tuples; unconnected sockets can use arbitrary destinations in one IP family.
Both correlate asynchronous ICMP errors with recently used remote endpoints.
IPConn applies the same recent-destination correlation to protocol payload
writes. Validated errors are returned by a subsequent read and retained in
IPInfo; Packet Too Big also updates the shared destination PMTU. Its explicit
probe and confirmation methods follow the same application-acknowledgement
contract as UDP.
ICMP echo, unreachable, packet-too-big, IPv4 fragmentation, IPv6 source fragmentation, and bounded IPv4 and IPv6 reassembly are handled internally. Fragment overlap drops the complete datagram. Incomplete sets have count, byte, piece, and lifetime limits. Unsolicited reset, port-unreachable, and echo responses are rate limited, as are RFC 5961 challenge ACKs. ICMPv6 error messages are capped at the IPv6 minimum MTU and active unsupported Routing Headers receive the required Parameter Problem response.
Raw protocol sockets receive reassembled payload copies before the built-in TCP, UDP, or ICMP handler runs. Multiple matching sockets receive independent copies. A listener for an otherwise unknown protocol suppresses Protocol Unreachable while its receive queue accepts or drops matching traffic.
Stack.Stats returns a lock-free snapshot of active socket counts, categorized
IP/TCP packet and actor-queue drops, passive handshake, SYN-cookie and accept
queue outcomes, retransmission modes, PMTU changes, fragment cleanup, and rate
limiting.
Optional surfaces are arranged for ordinary Go linker reachability rather than
build tags. A consumer that only dials TCP and listens for UDP does not retain
TCP listener/SYN-cookie code, ReusePort registries, raw IPConn support,
message-control helpers, or other unreferenced public methods. No package-level
registration table or reflection root keeps these APIs alive.
MIPS is an endpoint stack, not a general host network stack. IPConn
exchanges protocol payloads while MIPS owns the IP header; header-included
raw packets and operating-system file descriptors are deliberately absent. It
also does not implement forwarding, NAT, multicast sockets, TCP urgent data,
or next-hop routing. LocalAddresses controls endpoint ownership and source
selection. Routes provides destination admission, longest-prefix selection,
metrics, and optional preferred sources, while the lower link remains
responsible for gateways, next hops, and L2 neighbor handling. Applications
requiring those facilities should use a mature general-purpose userspace
stack.
MIPS is licensed under the Mozilla Public License 2.0. See LICENSE.