From 60cf835b1018f5a97a8e05febfd5a534334d15ae Mon Sep 17 00:00:00 2001 From: Maxb Date: Fri, 28 Mar 2025 11:52:29 -0700 Subject: [PATCH 1/2] Add comprehensive godoc comments to pseudotcp package public fields and methods --- dns.go | 10 ++++- packet.go | 29 +++++++++----- protect.go | 4 +- pseudotcp.go | 104 ++++++++++++++++++++++++++++++++++++--------------- 4 files changed, 104 insertions(+), 43 deletions(-) diff --git a/dns.go b/dns.go index f63ac60..60245b2 100644 --- a/dns.go +++ b/dns.go @@ -16,11 +16,17 @@ import ( "golang.org/x/net/http2" ) +// DNSResponse represents the structure of a DNS-over-HTTPS response in JSON format. type DNSResponse struct { + // Answer contains the DNS records returned in the response Answer []struct { + // Name is the domain name for this record Name string `json:"name"` - Type int `json:"type"` - TTL int `json:"ttl"` + // Type is the DNS record type (1 for A, 28 for AAAA, etc.) + Type int `json:"type"` + // TTL is the time-to-live for this record in seconds + TTL int `json:"ttl"` + // Data is the record data (IP address for A/AAAA records, etc.) Data string `json:"data"` } `json:"Answer"` } diff --git a/packet.go b/packet.go index a98d877..a5901df 100644 --- a/packet.go +++ b/packet.go @@ -4,9 +4,12 @@ import "net" // IP protocol numbers for L4 headers. const ( + // PROTO_ICMP is the IP protocol number for ICMP PROTO_ICMP byte = 1 - PROTO_TCP byte = 6 - PROTO_UDP byte = 17 + // PROTO_TCP is the IP protocol number for TCP + PROTO_TCP byte = 6 + // PROTO_UDP is the IP protocol number for UDP + PROTO_UDP byte = 17 ) // Byte offsets for IPv4 header fields. @@ -61,14 +64,22 @@ const ( // Header size constants. const ( - DEFAULT_IP_HDR_SIZE int = 20 - DEFAULT_TCP_HDR_SIZE int = 20 - DEFAULT_UDP_HDR_SIZE int = 8 + // DEFAULT_IP_HDR_SIZE is the size of a standard IPv4 header + DEFAULT_IP_HDR_SIZE int = 20 + // DEFAULT_TCP_HDR_SIZE is the size of a standard TCP header + DEFAULT_TCP_HDR_SIZE int = 20 + // DEFAULT_UDP_HDR_SIZE is the size of a standard UDP header + DEFAULT_UDP_HDR_SIZE int = 8 + // DEFAULT_ICMP_UNREACHABLE_HDR_SIZE is the size of an ICMP unreachable message header DEFAULT_ICMP_UNREACHABLE_HDR_SIZE int = 8 - SYNACK_SIZE = DEFAULT_IP_HDR_SIZE + DEFAULT_TCP_HDR_SIZE + 4 - RST_SIZE = DEFAULT_IP_HDR_SIZE + DEFAULT_TCP_HDR_SIZE - ACK_SIZE = DEFAULT_IP_HDR_SIZE + DEFAULT_TCP_HDR_SIZE - ICMP_UNREACHABLE_SIZE = DEFAULT_IP_HDR_SIZE + DEFAULT_ICMP_UNREACHABLE_HDR_SIZE + (DEFAULT_IP_HDR_SIZE + 8) + // SYNACK_SIZE is the total size of a SYN-ACK packet + SYNACK_SIZE = DEFAULT_IP_HDR_SIZE + DEFAULT_TCP_HDR_SIZE + 4 + // RST_SIZE is the total size of a RST packet + RST_SIZE = DEFAULT_IP_HDR_SIZE + DEFAULT_TCP_HDR_SIZE + // ACK_SIZE is the total size of an ACK packet + ACK_SIZE = DEFAULT_IP_HDR_SIZE + DEFAULT_TCP_HDR_SIZE + // ICMP_UNREACHABLE_SIZE is the total size of an ICMP unreachable message + ICMP_UNREACHABLE_SIZE = DEFAULT_IP_HDR_SIZE + DEFAULT_ICMP_UNREACHABLE_HDR_SIZE + (DEFAULT_IP_HDR_SIZE + 8) ) // Pre-baked packets that have common fields set and the rest of the headers set to defaults or zero. diff --git a/protect.go b/protect.go index 62ce59f..a25e707 100644 --- a/protect.go +++ b/protect.go @@ -5,7 +5,9 @@ import ( "syscall" ) -// Configure sets up a socket protect function to be usable as currentProtect +// ConfigureProtect sets up a socket protect function to be used by PseudoTCP. +// This function is used to prevent VPN traffic from being routed through the VPN itself, +// which would create a loop. On Android, this typically calls VpnService.protect(). func (t *PseudoTCP) ConfigureProtect(protect SocketProtector) { t.currentProtect = protect } diff --git a/pseudotcp.go b/pseudotcp.go index 80eb487..543b3eb 100644 --- a/pseudotcp.go +++ b/pseudotcp.go @@ -17,59 +17,78 @@ import ( "github.com/google/gopacket/layers" ) +// ProxyClient defines the interface for a client that can connect to a proxy server +// and create TCP and UDP streams through that proxy. type ProxyClient interface { + // Connect establishes a connection to the proxy server. Connect() error + + // CurrentProxyIP returns the IP address of the currently connected proxy. CurrentProxyIP() string + + // CreateTCPStream creates a TCP stream to the specified address through the proxy. CreateTCPStream(string) (io.ReadWriteCloser, error) + + // CreateUDPStream creates a UDP stream to the specified address through the proxy. CreateUDPStream(string) (io.ReadWriteCloser, error) + + // Close terminates the connection to the proxy server. Close() error } // UDPFlow tracks the state of a MASQUE connection to a _destination IP/port_ (different from TCP). type UDPFlow struct { - src uint32 - sport uint16 - dst uint32 - dport uint16 - garbageCollect bool - proxyConn io.ReadWriteCloser + src uint32 // Source IP address + sport uint16 // Source port + dst uint32 // Destination IP address + dport uint16 // Destination port + garbageCollect bool // Flag indicating if this flow should be garbage collected + proxyConn io.ReadWriteCloser // Connection to the proxy for this flow } // UDPFlowKey keys the activeUDPFlows map with the client (src) IP/port and server (dst) IP/port. type UDPFlowKey struct { - src uint32 - sport uint16 - dst uint32 - dport uint16 + src uint32 // Source IP address + sport uint16 // Source port + dst uint32 // Destination IP address + dport uint16 // Destination port } // TCPFlow tracks the state of a virtual TCP connection between Android and the MASQUE proxy. type TCPFlow struct { - src uint32 - sport uint16 - dst uint32 - dport uint16 - seq uint32 - ack uint32 - rwin int32 - rwinScale uint8 - garbageCollect bool - proxyConn io.ReadWriteCloser + src uint32 // Source IP address + sport uint16 // Source port + dst uint32 // Destination IP address + dport uint16 // Destination port + seq uint32 // Current sequence number + ack uint32 // Current acknowledgment number + rwin int32 // Receive window size + rwinScale uint8 // Window scale factor (RFC 1323) + garbageCollect bool // Flag indicating if this flow should be garbage collected + proxyConn io.ReadWriteCloser // Connection to the proxy for this flow } -// Protect overrides Android's VpnService.protect() -// Arguments: -// fileDescriptor is a system file descriptor to protect from the VPN +// SocketProtector is a function type that overrides Android's VpnService.protect() +// It takes a system file descriptor and protects it from being routed through the VPN. +// This is necessary to prevent loops when the VPN itself needs to make network connections. type SocketProtector func(fileDescriptor int) error +// SendPacket is a function type for sending packets to the TUN interface. +// It takes a packet buffer and its length, and returns an error if the send fails. type SendPacket func(packet []byte, length int) error const ( - // MTU of the TUN interface we're using, has to match Android. - TUN_MTU = 32000 - INTERNET_MTU = 1500 + // TUN_MTU is the Maximum Transmission Unit of the TUN interface we're using, has to match Android. + TUN_MTU = 32000 + + // INTERNET_MTU is the standard Maximum Transmission Unit for Internet connections. + INTERNET_MTU = 1500 + + // DNS_CACHE_TIMEOUT_SECONDS is the time in seconds that DNS responses are cached. DNS_CACHE_TIMEOUT_SECONDS = 300 - DEFAULT_PROXY_PORT = "443" + + // DEFAULT_PROXY_PORT is the default port used for proxy connections. + DEFAULT_PROXY_PORT = "443" ) const ( @@ -77,6 +96,8 @@ const ( WAKEUP_PACKET_IP_VALUE byte = 10 ) +// PseudoTCP is the main structure that handles the interposition between +// packet-based VPN interfaces and stream-based proxy connections. type PseudoTCP struct { // proxyClient is generally a masque proxy client though it can be any struct satisfying the ProxyClient interface, // ie that it can create TCP and UDP streams to specified endpoints @@ -130,16 +151,27 @@ type PseudoTCP struct { activeDoHServers []string } +// PseudoTCPConfig contains the configuration options for creating a new PseudoTCP instance. type PseudoTCPConfig struct { + // Logger is the structured logger to use for logging messages. + // If nil, logging will be discarded. Logger *slog.Logger + // ProxyClient is the client that will be used to connect to the proxy server. + // This must implement the ProxyClient interface. ProxyClient ProxyClient + // SendPacket is the function that will be called to send packets to the TUN interface. SendPacket SendPacket + // ProhibitDisallowedIPPorts determines whether to block packets to disallowed IP/port combinations. + // When true, packets to private IP ranges will be blocked with ICMP host unreachable. ProhibitDisallowedIPPorts bool } +// NewPseudoTCP creates a new PseudoTCP instance with the provided configuration. +// It initializes internal data structures but does not start processing packets. +// Call Init() after creating a PseudoTCP instance to start processing. func NewPseudoTCP(config *PseudoTCPConfig) *PseudoTCP { // TODO: Either sane defaults or return an error here @@ -165,6 +197,8 @@ func NewPseudoTCP(config *PseudoTCPConfig) *PseudoTCP { } // Init initializes the userspace network stack module. This must be called before any other UserStack function. +// It sets up internal data structures, connects to the proxy, and initializes the DNS client. +// Returns an error if initialization fails. func (t *PseudoTCP) Init() error { t.logger.Debug("Initializing") @@ -191,10 +225,14 @@ func (t *PseudoTCP) Init() error { return nil } +// Send processes an IP packet from the TUN interface. +// This is the main entry point for packets coming from the device. func (t *PseudoTCP) Send(packetData []byte) { t.UserStackIPProcessPacket(packetData) } +// SetLogger updates the logger used by PseudoTCP. +// This can be used to change logging behavior after initialization. func (t *PseudoTCP) SetLogger(l *slog.Logger) { t.logger = l } @@ -443,8 +481,8 @@ func (t *PseudoTCP) setupUDPMasque(dstIP net.IP, flow *UDPFlow) error { return nil } -// CurrentProxyIP returns the IP of the Proxy A server we are connected to. -// If relay is not active, returns empty string. +// CurrentProxyIP returns the IP of the proxy server we are connected to. +// If PseudoTCP is not active, returns empty string. func (t *PseudoTCP) CurrentProxyIP() string { if t.active { return t.proxyClient.CurrentProxyIP() @@ -463,8 +501,10 @@ func (t *PseudoTCP) initWakeupUDPConn() error { return nil } -// ReconnectToProxy indicates to the Relay code that it should try to connect to the proxy again. -// This is good to call when Android detects that a network interface has come back up. +// ReconnectToProxy attempts to reconnect to the proxy server. +// This is useful when the network connection changes, such as when Android detects +// that a network interface has come back up. +// It resets all active flows and establishes a new connection to the proxy. func (t *PseudoTCP) ReconnectToProxy() error { t.active = false t.initActiveFlows() @@ -482,6 +522,8 @@ func (t *PseudoTCP) ReconnectToProxy() error { return nil } +// Shutdown gracefully terminates PseudoTCP operation. +// It closes all active flows and disconnects from the proxy. func (t *PseudoTCP) Shutdown() { t.active = false t.terminateActiveFlows() From 08021ec7e9ac8cd7f636b475e1c082982515df55 Mon Sep 17 00:00:00 2001 From: Maxb Date: Fri, 28 Mar 2025 12:12:41 -0700 Subject: [PATCH 2/2] Improve README --- README.md | 73 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 0eab274..dff3095 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,72 @@ -# PseudoTCP: A lightweight partial TCP stack for packet to stream interposition in Go -[![Lint and Test](https://github.com/invisv-privacy/pseudotcp/actions/workflows/build.yaml/badge.svg)](https://github.com/Invisv-Privacy/pseudotcp/actions/workflows/build.yaml)[![GoDoc](https://pkg.go.dev/badge/github.com/invisv-privacy/pseudotcp?status.svg)](https://pkg.go.dev/github.com/invisv-privacy/pseudotcp) -## What is PseudoTCP? +# PseudoTCP + +[![Lint and Test](https://github.com/invisv-privacy/pseudotcp/actions/workflows/build.yaml/badge.svg)](https://github.com/Invisv-Privacy/pseudotcp/actions/workflows/build.yaml) +[![GoDoc](https://pkg.go.dev/badge/github.com/invisv-privacy/pseudotcp?status.svg)](https://pkg.go.dev/github.com/invisv-privacy/pseudotcp) +[![Go Report Card](https://goreportcard.com/badge/github.com/invisv-privacy/pseudotcp)](https://goreportcard.com/report/github.com/invisv-privacy/pseudotcp) + +A lightweight partial TCP stack for packet to stream interposition in Go + +## ๐Ÿ“– What is PseudoTCP? Many modern tunneling protocols, including IETF MASQUE, operate at a higher level of abstraction, dealing with flows rather than individual packets. However, this mismatch between the Android VPN interface and flow-based protocols poses a significant challenge for deploying MASQUE on Android devices. -We are building this interposition stack we call PseudoTCP, which enables the use of unmodified applications and unmodified Android devices with a MASQUE-enabled Android VPN that uses PseudoTCP to transparently handle translation between packets and flows as needed. This will make it possible for ordinary users to use a MASQUE-based Android VPN application as they would any other VPN or circumvention tool, yet the traffic will be tunneled using our MASQUE stack as HTTPS traffic via the MASQUE-enabled infrastructure in use for that service. +PseudoTCP is an interposition stack that enables the use of unmodified applications and unmodified Android devices with a MASQUE-enabled Android VPN. It transparently handles translation between packets and flows as needed, making it possible for ordinary users to use a MASQUE-based Android VPN application as they would any other VPN or circumvention tool. The traffic is tunneled using our MASQUE stack as HTTPS traffic via the MASQUE-enabled infrastructure. -This will eventually integrate with our INVISV **masque** stack, which is an implementation of the [IETF MASQUE](https://datatracker.ietf.org/wg/masque/about/) tunneling protocol, written in Go. INVISV **masque** provides the client-side functionality needed for running a [Multi-Party Relay](https://invisv.com/articles/relay.html) service to protect users' network privacy. +This project integrates with our INVISV **masque** stack, which is an implementation of the [IETF MASQUE](https://datatracker.ietf.org/wg/masque/about/) tunneling protocol, written in Go. INVISV **masque** provides the client-side functionality needed for running a [Multi-Party Relay](https://invisv.com/articles/relay.html) service to protect users' network privacy. **masque** enables application code on the client to tunnel bytestream (TCP) and packet (UDP) traffic via a MASQUE-supporting proxy, such as the [MASQUE service operated by Fastly](https://www.fastly.com/blog/kicking-off-privacy-week-fastly). -## How do I use PseudoTCP? +## ๐Ÿš€ Getting Started + +### Prerequisites + +- Go 1.23 or higher +- Docker (for running integration tests) -PseudoTCP is intended to be used as part of an android VPN app. We have a [sample Android VPN app](https://github.com/Invisv-Privacy/pseudotcp-example-app) that uses this stack that can be referenced. +## ๐Ÿ”ง Usage -We also have an [example binary](./example/tun/README.md) that you can use in order to bind the pseudotcp stack to a TUN interface for some amount of demonstration/evaluation. +PseudoTCP is intended to be used as part of an Android VPN app. We provide: -## Testing -We currently have [integration tests](./tests/integration). They spin up a dockerized h2o proxy and leverage [gvisor's tcpip netstack](https://github.com/google/gvisor/tree/1a9abee80b7cb8655db7ba5714f0d3a8c00ccc67/pkg/tcpip) to emulate the android VPN host. +1. A [sample Android VPN app](https://github.com/Invisv-Privacy/pseudotcp-example-app) that demonstrates how to use this stack +2. An [example binary](./example/tun/README.md) that binds the pseudotcp stack to a TUN interface for demonstration and evaluation + +## ๐Ÿงช Testing + +We have comprehensive [integration tests](./tests/integration) that: +- Spin up a dockerized h2o proxy +- Leverage [gvisor's tcpip netstack](https://github.com/google/gvisor/tree/master/pkg/tcpip) to emulate the Android VPN host +- Assert that both [HTTPS GET requests](./tests/integration/https_get_test.go) as well as [UDP connections](./tests/integration/udp_test.go) are successful. To run the integration tests: + +```bash +$ go test -v ./tests/integration ``` -$ go test -v ./... -``` -## Benchmarking -We include a benchmark that evaluates the performance of a TCP connection over our stack with a HTTP GET request of various sizes. We can then compare those figures to a matching HTTP GET request directly from client to httptest. +### Linting + +We use golangci-lint for code quality checks. See [the install instructions](https://golangci-lint.run/welcome/install/) for comprehensive directions for your platform. +```bash +# Run linting +$ golangci-lint run ``` -$ go test -bench=. -run='^#' -benchtime=20x ./tests/integration > bench-results.txt + +## ๐Ÿ“Š Benchmarking + +We include [benchmarks](./tests/integration/benchmark_test.go) that evaluate the performance of TCP connections over our stack with HTTP GET requests of various sizes, comparing them to direct HTTP GET requests: + +```bash +# Run benchmarks +$ go test -bench=. -run='^#' -benchtime=20x ./tests/integration > bench-results.txt + +# Analyze results $ benchstat ./bench-results.txt +``` + +Sample benchmark results: + +``` goos: linux goarch: amd64 pkg: github.com/invisv-privacy/pseudotcp/tests/integration @@ -50,4 +87,8 @@ geomean 8.202m ยน need >= 6 samples for confidence interval at level 0.95 ``` -It's important to note that "with pseudotcp" vs "without pseudotcp" is an extremely unfavorable comparison as "with pseudotcp" includes the overhead of not only our pseudotcp stack, but also the MASQUE connection overhead as well as the h2o proxy container and associated docker networking traversal. +**Note:** "with pseudotcp" vs "without pseudotcp" is an unfavorable comparison as "with pseudotcp" includes the overhead of not only our pseudotcp stack, but also the MASQUE connection overhead as well as the h2o proxy container and associated docker networking traversal. + +## ๐Ÿ“„ License + +This project is licensed under the BSD 3-Clause License - see the [LICENSE](./LICENSE) file.