Compare commits

..

186 Commits

Author SHA1 Message Date
Quentin McGaw
801c9a3086 wip 2024-11-08 16:52:47 +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
545 changed files with 44710 additions and 23510 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 qmcgaw/godevcontainer:v0.20-alpine
RUN apk add wireguard-tools htop openssl 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
- [Docker Compose](https://docs.docker.com/compose/install/) installed
## Setup ## Setup
1. Create the following files on your host if you don't have them: 1. Create the following files and directory on your host if you don't have them:
```sh ```sh
touch ~/.gitconfig ~/.zsh_history touch ~/.gitconfig ~/.zsh_history
mkdir -p ~/.ssh
``` ```
Note that the development container will create the empty directories `~/.docker`, `~/.ssh` and `~/.kube` if you don't have them. 1. **For Docker on OSX**: ensure the project directory and your home directory `~` are accessible by Docker.
1. **For Docker on OSX or Windows without WSL**: ensure your home directory `~` is 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.
## 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
RUN apk add curl
```
To rebuild the image, either: - 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:
- With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container` ```json
- With a terminal, go to this directory and `docker-compose build` // Welcome script
{
### Customize VS code settings "source": "/yourpath/.welcome.sh",
"target": "/root/.welcome.sh",
You can customize **settings** and **extensions** in the [devcontainer.json](devcontainer.json) definition file. "type": "bind"
},
### Entrypoint script
You can bind mount a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/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). You can also now do it directly with VSCode without restarting the container.
### 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,16 +1,50 @@
{ {
"name": "gluetun-dev", "name": "gluetun-dev",
"dockerComposeFile": [ // User defined settings
"docker-compose.yml" "containerEnv": {
"TZ": ""
},
// Fixed settings
"build": {
"dockerfile": "./Dockerfile"
},
"postCreateCommand": "~/.windows.sh && go mod download",
"capAdd": [
"NET_ADMIN", // Gluetun specific
"SYS_PTRACE" // for dlv Go debugging
], ],
"service": "vscode", "securityOpt": [
"runServices": [ "seccomp=unconfined" // for dlv Go debugging
"vscode" ],
"mounts": [
// Zsh commands history persistence
{
"source": "${localEnv:HOME}/.zsh_history",
"target": "/root/.zsh_history",
"type": "bind"
},
// Git configuration file
{
"source": "${localEnv:HOME}/.gitconfig",
"target": "/root/.gitconfig",
"type": "bind"
},
// SSH directory for Linux, OSX and WSL
// On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
// created in the container. On Windows, files are copied
// from /mnt/ssh to ~/.ssh to fix permissions.
{
"source": "${localEnv:HOME}/.ssh",
"target": "/mnt/ssh",
"type": "bind"
},
// Docker socket to access the host Docker server
{
"source": "/var/run/docker.sock",
"target": "/var/run/docker.sock",
"type": "bind"
}
], ],
"shutdownAction": "stopCompose",
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
"workspaceFolder": "/workspace",
// "overrideCommand": "",
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [
@@ -37,17 +71,18 @@
"go.useLanguageServer": true, "go.useLanguageServer": true,
"[go]": { "[go]": {
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": true "source.organizeImports": "explicit"
} }
}, },
"[go.mod]": { "[go.mod]": {
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": true "source.organizeImports": "explicit"
} }
}, },
"gopls": { "gopls": {
"usePlaceholders": false, "usePlaceholders": false,
"staticcheck": true "staticcheck": true,
"formatting.gofumpt": true,
}, },
"go.lintTool": "golangci-lint", "go.lintTool": "golangci-lint",
"go.lintOnSave": "package", "go.lintOnSave": "package",

View File

@@ -1,28 +0,0 @@
version: "3.7"
services:
vscode:
build: .
volumes:
- ../:/workspace
# Docker socket to access Docker server
- /var/run/docker.sock:/var/run/docker.sock
# SSH directory for Linux, OSX and WSL
# On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
# created in the container. On Windows, files are copied
# from /mnt/ssh to ~/.ssh to fix permissions.
- ~/.ssh:/mnt/ssh
# Shell history persistence
- ~/.zsh_history:/root/.zsh_history
# Git config
- ~/.gitconfig:/root/.gitconfig
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

@@ -50,6 +50,7 @@ body:
- Cyberghost - Cyberghost
- ExpressVPN - ExpressVPN
- FastestVPN - FastestVPN
- Giganews
- HideMyAss - HideMyAss
- IPVanish - IPVanish
- IVPN - IVPN

View File

@@ -1,9 +1,10 @@
blank_issues_enabled: false
contact_links: contact_links:
- name: Report a Wiki issue - name: Report a Wiki issue
url: https://github.com/qdm12/gluetun-wiki/issues/new url: https://github.com/qdm12/gluetun-wiki/issues/new/choose
about: Please create an issue on the gluetun-wiki repository. 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

201
.github/labels.yml vendored
View File

@@ -1,133 +1,152 @@
# Temporary status - name: "Status: 🗯️ Waiting for feedback"
- name: "🗯️ Waiting for feedback" color: "f7d692"
color: "aadefa" - name: "Status: 🔴 Blocked"
description: "" color: "f7d692"
- name: "🔴 Blocked"
color: "ff3f14"
description: "Blocked by another issue or pull request" description: "Blocked by another issue or pull request"
- name: "🔒 After next release" - name: "Status: 📌 Before next release"
color: "e8f274" color: "f7d692"
description: "Has to be done before the next release"
- name: "Status: 🔒 After next release"
color: "f7d692"
description: "Will be done after the next release" description: "Will be done after the next release"
- name: "Status: 🟡 Nearly resolved"
color: "f7d692"
description: "This might be resolved or is about to be resolved"
# 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"
# Complexity - name: "Priority: 🚨 Urgent"
- name: "☣️ Hard to do" color: "03adfc"
color: "7d0008" - name: "Priority: 💤 Low priority"
description: "" color: "03adfc"
- name: "🟩 Easy to do"
color: "34cf43" - name: "Complexity: ☣️ Hard to do"
description: "" 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: AirVPN" - name: "☁️ AirVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Custom"
- name: ":cloud: Cyberghost"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Cyberghost"
- name: ":cloud: HideMyAss"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Giganews"
- name: ":cloud: IPVanish"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ HideMyAss"
- name: ":cloud: IVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ IPVanish"
- name: ":cloud: ExpressVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ IVPN"
- name: ":cloud: FastestVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ ExpressVPN"
- name: ":cloud: Mullvad"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ FastestVPN"
- name: ":cloud: NordVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Mullvad"
- name: ":cloud: Perfect Privacy"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ NordVPN"
- name: ":cloud: PIA"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Perfect Privacy"
- name: ":cloud: Privado"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ PIA"
- name: ":cloud: PrivateVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Privado"
- name: ":cloud: ProtonVPN"
color: "cfe8d4" color: "cfe8d4"
- name: ":cloud: PureVPN" - name: "☁️ PrivateVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ ProtonVPN"
- name: ":cloud: SlickVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ PureVPN"
- name: ":cloud: Surfshark"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ SlickVPN"
- name: ":cloud: Torguard"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Surfshark"
- name: ":cloud: VPNSecure.me"
color: "cfe8d4" color: "cfe8d4"
- name: ":cloud: VPNUnlimited" - name: "☁️ Torguard"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ VPNSecure.me"
- name: ":cloud: Vyprvpn"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ VPNUnlimited"
- name: ":cloud: WeVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" - name: "☁️ Vyprvpn"
- name: ":cloud: Windscribe" color: "cfe8d4"
- name: "☁️ WeVPN"
color: "cfe8d4"
- name: "☁️ Windscribe"
color: "cfe8d4" color: "cfe8d4"
description: ""
# Problem category - name: "Category: User error 🤦"
- name: "Config problem" from_name: "Category: Config problem 📝"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Healthcheck 🩺"
- name: "Openvpn"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Documentation ✒️"
- name: "Wireguard" description: "A problem with the readme or a code comment."
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Maintenance ⛓️"
- name: "Unbound (DNS over TLS)" description: "Anything related to code or other maintenance"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Logs 📚"
- name: "Firewall" description: "Something to change in logs"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Good idea 🎯"
- name: "Routing" description: "This is a good idea, judged by the maintainers"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Motivated! 🙌"
- name: "IPv6" description: "Your pumpness makes me pumped! The issue or PR shows great motivation!"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Foolproof settings 👼"
- name: "Port forwarding"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: Label missing ❗"
- name: "HTTP proxy"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: updater ♻️"
- name: "Shadowsocks"
color: "ffc7ea" color: "ffc7ea"
description: "" description: "Concerns the code to update servers data"
- name: "Healthcheck server" - name: "Category: New provider 🆕"
color: "ffc7ea" color: "ffc7ea"
description: "" - name: "Category: OpenVPN 🔐"
- name: "Control server" 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: ""

View File

@@ -66,6 +66,33 @@ jobs:
- name: Build final image - name: Build final image
run: docker build -t final-image . run: docker build -t final-image .
- name: Run Wireguard
if: |
github.repository == 'qdm12/gluetun' &&
(
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
)
run: |
docker run -it --rm --cap-add=NET_ADMIN -e VPNSP=mullvad -e VPN_TYPE=wireguard \
-e WIREGUARD_PRIVATE_KEY=${{ secrets.WIREGUARD_PRIVATE_KEY }} \
-e WIREGUARD_ADDRESS=${{ secrets.WIREGUARD_ADDRESS }} \
final-image \
exit-once-connected
- name: Run OpenVPN
if: |
github.repository == 'qdm12/gluetun' &&
(
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
)
run: |
docker run -it --rm --cap-add=NET_ADMIN -e VPNSP=mullvad -e VPN_TYPE=openvpn \
-e OPENVPN_PASSWORD=${{ secrets.OPENVPN_PASSWORD }} \
final-image \
exit-once-connected
codeql: codeql:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -74,6 +101,9 @@ jobs:
security-events: write security-events: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "^1.23"
- uses: github/codeql-action/init@v3 - uses: github/codeql-action/init@v3
with: with:
languages: go languages: go
@@ -135,7 +165,7 @@ jobs:
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@v5 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 }}

View File

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

View File

@@ -20,7 +20,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v14 - uses: DavidAnson/markdownlint-cli2-action@v16
with: with:
globs: "**.md" globs: "**.md"
config: .markdownlint.json config: .markdownlint.json
@@ -35,8 +35,9 @@ jobs:
- uses: gaurav-nelson/github-action-markdown-link-check@v1 - uses: gaurav-nelson/github-action-markdown-link-check@v1
with: with:
use-quiet-mode: yes use-quiet-mode: yes
config-file: .github/workflows/configs/mlc-config.json
- uses: peter-evans/dockerhub-description@v3 - uses: peter-evans/dockerhub-description@v4
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push' if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
with: with:
username: qmcgaw username: qmcgaw

View File

@@ -7,27 +7,12 @@ issues:
- path: _test\.go - path: _test\.go
linters: linters:
- dupl - dupl
- goerr113 - err113
- containedctx - containedctx
- goconst
- maintidx - maintidx
- path: "internal\\/server\\/.+\\.go" - path: "internal\\/server\\/.+\\.go"
linters: linters:
- dupl - dupl
- path: "internal\\/configuration\\/settings\\/.+\\.go"
linters:
- dupl
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
source: "^.+= os\\.OpenFile\\(.+, .+, 0[0-9]{3}\\)"
linters:
- gomnd
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
source: "^.+= os\\.MkdirAll\\(.+, 0[0-9]{3}\\)"
linters:
- gomnd
- linters:
- lll
source: "^//go:generate .+$"
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)" - text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
linters: linters:
- ireturn - ireturn
@@ -35,18 +20,6 @@ issues:
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)" text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
linters: linters:
- ireturn - ireturn
- path: "internal\\/firewall\\/.*\\.go"
text: "string `-i ` has [1-9][0-9]* occurrences, make it a constant"
linters:
- goconst
- path: "internal\\/provider\\/ipvanish\\/updater\\/servers.go"
text: "string ` in ` has 3 occurrences, make it a constant"
linters:
- goconst
- path: "internal\\/vpn\\/portforward.go"
text: 'directive `//nolint:ireturn` is unused for linter "ireturn"'
linters:
- nolintlint
linters: linters:
enable: enable:
@@ -57,16 +30,17 @@ linters:
- bidichk - bidichk
- bodyclose - bodyclose
- containedctx - containedctx
- copyloopvar
- decorder - decorder
- dogsled - dogsled
- dupl - dupl
- dupword - dupword
- durationcheck - durationcheck
- err113
- errchkjson - errchkjson
- errname - errname
- execinquery
- exhaustive - exhaustive
- exportloopref - fatcontext
- forcetypeassert - forcetypeassert
- gci - gci
- gocheckcompilerdirectives - gocheckcompilerdirectives
@@ -77,10 +51,9 @@ linters:
- gocritic - gocritic
- gocyclo - gocyclo
- godot - godot
- goerr113 - gofumpt
- goheader - goheader
- goimports - goimports
- gomnd
- gomoddirectives - gomoddirectives
- goprintffuncname - goprintffuncname
- gosec - gosec
@@ -88,12 +61,14 @@ linters:
- grouper - grouper
- importas - importas
- interfacebloat - interfacebloat
- intrange
- ireturn - ireturn
- lll - lll
- maintidx - maintidx
- makezero - makezero
- mirror - mirror
- misspell - misspell
- mnd
- musttag - musttag
- nakedret - nakedret
- nestif - nestif
@@ -120,9 +95,3 @@ linters:
- wastedassign - wastedassign
- whitespace - whitespace
- zerologlint - zerologlint
run:
skip-dirs:
- .devcontainer
- .github
- doc

View File

@@ -21,7 +21,7 @@
"go.useLanguageServer": true, "go.useLanguageServer": true,
"[go]": { "[go]": {
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": true "source.organizeImports": "explicit"
} }
}, },
"go.lintTool": "golangci-lint", "go.lintTool": "golangci-lint",

View File

@@ -1,8 +1,8 @@
ARG ALPINE_VERSION=3.18 ARG ALPINE_VERSION=3.20
ARG GO_ALPINE_VERSION=3.18 ARG GO_ALPINE_VERSION=3.20
ARG GO_VERSION=1.21 ARG GO_VERSION=1.23
ARG XCPUTRANSLATE_VERSION=v0.6.0 ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.56.2 ARG GOLANGCI_LINT_VERSION=v1.61.0
ARG MOCKGEN_VERSION=v1.6.0 ARG MOCKGEN_VERSION=v1.6.0
ARG BUILDPLATFORM=linux/amd64 ARG BUILDPLATFORM=linux/amd64
@@ -76,23 +76,26 @@ 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=root \ OPENVPN_PROCESS_USER=root \
OPENVPN_MSSFIX= \
OPENVPN_CUSTOM_CONFIG= \ OPENVPN_CUSTOM_CONFIG= \
# Wireguard # Wireguard
WIREGUARD_ENDPOINT_IP= \
WIREGUARD_ENDPOINT_PORT= \
WIREGUARD_CONF_SECRETFILE=/run/secrets/wg0.conf \ WIREGUARD_CONF_SECRETFILE=/run/secrets/wg0.conf \
WIREGUARD_PRIVATE_KEY= \ WIREGUARD_PRIVATE_KEY= \
WIREGUARD_PRIVATE_KEY_SECRETFILE=/run/secrets/wireguard_private_key \ WIREGUARD_PRIVATE_KEY_SECRETFILE=/run/secrets/wireguard_private_key \
@@ -100,9 +103,10 @@ ENV VPN_SERVICE_PROVIDER=pia \
WIREGUARD_PRESHARED_KEY_SECRETFILE=/run/secrets/wireguard_preshared_key \ WIREGUARD_PRESHARED_KEY_SECRETFILE=/run/secrets/wireguard_preshared_key \
WIREGUARD_PUBLIC_KEY= \ WIREGUARD_PUBLIC_KEY= \
WIREGUARD_ALLOWED_IPS= \ WIREGUARD_ALLOWED_IPS= \
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
WIREGUARD_ADDRESSES= \ WIREGUARD_ADDRESSES= \
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \ WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
WIREGUARD_MTU=1400 \ WIREGUARD_MTU=1320 \
WIREGUARD_IMPLEMENTATION=auto \ WIREGUARD_IMPLEMENTATION=auto \
# VPN server filtering # VPN server filtering
SERVER_REGIONS= \ SERVER_REGIONS= \
@@ -119,6 +123,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
VPN_PORT_FORWARDING_LISTENING_PORT=0 \ VPN_PORT_FORWARDING_LISTENING_PORT=0 \
VPN_PORT_FORWARDING_PROVIDER= \ VPN_PORT_FORWARDING_PROVIDER= \
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \ VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
VPN_PORT_FORWARDING_USERNAME= \
VPN_PORT_FORWARDING_PASSWORD= \
# # Cyberghost only: # # Cyberghost only:
OPENVPN_CERT= \ OPENVPN_CERT= \
OPENVPN_KEY= \ OPENVPN_KEY= \
@@ -133,16 +139,20 @@ ENV VPN_SERVICE_PROVIDER=pia \
SERVER_NUMBER= \ SERVER_NUMBER= \
# # PIA 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: # # VPN Secure only:
PREMIUM_ONLY= \ PREMIUM_ONLY= \
# # PIA only: # # PIA and ProtonVPN only:
PORT_FORWARD_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= \
@@ -159,9 +169,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
DOT=on \ DOT=on \
DOT_PROVIDERS=cloudflare \ DOT_PROVIDERS=cloudflare \
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \ DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
DOT_VERBOSITY=1 \
DOT_VERBOSITY_DETAILS=0 \
DOT_VALIDATION_LOGLEVEL=0 \
DOT_CACHING=on \ DOT_CACHING=on \
DOT_IPV6=off \ DOT_IPV6=off \
BLOCK_MALICIOUS=on \ BLOCK_MALICIOUS=on \
@@ -190,15 +197,18 @@ ENV VPN_SERVICE_PROVIDER=pia \
# Control server # Control server
HTTP_CONTROL_SERVER_LOG=on \ 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 \
# Server data updater # Server data updater
UPDATER_PERIOD=0 \ UPDATER_PERIOD=0 \
UPDATER_MIN_RATIO=0.8 \ UPDATER_MIN_RATIO=0.8 \
UPDATER_VPN_SERVICE_PROVIDERS= \ UPDATER_VPN_SERVICE_PROVIDERS= \
# Public IP # Public IP
PUBLICIP_FILE="/tmp/gluetun/ip" \ PUBLICIP_FILE="/tmp/gluetun/ip" \
PUBLICIP_PERIOD=12h \ PUBLICIP_ENABLED=on \
PUBLICIP_API=ipinfo \ PUBLICIP_API=ipinfo,ifconfigco,ip2location,cloudflare \
PUBLICIP_API_TOKEN= \ 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 \
@@ -211,18 +221,15 @@ ENV VPN_SERVICE_PROVIDER=pia \
PGID= PGID=
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 wget && \ RUN apk add --no-cache --update -l wget && \
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.17/main" openvpn\~2.5 && \ 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.5 && \ mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
apk del openvpn && \ apk del openvpn && \
apk add --no-cache --update openvpn ca-certificates iptables ip6tables unbound tzdata && \ apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy tzdata && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \ mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \
# Fix vulnerability issue rm -rf /var/cache/apk/* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
apk add --no-cache --update busybox && \
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /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

View File

@@ -56,26 +56,25 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
## Features ## Features
- Based on Alpine 3.18 for a small Docker image of 35.6MB - Based on Alpine 3.20 for a small Docker image of 35.6MB
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers - 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
- Supports OpenVPN for all providers listed - Supports OpenVPN for all providers listed
- Supports Wireguard both kernelspace and userspace - Supports Wireguard both kernelspace and userspace
- For **AirVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Surfshark** and **Windscribe** - For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
- For **ProtonVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited**, **VyprVPN** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134) - More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
- DNS over TLS baked in with service provider(s) of your choice - DNS over TLS baked in with service provider(s) of your choice
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours - DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
- Choose the vpn network protocol, `udp` or `tcp` - Choose the vpn network protocol, `udp` or `tcp`
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices - Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP) - Built in Shadowsocks proxy server (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP) - Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md) - [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md) - [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆 - 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/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding) - 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)
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers - Possibility of split horizon DNS by selecting multiple DNS over TLS providers
- Unbound subprogram drops root privileges once launched
- Can work as a Kubernetes sidecar container, thanks @rorph - Can work as a Kubernetes sidecar container, thanks @rorph
## Setup ## Setup
@@ -84,7 +83,7 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)! Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun-wiki/issues/new) [🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun-wiki/issues/new/choose)
Here's a docker-compose.yml for the laziest: Here's a docker-compose.yml for the laziest:

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/command"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/configuration/sources/env"
"github.com/qdm12/gluetun/internal/configuration/sources/files" "github.com/qdm12/gluetun/internal/configuration/sources/files"
mux "github.com/qdm12/gluetun/internal/configuration/sources/merge"
"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"
@@ -34,7 +34,6 @@ import (
"github.com/qdm12/gluetun/internal/pprof" "github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip" "github.com/qdm12/gluetun/internal/publicip"
pubipapi "github.com/qdm12/gluetun/internal/publicip/api"
"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"
@@ -44,14 +43,14 @@ import (
"github.com/qdm12/gluetun/internal/updater/resolver" "github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip" "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/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/log" "github.com/qdm12/log"
"github.com/qdm12/updated/pkg/dnscrypto"
) )
//nolint:gochecknoglobals //nolint:gochecknoglobals
@@ -80,23 +79,32 @@ func main() {
netLinkDebugLogger := logger.New(log.SetComponent("netlink")) netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
netLinker := netlink.New(netLinkDebugLogger) netLinker := netlink.New(netLinkDebugLogger)
cli := cli.New() cli := cli.New()
cmder := command.NewCmder() cmder := command.New()
secretsReader := secrets.New() reader := reader.New(reader.Settings{
filesReader := files.New() Sources: []reader.Source{
envReader := env.New(logger) secrets.New(logger),
muxReader := mux.New(secretsReader, filesReader, envReader) 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 var err error
select { select {
case signal := <-signalCh: case receivedSignal := <-signalCh:
signal.Stop(signalCh)
fmt.Println("") fmt.Println("")
logger.Warn("Caught OS signal " + signal.String() + ", shutting down") logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down")
cancel() cancel()
case err = <-errorCh: case err = <-errorCh:
close(errorCh) close(errorCh)
@@ -107,15 +115,14 @@ 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 shutdownErr := <-errorCh: case shutdownErr := <-errorCh:
if !timer.Stop() { timer.Stop()
<-timer.C
}
if shutdownErr != nil { if shutdownErr != nil {
logger.Warnf("Shutdown not completed gracefully: %s", shutdownErr) logger.Warnf("Shutdown failed: %s", shutdownErr)
os.Exit(1) os.Exit(1)
} }
@@ -127,39 +134,40 @@ func main() {
case <-timer.C: case <-timer.C:
logger.Warn("Shutdown timed out") logger.Warn("Shutdown timed out")
os.Exit(1) os.Exit(1)
case signal := <-signalCh:
logger.Warn("Caught OS signal " + signal.String() + ", forcing shut down")
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 log.LoggerInterface, source Source, args []string, logger log.LoggerInterface, reader *reader.Reader,
tun Tun, netLinker netLinker, cmder command.RunStarter, tun Tun, netLinker netLinker, cmder RunStarter,
cli clier) error { cli clier,
) error {
var exitOnceConnected bool
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, netLinker) 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:])
case "exit-once-connected":
exitOnceConnected = true
default: default:
return fmt.Errorf("%w: %s", errCommandUnknown, args[1]) return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
} }
} }
announcementExp, err := time.Parse(time.RFC3339, "2023-07-01T00:00:00Z") announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
if err != nil { if err != nil {
return err return err
} }
@@ -169,8 +177,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: "Wiki moved to https://github.com/qdm12/gluetun-wiki", 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",
@@ -180,17 +188,22 @@ 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)
logger.Patch(log.SetLevel(*allSettings.Log.Level)) if err != nil {
netLinker.PatchLoggerLevel(*allSettings.Log.Level) return fmt.Errorf("log level: %w", err)
}
logger.Patch(log.SetLevel(logLevel))
netLinker.PatchLoggerLevel(logLevel)
routingLogger := logger.New(log.SetComponent("routing")) routingLogger := logger.New(log.SetComponent("routing"))
if *allSettings.Firewall.Debug { // To remove in v4 if *allSettings.Firewall.Debug { // To remove in v4
@@ -227,7 +240,7 @@ 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.New(log.SetComponent("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
} }
@@ -237,7 +250,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return fmt.Errorf("checking for IPv6 support: %w", err) return fmt.Errorf("checking for IPv6 support: %w", err)
} }
err = allSettings.Validate(storage, ipv6Supported) err = allSettings.Validate(storage, ipv6Supported, logger)
if err != nil { if err != nil {
return err return err
} }
@@ -257,19 +270,12 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
ovpnConf := openvpn.New( ovpnConf := openvpn.New(
logger.New(log.SetComponent("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.5", getVersion: ovpnConf.Version25}, {name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26}, {name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
{name: "Unbound", getVersion: dnsConf.Version}, {name: "IPtables", getVersion: firewallConf.Version},
{name: "IPtables", getVersion: func(ctx context.Context) (version string, err error) {
return firewall.Version(ctx, cmder)
}},
}) })
if err != nil { if err != nil {
return err return err
@@ -281,10 +287,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
logger.Warn(warning) logger.Warn(warning)
} }
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil { 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
} }
@@ -296,15 +305,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
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?")
@@ -331,11 +333,15 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
} }
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)
} }
} }
@@ -359,7 +365,8 @@ 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...)
@@ -382,28 +389,29 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return fmt.Errorf("starting port forwarding loop: %w", err) return fmt.Errorf("starting port forwarding loop: %w", err)
} }
unboundLogger := logger.New(log.SetComponent("dns")) dnsLogger := logger.New(log.SetComponent("dns"))
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient, dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
unboundLogger) dnsLogger)
if err != nil {
return fmt.Errorf("creating DNS loop: %w", err)
}
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)
publicipAPI, _ := pubipapi.ParseProvider(allSettings.PublicIP.API) publicIPLooper, err := publicip.NewLoop(allSettings.PublicIP, puid, pgid, httpClient,
ipFetcher, err := pubipapi.New(publicipAPI, httpClient, *allSettings.PublicIP.APIToken) logger.New(log.SetComponent("ip getter")))
if err != nil { if err != nil {
return fmt.Errorf("creating public IP API client: %w", err) return fmt.Errorf("creating public ip loop: %w", err)
} }
publicIPLooper := publicip.NewLoop(ipFetcher,
logger.New(log.SetComponent("ip getter")),
allSettings.PublicIP, puid, pgid)
publicIPRunError, err := publicIPLooper.Start(ctx) publicIPRunError, err := publicIPLooper.Start(ctx)
if err != nil { if err != nil {
return fmt.Errorf("starting public ip loop: %w", err) return fmt.Errorf("starting public ip loop: %w", err)
@@ -415,12 +423,12 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress) parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
openvpnFileExtractor := extract.New() openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, updaterLogger, providers := provider.NewProviders(storage, time.Now, updaterLogger,
httpClient, unzipper, parallelResolver, ipFetcher, openvpnFileExtractor) httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor)
vpnLogger := logger.New(log.SetComponent("vpn")) vpnLogger := logger.New(log.SetComponent("vpn"))
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts, vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper, providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient, 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))
@@ -460,7 +468,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
"http server", goroutine.OptionTimeout(defaultShutdownTimeout)) "http server", goroutine.OptionTimeout(defaultShutdownTimeout))
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging, httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
logger.New(log.SetComponent("http server")), logger.New(log.SetComponent("http server")),
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper, allSettings.ControlServer.AuthFilePath,
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
storage, ipv6Supported) storage, ipv6Supported)
if err != nil { if err != nil {
return fmt.Errorf("setting up control server: %w", err) return fmt.Errorf("setting up control server: %w", err)
@@ -471,7 +480,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
controlGroupHandler.Add(httpServerHandler) controlGroupHandler.Add(httpServerHandler)
healthLogger := logger.New(log.SetComponent("healthcheck")) healthLogger := logger.New(log.SetComponent("healthcheck"))
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper) healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper,
exitOnceConnected)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler( healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout)) "HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
go healthcheckServer.Run(healthServerCtx, healthServerDone) go healthcheckServer.Run(healthServerCtx, healthServerDone)
@@ -520,7 +530,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()
@@ -578,9 +589,10 @@ type Linker interface {
type clier interface { type clier interface {
ClientKey(args []string) error ClientKey(args []string) error
FormatServers(args []string) error FormatServers(args []string) error
OpenvpnConfig(logger cli.OpenvpnConfigLogger, source cli.Source, ipv6Checker cli.IPv6Checker) error OpenvpnConfig(logger cli.OpenvpnConfigLogger, reader *reader.Reader, ipv6Checker cli.IPv6Checker) error
HealthCheck(ctx context.Context, source cli.Source, warner cli.Warner) error HealthCheck(ctx context.Context, reader *reader.Reader, warner cli.Warner) error
Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error
GenKey(args []string) error
} }
type Tun interface { type Tun interface {
@@ -588,8 +600,8 @@ type Tun interface {
Create(tunDevice string) error Create(tunDevice string) error
} }
type Source interface { type RunStarter interface {
Read() (settings settings.Settings, err error) Run(cmd *exec.Cmd) (output string, err error)
ReadHealth() (health settings.Health, err error) Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
String() string waitError <-chan error, err error)
} }

73
go.mod
View File

@@ -1,55 +1,62 @@
module github.com/qdm12/gluetun module github.com/qdm12/gluetun
go 1.21 go 1.23
require ( require (
github.com/breml/rootcerts v0.2.16 github.com/breml/rootcerts v0.2.18
github.com/fatih/color v1.16.0 github.com/fatih/color v1.18.0
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/klauspost/compress v1.17.4 github.com/klauspost/compress v1.17.11
github.com/klauspost/pgzip v1.2.6 github.com/klauspost/pgzip v1.2.6
github.com/qdm12/dns v1.11.0 github.com/pelletier/go-toml/v2 v2.2.3
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 github.com/qdm12/dns/v2 v2.0.0-rc8
github.com/qdm12/gosettings v0.4.0-rc1 github.com/qdm12/gosettings v0.4.3
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.2.0-rc1
github.com/qdm12/log v0.1.0 github.com/qdm12/log v0.1.0
github.com/qdm12/ss-server v0.5.0 github.com/qdm12/ss-server v0.6.0
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.8.4
github.com/ulikunitz/xz v0.5.11 github.com/ulikunitz/xz v0.5.11
github.com/vishvananda/netlink v1.2.1-beta.2 github.com/vishvananda/netlink v1.2.1
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
golang.org/x/net v0.19.0 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
golang.org/x/sys v0.15.0 golang.org/x/net v0.30.0
golang.org/x/text v0.14.0 golang.org/x/sys v0.27.0
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b golang.org/x/text v0.19.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde 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 gopkg.in/ini.v1 v1.67.0
inet.af/netaddr v0.0.0-20220811202034-502d2d690317
) )
require ( require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.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.9 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/josharian/native v1.0.0 // indirect github.com/josharian/native v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/genetlink v1.2.0 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.6.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.2.3 // 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/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.0 // 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.4 // indirect
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect golang.org/x/crypto v0.28.0 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect golang.org/x/mod v0.21.0 // indirect
golang.org/x/crypto v0.17.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/tools v0.26.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // 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 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
) )

286
go.sum
View File

@@ -1,254 +1,152 @@
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/breml/rootcerts v0.2.18 h1:KjZaNT7AX/akUjzpStuwTMQs42YHlPyc6NmdwShVba0=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/breml/rootcerts v0.2.18/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/breml/rootcerts v0.2.16 h1:yN1TGvicfHx8dKz3OQRIrx/5nE/iN3XT1ibqGbd6urc=
github.com/breml/rootcerts v0.2.16/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
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/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/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 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.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.6.2 h1:D2zGSkvYsJ6NreeED3JiVTu1lj2sIYATqSaZlhPzUgQ= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/netlink v1.6.2/go.mod h1:O1HXX2sIWSMJ3Qn1BYZk1yZM+7iMki/uYGGiwGyq/iU= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
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.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
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/qdm12/gosettings v0.4.0-rc1 h1:UYA92yyeDPbmZysIuG65yrpZVPtdIoRmtEHft/AyI38= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/qdm12/gosettings v0.4.0-rc1/go.mod h1:JRV3opOpHvnKlIA29lKQMdYw1WSMVMfHYLLHPHol5ME= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/qdm12/dns/v2 v2.0.0-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ18=
github.com/qdm12/dns/v2 v2.0.0-rc8/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
github.com/qdm12/gosettings v0.4.3 h1:oGAjiKVtml9oHVlPQo6H3yk6TmtWpVYicNeGFcM7AP8=
github.com/qdm12/gosettings v0.4.3/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
github.com/qdm12/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.2.0-rc1 h1:4iYQvU4ibrASgzelsEgZX4JyKX3UTB/DcHObzQ7BXtw=
github.com/qdm12/govalid v0.2.0-rc1/go.mod h1:/uWzVWMuS71wmbsVnlUxpQiy6EAXqm8eQ2RbyA72roQ=
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw= github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI= github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
github.com/qdm12/ss-server v0.5.0 h1:ARAqJayohDM51BmJ/R5Yplkpo+Qxgp7xizBF1HWd7uQ= github.com/qdm12/ss-server v0.6.0 h1:OaOdCIBXx0z3DGHPT6Th0v88vGa3MtAS4oRgUsDHGZE=
github.com/qdm12/ss-server v0.5.0/go.mod h1:eFd8PL/uy0ZvJ4KeSUzToruJctVQoYqXk+LRy9vcOiI= 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA=
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netlink v1.2.1/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
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/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
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/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
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/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/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-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
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/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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-20190503192946-f4e77d36d62c/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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
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/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-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-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-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-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-20210119212857-b64e53b001e4/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde h1:ybF7AMzIUikL9x4LgwEmzhXtzRpKNqngme1VGDWz+Nk= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ= 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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0/go.mod h1:Dn5idtptoW1dIos9U6A2rpebLs/MtTwFacjKb8jLdQA= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls= kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw=
inet.af/netaddr v0.0.0-20220811202034-502d2d690317 h1:U2fwK6P2EqmopP/hFLTOAjWTki0qgd4GMJn5X8wOleU= kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
inet.af/netaddr v0.0.0-20220811202034-502d2d690317/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=

View File

@@ -3,14 +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")
)
// 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) {
@@ -39,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

@@ -6,13 +6,12 @@ import (
"io" "io"
"os" "os"
"strings" "strings"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
) )
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
} }
@@ -28,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,6 +4,7 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -16,13 +17,13 @@ import (
) )
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") ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
) )
func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool, func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
provider string, titleCaser cases.Caser) { provider string, titleCaser cases.Caser,
) {
boolPtr, ok := providerToFormat[provider] boolPtr, ok := providerToFormat[provider]
if !ok { if !ok {
panic(fmt.Sprintf("unknown provider in format map: %s", provider)) panic(fmt.Sprintf("unknown provider in format map: %s", provider))
@@ -43,7 +44,7 @@ func (c *CLI) FormatServers(args []string) error {
providersToFormat[provider] = new(bool) providersToFormat[provider] = new(bool)
} }
flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError) flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError)
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'") 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")
titleCaser := cases.Title(language.English) titleCaser := cases.Title(language.English)
for _, provider := range allProviderFlags { for _, provider := range allProviderFlags {
@@ -53,9 +54,7 @@ func (c *CLI) FormatServers(args []string) error {
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. // Verify only one provider is set to be formatted.
var providers []string var providers []string
@@ -87,10 +86,14 @@ func (c *CLI) FormatServers(args []string) error {
return fmt.Errorf("creating servers storage: %w", err) return fmt.Errorf("creating servers storage: %w", err)
} }
formatted := storage.FormatToMarkdown(providerToFormat) formatted, err := storage.Format(providerToFormat, format)
if err != nil {
return fmt.Errorf("formatting servers: %w", err)
}
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("opening output file: %w", err) return fmt.Errorf("opening output file: %w", err)
} }

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,12 +6,15 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/healthcheck" "github.com/qdm12/gluetun/internal/healthcheck"
"github.com/qdm12/gosettings/reader"
) )
func (c *CLI) HealthCheck(ctx context.Context, source Source, _ Warner) error { func (c *CLI) HealthCheck(ctx context.Context, reader *reader.Reader, _ Warner) (err 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
} }

View File

@@ -1,16 +1,9 @@
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(string) {} func (l *noopLogger) Info(string) {}
func (l *noopLogger) Info(string) {}
func (l *noopLogger) Warn(string) {}
func (l *noopLogger) Error(string) {}
func (l *noopLogger) PatchLevel(logging.Level) {}
func (l *noopLogger) PatchPrefix(string) {}

View File

@@ -8,12 +8,14 @@ import (
"strings" "strings"
"time" "time"
"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/models"
"github.com/qdm12/gluetun/internal/openvpn/extract" "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/gluetun/internal/updater/resolver"
"github.com/qdm12/gosettings/reader"
) )
type OpenvpnConfigLogger interface { type OpenvpnConfigLogger interface {
@@ -32,6 +34,8 @@ type ParallelResolver interface {
} }
type IPFetcher interface { type IPFetcher interface {
String() string
CanFetchAnyIP() bool
FetchInfo(ctx context.Context, ip netip.Addr) (data models.PublicIP, err error) FetchInfo(ctx context.Context, ip netip.Addr) (data models.PublicIP, err error)
} }
@@ -39,14 +43,16 @@ type IPv6Checker interface {
IsIPv6Supported() (supported bool, err error) IsIPv6Supported() (supported bool, err error)
} }
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, source Source, func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
ipv6Checker IPv6Checker) error { 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
} }
allSettings, err := source.Read() var allSettings settings.Settings
err = allSettings.Read(reader, logger)
if err != nil { if err != nil {
return err return err
} }
@@ -56,7 +62,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, source Source,
return fmt.Errorf("checking for IPv6 support: %w", err) return fmt.Errorf("checking for IPv6 support: %w", err)
} }
if err = allSettings.Validate(storage, ipv6Supported); err != nil { if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil {
return fmt.Errorf("validating settings: %w", err) return fmt.Errorf("validating settings: %w", err)
} }
@@ -70,7 +76,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, source Source,
providers := provider.NewProviders(storage, time.Now, warner, client, providers := provider.NewProviders(storage, time.Now, warner, client,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor) unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
providerConf := providers.Get(*allSettings.VPN.Provider.Name) providerConf := providers.Get(allSettings.VPN.Provider.Name)
connection, err := providerConf.GetConnection( connection, err := providerConf.GetConnection(
allSettings.VPN.Provider.ServerSelection, ipv6Supported) allSettings.VPN.Provider.ServerSelection, ipv6Supported)
if err != nil { if err != nil {

View File

@@ -80,10 +80,17 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
httpClient := &http.Client{Timeout: clientTimeout} httpClient := &http.Client{Timeout: clientTimeout}
unzipper := unzip.New(httpClient) unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(options.DNSAddress) parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
ipFetcher, err := api.New(api.IPInfo, httpClient, ipToken) nameTokenPairs := []api.NameToken{
if err != nil { {Name: string(api.IPInfo), Token: ipToken},
return fmt.Errorf("creating public IP API client: %w", err) {Name: string(api.IP2Location)},
{Name: string(api.IfConfigCo)},
} }
fetchers, err := api.New(nameTokenPairs, httpClient)
if err != nil {
return fmt.Errorf("creating public IP fetchers: %w", err)
}
ipFetcher := api.NewResilient(fetchers, logger)
openvpnFileExtractor := extract.New() openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, logger, httpClient, providers := provider.NewProviders(storage, time.Now, logger, httpClient,

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

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,25 @@
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.",
}
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

@@ -5,6 +5,7 @@ import (
"net/netip" "net/netip"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -49,14 +50,6 @@ func (d *DNS) Copy() (copied DNS) {
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (d *DNS) mergeWith(other DNS) {
d.ServerAddress = gosettings.MergeWithValidator(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = gosettings.MergeWithPointer(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.
@@ -87,3 +80,22 @@ func (d DNS) toLinesNode() (node *gotree.Node) {
node.AppendNode(d.DoT.toLinesNode()) node.AppendNode(d.DoT.toLinesNode())
return node return node
} }
func (d *DNS) read(r *reader.Reader) (err error) {
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
if err != nil {
return err
}
d.KeepNameserver, err = r.BoolPtr("DNS_KEEP_NAMESERVER")
if err != nil {
return err
}
err = d.DoT.read(r)
if err != nil {
return fmt.Errorf("DNS over TLS settings: %w", err)
}
return nil
}

View File

@@ -3,11 +3,13 @@ package settings
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/http"
"net/netip" "net/netip"
"regexp" "regexp"
"github.com/qdm12/dns/pkg/blacklist" "github.com/qdm12/dns/v2/pkg/blockbuilder"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -63,16 +65,6 @@ func (b DNSBlacklist) copy() (copied DNSBlacklist) {
} }
} }
func (b *DNSBlacklist) mergeWith(other DNSBlacklist) {
b.BlockMalicious = gosettings.MergeWithPointer(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = gosettings.MergeWithPointer(b.BlockAds, other.BlockAds)
b.BlockSurveillance = gosettings.MergeWithPointer(b.BlockSurveillance, other.BlockSurveillance)
b.AllowedHosts = gosettings.MergeWithSlice(b.AllowedHosts, other.AllowedHosts)
b.AddBlockedHosts = gosettings.MergeWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = gosettings.MergeWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = gosettings.MergeWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
}
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) { func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
b.BlockMalicious = gosettings.OverrideWithPointer(b.BlockMalicious, other.BlockMalicious) b.BlockMalicious = gosettings.OverrideWithPointer(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = gosettings.OverrideWithPointer(b.BlockAds, other.BlockAds) b.BlockAds = gosettings.OverrideWithPointer(b.BlockAds, other.BlockAds)
@@ -83,16 +75,19 @@ func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes) b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
} }
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: netipAddressesToNetaddrIPs(b.AddBlockedIPs), AddBlockedIPs: b.AddBlockedIPs,
AddBlockedIPPrefixes: netipPrefixesToNetaddrIPPrefixes(b.AddBlockedIPPrefixes), AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
}, nil }
} }
func (b DNSBlacklist) String() string { func (b DNSBlacklist) String() string {
@@ -107,32 +102,94 @@ func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
node.Appendf("Block surveillance: %s", gosettings.BoolToYesNo(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())
} }
} }
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 = readDoTPrivateAddresses(r) // TODO v4 split in 2
if err != nil {
return err
}
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
return nil
}
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
ipPrefixes []netip.Prefix, err error,
) {
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
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

@@ -3,9 +3,12 @@ package settings
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/netip"
"time" "time"
"github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -15,22 +18,24 @@ type DoT struct {
// and used. It defaults to true, and cannot be nil // and used. It defaults to true, and cannot be nil
// in the internal state. // in the internal state.
Enabled *bool Enabled *bool
// UpdatePeriod is the period to update DNS block // UpdatePeriod is the period to update DNS block lists.
// lists and cryptographic files for DNSSEC validation.
// It can be set to 0 to disable the update. // It can be set to 0 to disable the update.
// It defaults to 24h and cannot be nil in // It defaults to 24h and cannot be nil in
// the internal state. // the internal state.
UpdatePeriod *time.Duration UpdatePeriod *time.Duration
// Unbound contains settings to configure Unbound. // Providers is a list of DNS over TLS providers
Unbound Unbound Providers []string `json:"providers"`
// Caching is true if the DoT server should cache
// DNS responses.
Caching *bool `json:"caching"`
// IPv6 is true if the DoT server should connect over IPv6.
IPv6 *bool `json:"ipv6"`
// Blacklist contains settings to configure the filter // Blacklist contains settings to configure the filter
// block lists. // block lists.
Blacklist DNSBlacklist Blacklist DNSBlacklist
} }
var ( var ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
)
func (d DoT) validate() (err error) { func (d DoT) validate() (err error) {
const minUpdatePeriod = 30 * time.Second const minUpdatePeriod = 30 * time.Second
@@ -39,9 +44,12 @@ func (d DoT) validate() (err error) {
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod) ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
} }
err = d.Unbound.validate() providers := provider.NewProviders()
if err != nil { for _, providerName := range d.Providers {
return err _, err := providers.Get(providerName)
if err != nil {
return err
}
} }
err = d.Blacklist.validate() err = d.Blacklist.validate()
@@ -56,27 +64,22 @@ func (d *DoT) copy() (copied DoT) {
return DoT{ return DoT{
Enabled: gosettings.CopyPointer(d.Enabled), Enabled: gosettings.CopyPointer(d.Enabled),
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod), UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
Unbound: d.Unbound.copy(), Providers: gosettings.CopySlice(d.Providers),
Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.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 = gosettings.MergeWithPointer(d.Enabled, other.Enabled)
d.UpdatePeriod = gosettings.MergeWithPointer(d.UpdatePeriod, other.UpdatePeriod)
d.Unbound.mergeWith(other.Unbound)
d.Blacklist.mergeWith(other.Blacklist)
}
// 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 *DoT) overrideWith(other DoT) { func (d *DoT) overrideWith(other DoT) {
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled) d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod) d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
d.Unbound.overrideWith(other.Unbound) 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.Blacklist.overrideWith(other.Blacklist)
} }
@@ -84,10 +87,26 @@ func (d *DoT) setDefaults() {
d.Enabled = gosettings.DefaultPointer(d.Enabled, true) d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
const defaultUpdatePeriod = 24 * time.Hour const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod) d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
d.Unbound.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.Blacklist.setDefaults()
} }
func (d DoT) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
providers := provider.NewProviders()
provider, err := providers.Get(d.Providers[0])
if err != nil {
// Settings should be validated before calling this function,
// so an error happening here is a programming error.
panic(err)
}
return provider.DoT.IPv4[0].Addr()
}
func (d DoT) String() string { func (d DoT) String() string {
return d.toLinesNode().String() return d.toLinesNode().String()
} }
@@ -106,8 +125,46 @@ func (d DoT) toLinesNode() (node *gotree.Node) {
} }
node.Appendf("Update period: %s", update) node.Appendf("Update period: %s", update)
node.AppendNode(d.Unbound.toLinesNode()) upstreamResolvers := node.Append("Upstream resolvers:")
for _, provider := range d.Providers {
upstreamResolvers.Append(provider)
}
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
node.AppendNode(d.Blacklist.toLinesNode()) node.AppendNode(d.Blacklist.toLinesNode())
return node return node
} }
func (d *DoT) read(reader *reader.Reader) (err error) {
d.Enabled, err = reader.BoolPtr("DOT")
if err != nil {
return err
}
d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD")
if err != nil {
return err
}
d.Providers = reader.CSV("DOT_PROVIDERS")
d.Caching, err = reader.BoolPtr("DOT_CACHING")
if err != nil {
return err
}
d.IPv6, err = reader.BoolPtr("DOT_IPV6")
if err != nil {
return err
}
err = d.Blacklist.read(reader)
if err != nil {
return err
}
return nil
}

View File

@@ -3,6 +3,7 @@ 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") ErrCategoryNotValid = errors.New("the category specified is not valid")
@@ -27,7 +28,8 @@ 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")
@@ -49,5 +51,6 @@ var (
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") ErrWireguardImplementationNotValid = errors.New("implementation is not valid")
) )

View File

@@ -5,6 +5,7 @@ import (
"net/netip" "net/netip"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -54,18 +55,6 @@ func (f *Firewall) copy() (copied Firewall) {
} }
} }
// 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 = gosettings.MergeWithSlice(f.VPNInputPorts, other.VPNInputPorts)
f.InputPorts = gosettings.MergeWithSlice(f.InputPorts, other.InputPorts)
f.OutboundSubnets = gosettings.MergeWithSlice(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = gosettings.MergeWithPointer(f.Enabled, other.Enabled)
f.Debug = gosettings.MergeWithPointer(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.
@@ -115,10 +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 {
subnet := subnet
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

@@ -59,7 +59,6 @@ func Test_Firewall_validate(t *testing.T) {
} }
for name, testCase := range testCases { for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Parallel() t.Parallel()

View File

@@ -6,8 +6,9 @@ import (
"time" "time"
"github.com/qdm12/gosettings" "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.
@@ -36,9 +37,7 @@ type Health struct {
} }
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)
} }
@@ -62,38 +61,27 @@ func (h *Health) copy() (copied Health) {
} }
} }
// MergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *Health) MergeWith(other Health) {
h.ServerAddress = gosettings.MergeWithString(h.ServerAddress, other.ServerAddress)
h.ReadHeaderTimeout = gosettings.MergeWithNumber(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
h.ReadTimeout = gosettings.MergeWithNumber(h.ReadTimeout, other.ReadTimeout)
h.TargetAddress = gosettings.MergeWithString(h.TargetAddress, other.TargetAddress)
h.SuccessWait = gosettings.MergeWithNumber(h.SuccessWait, other.SuccessWait)
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 = gosettings.OverrideWithString(h.ServerAddress, other.ServerAddress) h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
h.ReadHeaderTimeout = gosettings.OverrideWithNumber(h.ReadHeaderTimeout, other.ReadHeaderTimeout) h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
h.ReadTimeout = gosettings.OverrideWithNumber(h.ReadTimeout, other.ReadTimeout) h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
h.TargetAddress = gosettings.OverrideWithString(h.TargetAddress, other.TargetAddress) h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
h.SuccessWait = gosettings.OverrideWithNumber(h.SuccessWait, other.SuccessWait) h.SuccessWait = gosettings.OverrideWithComparable(h.SuccessWait, other.SuccessWait)
h.VPN.overrideWith(other.VPN) h.VPN.overrideWith(other.VPN)
} }
func (h *Health) SetDefaults() { func (h *Health) SetDefaults() {
h.ServerAddress = gosettings.DefaultString(h.ServerAddress, "127.0.0.1:9999") h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
const defaultReadHeaderTimeout = 100 * time.Millisecond const defaultReadHeaderTimeout = 100 * time.Millisecond
h.ReadHeaderTimeout = gosettings.DefaultNumber(h.ReadHeaderTimeout, defaultReadHeaderTimeout) h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
const defaultReadTimeout = 500 * time.Millisecond const defaultReadTimeout = 500 * time.Millisecond
h.ReadTimeout = gosettings.DefaultNumber(h.ReadTimeout, defaultReadTimeout) h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
h.TargetAddress = gosettings.DefaultString(h.TargetAddress, "cloudflare.com:443") h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
const defaultSuccessWait = 5 * time.Second const defaultSuccessWait = 5 * time.Second
h.SuccessWait = gosettings.DefaultNumber(h.SuccessWait, defaultSuccessWait) h.SuccessWait = gosettings.DefaultComparable(h.SuccessWait, defaultSuccessWait)
h.VPN.setDefaults() h.VPN.setDefaults()
} }
@@ -111,3 +99,21 @@ func (h Health) toLinesNode() (node *gotree.Node) {
node.AppendNode(h.VPN.toLinesNode("VPN")) node.AppendNode(h.VPN.toLinesNode("VPN"))
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.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION")
if err != nil {
return err
}
err = h.VPN.read(r)
if err != nil {
return fmt.Errorf("VPN health settings: %w", err)
}
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"time" "time"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -23,8 +24,6 @@ func (h HealthyWait) validate() (err error) {
return nil return nil
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HealthyWait) copy() (copied HealthyWait) { func (h *HealthyWait) copy() (copied HealthyWait) {
return HealthyWait{ return HealthyWait{
Initial: gosettings.CopyPointer(h.Initial), Initial: gosettings.CopyPointer(h.Initial),
@@ -32,13 +31,6 @@ func (h *HealthyWait) copy() (copied HealthyWait) {
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HealthyWait) mergeWith(other HealthyWait) {
h.Initial = gosettings.MergeWithPointer(h.Initial, other.Initial)
h.Addition = gosettings.MergeWithPointer(h.Addition, other.Addition)
}
// 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.
@@ -64,3 +56,21 @@ func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) {
node.Appendf("Additional duration: %s", *h.Addition) node.Appendf("Additional duration: %s", *h.Addition)
return node return node
} }
func (h *HealthyWait) read(r *reader.Reader) (err error) {
h.Initial, err = r.DurationPtr(
"HEALTH_VPN_DURATION_INITIAL",
reader.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL"))
if err != nil {
return err
}
h.Addition, err = r.DurationPtr(
"HEALTH_VPN_DURATION_ADDITION",
reader.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION"))
if err != nil {
return err
}
return nil
}

View File

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

View File

@@ -1,8 +0,0 @@
package helpers
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,11 +3,13 @@ package settings
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"time" "time"
"github.com/qdm12/gosettings" "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.
@@ -44,9 +46,7 @@ type HTTPProxy struct {
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)
} }
@@ -67,44 +67,31 @@ func (h *HTTPProxy) copy() (copied HTTPProxy) {
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HTTPProxy) mergeWith(other HTTPProxy) {
h.User = gosettings.MergeWithPointer(h.User, other.User)
h.Password = gosettings.MergeWithPointer(h.Password, other.Password)
h.ListeningAddress = gosettings.MergeWithString(h.ListeningAddress, other.ListeningAddress)
h.Enabled = gosettings.MergeWithPointer(h.Enabled, other.Enabled)
h.Stealth = gosettings.MergeWithPointer(h.Stealth, other.Stealth)
h.Log = gosettings.MergeWithPointer(h.Log, other.Log)
h.ReadHeaderTimeout = gosettings.MergeWithNumber(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
h.ReadTimeout = gosettings.MergeWithNumber(h.ReadTimeout, other.ReadTimeout)
}
// 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 = gosettings.OverrideWithPointer(h.User, other.User) h.User = gosettings.OverrideWithPointer(h.User, other.User)
h.Password = gosettings.OverrideWithPointer(h.Password, other.Password) h.Password = gosettings.OverrideWithPointer(h.Password, other.Password)
h.ListeningAddress = gosettings.OverrideWithString(h.ListeningAddress, other.ListeningAddress) h.ListeningAddress = gosettings.OverrideWithComparable(h.ListeningAddress, other.ListeningAddress)
h.Enabled = gosettings.OverrideWithPointer(h.Enabled, other.Enabled) h.Enabled = gosettings.OverrideWithPointer(h.Enabled, other.Enabled)
h.Stealth = gosettings.OverrideWithPointer(h.Stealth, other.Stealth) h.Stealth = gosettings.OverrideWithPointer(h.Stealth, other.Stealth)
h.Log = gosettings.OverrideWithPointer(h.Log, other.Log) h.Log = gosettings.OverrideWithPointer(h.Log, other.Log)
h.ReadHeaderTimeout = gosettings.OverrideWithNumber(h.ReadHeaderTimeout, other.ReadHeaderTimeout) h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
h.ReadTimeout = gosettings.OverrideWithNumber(h.ReadTimeout, other.ReadTimeout) h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
} }
func (h *HTTPProxy) setDefaults() { func (h *HTTPProxy) setDefaults() {
h.User = gosettings.DefaultPointer(h.User, "") h.User = gosettings.DefaultPointer(h.User, "")
h.Password = gosettings.DefaultPointer(h.Password, "") h.Password = gosettings.DefaultPointer(h.Password, "")
h.ListeningAddress = gosettings.DefaultString(h.ListeningAddress, ":8888") h.ListeningAddress = gosettings.DefaultComparable(h.ListeningAddress, ":8888")
h.Enabled = gosettings.DefaultPointer(h.Enabled, false) h.Enabled = gosettings.DefaultPointer(h.Enabled, false)
h.Stealth = gosettings.DefaultPointer(h.Stealth, false) h.Stealth = gosettings.DefaultPointer(h.Stealth, false)
h.Log = gosettings.DefaultPointer(h.Log, false) h.Log = gosettings.DefaultPointer(h.Log, false)
const defaultReadHeaderTimeout = time.Second const defaultReadHeaderTimeout = time.Second
h.ReadHeaderTimeout = gosettings.DefaultNumber(h.ReadHeaderTimeout, defaultReadHeaderTimeout) h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
const defaultReadTimeout = 3 * time.Second const defaultReadTimeout = 3 * time.Second
h.ReadTimeout = gosettings.DefaultNumber(h.ReadTimeout, defaultReadTimeout) h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
} }
func (h HTTPProxy) String() string { func (h HTTPProxy) String() string {
@@ -128,3 +115,68 @@ func (h HTTPProxy) toLinesNode() (node *gotree.Node) {
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,7 +1,10 @@
package settings package settings
import ( import (
"fmt"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"github.com/qdm12/log" "github.com/qdm12/log"
) )
@@ -9,35 +12,33 @@ import (
// 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 *log.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: gosettings.CopyPointer(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 = gosettings.MergeWithPointer(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 = gosettings.OverrideWithPointer(l.Level, other.Level) l.Level = gosettings.OverrideWithComparable(l.Level, other.Level)
} }
func (l *Log) setDefaults() { func (l *Log) setDefaults() {
l.Level = gosettings.DefaultPointer(l.Level, log.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

@@ -1,36 +0,0 @@
package settings
import (
"net/netip"
"inet.af/netaddr"
)
func netipAddressToNetaddrIP(address netip.Addr) (ip netaddr.IP) {
if address.Is4() {
return netaddr.IPFrom4(address.As4())
}
return netaddr.IPFrom16(address.As16())
}
func netipAddressesToNetaddrIPs(addresses []netip.Addr) (ips []netaddr.IP) {
ips = make([]netaddr.IP, len(addresses))
for i := range addresses {
ips[i] = netipAddressToNetaddrIP(addresses[i])
}
return ips
}
func netipPrefixToNetaddrIPPrefix(prefix netip.Prefix) (ipPrefix netaddr.IPPrefix) {
netaddrIP := netipAddressToNetaddrIP(prefix.Addr())
bits := prefix.Bits()
return netaddr.IPPrefixFrom(netaddrIP, uint8(bits))
}
func netipPrefixesToNetaddrIPPrefixes(prefixes []netip.Prefix) (ipPrefixes []netaddr.IPPrefix) {
ipPrefixes = make([]netaddr.IPPrefix, len(prefixes))
for i := range ipPrefixes {
ipPrefixes[i] = netipPrefixToNetaddrIPPrefix(prefixes[i])
}
return ipPrefixes
}

View File

@@ -4,7 +4,8 @@ package settings
// and SERVER_REGIONS is now the continent field for servers. // and SERVER_REGIONS is now the continent field for servers.
// TODO v4 remove. // TODO v4 remove.
func nordvpnRetroRegion(selection ServerSelection, validRegions, validCountries []string) ( func nordvpnRetroRegion(selection ServerSelection, validRegions, validCountries []string) (
updatedSelection ServerSelection) { updatedSelection ServerSelection,
) {
validRegionsMap := stringSliceToMap(validRegions) validRegionsMap := stringSliceToMap(validRegions)
validCountriesMap := stringSliceToMap(validCountries) validCountriesMap := stringSliceToMap(validCountries)

View File

@@ -4,12 +4,14 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"regexp" "regexp"
"strings"
"github.com/qdm12/gluetun/internal/constants/openvpn" "github.com/qdm12/gluetun/internal/constants/openvpn"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/openvpn/extract" "github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets" "github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate" "github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -153,7 +155,8 @@ 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
} }
@@ -177,7 +180,8 @@ func validateOpenVPNConfigFilepath(isCustom bool,
} }
func validateOpenVPNClientCertificate(vpnProvider, func validateOpenVPNClientCertificate(vpnProvider,
clientCert string) (err error) { clientCert string,
) (err error) {
switch vpnProvider { switch vpnProvider {
case case
providers.Airvpn, providers.Airvpn,
@@ -224,7 +228,8 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
} }
func validateOpenVPNEncryptedKey(vpnProvider, func validateOpenVPNEncryptedKey(vpnProvider,
encryptedPrivateKey string) (err error) { encryptedPrivateKey string,
) (err error) {
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" { if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
return fmt.Errorf("%w", ErrMissingValue) return fmt.Errorf("%w", ErrMissingValue)
} }
@@ -261,32 +266,11 @@ func (o *OpenVPN) copy() (copied OpenVPN) {
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (o *OpenVPN) mergeWith(other OpenVPN) {
o.Version = gosettings.MergeWithString(o.Version, other.Version)
o.User = gosettings.MergeWithPointer(o.User, other.User)
o.Password = gosettings.MergeWithPointer(o.Password, other.Password)
o.ConfFile = gosettings.MergeWithPointer(o.ConfFile, other.ConfFile)
o.Ciphers = gosettings.MergeWithSlice(o.Ciphers, other.Ciphers)
o.Auth = gosettings.MergeWithPointer(o.Auth, other.Auth)
o.Cert = gosettings.MergeWithPointer(o.Cert, other.Cert)
o.Key = gosettings.MergeWithPointer(o.Key, other.Key)
o.EncryptedKey = gosettings.MergeWithPointer(o.EncryptedKey, other.EncryptedKey)
o.KeyPassphrase = gosettings.MergeWithPointer(o.KeyPassphrase, other.KeyPassphrase)
o.PIAEncPreset = gosettings.MergeWithPointer(o.PIAEncPreset, other.PIAEncPreset)
o.MSSFix = gosettings.MergeWithPointer(o.MSSFix, other.MSSFix)
o.Interface = gosettings.MergeWithString(o.Interface, other.Interface)
o.ProcessUser = gosettings.MergeWithString(o.ProcessUser, other.ProcessUser)
o.Verbosity = gosettings.MergeWithPointer(o.Verbosity, other.Verbosity)
o.Flags = gosettings.MergeWithSlice(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 = gosettings.OverrideWithString(o.Version, other.Version) o.Version = gosettings.OverrideWithComparable(o.Version, other.Version)
o.User = gosettings.OverrideWithPointer(o.User, other.User) o.User = gosettings.OverrideWithPointer(o.User, other.User)
o.Password = gosettings.OverrideWithPointer(o.Password, other.Password) o.Password = gosettings.OverrideWithPointer(o.Password, other.Password)
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile) o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
@@ -298,14 +282,14 @@ func (o *OpenVPN) overrideWith(other OpenVPN) {
o.KeyPassphrase = gosettings.OverrideWithPointer(o.KeyPassphrase, other.KeyPassphrase) o.KeyPassphrase = gosettings.OverrideWithPointer(o.KeyPassphrase, other.KeyPassphrase)
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset) o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
o.MSSFix = gosettings.OverrideWithPointer(o.MSSFix, other.MSSFix) o.MSSFix = gosettings.OverrideWithPointer(o.MSSFix, other.MSSFix)
o.Interface = gosettings.OverrideWithString(o.Interface, other.Interface) o.Interface = gosettings.OverrideWithComparable(o.Interface, other.Interface)
o.ProcessUser = gosettings.OverrideWithString(o.ProcessUser, other.ProcessUser) o.ProcessUser = gosettings.OverrideWithComparable(o.ProcessUser, other.ProcessUser)
o.Verbosity = gosettings.OverrideWithPointer(o.Verbosity, other.Verbosity) o.Verbosity = gosettings.OverrideWithPointer(o.Verbosity, other.Verbosity)
o.Flags = gosettings.OverrideWithSlice(o.Flags, other.Flags) o.Flags = gosettings.OverrideWithSlice(o.Flags, other.Flags)
} }
func (o *OpenVPN) setDefaults(vpnProvider string) { func (o *OpenVPN) setDefaults(vpnProvider string) {
o.Version = gosettings.DefaultString(o.Version, openvpn.Openvpn25) o.Version = gosettings.DefaultComparable(o.Version, openvpn.Openvpn26)
o.User = gosettings.DefaultPointer(o.User, "") o.User = gosettings.DefaultPointer(o.User, "")
if vpnProvider == providers.Mullvad { if vpnProvider == providers.Mullvad {
o.Password = gosettings.DefaultPointer(o.Password, "m") o.Password = gosettings.DefaultPointer(o.Password, "m")
@@ -326,8 +310,8 @@ func (o *OpenVPN) setDefaults(vpnProvider string) {
} }
o.PIAEncPreset = gosettings.DefaultPointer(o.PIAEncPreset, defaultEncPreset) o.PIAEncPreset = gosettings.DefaultPointer(o.PIAEncPreset, defaultEncPreset)
o.MSSFix = gosettings.DefaultPointer(o.MSSFix, 0) o.MSSFix = gosettings.DefaultPointer(o.MSSFix, 0)
o.Interface = gosettings.DefaultString(o.Interface, "tun0") o.Interface = gosettings.DefaultComparable(o.Interface, "tun0")
o.ProcessUser = gosettings.DefaultString(o.ProcessUser, "root") o.ProcessUser = gosettings.DefaultComparable(o.ProcessUser, "root")
o.Verbosity = gosettings.DefaultPointer(o.Verbosity, 1) o.Verbosity = gosettings.DefaultPointer(o.Verbosity, 1)
} }
@@ -395,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

@@ -32,7 +32,6 @@ func Test_ivpnAccountID(t *testing.T) {
} }
for _, testCase := range testCases { for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.s, func(t *testing.T) { t.Run(testCase.s, func(t *testing.T) {
t.Parallel() t.Parallel()

View File

@@ -2,11 +2,14 @@ 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/providers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets" "github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate" "github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -17,10 +20,10 @@ type OpenVPNSelection struct {
// 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 `json:"config_file_path"` 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 `json:"tcp"` 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.
@@ -40,12 +43,17 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
} }
} }
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,
providers.Giganews,
providers.Ipvanish, providers.Ipvanish,
providers.Perfectprivacy, providers.Perfectprivacy,
providers.Privado, providers.Privado,
providers.VPNUnlimited,
providers.Vyprvpn, providers.Vyprvpn,
) { ) {
return fmt.Errorf("%w: for VPN service provider %s", return fmt.Errorf("%w: for VPN service provider %s",
@@ -60,7 +68,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
providers.Privatevpn, providers.Torguard: providers.Privatevpn, providers.Torguard:
// no custom port allowed // no custom port allowed
case providers.Expressvpn, providers.Fastestvpn, case providers.Expressvpn, providers.Fastestvpn,
providers.Ipvanish, providers.Nordvpn, providers.Giganews, providers.Ipvanish, providers.Nordvpn,
providers.Privado, providers.Purevpn, providers.Privado, providers.Purevpn,
providers.Surfshark, providers.VPNSecure, providers.Surfshark, providers.VPNSecure,
providers.VPNUnlimited, providers.Vyprvpn: providers.VPNUnlimited, providers.Vyprvpn:
@@ -104,7 +112,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
} }
allowedPorts := allowedUDP allowedPorts := allowedUDP
if *o.TCP { if o.Protocol == constants.TCP {
allowedPorts = allowedTCP allowedPorts = allowedTCP
} }
err = validate.IsOneOf(*o.CustomPort, allowedPorts...) err = validate.IsOneOf(*o.CustomPort, allowedPorts...)
@@ -133,29 +141,22 @@ 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: gosettings.CopyPointer(o.ConfFile), ConfFile: gosettings.CopyPointer(o.ConfFile),
TCP: gosettings.CopyPointer(o.TCP), Protocol: o.Protocol,
CustomPort: gosettings.CopyPointer(o.CustomPort), CustomPort: gosettings.CopyPointer(o.CustomPort),
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset), PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
} }
} }
func (o *OpenVPNSelection) mergeWith(other OpenVPNSelection) {
o.ConfFile = gosettings.MergeWithPointer(o.ConfFile, other.ConfFile)
o.TCP = gosettings.MergeWithPointer(o.TCP, other.TCP)
o.CustomPort = gosettings.MergeWithPointer(o.CustomPort, other.CustomPort)
o.PIAEncPreset = gosettings.MergeWithPointer(o.PIAEncPreset, other.PIAEncPreset)
}
func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) { func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile) o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
o.TCP = gosettings.OverrideWithPointer(o.TCP, other.TCP) o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort) o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
o.PIAEncPreset = gosettings.OverrideWithPointer(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 = gosettings.DefaultPointer(o.ConfFile, "") o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
o.TCP = gosettings.DefaultPointer(o.TCP, false) o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0) o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
var defaultEncPreset string var defaultEncPreset string
@@ -171,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)
@@ -187,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

@@ -6,6 +6,7 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate" "github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -18,7 +19,7 @@ type PortForwarding struct {
// Provider is set to specify which custom port forwarding code // Provider is set to specify which custom port forwarding code
// should be used. This is especially necessary for the custom // should be used. This is especially necessary for the custom
// provider using Wireguard for a provider where Wireguard is not // provider using Wireguard for a provider where Wireguard is not
// natively supported but custom port forwading code is available. // natively supported but custom port forwarding code is available.
// It defaults to the empty string, meaning the current provider // It defaults to the empty string, meaning the current provider
// should be the one used for port forwarding. // should be the one used for port forwarding.
// It cannot be nil for the internal state. // It cannot be nil for the internal state.
@@ -32,6 +33,10 @@ type PortForwarding struct {
// forwarded port. The redirection is disabled if it is set to 0, which // forwarded port. The redirection is disabled if it is set to 0, which
// is its default as well. // is its default as well.
ListeningPort *uint16 `json:"listening_port"` 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) {
@@ -45,7 +50,9 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
providerSelected = *p.Provider providerSelected = *p.Provider
} }
validProviders := []string{ validProviders := []string{
providers.Perfectprivacy,
providers.PrivateInternetAccess, providers.PrivateInternetAccess,
providers.Privatevpn,
providers.Protonvpn, providers.Protonvpn,
} }
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil { if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
@@ -60,6 +67,15 @@ 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
} }
@@ -69,21 +85,18 @@ func (p *PortForwarding) Copy() (copied PortForwarding) {
Provider: gosettings.CopyPointer(p.Provider), Provider: gosettings.CopyPointer(p.Provider),
Filepath: gosettings.CopyPointer(p.Filepath), Filepath: gosettings.CopyPointer(p.Filepath),
ListeningPort: gosettings.CopyPointer(p.ListeningPort), ListeningPort: gosettings.CopyPointer(p.ListeningPort),
Username: p.Username,
Password: p.Password,
} }
} }
func (p *PortForwarding) mergeWith(other PortForwarding) {
p.Enabled = gosettings.MergeWithPointer(p.Enabled, other.Enabled)
p.Provider = gosettings.MergeWithPointer(p.Provider, other.Provider)
p.Filepath = gosettings.MergeWithPointer(p.Filepath, other.Filepath)
p.ListeningPort = gosettings.MergeWithPointer(p.ListeningPort, other.ListeningPort)
}
func (p *PortForwarding) OverrideWith(other PortForwarding) { func (p *PortForwarding) OverrideWith(other PortForwarding) {
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled) p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider) p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath) p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort) p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
} }
func (p *PortForwarding) setDefaults() { func (p *PortForwarding) setDefaults() {
@@ -122,5 +135,54 @@ 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.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.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,10 +2,12 @@ package settings
import ( import (
"fmt" "fmt"
"strings"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn" "github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate" "github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -13,8 +15,8 @@ import (
// 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 `json:"name"` Name string `json:"name"`
// ServerSelection is the settings to // ServerSelection is the settings to
// select the VPN server. // select the VPN server.
ServerSelection ServerSelection `json:"server_selection"` ServerSelection ServerSelection `json:"server_selection"`
@@ -23,7 +25,7 @@ type Provider struct {
} }
// TODO v4 remove pointer for receiver (because of Surfshark). // TODO v4 remove pointer for receiver (because of Surfshark).
func (p *Provider) validate(vpnType string, storage Storage) (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 == vpn.OpenVPN { if vpnType == vpn.OpenVPN {
@@ -33,23 +35,25 @@ func (p *Provider) validate(vpnType string, storage Storage) (err error) {
validNames = []string{ validNames = []string{
providers.Airvpn, providers.Airvpn,
providers.Custom, providers.Custom,
providers.Fastestvpn,
providers.Ivpn, providers.Ivpn,
providers.Mullvad, providers.Mullvad,
providers.Nordvpn, providers.Nordvpn,
providers.Protonvpn,
providers.Surfshark, providers.Surfshark,
providers.Windscribe, providers.Windscribe,
} }
} }
if err = validate.IsOneOf(*p.Name, validNames...); err != nil { if err = validate.IsOneOf(p.Name, validNames...); err != nil {
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err) return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
} }
err = p.ServerSelection.validate(*p.Name, storage) 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)
} }
@@ -59,28 +63,22 @@ func (p *Provider) validate(vpnType string, storage Storage) (err error) {
func (p *Provider) copy() (copied Provider) { func (p *Provider) copy() (copied Provider) {
return Provider{ return Provider{
Name: gosettings.CopyPointer(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 = gosettings.MergeWithPointer(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 = gosettings.OverrideWithPointer(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 = gosettings.DefaultPointer(p.Name, providers.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 {
@@ -89,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,37 @@ package settings
import ( import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"time"
"github.com/qdm12/gluetun/internal/publicip/api" "github.com/qdm12/gluetun/internal/publicip/api"
"github.com/qdm12/gosettings" "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
// API is the API name to use to fetch public IP information. // APIs is the list of public ip APIs to use to fetch public IP information.
// It can be ipinfo or ip2location. It defaults to ipinfo. // If there is more than one API, the first one is used
API string // by default and the others are used as fallbacks in case of
// APIToken is the token to use for the IP data service // the service rate limiting us. It defaults to use all services,
// such as ipinfo.io. It can be the empty string to // with the first one being ipinfo.io for historical reasons.
// indicate not to use a token. It cannot be nil for the APIs []PublicIPAPI
// internal state. }
APIToken *string
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 // UpdateWith deep copies the receiving settings, overrides the copy with
@@ -47,12 +51,6 @@ func (p PublicIP) UpdateWith(partialUpdate PublicIP) (updatedSettings PublicIP,
} }
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 {
@@ -60,9 +58,11 @@ func (p PublicIP) validate() (err error) {
} }
} }
_, err = api.ParseProvider(p.API) for _, publicIPAPI := range p.APIs {
if err != nil { _, err = api.ParseProvider(publicIPAPI.Name)
return fmt.Errorf("API name: %w", err) if err != nil {
return fmt.Errorf("API name: %w", err)
}
} }
return nil return nil
@@ -70,33 +70,27 @@ func (p PublicIP) validate() (err error) {
func (p *PublicIP) copy() (copied PublicIP) { func (p *PublicIP) copy() (copied PublicIP) {
return PublicIP{ return PublicIP{
Period: gosettings.CopyPointer(p.Period), Enabled: gosettings.CopyPointer(p.Enabled),
IPFilepath: gosettings.CopyPointer(p.IPFilepath), IPFilepath: gosettings.CopyPointer(p.IPFilepath),
API: p.API, APIs: gosettings.CopySlice(p.APIs),
APIToken: gosettings.CopyPointer(p.APIToken),
} }
} }
func (p *PublicIP) mergeWith(other PublicIP) {
p.Period = gosettings.MergeWithPointer(p.Period, other.Period)
p.IPFilepath = gosettings.MergeWithPointer(p.IPFilepath, other.IPFilepath)
p.API = gosettings.MergeWithString(p.API, other.API)
p.APIToken = gosettings.MergeWithPointer(p.APIToken, other.APIToken)
}
func (p *PublicIP) overrideWith(other PublicIP) { func (p *PublicIP) overrideWith(other PublicIP) {
p.Period = gosettings.OverrideWithPointer(p.Period, other.Period) p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath) p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath)
p.API = gosettings.OverrideWithString(p.API, other.API) p.APIs = gosettings.OverrideWithSlice(p.APIs, other.APIs)
p.APIToken = gosettings.OverrideWithPointer(p.APIToken, other.APIToken)
} }
func (p *PublicIP) setDefaults() { func (p *PublicIP) setDefaults() {
const defaultPeriod = 12 * time.Hour p.Enabled = gosettings.DefaultPointer(p.Enabled, true)
p.Period = gosettings.DefaultPointer(p.Period, defaultPeriod)
p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip") p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip")
p.API = gosettings.DefaultString(p.API, "ipinfo") p.APIs = gosettings.DefaultSlice(p.APIs, []PublicIPAPI{
p.APIToken = gosettings.DefaultPointer(p.APIToken, "") {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 {
@@ -104,28 +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)
} }
node.Appendf("Public IP data API: %s", p.API) baseAPIString := "Public IP data base API: " + p.APIs[0].Name
if p.APIs[0].Token != "" {
if *p.APIToken != "" { baseAPIString += " (token " + gosettings.ObfuscateKey(p.APIs[0].Token) + ")"
node.Appendf("API token: %s", gosettings.ObfuscateKey(*p.APIToken)) }
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

@@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -18,6 +19,11 @@ 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
} }
func (c ControlServer) validate() (err error) { func (c ControlServer) validate() (err error) {
@@ -43,29 +49,25 @@ func (c ControlServer) validate() (err error) {
func (c *ControlServer) copy() (copied ControlServer) { func (c *ControlServer) copy() (copied ControlServer) {
return ControlServer{ return ControlServer{
Address: gosettings.CopyPointer(c.Address), Address: gosettings.CopyPointer(c.Address),
Log: gosettings.CopyPointer(c.Log), Log: gosettings.CopyPointer(c.Log),
AuthFilePath: c.AuthFilePath,
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (c *ControlServer) mergeWith(other ControlServer) {
c.Address = gosettings.MergeWithPointer(c.Address, other.Address)
c.Log = gosettings.MergeWithPointer(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 = gosettings.OverrideWithPointer(c.Address, other.Address) c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log) c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
} }
func (c *ControlServer) setDefaults() { func (c *ControlServer) setDefaults() {
c.Address = gosettings.DefaultPointer(c.Address, ":8000") c.Address = gosettings.DefaultPointer(c.Address, ":8000")
c.Log = gosettings.DefaultPointer(c.Log, true) c.Log = gosettings.DefaultPointer(c.Log, true)
c.AuthFilePath = gosettings.DefaultComparable(c.AuthFilePath, "/gluetun/auth/config.toml")
} }
func (c ControlServer) String() string { func (c ControlServer) String() string {
@@ -76,5 +78,19 @@ 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", gosettings.BoolToYesNo(c.Log)) node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
node.Appendf("Authentication file path: %s", c.AuthFilePath)
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")
return nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/qdm12/gluetun/internal/constants/vpn" "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"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate" "github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -54,14 +55,21 @@ type ServerSelection struct { //nolint:maligned
// TODO extend to providers using FreeOnly. // TODO extend to providers using FreeOnly.
PremiumOnly *bool `json:"premium_only"` 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 `json:"stream_only"` 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 `json:"multi_hop_only"` MultiHopOnly *bool `json:"multi_hop_only"`
// PortForwardOnly is true if VPN servers that don't support // PortForwardOnly is true if VPN servers that don't support
// port forwarding should be filtered. This is used with PIA. // port forwarding should be filtered. This is used with PIA
// and ProtonVPN.
PortForwardOnly *bool `json:"port_forward_only"` 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 `json:"openvpn"` OpenVPN OpenVPNSelection `json:"openvpn"`
@@ -78,17 +86,20 @@ var (
ErrMultiHopOnlyNotSupported = errors.New("multi hop 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") ErrPortForwardOnlyNotSupported = errors.New("port forwarding only filter is not supported")
ErrFreePremiumBothSet = errors.New("free only and premium only filters are both set") 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,
storage Storage) (err error) { filterChoicesGetter FilterChoicesGetter, warner Warner,
) (err error) {
switch ss.VPN { switch ss.VPN {
case vpn.OpenVPN, vpn.Wireguard: case vpn.OpenVPN, vpn.Wireguard:
default: default:
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN) return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
} }
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, storage) filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, filterChoicesGetter, warner)
if err != nil { if err != nil {
return err // already wrapped error return err // already wrapped error
} }
@@ -101,60 +112,19 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
*ss = surfsharkRetroRegion(*ss) *ss = surfsharkRetroRegion(*ss)
} }
err = validateServerFilters(*ss, filterChoices) err = validateServerFilters(*ss, filterChoices, vpnServiceProvider, warner)
if err != nil { if err != nil {
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err) return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
} }
if *ss.OwnedOnly && err = validateSubscriptionTierFilters(*ss, vpnServiceProvider)
vpnServiceProvider != providers.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 {
providers.Protonvpn, return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
providers.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrFreeOnlyNotSupported, vpnServiceProvider)
}
if *ss.PremiumOnly &&
!helpers.IsOneOf(vpnServiceProvider,
providers.VPNSecure,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrPremiumOnlyNotSupported, vpnServiceProvider)
}
if *ss.FreeOnly && *ss.PremiumOnly {
return fmt.Errorf("%w", ErrFreePremiumBothSet)
}
if *ss.StreamOnly &&
!helpers.IsOneOf(vpnServiceProvider,
providers.Protonvpn,
providers.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrStreamOnlyNotSupported, vpnServiceProvider)
}
if *ss.MultiHopOnly &&
vpnServiceProvider != providers.Surfshark {
return fmt.Errorf("%w: for VPN service provider %s",
ErrMultiHopOnlyNotSupported, vpnServiceProvider)
}
if *ss.PortForwardOnly &&
vpnServiceProvider != providers.PrivateInternetAccess {
// ProtonVPN also supports port forwarding, but on all their servers, so these
// don't have the port forwarding boolean field. As a consequence, we only allow
// the use of PortForwardOnly for Private Internet Access.
return fmt.Errorf("%w: for VPN service provider %s",
ErrPortForwardOnlyNotSupported, vpnServiceProvider)
} }
if ss.VPN == vpn.OpenVPN { if ss.VPN == vpn.OpenVPN {
@@ -173,19 +143,20 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
} }
func getLocationFilterChoices(vpnServiceProvider string, func getLocationFilterChoices(vpnServiceProvider string,
ss *ServerSelection, storage Storage) (filterChoices models.FilterChoices, ss *ServerSelection, filterChoicesGetter FilterChoicesGetter, warner Warner) (
err error) { filterChoices models.FilterChoices, err error,
filterChoices = storage.GetFilterChoices(vpnServiceProvider) ) {
filterChoices = filterChoicesGetter.GetFilterChoices(vpnServiceProvider)
if vpnServiceProvider == providers.Surfshark { if vpnServiceProvider == providers.Surfshark {
// // Retro compatibility // // Retro compatibility
// TODO v4 remove // TODO v4 remove
newAndRetroRegions := append(filterChoices.Regions, validation.SurfsharkRetroLocChoices()...) //nolint:gocritic newAndRetroRegions := append(filterChoices.Regions, validation.SurfsharkRetroLocChoices()...) //nolint:gocritic
err := validate.AreAllOneOfCaseInsensitive(ss.Regions, newAndRetroRegions) err := atLeastOneIsOneOfCaseInsensitive(ss.Regions, newAndRetroRegions, warner)
if err != nil { if err != nil {
// Only return error comparing with newer regions, we don't want to confuse the user // Only return error comparing with newer regions, we don't want to confuse the user
// with the retro regions in the error message. // with the retro regions in the error message.
err = validate.AreAllOneOfCaseInsensitive(ss.Regions, filterChoices.Regions) err = atLeastOneIsOneOfCaseInsensitive(ss.Regions, filterChoices.Regions, warner)
return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err) return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err)
} }
} }
@@ -195,38 +166,54 @@ func getLocationFilterChoices(vpnServiceProvider string,
// 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, filterChoices models.FilterChoices) (err error) { func validateServerFilters(settings ServerSelection, filterChoices models.FilterChoices,
err = validate.AreAllOneOfCaseInsensitive(settings.Countries, filterChoices.Countries) vpnServiceProvider string, warner Warner,
) (err error) {
err = atLeastOneIsOneOfCaseInsensitive(settings.Countries, filterChoices.Countries, warner)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrCountryNotValid, err) return fmt.Errorf("%w: %w", ErrCountryNotValid, err)
} }
err = validate.AreAllOneOfCaseInsensitive(settings.Regions, filterChoices.Regions) err = atLeastOneIsOneOfCaseInsensitive(settings.Regions, filterChoices.Regions, warner)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrRegionNotValid, err) return fmt.Errorf("%w: %w", ErrRegionNotValid, err)
} }
err = validate.AreAllOneOfCaseInsensitive(settings.Cities, filterChoices.Cities) err = atLeastOneIsOneOfCaseInsensitive(settings.Cities, filterChoices.Cities, warner)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrCityNotValid, err) return fmt.Errorf("%w: %w", ErrCityNotValid, err)
} }
err = validate.AreAllOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs) err = atLeastOneIsOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs, warner)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrISPNotValid, err) return fmt.Errorf("%w: %w", ErrISPNotValid, err)
} }
err = validate.AreAllOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames) err = atLeastOneIsOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames, warner)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err) return fmt.Errorf("%w: %w", ErrHostnameNotValid, err)
} }
err = validate.AreAllOneOfCaseInsensitive(settings.Names, filterChoices.Names) if vpnServiceProvider == providers.Custom {
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 { if err != nil {
return fmt.Errorf("%w: %w", ErrNameNotValid, err) return fmt.Errorf("%w: %w", ErrNameNotValid, err)
} }
err = validate.AreAllOneOfCaseInsensitive(settings.Categories, filterChoices.Categories) err = atLeastOneIsOneOfCaseInsensitive(settings.Categories, filterChoices.Categories, warner)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err) return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
} }
@@ -234,6 +221,81 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
return nil return nil
} }
func atLeastOneIsOneOfCaseInsensitive(values, choices []string,
warner Warner,
) (err error) {
if len(values) > 0 && len(choices) == 0 {
return fmt.Errorf("%w", validate.ErrNoChoice)
}
set := make(map[string]struct{}, len(choices))
for _, choice := range choices {
lowercaseChoice := strings.ToLower(choice)
set[lowercaseChoice] = struct{}{}
}
invalidValues := make([]string, 0, len(values))
for _, value := range values {
lowercaseValue := strings.ToLower(value)
_, 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 validateFeatureFilters(settings ServerSelection, vpnServiceProvider string) error {
switch {
case *settings.OwnedOnly && vpnServiceProvider != providers.Mullvad:
return fmt.Errorf("%w", ErrOwnedOnlyNotSupported)
case vpnServiceProvider == providers.Protonvpn && *settings.FreeOnly && *settings.PortForwardOnly:
return fmt.Errorf("%w: together with free only filter", ErrPortForwardOnlyNotSupported)
case *settings.StreamOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
return fmt.Errorf("%w", ErrStreamOnlyNotSupported)
case *settings.MultiHopOnly && vpnServiceProvider != providers.Surfshark:
return fmt.Errorf("%w", ErrMultiHopOnlyNotSupported)
case *settings.PortForwardOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.PrivateInternetAccess, providers.Protonvpn):
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
}
}
func (ss *ServerSelection) copy() (copied ServerSelection) { func (ss *ServerSelection) copy() (copied ServerSelection) {
return ServerSelection{ return ServerSelection{
VPN: ss.VPN, VPN: ss.VPN,
@@ -250,6 +312,8 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
FreeOnly: gosettings.CopyPointer(ss.FreeOnly), FreeOnly: gosettings.CopyPointer(ss.FreeOnly),
PremiumOnly: gosettings.CopyPointer(ss.PremiumOnly), PremiumOnly: gosettings.CopyPointer(ss.PremiumOnly),
StreamOnly: gosettings.CopyPointer(ss.StreamOnly), StreamOnly: gosettings.CopyPointer(ss.StreamOnly),
SecureCoreOnly: gosettings.CopyPointer(ss.SecureCoreOnly),
TorOnly: gosettings.CopyPointer(ss.TorOnly),
PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly), PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly),
MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly), MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly),
OpenVPN: ss.OpenVPN.copy(), OpenVPN: ss.OpenVPN.copy(),
@@ -257,30 +321,8 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
} }
} }
func (ss *ServerSelection) mergeWith(other ServerSelection) {
ss.VPN = gosettings.MergeWithString(ss.VPN, other.VPN)
ss.TargetIP = gosettings.MergeWithValidator(ss.TargetIP, other.TargetIP)
ss.Countries = gosettings.MergeWithSlice(ss.Countries, other.Countries)
ss.Categories = gosettings.MergeWithSlice(ss.Categories, other.Categories)
ss.Regions = gosettings.MergeWithSlice(ss.Regions, other.Regions)
ss.Cities = gosettings.MergeWithSlice(ss.Cities, other.Cities)
ss.ISPs = gosettings.MergeWithSlice(ss.ISPs, other.ISPs)
ss.Hostnames = gosettings.MergeWithSlice(ss.Hostnames, other.Hostnames)
ss.Names = gosettings.MergeWithSlice(ss.Names, other.Names)
ss.Numbers = gosettings.MergeWithSlice(ss.Numbers, other.Numbers)
ss.OwnedOnly = gosettings.MergeWithPointer(ss.OwnedOnly, other.OwnedOnly)
ss.FreeOnly = gosettings.MergeWithPointer(ss.FreeOnly, other.FreeOnly)
ss.PremiumOnly = gosettings.MergeWithPointer(ss.PremiumOnly, other.PremiumOnly)
ss.StreamOnly = gosettings.MergeWithPointer(ss.StreamOnly, other.StreamOnly)
ss.MultiHopOnly = gosettings.MergeWithPointer(ss.MultiHopOnly, other.MultiHopOnly)
ss.PortForwardOnly = gosettings.MergeWithPointer(ss.PortForwardOnly, other.PortForwardOnly)
ss.OpenVPN.mergeWith(other.OpenVPN)
ss.Wireguard.mergeWith(other.Wireguard)
}
func (ss *ServerSelection) overrideWith(other ServerSelection) { func (ss *ServerSelection) overrideWith(other ServerSelection) {
ss.VPN = gosettings.OverrideWithString(ss.VPN, other.VPN) ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN)
ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP) ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP)
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries) ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories) ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
@@ -294,21 +336,30 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
ss.FreeOnly = gosettings.OverrideWithPointer(ss.FreeOnly, other.FreeOnly) ss.FreeOnly = gosettings.OverrideWithPointer(ss.FreeOnly, other.FreeOnly)
ss.PremiumOnly = gosettings.OverrideWithPointer(ss.PremiumOnly, other.PremiumOnly) ss.PremiumOnly = gosettings.OverrideWithPointer(ss.PremiumOnly, other.PremiumOnly)
ss.StreamOnly = gosettings.OverrideWithPointer(ss.StreamOnly, other.StreamOnly) 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.MultiHopOnly = gosettings.OverrideWithPointer(ss.MultiHopOnly, other.MultiHopOnly)
ss.PortForwardOnly = gosettings.OverrideWithPointer(ss.PortForwardOnly, other.PortForwardOnly) 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 = gosettings.DefaultString(ss.VPN, vpn.OpenVPN) ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
ss.TargetIP = gosettings.DefaultValidator(ss.TargetIP, netip.IPv4Unspecified()) ss.TargetIP = gosettings.DefaultValidator(ss.TargetIP, netip.IPv4Unspecified())
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false) ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false) ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false) ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
ss.StreamOnly = gosettings.DefaultPointer(ss.StreamOnly, 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) ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, false) defaultPortForwardOnly := false
if portForwardingEnabled && helpers.IsOneOf(vpnProvider,
providers.PrivateInternetAccess, providers.Protonvpn) {
defaultPortForwardOnly = true
}
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
ss.OpenVPN.setDefaults(vpnProvider) ss.OpenVPN.setDefaults(vpnProvider)
ss.Wireguard.setDefaults() ss.Wireguard.setDefaults()
} }
@@ -375,10 +426,22 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
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.PortForwardOnly {
node.Appendf("Port forwarding only servers: yes")
}
if ss.VPN == vpn.OpenVPN { if ss.VPN == vpn.OpenVPN {
node.AppendNode(ss.OpenVPN.toLinesNode()) node.AppendNode(ss.OpenVPN.toLinesNode())
} else { } else {
@@ -391,6 +454,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

@@ -2,12 +2,14 @@ package settings
import ( import (
"fmt" "fmt"
"net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn" "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"
) )
@@ -20,6 +22,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
@@ -27,14 +30,16 @@ type Settings struct {
Pprof pprof.Settings Pprof pprof.Settings
} }
type Storage interface { type FilterChoicesGetter interface {
GetFilterChoices(provider string) models.FilterChoices 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(storage Storage, ipv6Supported bool) (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,
@@ -44,12 +49,13 @@ func (s *Settings) Validate(storage Storage, ipv6Supported bool) (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(storage, ipv6Supported) return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
}, },
} }
@@ -73,6 +79,7 @@ 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(),
@@ -81,24 +88,9 @@ func (s *Settings) copy() (copied Settings) {
} }
} }
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,
storage Storage, ipv6Supported bool) (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)
@@ -108,12 +100,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.OverrideWith(other.Pprof) patchedSettings.Pprof.OverrideWith(other.Pprof)
err = patchedSettings.Validate(storage, ipv6Supported) err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
if err != nil { if err != nil {
return err return err
} }
@@ -130,10 +123,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()
} }
@@ -152,6 +146,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())
@@ -162,16 +157,16 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
} }
func (s Settings) Warnings() (warnings []string) { func (s Settings) Warnings() (warnings []string) {
if *s.VPN.Provider.Name == providers.HideMyAss { if s.VPN.Provider.Name == providers.HideMyAss {
warnings = append(warnings, "HideMyAss dropped support for Linux OpenVPN "+ warnings = append(warnings, "HideMyAss dropped support for Linux OpenVPN "+
" so this will likely not work anymore. See https://github.com/qdm12/gluetun/issues/1498.") " so this will likely not work anymore. See https://github.com/qdm12/gluetun/issues/1498.")
} }
if helpers.IsOneOf(*s.VPN.Provider.Name, providers.SlickVPN) && if helpers.IsOneOf(s.VPN.Provider.Name, providers.SlickVPN) &&
s.VPN.Type == vpn.OpenVPN { s.VPN.Type == vpn.OpenVPN {
warnings = append(warnings, "OpenVPN 2.5 uses OpenSSL 3 "+ warnings = append(warnings, "OpenVPN 2.5 and 2.6 use OpenSSL 3 "+
"which prohibits the usage of weak security in today's standards. "+ "which prohibits the usage of weak security in today's standards. "+
*s.VPN.Provider.Name+" uses weak security which is out "+ s.VPN.Provider.Name+" uses weak security which is out "+
"of Gluetun's control so the only workaround is to allow such weaknesses "+ "of Gluetun's control so the only workaround is to allow such weaknesses "+
`using the OpenVPN option tls-cipher "DEFAULT:@SECLEVEL=0". `+ `using the OpenVPN option tls-cipher "DEFAULT:@SECLEVEL=0". `+
"You might want to reach to your provider so they upgrade their certificates. "+ "You might want to reach to your provider so they upgrade their certificates. "+
@@ -179,5 +174,49 @@ func (s Settings) Warnings() (warnings []string) {
"by creating an issue, attaching the new certificate and we will update Gluetun.") "by creating an issue, attaching the new certificate and we will update Gluetun.")
} }
// TODO remove in v4
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
" so the DNS over TLS (DoT) server will not be used."+
" The default value changed to 127.0.0.1 so it uses the internal DoT serves."+
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server"+
" corresponding to the first DoT provider chosen is used.")
}
return warnings 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
}

View File

@@ -30,7 +30,7 @@ func Test_Settings_String(t *testing.T) {
| | ├── Protocol: UDP | | ├── Protocol: UDP
| | └── Private Internet Access encryption preset: strong | | └── Private Internet Access encryption preset: strong
| └── OpenVPN settings: | └── OpenVPN settings:
| ├── OpenVPN version: 2.5 | ├── OpenVPN version: 2.6
| ├── User: [not set] | ├── User: [not set]
| ├── Password: [not set] | ├── Password: [not set]
| ├── Private Internet Access encryption preset: strong | ├── Private Internet Access encryption preset: strong
@@ -43,18 +43,10 @@ func Test_Settings_String(t *testing.T) {
| └── DNS over TLS settings: | └── DNS over TLS settings:
| ├── Enabled: yes | ├── Enabled: yes
| ├── Update period: every 24h0m0s | ├── Update period: every 24h0m0s
| ├── Unbound settings: | ├── Upstream resolvers:
| | ── Authoritative servers: | | ── Cloudflare
| | | └── Cloudflare | ├── Caching: yes
| | ├── Caching: yes | ├── IPv6: no
| | ├── IPv6: no
| | ├── Verbosity level: 1
| | ├── Verbosity details level: 0
| | ├── Validation log level: 0
| | ├── System user: root
| | └── Allowed networks:
| | ├── 0.0.0.0/0
| | └── ::/0
| └── DNS filtering settings: | └── DNS filtering settings:
| ├── Block malicious: yes | ├── Block malicious: yes
| ├── Block ads: no | ├── Block ads: no
@@ -78,21 +70,26 @@ func Test_Settings_String(t *testing.T) {
| └── Enabled: no | └── Enabled: no
├── Control server settings: ├── Control server settings:
| ├── Listening address: :8000 | ├── Listening address: :8000
| ── Logging: yes | ── Logging: yes
| └── Authentication file path: /gluetun/auth/config.toml
├── Storage settings:
| └── Filepath: /gluetun/servers.json
├── OS Alpine settings: ├── OS Alpine settings:
| ├── Process UID: 1000 | ├── Process UID: 1000
| └── Process GID: 1000 | └── Process GID: 1000
├── Public IP settings: ├── Public IP settings:
| ├── Fetching: every 12h0m0s
| ├── IP file path: /tmp/gluetun/ip | ├── IP file path: /tmp/gluetun/ip
| ── Public IP data API: ipinfo | ── Public IP data base API: ipinfo
| └── Public IP data backup APIs:
| ├── cloudflare
| ├── ifconfigco
| └── ip2location
└── Version settings: └── Version settings:
└── Enabled: yes`, └── Enabled: yes`,
}, },
} }
for name, testCase := range testCases { for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Parallel() t.Parallel()

View File

@@ -1,7 +1,10 @@
package settings package settings
import ( import (
"fmt"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"github.com/qdm12/ss-server/pkg/tcpudp" "github.com/qdm12/ss-server/pkg/tcpudp"
) )
@@ -26,13 +29,6 @@ func (s *Shadowsocks) copy() (copied Shadowsocks) {
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (s *Shadowsocks) mergeWith(other Shadowsocks) {
s.Enabled = gosettings.MergeWithPointer(s.Enabled, other.Enabled)
s.Settings = s.Settings.MergeWith(other.Settings)
}
// 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.
@@ -59,10 +55,44 @@ func (s Shadowsocks) toLinesNode() (node *gotree.Node) {
} }
// TODO have ToLinesNode in qdm12/ss-server // TODO have ToLinesNode in qdm12/ss-server
node.Appendf("Listening address: %s", *s.Address) node.Appendf("Listening address: %s", *s.Settings.Address)
node.Appendf("Cipher: %s", s.CipherName) node.Appendf("Cipher: %s", s.Settings.CipherName)
node.Appendf("Password: %s", gosettings.ObfuscateKey(*s.Password)) node.Appendf("Password: %s", gosettings.ObfuscateKey(*s.Settings.Password))
node.Appendf("Log addresses: %s", gosettings.BoolToYesNo(s.LogAddresses)) node.Appendf("Log addresses: %s", gosettings.BoolToYesNo(s.Settings.LogAddresses))
return node return node
} }
func (s *Shadowsocks) read(r *reader.Reader) (err error) {
s.Enabled, err = r.BoolPtr("SHADOWSOCKS")
if err != nil {
return err
}
s.Settings.Address, err = readShadowsocksAddress(r)
if err != nil {
return err
}
s.Settings.LogAddresses, err = r.BoolPtr("SHADOWSOCKS_LOG")
if err != nil {
return err
}
s.Settings.CipherName = r.String("SHADOWSOCKS_CIPHER",
reader.RetroKeys("SHADOWSOCKS_METHOD"))
s.Settings.Password = r.Get("SHADOWSOCKS_PASSWORD",
reader.ForceLowercase(false))
return nil
}
func readShadowsocksAddress(r *reader.Reader) (address *string, err error) {
const currentKey = "SHADOWSOCKS_LISTENING_ADDRESS"
port, err := r.Uint16Ptr("SHADOWSOCKS_PORT", reader.IsRetro(currentKey)) // retro-compatibility
if err != nil {
return nil, err
} else if port != nil {
return ptrTo(fmt.Sprintf(":%d", *port)), nil
}
return r.Get(currentKey), nil
}

View File

@@ -0,0 +1,59 @@
package settings
import (
"fmt"
"path/filepath"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
// Storage contains settings to configure the storage.
type Storage struct {
// Filepath is the path to the servers.json file. An empty string disables on-disk storage.
Filepath *string
}
func (s Storage) validate() (err error) {
if *s.Filepath != "" { // optional
_, err := filepath.Abs(*s.Filepath)
if err != nil {
return fmt.Errorf("filepath is not valid: %w", err)
}
}
return nil
}
func (s *Storage) copy() (copied Storage) {
return Storage{
Filepath: gosettings.CopyPointer(s.Filepath),
}
}
func (s *Storage) overrideWith(other Storage) {
s.Filepath = gosettings.OverrideWithPointer(s.Filepath, other.Filepath)
}
func (s *Storage) setDefaults() {
const defaultFilepath = "/gluetun/servers.json"
s.Filepath = gosettings.DefaultPointer(s.Filepath, defaultFilepath)
}
func (s Storage) String() string {
return s.toLinesNode().String()
}
func (s Storage) toLinesNode() (node *gotree.Node) {
if *s.Filepath == "" {
return gotree.New("Storage settings: disabled")
}
node = gotree.New("Storage settings:")
node.Appendf("Filepath: %s", *s.Filepath)
return node
}
func (s *Storage) read(r *reader.Reader) (err error) {
s.Filepath = r.Get("STORAGE_FILEPATH", reader.AcceptEmpty(true))
return nil
}

View File

@@ -7,7 +7,8 @@ import (
) )
func surfsharkRetroRegion(selection ServerSelection) ( func surfsharkRetroRegion(selection ServerSelection) (
updatedSelection ServerSelection) { updatedSelection ServerSelection,
) {
locationData := servers.LocationData() locationData := servers.LocationData()
retroToLocation := make(map[string]servers.ServerLocation, len(locationData)) retroToLocation := make(map[string]servers.ServerLocation, len(locationData))

View File

@@ -2,6 +2,7 @@ package settings
import ( import (
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -25,16 +26,10 @@ func (s *System) copy() (copied System) {
} }
} }
func (s *System) mergeWith(other System) {
s.PUID = gosettings.MergeWithPointer(s.PUID, other.PUID)
s.PGID = gosettings.MergeWithPointer(s.PGID, other.PGID)
s.Timezone = gosettings.MergeWithString(s.Timezone, other.Timezone)
}
func (s *System) overrideWith(other System) { func (s *System) overrideWith(other System) {
s.PUID = gosettings.OverrideWithPointer(s.PUID, other.PUID) s.PUID = gosettings.OverrideWithPointer(s.PUID, other.PUID)
s.PGID = gosettings.OverrideWithPointer(s.PGID, other.PGID) s.PGID = gosettings.OverrideWithPointer(s.PGID, other.PGID)
s.Timezone = gosettings.OverrideWithString(s.Timezone, other.Timezone) s.Timezone = gosettings.OverrideWithComparable(s.Timezone, other.Timezone)
} }
func (s *System) setDefaults() { func (s *System) setDefaults() {
@@ -59,3 +54,18 @@ func (s System) toLinesNode() (node *gotree.Node) {
return node return node
} }
func (s *System) read(r *reader.Reader) (err error) {
s.PUID, err = r.Uint32Ptr("PUID", reader.RetroKeys("UID"))
if err != nil {
return err
}
s.PGID, err = r.Uint32Ptr("PGID", reader.RetroKeys("GID"))
if err != nil {
return err
}
s.Timezone = r.String("TZ")
return nil
}

View File

@@ -1,202 +0,0 @@
package settings
import (
"errors"
"fmt"
"net/netip"
"github.com/qdm12/dns/pkg/provider"
"github.com/qdm12/dns/pkg/unbound"
"github.com/qdm12/gosettings"
"github.com/qdm12/gotree"
)
// Unbound is settings for the Unbound program.
type Unbound struct {
Providers []string `json:"providers"`
Caching *bool `json:"caching"`
IPv6 *bool `json:"ipv6"`
VerbosityLevel *uint8 `json:"verbosity_level"`
VerbosityDetailsLevel *uint8 `json:"verbosity_details_level"`
ValidationLogLevel *uint8 `json:"validation_log_level"`
Username string `json:"username"`
Allowed []netip.Prefix `json:"allowed"`
}
func (u *Unbound) setDefaults() {
if len(u.Providers) == 0 {
u.Providers = []string{
provider.Cloudflare().String(),
}
}
u.Caching = gosettings.DefaultPointer(u.Caching, true)
u.IPv6 = gosettings.DefaultPointer(u.IPv6, false)
const defaultVerbosityLevel = 1
u.VerbosityLevel = gosettings.DefaultPointer(u.VerbosityLevel, defaultVerbosityLevel)
const defaultVerbosityDetailsLevel = 0
u.VerbosityDetailsLevel = gosettings.DefaultPointer(u.VerbosityDetailsLevel, defaultVerbosityDetailsLevel)
const defaultValidationLogLevel = 0
u.ValidationLogLevel = gosettings.DefaultPointer(u.ValidationLogLevel, defaultValidationLogLevel)
if u.Allowed == nil {
u.Allowed = []netip.Prefix{
netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0),
netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0),
}
}
u.Username = gosettings.DefaultString(u.Username, "root")
}
var (
ErrUnboundVerbosityLevelNotValid = errors.New("Unbound verbosity level is not valid")
ErrUnboundVerbosityDetailsLevelNotValid = errors.New("Unbound verbosity details level is not valid")
ErrUnboundValidationLogLevelNotValid = errors.New("Unbound validation log level is not valid")
)
func (u Unbound) validate() (err error) {
for _, s := range u.Providers {
_, err := provider.Parse(s)
if err != nil {
return err
}
}
const maxVerbosityLevel = 5
if *u.VerbosityLevel > maxVerbosityLevel {
return fmt.Errorf("%w: %d must be between 0 and %d",
ErrUnboundVerbosityLevelNotValid,
*u.VerbosityLevel,
maxVerbosityLevel)
}
const maxVerbosityDetailsLevel = 4
if *u.VerbosityDetailsLevel > maxVerbosityDetailsLevel {
return fmt.Errorf("%w: %d must be between 0 and %d",
ErrUnboundVerbosityDetailsLevelNotValid,
*u.VerbosityDetailsLevel,
maxVerbosityDetailsLevel)
}
const maxValidationLogLevel = 2
if *u.ValidationLogLevel > maxValidationLogLevel {
return fmt.Errorf("%w: %d must be between 0 and %d",
ErrUnboundValidationLogLevelNotValid,
*u.ValidationLogLevel, maxValidationLogLevel)
}
return nil
}
func (u Unbound) copy() (copied Unbound) {
return Unbound{
Providers: gosettings.CopySlice(u.Providers),
Caching: gosettings.CopyPointer(u.Caching),
IPv6: gosettings.CopyPointer(u.IPv6),
VerbosityLevel: gosettings.CopyPointer(u.VerbosityLevel),
VerbosityDetailsLevel: gosettings.CopyPointer(u.VerbosityDetailsLevel),
ValidationLogLevel: gosettings.CopyPointer(u.ValidationLogLevel),
Username: u.Username,
Allowed: gosettings.CopySlice(u.Allowed),
}
}
func (u *Unbound) mergeWith(other Unbound) {
u.Providers = gosettings.MergeWithSlice(u.Providers, other.Providers)
u.Caching = gosettings.MergeWithPointer(u.Caching, other.Caching)
u.IPv6 = gosettings.MergeWithPointer(u.IPv6, other.IPv6)
u.VerbosityLevel = gosettings.MergeWithPointer(u.VerbosityLevel, other.VerbosityLevel)
u.VerbosityDetailsLevel = gosettings.MergeWithPointer(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel)
u.ValidationLogLevel = gosettings.MergeWithPointer(u.ValidationLogLevel, other.ValidationLogLevel)
u.Username = gosettings.MergeWithString(u.Username, other.Username)
u.Allowed = gosettings.MergeWithSlice(u.Allowed, other.Allowed)
}
func (u *Unbound) overrideWith(other Unbound) {
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
u.Caching = gosettings.OverrideWithPointer(u.Caching, other.Caching)
u.IPv6 = gosettings.OverrideWithPointer(u.IPv6, other.IPv6)
u.VerbosityLevel = gosettings.OverrideWithPointer(u.VerbosityLevel, other.VerbosityLevel)
u.VerbosityDetailsLevel = gosettings.OverrideWithPointer(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel)
u.ValidationLogLevel = gosettings.OverrideWithPointer(u.ValidationLogLevel, other.ValidationLogLevel)
u.Username = gosettings.OverrideWithString(u.Username, other.Username)
u.Allowed = gosettings.OverrideWithSlice(u.Allowed, other.Allowed)
}
func (u Unbound) ToUnboundFormat() (settings unbound.Settings, err error) {
providers := make([]provider.Provider, len(u.Providers))
for i := range providers {
providers[i], err = provider.Parse(u.Providers[i])
if err != nil {
return settings, err
}
}
const port = 53
return unbound.Settings{
ListeningPort: port,
IPv4: true,
Providers: providers,
Caching: *u.Caching,
IPv6: *u.IPv6,
VerbosityLevel: *u.VerbosityLevel,
VerbosityDetailsLevel: *u.VerbosityDetailsLevel,
ValidationLogLevel: *u.ValidationLogLevel,
AccessControl: unbound.AccessControlSettings{
Allowed: netipPrefixesToNetaddrIPPrefixes(u.Allowed),
},
Username: u.Username,
}, nil
}
var (
ErrConvertingNetip = errors.New("converting net.IP to netip.Addr failed")
)
func (u Unbound) GetFirstPlaintextIPv4() (ipv4 netip.Addr, err error) {
s := u.Providers[0]
provider, err := provider.Parse(s)
if err != nil {
return ipv4, err
}
ip := provider.DNS().IPv4[0]
ipv4, ok := netip.AddrFromSlice(ip)
if !ok {
return ipv4, fmt.Errorf("%w: for ip %s (%#v)",
ErrConvertingNetip, ip, ip)
}
return ipv4.Unmap(), nil
}
func (u Unbound) String() string {
return u.toLinesNode().String()
}
func (u Unbound) toLinesNode() (node *gotree.Node) {
node = gotree.New("Unbound settings:")
authServers := node.Appendf("Authoritative servers:")
for _, provider := range u.Providers {
authServers.Appendf(provider)
}
node.Appendf("Caching: %s", gosettings.BoolToYesNo(u.Caching))
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(u.IPv6))
node.Appendf("Verbosity level: %d", *u.VerbosityLevel)
node.Appendf("Verbosity details level: %d", *u.VerbosityDetailsLevel)
node.Appendf("Validation log level: %d", *u.ValidationLogLevel)
node.Appendf("System user: %s", u.Username)
allowedNetworks := node.Appendf("Allowed networks:")
for _, network := range u.Allowed {
allowedNetworks.Appendf(network.String())
}
return node
}

View File

@@ -1,43 +0,0 @@
package settings
import (
"encoding/json"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Unbound_JSON(t *testing.T) {
t.Parallel()
settings := Unbound{
Providers: []string{"cloudflare"},
Caching: boolPtr(true),
IPv6: boolPtr(false),
VerbosityLevel: uint8Ptr(1),
VerbosityDetailsLevel: nil,
ValidationLogLevel: uint8Ptr(0),
Username: "user",
Allowed: []netip.Prefix{
netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0),
netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0),
},
}
b, err := json.Marshal(settings)
require.NoError(t, err)
const expected = `{"providers":["cloudflare"],"caching":true,"ipv6":false,` +
`"verbosity_level":1,"verbosity_details_level":null,"validation_log_level":0,` +
`"username":"user","allowed":["0.0.0.0/0","::/0"]}`
assert.Equal(t, expected, string(b))
var resultSettings Unbound
err = json.Unmarshal(b, &resultSettings)
require.NoError(t, err)
assert.Equal(t, settings, resultSettings)
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate" "github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -64,28 +65,19 @@ func (u *Updater) copy() (copied Updater) {
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (u *Updater) mergeWith(other Updater) {
u.Period = gosettings.MergeWithPointer(u.Period, other.Period)
u.DNSAddress = gosettings.MergeWithString(u.DNSAddress, other.DNSAddress)
u.MinRatio = gosettings.MergeWithNumber(u.MinRatio, other.MinRatio)
u.Providers = gosettings.MergeWithSlice(u.Providers, other.Providers)
}
// 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 (u *Updater) overrideWith(other Updater) { func (u *Updater) overrideWith(other Updater) {
u.Period = gosettings.OverrideWithPointer(u.Period, other.Period) u.Period = gosettings.OverrideWithPointer(u.Period, other.Period)
u.DNSAddress = gosettings.OverrideWithString(u.DNSAddress, other.DNSAddress) u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
u.MinRatio = gosettings.OverrideWithNumber(u.MinRatio, other.MinRatio) u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers) u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
} }
func (u *Updater) SetDefaults(vpnProvider string) { func (u *Updater) SetDefaults(vpnProvider string) {
u.Period = gosettings.DefaultPointer(u.Period, 0) u.Period = gosettings.DefaultPointer(u.Period, 0)
u.DNSAddress = gosettings.DefaultString(u.DNSAddress, "1.1.1.1:53") u.DNSAddress = gosettings.DefaultComparable(u.DNSAddress, "1.1.1.1:53")
if u.MinRatio == 0 { if u.MinRatio == 0 {
const defaultMinRatio = 0.8 const defaultMinRatio = 0.8
@@ -114,3 +106,33 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
return node return node
} }
func (u *Updater) read(r *reader.Reader) (err error) {
u.Period, err = r.DurationPtr("UPDATER_PERIOD")
if err != nil {
return err
}
u.DNSAddress, err = readUpdaterDNSAddress()
if err != nil {
return err
}
u.MinRatio, err = r.Float64("UPDATER_MIN_RATIO")
if err != nil {
return err
}
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
return nil
}
func readUpdaterDNSAddress() (address string, err error) {
// TODO this is currently using Cloudflare in
// plaintext to not be blocked by DNS over TLS by default.
// If a plaintext address is set in the DNS settings, this one will be used.
// use custom future encrypted DNS written in Go without blocking
// as it's too much trouble to start another parallel unbound instance for now.
return "", nil
}

View File

@@ -2,6 +2,7 @@ package settings
import ( import (
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -23,12 +24,6 @@ func (v *Version) copy() (copied Version) {
} }
} }
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (v *Version) mergeWith(other Version) {
v.Enabled = gosettings.MergeWithPointer(v.Enabled, other.Enabled)
}
// 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.
@@ -51,3 +46,12 @@ func (v Version) toLinesNode() (node *gotree.Node) {
return node return node
} }
func (v *Version) read(r *reader.Reader) (err error) {
v.Enabled, err = r.BoolPtr("VERSION_INFORMATION")
if err != nil {
return err
}
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/qdm12/gluetun/internal/constants/vpn" "github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate" "github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -20,25 +21,25 @@ type VPN struct {
} }
// TODO v4 remove pointer for receiver (because of Surfshark). // TODO v4 remove pointer for receiver (because of Surfshark).
func (v *VPN) Validate(storage Storage, ipv6Supported bool) (err error) { func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner) (err error) {
// Validate Type // Validate Type
validVPNTypes := []string{vpn.OpenVPN, vpn.Wireguard} validVPNTypes := []string{vpn.OpenVPN, vpn.Wireguard}
if err = validate.IsOneOf(v.Type, validVPNTypes...); err != nil { if err = validate.IsOneOf(v.Type, validVPNTypes...); err != nil {
return fmt.Errorf("%w: %w", ErrVPNTypeNotValid, err) return fmt.Errorf("%w: %w", ErrVPNTypeNotValid, err)
} }
err = v.Provider.validate(v.Type, storage) err = v.Provider.validate(v.Type, filterChoicesGetter, warner)
if err != nil { if err != nil {
return fmt.Errorf("provider settings: %w", err) return fmt.Errorf("provider settings: %w", err)
} }
if v.Type == vpn.OpenVPN { if v.Type == vpn.OpenVPN {
err := v.OpenVPN.validate(*v.Provider.Name) err := v.OpenVPN.validate(v.Provider.Name)
if err != nil { if err != nil {
return fmt.Errorf("OpenVPN settings: %w", err) return fmt.Errorf("OpenVPN settings: %w", err)
} }
} else { } else {
err := v.Wireguard.validate(*v.Provider.Name, ipv6Supported) err := v.Wireguard.validate(v.Provider.Name, ipv6Supported)
if err != nil { if err != nil {
return fmt.Errorf("Wireguard settings: %w", err) return fmt.Errorf("Wireguard settings: %w", err)
} }
@@ -56,25 +57,18 @@ func (v *VPN) Copy() (copied VPN) {
} }
} }
func (v *VPN) mergeWith(other VPN) {
v.Type = gosettings.MergeWithString(v.Type, other.Type)
v.Provider.mergeWith(other.Provider)
v.OpenVPN.mergeWith(other.OpenVPN)
v.Wireguard.mergeWith(other.Wireguard)
}
func (v *VPN) OverrideWith(other VPN) { func (v *VPN) OverrideWith(other VPN) {
v.Type = gosettings.OverrideWithString(v.Type, other.Type) v.Type = gosettings.OverrideWithComparable(v.Type, other.Type)
v.Provider.overrideWith(other.Provider) v.Provider.overrideWith(other.Provider)
v.OpenVPN.overrideWith(other.OpenVPN) v.OpenVPN.overrideWith(other.OpenVPN)
v.Wireguard.overrideWith(other.Wireguard) v.Wireguard.overrideWith(other.Wireguard)
} }
func (v *VPN) setDefaults() { func (v *VPN) setDefaults() {
v.Type = gosettings.DefaultString(v.Type, vpn.OpenVPN) v.Type = gosettings.DefaultComparable(v.Type, vpn.OpenVPN)
v.Provider.setDefaults() v.Provider.setDefaults()
v.OpenVPN.setDefaults(*v.Provider.Name) v.OpenVPN.setDefaults(v.Provider.Name)
v.Wireguard.setDefaults(*v.Provider.Name) v.Wireguard.setDefaults(v.Provider.Name)
} }
func (v VPN) String() string { func (v VPN) String() string {
@@ -94,3 +88,24 @@ func (v VPN) toLinesNode() (node *gotree.Node) {
return node return node
} }
func (v *VPN) read(r *reader.Reader) (err error) {
v.Type = r.String("VPN_TYPE")
err = v.Provider.read(r, v.Type)
if err != nil {
return fmt.Errorf("VPN provider: %w", err)
}
err = v.OpenVPN.read(r)
if err != nil {
return fmt.Errorf("OpenVPN: %w", err)
}
err = v.Wireguard.read(r)
if err != nil {
return fmt.Errorf("wireguard: %w", err)
}
return nil
}

View File

@@ -4,10 +4,13 @@ import (
"fmt" "fmt"
"net/netip" "net/netip"
"regexp" "regexp"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate" "github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@@ -32,12 +35,16 @@ type Wireguard struct {
// Interface is the name of the Wireguard interface // Interface is the name of the Wireguard interface
// to create. It cannot be the empty string in the // to create. It cannot be the empty string in the
// internal state. // internal state.
Interface string `json:"interface"` Interface string `json:"interface"`
PersistentKeepaliveInterval *time.Duration `json:"persistent_keep_alive_interval"`
// Maximum Transmission Unit (MTU) of the Wireguard interface. // Maximum Transmission Unit (MTU) of the Wireguard interface.
// It cannot be zero in the internal state, and defaults to // It cannot be zero in the internal state, and defaults to
// 1400. Note it is not the wireguard-go MTU default of 1420 // 1320. Note it is not the wireguard-go MTU default of 1420
// because this impacts bandwidth a lot on some VPN providers, // because this impacts bandwidth a lot on some VPN providers,
// see https://github.com/qdm12/gluetun/issues/1650. // see https://github.com/qdm12/gluetun/issues/1650.
// It has been lowered to 1320 following quite a bit of
// investigation in the issue:
// https://github.com/qdm12/gluetun/issues/2533.
MTU uint16 `json:"mtu"` MTU uint16 `json:"mtu"`
// Implementation is the Wireguard implementation to use. // Implementation is the Wireguard implementation to use.
// It can be "auto", "userspace" or "kernelspace". // It can be "auto", "userspace" or "kernelspace".
@@ -54,9 +61,11 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
if !helpers.IsOneOf(vpnProvider, if !helpers.IsOneOf(vpnProvider,
providers.Airvpn, providers.Airvpn,
providers.Custom, providers.Custom,
providers.Fastestvpn,
providers.Ivpn, providers.Ivpn,
providers.Mullvad, providers.Mullvad,
providers.Nordvpn, providers.Nordvpn,
providers.Protonvpn,
providers.Surfshark, providers.Surfshark,
providers.Windscribe, providers.Windscribe,
) { ) {
@@ -121,6 +130,11 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
} }
} }
if *w.PersistentKeepaliveInterval < 0 {
return fmt.Errorf("%w: %s", ErrWireguardKeepAliveNegative,
*w.PersistentKeepaliveInterval)
}
// Validate interface // Validate interface
if !regexpInterfaceName.MatchString(w.Interface) { if !regexpInterfaceName.MatchString(w.Interface) {
return fmt.Errorf("%w: '%s' does not match regex '%s'", return fmt.Errorf("%w: '%s' does not match regex '%s'",
@@ -137,53 +151,52 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
func (w *Wireguard) copy() (copied Wireguard) { func (w *Wireguard) copy() (copied Wireguard) {
return Wireguard{ return Wireguard{
PrivateKey: gosettings.CopyPointer(w.PrivateKey), PrivateKey: gosettings.CopyPointer(w.PrivateKey),
PreSharedKey: gosettings.CopyPointer(w.PreSharedKey), PreSharedKey: gosettings.CopyPointer(w.PreSharedKey),
Addresses: gosettings.CopySlice(w.Addresses), Addresses: gosettings.CopySlice(w.Addresses),
AllowedIPs: gosettings.CopySlice(w.AllowedIPs), AllowedIPs: gosettings.CopySlice(w.AllowedIPs),
Interface: w.Interface, PersistentKeepaliveInterval: gosettings.CopyPointer(w.PersistentKeepaliveInterval),
MTU: w.MTU, Interface: w.Interface,
Implementation: w.Implementation, MTU: w.MTU,
Implementation: w.Implementation,
} }
} }
func (w *Wireguard) mergeWith(other Wireguard) {
w.PrivateKey = gosettings.MergeWithPointer(w.PrivateKey, other.PrivateKey)
w.PreSharedKey = gosettings.MergeWithPointer(w.PreSharedKey, other.PreSharedKey)
w.Addresses = gosettings.MergeWithSlice(w.Addresses, other.Addresses)
w.AllowedIPs = gosettings.MergeWithSlice(w.AllowedIPs, other.AllowedIPs)
w.Interface = gosettings.MergeWithString(w.Interface, other.Interface)
w.MTU = gosettings.MergeWithNumber(w.MTU, other.MTU)
w.Implementation = gosettings.MergeWithString(w.Implementation, other.Implementation)
}
func (w *Wireguard) overrideWith(other Wireguard) { func (w *Wireguard) overrideWith(other Wireguard) {
w.PrivateKey = gosettings.OverrideWithPointer(w.PrivateKey, other.PrivateKey) w.PrivateKey = gosettings.OverrideWithPointer(w.PrivateKey, other.PrivateKey)
w.PreSharedKey = gosettings.OverrideWithPointer(w.PreSharedKey, other.PreSharedKey) w.PreSharedKey = gosettings.OverrideWithPointer(w.PreSharedKey, other.PreSharedKey)
w.Addresses = gosettings.OverrideWithSlice(w.Addresses, other.Addresses) w.Addresses = gosettings.OverrideWithSlice(w.Addresses, other.Addresses)
w.AllowedIPs = gosettings.OverrideWithSlice(w.AllowedIPs, other.AllowedIPs) w.AllowedIPs = gosettings.OverrideWithSlice(w.AllowedIPs, other.AllowedIPs)
w.Interface = gosettings.OverrideWithString(w.Interface, other.Interface) w.PersistentKeepaliveInterval = gosettings.OverrideWithPointer(w.PersistentKeepaliveInterval,
w.MTU = gosettings.OverrideWithNumber(w.MTU, other.MTU) other.PersistentKeepaliveInterval)
w.Implementation = gosettings.OverrideWithString(w.Implementation, other.Implementation) w.Interface = gosettings.OverrideWithComparable(w.Interface, other.Interface)
w.MTU = gosettings.OverrideWithComparable(w.MTU, other.MTU)
w.Implementation = gosettings.OverrideWithComparable(w.Implementation, other.Implementation)
} }
func (w *Wireguard) setDefaults(vpnProvider string) { func (w *Wireguard) setDefaults(vpnProvider string) {
w.PrivateKey = gosettings.DefaultPointer(w.PrivateKey, "") w.PrivateKey = gosettings.DefaultPointer(w.PrivateKey, "")
w.PreSharedKey = gosettings.DefaultPointer(w.PreSharedKey, "") w.PreSharedKey = gosettings.DefaultPointer(w.PreSharedKey, "")
if vpnProvider == providers.Nordvpn { switch vpnProvider {
case providers.Nordvpn:
defaultNordVPNAddress := netip.AddrFrom4([4]byte{10, 5, 0, 2}) defaultNordVPNAddress := netip.AddrFrom4([4]byte{10, 5, 0, 2})
defaultNordVPNPrefix := netip.PrefixFrom(defaultNordVPNAddress, defaultNordVPNAddress.BitLen()) defaultNordVPNPrefix := netip.PrefixFrom(defaultNordVPNAddress, defaultNordVPNAddress.BitLen())
w.Addresses = gosettings.DefaultSlice(w.Addresses, []netip.Prefix{defaultNordVPNPrefix}) w.Addresses = gosettings.DefaultSlice(w.Addresses, []netip.Prefix{defaultNordVPNPrefix})
case providers.Protonvpn:
defaultAddress := netip.AddrFrom4([4]byte{10, 2, 0, 2})
defaultPrefix := netip.PrefixFrom(defaultAddress, defaultAddress.BitLen())
w.Addresses = gosettings.DefaultSlice(w.Addresses, []netip.Prefix{defaultPrefix})
} }
defaultAllowedIPs := []netip.Prefix{ defaultAllowedIPs := []netip.Prefix{
netip.PrefixFrom(netip.IPv4Unspecified(), 0), netip.PrefixFrom(netip.IPv4Unspecified(), 0),
netip.PrefixFrom(netip.IPv6Unspecified(), 0), netip.PrefixFrom(netip.IPv6Unspecified(), 0),
} }
w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs) w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs)
w.Interface = gosettings.DefaultString(w.Interface, "wg0") w.PersistentKeepaliveInterval = gosettings.DefaultPointer(w.PersistentKeepaliveInterval, 0)
const defaultMTU = 1400 w.Interface = gosettings.DefaultComparable(w.Interface, "wg0")
w.MTU = gosettings.DefaultNumber(w.MTU, defaultMTU) const defaultMTU = 1320
w.Implementation = gosettings.DefaultString(w.Implementation, "auto") w.MTU = gosettings.DefaultComparable(w.MTU, defaultMTU)
w.Implementation = gosettings.DefaultComparable(w.Implementation, "auto")
} }
func (w Wireguard) String() string { func (w Wireguard) String() string {
@@ -205,12 +218,16 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
addressesNode := node.Appendf("Interface addresses:") addressesNode := node.Appendf("Interface addresses:")
for _, address := range w.Addresses { for _, address := range w.Addresses {
addressesNode.Appendf(address.String()) addressesNode.Append(address.String())
} }
allowedIPsNode := node.Appendf("Allowed IPs:") allowedIPsNode := node.Appendf("Allowed IPs:")
for _, allowedIP := range w.AllowedIPs { for _, allowedIP := range w.AllowedIPs {
allowedIPsNode.Appendf(allowedIP.String()) allowedIPsNode.Append(allowedIP.String())
}
if *w.PersistentKeepaliveInterval > 0 {
node.Appendf("Persistent keepalive interval: %s", w.PersistentKeepaliveInterval)
} }
interfaceNode := node.Appendf("Network interface: %s", w.Interface) interfaceNode := node.Appendf("Network interface: %s", w.Interface)
@@ -222,3 +239,44 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
return node return node
} }
func (w *Wireguard) read(r *reader.Reader) (err error) {
w.PrivateKey = r.Get("WIREGUARD_PRIVATE_KEY", reader.ForceLowercase(false))
w.PreSharedKey = r.Get("WIREGUARD_PRESHARED_KEY", reader.ForceLowercase(false))
w.Interface = r.String("VPN_INTERFACE",
reader.RetroKeys("WIREGUARD_INTERFACE"), reader.ForceLowercase(false))
w.Implementation = r.String("WIREGUARD_IMPLEMENTATION")
addressStrings := r.CSV("WIREGUARD_ADDRESSES", reader.RetroKeys("WIREGUARD_ADDRESS"))
// WARNING: do not initialize w.Addresses to an empty slice
// or the defaults for nordvpn will not work.
for _, addressString := range addressStrings {
if !strings.ContainsRune(addressString, '/') {
addressString += "/32"
}
addressString = strings.TrimSpace(addressString)
address, err := netip.ParsePrefix(addressString)
if err != nil {
return fmt.Errorf("parsing address: %w", err)
}
w.Addresses = append(w.Addresses, address)
}
w.AllowedIPs, err = r.CSVNetipPrefixes("WIREGUARD_ALLOWED_IPS")
if err != nil {
return err // already wrapped
}
w.PersistentKeepaliveInterval, err = r.DurationPtr("WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL")
if err != nil {
return err
}
mtuPtr, err := r.Uint16Ptr("WIREGUARD_MTU")
if err != nil {
return err
} else if mtuPtr != nil {
w.MTU = *mtuPtr
}
return nil
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings" "github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate" "github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@@ -37,8 +38,9 @@ type WireguardSelection struct {
func (w WireguardSelection) validate(vpnProvider string) (err error) { func (w WireguardSelection) validate(vpnProvider string) (err error) {
// Validate EndpointIP // Validate EndpointIP
switch vpnProvider { switch vpnProvider {
case providers.Airvpn, providers.Ivpn, providers.Mullvad, case providers.Airvpn, providers.Fastestvpn, providers.Ivpn,
providers.Nordvpn, providers.Surfshark, providers.Windscribe: providers.Mullvad, providers.Nordvpn, providers.Protonvpn,
providers.Surfshark, providers.Windscribe:
// endpoint IP addresses are baked in // endpoint IP addresses are baked in
case providers.Custom: case providers.Custom:
if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() { if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() {
@@ -55,7 +57,8 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
return fmt.Errorf("%w", ErrWireguardEndpointPortNotSet) return fmt.Errorf("%w", ErrWireguardEndpointPortNotSet)
} }
// EndpointPort cannot be set // EndpointPort cannot be set
case providers.Surfshark, providers.Nordvpn: case providers.Fastestvpn, providers.Nordvpn,
providers.Protonvpn, providers.Surfshark:
if *w.EndpointPort != 0 { if *w.EndpointPort != 0 {
return fmt.Errorf("%w", ErrWireguardEndpointPortSet) return fmt.Errorf("%w", ErrWireguardEndpointPortSet)
} }
@@ -88,7 +91,7 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
// Validate PublicKey // Validate PublicKey
switch vpnProvider { switch vpnProvider {
case providers.Ivpn, providers.Mullvad, case providers.Fastestvpn, providers.Ivpn, providers.Mullvad,
providers.Surfshark, providers.Windscribe: providers.Surfshark, providers.Windscribe:
// public keys are baked in // public keys are baked in
case providers.Custom: case providers.Custom:
@@ -116,16 +119,10 @@ func (w *WireguardSelection) copy() (copied WireguardSelection) {
} }
} }
func (w *WireguardSelection) mergeWith(other WireguardSelection) {
w.EndpointIP = gosettings.MergeWithValidator(w.EndpointIP, other.EndpointIP)
w.EndpointPort = gosettings.MergeWithPointer(w.EndpointPort, other.EndpointPort)
w.PublicKey = gosettings.MergeWithString(w.PublicKey, other.PublicKey)
}
func (w *WireguardSelection) overrideWith(other WireguardSelection) { func (w *WireguardSelection) overrideWith(other WireguardSelection) {
w.EndpointIP = gosettings.OverrideWithValidator(w.EndpointIP, other.EndpointIP) w.EndpointIP = gosettings.OverrideWithValidator(w.EndpointIP, other.EndpointIP)
w.EndpointPort = gosettings.OverrideWithPointer(w.EndpointPort, other.EndpointPort) w.EndpointPort = gosettings.OverrideWithPointer(w.EndpointPort, other.EndpointPort)
w.PublicKey = gosettings.OverrideWithString(w.PublicKey, other.PublicKey) w.PublicKey = gosettings.OverrideWithComparable(w.PublicKey, other.PublicKey)
} }
func (w *WireguardSelection) setDefaults() { func (w *WireguardSelection) setDefaults() {
@@ -154,3 +151,18 @@ func (w WireguardSelection) toLinesNode() (node *gotree.Node) {
return node return node
} }
func (w *WireguardSelection) read(r *reader.Reader) (err error) {
w.EndpointIP, err = r.NetipAddr("WIREGUARD_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
if err != nil {
return err
}
w.EndpointPort, err = r.Uint16Ptr("WIREGUARD_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
if err != nil {
return err
}
w.PublicKey = r.String("WIREGUARD_PUBLIC_KEY", reader.ForceLowercase(false))
return nil
}

View File

@@ -1,55 +0,0 @@
package env
import (
"fmt"
"net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readDNS() (dns settings.DNS, err error) {
dns.ServerAddress, err = s.readDNSServerAddress()
if err != nil {
return dns, err
}
dns.KeepNameserver, err = s.env.BoolPtr("DNS_KEEP_NAMESERVER")
if err != nil {
return dns, err
}
dns.DoT, err = s.readDoT()
if err != nil {
return dns, fmt.Errorf("DoT settings: %w", err)
}
return dns, nil
}
func (s *Source) readDNSServerAddress() (address netip.Addr, err error) {
const currentKey = "DNS_ADDRESS"
key := firstKeySet(s.env, "DNS_PLAINTEXT_ADDRESS", currentKey)
switch key {
case "":
return address, nil
case currentKey:
default: // Retro-compatibility
s.handleDeprecatedKey(key, currentKey)
}
address, err = s.env.NetipAddr(key)
if err != nil {
return address, err
}
// TODO remove in v4
if address.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
s.warner.Warn(key + " is set to " + address.String() +
" so the DNS over TLS (DoT) server will not be used." +
" The default value changed to 127.0.0.1 so it uses the internal DoT serves." +
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server" +
" corresponding to the first DoT provider chosen is used.")
}
return address, nil
}

View File

@@ -1,73 +0,0 @@
package env
import (
"errors"
"fmt"
"net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readDNSBlacklist() (blacklist settings.DNSBlacklist, err error) {
blacklist.BlockMalicious, err = s.env.BoolPtr("BLOCK_MALICIOUS")
if err != nil {
return blacklist, err
}
blacklist.BlockSurveillance, err = s.env.BoolPtr("BLOCK_SURVEILLANCE",
env.RetroKeys("BLOCK_NSA"))
if err != nil {
return blacklist, err
}
blacklist.BlockAds, err = s.env.BoolPtr("BLOCK_ADS")
if err != nil {
return blacklist, err
}
blacklist.AddBlockedIPs, blacklist.AddBlockedIPPrefixes,
err = s.readDoTPrivateAddresses() // TODO v4 split in 2
if err != nil {
return blacklist, err
}
blacklist.AllowedHosts = s.env.CSV("UNBLOCK") // TODO v4 change name
return blacklist, nil
}
var (
ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
)
func (s *Source) readDoTPrivateAddresses() (ips []netip.Addr,
ipPrefixes []netip.Prefix, err error) {
privateAddresses := s.env.CSV("DOT_PRIVATE_ADDRESS")
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,29 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readDoT() (dot settings.DoT, err error) {
dot.Enabled, err = s.env.BoolPtr("DOT")
if err != nil {
return dot, err
}
dot.UpdatePeriod, err = s.env.DurationPtr("DNS_UPDATE_PERIOD")
if err != nil {
return dot, err
}
dot.Unbound, err = s.readUnbound()
if err != nil {
return dot, err
}
dot.Blacklist, err = s.readDNSBlacklist()
if err != nil {
return dot, err
}
return dot, nil
}

View File

@@ -1,36 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readFirewall() (firewall settings.Firewall, err error) {
firewall.VPNInputPorts, err = s.env.CSVUint16("FIREWALL_VPN_INPUT_PORTS")
if err != nil {
return firewall, err
}
firewall.InputPorts, err = s.env.CSVUint16("FIREWALL_INPUT_PORTS")
if err != nil {
return firewall, err
}
firewall.OutboundSubnets, err = s.env.CSVNetipPrefixes("FIREWALL_OUTBOUND_SUBNETS",
env.RetroKeys("EXTRA_SUBNETS"))
if err != nil {
return firewall, err
}
firewall.Enabled, err = s.env.BoolPtr("FIREWALL")
if err != nil {
return firewall, err
}
firewall.Debug, err = s.env.BoolPtr("FIREWALL_DEBUG")
if err != nil {
return firewall, err
}
return firewall, nil
}

View File

@@ -1,35 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) ReadHealth() (health settings.Health, err error) {
health.ServerAddress = s.env.String("HEALTH_SERVER_ADDRESS")
health.TargetAddress = s.env.String("HEALTH_TARGET_ADDRESS",
env.RetroKeys("HEALTH_ADDRESS_TO_PING"))
successWaitPtr, err := s.env.DurationPtr("HEALTH_SUCCESS_WAIT_DURATION")
if err != nil {
return health, err
} else if successWaitPtr != nil {
health.SuccessWait = *successWaitPtr
}
health.VPN.Initial, err = s.env.DurationPtr(
"HEALTH_VPN_DURATION_INITIAL",
env.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL"))
if err != nil {
return health, err
}
health.VPN.Addition, err = s.env.DurationPtr(
"HEALTH_VPN_DURATION_ADDITION",
env.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION"))
if err != nil {
return health, err
}
return health, nil
}

View File

@@ -1,33 +0,0 @@
package env
import (
"fmt"
"os"
"github.com/qdm12/gosettings/sources/env"
)
func unsetEnvKeys(envKeys []string, err error) (newErr error) {
newErr = err
for _, envKey := range envKeys {
unsetErr := os.Unsetenv(envKey)
if unsetErr != nil && newErr == nil {
newErr = fmt.Errorf("unsetting environment variable %s: %w", envKey, unsetErr)
}
}
return newErr
}
func ptrTo[T any](value T) *T {
return &value
}
func firstKeySet(e env.Env, keys ...string) (firstKeySet string) {
for _, key := range keys {
value := e.Get(key)
if value != nil {
return key
}
}
return ""
}

View File

@@ -1,84 +0,0 @@
package env
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
"github.com/qdm12/govalid/binary"
)
func (s *Source) readHTTPProxy() (httpProxy settings.HTTPProxy, err error) {
httpProxy.User = s.env.Get("HTTPPROXY_USER",
env.RetroKeys("PROXY_USER", "TINYPROXY_USER"),
env.ForceLowercase(false))
httpProxy.Password = s.env.Get("HTTPPROXY_PASSWORD",
env.RetroKeys("PROXY_PASSWORD", "TINYPROXY_PASSWORD"),
env.ForceLowercase(false))
httpProxy.ListeningAddress, err = s.readHTTProxyListeningAddress()
if err != nil {
return httpProxy, err
}
httpProxy.Enabled, err = s.env.BoolPtr("HTTPPROXY", env.RetroKeys("PROXY", "TINYPROXY"))
if err != nil {
return httpProxy, err
}
httpProxy.Stealth, err = s.env.BoolPtr("HTTPPROXY_STEALTH")
if err != nil {
return httpProxy, err
}
httpProxy.Log, err = s.readHTTProxyLog()
if err != nil {
return httpProxy, err
}
return httpProxy, nil
}
func (s *Source) readHTTProxyListeningAddress() (listeningAddress string, err error) {
const currentKey = "HTTPPROXY_LISTENING_ADDRESS"
key := firstKeySet(s.env, "HTTPPROXY_PORT", "TINYPROXY_PORT", "PROXY_PORT",
currentKey)
switch key {
case "":
return "", nil
case currentKey:
return s.env.String(key), nil
}
// Retro-compatible keys using a port only
s.handleDeprecatedKey(key, currentKey)
port, err := s.env.Uint16Ptr(key)
if err != nil {
return "", err
}
return fmt.Sprintf(":%d", *port), nil
}
func (s *Source) readHTTProxyLog() (enabled *bool, err error) {
const currentKey = "HTTPPROXY_LOG"
key := firstKeySet(s.env, "PROXY_LOG", "TINYPROXY_LOG", "HTTPPROXY_LOG")
switch key {
case "":
return nil, nil //nolint:nilnil
case currentKey:
return s.env.BoolPtr(key)
}
// Retro-compatible keys using different boolean verbs
s.handleDeprecatedKey(key, currentKey)
value := s.env.String(key)
retroOption := binary.OptionEnabled("on", "info", "connect", "notice")
enabled, err = binary.Validate(value, retroOption)
if err != nil {
return nil, fmt.Errorf("environment variable %s: %w", key, err)
}
return enabled, nil
}

View File

@@ -1,53 +0,0 @@
package env
import (
"errors"
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/log"
)
func (s *Source) readLog() (log settings.Log, err error) {
log.Level, err = s.readLogLevel()
if err != nil {
return log, err
}
return log, nil
}
func (s *Source) readLogLevel() (level *log.Level, err error) {
value := s.env.String("LOG_LEVEL")
if value == "" {
return nil, nil //nolint:nilnil
}
level = new(log.Level)
*level, err = parseLogLevel(value)
if err != nil {
return nil, fmt.Errorf("environment variable LOG_LEVEL: %w", err)
}
return level, nil
}
var ErrLogLevelUnknown = errors.New("log level is unknown")
func parseLogLevel(s string) (level log.Level, err error) {
switch strings.ToLower(s) {
case "debug":
return log.LevelDebug, nil
case "info":
return log.LevelInfo, nil
case "warning":
return log.LevelWarn, nil
case "error":
return log.LevelError, nil
default:
return level, fmt.Errorf(
"%w: %q is not valid and can be one of debug, info, warning or error",
ErrLogLevelUnknown, s)
}
}

View File

@@ -1,77 +0,0 @@
package env
import (
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readOpenVPN() (
openVPN settings.OpenVPN, err error) {
defer func() {
err = unsetEnvKeys([]string{"OPENVPN_KEY", "OPENVPN_CERT",
"OPENVPN_KEY_PASSPHRASE", "OPENVPN_ENCRYPTED_KEY"}, err)
}()
openVPN.Version = s.env.String("OPENVPN_VERSION")
openVPN.User = s.env.Get("OPENVPN_USER",
env.RetroKeys("USER"), env.ForceLowercase(false))
openVPN.Password = s.env.Get("OPENVPN_PASSWORD",
env.RetroKeys("PASSWORD"), env.ForceLowercase(false))
openVPN.ConfFile = s.env.Get("OPENVPN_CUSTOM_CONFIG", env.ForceLowercase(false))
openVPN.Ciphers = s.env.CSV("OPENVPN_CIPHERS", env.RetroKeys("OPENVPN_CIPHER"))
openVPN.Auth = s.env.Get("OPENVPN_AUTH")
openVPN.Cert = s.env.Get("OPENVPN_CERT", env.ForceLowercase(false))
openVPN.Key = s.env.Get("OPENVPN_KEY", env.ForceLowercase(false))
openVPN.EncryptedKey = s.env.Get("OPENVPN_ENCRYPTED_KEY", env.ForceLowercase(false))
openVPN.KeyPassphrase = s.env.Get("OPENVPN_KEY_PASSPHRASE", env.ForceLowercase(false))
openVPN.PIAEncPreset = s.readPIAEncryptionPreset()
openVPN.MSSFix, err = s.env.Uint16Ptr("OPENVPN_MSSFIX")
if err != nil {
return openVPN, err
}
openVPN.Interface = s.env.String("VPN_INTERFACE",
env.RetroKeys("OPENVPN_INTERFACE"), env.ForceLowercase(false))
openVPN.ProcessUser, err = s.readOpenVPNProcessUser()
if err != nil {
return openVPN, err
}
openVPN.Verbosity, err = s.env.IntPtr("OPENVPN_VERBOSITY")
if err != nil {
return openVPN, err
}
flagsPtr := s.env.Get("OPENVPN_FLAGS", env.ForceLowercase(false))
if flagsPtr != nil {
openVPN.Flags = strings.Fields(*flagsPtr)
}
return openVPN, nil
}
func (s *Source) readPIAEncryptionPreset() (presetPtr *string) {
return s.env.Get(
"PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET",
env.RetroKeys("ENCRYPTION", "PIA_ENCRYPTION"))
}
func (s *Source) readOpenVPNProcessUser() (processUser string, err error) {
value, err := s.env.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 s.env.String("OPENVPN_PROCESS_USER"), nil
}

View File

@@ -1,56 +0,0 @@
package env
import (
"errors"
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readOpenVPNSelection() (
selection settings.OpenVPNSelection, err error) {
selection.ConfFile = s.env.Get("OPENVPN_CUSTOM_CONFIG", env.ForceLowercase(false))
selection.TCP, err = s.readOpenVPNProtocol()
if err != nil {
return selection, err
}
selection.CustomPort, err = s.env.Uint16Ptr("VPN_ENDPOINT_PORT",
env.RetroKeys("PORT", "OPENVPN_PORT"))
if err != nil {
return selection, err
}
selection.PIAEncPreset = s.readPIAEncryptionPreset()
return selection, nil
}
var ErrOpenVPNProtocolNotValid = errors.New("OpenVPN protocol is not valid")
func (s *Source) readOpenVPNProtocol() (tcp *bool, err error) {
const currentKey = "OPENVPN_PROTOCOL"
envKey := firstKeySet(s.env, "PROTOCOL", currentKey)
switch envKey {
case "":
return nil, nil //nolint:nilnil
case currentKey:
default: // Retro compatibility
s.handleDeprecatedKey(envKey, currentKey)
}
protocol := s.env.String(envKey)
switch strings.ToLower(protocol) {
case constants.UDP:
return ptrTo(false), nil
case constants.TCP:
return ptrTo(true), nil
default:
return nil, fmt.Errorf("environment variable %s: %w: %s",
envKey, ErrOpenVPNProtocolNotValid, protocol)
}
}

View File

@@ -1,34 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readPortForward() (
portForwarding settings.PortForwarding, err error) {
portForwarding.Enabled, err = s.env.BoolPtr("VPN_PORT_FORWARDING",
env.RetroKeys(
"PORT_FORWARDING",
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING",
))
if err != nil {
return portForwarding, err
}
portForwarding.Provider = s.env.Get("VPN_PORT_FORWARDING_PROVIDER")
portForwarding.Filepath = s.env.Get("VPN_PORT_FORWARDING_STATUS_FILE",
env.ForceLowercase(false),
env.RetroKeys(
"PORT_FORWARDING_STATUS_FILE",
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
))
portForwarding.ListeningPort, err = s.env.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
if err != nil {
return portForwarding, err
}
return portForwarding, nil
}

View File

@@ -1,26 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/pprof"
)
func (s *Source) readPprof() (settings pprof.Settings, err error) {
settings.Enabled, err = s.env.BoolPtr("PPROF_ENABLED")
if err != nil {
return settings, err
}
settings.BlockProfileRate, err = s.env.IntPtr("PPROF_BLOCK_PROFILE_RATE")
if err != nil {
return settings, err
}
settings.MutexProfileRate, err = s.env.IntPtr("PPROF_MUTEX_PROFILE_RATE")
if err != nil {
return settings, err
}
settings.HTTPServer.Address = s.env.String("PPROF_HTTP_SERVER_ADDRESS")
return settings, nil
}

View File

@@ -1,50 +0,0 @@
package env
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readProvider(vpnType string) (provider settings.Provider, err error) {
provider.Name = s.readVPNServiceProvider(vpnType)
var providerName string
if provider.Name != nil {
providerName = *provider.Name
}
provider.ServerSelection, err = s.readServerSelection(providerName, vpnType)
if err != nil {
return provider, fmt.Errorf("server selection: %w", err)
}
provider.PortForwarding, err = s.readPortForward()
if err != nil {
return provider, fmt.Errorf("port forwarding: %w", err)
}
return provider, nil
}
func (s *Source) readVPNServiceProvider(vpnType string) (vpnProviderPtr *string) {
valuePtr := s.env.Get("VPN_SERVICE_PROVIDER", env.RetroKeys("VPNSP"))
if valuePtr == nil {
if vpnType != vpn.Wireguard && s.env.Get("OPENVPN_CUSTOM_CONFIG") != nil {
// retro compatibility
return ptrTo(providers.Custom)
}
return nil
}
value := *valuePtr
value = strings.ToLower(value)
if value == "pia" { // retro compatibility
return ptrTo(providers.PrivateInternetAccess)
}
return ptrTo(value)
}

View File

@@ -1,22 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readPublicIP() (publicIP settings.PublicIP, err error) {
publicIP.Period, err = s.env.DurationPtr("PUBLICIP_PERIOD")
if err != nil {
return publicIP, err
}
publicIP.IPFilepath = s.env.Get("PUBLICIP_FILE",
env.ForceLowercase(false), env.RetroKeys("IP_STATUS_FILE"))
publicIP.API = s.env.String("PUBLICIP_API")
publicIP.APIToken = s.env.Get("PUBLICIP_API_TOKEN")
return publicIP, nil
}

View File

@@ -1,103 +0,0 @@
package env
import (
"os"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
type Source struct {
env env.Env
warner Warner
handleDeprecatedKey func(deprecatedKey, newKey string)
}
type Warner interface {
Warn(s string)
}
func New(warner Warner) *Source {
handleDeprecatedKey := func(deprecatedKey, newKey string) {
warner.Warn(
"You are using the old environment variable " + deprecatedKey +
", please consider changing it to " + newKey)
}
return &Source{
env: *env.New(os.Environ(), handleDeprecatedKey),
warner: warner,
handleDeprecatedKey: handleDeprecatedKey,
}
}
func (s *Source) String() string { return "environment variables" }
func (s *Source) Read() (settings settings.Settings, err error) {
settings.VPN, err = s.readVPN()
if err != nil {
return settings, err
}
settings.Firewall, err = s.readFirewall()
if err != nil {
return settings, err
}
settings.System, err = s.readSystem()
if err != nil {
return settings, err
}
settings.Health, err = s.ReadHealth()
if err != nil {
return settings, err
}
settings.HTTPProxy, err = s.readHTTPProxy()
if err != nil {
return settings, err
}
settings.Log, err = s.readLog()
if err != nil {
return settings, err
}
settings.PublicIP, err = s.readPublicIP()
if err != nil {
return settings, err
}
settings.Updater, err = s.readUpdater()
if err != nil {
return settings, err
}
settings.Version, err = s.readVersion()
if err != nil {
return settings, err
}
settings.Shadowsocks, err = s.readShadowsocks()
if err != nil {
return settings, err
}
settings.DNS, err = s.readDNS()
if err != nil {
return settings, err
}
settings.ControlServer, err = s.readControlServer()
if err != nil {
return settings, err
}
settings.Pprof, err = s.readPprof()
if err != nil {
return settings, err
}
return settings, nil
}

View File

@@ -1,31 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readControlServer() (controlServer settings.ControlServer, err error) {
controlServer.Log, err = s.env.BoolPtr("HTTP_CONTROL_SERVER_LOG")
if err != nil {
return controlServer, err
}
controlServer.Address = s.readControlServerAddress()
return controlServer, nil
}
func (s *Source) readControlServerAddress() (address *string) {
const currentKey = "HTTP_CONTROL_SERVER_ADDRESS"
key := firstKeySet(s.env, "CONTROL_SERVER_ADDRESS", currentKey)
if key == currentKey {
return s.env.Get(key)
}
s.handleDeprecatedKey(key, currentKey)
value := s.env.Get("CONTROL_SERVER_ADDRESS")
if value == nil {
return nil
}
return ptrTo(":" + *value)
}

View File

@@ -1,92 +0,0 @@
package env
import (
"errors"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readServerSelection(vpnProvider, vpnType string) (
ss settings.ServerSelection, err error) {
ss.VPN = vpnType
ss.TargetIP, err = s.env.NetipAddr("VPN_ENDPOINT_IP",
env.RetroKeys("OPENVPN_TARGET_IP"))
if err != nil {
return ss, err
}
ss.Countries = s.env.CSV("SERVER_COUNTRIES", env.RetroKeys("COUNTRY"))
if vpnProvider == providers.Cyberghost && len(ss.Countries) == 0 {
// Retro-compatibility for Cyberghost using the REGION variable
ss.Countries = s.env.CSV("REGION")
if len(ss.Countries) > 0 {
s.handleDeprecatedKey("REGION", "SERVER_COUNTRIES")
}
}
ss.Regions = s.env.CSV("SERVER_REGIONS", env.RetroKeys("REGION"))
ss.Cities = s.env.CSV("SERVER_CITIES", env.RetroKeys("CITY"))
ss.ISPs = s.env.CSV("ISP")
ss.Hostnames = s.env.CSV("SERVER_HOSTNAMES", env.RetroKeys("SERVER_HOSTNAME"))
ss.Names = s.env.CSV("SERVER_NAMES", env.RetroKeys("SERVER_NAME"))
ss.Numbers, err = s.env.CSVUint16("SERVER_NUMBER")
ss.Categories = s.env.CSV("SERVER_CATEGORIES")
if err != nil {
return ss, err
}
// Mullvad only
ss.OwnedOnly, err = s.env.BoolPtr("OWNED_ONLY", env.RetroKeys("OWNED"))
if err != nil {
return ss, err
}
// VPNUnlimited and ProtonVPN only
ss.FreeOnly, err = s.env.BoolPtr("FREE_ONLY")
if err != nil {
return ss, err
}
// VPNSecure only
ss.PremiumOnly, err = s.env.BoolPtr("PREMIUM_ONLY")
if err != nil {
return ss, err
}
// Surfshark only
ss.MultiHopOnly, err = s.env.BoolPtr("MULTIHOP_ONLY")
if err != nil {
return ss, err
}
// VPNUnlimited only
ss.StreamOnly, err = s.env.BoolPtr("STREAM_ONLY")
if err != nil {
return ss, err
}
// PIA only
ss.PortForwardOnly, err = s.env.BoolPtr("PORT_FORWARD_ONLY")
if err != nil {
return ss, err
}
ss.OpenVPN, err = s.readOpenVPNSelection()
if err != nil {
return ss, err
}
ss.Wireguard, err = s.readWireguardSelection()
if err != nil {
return ss, err
}
return ss, nil
}
var (
ErrInvalidIP = errors.New("invalid IP address")
)

View File

@@ -1,42 +0,0 @@
package env
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readShadowsocks() (shadowsocks settings.Shadowsocks, err error) {
shadowsocks.Enabled, err = s.env.BoolPtr("SHADOWSOCKS")
if err != nil {
return shadowsocks, err
}
shadowsocks.Address, err = s.readShadowsocksAddress()
if err != nil {
return shadowsocks, err
}
shadowsocks.LogAddresses, err = s.env.BoolPtr("SHADOWSOCKS_LOG")
if err != nil {
return shadowsocks, err
}
shadowsocks.CipherName = s.env.String("SHADOWSOCKS_CIPHER",
env.RetroKeys("SHADOWSOCKS_METHOD"))
shadowsocks.Password = s.env.Get("SHADOWSOCKS_PASSWORD", env.ForceLowercase(false))
return shadowsocks, nil
}
func (s *Source) readShadowsocksAddress() (address *string, err error) {
const currentKey = "SHADOWSOCKS_LISTENING_ADDRESS"
port, err := s.env.Uint16Ptr("SHADOWSOCKS_PORT") // retro-compatibility
if err != nil {
return nil, err
} else if port != nil {
s.handleDeprecatedKey("SHADOWSOCKS_PORT", currentKey)
return ptrTo(fmt.Sprintf(":%d", *port)), nil
}
return s.env.Get(currentKey), nil
}

View File

@@ -1,22 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readSystem() (system settings.System, err error) {
system.PUID, err = s.env.Uint32Ptr("PUID", env.RetroKeys("UID"))
if err != nil {
return system, err
}
system.PGID, err = s.env.Uint32Ptr("PGID", env.RetroKeys("GID"))
if err != nil {
return system, err
}
system.Timezone = s.env.String("TZ")
return system, nil
}

View File

@@ -1,36 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readUnbound() (unbound settings.Unbound, err error) {
unbound.Providers = s.env.CSV("DOT_PROVIDERS")
unbound.Caching, err = s.env.BoolPtr("DOT_CACHING")
if err != nil {
return unbound, err
}
unbound.IPv6, err = s.env.BoolPtr("DOT_IPV6")
if err != nil {
return unbound, err
}
unbound.VerbosityLevel, err = s.env.Uint8Ptr("DOT_VERBOSITY")
if err != nil {
return unbound, err
}
unbound.VerbosityDetailsLevel, err = s.env.Uint8Ptr("DOT_VERBOSITY_DETAILS")
if err != nil {
return unbound, err
}
unbound.ValidationLogLevel, err = s.env.Uint8Ptr("DOT_VALIDATION_LOGLEVEL")
if err != nil {
return unbound, err
}
return unbound, nil
}

View File

@@ -1,35 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readUpdater() (updater settings.Updater, err error) {
updater.Period, err = s.env.DurationPtr("UPDATER_PERIOD")
if err != nil {
return updater, err
}
updater.DNSAddress, err = readUpdaterDNSAddress()
if err != nil {
return updater, err
}
updater.MinRatio, err = s.env.Float64("UPDATER_MIN_RATIO")
if err != nil {
return updater, err
}
updater.Providers = s.env.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
return updater, nil
}
func readUpdaterDNSAddress() (address string, err error) {
// TODO this is currently using Cloudflare in
// plaintext to not be blocked by DNS over TLS by default.
// If a plaintext address is set in the DNS settings, this one will be used.
// use custom future encrypted DNS written in Go without blocking
// as it's too much trouble to start another parallel unbound instance for now.
return "", nil
}

View File

@@ -1,14 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readVersion() (version settings.Version, err error) {
version.Enabled, err = s.env.BoolPtr("VERSION_INFORMATION")
if err != nil {
return version, err
}
return version, nil
}

View File

@@ -1,28 +0,0 @@
package env
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readVPN() (vpn settings.VPN, err error) {
vpn.Type = s.env.String("VPN_TYPE")
vpn.Provider, err = s.readProvider(vpn.Type)
if err != nil {
return vpn, fmt.Errorf("VPN provider: %w", err)
}
vpn.OpenVPN, err = s.readOpenVPN()
if err != nil {
return vpn, fmt.Errorf("OpenVPN: %w", err)
}
vpn.Wireguard, err = s.readWireguard()
if err != nil {
return vpn, fmt.Errorf("wireguard: %w", err)
}
return vpn, nil
}

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