Add DNS filtering with dnsmasq address=/ directives for wildcard blocking

Adds Pi-hole-style DNS filtering using dnsmasq's native address=/ directives,
which block domains and all subdomains. Users subscribe to blocklists by URL
or upload files, with suggested lists from Hagezi, Steven Black, and OISD.
Background runner refreshes lists on a configurable interval (default 24h).
This commit is contained in:
2026-07-12 12:44:19 +00:00
parent 134c01fe5e
commit 518cdbbce5
12 changed files with 2009 additions and 2 deletions

View File

@@ -0,0 +1,101 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { dnsFilterApi, type CustomEntry } from '../services/api/dnsfilter';
export function useDnsFilter() {
const queryClient = useQueryClient();
const statusQuery = useQuery({
queryKey: ['dns-filter', 'status'],
queryFn: () => dnsFilterApi.getStatus(),
});
const configQuery = useQuery({
queryKey: ['dns-filter', 'config'],
queryFn: () => dnsFilterApi.getConfig(),
});
const listsQuery = useQuery({
queryKey: ['dns-filter', 'lists'],
queryFn: () => dnsFilterApi.getLists(),
});
const customQuery = useQuery({
queryKey: ['dns-filter', 'custom'],
queryFn: () => dnsFilterApi.getCustomEntries(),
});
const suggestedQuery = useQuery({
queryKey: ['dns-filter', 'suggested'],
queryFn: () => dnsFilterApi.getSuggested(),
staleTime: Infinity,
});
const invalidateAll = () => {
queryClient.invalidateQueries({ queryKey: ['dns-filter'] });
};
const updateConfigMutation = useMutation({
mutationFn: dnsFilterApi.updateConfig,
onSuccess: invalidateAll,
});
const addListMutation = useMutation({
mutationFn: (data: { url: string; name: string }) => dnsFilterApi.addList(data),
onSuccess: invalidateAll,
});
const removeListMutation = useMutation({
mutationFn: (id: string) => dnsFilterApi.removeList(id),
onSuccess: invalidateAll,
});
const toggleListMutation = useMutation({
mutationFn: ({ id, enabled }: { id: string; enabled: boolean }) => dnsFilterApi.toggleList(id, enabled),
onSuccess: invalidateAll,
});
const addCustomMutation = useMutation({
mutationFn: (entry: CustomEntry) => dnsFilterApi.setCustomEntry(entry),
onSuccess: invalidateAll,
});
const removeCustomMutation = useMutation({
mutationFn: (domain: string) => dnsFilterApi.removeCustomEntry(domain),
onSuccess: invalidateAll,
});
const triggerUpdateMutation = useMutation({
mutationFn: () => dnsFilterApi.triggerUpdate(),
onSuccess: () => {
// Delay invalidation to give the runner time to download + compile
setTimeout(invalidateAll, 3000);
},
});
const uploadListMutation = useMutation({
mutationFn: ({ name, file }: { name: string; file: File }) => dnsFilterApi.uploadList(name, file),
onSuccess: invalidateAll,
});
return {
status: statusQuery.data,
isLoadingStatus: statusQuery.isLoading,
config: configQuery.data,
lists: listsQuery.data?.lists ?? [],
customEntries: customQuery.data?.customEntries ?? [],
suggestedLists: suggestedQuery.data?.lists ?? [],
updateConfig: updateConfigMutation.mutateAsync,
isUpdatingConfig: updateConfigMutation.isPending,
addList: addListMutation.mutateAsync,
isAddingList: addListMutation.isPending,
removeList: removeListMutation.mutateAsync,
toggleList: toggleListMutation.mutateAsync,
addCustomEntry: addCustomMutation.mutateAsync,
removeCustomEntry: removeCustomMutation.mutateAsync,
triggerUpdate: triggerUpdateMutation.mutateAsync,
isUpdating: triggerUpdateMutation.isPending,
uploadList: uploadListMutation.mutateAsync,
isUploading: uploadListMutation.isPending,
refetch: invalidateAll,
};
}