Merge pull request #1971 from water-sucks/add-tls-server-name-to-health-check-fields

feat(healthcheck): add SNI support for target healthchecks
This commit is contained in:
Owen Schwartz
2025-12-04 14:42:25 -05:00
committed by GitHub
11 changed files with 72 additions and 15 deletions

View File

@@ -526,6 +526,8 @@
"targetCreatedDescription": "Target has been created successfully",
"targetErrorCreate": "Failed to create target",
"targetErrorCreateDescription": "An error occurred while creating the target",
"tlsServerName": "TLS Server Name",
"tlsServerNameDescription": "The TLS server name to use for SNI",
"save": "Save",
"proxyAdditional": "Additional Proxy Settings",
"proxyAdditionalDescription": "Configure how your resource handles proxy settings",

View File

@@ -175,7 +175,8 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
hcFollowRedirects: boolean("hcFollowRedirects").default(true),
hcMethod: varchar("hcMethod").default("GET"),
hcStatus: integer("hcStatus"), // http code
hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy"
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
hcTlsServerName: text("hcTlsServerName"),
});
export const exitNodes = pgTable("exitNodes", {

View File

@@ -195,7 +195,8 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
}).default(true),
hcMethod: text("hcMethod").default("GET"),
hcStatus: integer("hcStatus"), // http code
hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy"
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
hcTlsServerName: text("hcTlsServerName"),
});
export const exitNodes = sqliteTable("exitNodes", {

View File

@@ -272,7 +272,8 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
hcUnhealthyInterval: targetHealthCheck.hcUnhealthyInterval,
hcTimeout: targetHealthCheck.hcTimeout,
hcHeaders: targetHealthCheck.hcHeaders,
hcMethod: targetHealthCheck.hcMethod
hcMethod: targetHealthCheck.hcMethod,
hcTlsServerName: targetHealthCheck.hcTlsServerName,
})
.from(targets)
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
@@ -344,7 +345,8 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
hcUnhealthyInterval: target.hcUnhealthyInterval, // in seconds
hcTimeout: target.hcTimeout, // in seconds
hcHeaders: hcHeadersSend,
hcMethod: target.hcMethod
hcMethod: target.hcMethod,
hcTlsServerName: target.hcTlsServerName,
};
});

View File

@@ -66,7 +66,8 @@ export async function addTargets(
hcUnhealthyInterval: hc.hcUnhealthyInterval, // in seconds
hcTimeout: hc.hcTimeout, // in seconds
hcHeaders: hcHeadersSend,
hcMethod: hc.hcMethod
hcMethod: hc.hcMethod,
hcTlsServerName: hc.hcTlsServerName,
};
});

View File

@@ -48,6 +48,7 @@ const createTargetSchema = z.strictObject({
hcFollowRedirects: z.boolean().optional().nullable(),
hcMethod: z.string().min(1).optional().nullable(),
hcStatus: z.int().optional().nullable(),
hcTlsServerName: z.string().optional().nullable(),
path: z.string().optional().nullable(),
pathMatchType: z
.enum(["exact", "prefix", "regex"])
@@ -247,7 +248,8 @@ export async function createTarget(
hcFollowRedirects: targetData.hcFollowRedirects ?? null,
hcMethod: targetData.hcMethod ?? null,
hcStatus: targetData.hcStatus ?? null,
hcHealth: "unknown"
hcHealth: "unknown",
hcTlsServerName: targetData.hcTlsServerName ?? null
})
.returning();

View File

@@ -57,6 +57,7 @@ function queryTargets(resourceId: number) {
hcMethod: targetHealthCheck.hcMethod,
hcStatus: targetHealthCheck.hcStatus,
hcHealth: targetHealthCheck.hcHealth,
hcTlsServerName: targetHealthCheck.hcTlsServerName,
path: targets.path,
pathMatchType: targets.pathMatchType,
rewritePath: targets.rewritePath,

View File

@@ -42,6 +42,7 @@ const updateTargetBodySchema = z.strictObject({
hcFollowRedirects: z.boolean().optional().nullable(),
hcMethod: z.string().min(1).optional().nullable(),
hcStatus: z.int().optional().nullable(),
hcTlsServerName: z.string().optional().nullable(),
path: z.string().optional().nullable(),
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(),
rewritePath: z.string().optional().nullable(),
@@ -217,7 +218,8 @@ export async function updateTarget(
hcHeaders: hcHeaders,
hcFollowRedirects: parsedBody.data.hcFollowRedirects,
hcMethod: parsedBody.data.hcMethod,
hcStatus: parsedBody.data.hcStatus
hcStatus: parsedBody.data.hcStatus,
hcTlsServerName: parsedBody.data.hcTlsServerName,
})
.where(eq(targetHealthCheck.targetId, targetId))
.returning();

View File

@@ -464,6 +464,7 @@ export default function ReverseProxyTargets(props: {
hcStatus: null,
hcMode: null,
hcUnhealthyInterval: null,
hcTlsServerName: null,
siteType: sites.length > 0 ? sites[0].type : null,
new: true,
updated: false
@@ -629,7 +630,8 @@ export default function ReverseProxyTargets(props: {
hcHealth: "unknown",
hcStatus: null,
hcMode: null,
hcUnhealthyInterval: null
hcUnhealthyInterval: null,
hcTlsServerName: null,
};
setTargets([...targets, newTarget]);
@@ -729,7 +731,8 @@ export default function ReverseProxyTargets(props: {
hcMethod: target.hcMethod || null,
hcStatus: target.hcStatus || null,
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
hcMode: target.hcMode || null
hcMode: target.hcMode || null,
hcTlsServerName: target.hcTlsServerName,
};
// Only include path-related fields for HTTP resources
@@ -1822,7 +1825,9 @@ export default function ReverseProxyTargets(props: {
hcMode: selectedTargetForHealthCheck.hcMode || "http",
hcUnhealthyInterval:
selectedTargetForHealthCheck.hcUnhealthyInterval ||
30
30,
hcTlsServerName: selectedTargetForHealthCheck.hcTlsServerName ||
undefined,
}}
onChanges={async (config) => {
console.log("here");

View File

@@ -297,6 +297,7 @@ export default function Page() {
hcStatus: null,
hcMode: null,
hcUnhealthyInterval: null,
hcTlsServerName: null,
siteType: sites.length > 0 ? sites[0].type : null,
new: true,
updated: false
@@ -454,7 +455,8 @@ export default function Page() {
hcHealth: "unknown",
hcStatus: null,
hcMode: null,
hcUnhealthyInterval: null
hcUnhealthyInterval: null,
hcTlsServerName: null
};
setTargets([...targets, newTarget]);
@@ -576,7 +578,8 @@ export default function Page() {
target.hcFollowRedirects || null,
hcStatus: target.hcStatus || null,
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
hcMode: target.hcMode || null
hcMode: target.hcMode || null,
hcTlsServerName: target.hcTlsServerName
};
// Only include path-related fields for HTTP resources
@@ -1800,7 +1803,10 @@ export default function Page() {
"http",
hcUnhealthyInterval:
selectedTargetForHealthCheck.hcUnhealthyInterval ||
30
30,
hcTlsServerName:
selectedTargetForHealthCheck.hcTlsServerName ||
undefined
}}
onChanges={async (config) => {
if (selectedTargetForHealthCheck) {

View File

@@ -51,6 +51,7 @@ type HealthCheckConfig = {
hcFollowRedirects: boolean;
hcMode: string;
hcUnhealthyInterval: number;
hcTlsServerName: string;
};
type HealthCheckDialogProps = {
@@ -93,7 +94,8 @@ export default function HealthCheckDialog({
hcPort: z.number().positive().gt(0).lte(65535),
hcFollowRedirects: z.boolean(),
hcMode: z.string(),
hcUnhealthyInterval: z.int().positive().min(5)
hcUnhealthyInterval: z.int().positive().min(5),
hcTlsServerName: z.string()
});
const form = useForm<z.infer<typeof healthCheckSchema>>({
@@ -129,7 +131,8 @@ export default function HealthCheckDialog({
hcPort: initialConfig?.hcPort,
hcFollowRedirects: initialConfig?.hcFollowRedirects,
hcMode: initialConfig?.hcMode,
hcUnhealthyInterval: initialConfig?.hcUnhealthyInterval
hcUnhealthyInterval: initialConfig?.hcUnhealthyInterval,
hcTlsServerName: initialConfig?.hcTlsServerName ?? ""
});
}, [open]);
@@ -531,6 +534,37 @@ export default function HealthCheckDialog({
)}
/>
{/*TLS Server Name (SNI)*/}
<FormField
control={form.control}
name="hcTlsServerName"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("tlsServerName")}
</FormLabel>
<FormControl>
<Input
{...field}
onChange={(e) => {
field.onChange(e);
handleFieldChange(
"hcTlsServerName",
e.target.value
);
}}
/>
</FormControl>
<FormDescription>
{t(
"tlsServerNameDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Custom Headers */}
<FormField
control={form.control}