This repository has been archived by the owner on Jul 25, 2018. It is now read-only.
/
tpl_firewall.go
408 lines (344 loc) · 10.9 KB
/
tpl_firewall.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
package main
import (
"fmt"
"net"
"strconv"
"github.com/dynport/urknall"
"github.com/dynport/urknall/cmd"
)
type Firewall struct {
Interface string `urknall:"default=eth0"`
WithVPN bool
Paranoid bool
Rules []*FirewallRule
IPSets []*FirewallIPSet // List of ipsets for the firewall.
}
func (fw *Firewall) Render(pkg urknall.Package) {
var ipsetsCmd cmd.Command
if len(fw.IPSets) > 0 {
ipsetsCmd = WriteFile("/etc/iptables/ipsets", fwIpset, "root", 0644)
} else {
ipsetsCmd = Shell("rm -f /etc/iptables/ipsets")
}
pkg.AddCommands("base",
InstallPackages("iptables", "ipset"),
WriteFile("/etc/network/if-pre-up.d/iptables", firewallUpstart, "root", 0744),
WriteFile("/etc/iptables/rules_ipv4", fw_rules_ipv4, "root", 0644),
WriteFile("/etc/iptables/rules_ipv6", fw_rules_ipv6, "root", 0644),
ipsetsCmd,
Shell("{ modprobe iptable_filter && modprobe iptable_nat; }; /bin/true"), // here to make sure next command succeeds.
Shell("IFACE={{ .Interface }} /etc/network/if-pre-up.d/iptables"),
)
}
// IPSets are the possibility to change a rule, without actually rewriting the rules. That is they add some sort of
// flexibility with regard to dynamic entities like a load balancer, which must have access to the different machines
// that should take the load.
//
// A set is defined by a name, that is used in iptables rule (see "Rule.(Source|Destination).IPSet") to reference the
// contained entities. The type defines what parameters must match an entry (see "ipset --help" output and the man page
// for a list of allowed values), for example a set could define hosts and ports.
//
// The family defines the type of IP address to handle, either IPv4 or IPv6. The allowed values are "inet" and "inet6"
// respectively.
//
// There are some ipset internal parameters that shouldn't need to be changed often. Those are "HashSize" that defines
// the size of the underlying hash. This value defaults to 1024. The "MaxElem" number determines how much elements there
// can be in the set at most.
//
// An initial set of members can be defined, if reasonable.
type FirewallIPSet struct {
Name string // Name of the ipset.
Type string // Type of the ipset.
Family string // Network address family.
HashSize int // Size of the hash.
MaxElem int // Max number of elements of the set.
Members []net.IP // Initial set of members.
}
func (ips *FirewallIPSet) IPSetRestore() (cmd string) {
cmd = fmt.Sprintf("create %s %s family %s hashsize %d maxelem %d\n",
ips.Name, ips.Type, ips.family(), ips.hashsize(), ips.maxelem())
for idx := range ips.Members {
cmd += fmt.Sprintf("add %s %s\n", ips.Name, ips.Members[idx].String())
}
return cmd + "\n"
}
func (ips *FirewallIPSet) family() string {
if ips.Family == "" {
return "inet"
}
return ips.Family
}
func (i *FirewallIPSet) hashsize() int {
if i.HashSize == 0 {
return 1024
}
return i.HashSize
}
func (i *FirewallIPSet) maxelem() int {
if i.MaxElem == 0 {
return 65536
}
return i.MaxElem
}
// A rule defines what is allowed to flow from some source to some destination. A description can be added to make the
// resulting scripts more readable.
//
// The "Chain" field determines which chain the rule is added to. This should be either "INPUT", "OUTPUT", or "FORWARD",
// with the names of the chains mostly speaking for themselves.
//
// The protocol setting is another easy match for the rule and especially required for some of the target's settings,
// i.e. if a port is specified the protocol must be given too.
//
// Source and destination are the two communicating entities. For the input chain the local host is destination and for
// output it is the source.
type FirewallRule struct {
Description string
Chain string // Chain to add the rule to.
Protocol string // The protocol used.
Source *FirewallTarget // The source of the packet.
Destination *FirewallTarget // The destination of the packet.
}
// Method to create something digestable for IPtablesRestore (aka users might well ignore this).
func (r *FirewallRule) Filter() (cmd string) {
cfg := &iptConfig{rule: r, moduleConfig: map[string]iptModConfig{}}
if r.Source != nil {
r.Source.convert(cfg, "src")
}
if r.Destination != nil {
r.Destination.convert(cfg, "dest")
}
return cfg.FilterTableRule()
}
func (r *FirewallRule) isNATRule() bool {
return r.Chain == "FORWARD" &&
((r.Source != nil && r.Source.NAT != "") ||
(r.Destination != nil && r.Destination.NAT != ""))
}
func (r *FirewallRule) NAT() (cmd string) {
if !r.isNATRule() {
return ""
}
cfg := &iptConfig{rule: r, moduleConfig: map[string]iptModConfig{}}
if r.Source != nil {
r.Source.convert(cfg, "src")
}
if r.Destination != nil {
r.Destination.convert(cfg, "dest")
}
return cfg.NATTableRule()
}
func (r *FirewallRule) IPsets() {
}
type iptModConfig map[string]string
type iptConfig struct {
rule *FirewallRule
sourceIP string
destIP string
sourceIface string
destIface string
sourceNAT string
destNAT string
moduleConfig map[string]iptModConfig
}
func (cfg *iptConfig) basicSettings(natRule bool) (s string) {
if cfg.rule.Protocol != "" {
s += " --protocol " + cfg.rule.Protocol
}
if cfg.sourceIP != "" {
s += " --source " + cfg.sourceIP
}
if cfg.sourceIface != "" {
if cfg.rule.Chain == "FORWARD" {
if !natRule || cfg.destNAT != "" {
s += " --in-interface " + cfg.sourceIface
}
} else {
s += " --out-interface " + cfg.sourceIface
}
}
if cfg.destIP != "" {
s += " --destination " + cfg.destIP
}
if cfg.destIface != "" {
if cfg.rule.Chain == "FORWARD" {
if !natRule || cfg.sourceNAT != "" {
s += " --out-interface " + cfg.destIface
}
} else {
s += " --in-interface " + cfg.destIface
}
}
return s
}
func (cfg *iptConfig) FilterTableRule() (s string) {
if cfg.rule.Description != "" {
s = "# " + cfg.rule.Description + "\n"
}
s += "-A " + cfg.rule.Chain
s += cfg.basicSettings(false)
for module, modOptions := range cfg.moduleConfig {
s += " " + module
for option, value := range modOptions {
s += " " + option + " " + value
}
}
s += " -m state --state NEW -j ACCEPT\n"
return s
}
func (cfg *iptConfig) NATTableRule() (s string) {
if cfg.rule.Description != "" {
s = "# " + cfg.rule.Description + "\n"
}
switch {
case cfg.sourceNAT != "" && cfg.destNAT == "":
s += "-A POSTROUTING"
case cfg.sourceNAT == "" && cfg.destNAT != "":
s += "-A PREROUTING"
default:
panic("but you said NAT would be configured?!")
}
s += cfg.basicSettings(true)
switch {
case cfg.sourceNAT == "MASQ":
s += " -j MASQUERADE"
case cfg.sourceNAT != "":
s += " -j SNAT --to " + cfg.sourceNAT
case cfg.destNAT != "":
s += " -j DNAT --to " + cfg.destNAT
}
return s
}
func (t *FirewallTarget) convert(cfg *iptConfig, tType string) {
if t.Port != 0 {
if cfg.rule.Protocol == "" {
panic("port requires the protocol to be specified")
}
module := "-m " + cfg.rule.Protocol
if _, found := cfg.moduleConfig[module]; !found {
cfg.moduleConfig[module] = iptModConfig{}
}
switch tType {
case "src":
cfg.moduleConfig[module]["--source-port"] = strconv.Itoa(t.Port)
case "dest":
cfg.moduleConfig[module]["--destination-port"] = strconv.Itoa(t.Port)
}
}
if t.IP != nil {
switch tType {
case "src":
cfg.sourceIP = t.IP.String()
case "dest":
cfg.destIP = t.IP.String()
}
}
if t.IPSet != "" {
module := "-m set"
if _, found := cfg.moduleConfig[module]; !found {
cfg.moduleConfig[module] = iptModConfig{}
}
value := cfg.moduleConfig[module]["--match-set "+t.IPSet]
set := ""
switch tType {
case "src":
set = "src"
case "dest":
set = "dst"
}
if value != "" {
cfg.moduleConfig[module]["--match-set "+t.IPSet] = value + "," + set
} else {
cfg.moduleConfig[module]["--match-set "+t.IPSet] = set
}
}
if t.Interface != "" {
switch tType {
case "src":
cfg.sourceIface = t.Interface
case "dest":
cfg.destIface = t.Interface
}
}
if t.NAT != "" {
switch tType {
case "src": // for input on the source the destination address can be modified.
cfg.destNAT = t.NAT
case "dest": // for output on the destination the source address can be modified.
cfg.sourceNAT = t.NAT
}
if cfg.sourceNAT != "" && cfg.destNAT != "" {
panic("only source or destination NAT allowed!")
}
}
}
// The target of a rule. It can be specified either by IP or the name of an IPSet. Additional parameters are the port
// and interface used. It's totally valid to only specify a subset (or even none) of the fields. For example IP and
// IPSet must not be given for the host the rule is applied on.
type FirewallTarget struct {
IP net.IP // IP of the target.
IPSet string // IPSet used for matching.
Port int // Port packets must use to match.
Interface string // Interface the packet goes through.
NAT string // NAT configuration (empty, "MASQ", or Interface's IP).
}
const fw_rules_ipv6 = `
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
COMMIT
`
const fw_rules_ipv4 = `*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
# Accept any related or established connections.
-I INPUT 1 -m state --state RELATED,ESTABLISHED -j ACCEPT
-I FORWARD 1 -m state --state RELATED,ESTABLISHED -j ACCEPT
-I OUTPUT 1 -m state --state {{ if not .Paranoid }}NEW,{{ end }}RELATED,ESTABLISHED -j ACCEPT
# Allow all traffic on the loopback interface.
-A INPUT -i lo -j ACCEPT
-A OUTPUT -o lo -j ACCEPT
{{ if .WithVPN }}
# Allow all traffic on the VPN interface.
-A INPUT -i tun0 -j ACCEPT
-A OUTPUT -o tun0 -j ACCEPT
{{ end }}
{{ if .Paranoid }}
# Outbound DNS lookups
-A OUTPUT -o {{ .Interface }} -p udp -m udp --dport 53 -j ACCEPT
# Outbound PING requests
-A OUTPUT -p icmp -j ACCEPT
# Outbound Network Time Protocol (NTP) request
-A OUTPUT -p udp --dport 123 --sport 123 -j ACCEPT
# Allow outbound DHCP request - Some hosts (Linode) automatically assign the primary IP
-A OUTPUT -p udp --dport 67:68 --sport 67:68 -m state --state NEW -j ACCEPT
# Outbound HTTP
-A OUTPUT -o {{ .Interface }} -p tcp -m tcp --dport 80 -m state --state NEW -j ACCEPT
-A OUTPUT -o {{ .Interface }} -p tcp -m tcp --dport 443 -m state --state NEW -j ACCEPT
{{ end }}
# SSH
-A INPUT -i {{ .Interface }} -p tcp -m tcp --dport 22 -m state --state NEW -j ACCEPT
{{ range .Rules }}{{ .Filter }}{{ end }}
{{ if .WithVPN }}
# Outbound OpenVPN traffic (required to connect to the VPN).
-A OUTPUT -o {{ .Interface }} -p tcp -m tcp --dport 1194 -m state --state NEW -j ACCEPT
-A OUTPUT -o {{ .Interface }} -p udp -m udp --dport 1194 -m state --state NEW -j ACCEPT
{{ end }}
COMMIT
*nat
{{ range .Rules }}{{ .NAT }}{{ end }}
COMMIT
`
const fwIpset = `# IPSet configuration
{{ range .IPSets }}{{ .IPSetRestore }}{{ end }}`
const firewallUpstart = `#!/bin/sh
set -e
case "$IFACE" in
{{ .Interface }})
test -e /etc/iptables/ipsets && /usr/sbin/ipset restore -! < /etc/iptables/ipsets
/sbin/iptables-restore < /etc/iptables/rules_ipv4
/sbin/ip6tables-restore < /etc/iptables/rules_ipv6
;;
esac
`