diff --git a/README.md b/README.md index 31d64cc9..2d402718 100644 --- a/README.md +++ b/README.md @@ -598,8 +598,8 @@ hack up --project my-project OAuth providers (notably Google) require `localhost` or a host that ends with a real public suffix. We keep `.hack` as the primary local dev domain, and optionally expose an alias domain for OAuth flows. -If the OAuth alias is enabled, `hack global install` configures `*.hack.gy` to resolve to the Caddy -container IP via dnsmasq + the OS resolver (bypasses port forwarding issues with Tailscale/VPNs). +If the OAuth alias is enabled, `hack global install` configures `*.hack.gy` to resolve to `127.0.0.1` +via dnsmasq + the OS resolver. Routing rules: diff --git a/docs/architecture.md b/docs/architecture.md index 76533665..3442cfd4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -52,7 +52,7 @@ graph LR - Global scope (`~/.hack`) - Caddy proxy on 80/443 (routes via Docker labels) - CoreDNS for container DNS (`*.hack` → Caddy) - - macOS DNS helper: dnsmasq + `/etc/resolver` for `*.hack` → Caddy container IP + - macOS DNS helper: dnsmasq + `/etc/resolver` for `*.hack` → `127.0.0.1` - Logging stack (Alloy → Loki → Grafana) - Global config: `hack.config.json` (control plane defaults + extension settings) - Gateway bind/port/allowWrites diff --git a/src/commands/doctor-utils.ts b/src/commands/doctor-utils.ts index 9a0c53eb..0759d7c5 100644 --- a/src/commands/doctor-utils.ts +++ b/src/commands/doctor-utils.ts @@ -1,6 +1,10 @@ import { YAML } from "bun"; -import { DEFAULT_CADDY_IP, DEFAULT_INGRESS_NETWORK } from "../constants.ts"; +import { + DEFAULT_CADDY_IP, + DEFAULT_HOST_DNS_IP, + DEFAULT_INGRESS_NETWORK, +} from "../constants.ts"; import { isRecord, isStringArray } from "../lib/guards.ts"; export type ComposeNetworkHygieneError = @@ -26,10 +30,10 @@ export function dnsmasqConfigHasDomain(opts: { readonly text: string; readonly domain: string; }): boolean { - // Accept container IP (preferred), IPv6, or legacy IPv4 config + // Accept localhost (preferred), container IP, or IPv6 config. const containerIpLine = `address=/.${opts.domain}/${DEFAULT_CADDY_IP}`; const ipv6Line = `address=/.${opts.domain}/::1`; - const ipv4Line = `address=/.${opts.domain}/127.0.0.1`; + const ipv4Line = `address=/.${opts.domain}/${DEFAULT_HOST_DNS_IP}`; return ( opts.text.includes(containerIpLine) || opts.text.includes(ipv6Line) || diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 06f2eb76..4625aa7d 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -7,6 +7,7 @@ import { optPath } from "../cli/options.ts"; import { DEFAULT_CADDY_IP, DEFAULT_GRAFANA_HOST, + DEFAULT_HOST_DNS_IP, DEFAULT_INGRESS_GATEWAY, DEFAULT_INGRESS_NETWORK, DEFAULT_INGRESS_SUBNET, @@ -793,7 +794,7 @@ async function checkMacResolverForDomain(domain: string): Promise { async function checkMacDnsmasqConfigForDomain( domain: string ): Promise { - const desiredLine = `address=/.${domain}/${DEFAULT_CADDY_IP}`; + const desiredLine = `address=/.${domain}/${DEFAULT_HOST_DNS_IP}`; const brew = await findExecutableInPath("brew"); if (!brew) { @@ -1256,7 +1257,7 @@ async function checkProxyPortForwarding(): Promise { name: "proxy ports", status: "warn", message: - "Port 443 not forwarding properly. Fix: hack global install (configures DNS to use container IP)", + "Port 443 not forwarding properly. Fix: hack global install (configures DNS to use localhost)", }; } @@ -1700,15 +1701,11 @@ async function maybeMigrateDnsmasq(): Promise { return; } - note( - "dnsmasq migrated to container IP - port forwarding issues resolved", - "doctor" - ); + note("dnsmasq migrated to localhost for macOS host routing", "doctor"); } /** - * Check if dnsmasq has legacy localhost config and offer to migrate to container IP. - * Using the container IP directly bypasses OrbStack port forwarding issues. + * Check if dnsmasq still points at the container IP and offer to migrate it back to localhost. */ async function migrateDnsmasqToContainerIpIfNeeded(): Promise< "migrated" | "skipped" | "not-needed" @@ -1728,25 +1725,25 @@ async function migrateDnsmasqToContainerIpIfNeeded(): Promise< return "skipped"; } - const containerIpHackLine = `address=/.${DEFAULT_PROJECT_TLD}/${DEFAULT_CADDY_IP}`; - const containerIpOauthLine = `address=/.${DEFAULT_OAUTH_ALIAS_ROOT}/${DEFAULT_CADDY_IP}`; const legacyLines = [ - `address=/.${DEFAULT_PROJECT_TLD}/127.0.0.1`, - `address=/.${DEFAULT_OAUTH_ALIAS_ROOT}/127.0.0.1`, + `address=/.${DEFAULT_PROJECT_TLD}/${DEFAULT_CADDY_IP}`, + `address=/.${DEFAULT_OAUTH_ALIAS_ROOT}/${DEFAULT_CADDY_IP}`, `address=/.${DEFAULT_PROJECT_TLD}/::1`, `address=/.${DEFAULT_OAUTH_ALIAS_ROOT}/::1`, ]; + const desiredHackLine = `address=/.${DEFAULT_PROJECT_TLD}/${DEFAULT_HOST_DNS_IP}`; + const desiredOauthLine = `address=/.${DEFAULT_OAUTH_ALIAS_ROOT}/${DEFAULT_HOST_DNS_IP}`; - const hasContainerIp = - text.includes(containerIpHackLine) && text.includes(containerIpOauthLine); + const hasDesired = + text.includes(desiredHackLine) && text.includes(desiredOauthLine); const hasLegacy = legacyLines.some((line) => text.includes(line)); - if (hasContainerIp || !hasLegacy) { + if (hasDesired || !hasLegacy) { return "not-needed"; } const okMigrate = await confirm({ - message: "Migrate dnsmasq to container IP? (fixes port forwarding issues)", + message: "Migrate dnsmasq to localhost? (fixes macOS host routing)", initialValue: true, }); if (isCancel(okMigrate)) { @@ -1756,13 +1753,13 @@ async function migrateDnsmasqToContainerIpIfNeeded(): Promise< return "skipped"; } - // Remove legacy lines and add container IP + // Remove legacy lines and add localhost mapping. let updated = text; for (const legacyLine of legacyLines) { updated = updated.replace(legacyLine, ""); } updated = updated.replace(/\n{3,}/g, "\n\n").trim(); - updated = `${updated}\n${containerIpHackLine}\n${containerIpOauthLine}\n`; + updated = `${updated}\n${desiredHackLine}\n${desiredOauthLine}\n`; await writeTextFileIfChanged(dnsmasqConf, updated); @@ -2013,8 +2010,8 @@ function renderMacNote(): void { note( [ "macOS tip:", - `- wildcard DNS: /etc/resolver/${DEFAULT_PROJECT_TLD} + dnsmasq address=/.${DEFAULT_PROJECT_TLD}/${DEFAULT_CADDY_IP}`, - `- OAuth alias DNS: /etc/resolver/${DEFAULT_OAUTH_ALIAS_ROOT} + dnsmasq address=/.${DEFAULT_OAUTH_ALIAS_ROOT}/${DEFAULT_CADDY_IP}`, + `- wildcard DNS: /etc/resolver/${DEFAULT_PROJECT_TLD} + dnsmasq address=/.${DEFAULT_PROJECT_TLD}/${DEFAULT_HOST_DNS_IP}`, + `- OAuth alias DNS: /etc/resolver/${DEFAULT_OAUTH_ALIAS_ROOT} + dnsmasq address=/.${DEFAULT_OAUTH_ALIAS_ROOT}/${DEFAULT_HOST_DNS_IP}`, ].join("\n"), "doctor" ); diff --git a/src/commands/global.ts b/src/commands/global.ts index fd436132..1afb5bfb 100644 --- a/src/commands/global.ts +++ b/src/commands/global.ts @@ -17,6 +17,7 @@ import { import { DEFAULT_CADDY_IP, DEFAULT_COREDNS_IP, + DEFAULT_HOST_DNS_IP, DEFAULT_INGRESS_GATEWAY, DEFAULT_INGRESS_NETWORK, DEFAULT_INGRESS_SUBNET, @@ -2274,13 +2275,13 @@ async function resolveBrewPrefix(): Promise { async function ensureDnsmasqHackAliases(opts: { readonly dnsmasqConf: string; }): Promise { - const containerIpLines = [ - `address=/.${DEFAULT_PROJECT_TLD}/${DEFAULT_CADDY_IP}`, - `address=/.${DEFAULT_OAUTH_ALIAS_ROOT}/${DEFAULT_CADDY_IP}`, + const desiredLines = [ + `address=/.${DEFAULT_PROJECT_TLD}/${DEFAULT_HOST_DNS_IP}`, + `address=/.${DEFAULT_OAUTH_ALIAS_ROOT}/${DEFAULT_HOST_DNS_IP}`, ] as const; const legacyLines = [ - `address=/.${DEFAULT_PROJECT_TLD}/127.0.0.1`, - `address=/.${DEFAULT_OAUTH_ALIAS_ROOT}/127.0.0.1`, + `address=/.${DEFAULT_PROJECT_TLD}/${DEFAULT_CADDY_IP}`, + `address=/.${DEFAULT_OAUTH_ALIAS_ROOT}/${DEFAULT_CADDY_IP}`, `address=/.${DEFAULT_PROJECT_TLD}/::1`, `address=/.${DEFAULT_OAUTH_ALIAS_ROOT}/::1`, ] as const; @@ -2290,7 +2291,7 @@ async function ensureDnsmasqHackAliases(opts: { content: existing, legacyLines, }); - const missing = containerIpLines.filter( + const missing = desiredLines.filter( (line) => !migrated.content.includes(line) ); const shouldWrite = migrated.changed || missing.length > 0; @@ -2331,7 +2332,7 @@ function removeLegacyDnsmasqLines(opts: { // Clean up any double newlines left from removal. const cleaned = updated.replace(/\n{3,}/g, "\n\n").trim(); - logger.info({ message: "Migrating dnsmasq to use container IP..." }); + logger.info({ message: "Migrating dnsmasq to use localhost..." }); return { content: cleaned, changed: true }; } @@ -2403,8 +2404,8 @@ async function flushMacDnsCache(): Promise { function noteDnsConfigured(opts: { readonly dnsmasqConf: string }): void { note( [ - `DNS configured: *.${DEFAULT_PROJECT_TLD} → ${DEFAULT_CADDY_IP} (container)`, - `DNS configured: *.${DEFAULT_OAUTH_ALIAS_ROOT} → ${DEFAULT_CADDY_IP} (container)`, + `DNS configured: *.${DEFAULT_PROJECT_TLD} → ${DEFAULT_HOST_DNS_IP} (localhost)`, + `DNS configured: *.${DEFAULT_OAUTH_ALIAS_ROOT} → ${DEFAULT_HOST_DNS_IP} (localhost)`, `- dnsmasq: ${opts.dnsmasqConf}`, `- resolver: /etc/resolver/${DEFAULT_PROJECT_TLD}`, `- resolver: /etc/resolver/${DEFAULT_OAUTH_ALIAS_ROOT}`, diff --git a/src/constants.ts b/src/constants.ts index 6b0ec637..1bffc7df 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,6 +7,7 @@ export const DEFAULT_INGRESS_SUBNET = "172.30.0.0/16" as const; export const DEFAULT_INGRESS_GATEWAY = "172.30.0.1" as const; export const DEFAULT_CADDY_IP = "172.30.0.2" as const; export const DEFAULT_COREDNS_IP = "172.30.0.53" as const; +export const DEFAULT_HOST_DNS_IP = "127.0.0.1" as const; export const DEFAULT_PROJECT_TLD = "hack" as const; export const DEFAULT_GRAFANA_HOST = `logs.${DEFAULT_PROJECT_TLD}` as const;