Compare commits

..

212 Commits

Author SHA1 Message Date
Quentin McGaw
c04cd621b0 fix(docker): openvpn 2.4.12-r0 install 2022-03-31 20:53:28 +00:00
Quentin McGaw
69c3f4e872 fix(env): OPENVPN_FLAGS functionality 2022-03-31 20:47:39 +00:00
Quentin McGaw
e1c20595b1 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-21 20:38:55 +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
Quentin McGaw
b9a9319cb4 fix(ci): trigger CI on published releases 2021-11-27 12:09:33 +00:00
Quentin McGaw
77e4317135 chore(dev): fix devcontainer post create command 2021-11-27 12:01:07 +00:00
dependabot[bot]
b10d97e53a Chore(deps): Bump github.com/breml/rootcerts from 0.1.1 to 0.2.0 (#722)
Bumps [github.com/breml/rootcerts](https://github.com/breml/rootcerts) from 0.1.1 to 0.2.0.
- [Release notes](https://github.com/breml/rootcerts/releases)
- [Commits](https://github.com/breml/rootcerts/compare/v0.1.1...v0.2.0)

---
updated-dependencies:
- dependency-name: github.com/breml/rootcerts
  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>
2021-11-27 06:58:32 -05:00
dependabot[bot]
648a4c04d7 Build(deps): Bump actions/checkout from 2.3.4 to 2.4.0 (#705)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.4 to 2.4.0.
- [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.3.4...v2.4.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  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>
2021-11-27 06:58:16 -05:00
Quentin McGaw
3ca674dca7 feat(windscribe): update server information 2021-11-18 22:00:05 +00:00
Quentin McGaw
fa97fd496e feat(torguard): update server information 2021-11-18 09:35:30 +00:00
Quentin McGaw
c76a7ee8da chore(dot): add error description for update files failure 2021-11-18 08:12:07 +00:00
Quentin McGaw
80f6b78332 chore(config): fix bad error wrapping 2021-11-17 22:32:33 +00:00
Quentin McGaw
8dc54a7c44 feat(privatevpn): support OPENVPN_PORT 2021-11-17 22:32:18 +00:00
Quentin McGaw
8f080c537b fix(privatevpn): openvpn configuration values 2021-11-17 22:26:18 +00:00
Quentin McGaw
427cf86f44 chore(ci) disable snyk analysis for false positive 2021-11-12 23:39:01 +00:00
Quentin McGaw
2d244c08e7 Fix: 2 low vulnerability busybox issues 2021-11-12 23:04:42 +00:00
Quentin McGaw
82c0f523aa fix: openvpn at /usr/sbin/openvpn2.5
- Fix operation on QNAP devices
- Refer to #157
2021-11-12 22:48:19 +00:00
Quentin McGaw
c07a0b0ada chore(lint): add bidichk, ifshort, nilnil and tenv 2021-11-08 22:41:20 +00:00
Quentin McGaw
e4c306c0ee chore(linter): update golangci-lint to v1.43.0 2021-11-07 21:26:31 +00:00
Quentin McGaw
6ffb94f819 fix(updater): cli error message 2021-11-07 21:25:10 +00:00
Quentin McGaw
142238e8b7 feat(protonvpn): update built-in servers data 2021-11-07 21:24:57 +00:00
Quentin McGaw (desktop)
678e23c7d6 Change: run OpenVPN as root to clean routes on exit 2021-11-01 22:51:03 +00:00
Quentin McGaw (desktop)
0abcebe1d8 Feat: update NordVPN server information 2021-11-01 22:48:36 +00:00
Quentin McGaw (desktop)
f398af1169 Fix: check github http status code for version 2021-10-29 01:37:45 +00:00
Quentin McGaw (desktop)
afbea415e3 Maint: replace with for markdown generated tables 2021-10-25 22:38:59 +00:00
Quentin McGaw (desktop)
225bd5d25b Fix: CI to use short commits 2021-10-21 13:46:50 +00:00
Quentin McGaw (desktop)
3651cc6161 Maint: CI image tags rework 2021-10-16 14:58:11 +00:00
Quentin McGaw (desktop)
dc674014ff Fix: vyprvpn: openvpn comp-lzo option 2021-10-14 19:55:48 +00:00
Quentin McGaw (desktop)
0e0e03949d Docs: add urgent and low priority labels 2021-10-14 16:37:09 +00:00
Quentin McGaw (desktop)
f5bf5c236a Hotfix: CI if condition 2021-10-14 16:32:43 +00:00
dependabot[bot]
94480ecabb Maint: bump docker/build-push-action from 2.6.1 to 2.7.0 (#664) 2021-10-14 09:31:08 -07:00
dependabot[bot]
31ef9b1d45 Maint: bump github.com/breml/rootcerts from 0.1.0 to 0.1.1 (#668) 2021-10-14 09:30:21 -07:00
Quentin McGaw (desktop)
bf76132fd4 Maint: fix dependabot CI trigger 2021-10-14 16:29:22 +00:00
Quentin McGaw (desktop)
8cc2983318 Fix: NordVPN: Re-add comp-lzo option 2021-10-13 00:04:51 +00:00
Quentin McGaw (desktop)
caeca18ed7 Hotfix: ci workflow 2021-10-06 18:54:44 +00:00
dependabot[bot]
50febb41ff Maint: bump docker/build-push-action from 2.6.1 to 2.7.0 (#583) 2021-10-06 11:47:06 -07:00
Quentin McGaw (desktop)
79293e067c Doc: fix readme Wiki links 2021-10-06 17:47:08 +00:00
Quentin McGaw (desktop)
f45be80591 Maint: CI changes
- Only trigger on push and PR to master
- Do not push images for branches
- Add fork only workflow
- Add dependabot only workflow
- Do not trigger ci workflow from forked/dependabot PRs
2021-10-06 14:23:01 +00:00
Quentin McGaw (desktop)
d405ba8dca Feat: run OpenVPN without root 2021-10-05 21:33:15 +00:00
Quentin McGaw (desktop)
ca975b1c01 Feat: multiple OpenVPN ciphers for negotiation
- Perfect privacy to accept AES-256-CBC and AES-256-GCM
- Cyberghost default cipher set to AES-256-GCM
- `OPENVPN_CIPHER` accept comma separated cipher values
- Use `ncp-ciphers` for OpenVPN 2.4
2021-10-05 20:36:23 +00:00
Quentin McGaw
e0e3ca3832 Feat: Perfect privacy support (#606) 2021-10-05 10:44:15 -07:00
Quentin McGaw (desktop)
e7c952cbf7 Maint: remove opendns.com due to bad x509 cert 2021-09-30 16:01:35 +00:00
Quentin McGaw (desktop)
85ad2dd39a Maint: simplify warning logging in http proxy 2021-09-30 16:01:02 +00:00
Quentin McGaw (desktop)
0c4f0ec17b Doc: add ref to image tags in bug issue template 2021-09-30 15:34:21 +00:00
Quentin McGaw (desktop)
5ad4136955 Maint: move splash at start of program 2021-09-30 15:28:24 +00:00
Quentin McGaw (desktop)
a432de95a9 Maint: deduplicate ProtonVPN servers by entry IP 2021-09-30 15:23:18 +00:00
Quentin McGaw (desktop)
1d25a0e18c Fix: server data version diff when reading file 2021-09-30 15:22:57 +00:00
Quentin McGaw (desktop)
29fd95685f Doc: add custom provider option for bug issue template 2021-09-29 20:41:38 +00:00
Quentin McGaw (desktop)
62a6016882 Fix: FastestVPN new OpenVPN config
- Add required `comp-lzo`
- Add `reneg-sec 0` to match their config
- Do not filter `auth-token`
- Set UDP options only when using UDP
2021-09-28 13:52:07 +00:00
Quentin McGaw (desktop)
18a4a79763 Fix: log errors as error for OpenVPN 2021-09-28 11:57:32 +00:00
Quentin McGaw (desktop)
56ea722f93 Doc: update wiki issue template to use yml format 2021-09-27 23:47:10 +00:00
Quentin McGaw (desktop)
d2ab974933 Doc: update feature request issue template to use yml format 2021-09-27 23:44:15 +00:00
Quentin McGaw (desktop)
37d7a8b5fe Doc: add Unraid template discussion link to issues 2021-09-27 23:40:22 +00:00
Quentin McGaw (desktop)
e4dcadd825 Doc: update bug issue template to use yml format 2021-09-27 23:37:40 +00:00
Quentin McGaw (desktop)
fee99e9fe3 Doc: add Github discussion link to issue config 2021-09-27 14:15:56 +00:00
Quentin McGaw (desktop)
8ac4826126 Doc: add Console Substack interview link 2021-09-27 13:24:31 +00:00
Quentin McGaw (desktop)
7deb12e06d Maint: use github.com/breml/rootcerts 2021-09-26 22:26:11 +00:00
Quentin McGaw (desktop)
d6e218141b Maint: Go program uses time/tzdata instead of OS 2021-09-26 22:25:28 +00:00
Quentin McGaw (desktop)
f44121b044 Maint: upgrade qdm12/goshutdown to v0.3.0 2021-09-26 22:23:52 +00:00
Quentin McGaw (desktop)
5d8d92462d Feat: update FastestVPN server information 2021-09-25 13:30:29 +00:00
Quentin McGaw
985cf7b7dd Feat: ExpressVPN support (#623) 2021-09-23 10:19:30 -07:00
dependabot[bot]
dcbc10fd57 Build(deps): Bump github.com/fatih/color from 1.12.0 to 1.13.0 (#635) 2021-09-23 10:08:31 -07:00
Quentin McGaw (desktop)
79f243e98d Maint: package local log levels 2021-09-23 17:06:09 +00:00
Quentin McGaw (desktop)
cf95692b93 Maint: package local narrow Logger interfaces 2021-09-23 17:06:09 +00:00
Quentin McGaw
d8e008606f Feat: WeVPN support (#591) 2021-09-23 07:58:13 -07:00
Quentin McGaw (desktop)
3cd26a9f61 Feat: debug log Wireguard keys 2021-09-23 14:42:28 +00:00
Quentin McGaw (desktop)
5d74320ee7 Maint: truncate servers format output file 2021-09-23 14:38:46 +00:00
Quentin McGaw (desktop)
f9aadeef1c Maint: Remove CYBERGHOST_GROUP (change)
- It does not make any sense with newer server data
- It was to be deprecated anyway
2021-09-23 13:54:24 +00:00
Quentin McGaw (desktop)
625de1c834 Maint: migrate Cyberghost REGION to COUNTRY 2021-09-23 13:28:32 +00:00
Quentin McGaw (desktop)
1c0a3ed1a4 Feat: update Cyberghost servers data 2021-09-23 13:26:21 +00:00
Quentin McGaw (desktop)
03ba9169f4 Feat: format-servers CLI command 2021-09-23 13:13:17 +00:00
Quentin McGaw (desktop)
c22e0e9db7 Fix: HideMyAss: Cote d'Ivoire server country 2021-09-21 23:56:53 +00:00
Quentin McGaw (desktop)
6bcbaf085d Maint: remove NordVPN SERVER_NAME filter
- Filter was not effective
- Is to be deprecated in v4 anyway
- Bump NordVPN server model version to `3`
- Remove `Name` field from NordVPN server model
2021-09-21 23:56:29 +00:00
Quentin McGaw (desktop)
9a1d9c5d74 Fix: PIA's SERVER_NAME variable 2021-09-21 22:31:50 +00:00
Quentin McGaw (desktop)
59a3a072e0 Feat: support IPv6 routing for Wireguard 2021-09-21 15:12:48 +00:00
Quentin McGaw (desktop)
9f001bbc06 Feat: log wireguard server endpoint 2021-09-18 19:12:27 +00:00
Quentin McGaw (desktop)
b8356b60a6 Maint: use OPENVPN_PORT instead of PORT
with retro-compatibility
2021-09-18 16:09:21 +00:00
Quentin McGaw (desktop)
e2e218c74b Feat: update Mullvad server information 2021-09-18 15:37:49 +00:00
Quentin McGaw (desktop)
3bf23cbae5 Maint: remove enabled by default linters 2021-09-17 18:05:07 +00:00
Quentin McGaw (desktop)
da562d8206 Doc: update maintenance document 2021-09-16 20:42:39 +00:00
Quentin McGaw (desktop)
81bf83db13 Maint: remove disable-occ from PIA (match Wiki) 2021-09-16 20:32:34 +00:00
Quentin McGaw (desktop)
7a25dcd130 Doc: remove duplicate docker-compose.yml 2021-09-16 20:32:04 +00:00
Quentin McGaw (desktop)
877c7e1a9f Doc: update readme with updated Wiki 2021-09-16 20:24:30 +00:00
Quentin McGaw (desktop)
77b2512745 Doc: add wiki issue template 2021-09-16 19:53:37 +00:00
Quentin McGaw (desktop)
749b73ef15 Doc: remove help issue template 2021-09-16 19:51:32 +00:00
Quentin McGaw (desktop)
e499eca12c Maint: remove assignees in Github issues 2021-09-16 19:51:14 +00:00
Quentin McGaw (desktop)
80f25c34e5 Hotfix: default cyberghost cipher to aes-128-gcm 2021-09-15 12:54:36 +00:00
Quentin McGaw (desktop)
61677fbce2 Maint: migrate PROTOCOL to OPENVPN_PROTOCOL 2021-09-14 19:27:13 +00:00
Quentin McGaw (desktop)
dc6171185e Maint: add more linters to golangci-lint 2021-09-14 19:26:46 +00:00
Quentin McGaw (desktop)
f7e4331e93 Fix: PureVPN remove AES-256-CBC cipher 2021-09-14 15:47:06 +00:00
Quentin McGaw (desktop)
1340511b64 Maint: re-order OpenVPN options 2021-09-14 15:46:40 +00:00
Quentin McGaw (desktop)
c3078f84e8 Maint: OpenVPN option: remove all ping-* options
- Use the built-in healthcheck vpn restart mechanism instead
- Restarting with `ping-restart` or `ping-exit` would only restart with the same `remote` connection
- Specify `ping` options as VPN specific to ensure the server doesn't disconnect us
2021-09-14 15:23:56 +00:00
Quentin McGaw (desktop)
9f65157a0d Maint: OpenVPN: add explicit-exit-notify for UDP 2021-09-14 15:13:40 +00:00
Quentin McGaw (desktop)
89166cdabf Maint: OpenVPN: PIA: add tls-exit option 2021-09-14 15:09:22 +00:00
Quentin McGaw (desktop)
b872973e8b Maint: OpenVPN option tls-client removed
- It's redundant with `client` option
- Affects FastestVPN
2021-09-14 15:04:50 +00:00
Quentin McGaw (desktop)
2000e72357 Maint: OpenVPN option: remove tun-mtu 1500
- Since it defaults to `1500`
- Affects FastestVPN
- Affects NordVPN
- Affects ProtonVPN
- Affects Surfshark
- Affects Torguard
2021-09-14 14:59:04 +00:00
Quentin McGaw (desktop)
836e53642d Maint: OpenVPN option keepalive replaced by ping options 2021-09-14 14:57:31 +00:00
Quentin McGaw (desktop)
af3f882bb8 Maint: OpenVPN: only add persist-key when running without root 2021-09-14 14:55:39 +00:00
Quentin McGaw (desktop)
2ab05b9350 Maint: OpenVPN: only add persist-tun when running without root 2021-09-14 14:54:59 +00:00
Quentin McGaw (desktop)
1022eb8a6e Maint: remove OpenVPN option route-method
- Unneeded unless running on Windows
- Affects PureVPN
2021-09-14 14:49:02 +00:00
Quentin McGaw (desktop)
15fe62de32 Maint: remove OpenVPN route-delay option
- Affects Cyberghost
- Affects PureVPN
2021-09-14 14:48:14 +00:00
Quentin McGaw (desktop)
83d87f83f9 Maint: remove useless OpenVPN ping-timer-rem 2021-09-14 14:47:27 +00:00
Quentin McGaw (desktop)
76a0c1f6c4 Fix: OpenVPN remove compression options (security)
- Affects FastestVPN
- Affects Hide My Ass
- Affects IP Vanish
- Affects IVPN
- Affects NordVPN
- Affects PrivateVPN
- Affects ProtonVPN
- Affects VPN Unlimited
- Affects VyprVPN
2021-09-14 14:45:30 +00:00
Quentin McGaw (desktop)
a1588302a7 Change: Windscribe: OpenVPN cipher aes-256-gcm 2021-09-14 13:22:43 +00:00
Quentin McGaw (desktop)
91ce790b6b Fix: OpenVPN custom: do not deduplicate lines
- Remove case by case lines to avoid duplicates
- Do not deduplicate all lines
2021-09-14 12:32:15 +00:00
Quentin McGaw (desktop)
5d3982c2d2 Docs: update Wireguard support list in readme 2021-09-13 20:23:16 +00:00
Quentin McGaw (desktop)
2cf7f7b268 Maint: WIREGUARD_PORT to WIREGUARD_ENDPOINT_PORT 2021-09-13 20:06:47 +00:00
Quentin McGaw (desktop)
8645d978ba Feat: VPNSP=custom for Wireguard
- `WIREGUARD_PUBLIC_KEY` variable
- `WIREGUARD_ENDPOINT_IP` variable
2021-09-13 19:33:04 +00:00
Quentin McGaw (desktop)
cc18b158f4 Maint: remove all script-security OpenVPN options
- Affects Cyberghost
- Affects Mullvad
- Affects PureVPN
- Affects Surfshark
- Affects Torguard
- Affects Windscribe
2021-09-13 16:05:14 +00:00
Quentin McGaw (desktop)
0730b6db6e Maint: remove ncp-disable deprecated option
- Affects Cyberghost
- Affects PIA
- Affects Torguard
- Affects Windscribe
2021-09-13 15:59:33 +00:00
Quentin McGaw (desktop)
3d2a360401 Fix: remove OpenVPN compression (PIA, torguard) 2021-09-13 15:56:25 +00:00
Quentin McGaw (desktop)
0c60dab384 Maint: remove deprecated keysize OpenVPN option 2021-09-13 15:55:16 +00:00
Quentin McGaw (desktop)
f5f0ad7f28 Maint: remove deprecated tun-ipv6 option 2021-09-13 15:54:01 +00:00
Quentin McGaw
f807f756eb VPNSP value custom for OpenVPN custom config files (#621)
- Retro-compatibility: `OPENVPN_CUSTOM_CONFIG` set implies `VPNSP=custom`
- Change: `up` and `down` options are not filtered out
- Change: `OPENVPN_INTERFACE` overrides the network interface defined in the configuration file
- Change: `PORT` overrides any port found in the configuration file
- Feat: config file is read when building the OpenVPN configuration, so it's effectively reloaded on VPN restarts
- Feat: extract values from custom file at start to log out valid settings
- Maint: `internal/openvpn/extract` package instead of `internal/openvpn/custom` package
- Maint: All providers' `BuildConf` method return an error
- Maint: rename `CustomConfig` to `ConfFile` in Settings structures
2021-09-13 08:30:14 -07:00
Quentin McGaw (desktop)
11af6c10f1 HotFix: use newer HEALTH variables in Dockerfile 2021-09-13 01:30:37 +00:00
Quentin McGaw (desktop)
40342619e7 Maint: dynamically set allowed VPN input ports
- Feat: allow to change VPN type at runtime
- Feat: allow to change interface name at runtime
- Maint: Add cleanup method to cleanup VPN loop on a vpn shutdown
- Change: allow VPN inputs ports only when tunnel is up
2021-09-13 00:50:20 +00:00
Quentin McGaw (desktop)
19bf62c21f Fix: set non block on TUN device 2021-09-12 13:32:50 +00:00
Quentin McGaw (desktop)
2ea00d149f Feat: adapt logger prefix to VPN used
- `openvpn: ` for OpenVPN
- `wireguard: ` for Wireguard
2021-09-12 13:27:30 +00:00
Quentin McGaw (desktop)
cc677bde93 Maint: change default ping address to github.com to test DNS 2021-09-11 22:27:32 +00:00
Quentin McGaw (desktop)
6627cda96c Feat: HEALTH_ADDRESS_TO_PING variable
- Defaults to `1.1.1.1`
- Add more Ping integration tests with different addresses
- Add unit test pinging 127.0.0.1
- Add comment explaining why we need to use ICMP instead of UDP
2021-09-11 22:22:55 +00:00
Quentin McGaw (desktop)
cade2732b0 Maint: improve internal/configuration/health_test.go unit test 2021-09-11 22:14:37 +00:00
Quentin McGaw (desktop)
541a4a3271 Feat: healthcheck uses ping instead of DNS 2021-09-11 21:49:46 +00:00
Quentin McGaw (desktop)
0eccd068e5 Maint: rename health OpenVPN names to VPN
- `HEALTH_OPENVPN_DURATION_INITIAL` renamed to `HEALTH_VPN_DURATION_INITIAL` with retro-compatiblity
- `HEALTH_OPENVPN_DURATION_ADDITION` renamed to `HEALTH_VPN_DURATION_ADDITION` with retro-compatiblity
2021-09-11 21:04:21 +00:00
Quentin McGaw (desktop)
87f4b9e422 Docs: update maintenance document 2021-09-11 15:29:29 +00:00
Quentin McGaw (desktop)
bcaf2e42fd Maint: re-order Dockerfile environment variables 2021-09-11 15:24:00 +00:00
Quentin McGaw (desktop)
d39201f9b3 Fix: public IP loop deadlock 2021-09-10 22:54:02 +00:00
Quentin McGaw (desktop)
8ac2a816c3 Fix: close HTTP client connections when tunnel comes up 2021-09-10 22:53:05 +00:00
Quentin McGaw (desktop)
344f1bf9ee Docs: add wireguard in top description 2021-09-10 22:18:29 +00:00
Quentin McGaw (desktop)
f0a006fc43 Docs: add fix the unraid template link 2021-09-10 21:11:08 +00:00
Quentin McGaw (desktop)
145da0b21d Maint: rename wireguard CustomPort 2021-09-10 19:07:14 +00:00
Quentin McGaw (desktop)
094de89a3e Fix: PIA_ENCRYPTION default value outside Docker
- Defaults to `strong` instead of strong certificate string
- No impact on Docker images since variable is set to `strong` in Dockerfile
- Only read `PIA_ENCRYPTION` if service provider is PIA
2021-09-09 21:36:14 +00:00
Quentin McGaw (desktop)
65ace12def Maint: internal/openvpn/parse package
- Parse PEM key data for Cyberghost and VPNUnlimited
- Add more unit tests
2021-09-08 16:40:19 +00:00
Quentin McGaw (desktop)
9afe455635 Fix: missing status code check for Windscribe API 2021-09-08 16:09:32 +00:00
Quentin McGaw (desktop)
45ce422a89 Maint: use type aliases in internal/netlink 2021-09-07 02:35:39 +00:00
Quentin McGaw (desktop)
4a0738cd49 Fix: repo servers.json path 2021-09-06 13:41:45 +00:00
521 changed files with 30205 additions and 31822 deletions

View File

@@ -8,7 +8,7 @@
"vscode"
],
"shutdownAction": "stopCompose",
"postCreateCommand": "source ~/.windows.sh && go mod download && go mod tidy",
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
"workspaceFolder": "/workspace",
"extensions": [
"golang.go",

View File

@@ -1,49 +0,0 @@
---
name: Bug
about: Report a bug
title: 'Bug: FILL THIS TEXT!'
labels: ":bug: bug"
assignees: qdm12
---
<!---
⚠️ Answer the following or I'll insta-close your issue
-->
**Is this urgent?**: No
**Host OS** (approximate answer is fine too): Ubuntu 18
<!---
🚧 If this is about the Unraid template see https://github.com/qdm12/gluetun/discussions/550
-->
**CPU arch** or **device name**: amd64
**What VPN provider are you using**:
**What are you using to run your container?**: Docker Compose
**What is the version of the program** (See the line at the top of your logs)
```
Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)
```
**What's the problem** 🤔
That feature doesn't work
**Share your logs... (careful to remove in example tokens)**
```log
PASTE YOUR LOGS
IN THERE
```
<!---
💡 You can highlight your code with https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlight
-->

107
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,107 @@
name: Bug
description: Report a bug
title: "Bug: "
labels: [":bug: bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: dropdown
id: urgent
attributes:
label: Is this urgent?
description: |
Is this a critical bug, or do you need this fixed urgently?
If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun/wiki/Docker-image-tags) if that can help.
options:
- "No"
- "Yes"
- type: input
id: host-os
attributes:
label: Host OS
description: What is your host OS?
placeholder: "Debian Buster"
- type: dropdown
id: cpu-arch
attributes:
label: CPU arch
description: You can find it on Linux with `uname -m`.
options:
- x86_64
- aarch64
- armv7l
- "386"
- s390x
- ppc64le
- type: dropdown
id: vpn-service-provider
attributes:
label: VPN service provider
options:
- Custom
- Cyberghost
- ExpressVPN
- FastestVPN
- HideMyAss
- IPVanish
- IVPN
- Mullvad
- NordVPN
- Privado
- Private Internet Access
- PrivateVPN
- ProtonVPN
- PureVPN
- Surfshark
- TorGuard
- VPNUnlimited
- VyprVPN
- WeVPN
- Windscribe
validations:
required: true
- type: dropdown
id: docker
attributes:
label: What are you using to run the container
options:
- docker run
- docker-compose
- Portainer
- Kubernetes
- Podman
- Other
validations:
required: true
- type: input
id: version
attributes:
label: What is the version of Gluetun
description: |
Copy paste the version line at the top of your logs.
It should be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
validations:
required: true
- type: textarea
id: problem
attributes:
label: "What's the problem 🤔"
placeholder: "That feature does not work..."
validations:
required: true
- type: textarea
id: logs
attributes:
label: Share your logs
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
render: log
validations:
required: true
- type: textarea
id: config
attributes:
label: Share your configuration
description: Share your configuration such as `docker-compose.yml`. Ensure to remove credentials.
render: yml

7
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
contact_links:
- name: Configuration help?
url: https://github.com/qdm12/gluetun/discussions/new
about: Please create a Github discussion.
- name: Unraid template issue
url: https://github.com/qdm12/gluetun/discussions/550
about: Please read the relevant Github discussion.

View File

@@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest a feature to add to this project
title: 'Feature request: FILL THIS TEXT!'
labels: ":bulb: feature request"
assignees: qdm12
---
**What's the feature?** 🧐
- Support this new feature because that and that
**Optional extra information** 🚀
- I tried `docker run something` and it doesn't work
- That [url](https://github.com/qdm12/gluetun) is interesting

View File

@@ -0,0 +1,19 @@
name: Feature request
description: Suggest a feature to add to Gluetun
title: "Feature request: "
labels: [":bulb: feature request"]
body:
- type: textarea
id: description
attributes:
label: "What's the feature 🧐"
placeholder: "Make the tunnel resistant to earth quakes"
validations:
required: true
- type: textarea
id: extra
attributes:
label: "Extra information and references"
placeholder: |
- I tried `docker run something` and it doesn't work
- That [url](https://github.com/qdm12/gluetun) is interesting

View File

@@ -1,67 +0,0 @@
---
name: Help
about: Ask for help
title: 'Help: FILL THIS TEXT!'
labels: ":pray: help wanted"
assignees:
---
<!---
⚠️ If this about a Docker configuration problem or another service:
Start a discussion at https://github.com/qdm12/gluetun/discussions/new
OR I WILL INSTA-CLOSE YOUR ISSUE.
-->
<!---
⚠️ Answer the following or I'll insta-close your issue
-->
**Is this urgent?**: No
**Host OS** (approximate answer is fine too): Ubuntu 18
**CPU arch** or **device name**: amd64
**What VPN provider are you using**:
**What is the version of the program** (See the line at the top of your logs)
```
Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)
```
**What's the problem** 🤔
That feature doesn't work
**Share your logs... (careful to remove in example tokens)**
```log
PASTE YOUR LOGS
IN THERE
```
**What are you using to run your container?**: Docker Compose
<!---
💡 You can highlight your code with https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlight
-->
Please also share your configuration file:
```yml
your .yml
content
in here
```
or
```sh
# your docker
# run command
# in here
```

18
.github/ISSUE_TEMPLATE/wiki issue.yml vendored Normal file
View File

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

17
.github/labels.yml vendored
View File

@@ -14,6 +14,14 @@
color: "795548"
description: ""
# Priority
- name: "🚨 Urgent"
color: "d5232f"
description: ""
- name: "💤 Low priority"
color: "4285f4"
description: ""
# VPN providers
- name: ":cloud: Cyberghost"
color: "cfe8d4"
@@ -27,6 +35,9 @@
- name: ":cloud: IVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: ExpressVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: FastestVPN"
color: "cfe8d4"
description: ""
@@ -36,6 +47,9 @@
- name: ":cloud: NordVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Perfect Privacy"
color: "cfe8d4"
description: ""
- name: ":cloud: PIA"
color: "cfe8d4"
description: ""
@@ -62,6 +76,9 @@
- name: ":cloud: Vyprvpn"
color: "cfe8d4"
description: ""
- name: ":cloud: WeVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Windscribe"
color: "cfe8d4"
description: ""

View File

@@ -1,6 +1,24 @@
name: CI
on:
release:
types:
- published
push:
branches:
- master
paths:
- .github/workflows/ci.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
pull_request:
branches:
- master
paths:
- .github/workflows/ci.yml
- cmd/**
@@ -14,11 +32,22 @@ 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
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v2.4.0
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error
- name: Linting
run: docker build --target lint .
@@ -44,18 +73,42 @@ jobs:
- 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
# - name: Image security analysis
# uses: snyk/actions/docker@master
# env:
# SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
# with:
# image: final-image
publish:
# Only run if it's a push event or if it's a PR from this repository
if: |
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]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v2.4.0
# extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
flavor: |
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 != format('refs/heads/{0}', github.event.repository.default_branch) }}
type=ref,event=pr
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@v1
- uses: docker/setup-buildx-action@v1
@@ -65,35 +118,18 @@ jobs:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Set variables
id: vars
env:
EVENT_NAME: ${{ github.event_name }}
run: |
BRANCH=${GITHUB_REF#refs/heads/}
TAG=${GITHUB_REF#refs/tags/}
echo ::set-output name=commit::$(git rev-parse --short HEAD)
echo ::set-output name=created::$(date -u +%Y-%m-%dT%H:%M:%SZ)
if [ "$TAG" != "$GITHUB_REF" ]; then
echo ::set-output name=version::$TAG
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
elif [ "$BRANCH" = "master" ]; then
echo ::set-output name=version::latest
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
else
echo ::set-output name=version::$BRANCH
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
fi
- 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.6.1
uses: docker/build-push-action@v2.8.0
with:
platforms: ${{ steps.vars.outputs.platforms }}
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
labels: ${{ steps.meta.outputs.labels }}
build-args: |
CREATED=${{ steps.vars.outputs.created }}
COMMIT=${{ steps.vars.outputs.commit }}
VERSION=${{ steps.vars.outputs.version }}
tags: |
qmcgaw/gluetun:${{ steps.vars.outputs.version }}
qmcgaw/private-internet-access:${{ steps.vars.outputs.version }}
CREATED=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
COMMIT=${{ steps.shortcommit.outputs.value }}
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
tags: ${{ steps.meta.outputs.tags }}
push: true

37
.github/workflows/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
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

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
uses: actions/checkout@v2.4.0
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v2
with:

40
.github/workflows/fork.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
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.3.4
- uses: actions/checkout@v2.4.0
- uses: crazy-max/ghaction-github-labeler@v3
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.3.4
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error

View File

@@ -1,6 +1,4 @@
linters-settings:
maligned:
suggest-new: true
misspell:
locale: US
@@ -11,6 +9,7 @@ issues:
- dupl
- maligned
- goerr113
- containedctx
- path: internal/server/
linters:
- dupl
@@ -29,17 +28,38 @@ issues:
- text: "mnd: Magic number: 0400*"
linters:
- gomnd
- text: "variable 'mssFix' is only used in the if-statement*"
path: "openvpnconf.go"
linters:
- ifshort
- text: "variable 'auth' is only used in the if-statement*"
path: "openvpnconf.go"
linters:
- ifshort
- linters:
- lll
source: "^//go:generate "
linters:
disable-all: true
enable:
# - cyclop
# - errorlint
# - ireturn
# - varnamelen
# - wrapcheck
- asciicheck
- bidichk
- bodyclose
- deadcode
- containedctx
- decorder
- dogsled
- dupl
- errcheck
- durationcheck
- errchkjson
- errname
- exhaustive
- exportloopref
- forcetypeassert
- gci
- gochecknoglobals
- gochecknoinits
@@ -52,33 +72,35 @@ linters:
- goheader
- goimports
- gomnd
- gomoddirectives
- goprintffuncname
- gosec
- gosimple
- govet
- grouper
- ifshort
- importas
- ineffassign
- lll
- maintidx
- makezero
- misspell
- nakedret
- nestif
- nilerr
- nilnil
- noctx
- nolintlint
- prealloc
- predeclared
- predeclared
- promlinter
- revive
- rowserrcheck
- sqlclosecheck
- staticcheck
- structcheck
- tenv
- thelper
- tparallel
- typecheck
- unconvert
- unparam
- unused
- varcheck
- wastedassign
- whitespace
run:

View File

@@ -1,8 +1,8 @@
ARG ALPINE_VERSION=3.14
ARG GO_ALPINE_VERSION=3.14
ARG ALPINE_VERSION=3.15
ARG GO_ALPINE_VERSION=3.15
ARG GO_VERSION=1.17
ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.41.1
ARG GOLANGCI_LINT_VERSION=v1.44.2
ARG BUILDPLATFORM=linux/amd64
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
@@ -66,62 +66,67 @@ 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 \
VERSION_INFORMATION=on \
LOG_LEVEL=info \
ENV VPN_SERVICE_PROVIDER=pia \
VPN_TYPE=openvpn \
PROTOCOL=udp \
# Common VPN options
VPN_ENDPOINT_IP= \
VPN_ENDPOINT_PORT= \
VPN_INTERFACE=tun0 \
# OpenVPN
OPENVPN_PROTOCOL=udp \
OPENVPN_USER= \
OPENVPN_PASSWORD= \
OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \
OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
OPENVPN_VERSION=2.5 \
OPENVPN_VERBOSITY=1 \
OPENVPN_FLAGS= \
OPENVPN_ROOT=yes \
OPENVPN_TARGET_IP= \
OPENVPN_CIPHERS= \
OPENVPN_AUTH= \
OPENVPN_PROCESS_USER= \
OPENVPN_IPV6=off \
OPENVPN_CUSTOM_CONFIG= \
OPENVPN_INTERFACE=tun0 \
# Wireguard
WIREGUARD_PRIVATE_KEY= \
WIREGUARD_PRESHARED_KEY= \
WIREGUARD_ADDRESS= \
WIREGUARD_PORT= \
WIREGUARD_INTERFACE=wg0 \
TZ= \
PUID= \
PGID= \
PUBLICIP_FILE="/tmp/gluetun/ip" \
# VPN provider settings
OPENVPN_USER= \
OPENVPN_PASSWORD= \
USER_SECRETFILE=/run/secrets/openvpn_user \
PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
REGION= \
COUNTRY= \
CITY= \
PORT= \
SERVER_HOSTNAME= \
# Mullvad only:
WIREGUARD_PUBLIC_KEY= \
WIREGUARD_ADDRESSES= \
# VPN server filtering
SERVER_REGIONS= \
SERVER_COUNTRIES= \
SERVER_CITIES= \
SERVER_HOSTNAMES= \
# # Mullvad only:
ISP= \
OWNED=no \
# Private Internet Access only:
PIA_ENCRYPTION=strong \
PORT_FORWARDING=off \
PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
# Cyberghost only:
CYBERGHOST_GROUP="Premium UDP Europe" \
OWNED_ONLY=no \
# # Private Internet Access only:
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:
# # Nordvpn only:
SERVER_NUMBER= \
# NordVPN and ProtonVPN only:
SERVER_NAME= \
# ProtonVPN only:
# # PIA and ProtonVPN only:
SERVER_NAMES= \
# # ProtonVPN only:
FREE_ONLY= \
# Openvpn
OPENVPN_CIPHER= \
OPENVPN_AUTH= \
# # Surfshark only:
MULTIHOP_ONLY= \
# Firewall
FIREWALL=on \
FIREWALL_VPN_INPUT_PORTS= \
FIREWALL_INPUT_PORTS= \
FIREWALL_OUTBOUND_SUBNETS= \
FIREWALL_DEBUG=off \
# Logging
LOG_LEVEL=info \
# Health
HEALTH_OPENVPN_DURATION_INITIAL=6s \
HEALTH_OPENVPN_DURATION_ADDITION=5s \
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
HEALTH_TARGET_ADDRESS=github.com:443 \
HEALTH_VPN_DURATION_INITIAL=6s \
HEALTH_VPN_DURATION_ADDITION=5s \
# DNS over TLS
DOT=on \
DOT_PROVIDERS=cloudflare \
@@ -136,18 +141,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 \
# Firewall
FIREWALL=on \
FIREWALL_VPN_INPUT_PORTS= \
FIREWALL_INPUT_PORTS= \
FIREWALL_OUTBOUND_SUBNETS= \
FIREWALL_DEBUG=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 \
@@ -155,22 +154,42 @@ 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 \
UPDATER_PERIOD=0
ENTRYPOINT ["/entrypoint"]
# Control server
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
# Server data updater
UPDATER_PERIOD=0 \
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 ["/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 && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
# Fix vulnerability issue
apk add --no-cache --update busybox && \
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
deluser openvpn && \
deluser unbound && \
mkdir /gluetun
COPY --from=build /tmp/gobuild/entrypoint /entrypoint
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint

111
README.md
View File

@@ -1,11 +1,11 @@
# Gluetun VPN client
*Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, FastestVPN,
HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Privado, Private Internet Access, PrivateVPN,
ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN and Windscribe VPN servers
using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
*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!
**ANNOUNCEMENT**: Large settings refactor merged on 2022-06-01, please file issues if you find any problem!
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
@@ -40,10 +40,14 @@ using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
## Quick links
- Problem or suggestion?
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
- [Create an issue](https://github.com/qdm12/gluetun/issues)
- [Setup](#Setup)
- [Features](#Features)
- Problem?
- [Check the Wiki](https://github.com/qdm12/gluetun/wiki)
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
- Suggestion?
- [Create an issue](https://github.com/qdm12/gluetun/issues)
- [Join the Slack channel](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
- Happy?
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
@@ -53,69 +57,70 @@ using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
[![Video Gif](https://i.imgur.com/CetWunc.gif)](https://youtu.be/0F6I03LQcI4)
- [Substack Console interview](https://console.substack.com/p/console-72)
## Features
- Based on Alpine 3.14 for a small Docker image of 31MB
- Supports: **Cyberghost**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** servers
- Supports OpenVPN
- Supports Wireguard for **Mullvad**, **Ivpn** and **Windscribe** (more in progress, see #134)
- Based on Alpine 3.15 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 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)
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
- DNS over TLS baked in with service provider(s) of your choice
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
- Choose the vpn network protocol, `udp` or `tcp`
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
- [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-to-gluetun)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun/wiki/Connect-to-gluetun)
- [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun/wiki/Connect-a-LAN-device-to-gluetun)
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
- VPN server side port forwarding for Private Internet Access and Vyprvpn
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun/wiki/Private-internet-access#vpn-server-port-forwarding)
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
- Subprograms all drop root privileges once launched
- Subprograms output streams are all merged together
- Unbound subprogram drops root privileges once launched
- Can work as a Kubernetes sidecar container, thanks @rorph
## Setup
1. Ensure your `tun` kernel module is setup:
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
```sh
sudo modprobe tun
# or, if you don't have modprobe, with
sudo insmod /lib/modules/tun.ko
```
Go to the [Wiki](https://github.com/qdm12/gluetun/wiki)!
1. Extra steps:
- [For Synology users](https://github.com/qdm12/gluetun/wiki/Synology-setup)
- [For 32 bit Operating systems (**Rasberry Pis**)](https://github.com/qdm12/gluetun/wiki/32-bit-setup)
1. Launch the container with:
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun/issues/new?assignees=&labels=%F0%9F%93%84+Wiki+issue&template=wiki+issue.yml&title=Wiki+issue%3A+)
```bash
docker run -d --name gluetun --cap-add=NET_ADMIN \
-e VPNSP="private internet access" -e REGION="CA Montreal" \
-e OPENVPN_USER=js89ds7 -e OPENVPN_PASSWORD=8fd9s239G \
-v /yourpath:/gluetun \
qmcgaw/gluetun
```
Here's a docker-compose.yml for the laziest:
or use [docker-compose.yml](https://github.com/qdm12/gluetun/blob/master/docker-compose.yml) with:
```bash
docker-compose up -d
```
You should probably check the many [environment variables](https://github.com/qdm12/gluetun/wiki/Environment-variables) available to adapt the container to your needs.
## Further setup
The following points are all optional but should give you insights on all the possibilities with this container.
- [Test your setup](https://github.com/qdm12/gluetun/wiki/Test-your-setup)
- [How to connect other containers and devices to Gluetun](https://github.com/qdm12/gluetun/wiki/Connect-to-gluetun)
- [How to use Wireguard](https://github.com/qdm12/gluetun/wiki/Wireguard)
- [VPN server side port forwarding](https://github.com/qdm12/gluetun/wiki/Port-forwarding)
- [HTTP control server](https://github.com/qdm12/gluetun/wiki/HTTP-Control-server) to automate things, restart Openvpn etc.
- Update the image with `docker pull qmcgaw/gluetun:latest`. See this [Wiki document](https://github.com/qdm12/gluetun/wiki/Docker-image-tags) for Docker tags available.
- Use [Docker secrets](https://github.com/qdm12/gluetun/wiki/Docker-secrets) to read your credentials instead of environment variables
```yml
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
ports:
- 8888:8888/tcp # HTTP proxy
- 8388:8388/tcp # Shadowsocks
- 8388:8388/udp # Shadowsocks
volumes:
- /yourpath:/gluetun
environment:
# See https://github.com/qdm12/gluetun/wiki
- VPN_SERVICE_PROVIDER=ivpn
- VPN_TYPE=openvpn
# OpenVPN:
- OPENVPN_USER=
- OPENVPN_PASSWORD=
# Wireguard:
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
# - WIREGUARD_ADDRESSES=10.64.222.21/32
# Timezone for accurate log times
- TZ=
```
## License

View File

@@ -7,15 +7,20 @@ import (
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
_ "time/tzdata"
_ "github.com/breml/rootcerts"
"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"
@@ -25,6 +30,7 @@ import (
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/portforward"
"github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gluetun/internal/publicip"
"github.com/qdm12/gluetun/internal/routing"
"github.com/qdm12/gluetun/internal/server"
@@ -35,8 +41,10 @@ import (
"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/updated/pkg/dnscrypto"
)
@@ -48,11 +56,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,
@@ -72,12 +75,16 @@ func main() {
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 {
@@ -115,43 +122,119 @@ 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,
args []string, logger logging.ParentLogger, source sources.Source,
tun tun.Interface, netLinker netlink.NetLinker, cmder command.RunStarter,
cli 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":
return cli.FormatServers(args[2:])
default:
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
}
}
announcementExp, err := time.Parse(time.RFC3339, "2021-02-15T00:00:00Z")
if err != nil {
return err
}
splashSettings := gosplash.Settings{
User: "qdm12",
Repository: "gluetun",
Emails: []string{"quentin.mcgaw@gmail.com"},
Version: buildInfo.Version,
Commit: buildInfo.Commit,
BuildDate: buildInfo.Created,
Announcement: "Large settings parsing refactoring merged on 2022-01-06, please report any issue!",
AnnounceExp: announcementExp,
// Sponsor information
PaypalUser: "qmcgaw",
GithubSponsor: "qdm12",
}
for _, line := range gosplash.MakeLines(splashSettings) {
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.PatchLevel(*allSettings.Log.Level)
routingLogger := logger.NewChild(logging.Settings{
Prefix: "routing: ",
})
if *allSettings.Firewall.Debug { // To remove in v4
routingLogger.PatchLevel(logging.LevelDebug)
}
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: ",
})
if *allSettings.Firewall.Debug { // To remove in v4
firewallLogger.PatchLevel(logging.LevelDebug)
}
firewallConf := firewall.NewConfig(firewallLogger, cmder,
defaultInterface, defaultGateway, localNetworks, defaultIP)
if *allSettings.Firewall.Enabled {
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: "})
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(allServers)
if err != nil {
return err
}
logger.PatchLevel(allSettings.Log.Level)
puid, pgid := allSettings.System.PUID, allSettings.System.PGID
allSettings.Pprof.HTTPServer.Logger = logger
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}
@@ -165,27 +248,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
dnsConf := unbound.NewConfigurator(nil, cmder, dnsCrypto,
"/etc/unbound", "/usr/sbin/unbound", cacertsPath)
announcementExp, err := time.Parse(time.RFC3339, "2021-10-02T00:00:00Z")
if err != nil {
return err
}
splashSettings := gosplash.Settings{
User: "qdm12",
Repository: "gluetun",
Emails: []string{"quentin.mcgaw@gmail.com"},
Version: buildInfo.Version,
Commit: buildInfo.Commit,
BuildDate: buildInfo.Created,
Announcement: "Wireguard is now supported for Mullvad, IVPN and Windscribe!",
AnnounceExp: announcementExp,
// Sponsor information
PaypalUser: "qmcgaw",
GithubSponsor: "qdm12",
}
for _, line := range gosplash.MakeLines(splashSettings) {
fmt.Println(line)
}
err = printVersions(ctx, logger, []printVersionElement{
{name: "Alpine", getVersion: alpineConf.Version},
{name: "OpenVPN 2.4", getVersion: ovpnConf.Version24},
@@ -211,57 +273,25 @@ 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...")
@@ -277,24 +307,10 @@ 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
if err != nil {
return err
}
}
for _, vpnPort := range allSettings.Firewall.VPNInputPorts {
vpnIntf := allSettings.VPN.VPNInterface()
err = firewallConf.SetAllowedPort(ctx, vpnPort, vpnIntf)
err = tun.Create(tunDevice)
if err != nil {
return err
}
@@ -308,6 +324,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
} // TODO move inside firewall?
// Shutdown settings
const totalShutdownTimeout = 3 * time.Second
const defaultShutdownTimeout = 400 * time.Millisecond
defaultShutdownOnSuccess := func(goRoutineName string) {
logger.Info(goRoutineName + ": terminated ✔️")
@@ -315,34 +332,38 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
defaultShutdownOnFailure := func(goRoutineName string, err error) {
logger.Warn(goRoutineName + ": " + err.Error() + " ⚠️")
}
defaultGoRoutineSettings := goshutdown.GoRoutineSettings{Timeout: defaultShutdownTimeout}
defaultGroupSettings := goshutdown.GroupSettings{
Timeout: defaultShutdownTimeout,
OnSuccess: defaultShutdownOnSuccess,
}
defaultGroupOptions := []group.Option{
group.OptionTimeout(defaultShutdownTimeout),
group.OptionOnSuccess(defaultShutdownOnSuccess)}
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupSettings)
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupSettings)
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupSettings)
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
pprofReady := make(chan struct{})
pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server")
go pprofServer.Run(pprofCtx, pprofReady, pprofDone)
otherGroupHandler.Add(pprofHandler)
<-pprofReady
portForwardLogger := logger.NewChild(logging.Settings{Prefix: "port forwarding: "})
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
httpClient, firewallConf, portForwardLogger)
portForwardHandler, portForwardCtx, portForwardDone := goshutdown.NewGoRoutineHandler(
"port forwarding", goshutdown.GoRoutineSettings{Timeout: time.Second})
"port forwarding", goroutine.OptionTimeout(time.Second))
go portForwardLooper.Run(portForwardCtx, portForwardDone)
unboundLogger := logger.NewChild(logging.Settings{Prefix: "dns over tls: "})
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
unboundLogger)
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
"unbound", defaultGoRoutineSettings)
"unbound", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
go unboundLooper.Run(dnsCtx, dnsDone)
otherGroupHandler.Add(dnsHandler)
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
"dns ticker", defaultGoRoutineSettings)
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
controlGroupHandler.Add(dnsTickerHandler)
@@ -350,35 +371,35 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
logger.NewChild(logging.Settings{Prefix: "ip getter: "}),
allSettings.PublicIP, puid, pgid)
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
"public IP", defaultGoRoutineSettings)
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
go publicIPLooper.Run(pubIPCtx, pubIPDone)
otherGroupHandler.Add(pubIPHandler)
pubIPTickerHandler, pubIPTickerCtx, pubIPTickerDone := goshutdown.NewGoRoutineHandler(
"public IP", defaultGoRoutineSettings)
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone)
tickersGroupHandler.Add(pubIPTickerHandler)
vpnLogger := logger.NewChild(logging.Settings{Prefix: "vpn: "})
vpnLooper := vpn.NewLoop(allSettings.VPN,
vpnLooper := vpn.NewLoop(allSettings.VPN, allSettings.Firewall.VPNInputPorts,
allServers, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient,
buildInfo, allSettings.VersionInformation)
buildInfo, *allSettings.Version.Enabled)
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
"vpn", goshutdown.GoRoutineSettings{Timeout: time.Second})
"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: "}))
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
"updater", defaultGoRoutineSettings)
"updater", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
go updaterLooper.Run(updaterCtx, updaterDone)
tickersGroupHandler.Add(updaterHandler)
updaterTickerHandler, updaterTickerCtx, updaterTickerDone := goshutdown.NewGoRoutineHandler(
"updater ticker", defaultGoRoutineSettings)
"updater ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
go updaterLooper.RunRestartTicker(updaterTickerCtx, updaterTickerDone)
controlGroupHandler.Add(updaterTickerHandler)
@@ -386,21 +407,21 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
logger.NewChild(logging.Settings{Prefix: "http proxy: "}),
allSettings.HTTPProxy)
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
"http proxy", defaultGoRoutineSettings)
"http proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
otherGroupHandler.Add(httpProxyHandler)
shadowsocksLooper := shadowsocks.NewLooper(allSettings.ShadowSocks,
shadowsocksLooper := shadowsocks.NewLooper(allSettings.Shadowsocks,
logger.NewChild(logging.Settings{Prefix: "shadowsocks: "}))
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
"shadowsocks proxy", defaultGoRoutineSettings)
"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", defaultGoRoutineSettings)
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
httpServer := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
logger.NewChild(logging.Settings{Prefix: "http server: "}),
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper)
@@ -410,16 +431,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
healthLogger := logger.NewChild(logging.Settings{Prefix: "healthcheck: "})
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", defaultGoRoutineSettings)
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
go healthcheckServer.Run(healthServerCtx, healthServerDone)
const orderShutdownTimeout = 3 * time.Second
orderSettings := goshutdown.OrderSettings{
Timeout: orderShutdownTimeout,
OnFailure: defaultShutdownOnFailure,
OnSuccess: defaultShutdownOnSuccess,
}
orderHandler := goshutdown.NewOrder("gluetun", orderSettings)
orderHandler := goshutdown.NewOrderHandler("gluetun",
order.OptionTimeout(totalShutdownTimeout),
order.OptionOnSuccess(defaultShutdownOnSuccess),
order.OptionOnFailure(defaultShutdownOnFailure))
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
vpnHandler, portForwardHandler, otherGroupHandler)
@@ -437,7 +455,11 @@ type printVersionElement struct {
getVersion func(ctx context.Context) (version string, err error)
}
func printVersions(ctx context.Context, logger logging.Logger,
type infoer interface {
Info(s string)
}
func printVersions(ctx context.Context, logger infoer,
elements []printVersionElement) (err error) {
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)

View File

@@ -1,25 +0,0 @@
version: "3.7"
services:
gluetun:
image: qmcgaw/gluetun
container_name: gluetun
cap_add:
- NET_ADMIN
network_mode: bridge
ports:
- 8888:8888/tcp # HTTP proxy
- 8388:8388/tcp # Shadowsocks
- 8388:8388/udp # Shadowsocks
- 8000:8000/tcp # Built-in HTTP control server
# command:
volumes:
- /yourpath:/gluetun
environment:
# More variables are available, see the Wiki table
- OPENVPN_USER=
- OPENVPN_PASSWORD=
- VPNSP=private internet access
- VPN_TYPE=openvpn
# Timezone for accurate logs times
- TZ=
restart: always

17
go.mod
View File

@@ -3,16 +3,19 @@ module github.com/qdm12/gluetun
go 1.17
require (
github.com/fatih/color v1.12.0
github.com/breml/rootcerts v0.2.2
github.com/fatih/color v1.13.0
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.1.0
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/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/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
@@ -23,15 +26,15 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mdlayher/genetlink v1.0.0 // indirect
github.com/mdlayher/netlink v1.4.0 // indirect
github.com/miekg/dns v1.1.40 // indirect
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
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect

34
go.sum
View File

@@ -4,6 +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.2 h1:hkHEpbTdYaNvDoYeq+mwRvCeg/YTTl23DjQ1Tnj71Zs=
github.com/breml/rootcerts v0.2.2/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=
@@ -11,8 +13,9 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
@@ -64,10 +67,12 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
@@ -102,12 +107,16 @@ github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8
github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/qdm12/goshutdown v0.1.0 h1:lmwnygdXtnr2pa6VqfR/bm8077/BnBef1+7CP96B7Sw=
github.com/qdm12/goshutdown v0.1.0/go.mod h1:/LP3MWLqI+wGH/ijfaUG+RHzBbKXIiVKnrg5vXOCf6Q=
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
github.com/qdm12/gosplash v0.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/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=
@@ -121,10 +130,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
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/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=
@@ -175,7 +184,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=
@@ -183,7 +191,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=

View File

@@ -8,6 +8,7 @@ type CLIer interface {
HealthChecker
OpenvpnConfigMaker
Updater
ServersFormatter
}
type CLI struct {
@@ -16,6 +17,6 @@ type CLI struct {
func New() *CLI {
return &CLI{
repoServersPath: "./internal/constants/servers.json",
repoServersPath: "./internal/storage/servers.json",
}
}

View File

@@ -7,7 +7,7 @@ import (
"os"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
)
type ClientKeyFormatter interface {
@@ -16,7 +16,7 @@ type ClientKeyFormatter interface {
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

@@ -0,0 +1,130 @@
package cli
import (
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/storage"
)
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")
)
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
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")
if err := flagSet.Parse(args); err != nil {
return err
}
if format != "markdown" {
return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format)
}
logger := newNoopLogger()
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
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
}
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("cannot open output file: %w", err)
}
_, err = fmt.Fprint(file, formatted)
if err != nil {
_ = file.Close()
return fmt.Errorf("cannot write to output file: %w", err)
}
err = file.Close()
if err != nil {
return fmt.Errorf("cannot close output file: %w", err)
}
return nil
}

View File

@@ -6,24 +6,26 @@ 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/logging"
"github.com/qdm12/golibs/params"
)
type HealthChecker interface {
HealthCheck(ctx context.Context, env params.Interface, logger logging.Logger) error
HealthCheck(ctx context.Context, source sources.Source, warner Warner) error
}
func (c *CLI) HealthCheck(ctx context.Context, env params.Interface,
logger logging.Logger) 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, logger)
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

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

View File

@@ -5,36 +5,47 @@ import (
"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/provider"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/params"
)
type OpenvpnConfigMaker interface {
OpenvpnConfig(logger logging.Logger, env params.Interface) error
OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error
}
func (c *CLI) OpenvpnConfig(logger logging.Logger, env params.Interface) error {
type OpenvpnConfigLogger interface {
Info(s string)
Warn(s string)
}
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(allServers); err != nil {
return err
}
providerConf := provider.New(*allSettings.VPN.Provider.Name, allServers, time.Now)
connection, err := providerConf.GetConnection(allSettings.VPN.Provider.ServerSelection)
if err != nil {
return err
}
lines := providerConf.BuildConf(connection, allSettings.VPN.OpenVPN)
lines, err := providerConf.BuildConf(connection, allSettings.VPN.OpenVPN)
if err != nil {
return err
}
fmt.Println(strings.Join(lines, "\n"))
return nil
}

View File

@@ -6,64 +6,80 @@ import (
"errors"
"flag"
"fmt"
"net"
"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/storage"
"github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/golibs/logging"
)
var (
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainers 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")
ErrDNSAddress = errors.New("DNS address is not valid")
ErrNoProviderSpecified = errors.New("no provider was specified")
)
type Updater interface {
Update(ctx context.Context, args []string, logger logging.Logger) error
Update(ctx context.Context, args []string, logger UpdaterLogger) error
}
func (c *CLI) Update(ctx context.Context, args []string, logger logging.Logger) error {
options := configuration.Updater{CLI: true}
type UpdaterLogger interface {
Info(s string)
Warn(s string)
Error(s string)
}
func boolPtr(b bool) *bool { return &b }
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
options := settings.Updater{CLI: boolPtr(true)}
var endUserMode, maintainerMode, updateAll bool
var dnsAddress, 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/constants/servers.json to modify the program (for maintainers)")
flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use")
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
flagSet.StringVar(&dnsAddress, "dns", "8.8.8.8", "DNS resolver address to use")
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
flagSet.BoolVar(&options.Cyberghost, "cyberghost", false, "Update Cyberghost 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.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.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
}
options.DNSAddress = net.ParseIP(dnsAddress)
if options.DNSAddress == nil {
return fmt.Errorf("%w: %s", ErrDNSAddress, dnsAddress)
}
if updateAll {
options.EnableAll()
for _, provider := range constants.AllProviders() {
if provider == constants.Custom {
continue
}
options.Providers = append(options.Providers, provider)
}
} 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)
}
const clientTimeout = 10 * time.Second
@@ -71,25 +87,25 @@ func (c *CLI) Update(ctx context.Context, args []string, logger logging.Logger)
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()
updater := updater.New(options, httpClient, currentServers, logger)
allServers, err := updater.UpdateServers(ctx)
if err != nil {
return fmt.Errorf("%w: %s", ErrUpdateServerInformation, err)
return fmt.Errorf("cannot update server information: %w", err)
}
if endUserMode {
if err := storage.FlushToFile(allServers); err != nil {
return fmt.Errorf("%w: %s", ErrWriteToFile, err)
return fmt.Errorf("cannot write updated information to file: %w", err)
}
}
if maintainerMode {
if err := writeToEmbeddedJSON(c.repoServersPath, allServers); err != nil {
return fmt.Errorf("%w: %s", ErrWriteToFile, err)
return fmt.Errorf("cannot write updated information to file: %w", err)
}
}

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,50 +0,0 @@
package configuration
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants"
)
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.Groups, err = r.env.CSVInside("CYBERGHOST_GROUP",
constants.CyberghostGroupChoices(servers))
if err != nil {
return fmt.Errorf("environment variable CYBERGHOST_GROUP: %w", err)
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.CyberghostRegionChoices(servers))
if err != nil {
return fmt.Errorf("environment variable REGION: %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.env)
}
func (settings *OpenVPN) readCyberghost(r reader) (err error) {
settings.ClientKey, err = readClientKey(r)
if err != nil {
return err
}
settings.ClientCrt, err = readClientCertificate(r)
if err != nil {
return 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,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.env)
}

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,63 +0,0 @@
package configuration
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/params"
)
// Health contains settings for the healthcheck and health server.
type Health struct {
ServerAddress string
OpenVPN 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+"OpenVPN:")
for _, line := range settings.OpenVPN.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, logger logging.Logger) (err error) {
reader := newReader(env, models.AllServers{}, logger) // 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.logger.Warn("environment variable HEALTH_SERVER_ADDRESS: " + warning)
}
if err != nil {
return fmt.Errorf("environment variable HEALTH_SERVER_ADDRESS: %w", err)
}
settings.OpenVPN.Initial, err = r.env.Duration("HEALTH_OPENVPN_DURATION_INITIAL", params.Default("6s"))
if err != nil {
return fmt.Errorf("environment variable HEALTH_OPENVPN_DURATION_INITIAL: %w", err)
}
settings.OpenVPN.Addition, err = r.env.Duration("HEALTH_OPENVPN_DURATION_ADDITION", params.Default("5s"))
if err != nil {
return fmt.Errorf("environment variable HEALTH_OPENVPN_DURATION_ADDITION: %w", err)
}
return nil
}

View File

@@ -1,182 +0,0 @@
package configuration
import (
"errors"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/qdm12/golibs/logging/mock_logging"
"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()
var health Health
const expected = "|--Health:\n |--Server address: \n |--OpenVPN:\n |--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: ",
" |--OpenVPN:",
" |--Initial duration: 0s",
},
},
"filled settings": {
settings: Health{
ServerAddress: "address:9999",
OpenVPN: HealthyWait{
Initial: time.Second,
Addition: time.Minute,
},
},
lines: []string{
"|--Health:",
" |--Server address: address:9999",
" |--OpenVPN:",
" |--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")
testCases := map[string]struct {
openvpnInitialDuration time.Duration
openvpnInitialErr error
openvpnAdditionDuration time.Duration
openvpnAdditionErr error
serverAddress string
serverAddressWarning string
serverAddressErr error
expected Health
err error
}{
"success": {
openvpnInitialDuration: time.Second,
openvpnAdditionDuration: time.Minute,
serverAddress: "127.0.0.1:9999",
expected: Health{
ServerAddress: "127.0.0.1:9999",
OpenVPN: HealthyWait{
Initial: time.Second,
Addition: time.Minute,
},
},
},
"listening address error": {
openvpnInitialDuration: time.Second,
openvpnAdditionDuration: time.Minute,
serverAddress: "127.0.0.1:9999",
serverAddressWarning: "warning",
serverAddressErr: errDummy,
expected: Health{
ServerAddress: "127.0.0.1:9999",
},
err: errors.New("environment variable HEALTH_SERVER_ADDRESS: dummy"),
},
"initial error": {
openvpnInitialDuration: time.Second,
openvpnInitialErr: errDummy,
openvpnAdditionDuration: time.Minute,
expected: Health{
OpenVPN: HealthyWait{
Initial: time.Second,
},
},
err: errors.New("environment variable HEALTH_OPENVPN_DURATION_INITIAL: dummy"),
},
"addition error": {
openvpnInitialDuration: time.Second,
openvpnAdditionDuration: time.Minute,
openvpnAdditionErr: errDummy,
expected: Health{
OpenVPN: HealthyWait{
Initial: time.Second,
Addition: time.Minute,
},
},
err: errors.New("environment variable HEALTH_OPENVPN_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)
logger := mock_logging.NewMockLogger(ctrl)
env.EXPECT().ListeningAddress("HEALTH_SERVER_ADDRESS", gomock.Any()).
Return(testCase.serverAddress, testCase.serverAddressWarning,
testCase.serverAddressErr)
if testCase.serverAddressWarning != "" {
logger.EXPECT().Warn("environment variable HEALTH_SERVER_ADDRESS: " + testCase.serverAddressWarning)
}
if testCase.serverAddressErr == nil {
env.EXPECT().
Duration("HEALTH_OPENVPN_DURATION_INITIAL", gomock.Any()).
Return(testCase.openvpnInitialDuration, testCase.openvpnInitialErr)
if testCase.openvpnInitialErr == nil {
env.EXPECT().
Duration("HEALTH_OPENVPN_DURATION_ADDITION", gomock.Any()).
Return(testCase.openvpnAdditionDuration, testCase.openvpnAdditionErr)
}
}
r := reader{
env: env,
logger: logger,
}
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.env)
}

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.logger.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.env)
}

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 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("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,70 +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.env)
if err != nil {
return err
}
return settings.ServerSelection.Wireguard.readIVPN(r.env)
}
func (settings *OpenVPNSelection) readIVPN(env params.Interface) (err error) {
settings.TCP, err = readProtocol(env)
if err != nil {
return err
}
settings.CustomPort, err = readOpenVPNCustomPort(env, settings.TCP,
[]uint16{80, 443, 1443}, []uint16{53, 1194, 2049, 2050})
if err != nil {
return err
}
return nil
}
func (settings *WireguardSelection) readIVPN(env params.Interface) (err error) {
settings.CustomPort, err = readWireguardCustomPort(env,
[]uint16{2049, 2050, 53, 30587, 41893, 48574, 58237})
if err != nil {
return err
}
return nil
}

View File

@@ -1,253 +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
wgPort 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 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 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"},
wgPort: portCall{getCall: true, getErr: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errors.New("environment variable WIREGUARD_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"},
wgPort: 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{
CustomPort: 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("PROTOCOL", []string{constants.TCP, constants.UDP}, gomock.Any()).
Return(testCase.protocol.value, testCase.protocol.err)
}
if testCase.ovpnPort.getCall {
env.EXPECT().Get("PORT", gomock.Any()).
Return(testCase.ovpnPort.getValue, testCase.ovpnPort.getErr)
}
if testCase.ovpnPort.portCall {
env.EXPECT().Port("PORT").
Return(testCase.ovpnPort.portValue, testCase.ovpnPort.portErr)
}
if testCase.wgPort.getCall {
env.EXPECT().Get("WIREGUARD_PORT", gomock.Any()).
Return(testCase.wgPort.getValue, testCase.wgPort.getErr)
}
if testCase.wgPort.portCall {
env.EXPECT().Port("WIREGUARD_PORT").
Return(testCase.wgPort.portValue, testCase.wgPort.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,55 +0,0 @@
package configuration
import (
"encoding/pem"
"errors"
"strings"
"github.com/qdm12/gluetun/internal/constants"
)
func readClientKey(r reader) (clientKey string, err error) {
b, err := r.getFromFileOrSecretFile("OPENVPN_CLIENTKEY", constants.ClientKey)
if err != nil {
return "", err
}
return extractClientKey(b)
}
var errDecodePEMBlockClientKey = errors.New("cannot decode PEM block from client key")
func extractClientKey(b []byte) (key string, err error) {
pemBlock, _ := pem.Decode(b)
if pemBlock == nil {
return "", errDecodePEMBlockClientKey
}
parsedBytes := pem.EncodeToMemory(pemBlock)
s := string(parsedBytes)
s = strings.ReplaceAll(s, "\n", "")
s = strings.TrimPrefix(s, "-----BEGIN PRIVATE KEY-----")
s = strings.TrimSuffix(s, "-----END PRIVATE KEY-----")
return s, nil
}
func readClientCertificate(r reader) (clientCertificate string, err error) {
b, err := r.getFromFileOrSecretFile("OPENVPN_CLIENTCRT", constants.ClientCertificate)
if err != nil {
return "", err
}
return extractClientCertificate(b)
}
var errDecodePEMBlockClientCert = errors.New("cannot decode PEM block from client certificate")
func extractClientCertificate(b []byte) (certificate string, err error) {
pemBlock, _ := pem.Decode(b)
if pemBlock == nil {
return "", errDecodePEMBlockClientCert
}
parsedBytes := pem.EncodeToMemory(pemBlock)
s := string(parsedBytes)
s = strings.ReplaceAll(s, "\n", "")
s = strings.TrimPrefix(s, "-----BEGIN CERTIFICATE-----")
s = strings.TrimSuffix(s, "-----END CERTIFICATE-----")
return s, nil
}

View File

@@ -1,174 +0,0 @@
package configuration
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_extractClientKey(t *testing.T) {
t.Parallel()
const validPEM = `
-----BEGIN PRIVATE KEY-----
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCrQDrezCptkWxX
ywm3KdXtvti+rPF3vfzOmXRKiKXDMpMxzoiaD5Wspirxxjr4C+B14xTwZjJZfxJL
2HpPdOeBmA5tmAoGUESspnzxR/N1T4Uggx0vlAzFo0UZ0sutV6CJK19Kk38REwlG
AB8gl6JYeSUuu6qREjrLVwFRH72acvC/p4jBki/MjAfEaeHc0yDJT9jpjpchw+Hx
Ymy+1BnfNTAfGDdTVx9qWb+ByQ7xfvzuD9AOeqiWApDzZIuDDsaWn2orv+syoJVo
rV52/F+75zks6+fzQ+0sotBlRyvsZKGi80F89RIHwG+5LNSuRDWnVvrwv1oc6V2/
lMidwT7yb0kXt0IRW6JzbaHyB2LkPazBlr6IPNupk83x9t2Buw0HI2SQKHMKOChU
i2/906yLUOo3QpAi3Wk1c/Xu9DvGR/pOA15WCakiAfG3Fq6hUxNncmpOMeOLF/ez
L19jZ3KA4E2Te4+GA0NYlXgkDbsIILWapHwqHXcDukynHisr7RawjrvXoLyasm4L
O66aNXK9wtipSMDA7tdlQP6Xe9bHflDHxwreiuEGxnrsvLU7LHBWdD7UT2/u8zdr
pimqi4L7W5p5aOBMn8jSVCO9+4CAxiLlc2qx5vb4/EPMsdQfacYP7vY9iVh/qPi3
bcUVGUlg8wAJDrTksxU1K3FVR7BEPwIDAQABAoICAAhyrbTJ+5nWH7MhCASqIqyM
yqJ1Y6AVlkAW397BaPP9Lbe6SZDYDfkrZVjx/3y3EUafgivtzrQNibiGIFqFGNqS
xrtvUadIFGsz91vrwb3aw2V8MldjhVHGoSUJ+hQ+C2RY6GWEazNLbhyu6tovwMl+
iHAKv/pSHOZlD2KSH0dcPjYmLJ/n90Wu7r8ovgSnwalMsBWtfBUlVaMTyOuNCQ2y
0QHnrusElD8p2EGtynftXMrdqtTcBi8IR2BKaHt5oiBSEum/mPmxZE16p/tUreBW
IsLtjE663htimMc2QJtzx2mDeIqSiGYrfxdyd7d1E/SCXPS9a9ObS42k6FSn8NPu
K5kN6fPV5EDM2CqKEt9QZPlyrjZIuffOZtJj0xPuTwhRle4SOtyjn2c/vsv9Fkrp
B6B1v7T4+SSOIedOYkL+FP/IexMNG/ZTB5Y2hrZ03JW9RGpFAa4//qGG2qUCR3hE
rVS6v58qO/3+TCFSn/TI8AfcTcJbes3yTbVyLH6NAjATfYqJDJJFf+PG0qKc8q1N
KvXmT+x4JiBBM32cOg11GPflxIZSKi9He50hnPGnC042N06ba/pkUPG49XwE37hn
kIGmcFGcDIMDTEZnPBogPFqGpepYdwGWxbadRiUoX2wgurHRRmA0YM32MjVky9C1
12Q/Jy9PIk/qdjYdWfAhAoIBAQDcvxfUx7MKMFgYYm4E51X+7B9QoxdhVaxcoVgK
VwfvedsLi0Bk1B1JVSXqnNfyDZbpxFz2v5Xd/dSit2rjnfBm+DoJYN9ZNnrbIH+s
qsO1DuHZvMZlRDJbpt7PpVH/pcf7rEWRY+avkMMsiGwI/ruDs17eu7jULeG7N4jb
kh1mdvF7K56O6Xe8jGJu5qaOPRWOkABK1cEOjQ5hB1iAwO/ua5hehP87SvbYzIhz
nQTE3AqTWgWbIyC4R85U7tS9hsXnSQ/ICM9pWcyN0Y667LwR2tX0QKl5M/YoM0sG
mw+VQED8O2R45jTzSAcox77dRg55Pp3Xexsp2iVvaTIeAaevAoIBAQDGmZS1gFO4
TEgQXHdmibLizDUHLuw662GC+3Hilx+nZBZtWOc6t8yquUyggSKQmBDiKAf0ipMe
xFao+5I3StJJ2P4Vel95Vcu8KgqCF736Q1iNgDHuW8ho8e0y+YE371x5co3POGC0
SfbcnRTXQx2+wWXzZDh+KtnaDUyDN12/qCIUyAuSVLwEM28ZFM3qadG1aUdCB5oe
o8jfgsg6YSukm4uE/tuI3/wAI7FkaCqvt/zkLauRff5FcNa7os4EKtNnGfebxscP
yFYpMsW9VI0rfmYz02gho33lnofs4o8x/gxh6t5zfVbsZ7vUiSDJBahWboG9aE99
OY2TKb6ibsBxAoIBAQChDBVR2oPnqg+Lcrw7fZ8Cxbeu992F2KBQUDHQEWCruSYy
zNwk84+OQb3Q5a6yXHG+iNEd//ZRp+8q60/jUgXiybRlxTQNfS6ykYo0Kb1wabQi
S5Qeq1tl/F9P9JfXQFafaTaz9MOHUMDjy3+uLFIXqpRLQX995R9rm/+P2ZDzgVF5
///E2dXOTElACax3117TzIE6F6qqeASGi3ppLNmfAwZ95t/inTVsRARE/MhO6w4Y
JLQ0U7N6XoDM/BVfVGUr8OS/lpXjkW0oBjvwaehnylUPxuEdmOg8ufdBkX0T8XW3
z4jkn2cAGouGl/vKqWLD2AgF/j16Ejn/hyrWM3TnAoIBAA6lSssrwIDJ11KljwSX
yQJirtJtymv56cIACwD7xhDRF7pOoRa6cTRx383CWCszm6Mh8pw9D+Zn8kAZ9Ulw
khtyDiLFWH8ZLaIds5Kub4siJkihGI2MZTYgCS8GKVpXo4ktQnnynWcOQU85okzR
nULw/jS5wlTDkjc7XdYbYiV9H65KplfPOeJRbLL7zsensBhhwCiFaP8zct/QxDVR
7yb/dYWESepJIktcVnuiFuvIdLTbDVj4YqT6UkuaEPlLszVaO+FYAlwOmRQGs4Bn
2NVJR/4wa/B3HxSs4Tc96fN02bLq4CbCKoPajoZ46lsIuMZO9fBi3eHNObyNiopu
AnECggEBAJiJ0tK/PGh+Q9uv57Z4QcmbawoxMQW1qK/rLYwacYsSpzo8VhbZf+Jh
8biMg9AIQsLWnqmB3gmndePArGXkSxnilRozNLaeclTZy7rh00BctTEfgee4Kxdi
JKkJlVK0CE8I6txVRqkoOMyxsk1kRZ4l2yW2nxzyWlJKwvD75x2PQ6xWvpLAggyn
q00I3MzNIgR123jytN1NyC7l+mnGoC23ToXM7B3/PQjGYTq3jawKomrX1cmwzKBT
+pzjtJSWvMeUEZQS1PpOhxpPBRHagdKXt+ug2DqDtU6rfpDGtTBh5QNkg5SA7lxZ
zZjrL52saevO25cigVl+hxcnY8DTpbk=
-----END PRIVATE KEY-----
`
const validKeyString = "MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCrQDrezCptkWxXywm3KdXtvti+rPF3vfzOmXRKiKXDMpMxzoiaD5Wspirxxjr4C+B14xTwZjJZfxJL2HpPdOeBmA5tmAoGUESspnzxR/N1T4Uggx0vlAzFo0UZ0sutV6CJK19Kk38REwlGAB8gl6JYeSUuu6qREjrLVwFRH72acvC/p4jBki/MjAfEaeHc0yDJT9jpjpchw+HxYmy+1BnfNTAfGDdTVx9qWb+ByQ7xfvzuD9AOeqiWApDzZIuDDsaWn2orv+syoJVorV52/F+75zks6+fzQ+0sotBlRyvsZKGi80F89RIHwG+5LNSuRDWnVvrwv1oc6V2/lMidwT7yb0kXt0IRW6JzbaHyB2LkPazBlr6IPNupk83x9t2Buw0HI2SQKHMKOChUi2/906yLUOo3QpAi3Wk1c/Xu9DvGR/pOA15WCakiAfG3Fq6hUxNncmpOMeOLF/ezL19jZ3KA4E2Te4+GA0NYlXgkDbsIILWapHwqHXcDukynHisr7RawjrvXoLyasm4LO66aNXK9wtipSMDA7tdlQP6Xe9bHflDHxwreiuEGxnrsvLU7LHBWdD7UT2/u8zdrpimqi4L7W5p5aOBMn8jSVCO9+4CAxiLlc2qx5vb4/EPMsdQfacYP7vY9iVh/qPi3bcUVGUlg8wAJDrTksxU1K3FVR7BEPwIDAQABAoICAAhyrbTJ+5nWH7MhCASqIqyMyqJ1Y6AVlkAW397BaPP9Lbe6SZDYDfkrZVjx/3y3EUafgivtzrQNibiGIFqFGNqSxrtvUadIFGsz91vrwb3aw2V8MldjhVHGoSUJ+hQ+C2RY6GWEazNLbhyu6tovwMl+iHAKv/pSHOZlD2KSH0dcPjYmLJ/n90Wu7r8ovgSnwalMsBWtfBUlVaMTyOuNCQ2y0QHnrusElD8p2EGtynftXMrdqtTcBi8IR2BKaHt5oiBSEum/mPmxZE16p/tUreBWIsLtjE663htimMc2QJtzx2mDeIqSiGYrfxdyd7d1E/SCXPS9a9ObS42k6FSn8NPuK5kN6fPV5EDM2CqKEt9QZPlyrjZIuffOZtJj0xPuTwhRle4SOtyjn2c/vsv9FkrpB6B1v7T4+SSOIedOYkL+FP/IexMNG/ZTB5Y2hrZ03JW9RGpFAa4//qGG2qUCR3hErVS6v58qO/3+TCFSn/TI8AfcTcJbes3yTbVyLH6NAjATfYqJDJJFf+PG0qKc8q1NKvXmT+x4JiBBM32cOg11GPflxIZSKi9He50hnPGnC042N06ba/pkUPG49XwE37hnkIGmcFGcDIMDTEZnPBogPFqGpepYdwGWxbadRiUoX2wgurHRRmA0YM32MjVky9C112Q/Jy9PIk/qdjYdWfAhAoIBAQDcvxfUx7MKMFgYYm4E51X+7B9QoxdhVaxcoVgKVwfvedsLi0Bk1B1JVSXqnNfyDZbpxFz2v5Xd/dSit2rjnfBm+DoJYN9ZNnrbIH+sqsO1DuHZvMZlRDJbpt7PpVH/pcf7rEWRY+avkMMsiGwI/ruDs17eu7jULeG7N4jbkh1mdvF7K56O6Xe8jGJu5qaOPRWOkABK1cEOjQ5hB1iAwO/ua5hehP87SvbYzIhznQTE3AqTWgWbIyC4R85U7tS9hsXnSQ/ICM9pWcyN0Y667LwR2tX0QKl5M/YoM0sGmw+VQED8O2R45jTzSAcox77dRg55Pp3Xexsp2iVvaTIeAaevAoIBAQDGmZS1gFO4TEgQXHdmibLizDUHLuw662GC+3Hilx+nZBZtWOc6t8yquUyggSKQmBDiKAf0ipMexFao+5I3StJJ2P4Vel95Vcu8KgqCF736Q1iNgDHuW8ho8e0y+YE371x5co3POGC0SfbcnRTXQx2+wWXzZDh+KtnaDUyDN12/qCIUyAuSVLwEM28ZFM3qadG1aUdCB5oeo8jfgsg6YSukm4uE/tuI3/wAI7FkaCqvt/zkLauRff5FcNa7os4EKtNnGfebxscPyFYpMsW9VI0rfmYz02gho33lnofs4o8x/gxh6t5zfVbsZ7vUiSDJBahWboG9aE99OY2TKb6ibsBxAoIBAQChDBVR2oPnqg+Lcrw7fZ8Cxbeu992F2KBQUDHQEWCruSYyzNwk84+OQb3Q5a6yXHG+iNEd//ZRp+8q60/jUgXiybRlxTQNfS6ykYo0Kb1wabQiS5Qeq1tl/F9P9JfXQFafaTaz9MOHUMDjy3+uLFIXqpRLQX995R9rm/+P2ZDzgVF5///E2dXOTElACax3117TzIE6F6qqeASGi3ppLNmfAwZ95t/inTVsRARE/MhO6w4YJLQ0U7N6XoDM/BVfVGUr8OS/lpXjkW0oBjvwaehnylUPxuEdmOg8ufdBkX0T8XW3z4jkn2cAGouGl/vKqWLD2AgF/j16Ejn/hyrWM3TnAoIBAA6lSssrwIDJ11KljwSXyQJirtJtymv56cIACwD7xhDRF7pOoRa6cTRx383CWCszm6Mh8pw9D+Zn8kAZ9UlwkhtyDiLFWH8ZLaIds5Kub4siJkihGI2MZTYgCS8GKVpXo4ktQnnynWcOQU85okzRnULw/jS5wlTDkjc7XdYbYiV9H65KplfPOeJRbLL7zsensBhhwCiFaP8zct/QxDVR7yb/dYWESepJIktcVnuiFuvIdLTbDVj4YqT6UkuaEPlLszVaO+FYAlwOmRQGs4Bn2NVJR/4wa/B3HxSs4Tc96fN02bLq4CbCKoPajoZ46lsIuMZO9fBi3eHNObyNiopuAnECggEBAJiJ0tK/PGh+Q9uv57Z4QcmbawoxMQW1qK/rLYwacYsSpzo8VhbZf+Jh8biMg9AIQsLWnqmB3gmndePArGXkSxnilRozNLaeclTZy7rh00BctTEfgee4KxdiJKkJlVK0CE8I6txVRqkoOMyxsk1kRZ4l2yW2nxzyWlJKwvD75x2PQ6xWvpLAggynq00I3MzNIgR123jytN1NyC7l+mnGoC23ToXM7B3/PQjGYTq3jawKomrX1cmwzKBT+pzjtJSWvMeUEZQS1PpOhxpPBRHagdKXt+ug2DqDtU6rfpDGtTBh5QNkg5SA7lxZzZjrL52saevO25cigVl+hxcnY8DTpbk=" //nolint:lll
testCases := map[string]struct {
b []byte
key string
err error
}{
"no input": {
err: errDecodePEMBlockClientKey,
},
"bad input": {
b: []byte{1, 2, 3},
err: errDecodePEMBlockClientKey,
},
"valid key": {
b: []byte(validPEM),
key: validKeyString,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
key, err := extractClientKey(testCase.b)
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.key, key)
})
}
}
func Test_extractClientCertificate(t *testing.T) {
t.Parallel()
const validPEM = `
-----BEGIN CERTIFICATE-----
MIIGrDCCBJSgAwIBAgIEAdTnfTANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJS
TzESMBAGA1UEBxMJQnVjaGFyZXN0MRgwFgYDVQQKEw9DeWJlckdob3N0IFMuQS4x
GzAZBgNVBAMTEkN5YmVyR2hvc3QgUm9vdCBDQTEhMB8GCSqGSIb3DQEJARYSaW5m
b0BjeWJlcmdob3N0LnJvMB4XDTIwMDcwNDE1MjkzNloXDTMwMDcwMjE1MjkzNlow
fTELMAkGA1UEBhMCUk8xEjAQBgNVBAcMCUJ1Y2hhcmVzdDEYMBYGA1UECgwPQ3li
ZXJHaG9zdCBTLkEuMR0wGwYDVQQDDBRjLmoua2xhdmVyQGdtYWlsLmNvbTEhMB8G
CSqGSIb3DQEJARYSaW5mb0BjeWJlcmdob3N0LnJvMIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEAobp2NlGUHMNBe08YEOnVG3QJjF3ZaXbRhE/II9rmtgJT
NZtDohGChvFlNRsExKzVrKxHCeuJkVffwzQ6fYk4/M1RdYLJUh0UVw3e4WdApw8E
7TJZxDYm4SHQNXUvt1Rt5TjslcXxIpDZgrMSc/kHROYEL9tdgdzPZErUJehXyJPh
EzIrzmAJh501x7WwKPz9ctSVlItyavqEWFF2vyUa6X9DYmD9mQTz5c+VXNO5DkXm
PFBIaEVDnvFtcjGJ56yEvFnWVukL+OUX7ezowrIOFOcp9udjgpeiHq+XvsQ6ER0D
Jt25MiEId3NjkxtZ8BitDftTcLN/kt81hWKT7adMVc3kpIZ80cxrwRCttMd7sHAz
KI9u7pMxv10eUOsIEY87ewBe3l6KvEnjA+9uIjim6gLLebDIaEH50Ee9PzNJ8fqQ
2u54Ab4bt00/H1sUnJ6Ss/+WsQDOK1BsPRKKcnHZntOlHrs2Tu5+txKNU2cOapI8
SjVULUNKrRXASbpfWnLUfri/HO742bJb/TjkOJcOxta3hTPFAhaRWBusVlB41XVH
euH5DAhugYXeSNK6/6Ul8YvKUNH/7QbxuGIGXfth19Xl4QLI1umyEjZopSlt3tOi
O2V1soVNSQCCfxXVoCTMESMLjhkjWdmBDhdy2GTW7S4YoJfqVKiS18rYkN7I4ZMC
AwEAAaOCATQwggEwMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMDQGCWCGSAGG+EIB
DQQnFiVDeWJlckdob3N0IEdlbmVyYXRlZCBVc2VyIENlcnRpZmljYXRlMBEGCWCG
SAGG+EIBAQQEAwIHgDAdBgNVHQ4EFgQULwUtU5s6pL2NN9gPeEnKX0dhwiswga0G
A1UdIwSBpTCBooAU6tdK1g/He5qzjeAoM5eHt4in9iWhf6R9MHsxCzAJBgNVBAYT
AlJPMRIwEAYDVQQHEwlCdWNoYXJlc3QxGDAWBgNVBAoTD0N5YmVyR2hvc3QgUy5B
LjEbMBkGA1UEAxMSQ3liZXJHaG9zdCBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJp
bmZvQGN5YmVyZ2hvc3Qucm+CCQCcVButZsQ0uzANBgkqhkiG9w0BAQsFAAOCAgEA
ystGIMYhQWaEdTqlnLCytrr8657t+PuidZMNNIaPB3wN2Fi2xKf14DTg03mqxjmP
Pb+f+PVNIOV5PdWD4jcQwOP1GEboGV0DFzlRGeAtDcvKwdee4oASJbZq1CETqDao
hQTxKEWC+UBk2F36nOaEI6Sab+Mb4cR9//PAwvzOqrXuGF5NuIOX7eFtCMQSgQq6
lRRqTQjekm0Dxigx4JA92Jo2qZRwCJ0T3IXBJGL831HCFJbDWv8PV3lsfFb/i2+v
r54uywFQVWWp18dYi97gipfuQ4zRg2Ldx5aXSmnhhKpg5ioZvtk043QofF12YORh
obElqavRbvvhZvlCouvcuoq9QKi7IPe5SJZkZ1X7ezMesCwBzwFpt6vRUAcslsNF
bcYS1iSENlY/PTcDqBhbKuc9yAhq+/aUgaY/8VF5RWVzSRZufbf3BPwOkE4K0Uyb
aobO/YX0JOkCacAD+4tdR6YSXNIMMRAOCBQvxbxFXaHzhwhzBAjdsC56FrJKwXvQ
rRLU3tF4P0zFMeNTay8uTtUXugDK7EnklLESuYdpUJ8bUMlAUhJBi6UFI9/icMud
xXvLRvhnBW9EtKib5JnVFUovcEUt+3EJbyst05nkL4YPjQS4TC9DHdo5SyRAy1Tp
iOCYTbretAFZRhh6ycUN5hBeN8GMQxiMreMtDV4PEIQ=
-----END CERTIFICATE-----
`
const validCertificateString = "MIIGrDCCBJSgAwIBAgIEAdTnfTANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJSTzESMBAGA1UEBxMJQnVjaGFyZXN0MRgwFgYDVQQKEw9DeWJlckdob3N0IFMuQS4xGzAZBgNVBAMTEkN5YmVyR2hvc3QgUm9vdCBDQTEhMB8GCSqGSIb3DQEJARYSaW5mb0BjeWJlcmdob3N0LnJvMB4XDTIwMDcwNDE1MjkzNloXDTMwMDcwMjE1MjkzNlowfTELMAkGA1UEBhMCUk8xEjAQBgNVBAcMCUJ1Y2hhcmVzdDEYMBYGA1UECgwPQ3liZXJHaG9zdCBTLkEuMR0wGwYDVQQDDBRjLmoua2xhdmVyQGdtYWlsLmNvbTEhMB8GCSqGSIb3DQEJARYSaW5mb0BjeWJlcmdob3N0LnJvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAobp2NlGUHMNBe08YEOnVG3QJjF3ZaXbRhE/II9rmtgJTNZtDohGChvFlNRsExKzVrKxHCeuJkVffwzQ6fYk4/M1RdYLJUh0UVw3e4WdApw8E7TJZxDYm4SHQNXUvt1Rt5TjslcXxIpDZgrMSc/kHROYEL9tdgdzPZErUJehXyJPhEzIrzmAJh501x7WwKPz9ctSVlItyavqEWFF2vyUa6X9DYmD9mQTz5c+VXNO5DkXmPFBIaEVDnvFtcjGJ56yEvFnWVukL+OUX7ezowrIOFOcp9udjgpeiHq+XvsQ6ER0DJt25MiEId3NjkxtZ8BitDftTcLN/kt81hWKT7adMVc3kpIZ80cxrwRCttMd7sHAzKI9u7pMxv10eUOsIEY87ewBe3l6KvEnjA+9uIjim6gLLebDIaEH50Ee9PzNJ8fqQ2u54Ab4bt00/H1sUnJ6Ss/+WsQDOK1BsPRKKcnHZntOlHrs2Tu5+txKNU2cOapI8SjVULUNKrRXASbpfWnLUfri/HO742bJb/TjkOJcOxta3hTPFAhaRWBusVlB41XVHeuH5DAhugYXeSNK6/6Ul8YvKUNH/7QbxuGIGXfth19Xl4QLI1umyEjZopSlt3tOiO2V1soVNSQCCfxXVoCTMESMLjhkjWdmBDhdy2GTW7S4YoJfqVKiS18rYkN7I4ZMCAwEAAaOCATQwggEwMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMDQGCWCGSAGG+EIBDQQnFiVDeWJlckdob3N0IEdlbmVyYXRlZCBVc2VyIENlcnRpZmljYXRlMBEGCWCGSAGG+EIBAQQEAwIHgDAdBgNVHQ4EFgQULwUtU5s6pL2NN9gPeEnKX0dhwiswga0GA1UdIwSBpTCBooAU6tdK1g/He5qzjeAoM5eHt4in9iWhf6R9MHsxCzAJBgNVBAYTAlJPMRIwEAYDVQQHEwlCdWNoYXJlc3QxGDAWBgNVBAoTD0N5YmVyR2hvc3QgUy5BLjEbMBkGA1UEAxMSQ3liZXJHaG9zdCBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJpbmZvQGN5YmVyZ2hvc3Qucm+CCQCcVButZsQ0uzANBgkqhkiG9w0BAQsFAAOCAgEAystGIMYhQWaEdTqlnLCytrr8657t+PuidZMNNIaPB3wN2Fi2xKf14DTg03mqxjmPPb+f+PVNIOV5PdWD4jcQwOP1GEboGV0DFzlRGeAtDcvKwdee4oASJbZq1CETqDaohQTxKEWC+UBk2F36nOaEI6Sab+Mb4cR9//PAwvzOqrXuGF5NuIOX7eFtCMQSgQq6lRRqTQjekm0Dxigx4JA92Jo2qZRwCJ0T3IXBJGL831HCFJbDWv8PV3lsfFb/i2+vr54uywFQVWWp18dYi97gipfuQ4zRg2Ldx5aXSmnhhKpg5ioZvtk043QofF12YORhobElqavRbvvhZvlCouvcuoq9QKi7IPe5SJZkZ1X7ezMesCwBzwFpt6vRUAcslsNFbcYS1iSENlY/PTcDqBhbKuc9yAhq+/aUgaY/8VF5RWVzSRZufbf3BPwOkE4K0UybaobO/YX0JOkCacAD+4tdR6YSXNIMMRAOCBQvxbxFXaHzhwhzBAjdsC56FrJKwXvQrRLU3tF4P0zFMeNTay8uTtUXugDK7EnklLESuYdpUJ8bUMlAUhJBi6UFI9/icMudxXvLRvhnBW9EtKib5JnVFUovcEUt+3EJbyst05nkL4YPjQS4TC9DHdo5SyRAy1TpiOCYTbretAFZRhh6ycUN5hBeN8GMQxiMreMtDV4PEIQ=" //nolint:lll
testCases := map[string]struct {
b []byte
certificate string
err error
}{
"no input": {
err: errDecodePEMBlockClientCert,
},
"bad input": {
b: []byte{1, 2, 3},
err: errDecodePEMBlockClientCert,
},
"valid key": {
b: []byte(validPEM),
certificate: validCertificateString,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
certificate, err := extractClientCertificate(testCase.b)
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.certificate, certificate)
})
}
}

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,74 +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.env)
if err != nil {
return err
}
return settings.ServerSelection.Wireguard.readMullvad(r.env)
}
func (settings *OpenVPNSelection) readMullvad(env params.Interface) (err error) {
settings.TCP, err = readProtocol(env)
if err != nil {
return err
}
settings.CustomPort, err = readOpenVPNCustomPort(env, settings.TCP,
[]uint16{80, 443, 1401}, []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.CustomPort, err = readWireguardCustomPort(env, nil)
if err != nil {
return err
}
return nil
}

View File

@@ -1,63 +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.Names, err = r.env.CSVInside("SERVER_NAME", constants.NordvpnHostnameChoices(servers))
if err != nil {
return fmt.Errorf("environment variable SERVER_NAME: %w", err)
}
settings.ServerSelection.Numbers, err = readNordVPNServerNumbers(r.env)
if err != nil {
return err
}
return settings.ServerSelection.OpenVPN.readProtocolOnly(r.env)
}
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,204 +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"`
Cipher string `json:"cipher"`
Auth string `json:"auth"`
Config string `json:"custom_config"`
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.Cipher) > 0 {
lines = append(lines, indent+lastIndent+"Custom cipher: "+settings.Cipher)
}
if len(settings.Auth) > 0 {
lines = append(lines, indent+lastIndent+"Custom auth algorithm: "+settings.Auth)
}
if len(settings.Config) > 0 {
lines = append(lines, indent+lastIndent+"Custom configuration: "+settings.Config)
}
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) {
settings.Config, err = r.env.Get("OPENVPN_CUSTOM_CONFIG", params.CaseSensitiveValue())
if err != nil {
return fmt.Errorf("environment variable OPENVPN_CUSTOM_CONFIG: %w", err)
}
credentialsRequired := settings.Config == "" && serviceProvider != constants.VPNUnlimited
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("yes"))
if err != nil {
return fmt.Errorf("environment variable OPENVPN_ROOT: %w", err)
}
settings.Cipher, err = r.env.Get("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
}
settings.EncPreset, err = getPIAEncryptionPreset(r)
if err != nil {
return err
}
switch serviceProvider {
case constants.Cyberghost:
err = settings.readCyberghost(r)
case constants.VPNUnlimited:
err = settings.readVPNUnlimited(r)
}
if err != nil {
return err
}
return nil
}
func readProtocol(env params.Interface) (tcp bool, err error) {
protocol, err := env.Inside("PROTOCOL", []string{constants.TCP, constants.UDP}, params.Default(constants.UDP))
if err != nil {
return false, fmt.Errorf("environment variable 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,39 +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{},
}
data, err := json.MarshalIndent(in, "", " ")
require.NoError(t, err)
assert.Equal(t, `{
"user": "",
"password": "",
"verbosity": 0,
"flags": [],
"mssfix": 0,
"run_as_root": true,
"cipher": "",
"auth": "",
"custom_config": "",
"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,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.Hostnames, 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 = readPortOrZero(r.env, "PORT")
if err != nil {
return fmt.Errorf("environment variable PORT: %w", 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.PIACertificateStrong),
)
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.env)
}

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

View File

@@ -1,201 +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.Cyberghost:
err = settings.readCyberghost(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.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.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{
"cyberghost", "fastestvpn", "hidemyass", "ipvanish", "ivpn", "mullvad", "nordvpn",
"privado", "pia", "private internet access", "privatevpn", "protonvpn",
"purevpn", "surfshark", "torguard", constants.VPNUnlimited, "vyprvpn", "windscribe"}
case constants.Wireguard:
allowedVPNServiceProviders = []string{constants.Mullvad, constants.Windscribe,
constants.Ivpn}
}
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"
}
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
}
func readOpenVPNCustomPort(env params.Interface, tcp bool,
allowedTCP, allowedUDP []uint16) (port uint16, err error) {
port, err = readPortOrZero(env, "PORT")
if err != nil {
return 0, fmt.Errorf("environment variable PORT: %w", err)
} else if port == 0 {
return 0, nil
}
if tcp {
for i := range allowedTCP {
if allowedTCP[i] == port {
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(allowedTCP))
}
for i := range allowedUDP {
if allowedUDP[i] == port {
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(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_PORT")
if err != nil {
return 0, fmt.Errorf("environment variable WIREGUARD_PORT: %w", err)
} else if port == 0 {
return 0, nil
}
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, ", ")
}

View File

@@ -1,406 +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,
Groups: []string{"group"},
Regions: []string{"a", "El country"},
},
},
lines: []string{
"|--Cyberghost settings:",
" |--Server groups: group",
" |--Regions: a, El country",
" |--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",
},
},
"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",
},
},
"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 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("PROTOCOL", []string{"tcp", "udp"}, gomock.Any()).
Return(testCase.mockStr, testCase.mockErr)
tcp, err := readProtocol(env)
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.env)
}

View File

@@ -1,126 +0,0 @@
package configuration
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/params"
"github.com/qdm12/golibs/verification"
)
type reader struct {
servers models.AllServers
env params.Interface
logger logging.Logger
regex verification.Regex
}
func newReader(env params.Interface,
servers models.AllServers, logger logging.Logger) reader {
return reader{
servers: servers,
env: env,
logger: logger,
regex: verification.NewRegex(),
}
}
func (r *reader) onRetroActive(oldKey, newKey string) {
r.logger.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 "", envErr
}
return "", nil
} else if fileErr != nil {
return "", fmt.Errorf("%w: %s", ErrReadSecretFile, fileErr)
}
b, err := io.ReadAll(file)
if err != nil {
return "", fmt.Errorf("%w: %s", ErrReadSecretFile, err)
}
value = string(b)
value = cleanSuffix(value)
if compulsory && value == "" {
return "", 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,173 +0,0 @@
package configuration
import (
"fmt"
"net"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
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"`
// Cyberghost
Groups []string `json:"groups"`
// Fastestvpn, HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited
Countries []string `json:"countries"`
// HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited, Windscribe
Cities []string `json:"cities"`
// Fastestvpn, HideMyAss, IPVanish, IVPN, PrivateVPN, Windscribe, Privado, Protonvpn, VPNUnlimited
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.Groups) > 0 {
lines = append(lines, lastIndent+"Server groups: "+commaJoin(selection.Groups))
}
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 {
TCP bool `json:"tcp"` // UDP if TCP is false
CustomPort uint16 `json:"custom_port"` // HideMyAss, Mullvad, PIA, ProtonVPN, 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:")
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(env params.Interface) (err error) {
settings.TCP, err = readProtocol(env)
return err
}
func (settings *OpenVPNSelection) readProtocolAndPort(env params.Interface) (err error) {
settings.TCP, err = readProtocol(env)
if err != nil {
return err
}
settings.CustomPort, err = readPortOrZero(env, "PORT")
if err != nil {
return fmt.Errorf("environment variable PORT: %w", err)
}
return nil
}
type WireguardSelection struct {
CustomPort uint16 `json:"custom_port"` // Mullvad
}
func (settings *WireguardSelection) lines() (lines []string) {
lines = append(lines, lastIndent+"Wireguard selection:")
if settings.CustomPort != 0 {
lines = append(lines, indent+lastIndent+"Custom port: "+fmt.Sprint(settings.CustomPort))
}
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.logger.Warn(warning)
}
if err != nil {
return fmt.Errorf("environment variable HTTP_CONTROL_SERVER_PORT: %w", err)
}
return nil
}

View File

@@ -1,126 +0,0 @@
package configuration
import (
"errors"
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"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,
logger logging.Logger) (err error) {
r := newReader(env, servers, logger)
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,44 @@
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")
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, "github.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,190 @@
package helpers
import (
"net"
"time"
"github.com/qdm12/golibs/logging"
"inet.af/netaddr"
)
func CopyStringPtr(original *string) (copied *string) {
if original == nil {
return nil
}
copied = new(string)
*copied = *original
return copied
}
func CopyBoolPtr(original *bool) (copied *bool) {
if original == nil {
return nil
}
copied = new(bool)
*copied = *original
return copied
}
func CopyUint8Ptr(original *uint8) (copied *uint8) {
if original == nil {
return nil
}
copied = new(uint8)
*copied = *original
return copied
}
func CopyUint16Ptr(original *uint16) (copied *uint16) {
if original == nil {
return nil
}
copied = new(uint16)
*copied = *original
return copied
}
func CopyIntPtr(original *int) (copied *int) {
if original == nil {
return nil
}
copied = new(int)
*copied = *original
return copied
}
func CopyDurationPtr(original *time.Duration) (copied *time.Duration) {
if original == nil {
return nil
}
copied = new(time.Duration)
*copied = *original
return copied
}
func CopyLogLevelPtr(original *logging.Level) (copied *logging.Level) {
if original == nil {
return nil
}
copied = new(logging.Level)
*copied = *original
return copied
}
func CopyIP(original net.IP) (copied net.IP) {
if original == nil {
return nil
}
copied = make(net.IP, len(original))
copy(copied, original)
return copied
}
func CopyIPNet(original net.IPNet) (copied net.IPNet) {
if original.IP != nil {
copied.IP = make(net.IP, len(original.IP))
copy(copied.IP, original.IP)
}
if original.Mask != nil {
copied.Mask = make(net.IPMask, len(original.Mask))
copy(copied.Mask, original.Mask)
}
return copied
}
func CopyIPNetPtr(original *net.IPNet) (copied *net.IPNet) {
if original == nil {
return nil
}
copied = new(net.IPNet)
*copied = CopyIPNet(*original)
return copied
}
func CopyNetaddrIP(original netaddr.IP) (copied netaddr.IP) {
b, err := original.MarshalBinary()
if err != nil {
panic(err)
}
err = copied.UnmarshalBinary(b)
if err != nil {
panic(err)
}
return copied
}
func CopyIPPrefix(original netaddr.IPPrefix) (copied netaddr.IPPrefix) {
b, err := original.MarshalText()
if err != nil {
panic(err)
}
err = copied.UnmarshalText(b)
if err != nil {
panic(err)
}
return copied
}
func CopyStringSlice(original []string) (copied []string) {
if original == nil {
return nil
}
copied = make([]string, len(original))
copy(copied, original)
return copied
}
func CopyUint16Slice(original []uint16) (copied []uint16) {
if original == nil {
return nil
}
copied = make([]uint16, len(original))
copy(copied, original)
return copied
}
func CopyIPNetSlice(original []net.IPNet) (copied []net.IPNet) {
if original == nil {
return nil
}
copied = make([]net.IPNet, len(original))
for i := range original {
copied[i] = CopyIPNet(original[i])
}
return copied
}
func CopyIPPrefixSlice(original []netaddr.IPPrefix) (copied []netaddr.IPPrefix) {
if original == nil {
return nil
}
copied = make([]netaddr.IPPrefix, len(original))
for i := range original {
copied[i] = CopyIPPrefix(original[i])
}
return copied
}
func CopyNetaddrIPsSlice(original []netaddr.IP) (copied []netaddr.IP) {
if original == nil {
return nil
}
copied = make([]netaddr.IP, len(original))
for i := range original {
copied[i] = CopyNetaddrIP(original[i])
}
return copied
}

View File

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

View File

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

View File

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

View File

@@ -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/golibs/logging"
"github.com/qdm12/gotree"
)
// 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 *logging.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, logging.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,335 @@
package settings
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/openvpn/parse"
"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
}
func (o OpenVPN) validate(vpnProvider string) (err error) {
// Validate version
validVersions := []string{constants.Openvpn24, constants.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 == constants.Custom
if !isCustom && o.User == "" {
return ErrOpenVPNUserIsEmpty
}
if !isCustom && 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
}
return nil
}
func validateOpenVPNClientCertificate(vpnProvider,
clientCert string) (err error) {
switch vpnProvider {
case
constants.Cyberghost,
constants.VPNUnlimited:
if clientCert == "" {
return ErrMissingValue
}
}
if clientCert == "" {
return nil
}
_, err = parse.ExtractCert([]byte(clientCert))
if err != nil {
return err
}
return nil
}
func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
switch vpnProvider {
case
constants.Cyberghost,
constants.VPNUnlimited,
constants.Wevpn:
if clientKey == "" {
return ErrMissingValue
}
}
if clientKey == "" {
return nil
}
_, err = parse.ExtractPrivateKey([]byte(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, constants.Openvpn25)
if vpnProvider == constants.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 == constants.PrivateInternetAccess {
defaultEncPreset = constants.PIAEncryptionPresetStrong
}
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,172 @@
package settings
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants"
"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,
constants.Ipvanish,
constants.Perfectprivacy,
constants.Privado,
constants.VPNUnlimited,
constants.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 constants.Cyberghost, constants.HideMyAss,
constants.PrivateInternetAccess, constants.Privatevpn,
constants.Protonvpn, constants.Torguard:
// no custom port allowed
case constants.Expressvpn, constants.Fastestvpn,
constants.Ipvanish, constants.Nordvpn,
constants.Privado, constants.Purevpn,
constants.Surfshark, constants.VPNUnlimited,
constants.Vyprvpn:
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
default:
var allowedTCP, allowedUDP []uint16
switch vpnProvider {
case constants.Ivpn:
allowedTCP = []uint16{80, 443, 1143}
allowedUDP = []uint16{53, 1194, 2049, 2050}
case constants.Mullvad:
allowedTCP = []uint16{80, 443, 1401}
allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}
case constants.Perfectprivacy:
allowedTCP = []uint16{44, 443, 4433}
allowedUDP = []uint16{44, 443, 4433}
case constants.Wevpn:
allowedTCP = []uint16{53, 1195, 1199, 2018}
allowedUDP = []uint16{80, 1194, 1198}
case constants.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 == constants.PrivateInternetAccess {
validEncryptionPresets := []string{
constants.PIAEncryptionPresetNone,
constants.PIAEncryptionPresetNormal,
constants.PIAEncryptionPresetStrong,
}
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 == constants.PrivateInternetAccess {
defaultEncPreset = constants.PIAEncryptionPresetStrong
}
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"
"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{constants.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"
"github.com/qdm12/gluetun/internal/models"
"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, allServers models.AllServers) (err error) {
// Validate Name
var validNames []string
if vpnType == constants.OpenVPN {
validNames = constants.AllProviders()
validNames = append(validNames, "pia") // Retro-compatibility
} else { // Wireguard
validNames = []string{
constants.Custom,
constants.Ivpn,
constants.Mullvad,
constants.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, allServers)
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, constants.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,432 @@
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"
"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,
allServers models.AllServers) (err error) {
switch ss.VPN {
case constants.OpenVPN, constants.Wireguard:
default:
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
}
countryChoices, regionChoices, cityChoices,
ispChoices, nameChoices, hostnameChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, allServers)
if err != nil {
return err // already wrapped error
}
err = validateServerFilters(*ss, countryChoices, regionChoices, cityChoices,
ispChoices, nameChoices, hostnameChoices)
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 != constants.Mullvad {
return fmt.Errorf("%w: for VPN service provider %s",
ErrOwnedOnlyNotSupported, vpnServiceProvider)
}
if *ss.FreeOnly &&
!helpers.IsOneOf(vpnServiceProvider,
constants.Protonvpn,
constants.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrFreeOnlyNotSupported, vpnServiceProvider)
}
if *ss.StreamOnly &&
!helpers.IsOneOf(vpnServiceProvider,
constants.Protonvpn,
constants.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrStreamOnlyNotSupported, vpnServiceProvider)
}
if *ss.MultiHopOnly &&
vpnServiceProvider != constants.Surfshark {
return fmt.Errorf("%w: for VPN service provider %s",
ErrMultiHopOnlyNotSupported, vpnServiceProvider)
}
if ss.VPN == constants.OpenVPN {
err = ss.OpenVPN.validate(vpnServiceProvider)
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,
allServers models.AllServers) (
countryChoices, regionChoices, cityChoices,
ispChoices, nameChoices, hostnameChoices []string,
err error) {
switch vpnServiceProvider {
case constants.Custom:
case constants.Cyberghost:
servers := allServers.GetCyberghost()
countryChoices = validation.CyberghostCountryChoices(servers)
hostnameChoices = validation.CyberghostHostnameChoices(servers)
case constants.Expressvpn:
servers := allServers.GetExpressvpn()
countryChoices = validation.ExpressvpnCountriesChoices(servers)
cityChoices = validation.ExpressvpnCityChoices(servers)
hostnameChoices = validation.ExpressvpnHostnameChoices(servers)
case constants.Fastestvpn:
servers := allServers.GetFastestvpn()
countryChoices = validation.FastestvpnCountriesChoices(servers)
hostnameChoices = validation.FastestvpnHostnameChoices(servers)
case constants.HideMyAss:
servers := allServers.GetHideMyAss()
countryChoices = validation.HideMyAssCountryChoices(servers)
regionChoices = validation.HideMyAssRegionChoices(servers)
cityChoices = validation.HideMyAssCityChoices(servers)
hostnameChoices = validation.HideMyAssHostnameChoices(servers)
case constants.Ipvanish:
servers := allServers.GetIpvanish()
countryChoices = validation.IpvanishCountryChoices(servers)
cityChoices = validation.IpvanishCityChoices(servers)
hostnameChoices = validation.IpvanishHostnameChoices(servers)
case constants.Ivpn:
servers := allServers.GetIvpn()
countryChoices = validation.IvpnCountryChoices(servers)
cityChoices = validation.IvpnCityChoices(servers)
ispChoices = validation.IvpnISPChoices(servers)
hostnameChoices = validation.IvpnHostnameChoices(servers)
case constants.Mullvad:
servers := allServers.GetMullvad()
countryChoices = validation.MullvadCountryChoices(servers)
cityChoices = validation.MullvadCityChoices(servers)
ispChoices = validation.MullvadISPChoices(servers)
hostnameChoices = validation.MullvadHostnameChoices(servers)
case constants.Nordvpn:
servers := allServers.GetNordvpn()
regionChoices = validation.NordvpnRegionChoices(servers)
hostnameChoices = validation.NordvpnHostnameChoices(servers)
case constants.Perfectprivacy:
servers := allServers.GetPerfectprivacy()
cityChoices = validation.PerfectprivacyCityChoices(servers)
case constants.Privado:
servers := allServers.GetPrivado()
countryChoices = validation.PrivadoCountryChoices(servers)
regionChoices = validation.PrivadoRegionChoices(servers)
cityChoices = validation.PrivadoCityChoices(servers)
hostnameChoices = validation.PrivadoHostnameChoices(servers)
case constants.PrivateInternetAccess:
servers := allServers.GetPia()
regionChoices = validation.PIAGeoChoices(servers)
hostnameChoices = validation.PIAHostnameChoices(servers)
nameChoices = validation.PIANameChoices(servers)
case constants.Privatevpn:
servers := allServers.GetPrivatevpn()
countryChoices = validation.PrivatevpnCountryChoices(servers)
cityChoices = validation.PrivatevpnCityChoices(servers)
hostnameChoices = validation.PrivatevpnHostnameChoices(servers)
case constants.Protonvpn:
servers := allServers.GetProtonvpn()
countryChoices = validation.ProtonvpnCountryChoices(servers)
regionChoices = validation.ProtonvpnRegionChoices(servers)
cityChoices = validation.ProtonvpnCityChoices(servers)
nameChoices = validation.ProtonvpnNameChoices(servers)
hostnameChoices = validation.ProtonvpnHostnameChoices(servers)
case constants.Purevpn:
servers := allServers.GetPurevpn()
countryChoices = validation.PurevpnCountryChoices(servers)
regionChoices = validation.PurevpnRegionChoices(servers)
cityChoices = validation.PurevpnCityChoices(servers)
hostnameChoices = validation.PurevpnHostnameChoices(servers)
case constants.Surfshark:
servers := allServers.GetSurfshark()
countryChoices = validation.SurfsharkCountryChoices(servers)
cityChoices = validation.SurfsharkCityChoices(servers)
hostnameChoices = validation.SurfsharkHostnameChoices(servers)
regionChoices = validation.SurfsharkRegionChoices(servers)
// TODO v4 remove
regionChoices = append(regionChoices, validation.SurfsharkRetroLocChoices()...)
if err := helpers.AreAllOneOf(ss.Regions, regionChoices); err != nil {
return nil, nil, nil, nil, nil, nil, fmt.Errorf("%w: %s", ErrRegionNotValid, err)
}
// Retro compatibility
// TODO remove in v4
*ss = surfsharkRetroRegion(*ss)
case constants.Torguard:
servers := allServers.GetTorguard()
countryChoices = validation.TorguardCountryChoices(servers)
cityChoices = validation.TorguardCityChoices(servers)
hostnameChoices = validation.TorguardHostnameChoices(servers)
case constants.VPNUnlimited:
servers := allServers.GetVPNUnlimited()
countryChoices = validation.VPNUnlimitedCountryChoices(servers)
cityChoices = validation.VPNUnlimitedCityChoices(servers)
hostnameChoices = validation.VPNUnlimitedHostnameChoices(servers)
case constants.Vyprvpn:
servers := allServers.GetVyprvpn()
regionChoices = validation.VyprvpnRegionChoices(servers)
case constants.Wevpn:
servers := allServers.GetWevpn()
cityChoices = validation.WevpnCityChoices(servers)
hostnameChoices = validation.WevpnHostnameChoices(servers)
case constants.Windscribe:
servers := allServers.GetWindscribe()
regionChoices = validation.WindscribeRegionChoices(servers)
cityChoices = validation.WindscribeCityChoices(servers)
hostnameChoices = validation.WindscribeHostnameChoices(servers)
default:
return nil, nil, nil, nil, nil, nil, fmt.Errorf("%w: %s", ErrVPNProviderNameNotValid, vpnServiceProvider)
}
return countryChoices, regionChoices, cityChoices,
ispChoices, nameChoices, hostnameChoices, 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,
countryChoices, regionChoices, cityChoices, ispChoices,
nameChoices, hostnameChoices []string) (err error) {
if err := helpers.AreAllOneOf(settings.Countries, countryChoices); err != nil {
return fmt.Errorf("%w: %s", ErrCountryNotValid, err)
}
if err := helpers.AreAllOneOf(settings.Regions, regionChoices); err != nil {
return fmt.Errorf("%w: %s", ErrRegionNotValid, err)
}
if err := helpers.AreAllOneOf(settings.Cities, cityChoices); err != nil {
return fmt.Errorf("%w: %s", ErrCityNotValid, err)
}
if err := helpers.AreAllOneOf(settings.ISPs, ispChoices); err != nil {
return fmt.Errorf("%w: %s", ErrISPNotValid, err)
}
if err := helpers.AreAllOneOf(settings.Hostnames, hostnameChoices); err != nil {
return fmt.Errorf("%w: %s", ErrHostnameNotValid, err)
}
if err := helpers.AreAllOneOf(settings.Names, nameChoices); 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, constants.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 == constants.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,155 @@
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
}
// 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(allServers models.AllServers) (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(allServers)
},
}
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,
allServers models.AllServers) (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.MergeWith(other.Pprof)
err = patchedSettings.Validate(allServers)
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: github.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)
})
}
}

View File

@@ -0,0 +1,68 @@
package settings
import (
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
"github.com/qdm12/ss-server/pkg/tcpudp"
)
// Shadowsocks contains settings to configure the Shadowsocks server.
type Shadowsocks struct {
// Enabled is true if the server should be running.
// It defaults to false, and cannot be nil in the internal state.
Enabled *bool
// Settings are settings for the TCP+UDP server.
tcpudp.Settings
}
func (s Shadowsocks) validate() (err error) {
return s.Settings.Validate()
}
func (s *Shadowsocks) copy() (copied Shadowsocks) {
return Shadowsocks{
Enabled: helpers.CopyBoolPtr(s.Enabled),
Settings: s.Settings.Copy(),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (s *Shadowsocks) mergeWith(other Shadowsocks) {
s.Enabled = helpers.MergeWithBool(s.Enabled, other.Enabled)
s.Settings.MergeWith(other.Settings)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (s *Shadowsocks) overrideWith(other Shadowsocks) {
s.Enabled = helpers.OverrideWithBool(s.Enabled, other.Enabled)
s.Settings.OverrideWith(other.Settings)
}
func (s *Shadowsocks) setDefaults() {
s.Enabled = helpers.DefaultBool(s.Enabled, false)
s.Settings.SetDefaults()
}
func (s Shadowsocks) String() string {
return s.toLinesNode().String()
}
func (s Shadowsocks) toLinesNode() (node *gotree.Node) {
node = gotree.New("Shadowsocks server settings:")
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(s.Enabled))
if !*s.Enabled {
return node
}
// TODO have ToLinesNode in qdm12/ss-server
node.Appendf("Listening address: %s", s.Address)
node.Appendf("Cipher: %s", s.CipherName)
node.Appendf("Password: %s", helpers.ObfuscatePassword(*s.Password))
node.Appendf("Log addresses: %s", helpers.BoolPtrToYesNo(s.LogAddresses))
return node
}

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