Skip to content

Commit 54de23a

Browse files
committed
Add firewall tests.
1 parent ac72369 commit 54de23a

File tree

4 files changed

+154
-33
lines changed

4 files changed

+154
-33
lines changed

.travis.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
language: python
2+
python:
3+
- 2.6
4+
- 2.7
5+
- 3.5
6+
- pypy
7+
8+
install:
9+
- travis_retry pip install -q pytest mock
10+
11+
script:
12+
- py.test

sshuttle/firewall.py

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
from sshuttle.methods import get_auto_method, get_method
99

1010
hostmap = {}
11+
HOSTSFILE = '/etc/hosts'
1112

1213

1314
def rewrite_etc_hosts(port):
14-
HOSTSFILE = '/etc/hosts'
1515
BAKFILE = '%s.sbak' % HOSTSFILE
1616
APPEND = '# sshuttle-firewall-%d AUTOCREATED' % port
1717
old_content = ''
@@ -51,6 +51,30 @@ def restore_etc_hosts(port):
5151
rewrite_etc_hosts(port)
5252

5353

54+
# Isolate function that needs to be replaced for tests
55+
def setup_daemon():
56+
if os.getuid() != 0:
57+
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
58+
59+
# don't disappear if our controlling terminal or stdout/stderr
60+
# disappears; we still have to clean up.
61+
signal.signal(signal.SIGHUP, signal.SIG_IGN)
62+
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
63+
signal.signal(signal.SIGTERM, signal.SIG_IGN)
64+
signal.signal(signal.SIGINT, signal.SIG_IGN)
65+
66+
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
67+
# I'll die automatically.
68+
os.setsid()
69+
70+
# because of limitations of the 'su' command, the *real* stdin/stdout
71+
# are both attached to stdout initially. Clone stdout into stdin so we
72+
# can read from it.
73+
os.dup2(1, 0)
74+
75+
return sys.stdin, sys.stdout
76+
77+
5478
# This is some voodoo for setting up the kernel's transparent
5579
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
5680
# otherwise we delete it, then make them from scratch.
@@ -60,50 +84,33 @@ def restore_etc_hosts(port):
6084
# supercede it in the transproxy list, at least, so the leftover rules
6185
# are hopefully harmless.
6286
def main(method_name, syslog):
63-
if os.getuid() != 0:
64-
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
87+
stdin, stdout = setup_daemon()
6588

6689
if method_name == "auto":
6790
method = get_auto_method()
6891
else:
6992
method = get_method(method_name)
7093

71-
# because of limitations of the 'su' command, the *real* stdin/stdout
72-
# are both attached to stdout initially. Clone stdout into stdin so we
73-
# can read from it.
74-
os.dup2(1, 0)
75-
7694
if syslog:
7795
ssyslog.start_syslog()
7896
ssyslog.stderr_to_syslog()
7997

80-
debug1('firewall manager ready method name %s.\n' % method.name)
81-
sys.stdout.write('READY %s\n' % method.name)
82-
sys.stdout.flush()
83-
84-
# don't disappear if our controlling terminal or stdout/stderr
85-
# disappears; we still have to clean up.
86-
signal.signal(signal.SIGHUP, signal.SIG_IGN)
87-
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
88-
signal.signal(signal.SIGTERM, signal.SIG_IGN)
89-
signal.signal(signal.SIGINT, signal.SIG_IGN)
90-
91-
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
92-
# I'll die automatically.
93-
os.setsid()
98+
debug1('firewall manager ready method name %s.\n' % method_name)
99+
stdout.write('READY %s\n' % method_name)
100+
stdout.flush()
94101

95102
# we wait until we get some input before creating the rules. That way,
96103
# sshuttle can launch us as early as possible (and get sudo password
97104
# authentication as early in the startup process as possible).
98-
line = sys.stdin.readline(128)
105+
line = stdin.readline(128)
99106
if not line:
100107
return # parent died; nothing to do
101108

102109
subnets = []
103110
if line != 'ROUTES\n':
104111
raise Fatal('firewall: expected ROUTES but got %r' % line)
105112
while 1:
106-
line = sys.stdin.readline(128)
113+
line = stdin.readline(128)
107114
if not line:
108115
raise Fatal('firewall: expected route but got %r' % line)
109116
elif line.startswith("NSLIST\n"):
@@ -119,7 +126,7 @@ def main(method_name, syslog):
119126
if line != 'NSLIST\n':
120127
raise Fatal('firewall: expected NSLIST but got %r' % line)
121128
while 1:
122-
line = sys.stdin.readline(128)
129+
line = stdin.readline(128)
123130
if not line:
124131
raise Fatal('firewall: expected nslist but got %r' % line)
125132
elif line.startswith("PORTS "):
@@ -155,7 +162,7 @@ def main(method_name, syslog):
155162
debug2('Got ports: %d,%d,%d,%d\n'
156163
% (port_v6, port_v4, dnsport_v6, dnsport_v4))
157164

158-
line = sys.stdin.readline(128)
165+
line = stdin.readline(128)
159166
if not line:
160167
raise Fatal('firewall: expected GO but got %r' % line)
161168
elif not line.startswith("GO "):
@@ -169,26 +176,28 @@ def main(method_name, syslog):
169176
do_wait = None
170177
debug1('firewall manager: starting transproxy.\n')
171178

179+
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
172180
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
173181
if port_v6 > 0:
174182
do_wait = method.setup_firewall(
175-
port_v6, dnsport_v6, nslist,
183+
port_v6, dnsport_v6, nslist_v6,
176184
socket.AF_INET6, subnets_v6, udp)
177185
elif len(subnets_v6) > 0:
178186
debug1("IPv6 subnets defined but IPv6 disabled\n")
179187

188+
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
180189
subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET]
181190
if port_v4 > 0:
182191
do_wait = method.setup_firewall(
183-
port_v4, dnsport_v4, nslist,
192+
port_v4, dnsport_v4, nslist_v4,
184193
socket.AF_INET, subnets_v4, udp)
185194
elif len(subnets_v4) > 0:
186195
debug1('IPv4 subnets defined but IPv4 disabled\n')
187196

188-
sys.stdout.write('STARTED\n')
197+
stdout.write('STARTED\n')
189198

190199
try:
191-
sys.stdout.flush()
200+
stdout.flush()
192201
except IOError:
193202
# the parent process died for some reason; he's surely been loud
194203
# enough, so no reason to report another error
@@ -198,9 +207,9 @@ def main(method_name, syslog):
198207
# to stay running so that we don't need a *second* password
199208
# authentication at shutdown time - that cleanup is important!
200209
while 1:
201-
if do_wait:
210+
if do_wait is not None:
202211
do_wait()
203-
line = sys.stdin.readline(128)
212+
line = stdin.readline(128)
204213
if line.startswith('HOST '):
205214
(name, ip) = line[5:].strip().split(',', 1)
206215
hostmap[name] = ip

sshuttle/tests/test_firewall.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from mock import Mock, patch, call
2+
from contextlib import nested
3+
import io
4+
import os
5+
import os.path
6+
import shutil
7+
import filecmp
8+
import pytest
9+
10+
import sshuttle.firewall
11+
12+
13+
def setup_daemon():
14+
stdin = io.StringIO(u"""ROUTES
15+
2,24,0,1.2.3.0
16+
2,32,1,1.2.3.66
17+
10,64,0,2404:6800:4004:80c::
18+
10,128,1,2404:6800:4004:80c::101f
19+
NSLIST
20+
2,1.2.3.33
21+
10,2404:6800:4004:80c::33
22+
PORTS 1024,1025,1026,1027
23+
GO 1
24+
""")
25+
stdout = Mock()
26+
return stdin, stdout
27+
28+
29+
def test_rewrite_etc_hosts():
30+
if not os.path.isdir("tmp"):
31+
os.mkdir("tmp")
32+
33+
with open("tmp/hosts.orig", "w") as f:
34+
f.write("1.2.3.3 existing\n")
35+
36+
shutil.copyfile("tmp/hosts.orig", "tmp/hosts")
37+
38+
sshuttle.firewall.HOSTSFILE = "tmp/hosts"
39+
sshuttle.firewall.hostmap = {
40+
'myhost': '1.2.3.4',
41+
'myotherhost': '1.2.3.5',
42+
}
43+
sshuttle.firewall.rewrite_etc_hosts(10)
44+
with open("tmp/hosts") as f:
45+
line = f.next()
46+
s = line.split()
47+
assert s == ['1.2.3.3', 'existing']
48+
49+
line = f.next()
50+
s = line.split()
51+
assert s == ['1.2.3.4', 'myhost',
52+
'#', 'sshuttle-firewall-10', 'AUTOCREATED']
53+
54+
line = f.next()
55+
s = line.split()
56+
assert s == ['1.2.3.5', 'myotherhost',
57+
'#', 'sshuttle-firewall-10', 'AUTOCREATED']
58+
59+
with pytest.raises(StopIteration):
60+
line = f.next()
61+
62+
sshuttle.firewall.restore_etc_hosts(10)
63+
assert filecmp.cmp("tmp/hosts.orig", "tmp/hosts", shallow=False) is True
64+
65+
66+
def test_main():
67+
with nested(
68+
patch('sshuttle.firewall.setup_daemon'),
69+
patch('sshuttle.firewall.get_method')
70+
) as (mock_setup_daemon, mock_get_method):
71+
stdin, stdout = setup_daemon()
72+
mock_setup_daemon.return_value = stdin, stdout
73+
74+
sshuttle.firewall.main("test", False)
75+
76+
stdout.mock_calls == [
77+
call.write('READY test\n'),
78+
call.flush(),
79+
call.write('STARTED\n'),
80+
call.flush()
81+
]
82+
mock_setup_daemon.mock_calls == [call()]
83+
mock_get_method.mock_calls == [
84+
call('test'),
85+
call().setup_firewall(
86+
1024, 1026,
87+
[(10, u'2404:6800:4004:80c::33')],
88+
10,
89+
[(10, 64, False, u'2404:6800:4004:80c::'),
90+
(10, 128, True, u'2404:6800:4004:80c::101f')],
91+
True),
92+
call().setup_firewall(
93+
1025, 1027,
94+
[(2, u'1.2.3.33')],
95+
2,
96+
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
97+
True),
98+
call().setup_firewall()(),
99+
call().setup_firewall(1024, 0, [], 10, [], True),
100+
call().setup_firewall(1025, 0, [], 2, [], True),
101+
]

sshuttle/ui-macos/sshuttle

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)