Merge branch 'development-v6' into fix/overTimeGraphs

This commit is contained in:
DL6ER 2024-03-16 09:05:00 +01:00
commit 66653ae0d5
No known key found for this signature in database
GPG Key ID: 00135ACBD90B28DD
85 changed files with 3581 additions and 1511 deletions

View File

@ -0,0 +1,126 @@
name: Build and test
description: Builds and tests FTL on all supported platforms
inputs:
platform:
required: true
description: The platform to build for
git_branch:
required: true
description: The branch to build from
git_tag:
required: true
description: The tag to build from (if any)
bin_name:
required: true
description: The name of the binary to build
artifact_name:
required: true
description: The name of the artifact to upload
event_name:
required: true
description: The name of the event that triggered the workflow run
actor:
required: true
description: The name of the user or app that initiated the workflow run
target_dir:
required: true
description: The directory to deploy the artifacts to
# Secrets cannot be accessed in the action.yml file so we need to pass them as
# inputs to the action.
SSH_KEY:
required: true
description: The SSH private key to use for authentication
KNOWN_HOSTS:
required: true
description: The SSH known hosts file
SSH_USER:
required: true
description: The SSH user to use for authentication
SSH_HOST:
required: true
description: The SSH host to connect to
# Both the definition of environment variables and checking out the code
# needs to be done outside of the composite action as
# - environment variables cannot be defined using inputs
# - the checkout action needs to be the first step in the workflow, otherwise we
# cannot use the composite action as the corresponding "action.yml" isn't
# there yet
runs:
using: "composite"
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.1.0
-
name: Print directory contents
shell: bash
run: ls -l
-
name: Build and test FTL in ftl-build container (QEMU)
uses: Wandalen/wretry.action@v1.4.8
with:
attempt_limit: 3
action: docker/build-push-action@v5.0.0
with: |
platforms: ${{ inputs.platform }}
pull: true
push: false
context: .
target: result
file: .github/Dockerfile
outputs: |
type=tar,dest=build.tar
build-args: |
"CI_ARCH=${{ inputs.platform }}"
"GIT_BRANCH=${{ inputs.git_branch }}"
"GIT_TAG=${{ inputs.git_tag }}"
-
name: List files in current directory
shell: bash
run: ls -l
-
name: Extract FTL binary from container
shell: bash
run: |
tar -xf build.tar pihole-FTL
-
name: "Generate checksum file"
shell: bash
run: |
mv pihole-FTL "${{ inputs.bin_name }}"
sha1sum pihole-FTL-* > ${{ inputs.bin_name }}.sha1
-
name: Store binary artifacts for later deployoment
if: inputs.event_name != 'pull_request'
uses: actions/upload-artifact@v4.3.1
with:
name: ${{ inputs.artifact_name }}
path: '${{ inputs.bin_name }}*'
-
name: Extract documentation files from container
if: inputs.event_name != 'pull_request' && inputs.platform == 'linux/amd64'
shell: bash
run: |
tar -xf build.tar api-docs.tar.gz
-
name: Upload documentation artifacts for deployoment
if: inputs.event_name != 'pull_request' && inputs.platform == 'linux/amd64'
uses: actions/upload-artifact@v4.3.1
with:
name: pihole-api-docs
path: 'api-docs.tar.gz'
-
name: Deploy
if: inputs.event_name != 'pull_request'
uses: ./.github/actions/deploy
with:
pattern: ${{ inputs.bin_name }}-binary
target_dir: ${{ inputs.target_dir }}
event_name: ${{ inputs.event_name }}
actor: ${{ inputs.actor }}
SSH_KEY: ${{ inputs.SSH_KEY }}
KNOWN_HOSTS: ${{ inputs.KNOWN_HOSTS }}
SSH_USER: ${{ inputs.SSH_USER }}
SSH_HOST: ${{ inputs.SSH_HOST }}

96
.github/actions/deploy/action.yml vendored Normal file
View File

@ -0,0 +1,96 @@
name: Deploy
description: Deploy the FTL binary and documentation
inputs:
pattern:
required: true
description: The pattern to match the artifacts to download
target_dir:
required: true
description: The directory to deploy the artifacts to
event_name:
required: true
description: The name of the event that triggered the workflow run
actor:
required: true
description: The name of the user or app that initiated the workflow run
# Secrets cannot be accessed in the action.yml file so we need to pass them as
# inputs to the action.
SSH_KEY:
required: true
description: The SSH private key to use for authentication
KNOWN_HOSTS:
required: true
description: The SSH known hosts file
SSH_USER:
required: true
description: The SSH user to use for authentication
SSH_HOST:
required: true
description: The SSH host to connect to
runs:
using: "composite"
steps:
-
name: Get binaries built in previous jobs
uses: actions/download-artifact@v4.1.4
id: download
with:
path: ftl_builds/
pattern: ${{ inputs.pattern }}
merge-multiple: true
-
name: Get documentation files built in previous jobs
if: inputs.pattern == 'pihole-FTL-amd64-binary'
uses: actions/download-artifact@v4.1.4
with:
path: ftl_builds/
name: pihole-api-docs
-
name: Display structure of downloaded files
shell: bash
run: ls -R
working-directory: ${{steps.download.outputs.download-path}}
-
name: Install SSH Key
uses: benoitchantre/setup-ssh-authentication-action@1.0.1
with:
private-key: ${{ inputs.SSH_KEY }}
private-key-name: id_rsa
known-hosts: ${{ inputs.KNOWN_HOSTS }}
-
name: Set private key permissions
shell: bash
run: chmod 600 ~/.ssh/id_rsa
-
name: Untar documentation files
if: inputs.pattern == 'pihole-FTL-amd64-binary'
working-directory: ftl_builds/
shell: bash
run: |
mkdir docs/
tar xzvf api-docs.tar.gz -C docs/
-
name: Display structure of files ready for upload
working-directory: ftl_builds/
shell: bash
run: ls -R
-
name: Transfer Builds to Pi-hole server for pihole checkout
if: inputs.actor != 'dependabot[bot]'
env:
USER: ${{ inputs.SSH_USER }}
HOST: ${{ inputs.SSH_HOST }}
TARGET_DIR: ${{ inputs.target_dir }}
SOURCE_DIR: ftl_builds/
shell: bash
run: |
bash ./deploy.sh
-
name: Attach binaries to release
if: inputs.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: |
ftl_builds/*

View File

@ -13,3 +13,27 @@ updates:
pull-request-branch-name:
# Separate sections of the branch name with a hyphen
separator: "-"
groups:
github_action-dependencies:
patterns:
- "*"
# As above, but for development-v6
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
day: saturday
time: "10:00"
open-pull-requests-limit: 10
target-branch: development-v6
reviewers:
- "pi-hole/ftl-maintainers"
pull-request-branch-name:
# Separate sections of the branch name with a hyphen
separator: "-"
groups:
github_action-dependencies:
patterns:
- "*"

View File

@ -50,7 +50,7 @@ jobs:
[[ $FAIL == 1 ]] && exit 1 || echo "Branch name depth check passed."
shell: bash
build:
gha:
runs-on: ubuntu-latest
needs: smoke-tests
strategy:
@ -61,132 +61,67 @@ jobs:
bin_name: pihole-FTL-amd64
- platform: linux/386
bin_name: pihole-FTL-386
- platform: linux/riscv64
bin_name: pihole-FTL-riscv64
env:
CI_ARCH: ${{ matrix.platform }}
GIT_BRANCH: ${{ needs.smoke-tests.outputs.GIT_BRANCH }}
GIT_TAG: ${{ needs.smoke-tests.outputs.GIT_TAG }}
steps:
-
name: Checkout code
uses: actions/checkout@v4.1.1
-
name: Build and test and deploy FTL
uses: ./.github/actions/build-and-test
with:
platform: ${{ matrix.platform }}
bin_name: ${{ matrix.bin_name }}
artifact_name: ${{ matrix.bin_name }}-binary
target_dir: ${{ needs.smoke-tests.outputs.OUTPUT_DIR }}
git_branch: ${{ needs.smoke-tests.outputs.GIT_BRANCH }}
git_tag: ${{ needs.smoke-tests.outputs.GIT_TAG }}
event_name: ${{ github.event_name }}
actor: ${{ github.actor }}
SSH_KEY: ${{ secrets.SSH_KEY }}
KNOWN_HOSTS: ${{ secrets.KNOWN_HOSTS }}
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ secrets.SSH_HOST }}
self-hosted:
runs-on: self-hosted
needs: smoke-tests
strategy:
fail-fast: false
matrix:
include:
- platform: linux/arm/v6
bin_name: pihole-FTL-armv6
- platform: linux/arm/v7
bin_name: pihole-FTL-armv7
- platform: linux/arm64/v8
bin_name: pihole-FTL-arm64
- platform: linux/riscv64
bin_name: pihole-FTL-riscv64
env:
CI_ARCH: ${{ matrix.platform }}
GIT_BRANCH: ${{ needs.smoke-tests.outputs.GIT_BRANCH }}
GIT_TAG: ${{ needs.smoke-tests.outputs.GIT_TAG }}
steps:
-
name: Checkout code
uses: actions/checkout@v4.1.1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
-
name: Print directory contents
run: ls -l
-
name: Build and test FTL in ftl-build container (QEMU)
uses: Wandalen/wretry.action@v1.4.4
name: Build and test and deploy FTL
uses: ./.github/actions/build-and-test
with:
attempt_limit: 3
action: docker/build-push-action@v5.0.0
with: |
platforms: ${{ matrix.platform }}
pull: true
push: false
context: .
target: result
file: .github/Dockerfile
outputs: |
type=tar,dest=build.tar
build-args: |
"CI_ARCH=${{ matrix.platform }}"
"GIT_BRANCH=${{ needs.smoke-tests.outputs.GIT_BRANCH }}"
"GIT_TAG=${{ needs.smoke-tests.outputs.GIT_TAG }}"
-
name: List files in current directory
run: ls -l
-
name: Extract FTL binary from container
run: |
tar -xf build.tar pihole-FTL
-
name: "Generate checksum file"
run: |
mv pihole-FTL "${{ matrix.bin_name }}"
sha1sum pihole-FTL-* > ${{ matrix.bin_name }}.sha1
-
name: Store binary artifacts for later deployoment
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4.3.1
with:
name: ${{ matrix.bin_name }}-binary
path: '${{ matrix.bin_name }}*'
-
name: Extract documentation files from container
if: github.event_name != 'pull_request' && matrix.platform == 'linux/amd64'
run: |
tar -xf build.tar api-docs.tar.gz
-
name: Upload documentation artifacts for deployoment
if: github.event_name != 'pull_request' && matrix.platform == 'linux/amd64'
uses: actions/upload-artifact@v4.3.1
with:
name: pihole-api-docs
path: 'api-docs.tar.gz'
deploy:
if: github.event_name != 'pull_request'
needs: [smoke-tests, build]
runs-on: ubuntu-latest
steps:
-
name: Checkout code
uses: actions/checkout@v4.1.1
-
name: Get Binaries and documentation built in previous jobs
uses: actions/download-artifact@v4.1.2
id: download
with:
path: ftl_builds/
pattern: pihole-*
merge-multiple: true
-
name: Display structure of downloaded files
run: ls -R
working-directory: ${{steps.download.outputs.download-path}}
-
name: Install SSH Key
uses: benoitchantre/setup-ssh-authentication-action@1.0.1
with:
private-key: ${{ secrets.SSH_KEY }}
known-hosts: ${{ secrets.KNOWN_HOSTS }}
-
name: Untar documentation files
working-directory: ftl_builds/
run: |
mkdir docs/
tar xzvf api-docs.tar.gz -C docs/
-
name: Display structure of files ready for upload
run: ls -R
working-directory: ftl_builds/
-
name: Transfer Builds to Pi-hole server for pihole checkout
if: github.actor != 'dependabot[bot]'
env:
USER: ${{ secrets.SSH_USER }}
HOST: ${{ secrets.SSH_HOST }}
TARGET_DIR: ${{ needs.smoke-tests.outputs.OUTPUT_DIR }}
SOURCE_DIR: ftl_builds/
run: |
bash ./deploy.sh
-
name: Attach binaries to release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v1
with:
files: |
ftl_builds/*
platform: ${{ matrix.platform }}
bin_name: ${{ matrix.bin_name }}
artifact_name: ${{ matrix.bin_name }}-binary
target_dir: ${{ needs.smoke-tests.outputs.OUTPUT_DIR }}
git_branch: ${{ needs.smoke-tests.outputs.GIT_BRANCH }}
git_tag: ${{ needs.smoke-tests.outputs.GIT_TAG }}
event_name: ${{ github.event_name }}
actor: ${{ github.actor }}
SSH_KEY: ${{ secrets.SSH_KEY }}
KNOWN_HOSTS: ${{ secrets.KNOWN_HOSTS }}
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ secrets.SSH_HOST }}

View File

@ -23,14 +23,17 @@ jobs:
days-before-stale: 30
days-before-close: 5
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Please comment or update this issue or it will be closed in 5 days.'
stale-issue-label: $stale_label
stale-issue-label: '${{ env.stale_label }}'
exempt-issue-labels: 'Fixed in next release, Bug, Bug:Confirmed, Bugfix in progress, documentation needed, internal'
exempt-all-issue-assignees: true
operations-per-run: 300
close-issue-reason: 'not_planned'
remove_stale: # trigger "stale" removal immediately when stale issues are commented on
if: github.event_name == 'issue_comment'
remove_stale:
# trigger "stale" removal immediately when stale issues are commented on
# we need to explicitly check that the trigger does not run on comment on a PR as
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment-on-issues-only-or-pull-requests-only
if: ${{ !github.event.issue.pull_request && github.event_name != 'schedule' }}
permissions:
contents: read # for actions/checkout
issues: write # to edit issues label
@ -39,7 +42,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4.1.1
- name: Remove 'stale' label
run: gh issue edit ${{ github.event.issue.number }} --remove-label $stale_label
run: gh issue edit ${{ github.event.issue.number }} --remove-label ${{ env.stale_label }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -12,6 +12,6 @@ cmake_minimum_required(VERSION 2.8.12)
project(PIHOLE_FTL C)
set(DNSMASQ_VERSION pi-hole-v2.89-e1de9c2)
set(DNSMASQ_VERSION pi-hole-v2.90+1)
add_subdirectory(src)

View File

@ -766,6 +766,17 @@ static int api_config_patch(struct ftl_conn *api)
// If we reach this point, a valid setting was found and changed
// Validate new value (if validation function is defined)
char errbuf[VALIDATOR_ERRBUF_LEN] = { 0 };
if(!conf_item->c(&new_item->v, new_item->k, errbuf))
{
free_config(&newconf);
return send_json_error(api, 400,
"bad_request",
"Config item validation failed",
errbuf);
}
// Check if this item requires a config-rewrite + restart of dnsmasq
if(conf_item->f & FLAG_RESTART_FTL)
dnsmasq_changed = true;
@ -949,6 +960,21 @@ static int api_config_put_delete(struct ftl_conn *api)
}
// If we reach this point, a valid setting was found and changed
// Validate new value on PUT (if validation function is defined)
if(api->method == HTTP_PUT)
{
char errbuf[VALIDATOR_ERRBUF_LEN] = { 0 };
if(!new_item->c(&new_item->v, new_item->k, errbuf))
{
free_config(&newconf);
return send_json_error(api, 400,
"bad_request",
"Invalid value",
errbuf);
}
}
// Check if this item requires a config-rewrite + restart of dnsmasq
if(new_item->f & FLAG_RESTART_FTL)
dnsmasq_changed = true;

View File

@ -244,7 +244,10 @@ components:
- Authentication
operationId: "add_app"
description: |
Create a new application password. The generated password is shown only once and cannot be retrieved later so make sure to store it in a safe place. The application password can be used to authenticate against the API instead of the regular password. It does not require 2FA verification. Generating a new application password will invalidate all currently active sessions.
Create a new application password. The generated password is shown only once and cannot be retrieved later - make sure to store it in a safe place. The application password can be used to authenticate against the API instead of the regular password.
It does not require 2FA verification. Generating a new application password will invalidate all currently active sessions.
Note that this endpoint only generates an application password accompanied by its hash. To make this new password effective, the returned `hash` has to be set as `webserver.api.app_password` in the Pi-hole configuration in a follow-up step. This can be done in various ways, e.g. via the API (`PATCH /api/config/webserver/api/app_pwhash`), the graphical web interface (Settings -> All Settings) or by editing the configuration file directly.
responses:
'200':
description: OK

View File

@ -344,6 +344,8 @@ components:
type: integer
DBinterval:
type: integer
useWAL:
type: boolean
network:
type: object
properties:
@ -663,6 +665,7 @@ components:
DBexport: true
maxDBdays: 365
DBinterval: 60
useWAL: true
network:
parseARPcache: true
expire: 365

View File

@ -752,6 +752,115 @@ components:
type: boolean
description: Whether or not FTL is allowed to perform destructive actions
example: true
dnsmasq:
type: object
description: Metrics from the embedded dnsmasq resolver
properties:
dns_cache_inserted:
type: integer
description: Number of inserted entries in DNS cache
example: 8
dns_cache_live_freed:
type: integer
description: Number of freed live entries in DNS cache
example: 0
dns_queries_forwarded:
type: integer
description: Number of forwarded DNS queries
example: 2
dns_auth_answered:
type: integer
description: Number of DNS queries for authoritative zones
example: 0
dns_local_answered:
type: integer
description: Number of DNS queries answered from local cache
example: 74
dns_stale_answered:
type: integer
description: Number of DNS queries answered from local cache (stale entries)
example: 0
dns_unanswered:
type: integer
description: Number of unanswered DNS queries
example: 0
bootp:
type: integer
description: Number of BOOTP requests
example: 0
pxe:
type: integer
description: Number of PXE requests
example: 0
dhcp_ack:
type: integer
description: Number of DHCP ACK
example: 0
dhcp_decline:
type: integer
description: Number of DHCP DECLINE
example: 0
dhcp_discover:
type: integer
description: Number of DHCP DISCOVER
example: 0
dhcp_inform:
type: integer
description: Number of DHCP INFORM
example: 0
dhcp_nak:
type: integer
description: Number of DHCP NAK
example: 0
dhcp_offer:
type: integer
description: Number of DHCP OFFER
example: 0
dhcp_release:
type: integer
description: Number of DHCP RELEASE
example: 0
dhcp_request:
type: integer
description: Number of DHCP REQUEST
example: 0
noanswer:
type: integer
description: Number of DHCP requests without answer (rapid commit)
example: 0
leases_allocated_4:
type: integer
description: Number of allocated IPv4 leases
example: 0
leases_pruned_4:
type: integer
description: Number of pruned IPv4 leases
example: 0
leases_allocated_6:
type: integer
description: Number of allocated IPv6 leases
example: 0
leases_pruned_6:
type: integer
description: Number of pruned IPv6 leases
example: 0
tcp_connections:
type: integer
description: Number of dedicated TCP workers
example: 0
dnssec_max_crypto_use:
type: integer
description: DNSSEC per-query crypto work HWM
example: 0
dnssec_max_sig_fail:
type: integer
description: DNSSEC per-RRSet signature fails HWM
example: 0
dnssec_max_work:
type: integer
description: DNSSEC per-query subqueries HWM
example: 0
database:
type: object
properties:

View File

@ -5,6 +5,7 @@ components:
summary: Modify list
parameters:
- $ref: 'lists.yaml#/components/parameters/list'
- $ref: 'lists.yaml#/components/parameters/listtype'
get:
summary: Get lists
tags:
@ -449,3 +450,14 @@ components:
required: true
description: Address of the list
example: https://hosts-file.net/ad_servers.txt
listtype:
in: query
name: type
schema:
type: string
enum:
- "allow"
- "block"
required: false
description: Type of list, optional
example: block

View File

@ -368,7 +368,7 @@ static int get_hwmon_sensors(struct ftl_conn *api, cJSON *sensors)
if(hwmon_dir == NULL)
{
// Nothing to read here, leave array empty
log_warn("Cannot open %s: %s", dirname, strerror(errno));
log_debug(DEBUG_API, "Cannot open %s: %s", dirname, strerror(errno));
return 0;
}
@ -590,6 +590,11 @@ static int get_ftl_obj(struct ftl_conn *api, cJSON *ftl)
JSON_ADD_BOOL_TO_OBJECT(ftl, "allow_destructive", config.webserver.api.allow_destructive.v.b);
// dnsmasq struct
cJSON *dnsmasq = JSON_NEW_OBJECT();
get_dnsmasq_metrics_obj(dnsmasq);
JSON_ADD_ITEM_TO_OBJECT(ftl, "dnsmasq", dnsmasq);
// All okay
return 0;
}

View File

@ -36,7 +36,7 @@ static int api_list_read(struct ftl_conn *api,
sql_msg);
}
tablerow table;
tablerow table = { 0 };
cJSON *rows = JSON_NEW_ARRAY();
while(gravityDB_readTableGetRow(listtype, &table, &sql_msg))
{
@ -48,7 +48,9 @@ static int api_list_read(struct ftl_conn *api,
JSON_COPY_STR_TO_OBJECT(row, "name", table.name);
JSON_COPY_STR_TO_OBJECT(row, "comment", table.comment);
}
else if(listtype == GRAVITY_ADLISTS)
else if(listtype == GRAVITY_ADLISTS ||
listtype == GRAVITY_ADLISTS_BLOCK ||
listtype == GRAVITY_ADLISTS_ALLOW)
{
JSON_COPY_STR_TO_OBJECT(row, "address", table.address);
JSON_COPY_STR_TO_OBJECT(row, "comment", table.comment);
@ -126,7 +128,9 @@ static int api_list_read(struct ftl_conn *api,
JSON_ADD_NUMBER_TO_OBJECT(row, "date_modified", table.date_modified);
// Properties added in https://github.com/pi-hole/pi-hole/pull/3951
if(listtype == GRAVITY_ADLISTS)
if(listtype == GRAVITY_ADLISTS ||
listtype == GRAVITY_ADLISTS_BLOCK ||
listtype == GRAVITY_ADLISTS_ALLOW)
{
JSON_REF_STR_IN_OBJECT(row, "type", table.type);
JSON_ADD_NUMBER_TO_OBJECT(row, "date_updated", table.date_updated);
@ -147,7 +151,9 @@ static int api_list_read(struct ftl_conn *api,
cJSON *json = JSON_NEW_OBJECT();
if(listtype == GRAVITY_GROUPS)
objname = "groups";
else if(listtype == GRAVITY_ADLISTS)
else if(listtype == GRAVITY_ADLISTS ||
listtype == GRAVITY_ADLISTS_BLOCK ||
listtype == GRAVITY_ADLISTS_ALLOW)
objname = "lists";
else if(listtype == GRAVITY_CLIENTS)
objname = "clients";
@ -268,6 +274,8 @@ static int api_list_write(struct ftl_conn *api,
}
case GRAVITY_ADLISTS:
case GRAVITY_ADLISTS_BLOCK:
case GRAVITY_ADLISTS_ALLOW:
{
cJSON *json_address = cJSON_GetObjectItemCaseSensitive(api->payload.json, "address");
if(cJSON_IsString(json_address) && strlen(json_address->valuestring) > 0)
@ -331,6 +339,10 @@ static int api_list_write(struct ftl_conn *api,
NULL);
}
}
else if(listtype == GRAVITY_ADLISTS_BLOCK)
row.type_int = ADLIST_BLOCK;
else if(listtype == GRAVITY_ADLISTS_ALLOW)
row.type_int = ADLIST_ALLOW;
else
{
cJSON *json_type = cJSON_GetObjectItemCaseSensitive(api->payload.json, "type");
@ -552,7 +564,9 @@ static int api_list_remove(struct ftl_conn *api,
if(listtype == GRAVITY_DOMAINLIST_ALLOW_EXACT ||
listtype == GRAVITY_DOMAINLIST_DENY_EXACT ||
listtype == GRAVITY_DOMAINLIST_ALLOW_REGEX ||
listtype == GRAVITY_DOMAINLIST_DENY_REGEX)
listtype == GRAVITY_DOMAINLIST_DENY_REGEX ||
listtype == GRAVITY_ADLISTS_BLOCK ||
listtype == GRAVITY_ADLISTS_ALLOW)
{
int type = -1;
switch (listtype)
@ -568,12 +582,17 @@ static int api_list_remove(struct ftl_conn *api,
break;
case GRAVITY_DOMAINLIST_DENY_REGEX:
type = 3;
break;
case GRAVITY_ADLISTS_BLOCK:
type = ADLIST_BLOCK;
break;
case GRAVITY_ADLISTS_ALLOW:
type = ADLIST_ALLOW;
break;
// Not handled herein
case GRAVITY_GROUPS:
case GRAVITY_ADLISTS:
case GRAVITY_CLIENTS:
// No type required for these tables
break;
// Aggregate types cannot be handled by this routine
case GRAVITY_GRAVITY:
case GRAVITY_ANTIGRAVITY:
case GRAVITY_DOMAINLIST_ALLOW_ALL:
@ -582,7 +601,7 @@ static int api_list_remove(struct ftl_conn *api,
case GRAVITY_DOMAINLIST_ALL_REGEX:
case GRAVITY_DOMAINLIST_ALL_ALL:
default:
return false;
break;
}
// Create new JSON array with the item and type:
@ -827,6 +846,30 @@ int api_list(struct ftl_conn *api)
api->request->local_uri_raw);
}
// If this is a request for a list, we check if there is a request
// parameter narrowing down which kind of list. If so, we modify the
// list type accordingly
if(listtype == GRAVITY_ADLISTS && api->request->query_string != NULL)
{
// Check if there is a type parameter
char typestr[16] = { 0 };
if(get_string_var(api->request->query_string, "type", typestr, sizeof(typestr)) > 0)
{
if(strcasecmp(typestr, "allow") == 0)
listtype = GRAVITY_ADLISTS_ALLOW;
else if(strcasecmp(typestr, "block") == 0)
listtype = GRAVITY_ADLISTS_BLOCK;
else
{
// Invalid type parameter
return send_json_error(api, 400,
"bad_request",
"Invalid request: Invalid type parameter (should be either \"allow\" or \"block\")",
api->request->query_string);
}
}
}
if(api->method == HTTP_GET)
{
// Read list item identified by URI (or read them all)

View File

@ -293,6 +293,8 @@ int api_queries(struct ftl_conn *api)
char sort_dir[16] = { 0 };
char sort_col[16] = { 0 };
char search[2][512] = { { 0 }, { 0 } };
// We start with the most recent query at the beginning (until the cursor is changed)
unsigned long cursor, largest_db_index, mem_dbnum, disk_dbnum;
db_counts(&largest_db_index, &mem_dbnum, &disk_dbnum);
@ -435,6 +437,72 @@ int api_queries(struct ftl_conn *api)
log_warn("Sorting by column %d (%s) requested, but column name not found",
sort_column, sort_dir);
}
// Column searching?
// ID 3 = domain, ID 4 = client, every other combination is requested
for(unsigned int j = 0; j < 2; j++)
{
// Encoded URI string: %5B = [ and %5D = ]
// columns[X][search][value] is the search string for column X
char search_col[] = "columns%5BX%5D%5Bsearch%5D%5Bvalue%5D";
search_col[10] = '3' + j;
if(GET_STR(search_col, search[j], api->request->query_string) > 0)
{
// columns[X][data] is the name of column X
char search_col_id[] = "columns%5BX%5D%5Bdata%5D";
search_col_id[10] = '3' + j;
// Encoded URI string: %5B = [ and %5D = ]
char search_col_id_str[32] = { 0 };
if(GET_VAR(search_col_id, search_col_id_str, api->request->query_string) > 0)
{
size_t searchlen = min(strlen(search[j]), sizeof(search[j]) - 2);
// Replace "*" by SQLite3 wildcard character "%"
for(unsigned int i = 0; i < searchlen; i++)
{
if(search[j][i] == '*')
search[j][i] = '%';
}
// Add % at the end of the search string to
// make it a wildcard if there is none
if(search[j][searchlen - 1] != '%')
{
search[j][searchlen] = '%';
search[j][searchlen + 1] = '\0';
searchlen++;
}
// Add % at the beginning of the search
// string to make it a wildcard if there
// is none
if(search[j][0] != '%')
{
memmove(search[j] + 1, search[j], searchlen + 1);
search[j][0] = '%';
}
// Apply the search string to the query if this is an allowed column
if(j == 0 && strcasecmp(search_col_id_str, "domain") == 0)
{
log_debug(DEBUG_API, "Searching column domain: \"%s\"", search[j]);
add_querystr_string(api, querystr, "d.domain LIKE", ":domain_search", &where);
}
else if(j == 1 && (strcasecmp(search_col_id_str, "client.ip") == 0 || strcasecmp(search_col_id_str, "client") == 0))
{
log_debug(DEBUG_API, "Searching column client: \"%s\"", search[j]);
// We search both client IP and name
add_querystr_string(api, querystr, "c.ip LIKE :client_search OR c.name LIKE", ":client_search", &where);
}
else
log_warn("Column %u with name \"%s\" is not searchable (allowed: 3 = domain, 4 = client)",
3 + j, search_col_id_str);
}
else
log_warn("Column %u is not searchable (allowed: 3 = domain, 4 = client)", 3 + j);
}
}
}
// We use this boolean to memorize if we are filtering at all. It is used
@ -719,6 +787,36 @@ int api_queries(struct ftl_conn *api)
sqlite3_errstr(rc));
}
}
idx = sqlite3_bind_parameter_index(read_stmt, ":domain_search");
if(idx > 0)
{
log_debug(DEBUG_API, "adding :domain_search = \"%s\" to query", search[0]);
filtering = true;
if((rc = sqlite3_bind_text(read_stmt, idx, search[0], -1, SQLITE_STATIC)) != SQLITE_OK)
{
sqlite3_reset(read_stmt);
sqlite3_finalize(read_stmt);
return send_json_error(api, 500,
"internal_error",
"Internal server error, failed to bind domain_search to SQL query",
sqlite3_errstr(rc));
}
}
idx = sqlite3_bind_parameter_index(read_stmt, ":client_search");
if(idx > 0)
{
log_debug(DEBUG_API, "adding :client_search = \"%s\" to query", search[1]);
filtering = true;
if((rc = sqlite3_bind_text(read_stmt, idx, search[1], -1, SQLITE_STATIC)) != SQLITE_OK)
{
sqlite3_reset(read_stmt);
sqlite3_finalize(read_stmt);
return send_json_error(api, 500,
"internal_error",
"Internal server error, failed to bind client_search to SQL query",
sqlite3_errstr(rc));
}
}
}
// Debug logging
@ -1036,4 +1134,4 @@ bool compile_filter_regex(struct ftl_conn *api, const char *path, cJSON *json, r
// We are filtering, so we have to continue to step over the
// remaining rows to get the correct number of total records
return true;
}
}

View File

@ -33,6 +33,8 @@ set(sources
toml_reader.h
toml_helper.c
toml_helper.h
validator.c
validator.h
)
add_library(config OBJECT ${sources})

View File

@ -455,6 +455,18 @@ int set_config_from_CLI(const char *key, const char *value)
{
// Config item changed
// Validate new value(if validation function is defined)
if(new_item->c != NULL)
{
char errbuf[VALIDATOR_ERRBUF_LEN] = { 0 };
if(!new_item->c(&new_item->v, new_item->k, errbuf))
{
free_config(&newconf);
log_err("Invalid value: %s", errbuf);
return 3;
}
}
// Is this a dnsmasq option we need to check?
if(conf_item->f & FLAG_RESTART_FTL)
{

View File

@ -29,6 +29,8 @@
#include "api/api.h"
// exit_code
#include "signals.h"
// validation functions
#include "config/validator.h"
// getEnvVars()
#include "config/env.h"
// sha256sum()
@ -383,47 +385,54 @@ void initConfig(struct config *conf)
// struct dns
conf->dns.upstreams.k = "dns.upstreams";
conf->dns.upstreams.h = "Array of upstream DNS servers used by Pi-hole\n Example: [ \"8.8.8.8\", \"127.0.0.1#5353\", \"docker-resolver\" ]";
conf->dns.upstreams.h = "Array of upstream DNS servers used by Pi-hole\n Example: [ \"8.8.8.8\", \"127.0.0.1#5335\", \"docker-resolver\" ]";
conf->dns.upstreams.a = cJSON_CreateStringReference("array of IP addresses and/or hostnames, optionally with a port (#...)");
conf->dns.upstreams.t = CONF_JSON_STRING_ARRAY;
conf->dns.upstreams.d.json = cJSON_CreateArray();
conf->dns.upstreams.f = FLAG_RESTART_FTL;
conf->dns.upstreams.c = validate_stub; // Type-based checking + dnsmasq syntax checking
conf->dns.CNAMEdeepInspect.k = "dns.CNAMEdeepInspect";
conf->dns.CNAMEdeepInspect.h = "Use this option to control deep CNAME inspection. Disabling it might be beneficial for very low-end devices";
conf->dns.CNAMEdeepInspect.t = CONF_BOOL;
conf->dns.CNAMEdeepInspect.f = FLAG_ADVANCED_SETTING;
conf->dns.CNAMEdeepInspect.d.b = true;
conf->dns.CNAMEdeepInspect.c = validate_stub; // Only type-based checking
conf->dns.blockESNI.k = "dns.blockESNI";
conf->dns.blockESNI.h = "Should _esni. subdomains be blocked by default? Encrypted Server Name Indication (ESNI) is certainly a good step into the right direction to enhance privacy on the web. It prevents on-path observers, including ISPs, coffee shop owners and firewalls, from intercepting the TLS Server Name Indication (SNI) extension by encrypting it. This prevents the SNI from being used to determine which websites users are visiting.\n ESNI will obviously cause issues for pixelserv-tls which will be unable to generate matching certificates on-the-fly when it cannot read the SNI. Cloudflare and Firefox are already enabling ESNI. According to the IEFT draft (link above), we can easily restore piselserv-tls's operation by replying NXDOMAIN to _esni. subdomains of blocked domains as this mimics a \"not configured for this domain\" behavior.";
conf->dns.blockESNI.t = CONF_BOOL;
conf->dns.blockESNI.f = FLAG_ADVANCED_SETTING;
conf->dns.blockESNI.d.b = true;
conf->dns.blockESNI.c = validate_stub; // Only type-based checking
conf->dns.EDNS0ECS.k = "dns.EDNS0ECS";
conf->dns.EDNS0ECS.h = "Should we overwrite the query source when client information is provided through EDNS0 client subnet (ECS) information? This allows Pi-hole to obtain client IPs even if they are hidden behind the NAT of a router. This feature has been requested and discussed on Discourse where further information how to use it can be found: https://discourse.pi-hole.net/t/support-for-add-subnet-option-from-dnsmasq-ecs-edns0-client-subnet/35940";
conf->dns.EDNS0ECS.t = CONF_BOOL;
conf->dns.EDNS0ECS.f = FLAG_ADVANCED_SETTING;
conf->dns.EDNS0ECS.d.b = true;
conf->dns.EDNS0ECS.c = validate_stub; // Only type-based checking
conf->dns.ignoreLocalhost.k = "dns.ignoreLocalhost";
conf->dns.ignoreLocalhost.h = "Should FTL hide queries made by localhost?";
conf->dns.ignoreLocalhost.t = CONF_BOOL;
conf->dns.ignoreLocalhost.f = FLAG_ADVANCED_SETTING;
conf->dns.ignoreLocalhost.d.b = false;
conf->dns.ignoreLocalhost.c = validate_stub; // Only type-based checking
conf->dns.showDNSSEC.k = "dns.showDNSSEC";
conf->dns.showDNSSEC.h = "Should FTL should analyze and show internally generated DNSSEC queries?";
conf->dns.showDNSSEC.t = CONF_BOOL;
conf->dns.showDNSSEC.f = FLAG_ADVANCED_SETTING;
conf->dns.showDNSSEC.d.b = true;
conf->dns.showDNSSEC.c = validate_stub; // Only type-based checking
conf->dns.analyzeOnlyAandAAAA.k = "dns.analyzeOnlyAandAAAA";
conf->dns.analyzeOnlyAandAAAA.h = "Should FTL analyze *only* A and AAAA queries?";
conf->dns.analyzeOnlyAandAAAA.t = CONF_BOOL;
conf->dns.analyzeOnlyAandAAAA.f = FLAG_ADVANCED_SETTING;
conf->dns.analyzeOnlyAandAAAA.d.b = false;
conf->dns.analyzeOnlyAandAAAA.c = validate_stub; // Only type-based checking
conf->dns.piholePTR.k = "dns.piholePTR";
conf->dns.piholePTR.h = "Controls whether and how FTL will reply with for address for which a local interface exists.";
@ -440,6 +449,7 @@ void initConfig(struct config *conf)
conf->dns.piholePTR.t = CONF_ENUM_PTR_TYPE;
conf->dns.piholePTR.f = FLAG_ADVANCED_SETTING;
conf->dns.piholePTR.d.ptr_type = PTR_PIHOLE;
conf->dns.piholePTR.c = validate_stub; // Only type-based checking
conf->dns.replyWhenBusy.k = "dns.replyWhenBusy";
conf->dns.replyWhenBusy.h = "How should FTL handle queries when the gravity database is not available?";
@ -456,12 +466,14 @@ void initConfig(struct config *conf)
conf->dns.replyWhenBusy.t = CONF_ENUM_BUSY_TYPE;
conf->dns.replyWhenBusy.f = FLAG_ADVANCED_SETTING;
conf->dns.replyWhenBusy.d.busy_reply = BUSY_ALLOW;
conf->dns.replyWhenBusy.c = validate_stub; // Only type-based checking
conf->dns.blockTTL.k = "dns.blockTTL";
conf->dns.blockTTL.h = "FTL's internal TTL to be handed out for blocked queries in seconds. This settings allows users to select a value different from the dnsmasq config option local-ttl. This is useful in context of locally used hostnames that are known to stay constant over long times (printers, etc.).\n Note that large values may render whitelisting ineffective due to client-side caching of blocked queries.";
conf->dns.blockTTL.t = CONF_UINT;
conf->dns.blockTTL.f = FLAG_ADVANCED_SETTING;
conf->dns.blockTTL.d.ui = 2;
conf->dns.blockTTL.c = validate_stub; // Only type-based checking
conf->dns.hosts.k = "dns.hosts";
conf->dns.hosts.h = "Array of custom DNS records\n Example: hosts = [ \"127.0.0.1 mylocal\", \"192.168.0.1 therouter\" ]";
@ -469,18 +481,21 @@ void initConfig(struct config *conf)
conf->dns.hosts.t = CONF_JSON_STRING_ARRAY;
conf->dns.hosts.f = FLAG_ADVANCED_SETTING;
conf->dns.hosts.d.json = cJSON_CreateArray();
conf->dns.hosts.c = validate_dns_hosts;
conf->dns.domainNeeded.k = "dns.domainNeeded";
conf->dns.domainNeeded.h = "If set, A and AAAA queries for plain names, without dots or domain parts, are never forwarded to upstream nameservers";
conf->dns.domainNeeded.t = CONF_BOOL;
conf->dns.domainNeeded.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.domainNeeded.d.b = false;
conf->dns.domainNeeded.c = validate_stub; // Only type-based checking
conf->dns.expandHosts.k = "dns.expandHosts";
conf->dns.expandHosts.h = "If set, the domain is added to simple names (without a period) in /etc/hosts in the same way as for DHCP-derived names";
conf->dns.expandHosts.t = CONF_BOOL;
conf->dns.expandHosts.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.expandHosts.d.b = false;
conf->dns.expandHosts.c = validate_stub; // Only type-based checking
conf->dns.domain.k = "dns.domain";
conf->dns.domain.h = "The DNS domain used by your Pi-hole to expand hosts and for DHCP.\n\n Only if DHCP is enabled below: For DHCP, this has two effects; firstly it causes the DHCP server to return the domain to any hosts which request it, and secondly it sets the domain which it is legal for DHCP-configured hosts to claim. The intention is to constrain hostnames so that an untrusted host on the LAN cannot advertise its name via DHCP as e.g. \"google.com\" and capture traffic not meant for it. If no domain suffix is specified, then any DHCP hostname with a domain part (ie with a period) will be disallowed and logged. If a domain is specified, then hostnames with a domain part are allowed, provided the domain part matches the suffix. In addition, when a suffix is set then hostnames without a domain part have the suffix added as an optional domain part. For instance, we can set domain=mylab.com and have a machine whose DHCP hostname is \"laptop\". The IP address for that machine is available both as \"laptop\" and \"laptop.mylab.com\".\n\n You can disable setting a domain by setting this option to an empty string.";
@ -488,17 +503,20 @@ void initConfig(struct config *conf)
conf->dns.domain.t = CONF_STRING;
conf->dns.domain.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.domain.d.s = (char*)"lan";
conf->dns.domain.c = validate_domain;
conf->dns.bogusPriv.k = "dns.bogusPriv";
conf->dns.bogusPriv.h = "Should all reverse lookups for private IP ranges (i.e., 192.168.x.y, etc) which are not found in /etc/hosts or the DHCP leases file be answered with \"no such domain\" rather than being forwarded upstream?";
conf->dns.bogusPriv.t = CONF_BOOL;
conf->dns.bogusPriv.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.bogusPriv.d.b = true;
conf->dns.bogusPriv.c = validate_stub; // Only type-based checking
conf->dns.dnssec.k = "dns.dnssec";
conf->dns.dnssec.h = "Validate DNS replies using DNSSEC?";
conf->dns.dnssec.t = CONF_BOOL;
conf->dns.dnssec.f = FLAG_RESTART_FTL;
conf->dns.dnssec.c = validate_stub; // Only type-based checking
conf->dns.dnssec.d.b = false;
conf->dns.interface.k = "dns.interface";
@ -507,6 +525,7 @@ void initConfig(struct config *conf)
conf->dns.interface.t = CONF_STRING;
conf->dns.interface.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.interface.d.s = (char*)"";
conf->dns.interface.c = validate_stub; // Type-based checking + dnsmasq syntax checking
conf->dns.hostRecord.k = "dns.hostRecord";
conf->dns.hostRecord.h = "Add A, AAAA and PTR records to the DNS. This adds one or more names to the DNS with associated IPv4 (A) and IPv6 (AAAA) records";
@ -514,6 +533,7 @@ void initConfig(struct config *conf)
conf->dns.hostRecord.t = CONF_STRING;
conf->dns.hostRecord.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.hostRecord.d.s = (char*)"";
conf->dns.hostRecord.c = validate_stub; // Type-based checking + dnsmasq syntax checking
conf->dns.listeningMode.k = "dns.listeningMode";
conf->dns.listeningMode.h = "Pi-hole interface listening modes";
@ -531,12 +551,14 @@ void initConfig(struct config *conf)
conf->dns.listeningMode.t = CONF_ENUM_LISTENING_MODE;
conf->dns.listeningMode.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.listeningMode.d.listeningMode = LISTEN_LOCAL;
conf->dns.listeningMode.c = validate_stub; // Only type-based checking
conf->dns.queryLogging.k = "dns.queryLogging";
conf->dns.queryLogging.h = "Log DNS queries and replies to pihole.log";
conf->dns.queryLogging.t = CONF_BOOL;
conf->dns.queryLogging.f = FLAG_RESTART_FTL;
conf->dns.queryLogging.d.b = true;
conf->dns.queryLogging.c = validate_stub; // Only type-based checking
conf->dns.cnameRecords.k = "dns.cnameRecords";
conf->dns.cnameRecords.h = "List of CNAME records which indicate that <cname> is really <target>. If the <TTL> is given, it overwrites the value of local-ttl";
@ -544,12 +566,14 @@ void initConfig(struct config *conf)
conf->dns.cnameRecords.t = CONF_JSON_STRING_ARRAY;
conf->dns.cnameRecords.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.cnameRecords.d.json = cJSON_CreateArray();
conf->dns.cnameRecords.c = validate_dns_cnames;
conf->dns.port.k = "dns.port";
conf->dns.port.h = "Port used by the DNS server";
conf->dns.port.t = CONF_UINT16;
conf->dns.port.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.port.d.ui = 53u;
conf->dns.port.c = validate_stub; // Only type-based checking
// sub-struct dns.cache
conf->dns.cache.size.k = "dns.cache.size";
@ -557,18 +581,21 @@ void initConfig(struct config *conf)
conf->dns.cache.size.t = CONF_UINT;
conf->dns.cache.size.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.cache.size.d.ui = 10000u;
conf->dns.cache.size.c = validate_stub; // Only type-based checking
conf->dns.cache.optimizer.k = "dns.cache.optimizer";
conf->dns.cache.optimizer.h = "Query cache optimizer: If a DNS name exists in the cache, but its time-to-live has expired only recently, the data will be used anyway (a refreshing from upstream is triggered). This can improve DNS query delays especially over unreliable Internet connections. This feature comes at the expense of possibly sometimes returning out-of-date data and less efficient cache utilization, since old data cannot be flushed when its TTL expires, so the cache becomes mostly least-recently-used. To mitigate issues caused by massively outdated DNS replies, the maximum overaging of cached records is limited. We strongly recommend staying below 86400 (1 day) with this option.\n Setting the TTL excess time to zero will serve stale cache data regardless how long it has expired. This is not recommended as it may lead to stale data being served for a long time. Setting this option to any negative value will disable this feature altogether.";
conf->dns.cache.optimizer.t = CONF_INT;
conf->dns.cache.optimizer.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dns.cache.optimizer.d.i = 3600u;
conf->dns.cache.optimizer.c = validate_stub; // Only type-based checking
// sub-struct dns.blocking
conf->dns.blocking.active.k = "dns.blocking.active";
conf->dns.blocking.active.h = "Should FTL block queries?";
conf->dns.blocking.active.t = CONF_BOOL;
conf->dns.blocking.active.d.b = true;
conf->dns.blocking.active.c = validate_stub; // Only type-based checking
conf->dns.blocking.mode.k = "dns.blocking.mode";
conf->dns.blocking.mode.h = "How should FTL reply to blocked queries?";
@ -585,12 +612,14 @@ void initConfig(struct config *conf)
}
conf->dns.blocking.mode.t = CONF_ENUM_BLOCKING_MODE;
conf->dns.blocking.mode.d.blocking_mode = MODE_NULL;
conf->dns.blocking.mode.c = validate_stub; // Only type-based checking
conf->dns.revServers.k = "dns.revServers";
conf->dns.revServers.h = "Reverse server (former also called \"conditional forwarding\") feature\n Array of reverse servers each one in one of the following forms: \"<enabled>,<ip-address>[/<prefix-len>],<server>[#<port>],<domain>\"\n\n Individual components:\n\n <enabled>: either \"true\" or \"false\"\n\n <ip-address>[/<prefix-len>]: Address range for the reverse server feature in CIDR notation. If the prefix length is omitted, either 32 (IPv4) or 128 (IPv6) are substituted (exact address match). This is almost certainly not what you want here.\n Example: \"192.168.0.0/24\" for the range 192.168.0.1 - 192.168.0.255\n\n <server>[#<port>]: Target server to be used for the reverse server feature\n Example: \"192.168.0.1#53\"\n\n <domain>: Domain used for the reverse server feature (e.g., \"fritz.box\")\n Example: \"fritz.box\"";
conf->dns.revServers.a = cJSON_CreateStringReference("array of reverse servers each one in one of the following forms: \"<enabled>,<ip-address>[/<prefix-len>],<server>[#<port>],<domain>\", e.g., \"true,192.168.0.0/24,192.168.0.1,fritz.box\"");
conf->dns.revServers.t = CONF_JSON_STRING_ARRAY;
conf->dns.revServers.d.json = cJSON_CreateArray();
conf->dns.revServers.c = validate_dns_revServers;
conf->dns.revServers.f = FLAG_RESTART_FTL;
// sub-struct dns.rate_limit
@ -598,22 +627,26 @@ void initConfig(struct config *conf)
conf->dns.rateLimit.count.h = "Rate-limited queries are answered with a REFUSED reply and not further processed by FTL.\n The default settings for FTL's rate-limiting are to permit no more than 1000 queries in 60 seconds. Both numbers can be customized independently. It is important to note that rate-limiting is happening on a per-client basis. Other clients can continue to use FTL while rate-limited clients are short-circuited at the same time.\n For this setting, both numbers, the maximum number of queries within a given time, and the length of the time interval (seconds) have to be specified. For instance, if you want to set a rate limit of 1 query per hour, the option should look like RATE_LIMIT=1/3600. The time interval is relative to when FTL has finished starting (start of the daemon + possible delay by DELAY_STARTUP) then it will advance in steps of the rate-limiting interval. If a client reaches the maximum number of queries it will be blocked until the end of the current interval. This will be logged to /var/log/pihole/FTL.log, e.g. Rate-limiting 10.0.1.39 for at least 44 seconds. If the client continues to send queries while being blocked already and this number of queries during the blocking exceeds the limit the client will continue to be blocked until the end of the next interval (FTL.log will contain lines like Still rate-limiting 10.0.1.39 as it made additional 5007 queries). As soon as the client requests less than the set limit, it will be unblocked (Ending rate-limitation of 10.0.1.39).\n Rate-limiting may be disabled altogether by setting both values to zero (this results in the same behavior as before FTL v5.7).\n How many queries are permitted...";
conf->dns.rateLimit.count.t = CONF_UINT;
conf->dns.rateLimit.count.d.ui = 1000;
conf->dns.rateLimit.count.c = validate_stub; // Only type-based checking
conf->dns.rateLimit.interval.k = "dns.rateLimit.interval";
conf->dns.rateLimit.interval.h = "... in the set interval before rate-limiting?";
conf->dns.rateLimit.interval.t = CONF_UINT;
conf->dns.rateLimit.interval.d.ui = 60;
conf->dns.rateLimit.interval.c = validate_stub; // Only type-based checking
// sub-struct dns.special_domains
conf->dns.specialDomains.mozillaCanary.k = "dns.specialDomains.mozillaCanary";
conf->dns.specialDomains.mozillaCanary.h = "Should Pi-hole always replies with NXDOMAIN to A and AAAA queries of use-application-dns.net to disable Firefox automatic DNS-over-HTTP? This is following the recommendation on https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https";
conf->dns.specialDomains.mozillaCanary.t = CONF_BOOL;
conf->dns.specialDomains.mozillaCanary.d.b = true;
conf->dns.specialDomains.mozillaCanary.c = validate_stub; // Only type-based checking
conf->dns.specialDomains.iCloudPrivateRelay.k = "dns.specialDomains.iCloudPrivateRelay";
conf->dns.specialDomains.iCloudPrivateRelay.h = "Should Pi-hole always replies with NXDOMAIN to A and AAAA queries of mask.icloud.com and mask-h2.icloud.com to disable Apple's iCloud Private Relay to prevent Apple devices from bypassing Pi-hole? This is following the recommendation on https://developer.apple.com/support/prepare-your-network-for-icloud-private-relay";
conf->dns.specialDomains.iCloudPrivateRelay.t = CONF_BOOL;
conf->dns.specialDomains.iCloudPrivateRelay.d.b = true;
conf->dns.specialDomains.iCloudPrivateRelay.c = validate_stub; // Only type-based checking
// sub-struct dns.reply_addr
conf->dns.reply.host.force4.k = "dns.reply.host.force4";
@ -621,6 +654,7 @@ void initConfig(struct config *conf)
conf->dns.reply.host.force4.t = CONF_BOOL;
conf->dns.reply.host.force4.f = FLAG_ADVANCED_SETTING;
conf->dns.reply.host.force4.d.b = false;
conf->dns.reply.host.force4.c = validate_stub; // Only type-based checking
conf->dns.reply.host.v4.k = "dns.reply.host.IPv4";
conf->dns.reply.host.v4.h = "Custom IPv4 address for the Pi-hole host";
@ -628,12 +662,14 @@ void initConfig(struct config *conf)
conf->dns.reply.host.v4.t = CONF_STRUCT_IN_ADDR;
conf->dns.reply.host.v4.f = FLAG_ADVANCED_SETTING;
memset(&conf->dns.reply.host.v4.d.in_addr, 0, sizeof(struct in_addr));
conf->dns.reply.host.v4.c = validate_stub; // Only type-based checking
conf->dns.reply.host.force6.k = "dns.reply.host.force6";
conf->dns.reply.host.force6.h = "Use a specific IPv6 address for the Pi-hole host? See description for the IPv4 variant above for further details.";
conf->dns.reply.host.force6.t = CONF_BOOL;
conf->dns.reply.host.force6.f = FLAG_ADVANCED_SETTING;
conf->dns.reply.host.force6.d.b = false;
conf->dns.reply.host.force6.c = validate_stub; // Only type-based checking
conf->dns.reply.host.v6.k = "dns.reply.host.IPv6";
conf->dns.reply.host.v6.h = "Custom IPv6 address for the Pi-hole host";
@ -641,12 +677,14 @@ void initConfig(struct config *conf)
conf->dns.reply.host.v6.t = CONF_STRUCT_IN6_ADDR;
conf->dns.reply.host.v6.f = FLAG_ADVANCED_SETTING;
memset(&conf->dns.reply.host.v6.d.in6_addr, 0, sizeof(struct in6_addr));
conf->dns.reply.host.v6.c = validate_stub; // Only type-based checking
conf->dns.reply.blocking.force4.k = "dns.reply.blocking.force4";
conf->dns.reply.blocking.force4.h = "Use a specific IPv4 address in IP blocking mode? By default, FTL determines the address of the interface a query arrived on and uses this address for replying to A queries with the most suitable address for the requesting client. This setting can be used to use a fixed, rather than the dynamically obtained, address when Pi-hole responds in the following cases: IP blocking mode is used and this query is to be blocked, regular expressions with the ;reply=IP regex extension.";
conf->dns.reply.blocking.force4.t = CONF_BOOL;
conf->dns.reply.blocking.force4.f = FLAG_ADVANCED_SETTING;
conf->dns.reply.blocking.force4.d.b = false;
conf->dns.reply.blocking.force4.c = validate_stub; // Only type-based checking
conf->dns.reply.blocking.v4.k = "dns.reply.blocking.IPv4";
conf->dns.reply.blocking.v4.h = "Custom IPv4 address for IP blocking mode";
@ -654,12 +692,14 @@ void initConfig(struct config *conf)
conf->dns.reply.blocking.v4.t = CONF_STRUCT_IN_ADDR;
conf->dns.reply.blocking.v4.f = FLAG_ADVANCED_SETTING;
memset(&conf->dns.reply.blocking.v4.d.in_addr, 0, sizeof(struct in_addr));
conf->dns.reply.blocking.v4.c = validate_stub; // Only type-based checking
conf->dns.reply.blocking.force6.k = "dns.reply.blocking.force6";
conf->dns.reply.blocking.force6.h = "Use a specific IPv6 address in IP blocking mode? See description for the IPv4 variant above for further details.";
conf->dns.reply.blocking.force6.t = CONF_BOOL;
conf->dns.reply.blocking.force6.f = FLAG_ADVANCED_SETTING;
conf->dns.reply.blocking.force6.d.b = false;
conf->dns.reply.blocking.force6.c = validate_stub; // Only type-based checking
conf->dns.reply.blocking.v6.k = "dns.reply.blocking.IPv6";
conf->dns.reply.blocking.v6.h = "Custom IPv6 address for IP blocking mode";
@ -667,6 +707,7 @@ void initConfig(struct config *conf)
conf->dns.reply.blocking.v6.t = CONF_STRUCT_IN6_ADDR;
conf->dns.reply.blocking.v6.f = FLAG_ADVANCED_SETTING;
memset(&conf->dns.reply.blocking.v6.d.in6_addr, 0, sizeof(struct in6_addr));
conf->dns.reply.blocking.v6.c = validate_stub; // Only type-based checking
// sub-struct dhcp
conf->dhcp.active.k = "dhcp.active";
@ -674,6 +715,7 @@ void initConfig(struct config *conf)
conf->dhcp.active.t = CONF_BOOL;
conf->dhcp.active.f = FLAG_RESTART_FTL;
conf->dhcp.active.d.b = false;
conf->dhcp.active.c = validate_stub; // Only type-based checking
conf->dhcp.start.k = "dhcp.start";
conf->dhcp.start.h = "Start address of the DHCP address pool";
@ -681,6 +723,7 @@ void initConfig(struct config *conf)
conf->dhcp.start.t = CONF_STRUCT_IN_ADDR;
conf->dhcp.start.f = FLAG_RESTART_FTL;
memset(&conf->dhcp.start.d.in_addr, 0, sizeof(struct in_addr));
conf->dhcp.start.c = validate_stub; // Only type-based checking
conf->dhcp.end.k = "dhcp.end";
conf->dhcp.end.h = "End address of the DHCP address pool";
@ -688,6 +731,7 @@ void initConfig(struct config *conf)
conf->dhcp.end.t = CONF_STRUCT_IN_ADDR;
conf->dhcp.end.f = FLAG_RESTART_FTL;
memset(&conf->dhcp.end.d.in_addr, 0, sizeof(struct in_addr));
conf->dhcp.end.c = validate_stub; // Only type-based checking
conf->dhcp.router.k = "dhcp.router";
conf->dhcp.router.h = "Address of the gateway to be used (typically the address of your router in a home installation)";
@ -695,6 +739,7 @@ void initConfig(struct config *conf)
conf->dhcp.router.t = CONF_STRUCT_IN_ADDR;
conf->dhcp.router.f = FLAG_RESTART_FTL;
memset(&conf->dhcp.router.d.in_addr, 0, sizeof(struct in_addr));
conf->dhcp.router.c = validate_stub; // Only type-based checking
conf->dhcp.netmask.k = "dhcp.netmask";
conf->dhcp.netmask.h = "The netmask used by your Pi-hole. For directly connected networks (i.e., networks on which the machine running Pi-hole has an interface) the netmask is optional and may be set to an empty string (\"\"): it will then be determined from the interface configuration itself. For networks which receive DHCP service via a relay agent, we cannot determine the netmask itself, so it should explicitly be specified, otherwise Pi-hole guesses based on the class (A, B or C) of the network address.";
@ -702,6 +747,7 @@ void initConfig(struct config *conf)
conf->dhcp.netmask.t = CONF_STRUCT_IN_ADDR;
conf->dhcp.netmask.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
memset(&conf->dhcp.netmask.d.in_addr, 0, sizeof(struct in_addr));
conf->dhcp.netmask.c = validate_stub; // Only type-based checking
conf->dhcp.leaseTime.k = "dhcp.leaseTime";
conf->dhcp.leaseTime.h = "If the lease time is given, then leases will be given for that length of time. If not given, the default lease time is one hour for IPv4 and one day for IPv6.";
@ -709,24 +755,28 @@ void initConfig(struct config *conf)
conf->dhcp.leaseTime.t = CONF_STRING;
conf->dhcp.leaseTime.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dhcp.leaseTime.d.s = (char*)"";
conf->dhcp.leaseTime.c = validate_stub; // Type-based checking + dnsmasq syntax checking
conf->dhcp.ipv6.k = "dhcp.ipv6";
conf->dhcp.ipv6.h = "Should Pi-hole make an attempt to also satisfy IPv6 address requests (be aware that IPv6 works a whole lot different than IPv4)";
conf->dhcp.ipv6.t = CONF_BOOL;
conf->dhcp.ipv6.f = FLAG_RESTART_FTL;
conf->dhcp.ipv6.d.b = false;
conf->dhcp.ipv6.c = validate_stub; // Only type-based checking
conf->dhcp.multiDNS.k = "dhcp.multiDNS";
conf->dhcp.multiDNS.h = "Advertise DNS server multiple times to clients. Some devices will add their own proprietary DNS servers to the list of DNS servers, which can cause issues with Pi-hole. This option will advertise the Pi-hole DNS server multiple times to clients, which should prevent this from happening.";
conf->dhcp.multiDNS.t = CONF_BOOL;
conf->dhcp.multiDNS.f = FLAG_RESTART_FTL;
conf->dhcp.multiDNS.d.b = false;
conf->dhcp.multiDNS.c = validate_stub; // Only type-based checking
conf->dhcp.rapidCommit.k = "dhcp.rapidCommit";
conf->dhcp.rapidCommit.h = "Enable DHCPv4 Rapid Commit Option specified in RFC 4039. Should only be enabled if either the server is the only server for the subnet to avoid conflicts";
conf->dhcp.rapidCommit.t = CONF_BOOL;
conf->dhcp.rapidCommit.f = FLAG_RESTART_FTL;
conf->dhcp.rapidCommit.d.b = false;
conf->dhcp.rapidCommit.c = validate_stub; // Only type-based checking
conf->dhcp.hosts.k = "dhcp.hosts";
conf->dhcp.hosts.h = "Per host parameters for the DHCP server. This allows a machine with a particular hardware address to be always allocated the same hostname, IP address and lease time or to specify static DHCP leases";
@ -734,6 +784,7 @@ void initConfig(struct config *conf)
conf->dhcp.hosts.t = CONF_JSON_STRING_ARRAY;
conf->dhcp.hosts.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->dhcp.hosts.d.json = cJSON_CreateArray();
conf->dhcp.hosts.c = validate_stub; // Type-based checking + dnsmasq syntax checking
// struct resolver
@ -741,17 +792,20 @@ void initConfig(struct config *conf)
conf->resolver.resolveIPv6.h = "Should FTL try to resolve IPv6 addresses to hostnames?";
conf->resolver.resolveIPv6.t = CONF_BOOL;
conf->resolver.resolveIPv6.d.b = true;
conf->resolver.resolveIPv6.c = validate_stub; // Only type-based checking
conf->resolver.resolveIPv4.k = "resolver.resolveIPv4";
conf->resolver.resolveIPv4.h = "Should FTL try to resolve IPv4 addresses to hostnames?";
conf->resolver.resolveIPv4.t = CONF_BOOL;
conf->resolver.resolveIPv4.d.b = true;
conf->resolver.resolveIPv4.c = validate_stub; // Only type-based checking
conf->resolver.networkNames.k = "resolver.networkNames";
conf->resolver.networkNames.h = "Control whether FTL should use the fallback option to try to obtain client names from checking the network table. This behavior can be disabled with this option.\n Assume an IPv6 client without a host names. However, the network table knows - though the client's MAC address - that this is the same device where we have a host name for another IP address (e.g., a DHCP server managed IPv4 address). In this case, we use the host name associated to the other address as this is the same device.";
conf->resolver.networkNames.t = CONF_BOOL;
conf->resolver.networkNames.f = FLAG_ADVANCED_SETTING;
conf->resolver.networkNames.d.b = true;
conf->resolver.networkNames.c = validate_stub; // Only type-based checking
conf->resolver.refreshNames.k = "resolver.refreshNames";
conf->resolver.refreshNames.h = "With this option, you can change how (and if) hourly PTR requests are made to check for changes in client and upstream server hostnames.";
@ -768,6 +822,7 @@ void initConfig(struct config *conf)
conf->resolver.refreshNames.t = CONF_ENUM_REFRESH_HOSTNAMES;
conf->resolver.refreshNames.f = FLAG_ADVANCED_SETTING;
conf->resolver.refreshNames.d.refresh_hostnames = REFRESH_IPV4_ONLY;
conf->resolver.refreshNames.c = validate_stub; // Only type-based checking
// struct database
@ -775,21 +830,43 @@ void initConfig(struct config *conf)
conf->database.DBimport.h = "Should FTL load information from the database on startup to be aware of the most recent history?";
conf->database.DBimport.t = CONF_BOOL;
conf->database.DBimport.d.b = true;
conf->database.DBimport.c = validate_stub; // Only type-based checking
conf->database.DBexport.k = "database.DBexport";
conf->database.DBexport.h = "Should FTL store queries in the long-term database?";
conf->database.DBexport.t = CONF_BOOL;
conf->database.DBexport.d.b = true;
conf->database.DBexport.c = validate_stub; // Only type-based checking
conf->database.maxDBdays.k = "database.maxDBdays";
conf->database.maxDBdays.h = "How long should queries be stored in the database [days]?";
conf->database.maxDBdays.t = CONF_INT;
conf->database.maxDBdays.d.i = (365/4);
conf->database.maxDBdays.c = validate_stub; // Only type-based checking
conf->database.DBinterval.k = "database.DBinterval";
conf->database.DBinterval.h = "How often do we store queries in FTL's database [seconds]?";
conf->database.DBinterval.t = CONF_UINT;
conf->database.DBinterval.d.ui = 60;
conf->database.DBinterval.c = validate_stub; // Only type-based checking
conf->database.useWAL.k = "database.useWAL";
conf->database.useWAL.h = "Should FTL enable Write-Ahead Log (WAL) mode for the on-disk query database (configured via files.database)?\n It is recommended to leave this setting enabled for performance reasons. About the only reason to disable WAL mode is if you are experiencing specific issues with it, e.g., when using a database that is accessed from multiple hosts via a network share. When this setting is disabled, FTL will use SQLite3's default journal mode (rollback journal in DELETE mode).";
conf->database.useWAL.t = CONF_BOOL;
// Note: We would not necessarily need to restart FTL when this setting
// is changed, but we do it anyway as this ensures the database is
// properly re-initialized and the new journal mode is used. As this is
// a setting that will be changed very rarely, this seems the better
// compromise than adding special code that can transform the database
// while being active.
// The in-memory database is not affected by this setting as it uses a
// MEMORY journal mode anyway (there is nothing to be restored after power
// loss). The gravity database is also not affected as it is only written
// to on an individual basis (explicit API calls) and not continuously
// (like the query database).
conf->database.useWAL.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->database.useWAL.d.b = true;
conf->database.useWAL.c = validate_stub; // Only type-based checking
// sub-struct database.network
conf->database.network.parseARPcache.k = "database.network.parseARPcache";
@ -797,12 +874,14 @@ void initConfig(struct config *conf)
conf->database.network.parseARPcache.t = CONF_BOOL;
conf->database.network.parseARPcache.f = FLAG_ADVANCED_SETTING;
conf->database.network.parseARPcache.d.b = true;
conf->database.network.parseARPcache.c = validate_stub; // Only type-based checking
conf->database.network.expire.k = "database.network.expire";
conf->database.network.expire.h = "How long should IP addresses be kept in the network_addresses table [days]? IP addresses (and associated host names) older than the specified number of days are removed to avoid dead entries in the network overview table.";
conf->database.network.expire.t = CONF_UINT;
conf->database.network.expire.f = FLAG_ADVANCED_SETTING;
conf->database.network.expire.d.ui = conf->database.maxDBdays.d.ui;
conf->database.network.expire.c = validate_stub; // Only type-based checking
// struct http
@ -812,6 +891,7 @@ void initConfig(struct config *conf)
conf->webserver.domain.t = CONF_STRING;
conf->webserver.domain.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.domain.d.s = (char*)"pi.hole";
conf->webserver.domain.c = validate_domain;
conf->webserver.acl.k = "webserver.acl";
conf->webserver.acl.h = "Webserver access control list (ACL) allowing for restrictions to be put on the list of IP addresses which have access to the web server. The ACL is a comma separated list of IP subnets, where each subnet is prepended by either a - or a + sign. A plus sign means allow, where a minus sign means deny. If a subnet mask is omitted, such as -1.2.3.4, this means to deny only that single IP address. If this value is not set (empty string), all accesses are allowed. Otherwise, the default setting is to deny all accesses. On each request the full list is traversed, and the last (!) match wins. IPv6 addresses may be specified in CIDR-form [a:b::c]/64.\n\n Example 1: acl = \"+127.0.0.1,+[::1]\"\n ---> deny all access, except from 127.0.0.1 and ::1,\n Example 2: acl = \"+192.168.0.0/16\"\n ---> deny all accesses, except from the 192.168.0.0/16 subnet,\n Example 3: acl = \"+[::]/0\" ---> allow only IPv6 access.";
@ -819,6 +899,7 @@ void initConfig(struct config *conf)
conf->webserver.acl.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.acl.t = CONF_STRING;
conf->webserver.acl.d.s = (char*)"";
conf->webserver.acl.c = validate_stub; // Type-based checking + civetweb syntax checking
conf->webserver.port.k = "webserver.port";
conf->webserver.port.h = "Ports to be used by the webserver.\n Comma-separated list of ports to listen on. It is possible to specify an IP address to bind to. In this case, an IP address and a colon must be prepended to the port number. For example, to bind to the loopback interface on port 80 (IPv4) and to all interfaces port 8080 (IPv4), use \"127.0.0.1:80,8080\". \"[::]:80\" can be used to listen to IPv6 connections to port 80. IPv6 addresses of network interfaces can be specified as well, e.g. \"[::1]:80\" for the IPv6 loopback interface. [::]:80 will bind to port 80 IPv6 only.\n In order to use port 80 for all interfaces, both IPv4 and IPv6, use either the configuration \"80,[::]:80\" (create one socket for IPv4 and one for IPv6 only), or \"+80\" (create one socket for both, IPv4 and IPv6). The + notation to use IPv4 and IPv6 will only work if no network interface is specified. Depending on your operating system version and IPv6 network environment, some configurations might not work as expected, so you have to test to find the configuration most suitable for your needs. In case \"+80\" does not work for your environment, you need to use \"80,[::]:80\".\n If the port is TLS/SSL, a letter 's' must be appended, for example, \"80,443s\" will open port 80 and port 443, and connections on port 443 will be encrypted. For non-encrypted ports, it is allowed to append letter 'r' (as in redirect). Redirected ports will redirect all their traffic to the first configured SSL port. For example, if webserver.port is \"80r,443s\", then all HTTP traffic coming at port 80 will be redirected to HTTPS port 443. If this value is not set (empty string), the web server will not be started and, hence, the API will not be available.";
@ -826,12 +907,14 @@ void initConfig(struct config *conf)
conf->webserver.port.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.port.t = CONF_STRING;
conf->webserver.port.d.s = (char*)"80,[::]:80,443s,[::]:443s";
conf->webserver.port.c = validate_stub; // Type-based checking + civetweb syntax checking
conf->webserver.tls.rev_proxy.k = "webserver.tls.rev_proxy";
conf->webserver.tls.rev_proxy.h = "Is Pi-hole running behind a reverse proxy? If yes, Pi-hole will not consider HTTP-only connections being insecure. This is useful if you are running Pi-hole in a trusted environment, for example, in a local network, and you are using a reverse proxy to provide TLS encryption, e.g., by using Traefik (docker). If you are using a reverse proxy, you can alternatively set webserver.tls.cert to the path of the TLS certificate file and let Pi-hole handle true end-to-end encryption.";
conf->webserver.tls.rev_proxy.f = FLAG_ADVANCED_SETTING;
conf->webserver.tls.rev_proxy.t = CONF_BOOL;
conf->webserver.tls.rev_proxy.d.b = false;
conf->webserver.tls.rev_proxy.c = validate_stub; // Only type-based checking
conf->webserver.tls.cert.k = "webserver.tls.cert";
conf->webserver.tls.cert.h = "Path to the TLS (SSL) certificate file. This option is only required when at least one of webserver.port is TLS. The file must be in PEM format, and it must have both, private key and certificate (the *.pem file created must contain a 'CERTIFICATE' section as well as a 'RSA PRIVATE KEY' section).\n The *.pem file can be created using\n cp server.crt server.pem\n cat server.key >> server.pem\n if you have these files instead";
@ -839,16 +922,19 @@ void initConfig(struct config *conf)
conf->webserver.tls.cert.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.tls.cert.t = CONF_STRING;
conf->webserver.tls.cert.d.s = (char*)"/etc/pihole/tls.pem";
conf->webserver.tls.cert.c = validate_filepath;
conf->webserver.session.timeout.k = "webserver.session.timeout";
conf->webserver.session.timeout.h = "Session timeout in seconds. If a session is inactive for more than this time, it will be terminated. Sessions are continuously refreshed by the web interface, preventing sessions from timing out while the web interface is open.\n This option may also be used to make logins persistent for long times, e.g. 86400 seconds (24 hours), 604800 seconds (7 days) or 2592000 seconds (30 days). Note that the total number of concurrent sessions is limited so setting this value too high may result in users being rejected and unable to log in if there are already too many sessions active.";
conf->webserver.session.timeout.t = CONF_UINT;
conf->webserver.session.timeout.d.ui = 1800u;
conf->webserver.session.timeout.c = validate_stub; // Only type-based checking
conf->webserver.session.restore.k = "webserver.session.restore";
conf->webserver.session.restore.h = "Should Pi-hole backup and restore sessions from the database? This is useful if you want to keep your sessions after a restart of the web interface.";
conf->webserver.session.restore.t = CONF_BOOL;
conf->webserver.session.restore.d.b = true;
conf->webserver.session.restore.c = validate_stub; // Only type-based checking
// sub-struct paths
conf->webserver.paths.webroot.k = "webserver.paths.webroot";
@ -857,6 +943,7 @@ void initConfig(struct config *conf)
conf->webserver.paths.webroot.t = CONF_STRING;
conf->webserver.paths.webroot.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.paths.webroot.d.s = (char*)"/var/www/html";
conf->webserver.paths.webroot.c = validate_filepath;
conf->webserver.paths.webhome.k = "webserver.paths.webhome";
conf->webserver.paths.webhome.h = "Sub-directory of the root containing the web interface";
@ -864,12 +951,14 @@ void initConfig(struct config *conf)
conf->webserver.paths.webhome.t = CONF_STRING;
conf->webserver.paths.webhome.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.paths.webhome.d.s = (char*)"/admin/";
conf->webserver.paths.webhome.c = validate_filepath;
// sub-struct interface
conf->webserver.interface.boxed.k = "webserver.interface.boxed";
conf->webserver.interface.boxed.h = "Should the web interface use the boxed layout?";
conf->webserver.interface.boxed.t = CONF_BOOL;
conf->webserver.interface.boxed.d.b = true;
conf->webserver.interface.boxed.c = validate_stub; // Only type-based checking
conf->webserver.interface.theme.k = "webserver.interface.theme";
conf->webserver.interface.theme.h = "Theme used by the Pi-hole web interface";
@ -884,29 +973,34 @@ void initConfig(struct config *conf)
}
conf->webserver.interface.theme.t = CONF_ENUM_WEB_THEME;
conf->webserver.interface.theme.d.web_theme = THEME_DEFAULT_AUTO;
conf->webserver.interface.theme.c = validate_stub; // Only type-based checking
// sub-struct api
conf->webserver.api.searchAPIauth.k = "webserver.api.searchAPIauth";
conf->webserver.api.searchAPIauth.h = "Do local clients need to authenticate to access the search API? This settings allows local clients to use pihole -q ... without authentication. Note that \"local\" in the sense of the option means only 127.0.0.1 and [::1]";
conf->webserver.api.searchAPIauth.t = CONF_BOOL;
conf->webserver.api.searchAPIauth.d.b = false;
conf->webserver.api.searchAPIauth.c = validate_stub; // Only type-based checking
conf->webserver.api.localAPIauth.k = "webserver.api.localAPIauth";
conf->webserver.api.localAPIauth.h = "Do local clients need to authenticate to access the API? This settings allows local clients to use the API without authentication.";
conf->webserver.api.localAPIauth.t = CONF_BOOL;
conf->webserver.api.localAPIauth.d.b = true;
conf->webserver.api.localAPIauth.c = validate_stub; // Only type-based checking
conf->webserver.api.max_sessions.k = "webserver.api.max_sessions";
conf->webserver.api.max_sessions.h = "Number of concurrent sessions allowed for the API. If the number of sessions exceeds this value, no new sessions will be allowed until the number of sessions drops due to session expiration or logout. Note that the number of concurrent sessions is irrelevant if authentication is disabled as no sessions are used in this case.";
conf->webserver.api.max_sessions.t = CONF_UINT16;
conf->webserver.api.max_sessions.d.u16 = 16;
conf->webserver.api.max_sessions.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->webserver.api.max_sessions.c = validate_stub; // Only type-based checking
conf->webserver.api.prettyJSON.k = "webserver.api.prettyJSON";
conf->webserver.api.prettyJSON.h = "Should FTL prettify the API output (add extra spaces, newlines and indentation)?";
conf->webserver.api.prettyJSON.t = CONF_BOOL;
conf->webserver.api.prettyJSON.f = FLAG_ADVANCED_SETTING;
conf->webserver.api.prettyJSON.d.b = false;
conf->webserver.api.prettyJSON.c = validate_stub; // Only type-based checking
conf->webserver.api.pwhash.k = "webserver.api.pwhash";
conf->webserver.api.pwhash.h = "API password hash";
@ -914,6 +1008,7 @@ void initConfig(struct config *conf)
conf->webserver.api.pwhash.t = CONF_STRING;
conf->webserver.api.pwhash.f = FLAG_INVALIDATE_SESSIONS;
conf->webserver.api.pwhash.d.s = (char*)"";
conf->webserver.api.pwhash.c = validate_stub; // Only type-based checking
conf->webserver.api.password.k = "webserver.api.password";
conf->webserver.api.password.h = "Pi-hole web interface and API password. When set to something different than \""PASSWORD_VALUE"\", this property will compute the corresponding password hash to set webserver.api.pwhash";
@ -921,6 +1016,7 @@ void initConfig(struct config *conf)
conf->webserver.api.password.t = CONF_PASSWORD;
conf->webserver.api.password.f = FLAG_PSEUDO_ITEM | FLAG_INVALIDATE_SESSIONS;
conf->webserver.api.password.d.s = (char*)"";
conf->webserver.api.password.c = validate_stub; // Only type-based checking
conf->webserver.api.totp_secret.k = "webserver.api.totp_secret";
conf->webserver.api.totp_secret.h = "Pi-hole 2FA TOTP secret. When set to something different than \"""\", 2FA authentication will be enforced for the API and the web interface. This setting is write-only, you can not read the secret back.";
@ -928,6 +1024,7 @@ void initConfig(struct config *conf)
conf->webserver.api.totp_secret.t = CONF_STRING;
conf->webserver.api.totp_secret.f = FLAG_WRITE_ONLY | FLAG_INVALIDATE_SESSIONS;
conf->webserver.api.totp_secret.d.s = (char*)"";
conf->webserver.api.totp_secret.c = validate_stub; // Only type-based checking
conf->webserver.api.app_pwhash.k = "webserver.api.app_pwhash";
conf->webserver.api.app_pwhash.h = "Pi-hole application password.\n After you turn on two-factor (2FA) verification and set up an Authenticator app, you may run into issues if you use apps or other services that don't support two-step verification. In this case, you can create and use an app password to sign in. An app password is a long, randomly generated password that can be used instead of your regular password + TOTP token when signing in to the API. The app password can be generated through the API and will be shown only once. You can revoke the app password at any time. If you revoke the app password, be sure to generate a new one and update your app with the new password.";
@ -935,28 +1032,33 @@ void initConfig(struct config *conf)
conf->webserver.api.app_pwhash.t = CONF_STRING;
conf->webserver.api.app_pwhash.f = FLAG_INVALIDATE_SESSIONS;
conf->webserver.api.app_pwhash.d.s = (char*)"";
conf->webserver.api.app_pwhash.c = validate_stub; // Only type-based checking
conf->webserver.api.excludeClients.k = "webserver.api.excludeClients";
conf->webserver.api.excludeClients.h = "Array of clients to be excluded from certain API responses (regex):\n - Query Log (/api/queries)\n - Top Clients (/api/stats/top_clients)\n This setting accepts both IP addresses (IPv4 and IPv6) as well as hostnames.\n Note that backslashes \"\\\" need to be escaped, i.e. \"\\\\\" in this setting\n\n Example: [ \"^192\\\\.168\\\\.2\\\\.56$\", \"^fe80::341:[0-9a-f]*$\", \"^localhost$\" ]";
conf->webserver.api.excludeClients.a = cJSON_CreateStringReference("array of regular expressions describing clients");
conf->webserver.api.excludeClients.t = CONF_JSON_STRING_ARRAY;
conf->webserver.api.excludeClients.d.json = cJSON_CreateArray();
conf->webserver.api.excludeClients.c = validate_regex_array;
conf->webserver.api.excludeDomains.k = "webserver.api.excludeDomains";
conf->webserver.api.excludeDomains.h = "Array of domains to be excluded from certain API responses (regex):\n - Query Log (/api/queries)\n - Top Clients (/api/stats/top_domains)\n Note that backslashes \"\\\" need to be escaped, i.e. \"\\\\\" in this setting\n\n Example: [ \"(^|\\\\.)\\\\.google\\\\.de$\", \"\\\\.pi-hole\\\\.net$\" ]";
conf->webserver.api.excludeDomains.a = cJSON_CreateStringReference("array of regular expressions describing domains");
conf->webserver.api.excludeDomains.t = CONF_JSON_STRING_ARRAY;
conf->webserver.api.excludeDomains.d.json = cJSON_CreateArray();
conf->webserver.api.excludeDomains.c = validate_regex_array;
conf->webserver.api.maxHistory.k = "webserver.api.maxHistory";
conf->webserver.api.maxHistory.h = "How much history should be imported from the database and returned by the API [seconds]? (max 24*60*60 = 86400)";
conf->webserver.api.maxHistory.t = CONF_UINT;
conf->webserver.api.maxHistory.d.ui = MAXLOGAGE*3600;
conf->webserver.api.maxHistory.c = validate_stub; // Only type-based checking
conf->webserver.api.maxClients.k = "webserver.api.maxClients";
conf->webserver.api.maxClients.h = "Up to how many clients should be returned in the activity graph endpoint (/api/history/clients)?\n This setting can be overwritten at run-time using the parameter N. Setting this to 0 will always send all clients. Be aware that this may be challenging for the GUI if you have many (think > 1.000 clients) in your network";
conf->webserver.api.maxClients.t = CONF_UINT16;
conf->webserver.api.maxClients.d.u16 = 10;
conf->webserver.api.maxClients.c = validate_stub; // Only type-based checking
conf->webserver.api.client_history_global_max.k = "webserver.api.client_history_global_max";
conf->webserver.api.client_history_global_max.h = "How should the API compute the most active clients? If set to true, the API will return the clients with the most queries globally (within 24 hours). If set to false, the API will return the clients with the most queries per time slot individually.";
@ -967,12 +1069,14 @@ void initConfig(struct config *conf)
conf->webserver.api.allow_destructive.h = "Allow destructive API calls (e.g. deleting all queries, powering off the system, ...)";
conf->webserver.api.allow_destructive.t = CONF_BOOL;
conf->webserver.api.allow_destructive.d.b = true;
conf->webserver.api.allow_destructive.c = validate_stub; // Only type-based checking
// sub-struct webserver.api.temp
conf->webserver.api.temp.limit.k = "webserver.api.temp.limit";
conf->webserver.api.temp.limit.h = "Which upper temperature limit should be used by Pi-hole? Temperatures above this limit will be shown as \"hot\". The number specified here is in the unit defined below";
conf->webserver.api.temp.limit.t = CONF_DOUBLE;
conf->webserver.api.temp.limit.d.d = 60.0; // °C
conf->webserver.api.temp.limit.c = validate_stub; // Only type-based checking
conf->webserver.api.temp.unit.k = "webserver.api.temp.unit";
conf->webserver.api.temp.unit.h = "Which temperature unit should be used for temperatures processed by FTL?";
@ -987,7 +1091,7 @@ void initConfig(struct config *conf)
}
conf->webserver.api.temp.unit.t = CONF_ENUM_TEMP_UNIT;
conf->webserver.api.temp.unit.d.temp_unit = TEMP_UNIT_C;
conf->webserver.api.temp.unit.c = validate_stub; // Only type-based checking
// struct files
conf->files.pid.k = "files.pid";
@ -996,6 +1100,7 @@ void initConfig(struct config *conf)
conf->files.pid.t = CONF_STRING;
conf->files.pid.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.pid.d.s = (char*)"/run/pihole-FTL.pid";
conf->files.pid.c = validate_filepath;
conf->files.database.k = "files.database";
conf->files.database.h = "The location of FTL's long-term database";
@ -1003,6 +1108,7 @@ void initConfig(struct config *conf)
conf->files.database.t = CONF_STRING;
conf->files.database.f = FLAG_ADVANCED_SETTING;
conf->files.database.d.s = (char*)"/etc/pihole/pihole-FTL.db";
conf->files.database.c = validate_filepath;
conf->files.gravity.k = "files.gravity";
conf->files.gravity.h = "The location of Pi-hole's gravity database";
@ -1010,6 +1116,7 @@ void initConfig(struct config *conf)
conf->files.gravity.t = CONF_STRING;
conf->files.gravity.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.gravity.d.s = (char*)"/etc/pihole/gravity.db";
conf->files.gravity.c = validate_filepath;
conf->files.gravity_tmp.k = "files.gravity_tmp";
conf->files.gravity_tmp.h = "A temporary directory where Pi-hole can store files during gravity updates. This directory must be writable by the user running gravity (typically pihole).";
@ -1017,6 +1124,7 @@ void initConfig(struct config *conf)
conf->files.gravity_tmp.t = CONF_STRING;
conf->files.gravity_tmp.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.gravity_tmp.d.s = (char*)"/tmp";
conf->files.gravity_tmp.c = validate_stub; // Only type-based checking
conf->files.macvendor.k = "files.macvendor";
conf->files.macvendor.h = "The database containing MAC -> Vendor information for the network table";
@ -1024,6 +1132,7 @@ void initConfig(struct config *conf)
conf->files.macvendor.t = CONF_STRING;
conf->files.macvendor.f = FLAG_ADVANCED_SETTING;
conf->files.macvendor.d.s = (char*)"/etc/pihole/macvendor.db";
conf->files.macvendor.c = validate_filepath;
conf->files.setupVars.k = "files.setupVars";
conf->files.setupVars.h = "The old config file of Pi-hole used before v6.0";
@ -1031,6 +1140,7 @@ void initConfig(struct config *conf)
conf->files.setupVars.t = CONF_STRING;
conf->files.setupVars.f = FLAG_ADVANCED_SETTING;
conf->files.setupVars.d.s = (char*)"/etc/pihole/setupVars.conf";
conf->files.setupVars.c = validate_filepath;
conf->files.pcap.k = "files.pcap";
conf->files.pcap.h = "An optional file containing a pcap capture of the network traffic. This file is used for debugging purposes only. If you don't know what this is, you don't need it.\n Setting this to an empty string disables pcap recording. The file must be writable by the user running FTL (typically pihole). Failure to write to this file will prevent the DNS resolver from starting. The file is appended to if it already exists.";
@ -1038,6 +1148,10 @@ void initConfig(struct config *conf)
conf->files.pcap.t = CONF_STRING;
conf->files.pcap.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.pcap.d.s = (char*)"";
conf->files.pcap.c = validate_filepath_empty;
// sub-struct files.log
// conf->files.log.ftl is set in a separate function
conf->files.log.webserver.k = "files.log.webserver";
conf->files.log.webserver.h = "The log file used by the webserver";
@ -1045,9 +1159,7 @@ void initConfig(struct config *conf)
conf->files.log.webserver.t = CONF_STRING;
conf->files.log.webserver.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.log.webserver.d.s = (char*)"/var/log/pihole/webserver.log";
// sub-struct files.log
// conf->files.log.ftl is set in a separate function
conf->files.log.webserver.c = validate_filepath;
conf->files.log.dnsmasq.k = "files.log.dnsmasq";
conf->files.log.dnsmasq.h = "The log file used by the embedded dnsmasq DNS server";
@ -1055,6 +1167,7 @@ void initConfig(struct config *conf)
conf->files.log.dnsmasq.t = CONF_STRING;
conf->files.log.dnsmasq.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->files.log.dnsmasq.d.s = (char*)"/var/log/pihole/pihole.log";
conf->files.log.dnsmasq.c = validate_filepath_dash;
// struct misc
@ -1072,29 +1185,34 @@ void initConfig(struct config *conf)
}
conf->misc.privacylevel.t = CONF_ENUM_PRIVACY_LEVEL;
conf->misc.privacylevel.d.privacy_level = PRIVACY_SHOW_ALL;
conf->misc.privacylevel.c = validate_stub; // Only type-based checking
conf->misc.delay_startup.k = "misc.delay_startup";
conf->misc.delay_startup.h = "During startup, in some configurations, network interfaces appear only late during system startup and are not ready when FTL tries to bind to them. Therefore, you may want FTL to wait a given amount of time before trying to start the DNS revolver. This setting takes any integer value between 0 and 300 seconds. To prevent delayed startup while the system is already running and FTL is restarted, the delay only takes place within the first 180 seconds (hard-coded) after booting.";
conf->misc.delay_startup.t = CONF_UINT;
conf->misc.delay_startup.d.ui = 0;
conf->misc.delay_startup.c = validate_stub; // Only type-based checking
conf->misc.nice.k = "misc.nice";
conf->misc.nice.h = "Set niceness of pihole-FTL. Defaults to -10 and can be disabled altogether by setting a value of -999. The nice value is an attribute that can be used to influence the CPU scheduler to favor or disfavor a process in scheduling decisions. The range of the nice value varies across UNIX systems. On modern Linux, the range is -20 (high priority = not very nice to other processes) to +19 (low priority).";
conf->misc.nice.t = CONF_INT;
conf->misc.nice.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->misc.nice.d.i = -10;
conf->misc.nice.c = validate_stub; // Only type-based checking
conf->misc.addr2line.k = "misc.addr2line";
conf->misc.addr2line.h = "Should FTL translate its own stack addresses into code lines during the bug backtrace? This improves the analysis of crashed significantly. It is recommended to leave the option enabled. This option should only be disabled when addr2line is known to not be working correctly on the machine because, in this case, the malfunctioning addr2line can prevent from generating any backtrace at all.";
conf->misc.addr2line.t = CONF_BOOL;
conf->misc.addr2line.f = FLAG_ADVANCED_SETTING;
conf->misc.addr2line.d.b = true;
conf->misc.addr2line.c = validate_stub; // Only type-based checking
conf->misc.etc_dnsmasq_d.k = "misc.etc_dnsmasq_d";
conf->misc.etc_dnsmasq_d.h = "Should FTL load additional dnsmasq configuration files from /etc/dnsmasq.d/?";
conf->misc.etc_dnsmasq_d.t = CONF_BOOL;
conf->misc.etc_dnsmasq_d.f = FLAG_RESTART_FTL | FLAG_ADVANCED_SETTING;
conf->misc.etc_dnsmasq_d.d.b = false;
conf->misc.etc_dnsmasq_d.c = validate_stub; // Only type-based checking
conf->misc.dnsmasq_lines.k = "misc.dnsmasq_lines";
conf->misc.dnsmasq_lines.h = "Additional lines to inject into the generated dnsmasq configuration.\n Warning: This is an advanced setting and should only be used with care. Incorrectly formatted or duplicated lines as well as lines conflicting with the automatic configuration of Pi-hole can break the embedded dnsmasq and will stop DNS resolution from working.\n Use this option with extra care.";
@ -1102,28 +1220,33 @@ void initConfig(struct config *conf)
conf->misc.dnsmasq_lines.t = CONF_JSON_STRING_ARRAY;
conf->misc.dnsmasq_lines.f = FLAG_ADVANCED_SETTING | FLAG_RESTART_FTL;
conf->misc.dnsmasq_lines.d.json = cJSON_CreateArray();
conf->misc.dnsmasq_lines.c = validate_stub; // Type-based checking + dnsmasq syntax checking
conf->misc.extraLogging.k = "misc.extraLogging";
conf->misc.extraLogging.h = "Log additional information about queries and replies to pihole.log\n When this setting is enabled, the log has extra information at the start of each line. This consists of a serial number which ties together the log lines associated with an individual query, and the IP address of the requestor. This setting is only effective if dns.queryLogging is enabled, too. This option is only useful for debugging and is not recommended for normal use.";
conf->misc.extraLogging.t = CONF_BOOL;
conf->misc.extraLogging.f = FLAG_RESTART_FTL;
conf->misc.extraLogging.d.b = false;
conf->misc.extraLogging.c = validate_stub; // Only type-based checking
// sub-struct misc.check
conf->misc.check.load.k = "misc.check.load";
conf->misc.check.load.h = "Pi-hole is very lightweight on resources. Nevertheless, this does not mean that you should run Pi-hole on a server that is otherwise extremely busy as queuing on the system can lead to unnecessary delays in DNS operation as the system becomes less and less usable as the system load increases because all resources are permanently in use. To account for this, FTL regularly checks the system load. To bring this to your attention, FTL warns about excessive load when the 15 minute system load average exceeds the number of cores.\n This check can be disabled with this setting.";
conf->misc.check.load.t = CONF_BOOL;
conf->misc.check.load.d.b = true;
conf->misc.check.load.c = validate_stub; // Only type-based checking
conf->misc.check.disk.k = "misc.check.disk";
conf->misc.check.disk.h = "FTL stores its long-term history in a database file on disk. Furthermore, FTL stores log files. By default, FTL warns if usage of the disk holding any crucial file exceeds 90%. You can set any integer limit between 0 to 100 (interpreted as percentages) where 0 means that checking of disk usage is disabled.";
conf->misc.check.disk.t = CONF_UINT;
conf->misc.check.disk.d.ui = 90;
conf->misc.check.disk.c = validate_stub; // Only type-based checking
conf->misc.check.shmem.k = "misc.check.shmem";
conf->misc.check.shmem.h = "FTL stores history in shared memory to allow inter-process communication with forked dedicated TCP workers. If FTL runs out of memory, it cannot continue to work as queries cannot be analyzed any further. Hence, FTL checks if enough shared memory is available on your system and warns you if this is not the case.\n By default, FTL warns if the shared-memory usage exceeds 90%. You can set any integer limit between 0 to 100 (interpreted as percentages) where 0 means that checking of shared-memory usage is disabled.";
conf->misc.check.shmem.t = CONF_UINT;
conf->misc.check.shmem.d.ui = 90;
conf->misc.check.shmem.c = validate_stub; // Only type-based checking
// struct debug
@ -1132,168 +1255,196 @@ void initConfig(struct config *conf)
conf->debug.database.t = CONF_BOOL;
conf->debug.database.f = FLAG_ADVANCED_SETTING;
conf->debug.database.d.b = false;
conf->debug.database.c = validate_stub; // Only type-based checking
conf->debug.networking.k = "debug.networking";
conf->debug.networking.h = "Prints a list of the detected interfaces on the startup of pihole-FTL. Also, prints whether these interfaces are IPv4 or IPv6 interfaces.";
conf->debug.networking.t = CONF_BOOL;
conf->debug.networking.f = FLAG_ADVANCED_SETTING;
conf->debug.networking.d.b = false;
conf->debug.networking.c = validate_stub; // Only type-based checking
conf->debug.locks.k = "debug.locks";
conf->debug.locks.h = "Print information about shared memory locks. Messages will be generated when waiting, obtaining, and releasing a lock.";
conf->debug.locks.t = CONF_BOOL;
conf->debug.locks.f = FLAG_ADVANCED_SETTING;
conf->debug.locks.d.b = false;
conf->debug.locks.c = validate_stub; // Only type-based checking
conf->debug.queries.k = "debug.queries";
conf->debug.queries.h = "Print extensive query information (domains, types, replies, etc.). This has always been part of the legacy debug mode of pihole-FTL.";
conf->debug.queries.t = CONF_BOOL;
conf->debug.queries.f = FLAG_ADVANCED_SETTING;
conf->debug.queries.d.b = false;
conf->debug.queries.c = validate_stub; // Only type-based checking
conf->debug.flags.k = "debug.flags";
conf->debug.flags.h = "Print flags of queries received by the DNS hooks. Only effective when DEBUG_QUERIES is enabled as well.";
conf->debug.flags.t = CONF_BOOL;
conf->debug.flags.f = FLAG_ADVANCED_SETTING;
conf->debug.flags.d.b = false;
conf->debug.flags.c = validate_stub; // Only type-based checking
conf->debug.shmem.k = "debug.shmem";
conf->debug.shmem.h = "Print information about shared memory buffers. Messages are either about creating or enlarging shmem objects or string injections.";
conf->debug.shmem.t = CONF_BOOL;
conf->debug.shmem.f = FLAG_ADVANCED_SETTING;
conf->debug.shmem.d.b = false;
conf->debug.shmem.c = validate_stub; // Only type-based checking
conf->debug.gc.k = "debug.gc";
conf->debug.gc.h = "Print information about garbage collection (GC): What is to be removed, how many have been removed and how long did GC take.";
conf->debug.gc.t = CONF_BOOL;
conf->debug.gc.f = FLAG_ADVANCED_SETTING;
conf->debug.gc.d.b = false;
conf->debug.gc.c = validate_stub; // Only type-based checking
conf->debug.arp.k = "debug.arp";
conf->debug.arp.h = "Print information about ARP table processing: How long did parsing take, whether read MAC addresses are valid, and if the macvendor.db file exists.";
conf->debug.arp.t = CONF_BOOL;
conf->debug.arp.f = FLAG_ADVANCED_SETTING;
conf->debug.arp.d.b = false;
conf->debug.arp.c = validate_stub; // Only type-based checking
conf->debug.regex.k = "debug.regex";
conf->debug.regex.h = "Controls if FTLDNS should print extended details about regex matching into FTL.log.";
conf->debug.regex.t = CONF_BOOL;
conf->debug.regex.f = FLAG_ADVANCED_SETTING;
conf->debug.regex.d.b = false;
conf->debug.regex.c = validate_stub; // Only type-based checking
conf->debug.api.k = "debug.api";
conf->debug.api.h = "Print extra debugging information concerning API calls. This includes the request, the request parameters, and the internal details about how the algorithms decide which data to present and in what form. This very verbose output should only be used when debugging specific API issues and can be helpful, e.g., when a client cannot connect due to an obscure API error. Furthermore, this setting enables logging of all API requests (auth log) and details about user authentication attempts.";
conf->debug.api.t = CONF_BOOL;
conf->debug.api.f = FLAG_ADVANCED_SETTING;
conf->debug.api.d.b = false;
conf->debug.api.c = validate_stub; // Only type-based checking
conf->debug.tls.k = "debug.tls";
conf->debug.tls.h = "Print extra debugging information about TLS connections. This includes the TLS version, the cipher suite, the certificate chain and much more. This very verbose output should only be used when debugging specific TLS issues and can be helpful, e.g., when a client cannot connect due to an obscure TLS error as modern browsers do not provide much information about the underlying TLS connection and most often give only very generic error messages without much/any underlying technical information.";
conf->debug.tls.t = CONF_BOOL;
conf->debug.tls.f = FLAG_ADVANCED_SETTING;
conf->debug.tls.d.b = false;
conf->debug.tls.c = validate_stub; // Only type-based checking
conf->debug.overtime.k = "debug.overtime";
conf->debug.overtime.h = "Print information about overTime memory operations, such as initializing or moving overTime slots.";
conf->debug.overtime.t = CONF_BOOL;
conf->debug.overtime.f = FLAG_ADVANCED_SETTING;
conf->debug.overtime.d.b = false;
conf->debug.overtime.c = validate_stub; // Only type-based checking
conf->debug.status.k = "debug.status";
conf->debug.status.h = "Print information about status changes for individual queries. This can be useful to identify unexpected unknown queries.";
conf->debug.status.t = CONF_BOOL;
conf->debug.status.f = FLAG_ADVANCED_SETTING;
conf->debug.status.d.b = false;
conf->debug.status.c = validate_stub; // Only type-based checking
conf->debug.caps.k = "debug.caps";
conf->debug.caps.h = "Print information about capabilities granted to the pihole-FTL process. The current capabilities are printed on receipt of SIGHUP, i.e., the current set of capabilities can be queried without restarting pihole-FTL (by setting DEBUG_CAPS=true and thereafter sending killall -HUP pihole-FTL).";
conf->debug.caps.t = CONF_BOOL;
conf->debug.caps.f = FLAG_ADVANCED_SETTING;
conf->debug.caps.d.b = false;
conf->debug.caps.c = validate_stub; // Only type-based checking
conf->debug.dnssec.k = "debug.dnssec";
conf->debug.dnssec.h = "Print information about DNSSEC activity";
conf->debug.dnssec.t = CONF_BOOL;
conf->debug.dnssec.f = FLAG_ADVANCED_SETTING;
conf->debug.dnssec.d.b = false;
conf->debug.dnssec.c = validate_stub; // Only type-based checking
conf->debug.vectors.k = "debug.vectors";
conf->debug.vectors.h = "FTL uses dynamically allocated vectors for various tasks. This config option enables extensive debugging information such as information about allocation, referencing, deletion, and appending.";
conf->debug.vectors.t = CONF_BOOL;
conf->debug.vectors.f = FLAG_ADVANCED_SETTING;
conf->debug.vectors.d.b = false;
conf->debug.vectors.c = validate_stub; // Only type-based checking
conf->debug.resolver.k = "debug.resolver";
conf->debug.resolver.h = "Extensive information about hostname resolution like which DNS servers are used in the first and second hostname resolving tries (only affecting internally generated PTR queries).";
conf->debug.resolver.t = CONF_BOOL;
conf->debug.resolver.f = FLAG_ADVANCED_SETTING;
conf->debug.resolver.d.b = false;
conf->debug.resolver.c = validate_stub; // Only type-based checking
conf->debug.edns0.k = "debug.edns0";
conf->debug.edns0.h = "Print debugging information about received EDNS(0) data.";
conf->debug.edns0.t = CONF_BOOL;
conf->debug.edns0.f = FLAG_ADVANCED_SETTING;
conf->debug.edns0.d.b = false;
conf->debug.edns0.c = validate_stub; // Only type-based checking
conf->debug.clients.k = "debug.clients";
conf->debug.clients.h = "Log various important client events such as change of interface (e.g., client switching from WiFi to wired or VPN connection), as well as extensive reporting about how clients were assigned to its groups.";
conf->debug.clients.t = CONF_BOOL;
conf->debug.clients.f = FLAG_ADVANCED_SETTING;
conf->debug.clients.d.b = false;
conf->debug.clients.c = validate_stub; // Only type-based checking
conf->debug.aliasclients.k = "debug.aliasclients";
conf->debug.aliasclients.h = "Log information related to alias-client processing.";
conf->debug.aliasclients.t = CONF_BOOL;
conf->debug.aliasclients.f = FLAG_ADVANCED_SETTING;
conf->debug.aliasclients.d.b = false;
conf->debug.aliasclients.c = validate_stub; // Only type-based checking
conf->debug.events.k = "debug.events";
conf->debug.events.h = "Log information regarding FTL's embedded event handling queue.";
conf->debug.events.t = CONF_BOOL;
conf->debug.events.f = FLAG_ADVANCED_SETTING;
conf->debug.events.d.b = false;
conf->debug.events.c = validate_stub; // Only type-based checking
conf->debug.helper.k = "debug.helper";
conf->debug.helper.h = "Log information about script helpers, e.g., due to dhcp-script.";
conf->debug.helper.t = CONF_BOOL;
conf->debug.helper.f = FLAG_ADVANCED_SETTING;
conf->debug.helper.d.b = false;
conf->debug.helper.c = validate_stub; // Only type-based checking
conf->debug.config.k = "debug.config";
conf->debug.config.h = "Print config parsing details";
conf->debug.config.t = CONF_BOOL;
conf->debug.config.f = FLAG_ADVANCED_SETTING;
conf->debug.config.d.b = false;
conf->debug.config.c = validate_stub; // Only type-based checking
conf->debug.inotify.k = "debug.inotify";
conf->debug.inotify.h = "Debug monitoring of /etc/pihole filesystem events";
conf->debug.inotify.t = CONF_BOOL;
conf->debug.inotify.f = FLAG_ADVANCED_SETTING;
conf->debug.inotify.d.b = false;
conf->debug.inotify.c = validate_stub; // Only type-based checking
conf->debug.webserver.k = "debug.webserver";
conf->debug.webserver.h = "Debug monitoring of the webserver (CivetWeb) events";
conf->debug.webserver.t = CONF_BOOL;
conf->debug.webserver.f = FLAG_ADVANCED_SETTING;
conf->debug.webserver.d.b = false;
conf->debug.webserver.c = validate_stub; // Only type-based checking
conf->debug.extra.k = "debug.extra";
conf->debug.extra.h = "Temporary flag that may print additional information. This debug flag is meant to be used whenever needed for temporary investigations. The logged content may change without further notice at any time.";
conf->debug.extra.t = CONF_BOOL;
conf->debug.extra.f = FLAG_ADVANCED_SETTING;
conf->debug.extra.d.b = false;
conf->debug.extra.c = validate_stub; // Only type-based checking
conf->debug.reserved.k = "debug.reserved";
conf->debug.reserved.h = "Reserved debug flag";
conf->debug.reserved.t = CONF_BOOL;
conf->debug.reserved.f = FLAG_ADVANCED_SETTING;
conf->debug.reserved.d.b = false;
conf->debug.reserved.c = validate_stub; // Only type-based checking
conf->debug.all.k = "debug.all";
conf->debug.all.h = "Set all debug flags at once. This is a convenience option to enable all debug flags at once. Note that this option is not persistent, setting it to true will enable all *remaining* debug flags but unsetting it will disable *all* debug flags.";
conf->debug.all.t = CONF_ALL_DEBUG_BOOL;
conf->debug.all.f = FLAG_ADVANCED_SETTING;
conf->debug.all.d.b = false;
conf->debug.all.c = validate_stub; // Only type-based checking
// Post-processing:
// Initialize and verify config data
@ -1343,6 +1494,13 @@ void initConfig(struct config *conf)
log_err("Config option %s has NULL default JSON array!", conf_item->k);
continue;
}
// Verify that all config options have a validator function
if(conf_item->c == NULL)
{
log_err("Config option %s has no validator function!", conf_item->k);
continue;
}
}
}
@ -1491,6 +1649,7 @@ bool getLogFilePath(void)
config.files.log.ftl.f = FLAG_ADVANCED_SETTING;
config.files.log.ftl.d.s = (char*)"/var/log/pihole/FTL.log";
config.files.log.ftl.v.s = config.files.log.ftl.d.s;
config.files.log.ftl.c = validate_filepath;
// Check if the config file contains a different path
if(!getLogFilePathTOML())

View File

@ -38,6 +38,9 @@
// characters will be replaced by their UTF-8 escape sequences (UCS-2)
#define TOML_UTF8
// Size of the buffer used to report possible errors during config validation
#define VALIDATOR_ERRBUF_LEN 256
// Location of the legacy (pre-v6.0) config file
#define GLOBALCONFFILE_LEGACY "/etc/pihole/pihole-FTL.conf"
@ -109,6 +112,7 @@ struct conf_item {
uint8_t f; // additional Flags
union conf_value v; // current Value
union conf_value d; // Default value
bool (*c)(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]); // Function pointer to validate the value
};
struct enum_options {
@ -202,6 +206,7 @@ struct config {
struct conf_item DBexport;
struct conf_item maxDBdays;
struct conf_item DBinterval;
struct conf_item useWAL;
struct {
struct conf_item parseARPcache;
struct conf_item expire;

View File

@ -473,7 +473,7 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
if(active == NULL || cidr == NULL || target == NULL || domain == NULL)
{
log_err("Invalid reverse server string: %s", revServer->valuestring);
log_err("Skipped invalid dns.revServers[%u]: %s", i, revServer->valuestring);
free(copy);
continue;
}
@ -658,6 +658,15 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
}
}
// Add ANY filtering
fputs("# RFC 8482: Providing Minimal-Sized Responses to DNS Queries That Have QTYPE=ANY\n", pihole_conf);
fputs("# Filters replies to queries for type ANY. Everything other than A, AAAA, MX and CNAME\n", pihole_conf);
fputs("# records are removed. Since ANY queries with forged source addresses can be used in DNS amplification attacks\n", pihole_conf);
fputs("# replies to ANY queries can be large) this defangs such attacks, whilst still supporting the\n", pihole_conf);
fputs("# one remaining possible use of ANY queries. See RFC 8482 para 4.3 for details.\n", pihole_conf);
fputs("filter-rr=ANY\n", pihole_conf);
fputs("\n", pihole_conf);
// Add additional config lines to disk (if present)
if(conf->misc.dnsmasq_lines.v.json != NULL &&
cJSON_GetArraySize(conf->misc.dnsmasq_lines.v.json) > 0)
@ -697,6 +706,14 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
if(test_config && !test_dnsmasq_config(errbuf))
{
log_warn("New dnsmasq configuration is not valid (%s), config remains unchanged", errbuf);
// Remove temporary config file
if(remove(DNSMASQ_TEMP_CONF) != 0)
{
log_err("Cannot remove temporary dnsmasq config file: %s", strerror(errno));
return false;
}
return false;
}
@ -707,8 +724,14 @@ bool __attribute__((const)) write_dnsmasq_config(struct config *conf, bool test_
if(rename(DNSMASQ_TEMP_CONF, DNSMASQ_PH_CONFIG) != 0)
{
log_err("Cannot install dnsmasq config file: %s", strerror(errno));
// Remove temporary config file
if(remove(DNSMASQ_TEMP_CONF) != 0)
log_err("Cannot remove temporary dnsmasq config file: %s", strerror(errno));
return false;
}
log_debug(DEBUG_CONFIG, "Config file written to "DNSMASQ_PH_CONFIG);
}
else

View File

@ -48,6 +48,13 @@ void getEnvVars(void)
char *key = strtok(*env, "=");
char *value = strtok(NULL, "=");
// Log warning if value is missing
if(value == NULL)
{
log_warn("Environment variable %s has no value, substituting with empty string", key);
value = (char*)"";
}
// Add to list
struct env_item *new_item = calloc(1, sizeof(struct env_item));
new_item->used = false;

View File

@ -175,14 +175,17 @@ static void get_revServer_from_setupVars(void)
// Free memory, harmless to call if read_setupVarsconf() didn't return a result
clearSetupVarsArray();
// Only add the entry if all values are present and active
if(active && cidr != NULL && target != NULL && domain != NULL)
{
// Build comma-separated string of all values
char *old = calloc(strlen(active_str) + strlen(cidr) + strlen(target) + strlen(domain) + 4, sizeof(char));
// 8 = 3 commas, "true", and null terminator
char *old = calloc(strlen(cidr) + strlen(target) + strlen(domain) + 8, sizeof(char));
if(old)
{
// Add to new config
sprintf(old, "%s,%s,%s,%s", active_str, cidr, target, domain);
// active is always true as we only add active entries
sprintf(old, "true,%s,%s,%s", cidr, target, domain);
cJSON_AddItemToArray(config.dns.revServers.v.json, cJSON_CreateString(old));
free(old);
}

View File

@ -30,8 +30,8 @@
static toml_table_t *parseTOML(const unsigned int version);
static void reportDebugFlags(void);
// Migrate config from old to new, returns true if a restart is required
static bool migrate_config(toml_table_t *toml, struct config *newconf)
// Migrate dns.revServer -> dns.revServers[0]
static bool migrate_dns_revServer(toml_table_t *toml, struct config *newconf)
{
bool restart = false;
toml_table_t *dns = toml_table_in(toml, "dns");
@ -55,19 +55,48 @@ static bool migrate_config(toml_table_t *toml, struct config *newconf)
{
// Add to new config
sprintf(old, "%s,%s,%s,%s", active.u.s ? "true" : "false", cidr.u.s, target.u.s, domain.u.s);
log_debug(DEBUG_CONFIG, "Config setting dns.revServer MIGRATED: %s", old);
log_debug(DEBUG_CONFIG, "Config setting dns.revServer MIGRATED to dns.revServers[0]: %s", old);
cJSON_AddItemToArray(newconf->dns.revServers.v.json, cJSON_CreateString(old));
restart = true;
}
}
else
log_warn("Config setting dns.revServer INVALID - ignoring: %s %s %s %s", active.ok ? active.u.s : "NULL", cidr.ok ? cidr.u.s : "NULL", target.ok ? target.u.s : "NULL", domain.ok ? domain.u.s : "NULL");
{
// Invalid config - ignored but logged in case
// the user wants to know and restore it later
// manually after fixing whatever the problem is
log_warn("Config setting dns.revServer INVALID - ignoring: %s %s %s %s",
active.ok ? active.u.s : "NULL",
cidr.ok ? cidr.u.s : "NULL",
target.ok ? target.u.s : "NULL",
domain.ok ? domain.u.s : "NULL");
}
}
else
log_info("dns.revServer DOES NOT EXIST");
{
// Perfectly fine - it just means this old option does
// not exist and, hence, does not need to be migrated
log_debug(DEBUG_CONFIG, "dns.revServer does not exist - nothing to migrate");
}
}
else
log_info("dns DOES NOT EXIST");
{
// This is actually a problem as the old config file
// should always contain a "dns" section
log_warn("dns config tab does not exist - config file corrupt or incomplete");
}
return restart;
}
// Migrate config from old to new, returns true if a restart is required to
// apply the changes
static bool migrate_config(toml_table_t *toml, struct config *newconf)
{
bool restart = false;
// Migrate dns.revServer -> dns.revServers[0]
restart |= migrate_dns_revServer(toml, newconf);
return restart;
}
@ -86,17 +115,10 @@ bool readFTLtoml(struct config *oldconf, struct config *newconf,
return false;
}
// Check if we are in Adam mode
// (only read the env vars)
const char *envvar = getenv("FTLCONF_ENV_ONLY");
const bool adam_mode = (envvar != NULL &&
(strcmp(envvar, "true") == 0 ||
strcmp(envvar, "yes") == 0));
// Try to read debug config. This is done before the full config
// parsing to allow for debug output further down
// First try to read env variable, if this fails, read TOML
if((teleporter || !readEnvValue(&newconf->debug.config, newconf)) && !adam_mode)
if(teleporter || !readEnvValue(&newconf->debug.config, newconf))
{
toml_table_t *conf_debug = toml_table_in(toml, "debug");
if(conf_debug)
@ -124,10 +146,6 @@ bool readFTLtoml(struct config *oldconf, struct config *newconf,
continue;
}
// Do not read TOML file when in Adam mode
if(adam_mode)
continue;
// Get config path depth
unsigned int level = config_path_depth(new_conf_item->p);

556
src/config/validator.c Normal file
View File

@ -0,0 +1,556 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2023 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* FTL Engine
* Config validation routines
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
#include "validator.h"
#include "log.h"
// valid_domain()
#include "tools/gravity-parseList.h"
// regex
#include "regex_r.h"
// Stub validator for config types that need to dedicated validation as they can
// be tested by their type only (e.g., integers, strings, booleans, enums, etc.)
bool __attribute__((const)) validate_stub(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
{
return true;
}
// Validate the dns.hosts array
// Each entry needs to be a string in form "IP HOSTNAME"
bool validate_dns_hosts(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
{
if(!cJSON_IsArray(val->json))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not an array", key);
return false;
}
for(int i = 0; i < cJSON_GetArraySize(val->json); i++)
{
// Get array item
cJSON *item = cJSON_GetArrayItem(val->json, i);
// Check if it's a string
if(!cJSON_IsString(item))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a string",
key, i);
return false;
}
// Check if it's in the form "IP HOSTNAME"
char *str = strdup(item->valuestring);
char *tmp = str;
char *ip = strsep(&tmp, " ");
if(!ip || !*ip)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not an IP address (\"%s\")",
key, i, item->valuestring);
free(str);
return false;
}
// Check if IP is valid
struct in_addr addr;
struct in6_addr addr6;
if(inet_pton(AF_INET, ip, &addr) != 1 && inet_pton(AF_INET6, ip, &addr6) != 1)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: neither a valid IPv4 nor IPv6 address (\"%s\")",
key, i, ip);
free(str);
return false;
}
// Check if all hostnames are valid
// The HOSTS format allows any number of space-separated
// hostnames to come after the IP address
unsigned int hosts = 0;
char *host = NULL;
while((host = strsep(&tmp, " ")) != NULL)
{
if(!valid_domain(host, strlen(host), false))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: invalid hostname (\"%s\")",
key, i, host);
free(str);
return false;
}
hosts++;
}
// Check if there is at least one hostname in this record
if(hosts < 1)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: entry does not have at least one hostname (\"%s\")",
key, i, item->valuestring);
free(str);
return false;
}
free(str);
}
return true;
}
// Validate the dns.cnames array
// Each entry needs to be a string in form "<cname>,[<cname>,]<target>[,<TTL>]"
bool validate_dns_cnames(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
{
if(!cJSON_IsArray(val->json))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not an array", key);
return false;
}
for(int i = 0; i < cJSON_GetArraySize(val->json); i++)
{
// Get array item
cJSON *item = cJSON_GetArrayItem(val->json, i);
// Check if it's a string
if(!cJSON_IsString(item))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a string", key, i);
return false;
}
// Count the number of elements in the string
unsigned int elements = 1;
for(unsigned int j = 0; j < strlen(item->valuestring); j++)
if(item->valuestring[j] == ',')
elements++;
// Check if it's in the form "<cname>,[<cnameX>,]<target>[,<TTL>]"
// <cnameX> is optional and may be repeated
char *str = strdup(item->valuestring);
char *tmp = str, *s = NULL;
unsigned int j = 0;
while((s = strsep(&tmp, ",")) != NULL)
{
// Check if it's a valid cname
if(strlen(s) == 0)
{
// Contains an empty string
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: contains an empty string at position %u", key, i, j);
free(str);
return false;
}
j++;
}
free(str);
// Check if there are at least one cname and a target
if(j < 2)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a valid CNAME definition (too few elements)", key, i);
return false;
}
}
return true;
}
// Validate IPs in CIDR notation
bool validate_cidr(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
{
// Check if it's a valid CIDR
char *str = strdup(val->s);
char *tmp = str;
char *ip = strsep(&tmp, "/");
char *cidr = strsep(&tmp, "/");
char *tail = strsep(&tmp, "/");
// Check if there is an IP and no tail
if(!ip || !*ip || tail)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid IP in CIDR notation (\"%s\")", key, val->s);
free(str);
return false;
}
// Check if IP is valid
struct in_addr addr;
struct in6_addr addr6;
int ip4 = 0, ip6 = 0;
if((ip4 = inet_pton(AF_INET, ip, &addr) != 1) && (ip6 = inet_pton(AF_INET6, ip, &addr6)) != 1)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid IPv4 nor IPv6 address (\"%s\")", key, ip);
free(str);
return false;
}
// Check if CIDR is valid
if(cidr)
{
if(strlen(cidr) == 0)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: empty CIDR value", key);
free(str);
return false;
}
int cidr_int = atoi(cidr);
if(ip4 && (cidr_int < 0 || cidr_int > 32))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid IPv4 CIDR (\"%s\")", key, cidr);
free(str);
return false;
}
else if(ip6 && (cidr_int < 0 || cidr_int > 128))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid IPv6 CIDR (\"%s\")", key, cidr);
free(str);
return false;
}
}
free(str);
return true;
}
// Validate IP address optionally followed by a port (separator is "#")
bool validate_ip_port(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
{
// Check if it's a valid IP
char *str = strdup(val->s);
char *tmp = str;
char *ip = strsep(&tmp, "#");
char *port = strsep(&tmp, "#");
char *tail = strsep(&tmp, "#");
// Check if there is an IP and no tail
if(!ip || !*ip || tail)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid IP (\"%s\")", key, val->s);
free(str);
return false;
}
// Check if IP is valid
struct in_addr addr;
struct in6_addr addr6;
int ip4 = 0, ip6 = 0;
if((ip4 = inet_pton(AF_INET, ip, &addr) != 1) && (ip6 = inet_pton(AF_INET6, ip, &addr6)) != 1)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid IPv4 nor IPv6 address (\"%s\")", key, ip);
free(str);
return false;
}
// Check if port is valid
if(port)
{
if(strlen(port) == 0)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: empty port value", key);
free(str);
return false;
}
int port_int = atoi(port);
if(port_int < 0 || port_int > 65535)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid port (\"%s\")", key, port);
free(str);
return false;
}
}
free(str);
return true;
}
// Validate domain
bool validate_domain(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
{
// Check if domain is valid
if(!valid_domain(val->s, strlen(val->s), false))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid domain (\"%s\")", key, val->s);
return false;
}
return true;
}
// Validate file path
bool validate_filepath(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
{
// Check if the path contains only valid characters
for(unsigned int i = 0; i < strlen(val->s); i++)
{
if(!isalnum(val->s[i]) && val->s[i] != '/' && val->s[i] != '.' && val->s[i] != '-' && val->s[i] != '_' && val->s[i] != ' ')
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not a valid file path (\"%s\")", key, val->s);
return false;
}
}
return true;
}
// Validate file path (empty allowed)
bool validate_filepath_empty(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
{
// Empty paths are allowed, e.g., to disable a feature like PCAP
if(strlen(val->s) == 0)
return true;
// else:
return validate_filepath(val, key, err);
}
// Validate file path (dash allowed), used by files.log.dnsmasq
bool validate_filepath_dash(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
{
// Dash is allowed, this enabled printing to stderr
if(strlen(val->s) == 1 && val->s[0] == '-')
return true;
// else:
return validate_filepath(val, key, err);
}
// Validate a single regular expression
static bool validate_regex(const char *regex, char err[VALIDATOR_ERRBUF_LEN])
{
// Compile regex
regex_t preg = { 0 };
const int ret = regcomp(&preg, regex, REG_EXTENDED);
if(ret != 0)
{
regerror(ret, &preg, err, VALIDATOR_ERRBUF_LEN);
regfree(&preg);
return false;
}
// Free regex
regfree(&preg);
return true;
}
// Validate array of regexes
bool validate_regex_array(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
{
if(val == NULL || !cJSON_IsArray(val->json))
{
strncat(err, "%s: not an array", VALIDATOR_ERRBUF_LEN);
return false;
}
for(int i = 0; i < cJSON_GetArraySize(val->json); i++)
{
// Get array item
cJSON *item = cJSON_GetArrayItem(val->json, i);
// Check if it's a string
if(!cJSON_IsString(item))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a string",
key, i);
return false;
}
// Check if it's a valid regex
char errbuf[VALIDATOR_ERRBUF_LEN] = { 0 };
if(!validate_regex(item->valuestring, errbuf))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a valid regex (\"%s\"): %s",
key, i, item->valuestring, errbuf);
return false;
}
}
return true;
}
// Validate dns.revServers array
// Each entry has to be of form "<enabled>,<ip-address>[/<prefix-len>],<server>[#<port>],<domain>"
bool validate_dns_revServers(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN])
{
// Check if it's an array
if(!cJSON_IsArray(val->json))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s: not an array", key);
return false;
}
// Iterate over all array items
for(int i = 0; i < cJSON_GetArraySize(val->json); i++)
{
// Get array item
cJSON *item = cJSON_GetArrayItem(val->json, i);
// Check if it's a string
if(!cJSON_IsString(item))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: not a string", key, i);
return false;
}
// Count the number of elements in the string
unsigned int elements = 1;
for(unsigned int j = 0; j < strlen(item->valuestring); j++)
if(item->valuestring[j] == ',')
elements++;
// Check if it's in the form "<enabled>,<ip-address>[/<prefix-len>],<server>[#<port>],<domain>"
// Mandatory elements are: <enabled>, <ip-address>, <server>, and <domain>
// Optional elements are: [/<prefix-len>] and [#<port>]
char *str = strdup(item->valuestring);
char *tmp = str, *s = NULL;
unsigned int e = 0;
while((s = strsep(&tmp, ",")) != NULL)
{
// Check if it's a valid element
if(strlen(s) == 0)
{
// Contains an empty string
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: contains two commas following each other immediately", key, i);
free(str);
return false;
}
// Check if the zeroth element is a boolean
if(e == 0)
{
if(strcmp(s, "true") != 0 && strcmp(s, "false") != 0)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: <enabled> not a boolean (\"%s\")", key, i, s);
free(str);
return false;
}
}
// Check if the first element is an IP address
else if(e == 1)
{
// Extract IP and prefix length (if present)
char *ip = strsep(&s, "/");
char *prefix = strsep(&s, "/");
if(strlen(ip) == 0)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: <ip-address> empty", key, i);
free(str);
return false;
}
// Check if IP is valid
struct in_addr addr = { 0 };
struct in6_addr addr6 = { 0 };
const bool ipv4 = inet_pton(AF_INET, ip, &addr) == 1;
const bool ipv6 = inet_pton(AF_INET6, ip, &addr6) == 1;
if(!ipv4 && !ipv6)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: <ip-address> neither a valid IPv4 nor IPv6 address (\"%s\")", key, i, ip);
free(str);
return false;
}
// Check if prefix length is valid (if present)
if(prefix != NULL)
{
if(strlen(prefix) == 0)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: <prefix-len> empty", key, i);
free(str);
return false;
}
const int prefix_int = atoi(prefix);
if(prefix_int < 0 || (ipv4 && prefix_int > 32) || (ipv6 && prefix_int > 128))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: <prefix-len> not a valid %sprefix length (\"%s\")",
key, i, ipv4 ? "IPv4 " : ipv6 ? "IPv6 " : "", prefix);
free(str);
return false;
}
}
}
// Check if the second element is a valid server (either an IP address or a domain, optionally with a port)
else if(e == 2)
{
// Extract server and port (if present)
char *server = strsep(&s, "#");
char *port = strsep(&s, "#");
if(strlen(server) == 0)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: <server> empty", key, i);
free(str);
return false;
}
struct in_addr addr = { 0 };
struct in6_addr addr6 = { 0 };
const bool server_ipv4 = inet_pton(AF_INET, server, &addr) == 1;
const bool server_ipv6 = inet_pton(AF_INET6, server, &addr6) == 1;
const bool server_domain = valid_domain(server, strlen(server), false);
// Check if server is valid
if(!server_ipv4 && !server_ipv6 && !server_domain)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: <server> neither a valid domain nor an IPv4 or IPv6 address (\"%s\")", key, i, server);
free(str);
return false;
}
// Check if port is valid (if present)
if(port != NULL)
{
if(strlen(port) == 0)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: specified server <port> empty", key, i);
free(str);
return false;
}
const int port_int = atoi(port);
if(port_int < 0 || port_int > 65535)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: server <port> not a valid port (\"%s\")", key, i, port);
free(str);
return false;
}
}
}
// Check if the third element is a valid domain
else if(e == 3)
{
if(!valid_domain(s, strlen(s), false))
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: <domain> not a valid domain (\"%s\")", key, i, s);
free(str);
return false;
}
}
// Check if there are too many elements
else
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: too many elements", key, i);
free(str);
return false;
}
// Increment element counter
e++;
}
// Check if there are all required elements
if(e < 4)
{
snprintf(err, VALIDATOR_ERRBUF_LEN, "%s[%d]: entry does not have all required elements (<enabled>,<ip-address>[/<prefix-len>],<server>[#<port>],<domain>)", key, i);
free(str);
return false;
}
}
// Return success
return true;
}

29
src/config/validator.h Normal file
View File

@ -0,0 +1,29 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2023 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* FTL Engine
* Config validation routines
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
#ifndef CONFIG_VALIDATOR_H
#define CONFIG_VALIDATOR_H
#include "FTL.h"
#include "config/config.h"
bool validate_stub(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]) __attribute__((const));
bool validate_dns_hosts(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
bool validate_dns_cnames(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
bool validate_cidr(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
bool validate_ip_port(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
bool validate_domain(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
bool validate_filepath(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
bool validate_filepath_empty(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
bool validate_filepath_dash(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
bool validate_regex_array(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
bool validate_dns_revServers(union conf_value *val, const char *key, char err[VALIDATOR_ERRBUF_LEN]);
#endif // CONFIG_VALIDATOR_H

View File

@ -1575,6 +1575,8 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
// Nothing to be done for these tables
case GRAVITY_GROUPS:
case GRAVITY_ADLISTS:
case GRAVITY_ADLISTS_BLOCK:
case GRAVITY_ADLISTS_ALLOW:
case GRAVITY_CLIENTS:
break;
@ -1597,7 +1599,9 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
querystr = "INSERT INTO \"group\" (name,enabled,description) VALUES (:item,:enabled,:comment);";
}
else if(listtype == GRAVITY_ADLISTS)
else if(listtype == GRAVITY_ADLISTS ||
listtype == GRAVITY_ADLISTS_BLOCK ||
listtype == GRAVITY_ADLISTS_ALLOW)
{
querystr = "INSERT INTO adlist (address,enabled,comment,type) VALUES (:item,:enabled,:comment,:type);";
}
@ -1605,7 +1609,7 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
{
querystr = "INSERT INTO client (ip,comment) VALUES (:item,:comment);";
}
else // domainlis
else // domainlist
{
querystr = "INSERT INTO domainlist (domain,type,enabled,comment) VALUES (:item,:type,:enabled,:comment);";
}
@ -1625,9 +1629,11 @@ bool gravityDB_addToTable(const enum gravity_list_type listtype, tablerow *row,
querystr = "UPDATE \"group\" SET name = :name, enabled = :enabled, description = :comment "
"WHERE name = :item";
}
else if(listtype == GRAVITY_ADLISTS)
else if(listtype == GRAVITY_ADLISTS ||
listtype == GRAVITY_ADLISTS_BLOCK ||
listtype == GRAVITY_ADLISTS_ALLOW)
querystr = "INSERT INTO adlist (address,enabled,comment,type) VALUES (:item,:enabled,:comment,:type) "\
"ON CONFLICT(address) DO UPDATE SET enabled = :enabled, comment = :comment, type = :type;";
"ON CONFLICT(address,type) DO UPDATE SET enabled = :enabled, comment = :comment, type = :type;";
else if(listtype == GRAVITY_CLIENTS)
querystr = "INSERT INTO client (ip,comment) VALUES (:item,:comment) "\
"ON CONFLICT(ip) DO UPDATE SET comment = :comment;";
@ -1825,11 +1831,14 @@ bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON*
return false;
}
const bool isDomain = listtype == GRAVITY_DOMAINLIST_ALLOW_EXACT ||
listtype == GRAVITY_DOMAINLIST_DENY_EXACT ||
listtype == GRAVITY_DOMAINLIST_ALLOW_REGEX ||
listtype == GRAVITY_DOMAINLIST_DENY_REGEX ||
listtype == GRAVITY_DOMAINLIST_ALL_ALL; // batch delete
const bool hasType = listtype == GRAVITY_DOMAINLIST_ALLOW_EXACT ||
listtype == GRAVITY_DOMAINLIST_DENY_EXACT ||
listtype == GRAVITY_DOMAINLIST_ALLOW_REGEX ||
listtype == GRAVITY_DOMAINLIST_DENY_REGEX ||
listtype == GRAVITY_DOMAINLIST_ALL_ALL ||
listtype == GRAVITY_ADLISTS ||
listtype == GRAVITY_ADLISTS_BLOCK ||
listtype == GRAVITY_ADLISTS_ALLOW;
// Begin transaction
const char *querystr = "BEGIN TRANSACTION;";
@ -1843,7 +1852,7 @@ bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON*
}
// Create temporary table for JSON argument
if(isDomain)
if(hasType)
// Create temporary table for domains to be deleted
querystr = "CREATE TEMPORARY TABLE deltable (type INT, item TEXT);";
else
@ -1885,7 +1894,7 @@ bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON*
sqlite3_finalize(stmt);
// Prepare statement for inserting items into virtual table
if(isDomain)
if(hasType)
querystr = "INSERT INTO deltable (type, item) VALUES (:type, :item);";
else
querystr = "INSERT INTO deltable (item) VALUES (:item);";
@ -1910,12 +1919,24 @@ bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON*
{
// Bind type to prepared statement
cJSON *type = cJSON_GetObjectItemCaseSensitive(it, "type");
int type_int = cJSON_IsNumber(type) ? type->valueint : -1;
if(listtype == GRAVITY_ADLISTS_BLOCK)
type_int = ADLIST_BLOCK;
else if(listtype == GRAVITY_ADLISTS_ALLOW)
type_int = ADLIST_ALLOW;
else if(listtype == GRAVITY_ADLISTS && cJSON_IsString(type))
{
if(strcasecmp(type->valuestring, "block") == 0)
type_int = ADLIST_BLOCK;
else if(strcasecmp(type->valuestring, "allow") == 0)
type_int = ADLIST_ALLOW;
}
const int type_idx = sqlite3_bind_parameter_index(stmt, ":type");
if(type_idx > 0 && (!cJSON_IsNumber(type) || (rc = sqlite3_bind_int(stmt, type_idx, type->valueint)) != SQLITE_OK))
if(type_idx > 0 && (rc = sqlite3_bind_int(stmt, type_idx, type_int)) != SQLITE_OK)
{
*message = sqlite3_errmsg(gravity_db);
log_err("gravityDB_delFromTable(%d): Failed to bind type (error %d) - %s",
type->valueint, rc, *message);
type_int, rc, *message);
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
@ -1981,12 +2002,15 @@ bool gravityDB_delFromTable(const enum gravity_list_type listtype, const cJSON*
const char *querystrs[4] = {NULL, NULL, NULL, NULL};
if(listtype == GRAVITY_GROUPS)
querystrs[0] = "DELETE FROM \"group\" WHERE name IN (SELECT item FROM deltable);";
else if(listtype == GRAVITY_ADLISTS)
else if(listtype == GRAVITY_ADLISTS ||
listtype == GRAVITY_ADLISTS_BLOCK ||
listtype == GRAVITY_ADLISTS_ALLOW)
{
// This is actually a three-step deletion to satisfy foreign-key constraints
querystrs[0] = "DELETE FROM gravity WHERE adlist_id IN (SELECT id FROM adlist WHERE address IN (SELECT item FROM deltable));";
querystrs[1] = "DELETE FROM antigravity WHERE adlist_id IN (SELECT id FROM adlist WHERE address IN (SELECT item FROM deltable));";
querystrs[2] = "DELETE FROM adlist WHERE address IN (SELECT item FROM deltable);";
// This is actually a four-step deletion to satisfy foreign-key constraints
querystrs[0] = "DELETE FROM gravity WHERE adlist_id IN (SELECT id FROM adlist WHERE address IN (SELECT item FROM deltable WHERE type = 0));";
querystrs[1] = "DELETE FROM antigravity WHERE adlist_id IN (SELECT id FROM adlist WHERE address IN (SELECT item FROM deltable WHERE type = 1));";
querystrs[2] = "DELETE FROM adlist WHERE address IN (SELECT item FROM deltable WHERE type = 0) AND type = 0;";
querystrs[3] = "DELETE FROM adlist WHERE address IN (SELECT item FROM deltable WHERE type = 1) AND type = 1;";
}
else if(listtype == GRAVITY_CLIENTS)
querystrs[0] = "DELETE FROM client WHERE ip IN (SELECT item FROM deltable);";
@ -2096,12 +2120,16 @@ bool gravityDB_readTable(const enum gravity_list_type listtype,
case GRAVITY_DOMAINLIST_ALL_ALL:
type = "0,1,2,3";
break;
// No type required for these tables
case GRAVITY_GRAVITY:
case GRAVITY_ANTIGRAVITY:
case GRAVITY_GROUPS:
case GRAVITY_ADLISTS:
case GRAVITY_CLIENTS:
// No type required for these tables
case GRAVITY_ADLISTS:
// Type is set in the SQL query directly
case GRAVITY_ADLISTS_BLOCK:
case GRAVITY_ADLISTS_ALLOW:
break;
}
@ -2133,19 +2161,29 @@ bool gravityDB_readTable(const enum gravity_list_type listtype,
}
snprintf(querystr, buflen, "SELECT id,name,enabled,date_added,date_modified,description AS comment FROM \"group\"%s;", filter);
}
else if(listtype == GRAVITY_ADLISTS)
else if(listtype == GRAVITY_ADLISTS ||
listtype == GRAVITY_ADLISTS_BLOCK ||
listtype == GRAVITY_ADLISTS_ALLOW)
{
if(listtype == GRAVITY_ADLISTS_BLOCK)
filter = "type = 0";
else if(listtype == GRAVITY_ADLISTS_ALLOW)
filter = "type = 1";
else
filter = "TRUE";
const char *filter2 = "";
if(item != NULL && item[0] != '\0')
{
if(exact)
filter = " WHERE address = :item";
filter2 = " AND address = :item";
else
filter = " WHERE address LIKE :item";
filter2 = " AND address LIKE :item";
}
snprintf(querystr, buflen, "SELECT id,type,address,enabled,date_added,date_modified,comment,"
"(SELECT GROUP_CONCAT(group_id) FROM adlist_by_group g WHERE g.adlist_id = a.id) AS group_ids,"
"date_updated,number,invalid_domains,status,abp_entries "
"FROM adlist a%s;", filter);
"FROM adlist a WHERE %s%s;", filter, filter2);
}
else if(listtype == GRAVITY_CLIENTS)
{
@ -2312,6 +2350,8 @@ bool gravityDB_readTableGetRow(const enum gravity_list_type listtype, tablerow *
}
}
else if(listtype == GRAVITY_ADLISTS ||
listtype == GRAVITY_ADLISTS_ALLOW ||
listtype == GRAVITY_ADLISTS_BLOCK ||
listtype == GRAVITY_GRAVITY ||
listtype == GRAVITY_ANTIGRAVITY)
{
@ -2328,6 +2368,10 @@ bool gravityDB_readTableGetRow(const enum gravity_list_type listtype, tablerow *
break;
}
}
else
{
row->type = "unknown";
}
}
else if(strcasecmp(cname, "domain") == 0)
@ -2425,9 +2469,14 @@ bool gravityDB_edit_groups(const enum gravity_list_type listtype, cJSON *groups,
del_querystr = "DELETE FROM client_by_group WHERE client_id = :id;";
add_querystr = "INSERT INTO client_by_group (client_id,group_id) VALUES (:id,:gid);";
}
else if(listtype == GRAVITY_ADLISTS)
else if(listtype == GRAVITY_ADLISTS ||
listtype == GRAVITY_ADLISTS_BLOCK ||
listtype == GRAVITY_ADLISTS_ALLOW)
{
get_querystr = "SELECT id FROM adlist WHERE address = :item";
if(listtype == GRAVITY_ADLISTS)
get_querystr = "SELECT id FROM adlist WHERE address = :item";
else
get_querystr = "SELECT id FROM adlist WHERE address = :item AND type = :type";
del_querystr = "DELETE FROM adlist_by_group WHERE adlist_id = :id;";
add_querystr = "INSERT INTO adlist_by_group (adlist_id,group_id) VALUES (:id,:gid);";
}

View File

@ -753,7 +753,7 @@ static int update_netDB_interface(sqlite3 *db, const int network_id, const char
// Loop over all clients known to FTL and ensure we add them all to the database
static bool add_FTL_clients_to_network_table(sqlite3 *db, const enum arp_status *client_status,
const int clients, time_t now, unsigned int *additional_entries)
const int clients, const time_t now, unsigned int *additional_entries)
{
// Return early if database is known to be broken
if(FTLDBerror())
@ -1546,6 +1546,7 @@ void parse_neighbor_cache(sqlite3* db)
free(client_status);
return;
}
free(client_status);
client_status = NULL;

View File

@ -112,19 +112,47 @@ bool init_memory_database(void)
if(!attach_database(_memdb, NULL, config.files.database.v.s, "disk"))
return false;
// Change journal mode to WAL
// - WAL is significantly faster in most scenarios.
// - WAL provides more concurrency as readers do not block writers and a
// writer does not block readers. Reading and writing can proceed
// concurrently.
// - Disk I/O operations tends to be more sequential using WAL.
rc = sqlite3_exec(_memdb, "PRAGMA disk.journal_mode=WAL", NULL, NULL, NULL);
if( rc != SQLITE_OK )
// Enable WAL mode for the on-disk database (pihole-FTL.db) if
// configured (default is yes). User may not want to enable WAL
// mode if the database is on a network share as all processes
// accessing the database must be on the same host in WAL mode.
if(config.database.useWAL.v.b)
{
log_err("init_memory_database(): Step error while trying to set journal mode: %s",
sqlite3_errstr(rc));
sqlite3_close(_memdb);
return false;
// Change journal mode to WAL
// - WAL is significantly faster in most scenarios.
// - WAL provides more concurrency as readers do not block writers and a
// writer does not block readers. Reading and writing can proceed
// concurrently.
// - Disk I/O operations tend to be more sequential using WAL.
rc = sqlite3_exec(_memdb, "PRAGMA disk.journal_mode=WAL", NULL, NULL, NULL);
if( rc != SQLITE_OK )
{
log_err("init_memory_database(): Step error while trying to set journal mode: %s",
sqlite3_errstr(rc));
sqlite3_close(_memdb);
return false;
}
}
else
{
// Unlike the other journaling modes, PRAGMA journal_mode=WAL is
// persistent. If a process sets WAL mode, then closes and
// reopens the database, the database will come back in WAL
// mode. In contrast, if a process sets (for example) PRAGMA
// journal_mode=TRUNCATE and then closes and reopens the
// database will come back up in the default rollback mode of
// DELETE rather than the previous TRUNCATE setting.
// Change journal mode back to DELETE due to user configuration
// (might have been changed to WAL before)
rc = sqlite3_exec(_memdb, "PRAGMA disk.journal_mode=DELETE", NULL, NULL, NULL);
if( rc != SQLITE_OK )
{
log_err("init_memory_database(): Step error while trying to set journal mode: %s",
sqlite3_errstr(rc));
sqlite3_close(_memdb);
return false;
}
}
// Everything went well

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -126,6 +126,7 @@ static const struct {
{ 258, "AVC" }, /* Application Visibility and Control [Wolfgang_Riedel] AVC/avc-completed-template 2016-02-26*/
{ 259, "DOA" }, /* Digital Object Architecture [draft-durand-doa-over-dns] DOA/doa-completed-template 2017-08-30*/
{ 260, "AMTRELAY" }, /* Automatic Multicast Tunneling Relay [RFC8777] AMTRELAY/amtrelay-completed-template 2019-02-06*/
{ 261, "RESINFO" }, /* Resolver Information as Key/Value Pairs https://datatracker.ietf.org/doc/draft-ietf-add-resolver-info/06/ */
{ 32768, "TA" }, /* DNSSEC Trust Authorities [Sam_Weiler][http://cameo.library.cmu.edu/][ Deploying DNSSEC Without a Signed Root. Technical Report 1999-19, Information Networking Institute, Carnegie Mellon University, April 2004.] 2005-12-13*/
{ 32769, "DLV" }, /* DNSSEC Lookaside Validation (OBSOLETE) [RFC8749][RFC4431] */
};
@ -441,18 +442,21 @@ unsigned int cache_remove_uid(const unsigned int uid)
{
int i;
unsigned int removed = 0;
struct crec *crecp, **up;
struct crec *crecp, *tmp, **up;
for (i = 0; i < hash_size; i++)
for (crecp = hash_table[i], up = &hash_table[i]; crecp; crecp = crecp->hash_next)
if ((crecp->flags & (F_HOSTS | F_DHCP | F_CONFIG)) && crecp->uid == uid)
{
*up = crecp->hash_next;
free(crecp);
removed++;
}
else
up = &crecp->hash_next;
for (crecp = hash_table[i], up = &hash_table[i]; crecp; crecp = tmp)
{
tmp = crecp->hash_next;
if ((crecp->flags & (F_HOSTS | F_DHCP | F_CONFIG)) && crecp->uid == uid)
{
*up = tmp;
free(crecp);
removed++;
}
else
up = &crecp->hash_next;
}
return removed;
}
@ -815,32 +819,28 @@ void cache_end_insert(void)
read_write(daemon->pipe_to_parent, (unsigned char *)name, m, 0);
read_write(daemon->pipe_to_parent, (unsigned char *)&new_chain->ttd, sizeof(new_chain->ttd), 0);
read_write(daemon->pipe_to_parent, (unsigned char *)&flags, sizeof(flags), 0);
if (flags & (F_IPV4 | F_IPV6 | F_DNSKEY | F_DS | F_RR))
read_write(daemon->pipe_to_parent, (unsigned char *)&new_chain->addr, sizeof(new_chain->addr), 0);
if (flags & F_RR)
{
read_write(daemon->pipe_to_parent, (unsigned char *)&new_chain->addr, sizeof(new_chain->addr), 0);
if (flags & F_RR)
{
/* A negative RR entry is possible and has no data, obviously. */
if (!(flags & F_NEG) && (flags & F_KEYTAG))
blockdata_write(new_chain->addr.rrblock.rrdata, new_chain->addr.rrblock.datalen, daemon->pipe_to_parent);
}
#ifdef HAVE_DNSSEC
if (flags & F_DNSKEY)
{
read_write(daemon->pipe_to_parent, (unsigned char *)&class, sizeof(class), 0);
blockdata_write(new_chain->addr.key.keydata, new_chain->addr.key.keylen, daemon->pipe_to_parent);
}
else if (flags & F_DS)
{
read_write(daemon->pipe_to_parent, (unsigned char *)&class, sizeof(class), 0);
/* A negative DS entry is possible and has no data, obviously. */
if (!(flags & F_NEG))
blockdata_write(new_chain->addr.ds.keydata, new_chain->addr.ds.keylen, daemon->pipe_to_parent);
}
#endif
/* A negative RR entry is possible and has no data, obviously. */
if (!(flags & F_NEG) && (flags & F_KEYTAG))
blockdata_write(new_chain->addr.rrblock.rrdata, new_chain->addr.rrblock.datalen, daemon->pipe_to_parent);
}
#ifdef HAVE_DNSSEC
if (flags & F_DNSKEY)
{
read_write(daemon->pipe_to_parent, (unsigned char *)&class, sizeof(class), 0);
blockdata_write(new_chain->addr.key.keydata, new_chain->addr.key.keylen, daemon->pipe_to_parent);
}
else if (flags & F_DS)
{
read_write(daemon->pipe_to_parent, (unsigned char *)&class, sizeof(class), 0);
/* A negative DS entry is possible and has no data, obviously. */
if (!(flags & F_NEG))
blockdata_write(new_chain->addr.ds.keydata, new_chain->addr.ds.keylen, daemon->pipe_to_parent);
}
#endif
}
}
@ -851,7 +851,18 @@ void cache_end_insert(void)
if (daemon->pipe_to_parent != -1)
{
ssize_t m = -1;
read_write(daemon->pipe_to_parent, (unsigned char *)&m, sizeof(m), 0);
#ifdef HAVE_DNSSEC
/* Sneak out possibly updated crypto HWM values. */
m = daemon->metrics[METRIC_CRYPTO_HWM];
read_write(daemon->pipe_to_parent, (unsigned char *)&m, sizeof(m), 0);
m = daemon->metrics[METRIC_SIG_FAIL_HWM];
read_write(daemon->pipe_to_parent, (unsigned char *)&m, sizeof(m), 0);
m = daemon->metrics[METRIC_WORK_HWM];
read_write(daemon->pipe_to_parent, (unsigned char *)&m, sizeof(m), 0);
#endif
}
new_chain = NULL;
@ -870,7 +881,7 @@ int cache_recv_insert(time_t now, int fd)
cache_start_insert();
while(1)
while (1)
{
if (!read_write(fd, (unsigned char *)&m, sizeof(m), 1))
@ -878,13 +889,29 @@ int cache_recv_insert(time_t now, int fd)
if (m == -1)
{
#ifdef HAVE_DNSSEC
/* Sneak in possibly updated crypto HWM. */
if (!read_write(fd, (unsigned char *)&m, sizeof(m), 1))
return 0;
if (m > daemon->metrics[METRIC_CRYPTO_HWM])
daemon->metrics[METRIC_CRYPTO_HWM] = m;
if (!read_write(fd, (unsigned char *)&m, sizeof(m), 1))
return 0;
if (m > daemon->metrics[METRIC_SIG_FAIL_HWM])
daemon->metrics[METRIC_SIG_FAIL_HWM] = m;
if (!read_write(fd, (unsigned char *)&m, sizeof(m), 1))
return 0;
if (m > daemon->metrics[METRIC_WORK_HWM])
daemon->metrics[METRIC_WORK_HWM] = m;
#endif
cache_end_insert();
return 1;
}
if (!read_write(fd, (unsigned char *)daemon->namebuff, m, 1) ||
!read_write(fd, (unsigned char *)&ttd, sizeof(ttd), 1) ||
!read_write(fd, (unsigned char *)&flags, sizeof(flags), 1))
!read_write(fd, (unsigned char *)&flags, sizeof(flags), 1) ||
!read_write(fd, (unsigned char *)&addr, sizeof(addr), 1))
return 0;
daemon->namebuff[m] = 0;
@ -915,30 +942,23 @@ int cache_recv_insert(time_t now, int fd)
{
unsigned short class = C_IN;
if (flags & (F_IPV4 | F_IPV6 | F_DNSKEY | F_DS | F_RR))
{
if (!read_write(fd, (unsigned char *)&addr, sizeof(addr), 1))
return 0;
if ((flags & F_RR) && !(flags & F_NEG) && (flags & F_KEYTAG)
&& !(addr.rrblock.rrdata = blockdata_read(fd, addr.rrblock.datalen)))
return 0;
if ((flags & F_RR) && !(flags & F_NEG) && (flags & F_KEYTAG)
&& !(addr.rrblock.rrdata = blockdata_read(fd, addr.rrblock.datalen)))
return 0;
#ifdef HAVE_DNSSEC
if (flags & F_DNSKEY)
{
if (!read_write(fd, (unsigned char *)&class, sizeof(class), 1) ||
!(addr.key.keydata = blockdata_read(fd, addr.key.keylen)))
return 0;
}
else if (flags & F_DS)
{
if (!read_write(fd, (unsigned char *)&class, sizeof(class), 1) ||
(!(flags & F_NEG) && !(addr.key.keydata = blockdata_read(fd, addr.key.keylen))))
return 0;
}
#endif
if (flags & F_DNSKEY)
{
if (!read_write(fd, (unsigned char *)&class, sizeof(class), 1) ||
!(addr.key.keydata = blockdata_read(fd, addr.key.keylen)))
return 0;
}
else if (flags & F_DS)
{
if (!read_write(fd, (unsigned char *)&class, sizeof(class), 1) ||
(!(flags & F_NEG) && !(addr.key.keydata = blockdata_read(fd, addr.key.keylen))))
return 0;
}
#endif
crecp = really_insert(daemon->namebuff, &addr, class, now, ttl, flags);
}
}
@ -1839,8 +1859,18 @@ static void dump_cache_entry(struct crec *cache, time_t now)
p = buff;
*a = 0;
if (strlen(n) == 0 && !(cache->flags & F_REVERSE))
n = "<Root>";
if (cache->flags & F_REVERSE)
{
if ((cache->flags & F_NEG))
n = "";
}
else
{
if (strlen(n) == 0)
n = "<Root>";
}
p += sprintf(p, "%-30.30s ", sanitise(n));
if ((cache->flags & F_CNAME) && !is_outdated_cname_pointer(cache))
a = sanitise(cache_get_cname_target(cache));
@ -2005,9 +2035,19 @@ void dump_cache(time_t now)
#ifdef HAVE_AUTH
my_syslog(LOG_INFO, _("queries for authoritative zones %u"), daemon->metrics[METRIC_DNS_AUTH_ANSWERED]);
#endif
#ifdef HAVE_DNSSEC
my_syslog(LOG_INFO, _("DNSSEC per-query subqueries HWM %u"), daemon->metrics[METRIC_WORK_HWM]);
my_syslog(LOG_INFO, _("DNSSEC per-query crypto work HWM %u"), daemon->metrics[METRIC_CRYPTO_HWM]);
my_syslog(LOG_INFO, _("DNSSEC per-RRSet signature fails HWM %u"), daemon->metrics[METRIC_SIG_FAIL_HWM]);
#endif
blockdata_report();
my_syslog(LOG_INFO, _("child processes for TCP requests: in use %zu, highest since last SIGUSR1 %zu, max allowed %zu."),
daemon->metrics[METRIC_TCP_CONNECTIONS],
daemon->max_procs_used,
daemon->max_procs);
daemon->max_procs_used = daemon->metrics[METRIC_TCP_CONNECTIONS];
/* sum counts from different records for same server */
for (serv = daemon->servers; serv; serv = serv->next)
serv->flags &= ~SERV_MARK;
@ -2161,6 +2201,11 @@ const char *edestr(int ede)
case EDE_NO_AUTH: return "no reachable authority";
case EDE_NETERR: return "network error";
case EDE_INVALID_DATA: return "invalid data";
case EDE_SIG_E_B_V: return "signature expired before valid";
case EDE_TOO_EARLY: return "too early";
case EDE_UNS_NS3_ITER: return "unsupported NSEC3 iterations value";
case EDE_UNABLE_POLICY: return "uanble to conform to policy";
case EDE_SYNTHESIZED: return "synthesized";
default: return "unknown";
}
}

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -15,14 +15,17 @@
*/
#define FTABSIZ 150 /* max number of outstanding requests (default) */
#define MAX_PROCS 60 /* max no children for TCP requests */
#define MAX_PROCS 60 /* default max no children for TCP requests */
#define CHILD_LIFETIME 300 /* secs 'till terminated (RFC1035 suggests > 120s) */
#define TCP_MAX_QUERIES 100 /* Maximum number of queries per incoming TCP connection */
#define TCP_BACKLOG 32 /* kernel backlog limit for TCP connections */
#define EDNS_PKTSZ 1232 /* default max EDNS.0 UDP packet from from /dnsflagday.net/2020 */
#define SAFE_PKTSZ 1232 /* "go anywhere" UDP packet size, see https://dnsflagday.net/2020/ */
#define KEYBLOCK_LEN 40 /* choose to minimise fragmentation when storing DNSSEC keys */
#define DNSSEC_WORK 50 /* Max number of queries to validate one question */
#define DNSSEC_LIMIT_WORK 40 /* Max number of queries to validate one question */
#define DNSSEC_LIMIT_SIG_FAIL 20 /* Number of signature that can fail to validate in one answer */
#define DNSSEC_LIMIT_CRYPTO 200 /* max no. of crypto operations to validate one query. */
#define DNSSEC_LIMIT_NSEC3_ITERS 150 /* Max. number if iterations allowed in NSEC3 record. */
#define TIMEOUT 10 /* drop UDP queries after TIMEOUT seconds */
#define SMALL_PORT_RANGE 30 /* If DNS port range is smaller than this, use different allocation. */
#define FORWARD_TEST 1000 /* try all servers every 1000 queries */

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -92,7 +92,7 @@ void dhcp6_packet(time_t now)
struct iface_param parm;
struct cmsghdr *cmptr;
struct msghdr msg;
int if_index = 0;
uint32_t if_index = 0;
union {
struct cmsghdr align; /* this ensures alignment */
char control6[CMSG_SPACE(sizeof(struct in6_pktinfo))];

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -112,8 +112,11 @@
#define EDE_NO_AUTH 22 /* No Reachable Authority */
#define EDE_NETERR 23 /* Network error */
#define EDE_INVALID_DATA 24 /* Invalid Data */
#define EDE_SIG_E_B_V 25 /* Signature Expired before Valid */
#define EDE_TOO_EARLY 26 /* To Early */
#define EDE_UNS_NS3_ITER 27 /* Unsupported NSEC3 Iterations Value */
#define EDE_UNABLE_POLICY 28 /* Unable to conform to policy */
#define EDE_SYNTHESIZED 29 /* Synthesized */
struct dns_header {

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -36,12 +36,14 @@ static volatile int pipewrite;
volatile char FTL_terminate = 0;
static void set_dns_listeners(void);
static void set_tftp_listeners(void);
static void check_dns_listeners(time_t now);
static void sig_handler(int sig);
static void async_event(int pipe, time_t now);
static void fatal_event(struct event_desc *ev, char *msg);
static int read_event(int fd, struct event_desc *evp, char **msg);
static void poll_resolv(int force, int do_reload, time_t now);
static void tcp_init(void);
int main_dnsmasq (int argc, char **argv)
{
@ -135,19 +137,11 @@ int main_dnsmasq (int argc, char **argv)
'.' or NAME_ESCAPE then all would have to be escaped, so the
presentation format would be twice as long as the spec. */
daemon->keyname = safe_malloc((MAXDNAME * 2) + 1);
daemon->workspacename = safe_malloc((MAXDNAME * 2) + 1);
/* one char flag per possible RR in answer section (may get extended). */
daemon->rr_status_sz = 64;
daemon->rr_status = safe_malloc(sizeof(*daemon->rr_status) * daemon->rr_status_sz);
}
#endif
#if defined(HAVE_CONNTRACK) && defined(HAVE_UBUS)
/* CONNTRACK UBUS code uses this buffer, so if not allocated above,
we need to allocate it here. */
if (option_bool(OPT_CMARK_ALST_EN) && !daemon->workspacename)
daemon->workspacename = safe_malloc((MAXDNAME * 2) + 1);
#endif
#ifdef HAVE_DHCP
if (!daemon->lease_file)
@ -378,6 +372,13 @@ int main_dnsmasq (int argc, char **argv)
if (!enumerate_interfaces(1) || !enumerate_interfaces(0))
die(_("failed to find list of interfaces: %s"), NULL, EC_MISC);
#ifdef HAVE_DHCP
/* Determine lease FQDNs after enumerate_interfaces() call, since it needs
to call get_domain and that's only valid for some domain configs once we
have interface addresses. */
lease_calc_fqdns();
#endif
if (option_bool(OPT_NOWILD) || option_bool(OPT_CLEVERBIND))
{
@ -421,11 +422,13 @@ int main_dnsmasq (int argc, char **argv)
daemon->numrrand = max_fd/3;
/* safe_malloc returns zero'd memory */
daemon->randomsocks = safe_malloc(daemon->numrrand * sizeof(struct randfd));
tcp_init();
}
#ifdef HAVE_INOTIFY
if ((daemon->port != 0 || daemon->dhcp || daemon->doing_dhcp6)
&& (!option_bool(OPT_NO_RESOLV) || daemon->dynamic_dirs))
if ((daemon->port != 0 && !option_bool(OPT_NO_RESOLV)) ||
daemon->dynamic_dirs)
inotify_dnsmasq_init();
else
daemon->inotifyfd = -1;
@ -865,6 +868,8 @@ int main_dnsmasq (int argc, char **argv)
if (option_bool(OPT_LOCAL_SERVICE))
my_syslog(LOG_INFO, _("DNS service limited to local subnets"));
else if (option_bool(OPT_LOCALHOST_SERVICE))
my_syslog(LOG_INFO, _("DNS service limited to localhost"));
}
my_syslog(LOG_INFO, _("compile time options: %s"), compile_opts);
@ -1051,8 +1056,10 @@ int main_dnsmasq (int argc, char **argv)
pid = getpid();
daemon->pipe_to_parent = -1;
for (i = 0; i < MAX_PROCS; i++)
daemon->tcp_pipes[i] = -1;
if (daemon->port != 0)
for (i = 0; i < daemon->max_procs; i++)
daemon->tcp_pipes[i] = -1;
#ifdef HAVE_INOTIFY
/* Using inotify, have to select a resolv file at startup */
@ -1079,7 +1086,12 @@ int main_dnsmasq (int argc, char **argv)
(timeout == -1 || timeout > 1000))
timeout = 1000;
set_dns_listeners();
if (daemon->port != 0)
set_dns_listeners();
#ifdef HAVE_TFTP
set_tftp_listeners();
#endif
#ifdef HAVE_DBUS
if (option_bool(OPT_DBUS))
@ -1264,8 +1276,9 @@ int main_dnsmasq (int argc, char **argv)
check_ubus_listeners();
}
#endif
check_dns_listeners(now);
if (daemon->port != 0)
check_dns_listeners(now);
#ifdef HAVE_TFTP
check_tftp_listeners(now);
@ -1538,10 +1551,15 @@ static void async_event(int pipe, time_t now)
if (errno != EINTR)
break;
}
else
for (i = 0 ; i < MAX_PROCS; i++)
else if (daemon->port != 0)
for (i = 0 ; i < daemon->max_procs; i++)
if (daemon->tcp_pids[i] == p)
daemon->tcp_pids[i] = 0;
{
daemon->tcp_pids[i] = 0;
/* tcp_pipes == -1 && tcp_pids == 0 required to free slot */
if (daemon->tcp_pipes[i] == -1)
daemon->metrics[METRIC_TCP_CONNECTIONS]--;
}
break;
#if defined(HAVE_SCRIPT)
@ -1604,9 +1622,10 @@ static void async_event(int pipe, time_t now)
case EVENT_TERM:
/* Knock all our children on the head. */
for (i = 0; i < MAX_PROCS; i++)
if (daemon->tcp_pids[i] != 0)
kill(daemon->tcp_pids[i], SIGALRM);
if (daemon->port != 0)
for (i = 0; i < daemon->max_procs; i++)
if (daemon->tcp_pids[i] != 0)
kill(daemon->tcp_pids[i], SIGALRM);
#if defined(HAVE_SCRIPT) && defined(HAVE_DHCP)
/* handle pending lease transitions */
@ -1756,23 +1775,33 @@ void clear_cache_and_reload(time_t now)
#endif
}
static void set_dns_listeners(void)
{
struct serverfd *serverfdp;
struct listener *listener;
struct randfd_list *rfl;
int i;
#ifdef HAVE_TFTP
static void set_tftp_listeners(void)
{
int tftp = 0;
struct tftp_transfer *transfer;
struct listener *listener;
if (!option_bool(OPT_SINGLE_PORT))
for (transfer = daemon->tftp_trans; transfer; transfer = transfer->next)
{
tftp++;
poll_listen(transfer->sockfd, POLLIN);
}
for (listener = daemon->listeners; listener; listener = listener->next)
/* tftp == 0 in single-port mode. */
if (tftp <= daemon->tftp_max && listener->tftpfd != -1)
poll_listen(listener->tftpfd, POLLIN);
}
#endif
static void set_dns_listeners(void)
{
struct serverfd *serverfdp;
struct listener *listener;
struct randfd_list *rfl;
int i;
for (serverfdp = daemon->sfds; serverfdp; serverfdp = serverfdp->next)
poll_listen(serverfdp->fd, POLLIN);
@ -1786,7 +1815,7 @@ static void set_dns_listeners(void)
poll_listen(rfl->rfd->fd, POLLIN);
/* check to see if we have free tcp process slots. */
for (i = MAX_PROCS - 1; i >= 0; i--)
for (i = daemon->max_procs - 1; i >= 0; i--)
if (daemon->tcp_pids[i] == 0 && daemon->tcp_pipes[i] == -1)
break;
@ -1801,16 +1830,10 @@ static void set_dns_listeners(void)
we'll be called again when a slot becomes available. */
if (listener->tcpfd != -1 && i >= 0)
poll_listen(listener->tcpfd, POLLIN);
#ifdef HAVE_TFTP
/* tftp == 0 in single-port mode. */
if (tftp <= daemon->tftp_max && listener->tftpfd != -1)
poll_listen(listener->tftpfd, POLLIN);
#endif
}
if (!option_bool(OPT_DEBUG))
for (i = 0; i < MAX_PROCS; i++)
for (i = 0; i < daemon->max_procs; i++)
if (daemon->tcp_pipes[i] != -1)
poll_listen(daemon->tcp_pipes[i], POLLIN);
}
@ -1845,13 +1868,16 @@ static void check_dns_listeners(time_t now)
to free the process slot. Once the child process has gone, poll()
returns POLLHUP, not POLLIN, so have to check for both here. */
if (!option_bool(OPT_DEBUG))
for (i = 0; i < MAX_PROCS; i++)
for (i = 0; i < daemon->max_procs; i++)
if (daemon->tcp_pipes[i] != -1 &&
poll_check(daemon->tcp_pipes[i], POLLIN | POLLHUP) &&
!cache_recv_insert(now, daemon->tcp_pipes[i]))
{
close(daemon->tcp_pipes[i]);
daemon->tcp_pipes[i] = -1;
/* tcp_pipes == -1 && tcp_pids == 0 required to free slot */
if (daemon->tcp_pids[i] == 0)
daemon->metrics[METRIC_TCP_CONNECTIONS]--;
}
for (listener = daemon->listeners; listener; listener = listener->next)
@ -1860,17 +1886,12 @@ static void check_dns_listeners(time_t now)
if (listener->fd != -1 && poll_check(listener->fd, POLLIN))
receive_query(listener, now);
#ifdef HAVE_TFTP
if (listener->tftpfd != -1 && poll_check(listener->tftpfd, POLLIN))
tftp_request(listener, now);
#endif
/* check to see if we have a free tcp process slot.
Note that we can't assume that because we had
at least one a poll() time, that we still do.
There may be more waiting connections after
poll() returns then free process slots. */
for (i = MAX_PROCS - 1; i >= 0; i--)
for (i = daemon->max_procs - 1; i >= 0; i--)
if (daemon->tcp_pids[i] == 0 && daemon->tcp_pipes[i] == -1)
break;
@ -1986,6 +2007,9 @@ static void check_dns_listeners(time_t now)
/* i holds index of free slot */
daemon->tcp_pids[i] = p;
daemon->tcp_pipes[i] = pipefd[0];
daemon->metrics[METRIC_TCP_CONNECTIONS]++;
if (daemon->metrics[METRIC_TCP_CONNECTIONS] > daemon->max_procs_used)
daemon->max_procs_used = daemon->metrics[METRIC_TCP_CONNECTIONS];
}
close(confd);
@ -2171,7 +2195,11 @@ int delay_dhcp(time_t start, int sec, int fd, uint32_t addr, unsigned short id)
poll_reset();
if (fd != -1)
poll_listen(fd, POLLIN);
set_dns_listeners();
if (daemon->port != 0)
set_dns_listeners();
#ifdef HAVE_TFTP
set_tftp_listeners();
#endif
set_log_writer();
#ifdef HAVE_DHCP6
@ -2189,7 +2217,8 @@ int delay_dhcp(time_t start, int sec, int fd, uint32_t addr, unsigned short id)
now = dnsmasq_time();
check_log_writer(0);
check_dns_listeners(now);
if (daemon->port != 0)
check_dns_listeners(now);
#ifdef HAVE_DHCP6
if (daemon->doing_ra && poll_check(daemon->icmp6fd, POLLIN))
@ -2232,3 +2261,9 @@ void print_dnsmasq_version(const char *yellow, const char *green, const char *bo
printf(_("Features: %s\n\n"), compile_opts);
}
/**************************************************************************************/
void tcp_init(void)
{
daemon->tcp_pids = safe_malloc(daemon->max_procs*sizeof(pid_t));
daemon->tcp_pipes = safe_malloc(daemon->max_procs*sizeof(int));
}

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -14,7 +14,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#define COPYRIGHT "Copyright (c) 2000-2023 Simon Kelley"
#define COPYRIGHT "Copyright (c) 2000-2024 Simon Kelley"
/* We do defines that influence behavior of stdio.h, so complain
if included too early. */
@ -281,7 +281,8 @@ struct event_desc {
#define OPT_NORR 69
#define OPT_NO_IDENT 70
#define OPT_CACHE_RR 71
#define OPT_LAST 72
#define OPT_LOCALHOST_SERVICE 72
#define OPT_LAST 73
#define OPTION_BITS (sizeof(unsigned int)*8)
#define OPTION_SIZE ( (OPT_LAST/OPTION_BITS)+((OPT_LAST%OPTION_BITS)!=0) )
@ -340,7 +341,7 @@ union all_addr {
in the cache flags. */
struct datablock {
unsigned short rrtype;
unsigned char datalen;
unsigned char datalen; /* also length of SOA in negative records. */
char data[];
} rrdata;
};
@ -762,6 +763,9 @@ struct dyndir {
#define DNSSEC_FAIL_NONSEC 0x0040 /* No NSEC */
#define DNSSEC_FAIL_NODSSUP 0x0080 /* no supported DS algo. */
#define DNSSEC_FAIL_NOKEY 0x0100 /* no DNSKEY */
#define DNSSEC_FAIL_NSEC3_ITERS 0x0200 /* too many iterations in NSEC3 */
#define DNSSEC_FAIL_BADPACKET 0x0400 /* bad packet */
#define DNSSEC_FAIL_WORK 0x0800 /* too much crypto */
#define STAT_ISEQUAL(a, b) (((a) & 0xffff0000) == (b))
@ -799,7 +803,7 @@ struct frec {
struct blockdata *stash; /* Saved reply, whilst we validate */
size_t stash_len;
#ifdef HAVE_DNSSEC
int class, work_counter;
int class, work_counter, validate_counter;
struct frec *dependent; /* Query awaiting internally-generated DNSKEY or DS query */
struct frec *next_dependent; /* list of above. */
struct frec *blocking_query; /* Query which is blocking us. */
@ -836,6 +840,12 @@ struct frec {
#define LEASE_HAVE_HWADDR 128 /* Have set hwaddress */
#define LEASE_EXP_CHANGED 256 /* Lease expiry time changed */
#define LIMIT_SIG_FAIL 0
#define LIMIT_CRYPTO 1
#define LIMIT_WORK 2
#define LIMIT_NSEC3_ITERS 3
#define LIMIT_MAX 4
struct dhcp_lease {
int clid_len; /* length of client identifier */
unsigned char *clid; /* clientid */
@ -1238,16 +1248,14 @@ extern struct daemon {
char *packet; /* packet buffer */
int packet_buff_sz; /* size of above */
char *namebuff; /* MAXDNAME size buffer */
#if (defined(HAVE_CONNTRACK) && defined(HAVE_UBUS)) || defined(HAVE_DNSSEC)
/* CONNTRACK UBUS code uses this buffer, as well as DNSSEC code. */
char *workspacename;
#endif
#ifdef HAVE_DNSSEC
char *keyname; /* MAXDNAME size buffer */
unsigned long *rr_status; /* ceiling in TTL from DNSSEC or zero for insecure */
int rr_status_sz;
int dnssec_no_time_check;
int back_to_the_future;
int limit[LIMIT_MAX];
#endif
struct frec *frec_list;
struct frec_src *free_frec_src;
@ -1258,8 +1266,8 @@ extern struct daemon {
struct server *srv_save; /* Used for resend on DoD */
size_t packet_len; /* " " */
int fd_save; /* " " */
pid_t tcp_pids[MAX_PROCS];
int tcp_pipes[MAX_PROCS];
pid_t *tcp_pids;
int *tcp_pipes;
int pipe_to_parent;
int numrrand;
struct randfd *randomsocks;
@ -1319,6 +1327,8 @@ extern struct daemon {
/* file for packet dumps. */
int dumpfd;
#endif
int max_procs;
uint max_procs_used;
} *daemon;
struct server_details {
@ -1383,6 +1393,7 @@ int is_name_synthetic(int flags, char *name, union all_addr *addr);
int is_rev_synth(int flag, union all_addr *addr, char *name);
/* rfc1035.c */
int do_doctor(struct dns_header *header, size_t qlen, char *namebuff);
int extract_name(struct dns_header *header, size_t plen, unsigned char **pp,
char *name, int isExtract, int extrabytes);
unsigned char *skip_name(unsigned char *ansp, struct dns_header *header, size_t plen, int extrabytes);
@ -1393,7 +1404,7 @@ unsigned int extract_request(struct dns_header *header, size_t qlen,
void setup_reply(struct dns_header *header, unsigned int flags, int ede);
int extract_addresses(struct dns_header *header, size_t qlen, char *name,
time_t now, struct ipsets *ipsets, struct ipsets *nftsets, int is_sign,
int check_rebind, int no_cache_dnssec, int secure, int *doctored);
int check_rebind, int no_cache_dnssec, int secure);
#if defined(HAVE_CONNTRACK) && defined(HAVE_UBUS)
void report_addresses(struct dns_header *header, size_t len, u32 mark);
#endif
@ -1424,10 +1435,12 @@ int in_zone(struct auth_zone *zone, char *name, char **cut);
/* dnssec.c */
#ifdef HAVE_DNSSEC
size_t dnssec_generate_query(struct dns_header *header, unsigned char *end, char *name, int class, int type, int edns_pktsz);
int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class);
int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class);
int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, char *name,
char *keyname, int class, int *validate_count);
int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name,
char *keyname, int class, int *validate_count);
int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int *class,
int check_unsigned, int *neganswer, int *nons, int *nsec_ttl);
int check_unsigned, int *neganswer, int *nons, int *nsec_ttl, int *validate_count);
int dnskey_keytag(int alg, int flags, unsigned char *key, int keylen);
size_t filter_rrsigs(struct dns_header *header, size_t plen);
int setup_timestamp(void);
@ -1613,6 +1626,7 @@ void lease_update_from_configs(void);
int do_script_run(time_t now);
void rerun_scripts(void);
void lease_find_interfaces(time_t now);
void lease_calc_fqdns(void);
#ifdef HAVE_SCRIPT
void lease_add_extradata(struct dhcp_lease *lease, unsigned char *data,
unsigned int len, int delim);

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -22,12 +22,13 @@ static int match_domain(struct in_addr addr, struct cond_domain *c);
static struct cond_domain *search_domain6(struct in6_addr *addr, struct cond_domain *c);
static int match_domain6(struct in6_addr *addr, struct cond_domain *c);
int is_name_synthetic(int flags, char *name, union all_addr *addr)
int is_name_synthetic(int flags, char *name, union all_addr *addrp)
{
char *p;
struct cond_domain *c = NULL;
int prot = (flags & F_IPV6) ? AF_INET6 : AF_INET;
union all_addr addr;
for (c = daemon->synth_domains; c; c = c->next)
{
int found = 0;
@ -74,7 +75,7 @@ int is_name_synthetic(int flags, char *name, union all_addr *addr)
if (!c->is6 &&
index <= ntohl(c->end.s_addr) - ntohl(c->start.s_addr))
{
addr->addr4.s_addr = htonl(ntohl(c->start.s_addr) + index);
addr.addr4.s_addr = htonl(ntohl(c->start.s_addr) + index);
found = 1;
}
}
@ -86,8 +87,8 @@ int is_name_synthetic(int flags, char *name, union all_addr *addr)
index <= addr6part(&c->end6) - addr6part(&c->start6))
{
u64 start = addr6part(&c->start6);
addr->addr6 = c->start6;
setaddr6part(&addr->addr6, start + index);
addr.addr6 = c->start6;
setaddr6part(&addr.addr6, start + index);
found = 1;
}
}
@ -135,8 +136,8 @@ int is_name_synthetic(int flags, char *name, union all_addr *addr)
}
}
if (hostname_isequal(c->domain, p+1) && inet_pton(prot, tail, addr))
found = (prot == AF_INET) ? match_domain(addr->addr4, c) : match_domain6(&addr->addr6, c);
if (hostname_isequal(c->domain, p+1) && inet_pton(prot, tail, &addr))
found = (prot == AF_INET) ? match_domain(addr.addr4, c) : match_domain6(&addr.addr6, c);
}
/* restore name */
@ -148,7 +149,12 @@ int is_name_synthetic(int flags, char *name, union all_addr *addr)
if (found)
return 1;
{
if (addrp)
*addrp = addr;
return 1;
}
}
return 0;

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -344,7 +344,8 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr,
if (ad_reqd)
forward->flags |= FREC_AD_QUESTION;
#ifdef HAVE_DNSSEC
forward->work_counter = DNSSEC_WORK;
forward->work_counter = daemon->limit[LIMIT_WORK];
forward->validate_counter = daemon->limit[LIMIT_CRYPTO];
if (do_bit)
forward->flags |= FREC_DO_QUESTION;
#endif
@ -697,7 +698,7 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server
{
unsigned char *pheader, *sizep;
struct ipsets *ipsets = NULL, *nftsets = NULL;
int munged = 0, is_sign;
int is_sign;
unsigned int rcode = RCODE(header);
size_t plen;
/******** Pi-hole modification ********/
@ -706,8 +707,7 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server
(void)ad_reqd;
(void)do_bit;
(void)bogusanswer;
#ifdef HAVE_IPSET
if (daemon->ipsets && extract_request(header, n, daemon->namebuff, NULL))
ipsets = domain_find_sets(daemon->ipsets, daemon->namebuff);
@ -801,91 +801,108 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server
server->flags |= SERV_WARNED_RECURSIVE;
}
if (daemon->bogus_addr && rcode != NXDOMAIN &&
check_for_bogus_wildcard(header, n, daemon->namebuff, now))
if (header->hb3 & HB3_TC)
{
munged = 1;
SET_RCODE(header, NXDOMAIN);
header->hb3 &= ~HB3_AA;
cache_secure = 0;
ede = EDE_BLOCKED;
log_query(F_UPSTREAM, NULL, NULL, "truncated", 0);
header->ancount = htons(0);
header->nscount = htons(0);
header->arcount = htons(0);
}
else
{
int doctored = 0;
if (rcode == NXDOMAIN &&
extract_request(header, n, daemon->namebuff, NULL))
{
if (check_for_local_domain(daemon->namebuff, now) ||
lookup_domain(daemon->namebuff, F_CONFIG, NULL, NULL))
{
/* if we forwarded a query for a locally known name (because it was for
an unknown type) and the answer is NXDOMAIN, convert that to NODATA,
since we know that the domain exists, even if upstream doesn't */
munged = 1;
header->hb3 |= HB3_AA;
SET_RCODE(header, NOERROR);
cache_secure = 0;
}
}
switch (extract_addresses(header, n, daemon->namebuff, now, ipsets, nftsets, is_sign, check_rebind, no_cache, cache_secure, &doctored))
if (!(header->hb3 & HB3_TC) && (!bogusanswer || (header->hb4 & HB4_CD)))
{
if (rcode == NXDOMAIN && extract_request(header, n, daemon->namebuff, NULL) &&
(check_for_local_domain(daemon->namebuff, now) || lookup_domain(daemon->namebuff, F_CONFIG, NULL, NULL)))
{
case 1:
my_syslog(LOG_WARNING, _("possible DNS-rebind attack detected: %s"), daemon->namebuff);
munged = 1;
/* if we forwarded a query for a locally known name (because it was for
an unknown type) and the answer is NXDOMAIN, convert that to NODATA,
since we know that the domain exists, even if upstream doesn't */
header->hb3 |= HB3_AA;
SET_RCODE(header, NOERROR);
cache_secure = 0;
}
if (daemon->doctors && do_doctor(header, n, daemon->namebuff))
cache_secure = 0;
/* check_for_bogus_wildcard() does it's own caching, so
don't call extract_addresses() if it triggers. */
if (daemon->bogus_addr && rcode != NXDOMAIN &&
check_for_bogus_wildcard(header, n, daemon->namebuff, now))
{
header->ancount = htons(0);
header->nscount = htons(0);
header->arcount = htons(0);
SET_RCODE(header, NXDOMAIN);
header->hb3 &= ~HB3_AA;
cache_secure = 0;
ede = EDE_BLOCKED;
break;
/* extract_addresses() found a malformed answer. */
case 2:
munged = 1;
SET_RCODE(header, SERVFAIL);
cache_secure = 0;
ede = EDE_OTHER;
break;
/* Pi-hole modification */
case 99:
cache_secure = 0;
// Make a private copy of the pheader to ensure
// we are not accidentially rewriting what is in
// the pheader when we're creating a crafted reply
// further below (when a query is to be blocked)
if (pheader)
{
pheader_copy = calloc(1, plen);
memcpy(pheader_copy, pheader, plen);
}
// Generate DNS packet for reply, a possibly existing pseudo header
// will be restored later inside resize_packet()
n = FTL_make_answer(header, ((char *) header) + 65536, n, &ede);
break;
}
else
{
int rc = extract_addresses(header, n, daemon->namebuff, now, ipsets, nftsets, is_sign, check_rebind, no_cache, cache_secure);
if (rcode == NOERROR && rrfilter(header, &n, RRFILTER_CONF) > 0)
ede = EDE_FILTERED;
if (rc != 0)
{
header->ancount = htons(0);
header->nscount = htons(0);
header->arcount = htons(0);
cache_secure = 0;
}
if (rc == 1)
{
my_syslog(LOG_WARNING, _("possible DNS-rebind attack detected: %s"), daemon->namebuff);
ede = EDE_BLOCKED;
}
if (rc == 2)
{
/* extract_addresses() found a malformed answer. */
SET_RCODE(header, SERVFAIL);
ede = EDE_OTHER;
}
/* Pi-hole modification */
if(rc == 99)
{
cache_secure = 0;
// Make a private copy of the pheader to ensure
// we are not accidentially rewriting what is in
// the pheader when we're creating a crafted reply
// further below (when a query is to be blocked)
if (pheader)
{
pheader_copy = calloc(1, plen);
memcpy(pheader_copy, pheader, plen);
}
// Generate DNS packet for reply, a possibly existing pseudo header
// will be restored later inside resize_packet()
n = FTL_make_answer(header, ((char *) header) + 65536, n, &ede);
}
}
if (doctored)
cache_secure = 0;
if (RCODE(header) == NOERROR && rrfilter(header, &n, RRFILTER_CONF) > 0)
ede = EDE_FILTERED;
}
#ifdef HAVE_DNSSEC
if (bogusanswer && !(header->hb4 & HB4_CD) && !option_bool(OPT_DNSSEC_DEBUG))
{
/* Bogus reply, turn into SERVFAIL */
SET_RCODE(header, SERVFAIL);
munged = 1;
}
if (option_bool(OPT_DNSSEC_VALID))
{
header->hb4 &= ~HB4_AD;
if (!(header->hb4 & HB4_CD) && ad_reqd && cache_secure)
if (bogusanswer)
{
if (!(header->hb4 & HB4_CD) && !option_bool(OPT_DNSSEC_DEBUG))
{
/* Bogus reply, turn into SERVFAIL */
SET_RCODE(header, SERVFAIL);
header->ancount = htons(0);
header->nscount = htons(0);
header->arcount = htons(0);
ede = EDE_DNSSEC_BOGUS;
}
}
else if (!(header->hb4 & HB4_CD) && ad_reqd && cache_secure)
header->hb4 |= HB4_AD;
/* If the requestor didn't set the DO bit, don't return DNSSEC info. */
@ -893,20 +910,9 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server
rrfilter(header, &n, RRFILTER_DNSSEC);
}
#endif
/* do this after extract_addresses. Ensure NODATA reply and remove
nameserver info. */
if (munged)
{
header->ancount = htons(0);
header->nscount = htons(0);
header->arcount = htons(0);
header->hb3 &= ~HB3_TC;
}
/* the bogus-nxdomain stuff, doctor and NXDOMAIN->NODATA munging can all elide
sections of the packet. Find the new length here and put back pseudoheader
if it was removed. */
/* the code above can elide sections of the packet. Find the new length here
and put back pseudoheader if it was removed. */
n = resize_packet(header, n, pheader_copy ? pheader_copy : pheader, plen);
/******** Pi-hole modification ********/
// The line above was modified to use
@ -931,6 +937,9 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server
static void dnssec_validate(struct frec *forward, struct dns_header *header,
ssize_t plen, int status, time_t now)
{
struct frec *orig;
int log_resource = 0;
daemon->log_display_id = forward->frec_src.log_id;
/* We've had a reply already, which we're validating. Ignore this duplicate */
@ -955,6 +964,9 @@ static void dnssec_validate(struct frec *forward, struct dns_header *header,
log_query(F_UPSTREAM | F_NOEXTRA, daemon->namebuff, NULL, "truncated", (forward->flags & FREC_DNSKEY_QUERY) ? T_DNSKEY : T_DS);
}
}
/* Find the original query that started it all.... */
for (orig = forward; orig->dependent; orig = orig->dependent);
/* As soon as anything returns BOGUS, we stop and unwind, to do otherwise
would invite infinite loops, since the answers to DNSKEY and DS queries
@ -962,18 +974,16 @@ static void dnssec_validate(struct frec *forward, struct dns_header *header,
if (!STAT_ISEQUAL(status, STAT_BOGUS) && !STAT_ISEQUAL(status, STAT_TRUNCATED) && !STAT_ISEQUAL(status, STAT_ABANDONED))
{
if (forward->flags & FREC_DNSKEY_QUERY)
status = dnssec_validate_by_ds(now, header, plen, daemon->namebuff, daemon->keyname, forward->class);
status = dnssec_validate_by_ds(now, header, plen, daemon->namebuff, daemon->keyname, forward->class, &orig->validate_counter);
else if (forward->flags & FREC_DS_QUERY)
status = dnssec_validate_ds(now, header, plen, daemon->namebuff, daemon->keyname, forward->class);
status = dnssec_validate_ds(now, header, plen, daemon->namebuff, daemon->keyname, forward->class, &orig->validate_counter);
else
status = dnssec_validate_reply(now, header, plen, daemon->namebuff, daemon->keyname, &forward->class,
!option_bool(OPT_DNSSEC_IGN_NS) && (forward->sentto->flags & SERV_DO_DNSSEC),
NULL, NULL, NULL);
#ifdef HAVE_DUMPFILE
if (STAT_ISEQUAL(status, STAT_BOGUS))
dump_packet_udp((forward->flags & (FREC_DNSKEY_QUERY | FREC_DS_QUERY)) ? DUMP_SEC_BOGUS : DUMP_BOGUS,
header, (size_t)plen, &forward->sentto->addr, NULL, -daemon->port);
#endif
NULL, NULL, NULL, &orig->validate_counter);
if (STAT_ISEQUAL(status, STAT_ABANDONED))
log_resource = 1;
}
/* Can't validate, as we're missing key data. Put this
@ -1018,18 +1028,19 @@ static void dnssec_validate(struct frec *forward, struct dns_header *header,
return;
}
}
else if (orig->work_counter-- == 0)
{
my_syslog(LOG_WARNING, _("limit exceeded: per-query subqueries"));
log_resource = 1;
}
else
{
struct server *server;
struct frec *orig;
void *hash;
size_t nn;
int serverind, fd;
struct randfd_list *rfds = NULL;
/* Find the original query that started it all.... */
for (orig = forward; orig->dependent; orig = orig->dependent);
/* Make sure we don't expire and free the orig frec during the
allocation of a new one: third arg of get_new_frec() does that. */
if ((serverind = dnssec_server(forward->sentto, daemon->keyname, NULL, NULL)) != -1 &&
@ -1038,7 +1049,6 @@ static void dnssec_validate(struct frec *forward, struct dns_header *header,
daemon->keyname, forward->class,
STAT_ISEQUAL(status, STAT_NEED_KEY) ? T_DNSKEY : T_DS, server->edns_pktsz)) &&
(hash = hash_questions(header, nn, daemon->namebuff)) &&
--orig->work_counter != 0 &&
(fd = allocate_rfd(&rfds, server)) != -1 &&
(new = get_new_frec(now, server, 1)))
{
@ -1104,6 +1114,21 @@ static void dnssec_validate(struct frec *forward, struct dns_header *header,
status = STAT_ABANDONED;
}
if (log_resource)
{
/* Log the actual validation that made us barf. */
unsigned char *p = (unsigned char *)(header+1);
if (extract_name(header, plen, &p, daemon->namebuff, 0, 4) == 1)
my_syslog(LOG_WARNING, _("validation of %s failed: resource limit exceeded."),
daemon->namebuff[0] ? daemon->namebuff : ".");
}
#ifdef HAVE_DUMPFILE
if (STAT_ISEQUAL(status, STAT_BOGUS) || STAT_ISEQUAL(status, STAT_ABANDONED))
dump_packet_udp((forward->flags & (FREC_DNSKEY_QUERY | FREC_DS_QUERY)) ? DUMP_SEC_BOGUS : DUMP_BOGUS,
header, (size_t)plen, &forward->sentto->addr, NULL, -daemon->port);
#endif
/* Validated original answer, all done. */
if (!forward->dependent)
return_reply(now, forward, header, plen, status);
@ -1112,7 +1137,7 @@ static void dnssec_validate(struct frec *forward, struct dns_header *header,
/* validated subsidiary query/queries, (and cached result)
pop that and return to the previous query/queries we were working on. */
struct frec *prev, *nxt = forward->dependent;
free_frec(forward);
while ((prev = nxt))
@ -1378,6 +1403,12 @@ static void return_reply(time_t now, struct frec *forward, struct dns_header *he
log_query(F_SECSTAT, domain, &a, result, 0);
}
}
if ((daemon->limit[LIMIT_CRYPTO] - forward->validate_counter) > (int)daemon->metrics[METRIC_CRYPTO_HWM])
daemon->metrics[METRIC_CRYPTO_HWM] = daemon->limit[LIMIT_CRYPTO] - forward->validate_counter;
if ((daemon->limit[LIMIT_WORK] - forward->work_counter) > (int)daemon->metrics[METRIC_WORK_HWM])
daemon->metrics[METRIC_WORK_HWM] = daemon->limit[LIMIT_WORK] - forward->work_counter;
#endif
if (option_bool(OPT_NO_REBIND))
@ -2107,7 +2138,7 @@ static ssize_t tcp_talk(int first, int last, int start, unsigned char *packet,
/* Recurse down the key hierarchy */
static int tcp_key_recurse(time_t now, int status, struct dns_header *header, size_t n,
int class, char *name, char *keyname, struct server *server,
int have_mark, unsigned int mark, int *keycount)
int have_mark, unsigned int mark, int *keycount, int *validatecount)
{
int first, last, start, new_status;
unsigned char *packet = NULL;
@ -2121,20 +2152,34 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si
int log_save;
/* limit the amount of work we do, to avoid cycling forever on loops in the DNS */
if (--(*keycount) == 0)
new_status = STAT_ABANDONED;
else if (STAT_ISEQUAL(status, STAT_NEED_KEY))
new_status = dnssec_validate_by_ds(now, header, n, name, keyname, class);
if (STAT_ISEQUAL(status, STAT_NEED_KEY))
new_status = dnssec_validate_by_ds(now, header, n, name, keyname, class, validatecount);
else if (STAT_ISEQUAL(status, STAT_NEED_DS))
new_status = dnssec_validate_ds(now, header, n, name, keyname, class);
new_status = dnssec_validate_ds(now, header, n, name, keyname, class, validatecount);
else
new_status = dnssec_validate_reply(now, header, n, name, keyname, &class,
!option_bool(OPT_DNSSEC_IGN_NS) && (server->flags & SERV_DO_DNSSEC),
NULL, NULL, NULL);
NULL, NULL, NULL, validatecount);
if (!STAT_ISEQUAL(new_status, STAT_NEED_DS) && !STAT_ISEQUAL(new_status, STAT_NEED_KEY))
if (!STAT_ISEQUAL(new_status, STAT_NEED_DS) && !STAT_ISEQUAL(new_status, STAT_NEED_KEY) && !STAT_ISEQUAL(new_status, STAT_ABANDONED))
break;
if ((*keycount)-- == 0)
{
my_syslog(LOG_WARNING, _("limit exceeded: per-query subqueries"));
new_status = STAT_ABANDONED;
}
if (STAT_ISEQUAL(new_status, STAT_ABANDONED))
{
/* Log the actual validation that made us barf. */
unsigned char *p = (unsigned char *)(header+1);
if (extract_name(header, n, &p, daemon->namebuff, 0, 4) == 1)
my_syslog(LOG_WARNING, _("validation of %s failed: resource limit exceeded."),
daemon->namebuff[0] ? daemon->namebuff : ".");
break;
}
/* Can't validate because we need a key/DS whose name now in keyname.
Make query for same, and recurse to validate */
if (!packet)
@ -2148,7 +2193,7 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si
new_status = STAT_ABANDONED;
break;
}
m = dnssec_generate_query(new_header, ((unsigned char *) new_header) + 65536, keyname, class,
STAT_ISEQUAL(new_status, STAT_NEED_KEY) ? T_DNSKEY : T_DS, server->edns_pktsz);
@ -2163,10 +2208,11 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si
daemon->log_display_id = ++daemon->log_id;
log_query_mysockaddr(F_NOEXTRA | F_DNSSEC | F_SERVER, keyname, &server->addr,
STAT_ISEQUAL(new_status, STAT_NEED_KEY) ? "dnssec-query[DNSKEY]" : "dnssec-query[DS]", 0);
new_status = tcp_key_recurse(now, new_status, new_header, m, class, name, keyname, server, have_mark, mark, keycount);
STAT_ISEQUAL(new_status, STAT_NEED_KEY) ? "dnssec-query[DNSKEY]" : "dnssec-query[DS]", 0);
new_status = tcp_key_recurse(now, new_status, new_header, m, class, name, keyname, server,
have_mark, mark, keycount, validatecount);
daemon->log_display_id = log_save;
if (!STAT_ISEQUAL(new_status, STAT_OK))
@ -2508,9 +2554,10 @@ unsigned char *tcp_request(int confd, time_t now,
#ifdef HAVE_DNSSEC
if (option_bool(OPT_DNSSEC_VALID) && !checking_disabled && (master->flags & SERV_DO_DNSSEC))
{
int keycount = DNSSEC_WORK; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */
int keycount = daemon->limit[LIMIT_WORK]; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */
int validatecount = daemon->limit[LIMIT_CRYPTO];
int status = tcp_key_recurse(now, STAT_OK, header, m, 0, daemon->namebuff, daemon->keyname,
serv, have_mark, mark, &keycount);
serv, have_mark, mark, &keycount, &validatecount);
char *result, *domain = "result";
union all_addr a;
@ -2536,6 +2583,12 @@ unsigned char *tcp_request(int confd, time_t now,
}
log_query(F_SECSTAT, domain, &a, result, 0);
if ((daemon->limit[LIMIT_CRYPTO] - validatecount) > (int)daemon->metrics[METRIC_CRYPTO_HWM])
daemon->metrics[METRIC_CRYPTO_HWM] = daemon->limit[LIMIT_CRYPTO] - validatecount;
if ((daemon->limit[LIMIT_WORK] - keycount) > (int)daemon->metrics[METRIC_WORK_HWM])
daemon->metrics[METRIC_WORK_HWM] = daemon->limit[LIMIT_WORK] - keycount;
}
#endif

View File

@ -165,7 +165,7 @@ static void sha256_transform(SHA256_CTX *ctx, const BYTE data[])
WORD a, b, c, d, e, f, g, h, i, j, t1, t2, m[64];
for (i = 0, j = 0; i < 16; ++i, j += 4)
m[i] = (data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | (data[j + 3]);
m[i] = (((WORD)data[j]) << 24) | (((WORD)data[j + 1]) << 16) | (((WORD)data[j + 2]) << 8) | (((WORD)data[j + 3]));
for ( ; i < 64; ++i)
m[i] = SIG1(m[i - 2]) + m[i - 7] + SIG0(m[i - 15]) + m[i - 16];

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -94,7 +94,7 @@ void inotify_dnsmasq_init()
if (daemon->inotifyfd == -1)
die(_("failed to create inotify: %s"), NULL, EC_MISC);
if (option_bool(OPT_NO_RESOLV))
if (daemon->port == 0 || option_bool(OPT_NO_RESOLV))
return;
for (res = daemon->resolv_files; res; res = res->next)

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -15,7 +15,6 @@
*/
#include "dnsmasq.h"
#ifdef HAVE_DHCP
static struct dhcp_lease *leases = NULL, *old_leases = NULL;
@ -28,8 +27,7 @@ static int read_leases(time_t now, FILE *leasestream)
struct dhcp_lease *lease;
int clid_len, hw_len, hw_type;
int items;
char *domain = NULL;
*daemon->dhcp_buff3 = *daemon->dhcp_buff2 = '\0';
/* client-id max length is 255 which is 255*2 digits + 254 colons
@ -69,8 +67,8 @@ static int read_leases(time_t now, FILE *leasestream)
if (inet_pton(AF_INET, daemon->namebuff, &addr.addr4))
{
if ((lease = lease4_allocate(addr.addr4)))
domain = get_domain(lease->addr);
lease = lease4_allocate(addr.addr4);
hw_len = parse_hex(daemon->dhcp_buff2, (unsigned char *)daemon->dhcp_buff2, DHCP_CHADDR_MAX, NULL, &hw_type);
/* For backwards compatibility, no explicit MAC address type means ether. */
@ -90,10 +88,7 @@ static int read_leases(time_t now, FILE *leasestream)
}
if ((lease = lease6_allocate(&addr.addr6, lease_type)))
{
lease_set_iaid(lease, strtoul(s, NULL, 10));
domain = get_domain6(&lease->addr6);
}
lease_set_iaid(lease, strtoul(s, NULL, 10));
}
#endif
else
@ -114,7 +109,7 @@ static int read_leases(time_t now, FILE *leasestream)
hw_len, hw_type, clid_len, now, 0);
if (strcmp(daemon->dhcp_buff, "*") != 0)
lease_set_hostname(lease, daemon->dhcp_buff, 0, domain, NULL);
lease_set_hostname(lease, daemon->dhcp_buff, 0, NULL, NULL);
ei = atol(daemon->dhcp_buff3);
@ -946,6 +941,36 @@ static void kill_name(struct dhcp_lease *lease)
lease->hostname = lease->fqdn = NULL;
}
void lease_calc_fqdns(void)
{
struct dhcp_lease *lease;
for (lease = leases; lease; lease = lease->next)
{
char *domain;
if (lease->hostname)
{
#ifdef HAVE_DHCP6
if (lease->flags & (LEASE_TA | LEASE_NA))
domain = get_domain6(&lease->addr6);
else
#endif
domain = get_domain(lease->addr);
if (domain)
{
/* This is called only during startup, before forking, hence safe_malloc() */
lease->fqdn = safe_malloc(strlen(lease->hostname) + strlen(domain) + 2);
strcpy(lease->fqdn, lease->hostname);
strcat(lease->fqdn, ".");
strcat(lease->fqdn, domain);
}
}
}
}
void lease_set_hostname(struct dhcp_lease *lease, const char *name, int auth, char *domain, char *config_domain)
{
struct dhcp_lease *lease_tmp;

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -24,6 +24,9 @@ const char * metric_names[] = {
"dns_local_answered",
"dns_stale_answered",
"dns_unanswered",
"dnssec_max_crypto_use",
"dnssec_max_sig_fail",
"dnssec_max_work",
"bootp",
"pxe",
"dhcp_ack",
@ -39,6 +42,7 @@ const char * metric_names[] = {
"leases_pruned_4",
"leases_allocated_6",
"leases_pruned_6",
"tcp_connections",
};
const char* get_metric_name(int i) {

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -23,6 +23,9 @@ enum {
METRIC_DNS_LOCAL_ANSWERED,
METRIC_DNS_STALE_ANSWERED,
METRIC_DNS_UNANSWERED_QUERY,
METRIC_CRYPTO_HWM,
METRIC_SIG_FAIL_HWM,
METRIC_WORK_HWM,
METRIC_BOOTP,
METRIC_PXE,
METRIC_DHCPACK,
@ -38,6 +41,7 @@ enum {
METRIC_LEASES_PRUNED_4,
METRIC_LEASES_ALLOCATED_6,
METRIC_LEASES_PRUNED_6,
METRIC_TCP_CONNECTIONS,
__METRIC_MAX,
};

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -929,15 +929,24 @@ static int make_sock(union mysockaddr *addr, int type, int dienow)
errno = errsave;
if (dienow)
/* Failure to bind addresses given by --listen-address at this point
because there's no interface with the address is OK if we're doing bind-dynamic.
If/when an interface is created with the relevant address we'll notice
and attempt to bind it then. This is in the generic error path so we close the socket,
but EADDRNOTAVAIL is only a possible error from bind()
When a new address is created and we call this code again (dienow == 0) there
may still be configured addresses when don't exist, (consider >1 --listen-address,
when the first is created, the second will still be missing) so we suppress
EADDRNOTAVAIL even in that case to avoid confusing log entries.
*/
if (!option_bool(OPT_CLEVERBIND) || errno != EADDRNOTAVAIL)
{
/* failure to bind addresses given by --listen-address at this point
is OK if we're doing bind-dynamic */
if (!option_bool(OPT_CLEVERBIND))
if (dienow)
die(s, daemon->addrbuff, EC_BADNET);
else
my_syslog(LOG_WARNING, s, daemon->addrbuff, strerror(errno));
}
else
my_syslog(LOG_WARNING, s, daemon->addrbuff, strerror(errno));
return -1;
}

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -43,7 +43,8 @@ int add_to_nftset(const char *setname, const union all_addr *ipaddr, int flags,
const char *cmd = remove ? cmd_del : cmd_add;
int ret, af = (flags & F_IPV4) ? AF_INET : AF_INET6;
size_t new_sz;
char *new, *err, *nl;
char *err_str, *new, *nl;
const char *err;
static char *cmd_buf = NULL;
static size_t cmd_buf_sz = 0;
@ -78,14 +79,19 @@ int add_to_nftset(const char *setname, const union all_addr *ipaddr, int flags,
}
ret = nft_run_cmd_from_buffer(ctx, cmd_buf);
err = (char *)nft_ctx_get_error_buffer(ctx);
err = nft_ctx_get_error_buffer(ctx);
if (ret != 0)
{
/* Log only first line of error return. */
if ((nl = strchr(err, '\n')))
*nl = 0;
my_syslog(LOG_ERR, "nftset %s %s", setname, err);
if ((err_str = whine_malloc(strlen(err) + 1)))
{
strcpy(err_str, err);
if ((nl = strchr(err_str, '\n')))
*nl = 0;
my_syslog(LOG_ERR, "nftset %s %s", setname, err_str);
free(err_str);
}
}
return ret;

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -194,6 +194,8 @@ struct myoption {
#define LOPT_FILTER_RR 381
#define LOPT_NO_DHCP6 382
#define LOPT_NO_DHCP4 383
#define LOPT_MAX_PROCS 384
#define LOPT_DNSSEC_LIMITS 385
#ifdef HAVE_GETOPT_LONG
static const struct option opts[] =
@ -225,7 +227,7 @@ static const struct myoption opts[] =
{ "domain-suffix", 1, 0, 's' },
{ "interface", 1, 0, 'i' },
{ "listen-address", 1, 0, 'a' },
{ "local-service", 0, 0, LOPT_LOCAL_SERVICE },
{ "local-service", 2, 0, LOPT_LOCAL_SERVICE },
{ "bogus-priv", 0, 0, 'b' },
{ "bogus-nxdomain", 1, 0, 'B' },
{ "ignore-address", 1, 0, LOPT_IGNORE_ADDR },
@ -367,6 +369,7 @@ static const struct myoption opts[] =
{ "dnssec-check-unsigned", 2, 0, LOPT_DNSSEC_CHECK },
{ "dnssec-no-timecheck", 0, 0, LOPT_DNSSEC_TIME },
{ "dnssec-timestamp", 1, 0, LOPT_DNSSEC_STAMP },
{ "dnssec-limits", 1, 0, LOPT_DNSSEC_LIMITS },
{ "dhcp-relay", 1, 0, LOPT_RELAY },
{ "ra-param", 1, 0, LOPT_RA_PARAM },
{ "quiet-dhcp", 0, 0, LOPT_QUIET_DHCP },
@ -388,6 +391,7 @@ static const struct myoption opts[] =
{ "fast-dns-retry", 2, 0, LOPT_FAST_RETRY },
{ "use-stale-cache", 2, 0 , LOPT_STALE_CACHE },
{ "no-ident", 0, 0, LOPT_NO_IDENT },
{ "max-tcp-connections", 1, 0, LOPT_MAX_PROCS },
{ NULL, 0, 0, 0 }
};
@ -570,12 +574,13 @@ static struct {
{ LOPT_DNSSEC_CHECK, ARG_DUP, NULL, gettext_noop("Ensure answers without DNSSEC are in unsigned zones."), NULL },
{ LOPT_DNSSEC_TIME, OPT_DNSSEC_TIME, NULL, gettext_noop("Don't check DNSSEC signature timestamps until first cache-reload"), NULL },
{ LOPT_DNSSEC_STAMP, ARG_ONE, "<path>", gettext_noop("Timestamp file to verify system clock for DNSSEC"), NULL },
{ LOPT_DNSSEC_LIMITS, ARG_ONE, "<limit>,..", gettext_noop("Set resource limits for DNSSEC validation"), NULL },
{ LOPT_RA_PARAM, ARG_DUP, "<iface>,[mtu:<value>|<interface>|off,][<prio>,]<intval>[,<lifetime>]", gettext_noop("Set MTU, priority, resend-interval and router-lifetime"), NULL },
{ LOPT_QUIET_DHCP, OPT_QUIET_DHCP, NULL, gettext_noop("Do not log routine DHCP."), NULL },
{ LOPT_QUIET_DHCP6, OPT_QUIET_DHCP6, NULL, gettext_noop("Do not log routine DHCPv6."), NULL },
{ LOPT_QUIET_RA, OPT_QUIET_RA, NULL, gettext_noop("Do not log RA."), NULL },
{ LOPT_LOG_DEBUG, OPT_LOG_DEBUG, NULL, gettext_noop("Log debugging information."), NULL },
{ LOPT_LOCAL_SERVICE, OPT_LOCAL_SERVICE, NULL, gettext_noop("Accept queries only from directly-connected networks."), NULL },
{ LOPT_LOCAL_SERVICE, ARG_ONE, NULL, gettext_noop("Accept queries only from directly-connected networks."), NULL },
{ LOPT_LOOP_DETECT, OPT_LOOP_DETECT, NULL, gettext_noop("Detect and remove DNS forwarding loops."), NULL },
{ LOPT_IGNORE_ADDR, ARG_DUP, "<ipaddr>", gettext_noop("Ignore DNS responses containing ipaddr."), NULL },
{ LOPT_DHCPTTL, ARG_ONE, "<ttl>", gettext_noop("Set TTL in DNS responses with DHCP-derived addresses."), NULL },
@ -589,6 +594,7 @@ static struct {
{ LOPT_NORR, OPT_NORR, NULL, gettext_noop("Suppress round-robin ordering of DNS records."), NULL },
{ LOPT_NO_IDENT, OPT_NO_IDENT, NULL, gettext_noop("Do not add CHAOS TXT records."), NULL },
{ LOPT_CACHE_RR, ARG_DUP, "<RR-type>", gettext_noop("Cache this DNS resource record type."), NULL },
{ LOPT_MAX_PROCS, ARG_ONE, "<integer>", gettext_noop("Maximum number of concurrent tcp connections."), NULL },
{ 0, 0, NULL, NULL, NULL }
};
@ -1283,6 +1289,17 @@ static char *domain_rev6(int from_file, char *server, struct in6_addr *addr6, in
return NULL;
}
static void if_names_add(const char *ifname)
{
struct iname *new = opt_malloc(sizeof(struct iname));
new->next = daemon->if_names;
daemon->if_names = new;
/* new->name may be NULL if someone does
"interface=" to disable all interfaces except loop. */
new->name = opt_string_alloc(ifname);
new->flags = 0;
}
#ifdef HAVE_DHCP
static int is_tag_prefix(char *arg)
@ -1412,7 +1429,6 @@ static void dhcp_opt_free(struct dhcp_opt *opt)
free(opt);
}
/* This is too insanely large to keep in-line in the switch */
static int parse_dhcp_opt(char *errstr, char *arg, int flags)
{
@ -2836,14 +2852,8 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma
case 'i': /* --interface */
do {
struct iname *new = opt_malloc(sizeof(struct iname));
comma = split(arg);
new->next = daemon->if_names;
daemon->if_names = new;
/* new->name may be NULL if someone does
"interface=" to disable all interfaces except loop. */
new->name = opt_string_alloc(arg);
new->flags = 0;
comma = split(arg);
if_names_add(arg);
arg = comma;
} while (arg);
break;
@ -3409,6 +3419,15 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma
ret_err(gen_err);
else if (daemon->max_logs > 100)
daemon->max_logs = 100;
break;
case LOPT_LOCAL_SERVICE: /* --local-service */
if (!arg || !strcmp(arg, "net"))
set_option_bool(OPT_LOCAL_SERVICE);
else if (!strcmp(arg, "host"))
set_option_bool(OPT_LOCALHOST_SERVICE);
else
ret_err(gen_err);
break;
case 'P': /* --edns-packet-max */
@ -5246,6 +5265,24 @@ err:
}
#ifdef HAVE_DNSSEC
case LOPT_DNSSEC_LIMITS:
{
int lim, val;
for (lim = LIMIT_SIG_FAIL; arg && lim < LIMIT_MAX ; lim++, arg = comma)
{
comma = split(arg);
if (!atoi_check(arg, &val))
ret_err(gen_err);
if (val != 0)
daemon->limit[lim] = val;
}
break;
}
case LOPT_DNSSEC_STAMP: /* --dnssec-timestamp */
daemon->timestamp_file = opt_string_alloc(arg);
break;
@ -5317,7 +5354,17 @@ err:
break;
}
#endif
case LOPT_MAX_PROCS: /* --max-tcp-connections */
{
int max_procs;
/* Don't accept numbers less than 1. */
if (!atoi_check(arg, &max_procs) || max_procs < 1)
ret_err(gen_err);
daemon->max_procs = max_procs;
break;
}
default:
ret_err(_("unsupported option (check that dnsmasq was compiled with DHCP/TFTP/DNSSEC/DBus support)"));
@ -5822,6 +5869,7 @@ void read_opts(int argc, char **argv, char *compile_opts)
daemon = opt_malloc(sizeof(struct daemon));
memset(daemon, 0, sizeof(struct daemon));
daemon->namebuff = buff;
daemon->workspacename = safe_malloc((MAXDNAME * 2) + 1);
daemon->addrbuff = safe_malloc(ADDRSTRLEN);
/* Set defaults - everything else is zero or NULL */
@ -5845,6 +5893,13 @@ void read_opts(int argc, char **argv, char *compile_opts)
daemon->soa_expiry = SOA_EXPIRY;
daemon->randport_limit = 1;
daemon->host_index = SRC_AH;
daemon->max_procs = MAX_PROCS;
#ifdef HAVE_DNSSEC
daemon->limit[LIMIT_SIG_FAIL] = DNSSEC_LIMIT_SIG_FAIL;
daemon->limit[LIMIT_CRYPTO] = DNSSEC_LIMIT_CRYPTO;
daemon->limit[LIMIT_WORK] = DNSSEC_LIMIT_WORK;
daemon->limit[LIMIT_NSEC3_ITERS] = DNSSEC_LIMIT_NSEC3_ITERS;
#endif
/* See comment above make_servers(). Optimises server-read code. */
mark_servers(0);
@ -6148,7 +6203,16 @@ void read_opts(int argc, char **argv, char *compile_opts)
/* If there's access-control config, then ignore --local-service, it's intended
as a system default to keep otherwise unconfigured installations safe. */
if (daemon->if_names || daemon->if_except || daemon->if_addrs || daemon->authserver)
reset_option_bool(OPT_LOCAL_SERVICE);
{
reset_option_bool(OPT_LOCAL_SERVICE);
reset_option_bool(OPT_LOCALHOST_SERVICE);
}
else if (option_bool(OPT_LOCALHOST_SERVICE) && !option_bool(OPT_LOCAL_SERVICE))
{
/* listen only on localhost, emulate --interface=lo --bind-interfaces */
if_names_add(NULL);
set_option_bool(OPT_NOWILD);
}
if (testmode)
{

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -213,6 +213,14 @@ size_t rrfilter(struct dns_header *header, size_t *plen, int mode)
if (i < ntohs(header->ancount) && type == qtype && class == qclass)
continue;
}
else if (qtype == T_ANY && rr_on_list(daemon->filter_rr, T_ANY))
{
/* Filter replies to ANY queries in the spirit of
RFC RFC 8482 para 4.3 */
if (class != C_IN ||
type == T_A || type == T_AAAA || type == T_MX || type == T_CNAME)
continue;
}
else
{
/* Only looking at answer section now. */

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -585,8 +585,13 @@ static struct tftp_file *check_tftp_fileperm(ssize_t *len, char *prefix, char *c
void check_tftp_listeners(time_t now)
{
struct listener *listener;
struct tftp_transfer *transfer, *tmp, **up;
for (listener = daemon->listeners; listener; listener = listener->next)
if (listener->tftpfd != -1 && poll_check(listener->tftpfd, POLLIN))
tftp_request(listener, now);
/* In single port mode, all packets come via port 69 and tftp_request() */
if (!option_bool(OPT_SINGLE_PORT))
for (transfer = daemon->tftp_trans; transfer; transfer = transfer->next)

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,4 +1,4 @@
/* dnsmasq is Copyright (c) 2000-2023 Simon Kelley
/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -119,7 +119,7 @@ int rr_on_list(struct rrlist *list, unsigned short rr)
{
while (list)
{
if (list->rr == rr || list->rr == T_ANY)
if (list->rr == rr)
return 1;
list = list->next;

View File

@ -1417,10 +1417,10 @@ static bool _FTL_check_blocking(int queryID, int domainID, int clientID, const c
break;
}
// Skip all checks and continue if we hit already at least one whitelist in the chain
// Skip all checks and continue if we hit already at least one allowlist in the chain
if(query->flags.allowed)
{
log_debug(DEBUG_QUERIES, "Query is permitted as at least one whitelist entry matched");
log_debug(DEBUG_QUERIES, "Query is permitted as at least one allowlist entry matched");
return false;
}
@ -2423,6 +2423,17 @@ static void FTL_dnssec(const char *arg, const union all_addr *addr, const int id
else
log_warn("Unknown DNSSEC status \"%s\"", arg);
// Set reply to NONE (if not already set) as we will not reply to this
// query when the status is neither SECURE nor INSECURE
if (query->reply == REPLY_UNKNOWN &&
query->dnssec != DNSSEC_SECURE &&
query->dnssec != DNSSEC_INSECURE)
{
struct timeval response;
gettimeofday(&response, 0);
query_set_reply(0, REPLY_NONE, addr, query, response);
}
// Mark query for updating in the database
query->flags.database.changed = true;
@ -3486,3 +3497,9 @@ static const char *check_dnsmasq_name(const char *name)
// else
return name;
}
void get_dnsmasq_metrics_obj(cJSON *json)
{
for (unsigned int i = 0; i < __METRIC_MAX; i++)
cJSON_AddNumberToObject(json, get_metric_name(i), daemon->metrics[i]);
}

View File

@ -189,7 +189,9 @@ enum gravity_list_type {
GRAVITY_ADLISTS,
GRAVITY_CLIENTS,
GRAVITY_GRAVITY,
GRAVITY_ANTIGRAVITY
GRAVITY_ANTIGRAVITY,
GRAVITY_ADLISTS_BLOCK,
GRAVITY_ADLISTS_ALLOW
} __attribute__ ((packed));
enum gravity_tables {

View File

@ -221,7 +221,7 @@ void ls_dir(const char* path)
format_memory_size(prefix, (unsigned long long)st.st_size, &formatted);
// Log output for this file
log_info("%s %-15s %3.0f%s %s", permissions, usergroup, formatted, prefix, filename);
log_info("%s %-15s %3.0f%s %s", permissions, usergroup, formatted, strlen(prefix) > 0 ? prefix : " ", filename);
}
log_info("---------------------------------------------------");

View File

@ -10,6 +10,8 @@
#ifndef METRICS_H
#define METRICS_H
#include "webserver/cJSON/cJSON.h"
// defined in src/dnsmasq/cache.c
const char *rrtype_name(unsigned short type);
@ -88,4 +90,6 @@ struct metrics
void get_dnsmasq_metrics(struct metrics *ci);
void get_dnsmasq_metrics_obj(cJSON *json);
#endif // METRICS_H

View File

@ -54,7 +54,7 @@ inline bool __attribute__((pure)) valid_domain(const char *domain, const size_t
// Domain must not contain any character other than [a-zA-Z0-9.-_]
if(domain[i] != '-' && domain[i] != '.' && domain[i] != '_' &&
(domain[i] < 'a' || domain[i] > 'z') &&
// (domain[i] < 'A' || domain[i] > 'Z') && // not needed as all domains are converted to lowercase
(domain[i] < 'A' || domain[i] > 'Z') &&
(domain[i] < '0' || domain[i] > '9'))
return false;

View File

@ -78,6 +78,8 @@ src/dnsmasq/dnsmasq.c
my_syslog(LOG_WARNING, _("failed to access %s: %s"), res->name, strerror(errno));
src/dnsmasq/dnsmasq.c
my_syslog(LOG_WARNING, _("no servers found in %s, will retry"), latest->name);
src/dnsmasq/dnssec.c
my_syslog(LOG_WARNING, "limit exceeded: %s", message ? message : _("per-query crypto work"));
src/dnsmasq/dnssec.c
my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name);
src/dnsmasq/forward.c
@ -85,11 +87,21 @@ src/dnsmasq/forward.c
src/dnsmasq/forward.c
my_syslog(LOG_WARNING, _("nameserver %s refused to do a recursive query"), daemon->namebuff);
src/dnsmasq/forward.c
my_syslog(LOG_WARNING, _("possible DNS-rebind attack detected: %s"), daemon->namebuff);
my_syslog(LOG_WARNING, _("possible DNS-rebind attack detected: %s"), daemon->namebuff);
src/dnsmasq/forward.c
my_syslog(LOG_WARNING, _("limit exceeded: per-query subqueries"));
src/dnsmasq/forward.c
my_syslog(LOG_WARNING, _("validation of %s failed: resource limit exceeded."),
daemon->namebuff[0] ? daemon->namebuff : ".");
src/dnsmasq/forward.c
my_syslog(LOG_WARNING, _("reducing DNS packet size for nameserver %s to %d"), daemon->addrbuff, SAFE_PKTSZ);
src/dnsmasq/forward.c
my_syslog(LOG_WARNING, _("ignoring query from non-local network %s (logged only once)"), daemon->addrbuff);
src/dnsmasq/forward.c
my_syslog(LOG_WARNING, _("limit exceeded: per-query subqueries"));
src/dnsmasq/forward.c
my_syslog(LOG_WARNING, _("validation of %s failed: resource limit exceeded."),
daemon->namebuff[0] ? daemon->namebuff : ".");
src/dnsmasq/forward.c
my_syslog(LOG_WARNING, _("ignoring query from non-local network %s"), daemon->addrbuff);
src/dnsmasq/forward.c
@ -108,7 +120,7 @@ src/dnsmasq/lease.c
src/dnsmasq/log.c
my_syslog(LOG_WARNING, _("overflow: %d log entries lost"), e);
src/dnsmasq/network.c
my_syslog(LOG_WARNING, s, daemon->addrbuff, strerror(errno));
my_syslog(LOG_WARNING, s, daemon->addrbuff, strerror(errno));
src/dnsmasq/network.c
my_syslog(LOG_WARNING,
_("LOUD WARNING: listening on %s may accept requests via interfaces other than %s"),

View File

@ -114,6 +114,9 @@ pdnsutil add-record ftl. regex-multiple AAAA fe80::3f41
pdnsutil add-record ftl. regex-notMultiple A 192.168.3.12
pdnsutil add-record ftl. regex-notMultiple AAAA fe80::3f41
# TXT
pdnsutil add-record ftl. any TXT "\"Some example text\""
# Create reverse lookup zone
pdnsutil create-zone arpa ns1.ftl
pdnsutil add-record arpa. 1.1.168.192.in-addr PTR ftl.

View File

@ -487,6 +487,15 @@
# How often do we store queries in FTL's database [seconds]?
DBinterval = 60
# Should FTL enable Write-Ahead Log (WAL) mode for the on-disk query database
# (configured via files.database)?
# It is recommended to leave this setting enabled for performance reasons. About the
# only reason to disable WAL mode is if you are experiencing specific issues with it,
# e.g., when using a database that is accessed from multiple hosts via a network
# share. When this setting is disabled, FTL will use SQLite3's default journal mode
# (rollback journal in DELETE mode).
useWAL = true
[database.network]
# Should FTL analyze the local ARP cache? When disabled, client identification and the
# network table will stop working reliably.

View File

@ -311,6 +311,8 @@
printf "%s\n" "${lines[@]}"
[[ ${lines[@]} == *"192.168.3.1"* ]]
[[ ${lines[@]} == *"fe80::3c01"* ]]
# TXT records should not be returned due to filter-rr=ANY
[[ ${lines[@]} != *"Some example text"* ]]
}
@test "Local DNS test: CNAME cname-ok.ftl" {
@ -1420,12 +1422,135 @@
[[ ${lines[0]} == "145" ]]
}
@test "Check /api/lists?type=block returning only blocking lists" {
run bash -c 'curl -s 127.0.0.1/api/lists?type=block | jq ".lists[].type"'
printf "%s\n" "${lines[@]}"
# Check no allow entries are present
[[ ${lines[@]} != *"allow"* ]]
}
@test "Check /api/lists?type=allow returning only allowing lists" {
run bash -c 'curl -s 127.0.0.1/api/lists?type=allow | jq ".lists[].type"'
printf "%s\n" "${lines[@]}"
# Check no block entries are present
[[ ${lines[@]} != *"block"* ]]
}
@test "Check /api/lists without type parameter returning all lists" {
run bash -c 'curl -s 127.0.0.1/api/lists | jq ".lists[].type"'
printf "%s\n" "${lines[@]}"
# Check both block and allow entries are present
[[ ${lines[@]} == *"allow"* ]]
[[ ${lines[@]} == *"block"* ]]
}
@test "API: No UNKNOWN reply in API" {
run bash -c 'curl -s 127.0.0.1/api/queries?reply=UNKNOWN | jq .queries'
printf "%s\n" "${lines[@]}"
run bash -c 'curl -s 127.0.0.1/api/queries?reply=UNKNOWN | jq ".queries | length"'
[[ ${lines[0]} == "0" ]]
}
@test "API: No UNKNOWN status in API" {
run bash -c 'curl -s 127.0.0.1/api/queries?status=UNKNOWN | jq .queries'
printf "%s\n" "${lines[@]}"
run bash -c 'curl -s 127.0.0.1/api/queries?status=UNKNOWN | jq ".queries | length"'
[[ ${lines[0]} == "0" ]]
}
@test "API authorization (without password): No login required" {
run bash -c 'curl -s 127.0.0.1/api/auth'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == '{"session":{"valid":true,"totp":false,"sid":null,"validity":-1},"took":'*'}' ]]
}
@test "Config validation working on the CLI (type-based checking)" {
run bash -c './pihole-FTL --config dns.port true'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == 'Config setting dns.port is invalid, allowed options are: unsigned integer (16 bit)' ]]
[[ $status == 2 ]]
run bash -c './pihole-FTL --config dns.revServers "abc"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == 'Config setting dns.revServers is invalid: not valid JSON, error before: abc' ]]
[[ $status == 2 ]]
}
@test "Config validation working on the API (type-based checking)" {
run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config -d "{\"config\":{\"dns\":{\"blockESNI\":15.5}}}"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "{\"error\":{\"key\":\"bad_request\",\"message\":\"Config item is invalid\",\"hint\":\"dns.blockESNI: not of type bool\"},\"took\":"*"}" ]]
run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config -d "{\"config\":{\"dns\":{\"piholePTR\":\"something_else\"}}}"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "{\"error\":{\"key\":\"bad_request\",\"message\":\"Config item is invalid\",\"hint\":\"dns.piholePTR: invalid option\"},\"took\":"*"}" ]]
}
@test "Config validation working on the CLI (validator-based checking)" {
run bash -c './pihole-FTL --config dns.hosts "[\"111.222.333.444 abc\"]"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == 'Invalid value: dns.hosts[0]: neither a valid IPv4 nor IPv6 address ("111.222.333.444")' ]]
[[ $status == 3 ]]
run bash -c './pihole-FTL --config dns.hosts "[\"1.1.1.1 cf\",\"8.8.8.8 google\",\"1.2.3.4\"]"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == 'Invalid value: dns.hosts[2]: entry does not have at least one hostname ("1.2.3.4")' ]]
[[ $status == 3 ]]
run bash -c './pihole-FTL --config dns.revServers "[\"abc,def,ghi\"]"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == 'Invalid value: dns.revServers[0]: <enabled> not a boolean ("abc")' ]]
[[ $status == 3 ]]
run bash -c './pihole-FTL --config dns.revServers "[\"true,abc,def,ghi\"]"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == 'Invalid value: dns.revServers[0]: <ip-address> neither a valid IPv4 nor IPv6 address ("abc")' ]]
[[ $status == 3 ]]
run bash -c './pihole-FTL --config dns.revServers "[\"true,1.2.3.4/55,def,ghi\"]"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == 'Invalid value: dns.revServers[0]: <prefix-len> not a valid IPv4 prefix length ("55")' ]]
[[ $status == 3 ]]
run bash -c './pihole-FTL --config dns.revServers "[\"true,::1/255,def,ghi\"]"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == 'Invalid value: dns.revServers[0]: <prefix-len> not a valid IPv6 prefix length ("255")' ]]
[[ $status == 3 ]]
run bash -c './pihole-FTL --config dns.revServers "[\"true,1.1.1.1,def\"]"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == 'Invalid value: dns.revServers[0]: entry does not have all required elements (<enabled>,<ip-address>[/<prefix-len>],<server>[#<port>],<domain>)' ]]
[[ $status == 3 ]]
run bash -c './pihole-FTL --config dns.revServers "[\"true,1.1.1.1,def,ghi\"]"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == 'New dnsmasq configuration is not valid ('*'Name does not resolve at line '*' of /etc/pihole/dnsmasq.conf.temp: "rev-server=1.1.1.1,def"), config remains unchanged' ]]
[[ $status == 3 ]]
run bash -c './pihole-FTL --config webserver.api.excludeClients "[\".*\",\"$$$\",\"[[[\"]"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == 'Invalid value: webserver.api.excludeClients[2]: not a valid regex ("[[["): Missing '\'']'\' ]]
[[ $status == 3 ]]
}
@test "Config validation working on the API (validator-based checking)" {
run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config -d "{\"config\":{\"files\":{\"pcap\":\"%gh4b\"}}}"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "{\"error\":{\"key\":\"bad_request\",\"message\":\"Config item validation failed\",\"hint\":\"files.pcap: not a valid file path (\\\"%gh4b\\\")\"},\"took\":"*"}" ]]
run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config -d "{\"config\":{\"dns\":{\"cnameRecords\":[\"a\"]}}}"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "{\"error\":{\"key\":\"bad_request\",\"message\":\"Config item validation failed\",\"hint\":\"dns.cnameRecords[0]: not a valid CNAME definition (too few elements)\"},\"took\":"*"}" ]]
run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config -d "{\"config\":{\"dns\":{\"cnameRecords\":[\"a,b,c\",\"a,b,c,,c\"]}}}"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "{\"error\":{\"key\":\"bad_request\",\"message\":\"Config item validation failed\",\"hint\":\"dns.cnameRecords[1]: contains an empty string at position 3\"},\"took\":"*"}" ]]
run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config -d "{\"config\":{\"dns\":{\"cnameRecords\":[\"a,b,c\",\"a,b,c\",5]}}}"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "{\"error\":{\"key\":\"bad_request\",\"message\":\"Config item is invalid\",\"hint\":\"dns.cnameRecords: array has invalid elements\"},\"took\":"*"}" ]]
}
@test "Create, set, and use application password" {
run bash -c 'curl -s 127.0.0.1/api/auth/app'
printf "%s\n" "${lines[@]}"