diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts index 04410dd5..de7dab01 100644 --- a/src/lib/caddy.ts +++ b/src/lib/caddy.ts @@ -1495,8 +1495,13 @@ async function buildL4Servers(): Promise | null> { routes.push(route); } + // Determine protocol from the hosts on this listen address. + // All hosts sharing a listen address must use the same protocol. + const protocol = hosts[0].protocol as string; + const listenValue = protocol === "udp" ? `udp/${listenAddr}` : listenAddr; + servers[`l4_server_${serverIdx++}`] = { - listen: [listenAddr], + listen: [listenValue], routes, }; } diff --git a/tests/e2e/functional/l4-proxy-routing.spec.ts b/tests/e2e/functional/l4-proxy-routing.spec.ts index c74b0071..71b00f10 100644 --- a/tests/e2e/functional/l4-proxy-routing.spec.ts +++ b/tests/e2e/functional/l4-proxy-routing.spec.ts @@ -47,9 +47,11 @@ test.describe.serial('L4 TCP Proxy Routing', () => { expect(connected).toBe(true); }); - test('unused TCP port does not accept connections', async () => { - const connected = await tcpConnect('127.0.0.1', TCP_PORT_2, 2000); - expect(connected).toBe(false); + test('unused TCP port does not echo data back', async () => { + // Docker accepts the TCP connection at the container level even without a Caddy listener, + // but no data should be proxied/echoed back since there's no L4 host configured on this port. + const res = await tcpSend('127.0.0.1', TCP_PORT_2, 'probe', 2000); + expect(res.data).not.toContain('probe'); }); test('disabled TCP proxy host stops accepting connections', async ({ page }) => { diff --git a/tests/helpers/proxy-api.ts b/tests/helpers/proxy-api.ts index 7f191e25..70b6f986 100644 --- a/tests/helpers/proxy-api.ts +++ b/tests/helpers/proxy-api.ts @@ -157,7 +157,10 @@ export async function importCertificate(page: Page, config: ImportedCertificateC await page.locator('[name="private_key_pem"]').fill(config.privateKeyPem); await page.getByRole('button', { name: /^import certificate$/i }).click(); - await expect(page.getByText(config.name).first()).toBeVisible({ timeout: 10_000 }); + // Wait for the import sheet to close, then verify the cert appears in the table + await expect(page.getByRole('heading', { name: /^import certificate$/i })).not.toBeVisible({ timeout: 10_000 }); + await page.waitForTimeout(500); // allow page to revalidate + await expect(page.locator('table').getByText(config.name).first()).toBeVisible({ timeout: 10_000 }); } export async function generateCaCertificate(page: Page, config: GeneratedCaConfig): Promise { @@ -174,7 +177,8 @@ export async function generateCaCertificate(page: Page, config: GeneratedCaConfi } await page.getByRole('button', { name: /generate ca certificate/i }).click(); - await expect(page.getByText(config.name)).toBeVisible({ timeout: 15_000 }); + await expect(page.getByRole('heading', { name: /^add ca certificate$/i })).not.toBeVisible({ timeout: 10_000 }); + await expect(page.locator('table').getByText(config.name).first()).toBeVisible({ timeout: 15_000 }); } export async function issueClientCertificate(