Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
b4adfa9
Merge remote-tracking branch 'moddns/main' into develop
MaciejTe Jul 15, 2026
64b402c
chore(tests): add pytest/ruff config, gate failover suite, pin ipv6-t…
MaciejTe Jul 20, 2026
673c9d7
test(e2e): add DNSLib.wait_until polling to close the Redis master/re…
MaciejTe Jul 20, 2026
c1ac19d
chore(tests): rename integration-test mentions to backend E2E tests
MaciejTe Jul 20, 2026
be2c0c5
test(e2e): add stack readiness gate, account teardown, state isolatio…
MaciejTe Jul 20, 2026
970ef3e
test(e2e): introduce ProfileSession facade, unify constants and provi…
MaciejTe Jul 20, 2026
9f0bef4
refactor(e2e): Move generate_complex_password to libs/
MaciejTe Jul 20, 2026
4e30daf
test(e2e): machine-parseable tableRef annotations, skip-compose flag,…
MaciejTe Jul 20, 2026
a5ede33
test(e2e): run Redis failover tests as a dedicated CI step
MaciejTe Jul 20, 2026
9c47e16
Merge pull request #199 from ivpn/chore/e2e-tests-improvements
MaciejTe Jul 20, 2026
61028bd
feat(app): Expand Query Logs card
MaciejTe Jul 1, 2026
763e691
chore(app): Improve badges positioning
MaciejTe Jul 1, 2026
ccfe4a5
feat(proxy): Read EDE codes to identify DNSSEC validation errors
MaciejTe Jul 1, 2026
2480cff
Merge pull request #176 from ivpn/feat/show-block-reason-in-logs
MaciejTe Jul 20, 2026
39276f0
feat(app): Query Logs visual deduplication
MaciejTe Jul 14, 2026
565cd1a
fix(app): Show single time for one-second consolidated log groups
MaciejTe Jul 15, 2026
9b1a4a1
fix(app): Cap consolidated query-log group time span at 10s
MaciejTe Jul 15, 2026
6645342
Merge pull request #194 from ivpn/feat/consolidate-sequantial-duplica…
MaciejTe Jul 20, 2026
c5f6567
test(e2e): Fix flaky tests
MaciejTe Jul 20, 2026
1646353
Merge pull request #200 from ivpn/fix/flaky-e2e-tests
MaciejTe Jul 20, 2026
a251833
perf(proxy): optimize subdomain checking by building candidates incre…
lamtrinhdev Apr 30, 2026
0edb6c1
Enhance after Copilot feedback.
lamtrinhdev May 29, 2026
36cbcf2
perf(proxy): Skip redundant full-FQDN re-check in subdomain loop
MaciejTe Jul 21, 2026
57880de
test(proxy): Add benchmarks for subdomain candidate building
MaciejTe Jul 21, 2026
0aa0b93
Merge pull request #113 from lamtrinhdev/main
MaciejTe Jul 21, 2026
9dcdea2
feat(app): Link login screen logo to landing page
MaciejTe Jul 21, 2026
0831ad7
fix(app): Un-clip search input focus ring on Blocklists page
MaciejTe Jul 21, 2026
73f1cd8
fix(app): Un-clip search and filter focus rings on Logs page
MaciejTe Jul 21, 2026
cc0020f
fix(app): Stop animating mobile header on scroll reflows
MaciejTe Jul 21, 2026
42f073c
fix(app): Stack delete-profile danger card on narrow screens
MaciejTe Jul 21, 2026
1e4ea11
fix(app): Make tooltips toggle on tap for touch devices
MaciejTe Jul 21, 2026
c770711
fix(app): Constrain Edit Profile modal width and stack danger card
MaciejTe Jul 21, 2026
9a11fc5
fix(app): Rework mobile header: sticky, edge-to-edge, theme-aware bou…
MaciejTe Jul 21, 2026
7b3bc0e
fix(app): Separate IP copy and tooltip tap zones on Setup page
MaciejTe Jul 21, 2026
b0b9107
Merge pull request #201 from ivpn/feat/login-logo-landing-link
MaciejTe Jul 21, 2026
9772242
Merge pull request #202 from ivpn/fix/blocklists-search-focus-ring
MaciejTe Jul 21, 2026
942249e
Merge pull request #205 from ivpn/fix/tooltip-tap-support
MaciejTe Jul 21, 2026
f29df98
Merge pull request #204 from ivpn/fix/edit-profile-delete-overlap
MaciejTe Jul 21, 2026
d81ebde
Merge pull request #203 from ivpn/fix/mobile-header-scroll-gap
MaciejTe Jul 21, 2026
5cf4793
feat(libs): shared dohpath constant, dnsstamps implementation; proxy …
MaciejTe May 15, 2026
fe0bf6f
feat(api): POST /api/v1/dnsstamp returns DoH/DoT/DoQ sdns:// strings …
MaciejTe May 15, 2026
11f4e4a
feat(app): Add DNS Stamp tab in Routers guide
MaciejTe May 15, 2026
a784896
test(e2e): DNS stamps tests
MaciejTe May 15, 2026
9afab1b
tests(app): Fix Playwright tests
MaciejTe May 15, 2026
a5e7ecc
test(e2e): Fix cert location
MaciejTe May 15, 2026
72f4cd2
docs(faq): Add DNS stamps info
MaciejTe May 19, 2026
d676936
test(e2e): Update CA cert location to the new one
MaciejTe Jul 16, 2026
bc2c23f
Merge pull request #147 from ivpn/feat/dns-stamp-calculator
MaciejTe Jul 22, 2026
ef594bc
fix(app): Keep query-log cards visible during auto-refresh
MaciejTe Jul 23, 2026
19af026
test(e2e): Poll last-step position in setup-guide-scroll to tolerate …
MaciejTe Jul 24, 2026
ee8029b
Merge pull request #207 from ivpn/fix/logs-refresh-disappear
MaciejTe Jul 24, 2026
dd91c57
feat(app): Refresh UI after deployments via service worker update flow
MaciejTe Jul 16, 2026
38d7818
fix(app): Address PR review comments and style Refresh toast action i…
MaciejTe Jul 16, 2026
afc9c50
fix(app): Detect deploys on Safari/iOS via version.json poll and relo…
MaciejTe Jul 28, 2026
4cafa40
Merge pull request #198 from ivpn/feat/app-auto-update
MaciejTe Jul 28, 2026
683de5e
chore(app): Trigger update checks on SPA navigation and trim images f…
MaciejTe Jul 29, 2026
2449940
fix(app): Make blocklists Enable-listed button toggle to disable-all …
MaciejTe Jul 29, 2026
9ec52c3
fix(app): Suppress version-poll toast while SW install is in flight t…
MaciejTe Jul 29, 2026
665e779
Merge pull request #211 from ivpn/fix/enable-all-blocklists
MaciejTe Jul 29, 2026
dc0bff7
Merge pull request #210 from ivpn/chore/app-refresh-improvement
MaciejTe Jul 29, 2026
a75cb66
feat(app): dnscrypt-proxy setup guides (Routers + Linux)
MaciejTe Jul 22, 2026
770d48a
test(e2e): verify dnscrypt-proxy over DoH resolves per-profile
MaciejTe Jul 22, 2026
afb25eb
feat(app): clarify DNS Stamps section titles
MaciejTe Jul 23, 2026
ec49cf2
Merge pull request #206 from ivpn/feat/dnscrypt-over-doh-support
MaciejTe Jul 29, 2026
0ce3582
feat(api): add per-profile DNS rebinding protection setting
MaciejTe Jun 12, 2026
211a403
feat(proxy): block DNS rebinding via IP-phase filter (tier 150)
MaciejTe Jun 12, 2026
43e7717
feat(app): add DNS rebinding protection toggle to Security tab
MaciejTe Jun 12, 2026
5202f3c
fix(proxy): read rebinding toggle with ParseBool (go-redis stores boo…
MaciejTe Jun 12, 2026
a4ade35
test(e2e): cover DNS rebinding protection end-to-end
MaciejTe Jun 12, 2026
b2c7305
fix(app): render Rebinding protection block-reason chip in query logs
MaciejTe Jul 24, 2026
82ba809
feat(api): round-trip rebinding protection toggle through profile exp…
MaciejTe Jul 24, 2026
0220253
test(e2e): Add DNS rebinding protection check in backend E2E test
MaciejTe Jul 24, 2026
59da0bd
test(api): enforce profile-model export parity with reflection guardr…
MaciejTe Jul 27, 2026
06c6894
feat(proxy): classify query-log resolution outcomes (nodata, timeout,…
MaciejTe Jul 27, 2026
61585bc
feat(app): pair query types with outcomes in consolidated log rows
MaciejTe Jul 28, 2026
de929a4
fix(app): distinct outcome chips, honest reason labels, group-scope r…
MaciejTe Jul 28, 2026
aea3508
chore(app): Display QL card properly on hover/expansion
MaciejTe Jul 28, 2026
d1a551e
chore(app): Always display occurrences in QL card
MaciejTe Jul 28, 2026
21598a8
Merge pull request #164 from ivpn/feat/dns-rebinding-protection
MaciejTe Jul 29, 2026
906e0d4
fix(blocklists): swap blp_fakenews source to maintained StevenBlack f…
MaciejTe Jul 27, 2026
b2e9ec0
feat(app): amber 'No answer' micro-label on collapsed query-log cards…
MaciejTe Jul 29, 2026
a58113d
chore(tests): add broken.test blackholed zone to knot configs for una…
MaciejTe Jul 29, 2026
1a2e404
Merge pull request #209 from ivpn/fix/swap-fakenews-blocklist
MaciejTe Jul 29, 2026
55f0408
Merge pull request #212 from ivpn/feat/query-logs-no-answer-text-label
MaciejTe Jul 29, 2026
a99b29e
feat(app): show subtle rdns-600 hover outline on query-log cards in d…
MaciejTe Jul 29, 2026
33c4880
feat(app): persistent rdns-600 outline on open query-log cards, subtl…
MaciejTe Jul 29, 2026
ff78d37
Merge pull request #213 from ivpn/feat/ql-card-outline
MaciejTe Jul 30, 2026
6fc9452
docs(readme): fix recursor stack, versions, setup steps, architecture…
MaciejTe Jul 30, 2026
ee85f59
Merge pull request #214 from ivpn/docs/update-readme
MaciejTe Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions .github/workflows/integration_tests.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Smoke / integration tests
name: Smoke / backend E2E tests

on:
push:
Expand Down Expand Up @@ -30,7 +30,9 @@ jobs:
python_certs_path=`python -m certifi`
echo "Python CA store path: ${python_certs_path}"
echo "PYTHON_CERTS_PATH=${python_certs_path}" >> $GITHUB_ENV
echo "IVPN_CERT_PATH=./certs/moddns_dev_development_CA.crt" >> $GITHUB_ENV
# Absolute: consumed both from the repo root (cert setup step below) and
# from tests/ (pytest CWD, via libs/dns_lib.py for DoT/DoQ verification).
echo "IVPN_CERT_PATH=${GITHUB_WORKSPACE}/certs/moddns_dev_development_CA.crt" >> $GITHUB_ENV

- name: SSL cert setup
run: |
Expand All @@ -41,6 +43,14 @@ jobs:
run: |
sudo echo "127.0.0.1 moddns.dev" | sudo tee -a /etc/hosts

# dnscrypt-proxy client for the DoH-stamp E2E test (dns_tests/test_dnscrypt_proxy.py).
# The pinned version + URL + sha256 live in tests/libs/dnscrypt_proxy.py (single
# source of truth); this invokes that module to download + checksum-verify the
# binary and export its path. Fails loudly here; the test itself skips if unset.
- name: Install dnscrypt-proxy client (pinned)
run: |
echo "MODDNS_DNSCRYPT_PROXY_BIN=$(cd tests && python -m libs.dnscrypt_proxy)" >> "$GITHUB_ENV"

- name: Prepare .env from sample
run: |
if [ ! -f tests/.env ]; then
Expand All @@ -64,5 +74,11 @@ jobs:
fi
done < tests/.env

- name: Run integration tests
- name: Run backend E2E tests
run: cd tests/; make test_ci

# Destructive (stops/starts the Redis replica container), so it is
# excluded from the default invocation via pytest addopts and runs
# here as its own session with a fresh compose stack.
- name: Run Redis failover backend E2E tests
run: cd tests/; make test_failover
5 changes: 2 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ down: ## Stops all of the services.
-f compose.dnscheck.yml \
-f compose.sdns.yml \
-f compose.knot.yml \
down; \
docker kill -a
down

up_dns: ## Starts the DNS services (both recursors: sdns + knot).
docker compose \
Expand Down Expand Up @@ -137,7 +136,7 @@ gen_ts_client: ## Generates the typescript client from swagger spec.
rm -rf app/src/api/client/ || true
docker run -v ${CWD}:/app -w /app/api/docs --user $$(id -u):$$(id -g) --rm openapitools/openapi-generator-cli generate --package-name idns -i swagger.yaml -g typescript-axios -o /app/app/src/api/client --skip-validate-spec

build_tests_image: ## Builds the smoke / integration tests image.
build_tests_image: ## Builds the smoke / backend E2E tests image.
docker build -f tests/Dockerfile -t dns_tests:latest .

dev_tests: ## Starts the development tests docker container.
Expand Down
2 changes: 1 addition & 1 deletion README-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ mkcert automatically installs its root CA into the system trust store, so browse

> [!NOTE]
> The certificates committed under `certs/` (`moddns.dev+4.pem` / `moddns.dev+4-key.pem`, signed by
> `moddns_dev_development_CA.crt`) are what the integration tests use. mkcert is only needed if you want a
> `moddns_dev_development_CA.crt`) are what the backend E2E tests use. mkcert is only needed if you want a
> CA your **browser** trusts automatically for local dev. See `certs/README.md` for the regeneration recipe.

## Local DNS overrides with dnsmasq
Expand Down
127 changes: 77 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,71 +20,77 @@ modDNS is a full-stack DNS security platform that combines encrypted DNS transpo
modDNS is built as a microservices architecture with the following components:

```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ │───▶│ │───▶│ │───▶│ │
│ Web Client │ │ Nginx Proxy │ │ Frontend │ │ API Server │
│ │ │ │ │ (React) │ │ │
└──────────────┘ └──────────────┘ └──────────────┘ └──────┬───────┘
┌──────────┴──────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ Redis │ │ MongoDB │
│ (Caching) │ │ (Storage) │
└──────────────┘ └──────────────┘
┌──────────────┐ ┌──────────────┐ │
│ │───▶│ │────────────────────┘
│ DNS Clients │ │ DNS Proxy │
│ │ │ │
└──────────────┘ └──────┬───────┘
┌──────────────┐
│ │
│ DNS Resolver │
│(SDNS/Unbound)│
└──────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Web Client │──▶│ Nginx │──▶│ Frontend │
│ (browser) │ │ (:80, static)│ │ (React SPA) │
└──────┬───────┘ └──────────────┘ └──────────────┘
│ REST API (:3000)
┌──────────────┐ ┌──────────────────────┐
│ API Server │──▶│ Redis │
│ (Go/Fiber) │ │ (master + 2 replicas │
└──────┬───────┘ │ + sentinel) │
│ └──────────▲───────────┘
▼ │ profile/rule reads
┌──────────────┐ │ (dns replica)
│ MongoDB │ ┌──────────┴───┐ ┌──────────────────┐
│ │◀──│ DNS Proxy │──▶│ Recursors │
└──────────────┘ │ (TLS term.) │ │ (sdns + Knot) │
query logs & └──────▲───────┘ └──────────────────┘
statistics │ DoH / DoT / DoQ
┌──────┴───────┐
│ DNS Clients │
└──────────────┘
```

Additional services not shown above:

- **blocklists** — periodically downloads and ingests filter lists (AdGuard, Hagezi, OISD, StevenBlack, …) into Redis/MongoDB.
- **dnscheck** — standalone DNS diagnostics microservice with GeoIP (MaxMind) lookup.

Note that the DNS proxy terminates TLS for the encrypted DNS transports itself; Nginx only serves the web frontend. The API is exposed directly on port 3000.

## Core Technologies

**Backend Services**
- Go & Fiber for high-performance APIs
- MongoDB for persistent storage of accounts, profiles, and telemetry
- Redis for caching and background jobs
- SDNS/Unbound for DNS resolution and policy enforcement
- MongoDB for persistent storage of accounts, profiles, query logs, and statistics
- Redis (master + two replicas + sentinel) for caching and distributing profile configuration to the proxy
- sdns and Knot Resolver as bundled recursors (Knot is the default)

**Frontend**
- React + TypeScript SPA (shadcn/ui & Radix UI component system)
- Tailwind CSS for utility-first styling
- PWA with offline support and in-app update flow

**Infrastructure**
- Docker & Docker Compose for local orchestration
- Nginx as the public ingress & TLS termination layer
- Nginx serving the web frontend (the DNS proxy terminates TLS for encrypted DNS transports itself)
- GitHub Actions for CI/CD automation

## Features

**Encrypted DNS**
- DNS over HTTPS (DoH)
- DNS over TLS (DoT)
- DNS over QUIC (DoQ)
- DNS over HTTPS (DoH), DNS over TLS (DoT), DNS over QUIC (DoQ)
- Per-profile DNS stamps (`sdns://`) calculator and dnscrypt-proxy (via DoH) setup support
- DNSSEC validation (per profile, enabled by default)

**Content Filtering**
- Built-in blocklists (ads, malware, trackers)
- Custom allow/deny rules per profile
- Custom allow/deny rules per profile — domains and IPs, with rule groups and precedence
- Service-based blocking presets backed by ASN lookup (Google, Meta, TikTok, Netflix, …)
- DNS rebinding protection

**User & Profile Management**
- Multi-profile accounts with individualized policies
- MFA, email verification, and secure password workflows
- MFA (TOTP and WebAuthn/passkeys), email verification, and secure password workflows
- Profile settings export & import
- In-app announcements

**Observability**
- Near real-time DNS query logging
- Exportable analytics for auditing
- Near real-time DNS query logging with outcome classification and quick-rule creation from log entries (opt-in, configurable retention from 1 hour to 1 month)
- Statistics and exportable analytics for auditing
- Prometheus metrics exposed by the proxy

**Apple Device Integration**
- Managed `.mobileconfig` profiles
Expand All @@ -95,12 +101,27 @@ modDNS is built as a microservices architecture with the following components:
### Prerequisites
- Docker & Docker Compose
- Make (for the provided automation scripts)
- Node.js 18+ and npm (for the React application)
- Go 1.25+ (for backend services)
- Node.js 22+ and npm (for the React application)
- Go 1.25.8+ (toolchain version pinned in the `go.mod` files)
- Python 3.11 (for the backend E2E tests)
- mkcert (optional, for trusted local TLS certificates)

### Quick Start

Before the first `make up`, create the required (gitignored) environment files from the tracked samples and provide the GeoLite2 databases:

```bash
# 1. Environment files
cp api/.env.sample api/.env
cp proxy/.env.sample proxy/.env
cp dnscheck/.env.sample dnscheck/.env

# 2. MaxMind GeoLite2 databases (mounted by the proxy and dnscheck)
# Place them under bootstrap/GeoLite2-ASN/ and bootstrap/GeoLite2-City/
```

Then:

```bash
make up # Build and start every service stack
make down # Stop and remove containers
Expand All @@ -120,6 +141,9 @@ Certificates for local HTTPS access live in `certs/`. You can either generate th
| `proxy/` | DNS proxy implementation
| `tests/` | Integration and regression suites (pytest + testcontainers)
| `bootstrap/`, `compose.*.yml` | Docker-compose orchestration and bootstrap assets
| `certs/` | Development TLS certificates and local CA
| `scripts/` | Helper scripts
| `.github/` | CI workflows (GitHub Actions), lint configs, issue/PR templates

## Development Workflow

Expand All @@ -132,36 +156,39 @@ npm run lint
npm run tsc
npm run build
```
`npm run dev` sources `app/env/.env.local`, which is gitignored — create it first (see the tracked `app/env/.env.production`, `.env.staging`, and `.env.test` for reference).

### API service (`api/`)
```bash
cd api
go mod tidy
make test
```
(See `api/Makefile` for additional targets like `make lint`, `make dev`, etc.)
(See `api/Makefile` for additional targets like `make lint`, `make gow` (live reload), `make swag`, and `make mockery`. From the repo root, `make dev_api` runs live reload inside the running container.)

### Proxy service (`proxy/`)
```bash
cd api
cd proxy
go mod tidy
make test
```
(See `proxy/Makefile` for additional targets like `make lint`, `make dev`, etc.)
(See `proxy/Makefile` for additional targets like `make lint`, `make gow`, and `make mockery`. From the repo root, `make dev_proxy` runs live reload inside the running container.)

### Integration tests (`tests/`)
### Backend E2E tests (`tests/`)
```bash
python -m venv tests/venv
source tests/venv/bin/activate
pip install -r tests/requirements.txt
make test_ci # spins up containers via testcontainers
cd tests
python3.11 -m venv venv
source venv/bin/activate
make install_test_dependencies
make test_ci # spins up the stack via testcontainers
make test_failover # destructive Redis failover tests (excluded from test_ci, run separately)
```

## Testing

- **Web client**: `npm run lint && npm run tsc && npm run test` (unit) and `npm run test:e2e` (Playwright)
- **Go services**: `go test ./...` inside each Go module (`api/`, `blocklists/`, `proxy/`, etc.)
- **Integration**: `source tests/venv/bin/activate && make test_ci`
- **Integration**: `cd tests && source venv/bin/activate && make test_ci`
- **Static analysis**: `make lint` in relevant directories (Go linters + ESLint)

## Contributing
Expand Down
54 changes: 54 additions & 0 deletions api/api/dnsstamp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package api

import (
"strings"

"github.com/gofiber/fiber/v2"

"github.com/ivpn/dns/api/api/requests"
"github.com/ivpn/dns/api/api/responses"
"github.com/ivpn/dns/api/internal/auth"
)

// @Summary Generate DNS Stamps for a modDNS profile
// @Description Returns DoH, DoT, and DoQ sdns:// strings for the given profile,
// @Description optionally scoped to a specific device label. Stamps are
// @Description consumed by clients that don't expose separate hostname/path
// @Description fields (UniFi Network, dnscrypt-proxy, AdGuard Home upstreams, etc.).
// @Tags DNS Stamps
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param body body requests.DNSStampReq true "Generate DNS stamp request"
// @Success 200 {object} responses.DNSStampResponse
// @Failure 400 {object} ErrResponse
// @Failure 404 {object} ErrResponse
// @Failure 500 {object} ErrResponse
// @Router /api/v1/dnsstamp [post]
func (s *APIServer) generateDNSStamps() fiber.Handler {
return func(c *fiber.Ctx) error {
p := new(requests.DNSStampReq)
if err := c.BodyParser(p); err != nil {
return HandleError(c, err, ErrInvalidRequestBody.Error())
}

errMsgs := s.Validator.ValidateRequest(c, p, ErrFailedToGenerateDNSStamp.Error())
if len(errMsgs) > 0 {
return HandleError(c, ErrInvalidRequestBody, strings.Join(errMsgs, " and "))
}

// Ownership check — identical pattern to mobileconfig.go.
accountId := auth.GetAccountID(c)
if _, err := s.Service.GetProfile(c.Context(), accountId, p.ProfileId); err != nil {
return HandleError(c, err, ErrFailedToGenerateDNSStamp.Error())
}

resp, err := s.Service.GenerateStamps(c.Context(), *p)
if err != nil {
return HandleError(c, err, ErrFailedToGenerateDNSStamp.Error())
}

c.Set("Content-Type", "application/json")
return c.Status(fiber.StatusOK).JSON(responses.DNSStampResponse(resp))
}
}
Loading
Loading