From 182e3e76908f0824eac155b5e14775b50fe0aca5 Mon Sep 17 00:00:00 2001 From: atanarjuat Date: Sat, 21 May 2022 01:50:47 +0200 Subject: simplify testing of client and server --- .gitignore | 2 +- Makefile | 19 ++++ client/main.go | 66 ++++++++++++ client/obfsclient | Bin 0 -> 3255678 bytes dialer.go | 13 +-- docs/README.md | 124 +++++++++++++++++++++ go.mod | 3 +- go.sum | 6 +- listener.go | 3 +- obfsproxy/Makefile | 18 ---- obfsproxy/leap-vpn.sh | 13 --- obfsproxy/main.go | 238 ---------------------------------------- obfsproxy/main_test.go | 88 --------------- scripts/run-openvpn-client.sh | 16 +++ server/Makefile | 11 ++ server/main.go | 245 ++++++++++++++++++++++++++++++++++++++++++ server/main_test.go | 90 ++++++++++++++++ server/test_data/obfs4.json | 1 + 18 files changed, 586 insertions(+), 370 deletions(-) create mode 100644 Makefile create mode 100644 client/main.go create mode 100755 client/obfsclient create mode 100644 docs/README.md delete mode 100644 obfsproxy/Makefile delete mode 100755 obfsproxy/leap-vpn.sh delete mode 100644 obfsproxy/main.go delete mode 100644 obfsproxy/main_test.go create mode 100755 scripts/run-openvpn-client.sh create mode 100644 server/Makefile create mode 100644 server/main.go create mode 100644 server/main_test.go create mode 100644 server/test_data/obfs4.json diff --git a/.gitignore b/.gitignore index bb184ea..b21dfe4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -obfsproxy/test_data/* obfsproxy/obfsproxy +obfsclient/obfsclient *.swp *.swo diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3552bc0 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +OBFS4_ENDPOINT ?= +OBFS4_CERT ?= + +certs: + curl -k https://black.riseup.net/ca.crt > /tmp/ca.crt + curl -k https://api.black.riseup.net/3/cert > /tmp/cert.pem + +build-client: + go get ./... + go build -o obfsvpn-client ./client + +run-client: + ./obfsvpn-client -c ${OBFS4_CERT} + +run-openvpn: + ./scripts/run-openvpn-client.sh + +check: + curl https://wtfismyip.com/json diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..0c7edcc --- /dev/null +++ b/client/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "flag" + "fmt" + "io" + "log" + "net" + "os" + + "0xacab.org/leap/obfsvpn" + socks5 "github.com/armon/go-socks5" +) + +func main() { + // Setup logging. + logger := log.New(os.Stderr, "", log.LstdFlags) + debug := log.New(io.Discard, "DEBUG ", log.LstdFlags) + + // setup command line flags. + var ( + verbose bool + obfs4Cert string + obfs4Remote string + socksPort string + socksHost string + ) + + socksPort = "8080" + socksHost = "127.0.0.1" + + flags := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + flags.BoolVar(&verbose, "v", verbose, "Enable verbose logging") + flags.StringVar(&obfs4Cert, "c", obfs4Cert, "The remote obfs4 certificate") + flags.StringVar(&obfs4Remote, "r", obfs4Remote, "The remote obfs4 endpoint (ip:port)") + flags.StringVar(&socksPort, "p", socksPort, "The port for the local socks5 proxy (default: 8080)") + flags.StringVar(&socksHost, "i", socksHost, "The host for the local socks5 proxy (default: localhost)") + err := flags.Parse(os.Args[1:]) + if err != nil { + logger.Fatalf("error parsing flags: %v", err) + } + // Configure logging. + if verbose { + debug.SetOutput(os.Stderr) + } + + dialer, err := obfsvpn.NewDialerFromCert(obfs4Cert) + if err != nil { + logger.Fatalf("cannot get dialer: %v", err) + } + + socksConf := &socks5.Config{ + Dial: dialer.Dial, + } + + server, err := socks5.New(socksConf) + if err != nil { + panic(err) + } + + addr := net.JoinHostPort(socksHost, socksPort) + fmt.Printf("[+] Started socks5 proxy at %s\n", addr) + if err := server.ListenAndServe("tcp", addr); err != nil { + panic(err) + } +} diff --git a/client/obfsclient b/client/obfsclient new file mode 100755 index 0000000..9bd175b Binary files /dev/null and b/client/obfsclient differ diff --git a/dialer.go b/dialer.go index 5043aeb..db5d7b4 100644 --- a/dialer.go +++ b/dialer.go @@ -76,21 +76,22 @@ func unpackCert(cert string) (*ntor.NodeID, *ntor.PublicKey, error) { return nodeID, pubKey, nil } -// NewDialerCert creates a dialer from a node certificate. -func NewDialerCert(cert string) (*Dialer, error) { +// NewDialerFromCert creates a dialer from a node certificate. +func NewDialerFromCert(cert string) (*Dialer, error) { nodeID, publicKey, err := unpackCert(cert) if err != nil { return nil, err } - return &Dialer{ + d := &Dialer{ NodeID: nodeID, PublicKey: publicKey, - }, nil + } + return d, nil } -// NewDialerArgs creates a dialer from existing pluggable transport arguments. -func NewDialerArgs(args pt.Args) (*Dialer, error) { +// NewDialerFromArgs creates a dialer from existing pluggable transport arguments. +func NewDialerFromArgs(args pt.Args) (*Dialer, error) { cf, err := (&obfs4.Transport{}).ClientFactory("") if err != nil { return nil, err diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ef9e4c6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,124 @@ +# Developing + +These are notes to test the different components separately while developing. + +## Server-side proxy + +### 1. Compile and run obfserver + +First, we're going to set up the server-side proxy. Do note that we will skip +the part that generates the obfs4 keys (we will use the `obfsproxy/test_data` +folder for testing). + +`LHOST` is the local IP of the server (in case it's different than the public IP). +`RHOST` is the OpenVPN gateway IP. + +``` +export LHOST=10.10.1.10:4430 # this is our obfs4 endpoint (private network; if your public IP is exposed directly you should use the IP:PORT here instead). +export RHOST=163.172.126.44:443 # this is the GW IP (each obfsproxy is routing to only one openvpn GW). + +cd server && make build +sudo ./server -addr $(LHOST) -vpn $(RHOST) -state test_data -c test_data/obfs4.json +``` + +### 2. Run `obfsclient` to start a socks5 proxy in localhost + +It will start a local socks5 proxy, to where we will connect with `openvpn`. +For now, this is `TCP` only. By default, the socks5 proxy will listen in `127.0.0.1:8080`. + +``` +make build-client +make run-client OBFS4_CERT=8nuAbPJwFrKc/29KcCfL5LBuEWxQrjBASYXdUbwcm9d9pKseGK4r2Tg47e23+t6WghxGGw +``` + +### 3. Get certificates for the riseup gateways. + +``` +make certs +``` + +You should have certificates in `/tmp/cert.pem`, and the ca file in `/tmp/ca.crt`. + + +### 4. Run the `openvpn` client using the local `socks5` proxy. + +The `openvpn` binary needs to be invoked with the `OBFS4_ENDPOINT` as the +`--remote`, and the local `socks5` port as the `--proxy`. + +In this example, we pass the `OBFS4_ENDPOINT` variable via the `Makefile`: + +``` +make run-openvpn OBFS4_ENDPOINT=2.2.2.2 +``` + +If everything went well, now you should be connected to the gateway remote, and +all routes set up. + + +``` +❯ make run-openvpn OBFS4_ENDPOINT=2.2.2.2 +./scripts/run-openvpn-client.sh ++ sudo openvpn --verb 3 --tls-cipher DHE-RSA-AES128-SHA --cipher AES-128-CBC --auth-nocache --proto tcp --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 --pull-filter ignore ifconfig-ipv6 --pull-filter ignore route-ipv6 --socks-proxy 127.0.0.1 8080 --remote 2.2.2.2 443 --route 2.2.2.2 255.255.255.255 net_gateway +2022-05-21 03:41:35 OpenVPN 2.5.1 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on Mar 22 2022 +2022-05-21 03:41:35 library versions: OpenSSL 1.1.1l 24 Aug 2021, LZO 2.10 +2022-05-21 03:41:35 Deprecated TLS cipher name 'DHE-RSA-AES128-SHA', please use IANA name 'TLS-DHE-RSA-WITH-AES-128-CBC-SHA' +2022-05-21 03:41:35 TCP/UDP: Preserving recently used remote address: [AF_INET]127.0.0.1:8080 +2022-05-21 03:41:35 Socket Buffers: R=[131072->131072] S=[16384->16384] +2022-05-21 03:41:35 Attempting to establish TCP connection with [AF_INET]127.0.0.1:8080 [nonblock] +2022-05-21 03:41:35 TCP connection established with [AF_INET]127.0.0.1:8080 +2022-05-21 03:41:35 TCP_CLIENT link local: (not bound) +2022-05-21 03:41:35 TCP_CLIENT link remote: [AF_INET]127.0.0.1:8080 +2022-05-21 03:41:35 TLS: Initial packet from [AF_INET]127.0.0.1:8080, sid=61e85660 36666a7a +2022-05-21 03:41:35 VERIFY OK: depth=1, O=Riseup Networks, OU=https://riseup.net, CN=Riseup Networks Root CA +2022-05-21 03:41:35 VERIFY KU OK +2022-05-21 03:41:35 Validating certificate extended key usage +2022-05-21 03:41:35 ++ Certificate has EKU (str) TLS Web Server Authentication, expects TLS Web Server Authentication +2022-05-21 03:41:35 VERIFY EKU OK +2022-05-21 03:41:35 VERIFY OK: depth=0, CN=mouette.riseup.net, O=Riseup Networks, L=Seattle, ST=WA, C=US +2022-05-21 03:41:36 Control Channel: TLSv1.2, cipher SSLv3 DHE-RSA-AES128-SHA, 4096 bit RSA +2022-05-21 03:41:36 [mouette.riseup.net] Peer Connection Initiated with [AF_INET]127.0.0.1:8080 +2022-05-21 03:41:37 SENT CONTROL [mouette.riseup.net]: 'PUSH_REQUEST' (status=1) +2022-05-21 03:41:37 PUSH: Received control message: 'PUSH_REPLY,dhcp-option DNS 10.41.0.1,redirect-gateway def1,route-ipv6 2000::/3,tun-ipv6,route-gateway 10.41.0.1,topology subnet,ping 10,ping-restart 30,socket-flags TCP_NODELAY,ifconfig-ipv6 2001:db8:123::100d/64 2001:db8:123::1,ifconfig 10.41.0.15 255.255.248.0,peer-id 0,cipher AES-256-GCM' +2022-05-21 03:41:37 Pushed option removed by filter: 'route-ipv6 2000::/3' +2022-05-21 03:41:37 Pushed option removed by filter: 'ifconfig-ipv6 2001:db8:123::100d/64 2001:db8:123::1' +2022-05-21 03:41:37 OPTIONS IMPORT: timers and/or timeouts modified +2022-05-21 03:41:37 OPTIONS IMPORT: --socket-flags option modified +2022-05-21 03:41:37 Socket flags: TCP_NODELAY=1 succeeded +2022-05-21 03:41:37 OPTIONS IMPORT: --ifconfig/up options modified +2022-05-21 03:41:37 OPTIONS IMPORT: route options modified +2022-05-21 03:41:37 OPTIONS IMPORT: route-related options modified +2022-05-21 03:41:37 OPTIONS IMPORT: --ip-win32 and/or --dhcp-option options modified +2022-05-21 03:41:37 OPTIONS IMPORT: peer-id set +2022-05-21 03:41:37 OPTIONS IMPORT: adjusting link_mtu to 1626 +2022-05-21 03:41:37 OPTIONS IMPORT: data channel crypto options modified +2022-05-21 03:41:37 Data Channel: using negotiated cipher 'AES-256-GCM' +2022-05-21 03:41:37 Outgoing Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key +2022-05-21 03:41:37 Incoming Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key +2022-05-21 03:41:37 net_route_v4_best_gw query: dst 0.0.0.0 +2022-05-21 03:41:37 net_route_v4_best_gw result: via 192.168.1.1 dev eth0 +2022-05-21 03:41:37 ROUTE_GATEWAY 192.168.1.1/255.255.255.0 +2022-05-21 03:41:37 TUN/TAP device tun0 opened +2022-05-21 03:41:37 net_iface_mtu_set: mtu 1500 for tun0 +2022-05-21 03:41:37 net_iface_up: set tun0 up +2022-05-21 03:41:37 net_addr_v4_add: 10.41.0.15/21 dev tun0 +2022-05-21 03:41:37 net_route_v4_add: 127.0.0.1/32 via 192.168.18.1 dev [NULL] table 0 metric -1 +2022-05-21 03:41:37 net_route_v4_add: 0.0.0.0/1 via 10.41.0.1 dev [NULL] table 0 metric -1 +2022-05-21 03:41:37 net_route_v4_add: 128.0.0.0/1 via 10.41.0.1 dev [NULL] table 0 metric -1 +2022-05-21 03:41:37 net_route_v4_add: 2.2.2.2/32 via 192.168.1.1 dev [NULL] table 0 metric -1 +2022-05-21 03:41:37 Initialization Sequence Completed +``` + +You should verify that your exit IP has changed: + + +``` +curl https://wtfismyip.com/json +{ + "YourFuckingIPAddress": "51.159.55.86", + "YourFuckingLocation": "Paris, IDF, France", + "YourFuckingHostname": "51-159-55-86.rev.poneytelecom.eu", + "YourFuckingISP": "Scaleway", + "YourFuckingTorExit": false, + "YourFuckingCountryCode": "FR" +} +``` diff --git a/go.mod b/go.mod index 7ea5ce8..65a7ab2 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,15 @@ go 1.17 require ( git.torproject.org/pluggable-transports/goptlib.git v1.0.0 + github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 // Do not update obfs4 past e330d1b7024b, a backwards incompatible change was // made that will break negotiation. gitlab.com/yawning/obfs4.git v0.0.0-20210511220700-e330d1b7024b - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 ) require ( github.com/dchest/siphash v1.2.1 // indirect golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect ) diff --git a/go.sum b/go.sum index cff2b51..482ab37 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ git.torproject.org/pluggable-transports/goptlib.git v1.0.0 h1:ElTwFFPKf/tA6x5nuIk9g49JZzS4T5WN+eTQTjqd00A= git.torproject.org/pluggable-transports/goptlib.git v1.0.0/go.mod h1:YT4XMSkuEXbtqlydr9+OxqFAyspUv0Gr9qhM3B++o/Q= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4= github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= @@ -8,12 +10,8 @@ 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 82db98c..30ad401 100644 --- a/listener.go +++ b/listener.go @@ -28,8 +28,9 @@ type ListenConfig struct { StateDir string } +// NewListenConfig // 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 diff --git a/obfsproxy/Makefile b/obfsproxy/Makefile deleted file mode 100644 index 8694349..0000000 --- a/obfsproxy/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100755 index 4fb09c3..0000000 --- a/obfsproxy/leap-vpn.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/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 deleted file mode 100644 index 5e56789..0000000 --- a/obfsproxy/main.go +++ /dev/null @@ -1,238 +0,0 @@ -// The obfsproxy command creates a SOCKS5 obfuscating proxy. -package main - -import ( - "context" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "log" - "net" - "os" - "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) - debug := log.New(io.Discard, "DEBUG ", log.LstdFlags) - - // Setup command line flags. - var ( - verbose bool - addr string = "[::1]:0" - vpnAddr string - cfgFile string - stateDir string - ) - flags := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) - flags.BoolVar(&verbose, "v", verbose, "Enable verbose logging") - flags.StringVar(&addr, "addr", addr, "The address to listen on for client connections") - flags.StringVar(&vpnAddr, "vpn", vpnAddr, "The address of the OpenVPN server to connect to") - flags.StringVar(&cfgFile, "c", cfgFile, "The JSON config file to load") - flags.StringVar(&stateDir, "state", stateDir, "A directory in which to store bridge state") - err := flags.Parse(os.Args[1:]) - if err != nil { - logger.Fatalf("error parsing flags: %v", err) - } - - if vpnAddr == "" { - flags.PrintDefaults() - logger.Fatal("must specify -vpn") - } - - 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) - } - err = json.NewDecoder(fd).Decode(&cfg) - if err != nil { - logger.Fatalf("error decoding config: %v", err) - } - - // Configure logging. - if verbose { - debug.SetOutput(os.Stderr) - } - - log.Println("config:", cfg) - - // Setup graceful shutdown. - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - - 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) - } - - go func() { - <-ctx.Done() - // Stop releases the signal handling and falls back to the default behavior, - // so sending another interrupt will immediately terminate. - stop() - logger.Printf("shutting down…") - err := ln.Close() - if err != nil { - logger.Printf("error closing listener: %v", err) - } - }() - - info := &pt.ServerInfo{ - OrAddr: tcpVPNAddr, - } - - logger.Printf("listening on %s…", ln.Addr()) - for { - conn, err := ln.Accept() - if err != nil { - debug.Printf("error accepting connection: %v", err) - return - } - debug.Printf("accepted connection %v…", conn) - go proxyConn(ctx, info, conn, logger, debug) - } -} - -// 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 := obfsConn.Close() - if err != nil { - debug.Printf("error closing connection: %v", err) - } - }() - - // 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 - } - - 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 deleted file mode 100644 index cdead61..0000000 --- a/obfsproxy/main_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "flag" - "os" - "testing" -) - -type testWriter struct { - t *testing.T - prefix string -} - -func (w testWriter) Write(p []byte) (int, error) { - w.t.Logf("%s%s", w.prefix, p) - return len(p), nil -} - -func TestMain(m *testing.M) { - runProxy := flag.Bool("runproxy", false, "Start the command instead of running the tests") - flag.Parse() - if *runProxy { - os.Args = append(os.Args[0:1], flag.Args()...) - main() - return - } - os.Exit(m.Run()) -} - -/* -func TestRoundTrip(t *testing.T) { - // Setup and exec the proxy: - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - // Instead of passing a listener to the command or doing IPC to get the - // address of the listener created by the command back out, which would all - // require changes to the actual binary for something that has no use outside - // of tests and is just another potential source of errors, just start and - // stop a listener to get a random port and then pass that in (for the command - // to re-open) as a string. It's not ideal, but it's simple. - ln, err := net.Listen("tcp", "[::1]:0") - if err != nil { - t.Fatalf("error listening: %v", err) - } - addr := ln.Addr() - err = ln.Close() - if err != nil { - t.Fatalf("error closing listener: %v", err) - } - cmd := exec.CommandContext(ctx, os.Args[0], "-runproxy", "--", "-addr", addr.String(), "-proxy", "37.218.241.98:4430") - cmd.Stdout = testWriter{prefix: "stdout ", t: t} - cmd.Stderr = testWriter{prefix: "stderr ", t: t} - t.Logf("running proxy command %v", cmd.Args) - err = cmd.Start() - if err != nil { - t.Fatalf("error starting proxy: %v", err) - } - - // Once the proxy is running, try to connect: - ln, err = net.Listen("tcp", "[::1]:0") - if err != nil { - t.Fatalf("error listening for connection: %v", err) - } - go func() { - conn, err := ln.Accept() - if err != nil { - t.Logf("error accepting connection: %v", err) - } - t.Logf("got conn: %v", conn) - }() - dialer, err := proxy.SOCKS5("tcp", addr.String(), nil, proxy.Direct) - if err != nil { - t.Fatalf("error creating socks dialer: %v", err) - } - - // TODO: this is slow, flakey, and generally jank. Can we watch /proc for a - // new file descriptor or just poll until the listener is open? - t.Logf("waiting 3 seconds for command to start…") - time.Sleep(3 * time.Second) - - _, err = dialer.Dial("tcp", ln.Addr().String()) - if err != nil { - t.Fatalf("error dialing: %v", err) - } - - select {} -} -*/ diff --git a/scripts/run-openvpn-client.sh b/scripts/run-openvpn-client.sh new file mode 100755 index 0000000..a70fcdc --- /dev/null +++ b/scripts/run-openvpn-client.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -x +sudo openvpn \ + --verb 3 \ + --tls-cipher DHE-RSA-AES128-SHA \ + --cipher AES-128-CBC \ + --auth-nocache \ + --proto tcp \ + --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 \ + --pull-filter ignore ifconfig-ipv6 \ + --pull-filter ignore route-ipv6 \ + --socks-proxy 127.0.0.1 8080 \ + --remote $OBFS4_ENDPOINT 443 \ + --route $OBFS4_ENDPOINT 255.255.255.255 net_gateway diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 0000000..853469b --- /dev/null +++ b/server/Makefile @@ -0,0 +1,11 @@ +RHOST=163.172.126.44:443 +LHOST="10.0.0.209:443" + +build: + go build + +run: + sudo ./obfsproxy -addr ${LHOST} -vpn ${RHOST} -state test_data -c test_data/obfs4.json + +stop: + pkill -9 obfsproxy diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..5427184 --- /dev/null +++ b/server/main.go @@ -0,0 +1,245 @@ +// The obfsproxy command creates a SOCKS5 obfuscating proxy. +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "log" + "net" + "os" + "os/signal" + + "0xacab.org/leap/obfsvpn" + pt "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) + debug := log.New(io.Discard, "DEBUG ", log.LstdFlags) + + // Setup command line flags. + var ( + verbose bool + vpnAddr string + cfgFile string + stateDir string + addr = "[::1]:0" + ) + flags := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + flags.BoolVar(&verbose, "v", verbose, "Enable verbose logging") + flags.StringVar(&addr, "addr", addr, "The address to listen on for client connections") + flags.StringVar(&vpnAddr, "vpn", vpnAddr, "The address of the OpenVPN server to connect to") + flags.StringVar(&cfgFile, "c", cfgFile, "The JSON config file to load") + flags.StringVar(&stateDir, "state", stateDir, "A directory in which to store bridge state") + err := flags.Parse(os.Args[1:]) + if err != nil { + logger.Fatalf("error parsing flags: %v", err) + } + + if vpnAddr == "" { + flags.PrintDefaults() + logger.Fatal("must specify -vpn") + } + + // TODO this needs to be configurable if we switch to UDP mode for OpenVPN. + 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) + } + err = json.NewDecoder(fd).Decode(&cfg) + if err != nil { + logger.Fatalf("error decoding config: %v", err) + } + + // Configure logging. + if verbose { + debug.SetOutput(os.Stderr) + } + + log.Println("config:", cfg) + + // Setup graceful shutdown. + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + + 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) + } + + logger.Printf("DEBUG: %v", listenConfig) + + ln, err := listenConfig.Listen(ctx, "tcp", addr) + if err != nil { + logger.Fatalf("error binding to %s: %v", addr, err) + } + + go func() { + <-ctx.Done() + // Stop releases the signal handling and falls back to the default behavior, + // so sending another interrupt will immediately terminate. + stop() + logger.Printf("shutting down…") + err := ln.Close() + if err != nil { + logger.Printf("error closing listener: %v", err) + } + }() + + info := &pt.ServerInfo{ + OrAddr: tcpVPNAddr, + } + + logger.Printf("Listening on %s…", ln.Addr()) + + for { + conn, err := ln.Accept() + if err != nil { + debug.Printf("error accepting connection: %v", err) + return + } + debug.Printf("accepted connection %v…", conn) + go proxyConn(ctx, info, conn, logger, debug) + } +} + +// proxyConn 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 := obfsConn.Close() + if err != nil { + debug.Printf("Error closing connection: %v", err) + } + }() + + // 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 + } + + if err = CopyLoop(clearConn, obfsConn); err != nil { + debug.Printf("%s - closed connection: %s", "obfsvpn", err.Error()) + } else { + debug.Printf("%s - closed connection", "obfsvpn") + } +} + +// CopyLoop is a standard copy loop. We don't care too much 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/server/main_test.go b/server/main_test.go new file mode 100644 index 0000000..ecbb364 --- /dev/null +++ b/server/main_test.go @@ -0,0 +1,90 @@ +package main + +import ( + "flag" + "os" + "testing" +) + +/* +type testWriter struct { + t *testing.T + prefix string +} + +func (w testWriter) Write(p []byte) (int, error) { + w.t.Logf("%s%s", w.prefix, p) + return len(p), nil +} +*/ + +func TestMain(m *testing.M) { + runProxy := flag.Bool("runproxy", false, "Start the command instead of running the tests") + flag.Parse() + if *runProxy { + os.Args = append(os.Args[0:1], flag.Args()...) + main() + return + } + os.Exit(m.Run()) +} + +/* +func TestRoundTrip(t *testing.T) { + // Setup and exec the proxy: + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + // Instead of passing a listener to the command or doing IPC to get the + // address of the listener created by the command back out, which would all + // require changes to the actual binary for something that has no use outside + // of tests and is just another potential source of errors, just start and + // stop a listener to get a random port and then pass that in (for the command + // to re-open) as a string. It's not ideal, but it's simple. + ln, err := net.Listen("tcp", "[::1]:0") + if err != nil { + t.Fatalf("error listening: %v", err) + } + addr := ln.Addr() + err = ln.Close() + if err != nil { + t.Fatalf("error closing listener: %v", err) + } + cmd := exec.CommandContext(ctx, os.Args[0], "-runproxy", "--", "-addr", addr.String(), "-proxy", "37.218.241.98:4430") + cmd.Stdout = testWriter{prefix: "stdout ", t: t} + cmd.Stderr = testWriter{prefix: "stderr ", t: t} + t.Logf("running proxy command %v", cmd.Args) + err = cmd.Start() + if err != nil { + t.Fatalf("error starting proxy: %v", err) + } + + // Once the proxy is running, try to connect: + ln, err = net.Listen("tcp", "[::1]:0") + if err != nil { + t.Fatalf("error listening for connection: %v", err) + } + go func() { + conn, err := ln.Accept() + if err != nil { + t.Logf("error accepting connection: %v", err) + } + t.Logf("got conn: %v", conn) + }() + dialer, err := proxy.SOCKS5("tcp", addr.String(), nil, proxy.Direct) + if err != nil { + t.Fatalf("error creating socks dialer: %v", err) + } + + // TODO: this is slow, flakey, and generally jank. Can we watch /proc for a + // new file descriptor or just poll until the listener is open? + t.Logf("waiting 3 seconds for command to start…") + time.Sleep(3 * time.Second) + + _, err = dialer.Dial("tcp", ln.Addr().String()) + if err != nil { + t.Fatalf("error dialing: %v", err) + } + + select {} +} +*/ diff --git a/server/test_data/obfs4.json b/server/test_data/obfs4.json new file mode 100644 index 0000000..eb4a1e8 --- /dev/null +++ b/server/test_data/obfs4.json @@ -0,0 +1 @@ +{"node-id":"f27b806cf27016b29cff6f4a7027cbe4b06e116c","private-key":"540bd146653b484bd84c5ec646f6ba8232ec1ec2d91320b4fd50ced640d9e139","public-key":"50ae30404985dd51bc1c9bd77da4ab1e18ae2bd93838ededb7fade96821c461b","drbg-seed":"b96054e02220e45ecd128a63ba64771c7b4a2bfb0aeda045","iat-mode":0} -- cgit v1.2.3