Compare commits

..

254 Commits

Author SHA1 Message Date
Quentin McGaw
732f826ec2 fix(settings): read PEM files but b64 env vars
- Extract base64 data from PEM files and secret files
- Environment variables are not PEM encoded and only the base64 data
- Affects OpenVPN certificate, key and encrypted key
2022-08-24 19:31:52 +00:00
Quentin McGaw
0413a0a1ab chore(ci): rework docker hub description workflow
- Run only on base repository
- Rename job from `dockerHubDescription` to `docker-hub-description`
- Limit permissions of job to read only
- Remove unneeded names for steps
2022-07-03 14:31:49 +00:00
Quentin McGaw
cb6e9cb761 docs(readme): add links to add a provider 2022-07-03 13:39:47 +00:00
Quentin McGaw
420ae40901 feat(dev): Add provider example package 2022-07-02 21:04:57 +00:00
Quentin McGaw
34e67f9f99 chore(markdown): alphabetically sorted headers 2022-07-02 20:58:43 +00:00
Quentin McGaw
18c53aa597 docs(readme): simplify heading description 2022-07-02 20:58:43 +00:00
Quentin McGaw
6d2f9b9508 chore(updater): check servers have minimal information 2022-07-02 20:58:43 +00:00
Quentin McGaw
6826b05d58 chore(all): remove all package comments 2022-07-02 20:58:43 +00:00
barino28
9f959dbc6a fix(expressvpn): OpenVPN fragment option and add ciphers (#1047)
* Fragment was defined in `OpenVPNProviderSettings` but was not written to the OpenVPN configuration file.
* Added two additional ciphers to the configuration for ExpressVPN

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

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

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

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

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

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

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

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-09 16:09:28 -05:00
Quentin McGaw
69eee345d2 feat(ivpn): allow no password for account IDs
- When matching `i-xxxx-xxxx-xxxx` username
- When matching `ivpn-xxxx-xxxx-xxxx` username
2022-03-09 21:01:25 +00:00
Quentin McGaw
48afc05bcb docs(readme): re-add /dev/net/tun since some OS need it 2022-03-09 11:20:05 +00:00
Quentin McGaw
39a62f5db7 feat(firewall): improve error message when NET_ADMIN is missing 2022-03-09 11:16:10 +00:00
Quentin McGaw
006b218ade feat(firewall): auto-detect which iptables
- On `iptables` error, try to use `iptables-nft`
- On `ip6tables` error, try to use `ip6tables-nft`
2022-02-26 22:55:22 +00:00
Quentin McGaw
2b09b9c290 fix(ci): docker metadata image tags
- Move metata as top step in publish workflow
- Simplify `v0.x.x` check
- Dynamically determine base branch
2022-02-26 16:15:31 +00:00
Quentin McGaw
c42865b3d9 chore(ci): merge misspell workflow in ci workflow 2022-02-26 14:01:15 +00:00
Quentin McGaw
836f021a87 chore(lint): add containedctx, decorder and errchkjson 2022-02-26 13:49:53 +00:00
Quentin McGaw
26b049b361 fix(ci): docker/metadata-action logic 2022-02-26 13:39:35 +00:00
Quentin McGaw
e75627365d chore(lint): upgrade golangci-lint to v1.44.2 2022-02-20 21:26:38 +00:00
Quentin McGaw
ae0334c930 chore(sources): wrap error with source string 2022-02-20 03:04:16 +00:00
Quentin McGaw
920ad8b54b chore(errors): review all errors in codebase 2022-02-20 02:58:16 +00:00
Quentin McGaw
ac4a4f83fc chore(settings): split openvpn validation in functions 2022-02-20 00:08:55 +00:00
Quentin McGaw
a4652c2d32 feat(validation): reject server filters ignored for some VPN providers 2022-02-18 14:06:13 +00:00
Quentin McGaw
c40d4e075e chore(validation): move functions from constants
- Move validation functions from `internal/constants` to `internal/configuration/settings/validation`
- Concatenate all OpenVPN constants in `internal/constants/openvpn.go`
2022-02-13 01:21:25 +00:00
Quentin McGaw
95967136d3 feat(firewall): faster setup 75ms to 10ms 2022-02-09 13:41:38 +00:00
Quentin McGaw (desktop)
576c1ee0c5 fix(env): accept uppercase SHADOWSOCKS_CIPHER 2022-02-09 12:33:47 +00:00
Quentin McGaw (desktop)
5d4032edf4 fix(env): accept uppercase OPENVPN_PROTOCOL 2022-02-09 12:33:24 +00:00
Quentin McGaw (desktop)
ff3f84f9fd hotfix(env): OPENVPN_CIPHERS empty parsing 2022-02-06 22:58:23 +00:00
Quentin McGaw
2a19b68b9a hotfix(env): fix parsing of unset server filters 2022-02-06 20:13:40 +00:00
Quentin McGaw
ed6c010aff hotfix(env): fix BLOCK_SURVEILLANCE parsing 2022-02-06 20:06:58 +00:00
Quentin McGaw
783fb38e41 hotfix(env): allow empty VPN_ENDPOINT_IP 2022-02-06 20:02:45 +00:00
Quentin McGaw
fcab4ae3c6 chore(env): SERVER_NAMES variable
- With retro-compatibility with `SERVER_NAME`
2022-02-06 19:59:07 +00:00
Quentin McGaw
a69c456965 chore(env): SERVER_HOSTNAMES variable
- With retro-compatibility with `SERVER_HOSTNAME`
2022-02-06 19:59:07 +00:00
Quentin McGaw
0e6db2f1c5 chore(env): SERVER_REGIONS variable
- With retro-compatibility with `REGION`
2022-02-06 19:59:07 +00:00
Quentin McGaw
7aab18d197 chore(env): SERVER_CITIES variable
- With retro-compatibility with `CITY`
2022-02-06 19:59:07 +00:00
Quentin McGaw
d6b39e66d1 chore(env): SERVER_COUNTRIES variable
- With retro-compatibility with `COUNTRY`
2022-02-06 19:59:07 +00:00
Quentin McGaw
3f5c72d898 chore(env): simplify Cyberghost retro logic 2022-02-06 19:59:07 +00:00
Quentin McGaw
691ade794b chore(env): PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE
- With retro-compatibility with `PORT_FORWARDING_STATUS_FILE`
2022-02-06 19:59:07 +00:00
Quentin McGaw
1693c4ed8a chore(env): PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING
- With retro-compatibility with `PORT_FORWARDING`
2022-02-06 19:59:07 +00:00
Quentin McGaw
ae9b3279c3 chore(env): PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET variable
- With retro-compatibility with `PIA_ENCRYPTION` and `ENCRYPTION`
2022-02-06 19:59:07 +00:00
Quentin McGaw
04956e45c7 chore(env): OPENVPN_CIPHERS variable
- With retro-compatibility with `OPENVPN_CIPHER`
2022-02-06 19:59:07 +00:00
Quentin McGaw
027664af7b chore(env): VPN_SERVICE_PROVIDER variable
- With retro-compatibility with `VPNSP`
2022-02-06 19:59:07 +00:00
Quentin McGaw
f8d5f76bdf chore(env): WIREGUARD_ADDRESSES variable
- With retro-compatibility with `WIREGUARD_ADDRESS`
2022-02-06 19:59:07 +00:00
Quentin McGaw
114f9be47f chore(env): DNS_ADDRESS variable
- With retro-compatibility with `DNS_PLAINTEXT_ADDRESS`
2022-02-06 19:59:07 +00:00
Quentin McGaw
c73369e11c chore(constants): remove and move constant paths
- Remove unused paths
- Move paths to inline constants if used only once
2022-02-06 19:59:07 +00:00
Quentin McGaw
5603e25542 chore(env): VPN_INTERFACE
- With retro-compatibility with `OPENVPN_INTERFACE`
- With retro-compatibility with `WIREGUARD_INTERFACE`
2022-02-06 19:59:07 +00:00
Quentin McGaw
0d8cb66d43 chore(env): getEnvWithRetro helper function 2022-02-06 19:59:07 +00:00
Quentin McGaw
e7e4cfca4c fix(env): Retro-compatible precedence order for variables with defaults set in Dockerfile
- `BLOCK_NSA` has precedence over `BLOCK_SURVEILLANCE`
- `HEALTH_OPENVPN_DURATION_ADDITION` has precedence over `HEALTH_VPN_DURATION_ADDITION`
- `HEALTH_OPENVPN_DURATION_INITIAL` has precendence over `HEALTH_VPN_DURATION_INITIAL`
- Chain of precedence: `PROXY` > `TINYPROXY` > `HTTPPROXY`
- Chain of precedence: `PROXY_LOG_LEVEL` > `TINYPROXY_LOG` > `HTTPPROXY_LOG`
- `PROTOCOL` has precendence over `OPENVPN_PROTOCOL`
- `IP_STATUS_FILE` has precendence over `PUBLICIP_FILE`
- `SHADOWSOCKS_PORT` has precedence over `SHADOWSOCKS_LISTENING_ADDRESS`
- `SHADOWSOCKS_METHOD` has precedence over `SHADOWSOCKS_CIPHER`
2022-02-06 19:59:07 +00:00
Quentin McGaw
fd23f1a29b chore(env): do not validate control server port 2022-02-06 19:59:07 +00:00
Quentin McGaw
57481e3dd7 fix(cyberghost): compat log if COUNTRY is empty 2022-02-06 19:59:07 +00:00
Quentin McGaw
53952b143f fix(server): allow to bind on a random port 2022-02-06 19:59:07 +00:00
Quentin McGaw
e7b0f4c6be feat(vpn): VPN_ENDPOINT_PORT
- Deprecate `OPENVPN_PORT`
- Deprecate `WIREGUARD_ENDPOINT_PORT`
2022-02-06 19:59:07 +00:00
Quentin McGaw
ea143c0c9a feat(vpn): VPN_ENDPOINT_PORT
- Deprecate `OPENVPN_PORT`
- Deprecate `WIREGUARD_ENDPOINT_PORT`
2022-01-28 00:10:23 +00:00
Quentin McGaw
a951110461 feat(vpn): VPN_ENDPOINT_IP
- Deprecate `OPENVPN_TARGET_IP`
- Deprecate `WIREGUARD_ENDPOINT_IP`
2022-01-28 00:09:58 +00:00
Quentin McGaw
7a8f5f53d5 feat(openvpn): OPENVPN_PROCESS_USER and deprecates OPENVPN_ROOT 2022-01-27 23:34:19 +00:00
Quentin McGaw
1b585159d1 feat(server): HTTP_CONTROL_SERVER_PORT to HTTP_CONTROL_SERVER_ADDRESS 2022-01-27 23:15:08 +00:00
Quentin McGaw (desktop)
f3692cd47f feat(mullvad): OWNED to OWNED_ONLY 2022-01-27 14:12:25 +00:00
Quentin McGaw (desktop)
15800fd4ff feat(expressvpn): update built-in data 2022-01-27 13:01:08 +00:00
Quentin McGaw (desktop)
9fb085f361 hotfix(updater): do not default to custom 2022-01-27 12:57:27 +00:00
Quentin McGaw
1e3f878470 feat(updater): UPDATER_VPN_SERVICE_PROVIDERS
- Updater defaults to update the VPN provider in use if enabled
2022-01-26 22:41:06 +00:00
dependabot[bot]
bcf9bfa5d3 Chore(deps): Bump docker/build-push-action from 2.7.0 to 2.8.0 (#801)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 2.7.0 to 2.8.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v2.7.0...v2.8.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-26 17:35:50 -05:00
dependabot[bot]
56bdc1f0ae Chore(deps): Bump github.com/breml/rootcerts from 0.2.1 to 0.2.2 (#812)
Bumps [github.com/breml/rootcerts](https://github.com/breml/rootcerts) from 0.2.1 to 0.2.2.
- [Release notes](https://github.com/breml/rootcerts/releases)
- [Commits](https://github.com/breml/rootcerts/compare/v0.2.1...v0.2.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-26 17:35:36 -05:00
Quentin McGaw
9de6428585 feat(pprof): add pprof HTTP server (#807)
- `PPROF_ENABLED=no`
- `PPROF_BLOCK_PROFILE_RATE=0`
- `PPROF_MUTEX_PROFILE_RATE=0`
- `PPROF_HTTP_SERVER_ADDRESS=":6060"`
2022-01-26 17:23:55 -05:00
Quentin McGaw (desktop)
55e609cbf4 fix(ci): release tag semver check 2022-01-23 15:13:26 +00:00
Kazi
f7319eb7a5 docs(docker-compose): add container_name commented line (#806) 2022-01-22 16:44:16 -08:00
Quentin McGaw (desktop)
2cff64fd80 fix(settings): allow DNS_UPDATE_PERIOD=0 2022-01-23 00:36:45 +00:00
Quentin McGaw (desktop)
fdc0db07e0 fix(updater): do not allow or set custom provider 2022-01-22 22:23:40 +00:00
dependabot[bot]
779cb18590 Chore(deps): Bump github.com/breml/rootcerts from 0.2.0 to 0.2.1 (#769)
Bumps [github.com/breml/rootcerts](https://github.com/breml/rootcerts) from 0.2.0 to 0.2.1.
- [Release notes](https://github.com/breml/rootcerts/releases)
- [Commits](https://github.com/breml/rootcerts/compare/v0.2.0...v0.2.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-22 13:34:43 -08:00
Quentin McGaw (desktop)
a1a1128d6d fix(settings): trim trailing new line from files 2022-01-19 00:38:04 +00:00
Quentin McGaw (desktop)
7a50daca7c feat(Dockerfile): entrypoint name to gluetun-entrypoint 2022-01-19 00:23:50 +00:00
Quentin McGaw
2ad23a09e8 fix(dockerfile): change SHADOWSOCKS_ADDRESS to SHADOWSOCKS_LISTENING_ADDRESS 2022-01-13 17:10:09 +00:00
Quentin McGaw
7e2ea4d74d hotfix(custom): allow empty user and password for OpenVPN 2022-01-13 15:18:39 +00:00
Quentin McGaw
4bdce76041 hotfix(httpproxy): Retro-compat with old env vars 2022-01-12 14:40:19 +00:00
Quentin McGaw
6f5a78c22b hotfix(settings): hostnames merging 2022-01-07 22:41:57 +00:00
Quentin McGaw
d6d529278e feat(privado): update servers data 2022-01-07 22:32:04 +00:00
Quentin McGaw
a430d15ac5 feat(protonvpn): update built-in servers data 2022-01-07 22:29:45 +00:00
Quentin McGaw
b0c2d5f299 hotfix(updater): defaults for settings in CLI mode 2022-01-07 22:29:30 +00:00
Quentin McGaw
b32c01c11a fix(custom): OPENVPN_CUSTOM_CONFIG 2022-01-07 15:12:49 +00:00
Quentin McGaw
05f42f0cb8 fix(httpproxy): HTTPPROXY_PORT variable key 2022-01-07 15:00:23 +00:00
Quentin McGaw
3efe3a524a fix(control-server): listening port value 2022-01-07 09:42:15 +00:00
Quentin McGaw
8a788dfca5 fix(settings): read env for control server 2022-01-07 09:37:31 +00:00
Quentin McGaw
d5f1589ea1 fix(settings): VPNSP value read as lowercase 2022-01-07 09:23:45 +00:00
Quentin McGaw
c40c8413b5 fix(custom): settings parsing for custom VPNSP 2022-01-07 09:23:29 +00:00
Quentin McGaw
ae074dfb2b hotfix(surfshark): REGION retro-compatibility 2022-01-06 19:16:14 +00:00
Quentin McGaw
2c8a8f6cd5 fix(settings): HTTP proxy default address 2022-01-06 17:47:06 +00:00
Quentin McGaw
4c7a09c228 fix(settings): change 2^16 to 65535 2022-01-06 17:42:10 +00:00
Quentin McGaw
ab39edc692 fix(settings): system ID max value 2022-01-06 16:48:06 +00:00
Quentin McGaw
6132cd9df2 fix(pia): encryption strong accepted 2022-01-06 15:04:40 +00:00
Quentin McGaw
7d824a5179 chore(settings): refactor settings processing (#756)
- Better settings tree structure logged using `qdm12/gotree`
- Read settings from environment variables, then files, then secret files
- Settings methods to default them, merge them and override them
- `DNS_PLAINTEXT_ADDRESS` default changed to `127.0.0.1` to use DoT. Warning added if set to something else.
- `HTTPPROXY_LISTENING_ADDRESS` instead of `HTTPPROXY_PORT` (with retro-compatibility)
2022-01-06 06:40:23 -05:00
Quentin McGaw
46738b2934 fix(hidemyass): REGION validation 2021-12-25 11:41:47 +00:00
Quentin McGaw
ca82fcb48e feat(docker): upgrade Alpine to 3.15 2021-12-14 18:16:51 +00:00
Quentin McGaw
cfa3bb3b64 feat(internal/wireguard): opportunistic kernelspace
- Auto detect if kernelspace implementation is available
- Fallback to Go userspace implementation if kernel is not available
2021-12-14 11:03:36 +00:00
714 changed files with 100025 additions and 83164 deletions

View File

@@ -1,2 +1,2 @@
FROM qmcgaw/godevcontainer
RUN apk add wireguard-tools
RUN apk add wireguard-tools htop

View File

@@ -8,7 +8,7 @@
"vscode"
],
"shutdownAction": "stopCompose",
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
"postCreateCommand": "source ~/.windows.sh && go mod download && go mod tidy",
"workspaceFolder": "/workspace",
"extensions": [
"golang.go",
@@ -25,6 +25,7 @@
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
"IBM.output-colorizer", // Colorize your output/test logs
"mohsen1.prettify-json", // Prettify JSON data
"github.copilot",
],
"settings": {
"files.eol": "\n",

View File

@@ -3,7 +3,6 @@ version: "3.7"
services:
vscode:
build: .
image: godevcontainer
devices:
- /dev/net/tun:/dev/net/tun
volumes:
@@ -11,16 +10,16 @@ services:
# Docker socket to access Docker server
- /var/run/docker.sock:/var/run/docker.sock
# Docker configuration
- ~/.docker:/root/.docker:z
- ~/.docker:/root/.docker
# SSH directory for Linux, OSX and WSL
- ~/.ssh:/root/.ssh:z
- ~/.ssh:/root/.ssh
# For Windows without WSL, a copy will be made
# from /tmp/.ssh to ~/.ssh to fix permissions
#- ~/.ssh:/tmp/.ssh:ro
# Shell history persistence
- ~/.zsh_history:/root/.zsh_history:z
- ~/.zsh_history:/root/.zsh_history
# Git config
- ~/.gitconfig:/root/.gitconfig:z
- ~/.gitconfig:/root/.gitconfig
environment:
- TZ=
cap_add:

View File

@@ -96,7 +96,7 @@ body:
attributes:
label: Share your logs
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
render: log
render: plain text
validations:
required: true
- type: textarea

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

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

View File

@@ -32,24 +32,23 @@ on:
jobs:
verify:
# Only run if it's a push event or if it's a PR from this repository, and it is not dependabot.
if: |
github.actor != 'dependabot[bot]' &&
(github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository))
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v2.4.0
- uses: actions/checkout@v3
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error
- name: Linting
run: docker build --target lint .
- name: Go mod tidy check
run: docker build --target tidy .
- name: Build test image
run: docker build --target test -t test-container .
@@ -60,79 +59,71 @@ jobs:
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container
- name: Code security analysis
uses: snyk/actions/golang@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Build final image
run: docker build -t final-image .
# - name: Image security analysis
# uses: snyk/actions/docker@master
# env:
# SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
# with:
# image: final-image
codeql:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- uses: actions/checkout@v3
- uses: github/codeql-action/init@v2
with:
languages: go
- uses: github/codeql-action/autobuild@v2
- uses: github/codeql-action/analyze@v2
publish:
# Only run if it's a push event or if it's a PR from this repository
if: |
github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
needs: [verify]
github.repository == 'qdm12/gluetun' &&
(
github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
)
needs: [verify, codeql]
permissions:
actions: read
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.4.0
- uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Check for semver tag
id: semvercheck
run: |
if [[ ${{ github.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
MATCH=true
else
MATCH=false
fi
if [[ ! ${{ github.ref }} =~ ^refs/tags/v0\. ]]; then
MATCH=$MATCH_nonzero
fi
echo ::set-output name=match::$MATCH
- uses: actions/checkout@v3
# extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v4
with:
flavor: |
latest=${{ github.ref == 'refs/heads/master' }}
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
images: |
qmcgaw/gluetun
qmcgaw/private-internet-access
tags: |
type=ref,event=branch,enable=${{ github.ref != 'refs/heads/master' }}
type=ref,event=pr
type=ref,event=tag,enable=${{ !startsWith(steps.semvercheck.outputs.match, 'true') }}
type=semver,pattern=v{{major}}.{{minor}}.{{patch}},enable=${{ startsWith(steps.semvercheck.outputs.match, 'true') }}
type=semver,pattern=v{{major}}.{{minor}},enable=${{ startsWith(steps.semvercheck.outputs.match, 'true') }}
type=semver,pattern=v{{major}},enable=${{ startsWith(steps.semvercheck.outputs.match, 'true_nonzero') }}
type=raw,value=latest,enable=${{ !startsWith(steps.semvercheck.outputs.match, 'true') }}
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- uses: docker/login-action@v2
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Short commit
id: shortcommit
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
- name: Build and push final image
uses: docker/build-push-action@v2.7.0
uses: docker/build-push-action@v3.0.0
with:
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
labels: ${{ steps.meta.outputs.labels }}

View File

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

View File

@@ -1,18 +1,22 @@
name: Docker Hub description
on:
push:
branches: [master]
branches:
- master
paths:
- README.md
- .github/workflows/dockerhub-description.yml
jobs:
dockerHubDescription:
docker-hub-description:
if: github.repository == 'qdm12/gluetun'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- name: Checkout
uses: actions/checkout@v2.4.0
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v2
- uses: actions/checkout@v3
- uses: peter-evans/dockerhub-description@v3
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
name: Misspells
on:
pull_request:
branches: [master]
push:
branches: [master]
jobs:
misspell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.4.0
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error

View File

@@ -7,40 +7,47 @@ issues:
- path: _test\.go
linters:
- dupl
- maligned
- goerr113
- path: internal/server/
- containedctx
- path: "internal\\/server\\/.+\\.go"
linters:
- dupl
- path: internal/configuration/
- path: "internal\\/configuration\\/settings\\/.+\\.go"
linters:
- dupl
- path: internal/constants/
linters:
- dupl
- text: "exported: exported var Err*"
linters:
- revive
- text: "mnd: Magic number: 0644*"
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
source: "^.+= os\\.OpenFile\\(.+, .+, 0[0-9]{3}\\)"
linters:
- gomnd
- text: "mnd: Magic number: 0400*"
- 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\\)"
linters:
- ireturn
linters:
enable:
# - cyclop
# - errorlint
# - ireturn
# - varnamelen
# - wrapcheck
- asciicheck
- bidichk
- bodyclose
- containedctx
- decorder
- dogsled
- dupl
- durationcheck
- errchkjson
- errname
- execinquery
- exhaustive
- exportloopref
- forcetypeassert
@@ -59,9 +66,12 @@ linters:
- gomoddirectives
- goprintffuncname
- gosec
- grouper
- ifshort
- importas
- ireturn
- lll
- maintidx
- makezero
- misspell
- nakedret
@@ -70,6 +80,7 @@ linters:
- nilnil
- noctx
- nolintlint
- nosprintfhostport
- prealloc
- predeclared
- predeclared

25
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Update a VPN provider servers data",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "cmd/gluetun/main.go",
"args": [
"update",
"-enduser",
"-providers",
"${input:provider}"
],
}
],
"inputs": [
{
"id": "provider",
"type": "promptString",
"description": "Please enter a provider (or comma separated list of providers)",
}
]
}

View File

@@ -1,8 +1,8 @@
ARG ALPINE_VERSION=3.14
ARG GO_ALPINE_VERSION=3.14
ARG ALPINE_VERSION=3.16
ARG GO_ALPINE_VERSION=3.16
ARG GO_VERSION=1.17
ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.43.0
ARG GOLANGCI_LINT_VERSION=v1.46.2
ARG BUILDPLATFORM=linux/amd64
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
@@ -30,15 +30,6 @@ FROM --platform=${BUILDPLATFORM} base AS lint
COPY .golangci.yml ./
RUN golangci-lint run --timeout=10m
FROM --platform=${BUILDPLATFORM} base AS tidy
RUN git init && \
git config user.email ci@localhost && \
git config user.name ci && \
git add -A && git commit -m ci && \
sed -i '/\/\/ indirect/d' go.mod && \
go mod tidy && \
git diff --exit-code -- go.mod
FROM --platform=${BUILDPLATFORM} base AS build
ARG TARGETPLATFORM
ARG VERSION=unknown
@@ -66,8 +57,12 @@ LABEL \
org.opencontainers.image.source="https://github.com/qdm12/gluetun" \
org.opencontainers.image.title="VPN swiss-knife like client for multiple VPN providers" \
org.opencontainers.image.description="VPN swiss-knife like client to tunnel to multiple VPN servers using OpenVPN, IPtables, DNS over TLS, Shadowsocks, an HTTP proxy and Alpine Linux"
ENV VPNSP=pia \
ENV VPN_SERVICE_PROVIDER=pia \
VPN_TYPE=openvpn \
# Common VPN options
VPN_ENDPOINT_IP= \
VPN_ENDPOINT_PORT= \
VPN_INTERFACE=tun0 \
# OpenVPN
OPENVPN_PROTOCOL=udp \
OPENVPN_USER= \
@@ -77,41 +72,35 @@ ENV VPNSP=pia \
OPENVPN_VERSION=2.5 \
OPENVPN_VERBOSITY=1 \
OPENVPN_FLAGS= \
OPENVPN_CIPHER= \
OPENVPN_CIPHERS= \
OPENVPN_AUTH= \
OPENVPN_ROOT=yes \
OPENVPN_TARGET_IP= \
OPENVPN_PROCESS_USER= \
OPENVPN_IPV6=off \
OPENVPN_CUSTOM_CONFIG= \
OPENVPN_INTERFACE=tun0 \
OPENVPN_PORT= \
# Wireguard
WIREGUARD_PRIVATE_KEY= \
WIREGUARD_PRESHARED_KEY= \
WIREGUARD_PUBLIC_KEY= \
WIREGUARD_ADDRESS= \
WIREGUARD_ENDPOINT_IP= \
WIREGUARD_ENDPOINT_PORT= \
WIREGUARD_INTERFACE=wg0 \
WIREGUARD_ADDRESSES= \
# VPN server filtering
REGION= \
COUNTRY= \
CITY= \
SERVER_HOSTNAME= \
SERVER_REGIONS= \
SERVER_COUNTRIES= \
SERVER_CITIES= \
SERVER_HOSTNAMES= \
# # Mullvad only:
ISP= \
OWNED=no \
OWNED_ONLY=no \
# # Private Internet Access only:
PIA_ENCRYPTION=strong \
PORT_FORWARDING=off \
PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING=off \
PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
# # Cyberghost only:
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
# # Nordvpn only:
SERVER_NUMBER= \
# # PIA and ProtonVPN only:
SERVER_NAME= \
# # PIA only:
SERVER_NAMES= \
# # ProtonVPN only:
FREE_ONLY= \
# # Surfshark only:
@@ -126,7 +115,7 @@ ENV VPNSP=pia \
LOG_LEVEL=info \
# Health
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
HEALTH_ADDRESS_TO_PING=github.com \
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
HEALTH_VPN_DURATION_INITIAL=6s \
HEALTH_VPN_DURATION_ADDITION=5s \
# DNS over TLS
@@ -143,12 +132,12 @@ ENV VPNSP=pia \
BLOCK_ADS=off \
UNBLOCK= \
DNS_UPDATE_PERIOD=24h \
DNS_PLAINTEXT_ADDRESS=1.1.1.1 \
DNS_ADDRESS=127.0.0.1 \
DNS_KEEP_NAMESERVER=off \
# HTTP proxy
HTTPPROXY= \
HTTPPROXY_LOG=off \
HTTPPROXY_PORT=8888 \
HTTPPROXY_LISTENING_ADDRESS=":8888" \
HTTPPROXY_USER= \
HTTPPROXY_PASSWORD= \
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
@@ -156,26 +145,35 @@ ENV VPNSP=pia \
# Shadowsocks
SHADOWSOCKS=off \
SHADOWSOCKS_LOG=off \
SHADOWSOCKS_ADDRESS=":8388" \
SHADOWSOCKS_LISTENING_ADDRESS=":8388" \
SHADOWSOCKS_PASSWORD= \
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
# Control server
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
# Server data updater
UPDATER_PERIOD=0 \
UPDATER_MIN_RATIO=0.8 \
UPDATER_VPN_SERVICE_PROVIDERS= \
# Public IP
PUBLICIP_FILE="/tmp/gluetun/ip" \
PUBLICIP_PERIOD=12h \
# Pprof
PPROF_ENABLED=no \
PPROF_BLOCK_PROFILE_RATE=0 \
PPROF_MUTEX_PROFILE_RATE=0 \
PPROF_HTTP_SERVER_ADDRESS=":6060" \
# Extras
VERSION_INFORMATION=on \
TZ= \
PUID= \
PGID=
ENTRYPOINT ["/entrypoint"]
ENTRYPOINT ["/gluetun-entrypoint"]
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /entrypoint healthcheck
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheck
ARG TARGETPLATFORM
RUN apk add --no-cache --update -l apk-tools && \
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.12/main" openvpn==2.4.11-r0 && \
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.12/main" openvpn==2.4.12-r0 && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.4 && \
apk del openvpn && \
apk add --no-cache --update openvpn ca-certificates iptables ip6tables unbound tzdata && \
@@ -186,4 +184,4 @@ RUN apk add --no-cache --update -l apk-tools && \
deluser openvpn && \
deluser unbound && \
mkdir /gluetun
COPY --from=build /tmp/gobuild/entrypoint /entrypoint
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint

View File

@@ -1,11 +1,6 @@
# Gluetun VPN client
*Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, ExpressVPN, FastestVPN,
HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Perfect Privacy, Privado, Private Internet Access, PrivateVPN,
ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN, WeVPN and Windscribe VPN servers
using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
**ANNOUNCEMENT**: Wireguard is now supported for all providers supporting it!
Lightweight swiss-knife-like VPN client to multiple VPN sercice providers
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
@@ -53,6 +48,7 @@ using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
- **Want to add a VPN provider?** check [Development](https://github.com/qdm12/gluetun/wiki/Development) and [Add a provider](https://github.com/qdm12/gluetun/wiki/Add-a-provider)
- Video:
[![Video Gif](https://i.imgur.com/CetWunc.gif)](https://youtu.be/0F6I03LQcI4)
@@ -61,10 +57,10 @@ using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP
## Features
- Based on Alpine 3.14 for a small Docker image of 33MB
- Based on Alpine 3.16 for a small Docker image of 29MB
- Supports: **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
- Supports OpenVPN for all providers listed
- Supports Wireguard
- Supports Wireguard both kernelspace and userspace
- For **Mullvad**, **Ivpn** and **Windscribe**
- For **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
@@ -98,8 +94,12 @@ version: "3"
services:
gluetun:
image: qmcgaw/gluetun
# container_name: gluetun
# line above must be uncommented to allow external containers to connect. See https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun#external-container-to-gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- 8888:8888/tcp # HTTP proxy
- 8388:8388/tcp # Shadowsocks
@@ -108,14 +108,14 @@ services:
- /yourpath:/gluetun
environment:
# See https://github.com/qdm12/gluetun/wiki
- VPNSP=ivpn
- VPN_SERVICE_PROVIDER=ivpn
- VPN_TYPE=openvpn
# OpenVPN:
- OPENVPN_USER=
- OPENVPN_PASSWORD=
# Wireguard:
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
# - WIREGUARD_ADDRESS=10.64.222.21/32
# - WIREGUARD_ADDRESSES=10.64.222.21/32
# Timezone for accurate log times
- TZ=
```

View File

@@ -7,7 +7,6 @@ import (
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
@@ -17,7 +16,11 @@ import (
"github.com/qdm12/dns/pkg/unbound"
"github.com/qdm12/gluetun/internal/alpine"
"github.com/qdm12/gluetun/internal/cli"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/configuration/sources/env"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
"github.com/qdm12/gluetun/internal/configuration/sources/mux"
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/dns"
"github.com/qdm12/gluetun/internal/firewall"
@@ -26,23 +29,28 @@ import (
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/portforward"
"github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip"
"github.com/qdm12/gluetun/internal/publicip/ipinfo"
"github.com/qdm12/gluetun/internal/routing"
"github.com/qdm12/gluetun/internal/server"
"github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/tun"
"github.com/qdm12/gluetun/internal/updater"
updater "github.com/qdm12/gluetun/internal/updater/loop"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
"github.com/qdm12/gluetun/internal/vpn"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/params"
"github.com/qdm12/goshutdown"
"github.com/qdm12/goshutdown/goroutine"
"github.com/qdm12/goshutdown/group"
"github.com/qdm12/goshutdown/order"
"github.com/qdm12/gosplash"
"github.com/qdm12/log"
"github.com/qdm12/updated/pkg/dnscrypto"
)
@@ -53,11 +61,6 @@ var (
created = "an unknown date"
)
var (
errSetupRouting = errors.New("cannot setup routing")
errCreateUser = errors.New("cannot create user")
)
func main() {
buildInfo := models.BuildInformation{
Version: version,
@@ -66,33 +69,34 @@ func main() {
}
background := context.Background()
signalCtx, stop := signal.NotifyContext(background, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
ctx, cancel := context.WithCancel(background)
logger := logging.New(logging.Settings{
Level: logging.LevelInfo,
})
logger := log.New(log.SetLevel(log.LevelInfo))
args := os.Args
tun := tun.New()
netLinker := netlink.New()
cli := cli.New()
env := params.New()
cmder := command.NewCmder()
envReader := env.New(logger)
filesReader := files.New()
secretsReader := secrets.New()
muxReader := mux.New(envReader, filesReader, secretsReader)
errorCh := make(chan error)
go func() {
errorCh <- _main(ctx, buildInfo, args, logger, env, tun, netLinker, cmder, cli)
errorCh <- _main(ctx, buildInfo, args, logger, muxReader, tun, netLinker, cmder, cli)
}()
select {
case <-signalCtx.Done():
stop()
case signal := <-signalCh:
fmt.Println("")
logger.Warn("Caught OS signal, shutting down")
logger.Warn("Caught OS signal " + signal.String() + ", shutting down")
cancel()
case err := <-errorCh:
stop()
close(errorCh)
if err == nil { // expected exit such as healthcheck
os.Exit(0)
@@ -111,6 +115,8 @@ func main() {
logger.Info("Shutdown successful")
case <-timer.C:
logger.Warn("Shutdown timed out")
case signal := <-signalCh:
logger.Warn("Caught OS signal " + signal.String() + ", forcing shut down")
}
os.Exit(1)
@@ -120,19 +126,19 @@ var (
errCommandUnknown = errors.New("command is unknown")
)
//nolint:gocognit,gocyclo
//nolint:gocognit,gocyclo,maintidx
func _main(ctx context.Context, buildInfo models.BuildInformation,
args []string, logger logging.ParentLogger, env params.Interface,
tun tun.Interface, netLinker netlink.NetLinker, cmder command.RunStarter,
cli cli.CLIer) error {
args []string, logger log.LoggerInterface, source sources.Source,
tun Tun, netLinker netLinker, cmder command.RunStarter,
cli clier) error {
if len(args) > 1 { // cli operation
switch args[1] {
case "healthcheck":
return cli.HealthCheck(ctx, env, logger)
return cli.HealthCheck(ctx, source, logger)
case "clientkey":
return cli.ClientKey(args[2:])
case "openvpnconfig":
return cli.OpenvpnConfig(logger, env)
return cli.OpenvpnConfig(logger, source)
case "update":
return cli.Update(ctx, args[2:], logger)
case "format-servers":
@@ -142,7 +148,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
}
}
announcementExp, err := time.Parse(time.RFC3339, "2021-10-02T00:00:00Z")
announcementExp, err := time.Parse(time.RFC3339, "2021-02-15T00:00:00Z")
if err != nil {
return err
}
@@ -153,7 +159,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
Version: buildInfo.Version,
Commit: buildInfo.Commit,
BuildDate: buildInfo.Created,
Announcement: "Wireguard is now supported for Mullvad, IVPN and Windscribe!",
Announcement: "Large settings parsing refactoring merged on 2022-01-06, please report any issue!",
AnnounceExp: announcementExp,
// Sponsor information
PaypalUser: "qmcgaw",
@@ -163,30 +169,76 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
fmt.Println(line)
}
allSettings, err := source.Read()
if err != nil {
return err
}
// Note: no need to validate minimal settings for the firewall:
// - global log level is parsed from source
// - firewall Debug and Enabled are booleans parsed from source
logger.Patch(log.SetLevel(*allSettings.Log.Level))
routingLogger := logger.New(log.SetComponent("routing"))
if *allSettings.Firewall.Debug { // To remove in v4
routingLogger.Patch(log.SetLevel(log.LevelDebug))
}
routingConf := routing.New(netLinker, routingLogger)
defaultRoutes, err := routingConf.DefaultRoutes()
if err != nil {
return err
}
localNetworks, err := routingConf.LocalNetworks()
if err != nil {
return err
}
firewallLogger := logger.New(log.SetComponent("firewall"))
if *allSettings.Firewall.Debug { // To remove in v4
firewallLogger.Patch(log.SetLevel(log.LevelDebug))
}
firewallConf, err := firewall.NewConfig(ctx, firewallLogger, cmder,
defaultRoutes, localNetworks)
if err != nil {
return err
}
if *allSettings.Firewall.Enabled {
err = firewallConf.SetEnabled(ctx, true)
if err != nil {
return err
}
}
// TODO run this in a loop or in openvpn to reload from file without restarting
storageLogger := logger.NewChild(logging.Settings{Prefix: "storage: "})
storageLogger := logger.New(log.SetComponent("storage"))
storage, err := storage.New(storageLogger, constants.ServersData)
if err != nil {
return err
}
allServers := storage.GetServers()
var allSettings configuration.Settings
err = allSettings.Read(env, allServers,
logger.NewChild(logging.Settings{Prefix: "configuration: "}))
err = allSettings.Validate(storage)
if err != nil {
return err
}
logger.PatchLevel(allSettings.Log.Level)
puid, pgid := allSettings.System.PUID, allSettings.System.PGID
allSettings.Pprof.HTTPServer.Logger = logger.New(log.SetComponent("pprof"))
pprofServer, err := pprof.New(allSettings.Pprof)
if err != nil {
return fmt.Errorf("cannot create Pprof server: %w", err)
}
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
const clientTimeout = 15 * time.Second
httpClient := &http.Client{Timeout: clientTimeout}
// Create configurators
alpineConf := alpine.New()
ovpnConf := openvpn.New(
logger.NewChild(logging.Settings{Prefix: "openvpn configurator: "}),
logger.New(log.SetComponent("openvpn configurator")),
cmder, puid, pgid)
dnsCrypto := dnscrypto.New(httpClient, "", "")
const cacertsPath = "/etc/ssl/certs/ca-certificates.crt"
@@ -218,62 +270,30 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
const defaultUsername = "nonrootuser"
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
if err != nil {
return fmt.Errorf("%w: %s", errCreateUser, err)
return fmt.Errorf("cannot create user: %w", err)
}
if nonRootUsername != defaultUsername {
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.Unbound.Username = nonRootUsername
allSettings.VPN.OpenVPN.ProcUser = nonRootUsername
allSettings.DNS.DoT.Unbound.Username = nonRootUsername
allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername
if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
return err
}
firewallLogLevel := allSettings.Log.Level
if allSettings.Firewall.Debug {
firewallLogLevel = logging.LevelDebug
}
routingLogger := logger.NewChild(logging.Settings{
Prefix: "routing: ",
Level: firewallLogLevel,
})
routingConf := routing.New(netLinker, routingLogger)
defaultInterface, defaultGateway, err := routingConf.DefaultRoute()
if err != nil {
return err
}
localNetworks, err := routingConf.LocalNetworks()
if err != nil {
return err
}
defaultIP, err := routingConf.DefaultIP()
if err != nil {
return err
}
firewallLogger := logger.NewChild(logging.Settings{
Prefix: "firewall: ",
Level: firewallLogLevel,
})
firewallConf := firewall.NewConfig(firewallLogger, cmder,
defaultInterface, defaultGateway, localNetworks, defaultIP)
if err := routingConf.Setup(); err != nil {
if strings.Contains(err.Error(), "operation not permitted") {
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
}
return fmt.Errorf("%w: %s", errSetupRouting, err)
return fmt.Errorf("cannot setup routing: %w", err)
}
defer func() {
logger.Info("routing cleanup...")
routingLogger.Info("routing cleanup...")
if err := routingConf.TearDown(); err != nil {
logger.Error("cannot teardown routing: " + err.Error())
routingLogger.Error("cannot teardown routing: " + err.Error())
}
}()
@@ -284,25 +304,21 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err
}
if err := tun.Check(constants.TunnelDevice); err != nil {
const tunDevice = "/dev/net/tun"
if err := tun.Check(tunDevice); err != nil {
logger.Info(err.Error() + "; creating it...")
err = tun.Create(constants.TunnelDevice)
if err != nil {
return err
}
}
if allSettings.Firewall.Enabled {
err := firewallConf.SetEnabled(ctx, true) // disabled by default
err = tun.Create(tunDevice)
if err != nil {
return err
}
}
for _, port := range allSettings.Firewall.InputPorts {
err = firewallConf.SetAllowedPort(ctx, port, defaultInterface)
if err != nil {
return err
for _, defaultRoute := range defaultRoutes {
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
if err != nil {
return err
}
}
} // TODO move inside firewall?
@@ -323,14 +339,20 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
portForwardLogger := logger.NewChild(logging.Settings{Prefix: "port forwarding: "})
pprofReady := make(chan struct{})
pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server")
go pprofServer.Run(pprofCtx, pprofReady, pprofDone)
otherGroupHandler.Add(pprofHandler)
<-pprofReady
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
httpClient, firewallConf, portForwardLogger)
httpClient, firewallConf, portForwardLogger, puid, pgid)
portForwardHandler, portForwardCtx, portForwardDone := goshutdown.NewGoRoutineHandler(
"port forwarding", goroutine.OptionTimeout(time.Second))
go portForwardLooper.Run(portForwardCtx, portForwardDone)
unboundLogger := logger.NewChild(logging.Settings{Prefix: "dns over tls: "})
unboundLogger := logger.New(log.SetComponent("dns over tls"))
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
unboundLogger)
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
@@ -344,8 +366,9 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
controlGroupHandler.Add(dnsTickerHandler)
publicIPLooper := publicip.NewLoop(httpClient,
logger.NewChild(logging.Settings{Prefix: "ip getter: "}),
ipFetcher := ipinfo.New(httpClient)
publicIPLooper := publicip.NewLoop(ipFetcher,
logger.New(log.SetComponent("ip getter")),
allSettings.PublicIP, puid, pgid)
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
@@ -357,18 +380,25 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone)
tickersGroupHandler.Add(pubIPTickerHandler)
vpnLogger := logger.NewChild(logging.Settings{Prefix: "vpn: "})
updaterLogger := logger.New(log.SetComponent("updater"))
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, updaterLogger,
httpClient, unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
vpnLogger := logger.New(log.SetComponent("vpn"))
vpnLooper := vpn.NewLoop(allSettings.VPN, allSettings.Firewall.VPNInputPorts,
allServers, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient,
buildInfo, allSettings.VersionInformation)
buildInfo, *allSettings.Version.Enabled)
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
"vpn", goroutine.OptionTimeout(time.Second))
go vpnLooper.Run(vpnCtx, vpnDone)
updaterLooper := updater.NewLooper(allSettings.Updater,
allServers, storage, vpnLooper.SetServers, httpClient,
logger.NewChild(logging.Settings{Prefix: "updater: "}))
updaterLooper := updater.NewLoop(allSettings.Updater,
providers, storage, httpClient, updaterLogger)
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
"updater", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
@@ -381,31 +411,36 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
controlGroupHandler.Add(updaterTickerHandler)
httpProxyLooper := httpproxy.NewLoop(
logger.NewChild(logging.Settings{Prefix: "http proxy: "}),
logger.New(log.SetComponent("http proxy")),
allSettings.HTTPProxy)
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
"http proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
otherGroupHandler.Add(httpProxyHandler)
shadowsocksLooper := shadowsocks.NewLooper(allSettings.ShadowSocks,
logger.NewChild(logging.Settings{Prefix: "shadowsocks: "}))
shadowsocksLooper := shadowsocks.NewLoop(allSettings.Shadowsocks,
logger.New(log.SetComponent("shadowsocks")))
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
"shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
otherGroupHandler.Add(shadowsocksHandler)
controlServerAddress := ":" + strconv.Itoa(int(allSettings.ControlServer.Port))
controlServerLogging := allSettings.ControlServer.Log
controlServerAddress := *allSettings.ControlServer.Address
controlServerLogging := *allSettings.ControlServer.Log
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
httpServer := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
logger.NewChild(logging.Settings{Prefix: "http server: "}),
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
logger.New(log.SetComponent("http server")),
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper)
go httpServer.Run(httpServerCtx, httpServerDone)
if err != nil {
return fmt.Errorf("cannot setup control server: %w", err)
}
httpServerReady := make(chan struct{})
go httpServer.Run(httpServerCtx, httpServerReady, httpServerDone)
<-httpServerReady
controlGroupHandler.Add(httpServerHandler)
healthLogger := logger.NewChild(logging.Settings{Prefix: "healthcheck: "})
healthLogger := logger.New(log.SetComponent("healthcheck"))
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
@@ -452,3 +487,38 @@ func printVersions(ctx context.Context, logger infoer,
return nil
}
type netLinker interface {
AddrList(link netlink.Link, family int) (
addresses []netlink.Addr, err error)
AddrAdd(link netlink.Link, addr *netlink.Addr) error
IsWireguardSupported() (ok bool, err error)
RouteList(link netlink.Link, family int) (
routes []netlink.Route, err error)
RouteAdd(route *netlink.Route) error
RouteDel(route *netlink.Route) error
RouteReplace(route *netlink.Route) error
RuleList(family int) (rules []netlink.Rule, err error)
RuleAdd(rule *netlink.Rule) error
RuleDel(rule *netlink.Rule) error
LinkList() (links []netlink.Link, err error)
LinkByName(name string) (link netlink.Link, err error)
LinkByIndex(index int) (link netlink.Link, err error)
LinkAdd(link netlink.Link) (err error)
LinkDel(link netlink.Link) (err error)
LinkSetUp(link netlink.Link) (err error)
LinkSetDown(link netlink.Link) (err error)
}
type clier interface {
ClientKey(args []string) error
FormatServers(args []string) error
OpenvpnConfig(logger cli.OpenvpnConfigLogger, source sources.Source) error
HealthCheck(ctx context.Context, source sources.Source, warner cli.Warner) error
Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error
}
type Tun interface {
Check(tunDevice string) error
Create(tunDevice string) error
}

21
go.mod
View File

@@ -3,19 +3,22 @@ module github.com/qdm12/gluetun
go 1.17
require (
github.com/breml/rootcerts v0.2.0
github.com/breml/rootcerts v0.2.3
github.com/fatih/color v1.13.0
github.com/go-ping/ping v0.0.0-20210911151512-381826476871
github.com/golang/mock v1.6.0
github.com/qdm12/dns v1.11.0
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.1.0
github.com/qdm12/ss-server v0.3.0
github.com/qdm12/gotree v0.2.0
github.com/qdm12/govalid v0.1.0
github.com/qdm12/log v0.1.0
github.com/qdm12/ss-server v0.4.0
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
github.com/stretchr/testify v1.7.0
github.com/vishvananda/netlink v1.1.0
github.com/stretchr/testify v1.7.2
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
golang.org/x/text v0.3.7
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722
@@ -24,7 +27,6 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
@@ -34,11 +36,10 @@ require (
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

43
go.sum
View File

@@ -4,8 +4,8 @@ github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/g
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/breml/rootcerts v0.2.0 h1:bBIgVe8bS0Ec+orgWpZ/GRYt3a0O8yoW+g2kSBY2aLE=
github.com/breml/rootcerts v0.2.0/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88=
github.com/breml/rootcerts v0.2.3 h1:1vkYjKOiHVSyuz9Ue4AOrViEvUm8gk8phTg0vbcuU0A=
github.com/breml/rootcerts v0.2.3/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88=
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=
@@ -32,8 +32,6 @@ github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsd
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
github.com/go-ping/ping v0.0.0-20210911151512-381826476871 h1:wtjTfjwAR/BYYMJ+QOLI/3J/qGEI0fgrkZvgsEWK2/Q=
github.com/go-ping/ping v0.0.0-20210911151512-381826476871/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
@@ -47,8 +45,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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=
@@ -115,8 +111,14 @@ github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSE
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.1.0/go.mod h1:+A3fWW4/rUeDXhY3ieBzwghKdnIPFJgD8K3qQkenJlw=
github.com/qdm12/ss-server v0.3.0 h1:BfKv4OU6dYb2KcDMYpTc7LIuO2jB73g3JCzy988GrLI=
github.com/qdm12/ss-server v0.3.0/go.mod h1:ug+nWfuzKw/h5fxL1B6e9/OhkVuWJX4i2V1Pf0pJU1o=
github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c=
github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4=
github.com/qdm12/govalid v0.1.0 h1:UIFVmuaAg0Q+h0GeyfcFEZ5sQ5KJPvRQwycC1/cqDN8=
github.com/qdm12/govalid v0.1.0/go.mod h1:CyS/OEQdOvunBgrtIsW93fjd4jBkwZPBjGSpxq3NwA4=
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/ss-server v0.4.0 h1:lMMYfDGc9P86Lyvd3+p8lK4hhgHUKDzjZC91FqJYkDU=
github.com/qdm12/ss-server v0.4.0/go.mod h1:AY0p4huvPUPW+/CiWsJcDgT6sneDryk26VXSccPNCxY=
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=
@@ -128,12 +130,14 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 h1:b/k/BVWzWRS5v6AB0gf2ckFSbFsHN5jR0HoNso1pN+w=
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -141,8 +145,9 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/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-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -172,7 +177,6 @@ golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -185,7 +189,6 @@ golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -193,7 +196,9 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -205,7 +210,6 @@ golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -220,6 +224,8 @@ 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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
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=
@@ -245,8 +251,9 @@ gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQb
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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722 h1:Qws2rZnQudC58cIagVucPQDLmMi3kAXgxscsgD0v6DU=
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=

View File

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

View File

@@ -12,10 +12,6 @@ var (
ErrUserAlreadyExists = errors.New("user already exists")
)
type UserCreater interface {
CreateUser(username string, uid int) (createdUsername string, err error)
}
// CreateUser creates a user in Alpine with the given UID.
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
UIDStr := strconv.Itoa(uid)

View File

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

View File

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

View File

@@ -7,16 +7,12 @@ import (
"os"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
)
type ClientKeyFormatter interface {
ClientKey(args []string) error
}
func (c *CLI) ClientKey(args []string) error {
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
filepath := flagSet.String("path", constants.ClientKey, "file path to the client.key file")
filepath := flagSet.String("path", files.OpenVPNClientKeyPath, "file path to the client.key file")
if err := flagSet.Parse(args); err != nil {
return err
}

View File

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

View File

@@ -6,23 +6,22 @@ import (
"net/http"
"time"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/healthcheck"
"github.com/qdm12/golibs/params"
)
type HealthChecker interface {
HealthCheck(ctx context.Context, env params.Interface, warner configuration.Warner) error
}
func (c *CLI) HealthCheck(ctx context.Context, env params.Interface,
warner configuration.Warner) error {
func (c *CLI) HealthCheck(ctx context.Context, source sources.Source, warner Warner) error {
// Extract the health server port from the configuration.
config := configuration.Health{}
err := config.Read(env, warner)
config, err := source.ReadHealth()
if err != nil {
return err
}
err = config.Validate()
if err != nil {
return err
}
_, port, err := net.SplitHostPort(config.ServerAddress)
if err != nil {
return err

View File

@@ -1,47 +1,73 @@
package cli
import (
"context"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip/ipinfo"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/golibs/params"
"github.com/qdm12/gluetun/internal/updater/resolver"
)
type OpenvpnConfigMaker interface {
OpenvpnConfig(logger OpenvpnConfigLogger, env params.Interface) error
}
type OpenvpnConfigLogger interface {
Info(s string)
Warn(s string)
}
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, env params.Interface) error {
type Unzipper interface {
FetchAndExtract(ctx context.Context, url string) (
contents map[string][]byte, err error)
}
type ParallelResolver interface {
Resolve(ctx context.Context, settings resolver.ParallelSettings) (
hostToIPs map[string][]net.IP, warnings []string, err error)
}
type IPFetcher interface {
FetchMultiInfo(ctx context.Context, ips []net.IP) (data []ipinfo.Response, err error)
}
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error {
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
return err
}
allServers := storage.GetServers()
var allSettings configuration.Settings
err = allSettings.Read(env, allServers, logger)
allSettings, err := source.Read()
if err != nil {
return err
}
providerConf := provider.New(allSettings.VPN.Provider.Name, allServers, time.Now)
if err = allSettings.Validate(storage); err != nil {
return err
}
// Unused by this CLI command
unzipper := (Unzipper)(nil)
client := (*http.Client)(nil)
warner := (Warner)(nil)
parallelResolver := (ParallelResolver)(nil)
ipFetcher := (IPFetcher)(nil)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, warner, client,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
providerConf := providers.Get(*allSettings.VPN.Provider.Name)
connection, err := providerConf.GetConnection(allSettings.VPN.Provider.ServerSelection)
if err != nil {
return err
}
lines, err := providerConf.BuildConf(connection, allSettings.VPN.OpenVPN)
if err != nil {
return err
}
lines := providerConf.OpenVPNConfig(connection, allSettings.VPN.OpenVPN)
fmt.Println(strings.Join(lines, "\n"))
return nil

View File

@@ -2,32 +2,30 @@ package cli
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip/ipinfo"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
)
var (
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
ErrNewStorage = errors.New("cannot create storage")
ErrUpdateServerInformation = errors.New("cannot update server information")
ErrWriteToFile = errors.New("cannot write updated information to file")
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
ErrNoProviderSpecified = errors.New("no provider was specified")
)
type Updater interface {
Update(ctx context.Context, args []string, logger UpdaterLogger) error
}
type UpdaterLogger interface {
Info(s string)
Warn(s string)
@@ -35,87 +33,70 @@ type UpdaterLogger interface {
}
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
options := configuration.Updater{CLI: true}
options := settings.Updater{}
var endUserMode, maintainerMode, updateAll bool
var csvProviders string
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
flagSet.BoolVar(&maintainerMode, "maintainer", false,
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use")
const defaultMinRatio = 0.8
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
"Minimum ratio of servers to find for the update to succeed")
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
flagSet.BoolVar(&options.Cyberghost, "cyberghost", false, "Update Cyberghost servers")
flagSet.BoolVar(&options.Expressvpn, "expressvpn", false, "Update ExpressVPN servers")
flagSet.BoolVar(&options.Fastestvpn, "fastestvpn", false, "Update FastestVPN servers")
flagSet.BoolVar(&options.HideMyAss, "hidemyass", false, "Update HideMyAss servers")
flagSet.BoolVar(&options.Ipvanish, "ipvanish", false, "Update IpVanish servers")
flagSet.BoolVar(&options.Ivpn, "ivpn", false, "Update IVPN servers")
flagSet.BoolVar(&options.Mullvad, "mullvad", false, "Update Mullvad servers")
flagSet.BoolVar(&options.Nordvpn, "nordvpn", false, "Update Nordvpn servers")
flagSet.BoolVar(&options.Perfectprivacy, "perfectprivacy", false, "Update Perfect Privacy servers")
flagSet.BoolVar(&options.PIA, "pia", false, "Update Private Internet Access post-summer 2020 servers")
flagSet.BoolVar(&options.Privado, "privado", false, "Update Privado servers")
flagSet.BoolVar(&options.Privatevpn, "privatevpn", false, "Update Private VPN servers")
flagSet.BoolVar(&options.Protonvpn, "protonvpn", false, "Update Protonvpn servers")
flagSet.BoolVar(&options.Purevpn, "purevpn", false, "Update Purevpn servers")
flagSet.BoolVar(&options.Surfshark, "surfshark", false, "Update Surfshark servers")
flagSet.BoolVar(&options.Torguard, "torguard", false, "Update Torguard servers")
flagSet.BoolVar(&options.VPNUnlimited, "vpnunlimited", false, "Update VPN Unlimited servers")
flagSet.BoolVar(&options.Vyprvpn, "vyprvpn", false, "Update Vyprvpn servers")
flagSet.BoolVar(&options.Wevpn, "wevpn", false, "Update WeVPN servers")
flagSet.BoolVar(&options.Windscribe, "windscribe", false, "Update Windscribe servers")
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
if err := flagSet.Parse(args); err != nil {
return err
}
if !endUserMode && !maintainerMode {
return ErrModeUnspecified
}
if updateAll {
options.EnableAll()
options.Providers = providers.All()
} else {
if csvProviders == "" {
return ErrNoProviderSpecified
}
options.Providers = strings.Split(csvProviders, ",")
}
options.SetDefaults(options.Providers[0])
err := options.Validate()
if err != nil {
return fmt.Errorf("options validation failed: %w", err)
}
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
return fmt.Errorf("cannot create servers storage: %w", err)
}
const clientTimeout = 10 * time.Second
httpClient := &http.Client{Timeout: clientTimeout}
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
ipFetcher := ipinfo.New(httpClient)
openvpnFileExtractor := extract.New()
storage, err := storage.New(logger, constants.ServersData)
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
updater := updater.New(httpClient, storage, providers, logger)
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
if err != nil {
return fmt.Errorf("%w: %s", ErrNewStorage, err)
}
currentServers := storage.GetServers()
updater := updater.New(options, httpClient, currentServers, logger)
allServers, err := updater.UpdateServers(ctx)
if err != nil {
return fmt.Errorf("%w: %s", ErrUpdateServerInformation, err)
}
if endUserMode {
if err := storage.FlushToFile(allServers); err != nil {
return fmt.Errorf("%w: %s", ErrWriteToFile, err)
}
return fmt.Errorf("cannot update server information: %w", err)
}
if maintainerMode {
if err := writeToEmbeddedJSON(c.repoServersPath, allServers); err != nil {
return fmt.Errorf("%w: %s", ErrWriteToFile, err)
err := storage.FlushToFile(c.repoServersPath)
if err != nil {
return fmt.Errorf("cannot write servers data to embedded JSON file: %w", err)
}
}
return nil
}
func writeToEmbeddedJSON(repoServersPath string,
allServers models.AllServers) error {
const perms = 0600
f, err := os.OpenFile(repoServersPath,
os.O_TRUNC|os.O_WRONLY|os.O_CREATE, perms)
if err != nil {
return err
}
defer f.Close()
encoder := json.NewEncoder(f)
encoder.SetIndent("", " ")
return encoder.Encode(allServers)
}

5
internal/cli/warner.go Normal file
View File

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

View File

@@ -1,3 +0,0 @@
// Package configuration reads initial settings from environment variables
// and secret files.
package configuration

View File

@@ -1,6 +0,0 @@
package configuration
const (
lastIndent = "|--"
indent = " "
)

View File

@@ -1,95 +0,0 @@
package configuration
import (
"errors"
"fmt"
"net"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
var (
errCustomNotSupported = errors.New("custom provider is not supported")
errCustomExtractFromFile = errors.New("cannot extract configuration from file")
)
func (settings *Provider) readCustom(r reader, vpnType string) (err error) {
settings.Name = constants.Custom
switch vpnType {
case constants.OpenVPN:
return settings.ServerSelection.OpenVPN.readCustom(r)
case constants.Wireguard:
return settings.ServerSelection.Wireguard.readCustom(r)
default:
return fmt.Errorf("%w: for VPN type %s", errCustomNotSupported, vpnType)
}
}
func (settings *OpenVPNSelection) readCustom(r reader) (err error) {
configFile, err := r.env.Get("OPENVPN_CUSTOM_CONFIG", params.CaseSensitiveValue(), params.Compulsory())
if err != nil {
return fmt.Errorf("environment variable OPENVPN_CUSTOM_CONFIG: %w", err)
}
settings.ConfFile = configFile
// For display and consistency purposes only,
// these values are not actually used since the file is re-read
// before each OpenVPN start.
_, connection, err := r.ovpnExt.Data(configFile)
if err != nil {
return fmt.Errorf("%w: %s", errCustomExtractFromFile, err)
}
settings.TCP = connection.Protocol == constants.TCP
return nil
}
func (settings *OpenVPN) readCustom(r reader) (err error) {
settings.ConfFile, err = r.env.Path("OPENVPN_CUSTOM_CONFIG",
params.Compulsory(), params.CaseSensitiveValue())
if err != nil {
return fmt.Errorf("environment variable OPENVPN_CUSTOM_CONFIG: %w", err)
}
return nil
}
func (settings *WireguardSelection) readCustom(r reader) (err error) {
settings.PublicKey, err = r.env.Get("WIREGUARD_PUBLIC_KEY",
params.CaseSensitiveValue(), params.Compulsory())
if err != nil {
return fmt.Errorf("environment variable WIREGUARD_PUBLIC_KEY: %w", err)
}
settings.EndpointIP, err = readWireguardEndpointIP(r.env)
if err != nil {
return err
}
settings.EndpointPort, err = r.env.Port("WIREGUARD_ENDPOINT_PORT", params.Compulsory(),
params.RetroKeys([]string{"WIREGUARD_PORT"}, r.onRetroActive))
if err != nil {
return fmt.Errorf("environment variable WIREGUARD_ENDPOINT_PORT: %w", err)
}
return nil
}
// readWireguardEndpointIP reads and parses the server endpoint IP
// address from the environment variable WIREGUARD_ENDPOINT_IP.
func readWireguardEndpointIP(env params.Interface) (endpointIP net.IP, err error) {
s, err := env.Get("WIREGUARD_ENDPOINT_IP", params.Compulsory())
if err != nil {
return nil, fmt.Errorf("environment variable WIREGUARD_ENDPOINT_IP: %w", err)
}
endpointIP = net.ParseIP(s)
if endpointIP == nil {
return nil, fmt.Errorf("environment variable WIREGUARD_ENDPOINT_IP: %w: %s",
ErrInvalidIP, s)
}
return endpointIP, nil
}

View File

@@ -1,47 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
func (settings *Provider) readCyberghost(r reader) (err error) {
settings.Name = constants.Cyberghost
servers := r.servers.GetCyberghost()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY",
constants.CyberghostCountryChoices(servers),
params.RetroKeys([]string{"REGION"}, r.onRetroActive))
if err != nil {
return fmt.Errorf("environment variable COUNTRY: %w", err)
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME",
constants.CyberghostHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
return settings.ServerSelection.OpenVPN.readProtocolAndPort(r)
}
func (settings *OpenVPN) readCyberghost(r reader) (err error) {
settings.ClientKey, err = readClientKey(r)
if err != nil {
return fmt.Errorf("%w: %s", errClientKey, err)
}
settings.ClientCrt, err = readClientCertificate(r)
if err != nil {
return fmt.Errorf("%w: %s", errClientCert, err)
}
return nil
}

View File

@@ -1,117 +0,0 @@
package configuration
import (
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/qdm12/dns/pkg/blacklist"
"github.com/qdm12/dns/pkg/unbound"
"github.com/qdm12/golibs/params"
)
// DNS contains settings to configure Unbound for DNS over TLS operation.
type DNS struct { //nolint:maligned
Enabled bool
PlaintextAddress net.IP
KeepNameserver bool
UpdatePeriod time.Duration
Unbound unbound.Settings
BlacklistBuild blacklist.BuilderSettings
}
func (settings *DNS) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *DNS) lines() (lines []string) {
lines = append(lines, lastIndent+"DNS:")
if settings.PlaintextAddress != nil {
lines = append(lines, indent+lastIndent+"Plaintext address: "+settings.PlaintextAddress.String())
}
if settings.KeepNameserver {
lines = append(lines, indent+lastIndent+"Keep nameserver (disabled blocking): yes")
}
if !settings.Enabled {
return lines
}
lines = append(lines, indent+lastIndent+"DNS over TLS:")
lines = append(lines, indent+indent+lastIndent+"Unbound:")
for _, line := range settings.Unbound.Lines() {
lines = append(lines, indent+indent+indent+line)
}
lines = append(lines, indent+indent+lastIndent+"Blacklist:")
for _, line := range settings.BlacklistBuild.Lines(indent, lastIndent) {
lines = append(lines, indent+indent+indent+line)
}
if settings.UpdatePeriod > 0 {
lines = append(lines, indent+indent+lastIndent+"Update: every "+settings.UpdatePeriod.String())
}
return lines
}
var (
ErrUnboundSettings = errors.New("failed getting Unbound settings")
ErrBlacklistSettings = errors.New("failed getting DNS blacklist settings")
)
func (settings *DNS) read(r reader) (err error) {
settings.Enabled, err = r.env.OnOff("DOT", params.Default("on"))
if err != nil {
return fmt.Errorf("environment variable DOT: %w", err)
}
// Plain DNS settings
if err := settings.readDNSPlaintext(r.env); err != nil {
return err
}
settings.KeepNameserver, err = r.env.OnOff("DNS_KEEP_NAMESERVER", params.Default("off"))
if err != nil {
return fmt.Errorf("environment variable DNS_KEEP_NAMESERVER: %w", err)
}
// DNS over TLS external settings
if err := settings.readBlacklistBuilding(r); err != nil {
return fmt.Errorf("%w: %s", ErrBlacklistSettings, err)
}
settings.UpdatePeriod, err = r.env.Duration("DNS_UPDATE_PERIOD", params.Default("24h"))
if err != nil {
return fmt.Errorf("environment variable DNS_UPDATE_PERIOD: %w", err)
}
// Unbound settings
if err := settings.readUnbound(r); err != nil {
return fmt.Errorf("%w: %s", ErrUnboundSettings, err)
}
return nil
}
var (
ErrDNSAddressNotAnIP = errors.New("DNS plaintext address is not an IP address")
)
func (settings *DNS) readDNSPlaintext(env params.Interface) error {
s, err := env.Get("DNS_PLAINTEXT_ADDRESS", params.Default("1.1.1.1"))
if err != nil {
return fmt.Errorf("environment variable DNS_PLAINTEXT_ADDRESS: %w", err)
}
settings.PlaintextAddress = net.ParseIP(s)
if settings.PlaintextAddress == nil {
return fmt.Errorf("%w: %s", ErrDNSAddressNotAnIP, s)
}
return nil
}

View File

@@ -1,76 +0,0 @@
package configuration
import (
"net"
"testing"
"time"
"github.com/qdm12/dns/pkg/blacklist"
"github.com/qdm12/dns/pkg/provider"
"github.com/qdm12/dns/pkg/unbound"
"github.com/stretchr/testify/assert"
)
func Test_DNS_Lines(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
settings DNS
lines []string
}{
"disabled DOT": {
settings: DNS{
PlaintextAddress: net.IP{1, 1, 1, 1},
},
lines: []string{
"|--DNS:",
" |--Plaintext address: 1.1.1.1",
},
},
"enabled DOT": {
settings: DNS{
Enabled: true,
KeepNameserver: true,
Unbound: unbound.Settings{
Providers: []provider.Provider{
provider.Cloudflare(),
},
},
BlacklistBuild: blacklist.BuilderSettings{
BlockMalicious: true,
BlockAds: true,
BlockSurveillance: true,
},
UpdatePeriod: time.Hour,
},
lines: []string{
"|--DNS:",
" |--Keep nameserver (disabled blocking): yes",
" |--DNS over TLS:",
" |--Unbound:",
" |--DNS over TLS providers:",
" |--Cloudflare",
" |--Listening port: 0",
" |--Access control:",
" |--Allowed:",
" |--Caching: disabled",
" |--IPv4 resolution: disabled",
" |--IPv6 resolution: disabled",
" |--Verbosity level: 0/5",
" |--Verbosity details level: 0/4",
" |--Validation log level: 0/2",
" |--Username: ",
" |--Blacklist:",
" |--Blocked categories: malicious, surveillance, ads",
" |--Update: every 1h0m0s",
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
lines := testCase.settings.lines()
assert.Equal(t, testCase.lines, lines)
})
}
}

View File

@@ -1,87 +0,0 @@
package configuration
import (
"errors"
"fmt"
"github.com/qdm12/golibs/params"
"inet.af/netaddr"
)
func (settings *DNS) readBlacklistBuilding(r reader) (err error) {
settings.BlacklistBuild.BlockMalicious, err = r.env.OnOff("BLOCK_MALICIOUS", params.Default("on"))
if err != nil {
return fmt.Errorf("environment variable BLOCK_MALICIOUS: %w", err)
}
settings.BlacklistBuild.BlockSurveillance, err = r.env.OnOff("BLOCK_SURVEILLANCE", params.Default("on"),
params.RetroKeys([]string{"BLOCK_NSA"}, r.onRetroActive))
if err != nil {
return fmt.Errorf("environment variable BLOCK_SURVEILLANCE (or BLOCK_NSA): %w", err)
}
settings.BlacklistBuild.BlockAds, err = r.env.OnOff("BLOCK_ADS", params.Default("off"))
if err != nil {
return fmt.Errorf("environment variable BLOCK_ADS: %w", err)
}
if err := settings.readPrivateAddresses(r.env); err != nil {
return err
}
return settings.readBlacklistUnblockedHostnames(r)
}
var (
ErrInvalidPrivateAddress = errors.New("private address is not a valid IP or CIDR range")
)
func (settings *DNS) readPrivateAddresses(env params.Interface) (err error) {
privateAddresses, err := env.CSV("DOT_PRIVATE_ADDRESS")
if err != nil {
return fmt.Errorf("environment variable DOT_PRIVATE_ADDRESS: %w", err)
} else if len(privateAddresses) == 0 {
return nil
}
ips := make([]netaddr.IP, 0, len(privateAddresses))
ipPrefixes := make([]netaddr.IPPrefix, 0, len(privateAddresses))
for _, address := range privateAddresses {
ip, err := netaddr.ParseIP(address)
if err == nil {
ips = append(ips, ip)
continue
}
ipPrefix, err := netaddr.ParseIPPrefix(address)
if err == nil {
ipPrefixes = append(ipPrefixes, ipPrefix)
continue
}
return fmt.Errorf("%w: %s", ErrInvalidPrivateAddress, address)
}
settings.BlacklistBuild.AddBlockedIPs = append(settings.BlacklistBuild.AddBlockedIPs, ips...)
settings.BlacklistBuild.AddBlockedIPPrefixes = append(settings.BlacklistBuild.AddBlockedIPPrefixes, ipPrefixes...)
return nil
}
func (settings *DNS) readBlacklistUnblockedHostnames(r reader) (err error) {
hostnames, err := r.env.CSV("UNBLOCK")
if err != nil {
return fmt.Errorf("environment variable UNBLOCK: %w", err)
} else if len(hostnames) == 0 {
return nil
}
for _, hostname := range hostnames {
if !r.regex.MatchHostname(hostname) {
return fmt.Errorf("%w: %s", ErrInvalidHostname, hostname)
}
}
settings.BlacklistBuild.AllowedHosts = append(settings.BlacklistBuild.AllowedHosts, hostnames...)
return nil
}

View File

@@ -1,40 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) readExpressvpn(r reader) (err error) {
settings.Name = constants.Expressvpn
servers := r.servers.GetExpressvpn()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME",
constants.ExpressvpnHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.ExpressvpnCountriesChoices(servers))
if err != nil {
return fmt.Errorf("environment variable COUNTRY: %w", err)
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.ExpressvpnCityChoices(servers))
if err != nil {
return fmt.Errorf("environment variable CITY: %w", err)
}
settings.ServerSelection.OpenVPN.TCP, err = readOpenVPNProtocol(r)
if err != nil {
return err
}
return nil
}

View File

@@ -1,30 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) readFastestvpn(r reader) (err error) {
settings.Name = constants.Fastestvpn
servers := r.servers.GetFastestvpn()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME",
constants.FastestvpnHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.FastestvpnCountriesChoices(servers))
if err != nil {
return fmt.Errorf("environment variable COUNTRY: %w", err)
}
return settings.ServerSelection.OpenVPN.readProtocolOnly(r)
}

View File

@@ -1,99 +0,0 @@
package configuration
import (
"fmt"
"net"
"strings"
"github.com/qdm12/golibs/params"
)
// Firewall contains settings to customize the firewall operation.
type Firewall struct {
VPNInputPorts []uint16
InputPorts []uint16
OutboundSubnets []net.IPNet
Enabled bool
Debug bool
}
func (settings *Firewall) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *Firewall) lines() (lines []string) {
if !settings.Enabled {
lines = append(lines, lastIndent+"Firewall: disabled ⚠️")
return lines
}
lines = append(lines, lastIndent+"Firewall:")
if settings.Debug {
lines = append(lines, indent+lastIndent+"Debug: on")
}
if len(settings.VPNInputPorts) > 0 {
lines = append(lines, indent+lastIndent+"VPN input ports: "+
strings.Join(uint16sToStrings(settings.VPNInputPorts), ", "))
}
if len(settings.InputPorts) > 0 {
lines = append(lines, indent+lastIndent+"Input ports: "+
strings.Join(uint16sToStrings(settings.InputPorts), ", "))
}
if len(settings.OutboundSubnets) > 0 {
lines = append(lines, indent+lastIndent+"Outbound subnets: "+
strings.Join(ipNetsToStrings(settings.OutboundSubnets), ", "))
}
return lines
}
func (settings *Firewall) read(r reader) (err error) {
settings.Enabled, err = r.env.OnOff("FIREWALL", params.Default("on"))
if err != nil {
return fmt.Errorf("environment variable FIREWALL: %w", err)
}
settings.Debug, err = r.env.OnOff("FIREWALL_DEBUG", params.Default("off"))
if err != nil {
return fmt.Errorf("environment variable FIREWALL_DEBUG: %w", err)
}
if err := settings.readVPNInputPorts(r.env); err != nil {
return err
}
if err := settings.readInputPorts(r.env); err != nil {
return err
}
return settings.readOutboundSubnets(r)
}
func (settings *Firewall) readVPNInputPorts(env params.Interface) (err error) {
settings.VPNInputPorts, err = readCSVPorts(env, "FIREWALL_VPN_INPUT_PORTS")
if err != nil {
return fmt.Errorf("environment variable FIREWALL_VPN_INPUT_PORTS: %w", err)
}
return nil
}
func (settings *Firewall) readInputPorts(env params.Interface) (err error) {
settings.InputPorts, err = readCSVPorts(env, "FIREWALL_INPUT_PORTS")
if err != nil {
return fmt.Errorf("environment variable FIREWALL_INPUT_PORTS: %w", err)
}
return nil
}
func (settings *Firewall) readOutboundSubnets(r reader) (err error) {
retroOption := params.RetroKeys([]string{"EXTRA_SUBNETS"}, r.onRetroActive)
settings.OutboundSubnets, err = readCSVIPNets(r.env, "FIREWALL_OUTBOUND_SUBNETS", retroOption)
if err != nil {
return fmt.Errorf("environment variable FIREWALL_OUTBOUND_SUBNETS: %w", err)
}
return nil
}

View File

@@ -1,72 +0,0 @@
package configuration
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/params"
)
// Health contains settings for the healthcheck and health server.
type Health struct {
ServerAddress string
AddressToPing string
VPN HealthyWait
}
func (settings *Health) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *Health) lines() (lines []string) {
lines = append(lines, lastIndent+"Health:")
lines = append(lines, indent+lastIndent+"Server address: "+settings.ServerAddress)
lines = append(lines, indent+lastIndent+"Address to ping: "+settings.AddressToPing)
lines = append(lines, indent+lastIndent+"VPN:")
for _, line := range settings.VPN.lines() {
lines = append(lines, indent+indent+line)
}
return lines
}
// Read is to be used for the healthcheck query mode.
func (settings *Health) Read(env params.Interface, warner Warner) (err error) {
reader := newReader(env, models.AllServers{}, warner) // note: no need for servers data
return settings.read(reader)
}
func (settings *Health) read(r reader) (err error) {
var warning string
settings.ServerAddress, warning, err = r.env.ListeningAddress(
"HEALTH_SERVER_ADDRESS", params.Default("127.0.0.1:9999"))
if warning != "" {
r.warner.Warn("environment variable HEALTH_SERVER_ADDRESS: " + warning)
}
if err != nil {
return fmt.Errorf("environment variable HEALTH_SERVER_ADDRESS: %w", err)
}
settings.AddressToPing, err = r.env.Get("HEALTH_ADDRESS_TO_PING", params.Default("github.com"))
if err != nil {
return fmt.Errorf("environment variable HEALTH_ADDRESS_TO_PING: %w", err)
}
retroKeyOption := params.RetroKeys([]string{"HEALTH_OPENVPN_DURATION_INITIAL"}, r.onRetroActive)
settings.VPN.Initial, err = r.env.Duration("HEALTH_VPN_DURATION_INITIAL", params.Default("6s"), retroKeyOption)
if err != nil {
return fmt.Errorf("environment variable HEALTH_VPN_DURATION_INITIAL: %w", err)
}
retroKeyOption = params.RetroKeys([]string{"HEALTH_OPENVPN_DURATION_ADDITION"}, r.onRetroActive)
settings.VPN.Addition, err = r.env.Duration("HEALTH_VPN_DURATION_ADDITION", params.Default("5s"), retroKeyOption)
if err != nil {
return fmt.Errorf("environment variable HEALTH_VPN_DURATION_ADDITION: %w", err)
}
return nil
}

View File

@@ -1,272 +0,0 @@
package configuration
import (
"errors"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/qdm12/golibs/params/mock_params"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Health_String(t *testing.T) {
t.Parallel()
health := Health{
ServerAddress: "a",
AddressToPing: "b",
}
const expected = `|--Health:
|--Server address: a
|--Address to ping: b
|--VPN:
|--Initial duration: 0s`
s := health.String()
assert.Equal(t, expected, s)
}
func Test_Health_lines(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
settings Health
lines []string
}{
"empty": {
lines: []string{
"|--Health:",
" |--Server address: ",
" |--Address to ping: ",
" |--VPN:",
" |--Initial duration: 0s",
},
},
"filled settings": {
settings: Health{
ServerAddress: "address:9999",
AddressToPing: "github.com",
VPN: HealthyWait{
Initial: time.Second,
Addition: time.Minute,
},
},
lines: []string{
"|--Health:",
" |--Server address: address:9999",
" |--Address to ping: github.com",
" |--VPN:",
" |--Initial duration: 1s",
" |--Addition duration: 1m0s",
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
lines := testCase.settings.lines()
assert.Equal(t, testCase.lines, lines)
})
}
}
func Test_Health_read(t *testing.T) {
t.Parallel()
errDummy := errors.New("dummy")
type stringCall struct {
call bool
s string
err error
}
type stringCallWithWarning struct {
call bool
s string
warning string
err error
}
type durationCall struct {
call bool
duration time.Duration
err error
}
testCases := map[string]struct {
serverAddress stringCallWithWarning
addressToPing stringCall
vpnInitial durationCall
vpnAddition durationCall
expected Health
err error
}{
"success": {
serverAddress: stringCallWithWarning{
call: true,
s: "127.0.0.1:9999",
},
addressToPing: stringCall{
call: true,
s: "1.2.3.4",
},
vpnInitial: durationCall{
call: true,
duration: time.Second,
},
vpnAddition: durationCall{
call: true,
duration: time.Minute,
},
expected: Health{
ServerAddress: "127.0.0.1:9999",
AddressToPing: "1.2.3.4",
VPN: HealthyWait{
Initial: time.Second,
Addition: time.Minute,
},
},
},
"listening address error": {
serverAddress: stringCallWithWarning{
call: true,
s: "127.0.0.1:9999",
warning: "warning",
err: errDummy,
},
expected: Health{
ServerAddress: "127.0.0.1:9999",
},
err: errors.New("environment variable HEALTH_SERVER_ADDRESS: dummy"),
},
"address to ping error": {
serverAddress: stringCallWithWarning{
call: true,
},
addressToPing: stringCall{
call: true,
s: "address",
err: errDummy,
},
expected: Health{
AddressToPing: "address",
},
err: errors.New("environment variable HEALTH_ADDRESS_TO_PING: dummy"),
},
"initial error": {
serverAddress: stringCallWithWarning{
call: true,
},
addressToPing: stringCall{
call: true,
},
vpnInitial: durationCall{
call: true,
duration: time.Second,
err: errDummy,
},
expected: Health{
VPN: HealthyWait{
Initial: time.Second,
},
},
err: errors.New("environment variable HEALTH_VPN_DURATION_INITIAL: dummy"),
},
"addition error": {
serverAddress: stringCallWithWarning{
call: true,
},
addressToPing: stringCall{
call: true,
},
vpnInitial: durationCall{
call: true,
duration: time.Second,
},
vpnAddition: durationCall{
call: true,
duration: time.Minute,
err: errDummy,
},
expected: Health{
VPN: HealthyWait{
Initial: time.Second,
Addition: time.Minute,
},
},
err: errors.New("environment variable HEALTH_VPN_DURATION_ADDITION: dummy"),
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
env := mock_params.NewMockInterface(ctrl)
warner := NewMockWarner(ctrl)
if testCase.serverAddress.call {
value := testCase.serverAddress.s
warning := testCase.serverAddress.warning
err := testCase.serverAddress.err
env.EXPECT().ListeningAddress("HEALTH_SERVER_ADDRESS", gomock.Any()).
Return(value, warning, err)
if warning != "" {
warner.EXPECT().Warn("environment variable HEALTH_SERVER_ADDRESS: " + warning)
}
}
if testCase.addressToPing.call {
value := testCase.addressToPing.s
err := testCase.addressToPing.err
env.EXPECT().Get("HEALTH_ADDRESS_TO_PING", gomock.Any()).
Return(value, err)
}
if testCase.vpnInitial.call {
value := testCase.vpnInitial.duration
err := testCase.vpnInitial.err
env.EXPECT().
Duration("HEALTH_VPN_DURATION_INITIAL", gomock.Any()).
Return(value, err)
}
if testCase.vpnAddition.call {
value := testCase.vpnAddition.duration
err := testCase.vpnAddition.err
env.EXPECT().
Duration("HEALTH_VPN_DURATION_ADDITION", gomock.Any()).
Return(value, err)
}
r := reader{
env: env,
warner: warner,
}
var health Health
err := health.read(r)
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.expected, health)
})
}
}

View File

@@ -1,55 +0,0 @@
package configuration
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_HealthyWait_String(t *testing.T) {
t.Parallel()
var healthyWait HealthyWait
const expected = "|--Initial duration: 0s"
s := healthyWait.String()
assert.Equal(t, expected, s)
}
func Test_HealthyWait_lines(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
settings HealthyWait
lines []string
}{
"empty": {
lines: []string{
"|--Initial duration: 0s",
},
},
"filled settings": {
settings: HealthyWait{
Initial: time.Second,
Addition: time.Minute,
},
lines: []string{
"|--Initial duration: 1s",
"|--Addition duration: 1m0s",
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
lines := testCase.settings.lines()
assert.Equal(t, testCase.lines, lines)
})
}
}

View File

@@ -1,30 +0,0 @@
package configuration
import (
"strings"
"time"
)
type HealthyWait struct {
// Initial is the initial duration to wait for the program
// to be healthy before taking action.
Initial time.Duration
// Addition is the duration to add to the Initial duration
// after Initial has expired to wait longer for the program
// to be healthy.
Addition time.Duration
}
func (settings *HealthyWait) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *HealthyWait) lines() (lines []string) {
lines = append(lines, lastIndent+"Initial duration: "+settings.Initial.String())
if settings.Addition > 0 {
lines = append(lines, lastIndent+"Addition duration: "+settings.Addition.String())
}
return lines
}

View File

@@ -1,40 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) readHideMyAss(r reader) (err error) {
settings.Name = constants.HideMyAss
servers := r.servers.GetHideMyAss()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.HideMyAssCountryChoices(servers))
if err != nil {
return fmt.Errorf("environment variable COUNTRY: %w", err)
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.HideMyAssCountryChoices(servers))
if err != nil {
return fmt.Errorf("environment variable REGION: %w", err)
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.HideMyAssCityChoices(servers))
if err != nil {
return fmt.Errorf("environment variable CITY: %w", err)
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME",
constants.HideMyAssHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
return settings.ServerSelection.OpenVPN.readProtocolAndPort(r)
}

View File

@@ -1,106 +0,0 @@
package configuration
import (
"fmt"
"strconv"
"strings"
"github.com/qdm12/golibs/params"
)
// HTTPProxy contains settings to configure the HTTP proxy.
type HTTPProxy struct {
User string
Password string
Port uint16
Enabled bool
Stealth bool
Log bool
}
func (settings *HTTPProxy) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *HTTPProxy) lines() (lines []string) {
if !settings.Enabled {
return nil
}
lines = append(lines, lastIndent+"HTTP proxy:")
lines = append(lines, indent+lastIndent+"Port: "+strconv.Itoa(int(settings.Port)))
if settings.User != "" {
lines = append(lines, indent+lastIndent+"Authentication: enabled")
}
if settings.Log {
lines = append(lines, indent+lastIndent+"Log: enabled")
}
if settings.Stealth {
lines = append(lines, indent+lastIndent+"Stealth: enabled")
}
return lines
}
func (settings *HTTPProxy) read(r reader) (err error) {
settings.Enabled, err = r.env.OnOff("HTTPPROXY", params.Default("off"),
params.RetroKeys([]string{"TINYPROXY", "PROXY"}, r.onRetroActive))
if err != nil {
return fmt.Errorf("environment variable HTTPPROXY (or TINYPROXY, PROXY): %w", err)
}
settings.User, err = r.getFromEnvOrSecretFile("HTTPPROXY_USER", false, // compulsory
[]string{"TINYPROXY_USER", "PROXY_USER"})
if err != nil {
return fmt.Errorf("environment variable HTTPPROXY_USER (or TINYPROXY_USER, PROXY_USER): %w", err)
}
settings.Password, err = r.getFromEnvOrSecretFile("HTTPPROXY_PASSWORD", false,
[]string{"TINYPROXY_PASSWORD", "PROXY_PASSWORD"})
if err != nil {
return fmt.Errorf("environment variable HTTPPROXY_PASSWORD (or TINYPROXY_PASSWORD, PROXY_PASSWORD): %w", err)
}
settings.Stealth, err = r.env.OnOff("HTTPPROXY_STEALTH", params.Default("off"))
if err != nil {
return fmt.Errorf("environment variable HTTPPROXY_STEALTH: %w", err)
}
if err := settings.readLog(r); err != nil {
return err
}
var warning string
settings.Port, warning, err = r.env.ListeningPort("HTTPPROXY_PORT", params.Default("8888"),
params.RetroKeys([]string{"TINYPROXY_PORT", "PROXY_PORT"}, r.onRetroActive))
if len(warning) > 0 {
r.warner.Warn(warning)
}
if err != nil {
return fmt.Errorf("environment variable HTTPPROXY_PORT (or TINYPROXY_PORT, PROXY_PORT): %w", err)
}
return nil
}
func (settings *HTTPProxy) readLog(r reader) error {
s, err := r.env.Get("HTTPPROXY_LOG",
params.RetroKeys([]string{"PROXY_LOG_LEVEL", "TINYPROXY_LOG"}, r.onRetroActive))
if err != nil {
return fmt.Errorf("environment variable HTTPPROXY_LOG (or TINYPROXY_LOG, PROXY_LOG_LEVEL): %w", err)
}
switch strings.ToLower(s) {
case "on":
settings.Log = true
// Retro compatibility
case "info", "connect", "notice":
settings.Log = true
}
return nil
}

View File

@@ -1,35 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) readIpvanish(r reader) (err error) {
settings.Name = constants.Ipvanish
servers := r.servers.GetIpvanish()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.IpvanishCountryChoices(servers))
if err != nil {
return fmt.Errorf("environment variable COUNTRY: %w", err)
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.IpvanishCityChoices(servers))
if err != nil {
return fmt.Errorf("environment variable CITY: %w", err)
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME",
constants.IpvanishHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
return settings.ServerSelection.OpenVPN.readProtocolOnly(r)
}

View File

@@ -1,170 +0,0 @@
package configuration
import (
"errors"
"net"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/params/mock_params"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Provider_readIpvanish(t *testing.T) {
t.Parallel()
var errDummy = errors.New("dummy test error")
type singleStringCall struct {
call bool
value string
err error
}
type sliceStringCall struct {
call bool
values []string
err error
}
testCases := map[string]struct {
targetIP singleStringCall
countries sliceStringCall
cities sliceStringCall
hostnames sliceStringCall
protocol singleStringCall
settings Provider
err error
}{
"target IP error": {
targetIP: singleStringCall{call: true, value: "something", err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errors.New("environment variable OPENVPN_TARGET_IP: dummy test error"),
},
"countries error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errors.New("environment variable COUNTRY: dummy test error"),
},
"cities error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errors.New("environment variable CITY: dummy test error"),
},
"hostnames error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errors.New("environment variable SERVER_HOSTNAME: dummy test error"),
},
"protocol error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true},
protocol: singleStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errors.New("environment variable OPENVPN_PROTOCOL: dummy test error"),
},
"default settings": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true},
protocol: singleStringCall{call: true},
settings: Provider{
Name: constants.Ipvanish,
},
},
"set settings": {
targetIP: singleStringCall{call: true, value: "1.2.3.4"},
countries: sliceStringCall{call: true, values: []string{"A", "B"}},
cities: sliceStringCall{call: true, values: []string{"C", "D"}},
hostnames: sliceStringCall{call: true, values: []string{"E", "F"}},
protocol: singleStringCall{call: true, value: constants.TCP},
settings: Provider{
Name: constants.Ipvanish,
ServerSelection: ServerSelection{
OpenVPN: OpenVPNSelection{
TCP: true,
},
TargetIP: net.IPv4(1, 2, 3, 4),
Countries: []string{"A", "B"},
Cities: []string{"C", "D"},
Hostnames: []string{"E", "F"},
},
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
servers := []models.IpvanishServer{{Hostname: "a"}}
allServers := models.AllServers{
Ipvanish: models.IpvanishServers{
Servers: servers,
},
}
env := mock_params.NewMockInterface(ctrl)
if testCase.targetIP.call {
env.EXPECT().Get("OPENVPN_TARGET_IP").
Return(testCase.targetIP.value, testCase.targetIP.err)
}
if testCase.countries.call {
env.EXPECT().CSVInside("COUNTRY", constants.IpvanishCountryChoices(servers)).
Return(testCase.countries.values, testCase.countries.err)
}
if testCase.cities.call {
env.EXPECT().CSVInside("CITY", constants.IpvanishCityChoices(servers)).
Return(testCase.cities.values, testCase.cities.err)
}
if testCase.hostnames.call {
env.EXPECT().CSVInside("SERVER_HOSTNAME", constants.IpvanishHostnameChoices(servers)).
Return(testCase.hostnames.values, testCase.hostnames.err)
}
if testCase.protocol.call {
env.EXPECT().Inside("OPENVPN_PROTOCOL", []string{constants.TCP, constants.UDP}, gomock.Any()).
Return(testCase.protocol.value, testCase.protocol.err)
}
r := reader{
servers: allServers,
env: env,
}
var settings Provider
err := settings.readIpvanish(r)
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.settings, settings)
})
}
}

View File

@@ -1,73 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
func (settings *Provider) readIvpn(r reader) (err error) {
settings.Name = constants.Ivpn
servers := r.servers.GetIvpn()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.IvpnCountryChoices(servers))
if err != nil {
return fmt.Errorf("environment variable COUNTRY: %w", err)
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.IvpnCityChoices(servers))
if err != nil {
return fmt.Errorf("environment variable CITY: %w", err)
}
settings.ServerSelection.ISPs, err = r.env.CSVInside("ISP", constants.IvpnISPChoices(servers))
if err != nil {
return fmt.Errorf("environment variable ISP: %w", err)
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.IvpnHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
err = settings.ServerSelection.OpenVPN.readIVPN(r)
if err != nil {
return err
}
return settings.ServerSelection.Wireguard.readIVPN(r.env)
}
func (settings *OpenVPNSelection) readIVPN(r reader) (err error) {
settings.TCP, err = readOpenVPNProtocol(r)
if err != nil {
return err
}
settings.CustomPort, err = readOpenVPNCustomPort(r, openvpnPortValidation{
tcp: settings.TCP,
allowedTCP: []uint16{80, 443, 1443},
allowedUDP: []uint16{53, 1194, 2049, 2050},
})
if err != nil {
return err
}
return nil
}
func (settings *WireguardSelection) readIVPN(env params.Interface) (err error) {
settings.EndpointPort, err = readWireguardCustomPort(env,
[]uint16{2049, 2050, 53, 30587, 41893, 48574, 58237})
if err != nil {
return err
}
return nil
}

View File

@@ -1,274 +0,0 @@
package configuration
import (
"errors"
"net"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/params/mock_params"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Provider_readIvpn(t *testing.T) { //nolint:gocognit
t.Parallel()
var errDummy = errors.New("dummy test error")
type singleStringCall struct {
call bool
value string
err error
}
type portCall struct {
getCall bool
getValue string // "" or "0"
getErr error
portCall bool
portValue uint16
portErr error
}
type sliceStringCall struct {
call bool
values []string
err error
}
testCases := map[string]struct {
targetIP singleStringCall
countries sliceStringCall
cities sliceStringCall
isps sliceStringCall
hostnames sliceStringCall
protocol singleStringCall
ovpnPort portCall
ovpnOldPort portCall
wgPort portCall
wgOldPort portCall
settings Provider
err error
}{
"target IP error": {
targetIP: singleStringCall{call: true, value: "something", err: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errors.New("environment variable OPENVPN_TARGET_IP: dummy test error"),
},
"countries error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errors.New("environment variable COUNTRY: dummy test error"),
},
"cities error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errors.New("environment variable CITY: dummy test error"),
},
"isps error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
isps: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errors.New("environment variable ISP: dummy test error"),
},
"hostnames error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
isps: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errors.New("environment variable SERVER_HOSTNAME: dummy test error"),
},
"openvpn protocol error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
isps: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true},
protocol: singleStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errors.New("environment variable OPENVPN_PROTOCOL: dummy test error"),
},
"openvpn custom port error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
isps: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true},
protocol: singleStringCall{call: true},
ovpnPort: portCall{getCall: true, getErr: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errors.New("environment variable OPENVPN_PORT: dummy test error"),
},
"wireguard custom port error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
isps: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true},
protocol: singleStringCall{call: true},
ovpnPort: portCall{getCall: true, getValue: "0"},
ovpnOldPort: portCall{getCall: true, getValue: "0"},
wgPort: portCall{getCall: true, getErr: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errors.New("environment variable WIREGUARD_ENDPOINT_PORT: dummy test error"),
},
"default settings": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
isps: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true},
protocol: singleStringCall{call: true},
ovpnPort: portCall{getCall: true, getValue: "0"},
ovpnOldPort: portCall{getCall: true, getValue: "0"},
wgPort: portCall{getCall: true, getValue: "0"},
wgOldPort: portCall{getCall: true, getValue: "0"},
settings: Provider{
Name: constants.Ivpn,
},
},
"set settings": {
targetIP: singleStringCall{call: true, value: "1.2.3.4"},
countries: sliceStringCall{call: true, values: []string{"A", "B"}},
cities: sliceStringCall{call: true, values: []string{"C", "D"}},
isps: sliceStringCall{call: true, values: []string{"ISP 1"}},
hostnames: sliceStringCall{call: true, values: []string{"E", "F"}},
protocol: singleStringCall{call: true, value: constants.TCP},
ovpnPort: portCall{getCall: true, portCall: true, portValue: 443},
wgPort: portCall{getCall: true, portCall: true, portValue: 2049},
settings: Provider{
Name: constants.Ivpn,
ServerSelection: ServerSelection{
OpenVPN: OpenVPNSelection{
TCP: true,
CustomPort: 443,
},
Wireguard: WireguardSelection{
EndpointPort: 2049,
},
TargetIP: net.IPv4(1, 2, 3, 4),
Countries: []string{"A", "B"},
Cities: []string{"C", "D"},
ISPs: []string{"ISP 1"},
Hostnames: []string{"E", "F"},
},
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
env := mock_params.NewMockInterface(ctrl)
servers := []models.IvpnServer{{Hostname: "a"}}
allServers := models.AllServers{
Ivpn: models.IvpnServers{
Servers: servers,
},
}
if testCase.targetIP.call {
env.EXPECT().Get("OPENVPN_TARGET_IP").
Return(testCase.targetIP.value, testCase.targetIP.err)
}
if testCase.countries.call {
env.EXPECT().CSVInside("COUNTRY", constants.IvpnCountryChoices(servers)).
Return(testCase.countries.values, testCase.countries.err)
}
if testCase.cities.call {
env.EXPECT().CSVInside("CITY", constants.IvpnCityChoices(servers)).
Return(testCase.cities.values, testCase.cities.err)
}
if testCase.isps.call {
env.EXPECT().CSVInside("ISP", constants.IvpnISPChoices(servers)).
Return(testCase.isps.values, testCase.isps.err)
}
if testCase.hostnames.call {
env.EXPECT().CSVInside("SERVER_HOSTNAME", constants.IvpnHostnameChoices(servers)).
Return(testCase.hostnames.values, testCase.hostnames.err)
}
if testCase.protocol.call {
env.EXPECT().Inside("OPENVPN_PROTOCOL", []string{constants.TCP, constants.UDP}, gomock.Any()).
Return(testCase.protocol.value, testCase.protocol.err)
}
if testCase.ovpnPort.getCall {
env.EXPECT().Get("OPENVPN_PORT", gomock.Any()).
Return(testCase.ovpnPort.getValue, testCase.ovpnPort.getErr)
}
if testCase.ovpnPort.portCall {
env.EXPECT().Port("OPENVPN_PORT").
Return(testCase.ovpnPort.portValue, testCase.ovpnPort.portErr)
}
if testCase.ovpnOldPort.getCall {
env.EXPECT().Get("PORT", gomock.Any()).
Return(testCase.ovpnOldPort.getValue, testCase.ovpnOldPort.getErr)
}
if testCase.ovpnOldPort.portCall {
env.EXPECT().Port("PORT").
Return(testCase.ovpnOldPort.portValue, testCase.ovpnOldPort.portErr)
}
if testCase.wgPort.getCall {
env.EXPECT().Get("WIREGUARD_ENDPOINT_PORT", gomock.Any()).
Return(testCase.wgPort.getValue, testCase.wgPort.getErr)
}
if testCase.wgPort.portCall {
env.EXPECT().Port("WIREGUARD_ENDPOINT_PORT").
Return(testCase.wgPort.portValue, testCase.wgPort.portErr)
}
if testCase.wgOldPort.getCall {
env.EXPECT().Get("WIREGUARD_PORT", gomock.Any()).
Return(testCase.wgOldPort.getValue, testCase.wgOldPort.getErr)
}
if testCase.wgOldPort.portCall {
env.EXPECT().Port("WIREGUARD_PORT").
Return(testCase.wgOldPort.portValue, testCase.wgOldPort.portErr)
}
r := reader{
servers: allServers,
env: env,
}
var settings Provider
err := settings.readIvpn(r)
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.settings, settings)
})
}
}

View File

@@ -1,29 +0,0 @@
package configuration
import (
"errors"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/openvpn/parse"
)
var (
errClientCert = errors.New("cannot read client certificate")
errClientKey = errors.New("cannot read client key")
)
func readClientKey(r reader) (clientKey string, err error) {
b, err := r.getFromFileOrSecretFile("OPENVPN_CLIENTKEY", constants.ClientKey)
if err != nil {
return "", err
}
return parse.ExtractPrivateKey(b)
}
func readClientCertificate(r reader) (clientCertificate string, err error) {
b, err := r.getFromFileOrSecretFile("OPENVPN_CLIENTCRT", constants.ClientCertificate)
if err != nil {
return "", err
}
return parse.ExtractCert(b)
}

View File

@@ -1,22 +0,0 @@
package configuration
import (
"net"
"strconv"
)
func uint16sToStrings(uint16s []uint16) (strings []string) {
strings = make([]string, len(uint16s))
for i := range uint16s {
strings[i] = strconv.Itoa(int(uint16s[i]))
}
return strings
}
func ipNetsToStrings(ipNets []net.IPNet) (strings []string) {
strings = make([]string, len(ipNets))
for i := range ipNets {
strings[i] = ipNets[i].String()
}
return strings
}

View File

@@ -1,30 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/params"
)
type Log struct {
Level logging.Level `json:"level"`
}
func (settings *Log) lines() (lines []string) {
lines = append(lines, lastIndent+"Log:")
lines = append(lines, indent+lastIndent+"Level: "+settings.Level.String())
return lines
}
func (settings *Log) read(env params.Interface) (err error) {
defaultLevel := logging.LevelInfo.String()
settings.Level, err = env.LogLevel("LOG_LEVEL", params.Default(defaultLevel))
if err != nil {
return fmt.Errorf("environment variable LOG_LEVEL: %w", err)
}
return nil
}

View File

@@ -1,77 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
func (settings *Provider) readMullvad(r reader) (err error) {
settings.Name = constants.Mullvad
servers := r.servers.GetMullvad()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.MullvadCountryChoices(servers))
if err != nil {
return fmt.Errorf("environment variable COUNTRY: %w", err)
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.MullvadCityChoices(servers))
if err != nil {
return fmt.Errorf("environment variable CITY: %w", err)
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.MullvadHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
settings.ServerSelection.ISPs, err = r.env.CSVInside("ISP", constants.MullvadISPChoices(servers))
if err != nil {
return fmt.Errorf("environment variable ISP: %w", err)
}
settings.ServerSelection.Owned, err = r.env.YesNo("OWNED", params.Default("no"))
if err != nil {
return fmt.Errorf("environment variable OWNED: %w", err)
}
err = settings.ServerSelection.OpenVPN.readMullvad(r)
if err != nil {
return err
}
return settings.ServerSelection.Wireguard.readMullvad(r.env)
}
func (settings *OpenVPNSelection) readMullvad(r reader) (err error) {
settings.TCP, err = readOpenVPNProtocol(r)
if err != nil {
return err
}
settings.CustomPort, err = readOpenVPNCustomPort(r, openvpnPortValidation{
tcp: settings.TCP,
allowedTCP: []uint16{80, 443, 1401},
allowedUDP: []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400},
})
if err != nil {
return err
}
return nil
}
func (settings *WireguardSelection) readMullvad(env params.Interface) (err error) {
settings.EndpointPort, err = readWireguardCustomPort(env, nil)
if err != nil {
return err
}
return nil
}

View File

@@ -1,58 +0,0 @@
package configuration
import (
"fmt"
"strconv"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
func (settings *Provider) readNordvpn(r reader) (err error) {
settings.Name = constants.Nordvpn
servers := r.servers.GetNordvpn()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.NordvpnRegionChoices(servers))
if err != nil {
return fmt.Errorf("environment variable REGION: %w", err)
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.NordvpnHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
settings.ServerSelection.Numbers, err = readNordVPNServerNumbers(r.env)
if err != nil {
return err
}
return settings.ServerSelection.OpenVPN.readProtocolOnly(r)
}
func readNordVPNServerNumbers(env params.Interface) (numbers []uint16, err error) {
const possiblePortsCount = 65537
possibilities := make([]string, possiblePortsCount)
for i := range possibilities {
possibilities[i] = fmt.Sprintf("%d", i)
}
possibilities[65536] = ""
values, err := env.CSVInside("SERVER_NUMBER", possibilities)
if err != nil {
return nil, err
}
numbers = make([]uint16, len(values))
for i := range values {
n, err := strconv.Atoi(values[i])
if err != nil {
return nil, err
}
numbers[i] = uint16(n)
}
return numbers, nil
}

View File

@@ -1,207 +0,0 @@
package configuration
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
// OpenVPN contains settings to configure the OpenVPN client.
type OpenVPN struct {
User string `json:"user"`
Password string `json:"password"`
Verbosity int `json:"verbosity"`
Flags []string `json:"flags"`
MSSFix uint16 `json:"mssfix"`
Root bool `json:"run_as_root"`
Ciphers []string `json:"ciphers"`
Auth string `json:"auth"`
ConfFile string `json:"conf_file"`
Version string `json:"version"`
ClientCrt string `json:"-"` // Cyberghost
ClientKey string `json:"-"` // Cyberghost, VPNUnlimited
EncPreset string `json:"encryption_preset"` // PIA
IPv6 bool `json:"ipv6"` // Mullvad
ProcUser string `json:"procuser"` // Process username
Interface string `json:"interface"`
}
func (settings *OpenVPN) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *OpenVPN) lines() (lines []string) {
lines = append(lines, lastIndent+"OpenVPN:")
lines = append(lines, indent+lastIndent+"Version: "+settings.Version)
lines = append(lines, indent+lastIndent+"Verbosity level: "+strconv.Itoa(settings.Verbosity))
lines = append(lines, indent+lastIndent+"Network interface: "+settings.Interface)
if len(settings.Flags) > 0 {
lines = append(lines, indent+lastIndent+"Flags: "+strings.Join(settings.Flags, " "))
}
if settings.Root {
lines = append(lines, indent+lastIndent+"Run as root: enabled")
}
if len(settings.Ciphers) > 0 {
lines = append(lines, indent+lastIndent+"Custom ciphers: "+commaJoin(settings.Ciphers))
}
if len(settings.Auth) > 0 {
lines = append(lines, indent+lastIndent+"Custom auth algorithm: "+settings.Auth)
}
if settings.ConfFile != "" {
lines = append(lines, indent+lastIndent+"Configuration file: "+settings.ConfFile)
}
if settings.ClientKey != "" {
lines = append(lines, indent+lastIndent+"Client key is set")
}
if settings.ClientCrt != "" {
lines = append(lines, indent+lastIndent+"Client certificate is set")
}
if settings.IPv6 {
lines = append(lines, indent+lastIndent+"IPv6: enabled")
}
if settings.EncPreset != "" { // PIA only
lines = append(lines, indent+lastIndent+"Encryption preset: "+settings.EncPreset)
}
return lines
}
func (settings *OpenVPN) read(r reader, serviceProvider string) (err error) {
credentialsRequired := false
switch serviceProvider {
case constants.Custom:
case constants.VPNUnlimited:
default:
credentialsRequired = true
}
settings.User, err = r.getFromEnvOrSecretFile("OPENVPN_USER", credentialsRequired, []string{"USER"})
if err != nil {
return fmt.Errorf("environment variable OPENVPN_USER: %w", err)
}
// Remove spaces in user ID to simplify user's life, thanks @JeordyR
settings.User = strings.ReplaceAll(settings.User, " ", "")
if serviceProvider == constants.Mullvad {
settings.Password = "m"
} else {
settings.Password, err = r.getFromEnvOrSecretFile("OPENVPN_PASSWORD", credentialsRequired, []string{"PASSWORD"})
if err != nil {
return err
}
}
settings.Version, err = r.env.Inside("OPENVPN_VERSION",
[]string{constants.Openvpn24, constants.Openvpn25}, params.Default(constants.Openvpn25))
if err != nil {
return fmt.Errorf("environment variable OPENVPN_VERSION: %w", err)
}
settings.Verbosity, err = r.env.IntRange("OPENVPN_VERBOSITY", 0, 6, params.Default("1")) //nolint:gomnd
if err != nil {
return fmt.Errorf("environment variable OPENVPN_VERBOSITY: %w", err)
}
settings.Flags = []string{}
flagsStr, err := r.env.Get("OPENVPN_FLAGS")
if err != nil {
return fmt.Errorf("environment variable OPENVPN_FLAGS: %w", err)
}
if flagsStr != "" {
settings.Flags = strings.Fields(flagsStr)
}
settings.Root, err = r.env.YesNo("OPENVPN_ROOT", params.Default("no"))
if err != nil {
return fmt.Errorf("environment variable OPENVPN_ROOT: %w", err)
}
settings.Ciphers, err = r.env.CSV("OPENVPN_CIPHER")
if err != nil {
return fmt.Errorf("environment variable OPENVPN_CIPHER: %w", err)
}
settings.Auth, err = r.env.Get("OPENVPN_AUTH")
if err != nil {
return fmt.Errorf("environment variable OPENVPN_AUTH: %w", err)
}
const maxMSSFix = 10000
mssFix, err := r.env.IntRange("OPENVPN_MSSFIX", 0, maxMSSFix, params.Default("0"))
if err != nil {
return fmt.Errorf("environment variable OPENVPN_MSSFIX: %w", err)
}
settings.MSSFix = uint16(mssFix)
settings.IPv6, err = r.env.OnOff("OPENVPN_IPV6", params.Default("off"))
if err != nil {
return fmt.Errorf("environment variable OPENVPN_IPV6: %w", err)
}
settings.Interface, err = readInterface(r.env)
if err != nil {
return err
}
switch serviceProvider {
case constants.Custom:
err = settings.readCustom(r) // read OPENVPN_CUSTOM_CONFIG
case constants.Cyberghost:
err = settings.readCyberghost(r)
case constants.PrivateInternetAccess:
settings.EncPreset, err = getPIAEncryptionPreset(r)
case constants.VPNUnlimited:
err = settings.readVPNUnlimited(r)
case constants.Wevpn:
err = settings.readWevpn(r)
}
if err != nil {
return err
}
return nil
}
func readOpenVPNProtocol(r reader) (tcp bool, err error) {
protocol, err := r.env.Inside("OPENVPN_PROTOCOL", []string{constants.TCP, constants.UDP},
params.Default(constants.UDP), params.RetroKeys([]string{"PROTOCOL"}, r.onRetroActive))
if err != nil {
return false, fmt.Errorf("environment variable OPENVPN_PROTOCOL: %w", err)
}
return protocol == constants.TCP, nil
}
const openvpnIntfRegexString = `^.*[0-9]$`
var openvpnIntfRegexp = regexp.MustCompile(openvpnIntfRegexString)
var errInterfaceNameNotValid = errors.New("interface name is not valid")
func readInterface(env params.Interface) (intf string, err error) {
intf, err = env.Get("OPENVPN_INTERFACE", params.Default("tun0"))
if err != nil {
return "", fmt.Errorf("environment variable OPENVPN_INTERFACE: %w", err)
}
if !openvpnIntfRegexp.MatchString(intf) {
return "", fmt.Errorf("%w: does not match regex %s: %s",
errInterfaceNameNotValid, openvpnIntfRegexString, intf)
}
return intf, nil
}

View File

@@ -1,40 +0,0 @@
package configuration
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_OpenVPN_JSON(t *testing.T) {
t.Parallel()
in := OpenVPN{
Root: true,
Flags: []string{},
Ciphers: []string{},
}
data, err := json.MarshalIndent(in, "", " ")
require.NoError(t, err)
assert.Equal(t, `{
"user": "",
"password": "",
"verbosity": 0,
"flags": [],
"mssfix": 0,
"run_as_root": true,
"ciphers": [],
"auth": "",
"conf_file": "",
"version": "",
"encryption_preset": "",
"ipv6": false,
"procuser": "",
"interface": ""
}`, string(data))
var out OpenVPN
err = json.Unmarshal(data, &out)
require.NoError(t, err)
assert.Equal(t, in, out)
}

View File

@@ -1,43 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) readPerfectPrivacy(r reader) (err error) {
settings.Name = constants.Perfectprivacy
servers := r.servers.GetPerfectprivacy()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PerfectprivacyCityChoices(servers))
if err != nil {
return fmt.Errorf("environment variable CITY: %w", err)
}
return settings.ServerSelection.OpenVPN.readPerfectPrivacy(r)
}
func (settings *OpenVPNSelection) readPerfectPrivacy(r reader) (err error) {
settings.TCP, err = readOpenVPNProtocol(r)
if err != nil {
return err
}
portValidation := openvpnPortValidation{
tcp: settings.TCP,
allowedTCP: []uint16{44, 443, 4433},
allowedUDP: []uint16{44, 443, 4433},
}
settings.CustomPort, err = readOpenVPNCustomPort(r, portValidation)
if err != nil {
return err
}
return nil
}

View File

@@ -1,39 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) readPrivado(r reader) (err error) {
settings.Name = constants.Privado
servers := r.servers.GetPrivado()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PrivadoCountryChoices(servers))
if err != nil {
return fmt.Errorf("environment variable COUNTRY: %w", err)
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.PrivadoRegionChoices(servers))
if err != nil {
return fmt.Errorf("environment variable REGION: %w", err)
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PrivadoCityChoices(servers))
if err != nil {
return fmt.Errorf("environment variable CITY: %w", err)
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PrivadoHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
return nil
}

View File

@@ -1,75 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
func (settings *Provider) readPrivateInternetAccess(r reader) (err error) {
settings.Name = constants.PrivateInternetAccess
servers := r.servers.GetPia()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.PIAGeoChoices(servers))
if err != nil {
return fmt.Errorf("environment variable REGION: %w", err)
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PIAHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
settings.ServerSelection.Names, err = r.env.CSVInside("SERVER_NAME", constants.PIANameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_NAME: %w", err)
}
settings.PortForwarding.Enabled, err = r.env.OnOff("PORT_FORWARDING", params.Default("off"))
if err != nil {
return fmt.Errorf("environment variable PORT_FORWARDING: %w", err)
}
if settings.PortForwarding.Enabled {
settings.PortForwarding.Filepath, err = r.env.Path("PORT_FORWARDING_STATUS_FILE",
params.Default("/tmp/gluetun/forwarded_port"), params.CaseSensitiveValue())
if err != nil {
return fmt.Errorf("environment variable PORT_FORWARDING_STATUS_FILE: %w", err)
}
}
return settings.ServerSelection.OpenVPN.readPrivateInternetAccess(r)
}
func (settings *OpenVPNSelection) readPrivateInternetAccess(r reader) (err error) {
settings.EncPreset, err = getPIAEncryptionPreset(r)
if err != nil {
return err
}
settings.CustomPort, err = readOpenVPNCustomPort(r, openvpnPortValidation{allAllowed: true})
if err != nil {
return err
}
return nil
}
func getPIAEncryptionPreset(r reader) (encryptionPreset string, err error) {
encryptionPreset, err = r.env.Inside("PIA_ENCRYPTION",
[]string{constants.PIAEncryptionPresetNone, constants.PIAEncryptionPresetNormal, constants.PIAEncryptionPresetStrong},
params.RetroKeys([]string{"ENCRYPTION"}, r.onRetroActive),
params.Default(constants.PIAEncryptionPresetStrong),
)
if err != nil {
return "", fmt.Errorf("environment variable PIA_ENCRYPTION: %w", err)
}
return encryptionPreset, nil
}

View File

@@ -1,35 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) readPrivatevpn(r reader) (err error) {
settings.Name = constants.Privatevpn
servers := r.servers.GetPrivatevpn()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PrivatevpnCountryChoices(servers))
if err != nil {
return fmt.Errorf("environment variable COUNTRY: %w", err)
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PrivatevpnCityChoices(servers))
if err != nil {
return fmt.Errorf("environment variable CITY: %w", err)
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME",
constants.PrivatevpnHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
return settings.ServerSelection.OpenVPN.readProtocolAndPort(r)
}

View File

@@ -1,51 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
func (settings *Provider) readProtonvpn(r reader) (err error) {
settings.Name = constants.Protonvpn
servers := r.servers.GetProtonvpn()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.ProtonvpnCountryChoices(servers))
if err != nil {
return fmt.Errorf("environment variable COUNTRY: %w", err)
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.ProtonvpnRegionChoices(servers))
if err != nil {
return fmt.Errorf("environment variable REGION: %w", err)
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.ProtonvpnCityChoices(servers))
if err != nil {
return fmt.Errorf("environment variable CITY: %w", err)
}
settings.ServerSelection.Names, err = r.env.CSVInside("SERVER_NAME", constants.ProtonvpnNameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_NAME: %w", err)
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME",
constants.ProtonvpnHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
settings.ServerSelection.FreeOnly, err = r.env.YesNo("FREE_ONLY", params.Default("no"))
if err != nil {
return fmt.Errorf("environment variable FREE_ONLY: %w", err)
}
return settings.ServerSelection.OpenVPN.readProtocolAndPort(r)
}

View File

@@ -1,248 +0,0 @@
package configuration
import (
"errors"
"fmt"
"net"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
// Provider contains settings specific to a VPN provider.
type Provider struct {
Name string `json:"name"`
ServerSelection ServerSelection `json:"server_selection"`
PortForwarding PortForwarding `json:"port_forwarding"`
}
func (settings *Provider) lines() (lines []string) {
if settings.Name == "" { // custom OpenVPN configuration
return nil
}
lines = append(lines, lastIndent+strings.Title(settings.Name)+" settings:")
for _, line := range settings.ServerSelection.toLines() {
lines = append(lines, indent+line)
}
if settings.PortForwarding.Enabled { // PIA
lines = append(lines, indent+lastIndent+"Port forwarding:")
for _, line := range settings.PortForwarding.lines() {
lines = append(lines, indent+indent+line)
}
}
return lines
}
var (
ErrInvalidVPNProvider = errors.New("invalid VPN provider")
)
func (settings *Provider) read(r reader, vpnType string) error {
err := settings.readVPNServiceProvider(r, vpnType)
if err != nil {
return err
}
switch settings.Name {
case constants.Custom:
err = settings.readCustom(r, vpnType)
case constants.Cyberghost:
err = settings.readCyberghost(r)
case constants.Expressvpn:
err = settings.readExpressvpn(r)
case constants.Fastestvpn:
err = settings.readFastestvpn(r)
case constants.HideMyAss:
err = settings.readHideMyAss(r)
case constants.Ipvanish:
err = settings.readIpvanish(r)
case constants.Ivpn:
err = settings.readIvpn(r)
case constants.Mullvad:
err = settings.readMullvad(r)
case constants.Nordvpn:
err = settings.readNordvpn(r)
case constants.Perfectprivacy:
err = settings.readPerfectPrivacy(r)
case constants.Privado:
err = settings.readPrivado(r)
case constants.PrivateInternetAccess:
err = settings.readPrivateInternetAccess(r)
case constants.Privatevpn:
err = settings.readPrivatevpn(r)
case constants.Protonvpn:
err = settings.readProtonvpn(r)
case constants.Purevpn:
err = settings.readPurevpn(r)
case constants.Surfshark:
err = settings.readSurfshark(r)
case constants.Torguard:
err = settings.readTorguard(r)
case constants.VPNUnlimited:
err = settings.readVPNUnlimited(r)
case constants.Vyprvpn:
err = settings.readVyprvpn(r)
case constants.Wevpn:
err = settings.readWevpn(r)
case constants.Windscribe:
err = settings.readWindscribe(r)
default:
return fmt.Errorf("%w: %s", ErrInvalidVPNProvider, settings.Name)
}
if err != nil {
return err
}
settings.ServerSelection.VPN = vpnType
return nil
}
func (settings *Provider) readVPNServiceProvider(r reader, vpnType string) (err error) {
var allowedVPNServiceProviders []string
switch vpnType {
case constants.OpenVPN:
allowedVPNServiceProviders = []string{
constants.Custom,
"cyberghost", constants.Expressvpn, "fastestvpn", "hidemyass", "ipvanish",
"ivpn", "mullvad", "nordvpn",
constants.Perfectprivacy, "privado", "pia", "private internet access", "privatevpn", "protonvpn",
"purevpn", "surfshark", "torguard", constants.VPNUnlimited, "vyprvpn",
constants.Wevpn, "windscribe"}
case constants.Wireguard:
allowedVPNServiceProviders = []string{
constants.Custom, constants.Ivpn,
constants.Mullvad, constants.Windscribe,
}
}
vpnsp, err := r.env.Inside("VPNSP", allowedVPNServiceProviders,
params.Default("private internet access"))
if err != nil {
return fmt.Errorf("environment variable VPNSP: %w", err)
}
if vpnsp == "pia" { // retro compatibility
vpnsp = "private internet access"
}
if settings.isOpenVPNCustomConfig(r.env) { // retro compatibility
vpnsp = constants.Custom
}
settings.Name = vpnsp
return nil
}
func commaJoin(slice []string) string {
return strings.Join(slice, ", ")
}
func protoToString(tcp bool) string {
if tcp {
return constants.TCP
}
return constants.UDP
}
func readTargetIP(env params.Interface) (targetIP net.IP, err error) {
targetIP, err = readIP(env, "OPENVPN_TARGET_IP")
if err != nil {
return nil, fmt.Errorf("environment variable OPENVPN_TARGET_IP: %w", err)
}
return targetIP, nil
}
type openvpnPortValidation struct {
allAllowed bool
tcp bool
allowedTCP []uint16
allowedUDP []uint16
}
func readOpenVPNCustomPort(r reader, validation openvpnPortValidation) (
port uint16, err error) {
port, err = readPortOrZero(r.env, "OPENVPN_PORT")
if err != nil {
return 0, fmt.Errorf("environment variable OPENVPN_PORT: %w", err)
} else if port == 0 {
// Try using old variable name
port, err = readPortOrZero(r.env, "PORT")
if err != nil {
r.onRetroActive("PORT", "OPENVPN_PORT")
return 0, fmt.Errorf("environment variable PORT: %w", err)
}
}
if port == 0 || validation.allAllowed {
return port, nil
}
if validation.tcp {
for _, allowedPort := range validation.allowedTCP {
if port == allowedPort {
return port, nil
}
}
return 0, fmt.Errorf(
"environment variable PORT: %w: port %d for TCP protocol, can only be one of %s",
ErrInvalidPort, port, portsToString(validation.allowedTCP))
}
for _, allowedPort := range validation.allowedUDP {
if port == allowedPort {
return port, nil
}
}
return 0, fmt.Errorf(
"environment variable PORT: %w: port %d for UDP protocol, can only be one of %s",
ErrInvalidPort, port, portsToString(validation.allowedUDP))
}
// note: set allowed to an empty slice to allow all valid ports
func readWireguardCustomPort(env params.Interface, allowed []uint16) (port uint16, err error) {
port, err = readPortOrZero(env, "WIREGUARD_ENDPOINT_PORT")
if err != nil {
return 0, fmt.Errorf("environment variable WIREGUARD_ENDPOINT_PORT: %w", err)
} else if port == 0 {
port, _ = readPortOrZero(env, "WIREGUARD_PORT")
if err == nil {
return port, nil // 0 or WIREGUARD_PORT value
}
return 0, nil // default 0
}
if len(allowed) == 0 {
return port, nil
}
for i := range allowed {
if allowed[i] == port {
return port, nil
}
}
return 0, fmt.Errorf(
"environment variable WIREGUARD_PORT: %w: port %d, can only be one of %s",
ErrInvalidPort, port, portsToString(allowed))
}
func portsToString(ports []uint16) string {
slice := make([]string, len(ports))
for i := range ports {
slice[i] = fmt.Sprint(ports[i])
}
return strings.Join(slice, ", ")
}
// isOpenVPNCustomConfig is for retro compatibility to set VPNSP=custom
// if OPENVPN_CUSTOM_CONFIG is set.
func (settings Provider) isOpenVPNCustomConfig(env params.Interface) (ok bool) {
s, _ := env.Get("VPN_TYPE")
isOpenVPN := s == constants.OpenVPN
s, _ = env.Get("OPENVPN_CUSTOM_CONFIG")
return isOpenVPN && s != ""
}

View File

@@ -1,462 +0,0 @@
package configuration
import (
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params/mock_params"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var errDummy = errors.New("dummy")
func Test_Provider_lines(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
settings Provider
lines []string
}{
"cyberghost": {
settings: Provider{
Name: constants.Cyberghost,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Countries: []string{"a", "El country"},
},
},
lines: []string{
"|--Cyberghost settings:",
" |--Countries: a, El country",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"expressvpn": {
settings: Provider{
Name: constants.Expressvpn,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Hostnames: []string{"a", "b"},
Countries: []string{"c", "d"},
Cities: []string{"e", "f"},
},
},
lines: []string{
"|--Expressvpn settings:",
" |--Countries: c, d",
" |--Cities: e, f",
" |--Hostnames: a, b",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"fastestvpn": {
settings: Provider{
Name: constants.Fastestvpn,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Hostnames: []string{"a", "b"},
Countries: []string{"c", "d"},
},
},
lines: []string{
"|--Fastestvpn settings:",
" |--Countries: c, d",
" |--Hostnames: a, b",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"hidemyass": {
settings: Provider{
Name: constants.HideMyAss,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e", "f"},
},
},
lines: []string{
"|--Hidemyass settings:",
" |--Countries: a, b",
" |--Cities: c, d",
" |--Hostnames: e, f",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"ipvanish": {
settings: Provider{
Name: constants.Ipvanish,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e", "f"},
},
},
lines: []string{
"|--Ipvanish settings:",
" |--Countries: a, b",
" |--Cities: c, d",
" |--Hostnames: e, f",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"ivpn": {
settings: Provider{
Name: constants.Ivpn,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e", "f"},
},
},
lines: []string{
"|--Ivpn settings:",
" |--Countries: a, b",
" |--Cities: c, d",
" |--Hostnames: e, f",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"mullvad": {
settings: Provider{
Name: constants.Mullvad,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
ISPs: []string{"e", "f"},
OpenVPN: OpenVPNSelection{
CustomPort: 1,
},
},
},
lines: []string{
"|--Mullvad settings:",
" |--Countries: a, b",
" |--Cities: c, d",
" |--ISPs: e, f",
" |--OpenVPN selection:",
" |--Protocol: udp",
" |--Custom port: 1",
},
},
"nordvpn": {
settings: Provider{
Name: constants.Nordvpn,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Regions: []string{"a", "b"},
Numbers: []uint16{1, 2},
},
},
lines: []string{
"|--Nordvpn settings:",
" |--Regions: a, b",
" |--Numbers: 1, 2",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"perfectprivacy": {
settings: Provider{
Name: constants.Perfectprivacy,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Cities: []string{"a", "b"},
},
},
lines: []string{
"|--Perfect Privacy settings:",
" |--Cities: a, b",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"privado": {
settings: Provider{
Name: constants.Privado,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Hostnames: []string{"a", "b"},
},
},
lines: []string{
"|--Privado settings:",
" |--Hostnames: a, b",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"privatevpn": {
settings: Provider{
Name: constants.Privatevpn,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Hostnames: []string{"a", "b"},
Countries: []string{"c", "d"},
Cities: []string{"e", "f"},
},
},
lines: []string{
"|--Privatevpn settings:",
" |--Countries: c, d",
" |--Cities: e, f",
" |--Hostnames: a, b",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"protonvpn": {
settings: Provider{
Name: constants.Protonvpn,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Countries: []string{"a", "b"},
Regions: []string{"c", "d"},
Cities: []string{"e", "f"},
Names: []string{"g", "h"},
Hostnames: []string{"i", "j"},
},
},
lines: []string{
"|--Protonvpn settings:",
" |--Countries: a, b",
" |--Regions: c, d",
" |--Cities: e, f",
" |--Hostnames: i, j",
" |--Names: g, h",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"private internet access": {
settings: Provider{
Name: constants.PrivateInternetAccess,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Regions: []string{"a", "b"},
OpenVPN: OpenVPNSelection{
CustomPort: 1,
},
},
PortForwarding: PortForwarding{
Enabled: true,
Filepath: string("/here"),
},
},
lines: []string{
"|--Private Internet Access settings:",
" |--Regions: a, b",
" |--OpenVPN selection:",
" |--Protocol: udp",
" |--Custom port: 1",
" |--Port forwarding:",
" |--File path: /here",
},
},
"purevpn": {
settings: Provider{
Name: constants.Purevpn,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Regions: []string{"a", "b"},
Countries: []string{"c", "d"},
Cities: []string{"e", "f"},
},
},
lines: []string{
"|--Purevpn settings:",
" |--Countries: c, d",
" |--Regions: a, b",
" |--Cities: e, f",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"surfshark": {
settings: Provider{
Name: constants.Surfshark,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Regions: []string{"a", "b"},
},
},
lines: []string{
"|--Surfshark settings:",
" |--Regions: a, b",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"torguard": {
settings: Provider{
Name: constants.Torguard,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e"},
},
},
lines: []string{
"|--Torguard settings:",
" |--Countries: a, b",
" |--Cities: c, d",
" |--Hostnames: e",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
constants.VPNUnlimited: {
settings: Provider{
Name: constants.VPNUnlimited,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e", "f"},
FreeOnly: true,
StreamOnly: true,
},
},
lines: []string{
"|--Vpn Unlimited settings:",
" |--Countries: a, b",
" |--Cities: c, d",
" |--Free servers only",
" |--Stream servers only",
" |--Hostnames: e, f",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"vyprvpn": {
settings: Provider{
Name: constants.Vyprvpn,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Regions: []string{"a", "b"},
},
},
lines: []string{
"|--Vyprvpn settings:",
" |--Regions: a, b",
" |--OpenVPN selection:",
" |--Protocol: udp",
},
},
"wevpn": {
settings: Provider{
Name: constants.Wevpn,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Cities: []string{"a", "b"},
Hostnames: []string{"c", "d"},
OpenVPN: OpenVPNSelection{
CustomPort: 1,
},
},
},
lines: []string{
"|--Wevpn settings:",
" |--Cities: a, b",
" |--Hostnames: c, d",
" |--OpenVPN selection:",
" |--Protocol: udp",
" |--Custom port: 1",
},
},
"windscribe": {
settings: Provider{
Name: constants.Windscribe,
ServerSelection: ServerSelection{
VPN: constants.OpenVPN,
Regions: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e", "f"},
OpenVPN: OpenVPNSelection{
CustomPort: 1,
},
},
},
lines: []string{
"|--Windscribe settings:",
" |--Regions: a, b",
" |--Cities: c, d",
" |--Hostnames: e, f",
" |--OpenVPN selection:",
" |--Protocol: udp",
" |--Custom port: 1",
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
lines := testCase.settings.lines()
assert.Equal(t, testCase.lines, lines)
})
}
}
func Test_readProtocol(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
mockStr string
mockErr error
tcp bool
err error
}{
"error": {
mockErr: errDummy,
err: errors.New("environment variable OPENVPN_PROTOCOL: dummy"),
},
"success": {
mockStr: "tcp",
tcp: true,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
env := mock_params.NewMockInterface(ctrl)
env.EXPECT().
Inside("OPENVPN_PROTOCOL", []string{"tcp", "udp"}, gomock.Any(), gomock.Any()).
Return(testCase.mockStr, testCase.mockErr)
reader := reader{
env: env,
}
tcp, err := readOpenVPNProtocol(reader)
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.tcp, tcp)
})
}
}

View File

@@ -1,47 +0,0 @@
package configuration
import (
"fmt"
"strings"
"time"
"github.com/qdm12/golibs/params"
)
type PublicIP struct {
Period time.Duration `json:"period"`
IPFilepath string `json:"ip_filepath"`
}
func (settings *PublicIP) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *PublicIP) lines() (lines []string) {
if settings.Period == 0 {
lines = append(lines, lastIndent+"Public IP getter: disabled")
return lines
}
lines = append(lines, lastIndent+"Public IP getter:")
lines = append(lines, indent+lastIndent+"Fetch period: "+settings.Period.String())
lines = append(lines, indent+lastIndent+"IP file: "+settings.IPFilepath)
return lines
}
func (settings *PublicIP) read(r reader) (err error) {
settings.Period, err = r.env.Duration("PUBLICIP_PERIOD", params.Default("12h"))
if err != nil {
return fmt.Errorf("environment variable PUBLICIP_PERIOD: %w", err)
}
settings.IPFilepath, err = r.env.Path("PUBLICIP_FILE", params.CaseSensitiveValue(),
params.Default("/tmp/gluetun/ip"),
params.RetroKeys([]string{"IP_STATUS_FILE"}, r.onRetroActive))
if err != nil {
return fmt.Errorf("environment variable PUBLICIP_FILE (or IP_STATUS_FILE): %w", err)
}
return nil
}

View File

@@ -1,39 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) readPurevpn(r reader) (err error) {
settings.Name = constants.Purevpn
servers := r.servers.GetPurevpn()
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.PurevpnRegionChoices(servers))
if err != nil {
return fmt.Errorf("environment variable REGION: %w", err)
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PurevpnCountryChoices(servers))
if err != nil {
return fmt.Errorf("environment variable COUNTRY: %w", err)
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PurevpnCityChoices(servers))
if err != nil {
return fmt.Errorf("environment variable CITY: %w", err)
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PurevpnHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)
}
return settings.ServerSelection.OpenVPN.readProtocolOnly(r)
}

View File

@@ -1,134 +0,0 @@
package configuration
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/qdm12/gluetun/internal/models"
ovpnextract "github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/golibs/params"
"github.com/qdm12/golibs/verification"
)
//go:generate mockgen -destination=warner_mock_test.go -package configuration . Warner
type reader struct {
servers models.AllServers
env params.Interface
warner Warner
regex verification.Regex
ovpnExt ovpnextract.Interface
}
type Warner interface {
Warn(s string)
}
func newReader(env params.Interface,
servers models.AllServers, warner Warner) reader {
return reader{
servers: servers,
env: env,
warner: warner,
regex: verification.NewRegex(),
ovpnExt: ovpnextract.New(),
}
}
func (r *reader) onRetroActive(oldKey, newKey string) {
r.warner.Warn(
"You are using the old environment variable " + oldKey +
", please consider changing it to " + newKey)
}
var (
ErrInvalidPort = errors.New("invalid port")
)
func readCSVPorts(env params.Interface, key string) (ports []uint16, err error) {
s, err := env.Get(key)
if err != nil {
return nil, err
} else if s == "" {
return nil, nil
}
portsStr := strings.Split(s, ",")
ports = make([]uint16, len(portsStr))
for i, portStr := range portsStr {
portInt, err := strconv.Atoi(portStr)
if err != nil {
return nil, fmt.Errorf("%w: %s: %s", ErrInvalidPort, portStr, err)
} else if portInt <= 0 || portInt > 65535 {
return nil, fmt.Errorf("%w: %d: must be between 1 and 65535", ErrInvalidPort, portInt)
}
ports[i] = uint16(portInt)
}
return ports, nil
}
var (
ErrInvalidIPNet = errors.New("invalid IP network")
)
func readCSVIPNets(env params.Interface, key string, options ...params.OptionSetter) (
ipNets []net.IPNet, err error) {
s, err := env.Get(key, options...)
if err != nil {
return nil, err
} else if s == "" {
return nil, nil
}
ipNetsStr := strings.Split(s, ",")
ipNets = make([]net.IPNet, len(ipNetsStr))
for i, ipNetStr := range ipNetsStr {
_, ipNet, err := net.ParseCIDR(ipNetStr)
if err != nil {
return nil, fmt.Errorf("%w: %s: %s",
ErrInvalidIPNet, ipNetStr, err)
} else if ipNet == nil {
return nil, fmt.Errorf("%w: %s: subnet is nil", ErrInvalidIPNet, ipNetStr)
}
ipNets[i] = *ipNet
}
return ipNets, nil
}
var (
ErrInvalidIP = errors.New("invalid IP address")
)
func readIP(env params.Interface, key string) (ip net.IP, err error) {
s, err := env.Get(key)
if s == "" {
return nil, nil
} else if err != nil {
return nil, err
}
ip = net.ParseIP(s)
if ip == nil {
return nil, fmt.Errorf("%w: %s", ErrInvalidIP, s)
}
return ip, nil
}
func readPortOrZero(env params.Interface, key string) (port uint16, err error) {
s, err := env.Get(key, params.Default("0"))
if err != nil {
return 0, err
}
if s == "0" {
return 0, nil
}
return env.Port(key)
}

View File

@@ -1,119 +0,0 @@
package configuration
import (
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/qdm12/golibs/params"
)
var (
ErrGetSecretFilepath = errors.New("cannot get secret file path from env")
ErrReadSecretFile = errors.New("cannot read secret file")
ErrSecretFileIsEmpty = errors.New("secret file is empty")
ErrReadNonSecretFile = errors.New("cannot read non secret file")
ErrFilesDoNotExist = errors.New("files do not exist")
)
func cleanSuffix(value string) string {
value = strings.TrimSuffix(value, "\n")
value = strings.TrimSuffix(value, "\r")
return value
}
func (r *reader) getFromEnvOrSecretFile(envKey string, compulsory bool, retroKeys []string) (value string, err error) {
envOptions := []params.OptionSetter{
params.Compulsory(), // to fallback on file reading
params.CaseSensitiveValue(),
params.Unset(),
params.RetroKeys(retroKeys, r.onRetroActive),
}
value, envErr := r.env.Get(envKey, envOptions...)
if envErr == nil {
value = cleanSuffix(value)
return value, nil
}
secretFilepathEnvKey := envKey + "_SECRETFILE"
defaultSecretFile := "/run/secrets/" + strings.ToLower(envKey)
filepath, err := r.env.Get(secretFilepathEnvKey,
params.CaseSensitiveValue(),
params.Default(defaultSecretFile),
)
if err != nil {
return "", fmt.Errorf("%w: environment variable %s: %s",
ErrGetSecretFilepath, secretFilepathEnvKey, err)
}
file, fileErr := os.OpenFile(filepath, os.O_RDONLY, 0)
if os.IsNotExist(fileErr) {
if compulsory {
return "", fmt.Errorf("environment variable %s: %w", envKey, envErr)
}
return "", nil
} else if fileErr != nil {
return "", fmt.Errorf("%w: %s: %s", ErrReadSecretFile, filepath, fileErr)
}
b, err := io.ReadAll(file)
if err != nil {
return "", fmt.Errorf("%w: %s: %s", ErrReadSecretFile, filepath, err)
}
value = string(b)
value = cleanSuffix(value)
if compulsory && value == "" {
return "", fmt.Errorf("%s: %w", filepath, ErrSecretFileIsEmpty)
}
return value, nil
}
// Tries to read from the secret file then the non secret file.
func (r *reader) getFromFileOrSecretFile(secretName, filepath string) (
b []byte, err error) {
defaultSecretFile := "/run/secrets/" + strings.ToLower(secretName)
key := strings.ToUpper(secretName) + "_SECRETFILE"
secretFilepath, err := r.env.Get(key,
params.CaseSensitiveValue(),
params.Default(defaultSecretFile),
)
if err != nil {
return b, fmt.Errorf("environment variable %s: %w: %s", key, ErrGetSecretFilepath, err)
}
b, err = readFromFile(secretFilepath)
if err != nil && !os.IsNotExist(err) {
return b, fmt.Errorf("%w: %s", ErrReadSecretFile, err)
} else if err == nil {
return b, nil
}
// Secret file does not exist, try the non secret file
b, err = readFromFile(filepath)
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("%w: %s", ErrReadSecretFile, err)
} else if err == nil {
return b, nil
}
return nil, fmt.Errorf("%w: %s and %s", ErrFilesDoNotExist, secretFilepath, filepath)
}
func readFromFile(filepath string) (b []byte, err error) {
file, err := os.Open(filepath)
if err != nil {
return nil, err
}
b, err = io.ReadAll(file)
if err != nil {
_ = file.Close()
return nil, err
}
if err := file.Close(); err != nil {
return nil, err
}
return b, nil
}

View File

@@ -1,189 +0,0 @@
package configuration
import (
"fmt"
"net"
"github.com/qdm12/gluetun/internal/constants"
)
type ServerSelection struct { //nolint:maligned
// Common
VPN string `json:"vpn"` // note: this is required
TargetIP net.IP `json:"target_ip,omitempty"`
// TODO comments
// Cyberghost, PIA, Protonvpn, Surfshark, Windscribe, Vyprvpn, NordVPN
Regions []string `json:"regions"`
// Expressvpn, Fastestvpn, HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited
Countries []string `json:"countries"`
// Expressvpn, HideMyAss, IPVanish, IVPN, Mullvad, Perfectprivacy, PrivateVPN, Protonvpn,
// PureVPN, VPNUnlimited, WeVPN, Windscribe
Cities []string `json:"cities"`
// Expressvpn, Fastestvpn, HideMyAss, IPVanish, IVPN, PrivateVPN, Windscribe, Privado, Protonvpn, VPNUnlimited, WeVPN
Hostnames []string `json:"hostnames"`
Names []string `json:"names"` // Protonvpn
// Mullvad
ISPs []string `json:"isps"`
Owned bool `json:"owned"`
// NordVPN
Numbers []uint16 `json:"numbers"`
// ProtonVPN
FreeOnly bool `json:"free_only"`
// VPNUnlimited
StreamOnly bool `json:"stream_only"`
// Surfshark
MultiHopOnly bool `json:"multihop_only"`
OpenVPN OpenVPNSelection `json:"openvpn"`
Wireguard WireguardSelection `json:"wireguard"`
}
func (selection ServerSelection) toLines() (lines []string) {
if selection.TargetIP != nil {
lines = append(lines, lastIndent+"Target IP address: "+selection.TargetIP.String())
}
if len(selection.Countries) > 0 {
lines = append(lines, lastIndent+"Countries: "+commaJoin(selection.Countries))
}
if len(selection.Regions) > 0 {
lines = append(lines, lastIndent+"Regions: "+commaJoin(selection.Regions))
}
if len(selection.Cities) > 0 {
lines = append(lines, lastIndent+"Cities: "+commaJoin(selection.Cities))
}
if len(selection.ISPs) > 0 {
lines = append(lines, lastIndent+"ISPs: "+commaJoin(selection.ISPs))
}
if selection.FreeOnly {
lines = append(lines, lastIndent+"Free servers only")
}
if selection.StreamOnly {
lines = append(lines, lastIndent+"Stream servers only")
}
if len(selection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(selection.Hostnames))
}
if len(selection.Names) > 0 {
lines = append(lines, lastIndent+"Names: "+commaJoin(selection.Names))
}
if len(selection.Numbers) > 0 {
numbersString := make([]string, len(selection.Numbers))
for i, numberUint16 := range selection.Numbers {
numbersString[i] = fmt.Sprint(numberUint16)
}
lines = append(lines, lastIndent+"Numbers: "+commaJoin(numbersString))
}
if selection.VPN == constants.OpenVPN {
lines = append(lines, selection.OpenVPN.lines()...)
} else { // wireguard
lines = append(lines, selection.Wireguard.lines()...)
}
return lines
}
type OpenVPNSelection struct {
ConfFile string `json:"conf_file"` // Custom configuration file path
TCP bool `json:"tcp"` // UDP if TCP is false
CustomPort uint16 `json:"custom_port"` // HideMyAss, Mullvad, PIA, ProtonVPN, WeVPN, Windscribe
EncPreset string `json:"encryption_preset"` // PIA - needed to get the port number
}
func (settings *OpenVPNSelection) lines() (lines []string) {
lines = append(lines, lastIndent+"OpenVPN selection:")
if settings.ConfFile != "" {
lines = append(lines, indent+lastIndent+"Custom configuration file: "+settings.ConfFile)
}
lines = append(lines, indent+lastIndent+"Protocol: "+protoToString(settings.TCP))
if settings.CustomPort != 0 {
lines = append(lines, indent+lastIndent+"Custom port: "+fmt.Sprint(settings.CustomPort))
}
if settings.EncPreset != "" {
lines = append(lines, indent+lastIndent+"PIA encryption preset: "+settings.EncPreset)
}
return lines
}
func (settings *OpenVPNSelection) readProtocolOnly(r reader) (err error) {
settings.TCP, err = readOpenVPNProtocol(r)
return err
}
func (settings *OpenVPNSelection) readProtocolAndPort(r reader) (err error) {
settings.TCP, err = readOpenVPNProtocol(r)
if err != nil {
return err
}
settings.CustomPort, err = readOpenVPNCustomPort(r, openvpnPortValidation{allAllowed: true})
if err != nil {
return err
}
return nil
}
type WireguardSelection struct {
// EndpointPort is a the server port to use for the VPN server.
// It is optional for Wireguard VPN providers IVPN, Mullvad
// and Windscribe, and compulsory for the others
EndpointPort uint16 `json:"port,omitempty"`
// PublicKey is the server public key.
// It is only used with VPN providers generating Wireguard
// configurations specific to each server and user.
PublicKey string `json:"publickey,omitempty"`
// EndpointIP is the server endpoint IP address.
// It is only used with VPN providers generating Wireguard
// configurations specific to each server and user.
EndpointIP net.IP `json:"endpoint_ip,omitempty"`
}
func (settings *WireguardSelection) lines() (lines []string) {
lines = append(lines, lastIndent+"Wireguard selection:")
if settings.PublicKey != "" {
lines = append(lines, indent+lastIndent+"Public key: "+settings.PublicKey)
}
if settings.EndpointIP != nil {
endpoint := settings.EndpointIP.String() + ":" + fmt.Sprint(settings.EndpointPort)
lines = append(lines, indent+lastIndent+"Server endpoint: "+endpoint)
} else if settings.EndpointPort != 0 {
lines = append(lines, indent+lastIndent+"Custom port: "+fmt.Sprint(settings.EndpointPort))
}
return lines
}
// PortForwarding contains settings for port forwarding.
type PortForwarding struct {
Enabled bool `json:"enabled"`
Filepath string `json:"filepath"`
}
func (p *PortForwarding) lines() (lines []string) {
return []string{
lastIndent + "File path: " + p.Filepath,
}
}

View File

@@ -1,50 +0,0 @@
package configuration
import (
"fmt"
"strconv"
"strings"
"github.com/qdm12/golibs/params"
)
// ControlServer contains settings to customize the control server operation.
type ControlServer struct {
Port uint16
Log bool
}
func (settings *ControlServer) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *ControlServer) lines() (lines []string) {
lines = append(lines, lastIndent+"HTTP control server:")
lines = append(lines, indent+lastIndent+"Listening port: "+strconv.Itoa(int(settings.Port)))
if settings.Log {
lines = append(lines, indent+lastIndent+"Logging: enabled")
}
return lines
}
func (settings *ControlServer) read(r reader) (err error) {
settings.Log, err = r.env.OnOff("HTTP_CONTROL_SERVER_LOG", params.Default("on"))
if err != nil {
return fmt.Errorf("environment variable HTTP_CONTROL_SERVER_LOG: %w", err)
}
var warning string
settings.Port, warning, err = r.env.ListeningPort(
"HTTP_CONTROL_SERVER_PORT", params.Default("8000"))
if len(warning) > 0 {
r.warner.Warn(warning)
}
if err != nil {
return fmt.Errorf("environment variable HTTP_CONTROL_SERVER_PORT: %w", err)
}
return nil
}

View File

@@ -1,125 +0,0 @@
package configuration
import (
"errors"
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/params"
)
// Settings contains all settings for the program to run.
type Settings struct {
VPN VPN
System System
DNS DNS
Firewall Firewall
HTTPProxy HTTPProxy
ShadowSocks ShadowSocks
Updater Updater
PublicIP PublicIP
VersionInformation bool
ControlServer ControlServer
Health Health
Log Log
}
func (settings *Settings) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *Settings) lines() (lines []string) {
lines = append(lines, "Settings summary below:")
lines = append(lines, settings.VPN.lines()...)
lines = append(lines, settings.DNS.lines()...)
lines = append(lines, settings.Firewall.lines()...)
lines = append(lines, settings.Log.lines()...)
lines = append(lines, settings.System.lines()...)
lines = append(lines, settings.HTTPProxy.lines()...)
lines = append(lines, settings.ShadowSocks.lines()...)
lines = append(lines, settings.Health.lines()...)
lines = append(lines, settings.ControlServer.lines()...)
lines = append(lines, settings.Updater.lines()...)
lines = append(lines, settings.PublicIP.lines()...)
if settings.VersionInformation {
lines = append(lines, lastIndent+"Github version information: enabled")
}
return lines
}
var (
ErrVPN = errors.New("cannot read VPN settings")
ErrSystem = errors.New("cannot read System settings")
ErrDNS = errors.New("cannot read DNS settings")
ErrFirewall = errors.New("cannot read firewall settings")
ErrHTTPProxy = errors.New("cannot read HTTP proxy settings")
ErrShadowsocks = errors.New("cannot read Shadowsocks settings")
ErrControlServer = errors.New("cannot read control server settings")
ErrUpdater = errors.New("cannot read Updater settings")
ErrPublicIP = errors.New("cannot read Public IP getter settings")
ErrHealth = errors.New("cannot read health settings")
ErrLog = errors.New("cannot read log settings")
)
// Read obtains all configuration options for the program and returns an error as soon
// as an error is encountered reading them.
func (settings *Settings) Read(env params.Interface, servers models.AllServers,
warner Warner) (err error) {
r := newReader(env, servers, warner)
settings.VersionInformation, err = r.env.OnOff("VERSION_INFORMATION", params.Default("on"))
if err != nil {
return fmt.Errorf("environment variable VERSION_INFORMATION: %w", err)
}
if err := settings.VPN.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrVPN, err)
}
if err := settings.System.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrSystem, err)
}
if err := settings.DNS.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrDNS, err)
}
if err := settings.Firewall.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrFirewall, err)
}
if err := settings.HTTPProxy.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrHTTPProxy, err)
}
if err := settings.ShadowSocks.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrShadowsocks, err)
}
if err := settings.ControlServer.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrControlServer, err)
}
if err := settings.Updater.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrUpdater, err)
}
if ip := settings.DNS.PlaintextAddress; ip != nil {
settings.Updater.DNSAddress = ip.String()
}
if err := settings.PublicIP.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrPublicIP, err)
}
if err := settings.Health.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrHealth, err)
}
if err := settings.Log.read(r.env); err != nil {
return fmt.Errorf("%w: %s", ErrLog, err)
}
return nil
}

View File

@@ -0,0 +1,82 @@
package settings
import (
"fmt"
"net"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// DNS contains settings to configure DNS.
type DNS struct {
// ServerAddress is the DNS server to use inside
// the Go program and for the system.
// It defaults to '127.0.0.1' to be used with the
// DoT server. It cannot be nil in the internal
// state.
ServerAddress net.IP
// KeepNameserver is true if the Docker DNS server
// found in /etc/resolv.conf should be kept.
// Note settings this to true will go around the
// DoT server blocking.
// It defaults to false and cannot be nil in the
// internal state.
KeepNameserver *bool
// DOT contains settings to configure the DoT
// server.
DoT DoT
}
func (d DNS) validate() (err error) {
err = d.DoT.validate()
if err != nil {
return fmt.Errorf("failed validating DoT settings: %w", err)
}
return nil
}
func (d *DNS) Copy() (copied DNS) {
return DNS{
ServerAddress: helpers.CopyIP(d.ServerAddress),
KeepNameserver: helpers.CopyBoolPtr(d.KeepNameserver),
DoT: d.DoT.copy(),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (d *DNS) mergeWith(other DNS) {
d.ServerAddress = helpers.MergeWithIP(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = helpers.MergeWithBool(d.KeepNameserver, other.KeepNameserver)
d.DoT.mergeWith(other.DoT)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (d *DNS) overrideWith(other DNS) {
d.ServerAddress = helpers.OverrideWithIP(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = helpers.OverrideWithBool(d.KeepNameserver, other.KeepNameserver)
d.DoT.overrideWith(other.DoT)
}
func (d *DNS) setDefaults() {
localhost := net.IPv4(127, 0, 0, 1) //nolint:gomnd
d.ServerAddress = helpers.DefaultIP(d.ServerAddress, localhost)
d.KeepNameserver = helpers.DefaultBool(d.KeepNameserver, false)
d.DoT.setDefaults()
}
func (d DNS) String() string {
return d.toLinesNode().String()
}
func (d DNS) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS settings:")
node.Appendf("DNS server address to use: %s", d.ServerAddress)
node.Appendf("Keep existing nameserver(s): %s", helpers.BoolPtrToYesNo(d.KeepNameserver))
node.AppendNode(d.DoT.toLinesNode())
return node
}

View File

@@ -0,0 +1,138 @@
package settings
import (
"errors"
"fmt"
"regexp"
"github.com/qdm12/dns/pkg/blacklist"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
"inet.af/netaddr"
)
// DNSBlacklist is settings for the DNS blacklist building.
type DNSBlacklist struct {
BlockMalicious *bool
BlockAds *bool
BlockSurveillance *bool
AllowedHosts []string
AddBlockedHosts []string
AddBlockedIPs []netaddr.IP
AddBlockedIPPrefixes []netaddr.IPPrefix
}
func (b *DNSBlacklist) setDefaults() {
b.BlockMalicious = helpers.DefaultBool(b.BlockMalicious, true)
b.BlockAds = helpers.DefaultBool(b.BlockAds, false)
b.BlockSurveillance = helpers.DefaultBool(b.BlockSurveillance, true)
}
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
var (
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
)
func (b DNSBlacklist) validate() (err error) {
for _, host := range b.AllowedHosts {
if !hostRegex.MatchString(host) {
return fmt.Errorf("%w: %s", ErrAllowedHostNotValid, host)
}
}
for _, host := range b.AddBlockedHosts {
if !hostRegex.MatchString(host) {
return fmt.Errorf("%w: %s", ErrBlockedHostNotValid, host)
}
}
return nil
}
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
return DNSBlacklist{
BlockMalicious: helpers.CopyBoolPtr(b.BlockMalicious),
BlockAds: helpers.CopyBoolPtr(b.BlockAds),
BlockSurveillance: helpers.CopyBoolPtr(b.BlockSurveillance),
AllowedHosts: helpers.CopyStringSlice(b.AllowedHosts),
AddBlockedHosts: helpers.CopyStringSlice(b.AddBlockedHosts),
AddBlockedIPs: helpers.CopyNetaddrIPsSlice(b.AddBlockedIPs),
AddBlockedIPPrefixes: helpers.CopyIPPrefixSlice(b.AddBlockedIPPrefixes),
}
}
func (b *DNSBlacklist) mergeWith(other DNSBlacklist) {
b.BlockMalicious = helpers.MergeWithBool(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = helpers.MergeWithBool(b.BlockAds, other.BlockAds)
b.BlockSurveillance = helpers.MergeWithBool(b.BlockSurveillance, other.BlockSurveillance)
b.AllowedHosts = helpers.MergeStringSlices(b.AllowedHosts, other.AllowedHosts)
b.AddBlockedHosts = helpers.MergeStringSlices(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = helpers.MergeNetaddrIPsSlices(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = helpers.MergeIPPrefixesSlices(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
}
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
b.BlockMalicious = helpers.OverrideWithBool(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = helpers.OverrideWithBool(b.BlockAds, other.BlockAds)
b.BlockSurveillance = helpers.OverrideWithBool(b.BlockSurveillance, other.BlockSurveillance)
b.AllowedHosts = helpers.OverrideWithStringSlice(b.AllowedHosts, other.AllowedHosts)
b.AddBlockedHosts = helpers.OverrideWithStringSlice(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = helpers.OverrideWithNetaddrIPsSlice(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = helpers.OverrideWithIPPrefixesSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
}
func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) {
return blacklist.BuilderSettings{
BlockMalicious: *b.BlockMalicious,
BlockAds: *b.BlockAds,
BlockSurveillance: *b.BlockSurveillance,
AllowedHosts: b.AllowedHosts,
AddBlockedHosts: b.AddBlockedHosts,
AddBlockedIPs: b.AddBlockedIPs,
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
}, nil
}
func (b DNSBlacklist) String() string {
return b.toLinesNode().String()
}
func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS filtering settings:")
node.Appendf("Block malicious: %s", helpers.BoolPtrToYesNo(b.BlockMalicious))
node.Appendf("Block ads: %s", helpers.BoolPtrToYesNo(b.BlockAds))
node.Appendf("Block surveillance: %s", helpers.BoolPtrToYesNo(b.BlockSurveillance))
if len(b.AllowedHosts) > 0 {
allowedHostsNode := node.Appendf("Allowed hosts:")
for _, host := range b.AllowedHosts {
allowedHostsNode.Appendf(host)
}
}
if len(b.AddBlockedHosts) > 0 {
blockedHostsNode := node.Appendf("Blocked hosts:")
for _, host := range b.AddBlockedHosts {
blockedHostsNode.Appendf(host)
}
}
if len(b.AddBlockedIPs) > 0 {
blockedIPsNode := node.Appendf("Blocked IP addresses:")
for _, ip := range b.AddBlockedIPs {
blockedIPsNode.Appendf(ip.String())
}
}
if len(b.AddBlockedIPPrefixes) > 0 {
blockedIPPrefixesNode := node.Appendf("Blocked IP networks:")
for _, ipNetwork := range b.AddBlockedIPPrefixes {
blockedIPPrefixesNode.Appendf(ipNetwork.String())
}
}
return node
}

View File

@@ -0,0 +1,113 @@
package settings
import (
"errors"
"fmt"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// DoT contains settings to configure the DoT server.
type DoT struct {
// Enabled is true if the DoT server should be running
// and used. It defaults to true, and cannot be nil
// in the internal state.
Enabled *bool
// UpdatePeriod is the period to update DNS block
// lists and cryptographic files for DNSSEC validation.
// It can be set to 0 to disable the update.
// It defaults to 24h and cannot be nil in
// the internal state.
UpdatePeriod *time.Duration
// Unbound contains settings to configure Unbound.
Unbound Unbound
// Blacklist contains settings to configure the filter
// block lists.
Blacklist DNSBlacklist
}
var (
ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
)
func (d DoT) validate() (err error) {
const minUpdatePeriod = 30 * time.Second
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
return fmt.Errorf("%w: %s must be bigger than %s",
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
}
err = d.Unbound.validate()
if err != nil {
return err
}
err = d.Blacklist.validate()
if err != nil {
return err
}
return nil
}
func (d *DoT) copy() (copied DoT) {
return DoT{
Enabled: helpers.CopyBoolPtr(d.Enabled),
UpdatePeriod: helpers.CopyDurationPtr(d.UpdatePeriod),
Unbound: d.Unbound.copy(),
Blacklist: d.Blacklist.copy(),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (d *DoT) mergeWith(other DoT) {
d.Enabled = helpers.MergeWithBool(d.Enabled, other.Enabled)
d.UpdatePeriod = helpers.MergeWithDuration(d.UpdatePeriod, other.UpdatePeriod)
d.Unbound.mergeWith(other.Unbound)
d.Blacklist.mergeWith(other.Blacklist)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (d *DoT) overrideWith(other DoT) {
d.Enabled = helpers.OverrideWithBool(d.Enabled, other.Enabled)
d.UpdatePeriod = helpers.OverrideWithDuration(d.UpdatePeriod, other.UpdatePeriod)
d.Unbound.overrideWith(other.Unbound)
d.Blacklist.overrideWith(other.Blacklist)
}
func (d *DoT) setDefaults() {
d.Enabled = helpers.DefaultBool(d.Enabled, true)
const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = helpers.DefaultDuration(d.UpdatePeriod, defaultUpdatePeriod)
d.Unbound.setDefaults()
d.Blacklist.setDefaults()
}
func (d DoT) String() string {
return d.toLinesNode().String()
}
func (d DoT) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS over TLS settings:")
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(d.Enabled))
if !*d.Enabled {
return node
}
update := "disabled"
if *d.UpdatePeriod > 0 {
update = "every " + d.UpdatePeriod.String()
}
node.Appendf("Update period: %s", update)
node.AppendNode(d.Unbound.toLinesNode())
node.AppendNode(d.Blacklist.toLinesNode())
return node
}

View File

@@ -0,0 +1,45 @@
package settings
import "errors"
var (
ErrCityNotValid = errors.New("the city specified is not valid")
ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root")
ErrCountryNotValid = errors.New("the country specified is not valid")
ErrFilepathMissing = errors.New("filepath is missing")
ErrFirewallZeroPort = errors.New("cannot have a zero port to block")
ErrHostnameNotValid = errors.New("the hostname specified is not valid")
ErrISPNotValid = errors.New("the ISP specified is not valid")
ErrMinRatioNotValid = errors.New("minimum ratio is not valid")
ErrMissingValue = errors.New("missing value")
ErrNameNotValid = errors.New("the server name specified is not valid")
ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high")
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
ErrOpenVPNUserIsEmpty = errors.New("user is empty")
ErrOpenVPNVerbosityIsOutOfBounds = errors.New("verbosity value is out of bounds")
ErrOpenVPNVersionIsNotValid = errors.New("version is not valid")
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
ErrPublicIPPeriodTooShort = errors.New("public IP address check period is too short")
ErrRegionNotValid = errors.New("the region specified is not valid")
ErrServerAddressNotValid = errors.New("server listening address is not valid")
ErrSystemPGIDNotValid = errors.New("process group id is not valid")
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set")
ErrWireguardInterfaceNotValid = errors.New("interface name is not valid")
ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set")
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
ErrWireguardPublicKeyNotSet = errors.New("public key is not set")
ErrWireguardPublicKeyNotValid = errors.New("public key is not valid")
)

View File

@@ -0,0 +1,117 @@
package settings
import (
"fmt"
"net"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// Firewall contains settings to customize the firewall operation.
type Firewall struct {
VPNInputPorts []uint16
InputPorts []uint16
OutboundSubnets []net.IPNet
Enabled *bool
Debug *bool
}
func (f Firewall) validate() (err error) {
if hasZeroPort(f.VPNInputPorts) {
return fmt.Errorf("VPN input ports: %w", ErrFirewallZeroPort)
}
if hasZeroPort(f.InputPorts) {
return fmt.Errorf("input ports: %w", ErrFirewallZeroPort)
}
return nil
}
func hasZeroPort(ports []uint16) (has bool) {
for _, port := range ports {
if port == 0 {
return true
}
}
return false
}
func (f *Firewall) copy() (copied Firewall) {
return Firewall{
VPNInputPorts: helpers.CopyUint16Slice(f.VPNInputPorts),
InputPorts: helpers.CopyUint16Slice(f.InputPorts),
OutboundSubnets: helpers.CopyIPNetSlice(f.OutboundSubnets),
Enabled: helpers.CopyBoolPtr(f.Enabled),
Debug: helpers.CopyBoolPtr(f.Debug),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
// It merges values of slices together, even if they
// are set in the receiver settings.
func (f *Firewall) mergeWith(other Firewall) {
f.VPNInputPorts = helpers.MergeUint16Slices(f.VPNInputPorts, other.VPNInputPorts)
f.InputPorts = helpers.MergeUint16Slices(f.InputPorts, other.InputPorts)
f.OutboundSubnets = helpers.MergeIPNetsSlices(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = helpers.MergeWithBool(f.Enabled, other.Enabled)
f.Debug = helpers.MergeWithBool(f.Debug, other.Debug)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (f *Firewall) overrideWith(other Firewall) {
f.VPNInputPorts = helpers.OverrideWithUint16Slice(f.VPNInputPorts, other.VPNInputPorts)
f.InputPorts = helpers.OverrideWithUint16Slice(f.InputPorts, other.InputPorts)
f.OutboundSubnets = helpers.OverrideWithIPNetsSlice(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = helpers.OverrideWithBool(f.Enabled, other.Enabled)
f.Debug = helpers.OverrideWithBool(f.Debug, other.Debug)
}
func (f *Firewall) setDefaults() {
f.Enabled = helpers.DefaultBool(f.Enabled, true)
f.Debug = helpers.DefaultBool(f.Debug, false)
}
func (f Firewall) String() string {
return f.toLinesNode().String()
}
func (f Firewall) toLinesNode() (node *gotree.Node) {
node = gotree.New("Firewall settings:")
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(f.Enabled))
if !*f.Enabled {
return node
}
if *f.Debug {
node.Appendf("Debug mode: on")
}
if len(f.VPNInputPorts) > 0 {
vpnInputPortsNode := node.Appendf("VPN input ports:")
for _, port := range f.VPNInputPorts {
vpnInputPortsNode.Appendf("%d", port)
}
}
if len(f.InputPorts) > 0 {
inputPortsNode := node.Appendf("Input ports:")
for _, port := range f.InputPorts {
inputPortsNode.Appendf("%d", port)
}
}
if len(f.OutboundSubnets) > 0 {
outboundSubnets := node.Appendf("Outbound subnets:")
for _, subnet := range f.OutboundSubnets {
outboundSubnets.Appendf("%s", subnet)
}
}
return node
}

View File

@@ -0,0 +1,82 @@
package settings
import (
"fmt"
"os"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
"github.com/qdm12/govalid/address"
)
// Health contains settings for the healthcheck and health server.
type Health struct {
// ServerAddress is the listening address
// for the health check server.
// It cannot be the empty string in the internal state.
ServerAddress string
// TargetAddress is the address (host or host:port)
// to TCP dial to periodically for the health check.
// It cannot be the empty string in the internal state.
TargetAddress string
VPN HealthyWait
}
func (h Health) Validate() (err error) {
uid := os.Getuid()
_, err = address.Validate(h.ServerAddress,
address.OptionListening(uid))
if err != nil {
return fmt.Errorf("server listening address is not valid: %w", err)
}
err = h.VPN.validate()
if err != nil {
return fmt.Errorf("health VPN settings: %w", err)
}
return nil
}
func (h *Health) copy() (copied Health) {
return Health{
ServerAddress: h.ServerAddress,
TargetAddress: h.TargetAddress,
VPN: h.VPN.copy(),
}
}
// MergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *Health) MergeWith(other Health) {
h.ServerAddress = helpers.MergeWithString(h.ServerAddress, other.ServerAddress)
h.TargetAddress = helpers.MergeWithString(h.TargetAddress, other.TargetAddress)
h.VPN.mergeWith(other.VPN)
}
// OverrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (h *Health) OverrideWith(other Health) {
h.ServerAddress = helpers.OverrideWithString(h.ServerAddress, other.ServerAddress)
h.TargetAddress = helpers.OverrideWithString(h.TargetAddress, other.TargetAddress)
h.VPN.overrideWith(other.VPN)
}
func (h *Health) SetDefaults() {
h.ServerAddress = helpers.DefaultString(h.ServerAddress, "127.0.0.1:9999")
h.TargetAddress = helpers.DefaultString(h.TargetAddress, "cloudflare.com:443")
h.VPN.setDefaults()
}
func (h Health) String() string {
return h.toLinesNode().String()
}
func (h Health) toLinesNode() (node *gotree.Node) {
node = gotree.New("Health settings:")
node.Appendf("Server listening address: %s", h.ServerAddress)
node.Appendf("Target address: %s", h.TargetAddress)
node.AppendNode(h.VPN.toLinesNode("VPN"))
return node
}

View File

@@ -0,0 +1,66 @@
package settings
import (
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
type HealthyWait struct {
// Initial is the initial duration to wait for the program
// to be healthy before taking action.
// It cannot be nil in the internal state.
Initial *time.Duration
// Addition is the duration to add to the Initial duration
// after Initial has expired to wait longer for the program
// to be healthy.
// It cannot be nil in the internal state.
Addition *time.Duration
}
func (h HealthyWait) validate() (err error) {
return nil
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HealthyWait) copy() (copied HealthyWait) {
return HealthyWait{
Initial: helpers.CopyDurationPtr(h.Initial),
Addition: helpers.CopyDurationPtr(h.Addition),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HealthyWait) mergeWith(other HealthyWait) {
h.Initial = helpers.MergeWithDuration(h.Initial, other.Initial)
h.Addition = helpers.MergeWithDuration(h.Addition, other.Addition)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (h *HealthyWait) overrideWith(other HealthyWait) {
h.Initial = helpers.OverrideWithDuration(h.Initial, other.Initial)
h.Addition = helpers.OverrideWithDuration(h.Addition, other.Addition)
}
func (h *HealthyWait) setDefaults() {
const initialDurationDefault = 6 * time.Second
const additionDurationDefault = 5 * time.Second
h.Initial = helpers.DefaultDuration(h.Initial, initialDurationDefault)
h.Addition = helpers.DefaultDuration(h.Addition, additionDurationDefault)
}
func (h HealthyWait) String() string {
return h.toLinesNode("Health").String()
}
func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) {
node = gotree.New(kind + " wait durations:")
node.Appendf("Initial duration: %s", *h.Initial)
node.Appendf("Additional duration: %s", *h.Addition)
return node
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
package settings
func boolPtr(b bool) *bool { return &b }
func uint8Ptr(n uint8) *uint8 { return &n }

View File

@@ -0,0 +1,111 @@
package settings
import (
"fmt"
"os"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
"github.com/qdm12/govalid/address"
)
// HTTPProxy contains settings to configure the HTTP proxy.
type HTTPProxy struct {
// User is the username to use for the HTTP proxy.
// It cannot be nil in the internal state.
User *string
// Password is the password to use for the HTTP proxy.
// It cannot be nil in the internal state.
Password *string
// ListeningAddress is the listening address
// of the HTTP proxy server.
// It cannot be the empty string in the internal state.
ListeningAddress string
// Enabled is true if the HTTP proxy server should run,
// and false otherwise. It cannot be nil in the
// internal state.
Enabled *bool
// Stealth is true if the HTTP proxy server should hide
// each request has been proxied to the destination.
// It cannot be nil in the internal state.
Stealth *bool
// Log is true if the HTTP proxy server should log
// each request/response. It cannot be nil in the
// internal state.
Log *bool
}
func (h HTTPProxy) validate() (err error) {
// Do not validate user and password
uid := os.Getuid()
_, err = address.Validate(h.ListeningAddress, address.OptionListening(uid))
if err != nil {
return fmt.Errorf("%w: %s", ErrServerAddressNotValid, h.ListeningAddress)
}
return nil
}
func (h *HTTPProxy) copy() (copied HTTPProxy) {
return HTTPProxy{
User: helpers.CopyStringPtr(h.User),
Password: helpers.CopyStringPtr(h.Password),
ListeningAddress: h.ListeningAddress,
Enabled: helpers.CopyBoolPtr(h.Enabled),
Stealth: helpers.CopyBoolPtr(h.Stealth),
Log: helpers.CopyBoolPtr(h.Log),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HTTPProxy) mergeWith(other HTTPProxy) {
h.User = helpers.MergeWithStringPtr(h.User, other.User)
h.Password = helpers.MergeWithStringPtr(h.Password, other.Password)
h.ListeningAddress = helpers.MergeWithString(h.ListeningAddress, other.ListeningAddress)
h.Enabled = helpers.MergeWithBool(h.Enabled, other.Enabled)
h.Stealth = helpers.MergeWithBool(h.Stealth, other.Stealth)
h.Log = helpers.MergeWithBool(h.Log, other.Log)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (h *HTTPProxy) overrideWith(other HTTPProxy) {
h.User = helpers.OverrideWithStringPtr(h.User, other.User)
h.Password = helpers.OverrideWithStringPtr(h.Password, other.Password)
h.ListeningAddress = helpers.OverrideWithString(h.ListeningAddress, other.ListeningAddress)
h.Enabled = helpers.OverrideWithBool(h.Enabled, other.Enabled)
h.Stealth = helpers.OverrideWithBool(h.Stealth, other.Stealth)
h.Log = helpers.OverrideWithBool(h.Log, other.Log)
}
func (h *HTTPProxy) setDefaults() {
h.User = helpers.DefaultStringPtr(h.User, "")
h.Password = helpers.DefaultStringPtr(h.Password, "")
h.ListeningAddress = helpers.DefaultString(h.ListeningAddress, ":8888")
h.Enabled = helpers.DefaultBool(h.Enabled, false)
h.Stealth = helpers.DefaultBool(h.Stealth, false)
h.Log = helpers.DefaultBool(h.Log, false)
}
func (h HTTPProxy) String() string {
return h.toLinesNode().String()
}
func (h HTTPProxy) toLinesNode() (node *gotree.Node) {
node = gotree.New("HTTP proxy settings:")
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(h.Enabled))
if !*h.Enabled {
return node
}
node.Appendf("Listening address: %s", h.ListeningAddress)
node.Appendf("User: %s", *h.User)
node.Appendf("Password: %s", helpers.ObfuscatePassword(*h.Password))
node.Appendf("Stealth mode: %s", helpers.BoolPtrToYesNo(h.Stealth))
node.Appendf("Log: %s", helpers.BoolPtrToYesNo(h.Log))
return node
}

View File

@@ -0,0 +1,51 @@
package settings
import (
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
"github.com/qdm12/log"
)
// Log contains settings to configure the logger.
type Log struct {
// Level is the log level of the logger.
// It cannot be nil in the internal state.
Level *log.Level
}
func (l Log) validate() (err error) {
return nil
}
func (l *Log) copy() (copied Log) {
return Log{
Level: helpers.CopyLogLevelPtr(l.Level),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (l *Log) mergeWith(other Log) {
l.Level = helpers.MergeWithLogLevel(l.Level, other.Level)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (l *Log) overrideWith(other Log) {
l.Level = helpers.OverrideWithLogLevel(l.Level, other.Level)
}
func (l *Log) setDefaults() {
l.Level = helpers.DefaultLogLevel(l.Level, log.LevelInfo)
}
func (l Log) String() string {
return l.toLinesNode().String()
}
func (l Log) toLinesNode() (node *gotree.Node) {
node = gotree.New("Log settings:")
node.Appendf("Log level: %s", l.Level.String())
return node
}

View File

@@ -0,0 +1,350 @@
package settings
import (
"encoding/base64"
"fmt"
"regexp"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/openvpn"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gotree"
)
// OpenVPN contains settings to configure the OpenVPN client.
type OpenVPN struct {
// Version is the OpenVPN version to run.
// It can only be "2.4" or "2.5".
Version string
// User is the OpenVPN authentication username.
// It cannot be an empty string in the internal state
// if OpenVPN is used.
User string
// Password is the OpenVPN authentication password.
// It cannot be an empty string in the internal state
// if OpenVPN is used.
Password string
// ConfFile is a custom OpenVPN configuration file path.
// It can be set to the empty string for it to be ignored.
// It cannot be nil in the internal state.
ConfFile *string
// Ciphers is a list of ciphers to use for OpenVPN,
// different from the ones specified by the VPN
// service provider configuration files.
Ciphers []string
// Auth is an auth algorithm to use in OpenVPN instead
// of the one specified by the VPN service provider.
// It cannot be nil in the internal state.
// It is ignored if it is set to the empty string.
Auth *string
// ClientCrt is the OpenVPN client certificate.
// This is notably used by Cyberghost.
// It can be set to the empty string to be ignored.
// It cannot be nil in the internal state.
ClientCrt *string
// ClientKey is the OpenVPN client key.
// This is used by Cyberghost and VPN Unlimited.
// It can be set to the empty string to be ignored.
// It cannot be nil in the internal state.
ClientKey *string
// PIAEncPreset is the encryption preset for
// Private Internet Access. It can be set to an
// empty string for other providers.
PIAEncPreset *string
// IPv6 is set to true if IPv6 routing should be
// set to be tunnel in OpenVPN, and false otherwise.
// It cannot be nil in the internal state.
IPv6 *bool // TODO automate like with Wireguard
// MSSFix is the value (1 to 10000) to set for the
// mssfix option for OpenVPN. It is ignored if set to 0.
// It cannot be nil in the internal state.
MSSFix *uint16
// Interface is the OpenVPN device interface name.
// It cannot be an empty string in the internal state.
Interface string
// ProcessUser is the OpenVPN process OS username
// to use. It cannot be empty in the internal state.
// It defaults to 'root'.
ProcessUser string
// Verbosity is the OpenVPN verbosity level from 0 to 6.
// It cannot be nil in the internal state.
Verbosity *int
// Flags is a slice of additional flags to be passed
// to the OpenVPN program.
Flags []string
}
var ivpnAccountID = regexp.MustCompile(`^(i|ivpn)\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}$`)
func (o OpenVPN) validate(vpnProvider string) (err error) {
// Validate version
validVersions := []string{openvpn.Openvpn24, openvpn.Openvpn25}
if !helpers.IsOneOf(o.Version, validVersions...) {
return fmt.Errorf("%w: %q can only be one of %s",
ErrOpenVPNVersionIsNotValid, o.Version, strings.Join(validVersions, ", "))
}
isCustom := vpnProvider == providers.Custom
if !isCustom && o.User == "" {
return ErrOpenVPNUserIsEmpty
}
passwordRequired := !isCustom &&
(vpnProvider != providers.Ivpn || !ivpnAccountID.MatchString(o.User))
if passwordRequired && o.Password == "" {
return ErrOpenVPNPasswordIsEmpty
}
err = validateOpenVPNConfigFilepath(isCustom, *o.ConfFile)
if err != nil {
return fmt.Errorf("custom configuration file: %w", err)
}
err = validateOpenVPNClientCertificate(vpnProvider, *o.ClientCrt)
if err != nil {
return fmt.Errorf("client certificate: %w", err)
}
err = validateOpenVPNClientKey(vpnProvider, *o.ClientKey)
if err != nil {
return fmt.Errorf("client key: %w", err)
}
const maxMSSFix = 10000
if *o.MSSFix > maxMSSFix {
return fmt.Errorf("%w: %d is over the maximum value of %d",
ErrOpenVPNMSSFixIsTooHigh, *o.MSSFix, maxMSSFix)
}
if !regexpInterfaceName.MatchString(o.Interface) {
return fmt.Errorf("%w: '%s' does not match regex '%s'",
ErrOpenVPNInterfaceNotValid, o.Interface, regexpInterfaceName)
}
if *o.Verbosity < 0 || *o.Verbosity > 6 {
return fmt.Errorf("%w: %d can only be between 0 and 5",
ErrOpenVPNVerbosityIsOutOfBounds, o.Verbosity)
}
return nil
}
func validateOpenVPNConfigFilepath(isCustom bool,
confFile string) (err error) {
if !isCustom {
return nil
}
if confFile == "" {
return ErrFilepathMissing
}
err = helpers.FileExists(confFile)
if err != nil {
return err
}
extractor := extract.New()
_, _, err = extractor.Data(confFile)
if err != nil {
return fmt.Errorf("failed extracting information from custom configuration file: %w", err)
}
return nil
}
func validateOpenVPNClientCertificate(vpnProvider,
clientCert string) (err error) {
switch vpnProvider {
case
providers.Cyberghost,
providers.VPNUnlimited:
if clientCert == "" {
return ErrMissingValue
}
}
if clientCert == "" {
return nil
}
_, err = base64.StdEncoding.DecodeString(clientCert)
if err != nil {
return err
}
return nil
}
func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
switch vpnProvider {
case
providers.Cyberghost,
providers.VPNUnlimited,
providers.Wevpn:
if clientKey == "" {
return ErrMissingValue
}
}
if clientKey == "" {
return nil
}
_, err = base64.StdEncoding.DecodeString(clientKey)
if err != nil {
return err
}
return nil
}
func (o *OpenVPN) copy() (copied OpenVPN) {
return OpenVPN{
Version: o.Version,
User: o.User,
Password: o.Password,
ConfFile: helpers.CopyStringPtr(o.ConfFile),
Ciphers: helpers.CopyStringSlice(o.Ciphers),
Auth: helpers.CopyStringPtr(o.Auth),
ClientCrt: helpers.CopyStringPtr(o.ClientCrt),
ClientKey: helpers.CopyStringPtr(o.ClientKey),
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
IPv6: helpers.CopyBoolPtr(o.IPv6),
MSSFix: helpers.CopyUint16Ptr(o.MSSFix),
Interface: o.Interface,
ProcessUser: o.ProcessUser,
Verbosity: helpers.CopyIntPtr(o.Verbosity),
Flags: helpers.CopyStringSlice(o.Flags),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (o *OpenVPN) mergeWith(other OpenVPN) {
o.Version = helpers.MergeWithString(o.Version, other.Version)
o.User = helpers.MergeWithString(o.User, other.User)
o.Password = helpers.MergeWithString(o.Password, other.Password)
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
o.Ciphers = helpers.MergeStringSlices(o.Ciphers, other.Ciphers)
o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth)
o.ClientCrt = helpers.MergeWithStringPtr(o.ClientCrt, other.ClientCrt)
o.ClientKey = helpers.MergeWithStringPtr(o.ClientKey, other.ClientKey)
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
o.IPv6 = helpers.MergeWithBool(o.IPv6, other.IPv6)
o.MSSFix = helpers.MergeWithUint16(o.MSSFix, other.MSSFix)
o.Interface = helpers.MergeWithString(o.Interface, other.Interface)
o.ProcessUser = helpers.MergeWithString(o.ProcessUser, other.ProcessUser)
o.Verbosity = helpers.MergeWithIntPtr(o.Verbosity, other.Verbosity)
o.Flags = helpers.MergeStringSlices(o.Flags, other.Flags)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (o *OpenVPN) overrideWith(other OpenVPN) {
o.Version = helpers.OverrideWithString(o.Version, other.Version)
o.User = helpers.OverrideWithString(o.User, other.User)
o.Password = helpers.OverrideWithString(o.Password, other.Password)
o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile)
o.Ciphers = helpers.OverrideWithStringSlice(o.Ciphers, other.Ciphers)
o.Auth = helpers.OverrideWithStringPtr(o.Auth, other.Auth)
o.ClientCrt = helpers.OverrideWithStringPtr(o.ClientCrt, other.ClientCrt)
o.ClientKey = helpers.OverrideWithStringPtr(o.ClientKey, other.ClientKey)
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
o.IPv6 = helpers.OverrideWithBool(o.IPv6, other.IPv6)
o.MSSFix = helpers.OverrideWithUint16(o.MSSFix, other.MSSFix)
o.Interface = helpers.OverrideWithString(o.Interface, other.Interface)
o.ProcessUser = helpers.OverrideWithString(o.ProcessUser, other.ProcessUser)
o.Verbosity = helpers.OverrideWithIntPtr(o.Verbosity, other.Verbosity)
o.Flags = helpers.OverrideWithStringSlice(o.Flags, other.Flags)
}
func (o *OpenVPN) setDefaults(vpnProvider string) {
o.Version = helpers.DefaultString(o.Version, openvpn.Openvpn25)
if vpnProvider == providers.Mullvad {
o.Password = "m"
}
o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "")
o.Auth = helpers.DefaultStringPtr(o.Auth, "")
o.ClientCrt = helpers.DefaultStringPtr(o.ClientCrt, "")
o.ClientKey = helpers.DefaultStringPtr(o.ClientKey, "")
var defaultEncPreset string
if vpnProvider == providers.PrivateInternetAccess {
defaultEncPreset = presets.Strong
}
o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset)
o.IPv6 = helpers.DefaultBool(o.IPv6, false)
o.MSSFix = helpers.DefaultUint16(o.MSSFix, 0)
o.Interface = helpers.DefaultString(o.Interface, "tun0")
o.ProcessUser = helpers.DefaultString(o.ProcessUser, "root")
o.Verbosity = helpers.DefaultInt(o.Verbosity, 1)
}
func (o OpenVPN) String() string {
return o.toLinesNode().String()
}
func (o OpenVPN) toLinesNode() (node *gotree.Node) {
node = gotree.New("OpenVPN settings:")
node.Appendf("OpenVPN version: %s", o.Version)
node.Appendf("User: %s", helpers.ObfuscatePassword(o.User))
node.Appendf("Password: %s", helpers.ObfuscatePassword(o.Password))
if *o.ConfFile != "" {
node.Appendf("Custom configuration file: %s", *o.ConfFile)
}
if len(o.Ciphers) > 0 {
node.Appendf("Ciphers: %s", o.Ciphers)
}
if *o.Auth != "" {
node.Appendf("Auth: %s", *o.Auth)
}
if *o.ClientCrt != "" {
node.Appendf("Client crt: %s", helpers.ObfuscateData(*o.ClientCrt))
}
if *o.ClientKey != "" {
node.Appendf("Client key: %s", helpers.ObfuscateData(*o.ClientKey))
}
if *o.PIAEncPreset != "" {
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
}
node.Appendf("Tunnel IPv6: %s", helpers.BoolPtrToYesNo(o.IPv6))
if *o.MSSFix > 0 {
node.Appendf("MSS Fix: %d", *o.MSSFix)
}
if o.Interface != "" {
node.Appendf("Network interface: %s", o.Interface)
}
node.Appendf("Run OpenVPN as: %s", o.ProcessUser)
node.Appendf("Verbosity level: %d", *o.Verbosity)
if len(o.Flags) > 0 {
node.Appendf("Flags: %s", o.Flags)
}
return node
}
// WithDefaults is a shorthand using setDefaults.
// It's used in unit tests in other packages.
func (o OpenVPN) WithDefaults(provider string) OpenVPN {
o.setDefaults(provider)
return o
}

View File

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

View File

@@ -0,0 +1,178 @@
package settings
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gotree"
)
type OpenVPNSelection struct {
// ConfFile is the custom configuration file path.
// It can be set to an empty string to indicate to
// NOT use a custom configuration file.
// It cannot be nil in the internal state.
ConfFile *string
// TCP is true if the OpenVPN protocol is TCP,
// and false for UDP.
// It cannot be nil in the internal state.
TCP *bool
// CustomPort is the OpenVPN server endpoint port.
// It can be set to 0 to indicate no custom port should
// be used. It cannot be nil in the internal state.
CustomPort *uint16 // HideMyAss, Mullvad, PIA, ProtonVPN, WeVPN, Windscribe
// PIAEncPreset is the encryption preset for
// Private Internet Access. It can be set to an
// empty string for other providers.
PIAEncPreset *string
}
func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
// Validate ConfFile
if confFile := *o.ConfFile; confFile != "" {
err := helpers.FileExists(confFile)
if err != nil {
return fmt.Errorf("configuration file: %w", err)
}
}
// Validate TCP
if *o.TCP && helpers.IsOneOf(vpnProvider,
providers.Ipvanish,
providers.Perfectprivacy,
providers.Privado,
providers.VPNUnlimited,
providers.Vyprvpn,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNTCPNotSupported, vpnProvider)
}
// Validate CustomPort
if *o.CustomPort != 0 {
switch vpnProvider {
// no restriction on port
case providers.Cyberghost, providers.HideMyAss,
providers.Privatevpn, providers.Torguard:
// no custom port allowed
case providers.Expressvpn, providers.Fastestvpn,
providers.Ipvanish, providers.Nordvpn,
providers.Privado, providers.Purevpn,
providers.Surfshark, providers.VPNUnlimited,
providers.Vyprvpn:
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
default:
var allowedTCP, allowedUDP []uint16
switch vpnProvider {
case providers.Ivpn:
allowedTCP = []uint16{80, 443, 1143}
allowedUDP = []uint16{53, 1194, 2049, 2050}
case providers.Mullvad:
allowedTCP = []uint16{80, 443, 1401}
allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}
case providers.Perfectprivacy:
allowedTCP = []uint16{44, 443, 4433}
allowedUDP = []uint16{44, 443, 4433}
case providers.PrivateInternetAccess:
allowedTCP = []uint16{80, 110, 443}
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
case providers.Protonvpn:
allowedTCP = []uint16{443, 5995, 8443}
allowedUDP = []uint16{80, 443, 1194, 4569, 5060}
case providers.Wevpn:
allowedTCP = []uint16{53, 1195, 1199, 2018}
allowedUDP = []uint16{80, 1194, 1198}
case providers.Windscribe:
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
}
if *o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedTCP) {
return fmt.Errorf("%w: %d for VPN service provider %s; %s",
ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider,
helpers.PortChoicesOrString(allowedTCP))
} else if !*o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedUDP) {
return fmt.Errorf("%w: %d for VPN service provider %s; %s",
ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider,
helpers.PortChoicesOrString(allowedUDP))
}
}
}
// Validate EncPreset
if vpnProvider == providers.PrivateInternetAccess {
validEncryptionPresets := []string{
presets.None,
presets.Normal,
presets.Strong,
}
if !helpers.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...) {
return fmt.Errorf("%w: %s; valid presets are %s",
ErrOpenVPNEncryptionPresetNotValid, *o.PIAEncPreset,
helpers.ChoicesOrString(validEncryptionPresets))
}
}
return nil
}
func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
return OpenVPNSelection{
ConfFile: helpers.CopyStringPtr(o.ConfFile),
TCP: helpers.CopyBoolPtr(o.TCP),
CustomPort: helpers.CopyUint16Ptr(o.CustomPort),
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
}
}
func (o *OpenVPNSelection) mergeWith(other OpenVPNSelection) {
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
o.TCP = helpers.MergeWithBool(o.TCP, other.TCP)
o.CustomPort = helpers.MergeWithUint16(o.CustomPort, other.CustomPort)
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
}
func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile)
o.TCP = helpers.OverrideWithBool(o.TCP, other.TCP)
o.CustomPort = helpers.OverrideWithUint16(o.CustomPort, other.CustomPort)
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
}
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "")
o.TCP = helpers.DefaultBool(o.TCP, false)
o.CustomPort = helpers.DefaultUint16(o.CustomPort, 0)
var defaultEncPreset string
if vpnProvider == providers.PrivateInternetAccess {
defaultEncPreset = presets.Strong
}
o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset)
}
func (o OpenVPNSelection) String() string {
return o.toLinesNode().String()
}
func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("OpenVPN server selection settings:")
node.Appendf("Protocol: %s", helpers.TCPPtrToString(o.TCP))
if *o.CustomPort != 0 {
node.Appendf("Custom port: %d", *o.CustomPort)
}
if *o.PIAEncPreset != "" {
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
}
if *o.ConfFile != "" {
node.Appendf("Custom configuration file: %s", *o.ConfFile)
}
return node
}

View File

@@ -0,0 +1,89 @@
package settings
import (
"fmt"
"path/filepath"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gotree"
)
// PortForwarding contains settings for port forwarding.
type PortForwarding struct {
// Enabled is true if port forwarding should be activated.
// It cannot be nil for the internal state.
Enabled *bool
// Filepath is the port forwarding status file path
// to use. It can be the empty string to indicate not
// to write to a file. It cannot be nil for the
// internal state
Filepath *string
}
func (p PortForwarding) validate(vpnProvider string) (err error) {
if !*p.Enabled {
return nil
}
// Validate Enabled
validProviders := []string{providers.PrivateInternetAccess}
if !helpers.IsOneOf(vpnProvider, validProviders...) {
return fmt.Errorf("%w: for provider %s, it is only available for %s",
ErrPortForwardingEnabled, vpnProvider, strings.Join(validProviders, ", "))
}
// Validate Filepath
if *p.Filepath != "" { // optional
_, err := filepath.Abs(*p.Filepath)
if err != nil {
return fmt.Errorf("filepath is not valid: %w", err)
}
}
return nil
}
func (p *PortForwarding) copy() (copied PortForwarding) {
return PortForwarding{
Enabled: helpers.CopyBoolPtr(p.Enabled),
Filepath: helpers.CopyStringPtr(p.Filepath),
}
}
func (p *PortForwarding) mergeWith(other PortForwarding) {
p.Enabled = helpers.MergeWithBool(p.Enabled, other.Enabled)
p.Filepath = helpers.MergeWithStringPtr(p.Filepath, other.Filepath)
}
func (p *PortForwarding) overrideWith(other PortForwarding) {
p.Enabled = helpers.OverrideWithBool(p.Enabled, other.Enabled)
p.Filepath = helpers.OverrideWithStringPtr(p.Filepath, other.Filepath)
}
func (p *PortForwarding) setDefaults() {
p.Enabled = helpers.DefaultBool(p.Enabled, false)
p.Filepath = helpers.DefaultStringPtr(p.Filepath, "/tmp/gluetun/forwarded_port")
}
func (p PortForwarding) String() string {
return p.toLinesNode().String()
}
func (p PortForwarding) toLinesNode() (node *gotree.Node) {
if !*p.Enabled {
return nil
}
node = gotree.New("Automatic port forwarding settings:")
node.Appendf("Enabled: yes")
filepath := *p.Filepath
if filepath == "" {
filepath = "[not set]"
}
node.Appendf("Forwarded port file path: %s", filepath)
return node
}

View File

@@ -0,0 +1,19 @@
package settings
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_PortForwarding_String(t *testing.T) {
t.Parallel()
settings := PortForwarding{
Enabled: boolPtr(false),
}
s := settings.String()
assert.Empty(t, s)
}

View File

@@ -0,0 +1,93 @@
package settings
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gotree"
)
// Provider contains settings specific to a VPN provider.
type Provider struct {
// Name is the VPN service provider name.
// It cannot be nil in the internal state.
Name *string
// ServerSelection is the settings to
// select the VPN server.
ServerSelection ServerSelection
// PortForwarding is the settings about port forwarding.
PortForwarding PortForwarding
}
// TODO v4 remove pointer for receiver (because of Surfshark).
func (p *Provider) validate(vpnType string, storage Storage) (err error) {
// Validate Name
var validNames []string
if vpnType == vpn.OpenVPN {
validNames = providers.AllWithCustom()
validNames = append(validNames, "pia") // Retro-compatibility
} else { // Wireguard
validNames = []string{
providers.Custom,
providers.Ivpn,
providers.Mullvad,
providers.Windscribe,
}
}
if !helpers.IsOneOf(*p.Name, validNames...) {
return fmt.Errorf("%w: %q can only be one of %s",
ErrVPNProviderNameNotValid, *p.Name, helpers.ChoicesOrString(validNames))
}
err = p.ServerSelection.validate(*p.Name, storage)
if err != nil {
return fmt.Errorf("server selection: %w", err)
}
err = p.PortForwarding.validate(*p.Name)
if err != nil {
return fmt.Errorf("port forwarding: %w", err)
}
return nil
}
func (p *Provider) copy() (copied Provider) {
return Provider{
Name: helpers.CopyStringPtr(p.Name),
ServerSelection: p.ServerSelection.copy(),
PortForwarding: p.PortForwarding.copy(),
}
}
func (p *Provider) mergeWith(other Provider) {
p.Name = helpers.MergeWithStringPtr(p.Name, other.Name)
p.ServerSelection.mergeWith(other.ServerSelection)
p.PortForwarding.mergeWith(other.PortForwarding)
}
func (p *Provider) overrideWith(other Provider) {
p.Name = helpers.OverrideWithStringPtr(p.Name, other.Name)
p.ServerSelection.overrideWith(other.ServerSelection)
p.PortForwarding.overrideWith(other.PortForwarding)
}
func (p *Provider) setDefaults() {
p.Name = helpers.DefaultStringPtr(p.Name, providers.PrivateInternetAccess)
p.ServerSelection.setDefaults(*p.Name)
p.PortForwarding.setDefaults()
}
func (p Provider) String() string {
return p.toLinesNode().String()
}
func (p Provider) toLinesNode() (node *gotree.Node) {
node = gotree.New("VPN provider settings:")
node.Appendf("Name: %s", *p.Name)
node.AppendNode(p.ServerSelection.toLinesNode())
node.AppendNode(p.PortForwarding.toLinesNode())
return node
}

View File

@@ -0,0 +1,89 @@
package settings
import (
"fmt"
"path/filepath"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// PublicIP contains settings for port forwarding.
type PublicIP struct {
// Period is the period to get the public IP address.
// It can be set to 0 to disable periodic checking.
// It cannot be nil for the internal state.
// TODO change to value and add enabled field
Period *time.Duration
// IPFilepath is the public IP address status file path
// to use. It can be the empty string to indicate not
// to write to a file. It cannot be nil for the
// internal state
IPFilepath *string
}
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
_, err := filepath.Abs(*p.IPFilepath)
if err != nil {
return fmt.Errorf("filepath is not valid: %w", err)
}
}
return nil
}
func (p *PublicIP) copy() (copied PublicIP) {
return PublicIP{
Period: helpers.CopyDurationPtr(p.Period),
IPFilepath: helpers.CopyStringPtr(p.IPFilepath),
}
}
func (p *PublicIP) mergeWith(other PublicIP) {
p.Period = helpers.MergeWithDuration(p.Period, other.Period)
p.IPFilepath = helpers.MergeWithStringPtr(p.IPFilepath, other.IPFilepath)
}
func (p *PublicIP) overrideWith(other PublicIP) {
p.Period = helpers.OverrideWithDuration(p.Period, other.Period)
p.IPFilepath = helpers.OverrideWithStringPtr(p.IPFilepath, other.IPFilepath)
}
func (p *PublicIP) setDefaults() {
const defaultPeriod = 12 * time.Hour
p.Period = helpers.DefaultDuration(p.Period, defaultPeriod)
p.IPFilepath = helpers.DefaultStringPtr(p.IPFilepath, "/tmp/gluetun/ip")
}
func (p PublicIP) String() string {
return p.toLinesNode().String()
}
func (p PublicIP) toLinesNode() (node *gotree.Node) {
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 != "" {
node.Appendf("IP file path: %s", *p.IPFilepath)
}
return node
}

View File

@@ -0,0 +1,80 @@
package settings
import (
"fmt"
"net"
"os"
"strconv"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// ControlServer contains settings to customize the control server operation.
type ControlServer struct {
// Address is the listening address to use.
// It cannot be nil in the internal state.
Address *string
// Log can be true or false to enable logging on requests.
// It cannot be nil in the internal state.
Log *bool
}
func (c ControlServer) validate() (err error) {
_, portStr, err := net.SplitHostPort(*c.Address)
if err != nil {
return fmt.Errorf("listening address is not valid: %w", err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
return fmt.Errorf("listening port it not valid: %w", err)
}
uid := os.Getuid()
const maxPrivilegedPort = 1023
if uid != 0 && port != 0 && port <= maxPrivilegedPort {
return fmt.Errorf("%w: %d when running with user ID %d",
ErrControlServerPrivilegedPort, port, uid)
}
return nil
}
func (c *ControlServer) copy() (copied ControlServer) {
return ControlServer{
Address: helpers.CopyStringPtr(c.Address),
Log: helpers.CopyBoolPtr(c.Log),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (c *ControlServer) mergeWith(other ControlServer) {
c.Address = helpers.MergeWithStringPtr(c.Address, other.Address)
c.Log = helpers.MergeWithBool(c.Log, other.Log)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (c *ControlServer) overrideWith(other ControlServer) {
c.Address = helpers.OverrideWithStringPtr(c.Address, other.Address)
c.Log = helpers.OverrideWithBool(c.Log, other.Log)
}
func (c *ControlServer) setDefaults() {
c.Address = helpers.DefaultStringPtr(c.Address, ":8000")
c.Log = helpers.DefaultBool(c.Log, true)
}
func (c ControlServer) String() string {
return c.toLinesNode().String()
}
func (c ControlServer) toLinesNode() (node *gotree.Node) {
node = gotree.New("Control server settings:")
node.Appendf("Listening address: %s", *c.Address)
node.Appendf("Logging: %s", helpers.BoolPtrToYesNo(c.Log))
return node
}

View File

@@ -0,0 +1,324 @@
package settings
import (
"errors"
"fmt"
"net"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/configuration/settings/validation"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gotree"
)
type ServerSelection struct { //nolint:maligned
// VPN is the VPN type which can be 'openvpn'
// or 'wireguard'. It cannot be the empty string
// in the internal state.
VPN string
// TargetIP is the server endpoint IP address to use.
// It will override any IP address from the picked
// built-in server. It cannot be nil in the internal
// state, and can be set to an empty net.IP{} to indicate
// there is not target IP address to use.
TargetIP net.IP
// Counties is the list of countries to filter VPN servers with.
Countries []string
// Regions is the list of regions to filter VPN servers with.
Regions []string
// Cities is the list of cities to filter VPN servers with.
Cities []string
// ISPs is the list of ISP names to filter VPN servers with.
ISPs []string
// Names is the list of server names to filter VPN servers with.
Names []string
// Numbers is the list of server numbers to filter VPN servers with.
Numbers []uint16
// Hostnames is the list of hostnames to filter VPN servers with.
Hostnames []string
// OwnedOnly is true if VPN provider servers that are not owned
// should be filtered. This is used with Mullvad.
OwnedOnly *bool
// FreeOnly is true if VPN servers that are not free should
// be filtered. This is used with ProtonVPN and VPN Unlimited.
FreeOnly *bool
// StreamOnly is true if VPN servers not for streaming should
// be filtered. This is used with VPNUnlimited.
StreamOnly *bool
// MultiHopOnly is true if VPN servers that are not multihop
// should be filtered. This is used with Surfshark.
MultiHopOnly *bool
// OpenVPN contains settings to select OpenVPN servers
// and the final connection.
OpenVPN OpenVPNSelection
// Wireguard contains settings to select Wireguard servers
// and the final connection.
Wireguard WireguardSelection
}
var (
ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported")
ErrFreeOnlyNotSupported = errors.New("free only filter is not supported")
ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported")
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
)
func (ss *ServerSelection) validate(vpnServiceProvider string,
storage Storage) (err error) {
switch ss.VPN {
case vpn.OpenVPN, vpn.Wireguard:
default:
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
}
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, storage)
if err != nil {
return err // already wrapped error
}
err = validateServerFilters(*ss, filterChoices)
if err != nil {
if errors.Is(err, helpers.ErrNoChoice) {
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
}
return err // already wrapped error
}
if *ss.OwnedOnly &&
vpnServiceProvider != providers.Mullvad {
return fmt.Errorf("%w: for VPN service provider %s",
ErrOwnedOnlyNotSupported, vpnServiceProvider)
}
if *ss.FreeOnly &&
!helpers.IsOneOf(vpnServiceProvider,
providers.Protonvpn,
providers.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrFreeOnlyNotSupported, vpnServiceProvider)
}
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.VPN == vpn.OpenVPN {
err = ss.OpenVPN.validate(vpnServiceProvider)
if err != nil {
return fmt.Errorf("OpenVPN server selection settings: %w", err)
}
} else {
err = ss.Wireguard.validate(vpnServiceProvider)
if err != nil {
return fmt.Errorf("Wireguard server selection settings: %w", err)
}
}
return nil
}
func getLocationFilterChoices(vpnServiceProvider string,
ss *ServerSelection, storage Storage) (filterChoices models.FilterChoices,
err error) {
filterChoices = storage.GetFilterChoices(vpnServiceProvider)
if vpnServiceProvider == providers.Surfshark {
// // Retro compatibility
// TODO v4 remove
filterChoices.Regions = append(filterChoices.Regions, validation.SurfsharkRetroLocChoices()...)
if err := helpers.AreAllOneOf(ss.Regions, filterChoices.Regions); err != nil {
return models.FilterChoices{}, fmt.Errorf("%w: %s", ErrRegionNotValid, err)
}
*ss = surfsharkRetroRegion(*ss)
}
return filterChoices, nil
}
// validateServerFilters validates filters against the choices given as arguments.
// Set an argument to nil to pass the check for a particular filter.
func validateServerFilters(settings ServerSelection, filterChoices models.FilterChoices) (err error) {
if err := helpers.AreAllOneOf(settings.Countries, filterChoices.Countries); err != nil {
return fmt.Errorf("%w: %s", ErrCountryNotValid, err)
}
if err := helpers.AreAllOneOf(settings.Regions, filterChoices.Regions); err != nil {
return fmt.Errorf("%w: %s", ErrRegionNotValid, err)
}
if err := helpers.AreAllOneOf(settings.Cities, filterChoices.Cities); err != nil {
return fmt.Errorf("%w: %s", ErrCityNotValid, err)
}
if err := helpers.AreAllOneOf(settings.ISPs, filterChoices.ISPs); err != nil {
return fmt.Errorf("%w: %s", ErrISPNotValid, err)
}
if err := helpers.AreAllOneOf(settings.Hostnames, filterChoices.Hostnames); err != nil {
return fmt.Errorf("%w: %s", ErrHostnameNotValid, err)
}
if err := helpers.AreAllOneOf(settings.Names, filterChoices.Names); err != nil {
return fmt.Errorf("%w: %s", ErrNameNotValid, err)
}
return nil
}
func (ss *ServerSelection) copy() (copied ServerSelection) {
return ServerSelection{
VPN: ss.VPN,
TargetIP: helpers.CopyIP(ss.TargetIP),
Countries: helpers.CopyStringSlice(ss.Countries),
Regions: helpers.CopyStringSlice(ss.Regions),
Cities: helpers.CopyStringSlice(ss.Cities),
ISPs: helpers.CopyStringSlice(ss.ISPs),
Hostnames: helpers.CopyStringSlice(ss.Hostnames),
Names: helpers.CopyStringSlice(ss.Names),
Numbers: helpers.CopyUint16Slice(ss.Numbers),
OwnedOnly: helpers.CopyBoolPtr(ss.OwnedOnly),
FreeOnly: helpers.CopyBoolPtr(ss.FreeOnly),
StreamOnly: helpers.CopyBoolPtr(ss.StreamOnly),
MultiHopOnly: helpers.CopyBoolPtr(ss.MultiHopOnly),
OpenVPN: ss.OpenVPN.copy(),
Wireguard: ss.Wireguard.copy(),
}
}
func (ss *ServerSelection) mergeWith(other ServerSelection) {
ss.VPN = helpers.MergeWithString(ss.VPN, other.VPN)
ss.TargetIP = helpers.MergeWithIP(ss.TargetIP, other.TargetIP)
ss.Countries = helpers.MergeStringSlices(ss.Countries, other.Countries)
ss.Regions = helpers.MergeStringSlices(ss.Regions, other.Regions)
ss.Cities = helpers.MergeStringSlices(ss.Cities, other.Cities)
ss.ISPs = helpers.MergeStringSlices(ss.ISPs, other.ISPs)
ss.Hostnames = helpers.MergeStringSlices(ss.Hostnames, other.Hostnames)
ss.Names = helpers.MergeStringSlices(ss.Names, other.Names)
ss.Numbers = helpers.MergeUint16Slices(ss.Numbers, other.Numbers)
ss.OwnedOnly = helpers.MergeWithBool(ss.OwnedOnly, other.OwnedOnly)
ss.FreeOnly = helpers.MergeWithBool(ss.FreeOnly, other.FreeOnly)
ss.StreamOnly = helpers.MergeWithBool(ss.StreamOnly, other.StreamOnly)
ss.MultiHopOnly = helpers.MergeWithBool(ss.MultiHopOnly, other.MultiHopOnly)
ss.OpenVPN.mergeWith(other.OpenVPN)
ss.Wireguard.mergeWith(other.Wireguard)
}
func (ss *ServerSelection) overrideWith(other ServerSelection) {
ss.VPN = helpers.OverrideWithString(ss.VPN, other.VPN)
ss.TargetIP = helpers.OverrideWithIP(ss.TargetIP, other.TargetIP)
ss.Countries = helpers.OverrideWithStringSlice(ss.Countries, other.Countries)
ss.Regions = helpers.OverrideWithStringSlice(ss.Regions, other.Regions)
ss.Cities = helpers.OverrideWithStringSlice(ss.Cities, other.Cities)
ss.ISPs = helpers.OverrideWithStringSlice(ss.ISPs, other.ISPs)
ss.Hostnames = helpers.OverrideWithStringSlice(ss.Hostnames, other.Hostnames)
ss.Names = helpers.OverrideWithStringSlice(ss.Names, other.Names)
ss.Numbers = helpers.OverrideWithUint16Slice(ss.Numbers, other.Numbers)
ss.OwnedOnly = helpers.OverrideWithBool(ss.OwnedOnly, other.OwnedOnly)
ss.FreeOnly = helpers.OverrideWithBool(ss.FreeOnly, other.FreeOnly)
ss.StreamOnly = helpers.OverrideWithBool(ss.StreamOnly, other.StreamOnly)
ss.MultiHopOnly = helpers.OverrideWithBool(ss.MultiHopOnly, other.MultiHopOnly)
ss.OpenVPN.overrideWith(other.OpenVPN)
ss.Wireguard.overrideWith(other.Wireguard)
}
func (ss *ServerSelection) setDefaults(vpnProvider string) {
ss.VPN = helpers.DefaultString(ss.VPN, vpn.OpenVPN)
ss.TargetIP = helpers.DefaultIP(ss.TargetIP, net.IP{})
ss.OwnedOnly = helpers.DefaultBool(ss.OwnedOnly, false)
ss.FreeOnly = helpers.DefaultBool(ss.FreeOnly, false)
ss.StreamOnly = helpers.DefaultBool(ss.StreamOnly, false)
ss.MultiHopOnly = helpers.DefaultBool(ss.MultiHopOnly, false)
ss.OpenVPN.setDefaults(vpnProvider)
ss.Wireguard.setDefaults()
}
func (ss ServerSelection) String() string {
return ss.toLinesNode().String()
}
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("Server selection settings:")
node.Appendf("VPN type: %s", ss.VPN)
if len(ss.TargetIP) > 0 {
node.Appendf("Target IP address: %s", ss.TargetIP)
}
if len(ss.Countries) > 0 {
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
}
if len(ss.Regions) > 0 {
node.Appendf("Regions: %s", strings.Join(ss.Regions, ", "))
}
if len(ss.Cities) > 0 {
node.Appendf("Cities: %s", strings.Join(ss.Cities, ", "))
}
if len(ss.ISPs) > 0 {
node.Appendf("ISPs: %s", strings.Join(ss.ISPs, ", "))
}
if len(ss.Names) > 0 {
node.Appendf("Server names: %s", strings.Join(ss.Names, ", "))
}
if len(ss.Numbers) > 0 {
numbersNode := node.Appendf("Server numbers:")
for _, number := range ss.Numbers {
numbersNode.Appendf("%d", number)
}
}
if len(ss.Hostnames) > 0 {
node.Appendf("Hostnames: %s", strings.Join(ss.Hostnames, ", "))
}
if *ss.OwnedOnly {
node.Appendf("Owned only servers: yes")
}
if *ss.FreeOnly {
node.Appendf("Free only servers: yes")
}
if *ss.StreamOnly {
node.Appendf("Stream only servers: yes")
}
if *ss.MultiHopOnly {
node.Appendf("Multi-hop only servers: yes")
}
if ss.VPN == vpn.OpenVPN {
node.AppendNode(ss.OpenVPN.toLinesNode())
} else {
node.AppendNode(ss.Wireguard.toLinesNode())
}
return node
}
// WithDefaults is a shorthand using setDefaults.
// It's used in unit tests in other packages.
func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
ss.setDefaults(provider)
return ss
}

View File

@@ -0,0 +1,159 @@
package settings
import (
"fmt"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gotree"
)
type Settings struct {
ControlServer ControlServer
DNS DNS
Firewall Firewall
Health Health
HTTPProxy HTTPProxy
Log Log
PublicIP PublicIP
Shadowsocks Shadowsocks
System System
Updater Updater
Version Version
VPN VPN
Pprof pprof.Settings
}
type Storage interface {
GetFilterChoices(provider string) models.FilterChoices
}
// Validate validates all the settings and returns an error
// if one of them is not valid.
// TODO v4 remove pointer for receiver (because of Surfshark).
func (s *Settings) Validate(storage Storage) (err error) {
nameToValidation := map[string]func() error{
"control server": s.ControlServer.validate,
"dns": s.DNS.validate,
"firewall": s.Firewall.validate,
"health": s.Health.Validate,
"http proxy": s.HTTPProxy.validate,
"log": s.Log.validate,
"public ip check": s.PublicIP.validate,
"shadowsocks": s.Shadowsocks.validate,
"system": s.System.validate,
"updater": s.Updater.Validate,
"version": s.Version.validate,
// Pprof validation done in pprof constructor
"VPN": func() error {
return s.VPN.validate(storage)
},
}
for name, validation := range nameToValidation {
err = validation()
if err != nil {
return fmt.Errorf("%s settings: %w", name, err)
}
}
return nil
}
func (s *Settings) copy() (copied Settings) {
return Settings{
ControlServer: s.ControlServer.copy(),
DNS: s.DNS.Copy(),
Firewall: s.Firewall.copy(),
Health: s.Health.copy(),
HTTPProxy: s.HTTPProxy.copy(),
Log: s.Log.copy(),
PublicIP: s.PublicIP.copy(),
Shadowsocks: s.Shadowsocks.copy(),
System: s.System.copy(),
Updater: s.Updater.copy(),
Version: s.Version.copy(),
VPN: s.VPN.copy(),
Pprof: s.Pprof.Copy(),
}
}
func (s *Settings) MergeWith(other Settings) {
s.ControlServer.mergeWith(other.ControlServer)
s.DNS.mergeWith(other.DNS)
s.Firewall.mergeWith(other.Firewall)
s.Health.MergeWith(other.Health)
s.HTTPProxy.mergeWith(other.HTTPProxy)
s.Log.mergeWith(other.Log)
s.PublicIP.mergeWith(other.PublicIP)
s.Shadowsocks.mergeWith(other.Shadowsocks)
s.System.mergeWith(other.System)
s.Updater.mergeWith(other.Updater)
s.Version.mergeWith(other.Version)
s.VPN.mergeWith(other.VPN)
s.Pprof.MergeWith(other.Pprof)
}
func (s *Settings) OverrideWith(other Settings,
storage Storage) (err error) {
patchedSettings := s.copy()
patchedSettings.ControlServer.overrideWith(other.ControlServer)
patchedSettings.DNS.overrideWith(other.DNS)
patchedSettings.Firewall.overrideWith(other.Firewall)
patchedSettings.Health.OverrideWith(other.Health)
patchedSettings.HTTPProxy.overrideWith(other.HTTPProxy)
patchedSettings.Log.overrideWith(other.Log)
patchedSettings.PublicIP.overrideWith(other.PublicIP)
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
patchedSettings.System.overrideWith(other.System)
patchedSettings.Updater.overrideWith(other.Updater)
patchedSettings.Version.overrideWith(other.Version)
patchedSettings.VPN.overrideWith(other.VPN)
patchedSettings.Pprof.OverrideWith(other.Pprof)
err = patchedSettings.Validate(storage)
if err != nil {
return err
}
*s = patchedSettings
return nil
}
func (s *Settings) SetDefaults() {
s.ControlServer.setDefaults()
s.DNS.setDefaults()
s.Firewall.setDefaults()
s.Health.SetDefaults()
s.HTTPProxy.setDefaults()
s.Log.setDefaults()
s.PublicIP.setDefaults()
s.Shadowsocks.setDefaults()
s.System.setDefaults()
s.Version.setDefaults()
s.VPN.setDefaults()
s.Updater.SetDefaults(*s.VPN.Provider.Name)
s.Pprof.SetDefaults()
}
func (s Settings) String() string {
return s.toLinesNode().String()
}
func (s Settings) toLinesNode() (node *gotree.Node) {
node = gotree.New("Settings summary:")
node.AppendNode(s.VPN.toLinesNode())
node.AppendNode(s.DNS.toLinesNode())
node.AppendNode(s.Firewall.toLinesNode())
node.AppendNode(s.Log.toLinesNode())
node.AppendNode(s.Health.toLinesNode())
node.AppendNode(s.Shadowsocks.toLinesNode())
node.AppendNode(s.HTTPProxy.toLinesNode())
node.AppendNode(s.ControlServer.toLinesNode())
node.AppendNode(s.System.toLinesNode())
node.AppendNode(s.PublicIP.toLinesNode())
node.AppendNode(s.Updater.toLinesNode())
node.AppendNode(s.Version.toLinesNode())
node.AppendNode(s.Pprof.ToLinesNode())
return node
}

View File

@@ -0,0 +1,101 @@
package settings
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Settings_String(t *testing.T) {
t.Parallel()
withDefaults := func(s Settings) Settings {
s.SetDefaults()
return s
}
testCases := map[string]struct {
settings Settings
s string
}{
"default settings": {
settings: withDefaults(Settings{}),
s: `Settings summary:
├── VPN settings:
| ├── VPN provider settings:
| | ├── Name: private internet access
| | └── Server selection settings:
| | ├── VPN type: openvpn
| | └── OpenVPN server selection settings:
| | ├── Protocol: UDP
| | └── Private Internet Access encryption preset: strong
| └── OpenVPN settings:
| ├── OpenVPN version: 2.5
| ├── User: [not set]
| ├── Password: [not set]
| ├── Private Internet Access encryption preset: strong
| ├── Tunnel IPv6: no
| ├── Network interface: tun0
| ├── Run OpenVPN as: root
| └── Verbosity level: 1
├── DNS settings:
| ├── DNS server address to use: 127.0.0.1
| ├── Keep existing nameserver(s): no
| └── DNS over TLS settings:
| ├── Enabled: yes
| ├── Update period: every 24h0m0s
| ├── Unbound settings:
| | ├── Authoritative servers:
| | | └── Cloudflare
| | ├── Caching: yes
| | ├── 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:
| ├── Block malicious: yes
| ├── Block ads: no
| └── Block surveillance: yes
├── Firewall settings:
| └── Enabled: yes
├── Log settings:
| └── Log level: INFO
├── Health settings:
| ├── Server listening address: 127.0.0.1:9999
| ├── Target address: cloudflare.com:443
| └── VPN wait durations:
| ├── Initial duration: 6s
| └── Additional duration: 5s
├── Shadowsocks server settings:
| └── Enabled: no
├── HTTP proxy settings:
| └── Enabled: no
├── Control server settings:
| ├── Listening address: :8000
| └── Logging: yes
├── OS Alpine settings:
| ├── Process UID: 1000
| └── Process GID: 1000
├── Public IP settings:
| ├── Fetching: every 12h0m0s
| └── IP file path: /tmp/gluetun/ip
└── Version settings:
└── Enabled: yes`,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
s := testCase.settings.String()
assert.Equal(t, testCase.s, s)
})
}
}

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