lampshade: github.com/getlantern/lampshade Index | Examples | Files

package lampshade

import "github.com/getlantern/lampshade"

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

Package Files

crypto.go dialer.go lampshade.go listener.go receive_buffer.go sendbuffer.go session.go stream.go window.go xor.go

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
)

func WrapListener Uses

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

Code:

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)
}

type BoundDialer Uses

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 Uses

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 Uses

func NewBufferPool(maxBytes int) BufferPool

NewBufferPool constructs a BufferPool with the given maximum size in bytes

type Cipher Uses

type Cipher byte

Cipher specifies a stream cipher

func (Cipher) String Uses

func (c Cipher) String() string

type DialFN Uses

type DialFN func() (net.Conn, error)

DialFN is a function that dials the server

type Dialer Uses

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 Uses

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.

Code:

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()

type DialerOpts Uses

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 Uses

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 Uses

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 Uses

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 Uses

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

Package lampshade imports 32 packages (graph) and is imported by 1 packages. Updated 2020-03-03. Refresh now. Tools for package owners.