How to keep 2 PiHole 6.0 Instances synced?

hey,

is there a way for pihole v6.0 with 2 instances to keep them synced up?

I know there is GitHub - vmstan/gravity-sync: 💫 The easy way to synchronize the DNS configuration of two Pi-hole 5.x instances.
But it looks like the project does not support 6.0

Any other custom ways?
Thanks!

It sort of works...for me it seems to replicate the custom blocked sites list. I had to change the folder it was pointing to for LOCAL DNS entries but not sure that's working. If you edit /usr/local/bin/gravity-sync, you'll see some constants near the top of that file. One line is PH_CUSTOM_DNS. Change it to PH_CUSTOM_DNS=${PH_CUSTOM_DNS:-'hosts/custom.list'} (you can see I just added the hosts part since that's the new location).

I'd like to see this functionality built into Pi-hole and sync'd real-time.

1 Like

I see no V6 support.

Not yet. There are plans.

https://github.com/mattwebbio/orbital-sync/issues/190

2 Likes

Yeah - working on it now, but there's actually still some bugs in the Teleporter in Pi Hole caused by bugs in the webserver so it's a bit of a rabbit hole.

Thanks for your work. If you need a beta tester, let me know.

1 Like

I created GitHub - lovelaze/nebula-sync: Synchronize configuration of multiple Pi-hole v6.x instances. last week for syncing pihole v6 instances

Cheers @lovelaze !

I just set it up and it instantly works! Loving the manual config to since i run different DHCP Ranges so i can exclude them.

A different question:
Whats the difference between

SYNC_CONFIG_DNS

and

SYNC_CONFIG_RESOLVER ?

I will try to make the readme more clear about what the sync settings actually include, but for now a good way to visualize what is synced is to visit and enable "all settings" on your pihole:

http://pihole/admin/settings/all

I have not yet enabled syncing "Webserver and API" and "File locations"

ty!

The all settings, well, ..., already contains everything. Synchronizing v6 Pi-holes should be as doable in two lines of code. GET /api/config on the first and then POST /api/config exactly what you received on the second. This should create an exact clone of the first Pi-hole.

All the other pages are there to show less content at once with more descriptions for each - just to present the same content in a less overwhelmingly format especially for first-time users.

2 Likes

Yes and no. For a full sync a GET and PATCH to /api/config makes sense indeed.

But in this context what FurkanVG is asking for is regarding a partial sync, as in a patch of a subset of the fields in the GET /api/config response.

Thus, is there a good way in the web interface to find out what settings these objects contain? I assumed this is exactly what the tabs in "all settings" display.

{
  "config": {
    "dns": {},
    "dhcp": {},
    "ntp": {},
    "resolver": {},
    "database": {},
    "webserver": {},
    "files": {},
    "misc": {},
    "debug": {}
  }
}

This is correct. The headings of the boxes in All Settings match the objects name. This page is dynamically generated from API response and therefor always in-sync with the local API response to /config

1 Like

Can this be done via the API?

$ curl -s http://localhost/api/endpoints | jq ".endpoints[][].uri"
[..]
"/api/stats/database/query_types"
"/api/stats/database/upstreams"
"/api/config"
"/api/config"
"/api/network/gateway"
"/api/network/routes"
[..]
$ curl -s http://localhost/api/config | jq
{
  "config": {
    "dns": {
      "upstreams": [
        "127.0.0.1#5335"
      ],
      "CNAMEdeepInspect": true,
      "blockESNI": true,
      "EDNS0ECS": true,
      "ignoreLocalhost": false,
      "showDNSSEC": true,
      "analyzeOnlyAandAAAA": false,
      "piholePTR": "PI.HOLE",
      "replyWhenBusy": "ALLOW",
      "blockTTL": 2,
      "hosts": [
        XXXXX
        XXXXX
        XXXXX
      ],
      "domainNeeded": true,
      "expandHosts": true,
      "domain": "home.dehakkelaar.nl",
      "bogusPriv": true,
      "dnssec": false,
      "interface": "eth0",
      "hostRecord": "",
      "listeningMode": "LOCAL",
      "queryLogging": true,
      "cnameRecords": [],
      "port": 53,
      "revServers": [
        "true,10.0.0.0/24,10.0.0.2,home.dehakkelaar.nl"
      ],
      "cache": {
        "size": 10000,
        "optimizer": 3600
      },
      "blocking": {
        "active": true,
        "mode": "NULL"
      },
      "specialDomains": {
        "mozillaCanary": true,
        "iCloudPrivateRelay": true
      },
      "reply": {
        "host": {
          "force4": false,
          "IPv4": "",
          "force6": false,
          "IPv6": ""
        },
        "blocking": {
          "force4": false,
          "IPv4": "",
          "force6": false,
          "IPv6": ""
        }
      },
      "rateLimit": {
        "count": 1000,
        "interval": 60
      }
    },
    "dhcp": {
      "active": false,
      "start": "10.0.0.11",
      "end": "10.0.0.254",
      "router": "10.0.0.1",
      "netmask": "",
      "leaseTime": "24h",
      "ipv6": false,
      "rapidCommit": false,
      "multiDNS": true,
      "logging": false,
      "ignoreUnknownClients": false,
      "hosts": []
    },
    "ntp": {
      "ipv4": {
        "active": true,
        "address": ""
      },
      "ipv6": {
        "active": true,
        "address": ""
      },
      "sync": {
        "active": true,
        "server": "pool.ntp.org",
        "interval": 3600,
        "count": 8,
        "rtc": {
          "set": true,
          "device": "",
          "utc": true
        }
      }
    },
    "resolver": {
      "resolveIPv4": true,
      "resolveIPv6": true,
      "networkNames": true,
      "refreshNames": "IPV4_ONLY"
    },
    "database": {
      "DBimport": true,
      "maxDBdays": 91,
      "DBinterval": 60,
      "useWAL": true,
      "network": {
        "parseARPcache": true,
        "expire": 91
      }
    },
    "webserver": {
      "domain": "pi.hole",
      "acl": "",
      "port": "80,[::]:80,443s,[::]:443s",
      "session": {
        "timeout": 300,
        "restore": true
      },
      "tls": {
        "cert": "/etc/pihole/tls.pem"
      },
      "paths": {
        "webroot": "/var/www/html",
        "webhome": "/admin/"
      },
      "interface": {
        "boxed": true,
        "theme": "default-auto"
      },
      "api": {
        "max_sessions": 16,
        "prettyJSON": false,
        "pwhash": "",
        "password": "********",
        "totp_secret": "********",
        "app_pwhash": "",
        "app_sudo": false,
        "cli_pw": true,
        "excludeClients": [],
        "excludeDomains": [],
        "maxHistory": 86400,
        "maxClients": 10,
        "client_history_global_max": true,
        "allow_destructive": true,
        "temp": {
          "limit": 60,
          "unit": "C"
        }
      }
    },
    "files": {
      "pid": "/run/pihole-FTL.pid",
      "database": "/etc/pihole/pihole-FTL.db",
      "gravity": "/etc/pihole/gravity.db",
      "gravity_tmp": "/tmp",
      "macvendor": "/etc/pihole/macvendor.db",
      "setupVars": "/etc/pihole/setupVars.conf",
      "pcap": "",
      "log": {
        "ftl": "/var/log/pihole/FTL.log",
        "dnsmasq": "/var/log/pihole/pihole.log",
        "webserver": "/var/log/pihole/webserver.log"
      }
    },
    "misc": {
      "privacylevel": 0,
      "delay_startup": 0,
      "nice": -10,
      "addr2line": true,
      "etc_dnsmasq_d": false,
      "dnsmasq_lines": [],
      "extraLogging": false,
      "readOnly": false,
      "check": {
        "load": true,
        "shmem": 90,
        "disk": 90
      }
    },
    "debug": {
      "database": false,
      "networking": false,
      "locks": false,
      "queries": true,
      "flags": false,
      "shmem": false,
      "gc": false,
      "arp": false,
      "regex": false,
      "api": false,
      "tls": false,
      "overtime": false,
      "status": false,
      "caps": false,
      "dnssec": false,
      "vectors": false,
      "resolver": false,
      "edns0": false,
      "clients": false,
      "aliasclients": false,
      "events": false,
      "helper": false,
      "config": false,
      "inotify": false,
      "webserver": false,
      "extra": false,
      "reserved": false,
      "ntp": false,
      "all": false
    }
  },
  "took": 0.00011277198791503906
}

Truly amazing!
10.0.0.5 is another Pi-hole v6 instance:

$ curl -s http://localhost/api/config | jq
{
  "config": {
    "dns": {
      "upstreams": [
        "8.8.8.8",
        "8.8.4.4"
      ],
[..]
$ curl -s --request PATCH -d "$(curl -s http://10.0.0.5/api/config)" http://localhost/api/config | jq
{
  "config": {
    "dns": {
      "upstreams": [
        "127.0.0.1#5335"
      ],
[..]
$ curl -s http://localhost/api/config | jq
{
  "config": {
    "dns": {
      "upstreams": [
        "127.0.0.1#5335"
      ],
[..]

Two further hints. Even though they are documented in the specs, they can easily be missed:

  1. There is also a special detailed mode for GET which will give you human-readable descriptions/texts as well as hints which values have actually been changed and, if so, what the default value would have been. Use GET api/config?detailed=true for this. It is (obviously) read-only so I'm not exactly sure how useful it is in your case.

  2. Note that you can also explicitly request only a subset if you don't need much. Saves a bit of computing time and traffic (even when both shouldn't matter much), e.g. GET /api/config/dns/upstreams returning only

    {
      "config": {
        "dns": {
          "upstreams": [
            "127.0.0.1#5335"
          ]
        }
      },
      "took": 0.000024557113647460938
    }
    
1 Like

How sweet!

I tried syncing the adlists similar but failed or couldnt find a similar approach.
So I came up with below:

Local OOTB:

$ curl -s localhost/api/lists | jq '.lists[] | .address, .type, .enabled'
"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
"block"
true

Remote @10.0.0.5:

$ curl -s 10.0.0.5/api/lists | \
jq -r '.lists[] | "{\"address\":\"\(.address)\",\"type\":\"\(.type)\",\"enabled\":\(.enabled)}"' | \
xargs -n 1 -d '\n'

{"address":"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts","type":"block","enabled":true}
{"address":"https://dehakkelaar.nl/lists/cryptojacking_campaign.list.txt","type":"block","enabled":true}
{"address":"https://gitlab.com/ZeroDot1/CoinBlockerLists/raw/master/list.txt","type":"block","enabled":true}
{"address":"https://blocklist.cyberthreatcoalition.org/vetted/domain.txt","type":"block","enabled":true}

Pull them in:

$ curl -s 10.0.0.5/api/lists | \
jq -r '.lists[] | "{\"address\":\"\(.address)\",\"type\":\"\(.type)\",\"enabled\":\(.enabled)}"' | \
xargs -n 1 -d '\n' curl -s --request POST localhost/api/lists -d | jq

{
  "lists": [
    {
      "address": "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
      "comment": "Migrated from /etc/pihole/adlists.list",
      "groups": [
        0
      ],
      "enabled": true,
      "id": 1,
      "date_added": 1723426320,
      "date_modified": 1723426320,
      "type": "block",
      "date_updated": 1723427826,
      "number": 165668,
      "invalid_domains": 0,
      "abp_entries": 0,
      "status": 1
    }
  ],
  "processed": {
    "errors": [
      {
        "item": "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
        "error": "UNIQUE constraint failed: adlist.address, adlist.type"
      }
    ],
    "success": []
  },
  "took": 0.000641345977783203
}
{
  "lists": [
    {
      "address": "https://dehakkelaar.nl/lists/cryptojacking_campaign.list.txt",
      "comment": null,
      "groups": [
        0
      ],
      "enabled": true,
      "id": 11,
      "date_added": 1723519854,
      "date_modified": 1723519854,
      "type": "block",
      "date_updated": 0,
      "number": 0,
      "invalid_domains": 0,
      "abp_entries": 0,
      "status": 0
    }
  ],
  "processed": {
    "errors": [],
    "success": [
      {
        "item": "https://dehakkelaar.nl/lists/cryptojacking_campaign.list.txt"
      }
    ]
  },
  "took": 0.01030111312866211
}
{
  "lists": [
    {
      "address": "https://gitlab.com/ZeroDot1/CoinBlockerLists/raw/master/list.txt",
      "comment": null,
      "groups": [
        0
      ],
      "enabled": true,
      "id": 12,
      "date_added": 1723519854,
      "date_modified": 1723519854,
      "type": "block",
      "date_updated": 0,
      "number": 0,
      "invalid_domains": 0,
      "abp_entries": 0,
      "status": 0
    }
  ],
  "processed": {
    "errors": [],
    "success": [
      {
        "item": "https://gitlab.com/ZeroDot1/CoinBlockerLists/raw/master/list.txt"
      }
    ]
  },
  "took": 0.00451946258544922
}
{
  "lists": [
    {
      "address": "https://blocklist.cyberthreatcoalition.org/vetted/domain.txt",
      "comment": null,
      "groups": [
        0
      ],
      "enabled": true,
      "id": 13,
      "date_added": 1723519854,
      "date_modified": 1723519854,
      "type": "block",
      "date_updated": 0,
      "number": 0,
      "invalid_domains": 0,
      "abp_entries": 0,
      "status": 0
    }
  ],
  "processed": {
    "errors": [],
    "success": [
      {
        "item": "https://blocklist.cyberthreatcoalition.org/vetted/domain.txt"
      }
    ]
  },
  "took": 0.004606008529663086
}

Check:

$ curl -s localhost/api/lists | jq '.lists[] | .address, .type, .enabled'
"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
"block"
true
"https://dehakkelaar.nl/lists/cryptojacking_campaign.list.txt"
"block"
true
"https://gitlab.com/ZeroDot1/CoinBlockerLists/raw/master/list.txt"
"block"
true
"https://blocklist.cyberthreatcoalition.org/vetted/domain.txt"
"block"
true

The error: "UNIQUE constraint failed: adlist.address, adlist.type" is most likely bc that adlist URL was already present in the gravity DB.
And syncing .comments or .groups involves a bit more coding :wink:

Just testing. Looks like domains replicates fine, but what option in the configuration list includes local domain names? Tried DHCP (don't use) and misc. Here's what I have (servers not included here):

FULL_SYNC=false

#CRON=0 0,12 * * *

SYNC_CONFIG_DNS=false
SYNC_CONFIG_DHCP=false
SYNC_CONFIG_NTP=false
SYNC_CONFIG_RESOLVER=false
SYNC_CONFIG_DATABASE=false
SYNC_CONFIG_MISC=false
SYNC_CONFIG_DEBUG=false
SYNC_GRAVITY_DHCP_LEASES=false
SYNC_GRAVITY_GROUP=true
SYNC_GRAVITY_AD_LIST=true
SYNC_GRAVITY_AD_LIST_BY_GROUP=true
SYNC_GRAVITY_DOMAIN_LIST=true
SYNC_GRAVITY_DOMAIN_LIST_BY_GROUP=true
SYNC_GRAVITY_CLIENT=true
SYNC_GRAVITY_CLIENT_BY_GROUP=true

Thanks for doing this!

EDIT: I'm using the native executable, not docker.

Cheers for testing!

I'm not entirely sure what you're after but if it's local dns and cname records under Local DNS Settings in the side menu, then SYNC_CONFIG_DNS will sync this.