sniffer

package
v0.40.0 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: CC0-1.0 Imports: 14 Imported by: 0

README

sniffer

sniffer classifies the client-first prefix of a net.Conn without consuming that prefix from the next handler. It reads from a putback.Conn, then puts all inspected bytes back before it returns.

Classifiers

A classifier is an incremental state machine:

type Classifier interface {
	MinSniffBufferSize() int
	Feed(next []byte) State
}

MinSniffBufferSize reports the byte count that lets the classifier make its bounded decision.

Feed receives only newly read bytes. Its result is one of:

  • NeedMore: the prefix is still possible but incomplete.
  • Match: the classifier has matched.
  • Mismatch: no future suffix can make it match.

Match and Mismatch are terminal. Feed(nil) queries the initial or current state. The slice passed to Feed is read-only and can be reused after the call.

Classifiers can optionally expose typed parsed data:

type MetadataProvider interface {
	Metadata() any
}

Sniff itself does not depend on metadata. After a match, callers can inspect the matched classifier with sniffer.Metadata(classifier). Custom classifiers can return any stable value, usually a small struct. The built-in TLS classifier returns TLSClientHelloInfo; the HTTP classifier returns HTTPInfo. Wrappers such as Limit and WithMinSniffBufferSize preserve child metadata. Or returns the matched child's metadata. And returns the single child metadata value when only one child exposes metadata, or CompositeMetadata when multiple children expose metadata.

Factories create a fresh classifier for each connection:

type Factory interface {
	MinSniffBufferSize() int
	NewClassifier() Classifier
}

The factory size is the size needed by the classifiers it creates.

Included building blocks:

  • Prefix and PrefixFactory
  • SSH and SSHFactory, which match SSH- at offset zero
  • HTTP, HTTPWithConfig, HTTPFactory, and HTTPFactoryWithConfig, which match HTTP request lines and can filter by method, URL request-target, HTTP-version token, and hostname
  • HTTP2 and HTTP2Factory, which match the cleartext HTTP/2 client connection preface
  • AMQP and AMQPFactory, which match AMQP 0-9-1, AMQP 1.0, and AMQP 1.0 SASL protocol headers
  • MQTT and MQTTFactory, which match MQTT CONNECT headers
  • PostgreSQL and PostgreSQLFactory, which match PostgreSQL startup, SSLRequest, GSSENCRequest, and CancelRequest packets
  • MongoDB and MongoDBFactory, which match MongoDB wire protocol request headers
  • Redis and RedisFactory, which match Redis RESP array requests
  • TLS, TLSWithConfig, TLSFactory, and TLSFactoryWithConfig, which match TLS ClientHello records and can filter by offered version, visible SNI availability, ECH presence, visible SNI hostname, and ALPN
  • ProxyProtocolV1, ProxyProtocolV2, ProxyProtocol, ProxyProtocolV1Factory, ProxyProtocolV2Factory, and ProxyProtocolFactory, which match HAProxy PROXY protocol headers
  • SOCKS4, SOCKS5, SOCKS, SOCKS4Factory, SOCKS5Factory, and SOCKSFactory, which match SOCKS client requests or greetings
  • DNSOverTCP, DNSOverTCPWithConfig, DNSOverTCPFactory, and DNSOverTCPFactoryWithConfig, which match DNS-over-TCP query messages
  • RTSP and RTSPFactory, which match RTSP request lines
  • SIP and SIPFactory, which match SIP request lines
  • STUN and STUNFactory, which match STUN and TURN message headers
  • RDP and RDPFactory, which match RDP TPKT/X.224 connection requests
  • SMB and SMBFactory, which match SMB-over-TCP negotiate requests
  • LDAP and LDAPFactory, which match LDAP client request message prefixes
  • Cassandra and CassandraFactory, which match Cassandra native protocol STARTUP and OPTIONS requests
  • MemcachedBinary, MemcachedASCII, Memcached, MemcachedBinaryFactory, MemcachedASCIIFactory, and MemcachedFactory, which match memcached binary and text requests
  • And, Or, and Not
  • AndFactory, OrFactory, and NotFactory
  • Limit and LimitFactory for classifier-local byte limits
  • MinSniffBufferSize and MinFactorySniffBufferSize helpers
  • WithMinSniffBufferSize and FactoryWithMinSniffBufferSize wrappers for function adapters or other classifiers that need a non-zero size

HTTP accepts any valid method token, non-empty URL request-target, and HTTP-version token that starts with HTTP/. Use HTTPWithConfig or HTTPFactoryWithConfig for exact fields, multi-value fields, and glob patterns:

factory := sniffer.HTTPFactoryWithConfig(sniffer.HTTPConfig{
	Methods:          []string{"GET", "POST"},
	URLPatterns:      []string{"/api/*"},
	HostnamePatterns: []string{"*.example.test"},
})

Empty field groups are wildcards, so leaving Version and Versions empty accepts any HTTP version token. Non-empty values in one group are ORed, and all configured groups must match.

URLPatterns and HostnamePatterns are byte-oriented glob patterns over the whole value. * matches any byte sequence, ? matches one byte, and \ escapes the next byte. Use URL or URLs for literal request-targets that contain ?.

Hostname filters match a normalized hostname from an absolute-form request-target or the Host header. Matching is case-insensitive, and an optional port is removed before matching. Hostname filters make the classifier inspect headers until a matching Host header, a non-matching Host header, the header terminator, or the header byte limit.

HTTPConfig.MaxRequestLineBytes bounds the request line, including LF. HTTPConfig.MaxHeaderBytes bounds header inspection when hostname matching is enabled. Zero values use DefaultHTTPRequestLineMaxBytes and DefaultHTTPHeaderMaxBytes.

TLS accepts any syntactically valid TLS ClientHello. Use TLSWithConfig or TLSFactoryWithConfig for exact fields, multi-value fields, and glob patterns:

factory := sniffer.TLSFactoryWithConfig(sniffer.TLSConfig{
	Versions:         []uint16{tls.VersionTLS13},
	SNIAvailable:    sniffer.TLSFlagRequired,
	SNIEncrypted:    sniffer.TLSFlagForbidden,
	HostnamePatterns: []string{"*.example.test"},
	ALPNs:           []string{"h2", "http/1.1"},
})

TLSConfig.Version and TLSConfig.Versions match versions offered by the ClientHello. If the supported_versions extension is present, the classifier uses it. Otherwise it uses the ClientHello legacy_version field. The server-selected TLS version is not visible before routing.

TLSConfig.SNIAvailable matches whether the ClientHello contains a visible SNI host_name. TLSConfig.SNIEncrypted matches whether the encrypted_client_hello extension is present. This is an observable ECH signal only; the classifier cannot verify server ECH acceptance or reveal an encrypted inner hostname.

Hostname, Hostnames, and HostnamePatterns match the visible SNI hostname case-insensitively. With ECH, this is the outer ClientHello hostname, if the client sends one. ALPN, ALPNs, and ALPNPatterns match any protocol name in the ALPN extension case-sensitively. TLS hostname and ALPN patterns use the same whole-value glob syntax as HTTP patterns.

TLSConfig.MaxClientHelloBytes bounds the bytes inspected while parsing the first ClientHello, including TLS record headers and handshake headers. Zero uses DefaultTLSClientHelloMaxBytes.

HTTP2 matches the cleartext HTTP/2 prior-knowledge preface: PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n. TLS connections that negotiate HTTP/2 by ALPN are still TLS streams; use TLSWithConfig with ALPN: "h2" for those connections.

AMQP matches the eight-byte protocol header for AMQP 0-9-1, AMQP 1.0, and AMQP 1.0 SASL negotiation. It does not inspect later connection tuning, SASL frames, or AMQP frames.

MQTT matches MQTT CONNECT packets. It validates the fixed header packet type, Remaining Length field, protocol name, protocol level, connect flags, and keep-alive bytes. It accepts MQTT 3.1, 3.1.1, and 5. It matches before reading the client identifier or other payload fields.

PostgreSQL matches PostgreSQL client startup-family packets. It accepts normal protocol 3.0 startup messages, SSLRequest, GSSENCRequest, and CancelRequest packets. It does not inspect startup parameters, user names, databases, or cancellation keys.

MongoDB matches MongoDB wire protocol request messages. It validates the 16-byte wire message header, requires a client request opcode, and requires responseTo to be zero. It does not inspect BSON command documents or compressed message bodies.

Redis matches Redis RESP array requests. It validates the first request array and its first bulk-string command name. Inline Redis commands are not matched because their text forms are ambiguous with other line-oriented protocols. The command name must be non-empty, use printable non-space bytes, and be no larger than 128 bytes.

ProxyProtocolV1 matches the ASCII PROXY prefix. It is a routing heuristic, not a full v1 line parser. ProxyProtocolV2 validates the binary signature and fixed 16-byte header. It checks the version, command, address family, transport protocol, and minimum address payload length, but it does not inspect address values or TLVs. ProxyProtocol accepts either version.

SOCKS4 matches SOCKS4 and SOCKS4a request headers with CONNECT or BIND commands. It matches after the fixed eight-byte header and does not read the user ID or SOCKS4a hostname. SOCKS5 validates the version byte and waits for the declared method list. SOCKS accepts either SOCKS4 or SOCKS5.

DNSOverTCP matches a DNS query with the two-byte TCP length prefix. It validates the DNS header and question section, requires a client query rather than a response, and requires at least one question. DNSOverTCPConfig.MaxMessageBytes bounds the message bytes after the length prefix. Zero uses DefaultDNSOverTCPMessageMaxBytes.

RTSP matches a known RTSP method, a non-empty request URI, and the RTSP/1.0 version token on the first CRLF-terminated line. It does not inspect headers, Transport parameters, CSeq, or message bodies.

SIP matches a known SIP method, a non-empty Request-URI, and the SIP/2.0 version token on the first CRLF-terminated line. It does not inspect Via, To, From, Call-ID, CSeq, or message bodies.

STUN matches the fixed STUN header used by STUN and TURN. It validates the message type top bits, length alignment, magic cookie, and known method. It does not inspect attributes or integrity.

RDP matches an RDP connection request over TPKT. It validates the TPKT header and the X.224 Connection Request fixed fields, then matches before optional cookies, routing tokens, or negotiation data.

SMB matches SMB-over-TCP client negotiate requests. It validates the NetBIOS Session Service header and the start of an SMB1 or SMB2/3 negotiate request. It does not inspect dialects, security modes, capabilities, or later SMB messages.

LDAP matches LDAP client messages. It validates enough BER to find a non-zero message ID and a client request protocolOp tag. It does not parse request fields, controls, filters, or authentication data.

Cassandra matches Cassandra native protocol STARTUP and OPTIONS requests for protocol v3, v4, and v5. It validates the request header and body length rules for those first client messages, but it does not inspect the body string map or compression.

MemcachedBinary matches memcached binary protocol requests by validating the fixed request header. MemcachedASCII matches common memcached text protocol request lines and matches before storage value bytes. Memcached accepts either form.

Sniffing and routing

func route(raw net.Conn, pool bufpool.Pool) error {
	conn := putback.New(raw, pool)

	if err := conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
		return err
	}

	factories := []sniffer.Factory{
		sniffer.HTTPFactory(),
		sniffer.SSHFactory(),
	}
	index, err := sniffer.SniffFactoriesWithPool(
		sniffer.MinFactorySniffBufferSize(factories...),
		pool,
		conn,
		factories...,
	)

	if clearErr := conn.SetReadDeadline(time.Time{}); err == nil {
		err = clearErr
	}
	if err != nil {
		if gonnect.IsTimeout(err) {
			return proxyToOriginalDestination(conn)
		}
		return err
	}

	switch index {
	case 0:
		return handleHTTP(conn)
	case 1:
		return proxySSH(conn)
	case sniffer.NoMatch:
		return proxyToOriginalDestination(conn)
	default:
		panic("unreachable")
	}
}

Network middleware

Sniffer is a gonnect.Network middleware that routes calls to immutable output slots and can sniff outgoing TCP dials before the final route.

Construct it with output slots, classifiers, and two callbacks:

  • Control runs for every gonnect.Network method call. It can modify the call fields, choose a 1-based output slot, reject with slot 0, or request interception for outgoing TCP Dial and DialTCP. An interception request for any other call rejects the call as if slot 0 was selected.
  • SniffControl runs only after an intercepted TCP connection is sniffed. It receives SniffResult, including the matched classifier index and metadata from the matched classifier, and then chooses the final route.

An intercepted dial returns a local TCP connection immediately. A background worker reads client-first bytes from that connection, restores all inspected bytes, calls SniffControl, opens the selected output connection, and pipes the original stream through unchanged.

Close and Down close only connections and listeners returned by the Sniffer. They do not close the output networks. Closing one output externally does not change the Sniffer state and does not affect calls routed to other outputs. IsNative always returns false.

SniffWithPool and SniffFactoriesWithPool get the scratch buffer from bufpool and return it before they return. Pass the same pool to putback.New if replay copies must also use the pool. Call Sniff or SniffFactories directly when the caller already owns the scratch buffer.

When a scratch buffer is passed directly, its contents are ignored on entry and can be overwritten. The buffer length, not capacity, is the total inspection limit. A zero-length buffer returns NoMatch unless a classifier matches on Feed(nil).

Every byte read by Sniff is copied back before it returns. This is true for match, no-match, buffer-exhaustion, and read-error paths. You can chain sniffers on the same wrapper:

index, err := sniffer.SniffWithPool(tlsBudget, pool, conn, tlsClassifier)
if err != nil {
	return err
}
if index == sniffer.NoMatch {
	classifiers := []sniffer.Classifier{
		httpClassifier,
		sniffer.SSH(),
	}
	index, err = sniffer.SniffWithPool(
		sniffer.MinSniffBufferSize(classifiers...),
		pool,
		conn,
		classifiers...,
	)
}

Incomplete prefixes

If a classifier needs four bytes and the peer sends only three, the only correct state is NeedMore. The next byte can complete the signature, or the peer can be using another protocol.

The caller must set the policy boundary. A common policy treats a classification timeout as fallback and returns other read errors:

_ = conn.SetReadDeadline(time.Now().Add(classificationBudget))
index, err := sniffer.SniffFactoriesWithPool(
	classificationMaxBytes,
	pool,
	conn,
	factories...,
)
_ = conn.SetReadDeadline(time.Time{})

if err != nil {
	if gonnect.IsTimeout(err) {
		return proxyToOriginalDestination(conn)
	}
	return err
}

Use an absolute caller-owned classification deadline. Do not reset it after every byte, because a slow peer can keep an ambiguous connection alive without end.

Match selection

Sniff performs normal batched connection reads, but feeds classifiers one byte at a time and checks their states after every byte. The route does not depend on how TCP splits the stream across Read calls.

Sniff returns as soon as any classifier reports Match. If several classifiers match after the same byte, the lowest index wins. It does not wait for a lower-index classifier that still reports NeedMore.

For example, with Prefix("AB") followed by Prefix("A"), input A selects the second classifier immediately, even when AB arrived in one underlying Read. List order only breaks matches observed at the same byte position. It is not a longest-match parser. Use a combined classifier when overlapping signatures require another policy.

Limits

  • Server-first or silent-client protocols have no client bytes to classify. Use listener metadata, original-destination metadata, a caller-owned deadline, or a fallback route.
  • Some prefixes are ambiguous. Only more bytes or an external policy boundary can decide them.
  • Sniff has a fixed caller-provided budget. Complex classifiers must also enforce their own field or header limits.
  • TCP is a byte stream. Classifiers must not attach protocol meaning to Read boundaries.
  • Classifiers must not perform connection I/O, mutate the read-only Feed slice, or mutate external protocol state.
  • EOF is returned by Sniff; it is not fed to classifiers.
  • Sniff requires exclusive read-side ownership until it returns.
  • Put-back bytes are returned without calling the wrapped connection. Its read deadline is observed after the put-back buffer drains.

SNI, HTTP request lines, PROXY protocol, and similar formats should be bounded incremental classifiers. Malformed or over-limit input should return Mismatch. Classifier errors are intentionally not part of the API. Only errors returned by Conn.Read are returned from Sniff.

Documentation

Overview

Package sniffer incrementally classifies the client-first prefix of a stream while restoring every inspected byte to a putback.Conn before returning.

Classifiers are state machines. Sniff reads into the caller's buffer in normal batches, then feeds each active classifier one byte at a time in stream order. State is checked after every byte, making classification independent of net.Conn Read chunking. Sniff stops when a classifier reports Match, every classifier reports Mismatch, the caller-provided buffer is exhausted, or the connection returns an error. The complete read prefix is put back on every return path, so another sniffer or the selected protocol handler sees the original byte sequence.

SniffWithPool and SniffFactoriesWithPool allocate the temporary inspection buffer from a bufpool.Pool. Pass the same pool to putback.New when restored bytes should also use pooled storage.

Classifiers and factories report MinSniffBufferSize. Use MinSniffBufferSize or MinFactorySniffBufferSize to select a buffer large enough for the classifiers you pass to Sniff. ClassifierFunc and FactoryFunc report 0 by default; use WithMinSniffBufferSize or FactoryWithMinSniffBufferSize when they need a non-zero size.

Classifiers can optionally implement MetadataProvider. Sniff does not use metadata directly, but callers can inspect the matched classifier with Metadata after Sniff returns. Built-in HTTP and TLS classifiers expose HTTPInfo and TLSClientHelloInfo. Custom classifiers can expose any typed value that their routing code understands.

HTTP and HTTPFactory match HTTP request lines. HTTP2 and HTTP2Factory match the cleartext HTTP/2 client preface. HTTPWithConfig and HTTPFactoryWithConfig can also filter by exact or multi-value methods, exact or glob URL request-targets, HTTP-version tokens, normalized hostnames, and request-line or header byte limits.

TLS and TLSFactory match TLS ClientHello records. TLSWithConfig and TLSFactoryWithConfig can also filter by offered TLS versions, visible SNI availability, encrypted_client_hello extension presence, visible SNI hostnames, ALPN protocol names, and ClientHello byte limits. TLS version filters match versions offered by the ClientHello, not the server-selected version.

ProxyProtocolV1, ProxyProtocolV2, and ProxyProtocol match HAProxy PROXY protocol headers. SOCKS4, SOCKS5, and SOCKS match SOCKS client greetings or requests. DNSOverTCP matches length-prefixed DNS query messages. AMQP matches AMQP protocol headers. MQTT matches MQTT CONNECT headers. PostgreSQL matches startup-family packets. MongoDB matches wire protocol request headers. Redis matches RESP array requests. RTSP and SIP match request lines. STUN matches STUN and TURN headers. RDP matches TPKT/X.224 connection requests. SMB matches SMB-over-TCP negotiate requests. LDAP matches LDAP client request prefixes. Cassandra matches native protocol STARTUP and OPTIONS requests. Memcached matches binary and text protocol requests.

Sniff owns neither timeouts nor policy. Callers should set and clear read deadlines on the connection as appropriate. A deadline error is returned as the connection's read error. Use gonnect.IsTimeout to identify timeout errors when timeout means fallback for the caller. Buffer exhaustion is a normal NoMatch result.

Important limitations:

  • Only client-first information can be classified. A server-first protocol, or a client that waits for the server, produces no bytes to inspect. A caller must use metadata, a deadline, or a fallback route for that case.
  • Some prefixes are fundamentally ambiguous. If one protocol's complete signature is a prefix of another's, an immediate Match may select the shorter classifier. Sniff returns as soon as any classifier matches; it does not wait to learn whether a NeedMore classifier might match later. If several classifiers match after the same byte, the lowest index wins.
  • TCP read boundaries have no protocol meaning. A classifier must handle every fragmentation and coalescing when used outside Sniff and must not assume that one Feed call corresponds to one packet or protocol field.
  • Classifiers cannot return errors. Malformed, unsupported, or over-limit input must become Mismatch. The only errors returned by Sniff come from Conn.Read. EOF is returned to the caller; it is not fed to classifiers.
  • Sniff restores bytes, not external side effects. Classifiers must not read from or write to the connection themselves, and Feed input is read-only.
  • The caller must give Sniff exclusive ownership of the connection's read side until it returns. Concurrent readers can consume bytes that cannot be attributed or restored safely.
  • A putback.Conn preserves byte order, but buffered bytes do not call the wrapped connection. Read deadlines and raw socket state are observed only after the put-back buffer drains.

Sniffer is a gonnect.Network middleware in this package. It routes all Network calls to immutable output slots through a Control callback. For outgoing TCP Dial and DialTCP calls, Control can request interception: Sniffer returns a local TCP connection, sniffs client-first bytes from it, restores those bytes, runs SniffControl with the classifier index and metadata, then opens the selected output connection and pipes the stream. Close and Down close only objects returned by Sniffer and never close output Networks.

Index

Examples

Constants

View Source
const (
	// RejectSlot rejects an operation instead of routing it to an output.
	RejectSlot = 0

	// DefaultSlot is the default output slot used when no control callback is
	// configured. Slots are 1-based.
	DefaultSlot = 1
)
View Source
const DefaultDNSOverTCPMessageMaxBytes = 4096

DefaultDNSOverTCPMessageMaxBytes is the default DNS-over-TCP message inspection limit used by DNSOverTCP and DNSOverTCPFactory.

The limit applies to the DNS message bytes after the two-byte TCP length prefix. A message that declares a larger size mismatches.

View Source
const DefaultHTTPHeaderMaxBytes = 8192

DefaultHTTPHeaderMaxBytes is the default HTTP header inspection limit.

The limit applies only when the classifier must inspect headers, such as when Hostname, Hostnames, or HostnamePatterns is set.

View Source
const DefaultHTTPRequestLineMaxBytes = 4096

DefaultHTTPRequestLineMaxBytes is the default HTTP request-line inspection limit used by HTTP and HTTPFactory.

The limit includes the line-ending byte. A request line that does not end with LF within this many bytes mismatches.

View Source
const DefaultTLSClientHelloMaxBytes = 64 * 1024

DefaultTLSClientHelloMaxBytes is the default TLS ClientHello inspection limit used by TLS and TLSFactory.

The limit applies to bytes read from the stream while parsing the first ClientHello. It includes TLS record headers, handshake headers, and ClientHello data. A ClientHello that is not complete within this many bytes mismatches.

View Source
const NoMatch = -1

NoMatch is returned by Sniff when no classifier produced Match.

Variables

This section is empty.

Functions

func Metadata added in v0.38.0

func Metadata(classifier Classifier) any

Metadata returns metadata from classifier when it implements MetadataProvider. Otherwise it returns nil.

func MinFactorySniffBufferSize

func MinFactorySniffBufferSize(factories ...Factory) int

MinFactorySniffBufferSize returns the largest reported minimum Sniff buffer size from factories.

func MinSniffBufferSize

func MinSniffBufferSize(classifiers ...Classifier) int

MinSniffBufferSize returns the largest reported minimum Sniff buffer size from classifiers.

func Sniff

func Sniff(
	buffer []byte,
	conn putback.Conn,
	classifiers ...Classifier,
) (index int, err error)

Sniff incrementally classifies bytes read from conn.

buffer is both scratch storage and the total byte budget for this call. Its length, not its capacity, is the maximum number of bytes Sniff may inspect. The contents on entry are ignored and may be overwritten. A zero-length buffer causes an immediate NoMatch unless a classifier matches on its initial empty Feed.

classifiers are stateful instances intended for this one sniffing operation. Sniff first calls Feed(nil) on each classifier. It reads from conn in normal batches, but feeds classifiers one byte at a time and checks their states after every byte. This makes selection independent of net.Conn Read chunking. As soon as one or more classifiers match on a byte, Sniff returns the lowest matching index. It does not delay a match while another classifier still needs more bytes.

Every byte read by Sniff is put back before Sniff returns, including on no match and read-error paths. Thus callers may invoke another Sniff on the same putback.Conn or hand it to a protocol implementation without losing bytes.

Return values are:

  • index >= 0, err == nil: classifiers[index] matched.
  • index == NoMatch, err == nil: all classifiers mismatched, buffer was exhausted, buffer was empty, no classifiers were supplied, or a Read made no progress by returning (0, nil).
  • index == NoMatch, err != nil: conn.Read returned that error before a definitive classifier result.

Sniff creates no errors of its own. A nil conn or nil/invalid classifier is a programming error and causes a panic.

The caller must provide exclusive read-side access to conn until Sniff returns. Deadline policy is deliberately outside this function; set any read deadline before calling Sniff and clear or replace it afterward.

Example

ExampleSniff shows routing by a client-first prefix. A real server would wrap the net.Conn returned by Accept and set its own classification deadline.

package main

import (
	"fmt"
	"net"
	"time"

	"github.com/asciimoth/bufpool"
	"github.com/asciimoth/gonnect/putback"
	"github.com/asciimoth/gonnect/sniffer"
)

func closeExampleConn(conn net.Conn) {
	_ = conn.Close()
}

func main() {
	server, client := net.Pipe()
	defer closeExampleConn(server)
	defer closeExampleConn(client)

	go func() {
		_, _ = client.Write([]byte("SSH-2.0-example\r\n"))
	}()

	var pool bufpool.Pool
	conn := putback.New(server, pool)
	_ = conn.SetReadDeadline(time.Now().Add(time.Second))
	factories := []sniffer.Factory{
		sniffer.PrefixFactory([]byte("GET ")),
		sniffer.SSHFactory(),
	}
	index, err := sniffer.SniffFactoriesWithPool(
		sniffer.MinFactorySniffBufferSize(factories...),
		pool,
		conn,
		factories...,
	)
	_ = conn.SetReadDeadline(time.Time{})

	fmt.Println(index, err)

}
Output:
1 <nil>
Example (Chain)

ExampleSniff_chain shows that a no-match result does not consume the stream.

package main

import (
	"fmt"
	"net"

	"github.com/asciimoth/bufpool"
	"github.com/asciimoth/gonnect/putback"
	"github.com/asciimoth/gonnect/sniffer"
)

func closeExampleConn(conn net.Conn) {
	_ = conn.Close()
}

func main() {
	server, client := net.Pipe()
	defer closeExampleConn(server)
	defer closeExampleConn(client)

	go func() {
		_, _ = client.Write([]byte("SSH-2.0-example\r\n"))
	}()

	var pool bufpool.Pool
	conn := putback.New(server, pool)
	first, _ := sniffer.SniffWithPool(
		8,
		pool,
		conn,
		sniffer.Prefix([]byte{0x16, 0x03}), // simple TLS record prefix
	)
	second, _ := sniffer.SniffWithPool(4, pool, conn, sniffer.SSH())

	fmt.Println(first == sniffer.NoMatch, second)

}
Output:
true 0
Example (TimeoutFallback)

ExampleSniff_timeoutFallback shows how callers can turn a classification timeout into a fallback route while returning other read errors.

package main

import (
	"fmt"
	"net"
	"time"

	"github.com/asciimoth/bufpool"
	"github.com/asciimoth/gonnect"
	"github.com/asciimoth/gonnect/putback"
	"github.com/asciimoth/gonnect/sniffer"
)

func closeExampleConn(conn net.Conn) {
	_ = conn.Close()
}

func main() {
	server, client := net.Pipe()
	defer closeExampleConn(server)
	defer closeExampleConn(client)

	var pool bufpool.Pool
	conn := putback.New(server, pool)
	_ = conn.SetReadDeadline(time.Now().Add(-time.Nanosecond))
	index, err := sniffer.SniffWithPool(64, pool, conn, sniffer.SSH())
	_ = conn.SetReadDeadline(time.Time{})

	fmt.Println(index == sniffer.NoMatch, gonnect.IsTimeout(err))

}
Output:
true true

func SniffFactories

func SniffFactories(
	buffer []byte,
	conn putback.Conn,
	factories ...Factory,
) (int, error)

SniffFactories is a convenience wrapper that creates one fresh classifier from each factory and calls Sniff. The returned index refers to factories.

func SniffFactoriesWithPool

func SniffFactoriesWithPool(
	bufferSize int,
	pool bufpool.Pool,
	conn putback.Conn,
	factories ...Factory,
) (int, error)

SniffFactoriesWithPool is a convenience wrapper that creates one fresh classifier from each factory and calls SniffWithPool. The returned index refers to factories.

func SniffWithPool

func SniffWithPool(
	bufferSize int,
	pool bufpool.Pool,
	conn putback.Conn,
	classifiers ...Classifier,
) (int, error)

SniffWithPool gets the inspection buffer from pool, calls Sniff, and returns the buffer to pool before it returns.

bufferSize is the total byte budget for this call. A zero bufferSize has the same behavior as passing a zero-length buffer to Sniff. bufferSize must not be negative.

Types

type Action added in v0.38.0

type Action struct {
	Slot      int
	Intercept bool
}

Action is the routing decision returned by a control callback.

Slot is a 1-based output slot. Slot 0 rejects the call. Invalid slots and nil output slots also reject the call.

Intercept is honored only for outgoing TCP Dial and DialTCP calls. When it is true for those calls, Sniffer returns a local mock TCP connection, sniffs client-first bytes from that connection, and then calls SniffControl for the final route. When it is true for any other call, Sniffer rejects the call as if Slot were RejectSlot.

type Call added in v0.38.0

type Call struct {
	Operation Operation
	Network   string
	Src       string
	Dst       string
	Host      string
	Service   string
	Proto     string
	IfIndex   int
	IfName    string

	ListenConfig     *gonnect.ListenConfig
	MulticastOptions gonnect.MulticastOptions
}

Call contains the arguments for one Network method call.

The control callback receives a pointer to a Call copy. It may modify fields before returning its Action. Sniffer uses the modified fields for routing. The callback must not retain the pointer.

Dial-style operations use Dst for the remote address. DialTCP and DialUDP also use Src for the local address. Listen-style operations use Src for the listen address. Lookup operations use Host for the lookup name or address, except LookupPort, which uses Service, and LookupSRV, which uses Service, Proto, and Host. Interface lookups use IfIndex or IfName.

type Classifier

type Classifier interface {
	MinSniffBufferSizer

	Feed(p []byte) State
}

Classifier incrementally recognizes a byte stream prefix.

Feed receives only bytes not supplied in earlier calls, in stream order, and returns the state after consuming p. Feed may be called with an empty slice to query the initial/current state without advancing input. Implementations must therefore handle empty input.

Match and Mismatch are terminal: after returning either state, a classifier must return the same state from all later Feed calls. Sniff and the supplied combinators normally stop feeding terminal classifiers.

p is read-only. A classifier must not modify it and must not retain it unless it copies the bytes it needs. The caller's sniff buffer may be reused after Sniff returns.

Classifiers do not return errors. Invalid, malformed, unsupported, or classifier-specific over-limit data should result in Mismatch.

func AMQP

func AMQP() Classifier

AMQP returns a classifier for AMQP protocol headers.

It matches the eight-byte protocol header for AMQP 0-9-1, AMQP 1.0, or AMQP 1.0 SASL negotiation. It does not inspect later connection tuning, SASL frames, or AMQP frames.

func And

func And(children ...Classifier) Classifier

And returns a classifier that matches when every child matches, mismatches when any child mismatches, and otherwise needs more bytes.

And with no children is the boolean identity true and therefore matches on its initial empty Feed.

func Cassandra

func Cassandra() Classifier

Cassandra returns a classifier for Cassandra native protocol startup requests.

It validates a v3, v4, or v5 request header and accepts STARTUP or OPTIONS, which are the normal first client messages on a Cassandra connection. It does not inspect the body string map or compression.

func DNSOverTCP

func DNSOverTCP() Classifier

DNSOverTCP returns a classifier that matches a DNS-over-TCP query.

The classifier reads the two-byte TCP length prefix, then validates a DNS message header and question section. It requires a client query, not a response, and at least one question.

func DNSOverTCPWithConfig

func DNSOverTCPWithConfig(config DNSOverTCPConfig) Classifier

DNSOverTCPWithConfig returns a DNS-over-TCP classifier that uses config.

func HTTP

func HTTP() Classifier

HTTP returns a classifier that matches an HTTP request line.

The classifier accepts any syntactically valid method token, non-empty request-target, and HTTP-version token that starts with "HTTP/". Use HTTPWithConfig when the route must match specific methods, URL request-targets, HTTP versions, hostnames, or byte limits.

func HTTP2

func HTTP2() Classifier

HTTP2 returns a classifier for cleartext HTTP/2 prior knowledge.

It matches the HTTP/2 client connection preface at stream offset zero. TLS connections that negotiate HTTP/2 by ALPN are still TLS streams; use TLS with an ALPN filter for those connections.

func HTTPWithConfig

func HTTPWithConfig(config HTTPConfig) Classifier

HTTPWithConfig returns a classifier that matches an HTTP request and the fields requested by config.

func LDAP

func LDAP() Classifier

LDAP returns a classifier for LDAP client messages.

It validates enough BER to find a non-zero message ID and a client request protocolOp tag. It does not parse request fields, controls, filters, or authentication data.

func Limit

func Limit(limit int, child Classifier) Classifier

Limit wraps child and changes NeedMore to Mismatch after limit bytes have been fed to it. It is useful for making a classifier's own inspection bound explicit independently of Sniff's total buffer bound.

At most limit bytes are passed to child. If a Feed chunk crosses the limit, only the portion within the limit is passed. A child that matches or mismatches within that portion keeps its result. limit must be non-negative.

func MQTT

func MQTT() Classifier

MQTT returns a classifier for MQTT CONNECT packets.

It validates the fixed header packet type, decodes the Remaining Length field, and checks the CONNECT variable header through protocol name, version level, connect flags, and keep-alive. MQTT 3.1, 3.1.1, and 5 CONNECT headers are accepted. The classifier matches before reading the client identifier or other payload fields.

func Memcached

func Memcached() Classifier

Memcached returns a classifier for memcached binary or text protocol requests.

func MemcachedASCII

func MemcachedASCII() Classifier

MemcachedASCII returns a classifier for memcached text protocol requests.

It validates the first CRLF-terminated command line for common storage, retrieval, counter, delete, touch, flush, stats, version, quit, and verbosity commands. It matches before reading any value bytes after a storage command.

func MemcachedBinary

func MemcachedBinary() Classifier

MemcachedBinary returns a classifier for memcached binary protocol requests.

It validates the fixed 24-byte request header, request magic, known opcode, data type, extras length, key presence rules, and body length. It does not inspect the key, value, CAS, or opaque fields.

func MongoDB

func MongoDB() Classifier

MongoDB returns a classifier for MongoDB wire protocol request messages.

It validates the 16-byte wire message header, requires a client request opcode, and requires responseTo to be zero. It does not inspect BSON command documents or compressed message bodies.

func Not

func Not(child Classifier) Classifier

Not returns the boolean negation of child. NeedMore remains NeedMore, Match becomes Mismatch, and Mismatch becomes Match.

func Or

func Or(children ...Classifier) Classifier

Or returns a classifier that matches when any child matches, mismatches when every child mismatches, and otherwise needs more bytes.

Or with no children is the boolean identity false and therefore mismatches on its initial empty Feed.

func PostgreSQL

func PostgreSQL() Classifier

PostgreSQL returns a classifier for PostgreSQL client startup messages.

It validates the first eight bytes of the client startup packet. Normal protocol 3.0 startup messages, SSLRequest, GSSENCRequest, and CancelRequest packets are accepted. It does not inspect startup parameters, user names, databases, or cancellation keys.

func Prefix

func Prefix(prefix []byte) Classifier

Prefix returns a classifier that matches when the stream begins with prefix. It mismatches at the first differing byte and needs more bytes while the bytes seen so far are a proper prefix of prefix.

Prefix copies prefix. An empty prefix matches immediately on an empty Feed.

func ProxyProtocol

func ProxyProtocol() Classifier

ProxyProtocol returns a classifier for HAProxy PROXY protocol v1 or v2.

func ProxyProtocolV1

func ProxyProtocolV1() Classifier

ProxyProtocolV1 returns a classifier for HAProxy PROXY protocol v1.

It matches the ASCII prefix "PROXY " at stream offset zero. This is a routing heuristic, not a full PROXY v1 header parser. It does not validate the source address, destination address, ports, or line ending.

func ProxyProtocolV2

func ProxyProtocolV2() Classifier

ProxyProtocolV2 returns a classifier for HAProxy PROXY protocol v2.

It validates the binary signature and fixed 16-byte header. For PROXY commands with a known address family, it also checks that the declared payload length can contain the required source and destination addresses. It does not inspect address values or TLVs.

func RDP

func RDP() Classifier

RDP returns a classifier for RDP connection requests over TPKT.

It validates the TPKT header and the X.224 Connection Request fixed fields. It matches before optional cookies, routing tokens, or negotiation data are parsed.

func RTSP

func RTSP() Classifier

RTSP returns a classifier for RTSP request lines.

It matches a known RTSP method, a non-empty request URI, and the RTSP/1.0 version token on the first CRLF-terminated line. It does not inspect RTSP headers, Transport parameters, CSeq, or message bodies.

func Redis

func Redis() Classifier

Redis returns a classifier for Redis RESP array requests.

It validates the first request array and its first bulk-string command name. Inline Redis commands are intentionally not matched because their text forms are ambiguous with other line-oriented protocols. The command name must be non-empty, use printable non-space bytes, and be no larger than 128 bytes.

func SIP

func SIP() Classifier

SIP returns a classifier for SIP request lines.

It matches a known SIP method, a non-empty Request-URI, and the SIP/2.0 version token on the first CRLF-terminated line. It does not inspect Via, To, From, Call-ID, CSeq, or message bodies.

func SMB

func SMB() Classifier

SMB returns a classifier for SMB over TCP client negotiate requests.

It validates the NetBIOS Session Service header and the start of an SMB1 or SMB2/3 negotiate request. It does not inspect dialects, security modes, capabilities, or later SMB messages.

func SOCKS

func SOCKS() Classifier

SOCKS returns a classifier for SOCKS4, SOCKS4a, or SOCKS5 client streams.

func SOCKS4

func SOCKS4() Classifier

SOCKS4 returns a classifier for SOCKS4 and SOCKS4a requests.

It validates the fixed request header: version 4, CONNECT or BIND command, non-zero destination port, and non-zero destination address marker. It matches before reading the user ID or the optional SOCKS4a hostname.

func SOCKS5

func SOCKS5() Classifier

SOCKS5 returns a classifier for SOCKS5 client greetings.

It validates the version byte, waits for the declared method list, and rejects empty method lists.

func SSH

func SSH() Classifier

SSH returns a simple SSH transport classifier.

It matches the four-byte ASCII prefix "SSH-" at stream offset zero. This is intentionally a routing heuristic, not a complete SSH identification-line validator. It does not validate protocol/software versions and does not accept non-SSH preamble lines before the identification string.

func STUN

func STUN() Classifier

STUN returns a classifier for STUN and TURN messages.

It validates the fixed 20-byte STUN header: message type top bits, message length alignment, magic cookie, and known method. It accepts STUN methods used by TURN as well. It does not inspect attributes or integrity.

func TLS

func TLS() Classifier

TLS returns a classifier that matches a syntactically valid TLS ClientHello.

Use TLSWithConfig when the route must match offered TLS versions, SNI availability, ECH presence, SNI hostnames, ALPN protocols, or byte limits.

func TLSWithConfig

func TLSWithConfig(config TLSConfig) Classifier

TLSWithConfig returns a classifier that matches a TLS ClientHello and the fields requested by config.

func WithMinSniffBufferSize

func WithMinSniffBufferSize(
	minSize int,
	classifier Classifier,
) Classifier

WithMinSniffBufferSize wraps classifier and reports minSize as its minimum Sniff buffer size. It is useful for custom classifiers.

type ClassifierFunc

type ClassifierFunc func(p []byte) State

ClassifierFunc adapts a function to Classifier. It reports a minimum Sniff buffer size of 0. Use WithMinSniffBufferSize when f needs a larger buffer.

func (ClassifierFunc) Feed

func (f ClassifierFunc) Feed(p []byte) State

Feed calls f(p).

func (ClassifierFunc) MinSniffBufferSize

func (f ClassifierFunc) MinSniffBufferSize() int

MinSniffBufferSize returns 0.

type CompositeMetadata added in v0.38.0

type CompositeMetadata struct {
	Children []any
}

CompositeMetadata stores child metadata from a composite classifier.

Children has the same order as the child classifiers. A nil child value means that child had no metadata.

type Control added in v0.38.0

type Control func(*Call) Action

Control decides how a Network method call is processed.

type DNSOverTCPConfig

type DNSOverTCPConfig struct {
	MaxMessageBytes int
}

DNSOverTCPConfig configures a DNS-over-TCP classifier.

MaxMessageBytes bounds the DNS message bytes after the two-byte TCP length prefix. Zero uses DefaultDNSOverTCPMessageMaxBytes. A negative value, or a value above the 65535-byte DNS TCP length field, is a programming error and causes a panic.

type Factory

type Factory interface {
	MinSniffBufferSizer

	NewClassifier() Classifier
}

Factory constructs a fresh classifier for one stream.

NewClassifier must return an independent instance on every call. A Factory may be shared by goroutines and should therefore be safe for concurrent use.

func AMQPFactory

func AMQPFactory() Factory

AMQPFactory returns a factory for AMQP classifiers.

func AndFactory

func AndFactory(children ...Factory) Factory

AndFactory returns a factory that constructs a fresh And classifier around fresh classifiers from children.

func CassandraFactory

func CassandraFactory() Factory

CassandraFactory returns a factory for Cassandra classifiers.

func DNSOverTCPFactory

func DNSOverTCPFactory() Factory

DNSOverTCPFactory returns a factory for DNSOverTCP classifiers.

func DNSOverTCPFactoryWithConfig

func DNSOverTCPFactoryWithConfig(config DNSOverTCPConfig) Factory

DNSOverTCPFactoryWithConfig returns a factory for DNS-over-TCP classifiers that use config.

func FactoryWithMinSniffBufferSize

func FactoryWithMinSniffBufferSize(
	minSize int,
	factory Factory,
) Factory

FactoryWithMinSniffBufferSize wraps factory and reports minSize as the minimum Sniff buffer size needed by classifiers it creates.

func HTTP2Factory

func HTTP2Factory() Factory

HTTP2Factory returns a factory for HTTP2 classifiers.

func HTTPFactory

func HTTPFactory() Factory

HTTPFactory returns a factory for HTTP classifiers with the default config.

func HTTPFactoryWithConfig

func HTTPFactoryWithConfig(config HTTPConfig) Factory

HTTPFactoryWithConfig returns a factory for HTTP classifiers that use config.

func LDAPFactory

func LDAPFactory() Factory

LDAPFactory returns a factory for LDAP classifiers.

func LimitFactory

func LimitFactory(limit int, child Factory) Factory

LimitFactory returns a factory that applies Limit to fresh child instances.

func MQTTFactory

func MQTTFactory() Factory

MQTTFactory returns a factory for MQTT classifiers.

func MemcachedASCIIFactory

func MemcachedASCIIFactory() Factory

MemcachedASCIIFactory returns a factory for memcached text classifiers.

func MemcachedBinaryFactory

func MemcachedBinaryFactory() Factory

MemcachedBinaryFactory returns a factory for memcached binary classifiers.

func MemcachedFactory

func MemcachedFactory() Factory

MemcachedFactory returns a factory for memcached binary or text classifiers.

func MongoDBFactory

func MongoDBFactory() Factory

MongoDBFactory returns a factory for MongoDB classifiers.

func NotFactory

func NotFactory(child Factory) Factory

NotFactory returns a factory that constructs a fresh Not classifier around a fresh classifier from child.

func OrFactory

func OrFactory(children ...Factory) Factory

OrFactory returns a factory that constructs a fresh Or classifier around fresh classifiers from children.

func PostgreSQLFactory

func PostgreSQLFactory() Factory

PostgreSQLFactory returns a factory for PostgreSQL classifiers.

func PrefixFactory

func PrefixFactory(prefix []byte) Factory

PrefixFactory returns a factory for Prefix classifiers. It copies prefix when the factory is created, so later caller mutation does not affect instances.

func ProxyProtocolFactory

func ProxyProtocolFactory() Factory

ProxyProtocolFactory returns a factory for PROXY protocol classifiers.

func ProxyProtocolV1Factory

func ProxyProtocolV1Factory() Factory

ProxyProtocolV1Factory returns a factory for PROXY protocol v1 classifiers.

func ProxyProtocolV2Factory

func ProxyProtocolV2Factory() Factory

ProxyProtocolV2Factory returns a factory for PROXY protocol v2 classifiers.

func RDPFactory

func RDPFactory() Factory

RDPFactory returns a factory for RDP classifiers.

func RTSPFactory

func RTSPFactory() Factory

RTSPFactory returns a factory for RTSP classifiers.

func RedisFactory

func RedisFactory() Factory

RedisFactory returns a factory for Redis classifiers.

func SIPFactory

func SIPFactory() Factory

SIPFactory returns a factory for SIP classifiers.

func SMBFactory

func SMBFactory() Factory

SMBFactory returns a factory for SMB classifiers.

func SOCKS4Factory

func SOCKS4Factory() Factory

SOCKS4Factory returns a factory for SOCKS4 classifiers.

func SOCKS5Factory

func SOCKS5Factory() Factory

SOCKS5Factory returns a factory for SOCKS5 classifiers.

func SOCKSFactory

func SOCKSFactory() Factory

SOCKSFactory returns a factory for SOCKS classifiers.

func SSHFactory

func SSHFactory() Factory

SSHFactory returns a factory for SSH classifiers.

func STUNFactory

func STUNFactory() Factory

STUNFactory returns a factory for STUN classifiers.

func TLSFactory

func TLSFactory() Factory

TLSFactory returns a factory for TLS classifiers with the default config.

func TLSFactoryWithConfig

func TLSFactoryWithConfig(config TLSConfig) Factory

TLSFactoryWithConfig returns a factory for TLS classifiers that use config.

type FactoryFunc

type FactoryFunc func() Classifier

FactoryFunc adapts a function to Factory. It reports a minimum Sniff buffer size of 0. Use FactoryWithMinSniffBufferSize when f needs a larger buffer.

func (FactoryFunc) MinSniffBufferSize

func (f FactoryFunc) MinSniffBufferSize() int

MinSniffBufferSize returns 0.

func (FactoryFunc) NewClassifier

func (f FactoryFunc) NewClassifier() Classifier

NewClassifier calls f.

type HTTPConfig

type HTTPConfig struct {
	MaxRequestLineBytes int
	MaxHeaderBytes      int

	Method  string
	Methods []string

	URL         string
	URLs        []string
	URLPatterns []string

	Version  string
	Versions []string

	Hostname         string
	Hostnames        []string
	HostnamePatterns []string
}

HTTPConfig configures an HTTP request classifier.

Empty field groups are wildcards. Non-empty values in a field group are ORed within that group, and all configured groups must match. For example, Methods {"GET", "POST"} and URLPatterns {"/api/*"} matches GET or POST requests whose request-target matches /api/*.

The singular Method, URL, Version, and Hostname fields are exact-match shortcuts. They are ORed with their plural exact-match fields. Empty strings in plural fields are ignored.

URL and URLPatterns match the request-target exactly as sent on the wire, such as /path?q=1 or http://example.com/path. Version and Versions match the HTTP-version token, such as HTTP/1.1. Leave Version and Versions empty to accept any HTTP-version token that starts with HTTP/.

URLPatterns and HostnamePatterns are byte-oriented glob patterns. A pattern must match the whole value. In a pattern, * matches any byte sequence, ? matches one byte, and \ escapes the next byte.

Hostname, Hostnames, and HostnamePatterns match a normalized hostname from the absolute-form request-target or the Host header. Matching is case-insensitive, and an optional port is removed before matching.

MaxRequestLineBytes bounds how many request-line bytes the classifier may inspect, including LF. MaxHeaderBytes bounds bytes after the request line when hostname matching must inspect headers. Zero values use the defaults. A negative value is a programming error and causes a panic.

type HTTPInfo added in v0.38.0

type HTTPInfo struct {
	Method   string
	URL      string
	Version  string
	Hostname string
}

HTTPInfo is metadata parsed from an HTTP request prefix.

Method, URL, and Version come from the request line. Hostname is normalized and lower-case when it was visible from an absolute-form request-target or from a Host header inspected by the classifier. Hostname can be empty when the classifier did not need to inspect a Host header.

type MetadataProvider added in v0.38.0

type MetadataProvider interface {
	Metadata() any
}

MetadataProvider is implemented by classifiers that expose parsed metadata.

Metadata returns data that is meaningful to the classifier implementation. It should return nil when no metadata is available. A classifier that returns Match should return a stable value after the matching Feed call completes.

Sniff does not depend on metadata. Callers can type-assert the matched classifier to MetadataProvider after Sniff returns.

type MinSniffBufferSizer

type MinSniffBufferSizer interface {
	MinSniffBufferSize() int
}

MinSniffBufferSizer reports a minimum Sniff buffer size.

The value is the number of bytes needed by a classifier, or by classifiers made by a factory, to make its bounded decision. A caller that sniffs with several classifiers should use the largest reported size.

type Operation added in v0.38.0

type Operation string

Operation names the gonnect.Network method currently being controlled.

const (
	OpDial               Operation = "Dial"
	OpListen             Operation = "Listen"
	OpPacketDial         Operation = "PacketDial"
	OpListenPacket       Operation = "ListenPacket"
	OpDialTCP            Operation = "DialTCP"
	OpListenTCP          Operation = "ListenTCP"
	OpDialUDP            Operation = "DialUDP"
	OpListenUDP          Operation = "ListenUDP"
	OpListenPacketConfig Operation = "ListenPacketConfig"
	OpListenUDPConfig    Operation = "ListenUDPConfig"
	OpListenMulticastUDP Operation = "ListenMulticastUDP"
	OpLookupIP           Operation = "LookupIP"
	OpLookupIPAddr       Operation = "LookupIPAddr"
	OpLookupNetIP        Operation = "LookupNetIP"
	OpLookupHost         Operation = "LookupHost"
	OpLookupAddr         Operation = "LookupAddr"
	OpLookupCNAME        Operation = "LookupCNAME"
	OpLookupPort         Operation = "LookupPort"
	OpLookupNS           Operation = "LookupNS"
	OpLookupMX           Operation = "LookupMX"
	OpLookupSRV          Operation = "LookupSRV"
	OpLookupTXT          Operation = "LookupTXT"
	OpInterfaces         Operation = "Interfaces"
	OpInterfaceAddrs     Operation = "InterfaceAddrs"
	OpInterfaceMcast     Operation = "InterfaceMulticastAddrs"
	OpInterfacesByIndex  Operation = "InterfacesByIndex"
	OpInterfacesByName   Operation = "InterfacesByName"
)

type SniffControl added in v0.38.0

type SniffControl func(*SniffedCall) Action

SniffControl decides the final route for an intercepted and sniffed connection.

type SniffResult added in v0.38.0

type SniffResult struct {
	// Index is the matched classifier index, or NoMatch.
	Index int
	// Metadata is Metadata(classifier) for the matched classifier.
	Metadata any
	// Err is the read error returned by Sniff, if any.
	Err error
}

SniffResult is the output of an intercepted connection sniff.

type SniffedCall added in v0.38.0

type SniffedCall struct {
	Call
	Result SniffResult
}

SniffedCall is passed to SniffControl after an intercepted connection has been sniffed.

type Sniffer added in v0.38.0

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

Sniffer is a gonnect.Network middleware that routes calls to immutable output slots and can sniff outgoing TCP dials before the final route.

Sniffer does not close or move output Networks up or down. It owns only the connections and listeners returned by its own methods. Close permanently closes Sniffer, closes those owned objects, and makes future calls return net.ErrClosed or normal rejection errors. Down closes owned objects and rejects calls until Up is called.

Output slot closure affects only calls routed to that output. Sniffer does not subscribe to output lifecycle events.

func NewSniffer added in v0.38.0

func NewSniffer(config SnifferConfig) (*Sniffer, error)

NewSniffer creates a Sniffer Network.

func (*Sniffer) Classifiers added in v0.38.0

func (s *Sniffer) Classifiers() []Factory

Classifiers returns a copy of the configured classifier factories.

func (*Sniffer) Close added in v0.38.0

func (s *Sniffer) Close() error

Close permanently closes Sniffer. It does not close output Networks.

func (*Sniffer) Dial added in v0.38.0

func (s *Sniffer) Dial(
	ctx context.Context,
	network, address string,
) (net.Conn, error)

Dial routes or intercepts a stream dial.

func (*Sniffer) DialTCP added in v0.38.0

func (s *Sniffer) DialTCP(
	ctx context.Context,
	network, laddr, raddr string,
) (gonnect.TCPConn, error)

DialTCP routes or intercepts an outgoing TCP dial.

func (*Sniffer) DialUDP added in v0.38.0

func (s *Sniffer) DialUDP(
	ctx context.Context,
	network, laddr, raddr string,
) (gonnect.UDPConn, error)

DialUDP routes a UDP dial.

func (*Sniffer) Down added in v0.38.0

func (s *Sniffer) Down() error

Down disables Sniffer and closes all objects returned by it.

func (*Sniffer) InterfaceAddrs added in v0.38.0

func (s *Sniffer) InterfaceAddrs() ([]net.Addr, error)

InterfaceAddrs routes an interface address list call.

func (*Sniffer) InterfaceMulticastAddrs added in v0.38.0

func (s *Sniffer) InterfaceMulticastAddrs() ([]net.Addr, error)

InterfaceMulticastAddrs routes an interface multicast address list call.

func (*Sniffer) Interfaces added in v0.38.0

func (s *Sniffer) Interfaces() ([]gonnect.NetworkInterface, error)

Interfaces routes an interface list call.

func (*Sniffer) InterfacesByIndex added in v0.38.0

func (s *Sniffer) InterfacesByIndex(
	index int,
) ([]gonnect.NetworkInterface, error)

InterfacesByIndex routes an interface lookup by index.

func (*Sniffer) InterfacesByName added in v0.38.0

func (s *Sniffer) InterfacesByName(
	name string,
) ([]gonnect.NetworkInterface, error)

InterfacesByName routes an interface lookup by name.

func (*Sniffer) IsNative added in v0.38.0

func (s *Sniffer) IsNative() bool

IsNative always reports false because Sniffer can route and defer dials.

func (*Sniffer) IsUp added in v0.38.0

func (s *Sniffer) IsUp() (bool, error)

IsUp reports whether Sniffer is currently up and not closed.

func (*Sniffer) Listen added in v0.38.0

func (s *Sniffer) Listen(
	ctx context.Context,
	network, address string,
) (net.Listener, error)

Listen routes a stream listener.

func (*Sniffer) ListenMulticastUDP added in v0.38.0

func (s *Sniffer) ListenMulticastUDP(
	ctx context.Context,
	network, address string,
	opts gonnect.MulticastOptions,
) (gonnect.MulticastPacketConn, error)

ListenMulticastUDP routes a multicast UDP listener.

func (*Sniffer) ListenPacket added in v0.38.0

func (s *Sniffer) ListenPacket(
	ctx context.Context,
	network, address string,
) (gonnect.PacketConn, error)

ListenPacket routes a packet listener.

func (*Sniffer) ListenPacketConfig added in v0.38.0

func (s *Sniffer) ListenPacketConfig(
	ctx context.Context,
	lc *gonnect.ListenConfig,
	network, address string,
) (gonnect.PacketConn, error)

ListenPacketConfig routes a packet listener with a ListenConfig.

func (*Sniffer) ListenTCP added in v0.38.0

func (s *Sniffer) ListenTCP(
	ctx context.Context,
	network, laddr string,
) (gonnect.TCPListener, error)

ListenTCP routes a TCP listener.

func (*Sniffer) ListenUDP added in v0.38.0

func (s *Sniffer) ListenUDP(
	ctx context.Context,
	network, laddr string,
) (gonnect.UDPConn, error)

ListenUDP routes a UDP listener.

func (*Sniffer) ListenUDPConfig added in v0.38.0

func (s *Sniffer) ListenUDPConfig(
	ctx context.Context,
	lc *gonnect.ListenConfig,
	network, laddr string,
) (gonnect.UDPConn, error)

ListenUDPConfig routes a UDP listener with a ListenConfig.

func (*Sniffer) LookupAddr added in v0.38.0

func (s *Sniffer) LookupAddr(
	ctx context.Context,
	addr string,
) ([]string, error)

LookupAddr routes a reverse lookup.

func (*Sniffer) LookupCNAME added in v0.38.0

func (s *Sniffer) LookupCNAME(
	ctx context.Context,
	host string,
) (string, error)

LookupCNAME routes a CNAME lookup.

func (*Sniffer) LookupHost added in v0.38.0

func (s *Sniffer) LookupHost(
	ctx context.Context,
	host string,
) ([]string, error)

LookupHost routes a host lookup.

func (*Sniffer) LookupIP added in v0.38.0

func (s *Sniffer) LookupIP(
	ctx context.Context,
	network, address string,
) ([]net.IP, error)

LookupIP routes an IP lookup.

func (*Sniffer) LookupIPAddr added in v0.38.0

func (s *Sniffer) LookupIPAddr(
	ctx context.Context,
	host string,
) ([]net.IPAddr, error)

LookupIPAddr routes an IP-address lookup.

func (*Sniffer) LookupMX added in v0.38.0

func (s *Sniffer) LookupMX(
	ctx context.Context,
	name string,
) ([]*net.MX, error)

LookupMX routes an MX lookup.

func (*Sniffer) LookupNS added in v0.38.0

func (s *Sniffer) LookupNS(
	ctx context.Context,
	name string,
) ([]*net.NS, error)

LookupNS routes an NS lookup.

func (*Sniffer) LookupNetIP added in v0.38.0

func (s *Sniffer) LookupNetIP(
	ctx context.Context,
	network, host string,
) ([]netip.Addr, error)

LookupNetIP routes a netip lookup.

func (*Sniffer) LookupPort added in v0.38.0

func (s *Sniffer) LookupPort(
	ctx context.Context,
	network, service string,
) (int, error)

LookupPort routes a service lookup.

func (*Sniffer) LookupSRV added in v0.38.0

func (s *Sniffer) LookupSRV(
	ctx context.Context,
	service, proto, name string,
) (string, []*net.SRV, error)

LookupSRV routes an SRV lookup.

func (*Sniffer) LookupTXT added in v0.38.0

func (s *Sniffer) LookupTXT(
	ctx context.Context,
	name string,
) ([]string, error)

LookupTXT routes a TXT lookup.

func (*Sniffer) Outputs added in v0.38.0

func (s *Sniffer) Outputs() []gonnect.Network

Outputs returns a copy of the configured output slots.

func (*Sniffer) PacketDial added in v0.38.0

func (s *Sniffer) PacketDial(
	ctx context.Context,
	network, address string,
) (gonnect.PacketConn, error)

PacketDial routes a packet dial.

func (*Sniffer) SubscribeCloser added in v0.38.0

func (s *Sniffer) SubscribeCloser(c io.Closer) (func(), error)

SubscribeCloser registers c to be closed when Sniffer is closed.

func (*Sniffer) SubscribeUpDown added in v0.38.0

func (s *Sniffer) SubscribeUpDown(u gonnect.UpDown) (func(), error)

SubscribeUpDown registers u to follow Sniffer's Up and Down state.

func (*Sniffer) Up added in v0.38.0

func (s *Sniffer) Up() error

Up re-enables Sniffer after Down.

type SnifferConfig added in v0.38.0

type SnifferConfig struct {
	// Outputs are the immutable output slots. Slots are 1-based.
	Outputs []gonnect.Network

	// Control runs for every Network method call.
	Control Control

	// SniffControl runs after an intercepted outgoing TCP connection is
	// sniffed. If nil, the first Action is used as the final action.
	SniffControl SniffControl

	// Classifiers are used for each intercepted outgoing TCP connection.
	Classifiers []Factory

	// SniffBufferSize is the maximum byte count inspected from an intercepted
	// connection. Zero uses MinFactorySniffBufferSize(Classifiers...).
	SniffBufferSize int

	// Pool optionally provides sniff and put-back buffers.
	Pool bufpool.Pool

	// Spawner optionally starts background workers.
	Spawner gonnect.Spawner
}

SnifferConfig configures a Sniffer Network.

type State

type State uint8

State is the current terminal or non-terminal result of a Classifier.

const (
	// NeedMore means the bytes seen so far are compatible with the classifier,
	// but are insufficient for a definitive decision.
	NeedMore State = iota

	// Match means the classifier has definitively recognized its input.
	Match

	// Mismatch means no future suffix can make the classifier match.
	Mismatch
)

func (State) String

func (s State) String() string

String returns a human-readable state name.

type TLSClientHelloInfo

type TLSClientHelloInfo struct {
	Versions      []uint16
	SNIHostname   string
	SNIEncrypted  bool
	ALPNProtocols []string
}

TLSClientHelloInfo is the visible metadata from a TLS ClientHello.

Versions contains the protocol versions offered by the client. If the supported_versions extension is present, it is used. Otherwise Versions contains the legacy_version field.

SNIHostname is the normalized visible SNI host_name, if one is present. It is lower-case and never has a trailing dot. When ECH is present, this is the outer ClientHello hostname.

SNIEncrypted reports whether the encrypted_client_hello extension is present. This is only the visible ECH signal. It does not prove that the server will accept ECH and it cannot reveal an encrypted inner hostname.

ALPNProtocols contains the ALPN protocols offered by the client, in wire order.

func SniffTLSClientHello

func SniffTLSClientHello(
	buffer []byte,
	conn putback.Conn,
) (info TLSClientHelloInfo, ok bool, err error)

SniffTLSClientHello sniffs a syntactically valid TLS ClientHello and returns its visible metadata.

buffer is both scratch storage and the total byte budget. Its length limits how many bytes this function may inspect. All bytes read from conn are put back before this function returns, including on no-match and read-error paths.

The ok result is true only when a full valid TLS ClientHello was parsed within buffer. Non-TLS data, malformed TLS data, and TLS data that is over the byte budget return ok false with a nil error. Read errors are returned unchanged.

type TLSConfig

type TLSConfig struct {
	MaxClientHelloBytes int

	Version  uint16
	Versions []uint16

	SNIAvailable TLSFlag
	SNIEncrypted TLSFlag

	Hostname         string
	Hostnames        []string
	HostnamePatterns []string

	ALPN         string
	ALPNs        []string
	ALPNPatterns []string
}

TLSConfig configures a TLS ClientHello classifier.

Empty field groups are wildcards. Non-empty values in a field group are ORed within that group, and all configured groups must match. For example, Versions {tls.VersionTLS12, tls.VersionTLS13}, HostnamePatterns {"*.example.test"}, and ALPNs {"h2", "http/1.1"} match a TLS ClientHello that offers TLS 1.2 or TLS 1.3, has a visible SNI hostname below example.test, and offers h2 or http/1.1 by ALPN.

Version and Versions match protocol versions offered by the ClientHello. If the supported_versions extension is present, it is used. Otherwise the ClientHello legacy_version field is used. The server-selected TLS version is not visible before routing.

SNIAvailable matches whether a visible SNI host_name is present. SNIEncrypted matches whether the encrypted_client_hello extension is present. This is an observable ECH signal only; the classifier cannot verify that the server accepts ECH or reveal an encrypted inner hostname.

Hostname, Hostnames, and HostnamePatterns match the visible SNI hostname. Matching is case-insensitive. If ECH is present, the visible hostname is the outer ClientHello hostname, when the client sends one.

ALPN, ALPNs, and ALPNPatterns match any protocol name in the ALPN extension. Matching is case-sensitive.

HostnamePatterns and ALPNPatterns are byte-oriented glob patterns. A pattern must match the whole value. In a pattern, * matches any byte sequence, ? matches one byte, and \ escapes the next byte.

MaxClientHelloBytes bounds bytes inspected while parsing the first ClientHello. Zero uses DefaultTLSClientHelloMaxBytes. A negative value is a programming error and causes a panic.

type TLSFlag

type TLSFlag uint8

TLSFlag is a boolean filter used by TLSConfig.

The zero value is a wildcard. Required matches when the observed flag is true. Forbidden matches when the observed flag is false.

const (
	// TLSFlagAny accepts both true and false.
	TLSFlagAny TLSFlag = iota

	// TLSFlagRequired requires the observed flag to be true.
	TLSFlagRequired

	// TLSFlagForbidden requires the observed flag to be false.
	TLSFlagForbidden
)

Jump to

Keyboard shortcuts

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