ENTRADA2 is a scalable, high-performance platform for processing, enriching, and analyzing DNS traffic data. It ingests raw DNS packet captures (PCAP), transforms them into efficient Apache Parquet files, and stores them in the Apache Iceberg open table format for advanced analytics. ENTRADA2 enriches DNS records with geolocation, ASN, public resolver detection, and other metadata, supporting real-time and batch workflows across local, Kubernetes, and AWS environments. With built-in support for S3-compatible storage, message queuing, and integration with query engines such as Spark, Trino and Athena, ENTRADA2 enables powerful, flexible analysis of DNS data for security, research, and operational insights.
ENTRADA2 is an improvement on ENTRADA and includes support for multiple deployment types: Kubernetes, AWS and local deployment using Docker Compose.
A DNS query and response are combined into a single record and enriched by adding the following information:
- Geolocation (Country)
- Autonomous system (ASN)
- Domain name public suffix
- Detection of public resolvers (Google, OpenDNS and Cloudflare)
- TCP round-trip time (RTT)
ENTRADA2 supports the following features:
- S3 storage (Rustfs, AWS)
- Open table format (Apache Iceberg)
- Message queuing (RabbitMQ, AWS SQS)
- Metadata catalog (AWS Glue or Iceberg JDBC/REST catalog)
- Metrics (InfluxDB)
- Query engine (Trino, AWS Athena, Spark)
ENTRADA2 includes the following improvements and changes compared to ENTRADA:
- Removed Hadoop support
- Improved support for S3 object storage
- Automatically create required resources (S3 bucket and messaging queues)
- Added support for Kubernetes
- Added support for Apache Iceberg table format
- Split the QNAME into dns_domainname and dns_qname (labels before domain name) columns
- Rows are sorted by domainname for more efficient compression
- Default column compression changed to ZSTD (was Snappy)
- Bloomfilter for domainname column for performance boost when filtering on domainname
- Use small Parquet max dictionary size to prevent domainname column using dictionary instead of Bloomfilter
- Renamed table columns to indicate protocol source
- Removed unused columns
- Name server site pcaps can be processed by any container (no longer dedicated per server)
- No longer saving state between pcaps (might cause some unmatched packets)
- Added S3 event-based workflow (S3 new object scanning also supported)
- Added API to control containers (e.g., status/start/stop)
- Added option to include answer/authoritative/additional sections of DNS response in Parquet output
- Added support for decoding Extended RCODE
- Added support for Extended DNS Errors
The following deployment modes are supported:
- Local/Single system using Docker Compose (best for test and evaluation)
- Kubernetes
- AWS
export TOOL_VERSION=0.0.10
mvn clean && mvn package && docker build --tag=sidnlabs/entrada2:$TOOL_VERSION .ENTRADA2 uses the Maxmind GeoIP2 database. If you have a license, set the MAXMIND_LICENSE_PAID environment variable. Otherwise, sign up for the free GeoLite2 database and use the MAXMIND_LICENSE_FREE environment variable.
To get started quickly, use the provided Docker Compose script to create a test environment on a single host, containing all required services.
The configuration settings for the Docker Compose script can be found as environment variables in docker.env. The example uses the default Docker Compose script and starts 1 ENTRADA master container and 2 ENTRADA worker containers. The script has support for profiles - the test profile will also start ENTRADA containers, any other profile will only start the dependencies.
export MAXMIND_LICENSE_PAID=<your-key-here>
docker compose --profile test up --scale entrada-worker=2The Docker Compose script uses the "test" profile to start the ENTRADA containers. Not using this profile will only start the dependencies and not the ENTRADA containers.
ENTRADA2 converts a single PCAP file into one or more Parquet files. Many small input PCAP files may be automatically combined into a larger Parquet file. The iceberg.parquet.min-records option determines how many records a Parquet output file must contain before it is allowed to be closed and added to the Iceberg table.
The PCAP processing workflow uses 3 prefixes for PCAP objects in S3, defaults are:
pcap-in-prefixes: New PCAP objects must be uploaded using a prefix from this list (e.g., "pcap-in/provider1") to trigger processing of the PCAP filepcap-done: When the auto-delete option is not enabled, processed PCAP objects are moved to this prefixpcap-failed: When a PCAP object cannot be processed correctly, it is moved to this prefix
When all components have started up, you may upload a PCAP file to the S3 bucket using a prefix from the "pcap-in-prefixes" list. If ENTRADA2 created an S3 bucket notification configuration (enabled by default), then processing of the PCAP file will automatically start.
If the S3 storage solution used does not support events for newly uploaded objects, then enable scanning for new objects. Enable S3 object scanning by setting a value for the entrada.schedule.new-object-min property. The scanning feature will scan the S3 bucket every x minutes for new objects and will process the objects similar to how they would be processed when an event was created for the object.
Use the following S3 tags when uploading files to S3:
entrada-ns-server: Logical name of the name server (e.g., ns1.dns.nl)entrada-ns-anycast-site: Anycast site of the name server (e.g., ams)entrada-object-ts: (optional) ISO 8601 timestamp when PCAP was created (e.g., 2022-10-12T01:01:00.000Z)
Timestamps used in PCAP files are assumed to be using timezone UTC. The timestamp in the entrada-object-ts tag is used for sorting new S3 objects when multiple new objects are detected. This allows for bulk uploading older data and maintaining the correct order when processing. The tag entrada-object-ts is only used when rustfs/AWS S3 events are not configured and ENTRADA2 must periodically scan for newly uploaded objects itself.
The processing of new PCAP files must be started and stopped using the API endpoints. After starting new PCAP files will be detected but NOT processed yet. Processing will only start when the "start" command is sent to the API. This allows for uploading new PCAP files before starting the processing, which is especially useful when using the S3 object scanning feature.
aws s3api put-object \
--bucket entrada-bucket \
--key pcap-in/provider1/trace_tokyo_2_2023-08-14_09_20_26.pcap.gz \
--body trace_tokyo_2_2023-08-14_09_20_26.pcap.gz \
--tagging "entrada-object-ts=2024-10-12T01%3A01%3A00.000Z&entrada-ns-anycast-site=Amsterdam&entrada-ns-server=ns1.dns.nl"To prevent many small Parquet output files when the input stream contains many small PCAP files, there is a configuration option (iceberg.parquet.min-records, default 1000000) for setting the lower limit of the number of records in a Parquet output file. Output files are not closed and added to the table until this limit is reached. To force closing the output file(s), e.g., when doing maintenance, use the "flush" API endpoint.
PUT https://hostname:8080/api/v1/state/flush will cause all current open files to be closed and added to the table.
The results may be analyzed using different tools, such as AWS Athena, Trino or Apache Spark. The Docker Compose script automatically starts Trino for quickly analyzing a limited dataset.
Example using the Trino command-line client (installed in Trino container):
docker exec -it docker-trino-1 trinoSwitch to the correct catalog and namespace:
use iceberg.entrada2;Count rows in the DNS table:
select count(1)
from dns;Show partitions of DNS table:
select *
from "dns$partitions"Get the domain names that received the most queries (top 25):
select dns_domainname, count(1)
from dns
group by 1
order by 2 desc
limit 25;Get the most common RCODEs per day:
select day(time), dns_rcode, count(1)
from dns
group by 1,2
order by 1 asc
limit 25;Get the top 10 domains on a specific date:
select dns_domainname, count(1)
from dns
where date_trunc('day', time) = timestamp '2024-07-01'
group by 1
order by 2 desc
limit 10;When response data is enabled (entrada.rdata.enabled), records in the rdata column can be analyzed using lambda expressions.
Get all queries for a specific rdata response value:
select dns_qname, dns_domainname, dns_rdata
from dns
where any_match(dns_rdata, r -> r.data = '185.159.199.200') and dns_domainname = 'dns.nl'
limit 100;Get top 25 by A-records in responses:
select d.data, count(1)
from dns, UNNEST(dns.dns_rdata) d
where d.type = 1
group by 1
order by 2 desc
limit 25;To cleanup the test environment, stop the Docker containers, delete the Docker volumes and restart the containers:
docker compose down
docker system prune -f
docker volume rm docker_entradaInfluxDbVolume docker_entradaRustfsVolume docker_entradaPostgresqlVolume;
docker compose --profile test up --scale entrada-worker=2The ENTRADA2 configuration options are in the Spring Boot configuration file. All options in this file can be overridden by using environment variables, or in the case of Kubernetes you can also create a ConfigMap containing a custom configuration file.
The following table lists all configuration options starting with entrada.:
| Option | Description | Default |
|---|---|---|
entrada.tlds |
Comma-separated list of most frequently used TLDs for fast-path optimization | nl |
entrada.nameserver.default-name |
Default name server name when S3 objects have no tags | default-ns |
entrada.nameserver.default-site |
Default anycast site when S3 objects have no tags | default-site |
entrada.rdata.enabled |
Enable rdata from DNS response records in Parquet output (dns_rdata column) | false |
entrada.rdata.dnssec |
Include DNSSEC RRs such as RRSIG in output | false |
entrada.cname.enabled |
Enable CNAME record processing | true |
entrada.filter.tlds |
TLDs to filter out - DNS data for these TLDs will not be stored | (empty) |
entrada.object.max-wait-time-secs |
Max wait time before marking object as not picked up and resending to queue | 3600 |
entrada.object.max-proc-time-secs |
Max processing time before marking object as failed | 7200 |
entrada.object.max-tries |
Max number of attempts for decoding an object | 2 |
entrada.process.max-proc-time-secs |
Max time for PCAP processing before worker is marked as stalled | 600 |
entrada.process.max-request-cache-size |
Max size of internal DNS request cache for matching requests to responses | 1000000 |
entrada.schedule.updater-min |
Interval in minutes to update reference data | 120 |
entrada.schedule.liveness-min |
Interval in minutes to check for stalled workers | 1 |
entrada.schedule.expired-object-min |
Interval in minutes to check for expired objects | 10 |
entrada.schedule.new-object-secs |
Interval in seconds to check for new objects (disabled when empty) | (empty) |
entrada.security.token |
Token for REST API and actuator endpoints access (use X-API-KEY HTTP header) | 94591089610224297274859827590711 |
entrada.metrics.bin-size-secs |
Time bin size in seconds for historical metrics aggregation | 10 |
entrada.privacy.enabled |
When enabled, IP addresses are not written to Parquet files | false |
entrada.s3.access-key |
S3 access key for bucket containing source PCAPs | (empty) |
entrada.s3.secret-key |
S3 secret key for bucket containing source PCAPs | (empty) |
entrada.s3.region |
S3 region | ${AWS_REGION} |
entrada.s3.endpoint |
S3 endpoint (only when not using AWS) | (empty) |
entrada.s3.bucket |
S3 bucket name for PCAP files and Iceberg data | sidnlabs-iceberg-data |
entrada.s3.pcap-in-prefixes |
List of S3 prefixes for new PCAP files | pcap-in/provider1, pcap-in/provider2 |
entrada.s3.pcap-done-dir |
Directory for processed PCAP files (deleted if empty) | (empty) |
entrada.s3.pcap-delete |
Delete PCAPs after processing | true |
entrada.s3.reference-dir |
Directory for reference data | reference |
entrada.s3.warehouse-dir |
Directory for Iceberg data files | database |
entrada.messaging.request.name |
Queue name for S3 bucket lifecycle events | entrada-s3-event |
entrada.messaging.request.ttl |
Queue TTL in minutes | 60 |
entrada.messaging.request.aws.retention |
AWS SQS message retention period in seconds | 86400 |
entrada.messaging.request.aws.visibility-timeout |
AWS SQS visibility timeout in seconds | 600 |
entrada.messaging.command.name |
Queue name for sending commands to all instances | entrada-command |
entrada.messaging.command.aws.retention |
AWS SQS retention period for command queue in seconds | 60 |
entrada.messaging.command.aws.visibility-timeout |
AWS SQS visibility timeout for command queue in seconds | 1 |
entrada.messaging.leader.name |
Queue name for leader container | entrada-leader |
entrada.messaging.leader.batchSize |
Leader queue batch size | 50 |
entrada.messaging.leader.aws.retention |
AWS SQS retention period for leader queue in seconds | 86400 |
entrada.messaging.leader.aws.visibility-timeout |
AWS SQS visibility timeout for leader queue in seconds | 300 |
entrada.leader |
Set to true for leader container (max 1) when using non-Kubernetes deployment | false |
entrada.provisioning.enabled |
Auto-create required components (bucket and queues) | true |
The following table lists all configuration options starting with iceberg.:
| Option | Description | Default |
|---|---|---|
iceberg.s3.access-key |
S3 access key for Iceberg operations (leave empty if using Lakekeeper vended credentials) | (empty) |
iceberg.s3.secret-key |
S3 secret key for Iceberg operations (leave empty if using Lakekeeper vended credentials) | (empty) |
iceberg.s3.region |
S3 region for Iceberg operations | ${AWS_REGION} |
iceberg.s3.endpoint |
S3 endpoint (only when not using AWS) | (empty) |
iceberg.catalog.type |
Catalog type (rest, jdbc, or glue for AWS) | rest |
iceberg.catalog.uri |
REST catalog URI | (empty) |
iceberg.catalog.warehouse |
REST catalog warehouse identifier | (empty) |
iceberg.catalog.oauth2.uri |
OAuth2 token endpoint URI for REST catalog authentication | (empty) |
iceberg.catalog.oauth2.credential |
OAuth2 credential for REST catalog authentication | (empty) |
iceberg.catalog.oauth2.scope |
OAuth2 scope for REST catalog authentication | (empty) |
iceberg.catalog.warehouse-location |
S3 location for Iceberg warehouse | s3://${entrada.s3.bucket}/${entrada.s3.warehouse-dir} |
iceberg.catalog.host |
JDBC catalog host (PostgreSQL only) | (empty) |
iceberg.catalog.port |
JDBC catalog port | 5432 |
iceberg.catalog.name |
JDBC catalog database name | (empty) |
iceberg.catalog.user |
JDBC catalog username | (empty) |
iceberg.catalog.password |
JDBC catalog password | (empty) |
iceberg.compression |
Parquet compression codec (see Iceberg docs) | zstd |
iceberg.metadata.delete-after-commit |
Delete old metadata files after commit | true |
iceberg.metadata.version.max |
Maximum number of previous metadata versions to keep | 1 |
iceberg.table.name |
Iceberg table name | dns |
iceberg.table.namespace |
Iceberg table namespace | entrada |
iceberg.table.location |
S3 location for Iceberg table data | s3://${entrada.s3.bucket}/${entrada.s3.warehouse-dir}/${iceberg.table.namespace}/${iceberg.table.name} |
iceberg.parquet.dictionary-max-mb |
Maximum dictionary size in MB for Parquet encoding | 2 |
iceberg.parquet.bloomfilter |
Enable Bloom filter for dns_domainname column | true |
iceberg.parquet.min-records |
Minimum number of records required before closing Parquet output file | 10000000 |
iceberg.parquet.bloomfilter-max-size-mb |
Maximum Bloom filter size in MB | 1 |
The following table lists all configuration options starting with maxmind.:
| Option | Description | Default |
|---|---|---|
maxmind.max-age-hr |
Max age in hours of local MaxMind database before downloading update | 24 |
maxmind.license.free |
MaxMind license key for free GeoLite2 databases | (empty) |
maxmind.license.paid |
MaxMind license key for paid GeoIP2 databases | (empty) |
maxmind.country.free |
Database name for free country database | GeoLite2-Country |
maxmind.country.paid |
Database name for paid country database | GeoIP2-Country |
maxmind.asn.free |
Database name for free ASN database | GeoLite2-ASN |
maxmind.asn.paid |
Database name for paid ASN/ISP database | GeoIP2-ISP |
The following table lists all configuration options starting with management.influx.metrics.export.:
| Option | Description | Default |
|---|---|---|
management.influx.metrics.export.bucket |
InfluxDB bucket name for storing metrics | entrada |
management.influx.metrics.export.org |
InfluxDB organization name | SIDN |
management.influx.metrics.export.token |
InfluxDB authentication token | (empty) |
management.influx.metrics.export.uri |
InfluxDB server URI | (empty) |
management.influx.metrics.export.step |
Metrics export interval | 1m |
management.influx.metrics.export.enabled |
Enable metrics export to InfluxDB | false |
JVM options (e.g., for memory limits) can be passed to the container using the JAVA_OPTS environment variable in the Docker Compose configuration:
environment:
- JAVA_OPTS=-Xmx4g -Xms4gWhen deployed on AWS, ENTRADA2 will automatically create a new database and table using AWS Glue. The data in the table can be analyzed using Athena, Spark or any of the other compatible tools available on AWS. See the AWS documentation for more details.
To enable running on AWS, set the spring.cloud.aws.sqs.enabled property to true to enable the use of SQS queues for messaging, otherwise RabbitMQ is used.
When running ENTRADA2 on Kubernetes (on-premise or in cloud), all dependencies such as RabbitMQ must be deployed and available before using ENTRADA2. ENTRADA2 will automatically create a new database and table using the Iceberg JDBC catalog and it will also create the required messaging queues in RabbitMQ.
ENTRADA2 requires permission for creating and reading ConfigMaps for the leader election functionality. For example permissions, see: k8s/leader-election-auth.yml
The S3 implementation must send events to a RabbitMQ or AWS SQS queue when a new PCAP object has been uploaded. When not using AWS or the default Docker Compose configuration, the rustfs event configuration must be created manually.
The AMQP event destination for rustfs uses these options:
| Field name | Example |
|---|---|
| URL | amqp://admin:${DEFAULT_PASSWORD}@rabbitmq-hostname:5672 |
| Exchange | entrada-s3-event-exchange |
| Exchange Type | direct |
| Routing Key | entrada-s3-event |
| Durable | Yes |
Use the following values:
- ARN: The ARN for the above created event destination
- Prefix: The value of entrada option
entrada.s3.pcap-in-prefixes - Suffix: Optional suffix for PCAP files (e.g., pcap.gz)
If the S3 storage solution used does not support events for newly uploaded objects, then enable scanning for new objects. Enable S3 object scanning by setting a value for the entrada.schedule.new-object-min property. The scanning feature will scan the S3 bucket every x minutes for new objects and will process the objects similar to how they would be processed when an event was created for the object.
A basic API is available for controlling the lifecycle of ENTRADA2 containers.
| Endpoint | Description |
|---|---|
| PUT /api/v1/state/start | Start processing new PCAP files, the command will be relayed to all running containers |
| PUT /api/v1/state/stop | Stop processing new PCAP files, the command will be relayed to all running containers |
| PUT /api/v1/state/flush | Close open Parquet output files and add these to Iceberg table, the command will be relayed to all running containers |
| GET /api/v1/state | Get the current state of the container |
Running multiple worker containers, all listening to the same S3 bucket events is possible and the correct method for scaling up. Make sure that only 1 container is the "leader", responsible for committing new data files to the Iceberg table.
When running on Kubernetes, leader election is performed automatically. When the leader container is shutdown, the leader election process will automatically select another container to become the leader.
When using Docker, you must set the ENTRADA_LEADER option to true only for the master container. There is no failover mechanism for master containers when using Docker.
The column names use a prefix to indicate where the information was extracted from:
dns_: from the DNS messageip_: from the IP packetprot_: from transport UDP/TCPedns_: from EDNS records in DNS messagetcp_: from the TCP packet
| column name | type | Description |
|---|---|---|
| dns_id | int | ID field from DNS header |
| time | long | Time of packet |
| dns_qname | string | qname from DNS question (labels left of domain) |
| dns_domainname | string | domainname from DNS question |
| dns_tld | string | Top-Level Domain from the query name (Public Suffix) |
| dns_qname_full | string | Full query when no TLD is present |
| ip_ttl | int | TTL of IP packet |
| ip_version | int | IP version used |
| prot | int | Protocol used (UDP or TCP) |
| ip_src | string | IP source address |
| prot_src_port | int | Source port |
| ip_dst | string | IP destination address |
| prot_dst_port | int | Destination port |
| dns_aa | boolean | AA flag from DNS header |
| dns_tc | boolean | TC flag from DNS header |
| dns_rd | boolean | RD flag from DNS header |
| dns_ra | boolean | RA flag from DNS header |
| dns_ad | boolean | AD flag from DNS header |
| dns_cd | boolean | CD flag from DNS header |
| dns_ancount | int | Answer RR count DNS |
| dns_arcount | int | Additional RR count DNS |
| dns_nscount | int | Authority RR count DNS |
| dns_qdcount | int | Question RR count DNS |
| dns_opcode | int | OPCODE from DNS header |
| dns_rcode | int | RCODE from DNS header |
| dns_qtype | int | QTYPE from DNS header |
| dns_qclass | int | CLASS from DNS question |
| ip_geo_country | string | Country for source IP address |
| ip_asn | string | ASN for source IP address |
| ip_asn_org | string | ASN organisation name for source IP address |
| edns_udp | int | UDP size from EDNS |
| edns_version | int | EDNS version |
| edns_do | boolean | DNSSEC OK flag |
| edns_options | array(int) | EDNS options from request |
| edns_server_options | array(int) | EDNS options from response |
| edns_ecs | string | EDNS Client Subnet |
| edns_ecs_ip_asn | string | ASN for IP address in ECS |
| edns_ecs_ip_asn_org | string | ASN organisation for IP address in ECS |
| edns_ecs_ip_geo_country | string | Country for IP address in ECS |
| edns_ext_error | array(int) | EDNS extended error |
| dns_labels | int | Number of labels in DNS qname |
| dns_proc_time | int | Time between request and response (millis) |
| dns_pub_resolver | string | Name of public resolver source |
| dns_req_len | int | Size of DNS request message |
| dns_res_len | int | Size of DNS response message |
| tcp_rtt | int | RTT (millis) based on TCP-handshake |
| server | string | Name of NS server |
| server_location | string | Name of anycast site of NS server |
| dns_rdata | array(rec) | rdata from response records |
| dns_rdata.section | int | Section from response (0=answer, 1=authoritative, 3=additional) |
| dns_rdata.type | int | Resource Record (RR) type, according to IANA registration |
| dns_rdata.data | string | String representation of rdata part of RR |
| dns_cname | array(string) | CNAME records in the response chain |
Not all DNS resource records in ENTRADA have support for the dns_rdata.data column. For unsupported RRs, the value of this column will be null.
Metrics about the processed DNS data are generated when the configuration option management.influx.metrics.export.enabled is set to true. The metrics are sent to an InfluxDB instance, configured by the management.influx.metrics.export.* options.
Some of the components provide a web interface. Below are the URLs for the components started by the Docker Compose script. Login credentials can be found in the script (these are for test purposes only and do not constitute a security issue).
This project is distributed under the GPLv3. See LICENSE.
When building a product or service using ENTRADA2, we kindly request that you include the following attribution text in all advertising and documentation:
This product includes ENTRADA2 created by <a href="https://www.sidnlabs.nl">SIDN Labs</a>, available from
<a href="http://entrada.sidnlabs.nl">http://entrada.sidnlabs.nl</a>.