Files
amnezia-web-panel/tests/test_sudo_command_chains.py
hell 539ef92197 test: cover the exit-node paths these two suites already touch
Both files were submitted in #143 and #147, before the exit node existed here,
so their exit-specific halves had to be stripped to keep them importable. That
code is in the tree since #149, and the stripped copies now test less than they
could:

- test_ssh_error_text.py gains ExitLinkMessageTests: an SSH whose sudo call
  returns -1 with no output at all (a dropped transport) must make exit_link
  and exit_unlink raise a message that names the exit code, not
  "exit link apply failed: " with nothing after the colon. That is the failure
  the empty str(EOFError) produced, and it is the reason #147 exists.
- test_sudo_command_chains.py pins amnezia-exit, not amnezia-awg2, as the
  representative container that DNSManager attaches to amnezia-dns-net. Both
  are in vpn_containers, but only the exit one silently breaks DNS on an exit
  node when it falls out of the list.

No production code changes.
2026-09-11 01:39:13 +07:00

108 lines
3.9 KiB
Python

"""A privileged command chain must run inside one shell.
`SSHManager.run_sudo_command` builds `echo <pw> | sudo -S -p '' <command>`, so
in `sudo a && b` only `a` is privileged: `b` runs as the panel's unprivileged
SSH user. Wherever both halves need root, the chain has to be handed to a
single `sh -c` (the pattern the rest of BackupManager already uses).
"""
import unittest
from managers.backup_manager import BackupManager
from managers.adguard_manager import AdguardManager
from managers.dns_manager import DNSManager
from managers.wireguard_manager import WireGuardManager
class RecordingSSH:
def __init__(self, code=1):
self.commands = []
self.code = code
def run_sudo_command(self, command, timeout=60):
self.commands.append(command)
return '', 'not found', self.code
def run_command(self, command, timeout=60):
self.commands.append(command)
# DNSManager gates its install on this probe
return ('Docker version 27.0.3' if 'docker --version' in command else ''), '', 0
def write_file(self, path, content):
pass
def upload_file(self, content, path):
pass
def chain_is_wrapped(command):
"""True when every `&&`/`||`/`|` sits inside a `sh -c`/`bash -c` argument."""
for token in ('&&', '||', '|'):
idx = command.find(token)
if idx == -1:
continue
shell = min((command.find(s) for s in ('sh -c', 'bash -c') if command.find(s) != -1), default=-1)
if shell == -1 or shell > idx:
return False
return True
class SudoChainTests(unittest.TestCase):
def test_backup_fetch_runs_the_chain_in_one_shell(self):
ssh = RecordingSSH()
BackupManager(ssh)._fetch_remote_archive('/opt/amnezia/backups/awg2/awg2-2026.tar.gz')
cmd = ssh.commands[0]
self.assertTrue(cmd.startswith('sh -c '), cmd)
self.assertTrue(chain_is_wrapped(cmd), cmd)
# the copy still targets the same paths
self.assertIn('/opt/amnezia/backups/awg2/awg2-2026.tar.gz', cmd)
self.assertIn('chmod 0644', cmd)
def test_dns_network_creation_runs_the_chain_in_one_shell(self):
ssh = RecordingSSH(code=0)
try:
DNSManager(ssh).install_protocol()
except Exception:
pass
network = [c for c in ssh.commands if 'amnezia-dns-net' in c and 'network' in c]
self.assertTrue(network, ssh.commands)
self.assertTrue(chain_is_wrapped(network[0]), network[0])
def test_dns_attaches_existing_containers_in_one_shell(self):
ssh = RecordingSSH(code=0)
try:
DNSManager(ssh).install_protocol()
except Exception:
pass
connects = [c for c in ssh.commands if 'network connect' in c]
self.assertTrue(connects, ssh.commands)
for cmd in connects:
self.assertTrue(chain_is_wrapped(cmd), cmd)
# the exit-node container is among the ones attached
self.assertTrue(any('amnezia-exit' in c for c in connects))
def test_adguard_network_creation_runs_the_chain_in_one_shell(self):
ssh = RecordingSSH(code=0)
AdguardManager(ssh)._ensure_network()
self.assertTrue(chain_is_wrapped(ssh.commands[0]), ssh.commands[0])
def test_wireguard_bw_limits_run_the_chain_in_one_shell(self):
ssh = RecordingSSH(code=0)
manager = WireGuardManager(ssh)
manager.check_container_running = lambda: True
manager._apply_bw_limits([
{'userData': {'maxSpeed': 10, 'clientIp': '10.8.2.2'}},
])
execs = [c for c in ssh.commands if 'docker exec' in c and '_wg_tc.sh' in c]
self.assertTrue(execs, ssh.commands)
for cmd in execs:
self.assertTrue(chain_is_wrapped(cmd), cmd)
def test_helper_flags_a_bare_chain(self):
self.assertFalse(chain_is_wrapped("test -f a && cp a b"))
self.assertTrue(chain_is_wrapped("sh -c 'test -f a && cp a b'"))
if __name__ == '__main__':
unittest.main()