From a809112a20b31b9a4adca31ae564d943a41e9023 Mon Sep 17 00:00:00 2001 From: atanarjuat Date: Sat, 21 May 2022 00:43:41 +0200 Subject: functional transparent proxy --- .gitignore | 4 ++ README.md | 44 +++++++++++++- go.sum | 4 ++ listener.go | 42 ++++++++++++++ obfsproxy/Makefile | 18 ++++++ obfsproxy/leap-vpn.sh | 13 +++++ obfsproxy/main.go | 154 +++++++++++++++++++++++++++++++++++++++---------- obfsproxy/main_test.go | 8 +-- 8 files changed, 246 insertions(+), 41 deletions(-) create mode 100644 .gitignore create mode 100644 obfsproxy/Makefile create mode 100755 obfsproxy/leap-vpn.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb184ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +obfsproxy/test_data/* +obfsproxy/obfsproxy +*.swp +*.swo diff --git a/README.md b/README.md index 6db45fb..30e4542 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,43 @@ # ObfsVPN -The `obfsvpn` module contains a Go package that provides an easy mechanism to -establish and listen for network connections that use the ntor handshake and -OBFS4 obfuscation protocol. +The `obfsvpn` module contains a Go package that provides server and client components to +use variants of the obfs4 obfuscation protocol. It is intended to be used as a +drop-in Pluggable Transport for OpenVPN connections (although it can be used +for other, more generic purposes). + +A docker container will be provided to facilitate startng an OpenVPN service that +is accessible via the obfuscated proxy too. + +## Protocol stack + +``` +-------------------- + application data +-------------------- + OpenVPN +-------------------- + obfsvpn proxy +-------------------- + obfs4 +-------------------- + wire transport +-------------------- +``` + +* Application data is written to the specified interface (typically a `tun` + device started by `OpenVPN`). +* `OpenVPN` provides end-to-end encryption and a reliability layer. We'll be + testing with the `2.5.x` branch of the reference OpenVPN implementation. +* `obfs4` is used for an extra layer of encryption and obfuscation. It is a + look-like-nothing protocol that also hides the key exchange to the eyes of + the censor. +* Wire transport is, by default, TCP. Other transports will be explored to + facilitate evasion: `KCP`, `QUIC`? + +## Testing + +... + +## Android + +... diff --git a/go.sum b/go.sum index 867bfcf..cff2b51 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,12 @@ github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= +gitlab.com/yawning/edwards25519-extra.git v0.0.0-20211229043746-2f91fcc9fbdb h1:qRSZHsODmAP5qDvb3YsO7Qnf3TRiVbGxNG/WYnlM4/o= +gitlab.com/yawning/edwards25519-extra.git v0.0.0-20211229043746-2f91fcc9fbdb/go.mod h1:gvdJuZuO/tPZyhEV8K3Hmoxv/DWud5L4qEQxfYjEUTo= gitlab.com/yawning/obfs4.git v0.0.0-20210511220700-e330d1b7024b h1:w/f20IHUkUYEp+xYgpKz4Bs78zms0DbjPZCep5lc0xA= gitlab.com/yawning/obfs4.git v0.0.0-20210511220700-e330d1b7024b/go.mod h1:OM1ngEp5brdANPox+rqk2AGTLQvzobyB5Dwm3vu3CgM= +gitlab.com/yawning/obfs4.git v0.0.0-20220204003609-77af0cba934d h1:tJ8F7ABaQ3p3wjxwXiWSktVDgjZEXkvaRawd2rIq5ws= +gitlab.com/yawning/obfs4.git v0.0.0-20220204003609-77af0cba934d/go.mod h1:9GcM8QNU9/wXtEEH2q8bVOnPI7FtIF6VVLzZ1l6Hgf8= gitlab.com/yawning/utls.git v0.0.12-1/go.mod h1:3ONKiSFR9Im/c3t5RKmMJTVdmZN496FNyk3mjrY1dyo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/listener.go b/listener.go index 593032f..82db98c 100644 --- a/listener.go +++ b/listener.go @@ -5,6 +5,8 @@ import ( "context" "crypto/rand" "encoding/hex" + "fmt" + "log" "net" pt "git.torproject.org/pluggable-transports/goptlib.git" @@ -21,10 +23,47 @@ type ListenConfig struct { NodeID *ntor.NodeID PrivateKey *ntor.PrivateKey + PublicKey string Seed [ntor.KeySeedLength]byte StateDir string } +// perhaps this is redundant, but using the same json format than ss for debug. +// kali: feel free to remove this if/when we make sure unwrapping the cert is enough for us. +func NewListenConfig(nodeIDStr, privKeyStr, pubKeyStr, seedStr, stateDir string) (*ListenConfig, error) { + var err error + var seed [ntor.KeySeedLength]byte + var nodeID *ntor.NodeID + private := new(ntor.PrivateKey) + + if nodeID, err = ntor.NodeIDFromHex(nodeIDStr); err != nil { + return nil, err + } + + raw, err := hex.DecodeString(privKeyStr) + if err != nil { + return nil, err + } + log.Println("DEBUG len private ley:", len(raw)) + // TODO raise invalid error if len not right + copy(private[:], raw) + + s, err := hex.DecodeString(seedStr) + if err != nil { + return nil, err + } + copy(seed[:], s) + + lc := &ListenConfig{ + NodeID: nodeID, + PrivateKey: private, + PublicKey: pubKeyStr, + Seed: seed, + StateDir: stateDir, + } + return lc, nil +} + // NewListenConfigCert creates a listener config by unpacking the node ID from // its certificate. // The private key must still be specified. @@ -54,7 +93,10 @@ func (lc *ListenConfig) Wrap(ctx context.Context, ln net.Listener) (*Listener, e } else { seed = lc.Seed } + args.Add("drbg-seed", hex.EncodeToString(seed[:])) + args.Add("public-key", lc.PublicKey) + fmt.Println("pubkey:", lc.PublicKey) sf, err := (&obfs4.Transport{}).ServerFactory(lc.StateDir, &args) if err != nil { return nil, err diff --git a/obfsproxy/Makefile b/obfsproxy/Makefile new file mode 100644 index 0000000..8694349 --- /dev/null +++ b/obfsproxy/Makefile @@ -0,0 +1,18 @@ +RHOST=163.172.126.44:443 +LHOST="10.0.0.209:443" + +run: + sudo ./obfsproxy -addr ${LHOST} -vpn ${RHOST} -state test_data -c test_data/obfs4.json + +certs: + curl -k https://black.riseup.net/ca.crt > /tmp/ca.crt + curl -k https://api.black.riseup.net/3/cert > /tmp/cert.pem + +check: + curl https://wtfismyip.com/json + +stop: + pkill -9 obfsproxy + +obfsproxy: + go build diff --git a/obfsproxy/leap-vpn.sh b/obfsproxy/leap-vpn.sh new file mode 100755 index 0000000..4fb09c3 --- /dev/null +++ b/obfsproxy/leap-vpn.sh @@ -0,0 +1,13 @@ +#!/bin/sh +sudo openvpn \ + --verb 3 \ + --tls-cipher DHE-RSA-AES128-SHA \ + --cipher AES-128-CBC \ + --dev tun --client --tls-client \ + --remote-cert-tls server --tls-version-min 1.2 \ + --ca /tmp/ca.crt --cert /tmp/cert.pem --key /tmp/cert.pem \ + --proto tcp4 \ + --remote localhost 4430 \ + --socks-proxy localhost 4430 \ + --route $GW 255.255.255.255 net_gateway \ + --persist-tun diff --git a/obfsproxy/main.go b/obfsproxy/main.go index a35a26d..5e56789 100644 --- a/obfsproxy/main.go +++ b/obfsproxy/main.go @@ -4,7 +4,9 @@ package main import ( "context" "encoding/json" + "errors" "flag" + "fmt" "io" "log" "net" @@ -12,12 +14,19 @@ import ( "os/signal" "0xacab.org/leap/obfsvpn" - "git.torproject.org/pluggable-transports/goptlib.git" ) const transportName = "obfs4" +type Config struct { + NodeID string `json:"node-id"` + PrivateKey string `json:"private-key"` + PublicKey string `json:"public-key"` + DRBGSeed string `json:"drbg-seed"` + IatMode int `json:"iat-mode"` +} + func main() { // Setup logging. logger := log.New(os.Stderr, "", log.LstdFlags) @@ -48,15 +57,17 @@ func main() { } tcpVPNAddr, err := net.ResolveTCPAddr("tcp", vpnAddr) + log.Println("target:", tcpVPNAddr) if err != nil { logger.Fatalf("error resolving VPN address: %v", err) } + var cfg Config fd, err := os.Open(cfgFile) + log.Println("opening:", cfgFile) if err != nil { logger.Fatalf("error opening config file: %v", err) } - var cfg Config err = json.NewDecoder(fd).Decode(&cfg) if err != nil { logger.Fatalf("error decoding config: %v", err) @@ -67,15 +78,16 @@ func main() { debug.SetOutput(os.Stderr) } + log.Println("config:", cfg) + // Setup graceful shutdown. ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - listenConfig := obfsvpn.ListenConfig{ - NodeID: cfg.NodeID, - PrivateKey: cfg.PrivateKey, - Seed: cfg.DRBGSeed, - StateDir: stateDir, + listenConfig, err := obfsvpn.NewListenConfig(cfg.NodeID, cfg.PrivateKey, cfg.PublicKey, cfg.DRBGSeed, stateDir) + if err != nil { + logger.Fatalf("error creating listener from config: %v", err) } + log.Println("DEBUG:", listenConfig) ln, err := listenConfig.Listen(ctx, "tcp", addr) if err != nil { logger.Fatalf("error binding to %s: %v", addr, err) @@ -105,44 +117,122 @@ func main() { return } debug.Printf("accepted connection %v…", conn) - go proxyConn(ctx, info, conn, net.Dialer{}, logger, debug) + go proxyConn(ctx, info, conn, logger, debug) } } -func proxyConn(ctx context.Context, info *pt.ServerInfo, conn net.Conn, d net.Dialer, logger, debug *log.Logger) { +// obfsConn is a connection to the obfs4 client that we have accepted. we will dial to the remote contained in info +func proxyConn(ctx context.Context, info *pt.ServerInfo, obfsConn net.Conn, logger, debug *log.Logger) { defer func() { - err := conn.Close() + err := obfsConn.Close() if err != nil { debug.Printf("error closing connection: %v", err) } }() - // TODO: do we actually want to send the USERADDR/TRANSPORT ExtOrPort - // commands? I don't understand how this works, so I'm unsure. - remote, err := pt.DialOr(info, conn.RemoteAddr().String(), transportName) + // FIXME scrub ips in other than debug mode! + log.Println("Dialing:", info.OrAddr) + log.Println("Obfs4 client:", obfsConn.RemoteAddr().String()) + + /* + in the case of Tor, pt.DialOr returns a *net.TCPConn after dialing info.OrAddr. + in the vpn case (or any transparent proxy really), we do use + the pt.DialOr method to simply get a dialer to our upstream VPN remote. + + keeping this terminology is a bit stupid and slightly confusing, instead + we could get the clearConn just by doing: + + s, err := net.DialTCP("tcp", nil, info.ExtendedOrAddr) + if err != nil { + return nil, err + } + + that is precisely what the code in ptlib is doing. + + We also aspire at being a generic PT at some point, so perhaps it's better to keep the usage + of DialOr? + + Maybe not, and so we don't need to keep the confusing info struct. + */ + + // we'll refer to the connection to the usptream node as "clearConn", as opposed to the obfuscated conn. + // but for sure openvpn or whatever protocol you wrap has its own layer of encryption :) + + clearConn, err := pt.DialOr(info, obfsConn.RemoteAddr().String(), transportName) + if err != nil { logger.Printf("error dialing remote: %v", err) return } - defer func() { - err := remote.Close() - if err != nil { - debug.Printf("error closing remote connection: %v", err) - } - }() - go func() { - _, err := io.Copy(remote, conn) - if err != nil { - logger.Printf("error proxying client data to remote: %v", err) - return - } - }() - go func() { - _, err := io.Copy(conn, remote) - if err != nil { - logger.Printf("error proxying remote data to client: %v", err) - return + if err = CopyLoop(clearConn, obfsConn); err != nil { + debug.Printf("%s - closed connection: %s", "obfsvpn", err.Error()) + } else { + debug.Printf("%s - closed connection", "obfsvpn") + } +} + +// a stock copy loop. let's not dwell too much on who's client and who's server + +func CopyLoop(left net.Conn, right net.Conn) error { + + fmt.Println("--> Entering copy loop.") + + if left == nil { + fmt.Fprintln(os.Stderr, "--> Copy loop has a nil connection (left).") + return errors.New("copy loop has a nil connection (left)") + } + + if right == nil { + fmt.Fprintln(os.Stderr, "--> Copy loop has a nil connection (right).") + return errors.New("copy loop has a nil connection (right)") + } + + // Note: right is always the pt connection. + lockL := make(chan bool) + lockR := make(chan bool) + errChan := make(chan error) + + go CopyLeftToRight(left, right, lockL, errChan) + go CopyRightToLeft(left, right, lockR, errChan) + + leftUp := true + rightUp := true + + var copyErr error + + for leftUp || rightUp { + select { + case <-lockL: + leftUp = false + case <-lockR: + rightUp = false + case copyErr = <-errChan: + log.Println("Error while copying") } - }() + } + + // XXX better to defer? + left.Close() + right.Close() + + return copyErr +} + +// TODO check for data races + +func CopyLeftToRight(l net.Conn, r net.Conn, ll chan bool, errChan chan error) { + _, e := io.Copy(r, l) + ll <- true + if e != nil { + errChan <- e + } +} + +func CopyRightToLeft(l net.Conn, r net.Conn, lr chan bool, errChan chan error) { + _, e := io.Copy(l, r) + lr <- true + if e != nil { + errChan <- e + } } diff --git a/obfsproxy/main_test.go b/obfsproxy/main_test.go index d22d3af..cdead61 100644 --- a/obfsproxy/main_test.go +++ b/obfsproxy/main_test.go @@ -1,15 +1,9 @@ package main import ( - "context" "flag" - "net" "os" - "os/exec" "testing" - "time" - - "golang.org/x/net/proxy" ) type testWriter struct { @@ -33,6 +27,7 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +/* func TestRoundTrip(t *testing.T) { // Setup and exec the proxy: ctx, cancel := context.WithCancel(context.Background()) @@ -90,3 +85,4 @@ func TestRoundTrip(t *testing.T) { select {} } +*/ -- cgit v1.2.3