summaryrefslogtreecommitdiff
path: root/vendor/github.com/apparentlymart/go-openvpn-mgmt/openvpn/client.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/apparentlymart/go-openvpn-mgmt/openvpn/client.go')
-rw-r--r--vendor/github.com/apparentlymart/go-openvpn-mgmt/openvpn/client.go303
1 files changed, 303 insertions, 0 deletions
diff --git a/vendor/github.com/apparentlymart/go-openvpn-mgmt/openvpn/client.go b/vendor/github.com/apparentlymart/go-openvpn-mgmt/openvpn/client.go
new file mode 100644
index 0000000..7768004
--- /dev/null
+++ b/vendor/github.com/apparentlymart/go-openvpn-mgmt/openvpn/client.go
@@ -0,0 +1,303 @@
+package openvpn
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net"
+ "strconv"
+ "time"
+
+ "github.com/apparentlymart/go-openvpn-mgmt/demux"
+)
+
+var newline = []byte{'\n'}
+var successPrefix = []byte("SUCCESS: ")
+var errorPrefix = []byte("ERROR: ")
+var endMessage = []byte("END")
+
+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
+}
+
+// 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) 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()
+}