Documentation
¶
Overview ¶
Package hybrid provides versatile network types for building TCP- and UDP-based applications, proxies in particular, and for testing them without sockets:
- Splitter: Split serves a single UDP socket, or any other packet connection, to many handlers at once, the way a TCP listener serves a port to many connections, so that UDP can be proxied the same way as TCP. Each flow is served by a SplitConn, and the flows released by their handlers by a fallback connection.
- Packet info: PacketInfoUDPConn makes a UDP socket bound to an unspecified address reply from the address each client sent to, on every major platform.
- Pipes and trunks: UnbufferedPipe and BufferedPipe return in-memory stream connections behaving like TCP, and UnbufferedTrunk and BufferedTrunk in-memory packet connections behaving like UDP, for testing network code deterministically.
- Wrappers: small adapters changing one aspect of a connection, e.g. its addresses with FakeAddressPacketConn, its lifetime with IdleTimeoutPacketConn, or its orientation with PacketToStreamConn and StreamToPacketConn.
- Deadlines: SimpleDeadline and HookDeadline return reusable deadlines, which allocate nothing once set up, with an optional hook.
The name reflects the core idea: a Conn is a hybrid of a packet connection and a stream connection, which keeps packet boundaries while serving a single remote address, so that a UDP flow can be handled by code written for net.Conn.
Index ¶
- Constants
- func BufferedPipe(queueSize int) (StreamConn, StreamConn)
- func BufferedTrunk(queueSize int, addr1, addr2 net.Addr) (FromToPacketConn, FromToPacketConn)
- func ControlUDPPacketInfo(network, _ string, c syscall.RawConn) error
- func NeedPacketInfoUDPConn(conn UDPConn) bool
- func UnbufferedPipe(semiSync bool) (StreamConn, StreamConn)
- func UnbufferedTrunk(addr1, addr2 net.Addr) (FromToPacketConn, FromToPacketConn)
- type Conn
- type Deadline
- type FromToPacketConn
- type FromToReader
- type FromToUDPConn
- type FromToWriter
- type PacketConn
- type PacketListener
- type PacketOverStreamConn
- type SplitConfig
- type SplitConn
- type SplitListener
- type SplitStats
- type StoppablePacketConn
- type StoppableStreamConn
- type Stopper
- type StreamConn
- type StreamListener
- type StreamOverPacketConn
- type UDPConn
- type Wrapper
- type WrappingPacketConn
- type WrappingStreamConn
Examples ¶
Constants ¶
const ( DefaultSplitAcceptQueueSize = 8 DefaultSplitFallbackQueueSize = 1024 // As the fallback connection may serve many flows DefaultSplitIdleTimeout = 30 * time.Second DefaultSplitMaxFlows = 8192 DefaultSplitReadBufferSize = math.MaxUint16 - 8 // The largest UDP payload, over IPv6, as it is 20 bytes less over IPv4 DefaultSplitReadQueueSize = 32 DefaultSplitWriteQueueSize = 64 DefaultSplitWriteTimeout = 500 * time.Millisecond )
Defaults of the fields of SplitConfig, which apply when a field isn't set
Variables ¶
This section is empty.
Functions ¶
func BufferedPipe ¶
func BufferedPipe(queueSize int) (StreamConn, StreamConn)
BufferedPipe creates an in-memory, full duplex network connection that is buffered and asynchronous; both ends implement the StreamConn interface. A write on one end may be consumed by several reads on the other. It returns once it has been queued, unless the queue already holds queueSize writes, in which case it blocks. Data are copied internally, so the write buffer may be reused once a write returns. When the remote end is closed, reads return io.EOF once all the bytes queued internally have been read.
BufferedPipe panics if queueSize isn't positive.
Example ¶
A buffered pipe behaves like TCP, while its writes return once queued, so that a single goroutine may write and read.
package main
import (
"fmt"
"io"
"github.com/vnxme/hybrid"
)
func main() {
c1, c2 := hybrid.BufferedPipe(8)
defer func() { _ = c2.Close() }()
_, _ = c1.Write([]byte("hello, "))
_, _ = c1.Write([]byte("world"))
_ = c1.Close() // c2 reads io.EOF once it has read all the bytes queued
b, err := io.ReadAll(c2)
fmt.Printf("%q, %v\n", b, err)
}
Output: "hello, world", <nil>
func BufferedTrunk ¶
func BufferedTrunk(queueSize int, addr1, addr2 net.Addr) (FromToPacketConn, FromToPacketConn)
BufferedTrunk creates an in-memory, full duplex packet-oriented network connection that is buffered and asynchronous; both ends implement the FromToPacketConn interface. Reads on one end aren't matched with writes on the other: a write returns once its packet has been queued, unless the queue already holds queueSize packets, in which case it blocks. Packets are copied internally, so the write buffer may be reused once a write returns. When the remote end is closed, reads return net.ErrClosed once all the packets queued have been read, and writes return net.ErrClosed.
The ends have local addresses addr1 and addr2 respectively, or a generic trunk address if nil. A local address is returned by LocalAddr, used as the source address of a packet written without one, e.g. by WriteTo, and as the destination address of a packet read that was written without one. A packet is delivered to the other end whatever its destination address is, so that multiple packet flows may share a trunk, told apart by addresses.
A packet longer than the read buffer is truncated: a read returns the bytes copied along with io.ErrShortBuffer, as a temporary net.Error, and the rest of the packet is dropped.
BufferedTrunk panics if queueSize isn't positive.
Example ¶
A trunk keeps packet boundaries and addresses, as UDP does, so that several flows may share it, told apart by addresses.
package main
import (
"errors"
"fmt"
"io"
"net"
"github.com/vnxme/hybrid"
)
func main() {
a := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 1), Port: 1111}
b := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 2), Port: 2222}
c1, c2 := hybrid.BufferedTrunk(8, a, b)
defer func() { _ = c1.Close() }()
defer func() { _ = c2.Close() }()
other := &net.UDPAddr{IP: net.IPv4(198, 51, 100, 1), Port: 3333}
_, _ = c1.WriteTo([]byte("hello"), b) // From the local address of c1
_, _ = c1.WriteToFrom([]byte("world"), b, other, 0) // From another address
_, _ = c1.WriteTo(nil, b) // An empty packet
_, _ = c1.WriteTo([]byte("truncated"), b) // Longer than the read buffer
buf := make([]byte, 5)
for range 4 {
n, src, dst, _, err := c2.ReadFromTo(buf)
fmt.Printf("%v -> %v: %q, truncated: %t\n", src, dst, buf[:n], errors.Is(err, io.ErrShortBuffer))
}
}
Output: 192.0.2.1:1111 -> 192.0.2.2:2222: "hello", truncated: false 198.51.100.1:3333 -> 192.0.2.2:2222: "world", truncated: false 192.0.2.1:1111 -> 192.0.2.2:2222: "", truncated: false 192.0.2.1:1111 -> 192.0.2.2:2222: "trunc", truncated: true
func ControlUDPPacketInfo ¶
ControlUDPPacketInfo enables packet info on a UDP socket for every IP version it accepts. It's meant to be net.ListenConfig.Control, or to be called from another Control function.
func NeedPacketInfoUDPConn ¶
NeedPacketInfoUDPConn reports whether conn is bound to an unspecified address, so that the local address a packet comes to is only known from packet info, e.g. by wrapping conn with PacketInfoUDPConn. It returns false if conn already implements FromToUDPConn, if it isn't bound to a port, or if it doesn't provide access to the underlying socket, as PacketInfoUDPConn would fail.
func UnbufferedPipe ¶
func UnbufferedPipe(semiSync bool) (StreamConn, StreamConn)
UnbufferedPipe creates an in-memory, full duplex network connection that is unbuffered; both ends implement the StreamConn interface. A write on one end may be consumed by several reads on the other, but the moment the write returns depends on semiSync:
(a) Synchronous (semiSync == false). Reads on one end are matched with writes on the other, copying data directly between the two; there is no internal buffering. A write returns once all its bytes have been read, the same way as with net.Pipe.
(b) Semi-synchronous (semiSync == true). A write returns once a read on the other end has received it, even if the read buffer is shorter than the write buffer: the remaining bytes are retained internally for subsequent reads, and the next write blocks until they have been read. Data are copied internally, so the write buffer may be reused once a write returns, which is trivially true for (a) as well.
When the remote end is closed, reads return io.EOF once all the bytes retained internally in (b) have been read.
Example ¶
An unbuffered pipe behaves like TCP, while its writes return once read, as with net.Pipe.
package main
import (
"bufio"
"fmt"
"github.com/vnxme/hybrid"
)
func main() {
c1, c2 := hybrid.UnbufferedPipe(false)
defer func() { _ = c2.Close() }()
go func() {
defer func() { _ = c1.Close() }()
_, _ = fmt.Fprint(c1, "first line\nsecond ")
_, _ = fmt.Fprint(c1, "line\n")
}()
s := bufio.NewScanner(c2) // Stream reads ignore write boundaries
for s.Scan() {
fmt.Println(s.Text())
}
}
Output: first line second line
func UnbufferedTrunk ¶
func UnbufferedTrunk(addr1, addr2 net.Addr) (FromToPacketConn, FromToPacketConn)
UnbufferedTrunk creates an in-memory, full duplex packet-oriented network connection that is unbuffered and synchronous; both ends implement the FromToPacketConn interface. Reads on one end are matched with writes on the other, copying packets directly between the two; there is no internal buffering. A write returns once its packet has been copied by a read, so the write buffer may be reused after that. When the remote end is closed, reads and writes return net.ErrClosed. Local addresses and packet truncation are handled the same way as by BufferedTrunk.
Example ¶
An unbuffered trunk behaves like UDP, while its writes return once read, so that a write with no reader blocks until its deadline.
package main
import (
"errors"
"fmt"
"log"
"net"
"os"
"time"
"github.com/vnxme/hybrid"
)
func main() {
client := &net.UDPAddr{IP: net.IPv4(198, 51, 100, 1), Port: 1111}
server := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 1), Port: 5353}
c1, c2 := hybrid.UnbufferedTrunk(client, server)
defer func() { _ = c1.Close() }()
defer func() { _ = c2.Close() }()
done := make(chan struct{})
go func() {
defer close(done)
_, _ = c1.WriteTo([]byte("ping"), server) // Returns once c2 has read it
b := make([]byte, 1500)
n, src, err := c1.ReadFrom(b)
if err != nil {
log.Fatal(err)
}
fmt.Printf("client: %s from %v\n", b[:n], src)
}()
b := make([]byte, 1500)
n, src, err := c2.ReadFrom(b)
if err != nil {
log.Fatal(err)
}
fmt.Printf("server: %s from %v\n", b[:n], src)
_, _ = c2.WriteTo([]byte("pong"), src) // Returns once c1 has read it
<-done
_ = c1.SetWriteDeadline(time.Now().Add(10 * time.Millisecond))
_, err = c1.WriteTo([]byte("lost"), server) // Nothing reads it
fmt.Println("timed out:", errors.Is(err, os.ErrDeadlineExceeded))
}
Output: server: ping from 198.51.100.1:1111 client: pong from 192.0.2.1:5353 timed out: true
Types ¶
type Conn ¶
type Conn interface {
PacketConn
// Read reads a single packet from the remote address into b. A packet longer than b is truncated,
// silently or with an error, e.g. io.ErrShortBuffer, depending on the connection, and the rest of it
// is lost, so reading a packet in parts, e.g. with io.ReadFull, fails.
Read(b []byte) (n int, err error)
// RemoteAddr returns the remote network address.
RemoteAddr() net.Addr
// Write writes b as a single packet to the remote address.
Write(b []byte) (n int, err error)
}
Conn is a hybrid of a PacketConn and a StreamConn: a packet-oriented network connection serving a single remote address, which it reads packets from with Read and writes packets to with Write, so it's a StreamConn as well, but one keeping packet boundaries. Any *net.UDPConn, *net.IPConn or *net.UnixConn satisfies it, but serves a single remote address only if dialed.
type Deadline ¶
type Deadline interface {
// Set sets the point in time when the deadline will time out.
// A timeout event is signaled by closing the channel returned by Wait.
// Once a timeout has occurred, the deadline can be refreshed by specifying
// a time value in the future. A zero time value prevents timeout.
Set(t time.Time)
// Wait returns a channel that is closed when the deadline is exceeded. As Set replaces
// a closed channel with a new one, Wait should be called again after Set, rather than
// its channel kept.
Wait() <-chan struct{}
}
Deadline signals that a point in time has passed by closing a channel, which a select may wait on along with its other cases. SimpleDeadline and HookDeadline return implementations of it.
func HookDeadline ¶
func HookDeadline(hook func()) Deadline
HookDeadline returns a new Deadline that runs hook when its timer fires. Unless the hook argument is nil, it is run as follows:
- The hook is run on its own goroutine once the time passed to Set in the future has come. It isn't run if Set is called with a zero time or a time in the past.
- The hook is run before the channel returned by Wait is closed, so its effects are observed by any goroutine having received from the channel.
- Set waits for a running hook to return, so once Set returns, no hook started for an earlier time is running, and none will be started.
Consequently, the hook must not call Set on the same deadline, directly or indirectly, as it always deadlocks. The hook should also return promptly, as it blocks concurrent calls of Set. Wait never blocks, so the hook may call it.
func SimpleDeadline ¶
func SimpleDeadline() Deadline
SimpleDeadline returns a new Deadline without a hook.
type FromToPacketConn ¶
type FromToPacketConn interface {
PacketConn
FromToReader
FromToWriter
}
FromToPacketConn is a generic packet-oriented network connection that is aware of the local addresses and the local interface indices when it comes to reading and writing packets.
func PacketToFromToPacketConn ¶
func PacketToFromToPacketConn(pc PacketConn, tryPacketInfoUDP bool) FromToPacketConn
PacketToFromToPacketConn returns a FromToPacketConn implementation based on the underlying PacketConn.
If the given PacketConn satisfies FromToPacketConn, no wrapping is done at all. If tryPacketInfoUDP is true, the PacketConn implements UDPConn, and system calls succeed, a FromToUDPConn made by PacketInfoUDPConn is returned. Otherwise, this function creates a simple wrapper that returns zero values for the destination address and the local interface index on read, and ignores any values assigned to these parameters on write.
type FromToReader ¶
type FromToReader interface {
// ReadFromTo reads a packet from the connection, copying the payload into p.
// It returns the number of bytes copied into p (0 <= n <= len(p)), the return
// address that was on the packet, the local address the packet came to,
// the local interface index, and any error encountered. The local address
// is nil and the index is zero if the connection doesn't know them, e.g. a UDP
// connection without packet info. Callers should always process the n > 0 bytes
// returned before considering the error err.
//
// ReadFromTo can be made to time out and return an error after a fixed time limit;
// see [net.PacketConn.SetDeadline] and [net.PacketConn.SetReadDeadline].
ReadFromTo(p []byte) (n int, src, dst net.Addr, idx int, err error)
}
FromToReader is a capability of a generic packet-oriented network connection to return the local address and the local interface index in addition to the remote address while reading packets.
type FromToUDPConn ¶
type FromToUDPConn interface {
UDPConn
FromToReader
FromToWriter
ReadFromToUDP(b []byte) (n int, src *net.UDPAddr, dst *net.UDPAddr, idx int, err error)
ReadFromToUDPAddrPort(b []byte) (n int, src netip.AddrPort, dst netip.AddrPort, idx int, err error)
WriteToFromUDP(b []byte, dst, src *net.UDPAddr, idx int) (n int, err error)
WriteToFromUDPAddrPort(b []byte, dst, src netip.AddrPort, idx int) (n int, err error)
}
FromToUDPConn is a UDP connection that is aware of the local addresses and the local interface indices when it comes to reading and writing packets. Its UDP-specific methods work the same way as ReadFromTo and WriteToFrom, with a nil *net.UDPAddr or an invalid netip.AddrPort for an address unknown on read, or left to the system on write.
func PacketInfoUDPConn ¶
func PacketInfoUDPConn(conn UDPConn) (FromToUDPConn, error)
PacketInfoUDPConn enables packet info on conn for every IP version it accepts, and wraps it to read the local address and interface index of each packet, and to write them with each packet. It returns conn as is if it already implements FromToUDPConn.
Example ¶
A UDP socket bound to an unspecified address replies from the address each client sent to, once wrapped with PacketInfoUDPConn.
package main
import (
"context"
"log"
"net"
"github.com/vnxme/hybrid"
)
func main() {
lc := net.ListenConfig{Control: hybrid.ControlUDPPacketInfo} // Optional: PacketInfoUDPConn enables it too
pc, err := lc.ListenPacket(context.Background(), "udp", ":5353")
if err != nil {
log.Fatal(err)
}
conn, err := hybrid.PacketInfoUDPConn(pc.(*net.UDPConn))
if err != nil {
log.Fatal(err) // E.g. on a platform without packet info
}
defer func() { _ = conn.Close() }()
b := make([]byte, 64<<10)
for {
n, src, dst, idx, err := conn.ReadFromToUDPAddrPort(b)
if err != nil {
return
}
// Reply from the address the client sent to, and through the interface the packet came through
_, _ = conn.WriteToFromUDPAddrPort(b[:n], src, dst, idx)
}
}
Output:
type FromToWriter ¶
type FromToWriter interface {
// WriteToFrom writes a packet with payload p to the remote address dst from
// the local address src through the local interface with index idx. A nil src
// and a zero idx leave them to the system. A connection that can't choose them,
// e.g. one made by [PacketToFromToPacketConn] without packet info, ignores them.
//
// WriteToFrom can be made to time out and return an error after a fixed time limit;
// see [net.PacketConn.SetDeadline] and [net.PacketConn.SetWriteDeadline].
// On packet-oriented connections, write timeouts are rare.
WriteToFrom(p []byte, dst, src net.Addr, idx int) (n int, err error)
}
FromToWriter is a capability of a generic packet-oriented network connection to consider the local address and the local interface index in addition to the remote address while writing packets.
type PacketConn ¶
type PacketConn = net.PacketConn
PacketConn is an alias for net.PacketConn, i.e. a generic packet-oriented network connection.
type PacketListener ¶
PacketListener is an alias for net.Listener, i.e. a generic network listener for packet-oriented protocols, whose Accept returns a Conn, e.g. a SplitConn, as a StreamConn. A listener may also have a method returning the very type, e.g. AcceptSplit of a SplitListener, as *net.TCPListener has AcceptTCP.
type PacketOverStreamConn ¶
type PacketOverStreamConn interface {
PacketConn
Wrapper[StreamConn]
}
PacketOverStreamConn is a PacketConn over a StreamConn, which Unwrap returns.
func StreamToPacketConn ¶
func StreamToPacketConn(sc StreamConn) PacketOverStreamConn
StreamToPacketConn returns a PacketOverStreamConn based on the underlying StreamConn.
ReadFrom returns the remote address of the StreamConn, while WriteTo writes to it whatever address it's given, as the address argument is ignored. A packet read is what a single Read returns, so packets keep their boundaries only if the StreamConn keeps those of writes, as a pipe does with a buffer as long as any write, and TCP doesn't.
Example ¶
StreamToPacketConn serves a StreamConn as a PacketConn, whose packets keep their boundaries if the StreamConn keeps those of writes, as a pipe does, while TCP doesn't.
package main
import (
"fmt"
"github.com/vnxme/hybrid"
)
func main() {
s1, s2 := hybrid.BufferedPipe(8)
c1, c2 := hybrid.StreamToPacketConn(s1), hybrid.StreamToPacketConn(s2)
defer func() { _ = c1.Close() }()
defer func() { _ = c2.Close() }()
_, _ = c1.WriteTo([]byte("hello"), nil) // The address is ignored, as a StreamConn has a single remote one
_, _ = c1.WriteTo([]byte("world"), nil)
b := make([]byte, 1500)
for range 2 {
n, _, err := c2.ReadFrom(b)
fmt.Printf("%s, %v\n", b[:n], err)
}
}
Output: hello, <nil> world, <nil>
type SplitConfig ¶
type SplitConfig struct {
// ErrorFunc, if set, is called with each error of the underlying PacketConn that the splitter can't return
// otherwise: the one that stops the splitter, e.g. as the PacketConn fails, or is closed other than by Close,
// which matches net.ErrClosed, as Accept returns it then, and that of each write counted as WriteFailed.
// It's called on the goroutines of the splitter, so it shouldn't block.
ErrorFunc func(error)
// AcceptQueueSize is the maximum number of split connections waiting for Accept,
// [DefaultSplitAcceptQueueSize] by default. The first packet of a new flow is dropped
// if the accept queue is full.
AcceptQueueSize int
// ReadQueueSize is the maximum number of packets waiting to be read from each split connection,
// [DefaultSplitReadQueueSize] by default. A packet is dropped if the queue of its split connection is full.
ReadQueueSize int
// FallbackQueueSize is the maximum number of packets waiting to be read from the fallback connection,
// [DefaultSplitFallbackQueueSize] by default. A packet is dropped if the queue of the fallback connection
// is full.
FallbackQueueSize int
// IdleTimeout is how long a flow may go without a packet read or written before it ends,
// [DefaultSplitIdleTimeout] by default: its split connection is closed, and its next packet starts
// a new flow. A negative timeout keeps every flow until its split connection is closed, and a released
// flow as long as the splitter is open. As the next packet of an expired released flow goes to Accept
// again, the timeout should be no shorter than that of the application serving the fallback connection.
IdleTimeout time.Duration
// MaxFlows is the maximum number of flows, [DefaultSplitMaxFlows] by default.
// The first packet of a new flow is dropped if there are as many flows already.
MaxFlows int
// ReadBufferSize is the maximum size of a packet read from the underlying PacketConn,
// [DefaultSplitReadBufferSize] by default, and at least 512. A larger packet is dropped.
ReadBufferSize int
// WriteQueueSize is the maximum number of packets waiting to be written to the underlying PacketConn,
// [DefaultSplitWriteQueueSize] by default. A write waits for room until the write deadline of its
// connection if the queue is full.
WriteQueueSize int
// WriteTimeout is how long the write loop waits for the underlying PacketConn to write a packet before
// dropping it, [DefaultSplitWriteTimeout] by default. A negative timeout makes it wait as long as it takes.
WriteTimeout time.Duration
// FallbackConn gives the splitter a fallback connection, which serves the flows released by their handlers.
// Without one, the packets of a released flow are dropped. Once it's closed, a released flow ends.
FallbackConn bool
// PacketInfo makes the splitter enable packet info with [PacketInfoUDPConn] if the underlying PacketConn
// satisfies UDPConn, so that flows are told apart by their destination addresses as well, and replies
// are sent from the address each flow came to, e.g. for a UDPConn listening on an unspecified address.
// Without it, flows of a UDPConn are told apart by their source addresses alone.
PacketInfo bool
}
SplitConfig configures a splitter. The zero value of a field gives its default, as does a nil *SplitConfig.
type SplitConn ¶
type SplitConn interface {
Conn // With the remote address of the flow
FromToPacketConn
// Release hands the flow over to the fallback connection, e.g. as no handler recognizes it,
// and closes the split connection. The packets given, e.g. those read while matching, go first,
// as the packets of the flow, so that the fallback connection misses none of them.
Release(pkts ...[]byte) error
}
SplitConn is a split connection: a FromToPacketConn serving the packets of a single flow, which it reads from and writes to its remote address.
type SplitListener ¶
type SplitListener interface {
// Accept returns the split connection of the next new flow as a StreamConn, as AcceptSplit does,
// Addr returns the local address of the underlying PacketConn, and Close closes it and stops the splitter.
PacketListener
// Stop stops the splitter like Close does, but leaves the underlying PacketConn open, with no deadlines,
// for the caller to take it over, e.g. with Unwrap.
Stopper
// Unwrap returns the underlying PacketConn as given to Split, which the splitter reads from and writes to
// until it's stopped by Close or Stop.
Wrapper[PacketConn]
// AcceptSplit returns the split connection of the next new flow.
AcceptSplit() (SplitConn, error)
// Fallback returns the fallback connection of the splitter, or nil if it has none. Like a split connection,
// it reads a packet longer than the buffer in part, with io.ErrShortBuffer as a temporary net.Error.
Fallback() FromToPacketConn
// Stats returns the numbers of packets dropped so far, by reason.
Stats() SplitStats
}
SplitListener is a listener of a splitter, accepting split connections, with its fallback connection and statistics.
func Split ¶
func Split(pc PacketConn, config *SplitConfig) SplitListener
Split returns a SplitListener for the given PacketConn, configured by config, which may be nil for the defaults, and starts its internal loops. It takes pc over, clearing its write deadline, until Close or Stop.
The splitter may share a socket with other readers, e.g. in another splitter over another PacketConn of it, which set its read deadline as they need: it backs off from a read timeout it hasn't caused, for them to take their own, and clears the read deadline after 20 of them in a row, e.g. one left passed before Split. Stop sets a deadline passed in order to interrupt its read, so it interrupts theirs as well.
The splitter leaves the socket options of pc as they are. Notably, macOS and the BSDs refuse to send a datagram larger than the send buffer of a socket, which is 9216 bytes by default, so a larger packet written to a split connection is counted as WriteFailed, unless the send buffer has been raised, e.g. with net.UDPConn.SetWriteBuffer.
Example ¶
A splitter serves each flow recognized by a handler with a split connection, and the flows released by their handlers with the fallback connection. A trunk stands in for a UDP socket here.
package main
import (
"fmt"
"log"
"net"
"github.com/vnxme/hybrid"
)
func main() {
server := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 1), Port: 5353}
client, pc := hybrid.BufferedTrunk(8, nil, server)
ln := hybrid.Split(pc, &hybrid.SplitConfig{FallbackConn: true})
defer func() { _ = ln.Close() }() // Closes pc too
// The fallback connection serves the released flows together, e.g. as the PacketConn of a QUIC server
go func() {
fb := ln.Fallback()
b := make([]byte, 1500)
for {
n, addr, err := fb.ReadFrom(b)
if err != nil {
return // net.ErrClosed once the splitter is closed
}
_, _ = fb.WriteTo(fmt.Appendf(nil, "fallback: %s", b[:n]), addr)
}
}()
// Each split connection serves a single flow, as an accepted TCP connection does
go func() {
for {
c, err := ln.AcceptSplit()
if err != nil {
return // net.ErrClosed once the splitter is closed
}
go func() {
defer func() { _ = c.Close() }()
b := make([]byte, 1500)
n, err := c.Read(b)
if err != nil {
return
}
if string(b[:n]) != "ping" {
_ = c.Release(b[:n]) // Hands the flow over, the packet read first
return
}
_, _ = c.Write([]byte("pong")) // Goes to the client, from the address it sent to
}()
}
}()
b := make([]byte, 1500)
for _, pkt := range []struct {
from *net.UDPAddr
data string
}{
{&net.UDPAddr{IP: net.IPv4(198, 51, 100, 1), Port: 1111}, "ping"},
{&net.UDPAddr{IP: net.IPv4(198, 51, 100, 2), Port: 2222}, "hello"},
} {
if _, err := client.WriteToFrom([]byte(pkt.data), server, pkt.from, 0); err != nil {
log.Fatal(err)
}
n, src, dst, _, err := client.ReadFromTo(b)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%v -> %v: %s\n", src, dst, b[:n])
}
}
Output: 192.0.2.1:5353 -> 198.51.100.1:1111: pong 192.0.2.1:5353 -> 198.51.100.2:2222: fallback: hello
type SplitStats ¶
type SplitStats struct {
// Reading, in the order a packet meets the checks
Truncated uint64 // The packet was larger than ReadBufferSize
NoSource uint64 // The packet had no source address
NoDestination uint64 // The packet had no destination address, while the splitter uses them
TableFull uint64 // The flow table was full when the packet started a new flow
AcceptQueueFull uint64 // The accept queue was full when the packet started a new flow
ConnQueueFull uint64 // The queue of the split connection of the packet was full
ConnClosed uint64 // The split connection of the packet was closed before reading it
// Releasing
NoFallback uint64 // The packet of a released flow had no open fallback connection to go to
FallbackQueueFull uint64 // The queue of the fallback connection was full
FallbackClosed uint64 // The fallback connection was closed before reading the packet
// Writing
WriteTimedOut uint64 // The write loop timed out writing the packet
WriteFailed uint64 // The write loop failed to write the packet, e.g. to an unreachable address
WriteUnsent uint64 // The splitter closed, or its underlying connection did, before writing the packet
}
SplitStats counts the packets a splitter has dropped, by reason.
type StoppablePacketConn ¶
type StoppablePacketConn interface {
WrappingPacketConn
Stopper
}
StoppablePacketConn is a WrappingPacketConn that can stop what it does on its own, leaving the underlying PacketConn open for the caller to take over with Unwrap.
func IdleTimeoutPacketConn ¶
func IdleTimeoutPacketConn(pc PacketConn, timeout time.Duration) StoppablePacketConn
IdleTimeoutPacketConn returns a StoppablePacketConn that closes the underlying PacketConn asynchronously from a separate goroutine once it has been idle for the timeout.
The timeout is ignored if it's zero or negative. The idle time restarts on each successful read and write operation, i.e. when n > 0 or err == nil.
The idle time starts when the underlying connection is wrapped, i.e. before this function returns, and the timer stops for good once Stop is called, leaving the underlying connection open, or once the connection is closed, by Close or by the timer.
type StoppableStreamConn ¶
type StoppableStreamConn interface {
WrappingStreamConn
Stopper
}
StoppableStreamConn is a WrappingStreamConn that can stop what it does on its own, leaving the underlying StreamConn open for the caller to take over with Unwrap.
func IdleTimeoutStreamConn ¶
func IdleTimeoutStreamConn(sc StreamConn, timeout time.Duration) StoppableStreamConn
IdleTimeoutStreamConn returns a StoppableStreamConn that closes the underlying StreamConn asynchronously from a separate goroutine once it has been idle for the timeout.
The timeout is ignored if it's zero or negative. The idle time restarts on each successful read and write operation, i.e. when n > 0 or err == nil.
The idle time starts when the underlying connection is wrapped, i.e. before this function returns, and the timer stops for good once Stop is called, leaving the underlying connection open, or once the connection is closed, by Close or by the timer.
type Stopper ¶
type Stopper interface {
// Stop stops it for good. It returns net.ErrClosed if it has stopped already, or another error
// if it has failed to stop.
Stop() error
}
Stopper can stop what a wrapper or a splitter does on its own, e.g. closing an idle connection or reading packets from it, leaving the underlying connection open.
type StreamConn ¶
StreamConn is an alias for net.Conn, i.e. a generic stream-oriented network connection.
type StreamListener ¶
StreamListener is an alias for net.Listener, i.e. a generic network listener for stream-oriented protocols.
type StreamOverPacketConn ¶
type StreamOverPacketConn interface {
StreamConn
Wrapper[PacketConn]
}
StreamOverPacketConn is a StreamConn over a PacketConn, which Unwrap returns.
func PacketToStreamConn ¶
func PacketToStreamConn(pc PacketConn, remote net.Addr, filter bool) StreamOverPacketConn
PacketToStreamConn returns a StreamOverPacketConn based on the underlying PacketConn with the given remote address.
The remote address given is returned by RemoteAddr, and Write sends every packet to it. If filter is set and the remote address isn't nil, Read accepts a packet from the remote address only, as a dialed UDP socket does, and drops a packet from any other address, along with any error of its read, e.g. of its truncation if the underlying PacketConn reports the source of a truncated packet. A failed read, i.e. one reading nothing, returns its error whatever address it reports, e.g. once the connection closes or its read deadline passes. A UDP or IP address matches regardless of IPv4 mapping. Without the filter, Read accepts a packet from any address. ReadFrom of the underlying PacketConn applies no filter. Each Read reads a single packet, which is truncated if longer than the buffer, silently or with an error, e.g. io.ErrShortBuffer, depending on the underlying PacketConn, so that reading a packet in parts, e.g. with io.ReadFull, fails.
Example ¶
PacketToStreamConn serves a single remote address of a PacketConn as a StreamConn, e.g. for code written for net.Conn, and drops the packets from any other address if filter is set, as a dialed UDP socket does.
package main
import (
"fmt"
"log"
"net"
"github.com/vnxme/hybrid"
)
func main() {
server := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 1), Port: 5353}
client := &net.UDPAddr{IP: net.IPv4(198, 51, 100, 1), Port: 1111}
c1, c2 := hybrid.BufferedTrunk(8, client, server)
conn := hybrid.PacketToStreamConn(c1, server, true)
defer func() { _ = conn.Close() }()
defer func() { _ = c2.Close() }()
_, _ = fmt.Fprint(conn, "hello") // A packet to the remote address
b := make([]byte, 1500)
n, src, err := c2.ReadFrom(b)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%v: %s\n", src, b[:n])
other := &net.UDPAddr{IP: net.IPv4(203, 0, 113, 1), Port: 4444}
_, _ = c2.WriteToFrom([]byte("spoofed"), client, other, 0) // Dropped
_, _ = c2.WriteTo([]byte("world"), client)
n, err = conn.Read(b)
fmt.Printf("%v: %s, %v\n", conn.RemoteAddr(), b[:n], err)
}
Output: 198.51.100.1:1111: hello 192.0.2.1:5353: world, <nil>
type UDPConn ¶
type UDPConn interface {
PacketConn
SyscallConn() (syscall.RawConn, error)
ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error)
ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error)
WriteToUDP(b []byte, addr *net.UDPAddr) (int, error)
WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error)
ReadMsgUDP(b []byte, oob []byte) (n int, oobn int, flags int, addr *net.UDPAddr, err error)
ReadMsgUDPAddrPort(b []byte, oob []byte) (n int, oobn int, flags int, addr netip.AddrPort, err error)
WriteMsgUDP(b []byte, oob []byte, addr *net.UDPAddr) (n int, oobn int, err error)
WriteMsgUDPAddrPort(b []byte, oob []byte, addr netip.AddrPort) (n int, oobn int, err error)
}
UDPConn is an interface that is implicitly implemented by *net.UDPConn.
type Wrapper ¶
type Wrapper[T any] interface { // Unwrap returns the underlying value as given to the wrapper. Unwrap() T }
Wrapper can unwrap the underlying value of type T, e.g. a PacketConn or a StreamConn. T must be the very type Unwrap returns, e.g. a wrapper returning a FromToPacketConn isn't a Wrapper[PacketConn].
type WrappingPacketConn ¶
type WrappingPacketConn interface {
PacketConn
Wrapper[PacketConn]
}
WrappingPacketConn is a PacketConn wrapping another one, which Unwrap returns.
func FakeAddressPacketConn ¶
func FakeAddressPacketConn(pc PacketConn, local, remote net.Addr) WrappingPacketConn
FakeAddressPacketConn returns a WrappingPacketConn that shadows local and remote addresses.
Unless the local address is nil, it is returned by LocalAddr instead of the native local address. Similarly, unless the remote address is nil, it is returned by ReadFrom instead of the address a packet comes from, and WriteTo sends every packet to it, whatever address it is given.
func FakeClosePacketConn ¶
func FakeClosePacketConn(pc PacketConn, closeFunc func() error) WrappingPacketConn
FakeClosePacketConn returns a WrappingPacketConn that shadows Close.
Unless closeFunc is nil, Close calls it instead of closing the underlying PacketConn, and returns its error. Otherwise, Close does nothing and returns nil. Either way, the underlying PacketConn stays open: reads and writes go on, and Close neither unblocks them nor makes them fail, so whoever owns the PacketConn closes it, e.g. after Unwrap.
type WrappingStreamConn ¶
type WrappingStreamConn interface {
StreamConn
Wrapper[StreamConn]
}
WrappingStreamConn is a StreamConn wrapping another one, which Unwrap returns.
func FakeAddressStreamConn ¶
func FakeAddressStreamConn(sc StreamConn, local, remote net.Addr) WrappingStreamConn
FakeAddressStreamConn returns a WrappingStreamConn that shadows local and remote addresses.
Unless the local address is nil, it is returned by LocalAddr instead of the native local address. Similarly, unless the remote address is nil, it is returned by RemoteAddr instead of the native remote address.
func FakeCloseStreamConn ¶
func FakeCloseStreamConn(sc StreamConn, closeFunc func() error) WrappingStreamConn
FakeCloseStreamConn returns a WrappingStreamConn that shadows Close.
Unless closeFunc is nil, Close calls it instead of closing the underlying StreamConn, and returns its error. Otherwise, Close does nothing and returns nil. Either way, the underlying StreamConn stays open: reads and writes go on, and Close neither unblocks them nor makes them fail, so whoever owns the StreamConn closes it, e.g. after Unwrap.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
pktinfo
Package pktinfo enables, marshals and parses packet info control messages, which carry the local address and the local interface index of each UDP packet read or written.
|
Package pktinfo enables, marshals and parses packet info control messages, which carry the local address and the local interface index of each UDP packet read or written. |