https://github.com/omercnet/foyer
Foyer β a privacy-first captive-portal browser. A throwaway, incognito browser that resolves DNS through the captive network's DHCP resolver.
https://github.com/omercnet/foyer
Last synced: 10 days ago
JSON representation
Foyer β a privacy-first captive-portal browser. A throwaway, incognito browser that resolves DNS through the captive network's DHCP resolver.
- Host: GitHub
- URL: https://github.com/omercnet/foyer
- Owner: omercnet
- License: other
- Created: 2026-06-19T08:02:59.000Z (11 days ago)
- Default Branch: main
- Last Pushed: 2026-06-19T09:38:20.000Z (11 days ago)
- Last Synced: 2026-06-19T10:09:01.582Z (11 days ago)
- Language: Go
- Size: 52.7 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Security: SECURITY.md
Awesome Lists containing this project
README
# πͺ Foyer
> A privacy-first captive-portal browser.
[](https://github.com/omercnet/foyer/actions/workflows/ci.yml)
[](https://github.com/omercnet/foyer/releases)
[](https://scorecard.dev/viewer/?uri=github.com/omercnet/foyer)
[](https://pkg.go.dev/github.com/omercnet/foyer)
[](LICENSE)
A **foyer** is the small entryway you pass through before stepping inside β you
deal with the lock at the door and leave the mud (and the tracking cookies)
outside. Foyer launches a **throwaway, incognito browser** just to get you
through a WiFi captive portal, so your real browser β full of your sessions and
cookies β never touches the hotel/airport/cafΓ© login page.
---
## Why
Captive portals hijack DNS: the network's DHCP server hands out its own resolver
and redirects everything to a login page until you authenticate. But if you run
encrypted/pinned DNS (DoH, a custom resolver, a VPN), that hijack never reaches
you and **the network just looks broken**. macOS pops its own *Captive Network
Assistant* β a stripped-down mini-browser β but you might prefer not to trust it,
and it can't help on every platform.
Foyer solves this **without changing any global setting**:
1. It discovers the captive network's DHCP-advertised DNS server.
2. It runs a tiny local **SOCKS5 proxy** that resolves DNS **through that server**.
3. It opens a **disposable, incognito** Chromium window routed through the proxy.
Nothing about your system DNS or normal browser changes. Close the window and
the throwaway profile is deleted.
## Features
- **Zero-config.** Auto-detects your default interface, the DHCP DNS server, and
an installed Chromium-family browser (Chrome, Chromium, Brave, Edge).
- **Privacy-first.** Loopback-only proxy, incognito, throwaway profile deleted on
exit, and your real browser is never involved.
- **Minimal supply chain.** Pure Go, one dependency, a self-contained SOCKS5
proxy (RFC 1928) so the network path stays auditable.
- **Graceful.** Clean SIGINT/SIGTERM shutdown; an ephemeral proxy port avoids
"address already in use".
- **macOS integration.** Toggle the built-in Captive Network Assistant so Foyer
is the only thing that handles portals.
## Install
### Go
```sh
go install github.com/omercnet/foyer@latest
```
The binary is named `foyer`.
### Pre-built binaries
Download from [Releases](https://github.com/omercnet/foyer/releases).
Each release includes a CycloneDX SBOM and a cosign-signed `checksums.txt`.
Verify the checksums signature (keyless / Sigstore):
```sh
cosign verify-blob \
--certificate checksums.txt.pem \
--signature checksums.txt.sig \
--certificate-identity-regexp 'https://github.com/omercnet/foyer/.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
checksums.txt
```
## Usage
```sh
foyer # detect everything and open the captive-portal browser
foyer status # diagnose: show detected interface, DNS, browser, CNA state
foyer disable-cna # macOS: stop the system Captive Network Assistant pop-up
foyer enable-cna # macOS: restore it
foyer -verbose # debug logging
foyer version
```
Typical flow: join the WiFi, run `foyer`, log in on the portal page that opens,
then close the window β done.
### macOS: make Foyer the only portal handler
macOS auto-launches its own Captive Network Assistant, which races with Foyer.
Disable it once:
```sh
foyer disable-cna # runs: sudo defaults write \
# /Library/Preferences/SystemConfiguration/com.apple.captive.control Active -bool false
```
Now joining a portal network won't pop the system helper; run `foyer` instead.
Reverse it any time with `foyer enable-cna`.
## Configuration
Foyer needs **no config file**. To override defaults, copy
[`foyer.example.toml`](foyer.example.toml) to
`~/.config/foyer/config.toml` (or `$XDG_CONFIG_HOME/foyer/config.toml`) and edit.
| Key | Default | Meaning |
| ------------- | ----------------------------------------- | -------------------------------------------------------------- |
| `socks5-addr` | `127.0.0.1:0` (ephemeral) | Loopback listen address for the proxy. |
| `interface` | default-route interface | Interface to query / bind to. |
| `dhcp-dns` | platform default | Shell command; first IPv4 in its output is the captive DNS. |
| `browser` | auto-detected | Shell command to launch the browser (`$PROXY` is exported). |
| `start-url` | `http://example.com` | First page; any plain-HTTP URL triggers the portal redirect. |
| `bind-device` | `false` | Bind sockets to `interface` (Linux, needs `CAP_NET_RAW`). |
## How it works
```
ββββββββββββββββββββββββ foyer ββββββββββββββββββββββββ
β β
DHCP ββββΆ β discover DNS SOCKS5 proxy (loopback, no auth) β
lease β (ipconfig/ ββ CONNECT example.com ββββ β
β resolvectl) β resolve via captive DNSβ β
β β β dial the result βΌ β
β ββββββββββΆβ captive network β
β ββ pipe bytes βββββββββββββ β
β β² β
β incognito browser βββββ (--proxy-server=socks5, β
β throwaway profile --host-resolver-rules) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
The two load-bearing tricks:
- **Proxy-side DNS.** The upstream resolver uses Go's pure-Go resolver with a
`Dial` override so every query goes to the captive DNS server β see
[`internal/proxy/resolver.go`](internal/proxy/resolver.go).
- **No local resolution in the browser.** Chrome is launched with
`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE localhost"` plus
`--proxy-server="socks5://β¦"`, so it hands hostnames to the proxy instead of
resolving them itself.
IPv4 only β captive portals are an IPv4-era concern.
## Development
```sh
go test -race -cover ./... # unit + integration tests
go vet ./...
golangci-lint run # strict lint (see .golangci.yml)
goreleaser build --snapshot --clean
```
CI runs lint, a `-race` test matrix (Ubuntu + macOS), `govulncheck`, CodeQL, and
OpenSSF Scorecard on a hardened runner. Releases are automated with
**release-please** (Conventional Commits β version PR) and **GoReleaser**
(cross-platform binaries, SBOM, cosign signatures).
## Credits
Foyer is a from-scratch Go reimplementation **inspired by
[captive-browser](https://github.com/FiloSottile/captive-browser) by
[Filippo Valsorda](https://filippo.io)**. The original tool pioneered the
"resolve through DHCP DNS via a local SOCKS5 proxy" approach; all credit for the
idea goes to him. Foyer reimplements it independently with a self-contained
proxy, zero-config detection, graceful shutdown, and macOS CNA integration.
## License
[MIT](LICENSE).