Documentation ¶
Overview ¶
Package lampshade provides a transport between Lantern clients and proxies that provides obfuscated encryption as well as multiplexing. The protocol attempts to be indistinguishable in content and timing from a random stream of bytes, and mostly follows the OBFS4 threat model - https://github.com/Yawning/obfs4/blob/master/doc/obfs4-spec.txt#L35
Lampshade attempts to minimize overhead, so it uses less padding than OBFS4. Also, to avoid having to pad at all, lampshade coalesces consecutive small writes into single larger messages when there are multiple pending writes. Due to lampshade being multiplexed, especially during periods of high activity, coalescing is often possible.
Definitions:
physical connection - an underlying (e.g. TCP) connection stream - a virtual connection multiplexed over a physical connection session - unit for managing multiplexed streams, corresponds 1 to 1 with a physical connection
Protocol:
Session initiation client --> client init --> server Write client --> frame --> server client --> frame --> server client --> frame --> server ... continue up to transmit window client <-- ack <-- server client --> frame --> server client <-- ack <-- server client <-- ack <-- server client --> frame --> server client --> frame --> server ... etc ... Read (parallel to write) client <-- frame <-- server client <-- frame <-- server client <-- frame <-- server ... continue up to transmit window client --> ack --> server client <-- frame <-- server client --> ack --> server client --> ack --> server client <-- frame <-- server client <-- frame <-- server ... etc ...
General Protocol Features
protocol attempts to be indistinguishable in content and timing from a random stream of bytes, following the OBFS4 threat model - https://github.com/Yawning/obfs4/blob/master/doc/obfs4-spec.txt#L35
all numeric fields are unsigned integers in BigEndian format
Client Init Message
256 bytes, always combined with first data message to vary size. To initialize a session, the client sends the below, encrypted using RSA OAEP using the server's PK: +-----+---------+--------+--------+----------+----------+----------+----------+----+ | Win | Max Pad | Cipher | Secret | Send IV1 | Send IV2 | Recv IV1 | Recv IV2 | TS | +-----+---------+--------+--------+----------+----------+----------+----------+----+ | 4 | 1 | 1 | 32 | 12 | 12 | 12 | 12 | 8 | +-----+---------+--------+--------+----------+----------+----------+----------+----+ Win - transmit window size in # of frames Max Pad - maximum random padding Cipher - specifies the AEAD cipher used for encrypting frames 1 = None 2 = AES128_GCM 3 = ChaCha20_poly1305 Secret - 256 bits of secret (used for Len and Frames encryption) Send IV1/2 - 96 bits of initialization vector. IV1 is used for encrypting the frame length and IV2 is used for encrypting the data. Recv IV1/2 - 96 bits of initialization vector. IV1 is used for decrypting the frame length and IV2 is used for decrypting the data. TS - Optional, this is the timestamp of the client init message in seconds since epoch.
Session Framing:
Where possible, lampshade coalesces multiple stream-level frames into a single session-level frame on the wire. The session-level frames follow the below format. Len is encrypted using ChaCha20. Frames is encrypted using the configured AEAD and the resulting MAC is stored in MAC. +-----+---------+------+ | Len | Frames | MAC | +-----+---------+------+ | 2 | <=65518 | 16 | +-----+---------+------+ Len - the length of the frame, not including the Len field itself. This is encrypted using ChaCha20 to obscure the actual value. Frames - the data of the app frames. Padding appears at the end of this. MAC - the MAC resulting from applying the AEAD to Frames.
Encryption:
The Len field is encrypted using ChaCha20 as a stream cipher initialized with a session-level initialization vector for obfuscation purposes. The Frames field is encrypted using AEAD (either AES128_GCM or ChaCha20_Poly1305) in order to prevent chosen ciphertext attacks. The nonce for each message is derived from a session-level initialization vector XOR'ed with a frame sequence number, similar AES128_GCM in TLS 1.3 (see https://blog.cloudflare.com/tls-nonce-nse/).
Padding:
- used only when there weren't enough pending writes to coalesce
- size varies randomly based on max pad parameter in init message
- consists of empty data
- the "empty" data actually looks random on the wire since it's being encrypted with a cipher in streaming or GCM mode.
Stream Framing:
Stream frames follow the below format: +------------+-----------+----------+--------+ | | | Data Len | | | | | / Frames | Data | | Frame Type | Stream ID | / TS | | +------------+-----------+----------+--------+ | 1 | 2 | 2/4/8 | <=1443 | +------------+-----------+----------+--------+ Frame Type - indicates the message type. 0 = padding 1 = data 252 = ping 253 = echo 254 = ack 255 = rst (close connection) Stream ID - unique identifier for stream. (last field for ack and rst) Data Len - length of data (for type "data" or "padding") Frames - number of frames being ACK'd (for type ACK) Data - data (for type "data" or "padding") TS - time at which ping packet was sent as 64-bit uint. This is a passthrough value, so the client implementation can put whatever it wants in here in order to calculate its RTT. (for type "ping" and "echo")
Flow Control:
Stream-level flow control is managed using windows similarly to HTTP/2. - windows are sized based on # of frames rather than # of bytes - both ends of a stream maintain a transmit window - the window is initialized based on the win parameter in the client init message - as the sender transmits data, its transmit window decreases by the number of frames sent (not including headers) - if the sender's transmit window reaches 0, it stalls - as the receiver's buffers free up, it sends ACKs to the sender that instruct it to increase its transmit window by a given amount - blocked senders become unblocked when their transmit window exceeds 0 again - if the client requests a window larger than what the server is willing to buffer, the server can adjust the window by sending an ACK with a negative value
Ping Protocol:
Dialers can optionally be configured to use an embedded ping/echo protocol to maintain an exponential moving average round trip time (RTT). The ping protocol is similar to an ICMP ping. The client sends a ping packet containing a 64-bit unsigned integer timestamp and the server responds with an echo containing that same timestamp. For blocking resistance and efficiency, pings are only sent with other outgoing frames. If there's no outgoing traffic, no pings will be sent.
Index ¶
Examples ¶
Constants ¶
const ( // NoEncryption is no encryption NoEncryption = 1 // AES128GCM is 128-bit AES in GCM mode AES128GCM = 2 // ChaCha20Poly1305 is 256-bit ChaCha20Poly1305 with a 96-bit Nonce ChaCha20Poly1305 = 3 // MaxDataLen is the maximum length of data in a lampshade frame. MaxDataLen = maxFrameSize - dataHeaderSize )
Variables ¶
var ( // ErrTimeout indicates that an i/o operation timed out. ErrTimeout = &netError{"i/o timeout", true, true} // ErrConnectionClosed indicates that an i/o operation was attempted on a // closed stream. ErrConnectionClosed = &netError{"connection closed", false, false} // ErrListenerClosed indicates that an Accept was attempted on a closed // listener. ErrListenerClosed = &netError{"listener closed", false, false} )
var (
ReadTimeout = 15 * time.Second
)
Functions ¶
func WrapListener ¶
func WrapListener(wrapped net.Listener, pool BufferPool, serverPrivateKey *rsa.PrivateKey, opts *ListenerOpts) net.Listener
WrapListener wraps the given listener with support for multiplexing. Only connections that start with the special session start sequence will be multiplexed, otherwise connections behave as normal. This means that a single listener can be used to serve clients that do multiplexing as well as other clients that don't.
Multiplexed sessions can only be initiated immediately after opening a connection to the Listener.
wrapped - the net.Listener to wrap
pool - BufferPool to use
serverPrivateKey - RSA key to decrypt client init messages
opts - Options configuring the listener
Example ¶
pk, err := keyman.GeneratePK(2048) if err != nil { return } l, err := net.Listen("tcp", ":9352") if err != nil { return } ll := WrapListener(l, NewBufferPool(100), pk.RSA(), &ListenerOpts{ AckOnFirst: true, }) for { conn, err := ll.Accept() if err != nil { // handle error } go handleConn(conn) }
Output:
Types ¶
type BoundDialer ¶
type BoundDialer interface { StatsTracking // Dial creates a virtual connection to the lampshade server. Dial() (net.Conn, error) // DialContext is the same as Dial but with the specific context. DialContext(ctx context.Context) (net.Conn, error) }
BoundDialer is a Dialer bound to a specific DialFN for connecting to the lampshade server.
type BufferPool ¶
type BufferPool interface { // Get gets a truncated buffer sized to hold the data portion of a lampshade // frame Get() []byte // Put returns a buffer back to the pool, indicating that it is safe to // reuse. Put([]byte) // contains filtered or unexported methods }
BufferPool is a pool of reusable buffers
func NewBufferPool ¶
func NewBufferPool(maxBytes int) BufferPool
NewBufferPool constructs a BufferPool with the given maximum size in bytes
type Dialer ¶
type Dialer interface { StatsTracking // Dial creates a virtual connection to the lampshade server, using the given // DialFN to open a physical connection when necessary. Dial(dial DialFN) (net.Conn, error) // DialContext is the same as Dial but with the specific context. DialContext(ctx context.Context, dial DialFN) (net.Conn, error) // BoundTo returns a BoundDialer that uses the given DialFN to connect to the // lampshade server. BoundTo(dial DialFN) BoundDialer }
Dialer provides an interface for opening new lampshade connections.
func NewDialer ¶
func NewDialer(opts *DialerOpts) Dialer
NewDialer wraps the given dial function with support for lampshade. The returned Streams look and act just like regular net.Conns. The Dialer will multiplex everything over a single net.Conn until it encounters a read or write error on that Conn. At that point, it will dial a new conn for future streams, until there's a problem with that Conn, and so on and so forth.
If a new physical connection is needed but can't be established, the dialer returns the underlying dial error.
Example ¶
publicKey := loadPublicKey() dialer := NewDialer(&DialerOpts{ Pool: NewBufferPool(100), Cipher: AES128GCM, ServerPublicKey: &publicKey, }).BoundTo(func() (net.Conn, error) { return net.Dial("tcp", "myserver:9352") }) // Get a connection to the server dialer.Dial()
Output:
type DialerOpts ¶
type DialerOpts struct { // WindowSize - transmit window size in # of frames. If <= 0, defaults to 1250. WindowSize int // MaxPadding - maximum random padding to use when necessary. MaxPadding int // MaxLiveConns - limits the number of live physical connections on which // new streams can be created. If <=0, defaults to 1. MaxLiveConns int // MaxStreamsPerConn - limits the number of streams per physical connection. // If <=0, defaults to max uint16. MaxStreamsPerConn uint16 // IdleInterval - If we haven't dialed any new connections within this // interval, open a new physical connection on the next dial. IdleInterval time.Duration // PingInterval - how frequently to ping to calculate RTT, set to 0 to disable PingInterval time.Duration // RedialSessionInterval - how frequently to redial a new session when // there's no live session, for faster recovery after network failures. // Defaults to 5 seconds. // See https://github.com/getlantern/lantern-internal/issues/2534 RedialSessionInterval time.Duration // Pool - BufferPool to use (required) Pool BufferPool // Cipher - which cipher to use, 1 = AES128 in CTR mode, 2 = ChaCha20 Cipher Cipher // ServerPublicKey - if provided, this dialer will use encryption. ServerPublicKey *rsa.PublicKey }
DialerOpts configures options for creating Dialers
type ListenerOpts ¶
type ListenerOpts struct { // AckOnFirst forces an immediate ACK after receiving the first frame, which could help defeat timing attacks AckOnFirst bool // InitMsgTimeout controls how long the listener will wait before responding to bad client init // messages. This applies in 3 situations: // 1. The client has sent some, but not all of the init message. This situation is salvagable // if the client sends the remainder of the init message. // 2. The client sends an init message of the proper length, but we fail to decode it. // 3. The client sends an init message that is too long. // // It is important that each situation be indistinguishable to a client. This is to avoid // leaking information to probes (from bad actors) that we have a fixed-size init message. // It is also important for this to be a really long time, as most servers in // the wild which fail to respond to an unknown protocol from the client will // keep the connection open indefinitely. Our approach is to always close the // connection after the init message timeout has elapsed. // // The default value is forever InitMsgTimeout time.Duration // KeyCacheSize enables and sizes a cache of previously seen client keys to // protect against replay attacks. KeyCacheSize int // MaxClientInit age limits the maximum allowed age of client init messages if // and only if the init message includes a timestamp field. This helps protect // against replay attacks. MaxClientInitAge time.Duration // Optional callback for errors that arise when accepting connectinos OnError func(net.Conn, error) }
ListenerOpts provides options for configuring a Listener
type Session ¶
type Session interface { net.Conn // Wrapped() exposes access to the net.Conn that's wrapped by this Session. Wrapped() net.Conn }
Session is a wrapper around a net.Conn that supports multiplexing.
type StatsTracking ¶
type StatsTracking interface { // EMARTT() gets the estimated moving average RTT for all streams created by // this Dialer. EMARTT() time.Duration }
StatsTracking is an interface for anything that tracks stats.
type Stream ¶
type Stream interface { net.Conn // Session() exposes access to the Session on which this Stream is running. Session() Session // Wrapped() exposes the wrapped connection (same thing as Session(), but // implements netx.WrappedConn interface) Wrapped() net.Conn }
Stream is a net.Conn that also exposes access to the underlying Session