summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoratanarjuat <atanarjuat@example.com>2022-05-21 00:43:41 +0200
committeratanarjuat <atanarjuat@example.com>2022-05-21 05:04:06 +0200
commita809112a20b31b9a4adca31ae564d943a41e9023 (patch)
tree8cc8b641a25867a8c556a073f44c25e1ff36433d
parentcefa2c334751efbf1d4b479fb827c21c5b801a71 (diff)
functional transparent proxy
-rw-r--r--.gitignore4
-rw-r--r--README.md44
-rw-r--r--go.sum4
-rw-r--r--listener.go42
-rw-r--r--obfsproxy/Makefile18
-rwxr-xr-xobfsproxy/leap-vpn.sh13
-rw-r--r--obfsproxy/main.go154
-rw-r--r--obfsproxy/main_test.go8
8 files changed, 246 insertions, 41 deletions
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 {}
}
+*/