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
|
def iptables_chains_parse(iptables, table = "filter", &block)
assert(block_given?)
cmd = "#{iptables}-save -c -t #{table} | iptables-xml"
xml_str = $vm.execute_successfully(cmd).stdout
rexml = REXML::Document.new(xml_str)
rexml.get_elements('iptables-rules/table/chain').each do |element|
yield(
element.attribute('name').to_s,
element.attribute('policy').to_s,
element.get_elements('rule')
)
end
end
def ip4tables_chains(table = "filter", &block)
iptables_chains_parse('iptables', table, &block)
end
def ip6tables_chains(table = "filter", &block)
iptables_chains_parse('ip6tables', table, &block)
end
def iptables_rules_parse(iptables, chain, table)
iptables_chains_parse(iptables, table) do |name, _, rules|
return rules if name == chain
end
return nil
end
def iptables_rules(chain, table = "filter")
iptables_rules_parse("iptables", chain, table)
end
def ip6tables_rules(chain, table = "filter")
iptables_rules_parse("ip6tables", chain, table)
end
def ip4tables_packet_counter_sum(filters = {})
pkts = 0
ip4tables_chains do |name, _, rules|
next if filters[:tables] && not(filters[:tables].include?(name))
rules.each do |rule|
next if filters[:uid] && not(rule.elements["conditions/owner/uid-owner[text()=#{filters[:uid]}]"])
pkts += rule.attribute('packet-count').to_s.to_i
end
end
return pkts
end
def try_xml_element_text(element, xpath, default = nil)
node = element.elements[xpath]
(node.nil? or not(node.has_text?)) ? default : node.text
end
Then /^the firewall's policy is to (.+) all IPv4 traffic$/ do |expected_policy|
expected_policy.upcase!
ip4tables_chains do |name, policy, _|
if ["INPUT", "FORWARD", "OUTPUT"].include?(name)
assert_equal(expected_policy, policy,
"Chain #{name} has unexpected policy #{policy}")
end
end
end
Then /^the firewall is configured to only allow the (.+) users? to connect directly to the Internet over IPv4$/ do |users_str|
users = users_str.split(/, | and /)
expected_uids = Set.new
users.each do |user|
expected_uids << $vm.execute_successfully("id -u #{user}").stdout.to_i
end
allowed_output = iptables_rules("OUTPUT").find_all do |rule|
out_iface = rule.elements['conditions/match/o']
is_maybe_accepted = rule.get_elements('actions/*').find do |action|
not(["DROP", "REJECT", "LOG"].include?(action.name))
end
is_maybe_accepted &&
(
# nil => match all interfaces according to iptables-xml
out_iface.nil? ||
((out_iface.text == 'lo') == (out_iface.attribute('invert').to_s == '1'))
)
end
uids = Set.new
allowed_output.each do |rule|
rule.elements.each('actions/*') do |action|
destination = try_xml_element_text(rule, "conditions/match/d")
if action.name == "ACCEPT"
# nil == 0.0.0.0/0 according to iptables-xml
assert(destination == '0.0.0.0/0' || destination.nil?,
"The following rule has an unexpected destination:\n" +
rule.to_s)
state_cond = try_xml_element_text(rule, "conditions/state/state")
next if state_cond == "RELATED,ESTABLISHED"
assert_not_nil(rule.elements['conditions/owner/uid-owner'])
rule.elements.each('conditions/owner/uid-owner') do |owner|
uid = owner.text.to_i
uids << uid
assert(expected_uids.include?(uid),
"The following rule allows uid #{uid} to access the " +
"network, but we only expect uids #{expected_uids.to_a} " +
"(#{users_str}) to have such access:\n#{rule.to_s}")
end
elsif action.name == "call" && action.elements[1].name == "lan"
lan_subnets = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
assert(lan_subnets.include?(destination),
"The following lan-targeted rule's destination is " +
"#{destination} which may not be a private subnet:\n" +
rule.to_s)
else
raise "Unexpected iptables OUTPUT chain rule:\n#{rule.to_s}"
end
end
end
uids_not_found = expected_uids - uids
assert(uids_not_found.empty?,
"Couldn't find rules allowing uids #{uids_not_found.to_a.to_s} " \
"access to the network")
end
Then /^the firewall's NAT rules only redirect traffic for Tor's TransPort and DNSPort$/ do
loopback_address = "127.0.0.1/32"
tor_onion_addr_space = "127.192.0.0/10"
tor_trans_port = "9040"
dns_port = "53"
tor_dns_port = "5353"
ip4tables_chains('nat') do |name, _, rules|
if name == "OUTPUT"
good_rules = rules.find_all do |rule|
redirect = rule.get_elements('actions/*').all? do |action|
action.name == "REDIRECT"
end
destination = try_xml_element_text(rule, "conditions/match/d")
redir_port = try_xml_element_text(rule, "actions/REDIRECT/to-ports")
redirected_to_trans_port = redir_port == tor_trans_port
udp_destination_port = try_xml_element_text(rule, "conditions/udp/dport")
dns_redirected_to_tor_dns_port = (udp_destination_port == dns_port) &&
(redir_port == tor_dns_port)
redirect &&
(
(destination == tor_onion_addr_space && redirected_to_trans_port) ||
(destination == loopback_address && dns_redirected_to_tor_dns_port)
)
end
bad_rules = rules - good_rules
assert(bad_rules.empty?,
"The NAT table's OUTPUT chain contains some unexpected " +
"rules:\n#{bad_rules}")
else
assert(rules.empty?,
"The NAT table contains unexpected rules for the #{name} " +
"chain:\n#{rules}")
end
end
end
Then /^the firewall is configured to block all external IPv6 traffic$/ do
ip6_loopback = '::1/128'
expected_policy = "DROP"
ip6tables_chains do |name, policy, rules|
assert_equal(expected_policy, policy,
"The IPv6 #{name} chain has policy #{policy} but we " \
"expected #{expected_policy}")
good_rules = rules.find_all do |rule|
["DROP", "REJECT", "LOG"].any? do |target|
rule.elements["actions/#{target}"]
end \
||
["s", "d"].all? do |x|
try_xml_element_text(rule, "conditions/match/#{x}") == ip6_loopback
end
end
bad_rules = rules - good_rules
assert(bad_rules.empty?,
"The IPv6 table's #{name} chain contains some unexpected rules:\n" +
(bad_rules.map { |r| r.to_s }).join("\n"))
end
end
def firewall_has_dropped_packet_to?(proto, host, port)
regex = "^Dropped outbound packet: .* "
regex += "DST=#{Regexp.escape(host)} .* "
regex += "PROTO=#{Regexp.escape(proto)} "
regex += ".* DPT=#{port} " if port
$vm.execute("journalctl --dmesg --output=cat | grep -qP '#{regex}'").success?
end
When /^I open an untorified (TCP|UDP|ICMP) connections to (\S*)(?: on port (\d+))? that is expected to fail$/ do |proto, host, port|
assert(!firewall_has_dropped_packet_to?(proto, host, port),
"A #{proto} packet to #{host}" +
(port.nil? ? "" : ":#{port}") +
" has already been dropped by the firewall")
@conn_proto = proto
@conn_host = host
@conn_port = port
case proto
when "TCP"
assert_not_nil(port)
cmd = "echo | netcat #{host} #{port}"
user = LIVE_USER
when "UDP"
assert_not_nil(port)
cmd = "echo | netcat -u #{host} #{port}"
user = LIVE_USER
when "ICMP"
cmd = "ping -c 5 #{host}"
user = 'root'
end
@conn_res = $vm.execute(cmd, :user => user)
end
Then /^the untorified connection fails$/ do
case @conn_proto
when "TCP"
expected_in_stderr = "Connection refused"
conn_failed = !@conn_res.success? &&
@conn_res.stderr.chomp.end_with?(expected_in_stderr)
when "UDP", "ICMP"
conn_failed = !@conn_res.success?
end
assert(conn_failed,
"The untorified #{@conn_proto} connection didn't fail as expected:\n" +
@conn_res.to_s)
end
Then /^the untorified connection is logged as dropped by the firewall$/ do
assert(firewall_has_dropped_packet_to?(@conn_proto, @conn_host, @conn_port),
"No #{@conn_proto} packet to #{@conn_host}" +
(@conn_port.nil? ? "" : ":#{@conn_port}") +
" was dropped by the firewall")
end
When /^the system DNS is(?: still)? using the local DNS resolver$/ do
resolvconf = $vm.file_content("/etc/resolv.conf")
bad_lines = resolvconf.split("\n").find_all do |line|
!line.start_with?("#") && !/^nameserver\s+127\.0\.0\.1$/.match(line)
end
assert_empty(bad_lines,
"The following bad lines were found in /etc/resolv.conf:\n" +
bad_lines.join("\n"))
end
def stream_isolation_info(application)
case application
when "htpdate"
{
:grep_monitor_expr => '/curl\>',
:socksport => 9062
}
when "tails-security-check", "tails-upgrade-frontend-wrapper"
# We only grep connections with ESTABLISHED state since `perl`
# is also used by monkeysphere's validation agent, which LISTENs
{
:grep_monitor_expr => '\<ESTABLISHED\>.\+/perl\>',
:socksport => 9062
}
when "Tor Browser"
{
:grep_monitor_expr => '/firefox\>',
:socksport => 9150
}
when "Gobby"
{
:grep_monitor_expr => '/gobby\>',
:socksport => 9050
}
when "SSH"
{
:grep_monitor_expr => '/\(connect-proxy\|ssh\)\>',
:socksport => 9050
}
when "whois"
{
:grep_monitor_expr => '/whois\>',
:socksport => 9050
}
else
raise "Unknown application '#{application}' for the stream isolation tests"
end
end
When /^I monitor the network connections of (.*)$/ do |application|
@process_monitor_log = "/tmp/netstat.log"
info = stream_isolation_info(application)
$vm.spawn("while true; do " +
" netstat -taupen | grep \"#{info[:grep_monitor_expr]}\"; " +
" sleep 0.1; " +
"done > #{@process_monitor_log}")
end
Then /^I see that (.+) is properly stream isolated$/ do |application|
expected_port = stream_isolation_info(application)[:socksport]
assert_not_nil(@process_monitor_log)
log_lines = $vm.file_content(@process_monitor_log).split("\n")
assert(log_lines.size > 0,
"Couldn't see any connection made by #{application} so " \
"something is wrong")
log_lines.each do |line|
addr_port = line.split(/\s+/)[4]
assert_equal("127.0.0.1:#{expected_port}", addr_port,
"#{application} should use SocksPort #{expected_port} but " \
"was seen connecting to #{addr_port}")
end
end
And /^I re-run tails-security-check$/ do
$vm.execute_successfully("tails-security-check", :user => LIVE_USER)
end
And /^I re-run htpdate$/ do
$vm.execute_successfully("service htpdate stop && " \
"rm -f /var/run/htpdate/* && " \
"systemctl --no-block start htpdate.service")
step "the time has synced"
end
And /^I re-run tails-upgrade-frontend-wrapper$/ do
$vm.execute_successfully("tails-upgrade-frontend-wrapper", :user => LIVE_USER)
end
When /^I connect Gobby to "([^"]+)"$/ do |host|
@screen.wait("GobbyWindow.png", 30)
@screen.wait("GobbyWelcomePrompt.png", 10)
@screen.click("GnomeCloseButton.png")
@screen.wait("GobbyWindow.png", 10)
# This indicates that Gobby has finished initializing itself
# (generating DH parameters, etc.) -- before, the UI is not responsive
# and our CTRL-t is lost.
@screen.wait("GobbyFailedToShareDocuments.png", 30)
@screen.type("t", Sikuli::KeyModifier.CTRL)
@screen.wait("GobbyConnectPrompt.png", 10)
@screen.type(host + Sikuli::Key.ENTER)
@screen.wait("GobbyConnectionComplete.png", 60)
end
When /^the Tor Launcher autostarts$/ do
@screen.wait('TorLauncherWindow.png', 60)
end
When /^I configure some (\w+) pluggable transports in Tor Launcher$/ do |bridge_type|
bridge_type.downcase!
bridge_type.capitalize!
begin
@bridges = $config["Tor"]["Transports"][bridge_type]
assert_not_nil(@bridges)
assert(!@bridges.empty?)
rescue NoMethodError, Test::Unit::AssertionFailedError
raise(
<<EOF
It seems no '#{bridge_type}' pluggable transports are defined in your local configuration file (#{LOCAL_CONFIG_FILE}). See wiki/src/contribute/release_process/test/usage.mdwn for the format.
EOF
)
end
@bridge_hosts = []
for bridge in @bridges do
@bridge_hosts << bridge["ipv4_address"]
end
@screen.wait_and_click('TorLauncherConfigureButton.png', 10)
@screen.wait('TorLauncherBridgePrompt.png', 10)
@screen.wait_and_click('TorLauncherYesRadioOption.png', 10)
@screen.wait_and_click('TorLauncherNextButton.png', 10)
@screen.wait_and_click('TorLauncherBridgeList.png', 10)
for bridge in @bridges do
bridge_line = bridge_type.downcase + " " +
bridge["ipv4_address"] + ":" +
bridge["ipv4_port"].to_s
bridge_line += " " + bridge["fingerprint"].to_s if bridge["fingerprint"]
bridge_line += " " + bridge["extra"].to_s if bridge["extra"]
@screen.type(bridge_line + Sikuli::Key.ENTER)
end
@screen.wait_and_click('TorLauncherNextButton.png', 10)
@screen.hide_cursor
@screen.wait_and_click('TorLauncherFinishButton.png', 10)
@screen.wait('TorLauncherConnectingWindow.png', 10)
@screen.waitVanish('TorLauncherConnectingWindow.png', 120)
end
When /^all Internet traffic has only flowed through the configured pluggable transports$/ do
assert_not_nil(@bridge_hosts, "No bridges has been configured via the " +
"'I configure some ... bridges in Tor Launcher' step")
leaks = FirewallLeakCheck.new(@sniffer.pcap_file,
:accepted_hosts => @bridge_hosts)
leaks.assert_no_leaks
end
Then /^the Tor binary is configured to use the expected Tor authorities$/ do
tor_auths = Set.new
tor_binary_orport_strings = $vm.execute_successfully(
"strings /usr/bin/tor | grep -E 'orport=[0-9]+'").stdout.chomp.split("\n")
tor_binary_orport_strings.each do |potential_auth_string|
auth_regex = /^\S+ orport=\d+( bridge)?( no-v2)?( v3ident=[A-Z0-9]{40})? ([0-9\.]+):\d+( [A-Z0-9]{4}){10}$/
m = auth_regex.match(potential_auth_string)
if m
auth_ipv4_addr = m[4]
tor_auths << auth_ipv4_addr
end
end
expected_tor_auths = Set.new(TOR_AUTHORITIES)
assert_equal(expected_tor_auths, tor_auths,
"The Tor binary does not have the expected Tor authorities " +
"configured")
end
|