Full Code of Trellmor/bind-adblock for AI

master c621ce83e89d cached
12 files
19.8 KB
5.2k tokens
13 symbols
1 requests
Download .txt
Repository: Trellmor/bind-adblock
Branch: master
Commit: c621ce83e89d
Files: 12
Total size: 19.8 KB

Directory structure:
gitextract_2f3ac_vy/

├── .gitignore
├── DOCKER.md
├── Dockerfile
├── LICENSE
├── Pipfile
├── README.md
├── blocklist.txt
├── config.yml
├── docker-compose.yml
├── example_root_crontab
├── requirements.txt
└── update-zonefile.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
venv*
.cache/*
*.zone


================================================
FILE: DOCKER.md
================================================
# Docker

Dockerfile and docker-compose.yml provide alternative for installations and development

Some tweaking is expected to suit a production deployment

## Build

```shell
docker-compose build
```

## AdBlock Zone

The example container writes the generated rpz-adblocker.zone file under /bind-adblock

Docker-compose file has an options for using either a volume (i.e containerized bind9 integration) or host mount point (i.e. bind9 running on the docker host os) for that path.

## Deploy

```shell
docker-compose up -d
```

## Running Considerations

### Cron

The current image runs the python script at startup and then exits.
There is no cron scheduler provided at this time.

### On Demand

Once deployed, issuing a run will start the container again, run the update script, then exit after writing the zone file.

```shell
docker run bind-adblock_updater
```

### Bind9 Zone reloads

You might wish to combine bind-adblock and bind9 into the same container in which a cron service controls executing this python script and in turn can trigger named to reload the zone. 

An aggressive workaround is start the adblock updater container then fully restart the bind9 container.

```shell
docker-compose up updater >/dev/null 2>&1 && docker-compose restart bind9 >/dev/null 2>&1
```



================================================
FILE: Dockerfile
================================================
FROM python:latest

WORKDIR /root

COPY blocklist.txt .
COPY config.yml .
COPY update-zonefile.py .
COPY requirements.txt .

ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

RUN pip install --upgrade pip && \
    pip install -r requirements.txt

RUN mkdir /bind-adblock

CMD ["python3", "./update-zonefile.py", "--no-bind", "/bind-adblock/rpz-adblocker.zone", "rpz.adblocker"]


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 Daniel Triendl <daniel@pew.cc>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



================================================
FILE: Pipfile
================================================
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
requests = "*"
pathlib = "*"
dnspython = "*"
pyyaml = "*"
validators = "*"

[requires]
python_version = "3.9"


================================================
FILE: README.md
================================================
# BIND ad blocker

Fetch various blocklists and generate a BIND zone from them.

Configure BIND to return `NXDOMAIN` for ad and tracking domains to stop clients from contacting them.

Requires BIND 9.8 or newer for [RPZ](https://en.wikipedia.org/wiki/Response_policy_zone) support.

Uses the following sources:

* [Peter Lowe’s Ad and tracking server list](https://pgl.yoyo.org/adservers/)
* [MVPS HOSTS](http://winhelp2002.mvps.org/)
* [Adaway default blocklist](https://adaway.org/hosts.txt)
* [Dan Pollock’s hosts file](http://someonewhocares.org/hosts/zero/)
* [MalwareDomainList.com Hosts List](http://www.malwaredomainlist.com/hostslist/hosts.txt)
* [StevenBlack Unified hosts file](https://github.com/StevenBlack/hosts)
* [CAMELEON](http://sysctl.org/cameleon/)
* [Disconnect.me Basic tracking list](https://disconnect.me/trackerprotection)
* [Disconnect.me Ad Filter list](https://disconnect.me/trackerprotection)
* [Polish CERT Phishing list](https://www.cert.pl/ostrzezenia_phishing/)

## Setup

### Python packages

See [requirements.txt](requirements.txt)

To install
```
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
```


### Configure BIND

Add the `response-policy` statement to the BIND options

```
// For AdBlock
response-policy {
	zone "rpz.example.com";
};
```

Add your rpz zone. Replace example.com with a domain of your choice.

```
// AdBlock
zone "rpz.example.com" {
	type master;
	file "/etc/bind/db.rpz.example.com";
	masterfile-format text;
	allow-query { none; };
};
```

Create a zone file for your zone. Replace example.com with the domain you used before.
```
@ 3600 IN SOA @ admin.example.com. 0 86400 7200 2592000 86400
@ 3600 IN NS ns.example.com.
```

## Usage

    usage: update-zonefile.py [-h] [--no-bind] [--raw] [--empty] zonefile origin

    Update zone file from public DNS ad blocking lists

    positional arguments:
      zonefile    path to zone file
      origin      zone origin

    optional arguments:
      -h, --help  show this help message and exit
      --no-bind   Don't try to check/reload bind zone
      --raw       Save the zone file in raw format. Requires named-compilezone
      --empty     Create header-only (empty) rpz zone file
      --views     If using multiple BIND views, list where each zone is defined

Example: `update-zonefile.py /etc/bind/db.rpz.example.com rpz.example.com`

`update-zonefile.py` will update the zone file with the fetched adserver lists and issue a `rndc reload origin` afterwards.

### Multiple BIND Views

If you defined the  adblock rpz across multiple BIND views, then you will need to pass --views a space separated list of which views the zone is defined.

Doing so will issue 'rndc reload origin IN view' for each view provided for the origin zone.

```shell
--views "internal dmz test"
```

This argument can be omitted if the origin zone only occurs once in your configuration.
The following error is an indication you are using the rpz zone multiple views.

```text
zone 'rpz.adblocker' was found in multiple views
```

## Whitelist

You can either use an additional zone to whitelist domains (Or add them to `config.yml`) 
See [Whitelist](https://github.com/Trellmor/bind-adblock/wiki/whitelist) for adding a whitelist zone.


================================================
FILE: blocklist.txt
================================================
# Stop Firefox from enabeling DoH. Since we are running an ad-blocking DNS 
# Server we want to use it.
use-application-dns.net


================================================
FILE: config.yml
================================================
# Blocklist download request timeout
req_timeout_s: 10

# Also block *.domain.tld
wildcard_block: False

# Cache directory
cache: '.cache/bind_adblock'

# Can be either NULL or NXDOMAIN
blocking_mode: 'NXDOMAIN'

# Blocklists
# List of blocklists
# Format:
#   url: Link to blocklist
#   file: Blocklist file
#   format: domain or hosts, default: domain
lists:
  - file: 'blocklist.txt'
  - url: 'https://pgl.yoyo.org/as/serverlist.php?hostformat=nohtml&showintro=0'
  - url: 'http://mirror1.malwaredomains.com/files/justdomains'
  - url: 'http://winhelp2002.mvps.org/hosts.txt'
    format: hosts
  - url: 'https://adaway.org/hosts.txt'
    format: hosts
  - url: 'https://www.someonewhocares.org/hosts/zero/hosts'
    format: hosts

  # adlists from pi-hole: https://github.com/pi-hole/pi-hole/blob/master/adlists.default
  #
  # The below list amalgamates several lists we used previously.
  # See `https://github.com/StevenBlack/hosts` for details
  # StevenBlack's list
  - url: 'https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts'
    format: hosts

  # Cameleon
  - url: 'http://sysctl.org/cameleon/hosts'
    format: hosts

  # Disconnect.me Tracking
  - url: 'https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt'

  # Disconnect.me Ads
  - url: 'https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt'

  # Polish CERT - https://www.cert.pl/ostrzezenia_phishing/
  - url: 'https://hole.cert.pl/domains/domains.txt'

# Don't block domains listed here
domain_whitelist: []


================================================
FILE: docker-compose.yml
================================================
---
version: "3"

services:
  updater:
    container_name: adblock
    build:
      context: .
    volumes:
      - 'bind9-zones:/bind-adblock'
      # - '/var/bind:/bind-adblock'

volumes:
  bind9-zones:


================================================
FILE: example_root_crontab
================================================
# This is an example crontab file, which assumes the user (e.g. root) 
# has no crontab file yet, at all.  You may want to do:
# 'crontab -l' to validate that assumption...
# if valid, feel free to edit values below, and then run 
#     'crontab example_crontab_file'
# if invalid, you'll need to merge at least the last line into your
# current crontab file
#
# use /bin/bash to run commands, no matter what /etc/passwd says
SHELL=/bin/bash
# mail any output to user `nick', no matter whose crontab this is
# nick is an example username...replace with your prefered recipient
MAILTO=nick
CRON_TZ=EST5EDT
PATH=/usr/share/Modules/bin:/usr/lib64/ccache:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
# run five minutes after 2am, every day
# assumes your rpz domain file is /etc/bin/db.rpz.example.com and 
# your rpz domain is named rpz.example.com in your BIND config
5 2 * * * (cd /path/to/bind-adblock/bind-adblock-master; python3 ./update-zonefile.py /etc/bind/db.rpz.example.com rpz.example.com) 2>&1



================================================
FILE: requirements.txt
================================================
certifi==2022.12.7
charset-normalizer==3.0.1
decorator==5.1.1
dnspython==2.3.0
idna==3.4
pathlib==1.0.1
PyYAML==6.0
requests==2.28.2
urllib3==1.26.14
validators==0.20.0


================================================
FILE: update-zonefile.py
================================================
#!/usr/bin/env python3

'''
Copyright (c) 2018 Daniel Triendl <daniel@pew.cc>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''

import requests
from pathlib import Path
from datetime import datetime
import email.utils as eut
import os
import hashlib
import re
import sys
import dns.zone
import dns.name
import dns.version
from dns.exception import DNSException
import subprocess
import textwrap
import shutil
from argparse import ArgumentParser
import yaml
import validators

config = {
    # Blocklist download request timeout
    'req_timeout_s': 10,
    # Also block *.domain.tld
    'wildcard_block': False,
    # Cache directory
    'cache': Path(os.path.dirname(os.path.realpath(__file__)), )
}

parent_dir = os.path.dirname(os.path.realpath(__file__))
main_conf_file = os.path.join(parent_dir, 'config.yml')
config = yaml.safe_load(open(main_conf_file))
config['cache'] = Path(config['cache'])
if not config['cache'].is_absolute():
    config['cache'] = Path(parent_dir, config['cache'])

regex_domain = '^(127|0)\\.0\\.0\\.(0|1)[\\s\\t]+(?P<domain>([a-z0-9\\-_]+\\.)+[a-z][a-z0-9_-]*)$'
regex_no_comment = '^#.*|^$'
regex_no_comment_in_line = '^([^#]+)'

def download_list(url):
    headers = None

    cache = Path(config['cache'], hashlib.sha1(url.encode()).hexdigest())

    if cache.is_file():
        last_modified = datetime.utcfromtimestamp(cache.stat().st_mtime)
        headers = {
                'If-modified-since': eut.format_datetime(last_modified),
                'User-Agent': 'Bind adblock zonfile updater v1.0 (https://github.com/Trellmor/bind-adblock)'
                }

    try:
        r = requests.get(url, headers=headers, timeout=config['req_timeout_s'])

        if r.status_code == 200:
            with cache.open('w', encoding='utf8') as f:
                f.write(r.text)
            if 'last-modified' in r.headers:
                last_modified = eut.parsedate_to_datetime(r.headers['last-modified']).timestamp()
                os.utime(str(cache), times=(last_modified, last_modified))

            return r.text
    except requests.exceptions.RequestException as e:
        print(e)

    if cache.is_file():
        with cache.open('r', encoding='utf8') as f:
            return f.read()

def check_domain(domain, origin):
    if domain == '':
        return False

    if config['wildcard_block']:
        domain = '*.' + domain

    try:
        name = dns.name.from_text(domain, origin)
    except DNSException as e:
        return False

    if not validators.domain(domain):
        print('Ignoring invalid domain {}'.format(domain))
        return False

    return True

def read_list(filename):
    path = Path(filename)
    if path.exists:
        with path.open('r', encoding='utf8') as f:
            return f.read()


def parse_lists(origin):
    domains = set()
    origin_name = dns.name.from_text(origin)
    for l in config['lists']:
        data = None
        if 'url' in l:
            print(l['url'])
            data = download_list(l['url'])
        elif 'file' in l:
            print(l['file'])
            data = read_list(l['file'])

        if data:
            lines = data.splitlines()
            print("\t{} lines".format(len(lines)))

            c = len(domains)

            for line in data.splitlines():
                domain = ''

                if re.match(regex_no_comment, line):
                    continue

                m = re.search(regex_no_comment_in_line, line)
                if m:
                    line = m.group(1).strip()

                if line == '':
                    continue

                if l.get('format', 'domain') == 'hosts':
                    m = re.match(regex_domain, line)
                    if m:
                        domain = m.group('domain')
                else:
                    domain = line

                domain = domain.strip()
                if check_domain(domain, origin_name):
                    domains.add(domain)

            print("\t{} domains".format(len(domains) - c))

    print("\nTotal\n\t{} domains".format(len(domains)))
    return domains

def load_zone(zonefile, origin, raw):
    zone_text = ''
    path = Path(zonefile)
    tmpPath = Path(config['cache'], 'tempzone')

    if not path.exists():
        with tmpPath.open('w') as f:
            f.write('@ 3600 IN SOA @ admin.{}. 0 86400 7200 2592000 86400\n@ 3600 IN NS LOCALHOST.'.format(origin))

        save_zone(tmpPath, zonefile, origin, raw)

        print(textwrap.dedent('''\
                Zone file "{0}" created.

                Add BIND options entry:
                response-policy {{
                    zone "{1}";
                }};

                Add BIND zone entry:
                zone "{1}" {{
                    type master;
                    file "{0}";
                    masterfile-format {2};
                    allow-query {{ none; }};
                }};
        ''').format(path.resolve(), origin, 'raw' if raw else 'text'))

    if raw:
        try:
            compile_zone(zonefile, tmpPath, origin, 'raw', 'text')
            path = tmpPath
        except:
            pass


    with path.open('r') as f:
        for line in f:
            zone_text += line
            if "IN NS" in line:
                break

    return dns.zone.from_text(zone_text, origin)

def update_serial(zone):
    soaset = zone.get_rdataset('@', dns.rdatatype.SOA)
    soa = soaset[0]
    if dns.version.MAJOR < 2:
        soa.serial += 1
    else:
        soaset.add(soa.replace(serial=soa.serial + 1))

def check_zone(origin, zonefile):
    cmd = ['named-checkzone', '-q', origin, str(zonefile)]
    r = subprocess.call(cmd)
    return r == 0

def rndc_reload(cmd):
    try:
        r = subprocess.check_output(cmd, stderr=subprocess.PIPE)

    except subprocess.CalledProcessError as e:
        print( '{}'.format( e.stderr.decode(sys.getfilesystemencoding()) ) )
        if "multiple" in e.stderr.decode('utf-8'):
            sys.exit('Please pass --views the list of configured BIND views containing origin zone.')
        if e.returncode != 0:
            sys.exit('rndc failed with return code {}'.format(e.returncode))

    print( '{}'.format( r.decode(sys.getfilesystemencoding()) ) )

def reload_zone(origin, views):
    if views:
        for v in views.split():
            print ("view {}, {} ".format(v, origin), end='', flush=True)
            rndc_reload( ['rndc', 'reload', origin, "IN", v] )
    else:
        print ("{} ".format(origin), end='', flush=True)
        rndc_reload( ['rndc', 'reload', origin] )

def is_exe(fpath):
    return os.path.isfile(fpath) and os.access(fpath, os.X_OK)

def compile_zone(source, target, origin, fromFormat, toFormat):
    cmd = ['named-compilezone', '-f', fromFormat, '-F', toFormat, '-o', str(target), origin, str(source)]
    r = subprocess.call(cmd)
    if r != 0:
        raise Exception('named-compilezone failed with return code {}'.format(r))

def save_zone(tmpzonefile, zonefile, origin, raw):
    if raw:
        compile_zone(tmpzonefile, zonefile, origin, 'text', 'raw')
    else:
        shutil.move(str(tmpzonefile), str(zonefile))

def append_domain_to_zonefile(file, domain):
    if config['blocking_mode'] == 'NXDOMAIN' or "_" in domain:
        file.write(domain + ' IN CNAME .\n')
    else:
        file.write(domain + ' IN A 0.0.0.0\n')
        file.write(domain + ' IN AAAA ::\n')

if __name__ == '__main__':
    parser = ArgumentParser(description='Update zone file from public DNS ad blocking lists')
    parser.add_argument('--no-bind', dest='no_bind', action='store_true', help='Don\'t try to check/reload bind zone')
    parser.add_argument('--raw', dest='raw_zone', action='store_true', help='Save the zone file in raw format. Requires named-compilezone')
    parser.add_argument('--empty', dest='empty', action='store_true', help='Create header-only (empty) rpz zone file')
    parser.add_argument('--views', dest='views', type=str,
                        help='If using multiple BIND views, list where each zone is defined')
    parser.add_argument('zonefile', help='path to zone file')
    parser.add_argument('origin', help='zone origin')
    args = parser.parse_args()

    os.chdir(os.path.dirname(os.path.realpath(__file__)))

    if not config['cache'].is_dir():
        config['cache'].mkdir(parents=True)

    zone = load_zone(args.zonefile, args.origin, args.raw_zone)
    update_serial(zone)

    if args.empty:
        domains = set()
    else:
        domains = parse_lists(args.origin)

    tmpzonefile = Path(config['cache'], 'tempzone')
    zone.to_file(str(tmpzonefile))

    with tmpzonefile.open('a') as f:
        for d in (sorted(domains)):        
            if d in config['domain_whitelist']:
                continue
            append_domain_to_zonefile(f, d)
            if config['wildcard_block']:
                append_domain_to_zonefile(f, '*.' + d)

    if args.no_bind:
        save_zone(tmpzonefile, args.zonefile, args.origin, args.raw_zone)
    else:
        if check_zone(args.origin, tmpzonefile):
            save_zone(tmpzonefile, args.zonefile, args.origin, args.raw_zone)
            if is_exe('/usr/sbin/getenforce'):
                cmd = ['/usr/sbin/getenforce']
                r = subprocess.check_output(cmd).strip()
                print('SELinux getenforce output / Current State is: ',r)
                if r == b'Enforcing':
                    print('SELinux restorecon being run to reset MAC security context on zone file')
                    if is_exe('/sbin/restorecon'):
                        cmd = ['/sbin/restorecon', '-F', args.zonefile]
                        r = subprocess.call(cmd)
                        if r != 0:
                            raise Exception('Cannot run selinux restorecon on the zonefile - return code {}'.format(r))
            reload_zone(args.origin, args.views)
        else:
            print('Zone file invalid, not loading')
Download .txt
gitextract_2f3ac_vy/

├── .gitignore
├── DOCKER.md
├── Dockerfile
├── LICENSE
├── Pipfile
├── README.md
├── blocklist.txt
├── config.yml
├── docker-compose.yml
├── example_root_crontab
├── requirements.txt
└── update-zonefile.py
Download .txt
SYMBOL INDEX (13 symbols across 1 files)

FILE: update-zonefile.py
  function download_list (line 64) | def download_list(url):
  function check_domain (line 94) | def check_domain(domain, origin):
  function read_list (line 112) | def read_list(filename):
  function parse_lists (line 119) | def parse_lists(origin):
  function load_zone (line 166) | def load_zone(zonefile, origin, raw):
  function update_serial (line 210) | def update_serial(zone):
  function check_zone (line 218) | def check_zone(origin, zonefile):
  function rndc_reload (line 223) | def rndc_reload(cmd):
  function reload_zone (line 236) | def reload_zone(origin, views):
  function is_exe (line 245) | def is_exe(fpath):
  function compile_zone (line 248) | def compile_zone(source, target, origin, fromFormat, toFormat):
  function save_zone (line 254) | def save_zone(tmpzonefile, zonefile, origin, raw):
  function append_domain_to_zonefile (line 260) | def append_domain_to_zonefile(file, domain):
Condensed preview — 12 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (22K chars).
[
  {
    "path": ".gitignore",
    "chars": 22,
    "preview": "venv*\n.cache/*\n*.zone\n"
  },
  {
    "path": "DOCKER.md",
    "chars": 1293,
    "preview": "# Docker\n\nDockerfile and docker-compose.yml provide alternative for installations and development\n\nSome tweaking is expe"
  },
  {
    "path": "Dockerfile",
    "chars": 422,
    "preview": "FROM python:latest\n\nWORKDIR /root\n\nCOPY blocklist.txt .\nCOPY config.yml .\nCOPY update-zonefile.py .\nCOPY requirements.tx"
  },
  {
    "path": "LICENSE",
    "chars": 1088,
    "preview": "MIT License\n\nCopyright (c) 2018 Daniel Triendl <daniel@pew.cc>\n\nPermission is hereby granted, free of charge, to any per"
  },
  {
    "path": "Pipfile",
    "chars": 213,
    "preview": "[[source]]\nname = \"pypi\"\nurl = \"https://pypi.org/simple\"\nverify_ssl = true\n\n[dev-packages]\n\n[packages]\nrequests = \"*\"\npa"
  },
  {
    "path": "README.md",
    "chars": 3289,
    "preview": "# BIND ad blocker\n\nFetch various blocklists and generate a BIND zone from them.\n\nConfigure BIND to return `NXDOMAIN` for"
  },
  {
    "path": "blocklist.txt",
    "chars": 128,
    "preview": "# Stop Firefox from enabeling DoH. Since we are running an ad-blocking DNS \n# Server we want to use it.\nuse-application-"
  },
  {
    "path": "config.yml",
    "chars": 1510,
    "preview": "# Blocklist download request timeout\nreq_timeout_s: 10\n\n# Also block *.domain.tld\nwildcard_block: False\n\n# Cache directo"
  },
  {
    "path": "docker-compose.yml",
    "chars": 205,
    "preview": "---\nversion: \"3\"\n\nservices:\n  updater:\n    container_name: adblock\n    build:\n      context: .\n    volumes:\n      - 'bin"
  },
  {
    "path": "example_root_crontab",
    "chars": 1009,
    "preview": "# This is an example crontab file, which assumes the user (e.g. root) \n# has no crontab file yet, at all.  You may want "
  },
  {
    "path": "requirements.txt",
    "chars": 169,
    "preview": "certifi==2022.12.7\ncharset-normalizer==3.0.1\ndecorator==5.1.1\ndnspython==2.3.0\nidna==3.4\npathlib==1.0.1\nPyYAML==6.0\nrequ"
  },
  {
    "path": "update-zonefile.py",
    "chars": 10924,
    "preview": "#!/usr/bin/env python3\n\n'''\nCopyright (c) 2018 Daniel Triendl <daniel@pew.cc>\n\nPermission is hereby granted, free of cha"
  }
]

About this extraction

This page contains the full source code of the Trellmor/bind-adblock GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 12 files (19.8 KB), approximately 5.2k tokens, and a symbol index with 13 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!