From 533def6748744789ccd49c36e6cf6c924944c9c4 Mon Sep 17 00:00:00 2001 From: Sam Whited Date: Fri, 4 Mar 2022 11:55:08 -0500 Subject: obfsvpn: initial draft library API Signed-off-by: Sam Whited --- README.md | 5 +++ dialer.go | 57 +++++++++++++++++++++++++++++++++ doc.go | 2 ++ go.mod | 16 ++++++++++ go.sum | 19 +++++++++++ listener.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++ obfsvpn_test.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 280 insertions(+) create mode 100644 README.md create mode 100644 dialer.go create mode 100644 doc.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 listener.go create mode 100644 obfsvpn_test.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..6db45fb --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# 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. diff --git a/dialer.go b/dialer.go new file mode 100644 index 0000000..0c3f8c3 --- /dev/null +++ b/dialer.go @@ -0,0 +1,57 @@ +package obfsvpn + +import ( + "context" + "net" + "strconv" + + pt "git.torproject.org/pluggable-transports/goptlib.git" + "gitlab.com/yawning/obfs4.git/common/ntor" + "gitlab.com/yawning/obfs4.git/transports/base" + "gitlab.com/yawning/obfs4.git/transports/obfs4" +) + +// IATMode determines the amount of time sent between packets. +type IATMode int + +// Valid IAT modes. +const ( + IATNone IATMode = iota + IATEnabled + IATParanoid +) + +// Dialer contains options for connecting to an address and obfuscating traffic +// with the obfs4 protocol. +// It performs the ntor handshake on all dialed connections. +type Dialer struct { + Dialer net.Dialer + + NodeID *ntor.NodeID + PublicKey *ntor.PublicKey + IATMode IATMode + + cf base.ClientFactory +} + +// Dial creates an outbound net.Conn and performs the ntor handshake. +func (d *Dialer) Dial(ctx context.Context, network, address string) (net.Conn, error) { + if d.cf == nil { + cf, err := (&obfs4.Transport{}).ClientFactory("") + if err != nil { + return nil, err + } + d.cf = cf + } + ptArgs := make(pt.Args) + ptArgs.Add("node-id", d.NodeID.Hex()) + ptArgs.Add("public-key", d.PublicKey.Hex()) + ptArgs.Add("iat-mode", strconv.Itoa(int(d.IATMode))) + args, err := d.cf.ParseArgs(&ptArgs) + if err != nil { + return nil, err + } + return d.cf.Dial(network, address, func(network, address string) (net.Conn, error) { + return d.Dialer.DialContext(ctx, network, address) + }, args) +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..76647e4 --- /dev/null +++ b/doc.go @@ -0,0 +1,2 @@ +// Package obfsvpn implements network connections using obfs4 and ntor. +package obfsvpn // import "0xacab.org/leap/obfsvpn" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c508bed --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module 0xacab.org/leap/obfsvpn + +go 1.17 + +require ( + git.torproject.org/pluggable-transports/goptlib.git v1.0.0 + gitlab.com/yawning/obfs4.git v0.0.0-20220204003609-77af0cba934d +) + +require ( + filippo.io/edwards25519 v1.0.0-rc.1.0.20210721174708-390f27c3be20 // indirect + github.com/dchest/siphash v1.2.1 // indirect + gitlab.com/yawning/edwards25519-extra.git v0.0.0-20211229043746-2f91fcc9fbdb // indirect + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b64b2f4 --- /dev/null +++ b/go.sum @@ -0,0 +1,19 @@ +filippo.io/edwards25519 v1.0.0-rc.1.0.20210721174708-390f27c3be20 h1:iJoUgXvhagsNMrJrvavw7vu1eG8+hm6jLOxlLFcoODw= +filippo.io/edwards25519 v1.0.0-rc.1.0.20210721174708-390f27c3be20/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +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/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4= +github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= +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-20220204003609-77af0cba934d h1:tJ8F7ABaQ3p3wjxwXiWSktVDgjZEXkvaRawd2rIq5ws= +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/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= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/listener.go b/listener.go new file mode 100644 index 0000000..ae81b19 --- /dev/null +++ b/listener.go @@ -0,0 +1,82 @@ +package obfsvpn + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/hex" + "net" + + pt "git.torproject.org/pluggable-transports/goptlib.git" + "gitlab.com/yawning/obfs4.git/common/ntor" + "gitlab.com/yawning/obfs4.git/transports/base" + "gitlab.com/yawning/obfs4.git/transports/obfs4" +) + +// ListenConfig contains options for listening to an address. +// If Seed is not set it defaults to a randomized value. +// If StateDir is not set the current working directory is used. +type ListenConfig struct { + ListenConfig net.ListenConfig + + NodeID *ntor.NodeID + PrivateKey *ntor.PrivateKey + Seed [ntor.KeySeedLength]byte + StateDir string +} + +// Listen announces on the local network address. +// +// See func net.Dial for a description of the network and address parameters. +func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (*Listener, error) { + ln, err := lc.ListenConfig.Listen(ctx, network, address) + if err != nil { + return nil, err + } + args := make(pt.Args) + args.Add("node-id", lc.NodeID.Hex()) + args.Add("private-key", lc.PrivateKey.Hex()) + seed := ntor.KeySeed{} + if bytes.Equal(lc.Seed[:], seed[:]) { + _, err = rand.Read(seed[:]) + if err != nil { + return nil, err + } + } else { + seed = lc.Seed + } + args.Add("drbg-seed", hex.EncodeToString(seed[:])) + sf, err := (&obfs4.Transport{}).ServerFactory(lc.StateDir, &args) + if err != nil { + return nil, err + } + return &Listener{sf: sf, ln: ln}, nil +} + +// Listener is a network listener that accepts obfuscated connections and +// performs the ntor handshake on them. +type Listener struct { + sf base.ServerFactory + ln net.Listener +} + +// Accept waits for and returns the next connection to the listener. +func (l *Listener) Accept() (net.Conn, error) { + conn, err := l.ln.Accept() + if err != nil { + return nil, err + } + conn, err = l.sf.WrapConn(conn) + return conn, err +} + +// Close closes the listener. +// Any blcked Accept operations will be unblocked and return errors. +func (l *Listener) Close() error { + return l.ln.Close() +} + +// Addr returns the listener's network address. +func (l *Listener) Addr() net.Addr { + return l.ln.Addr() +} diff --git a/obfsvpn_test.go b/obfsvpn_test.go new file mode 100644 index 0000000..c627500 --- /dev/null +++ b/obfsvpn_test.go @@ -0,0 +1,99 @@ +package obfsvpn_test + +import ( + "context" + "io" + "testing" + "time" + + "gitlab.com/yawning/obfs4.git/common/ntor" + + "0xacab.org/leap/obfsvpn" +) + +func TestRoundTrip(t *testing.T) { + pair, err := ntor.NewKeypair(false) + if err != nil { + t.Fatalf("error generating keys: %v", err) + } + nodeID, err := ntor.NewNodeID(make([]byte, ntor.NodeIDLength)) + if err != nil { + t.Fatalf("error creating node ID: %v", err) + } + lc := obfsvpn.ListenConfig{ + NodeID: nodeID, + PrivateKey: pair.Private(), + StateDir: t.TempDir(), + } + ln, err := lc.Listen(context.Background(), "tcp", ":0") + if err != nil { + t.Fatalf("error listening for incoming connection: %v", err) + } + + const ( + clientSend = `Though they broke my legs, they gave me a crutch to walk.` + serverReply = `Her Majesty's a pretty nice girl, but she's pretty much obsolete.` + ) + + errs := make(chan error) + serverRecv := make([]byte, len(clientSend)) + go func() { + conn, err := ln.Accept() + if err != nil { + errs <- err + return + } + _, err = conn.Read(serverRecv) + if err != nil { + errs <- err + return + } + _, err = io.WriteString(conn, serverReply) + if err != nil { + errs <- err + return + } + }() + + select { + case err := <-errs: + t.Fatalf("error accepting connection: %v", err) + default: + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + d := obfsvpn.Dialer{ + NodeID: nodeID, + PublicKey: pair.Public(), + } + conn, err := d.Dial(ctx, "tcp", ln.Addr().String()) + if err != nil { + t.Fatalf("error dialing connection: %v", err) + } + _, err = io.WriteString(conn, clientSend) + if err != nil { + t.Fatalf("error writing client side: %v", err) + } + select { + case err := <-errs: + t.Fatalf("error reading server side: %v", err) + default: + } + clientRecv := make([]byte, len(serverReply)) + _, err = conn.Read(clientRecv) + if err != nil { + t.Fatalf("error reading client side: %v", err) + } + select { + case err := <-errs: + t.Fatalf("error writing server side: %v", err) + default: + } + + if s := string(clientRecv); s != serverReply { + t.Fatalf("wrong response from server: want=%q, got=%q", serverReply, s) + } + if s := string(serverRecv); s != clientSend { + t.Fatalf("wrong request from client: want=%q, got=%q", clientSend, s) + } +} -- cgit v1.2.3