Calendar: add FT sprint
[tails/test.git] / features / step_definitions / chutney.rb
blob8e02b6e51aaedcf90033e59b5b9ce79675d79d8e
1 def chutney_src_dir
2   "#{GIT_DIR}/submodules/chutney"
3 end
5 def chutney_status_log(cmd)
6   action = case cmd
7            when 'start'
8              'starting'
9            when 'stop'
10              'stopping'
11            when 'stop_old'
12              'cleaning up old instance'
13            when 'configure'
14              'configuring'
15            when 'wait_for_bootstrap'
16              'waiting for bootstrap (might take a few minutes)'
17            when 'done'
18              'started!'
19            else
20              return
21            end
22   puts("Chutney Tor network simulation: #{action} ...")
23 end
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"
36   assert(
37     File.executable?(chutney_script),
38     "It does not look like '#{chutney_src_dir}' is the Chutney source tree"
39   )
40   network_definition = "#{GIT_DIR}/features/chutney/test-network"
41   env = {
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',
47   }
49   chutney_data_dir_cleanup = proc do
50     if File.directory?(env['CHUTNEY_DATA_DIR'])
51       FileUtils.rm_r(env['CHUTNEY_DATA_DIR'])
52     end
53   end
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)
60     end
61   end
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.
68   begin
69     cmd_helper(['pkill', '--full', '--exact',
70                 "tor -f #{env['CHUTNEY_DATA_DIR']}/nodes/.*/torrc --quiet",])
71   rescue StandardError
72     # Nothing to kill
73   end
75   if KEEP_CHUTNEY
76     begin
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.'
85       else
86         chutney_cmd.call('configure')
87         chutney_cmd.call('start')
88       end
89     end
90   else
91     chutney_cmd.call('stop_old')
92     chutney_data_dir_cleanup.call
93     chutney_cmd.call('configure')
94     chutney_cmd.call('start')
95   end
97   # Documentation: submodules/chutney/README, "Waiting for the network" section
98   chutney_cmd.call('wait_for_bootstrap')
100   at_exit do
101     chutney_cmd.call('stop')
102     chutney_data_dir_cleanup.call unless KEEP_CHUTNEY
103   end
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)
112   assert_equal(
113     total, running, "Chutney is only running #{running}/#{total} nodes"
114   )
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,
128   # instead).
129   #
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:
133   #
134   # 1. During a proper boot, as soon as the remote shell is up in the
135   #    'the computer boots Tails' step.
136   #
137   # 2. When restoring a snapshot, in restore_background().
138   #
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
141   # a tagged scenario.
142   #
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',
154     'AssumeReachable 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',
173   ]
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"
178   )
179   dir_auth_lines = File.open(client_torrcs.first) do |f|
180     f.grep(/^(Alternate)?(Dir|Bridge)Authority\s/)
181   end
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"
189   ).first
190   hs_hostname = File.open(hs_hostname_file_path) do |f|
191     f.read.chomp
192   end
193   hs_torrc_path = Dir.glob(
194     "#{$config['TMPDIR']}/chutney-data/nodes/*hs/torrc"
195   ).first
196   _, hs_port, local_address_port = File.open(hs_torrc_path) do |f|
197     f.grep(/^HiddenServicePort/).first.split
198   end
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'
206   kill_redir = proc do
207     begin
208       if system('/bin/systemctl', bus, '--quiet', 'is-active', redir_unit_name)
209         system('/bin/systemctl', bus, 'stop', redir_unit_name)
210       end
211     rescue StandardError
212       # noop
213     end
214   end
215   kill_redir.call
216   local_address, local_port, = chutney_onionservice_info
217   $chutney_onionservice_job = fatal_system(
218     '/usr/bin/systemd-run',
219     bus,
220     "--unit=#{redir_unit_name}",
221     '--service-type=forking',
222     '--quiet',
223     # XXX: enable this once we require systemd v236 or newer
224     # for running our test suite
225     # '--collect',
226     '/usr/bin/redir',
227     "#{local_address}:#{local_port}",
228     "#{remote_address}:#{remote_port}"
229   )
230   add_after_scenario_hook { kill_redir.call }
231   $chutney_onionservice_job