summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/Makefile11
-rw-r--r--server/main.go245
-rw-r--r--server/main_test.go90
-rw-r--r--server/test_data/obfs4.json1
4 files changed, 347 insertions, 0 deletions
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}