This repository has been archived by the owner on May 14, 2021. It is now read-only.
/
firewall_linux.go
473 lines (406 loc) · 13.1 KB
/
firewall_linux.go
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
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
//
// Daemon for IVPN Client Desktop
// https://github.com/ivpn/desktop-app-daemon
//
// Created by Stelnykovych Alexandr.
// Copyright (c) 2020 Privatus Limited.
//
// This file is part of the Daemon for IVPN Client Desktop.
//
// The Daemon for IVPN Client Desktop is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any later version.
//
// The Daemon for IVPN Client Desktop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License
// along with the Daemon for IVPN Client Desktop. If not, see <https://www.gnu.org/licenses/>.
//
package firewall
import (
"fmt"
"net"
"strings"
"time"
"github.com/ivpn/desktop-app-daemon/netinfo"
"github.com/ivpn/desktop-app-daemon/service/platform"
"github.com/ivpn/desktop-app-daemon/shell"
)
var (
// key: is a string representation of allowed IP
// value: true - if exception rule is persistant (persistant, means will stay available even client is disconnected)
allowedHosts map[string]bool
// IP addresses of local interfaces (using for 'allow LAN' functionality)
allowedLanIPs []string
allowedForICMP map[string]struct{}
connectedVpnLocalIP string
delayedAllowLanAllowed bool = true
delayedAllowLanStarted bool = false
isPersistant bool = false
)
const (
multicastIP = "224.0.0.0/4"
)
func init() {
allowedHosts = make(map[string]bool)
}
func implInitialize() error { return nil }
func implGetEnabled() (bool, error) {
err := shell.Exec(nil, platform.FirewallScript(), "-status")
if err != nil {
exitCode, err := shell.GetCmdExitCode(err)
if err != nil {
return false, fmt.Errorf("failed to get Cmd exit code: %w", err)
}
if exitCode == 1 {
return false, nil
}
return false, nil
}
return true, nil
}
func implSetEnabled(isEnabled bool) error {
if isEnabled {
err := shell.Exec(nil, platform.FirewallScript(), "-enable")
if err != nil {
return fmt.Errorf("failed to execute shell command: %w", err)
}
// To fulfill such flow (example): Connected -> FWDisable -> FWEnable
// Here we should restore all exceptions (all hosts which are allowed)
return reApplyExceptions()
}
isPersistant = false
allowedForICMP = nil
return shell.Exec(nil, platform.FirewallScript(), "-disable")
}
func implSetPersistant(persistant bool) error {
isPersistant = persistant
if persistant {
// The persistence is based on such facts:
// - daemon is starting as on system boot
// - SetPersistant() called by service object on daemon start
// This means we just have to ensure that firewall enabled.
// Just ensure that firewall is enabled
ret := implSetEnabled(true)
// Some Linux distributions erasing IVPN rules during system boot
// During some period of time (60 seconds should be enough)
// check if FW rules still exist (if not - re-apply them)
go ensurePersistant(60)
return ret
}
return nil
}
// Some Linux distributions erasing IVPN rules during system boot
// During some period of time (60 seconds should be enough)
// check if FW rules still exist (if not - re-apply them)
func ensurePersistant(secondsToWait int) {
const delaySec = 5
log.Info("[ensurePersistant] started")
for i := 0; i <= secondsToWait/delaySec; i++ {
time.Sleep(time.Second * delaySec)
if isPersistant != true {
break
}
enabled, err := implGetEnabled()
if err != nil {
log.Error("[ensurePersistant] ", err)
continue
}
if isPersistant == true && enabled != true {
log.Warning("[ensurePersistant] Persistant FW rules not available. Retry to apply...")
implSetEnabled(true)
}
}
log.Info("[ensurePersistant] stopped.")
}
// ClientConnected - allow communication for local vpn/client IP address
func implClientConnected(clientLocalIPAddress net.IP, clientPort int, serverIP net.IP, serverPort int, isTCP bool) error {
connectedVpnLocalIP = clientLocalIPAddress.String()
inf, err := netinfo.InterfaceByIPAddr(clientLocalIPAddress)
if err != nil {
return fmt.Errorf("failed to get local interface by IP: %w", err)
}
protocol := "udp"
if isTCP {
protocol = "tcp"
}
scriptArgs := fmt.Sprintf("-connected %s %s %d %s %d %s",
inf.Name,
clientLocalIPAddress,
clientPort,
serverIP,
serverPort,
protocol)
err = shell.Exec(nil, platform.FirewallScript(), scriptArgs)
if err != nil {
return fmt.Errorf("failed to add rule for current connection directions: %w", err)
}
// Connection already established. The rule for VPN interface is defined.
// Removing host IP from exceptions
return removeHostsFromExceptions([]string{serverIP.String()}, false)
}
// ClientDisconnected - Disable communication for local vpn/client IP address
func implClientDisconnected() error {
connectedVpnLocalIP = ""
// remove all exceptions related to current connection (all non-persistant exceptions)
err := removeAllHostsFromExceptions()
if err != nil {
log.Error(err)
}
return shell.Exec(nil, platform.FirewallScript(), "-disconnected")
}
func implAllowLAN(isAllowLAN bool, isAllowLanMulticast bool) error {
const persistant = true
const notOnlyForICMP = false
if !isAllowLAN {
// LAN NOT ALLOWED
delayedAllowLanAllowed = false
if len(allowedLanIPs) <= 0 {
return nil
}
// disallow everything (LAN + multicast)
toRemove := allowedLanIPs
allowedLanIPs = nil
return removeHostsFromExceptions(toRemove, persistant)
}
// LAN ALLOWED
localIPs, err := getLanIPs()
if err != nil {
return fmt.Errorf("failed to get local IPs: %w", err)
}
if len(localIPs) > 0 {
delayedAllowLanAllowed = false
} else {
// this can happen, for example, on system boot (when no network interfaces initialized)
log.Info("Local LAN addresses not detected: no data to apply the 'Allow LAN' rule")
go delayedAllowLAN(isAllowLanMulticast)
return nil
}
if len(allowedLanIPs) > 0 {
removeHostsFromExceptions(allowedLanIPs, persistant)
}
allowedLanIPs = localIPs
if isAllowLanMulticast {
// allow LAN + multicast
allowedLanIPs = append(allowedLanIPs, multicastIP)
return addHostsToExceptions(allowedLanIPs, persistant, notOnlyForICMP)
}
// disallow Multicast
removeHostsFromExceptions([]string{multicastIP}, persistant)
// allow LAN
return addHostsToExceptions(allowedLanIPs, persistant, notOnlyForICMP)
}
func delayedAllowLAN(allowLanMulticast bool) {
if delayedAllowLanStarted || delayedAllowLanAllowed == false {
return
}
log.Info("Delayed 'Allow LAN': Will try to apply this rule few seconds later...")
delayedAllowLanStarted = true
defer func() { delayedAllowLanAllowed = false }()
for i := 0; i < 25 && delayedAllowLanAllowed; i++ {
time.Sleep(time.Second)
ipList, err := getLanIPs()
if err != nil {
log.Warning(fmt.Errorf("Delayed 'Allow LAN': failed to get local IPs: %w", err))
return
}
if len(ipList) >= 0 {
time.Sleep(time.Second) // just to ensure that everything initialized
if delayedAllowLanAllowed {
log.Info("Delayed 'Allow LAN': apply ...")
err := implAllowLAN(true, allowLanMulticast)
if err != nil {
log.Warning(fmt.Errorf("Delayed 'Allow LAN' error: %w", err))
}
}
return
}
}
log.Info("Delayed 'Allow LAN': no LAN interfaces detected")
}
// AddHostsToExceptions - allow comminication with this hosts
// Note!: all added hosts will be removed from exceptions after client disconnection (after call 'ClientDisconnected()')
func implAddHostsToExceptions(IPs []net.IP, onlyForICMP bool) error {
if onlyForICMP {
// no sense to add exception if firewall not enabled
if enabled, err := implGetEnabled(); err != nil || enabled == false {
return nil
}
}
IPsStr := make([]string, 0, len(IPs))
for _, ip := range IPs {
IPsStr = append(IPsStr, ip.String())
}
const persistant = false
return addHostsToExceptions(IPsStr, persistant, onlyForICMP)
}
// SetManualDNS - configure firewall to allow DNS which is out of VPN tunnel
// Applicable to Windows implementation (to allow custom DNS from local network)
func implSetManualDNS(addr net.IP) error {
// not in use for Linux
return nil
}
//---------------------------------------------------------------------
func applyAddHostsToExceptions(hostsIPs []string, isPersistant bool, onlyForICMP bool) error {
var ipList string
ipList = strings.Join(hostsIPs, ",")
if len(ipList) > 0 {
scriptCommand := "-add_exceptions"
if onlyForICMP {
scriptCommand = "-add_exceptions_icmp"
} else if isPersistant {
scriptCommand = "-add_exceptions_static"
}
log.Info(scriptCommand, " ", ipList)
return shell.Exec(nil, platform.FirewallScript(), scriptCommand, ipList)
}
return nil
}
func applyRemoveHostsFromExceptions(hostsIPs []string, isPersistant bool) error {
var ipList string
ipList = strings.Join(hostsIPs, ",")
if len(ipList) > 0 {
scriptCommand := "-remove_exceptions"
if isPersistant {
scriptCommand = "-remove_exceptions_static"
}
log.Info(scriptCommand, " ", ipList)
return shell.Exec(nil, platform.FirewallScript(), scriptCommand, ipList)
}
return nil
}
func reApplyExceptions() error {
// Allow LAN communication (if necessary)
// Restore all exceptions (all hosts which are allowed)
allowedIPs := make([]string, 0, len(allowedHosts))
allowedIPsPersistant := make([]string, 0, len(allowedHosts))
if len(allowedHosts) > 0 {
for ipStr, isPersistant := range allowedHosts {
if isPersistant {
allowedIPsPersistant = append(allowedIPsPersistant, ipStr)
} else {
allowedIPs = append(allowedIPs, ipStr)
}
}
}
allowedIPsICMP := make([]string, 0, len(allowedForICMP))
if len(allowedForICMP) > 0 {
for ipStr := range allowedForICMP {
allowedIPsICMP = append(allowedIPsICMP, ipStr)
}
}
const persistantTRUE = true
const persistantFALSE = false
const onlyIcmpTRUE = true
const onlyIcmpFALSE = false
// Apply all allowed hosts
err := applyAddHostsToExceptions(allowedIPsICMP, persistantFALSE, onlyIcmpTRUE)
if err != nil {
log.Error(err)
}
err = applyAddHostsToExceptions(allowedIPs, persistantFALSE, onlyIcmpFALSE)
if err != nil {
log.Error(err)
return err
}
err = applyAddHostsToExceptions(allowedIPsPersistant, persistantTRUE, onlyIcmpFALSE)
if err != nil {
log.Error(err)
}
return err
}
//---------------------------------------------------------------------
// allow communication with specified hosts
// if isPersistant == false - exception will be removed when client disctonnects
func addHostsToExceptions(IPs []string, isPersistant bool, onlyForICMP bool) error {
if len(IPs) == 0 {
return nil
}
newIPs := make([]string, 0, len(IPs))
if !onlyForICMP {
for _, ip := range IPs {
// do not add new IP if it already in exceptions
if _, exists := allowedHosts[ip]; exists == false {
allowedHosts[ip] = isPersistant // add to map
newIPs = append(newIPs, ip)
}
}
} else {
if allowedForICMP == nil {
allowedForICMP = make(map[string]struct{})
}
for _, ip := range IPs {
// do not add new IP if it already in exceptions
if _, exists := allowedForICMP[ip]; exists == false {
allowedForICMP[ip] = struct{}{} // add to map
newIPs = append(newIPs, ip)
}
}
}
if len(newIPs) == 0 {
return nil
}
err := applyAddHostsToExceptions(newIPs, isPersistant, onlyForICMP)
if err != nil {
log.Error(err)
}
return err
}
// Deprecate communication with this hosts
func removeHostsFromExceptions(IPs []string, isPersistant bool) error {
if len(IPs) == 0 {
return nil
}
toRemoveIPs := make([]string, 0, len(IPs))
for _, ip := range IPs {
if _, exists := allowedHosts[ip]; exists {
delete(allowedHosts, ip) // remove from map
toRemoveIPs = append(toRemoveIPs, ip)
}
}
if len(toRemoveIPs) == 0 {
return nil
}
err := applyRemoveHostsFromExceptions(toRemoveIPs, isPersistant)
if err != nil {
log.Error(err)
}
return err
}
// removeAllHostsFromExceptions - Remove hosts (which are related to a current connection) from exceptions
// Note: some exceptions should stay without changes, they are marked as 'persistant'
// (has 'true' value in allowedHosts; eg.: LAN and Multicast connectivity)
func removeAllHostsFromExceptions() error {
toRemoveIPs := make([]string, 0, len(allowedHosts))
for ipStr, isPersistant := range allowedHosts {
if isPersistant {
continue
}
toRemoveIPs = append(toRemoveIPs, ipStr)
delete(allowedHosts, ipStr) // erase map
}
return removeHostsFromExceptions(toRemoveIPs, false)
}
//---------------------------------------------------------------------
// getLanIPs - returns list of local IPs
func getLanIPs() ([]string, error) {
ipnetList, err := netinfo.GetAllLocalV4Addresses()
if err != nil {
return nil, fmt.Errorf("failed to get network interfaces: %w", err)
}
retIps := make([]string, 0, 4)
for _, ifs := range ipnetList {
// Skip localhost interface - we have separate rules for local iface
if ifs.IP.String() == "127.0.0.1" {
continue
}
if len(connectedVpnLocalIP) > 0 && ifs.IP.String() == connectedVpnLocalIP {
continue
}
retIps = append(retIps, ifs.String())
}
return retIps, nil
}