{"id":50995739,"url":"https://github.com/omercnet/foyer","last_synced_at":"2026-06-20T09:01:32.963Z","repository":{"id":365872118,"uuid":"1274138239","full_name":"omercnet/foyer","owner":"omercnet","description":"Foyer — a privacy-first captive-portal browser. A throwaway, incognito browser that resolves DNS through the captive network's DHCP resolver.","archived":false,"fork":false,"pushed_at":"2026-06-19T09:38:20.000Z","size":54,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-19T10:09:01.582Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/omercnet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-19T08:02:59.000Z","updated_at":"2026-06-19T09:32:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/omercnet/foyer","commit_stats":null,"previous_names":["omercnet/foyer"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/omercnet/foyer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omercnet%2Ffoyer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omercnet%2Ffoyer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omercnet%2Ffoyer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omercnet%2Ffoyer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/omercnet","download_url":"https://codeload.github.com/omercnet/foyer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omercnet%2Ffoyer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34563537,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-20T09:01:32.083Z","updated_at":"2026-06-20T09:01:32.958Z","avatar_url":"https://github.com/omercnet.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🪞 Foyer\n\n\u003e A privacy-first captive-portal browser.\n\n[![CI](https://github.com/omercnet/foyer/actions/workflows/ci.yml/badge.svg)](https://github.com/omercnet/foyer/actions/workflows/ci.yml)\n[![Release](https://img.shields.io/github/v/release/omercnet/foyer?sort=semver)](https://github.com/omercnet/foyer/releases)\n[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/omercnet/foyer/badge)](https://scorecard.dev/viewer/?uri=github.com/omercnet/foyer)\n[![Go Reference](https://pkg.go.dev/badge/github.com/omercnet/foyer.svg)](https://pkg.go.dev/github.com/omercnet/foyer)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\nA **foyer** is the small entryway you pass through before stepping inside — you\ndeal with the lock at the door and leave the mud (and the tracking cookies)\noutside. Foyer launches a **throwaway, incognito browser** just to get you\nthrough a WiFi captive portal, so your real browser — full of your sessions and\ncookies — never touches the hotel/airport/café login page.\n\n---\n\n## Why\n\nCaptive portals hijack DNS: the network's DHCP server hands out its own resolver\nand redirects everything to a login page until you authenticate. But if you run\nencrypted/pinned DNS (DoH, a custom resolver, a VPN), that hijack never reaches\nyou and **the network just looks broken**. macOS pops its own *Captive Network\nAssistant* — a stripped-down mini-browser — but you might prefer not to trust it,\nand it can't help on every platform.\n\nFoyer solves this **without changing any global setting**:\n\n1. It discovers the captive network's DHCP-advertised DNS server.\n2. It runs a tiny local **SOCKS5 proxy** that resolves DNS **through that server**.\n3. It opens a **disposable, incognito** Chromium window routed through the proxy.\n\nNothing about your system DNS or normal browser changes. Close the window and\nthe throwaway profile is deleted.\n\n## Features\n\n- **Zero-config.** Auto-detects your default interface, the DHCP DNS server, and\n  an installed Chromium-family browser (Chrome, Chromium, Brave, Edge).\n- **Privacy-first.** Loopback-only proxy, incognito, throwaway profile deleted on\n  exit, and your real browser is never involved.\n- **Minimal supply chain.** Pure Go, one dependency, a self-contained SOCKS5\n  proxy (RFC 1928) so the network path stays auditable.\n- **Graceful.** Clean SIGINT/SIGTERM shutdown; an ephemeral proxy port avoids\n  \"address already in use\".\n- **macOS integration.** Toggle the built-in Captive Network Assistant so Foyer\n  is the only thing that handles portals.\n\n## Install\n\n### Go\n\n```sh\ngo install github.com/omercnet/foyer@latest\n```\n\nThe binary is named `foyer`.\n\n### Pre-built binaries\n\nDownload from [Releases](https://github.com/omercnet/foyer/releases).\nEach release includes a CycloneDX SBOM and a cosign-signed `checksums.txt`.\n\nVerify the checksums signature (keyless / Sigstore):\n\n```sh\ncosign verify-blob \\\n  --certificate checksums.txt.pem \\\n  --signature checksums.txt.sig \\\n  --certificate-identity-regexp 'https://github.com/omercnet/foyer/.*' \\\n  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \\\n  checksums.txt\n```\n\n## Usage\n\n```sh\nfoyer                # detect everything and open the captive-portal browser\nfoyer status         # diagnose: show detected interface, DNS, browser, CNA state\nfoyer disable-cna    # macOS: stop the system Captive Network Assistant pop-up\nfoyer enable-cna     # macOS: restore it\nfoyer -verbose       # debug logging\nfoyer version\n```\n\nTypical flow: join the WiFi, run `foyer`, log in on the portal page that opens,\nthen close the window — done.\n\n### macOS: make Foyer the only portal handler\n\nmacOS auto-launches its own Captive Network Assistant, which races with Foyer.\nDisable it once:\n\n```sh\nfoyer disable-cna    # runs: sudo defaults write \\\n                     #   /Library/Preferences/SystemConfiguration/com.apple.captive.control Active -bool false\n```\n\nNow joining a portal network won't pop the system helper; run `foyer` instead.\nReverse it any time with `foyer enable-cna`.\n\n## Configuration\n\nFoyer needs **no config file**. To override defaults, copy\n[`foyer.example.toml`](foyer.example.toml) to\n`~/.config/foyer/config.toml` (or `$XDG_CONFIG_HOME/foyer/config.toml`) and edit.\n\n| Key           | Default                                   | Meaning                                                        |\n| ------------- | ----------------------------------------- | -------------------------------------------------------------- |\n| `socks5-addr` | `127.0.0.1:0` (ephemeral)                 | Loopback listen address for the proxy.                         |\n| `interface`   | default-route interface                   | Interface to query / bind to.                                  |\n| `dhcp-dns`    | platform default                          | Shell command; first IPv4 in its output is the captive DNS.    |\n| `browser`     | auto-detected                             | Shell command to launch the browser (`$PROXY` is exported).    |\n| `start-url`   | `http://example.com`                      | First page; any plain-HTTP URL triggers the portal redirect.   |\n| `bind-device` | `false`                                   | Bind sockets to `interface` (Linux, needs `CAP_NET_RAW`).      |\n\n## How it works\n\n```\n            ┌─────────────────────── foyer ───────────────────────┐\n            │                                                      │\n  DHCP ───▶ │  discover DNS    SOCKS5 proxy (loopback, no auth)    │\n  lease     │  (ipconfig/      ├─ CONNECT example.com ───┐         │\n            │   resolvectl)    │   resolve via captive DNS│         │\n            │        │         │   dial the result        ▼         │\n            │        └────────▶│                    captive network │\n            │                  └─ pipe bytes ◀───────────┘         │\n            │                         ▲                            │\n            │   incognito browser ────┘  (--proxy-server=socks5,   │\n            │   throwaway profile        --host-resolver-rules)    │\n            └──────────────────────────────────────────────────────┘\n```\n\nThe two load-bearing tricks:\n\n- **Proxy-side DNS.** The upstream resolver uses Go's pure-Go resolver with a\n  `Dial` override so every query goes to the captive DNS server — see\n  [`internal/proxy/resolver.go`](internal/proxy/resolver.go).\n- **No local resolution in the browser.** Chrome is launched with\n  `--host-resolver-rules=\"MAP * ~NOTFOUND , EXCLUDE localhost\"` plus\n  `--proxy-server=\"socks5://…\"`, so it hands hostnames to the proxy instead of\n  resolving them itself.\n\nIPv4 only — captive portals are an IPv4-era concern.\n\n## Development\n\n```sh\ngo test -race -cover ./...   # unit + integration tests\ngo vet ./...\ngolangci-lint run            # strict lint (see .golangci.yml)\ngoreleaser build --snapshot --clean\n```\n\nCI runs lint, a `-race` test matrix (Ubuntu + macOS), `govulncheck`, CodeQL, and\nOpenSSF Scorecard on a hardened runner. Releases are automated with\n**release-please** (Conventional Commits → version PR) and **GoReleaser**\n(cross-platform binaries, SBOM, cosign signatures).\n\n## Credits\n\nFoyer is a from-scratch Go reimplementation **inspired by\n[captive-browser](https://github.com/FiloSottile/captive-browser) by\n[Filippo Valsorda](https://filippo.io)**. The original tool pioneered the\n\"resolve through DHCP DNS via a local SOCKS5 proxy\" approach; all credit for the\nidea goes to him. Foyer reimplements it independently with a self-contained\nproxy, zero-config detection, graceful shutdown, and macOS CNA integration.\n\n## License\n\n[MIT](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fomercnet%2Ffoyer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fomercnet%2Ffoyer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fomercnet%2Ffoyer/lists"}