# iplist turns domain lists into router-ready IP and CIDR sets

> iplist is an MIT licensed asynchronous PHP service, built on amphp, that collects the IPv4, IPv6 and CIDR ranges behind a set of domains and publishes them in eighteen output formats, from plain text and JSON to MikroTik scripts, sing-box rule sets, ClashX, SwitchyOmega, Dnsmasq ipset and nfset, and Amnezia filter lists.

**rekryt/iplist** — IP Address Collection and Management Service with multiple output formats: mikrotik, json, text, ipset, nfset, clashx, keenetic, switchy, amnezia

- Repository: https://github.com/rekryt/iplist
- Website: https://iplist.opencck.org
- Stars: 1,909 · Forks: 61
- Language: HTML
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/rekryt-iplist

## A domain list in, a router address list out

iplist is described as an IP Address Collection and Management Service with multiple output formats, and the core of it is one substitution. You give it a list of domains, it works out the addresses and CIDR zones behind them, and it publishes those in the format your router or proxy client consumes.

The service is aimed at collecting and updating IP addresses, both IPv4 and IPv6, and their CIDR zones, for specified domains. The implementation is described as an asynchronous PHP web server based on AMPHP and the Linux utilities whois and ipcalc.

The list of output formats is the feature that makes it useful, and it is long.

| format | description |
|---|---|
| json | JSON format |
| text | newline separated |
| comma | comma separated |
| geoip | v2rayGeoIPDat |
| geosite | v2rayGeoSiteDat |
| singbox | sing-box rule-set (JSON) |
| srs | sing-box rule-set (binary) |
| mikrotik | MikroTik Script |
| switchy | SwitchyOmega RuleList |
| nfset | Dnsmasq nfset |
| ipset | Dnsmasq ipset |
| clashx | ClashX |
| kvas | Keenetic KVAS |
| bat | Keenetic Routes .bat |
| wildcard | Keenetic DNS |
| amnezia | Amnezia filter list |
| pac | Proxy Auto-Configuration file |

So the same collection is consumable by a MikroTik router running RouterOS, by a Keenetic router through its KVAS, KeaDNS or batch route mechanisms, by an OpenWRT box with ipset or dnsmasq nfset, by a sing-box or v2ray client as a rule set, by ClashX, by the SwitchyOmega browser extension, by AmneziaVPN, and by anything that reads a PAC file.

That breadth is the point. Without it, a user who wants a domain-based rule in sing-box and a domain-based rule in their router has to run two collection jobs, and the two lists drift. The value of a single collection with many serialisations is that the address data is fetched once and the formats are views on it.

The project publishes three portals, the main one at iplist.opencck.org, a portal with other services at beta.iplist.opencck.org, and one with Russian services at russia.iplist.opencck.org, and the README is in Russian with an English version at README.en.md.

## One JSON file per portal, with DNS and seed data

The configuration model is a JSON file per group and portal, stored in config/<group>/<portal>.json, and each file is a configuration for a specific portal, setting the domains to monitor and the initial IP and CIDR data sources.

```json
{
    "domains": [
        "youtube.com",
        "www.youtube.com",
        "m.youtube.com",
        "googlevideo.com",
        "ytimg.com"
    ],
    "dns": ["127.0.0.11:53", "77.88.8.88:53", "8.8.8.8:53"],
    "timeout": 43200,
    "ip4": [],
    "ip6": [],
    "cidr4": [],
    "cidr6": [],
    "external": {
        "domains": ["https://raw.githubusercontent.com/nickspaargaren/no-google/master/categories/youtubeparsed"],
        "ip4": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/ipv4_list.txt"],
        "cidr4": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/cidr4.txt"]
    }
}
```

The property table is more informative than the sample, because it separates what is observed from what is seeded. The domains are the portal's domain list. The dns property is the list of DNS servers used to update the addresses, and the README notes that besides the local and Google DNS you can use public Russian DNS resolvers such as Yandex. The timeout is the time between updates of a domain's addresses, in seconds, and 43200 is twelve hours.

The ip4, ip6, cidr4 and cidr6 properties are the initial lists, the starting point before the service has resolved anything itself.

The external object is the interesting one, because it is how existing lists get imported. The domains key is a list of URLs for topping up the portal's domains, and the ip4, ip6, cidr4 and cidr6 keys are URLs for topping up the address and CIDR data. So the service can start from a community maintained list and then refine it with its own resolution, and the example configuration points at exactly that kind of pair, a no-google domain list and a maintained iplist-youtube address list.

There is one more property that the README describes separately, the optional replace object, which replaces intersecting CIDR zones with narrower ones, and it is given its own section in the document because it is the only setting that changes the shape of the output rather than just its source.

## The container image resolves, formats and runs three Go binaries

The Dockerfile is where the real dependencies become visible, and it is more interesting than a typical PHP application image.

The runtime stage is php:8.2-cli, and the apt line installs ntpsec whois dnsutils ipcalc git. The whois utility is where the IP-to-domain attribution comes from, ipcalc does the CIDR arithmetic, and dnsutils provides the DNS query tooling. The pcntl extension is configured and installed for the event loop, and the ev extension comes from PECL, so the asynchronous runtime has both a process control extension and an event extension available.

The build stage is separate, and the comment explains why in detail. The stage is golang:1.25-bookworm, and the reason is that go.mod in v2fly/geoip requires go >= 1.25 while apt-get install golang in Debian bookworm, the base of the php:8.2-cli image, gives Go 1.19, which cannot even download the required toolchain, since that appeared in Go 1.21. Because of that the build inside the final image was failing on go build. The Go toolchain also no longer lands in the runtime image.

The geoip binary is built from a pinned tag.

```dockerfile
ARG GEOIP_REF=202607171233
RUN git clone --depth 1 --branch ${GEOIP_REF} https://github.com/v2fly/geoip.git /src/geoip \
  && cd /src/geoip \
  && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/geoip . \
  && test -x /out/geoip
```

The comment says the tag pinning is needed because without it git clone pulls HEAD and the build is not reproducible, and the -trimpath and -ldflags settings strip the paths and symbols. So the geoip and geosite formats, the v2rayGeoIPDat and v2rayGeoSiteDat outputs, are produced by a third party Go binary built at image build time.

The sing-box binary is handled differently, and the comment says the official release tarball is used rather than building from source, specifically the -glibc build, because the ordinary one dynamically links. So the srs and singbox formats come from the sing-box release itself, and the .env.example points at it with SINGBOX_PATH set to /usr/local/bin/sing-box.

So the image carries three Go binaries worth noting, geoip built from source at a pinned tag, sing-box from its official tarball, and the PHP application itself, plus the whois, ipcalc and dnsutils command line tools.

## The environment file is where the tuning lives

The .env.example lists the runtime knobs, and the names make the design legible.

```
STORAGE_SAVE_INTERVAL="120"
SYS_DNS_RESOLVE_IP4="true"
SYS_DNS_RESOLVE_IP6="true"
SYS_DNS_RESOLVE_CHUNK_SIZE="10"
SYS_DNS_RESOLVE_DELAY="100"
SYS_IP6_SUBNET_PREFIX_CAP="64"
SYS_REPLACE_ESCALATE_IPS="true"
SYS_REPLACE_AGGREGATE_SUBNETS="false"
SYS_REPLACE_COLLAPSE_THRESHOLD_IP4_24="0"
SYS_REPLACE_COLLAPSE_THRESHOLD_IP4_16="0"
SYS_REPLACE_COLLAPSE_THRESHOLD_IP6_64="0"
SYS_REPLACE_COLLAPSE_THRESHOLD_IP6_32="0"
SYS_MEMORY_LIMIT="1024M"
SYS_TIMEZONE="Europe/Moscow"
SYS_TMP_PATH=""
SYS_TMP_TTL="900"
SYS_TMP_SWEEP_INTERVAL="600"
SYS_ENCODE_WORKER_THRESHOLD="20000"
SYS_GEOIP_NATIVE="true"
```

The resolution settings are the collection knobs. DNS resolve for IPv4 and IPv6 can be turned on independently, the chunk size of 10 is how many addresses are resolved per batch, and the delay of 100 is the pause between them, which is the rate limiting that keeps the collector from hammering a resolver.

The IPv6 prefix cap of 64 is a real constraint that matters. A single IPv6 host is commonly given a /64 and uses addresses from it, so a naive collector that records the address it resolved would publish a single host address that changes every time the host does a privacy extension, and the whole list churns. Capping at a /64 means the list stays stable, at the cost of being much wider than one host.

The replace settings are the CIDR refinement. Escalate IPs is on by default, aggregate subnets is off, and there are four collapse thresholds, for IPv4 at /24 and /16 and IPv6 at /64 and /32, all defaulting to 0 which presumably disables them. The threshold naming suggests the semantics, collapse a set of prefixes into a single larger one only when it contains at least that many addresses, so a threshold of 0 would mean never collapse.

The operational settings are the rest. A storage save interval of 120 seconds, a memory limit of 1024M, a timezone, a temporary path with a TTL of 900 seconds swept every 600, an encode worker threshold of 20000, and a GEOIP_NATIVE flag.

The compose file is minimal, one service built from the context, an env_file of .env, four bind mounts for src, config, storage and public, port 8080 published, and json-file logging capped at 50m.

## Where iplist is the wrong choice

The first wrong case is a client that fetches rules at runtime. sing-box can download a remote rule set and evaluate it, and ClashX and the browser extensions can fetch a list. If your consumer is capable of pulling the list itself, running a collector that has to be queried and then pushed is the wrong architecture, and the deployment is more work than the problem deserves.

The second wrong case is a dynamic domain whose addresses change constantly. The service resolves a domain and publishes the addresses, so a domain on a CDN with a large and shifting address pool produces a list that is either large and stale or small and incomplete. The replace and escalate settings exist to manage that, and the IPv6 /64 cap is the same problem in the IPv6 case, but a domain with thousands of anycast addresses is a case where a CIDR list is the wrong representation.

The third wrong case is a network where you do not control the resolver path. The collection depends on the DNS servers you configure returning the addresses you expect, and the whois step attributes an address back to a domain. On a network with a split horizon DNS or a filtering resolver, the collected addresses are the ones your resolver sees, which may not be the ones the internet routes to.

The fourth wrong case is a team that wants the collection logic under version control and review. The data is in the service, the seed lists are URLs fetched at runtime, and the JavaScript test script in package.json is still the placeholder that exits with an error, so there is no automated check that the service produces what you expect. The PHP side has a phpunit.xml, but the repository also carries a doc/ directory, a frontend/ directory and an nginx/ directory, and the README is a user-facing document rather than an operator's manual.

## How it differs from a static rule list

The comparison worth making is with the static rule list, the file of domains or IP ranges that you download once and put in your client, and the difference in approach is who does the resolution and when.

A static list is a snapshot. Somebody, usually a project, maintains a list of the addresses for a set of services, publishes it, and your client fetches it. The advantage is simplicity, there is no service to run and nothing to schedule. The disadvantage is that the list is only as fresh as its last publication, and for a service whose addresses rotate that means either frequent updates or a list that increasingly points at addresses nobody uses.

iplist moves the resolution to the consumer's side of the boundary. The service resolves the domains itself, on the schedule the portal configuration sets, and the output is regenerated from that. The advantage is freshness, and the ability to produce the data in a format the router actually accepts rather than only in the format the upstream project chose. The disadvantage is the whole operational surface, a container to run, a scheduler to trust, a database to keep, and a change in the trust model, because the list now comes from infrastructure you operate rather than from a project you depend on.

The second difference is the breadth of the output. A static list project publishes one format. This service publishes eighteen, and the formats are not cosmetic variations, they are different applications of the same data, an ipset for a Linux firewall, a MikroTik script for a RouterOS address list, a sing-box rule set for a proxy client, a PAC file for a browser.

The third difference is the seed and refine model. The external property lets a service start from an existing community maintained list and then extend it with its own resolution, so you are not required to run the collection from scratch on day one. That is a pragmatic middle ground, and it also means the output depends on a third party URL staying available, which is a dependency the service does not control.

The fourth difference is the CIDR refinement, which no static list usually does. Narrowing overlapping zones so that a wide provider range does not swallow an unrelated service is a real operation, and the replace configuration exists to do it.

## Conclusion

iplist fits a home lab or a small office that wants a domain based routing policy enforced at the router, so a rule names a domain and the address list underneath it is maintained for you, and it fits a user of sing-box, MikroTik, Keenetic, OpenWRT, ClashX or SwitchyOmega who wants the address list regenerated rather than hand-maintained. It does not fit an environment where a rule set is fetched at runtime, since these are static lists that have to be refreshed and pushed, and it does not fit a team that wants the collection logic under its own control. Before deploying it, read the replace section in the README about narrowing overlapping CIDR zones, note that the PHP test script currently exits with an error placeholder, and plan for the fact that the service resolves the domains itself over the configured DNS servers.

## FAQ

### What is the iplist service and how does it work?

It is an asynchronous PHP web server based on amphp that collects and updates the IPv4, IPv6 and CIDR ranges behind a set of configured domains, using the Linux whois and ipcalc utilities, and publishes the result in a range of formats for routers, firewalls and proxy clients.

### Which output formats does iplist produce?

Eighteen, including json, text, comma, geoip for v2rayGeoIPDat, geosite for v2rayGeoSiteDat, singbox and srs for sing-box rule sets, mikrotik, switchy for SwitchyOmega, nfset and ipset for Dnsmasq, clashx, kvas, bat and wildcard for Keenetic, amnezia, and pac for proxy auto-configuration.

### How is iplist configured?

With JSON files stored in config/<group>/<portal>.json, each holding the domains to monitor, the DNS servers used to resolve them, a timeout in seconds between updates, initial ip4, ip6, cidr4 and cidr6 lists, an external object of URLs for importing seed data, and an optional replace object for narrowing overlapping CIDR zones.

### How do I run iplist with Docker?

The compose file defines one service built from the repository, reading .env, with the src, config, storage and public directories bind mounted and port 8080 published. The image is based on php:8.2-cli with whois, ipcalc and dnsutils installed, and it builds the v2fly geoip binary at a pinned tag in a separate Go stage before copying it in.

### What is the purpose of the SYS_IP6_SUBNET_PREFIX_CAP setting?

It caps the IPv6 prefix published for a resolved host, defaulting to 64. Since a single IPv6 host is commonly given a /64 and uses addresses from it, publishing the individual address would produce a list that churns on every privacy extension, so the cap keeps the entry stable at the cost of being wider than one host.

## Sources

- [Issues](https://github.com/rekryt/iplist/issues)
- [License: MIT](https://github.com/rekryt/iplist/blob/master/LICENSE)
- [Project website](https://iplist.opencck.org)
- [README](https://github.com/rekryt/iplist/blob/master/README.md)
- [rekryt/iplist on GitHub](https://github.com/rekryt/iplist)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/rekryt-iplist
