Compare commits

...

204 Commits

Author SHA1 Message Date
Quentin McGaw db947c17a8 feat(dns): restrict plain DNS output traffic 2026-02-10 16:19:08 +00:00
Quentin McGaw b0a75673bd chore(dev): ensure project compiles on darwin and windows 2026-02-09 15:41:52 +00:00
Quentin McGaw 5f0c499808 fix(protonvpn): support port 51820 for UDP OpenVPN 2026-02-09 15:41:52 +00:00
Quentin McGaw bdd69a1fb7 fix(healthcheck): prevent race condition and fix #3096 (#3123) 2026-02-07 18:11:04 +01:00
Quentin McGaw 1af75bb30c fix(openvpn): only log openvpn version corresponding to OPENVPN_VERSION 2026-02-07 16:49:21 +00:00
Chris Duck 9c1cd7e8b1 fix(protonvpn): update OpenVPN settings (#3120) 2026-02-06 14:18:10 +01:00
Quentin McGaw facc6df3be chore(all): replace netlink library for more flexibility (#3107) 2026-01-27 01:11:39 -08:00
Quentin McGaw e292a4c9be fix(httpproxy): remove info log when no Proxy-Authorization header is present 2026-01-24 19:39:20 +00:00
Quentin McGaw 9e4dd61c19 feat(ipvanish): update servers data 2026-01-24 19:32:21 +00:00
Quentin McGaw fe3d4a94d4 chore(all): make code compilable for other platforms than Linux 2026-01-24 17:56:10 +00:00
Quentin McGaw de38d759a4 feat(vpn): path MTU discovery to find the best MTU (#2586) 2026-01-21 09:02:23 -08:00
Quentin McGaw fba60af772 fix(wireguard): fix detection of kernelspace wireguard 2026-01-20 21:39:30 +00:00
Quentin McGaw 9b9b723887 chore(mullvad): add openvpn removal warning 2025-12-29 05:28:13 +00:00
Quentin McGaw a10349e378 fix(slickvpn): add missing servers found online 2025-12-24 20:29:05 +00:00
Marcin Łapaj 983330266a fix(purevpn/updater): parse country and city from hostname and merges with ip address information (#2991) 2025-12-24 05:02:57 +01:00
Jeremy Lin 6eb511fb2a fix(publicip/api/cloudflare): add Referer header (#3058) 2025-12-24 04:52:25 +01:00
Quentin McGaw 666f55767b hotfix(autoheal): monitor health errors asynchronously with other setup steps
- May resolve #3021
- no need to wait for dns, port forwarding etc. to setup to monitor health errors
2025-12-24 02:58:42 +00:00
Quentin McGaw 0a0bb4cf71 hotfix(healthcheck): only fallback to plain dns if icmp is not permitted on the very first try 2025-12-24 02:58:42 +00:00
Quentin McGaw 2b0719225d fix(purevpn): update openvpn configuration
- Credits to @mlapaj
- Taken and verified from #2991
2025-12-23 17:34:25 +00:00
Quentin McGaw c97bd1bb7c fix(publicip/api/ip2location): rename countries to match standard country names 2025-12-23 17:34:25 +00:00
Quentin McGaw 10a7c75aa6 feat(publicip/api): query all fetchers in parallel and pick most popular result 2025-12-23 17:34:25 +00:00
Quentin McGaw 617f1b764f chore(storage): ignore persisted servers data with a timestamp in the future
- force users to set a timestamp as the current one if manually editing servers.json which they should not do anyway
- favor future servers data updates through a container image pull to override older user-defined data
2025-12-23 15:38:17 +00:00
Quentin McGaw 600f2ab643 chore(storage): fix comment for ServersAreEqual 2025-12-23 15:36:08 +00:00
Quentin McGaw 7052d5490b hotfix(pia): revert port changes
- This reverts commit fd6e5e4e90.
- Port changes are more involved and require cipher, auth and certificate changes as well
2025-12-23 14:02:47 +00:00
Quentin McGaw 6a5a836cb6 feat(pia): update servers data 2025-12-23 13:51:34 +00:00
Quentin McGaw a649b0adc1 hotfix(slickvpn): remove unused code 2025-12-23 13:45:41 +00:00
Quentin McGaw beaa8b5589 fix(slickvpn/updater): only keep 11 servers
- this reverts most of commit e806fe02db.
2025-12-23 03:03:47 +00:00
Quentin McGaw e806fe02db fix(slickvpn/updater): update existing hostnames from servers.json
- SlickVPN location website is now total garbage which cannot be parsed
2025-12-23 02:50:15 +00:00
Quentin McGaw 92237658c3 chore(wevpn): removed since it is now decomissioned 2025-12-23 02:12:02 +00:00
Quentin McGaw e627504fb5 fix(vpnunlimited/updater): remove no longer valid hosts 2025-12-23 02:02:26 +00:00
Quentin McGaw cc1c253bad feat(vpnsecure): update servers data 2025-12-23 01:59:34 +00:00
Quentin McGaw c27dac10fe fix(vpnsecure/updater): allow region and city to be set to N/A 2025-12-23 01:59:23 +00:00
Quentin McGaw 7d1e2eb226 feat(torguard): servers data update 2025-12-23 01:54:48 +00:00
Quentin McGaw 5b5aa5e014 feat(surfshark): update servers data 2025-12-23 01:54:15 +00:00
Quentin McGaw 9ee3ed754d chore(cyberghost/updater): do not log warnings for "no such host" 2025-12-23 01:51:59 +00:00
Quentin McGaw 0ca466fbd5 feat(cyberghost): update servers data 2025-12-23 01:51:11 +00:00
Quentin McGaw 1c1d271967 fix(dns): retry on next period the blocklists update after a failed update
- Fix #3053
2025-12-23 01:40:29 +00:00
Quentin McGaw cc89b35b63 chore(pia): add test to ensure default ports are within ports from their API 2025-12-22 23:24:13 +00:00
Quentin McGaw fd6e5e4e90 fix(pia): update default openvpn ports and possible ports to choose
- Fix #3046
2025-12-22 22:47:17 +00:00
Quentin McGaw d702ed4122 fix(dns): dns over tls re-uses TCP connections
- Bump dns to v2.0.0-rc10
- DNS over HTTPs not affected since it did re-use connections already
2025-12-22 21:09:22 +00:00
Quentin McGaw 2d00f3fe25 hotfix(server/auth): case sensitivity for HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE 2025-12-05 17:03:42 +00:00
Quentin McGaw 56db5a83c0 chore(healthcheck): log one error per line on failure for readability 2025-12-05 17:03:04 +00:00
Quentin McGaw f5206375c0 docs(readme): add star history graph because it's fun 2025-11-26 14:08:46 +00:00
Quentin McGaw c25c9f6f0e feat(healthcheck/icmp): use the same id and increasing sequence number
- easier debug logs flow
- MAY cost less conntrack-ed slots on the VPN server
- resets id and sequence to 1 when reconnecting VPN
- resets id and sequence to 1 after 5 minutes
- resets id and sequence to 1 if sequence is complete (65535)
2025-11-26 13:41:55 +00:00
Quentin McGaw 08a7aae5f1 chore(ci): use qdm12/log library to have timed logs for tests 2025-11-26 13:41:55 +00:00
dependabot[bot] 57d8eb03c5 Chore(deps): Bump golang.org/x/crypto from 0.44.0 to 0.45.0 (#2999) 2025-11-26 00:29:02 +01:00
dependabot[bot] 2b55161fbb Chore(deps): Bump actions/checkout from 5 to 6 (#3001) 2025-11-26 00:27:44 +01:00
Quentin McGaw c4f2a224d4 change(dns): log filtered requests at the debug level 2025-11-25 16:50:17 +00:00
Quentin McGaw 8bb0cc324b fix(dns): prevent dns restart crash if DOT=off and DNS_KEEP_NAMESERVER=off 2025-11-24 16:45:53 +00:00
Quentin McGaw 2afa988174 hotfix(dns): resolve .site and .network domain names as non-local 2025-11-23 21:39:49 +00:00
Quentin McGaw a35c994bc8 feat(port-forwarding): add {{VPN_INTERFACE}} template variable 2025-11-22 23:32:26 +00:00
Quentin McGaw 0fad44fb68 chore(vpn): do not restart VPN if startup check fails and HEALTH_RESTART_VPN=off
- Note you still should not set HEALTH_RESTART_VPN=off this is for debugging only
2025-11-22 15:21:40 +00:00
Quentin McGaw 4f9dcff3f4 hotfix(ci): verify-private requirement for publish, not itself 2025-11-21 20:16:57 +00:00
Quentin McGaw 1abc90970d chore(health): add ip address to error messages for small checks 2025-11-20 20:03:02 +00:00
Quentin McGaw a445ba072c chore(health): log out duration of tries in milliseconds 2025-11-20 15:14:00 +00:00
Quentin McGaw 9e5624d32b feat(healthcheck): add HEALTH_SMALL_CHECK_TYPE option which can be dns or icmp (default)
Note if icmp is not permitted, it fallsback to dns anyway
2025-11-20 15:13:50 +00:00
Quentin McGaw 815fcdb711 chore(healthcheck/icmp): log what IP is being pinged when timing out 2025-11-20 14:46:31 +00:00
Quentin McGaw 0bb9f62755 hotfix(ci): require verify-private to pass for publish job to trigger 2025-11-19 19:58:58 +00:00
Quentin McGaw 93567a7804 hotfix(ci): bump container run timeout from 15s to 30s 2025-11-19 19:57:57 +00:00
Quentin McGaw 0afbb71634 feat(main): add ascii logo at exit 2025-11-19 18:08:10 +00:00
Quentin McGaw 9f39d47150 feat(healthcheck): HEALTH_ICMP_TARGET_IP -> HEALTH_ICMP_TARGET_IPS
- Specify fallback ICMP IP addresses
- Defaults changed from 1.1.1.1 to 1.1.1.1,8.8.8.8
- Small periodic check cycles through addresses as it fails and moves to retry
2025-11-19 16:03:09 +00:00
Quentin McGaw f9490656eb chore(healthcheck): mirror default icmp ip set in Dockerfile in the Go code 2025-11-19 16:02:19 +00:00
Quentin McGaw 482421dda3 feat(healthcheck): HEALTH_TARGET_ADDRESS -> HEALTH_TARGET_ADDRESSES
- Specify fallback addresses
- Defaults changed from cloudflare:443 to cloudflare:443,github.com:443
- Startup check runs a parallel dial to each of the addresses specified with a global 6s timeout
- Full periodic check cycles through addresses as it fails and moves to retry
2025-11-19 16:01:50 +00:00
Quentin McGaw 03f1fea123 hotfix(healthcheck): update outdated function comment 2025-11-19 15:34:22 +00:00
Quentin McGaw 31284542a2 fix(wireguard): WIREGUARD_ENDPOINT_IP overrides the IP address of a picked connection
- Regression introduced in v3.39.0
- Fix #2759
2025-11-19 13:11:43 +00:00
Quentin McGaw 5ff5fc4a5e chore(ci): run protonvpn config container 2025-11-18 22:08:25 +00:00
Quentin McGaw 5b93464fef fix(proton): giving proton password is not mandatory 2025-11-18 21:36:38 +00:00
Quentin McGaw debf3474e7 hotfix(protonvpn): fix retro-compatibility with UPDATER_PROTONVPN_USERNAME 2025-11-18 13:40:55 +00:00
Quentin McGaw 2853ca9033 feat(protonvpn): update servers data including paid data 2025-11-18 13:36:05 +00:00
Quentin McGaw 74d059dd77 fix(protonvpn/updater): API authentification fix using email
- `UPDATER_PROTONVPN_USERNAME` ->  `UPDATER_PROTONVPN_EMAIL`
- `-proton-username` -> `-proton-email`
- fix authentication flow to use email or username when appropriate
- fix #2985
2025-11-18 13:35:04 +00:00
Quentin McGaw 9963e18a8a fix(storage): do not write updated servers to file if file path is empty 2025-11-18 13:28:03 +00:00
Quentin McGaw 41cd8fb30d fix(storage): only log warning if flushing merged servers to file fails 2025-11-17 19:04:19 +00:00
dependabot[bot] 9ed6cd978d Chore(deps): Bump DavidAnson/markdownlint-cli2-action from 20 to 21 (#2984) 2025-11-17 19:57:57 +01:00
Quentin McGaw c4b9d459ed fix(dns): fix panic when using DNS_KEEP_NAMESERVER 2025-11-17 17:59:18 +00:00
Quentin McGaw 6e99ca573e chore(storage): do not read/write to user file when updating in maintainer mode 2025-11-17 15:31:40 +00:00
Quentin McGaw 2cf4d6b469 fix(protonvpn/updater): ignore casing when comparing received username 2025-11-17 15:23:02 +00:00
Quentin McGaw a17776673b docs(readme): warning on "official" websites 2025-11-17 12:46:45 +00:00
Quentin McGaw fcdba0a3cc feat(portforward): support {{PORT}} template variable 2025-11-16 00:18:01 +00:00
Quentin McGaw 4712d0cf79 change(healthcheck): bump tries and timeouts
- small periodic check from 10s+20s+30s to 5s+5s+5s+10s+10s+10s+15s+15s+15s+30s
- full periodic check from 10s+20s to 10s+15s+30s
2025-11-15 16:47:38 +00:00
Quentin McGaw 113c113615 feat(healthcheck): log duration for each failed attempt 2025-11-15 16:45:03 +00:00
Quentin McGaw 6023eb1878 hotfix(dns): compilation error due to dns package upgrade on master 2025-11-14 21:24:40 +00:00
Quentin McGaw a1ece20617 feat(dns): resolve network-local names (#2970) 2025-11-14 17:30:05 +01:00
Quentin McGaw 0bc67b73a8 feat(dns): info log all requests filtered out 2025-11-14 16:19:07 +00:00
Quentin McGaw c7ab5bd34c feat(dns): DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES option 2025-11-14 16:14:46 +00:00
Quentin McGaw 843bf08aa1 chore(deps): bump dns to https://github.com/qdm12/dns/commit/248acd28339f329b0f51c7da987f8f7c72e59e45 2025-11-14 16:14:46 +00:00
Quentin McGaw 5b25cc95a9 chore(docker): clear DNS_BLOCK_IP_PREFIXES values since DNS rebinding protection is built-in the filter middleware 2025-11-14 16:14:46 +00:00
dependabot[bot] 0fddbc54a2 Chore(deps): Bump github.com/cloudflare/circl from 1.6.0 to 1.6.1 (#2977) 2025-11-13 23:27:51 +01:00
dependabot[bot] 11fcfb7d19 Chore(deps): Bump golang.org/x/net from 0.46.0 to 0.47.0 (#2976) 2025-11-13 23:27:10 +01:00
dependabot[bot] 3cd7d7edcb Chore(deps): Bump golang.org/x/text from 0.30.0 to 0.31.0 (#2975) 2025-11-13 23:26:55 +01:00
Quentin McGaw 30609b6fe9 hotfix(configuration/settings): fix requirement for proton username and password 2025-11-13 21:58:46 +00:00
Quentin McGaw 8a0921748b fix(protonvpn): authenticated servers data updating (#2878)
- `-proton-username` flag for cli update
- `-proton-password` flag for cli update
- `UPDATER_PROTONVPN_USERNAME` option for periodic updates
- `UPDATER_PROTONVPN_PASSWORD` option for periodic updates
2025-11-13 20:05:26 +01:00
Quentin McGaw 3fac02a82a feat(server/auth): HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE option (JSON encoded)
- For example: `{"auth":"basic","username":"me","password":"pass"}`
- For example`{"auth":"apiKey","apikey":"xyz"}`
- For example`{"auth":"none"}` (I don't recommend)
2025-11-13 18:24:41 +00:00
Quentin McGaw f11f142bee feat(settings/wireguard): precise WIREGUARD_ENDPOINT_IP must be an IP address for now 2025-11-13 18:24:41 +00:00
dependabot[bot] 596faef8f2 Chore(deps): Bump golang.org/x/sys from 0.37.0 to 0.38.0 (#2973) 2025-11-13 16:47:26 +01:00
Quentin McGaw 3d1b6bc861 feat(server/portforward): change route from /v1/openvpn/portforwarded to /v1/portforward
- This route has nothing to do with openvpn specifically
- Remove the `ed` in `portforwarded` to accomodate future routes such as changing the state of port forwarding
- maintaining retrocompatibility with `/v1/openvpn/portforwarded`
- maintaining retrocompatibility with `/openvpn/portforwarded`
- Moved to its own handler `/v1/portforward` instead of `/v1/vpn/portforward` to reduce the complexity of the vpn handler
2025-11-13 14:50:36 +00:00
Quentin McGaw 46ad576233 fix(server/log): log out full URL path not just bottom request URI 2025-11-13 14:29:58 +00:00
Quentin McGaw 46beaac34b hotfix(server/auth): add old route /openvpn/portforwarded as valid 2025-11-13 14:21:50 +00:00
Quentin McGaw 3025476e8b chore(portforward): remove double log when clearing port forward file 2025-11-13 14:10:13 +00:00
Quentin McGaw cd6f9493a4 docs(Dockerfile): specify default PUID and PGID to avoid confusion
- Both of these already defaulted to 1000 in the Go code
2025-11-13 13:06:21 +00:00
Quentin McGaw 9984ad22d7 chore(settings/health): remove unneeded health fields 2025-11-13 12:27:33 +00:00
Quentin McGaw 3565ba67c4 hotfix(healthcheck/dns): use dns address tring with port 2025-11-12 01:45:10 +00:00
Quentin McGaw ffb0bec4da chore(vpn): rename openvpn* to vpn* variables 2025-11-07 15:26:24 +00:00
Quentin McGaw 4d2b8787e0 chore(dns): replace UNBLOCK with DNS_UNBLOCK_HOSTNAMES 2025-11-07 14:36:10 +00:00
Quentin McGaw d4831ad4a6 chore(dns): replace DOT_PRIVATE_ADDRESS with DNS_BLOCK_IPS and DNS_BLOCK_IP_PREFIXES 2025-11-07 14:31:09 +00:00
Quentin McGaw 9e1b53a732 feat(server): log number of roles read from auth file 2025-11-05 23:05:10 +00:00
Quentin McGaw d0113849d6 feat(dns): support doh upstream type 2025-11-05 21:21:16 +00:00
Quentin McGaw 7b25fdfee8 chore(deps): bump dns to v2.0.0-rc9 2025-11-05 20:56:37 +00:00
Quentin McGaw 5ed6e82922 feat(dns): DNS_UPSTREAM_RESOLVER_TYPE option which can be plain or DoT
- Migrate `DOT` to `DNS_SERVER`
- Migrate `DOT_PROVIDERS` to `DNS_UPSTREAM_RESOLVERS`
- Migrate `DOT_PRIVATE_ADDRESS` to `DNS_PRIVATE_ADDRESSES`
- Migrate `DOT_CACHING` to `DNS_CACHING`
- Migrate `DOT_IPV6` to `DNS_UPSTREAM_IPV6`
2025-11-05 20:47:21 +00:00
Quentin McGaw 7dbd14df27 chore(dns): merge DoT settings with DNS settings 2025-11-05 20:47:21 +00:00
dependabot[bot] 96d8b53338 Chore(deps): Bump github.com/breml/rootcerts from 0.3.2 to 0.3.3 (#2964) 2025-11-04 20:34:22 -05:00
Quentin McGaw 2bd19640d9 feat(health/dns): try another DNS server if one fails 2025-11-04 15:51:04 +00:00
Quentin McGaw 1047508bd7 docs(github): update provider issue template 2025-11-04 15:07:16 +00:00
Quentin McGaw eb49306b80 hotfix(health): change default icmp target to 1.1.1.1
- Cloudflare's 1.1.1.1 seems more reliable than the VPN server public IP address you connect to
- This can still be changed back to 0.0.0.0 to use the VPN server IP address if needed
2025-11-04 14:47:24 +00:00
Quentin McGaw 43da9ddbb3 fix(cyberghost): log warnings from updater resolver 2025-11-04 14:43:02 +00:00
Quentin McGaw 7fbc5c3c07 feat(cyberghost): update servers data 2025-11-04 14:43:02 +00:00
dependabot[bot] e03f545e07 Chore(deps): Bump github.com/stretchr/testify from 1.10.0 to 1.11.1 (#2959) 2025-11-04 15:33:12 +01:00
dependabot[bot] 942f1f2c0f Chore(deps): Bump github.com/pelletier/go-toml/v2 from 2.2.3 to 2.2.4 (#2958) 2025-11-04 15:33:00 +01:00
dependabot[bot] baf566d7a5 Chore(deps): Bump github.com/klauspost/compress from 1.17.11 to 1.18.1 (#2957) 2025-11-04 15:32:46 +01:00
Quentin McGaw 6712adfe6b hotfix(firewall): handle textual values for protocols
- Alpine / iptables-legacy bug introduced in Alpine 3.22
- Alpine: what the hell? Stop introducing breaking changes in iptables on every god damn release!
2025-11-04 14:16:11 +00:00
Quentin McGaw 2e2e5f9df5 fix(firewall): parse "all" protocol from iptables chains 2025-11-03 16:09:24 +00:00
Quentin McGaw 35e9b2365d fix(ci): consider 429 as valid status code for markdown links 2025-11-03 16:00:42 +00:00
Quentin McGaw b0b769d2c1 ci(markdown): fix config file path 2025-10-31 20:02:55 +00:00
Quentin McGaw d3c7d3c7bc docs(readme): update Alpine version and image size 2025-10-30 16:15:44 +00:00
Quentin McGaw 65f49ea012 fix(wireguard): specify IP family for new route (#2629) 2025-10-30 17:14:45 +01:00
Quentin McGaw 5687555921 chore(container): bump Alpine from 3.20 to 3.22 2025-10-30 16:08:40 +00:00
Quentin McGaw 0fb75036a0 chore(build): bump Go from 1.24 to 1.25 2025-10-30 16:04:10 +00:00
dependabot[bot] 2b513dd43d Chore(deps): Bump github.com/vishvananda/netlink from 1.2.1 to 1.3.1 (#2932) 2025-10-30 17:02:32 +01:00
Quentin McGaw 687d9b4736 hotfix(tests): fix unit test for healthcheck 2025-10-30 16:01:25 +00:00
dependabot[bot] c70c2ef932 Chore(deps): Bump golang.org/x/net from 0.34.0 to 0.46.0 (#2937) 2025-10-30 17:00:30 +01:00
dependabot[bot] af3ada109b Chore(deps): Bump actions/setup-go from 5 to 6 (#2929) 2025-10-30 17:00:15 +01:00
Quentin McGaw 9d40564734 chore(deps): bump breml/rootcerts from v0.2.20 to v0.3.2 2025-10-30 15:59:20 +00:00
Quentin McGaw 3734815ada hotfix(health): debug log failed attempts and warn log all attempt errors if all failed
- Reduce "worrying" noise of icmp attempt failing
- Only log when an action (restart the VPN) is taken
2025-10-30 15:57:40 +00:00
Quentin McGaw b9cc5c1fdc fix(port-forward): clear port file instead of removing it
- Prevent port forwarding loop crash when trying to delete a directly bind mounted file
- See https://github.com/qdm12/gluetun/issues/2942#issuecomment-3468510402
2025-10-30 15:45:01 +00:00
dependabot[bot] c646ca5766 Chore(deps): Bump peter-evans/create-or-update-comment from 4 to 5 (#2931) 2025-10-30 03:45:31 +01:00
dependabot[bot] 1394be5143 Chore(deps): Bump golang.org/x/sys from 0.29.0 to 0.37.0 (#2939) 2025-10-30 03:45:16 +01:00
Quentin McGaw 93442526f8 chore(ci): run container and wait for it to connect (#2956)
- Added safety to prevent panics/errors when skipping CI checks (shame on me, sometimes)
- Opens new possibilities for end to end integration tests. PRs accepted!
2025-10-30 03:44:31 +01:00
dependabot[bot] d85402050b Chore(deps): Bump github.com/ulikunitz/xz from 0.5.11 to 0.5.15 (#2955) 2025-10-30 01:57:18 +01:00
dependabot[bot] b1c62cb525 Chore(deps): Bump golang.org/x/text from 0.21.0 to 0.30.0 (#2938) 2025-10-30 01:56:53 +01:00
dependabot[bot] fae64a297a Chore(deps): Bump github/codeql-action from 3 to 4 (#2935) 2025-10-30 01:56:41 +01:00
Quentin McGaw 6e2682a9ce docs(readme): remove no longer valid LoC badge 2025-10-30 00:55:39 +00:00
Quentin McGaw 555049f09c feat(privado): update servers data 2025-10-29 12:30:48 +00:00
Quentin McGaw 712f7c3d35 chore(build): bump Go from 1.23 to 1.24 2025-10-29 02:34:22 +00:00
Quentin McGaw 7a51c211cd fix(publicip): respect PUBLICIP_ENABLED 2025-10-23 19:49:21 +00:00
Quentin McGaw c48189c1c4 feat(health/icmp): log out return address on errors 2025-10-23 19:22:31 +00:00
Quentin McGaw 9803fa1cfd hotfix(health): info log on healthcheck passing after failure 2025-10-23 18:58:19 +00:00
Quentin McGaw cf756f561a feat(health): info log when healthcheck passes after failure for the case of HEALTH_VPN_RESTART=off 2025-10-21 18:42:33 +00:00
Quentin McGaw a4021fedc3 feat(health): HEALTH_RESTART_VPN option
- You should really leave it to `on` ⚠️
- Turn it to `off` if you have trust issues with the healthcheck. Don't then report issues if the connection is dead though.
2025-10-21 15:36:15 +00:00
Quentin McGaw 31a36a9250 hotfix(health): increase timeout values and periods
- run small check every 60s, from 15s
- small check (icmp/dns) initial timeout from 3s to 10s
- small check (icmp/dns) timeout increase from 1s to 10s
- full check initial timeout increased from 10s to 20s
- full check extra timeout increase from 3s to 10s
2025-10-19 23:27:02 +00:00
Quentin McGaw 36fe349b70 chore(ci): ignore .github/pull_request_template.md with markdown linter 2025-10-19 23:23:41 +00:00
shwoop 3ef1cfd97c docs(github): add pull request template (#2918) 2025-10-17 20:34:05 +02:00
Quentin McGaw 669feb45f1 hotfix(healthcheck): correct error string for DNS plain lookup fallback 2025-10-17 18:08:24 +00:00
Quentin McGaw 85890520ab feat(healthcheck): combination of ICMP and TCP+TLS checks (#2923)
- New option: `HEALTH_ICMP_TARGET_IP` defaults to `0.0.0.0` meaning use the VPN server public IP address.
- Options removed: `HEALTH_VPN_INITIAL_DURATION` and `HEALTH_VPN_ADDITIONAL_DURATION` - times and retries are handpicked and hardcoded.
- Less aggressive checks and less false positive detection
2025-10-17 01:45:50 +02:00
dependabot[bot] 340016521e Chore(deps): Bump github.com/breml/rootcerts from 0.2.19 to 0.2.20 (#2683) 2025-10-06 13:36:00 +02:00
Matthew Bennett ef523df42c feat(expressvpn): update hardcoded servers data (#2888) 2025-10-06 13:33:36 +02:00
Quentin McGaw 5306e3bab1 feat(mullvad): update servers data 2025-10-03 15:25:12 +00:00
Vahin M 72a49afd2b docs(healthcheck): fix grammar issue in log (#2773) 2025-09-26 18:58:08 +02:00
Quentin McGaw 9b8edbb81e hotfix(vpnunlimited): fix formatting of certificates 2025-09-24 12:55:45 +00:00
Quentin McGaw a1554feb3f chore(dev): add vscode git remote add task 2025-09-24 12:54:16 +00:00
Quentin McGaw 490410bf09 chore(dev): convert .vscode/launch.json to tasks.json 2025-09-24 12:54:16 +00:00
mutschler 8c113f5268 fix(vpnunlimited): update certificate values (#2835) 2025-09-11 21:15:20 +02:00
shwoop 075cbd5a0f chore(ci): bump github actions and use go.mod Go version (#2880)
- actions/checkout from v4 to v5
- actions/setup-go uses go-version from go.mod file
- DavidAnson/markdownlint-cli2-action from v19 to v20
2025-09-11 21:14:19 +02:00
Quentin McGaw d82df2b431 hotfix(build): bump xcputranslate so it's available on ghcr.io
- v0.7.0 is a broken build
- v0.9.0 is the version available on ghcr.io
2025-08-16 20:34:07 +00:00
Quentin McGaw a09f8214d9 hotfix(build): bump xcputranslate so it's available on ghcr.io 2025-08-16 20:29:40 +00:00
Quentin McGaw 396e9c003e chore(ci): pull container images at build time from ghcr.io when possible
- Reduce silly image pull rate limiting from docker hub registry
- still rely on docker hub registry to pull golang and alpine images since these are not on ghcr.io
2025-08-16 20:12:21 +00:00
Quentin McGaw b0c4a28be6 chore(lint): upgrade linter to v2.4.0
- migrate configuration file
- fix existing code issues
- add exclusion rules
- update linter names
2025-08-16 20:10:19 +00:00
Quentin McGaw 85325e4a31 chore(dev): upgrade dev container to v0.21
See [release notes](https://github.com/qdm12/godevcontainer/releases/tag/v0.21.0)

Notably:
- Go upgraded from 1.23 to 1.25
- golangci-lint upgraded to v2.4.0
- Alpine upgraded from 3.20 to 3.22
- Disable package comment requirement by gopls' staticcheck
- Pull container image from ghcr.io
2025-08-16 20:10:14 +00:00
dependabot[bot] 9933dd3ec5 Chore(deps): Bump DavidAnson/markdownlint-cli2-action from 18 to 19 (#2632) 2025-01-22 09:27:10 +01:00
dependabot[bot] 13532c8b4b Chore(deps): Bump golang.org/x/net from 0.31.0 to 0.34.0 (#2648) 2025-01-22 09:26:57 +01:00
Leroy 3926797295 docs(readme): remove docker-compose example version field (#2663) 2025-01-22 09:26:39 +01:00
K1 febd3f784f docs(readme): "swiss-knife-like" -> "swiss-army-knife-like" (#2652) 2025-01-22 09:25:46 +01:00
dependabot[bot] 61b053f0e1 Chore(deps): Bump golang.org/x/crypto from 0.29.0 to 0.31.0 (#2619) 2024-12-27 21:15:31 +01:00
Quentin McGaw 8dae352ccc fix(cli): fix openvpnconfig command panic due to missing SetDefaults call 2024-12-27 09:31:04 +00:00
Quentin McGaw e890c50da6 feat(firewall): support icmp rules 2024-12-25 20:05:55 +00:00
Quentin McGaw ddd9f4d021 chore(natpmp): fix determinism for test Test_Client_ExternalAddress 2024-12-14 21:04:07 +00:00
dependabot[bot] 7e58b4baee Chore(deps): Bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#2600) 2024-12-14 21:19:30 +01:00
dependabot[bot] a21fbb9a4f Chore(deps): Bump github.com/breml/rootcerts from 0.2.18 to 0.2.19 (#2601) 2024-12-14 21:19:11 +01:00
Quentin McGaw 3b7d27c919 hotfix(ci): use --device /dev/net/tun for test container 2024-12-14 20:15:42 +00:00
dependabot[bot] 68ddbfc0fe Chore(deps): Bump golang.org/x/net from 0.30.0 to 0.31.0 (#2578) 2024-11-18 10:46:04 +01:00
dependabot[bot] a2047cb800 Chore(deps): Bump DavidAnson/markdownlint-cli2-action from 16 to 18 (#2588) 2024-11-18 10:45:49 +01:00
Quentin McGaw fdd499146c fix(wireguard): point to Kubernetes wiki page when encountering IP rule add file exists error (#2526) 2024-11-15 18:47:06 +01:00
Quentin McGaw 37900341cf hotfix(firewall): fix unit test for previous PR 2024-11-15 17:46:10 +00:00
Jean-François Roy 36bb368cad fix(firewall): iptables list uses -n flag for testing iptables path (#2574)
Signed-off-by: Jean-Francois Roy <jf@devklog.net>
2024-11-15 16:47:08 +01:00
Quentin McGaw f9bdb219d0 chore(deps): update gosettings to v0.4.4
- Better support for quote expressions especially for commands such as VPN_PORT_FORWARDING_UP_COMMAND
2024-11-12 09:11:48 +00:00
Quentin McGaw 0374c14e42 feat(portforwarding): VPN_PORT_FORWARDING_DOWN_COMMAND option 2024-11-10 10:18:29 +00:00
Alex Lavallee a035a151bd feat(portforwarding): allow running script upon port forwarding success (#2399) 2024-11-10 09:49:02 +01:00
Quentin McGaw e69966381d feat(fastestvpn): add aes-256-gcm to ciphers list 2024-11-09 15:44:05 +00:00
Quentin McGaw 94dfb2b1f2 fix(ipvanish): fix openvpn configuration
- update CA value
- add `comp-lzo` option
2024-11-09 15:43:51 +00:00
Quentin McGaw 92011205be feat(publicip): support custom API url echoip#https://... (#2529) 2024-11-08 17:37:08 +01:00
dependabot[bot] c9707646bd Chore(deps): Bump golang.org/x/sys from 0.26.0 to 0.27.0 (#2573) 2024-11-08 17:33:30 +01:00
dependabot[bot] c50705736b Chore(deps): Bump github.com/pelletier/go-toml/v2 from 2.2.2 to 2.2.3 (#2549) 2024-11-08 17:33:18 +01:00
dependabot[bot] ec284c17f4 Chore(deps): Bump github.com/klauspost/compress from 1.17.9 to 1.17.11 (#2550) 2024-11-07 12:28:04 -08:00
Quentin McGaw ad6c52dc4c feat(ipvanish): update servers data 2024-11-07 20:21:12 +00:00
Quentin McGaw 5f182febae fix(ipvanish): update openvpn zip file url for updater 2024-11-07 20:21:10 +00:00
Quentin McGaw 86d82c1098 chore(main): let system handle OS signals after first one to stop program 2024-11-07 20:19:24 +00:00
Quentin McGaw 842b9004da chore(routing): remove redundant rule ip rule in error messages 2024-11-07 20:19:24 +00:00
Quentin McGaw 6ac7ca4f0f feat(healthcheck): log out last error when auto healing VPN 2024-11-05 13:35:58 +00:00
Quentin McGaw ddfcbe1bee feat(healthcheck): run TLS handshake after TCP dial if address has 443 port 2024-11-05 13:35:58 +00:00
Quentin McGaw 88fd9388e4 chore(lint): remove canonicalheader since it's not reliable 2024-11-05 13:35:58 +00:00
Quentin McGaw 69aafa53c9 fix(server/auth): fix wiki link to authentication section 2024-11-05 13:35:58 +00:00
Quentin McGaw 3473fe9c15 fix(openvpn): set default mssfix to 1320 for all providers with no default
- Partially address #2533
2024-11-05 13:35:54 +00:00
Quentin McGaw c655500045 fix(wireguard): change default WIREGUARD_MTU from 1400 to 1320
- Partially address #2533
2024-11-05 09:57:03 +00:00
Quentin McGaw 96a8015af6 feat(netlink): debug rule logs contain the ip family 2024-11-03 20:14:41 +00:00
Quentin McGaw ddd3876f92 chore(dns): upgrade dependency from v2.0.0-rc7 to v2.0.0-rc8
- do not log dial error twice
- DNS subserver shuts down without waiting for connections to finish (UDP server would hang sometimes)
- DNS over TLS dialer uses tls.Dialer instead of wrapping connection with tls.Client
- connection type is just `tls` instead of `dns over tls` to reduce repetition in logs
- exchange errors contain the request question in their context
2024-11-03 12:35:01 +00:00
Quentin McGaw f1f34722ee feat(tun): mention in 'operation not permitted' error the user should specify --device /dev/net/tun 2024-10-28 09:22:08 +00:00
278 changed files with 41012 additions and 34240 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
FROM qmcgaw/godevcontainer:v0.20-alpine
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
RUN apk add wireguard-tools htop openssl
+3 -3
View File
@@ -19,16 +19,16 @@ It works on Linux, Windows (WSL2) and OSX.
mkdir -p ~/.ssh
```
1. **For Docker on OSX**: ensure the project directory and your home directory `~` are accessible by Docker.
1. **For OSX hosts**: ensure the project directory and your home directory `~` are accessible by Docker.
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
1. Select `Dev Containers: Open Folder in Container...` and choose the project directory.
1. Select `Dev-Containers: Open Folder in Container...` and choose the project directory.
## Customization
For any customization to take effect, you should "rebuild and reopen":
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
2. Select `Dev Containers: Rebuild Container`
2. Select `Dev-Containers: Rebuild Container`
Changes you can make are notably:
+3
View File
@@ -82,6 +82,9 @@
"gopls": {
"usePlaceholders": false,
"staticcheck": true,
"ui.diagnostic.analyses": {
"ST1000": false
},
"formatting.gofumpt": true,
},
"go.lintTool": "golangci-lint",
-1
View File
@@ -67,7 +67,6 @@ body:
- VPNSecure.me
- VPNUnlimited
- VyprVPN
- WeVPN
- Windscribe
validations:
required: true
+30 -7
View File
@@ -6,12 +6,35 @@ labels: ":bulb: New provider"
---
One of the following is required:
Important notes:
- Publicly accessible URL to a zip file containing the Openvpn configuration files
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately
- If it's not possible to support a provider natively, you can still use the [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
## For Wireguard
Wireguard can be natively supported ONLY if:
- the `PrivateKey` field value is the same across all servers for one user account
- the `Address` field value is:
- can be found in a structured (JSON etc.) list of servers publicly available; OR
- the same across all servers for one user account
- the `PublicKey` field value is:
- can be found in a structured (JSON etc.) list of servers publicly available; OR
- the same across all servers for one user account
- the `Endpoint` field value:
- can be found in a structured (JSON etc.) list of servers publicly available
- can be determined using a pattern, for example using country codes in hostnames
If any of these conditions are not met, Wireguard cannot be natively supported or there is no advantage compared to using a custom Wireguard configuration file.
If **all** of these conditions are met, please provide an answer for each of them.
## For OpenVPN
OpenVPN can be natively supported ONLY if one of the following can be provided, by preference in this order:
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP; OR
- Publicly accessible URL to a zip file containing the Openvpn configuration files; OR
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
If the list of servers requires to login **or** is hidden behind an interactive configurator,
you can only use a custom Openvpn configuration file.
[The Wiki's OpenVPN configuration file page](https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md) describes how to do so.
-2
View File
@@ -86,8 +86,6 @@
color: "cfe8d4"
- name: "☁️ Vyprvpn"
color: "cfe8d4"
- name: "☁️ WeVPN"
color: "cfe8d4"
- name: "☁️ Windscribe"
color: "cfe8d4"
+12
View File
@@ -0,0 +1,12 @@
# Description
<!-- Please describe the reason for the changes being proposed. -->
# Issue
<!-- Please link to the issue(s) this change relates to. -->
# Assertions
* [ ] I am aware that we do not accept manual changes to the servers.json file <!-- If this is your goal, please consult https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-using-the-command-line -->
* [ ] I am aware that any changes to settings should be reflected in the [wiki](https://github.com/qdm12/gluetun-wiki/)
+43 -10
View File
@@ -37,7 +37,7 @@ jobs:
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: reviewdog/action-misspell@v1
with:
@@ -59,13 +59,46 @@ jobs:
- name: Run tests in test container
run: |
touch coverage.txt
docker run --rm \
docker run --rm --cap-add=NET_ADMIN --device /dev/net/tun \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container
- name: Verify dev cross platform compatibility
run: docker build --target xcompile .
- name: Build final image
run: docker build -t final-image .
verify-private:
if: |
github.repository == 'qdm12/gluetun' &&
(
github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
)
needs: [verify]
runs-on: ubuntu-latest
environment: secrets
steps:
- uses: actions/checkout@v6
- run: docker build -t qmcgaw/gluetun .
- name: Setup Go for CI utility
uses: actions/setup-go@v6
with:
go-version-file: ci/go.mod
- name: Build utility
run: go build -C ./ci -o runner ./cmd/main.go
- name: Run Gluetun container with Mullvad configuration
run: echo -e "${{ secrets.MULLVAD_WIREGUARD_PRIVATE_KEY }}\n${{ secrets.MULLVAD_WIREGUARD_ADDRESS }}" | ./ci/runner mullvad
- name: Run Gluetun container with ProtonVPN configuration
run: echo -e "${{ secrets.PROTONVPN_WIREGUARD_PRIVATE_KEY }}" | ./ci/runner protonvpn
codeql:
runs-on: ubuntu-latest
permissions:
@@ -73,15 +106,15 @@ jobs:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: "^1.23"
- uses: github/codeql-action/init@v3
go-version-file: go.mod
- uses: github/codeql-action/init@v4
with:
languages: go
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
- uses: github/codeql-action/autobuild@v4
- uses: github/codeql-action/analyze@v4
publish:
if: |
@@ -91,14 +124,14 @@ jobs:
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
)
needs: [verify, codeql]
needs: [verify, verify-private, codeql]
permissions:
actions: read
contents: read
packages: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
# extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
issues: write
runs-on: ubuntu-latest
steps:
- uses: peter-evans/create-or-update-comment@v4
- uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ github.token }}
issue-number: ${{ github.event.issue.number }}
+2 -1
View File
@@ -8,6 +8,7 @@
"retryOn429": false,
"fallbackRetryDelay": "30s",
"aliveStatusCodes": [
200
200,
429
]
}
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
issues: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: crazy-max/ghaction-github-labeler@v5
with:
yaml-file: .github/labels.yml
+3 -3
View File
@@ -18,12 +18,12 @@ jobs:
actions: read
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: DavidAnson/markdownlint-cli2-action@v16
- uses: DavidAnson/markdownlint-cli2-action@v21
with:
globs: "**.md"
config: .markdownlint.json
config: .markdownlint-cli2.jsonc
- uses: reviewdog/action-misspell@v1
with:
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
issues: write
runs-on: ubuntu-latest
steps:
- uses: peter-evans/create-or-update-comment@v4
- uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ github.token }}
issue-number: ${{ github.event.issue.number }}
+67 -26
View File
@@ -1,27 +1,73 @@
linters-settings:
misspell:
locale: US
version: "2"
issues:
exclude-rules:
- path: _test\.go
linters:
- dupl
- err113
- containedctx
- maintidx
- path: "internal\\/server\\/.+\\.go"
linters:
- dupl
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
linters:
- ireturn
- path: "internal\\/openvpn\\/pkcs8\\/descbc\\.go"
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
linters:
- ireturn
formatters:
enable:
- gci
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
linters:
settings:
misspell:
locale: US
goconst:
ignore-string-values:
# commonly used settings strings
- "^disabled$"
# Firewall and routing strings
- "^(ACCEPT|DROP)$"
- "^--delete$"
- "^all$"
- "^(tcp|udp)$"
# Server route strings
- "^/status$"
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- containedctx
- dupl
- err113
- maintidx
path: _test\.go
- linters:
- dupl
path: internal\/server\/.+\.go
- linters:
- ireturn
text: returns interface \(github\.com\/vishvananda\/netlink\.Link\)
- linters:
- ireturn
path: internal\/openvpn\/pkcs8\/descbc\.go
text: newCipherDESCBCBlock returns interface \(github\.com\/youmark\/pkcs8\.Cipher\)
- linters:
- revive
path: internal\/provider\/(common|utils)\/.+\.go
text: "var-naming: avoid (bad|meaningless) package names"
- linters:
- lll
source: "^// https://.+$"
- linters:
- err113
- mnd
path: ci\/.+\.go
paths:
- third_party$
- builtin$
- examples$
enable:
# - cyclop
# - errorlint
@@ -29,7 +75,6 @@ linters:
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- copyloopvar
- decorder
@@ -43,7 +88,6 @@ linters:
- exhaustive
- fatcontext
- forcetypeassert
- gci
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
@@ -52,9 +96,7 @@ linters:
- gocritic
- gocyclo
- godot
- gofumpt
- goheader
- goimports
- gomoddirectives
- goprintffuncname
- gosec
@@ -87,7 +129,6 @@ linters:
- rowserrcheck
- sqlclosecheck
- tagalign
- tenv
- thelper
- tparallel
- unconvert
+9
View File
@@ -0,0 +1,9 @@
{
"config": {
"default": true,
"MD013": false,
},
"ignores": [
".github/pull_request_template.md"
]
}
-3
View File
@@ -1,3 +0,0 @@
{
"MD013": false
}
-35
View File
@@ -1,35 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Update a VPN provider servers data",
"type": "go",
"request": "launch",
"cwd": "${workspaceFolder}",
"program": "cmd/gluetun/main.go",
"args": [
"update",
"${input:updateMode}",
"-providers",
"${input:provider}"
],
}
],
"inputs": [
{
"id": "provider",
"type": "promptString",
"description": "Please enter a provider (or comma separated list of providers)",
},
{
"id": "updateMode",
"type": "pickString",
"description": "Update mode to use",
"options": [
"-maintainer",
"-enduser"
],
"default": "-maintainer"
},
]
}
+51
View File
@@ -0,0 +1,51 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Update a VPN provider servers data",
"type": "shell",
"command": "go",
"args": [
"run",
"./cmd/gluetun/main.go",
"update",
"${input:updateMode}",
"-providers",
"${input:provider}"
],
},
{
"label": "Add a Gluetun Github Git remote",
"type": "shell",
"command": "git",
"args": [
"remote",
"add",
"${input:githubRemoteUsername}",
"git@github.com:${input:githubRemoteUsername}/gluetun.git"
],
}
],
"inputs": [
{
"id": "provider",
"type": "promptString",
"description": "Please enter a provider (or comma separated list of providers)",
},
{
"id": "updateMode",
"type": "pickString",
"description": "Update mode to use",
"options": [
"-maintainer",
"-enduser"
],
"default": "-maintainer"
},
{
"id": "githubRemoteUsername",
"type": "promptString",
"description": "Please enter a Github username",
},
]
}
+35 -23
View File
@@ -1,14 +1,14 @@
ARG ALPINE_VERSION=3.20
ARG GO_ALPINE_VERSION=3.20
ARG GO_VERSION=1.23
ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.61.0
ARG ALPINE_VERSION=3.22
ARG GO_ALPINE_VERSION=3.22
ARG GO_VERSION=1.25
ARG XCPUTRANSLATE_VERSION=v0.9.0
ARG GOLANGCI_LINT_VERSION=v2.4.0
ARG MOCKGEN_VERSION=v1.6.0
ARG BUILDPLATFORM=linux/amd64
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
@@ -32,7 +32,7 @@ ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=a
FROM --platform=${BUILDPLATFORM} base AS lint
COPY .golangci.yml ./
RUN golangci-lint run --timeout=10m
RUN golangci-lint run
FROM --platform=${BUILDPLATFORM} base AS mocks
RUN git init && \
@@ -46,6 +46,10 @@ RUN git init && \
git diff --exit-code && \
rm -rf .git/
FROM --platform=${BUILDPLATFORM} base AS xcompile
RUN GOOS=darwin go build -o /dev/null ./...
RUN GOOS=windows go build -o /dev/null ./...
FROM --platform=${BUILDPLATFORM} base AS build
ARG TARGETPLATFORM
ARG VERSION=unknown
@@ -106,7 +110,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
WIREGUARD_ADDRESSES= \
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
WIREGUARD_MTU=1400 \
WIREGUARD_MTU=1320 \
WIREGUARD_IMPLEMENTATION=auto \
# VPN server filtering
SERVER_REGIONS= \
@@ -125,6 +129,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
VPN_PORT_FORWARDING_USERNAME= \
VPN_PORT_FORWARDING_PASSWORD= \
VPN_PORT_FORWARDING_UP_COMMAND= \
VPN_PORT_FORWARDING_DOWN_COMMAND= \
# # Cyberghost only:
OPENVPN_CERT= \
OPENVPN_KEY= \
@@ -161,20 +167,23 @@ ENV VPN_SERVICE_PROVIDER=pia \
LOG_LEVEL=info \
# Health
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
HEALTH_SUCCESS_WAIT_DURATION=5s \
HEALTH_VPN_DURATION_INITIAL=6s \
HEALTH_VPN_DURATION_ADDITION=5s \
# DNS over TLS
DOT=on \
DOT_PROVIDERS=cloudflare \
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
DOT_CACHING=on \
DOT_IPV6=off \
HEALTH_TARGET_ADDRESSES=cloudflare.com:443,github.com:443 \
HEALTH_ICMP_TARGET_IPS=1.1.1.1,8.8.8.8 \
HEALTH_SMALL_CHECK_TYPE=icmp \
HEALTH_RESTART_VPN=on \
# DNS
DNS_SERVER=on \
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
DNS_UPSTREAM_RESOLVERS=cloudflare \
DNS_BLOCK_IPS= \
DNS_BLOCK_IP_PREFIXES= \
DNS_CACHING=on \
DNS_UPSTREAM_IPV6=off \
BLOCK_MALICIOUS=on \
BLOCK_SURVEILLANCE=off \
BLOCK_ADS=off \
UNBLOCK= \
DNS_UNBLOCK_HOSTNAMES= \
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
DNS_UPDATE_PERIOD=24h \
DNS_ADDRESS=127.0.0.1 \
DNS_KEEP_NAMESERVER=off \
@@ -198,10 +207,13 @@ ENV VPN_SERVICE_PROVIDER=pia \
HTTP_CONTROL_SERVER_LOG=on \
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE="{}" \
# Server data updater
UPDATER_PERIOD=0 \
UPDATER_MIN_RATIO=0.8 \
UPDATER_VPN_SERVICE_PROVIDERS= \
UPDATER_PROTONVPN_EMAIL= \
UPDATER_PROTONVPN_PASSWORD= \
# Public IP
PUBLICIP_FILE="/tmp/gluetun/ip" \
PUBLICIP_ENABLED=on \
@@ -217,8 +229,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
# Extras
VERSION_INFORMATION=on \
TZ= \
PUID= \
PGID=
PUID=1000 \
PGID=1000
ENTRYPOINT ["/gluetun-entrypoint"]
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
+11 -6
View File
@@ -1,6 +1,8 @@
# Gluetun VPN client
Lightweight swiss-knife-like VPN client to multiple VPN service providers
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
@@ -26,7 +28,6 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
[![GitHub issues](https://img.shields.io/github/issues/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues)
[![GitHub closed issues](https://img.shields.io/github/issues-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
[![Lines of code](https://img.shields.io/tokei/lines/github/qdm12/gluetun)](https://github.com/qdm12/gluetun)
![Code size](https://img.shields.io/github/languages/code-size/qdm12/gluetun)
![GitHub repo size](https://img.shields.io/github/repo-size/qdm12/gluetun)
![Go version](https://img.shields.io/github/go-mod/go-version/qdm12/gluetun)
@@ -56,12 +57,12 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
## Features
- Based on Alpine 3.20 for a small Docker image of 35.6MB
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
- Based on Alpine 3.22 for a small Docker image of 41.1MB
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** servers
- Supports OpenVPN for all providers listed
- Supports Wireguard both kernelspace and userspace
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited**, **VyprVPN** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **VyprVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
- DNS over TLS baked in with service provider(s) of your choice
@@ -88,7 +89,7 @@ Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
Here's a docker-compose.yml for the laziest:
```yml
version: "3"
---
services:
gluetun:
image: qmcgaw/gluetun
@@ -124,6 +125,10 @@ services:
🆕 Image also available as `ghcr.io/qdm12/gluetun`
## Fun graphs
[![Star History Chart](https://api.star-history.com/svg?repos=qdm12/gluetun&type=date&legend=top-left)](https://www.star-history.com/#qdm12/gluetun&type=date&legend=top-left)
## License
[![MIT](https://img.shields.io/github/license/qdm12/gluetun)](https://github.com/qdm12/gluetun/blob/master/LICENSE)
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"github.com/qdm12/gluetun/ci/internal"
"github.com/qdm12/log"
)
func main() {
logger := log.New()
if len(os.Args) < 2 {
logger.Error("Usage: " + os.Args[0] + " <command>")
os.Exit(1)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
var err error
switch os.Args[1] {
case "mullvad":
err = internal.MullvadTest(ctx, logger)
case "protonvpn":
err = internal.ProtonVPNTest(ctx, logger)
default:
err = fmt.Errorf("unknown command: %s", os.Args[1])
}
stop()
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
logger.Info("test completed successfully")
}
+40
View File
@@ -0,0 +1,40 @@
module github.com/qdm12/gluetun/ci
go 1.25.0
require (
github.com/docker/docker v28.5.1+incompatible
github.com/opencontainers/image-spec v1.1.1
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/qdm12/log v0.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/time v0.14.0 // indirect
gotest.tools/v3 v3.5.2 // indirect
)
+109
View File
@@ -0,0 +1,109 @@
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
+27
View File
@@ -0,0 +1,27 @@
package internal
import (
"context"
"fmt"
)
func MullvadTest(ctx context.Context, logger Logger) error {
expectedSecrets := []string{
"Wireguard private key",
"Wireguard address",
}
secrets, err := readSecrets(ctx, expectedSecrets, logger)
if err != nil {
return fmt.Errorf("reading secrets: %w", err)
}
env := []string{
"VPN_SERVICE_PROVIDER=mullvad",
"VPN_TYPE=wireguard",
"LOG_LEVEL=debug",
"SERVER_COUNTRIES=USA",
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
"WIREGUARD_ADDRESSES=" + secrets[1],
}
return simpleTest(ctx, env, logger)
}
+25
View File
@@ -0,0 +1,25 @@
package internal
import (
"context"
"fmt"
)
func ProtonVPNTest(ctx context.Context, logger Logger) error {
expectedSecrets := []string{
"Wireguard private key",
}
secrets, err := readSecrets(ctx, expectedSecrets, logger)
if err != nil {
return fmt.Errorf("reading secrets: %w", err)
}
env := []string{
"VPN_SERVICE_PROVIDER=protonvpn",
"VPN_TYPE=wireguard",
"LOG_LEVEL=debug",
"SERVER_COUNTRIES=United States",
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
}
return simpleTest(ctx, env, logger)
}
+49
View File
@@ -0,0 +1,49 @@
package internal
import (
"bufio"
"context"
"fmt"
"os"
"strings"
)
type Logger interface {
Info(msg string)
Infof(format string, args ...any)
}
func readSecrets(ctx context.Context, expectedSecrets []string,
logger Logger,
) (lines []string, err error) {
scanner := bufio.NewScanner(os.Stdin)
lines = make([]string, 0, len(expectedSecrets))
for i := range expectedSecrets {
logger.Infof("🤫 reading %s from Stdin...", expectedSecrets[i])
if !scanner.Scan() {
break
}
lines = append(lines, strings.TrimSpace(scanner.Text()))
logger.Infof("🤫 %s secret read successfully", expectedSecrets[i])
if ctx.Err() != nil {
return nil, ctx.Err()
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("reading secrets from stdin: %w", err)
}
if len(lines) < len(expectedSecrets) {
return nil, fmt.Errorf("expected %d secrets via Stdin, but only received %d",
len(expectedSecrets), len(lines))
}
for i, line := range lines {
if line == "" {
return nil, fmt.Errorf("secret on line %d/%d was empty", i+1, len(lines))
}
}
return lines, nil
}
+134
View File
@@ -0,0 +1,134 @@
package internal
import (
"bufio"
"context"
"fmt"
"io"
"regexp"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
func ptrTo[T any](v T) *T { return &v }
func simpleTest(ctx context.Context, env []string, logger Logger) error {
const timeout = 30 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return fmt.Errorf("creating Docker client: %w", err)
}
defer client.Close()
config := &container.Config{
Image: "qmcgaw/gluetun",
StopTimeout: ptrTo(3),
Env: env,
}
hostConfig := &container.HostConfig{
AutoRemove: true,
CapAdd: []string{"NET_ADMIN", "NET_RAW"},
}
networkConfig := (*network.NetworkingConfig)(nil)
platform := (*v1.Platform)(nil)
const containerName = "" // auto-generated name
response, err := client.ContainerCreate(ctx, config, hostConfig, networkConfig, platform, containerName)
if err != nil {
return fmt.Errorf("creating container: %w", err)
}
for _, warning := range response.Warnings {
fmt.Println("Warning during container creation:", warning)
}
containerID := response.ID
defer stopContainer(client, containerID)
beforeStartTime := time.Now()
err = client.ContainerStart(ctx, containerID, container.StartOptions{})
if err != nil {
return fmt.Errorf("starting container: %w", err)
}
return waitForLogLine(ctx, client, containerID, beforeStartTime, logger)
}
func stopContainer(client *client.Client, containerID string) {
const stopTimeout = 5 * time.Second // must be higher than 3s, see above [container.Config]'s StopTimeout field
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
defer stopCancel()
err := client.ContainerStop(stopCtx, containerID, container.StopOptions{})
if err != nil {
fmt.Println("failed to stop container:", err)
}
}
var successRegexp = regexp.MustCompile(`^.+Public IP address is .+$`)
func waitForLogLine(ctx context.Context, client *client.Client, containerID string,
beforeStartTime time.Time, logger Logger,
) error {
logOptions := container.LogsOptions{
ShowStdout: true,
Follow: true,
Since: beforeStartTime.Format(time.RFC3339Nano),
}
reader, err := client.ContainerLogs(ctx, containerID, logOptions)
if err != nil {
return fmt.Errorf("error getting container logs: %w", err)
}
defer reader.Close()
var linesSeen []string
scanner := bufio.NewScanner(reader)
for ctx.Err() == nil {
if scanner.Scan() {
line := scanner.Text()
if len(line) > 8 { // remove Docker log prefix
line = line[8:]
}
linesSeen = append(linesSeen, line)
if successRegexp.MatchString(line) {
fmt.Println("✅ Success line logged")
return nil
}
continue
}
err := scanner.Err()
if err != nil && err != io.EOF {
logSeenLines(logger, linesSeen)
return fmt.Errorf("reading log stream: %w", err)
}
// The scanner is either done or cannot read because of EOF
logger.Info("the log scanner stopped")
logSeenLines(logger, linesSeen)
// Check if the container is still running
inspect, err := client.ContainerInspect(ctx, containerID)
if err != nil {
return fmt.Errorf("inspecting container: %w", err)
}
if !inspect.State.Running {
return fmt.Errorf("container stopped unexpectedly while waiting for log line. Exit code: %d", inspect.State.ExitCode)
}
}
return ctx.Err()
}
func logSeenLines(logger Logger, lines []string) {
fmt.Println("Logs seen so far:")
for _, line := range lines {
fmt.Println(" " + line)
}
}
+72 -36
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"io/fs"
"net/http"
"net/netip"
"os"
"os/exec"
"os/signal"
@@ -22,6 +23,7 @@ import (
"github.com/qdm12/gluetun/internal/configuration/sources/files"
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
"github.com/qdm12/gluetun/internal/constants"
copenvpn "github.com/qdm12/gluetun/internal/constants/openvpn"
"github.com/qdm12/gluetun/internal/dns"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/healthcheck"
@@ -98,11 +100,13 @@ func main() {
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
}()
// Wait for OS signal or run error
var err error
select {
case signal := <-signalCh:
case receivedSignal := <-signalCh:
signal.Stop(signalCh)
fmt.Println("")
logger.Warn("Caught OS signal " + signal.String() + ", shutting down")
logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down")
cancel()
case err = <-errorCh:
close(errorCh)
@@ -113,15 +117,14 @@ func main() {
cancel()
}
// Shutdown timed sequence, and force exit on second OS signal
const shutdownGracePeriod = 5 * time.Second
timer := time.NewTimer(shutdownGracePeriod)
select {
case shutdownErr := <-errorCh:
if !timer.Stop() {
<-timer.C
}
timer.Stop()
if shutdownErr != nil {
logger.Warnf("Shutdown not completed gracefully: %s", shutdownErr)
logger.Warnf("Shutdown failed: %s", shutdownErr)
os.Exit(1)
}
@@ -133,9 +136,6 @@ func main() {
case <-timer.C:
logger.Warn("Shutdown timed out")
os.Exit(1)
case signal := <-signalCh:
logger.Warn("Caught OS signal " + signal.String() + ", forcing shut down")
os.Exit(1)
}
}
@@ -166,6 +166,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
}
}
defer fmt.Println(gluetunLogo)
announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
if err != nil {
return err
@@ -269,11 +271,14 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
ovpnConf := openvpn.New(
logger.New(log.SetComponent("openvpn configurator")),
cmder, puid, pgid)
ovpnVersion := ovpnConf.Version26
if allSettings.VPN.OpenVPN.Version == copenvpn.Openvpn25 {
ovpnVersion = ovpnConf.Version25
}
err = printVersions(ctx, logger, []printVersionElement{
{name: "Alpine", getVersion: alpineConf.Version},
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
{name: "OpenVPN", getVersion: ovpnVersion},
{name: "IPtables", getVersion: firewallConf.Version},
})
if err != nil {
@@ -382,14 +387,14 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
routingConf, httpClient, firewallConf, portForwardLogger, puid, pgid)
routingConf, httpClient, firewallConf, portForwardLogger, cmder, puid, pgid)
portForwardRunError, err := portForwardLooper.Start(ctx)
if err != nil {
return fmt.Errorf("starting port forwarding loop: %w", err)
}
dnsLogger := logger.New(log.SetComponent("dns"))
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient, firewallConf,
dnsLogger)
if err != nil {
return fmt.Errorf("creating DNS loop: %w", err)
@@ -416,18 +421,26 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return fmt.Errorf("starting public ip loop: %w", err)
}
healthLogger := logger.New(log.SetComponent("healthcheck"))
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
go healthcheckServer.Run(healthServerCtx, healthServerDone)
healthChecker := healthcheck.NewChecker(healthLogger)
updaterLogger := logger.New(log.SetComponent("updater"))
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, updaterLogger,
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor)
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
openvpnFileExtractor, allSettings.Updater)
vpnLogger := logger.New(log.SetComponent("vpn"))
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
buildInfo, *allSettings.Version.Enabled)
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
"vpn", goroutine.OptionTimeout(time.Second))
@@ -461,13 +474,10 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
otherGroupHandler.Add(shadowsocksHandler)
controlServerAddress := *allSettings.ControlServer.Address
controlServerLogging := *allSettings.ControlServer.Log
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
logger.New(log.SetComponent("http server")),
allSettings.ControlServer.AuthFilePath,
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
storage, ipv6Supported)
if err != nil {
@@ -478,12 +488,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
<-httpServerReady
controlGroupHandler.Add(httpServerHandler)
healthLogger := logger.New(log.SetComponent("healthcheck"))
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
go healthcheckServer.Run(healthServerCtx, healthServerDone)
orderHandler := goshutdown.NewOrderHandler("gluetun",
order.OptionTimeout(totalShutdownTimeout),
order.OptionOnSuccess(defaultShutdownOnSuccess),
@@ -556,20 +560,20 @@ type netLinker interface {
}
type Addresser interface {
AddrList(link netlink.Link, family int) (
addresses []netlink.Addr, err error)
AddrReplace(link netlink.Link, addr netlink.Addr) error
AddrList(linkIndex uint32, family uint8) (
addresses []netip.Prefix, err error)
AddrReplace(linkIndex uint32, addr netip.Prefix) error
}
type Router interface {
RouteList(family int) (routes []netlink.Route, err error)
RouteList(family uint8) (routes []netlink.Route, err error)
RouteAdd(route netlink.Route) error
RouteDel(route netlink.Route) error
RouteReplace(route netlink.Route) error
}
type Ruler interface {
RuleList(family int) (rules []netlink.Rule, err error)
RuleList(family uint8) (rules []netlink.Rule, err error)
RuleAdd(rule netlink.Rule) error
RuleDel(rule netlink.Rule) error
}
@@ -577,11 +581,12 @@ type Ruler interface {
type Linker interface {
LinkList() (links []netlink.Link, err error)
LinkByName(name string) (link netlink.Link, err error)
LinkByIndex(index int) (link netlink.Link, err error)
LinkAdd(link netlink.Link) (linkIndex int, err error)
LinkDel(link netlink.Link) (err error)
LinkSetUp(link netlink.Link) (linkIndex int, err error)
LinkSetDown(link netlink.Link) (err error)
LinkByIndex(index uint32) (link netlink.Link, err error)
LinkAdd(link netlink.Link) (linkIndex uint32, err error)
LinkDel(linkIndex uint32) (err error)
LinkSetUp(linkIndex uint32) (err error)
LinkSetDown(linkIndex uint32) (err error)
LinkSetMTU(linkIndex, mtu uint32) error
}
type clier interface {
@@ -603,3 +608,34 @@ type RunStarter interface {
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, err error)
}
const gluetunLogo = ` @@@
@@@@
@@@@@@
@@@@.@@ @@@@@@@@@@
@@@@.@@@ @@@@@@@@==@@@@
@@@.@..@@ @@@@@@@=@..==@@@@
@@@@ @@@.@@.@@ @@@@@@===@@@@.=@@@
@...-@@ @@@@.@@.@@@ @@@ @@@@@@=======@@@=@@@@
@@@@@@@@ @@@.-%@.+@@@@@@@@ @@@@@%============@@@@
@@@.--@..@@@@.-@@@@@@@==============@@@@
@@@@ @@@-@--@@.@@.---@@@@@==============#@@@@@
@@@ @@@.@@-@@.@@--@@@@@===============@@@@@@
@@@@.@--@@@@@@@@@@================@@@@@@@
@@@..--@@*@@@@@@================@@@@+*@@
@@@.---@@.@@@@=================@@@@--@@
@@@-.---@@@@@@================@@@@*--@@@
@@@.:-#@@@@@@===============*@@@@.---@@
@@@.-------.@@@============@@@@@@.--@@@
@@@..--------:@@@=========@@@@@@@@.--@@@
@@@.-@@@@@@@@@@@========@@@@@ @@@.--@@
@@.@@@@===============@@@@@ @@@@@@---@@@@@@
@@@@@@@==============@@@@@@@@@@@@*@---@@@@@@@@
@@@@@@=============@@@@@ @@@...------------.*@@@
@@@@%===========@@@@@@ @@@..------@@@@.-----.-@@@
@@@@@@.=======@@@@@@ @@@.-------@@@@@@-.------=@@
@@@@@@@@@===@@@@@@ @@.------@@@@ @@@@.-----@@@
@@@==@@@=@@@@@@@ @@@.-@@@@@@@ @@@@@@@--@@
@@@@@@@@@@@@@ @@@@@@@@ @@@@@@@
@@@@@@@@ @@@@ @@@@
`
+27 -21
View File
@@ -1,59 +1,65 @@
module github.com/qdm12/gluetun
go 1.23
go 1.25.0
require (
github.com/breml/rootcerts v0.2.18
github.com/ProtonMail/go-srp v0.0.7
github.com/breml/rootcerts v0.3.3
github.com/fatih/color v1.18.0
github.com/golang/mock v1.6.0
github.com/klauspost/compress v1.17.9
github.com/jsimonetti/rtnetlink v1.4.2
github.com/klauspost/compress v1.18.1
github.com/klauspost/pgzip v1.2.6
github.com/pelletier/go-toml/v2 v2.2.2
github.com/qdm12/dns/v2 v2.0.0-rc7
github.com/qdm12/gosettings v0.4.3
github.com/mdlayher/genetlink v1.3.2
github.com/pelletier/go-toml/v2 v2.2.4
github.com/qdm12/dns/v2 v2.0.0-rc10
github.com/qdm12/gosettings v0.4.4
github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.2.0
github.com/qdm12/gotree v0.3.0
github.com/qdm12/log v0.1.0
github.com/qdm12/ss-server v0.6.0
github.com/stretchr/testify v1.9.0
github.com/ulikunitz/xz v0.5.11
github.com/vishvananda/netlink v1.2.1
github.com/stretchr/testify v1.11.1
github.com/ulikunitz/xz v0.5.15
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
golang.org/x/net v0.30.0
golang.org/x/sys v0.26.0
golang.org/x/text v0.19.0
golang.org/x/net v0.49.0
golang.org/x/sys v0.40.0
golang.org/x/text v0.33.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gopkg.in/ini.v1 v1.67.0
)
require (
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
github.com/ProtonMail/go-crypto v1.3.0-proton // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cronokirby/saferith v0.33.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.60.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/qdm12/goservices v0.1.0 // indirect
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/tools v0.26.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.40.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+80 -51
View File
@@ -1,10 +1,25 @@
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/breml/rootcerts v0.2.18 h1:KjZaNT7AX/akUjzpStuwTMQs42YHlPyc6NmdwShVba0=
github.com/breml/rootcerts v0.2.18/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/breml/rootcerts v0.3.3 h1://GnaRtQ/9BY2+GtMk2wtWxVdCRysiaPr5/xBwl7NKw=
github.com/breml/rootcerts v0.3.3/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
@@ -13,12 +28,14 @@ github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90=
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -34,16 +51,18 @@ github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy5
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
@@ -54,12 +73,12 @@ github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPA
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/qdm12/dns/v2 v2.0.0-rc7 h1:noFoWunMfFuJeIpcyTJsdS9BU7jRxxKrBV5xN2qjPHI=
github.com/qdm12/dns/v2 v2.0.0-rc7/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
github.com/qdm12/gosettings v0.4.3 h1:oGAjiKVtml9oHVlPQo6H3yk6TmtWpVYicNeGFcM7AP8=
github.com/qdm12/gosettings v0.4.3/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
github.com/qdm12/dns/v2 v2.0.0-rc10 h1:IyeNEYXfhBsaE1dwxx5eAqdAz1HS98dT+8c7xoKODa0=
github.com/qdm12/dns/v2 v2.0.0-rc10/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
@@ -74,66 +93,77 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA=
github.com/vishvananda/netlink v1.2.1/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -150,7 +180,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
+1
View File
@@ -7,3 +7,4 @@ func newNoopLogger() *noopLogger {
}
func (l *noopLogger) Info(string) {}
func (l *noopLogger) Warn(string) {}
+2 -1
View File
@@ -56,6 +56,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
if err != nil {
return err
}
allSettings.SetDefaults()
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
if err != nil {
@@ -75,7 +76,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, warner, client,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
providerConf := providers.Get(allSettings.VPN.Provider.Name)
connection, err := providerConf.GetConnection(
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
+24 -3
View File
@@ -6,6 +6,7 @@ import (
"flag"
"fmt"
"net/http"
"slices"
"strings"
"time"
@@ -24,6 +25,8 @@ import (
var (
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
ErrNoProviderSpecified = errors.New("no provider was specified")
ErrUsernameMissing = errors.New("username is required for this provider")
ErrPasswordMissing = errors.New("password is required for this provider")
)
type UpdaterLogger interface {
@@ -35,7 +38,7 @@ type UpdaterLogger interface {
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
options := settings.Updater{}
var endUserMode, maintainerMode, updateAll bool
var csvProviders, ipToken string
var csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
flagSet.BoolVar(&maintainerMode, "maintainer", false,
@@ -47,6 +50,10 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
flagSet.StringVar(&protonUsername, "proton-username", "",
"(Retro-compatibility) Username to use to authenticate with Proton. Use -proton-email instead.") // v4 remove this
flagSet.StringVar(&protonEmail, "proton-email", "", "Email to use to authenticate with Proton")
flagSet.StringVar(&protonPassword, "proton-password", "", "Password to use to authenticate with Proton")
if err := flagSet.Parse(args); err != nil {
return err
}
@@ -64,6 +71,16 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
options.Providers = strings.Split(csvProviders, ",")
}
if slices.Contains(options.Providers, providers.Protonvpn) {
if protonEmail == "" && protonUsername != "" {
protonEmail = protonUsername + "@protonmail.com"
logger.Warn("use -proton-email instead of -proton-username in the future. " +
"This assumes the email is " + protonEmail + " and may not work.")
}
options.ProtonEmail = &protonEmail
options.ProtonPassword = &protonPassword
}
options.SetDefaults(options.Providers[0])
err := options.Validate()
@@ -71,7 +88,11 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
return fmt.Errorf("options validation failed: %w", err)
}
storage, err := storage.New(logger, constants.ServersData)
serversDataPath := constants.ServersData
if maintainerMode {
serversDataPath = ""
}
storage, err := storage.New(logger, serversDataPath)
if err != nil {
return fmt.Errorf("creating servers storage: %w", err)
}
@@ -94,7 +115,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
updater := updater.New(httpClient, storage, providers, logger)
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
+150
View File
@@ -0,0 +1,150 @@
package command
import (
"bytes"
"errors"
"fmt"
"strings"
"unicode/utf8"
)
var (
ErrCommandEmpty = errors.New("command is empty")
ErrSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
ErrDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
ErrEscapeUnterminated = errors.New("unterminated backslash-escape")
)
// Split splits a command string into a slice of arguments.
// This is especially important for commands such as:
// /bin/sh -c "echo hello"
// which should be split into: ["/bin/sh", "-c", "echo hello"]
// It supports backslash-escapes, single-quotes and double-quotes.
// It does not support:
// - the $" quoting style.
// - expansion (brace, shell or pathname).
func Split(command string) (words []string, err error) {
if command == "" {
return nil, fmt.Errorf("%w", ErrCommandEmpty)
}
const bufferSize = 1024
buffer := bytes.NewBuffer(make([]byte, bufferSize))
startIndex := 0
for startIndex < len(command) {
// skip any split characters at the start
character, runeSize := utf8.DecodeRuneInString(command[startIndex:])
switch {
case strings.ContainsRune(" \n\t", character):
startIndex += runeSize
case character == '\\':
// Look ahead to eventually skip an escaped newline
if command[startIndex+runeSize:] == "" {
return nil, fmt.Errorf("%w: %q", ErrEscapeUnterminated, command)
}
character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:])
if character == '\n' {
startIndex += runeSize + runeSize // backslash and newline
}
default:
var word string
buffer.Reset()
word, startIndex, err = splitWord(command, startIndex, buffer)
if err != nil {
return nil, fmt.Errorf("splitting word in %q: %w", command, err)
}
words = append(words, word)
}
}
return words, nil
}
// WARNING: buffer must be cleared before calling this function.
func splitWord(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error,
) {
cursor := startIndex
for cursor < len(input) {
character, runeLength := utf8.DecodeRuneInString(input[cursor:])
cursor += runeLength
if character == '"' ||
character == '\'' ||
character == '\\' ||
character == ' ' ||
character == '\n' ||
character == '\t' {
buffer.WriteString(input[startIndex : cursor-runeLength])
}
switch {
case strings.ContainsRune(" \n\t", character): // spacing character
return buffer.String(), cursor, nil
case character == '"':
return handleDoubleQuoted(input, cursor, buffer)
case character == '\'':
return handleSingleQuoted(input, cursor, buffer)
case character == '\\':
return handleEscaped(input, cursor, buffer)
}
}
buffer.WriteString(input[startIndex:])
return buffer.String(), len(input), nil
}
func handleDoubleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error,
) {
cursor := startIndex
for cursor < len(input) {
nextCharacter, nextRuneLength := utf8.DecodeRuneInString(input[cursor:])
cursor += nextRuneLength
switch nextCharacter {
case '"': // end of the double quoted string
buffer.WriteString(input[startIndex : cursor-nextRuneLength])
return splitWord(input, cursor, buffer)
case '\\': // escaped character
escapedCharacter, escapedRuneLength := utf8.DecodeRuneInString(input[cursor:])
cursor += escapedRuneLength
if !strings.ContainsRune("$`\"\n\\", escapedCharacter) {
break
}
buffer.WriteString(input[startIndex : cursor-nextRuneLength-escapedRuneLength])
if escapedCharacter != '\n' {
// skip backslash entirely for the newline character
buffer.WriteRune(escapedCharacter)
}
startIndex = cursor
}
}
return "", 0, fmt.Errorf("%w", ErrDoubleQuoteUnterminated)
}
func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error,
) {
closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'')
if closingQuoteIndex == -1 {
return "", 0, fmt.Errorf("%w", ErrSingleQuoteUnterminated)
}
buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex])
const singleQuoteRuneLength = 1
startIndex += closingQuoteIndex + singleQuoteRuneLength
return splitWord(input, startIndex, buffer)
}
func handleEscaped(input string, startIndex int, buffer *bytes.Buffer) (
word string, newStartIndex int, err error,
) {
if input[startIndex:] == "" {
return "", 0, fmt.Errorf("%w", ErrEscapeUnterminated)
}
character, runeLength := utf8.DecodeRuneInString(input[startIndex:])
if character != '\n' { // backslash-escaped newline is ignored
buffer.WriteString(input[startIndex : startIndex+runeLength])
}
startIndex += runeLength
return splitWord(input, startIndex, buffer)
}
+110
View File
@@ -0,0 +1,110 @@
package command
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Split(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
command string
words []string
errWrapped error
errMessage string
}{
"empty": {
command: "",
errWrapped: ErrCommandEmpty,
errMessage: "command is empty",
},
"concrete_sh_command": {
command: `/bin/sh -c "echo 123"`,
words: []string{"/bin/sh", "-c", "echo 123"},
},
"single_word": {
command: "word1",
words: []string{"word1"},
},
"two_words_single_space": {
command: "word1 word2",
words: []string{"word1", "word2"},
},
"two_words_multiple_space": {
command: "word1 word2",
words: []string{"word1", "word2"},
},
"two_words_no_expansion": {
command: "word1* word2?",
words: []string{"word1*", "word2?"},
},
"escaped_single quote": {
command: "ain\\'t good",
words: []string{"ain't", "good"},
},
"escaped_single_quote_all_single_quoted": {
command: "'ain'\\''t good'",
words: []string{"ain't good"},
},
"empty_single_quoted": {
command: "word1 '' word2",
words: []string{"word1", "", "word2"},
},
"escaped_newline": {
command: "word1\\\nword2",
words: []string{"word1word2"},
},
"quoted_newline": {
command: "text \"with\na\" quoted newline",
words: []string{"text", "with\na", "quoted", "newline"},
},
"quoted_escaped_newline": {
command: "\"word1\\d\\\\\\\" word2\\\nword3 word4\"",
words: []string{"word1\\d\\\" word2word3 word4"},
},
"escaped_separated_newline": {
command: "word1 \\\n word2",
words: []string{"word1", "word2"},
},
"double_quotes_no_spacing": {
command: "word1\"word2\"word3",
words: []string{"word1word2word3"},
},
"unterminated_single_quote": {
command: "'abc'\\''def",
errWrapped: ErrSingleQuoteUnterminated,
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
},
"unterminated_double_quote": {
command: "\"abc'def",
errWrapped: ErrDoubleQuoteUnterminated,
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
},
"unterminated_escape": {
command: "abc\\",
errWrapped: ErrEscapeUnterminated,
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
},
"unterminated_escape_only": {
command: " \\",
errWrapped: ErrEscapeUnterminated,
errMessage: `unterminated backslash-escape: " \\"`,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
words, err := Split(testCase.command)
assert.Equal(t, testCase.words, words)
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
}
})
}
}
@@ -9,9 +9,11 @@ import (
func readObsolete(r *reader.Reader) (warnings []string) {
keyToMessage := map[string]string{
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
"HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete",
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
}
sortedKeys := maps.Keys(keyToMessage)
slices.Sort(sortedKeys)
+148 -18
View File
@@ -1,9 +1,13 @@
package settings
import (
"errors"
"fmt"
"net/netip"
"time"
"github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
@@ -11,10 +15,31 @@ import (
// DNS contains settings to configure DNS.
type DNS struct {
// ServerEnabled is true if the server should be running
// and used. It defaults to true, and cannot be nil
// in the internal state.
ServerEnabled *bool
// UpstreamType can be dot or plain, and defaults to dot.
UpstreamType string `json:"upstream_type"`
// UpdatePeriod is the period to update DNS block lists.
// It can be set to 0 to disable the update.
// It defaults to 24h and cannot be nil in
// the internal state.
UpdatePeriod *time.Duration
// Providers is a list of DNS providers
Providers []string `json:"providers"`
// Caching is true if the server should cache
// DNS responses.
Caching *bool `json:"caching"`
// IPv6 is true if the server should connect over IPv6.
IPv6 *bool `json:"ipv6"`
// Blacklist contains settings to configure the filter
// block lists.
Blacklist DNSBlacklist
// ServerAddress is the DNS server to use inside
// the Go program and for the system.
// It defaults to '127.0.0.1' to be used with the
// DoT server. It cannot be the zero value in the internal
// local server. It cannot be the zero value in the internal
// state.
ServerAddress netip.Addr
// KeepNameserver is true if the existing DNS server
@@ -23,20 +48,40 @@ type DNS struct {
// outside the VPN tunnel since it would go through
// the local DNS server of your Docker/Kubernetes
// configuration, which is likely not going through the tunnel.
// This will also disable the DNS over TLS server and the
// This will also disable the DNS forwarder server and the
// `ServerAddress` field will be ignored.
// It defaults to false and cannot be nil in the
// internal state.
KeepNameserver *bool
// DOT contains settings to configure the DoT
// server.
DoT DoT
}
var (
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
)
func (d DNS) validate() (err error) {
err = d.DoT.validate()
if !helpers.IsOneOf(d.UpstreamType, "dot", "doh", "plain") {
return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType)
}
const minUpdatePeriod = 30 * time.Second
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
return fmt.Errorf("%w: %s must be bigger than %s",
ErrDNSUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
}
providers := provider.NewProviders()
for _, providerName := range d.Providers {
_, err := providers.Get(providerName)
if err != nil {
return err
}
}
err = d.Blacklist.validate()
if err != nil {
return fmt.Errorf("validating DoT settings: %w", err)
return err
}
return nil
@@ -44,9 +89,15 @@ func (d DNS) validate() (err error) {
func (d *DNS) Copy() (copied DNS) {
return DNS{
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled),
UpstreamType: d.UpstreamType,
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
Providers: gosettings.CopySlice(d.Providers),
Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.copy(),
ServerAddress: d.ServerAddress,
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
DoT: d.DoT.copy(),
}
}
@@ -54,16 +105,48 @@ func (d *DNS) Copy() (copied DNS) {
// settings object with any field set in the other
// settings.
func (d *DNS) overrideWith(other DNS) {
d.ServerEnabled = gosettings.OverrideWithPointer(d.ServerEnabled, other.ServerEnabled)
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
d.Blacklist.overrideWith(other.Blacklist)
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
d.DoT.overrideWith(other.DoT)
}
func (d *DNS) setDefaults() {
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
d.ServerEnabled = gosettings.DefaultPointer(d.ServerEnabled, true)
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, "dot")
const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
provider.Cloudflare().Name,
})
d.Caching = gosettings.DefaultPointer(d.Caching, true)
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
d.Blacklist.setDefaults()
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress,
netip.AddrFrom4([4]byte{127, 0, 0, 1}))
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
d.DoT.setDefaults()
}
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
if d.ServerAddress.Compare(localhost) != 0 && d.ServerAddress.Is4() {
return d.ServerAddress
}
providers := provider.NewProviders()
provider, err := providers.Get(d.Providers[0])
if err != nil {
// Settings should be validated before calling this function,
// so an error happening here is a programming error.
panic(err)
}
return provider.Plain.IPv4[0].Addr()
}
func (d DNS) String() string {
@@ -77,11 +160,63 @@ func (d DNS) toLinesNode() (node *gotree.Node) {
return node
}
node.Appendf("DNS server address to use: %s", d.ServerAddress)
node.AppendNode(d.DoT.toLinesNode())
node.Appendf("DNS forwarder server enabled: %s", gosettings.BoolToYesNo(d.ServerEnabled))
if !*d.ServerEnabled {
return node
}
node.Appendf("Upstream resolver type: %s", d.UpstreamType)
upstreamResolvers := node.Append("Upstream resolvers:")
for _, provider := range d.Providers {
upstreamResolvers.Append(provider)
}
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
update := "disabled"
if *d.UpdatePeriod > 0 {
update = "every " + d.UpdatePeriod.String()
}
node.Appendf("Update period: %s", update)
node.AppendNode(d.Blacklist.toLinesNode())
return node
}
func (d *DNS) read(r *reader.Reader) (err error) {
d.ServerEnabled, err = r.BoolPtr("DNS_SERVER", reader.RetroKeys("DOT"))
if err != nil {
return err
}
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
if err != nil {
return err
}
d.Providers = r.CSV("DNS_UPSTREAM_RESOLVERS", reader.RetroKeys("DOT_PROVIDERS"))
d.Caching, err = r.BoolPtr("DNS_CACHING", reader.RetroKeys("DOT_CACHING"))
if err != nil {
return err
}
d.IPv6, err = r.BoolPtr("DNS_UPSTREAM_IPV6", reader.RetroKeys("DOT_IPV6"))
if err != nil {
return err
}
err = d.Blacklist.read(r)
if err != nil {
return err
}
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
if err != nil {
return err
@@ -92,10 +227,5 @@ func (d *DNS) read(r *reader.Reader) (err error) {
return err
}
err = d.DoT.read(r)
if err != nil {
return fmt.Errorf("DNS over TLS settings: %w", err)
}
return nil
}
+60 -16
View File
@@ -22,6 +22,9 @@ type DNSBlacklist struct {
AddBlockedHosts []string
AddBlockedIPs []netip.Addr
AddBlockedIPPrefixes []netip.Prefix
// RebindingProtectionExemptHostnames is a list of hostnames
// exempt from DNS rebinding protection.
RebindingProtectionExemptHostnames []string
}
func (b *DNSBlacklist) setDefaults() {
@@ -33,8 +36,9 @@ func (b *DNSBlacklist) setDefaults() {
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
var (
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
ErrRebindingProtectionExemptHostNotValid = errors.New("rebinding protection exempt host is not valid")
)
func (b DNSBlacklist) validate() (err error) {
@@ -50,18 +54,25 @@ func (b DNSBlacklist) validate() (err error) {
}
}
for _, host := range b.RebindingProtectionExemptHostnames {
if !hostRegex.MatchString(host) {
return fmt.Errorf("%w: %s", ErrRebindingProtectionExemptHostNotValid, host)
}
}
return nil
}
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
return DNSBlacklist{
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
BlockAds: gosettings.CopyPointer(b.BlockAds),
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
BlockAds: gosettings.CopyPointer(b.BlockAds),
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
RebindingProtectionExemptHostnames: gosettings.CopySlice(b.RebindingProtectionExemptHostnames),
}
}
@@ -73,6 +84,8 @@ func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
b.RebindingProtectionExemptHostnames = gosettings.OverrideWithSlice(b.RebindingProtectionExemptHostnames,
other.RebindingProtectionExemptHostnames)
}
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
@@ -129,6 +142,13 @@ func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
}
}
if len(b.RebindingProtectionExemptHostnames) > 0 {
exemptHostsNode := node.Append("Rebinding protection exempt hostnames:")
for _, host := range b.RebindingProtectionExemptHostnames {
exemptHostsNode.Append(host)
}
}
return node
}
@@ -149,23 +169,47 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
return err
}
b.AddBlockedIPs, b.AddBlockedIPPrefixes,
err = readDoTPrivateAddresses(r) // TODO v4 split in 2
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
if err != nil {
return err
}
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
b.RebindingProtectionExemptHostnames = r.CSV("DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES")
return nil
}
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
ipPrefixes []netip.Prefix, err error,
) {
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
ips, err = r.CSVNetipAddresses("DNS_BLOCK_IPS")
if err != nil {
return nil, nil, err
}
ipPrefixes, err = r.CSVNetipPrefixes("DNS_BLOCK_IP_PREFIXES")
if err != nil {
return nil, nil, err
}
// TODO v4 remove this block below
privateIPs, privateIPPrefixes, err := readDNSPrivateAddresses(r)
if err != nil {
return nil, nil, err
}
ips = append(ips, privateIPs...)
ipPrefixes = append(ipPrefixes, privateIPPrefixes...)
return ips, ipPrefixes, nil
}
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
ipPrefixes []netip.Prefix, err error,
) {
privateAddresses := r.CSV("DOT_PRIVATE_ADDRESS", reader.IsRetro("DNS_BLOCK_IP_PREFIXES"))
if len(privateAddresses) == 0 {
return nil, nil, nil
}
-170
View File
@@ -1,170 +0,0 @@
package settings
import (
"errors"
"fmt"
"net/netip"
"time"
"github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
// DoT contains settings to configure the DoT server.
type DoT struct {
// Enabled is true if the DoT server should be running
// and used. It defaults to true, and cannot be nil
// in the internal state.
Enabled *bool
// UpdatePeriod is the period to update DNS block lists.
// It can be set to 0 to disable the update.
// It defaults to 24h and cannot be nil in
// the internal state.
UpdatePeriod *time.Duration
// Providers is a list of DNS over TLS providers
Providers []string `json:"providers"`
// Caching is true if the DoT server should cache
// DNS responses.
Caching *bool `json:"caching"`
// IPv6 is true if the DoT server should connect over IPv6.
IPv6 *bool `json:"ipv6"`
// Blacklist contains settings to configure the filter
// block lists.
Blacklist DNSBlacklist
}
var ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
func (d DoT) validate() (err error) {
const minUpdatePeriod = 30 * time.Second
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
return fmt.Errorf("%w: %s must be bigger than %s",
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
}
providers := provider.NewProviders()
for _, providerName := range d.Providers {
_, err := providers.Get(providerName)
if err != nil {
return err
}
}
err = d.Blacklist.validate()
if err != nil {
return err
}
return nil
}
func (d *DoT) copy() (copied DoT) {
return DoT{
Enabled: gosettings.CopyPointer(d.Enabled),
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
Providers: gosettings.CopySlice(d.Providers),
Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.copy(),
}
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (d *DoT) overrideWith(other DoT) {
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
d.Blacklist.overrideWith(other.Blacklist)
}
func (d *DoT) setDefaults() {
d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
provider.Cloudflare().Name,
})
d.Caching = gosettings.DefaultPointer(d.Caching, true)
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
d.Blacklist.setDefaults()
}
func (d DoT) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
providers := provider.NewProviders()
provider, err := providers.Get(d.Providers[0])
if err != nil {
// Settings should be validated before calling this function,
// so an error happening here is a programming error.
panic(err)
}
return provider.DoT.IPv4[0].Addr()
}
func (d DoT) String() string {
return d.toLinesNode().String()
}
func (d DoT) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS over TLS settings:")
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(d.Enabled))
if !*d.Enabled {
return node
}
update := "disabled" //nolint:goconst
if *d.UpdatePeriod > 0 {
update = "every " + d.UpdatePeriod.String()
}
node.Appendf("Update period: %s", update)
upstreamResolvers := node.Append("Upstream resolvers:")
for _, provider := range d.Providers {
upstreamResolvers.Append(provider)
}
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
node.AppendNode(d.Blacklist.toLinesNode())
return node
}
func (d *DoT) read(reader *reader.Reader) (err error) {
d.Enabled, err = reader.BoolPtr("DOT")
if err != nil {
return err
}
d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD")
if err != nil {
return err
}
d.Providers = reader.CSV("DOT_PROVIDERS")
d.Caching, err = reader.BoolPtr("DOT_CACHING")
if err != nil {
return err
}
d.IPv6, err = reader.BoolPtr("DOT_IPV6")
if err != nil {
return err
}
err = d.Blacklist.read(reader)
if err != nil {
return err
}
return nil
}
@@ -36,6 +36,8 @@ var (
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
ErrUpdaterProtonPasswordMissing = errors.New("proton password is missing")
ErrUpdaterProtonEmailMissing = errors.New("proton email is missing")
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
+79 -52
View File
@@ -1,9 +1,10 @@
package settings
import (
"errors"
"fmt"
"net/netip"
"os"
"time"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
@@ -17,34 +18,51 @@ type Health struct {
// for the health check server.
// It cannot be the empty string in the internal state.
ServerAddress string
// ReadHeaderTimeout is the HTTP server header read timeout
// duration of the HTTP server. It defaults to 100 milliseconds.
ReadHeaderTimeout time.Duration
// ReadTimeout is the HTTP read timeout duration of the
// HTTP server. It defaults to 500 milliseconds.
ReadTimeout time.Duration
// TargetAddress is the address (host or host:port)
// to TCP dial to periodically for the health check.
// It cannot be the empty string in the internal state.
TargetAddress string
// SuccessWait is the duration to wait to re-run the
// healthcheck after a successful healthcheck.
// It defaults to 5 seconds and cannot be zero in
// the internal state.
SuccessWait time.Duration
// VPN has health settings specific to the VPN loop.
VPN HealthyWait
// TargetAddresses are the addresses (host or host:port)
// to TCP TLS dial to periodically for the health check.
// Addresses after the first one are used as fallbacks for retries.
// It cannot be empty in the internal state.
TargetAddresses []string
// ICMPTargetIPs are the IP addresses to use for ICMP echo requests
// in the health checker. The slice can be set to a single
// unspecified address (0.0.0.0) such that the VPN server IP is used,
// although this can be less reliable. It defaults to [1.1.1.1,8.8.8.8],
// and cannot be left empty in the internal state.
ICMPTargetIPs []netip.Addr
// SmallCheckType is the type of small health check to perform.
// It can be "icmp" or "dns", and defaults to "icmp".
// Note it changes automatically to dns if icmp is not supported.
SmallCheckType string
// RestartVPN indicates whether to restart the VPN connection
// when the healthcheck fails.
RestartVPN *bool
}
var (
ErrICMPTargetIPNotValid = errors.New("ICMP target IP address is not valid")
ErrICMPTargetIPsNotCompatible = errors.New("ICMP target IP addresses are not compatible")
ErrSmallCheckTypeNotValid = errors.New("small check type is not valid")
)
func (h Health) Validate() (err error) {
err = validate.ListeningAddress(h.ServerAddress, os.Getuid())
if err != nil {
return fmt.Errorf("server listening address is not valid: %w", err)
}
err = h.VPN.validate()
for _, ip := range h.ICMPTargetIPs {
switch {
case !ip.IsValid():
return fmt.Errorf("%w: %s", ErrICMPTargetIPNotValid, ip)
case ip.IsUnspecified() && len(h.ICMPTargetIPs) > 1:
return fmt.Errorf("%w: only a single IP address must be set if it is to be unspecified",
ErrICMPTargetIPsNotCompatible)
}
}
err = validate.IsOneOf(h.SmallCheckType, "icmp", "dns")
if err != nil {
return fmt.Errorf("health VPN settings: %w", err)
return fmt.Errorf("%w: %s", ErrSmallCheckTypeNotValid, err)
}
return nil
@@ -52,12 +70,11 @@ func (h Health) Validate() (err error) {
func (h *Health) copy() (copied Health) {
return Health{
ServerAddress: h.ServerAddress,
ReadHeaderTimeout: h.ReadHeaderTimeout,
ReadTimeout: h.ReadTimeout,
TargetAddress: h.TargetAddress,
SuccessWait: h.SuccessWait,
VPN: h.VPN.copy(),
ServerAddress: h.ServerAddress,
TargetAddresses: h.TargetAddresses,
ICMPTargetIPs: gosettings.CopySlice(h.ICMPTargetIPs),
SmallCheckType: h.SmallCheckType,
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
}
}
@@ -66,23 +83,21 @@ func (h *Health) copy() (copied Health) {
// settings.
func (h *Health) OverrideWith(other Health) {
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
h.SuccessWait = gosettings.OverrideWithComparable(h.SuccessWait, other.SuccessWait)
h.VPN.overrideWith(other.VPN)
h.TargetAddresses = gosettings.OverrideWithSlice(h.TargetAddresses, other.TargetAddresses)
h.ICMPTargetIPs = gosettings.OverrideWithSlice(h.ICMPTargetIPs, other.ICMPTargetIPs)
h.SmallCheckType = gosettings.OverrideWithComparable(h.SmallCheckType, other.SmallCheckType)
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
}
func (h *Health) SetDefaults() {
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
const defaultReadHeaderTimeout = 100 * time.Millisecond
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
const defaultReadTimeout = 500 * time.Millisecond
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
const defaultSuccessWait = 5 * time.Second
h.SuccessWait = gosettings.DefaultComparable(h.SuccessWait, defaultSuccessWait)
h.VPN.setDefaults()
h.TargetAddresses = gosettings.DefaultSlice(h.TargetAddresses, []string{"cloudflare.com:443", "github.com:443"})
h.ICMPTargetIPs = gosettings.DefaultSlice(h.ICMPTargetIPs, []netip.Addr{
netip.AddrFrom4([4]byte{1, 1, 1, 1}),
netip.AddrFrom4([4]byte{8, 8, 8, 8}),
})
h.SmallCheckType = gosettings.DefaultComparable(h.SmallCheckType, "icmp")
h.RestartVPN = gosettings.DefaultPointer(h.RestartVPN, true)
}
func (h Health) String() string {
@@ -92,28 +107,40 @@ func (h Health) String() string {
func (h Health) toLinesNode() (node *gotree.Node) {
node = gotree.New("Health settings:")
node.Appendf("Server listening address: %s", h.ServerAddress)
node.Appendf("Target address: %s", h.TargetAddress)
node.Appendf("Duration to wait after success: %s", h.SuccessWait)
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
node.Appendf("Read timeout: %s", h.ReadTimeout)
node.AppendNode(h.VPN.toLinesNode("VPN"))
targetAddrs := node.Appendf("Target addresses:")
for _, targetAddr := range h.TargetAddresses {
targetAddrs.Append(targetAddr)
}
switch h.SmallCheckType {
case "icmp":
icmpNode := node.Appendf("Small health check type: ICMP echo request")
if len(h.ICMPTargetIPs) == 1 && h.ICMPTargetIPs[0].IsUnspecified() {
icmpNode.Appendf("ICMP target IP: VPN server IP address")
} else {
icmpIPs := icmpNode.Appendf("ICMP target IPs:")
for _, ip := range h.ICMPTargetIPs {
icmpIPs.Append(ip.String())
}
}
case "dns":
node.Appendf("Small health check type: Plain DNS lookup over UDP")
}
node.Appendf("Restart VPN on healthcheck failure: %s", gosettings.BoolToYesNo(h.RestartVPN))
return node
}
func (h *Health) Read(r *reader.Reader) (err error) {
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS",
reader.RetroKeys("HEALTH_ADDRESS_TO_PING"))
h.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION")
h.TargetAddresses = r.CSV("HEALTH_TARGET_ADDRESSES",
reader.RetroKeys("HEALTH_ADDRESS_TO_PING", "HEALTH_TARGET_ADDRESS"))
h.ICMPTargetIPs, err = r.CSVNetipAddresses("HEALTH_ICMP_TARGET_IPS", reader.RetroKeys("HEALTH_ICMP_TARGET_IP"))
if err != nil {
return err
}
err = h.VPN.read(r)
h.SmallCheckType = r.String("HEALTH_SMALL_CHECK_TYPE")
h.RestartVPN, err = r.BoolPtr("HEALTH_RESTART_VPN")
if err != nil {
return fmt.Errorf("VPN health settings: %w", err)
return err
}
return nil
}
@@ -1,76 +0,0 @@
package settings
import (
"time"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
type HealthyWait struct {
// Initial is the initial duration to wait for the program
// to be healthy before taking action.
// It cannot be nil in the internal state.
Initial *time.Duration
// Addition is the duration to add to the Initial duration
// after Initial has expired to wait longer for the program
// to be healthy.
// It cannot be nil in the internal state.
Addition *time.Duration
}
func (h HealthyWait) validate() (err error) {
return nil
}
func (h *HealthyWait) copy() (copied HealthyWait) {
return HealthyWait{
Initial: gosettings.CopyPointer(h.Initial),
Addition: gosettings.CopyPointer(h.Addition),
}
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (h *HealthyWait) overrideWith(other HealthyWait) {
h.Initial = gosettings.OverrideWithPointer(h.Initial, other.Initial)
h.Addition = gosettings.OverrideWithPointer(h.Addition, other.Addition)
}
func (h *HealthyWait) setDefaults() {
const initialDurationDefault = 6 * time.Second
const additionDurationDefault = 5 * time.Second
h.Initial = gosettings.DefaultPointer(h.Initial, initialDurationDefault)
h.Addition = gosettings.DefaultPointer(h.Addition, additionDurationDefault)
}
func (h HealthyWait) String() string {
return h.toLinesNode("Health").String()
}
func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) {
node = gotree.New(kind + " wait durations:")
node.Appendf("Initial duration: %s", *h.Initial)
node.Appendf("Additional duration: %s", *h.Addition)
return node
}
func (h *HealthyWait) read(r *reader.Reader) (err error) {
h.Initial, err = r.DurationPtr(
"HEALTH_VPN_DURATION_INITIAL",
reader.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL"))
if err != nil {
return err
}
h.Addition, err = r.DurationPtr(
"HEALTH_VPN_DURATION_ADDITION",
reader.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION"))
if err != nil {
return err
}
return nil
}
+1 -2
View File
@@ -209,8 +209,7 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
case
providers.Airvpn,
providers.Cyberghost,
providers.VPNUnlimited,
providers.Wevpn:
providers.VPNUnlimited:
if clientKey == "" {
return fmt.Errorf("%w", ErrMissingValue)
}
@@ -2,6 +2,7 @@ package settings
import (
"fmt"
"net/netip"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
@@ -24,6 +25,12 @@ type OpenVPNSelection struct {
// and can be udp or tcp. It cannot be the empty string
// in the internal state.
Protocol string `json:"protocol"`
// EndpointIP is the server endpoint IP address.
// If set, it overrides any IP address from the picked
// built-in server connection. To indicate it should
// not be used, it should be set to [netip.IPv4Unspecified].
// It can never be the zero value in the internal state.
EndpointIP netip.Addr `json:"endpoint_ip"`
// CustomPort is the OpenVPN server endpoint port.
// It can be set to 0 to indicate no custom port should
// be used. It cannot be nil in the internal state.
@@ -97,13 +104,10 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
case providers.Protonvpn:
allowedTCP = []uint16{443, 5995, 8443}
allowedUDP = []uint16{80, 443, 1194, 4569, 5060}
allowedUDP = []uint16{80, 443, 1194, 4569, 5060, 51820}
case providers.SlickVPN:
allowedTCP = []uint16{443, 8080, 8888}
allowedUDP = []uint16{443, 8080, 8888}
case providers.Wevpn:
allowedTCP = []uint16{53, 1195, 1199, 2018}
allowedUDP = []uint16{80, 1194, 1198}
case providers.Windscribe:
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
@@ -142,6 +146,7 @@ func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
return OpenVPNSelection{
ConfFile: gosettings.CopyPointer(o.ConfFile),
Protocol: o.Protocol,
EndpointIP: o.EndpointIP,
CustomPort: gosettings.CopyPointer(o.CustomPort),
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
}
@@ -151,12 +156,14 @@ func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
o.EndpointIP = gosettings.OverrideWithValidator(o.EndpointIP, other.EndpointIP)
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
}
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
o.EndpointIP = gosettings.DefaultValidator(o.EndpointIP, netip.IPv4Unspecified())
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
var defaultEncPreset string
@@ -174,6 +181,10 @@ func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("OpenVPN server selection settings:")
node.Appendf("Protocol: %s", strings.ToUpper(o.Protocol))
if !o.EndpointIP.IsUnspecified() {
node.Appendf("Endpoint IP address: %s", o.EndpointIP)
}
if *o.CustomPort != 0 {
node.Appendf("Custom port: %d", *o.CustomPort)
}
@@ -194,6 +205,12 @@ func (o *OpenVPNSelection) read(r *reader.Reader) (err error) {
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
o.EndpointIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
if err != nil {
return err
}
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))
if err != nil {
@@ -29,6 +29,14 @@ type PortForwarding struct {
// to write to a file. It cannot be nil for the
// internal state
Filepath *string `json:"status_file_path"`
// UpCommand is the command to use when the port forwarding is up.
// It can be the empty string to indicate not to run a command.
// It cannot be nil in the internal state.
UpCommand *string `json:"up_command"`
// DownCommand is the command to use after the port forwarding goes down.
// It can be the empty string to indicate to NOT run a command.
// It cannot be nil in the internal state.
DownCommand *string `json:"down_command"`
// ListeningPort is the port traffic would be redirected to from the
// forwarded port. The redirection is disabled if it is set to 0, which
// is its default as well.
@@ -84,6 +92,8 @@ func (p *PortForwarding) Copy() (copied PortForwarding) {
Enabled: gosettings.CopyPointer(p.Enabled),
Provider: gosettings.CopyPointer(p.Provider),
Filepath: gosettings.CopyPointer(p.Filepath),
UpCommand: gosettings.CopyPointer(p.UpCommand),
DownCommand: gosettings.CopyPointer(p.DownCommand),
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
Username: p.Username,
Password: p.Password,
@@ -94,6 +104,8 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
@@ -103,6 +115,8 @@ func (p *PortForwarding) setDefaults() {
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
p.Provider = gosettings.DefaultPointer(p.Provider, "")
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
}
@@ -135,6 +149,13 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
}
node.Appendf("Forwarded port file path: %s", filepath)
if *p.UpCommand != "" {
node.Appendf("Forwarded port up command: %s", *p.UpCommand)
}
if *p.DownCommand != "" {
node.Appendf("Forwarded port down command: %s", *p.DownCommand)
}
if p.Username != "" {
credentialsNode := node.Appendf("Credentials:")
credentialsNode.Appendf("Username: %s", p.Username)
@@ -163,6 +184,12 @@ func (p *PortForwarding) read(r *reader.Reader) (err error) {
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
))
p.UpCommand = r.Get("VPN_PORT_FORWARDING_UP_COMMAND",
reader.ForceLowercase(false))
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
reader.ForceLowercase(false))
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
if err != nil {
return err
@@ -48,6 +48,10 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
}
if p.Name == providers.Mullvad && vpnType == vpn.OpenVPN {
warner.Warn("https://mullvad.net/en/blog/removing-openvpn-15th-january-2026")
}
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
if err != nil {
return fmt.Errorf("server selection: %w", err)
+40 -3
View File
@@ -1,11 +1,14 @@
package settings
import (
"bytes"
"encoding/json"
"fmt"
"net"
"os"
"strconv"
"github.com/qdm12/gluetun/internal/server/middlewares/auth"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
@@ -24,6 +27,9 @@ type ControlServer struct {
// It cannot be empty in the internal state and defaults to
// /gluetun/auth/config.toml.
AuthFilePath string
// AuthDefaultRole is a JSON encoded object defining the default role
// that applies to all routes without a previously user-defined role assigned to.
AuthDefaultRole string
}
func (c ControlServer) validate() (err error) {
@@ -44,14 +50,30 @@ func (c ControlServer) validate() (err error) {
ErrControlServerPrivilegedPort, port, uid)
}
jsonDecoder := json.NewDecoder(bytes.NewBufferString(c.AuthDefaultRole))
jsonDecoder.DisallowUnknownFields()
var role auth.Role
err = jsonDecoder.Decode(&role)
if err != nil {
return fmt.Errorf("default authentication role is not valid JSON: %w", err)
}
if role.Auth != "" {
err = role.Validate()
if err != nil {
return fmt.Errorf("default authentication role is not valid: %w", err)
}
}
return nil
}
func (c *ControlServer) copy() (copied ControlServer) {
return ControlServer{
Address: gosettings.CopyPointer(c.Address),
Log: gosettings.CopyPointer(c.Log),
AuthFilePath: c.AuthFilePath,
Address: gosettings.CopyPointer(c.Address),
Log: gosettings.CopyPointer(c.Log),
AuthFilePath: c.AuthFilePath,
AuthDefaultRole: c.AuthDefaultRole,
}
}
@@ -62,12 +84,21 @@ func (c *ControlServer) overrideWith(other ControlServer) {
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
c.AuthDefaultRole = gosettings.OverrideWithComparable(c.AuthDefaultRole, other.AuthDefaultRole)
}
func (c *ControlServer) setDefaults() {
c.Address = gosettings.DefaultPointer(c.Address, ":8000")
c.Log = gosettings.DefaultPointer(c.Log, true)
c.AuthFilePath = gosettings.DefaultComparable(c.AuthFilePath, "/gluetun/auth/config.toml")
c.AuthDefaultRole = gosettings.DefaultComparable(c.AuthDefaultRole, "{}")
if c.AuthDefaultRole != "{}" {
var role auth.Role
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
role.Name = "default"
roleBytes, _ := json.Marshal(role) //nolint:errchkjson
c.AuthDefaultRole = string(roleBytes)
}
}
func (c ControlServer) String() string {
@@ -79,6 +110,11 @@ func (c ControlServer) toLinesNode() (node *gotree.Node) {
node.Appendf("Listening address: %s", *c.Address)
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
node.Appendf("Authentication file path: %s", c.AuthFilePath)
if c.AuthDefaultRole != "{}" {
var role auth.Role
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
node.AppendNode(role.ToLinesNode())
}
return node
}
@@ -91,6 +127,7 @@ func (c *ControlServer) read(r *reader.Reader) (err error) {
c.Address = r.Get("HTTP_CONTROL_SERVER_ADDRESS")
c.AuthFilePath = r.String("HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH")
c.AuthDefaultRole = r.String("HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE", reader.ForceLowercase(false))
return nil
}
@@ -3,7 +3,6 @@ package settings
import (
"errors"
"fmt"
"net/netip"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
@@ -17,17 +16,11 @@ import (
"github.com/qdm12/gotree"
)
type ServerSelection struct { //nolint:maligned
type ServerSelection struct {
// VPN is the VPN type which can be 'openvpn'
// or 'wireguard'. It cannot be the empty string
// in the internal state.
VPN string `json:"vpn"`
// TargetIP is the server endpoint IP address to use.
// It will override any IP address from the picked
// built-in server. It cannot be the empty value in the internal
// state, and can be set to the unspecified address to indicate
// there is not target IP address to use.
TargetIP netip.Addr `json:"target_ip"`
// Countries is the list of countries to filter VPN servers with.
Countries []string `json:"countries"`
// Categories is the list of categories to filter VPN servers with.
@@ -299,7 +292,6 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string)
func (ss *ServerSelection) copy() (copied ServerSelection) {
return ServerSelection{
VPN: ss.VPN,
TargetIP: ss.TargetIP,
Countries: gosettings.CopySlice(ss.Countries),
Categories: gosettings.CopySlice(ss.Categories),
Regions: gosettings.CopySlice(ss.Regions),
@@ -323,7 +315,6 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
func (ss *ServerSelection) overrideWith(other ServerSelection) {
ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN)
ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP)
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions)
@@ -346,7 +337,6 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) {
ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
ss.TargetIP = gosettings.DefaultValidator(ss.TargetIP, netip.IPv4Unspecified())
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
@@ -354,11 +344,8 @@ func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
defaultPortForwardOnly := false
if portForwardingEnabled && helpers.IsOneOf(vpnProvider,
providers.PrivateInternetAccess, providers.Protonvpn) {
defaultPortForwardOnly = true
}
defaultPortForwardOnly := portForwardingEnabled &&
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
ss.OpenVPN.setDefaults(vpnProvider)
ss.Wireguard.setDefaults()
@@ -371,9 +358,6 @@ func (ss ServerSelection) String() string {
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("Server selection settings:")
node.Appendf("VPN type: %s", ss.VPN)
if !ss.TargetIP.IsUnspecified() {
node.Appendf("Target IP address: %s", ss.TargetIP)
}
if len(ss.Countries) > 0 {
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
@@ -464,12 +448,6 @@ func (ss *ServerSelection) read(r *reader.Reader,
) (err error) {
ss.VPN = vpnType
ss.TargetIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
if err != nil {
return err
}
countriesRetroKeys := []string{"COUNTRY"}
if vpnProvider == providers.Cyberghost {
countriesRetroKeys = append(countriesRetroKeys, "REGION")
+4 -4
View File
@@ -177,10 +177,10 @@ func (s Settings) Warnings() (warnings []string) {
// TODO remove in v4
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
" so the DNS over TLS (DoT) server will not be used."+
" The default value changed to 127.0.0.1 so it uses the internal DoT serves."+
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server"+
" corresponding to the first DoT provider chosen is used.")
" so the local forwarding DNS server will not be used."+
" The default value changed to 127.0.0.1 so it uses the internal DNS server."+
" If this server fails to start, the IPv4 address of the first plaintext DNS server"+
" corresponding to the first DNS provider chosen is used.")
}
return warnings
@@ -40,30 +40,31 @@ func Test_Settings_String(t *testing.T) {
DNS settings:
| Keep existing nameserver(s): no
| DNS server address to use: 127.0.0.1
| DNS over TLS settings:
| Enabled: yes
| Update period: every 24h0m0s
| Upstream resolvers:
| | Cloudflare
| Caching: yes
| IPv6: no
| DNS filtering settings:
| Block malicious: yes
| Block ads: no
| Block surveillance: yes
| DNS forwarder server enabled: yes
| Upstream resolver type: dot
| Upstream resolvers:
| | Cloudflare
| Caching: yes
| IPv6: no
| Update period: every 24h0m0s
| DNS filtering settings:
| Block malicious: yes
| Block ads: no
| Block surveillance: yes
Firewall settings:
| Enabled: yes
Log settings:
| Log level: INFO
Health settings:
| Server listening address: 127.0.0.1:9999
| Target address: cloudflare.com:443
| Duration to wait after success: 5s
| Read header timeout: 100ms
| Read timeout: 500ms
| VPN wait durations:
| Initial duration: 6s
| Additional duration: 5s
| Target addresses:
| | cloudflare.com:443
| | github.com:443
| Small health check type: ICMP echo request
| | ICMP target IPs:
| | 1.1.1.1
| | 8.8.8.8
| Restart VPN on healthcheck failure: yes
Shadowsocks server settings:
| Enabled: no
HTTP proxy settings:
@@ -15,7 +15,7 @@ type Shadowsocks struct {
// It defaults to false, and cannot be nil in the internal state.
Enabled *bool
// Settings are settings for the TCP+UDP server.
tcpudp.Settings
Settings tcpudp.Settings
}
func (s Shadowsocks) validate() (err error) {
+43 -4
View File
@@ -2,6 +2,7 @@ package settings
import (
"fmt"
"slices"
"strings"
"time"
@@ -31,6 +32,10 @@ type Updater struct {
// Providers is the list of VPN service providers
// to update server information for.
Providers []string
// ProtonEmail is the email to authenticate with the Proton API.
ProtonEmail *string
// ProtonPassword is the password to authenticate with the Proton API.
ProtonPassword *string
}
func (u Updater) Validate() (err error) {
@@ -51,6 +56,18 @@ func (u Updater) Validate() (err error) {
if err != nil {
return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err)
}
if provider == providers.Protonvpn {
authenticatedAPI := *u.ProtonEmail != "" || *u.ProtonPassword != ""
if authenticatedAPI {
switch {
case *u.ProtonEmail == "":
return fmt.Errorf("%w", ErrUpdaterProtonEmailMissing)
case *u.ProtonPassword == "":
return fmt.Errorf("%w", ErrUpdaterProtonPasswordMissing)
}
}
}
}
return nil
@@ -58,10 +75,12 @@ func (u Updater) Validate() (err error) {
func (u *Updater) copy() (copied Updater) {
return Updater{
Period: gosettings.CopyPointer(u.Period),
DNSAddress: u.DNSAddress,
MinRatio: u.MinRatio,
Providers: gosettings.CopySlice(u.Providers),
Period: gosettings.CopyPointer(u.Period),
DNSAddress: u.DNSAddress,
MinRatio: u.MinRatio,
Providers: gosettings.CopySlice(u.Providers),
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
ProtonPassword: gosettings.CopyPointer(u.ProtonPassword),
}
}
@@ -73,6 +92,8 @@ func (u *Updater) overrideWith(other Updater) {
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail)
u.ProtonPassword = gosettings.OverrideWithPointer(u.ProtonPassword, other.ProtonPassword)
}
func (u *Updater) SetDefaults(vpnProvider string) {
@@ -87,6 +108,10 @@ func (u *Updater) SetDefaults(vpnProvider string) {
if len(u.Providers) == 0 && vpnProvider != providers.Custom {
u.Providers = []string{vpnProvider}
}
// Set these to empty strings to avoid nil pointer panics
u.ProtonEmail = gosettings.DefaultPointer(u.ProtonEmail, "")
u.ProtonPassword = gosettings.DefaultPointer(u.ProtonPassword, "")
}
func (u Updater) String() string {
@@ -103,6 +128,10 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
node.Appendf("DNS address: %s", u.DNSAddress)
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
if slices.Contains(u.Providers, providers.Protonvpn) {
node.Appendf("Proton API email: %s", *u.ProtonEmail)
node.Appendf("Proton API password: %s", gosettings.ObfuscateKey(*u.ProtonPassword))
}
return node
}
@@ -125,6 +154,16 @@ func (u *Updater) read(r *reader.Reader) (err error) {
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
u.ProtonEmail = r.Get("UPDATER_PROTONVPN_EMAIL")
if u.ProtonEmail == nil {
protonUsername := r.String("UPDATER_PROTONVPN_USERNAME", reader.IsRetro("UPDATER_PROTONVPN_EMAIL"))
if protonUsername != "" {
protonEmail := protonUsername + "@protonmail.com"
u.ProtonEmail = &protonEmail
}
}
u.ProtonPassword = r.Get("UPDATER_PROTONVPN_PASSWORD")
return nil
}
+8 -4
View File
@@ -39,10 +39,14 @@ type Wireguard struct {
PersistentKeepaliveInterval *time.Duration `json:"persistent_keep_alive_interval"`
// Maximum Transmission Unit (MTU) of the Wireguard interface.
// It cannot be zero in the internal state, and defaults to
// 1400. Note it is not the wireguard-go MTU default of 1420
// 1320. Note it is not the wireguard-go MTU default of 1420
// because this impacts bandwidth a lot on some VPN providers,
// see https://github.com/qdm12/gluetun/issues/1650.
MTU uint16 `json:"mtu"`
// It has been lowered to 1320 following quite a bit of
// investigation in the issue:
// https://github.com/qdm12/gluetun/issues/2533.
// Note this should now be replaced with the PMTUD feature.
MTU uint32 `json:"mtu"`
// Implementation is the Wireguard implementation to use.
// It can be "auto", "userspace" or "kernelspace".
// It defaults to "auto" and cannot be the empty string
@@ -191,7 +195,7 @@ func (w *Wireguard) setDefaults(vpnProvider string) {
w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs)
w.PersistentKeepaliveInterval = gosettings.DefaultPointer(w.PersistentKeepaliveInterval, 0)
w.Interface = gosettings.DefaultComparable(w.Interface, "wg0")
const defaultMTU = 1400
const defaultMTU = 1320
w.MTU = gosettings.DefaultComparable(w.MTU, defaultMTU)
w.Implementation = gosettings.DefaultComparable(w.Implementation, "auto")
}
@@ -269,7 +273,7 @@ func (w *Wireguard) read(r *reader.Reader) (err error) {
return err
}
mtuPtr, err := r.Uint16Ptr("WIREGUARD_MTU")
mtuPtr, err := r.Uint32Ptr("WIREGUARD_MTU")
if err != nil {
return err
} else if mtuPtr != nil {
@@ -14,11 +14,11 @@ import (
type WireguardSelection struct {
// EndpointIP is the server endpoint IP address.
// It is only used with VPN providers generating Wireguard
// configurations specific to each server and user.
// To indicate it should not be used, it should be set
// to netip.IPv4Unspecified(). It can never be the zero value
// in the internal state.
// It is notably required with the custom provider.
// Otherwise it overrides any IP address from the picked
// built-in server connection. To indicate it should
// not be used, it should be set to [netip.IPv4Unspecified].
// It can never be the zero value in the internal state.
EndpointIP netip.Addr `json:"endpoint_ip"`
// EndpointPort is a the server port to use for the VPN server.
// It is optional for VPN providers IVPN, Mullvad, Surfshark
@@ -155,7 +155,8 @@ func (w WireguardSelection) toLinesNode() (node *gotree.Node) {
func (w *WireguardSelection) read(r *reader.Reader) (err error) {
w.EndpointIP, err = r.NetipAddr("WIREGUARD_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
if err != nil {
return err
return fmt.Errorf("%w - note this MUST be an IP address, "+
"see https://github.com/qdm12/gluetun/issues/788", err)
}
w.EndpointPort, err = r.Uint16Ptr("WIREGUARD_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
@@ -27,7 +27,6 @@ const (
VPNSecure = "vpnsecure"
VPNUnlimited = "vpn unlimited"
Vyprvpn = "vyprvpn"
Wevpn = "wevpn"
Windscribe = "windscribe"
)
@@ -56,7 +55,6 @@ func All() []string {
VPNSecure,
VPNUnlimited,
Vyprvpn,
Wevpn,
Windscribe,
}
}
+17
View File
@@ -0,0 +1,17 @@
package dns
import (
"context"
"net/netip"
)
type Logger interface {
Debug(s string)
Info(s string)
Warn(s string)
Error(s string)
}
type Firewall interface {
RestrictOutputAddrPort(ctx context.Context, addrPort netip.AddrPort) (err error)
}
-8
View File
@@ -1,8 +0,0 @@
package dns
type Logger interface {
Debug(s string)
Info(s string)
Warn(s string)
Error(s string)
}
+36 -18
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"net/netip"
"time"
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
@@ -16,28 +17,30 @@ import (
)
type Loop struct {
statusManager *loopstate.State
state *state.State
server *server.Server
filter *mapfilter.Filter
resolvConf string
client *http.Client
logger Logger
userTrigger bool
start <-chan struct{}
running chan<- models.LoopStatus
stop <-chan struct{}
stopped chan<- struct{}
updateTicker <-chan struct{}
backoffTime time.Duration
timeNow func() time.Time
timeSince func(time.Time) time.Duration
statusManager *loopstate.State
state *state.State
server *server.Server
filter *mapfilter.Filter
localResolvers []netip.Addr
resolvConf string
client *http.Client
firewall Firewall
logger Logger
userTrigger bool
start <-chan struct{}
running chan<- models.LoopStatus
stop <-chan struct{}
stopped chan<- struct{}
updateTicker <-chan struct{}
backoffTime time.Duration
timeNow func() time.Time
timeSince func(time.Time) time.Duration
}
const defaultBackoffTime = 10 * time.Second
func NewLoop(settings settings.DNS,
client *http.Client, logger Logger,
client *http.Client, firewall Firewall, logger Logger,
) (loop *Loop, err error) {
start := make(chan struct{})
running := make(chan models.LoopStatus)
@@ -48,7 +51,9 @@ func NewLoop(settings settings.DNS,
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
state := state.New(statusManager, settings, updateTicker)
filter, err := mapfilter.New(mapfilter.Settings{})
filter, err := mapfilter.New(mapfilter.Settings{
Logger: buildFilterLogger(logger),
})
if err != nil {
return nil, fmt.Errorf("creating map filter: %w", err)
}
@@ -60,6 +65,7 @@ func NewLoop(settings settings.DNS,
filter: filter,
resolvConf: "/etc/resolv.conf",
client: client,
firewall: firewall,
logger: logger,
userTrigger: true,
start: start,
@@ -100,3 +106,15 @@ func (l *Loop) signalOrSetStatus(status models.LoopStatus) {
l.statusManager.SetStatus(status)
}
}
type filterLogger struct {
logger Logger
}
func (l *filterLogger) Log(msg string) {
l.logger.Debug(msg)
}
func buildFilterLogger(logger Logger) *filterLogger {
return &filterLogger{logger: logger}
}
+13 -13
View File
@@ -1,24 +1,17 @@
package dns
import (
"context"
"net/netip"
"time"
"github.com/qdm12/dns/v2/pkg/nameserver"
)
func (l *Loop) useUnencryptedDNS(fallback bool) {
func (l *Loop) useUnencryptedDNS(ctx context.Context, fallback bool) {
settings := l.GetSettings()
// Try with user provided plaintext ip address
// if it's not 127.0.0.1 (default for DoT), otherwise
// use the first DoT provider ipv4 address found.
var targetIP netip.Addr
if settings.ServerAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
targetIP = settings.ServerAddress
} else {
targetIP = settings.DoT.GetFirstPlaintextIPv4()
}
targetIP := settings.GetFirstPlaintextIPv4()
if fallback {
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
@@ -27,18 +20,25 @@ func (l *Loop) useUnencryptedDNS(fallback bool) {
}
const dialTimeout = 3 * time.Second
const defaultDNSPort = 53
addrPort := netip.AddrPortFrom(targetIP, defaultDNSPort)
settingsInternalDNS := nameserver.SettingsInternalDNS{
IP: targetIP,
Timeout: dialTimeout,
AddrPort: addrPort,
Timeout: dialTimeout,
}
nameserver.UseDNSInternally(settingsInternalDNS)
settingsSystemWide := nameserver.SettingsSystemDNS{
IP: targetIP,
IPs: []netip.Addr{targetIP},
ResolvPath: l.resolvConf,
}
err := nameserver.UseDNSSystemWide(settingsSystemWide)
if err != nil {
l.logger.Error(err.Error())
}
err = l.firewall.RestrictOutputAddrPort(ctx, addrPort)
if err != nil {
l.logger.Error("restricting plain DNS traffic to " + targetIP.String() + ": " + err.Error())
}
}
+28 -14
View File
@@ -4,19 +4,27 @@ import (
"context"
"errors"
"github.com/qdm12/dns/v2/pkg/nameserver"
"github.com/qdm12/gluetun/internal/constants"
)
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
defer close(done)
var err error
l.localResolvers, err = nameserver.GetPrivateDNSServers()
if err != nil {
l.logger.Error("getting private DNS servers: " + err.Error())
return
}
if *l.GetSettings().KeepNameserver {
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " +
"this will likely leak DNS traffic outside the VPN " +
"and go through your container network DNS outside the VPN tunnel!")
} else {
const fallback = false
l.useUnencryptedDNS(fallback)
l.useUnencryptedDNS(ctx, fallback)
}
select {
@@ -26,18 +34,17 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
}
for ctx.Err() == nil {
// Upper scope variables for the DNS over TLS server only
// Upper scope variables for the DNS forwarder server only
// Their values are to be used if DOT=off
var runError <-chan error
settings := l.GetSettings()
for !*settings.KeepNameserver && *settings.DoT.Enabled {
for !*settings.KeepNameserver && *settings.ServerEnabled {
var err error
runError, err = l.setupServer(ctx)
if err == nil {
l.backoffTime = defaultBackoffTime
l.logger.Info("ready")
l.signalOrSetStatus(constants.Running)
break
}
@@ -49,16 +56,17 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
if !errors.Is(err, errUpdateBlockLists) {
const fallback = true
l.useUnencryptedDNS(fallback)
l.useUnencryptedDNS(ctx, fallback)
}
l.logAndWait(ctx, err)
settings = l.GetSettings()
}
l.signalOrSetStatus(constants.Running)
settings = l.GetSettings()
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
if !*settings.KeepNameserver && !*settings.ServerEnabled {
const fallback = false
l.useUnencryptedDNS(fallback)
l.useUnencryptedDNS(ctx, fallback)
}
l.userTrigger = false
@@ -74,15 +82,21 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
for {
select {
case <-ctx.Done():
l.stopServer()
// TODO revert OS and Go nameserver when exiting
settings := l.GetSettings()
if !*settings.KeepNameserver && *settings.ServerEnabled {
l.stopServer()
// TODO revert OS and Go nameserver when exiting
}
return true
case <-l.stop:
l.userTrigger = true
l.logger.Info("stopping")
const fallback = false
l.useUnencryptedDNS(fallback)
l.stopServer()
settings := l.GetSettings()
if !*settings.KeepNameserver && *settings.ServerEnabled {
const fallback = false
l.useUnencryptedDNS(ctx, fallback)
l.stopServer()
}
l.stopped <- struct{}{}
case <-l.start:
l.userTrigger = true
@@ -91,7 +105,7 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
case err := <-runError: // unexpected error
l.statusManager.SetStatus(constants.Crashed)
const fallback = true
l.useUnencryptedDNS(fallback)
l.useUnencryptedDNS(ctx, fallback)
l.logAndWait(ctx, err)
return false
}
@@ -101,6 +115,6 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
func (l *Loop) stopServer() {
stopErr := l.server.Stop()
if stopErr != nil {
l.logger.Error("stopping DoT server: " + stopErr.Error())
l.logger.Error("stopping server: " + stopErr.Error())
}
}
+64 -14
View File
@@ -3,12 +3,16 @@ package dns
import (
"context"
"fmt"
"net/netip"
"github.com/qdm12/dns/v2/pkg/doh"
"github.com/qdm12/dns/v2/pkg/dot"
cachemiddleware "github.com/qdm12/dns/v2/pkg/middlewares/cache"
"github.com/qdm12/dns/v2/pkg/middlewares/cache/lru"
filtermiddleware "github.com/qdm12/dns/v2/pkg/middlewares/filter"
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
"github.com/qdm12/dns/v2/pkg/middlewares/localdns"
"github.com/qdm12/dns/v2/pkg/plain"
"github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/dns/v2/pkg/server"
"github.com/qdm12/gluetun/internal/configuration/settings"
@@ -22,34 +26,63 @@ func (l *Loop) SetSettings(ctx context.Context, settings settings.DNS) (
return l.state.SetSettings(ctx, settings)
}
func buildDoTSettings(settings settings.DNS,
filter *mapfilter.Filter, logger Logger) (
func buildServerSettings(settings settings.DNS,
filter *mapfilter.Filter, localResolvers []netip.Addr,
logger Logger) (
serverSettings server.Settings, err error,
) {
serverSettings.Logger = logger
var dotSettings dot.Settings
dotSettings.Warner = logger
providersData := provider.NewProviders()
dotSettings.UpstreamResolvers = make([]provider.Provider, len(settings.DoT.Providers))
for i := range settings.DoT.Providers {
upstreamResolvers := make([]provider.Provider, len(settings.Providers))
for i := range settings.Providers {
var err error
dotSettings.UpstreamResolvers[i], err = providersData.Get(settings.DoT.Providers[i])
upstreamResolvers[i], err = providersData.Get(settings.Providers[i])
if err != nil {
panic(err) // this should already had been checked
}
}
dotSettings.IPVersion = "ipv4"
if *settings.DoT.IPv6 {
dotSettings.IPVersion = "ipv6"
ipVersion := "ipv4"
if *settings.IPv6 {
ipVersion = "ipv6"
}
serverSettings.Dialer, err = dot.New(dotSettings)
if err != nil {
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
var dialer server.Dialer
switch settings.UpstreamType {
case "dot":
dialerSettings := dot.Settings{
UpstreamResolvers: upstreamResolvers,
IPVersion: ipVersion,
}
dialer, err = dot.New(dialerSettings)
if err != nil {
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
}
case "doh":
dialerSettings := doh.Settings{
UpstreamResolvers: upstreamResolvers,
IPVersion: ipVersion,
}
dialer, err = doh.New(dialerSettings)
if err != nil {
return server.Settings{}, fmt.Errorf("creating DNS over HTTPS dialer: %w", err)
}
case "plain":
dialerSettings := plain.Settings{
UpstreamResolvers: upstreamResolvers,
IPVersion: ipVersion,
}
dialer, err = plain.New(dialerSettings)
if err != nil {
return server.Settings{}, fmt.Errorf("creating plain DNS dialer: %w", err)
}
default:
panic("unknown upstream type: " + settings.UpstreamType)
}
serverSettings.Dialer = dialer
if *settings.DoT.Caching {
if *settings.Caching {
lruCache, err := lru.New(lru.Settings{})
if err != nil {
return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err)
@@ -71,5 +104,22 @@ func buildDoTSettings(settings settings.DNS,
}
serverSettings.Middlewares = append(serverSettings.Middlewares, filterMiddleware)
localResolversAddrPorts := make([]netip.AddrPort, len(localResolvers))
const defaultDNSPort = 53
for i, addr := range localResolvers {
localResolversAddrPorts[i] = netip.AddrPortFrom(addr, defaultDNSPort)
}
localDNSMiddleware, err := localdns.New(localdns.Settings{
Resolvers: localResolversAddrPorts, // auto-detected at container start only
Logger: logger,
})
if err != nil {
return server.Settings{}, fmt.Errorf("creating local DNS middleware: %w", err)
}
// Place after cache middleware, since we want to avoid caching for local
// hostnames that may change regularly.
// Place after filter middleware to avoid conflicts with the rebinding protection.
serverSettings.Middlewares = append(serverSettings.Middlewares, localDNSMiddleware)
return serverSettings, nil
}
+14 -6
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"net/netip"
"github.com/qdm12/dns/v2/pkg/check"
"github.com/qdm12/dns/v2/pkg/nameserver"
@@ -20,14 +21,14 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
settings := l.GetSettings()
dotSettings, err := buildDoTSettings(settings, l.filter, l.logger)
serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.logger)
if err != nil {
return nil, fmt.Errorf("building DoT settings: %w", err)
return nil, fmt.Errorf("building server settings: %w", err)
}
server, err := server.New(dotSettings)
server, err := server.New(serverSettings)
if err != nil {
return nil, fmt.Errorf("creating DoT server: %w", err)
return nil, fmt.Errorf("creating server: %w", err)
}
runError, err = server.Start(ctx)
@@ -37,17 +38,24 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
l.server = server
// use internal DNS server
const defaultDNSPort = 53
addrPort := netip.AddrPortFrom(settings.ServerAddress, defaultDNSPort)
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
IP: settings.ServerAddress,
AddrPort: addrPort,
})
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
IP: settings.ServerAddress,
IPs: []netip.Addr{settings.ServerAddress},
ResolvPath: l.resolvConf,
})
if err != nil {
l.logger.Error(err.Error())
}
err = l.firewall.RestrictOutputAddrPort(ctx, addrPort)
if err != nil {
l.logger.Error("restricting plain DNS traffic to " + addrPort.Addr().String() + ": " + err.Error())
}
err = check.WaitForDNS(ctx, check.Settings{})
if err != nil {
l.stopServer()
+2 -2
View File
@@ -27,7 +27,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
// Check for only update period change
tempSettings := s.settings.Copy()
*tempSettings.DoT.UpdatePeriod = *settings.DoT.UpdatePeriod
*tempSettings.UpdatePeriod = *settings.UpdatePeriod
onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings)
s.settings = settings
@@ -40,7 +40,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
// Restart
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
if *settings.DoT.Enabled {
if *settings.ServerEnabled {
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
}
return outcome
+5 -3
View File
@@ -14,7 +14,7 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
timer.Stop()
timerIsStopped := true
settings := l.GetSettings()
if period := *settings.DoT.UpdatePeriod; period > 0 {
if period := *settings.UpdatePeriod; period > 0 {
timer.Reset(period)
timerIsStopped = false
}
@@ -35,6 +35,8 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
l.statusManager.SetStatus(constants.Crashed)
l.logger.Error(err.Error())
l.logger.Warn("skipping DNS server restart due to failed files update")
settings := l.GetSettings()
timer.Reset(*settings.UpdatePeriod)
continue
}
}
@@ -43,14 +45,14 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
settings := l.GetSettings()
timer.Reset(*settings.DoT.UpdatePeriod)
timer.Reset(*settings.UpdatePeriod)
case <-l.updateTicker:
if !timer.Stop() {
<-timer.C
}
timerIsStopped = true
settings := l.GetSettings()
newUpdatePeriod := *settings.DoT.UpdatePeriod
newUpdatePeriod := *settings.UpdatePeriod
if newUpdatePeriod == 0 {
continue
}
+2 -1
View File
@@ -12,7 +12,7 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) {
settings := l.GetSettings()
l.logger.Info("downloading hostnames and IP block lists")
blacklistSettings := settings.DoT.Blacklist.ToBlockBuilderSettings(l.client)
blacklistSettings := settings.Blacklist.ToBlockBuilderSettings(l.client)
blockBuilder, err := blockbuilder.New(blacklistSettings)
if err != nil {
@@ -37,6 +37,7 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) {
IPPrefixes: result.BlockedIPPrefixes,
}
updateSettings.BlockHostnames(result.BlockedHostnames)
updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames)
err = l.filter.Update(updateSettings)
if err != nil {
return fmt.Errorf("updating filter: %w", err)
+1 -1
View File
@@ -16,7 +16,7 @@ func isDeleteMatchInstruction(instruction string) bool {
fields := strings.Fields(instruction)
for i, field := range fields {
switch {
case field != "-D" && field != "--delete": //nolint:goconst
case field != "-D" && field != "--delete":
continue
case i == len(fields)-1: // malformed: missing chain name
return false
+2 -2
View File
@@ -69,8 +69,8 @@ func Test_deleteIPTablesRule(t *testing.T) {
"invalid_instruction": {
instruction: "invalid",
errWrapped: ErrIptablesCommandMalformed,
errMessage: "parsing iptables command: iptables command is malformed: " +
"fields count 1 is not even: \"invalid\"",
errMessage: "parsing iptables command: parsing \"invalid\": " +
"iptables command is malformed: flag \"invalid\" requires a value, but got none",
},
"list_error": {
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678",
+3 -1
View File
@@ -9,7 +9,7 @@ import (
"github.com/qdm12/gluetun/internal/routing"
)
type Config struct { //nolint:maligned
type Config struct {
runner CmdRunner
logger Logger
iptablesMutex sync.Mutex
@@ -29,6 +29,7 @@ type Config struct { //nolint:maligned
outboundSubnets []netip.Prefix
allowedInputPorts map[uint16]map[string]struct{} // port to interfaces set mapping
portRedirections portRedirections
outputAddrPort map[uint16]netip.Addr
stateMutex sync.Mutex
}
@@ -52,6 +53,7 @@ func NewConfig(ctx context.Context, logger Logger,
runner: runner,
logger: logger,
allowedInputPorts: make(map[uint16]map[string]struct{}),
outputAddrPort: make(map[uint16]netip.Addr),
ipTables: iptables,
ip6Tables: ip6tables,
customRulesPath: "/iptables/post-rules.txt",
+1 -1
View File
@@ -58,7 +58,7 @@ var ErrPolicyNotValid = errors.New("policy is not valid")
func (c *Config) setIPv6AllPolicies(ctx context.Context, policy string) error {
switch policy {
case "ACCEPT", "DROP": //nolint:goconst
case "ACCEPT", "DROP":
default:
return fmt.Errorf("%w: %s", ErrPolicyNotValid, policy)
}
+1 -1
View File
@@ -149,7 +149,7 @@ func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
) error {
protocol := connection.Protocol
if protocol == "tcp-client" {
protocol = "tcp" //nolint:goconst
protocol = "tcp"
}
instruction := fmt.Sprintf("%s OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT",
appendOrDelete(remove), connection.IP, defaultInterface, protocol,
+13
View File
@@ -2,6 +2,7 @@ package firewall
import (
"context"
"fmt"
)
func (c *Config) runMixedIptablesInstructions(ctx context.Context, instructions []string) error {
@@ -19,3 +20,15 @@ func (c *Config) runMixedIptablesInstruction(ctx context.Context, instruction st
}
return c.runIP6tablesInstruction(ctx, instruction)
}
func (c *Config) runIPv4AndV6IptablesInstructions(ctx context.Context,
ipv4Instructions, ipv6Instructions []string,
) error {
if err := c.runIptablesInstructions(ctx, ipv4Instructions); err != nil {
return fmt.Errorf("running iptables instructions: %w", err)
}
if err := c.runIP6tablesInstructions(ctx, ipv6Instructions); err != nil {
return fmt.Errorf("running ip6tables instructions: %w", err)
}
return nil
}
+6 -4
View File
@@ -22,7 +22,7 @@ type chainRule struct {
packets uint64
bytes uint64
target string // "ACCEPT", "DROP", "REJECT" or "REDIRECT"
protocol string // "tcp", "udp" or "" for all protocols.
protocol string // "icmp", "tcp", "udp" or "" for all protocols.
inputInterface string // input interface, for example "tun0" or "*""
outputInterface string // output interface, for example "eth0" or "*""
source netip.Prefix // source IP CIDR, for example 0.0.0.0/0. Must be valid.
@@ -323,10 +323,12 @@ var ErrProtocolUnknown = errors.New("unknown protocol")
func parseProtocol(s string) (protocol string, err error) {
switch s {
case "0":
case "6":
case "0", "all":
case "1", "icmp":
protocol = "icmp"
case "6", "tcp":
protocol = "tcp"
case "17":
case "17", "udp":
protocol = "udp"
default:
return "", fmt.Errorf("%w: %s", ErrProtocolUnknown, s)
+25 -1
View File
@@ -56,7 +56,9 @@ num pkts bytes target prot opt in out source destinati
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT 17 -- tun0 * 0.0.0.0/0 0.0.0.0/0 udp dpt:55405
2 0 0 ACCEPT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:55405
3 0 0 DROP 0 -- tun0 * 1.2.3.4 0.0.0.0/0
3 0 0 ACCEPT 1 -- tun0 * 0.0.0.0/0 0.0.0.0/0
4 0 0 DROP 0 -- tun0 * 1.2.3.4 0.0.0.0/0
5 0 0 ACCEPT all -- tun0 * 1.2.3.4 0.0.0.0/0
`,
table: chain{
name: "INPUT",
@@ -92,6 +94,17 @@ num pkts bytes target prot opt in out source destinati
lineNumber: 3,
packets: 0,
bytes: 0,
target: "ACCEPT",
protocol: "icmp",
inputInterface: "tun0",
outputInterface: "*",
source: netip.MustParsePrefix("0.0.0.0/0"),
destination: netip.MustParsePrefix("0.0.0.0/0"),
},
{
lineNumber: 4,
packets: 0,
bytes: 0,
target: "DROP",
protocol: "",
inputInterface: "tun0",
@@ -99,6 +112,17 @@ num pkts bytes target prot opt in out source destinati
source: netip.MustParsePrefix("1.2.3.4/32"),
destination: netip.MustParsePrefix("0.0.0.0/0"),
},
{
lineNumber: 5,
packets: 0,
bytes: 0,
target: "ACCEPT",
protocol: "",
inputInterface: "tun0",
outputInterface: "*",
source: netip.MustParsePrefix("1.2.3.4/32"),
destination: netip.MustParsePrefix("0.0.0.0/0"),
},
},
},
},
+111 -19
View File
@@ -9,9 +9,19 @@ import (
"strings"
)
type operation uint8
const (
opNone operation = iota
opAppend
opDelete
opInsert
opReplace
)
type iptablesInstruction struct {
table string // defaults to "filter", and can be "nat" for example.
append bool
operation operation
chain string // for example INPUT, PREROUTING. Cannot be empty.
target string // for example ACCEPT. Can be empty.
protocol string // "tcp" or "udp" or "" for all protocols.
@@ -22,6 +32,7 @@ type iptablesInstruction struct {
destinationPort uint16 // if zero, there is no destination port
toPorts []uint16 // if empty, there is no redirection
ctstate []string // if empty, there is no ctstate
lineNumber uint16 // for replace operation, the line number to replace
}
func (i *iptablesInstruction) setDefaults() {
@@ -60,6 +71,58 @@ func (i *iptablesInstruction) equalToRule(table, chain string, rule chainRule) (
}
}
func (i *iptablesInstruction) String() string {
var sb strings.Builder
if i.table != "" && i.table != "filter" {
sb.WriteString(fmt.Sprintf("-t %s ", i.table))
}
switch i.operation {
case opNone:
panic("no operation specified")
case opAppend:
sb.WriteString(fmt.Sprintf("--append %s ", i.chain))
case opDelete:
sb.WriteString(fmt.Sprintf("--delete %s ", i.chain))
case opInsert:
sb.WriteString(fmt.Sprintf("--insert %s ", i.chain))
case opReplace:
sb.WriteString(fmt.Sprintf("--replace %s %d ", i.chain, i.lineNumber))
}
if i.inputInterface != "" {
sb.WriteString(fmt.Sprintf("-i %s ", i.inputInterface))
}
if i.outputInterface != "" {
sb.WriteString(fmt.Sprintf("-o %s ", i.outputInterface))
}
if i.protocol != "" {
sb.WriteString(fmt.Sprintf("-p %s ", i.protocol))
}
if i.source.IsValid() {
sb.WriteString(fmt.Sprintf("-s %s ", i.source.String()))
}
if i.destination.IsValid() {
sb.WriteString(fmt.Sprintf("-d %s ", i.destination.String()))
}
if i.destinationPort != 0 {
sb.WriteString(fmt.Sprintf("--dport %d ", i.destinationPort))
}
if len(i.ctstate) > 0 {
sb.WriteString(fmt.Sprintf("--ctstate %s ", strings.Join(i.ctstate, ",")))
}
if len(i.toPorts) > 0 {
var portStrings []string
for _, port := range i.toPorts {
portStrings = append(portStrings, strconv.FormatUint(uint64(port), 10))
}
sb.WriteString(fmt.Sprintf("--to-ports %s ", strings.Join(portStrings, ",")))
}
if i.target != "" {
sb.WriteString(fmt.Sprintf("-j %s ", i.target))
}
return strings.TrimSpace(sb.String())
}
// instruction can be "" which equivalent to the "*" chain rule interface.
func networkInterfacesEqual(instruction, chainRule string) bool {
return instruction == chainRule || (instruction == "" && chainRule == "*")
@@ -77,34 +140,63 @@ func parseIptablesInstruction(s string) (instruction iptablesInstruction, err er
return iptablesInstruction{}, fmt.Errorf("%w: empty instruction", ErrIptablesCommandMalformed)
}
fields := strings.Fields(s)
if len(fields)%2 != 0 {
return iptablesInstruction{}, fmt.Errorf("%w: fields count %d is not even: %q",
ErrIptablesCommandMalformed, len(fields), s)
}
for i := 0; i < len(fields); i += 2 {
key := fields[i]
value := fields[i+1]
err = parseInstructionFlag(key, value, &instruction)
i := 0
for i < len(fields) {
consumed, err := parseInstructionFlag(fields[i:], &instruction)
if err != nil {
return iptablesInstruction{}, fmt.Errorf("parsing %q: %w", s, err)
}
i += consumed
}
instruction.setDefaults()
return instruction, nil
}
func parseInstructionFlag(key, value string, instruction *iptablesInstruction) (err error) {
switch key {
func parseInstructionFlag(fields []string, instruction *iptablesInstruction) (consumed int, err error) {
flag := fields[0]
// All flags use one value after the flag, except the following:
switch flag {
case "-R", "--replace":
const expected = 3
if len(fields) < expected {
return 0, fmt.Errorf("%w: flag %q requires at least 2 values, but got %s",
ErrIptablesCommandMalformed, flag, strings.Join(fields, " "))
}
consumed = expected
default:
const expected = 2
if len(fields) < expected {
return 0, fmt.Errorf("%w: flag %q requires a value, but got none",
ErrIptablesCommandMalformed, flag)
}
consumed = expected
}
value := fields[1]
switch flag {
case "-t", "--table":
instruction.table = value
case "-D", "--delete":
instruction.append = false
instruction.operation = opDelete
instruction.chain = value
case "-A", "--append":
instruction.append = true
instruction.operation = opAppend
instruction.chain = value
case "-I", "--insert":
instruction.operation = opInsert
instruction.chain = value
case "-R", "--replace":
instruction.operation = opReplace
instruction.chain = value
const base, bits = 10, 16
n, err := strconv.ParseUint(fields[2], base, bits)
if err != nil {
return 0, fmt.Errorf("parsing line number for --replace operation: %w", err)
}
instruction.lineNumber = uint16(n)
case "-j", "--jump":
instruction.target = value
case "-p", "--protocol":
@@ -117,18 +209,18 @@ func parseInstructionFlag(key, value string, instruction *iptablesInstruction) (
case "-s", "--source":
instruction.source, err = parseIPPrefix(value)
if err != nil {
return fmt.Errorf("parsing source IP CIDR: %w", err)
return 0, fmt.Errorf("parsing source IP CIDR: %w", err)
}
case "-d", "--destination":
instruction.destination, err = parseIPPrefix(value)
if err != nil {
return fmt.Errorf("parsing destination IP CIDR: %w", err)
return 0, fmt.Errorf("parsing destination IP CIDR: %w", err)
}
case "--dport":
const base, bitLength = 10, 16
destinationPort, err := strconv.ParseUint(value, base, bitLength)
if err != nil {
return fmt.Errorf("parsing destination port: %w", err)
return 0, fmt.Errorf("parsing destination port: %w", err)
}
instruction.destinationPort = uint16(destinationPort)
case "--ctstate":
@@ -140,14 +232,14 @@ func parseInstructionFlag(key, value string, instruction *iptablesInstruction) (
const base, bitLength = 10, 16
port, err := strconv.ParseUint(portString, base, bitLength)
if err != nil {
return fmt.Errorf("parsing port redirection: %w", err)
return 0, fmt.Errorf("parsing port redirection: %w", err)
}
instruction.toPorts[i] = uint16(port)
}
default:
return fmt.Errorf("%w: unknown key %q", ErrIptablesCommandMalformed, key)
return 0, fmt.Errorf("%w: unknown flag %q", ErrIptablesCommandMalformed, flag)
}
return nil
return consumed, nil
}
func parseIPPrefix(value string) (prefix netip.Prefix, err error) {
+8 -8
View File
@@ -23,19 +23,19 @@ func Test_parseIptablesInstruction(t *testing.T) {
"uneven_fields": {
s: "-A",
errWrapped: ErrIptablesCommandMalformed,
errMessage: "iptables command is malformed: fields count 1 is not even: \"-A\"",
errMessage: "parsing \"-A\": iptables command is malformed: flag \"-A\" requires a value, but got none",
},
"unknown_key": {
s: "-x something",
errWrapped: ErrIptablesCommandMalformed,
errMessage: "parsing \"-x something\": iptables command is malformed: unknown key \"-x\"",
errMessage: "parsing \"-x something\": iptables command is malformed: unknown flag \"-x\"",
},
"one_pair": {
s: "-A INPUT",
s: "-I INPUT",
instruction: iptablesInstruction{
table: "filter",
chain: "INPUT",
append: true,
table: "filter",
chain: "INPUT",
operation: opInsert,
},
},
"instruction_A": {
@@ -43,7 +43,7 @@ func Test_parseIptablesInstruction(t *testing.T) {
instruction: iptablesInstruction{
table: "filter",
chain: "INPUT",
append: true,
operation: opAppend,
inputInterface: "tun0",
protocol: "tcp",
source: netip.MustParsePrefix("1.2.3.4/32"),
@@ -57,7 +57,7 @@ func Test_parseIptablesInstruction(t *testing.T) {
instruction: iptablesInstruction{
table: "nat",
chain: "PREROUTING",
append: false,
operation: opDelete,
inputInterface: "tun0",
protocol: "tcp",
destinationPort: 43716,
+131
View File
@@ -3,6 +3,7 @@ package firewall
import (
"context"
"fmt"
"net/netip"
"strconv"
)
@@ -81,3 +82,133 @@ func (c *Config) RemoveAllowedPort(ctx context.Context, port uint16) (err error)
return nil
}
// RestrictOutputAddrPort allows outgoing traffic to a specific IP and port for both tcp and udp,
// while blocking other tcp or udp traffic to that port going to other IP addresses, both IPv4 and IPv6.
// If the port was previously allowed for another IP address, that previous allowance will be removed.
// Giving an invalid address will remove any existing restrictions for the port specified.
func (c *Config) RestrictOutputAddrPort(ctx context.Context, addrPort netip.AddrPort) (err error) {
c.stateMutex.Lock()
defer c.stateMutex.Unlock()
existingIP := c.outputAddrPort[addrPort.Port()]
switch {
case existingIP == addrPort.Addr():
return nil
case !addrPort.Addr().IsValid():
// invalid address, remove any existing rules for the port
return c.removeOutputAddrPortRestriction(ctx, existingIP, addrPort.Port())
case !existingIP.IsValid():
// no previous existing address for the port
return c.insertOutputAddrPortRestriction(ctx, addrPort)
default:
// existing rule in the same IP family or different family
return c.replaceOutputAddrPortRestriction(ctx, existingIP, addrPort)
}
}
func (c *Config) removeOutputAddrPortRestriction(ctx context.Context, existingIP netip.Addr, port uint16) (err error) {
commonInstructions := []string{
fmt.Sprintf("--delete OUTPUT -p udp --dport %d -j DROP", port),
fmt.Sprintf("--delete OUTPUT -p tcp --dport %d -j DROP", port),
}
ipv4Instructions := commonInstructions
ipv6Instructions := commonInstructions
familySpecificInstructions := []string{
fmt.Sprintf("--delete OUTPUT -p udp --dport %d -d %s -j ACCEPT", port, existingIP),
fmt.Sprintf("--delete OUTPUT -p tcp --dport %d -d %s -j ACCEPT", port, existingIP),
}
if existingIP.Is4() {
ipv4Instructions = append(ipv4Instructions, familySpecificInstructions...)
} else {
ipv6Instructions = append(ipv6Instructions, familySpecificInstructions...)
}
err = c.runIPv4AndV6IptablesInstructions(ctx, ipv4Instructions, ipv6Instructions)
if err != nil {
return err
}
delete(c.outputAddrPort, port)
return nil
}
func (c *Config) insertOutputAddrPortRestriction(ctx context.Context, addrPort netip.AddrPort) (err error) {
commonInstructions := []string{
fmt.Sprintf("--insert OUTPUT -p udp --dport %d -j DROP", addrPort.Port()),
fmt.Sprintf("--insert OUTPUT -p tcp --dport %d -j DROP", addrPort.Port()),
}
ipv4Instructions := commonInstructions
ipv6Instructions := commonInstructions
familySpecificInstructions := []string{
fmt.Sprintf("--insert OUTPUT -p udp --dport %d -d %s -j ACCEPT", addrPort.Port(), addrPort.Addr()),
fmt.Sprintf("--insert OUTPUT -p tcp --dport %d -d %s -j ACCEPT", addrPort.Port(), addrPort.Addr()),
}
if addrPort.Addr().Is4() {
ipv4Instructions = append(ipv4Instructions, familySpecificInstructions...)
} else {
ipv6Instructions = append(ipv6Instructions, familySpecificInstructions...)
}
err = c.runIPv4AndV6IptablesInstructions(ctx, ipv4Instructions, ipv6Instructions)
if err != nil {
return err
}
c.outputAddrPort[addrPort.Port()] = addrPort.Addr()
return nil
}
func (c *Config) replaceOutputAddrPortRestriction(ctx context.Context,
existingIP netip.Addr, addrPort netip.AddrPort,
) (err error) {
for _, protocol := range [...]string{"udp", "tcp"} {
switch {
case existingIP.Is4() && addrPort.Addr().Is4():
oldInstruction := fmt.Sprintf("--insert OUTPUT -p %s --dport %d -d %s -j ACCEPT",
protocol, addrPort.Port(), existingIP)
newInstruction := fmt.Sprintf("--insert OUTPUT -p %s --dport %d -d %s -j ACCEPT",
protocol, addrPort.Port(), addrPort.Addr())
err = c.replaceIptablesRule(ctx, oldInstruction, newInstruction)
if err != nil {
return fmt.Errorf("replacing existing IPv4 rule: %w", err)
}
case existingIP.Is6() && addrPort.Addr().Is6():
oldInstruction := fmt.Sprintf("--insert OUTPUT -p %s --dport %d -d %s -j ACCEPT",
protocol, addrPort.Port(), existingIP)
newInstruction := fmt.Sprintf("--insert OUTPUT -p %s --dport %d -d %s -j ACCEPT",
protocol, addrPort.Port(), addrPort.Addr())
err = c.replaceIP6tablesRule(ctx, oldInstruction, newInstruction)
if err != nil {
return fmt.Errorf("replacing existing IPv6 rule: %w", err)
}
case existingIP.Is4() && addrPort.Addr().Is6():
instruction := fmt.Sprintf("--delete OUTPUT -p %s --dport %d -d %s -j ACCEPT",
protocol, addrPort.Port(), existingIP)
err = c.runIptablesInstruction(ctx, instruction)
if err != nil {
return fmt.Errorf("removing existing IPv4 rule: %w", err)
}
instruction = fmt.Sprintf("--insert OUTPUT -p %s --dport %d -d %s -j ACCEPT",
protocol, addrPort.Port(), addrPort.Addr())
err = c.runIP6tablesInstruction(ctx, instruction)
if err != nil {
return fmt.Errorf("inserting new IPv6 rule: %w", err)
}
case existingIP.Is6() && addrPort.Addr().Is4():
instruction := fmt.Sprintf("--delete OUTPUT -p %s --dport %d -d %s -j ACCEPT",
protocol, addrPort.Port(), existingIP)
err = c.runIP6tablesInstruction(ctx, instruction)
if err != nil {
return fmt.Errorf("removing existing IPv6 rule: %w", err)
}
instruction = fmt.Sprintf("--insert OUTPUT -p %s --dport %d -d %s -j ACCEPT",
protocol, addrPort.Port(), addrPort.Addr())
err = c.runIptablesInstruction(ctx, instruction)
if err != nil {
return fmt.Errorf("inserting new IPv4 rule: %w", err)
}
}
}
c.outputAddrPort[addrPort.Port()] = addrPort.Addr()
return nil
}
+51
View File
@@ -0,0 +1,51 @@
package firewall
import (
"context"
"errors"
"fmt"
)
var errRuleNotFound = errors.New("rule not found")
func (c *Config) replaceIptablesRule(ctx context.Context, oldInstruction, newInstruction string) error {
targetRule, err := parseIptablesInstruction(oldInstruction)
if err != nil {
return fmt.Errorf("parsing iptables command to replace: %w", err)
}
lineNumber, err := findLineNumber(ctx, c.ipTables, targetRule, c.runner, c.logger)
if err != nil {
return fmt.Errorf("finding to-be-replaced chain rule line number: %w", err)
} else if lineNumber == 0 {
return fmt.Errorf("%w: matching to-be-replaced instruction %q", errRuleNotFound, oldInstruction)
}
parsed, err := parseIptablesInstruction(newInstruction)
if err != nil {
return fmt.Errorf("parsing replacement iptables command: %w", err)
}
parsed.operation = opReplace
parsed.lineNumber = lineNumber
return c.runIptablesInstruction(ctx, parsed.String())
}
func (c *Config) replaceIP6tablesRule(ctx context.Context, oldInstruction, newInstruction string) error {
targetRule, err := parseIptablesInstruction(oldInstruction)
if err != nil {
return fmt.Errorf("parsing iptables command to replace: %w", err)
}
lineNumber, err := findLineNumber(ctx, c.ip6Tables, targetRule, c.runner, c.logger)
if err != nil {
return fmt.Errorf("finding to-be-replaced chain rule line number: %w", err)
} else if lineNumber == 0 {
return fmt.Errorf("%w: matching to-be-replaced instruction %q", errRuleNotFound, oldInstruction)
}
parsed, err := parseIptablesInstruction(newInstruction)
if err != nil {
return fmt.Errorf("parsing replacement iptables command: %w", err)
}
parsed.operation = opReplace
parsed.lineNumber = lineNumber
return c.runIP6tablesInstruction(ctx, parsed.String())
}
+1 -1
View File
@@ -92,7 +92,7 @@ func testIptablesPath(ctx context.Context, path string,
// Set policy as the existing policy so no mutation is done.
// This is an extra check for some buggy kernels where setting the policy
// does not work.
cmd = exec.CommandContext(ctx, path, "-L", "INPUT")
cmd = exec.CommandContext(ctx, path, "-nL", "INPUT")
output, err = runner.Run(cmd)
if err != nil {
unsupportedMessage = fmt.Sprintf("%s (%s)", output, err)
+1 -1
View File
@@ -24,7 +24,7 @@ func newDeleteTestRuleMatcher(path string) *cmdMatcher {
func newListInputRulesMatcher(path string) *cmdMatcher {
return newCmdMatcher(path,
"^-L$", "^INPUT$")
"^-nL$", "^INPUT$")
}
func newSetPolicyMatcher(path, inputPolicy string) *cmdMatcher { //nolint:unparam
+336
View File
@@ -0,0 +1,336 @@
package healthcheck
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/netip"
"strings"
"sync"
"time"
"github.com/qdm12/gluetun/internal/healthcheck/dns"
"github.com/qdm12/gluetun/internal/healthcheck/icmp"
)
type Checker struct {
tlsDialAddrs []string
dialer *net.Dialer
echoer *icmp.Echoer
dnsClient *dns.Client
logger Logger
icmpTargetIPs []netip.Addr
smallCheckType string
configMutex sync.Mutex
icmpNotPermitted *bool
// Internal periodic service signals
stop context.CancelFunc
done <-chan struct{}
}
func NewChecker(logger Logger) *Checker {
return &Checker{
dialer: &net.Dialer{
Resolver: &net.Resolver{
PreferGo: true,
},
},
echoer: icmp.NewEchoer(logger),
dnsClient: dns.New(),
logger: logger,
}
}
// SetConfig sets the TCP+TLS dial addresses, the ICMP echo IP address
// to target and the desired small check type (dns or icmp).
// This function MUST be called before calling [Checker.Start].
func (c *Checker) SetConfig(tlsDialAddrs []string, icmpTargets []netip.Addr,
smallCheckType string,
) {
c.configMutex.Lock()
defer c.configMutex.Unlock()
c.tlsDialAddrs = tlsDialAddrs
c.icmpTargetIPs = icmpTargets
c.smallCheckType = smallCheckType
}
// Start starts the checker by first running a blocking 6s-timed TCP+TLS check,
// and, on success, starts the periodic checks in a separate goroutine:
// - a "small" ICMP echo check every minute
// - a "full" TCP+TLS check every 5 minutes
// It returns a channel `runError` that receives an error (nil or not) when a periodic check is performed.
// It returns an error if the initial TCP+TLS check fails.
// The Checker has to be ultimately stopped by calling [Checker.Stop].
func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error) {
if len(c.tlsDialAddrs) == 0 || len(c.icmpTargetIPs) == 0 || c.smallCheckType == "" {
panic("call Checker.SetConfig with non empty values before Checker.Start")
}
if c.icmpNotPermitted != nil && *c.icmpNotPermitted {
// restore forced check type to dns if icmp was found to be not permitted
c.smallCheckType = smallCheckDNS
}
c.echoer.Reset()
err = c.startupCheck(ctx)
if err != nil {
return nil, fmt.Errorf("startup check: %w", err)
}
ready := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
c.stop = cancel
done := make(chan struct{})
c.done = done
const smallCheckPeriod = time.Minute
smallCheckTimer := time.NewTimer(smallCheckPeriod)
const fullCheckPeriod = 5 * time.Minute
fullCheckTimer := time.NewTimer(fullCheckPeriod)
runErrorCh := make(chan error)
runError = runErrorCh
go func() {
defer close(done)
close(ready)
for {
select {
case <-ctx.Done():
fullCheckTimer.Stop()
smallCheckTimer.Stop()
return
case <-smallCheckTimer.C:
err := c.smallPeriodicCheck(ctx)
if err != nil {
err = fmt.Errorf("small periodic check: %w", err)
}
select {
case <-ctx.Done():
continue
case runErrorCh <- err:
}
smallCheckTimer.Reset(smallCheckPeriod)
case <-fullCheckTimer.C:
err := c.fullPeriodicCheck(ctx)
if err != nil {
err = fmt.Errorf("full periodic check: %w", err)
}
select {
case <-ctx.Done():
continue
case runErrorCh <- err:
}
fullCheckTimer.Reset(fullCheckPeriod)
}
}
}()
<-ready
return runError, nil
}
func (c *Checker) Stop() error {
c.stop()
<-c.done
c.tlsDialAddrs = nil
c.icmpTargetIPs = nil
c.smallCheckType = ""
return nil
}
func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
c.configMutex.Lock()
icmpTargetIPs := make([]netip.Addr, len(c.icmpTargetIPs))
copy(icmpTargetIPs, c.icmpTargetIPs)
c.configMutex.Unlock()
tryTimeouts := []time.Duration{
5 * time.Second,
5 * time.Second,
5 * time.Second,
10 * time.Second,
10 * time.Second,
10 * time.Second,
15 * time.Second,
15 * time.Second,
15 * time.Second,
30 * time.Second,
}
check := func(ctx context.Context, try int) error {
if c.smallCheckType == smallCheckDNS {
return c.dnsClient.Check(ctx)
}
ip := icmpTargetIPs[try%len(icmpTargetIPs)]
err := c.echoer.Echo(ctx, ip)
if c.icmpNotPermitted == nil && errors.Is(err, icmp.ErrNotPermitted) {
c.icmpNotPermitted = new(bool)
*c.icmpNotPermitted = true
c.smallCheckType = smallCheckDNS
c.logger.Infof("%s; permanently falling back to %s checks",
err, smallCheckTypeToString(c.smallCheckType))
return c.dnsClient.Check(ctx)
}
return err
}
return withRetries(ctx, tryTimeouts, c.logger, smallCheckTypeToString(c.smallCheckType), check)
}
func (c *Checker) fullPeriodicCheck(ctx context.Context) error {
// 20s timeout in case the connection is under stress
// See https://github.com/qdm12/gluetun/issues/2270
tryTimeouts := []time.Duration{10 * time.Second, 15 * time.Second, 30 * time.Second}
check := func(ctx context.Context, try int) error {
tlsDialAddr := c.tlsDialAddrs[try%len(c.tlsDialAddrs)]
return tcpTLSCheck(ctx, c.dialer, tlsDialAddr)
}
return withRetries(ctx, tryTimeouts, c.logger, "TCP+TLS dial", check)
}
func tcpTLSCheck(ctx context.Context, dialer *net.Dialer, targetAddress string) error {
// TODO use mullvad API if current provider is Mullvad
address, err := makeAddressToDial(targetAddress)
if err != nil {
return err
}
const dialNetwork = "tcp4"
connection, err := dialer.DialContext(ctx, dialNetwork, address)
if err != nil {
return fmt.Errorf("dialing: %w", err)
}
if strings.HasSuffix(address, ":443") {
host, _, err := net.SplitHostPort(address)
if err != nil {
return fmt.Errorf("splitting host and port: %w", err)
}
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: host,
}
tlsConnection := tls.Client(connection, tlsConfig)
err = tlsConnection.HandshakeContext(ctx)
if err != nil {
return fmt.Errorf("running TLS handshake: %w", err)
}
}
err = connection.Close()
if err != nil {
return fmt.Errorf("closing connection: %w", err)
}
return nil
}
func makeAddressToDial(address string) (addressToDial string, err error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
addrErr := new(net.AddrError)
ok := errors.As(err, &addrErr)
if !ok || addrErr.Err != "missing port in address" {
return "", fmt.Errorf("splitting host and port from address: %w", err)
}
host = address
const defaultPort = "443"
port = defaultPort
}
address = net.JoinHostPort(host, port)
return address, nil
}
var ErrAllCheckTriesFailed = errors.New("all check tries failed")
func withRetries(ctx context.Context, tryTimeouts []time.Duration,
logger Logger, checkName string, check func(ctx context.Context, try int) error,
) error {
maxTries := len(tryTimeouts)
type errData struct {
err error
durationMS int64
}
errs := make([]errData, maxTries)
for i, timeout := range tryTimeouts {
start := time.Now()
checkCtx, cancel := context.WithTimeout(ctx, timeout)
err := check(checkCtx, i)
cancel()
switch {
case err == nil:
return nil
case ctx.Err() != nil:
return fmt.Errorf("%s: %w", checkName, ctx.Err())
}
logger.Debugf("%s attempt %d/%d failed: %s", checkName, i+1, maxTries, err)
errs[i].err = err
errs[i].durationMS = time.Since(start).Round(time.Millisecond).Milliseconds()
}
errStrings := make([]string, len(errs))
for i, err := range errs {
errStrings[i] = fmt.Sprintf("attempt %d (%dms): %s", i+1, err.durationMS, err.err)
}
return fmt.Errorf("%w:\n\t%s", ErrAllCheckTriesFailed, strings.Join(errStrings, "\n\t"))
}
func (c *Checker) startupCheck(ctx context.Context) error {
// connection isn't under load yet when the checker starts, so a short
// 6 seconds timeout suffices and provides quick enough feedback that
// the new connection is not working. However, since the addresses to dial
// may be multiple, we run the check in parallel. If any succeeds, the check passes.
// This is to prevent false negatives at startup, if one of the addresses is down
// for external reasons.
const timeout = 6 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
errCh := make(chan error)
for _, address := range c.tlsDialAddrs {
go func(addr string) {
err := tcpTLSCheck(ctx, c.dialer, addr)
errCh <- err
}(address)
}
errs := make([]error, 0, len(c.tlsDialAddrs))
success := false
for range c.tlsDialAddrs {
err := <-errCh
if err == nil {
success = true
cancel()
continue
} else if success {
continue // ignore canceled errors after success
}
c.logger.Debugf("startup check parallel attempt failed: %s", err)
errs = append(errs, err)
}
if success {
return nil
}
errStrings := make([]string, len(errs))
for i, err := range errs {
errStrings[i] = fmt.Sprintf("parallel attempt %d/%d failed: %s", i+1, len(errs), err)
}
return fmt.Errorf("%w: %s", ErrAllCheckTriesFailed, strings.Join(errStrings, ", "))
}
const (
smallCheckDNS = "dns"
smallCheckICMP = "icmp"
)
func smallCheckTypeToString(smallCheckType string) string {
switch smallCheckType {
case smallCheckICMP:
return "ICMP echo"
case smallCheckDNS:
return "plain DNS over UDP"
default:
panic("unknown small check type: " + smallCheckType)
}
}
@@ -7,40 +7,41 @@ import (
"testing"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Server_healthCheck(t *testing.T) {
func Test_Checker_fullcheck(t *testing.T) {
t.Parallel()
t.Run("canceled real dialer", func(t *testing.T) {
t.Parallel()
dialer := &net.Dialer{}
const address = "cloudflare.com:443"
addresses := []string{"badaddress:9876", "cloudflare.com:443", "google.com:443"}
server := &Server{
dialer: dialer,
config: settings.Health{
TargetAddress: address,
},
checker := &Checker{
dialer: dialer,
tlsDialAddrs: addresses,
}
canceledCtx, cancel := context.WithCancel(context.Background())
cancel()
err := server.healthCheck(canceledCtx)
err := checker.fullPeriodicCheck(canceledCtx)
require.Error(t, err)
assert.Contains(t, err.Error(), "operation was canceled")
assert.EqualError(t, err, "TCP+TLS dial: context canceled")
})
t.Run("dial localhost:0", func(t *testing.T) {
t.Parallel()
listener, err := net.Listen("tcp4", "localhost:0")
const timeout = 100 * time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
listenConfig := &net.ListenConfig{}
listener, err := listenConfig.Listen(ctx, "tcp4", "localhost:0")
require.NoError(t, err)
t.Cleanup(func() {
err = listener.Close()
@@ -50,18 +51,12 @@ func Test_Server_healthCheck(t *testing.T) {
listeningAddress := listener.Addr()
dialer := &net.Dialer{}
server := &Server{
dialer: dialer,
config: settings.Health{
TargetAddress: listeningAddress.String(),
},
checker := &Checker{
dialer: dialer,
tlsDialAddrs: []string{listeningAddress.String()},
}
const timeout = 100 * time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
err = server.healthCheck(ctx)
err = checker.fullPeriodicCheck(ctx)
assert.NoError(t, err)
})
+66
View File
@@ -0,0 +1,66 @@
package dns
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"github.com/qdm12/dns/v2/pkg/provider"
)
// Client is a simple plaintext UDP DNS client, to be used for healthchecks.
// Note the client connects to a DNS server only over UDP on port 53,
// because we don't want to use DoT or DoH and impact the TCP connections
// when running a healthcheck.
type Client struct {
serverAddrs []netip.AddrPort
dnsIPIndex int
}
func New() *Client {
return &Client{
serverAddrs: concatAddrPorts([][]netip.AddrPort{
provider.Cloudflare().Plain.IPv4,
provider.Google().Plain.IPv4,
provider.Quad9().Plain.IPv4,
provider.OpenDNS().Plain.IPv4,
provider.LibreDNS().Plain.IPv4,
provider.Quadrant().Plain.IPv4,
provider.CiraProtected().Plain.IPv4,
}),
}
}
func concatAddrPorts(addrs [][]netip.AddrPort) []netip.AddrPort {
var result []netip.AddrPort
for _, addrList := range addrs {
result = append(result, addrList...)
}
return result
}
var ErrLookupNoIPs = errors.New("no IPs found from DNS lookup")
func (c *Client) Check(ctx context.Context) error {
dnsAddr := c.serverAddrs[c.dnsIPIndex].String()
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, "udp", dnsAddr)
},
}
ips, err := resolver.LookupIP(ctx, "ip", "github.com")
switch {
case err != nil:
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
return fmt.Errorf("with DNS server %s: %w", dnsAddr, err)
case len(ips) == 0:
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
return fmt.Errorf("with DNS server %s: %w", dnsAddr, ErrLookupNoIPs)
default:
return nil
}
}
+3 -1
View File
@@ -9,13 +9,15 @@ import (
type handler struct {
healthErr error
healthErrMu sync.RWMutex
logger Logger
}
var errHealthcheckNotRunYet = errors.New("healthcheck did not run yet")
func newHandler() *handler {
func newHandler(logger Logger) *handler {
return &handler{
healthErr: errHealthcheckNotRunYet,
logger: logger,
}
}
-104
View File
@@ -1,104 +0,0 @@
package healthcheck
import (
"context"
"errors"
"fmt"
"net"
"time"
)
func (s *Server) runHealthcheckLoop(ctx context.Context, done chan<- struct{}) {
defer close(done)
timeoutIndex := 0
healthcheckTimeouts := []time.Duration{
2 * time.Second,
4 * time.Second,
6 * time.Second,
8 * time.Second,
// This can be useful when the connection is under stress
// See https://github.com/qdm12/gluetun/issues/2270
10 * time.Second,
}
s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait)
for {
previousErr := s.handler.getErr()
timeout := healthcheckTimeouts[timeoutIndex]
healthcheckCtx, healthcheckCancel := context.WithTimeout(
ctx, timeout)
err := s.healthCheck(healthcheckCtx)
healthcheckCancel()
s.handler.setErr(err)
switch {
case previousErr != nil && err == nil: // First success
s.logger.Info("healthy!")
timeoutIndex = 0
s.vpn.healthyTimer.Stop()
s.vpn.healthyWait = *s.config.VPN.Initial
case previousErr == nil && err != nil: // First failure
s.logger.Debug("unhealthy: " + err.Error())
s.vpn.healthyTimer.Stop()
s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait)
case previousErr != nil && err != nil: // Nth failure
if timeoutIndex < len(healthcheckTimeouts)-1 {
timeoutIndex++
}
select {
case <-s.vpn.healthyTimer.C:
timeoutIndex = 0 // retry next with the smallest timeout
s.onUnhealthyVPN(ctx)
default:
}
case previousErr == nil && err == nil: // Nth success
timer := time.NewTimer(s.config.SuccessWait)
select {
case <-ctx.Done():
return
case <-timer.C:
}
}
}
}
func (s *Server) healthCheck(ctx context.Context) (err error) {
// TODO use mullvad API if current provider is Mullvad
address, err := makeAddressToDial(s.config.TargetAddress)
if err != nil {
return err
}
const dialNetwork = "tcp4"
connection, err := s.dialer.DialContext(ctx, dialNetwork, address)
if err != nil {
return fmt.Errorf("dialing: %w", err)
}
err = connection.Close()
if err != nil {
return fmt.Errorf("closing connection: %w", err)
}
return nil
}
func makeAddressToDial(address string) (addressToDial string, err error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
addrErr := new(net.AddrError)
ok := errors.As(err, &addrErr)
if !ok || addrErr.Err != "missing port in address" {
return "", fmt.Errorf("splitting host and port from address: %w", err)
}
host = address
const defaultPort = "443"
port = defaultPort
}
address = net.JoinHostPort(host, port)
return address, nil
}
+49
View File
@@ -0,0 +1,49 @@
package icmp
import (
"net"
"time"
"golang.org/x/net/ipv4"
)
var _ net.PacketConn = &ipv4Wrapper{}
// ipv4Wrapper is a wrapper around ipv4.PacketConn to implement
// the net.PacketConn interface. It's only used for Darwin or iOS.
type ipv4Wrapper struct {
ipv4Conn *ipv4.PacketConn
}
func ipv4ToNetPacketConn(ipv4 *ipv4.PacketConn) *ipv4Wrapper {
return &ipv4Wrapper{ipv4Conn: ipv4}
}
func (i *ipv4Wrapper) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, _, addr, err = i.ipv4Conn.ReadFrom(p)
return n, addr, err
}
func (i *ipv4Wrapper) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return i.ipv4Conn.WriteTo(p, nil, addr)
}
func (i *ipv4Wrapper) Close() error {
return i.ipv4Conn.Close()
}
func (i *ipv4Wrapper) LocalAddr() net.Addr {
return i.ipv4Conn.LocalAddr()
}
func (i *ipv4Wrapper) SetDeadline(t time.Time) error {
return i.ipv4Conn.SetDeadline(t)
}
func (i *ipv4Wrapper) SetReadDeadline(t time.Time) error {
return i.ipv4Conn.SetReadDeadline(t)
}
func (i *ipv4Wrapper) SetWriteDeadline(t time.Time) error {
return i.ipv4Conn.SetWriteDeadline(t)
}
+223
View File
@@ -0,0 +1,223 @@
package icmp
import (
"bytes"
"context"
cryptorand "crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"math/rand/v2"
"net"
"net/netip"
"strings"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
var (
ErrICMPBodyUnsupported = errors.New("ICMP body type is not supported")
ErrICMPEchoDataMismatch = errors.New("ICMP data mismatch")
)
type Echoer struct {
buffer []byte
randomSource io.Reader
logger Logger
seqStart time.Time
id int
seq int
}
func NewEchoer(logger Logger) *Echoer {
const maxICMPEchoSize = 1500
buffer := make([]byte, maxICMPEchoSize)
var seed [32]byte
_, _ = cryptorand.Read(seed[:])
randomSource := rand.NewChaCha8(seed)
return &Echoer{
buffer: buffer,
randomSource: randomSource,
logger: logger,
}
}
// Reset resets the [Echoer] icmp echo parameters:
// - ID is assigned a new random value
// - sequence is reset to 1
// - sequence start time is set to now
// It is used when the sequence is complete or when the VPN reconnects.
func (e *Echoer) Reset() {
const uint16Bytes = 2
idBytes := make([]byte, uint16Bytes)
_, _ = e.randomSource.Read(idBytes)
e.id = int(binary.BigEndian.Uint16(idBytes))
e.seq = 1
e.seqStart = time.Now()
}
var (
ErrTimedOut = errors.New("timed out waiting for ICMP echo reply")
ErrNotPermitted = errors.New("not permitted")
)
func (e *Echoer) Echo(ctx context.Context, ip netip.Addr) (err error) {
var ipVersion string
var conn net.PacketConn
if ip.Is4() {
ipVersion = "v4"
conn, err = listenICMPv4(ctx)
} else {
ipVersion = "v6"
conn, err = listenICMPv6(ctx)
}
if err != nil {
if strings.HasSuffix(err.Error(), "socket: operation not permitted") {
err = fmt.Errorf("%w: you can try adding NET_RAW capability to resolve this", ErrNotPermitted)
}
return fmt.Errorf("listening for ICMP packets: %w", err)
}
go func() {
<-ctx.Done()
conn.Close()
}()
const maxSeq = 1<<16 - 1
const refreshIDInterval = 5 * time.Minute
if e.seq > maxSeq && time.Since(e.seqStart) >= refreshIDInterval {
e.Reset()
}
message := buildMessageToSend(ipVersion, e.id, e.seq, e.randomSource)
encodedMessage, err := message.Marshal(nil)
if err != nil {
return fmt.Errorf("encoding ICMP message: %w", err)
}
_, err = conn.WriteTo(encodedMessage, &net.IPAddr{IP: ip.AsSlice()})
if err != nil {
if strings.HasSuffix(err.Error(), "sendto: operation not permitted") {
err = fmt.Errorf("%w", ErrNotPermitted)
}
return fmt.Errorf("writing ICMP message to %s: %w", ip, err)
}
defer func() {
e.seq++
}()
receivedData, err := receiveEchoReply(conn, e.id, e.seq, e.buffer, ipVersion, e.logger)
if err != nil {
if errors.Is(err, net.ErrClosed) && ctx.Err() != nil {
return fmt.Errorf("%w from %s", ErrTimedOut, ip)
}
return fmt.Errorf("receiving ICMP echo reply from %s: %w", ip, err)
}
sentData := message.Body.(*icmp.Echo).Data //nolint:forcetypeassert
if !bytes.Equal(receivedData, sentData) {
return fmt.Errorf("%w: sent %x to %s and received %x", ErrICMPEchoDataMismatch, sentData, ip, receivedData)
}
return nil
}
func buildMessageToSend(ipVersion string, id, seq int, randomSource io.Reader) (message *icmp.Message) {
var icmpType icmp.Type
switch ipVersion {
case "v4":
icmpType = ipv4.ICMPTypeEcho
case "v6":
icmpType = ipv6.ICMPTypeEchoRequest
default:
panic(fmt.Sprintf("IP version %q not supported", ipVersion))
}
const size = 32
messageBodyData := make([]byte, size)
_, _ = randomSource.Read(messageBodyData)
// See https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-types
message = &icmp.Message{
Type: icmpType, // echo request
Code: 0, // no code
Checksum: 0, // calculated at encoding (ipv4) or sending (ipv6)
Body: &icmp.Echo{
ID: id,
Seq: seq,
Data: messageBodyData,
},
}
return message
}
func receiveEchoReply(conn net.PacketConn, id, seq int, buffer []byte, ipVersion string, logger Logger,
) (data []byte, err error) {
var icmpProtocol int
const (
icmpv4Protocol = 1
icmpv6Protocol = 58
)
switch ipVersion {
case "v4":
icmpProtocol = icmpv4Protocol
case "v6":
icmpProtocol = icmpv6Protocol
default:
panic(fmt.Sprintf("unknown IP version: %s", ipVersion))
}
for {
// Note we need to read the whole packet in one call to ReadFrom, so the buffer
// must be large enough to read the entire reply packet. See:
// https://groups.google.com/g/golang-nuts/c/5dy2Q4nPs08/m/KmuSQAGEtG4J
bytesRead, returnAddr, err := conn.ReadFrom(buffer)
if err != nil {
return nil, fmt.Errorf("reading from ICMP connection: %w", err)
}
packetBytes := buffer[:bytesRead]
// Parse the ICMP message
message, err := icmp.ParseMessage(icmpProtocol, packetBytes)
if err != nil {
return nil, fmt.Errorf("parsing message: %w", err)
}
switch body := message.Body.(type) {
case *icmp.Echo:
switch {
case id != body.ID:
logger.Warnf("ignoring ICMP echo reply mismatching expected id %d "+
"(id: %d, seq: %d, type: %d, code: %d, length: %d, return address %s)",
id, body.Seq, body.ID, message.Type, message.Code, len(packetBytes), returnAddr)
continue // not the ID we are looking for
case seq != body.Seq:
logger.Warnf("ignoring ICMP echo reply mismatching expected sequence number %d "+
"(id: %d, seq: %d, type: %d, code: %d, length: %d, return address %s)",
seq, body.ID, body.Seq, message.Type, message.Code, len(packetBytes), returnAddr)
continue // not the seq we are looking for
}
return body.Data, nil
case *icmp.DstUnreach:
logger.Debugf("ignoring ICMP destination unreachable message "+
"(type: 3, code: %d, return address %s, expected id %d and seq %d)",
message.Code, returnAddr, id, seq)
// See https://github.com/qdm12/gluetun/pull/2923#issuecomment-3377532249
// on why we ignore this message. If it is actually unreachable, the timeout on waiting for
// the echo reply will do instead of returning an error error.
continue
case *icmp.TimeExceeded:
logger.Debugf("ignoring ICMP time exceeded message "+
"(type: 11, code: %d, return address %s, expected id %d and seq %d)",
message.Code, returnAddr, id, seq)
continue
default:
return nil, fmt.Errorf("%w: %T (type %d, code %d, return address %s, expected id %d and seq %d)",
ErrICMPBodyUnsupported, body, message.Type, message.Code, returnAddr, id, seq)
}
}
}
+6
View File
@@ -0,0 +1,6 @@
package icmp
type Logger interface {
Debugf(format string, args ...any)
Warnf(format string, args ...any)
}
+35
View File
@@ -0,0 +1,35 @@
package icmp
import (
"context"
"fmt"
"net"
"runtime"
"golang.org/x/net/ipv4"
)
func listenICMPv4(ctx context.Context) (conn net.PacketConn, err error) {
var listenConfig net.ListenConfig
const listenAddress = ""
packetConn, err := listenConfig.ListenPacket(ctx, "ip4:icmp", listenAddress)
if err != nil {
return nil, fmt.Errorf("listening for ICMP packets: %w", err)
}
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
packetConn = ipv4ToNetPacketConn(ipv4.NewPacketConn(packetConn))
}
return packetConn, nil
}
func listenICMPv6(ctx context.Context) (conn net.PacketConn, err error) {
var listenConfig net.ListenConfig
const listenAddress = ""
packetConn, err := listenConfig.ListenPacket(ctx, "ip6:ipv6-icmp", listenAddress)
if err != nil {
return nil, fmt.Errorf("listening for ICMPv6 packets: %w", err)
}
return packetConn, nil
}
+9
View File
@@ -0,0 +1,9 @@
package healthcheck
type Logger interface {
Debugf(format string, args ...any)
Info(s string)
Infof(format string, args ...any)
Warnf(format string, args ...any)
Error(s string)
}
-7
View File
@@ -1,7 +0,0 @@
package healthcheck
type Logger interface {
Debug(s string)
Info(s string)
Error(s string)
}
-25
View File
@@ -1,25 +0,0 @@
package healthcheck
import (
"context"
"time"
"github.com/qdm12/gluetun/internal/constants"
)
type vpnHealth struct {
loop StatusApplier
healthyWait time.Duration
healthyTimer *time.Timer
}
func (s *Server) onUnhealthyVPN(ctx context.Context) {
s.logger.Info("program has been unhealthy for " +
s.vpn.healthyWait.String() + ": restarting VPN")
s.logger.Info("👉 See https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md")
s.logger.Info("DO NOT OPEN AN ISSUE UNLESS YOU READ AND TRIED EACH POSSIBLE SOLUTION")
_, _ = s.vpn.loop.ApplyStatus(ctx, constants.Stopped)
_, _ = s.vpn.loop.ApplyStatus(ctx, constants.Running)
s.vpn.healthyWait += *s.config.VPN.Addition
s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait)
}
+4 -6
View File
@@ -10,14 +10,13 @@ import (
func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
defer close(done)
loopDone := make(chan struct{})
go s.runHealthcheckLoop(ctx, loopDone)
const readHeaderTimeout = 100 * time.Millisecond
const readTimeout = 500 * time.Millisecond
server := http.Server{
Addr: s.config.ServerAddress,
Handler: s.handler,
ReadHeaderTimeout: s.config.ReadHeaderTimeout,
ReadTimeout: s.config.ReadTimeout,
ReadHeaderTimeout: readHeaderTimeout,
ReadTimeout: readTimeout,
}
serverDone := make(chan struct{})
go func() {
@@ -37,6 +36,5 @@ func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
s.logger.Error(err.Error())
}
<-loopDone
<-serverDone
}
+7 -17
View File
@@ -2,7 +2,6 @@ package healthcheck
import (
"context"
"net"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/models"
@@ -11,30 +10,21 @@ import (
type Server struct {
logger Logger
handler *handler
dialer *net.Dialer
config settings.Health
vpn vpnHealth
}
func NewServer(config settings.Health,
logger Logger, vpnLoop StatusApplier,
) *Server {
func NewServer(config settings.Health, logger Logger) *Server {
return &Server{
logger: logger,
handler: newHandler(),
dialer: &net.Dialer{
Resolver: &net.Resolver{
PreferGo: true,
},
},
config: config,
vpn: vpnHealth{
loop: vpnLoop,
healthyWait: *config.VPN.Initial,
},
handler: newHandler(logger),
config: config,
}
}
func (s *Server) SetError(err error) {
s.handler.setErr(err)
}
type StatusApplier interface {
ApplyStatus(ctx context.Context, status models.LoopStatus) (
outcome string, err error)
-1
View File
@@ -13,7 +13,6 @@ func (h *handler) isAuthorized(responseWriter http.ResponseWriter, request *http
}
basicAuth := request.Header.Get("Proxy-Authorization")
if basicAuth == "" {
h.logger.Info("Proxy-Authorization header not found from " + request.RemoteAddr)
responseWriter.Header().Set("Proxy-Authenticate", `Basic realm="Access to Gluetun over HTTP"`)
responseWriter.WriteHeader(http.StatusProxyAuthRequired)
return false
+4 -1
View File
@@ -20,8 +20,10 @@ func (s *Server) Run(ctx context.Context, ready chan<- struct{}, done chan<- str
crashed := make(chan struct{})
shutdownDone := make(chan struct{})
listenCtx, listenCancel := context.WithCancel(ctx)
go func() {
defer close(shutdownDone)
defer listenCancel()
select {
case <-ctx.Done():
case <-crashed:
@@ -37,7 +39,8 @@ func (s *Server) Run(ctx context.Context, ready chan<- struct{}, done chan<- str
}
}()
listener, err := net.Listen("tcp", s.address)
listenConfig := &net.ListenConfig{}
listener, err := listenConfig.Listen(listenCtx, "tcp", s.address)
if err != nil {
close(s.addressSet)
close(crashed) // stop shutdown goroutine
@@ -1,3 +1,5 @@
//go:build !windows
package mod
import (
@@ -76,9 +76,9 @@ func initModule(path string) (err error) {
const flags = 0
err = unix.FinitModule(int(file.Fd()), moduleParams, flags)
switch {
case err == nil, err == unix.EEXIST: //nolint:goerr113
case err == nil, err == unix.EEXIST: //nolint:err113
return nil
case err != unix.ENOSYS: //nolint:goerr113
case err != unix.ENOSYS: //nolint:err113
if strings.HasSuffix(err.Error(), "operation not permitted") {
err = fmt.Errorf("%w; did you set the SYS_MODULE capability to your container?", err)
}
+7
View File
@@ -0,0 +1,7 @@
//go:build !linux
package mod
func Probe(moduleName string) error {
panic("not implemented")
}
-2
View File
@@ -166,8 +166,6 @@ func getMarkdownHeaders(vpnProvider string) (headers []string, err error) {
return []string{countryHeader, cityHeader, hostnameHeader, freeHeader, streamHeader, tcpHeader, udpHeader}, nil
case providers.Vyprvpn:
return []string{regionHeader, hostnameHeader, tcpHeader, udpHeader}, nil
case providers.Wevpn:
return []string{cityHeader, hostnameHeader, tcpHeader, udpHeader}, nil
case providers.Windscribe:
return []string{regionHeader, cityHeader, hostnameHeader, vpnHeader}, nil
default:

Some files were not shown because too many files have changed in this diff Show More