diff options
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.go | 303 |
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() +} |