summaryrefslogtreecommitdiff
path: root/vendor/github.com/apparentlymart/go-openvpn-mgmt/openvpn/client.go
blob: 776800432e332a12f1f6110527465376974120e1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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()
}