Skip to content

Commit d2ee34d

Browse files
author
Joao Vieira
committed
dns: Added --ns-hosts to tunnel only some requests
By default, the --dns flag configures the firewall to only intercept queries made to the nameservers defined in resolvconf. This flag enables the user to explicitly specify the nameservers which queries will be redirected. This can be useful when the local nameserver forwards queries to some domains to a nameserver on the remote site of the tunnel.
1 parent 3cf5002 commit d2ee34d

File tree

4 files changed

+38
-22
lines changed

4 files changed

+38
-22
lines changed

src/client.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,15 +277,18 @@ def print_listening(self, what):
277277
class FirewallClient:
278278

279279
def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude,
280-
dnsport_v6, dnsport_v4, method, udp):
280+
dnsport_v6, dnsport_v4, ns_hosts, method, udp):
281281
self.auto_nets = []
282282
self.subnets_include = subnets_include
283283
self.subnets_exclude = subnets_exclude
284+
self.ns_hosts = ns_hosts
284285
argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] +
285286
['-v'] * (helpers.verbose or 0) +
286287
['--firewall', str(port_v6), str(port_v4),
287288
str(dnsport_v6), str(dnsport_v4),
288289
method, str(int(udp))])
290+
if dnsport_v4 or dnsport_v6:
291+
argvbase += ['--ns-hosts', ns_hosts]
289292
if ssyslog._p:
290293
argvbase += ['--syslog']
291294
argv_tries = [
@@ -599,7 +602,7 @@ def onhostlist(hostlist):
599602

600603

601604
def main(listenip_v6, listenip_v4,
602-
ssh_cmd, remotename, python, latency_control, dns,
605+
ssh_cmd, remotename, python, latency_control, dns, ns_hosts,
603606
method, seed_hosts, auto_nets,
604607
subnets_include, subnets_exclude, syslog, daemon, pidfile):
605608

@@ -695,7 +698,9 @@ def main(listenip_v6, listenip_v4,
695698
udp_listener.print_listening("UDP redirector")
696699

697700
bound = False
698-
if dns:
701+
if dns or ns_hosts:
702+
if dns:
703+
ns_hosts += resolvconf_nameservers()
699704
# search for spare port for DNS
700705
debug2('Binding DNS:')
701706
ports = xrange(12300, 9000, -1)
@@ -735,9 +740,11 @@ def main(listenip_v6, listenip_v4,
735740
dnsport_v6 = 0
736741
dnsport_v4 = 0
737742
dns_listener = None
743+
ns_hosts = []
738744

739745
fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include,
740-
subnets_exclude, dnsport_v6, dnsport_v4, method, udp)
746+
subnets_exclude, dnsport_v6, dnsport_v4, ns_hosts,
747+
method, udp)
741748

742749
if fw.method == "tproxy":
743750
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)

src/firewall.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def _ipt_ttl(family, *args):
8383
# multiple copies shouldn't have overlapping subnets, or only the most-
8484
# recently-started one will win (because we use "-I OUTPUT 1" instead of
8585
# "-A OUTPUT").
86-
def do_iptables_nat(port, dnsport, family, subnets, udp):
86+
def do_iptables_nat(port, dnsport, nslist, family, subnets, udp):
8787
# only ipv4 supported with NAT
8888
if family != socket.AF_INET:
8989
raise Exception(
@@ -134,7 +134,6 @@ def ipt_ttl(*args):
134134
'--to-ports', str(port))
135135

136136
if dnsport:
137-
nslist = resolvconf_nameservers()
138137
for f, ip in filter(lambda i: i[0] == family, nslist):
139138
ipt_ttl('-A', chain, '-j', 'REDIRECT',
140139
'--dest', '%s/32' % ip,
@@ -143,7 +142,7 @@ def ipt_ttl(*args):
143142
'--to-ports', str(dnsport))
144143

145144

146-
def do_iptables_tproxy(port, dnsport, family, subnets, udp):
145+
def do_iptables_tproxy(port, dnsport, nslist, family, subnets, udp):
147146
if family not in [socket.AF_INET, socket.AF_INET6]:
148147
raise Exception(
149148
'Address family "%s" unsupported by tproxy method'
@@ -194,7 +193,6 @@ def ipt_ttl(*args):
194193
'-m', 'udp', '-p', 'udp')
195194

196195
if dnsport:
197-
nslist = resolvconf_nameservers()
198196
for f, ip in filter(lambda i: i[0] == family, nslist):
199197
ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
200198
'--dest', '%s/32' % ip,
@@ -442,7 +440,6 @@ def do_ipfw(port, dnsport, family, subnets, udp):
442440
IPPROTO_DIVERT)
443441
divertsock.bind(('0.0.0.0', port)) # IP field is ignored
444442

445-
nslist = resolvconf_nameservers()
446443
for f, ip in filter(lambda i: i[0] == family, nslist):
447444
# relabel and then catch outgoing DNS requests
448445
ipfw('add', sport, 'divert', sport,
@@ -483,7 +480,7 @@ def pfctl(args, stdin = None):
483480

484481
_pf_context = {'started_by_sshuttle': False, 'Xtoken':''}
485482

486-
def do_pf(port, dnsport, family, subnets, udp):
483+
def do_pf(port, dnsport, nslist, family, subnets, udp):
487484
global _pf_started_by_sshuttle
488485
tables = []
489486
translating_rules = []
@@ -502,7 +499,6 @@ def do_pf(port, dnsport, family, subnets, udp):
502499
filtering_rules.append('pass out route-to lo0 inet proto tcp to <forward_subnets> keep state')
503500

504501
if dnsport:
505-
nslist = resolvconf_nameservers()
506502
tables.append('table <dns_servers> {%s}' % ','.join([ns[1] for ns in nslist]))
507503
translating_rules.append('rdr pass on lo0 proto udp to <dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport)
508504
filtering_rules.append('pass out route-to lo0 inet proto udp to <dns_servers> port 53 keep state')
@@ -690,7 +686,7 @@ def pf_add_anchor_rule(type, name):
690686
# exit. In case that fails, it's not the end of the world; future runs will
691687
# supercede it in the transproxy list, at least, so the leftover rules
692688
# are hopefully harmless.
693-
def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog):
689+
def main(port_v6, port_v4, dnsport_v6, dnsport_v4, nslist, method, udp, syslog):
694690
assert(port_v6 >= 0)
695691
assert(port_v6 <= 65535)
696692
assert(port_v4 >= 0)
@@ -777,14 +773,14 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog):
777773
subnets_v6 = filter(lambda i: i[0] == socket.AF_INET6, subnets)
778774
if port_v6:
779775
do_wait = do_it(
780-
port_v6, dnsport_v6, socket.AF_INET6, subnets_v6, udp)
776+
port_v6, dnsport_v6, nslist, socket.AF_INET6, subnets_v6, udp)
781777
elif len(subnets_v6) > 0:
782778
debug1("IPv6 subnets defined but IPv6 disabled\n")
783779

784780
subnets_v4 = filter(lambda i: i[0] == socket.AF_INET, subnets)
785781
if port_v4:
786782
do_wait = do_it(
787-
port_v4, dnsport_v4, socket.AF_INET, subnets_v4, udp)
783+
port_v4, dnsport_v4, nslist, socket.AF_INET, subnets_v4, udp)
788784
elif len(subnets_v4) > 0:
789785
debug1('IPv4 subnets defined but IPv4 disabled\n')
790786

@@ -826,7 +822,7 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog):
826822
except:
827823
pass
828824
if port_v6:
829-
do_it(port_v6, 0, socket.AF_INET6, [], udp)
825+
do_it(port_v6, 0, [], socket.AF_INET6, [], udp)
830826
if port_v4:
831-
do_it(port_v4, 0, socket.AF_INET, [], udp)
827+
do_it(port_v4, 0, [], socket.AF_INET, [], udp)
832828
restore_etc_hosts(port_v6 or port_v4)

src/helpers.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,7 @@ def resolvconf_nameservers():
4848
for line in open('/etc/resolv.conf'):
4949
words = line.lower().split()
5050
if len(words) >= 2 and words[0] == 'nameserver':
51-
if ':' in words[1]:
52-
l.append((socket.AF_INET6, words[1]))
53-
else:
54-
l.append((socket.AF_INET, words[1]))
51+
l.append(family_ip_tuple(words[1]))
5552
return l
5653

5754

@@ -82,6 +79,13 @@ def islocal(ip, family):
8279
return True # it's a local IP, or there would have been an error
8380

8481

82+
def family_ip_tuple(ip):
83+
if ':' in ip:
84+
return (socket.AF_INET6, ip)
85+
else:
86+
return (socket.AF_INET, ip)
87+
88+
8589
def family_to_string(family):
8690
if family == socket.AF_INET6:
8791
return "AF_INET6"

src/main.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import server
88
import firewall
99
import hostwatch
10-
from helpers import log, Fatal
10+
from helpers import family_ip_tuple, log, Fatal
1111

1212

1313
# 1.2.3.4/5 or just 1.2.3.4
@@ -105,6 +105,9 @@ def parse_ipport6(s):
105105
(ip, port) = (ip or '::', int(port or 0))
106106
return (ip, port)
107107

108+
def parse_list(list):
109+
return re.split(r'[\s,]+', list.strip()) if list else []
110+
108111

109112
optspec = """
110113
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
@@ -116,6 +119,7 @@ def parse_ipport6(s):
116119
H,auto-hosts scan for remote hostnames and update local /etc/hosts
117120
N,auto-nets automatically determine subnets to route
118121
dns capture local DNS requests and forward to the remote DNS server
122+
ns-hosts= capture and forward remote DNS requests to the following servers
119123
method= auto, nat, tproxy, pf or ipfw
120124
python= path to python interpreter on the remote server
121125
r,remote= ssh hostname (and optional username) of remote sshuttle server
@@ -153,8 +157,10 @@ def parse_ipport6(s):
153157
elif opt.firewall:
154158
if len(extra) != 6:
155159
o.fatal('exactly six arguments expected')
160+
port, dnsport = int(extra[0]), int(extra[1])
161+
nslist = [family_ip_tuple(ns) for ns in parse_list(opt.ns_hosts)]
156162
sys.exit(firewall.main(int(extra[0]), int(extra[1]),
157-
int(extra[2]), int(extra[3]),
163+
int(extra[2]), int(extra[3]), nslist,
158164
extra[4], int(extra[5]), opt.syslog))
159165
elif opt.hostwatch:
160166
sys.exit(hostwatch.hw_main(extra))
@@ -171,6 +177,8 @@ def parse_ipport6(s):
171177
remotename = opt.remote
172178
if remotename == '' or remotename == '-':
173179
remotename = None
180+
#nslist = re.split(r'[\s,]+', opt.dnshosts.strip()) if opt.dnshosts else []
181+
nslist = [family_ip_tuple(ns) for ns in parse_list(opt.ns_hosts)]
174182
if opt.seed_hosts and not opt.auto_hosts:
175183
o.fatal('--seed-hosts only works if you also use -H')
176184
if opt.seed_hosts:
@@ -208,6 +216,7 @@ def parse_ipport6(s):
208216
opt.python,
209217
opt.latency_control,
210218
opt.dns,
219+
opt.ns_hosts,
211220
method,
212221
sh,
213222
opt.auto_nets,

0 commit comments

Comments
 (0)