From 6498e0ce00329a6a652ebc85ec4d432a86404b06 Mon Sep 17 00:00:00 2001 From: Sam Whited Date: Thu, 17 Mar 2022 16:05:00 -0400 Subject: obfsproxy: add initial proxy implementation Fixes #3 Signed-off-by: Sam Whited --- .gitlab-ci.yml | 2 +- go.mod | 1 + go.sum | 1 + obfsproxy/Makefile | 18 ++++++ obfsproxy/main.go | 148 +++++++++++++++++++++++++++++++++++++++++++++++++ obfsproxy/main_test.go | 92 ++++++++++++++++++++++++++++++ 6 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 obfsproxy/Makefile create mode 100644 obfsproxy/main.go create mode 100644 obfsproxy/main_test.go diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3c830e6..830e638 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -33,7 +33,7 @@ validate: staticcheck -checks inherit,ST1000,ST1016,ST1020,ST1021,ST1022,ST1023 ./... # gosec does not handle modules correctly. # See: https://github.com/securego/gosec/issues/622 - gosec -exclude-dir=obfsproxy ./... + gosec ./... go mod tidy git diff --exit-code -- go.mod go.sum diff --git a/go.mod b/go.mod index c508bed..5514555 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.17 require ( git.torproject.org/pluggable-transports/goptlib.git v1.0.0 gitlab.com/yawning/obfs4.git v0.0.0-20220204003609-77af0cba934d + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 ) require ( diff --git a/go.sum b/go.sum index b64b2f4..031a40c 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,7 @@ gitlab.com/yawning/obfs4.git v0.0.0-20220204003609-77af0cba934d h1:tJ8F7ABaQ3p3w gitlab.com/yawning/obfs4.git v0.0.0-20220204003609-77af0cba934d/go.mod h1:9GcM8QNU9/wXtEEH2q8bVOnPI7FtIF6VVLzZ1l6Hgf8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= diff --git a/obfsproxy/Makefile b/obfsproxy/Makefile new file mode 100644 index 0000000..c0ad4bc --- /dev/null +++ b/obfsproxy/Makefile @@ -0,0 +1,18 @@ +GW=37.218.241.98 + +certs: + curl -k https://black.riseup.net/ca.crt > /tmp/ca.crt + curl -k https://api.black.riseup.net/3/cert > /tmp/cert.pem + +proxy: certs + #./obfsproxy -proxy=${GW}:4430 -addr=127.0.0.1:4430 + GW=${GW} ./leap-vpn.sh + +check: + curl https://wtfismyip.com/json + +stop: + pkill -9 shape + +obfsproxy: + go build diff --git a/obfsproxy/main.go b/obfsproxy/main.go new file mode 100644 index 0000000..a35a26d --- /dev/null +++ b/obfsproxy/main.go @@ -0,0 +1,148 @@ +// The obfsproxy command creates a SOCKS5 obfuscating proxy. +package main + +import ( + "context" + "encoding/json" + "flag" + "io" + "log" + "net" + "os" + "os/signal" + + "0xacab.org/leap/obfsvpn" + + "git.torproject.org/pluggable-transports/goptlib.git" +) + +const transportName = "obfs4" + +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) + if err != nil { + logger.Fatalf("error resolving VPN address: %v", err) + } + + fd, err := os.Open(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) + } + + // Configure logging. + if verbose { + debug.SetOutput(os.Stderr) + } + + // 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, + } + 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, net.Dialer{}, logger, debug) + } +} + +func proxyConn(ctx context.Context, info *pt.ServerInfo, conn net.Conn, d net.Dialer, logger, debug *log.Logger) { + defer func() { + err := conn.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) + 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 + } + }() +} diff --git a/obfsproxy/main_test.go b/obfsproxy/main_test.go new file mode 100644 index 0000000..d22d3af --- /dev/null +++ b/obfsproxy/main_test.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "flag" + "net" + "os" + "os/exec" + "testing" + "time" + + "golang.org/x/net/proxy" +) + +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 {} +} -- cgit v1.2.3