From a5d56e3b5443263b53b0487c81125123411bd0cf Mon Sep 17 00:00:00 2001 From: Philip Hands Date: Wed, 11 May 2016 17:11:01 +0200 Subject: move cucumber things under cucumber/ --- cucumber/features/step_definitions/tor.rb | 402 ++++++++++++++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 cucumber/features/step_definitions/tor.rb (limited to 'cucumber/features/step_definitions/tor.rb') diff --git a/cucumber/features/step_definitions/tor.rb b/cucumber/features/step_definitions/tor.rb new file mode 100644 index 00000000..ac12fd4c --- /dev/null +++ b/cucumber/features/step_definitions/tor.rb @@ -0,0 +1,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 => '\.\+/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( +< @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 -- cgit v1.2.3-54-g00ecf