Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/commands/doctor-utils.ts
Original file line number Diff line number Diff line change
@@ -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 =
Expand All @@ -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) ||
Expand Down
37 changes: 17 additions & 20 deletions src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -793,7 +794,7 @@ async function checkMacResolverForDomain(domain: string): Promise<CheckResult> {
async function checkMacDnsmasqConfigForDomain(
domain: string
): Promise<CheckResult> {
const desiredLine = `address=/.${domain}/${DEFAULT_CADDY_IP}`;
const desiredLine = `address=/.${domain}/${DEFAULT_HOST_DNS_IP}`;

const brew = await findExecutableInPath("brew");
if (!brew) {
Expand Down Expand Up @@ -1256,7 +1257,7 @@ async function checkProxyPortForwarding(): Promise<CheckResult> {
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)",
};
}

Expand Down Expand Up @@ -1700,15 +1701,11 @@ async function maybeMigrateDnsmasq(): Promise<void> {
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"
Expand All @@ -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)) {
Expand All @@ -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);

Expand Down Expand Up @@ -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"
);
Expand Down
19 changes: 10 additions & 9 deletions src/commands/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -2274,13 +2275,13 @@ async function resolveBrewPrefix(): Promise<string> {
async function ensureDnsmasqHackAliases(opts: {
readonly dnsmasqConf: string;
}): Promise<void> {
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 };
}

Expand Down Expand Up @@ -2403,8 +2404,8 @@ async function flushMacDnsCache(): Promise<void> {
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}`,
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading