summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkali <kali@win>2021-04-06 13:02:07 +0200
committerkali kaneko (leap communications) <kali@leap.se>2022-07-31 03:40:47 -0400
commita85959a43eeeeab7181e0f54165c1b93e9449fb7 (patch)
tree9c377b6efe80da879c588675c4d45fab6f0582b5
parentd1252ef5b90870e55389188d351aabfe55a932f6 (diff)
[feat] send password to management interface
- Resolves: #104
-rw-r--r--go.mod4
-rw-r--r--pkg/bitmask/init.go1
-rw-r--r--pkg/vpn/demux/demuxer.go83
-rw-r--r--pkg/vpn/demux/demuxer_test.go238
-rw-r--r--pkg/vpn/demux/doc.go12
-rw-r--r--pkg/vpn/launcher_windows.go150
-rw-r--r--pkg/vpn/main.go5
-rw-r--r--pkg/vpn/management/client.go362
-rw-r--r--pkg/vpn/management/error.go32
-rw-r--r--pkg/vpn/management/event.go320
-rw-r--r--pkg/vpn/management/event_test.go367
-rw-r--r--pkg/vpn/management/server.go193
-rw-r--r--pkg/vpn/openvpn.go51
-rw-r--r--pkg/vpn/status.go15
14 files changed, 1743 insertions, 90 deletions
diff --git a/go.mod b/go.mod
index 3662b12..60c2d29 100644
--- a/go.mod
+++ b/go.mod
@@ -8,9 +8,13 @@ require (
0xacab.org/leap/obfsvpn v0.0.0-20220626143947-feff527c00e5
git.torproject.org/pluggable-transports/goptlib.git v1.2.0
git.torproject.org/pluggable-transports/snowflake.git v1.1.0
+ github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
+ github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
github.com/apparentlymart/go-openvpn-mgmt v0.0.0-20200929191752-4d2ce95ae600
github.com/cretz/bine v0.2.0
+ github.com/dchest/siphash v1.2.1 // indirect
+ github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19
github.com/pion/webrtc/v3 v3.1.41
github.com/sevlyar/go-daemon v0.1.5
diff --git a/pkg/bitmask/init.go b/pkg/bitmask/init.go
index a03a64b..54fb344 100644
--- a/pkg/bitmask/init.go
+++ b/pkg/bitmask/init.go
@@ -142,6 +142,7 @@ func maybeStartVPN(b Bitmask, conf *config.Config) error {
}
if b.CanStartVPN() {
+ log.Println("DEBUG starting")
err := b.StartVPN(config.Provider)
conf.SetUserStoppedVPN(false)
return err
diff --git a/pkg/vpn/demux/demuxer.go b/pkg/vpn/demux/demuxer.go
new file mode 100644
index 0000000..fcf43a4
--- /dev/null
+++ b/pkg/vpn/demux/demuxer.go
@@ -0,0 +1,83 @@
+// Copyright (c) 2016 Martin Atkins
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package demux
+
+import (
+ "bufio"
+ "io"
+)
+
+var readErrSynthEvent = []byte("FATAL:Error reading from OpenVPN")
+
+// Demultiplex reads from the given io.Reader, assumed to be the client
+// end of an OpenVPN Management Protocol connection, and splits it into
+// distinct messages from OpenVPN.
+//
+// It then writes the raw message buffers into either replyCh or eventCh
+// depending on whether each message is a reply to a client command or
+// an asynchronous event notification.
+//
+// The buffers written to replyCh are entire raw message lines (without the
+// trailing newlines), while the buffers written to eventCh are the raw
+// event strings with the prototcol's leading '>' indicator omitted.
+//
+// The caller should usually provide buffered channels of sufficient buffer
+// depth so that the reply channel will not be starved by slow event
+// processing.
+//
+// Once the io.Reader signals EOF, eventCh will be closed, then replyCh
+// will be closed, and then this function will return.
+//
+// As a special case, if a non-EOF error occurs while reading from the
+// io.Reader then a synthetic "FATAL" event will be written to eventCh
+// before the two buffers are closed and the function returns. This
+// synthetic message will have the error message "Error reading from OpenVPN".
+func Demultiplex(r io.Reader, replyCh, eventCh chan<- []byte) {
+ scanner := bufio.NewScanner(r)
+ for scanner.Scan() {
+ buf := scanner.Bytes()
+
+ if len(buf) < 1 {
+ // Should never happen but we'll be robust and ignore this,
+ // rather than crashing below.
+ continue
+ }
+
+ // Asynchronous messages always start with > to differentiate
+ // them from replies.
+ if buf[0] == '>' {
+ // Trim off the > when we post the message, since it's
+ // redundant after we've demuxed.
+ eventCh <- buf[1:]
+ } else {
+ replyCh <- buf
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ // Generate a synthetic FATAL event so that the caller can
+ // see that the connection was not gracefully closed.
+ eventCh <- readErrSynthEvent
+ }
+
+ close(eventCh)
+ close(replyCh)
+}
diff --git a/pkg/vpn/demux/demuxer_test.go b/pkg/vpn/demux/demuxer_test.go
new file mode 100644
index 0000000..096e35a
--- /dev/null
+++ b/pkg/vpn/demux/demuxer_test.go
@@ -0,0 +1,238 @@
+// Copyright (c) 2016 Martin Atkins
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package demux
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "reflect"
+ "testing"
+)
+
+func TestDemultiplex(t *testing.T) {
+ type TestCase struct {
+ Input []string
+ ExpectedReplies []string
+ ExpectedEvents []string
+ }
+
+ testCases := []TestCase{
+ {
+ Input: []string{},
+ ExpectedReplies: []string{},
+ ExpectedEvents: []string{},
+ },
+ {
+ Input: []string{
+ "SUCCESS: foo bar baz",
+ },
+ ExpectedReplies: []string{
+ "SUCCESS: foo bar baz",
+ },
+ ExpectedEvents: []string{},
+ },
+ {
+ Input: []string{
+ ">STATE:1234,ASSIGN_IP,,10.0.0.1,",
+ },
+ ExpectedReplies: []string{},
+ ExpectedEvents: []string{
+ "STATE:1234,ASSIGN_IP,,10.0.0.1,",
+ },
+ },
+ {
+ Input: []string{
+ ">STATE:1234,ASSIGN_IP,,10.0.0.1,",
+ ">STATE:5678,ASSIGN_IP,,10.0.0.1,",
+ ">STATE:9012,ASSIGN_IP,,10.0.0.1,",
+ },
+ ExpectedReplies: []string{},
+ ExpectedEvents: []string{
+ "STATE:1234,ASSIGN_IP,,10.0.0.1,",
+ "STATE:5678,ASSIGN_IP,,10.0.0.1,",
+ "STATE:9012,ASSIGN_IP,,10.0.0.1,",
+ },
+ },
+ {
+ Input: []string{
+ ">STATE:1234,ASSIGN_IP,,10.0.0.1,",
+ "SUCCESS: foo bar baz",
+ ">STATE:5678,ASSIGN_IP,,10.0.0.1,",
+ },
+ ExpectedReplies: []string{
+ "SUCCESS: foo bar baz",
+ },
+ ExpectedEvents: []string{
+ "STATE:1234,ASSIGN_IP,,10.0.0.1,",
+ "STATE:5678,ASSIGN_IP,,10.0.0.1,",
+ },
+ },
+ {
+ Input: []string{
+ "SUCCESS: foo bar baz",
+ ">STATE:1234,ASSIGN_IP,,10.0.0.1,",
+ "SUCCESS: baz bar foo",
+ },
+ ExpectedReplies: []string{
+ "SUCCESS: foo bar baz",
+ "SUCCESS: baz bar foo",
+ },
+ ExpectedEvents: []string{
+ "STATE:1234,ASSIGN_IP,,10.0.0.1,",
+ },
+ },
+ }
+
+ for i, testCase := range testCases {
+ r := mockReader(testCase.Input)
+ gotReplies, gotEvents := captureMsgs(r)
+
+ if !reflect.DeepEqual(gotReplies, testCase.ExpectedReplies) {
+ t.Errorf(
+ "test %d returned incorrect replies\ngot %#v\nwant %#v",
+ i, gotReplies, testCase.ExpectedReplies,
+ )
+ }
+
+ if !reflect.DeepEqual(gotEvents, testCase.ExpectedEvents) {
+ t.Errorf(
+ "test %d returned incorrect events\ngot %#v\nwant %#v",
+ i, gotEvents, testCase.ExpectedEvents,
+ )
+ }
+ }
+}
+
+func TestDemultiplex_error(t *testing.T) {
+ r := &alwaysErroringReader{}
+
+ gotReplies, gotEvents := captureMsgs(r)
+
+ expectedReplies := []string{}
+ expectedEvents := []string{
+ "FATAL:Error reading from OpenVPN",
+ }
+
+ if !reflect.DeepEqual(gotReplies, expectedReplies) {
+ t.Errorf(
+ "incorrect replies\ngot %#v\nwant %#v",
+ gotReplies, expectedReplies,
+ )
+ }
+
+ if !reflect.DeepEqual(gotEvents, expectedEvents) {
+ t.Errorf(
+ "incorrect events\ngot %#v\nwant %#v",
+ gotEvents, expectedEvents,
+ )
+ }
+}
+
+func mockReader(msgs []string) io.Reader {
+ var buf []byte
+ for _, msg := range msgs {
+ buf = append(buf, []byte(msg)...)
+ buf = append(buf, '\n')
+ }
+ return bytes.NewReader(buf)
+}
+
+func captureMsgs(r io.Reader) (replies, events []string) {
+ replyCh := make(chan []byte)
+ eventCh := make(chan []byte)
+
+ replies = make([]string, 0)
+ events = make([]string, 0)
+
+ go Demultiplex(r, replyCh, eventCh)
+
+ for replyCh != nil || eventCh != nil {
+ select {
+
+ case msg, ok := <-replyCh:
+ if ok {
+ replies = append(replies, string(msg))
+ } else {
+ replyCh = nil
+ }
+
+ case msg, ok := <-eventCh:
+ if ok {
+ events = append(events, string(msg))
+ } else {
+ eventCh = nil
+ }
+
+ }
+
+ }
+
+ return replies, events
+}
+
+type alwaysErroringReader struct{}
+
+func (r *alwaysErroringReader) Read(buf []byte) (int, error) {
+ return 0, fmt.Errorf("mock error")
+}
+
+// Somewhat-contrived example of blocking for a reply while concurrently
+// processing asynchronous events.
+func ExampleDemultiplex() {
+ // In a real caller we would have a net.IPConn as our reader,
+ // but we'll use a bytes reader here as a test.
+ r := bytes.NewReader([]byte(
+ // A reply to a hypothetical command interspersed between
+ // two asynchronous events.
+ ">HOLD:Waiting for hold release\nSUCCESS: foo\n>FATAL:baz\n",
+ ))
+
+ // No strong need for buffering on this channel because usually
+ // a message sender will immediately block waiting for the
+ // associated response message.
+ replyCh := make(chan []byte)
+
+ // Make sure the event channel buffer is deep enough that slow event
+ // processing won't significantly delay synchronous replies. If you
+ // process events quickly, or if you aren't sending any commands
+ // concurrently with acting on events, then this is not so important.
+ eventCh := make(chan []byte, 10)
+
+ // Start demultiplexing the message stream in the background.
+ // This goroutine will exit once the reader signals EOF.
+ go Demultiplex(r, replyCh, eventCh)
+
+ // Some coroutine has sent a hypothetical message to OpenVPN,
+ // and it can directly block until the associated reply arrives.
+ // The events will be concurrently handled by our event loop
+ // below while we wait for the reply to show up.
+ go func() {
+ replyMsgBuf := <-replyCh
+ fmt.Printf("Command reply: %s\n", string(replyMsgBuf))
+ }()
+
+ // Main event loop deals with the async events as they arrive,
+ // independently of any commands that are pending.
+ for msgBuf := range eventCh {
+ fmt.Printf("Event: %s\n", string(msgBuf))
+ }
+}
diff --git a/pkg/vpn/demux/doc.go b/pkg/vpn/demux/doc.go
new file mode 100644
index 0000000..263a267
--- /dev/null
+++ b/pkg/vpn/demux/doc.go
@@ -0,0 +1,12 @@
+// Package demux implements low-level demultiplexing of the stream of
+// messages sent from OpenVPN on the management channel.
+//
+// OpenVPN's protocol includes two different kinds of message from the OpenVPN
+// process: replies to commands sent by the management client, and asynchronous
+// event notifications.
+//
+// This package's purpose is to split these messages into two separate streams,
+// so that functions executing command/response sequences can just block
+// on the reply channel while an event loop elsewhere deals with any async
+// events that might show up.
+package demux
diff --git a/pkg/vpn/launcher_windows.go b/pkg/vpn/launcher_windows.go
index be5ef83..4f81ecd 100644
--- a/pkg/vpn/launcher_windows.go
+++ b/pkg/vpn/launcher_windows.go
@@ -1,4 +1,4 @@
-// +build windows
+// +build windows
// Copyright (C) 2018-2021 LEAP
//
// This program is free software: you can redistribute it and/or modify
@@ -17,19 +17,19 @@
package vpn
import (
+ "bufio"
+ "bytes"
+ "encoding/binary"
"errors"
+ "fmt"
"log"
"os"
"strings"
- "bufio"
- "fmt"
- "unicode/utf16"
- "bytes"
- "time"
- "encoding/binary"
+ "time"
+ "unicode/utf16"
- "github.com/natefinch/npipe"
"0xacab.org/leap/bitmask-vpn/pkg/vpn/bonafide"
+ "github.com/natefinch/npipe"
)
const pipeName = `\\.\pipe\openvpn\service`
@@ -49,49 +49,52 @@ func (l *launcher) close() error {
func (l *launcher) check() (helpers bool, privilege bool, err error) {
// TODO check if the named pipe exists
+ log.Println("bogus check on windows")
return true, true, nil
}
func (l *launcher) openvpnStart(flags ...string) error {
- var b bytes.Buffer
+ var b bytes.Buffer
+ /* DELETE-ME
var filtered []string
for _, v := range flags {
if v != "--tun-ipv6" {
filtered = append(filtered, v)
}
}
+ */
- cwd, _ := os.Getwd()
- opts := `--client --dev tun --block-outside-dns --redirect-gateway --script-security 0 ` + strings.Join(filtered, " ")
+ cwd, _ := os.Getwd()
+ opts := `--client --dev tun --block-outside-dns --redirect-gateway --script-security 0 ` + strings.Join(flags, " ")
log.Println("openvpn start: ", opts)
- timeout := 3 * time.Second
- conn, err := npipe.DialTimeout(pipeName, timeout)
- if err != nil {
- fmt.Println("ERROR opening pipe")
- return errors.New("cannot open openvpn pipe")
-
- }
- defer conn.Close()
-
- writeUTF16Bytes(&b, cwd)
- writeUTF16Bytes(&b, opts)
- writeUTF16Bytes(&b, `\n`)
- encoded := b.Bytes()
-
- rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
-
- _, err = rw.Write(encoded)
- if err != nil {
- fmt.Println("ERROR writing to pipe")
- return errors.New("cannot write to openvpn pipe")
- }
- rw.Flush()
- pid, err := getCommandResponse(rw)
- if err != nil {
- fmt.Println("ERROR getting pid")
- }
- fmt.Println("OpenVPN PID:", pid)
+ timeout := 3 * time.Second
+ conn, err := npipe.DialTimeout(pipeName, timeout)
+ if err != nil {
+ fmt.Println("ERROR opening pipe")
+ return errors.New("cannot open openvpn pipe")
+
+ }
+ defer conn.Close()
+
+ writeUTF16Bytes(&b, cwd)
+ writeUTF16Bytes(&b, opts)
+ writeUTF16Bytes(&b, `\n`)
+ encoded := b.Bytes()
+
+ rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
+
+ _, err = rw.Write(encoded)
+ if err != nil {
+ log.Println("ERROR writing to pipe")
+ return errors.New("cannot write to openvpn pipe")
+ }
+ rw.Flush()
+ pid, err := getCommandResponse(rw)
+ if err != nil {
+ log.Println("ERROR getting pid")
+ }
+ log.Println("OpenVPN PID:", pid)
return nil
}
@@ -102,57 +105,56 @@ func (l *launcher) openvpnStop() error {
// TODO we will have to bring our helper back to do firewall
func (l *launcher) firewallStart(gateways []bonafide.Gateway) error {
- log.Println("NO firewall in windows")
+ log.Println("start: no firewall in windows")
return nil
}
func (l *launcher) firewallStop() error {
- log.Println("NO firewall in windows")
+ log.Println("stop: no firewall in windows")
return nil
}
func (l *launcher) firewallIsUp() bool {
- log.Println("NO firewall in windows")
- return true
+ log.Println("up: no firewall in windows")
+ return false
}
-
func writeUTF16Bytes(b *bytes.Buffer, in string) {
- var u16 []uint16 = utf16.Encode([]rune(in + "\x00"))
- binary.Write(b, binary.LittleEndian, u16)
+ var u16 []uint16 = utf16.Encode([]rune(in + "\x00"))
+ binary.Write(b, binary.LittleEndian, u16)
}
func decodeUTF16String(s string) int {
- var code int
- var dec []byte
- for _, v := range []byte(s) {
- if byte(v) != byte(0) {
- dec = append(dec, v)
- }
- }
- _, err := fmt.Sscanf(string(dec), "%v", &code)
- if err != nil {
- fmt.Println("ERROR decoding")
- }
- return code
+ var code int
+ var dec []byte
+ for _, v := range []byte(s) {
+ if byte(v) != byte(0) {
+ dec = append(dec, v)
+ }
+ }
+ _, err := fmt.Sscanf(string(dec), "%v", &code)
+ if err != nil {
+ fmt.Println("ERROR decoding")
+ }
+ return code
}
func getCommandResponse(rw *bufio.ReadWriter) (int, error) {
- msg, err := rw.ReadString('\n')
- if err != nil {
- fmt.Println("ERROR reading")
- }
- ok := decodeUTF16String(msg)
- if ok != 0 {
- return -1, errors.New("command failed")
- }
- msg, err = rw.ReadString('\n')
- if err != nil {
- fmt.Println("ERROR reading")
- }
- pid := decodeUTF16String(msg)
- if pid == 0 {
- return -1, errors.New("command failed")
- }
- return pid, nil
+ msg, err := rw.ReadString('\n')
+ if err != nil {
+ fmt.Println("ERROR reading")
+ }
+ ok := decodeUTF16String(msg)
+ if ok != 0 {
+ return -1, errors.New("command failed")
+ }
+ msg, err = rw.ReadString('\n')
+ if err != nil {
+ fmt.Println("ERROR reading")
+ }
+ pid := decodeUTF16String(msg)
+ if pid == 0 {
+ return -1, errors.New("command failed")
+ }
+ return pid, nil
}
diff --git a/pkg/vpn/main.go b/pkg/vpn/main.go
index a97ab25..d0df2c6 100644
--- a/pkg/vpn/main.go
+++ b/pkg/vpn/main.go
@@ -27,8 +27,8 @@ import (
"0xacab.org/leap/bitmask-vpn/pkg/motd"
"0xacab.org/leap/bitmask-vpn/pkg/snowflake"
"0xacab.org/leap/bitmask-vpn/pkg/vpn/bonafide"
+ "0xacab.org/leap/bitmask-vpn/pkg/vpn/management"
obfsvpn "0xacab.org/leap/obfsvpn/client"
-
"github.com/apparentlymart/go-openvpn-mgmt/openvpn"
)
@@ -38,7 +38,7 @@ type Bitmask struct {
onGateway bonafide.Gateway
ptGateway bonafide.Gateway
statusCh chan string
- managementClient *openvpn.MgmtClient
+ managementClient *management.MgmtClient
bonafide *bonafide.Bonafide
launch *launcher
transport string
@@ -138,6 +138,7 @@ func (b *Bitmask) Close() {
if err != nil {
log.Printf("There was an error closing the launcher: %v", err)
}
+ time.Sleep(1 * time.Second)
err = os.RemoveAll(b.tempdir)
if err != nil {
log.Printf("There was an error removing temp dir: %v", err)
diff --git a/pkg/vpn/management/client.go b/pkg/vpn/management/client.go
new file mode 100644
index 0000000..e695416
--- /dev/null
+++ b/pkg/vpn/management/client.go
@@ -0,0 +1,362 @@
+// Copyright (c) 2016 Martin Atkins
+// Copyright (c) 2021 LEAP Encryption Access Project
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package management
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net"
+ "strconv"
+ "time"
+
+ "0xacab.org/leap/bitmask-vpn/pkg/vpn/demux"
+)
+
+var newline = []byte{'\n'}
+var successPrefix = []byte("SUCCESS: ")
+var errorPrefix = []byte("ERROR: ")
+var endMessage = []byte("END")
+
+// StatusFormat enum type
+type StatusFormat string
+
+// StatusFormatDefault openvpn default status format
+// StatusFormatV3 openvpn version 3 status format
+const (
+ StatusFormatDefault StatusFormat = ""
+ StatusFormatV3 StatusFormat = "3"
+)
+
+// MgmtClient .
+type MgmtClient struct {
+ wr io.Writer
+ replies <-chan []byte
+}
+
+// NewClient creates a new MgmtClient that communicates via the given
+// io.ReadWriter and emits events on the given channel.
+//
+// eventCh should be a buffered channel with a sufficient buffer depth
+// such that it cannot be filled under the expected event volume. Event
+// volume depends on which events are enabled and how they are configured;
+// some of the event-enabling functions have further discussion how frequently
+// events are likely to be emitted, but the caller should also factor in
+// how long its own event *processing* will take, since slow event
+// processing will create back-pressure that could cause this buffer to
+// fill faster.
+//
+// It probably goes without saying given the previous paragraph, but the
+// caller *must* constantly read events from eventCh to avoid its buffer
+// becoming full. Events and replies are received on the same channel
+// from OpenVPN, so if writing to eventCh blocks then this will also block
+// responses from the client's various command methods.
+//
+// eventCh will be closed to signal the closing of the client connection,
+// whether due to graceful shutdown or to an error. In the case of error,
+// a FatalEvent will be emitted on the channel as the last event before it
+// is closed. Connection errors may also concurrently surface as error
+// responses from the client's various command methods, should an error
+// occur while we await a reply.
+func NewClient(conn io.ReadWriter, eventCh chan<- Event) *MgmtClient {
+ replyCh := make(chan []byte)
+ rawEventCh := make(chan []byte) // not buffered because eventCh should be
+
+ go demux.Demultiplex(conn, replyCh, rawEventCh)
+
+ // Get raw events and upgrade them into proper event types before
+ // passing them on to the caller's event channel.
+ go func() {
+ for raw := range rawEventCh {
+ eventCh <- upgradeEvent(raw)
+ }
+ close(eventCh)
+ }()
+
+ return &MgmtClient{
+ // replyCh acts as the reader for our ReadWriter, so we only
+ // need to retain the io.Writer for it, so we can send commands.
+ wr: conn,
+ replies: replyCh,
+ }
+}
+
+// Dial is a convenience wrapper around NewClient that handles the common
+// case of opening an TCP/IP socket to an OpenVPN management port and creating
+// a client for it.
+//
+// See the NewClient docs for discussion about the requirements for eventCh.
+//
+// OpenVPN will create a suitable management port if launched with the
+// following command line option:
+//
+// --management <ipaddr> <port>
+//
+// Address may an IPv4 address, an IPv6 address, or a hostname that resolves
+// to either of these, followed by a colon and then a port number.
+//
+// When running on Unix systems it's possible to instead connect to a Unix
+// domain socket. To do this, pass an absolute path to the socket as
+// the target address, having run OpenVPN with the following options:
+//
+// --management /path/to/socket unix
+//
+func Dial(addr string, eventCh chan<- Event) (*MgmtClient, error) {
+ proto := "tcp"
+ if len(addr) > 0 && addr[0] == '/' {
+ proto = "unix"
+ }
+ conn, err := net.Dial(proto, addr)
+ if err != nil {
+ return nil, err
+ }
+
+ return NewClient(conn, eventCh), nil
+}
+
+// HoldRelease instructs OpenVPN to release any management hold preventing
+// it from proceeding, but to retain the state of the hold flag such that
+// the daemon will hold again if it needs to reconnect for any reason.
+//
+// OpenVPN can be instructed to activate a management hold on startup by
+// running it with the following option:
+//
+// --management-hold
+//
+// Instructing OpenVPN to hold gives your client a chance to connect and
+// do any necessary configuration before a connection proceeds, thus avoiding
+// the problem of missed events.
+//
+// When OpenVPN begins holding, or when a new management client connects while
+// a hold is already in effect, a HoldEvent will be emitted on the event
+// channel.
+func (c *MgmtClient) HoldRelease() error {
+ _, err := c.simpleCommand("hold release")
+ return err
+}
+
+// SetStateEvents either enables or disables asynchronous events for changes
+// in the OpenVPN connection state.
+//
+// When enabled, a StateEvent will be emitted from the event channel each
+// time the connection state changes. See StateEvent for more information
+// on the event structure.
+func (c *MgmtClient) SetStateEvents(on bool) error {
+ var err error
+ if on {
+ _, err = c.simpleCommand("state on")
+ } else {
+ _, err = c.simpleCommand("state off")
+ }
+ return err
+}
+
+// SetEchoEvents either enables or disables asynchronous events for "echo"
+// commands sent from a remote server to our managed OpenVPN client.
+//
+// When enabled, an EchoEvent will be emitted from the event channel each
+// time the server sends an echo command. See EchoEvent for more information.
+func (c *MgmtClient) SetEchoEvents(on bool) error {
+ var err error
+ if on {
+ _, err = c.simpleCommand("echo on")
+ } else {
+ _, err = c.simpleCommand("echo off")
+ }
+ return err
+}
+
+// SetByteCountEvents either enables or disables ongoing asynchronous events
+// for information on OpenVPN bandwidth usage.
+//
+// When enabled, a ByteCountEvent will be emitted at given time interval,
+// (which may only be whole seconds) describing how many bytes have been
+// transferred in each direction See ByteCountEvent for more information.
+//
+// Set the time interval to zero in order to disable byte count events.
+func (c *MgmtClient) SetByteCountEvents(interval time.Duration) error {
+ msg := fmt.Sprintf("bytecount %d", int(interval.Seconds()))
+ _, err := c.simpleCommand(msg)
+ return err
+}
+
+// SendSignal sends a signal to the OpenVPN process via the management
+// channel. In effect this causes the OpenVPN process to send a signal to
+// itself on our behalf.
+//
+// OpenVPN accepts a subset of the usual UNIX signal names, including
+// "SIGHUP", "SIGTERM", "SIGUSR1" and "SIGUSR2". See the OpenVPN manual
+// page for the meaning of each.
+//
+// Behavior is undefined if the given signal name is not entirely uppercase
+// letters. In particular, including newlines in the string is likely to
+// cause very unpredictable behavior.
+func (c *MgmtClient) SendSignal(name string) error {
+ msg := fmt.Sprintf("signal %q", name)
+ _, err := c.simpleCommand(msg)
+ return err
+}
+
+// LatestState retrieves the most recent StateEvent from the server. This
+// can either be used to poll the state or it can be used to determine the
+// initial state after calling SetStateEvents(true) but before the first
+// state event is delivered.
+func (c *MgmtClient) LatestState() (*StateEvent, error) {
+ err := c.sendCommand([]byte("state"))
+ if err != nil {
+ return nil, err
+ }
+
+ payload, err := c.readCommandResponsePayload()
+ if err != nil {
+ return nil, err
+ }
+
+ if len(payload) != 1 {
+ return nil, fmt.Errorf("Malformed OpenVPN 'state' response")
+ }
+
+ return &StateEvent{
+ body: payload[0],
+ }, nil
+}
+
+// LatestStatus retrieves the current daemon status information, in the same format as that produced by the OpenVPN --status directive.
+func (c *MgmtClient) LatestStatus(statusFormat StatusFormat) ([][]byte, error) {
+ var cmd []byte
+ if statusFormat == StatusFormatDefault {
+ cmd = []byte("status")
+ } else if statusFormat == StatusFormatV3 {
+ cmd = []byte("status 3")
+ } else {
+ return nil, fmt.Errorf("Incorrect 'status' format option")
+ }
+ err := c.sendCommand(cmd)
+ if err != nil {
+ return nil, err
+ }
+
+ payload, err := c.readCommandResponsePayload()
+ if err != nil {
+ return nil, err
+ }
+
+ return payload, nil
+}
+
+// Pid retrieves the process id of the connected OpenVPN process.
+func (c *MgmtClient) Pid() (int, error) {
+ raw, err := c.simpleCommand("pid")
+ if err != nil {
+ return 0, err
+ }
+
+ if !bytes.HasPrefix(raw, []byte("pid=")) {
+ return 0, fmt.Errorf("malformed response from OpenVPN")
+ }
+
+ pid, err := strconv.Atoi(string(raw[4:]))
+ if err != nil {
+ return 0, fmt.Errorf("error parsing pid from OpenVPN: %s", err)
+ }
+
+ return pid, nil
+}
+
+func (c *MgmtClient) SendPassword(pass string) ([]byte, error) {
+ return c.simpleCommand(pass + "\n")
+}
+
+func (c *MgmtClient) sendCommand(cmd []byte) error {
+ _, err := c.wr.Write(cmd)
+ if err != nil {
+ return err
+ }
+ _, err = c.wr.Write(newline)
+ return err
+}
+
+// sendCommandPayload can be called after sendCommand for
+// commands that expect a multi-line input payload.
+//
+// The buffer given in 'payload' *must* end with a newline,
+// or else the protocol will be broken.
+func (c *MgmtClient) sendCommandPayload(payload []byte) error {
+ _, err := c.wr.Write(payload)
+ if err != nil {
+ return err
+ }
+ _, err = c.wr.Write(endMessage)
+ if err != nil {
+ return err
+ }
+ _, err = c.wr.Write(newline)
+ return err
+}
+
+func (c *MgmtClient) readCommandResult() ([]byte, error) {
+ reply, ok := <-c.replies
+ if !ok {
+ return nil, fmt.Errorf("connection closed while awaiting result")
+ }
+
+ if bytes.HasPrefix(reply, successPrefix) {
+ result := reply[len(successPrefix):]
+ return result, nil
+ }
+
+ if bytes.HasPrefix(reply, errorPrefix) {
+ message := reply[len(errorPrefix):]
+ return nil, ErrorFromServer(message)
+ }
+
+ return nil, fmt.Errorf("malformed result message")
+}
+
+func (c *MgmtClient) readCommandResponsePayload() ([][]byte, error) {
+ lines := make([][]byte, 0, 10)
+
+ for {
+ line, ok := <-c.replies
+ if !ok {
+ // We'll give the caller whatever we got before the connection
+ // closed, in case it's useful for debugging.
+ return lines, fmt.Errorf("connection closed before END recieved")
+ }
+
+ if bytes.Equal(line, endMessage) {
+ break
+ }
+
+ lines = append(lines, line)
+ }
+
+ return lines, nil
+}
+
+func (c *MgmtClient) simpleCommand(cmd string) ([]byte, error) {
+ err := c.sendCommand([]byte(cmd))
+ if err != nil {
+ return nil, err
+ }
+ return c.readCommandResult()
+}
diff --git a/pkg/vpn/management/error.go b/pkg/vpn/management/error.go
new file mode 100644
index 0000000..1bc623d
--- /dev/null
+++ b/pkg/vpn/management/error.go
@@ -0,0 +1,32 @@
+// Copyright (c) 2016 Martin Atkins
+// Copyright (c) 2021 LEAP Encryption Access Project
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package management
+
+type ErrorFromServer []byte
+
+func (err ErrorFromServer) Error() string {
+ return string(err)
+}
+
+func (err ErrorFromServer) String() string {
+ return string(err)
+}
diff --git a/pkg/vpn/management/event.go b/pkg/vpn/management/event.go
new file mode 100644
index 0000000..cc9cf80
--- /dev/null
+++ b/pkg/vpn/management/event.go
@@ -0,0 +1,320 @@
+// Copyright (c) 2016 Martin Atkins
+// Copyright (c) 2021 LEAP Encryption Access Project
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package management
+
+import (
+ "bytes"
+ "fmt"
+ "strconv"
+)
+
+var eventSep = []byte(":")
+var fieldSep = []byte(",")
+var byteCountEventKW = []byte("BYTECOUNT")
+var byteCountCliEventKW = []byte("BYTECOUNT_CLI")
+var clientEventKW = []byte("CLIENT")
+var echoEventKW = []byte("ECHO")
+var fatalEventKW = []byte("FATAL")
+var holdEventKW = []byte("HOLD")
+var infoEventKW = []byte("INFO")
+var logEventKW = []byte("LOG")
+var needOkEventKW = []byte("NEED-OK")
+var needStrEventKW = []byte("NEED-STR")
+var passwordEventKW = []byte("PASSWORD")
+var stateEventKW = []byte("STATE")
+
+type Event interface {
+ String() string
+}
+
+// UnknownEvent represents an event of a type that this package doesn't
+// know about.
+//
+// Future versions of this library may learn about new event types, so a
+// caller should exercise caution when making use of events of this type
+// to access unsupported behavior. Backward-compatibility is *not*
+// guaranteed for events of this type.
+type UnknownEvent struct {
+ keyword []byte
+ body []byte
+}
+
+func (e *UnknownEvent) Type() string {
+ return string(e.keyword)
+}
+
+func (e *UnknownEvent) Body() string {
+ return string(e.body)
+}
+
+func (e *UnknownEvent) String() string {
+ return fmt.Sprintf("%s: %s", e.keyword, e.body)
+}
+
+// MalformedEvent represents a message from the OpenVPN process that is
+// presented as an event but does not comply with the expected event syntax.
+//
+// Events of this type should never be seen but robust callers will accept
+// and ignore them, possibly generating some kind of debugging message.
+//
+// One reason for potentially seeing events of this type is when the target
+// program is actually not an OpenVPN process at all, but in fact this client
+// has been connected to a different sort of server by mistake.
+type MalformedEvent struct {
+ raw []byte
+}
+
+func (e *MalformedEvent) String() string {
+ return fmt.Sprintf("Malformed Event %q", e.raw)
+}
+
+// HoldEvent is a notification that the OpenVPN process is in a management
+// hold and will not continue connecting until the hold is released, e.g.
+// by calling client.HoldRelease()
+type HoldEvent struct {
+ body []byte
+}
+
+func (e *HoldEvent) String() string {
+ return string(e.body)
+}
+
+// StateEvent is a notification of a change of connection state. It can be
+// used, for example, to detect if the OpenVPN connection has been interrupted
+// and the OpenVPN process is attempting to reconnect.
+type StateEvent struct {
+ body []byte
+
+ // bodyParts is populated only on first request, giving us the
+ // separate comma-separated elements of the message. Not all
+ // fields are populated for all states.
+ bodyParts [][]byte
+}
+
+func (e *StateEvent) RawTimestamp() string {
+ parts := e.parts()
+ return string(parts[0])
+}
+
+func (e *StateEvent) NewState() string {
+ parts := e.parts()
+ return string(parts[1])
+}
+
+func (e *StateEvent) Description() string {
+ parts := e.parts()
+ return string(parts[2])
+}
+
+// LocalTunnelAddr returns the IP address of the local interface within
+// the tunnel, as a string that can be parsed using net.ParseIP.
+//
+// This field is only populated for events whose NewState returns
+// either ASSIGN_IP or CONNECTED.
+func (e *StateEvent) LocalTunnelAddr() string {
+ parts := e.parts()
+ return string(parts[3])
+}
+
+// RemoteAddr returns the non-tunnel IP address of the remote
+// system that has connected to the local OpenVPN process.
+//
+// This field is only populated for events whose NewState returns
+// CONNECTED.
+func (e *StateEvent) RemoteAddr() string {
+ parts := e.parts()
+ return string(parts[4])
+}
+
+func (e *StateEvent) String() string {
+ newState := e.NewState()
+ switch newState {
+ case "ASSIGN_IP":
+ return fmt.Sprintf("%s: %s", newState, e.LocalTunnelAddr())
+ case "CONNECTED":
+ return fmt.Sprintf("%s: %s", newState, e.RemoteAddr())
+ default:
+ desc := e.Description()
+ if desc != "" {
+ return fmt.Sprintf("%s: %s", newState, desc)
+ } else {
+ return newState
+ }
+ }
+}
+
+func (e *StateEvent) parts() [][]byte {
+ if e.bodyParts == nil {
+ // State messages currently have only five segments, but
+ // we'll ask for 5 so any additional fields that might show
+ // up in newer versions will gather in an element we're
+ // not actually using.
+ e.bodyParts = bytes.SplitN(e.body, fieldSep, 6)
+
+ // Prevent crash if the server has sent us a malformed
+ // status message. This should never actually happen if
+ // the server is behaving itself.
+ if len(e.bodyParts) < 5 {
+ expanded := make([][]byte, 5)
+ copy(expanded, e.bodyParts)
+ e.bodyParts = expanded
+ }
+ }
+ return e.bodyParts
+}
+
+// EchoEvent is emitted by an OpenVPN process running in client mode when
+// an "echo" command is pushed to it by the server it has connected to.
+//
+// The format of the echo message is free-form, since this message type is
+// intended to pass application-specific data from the server-side config
+// into whatever client is consuming the management prototcol.
+//
+// This event is emitted only if the management client has turned on events
+// of this type using client.SetEchoEvents(true)
+type EchoEvent struct {
+ body []byte
+}
+
+func (e *EchoEvent) RawTimestamp() string {
+ sepIndex := bytes.Index(e.body, fieldSep)
+ if sepIndex == -1 {
+ return ""
+ }
+ return string(e.body[:sepIndex])
+}
+
+func (e *EchoEvent) Message() string {
+ sepIndex := bytes.Index(e.body, fieldSep)
+ if sepIndex == -1 {
+ return ""
+ }
+ return string(e.body[sepIndex+1:])
+}
+
+func (e *EchoEvent) String() string {
+ return fmt.Sprintf("ECHO: %s", e.Message())
+}
+
+// ByteCountEvent represents a periodic snapshot of data transfer in bytes
+// on a VPN connection.
+//
+// For OpenVPN *servers*, events are emitted for each client and the method
+// ClientId identifies thet client.
+//
+// For other OpenVPN modes, events are emitted only once per interval for the
+// single connection managed by the target process, and ClientId returns
+// the empty string.
+type ByteCountEvent struct {
+ hasClient bool
+ body []byte
+
+ // populated on first call to parts()
+ bodyParts [][]byte
+}
+
+func (e *ByteCountEvent) ClientId() string {
+ if !e.hasClient {
+ return ""
+ }
+
+ return string(e.parts()[0])
+}
+
+func (e *ByteCountEvent) BytesIn() int {
+ index := 0
+ if e.hasClient {
+ index = 1
+ }
+ str := string(e.parts()[index])
+ val, _ := strconv.Atoi(str)
+ // Ignore error, since this should never happen if OpenVPN is
+ // behaving itself.
+ return val
+}
+
+func (e *ByteCountEvent) BytesOut() int {
+ index := 1
+ if e.hasClient {
+ index = 2
+ }
+ str := string(e.parts()[index])
+ val, _ := strconv.Atoi(str)
+ // Ignore error, since this should never happen if OpenVPN is
+ // behaving itself.
+ return val
+}
+
+func (e *ByteCountEvent) String() string {
+ if e.hasClient {
+ return fmt.Sprintf("Client %s: %d in, %d out", e.ClientId(), e.BytesIn(), e.BytesOut())
+ } else {
+ return fmt.Sprintf("%d in, %d out", e.BytesIn(), e.BytesOut())
+ }
+}
+
+func (e *ByteCountEvent) parts() [][]byte {
+ if e.bodyParts == nil {
+ e.bodyParts = bytes.SplitN(e.body, fieldSep, 4)
+
+ wantCount := 2
+ if e.hasClient {
+ wantCount = 3
+ }
+
+ // Prevent crash if the server has sent us a malformed
+ // message. This should never actually happen if the
+ // server is behaving itself.
+ if len(e.bodyParts) < wantCount {
+ expanded := make([][]byte, wantCount)
+ copy(expanded, e.bodyParts)
+ e.bodyParts = expanded
+ }
+ }
+ return e.bodyParts
+}
+
+func upgradeEvent(raw []byte) Event {
+ splitIdx := bytes.Index(raw, eventSep)
+ if splitIdx == -1 {
+ // Should never happen, but we'll handle it robustly if it does.
+ return &MalformedEvent{raw}
+ }
+
+ keyword := raw[:splitIdx]
+ body := raw[splitIdx+1:]
+
+ switch {
+ case bytes.Equal(keyword, stateEventKW):
+ return &StateEvent{body: body}
+ case bytes.Equal(keyword, holdEventKW):
+ return &HoldEvent{body}
+ case bytes.Equal(keyword, echoEventKW):
+ return &EchoEvent{body}
+ case bytes.Equal(keyword, byteCountEventKW):
+ return &ByteCountEvent{hasClient: false, body: body}
+ case bytes.Equal(keyword, byteCountCliEventKW):
+ return &ByteCountEvent{hasClient: true, body: body}
+ default:
+ return &UnknownEvent{keyword, body}
+ }
+}
diff --git a/pkg/vpn/management/event_test.go b/pkg/vpn/management/event_test.go
new file mode 100644
index 0000000..5afc95f
--- /dev/null
+++ b/pkg/vpn/management/event_test.go
@@ -0,0 +1,367 @@
+// Copyright (c) 2016 Martin Atkins
+// Copyright (c) 2021 LEAP Encryption Access Project
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package management
+
+import (
+ "fmt"
+ "testing"
+)
+
+// A key requirement of our event parsing is that it must never cause a
+// panic, even if the OpenVPN process sends us malformed garbage.
+//
+// Therefore most of the tests in here are testing various tortured error
+// cases, which are all expected to produce an event object, though the
+// contents of that event object will be nonsensical if the OpenVPN server
+// sends something nonsensical.
+
+func TestMalformedEvent(t *testing.T) {
+ testCases := [][]byte{
+ []byte(""),
+ []byte("HTTP/1.1 200 OK"),
+ []byte(" "),
+ []byte("\x00"),
+ }
+
+ for i, testCase := range testCases {
+ event := upgradeEvent(testCase)
+
+ var malformed *MalformedEvent
+ var ok bool
+ if malformed, ok = event.(*MalformedEvent); !ok {
+ t.Errorf("test %d got %T; want %T", i, event, malformed)
+ continue
+ }
+
+ wantString := fmt.Sprintf("Malformed Event %q", testCase)
+ if gotString := malformed.String(); gotString != wantString {
+ t.Errorf("test %d String returned %q; want %q", i, gotString, wantString)
+ }
+ }
+}
+
+func TestUnknownEvent(t *testing.T) {
+ type TestCase struct {
+ Input []byte
+ WantType string
+ WantBody string
+ }
+ testCases := []TestCase{
+ {
+ Input: []byte("DUMMY:baz"),
+ WantType: "DUMMY",
+ WantBody: "baz",
+ },
+ {
+ Input: []byte("DUMMY:"),
+ WantType: "DUMMY",
+ WantBody: "",
+ },
+ {
+ Input: []byte("DUMMY:abc,123,456"),
+ WantType: "DUMMY",
+ WantBody: "abc,123,456",
+ },
+ }
+
+ for i, testCase := range testCases {
+ event := upgradeEvent(testCase.Input)
+
+ var unk *UnknownEvent
+ var ok bool
+ if unk, ok = event.(*UnknownEvent); !ok {
+ t.Errorf("test %d got %T; want %T", i, event, unk)
+ continue
+ }
+
+ if got, want := unk.Type(), testCase.WantType; got != want {
+ t.Errorf("test %d Type returned %q; want %q", i, got, want)
+ }
+ if got, want := unk.Body(), testCase.WantBody; got != want {
+ t.Errorf("test %d Body returned %q; want %q", i, got, want)
+ }
+ }
+}
+
+func TestHoldEvent(t *testing.T) {
+ testCases := [][]byte{
+ []byte("HOLD:"),
+ []byte("HOLD:waiting for hold release"),
+ }
+
+ for i, testCase := range testCases {
+ event := upgradeEvent(testCase)
+
+ var hold *HoldEvent
+ var ok bool
+ if hold, ok = event.(*HoldEvent); !ok {
+ t.Errorf("test %d got %T; want %T", i, event, hold)
+ continue
+ }
+ }
+}
+
+func TestEchoEvent(t *testing.T) {
+ type TestCase struct {
+ Input []byte
+ WantTimestamp string
+ WantMessage string
+ }
+ testCases := []TestCase{
+ {
+ Input: []byte("ECHO:123,foo"),
+ WantTimestamp: "123",
+ WantMessage: "foo",
+ },
+ {
+ Input: []byte("ECHO:123,"),
+ WantTimestamp: "123",
+ WantMessage: "",
+ },
+ {
+ Input: []byte("ECHO:,foo"),
+ WantTimestamp: "",
+ WantMessage: "foo",
+ },
+ {
+ Input: []byte("ECHO:,"),
+ WantTimestamp: "",
+ WantMessage: "",
+ },
+ {
+ Input: []byte("ECHO:"),
+ WantTimestamp: "",
+ WantMessage: "",
+ },
+ }
+
+ for i, testCase := range testCases {
+ event := upgradeEvent(testCase.Input)
+
+ var echo *EchoEvent
+ var ok bool
+ if echo, ok = event.(*EchoEvent); !ok {
+ t.Errorf("test %d got %T; want %T", i, event, echo)
+ continue
+ }
+
+ if got, want := echo.RawTimestamp(), testCase.WantTimestamp; got != want {
+ t.Errorf("test %d RawTimestamp returned %q; want %q", i, got, want)
+ }
+ if got, want := echo.Message(), testCase.WantMessage; got != want {
+ t.Errorf("test %d Message returned %q; want %q", i, got, want)
+ }
+ }
+}
+
+func TestStateEvent(t *testing.T) {
+ type TestCase struct {
+ Input []byte
+ WantTimestamp string
+ WantState string
+ WantDesc string
+ WantLocalAddr string
+ WantRemoteAddr string
+ }
+ testCases := []TestCase{
+ {
+ Input: []byte("STATE:"),
+ WantTimestamp: "",
+ WantState: "",
+ WantDesc: "",
+ WantLocalAddr: "",
+ WantRemoteAddr: "",
+ },
+ {
+ Input: []byte("STATE:,"),
+ WantTimestamp: "",
+ WantState: "",
+ WantDesc: "",
+ WantLocalAddr: "",
+ WantRemoteAddr: "",
+ },
+ {
+ Input: []byte("STATE:,,,,"),
+ WantTimestamp: "",
+ WantState: "",
+ WantDesc: "",
+ WantLocalAddr: "",
+ WantRemoteAddr: "",
+ },
+ {
+ Input: []byte("STATE:123,CONNECTED,good,172.16.0.1,192.168.4.1"),
+ WantTimestamp: "123",
+ WantState: "CONNECTED",
+ WantDesc: "good",
+ WantLocalAddr: "172.16.0.1",
+ WantRemoteAddr: "192.168.4.1",
+ },
+ {
+ Input: []byte("STATE:123,RECONNECTING,SIGHUP,,"),
+ WantTimestamp: "123",
+ WantState: "RECONNECTING",
+ WantDesc: "SIGHUP",
+ WantLocalAddr: "",
+ WantRemoteAddr: "",
+ },
+ {
+ Input: []byte("STATE:123,RECONNECTING,SIGHUP,,,extra"),
+ WantTimestamp: "123",
+ WantState: "RECONNECTING",
+ WantDesc: "SIGHUP",
+ WantLocalAddr: "",
+ WantRemoteAddr: "",
+ },
+ }
+
+ for i, testCase := range testCases {
+ event := upgradeEvent(testCase.Input)
+
+ var st *StateEvent
+ var ok bool
+ if st, ok = event.(*StateEvent); !ok {
+ t.Errorf("test %d got %T; want %T", i, event, st)
+ continue
+ }
+
+ if got, want := st.RawTimestamp(), testCase.WantTimestamp; got != want {
+ t.Errorf("test %d RawTimestamp returned %q; want %q", i, got, want)
+ }
+ if got, want := st.NewState(), testCase.WantState; got != want {
+ t.Errorf("test %d NewState returned %q; want %q", i, got, want)
+ }
+ if got, want := st.Description(), testCase.WantDesc; got != want {
+ t.Errorf("test %d Description returned %q; want %q", i, got, want)
+ }
+ if got, want := st.LocalTunnelAddr(), testCase.WantLocalAddr; got != want {
+ t.Errorf("test %d LocalTunnelAddr returned %q; want %q", i, got, want)
+ }
+ if got, want := st.RemoteAddr(), testCase.WantRemoteAddr; got != want {
+ t.Errorf("test %d RemoteAddr returned %q; want %q", i, got, want)
+ }
+ }
+}
+
+func TestByteCountEvent(t *testing.T) {
+ type TestCase struct {
+ Input []byte
+ WantClientId string
+ WantBytesIn int
+ WantBytesOut int
+ }
+ testCases := []TestCase{
+ {
+ Input: []byte("BYTECOUNT:"),
+ WantClientId: "",
+ WantBytesIn: 0,
+ WantBytesOut: 0,
+ },
+ {
+ Input: []byte("BYTECOUNT:123,456"),
+ WantClientId: "",
+ WantBytesIn: 123,
+ WantBytesOut: 456,
+ },
+ {
+ Input: []byte("BYTECOUNT:,"),
+ WantClientId: "",
+ WantBytesIn: 0,
+ WantBytesOut: 0,
+ },
+ {
+ Input: []byte("BYTECOUNT:5,"),
+ WantClientId: "",
+ WantBytesIn: 5,
+ WantBytesOut: 0,
+ },
+ {
+ Input: []byte("BYTECOUNT:,6"),
+ WantClientId: "",
+ WantBytesIn: 0,
+ WantBytesOut: 6,
+ },
+ {
+ Input: []byte("BYTECOUNT:6"),
+ WantClientId: "",
+ WantBytesIn: 6,
+ WantBytesOut: 0,
+ },
+ {
+ Input: []byte("BYTECOUNT:wrong,bad"),
+ WantClientId: "",
+ WantBytesIn: 0,
+ WantBytesOut: 0,
+ },
+ {
+ Input: []byte("BYTECOUNT:1,2,3"),
+ WantClientId: "",
+ WantBytesIn: 1,
+ WantBytesOut: 2,
+ },
+ {
+ // Intentionally malformed BYTECOUNT event sent as BYTECOUNT_CLI
+ Input: []byte("BYTECOUNT_CLI:123,456"),
+ WantClientId: "123",
+ WantBytesIn: 456,
+ WantBytesOut: 0,
+ },
+ {
+ Input: []byte("BYTECOUNT_CLI:"),
+ WantClientId: "",
+ WantBytesIn: 0,
+ WantBytesOut: 0,
+ },
+ {
+ Input: []byte("BYTECOUNT_CLI:abc123,123,456"),
+ WantClientId: "abc123",
+ WantBytesIn: 123,
+ WantBytesOut: 456,
+ },
+ {
+ Input: []byte("BYTECOUNT_CLI:abc123,123"),
+ WantClientId: "abc123",
+ WantBytesIn: 123,
+ WantBytesOut: 0,
+ },
+ }
+
+ for i, testCase := range testCases {
+ event := upgradeEvent(testCase.Input)
+
+ var bc *ByteCountEvent
+ var ok bool
+ if bc, ok = event.(*ByteCountEvent); !ok {
+ t.Errorf("test %d got %T; want %T", i, event, bc)
+ continue
+ }
+
+ if got, want := bc.ClientId(), testCase.WantClientId; got != want {
+ t.Errorf("test %d ClientId returned %q; want %q", i, got, want)
+ }
+ if got, want := bc.BytesIn(), testCase.WantBytesIn; got != want {
+ t.Errorf("test %d BytesIn returned %d; want %d", i, got, want)
+ }
+ if got, want := bc.BytesOut(), testCase.WantBytesOut; got != want {
+ t.Errorf("test %d BytesOut returned %d; want %d", i, got, want)
+ }
+ }
+}
diff --git a/pkg/vpn/management/server.go b/pkg/vpn/management/server.go
new file mode 100644
index 0000000..f9183a7
--- /dev/null
+++ b/pkg/vpn/management/server.go
@@ -0,0 +1,193 @@
+// Copyright (c) 2016 Martin Atkins
+// Copyright (c) 2021 LEAP Encryption Access Project
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package management
+
+import (
+ "net"
+ "time"
+)
+
+// MgmtListener accepts incoming connections from OpenVPN.
+//
+// The primary way to instantiate this type is via the function Listen.
+// See its documentation for more information.
+type MgmtListener struct {
+ l net.Listener
+}
+
+// NewMgmtListener constructs a MgmtListener from an already-established
+// net.Listener. In most cases it will be more convenient to use
+// the function Listen.
+func NewMgmtListener(l net.Listener) *MgmtListener {
+ return &MgmtListener{l}
+}
+
+// Listen opens a listen port and awaits incoming connections from OpenVPN
+// processes.
+//
+// OpenVPN will behave in this manner when launched with the following options:
+//
+// --management ipaddr port --management-client
+//
+// Note that in this case the terminology is slightly confusing, since from
+// the standpoint of TCP/IP it is OpenVPN that is the client and our program
+// that is the server, but once the connection is established the channel
+// is indistinguishable from the situation where OpenVPN exposed a management
+// *server* and we connected to it. Thus we still refer to our program as
+// the "client" and OpenVPN as the "server" once the connection is established.
+//
+// When running on Unix systems it's possible to instead listen on a Unix
+// domain socket. To do this, pass an absolute path to the socket as
+// the listen address, and then run OpenVPN with the following options:
+//
+// --management /path/to/socket unix --management-client
+//
+func Listen(laddr string) (*MgmtListener, error) {
+ proto := "tcp"
+ if len(laddr) > 0 && laddr[0] == '/' {
+ proto = "unix"
+ }
+ listener, err := net.Listen(proto, laddr)
+ if err != nil {
+ return nil, err
+ }
+
+ return NewMgmtListener(listener), nil
+}
+
+// Accept waits for and returns the next connection.
+func (l *MgmtListener) Accept() (*IncomingConn, error) {
+ conn, err := l.l.Accept()
+ if err != nil {
+ return nil, err
+ }
+
+ return &IncomingConn{conn}, nil
+}
+
+// Close closes the listener. Any blocked Accept operations
+// will be blocked and each will return an error.
+func (l *MgmtListener) Close() error {
+ return l.l.Close()
+}
+
+// Addr returns the listener's network address.
+func (l *MgmtListener) Addr() net.Addr {
+ return l.l.Addr()
+}
+
+// Serve will await new connections and call the given handler
+// for each.
+//
+// Serve does not return unless the listen port is closed; a non-nil
+// error is always returned.
+func (l *MgmtListener) Serve(handler IncomingConnHandler) error {
+ defer l.Close()
+
+ var tempDelay time.Duration
+
+ for {
+ incoming, err := l.Accept()
+ if err != nil {
+ if ne, ok := err.(net.Error); ok && ne.Temporary() {
+ if tempDelay == 0 {
+ tempDelay = 5 * time.Millisecond
+ } else {
+ tempDelay *= 2
+ }
+ if max := 1 * time.Second; tempDelay > max {
+ tempDelay = max
+ }
+
+ // Wait a while before we try again.
+ time.Sleep(tempDelay)
+ continue
+ } else {
+ // Listen socket is permanently closed or errored,
+ // so it's time for us to exit.
+ return err
+ }
+ }
+
+ // always reset our retry delay once we successfully read
+ tempDelay = 0
+
+ go handler.ServeOpenVPNMgmt(*incoming)
+ }
+}
+
+type IncomingConn struct {
+ conn net.Conn
+}
+
+// Open initiates communication with the connected OpenVPN process,
+// and establishes the channel on which events will be delivered.
+//
+// See the documentation for NewClient for discussion about the requirements
+// for eventCh.
+func (ic IncomingConn) Open(eventCh chan<- Event) *MgmtClient {
+ return NewClient(ic.conn, eventCh)
+}
+
+// Close abruptly closes the socket connected to the OpenVPN process.
+//
+// This is a rather abrasive way to close the channel, intended for rejecting
+// unwanted incoming clients that may or may not speak the OpenVPN protocol.
+//
+// Once communication is accepted and established, it is generally better
+// to close the connection gracefully using commands on the client returned
+// from Open.
+func (ic IncomingConn) Close() error {
+ return ic.conn.Close()
+}
+
+type IncomingConnHandler interface {
+ ServeOpenVPNMgmt(IncomingConn)
+}
+
+// IncomingConnHandlerFunc is an adapter to allow the use of ordinary
+// functions as connection handlers.
+//
+// Given a function with the appropriate signature, IncomingConnHandlerFunc(f)
+// is an IncomingConnHandler that calls f.
+type IncomingConnHandlerFunc func(IncomingConn)
+
+func (f IncomingConnHandlerFunc) ServeOpenVPNMgmt(i IncomingConn) {
+ f(i)
+}
+
+// ListenAndServe creates a MgmtListener for the given listen address
+// and then calls AcceptAndServe on it.
+//
+// This is just a convenience wrapper. See the AcceptAndServe method for
+// more details. Just as with AcceptAndServe, this function does not return
+// except on error; in addition to the error cases handled by AcceptAndServe,
+// this function may also fail if the listen socket cannot be established
+// in the first place.
+func ListenAndServe(laddr string, handler IncomingConnHandler) error {
+ listener, err := Listen(laddr)
+ if err != nil {
+ return err
+ }
+
+ return listener.Serve(handler)
+}
diff --git a/pkg/vpn/openvpn.go b/pkg/vpn/openvpn.go
index ee0e2f7..d98c45c 100644
--- a/pkg/vpn/openvpn.go
+++ b/pkg/vpn/openvpn.go
@@ -16,10 +16,13 @@
package vpn
import (
+ "crypto/rand"
+ "encoding/base64"
"errors"
"fmt"
"io/ioutil"
"log"
+ "math"
"os"
"path"
"path/filepath"
@@ -38,6 +41,7 @@ const (
// StartVPN for provider
func (b *Bitmask) StartVPN(provider string) error {
if !b.CanStartVPN() {
+ log.Println("BUG cannot start")
return errors.New("BUG: cannot start vpn")
}
@@ -109,8 +113,34 @@ func (b *Bitmask) startTransport(host string) (proxy string, err error) {
return "", fmt.Errorf("No working gateway for transport %s: %v", b.transport, err)
}
+// generates a password and returns the path for a temporary file where this password is written
+func (b *Bitmask) generateManagementPassword() string {
+ pass := getRandomPass(12)
+ tmpFile, err := ioutil.TempFile(b.tempdir, "leap-vpn-")
+ if err != nil {
+ log.Fatal("Cannot create temporary file", err)
+ }
+ tmpFile.Write([]byte(pass))
+ b.launch.mngPass = pass
+ return tmpFile.Name()
+}
+
func (b *Bitmask) startOpenVPN() error {
arg := b.openvpnArgs
+ /*
+ XXX has this changed??
+ arg, err := b.bonafide.GetOpenvpnArgs()
+ if err != nil {
+ return err
+ }
+ */
+ /*
+ XXX and this??
+ certPemPath, err := b.getCert()
+ if err != nil {
+ return err
+ }
+ */
b.statusCh <- Starting
if b.GetTransport() == "obfs4" {
gateways, err := b.bonafide.GetGateways("obfs4")
@@ -178,10 +208,12 @@ func (b *Bitmask) startOpenVPN() error {
// not overriding (or duplicating) some of the options we're adding here.
log.Println("VERB", verb)
+ passFile := b.generateManagementPassword()
+
arg = append(arg,
"--verb", openvpnVerb,
"--management-client",
- "--management", openvpnManagementAddr, openvpnManagementPort,
+ "--management", openvpnManagementAddr, openvpnManagementPort, passFile,
"--ca", b.getTempCaCertPath(),
"--cert", b.certPemPath,
"--key", b.certPemPath,
@@ -265,17 +297,15 @@ func (b *Bitmask) StopVPN() error {
b.obfsvpnProxy.Stop()
b.obfsvpnProxy = nil
}
- b.stopFromManagement()
+ b.tryStopFromManagement()
b.launch.openvpnStop()
return nil
}
-func (b *Bitmask) stopFromManagement() error {
- if b.managementClient == nil {
- return fmt.Errorf("No management connected")
+func (b *Bitmask) tryStopFromManagement() {
+ if b.managementClient != nil {
+ b.managementClient.SendSignal("SIGTERM")
}
- b.managementClient.SendSignal("SIGTERM")
- return nil
}
// Reconnect to the VPN
@@ -411,3 +441,10 @@ func (b *Bitmask) getTempCertPemPath() string {
func (b *Bitmask) getTempCaCertPath() string {
return path.Join(b.tempdir, "cacert.pem")
}
+
+func getRandomPass(l int) string {
+ buff := make([]byte, int(math.Round(float64(l)/float64(1.33333333333))))
+ rand.Read(buff)
+ str := base64.RawURLEncoding.EncodeToString(buff)
+ return str[:l] // strip 1 extra character we get from odd length results
+}
diff --git a/pkg/vpn/status.go b/pkg/vpn/status.go
index 692bf09..0e928fe 100644
--- a/pkg/vpn/status.go
+++ b/pkg/vpn/status.go
@@ -20,7 +20,7 @@ import (
"log"
"strings"
- "github.com/apparentlymart/go-openvpn-mgmt/openvpn"
+ "0xacab.org/leap/bitmask-vpn/pkg/vpn/management"
)
const (
@@ -47,23 +47,24 @@ var statusNames = map[string]string{
func (b *Bitmask) openvpnManagement() {
// TODO: we should warn the user on ListenAndServe errors
- newConnection := func(conn openvpn.IncomingConn) {
- eventCh := make(chan openvpn.Event, 10)
+ newConnection := func(conn management.IncomingConn) {
+ eventCh := make(chan management.Event, 10)
log.Println("New connection into the management")
b.managementClient = conn.Open(eventCh)
+ b.managementClient.SendPassword(b.launch.mngPass)
b.managementClient.SetStateEvents(true)
b.eventHandler(eventCh)
}
- log.Fatal(openvpn.ListenAndServe(
+ log.Fatal(management.ListenAndServe(
fmt.Sprintf("%s:%s", openvpnManagementAddr, openvpnManagementPort),
- openvpn.IncomingConnHandlerFunc(newConnection),
+ management.IncomingConnHandlerFunc(newConnection),
))
}
-func (b *Bitmask) eventHandler(eventCh <-chan openvpn.Event) {
+func (b *Bitmask) eventHandler(eventCh <-chan management.Event) {
for event := range eventCh {
log.Printf("Event: %v", event)
- stateEvent, ok := event.(*openvpn.StateEvent)
+ stateEvent, ok := event.(*management.StateEvent)
if !ok {
continue
}