@@ -27,7 +27,7 @@ import sys
2727
2828import dbus
2929import qubesdb
30- from ipaddress import IPv4Address
30+ from ipaddress import ip_address
3131import os
3232
3333def get_dns_resolv_conf ():
@@ -42,7 +42,7 @@ def get_dns_resolv_conf():
4242 if len (tokens ) < 2 or tokens [0 ] != "nameserver" :
4343 continue
4444 try :
45- nameservers .append (IPv4Address (tokens [1 ]))
45+ nameservers .append (ip_address (tokens [1 ]))
4646 except ValueError :
4747 pass
4848 return nameservers
@@ -74,74 +74,88 @@ def get_dns_resolved():
7474 raise
7575 # Use global entries first
7676 dns .sort (key = lambda x : x [0 ] != 0 )
77- # Only keep IPv4 entries. systemd-resolved is trusted to return valid
78- # addresses.
77+ # systemd-resolved is trusted to return valid addresses.
7978 # ToDo: We only need abridged IPv4 DNS entries for ifindex == 0.
8079 # to ensure static DNS of disconnected network interfaces are not added.
81- return [IPv4Address (bytes (addr )) for ifindex , family , addr in dns
82- if family == 2 ]
80+ return [ip_address (bytes (addr )) for ifindex , family , addr in dns ]
8381
8482def install_firewall_rules (dns ):
8583 qdb = qubesdb .QubesDB ()
86- qubesdb_dns = []
87- for i in ('/qubes-netvm-primary-dns' , '/qubes-netvm-secondary-dns' ):
88- ns_maybe = qdb .read (i )
89- if ns_maybe is None :
90- continue
91- try :
92- qubesdb_dns .append (IPv4Address (ns_maybe .decode ("ascii" , "strict" )))
93- except (UnicodeDecodeError , ValueError ):
94- pass
95- preamble = [
96- 'add table ip qubes' ,
97- # Add the chain so that the subsequent delete will work. If the chain already
98- # exists this is a harmless no-op.
99- 'add chain ip qubes dnat-dns' ,
100- # Delete the chain so that if the chain already exists, it will be removed.
101- # The removal of the old chain and addition of the new one happen as a single
102- # atomic operation, so there is no period where neither chain is present or
103- # where both are present.
104- 'delete chain ip qubes dnat-dns' ,
105- ]
106- rules = [
107- 'table ip qubes {' ,
108- 'chain dnat-dns {' ,
109- 'type nat hook prerouting priority dstnat; policy accept;' ,
110- ]
84+ nft_cmd = []
11185 dns_resolved = get_dns_resolved ()
112- if not dns_resolved :
113- # User has no IPv4 DNS set in sys-net. Maybe IPv6 only environment.
114- # Or maybe user wants to enforce DNS-Over-HTTPS.
115- # Drop IPv4 DNS requests to qubesdb_dns addresses.
116- for vm_nameserver in qubesdb_dns :
117- vm_ns_ = str (vm_nameserver )
118- rules += [
119- f"ip daddr { vm_ns_ } udp dport 53 drop" ,
120- f"ip daddr { vm_ns_ } tcp dport 53 drop" ,
121- ]
122- else :
123- for vm_nameserver , dest in zip (qubesdb_dns , cycle (dns_resolved )):
124- vm_ns_ = str (vm_nameserver )
125- dns_ = str (dest )
126- rules += [
127- f"ip daddr { vm_ns_ } udp dport 53 dnat to { dns_ } " ,
128- f"ip daddr { vm_ns_ } tcp dport 53 dnat to { dns_ } " ,
129- ]
130- rules += ["}" , "}" ]
86+ for family in [4 , 6 ]:
87+ ip46 = '6' if family == 6 else ''
88+ dns_servers = [dns_ip for dns_ip in dns_resolved if dns_ip .version == family ]
89+ qubesdb_dns = []
90+ for i in (f"/qubes-netvm-primary-dns{ ip46 } " , f"/qubes-netvm-secondary-dns{ ip46 } " ):
91+ ns_maybe = qdb .read (i )
92+ if ns_maybe is None :
93+ continue
94+ try :
95+ qubesdb_dns .append (ip_address (ns_maybe .decode ("ascii" , "strict" )))
96+ except (UnicodeDecodeError , ValueError ):
97+ pass
98+ preamble = [
99+ f"add table ip{ ip46 } qubes" ,
100+ # Add the chain so that the subsequent delete will work. If the chain already
101+ # exists this is a harmless no-op.
102+ f"add chain ip{ ip46 } qubes dnat-dns" ,
103+ # Delete the chain so that if the chain already exists, it will be removed.
104+ # The removal of the old chain and addition of the new one happen as a single
105+ # atomic operation, so there is no period where neither chain is present or
106+ # where both are present.
107+ f"delete chain ip{ ip46 } qubes dnat-dns" ,
108+ ]
109+ rules = [
110+ f"table ip{ ip46 } qubes {{" ,
111+ 'chain custom-dnat-dns {}' ,
112+ 'chain dnat-dns {' ,
113+ 'type nat hook prerouting priority dstnat; policy accept;' ,
114+ 'jump custom-dnat-dns' ,
115+ ]
116+ if not dns_servers :
117+ # User has no DNS set in sys-net.
118+ # Or maybe user wants to enforce DNS-Over-HTTPS.
119+ # Drop IPv4 DNS requests to qubesdb_dns addresses.
120+ for vm_nameserver in qubesdb_dns :
121+ vm_ns_ = str (vm_nameserver )
122+ rules += [
123+ f"ip{ ip46 } daddr { vm_ns_ } udp dport 53 drop" ,
124+ f"ip{ ip46 } daddr { vm_ns_ } tcp dport 53 drop" ,
125+ ]
126+ else :
127+ for (vm_nameserver , dest ) in zip (qubesdb_dns , cycle (dns_servers )):
128+ vm_ns_ = str (vm_nameserver )
129+ dns_ = str (dest )
130+ if dest is None or (vm_nameserver == dest and len (qubesdb_dns ) == 0 ):
131+ rules += [
132+ f"ip{ ip46 } daddr { vm_ns_ } tcp dport 53 reject with icmp{ ip46 } type host-unreachable" ,
133+ f"ip{ ip46 } daddr { vm_ns_ } udp dport 53 reject with icmp{ ip46 } type host-unreachable" ,
134+ ]
135+ else :
136+ rules += [
137+ f"ip{ ip46 } daddr { vm_ns_ } udp dport 53 dnat to { dns_ } " ,
138+ f"ip{ ip46 } daddr { vm_ns_ } tcp dport 53 dnat to { dns_ } " ,
139+ ]
140+ rules += ["}" , "}" ]
131141
132- # check if new rules are the same as the old ones - if so, don't reload
133- # and return that info via exit code
134- try :
135- old_rules = subprocess .check_output (
136- ["nft" , "list" , "chain" , "ip" , "qubes" , "dnat-dns" ]).decode ().splitlines ()
137- except subprocess .CalledProcessError :
138- old_rules = []
139- old_rules = [line .strip () for line in old_rules ]
142+ # check if new rules are the same as the old ones - if so, don't reload
143+ # and return that info via exit code
144+ try :
145+ old_rules = subprocess .check_output (
146+ ["nft" , "list" , "chain" , f "ip{ ip46 } " , "qubes" , "dnat-dns" ]).decode ().splitlines ()
147+ except subprocess .CalledProcessError :
148+ old_rules = []
149+ old_rules = [line .strip () for line in old_rules ]
140150
141- if old_rules == rules :
142- sys . exit ( 100 )
151+ if old_rules == rules :
152+ continue
143153
144- os .execvp ("nft" , ("nft" , "--" , "\n " .join (preamble + rules )))
154+ nft_cmd += [preamble , rules ]
155+
156+ if not nft_cmd :
157+ sys .exit (100 )
158+ os .execvp ("nft" , ("nft" , "--" , "\n " .join (nft_cmd )))
145159
146160if __name__ == '__main__' :
147161 install_firewall_rules (get_dns_resolved ())
0 commit comments