From a85959a43eeeeab7181e0f54165c1b93e9449fb7 Mon Sep 17 00:00:00 2001 From: kali Date: Tue, 6 Apr 2021 13:02:07 +0200 Subject: [feat] send password to management interface - Resolves: #104 --- go.mod | 4 + pkg/bitmask/init.go | 1 + pkg/vpn/demux/demuxer.go | 83 +++++++++ pkg/vpn/demux/demuxer_test.go | 238 +++++++++++++++++++++++++ pkg/vpn/demux/doc.go | 12 ++ pkg/vpn/launcher_windows.go | 150 ++++++++-------- pkg/vpn/main.go | 5 +- pkg/vpn/management/client.go | 362 ++++++++++++++++++++++++++++++++++++++ pkg/vpn/management/error.go | 32 ++++ pkg/vpn/management/event.go | 320 ++++++++++++++++++++++++++++++++++ pkg/vpn/management/event_test.go | 367 +++++++++++++++++++++++++++++++++++++++ pkg/vpn/management/server.go | 193 ++++++++++++++++++++ pkg/vpn/openvpn.go | 51 +++++- pkg/vpn/status.go | 15 +- 14 files changed, 1743 insertions(+), 90 deletions(-) create mode 100644 pkg/vpn/demux/demuxer.go create mode 100644 pkg/vpn/demux/demuxer_test.go create mode 100644 pkg/vpn/demux/doc.go create mode 100644 pkg/vpn/management/client.go create mode 100644 pkg/vpn/management/error.go create mode 100644 pkg/vpn/management/event.go create mode 100644 pkg/vpn/management/event_test.go create mode 100644 pkg/vpn/management/server.go 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 +// +// 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 } -- cgit v1.2.3