Commit c2a2465c authored by anonym's avatar anonym
Browse files

Completely rewrite the firewall leak detector.

The old design was very inflexible, which over time lead to the
implementation growing messy as different checks were added. The
issue was that it had to hard-code the particular checks we
wanted, and did not allow the user to formulate an expression for
which packets are considered leaks or not. So, let's instead
provide an assertion-like function to which the user passes a
block describing how we want all our packets to look.

Furthermore, now all firewall leak tests should be ok with the
simulated Tor network provided by Chutney. Since all Tor nodes
(incl. bridges) run from the same host (and IP address) we also
include the server port when verifying that no unexpected hosts were
contacted.

Note that in some cases we've lost a bit of information and
precision, e.g. among the anti-tests we no longer exactly match
the protocol that was leaked, but that wasn't very valuable to
begin with, and instead we test *exactly* the code that these are
anti tests for -- a true anti test, indeed!

Also, the 'no traffic has flowed to the LAN' (now renamed) had a
serious bug which was fixed in passing -- the `@lan_host`
variable was not set, so it is `nil`, which could never be among
the IPv4 TCP leaks, so that step always succeeded! :S
parent de731e93
......@@ -447,9 +447,10 @@ Then /^I (do not )?see "([^"]*)" after at most (\d+) seconds$/ do |negation, ima
end
Then /^all Internet traffic has only flowed through Tor$/ do
leaks = FirewallLeakCheck.new(@sniffer.pcap_file,
:accepted_hosts => get_all_tor_nodes)
leaks.assert_no_leaks
all_tor_nodes = get_all_tor_nodes
assert_all_connections(@sniffer.pcap_file) do |host|
all_tor_nodes.include?({ address: host.address, port: host.port })
end
end
Given /^I enter the sudo password in the pkexec prompt$/ do
......@@ -822,9 +823,9 @@ When /^I can print the current page as "([^"]+[.]pdf)" to the (default downloads
end
Given /^a web server is running on the LAN$/ do
web_server_ip_addr = $vmnet.bridge_ip_addr
web_server_port = 8000
@web_server_url = "http://#{web_server_ip_addr}:#{web_server_port}"
@web_server_ip_addr = $vmnet.bridge_ip_addr
@web_server_port = 8000
@web_server_url = "http://#{@web_server_ip_addr}:#{@web_server_port}"
web_server_hello_msg = "Welcome to the LAN web server!"
# I've tested ruby Thread:s, fork(), etc. but nothing works due to
......@@ -839,8 +840,8 @@ Given /^a web server is running on the LAN$/ do
require "webrick"
STDOUT.reopen("/dev/null", "w")
STDERR.reopen("/dev/null", "w")
server = WEBrick::HTTPServer.new(:BindAddress => "#{web_server_ip_addr}",
:Port => #{web_server_port},
server = WEBrick::HTTPServer.new(:BindAddress => "#{@web_server_ip_addr}",
:Port => #{@web_server_port},
:DocumentRoot => "/dev/null")
server.mount_proc("/") do |req, res|
res.body = "#{web_server_hello_msg}"
......
Then(/^the firewall leak detector has detected (.*?) leaks$/) do |type|
leaks = FirewallLeakCheck.new(@sniffer.pcap_file,
:accepted_hosts => get_all_tor_nodes)
case type.downcase
when 'ipv4 tcp'
if leaks.ipv4_tcp_leaks.empty?
leaks.save_pcap_file
raise "Couldn't detect any IPv4 TCP leaks"
end
when 'ipv4 non-tcp'
if leaks.ipv4_nontcp_leaks.empty?
leaks.save_pcap_file
raise "Couldn't detect any IPv4 non-TCP leaks"
end
when 'ipv6'
if leaks.ipv6_leaks.empty?
leaks.save_pcap_file
raise "Couldn't detect any IPv6 leaks"
end
when 'non-ip'
if leaks.nonip_leaks.empty?
leaks.save_pcap_file
raise "Couldn't detect any non-IP leaks"
end
else
raise "Incorrect packet type '#{type}'"
Then(/^the firewall leak detector has detected leaks$/) do
assert_raise(Test::Unit::AssertionFailedError) do
step 'all Internet traffic has only flowed through Tor'
end
end
......
......@@ -33,20 +33,8 @@ end
Then /^the real MAC address was (not )?leaked$/ do |mode|
is_leaking = mode.nil?
leaks = FirewallLeakCheck.new(@sniffer.pcap_file)
mac_leaks = leaks.mac_leaks
if is_leaking
if !mac_leaks.include?($vm.real_mac)
save_pcap_file
raise "The real MAC address was expected to leak but didn't. We " +
"observed the following MAC addresses: #{mac_leaks}"
end
else
if mac_leaks.include?($vm.real_mac)
save_pcap_file
raise "The real MAC address was leaked but was expected not to. We " +
"observed the following MAC addresses: #{mac_leaks}"
end
assert_all_connections(@sniffer.pcap_file) do |host|
[host.mac_saddr, host.mac_daddr].include?($vm.real_mac) == is_leaking
end
end
......
......@@ -352,7 +352,6 @@ When /^I configure some (\w+) pluggable transports in Tor Launcher$/ do |bridge_
port = nil
fingerprint = nil
extra = nil
@bridge_hosts << address
if bridge_type == 'bridge'
open(bridge_dir + "/torrc") do |f|
port = f.grep(/^OrPort\b/).first.split.last
......@@ -376,6 +375,7 @@ When /^I configure some (\w+) pluggable transports in Tor Launcher$/ do |bridge_
open(bridge_dir + "/fingerprint") do |f|
fingerprint = f.read.chomp.split.last
end
@bridge_hosts << { address: address, port: port.to_i }
bridge_line = bridge_type + " " + address + ":" + port
[fingerprint, extra].each { |e| bridge_line += " " + e.to_s if e }
@screen.type(bridge_line + Sikuli::Key.ENTER)
......@@ -390,9 +390,9 @@ 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
assert_all_connections(@sniffer.pcap_file) do |host|
@bridge_hosts.include?({ address: host.address, port: host.port })
end
end
Then /^the Tor binary is configured to use the expected Tor authorities$/ do
......
When /^no traffic has flowed to the LAN$/ do
leaks = FirewallLeakCheck.new(@sniffer.pcap_file, :ignore_lan => false)
assert(not(leaks.ipv4_tcp_leaks.include?(@lan_host)),
"Traffic was sent to LAN host #{@lan_host}")
Then /^there was no traffic sent to the web server on the LAN$/ do
assert_no_connections(@sniffer.pcap_file) do |host|
host.address == @web_server_ip_addr and host.port == @web_server_port
end
end
require 'packetfu'
require 'ipaddr'
# Extent IPAddr with a private/public address space checks
class IPAddr
PrivateIPv4Ranges = [
IPAddr.new("10.0.0.0/8"),
IPAddr.new("172.16.0.0/12"),
IPAddr.new("192.168.0.0/16"),
IPAddr.new("255.255.255.255/32")
]
PrivateIPv6Ranges = [
IPAddr.new("fc00::/7")
]
def private?
private_ranges = self.ipv4? ? PrivateIPv4Ranges : PrivateIPv6Ranges
private_ranges.any? { |range| range.include?(self) }
end
def public?
!private?
end
end
class FirewallLeakCheck
attr_reader :ipv4_tcp_leaks, :ipv4_nontcp_leaks, :ipv6_leaks, :nonip_leaks, :mac_leaks
def initialize(pcap_file, options = {})
options[:accepted_hosts] ||= []
options[:ignore_lan] ||= true
@pcap_file = pcap_file
packets = PacketFu::PcapFile.new.file_to_array(:filename => @pcap_file)
mac_leaks = Set.new
ipv4_tcp_packets = []
ipv4_nontcp_packets = []
ipv6_packets = []
nonip_packets = []
packets.each do |p|
if PacketFu::EthPacket.can_parse?(p)
packet = PacketFu::EthPacket.parse(p)
mac_leaks << packet.eth_saddr
mac_leaks << packet.eth_daddr
end
if PacketFu::TCPPacket.can_parse?(p)
ipv4_tcp_packets << PacketFu::TCPPacket.parse(p)
elsif PacketFu::IPPacket.can_parse?(p)
ipv4_nontcp_packets << PacketFu::IPPacket.parse(p)
elsif PacketFu::IPv6Packet.can_parse?(p)
ipv6_packets << PacketFu::IPv6Packet.parse(p)
elsif PacketFu::Packet.can_parse?(p)
nonip_packets << PacketFu::Packet.parse(p)
else
save_pcap_file
raise "Found something in the pcap file that cannot be parsed"
end
# Returns the unique edges (based on protocol, source/destination
# address/port) in the graph of all network flows.
def pcap_connections_helper(pcap_file, opts = {})
opts[:ignore_dhcp] ||= true
connections = Array.new
packets = PacketFu::PcapFile.new.file_to_array(:filename => pcap_file)
packets.each do |p|
if PacketFu::EthPacket.can_parse?(p)
eth_packet = PacketFu::EthPacket.parse(p)
else
raise 'Found something that is not an ethernet packet'
end
ipv4_tcp_hosts = filter_hosts_from_ippackets(ipv4_tcp_packets,
options[:ignore_lan])
accepted = Set.new(options[:accepted_hosts])
@mac_leaks = mac_leaks
@ipv4_tcp_leaks = ipv4_tcp_hosts.select { |host| !accepted.member?(host) }
@ipv4_nontcp_leaks = filter_hosts_from_ippackets(ipv4_nontcp_packets,
options[:ignore_lan])
@ipv6_leaks = filter_hosts_from_ippackets(ipv6_packets,
options[:ignore_lan])
@nonip_leaks = nonip_packets
end
def save_pcap_file
save_failure_artifact("Network capture", @pcap_file)
end
# Returns a list of all unique destination IP addresses found in
# `packets`. Exclude LAN hosts if ignore_lan is set.
def filter_hosts_from_ippackets(packets, ignore_lan)
hosts = []
packets.each do |p|
candidate = nil
if p.kind_of?(PacketFu::IPPacket)
candidate = p.ip_daddr
elsif p.kind_of?(PacketFu::IPv6Packet)
candidate = p.ipv6_header.ipv6_daddr
else
save_pcap_file
raise "Expected an IP{v4,v6} packet, but got something else:\n" +
p.peek_format
end
if candidate != nil and (not(ignore_lan) or IPAddr.new(candidate).public?)
hosts << candidate
end
sport = nil
dport = nil
if PacketFu::TCPPacket.can_parse?(p)
ip_packet = PacketFu::TCPPacket.parse(p)
protocol = 'tcp'
sport = ip_packet.tcp_sport
dport = ip_packet.tcp_dport
elsif PacketFu::UDPPacket.can_parse?(p)
ip_packet = PacketFu::UDPPacket.parse(p)
protocol = 'udp'
sport = ip_packet.udp_sport
dport = ip_packet.udp_dport
elsif PacketFu::ICMPPacket.can_parse?(p)
ip_packet = PacketFu::ICMPPacket.parse(p)
protocol = 'icmp'
elsif PacketFu::IPPacket.can_parse?(p)
ip_packet = PacketFu::IPPacket.parse(p)
protocol = 'ip'
elsif PacketFu::IPv6Packet.can_parse?(p)
ip_packet = PacketFu::IPv6Packet.parse(p)
protocol = 'ipv6'
else
raise "Found something that cannot be parsed"
end
hosts.uniq
end
def assert_no_leaks
err = ""
if !@ipv4_tcp_leaks.empty?
err += "The following IPv4 TCP non-Tor Internet hosts were " +
"contacted:\n" + ipv4_tcp_leaks.join("\n")
end
if !@ipv4_nontcp_leaks.empty?
err += "The following IPv4 non-TCP Internet hosts were contacted:\n" +
ipv4_nontcp_leaks.join("\n")
end
if !@ipv6_leaks.empty?
err += "The following IPv6 Internet hosts were contacted:\n" +
ipv6_leaks.join("\n")
end
if !@nonip_leaks.empty?
err += "Some non-IP packets were sent\n"
end
if !err.empty?
save_pcap_file
raise err
if protocol == "udp" and
sport == 68 and
dport == 67 and
ip_packet.ip_saddr == '0.0.0.0' and
ip_packet.ip_daddr == "255.255.255.255"
next if opts[:ignore_dhcp]
end
connections << {
mac_saddr: eth_packet.eth_saddr,
mac_daddr: eth_packet.eth_daddr,
protocol: protocol,
saddr: ip_packet.ip_saddr,
daddr: ip_packet.ip_daddr,
sport: sport,
dport: dport,
# Convenience aliases since we most often care about the
# destination side of connections (i.e. leaks):
address: ip_packet.ip_daddr,
port: dport,
}
end
connections.uniq.map { |p| OpenStruct.new(p) }
end
def assert_all_connections(pcap_file, opts = {}, &block)
all = pcap_connections_helper(pcap_file, opts)
good = all.find_all(&block)
bad = all - good
save_failure_artifact("Network capture", pcap_file) unless bad.empty?
assert(bad.empty?, "Unexpected hosts were contacted:\n#{bad}")
end
def assert_no_connections(pcap_file, opts = {}, &block)
assert_all_connections(pcap_file, opts) { |*args| not(block.call(*args)) }
end
......@@ -193,11 +193,19 @@ def cmd_helper(cmd, env = {})
end
end
# This command will grab all router IP addresses from the Tor
# consensus in the VM + the hardcoded TOR_AUTHORITIES.
def get_all_tor_nodes
cmd = 'awk "/^r/ { print \$6 }" /var/lib/tor/cached-microdesc-consensus'
$vm.execute(cmd).stdout.chomp.split("\n") + TOR_AUTHORITIES
nodes = Array.new
chutney_torrcs = Dir.glob(
"#{$config['TMPDIR']}/chutney-data/nodes/*/torrc"
)
chutney_torrcs.each do |torrc|
open(torrc) do |f|
nodes += f.grep(/^(Or|Dir)Port\b/).map do |line|
{ address: $vmnet.bridge_ip_addr, port: line.split.last.to_i }
end
end
end
return nodes
end
def get_free_space(machine, path)
......
require 'ipaddr'
require 'libvirt'
require 'rexml/document'
......
......@@ -253,14 +253,10 @@ end
After('@product', '@check_tor_leaks') do |scenario|
@tor_leaks_sniffer.stop
if scenario.passed?
if @bridge_hosts.nil?
expected_tor_nodes = get_all_tor_nodes
else
expected_tor_nodes = @bridge_hosts
expected_tor_nodes = @bridge_hosts ? @bridge_hosts : get_all_tor_nodes
assert_all_connections(@tor_leaks_sniffer.pcap_file) do |host|
expected_tor_nodes.include?({ address: host.address, port: host.port })
end
leaks = FirewallLeakCheck.new(@tor_leaks_sniffer.pcap_file,
:accepted_hosts => expected_tor_nodes)
leaks.assert_no_leaks
end
end
......
......@@ -18,34 +18,34 @@ Feature: The Tor enforcement is effective
And the firewall is configured to block all external IPv6 traffic
@fragile
Scenario: Anti test: Detecting IPv4 TCP leaks from the Unsafe Browser with the firewall leak detector
Scenario: Anti test: Detecting TCP leaks from the Unsafe Browser with the firewall leak detector
Given I have started Tails from DVD and logged in and the network is connected
And I capture all network traffic
When I successfully start the Unsafe Browser
And I open Tor Check in the Unsafe Browser
And I see Tor Check fail in the Unsafe Browser
Then the firewall leak detector has detected IPv4 TCP leaks
Then the firewall leak detector has detected leaks
Scenario: Anti test: Detecting IPv4 TCP leaks of TCP DNS lookups with the firewall leak detector
Scenario: Anti test: Detecting TCP leaks of DNS lookups with the firewall leak detector
Given I have started Tails from DVD and logged in and the network is connected
And 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
Then the firewall leak detector has detected leaks
Scenario: Anti test: Detecting IPv4 non-TCP leaks (UDP) of UDP DNS lookups with the firewall leak detector
Scenario: Anti test: Detecting UDP leaks of DNS lookups with the firewall leak detector
Given I have started Tails from DVD and logged in and the network is connected
And 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
Then the firewall leak detector has detected leaks
Scenario: Anti test: Detecting IPv4 non-TCP (ICMP) leaks of ping with the firewall leak detector
Scenario: Anti test: Detecting ICMP leaks of ping with the firewall leak detector
Given I have started Tails from DVD and logged in and the network is connected
And 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
Then the firewall leak detector has detected leaks
@check_tor_leaks
Scenario: The Tor enforcement is effective at blocking untorified TCP connection attempts
......
......@@ -14,7 +14,7 @@ Feature: Browsing the web using the Tor Browser
And the Tor Browser has started and loaded the startup page
And I open a page on the LAN web server in the Tor Browser
Then I see "TorBrowserUnableToConnect.png" after at most 20 seconds
And no traffic has flowed to the LAN
And there was no traffic sent to the web server on the LAN
@check_tor_leaks
Scenario: The Tor Browser directory is usable
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment