Skip to content

runZeroInc/conniver

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Conniver

Conniver is a small Go package that wraps net.Conn sockets and collects detailed event information. On common platforms, the TCP_INFO/TCP_CONNECTION socket options are used to obtained kernel-level statistics for the connection, including round-trip-time, max segment size, and more.

Overview

Conniver is best used by specifying a DialContext with a TCP or HTTP.

If you aren't able to wrap the net.Conn, you can instead call tcpinfo.TCPInfo() directly instead. You can find the documentation for this in the package README.

The following code demonstrates using conniver to collect connection data from the net/http.Client. The wrapped net.Conn should work with most Go packages that allow custom dialers or provide some way to provide a proxy net.Conn.

import (
    "context"
    "encoding/json"
    "net/http"
    "fmt"
    
    "github.com/runZeroInc/conniver"
)
func main() {
	timeout := 15 * time.Second
	d := net.Dialer{Timeout: timeout}
	cl := &http.Client{Transport: &http.Transport{
		TLSHandshakeTimeout: timeout,
		// Set DisableKeepAlives to true to force connection close after each request.
		// Alternatively, we can call client.CloseIdleConnections() manually.
		// DisableKeepAlives:     true,
		DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) {
			conn, err := d.DialContext(ctx, network, addr)
			if err != nil {
				return nil, err
			}
			return conniver.WrapConn(conn, func(c *conniver.Conn, state int) {
				if state != conniver.Closed {
					return
				}
				jb, _ := json.Marshal(c)
				fmt.Println("[" + conniver.StateMap[state] + "] " + string(jb) + "\n\n")
			}), err
		},
	}}
	resp, err := cl.Get("https://www.golang.org/")
	if err != nil {
		logrus.Fatalf("get: %v", err)
	}
	_ = resp.Body.Close()

	// Use client.CloseIdleConnections() to trigger the closed events for all wrapped connections.
	// Alteratively use `DisableKeepAlives: true`` in the HTTP transport.
	cl.CloseIdleConnections()

	return
}

Operating Systems

The current code supports detailed TCPINFO collection for Linux, macOS, and Windows.

Support for FreeBSD is planned.

Examples

The conniver.Conn struct includes basic socket details in addition to TCPInfo fields.

type Conn struct {
	net.Conn                   // The wrapped net.Conn
	Context    context.Context // The optional context
	OpenedAt   int64           // The opened time in unix nanoseconds
	ClosedAt   int64           // The closed time in unix nanoseconds
	FirstRxAt  int64           // The first successful read time in unix nanoseconds
	FirstTxAt  int64           // The first successful write time in unix nanoseconds
	LastRxAt   int64           // The last successful read time in unix nanoseconds
	LastTxAt   int64           // The last successful write time in unix nanoseconds
	TxBytes    int64           // The number of bytes sent successfully
	RxBytes    int64           // The number of bytes read successfully
	RxErr      error           // The last receive error, if any
	TxErr      error           // The last send error, if any
	InfoErr    error           // The last send error, if any
	Reconnects int             // The number of retries to connect (managed by the caller)
	OpenedInfo *tcpinfo.Info   // An OS-agnostic set of TCP information fields at open time
	ClosedInfo *tcpinfo.Info   // An OS-agnostic set of TCP information fields at close timeß
}

The tcpinfo.Info structure contains OS-normalized fields AND the entire platform-specific TCPINFO structure.

type Info struct {
	State         string        // Connection state
	TxOptions     []Option      // Requesting options
	RxOptions     []Option      // Options requested from peer
	TxMSS         uint64        // Maximum segment size for sender in bytes
	RxMSS         uint64        // Maximum segment size for receiver in bytes
	RTT           time.Duration // Round-trip time in nanoseconds
	RTTVar        time.Duration // Round-trip time variation in nanoseconds
	RTO           time.Duration // Retransmission timeout
	ATO           time.Duration // Delayed acknowledgement timeout [Linux only]
	LastTxAt      time.Duration // Nanoseconds since last data sent [Linux only]
	LastRxAt      time.Duration // Nanoseconds since last data received [FreeBSD and Linux]
	LastTxAckAt   time.Duration // Nanoseconds since last ack sent [Linux only]
	LastRxAckAt   time.Duration // Nanoseconds since last ack received [Linux only]
	RxWindow      uint64        // Advertised receiver window in bytes
	TxSSThreshold uint64        // Slow start threshold for sender in bytes or # of segments
	RxSSThreshold uint64        // Slow start threshold for receiver in bytes [Linux only]
	TxWindowBytes uint64        // Congestion window for sender in bytes [Darwin and FreeBSD]
	TxWindowSegs  uint64        // Congestion window for sender in # of segments [Linux and NetBSD]
	Retransmits   uint64        // Number of retransmissions (segments or packets)
	Sys           *SysInfo      // Platform-specific information
}

The *SysInfo fields vary dramatically by operating system and require OS build tags to use directly. The conniver.Conn, tcpinfoInfo, and SysInfo structs all support a ToMap() function, which returns a map[string]any that can be used to access OS-specific fields dynamically.

The function passed to conniver.WrapConn is called for both the opened and closed states. The opened callback fires right after the connection is established. The closed callback fires right before the connection is closed. Separate *tcpinfo.Info{} stats are recorded for both states.

The following reporting function will report the RTT at connection open and just before close, by catching the closed event and reviewing both fields.

func(c *conniver.Conn, state int) {
    if state != conniver.Closed {
        return
    }
    raw, _ := json.Marshal(c)
	fmt.Printf("Connection %s -> %s took %s, sent:%d/recv:%d bytes, starting RTT %s(%s) and ending RTT %s(%s)\nWarnings:%s\n%s\n\n",
        c.LocalAddr().String(), c.RemoteAddr().String(),
        time.Duration(c.ClosedAt-c.OpenedAt),
        c.SentBytes, c.RecvBytes,
        c.OpenedInfo.RTT, c.OpenedInfo.RTTVar,
        c.ClosedInfo.RTT, c.ClosedInfo.RTTVar,
		strings.Join(c.Warnings(), ","),
        string(raw),
    )
})
$ go run main.go

Connection 192.168.10.23:60032 -> 216.239.36.21:443 took 273.869ms, sent:1725/recv:5897 bytes, starting RTT 6ms(3ms) and ending RTT 6ms(1ms)

{"openedAt":1767404790007006000,"closedAt":1767404790280875000,"firstReadAt":1767404790023466000,"firstWriteAt":1767404790007418000,"sentBytes":1725,"recvBytes":5897,"openedInfo":{"state":"ESTABLISHED","options":["Timestamps","SACK","WindowScale:08"],"peerOptions":["Timestamps","SACK","WindowScale:06"],"sendMSS":1400,"recvMSS":1400,"rtt":6000000,"rttVar":3000000,"recvWindow":131648,"sendSSThreshold":1073725440,"sendCWindowdBytes":14000,"sendCWindowSegs":65535,"sysInfo":{"state":"ESTABLISHED","sendWScale":8,"recvWScale":6,"options":["Timestamps","SACK","WindowScale:08"],"peerOptions":["Timestamps","SACK","WindowScale:06"],"mss":1400,"sendSSThreshold":1073725440,"sendCWindowBytes":14000,"sendWnd":65535,"recvWnd":131648,"rttCur":6000000,"rttSmoothed":6000000,"rttVar":3000000}},"closedInfo":{"state":"ESTABLISHED","options":["Timestamps","SACK","WindowScale:08"],"peerOptions":["Timestamps","SACK","WindowScale:06"],"sendMSS":1400,"recvMSS":1400,"rtt":6000000,"rttVar":1000000,"rto":230000000,"recvWindow":125504,"sendSSThreshold":1073725440,"sendCWindowdBytes":15701,"sendCWindowSegs":267520,"sysInfo":{"state":"ESTABLISHED","sendWScale":8,"recvWScale":6,"options":["Timestamps","SACK","WindowScale:08"],"peerOptions":["Timestamps","SACK","WindowScale:06"],"rto":230000000,"mss":1400,"sendSSThreshold":1073725440,"sendCWindowBytes":15701,"sendWnd":267520,"sendSBBytes":24,"recvWnd":125504,"rttCur":252000000,"rttSmoothed":6000000,"rttVar":1000000,"txPackets":5,"txBytes":1725,"rxPackets":3,"rxBytes":11497}}}

Connection 192.168.10.23:60031 -> 142.251.116.141:443 took 329.892ms, sent:1707/recv:11868 bytes, starting RTT 6ms(3ms) and ending RTT 6ms(2ms)

{"openedAt":1767404789950983000,"closedAt":1767404790280875000,"firstReadAt":1767404789958865000,"firstWriteAt":1767404789951608000,"sentBytes":1707,"recvBytes":11868,"openedInfo":{"state":"ESTABLISHED","options":["Timestamps","SACK","WindowScale:08"],"peerOptions":["Timestamps","SACK","WindowScale:06"],"sendMSS":1400,"recvMSS":1400,"rtt":6000000,"rttVar":3000000,"recvWindow":131648,"sendSSThreshold":1073725440,"sendCWindowdBytes":14000,"sendCWindowSegs":65535,"sysInfo":{"state":"ESTABLISHED","sendWScale":8,"recvWScale":6,"options":["Timestamps","SACK","WindowScale:08"],"peerOptions":["Timestamps","SACK","WindowScale:06"],"mss":1400,"sendSSThreshold":1073725440,"sendCWindowBytes":14000,"sendWnd":65535,"recvWnd":131648,"rttCur":6000000,"rttSmoothed":6000000,"rttVar":3000000}},"closedInfo":{"state":"ESTABLISHED","options":["Timestamps","SACK","WindowScale:08"],"peerOptions":["Timestamps","SACK","WindowScale:06"],"sendMSS":1400,"recvMSS":1400,"rtt":6000000,"rttVar":2000000,"rto":230000000,"recvWindow":131072,"sendSSThreshold":1073725440,"sendCWindowdBytes":15683,"sendCWindowSegs":267520,"sysInfo":{"state":"ESTABLISHED","sendWScale":8,"recvWScale":6,"options":["Timestamps","SACK","WindowScale:08"],"peerOptions":["Timestamps","SACK","WindowScale:06"],"rto":230000000,"mss":1400,"sendSSThreshold":1073725440,"sendCWindowBytes":15683,"sendWnd":267520,"sendSBBytes":24,"recvWnd":131072,"rttCur":28000000,"rttSmoothed":6000000,"rttVar":2000000,"txPackets":5,"txBytes":1707,"rxPackets":3,"rxBytes":11868}}}

History

The tcpinfo package was bootstrapped from the following sources:

The httpstat command was derived from httpstat (MIT License).

About

A Go net.Conn wrapper that tracks detailed socket statistics (including TCPINFO).

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 6