summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Whited <sam@samwhited.com>2022-03-04 11:55:08 -0500
committerSam Whited <sam@samwhited.com>2022-03-04 11:55:08 -0500
commit533def6748744789ccd49c36e6cf6c924944c9c4 (patch)
tree1298289bb75eae23190c474e123a53081f2ba861
parent90a2efcfdb8cb73704d9030719916e96633af816 (diff)
obfsvpn: initial draft library API
Signed-off-by: Sam Whited <sam@samwhited.com>
-rw-r--r--README.md5
-rw-r--r--dialer.go57
-rw-r--r--doc.go2
-rw-r--r--go.mod16
-rw-r--r--go.sum19
-rw-r--r--listener.go82
-rw-r--r--obfsvpn_test.go99
7 files changed, 280 insertions, 0 deletions
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)
+ }
+}