mirror of https://gitlab.com/yawning/obfs4
Add the "meek_lite" transport, which does what one would expect.
This is a meek client only implementation, with the following differences with dcf's `meek-client`: - It is named `meek_lite` to differentiate it from the real thing. - It does not support using an external helper to normalize TLS signatures, so adversaries can look for someone using the Go TLS library to do HTTP. - It does the right thing with TOR_PT_PROXY, even when a helper is not present. Most of the credit goes to dcf, who's code I librerally cribbed and stole. It is intended primarily as a "better than nothina" option for enviornments that do not or can not presently use an external Firefox helper.merge-requests/3/head
parent
e52258edac
commit
611205be68
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
// Package meeklite provides an implementation of the Meek circumvention
|
||||
// protocol. Only a client implementation is provided, and no effort is
|
||||
// made to normalize the TLS fingerprint.
|
||||
//
|
||||
// It borrows quite liberally from the real meek-client code.
|
||||
package meeklite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"git.torproject.org/pluggable-transports/goptlib.git"
|
||||
"git.torproject.org/pluggable-transports/obfs4.git/transports/base"
|
||||
)
|
||||
|
||||
const transportName = "meek_lite"
|
||||
|
||||
// Transport is the Meek implementation of the base.Transport interface.
|
||||
type Transport struct{}
|
||||
|
||||
// Name returns the name of the Meek transport protocol.
|
||||
func (t *Transport) Name() string {
|
||||
return transportName
|
||||
}
|
||||
|
||||
// ClientFactory returns a new meekClientFactory instance.
|
||||
func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) {
|
||||
cf := &meekClientFactory{transport: t}
|
||||
return cf, nil
|
||||
}
|
||||
|
||||
// ServerFactory will one day return a new meekServerFactory instance.
|
||||
func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) {
|
||||
// TODO: Fill this in eventually, though for servers people should
|
||||
// just use the real thing.
|
||||
return nil, fmt.Errorf("server not supported")
|
||||
}
|
||||
|
||||
type meekClientFactory struct {
|
||||
transport base.Transport
|
||||
}
|
||||
|
||||
func (cf *meekClientFactory) Transport() base.Transport {
|
||||
return cf.transport
|
||||
}
|
||||
|
||||
func (cf *meekClientFactory) ParseArgs(args *pt.Args) (interface{}, error) {
|
||||
return newClientArgs(args)
|
||||
}
|
||||
|
||||
func (cf *meekClientFactory) Dial(network, addr string, dialFn base.DialFunc, args interface{}) (net.Conn, error) {
|
||||
// Validate args before opening outgoing connection.
|
||||
ca, ok := args.(*meekClientArgs)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid argument type for args")
|
||||
}
|
||||
|
||||
return newMeekConn(network, addr, dialFn, ca)
|
||||
}
|
||||
|
||||
var _ base.ClientFactory = (*meekClientFactory)(nil)
|
||||
var _ base.Transport = (*Transport)(nil)
|
@ -0,0 +1,358 @@
|
||||
/*
|
||||
* Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package meeklite
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
gourl "net/url"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.torproject.org/pluggable-transports/goptlib.git"
|
||||
"git.torproject.org/pluggable-transports/obfs4.git/transports/base"
|
||||
)
|
||||
|
||||
const (
|
||||
urlArg = "url"
|
||||
frontArg = "front"
|
||||
|
||||
maxChanBacklog = 16
|
||||
|
||||
// Constants shamelessly stolen from meek-client.go...
|
||||
maxPayloadLength = 0x10000
|
||||
initPollInterval = 100 * time.Millisecond
|
||||
maxPollInterval = 5 * time.Second
|
||||
pollIntervalMultiplier = 1.5
|
||||
maxRetries = 10
|
||||
retryDelay = 30 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotSupported is the error returned for a unsupported operation.
|
||||
ErrNotSupported = errors.New("meek_lite: operation not supported")
|
||||
|
||||
loopbackAddr = net.IPv4(127, 0, 0, 1)
|
||||
)
|
||||
|
||||
type meekClientArgs struct {
|
||||
url *gourl.URL
|
||||
front string
|
||||
}
|
||||
|
||||
func (ca *meekClientArgs) Network() string {
|
||||
return transportName
|
||||
}
|
||||
|
||||
func (ca *meekClientArgs) String() string {
|
||||
return transportName + ":" + ca.front + ":" + ca.url.String()
|
||||
}
|
||||
|
||||
func newClientArgs(args *pt.Args) (ca *meekClientArgs, err error) {
|
||||
ca = &meekClientArgs{}
|
||||
|
||||
// Parse the URL argument.
|
||||
str, ok := args.Get(urlArg)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing argument '%s'", urlArg)
|
||||
}
|
||||
ca.url, err = gourl.Parse(str)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed url: '%s'", str)
|
||||
}
|
||||
switch ca.url.Scheme {
|
||||
case "http", "https":
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid scheme: '%s'", ca.url.Scheme)
|
||||
}
|
||||
|
||||
// Parse the (optional) front argument.
|
||||
ca.front, _ = args.Get(frontArg)
|
||||
|
||||
return ca, nil
|
||||
}
|
||||
|
||||
type meekConn struct {
|
||||
sync.Mutex
|
||||
|
||||
args *meekClientArgs
|
||||
sessionID string
|
||||
transport *http.Transport
|
||||
|
||||
workerRunning bool
|
||||
workerWrChan chan []byte
|
||||
workerRdChan chan []byte
|
||||
workerCloseChan chan bool
|
||||
rdBuf *bytes.Buffer
|
||||
}
|
||||
|
||||
func (c *meekConn) Read(p []byte) (n int, err error) {
|
||||
// If there is data left over from the previous read,
|
||||
// service the request using the buffered data.
|
||||
if c.rdBuf != nil {
|
||||
if c.rdBuf.Len() == 0 {
|
||||
panic("empty read buffer")
|
||||
}
|
||||
n, err = c.rdBuf.Read(p)
|
||||
if c.rdBuf.Len() == 0 {
|
||||
c.rdBuf = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for the worker to enqueue more incoming data.
|
||||
b, ok := <-c.workerRdChan
|
||||
if !ok {
|
||||
// Close() was called and the worker's shutting down.
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
// Ew, an extra copy, but who am I kidding, it's meek.
|
||||
buf := bytes.NewBuffer(b)
|
||||
n, err = buf.Read(p)
|
||||
if buf.Len() > 0 {
|
||||
// If there's data pending, stash the buffer so the next
|
||||
// Read() call will use it to fulfuill the Read().
|
||||
c.rdBuf = buf
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *meekConn) Write(b []byte) (n int, err error) {
|
||||
// Check to see if the connection is actually open.
|
||||
c.Lock()
|
||||
closed := !c.workerRunning
|
||||
c.Unlock()
|
||||
if closed {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
if len(b) > 0 {
|
||||
// Copy the data to be written to a new slice, since
|
||||
// we return immediately after queuing and the peer can
|
||||
// happily reuse `b` before data has been sent.
|
||||
toWrite := len(b)
|
||||
b2 := make([]byte, toWrite)
|
||||
copy(b2, b)
|
||||
offset := 0
|
||||
for toWrite > 0 {
|
||||
// Chunk up the writes to keep them under the maximum
|
||||
// payload length.
|
||||
sz := toWrite
|
||||
if sz > maxPayloadLength {
|
||||
sz = maxPayloadLength
|
||||
}
|
||||
|
||||
// Enqueue a properly sized subslice of our copy.
|
||||
if ok := c.enqueueWrite(b2[offset : offset+sz]); !ok {
|
||||
// Technically we did enqueue data, but the worker's
|
||||
// got closed out from under us.
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
toWrite -= sz
|
||||
offset += sz
|
||||
runtime.Gosched()
|
||||
}
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (c *meekConn) Close() error {
|
||||
// Ensure that we do this once and only once.
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if !c.workerRunning {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tear down the worker.
|
||||
c.workerRunning = false
|
||||
c.workerCloseChan <- true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *meekConn) LocalAddr() net.Addr {
|
||||
return &net.IPAddr{IP: loopbackAddr}
|
||||
}
|
||||
|
||||
func (c *meekConn) RemoteAddr() net.Addr {
|
||||
return c.args
|
||||
}
|
||||
|
||||
func (c *meekConn) SetDeadline(t time.Time) error {
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
func (c *meekConn) SetReadDeadline(t time.Time) error {
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
func (c *meekConn) SetWriteDeadline(t time.Time) error {
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
func (c *meekConn) enqueueWrite(b []byte) (ok bool) {
|
||||
defer func() { recover() }()
|
||||
c.workerWrChan <- b
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *meekConn) roundTrip(sndBuf []byte) (recvBuf []byte, err error) {
|
||||
var req *http.Request
|
||||
var resp *http.Response
|
||||
|
||||
for retries := 0; retries < maxRetries; retries++ {
|
||||
url := *c.args.url
|
||||
host := url.Host
|
||||
if c.args.front != "" {
|
||||
url.Host = c.args.front
|
||||
}
|
||||
req, err = http.NewRequest("POST", url.String(), bytes.NewReader(sndBuf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.args.front != "" {
|
||||
req.Host = host
|
||||
}
|
||||
req.Header.Set("X-Session-Id", c.sessionID)
|
||||
|
||||
resp, err = c.transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err = fmt.Errorf("status code was %d, not %d", resp.StatusCode, http.StatusOK)
|
||||
time.Sleep(retryDelay)
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
recvBuf, err = ioutil.ReadAll(io.LimitReader(resp.Body, maxPayloadLength))
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *meekConn) ioWorker() {
|
||||
interval := initPollInterval
|
||||
loop:
|
||||
for {
|
||||
var sndBuf []byte
|
||||
select {
|
||||
case <-time.After(interval):
|
||||
// If the poll interval has elapsed, issue a request.
|
||||
case sndBuf = <-c.workerWrChan:
|
||||
// If there is data pending a send, issue a request.
|
||||
case _ = <-c.workerCloseChan:
|
||||
break loop
|
||||
}
|
||||
|
||||
// Issue a request.
|
||||
rdBuf, err := c.roundTrip(sndBuf)
|
||||
if err != nil {
|
||||
// Welp, something went horrifically wrong.
|
||||
break loop
|
||||
}
|
||||
if len(rdBuf) > 0 {
|
||||
// Received data, enqueue the read.
|
||||
c.workerRdChan <- rdBuf
|
||||
|
||||
// And poll immediately.
|
||||
interval = 0
|
||||
} else if sndBuf != nil {
|
||||
// Sent data, poll immediately.
|
||||
interval = 0
|
||||
} else if interval == 0 {
|
||||
// Neither sent nor received data, initialize the delay.
|
||||
interval = initPollInterval
|
||||
} else {
|
||||
// Apply a multiplicative backoff.
|
||||
interval = time.Duration(float64(interval) * pollIntervalMultiplier)
|
||||
if interval > maxPollInterval {
|
||||
interval = maxPollInterval
|
||||
}
|
||||
}
|
||||
|
||||
runtime.Gosched()
|
||||
}
|
||||
|
||||
// Unblock callers waiting in Read() for data that will never arrive,
|
||||
// and callers waiting in Write() for data that will never get sent.
|
||||
close(c.workerRdChan)
|
||||
close(c.workerWrChan)
|
||||
|
||||
// In case the close was done on an error condition, update the state
|
||||
// variable so that further calls to Write() will fail.
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.workerRunning = false
|
||||
}
|
||||
|
||||
func newMeekConn(network, addr string, dialFn base.DialFunc, ca *meekClientArgs) (net.Conn, error) {
|
||||
id, err := newSessionID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tr := &http.Transport{Dial: dialFn}
|
||||
conn := &meekConn{
|
||||
args: ca,
|
||||
sessionID: id,
|
||||
transport: tr,
|
||||
workerRunning: true,
|
||||
workerWrChan: make(chan []byte, maxChanBacklog),
|
||||
workerRdChan: make(chan []byte, maxChanBacklog),
|
||||
workerCloseChan: make(chan bool),
|
||||
}
|
||||
|
||||
// Start the I/O worker.
|
||||
go conn.ioWorker()
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func newSessionID() (string, error) {
|
||||
var b [64]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
h := sha256.Sum256(b[:])
|
||||
return hex.EncodeToString(h[:16]), nil
|
||||
}
|
||||
|
||||
var _ net.Conn = (*meekConn)(nil)
|
||||
var _ net.Addr = (*meekClientArgs)(nil)
|
Loading…
Reference in New Issue