Skip to content

feat: add Gluetun integration with VPN status widget#5812

Open
Lok3sh-kumar wants to merge 2 commits into
homarr-labs:devfrom
Lok3sh-kumar:feat/gluetun-integration
Open

feat: add Gluetun integration with VPN status widget#5812
Lok3sh-kumar wants to merge 2 commits into
homarr-labs:devfrom
Lok3sh-kumar:feat/gluetun-integration

Conversation

@Lok3sh-kumar
Copy link
Copy Markdown

Adds a Gluetun integration and accompanying dashboard widget surfacing VPN connection details from a running gluetun container.

  • GluetunIntegration calls the /v1/vpn/status, /v1/dns/status, /v1/publicip/ip and /v1/vpn/settings control endpoints in parallel and maps the results into a flat GluetunStatusInfo
  • Auth supports either an X-API-Key header or Basic auth from username/password secrets
  • Cached request handler with a 30s TTL feeds the widget through the gluetun tRPC router
  • Widget renders a single card for one VPN or a scroll list for many, with VPN/DNS status, public IP, city/country and provider/protocol
  • Tests: integration class (mocked HTTP), zod schema parsing, widget status-color helper

Homarr

Thank you for your contribution. Please ensure that your pull request meets the following pull request:

  • Builds without warnings or errors (pnpm build, autofix with pnpm format:fix)
  • Pull request targets dev branch
  • Commits follow the conventional commits guideline
  • No shorthand variable names are used (eg. x, y, i or any abbrevation)
  • Documentation is up to date. Create a pull request here.

Adds a Gluetun integration and accompanying dashboard widget surfacing
VPN connection details from a running gluetun container.

- GluetunIntegration calls the /v1/vpn/status, /v1/dns/status,
  /v1/publicip/ip and /v1/vpn/settings control endpoints in parallel
  and maps the results into a flat GluetunStatusInfo
- Auth supports either an X-API-Key header or Basic auth from
  username/password secrets
- Cached request handler with a 30s TTL feeds the widget through
  the gluetun tRPC router
- Widget renders a single card for one VPN or a scroll list for many,
  with VPN/DNS status, public IP, city/country and provider/protocol
- Tests: integration class (mocked HTTP), zod schema parsing, widget
  status-color helper

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Lok3sh-kumar Lok3sh-kumar requested a review from a team as a code owner May 28, 2026 16:18
@dokploy-homarr-labs
Copy link
Copy Markdown

🚨 Preview Deployment Blocked - Security Protection

Your pull request was blocked from triggering preview deployments

Why was this blocked?

  • User: Lok3sh-kumar
  • Repository: homarr
  • Permission Level: read
  • Required Level: write, maintain, or admin

How to resolve this:

Option 1: Get Collaborator Access (Recommended)
Ask a repository maintainer to invite you as a collaborator with write permissions or higher.

Option 2: Request Permission Override
Ask a repository administrator to disable security validation for this specific application if appropriate.

For Repository Administrators:

To disable this security check (⚠️ not recommended for public repositories):
Enter to preview settings and disable the security check.


This security measure protects against malicious code execution in preview deployments. Only trusted collaborators should have the ability to trigger deployments.

🛡️ Learn more about this security feature

This protection prevents unauthorized users from:

  • Executing malicious code on the deployment server
  • Accessing environment variables and secrets
  • Potentially compromising the infrastructure

Preview deployments are powerful but require trust. Only users with repository write access can trigger them.

@Lok3sh-kumar
Copy link
Copy Markdown
Author

Screenshot 2026-05-28 at 10 00 20 PM Screenshot 2026-05-28 at 10 00 29 PM

Comment on lines +7 to +17
getVpnInfo: publicProcedure.concat(createManyIntegrationMiddleware("query", "gluetun")).query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = gluetunVPNStatusHandler.handler(integration, {});
const result = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return result.data;
}),
);

return results;
}),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works when you refresh the page, but if the VPN goes down while you are on the page, the status will not be updated. Can you check the other integrations and widgets for example implementations on subscriptions?

Comment on lines +39 to +44
const [vpnStatus, dnsStatus, publicIp, vpnSettings] = await Promise.all([
this.getVpnStatusAsync(authHeaders),
this.getDnsStatusAsync(authHeaders),
this.getPublicIpAsync(authHeaders),
this.getVpnSettingAsync(authHeaders),
]);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but is there no endpoint to get all data at once? If there is not, you can leave it as is.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@manuel-rw we can't get all the details from one URL that's why it's like this. here is the documentation.

const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getVpnDetailsAsync();
},
cacheDuration: dayjs.duration(30, "seconds"),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cache duration is quite aggressive. I think 5 minutes would be fine.


if (integrations.length === 0) {
return <EmptyState />;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check the other widgets, you can throw an exception in this case.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this check as it can never happen due to an exception that is thrown in item-content.tsx

if (integrations.length === 1) {
const [vpn] = integrations;

if (!vpn) return <EmptyState />;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, throw instead

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception is thrown automatically, so no need to handle this. Just use something like

// It will always have at least one integration as otherwise the NoIntegrationSelectedError would be thrown in item-content.tsx
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const integration = integrations[0]!;

or similar

@manuel-rw manuel-rw requested a review from Meierschlumpf May 28, 2026 18:36
@manuel-rw
Copy link
Copy Markdown
Member

Just to be sure; this IP you posted isn't yours, correct?

@Lok3sh-kumar
Copy link
Copy Markdown
Author

Just to be sure; this IP you posted isn't yours, correct?

yeah not mine, just a placeholder for screnshot

iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/gluetun.svg",
category: ["gluetun"],
defaultUrl: "http://localhost:8001",
documentationUrl: createDocumentationLink("/docs/integrations/umami"),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update documentation url and add @ts-expect-error until docs have been created and merged

secretKinds: [["username", "password"], ["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/gluetun.svg",
category: ["gluetun"],
defaultUrl: "http://localhost:8001",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We haven't specified defaultUrl for other self hosted integrations, therefore I would suggest to remove it here

"tracearr",
"speedtestTracker",
"umami",
"gluetun",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it makes more sense to have a vpn widget? What do you guys think?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can work on that, but what else than gluetun would be a candidate?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what's your opinion on this @manuel-rw, but I think the value of this test is very limited. Not sure if we can add e2e tests instead that test with a running docker container?

Comment on lines +13 to +22
export const gluetunPublicIpSchema = z.object({
public_ip: z.string(),
region: z.string(),
country: z.string(),
city: z.string(),
location: z.string(),
organization: z.string(),
postal_code: z.string(),
timezone: z.string(),
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did you get this schema from? Based on the docs it only returns the public_ip

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't use the fields it might make more sense to add a link to the docs and remove the property so if the field has another format or does not exist for some users, it does not break the integration for them

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for all other schemas below

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Meierschlumpf the shemas come from this documentation . Since there was not a single endpoint i bunched them together to get the final schema

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it is worth to have a test for one simple ternary operator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants