From 7a4de03c0e8d77a12b62a0c6d5d2a3fa4b5fa730 Mon Sep 17 00:00:00 2001 From: Thomas Vervest Date: Tue, 7 Jul 2026 17:24:50 +0200 Subject: [PATCH] fix(integrations): accept gluetun-latest VPN settings payload (#6149) Co-authored-by: Thomas --- .../integrations/src/gluetun/gluetun-types.ts | 6 +- .../gluetun/test/gluetun-integration.spec.ts | 170 ++++++++++++++++++ 2 files changed, 173 insertions(+), 3 deletions(-) diff --git a/packages/integrations/src/gluetun/gluetun-types.ts b/packages/integrations/src/gluetun/gluetun-types.ts index 6886d72a7..d9fdc626a 100644 --- a/packages/integrations/src/gluetun/gluetun-types.ts +++ b/packages/integrations/src/gluetun/gluetun-types.ts @@ -46,7 +46,7 @@ const gluetunServerSelectionSchema = z.object({ isps: z.array(z.string()).nullable(), names: z.array(z.string()).nullable(), numbers: z.array(z.number()).nullable(), - hostnames: z.array(z.string()), + hostnames: z.array(z.string()).nullable(), owned_only: z.boolean(), free_only: z.boolean(), premium_only: z.boolean(), @@ -65,7 +65,7 @@ const gluetunPortForwardingSchema = z.object({ status_file_path: z.string(), up_command: z.string(), down_command: z.string(), - listening_port: z.number(), + listening_port: z.union([z.number(), z.array(z.number())]), username: z.string(), password: z.string(), }); @@ -98,7 +98,7 @@ const gluetunOpenvpnSettingsSchema = z.object({ const gluetunWireguardSettingsSchema = z.object({ private_key: z.string(), pre_shared_key: z.string(), - addresses: z.array(z.string()), + addresses: z.array(z.string()).nullable(), allowed_ips: z.array(z.string()), interface: z.string(), persistent_keep_alive_interval: z.number(), diff --git a/packages/integrations/src/gluetun/test/gluetun-integration.spec.ts b/packages/integrations/src/gluetun/test/gluetun-integration.spec.ts index 1f84005f3..0ff870fad 100644 --- a/packages/integrations/src/gluetun/test/gluetun-integration.spec.ts +++ b/packages/integrations/src/gluetun/test/gluetun-integration.spec.ts @@ -124,6 +124,126 @@ const VPN_SETTINGS_PAYLOAD = { }, }; +const ALT_VPN_SETTINGS_PAYLOAD = { + type: "openvpn", + provider: { + name: "custom", + server_selection: { + vpn: "openvpn", + countries: null, + categories: null, + regions: null, + cities: null, + isps: null, + names: null, + numbers: null, + hostnames: null, + owned_only: false, + free_only: false, + premium_only: false, + stream_only: false, + multi_hop_only: false, + port_forward_only: false, + secure_core_only: false, + tor_only: false, + openvpn: { + config_file_path: "/gluetun/custom.conf", + protocol: "tcp", + endpoint_ip: "0.0.0.0", + custom_port: 0, + pia_encryption_preset: "", + }, + wireguard: { + endpoint_ip: "0.0.0.0", + endpoint_port: 0, + public_key: "", + }, + }, + port_forwarding: { + enabled: false, + provider: "", + status_file_path: "/tmp/gluetun/forwarded_port", + up_command: "", + down_command: "", + listening_port: [0], + ports_count: 0, + username: "", + password: "", + }, + }, + amneziawg: { + wireguard: { + private_key: "", + pre_shared_key: "", + addresses: null, + allowed_ips: ["0.0.0.0/0", "::/0"], + interface: "tun0", + persistent_keep_alive_interval: 0, + mtu: 0, + implementation: "userspace", + }, + junk_packet_count: 0, + junk_packet_min: 0, + junk_packet_max: 0, + padding_s1: 0, + padding_s2: 0, + padding_s3: 0, + padding_s4: 0, + header_h1: "", + header_h2: "", + header_h3: "", + header_h4: "", + init_packet_i1: "", + init_packet_i2: "", + init_packet_i3: "", + init_packet_i4: "", + init_packet_i5: "", + }, + openvpn: { + version: "2.6", + user: "", + password: "", + config_file_path: "/gluetun/custom.conf", + ciphers: null, + auth: "", + cert: "", + key: "", + encrypted_key: "", + key_passphrase: "", + pia_encryption_preset: "", + mssfix: 0, + interface: "tun0", + process_user: "nonrootuser", + verbosity: 1, + flags: null, + }, + wireguard: { + private_key: "", + pre_shared_key: "", + addresses: null, + allowed_ips: ["0.0.0.0/0", "::/0"], + interface: "tun0", + persistent_keep_alive_interval: 0, + mtu: 0, + implementation: "auto", + }, + pmtud: { + icmp_addresses: ["1.1.1.1", "8.8.8.8"], + tcp_addresses: [ + "1.1.1.1:443", + "8.8.8.8:443", + "1.1.1.1:53", + "8.8.8.8:53", + "[2606:4700:4700::1111]:53", + "[2001:4860:4860::8888]:53", + "[2606:4700:4700::1111]:443", + "[2001:4860:4860::8888]:443", + ], + }, + up_command: "", + down_command: "", +}; + const jsonResponse = (body: unknown, init: ResponseInit = { status: 200 }): FetchResponse => new Response(JSON.stringify(body), { ...init, @@ -368,4 +488,54 @@ describe("gluetun schemas", () => { test("vpn settings schema rejects a non-string type", () => { expect(() => gluetunVpnSettingsSchema.parse({ ...VPN_SETTINGS_PAYLOAD, type: 1 })).toThrow(); }); + + test("vpn settings schema parses the live gluetun-latest custom/OpenVPN payload", () => { + const parsed = gluetunVpnSettingsSchema.parse(ALT_VPN_SETTINGS_PAYLOAD); + expect(parsed.type).toBe("openvpn"); + expect(parsed.provider.name).toBe("custom"); + }); + + test("vpn settings schema accepts null server-selection hostnames", () => { + const payload = { + ...VPN_SETTINGS_PAYLOAD, + provider: { + ...VPN_SETTINGS_PAYLOAD.provider, + server_selection: { + ...VPN_SETTINGS_PAYLOAD.provider.server_selection, + hostnames: null, + }, + }, + }; + + expect(() => gluetunVpnSettingsSchema.parse(payload)).not.toThrow(); + }); + + test("vpn settings schema accepts listening_port as a number or a list", () => { + const asList = { + ...VPN_SETTINGS_PAYLOAD, + provider: { + ...VPN_SETTINGS_PAYLOAD.provider, + port_forwarding: { + ...VPN_SETTINGS_PAYLOAD.provider.port_forwarding, + listening_port: [0], + }, + }, + }; + + // The happy-path fixture already exercises the scalar form (listening_port: 0). + expect(() => gluetunVpnSettingsSchema.parse(VPN_SETTINGS_PAYLOAD)).not.toThrow(); + expect(() => gluetunVpnSettingsSchema.parse(asList)).not.toThrow(); + }); + + test("vpn settings schema accepts null wireguard addresses", () => { + const payload = { + ...VPN_SETTINGS_PAYLOAD, + wireguard: { + ...VPN_SETTINGS_PAYLOAD.wireguard, + addresses: null, + }, + }; + + expect(() => gluetunVpnSettingsSchema.parse(payload)).not.toThrow(); + }); });