@@ -27,7 +27,8 @@ import sys
2727import dbus
2828import qubesdb
2929from typing import List
30- from ipaddress import IPv4Address
30+ from itertools import zip_longest
31+ from ipaddress import ip_address , IPv4Address , IPv6Address
3132import os
3233
3334def get_dns_resolv_conf ():
@@ -42,7 +43,7 @@ def get_dns_resolv_conf():
4243 if len (tokens ) < 2 or tokens [0 ] != "nameserver" :
4344 continue
4445 try :
45- nameservers .append (IPv4Address (tokens [1 ]))
46+ nameservers .append (ip_address (tokens [1 ]))
4647 except ValueError :
4748 pass
4849 return nameservers
@@ -71,16 +72,15 @@ def get_dns_resolved():
7172 raise
7273 # Use global entries first
7374 dns .sort (key = lambda x : x [0 ] != 0 )
74- # Only keep IPv4 entries. systemd-resolved is trusted to return valid
75- # addresses.
75+ # systemd-resolved is trusted to return valid addresses.
7676 # ToDo: We only need abridged IPv4 DNS entries for ifindex == 0.
7777 # to ensure static DNS of disconnected network interfaces are not added.
78- return [IPv4Address (bytes (addr )) for ifindex , family , addr in dns
79- if family == 2 ]
78+ return [ip_address (bytes (addr )) for ifindex , family , addr in dns ]
8079
8180def install_firewall_rules (dns ):
8281 qdb = qubesdb .QubesDB ()
8382 qubesdb_dns = []
83+ qubesdb_dns6 = []
8484 for i in ('/qubes-netvm-primary-dns' , '/qubes-netvm-secondary-dns' ):
8585 ns_maybe = qdb .read (i )
8686 if ns_maybe is None :
@@ -89,6 +89,14 @@ def install_firewall_rules(dns):
8989 qubesdb_dns .append (IPv4Address (ns_maybe .decode ("ascii" , "strict" )))
9090 except (UnicodeDecodeError , ValueError ):
9191 pass
92+ for i in ('/qubes-netvm-primary-dns6' , '/qubes-netvm-secondary-dns6' ):
93+ ns_maybe = qdb .read (i )
94+ if ns_maybe is None :
95+ continue
96+ try :
97+ qubesdb_dns6 .append (IPv6Address (ns_maybe .decode ("ascii" , "strict" )))
98+ except (UnicodeDecodeError , ValueError ):
99+ pass
92100 preamble = [
93101 'add table ip qubes' ,
94102 # Add the chain so that the subsequent delete will work. If the chain already
@@ -102,8 +110,10 @@ def install_firewall_rules(dns):
102110 ]
103111 rules = [
104112 'table ip qubes {' ,
113+ 'chain custom-dnat-dns {}' ,
105114 'chain dnat-dns {' ,
106115 'type nat hook prerouting priority dstnat; policy accept;' ,
116+ 'jump custom-dnat-dns' ,
107117 ]
108118 dns_resolved = get_dns_resolved ()
109119 if not dns_resolved :
@@ -119,12 +129,22 @@ def install_firewall_rules(dns):
119129 while len (qubesdb_dns ) > len (dns_resolved ):
120130 # Ensure that upstream DNS pool is larger than qubesdb_dns pool
121131 dns_resolved = dns_resolved + dns_resolved
122- for vm_nameserver , dest in zip (qubesdb_dns , dns_resolved ):
132+ for i , (vm_nameserver , dest ) in enumerate (zip_longest (qubesdb_dns ,
133+ [dns_ip for dns_ip in dns if type (dns_ip ) is IPv4Address ])):
134+ if i >= len (qubesdb_dns ):
135+ break
123136 dns_ = str (dest )
124- rules += [
125- f"ip daddr { vm_nameserver } udp dport 53 dnat to { dns_ } " ,
126- f"ip daddr { vm_nameserver } tcp dport 53 dnat to { dns_ } " ,
127- ]
137+ if dest is None or (vm_nameserver == dest and
138+ qdb .read ('/qubes-ip' ) is None ):
139+ rules += [
140+ f"ip daddr { vm_nameserver } tcp dport 53 reject with icmp type host-unreachable" ,
141+ f"ip daddr { vm_nameserver } udp dport 53 reject with icmp type host-unreachable" ,
142+ ]
143+ else :
144+ rules += [
145+ f"ip daddr { vm_nameserver } udp dport 53 dnat to { dns_ } " ,
146+ f"ip daddr { vm_nameserver } tcp dport 53 dnat to { dns_ } " ,
147+ ]
128148 rules += ["}" , "}" ]
129149
130150 # check if new rules are the same as the old ones - if so, don't reload
@@ -139,7 +159,41 @@ def install_firewall_rules(dns):
139159 if old_rules == rules :
140160 sys .exit (100 )
141161
142- os .execvp ("nft" , ("nft" , "--" , "\n " .join (preamble + rules )))
162+ # ipv6
163+ res = [
164+ 'add table ip6 qubes' ,
165+ # Add the chain so that the subsequent delete will work. If the chain already
166+ # exists this is a harmless no-op.
167+ 'add chain ip6 qubes dnat-dns' ,
168+ # Delete the chain so that if the chain already exists, it will be removed.
169+ # The removal of the old chain and addition of the new one happen as a single
170+ # atomic operation, so there is no period where neither chain is present or
171+ # where both are present.
172+ 'delete chain ip6 qubes dnat-dns' ,
173+ 'table ip6 qubes {' ,
174+ 'chain custom-dnat-dns {}' ,
175+ 'chain dnat-dns {' ,
176+ 'type nat hook prerouting priority dstnat; policy accept;' ,
177+ 'jump custom-dnat-dns' ,
178+ ]
179+ for i , (vm_nameserver , dest ) in enumerate (zip_longest (qubesdb_dns6 ,
180+ [dns_ip for dns_ip in dns if type (dns_ip ) is IPv6Address ])):
181+ if i >= len (qubesdb_dns6 ):
182+ break
183+ dns_ = str (dest )
184+ if dest is None or (vm_nameserver == dest and
185+ qdb .read ('/qubes-primary-dns6' ) is None ):
186+ res += [
187+ f"ip6 daddr { vm_nameserver } tcp dport 53 reject with icmpv6 type addr-unreachable" ,
188+ f"ip6 daddr { vm_nameserver } udp dport 53 reject with icmpv6 type addr-unreachable" ,
189+ ]
190+ else :
191+ res += [
192+ f"ip6 daddr { vm_nameserver } udp dport 53 dnat to { dns_ } " ,
193+ f"ip6 daddr { vm_nameserver } tcp dport 53 dnat to { dns_ } " ,
194+ ]
195+ res += ["}\n }\n " ]
196+ os .execvp ("nft" , ("nft" , "--" , "\n " .join (preamble + rules + res )))
143197
144198if __name__ == '__main__' :
145199 install_firewall_rules (get_dns_resolved ())
0 commit comments