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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
// Package tunservice is the TUN service (cmd/firefox-vpn-tun): it serves
// the tunapi protocol on the service's channel and runs one TUN session at
// a time, for the core that opened it, until that core closes it or its
// connection ends. Every TCP flow goes to the core's SOCKS5 server on
// loopback, but those of the programs that skip the VPN (programs.go):
// their traffic is the only one the service sends to the internet itself,
// directly, outside the TUN.
package tunservice
import (
"errors"
"io"
"net"
"net/netip"
"sync"
"github.com/armanlife/firefox-vpn-client/core/internal/logx"
"github.com/armanlife/firefox-vpn-client/core/internal/tun"
"github.com/armanlife/firefox-vpn-client/core/internal/tunapi"
"github.com/armanlife/firefox-vpn-client/core/internal/tunstack"
)
// adapterMTU is the TUN adapter's MTU; AdapterName (adapter_*.go) its name.
const adapterMTU = 9000
var (
// adapterV4 is the adapter's address in tun.DesktopNet; Windows sends
// DNS to the next one, which the stack answers like any DNS.
adapterV4 = netip.MustParsePrefix("198.18.0.1/30")
// adapterV6 routes IPv6 into the adapter too, so that it never leaves
// outside the VPN.
adapterV6 = netip.MustParsePrefix("fdfe:dcba:9876::1/126")
)
// Config is what the service does beyond the protocol.
type Config struct {
// Version is the service's program version.
Version string
// Self is the service's executable: the filters let it through.
Self string
// Update checks the executable at path and makes it the service, to
// run once this process ends; nil refuses updates.
Update func(path string) error
// Uninstall removes the service, which ends once Serve returns; nil
// refuses.
Uninstall func() error
}
// device is the part of tunstack.Device the server uses; tests fake it.
type device interface {
Interface() (name string, index uint32, luid uint64, err error)
Start() error
ResetFlows()
Close() error
}
// Server serves the protocol.
type Server struct {
cfg Config
// Replaced in tests.
openDevice func(tunstack.Config) (device, error)
startFilters func(index uint32, programs []string, blockIPv6 bool) (io.Closer, error)
adapterUp func(index uint32) bool
clientPath func(net.Conn) (string, error)
// allowed reports whether a client may use the service (a Unix
// socket's peer; on Windows the pipe's access list decided).
allowed func(net.Conn) bool
mu sync.Mutex
session *session
closing bool
listener net.Listener
conns map[*tunapi.Conn]struct{}
// ended is closed once an update or an uninstall asks the process to
// end; reason says which.
ended chan struct{}
reason string
}
// NewServer returns a server for cfg.
func NewServer(cfg Config) *Server {
return &Server{
cfg: cfg,
openDevice: func(c tunstack.Config) (device, error) {
return tunstack.Open(c)
},
startFilters: startFilters,
adapterUp: adapterUp,
allowed: tunapi.Allowed,
clientPath: func(c net.Conn) (string, error) {
pid, err := tunapi.ClientProcess(c)
if err != nil {
return "", err
}
return tunapi.ProcessPath(pid)
},
conns: map[*tunapi.Conn]struct{}{},
ended: make(chan struct{}),
}
}
// Ended is closed when an update or an uninstall wants the process to
// end; Reason then says which.
func (s *Server) Ended() <-chan struct{} { return s.ended }
// Reason is why the server ended: "updated" or "uninstalled".
func (s *Server) Reason() string {
s.mu.Lock()
defer s.mu.Unlock()
return s.reason
}
// Serve accepts clients on ln until Close.
func (s *Server) Serve(ln net.Listener) error {
s.mu.Lock()
if s.closing {
s.mu.Unlock()
ln.Close()
return net.ErrClosed
}
s.listener = ln
s.mu.Unlock()
for {
raw, err := ln.Accept()
if err != nil {
s.mu.Lock()
closing := s.closing
s.mu.Unlock()
if closing || errors.Is(err, net.ErrClosed) {
return nil
}
return err
}
go s.serveConn(raw)
}
}
// Close ends the session, the connections, and Serve.
func (s *Server) Close() {
s.mu.Lock()
s.closing = true
ln := s.listener
sess := s.session
s.session = nil
conns := make([]*tunapi.Conn, 0, len(s.conns))
for c := range s.conns {
conns = append(conns, c)
}
s.mu.Unlock()
if ln != nil {
ln.Close()
}
if sess != nil {
sess.close()
}
for _, c := range conns {
c.Close()
}
}
// end closes the server and tells the process to end.
func (s *Server) end(reason string) {
s.mu.Lock()
first := s.reason == ""
if first {
s.reason = reason
}
s.mu.Unlock()
if !first {
return
}
s.Close()
close(s.ended)
}
func (s *Server) serveConn(raw net.Conn) {
c := tunapi.NewConn(raw)
s.mu.Lock()
if s.closing {
s.mu.Unlock()
c.Close()
return
}
s.conns[c] = struct{}{}
s.mu.Unlock()
defer func() {
s.mu.Lock()
delete(s.conns, c)
s.mu.Unlock()
// A session ends with its core's connection, whatever the reason.
s.closeSession(c)
c.Close()
}()
hello, err := c.Receive()
if err != nil {
return
}
if hello.Type != tunapi.TypeHello {
_ = c.Send(tunapi.Errorf(tunapi.CodeBadRequest, "say hello first"))
return
}
// Nothing for a user who did not install the service, whatever the
// protocol (update and uninstall work in every one); told why, so the
// app installs it for this user.
if !s.allowed(raw) {
logx.Warn("a user who did not install the service connected")
_ = c.Send(tunapi.Errorf(tunapi.CodeNotAllowed, "the TUN service was installed for another user"))
return
}
protocol := tunapi.Common(hello.Protocol, hello.MinProtocol)
compatible := protocol != 0
reply := tunapi.Hello(s.cfg.Version)
if !compatible {
reply = tunapi.Errorf(tunapi.CodeProtocol, "no common protocol: the service speaks %d to %d, the client %d to %d", tunapi.MinProtocol, tunapi.Protocol, hello.MinProtocol, hello.Protocol)
reply.Protocol, reply.MinProtocol, reply.Version = tunapi.Protocol, tunapi.MinProtocol, s.cfg.Version
logx.Warn("a client of version %s speaks no common protocol", hello.Version)
}
if err := c.Send(reply); err != nil {
return
}
for {
m, err := c.Receive()
if err != nil {
return
}
var out tunapi.Message
switch m.Type {
case tunapi.TypeUpdate:
out = s.update(m.Path)
case tunapi.TypeUninstall:
out = s.uninstall()
case tunapi.TypeOpen, tunapi.TypeRoute, tunapi.TypeClose, tunapi.TypePrograms:
if !compatible {
out = tunapi.Errorf(tunapi.CodeProtocol, "no common protocol")
break
}
switch m.Type {
case tunapi.TypeOpen:
out = s.open(c, m, protocol)
case tunapi.TypeRoute:
out = s.route(c)
case tunapi.TypePrograms:
out = s.programs(c, m.Programs)
default:
s.closeSession(c)
out = tunapi.Message{Type: tunapi.TypeClosed}
}
default:
out = tunapi.Errorf(tunapi.CodeBadRequest, "unknown message %q", m.Type)
}
if err := c.Send(out); err != nil {
return
}
if out.Type == tunapi.TypeRouted {
// From now on the core reads what comes unasked.
if sess := s.sessionOf(c); sess != nil {
sess.live.Store(true)
}
}
if out.Type == tunapi.TypeUpdating {
s.end("updated")
return
}
if out.Type == tunapi.TypeUninstalling {
s.end("uninstalled")
return
}
}
}
func (s *Server) open(c *tunapi.Conn, m tunapi.Message, protocol int) tunapi.Message {
// The service connects nowhere but to the core on this computer.
if proxy, err := netip.ParseAddrPort(m.Proxy); err != nil || !proxy.Addr().IsLoopback() {
return tunapi.Errorf(tunapi.CodeBadRequest, "the proxy must be a loopback address and port, not %q", m.Proxy)
}
client, err := s.clientPath(c.Raw())
if err != nil {
return tunapi.Errorf(tunapi.CodeOpenFailed, "finding the client's program: %s", err)
}
s.mu.Lock()
if s.session != nil {
owner := s.session.owner
s.mu.Unlock()
if owner != c {
return tunapi.Errorf(tunapi.CodeBusy, "another program's TUN session runs")
}
return tunapi.Errorf(tunapi.CodeBadRequest, "the TUN session is already open")
}
// Taken now, so a second client meanwhile gets busy.
placeholder := &session{owner: c}
s.session = placeholder
s.mu.Unlock()
sess, err := newSession(s, c, client, protocol, m)
s.mu.Lock()
if s.session == placeholder {
s.session = sess
}
current := s.session == sess
s.mu.Unlock()
if err != nil {
s.mu.Lock()
if s.session == placeholder {
s.session = nil
}
s.mu.Unlock()
logx.Error("opening the TUN adapter failed err=%s", logx.Err(err))
return tunapi.Errorf(tunapi.CodeOpenFailed, "%s", err)
}
if !current {
// The server closed meanwhile.
sess.close()
return tunapi.Errorf(tunapi.CodeOpenFailed, "the service is stopping")
}
name, index, luid := sess.iface()
logx.Info("TUN adapter opened name=%q index=%d client=%s", name, index, client)
return tunapi.Message{Type: tunapi.TypeOpened, Interface: &tunapi.Interface{Name: name, Index: index, LUID: luid}}
}
func (s *Server) route(c *tunapi.Conn) tunapi.Message {
s.mu.Lock()
sess := s.session
s.mu.Unlock()
if sess == nil || sess.owner != c || sess.dev == nil {
return tunapi.Errorf(tunapi.CodeBadRequest, "open the TUN session first")
}
if err := sess.route(); err != nil {
s.closeSession(c)
logx.Error("starting the TUN failed err=%s", logx.Err(err))
return tunapi.Errorf(tunapi.CodeRouteFailed, "%s", err)
}
logx.Info("TUN started: all traffic goes to the core")
return tunapi.Message{Type: tunapi.TypeRouted}
}
// programs replaces the programs that skip the VPN in c's session.
func (s *Server) programs(c *tunapi.Conn, programs []tunapi.Program) tunapi.Message {
sess := s.sessionOf(c)
if sess == nil || sess.dev == nil {
return tunapi.Errorf(tunapi.CodeBadRequest, "open the TUN session first")
}
sess.setPrograms(programs)
return tunapi.Message{Type: tunapi.TypeProgramsSet}
}
// sessionOf is c's session, or nil.
func (s *Server) sessionOf(c *tunapi.Conn) *session {
s.mu.Lock()
defer s.mu.Unlock()
if s.session == nil || s.session.owner != c {
return nil
}
return s.session
}
// closeSession ends c's session, if it has one.
func (s *Server) closeSession(c *tunapi.Conn) {
s.mu.Lock()
sess := s.session
if sess == nil || sess.owner != c {
s.mu.Unlock()
return
}
s.session = nil
s.mu.Unlock()
sess.close()
}
// lost ends sess after its adapter went away, telling its core.
func (s *Server) lost(sess *session) {
s.mu.Lock()
if s.session != sess {
s.mu.Unlock()
return
}
s.session = nil
s.mu.Unlock()
logx.Warn("the TUN adapter went away; the TUN is stopped")
sess.close()
_ = sess.owner.Send(tunapi.Message{Type: tunapi.TypeFailed, Code: tunapi.CodeAdapterLost, Message: "the TUN adapter was disabled or removed"})
}
func (s *Server) update(path string) tunapi.Message {
if s.cfg.Update == nil {
return tunapi.Errorf(tunapi.CodeUpdateRefused, "this service takes no updates")
}
if err := s.cfg.Update(path); err != nil {
logx.Warn("an update was refused path=%s err=%s", path, logx.Err(err))
return tunapi.Errorf(tunapi.CodeUpdateRefused, "%s", err)
}
logx.Info("updated from path=%s; restarting", path)
return tunapi.Message{Type: tunapi.TypeUpdating}
}
func (s *Server) uninstall() tunapi.Message {
if s.cfg.Uninstall == nil {
return tunapi.Errorf(tunapi.CodeUninstallFailed, "this service cannot remove itself")
}
s.mu.Lock()
sess := s.session
s.session = nil
s.mu.Unlock()
if sess != nil {
sess.close()
}
if err := s.cfg.Uninstall(); err != nil {
logx.Error("removing the service failed err=%s", logx.Err(err))
return tunapi.Errorf(tunapi.CodeUninstallFailed, "%s", err)
}
logx.Info("the service is removed")
return tunapi.Message{Type: tunapi.TypeUninstalling}
}
// isFilterAddress is tun.IsFilterAddress, for the session.
var isFilterAddress = tun.IsFilterAddress
// adapterUp reports whether the interface index is there and up.
func adapterUp(index uint32) bool {
iface, err := net.InterfaceByIndex(int(index))
return err == nil && iface.Flags&net.FlagUp != 0
}