Compare commits

...

857 Commits

Author SHA1 Message Date
Quentin McGaw
4f0b8f7292 Merge branch 'master' into remove-keep-nameserver 2025-11-17 19:16:32 +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
25b381e138 fix linting errors 2025-11-17 15:17:57 +00:00
Quentin McGaw
35b6b709b2 DNS_UPSTREAM_PLAIN_ADDRESSES option
- New CSV format with port, for example `ip1:port1,ip2:port2`
- retrocompatibility with `DNS_ADDRESS`. If set, force upstream type to plain and empty user-picked providers. 127.0.0.1 is now ignored since it's always set to this value internally.
- requires `DNS_UPSTREAM_TYPE=plain` must be set to use `DNS_UPSTREAM_PLAIN_ADDRESSES` (unless using retro `DNS_ADDRESS`)
- Warning log on using private upstream resolvers updated
2025-11-17 15:04:06 +00:00
Quentin McGaw
40ea51a3ae Remove DNS_KEEP_NAMESERVER (always off) 2025-11-17 13:05:33 +00:00
Quentin McGaw
1a93a41a55 Remove DNS_SERVER option (always on) 2025-11-17 13:00:01 +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 248acd2833 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
Quentin McGaw
937c667ca8 hotfix(perfectprivacy): fix formatting from previous commit 2024-10-27 17:20:30 +00:00
Christoph Kehl
3c45f57aaa fix(perfectprivacy): update openvpn expired certificates (#2542) 2024-10-27 11:45:25 +01:00
Quentin McGaw
30640eefe2 chore(deps): upgrade dns to v2.0.0-cr7 2024-10-25 14:01:29 +00:00
Quentin McGaw
8567522594 chore(dev): pin godevcontainer image to tag v0.20-alpine 2024-10-20 16:18:52 +00:00
Quentin McGaw
bd8214e648 docs(dev): minor fixes to devcontainer readme 2024-10-20 12:57:58 +00:00
Quentin McGaw
a61302f135 feat(publicip): resilient public ip fetcher (#2518)
- `PUBLICIP_API` accepts a comma separated list of ip data sources, where the first one is the base default one, and sources after it are backup sources used if we are rate limited.
- `PUBLICIP_API` defaults to `ipinfo,ifconfigco,ip2location,cloudflare` such that it now has `ifconfigco,ip2location,cloudflare` as backup ip data sources.
- `PUBLICIP_API_TOKEN` accepts a comma separated list of ip data source tokens, each corresponding by position to the APIs listed in `PUBLICIP_API`.
- logs ip data source when logging public ip information
- assume a rate limiting error is for 30 days (no persistence)
- ready for future live settings updates
  - consider an ip data source no longer banned if the token changes
  - keeps track of ban times when updating the list of fetchers
2024-10-19 15:21:14 +02:00
Quentin McGaw
3dfb43e117 chore(netlink): debug log ip rule commands in netlink instead of routing package 2024-10-19 12:43:26 +00:00
Quentin McGaw
2388e0550b hotfix(publicip): return an error if trying to use cloudflare as ip provider for updating servers data 2024-10-11 21:57:25 +00:00
Quentin McGaw
a7d70dd9a3 fix(publicip): lock settings during entire update
- to prevent race conditions when data is cleared when vpn goes down
2024-10-11 21:24:18 +00:00
Quentin McGaw
76a4bb5dc3 chore: use gofumpt for code formatting 2024-10-11 19:27:29 +00:00
Quentin McGaw
3daf15a612 chore(lint): fix gopls govet errors 2024-10-11 19:14:50 +00:00
Quentin McGaw
81ffbaf057 feat(build): upgrade Go from 1.22 to 1.23 2024-10-11 18:58:10 +00:00
Quentin McGaw
abe9dcbe33 chore(lint): add new linters and update codebase
- add canonicalheader
- add copyloopvar
- add fatcontext
- add intrange
2024-10-11 18:28:00 +00:00
Quentin McGaw
3c8e80a1a4 chore(lint): upgrade linter from v1.56.2 to v1.61.0
- Remove no longer needed exclude rules
- Add new exclude rules for printf govet errors
- Remove deprecated linters `execinquery` and `exportloopref`
- Rename linter `goerr113` to `err113`
- Rename linter `gomnd` to `mnd`
2024-10-11 18:05:54 +00:00
Quentin McGaw
694988b32f chore(devcontainer): drop requirement for docker-compose and use devcontainer.json settings directly 2024-10-10 08:34:56 +00:00
Quentin McGaw
ea31886299 docs(devcontainer): update readme
- remove Windows without WSL step
- update 'remote containers extension' to 'dev containers extension'
- remove invalid warning on directories creation
- simplify customizations section
  - remove "publish a port" since it can be done at runtime now
  - remove "run other services" since it's rather unneeded in this case
  - expand documentation on custom welcome script and where to specify the bind mount
    - use bullet points instead of subsections headings
2024-10-10 08:33:33 +00:00
Quentin McGaw
5b2923ca65 feat(publicip): add ifconfigco option 2024-10-08 19:03:10 +00:00
Quentin McGaw
432eaa6c04 feat(vpn): run WaitForDNS before querying the public ip address
- Fix #2325 better
2024-10-08 11:30:35 +00:00
Quentin McGaw
5fd0af9395 feat(publicip): retry fetching information when connection refused error is encountered
- Fix #2325
2024-10-08 11:30:35 +00:00
Quentin McGaw
03deb9aed0 feat(publicip): PUBLICIP_ENABLED replaces PUBLICIP_PERIOD
- No point periodically fetch the public IP address. Could not find anything mentioning why this was added.
- Simplification of the publicip loop code
- `PUBLICIP_ENABLED` (on, off) can be set to enable or not public ip data fetching on VPN connection
- `PUBLICIP_PERIOD=0` still works to indicate to disable public ip fetching
- `PUBLICIP_PERIOD` != 0 means to enable public ip fetching
- Warnings logged when using `PUBLICIP_PERIOD`
2024-10-08 11:30:31 +00:00
Jeremy Lin
cbdd1a933c feat(publicip): cloudflare API support (#2502) 2024-10-06 15:30:33 +02:00
Quentin McGaw
99e9bc87cf fix(firewall): deduplicate VPN address accept rule for multiple default routes with the same network interface 2024-10-06 09:48:07 +00:00
Quentin McGaw
9ef14ee070 fix(firewall): deduplicate ipv6 multicast output accept rules 2024-10-06 09:46:47 +00:00
Quentin McGaw
7842ff4cdc fix(firewall): ipv6 multicast output address value 2024-10-06 09:28:39 +00:00
Quentin McGaw
3d6d03b327 fix(firewall): log warning if ipv6 nat filter not supported instead of returning an error
- Allow to port forward redirect for IPv4 and not IPv6 if IPv6 NAT is not supported
- Fix #2503
2024-10-05 07:52:30 +00:00
Quentin McGaw
7ebbaf4351 docs(Dockerfile): add OPENVPN_MSSFIX environment variable 2024-09-29 18:01:20 +00:00
Quentin McGaw
c665b13cec fix(settings): prevent using FREE_ONLY and PORT_FORWARD_ONLY together with protonvpn (see #2470) 2024-09-28 17:51:47 +00:00
Quentin McGaw
970b21a6eb docs(Dockerfile): add missing option definitions
- `STREAM_ONLY`
- `FREE_ONLY`
- Document `PORT_FORWARD_ONLY` is for both PIA and ProtonVPN
2024-09-28 17:49:03 +00:00
Quentin McGaw
62747f1eb8 fix(storage): add missing selection fields to build noServerFoundError
- `STREAM_ONLY`, `PORT_FORWARD_ONLY`, `SECURE_CORE_ONLY`, `TOR_ONLY` and target ip options affected
- Refers to issue #2470
2024-09-28 17:47:56 +00:00
Quentin McGaw
a2e76e1683 feat(server): role based authentication system (#2434)
- Parse toml configuration file, see https://github.com/qdm12/gluetun-wiki/blob/main/setup/advanced/control-server.md#authentication
- Retro-compatible with existing AND documented routes, until after v3.41 release
- Log a warning if an unprotected-by-default route is accessed unprotected
- Authentication methods: none, apikey, basic
- `genkey` command to generate API keys

Co-authored-by: Joe Jose <45399349+joejose97@users.noreply.github.com>
2024-09-18 13:29:36 +02:00
Quentin McGaw
07651683f9 feat(providers): add giganews support (#2479) 2024-09-18 13:01:37 +02:00
Quentin McGaw
429aea8e0f docs(github): change and add labels
- change "config problem" to "user error"
- add "performance" category
- add "investigation" category
2024-08-25 07:06:33 +00:00
Quentin McGaw
01fa9934bc hotfix(routing): detect vpn local gateway with new routes listing 2024-08-25 07:01:33 +00:00
Quentin McGaw
ff7cadb43b chore(server): move log middleware to internal/server/middlewares/log 2024-08-23 13:46:52 +00:00
Quentin McGaw
540acc915d chore(deps): upgrade vishvananda/netlink from v1.2.1-beta.2 to v1.2.1 2024-08-23 13:46:09 +00:00
dependabot[bot]
703a546c1d Chore(deps): Bump google.golang.org/protobuf from 1.30.0 to 1.33.0 (#2428) 2024-08-22 17:24:39 +02:00
Quentin McGaw
4851bd70da chore(deps): remove qdm12/golibs dependency
- Implement friendly duration formatting locally
2024-08-21 13:27:30 +00:00
Quentin McGaw
a2b3d7e30c chore(deps): implement github.com/qdm12/golibs/command locally (#2418) 2024-08-21 15:21:31 +02:00
Quentin McGaw
4d60b71583 feat(dns): replace unbound with qdm12/dns@v2.0.0-beta-rc6 (#1742)
- Faster start up
- Clearer error messages
- Allow for more Gluetun-specific customization
- DNSSEC validation is dropped for now (it's sort of unneeded)
- Fix #137
2024-08-21 14:35:41 +02:00
Quentin McGaw
3f130931d2 hotfix(firewall): fix ip prefix parsing for ipv6 (again) 2024-08-19 17:06:45 +00:00
Quentin McGaw
946f055fed hotfix(firewall): handle iptables CIDR ranges with 3 digits for IPv6 2024-08-19 14:02:53 +00:00
Quentin McGaw
eaece0cb8e fix(ivpn): split city into city and region
- Fix bad city values containing a comma
- update ivpn servers data
2024-08-19 03:10:53 +00:00
Quentin McGaw
4203f4fabf fix(nordvpn): remove commas from region values 2024-08-19 03:08:14 +00:00
Quentin McGaw
c39edb6378 fix(pia): support port forwarding using Wireguard (#2420)
- Build API IP address using the first 2 bytes of the gateway IP and adding `128.1` to it
- API IP address is valid for both OpenVPN and Wireguard
- Fix #2320
2024-08-19 03:19:16 +02:00
Quentin McGaw
b3cc2781ff hotfix(config): fix missing test lines for previous commit 2024-08-19 01:00:30 +00:00
Jean-François Roy
12c411e203 feat(storage): STORAGE_FILEPATH option (#2416)
- `STORAGE_FILEPATH=` disables storing to and reading from a local servers.json file
- `STORAGE_FILEPATH` defaults to `/gluetun/servers.json`
- Fix #2074
2024-08-19 02:26:46 +02:00
Quentin McGaw
3bf937d705 feat(privado): update servers data 2024-08-18 23:29:10 +00:00
Quentin McGaw
bc55c25e73 fix(firewall): delete chain rules by line number (#2411)
- Fix #2334 
- Parsing of iptables chains, contributing to progress for #1856
2024-08-17 20:12:22 +02:00
Quentin McGaw
897a9d7f57 feat(config): allow invalid server filters (#2419)
- Disallow setting a server filter when there is no choice available
- Allow setting an invalid server filter when there is at least one choice available
- Log at warn level when an invalid server filter is set
- Fix #2337
2024-08-17 12:01:26 +02:00
Quentin McGaw
4a128677dd chore(github): add 2 labels
- servers storage category
- nearly resolved status
2024-08-17 10:00:23 +00:00
Quentin McGaw
9233f3f5ba feat(pia/updater): use v6 API to get servers data 2024-08-16 12:40:22 +00:00
Quentin McGaw
11c2354408 feat(privatevpn): native port forwarding support (#2285) 2024-08-16 14:20:00 +02:00
Quentin McGaw
1f2882434a feat(format-servers): add json format option 2024-08-16 10:14:06 +00:00
dependabot[bot]
01aaf2c86a Chore(deps): Bump golang.org/x/net from 0.25.0 to 0.28.0 (#2401) 2024-08-09 11:35:01 +02:00
dependabot[bot]
d260ac7a49 Chore(deps): Bump golang.org/x/text from 0.15.0 to 0.17.0 (#2400) 2024-08-09 11:34:47 +02:00
dependabot[bot]
0bea0d4ecd Chore(deps): Bump docker/build-push-action from 5 to 6 (#2324) 2024-08-09 11:34:19 +02:00
dependabot[bot]
59994bd6e7 Chore(deps): Bump github.com/klauspost/compress from 1.17.8 to 1.17.9 (#2319) 2024-08-09 11:34:02 +02:00
dependabot[bot]
62799d2449 Chore(deps): Bump golang.org/x/sys from 0.20.0 to 0.24.0 (#2404) 2024-08-09 11:33:22 +02:00
Quentin McGaw
09c47c740c fix(version): log last release by tag name alphabetically instead of date 2024-08-09 07:43:48 +00:00
dependabot[bot]
ecbfc02713 Chore(deps): Bump github.com/breml/rootcerts from 0.2.16 to 0.2.17 (#2316) 2024-08-09 09:07:35 +02:00
Quentin McGaw
7be9288685 fix(privatevpn): set openvpn vpn type for no hostname server 2024-08-09 06:24:06 +00:00
Quentin McGaw
d1f57d0e36 chore(deps): bump gosplash to v0.2.0
- Merge same links in the same line
- Add `/choose` suffix to github links
2024-08-05 17:46:31 +00:00
Quentin McGaw
74ea1a0f5a hotfix(firewall): prefer ip6tables (nft) instead of ip6tables-legacy 2024-08-05 14:01:27 +00:00
Quentin McGaw
2a9ab29e7d fix(firewall): VPN_PORT_FORWARDING_LISTENING_PORT behavior fixed again
- allow redirection destination port in INPUT table
2024-08-05 13:57:30 +00:00
Quentin McGaw
8be78a5741 chore(github): add /choose suffix to issue and discussion links 2024-08-05 13:39:32 +00:00
Quentin McGaw
4a669c3458 chore(dev): upgrade organizeImports vscode setting from true to explicit 2024-08-05 13:39:01 +00:00
Quentin McGaw
ae5b71a864 chore(lint): remove now invalid skip-dirs configuration block 2024-08-05 13:38:32 +00:00
Quentin McGaw
6fff2ce1a4 chore(deps): tidy Go modules dependencies 2024-08-05 13:38:15 +00:00
Quentin McGaw
f6165d206a fix(firewall): VPN_PORT_FORWARDING_LISTENING_PORT behavior fixed
by not restricting the destination address to 127.0.0.1
2024-08-05 13:37:49 +00:00
Quentin McGaw
8dbe7b8888 hotfix(readme): add perfect privacy as port forwarding natively supported 2024-08-04 09:00:06 +00:00
Quentin McGaw
10f43d7a70 chore(github): add before next release github label 2024-08-04 08:35:57 +00:00
Quentin McGaw
01283def17 fix(format-servers): add missing vpn type column for natively supported providers
- nordvpn
- surfshark
2024-08-04 08:33:24 +00:00
Quentin McGaw
b32e085354 docs(readme): update list of providers supporting Wireguard with the custom provider 2024-08-03 14:32:41 +00:00
Quentin McGaw
ac9446e296 feat(protonvpn): Wireguard support (#2390) 2024-08-03 16:10:35 +02:00
Quentin McGaw
dea4080a7b fix(custom-openvpn): remove comments before parsing file 2024-08-03 13:37:57 +00:00
Quentin McGaw
2e63dba817 docs(readme): add protonvpn as custom port forwarding implementation 2024-08-03 09:54:14 +00:00
Quentin McGaw
10384c9e37 chore(github): add labels "Custom" and "Category: logs" 2024-08-01 12:20:55 +00:00
Quentin McGaw
34e8f5f3a9 hotfix(custom): assume all custom servers support port forwarding
- Fix custom wireguard with the protonvpn port forwarding implementation
- Might fix #2389
2024-08-01 11:52:38 +00:00
Quentin McGaw
ceb6ff4ca4 hotfix(protonvpn): fix free detection and update p2p->port_forward 2024-07-31 21:04:05 +00:00
Quentin McGaw
4c3da54303 chore(example/updater): simplify update code for openvpn+wireguard servers 2024-07-31 16:08:49 +00:00
Quentin McGaw
5d75bbc869 feat(config): only use port forwarding servers when port forwarding is enabled for ProtonVPN and PIA 2024-07-31 14:49:33 +00:00
Quentin McGaw
72e227f87d fix(config): log out if port forwarding only servers is enabled 2024-07-31 14:44:19 +00:00
Quentin McGaw
c5c37e7f96 hotfix(protonvpn): port forward only option
- Allow to use with Protonvpn (not just PIA)
- Update code comments
- Check server supports port forwarding when invoking port forward code
2024-07-31 14:43:59 +00:00
Quentin McGaw
aaf3019d8c hotfix(protonvpn): add markdown headers for servers 2024-07-31 14:33:24 +00:00
Quentin McGaw
5191f3558f hotfix(protonvpn): drop P2P_ONLY in favor of PORT_FORWARD_ONLY 2024-07-31 14:29:31 +00:00
Quentin McGaw
13ffffb157 feat(fastestvpn): Wireguard support (#2383)
Credits to @Zerauskire for the initial investigation and @jvanderzande for an initial implementation as well as reviewing the pull request
2024-07-31 16:16:50 +02:00
Quentin McGaw
7bc2972b27 feat(perfectprivacy): port forwarding support (#2378) 2024-07-30 22:00:26 +02:00
Quentin McGaw
ab08a5e666 feat(fastestvpn): update servers data using API instead of zip file
- Add city filter
- More dynamic to servers updates on fastestvpn's end
- Update servers data
2024-07-30 14:50:32 +00:00
Quentin McGaw
8c730a6e4a chore(port-forward): support multiple port forwarded 2024-07-29 13:55:28 +00:00
Quentin McGaw
4c47b6f142 feat(protonvpn): determine free status with tier value 2024-07-29 13:55:27 +00:00
Quentin McGaw
264480b659 hotfix(pia): remove crl verify
- Refers to issue #2376
2024-07-29 08:23:39 +00:00
Koen van Zuijlen
cb99f90bb5 feat(protonvpn): feature filters (#2182)
- `SECURE_CORE_ONLY`
- `TOR_ONLY`
- `P2P_ONLY`
2024-07-29 08:57:31 +02:00
Quentin McGaw
2bf2525bc5 chore(config): split server filter validation for features and subscription tier
- `validateSubscriptionTierFilters` function
- `validateFeatureFilters` function
- idea introduced in #2182
2024-07-29 06:18:28 +00:00
Quentin McGaw
26705f5a23 hotfix(firewall): re-add iptables-legacy for setups with nft kernel support 2024-07-29 05:43:34 +00:00
Quentin McGaw
ddbfdc9f14 feat(firewall): prefer using iptables nft instead of legacy 2024-07-28 14:29:00 +00:00
Quentin McGaw
9807d5f8f5 feat(docker): bump Alpine from 3.19 to 3.20
- Fix iptables and iptables-nft behavior
- Address systems not supporting iptables-legacy, see #2304
2024-07-28 12:43:33 +00:00
Quentin McGaw
921992ebc7 chore(build): do not upgrade busybox since vulnerabilities are fixed now 2024-07-28 12:41:54 +00:00
Quentin McGaw
8331ce6010 chore(github): disable blank issues 2024-07-27 11:44:23 +00:00
Quentin McGaw
36c8da7ea7 hotfix(config): split common VPN options per VPN type
- Split `VPN_ENDPOINT_IP` in `OPENVPN_ENDPOINT_IP` and `WIREGUARD_ENDPOINT_IP`
- Split `VPN_ENDPOINT_PORT` in `OPENVPN_ENDPOINT_PORT` and `WIREGUARD_ENDPOINT_PORT`
- Fixes bad usage of Wireguard config file endpoint for OpenVPN #2347
2024-07-27 10:42:01 +00:00
Quentin McGaw
73832d8b49 hotfix(firewall): add iptables -m flag for input port instructions 2024-07-26 11:40:12 +00:00
Quentin McGaw
a03041cfea hotfix(config): do not log retro-compat warning when using OPENVPN_USER or OPENVPN_PASSWORD 2024-07-26 08:42:32 +00:00
Quentin McGaw
e7381b3800 chore(config): rename FIREWALL to FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT 2024-07-26 08:25:05 +00:00
Quentin McGaw
9d50c23532 hotfix(config): upgrade gosettings to v0.4.2
- Fix handling of retro-compatible keys
2024-07-12 19:54:17 +00:00
Quentin McGaw
0501743814 feat(pia): port forwarding options VPN_PORT_FORWARDING_USERNAME and VPN_PORT_FORWARDING_PASSWORD
- Retro-compatible with `OPENVPN_USER` + `OPENVPN_PASSWORD`
- No more reading for the OpenVPN auth file
- Allow to use PIA port forwarding with Wireguard
2024-07-09 14:44:46 +00:00
Quentin McGaw
06c9bc55d3 hotfix(firewall): prefer using ip6tables-legacy
- Fixes issue #2334
2024-07-08 08:57:57 +00:00
Quentin McGaw
fe05521f2b feat(config): read wireguard config file without case sensitivity 2024-06-28 20:57:57 +00:00
Quentin McGaw
93ed87d12b hotfix(pia): allow one value in SERVER_NAMES for the custom provider 2024-06-17 22:34:59 +00:00
Quentin McGaw
4218dba177 fix(publicip): abort ip data fetch if vpn context is canceled
- Prevents requesting the public IP address N times after N VPN failures
- Fetching runs with a context local to the 'single run'
- Single run writes single run result to a channel back to the caller, RunOnce is now blocking
2024-05-18 18:06:01 +00:00
Quentin McGaw
7872ab91dc chore(pia): return an error to pf loop if server cannot port forward 2024-05-18 12:00:57 +00:00
dependabot[bot]
c9e75bd697 Chore(deps): Bump golang.org/x/net from 0.24.0 to 0.25.0 (#2269) 2024-05-18 12:10:58 +02:00
dependabot[bot]
7453f7f59a Chore(deps): Bump github.com/fatih/color from 1.16.0 to 1.17.0 (#2279) 2024-05-18 12:10:49 +02:00
Quentin McGaw
19a9ac9fd7 hotfix(torguard): remove second certificate 2024-05-16 18:50:36 +00:00
Quentin McGaw
ecb06836b5 chore(deps): bump wireguard go dependencies 2024-05-10 14:38:18 +00:00
Quentin McGaw
1e25372189 fix(health): docker healthcheck has 3 retries 2024-05-10 14:31:01 +00:00
Quentin McGaw
6042a9e3c2 feat(health): change timeout mechanism
- Healthcheck timeout is no longer fixed to 3 seconds
- Healthcheck timeout increases from 2s to 4s, 6s, 8s, 10s
- No 1 second wait time between check retries after failure
- VPN internal restart may be delayed by a maximum of 10 seconds
2024-05-10 14:15:03 +00:00
Quentin McGaw
fd4689ee70 fix(ipv6): detect ignoring loopback route destinations 2024-05-10 14:15:03 +00:00
Quentin McGaw
4bd16373f2 fix(torguard): update OpenVPN configuration
- add aes-128-gcm and aes-128-cbc ciphers
- add extra CA certificate value
- remove mssfix option
- remove sndbuf and rcvbuf ption
- remove ping option
- remove reneg option
2024-05-10 14:14:42 +00:00
Quentin McGaw
ce642a6d8b hotfix(firewall): prefer using iptables-legacy over nf_tables
- due to nf_tables bugs I discovered and reported
2024-05-09 14:33:34 +00:00
Quentin McGaw
ef6874fe57 fix(firewall): query iptables version for iptables found 2024-05-04 16:19:30 +00:00
Quentin McGaw
29bc60bc35 chore(github): add labels
- Popularity extreme and high
- Closed cannot be done
- Categories kernel and public IP service
2024-05-02 17:07:38 +00:00
Quentin McGaw
fb145d68a0 hotfix(firewall): support iptables-legacy for older kernels 2024-05-02 16:54:29 +00:00
Quentin McGaw
6dd27e53d4 chore(portforward): remove PIA dependency on storage package 2024-05-02 09:18:35 +00:00
Quentin McGaw
e0a977cf83 change(openvpn): default upgraded from 2.5 to 2.6 2024-05-02 08:13:51 +00:00
Quentin McGaw
4d002a3ad6 feat(docker): bump Alpine from 3.18 to 3.19 2024-05-02 07:43:05 +00:00
dependabot[bot]
4206859cad Chore(deps): Bump peter-evans/dockerhub-description from 3 to 4 (#2075) 2024-05-02 09:25:48 +02:00
wanshuangcheng
5dacbb994f chore(all): fix typos in code comments (#2216) 2024-05-02 09:24:49 +02:00
dependabot[bot]
ebf4bf9ea8 Chore(deps): Bump golang.org/x/net from 0.22.0 to 0.24.0 (#2208) 2024-05-02 09:20:22 +02:00
dependabot[bot]
241a9930c9 Chore(deps): Bump github.com/klauspost/compress from 1.17.7 to 1.17.8 (#2218) 2024-05-02 09:20:02 +02:00
Quentin McGaw
f1e8200cfc chore(deps): tidy go modules 2024-05-02 07:02:51 +00:00
dependabot[bot]
03eddb1698 Chore(deps): Bump DavidAnson/markdownlint-cli2-action from 14 to 16 (#2214) 2024-05-02 08:42:17 +02:00
Quentin McGaw
b25ee21e3e fix(custom): do not set server name if it does not exist 2024-05-01 19:35:40 +00:00
Quentin McGaw
7e0738d113 fix(vpnunlimited): allow OpenVPN TCP on port 1197 2024-04-30 08:05:22 +00:00
Quentin McGaw
0b078e5f5e fix(vpnunlimited): change UDP port from 1194 to 1197 2024-04-30 08:04:02 +00:00
Quentin McGaw
45fe38e670 fix(custom): parse port option line for OpenVPN 2024-04-30 08:02:28 +00:00
Quentin McGaw
72e2e4b82c fix(custom): set server name if names filter is not empty
- fix PIA port forwarding code usage
- refers to #2147
2024-04-29 19:23:34 +00:00
Quentin McGaw
bdc594c297 feat(airvpn): set default mssfix to 1320-28 2024-04-29 13:43:28 +00:00
Quentin McGaw
1afe01d8cd feat(vpnsecure): update servers data 2024-04-29 09:46:48 +00:00
Quentin McGaw
234e54ac5c chore(vpnsecure): associate "N / A" with no data for servers 2024-04-29 09:46:40 +00:00
Quentin McGaw
49b8f8b443 feat(surfshark): update servers data 2024-04-29 09:38:31 +00:00
Quentin McGaw
ce75c5ca21 fix(surfshark): remove outdated hardcoded retro servers 2024-04-29 09:37:58 +00:00
Quentin McGaw
e07966f71e fix(tun): only create tun device if it does not exist
- do not create if it exists and is problematic
- wrap errors with a better context
2024-04-29 09:29:06 +00:00
Quentin McGaw
c5395adfea hotfix(ci): skip console dead link check 2024-04-28 19:42:39 +00:00
Quentin McGaw
9d1ec69b73 chore(github): remove from_name in labels configuration 2024-04-28 19:28:04 +00:00
Quentin McGaw
ee8802ee86 docs(readme): clarify shadowsocks proxy is a server
- Fixes issue #2191
2024-04-28 19:27:14 +00:00
Quentin McGaw
0d7115c832 chore(github): review all labels
- add closed labels
- add category labels
- rename labels
- add label category prefix
- add emojis for each label
2024-04-28 19:24:46 +00:00
Quentin McGaw
08fb049f63 chore(github): remove empty label description fields 2024-04-28 13:43:35 +00:00
Quentin McGaw
c87c0e12fe feat(wireguard): WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL option 2024-04-25 10:44:13 +00:00
Quentin McGaw
7b4befce61 hotfix(settings): openvpn encrypted key reading from file 2024-04-08 07:40:14 +00:00
Quentin McGaw
6709a248d6 hotfix(settings): HTTPPROXY_LOG reading fixed 2024-04-08 07:35:24 +00:00
Quentin McGaw
bf4cc0dabf fix(server): /openvpn route status get and put
- get status return stopped if running wireguard
- put status changes vpn type if running wireguard
2024-04-08 07:28:56 +00:00
Quentin McGaw
982100782c hotfix(config/nordvpn): do not initialize wg addresses to an empty slice 2024-04-02 14:48:40 +00:00
Quentin McGaw
4afbe9332f hotfix(settings): trim space for each wireguard address 2024-04-01 13:04:00 +00:00
Quentin McGaw
4019ee3ea1 hotfix(settings): fix retro-compatible key message 2024-04-01 12:59:47 +00:00
Quentin McGaw
e859c60343 hotfix(config/wireguard): add /32 to address without bits specified 2024-03-29 15:06:34 +00:00
Quentin McGaw
8454123cae fix(publicip/api): ip2location parsing for latitude and longitude 2024-03-29 14:54:45 +00:00
Quentin McGaw
6b2f350ec9 hotfix(config): update to gosettings v0.4.1
- fix bad reading of files and secret files
2024-03-26 07:24:45 +00:00
Quentin McGaw
e01ce9c6d8 hotfix(config): read wireguard config from /gluetun/wireguard/wg0.conf 2024-03-26 07:04:26 +00:00
Quentin McGaw
ecc80a5a9e chore(config): upgrade to gosettings v0.4.0
- drop qdm12/govalid dependency
- upgrade qdm12/ss-server to v0.6.0
- do not unset sensitive config settings (makes no sense to me)
2024-03-25 19:14:20 +00:00
Quentin McGaw
23b0320cfb chore(tun): fix unit test for unprivileged user 2024-03-25 18:44:47 +00:00
Quentin McGaw
3e79509c97 chore(build): bump Go from 1.21 to 1.22 2024-03-25 18:44:44 +00:00
Quentin McGaw
2185f347ce chore(deps): bump github.com/stretchr/testify to v1.9.0 2024-03-25 18:29:55 +00:00
dependabot[bot]
aa3ef5a1c2 Chore(deps): Bump golang.org/x/net from 0.19.0 to 0.22.0 (#2138) 2024-03-25 18:29:55 +00:00
dependabot[bot]
acec050b95 Chore(deps): Bump golang.org/x/sys from 0.15.0 to 0.18.0 (#2139) 2024-03-25 18:29:55 +00:00
dependabot[bot]
9ca97fb04f Chore(deps): Bump github.com/klauspost/compress from 1.17.4 to 1.17.7 (#2178) 2024-03-25 18:29:55 +00:00
Quentin McGaw
4776948af6 chore(dev): fix source.organizeImports vscode setting value 2024-03-25 18:29:55 +00:00
Quentin McGaw
4d9c619b24 chore(config): use openvpn protocol string field instead of TCP bool 2024-03-25 18:29:51 +00:00
Quentin McGaw
62007bf1a1 chore(config): provider name field as string instead of string pointer 2024-03-23 09:41:25 +00:00
Quentin McGaw
7674efe8d7 chore(config): remove bad retro-compatiblity for HTTP_CONTROL_SERVER_ADDRESS
- Retro-compatible variable key CONTROL_SERVER_ADDRESS was never defined
- Old variable key CONTROL_SERVER_PORT was removed in v3.28.0 and no complain so far
2024-03-23 09:37:54 +00:00
Adam Hebden
b3ceece779 feat(nordvpn): filter with SERVER_CATEGORIES (#1806)
- update NordVPN servers data built-in
2024-03-22 10:02:31 +01:00
Quentin McGaw
c74e4178bb feat(nordvpn): update mechanism uses v2 API 2024-03-21 17:02:25 +00:00
Quentin McGaw
c0621bf381 chore(lint): upgrade linter to v1.56.2 2024-03-21 17:02:11 +00:00
Dennis Gaida
fb00fb16c2 feat(settings): load wireguard individual fields as secret files (#1348)
- Private key from `/run/secrets/wireguard_private_key` (path configurable with `WIREGUARD_PRIVATE_KEY_SECRETFILE`)
- Preshared key from `/run/secrets/wireguard_preshared_key` (path configurable with `WIREGUARD_PRESHARED_KEY_SECRETFILE`)
- Addresses from `/run/secrets/wireguard_addresses` (path configurable with `WIREGUARD_ADDRESSES_SECRETFILE`)
2024-03-21 10:08:41 +01:00
Quentin McGaw
6096b7ad4b feat(config): read Wireguard config from secret
- defaults to `/run/secrets/wg0.conf`
- can be changed with variable `WIREGUARD_CONF_SECRETFILE`
2024-03-21 08:18:14 +00:00
Quentin McGaw
9cb4c74493 chore(ci): pin docker/build-push-action to v5 2024-03-21 07:36:56 +00:00
Quentin McGaw
e470dc8a12 chore(ci): add opened issue workflow 2024-03-21 07:33:47 +00:00
dependabot[bot]
ab49f1f733 Chore(deps): Bump github.com/breml/rootcerts from 0.2.14 to 0.2.16 (#2094) 2024-03-21 08:33:38 +01:00
Quentin McGaw
62158a1739 hotfix(settings): copy port forward only field 2024-03-19 15:20:04 +00:00
Quentin McGaw
3d16798544 fix(torguard): set user agent to download zip files 2024-03-18 17:46:49 +00:00
Anton Nesterov
b51aa0c6b9 feat(pia): PORT_FORWARD_ONLY variable (#2070) 2024-03-18 18:40:09 +01:00
Anton Nesterov
84d00b42f1 fix(config): STREAM_ONLY should set StreamOnly flag for server selection (#2126) 2024-03-18 16:01:00 +01:00
Quentin McGaw
e201856667 fix(ci): set issue number correctly for closed issue workflow 2024-03-07 12:33:09 +00:00
Quentin McGaw
3254fc8aa6 feat(servers): update vyprnvpn data 2024-03-07 12:29:15 +00:00
Quentin McGaw
4bca4ca932 chore(github): add closed issue workflow 2024-02-21 17:41:41 +00:00
Quentin McGaw
a20695ffb3 feat(servers): update vpn unlimited data 2024-02-21 16:58:12 +00:00
Quentin McGaw
d01cfef039 feat(servers): update pia data 2024-02-21 16:54:49 +00:00
Quentin McGaw
0eed558b10 feat(log): change unhealthy log to debug level 2024-02-21 16:44:35 +00:00
Quentin McGaw
423a5c37e0 feat(publicip): PUBLICIP_API variable supporting ipinfo and ip2location 2024-02-14 07:36:45 +00:00
Quentin McGaw
cfca026621 chore(publicip): less coupling with ipinfo.io 2024-02-14 07:30:02 +00:00
Quentin McGaw
6a6337b98f feat(publicip/ipinfo): add PUBLICIP_API_TOKEN variable 2024-02-13 10:55:06 +00:00
Quentin McGaw
72b5afc771 fix(privado): update Zip file URL and update servers data
- Fix Bug: privado update url outdated #2104
2024-02-13 10:22:49 +00:00
Quentin McGaw
659bc0c9cb fix(surfshark): remove no longer valid multi hop regions 2024-02-07 08:36:33 +00:00
Quentin McGaw
827e591174 chore(settings): clearer error message for surfshark regions
- only log possible 'new' server regions
- do not log old retro-compatible server regions
- maintains compatibility with older names
2024-02-06 20:39:00 +00:00
Quentin McGaw
a369745101 chore(surfshark): fail validation for empty string region 2024-02-06 20:36:30 +00:00
Quentin McGaw
586b0e17a0 chore(health): more obvious log to see the health guide and to reduce issues 2024-02-03 20:13:05 +00:00
Quentin McGaw
b5f1055682 hotfix(settings): allow public IP outbound subnets but not the unspecified address 2024-02-03 20:02:43 +00:00
Quentin McGaw
6b9c775055 feat(settings): prevent public firewall outbound subnets 2024-01-29 18:26:23 +00:00
Quentin McGaw
d8b9b2a85b feat(natpmp): rpc error contain all failed attempt messages 2024-01-19 16:44:45 +00:00
Quentin McGaw
c826707d42 fix(vpnunlimited): specify OpenVPN cipher and auth 2024-01-01 18:21:24 +00:00
Quentin McGaw
8a17cd87c3 feat(windscribe): update servers data 2024-01-01 18:09:05 +00:00
Quentin McGaw
f8da1e79bc fix(vpnunlimited): remove DEFAULT:@SECLEVEL=0 2023-12-22 09:39:34 +00:00
Quentin McGaw
cfc29d6a6b feat(vpnunlimited): add second CA certificate for OpenVPN 2023-12-19 18:21:57 +00:00
Quentin McGaw
5467652b8b chore(openvpn): support multiple CAs in generated config 2023-12-19 18:21:03 +00:00
Quentin McGaw
daa63c276d fix(vpnunlimited): update CA certificate 2023-12-19 18:15:56 +00:00
dependabot[bot]
ab96acdc5b Chore(deps): Bump golang.org/x/crypto from 0.16.0 to 0.17.0 (#2012) 2023-12-19 19:07:14 +01:00
dependabot[bot]
6e108706a1 Chore(deps): Bump docker/build-push-action from 5.0.0 to 5.1.0 (#1969) 2023-12-19 19:07:03 +01:00
Quentin McGaw
4a6c229504 fix(settings): add VPN Unlimited warning for OpenSSL 3 2023-12-15 10:32:53 +00:00
dependabot[bot]
ed3a72790a Chore(deps): Bump DavidAnson/markdownlint-cli2-action from 13 to 14 (#1982) 2023-12-14 17:08:12 +01:00
dependabot[bot]
4bf5777f23 Chore(deps): Bump golang.org/x/net from 0.18.0 to 0.19.0 (#1985) 2023-12-14 17:07:43 +01:00
dependabot[bot]
f0f9bdb883 Chore(deps): Bump github.com/klauspost/compress from 1.17.2 to 1.17.4 (#1993) 2023-12-14 17:07:33 +01:00
dependabot[bot]
4984d90b5a Chore(deps): Bump github/codeql-action from 2 to 3 (#2002) 2023-12-14 17:07:20 +01:00
dependabot[bot]
b5e648d13a Chore(deps): Bump github.com/breml/rootcerts from 0.2.13 to 0.2.14 (#1981) 2023-12-14 16:18:49 +01:00
yifangd
f71a1b083b fix(purevpn): fix update url and update servers (#1992)
See https://support.purevpn.com/purevpn/openvpn-files
2023-12-14 16:18:32 +01:00
Quentin McGaw
75fd869625 fix(firewall): handle OpenVPN tcp-client as tcp 2023-12-14 15:10:33 +00:00
Quentin McGaw
657b4b787f fix(custom): read wireguard presharedkey from peer section 2023-12-09 17:26:01 +00:00
Quentin McGaw
32d6453918 fix(custom): default TCP port for any tcp proto 2023-11-28 07:51:29 +00:00
Quentin McGaw
c326b616b4 feat(custom): support tcp-client proto for OpenVPN 2023-11-28 07:48:55 +00:00
Quentin McGaw
d5376629df fix(format-servers): add server name header for PIA 2023-11-28 07:31:36 +00:00
Quentin McGaw
3e825d7a08 fix(format-servers): for providers with dashes 2023-11-28 07:27:29 +00:00
dependabot[bot]
059b12883f Chore(deps): Bump golang.org/x/text from 0.13.0 to 0.14.0 (#1946) 2023-11-23 08:37:45 +00:00
dependabot[bot]
74aa509644 Chore(deps): Bump golang.org/x/net from 0.17.0 to 0.18.0 (#1953) 2023-11-23 08:37:45 +00:00
Quentin McGaw
4105f74ce1 feat(portforward): port redirection with VPN_PORT_FORWARDING_LISTENING_PORT 2023-11-23 08:37:43 +00:00
Quentin McGaw
8318be3159 feat(nordvpn): add access token warning if used as wireguard private key 2023-11-08 10:07:02 +00:00
Quentin McGaw
de196490db fix(settings): wireguard preshared key from toml file 2023-11-08 10:06:57 +00:00
dependabot[bot]
ab7d1ccf3d Chore(deps): Bump github.com/fatih/color from 1.15.0 to 1.16.0 (#1950) 2023-11-08 10:36:05 +01:00
dependabot[bot]
ed49a7a7c0 Chore(deps): Bump docker/login-action from 2 to 3 (#1936) 2023-11-01 14:39:03 +01:00
dependabot[bot]
135832d985 Chore(deps): Bump docker/metadata-action from 4 to 5 (#1937) 2023-11-01 14:38:54 +01:00
dependabot[bot]
1adbd9f692 Chore(deps): Bump docker/setup-buildx-action from 2 to 3 (#1938) 2023-11-01 14:38:46 +01:00
Quentin McGaw
26e1c92841 fix(shadowsocks): bump from v0.5.0-rc1 to v0.5.0
- treat udp read error as non critical
- log out crash error for tcpudp combined server
2023-11-01 13:38:30 +00:00
dependabot[bot]
3c5b3514fb Chore(deps): Bump actions/checkout from 3 to 4 (#1847) 2023-10-31 16:04:29 +01:00
dependabot[bot]
f884293f6e Chore(deps): Bump github.com/breml/rootcerts from 0.2.11 to 0.2.13 (#1800) 2023-10-31 16:03:28 +01:00
dependabot[bot]
c67bd1aa2a Chore(deps): Bump golang.org/x/text from 0.11.0 to 0.13.0 (#1845) 2023-10-31 16:03:16 +01:00
dependabot[bot]
77ace9377d Chore(deps): Bump golang.org/x/net from 0.12.0 to 0.17.0 (#1907) 2023-10-31 16:02:46 +01:00
dependabot[bot]
6e676209ff Chore(deps): Bump docker/setup-qemu-action from 2 to 3 (#1861) 2023-10-31 14:08:18 +01:00
dependabot[bot]
80917d58b2 Chore(deps): Bump docker/build-push-action from 4.1.1 to 5.0.0 (#1860) 2023-10-31 14:08:08 +01:00
dependabot[bot]
fa49f13f19 Chore(deps): Bump crazy-max/ghaction-github-labeler from 4 to 5 (#1858) 2023-10-31 14:07:35 +01:00
dependabot[bot]
1fcabd152f Chore(deps): Bump DavidAnson/markdownlint-cli2-action from 11 to 13 (#1871) 2023-10-31 14:07:25 +01:00
dependabot[bot]
385879c297 Chore(deps): Bump github.com/klauspost/compress from 1.16.7 to 1.17.2 (#1922) 2023-10-31 14:06:59 +01:00
dependabot[bot]
e0515cb458 Chore(deps): Bump golang.org/x/sys from 0.11.0 to 0.13.0 (#1897) 2023-10-31 14:06:48 +01:00
Quentin McGaw
1c43a1d55b fix(portforward): service start error not treated as critical
A service start error can happen if the service is started after the Wireguard VPN tunnel is up, but the tunnel does not work. The VPN is then internally restarted, causing the service start error, so it should not be treated as a critical error.
2023-10-07 13:21:32 +00:00
Quentin McGaw
6c639fcf7f fix(publicip): do not retry on too many requests 2023-10-07 12:59:43 +00:00
Quentin McGaw
ec1f252528 fix(portforward): different validation when vpn is up or not 2023-10-07 12:43:36 +00:00
Quentin McGaw
ee413f59a2 fix(protonvpn): set natpmp external port to 1 2023-10-06 16:09:05 +00:00
Quentin McGaw
d4df87286e fix(portforward): trigger after VPN restart 2023-09-28 14:00:58 +00:00
Quentin McGaw
a194906bdd chore(protonvpn): add debug logs for keeping port forwarded 2023-09-28 07:08:07 +00:00
Quentin McGaw
9b00763a69 feat(config): add /32 if not present for Wireguard addresses 2023-09-24 16:50:34 +00:00
Quentin McGaw
4d627bb7b1 feat(protonvpn): port forwarding connection refused error points to add +pmp to OpenVPN user 2023-09-24 15:15:05 +00:00
Quentin McGaw
dc8fc5f81f feat(updater): log warning about using -minratio 2023-09-24 15:05:39 +00:00
Quentin McGaw
b787e12e25 feat(surfshark): update servers data 2023-09-24 15:02:08 +00:00
Quentin McGaw
f96448947f fix(publicip): rework run loop and fix restarts
- Clearing IP data on VPN disconnection clears file
- More efficient partial updates
- Fix loop exit
- Validate settings before updating
2023-09-24 14:55:51 +00:00
Quentin McGaw
e64e5af4c3 chore(portforward): improve loop reliability
- handle settings update within run function
- signal back start result to update call
- update loop settings only when service is started
2023-09-24 10:28:10 +00:00
Quentin McGaw
aa6dc786a4 chore(provider): use type assertion for port forwarders 2023-09-23 13:02:09 +00:00
Quentin McGaw
84300db7c1 fix(portforward): restart service on run error
- fix when port assigned changes
2023-09-23 12:39:49 +00:00
Quentin McGaw
2ac0f35060 fix(protonvpn): crash service if port assigned changes 2023-09-23 12:36:13 +00:00
Quentin McGaw
1a865f56d5 chore(vpn): fix typo portForwader 2023-09-23 12:03:56 +00:00
Quentin McGaw
0406de399d chore(portforward): move vpn gateway obtention within port forwarding service 2023-09-23 12:03:06 +00:00
Quentin McGaw
71201411f4 fix(portforward): rework run loop and fix deadlocks (#1874) 2023-09-23 12:57:12 +02:00
Quentin McGaw
c435bbb32c docs(issue): provide minimum requirements for an issue
- title must be filled
- at least 10 lines of log provided
- Gluetun version must be provided
2023-09-22 09:22:13 +00:00
Quentin McGaw
4cbfea41f2 docs(issues): add Unraid as option in bug template 2023-09-22 09:16:44 +00:00
Quentin McGaw
f9c9ad34f7 feat(protonvpn): check udp vs tcp port forwarded 2023-09-22 08:50:19 +00:00
Quentin McGaw
4ea474b896 fix(routing): change firewall only for matching ip families 2023-09-20 10:45:13 +00:00
Quentin McGaw
6aa4a93665 change(format): use dashes instead of spaces for provider names
- `-private\ internet\ access` -> `private-internet-access`
- `-perfect\ privacy` -> `-perfect-privacy`
- `-vpn\ unlimited` -> `-vpn-unlimited`
2023-09-20 10:24:32 +00:00
Quentin McGaw
ea25a0ff89 fix(protonvpn): natpmp assigned ports logs removed 2023-09-20 09:51:13 +00:00
Quentin McGaw
659da67ed5 feat(cyberghost): update servers data 2023-09-20 09:35:28 +00:00
Quentin McGaw
ffc6d2e593 chore(lint): upgrade linter to v1.54.1 2023-09-20 09:34:32 +00:00
Quentin McGaw
03ce08e23d chore(build): upgrade Go to 1.21 2023-09-20 09:34:29 +00:00
Aleksa Siriški
3449e7a0e1 fix(publicip): IPv6 endpoint for ipinfo (#1853) 2023-09-13 16:37:39 +02:00
Quentin McGaw
c0062fb807 fix(protonvpn): natpmp check for assigned internal port 2023-09-13 14:18:35 +00:00
dependabot[bot]
1ac031e78c Chore(deps): Bump golang.org/x/sys from 0.10.0 to 0.11.0 (#1786) 2023-08-24 02:04:07 -07:00
Quentin McGaw
e556871e8b change(dns): DNS_KEEP_NAMESERVER leaves DNS fully untouched 2023-08-11 11:03:40 +00:00
Quentin McGaw
082a38b769 fix(netlink): try loading Wireguard module if not found (#1741) 2023-08-04 13:09:56 +02:00
Quentin McGaw
39ae57f49d fix(routing): add outbound subnets routes only for matching ip families 2023-07-28 07:24:26 +00:00
Quentin McGaw
9024912e17 fix(custom): allow custom endpoint port setting 2023-07-27 10:32:08 +00:00
Quentin McGaw
eecfb3952f chore(settings): change source precedence order
1. Secret files (program scope)
2. Files (program scope)
3. Environment variables (OS scope)
Fix #1759
2023-07-22 16:02:32 +00:00
Quentin McGaw
0ebfe534d3 feat(settings): parse Wireguard settings from /gluetun/wireguard/wg0.conf (#1120) 2023-07-22 17:25:30 +02:00
eiqnepm
c5cc240a6c feat(surfshark): update API endpoint and servers data (#1560) 2023-07-21 20:21:46 +02:00
Quentin McGaw
1a5a0148ea feat(torguard): update severs data 2023-07-18 16:02:06 +00:00
Quentin McGaw
abe2aceb18 feat(wireguard): clarify wireguard is up message 2023-07-18 15:53:39 +00:00
Quentin McGaw
fa541b8fc2 chore(deps): bump gosettings to v0.4.0-rc1 2023-07-11 13:26:55 +00:00
dependabot[bot]
a681d38dfb Chore(deps): Bump golang.org/x/net from 0.10.0 to 0.12.0 (#1729) 2023-07-09 14:22:14 +02:00
dependabot[bot]
a7b96e3f4d Chore(deps): Bump golang.org/x/sys from 0.8.0 to 0.10.0 (#1732) 2023-07-07 15:32:23 +02:00
dependabot[bot]
04ef92edab Chore(deps): Bump golang.org/x/text from 0.10.0 to 0.11.0 (#1726) 2023-07-07 12:56:47 +02:00
Quentin McGaw
919b55c3aa feat(wireguard): WIREGUARD_ALLOWED_IPS variable (#1291) 2023-07-06 09:08:59 +02:00
Quentin McGaw
9c0f187a12 chore(natpmp): more robust tests with longer connection durations 2023-07-06 06:54:01 +00:00
Quentin McGaw
075a1e2a80 chore(natpmp): initialRetry -> initialConnectionDuration 2023-07-06 06:50:17 +00:00
Quentin McGaw
f31a846cda chore(ci): add markdown-skip workflow 2023-07-05 15:45:46 +00:00
Quentin McGaw
9bef46db77 chore(ci): trigger markdown on pull requests
- Verification steps
- Publishing step to Docker Hub is reserved for pushes to the master branch
2023-07-05 15:44:33 +00:00
Quentin McGaw
d83217f7ac chore(ci): add markdown dead link checking 2023-07-05 14:47:52 +00:00
Quentin McGaw
1cd2fec796 chore(ci): add markdown linting to markdown workflow 2023-07-05 14:31:09 +00:00
Quentin McGaw
235f24ee5b chore(ci): add misspell action to markdown job 2023-07-05 14:28:56 +00:00
Quentin McGaw
2e34c6009e chore(ci): Markdown workflow triggers on *.md files 2023-07-05 14:28:50 +00:00
Quentin McGaw
c0eb2f2315 chore(ci): rename workflow to Markdown 2023-07-05 14:27:14 +00:00
Quentin McGaw
8ad16cdc12 feat(protonvpn): port forwarding support with NAT-PMP (#1543)
Co-authored-by: Nicholas Xavier <nicho@nicho.dev>
2023-06-30 20:09:44 +02:00
Quentin McGaw
fae6544431 feat(pf): VPN_PORT_FORWARDING_PROVIDER variable (#1616) 2023-06-30 19:24:01 +02:00
Quentin McGaw
f8a41b2133 fix(protonvpn): add aes-256-gcm cipher for openvpn 2023-06-30 17:14:44 +00:00
Quentin McGaw
ff9b56d6d8 docs(all): update to use newer wiki repository
- Update URLs logged by program
- Update README.md links
- Update contributing guide link
- Update issue templates links
- Replace Wiki issue template by link to Gluetun Wiki repository issue creation
- Set program announcement about Github wiki new location
2023-06-30 10:31:26 +00:00
Quentin McGaw
99d5a591b9 docs(readme): fixes and small changes
- remove `UPDATER_VPN_SERVICE_PROVIDERS` in docker-compose config
- remove Slack channel link (don't have time to check it)
- Update Wireguard native integrations support list
2023-06-29 16:28:24 +00:00
Quentin McGaw
fbe252a9b6 chore(Docker): add missing environment variables
- `OPENVPN_PROCESS_USER` defaults to `root`
- Add `HTTPPROXY_STEALTH=off`
- Add `HTTP_CONTROL_SERVER_LOG=on`
2023-06-29 16:20:25 +00:00
Quentin McGaw
76a92b90e3 fix(routing): VPNLocalGatewayIP Wireguard support 2023-06-28 14:23:34 +00:00
Quentin McGaw
2873b06275 fix(wireguard): wrap setupIPv6 rule error correctly 2023-06-28 13:08:23 +00:00
Quentin McGaw
9cdd6294d2 feat(mullvad): update servers data 2023-06-28 13:06:40 +00:00
dependabot[bot]
44bc60b00d Chore(deps): Bump docker/build-push-action from 4.0.0 to 4.1.1 (#1684) 2023-06-28 14:28:59 +02:00
dependabot[bot]
6f0be57860 Chore(deps): Bump golang.org/x/text from 0.9.0 to 0.10.0 (#1681) 2023-06-28 14:28:44 +02:00
Quentin McGaw
d3d8484b8e hotfix(env): case sensitivity for OPENVPN_CUSTOM_CONFIG 2023-06-28 12:27:13 +00:00
Quentin McGaw
515ae8efb3 hotfix(nordvpn): update url 2023-06-18 11:00:36 +00:00
Quentin McGaw
83826e1253 hotfix(settings): fix godot lint error 2023-06-12 13:51:50 +00:00
Quentin McGaw
4292a500ae fix(wireguard): delete existing Wireguard link before adding it 2023-06-10 20:23:21 +00:00
Quentin McGaw
4a0f9c36ba hotfix(nordvpn): accept countries in SERVER_REGIONS 2023-06-10 16:29:30 +00:00
Quentin McGaw
ea1991496e hotfix(routing): remove debug prints 2023-06-08 22:44:08 +00:00
Quentin McGaw
4675572328 hotfix(routing): change main table from 0 to 254 2023-06-08 20:03:07 +00:00
Quentin McGaw
412921fc1f hotfix(routing): ignore non-main table for routes
- When searching for default routes
- When searching for local networks
2023-06-08 19:50:42 +00:00
Quentin McGaw
1c905d0e6f chore(labels): add problem category labels
- Config problem
- Routing
- IPv6
- Port forwarding
2023-06-08 10:04:09 +00:00
Quentin McGaw
2ec9293324 feat(wireguard): MTU defaults to 1400 instead of 1420 2023-06-08 09:50:21 +00:00
Quentin McGaw
9b39a301a8 chore(routing): remove unused VPNDestinationIP 2023-06-08 09:17:27 +00:00
Quentin McGaw
cade2b99bf chore(routing): unexport IPIsPrivate as ipIsPrivate 2023-06-08 09:14:17 +00:00
Quentin McGaw
40cdb4f662 fix(netlink): RouteList list routes from all tables
- Do not filter by link anymore
- IPv6 detection simplified
2023-06-08 09:12:46 +00:00
Quentin McGaw
c58d6d4de2 chore(lint): upgrade to v1.53.2 and add linters
- gosmopolitan
- mirror
- tagalign
- zerologlint
2023-06-08 07:43:30 +00:00
Quentin McGaw
0da2b6ad0b chore(lint): add musttag linter and fix lint errors
Breaking change: JSON fields changed in the server API
2023-06-08 07:43:26 +00:00
Quentin McGaw
37f0e5c73b chore(lint): add linters dupword, paralleltest and gocheckcompilerdirectives 2023-06-08 07:40:37 +00:00
Quentin McGaw
a9cd7be3f9 chore(sources/env): bump gosettings to v0.3.0-rc13
- Use `RetroKeys` option with env.* method calls
- Use `CSV*` typed methods
- Inject `handleDeprecatedKey` function
2023-06-08 07:40:37 +00:00
Julio Gutierrez
07459ee854 feat(nordvpn): new API endpoint and wireguard support (#1380)
Co-authored-by: Quentin McGaw <quentin.mcgaw@gmail.com>
2023-06-08 09:39:07 +02:00
Quentin McGaw
943943e8d1 fix(settings): MergeWithSlice for both elements nil 2023-06-01 10:00:44 +00:00
Quentin McGaw
5927ee9dec chore(ci): trigger for PR to other branches 2023-06-01 09:09:01 +00:00
Quentin McGaw
3b136e02db chore(secrets): add test for readSecretFileAsStringPtr 2023-06-01 09:07:25 +00:00
Quentin McGaw
482447c151 chore(env): bump qdm12/gosettings to v0.3.0-rc11 2023-06-01 09:07:22 +00:00
Quentin McGaw
5d8fbf8006 fix(sources/secrets): do not lowercase env secret file paths 2023-06-01 08:20:13 +00:00
Quentin McGaw
2ab80771d9 feat(shadowsocks): bump from v0.4.0 to v0.5.0-rc1 2023-05-31 14:31:56 +00:00
Quentin McGaw
7399c00508 chore(sources/env): bump gosettings to v0.3.0-rc9 2023-05-31 14:31:56 +00:00
Leeroy Ding
2d2f657851 docs(readme): fix Alpine version from 3.17 to 3.18 (#1636) 2023-05-31 16:27:10 +02:00
dependabot[bot]
0e21fdc9de Chore(deps): Bump github.com/stretchr/testify from 1.8.3 to 1.8.4 (#1633) 2023-05-31 16:24:49 +02:00
Quentin McGaw
b87b2109b1 chore(settings): use gosettings/sources/env functions 2023-05-30 13:02:10 +00:00
Quentin McGaw
2c30984a10 hotfix(env): read some settings with case sensitivity 2023-05-30 12:46:10 +00:00
Quentin McGaw
47593928f9 fix(settings): use qdm12/gosettings env.Get 2023-05-29 20:43:06 +00:00
Quentin McGaw
b961284845 feat(dev): specify vscode recommendations 2023-05-29 16:42:00 +00:00
Quentin McGaw
b5d230d47a chore(dev): set build tag as linux for cross development 2023-05-29 16:40:10 +00:00
Quentin McGaw
c2972f7bf6 chore(dev): update devcontainer definitions 2023-05-29 15:57:09 +00:00
Quentin McGaw
aed235f52d chore(httpproxy): add Test_returnRedirect to prevent error wrap of ErrUseLastResponse 2023-05-29 09:44:49 +00:00
Quentin McGaw
bfe5e4380f fix(httpproxy): redirect from http to https 2023-05-29 09:39:48 +00:00
Quentin McGaw
eca182a32f chore(tun): not linux or not darwin tagged files 2023-05-29 09:36:29 +00:00
Quentin McGaw
caabaf918e feat(dev): support development on darwin (OSX)
- Netlink linux tagged files
- Netlink linux || darwin tagged files
- Create non-implemented files for NOT linux
- Create non-implemented files for NOT linux and NOT darwin
- Specify wireguard netlink integration test as for linux only
2023-05-29 07:26:59 +00:00
Quentin McGaw
d6924597dd chore(netlink): separate linux only and OS independent code
- Move `Addr` and its `String` method to `types.go`
- Move `IsWireguardSupported` to `wireguard.go` to have `family.go` OS independant
- Remove dependency on vishvananda/netlink in `ipv6.go`
- Move `Link` to `types.go`
- Move `Route` to `types.go`
- Move `Rule` and its `String` method to `types.go`
2023-05-29 06:56:55 +00:00
Quentin McGaw
c26476a2fd chore(netlink): remove unused link fields 2023-05-29 06:56:52 +00:00
Quentin McGaw
5be0d0bbba feat(wireguard): debug logs log obfuscated keys 2023-05-29 06:45:12 +00:00
Quentin McGaw
38ddcfa756 chore(netlink): define own types with minimal fields
- Allow to swap `github.com/vishvananda/netlink`
- Allow to add build tags for each platform
- One step closer to development on non-Linux platforms
2023-05-29 06:44:58 +00:00
Quentin McGaw
163ac48ce4 chore(wireguard): fix netlink integration test
- Broken since recent commit 9d1a0b60a2
2023-05-29 05:54:01 +00:00
Quentin McGaw
def407d610 chore(settings): use qdm12/gosettings functions
- use: FileExists, ObfuscateKey, BoolToYesNo
- remove local functions moved to gosettings
2023-05-28 10:33:36 +00:00
Quentin McGaw
22b2e2cc6e chore(deps): bump qdm12/gosettings to v0.3.0-rc4 2023-05-28 10:29:15 +00:00
Quentin McGaw
c92962e97c chore(deps): tidy Go dependencies 2023-05-28 10:26:25 +00:00
Quentin McGaw
9d1a0b60a2 fix(netlink): use AddrReplace instead of AddrAdd 2023-05-28 10:22:51 +00:00
Quentin McGaw
9cf2c9c4d2 chore(settings): remove now unused helpers/messages.go 2023-05-28 10:22:51 +00:00
Quentin McGaw
e7150ba254 chore(settings): remove unused settings helpers 2023-05-28 10:22:51 +00:00
Filippo Buletto
7ba70f19ef fix(settings): fix httpproxy.go error message (#1596) 2023-05-27 20:01:55 +02:00
dependabot[bot]
9488a9f88a Chore(deps): Bump github.com/breml/rootcerts from 0.2.10 to 0.2.11 (#1567) 2023-05-27 20:01:17 +02:00
dependabot[bot]
020196f1c3 Chore(deps): Bump github.com/stretchr/testify from 1.8.2 to 1.8.3 (#1575) 2023-05-27 20:01:08 +02:00
Quentin McGaw
7e325715c7 hotfix(settings): case insensitivity for server filters 2023-05-27 08:53:18 +00:00
Quentin McGaw
75670a80b8 chore(deps): bump gosettings and govalid 2023-05-27 08:52:41 +00:00
Quentin McGaw
a43973c093 chore(settings): use github.com/qdm12/gosettings 2023-05-25 12:08:43 +00:00
Quentin McGaw
1827a03afd fix(airvpn): allow Airvpn as Wireguard provider 2023-05-24 21:47:31 +00:00
Quentin McGaw
3100cc1e5e hotfix(routing): unmap ipv4-in-ipv6 when converting 2023-05-22 08:03:52 +00:00
Quentin McGaw
eed62fdc6d fix(routing): ip family match function
- ipv4-in-ipv6 should match ipv6
2023-05-22 06:01:52 +00:00
Quentin McGaw
d2b8dbcb10 chore(routing): remove old assigned ip debug log 2023-05-22 06:01:07 +00:00
Quentin McGaw
90d43856ef fix(routing): net.IPNet to netip.Prefix conversion 2023-05-22 06:00:24 +00:00
Quentin McGaw
86f95cb390 chore(docker): bump Alpine from 3.17 to 3.18 2023-05-21 13:25:01 +00:00
Quentin McGaw
3b807e2ca9 feat(openvpn): add support for openvpn 2.6 2023-05-21 13:23:51 +00:00
Quentin McGaw
e8f2296a0d change(openvpn): Openvpn 2.4 no longer supported 2023-05-21 13:20:02 +00:00
Lars Haalck
1dd38bc658 feat(wireguard): WIREGUARD_MTU enviromnent variable (#1571) 2023-05-21 15:11:07 +02:00
Quentin McGaw
63303bc311 fix(mullvad): add aes-256-gcm cipher 2023-05-21 12:33:27 +00:00
Julio Gutierrez
5200ee5722 chore(settings): use generics for helping functions (#1427) 2023-05-20 22:37:23 +02:00
Quentin McGaw
86ec75722a chore(wireguard): use netip.AddrPort instead of *net.UDPAddr 2023-05-20 20:06:12 +00:00
Quentin McGaw
0a29337c3b chore(all): replace net.IP with netip.Addr 2023-05-20 20:06:12 +00:00
Quentin McGaw
00ee6ff9a7 chore(wireguard): fix netlink integration tests 2023-05-20 20:06:12 +00:00
Quentin McGaw
6d0a2a968f chore(settings): remove unneeded CopyNetipPrefix 2023-05-20 20:06:12 +00:00
dependabot[bot]
4bb77ebcc5 Chore(deps): Bump golang.org/x/net from 0.9.0 to 0.10.0 (#1561) 2023-05-10 11:00:50 +02:00
dependabot[bot]
56ecfcb9f4 Chore(deps): Bump golang.org/x/sys from 0.7.0 to 0.8.0 (#1557) 2023-05-10 10:58:31 +02:00
15ky3
9a0fcbc011 fix(perfectprivacy): update cert and key (#1549)
Credits to @Thamos88 and @15ky3
2023-05-10 10:56:32 +02:00
Quentin McGaw
b6c8399c3b feat(health): HEALTH_SUCCESS_WAIT_DURATION 2023-05-07 09:35:51 +00:00
Quentin McGaw
7a88a09341 chore(healthcheck): prefer Go dialer 2023-05-06 07:14:34 +00:00
Quentin McGaw
912b31cfc6 fix(settings): clarify Wireguard provider error 2023-05-01 08:00:25 +00:00
Quentin McGaw
d21a943779 chore(all): use netip.Prefix for ip networks
- remove usage of `net.IPNet`
- remove usage of `netaddr.IPPrefix`
2023-04-27 13:42:50 +00:00
Quentin McGaw
801a7fd6fe chore(routing): simplify default routes for loop 2023-04-27 10:41:18 +00:00
Quentin McGaw
80053f6b7d feat(routing): log default route family as string 2023-04-27 10:41:03 +00:00
Quentin McGaw
e165bb6870 chore(dev): do not bind mount ~/.gitconfig 2023-04-27 10:27:40 +00:00
Quentin McGaw
67bd1171ae feat(env): rename vpn port forwarding variables
- `VPN_PORT_FORWARDING_STATUS_FILE`
- `VPN_PORT_FORWARDING`
- Deprecate PIA specific variables for VPN port forwarding
2023-04-27 10:23:55 +00:00
Quentin McGaw
4e2e46014d chore(settings): inet.af/netaddr -> net/netip 2023-04-23 11:43:50 +00:00
Quentin McGaw
1693c59e0d chore(lint): fix issues
- sources/env: remove unused `envToInt`
- fix `ireturn` error for `newCipherDESCBCBlock`
2023-04-22 11:02:53 +00:00
Quentin McGaw
9d4105ee59 chore(settings): remove unneeded pointers.go 2023-04-22 11:02:53 +00:00
Quentin McGaw
19585da3bc chore(deps): bump inet.af/netaddr 2023-04-20 23:25:41 +00:00
Quentin McGaw
51f830cfc1 chore(wireguard): bump dependencies 2023-04-20 23:24:58 +00:00
Quentin McGaw
804ea7ebd6 feat(surfshark): update servers data 2023-04-20 23:22:27 +00:00
Quentin McGaw
3294b8df60 feat(perfectprivacy): update servers data 2023-04-20 23:10:57 +00:00
Quentin McGaw
d77ec7a6cb fix(perfectprivacy): remove check for hostname in servers 2023-04-20 23:10:06 +00:00
Quentin McGaw
219d1f371c chore(all): wrap all sentinel errors
- Force to use `errors.Is` instead of `==` to compare errors
2023-04-20 23:10:06 +00:00
Quentin McGaw
fa7fd5f076 fix(pprof): settings rates can be nil 2023-04-20 23:10:02 +00:00
dependabot[bot]
d4f8eea7bf Chore(deps): Bump github.com/vishvananda/netlink from 1.1.1-0.20211129163951-9ada19101fc5 to 1.2.1-beta.2 (#1414) 2023-04-12 05:30:45 -07:00
Quentin McGaw
723d0f5e12 chore(lint): upgrade from v1.51.2 to v1.52.2 2023-04-12 09:40:00 +00:00
dependabot[bot]
20f4d8cc0b Chore(deps): Bump github.com/fatih/color from 1.14.1 to 1.15.0 (#1484) 2023-04-11 09:04:35 -07:00
dependabot[bot]
64cca69bf3 Chore(deps): Bump golang.org/x/net from 0.0.0-20220418201149-a630d4f3e7a2 to 0.9.0 (#1509) 2023-04-11 09:04:11 -07:00
Kyle Manna
fc8a2abb8f fix(routing): add policy rules for each destination local networks (#1493) 2023-04-11 09:03:07 -07:00
Quentin McGaw
16ecf48b89 fix(vpnunlimited): lower TLS security level to 0 (#1476) 2023-04-11 14:08:54 +02:00
Quentin McGaw
8fa4fd1b64 chore(labels): update labels
- remove issue category labels
- Add temporary status labels
- Add complexity labels
2023-04-03 11:58:12 +00:00
Quentin McGaw
4db6d1ecf9 chore(dev): add openssl to dev container 2023-04-03 10:41:37 +00:00
Quentin McGaw
3b86927ca7 fix(vpnsecure): upgrade Openvpn key encryption if needed (#1471) 2023-04-03 03:40:09 -07:00
Quentin McGaw
8bfa2f9b27 chore(docker): loosen pin for openssl 1.1
- Credits to @kylemanna
2023-04-03 08:21:58 +00:00
Quentin McGaw
fe2a3e4d11 chore(docker): remove no longer needed apk-tools 2023-04-03 08:20:08 +00:00
Quentin McGaw
b0451d8e50 feat(docker): install full-featured wget
- Fixes #1260, #1494
- Does not spawn openssl zombie processes
2023-04-03 08:10:48 +00:00
Quentin McGaw
a0b9044fd3 fix(hidemyass): add warning of end of life
- Fixes #1498
- Credits to @Fukitsu
2023-04-03 07:58:04 +00:00
Quentin McGaw
c7a841f4b4 chore(openvpn/extract): simplify PEM extract 2023-04-01 16:57:18 +00:00
Quentin McGaw
4ba159e483 chore(all): review error wrappings
- remove repetitive `cannot` and `failed` prefixes
- rename `unmarshaling` to `decoding`
2023-04-01 16:57:18 +00:00
dependabot[bot]
63a696d7e7 Chore(deps): Bump docker/build-push-action from 3.2.0 to 4.0.0 (#1378) 2023-04-01 08:28:34 -07:00
Quentin McGaw
d457342b46 feat(log): warn Openvpn 2.4 is to be removed 2023-04-01 15:24:42 +00:00
Quentin McGaw
c246dae2cc feat(log): log warnings about user settings
- Warn when using Openvpn 2.4 and SlickVPN
- Warn when using Openvpn 2.5 and SlickVPN
2023-04-01 15:22:32 +00:00
Quentin McGaw
0f4a2e5224 hotfix(deps): bump old openssl to 1.1.1t-r2 2023-03-31 09:34:33 +00:00
dependabot[bot]
db262050d5 Chore(deps): Bump github.com/stretchr/testify from 1.8.1 to 1.8.2 (#1483) 2023-03-30 10:54:44 -07:00
Quentin McGaw
227cdea0c8 fix(slickvpn): allow AES-256-GCM 2023-03-26 12:38:55 +02:00
Quentin McGaw
33a6f1c01b fix(slickvpn): lower TLS security level to 0 2023-03-26 12:38:55 +02:00
Quentin McGaw
f6f3c110f0 fix(slickvpn): all servers support TCP and UDP 2023-03-26 12:38:55 +02:00
Quentin McGaw
27a3f2c846 fix(slickvpn): precise default TCP port as 443 2023-03-26 12:38:55 +02:00
dependabot[bot]
62169baeea Chore(deps): Bump golang.org/x/text from 0.5.0 to 0.8.0 (#1436) 2023-03-26 12:36:29 +02:00
dependabot[bot]
4b18636a91 Chore(deps): Bump golang.org/x/sys from 0.3.0 to 0.6.0 (#1438) 2023-03-25 22:11:01 +01:00
Quentin McGaw
51432ca05f hotfix(health): remove previous err debug line 2023-03-25 17:34:56 +00:00
colereynolds
b5ebdcd040 docs(readme): add servers updater environment variables (#1393) 2023-03-25 16:14:07 +01:00
Quentin McGaw
416c1ee113 chore(deps): tidy dependencies 2023-03-25 15:09:27 +00:00
Quentin McGaw
fe97e28461 fix(health): log link to Wiki on VPN restart 2023-03-25 15:09:13 +00:00
Quentin McGaw
cbd8711a21 feat(airvpn): update servers data 2023-03-25 14:50:49 +00:00
stevenl4
7578e52ed5 fix(ipvanish): updater zip file url (#1449) 2023-03-25 15:36:44 +01:00
Quentin McGaw
0df68f76d5 fix(airvpn): remove commas from city names 2023-03-25 12:55:21 +00:00
Quentin McGaw
9a528c42f8 chore(settings): precise base64 DER for some OpenVPN fields 2023-03-25 12:10:01 +00:00
Quentin McGaw
5607916af6 hotfix: bump old openssl to 1.1.1t-r1 2023-03-23 16:36:14 +00:00
Quentin McGaw
4ad7a2a444 feat(mullvad): update servers data 2023-03-23 15:23:53 +00:00
Quentin McGaw
ab5dbdca97 feat(pia): update servers data 2023-03-01 13:00:18 +00:00
Quentin McGaw
a97fcda283 fix(version): add name in version check error 2023-02-27 20:16:55 +00:00
Quentin McGaw
e955adc1e1 hotfix: install older openssh for openvpn 2.4 2023-02-27 05:21:42 +00:00
Quentin McGaw
ac5141b411 Chore(deps): Bump github.com/breml/rootcerts from 0.2.8 to 0.2.10 2023-02-26 18:14:59 +00:00
dependabot[bot]
f8c189e48a Chore(deps): Bump github.com/fatih/color from 1.13.0 to 1.14.1 (#1369) 2023-02-26 11:14:15 -08:00
Quentin McGaw
2f2a904c64 feat(mullvad): update servers data 2023-02-26 15:16:22 +00:00
Quentin McGaw
9261dca8ab chore(lint): bump from v1.49.0 to v1.51.2 2023-02-26 15:15:34 +00:00
Quentin McGaw
7b5d5c3884 feat(alpine): bump from 3.16 to 3.17 2023-02-26 15:15:34 +00:00
Quentin McGaw
7c80d80904 chore(build): upgrade Go from 1.19 to 1.20 2023-02-26 15:15:30 +00:00
Quentin McGaw
ea40b84ec0 fix(settings): print outbound subnets correctly 2022-12-31 17:46:55 +00:00
Quentin McGaw
4e6ef649c4 fix(airvpn): remove commas from API locations 2022-12-31 17:30:31 +00:00
Quentin McGaw
dd40f1d2e6 chore(devcontainer): same ssh bind mount for all platforms 2022-12-31 17:30:31 +00:00
dependabot[bot]
490693bb26 Chore(deps): Bump golang.org/x/text from 0.4.0 to 0.5.0 (#1275) 2022-12-15 04:58:43 -05:00
Quentin McGaw
c8d33ca5f3 fix(surfshark): update location data
- Add 2 new 'HK' servers
- Remove 3 servers no longer resolving
2022-12-15 09:29:48 +00:00
Quentin McGaw
e6df026332 feat(surfshark): update servers data 2022-12-15 09:29:09 +00:00
Quentin McGaw
7a30343053 docs(readme): document alternative ghcr image name 2022-12-15 09:02:37 +00:00
ksurl
fc02ae9c13 feat(docker): ghcr.io/qdm12/gluetun image (#1231) 2022-12-14 16:04:41 -05:00
Quentin McGaw
f70f0aca9c fix(settings): validate Wireguard addresses depending on IPv6 support 2022-12-14 11:52:03 +00:00
Quentin McGaw
16acd1b162 chore(netlink): log ipv6 support at debug level 2022-12-14 11:52:03 +00:00
Quentin McGaw
2e3eb1fd7b fix(wireguard): ignore IPv6 addresses if IPv6 is not supported 2022-12-14 11:52:03 +00:00
Quentin McGaw
a4cf17f81e fix(netlink): change logger level 2022-12-14 11:50:36 +00:00
Quentin McGaw
c0a301611d fix(health): set config to default in healthcheck mode 2022-12-07 10:34:24 +00:00
Quentin McGaw
cc934f5c68 hotfix(netlink): ipv6 detection for nil src/dst in routes 2022-12-02 11:39:37 +00:00
Quentin McGaw
74426f6202 feat(netlink): add debug logger 2022-12-02 11:26:52 +00:00
Quentin McGaw
03ed3cb1c8 feat(wireguard): WIREGUARD_IMPLEMENTATION variable
- Can be `auto` (default), `userspace` or `kernelspace`
2022-12-02 11:16:27 +00:00
Quentin McGaw
1b1335835b fix(netlink): inspect each route for IPv6 support 2022-12-01 12:18:46 +00:00
Quentin McGaw
5070dbcf7f feat(fastestvpn): update servers data 2022-11-30 19:21:56 +00:00
rsquarev
90b9d85742 fix(fastesvpn): updater zip file url (#1264) 2022-11-30 14:19:14 -05:00
Quentin McGaw
7a3b9941aa fix(exit): exit with 1 on runtime error 2022-11-18 09:46:31 +00:00
Quentin McGaw
698095f0a0 fix(pprof): do not run if disabled 2022-11-15 12:45:47 +00:00
Quentin McGaw
5a06d8e155 fix(firewall): iptables detection with permission denied 2022-11-15 12:34:25 +00:00
Quentin McGaw
7421dcb45f feat(openvpn): explain ip route error in logs
- `RTNETLINK answers: File exists` changed to warning with explanation
- `Linux route add command failed:` changed to warning with explanation
2022-11-11 09:48:55 +00:00
Quentin McGaw
554a6cdb92 feat(healthcheck): add FAQ url on unhealthy log 2022-11-11 09:43:07 +00:00
Quentin McGaw
5aa39be973 fix(firewall): remove previously allowed input ports 2022-11-11 09:19:03 +00:00
Quentin McGaw
192a7a56a3 fix(httpproxy): lower shutdown wait from 2s to 100ms 2022-10-31 11:21:25 +00:00
Quentin McGaw
1d1657e9be fix(pia): remove username+password from login bad http status code 2022-10-31 11:00:43 +00:00
Quentin McGaw
49b7301295 feat(mullvad): update servers data 2022-10-31 10:44:36 +00:00
dependabot[bot]
126804c15e Chore(deps): Bump golang.org/x/text from 0.3.7 to 0.4.0 (#1198) 2022-10-28 07:24:35 -04:00
dependabot[bot]
a7643c6201 Chore(deps): Bump github.com/stretchr/testify from 1.8.0 to 1.8.1 (#1210) 2022-10-28 07:24:20 -04:00
dependabot[bot]
db2de5fc84 Chore(deps): Bump docker/build-push-action from 3.1.1 to 3.2.0 (#1193) 2022-10-28 07:24:04 -04:00
dependabot[bot]
5c7b9aa6a1 Chore(deps): Bump github.com/breml/rootcerts from 0.2.6 to 0.2.8 (#1173) 2022-10-28 07:23:40 -04:00
Quentin McGaw
63890c159e feat(servers): update FastestVPN servers 2022-10-28 11:02:39 +00:00
Quentin McGaw
e7d5ae5dc1 fix(fastestvpn): handle lowercase .ovpn filenames 2022-10-28 11:02:24 +00:00
Quentin McGaw
b275354a92 fix(env): trim space for wireguard addresses 2022-10-28 10:49:51 +00:00
Quentin McGaw
ac02a64d17 fix(ipinfo): handle 403 as too many requests 2022-10-28 10:35:29 +00:00
Quentin McGaw
9c80150e09 fix(publicip): no retry when too many requests to ipinfo.io 2022-10-28 10:35:09 +00:00
Quentin McGaw
31a8bc9062 feat(servers): update PureVPN 2022-10-28 09:11:33 +00:00
Quentin McGaw
f15dde6502 feat(providers): add AirVPN support (#1145) 2022-10-17 02:54:56 -04:00
Quentin McGaw
f70609c464 fix(wireguard): ignore IPv6 addresses if IPv6 disabled 2022-10-17 06:31:32 +00:00
Quentin McGaw
c954e6f231 fix: parse udp4, udp6, tcp4 or tcp6 2022-10-16 16:54:12 +00:00
Quentin McGaw
cb804577a9 feat(httpproxy): log credentials sent on mismatch 2022-10-02 09:31:03 +00:00
Quentin McGaw
e5be20d719 fix(exit): exit with 0 on successful shutdown 2022-09-14 13:23:31 +00:00
EkilDeew
875690ab18 feat(network): enable ipv6 connection and tunneling (#1114)
Co-authored-by: Quentin McGaw <quentin.mcgaw@gmail.com>
2022-09-13 17:18:10 -07:00
Quentin McGaw
6a5aa8eddb fix(openvpn): do not set tun-ipv6
- Server should push `tun-ipv6` if it is available
- Add ignore filter for `tun-ipv6` if ipv6 is not supported on client
- Fixes #435
2022-09-14 00:03:31 +00:00
Quentin McGaw
7fdc7de210 feat(ipv6): use ipv6 endpoint IPs if supported 2022-09-12 21:31:37 +00:00
Quentin McGaw
dd7630997b fix(vpnsecure): allow empty OpenVPN user+password 2022-09-10 14:46:17 +00:00
Quentin McGaw
aba5ca4536 fix(cyberghost): remove outdated server groups
- Remove `94-1` pemium udp usa
- Remove `95-1` premium udp asia
- Remove `93-1` pemium udp usa
- Remove `96-1` premium tcp asia
- Update servers data
2022-09-08 12:19:12 +00:00
Quentin McGaw
7506625f40 chore(tests): fix netlink test for previous crash 2022-09-06 12:17:51 +00:00
Quentin McGaw
5ddd703f6a feat(vpn): auto detection of IPv6 support
- `OPENVPN_IPV6` removed
- Affects OpenVPN
- Use the same mechanism for OpenVPN and Wireguard
- Check only once at program start since this is unlikely to change at runtime
- Log if IPv6 is supported
- Remove `IPv6` boolean from settings structs
- Move IPv6 detection as a method on NetLinker
2022-09-06 12:16:29 +00:00
Quentin McGaw
71c51a7455 chore(provider/utils): do not check for empty wg keys 2022-09-05 15:50:02 +00:00
Quentin McGaw
284d4340b1 fix(tests): fix netlink wireguard test 2022-09-05 15:23:31 +00:00
Quentin McGaw
2c1281d0a2 hotfix(tests): panic tests for previous commit 2022-09-05 15:04:43 +00:00
Stijn Hoop
532df9f8d4 fix(privateinternetaccess): get token for port forwarding (#1132) 2022-09-05 08:01:48 -07:00
Quentin McGaw
45b7da1058 chore(dev): improve missing provider panic string 2022-09-05 14:51:30 +00:00
Quentin McGaw
907daff483 chore(build): tidy Go modules dependencies 2022-09-04 23:01:02 +00:00
Quentin McGaw
7757e8a114 chore(dev): improve update command launch config
- Run without `debug` mode
- Run from workspace folder so it writes to the right path
- Pick `-maintainer` or `-enduser` update mode
2022-09-04 18:40:08 +00:00
Quentin McGaw
e59e28152f fix(ivpn): update mechanism for Wireguard servers 2022-09-02 00:36:13 +00:00
Quentin McGaw
2fe0594db7 feat(servers): update ProtonVPN servers data 2022-08-30 11:44:34 +00:00
Quentin McGaw
794e96b449 docs(readme): add ProtonVPN and PureVPN to Wireguard support 2022-08-29 00:31:38 +00:00
Quentin McGaw
07282f414c chore(wireguard): upgrade wireguard depdencies 2022-08-27 18:37:24 +00:00
Quentin McGaw
e583f9de47 fix(codeql): fix integer parsing (false positive) 2022-08-27 16:45:29 +00:00
Quentin McGaw
8570e09eb9 chore(config): rename Reader to Source struct 2022-08-26 15:40:35 +00:00
Quentin McGaw
ae5cba519c chore(config): define Source interface locally where needed 2022-08-26 15:03:59 +00:00
Quentin McGaw
26f3832187 chore(config): rename mux source to merge 2022-08-26 14:59:35 +00:00
Quentin McGaw
5989f29035 feat(surfshark): Wireguard support (#587) 2022-08-26 07:55:46 -07:00
Quentin McGaw
4ace99f318 chore(servers): remove "udp": true for Wireguard 2022-08-25 13:24:22 +00:00
Quentin McGaw
d1c5e00df8 fix(updater): error when server has not the minimal information 2022-08-25 13:23:27 +00:00
Quentin McGaw
5eacb46226 feat(servers): update servers data for Ivpn, Mullvad and Windscribe 2022-08-25 13:05:20 +00:00
Quentin McGaw
6c17612310 chore(filter): no network protocol filter for Wireguard 2022-08-25 13:03:58 +00:00
Quentin McGaw
fba73a0a0f fix(settings): OPENVPN_CUSTOM_CONFIG precedence for custom provider only if VPN_SERVICE_PROVIDER is empty 2022-08-25 04:01:17 +00:00
Quentin McGaw
4faef87c03 chore(build): bump Go from 1.18 to 1.19 2022-08-24 21:54:49 +00:00
Quentin McGaw
5914cb0e37 chore(build): bump Go from 1.17 to 1.18
- Unneeded disabled linters are: `rowserrcheck`, `sqlclosecheck`
- Disabled linter is `wastedassign` which is tolerable
2022-08-24 21:54:08 +00:00
Quentin McGaw
aa53436e56 chore(lint): upgrade golangci-lint to v1.49.0
- Add linter `interfacebloat` and fix code issues
- Add linter `reassign`
- Remove deprecated linter `nosnakecase`
2022-08-24 21:48:24 +00:00
Quentin McGaw
8dfaebc737 chore(all): remove deprecated io/ioutil import 2022-08-24 21:43:37 +00:00
Quentin McGaw
062b6a276c fix(settings): read PEM files but b64 env vars
- Extract base64 data from PEM files and secret files
- Environment variables are not PEM encoded and only the base64 data
- Affects OpenVPN certificate, key and encrypted key
2022-08-24 17:48:45 +00:00
Quentin McGaw
647cd07de7 feat(surfshark): update servers data 2022-08-24 13:04:34 +00:00
Quentin McGaw
a530c84c5f fix(surshark): remove invalid retro-servers 2022-08-24 13:04:18 +00:00
Quentin McGaw
0bb320065e feat(server): patch VPN settings
- `PUT` at `/v1/vpn/settings`
- Undocumented, experimental for now
2022-08-21 23:36:48 +00:00
Quentin McGaw
d685d78e74 feat(server): add vpn route to replace /openvpn 2022-08-21 23:29:25 +00:00
Quentin McGaw
48896176e5 chore(server): do not redact openvpn credentials from response 2022-08-21 22:04:04 +00:00
Quentin McGaw
54dcf28b31 chore(server): replace 404 with 401 for unsupported routes and methods 2022-08-21 22:02:06 +00:00
Quentin McGaw
f8bf32bb34 docs(readme): add slickvpn to list of providers 2022-08-16 00:02:34 +00:00
Quentin McGaw
748923021c fix(ci): permissions for labels workflow 2022-08-15 23:58:33 +00:00
Quentin McGaw
a182e3503b feat: add VPNsecure.me support (#848)
- `OPENVPN_ENCRYPTED_KEY` environment variable 
- `OPENVPN_ENCRYPTED_KEY_SECRETFILE` environment variable 
- `OPENVPN_KEY_PASSPHRASE` environment variable 
- `OPENVPN_KEY_PASSPHRASE_SECRETFILE` environment variable 
- `PREMIUM_ONLY` environment variable
- OpenVPN user and password not required for vpnsecure provider
2022-08-15 16:54:58 -07:00
Quentin McGaw
991cfb8659 chore(ci): limit labels workflow to not forked 2022-08-15 23:53:29 +00:00
Richard Hodgson
d0dfc21e2b feat: SlickVPN Support (#961)
- `internal/updater/html` package
- Add unit tests for slickvpn updating code
- Change shared html package to be more share-able
- Split html utilities in multiple files
- Fix processing .ovpn files with prefix space

Authored by @Rohaq 
Co-authored-by: Quentin McGaw <quentin.mcgaw@gmail.com>
2022-08-15 08:25:06 -07:00
dependabot[bot]
617bd0c600 Chore(deps): Bump github.com/stretchr/testify from 1.7.5 to 1.8.0 (#1052) 2022-08-13 12:16:57 -07:00
dependabot[bot]
349b5429ba Chore(deps): Bump docker/build-push-action from 3.1.0 to 3.1.1 (#1098) 2022-08-13 12:16:44 -07:00
Quentin McGaw
8db2944749 chore(settings): OpenVPN ClientCrt -> Cert 2022-08-13 18:59:07 +00:00
Quentin McGaw
5986432a22 chore(settings): OpenVPN ClientKey -> Key 2022-08-13 18:58:09 +00:00
Quentin McGaw
652daec509 Change: OPENVPN_CLIENTKEY -> OPENVPN_KEY
- No breaking change since this was undocumented
2022-08-13 18:56:37 +00:00
Quentin McGaw
f94d4b761a Change: OPENVPN_CLIENTCRT -> OPENVPN_CERT
- No breaking change since this was undocumented
2022-08-13 18:55:29 +00:00
Quentin McGaw
1ab74e6bb3 chore: OpenVPN user and password as nullable
- Username and password can be the empty string for custom provider
2022-08-13 18:01:26 +00:00
dependabot[bot]
8e101d49a1 Chore(deps): Bump github.com/breml/rootcerts from 0.2.4 to 0.2.6 (#1058) 2022-08-12 17:01:45 -07:00
Quentin McGaw
7c08e8f607 chore(lint): add asasalint and usestdlibvars 2022-08-12 23:54:20 +00:00
Quentin McGaw
a4caa61c47 chore(lint): add nosnakecase linter 2022-08-12 23:53:47 +00:00
Quentin McGaw
ebae167815 chore(lint): golangci-lint v1.47.2 -> v1.48.0
- Remove deprecated `ifshort` linter
- Fix bad `//nolint:gomnd` comment
2022-08-12 23:52:30 +00:00
Quentin McGaw
a6f00f2fb2 chore(lint): upgrade golangci-lint to v1.47.2
- Fix Slowloris attacks on HTTP servers
- Force set default of 5 minutes for pprof read timeout
- Change `ShutdownTimeout` to time.Duration since it cannot be set to 0
2022-08-01 21:09:16 +00:00
dependabot[bot]
877617cc53 Chore(deps): Bump docker/build-push-action from 3.0.0 to 3.1.0 (#1073) 2022-07-23 20:08:34 -07:00
Quentin McGaw
2800588ef7 feat(expressvpn): update servers data 2022-07-18 18:01:08 +00:00
Quentin McGaw
f5efa42aaf chore(lint): remove some linters
- remove duplicate `predeclared`
- remove commented `varnamelen` and `wrapcheck`
2022-07-17 23:11:42 +00:00
Hey
10bd0e1505 fix(readme): typo sercice to service (#1067) 2022-07-15 19:52:04 -04:00
Quentin McGaw
a4c80b3045 chore(ci): add mocks check
- Check for missing `//go:generate` comments
- Check for outdated mocks
2022-07-04 00:39:01 +00:00
Quentin McGaw
dbb71bd695 chore(mocks): use common mocks for ivpn and ipvanish 2022-07-04 00:34:48 +00:00
dependabot[bot]
a544f6e604 Chore(deps): Bump github.com/breml/rootcerts from 0.2.3 to 0.2.4 (#1033) 2022-07-03 16:50:38 -07:00
dependabot[bot]
a18e026b70 Chore(deps): Bump github.com/stretchr/testify from 1.7.2 to 1.7.5 (#1042) 2022-07-03 16:50:27 -07:00
Quentin McGaw
0413a0a1ab chore(ci): rework docker hub description workflow
- Run only on base repository
- Rename job from `dockerHubDescription` to `docker-hub-description`
- Limit permissions of job to read only
- Remove unneeded names for steps
2022-07-03 14:31:49 +00:00
Quentin McGaw
cb6e9cb761 docs(readme): add links to add a provider 2022-07-03 13:39:47 +00:00
Quentin McGaw
420ae40901 feat(dev): Add provider example package 2022-07-02 21:04:57 +00:00
Quentin McGaw
34e67f9f99 chore(markdown): alphabetically sorted headers 2022-07-02 20:58:43 +00:00
Quentin McGaw
18c53aa597 docs(readme): simplify heading description 2022-07-02 20:58:43 +00:00
Quentin McGaw
6d2f9b9508 chore(updater): check servers have minimal information 2022-07-02 20:58:43 +00:00
Quentin McGaw
6826b05d58 chore(all): remove all package comments 2022-07-02 20:58:43 +00:00
barino28
9f959dbc6a fix(expressvpn): OpenVPN fragment option and add ciphers (#1047)
* Fragment was defined in `OpenVPNProviderSettings` but was not written to the OpenVPN configuration file.
* Added two additional ciphers to the configuration for ExpressVPN

Authored-by: barino86 <barino@mac.com>
2022-06-29 05:23:16 -07:00
Quentin McGaw
87dbae5745 hotfix(fastestvpn): re-fix Openvpn configuration
- add `auth sha256` option
- remove `remote-cert-tls server` option
2022-06-26 21:29:05 +00:00
Quentin McGaw
037f19e852 hotfix(publicip): revert back JSON to public_ip 2022-06-26 18:08:11 +00:00
Quentin McGaw
62ad8bcd8f fix(pia): set port forward file owned with PUID and PGID 2022-06-25 15:44:29 +00:00
Quentin McGaw
2805c3388a hotfix(fastestvpn): add remote-cert-tls server 2022-06-25 15:16:38 +00:00
Quentin McGaw
535297dcf5 chore: extract.PEM replaces PEM parse functions 2022-06-24 23:10:00 +00:00
Quentin McGaw
b3b6933ef4 chore(lint): review exclude rules 2022-06-20 13:36:24 +00:00
Quentin McGaw
edbbcc041a fix(protonvpn): set free field for free servers 2022-06-18 18:30:27 +00:00
Quentin McGaw
d430ebc34f feat(protonvpn): update servers data 2022-06-18 18:30:05 +00:00
Quentin McGaw
0e9abc6e1d chore(tests): modify JSON tests to not need all providers listed 2022-06-18 15:08:59 +00:00
Quentin McGaw
0c0dd10766 chore(dev): add VSCode launch.json
- Credits to @Rohaq
2022-06-18 00:17:09 +00:00
Quentin McGaw
75454be6b6 fix(pprof): override operation in global settings 2022-06-18 00:16:14 +00:00
Quentin McGaw
4952e3b74e docs(bug): fix render of logs to be plain text 2022-06-18 00:15:29 +00:00
Quentin McGaw
04b34a266c chore(deps): update go4.org/unsafe/assume-no-moving-gc
- Allow development on Go 1.18 without `ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.18`
2022-06-17 23:59:39 +00:00
Quentin McGaw
89b6a031b0 chore(vpn): add check for empty public key for Wireguard 2022-06-12 15:59:48 +00:00
Quentin McGaw
d4c6a9bdb5 feat(storage): log time difference as a friendly duration 2022-06-12 15:35:04 +00:00
Quentin McGaw
cdc29d48b7 chore(ci): build PR images as pr-N (#1026)
- Remove event validity check (unneeded and was buggy)
- Remove `branch` metadata trigger since it conflicts with the PR trigger
2022-06-12 08:33:16 -07:00
dependabot[bot]
f4b464a7cf Chore(deps): Bump github.com/stretchr/testify from 1.7.1 to 1.7.2 (#1016) 2022-06-12 08:31:53 -07:00
dependabot[bot]
76690d3add Chore(deps): Bump crazy-max/ghaction-github-labeler from 3 to 4 (#1007) 2022-06-12 07:07:25 -07:00
Quentin McGaw
9898387579 feat(updater): Configurable min ratio
- `UPDATER_MIN_RATIO` variable
- `-minratio` flag for CLI operation
2022-06-12 14:03:00 +00:00
Quentin McGaw
1ea15a1a13 chore(protonvpn): document to remove SERVER_NAMES 2022-06-12 01:58:46 +00:00
Quentin McGaw
bda6707685 chore(all): remove unexported interfaces 2022-06-12 01:15:14 +00:00
Quentin McGaw
89277828ac chore(publicip): internal/publicip/ipinfo package 2022-06-12 01:11:22 +00:00
Quentin McGaw
83b4a3fe55 chore(publicip): refactoring
- Exported `Fetcher` interface
- Inject `Fetcher` to publicip loop and updaters
- Get public IP and information at the same time
- Only query ipinfo.io
- Make `MultiInfo` part of the `Fetch` object
2022-06-12 00:46:08 +00:00
Quentin McGaw
45c9e780c0 chore(updater): rename presolver to parallelResolver 2022-06-11 20:12:35 +00:00
Quentin McGaw
33b8f5f596 chore(settings): updater DNS address as string 2022-06-11 20:11:20 +00:00
Quentin McGaw
447a7c9891 updater: refactoring and set DNS server correctly
- Fix CLI operation not setting DNS server
- Fix periodic operation not setting DNS server
- Set DNS address for resolution once at start for both CLI and periodic operation
- Inject resolver to each provider instead of creating it within
- Use resolver settings on every call to `.Resolve` method, instead of passing it to constructor
- Move out minServers check from resolver
2022-06-11 19:47:46 +00:00
Quentin McGaw
1bd355ab96 chore(lint): add ireturn linter 2022-06-11 01:34:45 +00:00
Quentin McGaw
578ef768ab chore(all): return concrete types, accept interfaces
- Remove exported interfaces unused locally
- Define interfaces to accept arguments
- Return concrete types, not interfaces
2022-06-11 01:34:30 +00:00
Quentin McGaw
0378fe4a7b chore(all): Providers containing all provider objects
- Share the same providers for updater and vpn
- Initialise all providers at start
- Get from `Providers` instead of constructing on every run
2022-06-10 00:47:56 +00:00
Quentin McGaw
ebd94723c1 chore(updater): incorporate FetchServers method in Provider interface
- Each provider interface can now fetch updated servers data
- Rename each provider updater subpackage name to `updater`
- Updater constructor does not take a settings struct
- Updater update method takes in a slice of provider strings
2022-06-09 23:47:41 +00:00
Quentin McGaw
11b55abff3 fix(protonvpn): remove duplicate entry IPs 2022-06-09 22:11:40 +00:00
Quentin McGaw
7f32b43895 fix(pia): load custom PIA certificate for API 2022-06-09 17:11:24 +00:00
Quentin McGaw
899f10c35e chore(resolver): export structs instead of interfaces 2022-06-09 17:11:24 +00:00
Quentin McGaw
415cb7a945 chore(updater): create resolver in provider updater
- Pass min servers to resolve call
- Set settings when constructing resolver
- Construct resolver in each provider updater
- No more common resolver for all providers
2022-06-09 17:11:24 +00:00
Quentin McGaw
e37f557cd5 chore(provider): add Name() method per provider 2022-06-09 17:11:24 +00:00
Quentin McGaw
79f213d97a chore(updater): rename GetServers to FetchServers 2022-06-09 17:11:24 +00:00
Quentin McGaw
11e1c9f9bb feat(protonvpn): update hardcoded servers data 2022-06-09 16:41:04 +00:00
Quentin McGaw
3ff3816d77 fix(pia): restrict custom port choice 2022-06-09 16:36:59 +00:00
Quentin McGaw
c0bdae8baf fix(protonvpn): restrict custom port choice 2022-06-09 16:36:17 +00:00
Quentin McGaw
46e6bd16c9 fix(pia): remove duplicate log of pf data expiration 2022-06-09 16:34:28 +00:00
Quentin McGaw
5359257c65 hotfix(pia): port forwarding to use server name 2022-06-06 18:09:21 +00:00
Quentin McGaw
5e659dc5b3 feat(storage): add keep field for servers 2022-06-06 03:04:58 +00:00
Quentin McGaw
85e9d7d522 docs(maintenance): update document 2022-06-06 02:58:58 +00:00
Quentin McGaw
b71c8e58f4 fix(vpn): do not close wait error channel on consumer side 2022-06-06 02:56:40 +00:00
Quentin McGaw
e998372ce2 feat(ipvanish): update servers data and remove duplicates 2022-06-06 02:24:58 +00:00
Quentin McGaw
1216326867 chore(storage): common sorting for all servers 2022-06-06 02:24:54 +00:00
Quentin McGaw
f53f0cfffd feat(surfshark): update servers data 2022-06-06 01:41:00 +00:00
Quentin McGaw
f5f65d534a fix(ci): publish job trigger fixed 2022-06-05 16:01:40 +00:00
Quentin McGaw
684cef6eab hotfix(openvpn): openvpn udp specific lines added 2022-06-05 15:48:14 +00:00
Quentin McGaw
b4f6ae030d hotfix(purevpn): add missing key-direction 1 2022-06-05 15:44:33 +00:00
Quentin McGaw
e95c94294f feat(pia): update servers data 2022-06-05 15:20:03 +00:00
Quentin McGaw
36b504609b chore(all): memory and thread safe storage
- settings: get filter choices from storage for settings validation
- updater: update servers to the storage
- storage: minimal deep copying and data duplication
- storage: add merged servers mutex for thread safety
- connection: filter servers in storage
- formatter: format servers to Markdown in storage
- PIA: get server by name from storage directly
- Updater: get servers count from storage directly
- Updater: equality check done in storage, fix #882
2022-06-05 15:19:16 +00:00
Quentin McGaw
1e6b4ed5eb chore(provider): rename test functions to Test_Provider_GetConnection 2022-06-05 14:59:47 +00:00
Quentin McGaw
0549326dfb chore(updater): tiny code changes
- Remove unneeded ctx error check in cyberghost updating code
- Move global scope caser to function local scope
- Return error if updating a single provider in `UpdateServers`
- Add comments on different error paths in `UpdateServers`
2022-06-04 13:50:29 +00:00
Quentin McGaw
87c6ebe1c5 feat(purevpn): update servers data 2022-05-31 14:17:33 +00:00
Quentin McGaw
f0afac243b feat(privatevpn): update servers data 2022-05-31 14:16:41 +00:00
dependabot[bot]
53472077f4 Chore(deps): Bump docker/setup-buildx-action from 1 to 2 (#977) 2022-05-29 11:31:09 -07:00
dependabot[bot]
55afdf33e1 Chore(deps): Bump docker/setup-qemu-action from 1 to 2 (#978) 2022-05-29 11:28:05 -07:00
dependabot[bot]
d3c1f9263c Chore(deps): Bump docker/build-push-action from 2.10.0 to 3.0.0 (#979) 2022-05-29 11:27:55 -07:00
dependabot[bot]
6341d1dda6 Chore(deps): Bump docker/metadata-action from 3 to 4 (#980) 2022-05-29 11:27:44 -07:00
dependabot[bot]
e62e1883c2 Chore(deps): Bump docker/login-action from 1 to 2 (#981) 2022-05-29 11:27:33 -07:00
Quentin McGaw
501b98dbd3 chore(ci): skip workflow for required verify job 2022-05-29 17:33:35 +00:00
Derzsi Dániel
029fd1da1f feat(docker): upgrade Alpine from 3.15 to 3.16 (#1005) 2022-05-29 10:30:10 -07:00
Quentin McGaw
fd0267efef chore(ci): merge codeql job in CI workflow 2022-05-29 17:23:55 +00:00
Quentin McGaw
4414366370 chore(ci): restrict permissions to read actions+contents 2022-05-29 17:23:55 +00:00
Quentin McGaw
08553bc90b chore(ci): only publish image for qdm12/gluetun 2022-05-29 17:23:54 +00:00
Quentin McGaw
6f850c4ad4 chore(ci): merge dependabot and fork workflows in ci workflow 2022-05-29 17:23:48 +00:00
Quentin McGaw
8e1316bd8a chore(storage): minor refactoring
- Unexport `SyncServers`
- Re-generate mock file
- Remove single use function
2022-05-28 22:51:19 +00:00
Quentin McGaw
b345368257 hotfix(storage): JSON provider versioning safety 2022-05-28 22:44:14 +00:00
Quentin McGaw
90dd3b1b5c chore(storage): only pass hardcoded versions to read file 2022-05-28 22:36:16 +00:00
Quentin McGaw
22455ac76f chore(updater): shared not enough servers error 2022-05-28 22:02:18 +00:00
Quentin McGaw
eb18eaf0a9 fix(wireguard): continue on ipv6 route add permission denial 2022-05-28 21:06:21 +00:00
Quentin McGaw
90c6c8485b chore(updater): common GetServers signature
- Log warnings when running outside of CLI mode
- Remove updater CLI bool setting
- Warnings are logged in updating functions
2022-05-28 20:58:50 +00:00
Quentin McGaw
381089ebdf chore(storage): rename InfoErrorer to Infoer (bad name) 2022-05-28 16:05:19 +00:00
Quentin McGaw
292813831d chore(updater): internal/updater/loop subpackage
- Do not export updater interface
- Export updater struct
- Define local interfaces where needed
- More restrictive updater loop interface in http control server
- Inject `Updater` into updater loop as an interface
2022-05-28 16:03:59 +00:00
Quentin McGaw
991d75a1d0 chore(provider): rename all BuildConf to OpenVPNConfig 2022-05-27 22:04:14 +00:00
Quentin McGaw
d9dfb81cb4 feat(perfect privacy): update servers data 2022-05-27 21:56:52 +00:00
Quentin McGaw
67a9cacb61 hotfix(custom): allow empty servers data 2022-05-27 21:47:41 +00:00
Quentin McGaw
a91eb95456 chore(internal/provider): rename all structs to Provider 2022-05-27 18:05:04 +00:00
Quentin McGaw
a295269518 hotfix(formatter): cyberghost not forced as format 2022-05-27 17:50:14 +00:00
Quentin McGaw
42904b6749 chore(all): move sub-packages to internal/provider 2022-05-27 17:48:51 +00:00
Quentin McGaw
364f9de756 feat(env): clean env variable values
- Remove surrounding spaces
- Remove suffix new line characters
2022-05-27 17:27:54 +00:00
Quentin McGaw
7fd45cf17f feat(wireguard): add debug logs for IPv6 detection
- To debug issue #998
- Enable with `LOG_LEVEL=debug`
2022-05-27 17:27:53 +00:00
Quentin McGaw
eb71cfb144 chore(deps): upgrade gopkg.in/yaml.v3 to v3.0.1
- fix 'vulnerability' alert on github
- no impact really since it's just used in unit tests
- checked with `go mod why gopkg.in/yaml.v3`
2022-05-27 17:27:53 +00:00
Quentin McGaw
48e469917e chore(ci): remove tidy check
- Not really needed with newer `go install`
- Conflicts with Go 1.17 go.mod format
- Conflicts with manual indirect dependency upgrade
2022-05-27 17:27:53 +00:00
Quentin McGaw
4bcd8ee9f5 chore(constants): add internal/constants/openvpn package 2022-05-27 16:29:49 +00:00
Quentin McGaw
1b2bcf901a chore(surfshark): add package internal/provider/surshark/server
- Merge `internal/models/location.go` and `internal/constants/surfshark.go` into `internal/provider/surfshark/servers/locationdata.go`
2022-05-27 16:29:48 +00:00
Quentin McGaw
306de8feda chore(constants): add internal/provider/privateinternetacess/presets package 2022-05-27 16:29:48 +00:00
Quentin McGaw
e3696f1eea chore(constants): inline Openvpn values in each provider 2022-05-27 16:29:47 +00:00
Quentin McGaw
7ff14a356c chore(internal/providers): simplify OpenVPN config building 2022-05-27 16:29:47 +00:00
Quentin McGaw
4bde50fb3a chore(all): use casers instead of strings.Title
- Add `golang.org/x/text` dependency
- Update code to use `cases.Title(language.English)`
2022-05-27 16:29:41 +00:00
Quentin McGaw
bd0868d764 chore(all): provider to servers map in allServers
- Simplify formatting CLI
- Simplify updater code
- Simplify filter choices for config validation
- Simplify all servers deep copying
- Custom JSON marshaling methods for `AllServers`
- Simplify provider constructor switch
- Simplify storage merging
- Simplify storage reading and extraction
- Simplify updating code
2022-05-27 16:17:53 +00:00
Quentin McGaw
5ffe8555ba chore(lint): upgrade golangci-lint from v1.44.2 to v1.46.2
- Add linter `execinquery`
- Add linter `nosprintfhostport`
2022-05-27 00:52:25 +00:00
Quentin McGaw
78ccbb21cd change(servers.json): change provider names
- From `pia` to `private internet access`
- From `perfectprivacy` to `perfect privacy`
- From `vpnunlimited` to `vpn unlimited`
- This is done to match string constants in the code for another refactor
- Reset each of these providers servers version to `1`.
2022-05-27 00:47:58 +00:00
Quentin McGaw
92dbe1ebad chore(cli): refactor FormatServers to use provider strings 2022-05-08 19:05:36 +00:00
Quentin McGaw
2eec60cdd2 chore(custom): validate Openvpn file earlier 2022-05-07 19:33:21 +00:00
Quentin McGaw
da8c104ebd chore(internal/provider/utils): unexport functions 2022-05-07 19:33:12 +00:00
Quentin McGaw
0ef7b66047 chore(internal/provider): GetConnection test 2022-05-07 19:33:05 +00:00
Quentin McGaw
e32d251cc1 hotfix(windscribe): OpenVPN certificate validation 2022-05-07 07:05:24 +00:00
Quentin McGaw
9dd5e7bf1d fix: PUID and PGID as 32 bit unsigned integers 2022-05-01 16:29:56 +00:00
Quentin McGaw
b6de6035f6 hotfix(nordvpn): use aes-256-cbc before GCM 2022-04-28 13:47:24 +00:00
Quentin McGaw
88ccaf0b83 feat(torguard): update servers data 2022-04-26 11:01:42 +00:00
Quentin McGaw
52c8bc075f feat(nordvpn): update servers data 2022-04-26 11:01:05 +00:00
Quentin McGaw
2537cd5271 fix(port-forwarding): loop exit from vpn loop 2022-04-25 08:31:32 +00:00
Quentin McGaw
db91625de4 fix(pia): port forwarding certificate
- Do not use custom PIA certificate
- Only use OS certificates
- Update unit test
2022-04-25 08:31:27 +00:00
Quentin McGaw
df78386fbe chore(ci): add codeql analysis 2022-04-23 12:30:15 -04:00
Quentin McGaw
a1d70f740a fix(nordvpn): allow aes-256-gcm for Openvpn 2.4 2022-04-23 12:53:24 +00:00
Quentin McGaw
187f42277a fix(pia): hide escaped url query values 2022-04-23 11:21:08 +00:00
Quentin McGaw
e1f89bb569 fix(health): HEALTH_VPN_DURATION_ADDITION 2022-04-23 11:09:24 +00:00
Quentin McGaw
1d94f8ab2b chore(storage): remove unneeded VPN default 2022-04-23 11:09:04 +00:00
Quentin McGaw
045ecabb78 chore(updater): set vpn field for all providers
- Bump servers model versions for all providers except mullvad, ivpn, windscribe
- Do not leave `vpn` JSON field empty for any server
2022-04-23 11:08:59 +00:00
Quentin McGaw
e6c3cb078a chore(storage): tcp and udp fields for all servers
- Updater code sets UDP and TCP compatibility for all providers
- Increase servers.json model versions for affected providers (mullvad, windscribe, privado, protonvpn, privatevpn)
- Remove retro-compatibility server defaults
- Update all affected providers servers data (mullvad, windscribe, privado, protonvpn, privatevpn)
2022-04-23 10:23:41 +00:00
Quentin McGaw
afa51b3ff6 hotfix(storage): servers json versions updated 2022-04-22 21:12:27 +00:00
Quentin McGaw
f9c80b2285 hotfix(privatevpn): add missing IP addresses 2022-04-22 21:03:38 +00:00
Quentin McGaw
fc5cf44b2c fix(firewall): iptables detection improved
1. Try setting a dummy output rule
2. Remove the dummy output rule
3. Get the INPUT table policy
4. Set the INPUT table policy to its existing policy
2022-04-22 17:23:57 +00:00
Quentin McGaw
0c0f1663b1 chore: simplify provider GetConnection 2022-04-20 15:16:55 +00:00
Quentin McGaw
306d8494d6 hotfix(servers): assume UDP+TCP if not precised 2022-04-19 11:52:05 +00:00
Quentin McGaw
f5c00c3e2d chore(filter): common filter for all providers 2022-04-18 17:08:31 +00:00
Quentin McGaw
ac9571c6b2 chore(storage): runtime defaults on servers data
- `openvpn` default VPN protocol for servers
- True UDP if VPN protocol is Wireguard
2022-04-18 12:08:26 +00:00
Quentin McGaw
934fafb64b chore(constants): internal/constants/vpn package 2022-04-18 11:14:07 +00:00
Quentin McGaw
d51514015f chore(storage): simplify reading of server file 2022-04-18 11:14:02 +00:00
Quentin McGaw
a9cfd16d53 chore(validation): uniformize server filters build 2022-04-18 07:27:00 +00:00
Quentin McGaw
1a6f26fa3b feat(nordvpn): remove OpenVPN compression 2022-04-18 07:26:53 +00:00
Quentin McGaw
0dd723b29f chore(provider): add safety connection count check 2022-04-17 16:23:53 +00:00
Quentin McGaw
7ad6fc8e73 docs(maintenance): update document 2022-04-17 16:21:21 +00:00
Quentin McGaw
31c7e6362b chore(devcontainer): multiple changes and fixes
- Fix windows script sourcing
- Remove image name to avoid conflicts
- Bind mount normally without `:z`
- Install `htop`
2022-04-17 16:21:21 +00:00
Quentin McGaw
072b42d867 chore(v4): add v4 comments about server names 2022-04-17 16:21:21 +00:00
Quentin McGaw
5d66c193aa chore(models): common Server & Servers for all providers (#943) 2022-04-17 16:21:19 +00:00
Quentin McGaw
aa729515b9 chore(models): streamline all server models IPs (#942)
- Use `IPs []net.IP` for all server models
- Use `ips` JSON field for all server models
- Merge IPv4 and IPv6 addresses together for Mullvad
2022-04-17 16:18:34 +00:00
Quentin McGaw
54b7e23974 chore(constants): internal/constants/providers
- New package to avoid package import cycles
2022-04-16 19:30:26 +00:00
Quentin McGaw
ad80e0c1ab feat(protonvpn): update servers data 2022-04-16 17:52:53 +00:00
Quentin McGaw
5d7b278957 change(protonvpn): change server name JSON field from name to server_name 2022-04-16 17:51:15 +00:00
dependabot[bot]
678caaf6a0 Chore(deps): Bump docker/build-push-action from 2.9.0 to 2.10.0 (#893) 2022-04-15 12:23:38 -04:00
dependabot[bot]
7228cd7b12 Chore(deps): Bump github.com/breml/rootcerts from 0.2.2 to 0.2.3 (#926) 2022-04-15 12:22:55 -04:00
Martin Bjeldbak Madsen
7b598a3534 docs(readme): remove announcement (#938) 2022-04-15 12:22:30 -04:00
Quentin McGaw
9cdc9e9153 feat(pia): server data updated 2022-04-11 21:29:16 +00:00
Quentin McGaw
71ab0416b0 fix(iptables): use OUTPUT chain for test instead of INPUT 2022-04-11 21:05:12 +00:00
Quentin McGaw
10a13bc8a7 fix(health): change default target address to cloudflare.com:443 2022-04-11 20:21:15 +00:00
Mirco Ianese
be386a8e33 feat(fastestvpn): update servers data (#923) 2022-04-02 13:31:00 -04:00
Quentin McGaw
c33fb8bb97 fix(env): OPENVPN_FLAGS functionality 2022-03-31 20:49:01 +00:00
Quentin McGaw
20f20f051b fix(firewall): iptables support detection
- Add dummy rule to `INPUT` to test for iptables support
- This may resolve #896
2022-03-30 09:03:25 +00:00
Quentin McGaw
179274ade0 feat(log): use github.com/qdm12/log library 2022-03-30 09:03:20 +00:00
Quentin McGaw
84607e332b chore(server): use httpserver package for control server 2022-03-30 09:00:42 +00:00
Quentin McGaw
8186ef2342 chore(httpserver): remove name field 2022-03-30 09:00:36 +00:00
Mirco Ianese
19b184adba fix(purevpn): update servers Zip file download URL (#915)
- Fix PureVPN zip file download link
- Update all PureVPN server information
2022-03-28 15:47:40 -04:00
Quentin McGaw
a97fd35d6e fix(ci): openvpn 2.4.12-r0 2022-03-28 17:32:56 +00:00
dependabot[bot]
470ca020e2 Chore(deps): Bump github.com/stretchr/testify from 1.7.0 to 1.7.1 (#897)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.0...v1.7.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-28 13:23:08 -04:00
dependabot[bot]
f64d7c4343 Chore(deps): Bump peter-evans/dockerhub-description from 2 to 3 (#908)
Bumps [peter-evans/dockerhub-description](https://github.com/peter-evans/dockerhub-description) from 2 to 3.
- [Release notes](https://github.com/peter-evans/dockerhub-description/releases)
- [Commits](https://github.com/peter-evans/dockerhub-description/compare/v2...v3)

---
updated-dependencies:
- dependency-name: peter-evans/dockerhub-description
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-28 13:22:58 -04:00
Quentin McGaw
c6f68a64e6 fix(health): use TCP dialing instead of ping
- `HEALTH_TARGET_ADDRESS` to replace `HEALTH_ADDRESS_TO_PING`
- Remove `github.com/go-ping/ping` dependency
- Dial TCP the target address, appending `:443` if port is not set
2022-03-22 08:50:56 +00:00
Quentin McGaw
5aaa122460 feat(protonvpn): update server information 2022-03-17 19:25:33 +00:00
Quentin McGaw
de169c027f feat(privatevpn): update server information 2022-03-16 10:21:49 +00:00
Quentin McGaw
314c9663a2 fix(privatevpn): update servers without hostname 2022-03-16 10:21:42 +00:00
Quentin McGaw
21995eb3e3 feat(privado): update server information 2022-03-16 10:06:10 +00:00
Quentin McGaw
6fc700bd62 feat(mullvad): update server information 2022-03-16 10:05:01 +00:00
Quentin McGaw
acdbe2163e chore(protonvpn): remove unused exit IPs field 2022-03-16 09:44:57 +00:00
Quentin McGaw
c3a231e0ab chore(storage): omit empty fields in servers.json 2022-03-16 09:43:47 +00:00
Quentin McGaw
984e143336 feat(shutdown): log out OS signal name 2022-03-15 08:16:08 +00:00
Quentin McGaw
e2ba2f82c0 feat(routing): add IPv6 inbound routing 2022-03-13 19:36:45 +00:00
Quentin McGaw
ace5e97e68 fix(routing): only set routes for IPv4 default routes 2022-03-13 14:40:17 +00:00
Quentin McGaw
82d42297e8 chore(routing): remove unused LocalSubnetGetter 2022-03-13 13:32:19 +00:00
Quentin McGaw
f99d5e8656 feat(firewall): use all default routes
- Accept output traffic from all default routes through VPN interface
- Accept output from all default routes to outbound subnets
- Accept all input traffic on ports for all default routes
- Add IP rules for all default routes
2022-03-13 13:26:33 +00:00
dependabot[bot]
0795008c23 Chore(deps): Bump docker/build-push-action from 2.8.0 to 2.9.0 (#832)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 2.8.0 to 2.9.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v2.8.0...v2.9.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-09 16:09:51 -05:00
dependabot[bot]
c975a86a70 Chore(deps): Bump actions/checkout from 2.4.0 to 3 (#870)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.0 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.4.0...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-09 16:09:28 -05:00
Quentin McGaw
69eee345d2 feat(ivpn): allow no password for account IDs
- When matching `i-xxxx-xxxx-xxxx` username
- When matching `ivpn-xxxx-xxxx-xxxx` username
2022-03-09 21:01:25 +00:00
Quentin McGaw
48afc05bcb docs(readme): re-add /dev/net/tun since some OS need it 2022-03-09 11:20:05 +00:00
Quentin McGaw
39a62f5db7 feat(firewall): improve error message when NET_ADMIN is missing 2022-03-09 11:16:10 +00:00
Quentin McGaw
006b218ade feat(firewall): auto-detect which iptables
- On `iptables` error, try to use `iptables-nft`
- On `ip6tables` error, try to use `ip6tables-nft`
2022-02-26 22:55:22 +00:00
909 changed files with 315074 additions and 106689 deletions

View File

@@ -1,5 +1,4 @@
.dockerignore .dockerignore
devcontainer.json devcontainer.json
docker-compose.yml
Dockerfile Dockerfile
README.md README.md

View File

@@ -1,2 +1,2 @@
FROM qmcgaw/godevcontainer FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
RUN apk add wireguard-tools RUN apk add wireguard-tools htop openssl

View File

@@ -2,68 +2,47 @@
Development container that can be used with VSCode. Development container that can be used with VSCode.
It works on Linux, Windows and OSX. It works on Linux, Windows (WSL2) and OSX.
## Requirements ## Requirements
- [VS code](https://code.visualstudio.com/download) installed - [VS code](https://code.visualstudio.com/download) installed
- [VS code remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed - [VS code dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
- [Docker](https://www.docker.com/products/docker-desktop) installed and running - [Docker](https://www.docker.com/products/docker-desktop) installed and running
- If you don't use Linux or WSL 2, share your home directory `~/` and the directory of your project with Docker Desktop
- [Docker Compose](https://docs.docker.com/compose/install/) installed
- Ensure your host has the following and that they are accessible by Docker:
- `~/.ssh` directory
- `~/.gitconfig` file (can be empty)
## Setup ## Setup
1. Create the following files and directory on your host if you don't have them:
```sh
touch ~/.gitconfig ~/.zsh_history
mkdir -p ~/.ssh
```
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. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory. 1. Select `Dev-Containers: Open Folder in Container...` and choose the project directory.
1. For Docker running on Windows HyperV, if you want to use SSH keys, bind mount them at `/tmp/.ssh` by changing the `volumes` section in the [docker-compose.yml](docker-compose.yml).
## Customization ## Customization
### Customize the image For any customization to take effect, you should "rebuild and reopen":
You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be: 1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
2. Select `Dev-Containers: Rebuild Container`
```Dockerfile Changes you can make are notably:
FROM qmcgaw/godevcontainer
USER root
RUN apk add curl
USER vscode
```
Note that you may need to use `USER root` to build as root, and then change back to `USER vscode`. - Changes to the Docker image in [Dockerfile](Dockerfile)
- Changes to VSCode **settings** and **extensions** in [devcontainer.json](devcontainer.json).
- Change the entrypoint script by adding a bind mount in [devcontainer.json](devcontainer.json) of a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh). For example:
To rebuild the image, either: ```json
// Welcome script
- With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container` {
- With a terminal, go to this directory and `docker-compose build` "source": "/yourpath/.welcome.sh",
"target": "/root/.welcome.sh",
### Customize VS code settings "type": "bind"
},
You can customize **settings** and **extensions** in the [devcontainer.json](devcontainer.json) definition file.
### Entrypoint script
You can bind mount a shell script to `/home/vscode/.welcome.sh` to replace the [current welcome script](shell/.welcome.sh).
### Publish a port
To access a port from your host to your development container, publish a port in [docker-compose.yml](docker-compose.yml).
### Run other services
1. Modify [docker-compose.yml](docker-compose.yml) to launch other services at the same time as this development container, such as a test database:
```yml
database:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: password
``` ```
1. In [devcontainer.json](devcontainer.json), change the line `"runServices": ["vscode"],` to `"runServices": ["vscode", "database"],`. - More options are documented in the [devcontainer.json reference](https://containers.dev/implementors/json_reference/).
1. In the VS code command palette, rebuild the container.

View File

@@ -1,81 +1,111 @@
{ {
"name": "gluetun-dev", "name": "gluetun-dev",
"dockerComposeFile": [ // User defined settings
"docker-compose.yml" "containerEnv": {
], "TZ": ""
"service": "vscode", },
"runServices": [ // Fixed settings
"vscode" "build": {
], "dockerfile": "./Dockerfile"
"shutdownAction": "stopCompose", },
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy", "postCreateCommand": "~/.windows.sh && go mod download",
"workspaceFolder": "/workspace", "capAdd": [
"extensions": [ "NET_ADMIN", // Gluetun specific
"golang.go", "SYS_PTRACE" // for dlv Go debugging
"eamodio.gitlens", // IDE Git information ],
"davidanson.vscode-markdownlint", "securityOpt": [
"ms-azuretools.vscode-docker", // Docker integration and linting "seccomp=unconfined" // for dlv Go debugging
"shardulm94.trailing-spaces", // Show trailing spaces ],
"Gruntfuggly.todo-tree", // Highlights TODO comments "mounts": [
"bierner.emojisense", // Emoji sense for markdown // Zsh commands history persistence
"stkb.rewrap", // rewrap comments after n characters on one line {
"vscode-icons-team.vscode-icons", // Better file extension icons "source": "${localEnv:HOME}/.zsh_history",
"github.vscode-pull-request-github", // Github interaction "target": "/root/.zsh_history",
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting "type": "bind"
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked },
"IBM.output-colorizer", // Colorize your output/test logs // Git configuration file
"mohsen1.prettify-json", // Prettify JSON data {
], "source": "${localEnv:HOME}/.gitconfig",
"settings": { "target": "/root/.gitconfig",
"files.eol": "\n", "type": "bind"
"remote.extensionKind": { },
"ms-azuretools.vscode-docker": "workspace" // SSH directory for Linux, OSX and WSL
}, // On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
"editor.codeActionsOnSaveTimeout": 3000, // created in the container. On Windows, files are copied
"go.useLanguageServer": true, // from /mnt/ssh to ~/.ssh to fix permissions.
"[go]": { {
"editor.formatOnSave": true, "source": "${localEnv:HOME}/.ssh",
"editor.codeActionsOnSave": { "target": "/mnt/ssh",
"source.organizeImports": true, "type": "bind"
}, },
// Optional: Disable snippets, as they conflict with completion ranking. // Docker socket to access the host Docker server
"editor.snippetSuggestions": "none" {
}, "source": "/var/run/docker.sock",
"[go.mod]": { "target": "/var/run/docker.sock",
"editor.formatOnSave": true, "type": "bind"
"editor.codeActionsOnSave": { }
"source.organizeImports": true, ],
}, "customizations": {
}, "vscode": {
"gopls": { "extensions": [
"usePlaceholders": false, "golang.go",
"staticcheck": true "eamodio.gitlens", // IDE Git information
}, "davidanson.vscode-markdownlint",
"go.autocompleteUnimportedPackages": true, "ms-azuretools.vscode-docker", // Docker integration and linting
"go.gotoSymbol.includeImports": true, "shardulm94.trailing-spaces", // Show trailing spaces
"go.gotoSymbol.includeGoroot": true, "Gruntfuggly.todo-tree", // Highlights TODO comments
"go.lintTool": "golangci-lint", "bierner.emojisense", // Emoji sense for markdown
"go.buildOnSave": "workspace", "stkb.rewrap", // rewrap comments after n characters on one line
"go.lintOnSave": "workspace", "vscode-icons-team.vscode-icons", // Better file extension icons
"go.vetOnSave": "workspace", "github.vscode-pull-request-github", // Github interaction
"editor.formatOnSave": true, "redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
"go.toolsEnvVars": { "bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
"GOFLAGS": "-tags=", "IBM.output-colorizer", // Colorize your output/test logs
// "CGO_ENABLED": 1 // for the race detector "github.copilot" // AI code completion
}, ],
"gopls.env": { "settings": {
"GOFLAGS": "-tags=" "files.eol": "\n",
}, "remote.extensionKind": {
"go.testEnvVars": { "ms-azuretools.vscode-docker": "workspace"
"": "" },
}, "go.useLanguageServer": true,
"go.testFlags": [ "[go]": {
"-v", "editor.codeActionsOnSave": {
// "-race" "source.organizeImports": "explicit"
], }
"go.testTimeout": "10s", },
"go.coverOnSingleTest": true, "[go.mod]": {
"go.coverOnSingleTestFile": true, "editor.codeActionsOnSave": {
"go.coverOnTestPackage": true "source.organizeImports": "explicit"
} }
},
"gopls": {
"usePlaceholders": false,
"staticcheck": true,
"ui.diagnostic.analyses": {
"ST1000": false
},
"formatting.gofumpt": true,
},
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package",
"editor.formatOnSave": true,
"go.buildTags": "linux",
"go.toolsEnvVars": {
"CGO_ENABLED": "0"
},
"go.testEnvVars": {
"CGO_ENABLED": "1"
},
"go.testFlags": [
"-v",
"-race"
],
"go.testTimeout": "10s",
"go.coverOnSingleTest": true,
"go.coverOnSingleTestFile": true,
"go.coverOnTestPackage": true
}
}
}
} }

View File

@@ -1,33 +0,0 @@
version: "3.7"
services:
vscode:
build: .
image: godevcontainer
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ../:/workspace
# Docker socket to access Docker server
- /var/run/docker.sock:/var/run/docker.sock
# Docker configuration
- ~/.docker:/root/.docker:z
# SSH directory for Linux, OSX and WSL
- ~/.ssh:/root/.ssh:z
# For Windows without WSL, a copy will be made
# from /tmp/.ssh to ~/.ssh to fix permissions
#- ~/.ssh:/tmp/.ssh:ro
# Shell history persistence
- ~/.zsh_history:/root/.zsh_history:z
# Git config
- ~/.gitconfig:/root/.gitconfig:z
environment:
- TZ=
cap_add:
# For debugging with dlv
# - SYS_PTRACE
- NET_ADMIN
security_opt:
# For debugging with dlv
- seccomp:unconfined
entrypoint: zsh -c "while sleep 1000; do :; done"

View File

@@ -13,6 +13,6 @@ Contributions are [released](https://help.github.com/articles/github-terms-of-se
## Resources ## Resources
- [Gluetun guide on development](https://github.com/qdm12/gluetun/wiki/Development) - [Gluetun guide on development](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)

View File

@@ -7,13 +7,18 @@ body:
attributes: attributes:
value: | value: |
Thanks for taking the time to fill out this bug report! Thanks for taking the time to fill out this bug report!
⚠️ Your issue will be instantly closed as not planned WITHOUT explanation if:
- you do not fill out **the title of the issue** ☝️
- you do not provide the **Gluetun version** as requested below
- you provide **less than 10 lines of logs** as requested below
- type: dropdown - type: dropdown
id: urgent id: urgent
attributes: attributes:
label: Is this urgent? label: Is this urgent?
description: | description: |
Is this a critical bug, or do you need this fixed urgently? Is this a critical bug, or do you need this fixed urgently?
If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun/wiki/Docker-image-tags) if that can help. If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md) if that can help.
options: options:
- "No" - "No"
- "Yes" - "Yes"
@@ -40,10 +45,12 @@ body:
attributes: attributes:
label: VPN service provider label: VPN service provider
options: options:
- AirVPN
- Custom - Custom
- Cyberghost - Cyberghost
- ExpressVPN - ExpressVPN
- FastestVPN - FastestVPN
- Giganews
- HideMyAss - HideMyAss
- IPVanish - IPVanish
- IVPN - IVPN
@@ -54,8 +61,10 @@ body:
- PrivateVPN - PrivateVPN
- ProtonVPN - ProtonVPN
- PureVPN - PureVPN
- SlickVPN
- Surfshark - Surfshark
- TorGuard - TorGuard
- VPNSecure.me
- VPNUnlimited - VPNUnlimited
- VyprVPN - VyprVPN
- WeVPN - WeVPN
@@ -72,6 +81,7 @@ body:
- Portainer - Portainer
- Kubernetes - Kubernetes
- Podman - Podman
- Unraid
- Other - Other
validations: validations:
required: true required: true
@@ -81,7 +91,7 @@ body:
label: What is the version of Gluetun label: What is the version of Gluetun
description: | description: |
Copy paste the version line at the top of your logs. Copy paste the version line at the top of your logs.
It should be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`. It MUST be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
validations: validations:
required: true required: true
- type: textarea - type: textarea
@@ -94,9 +104,9 @@ body:
- type: textarea - type: textarea
id: logs id: logs
attributes: attributes:
label: Share your logs label: Share your logs (at least 10 lines)
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`. description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
render: log render: plain text
validations: validations:
required: true required: true
- type: textarea - type: textarea

View File

@@ -1,6 +1,10 @@
blank_issues_enabled: false
contact_links: contact_links:
- name: Report a Wiki issue
url: https://github.com/qdm12/gluetun-wiki/issues/new/choose
about: Please create an issue on the gluetun-wiki repository.
- name: Configuration help? - name: Configuration help?
url: https://github.com/qdm12/gluetun/discussions/new url: https://github.com/qdm12/gluetun/discussions/new/choose
about: Please create a Github discussion. about: Please create a Github discussion.
- name: Unraid template issue - name: Unraid template issue
url: https://github.com/qdm12/gluetun/discussions/550 url: https://github.com/qdm12/gluetun/discussions/550

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 - There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP - 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 - 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](https://github.com/qdm12/gluetun/wiki/Openvpn-file) describes how to do so.

View File

@@ -1,18 +0,0 @@
name: Wiki issue
description: Report a Wiki issue
title: "Wiki issue: "
labels: ["📄 Wiki issue"]
body:
- type: input
id: url
attributes:
label: "URL to the Wiki page"
placeholder: "https://github.com/qdm12/gluetun/wiki/OpenVPN-options"
validations:
required: true
- type: textarea
id: description
attributes:
label: "What's the issue?"
validations:
required: true

198
.github/labels.yml vendored
View File

@@ -1,110 +1,152 @@
- name: "Bug :bug:" - name: "Status: 🗯️ Waiting for feedback"
color: "b60205" color: "f7d692"
description: "" - name: "Status: 🔴 Blocked"
- name: "Feature request :bulb:" color: "f7d692"
color: "0e8a16" description: "Blocked by another issue or pull request"
description: "" - name: "Status: 📌 Before next release"
- name: "Help wanted :pray:" color: "f7d692"
color: "4caf50" description: "Has to be done before the next release"
description: "" - name: "Status: 🔒 After next release"
- name: "Documentation :memo:" color: "f7d692"
color: "c5def5" description: "Will be done after the next release"
description: "" - name: "Status: 🟡 Nearly resolved"
- name: "Needs more info :thinking:" color: "f7d692"
color: "795548" description: "This might be resolved or is about to be resolved"
description: ""
# Priority - name: "Closed: ⚰️ Inactive"
- name: "🚨 Urgent" color: "959a9c"
color: "d5232f" description: "No answer was received for weeks"
description: "" - name: "Closed: 👥 Duplicate"
- name: "💤 Low priority" color: "959a9c"
color: "4285f4" description: "Issue duplicates an existing issue"
description: "" - name: "Closed: 🗑️ Bad issue"
color: "959a9c"
- name: "Closed: ☠️ cannot be done"
color: "959a9c"
- name: "Priority: 🚨 Urgent"
color: "03adfc"
- name: "Priority: 💤 Low priority"
color: "03adfc"
- name: "Complexity: ☣️ Hard to do"
color: "ff9efc"
- name: "Complexity: 🟩 Easy to do"
color: "ff9efc"
- name: "Popularity: ❤️‍🔥 extreme"
color: "ffc7ea"
- name: "Popularity: ❤️ high"
color: "ffc7ea"
# VPN providers # VPN providers
- name: ":cloud: Cyberghost" - name: "☁️ AirVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Custom"
- name: ":cloud: HideMyAss"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Cyberghost"
- name: ":cloud: IPVanish"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Giganews"
- name: ":cloud: IVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ HideMyAss"
- name: ":cloud: ExpressVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ IPVanish"
- name: ":cloud: FastestVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ IVPN"
- name: ":cloud: Mullvad"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ ExpressVPN"
- name: ":cloud: NordVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ FastestVPN"
- name: ":cloud: Perfect Privacy"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Mullvad"
- name: ":cloud: PIA"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ NordVPN"
- name: ":cloud: Privado"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Perfect Privacy"
- name: ":cloud: PrivateVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ PIA"
- name: ":cloud: ProtonVPN"
color: "cfe8d4" color: "cfe8d4"
- name: ":cloud: PureVPN" - name: "☁️ Privado"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ PrivateVPN"
- name: ":cloud: Surfshark"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ ProtonVPN"
- name: ":cloud: Torguard"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ PureVPN"
- name: ":cloud: VPNUnlimited"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ SlickVPN"
- name: ":cloud: Vyprvpn"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Surfshark"
- name: ":cloud: WeVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Torguard"
- name: ":cloud: Windscribe" color: "cfe8d4"
- name: "☁️ VPNSecure.me"
color: "cfe8d4"
- name: "☁️ VPNUnlimited"
color: "cfe8d4"
- name: "☁️ Vyprvpn"
color: "cfe8d4"
- name: "☁️ WeVPN"
color: "cfe8d4"
- name: "☁️ Windscribe"
color: "cfe8d4" color: "cfe8d4"
description: ""
# Problem category - name: "Category: User error 🤦"
- name: "Openvpn" from_name: "Category: Config problem 📝"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Healthcheck 🩺"
- name: "Wireguard"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Documentation ✒️"
- name: "Unbound (DNS over TLS)" description: "A problem with the readme or a code comment."
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Maintenance ⛓️"
- name: "Firewall" description: "Anything related to code or other maintenance"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Logs 📚"
- name: "HTTP proxy" description: "Something to change in logs"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Good idea 🎯"
- name: "Shadowsocks" description: "This is a good idea, judged by the maintainers"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Motivated! 🙌"
- name: "Healthcheck server" description: "Your pumpness makes me pumped! The issue or PR shows great motivation!"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Foolproof settings 👼"
- name: "Control server" color: "ffc7ea"
- name: "Category: Label missing ❗"
color: "ffc7ea"
- name: "Category: updater ♻️"
color: "ffc7ea"
description: "Concerns the code to update servers data"
- name: "Category: New provider 🆕"
color: "ffc7ea"
- name: "Category: OpenVPN 🔐"
color: "ffc7ea"
- name: "Category: Wireguard 🔐"
color: "ffc7ea"
- name: "Category: DNS 📠"
color: "ffc7ea"
- name: "Category: Firewall ⛓️"
color: "ffc7ea"
- name: "Category: Routing 🛤️"
color: "ffc7ea"
- name: "Category: IPv6 🛰️"
color: "ffc7ea"
- name: "Category: VPN port forwarding 📥"
color: "ffc7ea"
- name: "Category: HTTP proxy 🔁"
color: "ffc7ea"
- name: "Category: Shadowsocks 🔁"
color: "ffc7ea"
- name: "Category: control server ⚙️"
color: "ffc7ea"
- name: "Category: kernel 🧠"
color: "ffc7ea"
- name: "Category: public IP service 💬"
color: "ffc7ea"
- name: "Category: servers storage 📦"
color: "ffc7ea"
- name: "Category: Performance 🚀"
color: "ffc7ea"
- name: "Category: Investigation 🔍"
color: "ffc7ea" color: "ffc7ea"
description: ""

12
.github/pull_request_template.md vendored Normal file
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/)

35
.github/workflows/ci-skip.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: No trigger file paths
on:
push:
branches:
- master
paths-ignore:
- .github/workflows/ci.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
pull_request:
paths-ignore:
- .github/workflows/ci.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
verify:
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: No trigger path triggered for required verify workflow.
run: exit 0

View File

@@ -17,8 +17,6 @@ on:
- go.mod - go.mod
- go.sum - go.sum
pull_request: pull_request:
branches:
- master
paths: paths:
- .github/workflows/ci.yml - .github/workflows/ci.yml
- cmd/** - cmd/**
@@ -32,28 +30,28 @@ on:
jobs: jobs:
verify: verify:
# Only run if it's a push event or if it's a PR from this repository, and it is not dependabot.
if: |
github.actor != 'dependabot[bot]' &&
(github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository))
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
actions: read
contents: read
env: env:
DOCKER_BUILDKIT: "1" DOCKER_BUILDKIT: "1"
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v5
- uses: reviewdog/action-misspell@v1 - uses: reviewdog/action-misspell@v1
with: with:
locale: "US" locale: "US"
level: error level: error
exclude: |
./internal/storage/servers.json
*.md
- name: Linting - name: Linting
run: docker build --target lint . run: docker build --target lint .
- name: Go mod tidy check - name: Mocks check
run: docker build --target tidy . run: docker build --target mocks .
- name: Build test image - name: Build test image
run: docker build --target test -t test-container . run: docker build --target test -t test-container .
@@ -61,69 +59,113 @@ jobs:
- name: Run tests in test container - name: Run tests in test container
run: | run: |
touch coverage.txt touch coverage.txt
docker run --rm \ docker run --rm --device /dev/net/tun \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \ -v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container test-container
- name: Code security analysis
uses: snyk/actions/golang@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Build final image - name: Build final image
run: docker build -t final-image . run: docker build -t final-image .
# - name: Image security analysis verify-private:
# uses: snyk/actions/docker@master
# env:
# SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
# with:
# image: final-image
publish:
# Only run if it's a push event or if it's a PR from this repository
if: | if: |
github.event_name == 'push' || github.repository == 'qdm12/gluetun' &&
github.event_name == 'release' || (
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) 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] needs: [verify]
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: secrets
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v5
- 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
codeql:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- uses: github/codeql-action/init@v4
with:
languages: go
- uses: github/codeql-action/autobuild@v4
- uses: github/codeql-action/analyze@v4
publish:
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, codeql]
permissions:
actions: read
contents: read
packages: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
# extract metadata (tags, labels) for Docker # extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action # https://github.com/docker/metadata-action
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v5
with: with:
flavor: | flavor: |
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
images: | images: |
ghcr.io/qdm12/gluetun
qmcgaw/gluetun qmcgaw/gluetun
qmcgaw/private-internet-access qmcgaw/private-internet-access
tags: | tags: |
type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) }}
type=ref,event=pr type=ref,event=pr
type=semver,pattern=v{{major}}.{{minor}}.{{patch}} type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
type=semver,pattern=v{{major}}.{{minor}} type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }} type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- uses: docker/setup-qemu-action@v1 - uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v1 - uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v1 - uses: docker/login-action@v3
with: with:
username: qmcgaw username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_PASSWORD }}
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: qdm12
password: ${{ github.token }}
- name: Short commit - name: Short commit
id: shortcommit id: shortcommit
run: echo "::set-output name=value::$(git rev-parse --short HEAD)" run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
- name: Build and push final image - name: Build and push final image
uses: docker/build-push-action@v2.8.0 uses: docker/build-push-action@v6
with: with:
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

21
.github/workflows/closed-issue.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Closed issue
on:
issues:
types: [closed]
jobs:
comment:
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ github.token }}
issue-number: ${{ github.event.issue.number }}
body: |
Closed issues are **NOT** monitored, so commenting here is likely to be not seen.
If you think this is *still unresolved* and have **more information** to bring, please create another issue.
This is an automated comment setup because @qdm12 is the sole maintainer of this project
which became too popular to monitor issues closed.

View File

@@ -0,0 +1,14 @@
{
"ignorePatterns": [
{
"pattern": "^https://console.substack.com/p/console-72$"
}
],
"timeout": "20s",
"retryOn429": false,
"fallbackRetryDelay": "30s",
"aliveStatusCodes": [
200,
429
]
}

View File

@@ -1,37 +0,0 @@
name: Dependabot
on:
pull_request:
branches:
- master
paths:
- .github/workflows/dependabot.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
verify:
if: ${{ github.actor == 'dependabot[bot]' }}
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v2.4.0
- name: Build test image
run: docker build --target test -t test-container .
- name: Run tests in test container
run: |
touch coverage.txt
docker run --rm \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container
- name: Build final image
run: docker build -t final-image .

View File

@@ -1,21 +0,0 @@
name: Docker Hub description
on:
push:
branches: [master]
paths:
- README.md
- .github/workflows/dockerhub-description.yml
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.4.0
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v2
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: qmcgaw/gluetun
short-description: Lightweight Swiss-knife VPN client to connect to several VPN providers
readme-filepath: README.md

View File

@@ -1,40 +0,0 @@
name: Fork
on:
pull_request:
branches:
- master
paths:
- .github/workflows/fork.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
verify:
if: github.event.pull_request.head.repo.full_name != github.repository && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v2.4.0
- name: Linting
run: docker build --target lint .
- name: Build test image
run: docker build --target test -t test-container .
- name: Run tests in test container
run: |
touch coverage.txt
docker run --rm \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container
- name: Build final image
run: docker build -t final-image .

View File

@@ -7,9 +7,11 @@ on:
- .github/workflows/labels.yml - .github/workflows/labels.yml
jobs: jobs:
labeler: labeler:
permissions:
issues: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v5
- uses: crazy-max/ghaction-github-labeler@v3 - uses: crazy-max/ghaction-github-labeler@v5
with: with:
yaml-file: .github/labels.yml yaml-file: .github/labels.yml

21
.github/workflows/markdown-skip.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Markdown
on:
push:
branches:
- master
paths-ignore:
- "**.md"
- .github/workflows/markdown.yml
pull_request:
paths-ignore:
- "**.md"
- .github/workflows/markdown.yml
jobs:
markdown:
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: No trigger path triggered for required markdown workflow.
run: exit 0

47
.github/workflows/markdown.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Markdown
on:
push:
branches:
- master
paths:
- "**.md"
- .github/workflows/markdown.yml
pull_request:
paths:
- "**.md"
- .github/workflows/markdown.yml
jobs:
markdown:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- uses: actions/checkout@v5
- uses: DavidAnson/markdownlint-cli2-action@v21
with:
globs: "**.md"
config: .markdownlint-cli2.jsonc
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error
pattern: |
*.md
- uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: yes
config-file: .github/workflows/configs/mlc-config.json
- uses: peter-evans/dockerhub-description@v4
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: qmcgaw/gluetun
short-description: Lightweight Swiss-knife VPN client to connect to several VPN providers
readme-filepath: README.md

22
.github/workflows/opened-issue.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Opened issue
on:
issues:
types: [opened]
jobs:
comment:
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ github.token }}
issue-number: ${{ github.event.issue.number }}
body: |
@qdm12 is more or less the only maintainer of this project and works on it in his free time.
Please:
- **do not** ask for updates, be patient
- :+1: the issue to show your support instead of commenting
@qdm12 usually checks issues at least once a week, if this is a new urgent bug,
[revert to an older tagged container image](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md)

View File

@@ -1,66 +1,94 @@
linters-settings: version: "2"
misspell:
locale: US
issues: formatters:
exclude-rules: enable:
- path: _test\.go - gci
linters: - gofumpt
- dupl - goimports
- maligned exclusions:
- goerr113 generated: lax
- containedctx paths:
- path: internal/server/ - third_party$
linters: - builtin$
- dupl - examples$
- path: internal/configuration/
linters:
- dupl
- path: internal/constants/
linters:
- dupl
- text: "exported: exported var Err*"
linters:
- revive
- text: "mnd: Magic number: 0644*"
linters:
- gomnd
- text: "mnd: Magic number: 0400*"
linters:
- gomnd
- text: "variable 'mssFix' is only used in the if-statement*"
path: "openvpnconf.go"
linters:
- ifshort
- text: "variable 'auth' is only used in the if-statement*"
path: "openvpnconf.go"
linters:
- ifshort
- linters:
- lll
source: "^//go:generate "
linters: 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: enable:
# - cyclop # - cyclop
# - errorlint # - errorlint
# - ireturn - asasalint
# - varnamelen
# - wrapcheck
- asciicheck - asciicheck
- bidichk - bidichk
- bodyclose - bodyclose
- containedctx - containedctx
- copyloopvar
- decorder - decorder
- dogsled - dogsled
- dupl - dupl
- dupword
- durationcheck - durationcheck
- err113
- errchkjson - errchkjson
- errname - errname
- exhaustive - exhaustive
- exportloopref - fatcontext
- forcetypeassert - forcetypeassert
- gci - gocheckcompilerdirectives
- gochecknoglobals - gochecknoglobals
- gochecknoinits - gochecknoinits
- gocognit - gocognit
@@ -68,43 +96,44 @@ linters:
- gocritic - gocritic
- gocyclo - gocyclo
- godot - godot
- goerr113
- goheader - goheader
- goimports
- gomnd
- gomoddirectives - gomoddirectives
- goprintffuncname - goprintffuncname
- gosec - gosec
- gosmopolitan
- grouper - grouper
- ifshort
- importas - importas
- interfacebloat
- intrange
- ireturn
- lll - lll
- maintidx - maintidx
- makezero - makezero
- mirror
- misspell - misspell
- mnd
- musttag
- nakedret - nakedret
- nestif - nestif
- nilerr - nilerr
- nilnil - nilnil
- noctx - noctx
- nolintlint - nolintlint
- nosprintfhostport
- paralleltest
- prealloc - prealloc
- predeclared - predeclared
- predeclared
- promlinter - promlinter
- reassign
- revive - revive
- rowserrcheck - rowserrcheck
- sqlclosecheck - sqlclosecheck
- tenv - tagalign
- thelper - thelper
- tparallel - tparallel
- unconvert - unconvert
- unparam - unparam
- usestdlibvars
- wastedassign - wastedassign
- whitespace - whitespace
- zerologlint
run:
skip-dirs:
- .devcontainer
- .github
- doc

9
.markdownlint-cli2.jsonc Normal file
View File

@@ -0,0 +1,9 @@
{
"config": {
"default": true,
"MD013": false,
},
"ignores": [
".github/pull_request_template.md"
]
}

8
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
// This list should be kept to the strict minimum
// to develop this project.
"recommendations": [
"golang.go",
"davidanson.vscode-markdownlint",
],
}

29
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,29 @@
{
// The settings should be kept to the strict minimum
// to develop this project.
"files.eol": "\n",
"editor.formatOnSave": true,
"go.buildTags": "linux",
"go.toolsEnvVars": {
"CGO_ENABLED": "0"
},
"go.testEnvVars": {
"CGO_ENABLED": "1"
},
"go.testFlags": [
"-v",
"-race"
],
"go.testTimeout": "10s",
"go.coverOnSingleTest": true,
"go.coverOnSingleTestFile": true,
"go.coverOnTestPackage": true,
"go.useLanguageServer": true,
"[go]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package"
}

51
.vscode/tasks.json vendored Normal file
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",
},
]
}

View File

@@ -1,18 +1,22 @@
ARG ALPINE_VERSION=3.15 ARG ALPINE_VERSION=3.22
ARG GO_ALPINE_VERSION=3.15 ARG GO_ALPINE_VERSION=3.22
ARG GO_VERSION=1.17 ARG GO_VERSION=1.25
ARG XCPUTRANSLATE_VERSION=v0.6.0 ARG XCPUTRANSLATE_VERSION=v0.9.0
ARG GOLANGCI_LINT_VERSION=v1.44.2 ARG GOLANGCI_LINT_VERSION=v2.4.0
ARG MOCKGEN_VERSION=v1.6.0
ARG BUILDPLATFORM=linux/amd64 ARG BUILDPLATFORM=linux/amd64
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint 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 FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
RUN apk --update add git g++ # Note: findutils needed to have xargs support `-d` flag for mocks stage.
RUN apk --update add git g++ findutils
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
COPY --from=golangci-lint /bin /go/bin/golangci-lint COPY --from=golangci-lint /bin /go/bin/golangci-lint
COPY --from=mockgen /bin /go/bin/mockgen
WORKDIR /tmp/gobuild WORKDIR /tmp/gobuild
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
@@ -28,16 +32,19 @@ ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=a
FROM --platform=${BUILDPLATFORM} base AS lint FROM --platform=${BUILDPLATFORM} base AS lint
COPY .golangci.yml ./ COPY .golangci.yml ./
RUN golangci-lint run --timeout=10m RUN golangci-lint run
FROM --platform=${BUILDPLATFORM} base AS tidy FROM --platform=${BUILDPLATFORM} base AS mocks
RUN git init && \ RUN git init && \
git config user.email ci@localhost && \ git config user.email ci@localhost && \
git config user.name ci && \ git config user.name ci && \
git add -A && git commit -m ci && \ git config core.fileMode false && \
sed -i '/\/\/ indirect/d' go.mod && \ git add -A && \
go mod tidy && \ git commit -m "snapshot" && \
git diff --exit-code -- go.mod grep -lr -E '^// Code generated by MockGen\. DO NOT EDIT\.$' . | xargs -r -d '\n' rm && \
go generate -run "mockgen" ./... && \
git diff --exit-code && \
rm -rf .git/
FROM --platform=${BUILDPLATFORM} base AS build FROM --platform=${BUILDPLATFORM} base AS build
ARG TARGETPLATFORM ARG TARGETPLATFORM
@@ -69,53 +76,85 @@ LABEL \
ENV VPN_SERVICE_PROVIDER=pia \ ENV VPN_SERVICE_PROVIDER=pia \
VPN_TYPE=openvpn \ VPN_TYPE=openvpn \
# Common VPN options # Common VPN options
VPN_ENDPOINT_IP= \
VPN_ENDPOINT_PORT= \
VPN_INTERFACE=tun0 \ VPN_INTERFACE=tun0 \
# OpenVPN # OpenVPN
OPENVPN_ENDPOINT_IP= \
OPENVPN_ENDPOINT_PORT= \
OPENVPN_PROTOCOL=udp \ OPENVPN_PROTOCOL=udp \
OPENVPN_USER= \ OPENVPN_USER= \
OPENVPN_PASSWORD= \ OPENVPN_PASSWORD= \
OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \ OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \
OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \ OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
OPENVPN_VERSION=2.5 \ OPENVPN_VERSION=2.6 \
OPENVPN_VERBOSITY=1 \ OPENVPN_VERBOSITY=1 \
OPENVPN_FLAGS= \ OPENVPN_FLAGS= \
OPENVPN_CIPHERS= \ OPENVPN_CIPHERS= \
OPENVPN_AUTH= \ OPENVPN_AUTH= \
OPENVPN_PROCESS_USER= \ OPENVPN_PROCESS_USER=root \
OPENVPN_IPV6=off \ OPENVPN_MSSFIX= \
OPENVPN_CUSTOM_CONFIG= \ OPENVPN_CUSTOM_CONFIG= \
# Wireguard # Wireguard
WIREGUARD_ENDPOINT_IP= \
WIREGUARD_ENDPOINT_PORT= \
WIREGUARD_CONF_SECRETFILE=/run/secrets/wg0.conf \
WIREGUARD_PRIVATE_KEY= \ WIREGUARD_PRIVATE_KEY= \
WIREGUARD_PRIVATE_KEY_SECRETFILE=/run/secrets/wireguard_private_key \
WIREGUARD_PRESHARED_KEY= \ WIREGUARD_PRESHARED_KEY= \
WIREGUARD_PRESHARED_KEY_SECRETFILE=/run/secrets/wireguard_preshared_key \
WIREGUARD_PUBLIC_KEY= \ WIREGUARD_PUBLIC_KEY= \
WIREGUARD_ALLOWED_IPS= \
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
WIREGUARD_ADDRESSES= \ WIREGUARD_ADDRESSES= \
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
WIREGUARD_MTU=1320 \
WIREGUARD_IMPLEMENTATION=auto \
# VPN server filtering # VPN server filtering
SERVER_REGIONS= \ SERVER_REGIONS= \
SERVER_COUNTRIES= \ SERVER_COUNTRIES= \
SERVER_CITIES= \ SERVER_CITIES= \
SERVER_HOSTNAMES= \ SERVER_HOSTNAMES= \
SERVER_CATEGORIES= \
# # Mullvad only: # # Mullvad only:
ISP= \ ISP= \
OWNED_ONLY=no \ OWNED_ONLY=no \
# # Private Internet Access only: # # Private Internet Access only:
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \ PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING=off \ VPN_PORT_FORWARDING=off \
PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \ VPN_PORT_FORWARDING_LISTENING_PORT=0 \
VPN_PORT_FORWARDING_PROVIDER= \
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: # # Cyberghost only:
OPENVPN_CERT= \
OPENVPN_KEY= \
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \ OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \ OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
# # VPNSecure only:
OPENVPN_ENCRYPTED_KEY= \
OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key \
OPENVPN_KEY_PASSPHRASE= \
OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase \
# # Nordvpn only: # # Nordvpn only:
SERVER_NUMBER= \ SERVER_NUMBER= \
# # PIA and ProtonVPN only: # # PIA only:
SERVER_NAMES= \ SERVER_NAMES= \
# # ProtonVPN only: # # VPNUnlimited and ProtonVPN only:
STREAM_ONLY= \
FREE_ONLY= \ FREE_ONLY= \
# # ProtonVPN only:
SECURE_CORE_ONLY= \
TOR_ONLY= \
# # Surfshark only: # # Surfshark only:
MULTIHOP_ONLY= \ MULTIHOP_ONLY= \
# # VPN Secure only:
PREMIUM_ONLY= \
# # PIA and ProtonVPN only:
PORT_FORWARD_ONLY= \
# Firewall # Firewall
FIREWALL=on \ FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \
FIREWALL_VPN_INPUT_PORTS= \ FIREWALL_VPN_INPUT_PORTS= \
FIREWALL_INPUT_PORTS= \ FIREWALL_INPUT_PORTS= \
FIREWALL_OUTBOUND_SUBNETS= \ FIREWALL_OUTBOUND_SUBNETS= \
@@ -124,29 +163,28 @@ ENV VPN_SERVICE_PROVIDER=pia \
LOG_LEVEL=info \ LOG_LEVEL=info \
# Health # Health
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \ HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
HEALTH_ADDRESS_TO_PING=github.com \ HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
HEALTH_VPN_DURATION_INITIAL=6s \ HEALTH_ICMP_TARGET_IP=1.1.1.1 \
HEALTH_VPN_DURATION_ADDITION=5s \ HEALTH_RESTART_VPN=on \
# DNS over TLS # DNS
DOT=on \ DNS_UPSTREAM_RESOLVER_TYPE=DoT \
DOT_PROVIDERS=cloudflare \ DNS_UPSTREAM_RESOLVERS=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 \ DNS_BLOCK_IPS= \
DOT_VERBOSITY=1 \ DNS_BLOCK_IP_PREFIXES= \
DOT_VERBOSITY_DETAILS=0 \ DNS_CACHING=on \
DOT_VALIDATION_LOGLEVEL=0 \ DNS_UPSTREAM_IPV6=off \
DOT_CACHING=on \
DOT_IPV6=off \
BLOCK_MALICIOUS=on \ BLOCK_MALICIOUS=on \
BLOCK_SURVEILLANCE=off \ BLOCK_SURVEILLANCE=off \
BLOCK_ADS=off \ BLOCK_ADS=off \
UNBLOCK= \ DNS_UNBLOCK_HOSTNAMES= \
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
DNS_UPDATE_PERIOD=24h \ DNS_UPDATE_PERIOD=24h \
DNS_ADDRESS=127.0.0.1 \ DNS_UPSTREAM_PLAIN_ADDRESSES= \
DNS_KEEP_NAMESERVER=off \
# HTTP proxy # HTTP proxy
HTTPPROXY= \ HTTPPROXY= \
HTTPPROXY_LOG=off \ HTTPPROXY_LOG=off \
HTTPPROXY_LISTENING_ADDRESS=":8888" \ HTTPPROXY_LISTENING_ADDRESS=":8888" \
HTTPPROXY_STEALTH=off \
HTTPPROXY_USER= \ HTTPPROXY_USER= \
HTTPPROXY_PASSWORD= \ HTTPPROXY_PASSWORD= \
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \ HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
@@ -159,13 +197,23 @@ ENV VPN_SERVICE_PROVIDER=pia \
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \ SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \ SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
# Control server # Control server
HTTP_CONTROL_SERVER_LOG=on \
HTTP_CONTROL_SERVER_ADDRESS=":8000" \ HTTP_CONTROL_SERVER_ADDRESS=":8000" \
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE="{}" \
# Server data updater # Server data updater
UPDATER_PERIOD=0 \ UPDATER_PERIOD=0 \
UPDATER_MIN_RATIO=0.8 \
UPDATER_VPN_SERVICE_PROVIDERS= \ UPDATER_VPN_SERVICE_PROVIDERS= \
UPDATER_PROTONVPN_USERNAME= \
UPDATER_PROTONVPN_PASSWORD= \
# Public IP # Public IP
PUBLICIP_FILE="/tmp/gluetun/ip" \ PUBLICIP_FILE="/tmp/gluetun/ip" \
PUBLICIP_PERIOD=12h \ PUBLICIP_ENABLED=on \
PUBLICIP_API=ipinfo,ifconfigco,ip2location,cloudflare \
PUBLICIP_API_TOKEN= \
# Storage
STORAGE_FILEPATH=/gluetun/servers.json \
# Pprof # Pprof
PPROF_ENABLED=no \ PPROF_ENABLED=no \
PPROF_BLOCK_PROFILE_RATE=0 \ PPROF_BLOCK_PROFILE_RATE=0 \
@@ -174,22 +222,19 @@ ENV VPN_SERVICE_PROVIDER=pia \
# Extras # Extras
VERSION_INFORMATION=on \ VERSION_INFORMATION=on \
TZ= \ TZ= \
PUID= \ PUID=1000 \
PGID= PGID=1000
ENTRYPOINT ["/gluetun-entrypoint"] ENTRYPOINT ["/gluetun-entrypoint"]
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheck HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN apk add --no-cache --update -l apk-tools && \ RUN apk add --no-cache --update -l wget && \
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.12/main" openvpn==2.4.11-r0 && \ apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.17/main" openvpn\~2.5 && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.4 && \
apk del openvpn && \
apk add --no-cache --update openvpn ca-certificates iptables ip6tables unbound tzdata && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \ mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
# Fix vulnerability issue apk del openvpn && \
apk add --no-cache --update busybox && \ apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy tzdata && \
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \ mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \
rm -rf /var/cache/apk/* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
deluser openvpn && \ deluser openvpn && \
deluser unbound && \
mkdir /gluetun mkdir /gluetun
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint

257
README.md
View File

@@ -1,127 +1,130 @@
# Gluetun VPN client # Gluetun VPN client
*Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, ExpressVPN, FastestVPN, ⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Perfect Privacy, Privado, Private Internet Access, PrivateVPN,
ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN, WeVPN and Windscribe VPN servers Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
**ANNOUNCEMENT**: Large settings refactor merged on 2022-06-01, please file issues if you find any problem!
[![Build status](https://github.com/qdm12/gluetun/actions/workflows/ci.yml/badge.svg)](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
[![Docker pulls qmcgaw/gluetun](https://img.shields.io/docker/pulls/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![Build status](https://github.com/qdm12/gluetun/actions/workflows/ci.yml/badge.svg)](https://github.com/qdm12/gluetun/actions/workflows/ci.yml) [![Docker pulls qmcgaw/private-internet-access](https://img.shields.io/docker/pulls/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![Docker pulls qmcgaw/gluetun](https://img.shields.io/docker/pulls/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun) [![Docker stars qmcgaw/gluetun](https://img.shields.io/docker/stars/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![Docker pulls qmcgaw/private-internet-access](https://img.shields.io/docker/pulls/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun) [![Docker stars qmcgaw/private-internet-access](https://img.shields.io/docker/stars/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![Docker stars qmcgaw/gluetun](https://img.shields.io/docker/stars/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun) ![Last release](https://img.shields.io/github/release/qdm12/gluetun?label=Last%20release)
[![Docker stars qmcgaw/private-internet-access](https://img.shields.io/docker/stars/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun) ![Last Docker tag](https://img.shields.io/docker/v/qmcgaw/gluetun?sort=semver&label=Last%20Docker%20tag)
[![Last release size](https://img.shields.io/docker/image-size/qmcgaw/gluetun?sort=semver&label=Last%20released%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags?page=1&ordering=last_updated)
![Last release](https://img.shields.io/github/release/qdm12/gluetun?label=Last%20release) ![GitHub last release date](https://img.shields.io/github/release-date/qdm12/gluetun?label=Last%20release%20date)
![Last Docker tag](https://img.shields.io/docker/v/qmcgaw/gluetun?sort=semver&label=Last%20Docker%20tag) ![Commits since release](https://img.shields.io/github/commits-since/qdm12/gluetun/latest?sort=semver)
[![Last release size](https://img.shields.io/docker/image-size/qmcgaw/gluetun?sort=semver&label=Last%20released%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags?page=1&ordering=last_updated)
![GitHub last release date](https://img.shields.io/github/release-date/qdm12/gluetun?label=Last%20release%20date) [![Latest size](https://img.shields.io/docker/image-size/qmcgaw/gluetun/latest?label=Latest%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags)
![Commits since release](https://img.shields.io/github/commits-since/qdm12/gluetun/latest?sort=semver)
[![GitHub last commit](https://img.shields.io/github/last-commit/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/commits/master)
[![Latest size](https://img.shields.io/docker/image-size/qmcgaw/gluetun/latest?label=Latest%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags) [![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/graphs/contributors)
[![GitHub closed PRs](https://img.shields.io/github/issues-pr-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/pulls?q=is%3Apr+is%3Aclosed)
[![GitHub last commit](https://img.shields.io/github/last-commit/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/commits/master) [![GitHub issues](https://img.shields.io/github/issues/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/graphs/contributors) [![GitHub closed issues](https://img.shields.io/github/issues-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
[![GitHub closed PRs](https://img.shields.io/github/issues-pr-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/pulls?q=is%3Apr+is%3Aclosed)
[![GitHub issues](https://img.shields.io/github/issues/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues) ![Code size](https://img.shields.io/github/languages/code-size/qdm12/gluetun)
[![GitHub closed issues](https://img.shields.io/github/issues-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed) ![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)
[![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) ![Visitors count](https://visitor-badge.laobi.icu/badge?page_id=gluetun.readme)
![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) ## Quick links
![Visitors count](https://visitor-badge.laobi.icu/badge?page_id=gluetun.readme) - [Setup](#setup)
- [Features](#features)
## Quick links - Problem?
- Check the Wiki [common errors](https://github.com/qdm12/gluetun-wiki/tree/main/errors) and [faq](https://github.com/qdm12/gluetun-wiki/tree/main/faq)
- [Setup](#Setup) - [Start a discussion](https://github.com/qdm12/gluetun/discussions)
- [Features](#Features) - [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
- Problem? - Suggestion?
- [Check the Wiki](https://github.com/qdm12/gluetun/wiki) - [Create an issue](https://github.com/qdm12/gluetun/issues)
- [Start a discussion](https://github.com/qdm12/gluetun/discussions) - Happy?
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550) - Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
- Suggestion? - Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
- [Create an issue](https://github.com/qdm12/gluetun/issues) - Drop me [an email](mailto:quentin.mcgaw@gmail.com)
- [Join the Slack channel](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk) - **Want to add a VPN provider?** check [the development page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md) and [add a provider page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/add-a-provider.md)
- Happy? - Video:
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw) [![Video Gif](https://i.imgur.com/CetWunc.gif)](https://youtu.be/0F6I03LQcI4)
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
- Video: - [Substack Console interview](https://console.substack.com/p/console-72)
[![Video Gif](https://i.imgur.com/CetWunc.gif)](https://youtu.be/0F6I03LQcI4) ## Features
- [Substack Console interview](https://console.substack.com/p/console-72) - 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**, **WeVPN**, **Windscribe** servers
## Features - Supports OpenVPN for all providers listed
- Supports Wireguard both kernelspace and userspace
- Based on Alpine 3.15 for a small Docker image of 29MB - For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
- Supports: **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers - 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)
- Supports OpenVPN for all providers listed - For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- Supports Wireguard both kernelspace and userspace - More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
- For **Mullvad**, **Ivpn** and **Windscribe** - DNS over TLS baked in with service provider(s) of your choice
- For **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider) - DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider) - Choose the vpn network protocol, `udp` or `tcp`
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134) - Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
- DNS over TLS baked in with service provider(s) of your choice - Built in Shadowsocks proxy server (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours - Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
- Choose the vpn network protocol, `udp` or `tcp` - [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices - [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP) - Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP) - Custom VPN server side port forwarding for [Perfect Privacy](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/perfect-privacy.md#vpn-server-port-forwarding), [Private Internet Access](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding), [PrivateVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/privatevpn.md#vpn-server-port-forwarding) and [ProtonVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/protonvpn.md#vpn-server-port-forwarding)
- [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun) - Possibility of split horizon DNS by selecting multiple DNS over TLS providers
- [Connect LAN devices to it](https://github.com/qdm12/gluetun/wiki/Connect-a-LAN-device-to-gluetun) - Can work as a Kubernetes sidecar container, thanks @rorph
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun/wiki/Private-internet-access#vpn-server-port-forwarding) ## Setup
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
- Unbound subprogram drops root privileges once launched 🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
- Can work as a Kubernetes sidecar container, thanks @rorph
Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
## Setup
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun-wiki/issues/new/choose)
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
Here's a docker-compose.yml for the laziest:
Go to the [Wiki](https://github.com/qdm12/gluetun/wiki)!
```yml
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun/issues/new?assignees=&labels=%F0%9F%93%84+Wiki+issue&template=wiki+issue.yml&title=Wiki+issue%3A+) ---
services:
Here's a docker-compose.yml for the laziest: gluetun:
image: qmcgaw/gluetun
```yml # container_name: gluetun
version: "3" # line above must be uncommented to allow external containers to connect.
services: # See https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md#external-container-to-gluetun
gluetun: cap_add:
image: qmcgaw/gluetun - NET_ADMIN
# container_name: gluetun devices:
# line above must be uncommented to allow external containers to connect. See https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun#external-container-to-gluetun - /dev/net/tun:/dev/net/tun
cap_add: ports:
- NET_ADMIN - 8888:8888/tcp # HTTP proxy
ports: - 8388:8388/tcp # Shadowsocks
- 8888:8888/tcp # HTTP proxy - 8388:8388/udp # Shadowsocks
- 8388:8388/tcp # Shadowsocks volumes:
- 8388:8388/udp # Shadowsocks - /yourpath:/gluetun
volumes: environment:
- /yourpath:/gluetun # See https://github.com/qdm12/gluetun-wiki/tree/main/setup#setup
environment: - VPN_SERVICE_PROVIDER=ivpn
# See https://github.com/qdm12/gluetun/wiki - VPN_TYPE=openvpn
- VPN_SERVICE_PROVIDER=ivpn # OpenVPN:
- VPN_TYPE=openvpn - OPENVPN_USER=
# OpenVPN: - OPENVPN_PASSWORD=
- OPENVPN_USER= # Wireguard:
- OPENVPN_PASSWORD= # - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
# Wireguard: # - WIREGUARD_ADDRESSES=10.64.222.21/32
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU= # Timezone for accurate log times
# - WIREGUARD_ADDRESSES=10.64.222.21/32 - TZ=
# Timezone for accurate log times # Server list updater
- TZ= # See https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
``` - UPDATER_PERIOD=
```
## License
🆕 Image also available as `ghcr.io/qdm12/gluetun`
[![MIT](https://img.shields.io/github/license/qdm12/gluetun)](https://github.com/qdm12/gluetun/master/LICENSE)
## License
[![MIT](https://img.shields.io/github/license/qdm12/gluetun)](https://github.com/qdm12/gluetun/blob/master/LICENSE)

33
ci/cmd/main.go Normal file
View File

@@ -0,0 +1,33 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"github.com/qdm12/gluetun/ci/internal"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("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)
default:
err = fmt.Errorf("unknown command: %s", os.Args[1])
}
stop()
if err != nil {
fmt.Println("❌", err)
os.Exit(1)
}
fmt.Println("✅ Test completed successfully.")
}

36
ci/go.mod Normal file
View File

@@ -0,0 +1,36 @@
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/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/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
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
)

97
ci/go.sum Normal file
View File

@@ -0,0 +1,97 @@
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/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/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/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.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=

193
ci/internal/mullvad.go Normal file
View File

@@ -0,0 +1,193 @@
package internal
import (
"bufio"
"context"
"fmt"
"io"
"os"
"regexp"
"strings"
"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 MullvadTest(ctx context.Context) error {
secrets, err := readSecrets(ctx)
if err != nil {
return fmt.Errorf("reading secrets: %w", err)
}
const timeout = 15 * 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: []string{
"VPN_SERVICE_PROVIDER=mullvad",
"VPN_TYPE=wireguard",
"LOG_LEVEL=debug",
"SERVER_COUNTRIES=USA",
"WIREGUARD_PRIVATE_KEY=" + secrets.mullvadWireguardPrivateKey,
"WIREGUARD_ADDRESSES=" + secrets.mullvadWireguardAddress,
},
}
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)
}
func ptrTo[T any](v T) *T { return &v }
type secrets struct {
mullvadWireguardPrivateKey string
mullvadWireguardAddress string
}
func readSecrets(ctx context.Context) (secrets, error) {
expectedSecrets := [...]string{
"Mullvad Wireguard private key",
"Mullvad Wireguard address",
}
scanner := bufio.NewScanner(os.Stdin)
lines := make([]string, 0, len(expectedSecrets))
for i := range expectedSecrets {
fmt.Println("🤫 reading", expectedSecrets[i], "from Stdin...")
if !scanner.Scan() {
break
}
lines = append(lines, strings.TrimSpace(scanner.Text()))
if ctx.Err() != nil {
return secrets{}, ctx.Err()
}
}
if err := scanner.Err(); err != nil {
return secrets{}, fmt.Errorf("reading secrets from stdin: %w", err)
}
if len(lines) < len(expectedSecrets) {
return secrets{}, fmt.Errorf("expected %d secrets via Stdin, but only received %d",
len(expectedSecrets), len(lines))
}
for i, line := range lines {
if line == "" {
return secrets{}, fmt.Errorf("secret on line %d/%d was empty", i+1, len(lines))
}
}
return secrets{
mullvadWireguardPrivateKey: lines[0],
mullvadWireguardAddress: lines[1],
}, nil
}
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,
) 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(linesSeen)
return fmt.Errorf("reading log stream: %w", err)
}
// The scanner is either done or cannot read because of EOF
fmt.Println("The log scanner stopped")
logSeenLines(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(lines []string) {
fmt.Println("Logs seen so far:")
for _, line := range lines {
fmt.Println(" " + line)
}
}

View File

@@ -4,8 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io/fs"
"net/http" "net/http"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"strings" "strings"
"syscall" "syscall"
@@ -13,13 +15,11 @@ import (
_ "time/tzdata" _ "time/tzdata"
_ "github.com/breml/rootcerts" _ "github.com/breml/rootcerts"
"github.com/qdm12/dns/pkg/unbound"
"github.com/qdm12/gluetun/internal/alpine" "github.com/qdm12/gluetun/internal/alpine"
"github.com/qdm12/gluetun/internal/cli" "github.com/qdm12/gluetun/internal/cli"
"github.com/qdm12/gluetun/internal/configuration/sources" "github.com/qdm12/gluetun/internal/command"
"github.com/qdm12/gluetun/internal/configuration/sources/env" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/configuration/sources/files" "github.com/qdm12/gluetun/internal/configuration/sources/files"
"github.com/qdm12/gluetun/internal/configuration/sources/mux"
"github.com/qdm12/gluetun/internal/configuration/sources/secrets" "github.com/qdm12/gluetun/internal/configuration/sources/secrets"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/dns" "github.com/qdm12/gluetun/internal/dns"
@@ -29,24 +29,28 @@ import (
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink" "github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/openvpn" "github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/portforward" "github.com/qdm12/gluetun/internal/portforward"
"github.com/qdm12/gluetun/internal/pprof" "github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip" "github.com/qdm12/gluetun/internal/publicip"
"github.com/qdm12/gluetun/internal/routing" "github.com/qdm12/gluetun/internal/routing"
"github.com/qdm12/gluetun/internal/server" "github.com/qdm12/gluetun/internal/server"
"github.com/qdm12/gluetun/internal/shadowsocks" "github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/tun" "github.com/qdm12/gluetun/internal/tun"
"github.com/qdm12/gluetun/internal/updater" updater "github.com/qdm12/gluetun/internal/updater/loop"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
"github.com/qdm12/gluetun/internal/vpn" "github.com/qdm12/gluetun/internal/vpn"
"github.com/qdm12/golibs/command" "github.com/qdm12/gosettings/reader"
"github.com/qdm12/golibs/logging" "github.com/qdm12/gosettings/reader/sources/env"
"github.com/qdm12/goshutdown" "github.com/qdm12/goshutdown"
"github.com/qdm12/goshutdown/goroutine" "github.com/qdm12/goshutdown/goroutine"
"github.com/qdm12/goshutdown/group" "github.com/qdm12/goshutdown/group"
"github.com/qdm12/goshutdown/order" "github.com/qdm12/goshutdown/order"
"github.com/qdm12/gosplash" "github.com/qdm12/gosplash"
"github.com/qdm12/updated/pkg/dnscrypto" "github.com/qdm12/log"
) )
//nolint:gochecknoglobals //nolint:gochecknoglobals
@@ -64,37 +68,45 @@ func main() {
} }
background := context.Background() background := context.Background()
signalCtx, stop := signal.NotifyContext(background, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
ctx, cancel := context.WithCancel(background) ctx, cancel := context.WithCancel(background)
logger := logging.New(logging.Settings{ logger := log.New(log.SetLevel(log.LevelInfo))
Level: logging.LevelInfo,
})
args := os.Args args := os.Args
tun := tun.New() tun := tun.New()
netLinker := netlink.New() netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
netLinker := netlink.New(netLinkDebugLogger)
cli := cli.New() cli := cli.New()
cmder := command.NewCmder() cmder := command.New()
envReader := env.New(logger) reader := reader.New(reader.Settings{
filesReader := files.New() Sources: []reader.Source{
secretsReader := secrets.New() secrets.New(logger),
muxReader := mux.New(envReader, filesReader, secretsReader) files.New(logger),
env.New(env.Settings{}),
},
HandleDeprecatedKey: func(source, deprecatedKey, currentKey string) {
logger.Warn("You are using the old " + source + " " + deprecatedKey +
", please consider changing it to " + currentKey)
},
})
errorCh := make(chan error) errorCh := make(chan error)
go func() { go func() {
errorCh <- _main(ctx, buildInfo, args, logger, muxReader, tun, netLinker, cmder, cli) errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
}() }()
// Wait for OS signal or run error
var err error
select { select {
case <-signalCtx.Done(): case receivedSignal := <-signalCh:
stop() signal.Stop(signalCh)
fmt.Println("") fmt.Println("")
logger.Warn("Caught OS signal, shutting down") logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down")
cancel() cancel()
case err := <-errorCh: case err = <-errorCh:
stop()
close(errorCh) close(errorCh)
if err == nil { // expected exit such as healthcheck if err == nil { // expected exit such as healthcheck
os.Exit(0) os.Exit(0)
@@ -103,48 +115,56 @@ func main() {
cancel() cancel()
} }
// Shutdown timed sequence, and force exit on second OS signal
const shutdownGracePeriod = 5 * time.Second const shutdownGracePeriod = 5 * time.Second
timer := time.NewTimer(shutdownGracePeriod) timer := time.NewTimer(shutdownGracePeriod)
select { select {
case <-errorCh: case shutdownErr := <-errorCh:
if !timer.Stop() { timer.Stop()
<-timer.C if shutdownErr != nil {
logger.Warnf("Shutdown failed: %s", shutdownErr)
os.Exit(1)
} }
logger.Info("Shutdown successful") logger.Info("Shutdown successful")
if err != nil {
os.Exit(1)
}
os.Exit(0)
case <-timer.C: case <-timer.C:
logger.Warn("Shutdown timed out") logger.Warn("Shutdown timed out")
os.Exit(1)
} }
os.Exit(1)
} }
var ( var errCommandUnknown = errors.New("command is unknown")
errCommandUnknown = errors.New("command is unknown")
)
//nolint:gocognit,gocyclo,maintidx //nolint:gocognit,gocyclo,maintidx
func _main(ctx context.Context, buildInfo models.BuildInformation, func _main(ctx context.Context, buildInfo models.BuildInformation,
args []string, logger logging.ParentLogger, source sources.Source, args []string, logger log.LoggerInterface, reader *reader.Reader,
tun tun.Interface, netLinker netlink.NetLinker, cmder command.RunStarter, tun Tun, netLinker netLinker, cmder RunStarter,
cli cli.CLIer) error { cli clier,
) error {
if len(args) > 1 { // cli operation if len(args) > 1 { // cli operation
switch args[1] { switch args[1] {
case "healthcheck": case "healthcheck":
return cli.HealthCheck(ctx, source, logger) return cli.HealthCheck(ctx, reader, logger)
case "clientkey": case "clientkey":
return cli.ClientKey(args[2:]) return cli.ClientKey(args[2:])
case "openvpnconfig": case "openvpnconfig":
return cli.OpenvpnConfig(logger, source) return cli.OpenvpnConfig(logger, reader, netLinker)
case "update": case "update":
return cli.Update(ctx, args[2:], logger) return cli.Update(ctx, args[2:], logger)
case "format-servers": case "format-servers":
return cli.FormatServers(args[2:]) return cli.FormatServers(args[2:])
case "genkey":
return cli.GenKey(args[2:])
default: default:
return fmt.Errorf("%w: %s", errCommandUnknown, args[1]) return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
} }
} }
announcementExp, err := time.Parse(time.RFC3339, "2021-02-15T00:00:00Z") announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
if err != nil { if err != nil {
return err return err
} }
@@ -154,8 +174,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
Emails: []string{"quentin.mcgaw@gmail.com"}, Emails: []string{"quentin.mcgaw@gmail.com"},
Version: buildInfo.Version, Version: buildInfo.Version,
Commit: buildInfo.Commit, Commit: buildInfo.Commit,
BuildDate: buildInfo.Created, Created: buildInfo.Created,
Announcement: "Large settings parsing refactoring merged on 2022-01-06, please report any issue!", Announcement: "All control server routes will become private by default after the v3.41.0 release",
AnnounceExp: announcementExp, AnnounceExp: announcementExp,
// Sponsor information // Sponsor information
PaypalUser: "qmcgaw", PaypalUser: "qmcgaw",
@@ -165,26 +185,30 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
fmt.Println(line) fmt.Println(line)
} }
allSettings, err := source.Read() var allSettings settings.Settings
err = allSettings.Read(reader, logger)
if err != nil { if err != nil {
return err return err
} }
allSettings.SetDefaults()
// Note: no need to validate minimal settings for the firewall: // Note: no need to validate minimal settings for the firewall:
// - global log level is parsed from source // - global log level is parsed below
// - firewall Debug and Enabled are booleans parsed from source // - firewall Debug and Enabled are booleans parsed from source
logLevel, err := log.ParseLevel(allSettings.Log.Level)
if err != nil {
return fmt.Errorf("log level: %w", err)
}
logger.Patch(log.SetLevel(logLevel))
netLinker.PatchLoggerLevel(logLevel)
logger.PatchLevel(*allSettings.Log.Level) routingLogger := logger.New(log.SetComponent("routing"))
routingLogger := logger.NewChild(logging.Settings{
Prefix: "routing: ",
})
if *allSettings.Firewall.Debug { // To remove in v4 if *allSettings.Firewall.Debug { // To remove in v4
routingLogger.PatchLevel(logging.LevelDebug) routingLogger.Patch(log.SetLevel(log.LevelDebug))
} }
routingConf := routing.New(netLinker, routingLogger) routingConf := routing.New(netLinker, routingLogger)
defaultInterface, defaultGateway, err := routingConf.DefaultRoute() defaultRoutes, err := routingConf.DefaultRoutes()
if err != nil { if err != nil {
return err return err
} }
@@ -194,19 +218,16 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err return err
} }
defaultIP, err := routingConf.DefaultIP() firewallLogger := logger.New(log.SetComponent("firewall"))
if *allSettings.Firewall.Debug { // To remove in v4
firewallLogger.Patch(log.SetLevel(log.LevelDebug))
}
firewallConf, err := firewall.NewConfig(ctx, firewallLogger, cmder,
defaultRoutes, localNetworks)
if err != nil { if err != nil {
return err return err
} }
firewallLogger := logger.NewChild(logging.Settings{
Prefix: "firewall: ",
})
if *allSettings.Firewall.Debug { // To remove in v4
firewallLogger.PatchLevel(logging.LevelDebug)
}
firewallConf := firewall.NewConfig(firewallLogger, cmder,
defaultInterface, defaultGateway, localNetworks, defaultIP)
if *allSettings.Firewall.Enabled { if *allSettings.Firewall.Enabled {
err = firewallConf.SetEnabled(ctx, true) err = firewallConf.SetEnabled(ctx, true)
if err != nil { if err != nil {
@@ -215,23 +236,26 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
} }
// TODO run this in a loop or in openvpn to reload from file without restarting // TODO run this in a loop or in openvpn to reload from file without restarting
storageLogger := logger.NewChild(logging.Settings{Prefix: "storage: "}) storageLogger := logger.New(log.SetComponent("storage"))
storage, err := storage.New(storageLogger, constants.ServersData) storage, err := storage.New(storageLogger, *allSettings.Storage.Filepath)
if err != nil { if err != nil {
return err return err
} }
allServers := storage.GetServers() ipv6Supported, err := netLinker.IsIPv6Supported()
if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err)
}
err = allSettings.Validate(allServers) err = allSettings.Validate(storage, ipv6Supported, logger)
if err != nil { if err != nil {
return err return err
} }
allSettings.Pprof.HTTPServer.Logger = logger allSettings.Pprof.HTTPServer.Logger = logger.New(log.SetComponent("pprof"))
pprofServer, err := pprof.New(allSettings.Pprof) pprofServer, err := pprof.New(allSettings.Pprof)
if err != nil { if err != nil {
return fmt.Errorf("cannot create Pprof server: %w", err) return fmt.Errorf("creating Pprof server: %w", err)
} }
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID) puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
@@ -241,21 +265,14 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
// Create configurators // Create configurators
alpineConf := alpine.New() alpineConf := alpine.New()
ovpnConf := openvpn.New( ovpnConf := openvpn.New(
logger.NewChild(logging.Settings{Prefix: "openvpn configurator: "}), logger.New(log.SetComponent("openvpn configurator")),
cmder, puid, pgid) cmder, puid, pgid)
dnsCrypto := dnscrypto.New(httpClient, "", "")
const cacertsPath = "/etc/ssl/certs/ca-certificates.crt"
dnsConf := unbound.NewConfigurator(nil, cmder, dnsCrypto,
"/etc/unbound", "/usr/sbin/unbound", cacertsPath)
err = printVersions(ctx, logger, []printVersionElement{ err = printVersions(ctx, logger, []printVersionElement{
{name: "Alpine", getVersion: alpineConf.Version}, {name: "Alpine", getVersion: alpineConf.Version},
{name: "OpenVPN 2.4", getVersion: ovpnConf.Version24},
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25}, {name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
{name: "Unbound", getVersion: dnsConf.Version}, {name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
{name: "IPtables", getVersion: func(ctx context.Context) (version string, err error) { {name: "IPtables", getVersion: firewallConf.Version},
return firewall.Version(ctx, cmder)
}},
}) })
if err != nil { if err != nil {
return err return err
@@ -263,40 +280,40 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
logger.Info(allSettings.String()) logger.Info(allSettings.String())
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil { for _, warning := range allSettings.Warnings() {
logger.Warn(warning)
}
const permission = fs.FileMode(0o644)
err = os.MkdirAll("/tmp/gluetun", permission)
if err != nil {
return err return err
} }
if err := os.MkdirAll("/gluetun", 0644); err != nil { err = os.MkdirAll("/gluetun", permission)
if err != nil {
return err return err
} }
const defaultUsername = "nonrootuser" const defaultUsername = "nonrootuser"
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid) nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
if err != nil { if err != nil {
return fmt.Errorf("cannot create user: %w", err) return fmt.Errorf("creating user: %w", err)
} }
if nonRootUsername != defaultUsername { if nonRootUsername != defaultUsername {
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid)) logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid))
} }
// set it for Unbound
// TODO remove this when migrating to qdm12/dns v2
allSettings.DNS.DoT.Unbound.Username = nonRootUsername
allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername
if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
return err
}
if err := routingConf.Setup(); err != nil { if err := routingConf.Setup(); err != nil {
if strings.Contains(err.Error(), "operation not permitted") { if strings.Contains(err.Error(), "operation not permitted") {
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?") logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
} }
return fmt.Errorf("cannot setup routing: %w", err) return fmt.Errorf("setting up routing: %w", err)
} }
defer func() { defer func() {
logger.Info("routing cleanup...") routingLogger.Info("routing cleanup...")
if err := routingConf.TearDown(); err != nil { if err := routingConf.TearDown(); err != nil {
logger.Error("cannot teardown routing: " + err.Error()) routingLogger.Error("cannot teardown routing: " + err.Error())
} }
}() }()
@@ -307,19 +324,30 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err return err
} }
err = routingConf.AddLocalRules(localNetworks)
if err != nil {
return fmt.Errorf("adding local rules: %w", err)
}
const tunDevice = "/dev/net/tun" const tunDevice = "/dev/net/tun"
if err := tun.Check(tunDevice); err != nil { err = tun.Check(tunDevice)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err)
}
logger.Info(err.Error() + "; creating it...") logger.Info(err.Error() + "; creating it...")
err = tun.Create(tunDevice) err = tun.Create(tunDevice)
if err != nil { if err != nil {
return err return fmt.Errorf("creating tun device: %w", err)
} }
} }
for _, port := range allSettings.Firewall.InputPorts { for _, port := range allSettings.Firewall.InputPorts {
err = firewallConf.SetAllowedPort(ctx, port, defaultInterface) for _, defaultRoute := range defaultRoutes {
if err != nil { err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
return err if err != nil {
return err
}
} }
} // TODO move inside firewall? } // TODO move inside firewall?
@@ -334,64 +362,85 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
} }
defaultGroupOptions := []group.Option{ defaultGroupOptions := []group.Option{
group.OptionTimeout(defaultShutdownTimeout), group.OptionTimeout(defaultShutdownTimeout),
group.OptionOnSuccess(defaultShutdownOnSuccess)} group.OptionOnSuccess(defaultShutdownOnSuccess),
}
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...) controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...) tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...) otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
pprofReady := make(chan struct{}) if *allSettings.Pprof.Enabled {
pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server") // TODO run in run loop so this can be patched at runtime
go pprofServer.Run(pprofCtx, pprofReady, pprofDone) pprofReady := make(chan struct{})
otherGroupHandler.Add(pprofHandler) pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server")
<-pprofReady go pprofServer.Run(pprofCtx, pprofReady, pprofDone)
otherGroupHandler.Add(pprofHandler)
<-pprofReady
}
portForwardLogger := logger.NewChild(logging.Settings{Prefix: "port forwarding: "}) portForwardLogger := logger.New(log.SetComponent("port forwarding"))
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding, portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
httpClient, firewallConf, portForwardLogger) routingConf, httpClient, firewallConf, portForwardLogger, cmder, puid, pgid)
portForwardHandler, portForwardCtx, portForwardDone := goshutdown.NewGoRoutineHandler( portForwardRunError, err := portForwardLooper.Start(ctx)
"port forwarding", goroutine.OptionTimeout(time.Second)) if err != nil {
go portForwardLooper.Run(portForwardCtx, portForwardDone) return fmt.Errorf("starting port forwarding loop: %w", err)
}
dnsLogger := logger.New(log.SetComponent("dns"))
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
dnsLogger)
if err != nil {
return fmt.Errorf("creating DNS loop: %w", err)
}
unboundLogger := logger.NewChild(logging.Settings{Prefix: "dns over tls: "})
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
unboundLogger)
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler( dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
"unbound", goroutine.OptionTimeout(defaultShutdownTimeout)) "dns", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker // wait for dnsLooper.Restart or its ticker launched with RunRestartTicker
go unboundLooper.Run(dnsCtx, dnsDone) go dnsLooper.Run(dnsCtx, dnsDone)
otherGroupHandler.Add(dnsHandler) otherGroupHandler.Add(dnsHandler)
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler( dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout)) "dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone) go dnsLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
controlGroupHandler.Add(dnsTickerHandler) controlGroupHandler.Add(dnsTickerHandler)
publicIPLooper := publicip.NewLoop(httpClient, publicIPLooper, err := publicip.NewLoop(allSettings.PublicIP, puid, pgid, httpClient,
logger.NewChild(logging.Settings{Prefix: "ip getter: "}), logger.New(log.SetComponent("ip getter")))
allSettings.PublicIP, puid, pgid) if err != nil {
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler( return fmt.Errorf("creating public ip loop: %w", err)
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout)) }
go publicIPLooper.Run(pubIPCtx, pubIPDone) publicIPRunError, err := publicIPLooper.Start(ctx)
otherGroupHandler.Add(pubIPHandler) if err != nil {
return fmt.Errorf("starting public ip loop: %w", err)
}
pubIPTickerHandler, pubIPTickerCtx, pubIPTickerDone := goshutdown.NewGoRoutineHandler( healthLogger := logger.New(log.SetComponent("healthcheck"))
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout)) healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger)
go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone) healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
tickersGroupHandler.Add(pubIPTickerHandler) "HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
go healthcheckServer.Run(healthServerCtx, healthServerDone)
healthChecker := healthcheck.NewChecker(healthLogger)
vpnLogger := logger.NewChild(logging.Settings{Prefix: "vpn: "}) updaterLogger := logger.New(log.SetComponent("updater"))
vpnLooper := vpn.NewLoop(allSettings.VPN, allSettings.Firewall.VPNInputPorts,
allServers, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper, unzipper := unzip.New(httpClient)
cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient, parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, updaterLogger,
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, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
buildInfo, *allSettings.Version.Enabled) buildInfo, *allSettings.Version.Enabled)
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler( vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
"vpn", goroutine.OptionTimeout(time.Second)) "vpn", goroutine.OptionTimeout(time.Second))
go vpnLooper.Run(vpnCtx, vpnDone) go vpnLooper.Run(vpnCtx, vpnDone)
updaterLooper := updater.NewLooper(allSettings.Updater, updaterLooper := updater.NewLoop(allSettings.Updater,
allServers, storage, vpnLooper.SetServers, httpClient, providers, storage, httpClient, updaterLogger)
logger.NewChild(logging.Settings{Prefix: "updater: "}))
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler( updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
"updater", goroutine.OptionTimeout(defaultShutdownTimeout)) "updater", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker // wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
@@ -404,48 +453,64 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
controlGroupHandler.Add(updaterTickerHandler) controlGroupHandler.Add(updaterTickerHandler)
httpProxyLooper := httpproxy.NewLoop( httpProxyLooper := httpproxy.NewLoop(
logger.NewChild(logging.Settings{Prefix: "http proxy: "}), logger.New(log.SetComponent("http proxy")),
allSettings.HTTPProxy) allSettings.HTTPProxy)
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler( httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
"http proxy", goroutine.OptionTimeout(defaultShutdownTimeout)) "http proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone) go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
otherGroupHandler.Add(httpProxyHandler) otherGroupHandler.Add(httpProxyHandler)
shadowsocksLooper := shadowsocks.NewLooper(allSettings.Shadowsocks, shadowsocksLooper := shadowsocks.NewLoop(allSettings.Shadowsocks,
logger.NewChild(logging.Settings{Prefix: "shadowsocks: "})) logger.New(log.SetComponent("shadowsocks")))
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler( shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
"shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout)) "shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone) go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
otherGroupHandler.Add(shadowsocksHandler) otherGroupHandler.Add(shadowsocksHandler)
controlServerAddress := *allSettings.ControlServer.Address
controlServerLogging := *allSettings.ControlServer.Log
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler( httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
"http server", goroutine.OptionTimeout(defaultShutdownTimeout)) "http server", goroutine.OptionTimeout(defaultShutdownTimeout))
httpServer := server.New(httpServerCtx, controlServerAddress, controlServerLogging, httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
logger.NewChild(logging.Settings{Prefix: "http server: "}), logger.New(log.SetComponent("http server")),
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper) buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
go httpServer.Run(httpServerCtx, httpServerDone) storage, ipv6Supported)
if err != nil {
return fmt.Errorf("setting up control server: %w", err)
}
httpServerReady := make(chan struct{})
go httpServer.Run(httpServerCtx, httpServerReady, httpServerDone)
<-httpServerReady
controlGroupHandler.Add(httpServerHandler) controlGroupHandler.Add(httpServerHandler)
healthLogger := logger.NewChild(logging.Settings{Prefix: "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", orderHandler := goshutdown.NewOrderHandler("gluetun",
order.OptionTimeout(totalShutdownTimeout), order.OptionTimeout(totalShutdownTimeout),
order.OptionOnSuccess(defaultShutdownOnSuccess), order.OptionOnSuccess(defaultShutdownOnSuccess),
order.OptionOnFailure(defaultShutdownOnFailure)) order.OptionOnFailure(defaultShutdownOnFailure))
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler, orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
vpnHandler, portForwardHandler, otherGroupHandler) vpnHandler, otherGroupHandler)
// Start VPN for the first time in a blocking call // Start VPN for the first time in a blocking call
// until the VPN is launched // until the VPN is launched
_, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable _, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable
<-ctx.Done() select {
case <-ctx.Done():
stoppers := []interface {
String() string
Stop() error
}{
portForwardLooper, publicIPLooper,
}
for _, stopper := range stoppers {
err := stopper.Stop()
if err != nil {
logger.Error(fmt.Sprintf("stopping %s: %s", stopper, err))
}
}
case err := <-portForwardRunError:
logger.Errorf("port forwarding loop crashed: %s", err)
case err := <-publicIPRunError:
logger.Errorf("public IP loop crashed: %s", err)
}
return orderHandler.Shutdown(context.Background()) return orderHandler.Shutdown(context.Background())
} }
@@ -460,7 +525,8 @@ type infoer interface {
} }
func printVersions(ctx context.Context, logger infoer, func printVersions(ctx context.Context, logger infoer,
elements []printVersionElement) (err error) { elements []printVersionElement,
) (err error) {
const timeout = 5 * time.Second const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout) ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel() defer cancel()
@@ -468,10 +534,69 @@ func printVersions(ctx context.Context, logger infoer,
for _, element := range elements { for _, element := range elements {
version, err := element.getVersion(ctx) version, err := element.getVersion(ctx)
if err != nil { if err != nil {
return err return fmt.Errorf("getting %s version: %w", element.name, err)
} }
logger.Info(element.name + " version: " + version) logger.Info(element.name + " version: " + version)
} }
return nil return nil
} }
type netLinker interface {
Addresser
Router
Ruler
Linker
IsWireguardSupported() (ok bool, err error)
IsIPv6Supported() (ok bool, err error)
PatchLoggerLevel(level log.Level)
}
type Addresser interface {
AddrList(link netlink.Link, family int) (
addresses []netlink.Addr, err error)
AddrReplace(link netlink.Link, addr netlink.Addr) error
}
type Router interface {
RouteList(family int) (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)
RuleAdd(rule netlink.Rule) error
RuleDel(rule netlink.Rule) error
}
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)
}
type clier interface {
ClientKey(args []string) error
FormatServers(args []string) error
OpenvpnConfig(logger cli.OpenvpnConfigLogger, reader *reader.Reader, ipv6Checker cli.IPv6Checker) error
HealthCheck(ctx context.Context, reader *reader.Reader, warner cli.Warner) error
Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error
GenKey(args []string) error
}
type Tun interface {
Check(tunDevice string) error
Create(tunDevice string) error
}
type RunStarter interface {
Run(cmd *exec.Cmd) (output string, err error)
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, err error)
}

89
go.mod
View File

@@ -1,46 +1,69 @@
module github.com/qdm12/gluetun module github.com/qdm12/gluetun
go 1.17 go 1.25.0
require ( require (
github.com/breml/rootcerts v0.2.2 github.com/ProtonMail/go-srp v0.0.7
github.com/fatih/color v1.13.0 github.com/breml/rootcerts v0.3.3
github.com/go-ping/ping v0.0.0-20210911151512-381826476871 github.com/fatih/color v1.18.0
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/qdm12/dns v1.11.0 github.com/klauspost/compress v1.18.1
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 github.com/klauspost/pgzip v1.2.6
github.com/pelletier/go-toml/v2 v2.2.4
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251114155417-248acd28339f
github.com/qdm12/gosettings v0.4.4
github.com/qdm12/goshutdown v0.3.0 github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.1.0 github.com/qdm12/gosplash v0.2.0
github.com/qdm12/gotree v0.2.0 github.com/qdm12/gotree v0.3.0
github.com/qdm12/govalid v0.1.0 github.com/qdm12/log v0.1.0
github.com/qdm12/ss-server v0.4.0 github.com/qdm12/ss-server v0.6.0
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e github.com/stretchr/testify v1.11.1
github.com/stretchr/testify v1.7.0 github.com/ulikunitz/xz v0.5.15
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 github.com/vishvananda/netlink v1.3.1
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c golang.org/x/net v0.47.0
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722 golang.org/x/sys v0.38.0
golang.org/x/text v0.31.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 ( 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/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.5.5 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.2.0 // indirect github.com/josharian/native v1.1.0 // indirect
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/genetlink v1.0.0 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/netlink v1.4.0 // indirect github.com/mdlayher/socket v0.4.1 // indirect
github.com/miekg/dns v1.1.40 // indirect github.com/miekg/dns v1.1.62 // indirect
github.com/mr-tron/base58 v1.2.0 // 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/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.1-0.20251104135713-6bee97bd4978 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect github.com/vishvananda/netns v0.0.5 // indirect
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect golang.org/x/crypto v0.44.0 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect golang.org/x/mod v0.29.0 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d // indirect golang.org/x/time v0.3.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/tools v0.38.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // 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
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 // indirect
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect
) )

373
go.sum
View File

@@ -1,257 +1,192 @@
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/breml/rootcerts v0.2.2 h1:hkHEpbTdYaNvDoYeq+mwRvCeg/YTTl23DjQ1Tnj71Zs= github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/breml/rootcerts v0.2.2/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88= github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
github.com/go-ping/ping v0.0.0-20210911151512-381826476871 h1:wtjTfjwAR/BYYMJ+QOLI/3J/qGEI0fgrkZvgsEWK2/Q=
github.com/go-ping/ping v0.0.0-20210911151512-381826476871/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 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/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
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/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
github.com/mdlayher/netlink v1.4.0 h1:n3ARR+Fm0dDv37dj5wSWZXDKcy+U0zwcXS3zKMnSiT0=
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= 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/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
github.com/pkg/errors v0.8.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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qdm12/dns v1.11.0 h1:jpcD5DZXXQSQe5a263PL09ghukiIdptvXFOZvyKEm6Q= github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/qdm12/dns v1.11.0/go.mod h1:FmQsNOUcrrZ4UFzWAiED56AKXeNgaX3ySbmPwEfNjjE= github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8XB7ADIoLJWp9ITRgsz3LroEI2FiOXLRg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg= github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg= 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-rc9.0.20251114155417-248acd28339f h1:6wN5D9wACfmXDsQ366egVt0jXY4nqL/QnIwg4nWhXco=
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251114155417-248acd28339f/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 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM= github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g= github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
github.com/qdm12/gosplash v0.1.0/go.mod h1:+A3fWW4/rUeDXhY3ieBzwghKdnIPFJgD8K3qQkenJlw= github.com/qdm12/gosplash v0.2.0/go.mod h1:k+1PzhO0th9cpX4q2Nneu4xTsndXqrM/x7NTIYmJ4jo=
github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c= github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI=
github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4= github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw=
github.com/qdm12/govalid v0.1.0 h1:UIFVmuaAg0Q+h0GeyfcFEZ5sQ5KJPvRQwycC1/cqDN8= github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
github.com/qdm12/govalid v0.1.0/go.mod h1:CyS/OEQdOvunBgrtIsW93fjd4jBkwZPBjGSpxq3NwA4= github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
github.com/qdm12/ss-server v0.4.0 h1:lMMYfDGc9P86Lyvd3+p8lK4hhgHUKDzjZC91FqJYkDU= github.com/qdm12/ss-server v0.6.0 h1:OaOdCIBXx0z3DGHPT6Th0v88vGa3MtAS4oRgUsDHGZE=
github.com/qdm12/ss-server v0.4.0/go.mod h1:AY0p4huvPUPW+/CiWsJcDgT6sneDryk26VXSccPNCxY= github.com/qdm12/ss-server v0.6.0/go.mod h1:0BO/zEmtTiLDlmQEcjtoHTC+w+cWxwItjBuGP6TWM78=
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e h1:4q+uFLawkaQRq3yARYLsjJPZd2wYwxn4g6G/5v0xW1g=
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e/go.mod h1:UvJRGkZ9XL3/D7e7JiTTVLm1F3Cymd3/gFpD6frEpBo=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 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/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 h1:b/k/BVWzWRS5v6AB0gf2ckFSbFsHN5jR0HoNso1pN+w= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.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-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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19 h1:ab2jcw2W91Rz07eHAb8Lic7sFQKO0NhBftjv6m/gL/0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls= kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722 h1:Qws2rZnQudC58cIagVucPQDLmMi3kAXgxscsgD0v6DU= kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=

View File

@@ -1,17 +1,9 @@
// Package alpine defines a configurator to interact with the Alpine operating system.
package alpine package alpine
import ( import (
"os/user" "os/user"
) )
var _ Alpiner = (*Alpine)(nil)
type Alpiner interface {
UserCreater
VersionGetter
}
type Alpine struct { type Alpine struct {
alpineReleasePath string alpineReleasePath string
passwdPath string passwdPath string

View File

@@ -3,18 +3,13 @@ package alpine
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/fs"
"os" "os"
"os/user" "os/user"
"strconv" "strconv"
) )
var ( var ErrUserAlreadyExists = errors.New("user already exists")
ErrUserAlreadyExists = errors.New("user already exists")
)
type UserCreater interface {
CreateUser(username string, uid int) (createdUsername string, err error)
}
// CreateUser creates a user in Alpine with the given UID. // CreateUser creates a user in Alpine with the given UID.
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) { func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
@@ -43,7 +38,8 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
ErrUserAlreadyExists, username, u.Uid, uid) ErrUserAlreadyExists, username, u.Uid, uid)
} }
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, 0644) const permission = fs.FileMode(0o644)
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, permission)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -7,11 +7,7 @@ import (
"strings" "strings"
) )
type VersionGetter interface { func (a *Alpine) Version(context.Context) (version string, err error) {
Version(ctx context.Context) (version string, err error)
}
func (a *Alpine) Version(ctx context.Context) (version string, err error) {
file, err := os.OpenFile(a.alpineReleasePath, os.O_RDONLY, 0) file, err := os.OpenFile(a.alpineReleasePath, os.O_RDONLY, 0)
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -2,6 +2,6 @@ package cli
import "context" import "context"
func (c *CLI) CI(context context.Context) error { func (c *CLI) CI(context.Context) error {
return nil return nil
} }

View File

@@ -1,16 +1,5 @@
// Package cli defines an interface CLI to run command line operations.
package cli package cli
var _ CLIer = (*CLI)(nil)
type CLIer interface {
ClientKeyFormatter
HealthChecker
OpenvpnConfigMaker
Updater
ServersFormatter
}
type CLI struct { type CLI struct {
repoServersPath string repoServersPath string
} }

View File

@@ -6,17 +6,12 @@ import (
"io" "io"
"os" "os"
"strings" "strings"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
) )
type ClientKeyFormatter interface {
ClientKey(args []string) error
}
func (c *CLI) ClientKey(args []string) error { func (c *CLI) ClientKey(args []string) error {
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError) flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
filepath := flagSet.String("path", files.OpenVPNClientKeyPath, "file path to the client.key file") const openVPNClientKeyPath = "/gluetun/client.key" // TODO deduplicate?
filepath := flagSet.String("path", openVPNClientKeyPath, "file path to the client.key file")
if err := flagSet.Parse(args); err != nil { if err := flagSet.Parse(args); err != nil {
return err return err
} }
@@ -32,9 +27,6 @@ func (c *CLI) ClientKey(args []string) error {
if err := file.Close(); err != nil { if err := file.Close(); err != nil {
return err return err
} }
if err != nil {
return err
}
s := string(data) s := string(data)
s = strings.ReplaceAll(s, "\n", "") s = strings.ReplaceAll(s, "\n", "")
s = strings.ReplaceAll(s, "\r", "") s = strings.ReplaceAll(s, "\r", "")

View File

@@ -4,126 +4,109 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/storage"
"golang.org/x/text/cases"
"golang.org/x/text/language"
) )
type ServersFormatter interface {
FormatServers(args []string) error
}
var ( var (
ErrFormatNotRecognized = errors.New("format is not recognized") ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
ErrProviderUnspecified = errors.New("VPN provider to format was not specified") ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
) )
func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
provider string, titleCaser cases.Caser,
) {
boolPtr, ok := providerToFormat[provider]
if !ok {
panic(fmt.Sprintf("unknown provider in format map: %s", provider))
}
flagSet.BoolVar(boolPtr, provider, false, "Format "+titleCaser.String(provider)+" servers")
}
func (c *CLI) FormatServers(args []string) error { func (c *CLI) FormatServers(args []string) error {
var format, output string var format, output string
var cyberghost, expressvpn, fastestvpn, hideMyAss, ipvanish, ivpn, mullvad, allProviders := providers.All()
nordvpn, perfectPrivacy, pia, privado, privatevpn, protonvpn, purevpn, surfshark, allProviderFlags := make([]string, len(allProviders))
torguard, vpnUnlimited, vyprvpn, wevpn, windscribe bool for i, provider := range allProviders {
flagSet := flag.NewFlagSet("markdown", flag.ExitOnError) allProviderFlags[i] = strings.ReplaceAll(provider, " ", "-")
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'") }
providersToFormat := make(map[string]*bool, len(allProviders))
for _, provider := range allProviderFlags {
providersToFormat[provider] = new(bool)
}
flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError)
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown' or 'json'")
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to") flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
flagSet.BoolVar(&cyberghost, "cyberghost", false, "Format Cyberghost servers") titleCaser := cases.Title(language.English)
flagSet.BoolVar(&expressvpn, "expressvpn", false, "Format ExpressVPN servers") for _, provider := range allProviderFlags {
flagSet.BoolVar(&fastestvpn, "fastestvpn", false, "Format FastestVPN servers") addProviderFlag(flagSet, providersToFormat, provider, titleCaser)
flagSet.BoolVar(&hideMyAss, "hidemyass", false, "Format HideMyAss servers") }
flagSet.BoolVar(&ipvanish, "ipvanish", false, "Format IpVanish servers")
flagSet.BoolVar(&ivpn, "ivpn", false, "Format IVPN servers")
flagSet.BoolVar(&mullvad, "mullvad", false, "Format Mullvad servers")
flagSet.BoolVar(&nordvpn, "nordvpn", false, "Format Nordvpn servers")
flagSet.BoolVar(&perfectPrivacy, "perfectprivacy", false, "Format Perfect Privacy servers")
flagSet.BoolVar(&pia, "pia", false, "Format Private Internet Access servers")
flagSet.BoolVar(&privado, "privado", false, "Format Privado servers")
flagSet.BoolVar(&privatevpn, "privatevpn", false, "Format Private VPN servers")
flagSet.BoolVar(&protonvpn, "protonvpn", false, "Format Protonvpn servers")
flagSet.BoolVar(&purevpn, "purevpn", false, "Format Purevpn servers")
flagSet.BoolVar(&surfshark, "surfshark", false, "Format Surfshark servers")
flagSet.BoolVar(&torguard, "torguard", false, "Format Torguard servers")
flagSet.BoolVar(&vpnUnlimited, "vpnunlimited", false, "Format VPN Unlimited servers")
flagSet.BoolVar(&vyprvpn, "vyprvpn", false, "Format Vyprvpn servers")
flagSet.BoolVar(&wevpn, "wevpn", false, "Format WeVPN servers")
flagSet.BoolVar(&windscribe, "windscribe", false, "Format Windscribe servers")
if err := flagSet.Parse(args); err != nil { if err := flagSet.Parse(args); err != nil {
return err return err
} }
if format != "markdown" { // Note the format is validated by storage.Format
return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format)
// Verify only one provider is set to be formatted.
var providers []string
for provider, formatPtr := range providersToFormat {
if *formatPtr {
providers = append(providers, provider)
}
}
switch len(providers) {
case 0:
return fmt.Errorf("%w", ErrProviderUnspecified)
case 1:
default:
return fmt.Errorf("%w: %d specified: %s",
ErrMultipleProvidersToFormat, len(providers),
strings.Join(providers, ", "))
}
var providerToFormat string
for _, providerToFormat = range allProviders {
if strings.ReplaceAll(providerToFormat, " ", "-") == providers[0] {
break
}
} }
logger := newNoopLogger() logger := newNoopLogger()
storage, err := storage.New(logger, constants.ServersData) storage, err := storage.New(logger, constants.ServersData)
if err != nil { if err != nil {
return fmt.Errorf("cannot create servers storage: %w", err) return fmt.Errorf("creating servers storage: %w", err)
} }
currentServers := storage.GetServers()
var formatted string formatted, err := storage.Format(providerToFormat, format)
switch { if err != nil {
case cyberghost: return fmt.Errorf("formatting servers: %w", err)
formatted = currentServers.Cyberghost.ToMarkdown()
case expressvpn:
formatted = currentServers.Expressvpn.ToMarkdown()
case fastestvpn:
formatted = currentServers.Fastestvpn.ToMarkdown()
case hideMyAss:
formatted = currentServers.HideMyAss.ToMarkdown()
case ipvanish:
formatted = currentServers.Ipvanish.ToMarkdown()
case ivpn:
formatted = currentServers.Ivpn.ToMarkdown()
case mullvad:
formatted = currentServers.Mullvad.ToMarkdown()
case nordvpn:
formatted = currentServers.Nordvpn.ToMarkdown()
case perfectPrivacy:
formatted = currentServers.Perfectprivacy.ToMarkdown()
case pia:
formatted = currentServers.Pia.ToMarkdown()
case privado:
formatted = currentServers.Privado.ToMarkdown()
case privatevpn:
formatted = currentServers.Privatevpn.ToMarkdown()
case protonvpn:
formatted = currentServers.Protonvpn.ToMarkdown()
case purevpn:
formatted = currentServers.Purevpn.ToMarkdown()
case surfshark:
formatted = currentServers.Surfshark.ToMarkdown()
case torguard:
formatted = currentServers.Torguard.ToMarkdown()
case vpnUnlimited:
formatted = currentServers.VPNUnlimited.ToMarkdown()
case vyprvpn:
formatted = currentServers.Vyprvpn.ToMarkdown()
case wevpn:
formatted = currentServers.Wevpn.ToMarkdown()
case windscribe:
formatted = currentServers.Windscribe.ToMarkdown()
default:
return ErrProviderUnspecified
} }
output = filepath.Clean(output) output = filepath.Clean(output)
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644) const permission = fs.FileMode(0o644)
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, permission)
if err != nil { if err != nil {
return fmt.Errorf("cannot open output file: %w", err) return fmt.Errorf("opening output file: %w", err)
} }
_, err = fmt.Fprint(file, formatted) _, err = fmt.Fprint(file, formatted)
if err != nil { if err != nil {
_ = file.Close() _ = file.Close()
return fmt.Errorf("cannot write to output file: %w", err) return fmt.Errorf("writing to output file: %w", err)
} }
err = file.Close() err = file.Close()
if err != nil { if err != nil {
return fmt.Errorf("cannot close output file: %w", err) return fmt.Errorf("closing output file: %w", err)
} }
return nil return nil

66
internal/cli/genkey.go Normal file
View File

@@ -0,0 +1,66 @@
package cli
import (
"crypto/rand"
"flag"
"fmt"
)
func (c *CLI) GenKey(args []string) (err error) {
flagSet := flag.NewFlagSet("genkey", flag.ExitOnError)
err = flagSet.Parse(args)
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
const keyLength = 128 / 8
keyBytes := make([]byte, keyLength)
_, _ = rand.Read(keyBytes)
key := base58Encode(keyBytes)
fmt.Println(key)
return nil
}
func base58Encode(data []byte) string {
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
const radix = 58
zcount := 0
for zcount < len(data) && data[zcount] == 0 {
zcount++
}
// integer simplification of ceil(log(256)/log(58))
ceilLog256Div58 := (len(data)-zcount)*555/406 + 1 //nolint:mnd
size := zcount + ceilLog256Div58
output := make([]byte, size)
high := size - 1
for _, b := range data {
i := size - 1
for carry := uint32(b); i > high || carry != 0; i-- {
carry += 256 * uint32(output[i]) //nolint:mnd
output[i] = byte(carry % radix)
carry /= radix
}
high = i
}
// Determine the additional "zero-gap" in the output buffer
additionalZeroGapEnd := zcount
for additionalZeroGapEnd < size && output[additionalZeroGapEnd] == 0 {
additionalZeroGapEnd++
}
val := output[additionalZeroGapEnd-zcount:]
size = len(val)
for i := range val {
output[i] = alphabet[val[i]]
}
return string(output[:size])
}

View File

@@ -6,21 +6,21 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/qdm12/gluetun/internal/configuration/sources" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/healthcheck" "github.com/qdm12/gluetun/internal/healthcheck"
"github.com/qdm12/gosettings/reader"
) )
type HealthChecker interface { func (c *CLI) HealthCheck(ctx context.Context, reader *reader.Reader, _ Warner) (err error) {
HealthCheck(ctx context.Context, source sources.Source, warner Warner) error
}
func (c *CLI) HealthCheck(ctx context.Context, source sources.Source, warner Warner) error {
// Extract the health server port from the configuration. // Extract the health server port from the configuration.
config, err := source.ReadHealth() var config settings.Health
err = config.Read(reader)
if err != nil { if err != nil {
return err return err
} }
config.SetDefaults()
err = config.Validate() err = config.Validate()
if err != nil { if err != nil {
return err return err

View File

@@ -1,9 +1,9 @@
package sources package cli
import "github.com/qdm12/gluetun/internal/configuration/settings" import "github.com/qdm12/gluetun/internal/configuration/settings"
type Source interface { type Source interface {
Read() (settings settings.Settings, err error) Read() (settings settings.Settings, err error)
ReadHealth() (settings settings.Health, err error) ReadHealth() (health settings.Health, err error)
String() string String() string
} }

View File

@@ -1,16 +1,10 @@
package cli package cli
import "github.com/qdm12/golibs/logging"
type noopLogger struct{} type noopLogger struct{}
func newNoopLogger() *noopLogger { func newNoopLogger() *noopLogger {
return new(noopLogger) return new(noopLogger)
} }
func (l *noopLogger) Debug(s string) {} func (l *noopLogger) Info(string) {}
func (l *noopLogger) Info(s string) {} func (l *noopLogger) Warn(string) {}
func (l *noopLogger) Warn(s string) {}
func (l *noopLogger) Error(s string) {}
func (l *noopLogger) PatchLevel(level logging.Level) {}
func (l *noopLogger) PatchPrefix(prefix string) {}

View File

@@ -1,50 +1,91 @@
package cli package cli
import ( import (
"context"
"fmt" "fmt"
"net/http"
"net/netip"
"strings" "strings"
"time" "time"
"github.com/qdm12/gluetun/internal/configuration/sources" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gosettings/reader"
) )
type OpenvpnConfigMaker interface {
OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error
}
type OpenvpnConfigLogger interface { type OpenvpnConfigLogger interface {
Info(s string) Info(s string)
Warn(s string) Warn(s string)
} }
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error { type Unzipper interface {
FetchAndExtract(ctx context.Context, url string) (
contents map[string][]byte, err error)
}
type ParallelResolver interface {
Resolve(ctx context.Context, settings resolver.ParallelSettings) (
hostToIPs map[string][]netip.Addr, warnings []string, err error)
}
type IPFetcher interface {
String() string
CanFetchAnyIP() bool
FetchInfo(ctx context.Context, ip netip.Addr) (data models.PublicIP, err error)
}
type IPv6Checker interface {
IsIPv6Supported() (supported bool, err error)
}
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
ipv6Checker IPv6Checker,
) error {
storage, err := storage.New(logger, constants.ServersData) storage, err := storage.New(logger, constants.ServersData)
if err != nil { if err != nil {
return err return err
} }
allServers := storage.GetServers()
allSettings, err := source.Read() var allSettings settings.Settings
err = allSettings.Read(reader, logger)
if err != nil {
return err
}
allSettings.SetDefaults()
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err)
}
if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil {
return fmt.Errorf("validating settings: %w", err)
}
// Unused by this CLI command
unzipper := (Unzipper)(nil)
client := (*http.Client)(nil)
warner := (Warner)(nil)
parallelResolver := (ParallelResolver)(nil)
ipFetcher := (IPFetcher)(nil)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, warner, client,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
providerConf := providers.Get(allSettings.VPN.Provider.Name)
connection, err := providerConf.GetConnection(
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
if err != nil { if err != nil {
return err return err
} }
if err = allSettings.Validate(allServers); err != nil { lines := providerConf.OpenVPNConfig(connection,
return err allSettings.VPN.OpenVPN, ipv6Supported)
}
providerConf := provider.New(*allSettings.VPN.Provider.Name, allServers, time.Now)
connection, err := providerConf.GetConnection(allSettings.VPN.Provider.ServerSelection)
if err != nil {
return err
}
lines, err := providerConf.BuildConf(connection, allSettings.VPN.OpenVPN)
if err != nil {
return err
}
fmt.Println(strings.Join(lines, "\n")) fmt.Println(strings.Join(lines, "\n"))
return nil return nil

View File

@@ -2,79 +2,78 @@ package cli
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"net"
"net/http" "net/http"
"os" "slices"
"strings" "strings"
"time" "time"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip/api"
"github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater" "github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
) )
var ( var (
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified") ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
ErrDNSAddress = errors.New("DNS address is not valid")
ErrNoProviderSpecified = errors.New("no provider was 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 Updater interface {
Update(ctx context.Context, args []string, logger UpdaterLogger) error
}
type UpdaterLogger interface { type UpdaterLogger interface {
Info(s string) Info(s string)
Warn(s string) Warn(s string)
Error(s string) Error(s string)
} }
func boolPtr(b bool) *bool { return &b }
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error { func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
options := settings.Updater{CLI: boolPtr(true)} options := settings.Updater{}
var endUserMode, maintainerMode, updateAll bool var endUserMode, maintainerMode, updateAll bool
var dnsAddress, csvProviders string var csvProviders, ipToken, protonUsername, protonPassword string
flagSet := flag.NewFlagSet("update", flag.ExitOnError) flagSet := flag.NewFlagSet("update", flag.ExitOnError)
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)") flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
flagSet.BoolVar(&maintainerMode, "maintainer", false, flagSet.BoolVar(&maintainerMode, "maintainer", false,
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)") "Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
flagSet.StringVar(&dnsAddress, "dns", "8.8.8.8", "DNS resolver address to use") flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use")
const defaultMinRatio = 0.8
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
"Minimum ratio of servers to find for the update to succeed")
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers") 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(&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", "", "Username 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 { if err := flagSet.Parse(args); err != nil {
return err return err
} }
if !endUserMode && !maintainerMode { if !endUserMode && !maintainerMode {
return ErrModeUnspecified return fmt.Errorf("%w", ErrModeUnspecified)
}
options.DNSAddress = net.ParseIP(dnsAddress)
if options.DNSAddress == nil {
return fmt.Errorf("%w: %s", ErrDNSAddress, dnsAddress)
} }
if updateAll { if updateAll {
for _, provider := range constants.AllProviders() { options.Providers = providers.All()
if provider == constants.Custom {
continue
}
options.Providers = append(options.Providers, provider)
}
} else { } else {
if csvProviders == "" { if csvProviders == "" {
return ErrNoProviderSpecified return fmt.Errorf("%w", ErrNoProviderSpecified)
} }
options.Providers = strings.Split(csvProviders, ",") options.Providers = strings.Split(csvProviders, ",")
} }
if slices.Contains(options.Providers, providers.Protonvpn) {
options.ProtonUsername = &protonUsername
options.ProtonPassword = &protonPassword
}
options.SetDefaults(options.Providers[0]) options.SetDefaults(options.Providers[0])
err := options.Validate() err := options.Validate()
@@ -82,48 +81,47 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
return fmt.Errorf("options validation failed: %w", err) return fmt.Errorf("options validation failed: %w", err)
} }
serversDataPath := constants.ServersData
if maintainerMode {
serversDataPath = ""
}
storage, err := storage.New(logger, serversDataPath)
if err != nil {
return fmt.Errorf("creating servers storage: %w", err)
}
const clientTimeout = 10 * time.Second const clientTimeout = 10 * time.Second
httpClient := &http.Client{Timeout: clientTimeout} httpClient := &http.Client{Timeout: clientTimeout}
unzipper := unzip.New(httpClient)
storage, err := storage.New(logger, constants.ServersData) parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
if err != nil { nameTokenPairs := []api.NameToken{
return fmt.Errorf("cannot create servers storage: %w", err) {Name: string(api.IPInfo), Token: ipToken},
{Name: string(api.IP2Location)},
{Name: string(api.IfConfigCo)},
} }
currentServers := storage.GetServers() fetchers, err := api.New(nameTokenPairs, httpClient)
updater := updater.New(options, httpClient, currentServers, logger)
allServers, err := updater.UpdateServers(ctx)
if err != nil { if err != nil {
return fmt.Errorf("cannot update server information: %w", err) return fmt.Errorf("creating public IP fetchers: %w", err)
} }
ipFetcher := api.NewResilient(fetchers, logger)
if endUserMode { openvpnFileExtractor := extract.New()
if err := storage.FlushToFile(allServers); err != nil {
return fmt.Errorf("cannot write updated information to file: %w", err) providers := provider.NewProviders(storage, time.Now, logger, httpClient,
} unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
updater := updater.New(httpClient, storage, providers, logger)
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
if err != nil {
return fmt.Errorf("updating server information: %w", err)
} }
if maintainerMode { if maintainerMode {
if err := writeToEmbeddedJSON(c.repoServersPath, allServers); err != nil { err := storage.FlushToFile(c.repoServersPath)
return fmt.Errorf("cannot write updated information to file: %w", err) if err != nil {
return fmt.Errorf("writing servers data to embedded JSON file: %w", err)
} }
} }
return nil return nil
} }
func writeToEmbeddedJSON(repoServersPath string,
allServers models.AllServers) error {
const perms = 0600
f, err := os.OpenFile(repoServersPath,
os.O_TRUNC|os.O_WRONLY|os.O_CREATE, perms)
if err != nil {
return err
}
defer f.Close()
encoder := json.NewEncoder(f)
encoder.SetIndent("", " ")
return encoder.Encode(allServers)
}

View File

@@ -0,0 +1,8 @@
package command
// Cmder handles running subprograms synchronously and asynchronously.
type Cmder struct{}
func New() *Cmder {
return &Cmder{}
}

View File

@@ -0,0 +1,11 @@
package command
import "io"
type execCmd interface {
CombinedOutput() ([]byte, error)
StdoutPipe() (io.ReadCloser, error)
StderrPipe() (io.ReadCloser, error)
Start() error
Wait() error
}

View File

@@ -0,0 +1,3 @@
package command
//go:generate mockgen -destination=mocks_local_test.go -package=$GOPACKAGE -source=interfaces_local.go

View File

@@ -0,0 +1,108 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: interfaces_local.go
// Package command is a generated GoMock package.
package command
import (
io "io"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockexecCmd is a mock of execCmd interface.
type MockexecCmd struct {
ctrl *gomock.Controller
recorder *MockexecCmdMockRecorder
}
// MockexecCmdMockRecorder is the mock recorder for MockexecCmd.
type MockexecCmdMockRecorder struct {
mock *MockexecCmd
}
// NewMockexecCmd creates a new mock instance.
func NewMockexecCmd(ctrl *gomock.Controller) *MockexecCmd {
mock := &MockexecCmd{ctrl: ctrl}
mock.recorder = &MockexecCmdMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockexecCmd) EXPECT() *MockexecCmdMockRecorder {
return m.recorder
}
// CombinedOutput mocks base method.
func (m *MockexecCmd) CombinedOutput() ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CombinedOutput")
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CombinedOutput indicates an expected call of CombinedOutput.
func (mr *MockexecCmdMockRecorder) CombinedOutput() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CombinedOutput", reflect.TypeOf((*MockexecCmd)(nil).CombinedOutput))
}
// Start mocks base method.
func (m *MockexecCmd) Start() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Start")
ret0, _ := ret[0].(error)
return ret0
}
// Start indicates an expected call of Start.
func (mr *MockexecCmdMockRecorder) Start() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockexecCmd)(nil).Start))
}
// StderrPipe mocks base method.
func (m *MockexecCmd) StderrPipe() (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StderrPipe")
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StderrPipe indicates an expected call of StderrPipe.
func (mr *MockexecCmdMockRecorder) StderrPipe() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StderrPipe", reflect.TypeOf((*MockexecCmd)(nil).StderrPipe))
}
// StdoutPipe mocks base method.
func (m *MockexecCmd) StdoutPipe() (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StdoutPipe")
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StdoutPipe indicates an expected call of StdoutPipe.
func (mr *MockexecCmdMockRecorder) StdoutPipe() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StdoutPipe", reflect.TypeOf((*MockexecCmd)(nil).StdoutPipe))
}
// Wait mocks base method.
func (m *MockexecCmd) Wait() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Wait")
ret0, _ := ret[0].(error)
return ret0
}
// Wait indicates an expected call of Wait.
func (mr *MockexecCmdMockRecorder) Wait() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockexecCmd)(nil).Wait))
}

30
internal/command/run.go Normal file
View File

@@ -0,0 +1,30 @@
package command
import (
"os/exec"
"strings"
)
// Run runs a command in a blocking manner, returning its output and
// an error if it failed.
func (c *Cmder) Run(cmd *exec.Cmd) (output string, err error) {
return run(cmd)
}
func run(cmd execCmd) (output string, err error) {
stdout, err := cmd.CombinedOutput()
output = string(stdout)
output = strings.TrimSuffix(output, "\n")
lines := stringToLines(output)
for i := range lines {
lines[i] = strings.TrimPrefix(lines[i], "'")
lines[i] = strings.TrimSuffix(lines[i], "'")
}
output = strings.Join(lines, "\n")
return output, err
}
func stringToLines(s string) (lines []string) {
s = strings.TrimSuffix(s, "\n")
return strings.Split(s, "\n")
}

View File

@@ -0,0 +1,54 @@
package command
import (
"errors"
"testing"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_run(t *testing.T) {
t.Parallel()
errDummy := errors.New("dummy")
testCases := map[string]struct {
stdout []byte
cmdErr error
output string
err error
}{
"no output": {},
"cmd error": {
stdout: []byte("'hello \nworld'\n"),
cmdErr: errDummy,
output: "hello \nworld",
err: errDummy,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
mockCmd := NewMockexecCmd(ctrl)
mockCmd.EXPECT().CombinedOutput().Return(testCase.stdout, testCase.cmdErr)
output, err := run(mockCmd)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.output, output)
})
}
}

150
internal/command/split.go Normal file
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)
}

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)
}
})
}
}

100
internal/command/start.go Normal file
View File

@@ -0,0 +1,100 @@
package command
import (
"bufio"
"errors"
"io"
"os"
"os/exec"
)
// Start launches a command and streams stdout and stderr to channels.
// All the channels returned are ready only and won't be closed
// if the command fails later.
func (c *Cmder) Start(cmd *exec.Cmd) (
stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error,
) {
return start(cmd)
}
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error,
) {
stop := make(chan struct{})
stdoutReady := make(chan struct{})
stdoutLinesCh := make(chan string)
stdoutDone := make(chan struct{})
stderrReady := make(chan struct{})
stderrLinesCh := make(chan string)
stderrDone := make(chan struct{})
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, nil, err
}
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh)
stderr, err := cmd.StderrPipe()
if err != nil {
_ = stdout.Close()
close(stop)
<-stdoutDone
return nil, nil, nil, err
}
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh)
err = cmd.Start()
if err != nil {
_ = stdout.Close()
_ = stderr.Close()
close(stop)
<-stdoutDone
<-stderrDone
return nil, nil, nil, err
}
waitErrorCh := make(chan error)
go func() {
err := cmd.Wait()
_ = stdout.Close()
_ = stderr.Close()
close(stop)
<-stdoutDone
<-stderrDone
waitErrorCh <- err
}()
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
}
func streamToChannel(ready chan<- struct{},
stop <-chan struct{}, done chan<- struct{},
stream io.Reader, lines chan<- string,
) {
defer close(done)
close(ready)
scanner := bufio.NewScanner(stream)
lineBuffer := make([]byte, bufio.MaxScanTokenSize) // 64KB
const maxCapacity = 20 * 1024 * 1024 // 20MB
scanner.Buffer(lineBuffer, maxCapacity)
for scanner.Scan() {
// scanner is closed if the context is canceled
// or if the command failed starting because the
// stream is closed (io.EOF error).
lines <- scanner.Text()
}
err := scanner.Err()
if err == nil || errors.Is(err, os.ErrClosed) {
return
}
// ignore the error if it is stopped.
select {
case <-stop:
return
default:
lines <- "stream error: " + err.Error()
}
}

View File

@@ -0,0 +1,118 @@
package command
import (
"bytes"
"errors"
"io"
"strings"
"testing"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func linesToReadCloser(lines []string) io.ReadCloser {
s := strings.Join(lines, "\n")
return io.NopCloser(bytes.NewBufferString(s))
}
func Test_start(t *testing.T) {
t.Parallel()
errDummy := errors.New("dummy")
testCases := map[string]struct {
stdout []string
stdoutPipeErr error
stderr []string
stderrPipeErr error
startErr error
waitErr error
err error
}{
"no output": {},
"success": {
stdout: []string{"hello", "world"},
stderr: []string{"some", "error"},
},
"stdout pipe error": {
stdoutPipeErr: errDummy,
err: errDummy,
},
"stderr pipe error": {
stderrPipeErr: errDummy,
err: errDummy,
},
"start error": {
startErr: errDummy,
err: errDummy,
},
"wait error": {
waitErr: errDummy,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
stdout := linesToReadCloser(testCase.stdout)
stderr := linesToReadCloser(testCase.stderr)
mockCmd := NewMockexecCmd(ctrl)
mockCmd.EXPECT().StdoutPipe().
Return(stdout, testCase.stdoutPipeErr)
if testCase.stdoutPipeErr == nil {
mockCmd.EXPECT().StderrPipe().Return(stderr, testCase.stderrPipeErr)
if testCase.stderrPipeErr == nil {
mockCmd.EXPECT().Start().Return(testCase.startErr)
if testCase.startErr == nil {
mockCmd.EXPECT().Wait().Return(testCase.waitErr)
}
}
}
stdoutLines, stderrLines, waitError, err := start(mockCmd)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
assert.Nil(t, stdoutLines)
assert.Nil(t, stderrLines)
assert.Nil(t, waitError)
return
}
require.NoError(t, err)
var stdoutIndex, stderrIndex int
done := false
for !done {
select {
case line := <-stdoutLines:
assert.Equal(t, testCase.stdout[stdoutIndex], line)
stdoutIndex++
case line := <-stderrLines:
assert.Equal(t, testCase.stderr[stderrIndex], line)
stderrIndex++
case err := <-waitError:
if testCase.waitErr != nil {
require.Error(t, err)
assert.Equal(t, testCase.waitErr.Error(), err.Error())
} else {
assert.NoError(t, err)
}
done = true
}
}
assert.Equal(t, len(testCase.stdout), stdoutIndex)
assert.Equal(t, len(testCase.stderr), stderrIndex)
})
}
}

View File

@@ -0,0 +1,31 @@
package settings
import (
"slices"
"github.com/qdm12/gosettings/reader"
"golang.org/x/exp/maps"
)
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.",
"HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete",
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
"DNS_SERVER": "DNS_SERVER is obsolete because the forwarding server is always enabled.",
"DOT": "DOT is obsolete because the forwarding server is always enabled.",
"DNS_KEEP_NAMESERVER": "DNS_KEEP_NAMESERVER is obsolete because the forwarding server is always used and " +
"forwards local names to private DNS resolvers found in /etc/resolv.conf",
}
sortedKeys := maps.Keys(keyToMessage)
slices.Sort(sortedKeys)
warnings = make([]string, 0, len(keyToMessage))
for _, key := range sortedKeys {
if r.Get(key) != nil {
warnings = append(warnings, keyToMessage[key])
}
}
return warnings
}

View File

@@ -1,37 +1,96 @@
package settings package settings
import ( import (
"errors"
"fmt" "fmt"
"net" "net/netip"
"slices"
"time"
"github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
const (
DNSUpstreamTypeDot = "dot"
DNSUpstreamTypeDoh = "doh"
DNSUpstreamTypePlain = "plain"
)
// DNS contains settings to configure DNS. // DNS contains settings to configure DNS.
type DNS struct { type DNS struct {
// ServerAddress is the DNS server to use inside // UpstreamType can be [dnsUpstreamTypeDot], [dnsUpstreamTypeDoh]
// the Go program and for the system. // or [dnsUpstreamTypePlain]. It defaults to [dnsUpstreamTypeDot].
// It defaults to '127.0.0.1' to be used with the UpstreamType string `json:"upstream_type"`
// DoT server. It cannot be nil in the internal // UpdatePeriod is the period to update DNS block lists.
// state. // It can be set to 0 to disable the update.
ServerAddress net.IP // It defaults to 24h and cannot be nil in
// KeepNameserver is true if the Docker DNS server // the internal state.
// found in /etc/resolv.conf should be kept. UpdatePeriod *time.Duration
// Note settings this to true will go around the // Providers is a list of DNS providers.
// DoT server blocking. // It defaults to either ["cloudflare"] or [] if the
// It defaults to false and cannot be nil in the // UpstreamPlainAddresses field is set.
// internal state. Providers []string `json:"providers"`
KeepNameserver *bool // Caching is true if the server should cache
// DOT contains settings to configure the DoT // DNS responses.
// server. Caching *bool `json:"caching"`
DoT DoT // 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
// UpstreamPlainAddresses are the upstream plaintext DNS resolver
// addresses to use by the built-in DNS server forwarder.
// Note, if the upstream type is [dnsUpstreamTypePlain] these are merged
// together with provider names set in the Providers field.
// If this field is set, the Providers field will default to the empty slice.
UpstreamPlainAddresses []netip.AddrPort
} }
var (
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
ErrDNSUpstreamPlainNoIPv6 = errors.New("upstream plain addresses do not contain any IPv6 address")
ErrDNSUpstreamPlainNoIPv4 = errors.New("upstream plain addresses do not contain any IPv4 address")
)
func (d DNS) validate() (err error) { func (d DNS) validate() (err error) {
err = d.DoT.validate() if !helpers.IsOneOf(d.UpstreamType, DNSUpstreamTypeDot, DNSUpstreamTypeDoh, DNSUpstreamTypePlain) {
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
}
}
if d.UpstreamType == DNSUpstreamTypePlain {
if *d.IPv6 && !slices.ContainsFunc(d.UpstreamPlainAddresses, func(addrPort netip.AddrPort) bool {
return addrPort.Addr().Is6()
}) {
return fmt.Errorf("%w: in %d addresses", ErrDNSUpstreamPlainNoIPv6, len(d.UpstreamPlainAddresses))
} else if !slices.ContainsFunc(d.UpstreamPlainAddresses, func(addrPort netip.AddrPort) bool {
return addrPort.Addr().Is4()
}) {
return fmt.Errorf("%w: in %d addresses", ErrDNSUpstreamPlainNoIPv4, len(d.UpstreamPlainAddresses))
}
}
err = d.Blacklist.validate()
if err != nil { if err != nil {
return fmt.Errorf("failed validating DoT settings: %w", err) return err
} }
return nil return nil
@@ -39,34 +98,86 @@ func (d DNS) validate() (err error) {
func (d *DNS) Copy() (copied DNS) { func (d *DNS) Copy() (copied DNS) {
return DNS{ return DNS{
ServerAddress: helpers.CopyIP(d.ServerAddress), UpstreamType: d.UpstreamType,
KeepNameserver: helpers.CopyBoolPtr(d.KeepNameserver), UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
DoT: d.DoT.copy(), Providers: gosettings.CopySlice(d.Providers),
Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.copy(),
UpstreamPlainAddresses: d.UpstreamPlainAddresses,
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (d *DNS) mergeWith(other DNS) {
d.ServerAddress = helpers.MergeWithIP(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = helpers.MergeWithBool(d.KeepNameserver, other.KeepNameserver)
d.DoT.mergeWith(other.DoT)
}
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (d *DNS) overrideWith(other DNS) { func (d *DNS) overrideWith(other DNS) {
d.ServerAddress = helpers.OverrideWithIP(d.ServerAddress, other.ServerAddress) d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
d.KeepNameserver = helpers.OverrideWithBool(d.KeepNameserver, other.KeepNameserver) d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
d.DoT.overrideWith(other.DoT) 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.UpstreamPlainAddresses = gosettings.OverrideWithSlice(d.UpstreamPlainAddresses, other.UpstreamPlainAddresses)
} }
func (d *DNS) setDefaults() { func (d *DNS) setDefaults() {
localhost := net.IPv4(127, 0, 0, 1) //nolint:gomnd d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, DNSUpstreamTypeDot)
d.ServerAddress = helpers.DefaultIP(d.ServerAddress, localhost) const defaultUpdatePeriod = 24 * time.Hour
d.KeepNameserver = helpers.DefaultBool(d.KeepNameserver, false) d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
d.DoT.setDefaults() 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.UpstreamPlainAddresses = gosettings.DefaultSlice(d.UpstreamPlainAddresses, []netip.AddrPort{})
}
func defaultDNSProviders() []string {
return []string{
provider.Cloudflare().Name,
}
}
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
if d.UpstreamType == DNSUpstreamTypePlain {
for _, addrPort := range d.UpstreamPlainAddresses {
if addrPort.Addr().Is4() {
return addrPort.Addr()
}
}
}
ipv4 = findPlainIPv4InProviders(d.Providers)
if ipv4.IsValid() {
return ipv4
}
// Either:
// - all upstream plain addresses are IPv6 and no provider is set
// - all providers set do not have a plaintext IPv4 address
ipv4 = findPlainIPv4InProviders(defaultDNSProviders())
if !ipv4.IsValid() {
panic("no plaintext IPv4 address found in default DNS providers")
}
return ipv4
}
func findPlainIPv4InProviders(providerNames []string) netip.Addr {
providers := provider.NewProviders()
for _, name := range providerNames {
provider, err := providers.Get(name)
if err != nil {
// Settings should be validated before calling this function,
// so an error happening here is a programming error.
panic(err)
}
if len(provider.Plain.IPv4) > 0 {
return provider.Plain.IPv4[0].Addr()
}
}
return netip.Addr{}
} }
func (d DNS) String() string { func (d DNS) String() string {
@@ -75,8 +186,100 @@ func (d DNS) String() string {
func (d DNS) toLinesNode() (node *gotree.Node) { func (d DNS) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS settings:") node = gotree.New("DNS settings:")
node.Appendf("DNS server address to use: %s", d.ServerAddress)
node.Appendf("Keep existing nameserver(s): %s", helpers.BoolPtrToYesNo(d.KeepNameserver)) node.Appendf("Upstream resolver type: %s", d.UpstreamType)
node.AppendNode(d.DoT.toLinesNode())
upstreamResolvers := node.Append("Upstream resolvers:")
if len(d.UpstreamPlainAddresses) > 0 {
if d.UpstreamType == DNSUpstreamTypePlain {
for _, addr := range d.UpstreamPlainAddresses {
upstreamResolvers.Append(addr.String())
}
} else {
node.Appendf("Upstream plain addresses: ignored because upstream type is not plain")
}
} else {
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 return node
} }
func (d *DNS) read(r *reader.Reader) (err error) {
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
}
err = d.readUpstreamPlainAddresses(r)
if err != nil {
return err
}
return nil
}
func (d *DNS) readUpstreamPlainAddresses(r *reader.Reader) (err error) {
// If DNS_UPSTREAM_PLAIN_ADDRESSES is set, the user must also set DNS_UPSTREAM_TYPE=plain
// for these to be used. This is an added safety measure to reduce misunderstandings, and
// reduce odd settings overrides.
d.UpstreamPlainAddresses, err = r.CSVNetipAddrPorts("DNS_UPSTREAM_PLAIN_ADDRESSES")
if err != nil {
return err
}
// Retro-compatibility - remove in v4
// If DNS_ADDRESS is set to a non-localhost address, append it to the other
// upstream plain addresses, assuming port 53, and force the upstream type to plain AND
// clear any user picked providers, to maintain retro-compatibility behavior.
serverAddress, err := r.NetipAddr("DNS_ADDRESS",
reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"),
reader.IsRetro("DNS_UPSTREAM_PLAIN_ADDRESSES"))
if err != nil {
return err
} else if !serverAddress.IsValid() {
return nil
}
isLocalhost := serverAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) == 0
if isLocalhost {
return nil
}
const defaultPlainPort = 53
addrPort := netip.AddrPortFrom(serverAddress, defaultPlainPort)
d.UpstreamPlainAddresses = append(d.UpstreamPlainAddresses, addrPort)
d.UpstreamType = DNSUpstreamTypePlain
d.Providers = []string{}
return nil
}

View File

@@ -0,0 +1,26 @@
package settings
import (
"testing"
"github.com/qdm12/dns/v2/pkg/provider"
"github.com/stretchr/testify/require"
)
func Test_defaultDNSProviders(t *testing.T) {
t.Parallel()
names := defaultDNSProviders()
found := false
providers := provider.NewProviders()
for _, name := range names {
provider, err := providers.Get(name)
require.NoError(t, err)
if len(provider.Plain.IPv4) > 0 {
found = true
break
}
}
require.True(t, found, "no default DNS provider has a plaintext IPv4 address")
}

View File

@@ -3,12 +3,14 @@ package settings
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/http"
"net/netip"
"regexp" "regexp"
"github.com/qdm12/dns/pkg/blacklist" "github.com/qdm12/dns/v2/pkg/blockbuilder"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"inet.af/netaddr"
) )
// DNSBlacklist is settings for the DNS blacklist building. // DNSBlacklist is settings for the DNS blacklist building.
@@ -18,21 +20,25 @@ type DNSBlacklist struct {
BlockSurveillance *bool BlockSurveillance *bool
AllowedHosts []string AllowedHosts []string
AddBlockedHosts []string AddBlockedHosts []string
AddBlockedIPs []netaddr.IP AddBlockedIPs []netip.Addr
AddBlockedIPPrefixes []netaddr.IPPrefix AddBlockedIPPrefixes []netip.Prefix
// RebindingProtectionExemptHostnames is a list of hostnames
// exempt from DNS rebinding protection.
RebindingProtectionExemptHostnames []string
} }
func (b *DNSBlacklist) setDefaults() { func (b *DNSBlacklist) setDefaults() {
b.BlockMalicious = helpers.DefaultBool(b.BlockMalicious, true) b.BlockMalicious = gosettings.DefaultPointer(b.BlockMalicious, true)
b.BlockAds = helpers.DefaultBool(b.BlockAds, false) b.BlockAds = gosettings.DefaultPointer(b.BlockAds, false)
b.BlockSurveillance = helpers.DefaultBool(b.BlockSurveillance, true) b.BlockSurveillance = gosettings.DefaultPointer(b.BlockSurveillance, true)
} }
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 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 ( var (
ErrAllowedHostNotValid = errors.New("allowed host is not valid") ErrAllowedHostNotValid = errors.New("allowed host is not valid")
ErrBlockedHostNotValid = errors.New("blocked 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) { func (b DNSBlacklist) validate() (err error) {
@@ -48,51 +54,53 @@ func (b DNSBlacklist) validate() (err error) {
} }
} }
for _, host := range b.RebindingProtectionExemptHostnames {
if !hostRegex.MatchString(host) {
return fmt.Errorf("%w: %s", ErrRebindingProtectionExemptHostNotValid, host)
}
}
return nil return nil
} }
func (b DNSBlacklist) copy() (copied DNSBlacklist) { func (b DNSBlacklist) copy() (copied DNSBlacklist) {
return DNSBlacklist{ return DNSBlacklist{
BlockMalicious: helpers.CopyBoolPtr(b.BlockMalicious), BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
BlockAds: helpers.CopyBoolPtr(b.BlockAds), BlockAds: gosettings.CopyPointer(b.BlockAds),
BlockSurveillance: helpers.CopyBoolPtr(b.BlockSurveillance), BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
AllowedHosts: helpers.CopyStringSlice(b.AllowedHosts), AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
AddBlockedHosts: helpers.CopyStringSlice(b.AddBlockedHosts), AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
AddBlockedIPs: helpers.CopyNetaddrIPsSlice(b.AddBlockedIPs), AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
AddBlockedIPPrefixes: helpers.CopyIPPrefixSlice(b.AddBlockedIPPrefixes), AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
RebindingProtectionExemptHostnames: gosettings.CopySlice(b.RebindingProtectionExemptHostnames),
} }
} }
func (b *DNSBlacklist) mergeWith(other DNSBlacklist) {
b.BlockMalicious = helpers.MergeWithBool(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = helpers.MergeWithBool(b.BlockAds, other.BlockAds)
b.BlockSurveillance = helpers.MergeWithBool(b.BlockSurveillance, other.BlockSurveillance)
b.AllowedHosts = helpers.MergeStringSlices(b.AllowedHosts, other.AllowedHosts)
b.AddBlockedHosts = helpers.MergeStringSlices(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = helpers.MergeNetaddrIPsSlices(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = helpers.MergeIPPrefixesSlices(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
}
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) { func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
b.BlockMalicious = helpers.OverrideWithBool(b.BlockMalicious, other.BlockMalicious) b.BlockMalicious = gosettings.OverrideWithPointer(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = helpers.OverrideWithBool(b.BlockAds, other.BlockAds) b.BlockAds = gosettings.OverrideWithPointer(b.BlockAds, other.BlockAds)
b.BlockSurveillance = helpers.OverrideWithBool(b.BlockSurveillance, other.BlockSurveillance) b.BlockSurveillance = gosettings.OverrideWithPointer(b.BlockSurveillance, other.BlockSurveillance)
b.AllowedHosts = helpers.OverrideWithStringSlice(b.AllowedHosts, other.AllowedHosts) b.AllowedHosts = gosettings.OverrideWithSlice(b.AllowedHosts, other.AllowedHosts)
b.AddBlockedHosts = helpers.OverrideWithStringSlice(b.AddBlockedHosts, other.AddBlockedHosts) b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = helpers.OverrideWithNetaddrIPsSlice(b.AddBlockedIPs, other.AddBlockedIPs) b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = helpers.OverrideWithIPPrefixesSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes) b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
b.RebindingProtectionExemptHostnames = gosettings.OverrideWithSlice(b.RebindingProtectionExemptHostnames,
other.RebindingProtectionExemptHostnames)
} }
func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) { func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
return blacklist.BuilderSettings{ settings blockbuilder.Settings,
BlockMalicious: *b.BlockMalicious, ) {
BlockAds: *b.BlockAds, return blockbuilder.Settings{
BlockSurveillance: *b.BlockSurveillance, Client: client,
BlockMalicious: b.BlockMalicious,
BlockAds: b.BlockAds,
BlockSurveillance: b.BlockSurveillance,
AllowedHosts: b.AllowedHosts, AllowedHosts: b.AllowedHosts,
AddBlockedHosts: b.AddBlockedHosts, AddBlockedHosts: b.AddBlockedHosts,
AddBlockedIPs: b.AddBlockedIPs, AddBlockedIPs: b.AddBlockedIPs,
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes, AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
}, nil }
} }
func (b DNSBlacklist) String() string { func (b DNSBlacklist) String() string {
@@ -102,37 +110,130 @@ func (b DNSBlacklist) String() string {
func (b DNSBlacklist) toLinesNode() (node *gotree.Node) { func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS filtering settings:") node = gotree.New("DNS filtering settings:")
node.Appendf("Block malicious: %s", helpers.BoolPtrToYesNo(b.BlockMalicious)) node.Appendf("Block malicious: %s", gosettings.BoolToYesNo(b.BlockMalicious))
node.Appendf("Block ads: %s", helpers.BoolPtrToYesNo(b.BlockAds)) node.Appendf("Block ads: %s", gosettings.BoolToYesNo(b.BlockAds))
node.Appendf("Block surveillance: %s", helpers.BoolPtrToYesNo(b.BlockSurveillance)) node.Appendf("Block surveillance: %s", gosettings.BoolToYesNo(b.BlockSurveillance))
if len(b.AllowedHosts) > 0 { if len(b.AllowedHosts) > 0 {
allowedHostsNode := node.Appendf("Allowed hosts:") allowedHostsNode := node.Append("Allowed hosts:")
for _, host := range b.AllowedHosts { for _, host := range b.AllowedHosts {
allowedHostsNode.Appendf(host) allowedHostsNode.Append(host)
} }
} }
if len(b.AddBlockedHosts) > 0 { if len(b.AddBlockedHosts) > 0 {
blockedHostsNode := node.Appendf("Blocked hosts:") blockedHostsNode := node.Append("Blocked hosts:")
for _, host := range b.AddBlockedHosts { for _, host := range b.AddBlockedHosts {
blockedHostsNode.Appendf(host) blockedHostsNode.Append(host)
} }
} }
if len(b.AddBlockedIPs) > 0 { if len(b.AddBlockedIPs) > 0 {
blockedIPsNode := node.Appendf("Blocked IP addresses:") blockedIPsNode := node.Append("Blocked IP addresses:")
for _, ip := range b.AddBlockedIPs { for _, ip := range b.AddBlockedIPs {
blockedIPsNode.Appendf(ip.String()) blockedIPsNode.Append(ip.String())
} }
} }
if len(b.AddBlockedIPPrefixes) > 0 { if len(b.AddBlockedIPPrefixes) > 0 {
blockedIPPrefixesNode := node.Appendf("Blocked IP networks:") blockedIPPrefixesNode := node.Append("Blocked IP networks:")
for _, ipNetwork := range b.AddBlockedIPPrefixes { for _, ipNetwork := range b.AddBlockedIPPrefixes {
blockedIPPrefixesNode.Appendf(ipNetwork.String()) blockedIPPrefixesNode.Append(ipNetwork.String())
}
}
if len(b.RebindingProtectionExemptHostnames) > 0 {
exemptHostsNode := node.Append("Rebinding protection exempt hostnames:")
for _, host := range b.RebindingProtectionExemptHostnames {
exemptHostsNode.Append(host)
} }
} }
return node return node
} }
func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
b.BlockMalicious, err = r.BoolPtr("BLOCK_MALICIOUS")
if err != nil {
return err
}
b.BlockSurveillance, err = r.BoolPtr("BLOCK_SURVEILLANCE",
reader.RetroKeys("BLOCK_NSA"))
if err != nil {
return err
}
b.BlockAds, err = r.BoolPtr("BLOCK_ADS")
if err != nil {
return err
}
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
if err != nil {
return err
}
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
b.RebindingProtectionExemptHostnames = r.CSV("DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES")
return nil
}
func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
ipPrefixes []netip.Prefix, err error,
) {
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
}
ips = make([]netip.Addr, 0, len(privateAddresses))
ipPrefixes = make([]netip.Prefix, 0, len(privateAddresses))
for _, privateAddress := range privateAddresses {
ip, err := netip.ParseAddr(privateAddress)
if err == nil {
ips = append(ips, ip)
continue
}
ipPrefix, err := netip.ParsePrefix(privateAddress)
if err == nil {
ipPrefixes = append(ipPrefixes, ipPrefix)
continue
}
return nil, nil, fmt.Errorf(
"environment variable DOT_PRIVATE_ADDRESS: %w: %s",
ErrPrivateAddressNotValid, privateAddress)
}
return ips, ipPrefixes, nil
}

View File

@@ -1,113 +0,0 @@
package settings
import (
"errors"
"fmt"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"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 and cryptographic files for DNSSEC validation.
// 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
// Unbound contains settings to configure Unbound.
Unbound Unbound
// 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)
}
err = d.Unbound.validate()
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: helpers.CopyBoolPtr(d.Enabled),
UpdatePeriod: helpers.CopyDurationPtr(d.UpdatePeriod),
Unbound: d.Unbound.copy(),
Blacklist: d.Blacklist.copy(),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (d *DoT) mergeWith(other DoT) {
d.Enabled = helpers.MergeWithBool(d.Enabled, other.Enabled)
d.UpdatePeriod = helpers.MergeWithDuration(d.UpdatePeriod, other.UpdatePeriod)
d.Unbound.mergeWith(other.Unbound)
d.Blacklist.mergeWith(other.Blacklist)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (d *DoT) overrideWith(other DoT) {
d.Enabled = helpers.OverrideWithBool(d.Enabled, other.Enabled)
d.UpdatePeriod = helpers.OverrideWithDuration(d.UpdatePeriod, other.UpdatePeriod)
d.Unbound.overrideWith(other.Unbound)
d.Blacklist.overrideWith(other.Blacklist)
}
func (d *DoT) setDefaults() {
d.Enabled = helpers.DefaultBool(d.Enabled, true)
const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = helpers.DefaultDuration(d.UpdatePeriod, defaultUpdatePeriod)
d.Unbound.setDefaults()
d.Blacklist.setDefaults()
}
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", helpers.BoolPtrToYesNo(d.Enabled))
if !*d.Enabled {
return node
}
update := "disabled"
if *d.UpdatePeriod > 0 {
update = "every " + d.UpdatePeriod.String()
}
node.Appendf("Update period: %s", update)
node.AppendNode(d.Unbound.toLinesNode())
node.AppendNode(d.Blacklist.toLinesNode())
return node
}

View File

@@ -3,19 +3,24 @@ package settings
import "errors" import "errors"
var ( var (
ErrValueUnknown = errors.New("value is unknown")
ErrCityNotValid = errors.New("the city specified is not valid") ErrCityNotValid = errors.New("the city specified is not valid")
ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root") ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root")
ErrCategoryNotValid = errors.New("the category specified is not valid")
ErrCountryNotValid = errors.New("the country specified is not valid") ErrCountryNotValid = errors.New("the country specified is not valid")
ErrFilepathMissing = errors.New("filepath is missing") ErrFilepathMissing = errors.New("filepath is missing")
ErrFirewallZeroPort = errors.New("cannot have a zero port to block") ErrFirewallZeroPort = errors.New("cannot have a zero port")
ErrFirewallPublicOutboundSubnet = errors.New("outbound subnet has an unspecified address")
ErrHostnameNotValid = errors.New("the hostname specified is not valid") ErrHostnameNotValid = errors.New("the hostname specified is not valid")
ErrISPNotValid = errors.New("the ISP specified is not valid") ErrISPNotValid = errors.New("the ISP specified is not valid")
ErrMinRatioNotValid = errors.New("minimum ratio is not valid")
ErrMissingValue = errors.New("missing value") ErrMissingValue = errors.New("missing value")
ErrNameNotValid = errors.New("the server name specified is not valid") ErrNameNotValid = errors.New("the server name specified is not valid")
ErrOpenVPNClientKeyMissing = errors.New("client key is missing") ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed") ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid") ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid") ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
ErrOpenVPNKeyPassphraseIsEmpty = errors.New("key passphrase is empty")
ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high") ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high")
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty") ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported") ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
@@ -23,22 +28,31 @@ var (
ErrOpenVPNVerbosityIsOutOfBounds = errors.New("verbosity value is out of bounds") ErrOpenVPNVerbosityIsOutOfBounds = errors.New("verbosity value is out of bounds")
ErrOpenVPNVersionIsNotValid = errors.New("version is not valid") ErrOpenVPNVersionIsNotValid = errors.New("version is not valid")
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled") ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
ErrPublicIPPeriodTooShort = errors.New("public IP address check period is too short") ErrPortForwardingUserEmpty = errors.New("port forwarding username is empty")
ErrPortForwardingPasswordEmpty = errors.New("port forwarding password is empty")
ErrRegionNotValid = errors.New("the region specified is not valid") ErrRegionNotValid = errors.New("the region specified is not valid")
ErrServerAddressNotValid = errors.New("server listening address is not valid") ErrServerAddressNotValid = errors.New("server listening address is not valid")
ErrSystemPGIDNotValid = errors.New("process group id is not valid") ErrSystemPGIDNotValid = errors.New("process group id is not valid")
ErrSystemPUIDNotValid = errors.New("process user id is not valid") ErrSystemPUIDNotValid = errors.New("process user id is not valid")
ErrSystemTimezoneNotValid = errors.New("timezone is not valid") ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small") ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
ErrUpdaterProtonPasswordMissing = errors.New("proton password is missing")
ErrUpdaterProtonUsernameMissing = errors.New("proton username is missing")
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid") ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
ErrVPNTypeNotValid = errors.New("VPN type is not valid") ErrVPNTypeNotValid = errors.New("VPN type is not valid")
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
ErrWireguardAllowedIPsNotSet = errors.New("allowed IPs is not set")
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set") ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed") ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set") ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
ErrWireguardEndpointPortSet = errors.New("endpoint port is set")
ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set") ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set")
ErrWireguardInterfaceAddressIPv6 = errors.New("interface address is IPv6 but IPv6 is not supported")
ErrWireguardInterfaceNotValid = errors.New("interface name is not valid") ErrWireguardInterfaceNotValid = errors.New("interface name is not valid")
ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set") ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set")
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set") ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
ErrWireguardPublicKeyNotSet = errors.New("public key is not set") ErrWireguardPublicKeyNotSet = errors.New("public key is not set")
ErrWireguardPublicKeyNotValid = errors.New("public key is not valid") ErrWireguardPublicKeyNotValid = errors.New("public key is not valid")
ErrWireguardKeepAliveNegative = errors.New("persistent keep alive interval is negative")
ErrWireguardImplementationNotValid = errors.New("implementation is not valid")
) )

View File

@@ -2,9 +2,10 @@ package settings
import ( import (
"fmt" "fmt"
"net" "net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -12,7 +13,7 @@ import (
type Firewall struct { type Firewall struct {
VPNInputPorts []uint16 VPNInputPorts []uint16
InputPorts []uint16 InputPorts []uint16
OutboundSubnets []net.IPNet OutboundSubnets []netip.Prefix
Enabled *bool Enabled *bool
Debug *bool Debug *bool
} }
@@ -26,6 +27,12 @@ func (f Firewall) validate() (err error) {
return fmt.Errorf("input ports: %w", ErrFirewallZeroPort) return fmt.Errorf("input ports: %w", ErrFirewallZeroPort)
} }
for _, subnet := range f.OutboundSubnets {
if subnet.Addr().IsUnspecified() {
return fmt.Errorf("%w: %s", ErrFirewallPublicOutboundSubnet, subnet)
}
}
return nil return nil
} }
@@ -40,40 +47,28 @@ func hasZeroPort(ports []uint16) (has bool) {
func (f *Firewall) copy() (copied Firewall) { func (f *Firewall) copy() (copied Firewall) {
return Firewall{ return Firewall{
VPNInputPorts: helpers.CopyUint16Slice(f.VPNInputPorts), VPNInputPorts: gosettings.CopySlice(f.VPNInputPorts),
InputPorts: helpers.CopyUint16Slice(f.InputPorts), InputPorts: gosettings.CopySlice(f.InputPorts),
OutboundSubnets: helpers.CopyIPNetSlice(f.OutboundSubnets), OutboundSubnets: gosettings.CopySlice(f.OutboundSubnets),
Enabled: helpers.CopyBoolPtr(f.Enabled), Enabled: gosettings.CopyPointer(f.Enabled),
Debug: helpers.CopyBoolPtr(f.Debug), Debug: gosettings.CopyPointer(f.Debug),
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
// It merges values of slices together, even if they
// are set in the receiver settings.
func (f *Firewall) mergeWith(other Firewall) {
f.VPNInputPorts = helpers.MergeUint16Slices(f.VPNInputPorts, other.VPNInputPorts)
f.InputPorts = helpers.MergeUint16Slices(f.InputPorts, other.InputPorts)
f.OutboundSubnets = helpers.MergeIPNetsSlices(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = helpers.MergeWithBool(f.Enabled, other.Enabled)
f.Debug = helpers.MergeWithBool(f.Debug, other.Debug)
}
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (f *Firewall) overrideWith(other Firewall) { func (f *Firewall) overrideWith(other Firewall) {
f.VPNInputPorts = helpers.OverrideWithUint16Slice(f.VPNInputPorts, other.VPNInputPorts) f.VPNInputPorts = gosettings.OverrideWithSlice(f.VPNInputPorts, other.VPNInputPorts)
f.InputPorts = helpers.OverrideWithUint16Slice(f.InputPorts, other.InputPorts) f.InputPorts = gosettings.OverrideWithSlice(f.InputPorts, other.InputPorts)
f.OutboundSubnets = helpers.OverrideWithIPNetsSlice(f.OutboundSubnets, other.OutboundSubnets) f.OutboundSubnets = gosettings.OverrideWithSlice(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = helpers.OverrideWithBool(f.Enabled, other.Enabled) f.Enabled = gosettings.OverrideWithPointer(f.Enabled, other.Enabled)
f.Debug = helpers.OverrideWithBool(f.Debug, other.Debug) f.Debug = gosettings.OverrideWithPointer(f.Debug, other.Debug)
} }
func (f *Firewall) setDefaults() { func (f *Firewall) setDefaults() {
f.Enabled = helpers.DefaultBool(f.Enabled, true) f.Enabled = gosettings.DefaultPointer(f.Enabled, true)
f.Debug = helpers.DefaultBool(f.Debug, false) f.Debug = gosettings.DefaultPointer(f.Debug, false)
} }
func (f Firewall) String() string { func (f Firewall) String() string {
@@ -83,7 +78,7 @@ func (f Firewall) String() string {
func (f Firewall) toLinesNode() (node *gotree.Node) { func (f Firewall) toLinesNode() (node *gotree.Node) {
node = gotree.New("Firewall settings:") node = gotree.New("Firewall settings:")
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(f.Enabled)) node.Appendf("Enabled: %s", gosettings.BoolToYesNo(f.Enabled))
if !*f.Enabled { if !*f.Enabled {
return node return node
} }
@@ -109,9 +104,39 @@ func (f Firewall) toLinesNode() (node *gotree.Node) {
if len(f.OutboundSubnets) > 0 { if len(f.OutboundSubnets) > 0 {
outboundSubnets := node.Appendf("Outbound subnets:") outboundSubnets := node.Appendf("Outbound subnets:")
for _, subnet := range f.OutboundSubnets { for _, subnet := range f.OutboundSubnets {
outboundSubnets.Appendf("%s", subnet) outboundSubnets.Appendf("%s", &subnet)
} }
} }
return node return node
} }
func (f *Firewall) read(r *reader.Reader) (err error) {
f.VPNInputPorts, err = r.CSVUint16("FIREWALL_VPN_INPUT_PORTS")
if err != nil {
return err
}
f.InputPorts, err = r.CSVUint16("FIREWALL_INPUT_PORTS")
if err != nil {
return err
}
f.OutboundSubnets, err = r.CSVNetipPrefixes(
"FIREWALL_OUTBOUND_SUBNETS", reader.RetroKeys("EXTRA_SUBNETS"))
if err != nil {
return err
}
f.Enabled, err = r.BoolPtr("FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT")
if err != nil {
return err
}
f.Debug, err = r.BoolPtr("FIREWALL_DEBUG")
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,73 @@
package settings
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Firewall_validate(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
firewall Firewall
errWrapped error
errMessage string
}{
"empty": {},
"zero_vpn_input_port": {
firewall: Firewall{
VPNInputPorts: []uint16{0},
},
errWrapped: ErrFirewallZeroPort,
errMessage: "VPN input ports: cannot have a zero port",
},
"zero_input_port": {
firewall: Firewall{
InputPorts: []uint16{0},
},
errWrapped: ErrFirewallZeroPort,
errMessage: "input ports: cannot have a zero port",
},
"unspecified_outbound_subnet": {
firewall: Firewall{
OutboundSubnets: []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"),
},
},
errWrapped: ErrFirewallPublicOutboundSubnet,
errMessage: "outbound subnet has an unspecified address: 0.0.0.0/0",
},
"public_outbound_subnet": {
firewall: Firewall{
OutboundSubnets: []netip.Prefix{
netip.MustParsePrefix("1.2.3.4/32"),
},
},
},
"valid_settings": {
firewall: Firewall{
VPNInputPorts: []uint16{100, 101},
InputPorts: []uint16{200, 201},
OutboundSubnets: []netip.Prefix{
netip.MustParsePrefix("192.168.1.0/24"),
netip.MustParsePrefix("10.10.1.1/32"),
},
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
err := testCase.firewall.validate()
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
}
})
}
}

View File

@@ -2,11 +2,13 @@ package settings
import ( import (
"fmt" "fmt"
"net/netip"
"os" "os"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"github.com/qdm12/govalid/address"
) )
// Health contains settings for the healthcheck and health server. // Health contains settings for the healthcheck and health server.
@@ -15,58 +17,52 @@ type Health struct {
// for the health check server. // for the health check server.
// It cannot be the empty string in the internal state. // It cannot be the empty string in the internal state.
ServerAddress string ServerAddress string
// AddressToPing is the IP address or domain name to // TargetAddress is the address (host or host:port)
// ping periodically for the health check. // to TCP TLS dial to periodically for the health check.
// It cannot be the empty string in the internal state. // It cannot be the empty string in the internal state.
AddressToPing string TargetAddress string
VPN HealthyWait // ICMPTargetIP is the IP address to use for ICMP echo requests
// in the health checker. It can be set to an unspecified address (0.0.0.0)
// such that the VPN server IP is used, which is also the default behavior.
ICMPTargetIP netip.Addr
// RestartVPN indicates whether to restart the VPN connection
// when the healthcheck fails.
RestartVPN *bool
} }
func (h Health) Validate() (err error) { func (h Health) Validate() (err error) {
uid := os.Getuid() err = validate.ListeningAddress(h.ServerAddress, os.Getuid())
_, err = address.Validate(h.ServerAddress,
address.OptionListening(uid))
if err != nil { if err != nil {
return fmt.Errorf("server listening address is not valid: %w", err) return fmt.Errorf("server listening address is not valid: %w", err)
} }
err = h.VPN.validate()
if err != nil {
return fmt.Errorf("health VPN settings: %w", err)
}
return nil return nil
} }
func (h *Health) copy() (copied Health) { func (h *Health) copy() (copied Health) {
return Health{ return Health{
ServerAddress: h.ServerAddress, ServerAddress: h.ServerAddress,
AddressToPing: h.AddressToPing, TargetAddress: h.TargetAddress,
VPN: h.VPN.copy(), ICMPTargetIP: h.ICMPTargetIP,
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
} }
} }
// MergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *Health) MergeWith(other Health) {
h.ServerAddress = helpers.MergeWithString(h.ServerAddress, other.ServerAddress)
h.AddressToPing = helpers.MergeWithString(h.AddressToPing, other.AddressToPing)
h.VPN.mergeWith(other.VPN)
}
// OverrideWith overrides fields of the receiver // OverrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (h *Health) OverrideWith(other Health) { func (h *Health) OverrideWith(other Health) {
h.ServerAddress = helpers.OverrideWithString(h.ServerAddress, other.ServerAddress) h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
h.AddressToPing = helpers.OverrideWithString(h.AddressToPing, other.AddressToPing) h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
h.VPN.overrideWith(other.VPN) h.ICMPTargetIP = gosettings.OverrideWithComparable(h.ICMPTargetIP, other.ICMPTargetIP)
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
} }
func (h *Health) SetDefaults() { func (h *Health) SetDefaults() {
h.ServerAddress = helpers.DefaultString(h.ServerAddress, "127.0.0.1:9999") h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
h.AddressToPing = helpers.DefaultString(h.AddressToPing, "github.com") h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
h.VPN.setDefaults() h.ICMPTargetIP = gosettings.DefaultComparable(h.ICMPTargetIP, netip.IPv4Unspecified()) // use the VPN server IP
h.RestartVPN = gosettings.DefaultPointer(h.RestartVPN, true)
} }
func (h Health) String() string { func (h Health) String() string {
@@ -76,7 +72,27 @@ func (h Health) String() string {
func (h Health) toLinesNode() (node *gotree.Node) { func (h Health) toLinesNode() (node *gotree.Node) {
node = gotree.New("Health settings:") node = gotree.New("Health settings:")
node.Appendf("Server listening address: %s", h.ServerAddress) node.Appendf("Server listening address: %s", h.ServerAddress)
node.Appendf("Address to ping: %s", h.AddressToPing) node.Appendf("Target address: %s", h.TargetAddress)
node.AppendNode(h.VPN.toLinesNode("VPN")) icmpTarget := "VPN server IP"
if !h.ICMPTargetIP.IsUnspecified() {
icmpTarget = h.ICMPTargetIP.String()
}
node.Appendf("ICMP target IP: %s", icmpTarget)
node.Appendf("Restart VPN on healthcheck failure: %s", gosettings.BoolToYesNo(h.RestartVPN))
return node 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.ICMPTargetIP, err = r.NetipAddr("HEALTH_ICMP_TARGET_IP")
if err != nil {
return err
}
h.RestartVPN, err = r.BoolPtr("HEALTH_RESTART_VPN")
if err != nil {
return err
}
return nil
}

View File

@@ -1,66 +0,0 @@
package settings
import (
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"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
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HealthyWait) copy() (copied HealthyWait) {
return HealthyWait{
Initial: helpers.CopyDurationPtr(h.Initial),
Addition: helpers.CopyDurationPtr(h.Addition),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HealthyWait) mergeWith(other HealthyWait) {
h.Initial = helpers.MergeWithDuration(h.Initial, other.Initial)
h.Addition = helpers.MergeWithDuration(h.Addition, other.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 = helpers.OverrideWithDuration(h.Initial, other.Initial)
h.Addition = helpers.OverrideWithDuration(h.Addition, other.Addition)
}
func (h *HealthyWait) setDefaults() {
const initialDurationDefault = 6 * time.Second
const additionDurationDefault = 5 * time.Second
h.Initial = helpers.DefaultDuration(h.Initial, initialDurationDefault)
h.Addition = helpers.DefaultDuration(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
}

View File

@@ -0,0 +1,5 @@
package settings
func ptrTo[T any](value T) *T {
return &value
}

View File

@@ -1,12 +1,6 @@
package helpers package helpers
import ( func IsOneOf[T comparable](value T, choices ...T) (ok bool) {
"errors"
"fmt"
"strings"
)
func IsOneOf(value string, choices ...string) (ok bool) {
for _, choice := range choices { for _, choice := range choices {
if value == choice { if value == choice {
return true return true
@@ -14,39 +8,3 @@ func IsOneOf(value string, choices ...string) (ok bool) {
} }
return false return false
} }
var (
ErrNoChoice = errors.New("one or more values is set but there is no possible value available")
ErrValueNotOneOf = errors.New("value is not one of the possible choices")
)
func AreAllOneOf(values, choices []string) (err error) {
if len(values) > 0 && len(choices) == 0 {
return ErrNoChoice
}
set := make(map[string]struct{}, len(choices))
for _, choice := range choices {
choice = strings.ToLower(choice)
set[choice] = struct{}{}
}
for _, value := range values {
_, ok := set[value]
if !ok {
return fmt.Errorf("%w: value %q, choices available are %s",
ErrValueNotOneOf, value, strings.Join(choices, ", "))
}
}
return nil
}
func Uint16IsOneOf(port uint16, choices []uint16) (ok bool) {
for _, choice := range choices {
if port == choice {
return true
}
}
return false
}

View File

@@ -1,190 +0,0 @@
package helpers
import (
"net"
"time"
"github.com/qdm12/golibs/logging"
"inet.af/netaddr"
)
func CopyStringPtr(original *string) (copied *string) {
if original == nil {
return nil
}
copied = new(string)
*copied = *original
return copied
}
func CopyBoolPtr(original *bool) (copied *bool) {
if original == nil {
return nil
}
copied = new(bool)
*copied = *original
return copied
}
func CopyUint8Ptr(original *uint8) (copied *uint8) {
if original == nil {
return nil
}
copied = new(uint8)
*copied = *original
return copied
}
func CopyUint16Ptr(original *uint16) (copied *uint16) {
if original == nil {
return nil
}
copied = new(uint16)
*copied = *original
return copied
}
func CopyIntPtr(original *int) (copied *int) {
if original == nil {
return nil
}
copied = new(int)
*copied = *original
return copied
}
func CopyDurationPtr(original *time.Duration) (copied *time.Duration) {
if original == nil {
return nil
}
copied = new(time.Duration)
*copied = *original
return copied
}
func CopyLogLevelPtr(original *logging.Level) (copied *logging.Level) {
if original == nil {
return nil
}
copied = new(logging.Level)
*copied = *original
return copied
}
func CopyIP(original net.IP) (copied net.IP) {
if original == nil {
return nil
}
copied = make(net.IP, len(original))
copy(copied, original)
return copied
}
func CopyIPNet(original net.IPNet) (copied net.IPNet) {
if original.IP != nil {
copied.IP = make(net.IP, len(original.IP))
copy(copied.IP, original.IP)
}
if original.Mask != nil {
copied.Mask = make(net.IPMask, len(original.Mask))
copy(copied.Mask, original.Mask)
}
return copied
}
func CopyIPNetPtr(original *net.IPNet) (copied *net.IPNet) {
if original == nil {
return nil
}
copied = new(net.IPNet)
*copied = CopyIPNet(*original)
return copied
}
func CopyNetaddrIP(original netaddr.IP) (copied netaddr.IP) {
b, err := original.MarshalBinary()
if err != nil {
panic(err)
}
err = copied.UnmarshalBinary(b)
if err != nil {
panic(err)
}
return copied
}
func CopyIPPrefix(original netaddr.IPPrefix) (copied netaddr.IPPrefix) {
b, err := original.MarshalText()
if err != nil {
panic(err)
}
err = copied.UnmarshalText(b)
if err != nil {
panic(err)
}
return copied
}
func CopyStringSlice(original []string) (copied []string) {
if original == nil {
return nil
}
copied = make([]string, len(original))
copy(copied, original)
return copied
}
func CopyUint16Slice(original []uint16) (copied []uint16) {
if original == nil {
return nil
}
copied = make([]uint16, len(original))
copy(copied, original)
return copied
}
func CopyIPNetSlice(original []net.IPNet) (copied []net.IPNet) {
if original == nil {
return nil
}
copied = make([]net.IPNet, len(original))
for i := range original {
copied[i] = CopyIPNet(original[i])
}
return copied
}
func CopyIPPrefixSlice(original []netaddr.IPPrefix) (copied []netaddr.IPPrefix) {
if original == nil {
return nil
}
copied = make([]netaddr.IPPrefix, len(original))
for i := range original {
copied[i] = CopyIPPrefix(original[i])
}
return copied
}
func CopyNetaddrIPsSlice(original []netaddr.IP) (copied []netaddr.IP) {
if original == nil {
return nil
}
copied = make([]netaddr.IP, len(original))
for i := range original {
copied[i] = CopyNetaddrIP(original[i])
}
return copied
}

View File

@@ -1,93 +0,0 @@
package helpers
import (
"net"
"time"
"github.com/qdm12/golibs/logging"
)
func DefaultInt(existing *int, defaultValue int) (
result *int) {
if existing != nil {
return existing
}
result = new(int)
*result = defaultValue
return result
}
func DefaultUint8(existing *uint8, defaultValue uint8) (
result *uint8) {
if existing != nil {
return existing
}
result = new(uint8)
*result = defaultValue
return result
}
func DefaultUint16(existing *uint16, defaultValue uint16) (
result *uint16) {
if existing != nil {
return existing
}
result = new(uint16)
*result = defaultValue
return result
}
func DefaultBool(existing *bool, defaultValue bool) (
result *bool) {
if existing != nil {
return existing
}
result = new(bool)
*result = defaultValue
return result
}
func DefaultString(existing string, defaultValue string) (
result string) {
if existing != "" {
return existing
}
return defaultValue
}
func DefaultStringPtr(existing *string, defaultValue string) (result *string) {
if existing != nil {
return existing
}
result = new(string)
*result = defaultValue
return result
}
func DefaultDuration(existing *time.Duration,
defaultValue time.Duration) (result *time.Duration) {
if existing != nil {
return existing
}
result = new(time.Duration)
*result = defaultValue
return result
}
func DefaultLogLevel(existing *logging.Level,
defaultValue logging.Level) (result *logging.Level) {
if existing != nil {
return existing
}
result = new(logging.Level)
*result = defaultValue
return result
}
func DefaultIP(existing net.IP, defaultValue net.IP) (
result net.IP) {
if existing != nil {
return existing
}
return defaultValue
}

View File

@@ -1,31 +0,0 @@
package helpers
import (
"errors"
"fmt"
"os"
"path/filepath"
)
var (
ErrFileDoesNotExist = errors.New("file does not exist")
ErrFileRead = errors.New("cannot read file")
ErrFileClose = errors.New("cannot close file")
)
func FileExists(path string) (err error) {
path = filepath.Clean(path)
f, err := os.Open(path)
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("%w: %s", ErrFileDoesNotExist, path)
} else if err != nil {
return fmt.Errorf("%w: %s", ErrFileRead, err)
}
if err := f.Close(); err != nil {
return fmt.Errorf("%w: %s", ErrFileClose, err)
}
return nil
}

View File

@@ -1,241 +0,0 @@
package helpers
import (
"net"
"net/http"
"time"
"github.com/qdm12/golibs/logging"
"inet.af/netaddr"
)
func MergeWithBool(existing, other *bool) (result *bool) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = new(bool)
*result = *other
return result
}
func MergeWithString(existing, other string) (result string) {
if existing != "" {
return existing
}
return other
}
func MergeWithInt(existing, other int) (result int) {
if existing != 0 {
return existing
}
return other
}
func MergeWithStringPtr(existing, other *string) (result *string) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = new(string)
*result = *other
return result
}
func MergeWithIntPtr(existing, other *int) (result *int) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = new(int)
*result = *other
return result
}
func MergeWithUint8(existing, other *uint8) (result *uint8) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = new(uint8)
*result = *other
return result
}
func MergeWithUint16(existing, other *uint16) (result *uint16) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = new(uint16)
*result = *other
return result
}
func MergeWithIP(existing, other net.IP) (result net.IP) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = make(net.IP, len(other))
copy(result, other)
return result
}
func MergeWithDuration(existing, other *time.Duration) (result *time.Duration) {
if existing != nil {
return existing
}
return other
}
func MergeWithLogLevel(existing, other *logging.Level) (result *logging.Level) {
if existing != nil {
return existing
} else if other == nil {
return nil
}
result = new(logging.Level)
*result = *other
return result
}
func MergeWithHTTPHandler(existing, other http.Handler) (result http.Handler) {
if existing != nil {
return existing
}
return other
}
func MergeStringSlices(a, b []string) (result []string) {
if a == nil && b == nil {
return nil
}
seen := make(map[string]struct{}, len(a)+len(b))
result = make([]string, 0, len(a)+len(b))
for _, s := range a {
if _, ok := seen[s]; ok {
continue // duplicate
}
result = append(result, s)
seen[s] = struct{}{}
}
for _, s := range b {
if _, ok := seen[s]; ok {
continue // duplicate
}
result = append(result, s)
seen[s] = struct{}{}
}
return result
}
func MergeUint16Slices(a, b []uint16) (result []uint16) {
if a == nil && b == nil {
return nil
}
seen := make(map[uint16]struct{}, len(a)+len(b))
result = make([]uint16, 0, len(a)+len(b))
for _, n := range a {
if _, ok := seen[n]; ok {
continue // duplicate
}
result = append(result, n)
seen[n] = struct{}{}
}
for _, n := range b {
if _, ok := seen[n]; ok {
continue // duplicate
}
result = append(result, n)
seen[n] = struct{}{}
}
return result
}
func MergeIPNetsSlices(a, b []net.IPNet) (result []net.IPNet) {
if a == nil && b == nil {
return nil
}
seen := make(map[string]struct{}, len(a)+len(b))
result = make([]net.IPNet, 0, len(a)+len(b))
for _, ipNet := range a {
key := ipNet.String()
if _, ok := seen[key]; ok {
continue // duplicate
}
result = append(result, ipNet)
seen[key] = struct{}{}
}
for _, ipNet := range b {
key := ipNet.String()
if _, ok := seen[key]; ok {
continue // duplicate
}
result = append(result, ipNet)
seen[key] = struct{}{}
}
return result
}
func MergeNetaddrIPsSlices(a, b []netaddr.IP) (result []netaddr.IP) {
if a == nil && b == nil {
return nil
}
seen := make(map[string]struct{}, len(a)+len(b))
result = make([]netaddr.IP, 0, len(a)+len(b))
for _, ip := range a {
key := ip.String()
if _, ok := seen[key]; ok {
continue // duplicate
}
result = append(result, ip)
seen[key] = struct{}{}
}
for _, ip := range b {
key := ip.String()
if _, ok := seen[key]; ok {
continue // duplicate
}
result = append(result, ip)
seen[key] = struct{}{}
}
return result
}
func MergeIPPrefixesSlices(a, b []netaddr.IPPrefix) (result []netaddr.IPPrefix) {
if a == nil && b == nil {
return nil
}
seen := make(map[string]struct{}, len(a)+len(b))
result = make([]netaddr.IPPrefix, 0, len(a)+len(b))
for _, ipPrefix := range a {
key := ipPrefix.String()
if _, ok := seen[key]; ok {
continue // duplicate
}
result = append(result, ipPrefix)
seen[key] = struct{}{}
}
for _, ipPrefix := range b {
key := ipPrefix.String()
if _, ok := seen[key]; ok {
continue // duplicate
}
result = append(result, ipPrefix)
seen[key] = struct{}{}
}
return result
}

View File

@@ -1,29 +0,0 @@
package helpers
import (
"fmt"
"strings"
)
func ChoicesOrString(choices []string) string {
return strings.Join(
choices[:len(choices)-1], ", ") +
" or " + choices[len(choices)-1]
}
func PortChoicesOrString(ports []uint16) (s string) {
switch len(ports) {
case 0:
return "there is no allowed port"
case 1:
return "allowed port is " + fmt.Sprint(ports[0])
}
s = "allowed ports are "
portStrings := make([]string, len(ports))
for i := range ports {
portStrings[i] = fmt.Sprint(ports[i])
}
s += ChoicesOrString(portStrings)
return s
}

View File

@@ -1,25 +0,0 @@
package helpers
func ObfuscateWireguardKey(fullKey string) (obfuscatedKey string) {
const minKeyLength = 10
if len(fullKey) < minKeyLength {
return "(too short)"
}
lastIndex := len(fullKey) - 1
return fullKey[0:2] + "..." + fullKey[lastIndex-2:]
}
func ObfuscatePassword(password string) (obfuscatedPassword string) {
if password != "" {
return "[set]"
}
return "[not set]"
}
func ObfuscateData(data string) (obfuscated string) {
if data != "" {
return "[set]"
}
return "[not set]"
}

View File

@@ -1,148 +0,0 @@
package helpers
import (
"net"
"net/http"
"time"
"github.com/qdm12/golibs/logging"
"inet.af/netaddr"
)
func OverrideWithBool(existing, other *bool) (result *bool) {
if other == nil {
return existing
}
result = new(bool)
*result = *other
return result
}
func OverrideWithString(existing, other string) (result string) {
if other == "" {
return existing
}
return other
}
func OverrideWithInt(existing, other int) (result int) {
if other == 0 {
return existing
}
return other
}
func OverrideWithStringPtr(existing, other *string) (result *string) {
if other == nil {
return existing
}
result = new(string)
*result = *other
return result
}
func OverrideWithIntPtr(existing, other *int) (result *int) {
if other == nil {
return existing
}
result = new(int)
*result = *other
return result
}
func OverrideWithUint8(existing, other *uint8) (result *uint8) {
if other == nil {
return existing
}
result = new(uint8)
*result = *other
return result
}
func OverrideWithUint16(existing, other *uint16) (result *uint16) {
if other == nil {
return existing
}
result = new(uint16)
*result = *other
return result
}
func OverrideWithIP(existing, other net.IP) (result net.IP) {
if other == nil {
return existing
}
result = make(net.IP, len(other))
copy(result, other)
return result
}
func OverrideWithDuration(existing, other *time.Duration) (result *time.Duration) {
if other == nil {
return existing
}
result = new(time.Duration)
*result = *other
return result
}
func OverrideWithLogLevel(existing, other *logging.Level) (result *logging.Level) {
if other == nil {
return existing
}
result = new(logging.Level)
*result = *other
return result
}
func OverrideWithHTTPHandler(existing, other http.Handler) (result http.Handler) {
if other != nil {
return other
}
return existing
}
func OverrideWithStringSlice(existing, other []string) (result []string) {
if other == nil {
return existing
}
result = make([]string, len(other))
copy(result, other)
return result
}
func OverrideWithUint16Slice(existing, other []uint16) (result []uint16) {
if other == nil {
return existing
}
result = make([]uint16, len(other))
copy(result, other)
return result
}
func OverrideWithIPNetsSlice(existing, other []net.IPNet) (result []net.IPNet) {
if other == nil {
return existing
}
result = make([]net.IPNet, len(other))
copy(result, other)
return result
}
func OverrideWithNetaddrIPsSlice(existing, other []netaddr.IP) (result []netaddr.IP) {
if other == nil {
return existing
}
result = make([]netaddr.IP, len(other))
copy(result, other)
return result
}
func OverrideWithIPPrefixesSlice(existing, other []netaddr.IPPrefix) (result []netaddr.IPPrefix) {
if other == nil {
return existing
}
result = make([]netaddr.IPPrefix, len(other))
copy(result, other)
return result
}

View File

@@ -1,11 +0,0 @@
package helpers
import "time"
// StringPtr returns a pointer to the string value
// passed as argument.
func StringPtr(s string) *string { return &s }
// DurationPtr returns a pointer to the duration value
// passed as argument.
func DurationPtr(d time.Duration) *time.Duration { return &d }

View File

@@ -1,15 +0,0 @@
package helpers
func BoolPtrToYesNo(b *bool) string {
if *b {
return "yes"
}
return "no"
}
func TCPPtrToString(tcp *bool) string {
if *tcp {
return "TCP"
}
return "UDP"
}

View File

@@ -1,4 +1,30 @@
package settings package settings
func boolPtr(b bool) *bool { return &b } import gomock "github.com/golang/mock/gomock"
func uint8Ptr(n uint8) *uint8 { return &n }
type sourceKeyValue struct {
key string
value string
}
func newMockSource(ctrl *gomock.Controller, keyValues []sourceKeyValue) *MockSource {
source := NewMockSource(ctrl)
var previousCall *gomock.Call
for _, keyValue := range keyValues {
transformedKey := keyValue.key
keyTransformCall := source.EXPECT().KeyTransform(keyValue.key).Return(transformedKey)
if previousCall != nil {
keyTransformCall.After(previousCall)
}
isSet := keyValue.value != ""
previousCall = source.EXPECT().Get(transformedKey).
Return(keyValue.value, isSet).After(keyTransformCall)
if isSet {
previousCall = source.EXPECT().KeyTransform(keyValue.key).
Return(transformedKey).After(previousCall)
previousCall = source.EXPECT().String().
Return("mock source").After(previousCall)
}
}
return source
}

View File

@@ -3,10 +3,13 @@ package settings
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"github.com/qdm12/govalid/address"
) )
// HTTPProxy contains settings to configure the HTTP proxy. // HTTPProxy contains settings to configure the HTTP proxy.
@@ -33,13 +36,17 @@ type HTTPProxy struct {
// each request/response. It cannot be nil in the // each request/response. It cannot be nil in the
// internal state. // internal state.
Log *bool Log *bool
// ReadHeaderTimeout is the HTTP header read timeout duration
// of the HTTP server. It defaults to 1 second if left unset.
ReadHeaderTimeout time.Duration
// ReadTimeout is the HTTP read timeout duration
// of the HTTP server. It defaults to 3 seconds if left unset.
ReadTimeout time.Duration
} }
func (h HTTPProxy) validate() (err error) { func (h HTTPProxy) validate() (err error) {
// Do not validate user and password // Do not validate user and password
err = validate.ListeningAddress(h.ListeningAddress, os.Getuid())
uid := os.Getuid()
_, err = address.Validate(h.ListeningAddress, address.OptionListening(uid))
if err != nil { if err != nil {
return fmt.Errorf("%w: %s", ErrServerAddressNotValid, h.ListeningAddress) return fmt.Errorf("%w: %s", ErrServerAddressNotValid, h.ListeningAddress)
} }
@@ -49,45 +56,42 @@ func (h HTTPProxy) validate() (err error) {
func (h *HTTPProxy) copy() (copied HTTPProxy) { func (h *HTTPProxy) copy() (copied HTTPProxy) {
return HTTPProxy{ return HTTPProxy{
User: helpers.CopyStringPtr(h.User), User: gosettings.CopyPointer(h.User),
Password: helpers.CopyStringPtr(h.Password), Password: gosettings.CopyPointer(h.Password),
ListeningAddress: h.ListeningAddress, ListeningAddress: h.ListeningAddress,
Enabled: helpers.CopyBoolPtr(h.Enabled), Enabled: gosettings.CopyPointer(h.Enabled),
Stealth: helpers.CopyBoolPtr(h.Stealth), Stealth: gosettings.CopyPointer(h.Stealth),
Log: helpers.CopyBoolPtr(h.Log), Log: gosettings.CopyPointer(h.Log),
ReadHeaderTimeout: h.ReadHeaderTimeout,
ReadTimeout: h.ReadTimeout,
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HTTPProxy) mergeWith(other HTTPProxy) {
h.User = helpers.MergeWithStringPtr(h.User, other.User)
h.Password = helpers.MergeWithStringPtr(h.Password, other.Password)
h.ListeningAddress = helpers.MergeWithString(h.ListeningAddress, other.ListeningAddress)
h.Enabled = helpers.MergeWithBool(h.Enabled, other.Enabled)
h.Stealth = helpers.MergeWithBool(h.Stealth, other.Stealth)
h.Log = helpers.MergeWithBool(h.Log, other.Log)
}
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (h *HTTPProxy) overrideWith(other HTTPProxy) { func (h *HTTPProxy) overrideWith(other HTTPProxy) {
h.User = helpers.OverrideWithStringPtr(h.User, other.User) h.User = gosettings.OverrideWithPointer(h.User, other.User)
h.Password = helpers.OverrideWithStringPtr(h.Password, other.Password) h.Password = gosettings.OverrideWithPointer(h.Password, other.Password)
h.ListeningAddress = helpers.OverrideWithString(h.ListeningAddress, other.ListeningAddress) h.ListeningAddress = gosettings.OverrideWithComparable(h.ListeningAddress, other.ListeningAddress)
h.Enabled = helpers.OverrideWithBool(h.Enabled, other.Enabled) h.Enabled = gosettings.OverrideWithPointer(h.Enabled, other.Enabled)
h.Stealth = helpers.OverrideWithBool(h.Stealth, other.Stealth) h.Stealth = gosettings.OverrideWithPointer(h.Stealth, other.Stealth)
h.Log = helpers.OverrideWithBool(h.Log, other.Log) h.Log = gosettings.OverrideWithPointer(h.Log, other.Log)
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
} }
func (h *HTTPProxy) setDefaults() { func (h *HTTPProxy) setDefaults() {
h.User = helpers.DefaultStringPtr(h.User, "") h.User = gosettings.DefaultPointer(h.User, "")
h.Password = helpers.DefaultStringPtr(h.Password, "") h.Password = gosettings.DefaultPointer(h.Password, "")
h.ListeningAddress = helpers.DefaultString(h.ListeningAddress, ":8888") h.ListeningAddress = gosettings.DefaultComparable(h.ListeningAddress, ":8888")
h.Enabled = helpers.DefaultBool(h.Enabled, false) h.Enabled = gosettings.DefaultPointer(h.Enabled, false)
h.Stealth = helpers.DefaultBool(h.Stealth, false) h.Stealth = gosettings.DefaultPointer(h.Stealth, false)
h.Log = helpers.DefaultBool(h.Log, false) h.Log = gosettings.DefaultPointer(h.Log, false)
const defaultReadHeaderTimeout = time.Second
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
const defaultReadTimeout = 3 * time.Second
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
} }
func (h HTTPProxy) String() string { func (h HTTPProxy) String() string {
@@ -96,16 +100,83 @@ func (h HTTPProxy) String() string {
func (h HTTPProxy) toLinesNode() (node *gotree.Node) { func (h HTTPProxy) toLinesNode() (node *gotree.Node) {
node = gotree.New("HTTP proxy settings:") node = gotree.New("HTTP proxy settings:")
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(h.Enabled)) node.Appendf("Enabled: %s", gosettings.BoolToYesNo(h.Enabled))
if !*h.Enabled { if !*h.Enabled {
return node return node
} }
node.Appendf("Listening address: %s", h.ListeningAddress) node.Appendf("Listening address: %s", h.ListeningAddress)
node.Appendf("User: %s", *h.User) node.Appendf("User: %s", *h.User)
node.Appendf("Password: %s", helpers.ObfuscatePassword(*h.Password)) node.Appendf("Password: %s", gosettings.ObfuscateKey(*h.Password))
node.Appendf("Stealth mode: %s", helpers.BoolPtrToYesNo(h.Stealth)) node.Appendf("Stealth mode: %s", gosettings.BoolToYesNo(h.Stealth))
node.Appendf("Log: %s", helpers.BoolPtrToYesNo(h.Log)) node.Appendf("Log: %s", gosettings.BoolToYesNo(h.Log))
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
node.Appendf("Read timeout: %s", h.ReadTimeout)
return node return node
} }
func (h *HTTPProxy) read(r *reader.Reader) (err error) {
h.User = r.Get("HTTPPROXY_USER",
reader.RetroKeys("PROXY_USER", "TINYPROXY_USER"),
reader.ForceLowercase(false))
h.Password = r.Get("HTTPPROXY_PASSWORD",
reader.RetroKeys("PROXY_PASSWORD", "TINYPROXY_PASSWORD"),
reader.ForceLowercase(false))
h.ListeningAddress, err = readHTTProxyListeningAddress(r)
if err != nil {
return err
}
h.Enabled, err = r.BoolPtr("HTTPPROXY", reader.RetroKeys("PROXY", "TINYPROXY"))
if err != nil {
return err
}
h.Stealth, err = r.BoolPtr("HTTPPROXY_STEALTH")
if err != nil {
return err
}
h.Log, err = readHTTProxyLog(r)
if err != nil {
return err
}
return nil
}
func readHTTProxyListeningAddress(r *reader.Reader) (listeningAddress string, err error) {
// Retro-compatible keys using a port only
port, err := r.Uint16Ptr("",
reader.RetroKeys("HTTPPROXY_PORT", "TINYPROXY_PORT", "PROXY_PORT"),
reader.IsRetro("HTTPPROXY_LISTENING_ADDRESS"))
if err != nil {
return "", err
} else if port != nil {
return fmt.Sprintf(":%d", *port), nil
}
const currentKey = "HTTPPROXY_LISTENING_ADDRESS"
return r.String(currentKey), nil
}
func readHTTProxyLog(r *reader.Reader) (enabled *bool, err error) {
const currentKey = "HTTPPROXY_LOG"
// Retro-compatible keys using different boolean verbs
value := r.String("",
reader.RetroKeys("PROXY_LOG", "TINYPROXY_LOG"),
reader.IsRetro(currentKey))
switch strings.ToLower(value) {
case "":
return r.BoolPtr(currentKey)
case "on", "info", "connect", "notice":
return ptrTo(true), nil
case "disabled", "no", "off":
return ptrTo(false), nil
default:
return nil, fmt.Errorf("HTTP retro-compatible proxy log setting: %w: %s",
ErrValueUnknown, value)
}
}

View File

@@ -0,0 +1,5 @@
package settings
type Warner interface {
Warn(message string)
}

View File

@@ -1,43 +1,44 @@
package settings package settings
import ( import (
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "fmt"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"github.com/qdm12/log"
) )
// Log contains settings to configure the logger. // Log contains settings to configure the logger.
type Log struct { type Log struct {
// Level is the log level of the logger. // Level is the log level of the logger.
// It cannot be nil in the internal state. // It cannot be empty in the internal state.
Level *logging.Level Level string
} }
func (l Log) validate() (err error) { func (l Log) validate() (err error) {
_, err = log.ParseLevel(l.Level)
if err != nil {
return fmt.Errorf("level: %w", err)
}
return nil return nil
} }
func (l *Log) copy() (copied Log) { func (l *Log) copy() (copied Log) {
return Log{ return Log{
Level: helpers.CopyLogLevelPtr(l.Level), Level: l.Level,
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (l *Log) mergeWith(other Log) {
l.Level = helpers.MergeWithLogLevel(l.Level, other.Level)
}
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (l *Log) overrideWith(other Log) { func (l *Log) overrideWith(other Log) {
l.Level = helpers.OverrideWithLogLevel(l.Level, other.Level) l.Level = gosettings.OverrideWithComparable(l.Level, other.Level)
} }
func (l *Log) setDefaults() { func (l *Log) setDefaults() {
l.Level = helpers.DefaultLogLevel(l.Level, logging.LevelInfo) l.Level = gosettings.DefaultComparable(l.Level, log.LevelInfo.String())
} }
func (l Log) String() string { func (l Log) String() string {
@@ -46,6 +47,11 @@ func (l Log) String() string {
func (l Log) toLinesNode() (node *gotree.Node) { func (l Log) toLinesNode() (node *gotree.Node) {
node = gotree.New("Log settings:") node = gotree.New("Log settings:")
node.Appendf("Log level: %s", l.Level.String()) node.Appendf("Log level: %s", l.Level)
return node return node
} }
func (l *Log) read(r *reader.Reader) (err error) {
l.Level = r.String("LOG_LEVEL")
return nil
}

View File

@@ -0,0 +1,4 @@
package settings
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Warner
//go:generate mockgen -destination=mocks_reader_test.go -package=$GOPACKAGE github.com/qdm12/gosettings/reader Source

View File

@@ -0,0 +1,77 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gosettings/reader (interfaces: Source)
// Package settings is a generated GoMock package.
package settings
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockSource is a mock of Source interface.
type MockSource struct {
ctrl *gomock.Controller
recorder *MockSourceMockRecorder
}
// MockSourceMockRecorder is the mock recorder for MockSource.
type MockSourceMockRecorder struct {
mock *MockSource
}
// NewMockSource creates a new mock instance.
func NewMockSource(ctrl *gomock.Controller) *MockSource {
mock := &MockSource{ctrl: ctrl}
mock.recorder = &MockSourceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSource) EXPECT() *MockSourceMockRecorder {
return m.recorder
}
// Get mocks base method.
func (m *MockSource) Get(arg0 string) (string, bool) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(bool)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockSourceMockRecorder) Get(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSource)(nil).Get), arg0)
}
// KeyTransform mocks base method.
func (m *MockSource) KeyTransform(arg0 string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyTransform", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// KeyTransform indicates an expected call of KeyTransform.
func (mr *MockSourceMockRecorder) KeyTransform(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyTransform", reflect.TypeOf((*MockSource)(nil).KeyTransform), arg0)
}
// String mocks base method.
func (m *MockSource) String() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "String")
ret0, _ := ret[0].(string)
return ret0
}
// String indicates an expected call of String.
func (mr *MockSourceMockRecorder) String() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockSource)(nil).String))
}

View File

@@ -0,0 +1,46 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/configuration/settings (interfaces: Warner)
// Package settings is a generated GoMock package.
package settings
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockWarner is a mock of Warner interface.
type MockWarner struct {
ctrl *gomock.Controller
recorder *MockWarnerMockRecorder
}
// MockWarnerMockRecorder is the mock recorder for MockWarner.
type MockWarnerMockRecorder struct {
mock *MockWarner
}
// NewMockWarner creates a new mock instance.
func NewMockWarner(ctrl *gomock.Controller) *MockWarner {
mock := &MockWarner{ctrl: ctrl}
mock.recorder = &MockWarnerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockWarner) EXPECT() *MockWarnerMockRecorder {
return m.recorder
}
// Warn mocks base method.
func (m *MockWarner) Warn(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Warn", arg0)
}
// Warn indicates an expected call of Warn.
func (mr *MockWarnerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), arg0)
}

View File

@@ -0,0 +1,43 @@
package settings
// Retro-compatibility because SERVER_REGIONS changed to SERVER_COUNTRIES
// and SERVER_REGIONS is now the continent field for servers.
// TODO v4 remove.
func nordvpnRetroRegion(selection ServerSelection, validRegions, validCountries []string) (
updatedSelection ServerSelection,
) {
validRegionsMap := stringSliceToMap(validRegions)
validCountriesMap := stringSliceToMap(validCountries)
updatedSelection = selection.copy()
updatedSelection.Regions = make([]string, 0, len(selection.Regions))
for _, region := range selection.Regions {
_, isValid := validRegionsMap[region]
if isValid {
updatedSelection.Regions = append(updatedSelection.Regions, region)
continue
}
_, isValid = validCountriesMap[region]
if !isValid {
// Region is not valid for the country or region
// just leave it to the validation to fail it later
continue
}
// Region is not valid for a region, but is a valid country
// Handle retro-compatibility and transfer the value to the
// country field.
updatedSelection.Countries = append(updatedSelection.Countries, region)
}
return updatedSelection
}
func stringSliceToMap(slice []string) (m map[string]struct{}) {
m = make(map[string]struct{}, len(slice))
for _, s := range slice {
m[s] = struct{}{}
}
return m
}

View File

@@ -1,94 +1,114 @@
package settings package settings
import ( import (
"encoding/base64"
"fmt" "fmt"
"regexp"
"strings" "strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/constants/openvpn"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/openvpn/parse" "github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
// OpenVPN contains settings to configure the OpenVPN client. // OpenVPN contains settings to configure the OpenVPN client.
type OpenVPN struct { type OpenVPN struct {
// Version is the OpenVPN version to run. // Version is the OpenVPN version to run.
// It can only be "2.4" or "2.5". // It can only be "2.5" or "2.6".
Version string Version string `json:"version"`
// User is the OpenVPN authentication username. // User is the OpenVPN authentication username.
// It cannot be an empty string in the internal state // It cannot be nil in the internal state if OpenVPN is used.
// if OpenVPN is used. // It is usually required but in some cases can be the empty string
User string // to indicate no user+password authentication is needed.
User *string `json:"user"`
// Password is the OpenVPN authentication password. // Password is the OpenVPN authentication password.
// It cannot be an empty string in the internal state // It cannot be nil in the internal state if OpenVPN is used.
// if OpenVPN is used. // It is usually required but in some cases can be the empty string
Password string // to indicate no user+password authentication is needed.
Password *string `json:"password"`
// ConfFile is a custom OpenVPN configuration file path. // ConfFile is a custom OpenVPN configuration file path.
// It can be set to the empty string for it to be ignored. // It can be set to the empty string for it to be ignored.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
ConfFile *string ConfFile *string `json:"config_file_path"`
// Ciphers is a list of ciphers to use for OpenVPN, // Ciphers is a list of ciphers to use for OpenVPN,
// different from the ones specified by the VPN // different from the ones specified by the VPN
// service provider configuration files. // service provider configuration files.
Ciphers []string Ciphers []string `json:"ciphers"`
// Auth is an auth algorithm to use in OpenVPN instead // Auth is an auth algorithm to use in OpenVPN instead
// of the one specified by the VPN service provider. // of the one specified by the VPN service provider.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
// It is ignored if it is set to the empty string. // It is ignored if it is set to the empty string.
Auth *string Auth *string `json:"auth"`
// ClientCrt is the OpenVPN client certificate. // Cert is the base64 encoded DER of an OpenVPN certificate for the <cert> block.
// This is notably used by Cyberghost. // This is notably used by Cyberghost and VPN secure.
// It can be set to the empty string to be ignored. // It can be set to the empty string to be ignored.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
ClientCrt *string Cert *string `json:"cert"`
// ClientKey is the OpenVPN client key. // Key is the base64 encoded DER of an OpenVPN key.
// This is used by Cyberghost and VPN Unlimited. // This is used by Cyberghost and VPN Unlimited.
// It can be set to the empty string to be ignored. // It can be set to the empty string to be ignored.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
ClientKey *string Key *string `json:"key"`
// EncryptedKey is the base64 encoded DER of an encrypted key for OpenVPN.
// It is used by VPN secure.
// It defaults to the empty string meaning it is not
// to be used. KeyPassphrase must be set if this one is set.
EncryptedKey *string `json:"encrypted_key"`
// KeyPassphrase is the key passphrase to be used by OpenVPN
// to decrypt the EncryptedPrivateKey. It defaults to the
// empty string and must be set if EncryptedPrivateKey is set.
KeyPassphrase *string `json:"key_passphrase"`
// PIAEncPreset is the encryption preset for // PIAEncPreset is the encryption preset for
// Private Internet Access. It can be set to an // Private Internet Access. It can be set to an
// empty string for other providers. // empty string for other providers.
PIAEncPreset *string PIAEncPreset *string `json:"pia_encryption_preset"`
// IPv6 is set to true if IPv6 routing should be
// set to be tunnel in OpenVPN, and false otherwise.
// It cannot be nil in the internal state.
IPv6 *bool // TODO automate like with Wireguard
// MSSFix is the value (1 to 10000) to set for the // MSSFix is the value (1 to 10000) to set for the
// mssfix option for OpenVPN. It is ignored if set to 0. // mssfix option for OpenVPN. It is ignored if set to 0.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
MSSFix *uint16 MSSFix *uint16 `json:"mssfix"`
// Interface is the OpenVPN device interface name. // Interface is the OpenVPN device interface name.
// It cannot be an empty string in the internal state. // It cannot be an empty string in the internal state.
Interface string Interface string `json:"interface"`
// ProcessUser is the OpenVPN process OS username // ProcessUser is the OpenVPN process OS username
// to use. It cannot be empty in the internal state. // to use. It cannot be empty in the internal state.
// It defaults to 'root'. // It defaults to 'root'.
ProcessUser string ProcessUser string `json:"process_user"`
// Verbosity is the OpenVPN verbosity level from 0 to 6. // Verbosity is the OpenVPN verbosity level from 0 to 6.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
Verbosity *int Verbosity *int `json:"verbosity"`
// Flags is a slice of additional flags to be passed // Flags is a slice of additional flags to be passed
// to the OpenVPN program. // to the OpenVPN program.
Flags []string Flags []string `json:"flags"`
} }
var ivpnAccountID = regexp.MustCompile(`^(i|ivpn)\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}$`)
func (o OpenVPN) validate(vpnProvider string) (err error) { func (o OpenVPN) validate(vpnProvider string) (err error) {
// Validate version // Validate version
validVersions := []string{constants.Openvpn24, constants.Openvpn25} validVersions := []string{openvpn.Openvpn25, openvpn.Openvpn26}
if !helpers.IsOneOf(o.Version, validVersions...) { if err = validate.IsOneOf(o.Version, validVersions...); err != nil {
return fmt.Errorf("%w: %q can only be one of %s", return fmt.Errorf("%w: %w", ErrOpenVPNVersionIsNotValid, err)
ErrOpenVPNVersionIsNotValid, o.Version, strings.Join(validVersions, ", "))
} }
isCustom := vpnProvider == constants.Custom isCustom := vpnProvider == providers.Custom
isUserRequired := !isCustom &&
vpnProvider != providers.Airvpn &&
vpnProvider != providers.VPNSecure
if !isCustom && o.User == "" { if isUserRequired && *o.User == "" {
return ErrOpenVPNUserIsEmpty return fmt.Errorf("%w", ErrOpenVPNUserIsEmpty)
} }
if !isCustom && o.Password == "" { passwordRequired := isUserRequired &&
return ErrOpenVPNPasswordIsEmpty (vpnProvider != providers.Ivpn || !ivpnAccountID.MatchString(*o.User))
if passwordRequired && *o.Password == "" {
return fmt.Errorf("%w", ErrOpenVPNPasswordIsEmpty)
} }
err = validateOpenVPNConfigFilepath(isCustom, *o.ConfFile) err = validateOpenVPNConfigFilepath(isCustom, *o.ConfFile)
@@ -96,16 +116,25 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
return fmt.Errorf("custom configuration file: %w", err) return fmt.Errorf("custom configuration file: %w", err)
} }
err = validateOpenVPNClientCertificate(vpnProvider, *o.ClientCrt) err = validateOpenVPNClientCertificate(vpnProvider, *o.Cert)
if err != nil { if err != nil {
return fmt.Errorf("client certificate: %w", err) return fmt.Errorf("client certificate: %w", err)
} }
err = validateOpenVPNClientKey(vpnProvider, *o.ClientKey) err = validateOpenVPNClientKey(vpnProvider, *o.Key)
if err != nil { if err != nil {
return fmt.Errorf("client key: %w", err) return fmt.Errorf("client key: %w", err)
} }
err = validateOpenVPNEncryptedKey(vpnProvider, *o.EncryptedKey)
if err != nil {
return fmt.Errorf("encrypted key: %w", err)
}
if *o.EncryptedKey != "" && *o.KeyPassphrase == "" {
return fmt.Errorf("%w", ErrOpenVPNKeyPassphraseIsEmpty)
}
const maxMSSFix = 10000 const maxMSSFix = 10000
if *o.MSSFix > maxMSSFix { if *o.MSSFix > maxMSSFix {
return fmt.Errorf("%w: %d is over the maximum value of %d", return fmt.Errorf("%w: %d is over the maximum value of %d",
@@ -126,31 +155,41 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
} }
func validateOpenVPNConfigFilepath(isCustom bool, func validateOpenVPNConfigFilepath(isCustom bool,
confFile string) (err error) { confFile string,
) (err error) {
if !isCustom { if !isCustom {
return nil return nil
} }
if confFile == "" { if confFile == "" {
return ErrFilepathMissing return fmt.Errorf("%w", ErrFilepathMissing)
} }
err = helpers.FileExists(confFile) err = validate.FileExists(confFile)
if err != nil { if err != nil {
return err return err
} }
extractor := extract.New()
_, _, err = extractor.Data(confFile)
if err != nil {
return fmt.Errorf("extracting information from custom configuration file: %w", err)
}
return nil return nil
} }
func validateOpenVPNClientCertificate(vpnProvider, func validateOpenVPNClientCertificate(vpnProvider,
clientCert string) (err error) { clientCert string,
) (err error) {
switch vpnProvider { switch vpnProvider {
case case
constants.Cyberghost, providers.Airvpn,
constants.VPNUnlimited: providers.Cyberghost,
providers.VPNSecure,
providers.VPNUnlimited:
if clientCert == "" { if clientCert == "" {
return ErrMissingValue return fmt.Errorf("%w", ErrMissingValue)
} }
} }
@@ -158,7 +197,7 @@ func validateOpenVPNClientCertificate(vpnProvider,
return nil return nil
} }
_, err = parse.ExtractCert([]byte(clientCert)) _, err = base64.StdEncoding.DecodeString(clientCert)
if err != nil { if err != nil {
return err return err
} }
@@ -168,11 +207,12 @@ func validateOpenVPNClientCertificate(vpnProvider,
func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) { func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
switch vpnProvider { switch vpnProvider {
case case
constants.Cyberghost, providers.Airvpn,
constants.VPNUnlimited, providers.Cyberghost,
constants.Wevpn: providers.VPNUnlimited,
providers.Wevpn:
if clientKey == "" { if clientKey == "" {
return ErrMissingValue return fmt.Errorf("%w", ErrMissingValue)
} }
} }
@@ -180,7 +220,25 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
return nil return nil
} }
_, err = parse.ExtractPrivateKey([]byte(clientKey)) _, err = base64.StdEncoding.DecodeString(clientKey)
if err != nil {
return err
}
return nil
}
func validateOpenVPNEncryptedKey(vpnProvider,
encryptedPrivateKey string,
) (err error) {
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
return fmt.Errorf("%w", ErrMissingValue)
}
if encryptedPrivateKey == "" {
return nil
}
_, err = base64.StdEncoding.DecodeString(encryptedPrivateKey)
if err != nil { if err != nil {
return err return err
} }
@@ -189,87 +247,72 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
func (o *OpenVPN) copy() (copied OpenVPN) { func (o *OpenVPN) copy() (copied OpenVPN) {
return OpenVPN{ return OpenVPN{
Version: o.Version, Version: o.Version,
User: o.User, User: gosettings.CopyPointer(o.User),
Password: o.Password, Password: gosettings.CopyPointer(o.Password),
ConfFile: helpers.CopyStringPtr(o.ConfFile), ConfFile: gosettings.CopyPointer(o.ConfFile),
Ciphers: helpers.CopyStringSlice(o.Ciphers), Ciphers: gosettings.CopySlice(o.Ciphers),
Auth: helpers.CopyStringPtr(o.Auth), Auth: gosettings.CopyPointer(o.Auth),
ClientCrt: helpers.CopyStringPtr(o.ClientCrt), Cert: gosettings.CopyPointer(o.Cert),
ClientKey: helpers.CopyStringPtr(o.ClientKey), Key: gosettings.CopyPointer(o.Key),
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset), EncryptedKey: gosettings.CopyPointer(o.EncryptedKey),
IPv6: helpers.CopyBoolPtr(o.IPv6), KeyPassphrase: gosettings.CopyPointer(o.KeyPassphrase),
MSSFix: helpers.CopyUint16Ptr(o.MSSFix), PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
Interface: o.Interface, MSSFix: gosettings.CopyPointer(o.MSSFix),
ProcessUser: o.ProcessUser, Interface: o.Interface,
Verbosity: helpers.CopyIntPtr(o.Verbosity), ProcessUser: o.ProcessUser,
Flags: helpers.CopyStringSlice(o.Flags), Verbosity: gosettings.CopyPointer(o.Verbosity),
Flags: gosettings.CopySlice(o.Flags),
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (o *OpenVPN) mergeWith(other OpenVPN) {
o.Version = helpers.MergeWithString(o.Version, other.Version)
o.User = helpers.MergeWithString(o.User, other.User)
o.Password = helpers.MergeWithString(o.Password, other.Password)
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
o.Ciphers = helpers.MergeStringSlices(o.Ciphers, other.Ciphers)
o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth)
o.ClientCrt = helpers.MergeWithStringPtr(o.ClientCrt, other.ClientCrt)
o.ClientKey = helpers.MergeWithStringPtr(o.ClientKey, other.ClientKey)
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
o.IPv6 = helpers.MergeWithBool(o.IPv6, other.IPv6)
o.MSSFix = helpers.MergeWithUint16(o.MSSFix, other.MSSFix)
o.Interface = helpers.MergeWithString(o.Interface, other.Interface)
o.ProcessUser = helpers.MergeWithString(o.ProcessUser, other.ProcessUser)
o.Verbosity = helpers.MergeWithIntPtr(o.Verbosity, other.Verbosity)
o.Flags = helpers.MergeStringSlices(o.Flags, other.Flags)
}
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (o *OpenVPN) overrideWith(other OpenVPN) { func (o *OpenVPN) overrideWith(other OpenVPN) {
o.Version = helpers.OverrideWithString(o.Version, other.Version) o.Version = gosettings.OverrideWithComparable(o.Version, other.Version)
o.User = helpers.OverrideWithString(o.User, other.User) o.User = gosettings.OverrideWithPointer(o.User, other.User)
o.Password = helpers.OverrideWithString(o.Password, other.Password) o.Password = gosettings.OverrideWithPointer(o.Password, other.Password)
o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile) o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
o.Ciphers = helpers.OverrideWithStringSlice(o.Ciphers, other.Ciphers) o.Ciphers = gosettings.OverrideWithSlice(o.Ciphers, other.Ciphers)
o.Auth = helpers.OverrideWithStringPtr(o.Auth, other.Auth) o.Auth = gosettings.OverrideWithPointer(o.Auth, other.Auth)
o.ClientCrt = helpers.OverrideWithStringPtr(o.ClientCrt, other.ClientCrt) o.Cert = gosettings.OverrideWithPointer(o.Cert, other.Cert)
o.ClientKey = helpers.OverrideWithStringPtr(o.ClientKey, other.ClientKey) o.Key = gosettings.OverrideWithPointer(o.Key, other.Key)
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset) o.EncryptedKey = gosettings.OverrideWithPointer(o.EncryptedKey, other.EncryptedKey)
o.IPv6 = helpers.OverrideWithBool(o.IPv6, other.IPv6) o.KeyPassphrase = gosettings.OverrideWithPointer(o.KeyPassphrase, other.KeyPassphrase)
o.MSSFix = helpers.OverrideWithUint16(o.MSSFix, other.MSSFix) o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
o.Interface = helpers.OverrideWithString(o.Interface, other.Interface) o.MSSFix = gosettings.OverrideWithPointer(o.MSSFix, other.MSSFix)
o.ProcessUser = helpers.OverrideWithString(o.ProcessUser, other.ProcessUser) o.Interface = gosettings.OverrideWithComparable(o.Interface, other.Interface)
o.Verbosity = helpers.OverrideWithIntPtr(o.Verbosity, other.Verbosity) o.ProcessUser = gosettings.OverrideWithComparable(o.ProcessUser, other.ProcessUser)
o.Flags = helpers.OverrideWithStringSlice(o.Flags, other.Flags) o.Verbosity = gosettings.OverrideWithPointer(o.Verbosity, other.Verbosity)
o.Flags = gosettings.OverrideWithSlice(o.Flags, other.Flags)
} }
func (o *OpenVPN) setDefaults(vpnProvider string) { func (o *OpenVPN) setDefaults(vpnProvider string) {
o.Version = helpers.DefaultString(o.Version, constants.Openvpn25) o.Version = gosettings.DefaultComparable(o.Version, openvpn.Openvpn26)
if vpnProvider == constants.Mullvad { o.User = gosettings.DefaultPointer(o.User, "")
o.Password = "m" if vpnProvider == providers.Mullvad {
o.Password = gosettings.DefaultPointer(o.Password, "m")
} else {
o.Password = gosettings.DefaultPointer(o.Password, "")
} }
o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "") o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
o.Auth = helpers.DefaultStringPtr(o.Auth, "") o.Auth = gosettings.DefaultPointer(o.Auth, "")
o.ClientCrt = helpers.DefaultStringPtr(o.ClientCrt, "") o.Cert = gosettings.DefaultPointer(o.Cert, "")
o.ClientKey = helpers.DefaultStringPtr(o.ClientKey, "") o.Key = gosettings.DefaultPointer(o.Key, "")
o.EncryptedKey = gosettings.DefaultPointer(o.EncryptedKey, "")
o.KeyPassphrase = gosettings.DefaultPointer(o.KeyPassphrase, "")
var defaultEncPreset string var defaultEncPreset string
if vpnProvider == constants.PrivateInternetAccess { if vpnProvider == providers.PrivateInternetAccess {
defaultEncPreset = constants.PIAEncryptionPresetStrong defaultEncPreset = presets.Strong
} }
o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset) o.PIAEncPreset = gosettings.DefaultPointer(o.PIAEncPreset, defaultEncPreset)
o.MSSFix = gosettings.DefaultPointer(o.MSSFix, 0)
o.IPv6 = helpers.DefaultBool(o.IPv6, false) o.Interface = gosettings.DefaultComparable(o.Interface, "tun0")
o.MSSFix = helpers.DefaultUint16(o.MSSFix, 0) o.ProcessUser = gosettings.DefaultComparable(o.ProcessUser, "root")
o.Interface = helpers.DefaultString(o.Interface, "tun0") o.Verbosity = gosettings.DefaultPointer(o.Verbosity, 1)
o.ProcessUser = helpers.DefaultString(o.ProcessUser, "root")
o.Verbosity = helpers.DefaultInt(o.Verbosity, 1)
} }
func (o OpenVPN) String() string { func (o OpenVPN) String() string {
@@ -279,8 +322,8 @@ func (o OpenVPN) String() string {
func (o OpenVPN) toLinesNode() (node *gotree.Node) { func (o OpenVPN) toLinesNode() (node *gotree.Node) {
node = gotree.New("OpenVPN settings:") node = gotree.New("OpenVPN settings:")
node.Appendf("OpenVPN version: %s", o.Version) node.Appendf("OpenVPN version: %s", o.Version)
node.Appendf("User: %s", helpers.ObfuscatePassword(o.User)) node.Appendf("User: %s", gosettings.ObfuscateKey(*o.User))
node.Appendf("Password: %s", helpers.ObfuscatePassword(o.Password)) node.Appendf("Password: %s", gosettings.ObfuscateKey(*o.Password))
if *o.ConfFile != "" { if *o.ConfFile != "" {
node.Appendf("Custom configuration file: %s", *o.ConfFile) node.Appendf("Custom configuration file: %s", *o.ConfFile)
@@ -294,20 +337,23 @@ func (o OpenVPN) toLinesNode() (node *gotree.Node) {
node.Appendf("Auth: %s", *o.Auth) node.Appendf("Auth: %s", *o.Auth)
} }
if *o.ClientCrt != "" { if *o.Cert != "" {
node.Appendf("Client crt: %s", helpers.ObfuscateData(*o.ClientCrt)) node.Appendf("Client crt: %s", gosettings.ObfuscateKey(*o.Cert))
} }
if *o.ClientKey != "" { if *o.Key != "" {
node.Appendf("Client key: %s", helpers.ObfuscateData(*o.ClientKey)) node.Appendf("Client key: %s", gosettings.ObfuscateKey(*o.Key))
}
if *o.EncryptedKey != "" {
node.Appendf("Encrypted key: %s (key passhrapse %s)",
gosettings.ObfuscateKey(*o.EncryptedKey), gosettings.ObfuscateKey(*o.KeyPassphrase))
} }
if *o.PIAEncPreset != "" { if *o.PIAEncPreset != "" {
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset) node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
} }
node.Appendf("Tunnel IPv6: %s", helpers.BoolPtrToYesNo(o.IPv6))
if *o.MSSFix > 0 { if *o.MSSFix > 0 {
node.Appendf("MSS Fix: %d", *o.MSSFix) node.Appendf("MSS Fix: %d", *o.MSSFix)
} }
@@ -333,3 +379,58 @@ func (o OpenVPN) WithDefaults(provider string) OpenVPN {
o.setDefaults(provider) o.setDefaults(provider)
return o return o
} }
func (o *OpenVPN) read(r *reader.Reader) (err error) {
o.Version = r.String("OPENVPN_VERSION")
o.User = r.Get("OPENVPN_USER", reader.RetroKeys("USER"), reader.ForceLowercase(false))
o.Password = r.Get("OPENVPN_PASSWORD", reader.RetroKeys("PASSWORD"), reader.ForceLowercase(false))
o.ConfFile = r.Get("OPENVPN_CUSTOM_CONFIG", reader.ForceLowercase(false))
o.Ciphers = r.CSV("OPENVPN_CIPHERS", reader.RetroKeys("OPENVPN_CIPHER"))
o.Auth = r.Get("OPENVPN_AUTH")
o.Cert = r.Get("OPENVPN_CERT", reader.ForceLowercase(false))
o.Key = r.Get("OPENVPN_KEY", reader.ForceLowercase(false))
o.EncryptedKey = r.Get("OPENVPN_ENCRYPTED_KEY", reader.ForceLowercase(false))
o.KeyPassphrase = r.Get("OPENVPN_KEY_PASSPHRASE", reader.ForceLowercase(false))
o.PIAEncPreset = r.Get("PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET",
reader.RetroKeys("ENCRYPTION", "PIA_ENCRYPTION"))
o.MSSFix, err = r.Uint16Ptr("OPENVPN_MSSFIX")
if err != nil {
return err
}
o.Interface = r.String("VPN_INTERFACE",
reader.RetroKeys("OPENVPN_INTERFACE"), reader.ForceLowercase(false))
o.ProcessUser, err = readOpenVPNProcessUser(r)
if err != nil {
return err
}
o.Verbosity, err = r.IntPtr("OPENVPN_VERBOSITY")
if err != nil {
return err
}
flagsPtr := r.Get("OPENVPN_FLAGS", reader.ForceLowercase(false))
if flagsPtr != nil {
o.Flags = strings.Fields(*flagsPtr)
}
return nil
}
func readOpenVPNProcessUser(r *reader.Reader) (processUser string, err error) {
value, err := r.BoolPtr("OPENVPN_ROOT") // Retro-compatibility
if err != nil {
return "", err
} else if value != nil {
if *value {
return "root", nil
}
const defaultNonRootUser = "nonrootuser"
return defaultNonRootUser, nil
}
return r.String("OPENVPN_PROCESS_USER"), nil
}

View File

@@ -0,0 +1,43 @@
package settings
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_ivpnAccountID(t *testing.T) {
t.Parallel()
testCases := []struct {
s string
match bool
}{
{},
{s: "abc"},
{s: "i"},
{s: "ivpn"},
{s: "ivpn-aaaa"},
{s: "ivpn-aaaa-aaaa"},
{s: "ivpn-aaaa-aaaa-aaa"},
{s: "ivpn-aaaa-aaaa-aaaa", match: true},
{s: "ivpn-aaaa-aaaa-aaaaa"},
{s: "ivpn-a6B7-fP91-Zh6Y", match: true},
{s: "i-aaaa"},
{s: "i-aaaa-aaaa"},
{s: "i-aaaa-aaaa-aaa"},
{s: "i-aaaa-aaaa-aaaa", match: true},
{s: "i-aaaa-aaaa-aaaaa"},
{s: "i-a6B7-fP91-Zh6Y", match: true},
}
for _, testCase := range testCases {
t.Run(testCase.s, func(t *testing.T) {
t.Parallel()
match := ivpnAccountID.MatchString(testCase.s)
assert.Equal(t, testCase.match, match)
})
}
}

View File

@@ -2,9 +2,15 @@ package settings
import ( import (
"fmt" "fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -13,37 +19,42 @@ type OpenVPNSelection struct {
// It can be set to an empty string to indicate to // It can be set to an empty string to indicate to
// NOT use a custom configuration file. // NOT use a custom configuration file.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
ConfFile *string ConfFile *string `json:"config_file_path"`
// TCP is true if the OpenVPN protocol is TCP, // Protocol is the OpenVPN network protocol to use,
// and false for UDP. // and can be udp or tcp. It cannot be the empty string
// It cannot be nil in the internal state. // in the internal state.
TCP *bool Protocol string `json:"protocol"`
// CustomPort is the OpenVPN server endpoint port. // CustomPort is the OpenVPN server endpoint port.
// It can be set to 0 to indicate no custom port should // It can be set to 0 to indicate no custom port should
// be used. It cannot be nil in the internal state. // be used. It cannot be nil in the internal state.
CustomPort *uint16 // HideMyAss, Mullvad, PIA, ProtonVPN, WeVPN, Windscribe CustomPort *uint16 `json:"custom_port"`
// PIAEncPreset is the encryption preset for // PIAEncPreset is the encryption preset for
// Private Internet Access. It can be set to an // Private Internet Access. It can be set to an
// empty string for other providers. // empty string for other providers.
PIAEncPreset *string PIAEncPreset *string `json:"pia_encryption_preset"`
} }
func (o OpenVPNSelection) validate(vpnProvider string) (err error) { func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
// Validate ConfFile // Validate ConfFile
if confFile := *o.ConfFile; confFile != "" { if confFile := *o.ConfFile; confFile != "" {
err := helpers.FileExists(confFile) err := validate.FileExists(confFile)
if err != nil { if err != nil {
return fmt.Errorf("configuration file: %w", err) return fmt.Errorf("configuration file: %w", err)
} }
} }
err = validate.IsOneOf(o.Protocol, constants.UDP, constants.TCP)
if err != nil {
return fmt.Errorf("network protocol: %w", err)
}
// Validate TCP // Validate TCP
if *o.TCP && helpers.IsOneOf(vpnProvider, if o.Protocol == constants.TCP && helpers.IsOneOf(vpnProvider,
constants.Ipvanish, providers.Giganews,
constants.Perfectprivacy, providers.Ipvanish,
constants.Privado, providers.Perfectprivacy,
constants.VPNUnlimited, providers.Privado,
constants.Vyprvpn, providers.Vyprvpn,
) { ) {
return fmt.Errorf("%w: for VPN service provider %s", return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNTCPNotSupported, vpnProvider) ErrOpenVPNTCPNotSupported, vpnProvider)
@@ -53,60 +64,74 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
if *o.CustomPort != 0 { if *o.CustomPort != 0 {
switch vpnProvider { switch vpnProvider {
// no restriction on port // no restriction on port
case constants.Cyberghost, constants.HideMyAss, case providers.Custom, providers.Cyberghost, providers.HideMyAss,
constants.PrivateInternetAccess, constants.Privatevpn, providers.Privatevpn, providers.Torguard:
constants.Protonvpn, constants.Torguard:
// no custom port allowed // no custom port allowed
case constants.Expressvpn, constants.Fastestvpn, case providers.Expressvpn, providers.Fastestvpn,
constants.Ipvanish, constants.Nordvpn, providers.Giganews, providers.Ipvanish, providers.Nordvpn,
constants.Privado, constants.Purevpn, providers.Privado, providers.Purevpn,
constants.Surfshark, constants.VPNUnlimited, providers.Surfshark, providers.VPNSecure,
constants.Vyprvpn: providers.VPNUnlimited, providers.Vyprvpn:
return fmt.Errorf("%w: for VPN service provider %s", return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNCustomPortNotAllowed, vpnProvider) ErrOpenVPNCustomPortNotAllowed, vpnProvider)
default: default:
var allowedTCP, allowedUDP []uint16 var allowedTCP, allowedUDP []uint16
switch vpnProvider { switch vpnProvider {
case constants.Ivpn: case providers.Airvpn:
allowedTCP = []uint16{
53, 80, 443, // IP in 1, 3
1194, 2018, 41185, // IP in 1, 2, 3, 4
}
allowedUDP = []uint16{53, 80, 443, 1194, 2018, 41185}
case providers.Ivpn:
allowedTCP = []uint16{80, 443, 1143} allowedTCP = []uint16{80, 443, 1143}
allowedUDP = []uint16{53, 1194, 2049, 2050} allowedUDP = []uint16{53, 1194, 2049, 2050}
case constants.Mullvad: case providers.Mullvad:
allowedTCP = []uint16{80, 443, 1401} allowedTCP = []uint16{80, 443, 1401}
allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400} allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}
case constants.Perfectprivacy: case providers.Perfectprivacy:
allowedTCP = []uint16{44, 443, 4433} allowedTCP = []uint16{44, 443, 4433}
allowedUDP = []uint16{44, 443, 4433} allowedUDP = []uint16{44, 443, 4433}
case constants.Wevpn: case providers.PrivateInternetAccess:
allowedTCP = []uint16{80, 110, 443}
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
case providers.Protonvpn:
allowedTCP = []uint16{443, 5995, 8443}
allowedUDP = []uint16{80, 443, 1194, 4569, 5060}
case providers.SlickVPN:
allowedTCP = []uint16{443, 8080, 8888}
allowedUDP = []uint16{443, 8080, 8888}
case providers.Wevpn:
allowedTCP = []uint16{53, 1195, 1199, 2018} allowedTCP = []uint16{53, 1195, 1199, 2018}
allowedUDP = []uint16{80, 1194, 1198} allowedUDP = []uint16{80, 1194, 1198}
case constants.Windscribe: case providers.Windscribe:
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783} allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783} allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
default:
panic(fmt.Sprintf("VPN provider %s has no registered allowed ports", vpnProvider))
} }
if *o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedTCP) { allowedPorts := allowedUDP
return fmt.Errorf("%w: %d for VPN service provider %s; %s", if o.Protocol == constants.TCP {
ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider, allowedPorts = allowedTCP
helpers.PortChoicesOrString(allowedTCP)) }
} else if !*o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedUDP) { err = validate.IsOneOf(*o.CustomPort, allowedPorts...)
return fmt.Errorf("%w: %d for VPN service provider %s; %s", if err != nil {
ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider, return fmt.Errorf("%w: for VPN service provider %s: %w",
helpers.PortChoicesOrString(allowedUDP)) ErrOpenVPNCustomPortNotAllowed, vpnProvider, err)
} }
} }
} }
// Validate EncPreset // Validate EncPreset
if vpnProvider == constants.PrivateInternetAccess { if vpnProvider == providers.PrivateInternetAccess {
validEncryptionPresets := []string{ validEncryptionPresets := []string{
constants.PIAEncryptionPresetNone, presets.None,
constants.PIAEncryptionPresetNormal, presets.Normal,
constants.PIAEncryptionPresetStrong, presets.Strong,
} }
if !helpers.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...) { if err = validate.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...); err != nil {
return fmt.Errorf("%w: %s; valid presets are %s", return fmt.Errorf("%w: %w", ErrOpenVPNEncryptionPresetNotValid, err)
ErrOpenVPNEncryptionPresetNotValid, *o.PIAEncPreset,
helpers.ChoicesOrString(validEncryptionPresets))
} }
} }
@@ -115,37 +140,30 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) { func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
return OpenVPNSelection{ return OpenVPNSelection{
ConfFile: helpers.CopyStringPtr(o.ConfFile), ConfFile: gosettings.CopyPointer(o.ConfFile),
TCP: helpers.CopyBoolPtr(o.TCP), Protocol: o.Protocol,
CustomPort: helpers.CopyUint16Ptr(o.CustomPort), CustomPort: gosettings.CopyPointer(o.CustomPort),
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset), PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
} }
} }
func (o *OpenVPNSelection) mergeWith(other OpenVPNSelection) {
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
o.TCP = helpers.MergeWithBool(o.TCP, other.TCP)
o.CustomPort = helpers.MergeWithUint16(o.CustomPort, other.CustomPort)
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
}
func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) { func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile) o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
o.TCP = helpers.OverrideWithBool(o.TCP, other.TCP) o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
o.CustomPort = helpers.OverrideWithUint16(o.CustomPort, other.CustomPort) o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset) o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
} }
func (o *OpenVPNSelection) setDefaults(vpnProvider string) { func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "") o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
o.TCP = helpers.DefaultBool(o.TCP, false) o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
o.CustomPort = helpers.DefaultUint16(o.CustomPort, 0) o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
var defaultEncPreset string var defaultEncPreset string
if vpnProvider == constants.PrivateInternetAccess { if vpnProvider == providers.PrivateInternetAccess {
defaultEncPreset = constants.PIAEncryptionPresetStrong defaultEncPreset = presets.Strong
} }
o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset) o.PIAEncPreset = gosettings.DefaultPointer(o.PIAEncPreset, defaultEncPreset)
} }
func (o OpenVPNSelection) String() string { func (o OpenVPNSelection) String() string {
@@ -154,7 +172,7 @@ func (o OpenVPNSelection) String() string {
func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) { func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("OpenVPN server selection settings:") node = gotree.New("OpenVPN server selection settings:")
node.Appendf("Protocol: %s", helpers.TCPPtrToString(o.TCP)) node.Appendf("Protocol: %s", strings.ToUpper(o.Protocol))
if *o.CustomPort != 0 { if *o.CustomPort != 0 {
node.Appendf("Custom port: %d", *o.CustomPort) node.Appendf("Custom port: %d", *o.CustomPort)
@@ -170,3 +188,20 @@ func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
return node return node
} }
func (o *OpenVPNSelection) read(r *reader.Reader) (err error) {
o.ConfFile = r.Get("OPENVPN_CUSTOM_CONFIG", reader.ForceLowercase(false))
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))
if err != nil {
return err
}
o.PIAEncPreset = r.Get("PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET",
reader.RetroKeys("ENCRYPTION", "PIA_ENCRYPTION"))
return nil
}

View File

@@ -3,10 +3,11 @@ package settings
import ( import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -14,24 +15,56 @@ import (
type PortForwarding struct { type PortForwarding struct {
// Enabled is true if port forwarding should be activated. // Enabled is true if port forwarding should be activated.
// It cannot be nil for the internal state. // It cannot be nil for the internal state.
Enabled *bool Enabled *bool `json:"enabled"`
// Provider is set to specify which custom port forwarding code
// should be used. This is especially necessary for the custom
// provider using Wireguard for a provider where Wireguard is not
// natively supported but custom port forwarding code is available.
// It defaults to the empty string, meaning the current provider
// should be the one used for port forwarding.
// It cannot be nil for the internal state.
Provider *string `json:"provider"`
// Filepath is the port forwarding status file path // Filepath is the port forwarding status file path
// to use. It can be the empty string to indicate not // to use. It can be the empty string to indicate not
// to write to a file. It cannot be nil for the // to write to a file. It cannot be nil for the
// internal state // internal state
Filepath *string 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.
ListeningPort *uint16 `json:"listening_port"`
// Username is only used for Private Internet Access port forwarding.
Username string `json:"username"`
// Password is only used for Private Internet Access port forwarding.
Password string `json:"password"`
} }
func (p PortForwarding) validate(vpnProvider string) (err error) { func (p PortForwarding) Validate(vpnProvider string) (err error) {
if !*p.Enabled { if !*p.Enabled {
return nil return nil
} }
// Validate Enabled // Validate current provider or custom provider specified
validProviders := []string{constants.PrivateInternetAccess} providerSelected := vpnProvider
if !helpers.IsOneOf(vpnProvider, validProviders...) { if *p.Provider != "" {
return fmt.Errorf("%w: for provider %s, it is only available for %s", providerSelected = *p.Provider
ErrPortForwardingEnabled, vpnProvider, strings.Join(validProviders, ", ")) }
validProviders := []string{
providers.Perfectprivacy,
providers.PrivateInternetAccess,
providers.Privatevpn,
providers.Protonvpn,
}
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err)
} }
// Validate Filepath // Validate Filepath
@@ -42,29 +75,49 @@ func (p PortForwarding) validate(vpnProvider string) (err error) {
} }
} }
if providerSelected == providers.PrivateInternetAccess {
switch {
case p.Username == "":
return fmt.Errorf("%w", ErrPortForwardingUserEmpty)
case p.Password == "":
return fmt.Errorf("%w", ErrPortForwardingPasswordEmpty)
}
}
return nil return nil
} }
func (p *PortForwarding) copy() (copied PortForwarding) { func (p *PortForwarding) Copy() (copied PortForwarding) {
return PortForwarding{ return PortForwarding{
Enabled: helpers.CopyBoolPtr(p.Enabled), Enabled: gosettings.CopyPointer(p.Enabled),
Filepath: helpers.CopyStringPtr(p.Filepath), 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,
} }
} }
func (p *PortForwarding) mergeWith(other PortForwarding) { func (p *PortForwarding) OverrideWith(other PortForwarding) {
p.Enabled = helpers.MergeWithBool(p.Enabled, other.Enabled) p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
p.Filepath = helpers.MergeWithStringPtr(p.Filepath, other.Filepath) p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
} p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
func (p *PortForwarding) overrideWith(other PortForwarding) { p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
p.Enabled = helpers.OverrideWithBool(p.Enabled, other.Enabled) p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
p.Filepath = helpers.OverrideWithStringPtr(p.Filepath, other.Filepath) p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
} }
func (p *PortForwarding) setDefaults() { func (p *PortForwarding) setDefaults() {
p.Enabled = helpers.DefaultBool(p.Enabled, false) p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
p.Filepath = helpers.DefaultStringPtr(p.Filepath, "/tmp/gluetun/forwarded_port") 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)
} }
func (p PortForwarding) String() string { func (p PortForwarding) String() string {
@@ -77,7 +130,18 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
} }
node = gotree.New("Automatic port forwarding settings:") node = gotree.New("Automatic port forwarding settings:")
node.Appendf("Enabled: yes")
listeningPort := "disabled"
if *p.ListeningPort != 0 {
listeningPort = fmt.Sprintf("%d", *p.ListeningPort)
}
node.Appendf("Redirection listening port: %s", listeningPort)
if *p.Provider == "" {
node.Appendf("Use port forwarding code for current provider")
} else {
node.Appendf("Use code for provider: %s", *p.Provider)
}
filepath := *p.Filepath filepath := *p.Filepath
if filepath == "" { if filepath == "" {
@@ -85,5 +149,67 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
} }
node.Appendf("Forwarded port file path: %s", filepath) 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)
credentialsNode.Appendf("Password: %s", gosettings.ObfuscateKey(p.Password))
}
return node return node
} }
func (p *PortForwarding) read(r *reader.Reader) (err error) {
p.Enabled, err = r.BoolPtr("VPN_PORT_FORWARDING",
reader.RetroKeys(
"PORT_FORWARDING",
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING",
))
if err != nil {
return err
}
p.Provider = r.Get("VPN_PORT_FORWARDING_PROVIDER")
p.Filepath = r.Get("VPN_PORT_FORWARDING_STATUS_FILE",
reader.ForceLowercase(false),
reader.RetroKeys(
"PORT_FORWARDING_STATUS_FILE",
"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
}
usernameKeys := []string{"VPN_PORT_FORWARDING_USERNAME", "OPENVPN_USER", "USER"}
for _, key := range usernameKeys {
p.Username = r.String(key, reader.ForceLowercase(false))
if p.Username != "" {
break
}
}
passwordKeys := []string{"VPN_PORT_FORWARDING_PASSWORD", "OPENVPN_PASSWORD", "PASSWORD"}
for _, key := range passwordKeys {
p.Password = r.String(key, reader.ForceLowercase(false))
if p.Password != "" {
break
}
}
return nil
}

View File

@@ -10,7 +10,7 @@ func Test_PortForwarding_String(t *testing.T) {
t.Parallel() t.Parallel()
settings := PortForwarding{ settings := PortForwarding{
Enabled: boolPtr(false), Enabled: ptrTo(false),
} }
s := settings.String() s := settings.String()

View File

@@ -2,51 +2,58 @@ package settings
import ( import (
"fmt" "fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
// Provider contains settings specific to a VPN provider. // Provider contains settings specific to a VPN provider.
type Provider struct { type Provider struct {
// Name is the VPN service provider name. // Name is the VPN service provider name.
// It cannot be nil in the internal state. // It cannot be the empty string in the internal state.
Name *string Name string `json:"name"`
// ServerSelection is the settings to // ServerSelection is the settings to
// select the VPN server. // select the VPN server.
ServerSelection ServerSelection ServerSelection ServerSelection `json:"server_selection"`
// PortForwarding is the settings about port forwarding. // PortForwarding is the settings about port forwarding.
PortForwarding PortForwarding PortForwarding PortForwarding `json:"port_forwarding"`
} }
// TODO v4 remove pointer for receiver (because of Surfshark). // TODO v4 remove pointer for receiver (because of Surfshark).
func (p *Provider) validate(vpnType string, allServers models.AllServers) (err error) { func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGetter, warner Warner) (err error) {
// Validate Name // Validate Name
var validNames []string var validNames []string
if vpnType == constants.OpenVPN { if vpnType == vpn.OpenVPN {
validNames = constants.AllProviders() validNames = providers.AllWithCustom()
validNames = append(validNames, "pia") // Retro-compatibility validNames = append(validNames, "pia") // Retro-compatibility
} else { // Wireguard } else { // Wireguard
validNames = []string{ validNames = []string{
constants.Custom, providers.Airvpn,
constants.Ivpn, providers.Custom,
constants.Mullvad, providers.Fastestvpn,
constants.Windscribe, providers.Ivpn,
providers.Mullvad,
providers.Nordvpn,
providers.Protonvpn,
providers.Surfshark,
providers.Windscribe,
} }
} }
if !helpers.IsOneOf(*p.Name, validNames...) { if err = validate.IsOneOf(p.Name, validNames...); err != nil {
return fmt.Errorf("%w: %q can only be one of %s", return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
ErrVPNProviderNameNotValid, *p.Name, helpers.ChoicesOrString(validNames))
} }
err = p.ServerSelection.validate(*p.Name, allServers) err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
if err != nil { if err != nil {
return fmt.Errorf("server selection: %w", err) return fmt.Errorf("server selection: %w", err)
} }
err = p.PortForwarding.validate(*p.Name) err = p.PortForwarding.Validate(p.Name)
if err != nil { if err != nil {
return fmt.Errorf("port forwarding: %w", err) return fmt.Errorf("port forwarding: %w", err)
} }
@@ -56,28 +63,22 @@ func (p *Provider) validate(vpnType string, allServers models.AllServers) (err e
func (p *Provider) copy() (copied Provider) { func (p *Provider) copy() (copied Provider) {
return Provider{ return Provider{
Name: helpers.CopyStringPtr(p.Name), Name: p.Name,
ServerSelection: p.ServerSelection.copy(), ServerSelection: p.ServerSelection.copy(),
PortForwarding: p.PortForwarding.copy(), PortForwarding: p.PortForwarding.Copy(),
} }
} }
func (p *Provider) mergeWith(other Provider) {
p.Name = helpers.MergeWithStringPtr(p.Name, other.Name)
p.ServerSelection.mergeWith(other.ServerSelection)
p.PortForwarding.mergeWith(other.PortForwarding)
}
func (p *Provider) overrideWith(other Provider) { func (p *Provider) overrideWith(other Provider) {
p.Name = helpers.OverrideWithStringPtr(p.Name, other.Name) p.Name = gosettings.OverrideWithComparable(p.Name, other.Name)
p.ServerSelection.overrideWith(other.ServerSelection) p.ServerSelection.overrideWith(other.ServerSelection)
p.PortForwarding.overrideWith(other.PortForwarding) p.PortForwarding.OverrideWith(other.PortForwarding)
} }
func (p *Provider) setDefaults() { func (p *Provider) setDefaults() {
p.Name = helpers.DefaultStringPtr(p.Name, constants.PrivateInternetAccess) p.Name = gosettings.DefaultComparable(p.Name, providers.PrivateInternetAccess)
p.ServerSelection.setDefaults(*p.Name)
p.PortForwarding.setDefaults() p.PortForwarding.setDefaults()
p.ServerSelection.setDefaults(p.Name, *p.PortForwarding.Enabled)
} }
func (p Provider) String() string { func (p Provider) String() string {
@@ -86,8 +87,42 @@ func (p Provider) String() string {
func (p Provider) toLinesNode() (node *gotree.Node) { func (p Provider) toLinesNode() (node *gotree.Node) {
node = gotree.New("VPN provider settings:") node = gotree.New("VPN provider settings:")
node.Appendf("Name: %s", *p.Name) node.Appendf("Name: %s", p.Name)
node.AppendNode(p.ServerSelection.toLinesNode()) node.AppendNode(p.ServerSelection.toLinesNode())
node.AppendNode(p.PortForwarding.toLinesNode()) node.AppendNode(p.PortForwarding.toLinesNode())
return node return node
} }
func (p *Provider) read(r *reader.Reader, vpnType string) (err error) {
p.Name = readVPNServiceProvider(r, vpnType)
err = p.ServerSelection.read(r, p.Name, vpnType)
if err != nil {
return fmt.Errorf("server selection: %w", err)
}
err = p.PortForwarding.read(r)
if err != nil {
return fmt.Errorf("port forwarding: %w", err)
}
return nil
}
func readVPNServiceProvider(r *reader.Reader, vpnType string) (vpnProvider string) {
vpnProvider = r.String("VPN_SERVICE_PROVIDER", reader.RetroKeys("VPNSP"))
if vpnProvider == "" {
if vpnType != vpn.Wireguard && r.Get("OPENVPN_CUSTOM_CONFIG") != nil {
// retro compatibility
return providers.Custom
}
return ""
}
vpnProvider = strings.ToLower(vpnProvider)
if vpnProvider == "pia" { // retro compatibility
return providers.PrivateInternetAccess
}
return vpnProvider
}

View File

@@ -3,33 +3,54 @@ package settings
import ( import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/publicip/api"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
// PublicIP contains settings for port forwarding. // PublicIP contains settings for port forwarding.
type PublicIP struct { type PublicIP struct {
// Period is the period to get the public IP address. // Enabled is set to true to fetch the public ip address
// It can be set to 0 to disable periodic checking. // information on VPN connection. It defaults to true.
// It cannot be nil for the internal state. Enabled *bool
// TODO change to value and add enabled field
Period *time.Duration
// IPFilepath is the public IP address status file path // IPFilepath is the public IP address status file path
// to use. It can be the empty string to indicate not // to use. It can be the empty string to indicate not
// to write to a file. It cannot be nil for the // to write to a file. It cannot be nil for the
// internal state // internal state
IPFilepath *string IPFilepath *string
// APIs is the list of public ip APIs to use to fetch public IP information.
// If there is more than one API, the first one is used
// by default and the others are used as fallbacks in case of
// the service rate limiting us. It defaults to use all services,
// with the first one being ipinfo.io for historical reasons.
APIs []PublicIPAPI
}
type PublicIPAPI struct {
// Name is the name of the public ip API service.
// It can be "cloudflare", "ifconfigco", "ip2location" or "ipinfo".
Name string
// Token is the token to use for the public ip API service.
Token string
}
// UpdateWith deep copies the receiving settings, overrides the copy with
// fields set in the partialUpdate argument, validates the new settings
// and returns them if they are valid, or returns an error otherwise.
// In all cases, the receiving settings are unmodified.
func (p PublicIP) UpdateWith(partialUpdate PublicIP) (updatedSettings PublicIP, err error) {
updatedSettings = p.copy()
updatedSettings.overrideWith(partialUpdate)
err = updatedSettings.validate()
if err != nil {
return updatedSettings, fmt.Errorf("validating updated settings: %w", err)
}
return updatedSettings, nil
} }
func (p PublicIP) validate() (err error) { func (p PublicIP) validate() (err error) {
const minPeriod = 5 * time.Second
if *p.Period < minPeriod {
return fmt.Errorf("%w: %s must be at least %s",
ErrPublicIPPeriodTooShort, p.Period, minPeriod)
}
if *p.IPFilepath != "" { // optional if *p.IPFilepath != "" { // optional
_, err := filepath.Abs(*p.IPFilepath) _, err := filepath.Abs(*p.IPFilepath)
if err != nil { if err != nil {
@@ -37,30 +58,39 @@ func (p PublicIP) validate() (err error) {
} }
} }
for _, publicIPAPI := range p.APIs {
_, err = api.ParseProvider(publicIPAPI.Name)
if err != nil {
return fmt.Errorf("API name: %w", err)
}
}
return nil return nil
} }
func (p *PublicIP) copy() (copied PublicIP) { func (p *PublicIP) copy() (copied PublicIP) {
return PublicIP{ return PublicIP{
Period: helpers.CopyDurationPtr(p.Period), Enabled: gosettings.CopyPointer(p.Enabled),
IPFilepath: helpers.CopyStringPtr(p.IPFilepath), IPFilepath: gosettings.CopyPointer(p.IPFilepath),
APIs: gosettings.CopySlice(p.APIs),
} }
} }
func (p *PublicIP) mergeWith(other PublicIP) {
p.Period = helpers.MergeWithDuration(p.Period, other.Period)
p.IPFilepath = helpers.MergeWithStringPtr(p.IPFilepath, other.IPFilepath)
}
func (p *PublicIP) overrideWith(other PublicIP) { func (p *PublicIP) overrideWith(other PublicIP) {
p.Period = helpers.OverrideWithDuration(p.Period, other.Period) p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
p.IPFilepath = helpers.OverrideWithStringPtr(p.IPFilepath, other.IPFilepath) p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath)
p.APIs = gosettings.OverrideWithSlice(p.APIs, other.APIs)
} }
func (p *PublicIP) setDefaults() { func (p *PublicIP) setDefaults() {
const defaultPeriod = 12 * time.Hour p.Enabled = gosettings.DefaultPointer(p.Enabled, true)
p.Period = helpers.DefaultDuration(p.Period, defaultPeriod) p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip")
p.IPFilepath = helpers.DefaultStringPtr(p.IPFilepath, "/tmp/gluetun/ip") p.APIs = gosettings.DefaultSlice(p.APIs, []PublicIPAPI{
{Name: string(api.IPInfo)},
{Name: string(api.Cloudflare)},
{Name: string(api.IfConfigCo)},
{Name: string(api.IP2Location)},
})
} }
func (p PublicIP) String() string { func (p PublicIP) String() string {
@@ -68,22 +98,78 @@ func (p PublicIP) String() string {
} }
func (p PublicIP) toLinesNode() (node *gotree.Node) { func (p PublicIP) toLinesNode() (node *gotree.Node) {
if !*p.Enabled {
return gotree.New("Public IP settings: disabled")
}
node = gotree.New("Public IP settings:") node = gotree.New("Public IP settings:")
if *p.Period == 0 {
node.Appendf("Enabled: no")
return node
}
updatePeriod := "disabled"
if *p.Period > 0 {
updatePeriod = "every " + p.Period.String()
}
node.Appendf("Fetching: %s", updatePeriod)
if *p.IPFilepath != "" { if *p.IPFilepath != "" {
node.Appendf("IP file path: %s", *p.IPFilepath) node.Appendf("IP file path: %s", *p.IPFilepath)
} }
baseAPIString := "Public IP data base API: " + p.APIs[0].Name
if p.APIs[0].Token != "" {
baseAPIString += " (token " + gosettings.ObfuscateKey(p.APIs[0].Token) + ")"
}
node.Append(baseAPIString)
if len(p.APIs) > 1 {
backupAPIsNode := node.Append("Public IP data backup APIs:")
for i := 1; i < len(p.APIs); i++ {
message := p.APIs[i].Name
if p.APIs[i].Token != "" {
message += " (token " + gosettings.ObfuscateKey(p.APIs[i].Token) + ")"
}
backupAPIsNode.Append(message)
}
}
return node return node
} }
func (p *PublicIP) read(r *reader.Reader, warner Warner) (err error) {
p.Enabled, err = readPublicIPEnabled(r, warner)
if err != nil {
return err
}
p.IPFilepath = r.Get("PUBLICIP_FILE",
reader.ForceLowercase(false), reader.RetroKeys("IP_STATUS_FILE"))
apiNames := r.CSV("PUBLICIP_API")
if len(apiNames) > 0 {
apiTokens := r.CSV("PUBLICIP_API_TOKEN")
p.APIs = make([]PublicIPAPI, len(apiNames))
for i := range apiNames {
p.APIs[i].Name = apiNames[i]
var token string
if i < len(apiTokens) { // only set token if it exists
token = apiTokens[i]
}
p.APIs[i].Token = token
}
}
return nil
}
func readPublicIPEnabled(r *reader.Reader, warner Warner) (
enabled *bool, err error,
) {
periodPtr, err := r.DurationPtr("PUBLICIP_PERIOD") // Retro-compatibility
if err != nil {
return nil, err
} else if periodPtr == nil {
return r.BoolPtr("PUBLICIP_ENABLED")
}
if *periodPtr == 0 {
warner.Warn("please replace PUBLICIP_PERIOD=0 with PUBLICIP_ENABLED=no")
return ptrTo(false), nil
}
warner.Warn("PUBLICIP_PERIOD is no longer used. " +
"It is assumed from its non-zero value you want PUBLICIP_ENABLED=yes. " +
"Please migrate to use PUBLICIP_ENABLED only in the future.")
return ptrTo(true), nil
}

View File

@@ -0,0 +1,161 @@
package settings
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gosettings/reader"
"github.com/stretchr/testify/assert"
)
func Test_PublicIP_read(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
makeReader func(ctrl *gomock.Controller) *reader.Reader
makeWarner func(ctrl *gomock.Controller) Warner
settings PublicIP
errWrapped error
errMessage string
}{
"nothing_read": {
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
source := newMockSource(ctrl, []sourceKeyValue{
{key: "PUBLICIP_PERIOD"},
{key: "PUBLICIP_ENABLED"},
{key: "IP_STATUS_FILE"},
{key: "PUBLICIP_FILE"},
{key: "PUBLICIP_API"},
})
return reader.New(reader.Settings{
Sources: []reader.Source{source},
})
},
},
"single_api_no_token": {
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
source := newMockSource(ctrl, []sourceKeyValue{
{key: "PUBLICIP_PERIOD"},
{key: "PUBLICIP_ENABLED"},
{key: "IP_STATUS_FILE"},
{key: "PUBLICIP_FILE"},
{key: "PUBLICIP_API", value: "ipinfo"},
{key: "PUBLICIP_API_TOKEN"},
})
return reader.New(reader.Settings{
Sources: []reader.Source{source},
})
},
settings: PublicIP{
APIs: []PublicIPAPI{
{Name: "ipinfo"},
},
},
},
"single_api_with_token": {
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
source := newMockSource(ctrl, []sourceKeyValue{
{key: "PUBLICIP_PERIOD"},
{key: "PUBLICIP_ENABLED"},
{key: "IP_STATUS_FILE"},
{key: "PUBLICIP_FILE"},
{key: "PUBLICIP_API", value: "ipinfo"},
{key: "PUBLICIP_API_TOKEN", value: "xyz"},
})
return reader.New(reader.Settings{
Sources: []reader.Source{source},
})
},
settings: PublicIP{
APIs: []PublicIPAPI{
{Name: "ipinfo", Token: "xyz"},
},
},
},
"multiple_apis_no_token": {
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
source := newMockSource(ctrl, []sourceKeyValue{
{key: "PUBLICIP_PERIOD"},
{key: "PUBLICIP_ENABLED"},
{key: "IP_STATUS_FILE"},
{key: "PUBLICIP_FILE"},
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
{key: "PUBLICIP_API_TOKEN"},
})
return reader.New(reader.Settings{
Sources: []reader.Source{source},
})
},
settings: PublicIP{
APIs: []PublicIPAPI{
{Name: "ipinfo"},
{Name: "ip2location"},
},
},
},
"multiple_apis_with_token": {
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
source := newMockSource(ctrl, []sourceKeyValue{
{key: "PUBLICIP_PERIOD"},
{key: "PUBLICIP_ENABLED"},
{key: "IP_STATUS_FILE"},
{key: "PUBLICIP_FILE"},
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
{key: "PUBLICIP_API_TOKEN", value: "xyz,abc"},
})
return reader.New(reader.Settings{
Sources: []reader.Source{source},
})
},
settings: PublicIP{
APIs: []PublicIPAPI{
{Name: "ipinfo", Token: "xyz"},
{Name: "ip2location", Token: "abc"},
},
},
},
"multiple_apis_with_and_without_token": {
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
source := newMockSource(ctrl, []sourceKeyValue{
{key: "PUBLICIP_PERIOD"},
{key: "PUBLICIP_ENABLED"},
{key: "IP_STATUS_FILE"},
{key: "PUBLICIP_FILE"},
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
{key: "PUBLICIP_API_TOKEN", value: "xyz"},
})
return reader.New(reader.Settings{
Sources: []reader.Source{source},
})
},
settings: PublicIP{
APIs: []PublicIPAPI{
{Name: "ipinfo", Token: "xyz"},
{Name: "ip2location"},
},
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
reader := testCase.makeReader(ctrl)
var warner Warner
if testCase.makeWarner != nil {
warner = testCase.makeWarner(ctrl)
}
var settings PublicIP
err := settings.read(reader, warner)
assert.Equal(t, testCase.settings, settings)
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
}
})
}
}

View File

@@ -1,12 +1,16 @@
package settings package settings
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"net" "net"
"os" "os"
"strconv" "strconv"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/server/middlewares/auth"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -18,6 +22,14 @@ type ControlServer struct {
// Log can be true or false to enable logging on requests. // Log can be true or false to enable logging on requests.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
Log *bool Log *bool
// AuthFilePath is the path to the file containing the authentication
// configuration for the middleware.
// 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) { func (c ControlServer) validate() (err error) {
@@ -38,34 +50,55 @@ func (c ControlServer) validate() (err error) {
ErrControlServerPrivilegedPort, port, uid) 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 return nil
} }
func (c *ControlServer) copy() (copied ControlServer) { func (c *ControlServer) copy() (copied ControlServer) {
return ControlServer{ return ControlServer{
Address: helpers.CopyStringPtr(c.Address), Address: gosettings.CopyPointer(c.Address),
Log: helpers.CopyBoolPtr(c.Log), Log: gosettings.CopyPointer(c.Log),
AuthFilePath: c.AuthFilePath,
AuthDefaultRole: c.AuthDefaultRole,
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (c *ControlServer) mergeWith(other ControlServer) {
c.Address = helpers.MergeWithStringPtr(c.Address, other.Address)
c.Log = helpers.MergeWithBool(c.Log, other.Log)
}
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (c *ControlServer) overrideWith(other ControlServer) { func (c *ControlServer) overrideWith(other ControlServer) {
c.Address = helpers.OverrideWithStringPtr(c.Address, other.Address) c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
c.Log = helpers.OverrideWithBool(c.Log, other.Log) 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() { func (c *ControlServer) setDefaults() {
c.Address = helpers.DefaultStringPtr(c.Address, ":8000") c.Address = gosettings.DefaultPointer(c.Address, ":8000")
c.Log = helpers.DefaultBool(c.Log, true) 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 { func (c ControlServer) String() string {
@@ -75,6 +108,26 @@ func (c ControlServer) String() string {
func (c ControlServer) toLinesNode() (node *gotree.Node) { func (c ControlServer) toLinesNode() (node *gotree.Node) {
node = gotree.New("Control server settings:") node = gotree.New("Control server settings:")
node.Appendf("Listening address: %s", *c.Address) node.Appendf("Listening address: %s", *c.Address)
node.Appendf("Logging: %s", helpers.BoolPtrToYesNo(c.Log)) 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 return node
} }
func (c *ControlServer) read(r *reader.Reader) (err error) {
c.Log, err = r.BoolPtr("HTTP_CONTROL_SERVER_LOG")
if err != nil {
return err
}
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")
return nil
}

View File

@@ -3,123 +3,131 @@ package settings
import ( import (
"errors" "errors"
"fmt" "fmt"
"net" "net/netip"
"strings" "strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/configuration/settings/validation" "github.com/qdm12/gluetun/internal/configuration/settings/validation"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
type ServerSelection struct { //nolint:maligned type ServerSelection struct {
// VPN is the VPN type which can be 'openvpn' // VPN is the VPN type which can be 'openvpn'
// or 'wireguard'. It cannot be the empty string // or 'wireguard'. It cannot be the empty string
// in the internal state. // in the internal state.
VPN string VPN string `json:"vpn"`
// TargetIP is the server endpoint IP address to use. // TargetIP is the server endpoint IP address to use.
// It will override any IP address from the picked // It will override any IP address from the picked
// built-in server. It cannot be nil in the internal // built-in server. It cannot be the empty value in the internal
// state, and can be set to an empty net.IP{} to indicate // state, and can be set to the unspecified address to indicate
// there is not target IP address to use. // there is not target IP address to use.
TargetIP net.IP TargetIP netip.Addr `json:"target_ip"`
// Counties is the list of countries to filter VPN servers with. // Countries is the list of countries to filter VPN servers with.
Countries []string Countries []string `json:"countries"`
// Categories is the list of categories to filter VPN servers with.
Categories []string `json:"categories"`
// Regions is the list of regions to filter VPN servers with. // Regions is the list of regions to filter VPN servers with.
Regions []string Regions []string `json:"regions"`
// Cities is the list of cities to filter VPN servers with. // Cities is the list of cities to filter VPN servers with.
Cities []string Cities []string `json:"cities"`
// ISPs is the list of ISP names to filter VPN servers with. // ISPs is the list of ISP names to filter VPN servers with.
ISPs []string ISPs []string `json:"isps"`
// Names is the list of server names to filter VPN servers with. // Names is the list of server names to filter VPN servers with.
Names []string Names []string `json:"names"`
// Numbers is the list of server numbers to filter VPN servers with. // Numbers is the list of server numbers to filter VPN servers with.
Numbers []uint16 Numbers []uint16 `json:"numbers"`
// Hostnames is the list of hostnames to filter VPN servers with. // Hostnames is the list of hostnames to filter VPN servers with.
Hostnames []string Hostnames []string `json:"hostnames"`
// OwnedOnly is true if VPN provider servers that are not owned // OwnedOnly is true if VPN provider servers that are not owned
// should be filtered. This is used with Mullvad. // should be filtered. This is used with Mullvad.
OwnedOnly *bool OwnedOnly *bool `json:"owned_only"`
// FreeOnly is true if VPN servers that are not free should // FreeOnly is true if VPN servers that are not free should
// be filtered. This is used with ProtonVPN and VPN Unlimited. // be filtered. This is used with ProtonVPN and VPN Unlimited.
FreeOnly *bool FreeOnly *bool `json:"free_only"`
// PremiumOnly is true if VPN servers that are not premium should
// be filtered. This is used with VPN Secure.
// TODO extend to providers using FreeOnly.
PremiumOnly *bool `json:"premium_only"`
// StreamOnly is true if VPN servers not for streaming should // StreamOnly is true if VPN servers not for streaming should
// be filtered. This is used with VPNUnlimited. // be filtered. This is used with ProtonVPN and VPNUnlimited.
StreamOnly *bool StreamOnly *bool `json:"stream_only"`
// MultiHopOnly is true if VPN servers that are not multihop // MultiHopOnly is true if VPN servers that are not multihop
// should be filtered. This is used with Surfshark. // should be filtered. This is used with Surfshark.
MultiHopOnly *bool MultiHopOnly *bool `json:"multi_hop_only"`
// PortForwardOnly is true if VPN servers that don't support
// port forwarding should be filtered. This is used with PIA
// and ProtonVPN.
PortForwardOnly *bool `json:"port_forward_only"`
// SecureCoreOnly is true if VPN servers without secure core should
// be filtered. This is used with ProtonVPN.
SecureCoreOnly *bool `json:"secure_core_only"`
// TorOnly is true if VPN servers without tor should
// be filtered. This is used with ProtonVPN.
TorOnly *bool `json:"tor_only"`
// OpenVPN contains settings to select OpenVPN servers // OpenVPN contains settings to select OpenVPN servers
// and the final connection. // and the final connection.
OpenVPN OpenVPNSelection OpenVPN OpenVPNSelection `json:"openvpn"`
// Wireguard contains settings to select Wireguard servers // Wireguard contains settings to select Wireguard servers
// and the final connection. // and the final connection.
Wireguard WireguardSelection Wireguard WireguardSelection `json:"wireguard"`
} }
var ( var (
ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported") ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported")
ErrFreeOnlyNotSupported = errors.New("free only filter is not supported") ErrFreeOnlyNotSupported = errors.New("free only filter is not supported")
ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported") ErrPremiumOnlyNotSupported = errors.New("premium only filter is not supported")
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported") ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported")
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
ErrPortForwardOnlyNotSupported = errors.New("port forwarding only filter is not supported")
ErrFreePremiumBothSet = errors.New("free only and premium only filters are both set")
ErrSecureCoreOnlyNotSupported = errors.New("secure core only filter is not supported")
ErrTorOnlyNotSupported = errors.New("tor only filter is not supported")
) )
func (ss *ServerSelection) validate(vpnServiceProvider string, func (ss *ServerSelection) validate(vpnServiceProvider string,
allServers models.AllServers) (err error) { filterChoicesGetter FilterChoicesGetter, warner Warner,
) (err error) {
switch ss.VPN { switch ss.VPN {
case constants.OpenVPN, constants.Wireguard: case vpn.OpenVPN, vpn.Wireguard:
default: default:
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN) return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
} }
countryChoices, regionChoices, cityChoices, filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, filterChoicesGetter, warner)
ispChoices, nameChoices, hostnameChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, allServers)
if err != nil { if err != nil {
return err // already wrapped error return err // already wrapped error
} }
err = validateServerFilters(*ss, countryChoices, regionChoices, cityChoices, // Retro-compatibility
ispChoices, nameChoices, hostnameChoices) switch vpnServiceProvider {
case providers.Nordvpn:
*ss = nordvpnRetroRegion(*ss, filterChoices.Regions, filterChoices.Countries)
case providers.Surfshark:
*ss = surfsharkRetroRegion(*ss)
}
err = validateServerFilters(*ss, filterChoices, vpnServiceProvider, warner)
if err != nil { if err != nil {
if errors.Is(err, helpers.ErrNoChoice) { return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
}
return err // already wrapped error
} }
if *ss.OwnedOnly && err = validateSubscriptionTierFilters(*ss, vpnServiceProvider)
vpnServiceProvider != constants.Mullvad { if err != nil {
return fmt.Errorf("%w: for VPN service provider %s", return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
ErrOwnedOnlyNotSupported, vpnServiceProvider)
} }
if *ss.FreeOnly && err = validateFeatureFilters(*ss, vpnServiceProvider)
!helpers.IsOneOf(vpnServiceProvider, if err != nil {
constants.Protonvpn, return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
constants.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrFreeOnlyNotSupported, vpnServiceProvider)
} }
if *ss.StreamOnly && if ss.VPN == vpn.OpenVPN {
!helpers.IsOneOf(vpnServiceProvider,
constants.Protonvpn,
constants.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrStreamOnlyNotSupported, vpnServiceProvider)
}
if *ss.MultiHopOnly &&
vpnServiceProvider != constants.Surfshark {
return fmt.Errorf("%w: for VPN service provider %s",
ErrMultiHopOnlyNotSupported, vpnServiceProvider)
}
if ss.VPN == constants.OpenVPN {
err = ss.OpenVPN.validate(vpnServiceProvider) err = ss.OpenVPN.validate(vpnServiceProvider)
if err != nil { if err != nil {
return fmt.Errorf("OpenVPN server selection settings: %w", err) return fmt.Errorf("OpenVPN server selection settings: %w", err)
@@ -134,225 +142,221 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
return nil return nil
} }
func getLocationFilterChoices(vpnServiceProvider string, ss *ServerSelection, func getLocationFilterChoices(vpnServiceProvider string,
allServers models.AllServers) ( ss *ServerSelection, filterChoicesGetter FilterChoicesGetter, warner Warner) (
countryChoices, regionChoices, cityChoices, filterChoices models.FilterChoices, err error,
ispChoices, nameChoices, hostnameChoices []string, ) {
err error) { filterChoices = filterChoicesGetter.GetFilterChoices(vpnServiceProvider)
switch vpnServiceProvider {
case constants.Custom: if vpnServiceProvider == providers.Surfshark {
case constants.Cyberghost: // // Retro compatibility
servers := allServers.GetCyberghost()
countryChoices = validation.CyberghostCountryChoices(servers)
hostnameChoices = validation.CyberghostHostnameChoices(servers)
case constants.Expressvpn:
servers := allServers.GetExpressvpn()
countryChoices = validation.ExpressvpnCountriesChoices(servers)
cityChoices = validation.ExpressvpnCityChoices(servers)
hostnameChoices = validation.ExpressvpnHostnameChoices(servers)
case constants.Fastestvpn:
servers := allServers.GetFastestvpn()
countryChoices = validation.FastestvpnCountriesChoices(servers)
hostnameChoices = validation.FastestvpnHostnameChoices(servers)
case constants.HideMyAss:
servers := allServers.GetHideMyAss()
countryChoices = validation.HideMyAssCountryChoices(servers)
regionChoices = validation.HideMyAssRegionChoices(servers)
cityChoices = validation.HideMyAssCityChoices(servers)
hostnameChoices = validation.HideMyAssHostnameChoices(servers)
case constants.Ipvanish:
servers := allServers.GetIpvanish()
countryChoices = validation.IpvanishCountryChoices(servers)
cityChoices = validation.IpvanishCityChoices(servers)
hostnameChoices = validation.IpvanishHostnameChoices(servers)
case constants.Ivpn:
servers := allServers.GetIvpn()
countryChoices = validation.IvpnCountryChoices(servers)
cityChoices = validation.IvpnCityChoices(servers)
ispChoices = validation.IvpnISPChoices(servers)
hostnameChoices = validation.IvpnHostnameChoices(servers)
case constants.Mullvad:
servers := allServers.GetMullvad()
countryChoices = validation.MullvadCountryChoices(servers)
cityChoices = validation.MullvadCityChoices(servers)
ispChoices = validation.MullvadISPChoices(servers)
hostnameChoices = validation.MullvadHostnameChoices(servers)
case constants.Nordvpn:
servers := allServers.GetNordvpn()
regionChoices = validation.NordvpnRegionChoices(servers)
hostnameChoices = validation.NordvpnHostnameChoices(servers)
case constants.Perfectprivacy:
servers := allServers.GetPerfectprivacy()
cityChoices = validation.PerfectprivacyCityChoices(servers)
case constants.Privado:
servers := allServers.GetPrivado()
countryChoices = validation.PrivadoCountryChoices(servers)
regionChoices = validation.PrivadoRegionChoices(servers)
cityChoices = validation.PrivadoCityChoices(servers)
hostnameChoices = validation.PrivadoHostnameChoices(servers)
case constants.PrivateInternetAccess:
servers := allServers.GetPia()
regionChoices = validation.PIAGeoChoices(servers)
hostnameChoices = validation.PIAHostnameChoices(servers)
nameChoices = validation.PIANameChoices(servers)
case constants.Privatevpn:
servers := allServers.GetPrivatevpn()
countryChoices = validation.PrivatevpnCountryChoices(servers)
cityChoices = validation.PrivatevpnCityChoices(servers)
hostnameChoices = validation.PrivatevpnHostnameChoices(servers)
case constants.Protonvpn:
servers := allServers.GetProtonvpn()
countryChoices = validation.ProtonvpnCountryChoices(servers)
regionChoices = validation.ProtonvpnRegionChoices(servers)
cityChoices = validation.ProtonvpnCityChoices(servers)
nameChoices = validation.ProtonvpnNameChoices(servers)
hostnameChoices = validation.ProtonvpnHostnameChoices(servers)
case constants.Purevpn:
servers := allServers.GetPurevpn()
countryChoices = validation.PurevpnCountryChoices(servers)
regionChoices = validation.PurevpnRegionChoices(servers)
cityChoices = validation.PurevpnCityChoices(servers)
hostnameChoices = validation.PurevpnHostnameChoices(servers)
case constants.Surfshark:
servers := allServers.GetSurfshark()
countryChoices = validation.SurfsharkCountryChoices(servers)
cityChoices = validation.SurfsharkCityChoices(servers)
hostnameChoices = validation.SurfsharkHostnameChoices(servers)
regionChoices = validation.SurfsharkRegionChoices(servers)
// TODO v4 remove // TODO v4 remove
regionChoices = append(regionChoices, validation.SurfsharkRetroLocChoices()...) newAndRetroRegions := append(filterChoices.Regions, validation.SurfsharkRetroLocChoices()...) //nolint:gocritic
if err := helpers.AreAllOneOf(ss.Regions, regionChoices); err != nil { err := atLeastOneIsOneOfCaseInsensitive(ss.Regions, newAndRetroRegions, warner)
return nil, nil, nil, nil, nil, nil, fmt.Errorf("%w: %s", ErrRegionNotValid, err) if err != nil {
// Only return error comparing with newer regions, we don't want to confuse the user
// with the retro regions in the error message.
err = atLeastOneIsOneOfCaseInsensitive(ss.Regions, filterChoices.Regions, warner)
return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err)
} }
// Retro compatibility
// TODO remove in v4
*ss = surfsharkRetroRegion(*ss)
case constants.Torguard:
servers := allServers.GetTorguard()
countryChoices = validation.TorguardCountryChoices(servers)
cityChoices = validation.TorguardCityChoices(servers)
hostnameChoices = validation.TorguardHostnameChoices(servers)
case constants.VPNUnlimited:
servers := allServers.GetVPNUnlimited()
countryChoices = validation.VPNUnlimitedCountryChoices(servers)
cityChoices = validation.VPNUnlimitedCityChoices(servers)
hostnameChoices = validation.VPNUnlimitedHostnameChoices(servers)
case constants.Vyprvpn:
servers := allServers.GetVyprvpn()
regionChoices = validation.VyprvpnRegionChoices(servers)
case constants.Wevpn:
servers := allServers.GetWevpn()
cityChoices = validation.WevpnCityChoices(servers)
hostnameChoices = validation.WevpnHostnameChoices(servers)
case constants.Windscribe:
servers := allServers.GetWindscribe()
regionChoices = validation.WindscribeRegionChoices(servers)
cityChoices = validation.WindscribeCityChoices(servers)
hostnameChoices = validation.WindscribeHostnameChoices(servers)
default:
return nil, nil, nil, nil, nil, nil, fmt.Errorf("%w: %s", ErrVPNProviderNameNotValid, vpnServiceProvider)
} }
return countryChoices, regionChoices, cityChoices, return filterChoices, nil
ispChoices, nameChoices, hostnameChoices, nil
} }
// validateServerFilters validates filters against the choices given as arguments. // validateServerFilters validates filters against the choices given as arguments.
// Set an argument to nil to pass the check for a particular filter. // Set an argument to nil to pass the check for a particular filter.
func validateServerFilters(settings ServerSelection, func validateServerFilters(settings ServerSelection, filterChoices models.FilterChoices,
countryChoices, regionChoices, cityChoices, ispChoices, vpnServiceProvider string, warner Warner,
nameChoices, hostnameChoices []string) (err error) { ) (err error) {
if err := helpers.AreAllOneOf(settings.Countries, countryChoices); err != nil { err = atLeastOneIsOneOfCaseInsensitive(settings.Countries, filterChoices.Countries, warner)
return fmt.Errorf("%w: %s", ErrCountryNotValid, err) if err != nil {
return fmt.Errorf("%w: %w", ErrCountryNotValid, err)
} }
if err := helpers.AreAllOneOf(settings.Regions, regionChoices); err != nil { err = atLeastOneIsOneOfCaseInsensitive(settings.Regions, filterChoices.Regions, warner)
return fmt.Errorf("%w: %s", ErrRegionNotValid, err) if err != nil {
return fmt.Errorf("%w: %w", ErrRegionNotValid, err)
} }
if err := helpers.AreAllOneOf(settings.Cities, cityChoices); err != nil { err = atLeastOneIsOneOfCaseInsensitive(settings.Cities, filterChoices.Cities, warner)
return fmt.Errorf("%w: %s", ErrCityNotValid, err) if err != nil {
return fmt.Errorf("%w: %w", ErrCityNotValid, err)
} }
if err := helpers.AreAllOneOf(settings.ISPs, ispChoices); err != nil { err = atLeastOneIsOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs, warner)
return fmt.Errorf("%w: %s", ErrISPNotValid, err) if err != nil {
return fmt.Errorf("%w: %w", ErrISPNotValid, err)
} }
if err := helpers.AreAllOneOf(settings.Hostnames, hostnameChoices); err != nil { err = atLeastOneIsOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames, warner)
return fmt.Errorf("%w: %s", ErrHostnameNotValid, err) if err != nil {
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err)
} }
if err := helpers.AreAllOneOf(settings.Names, nameChoices); err != nil { if vpnServiceProvider == providers.Custom {
return fmt.Errorf("%w: %s", ErrNameNotValid, err) switch len(settings.Names) {
case 0:
case 1:
// Allow a single name to be specified for the custom provider in case
// the user wants to use VPN server side port forwarding with PIA
// which requires a server name for TLS verification.
filterChoices.Names = settings.Names
default:
return fmt.Errorf("%w: %d names specified instead of "+
"0 or 1 for the custom provider",
ErrNameNotValid, len(settings.Names))
}
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Names, filterChoices.Names, warner)
if err != nil {
return fmt.Errorf("%w: %w", ErrNameNotValid, err)
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Categories, filterChoices.Categories, warner)
if err != nil {
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
} }
return nil return nil
} }
func (ss *ServerSelection) copy() (copied ServerSelection) { func atLeastOneIsOneOfCaseInsensitive(values, choices []string,
return ServerSelection{ warner Warner,
VPN: ss.VPN, ) (err error) {
TargetIP: helpers.CopyIP(ss.TargetIP), if len(values) > 0 && len(choices) == 0 {
Countries: helpers.CopyStringSlice(ss.Countries), return fmt.Errorf("%w", validate.ErrNoChoice)
Regions: helpers.CopyStringSlice(ss.Regions), }
Cities: helpers.CopyStringSlice(ss.Cities),
ISPs: helpers.CopyStringSlice(ss.ISPs), set := make(map[string]struct{}, len(choices))
Hostnames: helpers.CopyStringSlice(ss.Hostnames), for _, choice := range choices {
Names: helpers.CopyStringSlice(ss.Names), lowercaseChoice := strings.ToLower(choice)
Numbers: helpers.CopyUint16Slice(ss.Numbers), set[lowercaseChoice] = struct{}{}
OwnedOnly: helpers.CopyBoolPtr(ss.OwnedOnly), }
FreeOnly: helpers.CopyBoolPtr(ss.FreeOnly),
StreamOnly: helpers.CopyBoolPtr(ss.StreamOnly), invalidValues := make([]string, 0, len(values))
MultiHopOnly: helpers.CopyBoolPtr(ss.MultiHopOnly), for _, value := range values {
OpenVPN: ss.OpenVPN.copy(), lowercaseValue := strings.ToLower(value)
Wireguard: ss.Wireguard.copy(), _, ok := set[lowercaseValue]
if ok {
continue
}
invalidValues = append(invalidValues, value)
}
switch len(invalidValues) {
case 0:
return nil
case len(values):
return fmt.Errorf("%w: none of %s is one of the choices available %s",
validate.ErrValueNotOneOf, strings.Join(values, ", "), strings.Join(choices, ", "))
default:
warner.Warn(fmt.Sprintf("values %s are not in choices %s",
strings.Join(invalidValues, ", "), strings.Join(choices, ", ")))
}
return nil
}
func validateSubscriptionTierFilters(settings ServerSelection, vpnServiceProvider string) error {
switch {
case *settings.FreeOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
return fmt.Errorf("%w", ErrFreeOnlyNotSupported)
case *settings.PremiumOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.VPNSecure):
return fmt.Errorf("%w", ErrPremiumOnlyNotSupported)
case *settings.FreeOnly && *settings.PremiumOnly:
return fmt.Errorf("%w", ErrFreePremiumBothSet)
default:
return nil
} }
} }
func (ss *ServerSelection) mergeWith(other ServerSelection) { func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string) error {
ss.VPN = helpers.MergeWithString(ss.VPN, other.VPN) switch {
ss.TargetIP = helpers.MergeWithIP(ss.TargetIP, other.TargetIP) case *settings.OwnedOnly && vpnServiceProvider != providers.Mullvad:
ss.Countries = helpers.MergeStringSlices(ss.Countries, other.Countries) return fmt.Errorf("%w", ErrOwnedOnlyNotSupported)
ss.Regions = helpers.MergeStringSlices(ss.Regions, other.Regions) case vpnServiceProvider == providers.Protonvpn && *settings.FreeOnly && *settings.PortForwardOnly:
ss.Cities = helpers.MergeStringSlices(ss.Cities, other.Cities) return fmt.Errorf("%w: together with free only filter", ErrPortForwardOnlyNotSupported)
ss.ISPs = helpers.MergeStringSlices(ss.ISPs, other.ISPs) case *settings.StreamOnly &&
ss.Hostnames = helpers.MergeStringSlices(ss.Hostnames, other.Hostnames) !helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
ss.Names = helpers.MergeStringSlices(ss.Names, other.Names) return fmt.Errorf("%w", ErrStreamOnlyNotSupported)
ss.Numbers = helpers.MergeUint16Slices(ss.Numbers, other.Numbers) case *settings.MultiHopOnly && vpnServiceProvider != providers.Surfshark:
ss.OwnedOnly = helpers.MergeWithBool(ss.OwnedOnly, other.OwnedOnly) return fmt.Errorf("%w", ErrMultiHopOnlyNotSupported)
ss.FreeOnly = helpers.MergeWithBool(ss.FreeOnly, other.FreeOnly) case *settings.PortForwardOnly &&
ss.StreamOnly = helpers.MergeWithBool(ss.StreamOnly, other.StreamOnly) !helpers.IsOneOf(vpnServiceProvider, providers.PrivateInternetAccess, providers.Protonvpn):
ss.MultiHopOnly = helpers.MergeWithBool(ss.MultiHopOnly, other.MultiHopOnly) return fmt.Errorf("%w", ErrPortForwardOnlyNotSupported)
case *settings.SecureCoreOnly && vpnServiceProvider != providers.Protonvpn:
return fmt.Errorf("%w", ErrSecureCoreOnlyNotSupported)
case *settings.TorOnly && vpnServiceProvider != providers.Protonvpn:
return fmt.Errorf("%w", ErrTorOnlyNotSupported)
default:
return nil
}
}
ss.OpenVPN.mergeWith(other.OpenVPN) func (ss *ServerSelection) copy() (copied ServerSelection) {
ss.Wireguard.mergeWith(other.Wireguard) return ServerSelection{
VPN: ss.VPN,
TargetIP: ss.TargetIP,
Countries: gosettings.CopySlice(ss.Countries),
Categories: gosettings.CopySlice(ss.Categories),
Regions: gosettings.CopySlice(ss.Regions),
Cities: gosettings.CopySlice(ss.Cities),
ISPs: gosettings.CopySlice(ss.ISPs),
Hostnames: gosettings.CopySlice(ss.Hostnames),
Names: gosettings.CopySlice(ss.Names),
Numbers: gosettings.CopySlice(ss.Numbers),
OwnedOnly: gosettings.CopyPointer(ss.OwnedOnly),
FreeOnly: gosettings.CopyPointer(ss.FreeOnly),
PremiumOnly: gosettings.CopyPointer(ss.PremiumOnly),
StreamOnly: gosettings.CopyPointer(ss.StreamOnly),
SecureCoreOnly: gosettings.CopyPointer(ss.SecureCoreOnly),
TorOnly: gosettings.CopyPointer(ss.TorOnly),
PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly),
MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly),
OpenVPN: ss.OpenVPN.copy(),
Wireguard: ss.Wireguard.copy(),
}
} }
func (ss *ServerSelection) overrideWith(other ServerSelection) { func (ss *ServerSelection) overrideWith(other ServerSelection) {
ss.VPN = helpers.OverrideWithString(ss.VPN, other.VPN) ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN)
ss.TargetIP = helpers.OverrideWithIP(ss.TargetIP, other.TargetIP) ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP)
ss.Countries = helpers.OverrideWithStringSlice(ss.Countries, other.Countries) ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
ss.Regions = helpers.OverrideWithStringSlice(ss.Regions, other.Regions) ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
ss.Cities = helpers.OverrideWithStringSlice(ss.Cities, other.Cities) ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions)
ss.ISPs = helpers.OverrideWithStringSlice(ss.ISPs, other.ISPs) ss.Cities = gosettings.OverrideWithSlice(ss.Cities, other.Cities)
ss.Hostnames = helpers.OverrideWithStringSlice(ss.Hostnames, other.Hostnames) ss.ISPs = gosettings.OverrideWithSlice(ss.ISPs, other.ISPs)
ss.Names = helpers.OverrideWithStringSlice(ss.Names, other.Names) ss.Hostnames = gosettings.OverrideWithSlice(ss.Hostnames, other.Hostnames)
ss.Numbers = helpers.OverrideWithUint16Slice(ss.Numbers, other.Numbers) ss.Names = gosettings.OverrideWithSlice(ss.Names, other.Names)
ss.OwnedOnly = helpers.OverrideWithBool(ss.OwnedOnly, other.OwnedOnly) ss.Numbers = gosettings.OverrideWithSlice(ss.Numbers, other.Numbers)
ss.FreeOnly = helpers.OverrideWithBool(ss.FreeOnly, other.FreeOnly) ss.OwnedOnly = gosettings.OverrideWithPointer(ss.OwnedOnly, other.OwnedOnly)
ss.StreamOnly = helpers.OverrideWithBool(ss.StreamOnly, other.StreamOnly) ss.FreeOnly = gosettings.OverrideWithPointer(ss.FreeOnly, other.FreeOnly)
ss.MultiHopOnly = helpers.OverrideWithBool(ss.MultiHopOnly, other.MultiHopOnly) ss.PremiumOnly = gosettings.OverrideWithPointer(ss.PremiumOnly, other.PremiumOnly)
ss.StreamOnly = gosettings.OverrideWithPointer(ss.StreamOnly, other.StreamOnly)
ss.SecureCoreOnly = gosettings.OverrideWithPointer(ss.SecureCoreOnly, other.SecureCoreOnly)
ss.TorOnly = gosettings.OverrideWithPointer(ss.TorOnly, other.TorOnly)
ss.MultiHopOnly = gosettings.OverrideWithPointer(ss.MultiHopOnly, other.MultiHopOnly)
ss.PortForwardOnly = gosettings.OverrideWithPointer(ss.PortForwardOnly, other.PortForwardOnly)
ss.OpenVPN.overrideWith(other.OpenVPN) ss.OpenVPN.overrideWith(other.OpenVPN)
ss.Wireguard.overrideWith(other.Wireguard) ss.Wireguard.overrideWith(other.Wireguard)
} }
func (ss *ServerSelection) setDefaults(vpnProvider string) { func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) {
ss.VPN = helpers.DefaultString(ss.VPN, constants.OpenVPN) ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
ss.TargetIP = helpers.DefaultIP(ss.TargetIP, net.IP{}) ss.TargetIP = gosettings.DefaultValidator(ss.TargetIP, netip.IPv4Unspecified())
ss.OwnedOnly = helpers.DefaultBool(ss.OwnedOnly, false) ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
ss.FreeOnly = helpers.DefaultBool(ss.FreeOnly, false) ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
ss.StreamOnly = helpers.DefaultBool(ss.StreamOnly, false) ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
ss.MultiHopOnly = helpers.DefaultBool(ss.MultiHopOnly, false) ss.StreamOnly = gosettings.DefaultPointer(ss.StreamOnly, false)
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
defaultPortForwardOnly := portForwardingEnabled &&
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
ss.OpenVPN.setDefaults(vpnProvider) ss.OpenVPN.setDefaults(vpnProvider)
ss.Wireguard.setDefaults() ss.Wireguard.setDefaults()
} }
@@ -364,7 +368,7 @@ func (ss ServerSelection) String() string {
func (ss ServerSelection) toLinesNode() (node *gotree.Node) { func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("Server selection settings:") node = gotree.New("Server selection settings:")
node.Appendf("VPN type: %s", ss.VPN) node.Appendf("VPN type: %s", ss.VPN)
if len(ss.TargetIP) > 0 { if !ss.TargetIP.IsUnspecified() {
node.Appendf("Target IP address: %s", ss.TargetIP) node.Appendf("Target IP address: %s", ss.TargetIP)
} }
@@ -372,6 +376,10 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", ")) node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
} }
if len(ss.Categories) > 0 {
node.Appendf("Categories: %s", strings.Join(ss.Categories, ", "))
}
if len(ss.Regions) > 0 { if len(ss.Regions) > 0 {
node.Appendf("Regions: %s", strings.Join(ss.Regions, ", ")) node.Appendf("Regions: %s", strings.Join(ss.Regions, ", "))
} }
@@ -407,15 +415,31 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node.Appendf("Free only servers: yes") node.Appendf("Free only servers: yes")
} }
if *ss.PremiumOnly {
node.Appendf("Premium only servers: yes")
}
if *ss.StreamOnly { if *ss.StreamOnly {
node.Appendf("Stream only servers: yes") node.Appendf("Stream only servers: yes")
} }
if *ss.SecureCoreOnly {
node.Appendf("Secure Core only servers: yes")
}
if *ss.TorOnly {
node.Appendf("Tor only servers: yes")
}
if *ss.MultiHopOnly { if *ss.MultiHopOnly {
node.Appendf("Multi-hop only servers: yes") node.Appendf("Multi-hop only servers: yes")
} }
if ss.VPN == constants.OpenVPN { if *ss.PortForwardOnly {
node.Appendf("Port forwarding only servers: yes")
}
if ss.VPN == vpn.OpenVPN {
node.AppendNode(ss.OpenVPN.toLinesNode()) node.AppendNode(ss.OpenVPN.toLinesNode())
} else { } else {
node.AppendNode(ss.Wireguard.toLinesNode()) node.AppendNode(ss.Wireguard.toLinesNode())
@@ -427,6 +451,96 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
// WithDefaults is a shorthand using setDefaults. // WithDefaults is a shorthand using setDefaults.
// It's used in unit tests in other packages. // It's used in unit tests in other packages.
func (ss ServerSelection) WithDefaults(provider string) ServerSelection { func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
ss.setDefaults(provider) const portForwardingEnabled = false
ss.setDefaults(provider, portForwardingEnabled)
return ss return ss
} }
func (ss *ServerSelection) read(r *reader.Reader,
vpnProvider, vpnType string,
) (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")
}
ss.Countries = r.CSV("SERVER_COUNTRIES", reader.RetroKeys(countriesRetroKeys...))
ss.Regions = r.CSV("SERVER_REGIONS", reader.RetroKeys("REGION"))
ss.Cities = r.CSV("SERVER_CITIES", reader.RetroKeys("CITY"))
ss.ISPs = r.CSV("ISP")
ss.Hostnames = r.CSV("SERVER_HOSTNAMES", reader.RetroKeys("SERVER_HOSTNAME"))
ss.Names = r.CSV("SERVER_NAMES", reader.RetroKeys("SERVER_NAME"))
ss.Numbers, err = r.CSVUint16("SERVER_NUMBER")
ss.Categories = r.CSV("SERVER_CATEGORIES")
if err != nil {
return err
}
// Mullvad only
ss.OwnedOnly, err = r.BoolPtr("OWNED_ONLY", reader.RetroKeys("OWNED"))
if err != nil {
return err
}
// VPNUnlimited and ProtonVPN only
ss.FreeOnly, err = r.BoolPtr("FREE_ONLY")
if err != nil {
return err
}
// VPNSecure only
ss.PremiumOnly, err = r.BoolPtr("PREMIUM_ONLY")
if err != nil {
return err
}
// Surfshark only
ss.MultiHopOnly, err = r.BoolPtr("MULTIHOP_ONLY")
if err != nil {
return err
}
// VPNUnlimited and ProtonVPN only
ss.StreamOnly, err = r.BoolPtr("STREAM_ONLY")
if err != nil {
return err
}
// ProtonVPN only
ss.SecureCoreOnly, err = r.BoolPtr("SECURE_CORE_ONLY")
if err != nil {
return err
}
// ProtonVPN only
ss.TorOnly, err = r.BoolPtr("TOR_ONLY")
if err != nil {
return err
}
// PIA and ProtonVPN only
ss.PortForwardOnly, err = r.BoolPtr("PORT_FORWARD_ONLY")
if err != nil {
return err
}
err = ss.OpenVPN.read(r)
if err != nil {
return err
}
err = ss.Wireguard.read(r)
if err != nil {
return err
}
return nil
}

View File

@@ -3,8 +3,12 @@ package settings
import ( import (
"fmt" "fmt"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/pprof" "github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -17,6 +21,7 @@ type Settings struct {
Log Log Log Log
PublicIP PublicIP PublicIP PublicIP
Shadowsocks Shadowsocks Shadowsocks Shadowsocks
Storage Storage
System System System System
Updater Updater Updater Updater
Version Version Version Version
@@ -24,10 +29,16 @@ type Settings struct {
Pprof pprof.Settings Pprof pprof.Settings
} }
type FilterChoicesGetter interface {
GetFilterChoices(provider string) models.FilterChoices
}
// Validate validates all the settings and returns an error // Validate validates all the settings and returns an error
// if one of them is not valid. // if one of them is not valid.
// TODO v4 remove pointer for receiver (because of Surfshark). // TODO v4 remove pointer for receiver (because of Surfshark).
func (s *Settings) Validate(allServers models.AllServers) (err error) { func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool,
warner Warner,
) (err error) {
nameToValidation := map[string]func() error{ nameToValidation := map[string]func() error{
"control server": s.ControlServer.validate, "control server": s.ControlServer.validate,
"dns": s.DNS.validate, "dns": s.DNS.validate,
@@ -37,12 +48,13 @@ func (s *Settings) Validate(allServers models.AllServers) (err error) {
"log": s.Log.validate, "log": s.Log.validate,
"public ip check": s.PublicIP.validate, "public ip check": s.PublicIP.validate,
"shadowsocks": s.Shadowsocks.validate, "shadowsocks": s.Shadowsocks.validate,
"storage": s.Storage.validate,
"system": s.System.validate, "system": s.System.validate,
"updater": s.Updater.Validate, "updater": s.Updater.Validate,
"version": s.Version.validate, "version": s.Version.validate,
// Pprof validation done in pprof constructor // Pprof validation done in pprof constructor
"VPN": func() error { "VPN": func() error {
return s.VPN.validate(allServers) return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
}, },
} }
@@ -66,32 +78,18 @@ func (s *Settings) copy() (copied Settings) {
Log: s.Log.copy(), Log: s.Log.copy(),
PublicIP: s.PublicIP.copy(), PublicIP: s.PublicIP.copy(),
Shadowsocks: s.Shadowsocks.copy(), Shadowsocks: s.Shadowsocks.copy(),
Storage: s.Storage.copy(),
System: s.System.copy(), System: s.System.copy(),
Updater: s.Updater.copy(), Updater: s.Updater.copy(),
Version: s.Version.copy(), Version: s.Version.copy(),
VPN: s.VPN.copy(), VPN: s.VPN.Copy(),
Pprof: s.Pprof.Copy(), Pprof: s.Pprof.Copy(),
} }
} }
func (s *Settings) MergeWith(other Settings) {
s.ControlServer.mergeWith(other.ControlServer)
s.DNS.mergeWith(other.DNS)
s.Firewall.mergeWith(other.Firewall)
s.Health.MergeWith(other.Health)
s.HTTPProxy.mergeWith(other.HTTPProxy)
s.Log.mergeWith(other.Log)
s.PublicIP.mergeWith(other.PublicIP)
s.Shadowsocks.mergeWith(other.Shadowsocks)
s.System.mergeWith(other.System)
s.Updater.mergeWith(other.Updater)
s.Version.mergeWith(other.Version)
s.VPN.mergeWith(other.VPN)
s.Pprof.MergeWith(other.Pprof)
}
func (s *Settings) OverrideWith(other Settings, func (s *Settings) OverrideWith(other Settings,
allServers models.AllServers) (err error) { filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner,
) (err error) {
patchedSettings := s.copy() patchedSettings := s.copy()
patchedSettings.ControlServer.overrideWith(other.ControlServer) patchedSettings.ControlServer.overrideWith(other.ControlServer)
patchedSettings.DNS.overrideWith(other.DNS) patchedSettings.DNS.overrideWith(other.DNS)
@@ -101,12 +99,13 @@ func (s *Settings) OverrideWith(other Settings,
patchedSettings.Log.overrideWith(other.Log) patchedSettings.Log.overrideWith(other.Log)
patchedSettings.PublicIP.overrideWith(other.PublicIP) patchedSettings.PublicIP.overrideWith(other.PublicIP)
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks) patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
patchedSettings.Storage.overrideWith(other.Storage)
patchedSettings.System.overrideWith(other.System) patchedSettings.System.overrideWith(other.System)
patchedSettings.Updater.overrideWith(other.Updater) patchedSettings.Updater.overrideWith(other.Updater)
patchedSettings.Version.overrideWith(other.Version) patchedSettings.Version.overrideWith(other.Version)
patchedSettings.VPN.overrideWith(other.VPN) patchedSettings.VPN.OverrideWith(other.VPN)
patchedSettings.Pprof.MergeWith(other.Pprof) patchedSettings.Pprof.OverrideWith(other.Pprof)
err = patchedSettings.Validate(allServers) err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
if err != nil { if err != nil {
return err return err
} }
@@ -123,10 +122,11 @@ func (s *Settings) SetDefaults() {
s.Log.setDefaults() s.Log.setDefaults()
s.PublicIP.setDefaults() s.PublicIP.setDefaults()
s.Shadowsocks.setDefaults() s.Shadowsocks.setDefaults()
s.Storage.setDefaults()
s.System.setDefaults() s.System.setDefaults()
s.Version.setDefaults() s.Version.setDefaults()
s.VPN.setDefaults() s.VPN.setDefaults()
s.Updater.SetDefaults(*s.VPN.Provider.Name) s.Updater.SetDefaults(s.VPN.Provider.Name)
s.Pprof.SetDefaults() s.Pprof.SetDefaults()
} }
@@ -145,6 +145,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
node.AppendNode(s.Shadowsocks.toLinesNode()) node.AppendNode(s.Shadowsocks.toLinesNode())
node.AppendNode(s.HTTPProxy.toLinesNode()) node.AppendNode(s.HTTPProxy.toLinesNode())
node.AppendNode(s.ControlServer.toLinesNode()) node.AppendNode(s.ControlServer.toLinesNode())
node.AppendNode(s.Storage.toLinesNode())
node.AppendNode(s.System.toLinesNode()) node.AppendNode(s.System.toLinesNode())
node.AppendNode(s.PublicIP.toLinesNode()) node.AppendNode(s.PublicIP.toLinesNode())
node.AppendNode(s.Updater.toLinesNode()) node.AppendNode(s.Updater.toLinesNode())
@@ -153,3 +154,66 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
return node return node
} }
func (s Settings) Warnings() (warnings []string) {
if s.VPN.Provider.Name == providers.HideMyAss {
warnings = append(warnings, "HideMyAss dropped support for Linux OpenVPN "+
" so this will likely not work anymore. See https://github.com/qdm12/gluetun/issues/1498.")
}
if helpers.IsOneOf(s.VPN.Provider.Name, providers.SlickVPN) &&
s.VPN.Type == vpn.OpenVPN {
warnings = append(warnings, "OpenVPN 2.5 and 2.6 use OpenSSL 3 "+
"which prohibits the usage of weak security in today's standards. "+
s.VPN.Provider.Name+" uses weak security which is out "+
"of Gluetun's control so the only workaround is to allow such weaknesses "+
`using the OpenVPN option tls-cipher "DEFAULT:@SECLEVEL=0". `+
"You might want to reach to your provider so they upgrade their certificates. "+
"Once this is done, you will have to let the Gluetun maintainers know "+
"by creating an issue, attaching the new certificate and we will update Gluetun.")
}
for _, upstreamAddress := range s.DNS.UpstreamPlainAddresses {
if upstreamAddress.Addr().IsPrivate() {
warnings = append(warnings, "DNS upstream address "+upstreamAddress.String()+" is private: "+
"DNS traffic might leak out of the VPN tunnel to that address.")
}
}
return warnings
}
func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
warnings := readObsolete(r)
for _, warning := range warnings {
warner.Warn(warning)
}
readFunctions := map[string]func(r *reader.Reader) error{
"control server": s.ControlServer.read,
"DNS": s.DNS.read,
"firewall": s.Firewall.read,
"health": s.Health.Read,
"http proxy": s.HTTPProxy.read,
"log": s.Log.read,
"public ip": func(r *reader.Reader) error {
return s.PublicIP.read(r, warner)
},
"shadowsocks": s.Shadowsocks.read,
"storage": s.Storage.read,
"system": s.System.read,
"updater": s.Updater.read,
"version": s.Version.read,
"VPN": s.VPN.read,
"profiling": s.Pprof.Read,
}
for name, read := range readFunctions {
err = read(r)
if err != nil {
return fmt.Errorf("reading %s settings: %w", name, err)
}
}
return nil
}

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