Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 57 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
10 changes: 8 additions & 2 deletions dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down
29 changes: 20 additions & 9 deletions packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion protect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
104 changes: 73 additions & 31 deletions pseudotcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,66 +17,87 @@ 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 (
WAKEUP_PACKET_IP_PORT string = "10.10.10.10:1234" // Invalid destination for the wakeup packet.
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
Expand Down Expand Up @@ -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

Expand All @@ -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")

Expand All @@ -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
}
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand Down