summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/chroot_local-includes/usr/local/sbin/autotest_remote_shell.py13
-rw-r--r--features/apt.feature5
-rw-r--r--features/firewall_leaks.feature37
-rw-r--r--features/images/GnomeApplicationsGobby.pngbin0 -> 1861 bytes
-rw-r--r--features/images/GnomeCloseButton.pngbin0 -> 1069 bytes
-rw-r--r--features/images/GobbyConnectPrompt.pngbin0 -> 2020 bytes
-rw-r--r--features/images/GobbyConnectionComplete.pngbin0 -> 994 bytes
-rw-r--r--features/images/GobbyWelcomePrompt.pngbin0 -> 3411 bytes
-rw-r--r--features/images/GobbyWindow.pngbin0 -> 1271 bytes
-rw-r--r--features/images/SSHAuthVerification.pngbin0 -> 2119 bytes
-rw-r--r--features/pidgin.feature7
-rw-r--r--features/step_definitions/common_steps.rb39
-rw-r--r--features/step_definitions/erase_memory.rb4
-rw-r--r--features/step_definitions/firewall_leaks.rb10
-rw-r--r--features/step_definitions/i2p.rb2
-rw-r--r--features/step_definitions/tor.rb320
-rw-r--r--features/step_definitions/torified_misc.rb4
-rw-r--r--features/support/helpers/exec_helper.rb16
-rw-r--r--features/support/helpers/firewall_helper.rb34
-rw-r--r--features/support/helpers/misc_helpers.rb8
-rw-r--r--features/support/helpers/sniffing_helper.rb (renamed from features/support/helpers/net_helper.rb)9
-rw-r--r--features/support/helpers/storage_helper.rb8
-rw-r--r--features/support/helpers/vm_helper.rb148
-rw-r--r--features/support/hooks.rb27
-rw-r--r--features/time_syncing.feature2
-rw-r--r--features/tor_enforcement.feature72
-rw-r--r--features/tor_stream_isolation.feature60
-rw-r--r--features/torified_browsing.feature8
-rw-r--r--features/torified_git.feature6
-rw-r--r--features/torified_gnupg.feature5
-rw-r--r--features/torified_misc.feature7
-rw-r--r--features/totem.feature3
-rw-r--r--wiki/src/contribute/release_process/test.mdwn159
33 files changed, 647 insertions, 366 deletions
diff --git a/config/chroot_local-includes/usr/local/sbin/autotest_remote_shell.py b/config/chroot_local-includes/usr/local/sbin/autotest_remote_shell.py
index 8778ddd..77a5309 100644
--- a/config/chroot_local-includes/usr/local/sbin/autotest_remote_shell.py
+++ b/config/chroot_local-includes/usr/local/sbin/autotest_remote_shell.py
@@ -19,16 +19,15 @@ def mk_switch_user_fn(uid, gid):
return switch_user
def run_cmd_as_user(cmd, user):
- env = environ.copy()
pwd_user = getpwnam(user)
switch_user_fn = mk_switch_user_fn(pwd_user.pw_uid,
pwd_user.pw_gid)
- env['USER'] = user
- env['LOGNAME'] = user
- env['USERNAME'] = user
- env['HOME'] = pwd_user.pw_dir
- env['MAIL'] = "/var/mail/" + user
- env['PWD'] = env['HOME']
+ # We try to create an environment identical to what's expected
+ # inside Tails for the user by logging in (via `su`) as the user and
+ # extracting the environment.
+ pipe = Popen('su -c env ' + user, stdout=PIPE, shell=True)
+ env_data = pipe.communicate()[0]
+ env = dict((line.split('=', 1) for line in env_data.splitlines()))
env['DISPLAY'] = ':0.0'
try:
env['XAUTHORITY'] = glob("/var/run/gdm3/auth-for-amnesia-*/database")[0]
diff --git a/features/apt.feature b/features/apt.feature
index e86d3c6..689a44e 100644
--- a/features/apt.feature
+++ b/features/apt.feature
@@ -7,7 +7,6 @@ Feature: Installing packages through APT
Background:
Given a computer
- And I capture all network traffic
And I start the computer
And the computer boots Tails
And I enable more Tails Greeter options
@@ -22,13 +21,13 @@ Feature: Installing packages through APT
Scenario: APT sources are configured correctly
Then the only hosts in APT sources are "ftp.us.debian.org,security.debian.org,backports.debian.org,deb.tails.boum.org,deb.torproject.org,mozilla.debian.net"
+ @check_tor_leaks
Scenario: Install packages using apt-get
When I update APT using apt-get
Then I should be able to install a package using apt-get
- And all Internet traffic has only flowed through Tor
+ @check_tor_leaks
Scenario: Install packages using Synaptic
When I start Synaptic
And I update APT using Synaptic
Then I should be able to install a package using Synaptic
- And all Internet traffic has only flowed through Tor
diff --git a/features/firewall_leaks.feature b/features/firewall_leaks.feature
deleted file mode 100644
index 775c6e1..0000000
--- a/features/firewall_leaks.feature
+++ /dev/null
@@ -1,37 +0,0 @@
-@product
-Feature:
- As a Tails developer
- I want to ensure that the automated test suite detects firewall leaks reliably
-
- Background:
- Given a computer
- And I capture all network traffic
- And I start the computer
- And the computer boots Tails
- And I log in to a new session
- And Tor is ready
- And all notifications have disappeared
- And available upgrades have been checked
- And all Internet traffic has only flowed through Tor
- And I save the state so the background can be restored next scenario
-
- Scenario: Detecting IPv4 TCP leaks from the Unsafe Browser
- When I successfully start the Unsafe Browser
- And I open the address "https://check.torproject.org" in the Unsafe Browser
- And I see "UnsafeBrowserTorCheckFail.png" after at most 60 seconds
- Then the firewall leak detector has detected IPv4 TCP leaks
-
- Scenario: Detecting IPv4 TCP leaks of TCP DNS lookups
- Given I disable Tails' firewall
- When I do a TCP DNS lookup of "torproject.org"
- Then the firewall leak detector has detected IPv4 TCP leaks
-
- Scenario: Detecting IPv4 non-TCP leaks (UDP) of UDP DNS lookups
- Given I disable Tails' firewall
- When I do a UDP DNS lookup of "torproject.org"
- Then the firewall leak detector has detected IPv4 non-TCP leaks
-
- Scenario: Detecting IPv4 non-TCP (ICMP) leaks of ping
- Given I disable Tails' firewall
- When I send some ICMP pings
- Then the firewall leak detector has detected IPv4 non-TCP leaks
diff --git a/features/images/GnomeApplicationsGobby.png b/features/images/GnomeApplicationsGobby.png
new file mode 100644
index 0000000..dced881
--- /dev/null
+++ b/features/images/GnomeApplicationsGobby.png
Binary files differ
diff --git a/features/images/GnomeCloseButton.png b/features/images/GnomeCloseButton.png
new file mode 100644
index 0000000..bf1efe1
--- /dev/null
+++ b/features/images/GnomeCloseButton.png
Binary files differ
diff --git a/features/images/GobbyConnectPrompt.png b/features/images/GobbyConnectPrompt.png
new file mode 100644
index 0000000..03c358b
--- /dev/null
+++ b/features/images/GobbyConnectPrompt.png
Binary files differ
diff --git a/features/images/GobbyConnectionComplete.png b/features/images/GobbyConnectionComplete.png
new file mode 100644
index 0000000..db904c7
--- /dev/null
+++ b/features/images/GobbyConnectionComplete.png
Binary files differ
diff --git a/features/images/GobbyWelcomePrompt.png b/features/images/GobbyWelcomePrompt.png
new file mode 100644
index 0000000..861a5f1
--- /dev/null
+++ b/features/images/GobbyWelcomePrompt.png
Binary files differ
diff --git a/features/images/GobbyWindow.png b/features/images/GobbyWindow.png
new file mode 100644
index 0000000..873f391
--- /dev/null
+++ b/features/images/GobbyWindow.png
Binary files differ
diff --git a/features/images/SSHAuthVerification.png b/features/images/SSHAuthVerification.png
new file mode 100644
index 0000000..abfa39a
--- /dev/null
+++ b/features/images/SSHAuthVerification.png
Binary files differ
diff --git a/features/pidgin.feature b/features/pidgin.feature
index 67d7d75..83330ce 100644
--- a/features/pidgin.feature
+++ b/features/pidgin.feature
@@ -9,11 +9,11 @@ Feature: Chatting anonymously using Pidgin
Background:
Given a computer
- And I capture all network traffic
When I start Tails from DVD and I login
Then Pidgin has the expected accounts configured with random nicknames
And I save the state so the background can be restored next scenario
+ @check_tor_leaks
Scenario: Connecting to the #tails IRC channel with the pre-configured account
When I start Pidgin through the GNOME menu
Then I see Pidgin's account manager window
@@ -26,7 +26,6 @@ Feature: Chatting anonymously using Pidgin
Then I see the Tails roadmap URL
When I click on the Tails roadmap URL
Then the Tor Browser has started and loaded the Tails roadmap
- And all Internet traffic has only flowed through Tor
Scenario: Adding a certificate to Pidgin
And I start Pidgin through the GNOME menu
@@ -40,7 +39,7 @@ Feature: Chatting anonymously using Pidgin
And I close Pidgin's account manager window
Then I cannot add a certificate from the "/home/amnesia/.gnupg" directory to Pidgin
- @keep_volumes
+ @keep_volumes @check_tor_leaks
Scenario: Using a persistent Pidgin configuration
Given the USB drive "current" contains Tails with persistence configured and password "asdf"
And a computer
@@ -52,7 +51,6 @@ Feature: Chatting anonymously using Pidgin
# And I take note of the OTR key for Pidgin's "irc.oftc.net" account
And I shutdown Tails and wait for the computer to power off
Given a computer
- And I capture all network traffic
And I start Tails from USB drive "current" and I login with persistence password "asdf"
And Pidgin has the expected persistent accounts configured
# And Pidgin has the expected persistent OTR keys
@@ -62,7 +60,6 @@ Feature: Chatting anonymously using Pidgin
And I close Pidgin's account manager window
Then Pidgin successfully connects to the "irc.oftc.net" account
And I can join the "#tails" channel on "irc.oftc.net"
- And all Internet traffic has only flowed through Tor
# Exercise Pidgin AppArmor profile with persistence enabled.
# This should really be in dedicated scenarios, but it would be
# too costly to set up the virtual USB drive with persistence more
diff --git a/features/step_definitions/common_steps.rb b/features/step_definitions/common_steps.rb
index f5e298b..22b4296 100644
--- a/features/step_definitions/common_steps.rb
+++ b/features/step_definitions/common_steps.rb
@@ -56,14 +56,14 @@ def restore_background
@vm.host_to_guest_time_sync
@vm.execute("service tor start")
wait_until_tor_is_working
- @vm.spawn("/usr/local/sbin/restart-vidalia")
+ @vm.spawn("restart-vidalia")
end
end
end
Given /^a computer$/ do
- @vm.destroy if @vm
- @vm = VM.new(VM_XML_PATH, DISPLAY)
+ @vm.destroy_and_undefine if @vm
+ @vm = VM.new($virt, VM_XML_PATH, $vmnet, $vmstorage, DISPLAY)
end
Given /^the computer has (\d+) ([[:alpha:]]+) of RAM$/ do |size, unit|
@@ -108,12 +108,14 @@ Then /^drive "([^"]+)" is detected by Tails$/ do |name|
end
Given /^the network is plugged$/ do
- next if @skip_steps_while_restoring_background
+ # We don't skip this step when restoring the background to ensure
+ # that the network state is actually the same after restoring as
+ # when the snapshot was made.
@vm.plug_network
end
Given /^the network is unplugged$/ do
- next if @skip_steps_while_restoring_background
+ # See comment in the step "the network is plugged".
@vm.unplug_network
end
@@ -121,7 +123,7 @@ Given /^I capture all network traffic$/ do
# Note: We don't want skip this particular stpe if
# @skip_steps_while_restoring_background is set since it starts
# something external to the VM state.
- @sniffer = Sniffer.new("TestSniffer", @vm.net.bridge_name)
+ @sniffer = Sniffer.new("sniffer", $vmnet)
@sniffer.capture
end
@@ -205,7 +207,7 @@ end
When /^I destroy the computer$/ do
next if @skip_steps_while_restoring_background
- @vm.destroy
+ @vm.destroy_and_undefine
end
Given /^the computer (re)?boots Tails$/ do |reboot|
@@ -412,28 +414,7 @@ end
Then /^all Internet traffic has only flowed through Tor$/ do
next if @skip_steps_while_restoring_background
leaks = FirewallLeakCheck.new(@sniffer.pcap_file, get_tor_relays)
- if !leaks.empty?
- if !leaks.ipv4_tcp_leaks.empty?
- puts "The following IPv4 TCP non-Tor Internet hosts were contacted:"
- puts leaks.ipv4_tcp_leaks.join("\n")
- puts
- end
- if !leaks.ipv4_nontcp_leaks.empty?
- puts "The following IPv4 non-TCP Internet hosts were contacted:"
- puts leaks.ipv4_nontcp_leaks.join("\n")
- puts
- end
- if !leaks.ipv6_leaks.empty?
- puts "The following IPv6 Internet hosts were contacted:"
- puts leaks.ipv6_leaks.join("\n")
- puts
- end
- if !leaks.nonip_leaks.empty?
- puts "Some non-IP packets were sent\n"
- end
- save_pcap_file
- raise "There were network leaks!"
- end
+ leaks.assert_no_leaks
end
Given /^I enter the sudo password in the gksu prompt$/ do
diff --git a/features/step_definitions/erase_memory.rb b/features/step_definitions/erase_memory.rb
index 97970c7..b4bb56b 100644
--- a/features/step_definitions/erase_memory.rb
+++ b/features/step_definitions/erase_memory.rb
@@ -17,7 +17,7 @@ Given /^the computer is an old pentium without the PAE extension$/ do
end
def which_kernel
- kernel_path = @vm.execute("/usr/local/bin/tails-get-bootinfo kernel").stdout.chomp
+ kernel_path = @vm.execute("tails-get-bootinfo kernel").stdout.chomp
return File.basename(kernel_path)
end
@@ -105,7 +105,7 @@ Given /^I fill the guest's memory with a known pattern(| without verifying)$/ do
# since the others otherwise may continue re-filling the same memory
# unnecessarily.
instances = (@detected_ram_m.to_f/(2**10)).ceil
- instances.times { @vm.spawn('/usr/local/sbin/fillram; killall fillram') }
+ instances.times { @vm.spawn('fillram; killall fillram') }
# We make sure that the filling has started...
try_for(10, { :msg => "fillram didn't start" }) {
@vm.has_process?("fillram")
diff --git a/features/step_definitions/firewall_leaks.rb b/features/step_definitions/firewall_leaks.rb
index 5b967ad..8a27ff3 100644
--- a/features/step_definitions/firewall_leaks.rb
+++ b/features/step_definitions/firewall_leaks.rb
@@ -4,22 +4,22 @@ Then(/^the firewall leak detector has detected (.*?) leaks$/) do |type|
case type.downcase
when 'ipv4 tcp'
if leaks.ipv4_tcp_leaks.empty?
- save_pcap_file
+ leaks.save_pcap_file
raise "Couldn't detect any IPv4 TCP leaks"
end
when 'ipv4 non-tcp'
if leaks.ipv4_nontcp_leaks.empty?
- save_pcap_file
+ leaks.save_pcap_file
raise "Couldn't detect any IPv4 non-TCP leaks"
end
when 'ipv6'
if leaks.ipv6_leaks.empty?
- save_pcap_file
+ leaks.save_pcap_file
raise "Couldn't detect any IPv6 leaks"
end
when 'non-ip'
if leaks.nonip_leaks.empty?
- save_pcap_file
+ leaks.save_pcap_file
raise "Couldn't detect any non-IP leaks"
end
else
@@ -29,7 +29,7 @@ end
Given(/^I disable Tails' firewall$/) do
next if @skip_steps_while_restoring_background
- @vm.execute("/usr/local/sbin/do_not_ever_run_me")
+ @vm.execute("do_not_ever_run_me")
iptables = @vm.execute("iptables -L -n -v").stdout.chomp.split("\n")
for line in iptables do
if !line[/Chain (INPUT|OUTPUT|FORWARD) \(policy ACCEPT/] and
diff --git a/features/step_definitions/i2p.rb b/features/step_definitions/i2p.rb
index fb41e12..4a20afa 100644
--- a/features/step_definitions/i2p.rb
+++ b/features/step_definitions/i2p.rb
@@ -50,8 +50,10 @@ Then /^the I2P firewall rules are (enabled|disabled)$/ do |mode|
accept_rules_count = accept_rules.lines.count
if mode == 'enabled'
assert_equal(13, accept_rules_count)
+ step 'the firewall is configured to only allow the clearnet, i2psvc and debian-tor users to connect directly to the Internet over IPv4'
elsif mode == 'disabled'
assert_equal(0, accept_rules_count)
+ step 'the firewall is configured to only allow the clearnet and debian-tor users to connect directly to the Internet over IPv4'
else
raise "Unsupported mode passed: '#{mode}'"
end
diff --git a/features/step_definitions/tor.rb b/features/step_definitions/tor.rb
new file mode 100644
index 0000000..de6de52
--- /dev/null
+++ b/features/step_definitions/tor.rb
@@ -0,0 +1,320 @@
+def iptables_parse(iptables_output)
+ chains = Hash.new
+ cur_chain = nil
+ cur_chain_policy = nil
+ parser_state = :expecting_chain_def
+
+ iptables_output.split("\n").each do |line|
+ line.strip!
+ if /^Chain /.match(line)
+ assert_equal(:expecting_chain_def, parser_state)
+ m = /^Chain (.+) \(policy (.+) \d+ packets, \d+ bytes\)$/.match(line)
+ if m.nil?
+ m = /^Chain (.+) \(\d+ references\)$/.match(line)
+ end
+ assert_not_nil m
+ _, cur_chain, cur_chain_policy = m.to_a
+ chains[cur_chain] = {
+ "policy" => cur_chain_policy,
+ "rules" => Array.new
+ }
+ parser_state = :expecting_col_descs
+ elsif /^pkts\s/.match(line)
+ assert_equal(:expecting_col_descs, parser_state)
+ assert_equal(["pkts", "bytes", "target", "prot", "opt",
+ "in", "out", "source", "destination"],
+ line.split(/\s+/))
+ parser_state = :expecting_rule_or_empty
+ elsif line.empty?
+ assert_equal(:expecting_rule_or_empty, parser_state)
+ cur_chain = nil
+ parser_state = :expecting_chain_def
+ else
+ assert_equal(:expecting_rule_or_empty, parser_state)
+ _, _, target, prot, opt, in_iface, out_iface, source, destination, extra =
+ line.split(/\s+/, 10)
+ [target, prot, opt, in_iface, out_iface, source, destination].each do |var|
+ assert_not_empty(var)
+ assert_not_nil(var)
+ end
+ chains[cur_chain]["rules"] << {
+ "rule" => line,
+ "target" => target,
+ "protocol" => prot,
+ "opt" => opt,
+ "in_iface" => in_iface,
+ "out_iface" => out_iface,
+ "source" => source,
+ "destination" => destination,
+ "extra" => extra
+ }
+ end
+ end
+ assert_equal(:expecting_rule_or_empty, parser_state)
+ return chains
+end
+
+Then /^the firewall's policy is to (.+) all IPv4 traffic$/ do |expected_policy|
+ next if @skip_steps_while_restoring_background
+ expected_policy.upcase!
+ iptables_output = @vm.execute_successfully("iptables -L -n -v").stdout
+ chains = iptables_parse(iptables_output)
+ ["INPUT", "FORWARD", "OUTPUT"].each do |chain_name|
+ policy = chains[chain_name]["policy"]
+ assert_equal(expected_policy, policy,
+ "Chain #{chain_name} has unexpected policy #{policy}")
+ end
+end
+
+Then /^the firewall is configured to only allow the (.+) users? to connect directly to the Internet over IPv4$/ do |users_str|
+ next if @skip_steps_while_restoring_background
+ 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
+ iptables_output = @vm.execute_successfully("iptables -L -n -v").stdout
+ chains = iptables_parse(iptables_output)
+ allowed_output = chains["OUTPUT"]["rules"].find_all do |rule|
+ !(["DROP", "REJECT", "LOG"].include? rule["target"]) &&
+ rule["out_iface"] != "lo"
+ end
+ uids = Set.new
+ allowed_output.each do |rule|
+ case rule["target"]
+ when "ACCEPT"
+ expected_destination = "0.0.0.0/0"
+ assert_equal(expected_destination, rule["destination"],
+ "The following rule has an unexpected destination:\n" +
+ rule["rule"])
+ next if rule["extra"] == "state RELATED,ESTABLISHED"
+ m = /owner UID match (\d+)/.match(rule["extra"])
+ assert_not_nil(m)
+ uid = m[1].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} (#{users_str}) to " \
+ "have such access:\n#{rule["rule"]}")
+ when "lan"
+ lan_subnets = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
+ assert(lan_subnets.include?(rule["destination"]),
+ "The following lan-targeted rule's destination is " \
+ "#{rule["destination"]} which may not be a private subnet:\n" +
+ rule["rule"])
+ else
+ raise "Unexpected iptables OUTPUT chain rule:\n#{rule["rule"]}"
+ 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
+ next if @skip_steps_while_restoring_background
+ tor_onion_addr_space = "127.192.0.0/10"
+ iptables_nat_output = @vm.execute_successfully("iptables -t nat -L -n -v").stdout
+ chains = iptables_parse(iptables_nat_output)
+ chains.each_pair do |name, chain|
+ rules = chain["rules"]
+ if name == "OUTPUT"
+ good_rules = rules.find_all do |rule|
+ rule["target"] == "REDIRECT" &&
+ (
+ (
+ rule["destination"] == tor_onion_addr_space &&
+ rule["extra"] == "redir ports 9040"
+ ) ||
+ rule["extra"] == "udp dpt:53 redir ports 5353"
+ )
+ end
+ assert_equal(rules, good_rules,
+ "The NAT table's OUTPUT chain contains some unexptected " \
+ "rules:\n" +
+ ((rules - good_rules).map { |r| r["rule"] }).join("\n"))
+ else
+ assert(rules.empty?,
+ "The NAT table contains unexpected rules for the #{name} " \
+ "chain:\n" + (rules.map { |r| r["rule"] }).join("\n"))
+ end
+ end
+end
+
+Then /^the firewall is configured to block all IPv6 traffic$/ do
+ next if @skip_steps_while_restoring_background
+ expected_policy = "DROP"
+ ip6tables_output = @vm.execute_successfully("ip6tables -L -n -v").stdout
+ chains = iptables_parse(ip6tables_output)
+ chains.each_pair do |name, chain|
+ policy = chain["policy"]
+ assert_equal(expected_policy, policy,
+ "The IPv6 #{name} chain has policy #{policy} but we " \
+ "expected #{expected_policy}")
+ rules = chain["rules"]
+ bad_rules = rules.find_all do |rule|
+ !["DROP", "REJECT", "LOG"].include?(rule["target"])
+ end
+ assert(bad_rules.empty?,
+ "The IPv6 table's #{name} chain contains some unexptected rules:\n" +
+ (bad_rules.map { |r| r["rule"] }).join("\n"))
+ end
+end
+
+def firewall_has_dropped_packet_to?(proto, host, port)
+ regex = "Dropped outbound packet: .* DST=#{host} .* PROTO=#{proto} "
+ regex += ".* DPT=#{port} " if port
+ @vm.execute("grep -q '#{regex}' /var/log/syslog").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|
+ next if @skip_steps_while_restoring_background
+ 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}"
+ when "UDP"
+ assert_not_nil(port)
+ cmd = "echo | netcat -u #{host} #{port}"
+ when "ICMP"
+ cmd = "ping -c 5 #{host}"
+ end
+ @conn_res = @vm.execute(cmd, LIVE_USER)
+end
+
+Then /^the untorified connection fails$/ do
+ next if @skip_steps_while_restoring_background
+ 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
+ next if @skip_steps_while_restoring_background
+ 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
+ next if @skip_steps_while_restoring_background
+ 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|
+ next if @skip_steps_while_restoring_background
+ @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|
+ next if @skip_steps_while_restoring_background
+ 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
+ next if @skip_steps_while_restoring_background
+ @vm.execute_successfully("tails-security-check", LIVE_USER)
+end
+
+And /^I re-run htpdate$/ do
+ next if @skip_steps_while_restoring_background
+ @vm.execute_successfully("service htpdate stop && " \
+ "rm -f /var/run/htpdate/* && " \
+ "service htpdate start")
+ step "the time has synced"
+end
+
+And /^I re-run tails-upgrade-frontend-wrapper$/ do
+ next if @skip_steps_while_restoring_background
+ @vm.execute_successfully("tails-upgrade-frontend-wrapper", LIVE_USER)
+end
+
+When /^I connect Gobby to "([^"]+)"$/ do |host|
+ next if @skip_steps_while_restoring_background
+ @screen.wait("GobbyWindow.png", 30)
+ @screen.wait("GobbyWelcomePrompt.png", 10)
+ @screen.click("GnomeCloseButton.png")
+ @screen.wait("GobbyWindow.png", 10)
+ @screen.type("t", Sikuli::KeyModifier.CTRL)
+ @screen.wait("GobbyConnectPrompt.png", 10)
+ @screen.type(host + Sikuli::Key.ENTER)
+ @screen.wait("GobbyConnectionComplete.png", 60)
+end
diff --git a/features/step_definitions/torified_misc.rb b/features/step_definitions/torified_misc.rb
index d24b2c9..610234f 100644
--- a/features/step_definitions/torified_misc.rb
+++ b/features/step_definitions/torified_misc.rb
@@ -1,7 +1,7 @@
When /^I query the whois directory service for "([^"]+)"$/ do |domain|
next if @skip_steps_while_restoring_background
@vm_execute_res = @vm.execute(
- "/usr/local/bin/whois '#{domain}'",
+ "whois '#{domain}'",
LIVE_USER)
end
@@ -10,7 +10,7 @@ When /^I wget "([^"]+)" to stdout(?:| with the '([^']+)' options)$/ do |url, opt
arguments = "-O - '#{url}'"
arguments = "#{options} #{arguments}" if options
@vm_execute_res = @vm.execute(
- "/usr/local/bin/wget #{arguments}",
+ "wget #{arguments}",
LIVE_USER)
end
diff --git a/features/support/helpers/exec_helper.rb b/features/support/helpers/exec_helper.rb
index 43130bc..400b0ae 100644
--- a/features/support/helpers/exec_helper.rb
+++ b/features/support/helpers/exec_helper.rb
@@ -11,12 +11,10 @@ class VMCommand
end
def VMCommand.wait_until_remote_shell_is_up(vm, timeout = 30)
- begin
- Timeout::timeout(timeout) do
- VMCommand.execute(vm, "true", { :user => "root", :spawn => false })
+ try_for(30, :msg => "Remote shell seems to be down") do
+ Timeout::timeout(3) do
+ VMCommand.execute(vm, "echo 'hello?'")
end
- rescue Timeout::Error
- raise "Remote shell seems to be down"
end
end
@@ -58,4 +56,12 @@ class VMCommand
return @returncode == 0
end
+ def to_s
+ "Return status: #{@returncode}\n" +
+ "STDOUT:\n" +
+ @stdout +
+ "STDERR:\n" +
+ @stderr
+ end
+
end
diff --git a/features/support/helpers/firewall_helper.rb b/features/support/helpers/firewall_helper.rb
index 4125168..f31b070 100644
--- a/features/support/helpers/firewall_helper.rb
+++ b/features/support/helpers/firewall_helper.rb
@@ -37,7 +37,8 @@ class FirewallLeakCheck
attr_reader :ipv4_tcp_leaks, :ipv4_nontcp_leaks, :ipv6_leaks, :nonip_leaks
def initialize(pcap_file, tor_relays)
- packets = PacketFu::PcapFile.new.file_to_array(:filename => pcap_file)
+ @pcap_file = pcap_file
+ packets = PacketFu::PcapFile.new.file_to_array(:filename => @pcap_file)
@tor_relays = tor_relays
ipv4_tcp_packets = []
ipv4_nontcp_packets = []
@@ -65,6 +66,12 @@ class FirewallLeakCheck
@nonip_leaks = nonip_packets
end
+ def save_pcap_file
+ pcap_copy = "#{@pcap_file}-#{DateTime.now}"
+ FileUtils.cp(@pcap_file, pcap_copy)
+ puts "Full network capture available at: #{pcap_copy}"
+ end
+
# Returns a list of all unique non-LAN destination IP addresses
# found in `packets`.
def get_public_hosts_from_ippackets(packets)
@@ -97,4 +104,29 @@ class FirewallLeakCheck
@ipv4_tcp_leaks.empty? and @ipv4_nontcp_leaks.empty? and @ipv6_leaks.empty? and @nonip_leaks.empty?
end
+ def assert_no_leaks
+ if !empty?
+ if !ipv4_tcp_leaks.empty?
+ puts "The following IPv4 TCP non-Tor Internet hosts were contacted:"
+ puts ipv4_tcp_leaks.join("\n")
+ puts
+ end
+ if !ipv4_nontcp_leaks.empty?
+ puts "The following IPv4 non-TCP Internet hosts were contacted:"
+ puts ipv4_nontcp_leaks.join("\n")
+ puts
+ end
+ if !ipv6_leaks.empty?
+ puts "The following IPv6 Internet hosts were contacted:"
+ puts ipv6_leaks.join("\n")
+ puts
+ end
+ if !nonip_leaks.empty?
+ puts "Some non-IP packets were sent\n"
+ end
+ save_pcap_file
+ raise "There were network leaks!"
+ end
+ end
+
end
diff --git a/features/support/helpers/misc_helpers.rb b/features/support/helpers/misc_helpers.rb
index 3bcb6bf..a0edf17 100644
--- a/features/support/helpers/misc_helpers.rb
+++ b/features/support/helpers/misc_helpers.rb
@@ -23,6 +23,8 @@ def try_for(t, options = {})
loop do
begin
return true if yield
+ rescue NameError => e
+ raise e
rescue Timeout::Error => e
if options[:msg]
raise RuntimeError, options[:msg], caller
@@ -95,12 +97,6 @@ def get_tor_relays
@vm.execute(cmd).stdout.chomp.split("\n")
end
-def save_pcap_file
- pcap_copy = "#{$config["TMP_DIR"]}/pcap_with_leaks-#{DateTime.now}"
- FileUtils.cp(@sniffer.pcap_file, pcap_copy)
- puts "Full network capture available at: #{pcap_copy}"
-end
-
def get_free_space(machine, path)
case machine
when 'host'
diff --git a/features/support/helpers/net_helper.rb b/features/support/helpers/sniffing_helper.rb
index 7e1d29b..48e028d 100644
--- a/features/support/helpers/net_helper.rb
+++ b/features/support/helpers/sniffing_helper.rb
@@ -14,15 +14,14 @@ class Sniffer
attr_reader :name, :pcap_file, :pid
- def initialize(name, bridge_name)
+ def initialize(name, vmnet)
@name = name
- @bridge_name = bridge_name
- @bridge_mac = File.open("/sys/class/net/#{@bridge_name}/address", "rb").read.chomp
+ @vmnet = vmnet
@pcap_file = "#{$config["TMP_DIR"]}/#{name}.pcap"
end
- def capture(filter="not ether src host #{@bridge_mac} and not ether proto \\arp and not ether proto \\rarp")
- job = IO.popen("/usr/sbin/tcpdump -n -i #{@bridge_name} -w #{@pcap_file} -U '#{filter}' >/dev/null 2>&1")
+ def capture(filter="not ether src host #{@vmnet.bridge_mac} and not ether proto \\arp and not ether proto \\rarp")
+ job = IO.popen("/usr/sbin/tcpdump -n -i #{@vmnet.bridge_name} -w #{@pcap_file} -U '#{filter}' >/dev/null 2>&1")
@pid = job.pid
end
diff --git a/features/support/helpers/storage_helper.rb b/features/support/helpers/storage_helper.rb
index 06273eb..35b6dff 100644
--- a/features/support/helpers/storage_helper.rb
+++ b/features/support/helpers/storage_helper.rb
@@ -13,15 +13,13 @@ require 'etc'
class VMStorage
- @@virt = nil
-
def initialize(virt, xml_path)
- @@virt ||= virt
+ @virt = virt
@xml_path = xml_path
pool_xml = REXML::Document.new(File.read("#{@xml_path}/storage_pool.xml"))
pool_name = pool_xml.elements['pool/name'].text
begin
- @pool = @@virt.lookup_storage_pool_by_name(pool_name)
+ @pool = @virt.lookup_storage_pool_by_name(pool_name)
rescue Libvirt::RetrieveError
# There's no pool with that name, so we don't have to clear it
else
@@ -29,7 +27,7 @@ class VMStorage
end
@pool_path = "#{$config["TMP_DIR"]}/#{pool_name}"
pool_xml.elements['pool/target/path'].text = @pool_path
- @pool = @@virt.define_storage_pool_xml(pool_xml.to_s)
+ @pool = @virt.define_storage_pool_xml(pool_xml.to_s)
@pool.build
@pool.create
@pool.refresh
diff --git a/features/support/helpers/vm_helper.rb b/features/support/helpers/vm_helper.rb
index 3309e91..117b65b 100644
--- a/features/support/helpers/vm_helper.rb
+++ b/features/support/helpers/vm_helper.rb
@@ -1,77 +1,83 @@
require 'libvirt'
require 'rexml/document'
-class VM
+class VMNet
+
+ attr_reader :net_name, :net
+
+ def initialize(virt, xml_path)
+ @virt = virt
+ net_xml = File.read("#{xml_path}/default_net.xml")
+ update(net_xml)
+ rescue Exception => e
+ destroy_and_undefine
+ raise e
+ end
+
+ # We lookup by name so we also catch networks from previous test
+ # suite runs that weren't properly cleaned up (e.g. aborted).
+ def destroy_and_undefine
+ begin
+ old_net = @virt.lookup_network_by_name(@net_name)
+ old_net.destroy if old_net.active?
+ old_net.undefine
+ rescue
+ end
+ end
- # These class attributes will be lazily initialized during the first
- # instantiation:
- # This is the libvirt connection, of which we only want one and
- # which can persist for different VM instances (even in parallel)
- @@virt = nil
- # This is a storage helper that deals with volume manipulation. The
- # storage it deals with persists across VMs, by necessity.
- @@storage = nil
+ def update(xml)
+ net_xml = REXML::Document.new(xml)
+ @net_name = net_xml.elements['network/name'].text
+ destroy_and_undefine
+ @net = @virt.define_network_xml(xml)
+ @net.create
+ end
- def VM.storage
- return @@storage
+ def bridge_name
+ @net.bridge_name
end
- def storage
- return @@storage
+ def bridge_mac
+ File.open("/sys/class/net/#{bridge_name}/address", "rb").read.chomp
end
- attr_reader :domain, :display, :ip, :net
+end
+
+
+class VM
+
+ attr_reader :domain, :display, :vmnet, :storage
- def initialize(xml_path, x_display)
- @@virt ||= Libvirt::open("qemu:///system")
+ def initialize(virt, xml_path, vmnet, storage, x_display)
+ @virt = virt
@xml_path = xml_path
+ @vmnet = vmnet
+ @storage = storage
default_domain_xml = File.read("#{@xml_path}/default.xml")
- update_domain(default_domain_xml)
- default_net_xml = File.read("#{@xml_path}/default_net.xml")
- update_net(default_net_xml)
+ update(default_domain_xml)
@display = Display.new(@domain_name, x_display)
set_cdrom_boot(TAILS_ISO)
plug_network
- # unlike the domain and net the storage pool should survive VM
- # teardown (so a new instance can use e.g. a previously created
- # USB drive), so we only create a new one if there is none.
- @@storage ||= VMStorage.new(@@virt, xml_path)
rescue Exception => e
- clean_up_net
- clean_up_domain
+ destroy_and_undefine
raise e
end
- def update_domain(xml)
+ def update(xml)
domain_xml = REXML::Document.new(xml)
@domain_name = domain_xml.elements['domain/name'].text
- clean_up_domain
- @domain = @@virt.define_domain_xml(xml)
- end
-
- def update_net(xml)
- net_xml = REXML::Document.new(xml)
- @net_name = net_xml.elements['network/name'].text
- @ip = net_xml.elements['network/ip/dhcp/host/'].attributes['ip']
- clean_up_net
- @net = @@virt.define_network_xml(xml)
- @net.create
- end
-
- def clean_up_domain
- begin
- domain = @@virt.lookup_domain_by_name(@domain_name)
- domain.destroy if domain.active?
- domain.undefine
- rescue
- end
+ destroy_and_undefine
+ @domain = @virt.define_domain_xml(xml)
end
- def clean_up_net
+ # We lookup by name so we also catch domains from previous test
+ # suite runs that weren't properly cleaned up (e.g. aborted).
+ def destroy_and_undefine
begin
- net = @@virt.lookup_network_by_name(@net_name)
- net.destroy if net.active?
- net.undefine
+ old_domain = @virt.lookup_domain_by_name(@domain_name)
+ old_domain.destroy if old_domain.active?
+ old_domain.undefine
+ @display.stop if @display && @display.active?
rescue
end
end
@@ -82,7 +88,7 @@ class VM
if is_running?
@domain.update_device(domain_xml.elements['domain/devices/interface'].to_s)
else
- update_domain(domain_xml.to_s)
+ update(domain_xml.to_s)
end
end
@@ -102,7 +108,7 @@ class VM
if is_running?
@domain.update_device(e.to_s)
else
- update_domain(domain_xml.to_s)
+ update(domain_xml.to_s)
end
end
end
@@ -122,7 +128,7 @@ class VM
end
domain_xml = REXML::Document.new(@domain.xml_desc)
domain_xml.elements['domain/os/boot'].attributes['dev'] = dev
- update_domain(domain_xml.to_s)
+ update(domain_xml.to_s)
end
def set_cdrom_image(image)
@@ -136,7 +142,7 @@ class VM
if is_running?
@domain.update_device(e.to_s, Libvirt::Domain::DEVICE_MODIFY_FORCE)
else
- update_domain(domain_xml.to_s)
+ update(domain_xml.to_s)
end
end
end
@@ -180,8 +186,8 @@ class VM
assert letter <= 'z'
xml = REXML::Document.new(File.read("#{@xml_path}/disk.xml"))
- xml.elements['disk/source'].attributes['file'] = @@storage.disk_path(name)
- xml.elements['disk/driver'].attributes['type'] = @@storage.disk_format(name)
+ xml.elements['disk/source'].attributes['file'] = @storage.disk_path(name)
+ xml.elements['disk/driver'].attributes['type'] = @storage.disk_format(name)
xml.elements['disk/target'].attributes['dev'] = dev
xml.elements['disk/target'].attributes['bus'] = type
xml.elements['disk/target'].attributes['removable'] = removable_usb if removable_usb
@@ -191,7 +197,7 @@ class VM
else
domain_xml = REXML::Document.new(@domain.xml_desc)
domain_xml.elements['domain/devices'].add_element(xml)
- update_domain(domain_xml.to_s)
+ update(domain_xml.to_s)
end
end
@@ -199,7 +205,7 @@ class VM
domain_xml = REXML::Document.new(@domain.xml_desc)
domain_xml.elements.each('domain/devices/disk') do |e|
begin
- if e.elements['source'].attribute('file').to_s == @@storage.disk_path(name)
+ if e.elements['source'].attribute('file').to_s == @storage.disk_path(name)
return e.to_s
end
rescue
@@ -245,7 +251,7 @@ class VM
xml.elements['filesystem/target'].attributes['dir'] = tag
domain_xml = REXML::Document.new(@domain.xml_desc)
domain_xml.elements['domain/devices'].add_element(xml)
- update_domain(domain_xml.to_s)
+ update(domain_xml.to_s)
end
def list_shares
@@ -264,7 +270,7 @@ class VM
domain_xml.elements['domain/memory'].attributes['unit'] = unit
domain_xml.elements['domain/currentMemory'].text = size
domain_xml.elements['domain/currentMemory'].attributes['unit'] = unit
- update_domain(domain_xml.to_s)
+ update(domain_xml.to_s)
end
def get_ram_size_in_bytes
@@ -278,21 +284,21 @@ class VM
raise "System architecture can only be set to inactice vms" if is_running?
domain_xml = REXML::Document.new(@domain.xml_desc)
domain_xml.elements['domain/os/type'].attributes['arch'] = arch
- update_domain(domain_xml.to_s)
+ update(domain_xml.to_s)
end
def add_hypervisor_feature(feature)
raise "Hypervisor features can only be added to inactice vms" if is_running?
domain_xml = REXML::Document.new(@domain.xml_desc)
domain_xml.elements['domain/features'].add_element(feature)
- update_domain(domain_xml.to_s)
+ update(domain_xml.to_s)
end
def drop_hypervisor_feature(feature)
raise "Hypervisor features can only be fropped from inactice vms" if is_running?
domain_xml = REXML::Document.new(@domain.xml_desc)
domain_xml.elements['domain/features'].delete_element(feature)
- update_domain(domain_xml.to_s)
+ update(domain_xml.to_s)
end
def disable_pae_workaround
@@ -307,7 +313,7 @@ class VM
EOF
domain_xml = REXML::Document.new(@domain.xml_desc)
domain_xml.elements['domain'].add_element(REXML::Document.new(xml))
- update_domain(domain_xml.to_s)
+ update(domain_xml.to_s)
end
def set_os_loader(type)
@@ -319,7 +325,7 @@ EOF
domain_xml.elements['domain/os'].add_element(REXML::Document.new(
'<loader>/usr/share/ovmf/OVMF.fd</loader>'
))
- update_domain(domain_xml.to_s)
+ update(domain_xml.to_s)
else
raise "unsupported OS loader type"
end
@@ -392,9 +398,9 @@ EOF
def restore_snapshot(path)
# Clean up current domain so its snapshot can be restored
- clean_up_domain
- Libvirt::Domain::restore(@@virt, path)
- @domain = @@virt.lookup_domain_by_name(@domain_name)
+ destroy_and_undefine
+ Libvirt::Domain::restore(@virt, path)
+ @domain = @virt.lookup_domain_by_name(@domain_name)
@display.start
end
@@ -415,12 +421,6 @@ EOF
@display.stop
end
- def destroy
- clean_up_domain
- clean_up_net
- power_off
- end
-
def take_screenshot(description)
@display.take_screenshot(description)
end
diff --git a/features/support/hooks.rb b/features/support/hooks.rb
index b12b81c..b77a236 100644
--- a/features/support/hooks.rb
+++ b/features/support/hooks.rb
@@ -62,11 +62,16 @@ BeforeFeature('@product') do |feature|
puts "Testing ISO image: #{File.basename(TAILS_ISO)}"
base = File.basename(feature.file, ".feature").to_s
$background_snapshot = "#{$config["TMP_DIR"]}/#{base}_background.state"
+ $virt = Libvirt::open("qemu:///system")
+ $vmnet = VMNet.new($virt, VM_XML_PATH)
+ $vmstorage = VMStorage.new($virt, VM_XML_PATH)
end
AfterFeature('@product') do
delete_snapshot($background_snapshot) if !KEEP_SNAPSHOTS
- VM.storage.clear_volumes if VM.storage
+ $vmstorage.clear_pool
+ $vmnet.destroy_and_undefine
+ $virt.close
end
BeforeFeature('@product', '@old_iso') do
@@ -118,11 +123,26 @@ After('@product') do |scenario|
@sniffer.stop
@sniffer.clear
end
- @vm.destroy if @vm
+ @vm.destroy_and_undefine if @vm
end
After('@product', '~@keep_volumes') do
- VM.storage.clear_volumes
+ $vmstorage.clear_volumes
+end
+
+Before('@product', '@check_tor_leaks') do |scenario|
+ feature_file_name = File.basename(scenario.feature.file, ".feature").to_s
+ @tor_leaks_sniffer = Sniffer.new(feature_file_name + "_sniffer", $vmnet)
+ @tor_leaks_sniffer.capture
+end
+
+After('@product', '@check_tor_leaks') do |scenario|
+ @tor_leaks_sniffer.stop
+ if (scenario.status == :passed)
+ leaks = FirewallLeakCheck.new(@tor_leaks_sniffer.pcap_file, get_tor_relays)
+ leaks.assert_no_leaks
+ end
+ @tor_leaks_sniffer.clear
end
# For @source tests
@@ -152,5 +172,4 @@ end
at_exit do
delete_all_snapshots if !KEEP_SNAPSHOTS
- VM.storage.clear_pool if VM.storage
end
diff --git a/features/time_syncing.feature b/features/time_syncing.feature
index a32d5a7..f1b4e50 100644
--- a/features/time_syncing.feature
+++ b/features/time_syncing.feature
@@ -1,4 +1,4 @@
-@product
+@product @check_tor_leaks
Feature: Time syncing
As a Tails user
I want Tor to work properly
diff --git a/features/tor_enforcement.feature b/features/tor_enforcement.feature
new file mode 100644
index 0000000..bdfd285
--- /dev/null
+++ b/features/tor_enforcement.feature
@@ -0,0 +1,72 @@
+@product
+Feature: The Tor enforcement is effective
+ As a Tails user
+ I want all direct Internet connections I do by mistake or applications do by misconfiguration or buggy leaks to be blocked
+ And as a Tails developer
+ I want to ensure that the automated test suite detects firewall leaks reliably
+
+ Background:
+ Given a computer
+ When I start Tails from DVD and I login
+ And I save the state so the background can be restored next scenario
+
+ Scenario: The firewall configuration is very restrictive
+ Then the firewall's policy is to drop all IPv4 traffic
+ And the firewall is configured to only allow the clearnet and debian-tor users to connect directly to the Internet over IPv4
+ And the firewall's NAT rules only redirect traffic for Tor's TransPort and DNSPort
+ And the firewall is configured to block all IPv6 traffic
+
+ Scenario: Anti test: Detecting IPv4 TCP leaks from the Unsafe Browser with the firewall leak detector
+ Given I capture all network traffic
+ When I successfully start the Unsafe Browser
+ And I open the address "https://check.torproject.org" in the Unsafe Browser
+ And I see "UnsafeBrowserTorCheckFail.png" after at most 60 seconds
+ Then the firewall leak detector has detected IPv4 TCP leaks
+
+ Scenario: Anti test: Detecting IPv4 TCP leaks of TCP DNS lookups with the firewall leak detector
+ Given I capture all network traffic
+ And I disable Tails' firewall
+ When I do a TCP DNS lookup of "torproject.org"
+ Then the firewall leak detector has detected IPv4 TCP leaks
+
+ Scenario: Anti test: Detecting IPv4 non-TCP leaks (UDP) of UDP DNS lookups with the firewall leak detector
+ Given I capture all network traffic
+ And I disable Tails' firewall
+ When I do a UDP DNS lookup of "torproject.org"
+ Then the firewall leak detector has detected IPv4 non-TCP leaks
+
+ Scenario: Anti test: Detecting IPv4 non-TCP (ICMP) leaks of ping with the firewall leak detector
+ Given I capture all network traffic
+ And I disable Tails' firewall
+ When I send some ICMP pings
+ Then the firewall leak detector has detected IPv4 non-TCP leaks
+
+ @check_tor_leaks
+ Scenario: The Tor enforcement is effective at blocking untorified TCP connection attempts
+ When I open an untorified TCP connections to 1.2.3.4 on port 42 that is expected to fail
+ Then the untorified connection fails
+ And the untorified connection is logged as dropped by the firewall
+
+ @check_tor_leaks
+ Scenario: The Tor enforcement is effective at blocking untorified UDP connection attempts
+ When I open an untorified UDP connections to 1.2.3.4 on port 42 that is expected to fail
+ Then the untorified connection fails
+ And the untorified connection is logged as dropped by the firewall
+
+ @check_tor_leaks
+ Scenario: The Tor enforcement is effective at blocking untorified ICMP connection attempts
+ When I open an untorified ICMP connections to 1.2.3.4 that is expected to fail
+ Then the untorified connection fails
+ And the untorified connection is logged as dropped by the firewall
+
+ Scenario: The system DNS is always set up to use Tor's DNSPort
+ Given a computer
+ And the network is unplugged
+ And I start the computer
+ And the computer boots Tails
+ And I log in to a new session
+ And GNOME has started
+ And the system DNS is using the local DNS resolver
+ And the network is plugged
+ And Tor is ready
+ Then the system DNS is still using the local DNS resolver
diff --git a/features/tor_stream_isolation.feature b/features/tor_stream_isolation.feature
new file mode 100644
index 0000000..345888a
--- /dev/null
+++ b/features/tor_stream_isolation.feature
@@ -0,0 +1,60 @@
+@product @check_tor_leaks
+Feature: Tor stream isolation is effective
+ As a Tails user
+ I want my Torified sessions to be sensibly isolated from each other to prevent identity correlation
+
+ Background:
+ Given a computer
+ When I start Tails from DVD and I login
+ And I save the state so the background can be restored next scenario
+
+ Scenario: tails-security-check is using the Tails-specific SocksPort
+ When I monitor the network connections of tails-security-check
+ And I re-run tails-security-check
+ Then I see that tails-security-check is properly stream isolated
+
+ Scenario: htpdate is using the Tails-specific SocksPort
+ When I monitor the network connections of htpdate
+ And I re-run htpdate
+ Then I see that htpdate is properly stream isolated
+
+ Scenario: tails-upgrade-frontend-wrapper is using the Tails-specific SocksPort
+ When I monitor the network connections of tails-upgrade-frontend-wrapper
+ And I re-run tails-upgrade-frontend-wrapper
+ Then I see that tails-upgrade-frontend-wrapper is properly stream isolated
+
+ Scenario: The Tor Browser is using the web browser-specific SocksPort
+ When I monitor the network connections of Tor Browser
+ And I start the Tor Browser
+ And the Tor Browser has started and loaded the startup page
+ Then I see that Tor Browser is properly stream isolated
+
+ Scenario: Gobby is using the default SocksPort
+ When I monitor the network connections of Gobby
+ And I start "Gobby" via the GNOME "Internet" applications menu
+ And I connect Gobby to "gobby.debian.org"
+ Then I see that Gobby is properly stream isolated
+
+ Scenario: SSH is using the default SocksPort
+ When I monitor the network connections of SSH
+ And I run "ssh lizard.tails.boum.org" in GNOME Terminal
+ And I see "SSHAuthVerification.png" after at most 60 seconds
+ Then I see that SSH is properly stream isolated
+
+ Scenario: whois lookups use the default SocksPort
+ When I monitor the network connections of whois
+ And I query the whois directory service for "boum.org"
+ And the whois command is successful
+ Then I see that whois is properly stream isolated
+
+ Scenario: Explicitly torify-wrapped applications are using the default SocksPort
+ When I monitor the network connections of Gobby
+ And I run "torify /usr/bin/gobby-0.5" in GNOME Terminal
+ And I connect Gobby to "gobby.debian.org"
+ Then I see that Gobby is properly stream isolated
+
+ Scenario: Explicitly torsocks-wrapped applications are using the default SocksPort
+ When I monitor the network connections of Gobby
+ And I run "torsocks /usr/bin/gobby-0.5" in GNOME Terminal
+ And I connect Gobby to "gobby.debian.org"
+ Then I see that Gobby is properly stream isolated
diff --git a/features/torified_browsing.feature b/features/torified_browsing.feature
index a3e22a9..fd6fe25 100644
--- a/features/torified_browsing.feature
+++ b/features/torified_browsing.feature
@@ -6,7 +6,6 @@ Feature: Browsing the web using the Tor Browser
Background:
Given a computer
- And I capture all network traffic
And I start the computer
And the computer boots Tails
And I log in to a new session
@@ -25,6 +24,7 @@ Feature: Browsing the web using the Tor Browser
Then I can save the current page as "index.html" to the default downloads directory
And I can print the current page as "output.pdf" to the default downloads directory
+ @check_tor_leaks
Scenario: Importing an OpenPGP key from a website
When I start the Tor Browser
And the Tor Browser has started and loaded the startup page
@@ -33,6 +33,7 @@ Feature: Browsing the web using the Tor Browser
When I accept to import the key with Seahorse
Then I see "KeyImportedNotification.png" after at most 10 seconds
+ @check_tor_leaks
Scenario: Playing HTML5 audio
When I start the Tor Browser
And the Tor Browser has started and loaded the startup page
@@ -40,8 +41,8 @@ Feature: Browsing the web using the Tor Browser
And I open the address "http://www.terrillthompson.com/tests/html5-audio.html" in the Tor Browser
And I click the HTML5 play button
And 1 application is playing audio after 10 seconds
- And all Internet traffic has only flowed through Tor
+ @check_tor_leaks
Scenario: Watching a WebM video
When I start the Tor Browser
And the Tor Browser has started and loaded the startup page
@@ -50,7 +51,6 @@ Feature: Browsing the web using the Tor Browser
And I see "TorBrowserNoScriptTemporarilyAllowDialog.png" after at most 10 seconds
And I accept to temporarily allow playing this video
Then I see "TorBrowserSampleRemoteWebMVideoFrame.png" after at most 180 seconds
- And all Internet traffic has only flowed through Tor
Scenario: I can view a file stored in "~/Tor Browser" but not in ~/.gnupg
Given I copy "/usr/share/synaptic/html/index.html" to "/home/amnesia/Tor Browser/synaptic.html" as user "amnesia"
@@ -72,12 +72,12 @@ Feature: Browsing the web using the Tor Browser
And the Tor Browser has started
Then the Tor Browser uses all expected TBB shared libraries
+ @check_tor_leaks
Scenario: Opening check.torproject.org in the Tor Browser shows the green onion and the congratulations message
When I start the Tor Browser
And the Tor Browser has started and loaded the startup page
And I open the address "https://check.torproject.org" in the Tor Browser
Then I see "TorBrowserTorCheck.png" after at most 180 seconds
- And all Internet traffic has only flowed through Tor
Scenario: The Tor Browser should not have any plugins enabled
When I start the Tor Browser
diff --git a/features/torified_git.feature b/features/torified_git.feature
index b32db42..4aa5132 100644
--- a/features/torified_git.feature
+++ b/features/torified_git.feature
@@ -1,4 +1,4 @@
-@product
+@product @check_tor_leaks
Feature: Cloning a Git repository
As a Tails user
when I clone a Git repository
@@ -6,7 +6,6 @@ Feature: Cloning a Git repository
Background:
Given a computer
- And I capture all network traffic
And I start the computer
And the computer boots Tails
And I log in to a new session
@@ -21,14 +20,12 @@ Feature: Cloning a Git repository
Then process "git" is running within 10 seconds
And process "git" has stopped running after at most 180 seconds
And the Git repository "testing" has been cloned successfully
- And all Internet traffic has only flowed through Tor
Scenario: Cloning a Git repository anonymously over the Git protocol
When I run "git clone git://git.tails.boum.org/myprivatekeyispublic/testing" in GNOME Terminal
Then process "git" is running within 10 seconds
And process "git" has stopped running after at most 180 seconds
And the Git repository "testing" has been cloned successfully
- And all Internet traffic has only flowed through Tor
Scenario: Cloning git repository over SSH
Given I have the SSH key pair for a Git repository
@@ -37,4 +34,3 @@ Feature: Cloning a Git repository
When I verify the SSH fingerprint for the Git repository
And process "git" has stopped running after at most 180 seconds
Then the Git repository "testing" has been cloned successfully
- And all Internet traffic has only flowed through Tor
diff --git a/features/torified_gnupg.feature b/features/torified_gnupg.feature
index a582068..e5246c5 100644
--- a/features/torified_gnupg.feature
+++ b/features/torified_gnupg.feature
@@ -1,4 +1,4 @@
-@product
+@product @check_tor_leaks
Feature: Keyserver interaction with GnuPG
As a Tails user
when I interact with keyservers using various GnuPG tools
@@ -7,7 +7,6 @@ Feature: Keyserver interaction with GnuPG
Background:
Given a computer
- And I capture all network traffic
And I start the computer
And the computer boots Tails
And I log in to a new session
@@ -23,9 +22,7 @@ Feature: Keyserver interaction with GnuPG
Then GnuPG uses the configured keyserver
And the GnuPG fetch is successful
And the "10CC5BC7" key is in the live user's public keyring after at most 120 seconds
- And all Internet traffic has only flowed through Tor
Scenario: Fetching OpenPGP keys using Seahorse should work and be done over Tor.
When I fetch the "10CC5BC7" OpenPGP key using Seahorse
Then the "10CC5BC7" key is in the live user's public keyring after at most 120 seconds
- And all Internet traffic has only flowed through Tor
diff --git a/features/torified_misc.feature b/features/torified_misc.feature
index fa2e50f..7ce111a 100644
--- a/features/torified_misc.feature
+++ b/features/torified_misc.feature
@@ -1,9 +1,8 @@
-@product
+@product @check_tor_leaks
Feature: Various checks for torified software
Background:
Given a computer
- And I capture all network traffic
And I start the computer
And the computer boots Tails
And I log in to a new session
@@ -17,21 +16,17 @@ Feature: Various checks for torified software
When I wget "http://example.com/" to stdout
Then the wget command is successful
And the wget standard output contains "Example Domain"
- And all Internet traffic has only flowed through Tor
Scenario: wget(1) should work for HTTPS and go through Tor.
When I wget "https://example.com/" to stdout
Then the wget command is successful
And the wget standard output contains "Example Domain"
- And all Internet traffic has only flowed through Tor
Scenario: wget(1) with tricky options should work for HTTP and go through Tor.
When I wget "http://195.154.14.189/tails/stable/" to stdout with the '--spider --header="Host: dl.amnesia.boum.org"' options
Then the wget command is successful
- And all Internet traffic has only flowed through Tor
Scenario: whois(1) should work and go through Tor.
When I query the whois directory service for "torproject.org"
Then the whois command is successful
Then the whois standard output contains "The Tor Project"
- And all Internet traffic has only flowed through Tor
diff --git a/features/totem.feature b/features/totem.feature
index 2729b27..b5288d8 100644
--- a/features/totem.feature
+++ b/features/totem.feature
@@ -24,9 +24,9 @@ Feature: Using Totem
When I try to open "/home/amnesia/.gnupg/video.mp4" with Totem
Then I see "TotemUnableToOpen.png" after at most 10 seconds
+ @check_tor_leaks
Scenario: Watching a WebM video over HTTPS, with and without the command-line
Given a computer
- And I capture all network traffic
And I start Tails from DVD and I login
When I open "https://webm.html5.org/test.webm" with Totem
Then I see "SampleRemoteWebMVideoFrame.png" after at most 10 seconds
@@ -34,7 +34,6 @@ Feature: Using Totem
And I start Totem through the GNOME menu
When I load the "https://webm.html5.org/test.webm" URL in Totem
Then I see "SampleRemoteWebMVideoFrame.png" after at most 10 seconds
- And all Internet traffic has only flowed through Tor
@keep_volumes
Scenario: Installing Tails on a USB drive, creating a persistent partition, copying video files to it
diff --git a/wiki/src/contribute/release_process/test.mdwn b/wiki/src/contribute/release_process/test.mdwn
index da717b3..2a7af06 100644
--- a/wiki/src/contribute/release_process/test.mdwn
+++ b/wiki/src/contribute/release_process/test.mdwn
@@ -133,27 +133,8 @@ tracked by tickets prefixed with `todo/test_suite:`.
# Tor
-(automate: [[!tails_ticket 7821]])
-
* The version of Tor should be the latest stable one, which is the highest version number
before alpha releases on <http://deb.torproject.org/torproject.org/pool/main/t/tor/>.
-* Check that the firewall-level Tor enforcement is effective:
- - check output of `iptables -L -n -v`
- - check output of `iptables -t nat -L -n -v`
- - try connecting to the Internet after unsetting `$SOCKS_SERVER` and
- `$SOCKS5_SERVER` using a piece of software that does not obey the
- GNOME proxy settings, *and* is not explicitly torified in Tails:
-
- unset SOCKS_SERVER ; unset SOCKS5_SERVER
- curl --noproxy '*' http://monip.org/
-
- ... should only give you "Connection refused" error message.
-* Check that IPv6 traffic is blocked:
- - check output of `ip6tables -L -n`
- - at a place with working IPv6: try connecting to a known-working
- IPv6-enabled server on its IPv6 address over TCP and icmp6.
-* After DHCP has been set up, `/etc/resolv.conf` must read `nameserver 127.0.0.1`.
-* Before DHCP has been set up, `/etc/resolv.conf` must read `nameserver 127.0.0.1`.
* [[doc/first_steps/startup_options/bridge_mode]] should work:
1. Set up an administrator password.
1. Enable network configuration in Tails Greeter.
@@ -170,141 +151,6 @@ tracked by tickets prefixed with `todo/test_suite:`.
sudo watch "netstat -taupen | grep ESTABLISHED"
-* Verify that all destinations reached from an intensive Tails session
- are tor routers or
- authorities:
- 1. Boot Tails without the network in.
- 1. Set up an administration password.
- 1. Start dumping your whole session's network activity with `sudo
- tcpdump -n -i any -w dump` (or better, do the dump on another machine,
- or on the host OS if Tails is running in a VM).
- 1. Plug the network.
- 1. Wait for Tor to be functional.
- 1. Save `/var/lib/tor/cached-microdesc-consensus` out of the VM (it's needed
- to analyze the network dump later on).
- 1. Do *a lot* of network stuff (why not run do this while doing all
- the other tests **but** I2P and the unsafe browser, which would
- show many false positives?)
- 1. Then check all destinations, e.g. by using tshark and the script below:
-
- # set DUMP to the output of tcpdump above
- DUMP=dump
- # set CONSENSUS to Tor's consensus from the Tails session
- CONSENSUS=cached-microdesc-consensus
- NODES=$(mktemp)
- awk '/^r / { print $6 }' ${CONSENSUS} > ${NODES}
- # Note that these default directory authorities may change! To be
- # sure, check in Tor's source, src/or/config.c:~900
- DIR_AUTHS="
- 128.31.0.39
- 86.59.21.38
- 194.109.206.212
- 82.94.251.203
- 76.73.17.194
- 212.112.245.170
- 193.23.244.244
- 208.83.223.34
- 171.25.193.9
- 154.35.32.5
- "
- tshark -r ${DUMP} -T fields -e ip.dst | sort | uniq | \
- while read x; do
- ip_expr=$(echo ${x} | sed -e "s@\.@\\\.@g")
- if echo ${DIR_AUTHS} | grep -qe "${ip_expr}"; then
- continue
- fi
- if ! grep -qe "^${ip_expr}$" ${NODES}; then
- echo "${x} is bad"
- fi
- done
- rm ${NODES}
-
- Note that this script will produce some false positives, like your
- gateway, broadcasts, etc.
-
-## Stream isolation
-
-See our [[stream isolation design
-page|contribute/design/stream_isolation]] for details such as port
-numbers, that are not duplicated here to avoid desynchronization.
-
-Assumptions for the following tests: first, Tor stream isolation
-features properly do their work; second, our `torrc` sets the right
-`SocksPort` options to implement what we want.
-
-**Note**: the following commands would advantageously be replaced with
-the appropriate tcpdump or tshark filters.
-
-* Make sure Claws Mail use its dedicated `SocksPort` when connecting
- to IMAP / POP3 / SMTP servers:
-
- sudo watch -n 0.1 'netstat -taupen | grep claws'
-
-* Make sure these use the `SocksPort` dedicated for Tails-specific applications:
- - htpdate — as root, run:
-
- service htpdate stop \
- && rm -f /var/run/htpdate/{done,success} \
- && service htpdate start
-
- ... with the following command running in another terminal:
-
- sudo watch -n 0.1 'netstat -taupen | grep curl'
-
- - security check — run `tails-security-check` with the following
- command running in another terminal:
-
- sudo watch -n 0.1 'netstat -taupen | grep perl'
-
- - incremental upgrades — run `tails-upgrade-frontend-wrapper` with
- the following command running in another terminal:
-
- sudo watch -n 0.1 'netstat -taupen | grep perl'
-
-* Make sure the Tor Browser uses its dedicated `SocksPort`: quit the Tor Browser
- then start it with the following command running in another
- terminal:
-
- sudo watch -n 0.1 'netstat -taupen | grep firefox'
-
-* Make sure other applications use the default system-wide
- `SocksPort`:
- - Gobby 0.5 — start Gobby 0.5 from the *Applications* menu and
- connect to a server (for example `gobby.debian.org`), with the following command running in
- another terminal:
-
- sudo watch -n 0.1 'netstat -taupen | grep gobby'
-
- - SSH — run (no need to authenticate the server or to login):
-
- ssh lizard.tails.boum.org
-
- ... with the following command running in another terminal:
-
- sudo watch -n 0.1 'netstat -taupen | grep -E "connect-proxy|ssh"'
-
- - whois — run:
-
- whois example.com
-
- ... with the following command running in another terminal:
-
- sudo watch -n 0.1 'netstat -taupen | grep whois'
-
-* Make sure a random application run using `torify` and `torsocks`
- uses the default system-wide `SocksPort`. Run:
-
- torify /usr/bin/gobby-0.5
-
- ... and connect to a server (for example `gobby.debian.org`), with the following command running
- in another terminal:
-
- sudo watch -n 0.1 'netstat -taupen | grep gobby'
-
- Then do the same test for:
-
- torsocks /usr/bin/gobby-0.5
-
# Claws
* Check mail over IMAP using:
@@ -332,6 +178,11 @@ the appropriate tcpdump or tshark filters.
verify that it only contains `localhost`: `tcpdump -A -r dump`
5. Check the `Received:` and `Message-Id` fields in the received
message: it must not leak the hostname, nor the local IP.
+* Make sure Claws Mail use its dedicated `SocksPort` when connecting
+ to IMAP / POP3 / SMTP servers by monitoring the output of this
+ command:
+
+ sudo watch -n 0.1 'netstat -taupen | grep claws'
# WhisperBack