2 "#{GIT_DIR}/submodules/chutney"
5 def chutney_status_log(cmd)
12 'cleaning up old instance'
15 when 'wait_for_bootstrap'
16 'waiting for bootstrap (might take a few minutes)'
22 puts("Chutney Tor network simulation: #{action} ...")
25 # XXX: giving up on a few worst offenders for now
26 # rubocop:disable Metrics/AbcSize
27 # rubocop:disable Metrics/MethodLength
28 def ensure_chutney_is_running
29 # Ensure that a fresh chutney instance is running, and that it will
30 # be cleaned upon exit. We only do it once, though, since the same
31 # setup can be used throughout the same test suite run.
32 return if $chutney_initialized
34 chutney_listen_address = $vmnet.bridge_ip_addr
35 chutney_script = "#{chutney_src_dir}/chutney"
37 File.executable?(chutney_script),
38 "It does not look like '#{chutney_src_dir}' is the Chutney source tree"
40 network_definition = "#{GIT_DIR}/features/chutney/test-network"
42 'CHUTNEY_LISTEN_ADDRESS' => chutney_listen_address,
43 'CHUTNEY_DATA_DIR' => "#{$config['TMPDIR']}/chutney-data",
44 # The default value (60s) is too short for "chutney wait_for_bootstrap"
45 # to succeed reliably.
46 'CHUTNEY_START_TIME' => '600',
49 chutney_data_dir_cleanup = proc do
50 if File.directory?(env['CHUTNEY_DATA_DIR'])
51 FileUtils.rm_r(env['CHUTNEY_DATA_DIR'])
55 chutney_cmd = proc do |cmd|
56 chutney_status_log(cmd)
57 cmd = 'stop' if cmd == 'stop_old'
58 Dir.chdir(chutney_src_dir) do
59 cmd_helper([chutney_script, cmd, network_definition], env)
63 # After an unclean shutdown of the test suite (e.g. Ctrl+C) the
64 # tor processes are left running, listening on the same ports we
65 # are about to use. If chutney's data dir also was removed, this
66 # will prevent chutney from starting the network unless the tor
67 # processes are killed manually.
69 cmd_helper(['pkill', '--full', '--exact',
70 "tor -f #{env['CHUTNEY_DATA_DIR']}/nodes/.*/torrc --quiet",])
77 chutney_cmd.call('start')
78 rescue Test::Unit::AssertionFailedError
79 if File.directory?(env['CHUTNEY_DATA_DIR'])
80 raise 'You are running with --keep-snapshots or --keep-chutney, ' \
81 'but Chutney failed ' \
82 'to start with its current data directory. To recover you ' \
83 "likely want to delete '#{env['CHUTNEY_DATA_DIR']}' and " \
84 'all test suite snapshots and then start over.'
86 chutney_cmd.call('configure')
87 chutney_cmd.call('start')
91 chutney_cmd.call('stop_old')
92 chutney_data_dir_cleanup.call
93 chutney_cmd.call('configure')
94 chutney_cmd.call('start')
97 # Documentation: submodules/chutney/README, "Waiting for the network" section
98 chutney_cmd.call('wait_for_bootstrap')
101 chutney_cmd.call('stop')
102 chutney_data_dir_cleanup.call unless KEEP_CHUTNEY
105 # We have to sanity check that all nodes are running because
106 # `chutney start` will return success even if some nodes fail.
107 status = chutney_cmd.call('status')
108 match = Regexp.new('^(\d+)/(\d+) nodes are running$').match(status)
109 assert_not_nil(match, "Chutney's status did not contain the expected " \
110 'string listing the number of running nodes')
111 running, total = match[1, 2].map(&:to_i)
113 total, running, "Chutney is only running #{running}/#{total} nodes"
116 $chutney_initialized = true
117 chutney_status_log('done')
119 # rubocop:enable Metrics/AbcSize
120 # rubocop:enable Metrics/MethodLength
122 When /^I configure Tails to use a simulated Tor network$/ do
123 # At the moment this step essentially assumes that we boot with 'the
124 # network is unplugged', run this step, and then 'the network is
125 # plugged'. I believe we can make this pretty transparent without
126 # the need of a dedicated step by using tags (e.g. @fake_tor or
127 # whatever -- possibly we want the opposite, @real_tor,
130 # There are two time points where we for a scenario must ensure that
131 # the client configuration below is enabled if and only if the
132 # scenario is tagged, and that is:
134 # 1. During a proper boot, as soon as the remote shell is up in the
135 # 'the computer boots Tails' step.
137 # 2. When restoring a snapshot, in restore_background().
139 # If we do this, it doesn't even matter if a snapshot is made of an
140 # untagged scenario (without the conf), and we later restore it with
143 # Note: We probably have to clear the /var/lib/tor data dir when we
144 # switch mode. Possibly there are other such problems that make this
145 # abstraction impractical and it's better that we avoid it an go
146 # with the more explicit, step-based approach.
148 assert($vm.execute('service tor status').failure?,
149 'Running this step when Tor is running is probably not intentional')
150 ensure_chutney_is_running
151 # Most of these lines are taken from chutney's client template.
152 client_torrc_lines = [
153 'TestingTorNetwork 1',
155 'PathsNeededToBuildCircuits 0.25',
156 'TestingBridgeDownloadSchedule 0, 5',
157 'TestingClientConsensusDownloadSchedule 0, 5',
158 'TestingClientDownloadSchedule 0, 5',
159 'TestingDirAuthVoteExit *',
160 'TestingDirAuthVoteGuard *',
161 'TestingDirAuthVoteHSDir *',
162 'TestingMinExitFlagThreshold 0',
163 'V3AuthNIntervalsValid 2',
164 # Enabling TestingTorNetwork disables ClientRejectInternalAddresses
165 # so the Tor client will happily try LAN connections. Coupled with
166 # that TestingTorNetwork is enabled on all exits, and their
167 # ExitPolicyRejectPrivate is disabled, we will allow exiting to
168 # LAN hosts. We have at least one test that tries to make sure
169 # that is *not* possible (Scenario: The Tor Browser cannot access
170 # the LAN) so we cannot allow it. We'll have to rethink all this
171 # if we ever want to run all services locally as well (#9520).
172 'ClientRejectInternalAddresses 1',
174 # We run one client in chutney so we easily can grep the generated
175 # DirAuthority lines and use them.
176 client_torrcs = Dir.glob(
177 "#{$config['TMPDIR']}/chutney-data/nodes/*client/torrc"
179 dir_auth_lines = File.open(client_torrcs.first) do |f|
180 f.grep(/^(Alternate)?(Dir|Bridge)Authority\s/)
182 client_torrc_lines.concat(dir_auth_lines)
183 $vm.file_append('/etc/tor/torrc', client_torrc_lines)
186 def chutney_onionservice_info
187 hs_hostname_file_path = Dir.glob(
188 "#{$config['TMPDIR']}/chutney-data/nodes/*hs/hidden_service/hostname"
190 hs_hostname = File.open(hs_hostname_file_path) do |f|
193 hs_torrc_path = Dir.glob(
194 "#{$config['TMPDIR']}/chutney-data/nodes/*hs/torrc"
196 _, hs_port, local_address_port = File.open(hs_torrc_path) do |f|
197 f.grep(/^HiddenServicePort/).first.split
199 local_address, local_port = local_address_port.split(':')
200 [local_address, local_port, hs_hostname, hs_port]
203 def chutney_onionservice_redir(remote_address, remote_port)
204 redir_unit_name = 'tails-test-suite-redir.service'
205 bus = ENV['USER'] == 'root' ? '--system' : '--user'
208 if system('/bin/systemctl', bus, '--quiet', 'is-active', redir_unit_name)
209 system('/bin/systemctl', bus, 'stop', redir_unit_name)
216 local_address, local_port, = chutney_onionservice_info
217 $chutney_onionservice_job = fatal_system(
218 '/usr/bin/systemd-run',
220 "--unit=#{redir_unit_name}",
221 '--service-type=forking',
223 # XXX: enable this once we require systemd v236 or newer
224 # for running our test suite
227 "#{local_address}:#{local_port}",
228 "#{remote_address}:#{remote_port}"
230 add_after_scenario_hook { kill_redir.call }
231 $chutney_onionservice_job